news 2026/10/22 13:35:43

vLLM+K8s实战:GPU弹性调度与推理服务稳定性治理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
vLLM+K8s实战:GPU弹性调度与推理服务稳定性治理

做了一年多大模型推理,我用 vLLM 搭过单机服务,也踩过 K8s 的坑,最后总算跑通了一套能弹性部署的 GPU 调度方案。今天这篇东西,就是围绕 vLLM + K8s 的一次实战复盘,核心围绕大模型推理的弹性部署、GPU 调度、自动伸缩与稳定性治理,适合那些已经在本地跑过 vLLM、想上集群却不知道从哪动手的读者,也适合集群运维看了之后能直接和算法组对上线话。

先给个总览:我会从为什么必须上 K8s 讲起,然后是集群环境准备、vLLM 服务设计、GPU 调度细节、弹性伸缩实现,再到故障排查和性能调优。过程中会给出具体的 Yaml、命令、参数计算逻辑和已经踩过的坑,尽量做到可复制。

1. 为什么大模型推理非用 vLLM + K8s 不可

1.1 单节点玩得转,流量一来就崩

本地单卡跑 vLLM 很舒服,一个进程起来,测测生成速度,看看显存占用,一切都很美好。但一旦上了生产,问题立刻变多:多个模型需要同时服务,模型版本要更新迭代,流量高峰集中在某些时段,GPU 资源紧张但每个节点又都有剩余碎片。这个时候单机进程的管理方式完全扛不住。

我之前做过一个比较典型的业务:内部一个智能客服场景,用了大概 7B 级别的开源对话模型。白天上班时间并发可能就几个 QPS,但晚上系统离线任务跑批,或者市场活动一上来,请求量能瞬间涨到几十倍。如果用一台固定配额的 GPU 节点去扛,要么平时浪费,要么高峰时排队到用户骂娘。这就是弹性部署最直接的需求来源——你不可能为峰值流量一直养着一批闲置 GPU,也不可能在高负载时手动去扩容 Pod。

1.2 vLLM 和 K8s 组合到底解决了什么

vLLM 本身解决的是推理引擎层面的吞吐和显存效率,它用 PagedAttention 把 KV Cache 切成块,显存利用率比传统方案高很多。但 vLLM 只是个单机进程,不具备集群调度能力。K8s 补齐的是资源编排层:把 GPU 当成一种可声明、可调度的资源,让多个推理服务共享一个集群,按需启动、自动扩容、故障重启、滚动发布。

两者结合后,你得到的是三层能力:

  • 资源池化:多个 GPU 节点组成一个池子,某个服务要用 1 张卡、2 张卡还是 4 张卡,由调度器统一分配。
  • 弹性伸缩:根据请求量、GPU 利用率、队列长度等指标,自动调整副本数量,高峰扩容、低谷缩容。
  • 故障自愈:Pod 崩溃、节点宕机、GPU 异常时,K8s 自动重新调度,不需要半夜爬起来手动重启。

如果只是做 demo,完全没必要上 K8s。但一旦涉及多模型、多人协作、夜间自动扩缩容,这套组合的收益是实打实的。

1.3 适合读这篇文章的人

我把读者分成三类:第一类是算法工程师,vLLM 会在你手上跑起来,但你不太懂 K8s;第二类是平台或运维工程师,K8s 很熟,但没处理过 GPU 资源和大模型推理负载;第三类是架构师,需要把整套推理平台搭起来给多个业务线复用。这篇文章的内容会让第一类理解为什么要加层调度,让第二类避开 GPU 调度的经典陷阱,让第三类直接拿去做方案评审。

2. 集群环境准备:GPU 驱动、插件与调度前提

2.1 物理或虚拟集群的最低配置要求

先不要急着写 Yaml,GPU 节点能不能被 K8s 识别是一个前置条件。以单节点测试为例,至少需要一块支持 CUDA 的显卡,显存建议 24GB 起。如果是 7B 模型 FP16,单卡 16GB 勉强能跑,但加上 KV Cache 和 CUDA context 就非常紧张;13B 模型 FP16 大约需要 26GB 权重,单卡基本上得 40GB 或以上,或者用双卡 Tensor Parallel。生产环境建议直接把单节点内存做到 256GB 以上,因为加载模型、处理 prompt 时的 CPU 预处理和 tokenizer 也会占内存,更不用说数据预处理服务了。

