CV融合算子性能调优案例集

写在前面

本文整理CATLASS中CV融合算子的性能分析和调优方法。这里的CV融合指同一个kernel中同时使用AIC和AIV,将矩阵计算、后处理、归约、Softmax或数据整理等阶段放到一条流水中执行。

本文主要覆盖三个样例:

样例 路径 融合形态
quant_matmul examples/12_quant_matmul AIC执行int8 matmul,AIV执行per-token/per-channel反量化和写回
streamk_matmul examples/37_streamk_matmul AIC执行普通块和尾轮Stream-K块,AIV归约尾轮部分和
FAI examples/49_ascend950_flash_attention_infer AIC执行QK/PV,AIV执行Online Softmax/RescaleO

CV融合算子性能调优时不能只看单个阶段的算力利用率,它的调优收益来自阶段重叠,最终耗时通常由最长流水、流水空泡、核间长尾、GM workspace搬运和同步开销共同决定。

1. 通用分析方法

获取性能数据

建议先按单算子方式执行样例并采集Profiling数据,确认AIC、AIV、MTE2、MTE3、FixPipe占比。CATLASS中性能工具的使用方式可参考msProf&Profiling

采集数据时建议准备两组结果:

数据 用途
融合kernel总耗时 判断整体收益和不同参数组合的最终效果
AIC/AIV/MTE/FixPipe流水占比 判断是Cube/Vector、搬运/写回成为瓶颈
每核任务量或每核耗时 判断核间负载是否均衡
workspace大小和对齐信息 判断CV阶段间中间结果是否引入额外GM瓶颈

如果需要观察更细的流水并行关系,可以结合性能流水仿真,重点看AIC和AIV之间是否存在长时间互等。

建立融合收益模型

可以用下面的简化模型判断一个CV融合优化是否值得继续推进:

串行耗时 = AIC阶段耗时 + AIV阶段耗时 + 搬运/写回耗时
融合耗时 ~= max(AIC流水耗时, AIV流水耗时, 搬运流水耗时) + 首尾flush开销 + 同步开销
融合收益 = (串行耗时 - 融合耗时) / 串行耗时

理想情况下,AIC和AIV耗时接近,且中间数据不需要反复搬入搬出GM,融合收益更明显。如果某一侧远慢于另一侧,另一侧会出现等待,融合收益会收敛到瓶颈阶段耗时。若切块过小、阶段数过多或workspace访问低效,融合后性能还可能因为调度、同步和搬运问题而下降。

设计优化方案

CV融合算子的调优通常按以下顺序推进:

  1. 调整TileShape或基本块大小,让单块计算粒度足够大,同时满足L1/L0/UB资源限制。
  2. 调整block scheduler和swizzle,让基本块数量与AIC数量更匹配,实现负载均衡。
  3. 调整CV流水深度或workspace stages,让AIC产出和AIV消费尽量重叠。
  4. 检查中间结果路径,优先使用片上缓存或对齐的workspace,避免低效GM中转。
  5. 根据Profiling定位MTE2、MTE3、FixPipe问题,针对输入布局、stride、workspace对齐和写回路径优化。

2. 案例集

案例一:quant_matmul反量化融合

样例结构

examples/12_quant_matmul/quant_matmul.cpp实现int8输入、int32累加、half输出的量化矩阵乘:

C_int32 = A_int8 * B_int8
D_half = C_int32 * scale[n] * perTokenScale[m]

样例中的关键模板配置如下:

using L1TileShape = GemmShape<128, 256, 512>;
using L0TileShape = GemmShape<128, 256, 128>;
constexpr uint32_t workspaceStages = 2;
constexpr uint32_t ubStages = 2;
using EpilogueDispatchPolicy = Epilogue::EpilogueAtlasA2PerTokenDequant<ubStages>;
using EpilogueTileShape = MatrixShape<32, 256>;
using MatmulKernel =
    Gemm::Kernel::QuantMatmulMultiStageWorkspace<BlockMmad, BlockEpilogue, BlockScheduler, workspaceStages>;

该kernel的融合边界在include/catlass/gemm/kernel/quant_matmul_multistage_workspace.hpp中:

