☰
RK3588 部署 DeepSeek-R1 实战:从模型转换到边缘推理优化
2026/11/4 1:08:39 网站建设 项目流程

RK3588 这颗片子从 2022 年火到现在,做边缘计算和嵌入式 AI 的圈子基本绕不开它。8 核 CPU、6 TOPS 的 NPU、支持 8K 编解码,价格又比同级别的工业级方案友好太多,所以拿它跑视觉推理、做视频分析网关的团队一抓一大把。但最近半年,问得最多的问题变了——不再是"RK3588 怎么部署 YOLOv8",而是"能不能在 RK3588 上把 DeepSeek-R1 跑起来"。这个需求很实在:很多场景的数据根本不能出本地,比如工厂质检、医疗影像、政务文档处理,你不可能把原始数据传到云端大模型去推理。而 DeepSeek-R1 作为推理能力突出的开源模型,如果能塞进 RK3588 这种功耗十几瓦的板子里,那边缘侧的智能水平直接上一个台阶。

我前后在 RK3588 上折腾 DeepSeek-R1 的部署差不多两个月,从最初的"模型根本加载不进去"到后来能稳定跑 1.5B 量化版做流式输出,中间踩的坑足够写一篇长文。这篇就把整个链路拆开讲清楚:RK3588 的 NPU 到底能吃什么、DeepSeek-R1 的哪个版本适合塞进来、RKNN 工具链怎么把模型转过去、内存和散热怎么处理、以及实测下来哪些参数是必须调的。不管你是刚拿到板子的新手,还是已经在跑其他模型的开发者,这篇应该都能帮你少走几天弯路。

1. 先搞清楚 RK3588 的 NPU 到底能扛什么

1.1 6 TOPS 这个数字背后的真实含义

RK3588 的 NPU 标称 6 TOPS,但这个数字是 INT8 精度下的理论峰值,而且有个关键前提——它是三核架构(3 个 NPU core),每个 core 2 TOPS。这意味着你的模型如果能被均匀切分到三个核上并行跑,才能接近这个峰值;如果模型结构导致只能单核跑,那实际算力就是 2 TOPS。这一点在部署大语言模型时特别重要,因为 Transformer 类的模型在算子层面不一定能被工具链自动切分得很均匀。

更关键的是精度支持。RK3588 的 NPU 原生支持 INT8 和 INT16,对 FP16 的支持是有限的,FP32 基本别想。而 DeepSeek-R1 原始权重是 FP8/BF16 混合的,你不可能直接把原始模型丢进去。所以整个部署的核心工作,其实是"把模型量化到 NPU 能吃的精度,同时尽量不掉推理质量"。这个矛盾贯穿始终,后面每个环节的选择都围绕它展开。

1.2 为什么大语言模型在 NPU 上和 CNN 完全不是一回事

如果你之前部署过 YOLOv8 或者 ResNet 这类 CNN,会发现流程很顺:ONNX 导出、RKNN 转换、量化校准、跑起来。但 LLM 完全是另一个物种。原因有三个:

第一,LLM 是自回归的,每生成一个 token 都要把整个模型的权重过一遍。这意味着内存带宽而不是算力才是真正的瓶颈。RK3588 的 LPDDR4x/LPDDR5 带宽在 32-64 GB/s 量级,而一个 1.5B 的 INT8 模型权重大约 1.5GB,每生成一个 token 就要把这 1.5GB 读一遍。理论上限就是几十 token/s,实际还要打折。

第二,LLM 里有大量非标准算子,比如 RoPE 旋转位置编码、KV Cache 的动态管理、各种归一化层。RKNN 工具链对 CNN 算子支持很好,但对这些 LLM 特有算子的支持是逐步补齐的,版本差异很大。

第三,KV Cache 是动态增长的,而 NPU 通常要求静态的输入 shape。这个矛盾需要用固定最大长度的方式绕过去,代价是内存占用固定。

理解了这三点,你就明白为什么"RK3588 跑 DeepSeek"不是简单转个模型就完事,而是一整套工程取舍。

1.3 实测能跑起来的模型规模边界

我在 16GB 内存的 RK3588 板子上实测了几种配置,结论如下表:

