ARTICLE DETAIL

资讯详情

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

昇思MindSpore大模型训练:评估体系搭建与性能优化实战

昇思MindSpore大模型训练:评估体系搭建与性能优化实战 做昇思 MindSpore 大模型训练我先把话说在前面性能优化做得再好评估体系跟不上训练过程就是盲飞。这里的“评估”不是训完以后跑几个下游 benchmark 那么简单而是覆盖训练全过程的一套判断标准——用哪些指标判断模型状态、在哪个 step 采样、按什么条件挑选 checkpoint、出现异常时从哪里开始排查。没有这套体系你很难回答“这个 loss 值算正常吗”“什么时候可以停”“这次改动是变好了还是变坏了”。我实际用昇思 MindSpore 跑过多轮 10B 级别的大模型训练也处理过集群上常见的通信瓶颈和显存 OOM。这篇文章不画架构图直接讲我在项目里怎么搭评估体系、怎么做性能优化以及遇到问题时的排查顺序。适合已经在用 MindSpore 训练、准备把训练流程工程化的人如果你是刚入门正文里的指标定义和优化思路也可以直接套用不一定非得上千卡集群单机多卡同样适用。1. 大模型训练里的评估体系到底在评估什么1.1 训练过程评估和模型能力评估是两件事我见过不少团队把“评估”两个字等同于验证集准确率这是大模型训练里最容易翻车的认知。验证集准确率是模型能力评估它解决的是“模型最终好不好用”的问题但大模型训练过程长达数天甚至数周中间任何一个环节出问题都可能让整个训练白跑这时候需要的是训练过程评估。训练过程评估关注的是模型在训练中是否健康loss 是否在预期区间下降、梯度范数有没有突然爆炸、loss scale 是否稳定、每个 step 的吞吐量有没有劣化。它解决的是“训练还没结束我怎么判断它会不会成功”的问题。所以我的做法是把评估拆成两张表一张盯过程一张盯结果。评估类型评判对象常用指标采样频率主要用途训练过程评估损失、梯度、算力利用率loss、grad_norm、tokens/s、step耗时每步或每隔几步早停、调整学习率、发现数据问题模型能力评估验证集、下游任务集perplexity、ACC、F1、ROUGE每隔固定步数或固定token数筛选checkpoint、对齐上线需求很多人在训练脚本里只加了一个LossMonitor然后盯着终端日志看 loss 小数点后第三位的变化这远远不够。尤其是当你同时在调混合精度、并行策略、数据预处理这三件事时必须靠评估体系把“性能变化”和“模型质量变化”分开。否则你可能为了把一个算子的利用率从 70% 提到 80%改变了算子放置顺序结果模型能力评估指标悄悄劣化了而你浑然不觉。1.2 指标落地loss、梯度范数、吞吐量缺一不可在昇思 MindSpore 里最基础的评估指标肯定是 loss但它绝不能只看一个点。我通常把训练日志按 step 输出成结构化 JSON每个 step 记录这几项mean loss当前 step 的平均 lossgrad norm所有参数梯度的 L2 范数lr当前真实学习率尤其是使用 warmup 和 cosine 衰减时tokens per second每秒处理的 token 数card utilization如果 profiling 方便的话记录 NPU/GPU 利用率。为什么要额外盯 grad_norm因为 loss 平滑下降不代表训练稳定。我遇到过 loss 一直很好但某个时刻 grad_norm 突增 10 倍随后两个 step 内 loss 直接 NaN 的情况。grad_norm 是比 loss 更灵敏的预警信号它暴涨时往往还没反映到 loss 上这时候你可以来得及把学习率调小或者把数据卡住排查。tokens/s 这个指标属于性能评估但它必须和 loss 放在一起看。比如模型参数量相同A 方案每秒处理 5000 tokensB 方案每秒处理 4000 tokens看起来 B 更差但 B 的 loss 在相同 token 数下下降得更快最终反而更早收敛。这就是为什么评估体系不能只盯算力利用率而要把“每秒 loss 下降幅度”作为大模型训练的核心效率指标。1.3 用校准子集摆脱评估的盲目性大模型训练时不可能每个 step 都跑完整验证集那等于把训练停下来做推理既浪费算力又破坏流水线。我的习惯是固定一个“校准子集”从验证集里按固定随机种子抽取 500 到 2000 条样本保证类别覆盖和长度分布与完整验证集基本一致然后在每隔固定 token 数时跑一次这个子集。这里的关键是“固定 token 数”而不是“固定 step 数”。因为 batch size 和并行策略调整后step 对应的实际训练数据量会变化用 step 做评估间隔在不同配置之间没法横向对比。用 token 数做标尺无论你从数据并行切到张量并行还是从单机扩到多机评估点对应的模型学习量是一致的。校准子集跑出来的指标本身不追求绝对值精确它只是为了回答两个问题第一模型在这一点上是否在继续变好第二当前 checkpoint 是否值得去做一次完整下游任务评估。真正决定发布哪个版本还是要在完整验证集或者固定评测集上做最终评估。2. 在昇思 MindSpore 里搭建评估闭环2.1 训练开始前先把评估清单写好我吃过最大的亏就是训练跑起来了才想着补评估结果前面的两三天完全没有评估数据后面出了问题也没法定位是哪一步引入的。所以现在每次启动一个大模型训练任务我会先写一份评估运行卡内容不长但必须全checkpoint 保存间隔按 token 数换算成 step 数预训练任务通常开 2 个间隔一个用于断点续训的完整保存一个用于新版本模型能力评估的轻量保存校准子集的文件路径和随机种子训练过程评估指标的采样频率例如每 10 个 step 记录一次 loss 和 grad_norm模型能力评估的触发点例如每 5B token 做一次完整验证集评估早停判定条件例如“连续 3 个评估点 loss 下降小于 0.5%”触发早停断点续训需要保存的额外状态包括 optimizer 状态、loss scale、随机数状态。这份清单会一直挂在项目文档里每次调参后都更新一次。它不一定能直接提升性能但能让你在性能优化过程中及时判断每一次改动是不是真的有效。比如我把混合精度等级从 O1 调到 O2如果只看训练吞吐量可能发现提升很明显但校准子集上的 loss 曲线斜率如果变缓了说明这个改动并不适合当前模型需要回滚。2.2 用回调函数把指标埋进训练流程昇思 MindSpore 的ModelAPI 本身带了TimeMonitor、LossMonitor等回调小规模模型直接用没问题。但大模型训练基本都要自定义训练循环和训练步函数所以我习惯把评估逻辑写成一个独立的 Callback挂到训练流程里例如这样import mindspore as ms from mindspore import nn from mindspore.train import Model, LossMonitor, TimeMonitor from mindspore.train.callback import CheckpointConfig, ModelCheckpoint ms.set_context(modems.GRAPH_MODE, device_targetAscend) # 假设 network、loss、optimizer、train_dataset、eval_dataset 已定义 model Model( network, loss_fnloss, optimizeroptimizer, metrics{loss: nn.Loss()}, ) ckpt_config CheckpointConfig( save_checkpoint_steps500, keep_checkpoint_max5, integrated_saveTrue, ) ckpt_callback ModelCheckpoint( prefixllm_model, directory./checkpoints, configckpt_config, ) model.train( epoch_size, train_dataset, callbacks[TimeMonitor(), LossMonitor(per_print_times10), ckpt_callback], )这里需要注意Model封装适合快速验证但真要在大规模并行训练里玩转很多人会拆出TrainOneStepCell自己控制梯度累积和 loss scale。这时评估回调就更有价值了因为它可以拿到训练过程中的原始输出我通常在里面做三件事把 loss 和 grad_norm 写进结构化日志每攒够 N 个 step 就调用一次校准子集评估函数满足条件时保存一份带 step 标记的临时 checkpoint。自定义 Callback 的 API 在不同 MindSpore 版本里略有差别但核心思路一样在step_end里读run_context的cb_params从中拿到当前 step、loss、网络输出等关键数据。你不需要把整套逻辑写在回调里回调只负责触发和记录真正复杂的评估函数单独放一个模块。2.3 用检查点和评估结果联合“选模型”很多人以为 checkpoint 只是用来容灾的其实它更是评估体系的产物。我跑大模型训练时会把 checkpoint 分成两类一类是“容灾型 checkpoint”保存频率高保留最近几个内容包括完整权重、optimizer 状态、loss scale、数据采样器状态目的就是断了能续。这类 checkpoint 不需要每份都去跑评估跑一次完整评估的推理成本太高了。另一类是“候选型 checkpoint”每隔固定 token 数保存一次频率低保留多份每份都在校准子集上跑出指标。训练结束后我在这些候选版本上做完整下游评估再决定最终选哪一版。这样做的好处是最终选模型不是靠“最后一次训练保存的模型”而是靠“训练线上每个评估点的数据比较”。大模型训练越到后期loss 差异越小有时候前一个评估点的验证集 loss 比后一个更低但后一个在某个下游任务上反而表现更好。没有多评估点数据的话你根本没法解释这种差异只能随便选一个。2.4 评估采样中三个容易忽略的坑第一个坑是评估子集被污染。我一直用固定随机种子抽校准子集但后来发现验证集和训练集有少量重复文本模型训练过这些文本之后校准子集上的 loss 会虚低。解决方法是做清洗时先做相似度去重把训练集和验证集的重复项彻底拆开并且每轮训练之前都重新确认一次。第二个坑是评估时的模式没切干净。MindSpore 里network.set_train(mode)会控制 dropout 和 batch norm 的行为但如果你在评估时忘了把整个 network 切回 inference 模式尤其是模型里有 dropout 层时评估指标会带随机噪声同一个 checkpoint 跑两次结果都不一样。我在回调里每次触发评估前都会强制切 mode评估完再切回来。第三个坑是 tokenizer 不一致。训练时左 padding 还是右 padding、有没有加特殊 token、截断长度是多少这些细节必须固定下来。我遇到过验证集 loss 比训练集还低的反常情况查到最后发现是验证集预处理时漏了给文本 token 加结尾标记导致有效序列长度变短loss 自然被低估。这种问题很难靠看曲线发现只能靠检查评估链路每个环节的输入输出。3. 性能优化实践先定位瓶颈再开刀3.1 拿到一份可复现的性能基线性能优化最忌讳“头痛医头”看到一个算子慢就去优化那个算子看到通信占比高就去调并行策略实际上问题根源往往在数据加载或者 host 端。我每次做优化前都会先用 profiler 拿到一份完整基线记录 10 到 20 个 step 的数据然后把这几个指标记下来每个 step 总耗时计算时间、通信时间、空闲时间分别占比模型参数和优化器参数占多少显存数据加载线程的耗时分布算子在 NPU 上的实际执行时间。在昇思 MindSpore 上可以使用 profiler 工具训练脚本里加一段简单逻辑from mindspore.profiler import Profiler profiler Profiler(output_path./profiler_output, profile_memoryTrue) # 跑到 20 个 step 左右就可以结束 # 训练循环结束后调用 profiler.analyse()不同版本的具体用法可能稍有出入但核心原则是一样的profiling 一定要在正式训练数据上跑不能拿一个小 toy 数据替代。大模型训练时显存压力和通信压力都和数据规模强相关玩具数据上看到的瓶颈分布参考意义不大。拿到基线后我会先看“空闲时间”。如果一个 step 里 GPU/NPU 有 30% 时间在空等说明数据加载或者 host 端同步拖了后腿这时候去优化算子毫无意义。只有当计算占比本身就很高时才算到了真正的算子优化环节。3.2 混合精度不是把等级拉满就万事大吉昇思 MindSpore 的 AMP 分为 O1、O2、O3 等不同等级等级越高能转成低精度的算子越多训练速度通常越快但精度风险也越高。我做十亿级以上模型时默认从 O1 或 O2 开始很少一上来直接拉最高等级。O1 相当于把常见算子比如 Conv、MatMul 自动切到 FP16其他算子保持 FP32O2 覆盖更广还会调整 loss scale 和梯度更新的行为。两者在训练吞吐量上的差距要看模型结构里哪些算子是热点如果热点是 embedding 或者 norm 这类对精度敏感的算子O3 带来的性能提升可能会被反复训练发散给抵消掉。混合精度训练一定要关注 loss scale特别是训练的早期阶段。loss scale 太小会导致梯度下溢loss 迟迟不变loss scale 太大又容易让梯度溢出出现 Inf 或 NaN。MindSpore 的动态 loss scale 机制会自动调整阈值但它在遇到异常梯度时可能会频繁降阈值导致训练速度打折扣。我的建议是定期把 loss scale 当前值打进评估日志如果发现它在某个阶段反复震荡优先去查梯度里有没有异常大的值而不是直接调混合精度等级。3.3 并行策略与通信开销的取舍模型到一定规模后单卡放不下并行策略就成了必选项。昇思 MindSpore 支持数据并行、张量并行、流水线并行也可以组合使用。我自己的经验是并行策略的选择要把通信开销放在第一位考虑。数据并行最简单每个设备上都有一份完整模型只做梯度同步但模型大到单卡放不下时就用不了。张量并行能把单个算子切到多卡上解决单卡放不下的问题但它通信非常密集每过一个关键算子就要做一次 allreduce张量并行度越高通信占比越高。流水线并行按层切分不同阶段跑不同层通信压力比张量并行低但它会产生流水线气泡也就是一部分设备在等前一个阶段的数据。大模型训练里我一般会把流水线并行和张量并行搭配使用比如先切 4 路张量并行解决单层过大问题再切 8 段流水线压显存最后用数据并行扩展集群规模。通信优化的技巧细节层面无外乎这几点把多个小梯度张量合并成一个大张量再执行 allreduce减少通信次数让通信算子和计算算子尽量重叠MindSpore 在自动并行时会有调度优化但手动写训练逻辑时也要避免“先算完、再通信、再算下一层”这种串行方式梯度累积时注意累积步数别让通信频率和累积次数互相打架集群规模变大后跨节点通信和节点内通信差异很大尽量让高通信量的并行维度落在节点内部。3.4 显存优化重算、梯度累积与状态卸载大模型训练最常见的硬性问题是 OOM。昇思 MindSpore 训练时显存主要被三块占用模型参数、优化器状态、激活值。参数和优化器状态几乎是刚性的能挤的只有激活值这一块。激活重算activation recompute是最常用的手段。训练时前向传播把每层激活值存下来反向传播要用时直接读取速度最快但显存消耗大。重算策略则是反向传播时重新算一遍前向省掉保存激活值的显存代价是增加计算量。我在十亿级模型上打开重算后激活显存可以减少一半以上训练总时间增加 10% 到 20%但换来的是能塞进原来的卡里而不需要扩容整体收益很高。另一个容易忽略显存大头是优化器状态。像 AdamW 这类优化器需要保存一阶动量、二阶动量和 fp32 的 master weight参数量的好几倍显存都被优化器吃掉了。如果要继续扩大模型规模可以考虑把优化器状态卸载到 CPU/宿主机内存或者使用低比特优化器状态压缩但这需要和训练框架的高阶 API 配合不是简单改一行配置的事。梯度累积也常被用来缓解显存压力它把一个大 batch 拆成多个 micro batch每次只在最小 batch 上做前向和反向攒够一定次数后再更新参数。代价是训练时间可能变长因为前向反向次数没有减少只是降低了每一步对显存的峰值需求。它和评估体系的联系在于评估触发点要按累积后的真实 token 数来算否则你看到的“每 step 评估一次”实际上跨过了不同的等价训练数据量。3.5 数据管线优化往往被低估我见过不少团队把大模型训练性能不佳的原因归结到算力上跑一波 profiler 才发现数据加载耗时占了四成。昇思 MindSpore 的数据处理流程如果用 Python 逐条做预处理每条样本都要过 Python 解释器在大 batch 下很容易顶不住。第一个建议是数据预处理尽量离线化把文本转换成 token id 之后存成索引格式训练时直接加载。昇思 MindSpore 原生支持 MindRecord 数据格式训练前做一次数据转换能明显减少训练时的 CPU 压力。第二个建议是为map和batch操作设置足够的并行度但也要观察内存占用并行 worker 开太多反而会因为内存换页拖慢速度。数据管线和评估体系还有一个深层联动当你优化了数据读取速度后训练吞吐量可能直接翻倍但评估表的评估间隔是按 token 数定的此时训练到达评估点的速度也变快了。如果评估链路本身很慢比如每次从 checkpoint 重建模型这个过程要几分钟那你的训练流程会频繁停下来等评估整体吞吐量反而下降。所以性能优化不单是优化训练主链路也要优化评估子系统的推理速度。4. 性能优化与评估体系的联调排错4.1 loss 不降反升先查评估数据有没有穿透有些同学一看到 loss 上涨就开始调学习率、调 warmup我个人的经验是第一步先把“评估数据本身是不是干净”这件事确认掉。训练集和验证集如果有数据串样或者数据采样器没有正确 shuffle都会让评估结果异常。排查顺序通常是这样的先用校准子集在固定 checkpoint 上跑一次看是否仍然异常如果校准子集表现正常说明问题出在完整验证集的采样逻辑如果校准子集也异常继续往模型训练环节查比如学习率、梯度裁剪、混合精度配置。把评估体系和性能优化放在同一个排错流程里能少走很多弯路。遇到 loss 在前几百步下降后期突然反弹我还会重点检查数据桶的构造。很多分词器按序列长度分桶如果桶边界设置不合理模型在某个桶上见到的样本长度分布发生变化loss 就会跳变。评估体系如果只记录平均 loss会掩盖这种分布变化所以最好把样本长度分桶后的 loss 也算出来至少每个评估周期看一次分桶曲线。4.2 吞吐量上不去先看 profiler 的“等待时间”如果一个 step 的耗时比理论值高很多先别急着优化算子。我拿到 profiler 报告后第一眼看的是设备空闲率也就是一个 step 里数据准备好之前设备有多少时间在干等。常见瓶颈有三类第一类是数据加载慢。现象是设备利用率低、每个 step 的边界有明显的长尾等待。解法是看数据管线是否有 Python 层瓶颈增加并行 worker、提前转成 MindRecord、把 tokenizer 放到离线步骤里。第二类是通信时间占比高。现象是设备利用率不低但算子时间只占一部分剩下都是通信等待。解法是调整并行策略、合并通信张量、让通信和计算重叠。第三类是调度开销大。模型切分粒度太细、算子太多但每个算子耗时很短时框架的调度开销会凸现出来。解法是打开图模式、尽量使用融合算子、减少在训练热路径上写 Python 控制流。第三类问题在昇思 MindSpore 上尤其值得关注。GRAPH_MODE 静态图模式会帮你在框架层做更多算子融合PyNative 模式调试方便但性能上限低。大模型正式训练我基本只在静态图模式下跑调试阶段才切到 PyNative。4.3 训练指标好看但验证集拉垮优先查 mode 和 tokenizer“训练 loss 一直降验证集指标不动甚至变差”是另一个高频问题。如果模型能力评估和训练过程评估之间存在系统性差异我第一个怀疑的是模型在验证集上运行时没有进入正确的推理状态。MindSpore 中网络对象有 train/eval 两种模式set_train(False)会关闭 dropout 等随机层但同时要注意 loss 的构造方式。训练时通常用 label smoothing验证时如果还用同样配置会把 loss 打高表现成验证集指标差。这种情况不是模型问题是评估口径没对齐。另一个怀疑点是 tokenizer 把验证集文本截断得太短。大模型对长文本依赖强如果训练时用 4096 token 长度验证时截成 512指标自然上不去。我会把验证集按长度分布拆成几段来评估既看总体指标也看长文本子集的表现这样才能区分是模型能力不足还是评估配置失误。4.4 断点续训后指标漂移问题多半在状态没恢复完整大模型训练总要面对断点续训。如果你发现恢复训练后 loss 曲线出现一段明显的跳变甚至一直回不到原来的下降轨道大概率不是随机数的问题而是 checkpoint 里状态存得不完整。完整保存需要包括网络权重、optimizer 各状态参数、loss scale、学习率调度器的当前值、数据采样器的当前位置。昇思 MindSpore 的ModelCheckpoint默认会保存网络和优化器参数但如果你想恢复 loss scale 和随机数状态需要自己把这些额外信息一起管理起来。我一般会在 checkpoint 里额外存一个 JSON 文件内容就是评估运行卡里的当前标尺已经训练的 token 数、当前学习率、当前 loss scale、下一次评估触发点。恢复训练时先读这个 JSON再决定是从哪个 step 继续评估。不要小看这一步我因为这个原因重训过一个模型浪费了不少算力。5. 最后一次训完总结我的习惯与复盘清单我现在每开一个昇思 MindSpore 大模型训练任务无论模型多大都会在项目目录下放三样东西一份评估运行卡、一份性能基线报告、一份改动日志。评估运行卡记录评估口径和触发点性能基线报告记录开始时的算子耗时和吞吐量改动日志记录每次优化改了什么配置、效果如何。别觉得这很麻烦大模型训练周期长改动点密密麻麻没有这套记录复盘时只能靠猜测。举一个我自己的真实例子有次为了提升吞吐量我把某个 transformer 算子的切分方式改了当前评估点上的校准子集 loss 微微变差但幅度没超过阈值我就继续训练了。等到整个训练结束完整下游评估发现多个任务指标明显下降再回头看改动日志才定位到就是那一次算子切分改动引起的。从那以后我把“每个改动都必须同时记录性能指标和评估指标变化”写进了团队规范。最后再分享一个小学问评估体系里的所有阈值不要拍脑袋定。先跑 200 个 step收集一波正常的 loss 波动范围然后用这个范围乘一个系数去定早停线。比如正常情况下校准子集 loss 相邻两个评估点的波动在 0.02 以内那你设置“连续三个评估点没有下降超过 0.05%”作为早停条件就不会被正常波动误伤。大模型训练的优化是一条长链路评估体系是这条链路上的仪表盘。没有仪表盘你踩油门踩到哪了、发动机有没有过热全都看不见。先把仪表盘装好再谈性能优化这是我个人认为最值得分享的一条经验。
返回列表