
去年我们把一个在 Hugging Face 上已经跑到 SFT 阶段的 LLaMA 规模模型迁到 MindSpore Transformers 上训练原本以为只是换框架导入语句的事结果第一关就卡在了一份transformer_config.json上。同一个文件在 HF 生态里是模型结构说明书在 MindSpore 的大模型训练套件下文按 MindFormers 这套体系讲里只是配置体系的冰山一角字段名、默认值、读取逻辑全都不一样。这篇文章就是整理这次迁移的所见所得重点拆两件事transformer_config里的关键字段到了 MindSpore 侧分别怎么解析以及从 HF 到 MindSpore 的完整迁移方案长什么样顺带把迁移过程中遇到的高频报错——包括那个让人一头雾水的aimv2 is already used by a transformers config, pick another name.——的排查过程记录下来。适合正在做类似迁移的算法工程师和训练工程同学不管你是被迫换国产算力还是主动想试 MindSpore 的整图编译优化这份配置解析和迁移路径都值得先看一眼。1. 迁移前先想清楚两套生态里 transformer_config 的定位完全不同1.1 HF 里的配置是一份模型结构说明书在 Hugging Face Transformers 里transformer_config.json本质上是和一个权重目录绑定的模型结构参数集。AutoConfig.from_pretrained()读取它AutoModel.from_pretrained()再根据其中的model_type和architectures字段决定实例化哪个模型类。它的设计哲学是配置即结构——一个 json 文件加一份权重就能完整还原一个模型的网络结构。这个文件很小但是作用非常大因为它把模型内部所有超参数比如层数、隐藏维度、注意力头数、激活函数类型、RoPE 相关的长度参数全部收敛到了一个字典里。任何下游工具拿到这个字典不需要看训练代码就能重建网络。但也正因为如此这个字典里存的是模型结构视角的字段它基本不关心你的数据管线怎么搭、优化器用什么、学习率怎么调度、并行策略怎么切。那些东西在 HF 生态里分散到TrainingArguments、TrainerCallback、数据 collator 里和transformer_config.json是两套完全独立的体系。这个习惯到了 MindSpore 侧需要彻底改过来。1.2 MindSpore Transformers 里的配置是整条训练管线的施工图MindSpore Transformers 这套大模型套件我这边实际用的是 MindFormers 演进过来的那套机制对配置的理解和 HF 不一样。它更倾向于把模型结构、训练超参、数据配置、并行策略、回调逻辑全部放到一套 YAML 或 Python dict 形式的配置里模型结构参数只是其中一个model子树。也就是说transformer_config.json里那些字段在 MindSpore 侧往往只是某个模型Config类的构造参数真正的总配置入口是一个 YAML 文件里面除了model之外还有data、optimizer、parallel、runner这些章节。这个差异带来的第一个坑就是你不能把 HF 的transformer_config.json直接丢给 MindSpore 去加载哪怕字段长得再像也不行。MindSpore 侧需要一个LLaMAConfig或者你自定义的 Config 类来承接结构参数然后把训练参数放到别的配置块里。换句话说迁移的第一步不是改字段名而是先理解这些字段分别会被谁消费、在哪一层被消费。为了直观对比我把两边配置体系的差异整理成了一个表对比维度Hugging Face TransformersMindSpore TransformersMindFormers 体系配置载体transformer_config.jsonYAML 模型 Config 类配置内容仅模型结构模型结构 数据 优化器 并行 回调加载方式AutoConfig.from_pretrained注册表 from_pretrained / YAML 装配权重格式.bin / .safetensors.ckpt执行模式动态图为主PyNative 与 GRAPH 模式并存训练以 GRAPH 为主1.3 什么情况下才值得做这次迁移先说结论如果只是在一个单卡、FP16、几亿参数规模的任务上做快速验证我其实不建议迁移HF 生态的开发效率高太多了文档、社区、现成组件都更成熟。真正值得迁移的场景我这次总结下来是这三类一是目标硬件是 NPU 这类国产加速卡。PyTorch 官方对这类硬件的支持链条太长很多算子要绕而 MindSpore 原生就为这个硬件做了适配图算融合和内存管理都更贴近硬件特性。二是你的训练规模大到需要框架级优化。HF 跑大模型时很多优化靠的是外部库叠加MindSpore 则把整图编译、算子融合、并行切分下沉到了框架内部你在 YAML 里声明并行策略框架帮你落地。三是团队有明确的国产化栈要求这属于非技术因素但实践中占比很大。这里要特别提醒一句迁移是有成本的别把 HF 那条训练链路直接扔掉。最好的做法是两条代码路径并存一段时间用同一份数据和超参做交叉验证确认 loss 曲线和下游指标对齐后再切流量。我们当时的做法是保留 HF 分支做对照哪怕只是多跑几个 step 对比 loss也能在迁移早期抓出大量隐蔽问题。2. transformer_config.json 逐字段拆解同一个字段在两套体系里的不同命运2.1 模型结构类字段直接映射但存在默认值陷阱一份典型的 LLaMA 系列 HF 配置大概长这样{ architectures: [LlamaForCausalLM], model_type: llama, hidden_size: 4096, num_hidden_layers: 32, num_attention_heads: 32, num_key_value_heads: 8, intermediate_size: 11008, max_position_embeddings: 2048, rms_norm_eps: 1e-6, vocab_size: 32000, bos_token_id: 1, eos_token_id: 2, torch_dtype: float16 }先说直接映射的那一批。hidden_size、num_hidden_layers、num_attention_heads、num_key_value_heads、intermediate_size、rms_norm_eps、vocab_size这些字段在 MindSpore 侧的模型 Config 类里基本都能找到对应参数名字可能略有出入比如num_hidden_layers在 MindSpore 的LLaMAConfig里通常叫num_layersnum_attention_heads对应num_heads。这类改名问题不可怕靠一个字段映射表就能解决真正的陷阱在于默认值不一致。举一个我们真实踩过的例子rms_norm_eps。HF 的 LLaMA 默认是1e-6但有一些社区权重用的是1e-5。如果你的自定义 Config 类里没有显式把这个字段传进去MindSpore 侧可能会用自己内置的默认值两边一旦不一致损失曲线在训练初期就会出现细微的偏移而且很难查因为它不报错只是收敛行为不一样。所以我在做字段映射时有一条铁律所有数值型字段一律显式赋值不允许依赖目标框架的默认值。2.2 special token 字段在 MindSpore 侧影响数据管线bos_token_id、eos_token_id、pad_token_id这三个字段在 HF 里主要是给生成和 tokenizer 用的模型结构上不直接影响参数量。但在 MindSpore 侧pad_token_id的影响半径明显变大。因为 MindSpore 的 GRAPH 模式对动态 shape 支持有限很多训练数据管线会统一做 padding 到固定长度padding 用的 token id 就来自这个字段。如果它没设置对你会发现数据集构建阶段不报错但训练出来的模型预测结果非常奇怪——因为有效 token 和 padding token 在 attention 里没有被 mask 干净。另外eos_token_id和pad_token_id在我们实际处理时容易搞混。比如一些英文语料里 eos 和 pad 用的都是 2这在 HF 里问题不大但 MindSpore 侧很多 loss 计算实现是除非 label 为 ignore_token_id否则都要算 loss如果你没有显式把 padding token 对应的 label 设成ignore_token_idpadding 部分也会参与 loss 计算导致训练指标虚低。这个细节在迁移时一定要去框架源码里确认一下 loss 函数是按什么规则忽略 padding 的不要想当然。2.3 容易被忽略的隐藏字段torch_dtype 和 transformers_versiontorch_dtype这个字段在 HF 里只是记录权重保存时的精度比如float16、bfloat16。到了 MindSpore 侧类似表达需要用两个字段分别描述参数存储精度param_init_type和计算精度compute_dtype。这两个精度可以不一样典型做法是权重用 fp16 或 bf16 存储计算时部分算子升级到 fp32 做累加降低精度溢出风险。如果你在 HF 里习惯了靠torch_dtype一个字段打天下迁过来之后一定要把这两个字段分开设置。transformers_version看起来是一个纯记录字段但它背后有一个隐蔽影响同一个transformer_config.json在不同版本的 Transformers 里实例化出的结构可能不同。比如某个版本修了 attention mask 的实现某个版本改了num_key_value_heads的默认值这些改动不会体现在 json 里但会反映在模型行为上。所以迁移前先固定 HF 版本并且用一个版本生成好参考权重避免两边模型定义对不上。3. 一套可落地的三步迁移方案权重、配置、代码分别怎么处理3.1 权重转换先做 key 映射 diff再写转换脚本权重迁移这块最忌讳的就是上来就写转换脚本。正确步骤是先把 HF 的state_dictkeys 和 MindSpore 参考模型或从零初始化的 MindSpore ckpt的 keys 全部打印出来放到一起做 diff。这个动作虽然枯燥但能让你一眼看清所有命名差异比训练到一半报shape mismatch再回头查高效得多。以 LLaMA 架构为例常见的命名差异有这么几处HF 里 embedding 层叫model.embed_tokens.weightMindSpore 某些实现里叫model.tok_embeddings.weightattention 里的q_proj、k_proj、v_proj、o_proj在 MindSpore 侧可能被整合成attention.wq、attention.wk、attention.wv、attention.woMLP 的gate_proj、up_proj、down_proj对应到feed_forward.w1、feed_forward.w3、feed_forward.w2。另外HF 的 LLaMA 通常把lm_head.weight和embed_tokens.weight做权重绑定tied embeddingsMindSpore 侧有些模型实现默认不绑定转换时需要对lm_head单独处理。我一般会写一个可复用的键映射脚本核心就是一张替换表def convert_hf_key_to_mindspore(name: str) - str: mapping [ (embed_tokens, tok_embeddings), (self_attn.q_proj, attention.wq), (self_attn.k_proj, attention.wk), (self_attn.v_proj, attention.wv), (self_attn.o_proj, attention.wo), (mlp.gate_proj, feed_forward.w1), (mlp.up_proj, feed_forward.w3), (mlp.down_proj, feed_forward.w2), (model.norm, model.norm), (lm_head, lm_head), ] for old, new in mapping: name name.replace(old, new) return name这个脚本不是一次性用品模型结构一旦调整比如把 MLP 改成 MoE映射规则就要跟着变。所以我强烈建议把映射表单独提成一个常量脚本只负责执行替换和 shape 校验这样后续维护成本低很多。3.2 配置改写不要手写全新 Config先找同架构基类继承很多同学迁移时喜欢从零手写一个模型 Config 类我的建议是别这么做。MindSpore Transformers 里已经有大量公开模型的 Config 实现你要迁移的 HF 模型大概率能找到同架构或近架构的基类。比如迁 LLaMA就直接继承mindformers.models.llama.LLaMAConfig把 HF config 里的字段翻译成它的构造参数from mindformers.models.llama import LlamaConfig def build_mindspore_config(hf_cfg: dict) - LlamaConfig: return LlamaConfig( hidden_sizehf_cfg[hidden_size], num_layershf_cfg[num_hidden_layers], num_headshf_cfg[num_attention_heads], n_kv_headshf_cfg.get(num_key_value_heads, hf_cfg[num_attention_heads]), intermediate_sizehf_cfg[intermediate_size], max_position_embeddingshf_cfg[max_position_embeddings], rms_norm_epshf_cfg[rms_norm_eps], vocab_sizehf_cfg[vocab_size], bos_token_idhf_cfg[bos_token_id], eos_token_idhf_cfg[eos_token_id], pad_token_idhf_cfg.get(pad_token_id, hf_cfg[eos_token_id]), )注意num_key_value_heads这个字段。LLaMA 2 开始引入 GQA 之后HF 配置里才有了这个字段但 MindSpore 侧不同版本对这个字段的支持程度不一样有的版本在LLaMAConfig里就叫n_kv_heads有的版本可能还没有开放这个参数需要你在更底层的 config 里绕过去设置。我在迁移时就遇到过一次参数传进去了训练也没报错但显存占用是完整 MHA 的量级回头查源码才发现那个版本的LLaMAConfig压根没读n_kv_heads等于白白多算了一倍的 kv cache。所以配置改写完之后一定要打印一下最终实例化出的模型结构确认 attention 分支数符合预期不要只看参数有没有设置成功。3.3 训练代码适配模型初始化、数据管线、回调三处重点改模型初始化这块MindSpore 和 PyTorch 的差异在于多了一个amp_level的概念。HF 里你只要设fp16True框架自动帮你做半精度训练MindSpore 里你需要通过mindspore.Model包装训练网络并显式指定amp_level比如O2表示大部分算子自动转半精度并且自动插入 loss scaleO0表示全 fp32。这里要注意amp_level的粒度是层级别而不是算子级别它有一套自己的黑白名单机制。如果你发现某个模型迁移后 loss 经常变成 NaN先不要怀疑优化器先去查amp_level对应的白名单里有没有把某些敏感的归一化算子排除掉。数据管线是另一个大改点。HF 的Dataset.map用起来很顺手但 MindSpore 侧如果你走 GRAPH 模式推荐把数据管线改成GeneratorDataset配合mindspore.dataset的算子来做。这里的核心限制是图模式下面数据 shape 必须静态化。所以 collator 里不要做动态 padding提前把序列长度固定成max_seq_length然后用 attention mask 区分有效 token。这么做虽然会浪费一点算力但能在 GRAPH 模式下避免大量的编译和调试成本。如果想省显存可以配合dynamic_shape模式但那属于进阶玩法初期迁移别一上来就这么干。回调这块反而简单。HF 的TrainerCallback对应MindSpore 的Callback实现step_end、epoch_end之类的钩子即可。不过要注意MindSpore 里读取训练中间状态的方式和 PyTorch 不一样比如你要拿当前的loss值需要通过cb_params或者打印到日志再解析不能直接用model.train_loss这种属性去取API 差异比较多建议看官方示例而不是自己闷头试。调试阶段我们常年在 VSCode 里配一个 MindSpore 内核来跑中间验证脚本比 Jupyter 更适合断点跟踪配置加载和注册表状态。4. 高频报错与根因排查从 pick another name 到算子适配4.1 复现场景aimv2 is already used by a transformers config 到底在说什么这个报错是这次迁移里最让人迷惑的一条。当时的场景是团队在 HF 侧注册了一个自定义模型model_type起名aimv2然后做了完整训练和保存。迁移时为了让 HF 分支和 MindSpore 分支共享一部分代码我们把两个框架的 import 放到了同一个训练进程里。结果一加载权重Transformers 直接抛出了这么一句话aimv2 is already used by a transformers config, pick another name.第一反应是谁把 aimv2 占了我们不是叫 aimv2 吗后来排查才发现这个报错根本不是和 MindSpore 冲突而是 HF Transformers 自身注册表内部的冲突。AutoConfig和AutoModel的注册表是一个以model_type为 key 的全局字典同一个环境里如果两个类用了同一个model_type注册新注册的不会覆盖旧的而是直接报错让你换个名字。我们自定义模型叫AimV2Configmodel_type也设成了aimv2但新版 Transformers 内部恰好已经有一个以aimv2为model_type的模型架构了。两边都往同一个注册表里写自然撞车。这个报错的本质不是名字被别人抢了而是命名空间没有做隔离。在 HF 这样的巨型生态里每个版本都会不断新增模型架构你今天起的名字很可能是别人已经注册过的。所以迁移多框架混合的项目时自定义模型的model_type一定要加一个项目前缀比如mycompany_aimv2把命名空间和上游隔离一劳永逸。4.2 排查链路三步定位注册表冲突如果你也遇到了类似的注册冲突可以按这个顺序排查速度会快很多。第一步看堆栈里抛错的地方到底是哪个库。如果是transformers.models.auto.configuration_auto那和 MindSpore 一点关系都没有纯粹是 HF 注册表问题。如果堆栈指向mindformers的注册逻辑那就是 MindSpore 侧的MindFormerRegister冲突。先分清楚是哪一边再往下查。第二步打印当前进程里的注册表。HF 侧可以用这样一段代码from transformers.models.auto.configuration_auto import CONFIG_MAPPING print(CONFIG_MAPPING)然后 grep 一下你的model_type是否已经存在。如果已经存在还要顺着代码去看它在MODEL_MAPPING里对应的是哪个类确认是不是我们自定义的类。很多时候你以为是自己注册的实际是某个依赖库在 import 时顺手注册了一个同名架构。第三步检查两个 Transformers 生态是否在同一个进程里互相 import。这里有一个隐蔽场景MindSpore 套件为了兼容 HF 权重格式内部也可能 importtransformers而你的训练脚本里又 import 了另一个版本的transformers两个版本如果在同一个环境里被混用注册表的全局状态就会被污染。解决方法是固定transformers版本或者在项目里做依赖隔离不要让两套版本的transformers共存于一个环境。4.3 其他高频报错算子和动态 shape 问题汇总除了注册冲突迁移过程中还有几个报错属于必踩级别我把现象、根因和应对思路整理成了一个表方便快速对照报错现象根因解决思路Kernel not found / Not support operator目标硬件或 GRAPH 模式下还没实现某算子替换为等效算子组合或者先切 PyNative 跑通再逐算子排查Shape mismatch at weight load权重 key 映射不完整或维度顺序不同打印 state_dict 与 ckpt 的 keys 做 diff重点核对转置型参数Dynamic shape is not supported图模式下数据 shape 不稳定固定 seq_length禁用动态 paddingOut of memory during compile并行切分不合理或未开重计算检查并行策略打开 gradient checkpointLoss is stuck at a constant value混合精度配置异常或 label 没做 padding mask分别检查 compute_dtype、param_init_type 与 ignore_token_id这里单独说说算子不支持的问题。MindSpore 对 Transformer 常见算子覆盖得已经很全但总有一些冷门操作会触发报错。我们的经验是不要试图跟框架较劲优先在模型等价的前提下去替换。比如某个自定义 attention 里用了一个框架不支持的 mask 合并方式可以转换成标准的[bs, seq, seq]加法 mask框架底层会融合到 Flash Attention 里性能反而更好。5. 训练跑通之后稳定性调优和性能验证的实测经验5.1 混合精度和 LossScale 的配置差异HF 里一个fp16True就解决了大部分问题MindSpore 侧则需要更精细地控制。前面提到过param_init_type和compute_dtype要分开设置这里补充一下 loss scale 的实践体会。MindSpore 的动态 loss scale 机制默认是开启的它会根据梯度溢出情况自动调整缩放因子。但我在迁移一个大批次任务时发现动态 loss scale 在训练初期频繁调整导致前几百步的 loss 曲线非常抖。后来改成固定 loss scale比如初始值1024.0配合梯度裁剪曲线明显稳定了。这个问题在 HF 里不太会遇到因为 HF 的 Trainer 把动态 scale 的调参封装得比较完善而 MindSpore 侧需要你自己关注loss_scale_manager的配置。一个比较实用的经验是迁移初期先用 fp32 跑通几十步确认前向反向逻辑没问题再切换到混合精度。不要一上来就开混合精度否则出问题时你无法判断是精度问题还是模型逻辑问题。fp32 跑通后再开amp_levelO2固定 loss scale 跑验证最后再调优化器的grad_clip一层一层往上叠比一次性把所有优化全打开要稳得多。5.2 并行策略从单卡到多卡要改 YAML 而不是改代码MindSpore 这套体系里并行策略的载体是 YAML不是代码。data_parallel、model_parallel、pipeline_parallel这些参数在配置里声明后框架负责把模型切分到多卡上。这一点比 HF 侧手动device_map要省心但也带来一个调试难点同一个模型单卡能跑改成 8 卡张量并行后可能直接编译失败。原因往往出在 attention 头数对 tensor parallel 的维度不整除比如num_attention_heads是 40切成 8 份恰好每份 5 个头但是如果并行度设成 4就切不匀了。我的建议是多卡并行前先检查所有需要切分的维度是否可以被并行度整除包括num_attention_heads、num_key_value_heads、intermediate_size。如果某个维度切不匀先调整并行度而不是强行修改模型结构否则后期维护代价很大。另外第一次上多卡时先开设备日志确认每张卡上的权重分片体积和计算负载基本均衡这一步能帮你快速发现切分配置里的低级错误。5.3 收敛性验证用相同数据顺序对比 loss 曲线迁移完不是能跑通就算完必须做收敛性验证。这里有一个容易被忽略的点HF 和 MindSpore 的Datasetshuffle 逻辑不同哪怕你用同一个随机种子、同一份数据两条管线的数据顺序也不会一致。直接对比 loss 曲线其实是不公平的因为模型看到的 batch 序列不同。要对比就得固定数据顺序——两边都关掉随机 shuffle按文件名的哈希顺序取样本这样每个 step 模型看到的数据完全一致loss 曲线才有可比性。实操中我还会做一个同 checkpoint 对比的验证把 HF 侧训练了 N 步的权重转成 MindSpore ckpt加载进 MindSpore 模型然后在固定数据顺序下跑 20 步回传 loss 值和 HF 侧同一份权重在同一批数据上的 loss 对比。两者一致说明权重映射和模型结构完全对齐有偏差就说明模型结构实现细节还有差异需要回到配置文件上排查。这个验证方法帮我抓出过两次rms_norm_eps不一致和一次 attention mask 实现差异强烈建议迁移团队都做一次。性能这块比较实际的度量方式是记录每个 step 的 wall time然后估算模型 FLOPs算一个近似 MFU。不要只看 step time因为 step time 受 batch size 影响很大。我们最终迁移完成后在相同 batch size 下MindSpore GRAPH 模式对比 HF 动态图模式step time 有 10% 到 15% 的提升这还没算上框架层重计算和并行切分省下的显存收益。不过这个数字样本量小不具备普遍参考价值只是说明迁移确实有性能收益空间值得投入精力去调。这次迁移给我最大的感受是transformer_config不是拿来就能用的。它在 HF 里描述的是一个模型在 MindSpore Transformers 里描述的是一个训练系统。我个人建议把这个 json 当成字段字典而不是配置本体迁移前先花半天把每个字段在两侧的映射关系画清楚后面会省掉大量反复试错的时间。另外一个小技巧保留 HF 侧的训练脚本分支迁移期间每次改动都自动在两个分支上同时跑 10 个 step对比 loss 是否在同一量级这个习惯帮我抓出了好几个rms_norm_eps不一致的问题。最后再说一句那个pick another name报错本质上不是 MindSpore 的锅而是注册表命名空间的通用约束——任何多框架混用环境里都值得先用注册表打印命令确认一遍再动手。