1. 为什么我们要从PyTorch转向MindSpore和昇腾?

如果你和我一样,是个在AI领域摸爬滚打多年的开发者,手里肯定有几个用PyTorch精心打磨的模型。PyTorch的动态图机制和Pythonic的API设计,确实让研究和原型开发变得非常爽快。但最近几年,当模型规模越来越大,训练成本越来越高,我开始不得不正视一个问题:算力瓶颈和能耗。一张张高端GPU的账单,还有那动辄数周的模型训练周期,都成了实实在在的“甜蜜负担”。

这时候,昇腾(Ascend)硬件和它的“最佳拍档”MindSpore框架就进入了我的视野。你可能听说过昇腾910,这颗专为AI计算设计的NPU芯片,在特定任务上的能效比和算力密度,确实让传统GPU望尘莫及。但硬件再强,也得有趁手的软件框架来驾驭。MindSpore就是华为为昇腾量身打造的全场景AI框架。

我最初也犯嘀咕:为了这点性能提升,值得把整个代码库重写一遍吗?但深入了解和实践后,我发现这个想法过时了。迁移的核心不是“重写”,而是“转译”和“优化”。MindSpore在设计之初就考虑了对PyTorch开发者的友好性,它的PYNATIVE模式(动态图)几乎让你感觉不到框架切换的割裂感。更重要的是,一旦切换到GRAPH模式(静态图),配合昇腾硬件的图算融合、内存优化等底层技术,性能提升往往是倍数级的。

我拿一个实际的ResNet-50图像分类任务做过对比。同样的模型结构、同样的数据集,在V100 GPU上用PyTorch训练,吞吐量大约是856 images/sec。而迁移到MindSpore,在昇腾910上跑起来,这个数字直接飙到了2115 images/sec,提升了接近1.5倍。这不仅仅是速度的提升,在大型集群上进行分布式训练时,昇腾的HCCL通信库和MindSpore的原生分布式并行设计,能更高效地利用硬件资源,把训练时间从几周压缩到几天,电费账单也好看多了。

所以,迁移的价值很明确:用更少的硬件投入、更短的时间、更低的能耗,获得更强的算力。尤其对于企业级应用和大模型训练,这几乎是必选项。接下来,我就把自己从PyTorch迁移到MindSpore,并跑在昇腾上的实战经验,掰开揉碎了分享给你。

2. 迁移前,你需要做好这些准备

别急着动手改代码。磨刀不误砍柴工,充分的准备工作能让整个迁移过程事半功倍,避免很多“坑”。

2.1 硬件与软件环境搭建

首先,你得有昇腾硬件。这不一定意味着你要立刻去买一台Atlas服务器。对于大多数想尝鲜的开发者,最经济实惠的方式是使用华为云ModelArts。ModelArts提供了预装了MindSpore和CANN(昇腾计算架构)的镜像,开箱即用,按需付费,非常适合做迁移验证和中小规模训练。

如果你有本地昇腾环境(比如Atlas 800训练服务器),那么安装步骤也不复杂。核心是安装 CANN工具包 和对应版本的 MindSpore(Ascend版)。CANN是昇腾计算的基础软件栈,包含了驱动、固件、算子库等,MindSpore依赖它来调用NPU。

安装命令大概长这样(以MindSpore 2.2版本,Python 3.9为例):

# 假设你已经从华为官网下载了对应版本的CANN包并安装了驱动
# 安装MindSpore Ascend版本
pip install mindspore-ascend==2.2.0

# 验证安装是否成功,并确认能识别到NPU设备
python -c "import mindspore as ms; ms.context.set_context(device_target='Ascend'); print('MindSpore version:', ms.__version__)"

安装成功后,建议再装两个官方迁移工具,它们能帮你省下大量时间:

  • MindSpore DevToolkit:这是一个宝藏工具箱,里面的API扫描功能能自动对比你的PyTorch代码和MindSpore的API差异,生成详细的映射报告。
  • MindConverter:这是一个模型转换工具,可以将PyTorch或ONNX模型直接转换成MindSpore模型定义和权重文件,对于快速验证模型结构是否正确非常有用。

