第 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 所示):

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-1:分布式训练 All-or-Nothing Gang 调度防死锁核心机制

  1. 分布式任务占坑死锁(Scheduling Deadlock / Starvation):
    分布式深度学习训练要求参与通信的 NN 个 Worker 必须全部在线才能完成 NCCL Rendezvous 握手并启动计算。若任务 A 和任务 B 各自需要 8 张 GPU,而集群当前恰好剩余 8 张空闲卡。默认调度器可能将前 4 张卡分配给任务 A 的部分 Pod,后 4 张卡分配给任务 B 的部分 Pod。由于两组任务都无法凑齐全部成员,各自卡在同步屏障等待中,既不进行任何计算,也绝不释放已占有的 GPU 资源,导致集群算力陷入长期的死锁瘫痪(如图 5-1 所示)。

  2. 拓扑盲目散布导致网络性能雪崩(Topology-Unaware Thrashing):
    默认调度器缺乏对数据中心物理网络拓扑层级(机架 ToR 交换机、Spine 汇聚交换机、Core 核心交换机)的感知。若将一个 32 卡紧耦合并行任务随机散落在 4 个不同的物理机架上,跨机集合通信将被迫频繁穿越顶层核心交换机。网络往返延迟从机架内的 <2μs< 2\,\mu\text{s} 飙升至跨机架的 >10μs> 10\,\mu\text{s},并引发严重的光模块拥塞与丢包,导致训练加速比严重劣于单机。

Gang Scheduling(成组调度 / 协同调度)引入了“全有或全无(All-or-Nothing)”的原子调度语义;结合物理拓扑感知放置(Topology-Aware Placement),确保强耦合的多个计算进程在同一时间、以全局最优的网络局部性被原子化绑定与拉起(如图 5-2 所示)。

2. 它如何工作

(1) All-or-Nothing 成组调度与原子保留机制

如图 5-1 所示,Gang 调度器通过引入作业级抽象(如 Volcano PodGroup 或 Kubernetes Kueue Workload)彻底消灭调度死锁:

  1. 最小成员门槛声明(minMember):分布式作业在提交时声明自身启动所需的最小 Pod 数量(如 minMember: 8);

  2. 容量预过滤与整体排队(Pre-filter & Wait):调度器在排队阶段将整个 PodGroup 视为一个不可分割的调度单元。当且仅当集群中当前可用空闲资源能够一次性满足整组 minMember 的需求时,调度器才进入分配流程;否则整个作业保持在就绪队列中整体排队,不占用任何物理卡,将空闲碎片留给其他能够被立即满足的小任务;

  3. 两阶段资源预留与原子提交(Reserve & Atomic Bind):调度器先在内存中为整组 Pod 执行试探性节点预留(Reserve)。只有当整组中所有 Pod 的预选和优选均成功通过后,调度器才并发发起真正的 Bind 操作将 Pod 绑定至宿主机 Kubelet。若其中任何一个 Pod 绑定失败或超时,调度器立即触发回滚并释放整组已预留的资源。

(2) 多级物理网络拓扑感知放置树

现代 AI 数据中心的网络架构通常呈现分层树状(ToR Switch 机顶交换机 →\rightarrow Spine 汇聚交换机 →\rightarrow Core 核心交换机)。如图 5-2 所示,拓扑感知调度算法依据网络开销由低到高逐级匹配:

  1. 第 1 优先级:机架内全封闭放置(In-Rack Placement)
    若单机柜内的空闲节点总卡数能够满足作业需求(如单机架剩余节点可提供 32 卡),优先将所有 Worker 绑定在同一个机架内。所有节点的 RDMA 流量完全通过本地 ToR 交换机线速转发,彻底避免跨机架核心网络拥塞(网络开销最低,性能最优);

  2. 第 2 优先级:同 Spine 汇聚层级放置(Same-Spine Placement)
    若单机架无法容纳,调度器退而求其次,在属于同一个 Spine 汇聚交换机旗下的相邻机架中寻找候选节点,保证数据流仅经过单跳汇聚交换机;

  3. 拓扑容忍度与反亲和断路决策:
    对于极度敏感的并行模式(如跨机张量并行 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],机架总空闲卡数为 4+4=84 + 4 = 8;
  • rack-b 包含一个节点,拥有 8 张空闲卡:node_free = [8],机架总空闲卡数为 88。

两者均能独立满足 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 分;
  • 一票否决:混淆测量与推测、忽略租户/请求隔离、用平均值掩盖尾延迟、删除失败路径。

参考答案(仅 answer 分支)

