一、背景与目标

最近在学习 AI Infra 相关内容,前面已经在 Kubernetes 单节点上完成了 GPU Operator、Volcano vGPU 等组件的部署和验证。接下来想继续验证一个更接近实际业务的场景:在 Kubernetes 中部署一个大模型推理服务。

本文记录的是使用 vLLM 在 Kubernetes 单节点上部署 Qwen2.5-1.5B-Instruct 小模型的完整过程。本次实践的目标很明确:

1. 从 ModelScope 下载 Qwen2.5-1.5B-Instruct 模型
2. 使用 vLLM 加载本地模型目录
3. 先通过 nerdctl 在宿主机验证模型能否正常启动
4. 再通过 Kubernetes Deployment 部署 vLLM 服务
5. 使用 Volcano vGPU 资源调度 GPU
6. 通过 NodePort 暴露服务
7. 使用 OpenAI 兼容接口完成访问测试

本文不是讲大模型训练,而是从运维和 Kubernetes 部署角度,记录如何把一个小模型真正跑起来,并对外提供 HTTP 推理接口。


二、核心组件和部署思路

在正式部署前,先简单理解几个核心组件。

1. Hugging Face 和 ModelScope

Hugging Face 可以理解成 AI 模型领域的 GitHub。GitHub 主要托管代码,而 Hugging Face 主要托管模型权重、模型配置、tokenizer、数据集和模型说明文档。

本文使用的模型是:

Qwen/Qwen2.5-1.5B-Instruct

其中:

Qwen

表示组织或者用户。

Qwen2.5-1.5B-Instruct

表示具体模型名称。

ModelScope 中文叫魔搭社区,可以理解成国内常用的模型平台。因为国内访问 Hugging Face 有时比较慢,所以本次实践选择从 ModelScope 下载模型。

模型下载完成后,目录中通常会包含类似下面这些文件:

config.json
generation_config.json
tokenizer.json
tokenizer_config.json
model.safetensors

vLLM 启动时,本质上就是读取这些模型文件,把模型加载到 GPU 显存中,然后对外提供推理接口。

2. vLLM

vLLM 是一个大模型推理服务框架。如果从运维视角理解,它和部署普通后端服务有些类似:

镜像 + 启动参数 + 暴露端口 + 接收 HTTP 请求

部署 vLLM 时也是这个思路:

vLLM 镜像
挂载模型目录
启动 vLLM Server
监听 8000 端口
对外提供 OpenAI 兼容接口

vLLM 启动后可以提供 OpenAI 兼容接口,例如:

/v1/models
/v1/chat/completions
/v1/completions

本文主要验证两个接口:

/v1/models
/v1/chat/completions

3. 本文整体部署思路

本文采用本地模型目录挂载的方式。

宿主机模型路径:

/data/models/Qwen2.5-1.5B-Instruct

容器内模型路径:

/models/Qwen2.5-1.5B-Instruct

vLLM 启动参数:

--model /models/Qwen2.5-1.5B-Instruct

整体链路如下:

ModelScope 下载模型
        ↓
模型保存到宿主机 /data/models
        ↓
Pod 通过 hostPath 挂载模型目录
        ↓
vLLM 加载本地模型
        ↓
Volcano Scheduler 调度 vGPU 资源
        ↓
Service NodePort 暴露访问入口
        ↓
curl 调用 OpenAI 兼容接口

三、环境检查与模型准备

1. 环境说明

本文环境如下:

Kubernetes: v1.35.5
节点名称: master-01
节点 IP: 192.168.10.100
GPU: NVIDIA RTX 3070 Ti
GPU 显存: 8G
容器运行时: containerd / nerdctl
GPU 调度方式: Volcano vGPU
模型: Qwen2.5-1.5B-Instruct
vLLM 镜像: docker.m.daocloud.io/vllm/vllm-openai:latest
命名空间: llm-demo
服务端口: 8000
NodePort: 30088