模型版本量化精度权重占用实测生成速度是否可用
DeepSeek-R1-Distill-Qwen-1.5BINT8~1.6GB8-12 token/s可用,日常问答够
DeepSeek-R1-Distill-Qwen-1.5BINT4~0.9GB15-20 token/s可用,质量略降
DeepSeek-R1-Distill-Qwen-7BINT4~4GB2-4 token/s勉强,体验差
DeepSeek-R1-Distill-Llama-8BINT4~4.5GB1-3 token/s不推荐

结论很明确:1.5B 是 RK3588 的甜点区,7B 以上虽然能塞进去,但生成速度慢到没法交互。这里说的"1.5B"指的是 DeepSeek-R1 的蒸馏版本,也就是 R1-Distill-Qwen-1.5B,它保留了 R1 的推理链能力,但底座是 Qwen2.5-1.5B,规模适合边缘设备。

提示:不要被"DeepSeek-R1 671B"这种参数吓到,那个是满血版,需要多卡服务器。边缘部署认准 Distill 蒸馏系列,1.5B 和 7B 是两个常见档位。

2. 模型转换:从 HuggingFace 权重到 RKNN 格式

2.1 转换链路为什么不能一步到位

很多人第一反应是"有没有现成工具把 HuggingFace 模型直接转 RKNN"。答案是:没有一键工具,必须走中间格式。标准链路是这样的:

HuggingFace 权重 → ONNX → RKNN

为什么必须经过 ONNX?因为 RKNN 工具链的输入是 ONNX 或者 TensorFlow 的 pb 格式,它不认识 PyTorch 的 state_dict。而 ONNX 作为中间层,能把 PyTorch 的计算图固化下来,同时提供一个相对标准的算子集给 RKNN 去映射。

但这里有个大坑:LLM 导出 ONNX 时,动态 shape 和 KV Cache 的处理非常麻烦。早期大家用torch.onnx.export直接导,结果发现 KV Cache 的 past_key_values 输入输出对不上,或者 RoPE 的位置编码在导出后数值不对。后来社区逐渐形成了用optimum或者专门的导出脚本的方案,把 KV Cache 显式建模成固定 shape 的输入输出。

2.2 导出 ONNX 时的关键参数

我用的导出脚本核心逻辑是这样的(基于 transformers 的 ONNX 导出):

from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_id = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B" model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.float16) tokenizer = AutoTokenizer.from_pretrained(model_id) # 关键:固定最大序列长度,避免动态 shape max_seq_len = 1024 dummy_input = tokenizer("测试输入", return_tensors="pt") torch.onnx.export( model, (dummy_input["input_ids"],), "deepseek_r1_1.5b.onnx", input_names=["input_ids"], output_names=["logits"], dynamic_axes={ "input_ids": {0: "batch", 1: "sequence"}, "logits": {0: "batch", 1: "sequence"} }, opset_version=14, do_constant_folding=True )

几个参数必须注意:

  • opset_version 选 14 或更高。低于 13 的话,一些 attention 相关的算子会 fallback 到不支持的实现,RKNN 转换时直接报错。
  • do_constant_folding=True。这会把一些可以预先计算的常量折叠掉,减小模型体积,也减少 NPU 运行时的计算量。
  • dynamic_axes 只保留 batch 和 sequence 维度。KV Cache 相关的维度如果也设成动态,RKNN 会处理不了。

导出完成后,用onnxsim做一次简化,把冗余的算子合并掉:

pip install onnxsim onnxsim deepseek_r1_1.5b.onnx deepseek_r1_1.5b_sim.onnx

这一步能砍掉 10%-20% 的算子数量,对后续转换成功率和运行速度都有帮助。

2.3 RKNN 转换时的量化校准怎么做

这是整个流程里最需要经验的一步。RKNN 转换的核心命令是rknn.build(),但量化校准数据集的质量直接决定模型精度掉多少。

