ContainerDays 2026 Singapore · 动手工作坊

从零共享 GPU

基于 HAMi 的 Kubernetes GPU 调度动手实践

宋净超
密瓜智能开源生态 VP
rootsongjc jimmysongio
Reza Jelveh
密瓜智能解决方案架构师
fishman rezajelveh
1

这 45 分钟

10 月 27 日 12:30 至 13:15

12:30 · Lab 1

安装 HAMi。kind 节点把两块模拟 A100 作为 vGPU 容量上报。

12:40 · Lab 2

共享一块 GPU。细粒度申请、完整拦截链路,以及一个永远调度不上的 Pod。

12:50 · Lab 3

调度策略。卡维度上的 binpack 与 spread,以及拓扑感知。

13:00 · Lab 4

超越默认调度器。KAI Scheduler、Volcano 与 DRA 路径。

13:10 · 收尾

两套指标体系,以及课后可以尝试什么。

实验手册:workshop.project-hami.io/labs/containerdays-singapore

2

课前准备

所有实验都在笔记本上运行,GPU 是模拟的

git clone https://github.com/Project-HAMi/hami-workshop
cd hami-workshop/examples/containerdays-singapore
git clone https://github.com/fishman/hami-mock-monitor
docker pull kindest/node:v1.37.0
docker pull busybox:1.36
docker pull ghcr.io/llm-d/llm-d-inference-sim:v0.12.0
3

为什么要共享 GPU

4

默认:一个任务独占一块 GPU

这样做的代价

Kubernetes 以整卡为粒度分配 GPU:一块完整设备对应一个任务。

Device Plugin 与 DRA

5

HAMi 是什么

一块 GPU,多个任务

After HAMi

6

HAMi 的六项能力

HAMi 为 GPU 调度带来的六件事

异构算力管理

用一套工作流管理 GPU、NPU、MLU 等各类加速器。

硬隔离

运行时对显存与算力做硬隔离切分。

高级调度

binpack、spread 与拓扑感知的放置策略。

Kubernetes 原生

Kubernetes 原生 API,支持 DRA 与 CDI。

资源隔离与 QoS

显存与算力配额,保障共享的公平与稳定。

统一监控

跨厂商一致的指标与可观测性。

7

申请一份算力

跨厂商统一 API,业务零改造

  • 支持 NVIDIA、Ascend、Cambricon、Hygon、Iluvatar 等
  • 细粒度:显存最小 1 MB,算力最小为单卡的 1%
  • 对任务透明:无需修改代码
  • 容器内硬资源隔离
# NVIDIA:单卡的 15 GB 显存 + 30% 算力
resources:
  limits:
    nvidia.com/gpu: 1
    nvidia.com/gpumem: 15000
    nvidia.com/gpucores: 30
# 昇腾 910C:8GB 显存 + 20% 算力
resources:
  limits:
    huawei.com/Ascend910C: "1"
    huawei.com/Ascend910C-core: "20"
    huawei.com/Ascend910C-memory: "8192"
8

架构

代码入口:cmd/scheduler、cmd/device-plugin、cmd/vGPUmonitor、pkg/device/<vendor>

9

实验环境:真调度器,假 GPU

实验环境架构

10

实验环境的两个 mock 组件

mock-device-plugin

把节点注解转换为 nvidia.com/gpumem 与 nvidia.com/gpucores 容量。真实 device plugin 从 NVML 写入同样的注解。

hami-mock-monitor

用负载源替代 GPU,产生 HAMi 的使用量指标(正常来自 vGPUmonitor 与 HAMi-core)。

没有真实 GPU 就少了一样东西:容器内的显存与算力隔离。那是 HAMi-core 的职责,收尾环节在真实显卡上现场演示。

11

Lab 1:安装 HAMi

12

Lab 1:一条命令

./setup.sh

这个脚本做了什么:

结束时的预期输出:

{ "nvidia.com/gpu": "20", "nvidia.com/gpucores": "200",
  "nvidia.com/gpumem": "81920", "nvidia.com/gpumem-percentage": "200" }
13

Lab 1:观察环境

kubectl -n kube-system get pods | grep hami
kubectl -n kube-system get cm hami-scheduler-device -o yaml | less
kubectl get node hami-lab-control-plane \
  -o jsonpath='{.metadata.annotations.hami\.io/node-nvidia-register}' | jq
[{"id":"GPU-mock-0","index":0,"count":10,"devmem":40960,"devcore":100,
  "type":"NVIDIA-A100-SXM4-40GB","health":true,"numa":0,"mode":"hami-core"}, ...]
14

Lab 2:共享一块 GPU

15

Lab 2:三个 Pod,两块卡

kubectl apply -f share.yaml -f too-big.yaml
kubectl wait --for=condition=Ready pod/share-1 pod/share-2 pod/share-3 --timeout=120s
kubectl get pods -o wide

share-1..3 各申请 15000 MB 与单卡 30% 算力,都能运行;too-big 申请 45000 MB,超过任何一张 40960 MB 的卡,一直 Pending:

kubectl get events --field-selector involvedObject.name=too-big | tail -2
# 0/1 nodes are available: 1 2/2 CardInsufficientMemory.

45000 MB 没超过节点总量,但装不进任何一张 40960 MB 的卡。

16

Lab 2:读取 HAMi 的决策

for p in share-1 share-2 share-3; do
  echo -n "$p "; kubectl get pod $p -o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
done
# share-1 GPU-mock-1,NVIDIA,15000,30:;
# share-2 GPU-mock-0,NVIDIA,15000,30:;
# share-3 GPU-mock-1,NVIDIA,15000,30:;

格式为每个设备 uuid,type,memory_mb,cores:,每个容器以 ; 分隔。device plugin 在 Allocate 阶段读取它。

17

拦截链路

四步,一个节点

mock plugin 跳过了第 4 步的完成动作,这正是 hami-mock-monitor completeBinds 存在的原因。没有它,下一个 Pod 要等锁最多 5 分钟。

真实节点上,第 4 步还会通过 ld.so.preload 注入 libvgpu.so,HAMi-core 从这一刻起执行显存与算力限额。

18

Lab 3:调度策略

