一块 A100 80GB,只跑了一个占用 2GB 显存的推理服务,<span leaf="">nvidia-smi</span> 里 GPU 利用率长期 5%。
这不是个别现象。很多 AI 集群真正浪费的不是 CPU,也不是内存,而是被“整卡分配”锁死的 GPU。
Kubernetes 原生 GPU 管理只认识 <span leaf="">nvidia.com/gpu: 1</span> 这种整卡资源。但推理服务、Notebook、开发调试、轻量训练,往往只需要几 GB 显存和一部分算力。
要把 GPU 用起来,就得把链路打通:驱动、运行时、device-plugin、调度器、容器内限制,一个都不能少。
先看原生 K8s 卡在哪里
Kubernetes 管 GPU 的入口是 Device Plugin Framework。
NVIDIA 的 <span leaf="">k8s-device-plugin</span> 会在节点上发现 GPU,然后通过 gRPC 向 kubelet 注册扩展资源,比如:
resources:
limits:
nvidia.com/gpu: 1 # 只能按整卡申请Pod 调度时,scheduler 只知道某个节点还有几张 GPU。kubelet 在创建容器时,调用 device-plugin 的 <span leaf="">Allocate</span>,拿到设备 ID,再把 GPU 暴露给容器。
这个模型很稳,但粒度太粗。
比如一台机器有 8 张 A100。K8s 看到的是 <span leaf="">nvidia.com/gpu=8</span>,不是 <span leaf="">gpu-memory=640GB</span>,也不是 <span leaf="">gpu-cores=800%</span>。
所以只要一个 Pod 申请了 1 张卡,即使它只用了 2GB 显存,剩下 78GB 对 K8s 来说也已经“不可用”。
这就是 GPU 共享方案要解决的核心问题:让调度器理解显存和算力,并在运行时真的限制住它们。
GPU Operator 负责铺路,不负责切卡
生产环境里,不建议手工在 GPU 节点上装驱动、装 container runtime、装 device-plugin。
节点一多,驱动版本、CUDA 版本、container-toolkit 版本,很容易变成运维事故现场。
NVIDIA GPU Operator 的价值在这里。
它以 Operator 方式托管 GPU 节点的软件栈,包括:
| 组件 | 作用 |
|---|---|
| NVIDIA Driver | 安装或管理 GPU 驱动 |
| nvidia-container-toolkit | 让容器能访问 GPU |
| device-plugin | 向 kubelet 注册 GPU 资源 |
| DCGM Exporter | 采集 GPU 监控指标 |
| MIG Manager | 管理 MIG 实例 |
| Node Feature Discovery | 标记 GPU 节点能力 |
但要注意一点:GPU Operator 本身不是细粒度共享方案。
它解决的是 GPU 节点 Day-0 / Day-1 运维问题。真正的显存切分、算力配额、虚拟资源调度,需要 MIG、MPS、HAMi 这类方案接上来。
可以把整体链路理解成这样:

如果你用 HAMi,通常会让 HAMi 的 device-plugin 接管 GPU 资源注册和分配。GPU Operator 仍然可以负责驱动、runtime、DCGM 等底层能力。
MIG、MPS、HAMi,到底选哪个?
GPU 虚拟化不是一个方案打天下。
不同场景,对隔离性、利用率、硬件兼容、运维复杂度的要求不同。
| 方案 | 切分方式 | 隔离性 | 支持硬件 | 典型场景 |
|---|---|---|---|---|
| MIG | 硬件级切分 | 强 | A100/A30/H100 等 | 在线推理、多租户 |
| MPS | 多进程共享上下文 | 弱 | 多数 NVIDIA GPU | 单租户高并发任务 |
| HAMi | 软件虚拟化 + 调度扩展 | 中 | NVIDIA/AMD/寒武纪等 | 开发环境、推理、混部 |
| 商业 qGPU/cGPU | 厂商实现 | 中到强 | 云厂商平台 | 公有云托管场景 |
MIG 的优势是隔离性强。每个实例有独立的显存、计算单元、缓存切分,故障隔离也更好。
问题是粒度固定。比如 A100 的 MIG profile 是预定义的,你不能随意切一个 3GB 显存、20% 算力的实例。
MPS 更像是性能优化手段。它让多个 CUDA 进程共享 GPU 上下文,减少上下文切换,但不擅长做租户隔离和配额约束。
HAMi 的定位更偏 K8s 云原生。它让用户可以按 <span leaf="">显存 + 算力比例</span> 申请 GPU,并通过调度器和容器内拦截机制做限制。
如果是强隔离在线业务,优先看 MIG。如果是研发环境、推理小模型、高密度混部,HAMi 更灵活。
HAMi 是怎么把一张卡拆开的?
HAMi,原名 <span leaf="">k8s-vgpu-scheduler</span> / <span leaf="">vGPU scheduler</span>,现在叫 Heterogeneous AI computing Management Interface。它在 2024 年进入 CNCF Sandbox,社区活跃度和生态兼容性都在提升。
它的关键点不是“真的把 GPU 物理切开”,而是在三个阶段协同完成虚拟化。
1. 调度阶段:让 scheduler 看懂虚拟资源
用户提交 Pod 时,除了申请 GPU 数量,还可以申请显存和算力比例。
一个典型 Pod 可能长这样:
resources:
limits:
nvidia.com/gpu: 1 # 申请 1 个虚拟 GPU
nvidia.com/gpumem: 4096 # 申请 4GB 显存
nvidia.com/gpucores: 30 # 申请 30% 算力HAMi scheduler extender 会参与调度决策。
它会检查每个节点、每张物理 GPU 上还剩多少显存、多少算力。然后根据 binpack 或 spread 策略,选择合适的节点和 GPU。
调度完成后,分配结果会写入 Pod annotation。后面的 device-plugin 和运行时限制都依赖这份分配信息。
2. 分配阶段:device-plugin 注入环境
HAMi device-plugin 会替换或接管原生 NVIDIA device-plugin 的能力。
当 kubelet 调用 <span leaf="">Allocate</span> 时,它会读取 Pod annotation,然后向容器注入必要的环境变量和挂载。
常见信息包括:
CUDA_VISIBLE_DEVICES=0
GPU_MEMORY_LIMIT=4096
GPU_CORE_LIMIT=30
LD_PRELOAD=/usr/local/vgpu/libvgpu.so这些变量不是给 K8s 看的,而是给容器里的 CUDA 进程看的。
也就是说,K8s 调度层只负责“分给谁”。真正限制显存和算力,要进入容器运行时阶段。
3. 运行时阶段:拦截 CUDA API
HAMi 的显存限制通常依赖 LD_PRELOAD。
容器启动后,libvgpu.so 会优先加载。它会劫持 CUDA Driver API,比如 cuMemAlloc、cuMemFree,维护每个进程的显存计数。
当进程申请显存时,流程大概是:

