协会地址:上海市长宁区古北路620号图书馆楼309-313室
使用 Kamaji 和 HAMi 在模拟硬件上运行多租户 GPU Kubernetes
来源: 云原生计算基金会(Cloud Native Computing Foundation)
作者: Anish Bista | CNCF Golden Kubestronaut
发布时间: 2026/10/23 19:30:00
原文链接: https://www.cncf.io/blog/2026/10/23/multi-tenant-gpu-kubernetes-on-simulated-hardware-with-kamaji-and-hami/
本文重点介绍的 CNCF 项目
图片链接: https://www.cncf.io/projects/cert-manager
图片链接: https://www.cncf.io/projects/hami
图片链接: https://www.cncf.io/projects/kubernetes
GPU 团队通常面临一个糟糕的取舍。给每个团队分配独立的 Kubernetes 集群,就要为每个团队支付一个控制平面的成本。把所有人放进一个共享集群,隔离性又很弱。无论哪种方式,Kubernetes 都以整卡为单位分配 GPU,因此一个只需要 80 GB GPU 中 4 GB 的 Pod,仍然会占住整张卡。
本文通过组合两个开源项目给出了一条出路:
- Kamaji 将每个租户的 Kubernetes 控制平面作为 Pod 运行在一个管理集群内。
- HAMi 允许 Pod 请求 GPU 的一部分(内存以 MB 计,算力以 % 计),并将多个 Pod 打包到一张卡上。
我在一台没有物理 GPU 的单机上搭建了整套环境,使用 HAMi 的 mock device plugin(模拟设备插件)来模拟显卡。结果是:两个完全隔离的租户集群,各自通过 HAMi 调度分片 GPU 工作负载,八项测试全部通过。
下面的所有内容都可复现:确切的版本、实际可用的命令、我遇到的错误以及我是如何修复它们的。
架构
一个 kind 集群承载 Kamaji 和两个租户的控制平面;每个租户的 worker 是一个独立容器,通过 kind 网络加入并运行 HAMi。
租户之间从不共享节点或 API 服务器。唯一共享的部分是管理集群和 kamaji-etcd 数据存储,其中每个租户的数据都保持相互隔离。
为什么选择 Kamaji + HAMi
Kamaji 解决集群问题,HAMi 解决 GPU 问题;它们工作在不同的层级,因此彼此不会重叠。
有趣之处在于两者交汇的地方。租户的 API server 位于管理集群中,而 HAMi 的 webhook 则运行在租户的工作节点上。因此,每次创建 GPU pod 时,都依赖 API server 通过 Kamaji 的 Konnectivity 隧道反向访问租户集群。验证这条链路能否正常工作,正是本次实验的主要目标。
实验环境搭建
所有组件都以 Docker 容器的形式运行在一台 Linux 主机上;4 个 CPU 核心、8 GB 可用内存和 20 GB 磁盘空间就足够了。
如何在没有 GPU 的情况下测试 GPU 共享?
HAMi 的真实设备插件(device plugin)需要与 NVIDIA 驱动通信,因此没有硬件就无法启动。模拟设备插件(mock device plugin)则跳过硬件发现。你只需在节点注解(annotation)中描述虚拟显卡,它便会据此注册 gpumem 和 gpucores 容量。随后,HAMi 的 webhook、调度器(scheduler)和配额(quota)逻辑就会像在真实节点上一样运行。
两种模拟配置让各租户(tenant)易于区分:
- tenant-a: 1 块 NVIDIA A100 80 GB(81,920 MB,100 核)
- tenant-b: 2 块 NVIDIA L4 24 GB(49,152 MB,200 核)
分步构建
整个构建共分五步;第 3 步会一次性创建两个租户,第 4 步和第 5 步则需按租户分别重复执行。直接使用 kubectl 连接的是管理集群;加上 --kubeconfig tenant-a.kubeconfig 则连接某个租户。
第 1 步:管理集群、cert-manager 与 MetalLB
`
kind create cluster –name kamaji
`
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.21.2/cert-manager.yaml
kubectl -n cert-manager wait pod –all –for=condition=Ready –timeout=180s
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.16.1/config/manifests/metallb-frr-k8s.yaml
kubectl -n metallb-system wait pod –all –for=condition=Ready –timeout=180s
`
找到你的 kind 网络的 IPv4 子网,然后在子网顶部为 MetalLB 分配一个地址范围:
`
docker network inspect kind | grep Subnet
cat <<EOF | kubectl apply -f –
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: kind-ip-pool
namespace: metallb-system
spec:
addresses: – 172.18.255.200-172.18.255.250
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: empty
namespace: metallb-system
EOF
`
第 2 步:安装 Kamaji
`
`
helm repo add clastix https://clastix.github.io/charts
helm upgrade –install kamaji clastix/kamaji \
–namespace kamaji-system –create-namespace \
–set ‘resources=null’ –version 0.0.0+latest
kubectl -n kamaji-system get pods
`
第 3 步:创建租户控制平面(Tenant Control Plane)
以下命令将创建两个租户:tenant-a 和 tenant-b:
`
export TENANT_VERSION=v1.36.4
kubectl create namespace tenants
for t in tenant-a tenant-b; do
cat <`
一旦两个租户控制平面(tenant control plane)Pod 都处于 Running 状态,请为每个租户分别保存一份 kubeconfig:
`
for t in tenant-a tenant-b; do
kubectl -n tenants get secret ${t}-admin-kubeconfig -o jsonpath='{.data.admin.conf}’ | base64 -d > ${t}.kubeconfig
kubectl –kubeconfig ${t}.kubeconfig cluster-info
Done
`
每个租户控制平面(tenant control plane)都是管理集群(management cluster)中 tenants 命名空间里的一个单独 Pod,运行在 kamaji-control-plane 节点上。READY 列中的 4/4 表示它包含的四个容器:
kube-apiserver:租户的 API 服务器,通过 MetalLB IP 暴露kube-scheduler:租户的默认调度器(scheduler)kube-controller-manager:运行租户的内置控制器(controller)konnectivity-server:隧道的管理集群端,使 API 服务器能够访问租户的工作节点(worker node)
第 4 步:加入工作节点
Kamaji 没有用于工作节点的 kind 辅助工具,因此工作节点是 kind 网络上的一个 kindest/node 容器,通过标准 kubeadm 加入。其版本必须与租户(tenant)匹配。
`
docker run -d –name tenant-a-worker –hostname tenant-a-worker \
–network kind –privileged \
–security-opt seccomp=unconfined –security-opt apparmor=unconfined \
–tmpfs /tmp –tmpfs /run -v /var -v /lib/modules:/lib/modules:ro \
kindest/node:v1.36.4@sha256:099e049362a1526b2db71494e1947aae99bd16290d7c895f2b7ea312e3cbfaed
sleep 15
`
docker exec -i tenant-a-worker sh -c ‘cat > /root/tenant-a.kubeconfig’ < tenant-a.kubeconfig
JOIN_CMD=$(docker exec tenant-a-worker kubeadm token create –print-join-command \
–kubeconfig /root/tenant-a.kubeconfig)
echo “$JOIN_CMD”
docker exec tenant-a-worker bash -c “${JOIN_CMD} –ignore-preflight-errors=all”
`
在租户(而非管理集群)中安装 Calico,并将其地址池设置为租户的 podCidr:
`
curl -sLO https://raw.githubusercontent.com/projectcalico/calico/v3.31.2/manifests/calico.yaml
`
sed -i ‘s|# – name: CALICO_IPV4POOL_CIDR|- name: CALICO_IPV4POOL_CIDR|; s|# value: “192.168.0.0/16″| value: “10.244.0.0/16″|’ calico.yaml
kubectl –kubeconfig tenant-a.kubeconfig apply -f calico.yaml
kubectl –kubeconfig tenant-a.kubeconfig get nodes
`
步骤 5:租户(tenant)中的 HAMi 与模拟 GPU(mock GPU) #
不要给工作节点(worker)打上 gpu=on; 标签;这会启动 HAMi 的真实 NVIDIA 设备插件(device plugin),而在没有驱动的情况下该插件会启动失败。
`
export KUBECONFIG=$PWD/tenant-a.kubeconfig
helm repo add hami-charts https://project-hami.github.io/HAMi/
helm install hami hami-charts/hami -n kube-system
等待 hami-scheduler 运行,并且 hami-scheduler-device ConfigMap 存在
`
kubectl apply -f https://raw.githubusercontent.com/Project-HAMi/mock-device-plugin/master/k8s-mock-rbac.yaml
kubectl apply -f https://raw.githubusercontent.com/Project-HAMi/mock-device-plugin/master/k8s-mock-plugin.yaml
模拟 1 张 A100 80 GB
NODE=tenant-a-worker
kubectl patch node $NODE –subresource=status –type=json \
-p ‘[{“op”:”add”,”path”:”/status/capacity/nvidia.com~1gpu”,”value”:”10″}]’
kubectl annotate node $NODE \
‘hami.io/node-nvidia-register=[{“id”:”GPU-MOCK-0″,”count”:10,”devmem”:81920,”devcore”:100,”type”:”NVIDIA-A100-SXM4-80GB”,”health”:true,”numa”:0,”mode”:”hami-core”}]’
`
大约 30 秒后,该节点就会对外公布这张伪造的 GPU 卡:
`
{
`
“nvidia.com/gpu”: “10”,
“nvidia.com/gpucores”: “100”,
“nvidia.com/gpumem”: “81920”,
“nvidia.com/gpumem-percentage”: “100”
}
`
对于 tenant-b,请使用 tenant-b 的名称重复步骤 4 和步骤 5。其注解描述了两张卡(GPU-MOCK-B0 和 GPU-MOCK-B1,每张 24,576 MB 和 100 核),并将 nvidia.com/gpu 容量设置为 20。
GPU Pod 如何被调度
一个 GPU Pod 在落到某个节点之前,会从管理集群进入租户(tenant),然后再返回,正是这一往返过程让这两个项目协同工作。
虚线跳转(dashed hop)是关键的集成点。API 服务器运行在管理集群(management cluster)内,但 HAMi 的 webhook 运行在租户工作节点(tenant worker)上,因此准入调用(admission call)需要经由 Kamaji 的 Konnectivity 隧道传输。如果这一跳失败,GPU Pod 就会回退到默认调度器(default scheduler),HAMi 将永远无法看到它们。
测试结果
全部八项测试均已通过。
TC-03 和 TC-05:三个 Pod 共享一张 GPU 卡
每个 Pod 申请 20,000 MB 显存和 30% 的 GPU。这个循环会依次创建全部三个 Pod。在每个 Pod 完成调度后,需要手动释放 HAMi 的节点锁,因为 mock 插件从不主动释放(参见注意事项 2):
`
export KUBECONFIG=$PWD/tenant-a.kubeconfig
`
for i in 1 2 3; do
cat <<EOF | kubectl apply -f –
apiVersion: v1
kind: Pod
metadata:
name: gpu-share-${i}
spec:
containers:
– name: app
image: busybox
command: ["sleep", "infinity"]
resources:
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem: 20000 # MB
nvidia.com/gpucores: 30 # % of one card
EOF
kubectl wait pod/gpu-share-${i} –for=condition=PodScheduled –timeout=120s
kubectl annotate pod gpu-share-${i} hami.io/bind-phase=success –overwrite
kubectl annotate node tenant-a-worker hami.io/mutex.lock-
done
`
Webhook(网络钩子)将该 Pod 路由到 HAMi,HAMi 再将其调度到这张模拟卡上:
`
$ kubectl get pod gpu-share-1 -o jsonpath='{.spec.schedulerName}’
Hami-scheduler
注解:hami.io/vgpu-devices-allocated: GPU-MOCK-0,NVIDIA,20000,30:;
hami.io/vgpu-node: tenant-a-worker
事件:
Normal FilteringSucceed hami-scheduler 找到合适的节点(tenant-a-worker),0 个节点不匹配,1 个节点匹配
Normal BindingSucceed hami-scheduler 成功将节点 [tenant-a-worker] 绑定到 default/gpu-share-1
`
三个这样的 Pod 最终运行在同一张卡上:占用了 60,000 MB 显存和 90% 的计算资源。
`
NAME READY STATUS NODE
gpu-share-1 1/1 Running tenant-a-worker
gpu-share-2 1/1 Running tenant-a-worker
gpu-share-3 1/1 Running tenant-a-worker
`
TC-06 和 TC-07:HAMi 拒绝超额分配
第四个 30% Pod 将需要 100 个核心中的 120 个:
`
cat <`
它会一直保持 Pending(等待中)状态,HAMi 会说明其中的原因:
`
$ kubectl describe pod gpu-share-4 | sed -n ‘/Events/,$p’
Events:
Type Reason Age From Message
Warning FailedScheduling 15s hami-scheduler 0/1 nodes are available: 1 1/1 CardInsufficientCore. no new claims to deallocate, preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.
Warning FilteringFailed 15s (x2 over 15s) hami-scheduler 1 nodes CardInsufficientCore(tenant-a-worker)
Warning FilteringFailed 15s (x2 over 15s) hami-scheduler no available node, 1 nodes do not meet
`
请求 90,000 MB 的 Pod 超过了 81,920 MB 显卡的容量:
`
cat <`
它一直保持 Pending 状态,这次显示的是 CardInsufficientMemory.
TC-08:租户之间互相不可见
对每个租户的 API 服务器运行相同的查询,并比较返回的结果:
`
for t in a b; do
echo “===== tenant-$t =====”
kubectl –kubeconfig tenant-$t.kubeconfig get nodes
kubectl –kubeconfig tenant-$t.kubeconfig get pods -A | grep -E ‘gpu-|NAME’
kubectl –kubeconfig tenant-$t.kubeconfig get nodes -o json \
| jq ‘.items[].status.allocatable | with_entries(select(.key|test(“gpumem|gpucores”)))’
done
===== tenant-a =====
NAME STATUS ROLES AGE VERSION
tenant-a-worker Ready 29m v1.36.4
NAMESPACE NAME READY STATUS RESTARTS AGE
default gpu-share-1 1/1 Running 0 14m
default gpu-share-2 1/1 Running 0 12m
default gpu-share-3 1/1 Running 0 12m
{
“nvidia.com/gpucores”: “100”,
“nvidia.com/gpumem”: “81920”,
“nvidia.com/gpumem-percentage”: “100”
}
===== tenant-b =====
NAME STATUS ROLES AGE VERSION
tenant-b-worker Ready 5m28s v1.36.4
NAMESPACE NAME READY STATUS RESTARTS AGE
{
“nvidia.com/gpucores”: “200”,
“nvidia.com/gpumem”: “49152”,
“nvidia.com/gpumem-percentage”: “200”
}
`
每个租户只能看到自己的工作节点、自己的 Pod 以及自己的 GPU 容量:
两个控制平面(control plane)都运行在同一个管理集群(management cluster)中,但任一租户(tenant)的 API 都不会返回属于另一个租户的任何内容。
陷阱与修复
过程中有几处出了问题;每处都有一个一行修复方法。
1. kind 网络上的 IPv6 破坏了 MetalLB 地址池。 按照 Kamaji 指南进行网关查找时输出了 fc00:f853:ccd:e793::1172.18.0.1,其中 IPv6 和 IPv4 网关粘在了一起。修复方法:使用 docker network inspect kind | grep Subnet 读取 IPv4 子网,并直接写入范围 (172.18.255.200-172.18.255.250)。
2. GPU Pod 卡在“节点锁争用(node lock contention)”上。 在第一个 GPU Pod 之后,后续 Pod 一直处于 Pending 状态,并显示:
`
Warning BindingFailed hami-scheduler node tenant-a-worker has been locked within 5m0s: node lock contention
`
HAMi 在绑定 Pod(容器组)时会锁定 node(节点),并将其标记为 hami.io/bind-phase: allocating。在真实节点上,HAMi 的 device plugin(设备插件)会完成分配并释放锁。mock plugin(模拟插件)不会这样做,因此锁只会在 5 分钟后过期。针对 lab(实验环境)的修复方法:手动完成设备插件的工作。
`
kubectl annotate pod gpu-share-1 hami.io/bind-phase=success –overwrite
kubectl annotate node tenant-a-worker hami.io/mutex.lock-
`
下一个 Pod 在几秒内就完成了绑定。这只是在 mock 环境中的产物,并非 Kamaji 或 HAMi 的缺陷。
这个实验无法证明什么
这个实验验证的是各组件之间的连接方式以及调度决策,而非运行时 GPU 隔离。
- 没有真实的内存或计算限制。 HAMi-core 通过 NVIDIA 驱动在容器内部实施 GPU 限制。mock 卡没有任何东西可供强制执行。
- 没有性能数据。 未运行任何 CUDA 工作负载,因此吞吐量和邻居干扰(noisy-neighbour)效应均未经过测试。
- 手工构造的设备数据。 在真实节点上,HAMi 的设备插件(device plugin)会写入节点注解并完成每次分配;在这里,这两件事都是我手动完成的。
- 非官方的工作节点路径。 加入 kindest/node 容器的方式可行,但 Kamaji 支持的工作节点接入方式是真实机器上的 kubeadm 或 Cluster API。
结语
Kamaji 和 HAMi 无需任何胶水代码即可配合使用。Kamaji 让每个团队拥有自己的 Kubernetes API,而无需为每个团队配备一台控制平面机器;HAMi 则在这些集群内部把每块 GPU 变成可切分的资源池。唯一真正的集成风险——托管 API 服务器通过 Konnectivity 回调 HAMi 的 Webhook——第一次尝试就成功了。如果你没有 GPU,mock 设备插件足以让你在购买 GPU 之前构建并测试整个平台。







