上一篇我们用 Kueue + NVIDIA 原生 DRA 跑通了 GPU 整卡配额:Job 先进入队列,Kueue 判断配额够不够,够了再放行给调度器。
但整卡只是第一步。真实的 GPU 集群里,一张 GPU 往往不会只给一个 Pod 用,HAMi 可以继续按显存和算力切成 vGPU。问题也跟着来了:切完之后,队列系统还能不能知道每个任务用了多少?能不能做到两个任务放行、第三个因为显存或算力配额不够继续排队?
这篇就围绕这个问题跑一遍:HAMi 把 GPU 切给 Pod,Kueue 在 Job 准入阶段先把 vGPU、显存和算力配额算清楚。重点不是“能不能切卡”,而是“切完之后还能不能管起来” 。
1. 环境准备
环境基本沿用上一篇 Kueue + NVIDIA DRA 的测试环境。区别只有两点:
GPU Operator 关闭 DevicePlugin 安装 安装 HAMi 开启 DRA 模式 本文的命令和输出来自这套环境:
1
2
3
4
5
6
7
节点:lixd-test-gpu
GPU:1 x Tesla T4,15Gi 显存
K8s:1.36.1
containerd:2.2.4
Kueue:0.18.1
HAMi:2.9.0
HAMi Chart:2.9.0
K8s 集群、GPU Operator、Kueue 的安装过程前几篇已经写过,这里不再重复。需要确认的一点是:后面由 HAMi 接管 GPU 切分,所以 GPU Operator 安装时要关闭 NVIDIA DevicePlugin:
1
--set devicePlugin.enabled=false
节点上能看到 T4 即可:
1
2
$ kubectl get node lixd-test-gpu -o jsonpath = '{.metadata.labels.nvidia\.com/gpu\.product}{"\n"}'
Tesla-T4
1.1 安装 HAMi
HAMi DRA Webhook 需要 TLS 证书,因此需要提前安装 cert-manager 用于自动签发。
1
2
3
4
5
6
helm repo add cert-manager https://charts.jetstack.io
helm repo update
helm install cert-manager cert-manager/cert-manager \
-n cert-manager --create-namespace \
--set crds.enabled= true
给节点打上 gpu=on 标签。未标记的节点不会被 HAMi 接管。
1
2
# kubectl label nodes {nodeid} gpu=on
kubectl label nodes lixd-test-gpu gpu = on --overwrite
安装 HAMi,并通过 --set dra.enabled=true 开启 DRA 模式:
1
2
3
4
5
6
7
8
helm repo add hami-charts https://project-hami.github.io/HAMi/
helm repo update
helm -n hami-system install hami hami-charts/hami \
--version 2.9.0 \
--set dra.enabled= true \
--set devicePlugin.enabled= false \
--create-namespace
注意 :DRA 模式与传统模式不兼容,请勿同时启用。
gpu=on 主要给 HAMi 做节点选择。后面 ResourceFlavor 用的 nvidia.com/gpu.product=Tesla-T4,则是 GPU Operator / GFD 自动打到节点上的标签。
另外,如果 GPU 驱动是主机预装,非 GPU Operator 安装,则安装时需额外指定:
1
--set hami-dra.drivers.nvidia.containerDriver= false
1.2 验证 HAMi 已接管 GPU
HAMi 正常启动后,hami-system 下会看到三个核心组件:
1
2
3
4
5
$ kubectl get pods -n hami-system
NAME READY STATUS
hami-dra-driver-kubelet-plugin-fb4zm 1/1 Running
hami-hami-dra-monitor-7b8df84bd-jsjrd 1/1 Running
hami-hami-dra-webhook-7bb65cbcc5-g5742 1/1 Running
底层发布出来的 GPU 资源也要确认一下:
1
2
3
4
5
6
$ kubectl get deviceclass,resourceslice
NAME AGE
deviceclass.resource.k8s.io/hami-core-gpu.project-hami.io 32m
NAME NODE DRIVER
resourceslice.resource.k8s.io/lixd-test-gpu-hami-core-gpu.project-hami.io-2drs2 lixd-test-gpu hami-core-gpu.project-hami.io
ResourceSlice 里记录了这张 T4 的显存和算力容量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spec :
devices :
- allowMultipleAllocations : true
attributes :
productName :
string : Tesla T4
type :
string : hami-gpu
capacity :
cores :
value : "100"
memory :
value : 15Gi
name : hami-gpu-0
driver : hami-core-gpu.project-hami.io
需要注意的是 allowMultipleAllocations: true:同一张物理 GPU 可以被多个 ResourceClaim 消费,只要显存和算力容量还够,切卡才有空间。
2. 资源模型:为什么选择兼容模式
对 HAMi 不熟悉的同学可以先看看这篇文章:Kubernetes GPU 虚拟化实战:HAMi DRA 模式完整指南
HAMi DRA 现在有两种使用方式:
模式 用户怎么写 谁创建底层设备申请 更适合什么场景 DRA 原生模式 手写 ResourceClaim / ResourceClaimTemplate 用户 新业务直接接 DRA API 兼容模式 写 nvidia.com/gpu/gpumem/gpucores HAMi webhook 存量 HAMi 业务迁移
我这次实测下来,Kueue 接 HAMi 原生 DRA 时,更容易先做到按 claim / 设备数量做准入;但要把 gpumem/gpucores 也纳入 Kueue 配额,兼容模式反而更顺:业务侧仍然写 nvidia.com/gpu/gpumem/gpucores,Kueue 通过 ResourceTransformation 把它们折算成队列里的总量配额资源。
所以这篇走兼容模式,业务 YAML 继续写熟悉的 HAMi 资源:
1
2
3
4
5
resources :
limits :
nvidia.com/gpu : 1
nvidia.com/gpumem : 4096
nvidia.com/gpucores : 50
含义是:
资源 含义 nvidia.com/gpu: 1申请 1 个 vGPU 设备实例 nvidia.com/gpumem: 4096每个 vGPU 申请 4096Mi 显存 nvidia.com/gpucores: 50每个 vGPU 申请 50% 算力
HAMi webhook 会拦截这个 Pod,把上面的资源申请转换成底层设备申请。调度时,kube-scheduler 看到的是 HAMi 发布出来的 GPU 设备,以及对应的显存、算力容量。
3. Demo:Kueue 管理 HAMi vGPU 配额
这里用一个最小例子把链路跑通。先验证 HAMi 能不能切出 4Gi / 50 cores;确认没有问题以后,再接入 Kueue,观察它会不会在准入阶段扣配额。
3.1 先跑通 HAMi vGPU
为了把变量降到最低,先不接 Kueue,直接提交一个 Pod:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion : v1
kind : Pod
metadata :
name : hami-compatible-smoke
spec :
restartPolicy : Never
containers :
- name : cuda
image : nvcr.io/nvidia/cuda:12.6.0-base-ubuntu24.04
command : [ "bash" , "-c" , "nvidia-smi && sleep 3600" ]
resources :
limits :
nvidia.com/gpu : 1
nvidia.com/gpumem : 4096
nvidia.com/gpucores : 50
Pod 创建后,HAMi webhook 会把原始资源申请改成 ResourceClaim:
1
2
3
4
5
6
7
8
9
spec :
containers :
- name : cuda
resources :
claims :
- name : hami-kueue-demo-hami-compatible-smoke-cuda
resourceClaims :
- name : hami-kueue-demo-hami-compatible-smoke-cuda
resourceClaimName : hami-kueue-demo-hami-compatible-smoke-cuda
对应的 ResourceClaim 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spec :
devices :
requests :
- exactly :
allocationMode : ExactCount
capacity :
requests :
cores : "50"
memory : "4294967296"
count : 1
deviceClassName : hami-core-gpu.project-hami.io
name : gpu
status :
allocation :
devices :
results :
- consumedCapacity :
cores : "50"
memory : 4Gi
device : hami-gpu-0
driver : hami-core-gpu.project-hami.io
容器里看到的显存也变成了 4096Mi:
1
2
$ kubectl exec hami-compatible-smoke -- nvidia-smi
| 0 Tesla T4 ... | 0MiB / 4096MiB | 0% Default |
到这里可以确认 HAMi vGPU 已经生效:业务 YAML 仍然写 nvidia.com/gpu/gpumem/gpucores,容器里实际只看到被切分后的 4096Mi 显存。
3.2 让 Kueue 能看懂 gpumem/gpucores
HAMi 验证通过后,就可以把 Kueue 接进来了。接入前需要先把配额口径捋清楚:HAMi 的 nvidia.com/gpumem 和 nvidia.com/gpucores 是“每个 vGPU”的资源。比如这次 Demo 里有两个相同规格的 Job,每个 Job 都申请:
1
2
3
nvidia.com/gpu : 1
nvidia.com/gpumem : 4096
nvidia.com/gpucores : 50
那么队列整体在配额上应该按总量扣减:
1
2
3
vGPU 个数:2
总显存:2 x 4096Mi = 8192Mi
总算力:2 x 50 = 100
Kueue 管队列配额时应该看总量,所以要在 Kueue 配置里加 ResourceTransformation。这次用的是 Kueue 0.18.1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion : config.kueue.x-k8s.io/v1beta2
kind : Configuration
integrations :
frameworks :
- batch/job
resources :
transformations :
- input : nvidia.com/gpumem
strategy : Replace
outputs :
nvidia.com/total-gpumem : 1
multiplyBy : nvidia.com/gpu
- input : nvidia.com/gpucores
strategy : Replace
outputs :
nvidia.com/total-gpucores : 1
multiplyBy : nvidia.com/gpu
这几行最关键的是两个动作:
Workload 里不再直接统计 nvidia.com/gpumem 改成统计 nvidia.com/total-gpumem gpucores 同理,转成 nvidia.com/total-gpucoresmultiplyBy: nvidia.com/gpu 表示先乘以 vGPU 个数,再进入 Kueue 配额修改 kueue-manager-config 后重启 Kueue:
1
2
kubectl rollout restart deployment/kueue-controller-manager -n kueue-system
kubectl rollout status deployment/kueue-controller-manager -n kueue-system
3.3 配置 Kueue 队列
这次只用一个队列,把资源扣减先看清楚。
场景:
1
2
3
4
5
6
7
8
物理资源:1 x Tesla T4,15Gi 显存
队列配额:最多准入 2 个 vGPU、8192Mi 显存、100 cores
单个 Job:1 个 vGPU、4Gi 显存、50 cores
预期:
第 1 个 Job:准入
第 2 个 Job:准入
第 3 个 Job:配额不够,继续 Suspended
Kueue 配置:
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
apiVersion : v1
kind : Namespace
metadata :
name : hami-kueue-demo
---
apiVersion : kueue.x-k8s.io/v1beta2
kind : ResourceFlavor
metadata :
name : hami-t4
spec :
nodeLabels :
nvidia.com/gpu.product : Tesla-T4
---
apiVersion : kueue.x-k8s.io/v1beta2
kind : ClusterQueue
metadata :
name : hami-cq
spec :
namespaceSelector : {}
resourceGroups :
- coveredResources :
- nvidia.com/gpu
- nvidia.com/total-gpumem
- nvidia.com/total-gpucores
flavors :
- name : hami-t4
resources :
- name : nvidia.com/gpu
nominalQuota : 2
- name : nvidia.com/total-gpumem
nominalQuota : 8192
- name : nvidia.com/total-gpucores
nominalQuota : 100
---
apiVersion : kueue.x-k8s.io/v1beta2
kind : LocalQueue
metadata :
namespace : hami-kueue-demo
name : hami-queue
spec :
clusterQueue : hami-cq
这里的显存配额写 8192,和业务侧的 nvidia.com/gpumem: 4096 保持同一个口径:单位都是 Mi。单个 Job 会被 Kueue 统计成 nvidia.com/total-gpumem: 4096,两个相同 Job 加起来就是 8192。
3.4 提交 Kueue 管理的 HAMi Job
提交一个 4Gi / 50 cores 的 Job:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion : batch/v1
kind : Job
metadata :
namespace : hami-kueue-demo
name : hami-kueue-running-a
labels :
kueue.x-k8s.io/queue-name : hami-queue
spec :
suspend : true
parallelism : 1
completions : 1
template :
spec :
restartPolicy : Never
containers :
- name : cuda
image : nvcr.io/nvidia/cuda:12.6.0-base-ubuntu24.04
command : [ "bash" , "-c" , "nvidia-smi && echo HAMI_KUEUE_RUNNING_A && sleep 3600" ]
resources :
limits :
nvidia.com/gpu : 1
nvidia.com/gpumem : 4096
nvidia.com/gpucores : 50
结果:
1
2
3
4
5
6
7
8
9
$ kubectl get job,pod,workload
NAME STATUS COMPLETIONS
job.batch/hami-kueue-running-a Running 0/1
NAME READY STATUS
pod/hami-kueue-running-a-dzjrz 1/1 Running
NAME QUEUE RESERVED IN ADMITTED
workload.kueue.x-k8s.io/job-hami-kueue-running-a-59997 hami-queue hami-cq True
看 Workload 的准入结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
status :
admission :
clusterQueue : hami-cq
podSetAssignments :
- count : 1
flavors :
nvidia.com/gpu : hami-t4
nvidia.com/total-gpucores : hami-t4
nvidia.com/total-gpumem : hami-t4
resourceUsage :
nvidia.com/gpu : "1"
nvidia.com/total-gpucores : "50"
nvidia.com/total-gpumem : "4096"
这个结果就是 Kueue 介入后的关键变化:Pod 还没进入调度阶段,Workload 已经先按 vGPU 个数、总显存、总算力扣了一次配额。
HAMi 生成的 ResourceClaim 也可以对上:
1
2
3
4
5
6
7
8
9
status :
allocation :
devices :
results :
- consumedCapacity :
cores : "50"
memory : 4Gi
device : hami-gpu-0
driver : hami-core-gpu.project-hami.io
容器里也能看到 4096Mi 显存上限:
1
2
$ kubectl exec hami-kueue-running-a-dzjrz -- nvidia-smi
| 0 Tesla T4 ... | 0MiB / 4096MiB | 0% Default |
3.5 验证配额阻塞
继续提交两个相同规格的 Job:
1
2
3
hami-kueue-running-a:1 vGPU + 4096Mi + 50 cores
hami-kueue-running-b:1 vGPU + 4096Mi + 50 cores
hami-kueue-pending-c:1 vGPU + 4096Mi + 50 cores
队列总配额是:
1
2 vGPU + 8192Mi + 100 cores
因此第 2 个 Job 可以准入,第 3 个 Job 会继续留在队列里:
1
2
3
4
5
6
7
8
9
10
$ kubectl get job,workload
NAME STATUS
job.batch/hami-kueue-running-a Running
job.batch/hami-kueue-running-b Running
job.batch/hami-kueue-pending-c Suspended
NAME QUEUE RESERVED IN ADMITTED
workload.kueue.x-k8s.io/job-hami-kueue-running-a-59997 hami-queue hami-cq True
workload.kueue.x-k8s.io/job-hami-kueue-running-b-9d737 hami-queue hami-cq True
workload.kueue.x-k8s.io/job-hami-kueue-pending-c-d854d hami-queue
ClusterQueue 的用量已经打满:
1
2
3
4
5
6
7
8
9
10
11
12
status :
admittedWorkloads : 2
flavorsUsage :
- name : hami-t4
resources :
- name : nvidia.com/gpu
total : "2"
- name : nvidia.com/total-gpucores
total : "100"
- name : nvidia.com/total-gpumem
total : "8192"
pendingWorkloads : 1
Pending Workload 里能看到原因:
1
2
3
4
5
6
7
8
9
10
11
status :
conditions :
- reason : Pending
message: 'couldn''t assign flavors to pod set main : insufficient unused quota
for nvidia.com/gpu in flavor hami-t4, 1 more needed, insufficient unused quota
for nvidia.com/total-gpucores in flavor hami-t4, 50 more needed'
resourceRequests :
- resources :
nvidia.com/gpu : "1"
nvidia.com/total-gpucores : "50"
nvidia.com/total-gpumem : "4096"
这也是我更关心的点:不是等 Pod 到调度阶段才 Pending,而是在 Job 准入阶段就把超配额任务留在队列里。
4. 容易踩的坑
不要写小数 GPU
第一个坑是小数 GPU。不要写 nvidia.com/gpu: "0.5"。
Kubernetes 扩展资源必须是整数,而且 GPU 这类不可超卖资源要写在 limits 里。正确写法是:
1
2
3
4
5
resources :
limits :
nvidia.com/gpu : 1
nvidia.com/gpumem : 4096
nvidia.com/gpucores : 50
半张卡 这种说法在 HAMi 里应该理解成:1 个 vGPU 实例 + 部分显存 + 部分算力 ,不是 0.5 个 nvidia.com/gpu。
Kueue 需要做资源转换
只把 nvidia.com/gpu 放进 Kueue,只能限制 vGPU 个数,管不了显存和算力。
要把 HAMi 的切分资源纳入 Kueue 配额,需要做这两个转换:
1
2
nvidia.com/gpumem -> nvidia.com/total-gpumem
nvidia.com/gpucores -> nvidia.com/total-gpucores
ClusterQueue 里也要同时配置这三个资源:
1
2
3
4
coveredResources :
- nvidia.com/gpu
- nvidia.com/total-gpumem
- nvidia.com/total-gpucores
5. 小结
这一篇跑完后,Kueue 系列基本就从 CPU 队列一路串到了 GPU 整卡和 HAMi vGPU。真落到 GPU 集群里,光能切卡还不够,准入和配额也一样重要: GPU 虚拟化解决的是"怎么切",Kueue 解决的是"谁先用、谁能用多少"。两者结合起来,GPU 集群才真正具备了多租户管理能力 。