性能调优思路

性能优化是一项系统性工作,建议采用 "分析 - 定位 - 优化" 的流程,通过性能分析工具定位瓶颈后实施针对性优化。

通过 profiling 工具获取算子级性能数据定位性能瓶颈点,主要涉及算子计算时间与调度通信时间。

常用优化策略中,计算时间过长需依靠算子自身优化升级,可收集算子的 shape 和 dtype 向算子开发部门提交工单并跟踪进展;调度过程包含多个环节,目前最突出的问题是重复编译导致的性能损耗,可通过关闭在线编译、替换为更高效的 aclnn 算子、算子融合等手段优化。

性能数据采集

profiler工具可以采集模型中每个算子的计算时间和调度时间,具体步骤如下:

导入profiler工具

代码中导入paddlepaddle的profiler工具:

import paddle.profiler as profiler

初始化profiler工具

指定profiler工具采集目标:

profiler = profiler.Profiler(targets=[profiler.ProfilerTarget.CUSTOM_DEVICE], custom_device_types=['npu'])
  • "targets":设置为CUSTOM_DEVICE;
  • "custom_device_types":设置为NPU。

注:若代码中已有profiler,需要把as profiler中的profiler统一改个名字,并把后续步骤中的profiler改成此名字,例如:

import paddle.profiler as profilerws
profilerws = profilerws.Profiler(targets=[profilerws.ProfilerTarget.CUSTOM_DEVICE], custom_device_types=['npu'])

数据采集

采集时机选择

建议在训练过程中选择稳态阶段进行性能数据采集,具体参考以下准则:

  • 前 100 个 step 通常处于初始化阶段,性能数据不具备代表性,建议在 100step 后开始采集;
  • 若单 epoch 包含 step 数不足 100,则选择 epoch 末尾稳定阶段的step数据。

    采集代码示例

    将 profiler 的 start / stop 逻辑嵌入训练脚本 train.py 中:

if step == 100:
    profiler.start()
elif step == 101:
    profiler.stop()

具体示例如下:

插入以上代码后,启动训练操作。

性能数据分析

msprof解析性能数据

训练结束后,会在当前工作路径下生成一个名为ascend_profiling的性能数据目录,可使用CANN包自带的 msprof 工具对数据进行解析,具体命令如下:

alias msprof='/usr/local/Ascend/ascend-toolkit/latest/tools/profiler/bin/msprof'
msprof --export=on --output=ascend_profiling

注:/usr/local/Ascend部分是CANN包默认安装路径,如果安装在自定义路径下,此处需要做相应修改。

解析成功后有如下打屏日志:
02_msprof解析成功

解析后会生成目录文件如下(多张卡会生成多个文件夹):
03_msprof解析生成目录

打开任意一张卡的解析结果:
04_msprof单卡解析结果

需要关注的是最后一个文件夹mindstudio_profiler_output,打开后需要关注的是红色框圈住的部分:
05_mindstudio_profiler_output内容

  • msprof_*.json #timeline文件,可以用chrome://tracing解析,解析成可视化的时间线,看上去比较的清晰直观
  • op_statistic_*.csv
  • op_summary_*.csv

op_statistic_*.csv 分析

op_statistic_*.csv 是 NPU 算子的性能统计文件,包含以下关键字段:
OP Type:算子类型标识(全局唯一)
Ratio (%):算子计算总耗时的占比(性能瓶颈量化指标)
Core Type:标识算子执行的计算单元类型
Total Time(us):算子计算总耗时
06_op_statistic文件关键字段

OP Type字段

OP Type 字段记录性能分析过程中所有算子的具体名称,通过该字段可快速定位特定算子的性能表现。

Ratio字段

Ratio(%) 是算子计算总耗时的占比,一般Ratio(%)不大于40问题不大,若占比过大,可能存在算子重复编译问题,可通过设置环境变量export FLAGS_npu_jit_compile=false来避免重复编译。

以占比排名第一的TransData算子为例,被调用次数较多但比例还算是正常。当设置环境变量后,可以发现TransData OP的Ratio(%)从19.637%下降至18.176%:
07_TransData算子关闭在线编译环境变量

Total Time字段

Total Time(us)是算子计算总耗时,如果占比很大,或者算子的最大计算时间比平均计算时间大很多,则有可能是算子的调用有问题。

Core Type字段

Core Type 标识算子执行的NPU计算单元类型,主要分为以下四类:

  • AI_CORE:NPU高性能计算单元,主要用于卷积、矩阵乘法等计算密集型操作,最大化 AI_CORE 利用率是提升整体性能的关键;
  • AI_CPU:NPU通用计算单元,支持全数据类型计算,主要负责标量运算、控制流等轻量级操作,其灵活性高但计算吞吐量低于AI_CORE;
  • AI_VECTOR_CORE:AI_CORE 的子单元,负责向量计算,不需要特别关注;
  • MIX_AIV:混合执行模式,部分计算在 AI_CORE,部分在 AI_CPU,如果其占比不是特别大,也不需要特别关注;

op_summary_*.csv 分析

op_summary_*.csv 文件记录了性能统计周期内的算子执行时序数据,包含以下关键字段:
Op Name:当次运算的算子实例名称
OP Type:算子类型标识(对应 op_statistic_*.csv 文件中的 OP Type 字段)
Task Duration(us):算子的计算耗时
Task Wait Time(us):算子的调度耗时

Op Name字段

Op Name指的是当次运算的算子名称,有两种形式:一种是带有aclnn前缀、一种是不带aclnn前缀,分别对应CANN算子调用的两种方式。
08_opname和optype

  • aclop的优点是支持度比较高,几乎覆盖所有算子;
  • aclop的缺点是性能比aclnn差;
  • aclop有一个更大的问题是在attribute属性改变时,会导致算子重复编译,如果重复编译较多,最好调整成aclnn相关的算子,能够减少一部分计算时间。

Task Duration字段

Task Duration(us)指的是算子的计算耗时,即算子在NPU侧的计算时间。

通常,通过对 op_summary_*.csv 表格中的 Task Duration (us) 进行降序排列,能够定位主要的性能瓶颈。

若算子Task Duration(us)时间过长(大于1000us),则需要重点关注,可以在模型侧查看算子的调用以及它的适配层逻辑是否有问题。

Task Wait Time字段

Task Wait Time(us)指的是算子的调度耗时,也有等待耗时的意思,即算子从CPU下发到NPU的时间。

在适配层,Task Wait Time(us) 相对更易于优化。对 Task Wait Time(us) 进行降序处理后会发现,耗时较长的情况主要有以下三种:

  • 前反向中间等待:属正常等待时间;
  • 算子重复编译:最常见的延长Task Wait Time(us)的原因;
  • 前一算子运行于 CPU:此类情况在性能数据中不会被统计,但会致使当前算子的等待时间大幅增加。

后两种情况是优化的重点。需要先定位等待时间超长的位置,随后在代码中梳理其调用关系,进而实施针对性优化。


讲道义的遥控器
1 声望0 粉丝