目录

昇腾迁移适配

1.1 torch.cuda → torch.npu 逐项替换清单

1.1.1 设备管理类

1.1.2 内存管理类

1.1.3 张量操作类

1.2 代码迁移三步策略

方案A:torch_npu 零侵入替换(推荐)

1.3 常见迁移陷阱与解决方案

陷阱1:pin_memory=True 导致错误

陷阱2:DataParallel 不兼容

陷阱3:自定义 CUDA Extension

陷阱4:torch.compile 需要替换

陷阱5:模型保存与加载跨平台

1.4 迁移前后代码 diff 示例

train.py 迁移前后 diff

distributed_train.py 迁移前后 diff

二、模型训练适配

2.1 训练脚本改造要点

2.1.1 数据加载流水线适配

2.1.3 优化器与学习率调度

2.2 混合精度训练配置

2.2.1 torch.npu.amp 完整配置示例

2.2.2 精度敏感层白名单/黑名单策略

2.3 分布式训练适配(DDP → HCCL)

2.3.1 init_process_group(backend=‘hccl’)

2.3.2 DistributedSampler 调整

2.3.3 HCCL 通信环境变量调优

2.4 断点续训与模型保存/加载

2.4.1 state_dict 跨平台兼容性

2.4.2 map_location 处理

三、关键特性适配

3.1 算子适配

3.1.1 标准算子 —

3.1.2 自定义 CUDA 算子 → TBE / DSL 重写

3.1.3 算子替换对照表

3.2 推理特性适配

3.2.1 静态 Shape 推理优化

3.2.3 KV Cache 管理与显存优化

3.3 量化与压缩

3.3.1 AMCT(昇腾模型压缩工具)替代 bitsandbytes

3.3.2 INT8 / INT4 量化流程

3.3.3 量化精度回退机制

3.4 推理框架适配(可选)

3.4.1 推理框架对比

3.4.2 OM 模型导出(ONNX → OM)

3.5 算子性能调优

3.5.1 算子融合策略

3.5.2 算子缓存优化

3.5.3 亲和 API 替换(承接迁移分析)

精度调试 — 学习笔记

一、精度问题根因分析

1.1 精度差异的三大来源

来源一:算子实现差异

来源二:数据类型差异

来源三:随机性差异

1.2 精度问题分类速查

二、精度调试工具与方法

2.1 逐层精度比对(核心方法)

方案A:Hook + 固定输入(最通用,推荐)

方案B:adc precision_compare(华为官方工具)

方案C:对比日志自动分析脚本

2.2 算子级 Dump 调试

ASCEND_OP_DUMP 环境变量配置

2.3 确定性调试

固定随机种子最佳实践

HCCL_DETERMINISTIC 配置

⚠️ 确定性模式对性能的影响

2.4 精度比对指标与阈值

核心指标

各精度模式下的可接受阈值

三、常见精度问题修复手册

3.1 LayerNorm 精度偏差

3.2 Softmax 精度偏差

3.3 fp16 下梯度溢出

3.4 注意力机制精度偏差

3.5 随机性不一致

四、精度调试流程

4.1 快速排查(5分钟)

4.2 系统排查(1-2小时)

4.3 长期稳定性验证(按需)

性能调优 — 学习笔记

一、性能分析工具与方法

1.1 昇腾 Profiling 工具

msprof:时间线分析(对标 NVIDIA nsys)

msvp:算子级性能指标(对标 NVIDIA ncu)

二、计算性能优化

2.1 算子优化

亲和 API 替换(承接迁移分析结果)

算子合:减少 kernel launch 次数

2.2 算子缓存优化

2.3 图模式编译

2.4 混合精度性能调优

三、显存优化

3.1 显存分析与监控

3.2 训练显存优化

Batch Size 阶梯搜索

梯度检查点(Gradient Checkpointing)

ZeRO 优化器状态分片

3.3 推理显存优化

KV Cache 优化

连续批处理(Continuous Batching)

四、数据流水线优化

4.1 数据加载瓶颈分析

num_workers 最优值搜索

4.2 数据搬运优化

Host-Device 传输

五、分布式性能优化

5.1 HCCL 通信优化

HCCL 环境变量调优清单

通信拓扑感知与亲和性设置

通信-计算重叠策略

5.2 分布式策略选择

5.3 多卡扩展性评估

六、推理性能优化(可选)

6.1 静态 Shape 推理最佳实践

七、性能验收与持续优化

7.1 性能验收清单

7.2 性能优化跟踪表

昇腾迁移适配

概述: 脚本迁移是整个迁移工作的第一步,也是改动量最大的一步。核心思路是将所有 torch.cuda.xxx 调用替换为 torch.npu.xxx。昇腾提供了 torch_npu 扩展库,API 设计高度对标 PyTorch 原生接口,大部分替换可以做到"一行改"。


1.1 torch.cuda → torch.npu 逐项替换清单

1.1.1 设备管理类
原写法 (GPU) 替换后 (NPU) 说明
torch.cuda.is_available() torch.npu.is_available() 返回 NPU 是否可用
torch.cuda.device_count() torch.npu.device_count() NPU 设备数量
torch.cuda.set_device(0) torch.npu.set_device(0) 设置当前设备
torch.cuda.current_device() torch.npu.current_device() 当前设备索引
torch.cuda.device(0) torch.npu.device(0) 设备上下文管理器
torch.device('cuda') torch.device('npu') 设备对象
torch.device('cuda:0') torch.device('npu:0') 指定设备号
1.1.2 内存管理类
原写法 (GPU) 替换后 (NPU) 说明
torch.cuda.memory_allocated() torch.npu.memory_allocated() 当前显存使用量
torch.cuda.max_memory_allocated() torch.npu.max_memory_allocated() 峰值显存量
torch.cuda.empty_cache() torch.npu.empty_cache() 清空缓存
torch.cuda.reset_peak_memory_stats() torch.npu.reset_peak_memory_stats() 重置峰值统计
torch.cuda.memory_summary() torch.npu.memory_summary() 显存摘要报告
torch.cuda.set_per_process_memory_fraction(0.9) torch.npu.set_per_process_memory_fraction(0.9) 显存上限限制
1.1.3 张量操作类
原写法 (GPU) 替换后 (NPU) 说明
tensor.to('cuda') tensor.to('npu') 张量迁移到 NPU
tensor.cuda() tensor.npu() 张量迁移到 NPU
tensor.is_cuda tensor.is_npu 判断是否在 NPU 上
torch.zeros(3,3).cuda() torch.zeros(3,3).npu() 创建 NPU 张量
torch.randn(3,3, device='cuda') torch.randn(3,3, device='npu') 指定 device 参数

1.2 代码迁移三步策略

方案A:torch_npu 零侵入替换(推荐)

核心思想: 在代码开头导入 torch_npu,它会自动向 PyTorch 注册 NPU 后端。之后只需将 'cuda' 替换为 'npu' 即可。

# 迁移前(GPU 代码)
import torch

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = Model().to(device)
data = data.to(device)

# 混合精度
scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
    output = model(data)
    loss = loss_fn(output, target)

# 迁移后(NPU 代码)—— 只需两处改动
import torch
import torch_npu                      # ← 新增:导入昇腾扩展

device = torch.device('npu' if torch.npu.is_available() else 'cpu')  # ← cuda → npu
model = Model().to(device)
data = data.to(device)

# 混合精度
scaler = torch.npu.amp.GradScaler()   # ← cuda → npu
with torch.npu.amp.autocast():        # ← cuda → npu
    output = model(data)
    loss = loss_fn(output, target)

适用场景: 新项目、代码结构清晰、无大量硬编码 'cuda' 字符串的项目。


1.3 常见迁移陷阱与解决方案

陷阱1:pin_memory=True 导致错误
# ❌ 错误写法(NPU 不支持 pin_memory)
dataloader = DataLoader(dataset, batch_size=32, pin_memory=True)

# ✅ 正确写法
dataloader = DataLoader(dataset, batch_size=32, pin_memory=False)

# 或者用条件写法兼容双平台
pin = False if torch.npu.is_available() else True
dataloader = DataLoader(dataset, batch_size=32, pin_memory=pin)

原因: 昇腾 NPU 的 Host-Device 通信机制与 CUDA 不同,不支持 pinned memory 机制。


陷阱2:DataParallel 不兼容
# ❌ NPU 不支持 DataParallel
model = torch.nn.DataParallel(model, device_ids=[0, 1])

# ✅ 必须替换为 DistributedDataParallel
import torch.distributed as dist
dist.init_process_group(backend='hccl', rank=rank, world_size=world_size)
torch.npu.set_device(local_rank)
model = torch.nn.parallel.DistributedDataParallel(
    model, device_ids=[local_rank], output_device=local_rank
)

原因: 昇腾没有对标 CUDA 的 DataParallel 实现。DDP 是推荐的分布式训练方案(GPU 上也一样)。


陷阱3:自定义 CUDA Extension
# ❌ .cu 文件 / CUDAExtension 完全不兼容
from torch.utils.cpp_extension import CUDAExtension
setup(ext_modules=[CUDAExtension('my_op', ['my_op.cu'])])

# ✅ 方案A:用标准 torch API 替代
# ✅ 方案B:用 TBE(Tensor Boost Engine)重写
# ✅ 方案C:用昇腾 DSL 自定义算子

原因: CUDA C++ 代码无法在昇腾上编译。标准 PyTorch 算子通常已有替代,自定义高性能算子需用 TBE/DSL 重写。


陷阱4:torch.compile 需要替换
# ❌ GPU 上的图模式编译
model = torch.compile(model)  # torch 2.0+ 特性

# ✅ NPU 上的图模式编译
import torch_npu
model = torch_npu.compile(model)

原因: torch.compile 底层依赖 Triton,昇腾不支持。torch_npu.compile 使用昇腾的图编译引擎。


陷阱5:模型保存与加载跨平台
# ❌ 在GPU上保存,在NPU上直接加载可能出问题
torch.save(model.state_dict(), 'model.pth')

# ✅ 加载时指定 map_location
model.load_state_dict(
    torch.load('model.pth', map_location='npu')   # 或 map_location='cpu'
)

# ✅ 最佳实践:训练时保存到 CPU 张量(跨平台兼容)
torch.save({k: v.cpu() for k, v in model.state_dict().items()}, 'model.pth')

