跳到主要内容

批处理负载挤占了你的实时路径:GPU 预留的惨痛教训

· 阅读需 10 分钟
Tian Pan
Software Engineer

每晚的微调任务在 UTC 时间 02:00 开始。它进入共享 GPU 池,占用它能找到的每一个槽位并持续持有。到 09:30,当工作日的首波推理流量到达时,自动扩缩器(autoscaler)试图声明已被连续占用七个半小时的容量。早晨的前 90 分钟,系统运行在约为基准 p99 延迟四倍的水平上。仪表盘报告了一个“喧闹的早晨尾部(noisy morning tail)”,推理团队将其归因于用户行为,因为实际的资源争用发生在一个推理团队并不拥有的任务队列中。

这是你在容量评审的成本归因幻灯片中无法捕捉到的 GPU 共享失败模式。共享被宣传为利用率的胜利——晚上训练,白天服务,填补低谷。实际交付的却是直到池按延迟类别(而非按团队或按时间)进行分区之前,你都无法摆脱的延迟长尾。

在电子表格上,跨工作负载共享 GPU 的本能是正确的,但在排队论中结构性地错了。CPU 的上下文切换(context-switch)以微秒计;调度器可以在亚毫秒粒度上交错处理突发工作负载,而不会有人注意到。GPU 的上下文切换则需要几秒钟,因为必须被驱逐的工作集包括存储在 HBM 中的数 GB 权重、KV 缓存和优化器状态,而将其换出并载入新租户状态的成本是以墙上时钟(wall-clock)秒数来衡量的——而不是 CPU 世界教给我们的那些需要精打细算的周期。适用于无状态异构 CPU 工作负载的调度原语,无法推广到上下文切换成本占主导地位的加速器上。

训练和推理并非同一类工作负载

训练任务可以容忍长时间的无抢占持有。它批量处理前向和后向传播,预期连续几分钟到几小时占用同一块 GPU,并且愿意吸收排队延迟,因为释放并重新获取其权重和优化器状态的成本比等待的成本更高。任务所有者以吞吐量和每个训练 Token 的成本来衡量成功。其调度器希望最大化 GPU 执行有用工作的时间。

推理路径则完全相反。请求是短促、突发且对延迟敏感的。推荐后端可以在一分钟内看到十倍的流量激增,而聊天机器人用户会在三分钟的调度延迟结束前就关闭标签页。服务路径需要快速驱逐、突发余量和可预测的尾部延迟。其调度器希望 GPU 随时待命,而不是始终繁忙。

当两者共享一个预留时,训练调度器在每一次无竞争的准入中都会胜出,因为它先请求、请求更多,且持有时间更长。其结果并非公平分配,而是一种结构性倒置:不需要低延迟的工作负载消耗了对延迟敏感的工作负载所需要的预留容量。

Kubernetes 社区在 2024 年和 2025 年致力于解决这个问题——Kueue 的 gang 准入、Volcano 基于队列的优先级、Run:ai 式的 GPU 分数核算、1.31 版本中动态资源分配(DRA)的成熟——一贯的教训是,分区必须在准入阶段表达,而非在调度阶段表达。一旦批处理任务进入集群并持有 GPU,服务路径的驱逐选项就会受到迁出批处理租户成本的限制,而这个成本正是最初让你想要共享 GPU 的那个数字。

在深夜失效的成本归因技巧

证明共享池合理性的核算模型非常直观:训练和服务具有互补的昼夜分布,预留费用由服务的预算支付,训练则免费使用“空闲”容量。当“空闲”是 GPU 的一种属性且上下文切换成本近似为零时,该模型有效。

一旦早晨的流量攀升开始,它就不再奏效了。批处理租户并不会为它在服务路径上造成的延迟长尾付费,因为延迟长尾不会出现在批处理租户的成本行中。它出现在服务团队的 p99 仪表盘上、他们的值班疲劳中、他们的客户流失中——而容量模型不会将这些归因于批处理团队。服务团队正在消化一个批处理团队未计价的外部性(externality)。

试图通过一个“有礼貌”的批处理租户(在早晨流量攀升前释放容量)来解决此问题的本能,会遇到二阶问题。训练调度器不知道早晨的攀升何时到来,因为流量攀升是统计学上的,而非计划好的。即使在 UTC 09:00 释放,驱逐成本也会由下一个必须重新加载状态的训练步同步承担,这使得批处理租户的所有者不愿释放。即使他们释放了,推理自动扩缩器也必须获取并预热新的副本——KV 缓存、模型权重、JIT 编译的内核——这在冷 GPU 上需要数分钟,而这本身就是仪表盘会错误归因的延迟长尾事件。

分区必须是物理层面的