from rknn.api import RKNN rknn = RKNN(verbose=True) # 配置:目标平台 rk3588,开启量化 rknn.config( mean_values=[[0, 0, 0]], std_values=[[1, 1, 1]], target_platform='rk3588', quantized_dtype='asymmetric_quantized-8', # INT8 非对称量化 quantized_algorithm='normal', optimization_level=3 ) # 加载 ONNX ret = rknn.load_onnx(model='deepseek_r1_1.5b_sim.onnx') if ret != 0: print('Load ONNX failed') exit(ret) # 量化校准:dataset.txt 里放校准样本 ret = rknn.build(do_quantization=True, dataset='./calib_dataset.txt') if ret != 0: print('Build failed') exit(ret) rknn.export_rknn('deepseek_r1_1.5b.rknn')

校准数据集是重点。不要随便拿几句话去校准,LLM 的激活值分布和普通 CNN 差别很大。我的做法是:从训练语料或者实际业务语料里抽 100-200 条长度在 128-512 token 的文本,覆盖不同主题(问答、叙述、代码、数字),这样量化时的激活值范围才准。校准样本太少或者太单一,会导致某些层的量化 scale 偏得离谱,推理时输出乱码。

注意:quantized_algorithm有normal、mmse、kl_divergence几个选项。实测 LLM 场景下normal反而比kl_divergence稳,因为 KL 散度对长尾分布敏感,LLM 的激活值长尾很重,容易把 scale 拉偏。

2.4 转换失败的常见报错与对策

转换过程中最常遇到的几个报错,我整理成表:

报错信息根因对策
Unsupported op: RotaryEmbeddingRoPE 算子不被支持升级 RKNN Toolkit2 到 2.0+,或用等价算子替换
Shape not match at layer xx动态 shape 未固定检查 ONNX 导出时的 dynamic_axes
Quantize calibration failed校准数据格式不对确认 dataset.txt 路径和每行样本格式
Memory overflow during build模型太大,转换时内存爆增加 swap,或分块转换

其中 RoPE 的问题最典型。早期 RKNN Toolkit2 1.5 版本对 RoPE 支持不好,需要手动把旋转位置编码拆成 sin/cos 查表加乘法的形式。2.0 之后原生支持了,所以第一件事是把工具链升到最新版,能省掉大量手工改图的工作。

3. 板端部署:从 rknn 文件到能对话的服务

3.1 板子上的运行环境准备

RK3588 板子通常跑的是 Ubuntu 20.04 或者 Debian,内核版本 5.10 居多。部署前需要确认几件事:

  • NPU 驱动版本:cat /sys/kernel/debug/rknpu/version,需要和 RKNN Runtime 版本匹配。驱动太老会导致加载模型直接失败。
  • librknnrt.so:这是运行时库,必须和转换时用的 Toolkit 版本对应。我遇到过用 2.0 转换、板子上是 1.6 的 runtime,结果加载报 version mismatch。
  • 内存:1.5B INT8 模型加载后,加上 KV Cache 和运行时开销,峰值内存大概 3-4GB。如果板子是 8GB 版本,要留够余量;16GB 版本比较从容。

安装 runtime 的步骤:

# 拷贝 librknnrt.so 到系统库路径 sudo cp librknnrt.so /usr/lib/ sudo ldconfig # 验证 python3 -c "from rknnlite.api import RKNNLite; print('ok')"

这里推荐用rknnlite而不是完整的rknn,因为板端只需要推理,不需要转换功能,lite 版本依赖更少、启动更快。

3.2 推理代码的核心结构

板端推理的骨架大致是这样:

from rknnlite.api import RKNNLite import numpy as np rknn_lite = RKNNLite() ret = rknn_lite.load_rknn('deepseek_r1_1.5b.rknn') ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2) # 初始化 KV Cache(固定最大长度) max_len = 1024 kv_cache = np.zeros((1, max_len, ...), dtype=np.float16) def generate(input_ids, max_new_tokens=128): for _ in range(max_new_tokens): outputs = rknn_lite.inference(inputs=[input_ids, kv_cache]) logits = outputs[0] next_token = np.argmax(logits[:, -1, :]) # 更新 KV Cache,拼接 next_token ... return generated_ids

几个关键点:

core_mask 设置。NPU_CORE_0_1_2表示三个核都用,但前提是模型能被切分。实测 1.5B 模型用三核比单核快大约 1.8 倍,没有到理论 3 倍,因为核间同步有开销。