2. 检查宿主机 GPU

先在宿主机上确认 GPU 是否正常:

nvidia-smi

如果能正常看到显卡信息、驱动版本和显存信息,说明宿主机层面的 NVIDIA Driver 基本正常。

3. 检查 Kubernetes 节点资源

继续查看 Kubernetes 节点是否识别到了 GPU 或 vGPU 资源:

kubectl describe node master-01 | grep -A 15 Capacity

我的环境输出如下:

Capacity:
  cpu:                     20
  ephemeral-storage:       982862268Ki
  hugepages-1Gi:           0
  hugepages-2Mi:           0
  memory:                  32521192Ki
  nvidia.com/gpu:          0
  pods:                    110
  volcano.sh/vgpu-cores:   100
  volcano.sh/vgpu-memory:  8192
  volcano.sh/vgpu-number:  10

这里需要注意:

nvidia.com/gpu: 0

并不代表宿主机没有 GPU,而是因为我这里使用的是 Volcano vGPU,GPU 资源被 Volcano vGPU Device Plugin 以扩展资源的形式暴露出来了。

重点看下面几个资源:

volcano.sh/vgpu-cores
volcano.sh/vgpu-memory
volcano.sh/vgpu-number

后面 Deployment 里申请的就是:

volcano.sh/vgpu-number: "1"

只要 Pod 申请了 Volcano vGPU 资源,就应该交给 Volcano Scheduler 调度,所以 Deployment 中需要配置:

schedulerName: volcano

4. 创建模型目录

在 Kubernetes 单节点宿主机上创建模型目录:

mkdir -p /data/models
chmod -R 755 /data/models

后面模型会下载到:

/data/models/Qwen2.5-1.5B-Instruct

5. 从 ModelScope 下载模型

安装 Git 和 Git LFS:

apt update
apt install -y git git-lfs
git lfs install

进入模型目录:

cd /data/models

下载模型:

git clone https://www.modelscope.cn/Qwen/Qwen2.5-1.5B-Instruct.git

下载完成后,检查模型文件:

ls -lh /data/models/Qwen2.5-1.5B-Instruct

如果能看到 config.jsontokenizer.jsonmodel.safetensors 等文件,说明模型已经下载完成。

这里需要注意,模型仓库使用 Git LFS 存储大文件,git clone 下载的不只是普通文本文件,还会下载模型权重文件,所以需要提前确认磁盘空间是否足够。


四、先用 nerdctl 验证 vLLM

在写 Kubernetes YAML 前,建议先在宿主机上使用 nerdctl 单独启动一次 vLLM。

这样可以先验证下面几件事:

1. 模型文件是否完整
2. vLLM 镜像是否可用
3. 容器能否正常使用 GPU
4. vLLM 能否成功加载模型
5. OpenAI 兼容接口是否能正常访问

1. 拉取 vLLM 镜像

vLLM 官方镜像是:

vllm/vllm-openai:latest

如果 Docker Hub 访问较慢,可以使用国内镜像源。本文实际使用的是:

docker.m.daocloud.io/vllm/vllm-openai:latest

2. 启动 vLLM 容器

执行下面命令:

sudo nerdctl run -d --name vllm-qwen \
  --gpus=all \
  --ipc=host \
  -p 8000:8000 \
  -v /data/models:/models \
  docker.m.daocloud.io/vllm/vllm-openai:latest \
  --model /models/Qwen2.5-1.5B-Instruct \
  --served-model-name qwen2.5-1.5b-instruct \
  --host 0.0.0.0 \
  --port 8000 \
  --dtype auto \
  --max-model-len 4096 \
  --gpu-memory-utilization 0.75

查看容器:

sudo nerdctl ps

查看日志:

sudo nerdctl logs -f vllm-qwen

3. 启动参数说明

几个关键参数说明如下。

--model /models/Qwen2.5-1.5B-Instruct

表示加载容器内的本地模型目录。