能够经受生产环境考验的解决方案是:停止将加速器容量视为一个同质化的资源池。它不是一个带有利用率指标的统一池。它至少是两个具有不同排队动态、不同驱逐成本和不同延迟契约的池。账务审计也必须随之跟进。

对于推理服务(Serving),预留资源是不可逾越的:无论利用率如何,都严禁批处理(Batch)任务占用。需要关注的指标不是推理预留池中的 GPU 利用率,而是针对推理路径在不溢出到更慢层级的情况下所能吸收的突发流量包络的余量(Headroom)。推理池 60% 的利用率并不是浪费——它是保护 p99 延迟免受下一次流量激增影响的余量。如果容量模型将其视为浪费,它最终会被批处理租户收回,而下一次流量激增将降临在一个无法吸收它的资源池上。

对于批处理,预留的形式截然相反:它竞价推理预留之外的 Spot 容量,将抢占视为正常的运行状态,并根据检查点恢复(Checkpoint-recovery)的开销而非完全预留 GPU 的成本进行定价。Volcano 的基于队列的准入和 Kueue 的队列组借用(Cohort borrowing)都体现了这一点——让批处理工作负载借用它可以被驱逐的容量,而绝不是推理路径 SLO 所依赖的容量。

让这一切得以强制执行的硬件特性是 MIG。将一块 H100 划分为七个实例,为每个实例分配专门的流式多处理器(SM)、L2 缓存、内存通道和一个故障域。隔离墙是物理层面的。一个分片上的训练租户无法驱逐另一个分片上的推理租户,因为硬件层面上不存在驱逐原语。你放弃了一个租户吸收另一个租户空闲容量的选择权,换取的是当邻居变动时依然丝毫不动的尾部延迟。对于具有 p99 SLO 的推理路径来说,这正是你想要的交易。

对于不需要 MIG 级别隔离的工作负载——开发集群、低关键性的批处理——时间分片(Time-slicing)是更便宜的选择。它在驱动层为你提供上下文切换,但在副本之间没有内存隔离。这对于开发命名空间来说没问题,但对于与训练任务共享容量的推理池来说则是灾难性的。分类标准不是“MIG vs 时间分片 vs 整卡 GPU”,而是“延迟 A 类获得物理隔离,延迟 B 类获得软共享,延迟 C 类运行在 Spot 实例上”,集群将这三者表现为调度器据此准入的独立设备类。

容量规划器必须停止对单一池建模

架构上的实现是,一个 AI 集群并不是一个拥有三种工作负载类型的资源池。它是拥有一个采购部门的三个独立资源池。将集群建模为具有利用率目标的单一池的容量规划器将不断建议进行整合,因为当延迟成本由其他部门承担时,整合看起来总是一个胜利。

守住底线的改变是从第一次预留开始,就将加速器容量建模为延迟等级分区:

  • 推理级 GPU 的目标是余量,而不是利用率。账务问题是“我们能吸收多少突发流量”,而不是“它有多满”。
  • 批处理级 GPU 的目标是吞吐量,并接受抢占。账务问题是“检查点恢复的成本是多少”,而不是“排队延迟是多少”。
  • Spot 或可中断容量是一个独立的类别,批处理可以竞价,而推理服务永远不会触及。

成本分摊模型必须紧随其后。导致推理路径出现尾部延迟事故的工作负载团队,就是其预算应为此买单的团队。如果批处理团队的事故成本项为零,因为推理团队的 p99 仪表盘吸收了它,那么批处理团队就会不断要求更多的共享容量,而推理团队将不断在职业倦怠中为此付出代价。一旦外部性被定价,批处理团队就会发现他们更倾向于分区容量,因为分区的成本比事故账单更便宜。

更深层次的认识是,GPU 共享并非只是成本系数不同的 CPU 共享模式。它是一种完全不同的运维准则。适用于无状态 CPU 工作负载的调度原语——公平共享、资源利用最大化队列(Work-conserving queues)、快速抢占——假设了 GPU 并不具备的上下文切换成本。将 GPU 共享定价为利用率胜利的团队,交付的是一个无法在软件中优化的延迟尾部。分区必须是物理层面的,账务必须是诚实的,容量规划器必须停止幻想一个池可以服务于两种契约。

早晨的尾部延迟在复盘报告中不会表现为调度决策。它会表现为“用户行为”或“间歇性供应商降级”或“自动扩缩容微调间隙”——除了“将两个排队动态从未兼容的工作负载共享预留空间”这一架构选择之外的任何原因。想要消除尾部延迟的团队必须明确这种选择,对资源池进行分区,并为外部性定价。在此之前,每天早上 09:30,推理团队都会为批处理团队的隔夜运行买单,而仪表盘不会告诉他们原因。

References:Let's stay in touch and Follow me for more thoughts and updates