使用 vLLM 在 Kubernetes 单节点部署 Qwen2.5 小模型实践
一、背景与目标最近在学习 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 和 ModelScopeHugging 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.safetensorsvLLM 启动时本质上就是读取这些模型文件把模型加载到 GPU 显存中然后对外提供推理接口。2. vLLMvLLM 是一个大模型推理服务框架。如果从运维视角理解它和部署普通后端服务有些类似镜像 启动参数 暴露端口 接收 HTTP 请求部署 vLLM 时也是这个思路vLLM 镜像 挂载模型目录 启动 vLLM Server 监听 8000 端口 对外提供 OpenAI 兼容接口vLLM 启动后可以提供 OpenAI 兼容接口例如/v1/models /v1/chat/completions /v1/completions本文主要验证两个接口/v1/models /v1/chat/completions3. 本文整体部署思路本文采用本地模型目录挂载的方式。宿主机模型路径/data/models/Qwen2.5-1.5B-Instruct容器内模型路径/models/Qwen2.5-1.5B-InstructvLLM 启动参数--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: 300882. 检查宿主机 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 vGPUGPU 资源被 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: volcano4. 创建模型目录在 Kubernetes 单节点宿主机上创建模型目录mkdir -p /data/models chmod -R 755 /data/models后面模型会下载到/data/models/Qwen2.5-1.5B-Instruct5. 从 ModelScope 下载模型安装 Git 和 Git LFSapt 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.json、tokenizer.json、model.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:latest2. 启动 vLLM 容器执行下面命令sudo nerdctl run -d --name vllm-qwen \ --gpusall \ --ipchost \ -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-qwen3. 启动参数说明几个关键参数说明如下。--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。--ipchost表示让容器使用宿主机的 IPC namespace。简单理解就是让容器可以使用宿主机的共享内存空间避免容器默认/dev/shm太小。vLLM 底层依赖 PyTorch在某些场景下会使用共享内存所以本地容器启动时经常会使用这个参数。在 Kubernetes 中可以使用hostIPC: true来接近--ipchost的效果。不过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。五、部署到 Kubernetes1. 创建命名空间创建一个单独的命名空间kubectl create namespace llm-demo2. 编写 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 启动时的--ipchost用于解决容器默认共享内存较小的问题。第三个是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:300884. 应用 YAML查看 Podkubectl -n llm-demo get pod输出如下NAME READY STATUS RESTARTS AGE vllm-qwen-d8877997d-tzrts 1/1 Running 0 96s查看 Servicekubectl -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 } ] } ] }这里重点看两个字段。第一个是模型 IDid: 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/models2. 测试聊天接口执行下面命令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 平台方向演进。