1.4 迁移前后代码 diff 示例

以一个典型的训练脚本为例,展示逐文件对照:

train.py 迁移前后 diff
 import torch
+import torch_npu
 import torch.nn as nn
 from torch.utils.data import DataLoader

 # 设备初始化
-device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
+device = torch.device('npu' if torch.npu.is_available() else 'cpu')

 # 数据集加载
-dataloader = DataLoader(dataset, batch_size=32, pin_memory=True)
+dataloader = DataLoader(dataset, batch_size=32, pin_memory=False)

 # 模型
 model = MyModel().to(device)

 # 混合精度
-scaler = torch.cuda.amp.GradScaler()
+scaler = torch.npu.amp.GradScaler()

 # 训练循环
 for data, target in dataloader:
     data, target = data.to(device), target.to(device)
-    with torch.cuda.amp.autocast():
+    with torch.npu.amp.autocast():
         output = model(data)
         loss = criterion(output, target)
     scaler.scale(loss).backward()
     scaler.step(optimizer)
     scaler.update()
distributed_train.py 迁移前后 diff
 import torch
+import torch_npu
 import torch.distributed as dist

 # 分布式初始化
 dist.init_process_group(
-    backend='nccl',
+    backend='hccl',
     init_method='env://',
     rank=rank,
     world_size=world_size
 )

 # 设备设置
-torch.cuda.set_device(local_rank)
+torch.npu.set_device(local_rank)
-device = torch.device(f'cuda:{local_rank}')
+device = torch.device(f'npu:{local_rank}')

 model = MyModel().to(device)
 model = DDP(model, device_ids=[local_rank], output_device=local_rank)

二、模型训练适配

概述: 脚本跑通只是第一步。训练适配要解决的是"能不能收敛"的问题——包括数据流水线、混合精度、分布式通信、断点续训、以及收敛性验证。大部分训练逻辑(Loss 计算、反向传播、优化器更新)无需改动,改动集中在环境适配层


2.1 训练脚本改造要点

2.1.1 数据加载流水线适配
# 完整的 NPU 数据流水线配置
dataloader = DataLoader(
    dataset,
    batch_size=32,
    shuffle=True,
    num_workers=4,                # 可保留,CPU 预处理不受影响
    pin_memory=False,             # ❗ 必须关闭
    prefetch_factor=2,            # 预取因子(可选)
    persistent_workers=True,      # 复用 worker 进程(推荐)
)


2.1.2 Loss 计算与反向传播

无需改动。 Loss 计算、loss.backward()optimizer.step() 在 GPU 和 NPU 上使用完全相同的 PyTorch API。

# 这段代码在 GPU 和 NPU 上完全一致
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
optimizer.zero_grad()
2.1.3 优化器与学习率调度

无需改动。 标准 PyTorch 优化器(SGD、Adam、AdamW)和学习率调度器(CosineAnnealingLR、LinearLR 等)在 NPU 上同样支持。

# GPU 和 NPU 完全一致
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)

2.2 混合精度训练配置

2.2.1 torch.npu.amp 完整配置示例
import torch
import torch_npu

# 初始化
device = torch.device('npu')
model = MyModel().to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)

# 混合精度组件
scaler = torch.npu.amp.GradScaler(
    init_scale=2.**16,           # 初始缩放因子
    growth_factor=2.0,           # 成功时的增长倍数
    backoff_factor=0.5,          # 溢出时的衰减倍数
    growth_interval=2000         # 连续成功步数后增长
)

# 训练循环
for batch in dataloader:
    data, target = batch
    data, target = data.to(device), target.to(device)
    
    optimizer.zero_grad()
    
    with torch.npu.amp.autocast():           # 自动混合精度上下文
        output = model(data)
        loss = criterion(output, target)
    
    scaler.scale(loss).backward()            # 缩放损失 → 反传
    scaler.step(optimizer)                   # 反缩放 → 更新参数
    scaler.update()                          # 调整缩放因子
2.2.2 精度敏感层白名单/黑名单策略

某些层在 fp16 下精度下降明显,需要排除在混合精度之外:

class MixedPrecisionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 64, 3)
        self.layer_norm = nn.LayerNorm(64)   # ← 精度敏感层
        self.classifier = nn.Linear(64, 10)
    
    def forward(self, x):
        x = self.conv(x)
        # 方法一:用 autocast(enabled=False) 包裹敏感层
        with torch.npu.amp.autocast(enabled=False):
            x = self.layer_norm(x.float())   # 强制 fp32
        x = self.classifier(x)
        return x


2.2.3 fp16 下常见发散原因与修复

现象 可能原因 修复方案
loss → NaN(早期) fp16 下梯度溢出 增加 init_scale / 敏感层切 fp32
loss → NaN(后期) 学习率过高 降低学习率 / 增加 warmup
loss 缓慢发散 LayerNorm 精度漂移累积 LayerNorm 强制 fp32
准确率低于预期 Softmax 分布偏移 开启 ASCEND_SOFTMAX_OPTIMIZE

