上周折腾了一件大事:手头有块 Orange Pi 5 Plus(RK3588,16GB 内存,带 6 TOPS 的 NPU),之前已经在 Jetson Orin 上跑通了 Qwen2.5,这次想看看 RK3588 这块国产芯片能不能也把大模型跑起来。而且光跑起来还不够------RK3588 上有大模型推理的两条主流路线:llama.cpp(社区 NPU fork) 和 rknn-llm(瑞芯微官方工具链),到底谁快?网上众说纷纭,干脆自己动手,同板同模型同协议测一遍。
前后折腾了三天,踩的坑能绕地球半圈:NPU 输出乱码查了一下午发现是驱动版本问题、CMA 内存不够导致模型加载直接失败、压测时被 prompt 缓存骗了数据虚高十倍......最终结果还挺有意思的:rknn-llm 全维度碾压,decode 速度是 llama.cpp 的 3~4.3 倍。今天就把这个实战过程完整分享出来,全是实测数据,零编造。
🎯 为什么要做这个实验?
先说下背景。我之前的边缘 AI 部署都是在 Jetson Orin 上做的(TensorRT / MLC-LLM 那套),这次换 RK3588 主要有几个考虑:
- 💰 成本:Orange Pi 5 Plus 二手价大概 Jetson 的零头,但 NPU 算力有 6 TOPS(INT8)
- 🌏 国产化:RK3588 在国内边缘 AI 圈子太常见了,垃圾cek、智能座舱、工业网关到处都是,学会这套栈很实用
- ⚔️ 两条路线之争:RK3588 上跑 LLM 有两种玩法,谁快一直没个准数
这两条路线分别是:
| 路线 | 引擎 | 量化方式 | 模型格式 |
|---|---|---|---|
| A | llama.cpp 纯 CPU | 量化好的 GGUF 直接跑 | GGUF |
| B | rk-llama.cpp(社区 NPU fork) | GGUF 加载时运行时再量化 | GGUF |
| C | rknn-llm v1.3.0(瑞芯微官方) | PC 端转换期量化(W8A8) | .rkllm 专有格式 |
一句话总结区别:llama.cpp 系是"GGUF 直接吃",rknn-llm 是"先在 PC 上把模型转成专用格式"。前者省事,后者理论上对 NPU 利用更彻底------到底差多少?往下看。
测试模型:Qwen2.5-1.5B-Instruct 和 Qwen2.5-3B-Instruct,一个基线一个压极限。
🛠️ 环境准备:先踩两个系统级的坑
硬件与软件配置
- 板卡:Orange Pi 5 Plus 16GB(RK3588,NPU 三核各 2 TOPS)
- 系统:Armbian 26.8.2 Debian Trixie minimal(注意,这个是后来换刷的,原因见下文)
- 内核:6.1.115-vendor-rk35xx
- NPU 驱动:v0.9.8
- 数据盘:NVMe 挂在 /mnt/nvme(系统盘和数据盘分离,强烈推荐这个架构!)
坑 #1:NPU 驱动版本不对,输出全是乱码(最坑的一个!)
这个坑必须放在最前面讲,因为它是静默失败------不报错,但结果全错,非常误导人。
一开始我用的是板子出厂的 Orange Pi Ubuntu 20.04 系统(内核 5.10.160),llama.cpp NPU fork 编译顺利、模型加载顺利、NPU 三核也有负载(27~70%),一切看起来都正常。然后我发了个 prompt 过去:
"请用一段话介绍 RK3588 NPU 的技术特点"
结果返回的是一堆乱码。Q8_0 和 Q4_0 两种量化都试了,全是乱码。
更诡异的是性能:NPU 路径反而比纯 CPU 还慢(prefill 33.9 vs CPU 52.1 tok/s)。NPU 明明在跑,负载也有,就是又慢又错。
排查过程走了一圈排除法:
| 嫌疑 | 检查结果 |
|---|---|
| Q4_0 量化问题? | ❌ Q8_0 同样乱码,是全局问题 |
| 环境变量配错? | ❌ 默认映射本应工作 |
| librknnrt 版本混淆? | ❌ fork 自带 2.3.2,ldd 证实 |
| 内核驱动版本? | ✅根因找到了 |
最后在 fork 的 CHANGELOG 里发现一行关键信息:"Kernel driver: Matching NPU driver version 0.9.8+" 。而板子出厂内核自带的驱动是 v0.9.6 ------差了两个小版本,rknn_matmul 就会静默返回错误结果。
💡 关键教训:驱动是编译进内核的,没法单独升级,唯一的办法就是换系统。我换刷了 Armbian vendor 内核(自带 v0.9.8),乱码立刻消失。修复前后对比:
| 路径 | 输出 | prefill | decode |
|---|---|---|---|
| 修复前 NPU(驱动 0.9.6) | ❌ 乱码 | 33.9 tok/s | 3.7 tok/s |
| CPU 参照 | ✅ 正常 | 52.1 tok/s | 12.8 tok/s |
| 修复后 NPU(驱动 0.9.8) | ✅通顺 | 57.1 tok/s | 6.34 tok/s |
修复后 prefill 直接反超 CPU(57.1 vs 52.1),因果链非常干净。
📌 划重点:拿到任何 RK3588 板子,第一件事执行这条命令,比看任何 README 都重要:
bash
cat /sys/kernel/debug/rknpu/version
# 期望输出:RKNPU driver: v0.9.8 或更高
坑 #2:CMA 内存不够,模型加载直接 ENOMEM
驱动修好之前还遇到过一个更早期的问题:llama-server 加载模型时直接报错退出:
RKNPU_DMA_ALLOC: ioctl DMA_HEAP_IOCTL_ALLOC failed: Cannot allocate memory
alloc_tensor_range: failed to allocate RKNPU buffer of size 699662336 ← 667MB
查了下原因:CMA(Contiguous Memory Allocator,物理连续内存池)是 NPU 这类 DMA 设备专用的,出厂配置只有 128M------那是给摄像头视频解码用的配置,跑 LLM 完全不够。Qwen2.5 光是词表嵌入张量(151936 词 × 2048 维 F16)就要 667MB。
解法很简单,改内核启动参数:
bash
# /boot/armbianEnv.txt 追加
extraargs=cma=3G
# 重启后验证
grep CmaTotal /proc/meminfo # 期望 3145728 kB
实测 3G 够 3B W8A8 模型(峰值内存 3313MB)正常跑。
另外还有个必做的小配置:ulimit -n 1000000 。RKNN 每个 DMA buffer 占一个文件描述符,默认 1024 上限会被 3B 模型直接打爆(报 errno:24 Too many open files)。这个坑我也实打实踩过一次,3B CLI 崩了一次才想起来。
🔥 第一条路线:llama.cpp(社区 NPU fork)
fork 选型:这里的水很深
RK3588 的 llama.cpp NPU 加速不是官方功能 ,全靠社区 fork。我一开始用的是 KHAEntertainment 的 fork,后来做源码考察时发现了 invisiofficial 的 rknpu2 分支------对比之后果断切换:
| invisiofficial@rknpu2(最终采用) | KHAEntertainment(弃用) | |
|---|---|---|
| 活跃度 | 2026-08 还在提交(刚合入 per-channel 量化) | 停更 5 个月 |
| 环境变量 | RKNPU_HYBRID(主流文档都按这个写) |
HYBRID_PATTERN(名字不一样,文档经常对不上) |
| NPU 内存 | 走 librknnrt 官方 API | 自写 dma_heap 直 open+ioctl(有个回退 bug) |
| gcc14 编译 | 干净通过,无需补丁 | 需要打补丁 |
💡 踩坑心得 :这两个 fork 是"同源异名"关系,环境变量名都不一样,网上搜到的教程经常互相矛盾------不是谁写错了,是fork 版本不同。看教程之前先确认自己用的是哪个 fork。
编译与启动
板上原生编译(gcc 14.2.0),十几分钟搞定:
bash
cd rk-llama.cpp-inv
cmake -B build-npu -DLLAMA_RKNPU2=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build-npu -j$(nproc)
启动参数我把纪律都固化到启动器脚本里了:
bash
export RKNPU_HYBRID=W8A8_STANDARD # Q8_0 权重 → INT8 流水线
ulimit -n 1000000 # 防 DMA fd 耗尽
taskset -c 4-7 ./build-npu/bin/llama-server \
-m models/qwen2.5-1.5b-instruct-q8_0.gguf \
-ngl 99 -c 4096 --no-warmup \
--temp 0.0 --top-p 1.0 --jinja \
--host 0.0.0.0 --port 8000
注意 taskset -c 4-7 是把进程绑到 A76 大核上(0-3 是 A55 小核),这个对延迟影响不小。
llama.cpp NPU 的两个"天然限制"
实测之后我修正了几个以前的错误认知,这里也分享给大家:
- NPU 不是全算子覆盖。这个 fork 是运行时混合调度:权重加载时 CPU 反量化 → 校准 → 按 NPU 流水线再量化,部分算子会回退 CPU(ARM NEON)。所以它吃不到 NPU 的全部红利
- NPU 的强项是 prefill,不是 decode。fork README 官方就承认:prefill 提升 2.8~5 倍、功耗减半,但 decode 并不快于 CPU。我的实测完全印证了这一点(数据见后文,3B 的 decode NPU 甚至比 CPU 还慢一点)
⚡ 第二条路线:rknn-llm(瑞芯微官方工具链)
架构:和 llama.cpp 思路完全不同
rknn-llm 的核心思路是**"量化在 PC 上一次性完成,板端只做推理"**:
[PC 端 x86_64]
HuggingFace 权重(fp32)
↓ rkllm-toolkit 生成校准集(22 条 prompt)
↓ build(W8A8) + export_rkllm()
Qwen2.5_1.5B_Instruct_W8A8_RK3588.rkllm
↓ rsync 上板
[板端 aarch64]
librkllmrt.so(官方预编译,零编译)+ Flask 服务(纯 Python,ctypes 加载 .so)
↓ NPU 三核全算子推理
对比一下就很清楚:llama.cpp 是"运行时边加载边量化",rknn-llm 是"离线精心量化 + 专用 runtime"。后者的板端 runtime 是全算子 NPU 化的,没有回退 CPU 的问题------这就是最终性能差距的根源。
重要勘误:RK3588 只支持 W8A8!
我原计划 3B 模型用 W4A16 量化(4-bit 权重省内存),结果 toolkit 直接报错:
target_platform: rk3588 not support quantized_dtype: w4a16
查了下:RK3588 的 NPU 只支持 INT8 激活(W8A8),W4A16/W8A16 这些 A16 档位只有 RK3576 支持(RK3576 的 NPU 带 FP16 单元)。所以最终 1.5B 和 3B 都用了 W8A8 + normal 算法。
PC 端转换实操
转换我写了一键脚本,核心就三行 API:
python
from rkllm.api import RKLLM
llm = RKLLM()
llm.load_huggingface(model="...", device="cpu", dtype="float32")
llm.build(do_quantization=True, quantized_dtype="W8A8",
quantized_algorithm="normal", target_platform="RK3588",
num_npu_core=3, dataset="data_quant.json", max_context=4096)
llm.export_rkllm("Qwen2.5_1.5B_Instruct_W8A8_RK3588.rkllm")
实测转换开销:
| 模型 | 转换耗时 | 产物大小 | 板端峰值内存 |
|---|---|---|---|
| Qwen2.5-0.5B | ~24 min | 800MB | 714MB |
| Qwen2.5-1.5B | ~30-60 min | 2.0G | 1792MB |
| Qwen2.5-3B | ~56 min | 3.5G | 3313MB |
几个转换的坑:
- 必须
--device cpu:我机器上是 GTX 1080 Ti,新版 torch(cu130)不支持 Pascal 架构,cuda 探测会误报然后崩 - 权重下载走 hf-mirror,且必须设
HF_HUB_DISABLE_XET=1:Xet 存储的 CAS 域名不走镜像,匿名请求会 401,这个报错信息完全看不出是镜像问题 - 1.5B 和 3B 串行转换:fp32 加载要 6~16G 内存,并行跑会 OOM
板端部署:编译量几乎为零
这是 rknn-llm 路线体验最好的地方------runtime 是官方预编译的 .so,直接随包分发,唯一要编的是一个 300 行的 demo(甚至不编也行,Flask 服务是纯 Python 的)。板上一条命令起服务:
bash
bash run_rkllm_server.sh models/Qwen2.5_1.5B_Instruct_W8A8_RK3588.rkllm
# 监听 0.0.0.0:8000,OpenAI 兼容 + SSE 流式 + Function Calling
服务起来后 NPU 三核负载各 ~80%,说明是真·NPU 推理,不是 CPU 兜底。冒烟验证全部通过:
| 模型 | prefill | decode | 峰值内存 |
|---|---|---|---|
| 0.5B W8A8 | 287.8 tok/s | 31.1 tok/s | 714MB |
| 1.5B W8A8 | 150.6 tok/s | 14.18 tok/s | 1792MB |
| 3B W8A8 | 106.7 tok/s | 7.86 tok/s | 3313MB |
📌 版本配对纪律 :toolkit、runtime、驱动三者必须配对(我用的 1.3.0 ↔ 1.3.0 ↔ 0.9.8),混用会报 model version is too old。
📏 基准测试:先说一个骗了我一次的大坑
三条路线都提供 OpenAI 兼容接口,所以我用同一个基准脚本 (bench_ttft_rkllm.py)打分,协议完全一致:
- 长度档:32 / 256 / 1024 中文字,各 3 轮取中位数
max_tokens=256,temperature=0,首轮回热不计入- 指标:TTFT(发出请求 → 首个内容 chunk)、decode 速度、prefill 吞吐(对 TTFT 做线性拟合消掉固定开销)
⚠️ 大坑预警:RKLLM 有 prompt 缓存!
我第一版压测脚本用固定 prompt 跑 3 轮,结果发现第 2 轮开始 TTFT 骤降到 ~90ms,而且跟输入长度完全无关------1024 字和 32 字一个速度,这明显不对劲。
查了半天才发现:RKLLM 运行时对相同/公共前缀的 prompt 有缓存,命中后 prefill 整个被跳过。等于输入速度虚高了十倍,如果照这个数据发出去就闹笑话了。
修复方法:脚本里每一轮都把材料内容做循环移位,保证每轮都是真实 prefill。在 RK3588 上做 LLM 压测的朋友,务必检查你的测试工具有没有处理这个。
🏆 最终数据:rknn-llm 全面胜出
好,前面铺垫了这么多,直接上干货。三路线同板同日连续实测(各档 3 轮中位数):
Qwen2.5-1.5B(llama.cpp Q8_0 / rkllm W8A8)
| 路线 | TTFT@32字 | TTFT@256字 | TTFT@1024字 | decode | prefill(拟合) |
|---|---|---|---|---|---|
| llama.cpp CPU | 559 ms | 2473 ms | 11174 ms | 6.06 tok/s | ~92 tok/s |
| llama.cpp NPU | 504 ms | 970 ms | 2911 ms | 6.30 tok/s | ~408 tok/s |
| rknn-llm | 100 ms | 516 ms | 2100 ms | 25.88 tok/s | ~493 tok/s |
Qwen2.5-3B(llama.cpp Q4_0 / rkllm W8A8)
| 路线 | TTFT@32字 | TTFT@256字 | TTFT@1024字 | decode | prefill(拟合) |
|---|---|---|---|---|---|
| llama.cpp CPU | 1292 ms | 8726 ms | 39898 ms | 4.74 tok/s | ~25 tok/s |
| llama.cpp NPU | 750 ms | 1582 ms | 5020 ms | 4.16 tok/s | ~230 tok/s |
| rknn-llm | 164 ms | 879 ms | 3635 ms | 14.37 tok/s | ~284 tok/s |
三组关键对比
① llama.cpp 开 NPU 值不值?(NPU vs CPU)
| 维度 | 1.5B | 3B |
|---|---|---|
| prefill 提升 | 4.4x(92→408 tok/s) | 9.2x(25→230 tok/s) |
| decode 变化 | +4%(基本持平) | -12%(反而更慢) |
结论很清晰:llama.cpp 的 NPU 后端只加速 prefill,decode 一分不赚甚至倒贴。如果你的应用是"长文档输入 + 短回答"(RAG 检索类),它很值;如果是"聊天长输出",收益有限。
② rknn-llm 比 llama.cpp 快多少?
decode 是重头戏:1.5B 上 4.3 倍(25.88 vs 6.06),3B 上 3 倍(14.37 vs 4.74)。TTFT 全档位最优,1.5B 短指令首字响应只要 100ms------这个延迟已经接近云端大模型的体感了。
有意思的是 prefill:rknn-llm 对 llama.cpp NPU 只再快 20% 左右(493 vs 408),说明两者 prefill 都已接近 NPU 算力天花板。真正的差距在 decode------转换期 W8A8 离线量化 + 全算子 NPU runtime,把运行时混合路径按在地上摩擦。
③ 模型从 1.5B 加到 3B,衰减多少?
| 路线 | decode 衰减 |
|---|---|
| llama.cpp CPU | 0.78x |
| llama.cpp NPU | 0.66x |
| rknn-llm | 0.56x(25.88→14.37,但绝对值仍是唯一能打的) |
3B 在 rknn-llm 上 decode 14.37 tok/s,一句话回答 2 秒内出完,交互完全可用;而 llama.cpp 两条路径都只有 4~5 tok/s,体感就是"看着字一个个蹦"。
📋 结论:RK3588 上怎么选?
| 场景 | 推荐 | 理由 |
|---|---|---|
| RK3588 跑 Qwen2.5 级文本模型 | rknn-llm W8A8 | 全维度最优,decode 3~4.3x |
| 想直接用 GGUF、免 PC 转换、频繁换模型 | llama.cpp | GGUF 直接跑,转换零成本 |
| 长上下文(>8K) | llama.cpp | rkllm 的 max_context 硬限 16K,llama.cpp 只受内存限制 |
| 只想加速 prefill(RAG 短回答场景) | rk-llama.cpp NPU | prefill 4.4~9.2x,decode 不劣化 |
| 多模态(Qwen-VL) | rknn-llm | 官方支持,fork 不支持 VLM |
部署复杂度也说一下:rknn-llm 多了 PC 端转换环节(1.5B 约半小时、3B 约一小时),但板端几乎零编译;llama.cpp 免转换但板端全量编译 + fork 环境踩坑多。一次性部署选 rknn-llm,频繁换模型验证选 llama.cpp。
🩸 踩坑清单速查表(建议收藏)
- 驱动版本第一检查 :
cat /sys/kernel/debug/rknpu/version,低于 0.9.8 会静默算错(乱码不报错),换 Armbian vendor 内核解决 - CMA 改 3G :
extraargs=cma=3G,出厂 128M 跑 LLM 必 ENOMEM ulimit -n 1000000必设:DMA buffer 占 fd,默认 1024 被 3B 打爆- RK3588 只支持 W8A8 :W4A16 报错
not support quantized_dtype,别浪费时间试 - 压测每轮必须换 prompt:RKLLM 有前缀缓存,命中后 prefill 被跳过,数据虚高十倍
- 性能对比必须同路径:rkllm 的 HTTP 路径比 C demo 路径 decode 快 ~1.7 倍(计时口径不同),混着比会得出错误结论
- 板端先换 apt 镜像:官方源在板端 ~1 包/分钟,换北外镜像后分钟级
- HF 下载设
HF_HUB_DISABLE_XET=1:Xet 域名不走 hf-mirror,401 报错很迷惑 pgrep -f '[f]lask_server.py'要加方括号:不然会匹配到 ssh 包装进程,服务没起也"检测通过"- fork 环境变量先 grep 源码确认:两个 fork 变量名不同,教程照抄可能无效
📝 写在最后
三天折腾下来,最大的感受有两点:
一是"静默失败"是端侧部署最恶心的故障模式 。驱动版本不对导致 NPU 输出乱码,全程没有任何报错,NPU 负载还正常,如果不是拿 CPU 路径做对照,可能就一直以为"模型量化坏了"或者"NPU 就这水平"。永远先验版本配对,再谈性能。
二是官方工具链的护城河比想象中深。rknn-llm 靠"离线量化 + 全算子专用 runtime"在 decode 上拉开了 3~4 倍差距,这个差距不是社区 fork 靠调参能追回来的。但 llama.cpp 的价值在别处------GGUF 生态、零转换成本、长上下文灵活,两条路线不是替代关系,是互补关系。
后续还打算在这块板上试试 Qwen2.5-VL 多模态(走 rknn-llm 官方视觉塔 + 语言塔协同),以及 W8A8_HADAMARD 量化流水线的精度 A/B(官方 KL 散度 0.0033,近乎无损),有兴趣的朋友可以关注。
所有实验数据、脚本和日志都已归档,有问题欢迎评论区交流~
环境版本锚定:Orange Pi 5 Plus 16GB / Armbian 26.8.2 Trixie / kernel 6.1.115 / RKNPU driver v0.9.8 / rknn-llm v1.3.0 / rk-llama.cpp invisiofficial@rknpu2 (tag rknpu2-b8675)。测试协议:32/256/1024 字提示 × 3 轮取中位,max_tokens=256,temperature=0,每轮轮换 prompt 内容防缓存命中。