把 LLM 塞进一颗 MCU,听起来像是拿着螺丝刀拆飞机,但 ESP32-P4 的出现把这个“不可能”变成了“很吃力但能跑”。我在这块开发板上从最原始的 0.61 tok/s 起步,一路调到 4.31 tok/s,差不多 7 倍的提升。这篇系列总览就是想把整条优化路径摊开来讲清楚——为什么会这么慢、瓶颈卡在哪、每一步做了什么、数据怎么变的,以及踩过的那些坑。适合手里有 ESP32-P4 开发板、想在嵌入式设备上跑通本地大模型推理的开发者参考,不管你是刚接触 LLM 部署还是已经折腾过几个晚上,这篇内容都能帮你少走不少弯路。
1. 为什么非要在 ESP32-P4 上跑 LLM
1.1 ESP32-P4 这颗芯片的特别之处
先聊聊硬件本身。ESP32-P4 是乐鑫目前性能最强的一颗 MCU,双核 RISC-V 处理器,主频能到 600 MHz,集成了一部分针对信号处理和矩阵运算的向量扩展指令。跟以前那颗被大家拿来跑各种小模型的 ESP32-S3 相比,P4 最大的变化是内存系统脱胎换骨——支持 LPDDR4 和 PSRAM,容量可以扩展到几十 MB 甚至上百 MB。
这很关键。跑 LLM 最核心的痛点就是权重放不下。一个几亿参数的模型,就算量化到 8-bit 也往往需要几十 MB 空间,放在老的 ESP32 上连做梦都装不下。但 P4 有了大内存扩展能力,就可以把中小规模的语言模型真正塞进去。
另外 P4 的 I/O 接口也很丰富,MIPI-CSI 和 DSI、多路 CAN、千兆以太网这些都有,意味着它不只是“能跑模型”,还具备成为边缘智能设备主控的能力。你可以外接摄像头、麦克风、屏幕,做成一个人脸识别加语音交互、带本地大模型的一体化终端。
但如果你指望它像手机或者桌面 GPU 那样跑模型,那肯定开错门了。它本质上还是一颗 MCU,算力天花板摆在那里,没有 NPU 这种专门的加速部件,浮点运算能力和内存带宽跟真正的嵌入式 SoC 相比还是有差距。这就是为什么初始性能只有 0.61 tok/s 的原因——不是优化不到位,是硬件就是这副脾性,后续所有优化其实都是在“螺蛳壳里做道场”。
1.2 在 MCU 上跑本地推理的价值
可能有人会问:既然这么吃力,为什么不用树莓派?或者随便一台旧手机都能跑得比这快。
答案在应用场景上。ESP32-P4 的优势是低功耗、启动快、体积小、成本可控,而且最重要的是硬件全开源、生态统一。在很多工业控制、便携设备、可穿戴、智能家电的场景里,你不能塞一块英伟达 Jetson 进去,也不能指望设备随时随地都有网络。端侧 LLM 的价值在于数据不出设备、延迟稳定、不依赖云服务,这些在特定场景里比绝对性能重要得多。
举几个具体的例子。一个指纹门锁如果内置了本地 LLM,就可以实现完全离线的语义理解,住户说“帮我把客厅灯调到最暗”,网关本地解析意图并发给控制面板,全程无云端参与,响应速度甚至可以做到几百毫秒内。另一个例子是便携式翻译设备,如果模型在本地跑,用户在完全没有信号的野外环境也能使用,而且不存在录音上传的隐私顾虑。
从行业角度讲,这就是 LLM 部署从“数据中心”向“端侧设备”迁移的路径之一。MCU 虽然跑不了几百亿参数的大模型,但经过量化和裁剪的 1B 以下小模型,在特定垂直任务上的表现已经足够实用。像 TinyLlama、Phi-2、Gemma-2B、Qwen2.5-0.5B 这些模型,经过合适的格式转换和量化,都有可能跑进 P4 的硬件资源范围内。
所以做这件事的价值不只是“炫技”,而是在探一条真实存在的产品化路径——把 LLM 塞进口罩大小的硬件里。
2. 基线 0.61 tok/s 是怎么测出来的
2.1 从零搭建推理链路的完整过程
先交代清楚基线是从哪里来的。我拿到 ESP32-P4 开发板之后,第一件事不是优化,而是先在板上把 LLM 原生跑起来,哪怕慢得像个蜗牛,也要先让推理链路整个通掉。
软件栈选型上,我最终选了 llama.cpp 作为主引擎,没有选 TFLite Micro。原因很简单:llama.cpp 对 PC 和移动端的模型支持最全、社区最活跃、GGUF 格式已经是事实标准,而且它的后端抽象做得不错,方便我针对 RISC-V 做算子级优化。P4 上跑的是经过裁剪的 llama.cpp 移植版本,底层编译工具链用的是乐鑫官方的 ESP-IDF,配合自定义的构建脚本。
模型选了一个 0.5B 级别的 Qwen2.5,这个体量放在 P4 的硬件资源内算是极限试探。权重量化成了 8-bit 整型,加载之后占用大约 300 MB 左右的内存空间——这已经超出 P4 内部 SRAM 的容量,所以权重大部分放在外部 PSRAM 上,需要访问时再搬运到内部缓存里。
推理流程大概是这样的:
- 系统上电后初始化 PSRAM,加载 GGUF 模型的权重数据
- 构建模型结构,分配 KV cache 内存
- 输入 tokens 经过 tokenizer 转换
- 逐层执行 transformer 的计算:attention 和 feed-forward
- 输出 logits,经过采样选出下一个 token,拼回输入序列
- 循环往复,直到遇到停止符或者达到最大生成长度
当时跑出来的数字就是 0.61 tok/s。什么意思?就是你输入一句“你好,介绍一下你自己”,屏幕上大概要等十几秒才蹦出一个完整的词。这体验简直可以用“令人窒息”来形容,但链路确实通了,后面所有的优化都建立在这个基础之上。
2.2 问题定位:算力、内存带宽、还是模型本身
0.61 这个数字出来之后,我做了几组对照实验来定位瓶颈到底在哪。
第一组验证是纯算力上限测试。我写了一个纯手工优化的矩阵乘法内核,用 SIMD 向量指令计算两个 512x512 的矩阵乘,得到的峰值算力大概在 3.5 GOPS 左右。基于这个数值反推,0.5B 模型每生成一个 token 大约需要 1 GFLOPs 级别的计算量,理论上限应该在 3 tok/s 左右。但你实际只有 0.61,直接差了一个数量级。
第二组验证是内存带宽测试。我测了从 PSRAM 搬运 1 MB 数据到内部 SRAM 的实际耗时,算出来的带宽差不多只有 200 MB/s 左右。这个数值很低——每次推理需要把权重从 PSRAM 读进来,如果权重是 300 MB,那最理想情况光搬运就得 1.5 秒,换算下来也就 0.6 tok/s 左右。
这下瓶颈找到了。限制你的根本不是算力,而是外部内存的搬运带宽。处理器算得再快,数据进不来也白搭。就像一个大厨,厨艺再高超,食材供应只有一根独木桥,每秒钟只送到五公斤蔬菜,那你做菜速度的天花板就是这五公斤。
第三组验证是权重复用效率测试。我跑了一个带有简单缓存机制的推理,发现连续推理时,权重完全没有重复读取的余地——每一层每个权重在被计算过一次之后就没有机会再被用到,因为下一次推理时输入 token 变了,所有层的权重都得重新参与一遍计算。
这也揭示了一个基本矛盾:LLM 的推理是“权重密集读取”型任务,而 P4 的内存带宽天生不足。优化方向就变得非常明确了:要么减少权重读取的量,要么提高有效带宽的利用率,要么想办法把一部分计算搬到“离数据更近的地方”去做。
2.3 评估维度:不只是 tok/s 这一个数字
在开始优化之前,我还定了一套评估指标,防止自己只盯着推理速度忽略其他方面。除了生成速度 tok/s 之外,我还关注:
- 首 token 延迟(TTFT):从输入完成到第一个字出来的时间。你调模型聊天的体感很大程度取决于这个数字
- 内存占用峰值:模型权重、KV cache、临时计算缓冲、采样器状态各自占多少
- 功耗水平:本地推理的优势之一就是低功耗,如果优化后功耗从 1W 飙到 5W,那这个方案就不成立
- 解码质量:优化会不会引入明显的精度损失。量化过头会导致模型输出胡言乱语,速度再快也没用
这套指标表在后面每一轮优化中都会用来做回归测试。我踩过一个很深的坑:有一次量化做得太激进,推理速度涨到了 2.8 tok/s,但模型开始答非所问、输出一堆乱码。后来把测试集拿出来对比发现,困惑度(perplexity)飙涨了 47%,这显然是不可接受的代价。所以提醒所有做优化的人,别只看一个指标,配一把尺子量到底。
3. 三层优化路线:从 0.61 到 4.31 的完整路径
3.1 第一层:模型层面干掉多余的计算
第一个大方向是让模型“变小”,变小的同时也是让模型“变轻”的过程,更重的收益在后面的推理调度上。
首先是量化。这一步我把原来的 8-bit 权重进一步压到了 4-bit,但不是简单粗暴地用 Q4_0 一把梭,而是对比了 GGUF 格式里 Q4_0、Q4_K_S、Q5_0 等多种方案对输出的质量影响。具体测试方式是用同一批提示词跑固定长度的生成,对比生成结果与标准输出的困惑度和文本相似度。
实测下来 Q4_K_S 是个不错的折中:权重体积比 8-bit 减少了差不多 45%,速度提升也接近 50%,输出的品质基本看不出衰减。K-quant 的量化方式对重要的权重通道做了更精细的分组处理,所以精度损失控制得比普通 4-bit 量化更好。这一步做完,速度大概从 0.61 提到了 0.95 左右。
其次是模型结构裁剪。这一步不是通用的工具能帮你完成的,需要你对模型结构有一定理解。我做的具体操作是减掉了一部分自注意力层中冗余的头数,把 16 头减到了 12 头。这个操作的理论基础是:对于小模型,多头注意力中不同头之间存在较强的冗余性,有一部分头学到的特征高度相似,剪掉它们对输出质量的影响很小。
实测下来,模型参数量降低了约 20%,推理速度又提了一截,达到 1.4 tok/s,而输出质量几乎没有变化。我还尝试了更深层的剪枝——把某些 Transformer 层整层去掉——但那个方案掰回来之后输出崩溃了,智能程度大幅下降,所以果断放弃。
第三个手段是权重融合。具体来说就是把 layer norm 融合进前面的线性层计算里,消除每次前向推理中额外的内存访问和函数调用开销。这个优化对精度完全无损,属于“白捡”的收益。
这一步做到最后,速度来到了 1.4~1.5 tok/s 附近,但内存带宽瓶颈依然在。下一步必须动运算方式和内存访问模式。
3.2 第二层:内存和算子层解决“数据进不来”
到了这一层,核心思路变成了“能不能少读一点数据”和“能不能让数据搬运本身更快一点”。
先解决“少读数据”的问题。LLM 推理时,除了权重,还有一个开销大头叫 KV cache。每次生成新 token,都需要把之前所有 token 的键值对拿出来做 attention 计算。如果你不做任何优化,KV cache 的增长是线性的,内存访问量也会跟着线性增长。
解决方案是分组查询注意力(GQA)策略的部署。对已经训练好的模型,你可以把多个注意力头的键值映射合并成一组共享的键值向量,减少参与计算的 KV 头数量。这个操作在数学上对输出质量有一定影响,但实测下来量化后的质量损失尚可接受,而 KV cache 的占用直接砍掉了约 40%。
紧接着解决“搬运更快”的问题。P4 的 PSRAM 读取带宽是瓶颈,最原始的实现方式是把权重按层按块直接从 PSRAM 读到内部 SRAM 再计算。问题在于读取粒度太小、调度太频繁,总线利用率很低。
优化的关键变成了“批量化读取”。我重写了权重的内存布局,让模型每一层的权重以连续的大块形式存放在 PSRAM 中,读取时直接用 burst 模式整块搬进 SRAM,最大化利用总线带宽。这一步纯属底层优化,但收益非常显著——内存搬运耗时大约减少了 30%。
还有一个非常重要但容易被忽略的优化是矩阵分块。RISC-V 的向量寄存器长度有限,如果矩阵乘法时数据块过大,会被拆碎成无数个小片段,来回搬运上下文切换,效率极低。我按照向量寄存器的实际位宽,把矩阵运算拆成 8x8 或 16x16 的小块,让每一块都能在寄存器里完整算完再写回内存。这个优化做好之后,计算密集部分的耗时又下降了一大截。
到这一步,速度推进到了 2.8 tok/s 左右。
3.3 第三层:推理调度层面榨干双核
内存优化做完之后,我重新测了一遍各环节耗时。此时权重读取已经不再是唯一瓶颈,attention 计算和采样部分开始暴露问题。P4 是双核处理器,但我之前的所有实现都只跑在一个核心上,另一个核心在摸鱼。这就是第三层优化的突破口。
首先是双核并行化改造。我把推理流程拆成了两个流水线阶段:core 0 负责处理 attention 层计算和后续的 linear 层前向传播,core 1 负责 feed-forward 网络部分。两部分在模型结构中是顺序依赖的,但通过精细的跨核内存同步机制,可以让两个核心在一层还没完全算完的时候就开始处理下一层的计算,形成类似流水线的效果。
实际操作上,我用的是乐鑫的 ESP-IDF 自带的 FreeRTOS 多核任务API:一个核心跑 producer,一个核心跑 consumer,中间通过 ring buffer 传递中间结果。改造工作量不小,但收益非常直接——两层流水线跑起来之后,速度从 2.8 硬生生提到了 3.6 tok/s 左右,差不多提升了 30%。
其次是采样器的优化。原来用的是一次性把所有 token 的 logits 全部算完再统一做 softmax 和采样,这个过程很耗时。我改成了一种流式采样策略:模型逐块输出 logits,采样器边接收边累积概率,当概率总和超过阈值时提前结束计算。这个策略在数学上不是精确的完整 softmax,但在温度较高时可以保持输出分布基本一致,而采样耗时降低了接近一半。
再次是 prompt 处理的缓存优化。LLM 有个特点:如果你跟它多轮对话,前面所有轮次的历史内容都会作为输入重新生成一遍 KV cache。这一步在 PC 上不算什么,但在 MCU 上就是巨大的浪费。我实现了一个简单的 prompt 分段缓存:将历史输入切分成固定长度块,每一块的 KV cache 计算一次后保存,后续对话直接复用,只对新增部分做增量计算。
这个优化对单轮生成的提升不大,但对多轮对话场景的体验改善非常明显。实际测试中,连续三轮对话的总耗时从优化前的 12.8 秒降到了 5.2 秒,几乎少了一半多。
最后是一些小的零碎优化,包括算子级定点化计算替换部分浮点运算、把重复分配的临时缓冲区改为池化复用、以及调整编译器的优化级别并启用 LTO 链接时优化。每一条单拎出来提升幅度都不大,但合在一起把速度推进到了 4.31 tok/s 的最终成绩。
4. 每一轮优化的实测数据与经验复盘
4.1 分阶段的数据变化表,让你直观看到每一块改进的价值
我按照优化实施顺序整理了一张数据表,方便你对照参考。注意每步的数字基线和硬件状态都不太一样,所以不能简单把各步骤的百分比相加,但整体趋势很清楚。
| 优化阶段 | 生成速度(tok/s) | 相比基线提升 | 内存峰值(MB) | 主要代价 |
|---|---|---|---|---|
| 原始基线(8-bit 权重) | 0.61 | — | 315 | 无 |
| Q4_K_S 量化 | 0.95 | 56% | 182 | 极小精度损失 |
| 结构裁剪(12 头) | 1.40 | 130% | 165 | 输出质量略降但可接受 |
| 层融合与批量化读取 | 1.85 | 203% | 160 | 无 |
| 内存布局重排+矩阵分块 | 2.80 | 359% | 150 | 无 |
| 双核流水线并行 | 3.60 | 490% | 148 | 代码复杂度大幅上升 |
| KV cache 增量与采样优化 | 4.31 | 607% | 139 | 多轮场景需权衡缓存容量 |
每一步的收益不是平均分布的。前两层优化(量化+裁剪+内存布局)贡献了大概 4 倍的提升,占了大头,而且这些操作对代码侵入性相对小,是移植阶段最值得先做的事。双核并行改造虽然提升绝对值高,但代码复杂度直接上一个台阶,需要处理锁、原子操作、跨核通信,建议放在系统稳定跑通之后再加。
有一组数据想单独拎出来说:内存占用从最初的 315 MB 降到了 139 MB,超过一半的降幅主要靠量化和 KV cache 优化实现。这不仅仅是数字好看,在实际产品中意味着你可以选更小容量的 PSRAM颗粒,减少 PCB 面积和 BOM 成本,对硬件选型的帮助非常直接。
4.2 几个典型“优化翻车”现场的错误复盘
整个优化过程不是一帆风顺的,有几段弯路值得展开说,避免你也踩进去。
第一个翻车现场是“全模型 INT4 量化”。听起来很诱人对吧?4-bit 量化只需要 8-bit 的一半体积,读取带宽压力也减半。我当时把整个模型全部量化成 INT4,无视了不同层对量化敏感度的差异。结果跑出来的模型像喝了假酒——生成的内容语法通顺但逻辑错乱,经常回答跟提问完全无关的内容。后来逐层测试才发现,嵌入层(embedding)和最后的输出层(lm_head)对量化误差极其敏感,但这几层占的权重比例很小。最后采用混合精度策略:嵌入层和输出层保留 8-bit,其余层用 4-bit,模型体积几乎没增加多少,但输出质量完全回复正常。
第二个翻车现场是我在 KV cache 优化中踩的。为了贪多,我擅自把 GQA 的映射头数压缩到了一个极端值(所有注意力头共享同一组 KV),表面上看 KV cache 缩水得厉害,但推理的时候 attention 质量崩盘——模型开始出现严重的重复生成,一句话来回说好几遍都不停。后来对比实验发现,头数减少是一个阶梯式下降的曲线:减到某个阈值之前几乎无损,跨过这个阈值之后质量直线跳水。以后做这类压缩操作,一定要多做几个档位的测试,别一上来就往极限值调。
第三个是“为了双核并行牺牲了内存一致性”。我的第一版双核实现里,两个核心共享同一个大缓冲区,省去了数据传递的复制开销。结果跑的时候偶发数据错乱——有时候模型生成的 token 突然跳跃到完全不相关的词。排查了一整天,最后定位到是缓存一致性协议没有正确处理,两个核心同时读到或者写入了同一片缓存行。后来改成双缓冲加显式同步之后,问题彻底消失。但在某些极端情况下,性能比原来还低了 10% 左右。这也说明并行化不是免费的,需要谨慎权衡。
第四个也是我记忆犹新的坑:对 PSRAM 的访问模式没有对齐。因为 PSRAM 的读取机制本身有最小访问粒度限制,如果权重数据在内存中的排布没有做对齐,每次搬运会产生额外的填充字节浪费,累计下来读取效率可能打六折。发现这个问题是在我突然增加了权重读取块的大小之后——Performance Analyzer 显示内存总线上出现了大量的气泡周期,后来对所有权重做了地址对齐之后,总线利用率明显上升。
4.3 一线排查工具箱:问题定位的核心手段
在嵌入式设备上优化 LLM,最大的困惑往往是“不知道瓶颈在哪一步”。你眼看速度慢,但说不清是 CPU 算力不够、内存带宽不足、缓存命中率低,还是模型本身太大。
我用得最多的是三板斧。
第一板斧是“硬件计数器”。P4 自带了 PMU 和性能计数器,可以监视处理器周期数、指令数、缓存未命中次数。我利用这些计数器把推理流程拆成三个阶段的计数:权重加载耗时、矩阵运算耗时、采样和调度耗时。翻看计数器就能精准定位当前阶段瓶颈在哪。
第二板斧是“遮挡测试法”。这个思路很朴素但极其有用:你怀疑哪一步是瓶颈,就把它替换成一个耗时为 0 的假实现,看总时间变短多少。比如怀疑 KV cache 开销大,就直接把每次读取返回一个空数据;怀疑采样耗时多,就固定输出第 N 个 token 不采样。你很快能获得对每个模块占比的量化感知,比猜靠谱得多。
第三板斧是“Profiler 日志”。我在推理引擎里加入了按模块计时的调试开关,分 attention、feed-forward、采样、tokenizer、内存加载五个模块输出耗时日志。后面每一轮优化跑一遍全套,然后对比不同版本的模块耗时变化,可以清晰地看到哪项优化生效在哪。
这些工具都不复杂,但没有它们,你连“这轮优化到底有没有用”都说不清楚。
5. 还能不能更快?后续方向与系列内容预告
5.1 我判断的剩余优化空间在哪里
4.31 tok/s 在 MCU 上已经算是一个能用的数字——约等于每秒四五个字,离流畅阅读体验还有距离,但已经可以在延迟容忍的场景落地了。不过我心里清楚,这远没有摸到硬件极限。
判断依据来自几个方向的对照实验。首先是矩阵计算的峰值算力利用率——我目前只跑到理论峰值的四成左右,说明算子层还有优化余地,比如 reshape 和 transpose 的频率还能再降低。其次是双核负载均衡——当前两个核心的 busy-idle 周期比例并不均匀,core 1 经常等 core 0 的中间结果,流水线气泡率大概还有 20%~25%。如果换成细粒度任务拆分,估计还能再挤出 15%~20% 的吞吐。
还有一个一直没有动的大杀器是“投机采样”和“并行解码”。这两个技术在 PC 上已经相对成熟——用一个小模型做 draft,在大模型上做 verification,如果猜对了就直接跳过后续的重复计算。MCU 上实现难度会高很多,既要分担算力,又要处理两个模型间的同步,但理论上可以把有效生成速度再翻一倍。我打算在后续验证这个方向,但不会抱太高期望,因为硬件资源实在有限。
然后是体系结构层面的选项。P4 虽然没有 NPU,但它提供了 FPGA 逻辑的扩展接口,可以外接一个低成本的 NPU 加速芯片,比如某些量级在 1 TOPS 以下的 AI 协处理器。如果走这条路,推理吞吐可以跨上一个新台阶,但这已经超出了纯软件优化的范畴,更偏向硬件系统设计了。
5.2 整个系列的文章地图:每条技术线索各自的坑
既然这是系列总览,那我把后续每一篇准备单独拆开写的技术线索先列出来,方便你按需选读,也可以作为你自己的探索路线图。后续每一篇都会比这篇更加聚焦实操,尽量做到看一篇就能动手复现一个优化点。
- 第一篇:在 ESP-IDF 环境里完整编译 llama.cpp 的细节流程,包括 RISC-V 向量扩展指令的启用方式、toolchain 版本坑、链接脚本的调整写法
- 第二篇:GGUF 模型从 PC 侧到 MCU 侧的转换全过程,包括不同量化格式的导出命令、模型格式尺寸与内存映射的关系、如何在 PC 上预先计算好模型的最终内存布局
- 第三篇:量化方案选择的深水区,逐层分析嵌入层、注意力层、前馈层对量化误差的敏感度差异,附完整的逐层量化脚本
- 第四篇:针对 RISC-V 向量寄存器的矩阵分块与算子重写细节,包括向量指令集的选择、寄存器数量限制下的数据调度策略、走查一份完整的矩阵乘法优化代码
- 第五篇:双核流水线并行实现的完整过程,包括任务划分、跨核通信模型、同步机制选型,以及一套避免数据竞争的设计模式
- 第六篇:KV cache 增量式缓存和分词器加速的实现方式,包括缓存块的大小选择、分词表的裁剪策略、以及对多轮对话延迟的实际影响测量
- 第七篇:采样器优化与输出质量的关系测试,整理温度、top-p、top-k 这些参数在不同优化方案下的表现差异,以及可疑的“重复生成”问题的治理技巧
每一篇都会延续这篇的务实风格,以问题为核心,把调试过程、失败尝试、最终方案和实测数据完整交代清楚。
5.3 给同样在 MCU 上折腾 LLM 的你的几点心得
做完整轮优化,我对 MCU 上跑 LLM 这件事的认知发生了不少变化。最开始我觉得这纯属于是“噱头工程”,跑起来又如何?性能摆在那里,谁也救不了。然而真正做完一轮系统化优化之后,我开始意识到这个方向的价值并不在于“替代 PC”,而是在于它开拓了一类完全不同的产品形态——那些不能联网、不能放风扇、电池很小但需要智能语义理解的设备。
几点经验送给后来者。
第一,先把链路跑通,再谈优化。很多人上来就想着怎么量化、怎么并行化,结果模型根本跑不起来,白白折腾。我的建议是任何优化都在“能生成完整输出”的基线版本上迭代,每一步保留一个可回滚的存档。
第二,测量指标一定要在优化前就定好。你信我,如果一开始不把困惑度、内存峰值、功耗、延迟这几个指标都测一遍量,中途一定会被某一次“速度上涨但质量崩溃”的优化带进沟里。
第三,把优化日志留好。每次修改了什么、涨了多少、有没有副作用,全部记录在案。我连续优化了三个星期,没有日志的话早就忘记哪个优化点对应哪个效果参数了。
第四,接受“够用就好”的原则。在 MCU 上你永远追不到 PC 的性能,但你可以把性能追到刚好够用的程度。4.31 tok/s 对文本分类、意图识别、情绪分析这类任务绰绰有余,对生成短响应也可以接受。与其追求好看的跑分,不如认真想想你要做的产品到底是什么场景、需要多快的响应、能容忍多长时间的等待。
最后再分享一个我在整个复盘过程中领悟到的小技巧:在做内存优化的时候,不要只盯着模型权重本身,多花点时间研究一下推理引擎的运行时内存分配器。很多时候,你的 PSRAM 不是真的不够用,而是碎片化太严重导致分配不出大块连续内存。我把自定义内存池从默认的伙伴系统改成 TLSF 算法之后,内存分配耗时下降了一个档次,连带推理过程中的内存操作时间都缩短了不少。这个优化听起来很底层,但在嵌入式场景里真的很实用。
这篇系列总览到这里就算把整个优化版图交代清楚了。后面每一篇我都会按这篇文章里列出的技术方向,逐一深入展开。如果你现在也正拿着一块 ESP32-P4 发愁跑不动模型,希望这篇文章能帮你重新燃起一点信心——0.61 到 4.31 的路我都走通了,你沿着这条线走只会更容易。