rk3588大模型上手体验
过尽千帆皆不是,斜晖脉脉水悠悠,肠断白频洲
继上个月到手国产树毒派后,刷官方系统把玩了一番。参考了一些论坛和博客后,决定换其它系统玩玩。然后发现大模型在rk系列的NPU上可以跑一些蒸馏版本,于是便开始体验一下。
rknn大模型部署
官方的大模型主要是c和c++的一些库和demo组成,参考官方文档安装RKNN环境。通过下面命令可以查看目前npu驱动版本:
sudo dmesg | grep "Initialized rknpu"
根据板端系统的 python 版本将对应的 rknn_toolkit_lite2-2.3.0-cp3X-cp3X-manylinux_2_17_aarch64.manylinux2014_aarch64.whl 复制到板端,进入虚拟环境后使用 pip3 安装
pip3 install ./rknn_toolkit_lite2-2.3.0-cp3X-cp3X-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
然后安装rknn-toolkit2:
pip install -U rknn-toolkit2
安装rknn-toolkit2 lib:
wget https://raw.githubusercontent.com/airockchip/rknn-toolkit2/refs/heads/master/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so
sudo cp librknnrt.so /usr/lib/
Yolo系列
目前最新的yolo11版本已经可以通过ultralytics库来调用和运行,相关大模型权重文件在官方文档中可以查看下载,也可以通过ultralytics库来自己转换权重为rknn文件。注意调用时要带上metadata.yaml文件,权重文件路径不要加后缀.rknn,下面是测试脚本:
#!/bin/bash
yolo predict model='./yolo11n_3588_rknn_model' source='https://ultralytics.com/images/bus.jpg'
python测试脚本如下:
from ultralytics import YOLO
# Load the exported RKNN model
rknn_model = YOLO("./yolo11n_3588_rknn_model")
# Run inference
results = rknn_model("https://ultralytics.com/images/bus.jpg")
Deepseek蒸馏模型
参考官方的文件,可以用c++来部署,但是还是更习惯用python来部署。RKNN‑LLM(又名 RKLLM)目前没有官方 Python 接口用于直接在脚本中调用推理功能。理解它的运行机制如下: 🧠 当前 SDK 架构
- RKLLM‑Toolkit(PC 端模型转换工具) – 提供 Python API,用于将 HuggingFace 格式模型导出为 .rkllm 文件,可进行量化与平台优化。
- RKLLM Runtime(板端推理库) – 运行于 RK3588/RK3576 等设备上的 C/C++ 动态库,生成 .rkllm 后需通过调用 C 接口进行推理。
- 示例项目(C++ demo、shell 脚本等) – GitHub 上提供的示例多为 C/C++ 形式,而非 Python。
所以这里我利用 ctypes 或 cffi 封装 C 接口(如 rkllm_init、rkllm_infer、rkllm_release),为 Python 提供绑定接口。✅ ctypes 是最轻量级、最常用的做法。
✅ 使用前提
已有 RKNN-LLM 推理库,比如:
- 动态库封装的头文件,官方在github仓库中给出
- 动态库路径:/usr/lib/librkllm_runtime.so
- 模型文件:chatglm3.rkllm
- 你了解推理输入输出格式(例如是字符串,还是 token ID)
📦 示例目录结构
DeepSeek-R1-Distill-Qwen-1.5B_RKLLM/
├── python/
│ ├── rkllm_wrapper.py
│ └── run.py
├── demo_Linux_aarch64/
│ └── lib/
│ └── librkllmrt.so
├── DeepSeek-R1-Distill-Qwen-1.5B_W8A8_RK3588.rkllm
⚠️ 可选:设置 LD_LIBRARY_PATH
如果遇到 .so 加载问题,建议在运行前:
export LD_LIBRARY_PATH=../demo_Linux_aarch64/lib:$LD_LIBRARY_PATH
🧰步骤详解:用 ctypes 封装 .so 和头文件
- 1️⃣ 准备头文件(例如
rkllm.h)
从 GitHub 上 rkllm.h 看,你有类似下面的结构:
typedef struct {
char* model_path;
int context_len;
float temperature;
} rkllm_param;
int rkllm_init(rkllm_param* param);
char* rkllm_run(const char* input);
- 2️⃣ 加载
.so文件(如librkllmrt.so)
import ctypes
from ctypes import *
# 加载 so 文件
rkllm = ctypes.CDLL("/path/to/librkllmrt.so") # 注意路径
- 3️⃣ 定义结构体
rkllm_param(对应 C 的结构)
class rkllm_param(Structure):
_fields_ = [
("model_path", c_char_p),
("context_len", c_int),
("temperature", c_float)
]
- 4️⃣ 定义函数签名
# int rkllm_init(rkllm_param* param);
rkllm.rkllm_init.argtypes = [POINTER(rkllm_param)]
rkllm.rkllm_init.restype = c_int
# const char* rkllm_run(const char* input);
rkllm.rkllm_run.argtypes = [c_char_p]
rkllm.rkllm_run.restype = c_char_p
- 5️⃣ 调用函数
# 准备参数结构体
param = rkllm_param(
model_path=b"/path/to/model.bin", # 注意要是 bytes 类型
context_len=512,
temperature=0.8
)
# 初始化
ret = rkllm.rkllm_init(byref(param))
print("init returned", ret)
# 推理
output = rkllm.rkllm_run(b"你好,请问你是谁?")
print("output:", output.decode())
Python 调用 .so 的本质流程
.so本质是 ELF 格式共享库,导出符号函数如rkllm_run;ctypes.CDLL或cdll.LoadLibrary会读取动态库符号表;- 你需要设置
.argtypes和.restype告诉 Python 参数类型; ctypes.Structure映射C struct;- Python 调用时,底层通过 FFI(Foreign Function Interface)传参、执行并返回结果。
常见问题提示
| 问题类型 | 原因或解决方式 |
|---|---|
Segmentation fault |
参数传错(如结构体没初始化正确、内存越界) |
| Python crash | restype 或 argtypes 没定义或错误 |
| 字符串乱码 | 应使用 c_char_p 且传入 bytes 类型 |
| 返回结构体 | 用 POINTER(MyStruct) + .contents 提取 |
是否需要自动解析头文件?
你也可以用工具自动将 .h 头文件转换为 Python 结构,比如:
🔍 补充说明
下面是Python ctypes 封装代码,用于调用 rkllm.h 中定义的所有结构体、枚举和函数:
import ctypes
from ctypes import c_int, c_float, c_char_p, c_bool, c_void_p, POINTER, Structure, Union, c_size_t, c_int8, c_uint32, c_ubyte
# —— 常量定义 ——
CPU0 = 1 << 0
CPU1 = 1 << 1
# … 同理 CPU2–CPU7
# —— 枚举值 ——
RKLLM_RUN_NORMAL = 0
RKLLM_RUN_WAITING = 1
RKLLM_RUN_FINISH = 2
RKLLM_RUN_ERROR = 3
...
# —— 结构体定义 ——
class RKLLMExtendParam(Structure):
_fields_ = [
("base_domain_id", c_int),
("embed_flash", c_int8),
("enabled_cpus_num", c_int8),
("enabled_cpus_mask", c_uint32),
("reserved", c_ubyte * 106),
]
class RKLLMParam(Structure):
_fields_ = [
("model_path", c_char_p),
("max_context_len", c_int),
("max_new_tokens", c_int),
("top_k", c_int),
("n_keep", c_int),
("top_p", c_float),
("temperature", c_float),
("repeat_penalty", c_float),
("frequency_penalty", c_float),
("presence_penalty", c_float),
("mirostat", c_int),
("mirostat_tau", c_float),
("mirostat_eta", c_float),
("skip_special_token", c_bool),
("is_async", c_bool),
("img_start", c_char_p),
("img_end", c_char_p),
("img_content", c_char_p),
("extend_param", RKLLMExtendParam),
]
class RKLLMLoraAdapter(Structure):
_fields_ = [
("lora_adapter_path", c_char_p),
("lora_adapter_name", c_char_p),
("scale", c_float),
]
...
class RKLLMInputUnion(Union):
_fields_ = [
("prompt_input", c_char_p),
("embed_input", RKLLMEmbedInput),
("token_input", RKLLMTokenInput),
("multimodal_input", RKLLMMultiModelInput),
]
class RKLLMInput(Structure):
_anonymous_ = ("input",)
_fields_ = [
("input_type", c_int),
("input", RKLLMInputUnion),
]
...
class RKLLMInferParam(Structure):
_fields_ = [
("mode", c_int),
("lora_params", POINTER(RKLLMLoraParam)),
("prompt_cache_params", POINTER(RKLLMPromptCacheParam)),
("keep_history", c_int),
]
...
# —— 类型定义 ——
LLMHandle = c_void_p
LLMResultCallback = ctypes.CFUNCTYPE(None, POINTER(RKLLMResult), c_void_p, c_int)
# —— 加载 SO 库并设置函数签名 ——
rkllm = ctypes.cdll.LoadLibrary("librkllmrt.so")
rkllm.rkllm_createDefaultParam.restype = RKLLMParam
...
callback 函数是一个 C 回调,用于处理模型推理的结果(如文本、隐藏层、错误状态等)。我们可以将它完整转换为 Python 的 ctypes 回调函数,保留所有逻辑,包括保存隐藏层输出。
✅ Python 回调转换如下:
import ctypes
from ctypes import POINTER, c_void_p, c_int, cast
import numpy as np
# 假设这些枚举常量已定义
RKLLM_RUN_NORMAL = 0
RKLLM_RUN_WAITING = 1
RKLLM_RUN_FINISH = 2
RKLLM_RUN_ERROR = 3
# —— 类型定义 ——
'''
* @typedef LLMHandle
* @brief A handle used to manage and interact with the large language model.
'''
LLMHandle = c_void_p
'''
* @typedef LLMResultCallback
* @brief Callback function to handle LLM results.
* @param result Pointer to the LLM result.
* @param userdata Pointer to user data for the callback.
* @param state State of the LLM call (e.g., finished, error).
'''
LLMResultCallback = ctypes.CFUNCTYPE(None, POINTER(RKLLMResult), c_void_p, c_int)
# 假设 RKLLMResult、RKLLMResultLastHiddenLayer 等结构体已正确定义
# LLMResultCallback 类型也已定义为:
# LLMResultCallback = ctypes.CFUNCTYPE(None, POINTER(RKLLMResult), c_void_p, c_int)
def python_callback(result_ptr, userdata, state):
if not result_ptr:
result = None
else:
result = result_ptr.contents
if state == RKLLM_RUN_FINISH:
print('\n')
elif state == RKLLM_RUN_ERROR:
print('\\run error\n')
elif state == RKLLM_RUN_NORMAL:
''' ================================================================================================================
若使用GET_LAST_HIDDEN_LAYER功能,callback接口会回传内存指针:last_hidden_layer,token数量:num_tokens与隐藏层大小:embd_size
通过这三个参数可以取得last_hidden_layer中的数据
注:需要在当前callback中获取,若未及时获取,下一次callback会将该指针释放
=============================================================================================================== '''
if result != None:
print(result.text.decode('utf-8'), end='', flush=True)
else:
return
# 如果有隐藏层数据
embd_size = result.last_hidden_layer.embd_size
num_tokens = result.last_hidden_layer.num_tokens
hidden_ptr = result.last_hidden_layer.hidden_states
if embd_size != 0 and num_tokens != 0 and hidden_ptr:
data_size = embd_size * num_tokens
print(f"\n[隐藏层] embd_size: {embd_size}, num_tokens: {num_tokens}, 总大小: {data_size * 4} 字节")
# 将 C 指针转换为 numpy 数组
array_type = ctypes.c_float * data_size
np_array = np.ctypeslib.as_array(cast(hidden_ptr, POINTER(array_type)).contents)
np_array = np_array.reshape((num_tokens, embd_size))
# 保存为二进制文件
try:
np_array.astype(np.float32).tofile("last_hidden_layer.bin")
print("隐藏层数据已保存到 last_hidden_layer.bin")
except Exception as e:
print(f"保存文件失败: {e}")
# 将 python_callback 包装成 ctypes 函数指针
cb = LLMResultCallback(python_callback)
result.text是c_char_p,需要.decode('utf-8')。- 隐藏层是一个二维数组
[num_tokens][embd_size],我们用numpy把它还原出来。 - 数据写入
last_hidden_layer.bin与 C++ 等效。 - 你需要提前在 Python 中定义好
RKLLMResult和其内部结构体。 - 若不使用 numpy,也可以用 Python 的
bytearray手动复制数据,但 numpy 更高效且方便调试。 llmHandle为ctypes的c_void_p或实际结构体指针。rkllm_destroy()是你绑定的原生 C API,用ctypes加载即可。sys.exit(signal)会让程序以指定信号码退出。- 在
ctypes.Structure中,字段未赋值时默认是 0,但为了完全对应 C 的memset(&x, 0, sizeof(x)),我们调用了ctypes.memset。 - 如果你不确定结构体字段,只声明结构体时
memset也可清零所有未初始化内存。 ctypes.byref获取结构体引用,适用于类似memset(&obj, ...)的调用场景。
armbian系统相关
最后说一说armbian Debian 12 (Bookworm),蓝牙体验非常好,果然比官方系统好用!很多软件和依赖都可以直接sudo apt install装完即用。先来说说迁移系统后的一些问题:
-
更改语言后,在
~/.xsessionrc和~/.bashrc里也要更改对应的locales。 -
nmcli创建的热点需要安装
dnsmasq后才能成功开启 - 无屏模式
sudo systemctl set-default multi-user.target -
安装Cockpit,使用
armbian-config选择Software-Management-Cockpit安装 - docker安装出错,按照上面官方镜像一样安装会出错,需要安装
sudo apt update sudo apt install apparmor-utils sudo systemctl restart docker -
网络部分,参考debian官方文档
- RDP部分安装:
sudo apt update sudo apt install -y xfce4 xfce4-goodies sudo apt install -y xrdp xorgxrdp xserver-xorg-core xserver-xorg-video-fbdev echo startxfce4 > ~/.xsession chmod +x ~/.xsession - docker容器连wifi
docker run -d --name openwrt --network none --restart unless-stopped -v /home/lyc/Workspace/easytier-linux-aarch64:/data2 --privileged kiddin9/openwrt /sbin/init sudo ip link add veth-host type veth peer name veth-openwrt sudo ip addr add 192.168.166.1/24 dev veth-host sudo ip link set veth-host up PID=$(docker inspect -f '\{\{.State.Pid\}\}' openwrt) sudo ip link set veth-openwrt netns $PID sudo nsenter -t $PID -n ip addr add 192.168.166.2/24 dev veth-openwrt sudo nsenter -t $PID -n ip link set veth-openwrt up sudo nsenter -t $PID -n ip route add default via 192.168.166.1在容器里面先用
passwd改密码,接下来编辑/etc/config/network文件,加入下面的部分:config device option name 'veth-openwrt' option type 'ethernet' config interface 'wan' option device 'veth-openwrt' option proto 'static' option ipaddr '192.168.166.2' option netmask '255.255.255.0' option gateway '192.168.166.1' option ip6assign '60'。然后运行
/etc/init.d/network restart重启网络。关闭防火墙etc/init.d/firewall stop后就可以访问网页https://192.168.166.2来打开Luci网页了。为了联网需要开启NAT转发echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward sudo iptables -t nat -A POSTROUTING -s 192.168.166.0/24 -o wlx0c82680ba357 -j MASQUERADE禁用 docker 的默认 FORWARD 限制,查看是否被
FORWARD策略阻挡:sudo iptables -L FORWARD如果默认是
DROP,请设置为ACCEPT:sudo iptables -P FORWARD ACCEPT为了转发局域网的端口,接下来可行的操作就很多了,一个个介绍一下吧。
-
Cloudflared
首先安装一下
opkg update opkg install cloudflared cloudflared update然后开始登录
cloudflared tunnel login这个命令会在终端中显示一个 URL,你需要在浏览器中打开并授权登录 Cloudflare 账号。接下来通过以下命令创建一个新的 tunnel:
cloudflared tunnel create <your-tunnel-name>这个命令会生成一个 tunnel_id,后续需要用到。配置文件一般保存在 /etc/cloudflared/config.yml 中,若没有此文件,需要手动创建。
nano /etc/cloudflared/config.yml在文件中,配置你的 tunnel_id 和要代理的服务(例如 OpenWrt 的 Web 管理端口)。下面是一个例子:
tunnel: <your-tunnel-id> credentials-file: /etc/cloudflared/<your-tunnel-id>.json ingress: - hostname: <your-subdomain>.example.com service: ssh://10.12.0.3:22 # 这里是你的 OpenWrt 地址和端口 - service: http_status:404your-tunnel-id是你通过cloudflared tunnel create命令生成的 Tunnel ID。hostname是你想在 Cloudflare 上配置的域名,注意你需要提前将该子域的 DNS 解析设置为 Cloudflare 代理。service是你想通过 Tunnel 代理的服务地址,这里是内网某主机ssh服务10.12.0.3:22。 配置好之后,你可以通过以下命令启动 Tunnel:cloudflared tunnel run <your-tunnel-name> -
frp服务 本服务需要有公网IP,将本地内网的某个服务绑定到公网IP的某一端口。包括frpc和frps两个部分。一般frps部署在具有公网IP的机器上,s即是server。先下载文件
wget https://github.com/fatedier/frp/releases/download/v0.62.1/frp_0.62.1_linux_arm64.tar.gz tar -zxvf frp_0.62.1_linux_arm64.tar.gz然后在上面进行开启frp服务,先写入配置文件
frp.ini下面的内容:[common] bind_port = 7000 dashboard_port = 7500 dashboard_user = admin dashboard_pwd = admin token = set_your_token之后开启服务:
nohup ./frps -c ./frp.ini > log.txt 2>&1 &接下来配置frp客户端。直接在网页上System-SOftware里下载luci-frpc-app然后在网页进行配置即可。也可以将frps配置为systemd系统服务,就不用每次使用命令 自己启动了,具体步骤如下。先创建文件
/etc/systemd/system/frps.service,然后填入下面的内容,路径自行修改:[Unit] Description=Frp Server Service After=network.target [Service] Type=simple User=nobody Restart=on-failure RestartSec=5s ExecStart=/path/to/frps -c /path/to/frps/config/file.ini [Install] WantedBy=multi-user.target然后使用
sudo systemctl daemon-reload来加载新的服务单元,使用sudo systemctl start frps来启动服务,使用sudo systemctl enable frps来开机启动。 注意User=nobody时,使用sudo -u nobody /path/to/frps -h来验证nobody的运行权限,否则会出现systemd 203错误。可以放到/usr/local/bin目录下,另外 将配置文件的读取权限更改,运行命令:setfacl -m u:nobody:r /path/to/frp.ini。 -
socat服务
相比之下socat服务更简单,只需要来到iStoreOS来安装一下Socat。但是容器本身需要配置端口映射,将一些端口映射到容器外。此时如果宿主机本身就有公网IP的话,就可以将端口直接映射到域名+端口了。最后去网页iStore安装socat,通过网页接口设置端口转发功能。另外可以在网页的
System-Software里面安装lua-app-easytire和easytire来开启组网。
-
-
docker容器安装ssh,启用gpu支持
docker run -it --gpus all --name pydev -v /data/yanchaoliu:/data2 --privileged -p 65530:65530 -p 65531:65531 -p 65532:65532 ubuntu:latest /bin/bash进去就可以使用命令
nvidia-smi查看gpu情况。然后安装adduser和sudo,apt install -y adduser sudo nano创建用户lyc,并加入sudo权限
adduser lyc usermod -aG sudo lyc加入公钥
su lyc mkdir ~/.ssh cd ~/.ssh touch authorized_keys nano authorized_keys # 加入公钥到文件 chmod 600 authorized_keys然后安装ssh服务,编辑配置文件
sudo apt install openssh-server sudo nano /etc/ssh/sshd_config sudo mkdir /var/run/sshd sudo /etc/init.d/ssh start - docker event监听事件触发
如果你需要长期在宿主机里实现“事件驱动”效果:写一个监听脚本如下
#!/bin/bash docker events --filter 'event=start' --filter 'container=my_container' | while read event do echo "容器启动事件触发: $event" /opt/scripts/my_script.sh done用 systemd service 或 supervisord 管理它,让它常驻并随宿主机启动。例如
/etc/systemd/system/docker-event-listener.service:[Unit] Description=Run script when my_container starts After=docker.service [Service] ExecStart=/bin/bash /opt/scripts/docker_event_listener.sh Restart=always [Install] WantedBy=multi-user.target这样就真的和“中断”一样了,事件发生时触发,不发生时零开销地挂在那里。
| 微信(WeChat Pay) | 支付宝(AliPay) |
|
|
| 比特币(Bitcoin) | 以太坊(Ethereum) |
|
|
| 以太坊(Base) | 索拉纳(Solana) |
|
|