欢迎光临
我们一直在努力

AI 云原生调度引擎:从 GPU 感知编排到弹性伸缩的架构设计

AI 云原生调度引擎:从 GPU 感知编排到弹性伸缩的架构设计

一、GPU 资源碎片与调度延迟:AI 工作负载的编排困境

Kubernetes 原生调度器为通用工作负载设计,对 AI 推理和训练任务的调度需求缺乏感知。最典型的痛点是 GPU 资源碎片化:集群中剩余 4 张 A100,但分布在 4 个不同的节点上,无法满足一个需要 4 卡张量并行的推理任务。原生调度器只能看到"每节点 1 张空闲 GPU",无法识别跨节点的 GPU 拓扑约束。

AI 工作负载的调度延迟同样不可忽视。训练任务启动需要加载模型权重到 GPU(70B 模型约需 2 分钟),原生调度器在 Pod 调度成功后才开始拉取镜像和加载模型。如果调度到的节点 GPU 显存不足(被其他任务占用),任务会因 OOM 失败,调度器重新调度,形成"调度-失败-重调度"的恶性循环。

更深层的问题在于,AI 工作负载的弹性伸缩与通用 Web 服务截然不同。推理服务的扩容需要等待模型加载(冷启动 1-3 分钟),不能像 Web 服务那样秒级扩容。训练任务的缩容需要等待 Checkpoint 保存(可能需要数十秒),直接 Kill Pod 会导致数小时的训练成果丢失。

二、AI 调度引擎的核心机制与架构模型

AI 调度引擎需要在 Kubernetes 原生调度器之上,增加 GPU 拓扑感知、工作负载优先级和弹性伸缩策略三个维度的调度能力。

flowchart TB
A[AI 工作负载提交] –> B[调度策略引擎]
B –> C{工作负载类型}

C –>|推理服务| D[GPU 拓扑感知调度]
C –>|训练任务| E[排队与优先级调度]
C –>|批处理| F[抢占式调度]

D –> G{GPU 拓扑检查}
G –>|NVLink 互联| H[调度到同节点]
G –>|PCIe 互联| I[调度到跨节点]

E –> J[优先级队列]
J –> K{资源是否充足?}
K –>|充足| L[立即调度]
K –>|不足| M[排队等待或抢占低优先级任务]

F –> N[利用空闲 GPU 时间片]
N –> O[高优先级任务到达时被抢占]

subgraph 弹性伸缩控制器
P[指标采集] –> Q{GPU 利用率 > 阈值?}
Q –>|是| R[扩容:预热新实例]
Q –>|否| S[缩容:等待 Checkpoint]
R –> T[模型预热池]
S –> U[安全缩容]
end

style B fill:#e74c3c,color:#fff
style G fill:#3498db,color:#fff
style P fill:#27ae60,color:#fff

GPU 拓扑感知调度:GPU 之间的互联方式(NVLink vs PCIe)直接影响张量并行的通信效率。调度器需要读取节点的 GPU 拓扑信息,将需要多卡并行的任务优先调度到 NVLink 互联的 GPU 组上。这需要通过 Device Plugin 上报 GPU 拓扑信息,调度器在 Filter 阶段增加拓扑约束检查。

排队与优先级调度:当集群 GPU 资源不足时,训练任务需要排队等待。调度器维护一个优先级队列,高优先级任务可以抢占低优先级任务的 GPU 资源。抢占不是直接 Kill,而是发送优雅终止信号,等待被抢占任务完成 Checkpoint 保存后再释放资源。

模型预热池:为解决推理服务的冷启动问题,维护一组预加载模型的备用 Pod。当扩容触发时,从预热池中取出已加载模型的 Pod 直接接入流量,将扩容延迟从分钟级缩短到秒级。预热池的大小根据历史流量模式动态调整。

三、AI 调度引擎的生产级实现

3.1 GPU 拓扑感知调度器

package scheduler

import (
"context"
"sort"

v1 "k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/scheduler/framework"
)

// GPUTopologyScheduler GPU 拓扑感知调度插件
// 实现 Kubernetes Scheduler Framework 的 Filter 和 Score 接口
type GPUTopologyScheduler struct {
handle framework.Handle
topology *GPUTopologyCache
}