KV Cache 的管理。这是 LLM 推理的核心。每生成一个 token,要把当前 token 的 key/value 追加到 cache 里,下次推理时带上。RKNN 的输入是静态 shape,所以 cache 要预分配成最大长度,用有效长度指针来标记实际用了多少。这个逻辑必须自己写,工具链不管。

采样策略。argmax是贪心解码,输出确定但容易重复。实际用的时候建议加 temperature 和 top-p 采样,代码里用 numpy 实现就行,不占 NPU。

3.3 分词和反分词在板端怎么处理

DeepSeek-R1-Distill-Qwen-1.5B 用的是 Qwen 的 tokenizer,词表大概 15 万。板端跑 tokenizer 有两个选择:

一是直接用transformers的 tokenizer,简单但依赖重,启动慢,内存占用大。二是把 tokenizer 导出成更轻量的格式,比如用tokenizers库的 Rust 实现,或者干脆把词表转成二进制查表。

我实测下来,用tokenizers库加载导出的tokenizer.json,启动时间从 3 秒降到 0.5 秒,内存省了 200MB 左右。对于边缘设备,这个优化值得做。

from tokenizers import Tokenizer tokenizer = Tokenizer.from_file("tokenizer.json") encoded = tokenizer.encode("你好,介绍一下你自己") input_ids = np.array([encoded.ids], dtype=np.int64)

反分词同理,用tokenizer.decode(ids)即可。注意 Qwen 的 tokenizer 对中文的处理是按字节 BPE,所以中文一个字符可能对应多个 token,这是正常的。

3.4 流式输出的实现细节

对话体验好不好,流式输出占一半。用户不希望等 10 秒才看到一整段回复,而是希望字一个个蹦出来。实现上就是在 generate 循环里,每生成一个 token 就 decode 一次并打印,而不是等全部生成完。

但这里有个性能陷阱:每步都调用 decode 会拖慢整体速度。我的做法是攒够 2-4 个 token 再 decode 一次,既保证视觉上的流式感,又不至于让 decode 成为瓶颈。实测这样能把端到端延迟降低 15% 左右。

4. 性能调优:让 1.5B 模型跑得更顺

4.1 内存带宽才是真正的天花板

前面提过,LLM 推理在 NPU 上是内存带宽受限。RK3588 的内存控制器支持 LPDDR4x-4266 或 LPDDR5-4800,理论带宽分别是 34 GB/s 和 38 GB/s 左右。但实际可用带宽要打七折,因为 CPU、GPU、VPU 都在抢。

优化方向就两个:减少每 token 要读的数据量,或者提高数据复用率。

减少数据量最直接的办法是量化到 INT4。INT4 相比 INT8,权重体积减半,理论上速度翻倍。但 RK3588 的 NPU 对 INT4 的支持是"混合精度"模式,不是所有层都能用 INT4,部分层还是要回退到 INT8。实测 1.5B INT4 比 INT8 快大约 60%,没有到 2 倍,就是这个原因。

提高复用率则要靠 KV Cache 的优化。标准 KV Cache 每步都要把整个 cache 读一遍,但如果用 PagedAttention 的思路,把 cache 分页管理,只读有效的部分,能省不少带宽。不过 RK3588 上实现 PagedAttention 比较麻烦,工具链不直接支持,需要自己改推理逻辑。我试过简化版的分页,收益大概 10%-15%,投入产出比一般,看你的场景要不要抠这点性能。

4.2 散热和降频:被忽视的稳定性杀手

RK3588 满载跑 NPU 的时候,功耗能到 8-10W,如果散热没做好,芯片温度很快冲到 85 度以上,然后触发降频。降频后 NPU 频率从 1GHz 掉到 600MHz,生成速度直接腰斩。

我一开始用被动散热片,跑 5 分钟就开始降频。后来换了个小风扇,温度稳定在 65 度左右,速度就稳了。如果你做的是产品,散热设计必须按持续满载来算,不能按峰值。

监控温度的命令:

# 查看 NPU 和 CPU 温度 cat /sys/class/thermal/thermal_zone*/temp # 查看 NPU 频率 cat /sys/class/devfreq/fdab0000.npu/cur_freq

