34 · 两行 GEMV 共享激活,只换来 0.6%:一个反向证明 34 · 两行 GEMV 共享激活只换来 0.6%一个反向证明本篇对应源码main/pie_dotprod.S·main/kmcu.c目标单核 GEMV 只跑到 PSRAM 上限的 66–78%怀疑是「每行重装激活」的开销。结论省掉 12.5% 的指令端到端只快 0.6%——这不是指令受限是字节带宽受限。关键词PIE 汇编 · 两行 GEMV · 带宽受限封面省 12.5% 指令只快 0.6%一、结论先行阶段单行内核两行内核ffn25.5 ms25.1 msqkv18.6 ms18.4 mso10.0 ms9.8 msple9.6 ms9.6 mstotal19 token 均值159.9 / 160.0 ms159.0 / 159.0 ms位级20 token 序列完全一致sd fnv 817147c7、kv fnv 739dd3ef两侧相同。二、动机每块 64 B 激活 vs 32 B 权重单行内核处理一个 32 元素块时要做的事装 4 个 128-bit 的xq32 个 int16 64 B装 2 个vldext的权重32 个 int8 32 B4 条vmulas 1 条srs 约 5 条标量激活的字节数是权重的两倍。对 gate/up 这种「同一份激活、几十行权重」的形状看起来明显浪费。三、内核一次装载两条点积链pie_gemv_row2: /* a0qa a1qb a2x a3da a4db a5nblk a6ya a7yb */ esp.vld.128.ip q2, a2, 16 /* 激活块 32 个 int16两行共用 */ esp.vld.128.ip q3, a2, 16 esp.vld.128.ip q4, a2, 16 esp.vld.128.ip q5, a2, 16 /* 行 A */ esp.zero.xacc esp.vldext.s8.ip q0, q1, a0, 16 esp.vmulas.s16.xacc q0, q2 esp.vmulas.s16.xacc q1, q3 esp.vldext.s8.ip q0, q1, a0, 16 esp.vmulas.s16.xacc q0, q4 esp.vmulas.s16.xacc q1, q5 esp.srs.s.xacc t3, t4 /* 行 B 同样一遍只是权重指针换成 a1 */PIE 只有一个xacc累加器所以「共享激活」只能在寄存器层做先把 32 个激活装进 q2–q5然后两行依次把权重流过去。每块从 32 条指令降到 28 条。两个硬件约束值得记下来踩过一次srs/vldext的 GPR 只能编码 x8–x15 / x24–x31所以右移量放t4(x25)、结果暂存用t3(x24)/t5(x26)指针留在 a0–a5esp.vldext.s8.ip一次只能出 2 个寄存器16 个 int8 符号扩展成两个8×int16所以 32 个 int8 要两条。objdump复核函数体 0x1a0–0x22d没有栈帧、没有寄存器溢出。四、C 侧成对调用奇数回退intr0;for(;r1n_rows;r2){pie_gemv_row2(qr*align,q(r1)*align,xq,dr*nblk,d(r1)*nblk,nblk,y[r],y[r1]);y[r]*coeff;y[r1]*coeff;}for(;rn_rows;r)y[r]pie_gemv_row(qr*align,xq,dr*nblk,nblk,0)*coeff;行数为奇数时最后一行回退单行内核所以任何形状都能用。五、为什么只快 0.6%这篇的真正价值每块指令数32 → 28−12.5%端到端159.9 → 159.0 ms−0.6%两者差了一个数量级只能说明一件事这些阶段的时间不是被指令吞吐决定的而是被从 PSRAM 搬权重决定的。逐阶段算有效带宽单核上限 121 MB/s阶段字节量耗时有效带宽占上限ffn2.4 MB25.5 ms94 MB/s78%qkv1.62 MB18.6 ms87 MB/s72%o0.8 MB10.0 ms80 MB/s66%省指令不会让 PSRAM 变快所以两行内核只能把「剩下的那点非带宽开销」挤掉一点。顺带得到一个可用的预算式每 token 必须过 15.4 MBhead 10.24 ffn 2.4 qkv 1.6 o 0.8 其余按双核 157 MB/s 是 98 ms加 PLE 行读 14.4 ms理论上限约 113–117 ms。当前 159–170 ms剩余空间全在「让这几段也吃满双核」。六、诚实边界0.9 ms 的收益在任何单次测量里都不显眼它是两次开、两次关交替得到的稳定差值159.0/159.0 vs 159.9/160.0不是一次跑出来的。收益虽小但位级一致所以保留默认开启KMCU_GEMV21并留了运行期g键与构建期-DKMCU_GEMV20两条退路。这个内核只对「同一 tensor 的相邻两行」有效输出头本身是带宽打满的用不上它也没有收益。七、一句话这是一次「反向证明」花力气省掉 12.5% 的指令只换来 0.6%反而确认了瓶颈是 PSRAM 字节带宽——从此不必再在 GEMV 指令级上找空间。对应源码文件关键符号 / 位置支撑本文哪部分main/pie_dotprod.Spie_gemv_row2两行共享激活的内核main/kmcu.cgemv_q8_pre()的成对循环C 侧接入与奇数回退main/kmcu.ckm_gemv2_selftest()与单行内核的逐位比较main/main.ccase g运行期逃生舱《Kestrel-MCU 手记》· 全系列 37 篇在 ESP32-P4 上从零训练并部署 64M 参数 MoE 大模型5.8 tok/s源码、权重、训练脚本与全部文章开源可复现。仓库https://gitee.com/pei-xiaoguang/kestrel-llm-mcu · 觉得有用欢迎 Star