RK3588 NPU 跑大模型实测:rknn-llm 解码 25.9 tok/s,是 llama.cpp 的 4 倍!附三天完整踩坑记录

上周折腾了一件大事:手头有块 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 的两个"天然限制"

实测之后我修正了几个以前的错误认知,这里也分享给大家:

  1. NPU 不是全算子覆盖。这个 fork 是运行时混合调度:权重加载时 CPU 反量化 → 校准 → 按 NPU 流水线再量化,部分算子会回退 CPU(ARM NEON)。所以它吃不到 NPU 的全部红利
  2. 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=256temperature=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。

🩸 踩坑清单速查表(建议收藏)

  1. 驱动版本第一检查cat /sys/kernel/debug/rknpu/version,低于 0.9.8 会静默算错(乱码不报错),换 Armbian vendor 内核解决
  2. CMA 改 3Gextraargs=cma=3G,出厂 128M 跑 LLM 必 ENOMEM
  3. ulimit -n 1000000 必设:DMA buffer 占 fd,默认 1024 被 3B 打爆
  4. RK3588 只支持 W8A8 :W4A16 报错 not support quantized_dtype,别浪费时间试
  5. 压测每轮必须换 prompt:RKLLM 有前缀缓存,命中后 prefill 被跳过,数据虚高十倍
  6. 性能对比必须同路径:rkllm 的 HTTP 路径比 C demo 路径 decode 快 ~1.7 倍(计时口径不同),混着比会得出错误结论
  7. 板端先换 apt 镜像:官方源在板端 ~1 包/分钟,换北外镜像后分钟级
  8. HF 下载设 HF_HUB_DISABLE_XET=1:Xet 域名不走 hf-mirror,401 报错很迷惑
  9. pgrep -f '[f]lask_server.py' 要加方括号:不然会匹配到 ssh 包装进程,服务没起也"检测通过"
  10. 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 内容防缓存命中。

相关推荐
hacker7072 小时前
bolt.diy 鸿蒙 PC 适配全记录:让 AI 全栈开发工作台在 HarmonyOS PC 上真正跑起来
人工智能·华为·harmonyos
深小乐9 小时前
我在 Anthropic ELI5 上加了5样东西,做成了更好用的 ELI5+
人工智能
东风破_9 小时前
《LangGraph Memory:为什么 Agent 需要 Checkpointer?》
人工智能
锋行天下9 小时前
LangGraph 同级节点之间修改数据先后问题
人工智能
u1301309 小时前
AI 日报(2026年9月12日)
人工智能
东风破_10 小时前
《LangGraph Human-in-the-loop:Interrupt 如何让 Agent 等待人工确认》
人工智能
2601_9622186110 小时前
万象生鲜系统业财一体化底层打通技术自动生成经营账单
大数据·数据库·人工智能·python·算法
智塑未来10 小时前
中文短剧出海翻译工具横评:VMEG AI、鬼手、Vozo AI、ElevenLabs怎么选?
人工智能
东风破_10 小时前
《LangGraph 条件路由与循环:如何让 Agent 自己决定下一步?》
人工智能