跳到正文

GPU 集群与部署运维 ​

一句话:推理文章讲的是引擎内部怎么省显存、怎么排队,但上线后第一批事故几乎都在节点层:驱动版本不匹配、device plugin 没就绪、显存被残留进程占住、跨节点通信走错网卡、模型盘挂载慢过探针窗口。把“从裸机到可调度 GPU”这一段补齐,引擎参数才有意义。

适用:2026 年自建 K8s + GPU 推理集群。云托管 GPU 的节点层由厂商负责,本文只讲自建时要自己担的部分。

为什么需要 ​

把 GPU 机器加进集群,看起来只是装个驱动,实际有三类耦合:

  • 版本耦合:驱动 ↔ CUDA ↔ 容器 toolkit ↔ 推理引擎,四者版本必须对得上;节点内核升级也会让驱动失配,表现是 nvidia-smi 正常但容器里看不到卡。
  • 资源语义耦合:K8s 只认识 nvidia.com/gpu: 1 这种整数资源,不认识“显存够不够”“是不是同一张卡”“要不要 NVLink 拓扑”。不做额外建模,调度器会把一个需要 80 GB 的任务放到 40 GB 的卡上。
  • 数据面耦合:模型权重动辄几十 GB,镜像里塞权重会让每个节点重复拉取;挂载慢、下载不幂等,Pod 就会在启动探针超时里反复重启。

这三件事都不属于模型,却决定了服务能不能起来、扩容有多快、故障能不能定位。

核心机制 ​

一、节点上架:三件套 + 两个开关 ​

一台 GPU 节点进入可调度状态,需要驱动、容器运行时钩子(container toolkit)、device plugin 三件套齐全,缺一个的表现各不相同:

缺什么现象
驱动nvidia-smi 报错,或内核日志里驱动加载失败
toolkit容器内看不到 /dev/nvidia*,引擎报找不到 GPU
device plugin节点资源里没有 nvidia.com/gpu,Pod 一直 Pending

两个必须显式处理的开关:禁用 nouveau(开源驱动会抢占设备,nvidia 驱动装不上或反复掉卡);给节点打标签与污点(按 GPU 型号、显存、是否允许共享分池,避免小任务占满大卡)。

二、GPU Operator:把节点运维变成声明式 ​

手工装驱动的问题是“节点一多就不一致”。GPU Operator 把驱动、toolkit、device plugin、DCGM 指标、MIG 管理、GPU 特征发现都做成 DaemonSet + CRD,节点加进来后自动收敛到目标状态。

代价是它接管了驱动生命周期:驱动升级会触发节点重启或 Pod 驱逐,必须配合节点排空窗口;如果集群用的是云厂商预装驱动的镜像,就不要让 Operator 再管驱动,否则两套机制互相打架。

三、共享策略:独占、硬切、软切 ​

方式隔离适用代价
整卡独占显存与算力都隔离大模型、SLA 严格利用率低,小模型浪费
MIG 硬切片显存与算力按实例隔离多租户、小模型混布只支持特定卡型,切分后不可动态合并
时间片共享只隔离时间,不隔离显存开发测试、低优先级批处理一个进程 OOM 会拖垮同卡其他进程

关键区别是显存隔离:时间片共享下,任何一个进程占满显存,同卡其他进程就会失败。把生产流量放到时间片共享的卡上,等于把稳定性交给邻居。MIG 能解决隔离,但切片规格在创建时固定,负载变化后要重建实例。

四、网络与存储:两个容易被忽略的数据面 ​

多机推理(张量并行、流水线并行)依赖 NCCL 跨节点通信。默认走容器 overlay 网络,常见问题是走错网卡和 MTU 不一致:节点有多张网卡时,NCCL 可能选到管理网,带宽掉一个数量级。做法是显式指定通信网卡、核对 MTU,并把高带宽网络与业务网络分开。

模型权重不要烘进镜像。同样的引擎配不同模型,如果每个模型打一个镜像,会出现十几个几十 GB 的镜像,节点磁盘和拉取时间都受不了。分层做法是:基础镜像(OS + 驱动用户态 + CUDA)→ 引擎镜像(vLLM/SGLang 等)→ 业务适配层,权重放对象存储或 PVC,由 InitContainer 幂等下载(先校验哈希,命中则跳过)。

五、工作负载形态:什么时候用 StatefulSet ​