// Filter 阶段:过滤不满足 GPU 拓扑约束的节点
// 为什么在 Filter 而非 Score 阶段做:拓扑约束是硬性要求,
// PCIe 互联的跨节点调度会导致通信延迟翻倍,不可接受
func (s *GPUTopologyScheduler) Filter(ctx context.Context,
state *framework.CycleState,
pod *v1.Pod,
nodeInfo *framework.NodeInfo) *framework.Status {

gpuRequest := getGPURequest(pod)
if gpuRequest == 0 {
return framework.NewStatus(framework.Success)
}

nodeName := nodeInfo.Node().Name
nodeGPU := s.topology.GetNodeGPU(nodeName)

// 检查节点空闲 GPU 数量
if nodeGPU.FreeGPUCount() < gpuRequest {
return framework.NewStatus(framework.Unschedulable,
"insufficient GPU resources")
}

// 多卡任务检查 GPU 互联拓扑
if gpuRequest > 1 {
// 查找满足 GPU 数量的互联组
groups := nodeGPU.GetNVLinkGroups()
hasNVLinkGroup := false
for _, group := range groups {
freeInGroup := group.FreeGPUCount()
if freeInGroup >= gpuRequest {
hasNVLinkGroup = true
break
}
}
if !hasNVLinkGroup {
// 没有 NVLink 组满足需求,标记为低优先级
// 不直接拒绝,允许降级到 PCIe 互联
return framework.NewStatus(framework.Success,
"no NVLink group, will use PCIe")
}
}

return framework.NewStatus(framework.Success)
}

// Score 阶段:优先选择 GPU 拓扑最优的节点
// 评分逻辑:NVLink 互联 > 同节点 PCIe > 跨节点
func (s *GPUTopologyScheduler) Score(ctx context.Context,
state *framework.CycleState,
pod *v1.Pod,
nodeName string) (int64, *framework.Status) {

gpuRequest := getGPURequest(pod)
if gpuRequest <= 1 {
// 单卡任务:选择空闲 GPU 最多的节点,减少碎片
nodeGPU := s.topology.GetNodeGPU(nodeName)
return int64(nodeGPU.FreeGPUCount()) * 10, nil
}

// 多卡任务:NVLink 组得分最高
nodeGPU := s.topology.GetNodeGPU(nodeName)
groups := nodeGPU.GetNVLinkGroups()

maxScore := int64(0)
for _, group := range groups {
if group.FreeGPUCount() >= gpuRequest {
// NVLink 互联组:满分 100
maxScore = 100
break
}
}
if maxScore == 0 {
// 同节点 PCIe 互联:60 分
if nodeGPU.FreeGPUCount() >= gpuRequest {
maxScore = 60
}
}
return maxScore, nil
}

3.2 推理服务弹性伸缩控制器

package autoscaler

import (
"context"
"time"
)

// AIInferenceAutoscaler AI 推理服务弹性伸缩控制器
// 与标准 HPA 的核心区别:支持模型预热和冷启动感知
type AIInferenceAutoscaler struct {
k8sClient KubernetesClient
metricsClient MetricsClient
warmPool *ModelWarmPool
// 扩缩容配置
scaleConfig ScaleConfig
}

type ScaleConfig struct {
// GPU 利用率扩容阈值
ScaleUpGPUUtilization float64
// GPU 利用率缩容阈值
ScaleDownGPUUtilization float64
// 扩容冷却时间(避免抖动)
ScaleUpCoolDown time.Duration
// 缩容冷却时间(比扩容更长,避免频繁缩容)
ScaleDownCoolDown time.Duration
// 预热池最小实例数
WarmPoolMinSize int
// 最大实例数
MaxReplicas int
// 最小实例数
MinReplicas int
}

