性能调优最佳实践¶
本文档总结 PTO 算子性能调优的最佳实践,提供系统化的优化方法和经验总结。
目录¶
1. 优化流程¶
1.1 标准优化流程¶
正确性验证 → 性能基线 → 瓶颈分析 → 针对性优化 → 验证 → 迭代
详细步骤:
步骤 1:确保正确性¶
# CPU 仿真验证
python3 tests/run_cpu.py --testcase your_op --verbose
# NPU 验证
python3 tests/script/run_st.py -r npu -v a3 -t your_op
检查点: - ✅ 数值误差 < 1e-5(fp32)或 < 1e-3(fp16) - ✅ 所有测试用例通过 - ✅ 边界条件正确处理
步骤 2:建立性能基线¶
# 使用 msprof 采集性能数据
msprof --application="your_app" --output=./baseline
记录指标: - 总执行时间 - 各阶段时间占比(TLOAD/TMATMUL/TSTORE) - 内存带宽利用率 - 计算单元利用率
步骤 3:识别瓶颈¶
分析 profiler 输出:
TLOAD: 45% ← 内存搬运
TEXTRACT: 10% ← 布局转换
TMATMUL: 40% ← 计算
TSTORE: 5% ← 写回
瓶颈类型: - 内存受限:TLOAD/TSTORE 占比 > 60% - 计算受限:TMATMUL 占比 > 70% - 转换受限:TEXTRACT/TMOV 占比 > 20%
步骤 4:针对性优化¶
根据瓶颈类型选择优化策略(见后续章节)。
步骤 5:验证优化效果¶
对比指标: - 性能提升百分比 - 各阶段时间变化 - 数值正确性保持
步骤 6:迭代优化¶
重复步骤 3-5,直到达到性能目标或优化空间耗尽。
2. 性能分析方法¶
2.1 使用 msprof 工具¶
基础用法:
# 采集性能数据
msprof --application="./your_app" \
--output=./profiling_data \
--ai-core=on \
--task-time=on
# 查看报告
msprof --export=on \
--output=./profiling_data
关键指标:
| 指标 | 含义 | 目标值 |
|---|---|---|
| TMATMUL 占比 | Cube 单元利用率 | > 50% |
| TLOAD 占比 | 内存搬运时间 | < 40% |
| MTE 带宽 | 内存带宽利用率 | > 70% |
| 流水线气泡 | 空闲时间 | < 10% |
2.2 手动计时¶
在关键路径插入计时代码:
#include <chrono>
auto start = std::chrono::high_resolution_clock::now();
// 关键代码段
for (int i = 0; i < N; i++) {
TLOAD(tile, ...);
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
printf("TLOAD time: %ld us\n", duration.count());
2.3 理论性能计算¶
GEMM 理论峰值:
理论 TFLOPS = 硬件峰值 × 核数 × 利用率
例如 A3(24核):
- 硬件峰值:~50 TFLOPS/核(fp16)
- 理论峰值:50 × 24 = 1200 TFLOPS
- 实际可达:~70-80% = 840-960 TFLOPS
内存带宽理论值:
理论带宽 = 硬件带宽 × 利用率
例如 A3:
- 硬件带宽:~900 GB/s
- 实际可达:~70-80% = 630-720 GB/s
3. 常见性能问题¶
3.1 内存带宽受限¶
症状: - TLOAD/TSTORE 占比 > 60% - TMATMUL 占比 < 30%
原因: - Tile 太小,数据复用不足 - 频繁的 GM ↔ L1 搬运 - 未使用流水线重叠
解决方案:
✅ 增大 Tile 尺寸
// 优化前:小 Tile
using TileT = Tile<TileType::Vec, float, 8, 64>; // 2KB
// 优化后:大 Tile
using TileT = Tile<TileType::Vec, float, 16, 256>; // 16KB
✅ 提升数据复用
// GEMM:K 维度分块
for (int k = 0; k < K; k += TILE_K) {
TLOAD(tileA, ...); // 加载一次
TLOAD(tileB, ...); // 加载一次
TMATMUL(acc, tileA, tileB); // 复用多次
}
✅ 使用双缓冲
// 预加载
TLOAD(tile[0], ...);
for (int i = 0; i < N; i++) {
int curr = i % 2;
int next = (i + 1) % 2;
// 计算当前
TCOMPUTE(result[curr], tile[curr]);
// 同时加载下一个
if (i + 1 < N) {
TLOAD(tile[next], ...);
}
}
3.2 计算单元利用率低¶
症状: - TMATMUL 占比 < 40% - 大量流水线气泡
原因: - 数据搬运跟不上计算速度 - 同步过于频繁 - Tile 形状不匹配硬件
解决方案:
✅ 优化流水线重叠
// 使用事件而非全局同步
Event<Op::TLOAD, Op::TMATMUL> e;
e = TLOAD(tile, ...);
TMATMUL(acc, tile, ..., e); // 只等待 TLOAD
✅ 调整 Tile 形状
// A2/A3 推荐:
// Left: 128×64, Right: 64×256, Acc: 128×256
// A5 推荐:
// Left: 256×128, Right: 128×512, Acc: 256×512
3.3 布局转换开销大¶
症状: - TEXTRACT/TMOV 占比 > 20% - TMATMUL 占比正常但总性能差
原因: - 频繁的布局转换 - 输入输出布局不匹配
解决方案:
✅ 选择合适的输入布局
// 如果输入是 ND,直接使用 ND
using GT = GlobalTensor<float, Shape<...>, Stride<...>, Layout::ND>;
// 避免不必要的 NZ ↔ ND 转换
✅ 合并转换操作
// 不好:多次转换
TMOV(temp1, src);
TTRANS(temp2, temp1);
TMOV(dst, temp2);
// 好:一次转换
TTRANS(dst, src); // 如果支持直接转置
3.4 核间负载不均衡¶
症状: - 部分核心利用率高,部分低 - 总执行时间由最慢的核决定
原因: - 数据划分不均匀 - 边界处理逻辑复杂
解决方案:
✅ 均匀划分数据
// 计算每个核的工作量
int total_work = M * N;
int num_cores = get_block_num();
int work_per_core = (total_work + num_cores - 1) / num_cores;
// 确保每个核的工作量相近
int block_idx = get_block_idx();
int work_start = block_idx * work_per_core;
int work_end = min(work_start + work_per_core, total_work);
✅ 简化边界处理
// 使用 padding 避免特殊处理
int padded_M = (M + TILE_M - 1) / TILE_M * TILE_M;
int padded_N = (N + TILE_N - 1) / TILE_N * TILE_N;
4. 优化技巧清单¶
4.1 Tiling 优化¶
✅ 选择合适的 Tile 大小 - 平衡片上容量和数据复用 - A2/A3:单个 Tile 通常 2-32 KB - A5:单个 Tile 可以更大(4-64 KB)
✅ 多级 Tiling
// 全局 → 核级 → 块级
// M×K×N → singleCoreM×singleCoreK×singleCoreN → baseM×baseK×baseN
✅ 考虑硬件对齐要求 - 行主序:Cols × sizeof(T) 对齐到 32 字节 - 列主序:Rows × sizeof(T) 对齐到 32 字节 - NZ 布局:特殊的分形对齐要求
4.2 内存访问优化¶
✅ 连续访问
// 好:连续访问
for (int i = 0; i < M; i++) {
TLOAD(tile, A[i, :]); // 行连续
}
// 不好:跨步访问
for (int i = 0; i < M; i++) {
TLOAD(tile, A[:, i]); // 列访问,可能不连续
}
✅ 数据预取
// 提前加载下一批数据
TPREFETCH(next_data, ...);
✅ 减少 GM 访问次数
// 在 L1 中缓存频繁访问的数据
TLOAD(cached_tile, ...); // 加载一次
for (int i = 0; i < N; i++) {
TCOMPUTE(result, cached_tile, ...); // 复用多次
}
4.3 计算优化¶
✅ 使用合适的数据类型
// fp16 计算更快,但精度较低
// fp32 精度高,但速度较慢
// 根据需求选择
// 混合精度:输入 fp16,累加 fp32
using TileLeft = TileLeft<half, 128, 64>;
using TileAcc = TileAcc<float, 128, 256>;
✅ 向量化操作
// 使用 Tile 操作而非标量循环
TADD(c, a, b); // 并行处理所有元素
// 避免:
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
c[i][j] = a[i][j] + b[i][j]; // 串行
}
}
✅ 算子融合
// 融合多个操作减少中间结果存储
// 例如:Softmax = exp(x - max) / sum(exp(x - max))
// 可以融合为一个 kernel
4.4 同步优化¶
✅ 使用细粒度事件
// 好:只等待必要的依赖
Event<Op::TLOAD, Op::TADD> e;
e = TLOAD(tile, ...);
TADD(result, tile, ..., e);
// 不好:全局同步
TLOAD(tile, ...);
TSYNC<Op::TLOAD>(); // 等待所有 TLOAD
TADD(result, tile, ...);
✅ 避免稳态循环中的 drain
// 不好:每次迭代都 drain
for (int i = 0; i < N; i++) {
TLOAD(tile, ...);
TCOMPUTE(result, tile);
TSYNC(); // 等待所有操作完成
}
// 好:只在循环外 drain
for (int i = 0; i < N; i++) {
TLOAD(tile, ...);
TCOMPUTE(result, tile);
}
TSYNC(); // 只在最后同步一次
4.5 调试优化¶
✅ 保留正确性检查
#ifdef DEBUG
// 验证中间结果
float max_diff = CheckError(result, expected);
assert(max_diff < 1e-5);
#endif
✅ 逐步优化 - 每次只改一个优化点 - 优化后立即验证正确性和性能 - 记录每次优化的效果
✅ 性能回归测试
# 建立性能基线
./benchmark --baseline > baseline.txt
# 优化后对比
./benchmark --compare baseline.txt
5. 平台特定优化¶
5.1 A2/A3 优化要点¶
硬件特点: - 24 核 - L1 容量:~512 KB/核 - Cube 峰值:~50 TFLOPS/核(fp16)
推荐配置:
// GEMM Tile 大小
constexpr int baseM = 128;
constexpr int baseK = 64;
constexpr int baseN = 256;
// 分形大小
constexpr int fractalABSize = 512; // A/B 操作数
constexpr int fractalCSize = 1024; // 累加器
优化重点: - 优先优化 K 维度的数据复用 - 使用双缓冲重叠 TLOAD 和 TMATMUL - 注意 L1 容量限制
5.2 A5 优化要点¶
硬件特点: - 更多核心 - 更大的 L1 容量:~1 MB/核 - 更高的 Cube 峰值
推荐配置:
// GEMM Tile 大小(可以更大)
constexpr int baseM = 256;
constexpr int baseK = 128;
constexpr int baseN = 512;
优化重点: - 利用更大的 L1 容量增大 Tile - 更激进的流水线优化 - 考虑使用 MXFP4/MXFP8 混合精度
5.3 CPU 仿真优化¶
注意事项: - CPU 仿真主要用于验证正确性 - 性能特征与 NPU 不同 - 不要基于 CPU 性能做优化决策
建议:
#ifdef __CPU_SIM
// CPU 仿真:使用小 Tile 加快验证
constexpr int TILE_SIZE = 16;
#else
// NPU:使用大 Tile 优化性能
constexpr int TILE_SIZE = 256;
#endif
6. 性能优化案例¶
6.1 GEMM 优化历程¶
初始版本: - 性能:100 TFLOPS - TLOAD 占比:80% - TMATMUL 占比:15%
优化 1:增大 Tile 尺寸 - 性能:180 TFLOPS(+80%) - TLOAD 占比:65% - TMATMUL 占比:30%
优化 2:双缓冲 - 性能:320 TFLOPS(+78%) - TLOAD 占比:45% - TMATMUL 占比:50%
优化 3:K 维度分块优化 - 性能:420 TFLOPS(+31%) - TLOAD 占比:40% - TMATMUL 占比:55%
最终性能:420 TFLOPS(初始版本的 4.2×)
详细分析:GEMM 性能优化
6.2 Flash Attention 优化¶
关键优化点: - 动态 Tile 大小选择(128 vs 256) - 多阶段流水线重叠 - 在线 softmax 算法
详细实现:Flash Attention 优化
7. 性能优化检查清单¶
开始优化前¶
- [ ] 正确性已验证(CPU + NPU)
- [ ] 建立了性能基线
- [ ] 采集了 profiler 数据
- [ ] 识别了性能瓶颈
Tiling 优化¶
- [ ] Tile 大小合理(不超片上容量)
- [ ] 考虑了硬件对齐要求
- [ ] 数据复用充分
内存优化¶
- [ ] 使用了双缓冲或多缓冲
- [ ] 内存访问连续
- [ ] 减少了 GM 访问次数
计算优化¶
- [ ] 选择了合适的数据类型
- [ ] 使用了向量化操作
- [ ] 考虑了算子融合
并行优化¶
- [ ] 多核负载均衡
- [ ] 流水线充分重叠
- [ ] 同步开销最小化
验证¶
- [ ] 性能提升已量化
- [ ] 正确性保持
- [ ] 建立了性能回归测试
8. 常见误区¶
❌ 过早优化 - 在验证正确性前就开始优化 - 没有 profiler 数据就盲目优化
❌ 过度优化 - 为了 1% 的性能提升牺牲可读性 - 优化不是瓶颈的部分
❌ 忽略正确性 - 优化后不验证数值结果 - 没有回归测试
❌ 平台特定优化 - 针对单一平台过度优化 - 牺牲跨平台兼容性