13 · MoE 上板缺的最后一环:转换器与推理内核的部署衔接缺口
English version:en/13-moe-deployment-gap.md
本篇对应源码:
main/kmcu.c·main/kmcu.h·tools/convert_minueza.py·docs/moe_framework.md
目标:讲清楚训练侧闭环后,最优先要补的硬缺口——「训练产出 → MCU 部署」之间,
convert 脚本和推理内核都还不支持 MoE。这是打通端到端的第一道门槛。
一、缺口在哪
训练侧已经跑通(ffn_e=128 全量 MoE,match 0.641),但训练出的模型送不上 MCU:
| 组件 | 现状 | 缺什么 |
|---|---|---|
convert_minueza.py | 只支持稠密单 FFN(gate/up/down) | 专家分类存储(每层 router + N×expert)+ q4_0 量化 |
kmcu.c | topk 只是输出头 argmax 对拍 | MoE FFN 内核(router 前向 → top-k → 按索引查表加载专家 → 稀疏 FFN) |
二、「分类存储 / 查表」是什么
这是 MoE 稀疏激活的物理本质,不是可选优化:
- 分类存储:8 个专家的权重按专家分组连续存放——expert0 的 gate/up/down 连续,
expert1 紧跟,依此类推。 - 查表加载:router 算出 top-k 索引后,按索引直接定位并加载对应专家的权重,
只算被选中的专家,其余专家的权重根本不用读。
这正是「每 token 只读 top-k 专家」的带宽收益来源:ffn_e=128 时每 token 只读2×128=256维的专家权重,比稠密 FFN(1092 维)少读4.27 倍。
三、必须区分:两处「查表」不是一回事
| 场景 | 位置 | 结论 |
|---|---|---|
| 输出头 ANN 剪枝(M3-11) | lm_head 32002 行,K-means 聚类 + 选簇 | ❌ 已判否(margin 极小 + 高维维度灾难) |
| MoE 专家分类存储 + 查表 | 每层 FFN,router 选 top-k | ✅ 稀疏激活本质,必然要做 |
M3-11 借鉴 esp32-ai 的「PLE 稀疏查表」用于输出头剪枝,结果剪枝率趋近 0 判否。
而 MoE 的专家分类存储是另一回事——8 个专家天然分组,router 查表是稀疏计算的物理实现,
不是剪枝优化,不会遇到 M3-11 的高维 argmax 问题。
四、要补的两件事
1. convert_minueza.py 扩展 MoE
现有脚本只处理稠密mlp.gate_proj/up_proj/down_proj,需扩展为:
- 每层加
router([8, dim] 小矩阵); - 每个专家
expert{e}.gate/up/down独立张量,按专家分类存储; - 专家权重仍走 q4_0 量化(复用现有
q4_quantize规则)。
2. kmcu.c 增加 MoE FFN 内核
现有 topk 只是输出头 argmax 对拍,需新增:
router 前向(小 GEMV:h @ router.T,产出 8 个 logits) → top-k 选择(选得分最高的 k 个专家索引) → 按索引查表加载专家权重(只读被选中专家的 gate/up/down) → 稀疏 FFN(SwiGLU,只算选中专家,加权累加)五、结论
- 训练侧产出「ffn_e=128 全量 MoE」后,最优先是打通 convert + 内核这条链,
让模型真正能上 ESP32-P4。 - 「分类存储/查表」是 MoE 稀疏激活的本质,与 M3-11 判否的输出头剪枝是两回事,不要混淆。
- 这是「训练 → convert → MCU 部署」端到端的最后一块拼图。
六、缺口已闭环(2026-09-27 更新)
本文所述的两件事(convert 扩展 + MoE 内核)均已完成,且端到端在 ESP32-P4 上验证通过。
落地结果
| 项 | 结果 |
|---|---|
| 模型 | moe_3ep(3 epoch,argmax match 0.744,32.18M 参数,q4 = 17.78MB) |
| convert | router(f32) + 8×expert(q4) 分类存储 + 独立 lm_head(tie=false) |
| 内核 | km_decode_step/km_decode_step_stream均支持 router→softmax→top-k→专家加权 |
| 对拍 | 板端 20/20 与 PC 参考ref_out_3ep.txt逐 token 一致 |
两条部署路径(性能天壤之别)
| 路径 | 每 token 读量 | decode | 判读 |
|---|---|---|---|
| SD_STREAM(全流式) | 11351 KB | 20.6 s/token | 每 token 重读全量权重,正确性验证件,不可用 |
| SD_RESIDENT(方案 B) | 180 B(仅 embed 一行) | 0.157 s/token | lm_head+专家+attention 常驻 PSRAM(23.6MB),131× |
关键认知:MoE 的 32M 参数展开成 int8 仅 23.6MB,完全装得进 32MB PSRAM。
之前的「PSRAM 超限」只存在于**全量展开(含 embed)**这一错误假设——方案 B 只流式读
embed(每 token 180B),其余常驻,既解决容量又解决速度。
过程中修掉的两个实现级 bug
- convert 16B 对齐 gap:数据区没补齐
data_offset对齐 gap,导致目录 offset 错位、
scale 变 nan(稠密 gap=0 未暴露,MoE 模型才暴露)。 km_f16_to_f32subnormal 指数 bug:subnormal 分支误写127-15-e(应为127-14-e),
把 145830 个(14.2%)subnormal scale 减半,板端 logits 误差 ~0.08、翻转 tok[5](650→1306)。
这是本次「板端对拍翻转」的真正根因,不是量化精度(w8a16 误差仅 ~1.3e-3)。
教训:手写 fp16→fp32 转换必须单独验证 subnormal 分支;「量化误差导致翻转」这类归因,
先用同量化权重的 PC 参考做逐 GEMV 模拟排除量化嫌疑,再查实现级 bug。
对应源码
| 文件 | 关键符号 / 位置 | 支撑本文哪部分 |
|---|---|---|
tools/convert_minueza.py | DT_TERNARY、q4_quantize | 专家分类存储与 q4_0 量化扩展 |
main/kmcu.c | km_decode_step()、km_decode_step_stream() | MoE FFN 内核与两条部署路径 |
main/kmcu.h | km_f16_to_f32()、KM_DT_TERNARY | subnormal 修复与张量类型定义 |
docs/moe_framework.md | is_moe、top_k | 缺口定义与 v2 格式/内核设计 |
《Kestrel-MCU 手记》· 全系列 28 篇:在 ESP32-P4 上从零训练并部署 32M~64M 参数 MoE 大模型(5.7~6.4 tok/s),源码、权重、训练脚本与全部文章开源可复现。
仓库:https://gitee.com/pei-xiaoguang/kestrel-llm-mcu · 觉得有用欢迎 Star