2.3 分布式训练适配(DDP → HCCL

2.3.1 init_process_group(backend=‘hccl’)
# 完整的 NPU 分布式训练启动模板
import torch
import torch_npu
import torch.distributed as dist
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP

def train(rank, world_size):
    # 1. 初始化分布式环境
    dist.init_process_group(
        backend='hccl',                    # ← NCCL → HCCL
        init_method='env://',
        rank=rank,
        world_size=world_size
    )
    
    # 2. 设置当前 NPU 设备
    torch.npu.set_device(rank)             # ← cuda → npu
    
    # 3. 模型
    device = torch.device(f'npu:{rank}')
    model = MyModel().to(device)
    model = DDP(model, device_ids=[rank], output_device=rank)
    
    # 4. 数据加载器(分布式)
    sampler = torch.utils.data.distributed.DistributedSampler(
        dataset, num_replicas=world_size, rank=rank
    )
    dataloader = DataLoader(
        dataset, batch_size=32, sampler=sampler,
        pin_memory=False                    # ← 关闭 pin_memory
    )
    
    # 5. 训练循环(与单卡相同)
    for epoch in range(epochs):
        sampler.set_epoch(epoch)
        for data, target in dataloader:
            ...

def main():
    world_size = torch.npu.device_count()
    mp.spawn(train, args=(world_size,), nprocs=world_size)

if __name__ == '__main__':
    main()
2.3.2 DistributedSampler 调整

与 GPU 版本相比,只需确保 pin_memory=False

# GPU 版本
sampler = DistributedSampler(dataset, rank=rank, num_replicas=world_size)
loader = DataLoader(dataset, batch_size=32, sampler=sampler, pin_memory=True)

# NPU 版本(唯一变化)
loader = DataLoader(dataset, batch_size=32, sampler=sampler, pin_memory=False)
2.3.3 HCCL 通信环境变量调优
# HCCL 基础配置
export HCCL_IFACE=eth0                    # 通信网口(根据实际网络接口设置)
export HCCL_INTRA_ROCE_ENABLE=1           # 开启 RoCE 加速(如有 RoCE 网卡)

# 通信超时与容错
export HCCL_CONNECT_TIMEOUT=600           # 建连超时(秒)
export HCCL_EXEC_TIMEOUT=600              # 执行超时

# 通信调优
export HCCL_BUFFER_SIZE=256               # 通信缓冲区大小(MB)
export HCCL_NETWORK_DRIVER=1              # 网络驱动模式
export HCCL_ALGO_RING=1                   # 使用 Ring AllReduce 算法

# 调试(仅调试时开启,生产环境关闭)
export HCCL_DEBUG=INFO                    # 通信调试日志级别

2.4 断点续训与模型保存/加载

2.4.1 state_dict 跨平台兼容性

核心原则: state_dict 中的张量是平台无关的。在 GPU 上训练的参数可以无缝加载到 NPU 模型上,反之亦然。

# ✅ 跨平台保存(推荐保存到 CPU 张量)
checkpoint = {
    'epoch': epoch,
    'model_state_dict': {k: v.cpu() for k, v in model.state_dict().items()},
    'optimizer_state_dict': optimizer.state_dict(),
    'loss': loss,
    'scaler_state_dict': scaler.state_dict(),
}
torch.save(checkpoint, 'checkpoint.pt')
2.4.2 map_location 处理
# 加载时指定 map_location
checkpoint = torch.load('checkpoint.pt', map_location='npu:0')
# 或加载到 CPU 再手动迁移
checkpoint = torch.load('checkpoint.pt', map_location='cpu')
model.load_state_dict(checkpoint['model_state_dict'])
model = model.to('npu:0')

⚠️ 注意事项:

  • 如果 checkpoint 是在 GPU 上保存的,加载时 map_location 会自动处理张量所在的设备
  • 优化器状态可能包含设备相关的缓冲区(如 Adam 的 exp_avg),加载后需确认 optimizer 状态正确
  • 跨平台加载后建议运行 1-2 个 step 验证 loss 正常


三、关键特性适配

概述: 脚本跑通、训练收敛之后,还需要将一些高级特性适配到昇腾平台上。包括算子适配(处理标准算子之外的边缘情况)、推理优化、量化压缩、推理框架替换、以及算子级性能调优。这一章解决的是"能力完整"的问题。


3.1 算子适配

3.1.1 标准算子 —

以下标准 PyTorch 算子在昇腾上已完全验证,可直接使用:

类别 算子 说明
卷积 nn.Conv1d/2d/3dnn.ConvTranspose2d ✅ 已适配 AI Core
归一化 nn.LayerNormnn.BatchNorm1d/2dnn.RMSNorm ✅ Vector Unit 加速
激活 nn.ReLUnn.GELUnn.SiLUnn.Tanh ✅ 融合进激活算子
注意力 nn.MultiheadAttention ✅ 支持
池化 nn.MaxPool2dnn.AvgPool2d ✅ 支持
线性 nn.Linearnn.Bilinear ✅ Cube Unit 加速
嵌入 nn.Embedding ✅ 支持
Dropout nn.Dropoutnn.Dropout2d ✅ 支持
损失 nn.CrossEntropyLossnn.MSELossnn.BCEWithLogitsLoss ✅ 支持
3.1.2 自定义 CUDA 算子 → TBE / DSL 重写

当遇到 PyTorch 标准库中没有覆盖的自定义算子时,需要重写:

# 昇腾自定义算子开发套件
# TBE(Tensor Boost Engine):Python 定义算子,编译为 NPU 指令
# DSL(Domain Specific Language):更高层的描述语言

# TBE 算子示例(custom_add.py)
from te import tvm
from te.platform.cce_conf import api_check_support

def custom_add(x, y, output, kernel_name="custom_add"):
    """自定义加法算子 TBE 实现"""
    data_x = tvm.placeholder(x.get("shape"), dtype=x.get("dtype"), name="data_x")
    data_y = tvm.placeholder(y.get("shape"), dtype=y.get("dtype"), name="data_y")
    res = tvm.compute(data_x.shape, lambda *i: data_x(*i) + data_y(*i), name="res")
    
    # 编译
    with tvm.target.cce():
        schedule = tvm.create_schedule(res.op)
        config = {"print_ir": False, "name": kernel_name, "tensor_list": [data_x, data_y, res]}
        tvm.build(schedule, [data_x, data_y, res], "cce", name=kernel_name)

什么时候需要写自定义算子:

  • 模型中存在自定义 CUDA kernel(.cu 文件)
  • 使用的第三方库包含 Triton kernel(如 flash-attn、vLLM)
  • 特定业务场景需要极致优化的专有算子
3.1.3 算子替换对照表
原算子 替换方案 场景
F.scaled_dot_product_attention (flash_attn=TRUE) 昇腾 FlashAttention 算子 NLP 注意力计算
bitsandbytes 的 8bit 量化 Linear 昇腾 AMCT LinearReplacement 量化推理
Triton kernel TBE / DSL 重写 自定义高性能算子
NVIDIA Apex 的 FusedAdam 标准 torch.optim.AdamW(已足够) 优化器
apex.normalization.FusedLayerNorm 标准 nn.LayerNorm(已足够) 归一化

3.2 推理特性适配

3.2.1 静态 Shape 推理优化

昇腾推理的最佳实践是静态 Shape——固定 batch size 和序列长度,避免重编译开销。

# 静态 Shape 推理配置
class StaticShapeInference:
    def __init__(self, model, max_batch=1, max_seq_len=512):
        self.model = model.eval()
        self.max_batch = max_batch
        self.max_seq_len = max_seq_len
        
        # 预热:用目标 Shape 运行一次(触发编译)
        dummy = torch.randint(0, 1000, (max_batch, max_seq_len)).npu()
        with torch.no_grad():
            self.model(dummy)
        print("✅ 静态 Shape 编译完成")
    
    @torch.no_grad()
    def infer(self, input_ids):
        # 输入不足时 padding 到固定长度
        batch, seq = input_ids.shape
        assert batch <= self.max_batch, f"batch 超过最大限制 {self.max_batch}"
        
        if seq < self.max_seq_len:
            # padding
            pad_len = self.max_seq_len - seq
            input_ids = torch.nn.functional.pad(
                input_ids, (0, pad_len), value=0
            )
        
        output = self.model(input_ids)
        return output[:, :seq]  # 截取有效部分
3.2.3 KV Cache 管理与显存优化
# KV Cache 管理工具
class KVCacheManager:
    def __init__(self, max_batch, max_seq_len, num_layers, num_heads, head_dim, dtype=torch.float16):
        self.max_batch = max_batch
        self.max_seq_len = max_seq_len
        
        # 预分配固定大小的 KV Cache 张量
        shape = (max_batch, num_layers, 2, max_seq_len, num_heads, head_dim)
        self.cache = torch.zeros(shape, dtype=dtype, device='npu')
        self.current_len = 0
    
    def update(self, layer_idx, key, value, seq_pos):
        """更新指定位置 KV Cache"""
        batch = key.shape[0]
        seq_len = key.shape[2]
        self.cache[:batch, layer_idx, 0, seq_pos:seq_pos+seq_len] = key
        self.cache[:batch, layer_idx, 1, seq_pos:seq_pos+seq_len] = value
    
    def get(self, layer_idx, batch):
        """获取当前有效 KV Cache"""
        return self.cache[:batch, layer_idx, 0, :self.current_len], \
               self.cache[:batch, layer_idx, 1, :self.current_len]
    
    def reset(self):
        """重置 cache(新序列)"""
        self.cache.zero_()
        self.current_len = 0

3.3 量化与压缩

3.3.1 AMCT(昇腾模型压缩工具)替代 bitsandbytes

昇腾的量化工具链是 AMCT(Ascend Model Compression Toolkit),对标 NVIDIA 的 TensorRT + bitsandbytes。

# 安装 AMCT
pip install amct-lite  # 推理场景
# 或
pip install amct       # 训练场景

AMCT 量化流程:

# 1. 导入 AMCT
import amct_lite as amct

# 2. 定义校准数据集(少量无标签数据,用于确定量化参数)
def calibration_dataset():
    for _ in range(100):
        yield torch.randn(1, 3, 224, 224).npu()

# 3. 量化
config = amct.QuantConfig(
    quant_mode='all',           # 全部层量化
    bits=8,                     # 8bit
    calibration_batch_size=32,
    calibration_iters=100,
)

# 4. 执行量化
model = Model().npu().eval()
model_quant = amct.quantize_model(
    model, 
    calib_dataset=calibration_dataset(),
    config=config,
)

# 5. 验证精度
# ...
3.3.2 INT8 / INT4 量化流程
# INT8 量化步骤
# 1. 准备校准数据(约 100-500 条)
# 2. 运行 AMCT 校准
# 3. 评估量化后精度
# 4. 如果精度下降 > 1%,回退部分层为 fp16

# 各精度模式的显存和性能预期
# fp16:  1x 显存, 1x 速度
# int8:  0.5x 显存, 1.5-2x 速度
# int4:  0.25x 显存, 2-3x 速度
3.3.3 量化精度回退机制
# 逐层精度评估,自动回退精度损失大的层
class AdaptiveQuantizer:
    def __init__(self, model, calib_data, threshold=0.01):
        self.model = model
        self.calib_data = calib_data
        self.threshold = threshold  # 精度损失阈值
    
    def quantize_with_fallback(self):
        """量化全部层,评估精度,回退异常层"""
        # 1. 获取 fp16 baseline 输出
        fp16_output = self._get_reference_output()
        
        # 2. 逐层量化 + 评估
        quant_layers = []
        fallback_layers = []
        
        for name, module in self.model.named_modules():
            if self._is_quantizable(module):
                # 尝试量化该层
                quant_output = self._quantize_and_run(name, module)
                error = self._compute_error(fp16_output, quant_output)
                
                if error < self.threshold:
                    quant_layers.append(name)
                else:
                    fallback_layers.append(name)
        
        return quant_layers, fallback_layers

3.4 推理框架适配(可选)

对于生产级推理部署,可能需要将推理框架从 VLLM / TensorRT-LLM 切换到昇腾生态的方案。

3.4.1 推理框架对比
场景 GPU 方案 NPU 方案 切换难度
LLM 在线推理 VLLM / TGI MindIE / MindSpore Lite ⭐⭐⭐ 中
高性能推理 TensorRT-LLM 昇腾 Batch推理 / OM 模型 ⭐⭐⭐⭐ 高
端侧推理 TensorRT / ONNX Runtime MindSpore Lite ⭐⭐ 低
标准 PyTorch 推理 TorchScript / torch.compile torch_npu 直接推理 ⭐ 低(无需切换)

最简方案: 如果只是部署标准 PyTorch 模型推理,直接使用 torch_npu 即可,无需切换推理框架。

3.4.2 OM 模型导出(ONNX → OM)
# 1. PyTorch → ONNX
python export_onnx.py

# 2. ONNX → OM(昇腾离线模型)
atc --model=model.onnx \
    --output=model_om \
    --soc_version=Ascend910B \
    --input_shape="input:1,3,224,224" \
    --log=info

# 3. 推理
# 使用 MindIE 或 MindSpore Lite 加载 OM 模型

3.5 算子性能调优

这一节承接"迁移分析阶段"的亲和 API 分析结果,在代码层面进行针对性优化。

3.5.1 算子融合策略

昇腾编译器会自动进行算子融合,但以下手动融合策略可进一步优化:

# 手动融合常见模式
# 模式1:Conv + BN + ReLU → 融合为单个算子
class FusedConvBNReLU(nn.Module):
    def __init__(self, in_ch, out_ch, kernel_size):
        super().__init__()
        self.conv = nn.Conv2d(in_ch, out_ch, kernel_size, bias=False)
        self.bn = nn.BatchNorm2d(out_ch)
        self.relu = nn.ReLU(inplace=True)
        # 开启算子融合标记
        self.conv.__class__.__name__ = "FusedConvBNReLU"
    
    def forward(self, x):
        return self.relu(self.bn(self.conv(x)))
3.5.2 算子缓存优化
# 核心优化手段:开启算子缓存
export MSRUN_GENERATE_CACHE=true

# 缓存预热:首次运行时将算子编译结果存入缓存目录
# 缓存位置:~/.cache/ascend/ 或 $ASCEND_CACHE_PATH
export ASCEND_CACHE_PATH=/path/to/cache

# 缓存共享:多进程共用缓存,避免重复编译
export ASCEND_SHARE_CACHE=true

预期收益: 首次运行后,后续运行避免算子重新编译,性能提升 30-50%(尤其是多卡场景)。

3.5.3 亲和 API 替换(承接迁移分析)

来自迁移分析阶段的亲和 API 推荐,在代码中实施替换:

# 来自 msFmkTrans 亲和 API 分析的建议
# 在代码中实施替换

# 替换前
out = torch.bmm(x, y)
out = F.softmax(x, dim=-1)

# 替换后
out = torch.matmul(x, y)                          # 亲和替换:bmm → matmul
out = torch_npu.npu_softmax_v2(x, dim=-1)         # 亲和替换:使用昇腾优化版 Softmax

精度调试 — 学习笔记

板块二:昇腾 NPU 上的精度对齐方法与工具。迁移适配让代码"能跑通",精度调试解决"输出对不对"的问题。

目标:逐层对齐 NPU 输出与 GPU baseline,确保最终精度指标一致。

一、精度问题根因分析

概述: 精度差异的本质原因并不复杂——底层算子实现不同。但同样的根因在不同模型结构下表现各异,需要系统性地理解差异来源才能高效定位和修复。


1.1 精度差异的三大来源

来源一:算子实现差异

同一个数学运算,GPU(CUDA)和 NPU(CANN)的实现方式不同,导致浮点结果有细微差异。

加法顺序不同导致的结果差异示例:

GPU 计算:  a + b + c + d = ((a + b) + c) + d
NPU 计算:  a + b + c + d = (a + b) + (c + d)     ← 结合顺序不同

输入: a=1e-7, b=1e-7, c=1e8, d=-1e8
GPU:  ((1e-7 + 1e-7) + 1e8) + (-1e8) = 2e-7        ← 正确
NPU:  (1e-7 + 1e-7) + (1e8 + (-1e8)) = 0.0          ← 精度丢失!

累加器位宽差异:CUDA TensorCore 和昇腾 CubeUnit 的内部累加器位宽可能不同(如 GPU 使用 fp32 累加,NPU 使用 fp16 累加),导致大数值计算时的精度损失。

来源二:数据类型差异
特性 GPU (CUDA) NPU (CANN) 影响
fp16 指数位宽 5 bit 5 bit 一致
fp16 尾数位宽 10 bit 10 bit 一致
非规格化数 (denorm) 支持 可能被 flush-to-zero 小数值计算精度下降
fp16 最大值 65504 65504 一致
fp16 最小值(规格化) 6.1e-5 6.1e-5 一致

关键差异: NPU 在处理极小的 fp16 数值时可能将其 flush-to-zero,导致累积误差。

来源三:随机性差异
# GPU 上的 dropout mask 生成
torch.manual_seed(42)
dropout = nn.Dropout(0.1)
mask_gpu = dropout(torch.ones(100))

# NPU 上的 dropout mask 生成(相同 seed)
torch.manual_seed(42)
# 即使 seed 相同,底层随机数生成算法可能不同
mask_npu = dropout(torch.ones(100).npu()).cpu()

# mask_gpu 和 mask_npu 可能不同!

注意: GPU 和 NPU 使用不同的随机数生成器实现,即使 seed 相同也不能保证 dropout mask 一致。精度比对时应关闭 dropout 或固定 mask。


1.2 精度问题分类速查

问题类型 现象 可能原因 紧急程度
整网发散 loss → NaN / inf 梯度爆炸、fp16 溢出 🔴 必须修复
逐层漂移 每层误差 1e-4 ~ 1e-3,累积后整体偏移 算子实现差异、数值稳定性 🟡 高精度场景需修复
个别算子异常 某层 cos_sim < 0.9 特定算子实现 Bug 🔴 必须修复
随机不一致 同一输入两次输出不同 dropout、非确定性算子 🟢 不影响评估 metric
小数值精度丢失 接近 0 的梯度过早归零 denorm flush-to-zero 🟡 影响收敛时需修复

二、精度调试工具与方法

概述: 精度调试的核心方法是"逐层比对"——在两个平台上用相同的输入跑一次前向,逐层比较输出张量的差异。找到差异层后,再下钻到该层内部定位具体算子。


2.1 逐层精度比对(核心方法)

方案A:Hook + 固定输入(最通用,推荐)
# 精度比对工具类 —— 在任意模型上插入 Hook,收集逐层输出
import torch
import torch.nn as nn
from collections import OrderedDict

class LayerOutputCollector:
    """收集模型各层前向输出的工具类"""
    
    def __init__(self, model):
        self.outputs = OrderedDict()
        self._hooks = []
        self._register_hooks(model)
    
    def _register_hooks(self, model, prefix=''):
        for name, module in model.named_modules():
            # 跳过容器模块,只收集有参数的实际算子层
            if not isinstance(module, (nn.Sequential, nn.ModuleList)):
                hook_name = f"{prefix}{name}" if prefix else name
                hook = self._make_hook(hook_name)
                self._hooks.append(module.register_forward_hook(hook))
    
    def _make_hook(self, name):
        def hook(module, inp, out):
            if isinstance(out, torch.Tensor):
                self.outputs[name] = out.detach().cpu()
            elif isinstance(out, (tuple, list)):
                # 取第一个输出(大多数情况)
                self.outputs[name] = out[0].detach().cpu()
        return hook
    
    def clear(self):
        self.outputs.clear()
    
    def remove_hooks(self):
        for hook in self._hooks:
            hook.remove()

def compare_models(gpu_model, npu_model, input_data, threshold=0.999):
    """
    逐层比对 GPU 和 NPU 模型的输出
    
    参数:
        gpu_model: GPU 上的模型(eval 模式)
        npu_model: NPU 上的模型(eval 模式)
        input_data: 相同的输入张量(在 CPU 上)
        threshold: 余弦相似度阈值
    
    返回:
        passed_layers: 通过的层名列表
        failed_layers: [{name, cos_sim, max_diff}, ...]
    """
    gpu_collector = LayerOutputCollector(gpu_model)
    npu_collector = LayerOutputCollector(npu_model)
    
    # 前向(关闭 dropout 等随机层)
    with torch.no_grad():
        _ = gpu_model(input_data.cuda())
        _ = npu_model(input_data.npu())
    
    passed = []
    failed = []
    
    for name in gpu_collector.outputs:
        if name not in npu_collector.outputs:
            failed.append({'name': name, 'error': 'NPU side missing'})
            continue
        
        gpu_out = gpu_collector.outputs[name]
        npu_out = npu_collector.outputs[name]
        
        # 计算指标
        cos_sim = nn.functional.cosine_similarity(
            gpu_out.flatten().unsqueeze(0),
            npu_out.flatten().unsqueeze(0)
        ).item()
        max_diff = (gpu_out - npu_out).abs().max().item()
        
        if cos_sim >= threshold:
            passed.append(name)
        else:
            failed.append({'name': name, 'cos_sim': cos_sim, 'max_diff': max_diff})
    
    gpu_collector.remove_hooks()
    npu_collector.remove_hooks()
    
    return passed, failed

# ===== 使用示例 =====
# 1. 准备固定输入(用之前保存的 baseline_input.pt)
input_data = torch.load('baseline_input.pt', map_location='cpu')

# 2. 分别创建 GPU 和 NPU 模型(使用相同权重)
gpu_model = MyModel().cuda().eval()
npu_model = MyModel().npu().eval()
# 确保权重相同
npu_model.load_state_dict(gpu_model.state_dict())

# 3. 执行比对
passed, failed = compare_models(gpu_model, npu_model, input_data, threshold=0.999)

# 4. 输出结果
print(f"✅ 通过层数: {len(passed)}")
for f in failed:
    print(f"❌ {f['name']}: cos_sim={f.get('cos_sim', 'N/A'):.6f}, max_diff={f.get('max_diff', 'N/A'):.2e}")

方案B:adc precision_compare(华为官方工具)
# 安装 adc(Ascend Debugging Center)
pip install adc

# 1. GPU 侧:dump 逐层输出到文件
# 需要先插入 dump 代码
python gpu_dump.py --output-dir=./gpu_dump

# 2. NPU 侧:dump 逐层输出到文件
python npu_dump.py --output-dir=./npu_dump

# 3. 执行自动比对
adc precision_compare \
    --gpu-dump-dir=./gpu_dump \
    --npu-dump-dir=./npu_dump \
    --output-dir=./compare_result \
    --tolerance=cos_sim:0.99

# 输出
# open ./compare_result/compare_visual.html  # 可视化报告

方案C:对比日志自动分析脚本
# 自动分析训练日志,检测精度异常
import re

def analyze_training_logs(gpu_log_path, npu_log_path):
    """从训练日志中提取 loss 并对比"""
    
    def extract_losses(log_path):
        losses = []
        pattern = re.compile(r'Loss:\s*([\d.]+)')
        with open(log_path) as f:
            for line in f:
                match = pattern.search(line)
                if match:
                    losses.append(float(match.group(1)))
        return losses
    
    gpu_losses = extract_losses(gpu_log_path)
    npu_losses = extract_losses(npu_log_path)
    
    if len(gpu_losses) != len(npu_losses):
        print(f"⚠️ 步数不一致: GPU={len(gpu_losses)}, NPU={len(npu_losses)}")
    
    min_steps = min(len(gpu_losses), len(npu_losses))
    
    # 计算滑动平均差异
    window = 10
    diffs = []
    for i in range(min_steps - window + 1):
        gpu_avg = sum(gpu_losses[i:i+window]) / window
        npu_avg = sum(npu_losses[i:i+window]) / window
        diffs.append(abs(gpu_avg - npu_avg))
    
    avg_diff = sum(diffs) / len(diffs)
    max_diff = max(diffs)
    
    print(f"滑动平均 loss 差异: 平均={avg_diff:.6f}, 最大={max_diff:.6f}")
    
    # 检查趋势性偏离
    trending = False
    if len(diffs) > 100:
        early = sum(diffs[:50]) / 50
        late = sum(diffs[-50:]) / 50
        if late > early * 3:  # 后期差异是前期的 3 倍 → 趋势性偏离
            trending = True
            print("⚠️ 检测到趋势性偏离: 后期差异持续增大")
    
    return {
        'avg_diff': avg_diff,
        'max_diff': max_diff,
        'trending': trending,
        'verdict': 'PASS' if avg_diff < 1e-3 and not trending else 'FAIL'
    }

2.2 算子级 Dump 调试

当逐层比对定位到异常层后,需要进一步下钻到该层内部的具体算子。

ASCEND_OP_DUMP 环境变量配置
# 开启算子 dump
export ASCEND_OP_DUMP=1
export ASCEND_OP_DUMP_PATH=/home/dump_data
export ASCEND_OP_DUMP_MODE=2              # 过滤模式
export ASCEND_OP_DUMP_FILTER=MatMul,Softmax  # 只 dump 特定算子
export ASCEND_OP_DUMP_STATUS_RUN=1        # 只在运行状态 dump(非编译阶段)

# 运行脚本
python train.py --steps=1

# dump 输出在 /home/dump_data/ 下
# 每个算子一个目录,包含 input_x.bin 和 output_y.bin

Dump 模式说明:

MODE 说明 适用场景
0 dump 所有算子 全面排查(数据量大,慎用)
1 黑名单模式 排除已知正常的算子
2 白名单模式(推荐) 只 dump 怀疑有问题的算子

Dump 数据离线比对方法:

# 离线比对 dump 出的算子输入输出
import numpy as np

def compare_op_dump(gpu_dump_path, npu_dump_path):
    """比对 GPU 和 NPU 侧 dump 出的同一算子"""
    
    def load_dump(path):
        """加载 dump 数据(fp16 格式)"""
        return np.fromfile(path, dtype=np.float16)
    
    gpu_input = load_dump(f"{gpu_dump_path}/input_0.bin")
    npu_input = load_dump(f"{npu_dump_path}/input_0.bin")
    
    # 先确认输入一致(如果输入都不一致,问题在前驱算子)
    input_diff = np.max(np.abs(gpu_input - npu_input))
    print(f"输入差异: {input_diff:.2e}")
    
    if input_diff > 1e-5:
        print("⚠️ 输入不一致,问题不在本算子,在前驱算子")
        return
    
    # 比对输出
    gpu_output = load_dump(f"{gpu_dump_path}/output_0.bin")
    npu_output = load_dump(f"{npu_dump_path}/output_0.bin")
    
    cos_sim = np.dot(gpu_output, npu_output) / (
        np.linalg.norm(gpu_output) * np.linalg.norm(npu_output)
    )
    max_diff = np.max(np.abs(gpu_output - npu_output))
    
    print(f"该算子: cos_sim={cos_sim:.6f}, max_diff={max_diff:.2e}")
    return cos_sim, max_diff

2.3 确定性调试

固定随机种子最佳实践
import torch
import numpy as np
import random
import os

def set_deterministic(seed=42):
    """全局固定随机种子,确保可复现"""
    os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'  # GPU 确定性
    os.environ['HCCL_DETERMINISTIC'] = 'true'             # NPU 确定性
    
    torch.manual_seed(seed)
    torch.npu.manual_seed_all(seed)
    np.random.seed(seed)
    random.seed(seed)
    
    # PyTorch 确定性模式(可能降低性能)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False
    
    # 注意:torch.use_deterministic_algorithms(True) 在某些算子会报错
    # 只在不报错的前提下使用
    try:
        torch.use_deterministic_algorithms(True)
    except RuntimeError as e:
        print(f"⚠️ 部分确定性算法不可用: {e}")
HCCL_DETERMINISTIC 配置
# 分布式训练确定性通信
export HCCL_DETERMINISTIC=true
⚠️ 确定性模式对性能的影响

确定性模式会禁用部分算子优化(如原子操作、浮点重排),性能可能下降 10-30%。仅在精度调试阶段开启,性能测试和正式训练时应关闭。


2.4 精度比对指标与阈值

核心指标
指标 公式 含义 理想值
余弦相似度 cos(A,B) = A·B / ( A B ) 方向和分布的相似程度 > 0.999
最大绝对误差 max A-B 单元素最大偏差 < 1e-5 (fp32)
均值绝对误差 mean A-B 平均偏差 < 1e-6 (fp32)
相对误差 A-B / max( A , B ) 相对大小偏差 < 1%
各精度模式下的可接受阈值
精度模式 cos_sim max_diff 场景
fp32 训练 > 0.9999 < 1e-5 高精度训练、研究场景
fp16 混合精度 > 0.999 < 1e-3 常规训练
INT8 量化推理 准确率下降 < 0.5% 部署推理

注意: 阈值不是绝对的。对于大模型(如 LLM),少量精度漂移在合理范围内,只要最终评估指标(acc / BLEU / loss)在可接受范围内即可。


三、常见精度问题修复手册

概述: 这一章汇集了昇腾迁移中最常见的精度问题场景及其修复方案。每种问题都配有"现象 → 根因 → 修复"的完整链路。


3.1 LayerNorm 精度偏差

现象:

  • loss 在前几步正常,随后缓慢发散
  • 特定层(通常在 Transformer Block 的 LayerNorm)cos_sim 偏低(0.99 左右)

根因:

  • GPU 和 NPU 的 LayerNorm 实现细节不同(加法顺序、累加器位宽差异)
  • fp16 下该偏差被放大,逐层累积后导致整体精度漂移

修复方案:

# 方案一(推荐):在 autocast 中排除 LayerNorm
class TransformerBlock(nn.Module):
    def __init__(self, hidden_size):
        super().__init__()
        self.norm1 = nn.LayerNorm(hidden_size)
        self.attn = nn.MultiheadAttention(hidden_size, num_heads=8)
        self.norm2 = nn.LayerNorm(hidden_size)
        self.ffn = nn.Sequential(
            nn.Linear(hidden_size, hidden_size * 4),
            nn.GELU(),
            nn.Linear(hidden_size * 4, hidden_size),
        )
    
    def forward(self, x):
        # LayerNorm 强制 fp32
        with torch.npu.amp.autocast(enabled=False):
            x = self.norm1(x.float())
        x = x.to(torch.half if torch.is_autocast_enabled('npu') else torch.float)
        x = x + self.attn(x, x, x)[0]
        
        with torch.npu.amp.autocast(enabled=False):
            x = self.norm2(x.float())
        x = x.to(torch.half if torch.is_autocast_enabled('npu') else torch.float)
        x = x + self.ffn(x)
        return x

# 方案二:使用 torch_npu 优化版 LayerNorm
import torch_npu
# 某些 CANN 版本提供 npu_layer_norm,精度更接近 GPU
# 但标准 nn.LayerNorm 也应足够,先试方案一

3.2 Softmax 精度偏差

现象:

  • 分类任务准确率下降 0.5-2%
  • 注意力分布出现异常(如某些 token 注意力权重异常低)

根因:

  • Softmax 实现涉及指数运算(exp),不同实现的舍入误差不同
  • 大 logit 值下,减去 max 后的指数运算尾数精度差异

修复方案:

# 方案一(推荐):开启昇腾 Softmax 优化模式
export ASCEND_SOFTMAX_OPTIMIZE=1
# 方案二:替换为昇腾亲和 API
import torch_npu

class StableSoftmax(nn.Module):
    def __init__(self, dim=-1):
        super().__init__()
        self.dim = dim
    
    def forward(self, x):
        # 使用昇腾优化版 Softmax(精度更高)
        return torch_npu.npu_softmax_v2(x, dim=self.dim)
# 方案三:手动实现数值稳定的 Softmax(备用)
def stable_softmax(x, dim=-1):
    """数值稳定的 Softmax(自定义实现,精度可控)"""
    x_max = x.max(dim=dim, keepdim=True)[0]
    x_exp = torch.exp(x - x_max)
    x_sum = x_exp.sum(dim=dim, keepdim=True)
    return x_exp / x_sum

3.3 fp16 下梯度溢出

现象:

  • loss 突变为 NaN(通常在训练的第 N 个 step)
  • 重启训练后,仍在相近的 step 出现 NaN

根因:

  • fp16 的表达范围有限(最大值 65504),梯度值超过该范围 → 溢出 → NaN
  • 常见于模型深层、大 batch size、高学习率场景

修复方案:

# 方案一(推荐):调整 GradScaler 参数
scaler = torch.npu.amp.GradScaler(
    init_scale=2.**16,       # 提高初始缩放因子(默认 2^16)
    growth_interval=100,     # 降低增长间隔(更快调整)
)

# 方案二:梯度裁剪(防止梯度暴涨)
scaler.scale(loss).backward()
# 先反缩放再裁剪
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
scaler.step(optimizer)
scaler.update()

# 方案三:将该层的计算强制切换到 fp32
class Fp32Linear(nn.Module):
    """强制 fp32 的 Linear 层"""
    def __init__(self, in_features, out_features):
        super().__init__()
        self.linear = nn.Linear(in_features, out_features)
    
    def forward(self, x):
        with torch.npu.amp.autocast(enabled=False):
            return self.linear(x.float())

3.4 注意力机制精度偏差

现象:

  • 文本生成质量明显下降
  • attention 分布与 GPU 相比有明显偏移

根因:

  • F.scaled_dot_product_attention 在某些模式下使用了 flash attention,NPU 上的 flash attention 实现不同
  • 注意力 mask 的数值处理差异

修复方案:

# 方案一:关闭 flash attention,使用标准 attention
import torch.nn.functional as F

def stable_attention(query, key, value, mask=None):
    """标准 attention 实现(不使用 flash attention)"""
    scale = query.size(-1) ** 0.5
    scores = torch.matmul(query, key.transpose(-2, -1)) / scale
    
    if mask is not None:
        scores = scores.masked_fill(mask == 0, float('-inf'))
    
    attn_weights = F.softmax(scores, dim=-1)
    output = torch.matmul(attn_weights, value)
    return output

# 方案二(Transformer 模型内替换)
# 在模型配置中关闭 flash attention
config.use_flash_attention = False  # 具体参数名依模型而异
model = ModelClass(config)

3.5 随机性不一致

现象:

  • 相同的 seed + 相同的输入,NPU 上两次输出不同
  • 无法在 NPU 上复现 GPU 上的 bug

根因:

  • Dropout mask 生成算法不同(即使 seed 相同)
  • 非确定性算子(某些聚合操作在硬件上无确定顺序)

修复方案:

# 方案一:精度比对前关闭 dropout
model.eval()  # eval 模式下 dropout 不生效

# 方案二:固定 dropout mask(需要精确比对时)
torch.manual_seed(42)
dropout_mask = (torch.randn_like(x) > 0.1).float()
x = x * dropout_mask / 0.9  # 手动 dropout(mask 可复现)

# 方案三:用 torch.nn.functional.dropout 指定 generator
gen = torch.Generator(device='npu').manual_seed(42)
x = F.dropout(x, p=0.1, generator=gen)

四、精度调试流程

概述: 精度调试不需要面面俱到,而是有优先级、有节奏地推进。根据问题严重程度选择不同的排查深度。


4.1 快速排查(5分钟)

适用于首次在 NPU 上跑模型时快速判断精度是否"基本正常"。

快速排查流程
│
├─ 1. 确保模型处于 eval 模式(关闭 dropout)
│
├─ 2. 准备固定输入(与 GPU 侧相同的输入)
│
├─ 3. 分别在 GPU 和 NPU 上跑一次前向
│
├─ 4. 比较最终输出
│   ├─ cos_sim > 0.999 → ✅ 精度通过,无需深入排查
│   ├─ 0.99 ~ 0.999 → ⚠️ 有轻微偏移,但可能可接受
│   └─ < 0.99 → 🔴 需要排查
│
└─ 5. 对比最终 loss 值
    ├─ 差异 < 0.01 → ✅
    ├─ 差异 0.01~0.1 → ⚠️ 需关注但可能不影响训练
    └─ 差异 > 0.1 → 🔴 需要深入排查

4.2 系统排查(1-2小时)

快速排查发现问题时,进入系统排查流程。

系统排查流程
│
├─ 步骤1:逐层收集输出(使用 LayerOutputCollector)
│   ├─ 收集 GPU 侧每层输出
│   └─ 收集 NPU 侧每层输出
│
├─ 步骤2:自动比对,输出 Top-K 差异最大的层
│   ├─ cos_sim 最低的 3 层
│   └─ max_diff 最大的 3 层
│
├─ 步骤3:分析差异层
│   ├─ 是 LayerNorm 吗? → 切 fp32 重试
│   ├─ 是 Softmax 吗? → 开启优化模式
│   ├─ 是 attention 吗? → 使用标准 attention
│   ├─ 是 fp16 相关层吗? → 切 fp32
│   └─ 是其他算子? → dump 该算子输入输出,进一步分析
│
├─ 步骤4:修复后回归验证
│   └─ 重新跑一次步骤1-2,确认差异消失或降到阈值以下
│
└─ 步骤5:全流程验证
    ├─ 跑 100 步训练,确认 loss 曲线与 GPU 一致
    └─ 跑完整验证集,确认评估指标达标

4.3 长期稳定性验证(按需)

适用于生产环境部署前的最终验证。

# 长期稳定性验证脚本
def stability_test(model, dataloader, device, num_epochs=10):
    """多 epoch 训练稳定性测试"""
    
    model = model.to(device)
    optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
    scaler = torch.npu.amp.GradScaler()
    
    metrics = {
        'epoch_loss': [],
        'epoch_acc': [],
        'nan_steps': [],
        'inf_steps': [],
    }
    
    for epoch in range(num_epochs):
        epoch_loss = 0.0
        num_batches = 0
        
        for data, target in dataloader:
            data, target = data.to(device), target.to(device)
            
            optimizer.zero_grad()
            with torch.npu.amp.autocast():
                output = model(data)
                loss = criterion(output, target)
            
            # 检测 NaN / inf
            if torch.isnan(loss).any():
                metrics['nan_steps'].append(epoch * len(dataloader) + num_batches)
                break
            if torch.isinf(loss).any():
                metrics['inf_steps'].append(epoch * len(dataloader) + num_batches)
                break
            
            scaler.scale(loss).backward()
            scaler.step(optimizer)
            scaler.update()
            
            epoch_loss += loss.item()
            num_batches += 1
        
        metrics['epoch_loss'].append(epoch_loss / max(num_batches, 1))
        print(f"Epoch {epoch+1}/{num_epochs}: Loss={metrics['epoch_loss'][-1]:.6f}")
    
    has_nan = len(metrics['nan_steps']) > 0
    has_inf = len(metrics['inf_steps']) > 0
    
    print(f"\n稳定性测试结果:")
    print(f"  NaN 步数: {len(metrics['nan_steps'])} {'❌' if has_nan else '✅'}")
    print(f"  Inf 步数: {len(metrics['inf_steps'])} {'❌' if has_inf else '✅'}")
    
    return not (has_nan or has_inf), metrics

性能调优 — 学习笔记

板块三:昇腾 NPU 上的性能分析与优化方法。迁移适配让代码"能跑通",精度调试让输出"对得上",性能调优让模型"跑得快"。

目标:让 NPU 发挥应有算力,达到生产可接受水平的吞吐、延迟和显存效率。


一、性能分析工具与方法

概述: 性能调优的第一步不是优化,而是测量。"哪里慢、为什么慢"比"怎么优化"更重要。昇腾提供了对标 NVIDIA Nsight 的全套 Profiling 工具链。


1.1 昇腾 Profiling 工具

msprof:时间线分析(对标 NVIDIA nsys)
# 基础用法:采集算子级时间线
msprof --output=./prof_report \
       --application="python train.py --epochs=1" \
       --level=level1 \
       --ai-core=true \       # 采集 AI Core(矩阵运算)耗时
       --ai-cpu=true          # 采集 AI CPU(标量运算)耗时

# 输出目录结构
# prof_report/
# ├── timeline.json           # Chrome Trace Viewer 可打开
# ├── op_statistic.csv        # 算子耗时统计
# └── memory_flow.csv         # 显存流水

msprof 级别说明(level0 ~ level4):

级别 数据量 粒度 场景
level0 模型级(总耗时) 快速摸清整体性能
level1 算子级(每个算子的耗时) 推荐的日常优化粒度
level2 流水线级(pipe stage) 深入分析算子内部
level3 很大 指令级 调试自定义算子
level4 极大 硬件微架构级 极端优化场景
msvp:算子级性能指标(对标 NVIDIA ncu)
# 算子级性能指标采集
msvp --output=./msvp_report \
     --application="python train.py --steps=10" \
     --op-statistic=true

# 输出包含:
# - 每个算子的 FLOPs / Bandwidth / Utilization
# - AI Core 利用率
# - 理论算力 vs 实际算力对比

二、计算性能优化

概述: 计算性能优化的核心是让 NPU 的 AI Core(矩阵乘法单元)和 Vector Core(向量运算单元)尽可能满载运行。减少等待、减少重编译、减少不必要的精度转换。


2.1 算子优化

亲和 API 替换(承接迁移分析结果)

来自"迁移分析阶段"的亲和 API 分析报告,在代码中实施替换:

# 来自 msFmkTrans 的分析建议,按优先级实施

# 🔥 高收益替换(来自分析报告)
torch.bmm → torch.matmul                 # +25%
F.softmax → torch_npu.npu_softmax_v2     # +30%
F.dropout → torch_npu.npu_dropout         # +20%

# 替换后验证性能收益
# 替换前: 0.42ms / 次
# 替换后: 0.28ms / 次 ← 需实测确认
算子合:减少 kernel launch 次数
# 手动融合示例:Conv + BN + ReLU 融合
class FusedConvBNReLU(nn.Module):
    """算子融合示例:将三个算子合并为一个逻辑单元
    
    原理:减少中间张量的读写开销和 kernel launch 次数
    CANN 编译器会自动尝试融合,但手动明确标记效果更好
    """
    def __init__(self, in_ch, out_ch, kernel_size):
        super().__init__()
        self.conv = nn.Conv2d(in_ch, out_ch, kernel_size, bias=False)
        self.bn = nn.BatchNorm2d(out_ch)
        self.relu = nn.ReLU(inplace=True)
    
    def forward(self, x):
        # 连续调用的算子,CANN 编译时可能自动融合
        return self.relu(self.bn(self.conv(x)))

常见可融合模式:

模式 预期收益 说明
Conv + BN + ReLU 10-20% 标准的 CNN 融合模式
Linear + GELU 5-10% Transformer FFN 中的常见模式
Add + LayerNorm 5-10% Transformer 残差连接
Cast + MatMul 5% 减少数据类型转换开销

2.2 算子缓存优化

这是昇腾上性价比最高的优化手段之一。 一个算子在第一次调用时编译,之后的调用直接从缓存加载,避免重复编译。

# 核心配置
export MSRUN_GENERATE_CACHE=true                          # 开启算子缓存
export ASCEND_CACHE_PATH=/path/to/cache                   # 缓存目录(默认 ~/.cache/ascend/)

缓存预热策略:

# 训练前用代表性输入预热缓存
def warmup_cache(model, input_shape, device='npu', steps=3):
    """
    预热算子缓存:用不同 Shape 的组合跑几次,让常用 Shape
    的编译结果都进入缓存
    """
    model = model.to(device).eval()
    
    # 常见的 batch_size 和 seq_len 组合
    configs = [
        (1, 512),   # 推理场景
        (4, 512),   # 小 batch 训练
        (8, 512),   # 中 batch 训练
        (16, 512),  # 大 batch 训练
    ]
    
    for batch, seq in configs:
        dummy = torch.randint(0, 1000, (batch, seq)).to(device)
        with torch.no_grad():
            for _ in range(steps):
                _ = model(dummy)
    
    print("✅ 缓存预热完成")

预期收益:

场景 首次运行 缓存后 收益
单卡训练 1x 0.6-0.7x 30-40%
8卡训练 1x 0.4-0.5x 50-60%
推理服务 首次慢 后续稳定 消除启动延迟

2.3 图模式编译

对标 GPU 上的 torch.compile,将动态图编译为静态图以消除 Python 解释开销。

import torch_npu

# GPU 上的图编译
# compiled_model = torch.compile(model)

# NPU 上的图编译
compiled_model = torch_npu.compile(model)

# 使用方式不变
output = compiled_model(input_data)

静态图 vs 动态图选择:

模式 优势 劣势 推荐场景
动态图(Eager) 调试方便、灵活 性能一般 开发调试阶段
静态图(Compile) 性能好(提升50-100%) 编译慢、部分算子不支持 稳定运行的训练/推理

图编译常见失败场景与回退:

# 安全做法:先尝试编译,失败时回退到动态图
try:
    model = torch_npu.compile(model)
    print("✅ 图编译成功")
except Exception as e:
    print(f"⚠️ 图编译失败: {e},回退到动态图模式")
    # 保持原模型不变

2.4 混合精度性能调优

混合精度的核心是平衡"性能"和"精度"——通常 fp16 的计算速度是 fp32 的 2-4 倍,但某些层在 fp16 下精度不足。

fp16 覆盖率与精度平衡:

# 逐层控制 fp16 / fp32
class MixedPrecisionController:
    """
    精细控制模型中每层的精度模式
    用于在"想让更多层跑 fp16 提性能"和"某些层不能跑 fp16"之间做权衡
    """
    def __init__(self, model):
        self.model = model
        self.fp32_layers = set()   # 需要 fp32 的层
        self.fp16_layers = set()   # 可以 fp16 的层
    
    def set_layer_precision(self, layer_name, dtype):
        """设置特定层的精度"""
        if dtype == 'fp32':
            self.fp32_layers.add(layer_name)
        elif dtype == 'fp16':
            self.fp16_layers.add(layer_name)
    
    def forward(self, x):
        for name, module in self.model.named_modules():
            if name in self.fp32_layers:
                # 该层强制 fp32
                with torch.npu.amp.autocast(enabled=False):
                    x = module(x.float())
                x = x.half() if self._is_fp16_rest() else x
            else:
                x = module(x)
        return x
    
    def _is_fp16_rest(self):
        """检查后续层是否使用 fp16"""
        return True  # 简化实现

精度敏感层策略(精度优先 vs 性能优先):

# 策略A:精度优先(所有敏感层用 fp32)
FP32_LAYERS_PRECISION_FIRST = {
    'layer_norm', 'rms_norm', 'batch_norm',
    'softmax',  # 大 logit 下 fp16 精度损失明显
    'cross_entropy',  # loss 计算建议 fp32
}

# 策略B:性能优先(仅必选层用 fp32)
FP32_LAYERS_PERFORMANCE_FIRST = {
    'layer_norm',  # 必须 fp32,否则发散
    # Softmax 用亲和 API + 优化模式可接受 fp16
    # loss 计算虽然 fp32 更好,但影响不大
}

三、显存优化

概述: 显存是 NPU 最稀缺的资源之一(特别是 Ascend 910B 的 64GB 对比 A100 的 80GB)。优化显存占用可以直接影响可训练的最大模型规模和 batch size。


3.1 显存分析与监控

# 显存状态监控
def memory_monitor(device='npu'):
    """显存监控工具"""
    if device == 'npu':
        allocated = torch.npu.memory_allocated() / 1024**3
        cached = torch.npu.memory_reserved() / 1024**3
        max_allocated = torch.npu.max_memory_allocated() / 1024**3
    else:
        allocated = torch.cuda.memory_allocated() / 1024**3
        cached = torch.cuda.memory_reserved() / 1024**3
        max_allocated = torch.cuda.max_memory_allocated() / 1024**3
    
    return {
        'allocated_gb': allocated,
        'cached_gb': cached,
        'peak_gb': max_allocated,
        'available_gb': 64 - allocated,  # 总显存 64GB
    }

# 更详细的显存报告(用于定位泄漏)
if device == 'npu':
    print(torch.npu.memory_summary())

显存泄漏检测方法:

# 逐 step 监控显存,检测泄漏
def detect_memory_leak(model, dataloader, device='npu', max_steps=100):
    """检测是否存在显存泄漏"""
    model = model.to(device)
    optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5)
    
    memory_history = []
    
    for i, (data, target) in enumerate(dataloader):
        if i >= max_steps:
            break
        
        data, target = data.to(device), target.to(device)
        
        optimizer.zero_grad()
        loss = model(data, target)
        loss.backward()
        optimizer.step()
        
        if device == 'npu':
            current = torch.npu.memory_allocated() / 1024**3
        else:
            current = torch.cuda.memory_allocated() / 1024**3
        
        memory_history.append(current)
    
    # 分析趋势
    if len(memory_history) > 10:
        # 取后 50% 和前 10% 对比
        early = sum(memory_history[:5]) / 5
        late = sum(memory_history[-5:]) / 5
        
        if late > early * 1.1:  # 显存增长超过 10%
            print(f"⚠️ 疑似显存泄漏: {early:.2f}G → {late:.2f}G")
        else:
            print(f"✅ 显存稳定: {early:.2f}G ~ {late:.2f}G")
    
    return memory_history

