HAMi vGPU 调度策略深度解析:基于 Spread 与 Binpack 的资源分配机制
图:HAMi 调度策略执行流程
在前文分析了 HAMi 调度组件的协同机制后,本文聚焦于核心逻辑——如何通过高级调度策略实现 vGPU 资源的高效分配。我们将深入剖析 Spread 与 Binpack 策略在节点与 GPU 层面的具体实现原理。
一、调度策略配置与运行时注入
在部署 HAMi 时可通过 Helm 参数指定调度策略:
helm install vgpu vgpu-charts/vgpu \
--set scheduler.defaultSchedulerPolicy.nodeSchedulerPolicy=binpack \
--set scheduler.defaultSchedulerPolicy.gpuSchedulerPolicy=spread
对应生成的 Deployment 配置如下:
- --node-scheduler-policy=binpack
- --gpu-scheduler-policy=spread
这些参数最终被注入到 hami-scheduler 组件中,作为后续调度决策的依据。
二、节点级调度策略:从打分到排序选择
1. 打分逻辑(资源利用率优先)
每个节点的得分基于其当前资源占用率计算:
func (ns *NodeScore) ComputeScore(devices DeviceUsageList) {
used, usedCore, usedMem := 0, 0, 0
for _, d := range devices.DeviceLists {
used += int(d.Device.Used)
usedCore += int(d.Device.Usedcores)
usedMem += int(d.Device.Usedmem)
}
total, totalCore, totalMem := 0, 0, 0
for _, d := range devices.DeviceLists {
total += int(d.Device.Count)
totalCore += int(d.Device.Totalcore)
totalMem += int(d.Device.Totalmem)
}
useScore := float32(used) / float32(total)
coreScore := float32(usedCore) / float32(totalCore)
memScore := float32(usedMem) / float32(totalMem)
ns.Score = float32(Weight) * (useScore + coreScore + memScore)
}
关键点:得分越高,表示该节点剩余资源越少。
2. 排序策略决定选择结果
排序逻辑由 Less 方法控制,根据策略不同产生相反行为:
func (l NodeScoreList) Less(i, j int) bool {
if l.Policy == "spread" {
return l.NodeList[i].Score > l.NodeList[j].Score // 降序
}
return l.NodeList[i].Score < l.NodeList[j].Score // 升序(默认)
}
- Binpack:升序排列 → 选最后一个节点 → 得分最高 → 剩余资源最少
- Spread:降序排列 → 选最后一个节点 → 得分最低 → 剩余资源最多
✅ 结果验证:
Binpack:集中使用,优先填满单个节点Spread:均衡分布,避免局部过载
3. 条件过滤:确保资源可用性
在打分前会进行预检查,例如:
- 若请求 2 个 GPU,但节点仅有一张卡,则直接排除
- 检查内存、核心是否满足申请要求
过滤逻辑位于 fitInDevices 函数中,确保只有符合条件的节点才会进入评分池。
三、GPU 级调度策略:多卡环境下的精细分配
当一个节点拥有多个 GPU 时,调度器需决定将任务分配至哪一块。
1. 同样采用"反向打分"机制
每块 GPU 的得分同样基于其资源使用率:
func (ds *DeviceListsScore) ComputeScore(requests util.ContainerDeviceRequests) {
request, core, mem := int32(0), int32(0), int32(0)
for _, r := range requests {
request += r.Nums
core += r.Coresreq
if r.MemPercentagereq != 0 && r.MemPercentagereq != 101 {
mem += ds.Device.Totalmem * (r.MemPercentagereq / 100.0)
} else {
mem += r.Memreq
}
}
usedScore := float32(request+ds.Device.Used) / float32(ds.Device.Count)
coreScore := float32(core+ds.Device.Usedcores) / float32(ds.Device.Totalcore)
memScore := float32(mem+ds.Device.Usedmem) / float32(ds.Device.Totalmem)
ds.Score = float32(Weight) * (usedScore + coreScore + memScore)
}
2. 排序策略影响分配顺序
排序逻辑嵌套在 fitInDevices 中:
sort.Sort(node.Devices)
具体比较逻辑如下:
func (l DeviceUsageList) Less(i, j int) bool {
if l.Policy == "binpack" {
if l.DeviceLists[i].Device.Numa == l.DeviceLists[j].Device.Numa {
return l.DeviceLists[i].Score < l.DeviceLists[j].Score
}
return l.DeviceLists[i].Device.Numa > l.DeviceLists[j].Device.Numa
}
// spread
if l.DeviceLists[i].Device.Numa == l.DeviceLists[j].Device.Numa {
return l.DeviceLists[i].Score > l.DeviceLists[j].Score
}
return l.DeviceLists[i].Device.Numa < l.DeviceLists[j].Device.Numa
}
- Binpack:按 NUMA 分组内升序 → 优先选择空闲较少的卡
- Spread:按 NUMA 分组内降序 → 优先选择空闲较多的卡
3. 分配顺序:从后往前遍历
实际分配过程从数组末尾开始迭代:
for i := len(node.Devices.DeviceLists) - 1; i >= 0; i-- {
// 检查资源是否足够...
if k.Nums == 0 {
return true, tmpDevs
}
}
这意味着:一旦找到第一个满足条件的设备,就立即返回,不再继续查找。
因此,最终选择的是排序后列表中的首个有效设备,而该设备的位置取决于排序结果。
四、调度结果传递:通过 Pod Annotations 实现信息同步
为使 DevicePlugin 能准确获取分配结果,系统将目标 GPU 信息写入 Pod 注解:
annotations:
hami.io/vgpu-devices-to-allocate: GPU-1afede84-4e70-2174-49af-f07ebb94d1ae,NVIDIA,20000,30:;
hami.io/vgpu-node: test
hami.io/bind-time: "1732072495"
DevicePlugin 在启动时读取此注解,解析出需要绑定的 GPU 列表,并完成资源注册。
解析逻辑如下:
func DecodeContainerDevices(str string) (ContainerDevices, error) {
parts := strings.Split(str, ",")
tmpdev.UUID = parts[0]
tmpdev.Type = parts[1]
tmpdev.Usedmem = int32(parts[2])
tmpdev.Usedcores = int32(parts[3])
return containerDevices, nil
}
五、总结:策略实现的本质
| 策略类型 | 核心思想 | 实现方式 |
|---|---|---|
| Binpack | 集中资源,提高单节点利用率 | 低得分优先(升序),选最后节点/卡 |
| Spread | 均衡负载,防止热点 | 高得分优先(降序),选最后节点/卡 |
📌 共性设计模式:
- 所有策略均基于"反向打分":剩余资源越少,得分越高
- 排序方向决定选择结果,依赖
sort.Interface的Less方法- 最终通过
Pod.Annotations传递分配结果给下游组件
六、延伸思考
- 如何支持自定义调度策略?可扩展
SchedulerPolicyName枚举并注册新策略。 - 多租户场景下如何隔离调度策略?可通过 Pod Annotation 动态覆盖全局设置。
- GPU 亲和性(NUMA)如何进一步优化?可在排序中引入拓扑感知权重。