在华为昇腾910处理器上训练ResNet50模型。对比了主流框架,最终选择MindSpore,主要看中其对昇腾的原生支持和自动并行特性。本文将分享从环境搭建到性能调优的全流程经验,重点介绍如何利用MindSpore的特性榨干昇腾硬件性能,并记录一些踩坑的解决方案。如果你正打算在昇腾上跑MindSpore,希望这篇文章能帮你少走弯路。
2. 环境准备
硬件:昇腾910 AI处理器(32GB显存) 软件栈:
- 操作系统:Ubuntu 18.04 / EulerOS
- 驱动:Ascend HD Driver 21.0.3
- CANN:5.0.2(华为昇腾计算架构)
- MindSpore:1.8.1(昇腾版本)
- Python:3.7.5
安装MindSpore时务必使用配套的CANN版本,否则可能出现算子兼容问题。推荐使用Docker镜像快速部署:
docker pull swr.cn-south-1.myhuaweicloud.com/mindspore/mindspore-ascend:1.8.1
3. 数据准备:MindRecord加速
ImageNet数据集通常以原始图片形式存在,训练时如果使用ImageFolderDataset实时解码,I/O会成为瓶颈。MindSpore提供了MindRecord数据格式,将图片预处理后打包成二进制文件,训练时直接读取,能显著提升数据加载速度。
转换脚本(关键片段):
import mindspore.dataset as ds
from mindspore.mindrecord import FileWriter
# 定义schema
schema_json = {"file_name": {"type": "string"},
"label": {"type": "int32"},
"data": {"type": "bytes"}}
writer = FileWriter(file_name="imagenet.mindrecord", shard_num=4)
writer.add_schema(schema_json, "imagenet_dataset")
# 遍历原始图片,读取并写入
for img_path, label in image_list:
with open(img_path, 'rb') as f:
img_bytes = f.read()
writer.write_raw_data([{"file_name": img_path, "label": label, "data": img_bytes}])
writer.commit()
训练时使用MindDataset加载:
dataset = ds.MindDataset(dataset_files="imagenet.mindrecord*", columns_list=["data", "label"])
dataset = dataset.map(operations=decode_and_resize, input_columns="data")
...
实测用MindRecord后,数据读取速度提升3倍以上,GPU利用率更稳定。
4. 模型构建:ResNet50的MindSpore实现
MindSpore官方ModelZoo提供了ResNet50脚本,但直接使用可能无法发挥硬件最佳性能。我们参考官方实现,但需注意以下几点:
- 使用
nn.Cell构建网络,所有层尽量使用MindSpore原生算子(如nn.Conv2d而非手动实现),确保昇腾亲和性。 - 激活函数推荐使用
nn.ReLU,避免使用nn.LeakyReLU(昇腾上性能略差)。 - 若需自定义算子,务必通过
@ms_ops.constexpr或Primitive封装,并测试能否在昇腾上编译。
5. 混合精度训练
昇腾910对FP16计算有深度优化,混合精度能大幅提升训练速度。MindSpore支持三种混合精度模式:
O0:纯FP32O2:除BatchNorm外全FP16O3:全FP16(极少用)
我们采用O2并开启动态损失缩放:
from mindspore import amp
net = ResNet50()
loss_fn = nn.SoftmaxCrossEntropyWithLogits()
optimizer = nn.Momentum(params=net.trainable_params(), learning_rate=0.1, momentum=0.9)
# 配置混合精度
net = amp.auto_mixed_precision(net, 'O2')
loss_scale_manager = amp.DynamicLossScaleManager()
# 封装模型
model = Model(net, loss_fn=loss_fn, optimizer=optimizer, loss_scale_manager=loss_scale_manager, metrics={'acc'})
注意:O2模式下,网络输入也需转为FP16,MindSpore会自动处理。若自定义数据处理需手动cast。
6. 自动并行与分布式训练
单卡训练时,昇腾910 32GB显存足够放下ResNet50的完整模型(batch size可开到256)。但若想训练更大模型或加快速度,可配置分布式数据并行。MindSpore的auto_parallel支持自动搜索最优并行策略,我们先用简单的数据并行:
from mindspore import context
from mindspore.communication import init
context.set_context(mode=context.GRAPH_MODE, device_target="Ascend")
init("nccl") # 昇腾上使用hccl
context.set_auto_parallel_context(parallel_mode=ParallelMode.DATA_PARALLEL, gradients_mean=True)
在多机多卡场景,auto_parallel的半自动并行非常强大,可以指定特定层进行模型并行,例如将全连接层切分到多卡。我们实验中尝试将最后一个全连接层按输出维度切分,减少了通信量,训练速度提升15%。配置方法:
from mindspore.nn import MatMul
from mindspore.ops import operations as P
class MyDense(nn.Cell):
def __init__(self, in_channels, out_channels):
super().__init__()
self.matmul = MatMul(transpose_b=True)
# 指定切分策略:权重按[in_channels, out_channels//2]切分
self.weight = Parameter(initializer('normal', [in_channels, out_channels]), name="w")
self.add = P.Add().shard(((1, 1), (1, 1))) # 偏置不切分
# 设置matmul的切分策略:输入不切,权重按列切
self.matmul.shard(((1, 1), (1, 2))) # 假设2卡
def construct(self, x):
x = self.matmul(x, self.weight)
x = self.add(x, self.bias)
return x
7. 性能调优技巧
7.1 数据流水线优化
- 使用
dataset.batch(batch_size, drop_remainder=True)确保每个batch大小一致。 - 将耗时预处理(如随机裁剪、色彩增强)放到昇腾的
DSL(Data Stream Library)上执行:通过.map(..., num_parallel_workers=8)开启多进程。 - 开启数据下沉模式:
model.train(epochs, dataset, sink_size=100),将多个step的计算下沉到硬件,减少host-device交互。