ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

万卡GPU调度实战:从排队机制到集群利用率优化

2026/9/11 4:57:42 拓冰建站 浏览量
万卡GPU调度实战:从排队机制到集群利用率优化 上个月帮一个朋友排查集群问题打开调度面板一看八百多张A100里利用率超过50%的不到一半。群里立刻有人问是不是显卡坏了查了一圈不是卡的问题是任务排队和时间片分配的问题。说白了真正决定这八百张卡能不能转起来的人不是运维不是训练工程师而是那个平时几乎没人注意的调度器。这也是我在AI Infra系列“训练与调度”这一章里最想讲清楚的事当集群规模走到一万张GPU调度器才是训练场的隐形老板。一万张卡听起来很多但放到一个大模型训练周期里它只是一个勉强够用的“工位池”。谁先上工、谁换卡、谁被请出去全是调度器说了算。这篇文章我就围绕“一万张GPU怎么排班”这件事把调度器背后的设计思路、关键机制、实操链路和我踩过的坑一次性说完。1. 调度到底在调度什么一份万卡级的“排班表”背后1.1 谁的卡——GPU不是任何人的私人财产很多第一次进大集群训练团队的人都会有一个错觉我提交了一个任务申请了八张卡这八张卡就好像“属于”我这个任务了。实际上在大规模集群里GPU不属于任何任务、任何部门它是公共工位。你的工作负载只是“被安排到”这些卡上运行一段时间跑完了就要腾出来。这个逻辑和公司工位非常像。一万张卡就是一万个工位训练任务就是一批批来上班的项目组。有的项目组要连续加班一个月大模型预训练有的项目组只过来开两小时会小规模调参有的项目组嘴上说用三天结果跑了两周还不出结果。调度器就是那个管工位的行政主管它要知道每个工位的状态哪些是空的哪些被占了哪些虽然被占了但底下那个人其实摸鱼摸了一整天利用率极低然后决定要不要把工位收回来。没有调度器或者调度器很弱会出现什么局面我在实际集群里见过最典型的场景一个团队提交了一个64卡任务因为调度策略太简单任务卡在一个节点上等资源等了四个小时没起来另一边另一个团队的调试任务霸占着32张卡每张卡利用率不到10%但任务一直不死。这种“饿的饿死、饱的撑死”的状态归根结底是调度器没有把资源当作公共资源来精细管理。1.2 排班表的三个维度时间、卡位、资源调度器看起来是在“分配GPU”但实际要管的排班表远比“哪张卡给谁”复杂。我做了一张思维上的优先级清单一个合格的调度器至少要同时处理下面三个维度时间维度任务什么时候开始跑、能跑多久、超时了怎么处理。一万张卡的集群里时间维度直接决定了排队体验。总有人问“为什么我的任务排在前面却一直不启动”多半是时间维度上的调度策略太粗糙。空间维度任务具体落在哪些节点、哪些卡上。卡的位置直接关系到分布式训练的网络拓扑。8卡任务放在同一台8卡机器上和放在两台4卡机器上通信开销完全不是一个量级。调度器必须知道哪张卡和哪张卡之间是NVLink直连哪些卡跨了交换机。资源维度除了算力本身显存、显存带宽、CPU内存、网络带宽都要匹配。大模型训练对显存的渴求是硬约束一张A100 80GB有的任务要60GB有的任务要75GB差了15GB就是装不下。调度器心里得有账不能只听说“要8张卡”就乱发卡。这三个维度不是独立存在的它们会互相牵扯。比如一个任务要64张卡但要求所有卡都在同一个物理集群内、网络延迟低于某个阈值那么调度器在时间上就要多等一段时间直到能凑出满足空间要求的、连续空闲的资源段。排班表排得好不好本质上是这三个目标之间的平衡。1.3 训练任务和推理任务的调度逻辑不一样在做调度方案的初期很多人都踩过一个认知坑把训练调度和推理调度混在一起设计。其实这两者差别非常大我从排班角度给你对比一下维度训练任务推理任务运行时长小时到周毫秒到分钟资源占用启动后基本固定随请求量波动失败惩罚高可能损失几天算力高直接影响线上服务调度目标高吞吐、稳定不被打断低延迟、快速扩缩容是否需要调度器介入启动时和失败重启时每次请求都可能被路由推理场景的核心是“服务千万别断”所以依靠负载均衡器和弹性伸缩来动态度数训练场景的核心是“跑起来之后别被打断”所以调度器更像是给长途货运排航线一旦出发尽量不要中途换车。这篇聊的“一万张GPU怎么排班”对象就是训练场景的调度别把推理那套思路直接搬过来。2. 别搞混任务编排和资源调度是两层事2.1 海豚调度器、XXL-Job管的是“什么时候跑”不是“用什么跑”热门搜索里经常能看到“海豚调度器”“XXL-Job定时调度任务”这类关键词很多人误以为这些就是GPU调度器。这里必须先把概念掰开像海豚调度器DolphinScheduler、XXL-Job这类工具管的是工作流编排和定时触发解决的是“今天凌晨两点该跑数据清洗任务了”“训练完了接着跑评估脚本”这种流程问题。它们的大脑在“业务逻辑层”不直接接触物理GPU。哪怕海豚调度器触发了一个训练任务最终还是要通过Kubernetes或者Slurm这一类真正的资源调度器去申请GPU。一张A100 80GB的卡到底分配给谁这类工作流引擎根本不管也管不了。我见过一个小团队踩的坑正好相反他们以为用了Kubernetes就万事大吉把训练任务提交进去后发现任务编排一团乱一会儿这个Pod先跑一会儿那个又要等前置任务结果折腾半天发现缺的是一个海豚调度器这种上层工作流工具。正确的姿势是两层配合上层工作流引擎管“流程编排”有向无环图、依赖关系、定时触发、失败重跑下层资源调度器管“资源分配”哪个Pod占哪块GPU、排队策略、抢占策略。两层各有各的老板不要混为一谈。2.2 现在主流的调度器选型Kueue、Volcano、Slurm万卡集群怎么选调度器目前业界主流基本逃不开下面几类Kubernetes原生体系如果集群已经基于K8s管理最贴近的是Kueue、Volcano这类调度组件。Kueue偏向“排队与配额管理”Volcano则内置了Gang调度、抢占、公平调度等更接近HPC高性能计算的能力。Slurm家族这是老牌超算和科研集群的首选。很多传统AI团队从超算时代带过来的经验对Slurm非常熟悉它处理排队、分区、节点分配的能力极其成熟。但Slurm和云原生生态的整合比较弱容器支持是后补的。内部自研调度器做到万卡以上规模很多大厂都会在K8s之上做二次开发或者直接自研调度器。自研的原因通常很现实开源调度器在大规模吞吐、特殊拓扑感知、多租户配额上不够灵活需要自己定制。从我个人经验看选型没有绝对好坏关键看你团队的现状。如果团队已经深度用K8s从Volcano开始是比较稳妥的如果主要是跑传统科学计算加少量训练Slurm更顺手如果有专门的调度团队自研也可能是长期最优解。最怕的不是选错而是选好了不去调优拿默认参数扛万卡流量那一定出事。2.3 为什么一万张卡不能只用“先来先服务”很多没接触过大集群的人会问一万张卡排队排不过来搞个先来先服务FCFS不就行了谁先申请谁先用大家公平。听起来没错但实际一跑就出问题。先来先服务最大的问题是不考虑任务大小的“头重脚轻”。想象一个场景一个任务要占8000张卡跑一周它排在队列最前面后面来了几百个只需要4张卡的小任务它们全部被挡在后面。结果就是8000张卡的“巨无霸”任务在等最后500张卡凑齐而剩下的9500张卡全被堵死。整个集群的利用率瞬间崩盘。这就是为什么现代调度器普遍引入优先级队列、配额机制和抢占策略。调度器会把集群分成多个队列离线训练队、调试队、临时任务队。每个队列有自己的配额上限防止一个团队把整个集群吃干抹净。大任务和小任务可以并行跑在集群的不同区域而不是全局排一条长龙。这背后的管理思想很简单一万张卡的排班不是“谁来早就谁干”而是“谁最需要、谁最合适、谁先干完能释放更多产能”。3. 一套懂排班的调度器必备的四个核心机制3.1 集体调度凑不齐人不开席防死锁分布式训练有个特点任务必须“成团”启动。一个任务申请64张卡你得等64张卡全部到位同时启动训练框架才能初始化成功。如果调度器只给了一半卡先启动另一半卡还在排队这个任务会卡在初始化阶段白白占着前32张卡干等而后面32张卡永远等不到因为前面那32张卡看起来“还被占用”。这就是分布式训练里最经典的死锁场景。解决方案就是Gang调度集体调度业内也叫“全有或全无”凑不齐足够的资源任务一个卡都不给凑齐了一次性全部分配同时启动。这个机制极其重要尤其是大模型训练没有Gang调度的集群跑起DP数据并行、TP张量并行、PP流水线并行混合并行的任务时经常会出现任务悬死。我打个比方给你听集体调度就像包间吃饭一桌十个人必须凑齐了才开席。如果你凑齐六个就让人先进去坐着服务员以为这桌已经开吃了剩下的四个人在外面根本排不上号结果就是一桌人坐在里面干瞪眼外面的人永远进不来。公司里的行政订餐不会犯这种错调度器也一样不能犯。3.2 队列、配额与优先级一定有人要插队怎么办现实世界里永远有插队需求训练集群也是一样。一个刚启动的核心项目老板盯着看效果你说让它排三天队不可能。所以调度器必须支持优先级和抢占。但是“插队”要讲究策略。调度器里通常有三种控制手段队列Queue把任务按业务线、项目组划分开每个队列独立排队互相不影响。比如A团队的任务不会排到B团队的队列后面去。配额Quota限制每个队列最多能同时占多少资源。A团队配额是3000张卡哪怕集群空闲5000张A团队最多也只能用3000张防止一个团队冒进导致其他团队长期饿死。优先级Priority队列内的任务再按优先级排序核心项目可以设高优先级。调度器优先满足高优先级任务必要时可以抢占低优先级任务的资源。这里有个容易被忽视的细节优先级不是设置得越高越好。如果高优先级任务频繁抢占低优先级任务低优先级任务可能永远跑不完形成“饿死”。我做调度策略时通常会限制抢占频率比如一天最多允许抢两次并且被抢占的任务重新排队时优先级会自动提升这样既能保证紧急任务插队又不至于让日常任务完全无法推进。3.3 显存模型调度器怎么知道显卡装不装得下调度器分配GPU的时候不能只看“有多少张卡”还要看“这张卡的显存能不能装下你的模型”。大模型训练的显存占用是可以估算的模型参数占一部分梯度占一部分优化器状态Adam优化器极其吃显存占一部分中间激活值占一部分。很多人问“GPU显存容量到底是测算推理还是训练用的”答案是训练用的显存估算远比推理复杂因为训练时要同时存参数、梯度和优化器状态三份副本。实际调度器做决策时通常不会动态去算模型大小而是信任用户提交任务时填写的“资源请求”。这个请求是一个显式的声明比如显卡数量: 8单卡显存: 60GiCPU核数: 32内存: 256Gi。调度器拿到这份声明就在空闲资源里找满足条件的卡。这里就出现一个实际运维里天天遇到的头疼问题用户把资源请求写大了。一个人明明8卡可以跑的训练他填了16卡明明只需要40GB显存他填了80GB。为什么因为怕任务失败、怕排队、怕被抢占。结果就是资源被大量浪费。反过来也有人把显存填小了启动时直接OOM内存溢出。所以调度器在显存模型上要做两个动作一是建立资源账单定期统计每个任务真实占用和申请量的偏差二是通过调度策略倒逼用户精确填写比如对长期闲置资源进行回收。3.4 调度粒度从整卡到显存切片早期GPU调度非常简单一张卡只能整卡分配哪怕你只用了10%显存这张卡也相当于被独占。这种“整卡调度”模式在大模型时代浪费极大于是很多集群开始支持显存级别的切片调度。举个例子一张A100 80GB某个推理任务只需要20GB显存某个小规模训练任务需要40GB调度器可以把一张物理卡切成两个逻辑GPU分别分配给两个任务。这就好比一个工位被隔成了两个工位利用率直接翻倍。但显存切片在训练场景里要非常谨慎。训练和推理不一样推理负载波动小、显存占用稳定切片风险低训练负载在训练过程中显存需求会动态变化前向传播时中间激活值巨大到反向传播时又释放如果硬切成两半很可能会在某个批次batch跑到一半时突然OOM。我的经验是显存切片调度主要适合推理、评测、小规模微调这类轻量任务正式的大规模训练尽量还是整卡调度安全第一。4. 从提交任务到GPU亮灯一次完整调度链路实操4.1 第一步任务进队——把需求说清楚调度链路的第一步永远是用户提交任务。这里面最重要的不是命令写得多漂亮而是任务描述信息要足够精确。我经常看到有人提任务只写一句“给我64卡要跑大模型”调度器根本没法排只能随便找64张卡给他结果网络拓扑不对训练速度慢得离谱。一个标准的训练调度申请至少要包含下面这些信息需要的显卡类型A100、H800、L40S等最好不要混用不同型号显卡数量以及是否需要单节点连续卡槽比如8卡全在同一个节点单卡显存需求这个要根据模型参数、批大小、序列长度、优化器类型粗略估算是否多节点训练如果是节点之间需要什么样的网络RDMA还是普通TCP预计运行时长方便调度器做时间片规划提交方式通常是YAML文件描述。一个典型的K8sVolcano训练任务长这样apiVersion: scheduling.volcano.sh/v1beta1 kind: PodGroup metadata: name: llm-pretrain-pg spec: minMember: 8 queue: training-queue priorityClassName: high-priority --- apiVersion: v1 kind: Pod metadata: name: llm-pretrain-worker-0 labels: app: llm-pretrain spec: schedulerName: volcano containers: - name: trainer image: registry.internal/llm/trainer:v3.2 resources: limits: nvidia.com/gpu: 8 memory: 512Gi requests: nvidia.com/gpu: 8 memory: 512Gi这里比较关键的字段是minMember它告诉调度器“我至少要8个Pod同时在位才算成功不够就一个都不要给我”。这就是上面说的Gang调度的落地方式。很多新手不会写这个字段导致任务被调度器拆开启动然后卡住我还见过有人因此骂了三天调度器其实问题是YAML写错了。4.2 第二步资源匹配——调度算法怎么挑卡任务进了队列之后调度器就开始了核心工作匹配资源。这一阶段我建议你理解两个词filter和score。Filter就是过滤器把不符合硬性条件的节点全部排掉。比如你要64GB显存但节点上空闲卡的显存只有40GB这个节点直接出局你要8卡必须在同一节点但该节点只剩4张空闲卡也出局。过滤是做减法的过程越减越少。Score就是打分在剩下的候选节点里按策略打分排序。这里有个非常常见的策略叫binpacking装箱打包意思是尽量把任务塞到已经用得比较满的节点上而不是摊开放。为什么因为任务越集中空闲节点就越多调度器就越容易在后续接到大任务时整块分配资源。这就像搬家打包每一箱都装满才能少用几个箱子下次搬大件东西才方便。不过binpacking也有副作用把所有任务都挤在一个物理节点上一旦节点发生故障比如某一张显卡宕了会影响很多任务。所以有些调度器会改用spread散布策略把任务尽量摊开提升故障隔离性。实际生产中通常取折中对小任务偏散布对大任务偏打包。调度完成后调度器会更新自身的资源状态“已分配”的GPU会被锁住等待容器启动。这时候如果你去看nvidia-smi会发现GPU已经被某个进程占了但利用率可能为0因为训练框架还没完全初始化起来。这个状态是正常的别太慌。4.3 第三步容器起来之后框架如何接管GPU调度器把任务调度到了某几张具体的GPU上但训练进程自己是“看不见”物理显卡的。容器起来之后框架需要知道“我能用哪几张卡”。在K8s体系里容器可以通过环境变量NVIDIA_VISIBLE_DEVICES拿到调度器分配给它的GPU序号这个变量由设备插件自动注入。PyTorch训练时则通常依靠CUDA_VISIBLE_DEVICES来屏蔽其他显卡。例如调度器给容器分配了物理卡2、3、4、5那么容器内的进程看到的就是这四张卡且通常会被重编号为0、1、2、3。你在启动训练脚本时常见这样的命令export CUDA_VISIBLE_DEVICES2,3,4,5 torchrun --nproc_per_node4 train.py --model-size 7Btorchrun负责在这四张卡上各起一个训练进程并通过NCCL做集合通信。这里有一个隐藏知识点调度器分配GPU时如果考虑到了拓扑比如这四张卡在同一个PCIe交换机下面训练速度会明显更快如果四张卡分布在两棵不同交换机树上跨交换机的通信带宽可能会成为瓶颈。所以调度器在打分时不仅要看显存够不够还要看卡和卡之间的“距离”。4.4 第四步运维视角的日常检查调度链路走通后日常运维要做的事情其实比想象中少但也比想象中关键。我最常用的几个检查手段查看集群整体资源水位kubectl describe node可以看到每台节点已分配GPU和可分配GPU快速判断是否还有空闲卡。查看队列排队情况通过调度器自带面板比如Volcano的queue状态查询能看到多少个任务在排队、每个任务等了多少时间。查看单卡真实利用率进到运行中的Pod里执行nvidia-smi看GPU-Util和显存占用。如果GPU-Util长期低于50%通常说明任务配比有问题数据加载太慢、通信等待太多而不是调度问题。定期清理僵尸任务很多训练任务因为代码Bug导致进程卡死既不退出也不释放资源调度器不会主动杀它。运维要设置超时策略比如任务超过申请运行时长就自动终止。检查项常用命令关注点节点GPU分配kubectl describe node已分配/可分配是否合理队列排队情况调度器面板/API等待时长、队首任务单卡利用率nvidia-smiGPU-Util是否过低显存占用nvidia-smi是否接近OOM边缘通信状态nvidia-smi topo -m卡间拓扑是否匹配5. 万卡集群调度我踩过的几个坑5.1 利用率虚高调度器以为在干活实际在等网络有一次集群GPU-Util面板显示85%我一度以为集群跑得很满很健康。后来仔细一查训练日志发现有一批任务大量时间都花在NCCL集合通信的等待上真正在计算的GPU周期可能连一半都不到。GPU-Util是“此刻这张卡有没有指令在跑”的统计它不会告诉你指令是不是在傻等网络数据。这个问题在万卡集群里尤其明显。当模型大到需要跨几百个节点做张量并行时每一轮迭代都要做大量AllReduce通信通信和计算的比例一旦失衡调度器再准也没用。调度器能做的是尽量把任务集中到网络拓扑友好的节点组合上减少跨交换机通信但更深层的优化比如通信与计算重叠、梯度压缩是训练框架层面的事。所以下次看到利用率虚高先别急着夸调度去查一下NCCL的等待时间占比可能真相比面板好看多了。5.2 慢节点问题一张卡拖垮整个集合通信集群里经常会有“慢性子”节点。同一批A100有的卡散热不好导致降频有的卡PCIe链路降速有的卡驱动版本不对导致通信异常。在大规模分布式训练里全体节点必须同步等最慢的那个节点跑完这一轮迭代一张卡变慢整个训练速度就被它拖住。这就是慢节点效应。调度器对慢节点的感知一般是滞后的因为它只关心“显卡有没有空闲”不关心“这张卡算得快不快”。我的解决办法是在调度之外加一层慢节点检测定期在疑似慢节点上跑一次基准测试比如30分钟的矩阵乘法benchmark和同型号标准结果对比。如果偏差超过15%就把这台节点标记为“不健康”调度器不再向它分配新任务。这个机制帮我们避免过至少三次大规模训练被单节点拖垮的事故。5.3 显存低估导致OOM任务反复重启训练任务显存估算不准是个高频问题。有人觉得“模型7BFP16参数量14GB加上激活值给40GB就够了吧”结果跑到第87步激活值在长序列下直接爆掉进程崩了。调度器看到进程退出按策略重新拉起任务然后再次OOM循环了一整晚直到第二天早上运维发现。要解决这个问题不能光靠调度器需要一套更细的“资源画像”机制。我建议在训练框架里集成显存监控每隔30秒上报一次当前显存使用峰值。任务跑到30分钟后调度器就能画出它的显存曲线如果发现峰值已经超过申请值的80%就提前预警如果反复OOM超过两次自动降低任务所在队列的并发数。这套机制不是调度器标准功能但值得自研补上。还有一个很常见的坑把PyTorch的显存池算错了。PyTorch为了加速内存分配默认会预占很大一块显存可通过PYTORCH_CUDA_ALLOC_CONF调整max_split_size_mb和垃圾回收策略。你在nvidia-smi里看到的显存占用可能比模型实际需要的多得多。调度器如果只看运行时占用会误判任务真实需求还是那句话尽量用“资源请求”作为分配依据别用瞬时观测值。5.4 调度器自身成了瓶颈调度器也是软件它也要跑CPU、吃内存、做计算。一万张GPU大概对应几千台物理节点调度器每次调度决策都要扫描这些节点的状态。如果调度器优化不到位任务提交高峰时它可能要花几分钟才做出一个决策反而拖慢了整个训练流程。我见过最夸张的情况因为调度器的数据存储用了效率很低的索引结构导致集群里同时提交100个任务时调度延迟从秒级飙升到十几分钟。后来优化了缓存和数据模型才把调度延迟压回一秒以内。所以在大规模集群里监控调度器本身和监控GPU同等重要。我会重点盯三个指标调度延迟从任务进入队列到做出调度决策的耗时调度吞吐单位时间内能完成多少任务的调度调度器资源占用CPU、内存、数据库连接数是否异常如果发现这三个指标有恶化趋势优先检查调度器的数据存储和缓存命中率别一上来就怪网络。我个人在实际运维里感受最深的一件事是调度策略的难度往往不在技术选型而在流程和共识。调度器可以做到很智能但如果团队内部不约定资源请求怎么写、不评审显存申请、不共享“这个大任务要跑两周”的信息再强的调度器也会被垃圾输入拖垮。万卡集群的排班表面上是调度器的活实际上是整个团队一起把规则定清楚让调度器有章可循。这也是我在这篇里反复强调“把需求写清楚”的原因。调度器不是一个神秘的黑盒它只是把你和团队的行为规则翻译成显卡上一盏盏亮起的绿灯。