Kubernetes GPU 超配:Time-Slicing、MIG 与队列回填
从 Kubernetes GPU 资源语义出发,对比队列回填、NVIDIA Time-Slicing、MIG 和运行时共享,并给出可执行配置、验证指标与回滚边界。
GPU 利用率低,不等于可以直接把一张卡声明成四张。先确认浪费发生在什么位置:任务排队方式不合理、显存长期占用但计算空闲,还是业务为了峰值申请了过多副本。原因不同,处理手段也不同。
Kubernetes 原生 GPU 通常以 nvidia.com/gpu 这类扩展资源出现。扩展资源按整数调度,不能像 CPU 那样原生超卖。节点有 8 张卡,默认最多只能同时满足 8 个 nvidia.com/gpu: 1 请求。要让更多 Pod 共享物理 GPU,必须由设备插件、运行时或硬件分区重新暴露资源;调度器本身不会自动完成显存隔离或算力限额。
先把四种方案分开
| 方案 | 是否同卡并发 | 隔离能力 | 适合场景 | 主要风险 |
|---|---|---|---|---|
| 队列回填、优先级、抢占 | 否 | 保持整卡隔离 | 训练、批处理、可中断任务 | 抢占成本、检查点恢复 |
| NVIDIA Time-Slicing | 是 | 无显存与故障隔离 | 开发环境、低风险推理、小任务 | OOM 相互影响、延迟抖动 |
| MIG | 是 | 硬件级计算与显存分区 | 多租户推理、稳定小规格任务 | 固定规格、资源碎片、重配成本 |
| MPS、vGPU 或框架级共享 | 是 | 取决于实现 | 需要更细粒度控制的固定负载 | 运维复杂、版本兼容、可观测性差异 |
如果问题只是“大任务挡住了后面的小任务”,先做队列治理,不要急着同卡共享。Kueue 的 Cohort 可以让队列借用空闲配额,配合优先级和抢占回收资源;这里借的是集群配额,不是把一张 GPU 的显存切成多份。
先建立共享前基线
至少采集一周数据,并覆盖发布、流量高峰和训练检查点阶段。只看 nvidia-smi 某一秒的利用率没有决策价值。
nvidia-smi \
--query-gpu=index,uuid,utilization.gpu,memory.used,memory.total,power.draw \
--format=csv
kubectl get pods -A --field-selector=status.phase=Pending
kubectl get events -A --sort-by='.lastTimestamp' | tail -n 100
kubectl describe node <gpu-node> | sed -n '/Capacity:/,/System Info:/p'
接入 DCGM Exporter 后,分别看持续利用率和峰值,而不是只看平均数:
avg_over_time(DCGM_FI_DEV_GPU_UTIL[1h])
max_over_time(DCGM_FI_DEV_FB_USED[24h])
max_over_time(DCGM_FI_DEV_MEM_COPY_UTIL[1h])
共享候选通常同时满足这些条件:
- GPU 计算存在稳定空档,不是采样误差。
- 显存峰值仍有余量,模型加载和突发 batch 不会把卡顶满。
- 任务可重试,单次失败不会造成数据损坏。
- 业务能接受一定尾延迟抖动。
- 已能关联 Pod、GPU、OOM、请求延迟和重试记录。
核心在线推理、长时间训练、无检查点任务,不应成为第一批试验对象。
用 Time-Slicing 做最小实验
下面使用独立资源名 nvidia.com/gpu.shared,避免共享 Pod 误落到整卡池。replicas: 4 表示每张物理 GPU 对外暴露四个可调度份额,不代表每个 Pod 获得四分之一显存或固定四分之一算力。
保存为 time-slicing-config.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config
namespace: gpu-operator
data:
shared: |-
version: v1
flags:
migStrategy: none
sharing:
timeSlicing:
renameByDefault: true
failRequestsGreaterThanOne: true
resources:
- name: nvidia.com/gpu
replicas: 4
应用配置,并让 GPU Operator 的 device plugin 使用它:
kubectl apply -f time-slicing-config.yaml
kubectl patch clusterpolicies.nvidia.com/cluster-policy \
-n gpu-operator --type merge \
-p '{"spec":{"devicePlugin":{"config":{"name":"time-slicing-config","default":"shared"}}}}'
kubectl get pods -n gpu-operator -w
如果只修改 ConfigMap,GPU Operator 不会自动监听并重启 device plugin。维护窗口内显式重启:
kubectl rollout restart -n gpu-operator daemonset/nvidia-device-plugin-daemonset
确认节点同时显示物理卡数量与共享资源容量:
kubectl get node <gpu-node> \
-o jsonpath='{.metadata.labels.nvidia\.com/gpu\.count}{" physical\n"}{.metadata.labels.nvidia\.com/gpu\.replicas}{" replicas\n"}{.status.allocatable.nvidia\.com/gpu\.shared}{" shared allocatable\n"}'
用四个 Pod 验证调度结果
先把试验限制在一台节点,不要直接改整个 GPU 池:
kubectl label node <gpu-node> gpu-mode=shared
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu-share-check
spec:
replicas: 4
selector:
matchLabels:
app: gpu-share-check
template:
metadata:
labels:
app: gpu-share-check
spec:
nodeSelector:
gpu-mode: shared
containers:
- name: check
image: nvcr.io/nvidia/cuda:12.8.1-base-ubuntu22.04
command: ["bash", "-lc"]
args: ["nvidia-smi -L && sleep infinity"]
resources:
limits:
nvidia.com/gpu.shared: 1
kubectl apply -f gpu-share-check.yaml
kubectl get pods -l app=gpu-share-check -o wide
kubectl exec deploy/gpu-share-check -- nvidia-smi -L
kubectl describe node <gpu-node> | sed -n '/Allocated resources:/,$p'
四个 Pod 能落到单卡节点,只能证明调度容量被放大。它不能证明显存有隔离,也不能证明业务吞吐一定提高。下一步必须运行真实模型并比较共享前后的每卡吞吐、P95/P99 延迟、OOM、Pod 重启和 GPU Xid 错误。
Time-Slicing 的边界
- 多个进程共享同一物理 GPU 和显存故障域,一个进程 OOM 可能影响同卡其他任务。
- 调度器只看到逻辑份额,看不到每个进程的实时显存峰值。
nvidia.com/gpu.shared: 2不代表获得两倍算力。示例中的failRequestsGreaterThanOne会直接拒绝这种请求。- DCGM 指标主要按物理 GPU 汇总。做 Pod 级归因时,需要结合进程、容器和业务指标。
- 高密度并发可能增加上下文切换,吞吐没有增加,尾延迟却先恶化。
因此,共享池要和整卡池分开。最简单的办法是使用独立节点标签、taint/toleration 和独立资源名;不要让延迟敏感服务靠“运气”避开共享节点。
什么时候改用 MIG
MIG 把支持的 NVIDIA GPU 切成固定硬件实例,每个实例拥有独立显存、缓存和计算资源。它比 Time-Slicing 更适合需要稳定延迟和租户隔离的推理服务,但会引入规格碎片:剩余资源不一定能拼成业务需要的 profile,重配也可能影响节点上现有任务。
选择 MIG 前先回答:
- 模型在目标 MIG profile 内能否加载,并保留足够 KV Cache 或训练峰值余量?
- 实例规格是否能覆盖常见模型,而不是为单个模型做一次性切分?
- 是否接受节点重配、驱逐和重新调度窗口?
- 监控、配额和容量报表能否识别 MIG 资源名?
如果这些问题还没有答案,先维持整卡或小规模 Time-Slicing,不要把硬件分区当成自动优化器。
发布与回滚门槛
从 replicas: 2 开始,只放可重试、低优先级任务。下面是一组试验门槛示例,数值应替换成业务自己的 SLO:
| 指标 | 继续扩大 | 立即回滚 |
|---|---|---|
| 每物理 GPU 有效吞吐 | 明显高于整卡基线 | 没提升或下降 |
| P99 延迟 | 仍在业务 SLO 内 | 连续超出 SLO |
| OOM、Pod 重启 | 不高于基线 | 同卡出现关联失败 |
| GPU Xid 错误 | 0 | 任意新增错误 |
| 排队时间 | 持续下降 | 只是把等待转成运行时抖动 |
回滚时恢复之前保存的 ClusterPolicy 配置,再重启 device plugin。不要在仍有共享任务运行时直接删除资源名;先停止新调度,排空共享节点,确认 Pod 已迁移,再恢复整卡配置。
最终判断
GPU 超配不是一个开关。优先级通常是:先修正 requests 和副本数,再做队列回填;仍有稳定空档时,给低风险任务试用 Time-Slicing;需要硬隔离时再评估 MIG 或 vGPU。
验收要看每张物理卡是否完成了更多有效工作,同时延迟、OOM、故障范围仍在可接受边界内。