昇思MindSpore大模型预训练实战:流程、并行与调优全解析
2026/9/5 4:23:46 网站建设 项目流程

先交代一个背景:这两年大模型相关的工具链、框架、教程层出不穷,但大部分资料都是围绕 PyTorch 生态展开的。昇思 MindSpore 作为另一条完整的技术栈,在国产算力适配、自动并行、动静统一等方面做了很多差异化的设计,尤其适合有昇腾硬件、或者希望绕开 CUDA 依赖的团队。我大概从去年开始把一部分预训练任务从 PyTorch 迁移到昇思上跑,中间踩了不少坑,也总结出了一套相对通用的操作路径。这篇文章不吹不黑,纯粹从实操角度,把昇思上做大模型预训练的通用流程、关键设计、常见问题梳理一遍,给想入坑或者正在迁移的同行做一个参考。

1. 整体流程设计与方案选型

1.1 昇思预训练到底在做什么

预训练这件事,本质上就是在大规模无标注文本(或者图文、语音)上,通过自监督目标函数让模型学会语言的统计规律和通用表示。昇思上的预训练流程,和你在 PyTorch 里做的事情没有本质区别,但因为底层实现不同,整个流程的组织方式有明显差异。

一个标准的昇思预训练任务,至少包含以下几个环节:数据集准备与流式加载、模型结构定义与初始化、并行策略配置(如果你有多卡或集群)、优化器与学习率调度、训练循环(前向、反向、梯度累积、参数更新)、checkpoint 保存与恢复、日志与指标监控、最后是评估和推理验证。

我在 PyTorch 时代习惯于把数据集全部加载到内存再 shuffle,到了昇思这里很快发现行不通。因为昇思的数据处理管线是基于GeneratorDataset+map+batch的流式架构,数据不会一次性全部进内存,而是按需生成和变换。这个设计对超大语料(几十 TB 甚至上百 TB)非常友好,但需要你调整自己的编程习惯。

1.2 为什么选择昇思而不是 PyTorch

这是每个迁移者都会问的问题。我当时的出发点很朴素:团队里有昇腾卡,而昇思在昇腾上的算子适配和显存管理确实做得更细。PyTorch 虽然可以通过各种方式在昇腾上跑,但总有一些算子要自己适配,分布式通信的效率也差一些。

从框架设计的角度看,昇思有几个对预训练特别友好的特性:

一是动静统一。你可以先用动态图模式快速调试模型结构和训练逻辑,等确认无误后用set_context(mode=GRAPH_MODE)切到静态图模式做高性能训练。PyTorch 里torch.compile也能做到类似效果,但昇思的动静切换在工程上更透明,调试期和生产期共用同一套代码。

二是自动并行。昇思的AutoParallel可以根据模型大小、数据量、集群拓扑自动搜索最优的并行策略,包括数据并行、算子级模型并行、流水线并行。你不需要像 Megatron-LM 那样手动切分 Transformer 层,框架会在计算图编译阶段帮你完成切分和通信插入。

三是统一的 Checkpoint 管理。昇思把模型参数、优化器状态、RNG 状态统一保存,恢复训练时能精确回到中断点,这对于动不动训练几周的大模型来说非常关键。

当然,PyTorch 生态更丰富,社区资源多,HuggingFace 上的模型基本能直接加载。昇思也有 MindSpore Transformers 仓库和 ModelZoo,很多主流模型结构都能直接拿到预训练权重或直接跑通训练脚本。如果你主要工作在昇腾硬件上,昇思的综合体验是优于 PyTorch 的。

1.3 预训练方法选择的三个层次

我把昇思上做预训练的方法分为三个层次,你可以根据自己的硬件资源、模型规模和技术储备来选择。

第一层:使用现成脚本和模型仓库。昇思提供了mindspore/models仓库,里面有 GPT、BERT、Llama 等模型的完整预训练脚本,包括数据处理、模型定义、训练循环、并行策略。你的工作是准备数据、调整超参数、启动训练。这个层次适合第一次接触昇思、或者只想快速验证硬件的团队。