无状态推理用 Deployment 就够了。出现下面任一需求时要换 StatefulSet:

  • 每个副本要挂自己的模型缓存盘,且希望重建后沿用同一块盘;
  • 需要稳定的网络标识做点对点通信;
  • 副本之间有启动顺序(例如先起 head 再起 worker)。

InitContainer 负责“准备数据”,主容器只负责“提供服务”,两者分开后,下载失败不会让服务进程反复崩溃重启,日志也能分清是数据问题还是引擎问题。

图里两个闭环:调度侧按“型号 + 显存 + 拓扑”筛节点,运维侧按 DCGM 指标摘除异常节点。缺任一环,都会出现“Pod 起来了但跑不动”或“卡坏了没人发现”。

六、多卡与拓扑:调度器不知道的事 ​

张量并行要求同一组卡之间带宽足够,跨 NUMA、跨 PCIe switch、跨节点性能差很多。K8s 原生调度只看资源数量,不看拓扑,所以要么用支持拓扑感知的调度插件,要么把“同一机型、同一交换机下”做成节点亲和,再用 gang scheduling 保证多卡任务要么全起、要么不起——否则部分副本起来占着卡等同伴,会把整个池拖死。

实战场景 ​

场景:新节点上架后 8 张卡一张都不可调度,12 个副本全 Pending ​

现象:自建 K8s 集群新上架一批 8 卡节点,kubectl get nodes -o custom-columns=...,GPU:.status.allocatable.nvidia\.com/gpu 这一栏全是 0(应为 8),当天扩容的 12 个推理副本全部 Pending,流量压到存量 6 个副本上,P99 从 380 ms 涨到 9.2 s、队列拒绝率 14%(经验值,需校准)。运维在节点上敲 nvidia-smi 正常、能看到 8 张卡,于是判断驱动没问题,转去调度器加节点亲和,问题没有变化。

排查路径:

排除对象判定证据结论
驱动与 nouveaukubectl debug node/<node> -- nvidia-smi -L 列出 8 张卡;dmesg 里没有 nouveau 抢占设备的记录排除:驱动加载与开源驱动抢占都不是原因
container toolkit起一个 runtimeClassName: nvidia 的探针 Pod,nvidia-smi -L 看到 8 张卡、/dev/nvidia* 齐全排除:容器内能看到设备,运行时钩子已生效
device plugin新节点上 nvidia-device-plugin 的 Pod 数是 0;节点带 nvidia.com/gpu 污点而 DaemonSet 没有对应 toleration,标签 gpu=true 存在但 Capacity 无 nvidia.com/gpu确认:三件套缺第三件,且被自己的污点挡在外面
对症做法:补 device plugin 并让它与节点污点成对出现,再把 allocatable 收敛当作上架判据。
yaml
node_bringup:                                          # 三件套按顺序验收,缺一项都不算上架
  driver:  {version: "580.x", nouveau: disabled}       # nvidia-smi -L 必须列出 8 张卡
  toolkit: {runtime: nvidia, default_runtime: true}    # 容器内能看到 /dev/nvidia*
  plugin:  {daemonset: nvidia-device-plugin, nodeSelector: {gpu: "true"},
            tolerations: ["nvidia.com/gpu"]}           # 与污点成对,否则装好也起不来
  verify:  {allocatable: 8, taint: "nvidia.com/gpu=present:NoSchedule"}

本章把三件套的缺失表现分开列:驱动缺失是 nvidia-smi 报错,toolkit 缺失是容器内看不到 /dev/nvidia*,device plugin 缺失是节点资源里没有 nvidia.com/gpu。前两行证据排除了前两项,能用“容器可见卡、资源不可见”唯一解释的就是第三件,所以动作是补 DaemonSet,而不是回滚驱动或改调度器。 复盘要点:上架判据不是 nvidia-smi 能跑,而是 allocatable 出现整数卡数,并用 runtimeClassName: nvidia 的探针 Pod 实测设备数对得上。红线:新节点 10 分钟内 allocatable 未收敛就自动打上 unschedulable 并告警,避免半成品节点吸走调度。更早的指标是 device plugin 的 desired 与 ready 之差,节点列表一变、ready 小于 desired 超过 5 分钟就是第一现场(阈值经验值,需校准)。

场景:扩容 40 个副本同时拉 140 GB 权重,对象存储 503 升到 18% ​