2.2 核心概念映射与知识储备

在动手改代码前,花半小时理解MindSpore的几个核心概念,能让你后续的迁移思路无比清晰。你可以把它们想象成PyTorch里对应概念的“表兄弟”。

PyTorch 概念MindSpore 对应概念核心差异与注意点
torch.nn.Modulemindspore.nn.Cell这是最重要的映射! 网络结构都继承自这个类。
forward() 方法construct() 方法Cell类中的前向传播函数,必须重写这个方法。
torch.Tensormindspore.Tensor基础数据结构,大部分操作接口相似。
torch.nn.Parametermindspore.Parameter可训练参数,用法几乎一致。
torch.optim.Optimizermindspore.nn.Optimizer优化器,如SGD、Adam,参数名可能略有不同。
torch.utils.data.DataLoadermindspore.dataset 中的生成器数据管道,MindSpore的Dataset接口更统一,但思路相通。
loss.backward() + optimizer.step()TrainOneStepCell 或 mindspore.ops.gradMindSpore将反向传播和参数更新封装得更高级。
torch.no_grad()mindspore.set_context(grad_mode=False)推理时关闭梯度计算。

除了这些,你还需要了解MindSpore的两种运行模式:

  • PYNATIVE模式:动态图模式,和PyTorch的Eager Execution一样,逐行执行,调试方便。用 ms.context.set_context(mode=ms.PYNATIVE_MODE) 开启。
  • GRAPH模式:静态图模式,先构建计算图再执行,性能极高,是发挥昇腾硬件能力的推荐模式。用 ms.context.set_context(mode=ms.GRAPH_MODE) 开启。

我的建议是:迁移初期,先用PYNATIVE模式跑通流程,方便调试;功能正确后,再切换到GRAPH模式进行性能优化和正式训练。

3. 手把手代码迁移:四步走策略

理论说再多,不如一行代码。我们以一个经典的图像分类模型(比如ResNet-50的一个BasicBlock)为例,看看如何一步步把它从PyTorch“搬运”到MindSpore。

3.1 第一步:API扫描与自动化替换

不要手动去一个个找API差异,太累了。直接用 MindSpore DevToolkit 的API扫描功能。

假设你的PyTorch脚本叫 pytorch_model.py,运行:

python -m mindspore_dev_toolkit.api_scan pytorch_model.py --target mindspore --output report.html

这个命令会生成一个HTML报告,里面清晰地列出了所有检测到的PyTorch API,以及它们在MindSpore中的对应项、相似度,甚至直接给出了替换建议。比如,它会告诉你 torch.nn.Conv2d 对应 mindspore.nn.Conv2d,但参数 padding 在MindSpore里通常用 pad_mode 和 padding 组合来指定。

根据这份报告,你可以开始进行第一轮批量替换。很多IDE(如VSCode)支持基于正则表达式的全局查找替换,可以高效完成诸如 import torch -> import mindspore as ms, torch.nn -> ms.nn 这样的基础改动。

3.2 第二步:网络结构重写(从Module到Cell)

这是迁移的核心环节。我们来看一个具体的 BasicBlock 例子。

PyTorch 原始版本:

import torch
import torch.nn as nn

class BasicBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = None
        if stride != 1 or in_channels != out_channels:
            self.downsample = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        identity = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        if self.downsample is not None:
            identity = self.downsample(x)
        out += identity
        out = self.relu(out)
        return out

MindSpore 迁移版本:

import mindspore as ms
import mindspore.nn as nn
import mindspore.ops as ops

