资讯动态

torch_npu profiler 实战:kernel_details 逐列解读与性能瓶颈定位

发布时间:2026/9/26 5:58:03 来源:尧图企业网站定制
1. 为什么需要认真对待 torch_npu profiler1.1 从一次真实的性能排查说起前阵子帮一个团队看昇腾上的训练慢问题。他们的场景很典型模型在 GPU 上跑得好好的迁到昇腾 NPU 之后单步耗时翻了将近一倍但看 loss 曲线又完全正常loss 下降速度也没问题就是单纯地慢。团队一开始怀疑是算子没适配好又怀疑是通信瓶颈折腾了好几天没定位到根因。我接手之后做的第一件事不是改代码而是先把torch_npu.profiler打开采一段几十个 step 的数据下来然后直接去看kernel_details.csv。结果非常清晰某个 reshape 相关的算子单次耗时只有几微秒但一个 step 里被调用了上万次累计起来占了将近 40% 的 device 时间。问题根本不在“某个算子慢”而在于“算子调用次数爆炸”。这种问题你不看 profiler 的明细数据靠猜是永远猜不到的。这就是我想写这篇东西的原因。torch_npu profiler这套工具链很多人知道它存在也知道大概怎么开但真正能把采集到的数据读懂、能从kernel_details里挖出有效信息的人并不多。大部分人的使用方式停留在“跑一下看看有没有报错”采完一堆文件放在那里不知道怎么下手。这篇就按我自己的实操习惯从采集配置一路讲到kernel_details的逐列解读把中间那些文档里不会写的坑一并说清楚。1.2 这套工具到底解决什么问题先把定位说清楚。torch_npu是 PyTorch 在昇腾 NPU 上的适配层它提供的profiler接口本质上是把 PyTorch 原生的torch.profiler能力对接到昇腾的底层性能采集框架上。你调用的是 PyTorch 风格的 API但底层采集的是昇腾 CANN 这一层的算子执行、内存拷贝、同步等待等硬件级事件。它能回答的问题大致分三类。第一类是时间去哪了每个算子在 device 上实际执行了多久host 侧下发花了多久两者之间的 gap 在哪里。第二类是调用是否合理哪些算子被高频调用哪些算子存在冗余哪些同步操作打断了流水。第三类是资源使用情况显存HBM占用峰值、AiCore 利用率、算子下发队列的排队情况。适合读这篇的人我大致分一下如果你正在昇腾上做模型训练感觉性能不达预期但不知道从哪查这篇能给你一套完整的排查路径如果你已经在用 profiler 但只会看总耗时这篇能帮你把kernel_details这张表真正用起来如果你是刚接触昇腾的新手建议先把环境跑通再回来看因为里面涉及不少 CANN 层面的概念。提示profiler 采集本身有开销采出来的绝对耗时不能直接当作线上性能指标。它的价值在于相对占比和调用关系而不是绝对数值。这一点后面会反复强调。2. 采集前的环境确认与配置选型2.1 先确认 torch_npu 真的可用在动手采集之前有个前置检查必须做而且这一步踩坑的人特别多。很多人会遇到这样的报错NPU is selected as device, but torch_npu is not available. Please ensure...这个报错的意思是你的代码里指定了devicenpu但torch_npu这个包没有被正确导入或者版本不匹配。注意它不是说你的 NPU 坏了而是 Python 层面根本没把 torch_npu 挂上去。正确的导入顺序是这样的import torch import torch_npu # 必须在 import torch 之后且在使用 npu 设备之前 # 验证是否可用 print(torch_npu.npu.is_available()) print(torch_npu.npu.device_count())这里有个细节torch_npu的导入会去 patchtorch的一些内部接口所以顺序不能反。如果你先import torch_npu再import torch在某些版本组合下会出现奇怪的属性缺失。我个人的习惯是永远保持torch在前。另外版本匹配是重灾区。torch_npu的版本必须和torch版本、CANN 版本三者对齐。比如 torch 2.1 对应某一批 torch_npu 版本CANN 又是另一条版本线。三者错配的典型表现就是导入不报错但一开 profiler 就崩或者采出来的数据缺字段。确认版本的方式# 查看 torch 和 torch_npu 版本 python -c import torch; print(torch.__version__) python -c import torch_npu; print(torch_npu.__version__) # 查看 CANN 版本 cat /usr/local/Ascend/ascend-toolkit/latest/version.cfg注意不要迷信“能 import 就说明环境没问题”。profiler 依赖 CANN 里的 msprof 采集组件如果 CANN 安装不完整比如只装了 runtime 没装 toolkitimport 是好的但采集会失败。2.2 profiler 的几种采集模式怎么选torch_npu.profiler提供了几种不同的采集配置选错了要么采不到想要的数据要么采出来的文件大到打不开。我把它归纳成三种典型模式对应不同的排查目标。采集模式关键配置适用场景数据量轻量模式只开activities的 CPU 和 NPU快速看整体耗时分布小标准模式开activitiesprofiler_level基础级常规性能分析看算子明细中深度模式开profiler_level高级 aic_metrics定位 AiCore 利用率、流水问题大轻量模式适合“我就想知道大概哪块慢”采几十个 step 也就几 MB。标准模式是我日常用得最多的能拿到kernel_details的完整信息。深度模式要谨慎它会采集 AiCore 的硬件计数器文件体积可能膨胀到几个 GB而且采集开销明显会显著拖慢训练。配置的写法大致是这样import torch_npu experimental_config torch_npu.profiler._ExperimentalConfig( aic_metricstorch_npu.profiler.AiCMetrics.PipeUtilization, profiler_leveltorch_npu.profiler.ProfilerLevel.Level1, l2_cacheFalse, ) with torch_npu.profiler.profile( activities[ torch_npu.profiler.ProfilerActivity.CPU, torch_npu.profiler.ProfilerActivity.NPU, ], scheduletorch_npu.profiler.schedule( wait1, warmup1, active3, repeat1 ), on_trace_readytorch_npu.profiler.tensorboard_trace_handler(./prof_result), experimental_configexperimental_config, ) as prof: for step, data in enumerate(dataloader): train_one_step(data) prof.step()这里schedule的参数值得单独说。wait1表示第一个 step 不采warmup1表示第二个 step 预热采集但不记录active3表示接下来三个 step 正式记录。为什么要 warmup因为第一个 step 往往包含算子编译、内存池初始化这些一次性开销采进去会污染数据。我一般会把 warmup 设成 2 到 3确保进入稳态。2.3 AiCMetrics 到底该不该开AiCMetrics是深度模式的核心它控制采集哪些硬件级指标。常见的有PipeUtilization流水线利用率、ArithmeticUtilization计算单元利用率、Memory访存相关等。我的建议是第一次排查不要开。原因有两个。第一开了之后采集开销大可能把原本的性能问题掩盖掉或者放大。第二AiCore 指标解读门槛高新手很容易被一堆百分比绕晕。正确的顺序是先用标准模式定位到“哪个算子有问题”再针对性地开 AiCMetrics 去看“这个算子为什么有问题”。如果你确实要开PipeUtilization是最通用的一个它能告诉你 MTE内存搬运、Vector、Cube 这几条流水线的占用情况。一个典型的判断逻辑如果某个算子 MTE 利用率很高而 Cube 很低说明它卡在数据搬运上可能是数据布局不合理反过来 Cube 高 MTE 低说明计算密集属于正常。3. kernel_details 逐列拆解与读法3.1 这张表到底记录了什么采集完成后输出目录里会有一堆文件kernel_details.csv是其中最核心的一张。它记录的是每一个算子在 device 上的执行明细一行对应一次算子执行。注意是“一次执行”不是“一种算子”所以同一个算子被调用一万次这里就有一万行。先看它有哪些列我按重要性排一下列名含义读法要点Name算子名称含算子类型和 shape 信息Type算子类别如 AI_CORE、AI_CPU、MIX_AICAccelerator Core执行核心AiCore 编号Start Time开始时间单位微秒相对时间轴Duration执行时长单位微秒核心指标Wait Time等待时长反映调度排队情况Block Dim核数实际使用的 AI Core 数量Input Shapes输入张量形状定位 shape 相关问题的关键Input Data Types输入数据类型排查精度和转换开销很多人拿到这张表第一反应是按Duration降序排看最慢的算子。这个做法对但只对了一半。因为最慢的单个算子往往不是瓶颈真正吃时间的是“单个不慢但调用次数极多”的算子。所以正确的第一步是按 Name 聚合算总耗时。3.2 Duration 和 Wait Time 的区别这两个字段是最容易被混淆的。Duration是算子真正在 AiCore 上执行的时间Wait Time是算子从下发到真正开始执行之间的等待时间。打个比方Duration是你办事的实际时间Wait Time是你排队的时间。如果Wait Time很长而Duration很短说明问题不在算子本身而在于调度——可能是前面的算子把流水线堵住了也可能是 host 侧下发太慢导致 device 空等。我遇到过一个案例某个小算子的Duration只有 2 微秒但Wait Time高达 800 微秒。一查发现是前面有个同步操作类似synchronize把流水线打断了导致后面所有算子都在排队。这种问题你只看Duration是永远发现不了的。判断标准我一般这么定如果某个算子的Wait Time超过Duration的 5 倍就值得单独拎出来看它前面的算子是什么。如果大量算子的Wait Time都偏高那基本可以确定是 host 侧下发瓶颈或者存在频繁同步。3.3 Block Dim 暴露的并行度问题Block Dim这一列反映的是这个算子实际用了多少个 AI Core。昇腾的 AI Core 数量是固定的不同型号不一样如果一个算子只用了很少的核说明它的并行度没打满可能存在优化空间。举个具体的例子。假设你的设备有 20 个 AI Core某个矩阵乘算子Block Dim显示是 4那意味着它只用了 4 个核在算剩下 16 个核闲着。这种情况通常出现在 shape 比较小的时候——数据量不够分切不出更多并行块。解决办法要么是增大 batch要么是算子融合把小算子合并成大算子。反过来如果Block Dim等于或接近总核数说明并行度是满的这个算子本身没什么可优化的要优化只能从算法层面减少它的调用次数。提示Block Dim要和Input Shapes一起看。同样是 Block Dim 偏低如果 shape 本来就小那是正常的如果 shape 很大但 Block Dim 还是低那就是算子实现或者切分策略的问题。3.4 用 Input Shapes 定位隐藏的 shape 抖动Input Shapes这一列的价值被严重低估了。它能帮你发现一个非常隐蔽的性能杀手shape 抖动。什么叫 shape 抖动就是同一个算子在训练过程中输入张量的形状反复变化。比如某个 reshape 操作有时候输入是[8, 128, 512]有时候是[8, 127, 512]因为序列长度不固定。每次 shape 变化昇腾都要重新做算子编译和内存分配这个开销非常大。排查方法很简单把kernel_details.csv按Name分组然后看每组的Input Shapes有几种不同的取值。如果同一个算子出现了几十种不同的 shape那基本可以确定存在抖动问题。解决办法通常是 padding 到固定长度或者用动态 shape 的算子替代。我见过最夸张的一个案例某个 attention 相关算子因为序列长度不固定采样的 100 个 step 里出现了 60 多种不同的 shape光算子编译就占了大量时间。改成固定长度 padding 之后整体性能提升了将近 30%。4. 从采集到定位的完整实操流程4.1 一个可复现的采集脚本光讲理论没用我给一个可以直接抄的采集脚本。这个脚本我用了很多次覆盖了从环境检查到结果落盘的完整流程。import torch import torch_npu import os def check_env(): assert torch_npu.npu.is_available(), NPU 不可用检查 torch_npu 导入顺序 print(ftorch: {torch.__version__}) print(ftorch_npu: {torch_npu.__version__}) print(fdevice count: {torch_npu.npu.device_count()}) def run_with_profiling(model, dataloader, steps10): device torch.device(npu:0) model model.to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-4) experimental_config torch_npu.profiler._ExperimentalConfig( profiler_leveltorch_npu.profiler.ProfilerLevel.Level1, aic_metricstorch_npu.profiler.AiCMetrics.AiCoreNone, ) out_dir ./prof_result os.makedirs(out_dir, exist_okTrue) with torch_npu.profiler.profile( activities[ torch_npu.profiler.ProfilerActivity.CPU, torch_npu.profiler.ProfilerActivity.NPU, ], scheduletorch_npu.profiler.schedule( wait1, warmup2, active5, repeat1 ), on_trace_readytorch_npu.profiler.tensorboard_trace_handler(out_dir), experimental_configexperimental_config, ) as prof: for step, batch in enumerate(dataloader): if step steps: break inputs batch[input_ids].to(device) labels batch[labels].to(device) outputs model(inputs, labelslabels) loss outputs.loss loss.backward() optimizer.step() optimizer.zero_grad() prof.step() print(f采集完成结果在 {out_dir}) if __name__ __main__: check_env() # model 和 dataloader 按你自己的场景替换 # run_with_profiling(model, dataloader)几个关键点解释一下。wait1, warmup2, active5这个组合的意思是跳过第 1 个 step预热 2 个 step正式采集 5 个 step。总共需要跑 8 个 step 才能采完。active不要设太大5 到 10 个 step 足够看出规律设太大文件会膨胀。aic_metrics这里设成了AiCoreNone也就是不开硬件指标这是标准模式的配置。等你定位到具体算子之后再改成PipeUtilization重新采一次。4.2 采集结果的目录结构跑完之后prof_result目录下会生成一个以时间戳命名的子目录里面结构大致是这样prof_result/ └── hostname_timestamp/ ├── ASCEND_PROFILER_OUTPUT/ │ ├── kernel_details.csv │ ├── operator_details.csv │ ├── step_trace_time.csv │ ├── trace_view.json │ └── ... └── PROF_XXX/ └── ...ASCEND_PROFILER_OUTPUT是我们主要关注的目录。kernel_details.csv是算子级明细operator_details.csv是框架层算子对应 PyTorch 的 aten 算子的明细step_trace_time.csv是每个 step 的耗时汇总trace_view.json可以拖到 Chrome 的chrome://tracing里看时间线。我一般的工作流是先看step_trace_time.csv确认每个 step 的总耗时和分布然后看kernel_details.csv找耗时大头最后用trace_view.json看时间线上的 gap 和同步点。4.3 用 pandas 做聚合分析kernel_details.csv动辄几万行用 Excel 打开会卡死。我习惯用 pandas 做聚合几行代码就能把关键信息提炼出来。import pandas as pd df pd.read_csv(prof_result/timestamp/ASCEND_PROFILER_OUTPUT/kernel_details.csv) # 按算子名聚合算总耗时和调用次数 agg df.groupby(Name).agg( total_duration(Duration, sum), call_count(Duration, count), avg_duration(Duration, mean), avg_wait(Wait Time, mean), ).sort_values(total_duration, ascendingFalse) print(agg.head(20)) # 计算每个算子的耗时占比 total agg[total_duration].sum() agg[pct] agg[total_duration] / total * 100 print(agg[[total_duration, pct, call_count]].head(20))这段代码跑完你就能看到“哪些算子吃掉了大部分时间”。注意看call_count这一列如果某个算子avg_duration很小但call_count极大那就是典型的“蚂蚁搬家”型瓶颈优化方向是减少调用次数而不是优化单次执行。4.4 定位 shape 抖动的脚本前面提到 shape 抖动这里给一个检测脚本# 统计每个算子的 shape 种类数 shape_variety df.groupby(Name)[Input Shapes].nunique().sort_values(ascendingFalse) print(shape_variety.head(20)) # 找出 shape 种类多且总耗时高的算子 merged agg.join(shape_variety.rename(shape_kinds)) suspicious merged[(merged[shape_kinds] 5) (merged[pct] 1)] print(suspicious)suspicious里列出来的就是“shape 变化多且耗时占比高”的算子这些是优先优化的对象。我一般会把这些算子的具体 shape 取值也打出来看看确认是不是真的在抖动。5. 常见问题与排查速查5.1 采集相关的典型报错报错信息原因解决方式torch_npu is not available导入顺序错或版本不匹配确保 import torch 在前核对三者版本profiler 启动后无输出CANN 采集组件缺失检查 toolkit 是否完整安装采集文件为空schedule 配置导致没进入 active检查 waitwarmupactive 与总 step 数采集过程 OOM采集开销叠加显存占用减小 active或降低 profiler_levelkernel_details 缺列版本不匹配对齐 torch_npu 与 CANN 版本5.2 数据解读的常见误区第一个误区是只看 Duration 不看 Wait Time。前面说过Wait Time 高说明调度有问题这是 Duration 看不出来的。第二个误区是把 profiler 的绝对耗时当性能指标。采集本身有开销采出来的 step 耗时通常比不采时高 10% 到 30%。你要看的是相对占比不是绝对值。第三个误区是忽略 host 侧时间。kernel_details.csv只记录 device 侧但很多时候瓶颈在 host 侧比如 Python 层的循环、数据加载。这时候要结合operator_details.csv和trace_view.json一起看。第四个误区是采样 step 太少。只采 1 到 2 个 step数据波动大可能刚好采到异常 step。我一般至少采 5 个 step看规律而不是看单点。5.3 几个我踩过的坑坑一warmup 设成 0。结果第一个 step 的算子编译开销全被采进去了数据完全没法看。后来我固定 warmup 至少 2。坑二在 profiler 上下文里做数据加载。dataloader 的耗时也被采进去了导致 device 时间被稀释。正确做法是把数据加载和 profiler 的 active 区间错开或者用record_function单独标注。坑三开了 AiCMetrics 但不会看。采了一堆硬件指标结果发现解读不了白白增加了采集开销。建议先用标准模式定位再针对性开硬件指标。坑四忘记 prof.step()。schedule 是靠prof.step()推进的如果忘了调用采集永远不会进入 active 阶段最后得到一个空结果。这个坑我踩过不止一次。5.4 一个完整的排查决策路径把上面的东西串起来我给一个我常用的决策路径先看step_trace_time.csv确认每个 step 的总耗时是否稳定。如果波动大先解决稳定性问题。用 pandas 聚合kernel_details.csv按总耗时排序看 Top 20 算子。对 Top 算子检查call_count。次数异常高的优先减少调用。检查Wait Time。偏高的看它前面的算子是不是有同步点。检查Block Dim。偏低的看Input Shapes是不是太小。检查Input Shapes的种类数。抖动严重的做 padding 或固定 shape。如果以上都正常但整体还是慢开 AiCMetrics 看流水线利用率。这套路径覆盖了我遇到过的绝大多数性能问题。真正难的不是工具怎么用而是拿到数据之后怎么形成判断。这个只能靠多采、多看、多对比没有捷径。6. 关于精度和硬件选型的一点补充6.1 精度选择对性能的影响热词里有人问“昇腾 310P3 使用什么精度”这个问题和 profiler 分析是相关的。因为精度直接决定了算子的执行路径和耗时。昇腾上常见的精度有 FP16、FP32、BF16以及 INT8 量化。FP16 和 BF16 在 AiCore 上有专门的加速路径通常比 FP32 快不少。如果你在 profiler 里发现某个算子耗时异常先确认一下它的Input Data Types是不是 FP32——如果是而模型本身允许 FP16那把它转成 FP16 可能直接就有收益。但要注意精度转换不是无脑转。有些算子对精度敏感比如 layernorm、softmax转成 FP16 可能导致数值不稳定。我的做法是先用 profiler 看哪些算子耗时占比高再针对这些算子做精度实验而不是全局一刀切。6.2 不同型号的差异昇腾系列不同型号的 AI Core 数量、HBM 带宽、支持的精度都不一样。这直接影响 profiler 数据的解读。比如同样一个算子在核数多的型号上Block Dim可能打满在核数少的型号上可能就受限。所以看Block Dim的时候一定要结合你实际用的型号来判断不能拿别人的数据直接套。另外型号不同profiler 能采到的字段也可能有差异。有些低端型号不支持 AiCMetrics 的某些指标采的时候会报错或者返回空值。遇到这种情况先查一下你的型号支持哪些采集能力别硬采。7. 我个人的一点使用体会torch_npu profiler这套工具我用了挺长时间最大的体会是它的价值不在于采集而在于解读。采集本身很简单几行代码的事但把kernel_details里几万行数据读成有用的结论需要的是对昇腾执行模型的理解加上大量的对比经验。我建议刚开始用的人不要一上来就追求采得全、采得深。先用标准模式采 5 个 step把 Top 20 算子看明白把Duration、Wait Time、Block Dim、Input Shapes这四列的关系理清楚。等你能从这四列里看出问题了再去开 AiCMetrics 看更底层的东西。还有一点profiler 数据一定要对比着看。改了一个地方之后重新采一次和之前的对比看目标算子的耗时占比有没有下降看整体 step 时间有没有变化。单次采集的数据只能告诉你现状对比才能告诉你优化有没有生效。我自己的习惯是每次优化前后都存一份结果用同样的聚合脚本跑一遍直接看数字变化。最后分享一个小技巧如果你觉得kernel_details.csv太大不好处理可以在采集配置里把profiler_level调低或者只采active3个 step。数据量小一点分析起来反而更快。性能分析这件事快比全重要先定位到大头再逐步深入比一次性采一大堆然后无从下手要高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