第二层:在现有模型结构上做定制。比如你要改注意力机制、换位置编码、或者调整 MoE 结构,那么可以在 MindSpore Transformers 的模型代码基础上修改,训练脚本复用现成的Trainer接口。这个层次需要你对模型实现和昇思 API 有一定理解,但不需要从零写分布式逻辑。

第三层:完全自定义模型和训练流程。你要自己定义nn.Cell,自己写TrainOneStepCell,自己配置并行策略。这个层次灵活性最大,但工作量也最大,适合做前沿算法研究的团队。

我个人建议,无论你处于哪个层次,先把官方仓库的标准训练脚本跑通一遍,再动手改自己的东西。这样你能确认环境、数据格式、训练流程都已经正确,后续排错的范围会小很多。

2. 环境准备与数据管线搭建

2.1 安装与版本匹配

昇思的版本和硬件驱动、固件、Python 版本有严格对应关系,这是新手最容易卡住的第一关。我建议先确定你的硬件型号和昇腾驱动版本,再根据官方版本匹配表选择 MindSpore 版本,最后创建对应的 Python 虚拟环境。

这里给出一个实际可用的组合示例:

  • 昇腾 910B 芯片,驱动版本 24.1.rc1
  • Python 3.9
  • MindSpore 2.3.0
  • CANN 8.0.RC1
  • mindspore-transformers 0.3.0

安装命令一般是这样:

conda create -n ms230 python=3.9 -y conda activate ms230 pip install mindspore==2.3.0 pip install mindspore-transformers==0.3.0 pip install datasets tokenizers numpy sentencepiece

注意,MindSpore 的 pip 包已经从 PyPI 移到了昇思官方源,所以需要加-i https://ms-release.obs.cn-north-4.myhuaweicloud.com/...这类地址。这个细节官方文档有写,但很多人没注意,导致pip install mindspore装到了旧版本或者直接报找不到包。

安装完成后,跑一个最简单的验证脚本:

import mindspore as ms from mindspore import Tensor ms.set_context(device_target="Ascend") x = Tensor([1.0, 2.0, 3.0]) print(x.sum())

能正常输出结果,说明环境基本没问题。

2.2 数据集的准备与流式加载

数据准备是预训练里最耗时、最容易出错的部分。昇思的mindspore.dataset提供了GeneratorDatasetMindDatasetTFRecordDataset等接口。对于超大规模语料,我建议把原始文本转换成MindRecord格式,也就是昇思自己的二进制数据格式,加载速度和随机读性能都远好于直接读文本。

转换流程大概是这样的:先把原始文本清洗、去重、过滤低质量内容,然后用 Tokenizer 把文本切成 token ids,再加上特殊 token、padding、truncation,最后写入 MindRecord。这一步可以在离线阶段用多进程并行处理,我当时是用 32 个进程跑了大概 6 个小时,把 5TB 的原始文本转成了约 3TB 的 MindRecord 文件。

有一个细节需要特别留意:即使你用MindDataset读取,训练时也经常需要在map操作里做动态 padding 或者 mask 构造。昇思的map操作支持 Python 函数,但函数会在数据管线的 worker 进程里执行,所以不要在函数里引用训练进程的大对象,否则序列化会很慢。

一个典型的预处理函数例子:

import mindspore.dataset as ds import numpy as np def preprocess_for_gpt(example): input_ids = example["input_ids"] # 构造标签,向右偏移一位 labels = input_ids[1:] + [tokenizer.pad_token_id] # 对 padding 部分做 mask attention_mask = [1] * len(input_ids) return input_ids, labels, attention_mask dataset = ds.MindDataset(dataset_files="data/*.mindrecord", num_shards=rank_size, shard_id=rank_id, shuffle=True) dataset = dataset.map(operations=preprocess_for_gpt, input_columns=["input_ids"], output_columns=["input_ids", "labels", "attention_mask"]) dataset = dataset.batch(batch_size=8, drop_remainder=True)

注意num_shardsshard_id这个参数,在多卡训练时每个卡读取不同的数据分片,避免数据重复。这个机制是昇思数据集模块自带的分布式采样,比 PyTorch 的DistributedSampler更直接。

2.3 Tokenizer 的一致性问题