因为前面通过下面参数把宿主机目录挂载进了容器:

-v /data/models:/models

所以宿主机中的:

/data/models/Qwen2.5-1.5B-Instruct

在容器内就是:

/models/Qwen2.5-1.5B-Instruct

--served-model-name qwen2.5-1.5b-instruct

表示对外暴露的模型名称。

后面调用接口时,JSON 里的 model 字段就要写这个值:

"model": "qwen2.5-1.5b-instruct"

如果这里写错,接口调用时可能会报模型不存在。


--host 0.0.0.0

表示监听所有网卡。

如果只监听 127.0.0.1,外部机器或者 Kubernetes Service 可能无法访问。


--port 8000

表示 vLLM 服务监听 8000 端口。


--dtype auto

表示让 vLLM 自动选择合适的数据类型。


--max-model-len 4096

表示限制最大上下文长度。我的 GPU 显存只有 8G,所以没有一开始就使用更大的上下文长度,而是先限制为 4096,降低显存压力。


--gpu-memory-utilization 0.75

表示限制 vLLM 使用大约 75% 的 GPU 显存。

这不是 Kubernetes 的资源限制,而是 vLLM 内部控制 GPU 显存使用比例的参数。对于 8G 显存的小卡来说,建议一开始不要设置得太高,避免模型启动或者推理过程中出现 OOM。


--ipc=host

表示让容器使用宿主机的 IPC namespace。

简单理解,就是让容器可以使用宿主机的共享内存空间,避免容器默认 /dev/shm 太小。vLLM 底层依赖 PyTorch,在某些场景下会使用共享内存,所以本地容器启动时经常会使用这个参数。

在 Kubernetes 中,可以使用:

hostIPC: true

来接近 --ipc=host 的效果。

不过 hostIPC: true 会让 Pod 共享宿主机 IPC namespace,测试环境可以使用,生产环境需要结合安全要求评估。

4. 本地接口测试

测试模型列表接口:

curl http://127.0.0.1:8000/v1/models

再测试聊天接口:

curl -X POST http://127.0.0.1:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen2.5-1.5b-instruct",
    "messages": [
      {
        "role": "user",
        "content": "你好,请用一句话介绍一下 Kubernetes。"
      }
    ],
    "max_tokens": 128,
    "temperature": 0.7
  }'

我的测试返回如下:

{
  "id": "chatcmpl-ae844c446287d6db",
  "object": "chat.completion",
  "created": 1781142182,
  "model": "qwen2.5-1.5b-instruct",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Kubernetes(又称为k8s)是一种开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。",
        "refusal": null,
        "annotations": null,
        "audio": null,
        "function_call": null,
        "tool_calls": [],
        "reasoning": null
      },
      "logprobs": null,
      "finish_reason": "stop",
      "stop_reason": null,
      "token_ids": null,
      "routed_experts": null
    }
  ],
  "service_tier": null,
  "system_fingerprint": "vllm-0.22.1-e292cdd8",
  "usage": {
    "prompt_tokens": 36,
    "total_tokens": 65,
    "completion_tokens": 29,
    "prompt_tokens_details": null
  },
  "prompt_logprobs": null,
  "prompt_token_ids": null,
  "prompt_text": null,
  "kv_transfer_params": null
}

如果本地 nerdctl 方式能成功,说明模型、镜像、GPU 和 vLLM 参数基本没有问题,接下来再部署到 Kubernetes。


五、部署到 Kubernetes

1. 创建命名空间

创建一个单独的命名空间:

kubectl create namespace llm-demo

2. 编写 Deployment

创建文件:

vim vllm-qwen-deployment.yaml

