先把话说在前面:这篇文章不是为了抬杠"嵌入式能不能跑LLM"。我见过太多人一听说要在板子上跑大模型,第一反应就是"你这是伪需求,直接调云端API不就好了"。但如果你真正在工业现场、车载设备、手持终端这些环境里待过,就会发现有些场景根本容不下网络抖动,有些数据压根不允许出设备,有些功耗预算就是卡死在5瓦以内。这时候"嵌入式+LLM"就不是一个噱头,而是一道真正的工程约束题。
我最近完整走了一遍这个流程,从需求拆解、硬件选型、交叉编译、模型量化,到最后的硬件在环调试,踩了不少坑,也总结出一些可以复用的方法论。这篇文章不聊概念,只聊我是怎么把"约束、构建、硬件闭环"这三件事串起来的。
1. 先认清一个事实:端侧LLM和云端LLM根本不是一个物种
很多做应用层开发的朋友一听到LLM,脑子里浮现的是几百G参数的大模型、A100显卡、分布式推理。但嵌入式领域的LLM,从第一性原理上就完全不同。它要回答的问题是:在一个可能只有2GB内存、算力以TOPS计但实际可用的只有几TOPS的CPU/NPU上,怎么跑起一个能在100毫秒内响应、占用的Flash不超过500MB的生成式模型?
这不是"把模型变小"这么简单,而是一整套围绕约束展开的系统工程。我把它拆成三个核心问题:
- 模型怎么选——7B参数的模型在嵌入式上几乎不可行,1B到3B是主流区间,而且要按实际任务裁剪。
- 运行时怎么搭——推理框架选什么、算子怎么优化、量化怎么做,直接决定性能下限。
- 周边的软硬件怎么闭环——传感器采集、知识库检索、结果输出、电源管理,这些看似外围的东西,往往才是真正决定项目成败的部分。
我举个例子。我们做一个车间设备的故障诊断助手,需求是现场工程师对着设备拍张照或者输入一行故障码,设备端本地就能回答"这个故障码对应什么原因、怎么处理"。硬性条件:不联网、数据不出厂、响应时间不超过2秒、整机功耗不超过10瓦。这个需求用云端API当然能实现,但客户明确说了网络环境和数据安全都不允许。这时候就必须走端侧方案。
那我的选型思路是这样的:先不考虑模型,先算账。可用内存、Flash空间、算力上限、功耗预算、延时目标,这些就是一堆硬约束。任何一个约束被打破,整个方案就要推翻重来。后面所有的工作,无论是模型量化还是编译优化,本质上都是在和约束做博弈。
这里有个关键认知:端侧LLM项目里,最稀缺的资源不是算力,而是内存带宽和开发者的调试时间。很多人一开始盯着算力死磕,结果发现模型推理速度上去了,但内存带宽不够,数据搬移成了瓶颈。这一点我会在硬件闭环章节详细展开。
2. 约束是第一等公民:三层约束决定你的设计方案
2.1 资源约束:先算账再选模型
拿到需求后,第一件事不是选模型,而是做资源预算。我这里有一套实操流程,分享给大家:
第一步,确定内存预算。如果你的板子是1GB RAM,操作系统和业务逻辑吃掉400MB,那留给模型推理的只有600MB。模型加载到内存后,激活值、KV Cache、中间计算buffer都要占空间。我用llama.cpp跑过实测,2B量化到INT8的模型,权重约占800MB,加上KV Cache和计算buffer,峰值内存要到1.2GB。如果你的板子只有1GB内存,这个方案直接淘汰。要么换更小的模型,要么量化到INT4,要么剪枝。
第二步,确定存储预算。嵌入式设备的Flash可能是512MB到8GB。一个4GB的Flash,系统镜像占1GB,业务程序占200MB,用户数据预留500MB,能留给模型的就只有2GB多一点。这时候1B模型的INT8量化(约500MB-700MB)就比2B模型(约1GB)更从容。
第三步,算算力账。嵌入式CPU的算力一般用GOPS(每秒十亿次运算)来衡量。一颗1.5GHz的四核A53,实际能跑起来的整数运算大约是10-20 GOPS,但LLM推理要的是矩阵乘法的乘加运算,通常还要打折。我实测在RK3588的CPU上跑Llama 2B INT8,生成速度大概3-5 tokens/s,这个速度做简单的问答勉强够用,但要处理大段上下文就很吃力。如果你需要10 tokens/s以上的速度,就得考虑NPU或者更大的CPU。
2.2 硬件约束:不是所有约束都在代码里
很多人以为约束只存在于模型和算法层面,其实硬件自身的约束同样致命。比如IO约束——你的摄像头接口带宽、串口波特率、显示接口刷新率,都在影响整个系统的数据吞吐。我在调试中遇到过一个问题:摄像头采集1080p图像,送入LLM做图文诊断,结果图像传输占了大量总线带宽,导致模型推理的数据搬移延迟变大,整个响应时间从预期的1.5秒变成了3秒。这个问题的根源不是模型,而是硬件总线的调度。
时钟约束也是容易被忽视的点。NPU和CPU的运行频率、DDR的时钟频率,直接决定了算力能不能吃满。有些开发板为了控制功耗默认锁频,你跑基准测试时看着处理器利用率很高,但频率上不去,实际推理速度远低于预期。我踩过这个坑——在测试RK3588的NPU时,文档说算力有6 TOPS,但我没有检查默认的NPU频率策略,结果实际只跑出3.5 TOPS的性能。手动调整DVFS策略后,性能才回到正常水平。
这里我建议用一个经验做法:拿到开发板后,先跑一遍算力和内存带宽的基准测试,把CPU、NPU、内存的真实能力摸清楚。不要相信宣传参数,也不要相信demo参数——demo一般跑的是最有利场景,你的实际任务可能完全不同。
2.3 解码约束:LLM不是嘴上说约束就有约束
前面说的都是硬件和资源约束,但还有一层约束往往被忽略——模型输出的约束。
LLM是一个概率生成器,它对输入序列逐token生成,如果你不加以约束,任何输入都可能产生意外的输出。在嵌入式工业场景里,这不可接受。比如说故障码"E0135"对应的是"压力传感器异常",你不可能让模型自由发挥去编一段"可能是电池电压偏低"这种话。
我对输出做约束的方式目前有三招,按可靠程度从低到高排列:
- 提示词约束(软约束):在系统提示词里写明"你只能回答以下知识库中的内容,不得编造",或者用Few-shot把回答格式固定下来。这个方法实现简单,但可靠性有限,模型仍然可能跑偏。
- 结构化输出约束(硬约束):让模型通过JSON Schema等方式按固定结构输出,配合推理时的logit mask或grammar约束,确保输出的格式严格合规。比如规定故障原因必须是枚举值、修复步骤必须是列表。
- 外部逻辑兜底(最可靠):在模型输出后面加一层规则引擎,用正则和知识库匹配对输出做二次校验。如果模型输出不在知识库范围内,就强制返回"未匹配到结果,请人工介入"。实际项目中我强烈建议加上这层兜底。
很多做嵌入式开发的工程师没有接触过大模型的输出随机性,习惯性地认为"模型说错了就改输入重试"。但嵌入式场景往往要求确定性输出,你不做输出约束,后面整个系统就没法闭环。这一点怎么强调都不为过。
3. 构建魔法:交叉编译、依赖裁剪与运行时瘦身
3.1 交叉编译:构建过程的第一个大坑
嵌入式开发绕不开交叉编译。在x86的服务器上写的代码,要编译并链接到ARM平台的库,这是一个标准的"构建"问题。但这个构建过程远比普通项目的gcc main.c -o app复杂,因为它牵扯到工具链、系统库、第三方依赖,每一层都可能踩坑。
我的构建环境是这样的:
- 宿主机:x86_64 Ubuntu 22.04
- 目标机:aarch64(ARM64)
- 工具链:aarch64-linux-gnu-gcc 11.3
- 推理框架:llama.cpp(静态编译)
- 模型量化工具:llama.cpp自带的quantize工具
第一坑:工具链版本不匹配。我一开始用的是Ubuntu自带的老版本交叉工具链(8.3),编译通过,但一在板子上跑就段错误。排查了半天,发现是老工具链对新ARM指令支持不全,Neon向量化的指令被错误编译。换成11.3后问题消失。
第二坑:NEON指令集与浮点ABI。现代ARM芯片普遍支持NEON SIMD指令集,llama.cpp在编译时会根据平台自动启用NEON优化,但你必须在CMake的CMAKE_C_FLAGS里明确指定-march或-mcpu参数,否则编译器会按最低兼容标准生成指令,性能大幅下降。我的实测经验是,加上-march=armv8.2-a+dotprod之后,向量化矩阵乘法的算力利用率提升了接近一倍。
第三坑:静态库与动态库的选择。嵌入式环境下我强烈建议静态链接。你的目标机系统里可能没有配套的动态库,或者版本不一致,动态链接会让你在部署阶段焦头烂额。llama.cpp本身就支持静态编译,加上BUILD_STATIC=ON就行。依赖系统库的部分,尽量用libc的常见子集,避免用到过于底层的动态特性。
3.2 构建系统选型与依赖管理
嵌入式项目的构建系统,我个人推荐CMake。虽然Makefile系统也能用,但CMake对交叉编译的原生支持、对第三方依赖的查找机制、对编译选项的精细控制,明显更适合复杂项目。
关于依赖管理,嵌入式环境不能像服务器那样随便apt install。我在构建llama.cpp时,发现它依赖了一些较新的库特性,例如需要C++17标准,以及OpenBLAS或BLAS库用于矩阵加速。但嵌入式环境往往没有这些库的预编译包,需要你亲自动手交叉编译。
我的建议是:能用纯头文件库就尽量用纯头文件库,能自己交叉编译的就提前编好,不要指望在目标板上现场编译——目标板的性能通常不足以支撑大项目的编译,而且编译工具链在板上也不一定齐全。
3.3 依赖剪裁:在"不参与构建"和"必须参与构建"之间做权衡
构建LLM项目时有个隐藏问题:模型推理框架通常会附带大量你不需要的功能代码。llama.cpp默认支持许多模型架构,但你可能只跑一个模型架构,剩下没用的代码只是浪费空间。
用C/C++的预处理宏和CMake选项可以把这些支持裁剪掉。llama.cpp里就提供了类似LLAMA_DISABLE_LOGS、GGML_USE_CPU之类的开关。我推荐的做法是:
- 只编译你需要的模型架构(比如只支持LLaMA架构,裁剪掉其他架构的代码),代码量可以减少不少。
- 关闭日志输出,在生产环境里日志几乎没有价值,但log占用的Flash和运行时的IO开销是实打实的。
- 把不需要的示例程序、测试程序从构建列表中剔除,只保留生产目标。
最后一个建议:构建出来的镜像要实测启动时间和内存占用。我遇到过几次"编译过了、但在板子上启动就OOM"的情况,原因都是没有把模型的预加载算进内存预算里。
4. 硬件闭环:从选型到调优的系统思维
4.1 硬件闭环的思想:模型不是孤岛,整个系统才是
"硬件闭环"这四个字,是我在梳理这次项目时最想说的一点。很多人做端侧LLM,眼睛只盯着"把模型跑起来"这一步,但真正能不能落地,要看的是从"输入采集"到"推理输出"再到"执行动作"的整个链条是不是通的。
拿我的项目来做例子:
传感器/输入设备 → 数据预处理 → LLM推理 → 输出解析 → 执行机构/显示终端这个链条的每一环都有延迟和资源消耗。我在调试中遇到的一个问题是,摄像头采集帧率是30fps,但送入LLM的预处理管线却把每一帧都做了一次完整的JPEG解码和缩放,导致CPU使用率飙升。后来改成"帧差检测+按需解码",CPU开销降了一半,推理能用的算力就变多了。
这就是硬件闭环的思维:模型推理在硬件上运行,但硬件上还同时跑着采集、处理、输出、控制这些任务。你要做的不是"把模型跑起来",而是"在有限的硬件上让整个系统闭环"。
4.2 NPU、CPU、DSP:算力怎么分配才合理
端侧LLM的算力分配,不是"能上NPU就上NPU"这么简单。我的实测经验是:
| 硬件单元 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|
| CPU | 通用性强,部署简单 | 算力密度低,功耗高 | 轻量模型,快速原型验证 |
| NPU | 算力密度高,功耗比优秀 | 算子支持受限,调试困难 | 固定模型结构的量产场景 |
| GPU(如果有) | 矩阵运算强,生态成熟 | 功耗高,嵌入式少见 | 需要高吞吐的边缘盒子 |
| DSP | 低功耗,实时性强 | 编程模型特殊,开发周期长 | 信号处理+轻量模型混合场景 |
以RK3588为例,它的NPU标称6 TOPS,但在跑LLM时,NPU能用的算子取决于RKNN-Toolkit的版本转换支持。如果你的模型用了NPU不支持的算子(比如某些Attention变体),会有两种结果:要么被替换成CPU算子,推理速度大幅下降;要么直接编译失败,整个模型无法转换。
我的建议是,在你花时间优化模型之前,先做一个"算子可行性检查"。把模型结构里用到的算子列出来,对照NPU工具链支持的算子列表。如果有不匹配的算子,要么换模型结构,要么干脆走CPU路线。这个决策要在项目前期就做,不要等到集成阶段才发现跑不起来。
4.3 内存带宽是隐形的皇帝
前面提到过内存带宽,这里我必须专门展开。LLM推理的每一步(Token生成)都需要把整个模型的权重从内存搬到计算单元。假设模型权重有1GB,每次推理要读一遍,就是1GB的流量。如果内存带宽是10GB/s,理论上一秒能推理10次,但实际因为缓存命中率、总线竞争、压缩率等因素,只能做到带宽的60%-70%。
这就是为什么同样的模型,在服务器上跑得飞快,在嵌入式板子上慢如蜗牛。服务器内存带宽动辄几百GB/s,嵌入式DDR4的带宽可能只有十几GB/s。所以很多嵌入式LLM项目的优化,本质上是在压内存带宽:
- 量化从INT8降到INT4,权重体积减半,带宽消耗减半。
- 使用更小的上下文窗口,减少KV Cache的带宽占用。
- 减少权重重复读取——通过批处理多个请求共用一次权重加载,提升缓存命中率。
4.4 一个完整的硬件闭环调试流程
最后分享我的调试流程,可以作为参考:
- 先跑一个最小推理demo(没有外围设备,纯模型推理),记录基线性能:单token延迟、内存占用、功耗。
- 接上输入设备(摄像头/传感器),记录整体系统的性能变化。重点观察CPU、内存带宽的竞争。
- 接上输出设备(显示屏/串口),测试完整闭环的端到端延迟。
- 做压力测试:长时间运行、反复唤醒、高并发请求,检查内存泄漏、热降频、稳定性。
这个流程最大的价值是"拆开看瓶颈"。如果步骤1里的单token延迟是80ms,但步骤3里的端到端延迟是2秒,那问题大概率不在推理,而在输入采集或输出处理上。没有拆分就很难定位问题。
我在做这个闭环测试时,发现一个有意思的现象:板子在长时间推理后温度上升,NPU自动降频,token生成速度从8 tokens/s掉到5 tokens/s。这个热降频问题在实验室里很难发现,但实际现场环境几乎必然发生。所以如果你要做工业级产品,一定要加散热测试这一关。
5. 推荐的开源工具链与工作流
如果让我给一个最小可复现的工具链组合,我会这样选:
- 模型调研:用ollama在x86机器上快速测试不同模型的效果(它支持各种量化版本,很方便)。
- 模型量化和推理:用llama.cpp,支持大量ARM交叉编译选项,且提供完整的量化工具(从FP16到INT4、IQ系列量化)。我实测llama.cpp在ARM CPU上的优化程度非常高,NEON、汇编级优化都做得比较到位。
- 如果需要NPU:根据自己的NPU品牌选择对应工具链,例如Rockchip的RKNN-Toolkit、地平线的工具链、算能的SDK。这里要特别提醒一句:模型转换工具的版本要和NPU驱动版本匹配,否则会出现各种莫名奇妙的失败。
- 系统镜像构建:用Buildroot或Yocto,可以把整个Linux系统裁剪到几十MB级别,给模型留出空间。
我还想推荐一个加分项:用容器或镜像方式管理整个运行时环境。嵌入式系统虽然资源紧张,但你至少可以把runtime依赖打包成只读文件系统,这样部署和回滚都会方便很多。
6. 实测数据与收益
我把自己项目上的一组实测数据贴出来,供大家对比参考:
| 配置 | 模型 | 量化 | 内存占用 | 生成速度 | 端到端响应 |
|---|---|---|---|---|---|
| RK3588 CPU only | Qwen 0.5B | INT8 | 约500MB | 9.2 tokens/s | 1.8s |
| RK3588 CPU only | Qwen 1.8B | INT4 | 约1.1GB | 5.1 tokens/s | 3.2s |
| RK3588 NPU+CPU | Qwen 1.8B | INT4 | 约1.2GB | 12.8 tokens/s | 0.9s |
| 树莓派4 (4GB) | Llama 2B | INT8 | 约1.4GB | 3.5 tokens/s | 4.1s |
从这组数据能看出几个结论:
- NPU确实能带来显著加速(CPU-only模式下1.8B模型5 tokens/s,NPU加速后接近13 tokens/s)。
- 模型大小和量化精度的选择,对最终体验的影响是数量级的。1.8B INT4的体验可以做到接近可用,2B INT8就力不从心。
- 端到端响应时间比生成速度快很多,因为首token延迟(Prefill阶段)往往只占一小部分,真正的瓶颈在连续生成阶段。
如果你的场景只需要短回答(比如"这个故障码对应什么原因"),那生成几秒和几十个token就够了,2B模型也能用。但如果你需要长文本输出(比如生成维修报告),生成速度就会成为致命瓶颈,必须上NPU或者更强的CPU。
7. 最后的几个建议
做完整套流程,我更强烈地认为,嵌入式+LLM不是简单的"把模型塞进板子",而是一个系统性工程问题。约束决定了方案,构建决定了效率,硬件闭环决定了可用性。
如果你正打算入这个方向,我的建议是:
- 不要一上来就追大模型。1B-3B的模型在特定任务上完全够用,先跑通小模型,再考虑做模型蒸馏或量化提升精度。
- 先把资源账算清楚。内存、存储、算力、功耗、延迟,这五个维度先画一个预算表,每个模块分配好预算,再开始选型。
- 输出约束一定要做。嵌入式LLM的价值不在于"能聊",而在于"能按规矩干活",没有输出约束就没有可靠性。
- 调试要按环节拆分。CPU瓶颈、内存带宽瓶颈、NPU算子瓶颈、散热降频瓶颈,每一个都可能成为项目的杀手,尽早做闭环测试才能暴露问题。
最后说一句:做嵌入式LLM最大的收获,是你会重新理解"约束"这个词的含义。约束不是阻碍,约束是设计的起点。这也是我这篇文章想传达的最核心的东西。