迁移第一步:搞清楚环境依赖

迁移之前,先确认你的环境和依赖版本。很多问题其实不是代码的问题,是环境没装对。

昇腾 NPU 的 PyTorch 支持依赖两样东西:torch_npu 插件和 CANN 驱动。这两个版本必须匹配,匹配表在昇腾官网有,但经常有人装错了版本然后花几天时间查 bug。

一个最直接的验证方法:

import torch
import torch_npu

print(torch.__version__)          # 你的 PyTorch 版本
print(torch_npu.__version__)      # torch_npu 版本
print(torch.npu.is_available())  # 必须是 True
print(torch.npu.get_device_name(0))

如果 is_available() 返回 False,先检查驱动和 CANN 是否装好,不要急着改代码。

还有一个容易忽略的点:昇腾 NPU 的驱动和 CANN 版本要跟你的硬件型号匹配。Ascend 910、Ascend 910 Pro、Ascend 910B、Ascend 910D……型号不同,能跑的算子和性能表现差异很大。买卡的时候最好问清楚具体型号,对应去昇腾社区下载正确的驱动包。

坑一:某些算子 NPU 上没有

这是最容易碰到的第一道坎。你的 PyTorch 代码里可能用了某个算子,GPU 上跑得好好的,但 NPU 上直接报 NotImplementedError 或者压根没报错但结果不对。

比如 torch.nanmeantorch.scatter_addtorch.gather 的某些用法,这些在昇腾 NPU 上不一定有原生实现。

解法:迁移之前先用 torch_npu 提供的能力映射检查工具扫一遍你的模型,把不支持的算子先列出来:

from torch_npu.utils.path_manager import PathManager
# 或者直接跑模型的转化,看报什么错

# 一个更直接的方法:强制把模型搬到 NPU 上跑一步
model = model.npu()  # 如果某个算子不支持,这里就会炸

如果发现不支持的算子,有三条路:

  1. 换算子:找一个等价的 NPU 支持的算子组合来替代,比如 scatter_add 可以用 index_add 来模拟
  2. 降级处理:把这个算子留在 CPU 上跑,数据搬进搬出虽然慢一点,但能跑通
  3. 用 Ascend C 自定义算子:最耗时但最彻底,适合高频使用的关键算子

第三条路门槛高,建议先把模型跑通再考虑,不要一开始就奔着写自定义算子去。

坑二:动态shape处理方式不同

PyTorch 动态图很灵活,你想传什么 shape 的 tensor 进去都行。但 GE 图引擎(负责编译的那一层)处理动态 shape 的能力和 GPU 不太一样,尤其是带控制流的模型(if/else、循环)。

一个典型的坑:训练阶段 batch_size 可以随意变化,但转成推理模型之后就固定了。ATC 转模型的时候如果用了 --input_shape 指定固定 shape,生产环境里传进去的 shape 一旦不一样就直接崩。

解法

如果你的模型 shape 是固定的,直接在 ATC 转换时指定:

atc --model=model.onnx \
    --framework=5 \
    --output=model_npu \
    --soc_version=Ascend910 \
    --input_shape="actual_input_1:1,3,224,224"

如果 shape 本身会变化(比如 NLP 任务里句子长度不一致),有几个选择:

  • padding 到固定最大长度,用 mask 机制处理
  • 用 --dynamic_batch_size 或 --dynamic_image_size 参数开启动态档位
  • 保留动态图推理方式,不转离线模型(性能会差一些,但灵活)

坑三:混合精度藏着显存炸弹

AMP(自动混合精度)是 PyTorch 训练加速的标配,GPU 上用得飞起。但昇腾 NPU 上用 AMP 有几个不一样的地方,最坑的是 loss scale 的处理逻辑不同

GPU 上的 AMP 如果遇到梯度变成 NaN,会自动降 loss scale 继续跑。但昇腾 NPU 上的 torch_npu.npu.amp 实现这套逻辑的方式不太一样,有时候梯度炸了你不会马上收到报错,等到发现的时候训练已经白跑了很久。

解法:不要裸用 AMP,先把 FP32 跑通,再开 AMP,加上梯度裁剪(gradient clipping)做保护:

from torch.npu.amp import GradScaler

scaler = GradScaler()

for data, target in dataloader:
    optimizer.zero_grad()
    
    # 自动混合精度前向传播
    with torch.npu.amp.autocast():
        output = model(data)
        loss = criterion(output, target)
    
    # 缩放 loss,反向传播
    scaler.scale(loss).backward()
    
    # 梯度裁剪,防止爆炸
    scaler.unscale_(optimizer)
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
    
    scaler.step(optimizer)
    scaler.update()