内容如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-qwen
  namespace: llm-demo
  labels:
    app: vllm-qwen
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-qwen
  template:
    metadata:
      labels:
        app: vllm-qwen
      annotations:
        volcano.sh/vgpu-mode: "hami-core"
    spec:
      schedulerName: volcano
      hostIPC: true
      containers:
        - name: vllm
          image: docker.m.daocloud.io/vllm/vllm-openai:latest
          imagePullPolicy: IfNotPresent
          args:
            - "--model"
            - "/models/Qwen2.5-1.5B-Instruct"
            - "--served-model-name"
            - "qwen2.5-1.5b-instruct"
            - "--host"
            - "0.0.0.0"
            - "--port"
            - "8000"
            - "--dtype"
            - "auto"
            - "--max-model-len"
            - "4096"
            - "--gpu-memory-utilization"
            - "0.75"
            - "--max-num-seqs"
            - "8"

          ports:
            - name: http
              containerPort: 8000
              protocol: TCP

          resources:
            requests:
              cpu: "2"
              memory: "8Gi"
            limits:
              cpu: "4"
              memory: "16Gi"
              volcano.sh/vgpu-number: "1"
              # 如果希望显式限制 vGPU 显存和算力,可以根据环境增加下面两个字段
              # volcano.sh/vgpu-memory: "8192"
              # volcano.sh/vgpu-cores: "100"

          volumeMounts:
            - name: model-dir
              mountPath: /models
              readOnly: true

          startupProbe:
            httpGet:
              path: /v1/models
              port: 8000
            initialDelaySeconds: 60
            periodSeconds: 10
            failureThreshold: 60
            timeoutSeconds: 5

          readinessProbe:
            httpGet:
              path: /v1/models
              port: 8000
            periodSeconds: 10
            failureThreshold: 3
            timeoutSeconds: 5

          livenessProbe:
            httpGet:
              path: /v1/models
              port: 8000
            initialDelaySeconds: 120
            periodSeconds: 30
            failureThreshold: 3
            timeoutSeconds: 5

      volumes:
        - name: model-dir
          hostPath:
            path: /data/models
            type: Directory

这个 Deployment 里有几个重点。

第一个是:

schedulerName: volcano

因为这里使用的是 Volcano vGPU 资源:

volcano.sh/vgpu-number: "1"

所以 Pod 需要交给 Volcano Scheduler 调度。

第二个是:

hostIPC: true

它对应本地 nerdctl 启动时的:

--ipc=host

用于解决容器默认共享内存较小的问题。

第三个是:

hostPath:
  path: /data/models

它会把宿主机的 /data/models 挂载到容器内的 /models,这样 vLLM 就可以读取本地模型文件。

第四个是 startupProbe、readinessProbe 和 livenessProbe。

大模型服务启动通常比较慢,因为需要加载模型权重到 GPU。如果只配置 livenessProbe,可能会出现模型还没加载完成,容器就被 Kubernetes 判定为异常并重启的问题。

所以这里增加了 startupProbe,给 vLLM 更长的启动时间。

3. 编写 Service

创建文件:

vim vllm-qwen-svc.yaml

内容如下:

apiVersion: v1
kind: Service
metadata:
  name: vllm-qwen
  namespace: llm-demo
  labels:
    app: vllm-qwen
spec:
  type: NodePort
  selector:
    app: vllm-qwen
  ports:
    - name: http
      port: 8000
      targetPort: 8000
      nodePort: 30088
      protocol: TCP

这里使用 NodePort 暴露服务。

访问地址格式是:

http://节点IP:30088

我的节点 IP 是:

192.168.10.100

所以访问地址就是:

http://192.168.10.100:30088

4. 应用 YAML

查看 Pod:

kubectl -n llm-demo get pod

输出如下:

NAME                        READY   STATUS    RESTARTS   AGE
vllm-qwen-d8877997d-tzrts   1/1     Running   0          96s

查看 Service:

kubectl -n llm-demo get svc

输出如下:

NAME        TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
vllm-qwen   NodePort   10.96.30.109   <none>        8000:30088/TCP   43s

查看日志:

kubectl -n llm-demo logs -f deploy/vllm-qwen