19

放置发生在两条轴上

轴注解binpackspread
哪个节点hami.io/node-scheduler-policy更少节点分散到更多节点
节点上的哪张卡hami.io/gpu-scheduler-policy同一张卡分散到不同卡
20

Lab 3: binpack

kubectl delete -f share.yaml -f too-big.yaml
kubectl apply -f binpack.yaml
kubectl wait --for=condition=Ready pod/pack-1 pod/pack-2 --timeout=120s
for p in pack-1 pack-2; do
  echo -n "$p "; kubectl get pod $p -o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
done

两个 Pod 报告同一个 GPU-mock-X:两个负载共享一张卡。为大盘任务腾出整卡。

21

Lab 3: spread

kubectl delete -f binpack.yaml
kubectl apply -f spread.yaml
kubectl wait --for=condition=Ready pod/spread-1 pod/spread-2 --timeout=120s
for p in spread-1 spread-2; do
  echo -n "$p "; kubectl get pod $p -o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
done

一卡一 Pod。同一集群、同样的申请、不同的策略、不同的放置。

metadata:
  annotations:
    hami.io/gpu-scheduler-policy: spread   # 逐 Pod 指定 spread 或 binpack
22

拓扑感知

23

Lab 4:超越默认调度器

24

HAMi 是两层东西

一个隔离层,多个调度大脑

一个隔离层,多个调度大脑

25

三种使用方式

标准 HAMi

HAMi 调度器扩展 + HAMi device plugin。

nvidia.com/gpu、gpumem、gpucores

Volcano vGPU

Volcano deviceshare + volcano-vgpu-device-plugin。

volcano.sh/vgpu-number、vgpu-memory、vgpu-cores

KAI Scheduler

KAI GPU 共享 + kai-resource-isolator。

gpu-memory 或 gpu-fraction 注解

三者都在容器内使用 HAMi-core 做隔离。一条规则:每个 GPU 节点只跑一条 device-plugin 路径。

26

KAI Scheduler:与 HAMi 组合

NVIDIA 面向 AI 负载的调度器(CNCF Sandbox):gang 调度、层级公平队列、GPU 纯量共享、拓扑感知放置。

helm install kai-scheduler oci://ghcr.io/nvidia/kai-scheduler --version v0.17.0 \
  --set global.gpuSharing=true --set binder.plugins.hamicore.enabled=true \
  -n kai-scheduler --create-namespace
27

Volcano:共享 GPU 上的 gang 作业

kind: Job                    # batch.volcano.sh/v1alpha1
spec:
  schedulerName: volcano
  minAvailable: 2            # 两个 worker 要么都来,要么都不来
  queue: vgpu-queue
  tasks:
  - replicas: 2
    template:
      spec:
        containers:
        - resources:
            limits:
              volcano.sh/vgpu-number: 1
              volcano.sh/vgpu-memory: 2000
              volcano.sh/vgpu-cores: 30
28

DRA:资源模型变了

github.com/Project-HAMi/HAMi-DRA

这一节不做现场安装:10 分钟装不完,一个模拟节点也用不上。回家可以试 Volcano vGPU 教程:project-hami.io/tutorials/labs/volcano-vgpu-gang-queue
29

收尾

两套指标体系,以及课后可以尝试什么

30

预留不等于使用

预留与使用

HAMi 暴露两套指标。把它们分开:一套是预留,一套是使用。

./metrics.sh scheduler   # 分配量,端口 9395
./metrics.sh monitor     # 使用量,端口 9394
31

两套指标来源

Scheduler :9395,分配量

  • gpu_memory_allocated_bytes
  • gpu_core_allocated_ratio
  • node_gpu_memory_allocated_ratio
  • gpu_shared_count
  • vgpu_memory_allocated_bytes
  • resource_quota_used

vGPUmonitor :9394,使用量

  • vgpu_memory_used_bytes
  • vgpu_memory_limit_bytes
  • container_device_utilization_ratio
  • host_gpu_utilization_ratio
  • host_gpu_memory_used_bytes
  • container_last_kernel_elapsed_seconds

所有指标以 hami_ 开头。标签相同:node、device_uuid、namespace、pod。可在 PromQL 中做连接。

32

给 mock 一个真实负载

kubectl apply -f sim.yaml      # llm-d-inference-sim,申请量在 0 到 5 之间波动
watch -n 5 "./metrics.sh monitor | grep 'pod=\"sim\"'"
hami_container_device_utilization_ratio{pod="sim"} 26.4
hami_node_gpu_memory_allocated_ratio{device_uuid="GPU-mock-1"} 0.73
hami_host_gpu_utilization_ratio{device_uuid="GPU-mock-1"}      20.0

提问:GPU-mock-1 显示已分配 73% 显存、只用了 20%。哪个数字进容量看板,哪个进成本报表?

33

现场:真实 GPU 上的隔离

申请

nvidia.com/gpumem: 3000

看到

Pod 里的 nvidia-smi 显示总显存 3000 MiB。

超额分配在 Pod 内部以 out of memory 失败,邻居负载继续运行。

34

课后:HAMi WebUI

metrics.sh 适合终端,HAMi WebUI 把同样的数字变成看板。它需要集群里有 Prometheus:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add hami-webui https://project-hami.github.io/HAMi-WebUI
helm upgrade --install prometheus prometheus-community/kube-prometheus-stack \
  --version 75.15.1 -n monitoring --create-namespace --set grafana.enabled=false --wait
kubectl apply -f monitoring.yaml   # 抓取 :9395 与 :9394
kubectl apply -f webui-rules.yaml  # 为 WebUI 转换指标名
helm upgrade --install my-hami-webui hami-webui/hami-webui \
  --set externalPrometheus.enabled=true --set dcgm-exporter.enabled=false \
  -n kube-system --wait
kubectl -n kube-system port-forward svc/my-hami-webui 3000:3000

打开 http://localhost:3000。

35

HAMi WebUI 集群总览

HAMi WebUI 集群总览

36

为什么需要 webui-rules.yaml

37

课后:参与贡献

HAMi

make test。从 Fit、PatchAnnotations(pkg/device/nvidia/device.go)或 Filter、Bind(pkg/scheduler/scheduler.go)入手。git commit -s。

