集群有 24 张 GPU,监控上还空着 16 张,一个要 16 张卡的训练任务却一直 Pending;更糟的是,一半 Pod 起来了,另一半没起,起来的那一半占着卡空等。Kubernetes 默认一次只给一个 Pod 找节点,而分布式训练要的是“一组 Pod 要么一起放下,要么一个也别占”;GPU 又只能整块、最好按拓扑分配。空闲卡的总数,说明不了大任务能不能跑。
待审核草稿。 这一篇还没有经过站主审阅,事实、取舍和措辞都可能修改;引用前请回到文中链接的一手资料核对。
Pod 里面的程序怎样把计算交给 GPU,见框架这一层;为什么训练的每个 rank 都必须到齐,见多卡训练。本文对照 kubernetes.io 与 NVIDIA 官方文档,2026-10-15 核对;当时 Kubernetes 的最新版本是 1.37.1(2026-09-15 发布,见 releases 页)。
只需要这几个 Kubernetes 概念
| 概念 | 是什么 | 和 GPU 任务的关系 |
|---|---|---|
| Pod | 能创建和管理的最小部署单元,一个或几个共享网络与存储的容器 | 一个推理副本、一组训练 rank,通常各是一个 Pod |
| Node | 一台虚拟机或物理机,上面跑 kubelet | 一台 8 卡服务器就是一个 node |
| kube-scheduler | 给还没分配节点的 Pod 先过滤、再打分,选出节点后通过 binding 告诉 API server | 默认一个 Pod 一个 Pod 地挑 |
| Controller | 控制循环:盯着集群状态,把实际状态往期望状态推 | Deployment、Job 背后都是 controller |
| Deployment | 声明式地维持一组副本 Pod,并按节奏更新 | 长期在线的推理服务 |
| Job | 创建 Pod 并重试,直到指定数量的 Pod 成功结束;Indexed 模式给每个 Pod 一个 0 到 completions−1 的编号 | 训练、评测、批处理;编号可以拿来当 rank |
| requests / limits | scheduler 按 requests 选节点;kubelet 按 limits 限制容器用量 | GPU 写在 limits 里 |
CPU 和内存的 limits 加起来可以超过节点容量(超卖):CPU 超过 limit 会被限流,内存超过 limit 会被 OOM kill。GPU 不一样。它是 extended resource,文档写明只能是整数,而且“Extended resources cannot be overcommitted, so request and limit must be equal …”(资源管理页),即不能超卖,两处都写时 request 和 limit 必须相等;同一块设备也不能在容器之间共享。按 GPU 调度页,GPU 只写在 limits 里,requests 默认等于 limits。
GPU 怎样变成可调度的资源
靠 device plugin。厂商插件通过 gRPC 向 kubelet 注册,用 ListAndWatch 上报自己管理的设备;kubelet 把数量写进节点状态,比如“这台机器有 8 个 nvidia.com/gpu”。scheduler 按这个计数判断放不放得下;容器创建时,kubelet 再调用插件的 Allocate,让它做设备相关的准备。于是一个 8 卡训练 Pod 这样写:
apiVersion: v1
kind: Pod
metadata:
name: trainer-0
spec:
containers:
- name: trainer
image: registry.example.com/trainer:v1
resources:
limits:
nvidia.com/gpu: 8
光有插件还不够,节点上还要装驱动、让容器运行时能用 GPU、给节点打上 GPU 型号标签、导出监控指标。NVIDIA GPU Operator 用 operator 把这些组件一起管起来:GPU 驱动(默认以容器方式运行,也可用节点上预装的)、Container Toolkit、device plugin、给节点打 GPU 标签的 GPU Feature Discovery、配置 MIG 的 MIG Manager,以及采集指标供 Prometheus 抓取的 DCGM Exporter。截至 2026-10-15,GitHub 上它的最新版本是 v26.7.2(2026-10-14 发布)。
Dynamic Resource Allocation(DRA) 是另一条路。它像用 PersistentVolumeClaim 申请存储那样申请设备:驱动发布 ResourceSlice 描述有哪些设备,管理员定义 DeviceClass,工作负载写 ResourceClaim,可以用 CEL 表达式按属性筛选设备、让多个 Pod 引用同一个 claim 共享设备、按 claim 配置设备。和 device plugin 相比,它不再只有“几张卡”这一个整数。
DRA 从哪个版本起稳定,官方材料说法不一(2026-10-15 核对)。v1.34 的发布公告(2025-08-27)写的是 DRA 的核心在 v1.34 进入 GA,resource.k8s.io/v1 API 升为 stable 并默认可用;DRA 概念页则标注 “Stable since Kubernetes v1.35”,说它最早出现在 v1.30,feature gate 已锁定、不能再关闭。feature gate 参考页把 DynamicResourceAllocation 列为 1.30–1.31 alpha、1.32–1.33 beta,1.34 和 1.35 起各有一行 stable;概念页的 v1.35 可能指开关锁定的版本,但页面没有这样说明。可以确定的是:在当前的 1.37 里它是稳定功能。概念页还写了一条限制:scheduler 不支持为 DRA 资源做抢占,高优先级 Pod 只能等占用设备的 Pod 结束。NVIDIA 一侧,GPU Operator 文档介绍了安装 DRA Driver for NVIDIA GPUs v0.5.1,作为 device plugin 的较新替代;其中动态 MIG、按份额共享、MPS 等功能仍是 alpha,而且一个集群里,DRA 用的 GPUCluster 和 device plugin 用的 ClusterPolicy 只能有一个。
一张卡给几个任务:MIG、time-slicing、MPS
推理小模型、notebook、评测脚本往往用不满一张卡。共享有三种办法,关键差别在于隔离了什么:
| 方式 | 怎么分 | 隔离了什么 | 没隔离什么,或其他限制 |
|---|---|---|---|
| MIG | 从 Ampere 架构起,硬件上最多切成 7 个 GPU instance,各有固定的 SM 和显存 | 显存通路(crossbar 端口、L2、显存控制器)各自独占,有故障隔离和可预期的 QoS | 只能按支持的 profile 切,切法要事先配置 |
| Time-slicing | 设备插件把一张卡报成多个 replica,进程轮流用时间片 | 几乎没有:只是轮流 | 没有显存或故障隔离;申请多个 replica 也不保证拿到成比例的算力 |
| MPS | 控制 daemon 让多个进程同时在一张卡上跑,可限制每个客户端的显存和算力份额 | 每个客户端有独立的 GPU 地址空间;显存与计算份额由 daemon 限制 | 一个客户端触发致命 GPU 错误,共享这张卡的其他客户端也会收到错误并需要退出 |
NVIDIA 的 device plugin README(2026-10-15 核对)还写着:time-slicing 和 MPS 二选一;MPS 共享自 v0.15.0 起标为 experimental,且不能在开启 MIG 的设备上使用;同一节点上的所有 GPU 用同一种共享方式。GPU Operator 的 MIG 页说明了两种策略:single 表示节点上所有 GPU 都开 MIG,mixed 表示不是全部都开;文档示例里,一张 H100 80GB 切成 7 个 1g.10gb 后,节点标签显示的 GPU 数是 7。
用 MIG profile 表算一笔:H100 80GB(PCIe 和 SXM5)的 1g.10gb 占 1/7 的 SM、1/8 的显存,最多 7 个;1g.20gb 同样 1/7 的 SM、1/4 的显存,最多 4 个。一个 7B 参数的模型用 BF16 存权重要 $7\times10^9\times2=14$ GB,放不进 10 GB 的实例;放进 20 GB 的实例后,留给 KV cache 和运行时的只剩约 6 GB。这是只算权重的教学账,完整显存账见 KV cache 推理预算。
为什么分布式训练要“成组调度”
用一个能手算的例子。3 台节点 A、B、C,每台 8 张卡,共 24 张。依次来了 4 个任务:两个单 Pod 的 4 卡任务 S4a、S4b,一个 16 卡训练 T16(2 个 Pod,各 8 卡,所有 rank 到齐才开始训练),一个单 Pod 的 8 卡任务 T8。总需求 32 张,超过 24,必然有人排队;问题是谁排队、排队的人占不占卡。
按 v1.37 的默认配置,scheduler 一个 Pod 一个 Pod 地放;NodeResourcesFit 插件默认用 LeastAllocated 打分,权重只给 CPU 和内存(见 scheduler 配置参考),GPU 用了多少不进入这一项打分。S4a 落在 A 之后,S4b 更可能去 CPU、内存更空的 B 或 C;假设去了 B。下表比较三种做法最终的状态:
| 做法 | 节点 A | 节点 B | 节点 C | 在干活的卡 | 还在排队 |
|---|---|---|---|---|---|
| 默认:逐个 Pod 放,按 CPU 和内存打分 | S4a(4) | S4b(4) | T16 的一半(8,空等) | 8 | T16 另一半、T8 |
| 加上成组调度 | S4a(4) | S4b(4) | T8(8) | 16 | T16 |
| 成组调度,并按 GPU 装箱 | S4a+S4b(8) | T16(8) | T16(8) | 24 | T8 |
第一行最糟:T16 的第一个 Pod 拿下 C,第二个 Pod 找不到剩 8 张卡的节点,进程已经启动却在等另一半 rank;A、B 各剩 4 张,合起来 8 张,却拼不出一个 8 卡 Pod,T8 也只能等。24 张卡里只有 8 张在干活。如果再来一个同样的 T16,两个任务可能各占一台节点、各等另一半,谁也开始不了,这就是死锁。
第二行:成组调度发现 T16 两个 Pod 不能同时放下,就一个也不绑定,C 留给 T8,16 张卡在干活。代价是 T16 要等两台整机同时空出来,没有优先级或预留时可能一直等下去。第三行把 scheduler 换成 MostAllocated 装箱策略并给 GPU 加权,两个小任务挤进同一台,留出两台整机给 T16,24 张卡都在干活;这时轮到 T8 等 A 空出来。哪种顺序更好取决于优先级和配额,装箱不是在任何情况下都更优。
放得下,还要放得近
T16 的 16 个 rank 每步都要做梯度 all-reduce。按理想 ring 算,每个 rank 发送 $2\frac{P-1}{P}S$ 字节(推导见多卡训练),环上每条边都承载这么多数据。把 16 个 rank 按节点顺序排成环,跨节点(cross-node)的边数等于用到的节点数:整机放在 2 台上是 2 条边走网络,其余走节点内互联;如果允许拆成 4 卡的小 Pod、摊在 3 台上,就有 3 条边走网络,而且每台要和更多邻居争网卡。再往上一层,两台节点若分在不同机架,跨机架的路径通常更长。这是教学用的理想环;NCCL 实际选什么算法,以及节点内外的带宽差多少,要看硬件和 NCCL 的配置。
这就是拓扑感知调度要解决的事:不只问“剩几张卡”,还问“这几张卡离得多近”。下面几个项目都在处理成组和拓扑,做法不同,这里只按官方文档中性地列出:
| 方案 | 做在哪一层 | 成组怎么实现 | 拓扑 | 状态(2026-10-15 核对) |
|---|---|---|---|---|
| Kubernetes 核心的 gang scheduling | kube-scheduler 与 PodGroup / Workload API | 放不下 minCount 个 Pod,就一个也不绑定 |
Topology-Aware Workload Scheduling 把整组放进同一拓扑域:v1.36 起 alpha,默认关闭 | v1.37 起 beta,默认关闭,需开启 GenericWorkload feature gate |
| Kueue | 在 kube-scheduler 之上做作业级排队、配额和准入,不替代它 | waitForPodsReady:超时前没全部就绪,就驱逐并重新排队(文档称之为一种简单的 all-or-nothing 实现) |
TAS:v0.14 起 beta,默认开启,可按 rack、block 等层级要求或偏好 | 最新 v0.20.2(2026-10-15) |
| Volcano | 自己的批调度器,由 actions 和 plugins 组成 | PodGroup 的 minMember:资源不够运行这么多 Pod,就一个都不调度 |
本文未核对 | 最新 v1.16.0(2026-10-14) |
| JobSet | 把一组 Job 当作一个单元管理的 API | 概览页没说它自己做成组调度;可以与 Kueue 配合排队 | 一个注解让每个子 Job 独占一个拓扑域(如机架) | 最新 v0.13.0(2026-10-12) |
Kubernetes v1.37 的博客(2026-09-08)还介绍了处于 alpha 的 CompositePodGroup,用来表达多层的成组和拓扑约束,并提到它面向 JobSet、LeaderWorkerSet 这类上层 API 的工作负载。
Slurm 还是 Kubernetes
训练集群的另一种做法是用 Slurm 排作业。两者的出发点不同,主要差别如下:
| 维度 | Slurm | Kubernetes |
|---|---|---|
| 设计重心 | 给用户分配一段时间的节点资源,管理待运行作业的队列 | 用控制器把容器化应用维持在期望状态 |
| 多节点作业 | 先分配一组节点,再在这组节点上启动并行作业 | 默认逐个 Pod 调度;成组需要 gang scheduling 或 Kueue、Volcano 等 |
| GPU 记账 | GRES(generic resources) | extended resource(nvidia.com/gpu)或 DRA |
| 排队与回填 | backfill 默认加载:不推迟高优先级作业预计开始时间的前提下,先跑低优先级作业;依赖准确的作业时间上限 | 默认调度器没有作业级配额队列;Kueue 等补上配额、公平共享和准入 |
| 网络拓扑 | topology/tree 找能满足作业的最低一层交换机;还有 topology/block | 节点标签,加上 Kueue TAS 或核心 TAS(alpha) |
| 在线服务 | 作业带时间上限,backfill 依赖它 | Deployment、Service、HPA 都是内置对象 |
| 容器 | 原生支持无特权的 OCI 容器,需要配置 oci.conf 等 |
以容器镜像为基本运行单位 |
| “gang scheduling” 这个词 | 指分时:几个作业分到同一批资源,轮流挂起 | 指一组 Pod 全有或全无 |
最后一行最容易误会:同一个词,在 Slurm 里是“轮流用”,在 Kubernetes 生态里是“一起上”。这张表比较的是机制,不是谁更好;选择取决于团队已有的工具、在线服务的比重和多租户需求。
推理服务:扩容不只是加副本
推理服务通常是 Deployment,靠 HorizontalPodAutoscaler(HPA)扩缩。HPA 的核心公式是:
$$ \text{desiredReplicas}=\left\lceil \text{currentReplicas}\times\frac{\text{currentMetricValue}}{\text{desiredMetricValue}}\right\rceil . $$
例如 4 个副本,每个副本平均排队 30 个请求,目标是 10 个,就要 $\lceil4\times30/10\rceil=12$ 个副本。比值落在容忍度内(默认 0.1)不动作;缩容可以设稳定窗口,文档示例是 300 秒。指标从哪来?按文档,非 CPU/内存的指标要通过 custom 或 external metrics API 提供,通常需要一个适配器;GPU 指标可以由 DCGM Exporter 交给 Prometheus,例如显存用量 DCGM_FI_DEV_FB_USED。选什么指标更要紧。Google Cloud 的 GKE 推理扩缩最佳实践(2026-10-13 更新)推荐用服务器的队列长度和 batch 大小,并指出 GPU 利用率只衡量 GPU 处于活动状态的时间,不衡量活动时做了多少工作。
再算冷启动(cold start)。一个 70B 参数的模型用 BF16 存,权重是 $70\times10^9\times2=140$ GB。新副本起来时,至少要把这 140 GB 从存储读进来,耗时约为 $140\text{ GB}/B$,$B$ 是有效读取带宽(下表全是教学假设,不是某种存储的规格):
| 有效读取带宽 | 读完 140 GB |
|---|---|
| 1 GB/s | 140 s |
| 3 GB/s | 约 47 s |
| 10 GB/s | 14 s |
| 25 GB/s | 5.6 s |
换一个条件:8 个副本同时启动,共用一个总带宽 10 GB/s 的共享存储,总共要读 $8\times140=1120$ GB,约 112 s,比单个副本慢 8 倍。读权重之外,还有加节点、拉镜像、初始化 CUDA、引擎预热(例如捕获 CUDA Graph、编译)等时间,这里都没算。回到上面的扩容例子:假设新副本总共要 2 分钟才能接请求(教学假设),从 4 扩到 12 个副本的决定做出之后,这 2 分钟里队列还在涨。常见对策是保留最少的热副本、用更早的信号触发扩容、缓慢缩容;具体效果要用真实请求回放(replay)验证。
排错时先问什么
| 看到的现象 | 先查什么 | 不能据此推出什么 |
|---|---|---|
| Pod 一直 Pending,提示 GPU 不够 | 每台节点还剩几张整卡,Pod 要几张;taint 和 nodeSelector 是否把节点排除了 | 集群空闲卡总数够,就一定放得下 |
| 训练一半 rank 起来,一半 Pending | 有没有成组调度或作业级准入 | 再等一会儿自然会好;可能已经死锁 |
节点上 nvidia.com/gpu 是 0 |
驱动、container toolkit、device plugin 是否就绪 | 这台机器没有 GPU |
| 共享卡上一个任务崩了,邻居也崩 | 用的是 time-slicing 还是 MPS | 一定是邻居的代码有问题 |
| 扩容之后,延迟还高一阵 | 新副本的冷启动:拉镜像、读权重、预热 | HPA 没有生效 |
| GPU 利用率很高,吞吐却低 | 这个指标只说明 GPU 忙的时间比例 | 利用率高就说明 batch 合适 |
调度器只决定卡分给谁;卡分到之后,一次请求在引擎里怎样排队和组 batch,见一次 LLM 请求;Pod 里的代码怎样把计算交给 GPU,回到框架这一层。
本页术语 3
- 跨节点 cross-node
- 通信或执行跨越不同物理机器
- 冷启动 cold start
- 用户或内容缺少历史证据,模型只能靠先验
- 回放 replay
- 用固定输入重新执行一段数据或模型链路,检查结果是否一致