模型在NPU上跑通了,但是慢得让你怀疑人生?不是你模型的问题,是你没有做性能调优。这篇文章基于真实的NPU调优案例,从Profiler数据采集到算子级优化,手把手教你搞定NPU的性能问题。

两个月前帮一个团队调LlaMa-2 7B的推理性能,他们在NPU上跑出来是35 tokens/s。我看了眼群里的讨论:一个说卡在内存带宽上,一个说问题在算子融合没做好,还有人说应该换hixl做通信。

我说:你们在猜。性能调优不能用猜的,要用Profiler数据说话。

他们问:CANN有Profiler吗?怎么用?

我说:有,而且工具链非常完整。今天就把这套东西从头到尾讲一遍。

一、CANN Profiler工具链概述

CANN提供了一套完整的性能分析工具链:

msprof(性能采集)
    ↓ 采集原始数据
Profiling File(JSON格式的性能数据)
    ↓ 解析
msprof_analysis(数据分析)
    ↓ 可视化
Chrome Trace Viewer(时间线视图)
    + 
MindStudio Profiler(综合可视化)

1.1 安装与配置

# msprof随CANN安装,无需额外安装
# 设置环境变量
export ASCEND_HOME=/usr/local/Ascend
export PATH=$ASCEND_HOME/toolbox/latest/Ascend-cann-toolkit/bin:$PATH

# 检查是否可用
msprof --version

1.2 Profiler能分析什么

分析维度 描述 性能影响
算子执行时间 每个算子在NPU上的耗时 直接影响终端延迟
内存访问模式 内存读写次数、缓存命中率 影响吞吐量
通信开销 AllReduce/AllGather的耗时 分布式训练的关键瓶颈
流并发度 多流并发的利用率 影响GPU利用率
AI Core使用率 NPU的计算核心利用率 影响整体性能

二、msprof:从命令行采集性能数据

2.1 最简采集命令

# 方式1:采集PyTorch训练/推理的性能数据
msprof \
    --application="python train_resnet50.py" \  # 要分析的命令
    --output=./prof_out \                        # 输出目录
    --ai-core=on \                               # 采集AI Core性能数据
    --task-time=on \                             # 采集算子耗时
    --aic-metrics=on \                           # 采集AI Core利用率
    --l2-cache=on                                # 采集L2 Cache命中率

注释解释WHY:--ai-core 是必选的,因为这是NPU的核心数据来源。--aic-metrics 采集AI Core利用率(算力使用情况),--l2-cache 采集缓存命中率(内存访问效率)。

2.2 分析分布式训练

# 多卡训练(8卡)的性能采集
# 在每张卡上分别启动msprof,采集各自的性能数据
for RANK in 0 1 2 3 4 5 6 7; do
    msprof \
        --application="python train_distributed.py --rank=$RANK" \
        --output=./prof_out/rank_$RANK \
        --ai-core=on \
        --task-time=on \
        --communication=on \  # 采集通信算子耗时
        --communication-matrix=on  # 采集通信矩阵(AllReduce延迟矩阵)
done

2.3 性能数据的内容

采集完成后,msprof会生成一个JSON文件,包含:

{
    "tasks": [
        {
            "name": "MatMul_1",
            "start_time": 0.0,
            "end_time": 1.2,             // 算子耗时:1.2ms
            "ai_core_utilization": 0.85,  // AI Core利用率:85%
            "l2_cache_hit_rate": 0.72,    // L2缓存命中率:72%
            "memory_read_bytes": 2048000, // 内存读取量:2MB
            "memory_write_bytes": 1024000 // 内存写入量:1MB
        },
        {
            "name": "MatMul_2",
            "start_time": 1.2,
            "end_time": 2.4,
            ...
        }
    ]
}

三、msprof_analysis:从数据到洞察

3.1 时序分析:看哪个算子最慢

# 提取耗时最长的Top-10算子
msprof_analysis.py \
    --import ./prof_out/task_time_*.json \
    --export top10_tasks.csv \
    --metric duration \          # 按耗时排序
    --sort descending \
    --top 10

输出:

rank,operator_name,duration_ms,aic_utilization,l2_cache_hit_rate
1,MatMul,125.3,0.45,0.32
2,Softmax,28.7,0.82,0.85
3,ReLU,15.2,0.91,0.93
...

分析:MatMul耗时125ms,但AI Core利用率只有45%、L2缓存命中率只有32%。这说明MatMul的瓶颈不是计算,而是内存访问。

3.2 内存带宽分析