HAMi-DRA

把 nvidia.com/gpu 申请变成 DRA ResourceClaim 的 webhook。需要 Kubernetes 1.34+。

mock-device-plugin

在 Allocate 中完成 bind(hami-mock-monitor/upstream/mock-device-plugin 里备好了补丁),或新增一个厂商。

hami-mock-monitor

更多厂商(Ascend、Hygon)、实验用 Grafana 看板、发布镜像。

找带 good first issue 标签的 issue。离开会场前提一个 draft PR。在 PR 中披露 AI 辅助。

38

清理

kind delete cluster --name hami-lab
39

谢谢

宋净超
密瓜智能开源生态 VP
rootsongjc jimmysongio
Reza Jelveh
密瓜智能解决方案架构师
fishman rezajelveh
40
1 / 40

ContainerDays 2026 Singapore · 动手工作坊

从零共享 GPU

基于 HAMi 的 Kubernetes GPU 调度动手实践

宋净超
密瓜智能开源生态 VP
rootsongjc jimmysongio
Reza Jelveh
密瓜智能解决方案架构师
fishman rezajelveh

这 45 分钟

10 月 27 日 12:30 至 13:15

12:30 · Lab 1

安装 HAMi。kind 节点把两块模拟 A100 作为 vGPU 容量上报。

12:40 · Lab 2

共享一块 GPU。细粒度申请、完整拦截链路,以及一个永远调度不上的 Pod。

12:50 · Lab 3

调度策略。卡维度上的 binpack 与 spread,以及拓扑感知。

13:00 · Lab 4

超越默认调度器。KAI Scheduler、Volcano 与 DRA 路径。

13:10 · 收尾

两套指标体系,以及课后可以尝试什么。

实验手册:workshop.project-hami.io/labs/containerdays-singapore

课前准备

所有实验都在笔记本上运行,GPU 是模拟的

git clone https://github.com/Project-HAMi/hami-workshop
cd hami-workshop/examples/containerdays-singapore
git clone https://github.com/fishman/hami-mock-monitor
docker pull kindest/node:v1.37.0
docker pull busybox:1.36
docker pull ghcr.io/llm-d/llm-d-inference-sim:v0.12.0
  • 需要 Docker(或 OrbStack、Colima)、4 核 CPU、8 GB 空闲内存,以及 kind、kubectl、helm、jq、git
  • 只有构建 HAMi 或参与贡献时才需要 Go 1.27+;Lab 4 需要 GitHub 账号
  • 会场 Wi-Fi 慢,请在家提前拉取镜像

为什么要共享 GPU

默认:一个任务独占一块 GPU

这样做的代价

Kubernetes 以整卡为粒度分配 GPU:一块完整设备对应一个任务。

  • 一个 1 GB 的任务会占用整块 80 GB 显卡
  • 大多数 GPU 大部分时间处于闲置
  • 还没有高级调度:没有 binpack、没有 spread、没有拓扑感知

Device Plugin 与 DRA

HAMi 是什么

一块 GPU,多个任务

After HAMi

HAMi 的六项能力

HAMi 为 GPU 调度带来的六件事

异构算力管理

用一套工作流管理 GPU、NPU、MLU 等各类加速器。

硬隔离

运行时对显存与算力做硬隔离切分。

高级调度

binpack、spread 与拓扑感知的放置策略。

Kubernetes 原生

Kubernetes 原生 API,支持 DRA 与 CDI。

资源隔离与 QoS

显存与算力配额,保障共享的公平与稳定。

统一监控

跨厂商一致的指标与可观测性。

申请一份算力

跨厂商统一 API,业务零改造

  • 支持 NVIDIA、Ascend、Cambricon、Hygon、Iluvatar 等
  • 细粒度:显存最小 1 MB,算力最小为单卡的 1%
  • 对任务透明:无需修改代码
  • 容器内硬资源隔离
# NVIDIA:单卡的 15 GB 显存 + 30% 算力
resources:
  limits:
    nvidia.com/gpu: 1
    nvidia.com/gpumem: 15000
    nvidia.com/gpucores: 30
# 昇腾 910C:8GB 显存 + 20% 算力
resources:
  limits:
    huawei.com/Ascend910C: "1"
    huawei.com/Ascend910C-core: "20"
    huawei.com/Ascend910C-memory: "8192"

架构

  • Mutating webhook:把 GPU Pod 路由到 hami-scheduler
  • 调度器扩展:在 Filter 打分、Bind 阶段基于每张卡的状态决策
  • Device plugin:注册显卡,实现 Allocate
  • HAMi-core:容器内的 libvgpu.so,负责限额执行
  • vGPUmonitor:容器级使用量指标

代码入口:cmd/scheduler、cmd/device-plugin、cmd/vGPUmonitor、pkg/device/<vendor>

实验环境:真调度器,假 GPU

实验环境架构

实验环境的两个 mock 组件

mock-device-plugin

把节点注解转换为 nvidia.com/gpumem 与 nvidia.com/gpucores 容量。真实 device plugin 从 NVML 写入同样的注解。

hami-mock-monitor

用负载源替代 GPU,产生 HAMi 的使用量指标(正常来自 vGPUmonitor 与 HAMi-core)。

没有真实 GPU 就少了一样东西:容器内的显存与算力隔离。那是 HAMi-core 的职责,收尾环节在真实显卡上现场演示。

Lab 1:安装 HAMi

Lab 1:一条命令

./setup.sh

这个脚本做了什么:

  • 创建 kind 集群 hami-lab 并给节点打标 gpu=on
  • 安装 HAMi 2.10.0,关闭真实 device plugin,启用 mock plugin
  • 伪造两块 A100-40GB,两个部分:计数资源 nvidia.com/gpu=20:mock plugin 的健康门槛,2 卡 x 10 切分
  • 来自 fake-gpus.json 的节点注解 hami.io/node-nvidia-register,对应真实 device plugin 读取 NVML 后写下的内容
  • 构建并安装 hami-mock-monitor,启用 completeBinds=true(目录不存在会自动克隆)
  • 等待 mock plugin 注册出 gpumem 与 gpucores,打印 allocatable,收尾提示 Ready for Lab 2

