
1. 为什么要在昇腾上做训练性能分析1.1 从一次真实的性能翻车说起去年帮一个团队调一个基于昇腾910B的ViT-Large训练任务单卡跑得挺欢一上8卡数据并行就出问题了吞吐量不升反降从单卡的每秒320张掉到8卡的每秒180张。第一反应是通信瓶颈但HCCL的带宽监控显示通信占比只有12%明显不是主因。后来用torch_npu的profiler抓了一轮数据打开kernel_details一看问题一目了然——某个自定义的LayerNorm反向kernel在8卡场景下耗时暴涨了6倍原因是算子在小batch下走了不同的tiling策略多卡切分后每个卡上的实际batch变小触发了低效分支。这件事让我意识到一个很现实的问题昇腾上的性能问题光靠看loss曲线和nvidia-smi式的粗粒度监控是抓不住的。你必须下沉到kernel级别看清楚每一个算子在NPU上到底花了多少时间、调用了什么类型的计算单元、有没有发生不必要的同步。而torch_npu profiler就是干这个事的工具。1.2 torch_npu profiler到底能给你什么简单说torch_npu profiler是PyTorch profiler在昇腾NPU上的适配实现。它复用了PyTorch profiler的采集框架但在底层接入了CANN的profiling能力能拿到NPU侧的真实执行数据。你用它采集一轮训练导出的数据里包含几个核心维度算子级耗时每个NPU kernel的执行时间精确到微秒AiCMetrics昇腾特有的硬件指标包括AI Core利用率、Cube/Vector单元占比、内存带宽占用等算子下发链路从Python侧调用到NPU实际执行的完整时间线能看出host侧下发是否成为瓶颈通信算子详情HCCL AllReduce、AllGather等集合通信的耗时和带宽这些数据最终会落到一个kernel_details.csv文件里这也是本文重点要解读的对象。很多人采集完了不知道怎么读这个文件或者只看了个总耗时排名就完事了其实里面藏着的信息量远超你的想象。1.3 适合谁来读这篇内容如果你正在做以下事情这篇内容应该能帮到你在昇腾NPU上训练模型发现性能不如预期但不知道瓶颈在哪已经会用torch_npu profiler采集数据但面对导出的csv和json不知道怎么分析想搞清楚AiCMetrics里那些指标到底意味着什么怎么用来指导优化遇到了npu is selected as device, but torch_npu is not available这类环境问题顺带想了解profiler的正确使用姿势我假设你已经能在昇腾上跑通基本的PyTorch训练流程对PyTorch profiler有基本概念。如果没有也没关系我会在关键地方补充背景知识。2. 采集前的环境准备与避坑2.1 确认torch_npu和CANN的版本匹配这是最容易翻车的地方。torch_npu的profiler功能依赖CANN的msprof工具链两者版本必须匹配。我见过太多次因为版本不对导致采集出来的数据缺字段或者直接报错的情况。先确认你的环境# 查看CANN版本 cat /usr/local/Ascend/ascend-toolkit/latest/version.cfg # 查看torch_npu版本 python -c import torch_npu; print(torch_npu.__version__) # 确认NPU设备可见 npu-smi info版本对应关系大致是这样的以常见的几个组合为例torch_npu版本CANN版本PyTorch版本profiler支持情况2.1.07.0.RC12.1.0完整支持kernel_details和AiCMetrics2.0.06.3.RC22.0.0支持kernel_detailsAiCMetrics字段较少1.11.06.0.RC11.11.0基础profiler支持建议升级注意如果你看到npu is selected as device, but torch_npu is not available这个报错八成是torch_npu没有正确安装或者环境变量没配好。先检查import torch_npu是否能成功再检查ASCEND_HOME环境变量是否指向了正确的CANN路径。2.2 profiler相关的环境变量CANN的profiling功能受几个环境变量控制采集前建议显式设置# 开启profiling功能 export PROFILING_MODEtrue # 设置profiling数据落盘路径 export PROFILING_OPTIONS{output:/home/profiling_data,training_trace:on,task_trace:on,aicpu:on,aic_metrics:PipeUtilization} # 确保日志级别不会淹没关键信息 export ASCEND_GLOBAL_LOG_LEVEL3其中aic_metrics这个参数很关键它决定了AiCMetrics采集哪些硬件指标。常用的几个选项PipeUtilization采集流水线利用率包括Cube、Vector、MTE等单元的占用率最常用ArithmeticUtilization采集算术单元利用率关注MAC吞吐Memory采集内存带宽和L2命中率MemoryL0更细粒度的L0缓存指标我一般先用PipeUtilization做第一轮分析定位到具体算子后再用Memory或ArithmeticUtilization做深入。2.3 采集代码的正确写法torch_npu profiler的API和PyTorch原生profiler基本一致但有几个昇腾特有的参数需要注意import torch import torch_npu from torch_npu.profiler import profile, ProfilerActivity from torch_npu.profiler import tensorboard_trace_handler # 初始化NPU设备 device torch.device(npu:0) torch.npu.set_device(device) # 构造模型和数据 model YourModel().to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-4) # profiler配置 experimental_config torch_npu.profiler._ExperimentalConfig( aic_metricstorch_npu.profiler.AiCMetrics.PipeUtilization, profiler_leveltorch_npu.profiler.ProfilerLevel.Level1, l2_cacheFalse, data_simplificationFalse ) with profile( activities[ ProfilerActivity.CPU, ProfilerActivity.NPU ], scheduletorch_npu.profiler.schedule( wait2, # 前2步不采集等系统稳定 warmup2, # 2步预热 active5, # 采集5步 repeat1 ), on_trace_readytensorboard_trace_handler(./prof_result), experimental_configexperimental_config ) as prof: for step, data in enumerate(dataloader): if step 11: # waitwarmupactive 225 9多跑几步保险 break outputs model(data) loss criterion(outputs, labels) loss.backward() optimizer.step() optimizer.zero_grad() prof.step()几个关键点解释一下schedule参数控制采集节奏。wait阶段让系统先跑几步进入稳态避免采集到冷启动的异常数据。warmup阶段让profiler自身初始化完成。active才是真正采集的步数。我一般采集5-10步就够了步数太多数据量大分析起来反而累。profiler_level决定采集粒度。Level0只采集算子级信息Level1额外采集AiCMetricsLevel2最详细但开销也最大。日常分析用Level1足够。data_simplification设为False可以保留完整的调用栈信息方便追溯是Python哪一行代码触发的算子。代价是数据文件会大一些。2.4 采集开销与注意事项profiler本身有开销实测下来大概会让训练速度降低20%-40%具体取决于采集粒度和模型复杂度。所以千万不要在生产训练任务上一直开着profiler只在需要分析的时候开几步。另外几个踩过的坑采集时不要同时开多个profiler实例会互相干扰如果模型有动态shape确保采集的几步shape一致否则kernel_details里会出现大量不同的kernel变体分布式训练时每个rank都会生成自己的profiling数据建议只采集rank0或者指定一个rank减少数据量采集完成后记得把环境变量PROFILING_MODE关掉否则后续训练会一直产生profiling日志3. kernel_details.csv的字段全解读3.1 文件结构概览采集完成后在输出目录下会生成一个ASCEND_PROFILER_OUTPUT文件夹里面最重要的就是kernel_details.csv。这个文件每一行代表一次kernel执行记录包含了几十个字段。第一次打开可能会觉得眼花缭乱但其实核心字段就那么几个。先看一个典型的kernel_details.csv的字段列表字段名含义重要程度Device_idNPU设备编号低Stream_id执行流ID中Task_id任务ID低Kernel_namekernel名称高Kernel_typekernel类型AI_CORE/AI_CPU/AI_VECTOR等高Duration(us)执行耗时微秒高Wait_time(us)等待时间中Block_dim核数中Grid_dim网格维度低Start_time(us)开始时间戳中AIC_MAC_RatioAI Core MAC利用率高AIV_VEC_RatioVector单元利用率高MTE1_RatioMTE1流水线利用率中MTE2_RatioMTE2流水线利用率中MTE3_RatioMTE3流水线利用率中Cube_RatioCube单元利用率高3.2 Kernel_name从命名看算子来源Kernel_name是最直观的字段但很多人只看到一堆乱码般的字符串就放弃了。其实昇腾的kernel命名是有规律的读懂命名能快速定位算子来源。常见的命名模式te_xxxTensor Engine生成的算子通常是框架自动融合产生的aiv_xxxAI Vector Core上执行的算子多为element-wise操作aic_xxxAI Core上执行的算子多为矩阵乘、卷积等计算密集型操作HcclxxxHCCL通信算子如HcclAllReduceaclnn_xxx通过aclnn接口调用的算子xxx_forward/xxx_backward前向/反向算子举个例子如果你看到te_matmul_128_1024_512这样的名字说明这是一个Tensor Engine生成的矩阵乘算子shape是128x1024x512。再比如aiv_add_0表示一个Vector Core上的加法算子。实操心得在分析时先把Kernel_name按前缀分组统计每组的耗时占比。这样能快速看出是计算算子、通信算子还是element-wise算子占了大头。我一般用pandas一行代码搞定df.groupby(df[Kernel_name].str.extract(r^(\w?)_)[0])[Duration(us)].sum().sort_values(ascendingFalse)。3.3 Duration与Wait_time区分真忙和假忙Duration(us)是kernel的实际执行时间这个大家都懂。但Wait_time(us)这个字段容易被忽略它表示kernel在流上等待被调度的时间。这两个字段的关系能告诉你很多信息Duration高、Wait_time低kernel本身计算量大是真正的计算瓶颈Duration低、Wait_time高kernel本身很快但被前面的任务阻塞了瓶颈在别处Duration高、Wait_time也高可能是资源竞争严重多个kernel抢同一个计算单元我遇到过一个案例某个模型的Duration总和只占step时间的60%但step时间就是下不来。后来看Wait_time发现大量小kernel在等待一个大的AllReduce通信完成导致计算流被阻塞。这种情况下优化计算kernel没用得去优化通信策略。3.4 AiCMetrics字段硬件到底在干什么这是昇腾profiler最有价值的字段组也是和GPU profiler最大的区别。AiCMetrics告诉你kernel执行期间NPU内部的各个计算单元到底有多忙。几个核心指标的含义Cube_RatioCube矩阵计算单元的利用率。昇腾的AI Core里Cube单元专门负责矩阵乘加运算。如果这个值很低比如低于30%说明矩阵计算没有喂饱可能是数据搬运跟不上或者tiling策略不合理。AIV_VEC_RatioVector单元的利用率。Vector单元负责element-wise运算、激活函数、归一化等。如果这个值高但Cube_Ratio低说明模型里element-wise操作占比过大。MTE1_Ratio / MTE2_Ratio / MTE3_Ratio分别是L1到L0A/L0B的搬运、GM到L1的搬运、L0C到GM的搬运流水线利用率。如果MTE2很高而Cube很低典型的搬运瓶颈——数据搬进来了但算不过来。AIC_MAC_RatioMAC乘累加单元的实际利用率。这个值直接反映了算力浪费程度。一个健康的计算密集型kernel理想状态下Cube_Ratio应该在70%以上MTE2_Ratio不超过50%。如果反过来MTE2高Cube低那就要考虑优化数据复用或者调整tiling了。3.5 Block_dim与Grid_dim并行度够不够Block_dim表示这个kernel用了多少个AI Core来并行执行。昇腾910B有20个AI Core具体数量取决于型号如果Block_dim远小于这个数说明并行度不足。比如一个矩阵乘kernelBlock_dim只有4那意味着只用了4个核剩下16个核在闲着。这种情况通常发生在矩阵维度太小、切分不够细的时候。解决办法是调整tiling策略把大矩阵切成更多小块让更多核参与计算。Grid_dim则是更上层的网格维度一般和Block_dim配合看。如果Grid_dim很大但Block_dim很小说明任务被切得很碎但每个核分到的活太少调度开销可能反而成了瓶颈。4. 从数据到结论典型性能问题分析实战4.1 案例一Cube利用率低导致的算力浪费先说一个我实际调过的案例。模型是一个BERT-Large在昇腾910B上单卡训练发现FPS只有预期的一半。采集profiler数据后按Duration排序取Top20的kernelimport pandas as pd df pd.read_csv(kernel_details.csv) top20 df.nlargest(20, Duration(us)) print(top20[[Kernel_name, Duration(us), Cube_Ratio, MTE2_Ratio, Block_dim]])输出结果里排第一的是一个te_batch_matmulDuration占了总时间的35%但Cube_Ratio只有22%MTE2_Ratio却高达78%。这是典型的搬运瓶颈——数据从GM搬到L1的时间远大于实际计算时间。进一步看Block_dim只有8。而BERT-Large的attention矩阵乘batch32、seq512、head16的情况下完全可以切出更多并行块。优化方案调整矩阵乘的tiling参数增大L1缓存的数据复用率同时把Block_dim提到16。改完之后Cube_Ratio从22%涨到61%MTE2_Ratio降到45%这个kernel的耗时直接降了40%。4.2 案例二小算子过多导致的调度开销另一个常见问题是element-wise小算子太多。昇腾的AI Core执行一个kernel有固定的启动开销大概几微秒如果大量kernel的Duration只有几微秒那大部分时间都花在调度上了。判断方法很简单统计Duration小于10微秒的kernel数量和总耗时占比。small_kernels df[df[Duration(us)] 10] print(f小算子数量: {len(small_kernels)}, 占比: {len(small_kernels)/len(df)*100:.1f}%) print(f小算子总耗时: {small_kernels[Duration(us)].sum():.0f}us) print(f占总耗时: {small_kernels[Duration(us)].sum()/df[Duration(us)].sum()*100:.1f}%)我之前遇到一个模型小算子数量占比65%总耗时占比28%。这意味着近三成的时间花在了启动-执行-结束的循环上而不是真正的计算。解决办法有两个方向一是用torch_npu的算子融合功能把连续的element-wise操作合并成一个kernel二是在模型层面减少不必要的中间操作比如把add layernorm dropout合并成一个自定义算子。4.3 案例三通信与计算的重叠分析分布式训练时通信和计算能不能重叠是性能的关键。kernel_details里可以通过时间戳分析这一点。思路是找出所有Hccl开头的通信kernel看它们的Start_time是否和计算kernel的Start_time有重叠。如果没有重叠说明通信是串行的白白浪费了计算资源。comm_kernels df[df[Kernel_name].str.startswith(Hccl)] compute_kernels df[~df[Kernel_name].str.startswith(Hccl)] # 计算通信总耗时 comm_time comm_kernels[Duration(us)].sum() total_time df[Duration(us)].sum() print(f通信耗时占比: {comm_time/total_time*100:.1f}%) # 检查重叠情况简化版实际需要按时间区间求交集 for _, comm in comm_kernels.iterrows(): overlap compute_kernels[ (compute_kernels[Start_time(us)] comm[Start_time(us)] comm[Duration(us)]) (compute_kernels[Start_time(us)] compute_kernels[Duration(us)] comm[Start_time(us)]) ] if len(overlap) 0: print(f通信kernel {comm[Kernel_name]} 无重叠)如果发现大量通信kernel没有和计算重叠可以考虑使用梯度累积减少通信频率调整HCCL的流优先级让通信在后台流执行检查是否因为同步操作如loss.item()打断了重叠4.4 常见问题速查表现象可能原因排查方法解决方向Cube_Ratio低、MTE2_Ratio高数据搬运瓶颈检查L1缓存命中率优化tiling增大数据复用小算子数量占比高算子融合不足统计Duration10us的kernel开启算子融合合并element-wise通信无重叠流同步或依赖检查时间戳重叠调整流优先级减少同步点Block_dim远小于核数并行度不足对比Block_dim和硬件核数调整切分策略增大并行块Wait_time异常高资源竞争或依赖阻塞按Stream_id分组分析检查是否有大kernel阻塞后续任务AIC_MAC_Ratio低算力浪费结合Cube_Ratio一起看检查是否有无效计算或padding过多5. 进阶技巧让profiler数据真正指导优化5.1 用时间线视图定位端到端瓶颈kernel_details.csv是表格数据适合做统计分析。但要看清楚kernel之间的依赖关系和执行顺序还得靠时间线视图。torch_npu profiler导出的数据可以用Chrome tracing打开chrome://tracing或者用Perfetto UI。在时间线视图里你能直观看到哪些kernel是串行执行的哪些是并行的计算流和通信流之间的同步点在哪里host侧下发和device侧执行之间的gap有多大我一般会先用时间线视图找到大的空洞——也就是NPU闲着没事干的时间段然后再回到kernel_details里找原因。5.2 对比分析优化前后的量化验证做性能优化最怕的就是感觉快了。profiler数据的好处是可以做量化对比。建议每次优化前后都采集一轮数据然后对比几个核心指标def compare_profiling(before_csv, after_csv): df_before pd.read_csv(before_csv) df_after pd.read_csv(after_csv) metrics { 总耗时(us): (Duration(us), sum), Kernel数量: (Duration(us), count), 平均Cube利用率: (Cube_Ratio, mean), 平均MTE2利用率: (MTE2_Ratio, mean), } for name, (col, agg) in metrics.items(): b getattr(df_before[col], agg)() a getattr(df_after[col], agg)() change (a - b) / b * 100 print(f{name}: {b:.1f} - {a:.1f} ({change:.1f}%))这种对比能帮你确认优化是否真的有效以及有没有引入新的瓶颈。5.3 几个容易忽略的细节Stream_id的妙用昇腾上不同的计算流对应不同的Stream_id。计算流、通信流、拷贝流通常在不同的Stream上。按Stream_id分组统计能快速看出哪个流是瓶颈。Task_id的连续性Task_id是递增的如果发现Task_id有跳跃说明中间有任务被调度到了其他设备或者被取消了。这在分布式场景下能帮你发现负载不均的问题。Start_time的基准不同rank的Start_time基准可能不同做跨rank对比时要先做时间对齐。我一般用每个rank的第一个kernel的Start_time作为基准做归一化。数据量控制如果采集步数太多kernel_details.csv可能几十万行pandas读起来都费劲。建议采集5步左右分析时如果发现某一步有异常再单独采集那一步做深入分析。5.4 从profiler数据到优化决策的完整链路最后梳理一下我通常的分析流程供参考第一步看全局。统计总耗时、kernel数量、通信占比对整体情况有个判断。第二步找大头。按Duration排序取Top20 kernel看它们的AiCMetrics判断是计算瓶颈、搬运瓶颈还是调度瓶颈。第三步查异常。统计小算子占比、Wait_time异常值、Block_dim不足的kernel这些往往是隐藏的性能杀手。第四步看重叠。分析通信和计算的重叠情况分布式场景下这一步很关键。第五步做对比。优化前后各采集一轮量化验证效果避免负优化。这套流程走下来大部分性能问题都能定位到具体原因。剩下的就是针对性地调整tiling、融合算子、优化通信策略了。profiler数据不会直接告诉你答案但它会告诉你该往哪个方向找答案。