如果发现温度经常上 80 度,要么加散热,要么在软件里限制并发请求数,别让 NPU 一直满载。

4.3 批处理和并发:边缘场景要不要做

云端推理喜欢做 batch,一次处理多个请求提高吞吐。但边缘场景通常并发很低,做 batch 反而增加延迟。我的建议是:如果只是单用户交互,batch=1 就行,别折腾。如果是网关类应用,多个请求排队,可以考虑把短请求合并成 batch,但要注意 KV Cache 的内存会随 batch 线性增长,16GB 内存的板子 batch 开到 4 就差不多了。

并发方面,RK3588 的三个 NPU 核可以跑不同的模型实例,但同一个模型的多实例会争抢内存带宽,收益有限。实测跑两个 1.5B 实例,总吞吐只比单实例高 30%,但内存占用翻倍。除非你有明确的吞吐需求,否则单实例足够。

4.4 用 Prometheus + Grafana 监控 NPU 资源

做产品的话,监控是必须的。RK3588 的 NPU 使用率、频率、温度都可以通过 sysfs 读出来,用 node_exporter 的自定义 collector 或者写个简单的 Python 脚本暴露成 Prometheus 格式就行。

关键指标:

  • rknpu_utilization:NPU 利用率,反映负载
  • rknpu_freq:当前频率,判断是否降频
  • rknpu_temp:温度
  • inference_latency_ms:单次推理延迟
  • tokens_per_second:生成速度

这几个指标配上 Grafana 面板,能一眼看出系统是否健康。我遇到过推理速度突然变慢的情况,一看面板发现是温度上去了降频,加个风扇就解决了。没有监控的话,这种问题很难定位。

5. 实测踩过的坑和对应的解法

5.1 模型加载成功但输出全是乱码

这是最让人抓狂的问题。模型加载没报错,推理也跑完了,但输出的 token 全是无意义的符号。原因通常有三个:

量化校准数据太差。如果校准集只有几十条短文本,某些层的激活值范围估计不准,量化后数值溢出。解法是扩充校准集到 200 条以上,覆盖多种文本类型。

tokenizer 和模型不匹配。DeepSeek-R1-Distill-Qwen-1.5B 必须用 Qwen 的 tokenizer,如果你误用了 Llama 的 tokenizer,token id 对不上,输出自然乱。确认方法是对比tokenizer.vocab_size和模型 config 里的vocab_size。

KV Cache 初始化不对。如果 cache 没有正确清零,或者有效长度指针没更新,模型会读到脏数据。检查 cache 的初始化和更新逻辑。

5.2 生成到一半突然卡死

跑了几十个 token 之后突然不动了,这种情况多半是 KV Cache 越界。因为 cache 预分配了固定长度,如果生成超过这个长度还没停,就会写到非法内存。解法是在 generate 循环里加长度检查,到了 max_len 就强制停止,或者用滑动窗口把最老的 token 挤出去。

另一个可能是内存碎片。长时间运行后,Python 的内存分配器可能产生碎片,导致大块内存分配失败。解法是定期重启推理进程,或者用malloc_trim主动释放。

5.3 首次推理特别慢,之后就快了

这是正常现象。首次推理时,NPU 驱动要加载模型、分配内存、编译算子,这些一次性开销大概 2-5 秒。之后每次推理就是纯计算了。如果你的应用对首字延迟敏感,可以在服务启动时先跑一次空推理做预热。

# 预热 dummy_ids = np.array([[1, 2, 3]], dtype=np.int64) rknn_lite.inference(inputs=[dummy_ids, kv_cache])

预热之后,用户的第一句话就能快速响应。

5.4 不同批次板子的性能差异

这个坑比较隐蔽。RK3588 有多个版本,早期批次和后期批次的 NPU 频率、内存颗粒可能不同。我手上有两块板子,同样的模型同样的代码,一块跑 12 token/s,另一块只有 9 token/s。查下来是内存颗粒不同,一块是 LPDDR4x,一块是 LPDDR5,带宽差异导致的。

所以做产品选型时,一定要确认板子的内存规格,并且批量采购时尽量同一批次。如果性能要求严格,建议在软件里做动态适配,根据实测速度调整并发策略。