结束时的预期输出:

{ "nvidia.com/gpu": "20", "nvidia.com/gpucores": "200",
  "nvidia.com/gpumem": "81920", "nvidia.com/gpumem-percentage": "200" }

Lab 1:观察环境

kubectl -n kube-system get pods | grep hami
kubectl -n kube-system get cm hami-scheduler-device -o yaml | less
kubectl get node hami-lab-control-plane \
  -o jsonpath='{.metadata.annotations.hami\.io/node-nvidia-register}' | jq
[{"id":"GPU-mock-0","index":0,"count":10,"devmem":40960,"devcore":100,
  "type":"NVIDIA-A100-SXM4-40GB","health":true,"numa":0,"mode":"hami-core"}, ...]
  • count:一张卡最多允许多少个 Pod 共享;devmem 单位 MB,devcore 单位百分比
  • 显卡列表保存在节点上,由 device plugin 写入,调度器在 Filter 阶段读取

Lab 2:共享一块 GPU

Lab 2:三个 Pod,两块卡

kubectl apply -f share.yaml -f too-big.yaml
kubectl wait --for=condition=Ready pod/share-1 pod/share-2 pod/share-3 --timeout=120s
kubectl get pods -o wide

share-1..3 各申请 15000 MB 与单卡 30% 算力,都能运行;too-big 申请 45000 MB,超过任何一张 40960 MB 的卡,一直 Pending:

kubectl get events --field-selector involvedObject.name=too-big | tail -2
# 0/1 nodes are available: 1 2/2 CardInsufficientMemory.

45000 MB 没超过节点总量,但装不进任何一张 40960 MB 的卡。

Lab 2:读取 HAMi 的决策

for p in share-1 share-2 share-3; do
  echo -n "$p "; kubectl get pod $p -o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
done
# share-1 GPU-mock-1,NVIDIA,15000,30:;
# share-2 GPU-mock-0,NVIDIA,15000,30:;
# share-3 GPU-mock-1,NVIDIA,15000,30:;

格式为每个设备 uuid,type,memory_mb,cores:,每个容器以 ; 分隔。device plugin 在 Allocate 阶段读取它。

拦截链路

四步,一个节点

  • 准入 webhook 把 schedulerName 改写为 hami-scheduler
  • 调度器扩展在 Filter 阶段读取节点注解 hami.io/node-nvidia-register(Lab 1 的显卡列表),逐卡检查显存与算力余量,选定物理卡 UUID
  • Bind 阶段把 hami.io/vgpu-devices-allocated 写到 Pod 上,置 hami.io/bind-phase=allocating,并持有节点锁 hami.io/mutex.lock
  • device plugin 的 Allocate 挂载 /dev/nvidia* 与 vGPU 文件,目标来自 Pod 注解,随后标记 success 并释放锁
mock plugin 跳过了第 4 步的完成动作,这正是 hami-mock-monitor completeBinds 存在的原因。没有它,下一个 Pod 要等锁最多 5 分钟。

真实节点上,第 4 步还会通过 ld.so.preload 注入 libvgpu.so,HAMi-core 从这一刻起执行显存与算力限额。

Lab 3:调度策略

放置发生在两条轴上

轴注解binpackspread
哪个节点hami.io/node-scheduler-policy更少节点分散到更多节点
节点上的哪张卡hami.io/gpu-scheduler-policy同一张卡分散到不同卡
  • 两条轴都按利用率打分:binpack 偏向更满的卡(整合),spread 偏向更空的卡(打散)
  • 本实验只有一个节点,节点轴只有一个候选
  • 卡轴在两块模拟 A100 上完全可观察

Lab 3: binpack

kubectl delete -f share.yaml -f too-big.yaml
kubectl apply -f binpack.yaml
kubectl wait --for=condition=Ready pod/pack-1 pod/pack-2 --timeout=120s
for p in pack-1 pack-2; do
  echo -n "$p "; kubectl get pod $p -o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
done

两个 Pod 报告同一个 GPU-mock-X:两个负载共享一张卡。为大盘任务腾出整卡。

Lab 3: spread

kubectl delete -f binpack.yaml
kubectl apply -f spread.yaml
kubectl wait --for=condition=Ready pod/spread-1 pod/spread-2 --timeout=120s
for p in spread-1 spread-2; do
  echo -n "$p "; kubectl get pod $p -o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
done

一卡一 Pod。同一集群、同样的申请、不同的策略、不同的放置。

metadata:
  annotations:
    hami.io/gpu-scheduler-policy: spread   # 逐 Pod 指定 spread 或 binpack

拓扑感知

  • 分配注解指定确切的显卡 UUID,选卡就是拓扑决策:多卡节点上它决定 NVLink 与 PCIe 局部性
  • fake-gpus.json 为每张卡携带 numa 字段,与真实 device plugin 从驱动记录的提示相同
  • 卡级 spread 让 Pod 分散在不同卡,binpack 把它们装进一张卡
  • 跨卡与跨节点的显式拓扑感知放置,正是下一个 Lab 里那些调度器补充的能力

Lab 4:超越默认调度器

HAMi 是两层东西

一个隔离层,多个调度大脑

一个隔离层,多个调度大脑

  • 容量模型:你刚用过的扩展资源加调度器扩展
  • HAMi-core:隔离库
  • 其他调度器可以替换放置大脑,保留隔离层

三种使用方式

标准 HAMi

HAMi 调度器扩展 + HAMi device plugin。

nvidia.com/gpu、gpumem、gpucores

Volcano vGPU

Volcano deviceshare + volcano-vgpu-device-plugin。

volcano.sh/vgpu-number、vgpu-memory、vgpu-cores

KAI Scheduler

KAI GPU 共享 + kai-resource-isolator。

gpu-memory 或 gpu-fraction 注解

三者都在容器内使用 HAMi-core 做隔离。一条规则:每个 GPU 节点只跑一条 device-plugin 路径。

KAI Scheduler:与 HAMi 组合

NVIDIA 面向 AI 负载的调度器(CNCF Sandbox):gang 调度、层级公平队列、GPU 纯量共享、拓扑感知放置。