Tokenizer 是预训练中一个容易被忽略但极其关键的组件。训练时用的 tokenizer 和推理时用的 tokenizer 必须完全一致,否则模型效果会大打折扣。昇思这边没有自己的 tokenizer 库,通常做法是直接用 HuggingFace 的tokenizers库或者transformers里的 tokenizer,在预处理阶段把文本转换成 token ids 存到 MindRecord 里。

这样做的好处是训练阶段完全不依赖 tokenizer 库,加快数据加载;坏处是一旦需要在线处理数据(比如某些动态任务),你得在训练进程里也加载 tokenizer,那就要保证 tokenizer 库的版本一致。我的建议是:预训练阶段全部离线转成 ids,不要在线 tokenize,能省不少事。

3. 模型定义与并行策略核心细节

3.1 用 nn.Cell 搭模型

昇思的模型定义继承mindspore.nn.Cell,和 PyTorch 的nn.Module很像,但有几个差异点需要注意。比如Cell__init__里必须调用super().__init__(),而construct方法相当于 PyTorch 的forward

一个最小化的 GPT 模型结构示例:

import mindspore as ms import mindspore.nn as nn from mindspore.common.initializer import Normal class GPTBlock(nn.Cell): def __init__(self, hidden_size, num_heads, intermediate_size): super().__init__() self.layernorm1 = nn.LayerNorm((hidden_size,)) self.attention = nn.MultiheadAttention(hidden_size, num_heads) self.layernorm2 = nn.LayerNorm((hidden_size,)) self.mlp = nn.SequentialCell([ nn.Dense(hidden_size, intermediate_size), nn.GELU(), nn.Dense(intermediate_size, hidden_size) ]) def construct(self, x, mask=None): x = x + self.attention(self.layernorm1(x), mask) x = x + self.mlp(self.layernorm2(x)) return x class GPTModel(nn.Cell): def __init__(self, vocab_size, hidden_size, num_layers, num_heads): super().__init__() self.embedding = nn.Embedding(vocab_size, hidden_size) self.blocks = nn.CellList([GPTBlock(hidden_size, num_heads, 4*hidden_size) for _ in range(num_layers)]) self.layernorm = nn.LayerNorm((hidden_size,)) self.lm_head = nn.Dense(hidden_size, vocab_size) def construct(self, input_ids): x = self.embedding(input_ids) for block in self.blocks: x = block(x) x = self.layernorm(x) logits = self.lm_head(x) return logits

我在实际使用中把nn.MultiheadAttention换成了自定义的 Attention 实现,因为预训练通常需要加 attention mask、position bias 等额外处理。如果你需要精细控制,建议直接看 MindSpore Transformers 仓库里的mindspore_transformers/models/gpt实现,里面有完整的 RMSNorm、旋转位置编码、GQA 等实现,直接改参数字典就能用。

3.2 并行策略:数据并行、模型并行、流水线并行怎么选

昇思的自动并行是一个非常值得了解的机制。它有两种用法:

第一种是ms.set_auto_parallel_context(parallel_mode=ms.ParallelMode.DATA_PARALLEL),也就是纯数据并行。每张卡上有完整的模型副本,数据按卡切分,梯度进行 all-reduce。这个模式适合模型单卡能放下、但数据量很大的场景,比如 7B 或者 13B 级别。

第二种是SEMI_AUTO_PARALLEL或者AUTO_PARALLEL,让框架自动插入通信集合通信原语,支持算子级模型并行、流水线并行、优化器状态切分。13B 以上模型或者单卡显存撑不下的场景必须用这个模式。

实际配置示例:

import mindspore as ms ms.set_context(mode=ms.GRAPH_MODE, device_target="Ascend") ms.set_auto_parallel_context( parallel_mode=ms.ParallelMode.SEMI_AUTO_PARALLEL, gradients_mean=True, comm_fusion={"gradient": 8}, # 梯度通信融合 pipeline_stages=2, # 流水线并行阶段数 )

这里pipeline_stages=2的含义是把 Transformer 层分成两组,分别放在不同的设备上,层与层之间通过点到点通信传递激活值和梯度。更大的模型可以把流水线阶段数调大。

你可能注意到我没有在模型代码里手动添加任何通信操作,这是因为昇思的编译图会自动分析哪些算子需要跨设备通信,并在图上插入合适的通信原语。这个机制非常强大,但也意味着你不能在construct里写不规范的 Python 控制流,否则自动并行分析可能失败。调试期建议先在PYNATIVE_MODE下跑通,再切到GRAPH_MODE

3.3 优化器:混合精度与梯度累积

大模型预训练几乎必用混合精度。昇思里用ms.amp模块来管理:

from mindspore import amp model = GPTModel(...) optimizer = nn.AdamWeightDecay(model.trainable_params(), learning_rate=3e-4) loss_scaler = amp.DynamicLossScaler(scale_value=2**16, scale_factor=2, scale_window=2000) model = amp.auto_mixed_precision(model, amp_level="O2")

amp_level="O2"表示大部分算子用 FP16 计算,部分对精度敏感的算子(如 LayerNorm、Softmax)保持 FP32。这个设置对预训练效果影响很大,如果你发现损失在训练中不稳定,可以把O2降到O1或者保守地自定义哪些算子保持 FP32。

梯度累积在昇思里是通过nn.TrainOneStepCell加上一个累积器实现的,一个好的方式是直接用mindspore.nn.wrap.GradAccumulation

from mindspore.nn.wrap import GradAccumulation accum_steps = 16 net_with_loss = nn.WithLossCell(model, loss_fn) train_net = GradAccumulation(net_with_loss, optimizer, accum_steps)

梯度累积的意义在于,当单卡 batch size 受显存限制比较小时,你可以通过累积多个 mini-batch 的梯度来等效一个大 batch。比如单卡 batch size 只能设为 8,累积 16 步就相当于 128 的全局 batch size。

4. 训练实操:从单卡到多卡

4.1 单卡调试与动态图模式

正式大规模训练之前,务必先在小数据上做单卡调试。我通常会用 1000 条样本跑 5 到 10 个 step,验证损失是否能正常下降、显存占用是否在预期范围内、梯度是否为正常数值。

调试时使用动态图模式:

ms.set_context(mode=ms.PYNATIVE_MODE, device_target="Ascend")

动态图模式下报错信息更直观,可以像 PyTorch 一样打断点、打印张量。但注意,动态图模式下不要做大规模并行,否则性能极差。调试完再切回GRAPH_MODE

4.2 多卡启动:rank 和卡号的坑

多卡训练需要正确设置设备 ID 和 rank 信息。最简单的启动方式是用昇思的modelartsrank_table方式,但如果你在裸机环境,可以用mpirun

mpirun -n 8 python train.py --config configs/gpt_7b.yaml

train.py里,你需要读取rank_idrank_size,通过环境变量RANK_IDRANK_SIZE获取。注意,昇思的设备 ID(物理卡号)和 rank ID(逻辑编号)可能不一致,需要通过如下方式绑定:

import os import mindspore as ms rank_id = int(os.getenv("RANK_ID", "0")) rank_size = int(os.getenv("RANK_SIZE", "1")) device_id = int(os.getenv("DEVICE_ID", rank_id % 8)) ms.set_context(device_id=device_id)

如果你用的是mpirun且没有设置RANK_ID,有可能所有进程都读到默认值 0,导致所有进程都使用 0 号卡,这是新手最常犯的错误。我的做法是在启动脚本里显式传入:

mpirun -n 8 -H localhost:8 bash -c 'export RANK_ID=$OMPI_COMM_WORLD_RANK && python train.py'

4.3 损失函数与训练循环

预训练分类任务的损失函数就是交叉熵,但在昇思上直接使用nn.CrossEntropyLoss需要留意标签形状和 ignore index。一个实用写法:

import mindspore.nn as nn class PretrainLoss(nn.Cell): def __init__(self, vocab_size): super().__init__() self.loss_fn = nn.CrossEntropyLoss(ignore_index=-100) self.vocab_size = vocab_size def construct(self, logits, labels): # logits: [batch, seq_len, vocab_size] # labels: [batch, seq_len] logits = logits.view(-1, self.vocab_size) labels = labels.view(-1) return self.loss_fn(logits, labels)

训练循环可以不用手写,直接用mindspore.Model的高级 API:

from mindspore import Model, TrainOneStepCell model = GPTModel(...) loss_fn = PretrainLoss(...) optimizer = nn.AdamWeightDecay(...) net_with_loss = nn.WithLossCell(model, loss_fn) train_net = TrainOneStepCell(net_with_loss, optimizer) for epoch in range(num_epochs): for step, (input_ids, labels, attention_mask) in enumerate(dataset): loss = train_net(input_ids, labels, attention_mask) if step % 100 == 0: print(f"step: {step}, loss: {loss}")

4.4 Checkpoint 保存与断点续训

训练几周突然因为机器故障中断,如果没有保存 checkpoint,前面的时间就全浪费了。昇思的 checkpoint 管理有几个接口要熟悉:

  • ms.save_checkpoint(network, path):保存模型参数
  • ms.load_checkpoint(path, network):加载模型参数
  • ms.load_param_into_net:将 checkpoint 中的参数更新到网络中

对于大模型,我建议保存以下内容:模型参数、优化器参数、step 计数、RNG 状态、学习率调度器状态。昇思的CheckpointConfig可以自动定期保存:

from mindspore.train.callback import CheckpointConfig, ModelCheckpoint ckpt_config = CheckpointConfig( save_checkpoint_steps=1000, keep_checkpoint_max=5, save_checkpoint_seconds=3600 ) ckpt_callback = ModelCheckpoint(prefix="gpt7b", directory="./ckpt", config=ckpt_config)

但注意,ModelCheckpoint默认只保存network参数,优化器状态不会自动保存。等你恢复训练时,如果优化器的动量、方差信息缺失,前期训练的节奏会被打断。我的做法是自定义一个 callback,在保存 checkpoint 时同时保存 optimizer 的状态:

class FullCheckpointCallback(Callback): def __init__(self, network, optimizer, config): self.network = network self.optimizer = optimizer self.config = config def step_end(self, run_context): cb_params = run_context.original_args() step = cb_params.cur_step_num if step % self.config.save_steps == 0: ckpt_path = f"./ckpt/gpt7b_step_{step}.ckpt" ms.save_checkpoint(self.network, ckpt_path) opt_path = f"./ckpt/gpt7b_step_{step}_optimizer.ckpt" ms.save_checkpoint(self.optimizer, opt_path)

恢复训练时,先加载模型参数再加载优化器参数,继续从 step 处开始即可。

5. 推理部署与模型导出

5.1 从训练态到推理态的转换

预训练完成后,需要把模型从训练状态切到推理状态。这里有三个关键操作:一是去掉损失函数和优化器,只保留model本身;二是把model.set_train(False),确保 dropout 等训练相关的层被关闭;三是如果你想用 MindSpore Lite 做推理,需要把模型导出为 MindIR 格式:

import numpy as np import mindspore as ms from mindspore import Tensor model.set_train(False) input_ids = Tensor(np.ones((1, 128), dtype=np.int32)) ms.export(model, input_ids, file_name="gpt7b", file_format="MINDIR")

导出时注意,ms.export需要传入一个样例输入来确定输入 shape 和 dtype,而且这个 shape 要和你在推理时用的 shape 一致,否则导出后推理可能报 shape 不匹配的错。

5.2 自回归生成推理示例

如果你只是做一个简单的在线推理 demo,不需要导出 MindIR,用 Python 直接跑construct也行。这里给出一个最基础的自回归生成流程:

def generate(model, tokenizer, prompt, max_new_tokens=64): model.set_train(False) input_ids = tokenizer.encode(prompt, return_tensors="np") for _ in range(max_new_tokens): logits = model(Tensor(input_ids, ms.int32)) next_token_logits = logits[0, -1, :] next_token = int(next_token_logits.argmax()) input_ids = np.concatenate([input_ids, [[next_token]]], axis=1) if next_token == tokenizer.eos_token_id: break return tokenizer.decode(input_ids[0])

注意,这里的 logits 形状是[batch, seq_len, vocab_size],每次迭代都要重新跑一遍完整的前向。如果序列很长,性能会比较差,实际生产建议用 KV Cache 优化。昇思的nn.MultiheadAttention没有自动暴露 KV cache,需要自己实现,官方 MindSpore Transformers 仓库里 GPT 模型有带 KV cache 的 generate 接口,可以直接参考。

5.3 部署推理时的一个精度细节

如果你在训练时用了混合精度amp_level="O2",推理时要保持一致。很多模型在训练时精度正常,部署时损失飙升、生成质量差,就是因为推理时没有开启同样的混合精度设置。建议在推理代码里也调用:

model = amp.auto_mixed_precision(model, amp_level="O2") model.set_train(False)

6. 热门工具链与生态协同

6.1 MindSpore Transformers 与 ModelZoo

如果你不想从零搭模型,建议直接使用mindspore-transformers这个库,它在mindspore.models目录下维护了大量预训练模型的实现,包括GPTBERTLlamaBloom等。安装后可以这样加载模型:

from mindformers import GPT2LMHeadModel, GPT2Config config = GPT2Config.from_pretrained("gpt2") model = GPT2LMHeadModel(config)

也可以直接用官方训练好的权重做推理或微调,节省大量训练时间。但注意,从 HuggingFace 下载的权重不能直接加载到昇思模型里,需要通过转换脚本把pytorch_model.bin转成昇思的 ckpt 格式。这类转换脚本在mindformerstools目录下都有,用convert_weight命令就可以。

6.2 与 HuggingFace tokenizer 的配合

昇思的模型输入输出格式和 HuggingFace 不完全一致。HuggingFace 的BertTokenizer返回的是dict,而昇思的模型construct常常接受位置参数。所以我在写数据管线时,总是先手动把 tokenizer 的返回值拆开,按模型需要的参数顺序传参。

这里有一个最佳实践:预处理阶段不要保存 tokenizer 的对象,直接保存 token ids 和 attention mask 到 MindRecord,这样训练代码完全不需要 import transformers,也避免了 tokenizer 库版本升级导致的不一致问题。

6.3 模型微调与 LoRA 进阶

预训练完成后,通常还要做领域微调或指令微调。昇思也支持 LoRA 这类参数高效微调方法。mindformers里的PeftPretrainedModel就是为 LoRA 设计的,你只需要在加载模型时指定lora_ranklora_alpha

from mindformers import GPT2LMHeadModel model = GPT2LMHeadModel.from_pretrained( "gpt2", lora_rank=8, lora_alpha=16, lora_dropout=0.1 )

LoRA 微调时冻结大部分参数,只训练低秩矩阵,显存占用和训练速度都比全参数微调理想得多。我通常先用 LoRA 跑一个快速实验,确认效果方向没问题,再决定是否做全参数微调。

6.4 常见的模型切换与权重迁移

从 PyTorch 迁移一个已经训练好的模型到昇思,主要工作是权重 key 的映射。HuggingFace 的模型参数名一般带transformer.h.0.attn.c_attn.weight这种前缀,而昇思模型的参数名可能是blocks.0.attention.query.weight,需要写一个映射字典来转换。mindformers提供了自动转换功能,但遇到自定义结构还是要自己写脚本。

一个简单的映射示例:

def convert_pytorch_to_ms(state_dict): new_state_dict = {} for key, value in state_dict.items(): new_key = key.replace("transformer.h", "blocks") new_key = new_key.replace("attn.c_attn", "attention.qkv") new_state_dict[new_key] = value return new_state_dict

看起来简单,但实际操作中多层嵌套的 key 替换很容易出错。建议转换后用一个随机输入做前向对齐测试,对比 PyTorch 和昇思的输出是否一致。

7. 常见问题排查与性能调优实录

7.1 启动训练时卡住或报错

常见情况一:runtime error: ascend device init failed。这个问题八成是设备 ID 设置错误,或者ASCEND_VISIBLE_DEVICES环境变量没设置。检查你的物理卡是否被其他进程占用,用npu-smi info查看。

常见情况二:多卡启动后只有 0 号卡在跑。这是因为你没有正确区分 rank 和 device,或者数据集shard_id没有按 rank 分配。

7.2 训练 loss 不下降或出现 NaN

loss 不下降,先检查学习率。大模型通常要用带 warmup 的余弦学习率,初始学习率一般在1e-43e-4,warmup 步数占训练总步数的 1% 到 2%。如果学习率没问题,再检查数据:标签是否正确偏移?mask 是否正确?有没有把 padding 位置的标签也计算损失了?

出现 NaN,大概率是混合精度问题。先切到纯 FP32 试一次,如果能跑通说明 FP16 下某些算子溢出了,需要给网络添加手动loss_scaler或者修改loss_scale策略。

7.3 显存不够怎么办

显存不够时,按优先级依次尝试:减小 batch size、开启梯度累积、开启激活重计算(recompute)、使用模型并行、切换混合精度等级到更激进。

昇思的激活重计算配置很简单:

from mindspore.nn import recompute for block in model.blocks: recompute(block)

这会在前向时保留部分中间激活,反向时重新计算,典型用显存换时间的方案。一般来说开启重计算后显存占用能下降 40% 以上。

7.4 性能调优经验

单卡训练时,先用npu-smi info看算力利用率是否打满。如果利用率低,问题通常出在数据加载上。检查 MindRecord 文件是否有足够多的分片文件、num_parallel_workers是否设置合理、shuffle buffer 大小是否合适。我一般把num_parallel_workers=8,shuffle buffer 设为训练数据一个 epoch 的 0.1% 左右。

多卡训练时,关注梯度通信耗时。如果通信占比过高,增大comm_fusion的融合值,把多个梯度打包一次通信发送。还有一种技巧是开启梯度压缩,昇思支持optimizergradient_compression_type=GradientCompressType.NONE,如果带宽瓶颈你还可以试GradientCompressType.ON

另外,AMP level 的选择也直接影响收敛速度和质量。不要贪图 O2 的加速而忽略了精度风险,可以从 O1 开始训,观察几个 epoch 的 loss 曲线无异常后再换 O2。

7.5 断点续训中的隐蔽坑

断点续训最隐蔽的问题是学习率调度器状态没恢复。如果你保存了模型参数和优化器参数,但把global_step重置为 0,学习率会重新从最高点开始,相当于浪费了前面的 warmup 过程,还会导致 loss 突然拉升。

所以我在保存 checkpoint 时,总是额外保存一个 JSON 文件,记录当前 step、epoch、学习率、RNG 状态。恢复训练时,把这个 step 传给学习率调度器,确保学习率曲线是连续的。

7.6 关于模型评测的一个建议

预训练阶段就要提前想好评测方案。很多团队把模型训完才发现评测代码和环境没准备,此时回滚成本极高。我的习惯是在训练脚本里就内置一个轻量评测函数,每隔 N 步用一个小验证集跑一下 perplexity,同时保存一组固定 prompt 的生成样例,直观观察训练效果。这样你能尽早发现模型是否过拟合、是否出现重复生成等问题。

8. 总结与长期迭代的一些体会

从 PyTorch 迁移到昇思做预训练,初期确实有学习成本,尤其是数据管线和并行策略的配置方式跟之前很不相同。但是一旦跑通,昇思在昇腾硬件上的性能表现是实打实的,尤其大规模自动并行这块,省去了很多手工切分模型的痛苦。

如果让我给一个实施顺序的建议,那就是:先用mindformers现成脚本跑通一个小模型,再逐步替换成自己的数据和自定义模型,最后再引入并行策略。不要一上来就追求 70B 甚至更大规模,在小模型上建立正确的工作流,比什么都重要。

最后分享一个小技巧:昇思的set_context(mode=GRAPH_MODE)编译时间会比较长,特别是大模型第一次编译可能要等十几分钟。如果只是小改动调参,尽量保留PYNATIVE_MODE去跑,等确认代码逻辑没有问题了再切图模式做正式训练。这个小习惯能帮你节省大量调试等待时间。

大模型预训练这条路没有捷径,但工具链选对了、流程打通了,后面就是耐心调参和等待 loss 曲线慢慢下降的事了。希望这篇分享能帮你在昇思上少走一些弯路,顺利把预训练任务跑起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询