K8s 集群本身不用太大,一个控制平面节点加三到五个 GPU 节点就够了。GPU 节点需要安装好 NVIDIA 驱动、CUDA 工具链(运行时版本能够被容器内依赖覆盖就行),然后装上 kubelet、container runtime,再注册到集群。有一个点容易被忽略:GPU 节点上不要同时跑太多 CPU 密集型的 Pod,否则会影响推理延迟。

2.2 安装 NVIDIA Device Plugin:让 GPU 变成可声明资源

K8s 原生不认识 GPU,需要通过 NVIDIA Device Plugin 把 nvidia.com/gpu 作为一种 Extended Resource 上报给 API Server。装上之后,你才可以在 Pod 的 resources 里写 nvidia.com/gpu: 1,调度器才知道往哪个节点放。

安装流程通常是这样:先确认节点上 nvidia-smi 可以正常输出,然后以 DaemonSet 方式部署 device plugin。部署 Yaml 大致如下:

apiVersion: apps/v1 kind: DaemonSet metadata: name: nvidia-device-plugin-daemonset namespace: kube-system spec: selector: matchLabels: name: nvidia-device-plugin-ds template: metadata: labels: name: nvidia-device-plugin-ds spec: tolerations: - operator: Exists effect: NoSchedule priorityClassName: system-node-critical containers: - image: nvcr.io/nvidia/k8s-device-plugin:v0.14.5 name: nvidia-device-plugin env: - name: NVIDIA_VISIBLE_DEVICES value: all - name: NVIDIA_DRIVER_CAPABILITIES value: compute,utility volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: nvidia-driver mountPath: /usr/local/nvidia readOnly: true volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: nvidia-driver hostPath: path: /usr/lib/x86_64-linux-gnu/nvidia

这里有个容易踩的坑:如果节点上驱动不是装在默认路径,hostPath 要改成你实际的驱动目录,否则容器里找不到 libcuda.so。另外旧版本的 device plugin 和使用 nvidia-container-toolkit 的新版本有一些差异,建议直接用官方当前稳定版,别迷信过时的博客配置。

装好之后,可以通过以下命令验证:

kubectl get node -o json | jq '.items[].status.capacity'

输出里应该能看到类似于"nvidia.com/gpu": "1"这样的资源量。如果 Node 显示的是 0,大概率是 device plugin 没有正常运行,或者驱动和容器运行时没匹配上。

2.3 节点池设计与 GPU 资源标签

生产集群不建议把 V100、A10、A100 这类不同型号的卡混在一个节点池里,因为调度器只能按nvidia.com/gpu这个数字资源去调度,无法感知显存大小、算力高低。如果你混用了 16GB 的卡和 80GB 的卡,调度器可能把一个需要 40GB 显存的模型调度到 16GB 的节点上,然后直接 OOM。

解决方法是给节点打标签,按 GPU 型号或显存容量区分节点池:

kubectl label node worker-gpu-01 gpu-type=a100 kubectl label node worker-gpu-02 gpu-type=a10

然后在 Deployment 的 nodeSelector 里指定模型适用的节点类型:

spec: nodeSelector: gpu-type: a100

这个看似多余的操作,能避免掉 80% 的显存 OOM 问题。更进一步,可以结合节点亲和性硬性要求,防止某些不支持多卡的机型被调度器塞进 Tensor Parallel 任务。

3. vLLM 推理服务镜像与 Deployment 设计

3.1 镜像构建:CUDA、vLLM、对应的模型文件

vLLM 官方提供了镜像,但生产环境我更推荐自己构建:一是因为模型文件要预下载或挂载,二是你需要控制 Python 依赖和 CUDA 版本,三是镜像里可能还要塞一些自研的鉴权、日志、监控组件。

一个最小可用的 Dockerfile 可以这样写:

FROM nvcr.io/nvidia/pytorch:24.01-py3 RUN pip install vllm RUN pip install prometheus-client WORKDIR /workspace COPY entrypoint.sh /workspace/entrypoint.sh RUN chmod +x /workspace/entrypoint.sh ENTRYPOINT ["/workspace/entrypoint.sh"]

entrypoint.sh 里主要是启动 vLLM server 的命令,我一般这么写:

exec python -m vllm.entrypoints.openai.api_server \ --model /models/my-7b \ --served-model-name my-7b \ --tensor-parallel-size ${TP_SIZE:-1} \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --port 8000

注意模型路径我挂载的是 /models/my-7b,不建议把模型打进镜像,这样镜像会膨胀到几十个 GB,每次发布拖镜像拖到怀疑人生。更好的做法是模型文件放在共享存储,比如 NFS、CephFS,或者每个节点本地磁盘预下载。如果只是单机多副本,也可以先在一台节点上 initContainer 负责下载模型,再用 emptyDir 共享给主容器。

3.2 Deployment 核心 Yaml:资源声明与探针

挂载 GPU 资源时,requests 和 limits 建议显式设置成一样。GPU 资源不能超卖,写成不同值反而容易让调度器产生误判。下面是经过实践验证的 Deployment 片段:

apiVersion: apps/v1 kind: Deployment metadata: name: vllm-7b-server namespace: inference spec: replicas: 1 selector: matchLabels: app: vllm-7b template: metadata: labels: app: vllm-7b spec: nodeSelector: gpu-type: a100 containers: - name: vllm image: registry.example.com/inference/vllm-7b:latest command: ["/bin/bash", "-c"] args: - | python -m vllm.entrypoints.openai.api_server \ --model /models/my-7b \ --served-model-name my-7b \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --port 8000 ports: - containerPort: 8000 name: http resources: requests: cpu: "8" memory: "32Gi" nvidia.com/gpu: "1" limits: cpu: "8" memory: "32Gi" nvidia.com/gpu: "1" readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 300 periodSeconds: 30 timeoutSeconds: 5 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 300 periodSeconds: 30 timeoutSeconds: 5

这里有一个常见的讨论点:为什么要等 300 秒才开始探活?因为 vLLM 启动时要把模型权重加载到显存,7B 模型在磁盘读取速度一般的情况下可能需要一两分钟,如果初始延迟设太短,kubelet 会连续杀掉重新创建,导致集群出现“CrashLoopBackOff”的假象。

最佳实践是结合一个 StartupProbe,它的initialDelaySeconds可以从容设置,等模型加载完成进入 ready 后再用 readinessProbe 控制流量。下面这段是我后来改的:

startupProbe: httpGet: path: /health port: 8000 failureThreshold: 60 periodSeconds: 10 readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 0 periodSeconds: 10 livenessProbe: httpGet: path: /health port: 8000 periodSeconds: 30

StartupProbe 会持续最多 600 秒,足够加载一个 7B 模型。等它成功后,readiness 和 liveness 才开始生效。

3.3 Service 和 Ingress:外部流量怎么进来

推理服务通常需要协议兼容 HTTP,vLLM 自带 OpenAI 风格接口,所以直接暴露一个 ClusterIP Service 就行:

apiVersion: v1 kind: Service metadata: name: vllm-7b-service namespace: inference spec: selector: app: vllm-7b ports: - port: 8000 targetPort: http

如果想在集群外部调用,再用 Ingress 把/v1/chat/completions路由到该 Service。注意 Ingress 的 body 大小默认可能有限制,大模型请求体可能包含很长的 prompt,有些 ingress-controller 如果你不调大 proxy-body-size,会直接 413。这是我在生产环境实际遇到过的问题,尤其是 Nginx Ingress,默认只能接受 1MB 的 body,而一个几万字的长文档 prompt 可能就有几百 KB 到数 MB,调整配置是必须的。

4. GPU 调度实战:从单卡独占到多卡推理

4.1 单卡独占、显存预留和 GPU 利用率

