资讯动态

ESP32-P4 上跑 LLM:从 0.61 到 4.31 tok/s 的七倍性能优化实录

发布时间:2026/10/8 7:40:08 来源:尧图企业网站定制
第一次在 ESP32-P4 上把 LLM 跑通的时候我盯着串口终端看了快半分钟屏幕上才歪歪扭扭蹦出第一个完整的词。生成速度 0.61 tok/s这个数字当时给我的感觉就四个字不可用纯纯的不可用。但这并不意味着这条路走不通恰恰相反它说明我已经站上了一个很有意思的起点。后面我花了三周左右的时间从量化方式、算子写法、内存布局到双核调度一路调下去最终把同一个模型的 token 生成速度从 0.61 tok/s 拉到了 4.31 tok/s差不多 7 倍提升。这件事值得完整复盘而且一篇根本装不下全部细节所以这是系列的第 00 篇定位是总览和路线图。我会先讲清楚整个优化项目的起因、硬件选型、第一版基线是怎么搭的、7 倍从哪来以及后面每一篇准备深挖什么。如果你也是想在小硬件上跑 LLM 的人或者正在被边缘侧推理性能折磨这篇文章应该能给你一个完整的坐标系。1. 为什么非要在 MCU 上跑 LLM项目起点与硬件选型1.1 边缘设备跑大模型到底图什么先回答一个绕不开的问题都 2025 年了随便一台手机或者迷你主机都能跑几十亿参数的小模型为什么还要折腾 MCU我自己的理由很简单有些场景压根不允许带一块完整的 Linux 板子。比如一个装在工业现场的小盒子要求 5V 供电、被动散热、延时稳定还得能离线工作。如果你在等一个需要联网翻译的嵌入式设备网络动不动断一下体验就会非常像卡在 0.61 tok/s 的那个初版固件。而把 LLM 直接塞进 MCU意味着推理在本地完成不依赖云端也没有网络延迟的问题。隐私也是一个很实在的考量。在端侧推理原始数据可以不出设备。对于很多采集现场数据、处理用户语音的硬件产品来说这比把数据传回服务器再删掉要让人安心得多。当然MCU 算力有限跑不了 7B 甚至 1B 的大模型能跑的都是压缩到极限的小模型。但小模型也是 LLM也能做文本生成、关键词抽取、意图识别这类很有价值的事情。关键是找到合适的硬件然后把性能压榨到能用的水平。1.2 为什么选了 ESP32-P4而不是树莓派或者手机选型的时候我其实对比过好几条路线树莓派 Zero、手机旧芯片、带 NPU 的开发板还有乐鑫的 ESP32 系列。树莓派 Zero 在软件生态上有绝对优势但功耗和体积摆在那里而且作为完整 Linux 系统启动要好几秒实时性也不可控。手机旧芯片就更别提了光是把屏幕和触摸从启动链里剥掉就够折腾一阵。ESP32 系列的优势在于功耗低、启动快、外设丰富、可以在裸机或者 RTOS 上跑而且社区里搞 AI 推理的人非常多。尤其是 ESP32-P4它和之前的 S3 完全不是一代产品。S3 着重在 Xtensa 核和向量指令上做文章而 P4 换成了双核 RISC-V 高性能核心主频更高同时引入了面向 AI 计算的向量扩展和矩阵加速逻辑。配合大容量 PSRAM 和高带宽的存储接口它实际上已经具备了一个微型 AI 推理盒的潜质。对比一圈以后我的判断是ESP32-P4 是当前 MCU 品类里把性能上限和功耗底线平衡得最好的一档。它的浮点能力不强但 LLM 推理的瓶颈本来就不在浮点算力上而在内存带宽和矩阵乘法的数据搬运效率这一点 P4 的设计明显考虑了。1.3 这套方案适合谁参考这个项目适合三类人嵌入式工程师想在 MCU 上跑 LLM 或者类 Transformer 模型做边缘 AI 产品的人需要评估小模型在端侧到底能做到什么水平以及纯粹好奇0.61 到 4.31 到底是怎么挤出来的的调优爱好者。如果你是第一种后面整个系列都值得跟如果你是第三种这篇总览里的分阶段表格应该就能满足大部分好奇心。2. 第一版能跑但只有 0.61 tok/s基线是怎么搭的瓶颈出在哪2.1 第一版架构逐层读 flash逐层算先说一下初始方案。我拿到 ESP32-P4 开发板以后直接用了社区里已有的 llama.cpp 嵌入式移植分支。模型选的是一个 100M 参数量级的开源小模型第一版用 int8 量化整个权重文件打包后是一百多兆字节。这里要说明一个物理约束开发板上的 PSRAM 虽然比普通 MCU 大得多但要一次性装下整个 int8 权重文件仍然不现实。所以第一版的做法非常笨每一层 transformer 计算之前先从 flash 里把这一层的权重读进 PSRAM 的一个临时 buffer算出结果然后丢掉 buffer再读下一层。听起来很笨但这是绝大多数 MCU 上跑大模型的默认做法因为内存实在装不下。问题也是很明显的flash 读取是串行的、慢速的整个推理周期里有相当长的时间在等权重从 flash 挪进内存。第一次编译烧录后我测到的速度就是 0.61 tok/s。生成一句 30 个字的回答大概要等快一分钟属于你盯着屏幕都能感受到它在挣扎的状态。2.2 用 profiler 打点找到真正的瓶颈拿到 0.61 这个数字以后我没有急着改代码而是先做了 profiling。方法很简单在推理主循环的各个阶段打时间戳分别统计flash/PSRAM 权重读取和搬运耗时GEMV 矩阵-向量乘法耗时LayerNorm、激活函数、Softmax 等小算子耗时采样和 tokenizer 耗时。打点结果让我很意外权重读取/搬运占到了接近 70%矩阵乘法计算本身只占了 20% 左右剩下的是各种小算子和采样开销。这个比例说明一个问题第一版根本没有在计算而是在等数据。解码阶段每个 token 都要把所有层跑一遍每层都要把权重从外部存储读进内存这个搬运操作是绝对的大头。我还算了一下理论极限int8 权重大概一百多兆以 0.61 tok/s 来算每秒从 flash 读取的权重也只有六十多兆字节。这个速度和 ESP32-P4 的存储接口上限差得很远说明问题不在接口带宽本身而在于读取模式太差、搬运和计算完全没有重叠。到这里我就心里有数了这不是算力不够的问题是数据搬运和计算编排的问题。优化空间至少有一个数量级7 倍的目标完全可以定。2.3 为什么会是 memory-bound而不是 compute-bound在继续之前我想把LLM 推理是 memory-bound这句话解释清楚这是整个优化的理论基础。Transformer 的解码阶段核心操作是矩阵-向量乘法GEMV也就是一个权重矩阵 W 乘一个当前的 token 向量 x。注意batch size 基本等于 1因为每次只生成一个 token。权重矩阵非常大而向量只有一列。这种情况下计算设备需要把 W 的每个元素从内存搬进寄存器然后做一次乘加。搬运数据的时间远大于做乘加本身的时间。可以打一个比方你要从仓库里把一万本书逐本搬到桌上然后每本书翻一页记下里面的一个数字。哪怕你翻书的速度再快总时间也主要由搬书决定翻页的耗时可以忽略不计。GEMV 就是典型的搬书场景所以优化方向必须是让书变得更轻量化、让搬书的动作和翻书的动作重叠流水线、让每次搬书尽可能高效内存对齐和突发读取。3. 7 倍性能提升的实际路径五个阶段逐一拆解优化不是一步到位的我把它拆成了五个阶段。每个阶段都相对独立且复用性很强。下表是总览后面会逐一解释每个阶段背后的思路。阶段关键改动实测 tok/s相对上一阶段提升基线llama.cpp 移植分支、int8、逐层串行读取0.61-阶段 1切换到 Q4_K_M 量化权重体积减半1.23约 2.0 倍阶段 2用 P4 向量指令重写 GEMV 内核1.78约 1.45 倍阶段 3双核流水线一个核预取权重一个核计算2.90约 1.63 倍阶段 4PSRAM 访问对齐、缓存策略、编译选项优化3.97约 1.37 倍阶段 5LayerNorm/激活函数等小算子融合进主循环4.31约 1.09 倍五个阶段叠乘下来正好接近 7 倍但这不是简单的乘法关系有些阶段的收益会和后面阶段相互影响。下面拆开讲。3.1 阶段 1量化减负为什么白捡的是最干净的两倍第一个动作是换量化。初版用的是 int8权重一百多兆。我把模型转成 Q4_K_M 格式之后权重体积直接降到六七十兆读取量几乎减半。为什么说这个收益最干净因为 memory-bound 场景下的耗时和读取数据量近似线性相关。权重小一半每次搬运的数据就小一半速度自然接近两倍。实测从 0.61 到 1.23基本符合预期。当然实际提升没有正好 2 倍原因有几个量化格式本身带一些元数据非权重部分的体积没有按比例缩减反量化在计算时需要额外处理以及 flash 搬运的固定开销仍然存在。但方向非常明确先把模型压到最小再来谈算得快。这里要特别提一句 Q4_K_M 而不是更激进的 Q4_0是我做完精度对比后的选择。Q4_0 能再小一点但生成的文本明显开始出现重复词和语义漂移。Q4_K_M 对关键层和 attention 部分保留了更高精度体量只多了几个百分点但生成质量稳很多。在做完这个切换后我也顺手把 embedding 表和 lm_head 单独留成了高精度格式因为这些部分的误差会被放大到所有输出 token 上。这个细节后面系列里会专门展开。3.2 阶段 2重写 GEMV把标量循环改成向量指令量化做完以后计算部分的占比相对升高了这时候再优化算子就很有价值。我最初用的 GGML 内核是通用版本在 ESP32-P4 上用纯 C 标量循环实现。由于 GEMV 每次要读取连续权重和单列向量做点积通用实现里有大量分支判断和低效的内存访问模式。我把矩阵的存储顺序改成更适合逐行点积的布局然后针对 P4 的向量扩展指令重写了内层循环。重写后一次循环能处理原来好几轮标量循环才能完成的数据读取也更连续。这一轮实测只提升了约 1.45 倍没有阶段 1 那么夸张但它为后面的流水线打下了基础如果 GEMV 算子还是一团乱麻双核并行时的计算耗时就没法和预取耗时做好的时间搭配。这一轮最核心的收获不是那一行 vector 指令本身而是我彻底理解了 GEMV 的每次读取都是可以预测的权重是顺序的向量是固定的这种模式天然适合提前预取和突发传输。知道这一点之后后面阶段的优化方向就非常清楚了。3.3 阶段 3双核流水线把等待变成计算P4 有两个高性能 RISC-V 核心第一反应自然是用两个核一起算。但实测很快给我泼了冷水两个核同时做 GEMV内存总线瞬间饱和两边互相抢带宽总耗时几乎没有减少。正确的做法是流水线而不是并行计算。我的方案是双缓冲加生产者-消费者模型核心 A 负责从 flash/PSRAM 读取下一层的权重填进一个 ready buffer核心 B 负责用当前层的权重做 GEMV 和其他算子两层之间通过事件标志切换 buffer避免一方在等另一方。这个设计把之前 70% 的等待时间拆出来和计算时间重叠了。核心 B 还在算当前层的时候核心 A 已经在预取下一层了。到了稳态以后每层的耗时几乎等于 max(读取时间, 计算时间)而不是读取时间加计算时间。实测从 1.78 到 2.90这 1.63 倍基本就是从浪费掉的等待时间里捞回来的。这里有一个非常容易踩的坑预取不能一次性把整层权重都搬进 buffer因为 PSRAM 空间本身很紧张。我的方案是按层内 block 粒度来预取这一层的矩阵被切成多个片段核心 A 不断填满一个固定大小的环形缓冲核心 B 从里面消费。粒度太粗会导致切换频繁粒度太细则同步开销变大。最后调下来我选了单个 block 大小在 4KB 到 8KB 之间性能最稳。3.4 阶段 4内存对齐和缓存策略把搬书姿势练到最优流水线跑通以后2.90 tok/s 看似不错但我总觉得还有余力。profiling 显示核心 B 在等核心 A 补数据的时间仍然存在说明预取还不是完全平滑。问题出在内存访问模式上。ESP32-P4 的 PSRAM 走的是高速接口但如果你访问的起始地址没有对齐或者读取长度不是突发传输的整数倍实际带宽会明显打折。我的第一版 buffer 分配没有刻意做对齐导致每次 DMA 传输都可能多花不少周期。这一阶段的改动包括所有 buffer 地址按 32 字节对齐分配flash 读取长度统一调整为突发传输的倍数把 P4 的缓存命中策略调成更适合顺序读的模式。另外我把 embedding 表和 lm_head 这两块访问频率极高的权重直接常驻在 PSRAM 的常驻区因为它们体积不大但每次 token 都要用从 flash 流式读它们太亏了。这些改动单独看都不大加起来却到了 3.97 tok/s提升 1.37 倍。核心原因很简单带宽一直就在那里之前的读取姿势太差没有把接口的每个时钟周期都用满。3.5 阶段 5小算子融合最后 8% 的细节价值最后一个阶段是我一开始不太看好的把 LayerNorm、激活函数、Softmax 这些小算子融合进 GEMV 的主循环。在原始实现里每个算子都会从内存读一遍输入张量写一遍输出张量然后下一个算子再读一遍。Transformer 的中间张量虽然不大但 MCU 上的内存带宽本来就不宽裕这些中间读写都在白耗时间。融合以后LayerNorm 的参数直接喂给向量计算单元GELU 激活函数在寄存器里就地完成不落回内存Softmax 和之前一步的乘加操作合并处理。中间张量的创建和销毁大幅减少。这一阶段把 3.97 推到了 4.31相对提升只有 9%但累计到这一步4.31 tok/s 已经让日常的短文本生成变得可以忍受了。4. 调优过程中踩过的四个坑现象、定位与解决4.1 DMA 搬运偶发校验失败罪魁祸首是地址没对齐第一次跑通流水线的时候系统并不稳定大概几十次推理会出现一次莫名其妙的输出错乱。排查了很久最后发现是预取 buffer 的地址没有对齐DMA 在传输时跨了边界导致部分数据被截断或覆盖。解决方式很简单在分配 buffer 时强制按 32 字节对齐并且将单次传输长度设成突发传输的整数倍。这个问题之后再也没有出现过。4.2 量化切到 Q4_K_M 后偶尔整段输出重复量化切到 Q4_K_M 以后速度是上去了但有时候生成文本会出现死循环式重复比如一整段都在重复同一个词。这个现象一开始我以为是模型本身的问题后来把 embedding 层和 lm_head 单独提出来用高精度格式保存重复现象立刻消失了。原因其实很好理解embedding 表的每一个向量都会影响所有后续 token 的语义方向lm_head 的误差会直接作用在最终采样的概率分布上。这两处对量化误差最敏感必须保留更高精度。这个教训对任何 LLM 端侧部署都有参考意义。4.3 双核互等导致死锁卡在上一个 token 正常这一个卡死双核流水线最头疼的问题就是同步。有一版代码出现了非常诡异的现象前一个 token 输出完全正常下一个 token 就永久卡住没有任何报错。查了很久才发现是环形缓冲的事件标志逻辑写错了两个核在某些时刻会互相等待对方释放同一个信号形成经典死锁。解决办法是给同步逻辑加上超时计数并且把事件顺序统一成生产者先更新写索引消费者再更新读索引从设计上消除互等。4.4 供电不足导致频率降档tok/s 从 4 掉到 2有一段时间我用电池给开发板供电发现实测速度上下浮动很厉害最差的时候只有 2.4 tok/s 左右。开始我以为是软件问题后来示波器一量P4 满载运行时电压跌落明显芯片自动降频保护了。换用稳压电源后速度立刻回到正常水平。这提醒我在 MCU 上做性能优化硬件供电同样是需要关注的一环尤其是双核满载 flash 持续读取的场景瞬时功耗比想象中大得多。5. 后续系列文章怎么规划一张路线图这次复盘的细节量非常大一篇总览不可能全部装下。我按照自己调优的顺序把后续系列文章做了如下规划5.1 01 篇环境搭建与第一版基准复现这一篇会讲清楚开发环境、工具链版本、如何把社区移植分支跑起来以及如何复现 0.61 tok/s 的初始基线。如果你有一块 ESP32-P4 开发板跟着这一篇就能搭好实验环境为后续优化打下基础。5.2 02 篇量化路线全景对比int8、Q4_0、Q4_K_M 等不同量化格式在 ESP32-P4 上的速度和生成质量对比包括 embedding 层精度保留的具体操作以及如何在项目里自动选择每层的最优量化方式。5.3 03 篇GEMV 算子底层实现详解从标量循环到向量指令的完整重写过程包括矩阵存储布局、循环展开、向量寄存器的使用方式以及不同参数下 GEMV 内核的性能差异。这一篇偏底层但对想做深度优化的人来说最有价值。5.4 04 篇双核流水线实现细节环形缓冲的大小选择、事件标志的同步逻辑、双核任务怎么分工以及如何在正确的粒度上切分权重数据让预取和计算尽量接近无缝衔接。5.5 05 篇端侧 LLM 的效果评估与微调思路硬件优化到最后还是要回到模型生成的东西能不能用这个问题上。这一篇会聊如何在端侧评估小模型的文本质量、如何选择更适合硬件的小模型以及是否需要做轻量微调来补偿量化带来的精度损失。写在最后的一点经验项目走到这一步我个人最大的感受是MCU 上跑 LLM 的瓶颈从来不是算力而是搬运。只要把数据读取和计算编排成流水线把权重压到最小速度的提升是实打实的。如果让我只留一条建议给你拿到任何板子先算清楚模型量化后的体积和目标 tok/s然后估算所需的内存带宽对比一下硬件的接口上限。只要带宽还没到上限就说明还有优化的空间如果已经接近上限那就得考虑换更小的模型或者更激进的量化了。这个系列还会继续更新我会把每一层的实现细节和踩坑记录都写出来。希望这篇总览能帮你建立一个完整的视角少走我走过的弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