3.2 训练显存优化

Batch Size 阶梯搜索
def find_optimal_batch_size(model, input_shape, device='npu'):
    """通过二分搜索找到最大可用 batch size"""
    
    # 快速定位 batch size 上限
    low, high = 1, 512
    
    while low < high:
        mid = (low + high + 1) // 2
        try:
            dummy = torch.randn(mid, *input_shape[1:]).to(device)
            with torch.npu.amp.autocast():
                _ = model(dummy)
            loss = _.sum()
            loss.backward()
            
            # 清理
            del dummy, _
            torch.npu.empty_cache()
            
            low = mid  # 成功,尝试更大
            print(f"  batch_size={mid} ✅")
        except (RuntimeError, torch.npu.OutOfMemoryError):
            high = mid - 1  # 失败,减小
            torch.npu.empty_cache()
            print(f"  batch_size={mid} ❌ OOM")
    
    print(f"  → 推荐 batch_size: {low}")
    return low
梯度检查点(Gradient Checkpointing)

以计算换显存——不保存中间激活值,反向传播时重新计算:

import torch.utils.checkpoint as checkpoint

class MemoryEfficientBlock(nn.Module):
    """使用梯度检查点的 Transformer Block"""
    def __init__(self, block):
        super().__init__()
        self.block = block
    
    def forward(self, x):
        # 反向传播时不保存中间激活值,而是重新计算
        return checkpoint.checkpoint(self.block, x, use_reentrant=False)

