☰
19 · PLE 上板实战:三值查表格式、内核实现与堆溢出 Bug 修复
2026/10/14 19:47:35 网站建设 项目流程

19 · PLE 上板实战:三值查表格式、内核实现与堆溢出 Bug 修复

English version:en/19-ple-deployment.md

本篇对应源码:tools/convert_minueza.py·main/kmcu.c·main/main.c·host_verify.c

目标:把训练侧已验证的完整配方(MoE core + PLE 三值查表 + tied head)真正送进 kmcu.c。
这是 13 篇(MoE 部署衔接)的姊妹篇——13 篇打通了 MoE 上板,
本篇打通 PLE 三值查表 + tied head 上板。训练侧验证见 14/16/17。

一、部署侧要补什么

训练侧产出的模型有四个东西是旧部署链路(MoE)没有的:

新增旧链路状态要补
PLE 查表(ple_table,三值 1.6bit)无此张量KMCU 新 dtype + base-3 解码
PLE 分支(ple_model_proj/ple_gate/ple_proj/norm)无convert 导出 + 内核前向
tied head(tie_word_embeddings=true)MoE 是独立 lm_head展开策略修正
三值 scale(ple_gamma)无header 存一个 f32

二、KMCU 格式扩展

在 KMCU v1 上做了最小扩展,不破坏旧格式(旧稠密/MoE 模型ple_dim=0走原路径):

  • 新增 dtypeDT_TERNARY=3:目录项n_rows=vocab、n_cols=table_width,
    nbytes = vocab × ceil(table_width/5),行对齐(每行独立打包,flash 逐行随机读)。
  • header 追加两个字段(offset 84/88):ple_dim(0=无 PLE)、ple_gamma(三值 scale,f32)。

三值编码(base-3):code = clip(round(w/gamma), -1, 1),trit = code+1 ∈ {0,1,2},
每字节装 5 个 trit:byte = t0 + t1·3 + t2·9 + t3·27 + t4·81。

三、convert 的三值打包

convert_minueza.py 新增ternary_pack:

gamma=float(np.abs(w).mean())# 与训练 STE 口径一致codes=np.clip(np.rint(w/gamma),-1,1)# {-1,0,1}trit=(codes+1).astype(np.uint8)# {0,1,2}# 每行补齐到 ceil(width/5)*5,5 trit 打包 1 字节

ple_table尺寸:32002 × 1280 = 41M 参数,fp32 163.8MB → 三值 base-38.2MB(20×)。

四、kmcu.c 的 PLE 内核

两条 decode 路径(常驻km_decode_step/ 流式km_decode_step_stream)都加了 PLE。

1. PLE 底部(每 token 一次,在 embed 之后)

ple = ple_model_proj(x) · dim^-0.5 (GEMV,w8a16) ple = RMSNorm(ple 按 ple_dim 切片) (ple_proj_norm) 读 ple_table 第 token 行(flash,base-3 逐 trit 解码) ple = (ple + table · pd^0.5) · 2^-0.5

2. 每层 PLE 分支(attention + ffn 之后)

g = gelu(ple_gate(x)) (GEMV + 精确 erf GELU) x = x + RMSNorm(ple_proj(g · ple[:, L, :]))

3. tied head 的展开策略(关键修正)

MoE 模型原本「tok_embed 流式跳过」(每 token 只读一行,省 PSRAM)。但 tied=1 时
tok_embed 同时是输出头(全 vocab 扫描),不能再跳过——否则输出头oh->q == NULL崩。

修正:skip 条件 = n_expert>0 && !tied_embed。tied=1 时 embed 照常展开进 PSRAM,
输入用q8_row_get(resident)、输出头用展开的oh->q。

五、PC 对拍:host_verify

板端pie_gemv_row是 ESP32-P4 PIE 汇编,PC 无法直接跑。新增
host_verify.c:用标量版pie_gemv_rowstub
编译 kmcu.c,加载 KMCU 镜像,跑与 ref_forward.py 完全相同的贪心 decode。

gcc -O2 -std=c11 -I main -o host_verify host_verify.c main/kmcu.c -lm ./host_verify model.kmcu

真实尺寸(dim312/vocab32002/64M)对拍结果:

ref : 1,450,2217,4996,3156,28723,415,28705,28740,28774,28774,28734,28713,28725,272,28705,28740,28774,28774,28734 host : 1,450,2217,4996,3156,28723,415,28705,28740,28774,28774,28734,28713,28725,272,28705,28740,28774,28774,28734

20/20 token 一致;top8@first-gen 一致(3156,522,323,28723,282,28725,753,1907),
logit 差 ~9e-4(w8a16 vs fp32,符合既有口径)。

六、过程中的 bug:PLE 三值行解码堆溢出

症状:host_verify 退出码0xC0000374(堆损坏),但输出 seq 仍正确。

根因:PLE 底部三值表解码时,用pleo(scratch 里dim_p=320个 float)暂存整行
nl×pd=1280个 trit。真实尺寸下越界写堆。

为什么微型模型没暴露:微型冒烟模型table_width=32 < dim_p=64,暂存不越界。
而 PLE 的table_width = nl·pd与dim_p无任何关联——真实配置(10层×128)下 1280 ≫ 320。

修复:改成逐 trit 解码就地合并,不暂存整行:

for(intb=0,oi=0;b<row_bytes&&oi<ple_rows;b++){uint32_tv=stage[b];for(intk=0;k<5&&oi<ple_rows;k++,v/=3u){floattv=(float)((int)(v%3u)-1)*a->ple_gamma;ple[oi]=(ple[oi]+tv*spd)*inv2;oi++;}}

教训:微型冒烟模型会掩盖缓冲区尺寸类 bug。PLE 的 table_width 与 dim 解耦,
必须用真实尺寸对拍才能暴露。这与 13 篇 的「16B gap 未暴露」
是同一类教训——缩小的测试件测不出「尺寸相关」的越界。

七、结论

  1. PLE 三值查表 + tied head 的部署链路(convert → KMCU → kmcu.c → host_verify)已打通;
  2. 真实尺寸 20/20 对拍通过,三值表 20× 压缩(163.8MB→8.2MB)不损质量;
  3. 完整配方「MoE core + PLE 三值 table + head tied」至此训练 + 部署 + 对拍全链走通。

对应源码

文件关键符号 / 位置支撑本文哪部分
tools/convert_minueza.pyDT_TERNARY二、KMCU 格式扩展(新增三值 dtype)
tools/convert_minueza.pyternary_pack()三、convert 的三值 PLE 表打包
main/kmcu.ckm_decode_step_stream()四、kmcu.c 的 PLE 内核(流式路径)
main/kmcu.ckm_fast_build_ex()四.3、tied head 展开策略修正
main/main.cSD_RESIDENT部署落地:大 PLE 模型走 SD 卡常驻
host_verify.cmain()五、PC 对拍 host_verify

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询