时间分片、MPS、MIG
一句话总结
时间切片中的 replicas: 5 不是把 GPU 切成五块,而是把同一设备注册五次。只有调度器看到的插槽数量扩大五倍,VRAM 仍然是一个未分割的整体。
为什么会想共享 GPU
购买 GPU 后,很快就会看到这样的图表:日均利用率只有 8%,等待队列却始终排满。原因很简单——一个 Pod 会独占一整块 GPU。打开 Jupyter Notebook 后一直阅读代码的人,以及每秒只接收一次推理请求的服务,即使几乎不使用 GPU,也会持续占用一整块设备。
于是产生了“让多人共享一块 GPU”的需求,解决方式有三种。它们性质完全不同,却因为名称相似而经常被混淆。
工作原理
| 方式 | 划分什么 | 内存隔离 | 故障隔离 | 必要条件 |
|---|---|---|---|---|
| 时间切片 | 仅执行时间(上下文切换) | 无 | 无 | 任意 GPU |
| MPS | 时间 + 内核并发执行 | 可以设置上限,但不是强制隔离 | 有限 | 需要守护进程 |
| MIG | 硬件分区 | 有 | 有 | A100、H100 级别 |
时间切片只需一份 device plugin 配置即可启用。
sharing:
timeSlicing:
failRequestsGreaterThanOne: true
resources:
- name: nvidia.com/gpu
replicas: 5
启用后,节点通告的 nvidia.com/gpu 数量会变成物理设备数 × 5。若有 4 块 GPU,就会显示 20。调度器相信有 20 个插槽,于是调度 20 个 Pod。但这 20 个 Pod 实际面对的仍然是 4 块 GPU;连接到同一块 GPU 的五个 Pod 会重叠使用同一份 40GiB VRAM。每个插槽获得保证的内存为 0。
因此会出现这种情况:五个用户中的一个加载大型模型,占用 38GiB,其余四个 Pod 会在保持 Running 的同时遇到 CUDA OOM。调度器没有做错——系统承诺提供插槽,它也确实提供了插槽;只是从未承诺提供内存。
failRequestsGreaterThanOne: true 是为了尽量减少这种误解。Pod 请求 nvidia.com/gpu: 2 时会被拒绝,因为即使拿到两个切片,也不表示拥有“两块 GPU 的容量”。最好保持该开关启用。
MPS 会把多个进程的 CUDA 内核合并到一个上下文中真正并发执行。省去上下文切换开销后,吞吐量会提高,还可以为每个进程设置内存上限。不过,这种上限更接近协作式限制,一个进程崩溃时仍可能影响其他进程。
只有 MIG 提供硬件级分割。它会物理划分 SM 与内存控制器,让每个实例拥有自己的 VRAM。无论相邻实例做什么,都不会影响当前实例。但它需要受支持的硬件,分区配置受固定规格限制,不能任意划分,而且更换规格前必须清空该 GPU 上的工作负载。
生产现场中的常见情况
第一,判断标准只有一个:“这些工作负载是否可以互相拖垮?”开发 Notebook、内部实验、演示使用时间切片就足够,收益也很明显。如果其中混有生产推理服务,没有隔离的共享迟早会导致事故。即使在同一集群中,通常也会划分节点池,为不同节点应用不同策略。
第二,不要被利用率图表欺骗。 启用时间切片后,GPU 利用率指标会变得很好看。但这个数字只表示“有人在运行内核”,并不表示“任务完成得更快”。五个任务轮流使用 GPU,每个任务都会变慢。真正应该观察的是任务完成时间和 OOM 发生次数,而不是利用率。
第三,明确记录向用户承诺了什么。 启用共享的那一刻起,nvidia.com/gpu: 1 请求的含义就改变了。昨天它表示“一整块 GPU”,今天则表示“一个等待轮到自己的位置”。如果不公告这项变化,用户永远不会知道任务为何变慢,只会不断怀疑基础设施。
后续文章要看什么
这类共享配置最终会通过 DaemonSet 滚动更新传递到节点。下一篇文章将介绍:当滚动更新停在某个节点时,集群会保留为什么样的状态,以及该状态为何不是错误,而是符合设计的行为。