
1. 为什么要在 MCU 上跑 LLM一个反直觉的工程选择第一次跟朋友聊起这个项目对方的第一反应基本都是同一句话“你是不是闲的” 这个反应很正常。ESP32-P4 是一颗面向嵌入式视觉和 HMI 场景的 MCURISC-V 双核主频 400MHz加上一堆外设和 MIPI 接口定位是“能跑 UI、能接摄像头”的实时控制器。而 LLM 这个词大家脑子里默认绑定的硬件是 GPU 集群、至少几十 GB 显存、动辄几百瓦功耗。把这两个东西放在一起直觉上就是错配。但实际做下来这件事的价值恰恰不在“跑得动大模型”本身而在于它逼着你去理解一个平时被算力掩盖掉的问题推理的瓶颈到底在哪一层。当你有 A100 的时候你不需要关心 KV Cache 的内存布局不需要关心权重是不是按 cache line 对齐不需要关心一次矩阵乘法的循环顺序会不会导致频繁的 cache miss。这些细节全被算力红利吃掉了。而在 ESP32-P4 上这些细节就是全部——0.61 tok/s 和 4.31 tok/s 之间的差距不是靠换硬件换来的是靠一层一层把这些被忽略的东西抠出来的。这个系列总览想做的事情是把整个优化链路完整摊开。不是那种“我调了个参数就快了”的爽文而是每一步为什么这么改、改之前测出来是什么数、改之后涨了多少、哪些改动其实是无效的。我踩过的坑包括但不限于以为瓶颈在算力结果发现卡在内存带宽、优化了半天发现是量化格式选错了、以及最经典的——某个“优化”让速度涨了 15% 但输出质量直接崩了。适合读这个系列的人有三类。第一类是嵌入式工程师手上有 ESP32-P4 或者类似的 RISC-V MCU想搞清楚这颗芯片在 AI 负载下的真实能力边界。第二类是做端侧推理的算法工程师平时在 ARM 或者 x86 上做量化部署想看看换到更受限的架构上哪些经验还能用、哪些直接失效。第三类是纯粹对“极限优化”这件事感兴趣的开发者喜欢看一个系统从能跑到跑得快中间到底发生了什么。如果你属于第三类那这个系列应该会让你看得比较过瘾因为中间有大量“测出来和预期完全相反”的时刻。先把结论性的数字放在这里方便你判断要不要继续往下读基线版本 0.61 tok/s最终版本 4.31 tok/s整体约 7 倍。这个 7 倍不是单一手段带来的是量化格式、内存布局、算子实现、KV Cache 管理、编译选项这几块叠加的结果每一块的贡献从 1.2 倍到 2 倍不等。后面会逐块拆。2. ESP32-P4 这颗芯片到底给了什么牌2.1 算力账400MHz 双核 RISC-V 能干什么ESP32-P4 的 CPU 部分是双核 RISC-V主频 400MHz带 FPU支持单精度浮点。先算一笔最朴素的账400MHz 意味着每秒 4 亿个时钟周期双核理论上 8 亿。如果每个周期能完成一次乘加MAC那峰值就是 800M MAC/s也就是 1.6 GFLOPS 左右。这个数字放在 GPU 面前不值一提但放在 MCU 里已经算不错了。问题在于通用 RISC-V 核每个周期根本做不到一次 MAC。一次 FP32 乘加取指、译码、取操作数、执行、写回流水线排下来实际吞吐可能只有峰值的几分之一。而且 LLM 推理里大量的操作是矩阵向量乘访存密集CPU 大部分时间在等数据而不是在算。所以真实可用的算力比纸面峰值还要打不少折扣。这就是为什么 P4 上的 PIE 协处理器变得关键。2.2 PIE被低估的向量加速单元PIE 是 ESP32-P4 里的向量扩展单元全称是 Processor Instruction Extension 之类的意思乐鑫的文档里对它的描述比较克制。它提供了一批 SIMD 风格的指令能在一个周期里对多个数据做并行运算。对于 LLM 推理里最常见的矩阵乘、点积、激活函数这些操作PIE 能带来的加速是实打实的。但 PIE 不是免费的午餐。它的指令集有自己的约束数据需要按特定方式对齐寄存器数量有限某些操作的数据类型支持不完整。我在项目里遇到的一个典型问题是PIE 对 INT8 的支持比对 FP32 好得多这直接影响了量化格式的选择——后面会详细讲这个决策链。提示如果你打算在 P4 上做任何计算密集型的工作先把 PIE 的指令手册过一遍。不是让你背指令而是搞清楚哪些操作有硬件加速、哪些只能走通用核。这个认知会直接决定你的算法该怎么写。2.3 内存真正的瓶颈所在P4 的片上 SRAM 是几百 KB 级别具体数字看具体型号和配置。外部可以挂 PSRAM容量能到几十 MB但带宽和延迟跟片上 SRAM 差了一个数量级。LLM 推理需要频繁访问权重和 KV Cache如果这些东西放在 PSRAM 里每次访问都要走外部总线延迟直接吃掉所有计算收益。我一开始的基线版本就是把所有权重放在 PSRAM 里结果就是 CPU 大部分时间在等内存。后来把最热的那部分权重比如 attention 里的 QKV 投影矩阵搬到片上 SRAM速度立刻上了一个台阶。这个改动的本质是在 MCU 上内存层级的重要性远大于算力。你有一块快的内存比有一个快的核更重要。3. 从 0.61 到 4.31优化链路的整体拆解3.1 基线版本长什么样基线版本是一个“能跑就行”的实现。模型选的是一个小型 Transformer参数量在几 M 级别量化到 INT8。推理代码是直接照着 PyTorch 的实现翻译成 C 的矩阵乘法就是三重循环没有用 PIE没有做内存对齐KV Cache 用最简单的数组实现每次生成新 token 都重新分配。这个版本跑出来 0.61 tok/s。说实话第一次看到这个数字的时候我是有点意外的——比预期慢但没慢到离谱。说明基本逻辑是对的只是每一层都有优化空间。3.2 七倍是怎么攒出来的整个优化过程可以分成几个阶段每个阶段的贡献大致如下优化阶段主要改动速度提升累计 tok/s基线纯 C 实现无优化-0.61量化格式调整INT8 权重 PIE 友好布局约 1.8x1.10内存布局优化权重搬片上 SRAM对齐 cache line约 1.5x1.65算子重写矩阵乘用 PIE 指令重写约 1.6x2.64KV Cache 管理预分配 环形缓冲约 1.3x3.43编译与调度编译选项 循环展开 双核分工约 1.25x4.31这张表是事后整理的实际过程中顺序有交叉有些改动是同时做的所以单独归因不一定完全准确。但大致的量级关系是对的没有哪一项是银弹七倍是攒出来的。3.3 哪些改动其实没用这个部分可能比“什么有用”更有价值。我试过但基本没效果的改动包括把 FP32 换成 FP16P4 的 FPU 对 FP16 没有额外加速反而因为转换开销更慢了。手动做循环展开编译器在 -O2 下已经做得不错了手动展开收益很小。把模型切得更小参数量减半速度只涨了不到 20%说明瓶颈不在计算量而在访存。用 DMA 搬数据对于小块的、频繁的访问DMA 的启动开销比直接访问还大。这些失败尝试的共同点是它们都在优化“计算”但真正的瓶颈在“访存”。这个认知是后面所有有效优化的前提。4. 量化格式的选择为什么 INT8 是唯一解4.1 FP32、FP16、INT8 在 P4 上的实测对比先看数据格式模型大小单 token 延迟输出质量备注FP32基准基准最好内存放不下需要频繁换入换出FP16约 50%比 FP32 慢接近 FP32FPU 无加速转换有开销INT8约 25%最快可接受PIE 有原生支持FP16 比 FP32 慢这个结果一开始让我很困惑。后来想明白了P4 的 FPU 是单精度的FP16 运算需要先转成 FP32 再算再转回去多出来的转换指令把省下的内存带宽又吃回去了。而 INT8 不一样PIE 有专门的 INT8 乘加指令一个周期能处理多个 INT8 数据这是真正的硬件加速。4.2 量化粒度per-tensor 还是 per-channel确定了 INT8 之后下一个问题是量化粒度。Per-tensor 是整个权重矩阵共用一个 scaleper-channel 是每一行或每一列有自己的 scale。Per-channel 精度更好但推理时需要额外的 scale 乘法在 P4 上这个开销不可忽略。我的选择是权重用 per-channel激活用 per-tensor。理由是权重的 scale 可以在编译期确定并预乘到权重里推理时不需要额外操作而激活的 scale 是动态的per-tensor 实现更简单精度损失在可接受范围内。4.3 量化校准集的坑量化校准集的选择比想象中重要。我一开始用了一段通用的英文文本做校准结果模型在中文输入上表现明显变差。后来换成中英混合的校准集问题解决。这个坑的教训是校准集要覆盖实际使用场景的数据分布不能随便找一段文本凑数。注意量化后的模型一定要做端到端的质量评估不能只看 perplexity。有些量化误差在 perplexity 上体现不明显但在具体任务上会导致输出完全不可用。5. 内存布局把数据放在对的地方5.1 片上 SRAM 和 PSRAM 的带宽差距前面提过P4 的片上 SRAM 和外部 PSRAM 在带宽和延迟上差距很大。具体数字因配置而异但量级上的差异是片上 SRAM 的访问延迟在几个周期PSRAM 在几十个周期。对于 LLM 推理这种访存密集的负载这个差距直接决定了性能上限。我的做法是把权重按“热度”分层attention 的 QKV 投影和 FFN 的第一层矩阵放在片上 SRAM其余放 PSRAM。这个分层的依据是访问频率——QKV 投影在每个 token 生成时都要用FFN 第一层次之输出层再次之。5.2 对齐一个被忽视的性能杀手数据对齐在 x86 上可能只是“最好做一下”的事情在 P4 上是“必须做”的事情。PIE 的向量指令要求操作数按特定边界对齐如果不对齐要么触发异常要么走慢速路径。我一开始没注意这个矩阵乘的输入指针对齐是随机的导致 PIE 指令有一半时间在走慢速路径。修复方法很简单所有权重数组用aligned_alloc分配确保起始地址按 16 字节或 32 字节对齐。这个改动本身只花了几行代码但带来的性能提升是实打实的。5.3 数据布局行优先还是列优先矩阵乘法的数据布局对 cache 命中率影响很大。PyTorch 默认是行优先但 LLM 推理里的矩阵乘往往是“矩阵乘向量”的形式这时候列优先可能更友好。我试过两种布局在 P4 上的实测结果是对于 PIE 实现的矩阵乘把权重转成列优先能减少一次转置操作整体快约 10%。这个改动需要在模型导出阶段就做好推理时直接加载转置后的权重。如果你是从 PyTorch 导出可以在导出脚本里加一步转置。6. 算子实现PIE 指令怎么用才不浪费6.1 矩阵乘的 PIE 实现思路矩阵乘是 LLM 推理里最耗时的算子也是 PIE 加速收益最大的地方。基本思路是把矩阵分块每块用 PIE 的向量乘加指令处理。关键参数是分块大小——块太小PIE 指令的启动开销占比高块太大寄存器不够用会溢出到栈上。我试了几组分块大小最终选的是 4x8 的块4 行 8 列。这个选择是基于 P4 的 PIE 寄存器数量和指令延迟实测出来的不一定对所有模型都最优但可以作为起点。6.2 激活函数的向量化LLM 里用到的激活函数主要是 GELU 和 SiLU。这两个函数都涉及指数运算在通用核上很慢。PIE 没有直接的指数指令但可以用多项式近似加向量指令实现。我用的近似方案是分段多项式精度损失在 1e-3 量级对最终输出质量的影响可以忽略。6.3 哪些算子不值得优化不是所有算子都值得花时间优化。我的经验是只优化占总时间超过 5% 的算子。在 LLM 推理里矩阵乘和 attention 占了 90% 以上的时间其余像 LayerNorm、残差连接这些优化收益很小。我一开始花了不少时间优化 LayerNorm后来发现它只占总时间的 2%优化到极致也就省 1%。7. KV Cache 的管理小改动大收益7.1 为什么 KV Cache 是瓶颈自回归生成时每生成一个新 token都需要用到之前所有 token 的 Key 和 Value。如果每次都重新计算复杂度是 O(n²)。KV Cache 的作用是把之前算过的 K 和 V 存下来新 token 只需要算自己的 K 和 V然后和缓存里的拼接。这个机制本身没问题问题在于缓存的分配和访问方式。我基线版本的做法是每次生成新 token 都重新分配一块更大的内存把旧的拷过去再追加新的。这个做法在 PC 上可能感觉不到在 P4 上就是灾难——每次分配和拷贝都要走内存总线而且频繁的分配会导致内存碎片。7.2 预分配加环形缓冲改成预分配之后速度立刻上了一个台阶。具体做法是根据模型的最大上下文长度一次性分配足够的 KV Cache 空间然后用环形缓冲的方式管理。新 token 的 K 和 V 写到环形缓冲的当前位置指针往前走走到头就绕回来。这个改动的收益主要来自两方面一是消除了分配和拷贝的开销二是环形缓冲的访问模式对 cache 更友好。7.3 精度换空间的取舍KV Cache 也可以用 INT8 存储能省一半内存。但 K 和 V 的量化误差会累积生成越长误差越大。我试过 INT8 KV Cache短序列128 token质量还行长序列就明显退化。最终选择是 K 用 INT8、V 用 FP16在内存和质量之间取了个平衡。8. 编译与调度最后那 25% 从哪来8.1 编译选项的实测效果编译选项这块我试过的组合和效果选项效果备注-O2基准默认-O35%循环展开更激进-O3 -funroll-loops8%手动指定展开-O3 -ffast-math12%但影响浮点精度慎用上述 LTO15%链接时优化跨文件内联最终用的是 -O3 -funroll-loops LTO没用 -ffast-math因为精度损失在长序列生成时会累积。8.2 双核分工P4 是双核但 LLM 推理的串行性很强不容易并行。我的做法是把矩阵乘的不同行分给两个核用信号量同步。这个改动的收益取决于矩阵大小大矩阵能到 1.3x小矩阵因为同步开销反而更慢。所以实际实现里是根据矩阵尺寸动态决定要不要并行。8.3 中断和任务调度的影响P4 上如果有其他任务在跑比如网络栈、UI 刷新会抢占推理任务的 CPU 时间。我的做法是把推理任务设成高优先级并且把它的栈放在片上 SRAM 里减少上下文切换的开销。这个改动对平均速度影响不大但对尾延迟改善明显。9. 实测数据与质量评估9.1 不同配置下的速度对比把各个优化阶段的数据汇总一下配置tok/s相对基线基线0.611.0x INT8 量化1.101.8x 内存布局1.652.7x PIE 算子2.644.3x KV Cache3.435.6x 编译调度4.317.1x9.2 输出质量的主观评估速度上去了质量怎么样我用了一组固定的 prompt 做对比包括问答、续写、翻译三类任务。INT8 量化后的输出和 FP32 相比在短序列上几乎看不出差别长序列256 token偶尔会出现重复或逻辑断裂。这个退化在可接受范围内毕竟速度涨了 7 倍。9.3 功耗和温度P4 跑 LLM 时的功耗比空载高不少具体数字取决于主频和外设状态。温度方面持续推理时芯片会明显发热需要做好散热。如果是在封闭环境里跑建议加散热片或者降频使用。10. 这个系列接下来会写什么这篇总览把整个优化链路和关键决策点过了一遍但每个部分都还有大量细节没展开。接下来的系列文章会按主题深入量化格式的详细对比和校准方法PIE 指令的实战用法和踩坑记录内存布局的具体实现和测量方法KV Cache 的多种管理策略对比编译选项的逐项实测数据每篇都会包含可复现的步骤和实测数据不是泛泛而谈。如果你在 P4 或者类似的 RISC-V MCU 上做推理优化希望这个系列能帮你少走一些弯路。最后分享一个我在这个项目里体会最深的事情在资源受限的平台上做优化最重要的不是知道什么技术而是知道瓶颈在哪。我见过太多人包括我自己一上来就想着用什么高级技术结果花了很多时间优化了一个根本不是瓶颈的地方。正确的做法是先测量、再优化、再测量让数据告诉你该做什么。这个原则在 P4 上适用在任何平台上都适用。