
1. 项目缘起为什么要在单片机上跑大模型第一次跟朋友聊起这个想法的时候对方的第一反应是你疯了。一块售价几十块钱的 MCU内存撑死几百 KB要跑一个动辄几百 MB 甚至上 GB 的大语言模型听起来确实像天方夜谭。但如果你真的动手去试会发现这条路并没有想象中那么不可行——前提是你得接受一个现实能跑但慢慢但可以优化。这个系列要复盘的核心就是我在 ESP32-P4 这块芯片上把一个量化后的小参数 LLM 从最初的0.61 tok/s一路优化到4.31 tok/s整整 7 倍提升的完整过程。tok/s 就是 tokens per second每秒能生成多少个词元这是衡量推理速度最直观的指标。0.61 tok/s 意味着生成一句话要等十几秒体验接近于打字机卡纸4.31 tok/s 虽然还是比不上手机上的表现但已经进入了能用的区间至少做简单的问答交互不会让人抓狂。先说清楚这个项目适合谁看。如果你是对RISC-V 架构、边缘端 AI 推理、嵌入式性能优化感兴趣的同学这个系列会非常有料如果你只是想找个现成方案直接部署那可能会失望因为整个过程充满了手工调优和反复试错。另外需要提前说明这里讨论的是量化后的小模型参数量在几十 M 到百 M 级别不是那种动辄 7B、13B 的大块头——那些模型在 MCU 上连加载都做不到。ESP32-P4 是乐鑫推出的一款高性能 MCU最大的特点是搭载了RISC-V 双核处理器主频能跑到 400 MHz还带了 SIMD 指令和一堆加速单元。相比前几代 ESP32P4 在算力上是一个质的飞跃这也是我选择它作为实验平台的根本原因。但算力提升不等于就能跑 LLM中间还有大量的工程问题要解决内存怎么分配、算子怎么优化、量化怎么做、缓存怎么利用……这些才是真正决定成败的地方。整个系列我会拆成几个部分来讲先讲清楚整体思路和方案选型再深入到具体的算子优化和内存管理然后是实测数据和调优过程最后是踩过的坑和排查经验。这一篇作为总览重点是把全局框架和核心决策讲透让你看完之后能对在 MCU 上跑 LLM这件事有一个完整的认知而不是零散的技巧堆砌。2. 整体方案设计从模型选型到推理框架2.1 模型选型为什么是 TinyLlama 级别的小模型在 MCU 上跑 LLM第一个要做的决策就是选什么模型。这个决策的约束条件非常硬内存。ESP32-P4 内部 SRAM 大概 768 KB加上外挂 PSRAM 可以扩展到 16 MB 甚至 32 MB。听起来不少但一个 FP32 的 1B 参数模型光权重就要 4 GB根本不可能。所以模型必须经过量化把每个参数的位宽从 32 位压到 4 位甚至更低。我最终选择的是一个参数量在几十 M 级别的量化模型权重用 4-bit 量化后大概占十几 MB刚好能塞进 PSRAM。这里有个关键取舍参数量越小推理越快但生成质量越差。我试过更小的模型速度快但答非所问也试过稍大的质量好一点但速度掉到 0.3 tok/s 以下完全没法用。最后这个尺寸是在速度和质量之间反复权衡后的结果。提示模型选型不要一上来就追求最强先跑通再优化。我一开始想直接上大一点的模型结果卡在加载阶段好几天后来换成小模型才把整条链路打通。2.2 推理框架为什么不用现成的而是自己裁剪市面上有 llama.cpp、ggml 这些成熟的推理框架理论上可以移植到 MCU 上。但实际试下来这些框架的抽象层太厚内存开销和函数调用开销在 PC 上无所谓在 MCU 上就是致命的。一个简单的矩阵乘法框架里可能经过好几层封装每层都有额外的内存拷贝和指针跳转累积起来就是几倍的性能损失。所以我的做法是自己裁剪一个极简推理内核只保留 LLM 推理必需的核心算子矩阵乘法、LayerNorm、Softmax、激活函数、注意力计算。每个算子都针对 ESP32-P4 的硬件特性做手工优化该用 SIMD 的地方用 SIMD该用 DMA 的地方用 DMA不做任何多余的抽象。这样做的工作量很大但换来的是对性能的完全掌控。2.3 量化策略4-bit 是甜点但细节决定成败量化是 LLM 在边缘设备上跑起来的前提。常见的方案有 INT8、INT4甚至更激进的 2-bit。我最终选的是4-bit 权重量化 INT8 激活的混合方案。为什么不是全 INT8因为 INT8 权重占的内存是 INT4 的两倍在 PSRAM 容量有限的情况下4-bit 能塞下更大的模型。为什么不是 2-bit因为 2-bit 的精度损失太严重生成质量断崖式下跌得不偿失。量化的细节很关键。简单的线性量化min-max实现容易但精度差我后来换成了分组量化每 32 个权重共享一组 scale 和 zero-point这样能更好地适应权重的分布。反量化的时候要注意不能每个权重单独反量化再算那样开销太大正确做法是在矩阵乘法的内层循环里做即时反量化把反量化的开销摊薄到计算过程中。量化方案权重内存占用生成质量推理速度适用场景FP32极大最好最慢PC/服务器INT8中等好中等手机/边缘盒子INT4 分组小可接受快MCUINT2极小差最快实验性2.4 内存布局把 PSRAM 当主战场ESP32-P4 的内存分两层内部 SRAM 快但小外部 PSRAM 大但慢。LLM 推理的内存访问模式是权重读得多、激活读写频繁所以布局策略是权重放 PSRAM激活和中间结果放 SRAM。权重的访问是顺序的、可预测的适合用 DMA 预取激活的访问是随机的、频繁的放在 SRAM 里能减少延迟。这里有个容易忽略的点PSRAM 的访问延迟比 SRAM 高一个数量级如果每次矩阵乘法都直接从 PSRAM 读权重性能会被拖垮。我的做法是用双缓冲一边用 DMA 把下一块权重从 PSRAM 搬到 SRAM一边用当前块做计算让搬运和计算重叠起来。这个优化单独就带来了接近 40% 的速度提升。3. 核心优化手段7 倍提升从哪来3.1 PIE 指令加速RISC-V 的隐藏武器ESP32-P4 的 RISC-V 核心支持PIEPacked Instruction Extension这是一组专门为 AI 推理设计的 SIMD 指令。简单说普通指令一次算一个数PIE 指令一次能算一组数理论上有 4 倍甚至 8 倍的吞吐提升。但前提是你得把代码改写成 PIE 能识别的形式编译器不会自动帮你做这件事。我做的第一件事是把矩阵乘法的内层循环用 PIE 指令重写。以 4-bit 量化为例一次可以加载 8 个权重32 位寄存器装 8 个 4-bit 数配合 INT8 激活用 PIE 的乘加指令一次性算出 8 个部分积。这一步的优化效果最明显从 0.61 tok/s 直接跳到 1.8 tok/s 左右接近 3 倍。注意PIE 指令对数据对齐有要求权重和激活的地址必须按特定边界对齐否则会触发异常或者性能暴跌。我在调试阶段因为对齐问题卡了整整两天后来写了个对齐检查的宏才定位到。3.2 算子融合减少内存往返LLM 推理里有很多读-算-写的算子链比如 LayerNorm 后面接矩阵乘法Softmax 后面接注意力加权。如果每个算子都独立执行中间结果要写回内存再读出来在 MCU 上这个开销非常大。算子融合就是把相邻的几个算子合并成一个中间结果留在寄存器里不落内存。我重点融合了三处LayerNorm QKV 投影、Softmax 注意力加权、残差连接 LayerNorm。融合之后内存访问次数减少了大概 30%速度从 1.8 tok/s 提升到 2.6 tok/s。融合的难点在于数据依赖分析你得确保融合后的计算顺序和原来完全等价不能改变数值结果。3.3 KV Cache 优化注意力计算的加速关键LLM 生成是自回归的每生成一个新 token都要和之前所有 token 做注意力计算。如果不做缓存每步都要重新计算之前所有 token 的 Key 和 Value复杂度是 O(n²)。KV Cache就是把之前算过的 Key 和 Value 存下来每步只算新 token 的复杂度降到 O(n)。在 MCU 上KV Cache 的存储和访问也是优化点。我把 KV Cache 放在 SRAM 里用环形缓冲区管理避免频繁的内存分配。访问的时候用分块加载一次加载一块 KV 到寄存器减少 PSRAM 访问。这个优化让长序列的推理速度提升了大概 25%从 2.6 tok/s 到 3.2 tok/s。3.4 循环展开与指令调度榨干流水线前面几步做完之后速度到了 3.2 tok/s但离 4.31 还有距离。剩下的提升主要来自底层微优化循环展开、指令重排、寄存器分配优化。这些工作很琐碎但累积起来效果可观。循环展开是把内层循环手动展开成几次减少循环控制的开销同时给编译器更多调度空间。我试过展开 2 次、4 次、8 次最后发现展开 4 次效果最好展开太多反而因为寄存器压力导致性能下降。指令重排是调整指令顺序让流水线尽量不空转比如把访存指令和计算指令交错排列。这部分优化从 3.2 tok/s 提升到 4.31 tok/s虽然只有 35%但每一分都是硬啃出来的。优化阶段速度 (tok/s)提升幅度主要手段基线0.61-朴素实现PIE 加速1.8195%SIMD 指令重写算子融合2.644%减少内存往返KV Cache3.223%注意力加速微优化4.3135%循环展开调度4. 实操过程从零到跑通的完整路径4.1 环境搭建与工具链配置第一步是把开发环境搭起来。ESP32-P4 用的是乐鑫的 ESP-IDF 框架我装的是 5.x 版本因为对 RISC-V 和 PIE 的支持比较完善。工具链是 riscv32-esp-elf-gcc编译的时候要打开-O3优化和 PIE 相关的编译选项。# 设置目标芯片 idf.py set-target esp32p4 # 配置 PIE 和优化选项 idf.py menuconfig # 在 Component config - ESP32-P4 Specific 里打开 PIE 支持 # 在 Compiler options 里设置优化等级为 -O3模型文件需要先转成 C 数组或者二进制格式我用的是二进制格式运行时从 flash 加载到 PSRAM。转换脚本是自己写的 Python 脚本把量化后的权重按层导出每层一个文件加载的时候按需读取。提示模型文件不要一次性全加载到内存按层加载能省不少 PSRAM。我一开始全加载结果 PSRAM 不够用后来改成按层流式加载才解决。4.2 推理内核的搭建与调试推理内核是整个项目的核心我把它拆成几个模块张量管理、算子库、调度器。张量管理负责内存分配和生命周期算子库是各种计算函数的集合调度器负责按顺序调用算子。调试阶段最痛苦的是数值对不齐。同样的输入PC 上跑出来的结果和 MCU 上不一样有时候差一点点有时候完全乱掉。排查下来发现几个原因一是量化反量化的精度损失二是浮点运算的顺序不同导致的舍入误差三是有几处内存越界写坏了数据。解决方法是逐层对比把每一层的输出都打印出来和 PC 对比定位到具体哪一层出问题。4.3 性能剖析与瓶颈定位跑通之后就是优化。优化的前提是知道瓶颈在哪我用的是手动打点计时在每个算子前后记录时间戳算出每个算子的耗时占比。结果发现矩阵乘法占了 70% 以上的时间注意力计算占 15%其他算子加起来不到 15%。所以优化的重点很明确死磕矩阵乘法。矩阵乘法的优化又分几步先看是不是内存瓶颈用性能计数器看 cache miss 率再看是不是指令瓶颈看 IPC每周期指令数。我这边主要是内存瓶颈PSRAM 的带宽不够所以后面重点做了 DMA 预取和双缓冲。4.4 实测数据与稳定性验证优化完之后要验证稳定性。我跑了 100 次推理记录每次的 tok/s算平均值和方差。平均值是 4.31 tok/s方差很小说明性能稳定。温度方面连续跑 10 分钟芯片温度稳定在 60 度左右没有过热降频。测试项结果备注平均速度4.31 tok/s100 次平均速度方差 0.1性能稳定连续运行10 分钟无异常温度 60 度内存占用PSRAM 12MB / SRAM 600KB留有余量5. 常见问题与排查技巧实录5.1 加载模型就崩溃内存不足的排查最常见的问题就是加载模型的时候直接崩溃串口打印一堆乱码或者直接重启。这通常是内存不足导致的。排查方法是先算一下模型需要多少内存再对比可用内存。如果不够要么换更小的模型要么改成分层加载。我遇到过一次诡异的情况内存明明够但加载到一半就崩。后来发现是内存碎片问题PSRAM 里有很多小块空闲内存但凑不出一块连续的大内存。解决办法是预分配一大块内存自己管理分配避免碎片。5.2 推理结果乱码数值问题的定位推理结果乱码或者答非所问通常是数值问题。可能的原因有量化参数不对、反量化公式写错、算子实现有 bug、内存越界。排查方法是逐层对比从第一层开始把 MCU 的输出和 PC 的参考输出对比找到第一个不一致的地方。我踩过的一个坑是Softmax 溢出。Softmax 里有指数运算输入稍微大一点就溢出成 inf结果全乱。解决办法是减去最大值这是标准做法但我一开始忘了调了好久才发现。5.3 速度不达预期性能瓶颈的分析速度上不去先别急着改代码先定位瓶颈。用打点计时看哪个算子最耗时用性能计数器看是计算瓶颈还是内存瓶颈。如果是内存瓶颈优化内存访问模式如果是计算瓶颈优化指令。我遇到过一次速度突然掉一半的情况查了半天发现是编译器优化等级被改了。某个头文件里有个 pragma 把优化等级降到了 -O0导致整个文件都变慢。这种问题很隐蔽建议编译的时候打开优化报告确认每个文件都是 -O3。5.4 常见问题速查表现象可能原因排查方法解决方案加载崩溃内存不足/碎片打印可用内存分层加载/预分配结果乱码数值错误逐层对比检查量化/算子速度慢瓶颈未定位打点计时针对性优化运行不稳定内存越界边界检查加保护/对齐温度过高散热不足测温降频/加散热提示调试的时候一定要打开串口日志把关键信息打出来。我习惯在每个算子前后打印时间和内存出问题的时候一眼就能看出哪里不对。6. 后续扩展方向与个人体会这个项目跑通之后我一直在想还能往哪些方向走。一个方向是多核并行ESP32-P4 是双核的目前我只用了一个核另一个核完全可以用来做权重预取或者并行计算理论上还能再快一截。另一个方向是更激进的量化比如混合精度量化重要的层用 8-bit不重要的层用 2-bit在质量和速度之间找更好的平衡点。还有一个我觉得很有意思的方向是动态批处理虽然 MCU 上一般只跑单条推理但如果能把多个短请求合并成一批矩阵乘法的效率会更高。不过这需要改调度逻辑复杂度不低。我个人在实际操作中的体会是在 MCU 上跑 LLM瓶颈往往不在算力而在内存。算力可以通过 SIMD 和指令优化来提升但内存带宽和容量是硬约束绕不过去。所以整个优化的思路应该是尽量减少内存访问算子融合、KV Cache、双缓冲本质上都是在做这件事。想清楚这一点很多优化决策就顺理成章了。最后分享一个小技巧优化之前先做基线测试优化之后再做对比测试中间不要改多个变量。我一开始贪心一次改好几个地方结果速度提升了但不知道是哪个改动起的作用后来只能一个个回退重测浪费了很多时间。一次只改一个变量虽然慢但每一步的提升都清清楚楚最后复盘的时候也有据可查。