graph-autofusion V2 节点性能建模实战:基于 af-perf-modeler 的 Cast/Reduce/Compare 算子性能公式推导指南
【免费下载链接】graph-autofusionGraph-autofusion 是一个面向昇腾(Ascend)芯片的轻量级、解耦式组件集合,旨在通过自动融合技术加速模型执行。 目前已开源 SuperKernel 组件和 Autofuse 组件,未来将持续开放更多自动融合相关模块。项目地址: https://gitcode.com/cann/graph-autofusion
af-perf-modeler是 graph-autofusion(CANN 昇腾芯片自动融合组件库)内置的 V2 节点性能建模 SKILL,面向 Cast、Reduce、Compare 等算子的性能公式建模、ATT 性能分析、MicroAPI 成本统计以及 codegen 与性能公式对齐场景。本文以该 SKILL 的完整方法论为主线,结合仓库内 codegen 实现、ATT 性能表和参数透传链路的真实源码,系统讲解如何从 AscendC/MicroAPI 源码与 codegen 实际传参反推性能公式,并给出可复用的建模输出模板与自查清单。
一、Skill 定位:graph-autofusion 的 V2 节点性能建模入口
在 graph-autofusion 仓库中,.claude/skills/af-perf-modeler/目录下包含两个文件:README.md 与 SKILL.md。其中 README 给出了 Skill 的定位、功能范围和验证方法,SKILL.md 则承载了完整的建模方法论,两者共同构成 af-perf-modeler 的完整能力描述。
该 Skill 覆盖三类典型工作:
- 算子性能公式建模:为 Cast、Reduce、Compare 等算子建立可解释、可追溯的性能公式;
- ATT 性能建模与 MicroAPI 成本分析:基于 ATT(Auto Tiling Tool,自动切分工具)的 VF(Vector Function)性能管线,统计
repeat_time/call_count等循环执行参数并累加 MicroAPI 成本; - codegen 与性能公式对齐、tiling 参数透传:确保 ATT 性能计算拿到的参数与 codegen 生成代码时实际传给 AscendC API 的参数一一对应。
触发场景
当用户提出以下诉求时,即进入 af-perf-modeler 的作用域:
- 提到 Cast / Reduce / Compare 等算子的性能公式;
- 提到 ATT 性能建模、MicroAPI 成本、
repeat_time/call_count; - 提到 codegen 与性能公式对齐、tiling 参数透传。
核心原则
Skill 明确了一条不可妥协的建模红线:性能公式必须从节点对应的 AscendC/MicroAPI 源码和 codegen 实际传参反推,不能只套现有公式或凭经验估算。任何源码入口、API 参数、codegen 传参、分支含义或 MicroAPI 语义不确定的点,都必须先向用户说明并询问,严禁编造公式。
二、建模前提:先定位五类核心源码文件
建模开始前,必须先定位并读取以下文件(SKILL.md 中列出的建模前提,路径均以仓库根目录为基准):
| 建模要素 | 仓库路径 | 作用 |
|---|---|---|
| codegen impl | v2_ascir_codegen_impl.h | 每个 V2 节点类型对应一个AscIrCodegenImplV2子类,通过GetApiName()/GetApiCallName()指明 API 与调用生成器 |
| AscendC API 源码 | autofuse/v35/ascendc/api_regbase/ | 由GetApiName()指向的算子真实实现及其调用的 helper |
| codegen API 调用生成逻辑 | autofuse/v35/codegen/ | 由GetApiCallName()指向的{ApiCallName}::Generate,决定生成代码实际传给 API 的参数 |
| 性能表 | perf_param_v2.cpp | kVfInstructPerfTable等性能表,MicroAPI 到 latency / throughput 的映射 |
| 当前性能注册/实现 | ascir_api_perf_v2.cpp、ascendc_regbase_perf.cpp | 性能公式的最终落地实现 |
| 参数透传参考 | ascir_node_param.h 中的VectorFuncNodeParams、CastNodeParams等 | codegen 向 ATT 透传参数的载体 |
以 Cast 为例,在 v2_ascir_codegen_impl.h 中可以看到CastAscIrCodegenImplV2类:GetApiCallName()返回"CastV2ApiCall",GetApiName()返回"CastExtend",并实现了IsVectorFunctionSupported、IsSimtScalarSupported等判定逻辑,用于决定节点走 VF 融合还是 SIMT 标量路径。建模时需据此追溯到真实 API 实现。
三、通用建模流程:八步从源码反推性能公式
1. 定位算子源码入口
从v2_ascir_codegen_impl.h找到目标算子对应的GetApiName(),再到autofuse/v35/ascendc/api_regbase/下查找该 API 名称的定义。读取 API 实现及其调用的 helper,不只读 perf 代码,因为性能公式的分支与 MicroAPI 序列完全由源码决定。若 API 名称与源码实现无法唯一对应,先询问用户确认。
2. 确认源码输入参数与 codegen 实际传参
- 先分析 AscendC API 源码的函数签名,确认参数含义、类型、顺序和模板参数;
- 从
v2_ascir_codegen_impl.h找到目标算子的GetApiCallName(),据此在autofuse/v35/codegen/下找到{ApiCallName}::Generate; - 从
Generate中分析生成代码实际传入 AscendC API 的参数,包括inputs、outputs、scalar、offset、stride、mask、loop 参数、tmp buf、axis 信息等。
一个必须警惕的语义陷阱:codegen 的inputs/outputs是生成代码用的Tensor,而 ATT 性能计算的input_shapes/output_shapes是性能建模用的TensorShapeInfo,两者语义对应但不是同一对象,建模时不可混用。
3. 必要参数从 codegen 传递到 ATT
如果性能公式需要 codegen 阶段才能确定的参数(例如 loop merge 后的cal_count、outer_repeats、stride、mask mode、特殊分支标记),禁止在 ATT 中凭空假设。参数传递遵循一条标准链路:
EnrichAscirGraphNodeParams 预注册空参数结构体 -> {ApiCallName}::Generate 按实际调用填充具体值 -> FillSpecificParams 提取到 NodeInfo -> perf 函数读取这条链路在仓库中有完整实现:
- 预注册入口:ascir_param_builder.h 声明
EnrichAscirGraphNodeParams(const af::AscGraph &graph); - 参数载体:ascir_node_param.h 中
AscirNodeParams::specific_params是一个std::variant,可承载ReduceNodeParams、VectorFuncNodeParams、CastNodeParams、BroadcastNodeParams、CompareNodeParams、WhereNodeParams、UnaryBitWidthChangeNodeParams、TransposeNodeParams等专用结构体; - 提取入口:specific_params_builder.h 声明
FillSpecificParams(const af::AscNodePtr &ge_node, NodeInfo &node_info)。
以 Cast 为例,cast_v2_api_call.cpp 中的FillCastNodeParams将output_dims、output_strides、input_strides写入CastNodeParams;该结构体在 ascir_node_param.h 中定义,字段与 SKILL 中强调的"保存output_dims、output_strides、input_strides,仅排除dst、src"完全对应。
透传时的两条硬性要求:
- 只预注册空结构:对只能在
Generate中确定的参数(如ApiLoopParams、合轴后实参、实际生成分支),EnrichAscirGraphNodeParams只负责预注册空结构体,不要在预注册阶段猜具体值; - 保存原始符号表达式:
Generate填充给 ATT 的节点参数必须使用 codegen/merge 阶段的原始符号参数(如merge_info、ApiLoopParams中的 repeat、stride、count),不要保存tpipe.tiler.ActualSize(...)、tpipe.tiler.Size(...)等用于生成 C++ 调用字符串的展开表达式,因为 ATT 性能公式无法稳定识别这些 tiler 表达式。
这一点在 Cast 的生成代码中有直观对照:CastV2ApiCall::Generate在生成 API 调用字符串时使用tpipe.tiler.Size(param.output_second_to_last_stride)(见 cast_v2_api_call.cpp),而写入节点参数时则使用merge_info.merge_repeats、param.cal_count、param.output_second_to_last_stride等原始符号(见同文件 L87-L106),两者用途严格分离。
4. 分析源码分支
保持与源码if constexpr、if、模板参数、dtype 特化、dim 分支、broadcast 分支、scalar 分支、mask mode 分支一致。每个源码分支都要说明触发条件、codegen 参数来源和公式差异,不允许只按 dtype 或经验简化掉源码中的关键分支。
以 Cast 的 VF 支持判定为例,v2_ascir_codegen_impl.h 中的IsVectorFunctionSupported就体现了典型的源码分支约束:Cast 只能处理 2 倍及以内位宽变化(input_dtype_size > output_dtype_size * 2U时拒绝 VF 融合);8 字节与 4 字节之间的转换在 MicroAPI 层要求源/目的使用双寄存器,VF codegen 只生成单寄存器张量会导致设备上仅填充半数 lane(真机验证约 50% mismatch),因此该组合也必须拒绝 VF 融合而保留在根图走CastExtend。这些分支直接影响建模时是否将节点计入 VF 性能管线。
5. 枚举 MicroAPI
以源码中的每个MicroAPI::xxx或 AscendC 基础向量 API 作为最小建模粒度,将其映射到perf_param_v2.cpp中的kVfInstructPerfTable类型。映射前必须查表确认,不要假设表项存在;找不到精确匹配时使用kPlaceholder,并在代码注释中说明真实 MicroAPI 名称和暂用原因。
kVfInstructPerfTable在 perf_param_v2.cpp 中定义,其中既有kAdd、kSub、kCast、kCompareScalarNE、kUpdateMask、kDuplicate、kPlaceholder等指令级条目(每条目给出支持的 dtype 集合及 latency、throughput 数值),也有kCast的 dtype 映射表kVfInstructDtypeMappingPerfTable(见 L465-L484),用于处理需要按输入/输出类型分别判断的场景。
6. 计算 MicroAPI 执行次数
- 每个源码中的 MicroAPI 都应能追溯到性能公式中的一次
VfPerfUtils::AddVfInstructPerf计算,其最后一个参数必须来自源码循环次数或 codegen 透传参数; - 对嵌套循环计算总次数,例如
outer_count * repeat_time; - 对只在循环外执行一次的
Duplicate、mask 创建、scalar 初始化等,次数按源码实际执行次数建模,不要误乘repeat_time; - 如果循环次数依赖运行时数据值,必须说明无法精确获取的原因,并采用保守估计或要求透传参数。
7. 合并同类 MicroAPI
同一种 MicroAPI 类型可以只调用一次AddVfInstructPerf,最后一个参数使用该 MicroAPI 在当前分支中的总执行次数,合并时必须保留注释或表格追溯每一部分次数的源码位置。合并受以下约束:
- 对
kPlaceholder,合并键必须包含真实 MicroAPI、操作方向/模式和 dtype,三者都相同才能合并; - 即使性能表类型相同,也禁止合并不同真实 MicroAPI(如
Not与ShiftRights、Pack与UnPack); MicroAPI::DataCopy的 load 与 store 必须分开,不能因为都映射为kPlaceholder而汇总;kPlaceholder直接调用VfPerfUtils::AddVfInstructPerf,不要增加AddPlaceholderPerf一类包装函数。
8. 套用统一 cost 结构
每个 MicroAPI 通过VfPerfUtils::AddVfInstructPerf(type, dtype, max_latency, all_vf_instruct_cost, count)累加;内部查询 latency 和 throughput,latency 取所有 MicroAPI 的最大值,throughput * count累加到all_vf_instruct_cost。最终公式固定为:
Expr res = VfPerfUtils::GetVFHeadCost() + max_latency + all_vf_instruct_cost; res.Simplify(); perf.pipe_res[PipeType::AIV_VEC] = res;VfPerfUtils的接口定义见 vf_perf_utils.h,包括AddVfInstructPerf、GetVfInstructDtypeMappingPerf、GetVFHeadCost、GetVectorFunctionPerf等。这套公式结构在 ascendc_regbase_perf.cpp 中有真实落地:res = VfPerfUtils::GetVFHeadCost() + max_latency + all_vf_instruct_cost; perf.pipe_res[PipeType::AIV_VEC] = res;。部分场景(如 mask 相关路径)还会用call_count整体加权:res = (VfPerfUtils::GetVFHeadCost() + max_latency + all_vf_instruct_cost) * call_count;(见同文件 L205-L207),这印证了repeat_time / call_count分析在 Skill 中的重要性。
四、API 参数对齐原则与透传链路
性能模型透传的参数必须与实际 AscendC API 调用保持一一对应:
- 排除输入、输出 Tensor 后,按 API 签名的原始顺序保存其余参数;不要用
repeat_count、flattened_count或分支标志替换原始参数; - 参数的维度、stride、mask、offset 和 loop 数组要保持与生成代码相同的语义粒度,性能模型只在计算公式内部派生临时量;
- 以
CastExtend(dst, src, output_dims, output_stride, input_stride)为例,节点参数应保存output_dims、output_strides和input_strides,仅排除dst、src; CastExtend节点参数中的output_dims、output_strides、input_strides应来自merge_info/ApiLoopParams的原始表达式;实际生成 API 调用时可以使用tpipe.tiler,但传给性能公式的节点参数不能使用 tiler 展开结果;- 参数链路必须可追溯:
EnrichAscirGraphNodeParams预注册 →{ApiCallName}::Generate按实际调用填充 →FillSpecificParams提取 →NodeInfo→ perf 函数; - 如果原始 API 参数缺失,必须明确回退到 shape 信息的条件和精度影响;禁止在 ATT 中凭经验重建 codegen 已经确定的参数。
在仓库测试中可以看到这条链路的完整验证方式,例如 test_ascir_node_params.cpp 覆盖了节点参数结构体的构建与读取,ascir_reduce_test_helpers.h 展示了EnrichAscirGraphNodeParams(env.graph)与att::FillSpecificParams(env.node, node_info)的串联调用,为参数透传链路提供了可执行的测试佐证。
五、MicroAPI 映射规则
一般情况下,性能表类型为 MicroAPI 指令名增加k前缀,例如MicroAPI::Add对应kAdd、MicroAPI::UpdateMask对应kUpdateMask。Skill 不维护完整的一一映射表,建模时按以下顺序确认:
- 提取真实 MicroAPI 指令名,以
k + 指令名作为候选性能表类型; - 在
perf_param_v2.cpp和常量定义中确认候选类型真实存在,并确认目标 dtype 受支持;不能只根据命名猜测; - 对带模式语义的模板 API,使用性能表中对应的语义后缀,例如
CompareScalar<..., CMPMODE::NE>对应kCompareScalarNE(该条目确实存在于 perf_param_v2.cpp); - 如果没有精确表项,使用
kPlaceholder,并按真实 MicroAPI、操作方向/模式和 dtype 分开调用; DataCopy的 load 与 store 即使都没有精确表项,也必须分开建模并分别注释;同一次 load 或 store 的LoadDist/StoreDist模式不额外重复建模;- 如果性能表已有对应 MicroAPI 类型但暂不支持源码中的真实 dtype,仍按源码真实 dtype 调用该 MicroAPI 类型,不要为了命中现有表项改成中间 dtype、输出 dtype 或
kPlaceholder;性能表缺项应后续扩展,公式必须先保持源码语义正确。
MicroAPI::DataCopy的统一建模约定:load 使用 input dtype,store 使用 output dtype,LoadDist、StoreDist、pack/unpack 模式只作为注释说明,不因模式变化额外增加一条 DataCopy 成本。
六、Placeholder 调用示例:占位与可追溯性
当源码 MicroAPI 在性能表中没有精确条目时,占位调用必须保持"每一项都可追溯到唯一真实操作",下面是对比示例(摘自 SKILL.md 并保留原文语义)。
错误做法:不同真实操作或不同方向被汇总,后续无法替换为精确表项。
VfPerfUtils::AddVfInstructPerf(kPlaceholder, dtype, max_latency, all_vf_instruct_cost, repeat_time * 4);正确做法:直接调用AddVfInstructPerf,每个占位项都能追溯到唯一真实操作。
// MicroAPI::DataCopy (load). VfPerfUtils::AddVfInstructPerf(kPlaceholder, input_dtype, max_latency, all_vf_instruct_cost, repeat_time); // MicroAPI::DataCopy (store). VfPerfUtils::AddVfInstructPerf(kPlaceholder, output_dtype, max_latency, all_vf_instruct_cost, repeat_time); // MicroAPI::Pack<uint32_t, int64_t>, two calls with the same mode and dtype. VfPerfUtils::AddVfInstructPerf(kPlaceholder, int64_dtype, max_latency, all_vf_instruct_cost, repeat_time * 2); // MicroAPI::UnPack<uint64_t, uint32_t>. VfPerfUtils::AddVfInstructPerf(kPlaceholder, uint32_dtype, max_latency, all_vf_instruct_cost, repeat_time);注意:kPlaceholder在性能表中真实存在(perf_param_v2.cpp),latency 与 throughput 均为 0,因此占位项只是成本占位,真正约束公式语义的是其唯一可追溯性。仓库中的实际 perf 实现也遵循直接调用原则,例如 ascendc_regbase_perf.cpp 中对无法精确映射的 MicroAPI 直接以kPlaceholder调用AddVfInstructPerf。
七、建模输出模板
完成建模后,按以下模板输出结构化结论(SKILL.md 原文模板,此处完整保留):
### 源码依据 - codegen impl:`...` - API 名称:`...` - API 源码:`...` - API 调用生成:`...::{ApiCallName}::Generate` - 性能表:`.../perf_param_v2.cpp` - 当前 perf 实现:`.../ascendc_regbase_perf.cpp` ### 参数来源 | 源码参数 | codegen 传参来源 | ATT 是否可直接获取 | 处理方式 | | --- | --- | --- | --- | | `dst` | `outputs[0]` | 是/否 | ... | | `src` | `inputs[0]` | 是/否 | ... | | `loop_param` | `ApiLoopParams` | 否 | 需通过节点参数透传 | ### 分支分析 - 分支 A:触发条件,参数来源,源码行,公式差异 - 分支 B:触发条件,参数来源,源码行,公式差异 ### MicroAPI 计数 | MicroAPI | perf 类型 | 执行次数 | dtype | 说明 | | --- | --- | --- | --- | --- | ### 公式 ```cpp Expr max_latency = CreateExpr(0); Expr all_vf_instruct_cost = CreateExpr(0); // AddVfInstructPerf(...) Expr res = VfPerfUtils::GetVFHeadCost() + max_latency + all_vf_instruct_cost; ``` ### 不确定点 - 无;或列出需要用户确认/源码待确认的问题。八、常见错误自查表
建模中最容易犯的错误集中在"公式与源码脱节"和"参数来源不清晰"两类,SKILL.md 给出的错误对照表完整如下:
| 错误 | 正确做法 |
|---|---|
| 直接复述现有 perf 公式 | 先读 API 源码和 codegen 传参,再解释现有公式是否简化 |
只看input_shapes/output_shapes | 同时看{ApiCallName}::Generate,确认实际传入 API 的参数 |
| 需要 loop 参数却在 ATT 中猜测 | 通过节点参数从 codegen 透传,或说明无法精确获取 |
Generate 才能确定的参数却要求EnrichAscirGraphNodeParams计算具体值 | EnrichAscirGraphNodeParams只预注册空结构,Generate填具体值 |
性能函数只能访问NodeInfo却让 ATT 直接读 Node | 补FillSpecificParams,把节点参数提取到NodeInfo |
| 只写入节点参数但 ATT 读取路径不明确 | 明确链路:预注册空结构 → Generate 填值 → FillSpecificParams → NodeInfo → perf |
给 ATT 的节点参数保存tpipe.tiler.Size/ActualSize展开值 | 保存merge_info/ApiLoopParams原始符号表达式,tiler 表达式只用于生成 C++ 调用字符串 |
忽略if constexpr或 scalar/mask 分支 | 每个源码分支单独列触发条件和计数 |
把所有 MicroAPI 都乘repeat_time | 按源码实际循环层级计算次数 |
| 找不到性能表项就跳过 | 使用kPlaceholder并注释真实 MicroAPI |
| 性能表暂不支持真实 dtype 就换成支持的 dtype | 保持源码真实 MicroAPI 类型和 dtype,后续扩展性能表 |
| 多个同类 MicroAPI 重复调用多次 | 合并为一次AddVfInstructPerf,次数求和 |
把不同真实 MicroAPI 汇总到一个kPlaceholder | 按真实 MicroAPI、方向/模式和 dtype 分开调用 |
把DataCopyload 和 store 合并 | load 与 store 分开调用并分别注释 |
因DataCopy的 pack/unpack 模式再额外补一条成本 | DataCopy 只按 load/store 建模,dtype 分别使用 input/output |
用辅助函数包装kPlaceholder | 直接调用VfPerfUtils::AddVfInstructPerf(kPlaceholder, ...) |
UpdateMask使用kPlaceholder | 使用性能表已有的kUpdateMask |
| 未说明不确定点 | 不确定时先提问或列入"不确定点" |
九、验证要求与验证方法
公式完成后的逐项检查清单
完成公式后至少检查以下事项(来自 SKILL.md 的验证要求,逐条保留):
- API 源码入口能从
GetApiName()追溯; - API 调用参数能从
{ApiCallName}::Generate追溯; - ATT 使用的额外参数若来自 codegen,已通过节点参数传递;若性能函数只能访问
NodeInfo,必须确认EnrichAscirGraphNodeParams已预注册空结构、Generate已填值、FillSpecificParams已提取到NodeInfo; - 公式中的每个
AddVfInstructPerf都能追溯到源码 MicroAPI; - 每个循环次数都能追溯到源码循环、codegen 参数或明确的保守估计;
- 每个 MicroAPI 类型都在
perf_param_v2.cpp中存在;不存在时已用kPlaceholder并说明; - 每个
kPlaceholder都有唯一的真实 MicroAPI、操作方向/模式和 dtype;load/store 及不同真实类型未合并; kPlaceholder均直接调用VfPerfUtils::AddVfInstructPerf,未增加包装函数;MicroAPI::UpdateMask使用kUpdateMask;- 最终 cost 使用
VfPerfUtils::GetVFHeadCost() + max_latency + all_vf_instruct_cost。
Skill 生效验证
在 opencode 中输入以下指令验证 Skill 是否生效(README 提供的官方验证方法):
- "计算cast算子的性能公式"
- "分析compare算子性能公式"
十、深入源码与测试:进一步学习路径
如果希望进一步验证本 Skill 方法论在仓库中的落地情况,可以按以下路径继续阅读:
- codegen 侧:v2_ascir_codegen_impl.h 定义全部 V2 节点的 codegen 实现(含
GetApiName/GetApiCallName);cast_v2_api_call.cpp 展示CastV2ApiCall::Generate如何填充CastNodeParams;reg_api_call/ 目录下还有compare_v2_api_call.cpp、unary_bitwidth_change_api_call_v2.cpp、reg_load_api_call.cpp等同类实现; - ATT 侧:ascendc_regbase_perf.cpp 与 ascir_reduce_api_perf_v2.cpp 是性能公式的落地实现,perf_param_v2.cpp 是性能表数据源;
- 测试侧:test_ascir_perf_v2.cpp、test_reduce_min_max_api_perf_v2.cpp 覆盖 ATT 性能公式,test_codegen_cast_reg_api_call.cpp 与 test_codegen_compare_reg_api_call.cpp 覆盖 codegen 传参,test_ascir_node_params.cpp 覆盖节点参数结构体。
结语
af-perf-modeler 提供了一套"源码 → codegen 传参 → 节点参数透传 → ATT 性能公式"全链路可追溯的 V2 节点建模方法论。对开发者而言,掌握这套流程意味着:面对任意 V2 算子,都能从GetApiName()/GetApiCallName()出发,沿EnrichAscirGraphNodeParams→Generate→FillSpecificParams→NodeInfo→ perf 函数的链路,推导出与真实生成代码语义一致的性能公式,而不是复制粘贴一份无法解释的既有公式。这套方法同时是 codegen、ATT 与性能模型三方对齐的工程规范,值得在新增算子建模或性能问题定位时直接复用。
【免费下载链接】graph-autofusionGraph-autofusion 是一个面向昇腾(Ascend)芯片的轻量级、解耦式组件集合,旨在通过自动融合技术加速模型执行。 目前已开源 SuperKernel 组件和 Autofuse 组件,未来将持续开放更多自动融合相关模块。项目地址: https://gitcode.com/cann/graph-autofusion
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考