
一万张 GPU 的排班问题远比排队打饭复杂得多有算法同学问我我提交的训练任务在集群里等了二十分钟到底在等什么为什么明明有 GPU 空闲我的任务还是卡在 PENDING这种问题几乎每天都会出现。训练框架PyTorch、DeepSpeed 这些管的是代码怎么跑而调度器管的是你的代码到底什么时候能跑、在哪些 GPU 上跑。当一个集群规模走到万卡级别调度器就是这个训练场里真正发号施令的人它决定谁先用显卡、谁往后靠、谁的任务要被迫让位。它不产生训练收益但它决定整个集群的产能上限。这一篇我把自己的经验梳理成几个核心维度调度器到底在解决什么、核心机制有哪些、主流方案之间的差异性以及实际运营上万卡集群时的那些坑。内容偏实践向适合正在搭集群、或者被集群排队折磨的人读。1. 调度器解决的问题不是一个字排那么简单先说一个反直觉的结论调度器不是在分配 GPU而是在做资源的时间切片和空间排列。一万张 GPU 听起来很多问题是一万张卡不是一整块它们分布在几百台机器上每台机器有 8 张卡机器之间又有不同的网络带宽。你的任务需要几卡、需要哪些卡在一起、需要多大显存、需要跑多久调度器都要在毫秒级做出判断。1.1 一个训练任务提交之后后台到底发生了什么拿一个典型的 PyTorch DDP 任务举例。你执行python -m torch.distributed.run --nproc_per_node8 train.py训练框架会申请 8 张 GPU。如果是在单机上跑操作系统帮你搞定一切但在集群上你面对的是一堆节点调度器的第一个动作是找合适的节点。这个合适有三个维度数量匹配你要 8 张卡一台机器正好有 8 张空闲这是最好的情况。显存匹配你的模型需要 40GB 显存机器上有的是 32GB 的卡给你也没用必须按需匹配。网络匹配你要做分布式训练8 张卡如果横跨两台机器就需要走高速网络比如 RoCE 或者 InfiniBand。如果集群里高速网络端口不够调度器还要考虑尽量不跨机。我刚接触集群调度时以为调度器就是看哪个节点空闲就把任务扔过去实际远没有这么简单。调度器要把整个集群抽象成一个资源池每个节点定期上报自己的状态总显存、已用显存、空闲卡数、网络带宽、GPU 型号。调度器收到任务请求后在这个资源池里做匹配。1.2 万卡集群的调度本质是两个人的协作一个训练集群的正常运转需要两个人配合任务方训练框架负责把训练任务拆碎DDP 把数据并行到多张卡每个进程处理一份 batch然后互相同步梯度。任务方知道自己需要多少资源。资源方调度器负责在多个任务之间做取舍你手头有三个任务一个要 512 卡跑大模型一个要 16 卡调参还有一个要 64 卡做数据并行微调。全部同时跑资源不够。让跑 16 卡的先上大模型的 512 卡任务进度立刻被打断谁更亏这两方的目标函数还不一样训练框架永远想尽快拿到更多资源而调度器要兼顾所有任务的公平性和整个集群的吞吐量。这个矛盾就是我在标题里说的隐形的老板——它总在替你做你不想面对的取舍决策。在这个协作模型里调度器更像是一个操作系统进程调度器的放大版本进程调度管的是 CPU 时间片集群调度管的是 GPU 资源分片。但放大到网络层面后复杂度指数级上升。CPU 的进程切换是微秒级网络传输和分布式训练的资源调度要做到秒级响应而且一次判断错误可能导致整个训练任务失败损失以小时计。2. 调度器的三个核心能力队列、优先级和生命周期很多想自研调度器的人第一个版本只做了资源匹配有空闲就分配没空闲就排队。跑起来才发现队列管理、优先级、生命周期管理才是真正打磨的地方。2.1 排队不是谁先来谁先跑而是动态博弈当你提交一个训练任务它进入调度队列。队列不是简单的 FIFO先来先服务否则一个跑 72 小时的大任务会把所有人堵死。大多数训练集群采用多级队列的设计短任务队列跑实验、跑 eval、数据处理任务时间预计在 40 分钟以内。调度器会尽量优先安排因为这些任务通常人等机器等太久实验人员就阻塞了。训练任务队列正式训练动辄数天到数周。调度器会保证它有足够的资源连续性。服务队列推理服务或在线任务对延迟敏感一旦调度就不能随意抢占因为在线服务的 SLA 不允许训练任务干扰。真实调度器里队列之间还有权重。比如短任务队列权重高但每个短任务最多只能拿 N 张卡训练任务优先级低但是独占性好。队列设计的目的很明确不同任务吃的是不同的资源特征用一套策略管所有任务最终就是既让大任务吃不饱也让小任务饿死。我之前遇到过一个典型槽点把离线训练和在线推理放在同一个队列结果某次训练任务做大规模 checkpoint磁盘 IO 打满在线推理的延迟从 10ms 飙到 2 秒。后来强制分离队列各自限流问题才解决。队列不只是排队还是隔离。2.2 优先级和配额谁也不能无限抢占多级队列解决的是任务分类但同一个队列里谁先跑还需要优先级。优先级系统通常分两层用户/项目配额每个部门、每个项目组在集群里有一个资源配额上限。比如 A 组配额 2000 卡B 组配额 5000 卡。调度器保证配额的存在防止某个组把资源全部占光。任务优先级同配额内任务有高、中、低三级。高优先级任务可以挤占低优先级任务。但这里有个非常微妙的点为什么不能只用配额把任务隔离开还要做抢占因为配额是静态的任务却是动态的。A 组的 2000 卡配额 60% 处于空闲状态B 组的 5000 卡已经排了很长的队。这时候如果严格按配额A 组空闲卡就浪费了。所以调度器要允许一种借资源的机制B 组可以临时使用 A 组的空闲配额但一旦 A 组有高优任务提交B 组的任务需要能被抢占preempt释放资源。抢占机制是调度器里最危险的部分。一个训练任务被抢占不只是暂停等一下而是直接杀掉进程中间的显存状态全部丢失模型权重回到上一个 checkpoint。如果 checkpoint 频率是一个小时被抢占就意味着浪费一个小时。所以调度器要做的不是单纯抢占而是根据 checkpoint 的进度来决定现在可以杀谁。这个机制做得好不好直接决定了用户对集群的信任程度。频繁被抢会导致用户抗拒使用集群宁可自建小规模的资源孤岛。所以大规模调度器里的抢占一般都配置了最小保护时间——一个任务跑起来之后至少给两小时稳定运行时间不让它刚启动就被抢占。2.3 生命周期管理从提交到回收的完整链路调度器从头到尾管着一个任务的完整生命周期提交用户提交任务定义包括镜像、启动命令、资源需求、运行时间上限。排队等待调度器分配资源。运行中任务绑定 GPU 之后调度器持续监控健康状态。结束/失败正常结束则回收资源失败则判断是否需要重启同时保留日志。这里有个容易忽略的环节是冷启动时间。集群里有几千个任务同时排队调度器要负责把镜像从镜像仓库拉取到计算节点。如果一个镜像有 10GB几百个任务同时启动就可能把镜像仓库的带宽打爆。为了优化这个环节很多集群会做镜像预拉取——节点空闲时就把常用镜像缓存好任务提交后直接复用冷启动时间从十几分钟压缩到几十秒。这个功能通常不是调度器核心但实际使用体验差异巨大。3. 调度器内部的关键算法从资源匹配到 Gang 调度调度器的核心说白了是一套资源规划算法。我按照实战中遇到的复杂度从低到高来说。3.1 最基本的资源匹配Binpack 还是 Spread一个节点上有多张 GPU调度器分配时会面临两个策略选择Binpack装箱尽量把任务集中到少量节点上让其他节点空出来。好处是省电空节点可以关机而且大任务来了更容易找到整块的空闲区域。Spread分散把任务尽量分散开。好处是单个节点的故障影响面小坏处是节点碎片化比如 8 台机器每台只剩下 1 张卡但一个任务要 2 张卡就因为不连续而无法调度。真实场景里两种策略都要用小任务用 Binpack 把它们堆到一起大任务用 Spread 避免互相争抢网络带宽。调度器需要动态评估两种策略的利弊这就是编排的核心含义。3.2 拓扑感知不是所有 GPU 都是平等的这是新手做调度器最容易忽略的维度。一个 8 卡节点内部GPU 之间的连接方式是 NVLink 和 PCIe 的混合拓扑两个节点之间走的是网卡和交换机。对训练任务来说通信开销直接决定扩展效率同一个节点内 8 卡使用 NVLink 通信带宽可以达到 600GB/s。跨节点通信走 RoCE比如 200Gbps 网络实际有效带宽远低于 NVLink。所以调度器分卡时要考虑通信拓扑亲和性。同样是分配 8 张卡从同一台机器上分 8 张和从两台机器上各分 4 张性能差异可能超过 30%。一个合格的调度系统需要维护整个集群的拓扑图哪个 GPU 在哪个节点节点之间有几跳网络哪些节点共享同一个 leaf 交换机。调度的时候不仅算资源够不够还要算通信成本。具体到实现调度器会维护一张分层视图集群 → 机架 → 节点 → GPU。分配任务时优先在最小层级内完成。NCCL 的NCCL_P2P_LEVEL和NCCL_TOPO_DUMP_FILE就是用来查看和调整通信路径的但真正好用的方式是调度器直接把任务安排在拓扑内聚的节点上从源头减少跨机通信。3.3 Gang 调度要么全部给我要么我全都不要这是训练集群调度区别于普通服务的核心特性。普通微服务扩缩容是有多少资源就启动多少实例训练任务不行——一个数据并行任务需要 N 个进程同时跑任何进程缺失都导致训练卡住。这种要么全有要么全无的分配方式叫Gang Scheduling。一个 128 卡的任务必须等 128 张卡全部就绪才能启动。如果只凑到 127 张任务就一直在等待那 127 张卡也处于被预订但未使用的状态造成资源浪费。这个问题在大型训练任务上尤其突出。我记得有一次集群资源碎片化严重每台机器剩下 3-4 张卡而排在队首的是一个需要 128 卡的大任务。小任务进不来因为大任务把资源都预订了大任务凑不齐因为资源太碎。这就是经典的头阻塞问题。业界主流的解法有两个Backfill 机制大任务等待时允许小任务临时使用它预订的资源但要保证在大任务资源凑齐前小任务能结束或者可以被抢占。时间片切分调度器规定一个等待周期大任务在周期内无法凑齐资源则让位给小任务避免无限期占位。Volcano 调度器里对这两个都有支持job.minAvailable配合job.queue来标记 Gang 调度。但真正的难点是backfill 进来小任务是否会影响大任务启动时间这个判断逻辑需要调度器对任务运行时长做精确预估。3.4 抢占机制的实现细节优雅终止比强杀重要当一个高优先级任务进入队列调度器要抢资源时什么时候触发、选谁下手、怎么下发指令每个环节都有技术讲究触发条件高优任务排队超过阈值比如 5 分钟调度器才会触发抢占。选择被抢占对象优先选择 checkpoint 频率最高的任务损失最小然后再看优先级最低的那个。下发方式调度器不是直接 kill 进程而是发送 SIGTERM 信号让训练框架有机会做优雅退出——保存当前状态、释放显存、上传日志。如果超过宽限期通常 60 秒还没退出再发 SIGKILL。这里我最想强调的是一定要留好优雅退出的逃生门。很多用户训练脚本没有注册 SIGTERM 信号处理器被抢占就等于直接丢进度这个体验非常糟糕。调度器至少要保证被杀的任务有足够时间把日志和 checkpoint 保住。4. 主流技术选型Slurm、Ray、Volcano 和自研平台的取舍调度系统领域没有万能的银弹。不同技术栈有各自的适用边界我在实际项目中踩了不少坑这里把几套主流的方案拉一起复盘。4.1 Slurm老牌 HPC 调度器可靠但设计偏批处理Slurm 是学术圈和高性能计算领域使用最广的调度器。它把任务当作批处理作业来管理稳定性极高一套 Slurm 集群跑几年不出大事是常见的。优势成熟稳定大规模部署案例多生态文档齐全。原生的 Gang 调度scontrol show job里能看到 job 的节点分配状态。对网络拓扑敏感度不高HPC 领域本来就是强关联的专用网络。局限对 GPU 任务的资源抽象相对粗糙主要靠--gpus参数和 GRES 插件来管理显存维度管理能力弱。对弹性伸缩支持差训练任务运行过程中不能动态加减卡。对长时间运行的分布式训练任务支持一般尤其是涉及动态组网的任务比如 PyTorch elastic 模式。如果团队主要做传统 HPC 或者简单的小型训练Slurm 可以快速上线。但大型 AI 训练集群用它调度策略上会有不少挑战。4.2 Ray训练/推理一体化的弹性调度器Ray 从开发之初就是为分布式 Python尤其是机器学习负载设计的不是一个通用的集群资源管理器但它天然适合训练和推理混合的场景。Ray 的调度强调对象存储 任务图的运行模型每个任务把数据输入输出定义为分布式对象。优势对 Python 生态非常友好和 PyTorch 深度集成跑 DDP 任务的时候 Ray 可以直接拉起每个 rank 的进程。弹性扩缩容能力极强ray autoscaler可以按需申请和释放云上节点。任务优先级队列内置于ray.remote的num_cpus/num_gpus调度器中开发者不需要额外学习一套作业描述语言。局限调度策略相对简单内置于 GCS统一调度超大规模上万节点的调度性能可能成为瓶颈。高可用方案部署复杂Ray 自带 GCSGlobal Control Store如果挂了整个集群调度就停了需要额外做容灾。Ray 更适合做AI 应用开发的统一运行时但如果你要把一个庞大的内部集群管好通常还是要套一层自己的资源管控组件。4.3 Volcano Kubernetes云原生时代的主流组合Kubernetes 原生调度器不擅长批处理和 Gang 调度于是出现了 Volcano 这类扩展调度器。Volcano 是 CNCF 项目专门为 AI/大数据工作负载服务在调度框架层面实现了 Queue、PriorityClass、Gang scheduling。优势和 Kubernetes 生态无缝集成镜像管理、存储编排、网络策略全部复用 K8s 能力。调度策略可做插件化定制比如自定义插件实现 binpack/拓扑感知。资源可视化管理比 Slurm 好K8s Dashboard 和 Metrics Server 直接展示资源使用情况。局限K8s 本身是面向长跑服务的系统批处理任务在 K8s 里的生命周期管理逻辑如 job 失败重试、超时清理不如 Slurm 直观。大规模集群里 K8s 的 API Server 和 etcd 会成为短板K8s 万节点集群需要非常多调优。调度性能和抢占策略的复杂度在极大规模下同样是个坑。我推荐的中型团队方案是 K8s Volcano业务能拿到云原生的整体收益同时 Volcano 的调度语义对 AI 场景的适配度足够高。但如果已经有一套 Slurm 在跑就没有必要强行迁移。4.4 自研调度平台的常见误区见过不少中大型公司走上自研调度器的路结果价值没做出来反而陷入维护泥潭。总结下来常见的误区有三个过度强调抢占和优先级忽视了链路稳定性。一个调度器最重要的能力是无论如何都不把集群搞死。调度器变成数据库。系统里存了太多的任务元数据、用户自定义字段和审计日志性能被拖垮真正调度决策时反而变得迟钝。缺少故障演练。调度器挂了怎么恢复有没有备份调度器很多系统只有一台调度节点挂了全都趴窝。如果你要自研我强烈建议在第一版就考虑好调度器本身无状态化、任务状态的持久化放外部存储、调度决策逻辑必须可以随时重建。也就是调度器本身可以挂但集群状态不会因为调度器挂掉而丢失。5. 运营经验分享调度器上线之后的真实挑战前面讲的是原理和选型最后这一部分我想讲讲调度器上线之后真正考验人的地方。调度器的开发工作可能只占 30%剩下 70% 都是日常运营中的问题定位、策略调优和用户沟通。5.1 从调度成功到训练不慢之间还有很长的路很多团队误以为调度成功任务开始跑万事大吉。实际上调度器的指标不能只看分配成功率最关键的指标是训练实际吞吐量。我之前排查过一个案例任务成功调度到 64 张 GPU 上但训练速度只有预期的 40%。排除了代码、存储、网络问题之后最后发现是调度时把任务的 8 个 rank 分散到了 4 个不同机架上训练通信的跨机流量远超预期。虽然调度器保证了资源够但完全忽视了通信路径最短这个要求。所以调度器设置完策略以后要持续收集训练任务的性能指标每次迭代耗时、通信耗时占比、吞吐量。一旦指标异常就要把调度方案和训练性能关联起来分析。这套可观测性体系的建设和调度器本身同样重要。5.2 碎片化问题的真实处理过程碎片化是每个训练集群的头号问题。我处理过一次相当严重的碎片危机集群合计空闲约 1000 张 GPU但每天排队超过 4 小时的用户很多因为他们要的都是 64 卡、128 卡的整块资源。排查发现碎片主要来源于三类小任务长时间占卡一些 4 卡的调参任务一跑就是 3 天期间机器剩余 4 卡被占死128 卡的训练任务永远凑不齐。节点故障未完全隔离节点上报只有 6 卡可用另外 2 张卡因为异常被屏蔽但这些残网没有自动归并导致整个节点的剩余容量利用率极低。模型新版本显存需求变大旧任务要求 40GB 显存新任务要求 60GB 显存集群里 40GB 和 80GB 的卡混布资源碎片直接翻倍。针对这些问题我做了两组策略调整一是对卡时长短的小任务启用抢占后可迁移策略被抢占之后自动把状态迁移到另外的空闲节点续跑二是对节点做容量归一化——通过池化技术把同型号 GPU 划到一个逻辑池调度的时候尽量让任务在池内完成。效果是单集群的 GPU 利用率从 46% 提升到 68%。这个提升在万卡规模下等于是凭空多出来两千多张卡。5.3 调度器挂了怎么办故障演练的必要性调度器自身的容灾设计在开发初期很少有人认真对待。我第一次做集群故障演练的时候直接手动 kill 掉了调度器主进程结果整个集群长达 15 分钟无法提交任何任务排队中的任务全部超时。新提交的任务因为没有调度器响应直接卡在 PENDING。这个问题解决起来并不难关键是提前做好设计调度器要能快速重启并从持久化存储恢复队列状态。已经运行中的任务不受调度器重启影响避免任务被误杀。调度器重启期间新任务可以先落到持久化队列等调度器恢复后继续调度。多副本部署或者主备切换真正具备自动 failover 能力。这套设计做完再演练故障恢复时间可以压到 1 分钟以内用户几乎没有感知。不过这里想提醒一下一定不要只在测试环境演练生产环境的负载特征、任务分布和测试环境差距很大建议最初几次演练选在线业务间歇期做。5.4 训练框架和调度器的配合细节好的调度器不是分配完资源就消失而是要能和训练框架产生化学反应。目前实测下来PyTorch 生态和调度器的配合要注意几个细节DDP 的初始化机制DDP 用torch.distributed.init_process_group通过环境变量MASTER_ADDR和MASTER_PORT做通信初始化调度器要保证每个 rank 都能正确拿到这些信息。torch.cuda.set_device调度器给任务分了物理 GPU 序号但 PyTorch 默认从 0 号开始用。如果任务跑在 GPU 4-7 上而脚本没有os.environ[CUDA_VISIBLE_DEVICES]做映射就会出现显存不够的报错。调度器要为每个任务注入正确的CUDA_VISIBLE_DEVICES环境变量。checkpoint 与抢占的配合更好的做法是在框架侧每隔固定步数把当前模型权重、优化器状态写到共享存储。调度器触发抢占时优先选中距离上次 checkpoint 时间较短的任务把它的损失控制到最小。这里可以沿着Pytorch 安装教程 GPU、DDP 分布式训练这些方向进一步展开核心思想就是调度器的资源抽象能力要能平滑对接到框架的资源消费方式否则调度器分配得再漂亮训练代码跑不起来也是白搭。5.5 资源的超额分配还是实时测度最后再补充一个新思路资源调度里一直存在静态分配和动态利用率的拉扯。很多训练任务申请了全部显存但实际运行中 GPU 计算利用率只有 30%比如数据加载跟不上。此时调度器如果严格按已申请显存来判断节点是否饱和就会造成大量资源整体闲置。后来我引入了 GPU 利用率的实时监控指标对于利用率低的节点允许调度器超额分配一部分任务等 GPU 真正吃紧时再触发抢占换出。这套基于实际使用率而非申请量的调度策略在混合负载训练推理数据处理场景下收益非常明显但风险也成倍增加——一旦某个任务突然吃饱其他任务可能因为抢不到 GPU 而停滞。所以针对超额分配任务我会额外设置一个兜底策略所有超额任务都可以被无条件抢占且被抢占时自动把进度保存好重新排队续跑。6. 调度器给不同角色的参考建议聊了这么多机制和坑落到实际应用层面不同角色面对的调度器问题是不同的。我最后按角色给一些偏实操的建议。6.1 如果你是算法工程师别再把调度器当成一个黑盒来抱怨。你至少要把这几个概念搞清楚你的任务被分配到了哪台机器、是哪个队列在服务你、你的任务有没有设置合理的运行时长上限、你是否注册了优雅退出信号。这些了解清楚之后你的任务排队时间和被抢占概率都会有明显下降。特别建议在训练脚本里加上 SIGTERM 的捕获处理逻辑哪怕只是保存一个简单的训练状态标记也比裸奔强得多。你永远不知道集群运维哪天调了抢占策略。6.2 如果你是平台/运维工程师建设调度器之前最先要建设的是监控和可观测性。具体来说至少要覆盖这些维度监控维度关键指标作用集群资源GPU 总数/已分配/空闲、显存使用率、网络带宽一眼掌握集群状况队列状态每个队列的排队任务数、排队时长、权重判断调度策略是否合理任务状态任务启动延迟、运行时长、结束原因占比正常/失败/被抢占定位任务卡住的根因资源碎片单个节点空闲卡数分布、可整块分配的节点数评估碎片化程度指导优先级/抢占策略调度器自身调度决策耗时、API 丢出率、故障恢复时间避免调度器本身成为瓶颈有了这套指标你要做任何策略调整才有依据而不是我觉得小任务优先级该提高这种拍脑袋决策。6.3 如果你在考虑训练和推理的混合调度目前很多平台把训练集群和推理集群完全分开这是非常简单粗暴的做法。推理集群在凌晨到上午通常会有大量空闲 GPU而训练任务恰恰在那个时间段往往有排队需求。混合调度的前提是推理平台有完善的弹性伸缩组件、训练任务能容忍被抢占后重排。如果这两个前提满足混合调度带来的利用率提升相当可观。我的建议是初期采用隔离优先、少量混部的策略让一小批对延迟不敏感的推理任务和训练任务混跑跑通之后再逐步扩大。6.4 关于调度器是隐形老板这个说法的个人体会回到开头那个比喻。我在这个领域做了几年之后越来越觉得调度器在集群里的角色确实像一个隐形老板你不用直接向它汇报工作但你的任务什么时候能开始、能跑多久、能拿到多少资源都是它说了算。和老板相处的正确方式不是抱怨它不公平而是理解它的决策逻辑优化自己的行为来获得更好的资源配置。对调度器开发者来说要明白自己做的不是一个资源分配组件而是一个在冲突中做取舍的系统。一万张 GPU 的排班问题终极目标不是让每个任务都立刻运行——这在数学上就不可能。调度器的目标是让整个集群的产出最大化同时尽量保证公平性。理解了这个目标很多设计决策都会有明确的方向。下次你的任务又在队列里等了二十分钟不妨想想那个隐形老板正在权衡的可能是好几个团队、几十个训练任务、几千张卡之间的复杂博弈。你要是能顺着它的逻辑优化自己的任务提交方式也许就能成为拿到最多资源的那个人。