【HCIE-AI】11.模型 昇腾迁移适配-精度调试-性能调优
目录
1.1 torch.cuda → torch.npu 逐项替换清单
distributed_train.py 迁移前后 diff
2.3.1 init_process_group(backend=‘hccl’)
3.1.2 自定义 CUDA 算子 → TBE / DSL 重写
3.3.1 AMCT(昇腾模型压缩工具)替代 bitsandbytes
方案B:adc precision_compare(华为官方工具)
昇腾迁移适配
概述: 脚本迁移是整个迁移工作的第一步,也是改动量最大的一步。核心思路是将所有
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/3d, nn.ConvTranspose2d |
✅ 已适配 AI Core |
| 归一化 | nn.LayerNorm, nn.BatchNorm1d/2d, nn.RMSNorm |
✅ Vector Unit 加速 |
| 激活 | nn.ReLU, nn.GELU, nn.SiLU, nn.Tanh |
✅ 融合进激活算子 |
| 注意力 | nn.MultiheadAttention |
✅ 支持 |
| 池化 | nn.MaxPool2d, nn.AvgPool2d |
✅ 支持 |
| 线性 | nn.Linear, nn.Bilinear |
✅ Cube Unit 加速 |
| 嵌入 | nn.Embedding |
✅ 支持 |
| Dropout | nn.Dropout, nn.Dropout2d |
✅ 支持 |
| 损失 | nn.CrossEntropyLoss, nn.MSELoss, nn.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 |
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)