Kernel 性能分析
【免费下载链接】MNNMNN: A blazing-fast, lightweight inference engine battle-tested by Alibaba, powering high-performance on-device LLMs and Edge AI.项目地址: https://gitcode.com/GitHub_Trending/mn/MNN
Kernel 名称: xxx_kernel代码位置: source/backend/opencl/execution/cl/xxx.cl性能数据: 占总时间 xx%,绝对耗时 xx us
计算强度: xx FLOPs/Byte(参考 optimization-handbook.md §1.1-1.2 中的 ridge point 数据)瓶颈类型: memory-bound / compute-bound / 均衡BW 利用率(memory-bound 时): xx%
代码级分析:
内存访问模式
- Global memory 读写次数
- 是否有重复读取同一数据?
- 访问是否连续(合并访问)?
- 是否可以用 local memory 缓存?
计算特征
- 寄存器使用: 是否有大量私有数组?
- 循环嵌套: 是否有多层循环?
- 数据依赖: 循环间是否有依赖?
- 并行度: 当前并行粒度是否合理?
Work-Group 组织
- 当前 GWS/LWS 配置
- 每个 work-item 的工作量
- 是否充分利用 GPU 并行能力?
### 2.3 定量分析方法论(roofline 模型) **计算强度(Arithmetic Intensity)** 是 roofline 模型的核心指标,判断 kernel 是 compute-bound 还是 memory-bound:AI = FLOPs / Bytes_transferred ridge_point = peak_FLOPS / peak_BW
- `AI < ridge_point` → **memory-bound**:性能受内存带宽限制 - `AI > ridge_point` → **compute-bound**:性能受计算能力限制 典型 kernel 的计算强度参考(详见 [optimization-handbook.md](https://link.gitcode.com/i/e441a6fc5ed2ae3b732d2fca49d571cd) §1.1): | Kernel 类型 | 典型 AI (FLOPs/Byte) | 通常瓶颈 | |------|------|------| | GEMV (decode, batch=1) | 0.5-2 (int4) | memory-bound | | GEMM (prefill, batch>1) | 随 batch 增长 | 小 batch memory,大 batch compute | | Elementwise / Raster | ≈ 0 | memory-bound | | Attention (长序列) | 中等 | 取决于 seq_len | 设备参考数据(§1.2,注意 LPDDR 理论带宽需打 6-8 折作为 GPU 单进程实测可达值): | 设备 | peak BW (实测) | peak FLOPS (FP16) | ridge point | |------|------|------|------| | Snapdragon 8 Gen3 | ~50 GB/s | ~4.5 TFLOPS | ~90 | | Snapdragon 8 Elite | ~55 GB/s | ~5.7 TFLOPS | ~104 | | Mali-G715 | ~35 GB/s | ~3.0 TFLOPS | ~86 | **按瓶颈类型选杠杆**:用 AI 与 ridge point 比较,判断 memory-bound / compute-bound / 均衡,再选方向——判断表以 optimization-handbook.md §1.3 为准(唯一来源)。memory-bound 时进一步算 BW 利用率:`BW_utilization = actual_bytes / kernel_time / peak_BW`;利用率 <50% 说明访存模式或 launch 开销有问题,50-70% 可调 WG 大小/加宽 tile,>70% 已接近带宽上限应减小数据量。 > **警惕 register/occupancy 墙**(§1.5):若 AI 说 memory-bound 但减流量/减 ALU 都不涨,甚至减 ALU 反降——大概率是每个 work-item 的寄存器数决定的 occupancy 墙(mobile GPU 的 L2/纹理缓存已把权重流量吸收)。此时应走技巧 8(tile 甜点扫描)或技巧 4(workgroup 重写),并避免技巧 3 的 block 外提(会引入活跃累加器)。注意精度模式决定寄存器账:LLM `precision:"low"` 是 fp16 compute(`COMPUTE_FLOAT=half`),累加器是 half(2 个/寄存器);normal/high 是 fp32 累加,同样 tile 翻倍占用。 --- ## 3. 优化策略选择(步骤 1.1) ### 3.1 按症状导航的优化决策树 下面的决策树是按**症状**导航的路径,与 handbook §1.3 的判断表(按瓶颈类型)互补:分析 kernel 瓶颈 → ├─ Global memory 访问频繁? │ ├─ 数据被多次读取? → 用 Local Memory 缓存 │ ├─ 访问不连续? → 调整数据排布或访问模式 │ └─ 写入频繁? → 用私有变量累积,最后写入 │ ├─ 寄存器压力大(私有数组多)? │ ├─ 中间结果可以不存储? → 消除中间数组 │ ├─ 可以用 local memory 替代? → 改用 local 数组 │ └─ 可以减少工作量? → 调整 work-group 组织 │ ├─ 循环有数据依赖无法并行? │ ├─ 可以拆解为独立部分? → 拆分为多个 kernel │ ├─ 可以用并行 reduce? → 实现并行归约算法 │ └─ 依赖无法消除? → 保持串行,优化其他部分 │ ├─ 并行度不足? │ ├─ 可以增加并行维度? → 使用 2D/3D work-group │ ├─ 每个 work-item 工作量太小? → 增加每个 work-item 的工作量 │ └─ 每个 work-item 工作量太大? → 减少工作量,增加 work-item 数 │ └─ 计算密集但访问连续? ├─ 可以向量化? → 使用 float4/float8 └─ 已经很优化? → 考虑算法级优化
### 3.2 技巧目录与跨切经验 具体技巧的完整定义(适用场景 / 做法 / 注意事项 / 收益 / 实战案例)一律以 [optimization-handbook.md](https://link.gitcode.com/i/e441a6fc5ed2ae3b732d2fca49d571cd) 为准:§2 是 kernel/访存级技巧目录(共 13 条),§5 是按难度和收益排序的速查表(含「针对瓶颈」列),§6 是待验证的候选手段。**改任何技巧前先读对应条目**,此处不重复复制。 两条关键的**跨切经验**(详见 handbook): - **组合多种技术效果远超单一**:LinearAttention 组合 local memory + 并行 reduce + 向量化 + 2D workgroup 重写,加速达 **8x → 112x**(decode 263x、prefill 60x,提交 `6a361d3e4`)。 - **GEMV 与 GEMM 最优策略不同**:GEMV 是 BW bound,每个 weight 只用一次,原生 packed kernel 更优;GEMM(prefill)有数据复用,weight 被多行复用,反量化开销可摊薄,可先反量化再走通用 GEMM。 > handbook 只收 kernel/访存级技巧;host/init 级优化(零拷贝、mmap、kernel 预编译、启发式 tune 等)不在其范围。 --- ## 4. 实施优化(步骤 1.2) ### 4.1 每次优化的标准流程- 记录当前性能数据
- 实施单个优化(只改一个优化点)
- 更新 cpp 调用代码(xxxExecution.cpp,参数传递和 GWS/LWS)
- 更新 kernel 映射(参考 SKILL.md ".cl 修改流程"): cd source/backend/opencl/execution/cl && python3 opencl_codegen.py . .
- 编译 → 推到真机 → 正确性验证 → 性能测试
- 评估结果:提升→保留 / 下降→回退 / 正确性失败→修复
> **代码写法约定**:kernel 内一律用 `FLOAT4/COMPUTE_FLOAT8/CONVERT_FLOAT4` 等宏而非裸 `float4`(精度模式兼容,见 handbook 陷阱 I)。 ### 4.2 改 `.cl` 必跑 codegen(最常见的"改了不生效"根因) MNN 的 OpenCL kernel 采用 `.cl` 源 + codegen 双轨机制:**`.cl` 源不会被构建系统直接编译,运行时读取的是 `*_mnn_cl.cpp` 嵌入字符串**。每次改 `.cl` 后必须运行: ```bash cd source/backend/opencl/execution/cl && python3 opencl_codegen.py . .codegen 脚本(opencl_codegen.py)会将每个.cl文件的内容转成const char*字符串,注册进OpenCLProgramMap,并生成对应的 MD5 摘要用于运行时缓存校验。验证嵌入是否成功:
grep -c '<新宏名>' <my_kernel>_mnn_cl.cpp # > 0 才算进 strings build/.../libMNN.so | grep '<新宏名>' # build 后再确认注意两点:
- codegen 会重新生成所有
*_mnn_cl.cpp,git diff 看到无关文件也变属正常,正常提交即可。 - 新加
QUANT_BIT==N分支时通常每个 shader 有 4 处都要加(WGS>=8 主循环 / WGS>=8 leaves / WGS<8 主循环 / WGS<8 leaves)。改完用grep -n "QUANT_BIT == 4" <file>.cl数一下,确认== 2也有同样的数量。实际源码中conv_2d_int_buf.cl等文件确实存在大量#if QUANT_BIT == N分支(见 conv_2d_int_buf.cl)。
4.3 实施前先做单文件编译检查
改完.cl先做单文件编译检查再上设备。.cl整体编译,任一 kernel 编译失败会让该文件所有 kernel 都拿不到,运行时表现是无声段错误,极易误判成自己索引写错:
make -j10 OpenCLProgramBuildTest.out # project/android/build_64 adb push <build>/OpenCLProgramBuildTest.out <build>/libMNN.so /data/local/tmp/X/ adb push source/backend/opencl/execution/cl/<name>.cl /data/local/tmp/X/kernel.cl # 宏别手写:直接抄运行时那一整行(precision=low 见 OpenCLRuntime.cpp:508,high 见 510) adb shell "cd /data/local/tmp/X && printf -- '%s\n' \"\$OPTS\" > option.txt && \ LD_LIBRARY_PATH=.:/data/local/tmp/MNN ./OpenCLProgramBuildTest.out kernel.cl"这个检查 1 分钟出结果,比在设备上撞段错误快得多。
5. 性能验证(步骤 1.3)
5.1 正确性验证
每次优化后都要验证。MNN 的测试框架提供三层 oracle(详见 SKILL.md「正确性验证」),从近到远:
| 层级 | oracle | 检验点 |
|---|---|---|
| 数值层 | CPU 跑同一 op,dump tensor | 单 op fp16 误差 < 1e-2 |
| op 层 | MNNV2Basic.out单层 conv | 输出与 CPU 对齐 |
| 端到端 | 跑模型 | 文本/输出语义合理 |
数值偏差容忍标准:
| 路径 | 容忍误差 |
|---|---|
| fp32 vs fp32 | abs < 1e-5, rel < 1e-4 |
| fp16 vs fp16 | abs < 1e-2, rel < 5e-3 |
| 量化 dequant + fp16 | abs < 1e-1, rel < 1e-2 |
op 层与 speed 测试用同一套run_test.out二进制(对应 test/op/ 下的MatMulTest.cpp、AttentionTest.cpp、LinearAttentionTest.cpp等用例,它们都通过MNNTestSuiteRegister注册):
# 正确性验证 adb shell "cd /data/local/tmp/MNN && ./run_test.out op/XxxTest 3 1 68" # 性能对比 adb shell "cd /data/local/tmp/MNN && ./run_test.out speed/XxxSpeed 3 1 68"参数含义:
3= OpenCL 后端;precision1=fp32 /2=fp16;numThread 中的+64强制 buffer 模式、+0为 auto(Adreno 默认 image,Mali 默认 buffer),68 = 64+4用于 LLM 全链路 buffer。强制 buffer 只适用于 LLM;优化非 LLM 算子时按其生产模式测(Adreno 上常为 image,用4),否则会落到没被调度的 kernel 路径——buffer/image 是两套 Execution(见 SKILL.md 核心原则 2、9)。
5.2 记录优化结果
## 优化尝试X: [优化名称] **优化方案**: 简要描述 **修改文件**: xxx.cl, xxxExecution.cpp | 场景 | 优化前(us) | 优化后(us) | 加速比 | 状态 | |------|-----------|-----------|--------|------| | decode_H4_d64 | 2,028 | 247 | **8.2x** | OK | **关键发现**: ... **决策**: 保留 / 回退 / 继续改进【免费下载链接】MNNMNN: A blazing-fast, lightweight inference engine battle-tested by Alibaba, powering high-performance on-device LLMs and Edge AI.项目地址: https://gitcode.com/GitHub_Trending/mn/MNN
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考