K8s中GPU容器共享技术
这篇文章记录在 Kubernetes 集群里落地 GPU 容器共享技术的完整过程。重点不是介绍概念,而是从一个真实的调度故障出发,完整复盘调研、决策、配置、踩坑、修正的全过程:集群里有哪些 GPU、各自支持什么共享方式、Time-Slicing 和 MIG 的原理与差异、为什么当前只能做 Time-Slicing 而 MIG 是中期目标,以及从 2 个物理 GPU slot 扩展到 11 个共享 slot 的每一步操作细节。
GPU 是高价稀缺资源。Kubernetes 默认行为是每个 Pod 独占一张完整物理 GPU,哪怕业务只用了 5% 的算力。随着 LLM 推理、NLP、PII 识别等 GPU 服务并发增多,GPU 利用率低下成了显著的成本浪费,也带来了实际的调度问题。
某生产集群共 8 个节点,其中 2 个挂有 GPU。某 NLP 推理服务申请了多个副本,每个副本 nvidia.com/gpu: 1,但集群里只有 2 张 GPU 且全被占满,导致新副本持续 Pending 超过 28 天,事件日志一直报 0/8 nodes are available: 8 Insufficient nvidia.com/gpu。这个问题直接催生了对 GPU 共享技术的调研。
NVIDIA 官方提供了两种在 Kubernetes 里共享 GPU 的主流方案:Time-Slicing 分时共享和 MIG 硬件虚拟化。二者的适用场景、配置方式和硬件前提差异很大,需要先摸清实际硬件再做决策。
集群 8 个节点全部 Ready,运行在腾讯云 TKE。通过节点 label nvidia-device-enable=enable 定位出 2 个 GPU 节点,再通过 privileged pod 进宿主机 namespace 执行 nvidia-smi -q 确认准确型号:
| 实例规格 | GPU 型号 | 架构 | 显存 | Compute Cap | Driver | MIG 支持 |
| PNV5b.8XLARGE96 | NVIDIA L20 | Ada Lovelace | 46068 MiB | 8.9 | 570.158.01 | ❌ |
| GN7.2XLARGE32 | Tesla T4 | Turing | 15360 MiB | 7.5 | 570.158.01 | ❌ |
两个节点驱动版本相同(CUDA 12.8),均不支持 MIG。值得注意的是,L20 的 Compute Capability 8.9 高于 Ampere(8.0),但 MIG 支持并不按 CC 连续覆盖——Ada Lovelace 的 L 系列整体不支持 MIG,Ampere(A100、A30)和 Hopper(H100、H20)才支持。
集群使用 TKE 原生的 nvidia-device-plugin:v0.14.5 以 DaemonSet 方式运行,启动参数为 --mig-strategy=single --fail-on-init-error=false --pass-device-specs=true,无 --config-file,无 time-slicing ConfigMap。每个 GPU 节点只向 K8s 注册 1 个 nvidia.com/gpu 资源,集群总共 2 个 GPU slot,且全部被占用。DaemonSet 元数据带有 meta.helm.sh/release-name: nvidia-gpu 注解,说明最初由 Helm 部署,但当前环境未安装 Helm CLI。
Time-Slicing 是 NVIDIA k8s-device-plugin 提供的过订阅能力。管理员通过 ConfigMap 声明每张 GPU 的副本数(replicas),Device Plugin 就把这张 GPU 以该数量向 K8s 注册为多个资源。本质是让多个 Pod 轮流使用同一张 GPU 的时间片,CUDA 调度器负责分时复用。
核心约束:第一,没有显存隔离,所有副本共享同一块物理显存,一个 Pod OOM 可能波及同 GPU 上的其他 Pod。第二,申请多个 shared GPU 不等于获得成比例的算力,GPU 只是把计算时间等分给所有进程。官方建议开启 failRequestsGreaterThanOne: true,让申请超过 1 个 shared GPU 的 Pod 直接报 UnexpectedAdmissionError,防止业务误解语义。
上下文切换开销约 10–15%,适用于工作负载轻量或突发型的场景。Kepler 及以上全系列均支持,配置无需重启节点。DCGM-Exporter 在 time-slicing 模式下无法将指标关联到容器,只能看到物理 GPU 层面的聚合数据——这是官方已知限制,不是配置问题。
MIG(Multi-Instance GPU)是 NVIDIA 从 Ampere 架构引入的硬件级 GPU 分割技术。物理 GPU 被切分为若干 GPU Instance(GI),每个 GI 拥有独立的 SM 切片、独立的显存分区(含 L2 cache 和带宽)、独立的 DMA 引擎,以及硬件层面的故障隔离——一个 GI 崩溃不影响其他 GI。这种隔离是 Time-Slicing 无法做到的。
MIG 实例对上层应用完全透明,在 K8s 里以独立资源类型呈现(如 nvidia.com/mig-1g.12gb)。上下文切换开销约 2–3%,性能接近物理独占。代价是硬件要求严格:只支持 Ampere 及以上(A100、A30、H100、H200、H20、B200 等)。修改 MIG Profile 时需要先 Drain 节点;Hopper+ 支持 GPU 热重置,不需要重启宿主机,Ampere 则需要停止所有 GPU 客户端。
| 维度 | Time-Slicing | MIG |
| 显存隔离 | 无,共享物理显存 | 硬件级,各实例独立 |
| 故障隔离 | 无 | 硬件级 |
| 算力保证 | 时间片等分,无保证 | 固定 SM 切片,有保证 |
| 性能开销 | 约 10–15% | 约 2–3% |
| GPU 支持范围 | Kepler+ 全系列 | Ampere+(A100、H100、H20 等,不含 L20) |
| DCGM 容器级监控 | 不支持 | 完整支持 |
| K8s 配置复杂度 | 低,修改 ConfigMap 即可 | 中,需要管理 GI 生命周期 |
| 可与对方叠加 | 可对 MIG 实例再做分时 | 不能与 MPS 同时使用 |
| 生产成熟度 | GA | GA |
H20 是 NVIDIA Hopper 架构(GH100,Compute Capability 9.0),96GB HBM3e 显存,最多可切分为 7 个 MIG 实例,是专为中国市场合规设计的出口管制版本。NVIDIA 官方 MIG User Guide(r580)明确列出: H20 Hopper GH100 9.0 96GB Max-Instances=7。
一张 H20 的常用切分方式:
| Profile | SM 占比 | 显存 | 单卡实例数 | 适用场景 |
| 1g.12gb | 1/7 | 12GB | 7 | ≤7B 小模型推理 |
| 2g.24gb | 2/7 | 24GB | 3 | ~13B 中型模型 |
| 3g.47gb | 3/7 | 47GB | 2 | ~30B 模型 |
| 4g.47gb | 4/7 | 47GB | 1 | 单实例大模型 |
| 7g.94gb | 7/7 | 94GB | 1 | 全卡独占(70B+) |
Time-Slicing 和 MIG 可以叠加:先用 MIG 切分 H20,再对某个 MIG 资源类型开启 time-slicing,进一步提升并发。叠加时 ConfigMap 里的 migStrategy 需设为 mixed:
|
1 2 3 4 5 |
sharing: timeSlicing: resources: - name: nvidia.com/mig-1g.12gb replicas: 2 |
ConfigMap 支持多个 key,每个 key 对应一种节点配置。 any 是兜底默认值,其余 key 通过节点 label 选择(见下文):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
apiVersion: v1 kind: ConfigMap metadata: name: time-slicing-config namespace: kube-system data: any: |- version: v1 flags: migStrategy: none sharing: timeSlicing: renameByDefault: false failRequestsGreaterThanOne: true resources: - name: nvidia.com/gpu replicas: 2 l20: |- version: v1 flags: migStrategy: none sharing: timeSlicing: renameByDefault: false failRequestsGreaterThanOne: true resources: - name: nvidia.com/gpu replicas: 8 t4: |- version: v1 flags: migStrategy: none sharing: timeSlicing: renameByDefault: false failRequestsGreaterThanOne: true resources: - name: nvidia.com/gpu replicas: 3 |
renameByDefault: false 时资源名保持 nvidia.com/gpu 不变,节点 label 会加 -SHARED 后缀(如 nvidia.com/gpu.product=Tesla-T4-SHARED),可用 nodeSelector 区分共享与非共享节点。replicas 数量根据单进程实测显存用量决定,参见实施记录。
v0.14.5 不支持 per-node 动态配置选择,需要两个 DaemonSet 各自用不同的 --config-file 指向不同的 ConfigMap key,通过 nodeSelector 绑定到对应型号节点:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# 给节点打型号 label kubectl label node <l20-node> nvidia.com/device-plugin.config=l20 kubectl label node <t4-node> nvidia.com/device-plugin.config=t4 # 修改原有 DaemonSet,使其只跑在 T4 节点,使用 t4 配置 kubectl patch daemonset nvidia-device-plugin-daemonset -n kube-system --type=json -p='[ {"op":"replace","path":"/spec/template/spec/containers/0/args/3", "value":"--config-file=/etc/nvidia/time-slicing-config/t4"}, {"op":"add","path":"/spec/template/spec/nodeSelector/nvidia.com~1device-plugin.config", "value":"t4"} ]' kubectl rollout restart daemonset/nvidia-device-plugin-daemonset -n kube-system |
L20 专用 DaemonSet(共享同一 ConfigMap,使用 l20 key):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
apiVersion: apps/v1 kind: DaemonSet metadata: name: nvidia-device-plugin-daemonset-l20 namespace: kube-system spec: selector: matchLabels: name: nvidia-device-plugin-ds-l20 updateStrategy: type: RollingUpdate template: metadata: labels: name: nvidia-device-plugin-ds-l20 spec: nodeSelector: nvidia-device-enable: enable nvidia.com/device-plugin.config: l20 tolerations: - operator: Exists priorityClassName: system-node-critical containers: - name: nvidia-device-plugin-ctr image: sgccr.ccs.tencentyun.com/tkeimages/nvidia-device-plugin:v0.14.5 command: [nvidia-device-plugin] args: - --fail-on-init-error=false - --mig-strategy=single - --pass-device-specs=true - --config-file=/etc/nvidia/time-slicing-config/l20 env: - name: NVIDIA_DRIVER_CAPABILITIES value: utility,compute resources: limits: cpu: 100m memory: 100Mi requests: cpu: 100m memory: 100Mi securityContext: capabilities: drop: [ALL] volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: time-slicing-config mountPath: /etc/nvidia/time-slicing-config volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: time-slicing-config configMap: name: time-slicing-config |
Hopper 架构支持 GPU 热重置,配置流程:先 Drain 节点,SSH 进入启用 MIG 并创建实例,再 Uncordon:
|
1 2 3 4 5 6 7 8 |
kubectl drain <h20-node> --ignore-daemonsets --delete-emptydir-data # SSH 进入 H20 节点 sudo nvidia-smi -mig 1 # 启用 MIG 模式 sudo nvidia-smi mig -cgi 19,19,19,19,19,19,19 -C # 创建 7 个 1g.12gb 实例(profile ID=19) nvidia-smi -L # 确认 7 个 MIG Device kubectl uncordon <h20-node> |
K8s 侧三种 MIG 策略: single(全节点同一 Profile,资源类型仍为 nvidia.com/gpu,workload 无需改动)、 mixed(多 Profile,资源类型变为 nvidia.com/mig-1g.12gb 等,Pod 需显式申请)、 none(不暴露 MIG,兼容旧 workload)。推荐通过 Helm 部署,避免手动 patch:
|
1 2 3 4 5 6 |
helm upgrade -i nvdp nvdp/nvidia-device-plugin \ --version=0.17.1 \ --namespace nvidia-device-plugin \ --create-namespace \ --set migStrategy=single \ --set gfd.enabled=true |
|
1 2 3 4 5 6 |
# 查看 DaemonSet 启动参数 kubectl get daemonset nvidia-device-plugin-daemonset -n kube-system -o yaml | grep -A 15 "containers:" # 确认是否已有 time-slicing ConfigMap kubectl get configmap -n kube-system | grep nvidia # 当前 GPU slot 数 kubectl get nodes -o custom-columns="NAME:.metadata.name,GPU:.status.capacity.nvidia\.com/gpu" |
结果:无 ConfigMap,无 --config-file,每个 GPU 节点 1 个 slot,集群共 2 个。
先以 any key(replicas=2)快速验证流程,之后再按型号差异化:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
kubectl apply -f time-slicing-config.yaml # 见"配置参考"章节 kubectl patch daemonset nvidia-device-plugin-daemonset -n kube-system --type=json -p='[ {"op":"add","path":"/spec/template/spec/volumes/-", "value":{"name":"time-slicing-config","configMap":{"name":"time-slicing-config"}}}, {"op":"add","path":"/spec/template/spec/containers/0/volumeMounts/-", "value":{"name":"time-slicing-config","mountPath":"/etc/nvidia/time-slicing-config"}}, {"op":"add","path":"/spec/template/spec/containers/0/args/-", "value":"--config-file=/etc/nvidia/time-slicing-config/any"} ]' kubectl rollout restart daemonset/nvidia-device-plugin-daemonset -n kube-system kubectl rollout status daemonset/nvidia-device-plugin-daemonset -n kube-system --timeout=120s |
坑一:Device Plugin 不会自动 watch ConfigMap 变更。修改 ConfigMap 内容后,必须手动 rollout restart DaemonSet 才能生效,仅 kubectl edit configmap 不够。
滚动重启约 6 秒完成。验证 slot 扩容:
|
1 2 3 |
kubectl get nodes -o custom-columns="NAME:.metadata.name,GPU-CAP:.status.capacity.nvidia\.com/gpu,GPU-ALLOC:.status.allocatable.nvidia\.com/gpu" # L20 节点 2 2 ← 从 1 扩到 2 # T4 节点 2 2 ← 从 1 扩到 2 |
Device Plugin 日志确认配置加载:
|
1 2 3 |
kubectl logs -n kube-system <device-plugin-pod> --tail=5 # "timeSlicing": {"failRequestsGreaterThanOne": true, "resources": [{"replicas": 2}]} # Registered device plugin for 'nvidia.com/gpu' with Kubelet |
坑二(意外发现):卡住的不只是 Pod,而是一次 28 天前的 Rolling Update。检查 ReplicaSet 才发现,Deployment 当时正在从老 RS 切换到新 RS,新 RS 需要 2 个 Pod,第一个因 GPU 不足 Pending,整个 Rolling Update 就此冻结。Deployment 默认策略是"先确保新 RS 可用再缩老 RS",新 RS 无法就绪,老 RS 的 Pod 也一直没被终止。表面看是"有个 Pod 一直 Pending",实质是一次版本发布卡了 28 天。Time-Slicing 生效后 43 秒内完成调度,Deployment Controller 立刻继续 Rolling Update,第二个新 RS Pod 随即调度,老 RS 缩容完成:
|
1 2 3 |
kubectl describe pod <pending-pod> -n <ns> | grep -A 3 "Events:" # Warning FailedScheduling (x1303 over 4d12h) 0/8 nodes are available: 8 Insufficient nvidia.com/gpu. # Normal Scheduled 43s Successfully assigned <pod> to <gpu-node> |
Time-Slicing 生效后,用 privileged pod 查两个节点的实际显存用量:
| GPU | 总显存 | 单进程实测用量 | 最终 replicas | 每 slot 理论余量 |
| NVIDIA L20 | 46068 MiB | 4621 MiB(约 10%) | 8 | ~1137 MiB |
| Tesla T4 | 15360 MiB | 4401 MiB(约 29%) | 3 | ~759 MiB |
L20 以 replicas=2 运行,每个 slot 有 22GB 空间,实际进程只用 4.6GB,GPU 容量利用率仅 10%,改为 8 个 slot 后利用率达 ~80%。
坑三:v0.14.5 不支持 --config-file 指向目录。文档中提到将 --config-file 指向目录并配合节点 label nvidia.com/device-plugin.config 实现 per-node 配置选择。实测在 v0.14.5 上直接崩溃:
|
1 2 3 |
# Pod CrashLoopBackOff,日志: # E unable to load config: unable to finalize config: unable to parse config file: # read error: read /etc/nvidia/time-slicing-config: is a directory |
这个 per-node 动态选择机制依赖 GPU Operator 的 config-manager sidecar,裸 device-plugin 不具备。解法是两个 DaemonSet,各自 nodeSelector 绑定 GPU 型号、各自 --config-file 指向对应 key 文件。完整 YAML 见"配置参考"章节。
最终 GPU slot 分配:
|
1 2 3 |
kubectl get nodes -o custom-columns="NAME:.metadata.name,GPU-CAP:.status.capacity.nvidia\.com/gpu,GPU-ALLOC:.status.allocatable.nvidia\.com/gpu" # L20 节点 8 8 ← 最终 8 个 slot # T4 节点 3 3 ← 最终 3 个 slot |
坑四:TKE 托管 DaemonSet 的 reconcile 风险。TKE 控制面在集群升级、节点组扩容等操作时可能重新 reconcile 原始 DaemonSet,将手动 patch 覆盖还原。目前缓解方式是把操作步骤文档化;彻底解法是通过 TKE 侧 GPU 共享功能或安装 GPU Operator,绕开直接修改托管资源的问题。
坑五:time-slicing 模式下无容器级 GPU 指标。集群已运行 nvidia-gpu-exporter,但 time-slicing 共享的 GPU 只能看到物理卡层面的聚合数据,无法区分具体 Pod 的显存和算力消耗。这是 DCGM 的已知限制,需要等 MIG 落地后才能解决。
| 项目 | 操作前 | 操作后 |
| GPU slot 总量 | 2(物理全卡) | 11(L20×8 + T4×3) |
| L20 利用率(显存维度) | ~10%(1 进程 / 46GB) | ~80%(8 slot 理论满载) |
| T4 利用率(显存维度) | ~29%(1 进程 / 15GB) | ~86%(3 slot 理论满载) |
| Pending pod | 1(持续 28 天) | 0 |
| 被卡住的 Rolling Update | 卡住 28 天 | 完成,新版本全部就绪 |
| DaemonSet 数量 | 1(通用) | 2(T4 专用 + L20 专用) |
| 显存 / 故障隔离 | 无 | 无(time-slicing 固有限制) |
| 容器级 GPU 指标 | 无 | 无(待 MIG 落地解决) |
NVIDIA MIG User Guide r580:https://docs.nvidia.com/datacenter/tesla/mig-user-guide/
GPU Operator — Time-Slicing GPUs in Kubernetes:https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html
NVIDIA k8s-device-plugin README (v0.14.5):https://github.com/NVIDIA/k8s-device-plugin/tree/v0.14.5
MIG Support in Kubernetes:https://docs.nvidia.com/datacenter/cloud-native/kubernetes/latest/index.html
腾讯云 GPU 计算型实例规格(PNV5b / GN7):https://www.tencentcloud.com/document/product/560/19701
Leave a Reply