// Reconcile 核心调谐逻辑
func (a *AIInferenceAutoscaler) Reconcile(ctx context.Context,
deploymentName string) error {

// 1. 采集当前指标
metrics, err := a.metricsClient.GetGPUMetrics(ctx, deploymentName)
if err != nil {
return err
}

currentReplicas, err := a.k8sClient.GetReplicas(ctx, deploymentName)
if err != nil {
return err
}

// 2. 扩容决策
if metrics.AvgGPUUtilization > a.scaleConfig.ScaleUpGPUUtilization {
targetReplicas := calculateTargetReplicas(
currentReplicas, metrics.AvgGPUUtilization,
a.scaleConfig.ScaleUpGPUUtilization)

if targetReplicas > currentReplicas &&
targetReplicas <= a.scaleConfig.MaxReplicas {

// 优先从预热池获取已加载模型的实例
warmPods := a.warmPool.AvailablePods()
if len(warmPods) > 0 {
// 从预热池取出 Pod,直接接入流量
err = a.warmPool.PromoteToActive(ctx, warmPods[0])
} else {
// 预热池为空,冷启动新实例
err = a.k8sClient.ScaleUp(ctx, deploymentName, 1)
}
return err
}
}

// 3. 缩容决策:选择 GPU 利用率最低的实例
if metrics.AvgGPUUtilization < a.scaleConfig.ScaleDownGPUUtilization &&
currentReplicas > a.scaleConfig.MinReplicas {

// 选择活跃连接数最少的实例进行缩容
// 为什么选最少连接而非随机:避免缩容正在处理请求的实例
victim, err := a.selectVictimInstance(ctx, deploymentName)
if err != nil {
return err
}

// 优雅缩容:先从负载均衡摘除,等待请求处理完成
err = a.k8sClient.DrainAndScaleDown(ctx, victim)
return err
}

return nil
}

3.3 训练任务排队与抢占调度

# 训练任务提交配置
# 使用 Kueue 或 Volcano 等批调度器实现排队和抢占
apiVersion: kueue.x-k8s.io/v1beta1
kind: Workload
metadata:
name: training-job-llama70b
spec:
priorityClassName: high-priority-training
podSets:
– name: trainer
template:
spec:
containers:
– name: trainer
image: pytorch/training:latest
resources:
requests:
nvidia.com/gpu: 8
memory: "256Gi"
cpu: "32"
limits:
nvidia.com/gpu: 8
memory: "256Gi"
cpu: "32"
# 优雅终止配置:保存 Checkpoint 后再退出
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c",
"python save_checkpoint.py && sleep 10"]
terminationGracePeriodSeconds: 300 # 给 Checkpoint 留足时间
# 队列配置:高优先级训练队列
queueName: high-priority-training

# 优先级类定义
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority-training
value: 1000
globalDefault: false
description: "高优先级训练任务,可抢占低优先级任务"

四、AI 调度引擎的架构代价与适用边界

GPU 拓扑感知的调度延迟:Filter 阶段需要查询每个节点的 GPU 拓扑信息,当集群规模超过 100 个节点时,调度延迟从毫秒级增加到百毫秒级。解决方案是将 GPU 拓扑信息缓存在调度器内存中,仅在节点状态变更时更新缓存。

模型预热池的资源浪费:预热池中的备用 Pod 占用 GPU 资源但不处理请求,资源利用率为 0。如果预热池维护 2 个备用 Pod(每个 4 卡 A100),相当于浪费 8 张 A100 的算力。需要根据业务流量的波动性来权衡预热池大小:流量波动大时预热池收益高,流量平稳时预热池浪费严重。

抢占式调度的 Checkpoint 风险:训练任务被抢占时需要保存 Checkpoint,但 Checkpoint 保存本身可能失败(磁盘满、网络中断)。如果 Checkpoint 保存失败,被抢占任务的训练进度将丢失。生产环境中,建议配置定期自动 Checkpoint(每 N 步保存一次),而非仅在抢占时保存。

适用边界:该架构适用于拥有 50+ GPU 节点的中大型 AI 集群。对于 10 个节点以下的小型集群,原生调度器配合简单的标签调度即可满足需求。对于纯推理服务(无训练任务),可以简化调度策略,去掉排队和抢占逻辑,只保留 GPU 拓扑感知和弹性伸缩。

五、总结

AI 调度引擎的核心价值是将 GPU 拓扑感知、工作负载优先级和冷启动感知注入 Kubernetes 调度决策。原生调度器无法理解 GPU 互联拓扑和模型加载延迟,导致资源碎片化和调度效率低下。

落地路线建议:第一步,部署 GPU Device Plugin 并上报拓扑信息;第二步,实现 GPU 拓扑感知调度插件,注册到 Kubernetes Scheduler Framework;第三步,部署推理服务的弹性伸缩控制器,配置预热池和冷启动策略;第四步,引入批调度器(Kueue/Volcano)实现训练任务的排队和抢占。每一步都需要在真实工作负载下验证调度决策的正确性,特别是多卡任务的拓扑分配和抢占的 Checkpoint 安全性。

赞(0)
未经允许不得转载:171主机测评 » AI 云原生调度引擎:从 GPU 感知编排到弹性伸缩的架构设计
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址