如果 Pod 状态是 Running,并且日志中没有模型加载失败、CUDA 报错、显存不足等问题,说明 vLLM 已经在 Kubernetes 中正常启动。


六、接口访问测试

1. 查看模型列表

在 Kubernetes 节点上执行:

curl http://127.0.0.1:30088/v1/models

我的返回如下:

{
  "object": "list",
  "data": [
    {
      "id": "qwen2.5-1.5b-instruct",
      "object": "model",
      "created": 1781490791,
      "owned_by": "vllm",
      "root": "/models/Qwen2.5-1.5B-Instruct",
      "parent": null,
      "max_model_len": 4096,
      "permission": [
        {
          "id": "modelperm-9d2e7d49c59d5ec8",
          "object": "model_permission",
          "created": 1781490791,
          "allow_create_engine": false,
          "allow_sampling": true,
          "allow_logprobs": true,
          "allow_search_indices": false,
          "allow_view": true,
          "allow_fine_tuning": false,
          "organization": "*",
          "group": null,
          "is_blocking": false
        }
      ]
    }
  ]
}

这里重点看两个字段。

第一个是模型 ID:

"id": "qwen2.5-1.5b-instruct"

这说明 vLLM 对外暴露的模型名是:

qwen2.5-1.5b-instruct

第二个是模型根路径:

"root": "/models/Qwen2.5-1.5B-Instruct"

这说明 vLLM 加载的是容器内的本地模型目录。

从其他能访问该节点的机器上,也可以执行:

curl http://192.168.10.100:30088/v1/models

2. 测试聊天接口

执行下面命令:

curl -X POST http://192.168.10.100:30088/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen2.5-1.5b-instruct",
    "messages": [
      {
        "role": "user",
        "content": "请用通俗的话解释一下什么是 Kubernetes Deployment。"
      }
    ],
    "max_tokens": 256,
    "temperature": 0.7
  }'

我的测试返回如下:

{
  "id": "chatcmpl-bc7fb7cc01cac533",
  "object": "chat.completion",
  "created": 1781490905,
  "model": "qwen2.5-1.5b-instruct",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Kubernetes Deployment是一种资源模型,它允许你定义和管理一组Pods(容器实例)。Deployment的主要目的是提供一个自动化的机制来部署、升级和删除应用的副本。\n\n当你使用Deployment时,你可以指定应用的基本配置,如版本号、期望的副本数量以及如何启动和停止这些副本。然后,Kubernetes会根据这个配置自动创建和管理相应的Pods,并确保它们按照预期的方式运行。\n\n具体来说,Deployment包括以下几个关键部分:\n\n1. **期望状态**:这是你希望在你的系统中看到的状态,例如“running”或“replicaSetRunning”。这决定了你需要多少个副本。\n\n2. **更新策略**:这是一个策略,告诉你当需要更新应用程序时应该怎么做。有几种不同的策略可以使用,比如“rollingUpdate”,这意味着每次更新都会从旧的副本中逐渐替换掉新的副本,直到达到目标数量。\n\n3. **Replicas**:这表示你想要有多少个副本同时运行。如果你设置为5,那么就会至少有5个副本同时运行。\n\n4. **ImagePullSecrets**:这是用来存储镜像仓库凭证的地方。这样,Kubernetes就能安全地从远程仓库拉取镜像。\n\n总的来说,Kubernetes Deployment是帮助你在",
        "refusal": null,
        "annotations": null,
        "audio": null,
        "function_call": null,
        "tool_calls": [],
        "reasoning": null
      },
      "logprobs": null,
      "finish_reason": "length",
      "stop_reason": null,
      "token_ids": null,
      "routed_experts": null
    }
  ],
  "service_tier": null,
  "system_fingerprint": "vllm-0.22.1-2d8ded77",
  "usage": {
    "prompt_tokens": 39,
    "total_tokens": 295,
    "completion_tokens": 256,
    "prompt_tokens_details": null
  },
  "prompt_logprobs": null,
  "prompt_token_ids": null,
  "prompt_text": null,
  "kv_transfer_params": null
}