在 K8s 里,声明nvidia.com/gpu: 1默认就是整卡独占。它是一种非常“暴力”的调度方式:Pod 拿到一张卡,整张卡对 Pod 完全可见,Pod 就算只用 5% 的显存,别的 Pod 也无法占用。

对于大模型推理来说,独占在安全性和隔离性上是必要的,尤其是你没有采用显存分片技术时。但独占也带来了资源浪费。以 7B 模型 FP16 为例,权重占约 14GB,如果显卡是 80GB 的 A100,剩下 65GB 全都被 KV Cache 和预留给 vLLM 的 buffer 吃掉。实际上你可以通过--gpu-memory-utilization来控制 vLLM 占用的显存上限,比如 0.9。但别的推理服务依旧不能用这张卡的剩余空间。

如果想让多个模型实例共用一张物理卡,常见方案有三种:NVIDIA MIG(仅支持部分型号)、vLLM 多实例在同一进程内服务多个模型、以及基于时间片的 GPU Sharing 调度器。MIG 配置比较复杂,而且不同卡支持粒度不同;同进程多模型又容易互相影响稳定性。在我的经验里,早期先别追求共享,老老实实一对一把服务跑稳,等流量提升、成本压力变大后再做显存共享规划。

4.2 多卡推理:Tensor Parallel 和 Pipeline Parallel

当模型超过单卡显存时,就需要多卡并行。vLLM 支持 Tensor Parallel(TP),通过--tensor-parallel-size N指定,通常要求 N 张卡在同一个节点上,并且节点内卡间通信是 NVLink 或高速 PCIe,不然性能会很难看。

TP 的显存计算可以用一个粗粒度公式估算:

模型权重显存 = 参数量 × 字节数

  • 7B FP16:约 14GB
  • 13B FP16:约 26GB
  • 70B FP16:约 140GB

如果单卡 40GB,跑 70B 模型至少需要 4 张卡才能放下权重,加上 KV Cache,可能还需要 8 张卡。部署时 Pod 里要写:

resources: limits: nvidia.com/gpu: "8" args: - --tensor-parallel-size 8

注意,K8s 调度器只会保证给你 8 张 GPU,但不保证这 8 张卡在同一节点,更不保证它们之间是高带宽互联。如果节点数很多,而且没有拓扑约束,调度器可能把 8 张卡分布在两个节点上,Pod 启动后 vLLM 会直接报错或性能惨不忍睹。

解决办法是给 Deployment 加上pod affinity或 nodeName 固定,或者使用节点级拓扑调度特性。比较实用的方案是给 GPU 节点打上 unique-label,比如node.kubernetes.io/gpu-group: "a100-8",再用 nodeSelector 保底,配合节点亲和性把同组节点限制住。

实际上一开始我们做 13B 双卡部署时,就遇到过因为两个 GPU 分在不同 PCIe switch,结果张量并行通信延迟过高,每秒生成 token 数量从 70 掉到 20 的惨状。所以多卡推理前,必须搞清楚节点上的 GPU 拓扑。

4.3 GPU 调度策略:binpack 和 spread 的选择

K8s 默认调度器对 GPU 资源采用类似 spread 的策略,尽量分散放置 Pod,这在大模型推理场景下并不总是好事。举个例子:你有一个 4 节点的集群,每个节点 8 张卡,现有 8 个 7B 推理 Pod。默认调度可能每个节点放 2 个 Pod,这样每个节点剩余的资源都是碎片化的;如果你逐渐扩容到 16 个 Pod,会发现没有一个节点能放下需要 8 卡的大模型任务。

为了让 GPU 尽量集中在更少的节点上,可以启用调度器的NodeResourcesFit中的MostAllocated策略,或者通过自定义调度器插件来做 binpack。社区里比较常见的是给调度器增加nodeResourceFit的 scoringStrategy,让 score 偏向已分配资源高的节点。不过这个改动需要自定义 kube-scheduler 配置,有一定运维成本。

如果你没有能力改调度器,也可以用 group 的 podAffinity 或反亲和,手动控制 Pod 分布。我建议在早期直接把 GPU 节点分成两组:一组跑在线小模型,一组跑离线大模型,通过 nodeSelector 分开,简化调度问题。