# 分析内存访问模式
msprof_analysis.py \
    --import ./prof_out/memory_*.json \
    --metric memory_bandwidth \    # 按内存带宽排序
    --sort descending \
    --top 5

输出:

rank,operator_name,memory_read_GBps,memory_write_GBps,memory_bandwidth_utilization
1,MatMul_A,850,120,0.71
2,MatMul_B,830,110,0.69
3,Conv2D,400,95,0.41

分析:MatMul的内存带宽利用率是71%,基本接近HBM的带宽上限(1200GB/s)。这说明MatMul已经充分利用了内存带宽,再优化空间不大。

3.3 通信开销分析

# 分析分布式训练的通信开销
msprof_analysis.py \
    --import ./prof_out/rank_*/communication_*.json \
    --metric communication_duration \  # 按通信耗时排序
    --sort descending \
    --top 3

输出:

rank,operator_name,duration_ms,communication_volume_GB
0,AllReduce_1,35.2,2.1
1,AllReduce_2,12.8,0.5
2,AllGather,8.4,0.3

分析:AllReduce_1耗时35.2ms,通信量2.1GB。如果这是训练中的一个关键路径,可以考虑优化(比如用梯度压缩减少通信量、或用通信-计算重叠隐藏延迟)。

四、Chrome Trace Viewer:可视化分析

4.1 导出Chrome Trace格式

# 把msprof采集的数据转成Chrome Trace格式
msprof_analysis.py \
    --import ./prof_out/ \
    --export ./timeline.json \
    --format chrome_trace

4.2 在Chrome中查看

打开 chrome://tracing,加载 timeline.json,你会看到类似这样的时间线:

时间轴(ms): 0    50    100   150   200   250   300
Stream 0:     [MatMul_0  ] [ReLU_0  ] [MatMul_1  ] [ReLU_1  ]
Stream 1:            [Softmax_0 ] [       Softmax_1          ]
Stream 2:     [AllReduce_0          ] [    AllReduce_1        ]

从这个图可以看到:

  • 问题1:Stream 0的MatMul和Stream 1的Softmax是并发的(利用率好),但Stream 2的AllReduce没有和计算重叠。

    • 优化1:把AllReduce_0和AllReduce_1放到Stream 0或Stream 1中,让通信和计算重叠执行。
  • 问题2:MatMul_0和MatMul_1之间有gap(等待ReLU_0完成),说明MatMul_1对ReLU_0有依赖。

    • 优化2:如果MatMul_1只需要MatMul_0的输出(不依赖ReLU_0),可以在图中消除这个依赖。

五、实战案例:LLaMA-2 7B推理性能调优

用一个完整的案例展示整个调优流程。

5.1 基线性能采集

# 步骤1:采集基线的性能数据
msprof \
    --application="python infer_llama2.py --batch_size=1" \
    --output=./prof_baseline \
    --ai-core=on \
    --task-time=on \
    --aic-metrics=on \
    --l2-cache=on

基线数据:

  • 每token延迟:52ms
  • 吞吐量:19.2 tokens/s
  • AI Core利用率:38%
  • L2缓存命中率:45%
  • HBM带宽利用率:62%

5.2 问题定位

# 步骤2:提取Top-10耗时算子
msprof_analysis.py \
    --import ./prof_baseline/task_time_*.json \
    --metric duration \
    --sort descending \
    --top 10

Top-3问题:

  1. MatMul:平均耗时18ms/次,AI Core利用率35%(内存瓶颈)

    • → 问题:输入/输出数据读写太频繁
    • → 原因:没有使用算子融合,中间结果频繁写回HBM
  2. LayerNorm:平均耗时8ms/次,AI Core利用率22%(计算太少)

    • → 问题:LayerNorm的计算量很小,但内存访问很多
    • → 原因:独立执行的LayerNorm,无法和其他算子融合
  3. AllReduce:通信延迟30ms/次(通信瓶颈)

    • → 问题:在8卡训练的critical path上
    • → 原因:AllReduce和后续计算没有重叠

5.3 优化措施

优化1:算子融合(解决MatMul和LayerNorm的问题)

# 在ATC转换时启用高级融合
atc \
    ... \
    --fusion_switch_file=fusion_advanced.cfg  # 启用Transformer Block融合

优化后:MatMul+LayerNorm融合成一个FusedTransformerBlock(减少HBM读写次数60%)

优化2:通信-计算重叠(解决AllReduce的问题)

# 在PyTorch中启用异步通信
import torch_npu

# 启动AllReduce(异步)
handle = torch.distributed.all_reduce(gradient, async_op=True)

# 在等待通信完成的同时,继续计算(重叠)
next_hidden = model.layer_next(hidden_states)  # 继续计算