helm install kai-scheduler oci://ghcr.io/nvidia/kai-scheduler --version v0.17.0 \
  --set global.gpuSharing=true --set binder.plugins.hamicore.enabled=true \
  -n kai-scheduler --create-namespace
  • v0.16.4 起,kai-resource-isolator DaemonSet 加 mutating webhook 向 Pod 交付同一个 libvgpu.so
  • 从 gpu-fraction 或 gpu-memory 注解注入 CUDA_DEVICE_MEMORY_LIMIT
  • KAI 负责放置决策,HAMi-core 负责把显存上限变成硬限制

Volcano:共享 GPU 上的 gang 作业

kind: Job                    # batch.volcano.sh/v1alpha1
spec:
  schedulerName: volcano
  minAvailable: 2            # 两个 worker 要么都来,要么都不来
  queue: vgpu-queue
  tasks:
  - replicas: 2
    template:
      spec:
        containers:
        - resources:
            limits:
              volcano.sh/vgpu-number: 1
              volcano.sh/vgpu-memory: 2000
              volcano.sh/vgpu-cores: 30
  • 走 vGPU 路径而非标准 HAMi chart:Volcano deviceshare 插件加 volcano-vgpu-device-plugin
  • 队列 capability 可按团队封顶 vGPU 显存,哪怕节点还有余量
  • 永远不要在同一节点同时运行标准 HAMi device plugin 与 Volcano vGPU 插件

DRA:资源模型变了

  • HAMi-DRA 是一个 mutating webhook,把 nvidia.com/gpu 申请改写为 DRA ResourceClaim
  • 同一份 Pod 清单在 Kubernetes 1.34+(开启 DRA Consumable Capacity 特性门控、运行时启用 CDI)上继续工作
  • 提供 ResourceClaimTemplate 模式以配合 Kueue 准入
  • Pod YAML 不变,变的是底下的机制

github.com/Project-HAMi/HAMi-DRA

这一节不做现场安装:10 分钟装不完,一个模拟节点也用不上。回家可以试 Volcano vGPU 教程:project-hami.io/tutorials/labs/volcano-vgpu-gang-queue

收尾

两套指标体系,以及课后可以尝试什么

预留不等于使用

预留与使用

HAMi 暴露两套指标。把它们分开:一套是预留,一套是使用。

./metrics.sh scheduler   # 分配量,端口 9395
./metrics.sh monitor     # 使用量,端口 9394

两套指标来源

Scheduler :9395,分配量

  • gpu_memory_allocated_bytes
  • gpu_core_allocated_ratio
  • node_gpu_memory_allocated_ratio
  • gpu_shared_count
  • vgpu_memory_allocated_bytes
  • resource_quota_used

vGPUmonitor :9394,使用量

  • vgpu_memory_used_bytes
  • vgpu_memory_limit_bytes
  • container_device_utilization_ratio
  • host_gpu_utilization_ratio
  • host_gpu_memory_used_bytes
  • container_last_kernel_elapsed_seconds

所有指标以 hami_ 开头。标签相同:node、device_uuid、namespace、pod。可在 PromQL 中做连接。

给 mock 一个真实负载

kubectl apply -f sim.yaml      # llm-d-inference-sim,申请量在 0 到 5 之间波动
watch -n 5 "./metrics.sh monitor | grep 'pod=\"sim\"'"
hami_container_device_utilization_ratio{pod="sim"} 26.4
hami_node_gpu_memory_allocated_ratio{device_uuid="GPU-mock-1"} 0.73
hami_host_gpu_utilization_ratio{device_uuid="GPU-mock-1"}      20.0
  • sim 是一个兼容 vLLM 的推理模拟器:利用率在 0 到 33 之间波动(负载 x 分配算力)
  • 显存维持在限制的 0.9 倍,因为 vLLM 会预分配 KV cache

提问:GPU-mock-1 显示已分配 73% 显存、只用了 20%。哪个数字进容量看板,哪个进成本报表?

现场:真实 GPU 上的隔离

申请

nvidia.com/gpumem: 3000

看到

Pod 里的 nvidia-smi 显示总显存 3000 MiB。

超额分配在 Pod 内部以 out of memory 失败,邻居负载继续运行。

课后:HAMi WebUI

metrics.sh 适合终端,HAMi WebUI 把同样的数字变成看板。它需要集群里有 Prometheus:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add hami-webui https://project-hami.github.io/HAMi-WebUI
helm upgrade --install prometheus prometheus-community/kube-prometheus-stack \
  --version 75.15.1 -n monitoring --create-namespace --set grafana.enabled=false --wait
kubectl apply -f monitoring.yaml   # 抓取 :9395 与 :9394
kubectl apply -f webui-rules.yaml  # 为 WebUI 转换指标名
helm upgrade --install my-hami-webui hami-webui/hami-webui \
  --set externalPrometheus.enabled=true --set dcgm-exporter.enabled=false \
  -n kube-system --wait
kubectl -n kube-system port-forward svc/my-hami-webui 3000:3000

打开 http://localhost:3000。

HAMi WebUI 集群总览

HAMi WebUI 集群总览

为什么需要 webui-rules.yaml

  • WebUI 查询 vGPUmonitor 的指标名(hami_memory_size、hami_core_util、hami_container_vgpu_allocated 等)
  • 本实验的调度器与 mock monitor 暴露的是更新的字节、比率命名(hami_gpu_memory_limit_bytes、hami_host_gpu_utilization_ratio 等)
  • PrometheusRule 把一套名字记录成另一套。没有它,所有分配与使用面板都是空的

课后:参与贡献

HAMi

make test。从 Fit、PatchAnnotations(pkg/device/nvidia/device.go)或 Filter、Bind(pkg/scheduler/scheduler.go)入手。git commit -s。

HAMi-DRA

把 nvidia.com/gpu 申请变成 DRA ResourceClaim 的 webhook。需要 Kubernetes 1.34+。

mock-device-plugin

在 Allocate 中完成 bind(hami-mock-monitor/upstream/mock-device-plugin 里备好了补丁),或新增一个厂商。

hami-mock-monitor

