共计 2511 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点:静态调度之殇
在 AI 训练和推理场景中,我们常常遇到这样的尴尬:
- GPU 显存碎片化 :某个任务申请了 40GB 显存中的 32GB,导致剩余 8GB 无法被其他任务利用,就像停车场里停了一辆大卡车,旁边的小车位全废了
- TPU 利用率过山车 :训练任务初期数据加载阶段 TPU 利用率不到 30%,等到反向传播时又冲到 90%,资源使用像心电图一样波动
- 冷启动延迟 :每次调度新任务都要重新加载模型,10 分钟的训练任务有 3 分钟花在环境准备上
传统 Kubernetes 默认调度器采用静态资源分配策略,就像用固定菜谱应付所有客人——不管你是四川人还是广东人,统统给一盘宫保鸡丁。这导致我们的 A100 集群平均利用率长期徘徊在 40% 左右。
算法设计:动态优先级调度
调度器选型对比
| 维度 | Kubernetes 默认调度器 | 自定义动态调度器 |
|---|---|---|
| 决策频率 | 任务提交时 | 实时 (10s/ 次) |
| 资源考量 | 显存 /CPU 请求量 | 多维动态评分 |
| 硬件亲和性 | 无 | NUMA 感知 |
| 抢占机制 | 全有或全无 | 分片抢占 |
动态优先级公式
# 动态优先级 = α* 资源匹配度 + β* 任务紧急度 + γ* 硬件亲和性
# 其中 α +β+γ=1,我们生产环境取值 α =0.6,β=0.3,γ=0.1
def calculate_priority(task, node):
# 资源需求预测(基于历史窗口移动平均)resource_fit = 1 - abs(task. 显存预测 - node. 可用显存)/node. 总显存
# 任务紧急度(SLA 剩余时间占比)urgency = min(1, task. 已运行时间 /task.SLA 总时间)
# 硬件亲和性(NUMA 节点距离评分)affinity = 0
if task. 历史运行节点 == node.id:
affinity = 0.8 # 缓存亲和
elif node.numa_distance <= 2:
affinity = 0.5
return 0.6*resource_fit + 0.3*urgency + 0.1*affinity
代码实现:带权重的最优匹配
from dataclasses import dataclass
from typing import List
import heapq
@dataclass
class DeviceProfile:
node_id: str
gpu_type: str # e.g. 'A100-40GB'
free_mem: float # GB
numa_distance: int # 0-3
@dataclass
class TaskRequirement:
task_id: str
min_mem: float # GB
preferred_nodes: List[str] # 历史运行节点
sla_total: int # 秒
elapsed: int # 已运行秒数
class DynamicScheduler:
def __init__(self, nodes: List[DeviceProfile]):
self.node_heap = []
for node in nodes:
# 使用最大堆,按可用资源排序
heapq.heappush(self.node_heap, (-node.free_mem, node))
def schedule(self, tasks: List[TaskRequirement]) -> dict:
"""时间复杂度 O(nlogn + mlogn),n 为节点数,m 为任务数"""
assignments = {}
temp_heap = self.node_heap.copy()
# 给任务按紧急度排序
sorted_tasks = sorted(tasks,
key=lambda t: t.elapsed/t.sla_total,
reverse=True)
for task in sorted_tasks:
best_node = None
max_score = -1
# 临时弹出节点检查(避免破坏原堆)temp_nodes = []
while temp_heap:
_, node = heapq.heappop(temp_heap)
if node.free_mem >= task.min_mem:
score = calculate_priority(task, node)
if score > max_score:
max_score = score
best_node = node
temp_nodes.append((-node.free_mem, node))
if best_node:
assignments[task.task_id] = best_node.node_id
best_node.free_mem -= task.min_mem
# 恢复堆
for item in temp_nodes:
heapq.heappush(temp_heap, item)
return assignments
生产验证:从 40% 到 72% 的飞跃
在 8 节点 A100 集群(每节点 8×40GB GPU)上的测试结果:

– GPU 利用率 :峰值从 85%→92%,均值 40%→72%
– 任务完成时间 :P99 从 53 分钟降至 41 分钟
– 冷启动优化 :通过预加载常用镜像,初始化时间从 180s→22s
关键优化点:
- 显存气球技术 :对 TensorFlow/PyTorch 等框架注入显存监控钩子,实时回收碎片
- 分片抢占 :当高优先级任务到来时,只抢占当前 epoch 的 1 / 4 资源而非整个任务
- NUMA 感知 :将数据加载线程绑定到与 GPU 直连的 CPU 核心
避坑指南:血泪经验
调度抖动防护
- 检查点防护 :强制每 30 分钟保存 checkpoint,通过 NFS 共享存储保证一致性
- 心跳超时 :设置 2 倍于调度间隔的心跳超时(默认 20s),避免网络波动误判
多租户公平性
采用两级权重分配:
- 静态权重 :按租户购买的资源配额分配
- 动态权重 :根据历史资源使用率动态调整
实际配额 = 基础配额 × (1 + 0.5×(1 - 最近 1h 使用率 / 承诺使用率))
写在最后
这套调度算法在我们生产环境稳定运行 9 个月后,最意外的收获是:凌晨 3 点的告警短信减少了 80%。原来那些因为资源死锁引发的训练中断问题,现在都被动态调度消化掉了。当然,任何算法都不是银弹——当遇到 AllReduce 通信密集型的超大规模训练时,我们仍然需要结合 MPI 特性做特殊处理。下一步计划将调度粒度从整个 Pod 细化到单个容器级别,让算力像水流一样自然分配到最需要的地方。
正文完