5. 弹性伸缩:让推理服务跟着流量走

5.1 为什么不能只用 CPU 或内存指标

传统 Web 服务的 HPA 常用 CPU 利用率作为伸缩指标,但推理服务完全不一样:CPU 利用率高不代表 推理负载高,GPU 利用率也不能直接代表“请求处理能力”。比如请求很短,GPU 利用率可能只有 20%,但 RPS 已经很高;如果请求都是长文本生成,GPU 利用率会冲到 90% 以上,但 QPS 其实很低。

所以要想实现科学的弹性伸缩,最好基于 vLLM 自身的指标,比如:

  • 正在排队的请求数
  • 当前正在生成的请求数
  • KV 缓存使用率
  • 平均每请求生成速度/token 数
  • RPS(每秒完成请求数)

vLLM 已经提供了 Prometheus 指标端点,默认在http://127.0.0.1:8000/metrics,里面包含大量有用指标,例如vllm:num_requests_waiting、vllm:num_requests_running、vllm:num_requests_swapped等。

5.2 用 Prometheus + Kubernetes HPA 实现自定义指标伸缩

整体架构是:vLLM 暴露 metrics -> Prometheus 采集 -> Prometheus Adapter 暴露自定义指标 -> HPA 根据指标调整副本数。

第一步,需要部署 Prometheus,并在 ServiceMonitor 或类似机制中抓取 vLLM 指标。如果你不用 ServiceMonitor,可以给 Pod 添加 prometheus.io/scrape: "true" 注解。

第二步,部署 Prometheus Adapter。它的核心是配置一个 seriesQuery,把 vLLM 指标翻译成 K8s 的自定义指标。

假设我们要根据vllm:num_requests_waiting(等待队列长度)做伸缩,当等待请求数超过 10 时扩容。Adapter 配置可以这样:

rules: - seriesQuery: 'vllm:num_requests_waiting{namespace!="",pod!=""}' resources: overrides: namespace: {resource: "namespace"} pod: {resource: "pod"} name: matches: "vllm:(.*)" as: "vllm_requests_waiting" metricsQuery: 'avg(vllm:num_requests_waiting{namespace!="",pod!=""}) by (namespace, pod)'

第三步,编写 HPA:

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: vllm-7b-hpa namespace: inference spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: vllm-7b-server minReplicas: 1 maxReplicas: 4 behavior: scaleDown: stabilizationWindowSeconds: 300 metrics: - type: Pods pods: metric: name: vllm_requests_waiting target: type: AverageValue averageValue: 10

注意behavior.scaleDown.stabilizationWindowSeconds很重要。推理 Pod 缩容时,如果模型正在处理长请求,直接 kill 会中断客户端连接。设置一个稳定窗口可以避免流量抖动时频繁缩容。更好的是配合preStop:在 Pod 被删除前,先调用 vLLM 的优雅退出接口,等当前请求完成再退出。

vLLM 本身支持 graceful shutdown,但 K8s 默认给 Pod 发 SIGTERM 后,只有 terminationGracePeriodSeconds 时间。这个时间默认 30 秒,对于长文本生成可能不够。建议将terminationGracePeriodSeconds调大到 300 秒,同时在容器中捕获 SIGTERM 并触发 vLLM 的 shutdown,这是我在生产中用过的最稳的组合。

5.3 基于 KEDA 的队列伸缩:适合离线异步推理

如果你的推理服务是离线批处理,比如批量对文档生成摘要,请求会先落到一个消息队列里,再用 vLLM 消费。这时候可以用 KEDA 监听队列长度,动态调整 consumer 副本。KEDA 相比 HPA 更像是一个事件驱动的伸缩器。

一个 Kafka 或 RabbitMQ 触发的 ScaledObject 配置大概是:

apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: vllm-batch-scaledobject spec: scaleTargetRef: name: vllm-batch-worker pollingInterval: 15 cooldownPeriod: 120 minReplicaCount: 0 maxReplicaCount: 10 triggers: - type: kafka metadata: topic: inference-tasks bootstrapServers: kafka-broker:9092 consumerGroup: vllm-batch-group lagThreshold: "100"