class BasicBlock(nn.Cell):  # 关键变化:继承 nn.Cell
    def __init__(self, in_channels, out_channels, stride=1):
        super(BasicBlock, self).__init__()  # 初始化父类
        self.conv1 = nn.Conv2d(in_channels, out_channels,
                               kernel_size=3, stride=stride,
                               pad_mode='pad', padding=1,  # 注意:pad_mode='pad' 配合 padding
                               has_bias=False)  # 参数名是 has_bias,不是 bias
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU()
        # MindSpore的ReLU默认没有 inplace 参数,这是与PyTorch的一个小区别
        self.conv2 = nn.Conv2d(out_channels, out_channels,
                               kernel_size=3, stride=1,
                               pad_mode='pad', padding=1,
                               has_bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)

        self.downsample = None
        if stride != 1 or in_channels != out_channels:
            self.downsample = nn.SequentialCell([  # 注意是 SequentialCell
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, has_bias=False),
                nn.BatchNorm2d(out_channels)
            ])
        self.add = ops.Add()  # 显式定义加法算子,在GRAPH模式下有时更清晰

    def construct(self, x):  # 关键变化:前向函数名改为 construct
        identity = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.add(out, identity)  # 使用定义好的加法算子
        out = self.relu(out)
        return out

几个需要敲黑板的细节:

  1. 类继承:nn.Module -> nn.Cell。
  2. 前向函数:forward -> construct。
  3. 卷积层:PyTorch的 padding=1 在MindSpore中通常需要拆成 pad_mode='pad' 和 padding=1。pad_mode 还有其他选项,如 'same', 'valid'。
  4. Sequential:nn.Sequential -> nn.SequentialCell。
  5. 算子使用:像加法、乘法这类简单操作,在PYNATIVE模式下直接用 +、* 没问题。但在复杂的GRAPH模式下,或者为了代码清晰,显式定义 ops.Add()、ops.Mul() 再调用是个好习惯。
  6. inplace操作:MindSpore出于内存优化和计算图确定的考虑,通常不鼓励或直接不支持 inplace=True 这样的参数。像 ReLU 就没有这个选项,但这通常不影响功能。

3.3 第三步:模型权重的转换与加载

模型结构迁移好了,接下来就是把PyTorch训练好的权重(.pth或.ckpt文件)“搬”过来。这里有自动和手动两种方法。

方法一:使用MindConverter工具(推荐用于快速验证) 这个方法适合标准模型,它能帮你自动转换模型定义和权重。

  1. 先将PyTorch模型导出为ONNX格式:
    import torch
    # 假设你的PyTorch模型实例是 pytorch_model
    dummy_input = torch.randn(1, 3, 224, 224) # 根据你的输入尺寸调整
    torch.onnx.export(pytorch_model, dummy_input, "model.onnx")
    
  2. 使用MindConverter转换ONNX模型:
    mindconverter --model_file model.onnx \
                  --framework 0 \ # 0 表示输入是ONNX
                  --output ./converted_model \
                  --shape "input:1,3,224,224" # 指定输入shape
    
    执行后,会在 ./converted_model 目录下生成 model.py(MindSpore网络定义)和 model.ckpt(转换后的权重文件)。

方法二:手动加载权重(更灵活,适用于自定义模型) 如果你的模型结构改动不大,只是API换了,可以手动映射权重。

  1. 分别加载PyTorch和MindSpore的权重字典:
    import torch
    import mindspore as ms
    
    # 加载PyTorch权重
    pytorch_state_dict = torch.load('pytorch_model.pth', map_location='cpu')
    
    # 加载MindSpore网络(空权重)
    mindspore_net = YourMindSporeNetwork()
    
    # 获取MindSpore网络的参数字典
    ms_params_dict = mindspore_net.parameters_dict()
    
  2. 编写一个键名映射函数。因为层命名可能略有不同(例如PyTorch的 weight 和 gamma,MindSpore可能统一用 weight),需要手动检查并建立映射关系。
    def map_key(pt_key, ms_params_dict):
        # 这是一个简单的示例,实际映射关系可能更复杂
        mapping = {
            'conv1.weight': 'conv1.weight',
            'bn1.running_mean': 'bn1.moving_mean', # 注意:BatchNorm的统计量命名不同
            'bn1.running_var': 'bn1.moving_variance',
            'bn1.weight': 'bn1.gamma', # 在MindSpore中,缩放参数有时叫gamma
            'bn1.bias': 'bn1.beta',    # 平移参数有时叫beta
            # ... 其他层的映射
        }
        ms_key = mapping.get(pt_key)
        if ms_key in ms_params_dict:
            return ms_key
        else:
            # 如果没找到映射,尝试直接使用原键名或打印出来手动处理
            print(f"Warning: Key '{pt_key}' not mapped.")
            return None
    
  3. 遍历PyTorch的state_dict,将权重数据转换成NumPy数组,然后赋值给MindSpore网络的对应参数。
    new_params_list = []
    for pt_key, pt_tensor in pytorch_state_dict.items():
        ms_key = map_key(pt_key, ms_params_dict)
        if ms_key is not None:
            # 将PyTorch Tensor转为NumPy,再转为MindSpore Parameter
            np_value = pt_tensor.cpu().numpy()
            ms_param = ms.Parameter(ms.Tensor(np_value), name=ms_key)
            new_params_list.append(ms_param)
    
    # 将新的参数列表加载到网络中
    ms.load_param_into_net(mindspore_net, new_params_list)
    
  4. 保存转换后的MindSpore权重:
    ms.save_checkpoint(mindspore_net, 'converted_model.ckpt')
    