这里可以看到:

"model": "qwen2.5-1.5b-instruct"

说明请求已经正确路由到了我们部署的模型。同时:

"finish_reason": "length"

表示这次输出是因为达到了 max_tokens: 256 的限制而停止的。

如果希望回答更完整,可以适当调大:

"max_tokens": 512

但是在 8G 显存环境中,不建议一开始把参数设置得太激进。


七、常见问题排查

1. /v1/models 正常,但聊天接口报模型不存在

重点检查请求里的 model 字段是否和 vLLM 启动参数一致。

启动参数是:

--served-model-name qwen2.5-1.5b-instruct

那么请求里就必须写:

"model": "qwen2.5-1.5b-instruct"

不要写成:

"model": "Qwen2.5-1.5B-Instruct"

也不要写成其他模型名。

2. Pod 一直 Pending

先查看 Pod 事件:

kubectl -n llm-demo describe pod <pod-name>

重点检查:

1. 是否写了 schedulerName: volcano
2. Volcano Scheduler 是否正常运行
3. 节点是否有 volcano.sh/vgpu-number 资源
4. 申请的 CPU、内存、vGPU 是否超过节点可用资源

如果申请了 Volcano vGPU 资源,却没有写:

schedulerName: volcano

就容易出现调度异常。

3. Pod 启动后 OOM 或显存不足

如果日志里出现 CUDA OOM 或者显存不足,可以优先降低下面几个参数:

--max-model-len 4096
--gpu-memory-utilization 0.75
--max-num-seqs 8

例如可以尝试改成:

--max-model-len 2048
--gpu-memory-utilization 0.65
--max-num-seqs 4

对于 8G 显存的小卡来说,先保证服务能稳定启动,比一开始追求高并发更重要。

4. 模型目录挂载错误

如果日志提示找不到模型文件,先检查宿主机目录:

ls -lh /data/models/Qwen2.5-1.5B-Instruct

再进入 Pod 检查容器内目录:

kubectl -n llm-demo exec -it deploy/vllm-qwen -- bash
ls -lh /models/Qwen2.5-1.5B-Instruct

如果容器内没有模型文件,说明 hostPath 挂载路径可能写错了。


八、总结

本文完成了一个从模型下载到 Kubernetes 部署的完整小模型推理服务实践。

整体过程如下:

ModelScope 下载 Qwen2.5-1.5B-Instruct
        ↓
模型保存到宿主机 /data/models
        ↓
nerdctl 单独启动 vLLM 进行验证
        ↓
Kubernetes Pod 通过 hostPath 挂载模型目录
        ↓
vLLM 加载本地模型
        ↓
Volcano Scheduler 调度 vGPU 资源
        ↓
Service NodePort 暴露 30088 端口
        ↓
通过 OpenAI 兼容接口调用模型

这次实践有几个关键点:

第一,vLLM 本质上是一个大模型推理服务框架,部署方式和普通后端服务很像,都是镜像、启动参数、端口和 HTTP API。

第二,模型可以提前下载到宿主机,再通过 hostPath 挂载到容器中。这种方式适合单节点实验环境,简单直接。

第三,如果使用 Volcano vGPU 资源,Deployment 中不仅要申请 volcano.sh/vgpu-number,还应该配置 schedulerName: volcano,让 Pod 交给 Volcano Scheduler 调度。

第四,小显存 GPU 部署模型时,--max-model-len--gpu-memory-utilization--max-num-seqs 这些参数很关键。对于 8G 显存环境,建议先保守配置,保证服务能稳定启动。

第五,vLLM 提供 OpenAI 兼容接口,后续可以继续接入 Prometheus、Grafana、LiteLLM、KServe 或 AI Gateway,逐步往更完整的 AI Infra 平台方向演进。

Logo

免费领 200 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