阶段 执行单元 工作
Matmul AIC 读取A/B,执行int8 BlockMmad,FixPipe把int32中间结果写到workspace
Epilogue AIV 从workspace读取int32,加载scaleperTokenScale,完成广播乘、类型转换和D写回

AIC和AIV在workspace槽位形成生产者-消费者流水。AIC写完当前stage后设置flagAicFinishStoreList[stageId],AIV消费完成后设置flagAivFinishComputeList[stageId],AIC在复用同一stage前等待AIV完成。

性能瓶颈判断

如果Profiling显示AIC占比高,优先看matmul侧:

现象 可能原因 调优方向
AIC长时间运行,AIV稀疏 L1TileShape过小或基本块数量过多 增大M/N/K tile,减少循环次数
MTE2占比高 A/B输入布局或stride不友好 检查A RowMajor、B ColumnMajor是否匹配业务输入
FixPipe占比高 int32中间结果写workspace压力大 检查workspace地址对齐和stage布局

如果AIV占比高,优先看epilogue侧:

现象 可能原因 调优方向
AIC频繁等待AIV释放stage 反量化UB tile过小或Vector计算重 调整EpilogueTileShapeubStages或减少scale搬运
scale/per-token scale搬运占比高 scale访问重复,M/N tile不匹配 调整M/N tile,让scale复用和输出写回更连续
尾块AIV耗时异常 M/N不对齐导致UB和GM搬运尾块多 优先选择能降低尾块比例的TileShape

调优要点

1. 配平AIC和AIV阶段

L1TileShape<128, 256, 512>意味着每个AIC基本块输出128 x 256个int32中间结果。AIV侧EpilogueTileShape<32, 256>会把一个AIC tile在M方向拆成4个epilogue tile处理。若AIV成为瓶颈,不能只增大matmul tile;更大的AIC tile会增加单个stage中AIV需要消费的数据,可能让AIC更频繁等待stage释放。

可按下面方式做候选搜索:

候选1:固定L1TileShape::M/N,调整EpilogueTileShape::ROW
候选2:固定EpilogueTileShape,调整L1TileShape::M/N以改善核间均衡
候选3:同时调整L1TileShape::K和L0TileShape::K以改善Cube效率

每组候选都需要确认workspace大小:

workspace_size = L1TileShape::M * L1TileShape::N * aicCoreNum * workspaceStages * sizeof(uint32_t)

workspace过大时,GM访问和cache压力可能抵消融合收益。

2. 调整workspaceStages

workspaceStages = 2是典型双缓冲。若流水图显示AIC写完stage后经常等待AIV,可以尝试增加stage数来扩大AIC提前量。但stage数不是越大越好,它会线性增加workspace容量和同步flag数量,也可能放大GM中间结果读写压力。

建议只有在下面条件同时满足时再增加stage:

条件 说明
AIC单块耗时短于AIV单块耗时 AIC有机会通过提前计算覆盖AIV
workspace带宽不是瓶颈 否则增加stage只会增加GM压力
基本块数量足够多 太少时首尾flush开销占比高,增加stage收益有限

3. 调整BlockScheduler和Swizzle

样例根据m > n选择GemmIdentityBlockSwizzle<3, 0>,否则选择<3, 1>。调优时先用当前TileShape估算基本块数量:

block_num = CeilDiv(M, L1TileShape::M) * CeilDiv(N, L1TileShape::N)
tail_blocks = block_num % aicCoreNum

tail_blocks很小但非0,会出现尾轮长尾;若block_num < aicCoreNum,AIC利用率不足。此时优先调整L1TileShape::M/N,让基本块数接近AIC数量的整数倍,再微调swizzle方向和offset改善A/B访问局部性。

案例二:streamk_matmul尾轮提前计算

样例结构

examples/37_streamk_matmul实现Stream-K matmul。普通块仍由AIC直接计算并写回C,尾轮中不足以填满所有AIC的基本块沿K方向切分,产生更多工作单元,AIC把部分和写到workspace,AIV再归约得到结果C。

样例关键配置如下:

using DispatchPolicy = Catlass::Gemm::MmadAtlasA2Streamk<enableUnitFlag, enableShuffleK>;
using L1TileShape = GemmShape<128, 256, 256>;
using L0TileShape = GemmShape<128, 256, 64>;
using BlockScheduler = typename Gemm::Block::StreamkGemmIdentityBlockSwizzle<3, 0>;
constexpr uint32_t computeLength = 192 * 1024 / sizeof(ElementAccumulator);
using ReduceAdd =
    Catlass::Gemm::Kernel::StreamkReduceAdd<ArchTag, BlockScheduler, ElementAccumulator, ElementC, computeLength>;

Stream-K的详细原理可参考StreamkMatmul说明。在CV融合视角下,它有两个关键点:

优化 CV融合含义
尾轮切K AIC把尾轮拆成更多K slice,减少尾轮AIC空闲
提前计算尾轮 AIC提前产生尾轮部分和,使AIV归约可以与后续Cube计算重叠

尾轮提前计算

普通matmul的尾轮形态通常是:

round 0..r-1: AIC计算完整基本块
round r:      少量AIC计算尾轮基本块,其余AIC空闲

Stream-K尾轮切分后,如果等所有普通块完成后再让AIV归约,Vector归约会落在kernel末尾,成为额外尾巴。样例中的调度把尾轮Stream-K块提前到倒数第二轮附近执行,使AIV能够更早开始读取workspace并归约部分和。抽象流水如下:

时间片 t:     AIC计算提前的Stream-K尾轮部分和 -> workspace
时间片 t+1:   AIV归约尾轮部分和
时间片 t+1:   AIC继续计算普通完整块

这种方式本质上是用后续普通块的Cube计算时间掩盖Vector归约时间。

性能瓶颈判断

Stream-K适用于尾轮负载不均衡的shape,经验判断如下:

B = CeilDiv(M, m1) * CeilDiv(N, n1)
当 B / C > 1 且 B % C <= C * 0.8 时,可能收益较好

其中C为AIC数量。可以按下面方式判断是否应该使用Stream-K:

现象 判断
B % C == 0 无尾轮,Stream-K归约开销通常没有收益
B < C M/N基本块太少,应优先考虑Split-K或更小M/N tile
B / C == 1 只有一轮普通块,提前尾轮可掩盖空间有限
尾轮块数很少 Stream-K更可能受益
K很短 切K后slice过少,AIV归约开销可能超过收益

调优要点

1. 先调M/N基本块,再决定是否Stream-K

Stream-K解决的是尾轮不均衡,不是所有负载不均衡问题。如果L1TileShape::M/N选择不合理导致基本块数量远小于AIC数量,应先调整TileShape。只有在完整轮次足够、尾轮块数不足时,Stream-K才更容易体现收益。

建议先计算:

m_blocks = CeilDiv(M, L1TileShape::M)
n_blocks = CeilDiv(N, L1TileShape::N)
mn_blocks = m_blocks * n_blocks
normal_rounds = mn_blocks / aicCoreNum
tail_blocks = mn_blocks % aicCoreNum
k_slices_per_block = CeilDiv(K, L1TileShape::K)
streamk_work = tail_blocks * k_slices_per_block

streamk_work越接近或超过AIC数量,尾轮越容易被均摊;如果k_slices_per_block太小,尾轮可切分粒度不足。

2. 控制AIV归约的UB压力

StreamkReduceAdd用UB读取多个partial sum并做Add/Cast/写回。样例中computeLength = 192 * 1024 / sizeof(ElementAccumulator),需要满足UB空间约束。归约时每个AIV处理的行数由COMPUTE_LENGTH、参与归约的AIV数量和tile列宽共同决定。

如果AIV归约成为瓶颈,可检查:

参数 影响
L1TileShape::N 决定每行partial sum长度,影响UB一次可处理行数
tail_blocks 决定需要归约的Stream-K块数量
splitkSliceNum 决定每个输出块要累加多少份partial sum
computeLength 决定AIV每次搬运和归约的数据上限

调优方向通常是避免过大的L1TileShape::N导致AIV每次只能处理很少行,同时避免过小tile导致AIC普通块效率下降。

3. 检查workspace大小和对齐

Stream-K workspace大小为:

max(2MB, L1TileShape::M * L1TileShape::N * sizeof(ElementAccumulator) * aicCoreNum * 2)

每个AIC有两片workspace,用于处理跨块时的前后两段partial sum。若Profiling显示MTE3或MTE2偏高,优先检查workspace起始地址、每片workspace步长和输出C矩阵stride是否满足高效搬运要求。对齐问题会直接影响AIC写partial sum和AIV读partial sum。

4. 验证尾轮提前是否真正覆盖了归约

观察流水图时重点看三件事:

目标 期望现象
AIC尾轮提前 Stream-K块不全部集中在kernel末尾
AIV归约重叠 AIV Add/Cast/MTE3与后续AIC BlockMmad有重叠
末尾flush短 kernel尾部只剩少量AIV归约和写回

如果AIV归约仍然集中在末尾,通常说明可提前计算的普通块不足、尾轮切分粒度不足或同步点过晚。

案例三:FAI多阶段CV流水

examples/49_ascend950_flash_attention_infer样例的详细性能优化分析在FA算子性能优化,本文只进行关键点概括。

FAI计算链路为:

QK(AIC) -> Online Softmax(AIV) -> PV(AIC) -> RescaleO(AIV)

这是典型的多阶段CV融合。与quant_matmul和streamk_matmul相比,FAI有更强的数据依赖:Softmax依赖QK结果,PV依赖Softmax结果,RescaleO依赖PV结果和online softmax状态。因此调优重点不是简单增加并发,而是让相邻kvSeq块之间形成稳定流水。

FAI样例中的关键优化方向包括:

方向 说明
TileShape调整 增大qSeqBase/kvSeqBase可减少循环和同步次数,但会增加UB/L1/L0占用
CV流水 通过QK、Softmax、PV、RescaleO的跨块重叠减少AIC/AIV互等
片上中转 AIV将Softmax结果P从UB搬到AIC侧L1,避免UB -> GM -> L1中转
多核负载均衡 causal或稀疏mask下按有效block数切分,避免按q行均分造成长尾
Paged Attention 检查block table、KV cache page连续性和对齐,避免MTE2跳读
FixPipe和workspace对齐 保证输出和workspace分段起始地址按512B等要求对齐

更详细的参数解释、资源预算和调优路径可参考FA算子性能优化

3. 融合算子调优检查表

Tile和资源

检查项 说明
L1/L0/UB是否超限 Tile变大前先计算静态buffer占用
M/N基本块数量 尽量避免block_num < aicCoreNum或尾轮过小
K方向切分粒度 Stream-K和Split-K需要足够K slice才能均衡
epilogue tile quant_matmul中AIV tile要和AIC输出tile配平

CV流水

检查项 说明
AIC是否等待AIV 可能是AIV计算、搬运或stage不足
AIV是否等待AIC 可能是Cube、MTE2或FixPipe瓶颈
stage数是否合适 stage过少覆盖不足,过多增加workspace和同步压力
首尾flush占比 小shape或少循环时融合流水收益有限

搬运和对齐

检查项 说明
输入layout A/B/Q/K/V物理布局要匹配kernel的TLA/Layout定义
GM workspace 起始地址和分段offset尽量保证512B对齐
stride 连续维stride应满足高效DataCopy/FixPipe要求
中间结果路径 优先片上中转;必须落GM时减少重复读写

负载均衡

检查项 说明
每核基本块数 Matmul类先看M/N基本块分布
每核有效block数 FAI causal/mask场景按有效块估算权重
尾轮大小 Stream-K重点看B % C和K slice数量
swizzle Tile确定后再调swizzle方向和offset

4. 总结

CV融合算子的性能优化核心是“配平”和“覆盖”:让AIC和AIV的单阶段耗时尽量接近,用stage、workspace或片上缓存把生产和消费重叠起来,同时避免切块、同步和GM中转带来的膨胀。

三个样例的侧重点不同:

样例 最重要的调优问题
quant_matmul AIC int8 matmul和AIV反量化是否配平,workspace stage是否合适
streamk_matmul 尾轮是否足够不均衡,提前计算是否覆盖AIV归约
FAI QK/Softmax/PV/RescaleO是否形成稳定流水,多核有效block是否均衡