另一个显存相关的坑:torch_npu 的显存分配策略和 CUDA 不太一样,默认的显存池大小可能不够用。如果是训练大模型,启动之前加一行:

torch.npu.set_per_process_memory_fraction(0.9)  # 留 10% 给系统

坑四:多卡训练 NCCL 换成 HCCL

单机多卡或者多机多卡训练的时候,GPU 上用的是 NCCL(NVIDIA Collective Communication Library)。昇腾 NPU 用的是 HCCL(Huawei Collective Communication Library),API 几乎一样,但有一些细节差异。

最容易踩的坑是多卡启动方式。GPU 上很多人用 torchrun 或者 torch.distributed.launch,昇腾 NPU 也支持这两个工具,但需要在环境变量里指定 HCCL 相关的配置:

# HCCL 多卡启动示例
 HCCL_WHITELIST_DISABLE=1 \
 OMP_NUM_THREADS=16 \
 torchrun \
     --nproc_per_node=8 \
     --nnodes=1 \
     train.py

还有个坑是通信后端。代码里如果写了 torch.distributed.Backend.NCCL,在 NPU 上要改成 torch.distributed.Backend.GLOO 或者直接用默认后端(torch_npu 会自动选 HCCL)。硬编码 NCCL 后端是最常见的踩坑点。

多卡训练时另一个常踩的是梯度同步时机。PyTorch DDP 的梯度同步是在反向传播过程中自动做的,但如果你在自定义训练逻辑里手动处理梯度,忘了调用 model.backward() 之后的同步,多卡训练出来的模型权重就是错的。这种 bug 很难发现,因为单卡跑完全正常。

坑五:模型转 .om 之后行为不一致

很多团队做完模型迁移,最后一步是用 ATC 把 PyTorch/ONNX 模型转成 .om 离线模型上线。结果转完之后发现:精度跟 PyTorch 原版不一样了

这个问题的根源通常是图融合引入的数值误差。GE 在做算子融合的时候,可能会改变计算顺序或者用近似算法替代某些操作,积累下来会影响到最终输出的数值。

解法

  1. 转模型之前,先用 PyTorch 跑一遍测试集,记录输出作为 baseline
  2. 转换时加 --log=info 看看 GE 做了哪些融合,记录关键步骤
  3. .om 模型跑同样的测试集,对比输出差异
  4. 如果差异超过可接受范围,用 --graph_engine_mode 参数调整融合策略

还有个更常见的坑是归一化处理的位置。有些模型的预处理(图像归一化、数据标准化)是在 PyTorch 代码里做的,ATC 转模型的时候没把这部分包进去。转完模型之后预处理还在 CPU 上跑,推理在 NPU 上跑,数据在 CPU 和 NPU 之间来回搬,既慢又容易出 bug。检查一下你的预处理逻辑,最好跟模型一起转进去。

迁移检查清单

总结一下迁移过程中要逐项检查的点:

环境检查

  •  torch.npu.is_available() 返回 True
  •  CANN 版本和驱动版本匹配
  •  确认硬件具体型号(Ascend 910/910Pro/910B/910D)

模型兼容性检查

  •  跑一遍不支持算子扫描
  •  确认 shape 处理方式(固定 vs 动态)
  •  检查归一化/预处理逻辑是否需要一起迁移

训练稳定性检查

  •  先用 FP32 跑通,再开 AMP
  •  加上梯度裁剪
  •  显存 fraction 设置合理

多卡检查

  •  后端改成 HCCL 或默认
  •  启动方式配置正确
  •  确认梯度同步时机

部署检查

  •  PyTorch 版本和 .om 版本输出对比精度
  •  预处理逻辑是否已集成到 .om

总结

PyTorch 到昇腾 NPU 的迁移,说难不难,说简单也不简单。最大的坑往往不是某个单一问题,而是多个小问题叠加在一起。

记住几条核心原则:

先跑通再优化。不要一上来就想着 AMP、多卡、离线模型,先用最简单的方式把模型在 NPU 上跑通,验证端到端流程没问题,再逐步加高级特性。

版本匹配是地基。环境问题排查起来最费时间,装对版本能省很多后续的麻烦。

梯度裁剪保平安。NPU 的混合精度和 CUDA 略有差异,加梯度裁剪是低成本高收益的操作。

多卡换 HCCL 是必做项。改一行后端代码的事情,但漏了就白跑。

昇腾 NPU 的生态这几年成熟了很多,大部分常见模型迁移过来问题不大。踩坑不可怕,踩多了就知道门道在哪了。

本篇文章涉及的相关仓库

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