minReplicaCount 设成 0 意味着没有任务时完全缩掉,非常节省 GPU。但这要求 vLLM 从冷启到加载模型足够快,如果你用的是网络存储挂载模型,加载一个 7B 模型可能要三分钟,ScaledObject 拉起来后前三分钟没有处理能力,队列会持续积压。所以生产上更常见的是保留一个兜底副本,让请求不至于全停在队列里。

6. 性能调优与稳定性治理

6.1 模型参数:max-model-len、gpu-memory-utilization、max-num-seqs

vLLM 的几个核心参数直接影响吞吐和部署密度:

  • --max-model-len:限制模型最长上下文长度。设置太大会让 KV Cache 预留过多显存;太小又会截断长文本。通常需要结合业务场景统计 prompt 长度分布,取一个 P95 值再加一点余量。我通常先看日志里 token 长度分布,如果 99% 的请求在 4000 token 以内,就设 8192,避免大纹理浪费显存。
  • --gpu-memory-utilization:允许 vLLM 使用的显存比例。默认 0.9,即最多使用 90% 的 GPU 显存。这个值不是越高越好,因为 CUDA context、驱动、其他库都需要一点显存,如果设成 0.99,容易触发 CUDA OOM。
  • --max-num-seqs:控制并发序列数。太大会导致显存中 KV Cache 过多,超出限制;太小则无法充分发挥吞吐。一般设成 256 或根据显存实测调整。

几个参数联动时,需要一定经验。我有一次把max-model-len从 8192 调到 16384,发现显存占用差不多翻倍,但实际业务中超过 8192 的请求极少,白白浪费了一半的并发能力。后来把这个参数调回 8192,--max-num-seqs提升到 256,整体吞吐提升了 50%。

6.2 服务并发模型:线程池、队列与超时控制

vLLM 的 API server 是一个异步 worker,如果并发请求数超过max-num-seqs,多余请求会排队。K8s 侧的 Pod 副本数如果不够,客户端会在 K8s Service 层出现大量 pending 状态。你可以通过限制客户端并发来控制整体压力。

另一个容易被忽略的是请求超时。长文本生成可能几十秒甚至几分钟,如果外部网关设了 30 秒超时,用户看到的就是“服务不可用”,可后端其实还在正常生成。我建议把超时策略分成两类:在线交互类,建议设 60 到 120 秒;离线批量类,不要设硬超时,而是用任务级超时。

6.3 安全与滚动发布:模型更新不能断流量

vLLM 服务升级时,如果用kubectl rollout restart,K8s 默认会先起新 Pod,等新 Pod ready 后再杀旧 Pod。但这时候如果两个 Pod 同时占用同一节点上的 GPU,可能因为显存不足导致新 Pod 调度失败。

解决办法是滚动更新策略设置maxSurge: 0,保证先旧后新,或者把 GPU 资源预留做得更从容:

strategy: rollingUpdate: maxSurge: 0 maxUnavailable: 1

另外模型文件如果有新版本,建议不要直接原地覆盖旧文件。因为 vLLM 进程可能已经读取或者缓存了模型句柄,覆盖后会出现资源竞争。更稳的方式是保留/models/my-7b-v2新目录,改环境变量指向新路径,再滚动更新。

6.4 监控 Dashboard:重点盯哪些指标

监控推理服务,不能只盯 GPU 利用率。我常用的面板会分成两组: 服务健康指标:Pod 启动时间、重启次数、队列长度、平均完成时间、token 吞吐、错误率。 资源指标:GPU 利用率、显存使用、GPU 温度、CPU 内存、网络带宽。

这里想特别说一个指标:GPU 显存使用率。很多新手上线后看到“显存 95%”就紧张,以为泄漏了。实际上 vLLM 会主动预留显存给 KV Cache,这个行为从外部看就是几乎写满。只要没有 CUDA OOM,高显存占用反而是合理状态。你需要看的是vllm:gpu_cache_usage_perc这个指标,它表示 KV Cache 当前使用量占预留 cache 的比例,这个值能更真实地反映负载压力。

