Qwen3-VL 是阿里巴巴Qwen团队于2025年9月24日发布的多模态视觉语言模型,4B 是该系列中经过蒸馏或轻量化设计的小参数版本,我们在RK3576上面运行此模型,使用硬件加速(NPU)必须要将模型进行格式的转化,我们需要分别转化这个模型,最终生成两个核心文件:
- .rknn 文件:包含模型的视觉编码器(Visual Encoder) 部分,负责将图像转换为特征向量。
- .rkllm 文件:包含模型的大语言模型(LLM) 部分,负责理解图像特征并进行文本推理与生成。
而要进行这两个文件的转化,需要用到两个瑞芯微(rockchip)工具:
RKNN-Toolkit2: 专门用于将视觉部分转换为 NPU 可执行的
.rknn格式。RKLLM-Toolkit: 专门用于将语言模型部分进行量化和转换,生成
.rkllm格式。
上述部分都完成之后,将模型运行在板子上面则需要另一个瑞芯微(rockchip)工具:
- RKLLM-Runtime:这是运行在开发板 Linux 系统上的推理引擎(C++ 库) ,它负责加载上述两个模型文件,并调用 NPU 驱动进行高性能推理。
一、流程说明
二、环境准备
主机环境:Ubuntu22.04(x86)
开发板:泰山派3M-RK3576
数据线:连接PC和开发板用于ADB传输文件。
RKNN-LLM
拉取RKNN-LLM仓库:
仓库地址:https://github.com/airockchip/rknn-llm
这是瑞芯微官方提供的开源仓库
git clone https://github.com/airockchip/rknn-llm.git安装miniforge3
为了防止在一个主机中不同的环境造成的 python 环境问题,我们使用 miniforge3 管理。
安装 miniforge3 :
# 下载 miniforge3 安装脚本
wget -c https://mirrors.bfsu.edu.cn/github-release/conda-forge/miniforge/LatestRelease/Miniforge3-Linux-x86_64.sh
# 运行安装脚本
bash Miniforge3-Linux-x86_64.sh
# 1.按下Enter回车继续运行
# 2.然后使用向下箭头,向下滚动查看协议
# 3.最后输入yes
# 4.提示Proceed with initialization?输入yes2
3
4
5
6
7
8
9
10
可以去 https://mirrors.bfsu.edu.cn/github-release/conda-forge/miniforge/LatestRelease/ 这个目录下查看目前最新的 .sh 文件名。
初始化 conda 环境变量:
source ~/miniforge3/bin/activate成功之后,命令行前方会显示一个
(base)
创建RKLLM-Toolkit环境
创建并激活 Conda 环境:TaishanPi3-RKLLM-Toolkit(这里推荐使用 python 3.10 版本)
# 创建环境
conda create -n TaishanPi3-RKLLM-Toolkit python=3.10
# 遇到Proceed ([y]/n)?
# 输入y即可2
3
4
5
激活 Conda 环境:
conda activate TaishanPi3-RKLLM-Toolkit安装RKLLM-Toolkit:
在
rknn-llm/rkllm-toolkit/packages/目录下有好几种 whl 文件可以选择:
- rkllm_toolkit-1.2.3-cp39-cp39-linux_x86_64.whl
- rkllm_toolkit-1.2.3-cp310-cp310-linux_x86_64.whl
- rkllm_toolkit-1.2.3-cp311-cp311-linux_x86_64.whl
- rkllm_toolkit-1.2.3-cp312-cp312-linux_x86_64.whl
其中我们根据 python 版本选择不同的文件,我们创建的 Conda 环境使用的是python3.10,所以我们选择
cp310-cp310字样的文件。如果是python3.12则可以使用
cp312-cp312字样的文件。
# 使用阿里云镜像https://mirrors.aliyun.com/pypi/simple
pip install rknn-llm/rkllm-toolkit/packages/rkllm_toolkit-1.2.3-cp310-cp310-linux_x86_64.whl -i https://mirrors.aliyun.com/pypi/simple2
安装完成之后,退出 TaishanPi3-RKLLM-Toolkit 环境:
conda deactivate创建RKNN-Toolkit2环境
创建并激活 Conda 环境:TaishanPi3-RKNN-Toolkit2(这里推荐使用 python 3.10 版本)
# 创建环境
conda create -n TaishanPi3-RKNN-Toolkit2 python=3.10
# 遇到Proceed ([y]/n)?
# 输入y即可2
3
4
5
激活 Conda 环境:
conda activate TaishanPi3-RKNN-Toolkit2安装RKNN-Toolkit2:
根据 官方文档 的要求至少要 >= 2.3.2
# 使用阿里云镜像https://mirrors.aliyun.com/pypi/simple
pip install rknn-toolkit2 -i https://mirrors.aliyun.com/pypi/simple2
安装完成之后,退出 TaishanPi3-RKNN-Toolkit2 环境:
conda deactivate三、拉取模型
我们所使用的是 Qwen3-VL-4B-Instruct模型 ,从huggingface/modelscope中拉取模型文件,以供我们后续操作:
- 进入
TaishanPi3-RKLLM-Toolkit环境
conda activate TaishanPi3-RKLLM-Toolkit- 安装
git-lts
sudo apt update && sudo apt install git-lfs- 拉取模型
git clone https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct
# 或者使用国内的魔塔社区的模型
git clone https://www.modelscope.cn/Qwen/Qwen3-VL-4B-Instruct.git2
3
4
5
四、模型转化
我们将继续在 TaishanPi3-RKLLM-Toolkit 环境中进行操作,导出两个模型文件:
- 导出 LLM 模型部分 (.rkllm)
- 导出 Vision 部分为 ONNX (.onnx)
进入rknn-llm/examples/multimodal_model_demo 目录下,为了防止py脚本中的路径出错:
cd rknn-llm/examples/multimodal_model_demo生成数据集文件
将 rknn-llm/examples/multimodal_model_demo/data/make_input_embeds_for_quantize.py 脚本文件,修改为下面的样子:
因为原本的情况下,在py脚本中默认使用的是qwen2_vl的架构格式去构建的,而我们需要的是qwen3_vl 架构,所以需要修改py脚本,同时兼容 Qwen2 和 Qwen3 的 API 差异。
import torch
import os
import torchvision.transforms as T
from torchvision.transforms.functional import InterpolationMode
from PIL import Image
import json
import numpy as np
from tqdm import tqdm
from transformers import AutoModel, AutoTokenizer, AutoProcessor
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--path', type=str, default='Qwen/Qwen2-VL-2B-Instruct', help='model path', required=False)
args = parser.parse_args()
path = args.path
if "Qwen3" in path:
from transformers import Qwen3VLForConditionalGeneration as ModelClass
else:
from transformers import Qwen2VLForConditionalGeneration as ModelClass
model = ModelClass.from_pretrained(
path, torch_dtype="auto", device_map="cpu",
low_cpu_mem_usage=True,
trust_remote_code=True).eval()
processor = AutoProcessor.from_pretrained(path)
datasets = json.load(open("data/datasets.json", 'r'))
for data in datasets:
image_name = data["image"].split(".")[0]
imgp = os.path.join(data["image_path"], data["image"])
image = Image.open(imgp)
conversation = [
{
"role": "user",
"content": [
{
"type": "image",
},
{"type": "text", "text": data["input"]},
],
}
]
text_prompt = processor.apply_chat_template(conversation, add_generation_prompt=True)
inputs = processor(
text=[text_prompt], images=[image], padding=True, return_tensors="pt"
)
inputs = inputs.to(model.device)
inputs_embeds = model.get_input_embeddings()(inputs["input_ids"])
pixel_values = inputs["pixel_values"].type(model.dtype)
image_mask = inputs["input_ids"] == model.config.image_token_id
image_embeds = model.visual(pixel_values, grid_thw=inputs["image_grid_thw"])
if isinstance(image_embeds, tuple):
image_embeds = image_embeds[0]
image_embeds = image_embeds.to(inputs_embeds.device)
inputs_embeds[image_mask] = image_embeds
print("inputs_embeds", inputs_embeds.shape)
os.makedirs("data/inputs_embeds/", exist_ok=True)
np.save("data/inputs_embeds/{}".format(image_name), inputs_embeds.to(dtype=torch.float16).cpu().detach().numpy())
with open('data/inputs.json', 'w') as json_file:
json_file.write('[\n')
first = True
for data in tqdm(datasets):
input_embed = np.load(os.path.join("data/inputs_embeds", data["image"].split(".")[0]+'.npy'))
target = data["target"]
input_dict = {
"input_embed": input_embed.tolist(),
"target": target
}
if not first:
json_file.write(',\n')
else:
first = False
json.dump(input_dict, json_file)
json_file.write('\n]')
print("Done")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
差异如下:
diff --git a/examples/multimodal_model_demo/data/make_input_embeds_for_quantize.py b/examples/multimodal_model_demo/data/make_input_embeds_for_quantize.py
index 2229b9a..3ef824e 100644
--- a/examples/multimodal_model_demo/data/make_input_embeds_for_quantize.py
+++ b/examples/multimodal_model_demo/data/make_input_embeds_for_quantize.py
@@ -6,7 +6,7 @@ from PIL import Image
import json
import numpy as np
from tqdm import tqdm
-from transformers import AutoModel, AutoTokenizer, AutoProcessor, Qwen2VLForConditionalGeneration
+from transformers import AutoModel, AutoTokenizer, AutoProcessor
import argparse
argparse = argparse.ArgumentParser()
@@ -14,7 +14,13 @@ argparse.add_argument('--path', type=str, default='Qwen/Qwen2-VL-2B-Instruct', h
args = argparse.parse_args()
path = args.path
-model = Qwen2VLForConditionalGeneration.from_pretrained(
+
+if "Qwen3" in path:
+ from transformers import Qwen3VLForConditionalGeneration as ModelClass
+else:
+ from transformers import Qwen2VLForConditionalGeneration as ModelClass
+
+model = ModelClass.from_pretrained(
path, torch_dtype="auto", device_map="cpu",
low_cpu_mem_usage=True,
trust_remote_code=True).eval()
@@ -43,10 +49,13 @@ for data in datasets:
text=[text_prompt], images=[image], padding=True, return_tensors="pt"
)
inputs = inputs.to(model.device)
- inputs_embeds = model.model.embed_tokens(inputs["input_ids"])
- pixel_values = inputs["pixel_values"].type(model.visual.get_dtype())
+ inputs_embeds = model.get_input_embeddings()(inputs["input_ids"])
+ pixel_values = inputs["pixel_values"].type(model.dtype)
image_mask = inputs["input_ids"] == model.config.image_token_id
- image_embeds = model.visual(pixel_values, grid_thw=inputs["image_grid_thw"]).to(inputs_embeds.device)
+ image_embeds = model.visual(pixel_values, grid_thw=inputs["image_grid_thw"])
+ if isinstance(image_embeds, tuple):
+ image_embeds = image_embeds[0]
+ image_embeds = image_embeds.to(inputs_embeds.device)
inputs_embeds[image_mask] = image_embeds
print("inputs_embeds", inputs_embeds.shape)
os.makedirs("data/inputs_embeds/", exist_ok=True)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
运行以下命令生成量化标准数据集文件,这个py脚本会读取 data/datasets.json 中的信息,结合拉取的模型文件,生成 data/inputs.json:
python data/make_input_embeds_for_quantize.py \
--path /home/lipeng/workspace/Qwen3-VL-4B-Instruct2
--path:使用绝对路径,此目录指向的是我们拉下来的模型项目目录。
导出LLM模型
python ./export/export_rkllm.py \
--path /home/lipeng/workspace/Qwen3-VL-4B-Instruct \
--target-platform rk3576 \
--num_npu_core 2 \
--quantized_dtype w8a8 \
--device cpu2
3
4
5
6
--path:使用绝对路径,此目录指向的是我们拉下来的模型项目目录,里面有包含config.json,model.safetensors,tokenizer.json等文件。--target-platform:用于填写目标板子的CPU型号。--num_npu_core:推理时使用的 NPU 核心数量。--quantized_dtype:量化精度类型- W8 (Weights 8-bit) :把模型的权重参数从 FP16 (16位浮点) 压缩成 8位整数。体积直接减半。
- A8 (Activations 8-bit) :在计算过程中,中间产生的激活值也用 8位整数表示。
--device:目前这台PC主机转换模型过程中使用的硬件,一般情况下CPU速度会慢,但是最安全兼容性最强。
导出之后,会在当前目录生成一个 rkllm/ 文件夹,存放我们导出的模型文件。
导出ONNX
退出 TaishanPi3-RKLLM-Toolkit 环境:
conda deactivate进入TaishanPi3-RKNN-Toolkit2 环境
conda activate TaishanPi3-RKNN-Toolkit2安装依赖包
具体的说明在 README 中查看
# 安装transformers4.57.0包
pip install transformers==4.57.0
# 安装onnx1.18.0包
pip install onnx==1.18.0
# 安装依赖
sudo apt-get update && sudo apt-get install -y libgl1 libglib2.0-0 libsm6 libxext62
3
4
5
6
7
8
导出 Vision 部分为 ONNX ( .onnx )
python export/export_vision.py \
--path=/home/lipeng/workspace/Qwen3-VL-4B-Instruct \
--model_name=qwen3-vl \
--height=448 \
--width=4482
3
4
5
会在当前目录下的
onnx文件夹里(rknn-llm/examples/multimodal_model_demo/onnx/)生成一个qwen3-vl_vision.onnx文件。
转化RKNN模型
我们将继续在 TaishanPi3-RKNN-Toolkit2 环境中进行操作,将刚刚导出的 .onnx 模型转化为 .rknn 格式的视觉模型。
python export/export_vision_rknn.py \
--path=./onnx/qwen3-vl_vision.onnx \
--model_name=qwen3-vl \
--target-platform=rk3576 \
--height=448 \
--width=4482
3
4
5
6
注意:
model_name的名字如果是qwen3-vl*****,推荐直接使用qwen3-vl
五、Demo编译(C++)
说明
在rockchip官方的开源项目中使用的是C++编写的Demo,可以通过运行
rknn-llm/examples/multimodal_model_demo/deploy/build-linux.shrknn-llm/examples/multimodal_model_demo/deploy/build-android.sh
这两个脚本(将交叉编译路径替换为实际路径)直接编译示例代码。
在部署目录中生成一个install/demo_Linux_aarch64 或 install/demo_Android_aarch64 文件夹,包含 imgenc、llm、demo 和 lib 文件夹。
退出环境
conda deactivate看到命令行前面出现 (base) 字样就可以了。
安装交叉编译器
我们需要在PC主机上面编译Demo生成文件,在泰山派3M-RK3576的板子上面运行,所以我们直接使用 apt 安装 aarch64-linux-gnu:
sudo apt update && \
sudo apt install -y cmake make gcc-aarch64-linux-gnu g++-aarch64-linux-gnu2
修改编译脚本
接下来我们需要更改交叉编译脚本,让此脚本使用我们安装之后的交叉编译器进行编译。
修改 rknn-llm/examples/multimodal_model_demo/deploy/build-linux.sh 脚本为:
set -e
rm -rf build
mkdir build && cd build
cmake .. -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
make -j8
make install2
3
4
5
6
7
8
9
10
11
12
具体的差异如下:
diff --git a/examples/multimodal_model_demo/deploy/build-linux.sh b/examples/multimodal_model_demo/deploy/build-linux.sh
index c75d9c5..1c9b6b0 100755
--- a/examples/multimodal_model_demo/deploy/build-linux.sh
+++ b/examples/multimodal_model_demo/deploy/build-linux.sh
@@ -2,9 +2,8 @@ set -e
rm -rf build
mkdir build && cd build
-GCC_COMPILER=~/opts/gcc-arm-10.2-2020.11-x86_64-aarch64-none-linux-gnu
-cmake .. -DCMAKE_CXX_COMPILER=${GCC_COMPILER}/bin/aarch64-none-linux-gnu-g++ \
- -DCMAKE_C_COMPILER=${GCC_COMPILER}/bin/aarch64-none-linux-gnu-gcc \
+cmake .. -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
+ -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
编译
进入指定目录:
cd rknn-llm/examples/multimodal_model_demo/deploy运行编译脚本:
./build-linux.sh最终生成 install/ 文件目录如下:
`-- demo_Linux_aarch64
|-- demo # 最终可执行文件
|-- demo.jpg # 多模态测试的图片
|-- imgenc
`-- lib # 所需要的一些依赖文件
|-- librkllmrt.so
`-- librknnrt.so
2 directories, 5 files2
3
4
5
6
7
8
9
六、板端Demo演示
接下来我们需要转移一些文件到我们的板子上面:
rknn-llm/examples/multimodal_model_demo/deploy/install/demo_Linux_aarch64rknn-llm/examples/multimodal_model_demo/rkllm/qwen3-vl-4b-instruct_w8a8_rk3576.rkllmrknn-llm/examples/multimodal_model_demo/rknn/qwen3-vl_vision_rk3576.rknn
在板子上面创建一个 qwen3-vl-4b-instruct 目录用来存放我们将要转移的文件:
mkdir ~/qwen3-vl-4b-instruct复制install文件夹
推荐使用 adb 工具,进行转移,泰山派3m默认开启ADB,或者使用TF卡,ssh或者U盘都可以。
参考:https://wiki.lckfb.com/zh-hans/tspi-3-rk3576/system-usage/debian12-usage/adb-usage.html
推送 install/demo_Linux_aarch64 整个目录到板子的 /home/lckfb/qwen3-vl-4b-instruct/ 下:
adb push rknn-llm/examples/multimodal_model_demo/deploy/install/demo_Linux_aarch64 /home/lckfb/qwen3-vl-4b-instruct/转移模型到板端
推送 qwen3-vl-4b-instruct_w8a8_rk3576.rkllm 模型到板子的 /home/lckfb/qwen3-vl-4b-instruct/ 下:
adb push rknn-llm/examples/multimodal_model_demo/rkllm/qwen3-vl-4b-instruct_w8a8_rk3576.rkllm /home/lckfb/qwen3-vl-4b-instruct/推送 qwen3-vl_vision_rk3576.rknn 模型到板子的 /home/lckfb/qwen3-vl-4b-instruct/ 下:
adb push rknn-llm/examples/multimodal_model_demo/rknn/qwen3-vl_vision_rk3576.rknn /home/lckfb/qwen3-vl-4b-instruct/板端运行
我们进入泰山派开发板的终端,然后进入 /home/lckfb/qwen3-vl-4b-instruct/demo_Linux_aarch64/ 目录:
# 进入目录
cd /home/lckfb/qwen3-vl-4b-instruct/demo_Linux_aarch64/2
设置动态库路径:(为当前目录下的 ./lib 目录下 )
# 设置动态库路径 (非常重要,否则会报错误)
export LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH2
如果你想要查看性能增加一个变量
export RKLLM_LOG_LEVEL=1
赋予demo可执行权限
sudo chmod +x demo运行Demo:
用法:
./demo [图片] [视觉模型] [语言模型] [生成长度] [上下文长度] [NPU核心数] [特殊Prompt标记...]特别注意:因为模型的路径是上一层级的目录所以用
../
./demo demo.jpg \
../qwen3-vl_vision_rk3576.rknn \
../qwen3-vl-4b-instruct_w8a8_rk3576.rkllm \
2048 4096 2 "<|vision_start|>" "<|vision_end|>" "<|image_pad|>"2
3
4
这里的"<|vision_start|>", "<|vision_end|>", "<|image_pad|>" 这三个实际上是视觉/多模态大模型输入中的特殊占位符(token) :
"<|vision_start|>":视觉片段开始标记(Visual Start Token) 代表图片信息在大模型输入序列中的起始位置,告诉模型“接下来要插入一张图片的内容了”。"<|vision_end|>":视觉片段结束标记(Visual End Token) 代表图片信息结束的地方,告诉模型“图片信息输入到这里为止了”。"<|image_pad|>":视觉内容补齐占位符(Image Padding Token) 在处理多张图片批量推理时,图片的 patch/token 长度不一致,为了对齐输入,往往会使用 Pad Token 补齐到一致的长度。这个 token 就是做补齐填充用的。
其实就是 告诉大模型“图片内容从哪开始、到哪结束、没填满时用什么补齐” 的专用字符串标记,用于多模态推理输入。
运行成功后,可以进行问答。
终端将输出模型对
demo.jpg图片的描述或回答。