ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

昇思MindSpore大模型训练评估与性能调优实战

昇思MindSpore大模型训练评估与性能调优实战 大模型训练这件事真正跑过一轮的人都知道最难的往往不是把模型结构搭出来而是训练过程中的评估和性能调优。我接触昇思 MindSpore 这套框架有段时间了从最初在小规模集群上跑通一个百亿参数的模型到后来在更大规模上做分布式训练调优中间踩过的坑、调过的参数、熬过的夜足够写一本小册子。这篇文章不打算讲太多理论而是把我自己在实际项目中积累的评估体系搭建思路和性能优化手段尽可能完整地分享出来。无论你是刚接触 MindSpore 的新手还是已经在做分布式训练的老手应该都能从中找到一些可以直接拿来用的东西。1. 为什么大模型训练的评估体系不能照搬小模型那套1.1 大模型评估的特殊性在哪里小模型训练的时候我们习惯看 loss 曲线、准确率、F1 值这些指标一轮评估跑下来几分钟就完事。但大模型完全是另一回事。一个百亿参数级别的模型单次完整评估可能要跑几个小时甚至更久如果评估策略设计得不好训练时间可能还没有评估时间长。更关键的是大模型训练本身就是一个高成本的过程每次评估都意味着 GPU 资源的占用如果评估频率过高训练效率会被严重拖累如果评估频率过低又可能错过模型退化的早期信号。我在早期项目中就犯过这个错误。当时按照小模型的习惯每训练几百个 step 就做一次验证集评估结果发现训练速度直接掉了一半。后来分析下来问题出在评估阶段没有做分布式并行单卡跑验证集成了整个训练流程的瓶颈。这个教训让我意识到大模型的评估体系必须从训练流程的整体效率出发来设计而不是简单地“训练完就评估”。另一个容易被忽视的点是评估指标的选取。大模型在很多任务上并不是单一指标能衡量的比如生成类任务BLEU、ROUGE 这些传统指标和人类感知的质量之间往往存在较大差距。如果只盯着这些自动指标很容易出现指标很好看但实际效果很差的情况。所以在大模型评估中我通常会建议同时关注多个维度的信号包括训练 loss、验证 loss、下游任务指标、生成样本的人工抽检等。1.2 评估频率与训练效率的平衡策略评估频率的设定需要根据模型规模、数据集大小和训练总步数来综合决定。我的经验是在训练初期可以适当降低评估频率因为模型参数还在剧烈变化频繁评估意义不大到了训练中后期模型逐渐收敛这时候可以适当增加评估密度以便及时发现过拟合或退化。具体来说对于一个训练步数在 10 万步左右的模型我会这样安排评估节奏前 1 万步每 2000 步评估一次中间 1 万到 5 万步每 1000 步评估一次5 万步以后每 500 步评估一次。当然这个节奏不是固定的需要根据实际 loss 曲线的变化来动态调整。如果发现验证 loss 开始上升那就说明可能出现了过拟合这时候应该加密评估频率同时考虑早停策略。还有一个实用技巧是使用“子集评估”。完整验证集评估太慢的话可以随机抽取验证集的一个子集来做快速评估比如取 10% 到 20% 的验证样本。这样虽然指标会有一定波动但趋势判断是够用的。等到训练结束或者关键节点再做一次完整评估。这个方法我在多个项目中都用过效果不错能把评估开销降低到原来的五分之一左右。1.3 分布式评估的实现要点在 MindSpore 中做分布式评估核心是要让评估过程也走数据并行。很多人只关注训练阶段的并行忽略了评估阶段同样需要并行化。具体做法是在评估时也使用nn.DataParallel或者手动做数据切分让每张卡处理一部分验证数据最后再汇总结果。这里有一个容易踩的坑评估阶段的 batch size 和训练阶段不一定相同。训练时因为要计算梯度batch size 受显存限制可能比较小但评估时不需要保存梯度batch size 可以适当放大这样能提高评估吞吐量。我在实际项目中通常会把评估的 batch size 设为训练时的 2 到 4 倍前提是显存允许。另外评估阶段的model.eval()调用一定要记得加否则 BatchNorm 和 Dropout 层的行为会和训练时一样导致评估结果不准确。这个看起来是常识但我在 review 别人代码的时候发现还真有不少人忘记加。2. MindSpore 训练性能优化的几个关键抓手2.1 图编译模式的选择与取舍MindSpore 提供了两种主要的执行模式PyNative 模式和 Graph 模式。PyNative 模式就是动态图写起来像普通 Python 代码调试方便Graph 模式会把整个计算图编译优化后再执行性能通常更好。在大模型训练中我一般会先用 PyNative 模式做小规模调试确认逻辑没问题后再切到 Graph 模式做正式训练。但 Graph 模式也不是没有代价的。编译过程本身需要时间而且有些动态控制流在 Graph 模式下支持得不够好。我遇到过一个情况模型里有一个根据条件动态选择分支的逻辑在 PyNative 下跑得好好的切到 Graph 模式后直接报错。后来是把那个逻辑改成了静态实现才解决。所以我的建议是如果模型里有复杂的动态控制流要么提前做好适配要么就老老实实用 PyNative 模式虽然慢一点但至少能跑通。还有一个细节是context.set_context(modecontext.GRAPH_MODE)这个设置的位置。它必须在任何计算操作之前调用否则可能不生效。我见过有人在定义完网络之后才设置结果模式没切过去白白浪费了优化机会。2.2 数据加载管道的优化大模型训练中数据加载往往是第一个瓶颈。如果数据管道跟不上计算速度GPU 就会经常处于等待状态利用率上不去。MindSpore 提供了mindspore.dataset模块里面有很多优化手段可以用。首先是num_parallel_workers这个参数它控制数据加载的并行度。默认值通常偏小我一般会设成 CPU 核心数的一半到三分之二。设太大了反而会因为线程切换开销导致性能下降。其次是prefetch_size这个参数控制预取的数据量适当调大可以让数据加载和计算更好地重叠。我通常设成 2 到 4 倍于 batch size 的数量。还有一个很实用的技巧是使用dataset.map的python_multiprocessing参数。如果数据预处理逻辑比较复杂用多进程而不是多线程来做 map 操作能绕过 Python GIL 的限制显著提升数据准备速度。不过要注意多进程模式下每个进程会复制一份数据内存占用会上去需要根据机器配置来权衡。对于超大规模数据集我强烈建议提前把数据转成 MindRecord 格式。MindRecord 是 MindSpore 自己的数据格式读取效率比原始文件高很多而且支持分布式读取。转换过程虽然需要一些时间但训练阶段节省的时间远远超过这个投入。2.3 混合精度训练的配置细节混合精度训练是大模型训练的标配用 FP16 做前向和反向计算用 FP32 保存模型参数既能节省显存又能加速计算。MindSpore 里通过amp模块来实现配置起来不算复杂但有几个细节需要注意。首先是 loss scaling 的策略。MindSpore 提供了静态 loss scale 和动态 loss scale 两种方式。静态方式需要手动设置一个固定的 scale 值动态方式会根据梯度情况自动调整。我一般推荐用动态方式因为它更省心而且对不同的模型和任务适应性更好。配置代码大概长这样from mindspore import amp model amp.build_train_network(network, optimizer, loss_fn, levelO2, loss_scale_manageramp.DynamicLossScaleManager())这里的levelO2表示除了 BatchNorm 等少数算子外大部分计算都用 FP16。如果发现训练不稳定可以降到O1只对部分算子用 FP16。另一个坑是某些算子在 FP16 下容易溢出。比如 softmax 在数值较大时FP16 的表示范围不够容易出现 NaN。解决办法是对这些算子做特殊处理要么强制用 FP32 计算要么在计算前做数值裁剪。MindSpore 的amp模块支持通过keep_batchnorm_fp32等参数来控制哪些层保持 FP32需要根据具体模型来调整。3. 分布式训练中的通信优化与显存管理3.1 数据并行与模型并行的选择逻辑当模型大到单卡放不下的时候就必须考虑分布式训练了。MindSpore 支持数据并行、模型并行和混合并行三种模式。数据并行是最简单的每张卡上放一份完整的模型副本数据切分到不同卡上梯度通过 AllReduce 同步。模型并行则是把模型的不同层放到不同卡上适合参数量特别大的情况。混合并行就是两者结合。选择哪种模式主要看模型大小和卡的数量。我的经验是如果模型单卡能放下优先用数据并行因为实现简单、通信模式成熟。如果单卡放不下但两张卡能放下可以考虑模型并行。如果模型特别大比如千亿参数级别那就必须用混合并行了。在 MindSpore 里配置数据并行比较简单用nn.DataParallel包装一下就行。但要注意数据并行下每张卡的 batch size 会变小如果总 batch size 不变学习率可能需要相应调整。我通常会在切换并行策略时重新做一次学习率 warmup避免训练不稳定。3.2 AllReduce 通信的调优手段数据并行中最主要的通信开销来自梯度 AllReduce。如果通信效率低再多卡也发挥不出优势。MindSpore 底层用的是华为的 HCCL 通信库在昇腾硬件上表现很好但如果是在 GPU 上跑可能用的是 NCCL。优化 AllReduce 有几个方向。一是增大通信 batch也就是把多个小梯度累积成一个大梯度再通信减少通信次数。MindSpore 支持梯度累积通过TrainOneStepCell的sens参数或者手动实现都可以。二是使用通信与计算重叠的技术让 AllReduce 和反向计算并行进行。MindSpore 的ParallelMode里有一些相关配置但需要根据具体硬件和网络环境来调。还有一个容易被忽视的点是网络拓扑。如果卡之间的网络带宽不均匀AllReduce 的效率会受很大影响。我建议在正式训练前先用一个简单的通信 benchmark 测一下实际带宽做到心里有数。如果发现某些卡之间通信特别慢可能需要调整卡的分配策略。3.3 显存优化的实战技巧大模型训练中显存永远是不够用的。除了混合精度之外还有几个手段可以显著降低显存占用。首先是梯度检查点Gradient Checkpointing也叫激活重计算。它的思路是不保存中间层的激活值在反向传播时重新计算一遍。这样能用时间换空间显存占用能降低不少。MindSpore 里通过model.recompute相关接口来配置可以指定对哪些层做重计算。我通常会对那些激活值特别大的层开启重计算比如 Transformer 里的 FFN 层。其次是优化器状态的显存占用。Adam 优化器会为每个参数保存一阶和二阶动量显存占用是参数量的两倍。如果显存紧张可以考虑用 AdaFactor 或者 SGD 这类显存占用更小的优化器。或者使用 ZeRO 技术把优化器状态切分到不同卡上但这需要框架层面的支持MindSpore 在这方面也在不断完善。还有一个技巧是及时释放不需要的中间变量。Python 的垃圾回收机制有时候不够及时可以手动调用del和gc.collect()来释放显存。特别是在评估阶段切换到训练阶段的时候把评估用的临时变量清掉能腾出不少显存。4. 训练过程中的监控与问题排查4.1 关键监控指标的采集与解读训练大模型的时候不能只看 loss。我通常会监控以下几类指标计算指标loss、梯度范数、学习率、系统指标GPU 利用率、显存占用、通信带宽、数据指标数据加载速度、队列长度。这些指标能帮助快速定位问题出在哪个环节。梯度范数是一个特别有用的指标。如果梯度范数突然变得很大说明可能出现了梯度爆炸需要检查学习率或者加梯度裁剪。如果梯度范数一直很小可能是梯度消失需要考虑调整网络结构或者初始化方式。我在训练 Transformer 类模型时一般会把梯度裁剪的阈值设在 1.0 左右效果比较稳定。GPU 利用率低是最常见的问题之一。如果发现 GPU 利用率长期在 50% 以下那基本可以确定是数据加载或者通信成了瓶颈。这时候需要去看数据队列的长度如果队列经常为空那就是数据加载太慢如果队列是满的但 GPU 利用率还是低那可能是通信或者计算本身的问题。4.2 常见训练异常的排查路径训练过程中最常见的异常是 loss 变成 NaN。这个问题可能由多种原因引起学习率太大、梯度爆炸、数据里有异常值、混合精度溢出等。排查的时候我一般按这个顺序来先检查数据确认没有 NaN 或 Inf 的样本然后降低学习率试试如果还不行就检查混合精度配置把 loss scale 调小或者切换到 FP32 跑几步看看。另一个常见问题是训练速度突然变慢。这种情况通常是某个环节出现了资源竞争。比如多个进程同时读写同一个文件或者通信库出现了重传。我遇到过一次训练跑着跑着速度掉了一半查了半天发现是某个日志文件写得太频繁把磁盘 IO 占满了。后来把日志级别调高问题就解决了。还有一个比较隐蔽的问题是模型收敛但指标不达标。这往往不是训练本身的问题而是评估方式或者数据处理有问题。比如验证集和训练集的数据分布不一致或者评估时的预处理和训练时不一致。这种问题需要仔细检查数据管道和评估代码确保两边完全对齐。4.3 日志与可视化工具的使用心得MindSpore 提供了SummaryCollector等工具来采集训练日志可以对接 TensorBoard 做可视化。我一般会在训练脚本里配置好 Summary 采集把 loss、学习率、梯度范数这些关键指标都记录下来。这样训练过程中可以随时在 TensorBoard 上看曲线不用盯着终端输出。但 Summary 采集本身也有开销如果采集频率太高会影响训练速度。我的做法是训练初期采集密一点方便观察趋势训练稳定后降低采集频率比如每 100 步采集一次就够了。另外Summary 文件会占用磁盘空间长时间训练的话要记得定期清理旧文件。除了框架自带的工具我还习惯在代码里加一些自定义的打印语句输出一些框架不直接提供的指标。比如每个 step 的实际耗时、数据加载的等待时间等。这些信息在排查性能问题时特别有用。不过要注意控制打印频率太多了反而会拖慢训练。5. 从单卡到多卡一次完整的性能调优实录5.1 初始配置与基线测试前段时间我接手了一个项目需要在 8 卡昇腾环境上训练一个 130 亿参数的模型。最初的配置很朴素数据并行、FP16 混合精度、batch size 设为 8。跑起来之后发现单步耗时大约 1.2 秒GPU 利用率只有 60% 左右。这个利用率明显偏低说明有优化空间。我先做了一次基线测试记录下各个环节的耗时分布。测试方法很简单在训练循环里加时间戳分别记录数据加载、前向计算、反向计算、梯度同步的耗时。结果发现数据加载占了大约 30% 的时间梯度同步占了 25%真正用于计算的时间只有 45%。这个分布说明数据加载和通信是两个主要的优化点。5.2 数据管道重构与效果验证针对数据加载慢的问题我做了三件事。第一把原始数据转成 MindRecord 格式减少读取时的解析开销。第二把num_parallel_workers从默认的 4 调到 12充分利用 CPU 资源。第三在数据管道里加了prefetch让数据加载和计算重叠起来。改完之后重新测试数据加载的耗时占比从 30% 降到了 12% 左右单步耗时降到了 0.95 秒。这个提升还是很明显的。不过要注意num_parallel_workers不是越大越好我试过调到 16结果因为线程切换开销性能反而下降了。所以这个参数需要根据实际机器配置来调没有万能值。5.3 通信优化与最终性能对比接下来处理通信问题。我首先尝试了梯度累积把 batch size 从 8 增加到 32但每 4 个 step 才做一次梯度同步。这样通信频率降低了 4 倍通信开销占比从 25% 降到了 8% 左右。不过梯度累积会改变训练的动态特性学习率需要相应调整。我一般会按 sqrt 比例来放大学习率比如 batch size 扩大 4 倍学习率扩大 2 倍。然后我又尝试了通信与计算重叠。MindSpore 在ParallelMode里提供了一些配置选项但需要根据具体的网络结构来调。我花了一些时间才找到合适的配置让 AllReduce 能和反向计算并行起来。这部分优化带来的提升大概在 10% 左右没有梯度累积那么明显但积少成多。最终优化完成后单步耗时从最初的 1.2 秒降到了 0.68 秒GPU 利用率提升到了 85% 以上。整个训练任务的时间从预计的 7 天缩短到了 4 天左右。这个过程中数据管道优化贡献了大约 40% 的提升通信优化贡献了 35%剩下的来自混合精度和其他细节调整。6. 一些容易被忽略但很关键的细节6.1 随机种子与可复现性大模型训练中可复现性是一个经常被忽视但很重要的问题。如果每次训练结果都不一样很难判断性能变化是来自代码改动还是随机因素。MindSpore 里可以通过mindspore.set_seed()来设置随机种子但要注意这只控制了 MindSpore 自身的随机性Python 的 random 模块和 NumPy 的随机性需要单独设置。另外分布式训练下的随机性控制更复杂。数据并行时每张卡上的数据顺序可能不同这会影响 BatchNorm 的统计量。如果对可复现性要求很高可能需要固定数据切分方式并确保每张卡上的数据顺序一致。不过完全可复现在大模型训练中往往很难做到因为浮点运算的顺序不同就会导致微小差异这些差异在长时间训练后会被放大。所以我的建议是对可复现性要求别太高关注趋势而不是精确数值。6.2 检查点保存与恢复的策略大模型训练动辄几天甚至几周中间难免遇到各种意外。检查点保存策略直接关系到能不能在故障后快速恢复。我一般会设置定期保存比如每 1000 步保存一次同时保留最近几个检查点防止最新的检查点损坏。保存检查点本身也有开销特别是模型很大的时候写一次检查点可能要几分钟。为了减少对训练的影响可以异步保存让保存操作在后台进行。MindSpore 支持CheckpointConfig里的async_save参数开启后保存操作不会阻塞训练。不过异步保存需要注意磁盘空间如果保存速度跟不上训练速度可能会堆积大量待保存的检查点。恢复训练的时候除了模型参数优化器状态、学习率调度器的状态、当前的 step 数这些都要恢复否则训练动态会不一致。MindSpore 的model.train接口支持从检查点恢复但需要确保保存和恢复的配置一致。6.3 硬件环境的适配问题不同的硬件环境对训练性能影响很大。昇腾和 GPU 在算子实现、通信库、内存管理上都有差异。如果代码需要跨平台运行最好在早期就做好适配测试不要等到最后才发现某个算子在目标平台上不支持。我在项目中遇到过一个问题某个自定义算子在昇腾上跑得好好的迁移到 GPU 上就报错。后来发现是那个算子用到了昇腾特有的指令GPU 上没有对应实现。解决办法是用 MindSpore 提供的标准算子重新实现或者写一个跨平台的版本。这个教训告诉我如果项目有跨平台需求尽量用框架的标准算子少用平台特有的优化。另外驱动版本和框架版本的匹配也很重要。我遇到过因为驱动版本太旧导致通信库性能下降的情况升级驱动后问题就解决了。所以建议在项目开始前先确认硬件驱动、通信库、框架版本之间的兼容性避免后期踩坑。6.4 训练中断后的快速恢复流程即使有检查点训练中断后的恢复流程也需要提前设计好。我一般会写一个恢复脚本自动找到最新的有效检查点加载模型和优化器状态然后从断点继续训练。这个脚本要能处理检查点损坏的情况自动回退到上一个可用检查点。恢复训练后前几个 step 要特别关注 loss 和梯度范数确认和中断前一致。如果发现异常可能是检查点加载不完整或者状态恢复有问题。我遇到过优化器状态没恢复对的情况导致恢复后 loss 突然跳变后来发现是保存检查点时漏掉了优化器的动量信息。还有一点是数据加载器的状态。如果用了 shuffle 或者自定义的采样器恢复训练时数据顺序可能和中断前不一致。这对训练效果影响不大但如果对可复现性有要求就需要把数据加载器的状态也保存下来。MindSpore 的 dataset 模块支持保存和恢复状态但需要手动配置。7. 关于评估与优化的一些个人体会做 MindSpore 大模型训练这段时间我最大的体会是评估体系和性能优化不是两个独立的事情而是相互影响的。评估设计得不好会拖慢训练速度性能优化不到位评估的迭代周期也会变长。所以最好在项目初期就把两者放在一起考虑而不是先搞训练再补评估。另一个体会是优化要有优先级。不要一上来就追求极致的性能先把流程跑通再逐步优化瓶颈。我见过有人花大量时间调通信参数结果发现真正的瓶颈在数据加载上。所以先用 profiling 工具找到真正的瓶颈再针对性地优化效率会高很多。最后大模型训练是一个系统工程涉及数据、计算、通信、存储多个环节。任何一个环节出问题都可能影响整体效率。保持对各个环节的监控建立快速排查问题的能力比单纯追求某个环节的极致优化更重要。毕竟一个稳定跑完的训练比一个跑得很快但经常崩溃的训练有价值得多。
返回列表