7. 常见问题排查与排错实录

7.1 Pod 一直 Pending,GPU 明明有空闲

这个问题我见过太多次。K8s 调度器显示 nvidia.com/gpu 资源不足,但 nvidia-smi 明明有空卡。原因通常是节点上 device plugin 上报的资源数量大于实际可用数量,或者有僵尸 Pod 占用了资源没有释放。排查步骤:

kubectl describe pod <pod-name> -n inference

重点看 Events 里的调度错误。如果是0/4 nodes are available: 4 Insufficient nvidia.com/gpu,检查一下集群中所有 Pod 的 GPU 用量:

kubectl get pods -A -o custom-columns='POD:.metadata.name,GPU:.spec.containers[*].resources.limits.nvidia\.com/gpu'

你会发现可能有几个之前处于 terminating 状态的 Pod 还占着卡。清理方法通常是强制删除,或者重启 kubelet 让它重新 sync。

7.2 vLLM 启动报 CUDA out of memory 但显存看起来很大

启动日志里出现CUDA error: out of memory,第一反应是看gpu-memory-utilization是不是设得太高。我遇到过用 0.92 启动 70B 模型,权重加载完发现差几百 MB 不够 KV Cache,直接报错。解决方法就是把它调低到 0.85 左右,或者增加max-num-seqs之前的预留余量。

另一个隐蔽原因是容器里继承了宿主机的 CUDA_VISIBLE_DEVICES 环境变量,导致 vLLM 只看到单卡,但 Pod request 了两张卡。这种情况下 vLLM 不会用第二张卡,自然报显存不够。解决:在容器环境变量里强制设置CUDA_VISIBLE_DEVICES为空,让 vLLM 依据--tensor-parallel-size自动发现所有 GPU。

7.3 请求排队但 GPU 利用率很低

出现这个现象往往不是 vLLM 本身的问题,而是上游并发受到了限制。比如客户端侧使用了连接池,连接数不够,大量请求在客户端排队等待连接。也有人是因为 Service 的类型是 NodePort 但节点没有开启并发连接优化,导致 Socket 队列堆积。

还可能是 vLLM 的--max-parallel-loading-workers太低,模型加载没占满;不过这只影响启动。真正在线期间利用率低,需要看vllm:num_requests_running指标。如果 running 数量长期为 0,但 Queue 很长,说明 Pod 的副本数或 max-num-seqs 限制在起作用,调大max-num-seqs或增加 HPA 副本数即可。

7.4 滚动更新后流量全部 502

有一次我改了一个镜像 tag,滚动更新后服务立刻 502。原因是我没有设置maxSurge: 0,新 Pod 和旧 Pod 同时申请显卡,新节点上的 GPU 被旧 Pod 占着,新 Pod 一直 Pending,但 Service 的 Endpoint 已经把新 Pod 的 IP 加进去了,因为端口探活失败才没摘流量。实际是旧 Pod 被删除后新 Pod 还没 ready,中间出现了空窗。

解决思路就是前面提到的滚动策略,加上 StartupProbe 的超长等待,确保流量切换只在新 Pod 真正 ready 时发生。同时在 Service 层设置publishNotReadyAddresses: false,确保未 ready 的 Pod 不会进入 Endpoint。

7.5 显存碎片化:大卡虽然空闲,但一个 8 卡任务放不下

集群中 GPU 总量足够,但每个节点只剩 1-2 张卡,导致需要 8 卡的模型无法调度。这是 binpack 策略没生效导致的。我在实际项目里用了一个相对简单的办法:把大模型任务单独放到一个大节点池,调度器优先打满节点。也就是通过 nodeSelector 把需要 8 卡的模型固定到一组 8 卡节点,其他小模型用另一组小卡节点,彻底避免互相干扰。

如果再深一层,可以引入 Kueue 或 Volcano 这类批量调度控制器来管理 GPU 排队与拓扑。它们支持 GPU 拓扑感知调度和 Gang Scheduling,也就是“要么一起给卡,要么一个都不给”,很适合大模型训练和推理的多卡任务。