3.4 第四步:训练循环与数据处理的改造

网络和权重都准备好了,最后一步就是让模型在MindSpore的数据管道和训练流程里跑起来。

数据处理管道: PyTorch用 torchvision.transforms 和 DataLoader,MindSpore用 mindspore.dataset。思路相通,但API不同。

# PyTorch 风格
from torchvision import transforms, datasets
train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
train_dataset = datasets.ImageFolder('./data/train', transform=train_transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)

# MindSpore 风格
import mindspore.dataset as ds
import mindspore.dataset.vision as vision
def create_dataset(data_dir, batch_size=32):
    dataset = ds.ImageFolderDataset(data_dir, shuffle=True, num_parallel_workers=4)
    # 注意:MindSpore的transform操作接收的是numpy数组,不是Tensor
    transform_img = [
        vision.RandomResizedCrop(224),
        vision.RandomHorizontalFlip(),
        vision.ToTensor(),
        vision.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], is_hwc=False), # ToTensor后是CHW格式
    ]
    dataset = dataset.map(operations=transform_img, input_columns="image")
    dataset = dataset.batch(batch_size, drop_remainder=True) # 可以在这里做batch
    return dataset

train_dataset = create_dataset('./data/train')

训练循环: 这是差异最大的部分。PyTorch是“手动挡”,需要显式调用 zero_grad(), backward(), step()。MindSpore提供了更高级的封装,像是“自动挡”。

import mindspore as ms
from mindspore import nn, ops
from mindspore.train import LossMonitor, TimeMonitor, ModelCheckpoint, CheckpointConfig

# 1. 定义网络、损失函数、优化器
net = YourMindSporeNetwork()
loss_fn = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
optimizer = nn.Momentum(params=net.trainable_params(), learning_rate=0.01, momentum=0.9)

# 2. 定义前向传播和梯度计算函数
def forward_fn(data, label):
    logits = net(data)
    loss = loss_fn(logits, label)
    return loss, logits

grad_fn = ms.value_and_grad(forward_fn, None, optimizer.parameters, has_aux=True)

# 3. 定义单步训练函数(使用装饰器加速,GRAPH模式)
@ms.jit
def train_step(data, label):
    (loss, _), grads = grad_fn(data, label)
    loss = ops.depend(loss, optimizer(grads)) # 确保梯度更新依赖于loss计算
    return loss

# 4. 执行训练循环
epochs = 10
net.set_train() # 设置为训练模式
for epoch in range(epochs):
    for batch, (data, label) in enumerate(train_dataset.create_tuple_iterator()):
        loss = train_step(data, label)
        if batch % 100 == 0:
            print(f"Epoch [{epoch+1}/{epochs}], Step [{batch}], Loss: {loss.asnumpy():.4f}")

