在昇腾910上高效训练ResNet50

1 阅读4分钟

在华为昇腾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.constexprPrimitive封装,并测试能否在昇腾上编译。

5. 混合精度训练

昇腾910对FP16计算有深度优化,混合精度能大幅提升训练速度。MindSpore支持三种混合精度模式:

  • O0:纯FP32
  • O2:除BatchNorm外全FP16
  • O3:全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交互。