调度器
HAMi Scheduler Extender 是 GPU 资源控制平面的决策引擎,通过 Filter、Score、Bind 三段式流程为每个 Pod 选择最优的 GPU 节点和设备。
调度器架构
Scheduler Extender 模式
Kubernetes 原生调度器不理解 GPU 资源的切分与共享模型。HAMi 通过 Scheduler Extender 模式扩展调度能力:kube-scheduler 在调度过程中通过 HTTP 调用 HAMi 的 Extender 端点,将 GPU 相关的决策委托给 HAMi。
HTTP 路由
HAMi Scheduler Extender 暴露以下 HTTP 端点:
| 路由 | 方法 | 调用时机 | 功能 |
|---|---|---|---|
/filter | POST | kube-scheduler 过滤阶段 | 筛选满足 GPU 资源需求的节点 |
/score | POST | kube-scheduler 评分阶段 | 对可行节点进行 GPU 相关评分 |
/bind | POST | kube-scheduler 绑定阶段 | 执行 Pod 到节点的绑定 |
/webhook | POST | 准入控制阶段 | 辅助 Webhook 处理 |
/healthz | GET | 健康检查 | 返回组件健康状态 |
/readyz | GET | 就绪检查 | 返回组件就绪状态 |
节点缓存与 Informer 同步
调度器维护一个内存中的节点资源缓存,通过 Kubernetes Informer 实时同步:
// 伪代码:节点缓存结构
type NodeCache struct {
sync.RWMutex
nodes map[string]*NodeInfo
}
type NodeInfo struct {
Name string
Devices []DeviceInfo // 节点上的 GPU 设备列表
Allocatable DeviceUsage // 可分配资源总量
Used DeviceUsage // 已分配资源量
Pods []PodAllocation // 已调度到该节点的 Pod
}
// Informer 事件处理
func (c *NodeCache) OnNodeUpdate(oldNode, newNode *v1.Node) {
c.Lock()
defer c.Unlock()
info := parseNodeDevices(newNode)
c.nodes[newNode.Name] = info
}
func (c *NodeCache) OnPodUpdate(oldPod, newPod *v1.Pod) {
c.Lock()
defer c.Unlock()
if newPod.Spec.NodeName != "" {
node := c.nodes[newPod.Spec.NodeName]
node.updateAllocation(newPod)
}
}Informer 通过 Watch 机制监听 Node 和 Pod 的变化事件,确保缓存与集群实际状态保持一致,避免每次调度请求都查询 API Server。
Filter 阶段
Filter 阶段的核心任务是:从候选节点列表中筛选出满足 Pod GPU 资源需求的节点。
收集 Pod 资源请求
// 解析 Pod 的 GPU 资源请求
func parsePodGPURequest(pod *v1.Pod) *GPURequest {
req := &GPURequest{}
for _, container := range pod.Spec.Containers {
limits := container.Resources.Limits
if val, ok := limits[HamivGPU]; ok {
req.Count += int(val.Value())
}
if val, ok := limits[HamivGPUMem]; ok {
req.Memory += int(val.Value())
}
if val, ok := limits[HamivGPUCores]; ok {
req.Cores += int(val.Value())
}
}
return req
}遍历候选节点
对每个候选节点,调度器执行一系列检查:
// Filter 阶段伪代码
func Filter(args *extender.ExtenderArgs) (*extender.ExtenderFilterResult, error) {
pod := args.Pod
nodes := args.Nodes.Items
gpuReq := parsePodGPURequest(pod)
// 无 GPU 请求,跳过过滤
if gpuReq.Count == 0 {
return &extender.ExtenderFilterResult{Nodes: args.Nodes}, nil
}
feasibleNodes := []v1.Node{}
failedNodes := map[string]string{}
for _, node := range nodes {
nodeInfo := cache.GetNode(node.Name)
if nodeInfo == nil {
failedNodes[node.Name] = "节点缓存未找到"
continue
}
// 检查 1:设备类型匹配
if !matchDeviceType(pod, nodeInfo) {
failedNodes[node.Name] = "设备类型不匹配"
continue
}
// 检查 2:GPU 资源余量
if !checkGPUFit(nodeInfo, gpuReq) {
failedNodes[node.Name] = "GPU 资源不足"
continue
}
// 检查 3:NUMA 亲和性
if !checkNUMAAffinity(pod, nodeInfo) {
failedNodes[node.Name] = "NUMA 亲和性不满足"
continue
}
// 检查 4:UUID 白黑名单
if !checkUUIDFilter(pod, nodeInfo) {
failedNodes[node.Name] = "UUID 过滤不通过"
continue
}
// 检查 5:设备健康状态
if !checkDeviceHealth(nodeInfo) {
failedNodes[node.Name] = "设备不健康"
continue
}
// 检查 6:配额检查
if !checkQuota(pod.Namespace, gpuReq) {
failedNodes[node.Name] = "命名空间配额超限"
continue
}
feasibleNodes = append(feasibleNodes, node)
}
return &extender.ExtenderFilterResult{
Nodes: &v1.NodeList{Items: feasibleNodes},
FailedNodes: failedNodes,
}, nil
}设备类型匹配
Pod 可以通过注解指定需要的 GPU 型号:
metadata:
annotations:
hami.io/gpu-type: "NVIDIA-A100-SXM4-40GB"调度器在 Filter 阶段比对注解中的设备类型与节点实际设备,排除不匹配的节点。
NUMA 检查
对于需要多 GPU 的工作负载(如分布式训练),NUMA 拓扑影响数据传输性能:
func checkNUMAAffinity(pod *v1.Pod, node *NodeInfo) bool {
numaHint := pod.Annotations["hami.io/numa-hint"]
if numaHint == "" {
return true // 未指定 NUMA 要求,跳过
}
requiredNUMA, _ := strconv.Atoi(numaHint)
for _, device := range node.Devices {
if device.NUMANode == requiredNUMA && device.HasEnoughResources(pod) {
return true
}
}
return false
}UUID 白黑名单
通过注解控制 Pod 可以使用或排除特定的 GPU 设备:
metadata:
annotations:
# 白名单:只使用指定的 GPU
hami.io/gpu-uuid-include: "GPU-abc123,GPU-def456"
# 黑名单:排除指定的 GPU
hami.io/gpu-uuid-exclude: "GPU-xyz789"健康状态与配额检查
- 健康状态调度器从 Device Plugin 上报的设备状态中获取健康信息,排除不健康的设备
- 配额检查结合 Kubernetes ResourceQuota 检查命名空间级别的 GPU 资源配额
Score 阶段
Score 阶段对 Filter 通过的节点进行评分,影响最终的节点选择。
binpack/spread/topology-aware 策略
HAMi 支持三种评分策略,可在全局或 Pod 级别配置:
评分算法
评分的核心是计算每个节点上 GPU 资源的"匹配度":
// 评分阶段伪代码
func Score(args *extender.ExtenderArgs) (map[string]int64, error) {
pod := args.Pod
nodes := args.Nodes.Items
policy := getScorePolicy(pod) // 从注解或全局配置获取
scores := map[string]int64{}
for _, node := range nodes {
nodeInfo := cache.GetNode(node.Name)
switch policy {
case "binpack":
scores[node.Name] = binpackScore(nodeInfo, pod)
case "spread":
scores[node.Name] = spreadScore(nodeInfo, pod)
case "topology-aware":
scores[node.Name] = topologyScore(nodeInfo, pod)
}
}
return scores, nil
}binpack 策略评分
优先将 Pod 调度到 GPU 使用率高的节点,最大化单节点资源利用率:
func binpackScore(node *NodeInfo, pod *v1.Pod) int64 {
var totalScore int64
for _, device := range node.Devices {
// 设备使用率越高,分数越高
memUsage := float64(device.UsedMemory) / float64(device.TotalMemory)
coreUsage := float64(device.UsedCores) / float64(device.TotalCores)
usageScore := int64((memUsage * 0.6 + coreUsage * 0.4) * 100)
totalScore += usageScore
}
return totalScore / int64(len(node.Devices))
}适用场景:资源受限环境,希望尽量少用节点。
spread 策略评分
优先将 Pod 调度到 GPU 使用率低的节点,实现负载均衡:
func spreadScore(node *NodeInfo, pod *v1.Pod) int64 {
var totalScore int64
for _, device := range node.Devices {
memUsage := float64(device.UsedMemory) / float64(device.TotalMemory)
coreUsage := float64(device.UsedCores) / float64(device.TotalCores)
// 使用率越低,分数越高
usageScore := int64((1 - (memUsage*0.6 + coreUsage*0.4)) * 100)
totalScore += usageScore
}
return totalScore / int64(len(node.Devices))
}适用场景:多 GPU 分布式训练,需要设备间高速通信。
topology-aware 策略评分
考虑 GPU 拓扑结构,优先选择同一 NUMA 节点或 PCIe 域内的设备:
func topologyScore(node *NodeInfo, pod *v1.Pod) int64 {
var score int64
// NUMA 亲和性评分(权重 50%)
numaScore := calculateNUMAAffinity(node, pod)
score += numaScore * 50
// PCIe 距离评分(权重 30%)
pcieScore := calculatePCIEDistance(node, pod)
score += pcieScore * 30
// P2P 互访能力评分(权重 20%)
p2pScore := calculateP2PCapability(node, pod)
score += p2pScore * 20
return score
}适用场景:多 GPU 分布式训练,需要设备间高速通信。
节点级 vs 卡级策略
HAMi 支持两层评分策略:
# 全局配置
scheduler:
policy:
nodeSchedulerPolicy: binpack # 节点级策略
gpuSchedulerPolicy: spread # 卡级策略(在同一节点的多张卡之间)- 节点级策略决定选择哪个节点
- 卡级策略在选定节点后,决定选择哪张 GPU 卡
Bind 阶段
Bind 阶段将 Pod 绑定到选定的节点,并记录设备分配决策。
绑定流程
节点加锁
func (s *Scheduler) Bind(args *extender.ExtenderBindingArgs) (*extender.ExtenderBindingResult, error) {
// 1. 获取节点级互斥锁
lock := s.lockManager.Acquire(args.NodeName)
defer lock.Release()
// 2. 读取节点当前资源状态
nodeInfo := s.cache.GetNode(args.NodeName)
// 3. 计算具体的设备分配方案
pod, _ := s.client.CoreV1().Pods(args.PodNamespace).Get(ctx, args.PodName, metav1.GetOptions{})
gpuReq := parsePodGPURequest(pod)
allocation := nodeInfo.ComputeAllocation(gpuReq)
if allocation == nil {
return nil, fmt.Errorf("资源不足以分配")
}
// 4. 写入分配注解
allocJSON, _ := json.Marshal(allocation)
patch := map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]string{
"hami.io/device-allocated": string(allocJSON),
"hami.io/device-bind-phase": "allocating",
"hami.io/bind-node": args.NodeName,
},
},
}
patchPod(s.client, pod.Name, pod.Namespace, patch)
// 5. 执行绑定
bind := &v1.Binding{
ObjectMeta: metav1.ObjectMeta{Name: args.PodName, UID: args.PodUID},
Target: v1.ObjectReference{Kind: "Node", Name: args.NodeName},
}
err := s.client.CoreV1().Pods(args.PodNamespace).Bind(ctx, bind, metav1.CreateOptions{})
// 6. 更新本地缓存
if err == nil {
s.cache.UpdateAllocation(args.NodeName, allocation)
}
return &extender.ExtenderBindingResult{}, err
}释放锁
锁在 defer lock.Release() 中自动释放,无论绑定成功或失败。这确保:
- 绑定成功后,其他 Pod 的调度请求能看到最新的资源状态
- 绑定失败后,资源不会被永久锁定
配额管理
命名空间 ResourceQuota 集成
HAMi 支持通过 Kubernetes 原生 ResourceQuota 限制命名空间级别的 GPU 资源使用:
apiVersion: v1
kind: ResourceQuota
metadata:
name: gpu-quota
namespace: ai-team
spec:
hard:
nvidia.com/gpu: "10" # 最多 10 个 GPU 设备
nvidia.com/gpumem: "40000" # 最多 40000 MB 显存
nvidia.com/cores: "200" # 最多 200% 算力按命名空间限制设备内存总量
调度器在 Filter 阶段检查当前命名空间已使用的 GPU 资源总量:
func checkQuota(namespace string, req *GPURequest) bool {
quota := getResourceQuota(namespace)
used := getNamespaceGPUUsage(namespace)
if used.Memory+req.Memory > quota.Memory {
return false // 显存配额超限
}
if used.Count+req.Count > quota.Count {
return false // GPU 数量配额超限
}
return true
}防止资源滥用
配额管理的设计目标:
- 公平分配多个团队共享集群时,防止单个团队垄断 GPU 资源
- 成本控制通过配额限制各命名空间的 GPU 使用上限
- 自助服务团队在配额范围内自主调度,无需管理员干预
事件增强
Pod 事件中显示成功/失败节点数量
调度器在调度完成后,会在 Pod 事件中记录详细的调度信息:
# 查看调度事件
kubectl describe pod ai-inference
# Events:
# Type Reason From Message
# ---- ------ ---- -------
# Normal Scheduled hami-scheduler Successfully assigned ai-inference to gpu-node-01
# Filter: 3/5 nodes feasible, 2 insufficient GPU memory
# Score: node-01=85, node-02=72, node-03=68
# Device: GPU-abc123 (4000MB/30cores allocated)标准化错误码
| 错误码 | 含义 | 建议操作 |
|---|---|---|
InsufficientGPUMemory | 节点显存不足 | 扩容 GPU 节点或降低显存请求 |
InsufficientGPUCores | 节点算力不足 | 扩容或降低算力请求 |
DeviceTypeMismatch | 设备类型不匹配 | 检查注解中的设备类型 |
DeviceUnhealthy | 设备不健康 | 检查 GPU 硬件状态 |
QuotaExceeded | 命名空间配额超限 | 联系管理员调整配额 |
NUMAUnsatisfied | NUMA 亲和性不满足 | 调整 NUMA 拓扑要求 |
UUIDFiltered | UUID 白黑名单过滤 | 检查 UUID 注解配置 |
日志分级
调度器使用 klog 分级日志:
# v4 级别:节点摘要
kubectl logs -n hami-system hami-scheduler-xxx -v=4
# I0101 "Filter result" node="gpu-01" feasible=true score=85
# I0101 "Filter result" node="gpu-02" feasible=false reason="InsufficientGPUMemory"
# v5 级别:设备细节
kubectl logs -n hami-system hami-scheduler-xxx -v=5
# I0101 "Device fit check" node="gpu-01" device="GPU-abc123" freeMem=8000 reqMem=4000 fit=true
# I0101 "Device fit check" node="gpu-01" device="GPU-def456" freeMem=2000 reqMem=4000 fit=false领导者选举与节点锁
领导者选举
多副本调度器通过 Kubernetes Lease 实现领导者选举:
// 领导者选举配置
leaderelection.LeaderElectionConfig{
Lock: &resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: "hami-scheduler-leader",
Namespace: "hami-system",
},
Client: client.CoordinationV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: hostname,
},
},
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: runScheduler,
OnStoppedLeading: func() {
klog.Info("Leader election lost, shutting down")
os.Exit(1)
},
},
}节点锁实现
type NodeLockManager struct {
locks map[string]*sync.Mutex
mu sync.Mutex
}
func (m *NodeLockManager) Acquire(nodeName string) *sync.Mutex {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.locks[nodeName]; !ok {
m.locks[nodeName] = &sync.Mutex{}
}
m.locks[nodeName].Lock()
return m.locks[nodeName]
}节点锁保障:即使同一个 Leader 调度器并行处理多个 Pod 的 Bind 请求,同一节点的资源分配操作也是串行执行的。
调度器配置
以下是通过 Helm values.yaml 配置调度器的完整示例:
# values.yaml - 调度器配置
scheduler:
enabled: true
nameOverride: "hami-scheduler"
# 镜像配置
image:
repository: projecthami/hami
tag: v2.5.0
pullPolicy: IfNotPresent
# 副本数(建议 >= 2)
replicas: 3
# 调度策略
policy:
nodeSchedulerPolicy: binpack # binpack | spread
gpuSchedulerPolicy: binpack # binpack | spread | topology-aware
# 资源超卖配置
overcommit:
enabled: true
deviceMemoryScaling: 1.0 # 显存超卖比例
deviceCoreScaling: 1.0 # 算力超卖比例
# 领导者选举
leaderElect:
enabled: true
leaseDuration: 15s
renewDeadline: 10s
retryPeriod: 2s
# 节点缓存同步
cache:
resyncPeriod: 300s # 全量同步周期
watchTimeout: 30s # Watch 超时
# 资源限制
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
# 优先级
priorityClassName: "system-cluster-critical"
# 反亲和性
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: hami-scheduler
topologyKey: kubernetes.io/hostname
# 容忍度
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"Scheduler Extender 注册
kube-scheduler 需要配置 Extender 以调用 HAMi:
# kube-scheduler 配置(ConfigMap)
apiVersion: v1
kind: ConfigMap
metadata:
name: scheduler-config
namespace: kube-system
data:
scheduler-config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: hami-scheduler
extenders:
- urlPrefix: "http://hami-scheduler.hami-system:9000"
filterVerb: "/filter"
scoreVerb: "/score"
bindVerb: "/bind"
weight: 1
managedResources:
- name: nvidia.com/gpu
- name: nvidia.com/gpumem
- name: nvidia.com/cores小结
本章详细介绍了 HAMi Scheduler Extender 的设计与实现:
- 架构:SchedulerExtender 模式,HTTP 路由,节点缓存与 Informer 同步
- Filter:阶段设备类型匹配、资源余量检查、NUMA 亲和性、UUID 过滤、健康状态和配额检查
- Score:阶段 binpack/spread/topology-aware 三种评分策略,节点级与卡级双层策略
- Bind:阶段节点加锁、分配注解写入、绑定执行、锁释放
- 配额管理:命名空间 ResourceQuota 集成,防止资源滥用
- 事件增强:调度详情事件、标准化错误码、分级日志
- 高可用:领导者选举、节点级锁
在下一章中,我们将深入 Device Plugin 和设备抽象层的实现细节。