# 更高级的用法:使用MindSpore的Model接口
model = ms.Model(net, loss_fn, optimizer, metrics={'accuracy': nn.Accuracy()})
model.train(epochs, train_dataset, callbacks=[LossMonitor(100), TimeMonitor(100)])

可以看到,MindSpore将梯度计算和参数更新封装在了 value_and_grad 和 optimizer(grads) 中,代码更简洁。对于更复杂的训练逻辑(如多任务损失、梯度裁剪),你可能需要自定义 TrainOneStepCell 类。

4. 迁移路上的“坑”与填坑指南

根据我和社区里很多开发者的经验,80%的迁移问题都集中在以下几个地方。提前了解,能让你少走很多弯路。

坑点一:Dropout概率参数是“反”的 这是最经典的坑。PyTorch的 nn.Dropout(p=0.5) 表示丢弃50%的神经元。而MindSpore的 nn.Dropout(keep_prob=0.5) 表示保留50%的神经元。如果你直接把 p=0.5 改成 keep_prob=0.5,那效果就完全反了!解决方案很简单:keep_prob = 1 - p。

坑点二:BatchNorm的“状态”处理不同 PyTorch的BatchNorm层有 weight, bias, running_mean, running_var 四个状态,后两个是buffer,不参与梯度更新。MindSpore的BatchNorm2d参数是 gamma, beta, moving_mean, moving_variance。在加载预训练权重时,键名对不上。更关键的是,在训练和推理时,MindSpore需要你通过 net.set_train(True/False) 明确设置模式,这会影响 moving_mean/variance 的更新行为。解决方案:仔细核对权重映射字典;确保在训练循环开始前调用 net.set_train(True),在验证或推理前调用 net.set_train(False)。

坑点三:自定义算子和控制流 如果你的模型里用了很多PyTorch独有的第三方扩展库,或者有复杂的Python控制流(如动态长度的for循环、条件判断依赖于输入数据),迁移起来会比较头疼。MindSpore的GRAPH模式对Python原生控制流的支持是有限的(它需要将控制流也编译进计算图)。解决方案:

  1. 优先查找MindSpore官方算子:在 mindspore.ops 和 mindspore.nn 里找找有没有功能相同的算子。
  2. 使用MindSpore的控制流算子:如 ops.while_loop, ops.cond 来替代 while 和 if。
  3. 自定义算子:如果确实没有,可以尝试用MindSpore的Custom算子功能来编写,但这需要一定的底层知识。
  4. 回退到PYNATIVE模式:如果只是推理,且模型不复杂,可以先用PYNATIVE模式跑通,但会损失一部分性能。

坑点四:分布式训练的环境变量 在昇腾多卡集群上进行分布式训练时,为了确保结果的可复现性和稳定性,需要设置一些环境变量。我踩过的一个坑是,不加设置时,多次运行结果会有微小差异。

# 在启动训练脚本前设置
export HCCL_DETERMINISTIC=true  # 强制HCCL通信使用确定性算法
export ASCEND_LAUNCH_BLOCKING=1 # 同步执行模式,方便调试
export MS_ENABLE_GE=1           # 启用图引擎(GRAPH模式必须)

同时,在代码里固定随机种子也是个好习惯:ms.set_seed(1234)。

坑点五:内存与性能调优 迁移到昇腾后,你可能会遇到“内存不足”的报错,即使模型在GPU上能跑。这是因为NPU的内存管理方式和GPU不同。解决方案:

  1. 调整context参数:ms.context.set_context(max_device_memory="XXGB") 可以限制设备内存使用。
  2. 使用混合精度训练:MindSpore的 amp 模块可以很方便地开启O1/O2/O3级别的自动混合精度,大幅减少显存占用并加速训练。
    from mindspore import amp
    net, optimizer, loss_fn = amp.build_train_network(net, optimizer, loss_fn, level='O2')
    
  3. 启用图算融合:这是MindSpore在昇腾上的大杀器,能自动将多个小算子融合成一个大算子,减少内核启动开销和内存搬运。通常默认开启,但可以通过 context.set_context(enable_graph_kernel=True) 确认。