更多厂商(Ascend、Hygon)、实验用 Grafana 看板、发布镜像。

找带 good first issue 标签的 issue。离开会场前提一个 draft PR。在 PR 中披露 AI 辅助。

清理

kind delete cluster --name hami-lab

谢谢

宋净超
密瓜智能开源生态 VP
rootsongjc jimmysongio
Reza Jelveh
密瓜智能解决方案架构师
fishman rezajelveh

ContainerDays 2026 Singapore · 动手工作坊

从零共享 GPU

基于 HAMi 的 Kubernetes GPU 调度动手实践

宋净超
密瓜智能开源生态 VP
rootsongjc jimmysongio
Reza Jelveh
密瓜智能解决方案架构师
fishman rezajelveh

这 45 分钟

10 月 27 日 12:30 至 13:15

12:30 · Lab 1

安装 HAMi。kind 节点把两块模拟 A100 作为 vGPU 容量上报。

12:40 · Lab 2

共享一块 GPU。细粒度申请、完整拦截链路,以及一个永远调度不上的 Pod。

12:50 · Lab 3

调度策略。卡维度上的 binpack 与 spread,以及拓扑感知。

13:00 · Lab 4

超越默认调度器。KAI Scheduler、Volcano 与 DRA 路径。

13:10 · 收尾

两套指标体系,以及课后可以尝试什么。

实验手册:workshop.project-hami.io/labs/containerdays-singapore

课前准备

所有实验都在笔记本上运行,GPU 是模拟的

git clone https://github.com/Project-HAMi/hami-workshop
cd hami-workshop/examples/containerdays-singapore
git clone https://github.com/fishman/hami-mock-monitor
docker pull kindest/node:v1.37.0
docker pull busybox:1.36
docker pull ghcr.io/llm-d/llm-d-inference-sim:v0.12.0
  • 需要 Docker(或 OrbStack、Colima)、4 核 CPU、8 GB 空闲内存,以及 kind、kubectl、helm、jq、git
  • 只有构建 HAMi 或参与贡献时才需要 Go 1.27+;Lab 4 需要 GitHub 账号
  • 会场 Wi-Fi 慢,请在家提前拉取镜像

为什么要共享 GPU

默认:一个任务独占一块 GPU

这样做的代价

Kubernetes 以整卡为粒度分配 GPU:一块完整设备对应一个任务。

  • 一个 1 GB 的任务会占用整块 80 GB 显卡
  • 大多数 GPU 大部分时间处于闲置
  • 还没有高级调度:没有 binpack、没有 spread、没有拓扑感知

Device Plugin 与 DRA

HAMi 是什么

一块 GPU,多个任务

After HAMi

HAMi 的六项能力

HAMi 为 GPU 调度带来的六件事

异构算力管理

用一套工作流管理 GPU、NPU、MLU 等各类加速器。

硬隔离

运行时对显存与算力做硬隔离切分。

高级调度

binpack、spread 与拓扑感知的放置策略。

Kubernetes 原生

Kubernetes 原生 API,支持 DRA 与 CDI。

资源隔离与 QoS

显存与算力配额,保障共享的公平与稳定。

统一监控

跨厂商一致的指标与可观测性。

申请一份算力

跨厂商统一 API,业务零改造

  • 支持 NVIDIA、Ascend、Cambricon、Hygon、Iluvatar 等
  • 细粒度:显存最小 1 MB,算力最小为单卡的 1%
  • 对任务透明:无需修改代码
  • 容器内硬资源隔离
# NVIDIA:单卡的 15 GB 显存 + 30% 算力
resources:
  limits:
    nvidia.com/gpu: 1
    nvidia.com/gpumem: 15000
    nvidia.com/gpucores: 30
# 昇腾 910C:8GB 显存 + 20% 算力
resources:
  limits:
    huawei.com/Ascend910C: "1"
    huawei.com/Ascend910C-core: "20"
    huawei.com/Ascend910C-memory: "8192"

架构

  • Mutating webhook:把 GPU Pod 路由到 hami-scheduler
  • 调度器扩展:在 Filter 打分、Bind 阶段基于每张卡的状态决策
  • Device plugin:注册显卡,实现 Allocate
  • HAMi-core:容器内的 libvgpu.so,负责限额执行
  • vGPUmonitor:容器级使用量指标

代码入口:cmd/scheduler、cmd/device-plugin、cmd/vGPUmonitor、pkg/device/<vendor>

实验环境:真调度器,假 GPU

实验环境架构

实验环境的两个 mock 组件

mock-device-plugin

把节点注解转换为 nvidia.com/gpumem 与 nvidia.com/gpucores 容量。真实 device plugin 从 NVML 写入同样的注解。

hami-mock-monitor

用负载源替代 GPU,产生 HAMi 的使用量指标(正常来自 vGPUmonitor 与 HAMi-core)。

没有真实 GPU 就少了一样东西:容器内的显存与算力隔离。那是 HAMi-core 的职责,收尾环节在真实显卡上现场演示。

Lab 1:安装 HAMi

Lab 1:一条命令

./setup.sh

这个脚本做了什么:

  • 创建 kind 集群 hami-lab 并给节点打标 gpu=on
  • 安装 HAMi 2.10.0,关闭真实 device plugin,启用 mock plugin
  • 伪造两块 A100-40GB,两个部分:计数资源 nvidia.com/gpu=20:mock plugin 的健康门槛,2 卡 x 10 切分
  • 来自 fake-gpus.json 的节点注解 hami.io/node-nvidia-register,对应真实 device plugin 读取 NVML 后写下的内容
  • 构建并安装 hami-mock-monitor,启用 completeBinds=true(目录不存在会自动克隆)
  • 等待 mock plugin 注册出 gpumem 与 gpucores,打印 allocatable,收尾提示 Ready for Lab 2

结束时的预期输出:

{ "nvidia.com/gpu": "20", "nvidia.com/gpucores": "200",
  "nvidia.com/gpumem": "81920", "nvidia.com/gpumem-percentage": "200" }

Lab 1:观察环境

