**一句话导读:**ARMv8.2 dotprod(vdotq_s32)单指令四点积:对照标量 C 与拓宽 mla 的板端微基准,讲清微基准会骗人------vdot 的收益要在 8x8/4x4 大内核的寄存器压力下兑现。
**关键词:**ARMv8.2、dotprod、vdotq、NEON、RK3588
上一篇 5-3 用对拍证明了"算得对"。可标量 C 路径每秒只能挤几十 GFLOPS------今天打开第一把加速钥匙:ARMv8.2 的 dotprod 指令扩展,vdotq_s32。
1. 知识点:NEON 向量化为什么还不够
NEON 是 SIMD:一条指令处理 128 位数据。但"乘加"(multiply-accumulate)有它自己的瓶颈------以 Q8 点积为例,两个 int8x16(16 个 int8)求内积,naive 的做法是:
- 把 int8 拓宽(widen)成 int16(
vmovl),因为 8bit×8bit 的积要 16bit 才放得下; - 拓宽后的 16bit 两两相乘累加(
vmull/vmlal)出 int32。
麻烦在于拓宽 :16 个 int8 拓宽成 16 个 int16 要占两倍寄存器,乘加指令数也跟着涨。ARMv8.2 引入的 dotprod 解决了这个痛点:它让硬件在 128 位内直接做 4 组"4 个 int8×4 个 int8 求和" ,一次给出 4 个 int32 部分和------不需要拓宽、一条指令完成本来要好几条指令的活。vdotq_s32(int32x4_t acc, int8x16_t a, int8x16_t b) 就是"一次点积 4 份",vdotq_laneq_s32 变体还允许从另一个向量取 lane 复用(4x4 内核靠它一次算 4 行,6-3 细讲)。
引擎里就留着这种对照(vllm_safetensors.c 第 3656--3668 行):
c
/* 16 int8 x 16 int8 -> int32x4 (4 lanes, each = 4 products). */
static inline int32x4_t i8x16_dot_s32(int8x16_t a, int8x16_t b) {
#if ST_NEON_DOTPROD
return vdotq_s32(vdupq_n_s32(0), a, b); /* 1 条指令 */
#else
int16x8_t la = vmovl_s8(...), ha = vmovl_s8(...); /* 拓宽回退:多条指令 */
int32x4_t acc = vmull_s16(...); acc = vmlal_s16(...); ...
#endif
}
同一个函数、两种实现:编译器有 dotprod 就用 1 条指令,没有就退成拓宽版(这正是 Day 2 翻车实验里 __ARM_FEATURE_DOTPROD 缺失时报错的代码)。dotprod 不是新代码路径,是同一路径的加速形态。
2. 对应代码:它在哪被用起来
dotprod 不是单点玩具,它撑起了引擎的量化矩阵内核:gemm_q8_0_8x8_neon(vllm_safetensors.c 第 4418 行起)以及 llama.cpp 派生的 4x4 asm 内核(第 3895 行注释:".inst sdot、16 累加器、软件流水")。这些内核里 vdot 一出手就是"8 行 × 8 列的权重块同时喂给点积单元"------单条指令的收益要在这个规模上才真正兑现。
3. 改动后果:把 dotprod 换成标量 C,看慢多少
我们用同一个 int8 点积任务(16×16,跑 4 百万次)对比三种实现,板端实测(RK3588 / gcc 11.4 / -march=armv8.2-a+dotprod / 2026-09):
text
scalar C loop : 72.641 ms (18.16 ns/dot)
widen mla : 9.598 ms ( 2.40 ns/dot)
dotprod vdot : 9.595 ms ( 2.40 ns/dot)
speedups: vdot/scalar=7.57x vdot/widen=1.00x
两个结论,一个惊喜一个"反直觉":
- vdot 比朴素 C 快 7.6 倍------这就是向量化 + dotprod 的价值下限;
- vdot 与"拓宽 mla"在这个玩具循环里打成平手 (都 2.40 ns/dot)。为什么?因为循环体太小、寄存器不缺,瓶颈在循环分发而不是指令数------微基准会骗人。vdot 的真正收益在 6-2/6-3 那种"几十个累加器挤在 32 个 NEON 寄存器里"的大内核中体现:指令少一半意味着寄存器更松、软件流水更顺,引擎级实测(6-2)里同一条 FPN 内核能差到 1.7×。
教学提醒:看到微基准先问它是否触及了真实瓶颈。单条指令的优越性,要在让它"被挤"的场景里才显形。
4. 学员调试任务
- A 档(板端动手):复刻第 3 节微基准(scalar / widen / vdot 三段计时),把你的板子上的三个数字与加速比记下来;再把循环里的点积换成 16 组连续累加(模拟大内核的寄存器压力),观察 vdot 是否开始拉开差距。
- B 档(纯读源码) :读
i8x16_dot_s32(第 3656--3668 行),数一数 widen 分支用了多少条指令、dotprod 分支几条;再在gemm_q8_0_8x8_neon(4418 行起)里找一个 vdot 调用点,说出它一次算几行几列。
预期输出:你能解释"为什么 int8 内积要先拓宽""dotprod 一次做几个点积",并懂得"微基准的平手不代表真实内核的平手"。
收尾
- 本篇源码点名 :vllm_safetensors.c(
i8x16_dot_s32第 3656 行、gemm_q8_0_8x8_neon第 4418 行)。 - 开源仓库 :Kestrel-LLM (Gitee)(源码可得双许可:学习 / 学术研究免费)
- 下篇预告:指令会用了,可它"吃"的数据长什么样?GEMM 内核快不快,一半在权重怎么排队。下一篇 6-2 讲 8x8 布局重排------把取数从运行期提前到转换期。
关键词 :NEON、SIMD、dotprod、ARMv8.2、矩阵乘法