让多个Pod共享同一张GPU的软件层分时方案
time-slicing(时间片共享) 是 NVIDIA device plugin 提供的一种 GPU 共享方式:通过配置,同一个 nvidia.com/gpu 资源被「复制」成多份,让多个 Pod 各自拿到一张「虚拟 GPU」,底层其实是同一张物理卡在多个进程之间轮转时间片。
它的机制非常朴素 —— device plugin 只是把可分配的数量改掉:
节点有 8 张物理 GPU,replicas 设为 10
→ 插件向 Kubernetes 宣告 80 个 nvidia.com/gpu
→ kubectl describe node 里显示 Capacity: nvidia.com/gpu: 80
→ 调度器认为这台机器有 80 张卡,可以往上塞 80 个 Pod
这也是理解所有坑的起点:Kubernetes 侧的「卡数」与物理现实脱钩了。
与另外两条路线的对比:
MIG 硬件切分,独立显存/SM/故障域,隔离性最好;几何固定,不能借用邻居
time-slicing 软件分时,所有 GPU 通用,利用率最高;无显存隔离、无故障隔离
MPS 软件空间分区,可限制每个客户端的显存与算力,吞吐优于分时
device plugin 侧自 v0.15.0 起标注为 experimental
device plugin 的配置文件(不是 CRD,是一段 YAML):
version: v1
sharing:
timeSlicing:
renameByDefault: false
failRequestsGreaterThanOne: false
resources:
- name: nvidia.com/gpu
replicas: 4
三个字段的语义:
resources[].name 要共享的资源名,通常是 nvidia.com/gpu
也支持 mixed MIG 策略产生的 nvidia.com/mig-* 资源名
resources[].replicas 复制份数。宣告数量 = 物理设备数 × replicas
renameByDefault true 时资源改名为 <name>.shared(即 nvidia.com/gpu.shared)
默认 false,即共享与独占混在同一个资源名里,无法区分
failRequestsGreaterThanOne
请求超过 1 个共享资源时直接判定失败(默认 false)
renameByDefault 回答了「要不要单独的资源名」这个问题:默认不分离 —— 开了共享之后,nvidia.com/gpu 既代表整卡也代表共享份额,调度侧无法区分一个 Pod 要的是独占还是共享。需要区分(例如只想让一部分业务共享、另一部分独占)时才打开它,共享池变成 nvidia.com/gpu.shared。
failRequestsGreaterThanOne 官方推荐打开,但默认是 false 以保持向后兼容;打开后,请求 nvidia.com/gpu: 2 的 Pod 会分配失败并进入 UnexpectedAdmissionError,需要手工删除或更新。
两条硬限制:
GPU Operator 的做法是:先建一个 ConfigMap,再让 ClusterPolicy 指向它。ConfigMap 的名字由你自己定(官方示例用 time-slicing-config),但必须与 GPU Operator 在同一个命名空间。
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config
namespace: gpu-operator
data:
any: |-
version: v1
flags:
migStrategy: none
sharing:
timeSlicing:
resources:
- name: nvidia.com/gpu
replicas: 4
安装时指定:
helm upgrade --install gpu-operator nvidia/gpu-operator -n gpu-operator \
--set devicePlugin.config.name=time-slicing-config
装好之后改 ClusterPolicy(把 default 指向 ConfigMap 里的键名,即可全局生效):
kubectl patch clusterpolicies.nvidia.com/cluster-policy -n gpu-operator --type merge \
-p '{"spec":{"devicePlugin":{"config":{"name":"time-slicing-config","default":"any"}}}}'
不设 default 时不会全局生效,必须用节点标签逐台指定用哪个配置键:
kubectl label node gpu-node-1 nvidia.com/device-plugin.config=any
配套的 GFD(GPU Feature Discovery)会写出描述共享状态的标签,可用于调度约束:
nvidia.com/gpu.sharing-strategy none / mps / time-slicing
nvidia.com/<resource-name>.replicas 复制份数
nvidia.com/gpu.product 开启 renameByDefault 或共享后名字会带 -SHARED 后缀
# 节点上宣告的数量
kubectl describe node gpu-node-1 | grep -A15 "Capacity:"
kubectl get node gpu-node-1 -o json | jq '.status.capacity | with_entries(select(.key|test("nvidia")))'
# 谁在共享同一张卡
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.containers[].resources.limits["nvidia.com/gpu"]!=null) | "\(.metadata.namespace)/\(.metadata.name)"'
# 真正落在同一张卡上的进程
kubectl exec -it <pod> -- nvidia-smi
# 会看到多个进程共享同一块 GPU 的显存与算力
# device plugin 日志
kubectl logs -n gpu-operator ds/nvidia-device-plugin-daemonset --tail=50
GPU Operator 不会监听 ConfigMap 的变化。 改完 time-slicing-config 之后,device plugin 不会自动感知,必须手工滚动重启:
kubectl rollout restart -n gpu-operator daemonset/nvidia-device-plugin-daemonset
已经在运行的 Pod 不受影响,继续运行;新的配置只作用于后续的分配。官方建议把这类变更安排在维护窗口。相对地,改 ClusterPolicy 的 devicePlugin.config 字段会由 Operator 主动触发 device plugin 与 GFD 的滚动重启。
如果用的是独立的 device plugin Helm chart(不经 GPU Operator),机制不同:chart 会部署一个 config-manager sidecar 监听节点标签 nvidia.com/device-plugin.config,变更时向插件进程发 SIGHUP 热加载,不重启 Pod。
MPS(CUDA Multi-Process Service)在配置上长得几乎一样,只是把 timeSlicing 换成 mps:
version: v1
sharing:
mps:
renameByDefault: false
resources:
- name: nvidia.com/gpu
replicas: 4
区别在于底层机制:MPS 会启动一个 control daemon 管理对 GPU 的访问,做的是「空间分区」 —— 可以显式限制每个客户端能用的显存与算力并强制执行,每个客户端拿到的是总资源的 1/replicas。因此 MPS 的吞吐与延迟表现通常好于 time-slicing。
但 MPS 有两个限制:
使用 MPS 时,per-client 的资源上限由两级控制,且环境变量只能进一步收紧,不能放宽:daemon 侧设置默认值(device plugin 会设成总量的 1/replicas),容器里还可以用
CUDA_MPS_ACTIVE_THREAD_PERCENTAGE 客户端上下文可用的线程百分比
CUDA_MPS_PINNED_DEVICE_MEM_LIMIT 客户端可分配的显存上限,语法同 CUDA_VISIBLE_DEVICES
独立 Helm chart 部署 MPS 时会带出 control daemon 的 DaemonSet,默认路径 /run/nvidia/mps,并且需要 hostPID: true(让 MPS server 通过 /proc/self 找到自己的 PID)。
CUDA out of memory,同卡上其他 Pod 的进程会一起被杀掉 —— 即使它们只申请了 1/4 的份额。replicas: 4 只是把可分配数量变成 4 倍,并没有把 80GB 切成 4×20GB。一个 Pod 完全可以吃掉整块显存,直到把邻居撑爆。nvidia.com/gpu 的数量会变得不可信。describe node 显示的 80 张卡是虚构的,容量规划、告警阈值、HPA 的容量判断都会因此失真,排障时先确认节点是不是开了共享。renameByDefault 一旦启用,清单里的资源名要跟着改。共享池变成 nvidia.com/gpu.shared,老的 nvidia.com/gpu 清单会去抢独占卡(或直接 Pending),这类改动必须同步所有工作负载与配额(ResourceQuota 里的 requests.nvidia.com/gpu 也要改)。nvidia-smi 会列出同卡的所有进程,容器内的 CUDA_VISIBLE_DEVICES 也只有一张卡的编号 —— 排查「为什么我的显存不够」时不要被这些假象带偏。kubectl — Kubernetes集群管理工具gpu-mig — 硬件切分,真隔离;与分时是两条路线nvidia-device-plugin — 共享配置的提供方gpu-operator — 通过ClusterPolicy下发共享配置dcgm-exporter — 分时场景下无法按容器归属指标resource-quota — 共享资源数量变化后需同步配额node — 节点标签决定用哪个共享配置pod — 共享导致的OOM与崩溃排障