
一张 GPU 怎么分给多个 Pod
先用一个具体例子建立直觉。
节点上有一张 NVIDIA A100(80GB 显存),DeviceSplitCount=10,意味着 HAMi 把这张 GPU 注册为 10 个”虚拟设备”给 kubelet。现在两个 Pod 先后来申请资源:
- Pod A 请求 1 GPU、30GB 显存、50% 算力
- Pod B 请求 1 GPU、20GB 显存、30% 算力
调度器在内存中维护每个设备的用量账本(DeviceUsage):
Pod A 通过检查(剩余显存 51200 ≥ 30720,剩余算力 50 ≥ 50,分片数 9 > 0),分配成功。Pod B 到来时,剩余显存 51200 ≥ 20480,剩余算力 50 ≥ 30,分片数 8 > 0,也通过。
但光在调度器里记账不够——容器启动后,Pod A 的进程凭什么只用 30GB 显存?这就是 HAMi 的两层机制:「调度器做资源记账和分配决策,容器内通过 libvgpu.so 劫持 CUDA API 做硬限制」。
四个组件怎么协作
下面逐层拆解。
1. MutatingWebhook:准入拦截
「入口」:pkg/scheduler/webhook.go:53Handle()
Webhook 拦截所有 Pod 创建请求,做三件事:
「a) 资源请求规范化」
调用每个设备厂商的 MutateAdmission()(pkg/device/nvidia/device.go:328)。NVIDIA 的实现包括:
- 验证显存百分比(0-100)
- 如果容器只请求了
nvidia.com/gpumem但没请求nvidia.com/gpu,自动补上 count 资源 - 如果请求了 100% 显存但没指定算力,自动设为 100 核(独占语义)
「b) 注入调度器名」
如果 Pod 请求了 HAMi 管理的资源,将 pod.Spec.SchedulerName 设为 "hami-scheduler"(webhook.go:107)。这样 kube-scheduler 才会把 Pod 转发给 HAMi 的 extender。
「c) 安全检查」
特权容器不允许使用 GPU(webhook.go:99)。因为 libvgpu.so 的拦截是协作式的,特权容器可以绕过 LD_PRELOAD,所以直接在准入阶段拒绝。
2. HAMi Scheduler:设备感知调度
「入口」:pkg/scheduler/scheduler.go:1009Filter()
HAMi 以 Kubernetes Scheduler Extender 的方式运行,注册了 /filter 和 /bind 两个 HTTP 路由。「关键设计:Filter 阶段同时完成 filter + score + node selection,直接返回单个节点。」
2.1 设备资源从哪来
设备插件在每个节点上运行,通过 NVML 获取 GPU 信息,每 30 秒将设备规格写入节点 annotation:
格式:{UUID},{可分片数},{显存MB},{算力%},{型号},{NUMA},{健康}
调度器的 RegisterFromNodeAnnotations()(scheduler.go:404)持续同步这些信息到内存缓存 nodeManager。
2.2 资源请求解析
device.Resourcereqs()(devices.go:636)遍历 Pod 的所有容器,调用每个厂商的 GenerateResourceRequests()(nvidia/device.go:501),从 Resources.Limits 中解析出:
| K8s 资源名 | 含义 | 解析结果 |
nvidia.com/gpu |
GPU 数量 | Nums |
nvidia.com/gpumem |
显存(MB) | Memreq |
nvidia.com/gpucores |
算力(%) | Coresreq |
nvidia.com/gpumem-percentage |
显存百分比 | MemPercentagereq |
2.3 节点使用快照
getNodesUsage()(scheduler.go:720)构建调度时刻的设备用量快照:
- 从
nodeManager读取所有节点的设备注册信息,初始化DeviceUsage(Used=0, Usedmem=0, Usedcores=0) - 遍历
podManager中所有已分配 Pod,累加使用量:
- 返回快照供评分使用
2.4 评分与设备匹配
calcScore()(score.go:342)对每个节点并发评分。核心是 scoreNode()(score.go:276):
设备级匹配在 fitInDevices()(score.go:63)中调用厂商的 Fit() 方法。
2.5 NVIDIA Fit:逐设备检查
「pkg/device/nvidia/device.go:670」Fit() 是设备共享调度的核心。遍历节点上的每张 GPU,按以下顺序检查:
全部通过则分配成功,调用 AddResourceUsage() 累加用量。
2.6 调度策略
两级调度策略,可通过 Pod annotation 覆盖:
| 层级 | 策略 | 行为 | 配置 |
| 节点级 | binpack
(默认) |
优先选使用率高的节点 | --node-scheduler-policy |
| 节点级 | spread |
优先选使用率低的节点 | |
| 设备级 | spread
(默认) |
优先选空闲 GPU | --gpu-scheduler-policy |
| 设备级 | binpack |
优先选已占用的 GPU | |
| 设备级 | mutex |
只选完全空闲的 GPU | |
| 设备级 | topology-aware |
基于 GPU 间 P2P 拓扑选最优组合 |
2.7 分配结果写入 Pod annotation
调度器调用 PatchAnnotations()(nvidia/device.go:486)将分配结果写入 Pod annotation:
格式:{UUID},{类型},{显存MB},{算力%}: 每个设备一组,; 分隔容器。
2.8 Bind 阶段
Bind()(scheduler.go:940)不重新计算分配,只做两件事:
- 加节点锁(通过 annotation
hami.io/mutex.lock实现互斥,防止并发调度冲突) - 调用 K8s API 绑定 Pod 到 Node
3. DevicePlugin:注入隔离层
「入口」:pkg/device-plugin/nvidiadevice/nvinternal/plugin/server.go:737Allocate()
kubelet 在创建容器前调用 DevicePlugin 的 Allocate(),HAMi 的设备插件在这里完成容器层面的资源隔离。
3.1 读取调度决策
decodePodSingleDevice() 从 Pod annotation 解析出设备分配信息。popNextContainerDevices()(util.go:112)按顺序弹出每个容器的设备——annotation 中 init 容器在前,app 容器在后。
3.2 注入环境变量
| 环境变量 | 作用 |
CUDA_DEVICE_MEMORY_LIMIT_{i} |
第 i 个设备的显存上限(MB) |
CUDA_DEVICE_SM_LIMIT |
SM 利用率限制(0-100) |
CUDA_DEVICE_MEMORY_SHARED_CACHE |
共享内存缓存文件路径 |
CUDA_OVERSUBSCRIBE |
超售开关(DeviceMemoryScaling > 1 时开启) |
GPU_CORE_UTILIZATION_POLICY |
算力限制策略(default/force/disable) |
3.3 挂载 libvgpu.so
ld.so.preload 文件内容为 /usr/local/vgpu/libvgpu.so。Linux 动态链接器在加载任何程序时,会先加载 ld.so.preload 中列出的共享库。这样 libvgpu.so 就被注入到容器内所有进程。
3.4 设备副本机制
设备插件向 kubelet 注册设备时,每张物理 GPU 被报告为 DeviceSplitCount 个独立设备(rm/devices.go:194GetPluginDevices())。ID 格式为 {原始UUID}-{副本序号},如 GPU-xxx-0, GPU-xxx-1。kubelet 将它们视为独立资源进行分配。
4. libvgpu.so:容器内 CUDA API 劫持
libvgpu.so 由 HAMi-core 项目编译,是设备共享在容器层面的执行者。
4.1 工作原理
4.2 共享内存协调
pkg/monitor/nvidia/v1/spec.go:57 定义了共享内存区域格式 sharedRegionT:
多个容器共享同一物理 GPU 时,通过共享内存区域协调各自的资源使用量。/tmp/vgpulock 提供进程间锁,确保共享内存操作的原子性。
4.3 安全边界
libvgpu.so 的拦截是「协作式」的,不是安全边界。有特权的容器可以通过取消 LD_PRELOAD 绕过限制。这也是 Webhook 拒绝特权容器使用 GPU 的原因。对于不可信工作负载,应使用 MIG 硬件隔离。
三种共享模式对比
NVIDIA 设备支持三种虚拟化模式(nvidia/device.go:54):
| 维度 | hami-core(软隔离) | MIG(硬件隔离) | MPS |
| 「隔离机制」 | CUDA API 劫持 | GPU 硬件分区 | NVIDIA MPS 服务 |
| 「隔离强度」 | 弱(可绕过) | 强(硬件级) | 中 |
| 「显存隔离」 | 环境变量 + libvgpu.so | 硬件独立显存 | MPS 共享 |
| 「算力隔离」 | SM 利用率限制 | 硬件独立 SM | MPS 调度 |
| 「切分粒度」 | 任意显存 + 0-100% 算力 | 固定 profile(1g.5gb 等) | — |
| 「支持 GPU」 | 所有 NVIDIA GPU | 仅 A100/H100 等 | 支持 MPS 的 GPU |
| 「超售」 | 支持 | 不支持 | — |
| 「故障隔离」 | 无 | 有 | 无 |
MIG 切分的关键逻辑
MIG 模式下,调度器从 MigProfiles 中选择合适的 profile 和 placement(nvidia/device.go:598selectMigCandidate()):
- 按 MemoryMB 升序排序,优先选最小的能满足需求的 profile
- 检查 placement 是否与已占用的位置冲突(
mig_topology.go:78migPlacementsOverlap()) - 分配后修正
Usedmem和Usedcores为 profile 实际值(不是请求值)
init 容器的资源折叠
init 容器和 app 容器的资源使用模式不同:init 容器顺序执行,app 容器并行执行。HAMi 在调度时取 max(init 峰值, app 总和) 作为有效使用量(initContainer.go:35CollapseInitContainerUsage())。
当所有 init 容器成功完成后,调度器检测到 AllInitContainersSucceeded(scheduler.go:204),将 Pod 的设备使用量从”折叠后的峰值”收缩为”仅 app 容器的使用量”,释放多余的资源配额。
配额管理
QuotaManager(quota.go:27)维护 namespace 级别的设备资源配额:
- 监听 K8s
ResourceQuota对象变化,读取limits.前缀的配额项 FitQuota()(quota.go:62)检查Used + memreq > Limit→ 拒绝LimitSet字段区分”显式限制为 0″和”未设置限制”MemoryFactor处理不同厂商的显存单位差异(如寒武纪 MLU 以 256 MiB 为单位)
异构设备统一管理
所有厂商设备实现统一的 Devices 接口(devices.go:34),调度器代码完全统一:
调度时通过 device.GetDevices()[k.Type] 查找对应厂商的实现,调用 Fit() 进行匹配。当前支持 15+ 种设备:NVIDIA、AMD、寒武纪、昇腾、海光 DCU、燧原、摩尔线程、MetaX、昆仑、壁仞、天数智芯、AWS Neuron 等。
文章来自:51CTO
![]()