# 使用:只在关键层启用
model = TransformerModel()
model.transformer_blocks = nn.Sequential(*[
    MemoryEfficientBlock(block) for block in model.transformer_blocks
])

显存 vs 计算时间权衡:

策略 显存节省 计算额外开销 推荐场景
全量保存 0% 0% 显存充裕
梯度检查点(全层) 50-70% 20-30% 大模型训练
梯度检查点(部分层) 30-40% 10-15% 平衡方案
ZeRO 优化器状态分片
# 使用 AscendSpeed(昇腾版 DeepSpeed)
pip install ascendspeed

# ZeRO Stage 选择
# Stage 1: 仅分片优化器状态(显存节省 ~50%)
# Stage 2: 分片优化器状态 + 梯度(显存节省 ~70%)
# Stage 3: 分片全部状态(包括模型参数,显存节省 ~90%)

# dp_zero_config.json
{
  "zero_optimization": {
    "stage": 2,
    "contiguous_gradients": true,
    "overlap_comm": true,
    "reduce_bucket_size": 5e8
  }
}

3.3 推理显存优化

KV Cache 优化
# KV Cache 共享与复用
class SharedKVCache:
    """
    PagedAttention 风格的 KV Cache 管理
    类似 vLLM 的实现思路,在昇腾上使用连续内存块管理
    """
    def __init__(self, block_size=64, num_blocks=1024, dtype=torch.float16):
        self.block_size = block_size
        self.num_blocks = num_blocks
        # 预分配所有 block
        self.k_cache = torch.zeros(
            num_blocks, block_size, dtype=dtype, device='npu'
        )
        self.v_cache = torch.zeros(
            num_blocks, block_size, dtype=dtype, device='npu'
        )
        self.free_blocks = set(range(num_blocks))
        self.allocated = {}
    
    def allocate(self, num_tokens):
        """分配 block"""
        num_blocks_needed = (num_tokens + self.block_size - 1) // self.block_size
        if len(self.free_blocks) < num_blocks_needed:
            raise RuntimeError("KV Cache 不足")
        
        blocks = []
        for _ in range(num_blocks_needed):
            block = self.free_blocks.pop()
            blocks.append(block)
        
        return blocks