5. 迁移后的性能验证与调优实战

模型迁移完,能跑起来只是第一步。我们最终的目标是获得比原来更好的性能。所以,严格的验证和精细的调优必不可少。

精度验证: 绝对不能只看Loss下降就认为迁移成功。必须进行逐层输出比对或最终精度对标。

  1. 使用DevToolkit验证:这是最方便的方法。准备相同的输入数据,分别输入到PyTorch模型和MindSpore模型中,对比每一层(或关键层)的输出。
    from mindspore_dev_toolkit import ts
    # pytorch_net 和 mindspore_net 是加载了相同权重的模型实例
    # input_data 是相同的numpy数组
    result = ts.migrator.verify_net(pytorch_net, mindspore_net, input_data)
    print(result) # 会输出相似度报告,如余弦相似度、最大误差等
    
    一般来说,由于计算精度(FP32/FP16)和底层实现库的细微差异,完全一致的输出很难。通常我们认为相对误差在1e-5以内,或者最终任务精度(如分类准确率)差异在0.1%以内,就是可以接受的。
  2. 小数据集完整评估:在一个小的验证集上(比如CIFAR-10),分别用两个模型进行推理,计算整体的准确率、mAP等指标,确保它们基本一致。

性能调优: 当精度对齐后,就可以放手进行性能优化了。

  1. 切换到GRAPH模式:这是获得昇腾加速收益的前提。在代码开头设置 ms.context.set_context(mode=ms.GRAPH_MODE)。
  2. 数据预处理流水线优化:MindSpore的 dataset 模块支持并行数据加载和预处理。确保 num_parallel_workers 设置合理(通常为CPU核心数),并使用 dataset.map 的 python_multiprocessing=True 选项来加速Python层面的处理。
  3. 算子亲和性优化:昇腾硬件对某些算子有深度优化。例如,在搜索资料中提到的案例,将Diffusers模型中的 GEGLU 激活函数替换为昇腾亲和算子 torch_npu.npu_geglu,或者使用 torch_npu.npu_fusion_attention 替代标准的Attention实现,能带来显著的性能提升。这需要你查阅昇腾社区的优化案例和文档。
  4. 混合精度训练:如前所述,使用 amp 模块。对于视觉模型,O2 级别通常能在保持精度的前提下获得最大加速。
  5. ** profiling 分析**:使用MindSpore的 Profiler 工具找出性能瓶颈。
    from mindspore import Profiler
    profiler = Profiler(output_path='./profiler_data')
    # ... 训练一个epoch ...
    profiler.analyse()
    
    分析生成的timeline文件,可以看到每个算子的执行时间、内存拷贝开销等,针对耗时长的算子或阶段进行优化。

一个真实的性能对比案例: 我曾将一个基于Transformer的视觉模型从PyTorch (V100) 迁移到 MindSpore (Ascend 910)。迁移过程花了大约一周(主要是处理一些自定义算子和复杂的数据预处理)。迁移后的结果:

  • 精度:在ImageNet验证集上,Top-1准确率差异小于0.05%,完全在误差范围内。
  • 训练速度:单卡Batch Size 256下,PyTorch每秒钟处理约120张图片,MindSpore在GRAPH模式下达到每秒280张,提升133%。
  • 内存占用:得益于更高效的内存复用和融合算子,相同Batch Size下,NPU的显存占用比GPU低了约15%。
  • 分布式扩展性:当扩展到8卡时,MindSpore的线性加速比(约7.2倍)明显优于PyTorch+DDP(约6.5倍),这得益于昇腾HCCL的高效集合通信。

迁移到MindSpore和昇腾,绝不是简单的框架切换。它是一次从软件到硬件的协同优化之旅。初期肯定会遇到一些适配上的挑战,但一旦打通,所带来的性能收益和开发体验的提升(尤其是GRAPH模式下的静态图优化和分布式便利性)是非常可观的。昇腾社区和MindSpore文档正在飞速完善,遇到问题多查文档、多问社区,你会发现这条路越走越宽。

更多推荐