8. 关于这套实战方案,我最想强调的几件事

如果让我给刚开始做 vLLM + K8s 的人一个最实在的建议,那就是不要一上来就追求“全自动 GPU 共享”和“超大规模多机部署”。先把单模型、单卡跑稳,把探针、滚动更新、优雅缩容这几件事做到位,再逐步引入自定义指标伸缩和拓扑调度。我在实际使用中发现,很多故障并不是 vLLM 本身的问题,而是 K8s 的默认行为跟长连接、大显存、慢启动这三个特性冲突。

最后再分享一个小技巧:每次改动 Deployment 之前,先做一次“预演检查”,用kubectl apply --dry-run=client验证 Yaml 格式,再用kubectl set image这种原生命令触发更新。不要来回手改 Yaml 字段,特别是 GPU 的 requests 和 limits,一个数字写错,可能整个集群调度就乱了。踩过几次坑之后,我现在所有 vLLM 服务都统一走 GitOps 流水线,Yaml 变更必须走 MR 评审,发布记录也是一目了然。这套工作流虽然刚开始麻烦,但帮你省下的排查时间绝对值得。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 13:34:56

OpenVINO加速推理实战:CPU性能调优与模型转换

入行深度学习应用开发这几年&#xff0c;OpenVINO加速推理算是我反复回踩、又反复受益的一条路。第一次拿它跑一个图像分割Demo时&#xff0c;CPU推理帧率从个位数直接拉到二三十&#xff0c;那个反差感至今还记得。这篇东西就围绕OpenVINO加速推理的基础使用来展开——从装环境…

作者头像 李华
网站建设 2026/10/22 13:34:33

docker ps高效用法:--filter与--format实战指南

一个机器上跑了几十个容器之后&#xff0c;你一定会遇到这样一个时刻&#xff1a; docker ps 刷出来的输出密密麻麻&#xff0c;你只想找到某个镜像启动的服务&#xff0c;或者把所有异常退出的容器一次性清理掉。默认的输出既塞不下你要的信息&#xff0c;也筛不出你要的范围…

作者头像 李华
网站建设 2026/10/22 13:33:51

AI辅助学术写作的正确方式:从选题到降重的人机协作实战指南

先澄清一下&#xff1a;别看到“一键生成论文”就兴奋&#xff0c;以为填个标题就能直接交稿。这六个字说的是“AI辅助学术写作”&#xff0c;不是“AI代写”。手里的AI工具用得好&#xff0c;是效率放大器&#xff1b;用歪了&#xff0c;就是学术不端的高危引线。这篇文章我会…

作者头像 李华
网站建设 2026/10/22 13:33:42

AI辅助任务书撰写:结构化生成逻辑与人工把关实战

前阵子帮A同学看一份开题材料&#xff0c;他熬了三个晚上写出来的任务书&#xff0c;还是被导师一句“你到底想干什么”打回来了。这种现象太常见了&#xff1a;研究背景写得像综述&#xff0c;目标写得像口号&#xff0c;阶段安排写得像日历流水账。任务书是很多人做项目的第一…

作者头像 李华
网站建设 2026/10/22 13:33:23

Spring Boot+Vue实战:从零搭建高可用个人博客系统

1. 实训项目复盘&#xff1a;从零搭建一个真正能用的个人博客 先交代一下背景。这是我在某高校软件学院创新实训阶段做的个人项目&#xff0c;主题很朴素——个人博客&#xff0c;但要求是从零开始完整走一遍开发流程&#xff1a;需求分析、技术选型、数据库设计、前后端实现、…

作者头像 李华
网站建设 2026/10/22 13:33:03

IDEA集成LeetCode插件:安装配置与Java刷题效率实战

很多Java开发者都经历过这种场景&#xff1a;准备面试刷题&#xff0c;打开网页版LeetCode&#xff0c;一边看题一边切到IDEA里写代码&#xff0c;写完再复制回网页提交&#xff0c;一套流程下来&#xff0c;上下文切换至少烧掉五分钟。其实这个问题早就有解了——在IDEA里装一…

作者头像 李华