flowchart TD
subgraph NonGang["传统调度器 (可能引发死锁)"]
JobA_Part["任务 A: 申请到 4 卡 (等待另外 4 卡)"]
JobB_Part["任务 B: 申请到 4 卡 (等待另外 4 卡)"]
Deadlock["资源互相锁死,均无法启动"]
JobA_Part -.-> Deadlock
JobB_Part -.-> Deadlock
end
subgraph Gang["Gang 调度器 (All-or-Nothing)"]
Pool["集群空闲卡池: 4 卡"]
Queue["任务队列: 任务 A 需 8 卡"]
Wait["任务 A 保持整体排队,不占用任何资源,彻底避免死锁"]
Pool & Queue --> Wait
end
第 5 课:Gang Scheduling 与拓扑感知放置
参考答案版:包含完整代码实现与问答题深度参考解析。建议先独立完成练习题后再展开核对。
本课目标与完成标准
本课产出:实现单 rack 内的 all-or-nothing gang 放置,并解释 pending、碎片与训练通信性能。
通过要求:唯一代码填空题通过给定检查;Q1~Q3 都给出因果链;能指出一个正确性边界和一个性能/运营取舍;总分至少 8/10。
与既有六条主线的边界
train/ 假定 GPU 已获得;本课解释调度器如何同时拿到整组资源,并把通信密集 ranks 放在合适拓扑域。
前置:Linux/网络基础、runtime 补充线、train 分布式章节。本课只补 AI Infra 缺口,不重复已经完成的 kernel 或并行算法推导。
核心心智模型
1. 它是什么,解决什么问题
在生产级大规模 AI 算力集群中,传统容器调度器(如 Kubernetes 默认调度器 kube-scheduler)基于无状态微服务假设构建,其核心调度机制是逐个 Pod 独立调度(Pod-by-Pod Scheduling)。
这种调度机制在大模型分布式训练中引发了两个灾难性的系统级瓶颈(如图 5-1 与图 5-2 所示):
图 5-1:分布式训练 All-or-Nothing Gang 调度防死锁核心机制
分布式任务占坑死锁(Scheduling Deadlock / Starvation):
分布式深度学习训练要求参与通信的 个 Worker 必须全部在线才能完成 NCCL Rendezvous 握手并启动计算。若任务 A 和任务 B 各自需要 8 张 GPU,而集群当前恰好剩余 8 张空闲卡。默认调度器可能将前 4 张卡分配给任务 A 的部分 Pod,后 4 张卡分配给任务 B 的部分 Pod。由于两组任务都无法凑齐全部成员,各自卡在同步屏障等待中,既不进行任何计算,也绝不释放已占有的 GPU 资源,导致集群算力陷入长期的死锁瘫痪(如图 5-1 所示)。拓扑盲目散布导致网络性能雪崩(Topology-Unaware Thrashing):
默认调度器缺乏对数据中心物理网络拓扑层级(机架 ToR 交换机、Spine 汇聚交换机、Core 核心交换机)的感知。若将一个 32 卡紧耦合并行任务随机散落在 4 个不同的物理机架上,跨机集合通信将被迫频繁穿越顶层核心交换机。网络往返延迟从机架内的 飙升至跨机架的 ,并引发严重的光模块拥塞与丢包,导致训练加速比严重劣于单机。
Gang Scheduling(成组调度 / 协同调度)引入了“全有或全无(All-or-Nothing)”的原子调度语义;结合物理拓扑感知放置(Topology-Aware Placement),确保强耦合的多个计算进程在同一时间、以全局最优的网络局部性被原子化绑定与拉起(如图 5-2 所示)。
2. 它如何工作
(1) All-or-Nothing 成组调度与原子保留机制
如图 5-1 所示,Gang 调度器通过引入作业级抽象(如 Volcano PodGroup 或 Kubernetes Kueue Workload)彻底消灭调度死锁:
最小成员门槛声明(
minMember):分布式作业在提交时声明自身启动所需的最小 Pod 数量(如minMember: 8);容量预过滤与整体排队(Pre-filter & Wait):调度器在排队阶段将整个 PodGroup 视为一个不可分割的调度单元。当且仅当集群中当前可用空闲资源能够一次性满足整组
minMember的需求时,调度器才进入分配流程;否则整个作业保持在就绪队列中整体排队,不占用任何物理卡,将空闲碎片留给其他能够被立即满足的小任务;两阶段资源预留与原子提交(Reserve & Atomic Bind):调度器先在内存中为整组 Pod 执行试探性节点预留(Reserve)。只有当整组中所有 Pod 的预选和优选均成功通过后,调度器才并发发起真正的
Bind操作将 Pod 绑定至宿主机 Kubelet。若其中任何一个 Pod 绑定失败或超时,调度器立即触发回滚并释放整组已预留的资源。
(2) 多级物理网络拓扑感知放置树
现代 AI 数据中心的网络架构通常呈现分层树状(ToR Switch 机顶交换机 Spine 汇聚交换机 Core 核心交换机)。如图 5-2 所示,拓扑感知调度算法依据网络开销由低到高逐级匹配:
第 1 优先级:机架内全封闭放置(In-Rack Placement)
若单机柜内的空闲节点总卡数能够满足作业需求(如单机架剩余节点可提供 32 卡),优先将所有 Worker 绑定在同一个机架内。所有节点的 RDMA 流量完全通过本地 ToR 交换机线速转发,彻底避免跨机架核心网络拥塞(网络开销最低,性能最优);第 2 优先级:同 Spine 汇聚层级放置(Same-Spine Placement)
若单机架无法容纳,调度器退而求其次,在属于同一个 Spine 汇聚交换机旗下的相邻机架中寻找候选节点,保证数据流仅经过单跳汇聚交换机;拓扑容忍度与反亲和断路决策:
对于极度敏感的并行模式(如跨机张量并行 TP 或流水线并行 PP),调度器执行强硬拓扑反亲和约束(Hard Topology Constraint),若无法满足前两级拓扑则宁可挂起排队,坚决拒绝散布放置;对于通信重叠良好的数据并行(DP/ZeRO),则允许在资源紧张时降级至跨汇聚层散布。
3. 正确性条件与常见误区
误区一:仅按数量计数 GPU,忽略设备底层真实硬件健康状态
调度器若仅凭节点标签上报的nvidia.com/gpu: 8静态容量做决策,而未能与底层 Device Plugin 的 NVML 硬件健康流实时同步,将引发严重事故。若某节点虽然物理上插了 8 张卡,但其中一张卡因严重硬件故障(如 Uncorrectable Double-Bit ECC 错误或供电异常)已被置为 Unhealthy,调度器若仍将其视为 8 卡节点强行调度gang size = 8的任务,Pod 启动后会立即因无法初始化该故障 GPU 而崩溃,导致整组 Pod 陷入无限重启震荡。误区二:拓扑约束越严格越好,导致全集群级活锁与严重饥饿(Starvation)
若所有提交的分布式任务均被强制打上“必须独占单机柜且绝不允许跨机架”的极硬约束,而在一个高并发共享集群中,每个机架都零星运行着若干长尾小作业,将导致没有任何一个机架能腾出完整的全空闲状态。即便整个集群的空闲卡数总和高达几百张,超大分布式任务也将陷入无限期排队饿死状态,集群总体资源利用率暴跌。误区三:调度超时后的残留锁泄露(Leaked Reservations)
在分布式调度的 Permit 阶段,若某个成员由于节点故障未能成功分配导致超时,调度器必须具备严格事务性与幂等清理能力,确保瞬时释放该组其余所有成员占有的预留锁,防止出现“幽灵占坑”资源泄露。
4. 性能与工程取舍
网络局部性极致追求与排队时延的博弈:
严格的机架级或 NUMA 级局部性带来了极佳的通信吞吐与近乎零抖动的 Step Time,但大幅压缩了调度器的解空间,导致任务平均排队等待时间(Queue Pending Time)成倍延长。工程实践应采取分层软硬约束(Soft vs. Hard Affinities):对通信敏感度极高的小步长并行施加硬机架亲和,对通信具有异步流水线掩盖的大模型数据并行则设置软亲和(优先紧凑,超时后允许降级散布)。主动抢占(Preemption)与资源碎片整理机制:
为了在不产生长期排队的前提下为大 Gang 作业腾出连续的机柜级算力空间,平台必须设计优雅的主动抢占机制:依据任务优先级(PriorityClass),允许高优先级大训练作业临时挂起或驱逐机架内零散分布的低优先级非通信敏感任务(如离线数据处理或单卡调试作业),完成机架算力的“碎片整理”,随后被驱逐的任务自动移至集群边缘碎片节点继续运行。
具体演示
设集群包含两个机架(rack-a 与 rack-b),新作业申请 required_gpus = 8:
rack-a包含两个节点,分别有 4 张空闲卡:node_free = [4, 4],机架总空闲卡数为 ;rack-b包含一个节点,拥有 8 张空闲卡:node_free = [8],机架总空闲卡数为 。
两者均能独立满足 8 卡的机柜内全量放置条件(避免跨机架通信)。在存在多个候选机架时,算法为了保证调度的确定性与可复现性,按机架名称字典序选定 min(candidates)。因此算法确定性选择 "rack-a" 进行整体绑定。若所有单个机架的总空闲卡数均不足 8,则返回 None 拒绝放置,让任务整体保持排队等待,杜绝半截占坑。
请先独立复述“输入 → 状态变化 → 输出/指标”,再开始填空。
实践任务:唯一代码填空题
补齐最小 rack 名称的 gang 放置;不能跨 rack 拼接。
只能修改 TODO/______ 位置,不得删除断言或放宽通过条件。
Code
def place_gang(free_gpus_by_rack, required_gpus):
if required_gpus <= 0:
raise ValueError("gang size must be positive")
candidates = []
for rack, node_free in free_gpus_by_rack.items():
if sum(node_free) >= required_gpus:
candidates.append(rack)
# TODO:无候选返回 None,否则确定性选择字典序最小 rack。
return ______
assert place_gang({"rack-b": [8], "rack-a": [4, 4]}, 8) == "rack-a"
assert place_gang({"rack-a": [4], "rack-b": [3, 1]}, 8) is None检查方法
运行本单元格,所有断言必须通过;再补一个边界输入并解释预期。
Q1
为什么普通逐 Pod 调度会让分布式训练死锁式占坑?
你的答案:
Q2
拓扑约束越严格越好吗?
你的答案:
Q3
节点有 8 GPU 但其中一张 ECC unhealthy,gang size=8 应如何处理?
你的答案:
评分规则
- 代码 4 分:正常输入 2 分、边界输入 1 分、解释 1 分;
- Q1~Q3 各 2 分;
- 一票否决:混淆测量与推测、忽略租户/请求隔离、用平均值掩盖尾延迟、删除失败路径。