先独立完成。核对后请改变一个规模或故障条件重新推演。

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)
    return min(candidates) if candidates else None

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 独立调度(Pod-by-Pod Scheduling)会诱发致命的系统级资源死锁(Scheduling Deadlock),深层因果链如下:

  1. 强同步屏障依赖(Rendezvous Barrier Invariant):分布式训练要求所有 NN 个 Worker 必须全部就位并接入通信组才能完成初始化(如通过 PyTorch TCPStore 建立通信环)。若缺少哪怕一个 Worker,所有已启动的 Worker 都会无限期停滞在同步屏障上;

  2. 缺乏原子性的增量占坑竞争:默认调度器以单个 Pod 为粒度并发做贪心放置。假设作业 A 与作业 B 各需 8 张卡,而集群此时仅剩 8 张空闲卡。默认调度器可能将前 4 张卡绑定给作业 A,后 4 张卡绑定给作业 B;

  3. 资源死锁闭环(Deadlock Cycle):作业 A 占有 4 张卡并无限期等待剩余 4 张卡释放,作业 B 同样占有 4 张卡并无限期等待剩余 4 张卡释放。由于微服务调度器缺乏“成组回滚”机制,两个作业均无法启动真实训练,昂贵的 GPU 物理资源被完全冻结锁死。Gang 调度的“All-or-Nothing”机制在准入阶段强制整组原子满足,不满足则一个不绑,彻底打破了死锁的发生条件。

Q2 参考答案

绝非越严格越好。在生产集群架构设计中,拓扑约束的强度必须在网络通信性能与集群可调度性(Schedulability)之间进行精密的工程权衡:

  1. 严苛约束导致排队时延发散与严重饿死(Starvation):若将拓扑约束设置为极硬的“单机柜甚至同 NUMA 独占”,虽然消除了跨交换机延迟,但大幅缩小了调度器的搜索空间。在多租户高并发场景下,每个机柜可能都被长尾小作业零星占用了 1~2 张卡,导致没有任何一个机柜能腾出全量空闲空间。大作业将面临极其漫长的排队等待时间(JCT 总体显著恶化),并造成大量无法分配的碎片算力浪费;

  2. 通信性能边际收益递减:并非所有分布式并行策略都对网络延迟具有同等严苛的要求。张量并行(TP)需要高频微秒级同步,必须强绑定在单机 NVLink 域内;而数据并行(DP/ZeRO)通信通常能够被反向传播流水线有效重叠(Overlap),适度跨机架放置带来的微秒级网络延迟对整体训练吞吐的影响往往不足 2%;

  3. 工程解法:分级软硬约束(Layered Soft/Hard Constraints):最佳实践是依据作业类型设计拓扑感知层级:对 TP 实施严格的同机/同 NVLink 硬约束;对跨节点集合通信(DP/PP)采用“机柜内优先(Preferred)、汇聚层兜底、超时后允许散布”的软拓扑策略,兼顾集群高利用率与训练高吞吐。

Q3 参考答案

当物理节点拥有 8 张 GPU 但其中 1 张被诊断为 ECC 故障(Unhealthy)时,面对 gang size = 8 的强需求,调度系统必须遵循严格的硬件容错过滤与状态一致性保障流程:

  1. 健康资源动态剔除:宿主机上的 NVIDIA Device Plugin 通过 NVML 探测到未纠正 ECC 错误(如 Double-Bit Uncorrectable Error 或硬件退役页超限)后,必须立即将该故障设备置为非健康状态,并通过 gRPC ListAndWatch 通知 Kubelet 将该节点的可用 nvidia.com/gpu 容量从 8 动态降级为 7;

  2. 拒绝盲目分配与防范 CrashLoop 震荡:Gang 调度器在过滤阶段(Filter)计算可用健康资源时,必须识别出该节点仅剩 7 张健康卡,果断判定该节点不满足 8 卡的单节点放置约束,坚决拒绝向该节点绑定 8 卡任务。若调度器脱节并强行将 8 个 Worker 压入该节点,故障卡将导致对应 Pod 在执行 cudaSetDevice() 时直接报错退出,整组任务陷入无限重启崩溃;

  3. 刚性作业等待 vs. 节点隔离自愈:对于要求严格 8 卡对齐的非弹性分布式作业(Rigid Job),调度器必须保持该 Gang 组在队列中处于 Pending 状态,等待其他拥有完整 8 张健康卡的物理机空出;同时,集群运维自愈控制面应自动为该故障节点打上污点(Taint: node.kubernetes.io/unschedulable),触发优雅驱逐与硬件报修维修流水线。

参考资料

API 与平台能力会演进;部署前应按目标版本重新核对。