现象:70B 模型(BF16 权重约 140 GB)的推理池在早高峰从 8 个副本扩到 48 个,40 个新 Pod 的 InitContainer 各自从同一对象存储从头下载权重,集群出口带宽打满 25 Gbps,对象存储 503 SlowDown 比例从 0.1% 升到 18%;40 × 140 GB ≈ 5.6 TB,按 25 Gbps 需要约 30 分钟,而 startupProbe 窗口只有 300 秒(10 秒 × 30 次,见 06),六成新 Pod 在超时里反复重启,扩容窗口的 P99 从 700 ms 涨到 12 s(经验值,需校准)。

排查路径:

排除对象判定证据结论
对象存储限流单桶 GET 峰值 25 Gbps,503 只出现在并发拉取窗口内,窗口外 0 错误排除:限流是被打出来的结果,不是根因
镜像与节点磁盘crictl images 显示推理镜像 6 GB 且已预拉;节点磁盘 IO 未饱和排除:镜像层没有重复拉取
探针与幂等标记startupProbe 已是 300 秒、单副本冷启动基线 70 秒;InitContainer 命令里只有 download,没有哈希校验也没有命中判断确认:探针够用,是被不幂等的全量下载撑爆的
对症做法:把下载从“每次启动的常态动作”降级为“节点级一次性动作”,标记与数据同盘同生命周期。
yaml
initContainers:
  - name: fetch-weights                      # 命中标记即退出;未命中才下载并校验哈希
    command: ["sh", "-c"]
    args: ["test -f /models/qwen3-70b/$REV/ok || (rclone copy --checksum --bwlimit 2G \
            oss:models/qwen3-70b/$REV /models/qwen3-70b/$REV && cd /models/qwen3-70b/$REV \
            && sha256sum -c SHA256SUMS && touch ok)"]
    volumeMounts: [{name: weights, mountPath: /models}]

本章要求权重走对象存储加本地缓存盘、由 InitContainer 先校验哈希、命中则跳过;改完后扩容的网络流量从“副本数 × 140 GB”变成“新节点数 × 140 GB”,老节点上的副本直接命中标记、启动回到 20 秒级,只有落在新节点的副本走网络(经验值,需校准)。节点上架时预先拉取当前模型,新节点也不必把这 5.6 TB 重付一遍。 复盘要点:判据是启动路径上的网络传输量应约等于“新节点数 × 权重体积”,一旦它随副本数线性增长,说明幂等没生效。红线:InitContainer 的下载必须以版本或内容哈希为键,标记与数据同盘,startupProbe 只允许覆盖“本地盘加载 + 引擎初始化”,不允许覆盖网络下载。早发现的指标是扩容窗口里“对象存储 GET 并发 ÷ 副本数”,这个比值恒定不降就是每个副本都在全量拉。

场景:3 个租户挤一张 80 GB 卡,一次 OOM 连坐 12 个副本 47 分钟 ​

现象:多租户小模型池用时间片共享把 12 个 7B 副本(14 GB 权重 + 4 GB KV)塞进 4 张 80 GB 卡,每卡 3 个 Pod、各自把 gpu_memory_utilization 压到 0.3 才起得来,节点利用率从 35% 升到 78%。三周内 5 次 OOM,其中一次某租户把 max_model_len 从 8192 调到 32768,激活峰值顶穿 24 GB 预算,同卡另外两个租户的进程在 3 分钟内一起报显存分配失败,12 个副本累计不可用 47 分钟、触发 SLA 赔付(经验值,需校准)。

排查路径:

排除对象判定证据结论
单模型显存估算nvidia-smi --query-compute-apps=pid,used_memory 显示三个进程各约 24 GB,都在各自预算内;只有该租户改过 max_model_len排除:权重与 KV 公式没算错
请求形态该 Pod 日志在 OOM 前是 32K 长度请求;同卡另两个 Pod 在同一分钟报分配失败、restartCount 同步增长确认:不是单 Pod 故障,是显存不隔离的连坐
共享模式节点 allocatable 是 8 卡却跑了 12 个 Pod,Pod 上没有整数 nvidia.com/gpu 申请,节点共享配置为 time-slicing确认:该用硬切的场景用了软切
对症做法:把 80 GB 卡切成 3 个 2g.20gb 实例、一实例一 Pod,让每个进程的显存分母从整卡变成切片。
yaml
# 80 GB 卡:MIG 2g.20gb × 3,显存与算力都按实例隔离
resources:
  limits: {nvidia.com/mig-2g.20gb: 1}       # 分母是 20 GB,不是整卡 80 GB
