00 · 在 ESP32-P4 上跑通 LLM:从 0.61 到 4.31 tok/s 的 7 倍优化全复盘(系列总览)
本篇对应源码:仓库根目录 ——
README.md·main/kmcu.c·tools/convert_minueza.py·docs/milestones.md
在ESP32-P4(RISC-V)上跑通一个 0.032B(22.8M 参数)Mistral 家族小模型,
并用私有 PIE 向量指令把 decode 加速 7× 的完整复盘。
一、这套文章解决什么问题
这是一份可复现的系列手记,覆盖从「零环境」到「0.032B 模型在 MCU 上 20/20 正确解码」的完整路径,
尤其把过程中踩过的每一个坑和每一个 bug都记录了下来。
适合读者:
- 想在 ESP32-P4 上跑 LLM 推理的嵌入式工程师;
- 想了解「MCU 上量化 + 手写 SIMD」全流程的算法/性能工程师;
- 想复现本项目、或移植到其他 RISC-V MCU 的人。
二、最终成果(先看结果)
| 里程碑 | 速度 | 说明 |
|---|---|---|
| M2(标量起点) | 1.63 s/token(0.61 tok/s) | q4 逐元素反量化,正确但慢 |
| M3-1(q4→int8 展开) | 1.11 s/token(0.90 tok/s) | 装载期展开 |
| M3-2(行对齐 + -O2) | 0.511 s/token(1.96 tok/s) | 修正了 -Og 的隐蔽压制 |
| M3-3(fp16→fp32) | 0.489 s/token(2.04 tok/s) | 无损,+4% |
| M3-4(PIE 定点 GEMV) | 0.232 s/token(4.31 tok/s) | 累计 7.0× |
正确性:20/20 token 与 PC 的 fp32 NumPy 参考完全一致,int16 激活量化对 argmax 零影响。
硬件:Waveshare ESP32-P4-WIFI6-DEV-KIT(SKU 32054)—— RISC-V 双核 400 MHz、32 MB PSRAM、16 MB NOR Flash。
对拍金标准速览(复现时用来核对)
single-token[1] argmax = 684 (PC 参考 logit=5.350636) 4-token prompt 末位 top1 = 23624 (PC 参考 logit=4.978983) 20-token 贪心序列: 1,450,2217,4996,23624,1199,8752,23624,1199,1199,8752,23624,1199,10436,23624,1199,3065,3161,4865,442PIE 单算子 bench(板端):
dotprod N=4096 match=1 speedup=20.97x(纯计算) gemv_row diff=0.000000e+00(分块 GEMV 逐位一致)实跑复现(2026-09-28,ESP32-P4 rev v3.1):
dotprod match=1与gemv_row diff=0.000000e+00可逐位复现;speedup随运行浮动,重跑得20.83x。
同理 M3-4 的 decode 速率在0.226–0.232 s/token(4.33–4.42 tok/s)间浮动,
表中 0.232 s/token 是其中一次读数。
完整 top-8 logits 见 02 篇「完整基准输出」。
三、模型
Felladrin/Minueza-32M-Base(Mistral 家族):
layers=10, dim=312, heads=12, kv_heads=4, head_dim=26, ffn=1092, vocab=32002 参数量 22.81M(`--force-tie` 强制权重共享),Q4_0 量化后 12.58 MB四、快速开始(最小命令集)
如果你只想最快跑起来,按顺序执行:
# 1) 环境(详见 01-环境搭建与构建.md)subst X:"%USERPROFILE%\.espressif"$env:IDF_TOOLS_PATH ="X:\"$env:IDF_SKIP_CHECK_SUBMODULES ="1".E:\esp-idf\export.ps1$env:PATH ="X:\python_env\idf6.0_py3.12_env\Scripts;"+$env:PATH# 2) 转换模型(详见 02-模型量化与KMCU格式.md)# 在仓库根目录执行;模型与产物统一放 models\ 子目录cd kestrel_mcu python tools\convert_minueza.py--src models\model_dir--out models\model.kmcu--force-tie python tools\ref_forward.py--km models\model.kmcu--ref_out models\ref_out.txt# 3) 构建 + 烧录(构建副本必须在纯 ASCII 路径 C:\kmcu,例如 C:\kmcu)cd C:\kmcu idf.py build idf.py-p COM5 flash# 4) 烧录模型分区(一次性)python-m esptool--chip esp32p4-p COM5-b 921600--before default-reset--after hard-resetwrite-flash0x210000 models\model.kmcu# 5) 监视输出idf.py-p COM5 monitor五、文章导航
| 篇 | 内容 | 适合 |
|---|---|---|
| 01-环境搭建与构建 | ESP-IDF v6.0.2 从零搭建 + 全部环境坑 | 复现第一步必读 |
| 02-模型量化与KMCU格式 | q4_0 量化、行对齐、KMCU v1 二进制格式 | 想改模型/格式的人 |
| 03-标量推理内核 | kmcu.c 的 decode 实现 + TF 卡 + M2 | 想改推理逻辑的人 |
| 04-性能优化全历程 | M3-1→M3-4 每次优化的动机与实测 | 想做性能的人 |
| 05-PIE汇编实战 | 手写 xespv 汇编 + 两个致命 bug | 想用 PIE 的人 |
| 06-踩坑速查表 | 所有坑的汇总速查 | 遇到问题先查这里 |
| 07-训练流程总览与结论 | 训练侧全地图 + 各技术点闸门判定 | 想知道模型怎么训出来的人 |
| 08-经典全量预训练 | 失败归因 + 阶段 0 全量训练突破 ppl 8.73 | 想复现训练的人 |
| 09-公理库底座与λ过渡路由 | SHS-2/3/T + LambdaMoE λ 退火 | 想懂框架特色的人 |
| 10-MoE稀疏化实战 | 结构蒸馏失败 → 全量 MoE + ffn_e 扫描 | 想训 MoE 的人 |
| 11-量化感知 | QAT vs PTQ,PTQ +0.9% 足够 | 想做量化的人 |
| 12-逐层适配优化 | 逐层量化的公理底座与落地路径 | 想跑大模型的人 |
| 13-MoE部署衔接缺口 | convert + 内核的 MoE 支持缺口 | 想打通端到端的人 |
| 14-架构寻路与PLE | 12 个优化判否全历程 → 发现并验证 PLE 正解 | 想做「更大模型」的人 |
| 15-三值量化变种 | 全模型三值判否 → 局部三值(PLE table)成立 → ReLU² 判否 | 想做极致压缩的人 |
| 16-PLE落地验证 | dim 312 真实配置下 PLE 成立 + 三值几乎无损 | 想落地 PLE 的人 |
| 17-MoE框架下的PLE实现 | MoEFFN 实现 + MoE 与 PLE 正交共存 + 完整配方验证 | 想做完整配方的人 |
| 18-对标PFor与esp32-ai | 与 PFor / esp32-ai 的技术级横向对比 + 借鉴与独有差异 | 想外部校准配方的人 |
| 19-PLE部署落地 | PLE 三值查表 + tied head 上板:DT_TERNARY 格式 + 内核 + 堆溢出 bug | 想打通 PLE 部署的人 |
| 20-PLE为什么不加速 | PLE 收益在容量不在速度:读量归因 + tied 只省存储不省读量 | 想知道速度真相的人 |
| 21-PFor选择速度我们选择质量 | 速度 vs 质量零和博弈:PFor 三值换速度、我们 q4 换质量 | 想懂设计取舍的人 |
| 22-正式训练与端到端闭环 | FineWeb-Edu 正式训练(ppl 37.13)+ 部署对拍 20/20 | 想复现完整配方的人 |
| 23-规模线与PLE-table本质 | 更大模型「大」在哪 + PLE table 到底是不是知识库 | 想懂规模线本质的人 |
| 24-决策头vs生成头 | Jev/Laya 启示:决策任务用「决策头」而非「生成头」,读量省 16000× | 想做 MCU 决策的人 |
| 25-双头融合-该判断时判断该输出时输出 | 决策头+生成头融合框架:三种形态 + 元决策 + agent 循环 | 想建统一模型的人 |
| 26-观察回接-Agent循环 | 观察回接核心机制:λ 框架 + 截断观察回接 + 增量 KV 位级一致性 | 想做 MCU agent 循环的人 |
| 27-map直挂缓存判否 | mmap 直挂 flash 带宽基准:14MB/s 是 PSRAM 1/7.6,大模型直挂判否 | 想省 PSRAM 装更大模型的人 |
六、关键结论(提炼)
- PIE 是纯定点 SIMD,没有 fp32 向量指令(乐鑫源码铁证)。要加速
int8 权重 × fp32 激活的 GEMV,
唯一路径是激活定点化(w8a16)。 - 实测胜于推论:纯计算加速 21×,但接入后被 PSRAM 带宽墙(~84–106 MB/s)压到2.11×。
-Og是隐形杀手:ESP-IDF 默认 debug 优化级会系统性压低性能读数,M1/M2/M3-1 的数据都被它污染过。- 两个 PIE 硬约束:标量操作数不能用 t0-t2 寄存器、向量加载需 16 字节对齐——不满足会得到「随机数据对拍正确、真实数据全错」的诡异现象。
七、项目文件索引
kestrel-llm-mcu/ # https://gitee.com/pei-xiaoguang/kestrel-llm-mcu ├── main/ 固件 │ ├── main.c # 入口:SHS 自检 + PSRAM/TF 探针 + 模型加载 + decode + PIE bench + 三模式 │ ├── kmcu.c / kmcu.h # KMCU 权重读取 + 标量/PIE Transformer decode + 决策头 + agent 循环 │ ├── pie_dotprod.S # PIE(xespv)定点 GEMV 汇编 │ └── CMakeLists.txt ├── tools/ 宿主侧工具链(详见 tools/README.md) │ ├── mistral_ple.py # 架构唯一权威源(MistralPLE) │ ├── convert_minueza.py# HF safetensors → KMCU 量化权重 │ ├── ref_forward.py # PC 端 fp32 NumPy 参考(唯一对拍基准) │ ├── train_moe_ple.py # 训练 │ ├── watch_boot.py # 复位 + boot 日志监视 │ ├── upload_uart.py # UART 上传(experimental) │ └── legacy/ # 归档的历史研究脚本(含本文系列早期用到的 train_moe*.py 等) ├── docs/ 设计文档 + 开发日志(中英双版) ├── host_verify.c # PC 侧对拍程序(编译同一份 kmcu.c) ├── partitions.csv # Flash 布局(app 2 MB / model 13.87 MB) ├── sdkconfig.defaults # 关键配置(-O2、Newlib、PSRAM hex、关 WDT) └── LICENSE / NOTICE # Apache-2.0 + 第三方声明注:
GitHupSRC/(vLLM-Kestrel 引擎)与kestrel_mcu/是两个独立项目,
本系列只讲kestrel_mcu/这一条独立 determinism line,不涉及引擎源码。
对应源码(全系列总表)
每篇文末都有一节「对应源码」,给出该篇的符号级映射(文件 + 函数/宏 + 它支撑文中的哪部分结论)。
下表是系列导航级的速查:想直接读代码,从这里进。
| 篇 | 主要源码 |
|---|---|
| 00 总览与阅读指南 | README.md·docs/milestones.md |
| 01 环境搭建与构建 | sdkconfig.defaults·partitions.csv·main/main.c·CMakeLists.txt |
| 02 模型量化与 KMCU 格式 | tools/convert_minueza.py·main/kmcu.h·tools/ref_forward.py |
| 03 标量推理内核 | main/kmcu.c·main/kmcu.h·host_verify.c |
| 04 性能优化全历程 | main/main.c·main/kmcu.c·main/pie_dotprod.S |
| 05 PIE 汇编实战 | main/pie_dotprod.S·main/main.c |
| 06 踩坑速查表 | main/kmcu.c·main/main.c·sdkconfig.defaults |
| 07 训练流程总览与结论 | tools/mistral_ple.py·tools/train_moe_ple.py·docs/lambda_transition_framework.md |
| 08 经典全量预训练 | tools/legacy/full_finetune.py·tools/mistral_ple.py |
| 09 公理库底座与 λ 过渡路由 | docs/lambda_transition_framework.md·tools/legacy/train_moe.py·tools/legacy/train_lambda.py |
| 10 MoE 稀疏化实战 | tools/legacy/train_moe.py·tools/legacy/train_lambda.py·docs/moe_framework.md |
| 11 量化感知 | tools/legacy/train_moe_qat.py·tools/legacy/verify_q2.py·tools/convert_minueza.py |
| 12 逐层适配优化 | docs/layerwise_adaptation.md·tools/legacy/train_moe_qat.py |
| 13 MoE 部署衔接缺口 | main/kmcu.c·main/kmcu.h·docs/moe_framework.md |
| 14 架构寻路与 PLE | tools/mistral_ple.py·tools/legacy/dense_baseline.py |
| 15 三值量化变种 | tools/mistral_ple.py·tools/convert_minueza.py·main/kmcu.c |
| 16 PLE 落地验证 | tools/mistral_ple.py·tools/train_moe_ple.py |
| 17 MoE 框架下的 PLE 实现 | tools/mistral_ple.py·tools/train_moe_ple.py·docs/moe_framework.md |
| 18 对标 PFor 与 esp32-ai | README.md·docs/milestones.md |
| 19 PLE 部署落地 | tools/convert_minueza.py·main/kmcu.c·host_verify.c |
| 20 PLE 为什么不加速 | main/kmcu.c·docs/milestones.md |
| 21 PFor 选择速度我们选择质量 | docs/milestones.md·README.md |
| 22 正式训练与端到端闭环 | tools/train_moe_ple.py·tools/convert_minueza.py·tools/ref_forward.py·host_verify.c |
| 23 规模线与 PLE-table 本质 | tools/mistral_ple.py·docs/milestones.md |
| 24 决策头 vs 生成头 | main/kmcu.c·main/main.c·docs/agent_loop_router.md |
| 25 双头融合 | main/main.c·main/kmcu.c·docs/agent_loop_router.md |
| 26 观察回接-Agent 循环 | main/kmcu.c·main/kmcu.h·docs/agent_loop_router.md |
| 27 map 直挂缓存判否 | main/main.c·main/kmcu.h·docs/milestones.md |
注:09 与 15 篇引用的
axiom_registry.json为内部文件,未随本仓库发布。
对应源码
| 文件 | 关键符号 / 位置 | 支撑本文哪部分 |
|---|---|---|
README.md | Highlights (measured)、Repository layout | §二 最终成果、§四 快速开始 |
docs/milestones.md | M1 / M2 / M3-x 全部小节 | §二 M2→M3-4 各档实测数据 |
main/main.c | shs_selftest()、probe_psram()、probe_sdcard()、model_test() | §四 第 3/4/5 步的板端自检与模型加载 |
tools/convert_minueza.py | SRC_DIR/OUT_PATH(KMCU_SRC/KMCU_OUT环境变量) | §四 第 2 步转换命令 |
仓库:https://gitee.com/pei-xiaoguang/kestrel-llm-mcu