skills/arm-cpu-optimize/step4-asm.md
目标:把已经用 C++ 验证过的热点 kernel,迁移为 NEON/SDOT/I8MM/SME2 汇编实现。
前置条件:步骤 0-3 已完成,已经知道热点路径、输入 layout、pack mode、正确性 oracle 和性能 baseline。
原则:不要直接写汇编。先标量、再 C++ SIMD 模拟、再寄存器计划、最后写
.S。
只有同时满足下面条件,才进入 asm:
如果这些条件不满足,先回步骤 0-3。
写一个最直接、最容易读懂的参考实现:
对比建议:
| 对比点 | 目的 |
|---|---|
| unpack 后的 int 值 | 确认 bit layout |
| dot accumulator | 确认 sdot/smmla 数学分组 |
| dequant 后 FP32 | 确认 scale/zp |
| postprocess 后 dst | 确认 bias/minmax/add-dst |
在 C++ 中模拟目标指令和寄存器,而不是直接写 .S:
std::array<int8_t, 16>、std::array<int32_t, 4> 或局部数组模拟 NEON lanes。sdot、smmla、smlal、zip/uzp/ext/tbl 等关键行为。vAcc0、vW0、vAux、vMin、vMax。低 bit 示例要点:
// 伪代码:表达意图,不要求直接复制
std::array<int8_t, 16> vW0 = unpackW2Plane0(bytes);
std::array<int8_t, 16> vW1 = unpackW2Plane1(bytes);
std::array<int32_t, 4> vAcc = sdot4(vAcc, vInput, combine(vW0, vW1));
这一步的价值是把“数学/pack 错”和“汇编寄存器/ABI 错”分开。
写 asm 前必须先列寄存器表。建议直接放在 .S 对应 macro 附近的注释中。
| 寄存器 | 用途 | live 范围 | 可复用条件 | 风险 |
|---|---|---|---|---|
x0-x7 | ABI 参数 | 函数入口到重映射 | 可复制到 temp | 参数被覆盖后难恢复 |
x8-x18 | 临时指针/loop | 当前 loop | loop 结束后 | tail 分支跳转 |
x19-x28 | 长生命周期指针 | 整个函数 | 保存/恢复后 | callee-saved |
v0-v7 | input/unpack tmp | 当前 K step | dot 后 | 被 postprocess 误用 |
v8-v15 | 长生命周期值 | 整个函数 | 保存低 64 位 | callee-saved |
v16-v25 | accumulator | compute 到 store | store 后 | unroll 增大压力 |
v26-v31 | scale/min/max/constants/tmp | postprocess 前后 | 明确分支后 | 最容易被 clobber |
必须显式回答:
source/backend/cpu/arm/arm64/MNNXxxKernel.S
source/backend/cpu/arm/arm64/MNNXxxKernelFP16.S
source/backend/cpu/arm/arm64/MNNXxxKernel_int8.S
source/backend/cpu/arm/arm64/sme2_asm/MNNXxxKernel_SME2.S
MNNAsmGlobal.h 和 asm_function 风格。x0-x7 是参数,x8-x18 caller-saved,x19-x28 callee-saved。v0-v7 caller-saved,v8-v15 的低 64 位 callee-saved,v16-v31 caller-saved。.arch armv8.2-a+dotprod、.arch armv8.6-a+i8mm 或 SME2 特性时,跟随仓库已有 asm 文件写法。#ifdef __aarch64__
#include "MNNAsmGlobal.h"
.text
.align 5
// void MNNXxxKernel(...)
asm_function MNNXxxKernel
// Register plan:
// x0: dst
// x1: src / packed weight
// x2: params
// v16-v19: accumulators
// v30-v31: fp32 min/max, only live in postprocess
L_loop:
// load
// unpack / compute
// postprocess
// store
ret
#endif
weightPtr 偏移必须使用真实 packed cell 字节数。tId>0 的 OC chunk。w2/w3 的有效带宽低,常见不是 DRAM 慢,而是 unpack 太重:
tbl/ext/ushr/shl/and 导致 issue bound。ld1r {.2d},避免 ld1 {.8b} + mov d[1]。sdot 每 4 byte 形成一个 int32 lane,C++ 模拟必须和这个分组一致。smmla 的 2x8 by 8x2 分组容易因为 B layout 错位而“看起来能跑但质量差”。在 C++ 中声明并注册 asm symbol 时,同时检查 pack mode:
extern "C" {
void MNNXxxKernel(...);
}
检查清单:
supportI8mm、supportSDot、supportSME2 的优先级是否符合预期。UNIT/SRC_UNIT/DST_XUNIT,packer 和 kernel 必须一起切换。cmake --build build -j 8
cd build
./run_test.out op/lowMemory/blockConv 0 1 4
./run_test.out op/lowMemory/HybridConv 0 1 4
./run_test.out op/lowMemory/blockConv 0 1 4 # 在 SDOT 目标设备/构建配置上复跑
./run_test.out op/lowMemory/HybridConv 0 1 4 # 在 SDOT 目标设备/构建配置上复跑
根据算子替换为更精确的 op test;低 bit LLM kernel 至少保留上面两类。
./llm_demo /path/to/w3/config.json prompt.txt 64 1
./llm_demo /path/to/w3/config.json prompt.txt 64 1 # 在 SDOT 目标设备/构建配置上复跑
./llm_demo /path/to/w2/config.json prompt.txt 64 1
./llm_demo /path/to/w2/config.json prompt.txt 64 1 # 在 SDOT 目标设备/构建配置上复跑
如果输出重复或异常:
./run_test.out speed/GemvBW 0 2
报告至少包含:
us/iter%peak| 现象 | 处理 |
|---|---|
| C++ SIMD 模拟和标量 oracle 不一致 | 先修 pack/unpack 数学,不写 asm |
| asm op test 过但 LLM 乱码 | 查 E=1、block64、fp32 min/max、multi-block 和 postprocess |
| i8mm 正常但 sdot 错,或反过来 | 分开检查各自 packer、kernel 注册和 runtime flag |
| 性能低于预期 | 看 unpack 指令数、寄存器压力、load/store、postprocess,不先扩大 packed bytes |
| 新 unroll 导致质量变差 | 回退到上一个正确 unroll,重新做 live range 表 |