连续批处理(Continuous Batching)
class ContinuousBatchingInference:
    """
    连续批处理推理:允许不同长度的请求同时推理
    不必等所有请求完成再组下一批
    """
    def __init__(self, model, max_batch=8):
        self.model = model.eval()
        self.max_batch = max_batch
        self.active_requests = {}  # request_id → state
    
    def add_request(self, request_id, input_ids):
        """添加推理请求到当前 batch"""
        if len(self.active_requests) >= self.max_batch:
            return False  # batch 已满
        
        self.active_requests[request_id] = {
            'input_ids': input_ids,
            'current_pos': 0,
            'done': False,
        }
        return True
    
    def step(self):
        """执行一步推理"""
        # 将所有活跃请求的当前 token 组为 batch
        batch_tokens = []
        active_ids = []
        for rid, state in self.active_requests.items():
            if not state['done']:
                batch_tokens.append(state['input_ids'][state['current_pos']])
                active_ids.append(rid)
        
        if not batch_tokens:
            return
        
        batch_input = torch.tensor(batch_tokens).npu()
        output = self.model(batch_input)
        # ... 处理输出并更新每个请求的状态

四、数据流水线优化

概述: 当 GPU/NPU 的计算速度足够快时,瓶颈往往转移到数据加载和预处理上。"数据跟不上计算"是性能优化的常见陷阱。


