Kubernetes GPU 自动伸缩:Karpenter、KAI Scheduler 与 G...


文档摘要

Kubernetes GPU 自动伸缩:Karpenter、KAI Scheduler 与 Gang Scheduling 本节摘要:GPU 自动伸缩不是一层,是三层。Karpenter 负责动态供给节点(1 分钟内,比 Cluster Autoscaler 快约 40%);KAI Scheduler 处理 gang 调度、拓扑感知与分层队列——它阻止「8 卡只来了 7 张、其余 7 张空转等最后一张」的局部分配陷阱;应用层自动伸缩器(NVIDIA Dynamo Planner、llm-d Workload Variant Autoscaler)依据推理专用信号(队列深度、KV 缓存利用率)做伸缩,而非 CPU 或 DCGM 占空比。

Kubernetes GPU 自动伸缩:Karpenter、KAI Scheduler 与 Gang Scheduling

本节摘要:GPU 自动伸缩不是一层,是三层。Karpenter 负责动态供给节点(1 分钟内,比 Cluster Autoscaler 快约 40%);KAI Scheduler 处理 gang 调度、拓扑感知与分层队列——它阻止「8 卡只来了 7 张、其余 7 张空转等最后一张」的局部分配陷阱;应用层自动伸缩器(NVIDIA Dynamo Planner、llm-d Workload Variant Autoscaler)依据推理专用信号(队列深度、KV 缓存利用率)做伸缩,而非 CPU 或 DCGM 占空比。经典的 HPA 陷阱在于:DCGM_FI_DEV_GPU_UTIL 是占空比度量——100% 可能是 10 个请求也可能是 100 个;vLLM 又会预分配 KV 缓存内存,所以内存指标永远不触发缩容。本节教你把三层组合起来,并避开 Karpenter 默认 WhenEmptyOrUnderutilized 策略——它会中途干掉正在推理的 GPU 任务。

