云原生
HAMi 详解:为什么你的 GPU 闲着,账单却没闲着
用 HAMi 在多个推理服务之间共享 GPU 的实践者指南:为什么整卡调度烧掉的是集群账单上最贵的一行,软件 vGPU 又是如何真正把限制执行到位的。
Ivan Porta
创始人兼 Principal Engineer

在 Dynamic Resource Allocation 及其 alpha 阶段的可分区设备(partitionable-device)和可消耗容量(consumable-capacity)扩展发布之前,Kubernetes 只能把 GPU 作为整卡来调度,没有原生手段去分配设备的一部分。例如,一个 pod 即便只需要 80 GB H100 中的 4 GB,也仍然会在整个生命周期内占住整块 GPU。这导致共享集群中昂贵硬件的利用率严重不足。为峰值流量设计的聊天机器人在低峰时段可能一直在专用卡上闲置;多个推理服务尽管使用率很低,却往往各自跑在单独的 GPU 上;监控仪表盘可能显示所有 GPU 都已分配,而 nvidia-smi 之类的工具却常常显示实际利用率微乎其微。结果就是,因为集群看起来已经满载,又冒出了申请更多 GPU 的新预算请求。DRA 通过防止超额分配设备声明的预算来缩小这个差距,但它并不会在 pod 运行之后原生地强制执行资源限制,这仍然是设备驱动的职责。这正是 HAMi 提供价值的地方。
HAMi(Heterogeneous AI Computing Virtualization Middleware,异构 AI 计算虚拟化中间件)填补了这个缺口:它允许 pod 精确申请 GPU 显存和算力的一部分,并通过容器内的强制执行层把 pod 约束在这个额度内,应用和镜像无需任何改动。SNOW 和蔚来(NIO)等公司已在生产环境中运行 HAMi,该项目也在 2026 年 7 月晋级为 CNCF Incubating 项目。
HAMi 到底是什么?
HAMi 是一个面向 Kubernetes 的开源 GPU 虚拟化层。它让 pod 可以精确申请一定量的显存和 AI 加速器的算力,然后把 pod 调度到有足够空闲容量的设备上,并在运行时把它绑定到那个切片上。同一套使用模式覆盖多种加速器,包括 NVIDIA GPU、寒武纪(Cambricon)MLU、海光(Hygon)DCU、昇腾(Ascend)NPU、摩尔线程(Moore Threads)、沐曦(MetaX)等,每种都通过自己的 device plugin 和隔离后端接入。以 NVIDIA 为例,HAMi 会替换原生的 device plugin,并通过在容器内拦截 CUDA 驱动 API 来强制执行显存上限;其他厂商则提供类似的库级或硬件级隔离。
用户体验刻意保持简单:在标准的设备资源请求之外,再加上一个或多个限制项,调度器在调度时读取这些限制,找到一块拥有足够空闲显存和算力资源的卡。在 NVIDIA 上,你继续请求 nvidia.com/gpu,并在旁边加上两个扩展资源:nvidia.com/gpumem 和 nvidia.com/gpucores。
resources:
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem: 8000
nvidia.com/gpucores: 30 通过两组对比,可以更清楚地看出这种隔离模型的定位。MIG 画的是一条硬件边界:每个实例都有自己的 SM,以及一条穿过 L2 缓存切片、内存控制器和 DRAM 通道的专属通路。HAMi 的边界是一道栅栏,而不是一堵墙。它的限制在容器内部由软件强制执行,能够可靠地把工作负载约束在各自的预算内,却无法遏制设备级故障:共享卡上一个挂起的进程、一次驱动重置或一个 XID 错误,仍然会波及卡上的每一个租户。不过两者工作在不同的层面,它们可以互补而非竞争;后面我们会回到 HAMi 如何直接驱动 MIG。
而与时间分片(time-slicing)相比,HAMi 在稳定性上有显著提升。时间分片只是把单张卡宣告成多个可调度的副本,并在计算引擎上交错执行它们的工作负载,既没有按副本的显存上限,也没有隔离。这使得一个工作负载非常容易分配超出其名义份额的显存,并把其他工作负载一起拖垮。HAMi 则会直接拒绝超额分配。下面的日志显示 PyTorch 报告的总容量与 nvidia.com/gpumem 设置的 1000 MB 限制一致;当 pod 试图超出限制时,分配器用一个 OOM 错误拦下了它,其他 vGPU 租户不受影响:
(EngineCore pid=95) ERROR 07-11 21:15:46 [core.py:1231] torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 20.00 MiB. GPU 0 has a total capacity of 1000.00 MiB of which 12.00 MiB is free. Including non-PyTorch memory, this process has 1020.00 MiB memory in use. Of the allocated memory 864.68 MiB is allocated by PyTorch, and 41.32 MiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is large try setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid fragmentation. See documentation for Memory Management (https://docs.pytorch.org/docs/stable/notes/cuda.html#optimizing-memory-usage-with-pytorch-cuda-alloc-conf)一个共享 GPU 请求如何流经 HAMi
从 manifest 到被强制执行的切片,一个请求要经过四道手,如下图所示:device plugin 上报每张卡的容量,mutating webhook 把 pod 改道到 HAMi 的调度器,scheduler extender 选出节点和切片,容器内的库在运行时强制执行这个切片。
在每个节点上,以 DaemonSet 方式运行的 device-plugin 每 30 秒通过厂商的管理库(例如 NVIDIA 的 NVML,也就是 nvidia-smi 背后的那个库)轮询本地设备。然后它把这些遥测数据通过给节点打上 hami.io/node-<vendor>-register 注解的方式发布出去。
kubectl get nodes gtrekter -o yaml
apiVersion: v1
kind: Node
metadata:
annotations:
hami.io/node-handshake: Requesting_2026-07-09 09:03:57
hami.io/node-nvidia-register: '[{"id":"GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e","count":10,"devmem":6141,"devcore":100,"type":"NVIDIA GeForce RTX 4050 Laptop GPU","mode":"hami-core","health":true,"devicepairscore":{}}]'
nvidia.com/gpu-driver-upgrade-enabled: "true"
...当一个 pod 被创建时,API server 会调用 HAMi 的 mutating webhook,由它对照所支持的设备驱动(包括 NVIDIA、昇腾、AWS Neuron 等)检查 pod 的 manifest。每个驱动都会在容器的 resources.limits 中查找自己识别的资源键。例如,NVIDIA 驱动查找 nvidia.com/gpu、nvidia.com/gpumem 和 nvidia.com/gpucores,而寒武纪 MLU 驱动匹配 cambricon.com/vmlu。一旦检测到兼容的请求,HAMi 就会把 pod 的 schedulerName 更新为 hami-scheduler,确保这个 pod 由 HAMi 的设备感知调度逻辑处理,而不是默认的 Kubernetes 调度器。
注意: 在 HAMi 下,请求 nvidia.com/gpu: 1 不再意味着“一整张卡”,而是“一张共享的物理卡”,具体切片由显存和核心限制决定。不过,省略 nvidia.com/gpumem 的 pod 会回退到配置的默认值(卡显存的 100%)。
kubectl logs -n hami-system deploy/hami-scheduler -c vgpu-scheduler-extender
I0710 13:51:38.552275 config.go:111] Initializing NVIDIA device
I0710 13:51:38.552355 config.go:248] All devices initialized successfully
...
I0710 14:02:14.161116 devices.go:647] "Resource requirements collected" pod="default/chat-qwen-64c4bd66f9-8qccs" requests=[{"NVIDIA":{"Nums":1,"Type":"NVIDIA","Memreq":4000,"MemPercentagereq":101,"Coresreq":60}}]vgpu-scheduler-extender 会为集群中每一块物理 GPU 构建一份实时的使用情况映射。它从每个节点的 hami.io/node-nvidia-register 注解中读取设备元数据,然后遍历已经分配到该节点的 pod,计算已消耗的 GPU 显存和算力容量。
kubectl logs -n hami-system deploy/hami-scheduler -c vgpu-scheduler-extender
...
I0710 14:02:14.161182 node_policy.go:82] node node1 used 0, usedCore 0, usedMem 0,
I0710 14:02:14.161191 gpu_policy.go:104] device GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e user 0, userCore 0, userMem 0,
...
I0710 16:54:54.475656 node_policy.go:82] node node1 used 1, usedCore 60, usedMem 4000,
I0710 16:54:54.475667 gpu_policy.go:104] device GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e user 1, userCore 60, userMem 4000,随后调度器过滤掉容量不足的节点,并根据配置的 HAMi 调度策略(nodeSchedulerPolicy 和 gpuSchedulerPolicy;chart 的默认值是节点级 binpack、GPU 级 spread)给剩余节点打分。选定的节点和 GPU 切片会以逗号分隔的 UUID,Type,memMB,cores 记录的形式写入 pod 的注解(hami.io/vgpu-devices-allocated)。
$ kubectl describe pod -n default embed-bge-f84696cc9-7587j
Name: embed-bge-f84696cc9-7587j
Labels: hami.io/vgpu-node=node1
...
Annotations: hami.io/vgpu-devices-allocated: GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e,NVIDIA,2000,30:;
hami.io/vgpu-devices-to-allocate: ;;
hami.io/bind-phase: success
hami.io/bind-time: 1783702494
...一旦 pod 绑定到节点,Kubelet 就会调用该节点上 HAMi device-plugin 的 Allocate。插件读取 pod 的 hami.io/vgpu-devices-to-allocate 注解(插件消费后即被清空,这也是上面 describe 输出中它为空的原因)来解码所请求的切片,确定目标物理设备及其确切的算力和显存限制。然后它返回一个响应,说明容器要在这个特定切片上运行所需的一切:环境变量、host-path 挂载(包括强制执行库)以及设备句柄。
kubectl logs -n hami-system ds/hami-device-plugin -c device-plugin | grep -i 'Allocate Response'
...
I0710 16:54:54.499747 232069 server.go:719] Allocate Response [envs:{key:"CUDA_DEVICE_MEMORY_LIMIT_0" value:"2000m"} envs:{key:"CUDA_DEVICE_MEMORY_SHARED_CACHE" value:"/usr/local/vgpu/584ed76a-0856-40b3-8113-1ad696daa252.cache"} envs:{key:"CUDA_DEVICE_SM_LIMIT" value:"30"} envs:{key:"LIBCUDA_LOG_LEVEL" value:"1"} envs:{key:"NVIDIA_VISIBLE_DEVICES" value:"GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e"} mounts:{container_path:"/usr/local/vgpu/libvgpu.so" host_path:"/usr/local/vgpu/libvgpu.so.v2.9.0" read_only:true} mounts:{container_path:"/usr/local/vgpu" host_path:"/usr/local/vgpu/containers/1a5bac44-7888-4ea5-9382-3ef49ae1f1ba_tei"} mounts:{container_path:"/tmp/vgpulock" host_path:"/tmp/vgpulock"} mounts:{container_path:"/etc/ld.so.preload" host_path:"/usr/local/vgpu/ld.so.preload" read_only:true}]Kubelet 在创建容器时直接应用这个响应。从那一刻起,HAMi 预加载的控制库就位于应用和厂商驱动之间,在进程内强制执行切片。它拦截驱动的显存分配调用,逐个请求核对切片的预算,一旦工作负载试图超过上限就返回 OOM 错误。它还会拦截并改写驱动的管理查询(比如 nvidia-smi),确保上报的总容量反映的是分配的限额,空闲容量则是限额减去当前使用量。
kubectl exec -n default chat-qwen-64c4bd66f9-8qccs -c vllm -- env
CUDA_VERSION=13.0.2
NVIDIA_VISIBLE_DEVICES=GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e
CUDA_DEVICE_MEMORY_LIMIT_0=4000m
CUDA_DEVICE_SM_LIMIT=60
CUDA_DEVICE_MEMORY_SHARED_CACHE=/usr/local/vgpu/5e999bc3-2dbf-46b0-ba81-cc479a1c11dd.cache
...
kubectl exec -n default chat-qwen-64c4bd66f9-8qccs -c vllm -- cat /etc/ld.so.preload
/usr/local/vgpu/libvgpu.so
kubectl exec -n default chat-qwen-64c4bd66f9-8qccs -c vllm -- nvidia-smi --query-gpu=memory.total --format=csv,noheader
4000 MiB
[HAMI-core Msg(272:133498069993280:multiprocess_memory_limit.c:703)]: Cleanup on exit for PID 272
[HAMI-core Msg(272:133498069993280:multiprocess_memory_limit.c:739)]: Exit cleanup complete for PID 272真正影响账单的调度策略
如前所述,调度器首先过滤掉容量不足的节点。对剩下的候选者,HAMi 通过两个相互独立的调度层级确定最优放置:节点级和 GPU 级。目前,HAMi 在每个层级都支持两种主要策略:binpack(把任务尽可能密集地打包)和 spread(把任务尽可能广泛地分散)。
metadata:
annotations:
hami.io/node-scheduler-policy: "binpack"
hami.io/gpu-scheduler-policy: "binpack"两种策略都伴随着各自的运营与财务权衡。
节点级策略
在节点级,binpack 策略把工作负载密集地打包到尽可能少的机器上。这种方式让集群自动扩缩器能够快速排空并关停空节点,直接削减云基础设施账单。然而,这种密度让租户高度集中,意味着单个节点或驱动故障的爆炸半径要大得多,同时也带来更多争用和更少的带宽余量。
反过来,spread 策略把工作负载均匀地分布到所有可用节点上。虽然这确实降低了每节点的争用,为意外的流量尖峰留出余量,并稳妥地分散了故障域,但它让每个节点都保持“温热”。结果是自动扩缩器无法缩减集群,你只能为闲置容量持续付费。
GPU 级策略
在 GPU 级,binpack 策略会先把一张物理卡完全填满,再把工作负载调度到下一张。这为需要完整资源的大模型保留了完全空闲的 GPU,并最大化共享一张卡的小切片数量。与生俱来的代价是它制造出密度最高的卡,把“吵闹邻居”压力推到最大,并让大量租户暴露在单卡 XID 故障之下。
另一种选择是 spread 策略,它把工作负载均衡到所有可用的卡上。这最大限度地减少了算力和显存带宽的争用,使其成为延迟敏感的推理工作负载的更优选择。缺点是它把可用显存碎片化到整个集群,意味着后续一个需要大块连续显存的大任务可能无法调度,即使集群的空闲显存总量在数字上是足够的。
这些只是放置时的策略:HAMi 在新 pod 到来时给它们打分,并不会主动迁移正在运行的工作负载。因此财务上的节省要随着 pod 的更替才会兑现,也就是说 binpack 在高度弹性的推理集群上回报很快,而在长期存活、被固定住的 pod 上收益要小得多。
HAMi 与 MIG:互补的层
栅栏和墙工作在不同的层面,你也不必在每个集群里二选一:HAMi 可以在一个资源池里同时管理两者。启用动态 MIG 后,HAMi 会驱动 nvidia-mig-parted 按到来的任务切分和重新切分 MIG 几何布局,免去手动的 nvidia-smi mig 操作和预先固定的切片布局。Pod 还是照原样请求 nvidia.com/gpu 和 nvidia.com/gpumem;默认情况下,调度器既可以用一个 hami-core 切片来满足请求,也可以用一个 MIG 实例来满足;需要硬隔离的工作负载可以通过 nvidia.com/vgpu-mode: "mig" 注解把自己钉在墙这一侧。代价是粒度:MIG 放置会向上取整到最近的允许几何布局(在 A100-40GB 上,一个 8 GB 的请求会落到一个 2g.10gb 实例上),而 hami-core 会精确圈出你所要求的量。
运营的现实
-
HAMi 会替换你的 device plugin。 HAMi 的 device plugin 和标准的
nvidia-device-plugin都想拥有nvidia.com/gpu;在同一节点上同时运行两者会让它们在资源注册上互相争抢,导致真实数量与虚拟化数量之间出现容量缺口,而 GPU 分配由最后注册的那个插件提供服务,悄无声息地绕过 HAMi 的隔离。如果你在运行 GPU Operator,就把驱动、container toolkit 和 DCGM 留给它,并在 HAMi 管理的节点上禁用它的 device plugin。用 gpu=on 节点标签给 HAMi 划定范围,按节点池逐步铺开,不要第一天就全集群上线。 -
调度器镜像要跟着你的控制平面走。
hami-scheduler封装了一个原生的kube-scheduler镜像,当前的 chart 在安装时根据 API server 的版本解析它的 tag,这意味着一次 Kubernetes 升级会让旧的调度器悄悄地继续运行,直到你下一次 Helm 升级。把scheduler.kubeScheduler.image.tag写进你的 Kubernetes 升级手册,否则它会在六个月后咬你一口。 -
gpucores是节流,不是预留。 调度器在放置时登记你的份额,但在运行时这个份额是天花板,从来不是地板。限制器是一个在 kernel 启动时检查的令牌桶,不是硬件分区:预期会有围绕目标值的振荡、在卡空闲时冲破份额的突发,以及邻居在突发中途到来时限制重新生效前几秒钟的重叠(设置GPU_CORE_UTILIZATION_POLICY=force可以让限制器始终保持开启)。在承诺 SLO 之前,先在有吵闹邻居的情况下对 p99 做基准测试。
一个务实的建议
如果你的 GPU 集群是 A100/H100 级别、由多个团队共享,而你的利用率仪表盘会让你在财务复盘会上下不来台,那就本周在一个推理节点池上试点 HAMi。安装一个下午就能完成:给两个节点打上 gpu=on 标签,helm install,把一个过度预留的推理 Deployment 转成 gpumem/gpucores 切片,然后让它跑一个星期,同时对照 DCGM 利用率观察分配情况。你要验证的算术很直白:一个 8 卡 H100 节点按需运行一年大约要 29 万到 48 万美元,具体取决于云厂商和区域,所以 bin-packing 排空的每一个节点都是真金白银。在自有硬件上,按每张 H100 大约 2.5 万到 3 万美元计算,一千卡规模的集群上每提升十个百分点的持续利用率,就是 250 万到 300 万美元的资本支出在干活而不是闲着。
FAQ
关于 HAMi,我们最常被问到的五个问题。
HAMi 可以用于生产环境了吗?
可以。该项目自 2026 年 7 月起为 CNCF Incubating 项目,贝壳(Beike)、蔚来(NIO)和 SNOW Corp 等企业已经在生产环境中使用它。
HAMi 需要修改我的应用或镜像吗?
不需要。一个预加载的库会在容器内拦截 CUDA 调用;pod 只需在 nvidia.com/gpu 旁边加上 nvidia.com/gpumem 和 nvidia.com/gpucores 限制。
HAMi 的隔离和 MIG 一样强吗?
默认情况下不是:HAMi-core 在软件层强制执行,所以卡级故障是共享的。面对不受信任的租户请使用 MIG——HAMi 自己就能驱动它来实现硬隔离。
HAMi vs vLLM:它们不都在管理 GPU 显存吗?
在不同的层。HAMi 在 pod 之间划分一张卡;vLLM 在单个 pod 内做显存预算。两者可以组合:vLLM 会把自己的 HAMi 切片当作整块 GPU。
Kubernetes 的 DRA 不会让 HAMi 过时吗?
不会。DRA 标准化的是 pod 请求设备的方式;它并不在容器内强制执行限制。HAMi 提供了一个 DRA 驱动,并继续通过 HAMi-core 做强制执行。
如果你不想自己动手做这次上线,Todea 可以作为我们托管平台服务的一部分,端到端地接手 HAMi 的安装与运维。