
开头先把一顶大帽子摘下来AI Infra里聊训练调度绝大多数人第一反应是上K8s、上集群、上队列好像单机根本不值得谈。但我在实际运维和调优过程中见过太多次这样的场面——一个团队申请了8张卡跑起训练来一看利用率7张闲得发慌1张也就50%上下。问题不在集群调度不在模型代码而是在单机这一层就没把资源喂饱。集群调度解决的是“卡分给谁”单机调优解决的是“分到的卡怎么真正干满活”这两者是递进关系前者建得再漂亮后者塌了整体产出依然是空的。这篇内容想记录的就是我在单机训练场景下摸爬滚打出来的经验和教训从指标怎么读、瓶颈怎么定位到数据管道怎么喂、显存怎么算、甚至单机多卡通信怎么不被拓扑坑。适合刚接手训练调优的算法工程师、做AI平台和AI Infra的研发同学以及那些“8卡机器跑不出8卡效果”但一直没找到切入点的团队。看完你至少能知道在把问题抛给集群调度之前先该在机器上做哪些事。1. 先搞清楚“8张卡7张闲”到底闲在哪很多人拿到一台8卡机器第一个动作就是跑nvidia-smi看到利用率不高就喊“GPU瓶颈”。但利用率这个数字本身只是表象它没有告诉你GPU到底在等什么、忙在哪里、瓶颈在CPU侧还是GPU侧。我建议第一步不是调优而是把这张卡的“工作状态”完整地读出来。1.1 利用率不等于干活——一张图看懂GPU到底忙不忙nvidia-smi里那个GPU-Util它统计的是GPU在采样周期内是否有kernel在执行、执行时间占比多少。注意它并不能反映GPU的计算单元到底被用到了几成。一个很典型的反例模型里有个算子特别慢整个step里GPU大部分时间都在等这一个算子的输出利用率照样显示90%以上但SM流式多处理器内部一堆单元是空的。所以我的建议是看GPU利用率至少要叠加五个维度一起看指标命令/来源说明GPU利用率nvidia-smi / DCGM有没有kernel在跑只能证明“在忙”不能证明“忙得有效”SM占用率ncu / nsys / DCGM sm_app_tick每个SM内部计算单元实际被使用的比例比利用率更接近真实负载显存占用nvidia-smi能看出batch size和模型尺寸是否合理显存带宽利用率ncu / DCGM dram_active很多模型瓶颈在显存带宽而不在算力比如GNN、推荐系统里的embedding功耗nvidia-smi -q -d POWER如果功耗只到TDP的50%说明GPU根本没吃饱实操里我常用的快速体检组合是dcgm-exporter接Prometheus5秒一个采样点跑一轮训练然后看两张图一张是SM占用率的时间序列一张是显存带宽的时间序列。如果SM占用率长期低于60%说明kernel没打满如果SM占用率挺高但显存带宽经常顶满说明你的模型是访存密集型这时候堆更多算力没有意义。1.2 最快的一次“体检”30分钟拿到完整画像不需要一上来就上重型profiler先用轻量手段拿整体画像。第一步开一个后台进程定时记录GPU状态nvidia-smi --query-gpuindex,utilization.gpu,utilization.memory,memory.used,power.draw,temperature.gpu \ --formatcsv,noheader -l 5 gpu_monitor.csv 第二步在训练脚本里混入CPU侧的监控用pidstat或者psutil记录CPU占用确保不是CPU侧先顶满了。第三步看训练日志里的step_time。step_time每轮训练稳定在某个值上下是正常的如果它周期性飙升那大概率有人在抢资源IO抖动、别的进程占CPU、甚至网卡被打满。这三步做完你就能回答一个关键问题GPU闲到底是在等数据还是在等通信还是在等kernel计算。这里要特别提醒一个误区不要看到nvidia-smi利用率低就立刻调大batch size。batch size调大确实能把利用率顶上去但如果瓶颈在数据管道的读取速度你调大batch size只会让每个step的等待时间更长整体吞吐反而可能下降。2. 单机调优第一关数据管道才是最常见的隐形瓶颈我在帮团队定位训练慢的问题时十次里有六七次最后都指向同一个地方数据加载。GPU算得再快数据喂不上就是白等。而且这个问题有个迷惑性从nvidia-smi上看GPU利用率是波动的有规律地“起来又掉下去”很多人还会误判成模型结构问题。2.1 DataLoader的“三件套”参数PyTorch的DataLoader是最常见的数据管道入口但默认参数对性能并不友好。我建议先对这三个参数做系统性的调整num_workers默认是0也就是主进程自己加载数据这等于让GPU和CPU串行干活。设为大于0后数据加载由子进程并行做。经验值是设成CPU核心数的一半到三分之二。不是越多越好worker太多会增加进程切换和内存拷贝的开销反而拖慢。prefetch_factor控制每个worker预取多少批数据默认是2。如果数据加载本身不算慢只是偶发抖动可以提高到4甚至8。它会多占内存但能明显平滑训练曲线。pin_memory设为True把数据固定在锁页内存里GPU从锁页内存拷贝数据速度更快因为它可以避免一次不必要的内存页面交换。一个我常用的起手式配置DataLoader( dataset, batch_size128, num_workers8, prefetch_factor4, pin_memoryTrue, persistent_workersTrue, )persistent_workersTrue确保worker在多个epoch之间不销毁重建省掉反复初始化的开销。加了这组配置后我见过不少场景直接把吞吐拉高30%以上而且改动成本极低。2.2 数据管道的进阶三板斧如果三件套调完还是喂不满GPU就要看数据管道的更深层瓶颈了。第一个是数据格式。如果你还在用小文件一堆jpg、png直接喂训练磁盘IO会先顶不住。小文件随机读的延迟是纳秒级延迟被放大成毫秒级IO的经典场景。把数据集打包成TFRecord、WebDataset或者直接用safetensors的切片格式顺序读大文件IO效率能提升一个量级。打包之后再用--shuffle做随机而不是依赖文件系统层面随机读。第二个是数据增强放哪边。很多人习惯在CPU侧做随机裁剪、翻转、色彩抖动这些计算会占CPU如果CPU是瓶颈就用GPU做。NVIDIA的DALI就是为了干这个的PyTorch也支持把部分增强移到GPU上比如torchvision.transforms.v2支持devicecuda。但注意不是所有增强都适合搬到GPU涉及CPU密集的字符串处理比如NLP的tokenize还是留CPU更合适。第三个是缓存。如果数据集不大比如几个GB以内直接映射到内存里。Linux下可以用vmtouch把文件锁进page cacheIO抖动能显著降低。对于中等规模的数据集把预处理后的结果直接缓存成内存映射文件效果也很明显。2.3 一次真实场景yolov8训练时加载瓶颈如何定位举个例子。之前我帮一个做视觉检测的团队调yolov8训练8卡机器上每个step只有2.3 it/s看起来GPU利用率能有70%但功耗只有TDP的45%。我当时判断是GPU在等数据但团队不太信因为利用率数字看着不低。我用nsys采集了30秒训练然后看timelinecudaMemcpy和H2D事件中间有大段的空白说明GPU kernel在等数据到达同时CPU侧看到有8个进程在跑数据增强个个都是100%CPU就是被数据增强打满了。调整方案分两步走。第一步把batch从64降到32降低单次拷入显存的数据量同时把workers从8提到16把prefetch_factor提到8。第二步把yolov8自带的mosaic增强中原先放在CPU侧的部分算子比如尺度缩放挪到GPU上执行。改完后再跑step_time从0.43秒降到了0.29秒功耗也上去了SM利用率从56%升到78%。这就是典型的“显存没爆、算力没打满、卡在数据管道”的案例。注意不要一上来就堆worker数量。worker太多会导致频繁的内存申请和上下文切换CPU反而被拖垮。我见过有人把worker设成64结果性能比8还差。确认瓶颈是CPU再往上加worker而且加的时候要同步观察CPU util和内存占用找到能从CPU榨出更多数据的关键拐点。3. 显存与batch size把压力算明白再谈利用率如果说数据管道是“喂饭”的问题那显存就是“碗有多大”的问题。很多人把batch size调大只是为了提高利用率但显存一爆就开始降batch来回试。其实训练过程中的显存占用是可以提前算出来的算明白了调参就有据可依不用瞎试。3.1 训练显存估算是怎么一回事大模型训练时显存里放的东西主要分四块模型参数权重、梯度、优化器状态比如Adam里的momentum和variance、以及激活值中间计算结果的缓存。推理只有第一块加激活而训练是四块全占这就是为什么同一个模型训练显存需求可能比推理大几倍。给一个能手动估算的简化公式假设模型参数量为P单位B十亿参数按照FP16/BF16混合精度训练计算项目计算方式显存占用权重FP162字节 × P2P GB梯度FP162字节 × P2P GBAdam优化器状态FP324字节 × P × 2momentum variance8P GB激活值与batch size、序列长度、层数、注意力头数相关不定通常占比最大以7B模型为例P7光权重加梯度加优化器就是2×7 2×7 8×7 84GB一张80GB的A100/H100塞不进去必须用ZeRO/DeepSpeed等方式把优化器状态和梯度做切分。这也是为什么很多人用单卡微调7B模型时总是OOM因为根本没算过这笔账。激活值还要另算它和batch size、序列长度是线性到二次方的关系。实际估算时建议直接跑代码玩起来比如用transformers的Trainer开一个很小的batch size比如1跑几步看nvidia-smi里的显存占用然后按batch size线性外推激活值是近似线性增长线性层有部分二次关系但粗算够用就能大概知道目标batch size会不会爆显存。3.2 batch size调不起来时怎么提升利用率如果显存不允许你把batch size调大但利用率确实不高有几个替代手段。梯度累积gradient accumulation是最常见的。把一个大batch拆成几个小的forward/backward梯度累加后统一更新。它能解决显存限制但需要注意它不会真正提高GPU利用率只是让你能模拟更大的batch size。对利用率的提升是间接的——有时候大batch收敛更快但单step吞吐并没有改变。混合精度AMP是提升利用率性价比最高的一招。FP16/BF16不仅让显存减半还能让Tensor Core跑起来。很多GPU的Tensor Core在FP16下的算力是FP32的数倍不开AMP等于白买卡。注意BF16在某些卡上不稳定如果loss波动大就换FP16或者加loss scaling。activation checkpointing也叫梯度检查点是另一个降低显存的大招。它不在前向过程中保存所有激活值而是反向传播时重新算一遍用算力换显存。这个对激活值占显存大的模型比如长序列、深网络效果非常明显但会增加前向计算量让step变慢。实际使用时建议只对大模型开小模型开它纯属浪费算力。序列打包sequence packing是LLM训练里常用的手段。把不同长度的样本拼到同一条序列里训练减少padding带来的无效计算。padding多的时候大量算力花在没意义的位置上利用率自然难看。但打包实现起来需要改attention mask对工程实现有一定要求。3.3 训练与推理对显存与调度需求的差异这节我想单独拎出来说因为很多团队在给卡分组时没搞清这个差异。训练和推理的资源画像完全不同训练是持续高负载、显存和算力长时间保持高位要求的是稳定性和吞吐多卡训练还需要卡间通信拓扑直接影响效率。推理是间歇性负载有请求才跑显存占用取决于模型尺寸和batch size但对时延极其敏感。推理调优更关注首token延迟、吞吐token/s以及能不能用批处理continuous batching把卡塞满。所以在集群调度上训练任务最好分配到通信拓扑好的8卡节点比如NVLink全互联而推理任务可以拆成更小的单位按显存切分比如MIG或虚拟化切分因为它的瓶颈不在通信而在访存和时延。用训练任务的评估指标去给推理卡分组或者反过来都会导致非常大的资源浪费。像热词里提到的“GPU显存容量是测算推理还是训练用的”答案就是要分开算。推理用权重加激活值通常按最长序列长度加一小部分KV cache训练用权重加梯度加优化器状态加激活值。两张口径差了数倍算错会在调度时出现大量资源碎片。4. 单机多卡调度与通信卡多不一定快别让NCCL拖后腿单机调优的最后一块硬骨头是多卡协同。很多人以为8卡机器数据并行速度应该直接翻8倍但实际跑出来可能只有3倍。这中间的差异大部分来自通信和拓扑的损耗。4.1 DDP单机多卡的基础认知PyTorch的DistributedDataParallelDDP是单机多卡训练最常用的方案。它每个进程持有一份模型副本用一块卡前向和反向各自算各自的反向算完梯度后用NCCL的AllReduce把所有进程的梯度求和并平均再各自更新参数。这里有个关键点AllReduce是同步操作所有卡必须等到最快和最慢的卡一起完成梯度同步才能进入下一步。这意味着只要有一张卡调度迟了、或者PCIe/NVLink路径比别人慢一点整个集群的步调就被拖慢了。木桶效应在这里非常明显。所以DDP的多卡加速不是线性的它有一个通信开销和计算收益的平衡点。小模型、小batch在8卡上跑通信时间可能占了总时间的40%以上此时多卡反而不如单卡。大模型、大batch才是DDP的主场。4.2 通信拓扑检查与背压跨卡通信走的是三个层级NVLinkGPU到GPU直连、PCIeGPU到CPU/跨交换机、网络跨机器。单机场景主要看前两者。nvidia-smi topo -m能打印卡与卡之间的拓扑连接关系NV表示NVLink直连PIX表示走PCIe交换机PXB表示走PCIe桥SYS表示走系统总线带宽依次下降。理想情况下8卡要尽量在同一颗CPU下且NVLink全互联否则跨CPU访问会走QPI/UPI链路带宽掉得厉害。实际操作中我经常遇到的情况是明明是8卡机器但DDP跑起来NCCL日志提示走的是PCIe而不是NVLink。排查方式很简单nccl-tests/build/all_reduce_perf -b 128M -e 1G -f 2 -g 8这个命令跑8卡AllReduce带宽测试如果单卡带宽和8卡聚合带宽相差很大比如单卡25GB/s、8卡聚合只有30GB/s而不是接近200GB/s就能确认通信拓扑存在瓶颈。还有一个常见的坑是CPU绑定binding。多卡训练时如果没有把每个进程绑定到对应GPU的NUMA节点上内存访问会走远端内存延迟飙升。用taskset或者torchrun自带的--nproc_per_node配合环境变量CUDA_VISIBLE_DEVICES确保进程和卡一一对应不要出现跨NUMA访问。4.3 从利用率到“效率”建立单机性能基线与调度依据调优到最后真正有价值的产物不是某个数字变好看了而是给这台机器建立一份性能基线。这份基线包括单卡单步时间、多卡吞吐随卡数变化的曲线、最优batch size范围、数据管道能支撑的最大吞吐、以及NCCL通信带宽上限。有了基线再谈集群调度才有意义。调度器决定把任务放在哪里之前如果能知道某个任务在单机上的“合理吞吐预期”就能判断它是不是需要更多卡、还是给了更多卡也吃不下。比如一个任务在8卡上吞吐只有单卡的4倍那你给它16卡大概率也到不了8倍反而浪费调度资源。我曾经帮团队搭建过一套简单的“单机基准测试流程”任务申请到卡后先跑5分钟小样本profiling自动生成上面那份基线报告调度器根据报告决定是继续扩容还是先做单机优化。这个流程跑起来后整个集群的GPU平均利用率从40%出头提升到了接近70%不是因为调度算法变聪明了而是把“给卡之前的体检”做扎实了。关于调度层还有一点值得提在单机内部即便只有一个训练任务也建议把GPU分配到固定的计算实例上。比如用MPSMulti-Process Service控制算力切片或用MIG做显存隔离避免多个任务在同一张卡上互相抢占导致性能抖动。很多“GPU利用率不稳”的现象其实是多个进程共享一张卡互相踩踏而不是任务本身有问题。最后再分享一个我踩过多次坑之后养成的习惯每次调优前先给机器拍一张“快照”把驱动版本、CUDA版本、PyTorch版本、NCCL版本、GPU拓扑、CPU型号全部记录下来。版本不匹配导致的性能问题我见过太多比如NCCL老版本在特定拓扑下会退化成环状通信换一个新版本直接快一倍。这种问题如果不留快照排查起来非常痛苦。希望这篇内容能帮你在面对“8张卡7张闲”的局面时先把账算清楚再动手调优。