一个团队有 32 张卡,利用率报告显示 68%,而训练任务的平均排队时间是 4 小时。看起来"还有 32% 空闲",实际是这些空闲分散在 32 张卡上,每张空一点,凑不出一次整机训练。而调度器按"整卡"分配,于是碎片不能被利用。
这个事实带来两个后果:碎片不可回收(不能用"半张卡"补另一个半张),以及抢占代价不对称(一次抢占可能丢掉几小时训练)。而 CPU 调度里这两件事都不成立。
GPU 调度与 CPU 调度的差别,全部来自一个事实:卡是稀缺且不可细分的。这决定了碎片、抢占、以及"独占还是共享"的取舍,都与传统集群不同。
1. 三种分配粒度
| 粒度 | 优点 | 缺点 |
|---|---|---|
| 整卡独占 | 无干扰、性能可预测 | 碎片多、利用率低 |
| 分时共享(MPS/时间片) | 利用率高 | 干扰、延迟抖动 |
| 显存切分(MIG) | 隔离较好 | 配置固定、不灵活 |
第一项是多数训练集群的选择,因为它换来"性能可预测"——而这在训练里比利用率重要(训练慢一倍比排队久更糟)。
第二项的干扰是真实的:两个进程共享一张卡时,显存带宽与 L2 争抢会让各自都变慢第三项(MIG)的"配置固定"是实打实的限制:切分方案在创建时确定,改一次要重配。所以它适合"长期稳定的多租户",不适合"每天变的任务组合"。
,而"慢多少"取决于对方的访存模式。
2. 碎片的三种形态
| 形态 | 表现 |
|---|---|
| 卡数碎片 | 每台空 1 张,凑不出 8 卡 |
| 显存碎片 | 卡上空 20GB,但训练要 40GB |
| 拓扑碎片 | 卡够但跨了 NVLink 域 |
第三项最容易被忽略:8 卡训练要求这 8 张卡在同一 NVLink 域,否则通信走 PCIe,吞吐显著下降。于是"集群里还有 8 张空闲卡"不等于"能跑一个 8 卡任务"。
第一项还有一个"时间"版本:上午每台占 2 卡、下午每台占 6 卡,全天看利用率不低,但从未出现过 8 卡空闲。所以碎片要看"连续窗口",而不是日均值。
第一项的解法是"装箱策略":优先把任务集中到少数机器,而不是均匀摊开。均匀摊开会让每台都空一点,而集中会让部分机器完全空出来。
// 两种装箱策略的对比(32 卡,每台 8 卡,4 台)
均匀:任务 A 用 2 卡/台 → 4 台各占 2 → 每台空 6,但都被占着
集中:任务 A 用 8 卡在 1 台 → 该台满,其余 3 台全空 → 可跑 8 卡任务
// 结论:碎片不是"总量不够",而是"分布不对"
3. 抢占的三个层次
| 层次 | 代价 |
|---|---|
| 排队(不抢占) | 高优任务也等 |
| 检查点抢占 | 丢失到上一个 checkpoint |
| 立即抢占 | 丢失当前进度 |
第二项的前提是"任务能存检查点",而这需要训练代码支持。检查点还有个"存的时间"成本:大模型一次 checkpoint 可能几十秒到几分钟,而它发生在关键路径上。所以"存得频繁"会降低有效算力,这个代价要和"被抢占的损失"一起算。
多数框架有,但"能存"与"存得够频繁"是两件事:checkpoint 间隔 30 分钟,被抢占就丢 30 分钟。
第三项只适合短任务,长训练被立即抢占会造成大量浪费。
调度还有一层"作业形态"的差异值得说清:训练任务长且不可中断,推理任务短且可漂移。所以一个集群同时跑两类时,应当物理分区或严格配额,而不是"混着调"——因为混着调的结果通常是"推理被训练挤死"。
// 两类作业的调度差异
训练:时长小时级、要整机、不可抢占(或检查点抢占)
推理:时长秒级、可切分、可迁移、有 SLO
// 结论:混跑必须有配额与优先级,否则 SLO 无法保证
4. 排队与优先级的设计
| 规则 | 效果 |
|---|---|
| FIFO | 公平但阻塞(大任务堵住小任务) |
| 优先级 + 老化 | 兼顾紧急与公平 |
| 配额(按团队) | 防单团队占满 |
| 回填(backfill) | 用小任务填空 |
第四项的前提是"时长估计准确"。用户申报的时长普遍偏乐观,所以实践上应当用"历史同类任务的 P50/P90"来估计,并给"超时任务"设上限(超时即抢占)。
第四项是最有效的利用率手段:在大任务等待期间,允许小任务"插空"运行,前提是小任务能在"大任务可开始之前"结束。这需要准确的时长估计。
第二项的"老化"防止饥饿:等待越久优先级越高,否则低优任务永远排不上。
// 排队策略的四条参数(示例)
queue:
policy: priority_with_backfill
max_wait_before_aging: 30m # 超过 30 分钟开始提优先级
quota: { team-a: 12, team-b: 12 } # 按团队配额,防止独占
backfill: true # 允许小任务插空
duration_estimate: from_history # 用历史时长估计,而非用户申报
碎片问题可以量化成一个"可排程度"指标:把空闲卡按机器分组,统计"能凑出的整机数"。这个数比"总空闲卡数"有用得多。
// 碎片量化(4 台 × 8 卡)
机器 空闲卡 能否满足 8 卡任务
node-1 2 否
node-2 3 否
node-3 8 是 ← 只有这台能
node-4 7 否
// 总空闲 20 卡,但只有 1 台能跑 8 卡任务 → "可排程度 = 1"
5. 利用率该怎么算
| 口径 | 含义 |
|---|---|
| 卡占用时长 / 总时长 | 常见的"利用率" |
| 有效计算 / 卡峰值 | 真正的算力使用 |
| 任务完成数 / 卡天数 | 产出视角 |
第一项会把"卡被占着但空转"算成利用:任务在等数据、在等通信、在 debug,卡都"被占用"。第二项(有效计算/峰值)需要 profiler 支持,成本较高;一个便宜的替代是看"训练吞吐(samples/s)是否达到该模型的经验值"——低于经验值 60% 就说明卡没被有效使用。
所以 68% 的占用可能对应 40% 的有效算力。
第三项最能反映价值:同样的卡天数,完成了多少任务。它把"利用率"和"产出"连起来。
还有一个"排队时间"的观测陷阱:看平均值会得到"排队 40 分钟"的乐观数字,而实际体验是"一半任务在 5 分钟内开始,另一半等了 6 小时"。所以排队必须看分位(P50/P90/P99),而不是均值。
// 排队时间的正确观测
P50: 3 分钟 ← 小任务,回填生效
P90: 2.1 小时 ← 中任务
P99: 7.4 小时 ← 大任务(8 卡 + 同域)
// 均值 40 分钟 → 完全掩盖了大任务的困境
6. 排错:五种"卡够但跑不起来"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 有空卡但任务排队 | 拓扑/卡数碎片 | 集中装箱 |
| 排队 4 小时但利用率 68% | 碎片 | 看碎片分布 |
| 8 卡任务总在等 | NVLink 域限制 | 按域调度 |
| 小任务挤占大任务 | 无优先级/配额 | 加老化与配额 |
| 训练慢但卡满 | 通信瓶颈 | 查拓扑与并行度 |
第三项(8 卡任务总在等)还可能是"没有按域调度":集群有 4 台 × 8 卡,但调度器把它们当成 32 个独立槽位,于是"凑不齐同一个域里的 8 张"。这在拓扑感知缺失时极常见。
第四项在共享集群里很常见:无数小任务把卡占满,大任务永远等。解法不是"提高大任务优先级",而是"配额 + 回填"——给大任务保留一部分卡,同时让小任务利用空隙。
7. 边界:什么情况不必复杂调度
| 场景 | 为什么 | 做法 |
|---|---|---|
| 单机 8 卡以内 | 无跨机调度 | 简单队列 |
| 任务都是同类同规模 | 无碎片问题 | FIFO 够 |
| 只有推理、无训练 | 可细粒度共享 | 按显存切 |
| 云上按需购买 | 无固定集群 | 按需扩容 |
判断口径一句话:排队时间是否主要来自"碎片"而不是"总量不足"。最后一条与"采购"有关的边界:先修调度,再买卡。因为"利用率 68% + 排队 4 小时"的集群,买卡后很可能变成"利用率 55% + 排队 4 小时"——碎片问题被放大而不是解决。
是,就该改调度;总量不足(卡就是不够),调度再优化也是排队。
8. 自测
- GPU 调度与 CPU 调度的根本差别是什么?为什么它决定了碎片问题?
- 三种碎片的形态是什么?为什么"集群里还有 8 张空闲卡"不等于"能跑 8 卡任务"?
- "集中装箱"与"均匀摊开"对碎片的影响为什么相反?
- 检查点抢占的前提是什么?"能存 checkpoint"与"存得够频繁"为什么是两件事?
- 常见的"利用率"口径会高估什么?哪个口径更能反映产出?
- 判断"是否需要复杂调度"的那个问题是什么?