4.1 数据加载瓶颈分析

# 检查数据加载是否是瓶颈
def check_data_bottleneck(dataloader, model, device='npu', num_batches=100):
    """检测数据加载是否成为性能瓶颈"""
    
    model = model.to(device).eval()
    
    # 测量纯数据加载时间
    start_data = time.time()
    batches = []
    for i, batch in enumerate(dataloader):
        if i >= num_batches:
            break
        batches.append(batch)
    end_data = time.time()
    data_time = end_data - start_data
    
    # 测量纯计算时间(数据已在 NPU 上)
    dummy_input = torch.randn(32, 3, 224, 224).to(device)
    start_compute = time.time()
    with torch.no_grad():
        for _ in range(num_batches):
            _ = model(dummy_input)
    torch.npu.synchronize()
    end_compute = time.time()
    compute_time = end_compute - start_compute
    
    return {
        'data_time': data_time,
        'compute_time': compute_time,
        'ratio': data_time / compute_time,
        'bottleneck': 'data' if data_time > compute_time else 'compute',
    }

# ratio > 1.0 → 数据加载是瓶颈
# ratio < 0.5 → 计算是瓶颈,数据部分已足够快
num_workers 最优值搜索
def find_optimal_num_workers(dataset, batch_size, device='npu', max_workers=16):
    """搜索最优 num_workers"""
    from torch.utils.data import DataLoader
    
    results = []
    
    for workers in [0, 1, 2, 4, 8, max_workers]:
        loader = DataLoader(dataset, batch_size=batch_size, 
                          num_workers=workers, pin_memory=False)
        
        start = time.time()
        for i, _ in enumerate(loader):
            if i >= 100:
                break
        elapsed = time.time() - start
        
        results.append({'workers': workers, 'time': elapsed, 'samples_per_sec': 100 * batch_size / elapsed})
        print(f"  num_workers={workers}: {100 * batch_size / elapsed:.0f} samples/s")
    
    best = max(results, key=lambda x: x['samples_per_sec'])
    print(f"  → 推荐 num_workers={best['workers']}")
    return best


