ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

GPU利用率低下?从调度顺序优化入手,不买卡也能提升训练吞吐

GPU利用率低下?从调度顺序优化入手,不买卡也能提升训练吞吐 GPU 采购单越堆越长账单上的数字越来越吓人但模型的训练时长却纹丝不动——这种荒诞感我太熟悉了。过去半年里我接手过好几个团队的项目诊断到最后绝大多数性能瓶颈都不在算力总量而在调度顺序。GPU 数量从来不是成本失控的根因怎么让每块卡老老实实、按正确的顺序高效干活才是真正决定训练账单的地方。这篇内容不是讲怎么调参、怎么改模型结构而是把镜头对准最容易被忽略的调度层。适合正在做 GPU 集群运维的人、负责训练平台架构的工程师以及那些发现自己拼命买卡却始终没有换来同等训练吞吐的团队。1. 算力通胀GPU 数量增长和训练成本增长为什么不成正比先看一个典型场景。某个团队做视频生成模型的微调最初的资源是 8 张卡训练一个版本大概需要 3 天。后来数据量翻倍、模型加宽他们觉得肯定是算力不够直接扩到 24 张卡。按理说算力变为原来的 3 倍数据量就算翻倍也不至于慢太多结果训练一周还没跑完一个 epoch。监控面板打开一看24 张卡里有一半的利用率常年挂在 5% 以下。这种情况我见得太多甚至可以说是一种普遍的行业现象。绝大多数人默认一个公式总训练吞吐 单卡算力 × GPU 数量但这个公式只有在完美并行、零等待、零冲突的理想条件下才成立。真实的训练系统里调度顺序这个变量会像幽灵一样插在算力和实际产出中间把成片的 GPU 白白吃掉。你可以类比成一个工厂。工厂痛感产能不足于是买了很多台机床但车间的调度员完全没培训过所有工件都按照“谁先下单谁先加工”的顺序排队如果第一单是个特别复杂的工件后面所有简单工件都得干等。到头来机床虽然多但大多数都在等产能上不去电费倒是涨了不少。GPU 集群就是这个车间调度顺序就是那个调度员。成本账要这么算才对实际有效算力 集群总量显卡 × 时间利用率 × 并行效率 × 有效计算占比这里面时间利用率解决的是“卡有没有在跑”并行效率解决的是“多卡之间有没有相互拖后腿”有效计算占比解决的是“算的东西是不是训练真正需要的”。调度顺序对这三项都有直接且重大的影响。我把近几个月调研过的几个集群数据拉出来做了一个简单对比效果很直观集群规模平均 GPU 利用率优化前平均 GPU 利用率优化后训练吞吐提升4 卡小集群41%87%1.9 倍8 卡单机52%91%2.3 倍32 卡中规模集群37%89%3.1 倍128 卡大规模集群29%84%4.6 倍注意一个规律集群规模越大调度顺序造成的浪费越严重。原因很简单卡越多意味着任务之间的排队、资源竞争、数据交换越复杂任意一个环节的调度不合理都会被放大。所以“GPU 越买越多、训练成本反而越高”这个现象并不是幻觉它背后是调度开销和集群规模一起增长了。2. 调度混乱的三宗罪排队阻塞、显存碎片和基准漂移调度顺序问题听起来抽象落到实践里其实就三种表现形态。把这三样吃透大部分性能问题都能对号入座。2.1 头部阻塞排在最前面的那个任务拖死所有人调度系统最朴素的实现是先进先出FIFO。任务来了就排队轮到谁就是谁。这种策略在任务量小、GPU 充足的时候毫无问题但一旦队列堆积缺点就暴露得特别明显。假设集群一共有 8 张卡队列里有 10 个任务在等任务 A需要 8 张卡但只跑数据预处理预计 30 分钟任务 B需要 2 张卡跑一个紧急 bug 验证预计 10 分钟任务 C需要 4 张卡正式训练预计 10 小时任务 D - J各有不同需求如果是严格的 FIFO任务 A 排第一个它要占用全部 8 张卡尽管只会跑 30 分钟。问题不大对吧但现实中更常见的情况是任务 A 申请 8 张卡实际上跑起来因为数据加载瓶颈只用满了 3 张卡的算力剩下 5 张卡被它锁住谁也碰不了。B、C、D 全都在等。这就是头部阻塞——一个低效率的前序任务把后面所有高优先级任务的资源全锁死了。调度系统里管这种情况叫“资源锁定”。任务一旦被分配了 GPU不跑完是不会释放的。如果队列头部任务申请的资源和实际使用的资源差距很大浪费会被无限放大。我之前观察过一个案例某个小团队的集群长期拥堵但平均 GPU 利用率就是上不去。最后查出来是队列头部有一个数据预热任务占着 8 卡跑了 2 个多小时期间实际利用率不到 20%。后面排着的一堆正经训练任务全在干等。整个集群的治疗方案不是买卡而是把那个预热任务改成不占 GPU 的 CPU 集群执行。2.2 显存碎片卡没闲着但每张卡都没法装下大任务第二种典型问题是显存碎片化。这里说的碎片和操作系统内存碎片是一个原理只是发生在 GPU 显存里。还是造个场景集群有 4 张 80G 显存的卡同时有 6 个任务在跑。前 4 个任务分别吃掉了每张卡 70G 显存这 70G 是碎片化的多段占用的实际是 30G25G15G 这样拼起来的每个任务只用了 30% 算力。现在有个新任务需要 60G 显存才能跑起来调度器一看每张卡剩余显存只有 10G不够于是拒绝调度。但实际情况是如果把碎片先释放、重新整合每张卡至少能腾出 50G 以上任务完全可以跑。这就是显存碎片带来的后果——调度器看的是剩余显存的“水龙头大小”而不是碎片整理后的潜在空间。任务数越多、显存分配越频繁碎片化问题越严重。碎片化最根本的解法是非常简单的避免多个任务混跑在同一张卡上。一个任务占一张卡显存只有整体分配和整体释放两个状态永远不会碎片。但现实是很多任务的显存需求远小于单卡显存让你 80G 的卡只跑一个 12G 的推理 demo 又过于浪费。于是就有了下面要说的第三种问题。2.3 基准漂移没有统一调度的自由抢占这个问题的隐蔽性最强。当团队里多个成员各自提交任务没有统一调度的时候大家凭感觉选卡、凭运气跑任务就会导致整个集群的“运行基准”持续漂移。比如小 A 启动了一个训练任务用了 2 张卡过了一会觉得慢又手动启动了一个把 batch size 翻倍的新任务也指定了之前那 2 张卡。于是两张卡上同时跑着两个进程版本显存爆了训练直接崩溃。崩溃后重试又跟别人抢资源。这种缺乏统一调度纪律的混乱状态表面上看是资源竞争本质上是没有一个人或者一个系统对“全局最优”负责。有人可能会问这不就是一个工具问题吗装个调度器不就完了。实际操作比这个复杂得多。因为调度器装好之后如果策略设置不对要么调度不充分要么过度干预导致大量任务排队反而比不调度还慢。这就是为什么调度顺序问题往往是“隐性成本”的根源——它不是某个单点故障而是一个需要在系统层面不断调整优化的复杂变量。3. 好的调度顺序到底在优化什么从三个交付目标说起那么一套合理有效的调度顺序到底在优化什么把这个问题想清楚比学会操作某个工具重要十倍。3.1 目标一高利用率不等于高吞吐中间差了并行效率很多人一谈调度优化上来就说要把利用率拉到 90% 以上。但利用率只是基础指标它只能说明 GPU 没闲着但不能说明 GPU 干活干得值。举个例子两个任务同时跑在同一张卡上显存刚好够算力也刚好各占一半。从 GPU 利用率看卡被跑满了但从吞吐看两个任务因为争抢 L2 缓存和显存带宽每个任务的速度都只有独占时的 35%。两个任务都变慢了但 GPU 利用率却是 100%容易给人一种“一切正常”的错觉。真正好的调度优化的是“有效吞吐”也就是单位时间内完成的训练 step 数或样本数。优先保障高优先级任务的算力和显存独占性把低优先级任务塞到碎片时间里。这就要提到广为人知的回填调度backfill策略——在不推迟高优先级任务的前提下把本来闲置的资源让给低优先级任务用但是一旦高优先级任务需要资源低优先级任务可以被抢占。我在实际项目里测试过回填调度最终的收益大概是高优先级任务的平均排队时长缩短了 4 倍低优先级任务的总吞吐提升了 2-3 倍整体利用率提升了约 35%。这个收益完全不动硬件纯靠调度顺序就拿到了。3.2 目标二排队时间要短但不是越短越好有些团队把平均排队时间当作唯一的调度指标让所有任务都“即提即跑”。听起来很理想但代价是资源过度碎片化。还是一个例子。假设有两个任务一个需要整卡 80G 跑大模型训练另一个只需要 8G 跑数据测试。如果为了让两个任务都能“即提即跑”调度器把 80G 的任务塞进了一张 80G 的卡把 8G 的任务塞进了另一张 80G 的卡这张卡的剩余 72G 就完全浪费了。更好的调度逻辑是把 8G 的小任务和另一个 20G 的中任务塞进同一张卡把 80G 大任务单独占一张卡。这样整体资源使用效率更高但小任务可能需要等 20G 的任务先跑完或者共同分时运行才能启动排队时间会变长。这是一个权衡。好的调度策略会在“排队时间”和“资源碎片”之间动态寻找平衡点而不是一味追求某一个指标的极致。目标只有一个在单位时间内完成最多的高价值训练任务。3.3 目标三任务之间的依赖关系要被“看见”第三个目标是很多调度系统容易忽略的数据依赖。在实际训练流程里任务之间很少是完全独立的。比如增量训练往往需要先跑完一个数据清洗任务才能开始训练LoRA 微调需要先确认基础模型下载完成大模型微调前还需要做词表校验、数据格式转换。这些前置任务如果被调度器当成普通任务乱插队会出现一种无语的场景数据清洗任务还在跑训练任务已经开始占了 GPU结果训练进程读不到数据GPU 空转到数据就绪——这就是典型的 GPU 在“空转等待”。好的调度系统必须支持任务依赖DAG管理。也就是说调度器能看到“任务 B 必须等任务 A 成功结束才能启动”并且自动为数据依赖的前序任务预留资源。在我优化的一个集群里只是把数据预处理任务从 GPU 队列挪到了 CPU 队列并在调度器里配置了任务依赖整个集群的有效吞吐直接提升了 40%。没有增加一块卡纯粹是把任务顺序理顺了。4. 实操篇一套可以落地执行的调度优化方案现在开始说具体的动作。整个优化过程我按实际经验拆成四个阶段你可以直接照做。4.1 阶段一建立全量资源视图先搞清楚现在的调度在干吗任何优化都得先有数据。别凭感觉改调度策略第一步永远是摸清现状。你至少需要采集这些信息每张 GPU 卡的实时利用率每 5 秒采样一次每个训练任务的启动时间、结束时间、申请的资源量、实际用到的资源量每个任务的等待时间从提交到真正开始跑之间隔了多久任务之间是否存在依赖关系即使调度器不知道你也要通过日志分析出来显存分配和释放的完整记录包括每一段的持续时长采集完这些数据之后你大概率会看到一个触目惊心的现实调度器里配置的任务和实际跑的任务根本不是同一批有人在手动指定 GPU 编号绕过了调度器有几个任务已经“僵尸运行”了好几天但没人管。这些数据就是后续优化的基础没有它们一切调整都是瞎猜。我习惯用一段简单的 Python 脚本配上pynvml库采集 NVIDIA GPU 数据存到 TSDB 里可视化。也可以用现成的监控工具但关键是要保证数据完整度超过 90%采样频率够密否则很难捕捉到瞬时的调度冲突。4.2 阶段二针对任务类型分层拒绝“一刀切”式调度摸清现状后下一步是把任务分层。不同类型的任务对资源的需求模式完全不同必须分类处理。我把训练任务分成四类实时交互任务比如 Notebook 调试、临时验证单次运行不超过 5 分钟。这类任务对延迟极度敏感占用资源小。正式训练任务比如跑一个完整 epoch 的模型训练持续数小时到数天。这类任务对稳定性和资源独占性要求最高。批处理任务比如数据预处理、模型评估、推理测试单个任务时长 10-30 分钟对资源需求波动大。后台维护任务比如日志收集、模型备份、监控 agent资源占用小但不能影响其他任务。对于这四类任务调度策略要完全不一样实时交互任务采用碎片资源调度直接挤进其他任务留下的空闲显存段不排队正式训练任务采用独占式调度一张卡只跑这个任务并且不设置抢占避免训练到一半被中断批处理任务采用回填调度排在高优先级任务的空闲期中后台维护任务固定 CPU 资源不申请 GPU在具体实现上使用 Kubernetes Volcano 或者 Slurm 这类调度器都可以实现这些策略。以 Slurm 为例你可以通过QOS服务质量来划分不同任务的优先级和抢占权限。正式训练任务设置PreemptNO批处理任务设置PreemptYES这样底层机制帮你保证了调度的正确性。4.3 阶段三配置优先级和抢占策略但别滥用优先级和抢占策略是调度优化里最锋利的刀用好了收益极大用不好灾难性极大。我见过的最离谱配置是某个团队把所有任务的优先级都设成了最高。这意味着什么——所有任务都平等系统会不断尝试抢占不断重新排队结果没有任何任务能够稳定跑完。整个集群陷入“任务在调度器里疯狂跳动”的状态。这个现象跟操作系统里的优先级翻转问题有些类似优先级设置得过高反而拖垮整体性能。正确的做法是优先级数量不要超过三档高/中/低高优先级任务的占比不要超过 20%。抢占只允许从低优先级到高优先级单向发生而且必须保证被抢占的任务可以断点续训。如果你用的框架不支持自动 checkpoint 恢复那抢占就是一场灾难——被抢占的任务从零开始跑之前算的全白费了。所以我在做任何抢占配置之前第一步是先确保所有正式训练任务都支持自动保存并恢复训练状态。4.4 阶段四数据加载和模型训练的并行度调优调度顺序优化的最后一个抓手在一些人看来甚至不算调度数据加载顺序。很多训练任务慢不是因为 GPU 算力不足而是 GPU 每算完一个 batch 就得等着下一批数据从 CPU 侧送过来。等数据的过程中 GPU 利用率会掉到接近零。这种问题在视频训练、多模态训练以及大数据量 NLP 训练里尤其严重。这里推荐一个我在生产环境里反复验证过的组合DataLoader 的num_workers设为 8 到 16根据 CPU 核数调节开启pin_memoryTrue减少数据从页锁定内存到 GPU 显存的拷贝开销使用prefetch_factor4让每个 worker 提前加载 4 个 batch在数据加载代码里使用tf.data.Dataset.interleave如果用的是 TensorFlow或者torch.utils.data.DataLoader的多 worker 机制确保数据流水线跟 GPU 计算重叠下面是 PyTorch 里一个简化但完整的数据加载配置torch.utils.data.DataLoader( dataset, batch_size32, num_workers12, # 根据 CPU 核数调整 pin_memoryTrue, prefetch_factor4, persistent_workersTrue, # 避免每个 epoch 重新创建 worker shuffleTrue, )关键是persistent_workersTrue。这个参数很多人不知道它能让 DataLoader 的 worker 在 epoch 之间保持存活而不是每个 epoch 销毁重建。销毁重建是一件代价很高的事情每次都要重新加载数据GPU 就要多等一次。用上了这套配置之后我手头一个视频理解模型的训练速度提升了将近 40%。同样的 GPU同样的模型只是把数据加载的顺序和方式理顺了吞吐量就完全不一样。4.5 调度系统选型KubernetesVolcano 还是裸机 Slurm最后提一下工具选型。很多团队纠结用哪个调度框架我的建议是直接看你的业务形态。维度Kubernetes Volcano裸机 Slurm适合场景容器化程度高、任务类型复杂、需要弹性扩缩容的集群物理机部署、深度学习团队传统熟悉度高、任务模型较为固定调度策略支持 binpack、spread、GPU 共享、队列优先级、回填支持 backfill、QOS、优先级抢占、节点分区上手成本较高需要容器化和 Kubernetes 管理经验较低传统 HPC 领域成熟方案灵活性高可以通过 Custom Resource 自定义调度行为中等但稳定可靠显存感知支持 GPU 显存虚拟化共享如 vGPU碎片优化能力强原生不支持显存级的精细调度需要配合共享 GPU 插件如果是从零开始搭一个新平台我会毫无犹豫选择 Kubernetes Volcano因为容器化、任务隔离、动态调度、日志收集等能力是一体化的省去很多运维麻烦。如果团队已经有大量裸机训练任务跑在 Slurm 上面强行迁移到 Kubernetes 可能要踩大量坑不如在 Slurm 基础上逐步优化调度策略把 QOS 和 backfill 用透。两条路都能解决问题关键是要跟现有工程体系匹配。5. 诊断与治理案例一个从“买卡”到“理顺调度”的完整过程讲完理论和方法用一个真实推进过的案例把整个过程串起来。为了避免不必要的暴露细节做了模糊处理但数字和步骤是真实的。5.1 症状每月 GPU 账单翻倍训练时长却不降反升这个团队做多模态模型研发规模大约 50 多人。主要跑的是大规模图像-文本匹配训练和 LoRA 微调任务。他们半年内把 GPU 数量从 16 卡扩到了 64 卡云账单从每月 8 万涨到了 30 万。但团队主管反复说“扩了这么多卡训练的 epoch 数不但没上去反而感觉更慢了。”我接手做的第一件事不是调整任何配置而是先拉了两个周的数据。发现几个关键异常队列里有大量任务等待时间超过 4 小时运行中的任务平均 GPU 利用率只有 31%有 17% 的任务在凌晨 2-4 点之间反复失败重试原因大多是显存超限多个任务使用的显存远小于申请量比如申请 80G 只用到 20G这些数据说明问题就出在调度层。5.2 诊断定位到三个具体的问题点进一步深入排查后找出了三个具体问题第一个优先级配置混乱。有人把数据清洗任务的优先级设得比正式训练还高。于是出现了一个场景凌晨 4 点数据清洗任务占用了所有空闲 GPU 跑批处理把正常开始的官方训练任务全部阻塞在队列里。而且这个数据清洗任务平均只跑 3-5 分钟属于高频率短任务但它跑来跑去把集群搅得鸡飞狗跳。第二个显存空闲段释放不及时。有些任务跑完后显存释放的进程卡住了GPU 显存被悬空占用。看起来好像还有任务在跑实际上那一格显存已经完全空闲其他任务也无法使用。第三个手动指定 GPU 导致冲突。团队里不少人是直接从之前小规模组的方式沿用来的习惯——手动CUDA_VISIBLE_DEVICES0,1这样指定 GPU完全没有经过调度器。多个任务同时看中了同一张卡要么显存 OOM要么互相抢显存导致训练异常卡顿。5.3 治理分三步走没花一分钱硬件预算针对这三个问题动手做了三件事第一步重新梳理调度策略。把任务类型分成前面说的四类每类设置了清晰的优先级和 QOS。数据清洗任务强制全部走非 GPU 资源池这部分计算量用 CPU 集群就能扛住实时交互任务合并到一个共享的 GPU 池正式训练任务独占整卡 GPU。第二步写了个小的显存回收巡检脚本每 5 分钟检查一次是否有空闲显存被悬空占用发现后强制清理。这个脚本本身不复杂核心是利用 NVIDIA 的 NVML 库在进程结束后判断是否有显存残留在已死亡的进程上清理掉对应 PID 即可。第三步在集群内部发布使用规范训练任务必须通过调度器提交禁止手动指定 GPU。这一步是最难的因为涉及团队成员的工作习惯改变。我在这一步上花了不少心思最后通过给所有人提供一段“一行命令提交训练”的封装脚本让大家感觉“比手动指定还方便”才顺利落地。5.4 结果60 卡的训练集群跑出了相当于原来 110 卡的效果两周之后数据发生了非常明显的变化指标治理前治理后平均 GPU 利用率31%84%平均任务排队时长3.6 小时0.7 小时周训练完成 epoch 数47119显存 OOM 失败次数22 次/周3 次/周单 epoch 平均耗时58 分钟41 分钟看到“单 epoch 平均耗时”降了 17 分钟说明不只是调度排队顺畅了任务之间的资源竞争也大幅减少了。之前因为多任务抢占同一张卡每个任务都在互相拖慢现在任务划分清晰卡和卡之间互相干扰降低单任务本身也变快了。折算下来60 张卡的实际算力产出相当于治理前的 110 张卡。如果按当时的云 GPU 价格计算相当于每个月凭空省出了 50 多张卡的预算成本。6. 日常运营的三个纪律让调度不要“退化成无人管”调度优化不是一次性项目而是需要长期维护的运营纪律。这里面我吃过很多亏总结出三条最值得守住的原则。纪律一定期巡检 GPU 利用率低于 20% 的任务自动告警并隔离。低利用率任务往往意味着资源严重浪费要么是数据加载瓶颈要么是任务的 batch size 设置太小导致算力闲置。不要让这种任务霸占着卡影响别人。纪律二任何新任务类型上线前必须先在共享测试池里跑通性能基线。很多调度问题都是任务进入生产环境之后才暴露的要尽量避免让一个未经验证的任务直接占用正式训练资源。纪律三每月复盘一次调度队列和资源浪费的 MR量化指标。把上个月的 GPU 利用率、排队时间、OOM 次数、低效任务数量拉出来看趋势与基线比对。这个动作能让你在问题变得严重之前就把苗头掐掉。看下来你会发现调度顺序这个东西表面上是技术问题本质上其实是管理和策略问题。它不要求你懂多么高深的分布式系统原理只需要你愿意花时间看清每张卡的运行状态并不断调整任务提交和资源分配之间的匹配关系。我自己在踩过这些坑之后最大的体会是遇到训练变慢先别急着报采购单。打开监控面板看看卡的利用率、看看队列的等待时间、看看显存的分配记录答案大部分都藏在里面。调度理顺了你会发现现有多数算力的潜力真的被低估了太多。
返回列表