ARTICLE DETAIL

资讯详情

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

MindSpore Transformers高效训练LLM:并行策略与混合精度调优实战

MindSpore Transformers高效训练LLM:并行策略与混合精度调优实战 1. 为什么在LLM预训练这条路上我最终选了MindSpore Transformers先说背景。我在上一家公司主要用PyTorch生态做BERT、GPT系列模型的微调和预训练跑的多是A100集群transformers配合deepspeed是一套非常顺手的组合。半年前调到新团队算力环境变成了昇腾NPU集群第一反应是完蛋PyTorch在NPU上不好搞但实际调研后发现现在的MindSpore框架配合昇思社区的mindspore_transformers库已经把主流LLM架构的预训练流程打包得比较完整。我花了两周把一套1.5B参数的模型预训练从PyTorch迁移到MindSpore上跑通之后整体吞吐反而比之前同卡数的GPU方案还高一截。如果你也在纠结要不要用MindSpore做LLM预训练我可以直接给结论如果你的目标是在昇腾NPU集群上以native方式跑LLMMindSpore Transformers基本是绕不开的最优解。它解决了三个关键问题一是算子层面对昇腾NPU做了深度适配不需要手动改算子二是分布式并行策略在框架层内置不用自己拼装DDPZeRO那一套复杂组合三是API风格跟Hugging Face Transformers高度对齐从PyTorch迁移过来的学习曲线很平滑——大部分AutoModel、AutoTokenizer、AutoConfig的调用习惯可以直接沿用。当然这里的高效训练不是指改几个参数就能白嫖性能。它涉及并行策略的选择、内存的精细管理、混合精度的调优、通信瓶颈的排查等多个层面。这些年有个很深的体会预训练LLM最大的成本不是代码本身而是试错成本。一次不合理的并行配置可能导致整个集群GPU/NPU利用率只有20%持续两三天一个不起眼的版本错配问题可能让几百张卡白等半天。所以这篇文章我想把迁移过程中沉淀下来的配置经验、踩坑排查链路、实测优化方法完整梳理一遍给正打算在MindSpore上跑LLM预训练的同学一份可以直接实操的参考。这篇内容适合谁看主要三类一是要把现有PyTorch LLM代码迁移到昇腾平台的人二是刚接触MindSpore分布式训练、被并行策略和显存优化搞得头大的新手三是已经在用但吞吐一直提不上去、想系统排查性能瓶颈的团队。你不需要对MindSpore很熟但最好有基本的Transformer模型训练认知知道attention、FFN、loss这些概念大概是怎么回事。2. 环境搭建与模型配置这里的水比想象中深2.1 版本匹配是第一个劝退点MindSpore的版本兼容性比PyTorch要敏感得多特别是结合mindspore_transformers时不要只盯着最新版本装而是要看配套矩阵。我踩过的坑是装了MindSpore 2.3.0配了一个比较新的mindspore-transformers结果跑llama模型时不断报算子不支持的错最后发现是MindSpore侧某些融合算子在该版本还没完善退回2.2.x反而一切正常。一个比较稳的组合参考组件推荐版本备注操作系统openEuler 22.03 / Ubuntu 20.04生产环境openEuler适配更好Python3.8 ~ 3.103.10以上部分算子包可能没预编译MindSpore2.2.x 或 2.3.x看昇腾驱动配套以官方配套表为准mindspore-transformers0.2.0 ~ 0.3.x注意和MindSpore主版本对应CANN对应MindSpore版本文档指定的版本版本不对最容易出算子编译失败昇腾驱动与CANN配套驱动和固件版本不匹配时训练中期会莫名卡死这里多说一句CANN版本比MindSpore版本更重要。我曾经在同一套MindSpore版本下换了不同CANN版本性能差距达到30%以上有些融合算子只有在较新CANN里才会被框架调用。安装完成后第一件事不是训练而是跑一个最小验证脚本import mindspore from mindspore import Tensor import numpy as np x Tensor(np.ones((2, 3), dtypenp.float32)) y x.sum() print(y) print(MindSpore version:, mindspore.__version__)如果这个能通过再看NPU是否被正确识别npu-smi info正常的话能看到卡的数量、显存、温度等信息。这一步很多人会忽略直接开跑结果训练到一半才发现npu-smi里显示0号卡的利用率是0%白费几个小时。2.2 那个报错aimv2 is already used by a transformers config, pick another name.网络热搜词里有个很典型的报错——aimv2 is already used by a transformers config, pick another name.。这个我在迁移时也遇到过先说结论它不是MindSpore特有的问题而是一个配置文件注册名冲突问题。具体来说transformers库在加载模型时会把模型名和配置类映射到内部注册表。当你实例化两个AutoConfig或多次加载同一个模型、而它们使用了相同的model_type标识时就可能触发这个提示。常见的触发场景有两个在同一个进程里重复调用AutoModel.from_pretrained加载同一配置且自定义配置类里的model_type字段重复加载aimv2这类新模型的预训练权重时本地缓存或代码路径中已经存在重名注册。排查思路按三步走第一步看堆栈信息里是哪一行触发的告警是from_pretrained还是AutoConfig.from_pretrained第二步检查模型目录下的config.json确认model_type字段是否与其它模型重名第三步如果是自定义模型把model_type改成唯一标识并清掉缓存目录下的~/.cache/huggingface或MindSpore相关的模型缓存。这个报错的本质是全局命名空间污染解决起来不复杂但如果是新模型比如aimv2、spatial LLM这类刚放出的结构还要额外注意配置类和模型类定义是否都注册到了transformers的__init__.py或对应模块里。2.3 数据预处理tokenizer是第一个性能瓶颈很多人优化半天模型并行结果发现数据加载慢得要死GPU/NPU的利用率一直跳不稳定。预训练场景下tokenizer的处理速度往往是隐藏瓶颈。在MindSpore Transformers里数据管线通常配合mindspore.dataset使用。不要把tokenize逻辑直接写在Python侧的map函数里transformers的tokenizer是纯Python实现一个字一个字处理速度感人。正确做法是用mindspore.dataset的.map配合num_parallel_workers并行并且尽量把tokenize后的结果固化成mindrecord格式多次重复读取时省去再tokenize的开销。我当时对一份约200GB的文本语料做tokenize先直接跑Python脚本每秒只能处理几千条样本预计要跑一周改成多进程并行分批写mindrecord后压缩到十几个小时。具体要点按批次读原始文本num_parallel_workers设成物理核数的一半到2/3tokenize时用batch_encode而不是单条encode可以启用padding和truncation的批量优化输出字段统一为input_ids、attention_mask、labels并固定max_seq_length每个mindrecord文件的大小控制在512MB左右避免单文件过大导致读取时内存抖动。2.4 模型加载前的几个必须对齐的配置在from_pretrained加载中文预训练模型比如roberta中文版或从零初始化模型前有几个config字段必须和你的tokenizer、数据保持一致否则会在训练中期暴雷vocab_size必须和tokenizer的词表大小严格一致不一致时先扩充embeddingmax_position_embeddings决定位置编码的上限seq_len不能超过这个值bos_token_id、eos_token_id、pad_token_id尤其pad_token_id很多loss计算在padding位置会产生梯度误差tie_word_embeddings是否绑定embedding和lm_head权重绑定可以省一大块参数但显存下降的同时表达能力也有微弱变化。这些字段在加载配置文件后我的习惯是写一个断言脚本from mindspore_transformers import AutoConfig, AutoTokenizer config AutoConfig.from_pretrained(./model_config) tokenizer AutoTokenizer.from_pretrained(./model_config) assert config.vocab_size len(tokenizer), fvocab mismatch: {config.vocab_size} vs {len(tokenizer)} print(feos{config.eos_token_id}, bos{config.bos_token_id}, pad{config.pad_token_id})这些看起来都是小事但真正跑大规模训练时一个小数点级别的错误可能毁掉几百卡几天的算力前置检查永远值得。3. 高效训练的核心并行策略怎么选3.1 先想清楚并行选择本质是对三块资源的取舍预训练LLM时GPU/NPU集群上有三块资源是相互制约的显存/内存、计算量、通信量。任何并行策略本质上都是在三者之间找平衡。没有一种策略是绝对最优的最优方案取决于你的模型多大、卡多少、网络拓扑多快。表格式地看你有哪些选择并行维度切分对象解决的问题主要代价数据并行DP训练数据按卡切分扩大batch吞吐AllReduce梯度同步通信张量并行TP单层内的权重/中间张量单层放不进单卡显存每层前后各一次AllReduce流水线并行PP模型按层切段整模型放不进单卡显存流水线气泡空闲序列并行SP序列维度切开attention计算超长序列显存爆炸额外通信/复杂度混合并行上述方式叠加大模型大集群综合提效调度复杂度指数级上升3.2 数据并行先跑通再优化如果你是第一次在MindSpore上跑预训练建议从纯数据并行开始。MindSpore的DATA_PARALLEL模式会把模型复制到每张卡各自读不同batch的数据每个step做一次梯度AllReduce。from mindspore import context context.set_context(device_targetAscend) context.reset_auto_parallel_context() context.set_auto_parallel_context( parallel_modecontext.ParallelMode.DATA_PARALLEL, gradients_meanTrue )数据并行的通信成本在卡数少比如8卡、16卡时很低几乎看不出性能瓶颈。但一旦卡数超过32张AllReduce同步的代价会指数级上升利用率会明显下滑。此时就该考虑混合并行。关键心得数据并行虽然简单但它是衡量其它并行策略的基线。先把DP跑通测出基线吞吐tokens/s再去切换其它策略否则你很难判断某个并行配置是否真的带来收益。3.3 张量并行把单个Transformer层拆开当单卡显存放不下一个50B参数的Transformer层时就需要张量并行。它的核心思路是把Self-Attention里的Q、K、V线性层的权重、以及FFN的两个线性层权重按列或按行切到多张卡上每张卡算一部分最后合并。MindSpore里开启张量并行需要进入SEMI_AUTO_PARALLEL模式并配合set_strategy对每个算子指定切分方式。以LLaMA的attention为例核心逻辑是import mindspore as ms from mindspore import nn from mindspore.common.initializer import initializer class MultiHeadAttention(nn.Cell): def __init__(self, hidden_size, num_heads, tp_size8): super().__init__() # 将hidden_size按tp_size列切分每个rank计算部分head self.qkv nn.Dense( hidden_size, hidden_size * 3 // tp_size, has_biasFalse ).shard(((1, 1), (1, tp_size))) # 输入行不切权重列切 self.o_proj nn.Dense( hidden_size // tp_size, hidden_size, has_biasFalse ).shard(((1, tp_size), (1, 1))).shard方法里传的就是矩阵乘法切分维度第一对元组是输入张量的切分第二对是权重的切分。(1, 1)表示不切(1, tp_size)表示第2维切到tp_size份。张量并行的代价是每个Transformer层的前向传播里包含多次通信QKV投影后需要AllReduce聚合Attention输出投影前也需要AllReduce。这导致TP规模越大通信占比越高。一般来说单机内用TP比较划算跨机的TP要谨慎因为机间带宽远低于机内带宽。举个例子8卡单机做TP8可能通信可以用NVLink或HCCS高速互联但如果TP16跨两台机器通信延迟会显著拉高性能可能还不如PP切分。3.4 流水线并行把模型切成几段流水线并行的思路更直观把Transformer按层切成多段每张卡负责一段。比如24层的LLaMAPP4时每张卡跑6层。这样可以显著降低单卡显存压力。但流水线有个经典问题气泡bubble。如果batch里只有一条数据在流水线上走任何时刻只有一张卡在计算其它卡都在等待——利用率只有1/PP。解决方法是把大的batch拆成多个micro-batch连续喂进流水线# 伪配置示意 micro_batch_size 4 accumulation_steps 16 global_batch_size micro_batch_size * accumulation_steps * device_nummindspore_transformers里通过pipeline_stages等参数配置分段方式。常见的一个直觉是纯PP的并行效率通常不如纯TP因为气泡损耗很难完全消除。业界经验是PP通常配合DP和TP一起用很少单独使用。3.5 实际工程里的混合策略一个大致的选型参考从1B到100B参数范围内我的经验选型大致如下1B ~ 7B、单机8卡/16卡纯DP配合ZeRO-2或MindSpore优化器等值状态切分FSDP类似机制7B ~ 30B、单机8卡DP(2) TP(4)或DP(4) TP(2)优先保证TP在机内30B ~ 70B、多机DP(节点数) TP(机内) PP(跨机)典型如TP4, PP4, DP卡数/(4*4)70B以上需要引入序列并行和专家并行MoE模型这里不展开。MindSpore里配置混合并行通常要显式指定每层算子的shard策略。这个环节是最花时间的。我的方法是先写一个脚本输出模型的层结构然后按embedding、attention、ffn、lm_head四类统一设置shard规则不要逐算子手调。4. 显存与计算效率混合精度、激活重计算与微批次设计4.1 混合精度bf16是LLM预训练的首选显存占用大约是参数量的多少倍LLM从业者都心里有数一个10B模型光参数就要40GBfp32用fp16存权重降到20GB叠加梯度、优化器状态单纯靠显存硬扛是永远扛不住的。MindSpore对混合精度的支持分了三个层次fp16模式把大部分算子转成fp16计算会损失精度小模型低精度场景可以用bf16模式BrainFloat16保留8位指数位动态范围跟fp32一样大预训练LLM首选几乎不担心溢出自定义loss_scale动态缩放fp16下配合动态loss scale来防梯度下溢。实际代码里通常这样开import mindspore as ms from mindspore import amp # bf16或fp16 model ... model.to_float(ms.bfloat16) # 如果使用fp16需要动态loss scale loss_scaler amp.DynamicLossScaler(scale_value2**16, scale_factor2, scale_window2000)bf16最直观的好处是不用像fp16那样精调loss_scale。我第一次用fp16训练时loss经常突然变成NaN排查了半天最后定位到是梯度下溢——某些小梯度在fp16里被舍入成0导致loss弥散。换到bf16后这个问题基本消失。4.2 激活重计算用算力换显存预训练时最大的显存杀手不是权重而是中间激活值。一个7B模型训练时的激活显存在batch size稍微大一点时可能超过50GB。激活重计算Activation Checkpointing / Gradient Checkpointing的思路是前向传播时不保存所有层的激活值只保存每若干层的边界激活反向传播时再重新计算中间层激活。MindSpore里可以通过mindspore.nn.Cell的重计算API控制class TransformerBlock(nn.Cell): def __init__(self, layers): super().__init__() self.layers layers def construct(self, x): for layer in self.layers: x layer(x) return x # 对包含较多层的Cell启用重计算 cell TransformerBlock(layers) cell.recompute()recompute()默认对该Cell内部所有算子生效。还可以cell.recompute(MPI_OP_LIST...)之类的更细粒控制不过我一般直接用默认。关于重计算的权衡我个人的经验是不是所有层都需要重计算。常见的做法是只对Block内部计算量大的attention和ffn层开重计算而embedding、lm_head这类算子本身显存占用不大、重算成本却不低不开。你可以按重计算让显存下降的幅度和重计算让训练变慢的百分比两个指标来评估。实测中LLaMA 7B开全量重计算显存大约省了35%但训练时间会多出约15%~20%。4.3 梯度累积与微批次预训练时全局batch_size通常要足够大几千到几万条样本但显存限制单卡只能放很小的micro_batch。梯度累积Gradient Accumulation是目前唯一能兼顾两者的方案多次前向反向累加梯度攒够N步后再做一次优化器更新。grad_accum_steps 8 global_batch_size micro_batch_size * grad_accum_steps * device_num这里有一个很多人忽略的细节梯度累积时loss的normalize方式要正确。如果你每步都对loss除以grad_accum_steps那梯度就是平均梯度如果直接累加梯度就变成了多步的梯度之和。两种方式对应不同的学习率修正策略。MindSpore Transformers里默认方式通常是除以累加步数不要在外部再手动除一次否则学习率会偏小。4.4 sequence length、batch size与吞吐的微妙关系LLM预训练里最容易被低估的调优手段是sequence length。同样是处理一批文本seq_len2048时的计算密集度比seq_len512高很多因为attention的复杂度随序列长度平方增长。但显存占用也会显著上涨。我实测过在不同seq_len下的吞吐变化seq_len单卡micro_batch显存占用tokens/s8卡备注5123232GB约110kattention开销占比低10241648GB约140k吞吐提升显存翻倍2048876GB约170kattention开始吃计算4096490GB约120k显存压缩了batch吞吐下降注意这个表不是普适数据它只说明一个规律seq_len和吞吐不是线性关系在显存允许的范围内把seq_len适当调大通常能提升总吞吐但一旦显存被逼到要缩小micro_batch吞吐就会因为batch过小而大幅下滑。在8卡机上2048是个比较甜的区间。5. 实测调优从单机多卡到集群性能瓶颈的定位方法5.1 训练不收敛先别怀疑框架换了MindSpore之后训练发散第一反应往往是框架有问题。我在迁移时也这么想踩了几次坑才发现90%的发散原因是学习率策略、warmup、数据顺序、随机种子这些跟框架无关的因素。MindSpore Transformer里常见的不稳定因素学习率没有设置warmupLLM预训练初期模型还在混沌状态直接上大学习率会导致毫无规律的loss爆炸。我惯用的方案是前1%~2%步线性warmup之后按cosine或WSDwarmup-stable-decay衰减。梯度裁剪缺失在MindSpore里通过nn.ClipByGlobalNorm或优化器里的grad_clip_norm配置。对于1B模型max_grad_norm1.0比较稳参数越大可以适当放宽到2.0。随机种子没有固定分布式训练时数据加载、dropout、权重初始化都需要在每张卡上保持一致。尤其要检查shuffle的seed是否按global rank派生否则不同节点数据重合或缺失。数据集里混入了过长文本超过max_seq_length的样本如果没有被正确截断会在计算attention mask时出现错位导致loss周期性跳变。5.2 通信瓶颈怎么定位不要凭感觉要用数据当集群利用率低于预期时我的排查链路是这样第一步看card利用率。npu-smi info能看到每个NPU的利用率。如果利用率曲线是锯齿状说明在等梯度同步——大概率是通信瓶颈。第二步看网络吞吐。用hccn_tool昇腾或ibstatusInfiniBand确认实际带宽。第三步用MindSpore自带的Profiler做算子耗时分析from mindspore.profiler import Profiler profiler Profiler(output_path./profiler_result) # 正常训练几步前 # profiler.start() # 训练代码 # profiler.stop()生成的timeline里能清楚看到Communication算子如AllReduce、Send、Receive的耗时占比。如果这个占比超过20%就应该优化并行策略。第四步替换数据准备逻辑。如果你的数据管线用了过多的map操作或Python侧循环NPU利用率会周期性掉到个位数。解决方法是提前用mindrecord固化数据并把dataset.batch里的drop_remainderTrue打开保证每个step的数据量一致不让最后一个不完整batch拖慢整体节奏。5.3 checkpoint与断点续训工程侧的最后一根稻草预训练任务动辄几十天中途一定会有机器故障、电力波动、人为误操作。没有可靠的checkpoint机制一切性能调优都白搭。MindSpore里的checkpoint保存有几个注意点别只存模型权重一定要存优化器状态、step计数、学习率调度器的当前值、RNGState。只存权重的话断点续训时优化器状态丢失学习率从头开始几乎等于重训。分布式训练时每张卡保存自己的卡号分片不要所有卡重复保存全量权重否则几百卡同时写几份大文件磁盘会在几秒内被打满。定期验证checkpoint的可恢复性。我吃过一次大亏训练到第三天任务故障恢复后加载checkpoint直接报显存不够——原因是保存/恢复时用了不同的并行策略模型结构变了但保存的键没变。从此之后每次修改并行策略都会先做一次1~2步的保存-加载-继续跑冒烟测试。一个比较稳妥的保存策略是# 每X步保存一次保存到专用文件夹 if step % save_interval 0 and rank 0: ms.save_checkpoint(model, fckpt/step_{step}.ckpt) # 如果有训练状态对象同步保存 # 恢复时 last_ckpt get_latest_ckpt() param_dict ms.load_checkpoint(last_ckpt) ms.load_param_into_net(model, param_dict)同时建议把step和optimizer的学习率状态当作独立的小文件保存。这样万一优化器状态加载失败版本升级导致格式不兼容至少模型权重还能恢复。5.4 一套实测的调优过程复盘拿一个1.5B参数、8卡昇腾场景举例我完整走了一遍调优初始状态直接用默认配置吞吐大约65k tokens/s利用率65%。排查后发现三个问题数据管线的num_parallel_workers设成4过小数据供给跟不上计算没有开激活重计算显存紧张导致micro_batch只能放4单step太大模型所有层都开了fp16部分算子精度不足导致loss噪声偏大。调整方案num_parallel_workers提到16数据管线改用mindrecord并把prefetch_size调大只对attention和ffn层开recompute把micro_batch从4提高到12切换到bf16混合精度开启梯度累积global_batch_size保持2048不变。最终吞吐从65k提升到约140k利用率从65%涨到88%。整个过程没有改模型结构纯调配置。6. 一些实话与经验总结最后说说在MindSpore上做LLM预训练的真实感受吧。如果你问我现在是不是迁移到MindSpore最好的时机我的回答是取决于你的部署平台。如果目标平台就是昇腾NPU那越早切越好因为MindSpore的适配优势在原生环境下才明显不要想着用PyTorch代码去蹭NPU再多的适配框架也不如直接用原生的算子库。如果目标平台是GPUPyTorch生态依然是更成熟的选择。还有个小技巧想分享在做大模型预训练时无论用哪个框架都先在最小规模上完整验证训练流程再上规模。所谓最小规模不光是模型变小还要让并行策略保持一致的逻辑。很多人习惯单卡跑通了就去跑几百卡结果在并行策略切换时爆出一堆问题排查效率极低。正确的做法是1卡跑通 - 2卡验证DP - 4卡验证TP - 8卡验证混合并行 - 再上集群。每个阶段都记录训练曲线和性能指标任何一步指标异常都能快速定位到是哪个环节出了问题。另外不要说反正框架都帮我算好了我不用理解它的原理。MindSpore Transformers确实封装了很多细节但封装越多你越需要知道背后发生了什么。至少要知道数据在卡间是怎么流动的、梯度在哪里做了同步、checkpoint存了哪些内容。有了这层理解遇到任何诡异问题时你都能有个基本的排查方向而不是一脚踩进黑盒里出不来。如果你正在用MindSpore做预训练或者准备迁移欢迎带着具体问题来交流。跑模型这件事永远是实际数据比感觉可靠多留一份profiler数据少走一段弯路。
返回列表