kubectl -n kube-system get pods | grep hami
kubectl -n kube-system get cm hami-scheduler-device -o yaml | less
kubectl get node hami-lab-control-plane \
  -o jsonpath='{.metadata.annotations.hami\.io/node-nvidia-register}' | jq
[{"id":"GPU-mock-0","index":0,"count":10,"devmem":40960,"devcore":100,
  "type":"NVIDIA-A100-SXM4-40GB","health":true,"numa":0,"mode":"hami-core"}, ...]
  • count:一张卡最多允许多少个 Pod 共享;devmem 单位 MB,devcore 单位百分比
  • 显卡列表保存在节点上,由 device plugin 写入,调度器在 Filter 阶段读取

Lab 2:共享一块 GPU

Lab 2:三个 Pod,两块卡

kubectl apply -f share.yaml -f too-big.yaml
kubectl wait --for=condition=Ready pod/share-1 pod/share-2 pod/share-3 --timeout=120s
kubectl get pods -o wide

share-1..3 各申请 15000 MB 与单卡 30% 算力,都能运行;too-big 申请 45000 MB,超过任何一张 40960 MB 的卡,一直 Pending:

kubectl get events --field-selector involvedObject.name=too-big | tail -2
# 0/1 nodes are available: 1 2/2 CardInsufficientMemory.

45000 MB 没超过节点总量,但装不进任何一张 40960 MB 的卡。

Lab 2:读取 HAMi 的决策

for p in share-1 share-2 share-3; do
  echo -n "$p "; kubectl get pod $p -o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
done
# share-1 GPU-mock-1,NVIDIA,15000,30:;
# share-2 GPU-mock-0,NVIDIA,15000,30:;
# share-3 GPU-mock-1,NVIDIA,15000,30:;

格式为每个设备 uuid,type,memory_mb,cores:,每个容器以 ; 分隔。device plugin 在 Allocate 阶段读取它。

拦截链路

四步,一个节点

  • 准入 webhook 把 schedulerName 改写为 hami-scheduler
  • 调度器扩展在 Filter 阶段读取节点注解 hami.io/node-nvidia-register(Lab 1 的显卡列表),逐卡检查显存与算力余量,选定物理卡 UUID
  • Bind 阶段把 hami.io/vgpu-devices-allocated 写到 Pod 上,置 hami.io/bind-phase=allocating,并持有节点锁 hami.io/mutex.lock
  • device plugin 的 Allocate 挂载 /dev/nvidia* 与 vGPU 文件,目标来自 Pod 注解,随后标记 success 并释放锁
mock plugin 跳过了第 4 步的完成动作,这正是 hami-mock-monitor completeBinds 存在的原因。没有它,下一个 Pod 要等锁最多 5 分钟。

真实节点上,第 4 步还会通过 ld.so.preload 注入 libvgpu.so,HAMi-core 从这一刻起执行显存与算力限额。

Lab 3:调度策略

放置发生在两条轴上

轴注解binpackspread
哪个节点hami.io/node-scheduler-policy更少节点分散到更多节点
节点上的哪张卡hami.io/gpu-scheduler-policy同一张卡分散到不同卡
  • 两条轴都按利用率打分:binpack 偏向更满的卡(整合),spread 偏向更空的卡(打散)
  • 本实验只有一个节点,节点轴只有一个候选
  • 卡轴在两块模拟 A100 上完全可观察

Lab 3: binpack

kubectl delete -f share.yaml -f too-big.yaml
kubectl apply -f binpack.yaml
kubectl wait --for=condition=Ready pod/pack-1 pod/pack-2 --timeout=120s
for p in pack-1 pack-2; do
  echo -n "$p "; kubectl get pod $p -o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
done

两个 Pod 报告同一个 GPU-mock-X:两个负载共享一张卡。为大盘任务腾出整卡。

Lab 3: spread

kubectl delete -f binpack.yaml
kubectl apply -f spread.yaml
kubectl wait --for=condition=Ready pod/spread-1 pod/spread-2 --timeout=120s
for p in spread-1 spread-2; do
  echo -n "$p "; kubectl get pod $p -o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
done

一卡一 Pod。同一集群、同样的申请、不同的策略、不同的放置。

metadata:
  annotations:
    hami.io/gpu-scheduler-policy: spread   # 逐 Pod 指定 spread 或 binpack

拓扑感知

  • 分配注解指定确切的显卡 UUID,选卡就是拓扑决策:多卡节点上它决定 NVLink 与 PCIe 局部性
  • fake-gpus.json 为每张卡携带 numa 字段,与真实 device plugin 从驱动记录的提示相同
  • 卡级 spread 让 Pod 分散在不同卡,binpack 把它们装进一张卡
  • 跨卡与跨节点的显式拓扑感知放置,正是下一个 Lab 里那些调度器补充的能力

Lab 4:超越默认调度器

HAMi 是两层东西

一个隔离层,多个调度大脑

一个隔离层,多个调度大脑

  • 容量模型:你刚用过的扩展资源加调度器扩展
  • HAMi-core:隔离库
  • 其他调度器可以替换放置大脑,保留隔离层

三种使用方式

标准 HAMi

HAMi 调度器扩展 + HAMi device plugin。

nvidia.com/gpu、gpumem、gpucores

Volcano vGPU

Volcano deviceshare + volcano-vgpu-device-plugin。

volcano.sh/vgpu-number、vgpu-memory、vgpu-cores

KAI Scheduler

KAI GPU 共享 + kai-resource-isolator。

gpu-memory 或 gpu-fraction 注解

三者都在容器内使用 HAMi-core 做隔离。一条规则:每个 GPU 节点只跑一条 device-plugin 路径。

KAI Scheduler:与 HAMi 组合

NVIDIA 面向 AI 负载的调度器(CNCF Sandbox):gang 调度、层级公平队列、GPU 纯量共享、拓扑感知放置。

helm install kai-scheduler oci://ghcr.io/nvidia/kai-scheduler --version v0.17.0 \
  --set global.gpuSharing=true --set binder.plugins.hamicore.enabled=true \
  -n kai-scheduler --create-namespace
  • v0.16.4 起,kai-resource-isolator DaemonSet 加 mutating webhook 向 Pod 交付同一个 libvgpu.so
  • 从 gpu-fraction 或 gpu-memory 注解注入 CUDA_DEVICE_MEMORY_LIMIT
  • KAI 负责放置决策,HAMi-core 负责把显存上限变成硬限制