env:
  - {name: GPU_MEM_UTIL, value: "0.90"}     # 0.90 × 20 = 18 GB:14 GB 权重 + 4 GB KV
  - {name: MAX_MODEL_LEN, value: "8192"}    # 单请求 KV 上限,32K 会顶穿切片

本章判据是显存是否隔离:时间片共享下 gpu_memory_utilization 的分母仍是整卡 80 GB,三个进程的预算相加逼近上限,任何一方超出都是同卡全体受罚;MIG 把卡切成 3 个 20 GB 实例后,每个 Pod 的分母变成 20 GB,18 GB 用量落在 90%,连坐在硬件层被切断,算力也按实例隔离。代价是本章边界那条:切片规格创建时固定、不可动态合并,超过 20 GB 的模型只能走整卡独占池。 复盘要点:判据是“同一张卡上是否允许两个不同 SLA 的租户共存”,只要允许就必须显存隔离(MIG 或整卡独占),时间片只留给开发测试和低优先级批处理。红线:生产租户的 Pod 没有 nvidia.com/gpu 或 nvidia.com/mig-* 的整数申请,一律拒入。早发现的指标是 --query-compute-apps 的显存合计除以卡容量,超过 0.9 持续 10 分钟就该告警,比等一次连坐 OOM 早一个量级。

最小上手 ​

先用一条命令确认节点真的可用,再谈部署:

bash
kubectl get nodes -l gpu=true -o custom-columns=NAME:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu
kubectl describe node <node> | grep -A5 "Allocated resources"
kubectl exec <pod> -- nvidia-smi -L
yaml
initContainers:
  - name: fetch-weights
    image: <model-loader>
    command: ["sh", "-c", "test -f /models/ok || (download && touch /models/ok)"]
    volumeMounts: [{name: models, mountPath: /models}]

验收看四件事:新节点加入后能自动出现 GPU 资源;同一个模型换节点不需要重打镜像;节点卡异常能被指标发现并排空;多卡任务不会出现“半启动”长期占卡。

生产动作 ​

  • 版本矩阵固定下来:驱动、CUDA、引擎、镜像基座四个版本一起测、一起发;升级前先在灰度池验证,不要直接滚全集群。
  • 权重与镜像分离:镜像只放运行时,权重走对象存储 + 本地缓存盘;本地缓存要有容量上限和淘汰策略,否则节点磁盘会被历史模型撑爆。
  • 健康检查看硬件指标:显存 ECC 错误、XID 错误、温度降频、掉卡,都要进告警;nvidia-smi 能跑不代表卡是健康的。
  • 排空要有时序:先停止调度、再等长任务结束、最后驱逐;直接驱逐会把正在 prefill 的请求全部打成 5xx。
  • 把共享策略写进资源模型:哪类任务允许时间片共享、哪类必须独占,用节点标签和准入控制约束,而不是靠人记得。

边界 ​

  • 小集群不必上 Operator:几台机器手工装驱动更可控,Operator 的收益在节点数量和一致性要求上。
  • MIG 不是万能:切片规格固定、部分卡型不支持,小模型用 MIG 可能比独占更浪费。
  • 时间片共享不适合生产 SLA:没有显存隔离,邻居故障就是自己的故障。
  • 拓扑感知有成本:调度插件与节点亲和都会降低调度自由度,资源紧张时可能排不上;先量化跨 NUMA 的实际损失,再决定要不要做。
  • 云托管场景不适用本文大半内容:节点层由厂商兜底,重点转向配额、抢占式实例与跨可用区容灾。

讲清楚 ​

GPU 上线的第一段路不在引擎里,而在节点上:驱动、toolkit、device plugin 三件套齐全,禁用 nouveau,按型号和用途打标签污点;节点多了用 GPU Operator 收敛状态,但要接受它接管驱动升级。共享有三种方式——整卡独占、MIG 硬切片、时间片软共享,区别在显存是否隔离,生产 SLA 不能放在软共享上。

多机推理要显式指定 NCCL 通信网卡并对齐 MTU;权重与镜像分离,用 InitContainer 幂等下载,需要稳定盘或稳定标识时用 StatefulSet。多卡任务要拓扑感知加 gang scheduling,避免半启动占卡。

判断这套东西做没做好,不看能不能跑起一个 Pod,而看:新节点能否自动可用、换模型是否要重打镜像、坏卡能否被发现并排空、扩容是否能跟上流量。

适用:2026 年自建 K8s + GPU 推理集群 | 初稿 2026-10

把 AI 工程讲到既懂也会用