算力限制更复杂。
常见做法是监控任务的 SM 使用情况,再通过时间片、sleep 注入等方式控制实际占用。这类限制不是硬件级隔离,所以会有一定抖动。
这也是为什么 HAMi 很适合提升利用率,但不能替代强隔离场景下的 MIG。
快速落一套 HAMi:先跑通,再调优
下面给一个最小化实践路径。
前提是 GPU 节点驱动和容器运行时已经正常。可以通过 GPU Operator 部署,也可以你自己维护,但生产里更推荐 Operator。
先确认节点能看到 GPU:
kubectl get nodes -l nvidia.com/gpu.present=true
kubectl describe node <gpu-node> | grep -A5 Capacity安装 HAMi 通常可以用 Helm。实际版本请以社区 release 为准,生产环境不要直接追 latest。
helm repo add hami https://project-hami.github.io/HAMi
helm repo update
helm install hami hami/hami \
-n kube-system \
--set scheduler.defaultSchedulerPolicy=binpack安装后重点看三个组件:
kubectl get pods -n kube-system | grep hami
kubectl get ds -n kube-system | grep hami
kubectl logs -n kube-system deploy/hami-scheduler -f然后提交一个申请 4GB 显存、30% 算力的测试 Pod。
apiVersion: v1
kind: Pod
metadata:
name: vgpu-test
spec:
containers:
- name: cuda
image: nvidia/cuda:12.2.0-base-ubuntu22.04
command: ["sleep", "3600"]
resources:
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem: 4096
nvidia.com/gpucores: 30Pod Running 后,不要只看 <span leaf="">nvidia-smi</span>。
你还要检查 Pod annotation、环境变量、容器内 CUDA 可见设备。
kubectl get pod vgpu-test -o yaml | grep -A8 annotations
kubectl exec -it vgpu-test -- env | grep -E "CUDA|GPU|VGPU"
kubectl exec -it vgpu-test -- nvidia-smi如果 annotation 没有分配信息,先查 scheduler。如果环境变量没注入,查 device-plugin。如果限制不生效,再查 LD_PRELOAD 和 CUDA 版本兼容。
这条排查链路很关键:调度成功,不代表运行时限制成功。
生产里最容易踩的坑
GPU 虚拟化上生产,难点不在安装,而在边界条件。
CUDA 版本兼容
LD\_PRELOAD 方案依赖 CUDA API 拦截。CUDA 版本变化后,API 行为、符号、调用路径可能变化。
如果你集群里同时跑 CUDA 11、CUDA 12,甚至不同基础镜像混用,一定要做矩阵测试。
建议至少覆盖:
- PyTorch / TensorFlow 主版本
- CUDA runtime 版本
- Driver 版本
- 常用推理框架,比如 Triton、vLLM、TensorRT-LLM
尤其是 vLLM 这类对显存管理比较激进的框架,最好单独压测。
监控不能只看物理卡
DCGM 和 nvidia-smi 看到的是物理 GPU 视角。虚拟 GPU 的配额、使用量、OOM 次数、限流次数,需要从 HAMi 或自定义 exporter 补齐。
生产里至少要有这些指标:
| 指标 | 用途 |
|---|---|
| 物理 GPU 显存使用率 | 判断整体水位 |
| 虚拟 GPU 显存配额 | 判断是否超分 |
| 虚拟 GPU 实际显存 | 判断单租户异常 |
| GPU SM 利用率 | 判断算力瓶颈 |
| CUDA OOM 次数 | 发现配额不合理 |
| 调度失败次数 | 发现碎片化问题 |
只看物理卡会出现一个问题:明明物理显存还有空闲,Pod 却因为虚拟配额不足调度失败。
这不是 bug,而是调度层的资源账本和物理层视角不一致。
软件隔离不是故障隔离
HAMi 可以限制显存申请,可以压算力占用。但它不能阻止所有 GPU 级别故障传播。
比如驱动异常、GPU reset、ECC 错误、某些非法 kernel 调用,仍可能影响同卡其他任务。
所以多租户强隔离场景要谨慎。
一个常见组合是:
- 在线核心推理:MIG
- 离线训练和实验:HAMi
- 单租户高吞吐:MPS
- 云厂商托管:商业 GPU 共享方案
不要为了利用率,把所有业务都塞进同一种虚拟化模型。
动态算力分配,下一步会怎么走?
现在很多集群的 GPU 配额还是静态的。
用户申请 4GB 显存、30% 算力,系统就按这个额度分配。但真实负载经常有潮汐:推理有峰谷,Notebook 有空闲,训练也有数据加载间隙。
更理想的方式是动态调整。
比如某个推理 Pod 当前 QPS 很低,只保留 10% 算力。另一个批处理任务进入高峰,就临时提升到 60%。
这要求系统具备三类能力:
- 实时采集虚拟 GPU 使用数据
- 根据策略调整算力份额
- 在不重启 Pod 的情况下让限制生效
显存动态调整会更难。
因为很多深度学习框架会预分配显存池。即使业务暂时不用,也不一定会主动释放给其他进程。
所以短期更现实的是:动态调算力,显存仍偏静态配额。
长期看,GPU Pool 化、远程 GPU、RDMA、跨节点调度会继续演进。但延迟、带宽、故障域都会变复杂,不是简单把 GPU 变成“远程磁盘”就完事。
写在最后
Kubernetes 原生 GPU 管理解决了“能不能用 GPU”,但没有解决“能不能把 GPU 用满”。
真正落地 GPU 虚拟化,需要从 device-plugin、GPU Operator、调度扩展、运行时拦截、监控告警一起看。HAMi 给了开源体系一条比较完整的路径,但生产里仍要结合 MIG、MPS 和业务隔离要求做取舍。