对应原课程:Phase 17 · Lesson 03 · 03-gpu-autoscaling-kubernetes(原英文 phases/17-infrastructure-and-production/03-gpu-autoscaling-kubernetes/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 画出三层自动伸缩(节点供给、gang 调度、应用层),并说出每层用的工具。
  2. 解释为什么 DCGM_FI_DEV_GPU_UTIL 是 vLLM 错误的 HPA 信号,并说出两个替代(队列深度、KV 缓存利用率)。
  3. 描述 gang 调度,以及 KAI Scheduler 阻止的局部分配失败模式(8 卡只用 7 张空转)。
  4. 点名会终止运行中 GPU 任务的 Karpenter 合并策略(WhenEmptyOrUnderutilized),并说出 2026 年的安全替代。

一、问题与直觉

你的团队在 Kubernetes 上线了一个 LLM 服务。你用 DCGM_FI_DEV_GPU_UTIL 做 HPA 信号。白天业务高峰,服务一直顶在 100% 利用率。HPA 却从不扩容——它以为你已经满了。你手动加副本,TTFT 降下来了。HPA 还是不动。信号在骗你。

与此同时,你用 Cluster Autoscaler 管节点。凌晨两点来了一个百万 token 的长提示,集群花 3 分钟供给一个节点,请求超时。

再来,你部署一个要 8 张 GPU、跨 2 个节点的 70B 模型。集群有 7 张空闲 GPU,还有 1 张散落在 3 个节点上。Cluster Autoscaler 为那 1 张缺失的 GPU 供给一个新节点。于是 7 个节点空等 4 分钟烧钱,等 Kubernetes 把最后那张 GPU 拉起来。

三层,三种失败模式。2026 年的 GPU 感知自动伸缩不是「打开 HPA」,而是把节点供给、gang 调度、应用信号伸缩组合起来。

二、核心概念

第一层 —— 节点供给(Karpenter)

Karpenter 盯着 pending 的 pod,约 4560 秒内供给节点(Cluster Autoscaler 对 GPU 节点通常要 90120 秒)。它按 NodePool 约束动态选实例类型——如果你的 pod 要 8 张 H100 而集群没有匹配节点,Karpenter 直接供给一个,而不是扩某个现有节点组。

合并陷阱:Karpenter 默认 consolidationPolicy: WhenEmptyOrUnderutilized 对 GPU 池很危险。它会终止正在运行的 GPU 节点,把 pod 迁到更便宜的合适实例。对推理工作负载,这意味着驱逐正在跑的请求、在新节点重载 70B 模型——损失是几分钟容量 + 请求失败。

GPU 池的安全设置:

disruption: consolidationPolicy: WhenEmpty # 只合并真正空的节点 consolidateAfter: 1h # 空了 1 小时才动手

这让 Karpenter 在节点真空闲一小时后合并,但绝不驱逐正在运行的任务

第二层 —— gang 调度(KAI Scheduler)

KAI Scheduler(项目曾名 "Karp",后改名)处理默认 kube-scheduler 不做的事:

Gang 调度 —— 全有或全无。一个要 8 张 GPU 的分布式推理 pod,要么 8 张一起启动,要么都不启动。没这个,你就会掉进局部分配陷阱:8 个 pod 起了 7 个,无限等待,烧钱。

拓扑感知 —— 知道哪些 GPU 共享 NVLink、哪些在同一机柜、哪些之间有 InfiniBand,据此放置 pod。DeepSeek-V3 67B 的张量并行工作负载必须留在一个 NVLink 域内,KAI Scheduler 尊重这一点。

分层队列 —— 多个团队抢同一个 GPU 池,带优先级与配额。团队 A 的生产吃紧,只有在优先级规则允许时才会被团队 B 的训练任务抢占。

KAI 作为二级调度器与 kube-scheduler 并存;你给工作负载打注解指定用它。Ray 和 vLLM production-stack 都已集成。

第三层 —— 应用层信号

HPA 陷阱:DCGM_FI_DEV_GPU_UTIL 是占空比指标——它度量的是每个采样间隔里 GPU「有没有在干活」。100% 利用率可能意味着 10 个并发请求,也可能 100 个——GPU 两种情况都在忙。按占空比伸缩等于闭眼伸缩

更糟的是,vLLM 这类引擎会预分配 KV 缓存内存(直到 --gpu-memory-utilization)。于是哪怕只有 1 个请求,内存占用也接近 90%。基于内存的 HPA 永远不缩容。

2026 年的替代信号:

  • 队列深度:等待 prefill 的请求数。
  • KV 缓存利用率:有多少比例的 block 分配给了活跃序列。
  • 每副本 P99 TTFT:你的 SLA 信号。
  • Goodput:每秒满足所有 SLO 的请求数。

NVIDIA Dynamo Planner 与 llm-d Workload Variant Autoscaler 消费这些信号来伸缩副本,在 LLM 服务场景里完全取代 HPA

各层该用谁

伸缩决策 工具
加/减节点 Karpenter
调度多 GPU 任务 KAI Scheduler
加/减副本 Dynamo Planner / llm-d WVA(或基于队列深度的自定义 HPA)
选 GPU 型号 Karpenter NodePool
抢占低优先级 KAI Scheduler 队列

分离式 prefill/decode 把一切搞复杂

如果你跑分离式 prefill/decode(第 17 节),你有两类 pod、不同的伸缩触发器:prefill pod 按队列深度伸缩,decode pod 按 KV 缓存压力伸缩。llm-d 把它们暴露成不同的 Service,各带按角色的 HPA。别在两者前面塞一个 HPA

冷启动在这里也要算

冷启动缓解(第 10 节)是节点供给时间变得用户可见的地方。Karpenter 的 4560 秒预热 + 20GB 模型加载 + 引擎初始化,意味着一个从零开始的请求要 25 分钟。对 SLO 关键路径保留暖池(min_workers=1),或在应用层用 Modal 式的 checkpoint 恢复。

你该记住的数字

  • Karpenter 节点供给:约 4560 秒 vs Cluster Autoscaler 约 90120 秒(GPU 节点)。
  • KAI Scheduler 阻止局部分配浪费——8 取 7 陷阱。
  • DCGM_FI_DEV_GPU_UTIL 作 HPA 信号:坏的;改用队列深度或 KV 利用率。
  • Karpenter WhenEmptyOrUnderutilized:会终止运行中的 GPU 任务。推理用 WhenEmpty + consolidateAfter: 1h

三、从零实现:队列深度伸缩器模拟

原课程 code/main.py 在突发 GPU 工作负载上模拟三层伸缩器,对比朴素 HPA(占空比)、队列深度 HPA、KAI gang 调度伸缩,报告未满足请求数、空闲 GPU 分钟与综合分。下面给一个最小可读的队列深度伸缩器骨架。

def queue_depth_autoscaler(arrival_rate, service_rate_per_replica, target_queue_depth=4, min_replicas=1, max_replicas=16): """按队列深度决定副本数。 参数: arrival_rate: 请求/秒 service_rate_per_replica: 单副本请求/秒 target_queue_depth: 目标队列深度(超过就扩) min/max_replicas: 副本上下限 返回: (desired_replicas, est_queue_depth) """ load_ratio = arrival_rate / service_rate_per_replica needed_for_load = max(min_replicas, int(load_ratio) + 1) # 排队估算:Little 定律 L = λ * W,这里用 load_ratio 超出整数部分的余量近似 est_queue = max(0.0, (load_ratio - int(load_ratio)) * 10) scale_for_queue = 0 if est_queue > target_queue_depth: scale_for_queue = 1 desired = min(max_replicas, max(needed_for_load + scale_for_queue, min_replicas)) return desired, round(est_queue, 2) # 突发场景:平时 5 req/s,峰值 40 req/s,单副本 4 req/s for label, rate in [("平稳", 5), ("峰值", 40)]: n, q = queue_depth_autoscaler(rate, 4) print(f"{label}: 到达 {rate} req/s → 期望副本 {n}, 估算队列 {q}")

对比「占空比 HPA」:同一份数据,占空比在 5 req/s 和 40 req/s 时都可能显示 100%(GPU 都在忙),于是两者都报「不需要扩容」。这就是为什么队列深度比占空比更诚实地反映真实压力

💡 HPA 替换不是「调阈值」,是「换信号」。先确认你的伸缩信号与用户体感(TTFT、排队)线性相关,再谈自动化——否则你只是在自动放大一个错误的判断。

四、框架对比:三层工具横向对照

工具 解决什么 经典陷阱
节点供给 Karpenter 45~60s 动态供节点,按 NodePool 选实例 默认 WhenEmptyOrUnderutilized 会杀运行中 GPU 任务
(对比) Cluster Autoscaler 90~120s,基于节点组扩容 慢,且不适配 GPU 拓扑
调度 KAI Scheduler gang + 拓扑 + 分层队列 默认 kube-scheduler 无 gang,会局部分配
应用伸缩 Dynamo Planner / llm-d WVA 按队列深度/KV 利用率扩副本 DCGM_FI_DEV_GPU_UTIL 的 HPA 失真

组合心法:Karpenter 管节点生死、KAI 管多卡任务原子调度、应用层管副本数——三者职责不重叠,缺一不可。任何一层用默认值都可能让你在半夜被叫醒。

五、可复用产物

本节产出 outputs/skill-gpu-autoscaler-plan.md(原课程目录)。给定集群拓扑、工作负载形状、SLO,它会:

  1. 设计三层伸缩计划:NodePool 配置(含安全合并策略)、KAI 调度注解、应用层信号选择。
  2. 给出分离式 prefill/decode 的按角色 HPA配置骨架。
  3. 标注冷启动预算:哪些路径必须暖池,哪些可接受 2~5 分钟冷启。
  4. 列出该避开的默认值:Karpenter 合并策略、DCGM 作 HPA、单一 HPA 罩两类 pod。

六、练习

  1. 跑通模拟器:运行 code/main.py。突发工作负载下,朴素占空比 HPA 会丢多少请求?队列深度 HPA 能接住多少?差异从哪来?

  2. 设计 NodePool:为「Llama 3.3 70B FP8 on H100 SXM5」集群设计 Karpenter NodePool,写出 capacity-typedisruption.consolidationPolicyconsolidateAfter,以及一个把非 GPU 工作负载挡在这些节点外的污点。

  3. 诊断 Pending:团队报部署卡在 Pending,提示「GPU 可用但 pod 不调度」。是 Karpenter、kube-scheduler 还是 KAI Scheduler 的问题?哪些指标能确认?

  4. 分离式信号:为分离式 prefill pod 选一个伸缩信号,为 decode pod 选另一个,分别论证。

  5. 算损失:一个 7×24 生产服务平均每天 60 次「P99 TTFT > 10s 的掉请求事件」,源于 WhenEmptyOrUnderutilized 合并陷阱。按你估的每次事件损失(失败请求 × 单请求收入 + SLA 罚款)算 24 小时成本,看改 WhenEmpty + 1h 多久回本。

本节要点回顾

  1. 三层而非一层:节点供给(Karpenter)、gang 调度(KAI)、应用信号(Dynamo Planner/llm-d WVA),各管一摊。
  2. DCGM_FI_DEV_GPU_UTIL 是占空比:100% 可能是 10 或 100 请求,作 HPA 信号等于闭眼伸缩。
  3. vLLM 预分配 KV 缓存:内存长期近 90%,基于内存的 HPA 永不缩容。
  4. 正确的伸缩信号:队列深度(prefill 瓶颈)、KV 缓存利用率(decode 瓶颈)、P99 TTFT、Goodput。
  5. Gang 调度防局部分配:8 卡只来 7 就空转,KAI 让全有或全无。
  6. Karpenter 默认合并策略有毒:WhenEmptyOrUnderutilized 会杀运行中 GPU 任务,推理改 WhenEmpty + consolidateAfter: 1h
  7. 分离式架构别用单 HPA:prefill 与 decode 信号不同,各带按角色 HPA。
  8. 冷启动预算要算进 SLO:Karpenter 4560s + 模型加载 + 引擎初始化 = 25 分钟,关键路径留暖池。

下一节,我们钻进 vLLM 引擎内部——看 PagedAttention、连续批处理(continuous batching)、分块 prefill(chunked prefill)如何让一块 GPU 同时高效服务几十个请求。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U