本篇属于《Kestrel-MCU 手记》系列(共 28 篇):在 ESP32-P4(RISC-V 双核 400MHz)上从零训练并部署 32M~64M 参数 MoE 大模型,纯标量 C11 推理内核,全部数字实测、代码开源可复现。

同源系列:Kestrel「红隼」边缘 LLM 推理引擎,同一套工程方法论,三个系列共 168 篇。

本系列按 01 → 27 连贯更新,建议关注 + 收藏,按顺序读收获最大。

English version: en/05-pie-assembly-in-practice.md

本篇对应源码:main/pie_dotprod.S · main/main.c · docs/milestones.md

目标:讲清楚 ESP32-P4 的 PIE(Processor Instruction Extension,xespv)到底是什么、
为什么必须手写汇编、以及两个会让人「随机数据对拍正确、真实数据全错」的致命 bug。

在这里插入图片描述

1. 调研:PIE 是什么、不是什么

ESP32-P4 的 CPU 是 RISC-V 双核 400 MHz + 单精度 FPU,另带一个乐鑫私有的
128-bit SIMD 扩展 PIE(xespv)。

用硬数据核实(不是道听途说):

事实依据
编译器认识 xespvgcc -dM -E -march=...xespv 出 __riscv_xespv=2001000
march 已含扩展-march=rv32imafc_zicsr_zifencei_zaamo_zalrsc_xesploop_xespv
工具链有两代xespv2p1 / xespv2p2 汇编器/反汇编器
无公开 intrinsics 头文件无 esp_pie.h/xespv.h,无 builtins
GCC 不自动向量化到 xespv反汇编 kmcu.c.o 4136 条指令全为标准 RV32I/F 标量,零 PIE

调研硬数据(可复现的原始输出)

构建 flags(build/toolchain/cflags,实测):

-mabi=ilp32f
-march=rv32imafc_zicsr_zifencei_zaamo_zalrsc_xesploop_xespv
-mtune=esp-base

gcc -dM -E 关键宏(证明编译器认识 xespv,且是单精度 FPU):

#define __riscv_xespv     2001000
#define __riscv_xesploop  1000000
#define __riscv_flen      32        ← 单精度 FPU(无 fp64)
#define __riscv_xlen      32

反汇编 kmcu.c.o 的指令分布(objdump -d,4136 条指令全标准标量,零 PIE、零标准 RVV):

Count Name          Count Name
  385 lw              54 jalr
  171 sw              54 auipc
  156 addi            35 beqz
  136 mv              34 j
  122 flw             33 fsw
  107 add             26 lui
   75 li              25 bnez
   74 slli            24 bne
   71 fcvt.s.w   ← int8→fp32 转换  24 mul
   71 fmadd.s   ← fp32 FMA       21 ret

关键观察:fcvt.s.w(71 次)+ fmadd.s(71 次)正是热循环里
(float)q[j] * x[j] 的标量实现——int8→fp32 转换 + fp32 FMA。
这证明 -O2 -march=...xespv 下 GCC 一条 PIE 指令都没生成。

工具链里 PIE 相关二进制(Glob 实测):

...\riscv32-esp-elf\bin\objdump-xespv2p1.exe
...\riscv32-esp-elf\bin\objdump-xespv2p2.exe
...\riscv32-esp-elf\bin\as-xespv2p1.exe
...\riscv32-esp-elf\bin\as-xespv2p2.exe

⇒ 两代 PIE:xespv2p1(chip rev v1.x)和 xespv2p2(chip rev v3.x)。本板是 rev v3.1,对应 PIE V2。

决定性结论:PIE 是纯定点 SIMD

乐鑫自己 ESP-DL 里 dl_esp32p4_dotprod_f32 的源码注释:

“The PIE SIMD extension only supports integer datatypes, so the float dot product
is implemented with the RISC-V single-precision FPU.”

即 PIE 没有 fp32 向量指令。所以要加速 int8 权重 × fp32 激活 的 GEMV,
唯一路径是激活定点化(w8a16:int8 权重 × int16 激活)。

注意区分:PIE(xespv,乐鑫私有)≠ 标准 RVV(v 扩展)。工具链里的
riscv_vector.h 对应标准 RVV,而 march 里没有 v,标准 RVV intrinsics 全不可用。