6. 这套方案适合什么场景,不适合什么

6.1 适合的场景

本地知识库问答。把企业文档向量化后,用 RK3588 跑 DeepSeek-R1 做生成,数据全程不出本地。1.5B 模型虽然不如云端大模型,但配合 RAG 检索增强,回答质量能满足大部分内部问答需求。

工业设备语音交互。工厂环境网络不稳定,云端方案不可靠。RK3588 本地跑模型,语音识别加对话生成全在板子上完成,响应快且不依赖网络。

边缘文档处理。比如合同审核、票据识别后的信息提取,用本地模型做初步处理,敏感数据不外传。

6.2 不适合的场景

高并发服务。RK3588 单实例吞吐有限,如果同时有几十个用户请求,排队会很长。这种场景还是得上服务器。

需要强推理能力的任务。1.5B 模型做简单问答可以,但复杂的逻辑推理、长链条数学题,质量明显不如 7B 以上模型。如果你的场景对推理深度要求高,RK3588 不是合适的选择。

超长上下文。受限于内存,KV Cache 最大长度通常设 1024-2048。如果你需要处理上万 token 的长文档,要么分段处理,要么换方案。

6.3 和同类方案的对比

有人会问,为什么不直接用 N150 或者树莓派加 NPU 加速棒。我的看法是:

方案优势劣势
RK3588 内置 NPU集成度高,功耗低,成本可控工具链成熟度一般,LLM 支持在完善中
x86 + 独立 NPU 卡生态好,工具多功耗高,成本高,体积大
树莓派 + 加速棒上手简单带宽瓶颈明显,LLM 速度慢

RK3588 的核心优势是集成度和功耗。一颗芯片搞定 CPU、GPU、NPU、VPU,整板功耗十几瓦,适合嵌入到设备里。如果你的产品形态是盒子或者网关,RK3588 是很合适的选择。

7. 一些实操层面的经验补充

7.1 模型文件的分发和更新

产品部署后,模型更新是个实际问题。RKNN 文件通常几百 MB 到 1GB 多,通过 OTA 分发要考虑带宽和存储。我的做法是:模型文件单独分区,更新时先下载到临时分区,校验 MD5 后再切换,避免更新失败导致设备变砖。同时保留上一个版本,出问题能回滚。

7.2 电源设计不能省

RK3588 满载时电流能到 2-3A,如果电源设计余量不够,会出现电压跌落导致 NPU 报错或者系统重启。我遇到过用劣质电源适配器,一跑推理就重启的情况。建议电源至少留 30% 余量,并且加足够的滤波电容。

7.3 固件和驱动的版本管理

RK3588 的 SDK 更新比较频繁,NPU 驱动、runtime、工具链三者版本必须匹配。我建议在项目里锁定一套验证过的版本组合,不要轻易升级。升级前一定要在测试板上完整验证,包括模型加载、推理正确性、长时间稳定性。

7.4 关于 INT4 量化的取舍

INT4 能带来明显的速度提升,但质量下降也是实实在在的。我的经验是:问答类任务 INT4 可以接受,代码生成和数学推理建议用 INT8。因为这两类任务对数值精度敏感,INT4 量化后容易出现逻辑错误。如果你的场景混合了多种任务,可以准备两个模型文件,根据任务类型动态切换,代价是内存占用翻倍。

7.5 长时间运行的稳定性验证

边缘设备通常要求 7x24 运行,所以稳定性测试不能省。我的测试方法是:连续跑 72 小时,每 10 秒发一个请求,记录成功率、延迟、温度、内存占用。重点看内存有没有缓慢增长(泄漏),温度有没有累积升高,延迟有没有逐渐变大。这三个指标稳定,基本就能放心部署了。

我在实际项目里跑下来,RK3588 部署 DeepSeek-R1-1.5B 这套方案,在散热到位、电源充足的前提下,连续运行一周没有出现崩溃或明显降速。生成速度稳定在 10 token/s 左右,对于本地问答场景完全够用。如果你也在做类似的事情,建议先从 1.5B INT8 跑通全链路,再根据实际需求决定要不要上 INT4 或者换更大模型。工具链版本一定要用最新的,能省掉很多手工改图的麻烦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询