4.2 数据搬运优化

Host-Device 传输
# 使用非阻塞传输(在计算的同时传输下一批数据)
class PrefetchLoader:
    """预取数据加载器:在 NPU 计算的同时,预取下一批数据到 NPU"""
    
    def __init__(self, dataloader, device='npu'):
        self.dataloader = dataloader
        self.device = device
        self.stream = torch.npu.Stream() if device == 'npu' else torch.cuda.Stream()
        self.next_batch = None
    
    def __iter__(self):
        self.iterator = iter(self.dataloader)
        # 预取第一批
        self._prefetch()
        return self
    
    def __next__(self):
        # 等待预取的 batch 完成
        if self.device == 'npu':
            torch.npu.current_stream().wait_stream(self.stream)
        else:
            torch.cuda.current_stream().wait_stream(self.stream)
        
        batch = self.next_batch
        if batch is None:
            raise StopIteration
        
        # 开始预取下一批
        self._prefetch()
        return batch
    
    def _prefetch(self):
        """在独立 stream 上将数据搬运到 NPU"""
        try:
            data = next(self.iterator)
        except StopIteration:
            self.next_batch = None
            return
        
        if self.device == 'npu':
            with torch.npu.stream(self.stream):
                self.next_batch = [t.npu() for t in data]
        else:
            with torch.cuda.stream(self.stream):
                self.next_batch = [t.cuda() for t in data]

# 使用
loader = PrefetchLoader(DataLoader(dataset, batch_size=32, num_workers=4, pin_memory=False))
for data, target in loader:
    # data 和 target 已经在 NPU 上
    output = model(data)

五、分布式性能优化

概述: 多卡训练的性能不仅取决于单卡算力,还取决于卡间通信效率。通信开销占比过大时,增加卡数反而不能线性提升性能。


5.1 HCCL 通信优化

HCCL 环境变量调优清单
# ===== 网络配置 =====
export HCCL_IFACE=eth0                    # 通信网口(根据机器网络接口名设置)
export HCCL_SOCKET_IFNAME=eth0            # socket 通信网口(与上一致)
export HCCL_INTRA_ROCE_ENABLE=1           # 开启 RoCE 加速(如硬件支持)

# ===== 缓冲区与算法 =====
export HCCL_BUFFER_SIZE=256               # 通信缓冲区大小(MB)
export HCCL_NETWORK_DRIVER=1              # 网络驱动模式(0:简易 1:高性能)
export HCCL_ALGO_RING=1                   # Ring AllReduce(通用推荐)
export HCCL_ALGO_TREE=0                   # Tree AllReduce(大消息场景)

# ===== 超时与容错 =====
export HCCL_CONNECT_TIMEOUT=600           # 建连超时(秒)
export HCCL_EXEC_TIMEOUT=600              # 执行超时

# ===== 通信拓扑 =====
export HCCL_NPU_NUM_PER_DEVICE=1          # 每设备 NPU 数量
export HCCL_DEVICE_CONNECT_ORDER=0        # 设备连接顺序

# ===== 调试(仅调试时开启) =====
# export HCCL_DEBUG=INFO                  # 通信调试日志
# export HCCL_DEBUG_FILE=./hccl_debug.log # 日志输出文件
通信拓扑感知与亲和性设置
# 查询 NPU 拓扑(物理连接关系)
npu-smi topology -t

# 设置进程亲和性(绑定到对应 NUMA 节点)
# 8卡场景典型的卡-网口映射:
# NPU 0-3 → eth0(第一个 NUMA 节点)
# NPU 4-7 → eth1(第二个 NUMA 节点)
export HCCL_IFACE=eth0,eth1

# 进程绑定
numactl --cpunodebind=0 --membind=0 python train.py --rank=0
numactl --cpunodebind=1 --membind=1 python train.py --rank=4
通信-计算重叠策略
# 使用 HCCL 的通信计算重叠
# 原理:在计算的同时进行通信,隐藏通信延迟

# 在模型中插入通信标记
class CommunicationAwareBlock(nn.Module):
    def __init__(self, block, layer_id, total_layers):
        super().__init__()
        self.block = block
        self.layer_id = layer_id
    
    def forward(self, x):
        # 当前层计算
        x = self.block(x)
        
        # 如果开启了通信重叠,在部分层之后触发通信
        # (通常每 N 层触发一次 all-reduce,而非每层都触发)
        return x

5.2 分布式策略选择

策略 显存节省 通信开销 昇腾支持度 推荐场景
DDP 0%(同单卡) 每步通信 ✅ 完整支持 模型可装入单卡显存
FSDP (Sharding) 60-80% 每层通信 ⚠️ 需适配 大模型训练
DeepSpeed ZeRO-1 ~50% 无额外通信 ✅ AscendSpeed 优化器显存紧张
DeepSpeed ZeRO-2 ~70% 通信量增加 ✅ AscendSpeed 通用训练
DeepSpeed ZeRO-3 ~90% 通信量大幅增加 ⚠️ 需适配 超大模型训练

推荐优先级:

模型能装入单卡 → DDP(最简单,性能最好)
模型略超单卡 → ZeRO-2 / FSDP
模型远超单卡 → ZeRO-3 / FSDP

5.3 多卡扩展性评估

# 扩展性测试脚本
def scaling_test(model_fn, dataset, npus=[1, 2, 4, 8]):
    """弱扩展(Weak Scaling)和强扩展(Strong Scaling)测试"""
    
    results = []
    
    for world_size in npus:
        # 弱扩展:每卡 batch size 固定,总 batch size 随卡数增加
        # 强扩展:总 batch size 固定,每卡 batch size 随卡数减少
        
        # 这里以弱扩展为例
        per_gpu_batch = 32
        total_batch = per_gpu_batch * world_size
        
        # 启动分布式训练
        start = time.time()
        # ... 分布式训练代码 ...
        end = time.time()
        
        throughput = total_batch / (end - start)
        results.append({
            'world_size': world_size,
            'throughput': throughput,
            'speedup': throughput / results[0]['throughput'] if results else 1.0,
            'efficiency': throughput / (results[0]['throughput'] * world_size) if results else 1.0,
        })
    
    print("弱扩展测试结果:")
    print(f"{'卡数':<8} {'吞吐':<15} {'加速比':<10} {'效率':<10}")
    for r in results:
        print(f"{r['world_size']:<8} {r['throughput']:<15.1f} {r['speedup']:<10.2f}x {r['efficiency']:<10.1%}")
    
    return results

# 扩展效率验收标准
# 8卡弱扩展效率 > 80% → 良好
# 8卡弱扩展效率 > 90% → 优秀
# 8卡弱扩展效率 < 60% → 存在严重通信瓶颈

六、推理性能优化(可选)

概述: 如果只需要 PyTorch 推理(非生产级部署),直接使用 torch_npu 即可。这一章适用于需要极致推理性能或生产部署的场景。


6.1 静态 Shape 推理最佳实践

class OptimizedInference:
    """静态 Shape 推理服务的最佳实践"""
    
    def __init__(self, model, max_batch=8, max_seq_len=512):
        self.model = model.eval().npu()
        self.max_batch = max_batch
        self.max_seq_len = max_seq_len
        
        # 编译(如果支持)
        try:
            self.model = torch_npu.compile(self.model)
            print("✅ 图编译成功")
        except Exception:
            print("⚠️ 图编译失败,使用动态图")
        
        # 预热(触发编译)
        self._warmup()
    
    def _warmup(self):
        """用目标 Shape 预热"""
        dummy = torch.randint(0, 1000, (self.max_batch, self.max_seq_len)).npu()
        with torch.no_grad():
            for _ in range(5):
                _ = self.model(dummy)
        torch.npu.synchronize()
    
    @torch.no_grad()
    def infer(self, input_ids):
        """推理入口"""
        batch, seq = input_ids.shape
        assert batch <= self.max_batch, f"batch {batch} > {self.max_batch}"
        
        # padding 到固定长度(避免 Shape 变化)
        if seq < self.max_seq_len:
            pad = torch.zeros(batch, self.max_seq_len - seq, 
                            dtype=input_ids.dtype).npu()
            input_ids = torch.cat([input_ids.npu(), pad], dim=1)
        
        output = self.model(input_ids)
        return output[:, :seq].cpu()  # 截取有效部分

七、性能验收与持续优化

概述: 性能优化不是一次性的工作,而是贯穿整个开发周期的持续过程。明确的验收标准和跟踪机制是保持优化成果的关键。


7.1 性能验收清单

## 性能验收清单

### 单卡训练
□ 吞吐不低于 GPU baseline 的 80%
□ 延迟(P50)不高于 GPU baseline 的 120%
□ 峰值显存不高于 NPU 总显存的 90%
□ 无显存泄漏(100+ step 显存稳定)

### 多卡训练
□ 8卡弱扩展效率 > 80%
□ 无通信超时(长时间运行无报错)
□ 多卡吞吐不低于单卡的 8×80% = 6.4x

### 推理
□ P50 延迟满足业务要求
□ P99 延迟满足业务要求
□ 连续批处理吞吐稳定
□ 首次推理延迟(冷启动)可接受

### 稳定性
□ 连续运行 8 小时无 OOM
□ 连续运行 8 小时无性能退化
□ 环境变量变更后性能可复现

7.2 性能优化跟踪表

## 性能优化跟踪表

| 优化手段 | 预期收益 | 实际收益 | 是否保留 | 备注 |
|---------|---------|---------|---------|------|
| 开启缓存(MSRUN_GENERATE_CACHE) | +30% | +35% | ✅ | — |
| 算子缓存预热 | +10% | +8% | ✅ | 仅首次有效 |
| 亲和API替换(softmax) | +5% | +4% | ✅ | 精度验证通过 |
| LayerNorm切fp32 | -3% | -3% | ✅ | 精度必须 |
| 梯度检查点 | -20%速度/+40%显存 | -18%/+38% | ✅ | 平衡取舍 |
| num_workers=8 | +15% | +12% | ✅ | 数据瓶颈 |

### 放弃的方案
| 方案 | 原因 | 日期 |
|------|------|------|
| torch_npu.compile | 编译时间过长(30min),且部分算子报错 | 2026-07-24 |


Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