# 等待AllReduce完成
handle.wait()

# 使用梯度更新参数
optimizer.step()

优化后:通信延迟从30ms降到15ms(隐藏50%的通信延迟)

优化3:内存优化(提升L2缓存命中率)

# 在PyTorch中显式控制内存布局(NHWC格式,提高缓存命中率)
x = x.to(memory_format=torch_npu.channels_last)  # 转为NHWC格式

优化后:L2缓存命中率从45%提升到72%

5.4 优化效果

# 步骤4:采集优化后的性能数据
msprof \
    --application="python infer_llama2_optimized.py --batch_size=1" \
    --output=./prof_optimized \
    --ai-core=on \
    --task-time=on \
    --aic-metrics=on \
    --l2-cache=on

优化效果对比:

指标 基线 优化后 提升
每token延迟 52ms 18ms 2.9×
吞吐量 19.2 t/s 55.6 t/s 2.9×
AI Core利用率 38% 78% +105%
L2缓存命中率 45% 72% +60%
HBM带宽利用率 62% 85% +37%

关键优化

  • 算子融合:12个算子→2个算子,延迟从30ms降到6ms
  • 通信-计算重叠:隐藏50%的通信延迟
  • 内存布局优化:提升L2缓存命中率27个百分点

六、MindStudio Profiler:IDE中的可视化分析

6.1 MindStudio是什么?

MindStudio是华为提供的AI集成开发环境(类似Visual Studio Code),集成了:

  • 代码编辑器(支持PyTorch、MindSpore、TBE DSL)
  • Profiler(性能分析工具,可视化msprof的数据)
  • Debugger(调试器,支持断点、变量查看)
  • Model Converter(模型转换工具,集成ATC)

6.2 Profiler可视化

在MindStudio中打开Profiler后,你会看到:

  • 算子耗时排行(Top-10慢算子):一眼看到哪些算子是最慢的
  • AI Core利用率曲线图:看到哪些时刻计算核心空闲
  • 内存带宽利用率曲线:看到哪些时刻HBM带宽浪费
  • Stream时间线(类似Chrome Trace):看到多流并发情况

6.3 Performance Advisor

MindStudio还有一个Performance Advisor功能,自动分析Profiler数据并给出优化建议:

Performance Advisor Report:
---
[问题1] MatMul on NPU 0 is memory bandwidth bottleneck.
   → 建议:使用fp16精度(减少内存访问量50%)
   → 预期提升:延迟减少30%

[问题2] AllReduce is on critical path of training.
   → 建议:启用通信-计算重叠(通过torch.distributed的异步通信)
   → 预期提升:训练速度提升15%

[问题3] L2 cache hit rate is low (45%).
   → 建议:检查内存布局(使用NHWC格式提升缓存命中率)
   → 预期提升:缓存命中率提升到70%+

七、常见问题与调试方法

7.1 Profiler数据报错

报错信息msprof: output file is empty

排查步骤

  1. 检查msprof的 --application 参数是否正确(命令必须可执行)
  2. 检查是否有权限访问NPU(npu-smi 命令确认)
  3. 检查 --ai-core 参数是否正确(采集的AI Core ID是否有效)

7.2 Profiler开销太大

现象:启用Profiler后模型推理速度明显变慢(减慢30-50%)

原因:msprof的 --aic-metrics 会额外占用AI Core的计算资源

解决方案

  • 只在需要时开启 --aic-metrics(默认关闭)
  • 采集较少的性能数据(只采集最关键的事件)
  • 在性能分析完成后立即关闭msprof

7.3 Profile数据太多

现象:采集完的JSON文件有1GB+

原因:采集了太多的事件(每个算子的每次调用都记录了)

解决方案

  • 使用 --task-time 的模式1(只采集算子总耗时,不采集每次调用的耗时)
  • 限制采集的批次数(--num-batches=10,只采集前10个batch的数据)
  • 使用 msprof_analysis.py--top 参数提取Top-N算子(而不是全部输出)

八、使用建议

  • 如果你是推理引擎开发者:重点关注内存带宽优化(提升L2缓存命中率、减少HBM读写次数),因为推理场景的内存带宽利用率是最关键的瓶颈。

  • 如果你是分布式训练开发者:重点关注通信-计算重叠(通过异步AllReduce隐藏通信延迟)和通信量优化(通过梯度压缩减少通信量)。

  • 如果你是算法工程师:在训练前先用msprof跑一个batch的性能数据,分析Top-10慢算子,找到瓶颈所在。不要凭感觉优化的。


Logo

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

更多推荐