Volcano:共享 GPU 上的 gang 作业

kind: Job                    # batch.volcano.sh/v1alpha1
spec:
  schedulerName: volcano
  minAvailable: 2            # 两个 worker 要么都来,要么都不来
  queue: vgpu-queue
  tasks:
  - replicas: 2
    template:
      spec:
        containers:
        - resources:
            limits:
              volcano.sh/vgpu-number: 1
              volcano.sh/vgpu-memory: 2000
              volcano.sh/vgpu-cores: 30
  • 走 vGPU 路径而非标准 HAMi chart:Volcano deviceshare 插件加 volcano-vgpu-device-plugin
  • 队列 capability 可按团队封顶 vGPU 显存,哪怕节点还有余量
  • 永远不要在同一节点同时运行标准 HAMi device plugin 与 Volcano vGPU 插件

DRA:资源模型变了

  • HAMi-DRA 是一个 mutating webhook,把 nvidia.com/gpu 申请改写为 DRA ResourceClaim
  • 同一份 Pod 清单在 Kubernetes 1.34+(开启 DRA Consumable Capacity 特性门控、运行时启用 CDI)上继续工作
  • 提供 ResourceClaimTemplate 模式以配合 Kueue 准入
  • Pod YAML 不变,变的是底下的机制

github.com/Project-HAMi/HAMi-DRA

这一节不做现场安装:10 分钟装不完,一个模拟节点也用不上。回家可以试 Volcano vGPU 教程:project-hami.io/tutorials/labs/volcano-vgpu-gang-queue

收尾

两套指标体系,以及课后可以尝试什么

预留不等于使用

预留与使用

HAMi 暴露两套指标。把它们分开:一套是预留,一套是使用。

./metrics.sh scheduler   # 分配量,端口 9395
./metrics.sh monitor     # 使用量,端口 9394

两套指标来源

Scheduler :9395,分配量

  • gpu_memory_allocated_bytes
  • gpu_core_allocated_ratio
  • node_gpu_memory_allocated_ratio
  • gpu_shared_count
  • vgpu_memory_allocated_bytes
  • resource_quota_used

vGPUmonitor :9394,使用量

  • vgpu_memory_used_bytes
  • vgpu_memory_limit_bytes
  • container_device_utilization_ratio
  • host_gpu_utilization_ratio
  • host_gpu_memory_used_bytes
  • container_last_kernel_elapsed_seconds

所有指标以 hami_ 开头。标签相同:node、device_uuid、namespace、pod。可在 PromQL 中做连接。

给 mock 一个真实负载

kubectl apply -f sim.yaml      # llm-d-inference-sim,申请量在 0 到 5 之间波动
watch -n 5 "./metrics.sh monitor | grep 'pod=\"sim\"'"
hami_container_device_utilization_ratio{pod="sim"} 26.4
hami_node_gpu_memory_allocated_ratio{device_uuid="GPU-mock-1"} 0.73
hami_host_gpu_utilization_ratio{device_uuid="GPU-mock-1"}      20.0
  • sim 是一个兼容 vLLM 的推理模拟器:利用率在 0 到 33 之间波动(负载 x 分配算力)
  • 显存维持在限制的 0.9 倍,因为 vLLM 会预分配 KV cache

提问:GPU-mock-1 显示已分配 73% 显存、只用了 20%。哪个数字进容量看板,哪个进成本报表?

现场:真实 GPU 上的隔离

申请

nvidia.com/gpumem: 3000

看到

Pod 里的 nvidia-smi 显示总显存 3000 MiB。

超额分配在 Pod 内部以 out of memory 失败,邻居负载继续运行。

课后:HAMi WebUI

metrics.sh 适合终端,HAMi WebUI 把同样的数字变成看板。它需要集群里有 Prometheus:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add hami-webui https://project-hami.github.io/HAMi-WebUI
helm upgrade --install prometheus prometheus-community/kube-prometheus-stack \
  --version 75.15.1 -n monitoring --create-namespace --set grafana.enabled=false --wait
kubectl apply -f monitoring.yaml   # 抓取 :9395 与 :9394
kubectl apply -f webui-rules.yaml  # 为 WebUI 转换指标名
helm upgrade --install my-hami-webui hami-webui/hami-webui \
  --set externalPrometheus.enabled=true --set dcgm-exporter.enabled=false \
  -n kube-system --wait
kubectl -n kube-system port-forward svc/my-hami-webui 3000:3000

打开 http://localhost:3000。

HAMi WebUI 集群总览

HAMi WebUI 集群总览

为什么需要 webui-rules.yaml

  • WebUI 查询 vGPUmonitor 的指标名(hami_memory_size、hami_core_util、hami_container_vgpu_allocated 等)
  • 本实验的调度器与 mock monitor 暴露的是更新的字节、比率命名(hami_gpu_memory_limit_bytes、hami_host_gpu_utilization_ratio 等)
  • PrometheusRule 把一套名字记录成另一套。没有它,所有分配与使用面板都是空的

课后:参与贡献

HAMi

make test。从 Fit、PatchAnnotations(pkg/device/nvidia/device.go)或 Filter、Bind(pkg/scheduler/scheduler.go)入手。git commit -s。

HAMi-DRA

把 nvidia.com/gpu 申请变成 DRA ResourceClaim 的 webhook。需要 Kubernetes 1.34+。

mock-device-plugin

在 Allocate 中完成 bind(hami-mock-monitor/upstream/mock-device-plugin 里备好了补丁),或新增一个厂商。

hami-mock-monitor

更多厂商(Ascend、Hygon)、实验用 Grafana 看板、发布镜像。

找带 good first issue 标签的 issue。离开会场前提一个 draft PR。在 PR 中披露 AI 辅助。

清理

kind delete cluster --name hami-lab

谢谢

宋净超
密瓜智能开源生态 VP
rootsongjc jimmysongio
Reza Jelveh
密瓜智能解决方案架构师
fishman rezajelveh
7s