Qwen3-VL是阿里巴巴Qwen团队于2025年9月24日发布的多模态视觉语言模型,2B 是该系列中经过蒸馏或轻量化设计的小参数版本,我们在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 :

# Qwen3-VL-2B全流程部署
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?输入yes

可以去 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即可

激活 Conda 环境:

conda activate TaishanPi3-RKLLM-Toolkit

安装RKLLM-Toolkit

rknn-llm/rkllm-toolkit/packages/目录下有好几种 whl 文件可以选择:

  1. rkllm_toolkit-1.2.3-cp39-cp39-linux_x86_64.whl
  2. rkllm_toolkit-1.2.3-cp310-cp310-linux_x86_64.whl
  3. rkllm_toolkit-1.2.3-cp311-cp311-linux_x86_64.whl
  4. 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/simple

安装完成之后,退出 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即可

激活 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/simple

安装完成之后,退出 TaishanPi3-RKNN-Toolkit2 环境:

conda deactivate

三、拉取模型

我们所使用的是 Qwen3-VL-4B-Instruct模型 ,从huggingface/modelscope中拉取模型文件,以供我们后续操作:

  1. 进入TaishanPi3-RKLLM-Toolkit 环境
conda activate TaishanPi3-RKLLM-Toolkit
  1. 安装 git-lts
sudo apt update && sudo apt install git-lfs
  1. 拉取模型
git clone https://huggingface.co/Qwen/Qwen3-VL-2B-Instruct

# 或者使用国内的魔塔社区的模型

git clone https://www.modelscope.cn/Qwen/Qwen3-VL-2B-Instruct.git

四、模型转化

我们将继续在 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")

差异如下:

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)

运行以下命令生成量化标准数据集文件,这个py脚本会读取 data/datasets.json 中的信息,结合拉取的模型文件,生成 data/inputs.json

python data/make_input_embeds_for_quantize.py \
    --path /home/lipeng/workspace/Qwen3-VL-2B-Instruct
  • --path使用绝对路径,此目录指向的是我们拉下来的模型项目目录。

导出LLM模型

python ./export/export_rkllm.py \
    --path /home/lipeng/workspace/Qwen3-VL-2B-Instruct \
    --target-platform rk3576 \
    --num_npu_core 2 \
    --quantized_dtype w8a8 \
    --device cpu
  • --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位整数表示。
  • 这是目前在端侧设备上性价比最高的方案。相比 FP16,W8A8 速度快得多,内存占用小,且精度损失通常在可接受范围内
  • --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 libxext6

导出 Vision 部分为 ONNX ( .onnx )

python export/export_vision.py \
    --path=/home/lipeng/workspace/Qwen3-VL-2B-Instruct \
    --model_name=qwen3-vl \
    --height=448 \
    --width=448

会在当前目录下的 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=448

注意:model_name 的名字如果是 qwen3***** 模型,直接使用 qwen3-vl

五、Demo编译(C++)

说明

rockchip官方的开源项目中使用的是C++编写的Demo,可以通过运行

  1. rknn-llm/examples/multimodal_model_demo/deploy/build-linux.sh
  2. rknn-llm/examples/multimodal_model_demo/deploy/build-android.sh

这两个脚本(将交叉编译路径替换为实际路径)直接编译示例代码。

部署目录中生成一个install/demo_Linux_aarch64install/demo_Android_aarch64 文件夹,包含 imgencllmdemolib 文件夹。

退出环境

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-gnu

修改编译脚本

接下来我们需要更改交叉编译脚本,让此脚本使用我们安装之后的交叉编译器进行编译。

修改 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 install

具体的差异如下:

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 \

编译

进入指定目录:

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 files

六、板端Demo演示

接下来我们需要转移一些文件到我们的板子上面:

  • rknn-llm/examples/multimodal_model_demo/deploy/install/demo_Linux_aarch64
  • rknn-llm/examples/multimodal_model_demo/rkllm/qwen3-vl-2b-instruct_w8a8_rk3576.rkllm
  • rknn-llm/examples/multimodal_model_demo/rknn/qwen3-vl_vision_rk3576.rknn

在板子上面创建一个 qwen3-vl-2b-instruct 目录用来存放我们将要转移的文件:

mkdir ~/qwen3-vl-2b-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-2b-instruct/

转移模型到板端

推送 qwen3-vl-2b-instruct_w8a8_rk3576.rkllm 模型到板子的 /home/lckfb/qwen3-vl-2b-instruct/ 下:

adb push rknn-llm/examples/multimodal_model_demo/rkllm/qwen3-vl-2b-instruct_w8a8_rk3576.rkllm /home/lckfb/qwen3-vl-2b-instruct/

推送 qwen3-vl_vision_rk3576.rknn 模型到板子的 /home/lckfb/qwen3-vl-2b-instruct/ 下:

adb push rknn-llm/examples/multimodal_model_demo/rknn/qwen3-vl_vision_rk3576.rknn /home/lckfb/qwen3-vl-2b-instruct/

板端运行

我们进入泰山派开发板的终端,然后进入 /home/lckfb/qwen3-vl-4b-instruct/demo_Linux_aarch64/ 目录:

# 进入目录
cd /home/lckfb/qwen3-vl-2b-instruct/demo_Linux_aarch64/

设置动态库路径:(为当前目录下的 ./lib 目录下 )

# 设置动态库路径 (非常重要,否则会报错误)
export LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH

如果你想要查看性能增加一个变量 export RKLLM_LOG_LEVEL=1

赋予demo可执行权限

sudo chmod +x demo

运行Demo:

用法: ./demo [图片] [视觉模型] [语言模型] [生成长度] [上下文长度] [NPU核心数] [特殊Prompt标记...]

特别注意:因为模型的路径是上一层级的目录所以用 ../

./demo demo.jpg \
  ../qwen3-vl_vision_rk3576.rknn \
  ../qwen3-vl-2b-instruct_w8a8_rk3576.rkllm \
  256 2048 2 "<|vision_start|>" "<|vision_end|>" "<|image_pad|>"

这里的"<|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 图片的描述或回答。

问题1

将之前传给他的 demo.jpg 分析一下:

问题2

问题3

更多推荐