2. 指令来源:移植乐鑫 ESP-DL

乐鑫 ESP-DL 的 dl_esp32p4_dotprod*.S 提供了可编译的 PIE 指令参考。核心点积指令:

指令语义
esp.zero.xacc清零累加器 xacc
esp.vldext.s8.ip q0,q1,addr,16加载 16 个 int8 并符号扩展成 q0,q1(16 个 int16)
esp.vld.128.ip q2,addr,16加载 16 字节(8 个 int16)
esp.vmulas.s16.xacc(.ld.ip) q0,q2xacc += q0×q2(8 个 int16 乘加);.ld.ip 变体同时后增加载
esp.vext.s8 q0,q1,q4把 q4(16 个 int8)符号扩展成 q0,q1
esp.srs.s.xacc t3,a4把 xacc 求和右移 a4 位到标量 t3

128-bit 向量寄存器 q0..q7,标量寄存器用 RISC-V 的 a0..a7/x*。

实测编码(objdump -d pie_dotprod.S.o,供核对指令是否被正确汇编):

esp.zero.xacc                 → 0x0000001b
esp.vldext.s8.ip q0,q1,a0,16  → 0x8821009f
esp.vld.128.ip q2,a1,16       → 0x0311889f
esp.vld.128.ip q3,a1,16       → 0x03118c9f
esp.vmulas.s16.xacc q0,q2     → 0x0aa3a01b
esp.vmulas.s16.xacc q1,q3     → 0x2ea3a01b
esp.srs.s.xacc t3,a4          → 0x9cf3261b

现成的点积格式

函数格式
dl_esp32p4_dotprod_i16k8o16int8 × int16(w8a16,最匹配现有 int8 权重)
dl_esp32p4_dotprod_i16k16o16int16 × int16
dl_esp32p4_dotprod_i8k8o16int8 × int8

3. 分块 GEMV 内核(pie_gemv_row)

本项目写了 pie_dotprod.S,其中 pie_gemv_row 做「分块 GEMV」:

acc = Σ_b d[b] · dot(q[b*32 .. b*32+32], x[b*32 .. b*32+32])

每块 32 元素 = 2 轮 16 元素,用乐鑫的 .ld.ip 流水线:

pie_gemv_row_blk:
    esp.zero.xacc
    esp.vldext.s8.ip q0, q1, a0, 16         /* 轮1: 16 int8 */
    esp.vld.128.ip q2, a1, 16
    esp.vld.128.ip q3, a1, 16
    esp.vmulas.s16.xacc.ld.ip q4, a0, 16, q0, q2   /* 轮1: +=q0*q2; q4=轮2 int8 */
    esp.vmulas.s16.xacc.ld.ip q2, a1, 16, q1, q3
    esp.vext.s8 q0, q1, q4                  /* q4 -> q0,q1 */
    esp.vld.128.ip q3, a1, 16
    esp.vmulas.s16.xacc.ld.ip q4, a0, 16, q0, q2   /* 轮2 */
    esp.vmulas.s16.xacc.ld.ip q2, a1, 16, q1, q3
    addi a0, a0, -16                        /* 回退多加载 */
    addi a1, a1, -16
    esp.srs.s.xacc t3, a4
    fcvt.s.w ft1, t3
    flw ft2, 0(a2)
    fmadd.s ft0, ft1, ft2, ft0              /* fp32 累加 d[b]*dot_b */

C 侧调用(kmcu.c):

extern float pie_gemv_row(const int8_t *q, const int16_t *x, const float *d, int nblk, int shift);

/* 量化激活 x → int16(per-vector scale) */
float S = 32767.0f / max_abs(x);
for (j) xq[j] = (int16_t)(x[j] * S + (x[j] >= 0 ? 0.5f : -0.5f));
for (r) y[r] = pie_gemv_row(q + r*align, xq, d + r*nblk, nblk, 0) * (1/S);

4. 两个致命 bug(务必先读)

Bug A:PIE 指令的标量操作数不能用 t0-t2(x5-x7)

症状:汇编报 illegal operands 'esp.srs.s.xacc t2,t1'、'esp.vld.128.ip q2,t2,16'。

原因:PIE 指令的地址/标量操作数有寄存器编码约束,t0-t2(x5-x7)非法。

