PyPTO 算子性能调优实战:从泳道图采集到 Man-In-The-Loop 深度优化 PyPTO 算子性能调优实战从泳道图采集到 Man-In-The-Loop 深度优化【免费下载链接】pyptoPyPTO发音: pai p-t-oParallel Tensor/Tile Operation编程范式。项目地址: https://gitcode.com/cann/pypto性能调优是融合算子开发中最困难、也最依赖经验的环节。本文基于 CANN 开源项目 PyPTO 的官方性能调优指南系统讲解如何使用 PyPTO Toolkit 可视化工具完成高性能融合算子开发先通过泳道图拿到算子初版开箱性能再结合 Stitch、TileShape、合图、调度策略等调优手段在采集性能数据 → 分析瓶颈 → 调整配置 → 重新采集的 Man-In-The-Loop人工参与优化闭环中逐步逼近硬件算力上限。读完本文你将掌握泳道图与 PMU/PMU Trace 数据的采集分析方法理解 loop 写法与 TileShape 初值对开箱性能的影响并能独立完成 Stitch 调优、Matmul/Vector TileShape 调优、深度与广度方向合图调优以及调度策略调优。整体流程以泳道图为核心的 Man-In-The-Loop 调优在完成精度调试后开发者可以利用 PyPTO Toolkit 查看对应算子的泳道图。泳道图用于直观展示计算图的实际调度与执行过程清晰呈现任务的执行顺序和耗时信息。基于此开发者可以得到所编写算子的初版性能亦可称之为开箱性能。通过观察泳道图开发者可以观察到当前算子实现的性能瓶颈点。在观察到瓶颈点后通过调整 Tiling 配置和采用不同的计算图编译策略得到新实现下的算子泳道图进行进一步地调整逐步提升算子性能实现 Man-In-The-Loop 的调优。这一闭环是整个性能调优章节的组织骨架工具采集数据 → 泳道图定位瓶颈 → 针对性调整 → 迭代验证。从源码结构看PyPTO 的运行时Runtime侧提供了完整的 Profiling 数据落盘链路包括 dump_device_perf.cpp 等 Runner 组件与output/output_时间戳目录的产物生成逻辑Python 侧则在 config.py 中集中定义了本文涉及的各类调试、运行与 Pass 配置接口方便在pypto.frontend.jit装饰器或对应 set 系列接口中统一配置。性能调优工具采集泳道图数据当前支持两种并列采集方式图执行阶段泳道图采集与AI CPU/AI Core 联合采集。两种方式可分别单独开启互不干扰也可同时开启。若同时开启可在泳道图中同时观察同一时间轴上的 AI CPU 与 AI Core Profiling 数据。方式一图执行阶段泳道图采集runtime_debug_mode适用场景用于常规性能调优重点关注子图执行顺序、任务耗时分布、AI Core 端到端耗时与 AI Core 利用率。通过给pypto.frontend.jit装饰器的入参debug_options配置图执行阶段调试开关启动性能数据采集pypto.frontend.jit( debug_options{runtime_debug_mode: 1} )关于该参数的含义config.py 中的set_debug_options接口给出了完整定义runtime_debug_mode取值为整数0表示关闭1表示开启执行阶段相关配置例如泳道图2表示使能 AICORE_MODEL 仿真3表示使能运行时依赖校验数据导出4表示使能运行时 GM 内存越界检查并对显式注册的assume_divisible表达式在控制流入口生成取模断言。常规性能调优使用1即可。执行用例以仓库中的 softmax 示例为例对应源码为 softmax.pypython3 examples/02_intermediate/operators/softmax/softmax.py采集结果输出到当前工作目录output/output_时间戳下详见下文采集结果文件说明。方式二AI CPU/AI Core 联合采集DUMP_DEVICE_PERF适用场景用于分析 AI CPU 调度与 AI Core 执行的协同关系定位首任务启动慢、调度等待等问题。限制说明该工具最多支持 200 轮数据采集和打屏超出部分将被截断。当前只支持采集 20 次 devTask 构建的数据。当 devTask 构建次数与stitch_function_max_num配置相关每次 stitch 对应一次 devTask 构建超过 20 次时超出部分的 perf 数据将被截断并在日志中出现如下告警提示Dev task num larger than: 20, the excess part will not be recorded这条告警在仓库源码 device_perf.h 中确有其对应实现属于运行时设备侧 Perf 采集的真实行为约束。通过环境变量使能export DUMP_DEVICE_PERFtrue执行用例python3 examples/02_intermediate/operators/softmax/softmax.py采集结果输出到当前工作目录output/output_时间戳下详见下文采集结果文件说明。待数据全部落盘后手动执行 analyze 命令查看 AI CPU/AI Core 数据汇总表python tools/scripts/machine_perf_trace.py analyze output/output_时间戳/machine_trace_perf_data_0.json该 analyze 子命令在 machine_perf_trace.py 中实现它会加载 Perf JSON 文件按轮次round解析并汇总 CTRL AICPU、SCHED AICPU、AICORE 三个阶段的事件耗时最终以表格形式打印汇总数据。仓库中的测试用例 test_dump_perf.py 对该落盘与汇总语义进行了系统看护可视为本工具用法的权威参考。采集结果文件说明和参数含义解释output 目录产物说明output/output_时间戳目录下泳道图相关文件文件用途machine_trace_perf_data*.jsonMachine 组件原始 Profiling 数据tilefwk_L1_prof_data_*.jsonMachine 组件 L1 层级 Profiling 数据merged_swimlane.jsonIDE 可视化综合泳道图machine_runtime_operator_trace*.jsonAI CPU / AI Core 泳道图联合时序machine_trace_perf_data*.json与tilefwk_L1_prof_data_*.json可判断底层采集是否成功merged_swimlane.json与machine_runtime_operator_trace*.json用于 PyPTO Toolkit 展示。参数含义解释CTRL AICPU阶段含义DEV_TASK_BUILD将计算图编译结果组装为设备可执行 DevTask 的耗时stitch 阶段Post-process所有 DevTask 构建完成后的收尾耗时直至 Control AICPU 退出Total run timeControl AICPU 从启动到退出的完整运行时间SCHED AICPU阶段含义ALLOC_THREAD_IDSchedule AICPU 线程分配耗时INITSchedule AICPU 初始化耗时CORE_HAND_SHAKESchedule AICPU 与 AI Core 建立通信握手的耗时DEV_TASK_RCV从 Control AICPU 接收 DevTask 的耗时Post-process所有 DevTask 下发执行完成后的收尾耗时含同步停止、等待 AI Core 退出等直至 Schedule AICPU 退出Total run timeSchedule AICPU 从启动到退出的完整运行时间AICORE阶段含义INITAI Core 启动后的头段开销包括初始化、与 Schedule AICPU 握手以及等待接收首个计算任务End-to-End time所有 AI Core 上实际执行计算任务的时间窗口从最早开始执行到最晚执行完成Post-process最后一个计算任务执行完成后的尾段开销直至所有 AI Core 退出Total run time所有 AI Core 从启动到退出的完整运行时间test_dump_perf.py中对上述阶段如DEV_TASK_BUILD、CORE_HAND_SHAKE、DEV_TASK_RCV、End-to-End time等的事件匹配与语义校验均有对应断言读者可以结合测试源码理解每个字段在底层事件流中的精确含义。查看泳道图数据通过终端查看 AI Core 执行端到端耗时以及 AI Core 利用率图1查看 AI Core Perf 信息通过 PyPTO Toolkit 插件查看泳道图右键单击对应 JSON 文件在弹出的菜单中选择使用 PyPTO Toolkit 打开。图2查看泳道图图中展示了任务的执行顺序和耗时信息帮助开发者分析性能瓶颈。结合 CTRL/SCHED/AICORE 三张阶段含义表可以快速定位瓶颈属于调度开销Control/Schedule AICPU 侧还是计算执行AI Core 侧。采集 PMU 数据PMUPerformance Monitoring Unit是现代处理器中关键的硬件模块专门用于监控和分析处理器的运行性能。PMU 内部有多个可编程计数器每个计数器可监控一种或多种事件。步骤1选择采集模式通过环境变量选择采集模式export PYPTO_PROF_PMU_EVENT_TYPEgroup_id其中group_id取值范围为[1, 2, 4, 5, 6, 7, 8]默认值为2。PMU 支持的模式如下步骤2数据采集适配编译宏后通过msprof命令采集 PMU 数据msprof --task-timel3 [--output数据存放路径] python xxx.py其中l3开启 PMU 采集开关--output指定 PROF 产物的输出路径默认落盘在项目根目录下步骤3数据解析PMU 数据采集完成后会落盘在output/PROF*/device_*/data/目录下。根据所选的PYPTO_PROF_PMU_EVENT_TYPE执行解析脚本python tools/profiling/tilefwk_pmu_to_csv.py -p PROF_xxx/device_x/data -pe$PYPTO_PROF_PMU_EVENT_TYPE --arch [dav_2201, dav_3510]解析完成后会在项目根目录下生成tilefwk_prof_pmu.csv文件。该脚本 tilefwk_pmu_to_csv.py 的参数与文档描述完全对应-p/--path指定待解析的 PROF 数据路径-pe/--pmuEvent指定事件类型合法取值正是[1, 2, 4, 5, 6, 7, 8]默认 2--arch指定目标架构dav_2201或dav_3510默认dav_2201--output可指定解析结果落盘路径。PMU TracePMU Trace 用于核内流水分析是算子深度性能调优的重要工具。开启该能力后开发者可以直观地观察 kernel 在 AI Core 上执行时各硬件流水线如 MTE2、MTE3、Vector、Cube 等的时序排布与重叠情况。通过分析流水排布可以识别搬运与计算之间的空闲等待、判断各流水线的利用率是否充分从而有针对性地调整 Tiling 配置或计算编排策略提升算子性能。限制说明产品支持情况Ascend 950PR/Ascend 950DT支持Atlas A3 训练系列产品/Atlas A3 推理系列产品不支持Atlas A2 训练系列产品/Atlas A2 推理系列产品不支持当前仅支持单算子采集不支持整网场景。当前最多只能采集 6 个核的数据。开启 PMU Trace通过codegen_options中的enable_pmu_trace参数开启。开启后codegen 会在每个 leaf function 对应 CCE 代码的首尾插入bisheng::cce::mark_stamp打点并为其分配 PMU ID用于在核内流水中定位对应 kernel。PMU ID 生成规则PMU ID 由CodeGenCloudNPU::GenPMUId()生成codegen_cloudnpu.cpp用于标识不同 kernel 函数。生成方式PMU ID [主块标记] funcHash 后三位再对 4096 取模后作为 stamp 值。组成部分规则示例主块标记ctx.isMainBlock true时前缀加1否则为空主块1非主块无funcHash 后三位取subFunc.GetFunctionHash()的最后 3 个字符123约束bisheng 编译器 stamp 值上限为 4096。为区分主块与尾块当前仅使用 funcHash 后三位子图数量超过约 1000 时跨 kernel 的 PMU ID 可能重复。从源码看打点实现位于 codegen_cloudnpu.cpp开启后会在 leaf function 的 CCE 代码首部插入__asm__ volatile(bar.all)与成对的bisheng::cce::mark_stampPIPE_MTE2, ${id_value}$()中间以NOP填充形成可辨识的时间间隔.rept 100\n\tNOP \n\t.endr这正是泳道图中尖峰标记的来源。支持两种配置方式方式一在pypto.frontend.jit装饰器中配置pypto.frontend.jit( codegen_options{enable_pmu_trace: True} )方式二通过set_codegen_options接口配置pypto.set_codegen_options(enable_pmu_traceTrue)set_codegen_options接口定义于 config.py其参数enable_pmu_trace的语义即为是否使能 PMU Trace 数据采集与文档描述一致。采集 PMU Trace 数据开启 PMU Trace 后还需在运行时通过msprof开启指令级采集msprof --instr-profilingon [--output数据存放路径] python xxx.py其中--instr-profilingon开启指令级 Profiling 采集--output手动指定落盘路径默认落盘在当前工作目录下采集结果采集完成后数据文件落盘在PROF_*/mindstudio_profiler_output/msprof_*.json中。开发者可在 PyPTO Toolkit 中加载该文件进行核内流水分析。在 PyPTO Toolkit 的核内流水视图中每对mark_stamp会显示为流水线上的尖峰标记NOP 填充使其在时间轴上形成可辨识的间隔。开发者通过匹配相同的 PMU ID即可将时间轴上的流水片段定位到对应的 leaf function从而分析该计算阶段内各流水线MTE2、MTE3、Vector、Cube 等的时序排布与利用率。开箱性能调优算子初始性能与 loop 的写法、TileShape 的设置最为密切。本章将介绍如何使用相关接口在算子初始编写过程中直接得到较好的开箱性能。正确选择 loop 写法由于不同 root function 之间的子图不能合并而子图合并是 PyPTO 优化性能的关键手段因此 loop 优化的核心原则是增加 root function 的大小减少它们的个数。静态轴使用 Python for 循环pypto.loop 方法会按当前轴循环展开成不同的 root function。因此静态轴上的循环应使用 Python 的 for 循环避免使用 PyPTO 的 loop。# ✅ 推荐静态轴使用Python for for i in range(batch_size): result[i] process(data[i]) # ❌ 避免静态轴使用PyPTO loop for i in pypto.loop(batch_size, nameLOOP_1, idx_namei): result[i] process(data[i])调优效果可参考 GDR 算子案例 3.3.1 章节。动态轴使用 PyPTO loop 并合理配置 view当算子内有动态 Shape 时动态轴的 dim 数值范围往往较广需要使用 loop 循环处理。此时需要注意 view 视图的参数配置选取的 Shape 范围不能过小否则会限制后续 TileShape 的配置范围导致每次循环的计算量过小循环次数增加部分运算还可能导致额外的重复搬运。比如矩阵乘运算TileShape 过小会导致大量的 root function 搬运相同的左矩阵或右矩阵。view TileShape 为 128 的写法示例如下# 推荐动态轴使用loop unroll bsz, h x.shape b 128 b_loop (bsz b - 1) // b for b_idx in pypto.loop(b_loop, nameLOOP_1, idx_nameb_idx): b_valid (bsz - b_idx * b).min(b) x_view pypto.view(x, [b, h], [b_idx * b, 0], valid_shape[b_valid, h]) # Matmul pypto.set_cube_tile_shapes([128, 128], [128, 128], [128, 128]) y pypto.matmul(x_view, W)尽可能合并 loop检查算子代码是否有可以合并的 loop 块应当将其合并以增大 root function。例如下面的两个 loop 就可以合并进而提升 Operation1 和 Operation2 运算合并的可能以减少 y 的冗余搬运。bsz x1.shape[0] for b_idx in pypto.loop(bsz, nameLOOP_1, idx_nameb_idx): out_1 Operation1(x1[b_idx, :], y) for b_idx in pypto.loop(bsz, nameLOOP_2, idx_nameb_idx): out_2 Operation2(x2[b_idx, :], y)动态轴范围较广时使用 loop_unroll当算子使用动态 Shape且 Shape 范围较广的场景应考虑使用 loop_unroll 代替 loop 接口。loop_unroll 功能与 loop 类似在其基础上增加了unroll_list参数支持多个展开方式。比如当算子中某个动态轴需要泛化支持 1~64k 的 Shape 范围时指定单一的动态轴切分大小难以满足要求。切分过大时小 Shape 场景会引入较多实际计算大小为 0 的空计算任务增加耗时切分过小时大 Shape 场景下循环次数过多影响整体性能。使用 loop_unroll 后无论大小 Shape 场景框架都会根据unroll_list参数选择合适的档位或组合避免冗余计算且循环次数可控因此可获得较好的性能。具体使用时注意以下几点伴随unroll_list参数配置的挡位数量增加编译过程需要处理的任务量也成倍增加导致编译时间变长。因此在最初编写算子时建议使用较短的unroll_list如[64, 16, 4]。使用动态分档时应当注意不同档位可能需要分别选择合适的 TileShape。多层循环嵌套场景下只有最内层的 loop_unroll 可以成功使用unroll_list参数。参考示例如下 inputA shape:[-1, 64] outputB shape:[-1, 64] -1表示动态Shape ..... for b, k in pypto.loop_unroll(A.shape[0] // 64, unroll_list[64, 16, 4], nameA, idx_nameb): ### 支持在不同的展开档位设置不同调优参数 if k 16: pypto.set_vec_tile_shapes(16, 64) else : pypto.set_vec_tile_shapes(64, 64) tile_a A[b * 64:(b k) * 64, :] tile_a tile_a 2 B[b * 64:, :] tile_a调优效果可参考 GDR 算子案例 3.3.3 章节。合理设置 TileShape 初值TileShape 配置的基本原理与使用约束等参考 Tiling 配置 章节。其切分大小一方面直接决定了算子切分后的任务数量从而决定了实际执行时的分核数、计算轮次。另一方面切分大小从理论层面决定了算子的算数强度。因此优化性能的关键是优化 Tiling 配置。通常切分越大算数强度越大计算越容易达到 Compute Bound进而充分使能 NPU 的算力。这是由于切分必然会引入重复搬运切分越多重复搬运量越大从而算数强度越低。而另一方面切分大小又受片上多级缓存空间L1、L0 或 UB的限制而不能无限增加。Matmul 初始 Tiling 配置针对矩阵运算场景以 A、B 矩阵均为 DT_BF16 或 DT_FP16 类型为例满足 Buffer 空间约束的推荐 Tiling 配置为# Cube的相关计算建议采用如下的TileShape可根据M、K、N实际尺寸选择最接近的配置 pypto.set_cube_tile_shapes([128, 128], [64, 256], [256, 256]) pypto.set_cube_tile_shapes([256, 256], [64, 256], [128, 128]) pypto.set_cube_tile_shapes([128, 128], [128, 512], [128, 128])以上 Tiling 配置的优点在满足 L0 Buffer 约束的条件下可以达到较大的算数强度由于 Tile 大小需要满足分型格式的对齐要求同时要考虑切分大小对于写入、写出带宽的影响一般取 128-256 的组合。后续进一步使用合图相关接口进行深度调优时有机会开启 Double Buffer使能流水并行。从接口实现看set_cube_tile_shapes 接收 M、K、N 三组 TileShape 列表并支持enable_split_k开关详见深度性能调优章节底层封装为CubeTile(m, k, n, enable_split_k)对象下发到编译器。Vector 初始 Tiling 配置针对向量运算场景应该按照 Operation 和芯片 UB 大小来确定合适的 TileShape。首先需要满足特定 Operation 对 TileShape 的规格约束。如 scatter update 要求尾轴 TileShape 和 Shape 一致即不对尾轴进行切分。各个 Operation 的具体限制可以参考相关接口文档。其次要保证 Operation 的输入与输出 Tensor 可以在 UB 中分配内存因此 TileShape 不能过大。同时由于子图和搬运的数据块较小会导致性能劣化因此 TileShape 又不能过小。以 Atlas A3 训练系列产品为例UB 的缓存容量为 192KB。因此合适的初始 TileShape 是既满足 Operation 的要求又使得数据块大小在 16 到 64KB 之间尾轴 32B 对齐。此外归约类计算Reduce 运算如sum、max、min 等尽可能不要在归约轴上进行切分。例如输入 Shape 为 (56, 1024) 的 RMSNorm它的最后一维 TileShape 应当设为 1024。下图的上半部分是对 reduce 轴切分的 RMSNorm 的泳道图例子多个子图的输出需要在同一个子图进行 reduce 操作导致产生 GM 搬运和调度开销。下半部分是不对 reduce 轴切分的例子此时上下游子图合并没有 GM 搬运和调度开销。综上考虑在初始算子开发阶段可以先使用如下设置后续再结合泳道图数据进一步调优。# Vector的相关计算建议采用如下的TileShape pypto.set_vec_tile_shapes(64, 512)针对 TileShape 调优的实际效果可参考 GDR 算子案例 3.2 章节。需要特别说明的是上述 Tiling 配置并非一成不变需要用户根据计算场景考虑输入 Shape、Dtype、Format 等以及硬件平台进行综合考虑。其他注意事项检查输入矩阵、尤其是 Shape 较大的权重矩阵是否可以提前以 NZ 格式存储。NZ 格式的数据搬运到 L1 的带宽更高。矩阵乘前后有 transpose 时可以尝试更换左右矩阵并使用左右矩阵转置的配置。由于 N 轴在尾轴上因此当 M 轴较大、N 轴较小时也可以尝试该方法使得左右矩阵有更大的尾轴提升搬运带宽。检查是否有不合理数据操作导致的冗余搬运例如更换 concat 为 assemble、尝试对 reshape 配置inplace True参数。深度性能调优进一步调优算子性能需要采用 man-in-loop 的方式通过获取并分析当前算子性能数据针对性调整各性能配置参数经过迭代调优逐步逼近最佳性能。算子性能数据可以通过泳道图获取泳道图的采集和分析是算子调优过程中的重要一环本章节的调优过程需要结合泳道图进行。Stitch 调优Stitch 配置决定了多少个 root function 被同时下发调度即该参数控制一次 stitch 能处理的最大 loop 数量会同时影响调度开销、控制流生成耗时以及 workspace 内存占用。因此 Stitch 设置较大后任务可以充分并行通常性能更优。当泳道图中出现大量空隙时可能是 Stitch 配置的值太小导致的。当前 Stitch 配置主要由 stitch_function_max_num 参数决定在 jit 装饰器中完成配置可参考如下配置pypto.frontend.jit( runtime_options{stitch_function_max_num: 128} )runtime_options会在编译入口被解析并下发到运行时仓库测试用例如 test_ctrl_cpu_perf.py 中pypto.frontend.jit(runtime_options{stitch_function_max_num: _STITCH_FUNCTION_MAX_NUM})即通过该参数控制 stitch 规模以观察 Control AICPU 的调度性能印证了其对调度行为的影响。以 glm_attention.py 算子为例对比下该值的影响当该值过小如配置为 1时每个任务都需要同步调度开销大性能差。算子 kernel 耗时为 1230us端到端耗时调度耗时执行耗时为 1590us对应泳道图如下当该值增大为 128 时泳道图明显更紧凑调度与同步开销大幅降低。算子 kernel 耗时为 180us端到端耗时为 803us对应泳道图如下当该值继续增大为 512 时泳道图更加紧凑但调度耗时明显增加。算子 kernel 耗时为 150us端到端耗时为 977us对应泳道图如下可见随着 Stitch 配置的增大算子 kernel 耗时持续减少。但是 Stitch 配置也并不是越大越好参数过大会导致调度耗时明显增加进而导致端到端耗时得不偿失。Stitch 设置较大时 workspace 也会增加大量任务并行可能导致 L2 命中率较低。调优建议在内存资源允许的前提下可逐步增大 Stitch 配置结合泳道图和端到端总耗时数据调整stitch_function_max_num参数寻找在性能收益与控制流开销之间的最佳平衡点使总耗时减少。实际效果还可以进一步参考 GDR 算子案例 3.1 章节。TileShape 调优Matmul TileShape 调优进一步调优 Matmul 的 TileShape需要充分考虑对算数强度和带宽的影响具体介绍见 Matmul 高性能编程 章节。当前环节主要关注减少重复载入和K 轴分核两个调优手段可对应set_cube_tile_shapes接口的enable_split_k配置参数。用户可以结合上述原理介绍推导并选择合适的开关配置策略也可以直接结合泳道图数据进行测试验证择优配置。两个参数是相互解耦的具体写法可参考如下配置pypto.set_cube_tile_shapes([128, 128], [64, 256], [256, 256], enable_split_kTrue)enable_split_k作为CubeTile构造参数在 config.py 中定义并由 C 绑定 controller.cpp 以命名参数形式暴露给 Python 侧属于 K 轴分核的官方配置入口。Vector TileShape 调优Vector 的 TileShape 配置除了需遵从前述章节介绍的原则以外还需结合泳道图关注以下几点下游 Vector Operation 的 TileShape 尽可能使用上游 Operation 的输出 TileShape。例如 Transpose 后接着一个 Add Operation 时假设前者的 TileShape 设置为 (64, 128)则后者的 TileShape 应该优先选择为 (128, 64)。上下游 Operation 的 TileShape 对齐时它们有着简单的一对一依赖pass 通常会自动将其合并在一个子图达成融合算子的优化效果。如果没有合并可以使用前文介绍的sg_set_scope或切图旋钮将其合并起来。上下游 Operation 的 TileShape 不对齐时可能产生多对多依赖无法正常合图。如下图所示每一种颜色代表一种子图。当前后的 Sqrt 和 Cast Operation 使用相同的 TileShape 时可以切出两个并行的融合子图反之 Sqrt 和 Cast Operation 使用不同 TileShape 时上下游子图之间是三对二的依赖此时不能得到并行的融合子图。根据泳道图上的子图大小和并行度来调整 TileShape。在优化目标的场景下当泳道图上某一部分并行的核数较少如只使用了不到一半的 Vector 核时应尝试减小该处 Operation 的 TileShape。反之泳道图上某一部分子图耗时较短、调度开销的耗时占比较高时应尝试增大该处 Operation 的 TileShape。需要注意的是这些调整应当避免在尾轴和规约轴上进行因为这些轴上的 TileShape 应当符合前文所述的优化原则。另外进行 TileShape 优化时可能会因为合图等原因导致 OoO 报错此时应找到报错相关的 Operation将其 TileShape 调小再进行上述的第三点优化。调整相邻的 Cube 和 Vector Operation 的 TileShape使 Cube 子图和 Vector 子图之间依赖变得更简单尽量避免多对多的依赖关系。合图调优在优化合图相关旋钮之前需要先完成 TileShape 的调优因为不合适的 TileShape 会导致复杂的依赖关系从理论上无法得到既多核并行又融合多个 Operation 的子图任务。合图是指将计算图中多个逻辑上独立的 Operation 合并为一个逻辑子图并由该子图最终生成一个物理计算内核Kernel的过程。深度学习模型的计算图往往由大量粒度较小的 Operation 构成传统逐 Operation 执行模式下每个 Operation 都会独立触发一次内核启动计算完毕后将中间结果写回全局内存GM。这种执行方式在实际硬件上会引入显著的内核启动开销和冗余内存访问难以充分发挥计算单元的全部计算能力。合图优化通过 Operation 逻辑聚合使多个 Operation 在同一 Kernel 中协同执行。计算的中间结果得以保存在片上高速缓冲中供下游 Operation 直接读取同构消除冗余的 GM 读写显著提升计算–访存比并改善整体执行效率。在 PyPTO 编程模型中开发者通过 Tensor 和 Tensor Operation 构建计算图。合图过程由编译器内部的优化 Pass 自动完成用户无需手工编写融合 Operation 代码。合图 Pass 会在保证计算结果正确性的前提下对计算图进行分析与重写将原始计算图划分并重组为更适合目标硬件执行的子图。PyPTO 的合图优化主要分为深度方向合图和广度方向合图两类分别针对不同的性能瓶颈场景。深度方向合图深度方向合图基于计算图中的生产者–消费者关系沿数据依赖路径将前后相邻的 Operation 进行融合。该方式通过消除中间结果的写回操作直接优化数据流路径使原本受限于带宽的算子链得以在单个内核内一气呵成地完成计算。PyPTO 框架在深度方向上已经实现了自动合图的功能在极致性能优化场景下可以手动指定合图方案把 operation 操作分配到特定的计算任务中进而改变各个任务的耗时以实现负载均衡。通过配置 set_pass_options 接口的sg_set_scope参数实现该功能。从接口实现看sg_set_scope在 config.py 中既支持传 int仅设置 scopeid向后兼容也支持传三元组(scopeid, allow_parallel_merge, allow_cross_scope_merge)其中scopeid为 int 类型的 scope IDallow_parallel_merge控制是否允许并行分支合并allow_cross_scope_merge控制是否允许带 scope 的超节点supernode与其他节点合并参数校验逻辑如 scope_id 范围 -1~2147483647也在该文件中实现。融合的目标通常来自于 Operation 间的依赖关系例如上下游的两个 Operation 间传输的数据量较大时应当将其合并以减少搬运耗时或者多个 Operation 进行 tile 切分后变成多个并行的连通分支时应当将这些 Operation 进行合并。融合的目标也可以来自于特定类型算子的固有经验例如 IFA 算子切 batch 轴、s2 轴和 g 轴后V1 和 V2 一般应各作为一个子图任务。当前主要考虑在连续的 Vector 计算过程中使用该能力暂不支持将 Matmul Operation 与 Vector Operation 进行合图。使用时需要结合泳道图信息进行分析和调整。广度方向合图广度方向合图针对计算图中处于同一层级、可并行执行的 Operation通过将多个并行 Operation 合并到同一 Kernel 中执行以增强单次 Kernel 的计算规模。在核内指令编排阶段多分支融合能更充分地填充硬件流水实现更优的多 pipe 并发。在访存层面通过归并同源访存将多次重复的 GM 到 L1 搬运整合为单次加载在节省内存带宽的同时有效摊薄了 Kernel 启动开销最终提升了硬件计算单元的整体吞吐率。针对 Matmul 与 Vector 计算PyPTO 提供了不同的广度方向合图调优接口将在接下来的章节分别详细介绍。Matmul 广度方向合图Matmul 运算场景下通过 set_pass_options 接口配置子图合并主要可以选择L1Reuse和CubeNBuffer两种策略。两者都是用于在广度方向上合并 Cube 子图前者用于合并具有冗余 L1 搬运的子图、后者用于合并同构的子图。L1Reuse 可以减少 L1 搬运量CubeNBuffer 可以在不同分支的矩阵乘间隐藏搬运和计算耗时。它们都能减少子图调度开销但考虑到大多数的矩阵乘场景的性能瓶颈都是数据搬运因此优先使用 L1Reuse 策略减少数据搬运量。实际上 L1Reuse 策略是默认开启的且自动计算并配置子图合并数量。极致性能优化场景下可以通过cube_l1_reuse_setting参数进行手动配置通常考虑 2、4、8 等值可以结合泳道图实测数据择优配置。cube_l1_reuse_setting支持函数粒度配置func{magic}_{order}格式可实现不同 root function 间的精细化控制仅影响指定的 function不影响其他 function。配置信息会直接展示在泳道图的 hashOrder-hint 字段中格式l1ReuseInfo hashOrder: func8_0, subGraphCount: 24可根据子图数量subGraphCount和核心数匹配合并力度。此外还支持基于 semantic_label 的配置示例如下# 函数粒度配置 pypto.frontend.jit( pass_options{cube_l1_reuse_setting: {DEFAULT: 4, func8_0: 1, func8_1: 1}} ) # semantic_label配置可与函数粒度key共存 pypto.frontend.jit( pass_options{cube_l1_reuse_setting: {C1: 4}} )在仓库的 config.py 中cube_l1_reuse_setting的值不仅可以传 int 合并数量还支持(count, side)元组形式其中side取left/right/auto用于将 L1 复用合并偏向到左矩阵consumer → L0A或右矩阵consumer → L0B一侧默认auto保持既有合并轴选择侧向偏置按匹配到的子图逐个生效若所选侧不存在候选则回退到默认选择示例写法如cube_l1_reuse_setting{DEFAULT: 4, func8_0: (1, left), MM1: (2, right)}。键格式方面int 键如0、1、-1为全局设置作用于所有 functionfunc{magic}_{order}字符串键为函数粒度设置仅作用于指定 functionMagic 与 local hashOrder 对应的函数。CubeNBuffer 针对的是不能使能 L1Reuse 的场景此类场景较少主要有以下两种Cube 子图间没有重复 L1 搬运。如 BatchMatmul 的左右矩阵 Shape 分别为 (128,64,64) 和 (128,64,64) 时pass 切出 128 个左右矩阵 Shape 分别为 (64,64) 和 (64,64) 的同构 Cube 子图后它们之间没有重复 L1 搬运。还有 FA 算子的 MM2不同 S2 block 的 MM2 子图之间没有重复 L1 搬运。K 轴很长。当没有进行切 K 时L1Reuse 要求左矩阵的一整行或右矩阵的一整列数据块驻留在 L1 中。而 L1 缓存容量有限。因此当 K 轴较长、且没有切 K 时无法使用 L1Reuse。此时可以配置cube_nbuffer_setting参数并结合泳道图实测数据进一步调优。cube_nbuffer_setting在 config.py 中定义为同构 AIC 子图合并数量配置键格式与vec_nbuffer_setting相同。Vector 广度方向合图Vector 运算场景下通过 set_pass_options 接口的vec_nbuffer_setting参数配置广度方向的合图操作。需要注意的是应先进行前面的优化步骤将上下游子图的切分和合并调到较合适后再尝试使用 vecNBuffer 来进行广度合并。当泳道图内有同构子图组具有大量的小子图耗时在 10us 以下时应该使用该功能进行优化以减少调度开销和 kernel 的头开销。vec_nbuffer_setting参数的配置方式与cube_nbuffer_setting相似。其语义在 config.py 中定义为同构 AIV 子图合并数量配置int 键为全局设置func{magic}_{order}字符串键为函数粒度设置。调度策略调优PyPTO 算子的核间的流水由 AICPU 对子图的调度确定它基于子图间的依赖关系和核间任务的调度策略。可以尝试更改该调度策略以达到更优的算子性能。当上下游子图之间依赖较为简单或下游子图输入 Tensor 的 L2 命中率较为重要时推荐使用 L2 亲和调度配置方式如下pypto.frontend.jit(runtime_options{device_sched_mode: 1})具体配置时应综合考虑 L2 复用与负载均衡的影响不同场景的最佳配置策略不同应结合泳道图具体分析。仓库中已有采用该策略的实践案例例如 lightning_indexer_prolog_quant_hif8_impl.py 即通过runtime_options{device_sched_mode: 1}配置 L2 亲和调度。其他调优方法特殊 Shape 时可以尝试使用 Vector 操作提前处理输入矩阵使其变成更标准的 Shape。以左右矩阵 Shape 分别为 (884736,16) 和 (16,16) 的矩阵乘为例。如果只使用 L1Reuse只能优化到 500us。而使用下面的写法提前将四个重复的右矩阵在对角线拼成一个 Shape 为 (64,64) 的新的右矩阵 c将它和 Reshape 后的左矩阵做矩阵乘则算子性能大幅提升到 40us。def matmul_kernel(a, b, out): # 构造c pypto.set_vec_tile_shapes(64, 64) d pypto.full([16, 16], 0.0, pypto.DT_BF16) c1 pypto.concat([b, d, d, d], 1) c2 pypto.concat([d, b, d, d], 1) c3 pypto.concat([d, d, b, d], 1) c4 pypto.concat([d, d, d, b], 1) c pypto.concat([c1, c2, c3, c4], 0) # a变形 a pypto.reshape(a, [221184, 64]) # 矩阵乘 pypto.set_pass_options(cube_l1_reuse_setting{DEFAULT: 9}) pypto.set_cube_tile_shapes([512, 512], [64, 64], [64, 64], True) e pypto.matmul(a, c, pypto.DT_BF16) e pypto.reshape(e, [884736, 16]) pypto.assemble(e, [0, 0], out)增加冗余计算来避免冗余依赖和搬运。例如通过将e_score_bias_2d复制tile_batch份后进行 cast 操作使得每一份的 cast 都和对应 batch 的其他操作进行了合图。这避免了e_score_bias_2d的 cast 操作和每个 batch 的后续计算产生一对多的子图依赖、增加调度开销还避免了 cast 结果的搬运。e_score_bias_2d_tile pypto.tensor([tile_batch, ne], e_score_bias_2d.dtype, e_score_bias_2d_tile) for tmp_idx in range(tile_batch): pypto.assemble(e_score_bias_2d, [tmp_idx, 0], e_score_bias_2d_tile) e_score_bias_2d_cast pypto.cast(e_score_bias_2d_tile, tile_logits_fp32.dtype)尽量避免处理尾轴长度较小的 Tensor。使用较大的 TileShape 也无法避免 Operation 的输入 Tensor 的尾轴较小时可以使用 concat、transpose 或 reshape 等数据操作 Operation 来增大尾轴。通过 set_cache_policy 设置合理的 L2 CacheMode对于只访问一次的 Global Memory 数据设置其访问状态为不进入 L2 Cache。当进行上述优化后算子性能仍然较差时需要考虑 TileOperation 本身实现是否较差。可以构造单独 Operation 的用例与 Ascend C 小算子的性能对比确认性能较差后检查是否没有使用更优的指令。总结PyPTO 的性能调优方法论可以概括为一条清晰的闭环路径先用泳道图工具拿到开箱性能再通过 loop 写法与 TileShape 初值做第一轮结构性优化最后以 Man-In-The-Loop 的方式在 Stitch、TileShape、合图深度/广度、调度策略四个维度上迭代调优。整个过程中泳道图既是性能瓶颈的X 光片也是验证每一次调整效果的标尺runtime_debug_mode、DUMP_DEVICE_PERF、PMU/PMU Trace 等采集手段则提供了从系统级调度到核内流水线级的完整观测能力。所有配置项均可在仓库源码config.py、codegen_cloudnpu.cpp、machine_perf_trace.py与测试用例test_dump_perf.py、test_ctrl_cpu_perf.py中找到对应实现与验证依据读者可以在实际算子开发中直接复用本文的调优流程与配置模板。【免费下载链接】pyptoPyPTO发音: pai p-t-oParallel Tensor/Tile Operation编程范式。项目地址: https://gitcode.com/cann/pypto创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考