解决:用 a0-a7(x10-x17)或 t3+(x28+)。

  • esp.srs.s.xacc t3, a4 ✅(t3=x28 合法)
  • esp.srs.s.xacc t2, t1 ❌(t2=x7, t1=x6 非法)

Bug B:16 字节对齐(最隐蔽)

症状:随机数据对拍 diff=0,真实模型权重全错——single-token argmax 从 684 变成 865,
dot 值越大错得越多。

定位过程(完整数据链):

  1. pie_dotprod_s8s16(点积)match=1 ✅
  2. pie_gemv_row 随机数据 diff=0 ✅
  3. 真实模型 GEMV 出错 ❌——前 3 个 GEMV 的 blk0 对拍:
[PIE-DBG] blk0 scalar_dot=-467146  pie_dot=-40541   d0=0.00287   ← 错
[PIE-DBG] blk0 scalar_dot=-138428  pie_dot=182837   d0=0.00531   ← 错(符号都反了)
[PIE-DBG] blk0 scalar_dot=-6589    pie_dot=-112498  d0=0.00189   ← 错

修复后(16 字节对齐):

[PIE-DBG] blk0 scalar_dot=-467146  pie_dot=-467146  d0=0.00287   ← 一致
[PIE-DBG] blk0 scalar_dot=-138428  pie_dot=-138428  d0=0.00531   ← 一致
[PIE-DBG] blk0 scalar_dot=-6589    pie_dot=-6589    d0=0.00189   ← 一致

关键判据:scalar_q(标量用同一份量化 xq 反量化算)与 scalar(标量用原始 fp32)只差 ~3e-6,
说明量化正确;而 pie(PIE 用 xq)与 scalar_q 差 0.6~0.8,说明是 PIE 计算错了,
不是量化精度不足。这排除了「量化误差」、锁定了「实现 bug」。

原因:esp.vld.128 / vldext.s8 要求 16 字节对齐地址。
scratch 里的 int16 量化缓冲 xq 用 heap_caps_malloc 分配(只保证 4 字节对齐),
导致 vld 读到了错位的数据。随机数据(static aligned(16))恰好对齐,所以没暴露。

解决:用 heap_caps_aligned_alloc(16, size, MALLOC_CAP_8BIT) 分配 scratch。

float *sc = (float *)heap_caps_aligned_alloc(16, sc_f * sizeof(float), MALLOC_CAP_8BIT);

教训:「随机数据对拍通过」不等于「实现正确」。量化值满量程、dot 值大时,
对齐/溢出类 bug 才会暴露。对拍测试要覆盖满量程输入和真实权重两种场景。

5. 单算子验证(阶段 0)

在接入 forward 之前,先用 pie_bench 做单算子对拍 + 测速:

[PIE] dotprod N=4096  scalar=19713  pie=19713  match=1
[PIE] 200 iters: scalar=16396 us  pie=782 us  speedup=20.97x

纯计算加速 21×(数据在内部 RAM,无 PSRAM 带宽墙)。接入 forward 后收敛到 2.11×
——正好落在预估的带宽墙区间(2–4×)。

6. 复现建议

  1. 先跑通 pie_dotprod_s8s16(点积)并确认 match=1;
  2. 再跑 pie_gemv_row 对拍,务必用满量程输入(x ∈ ±32767)验证对齐;
  3. 最后才接入 gemv_q8 和输出头,并用 single-token argmax 作为快速正确性哨兵。

对应源码

文件关键符号 / 位置支撑本文哪部分
main/pie_dotprod.Spie_dotprod_s8s16、pie_gemv_row、pie_gemv_row_blk第 3 节分块 GEMV 内核(pie_gemv_row_blk 标签)
main/main.cpie_bench、pie_gemv_row第 5 节单算子对拍与测速
docs/milestones.mddl_esp32p4_dotprod_i16k8o16、esp.vldext.s8.ip第 1–2 节 PIE 调研与指令来源

《Kestrel-MCU 手记》 · 全系列 28 篇:在 ESP32-P4 上从零训练并部署 32M~64M 参数 MoE 大模型(5.7~6.4 tok/s),源码、权重、训练脚本与全部文章开源可复现。

仓库:https://gitee.com/pei-xiaoguang/kestrel-llm-mcu · 觉得有用欢迎 Star

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