在 QNAP TS416(RK3568)上复用 QuMagie 容器的 NPU 运行自定义 RKNN 模型

声明:本文由 AI 辅助整理,基于一次真实的设备调试过程。所有命令、报错和输出均来自实际终端记录,目标是提供一份足够详细的、可完整复现的参考文档。


目录

  1. 背景与目标
  2. 环境信息
  3. 第一阶段:认识 QuMagie 容器的启动架构
  4. 第二阶段:进入容器,发现 NPU 相关进程
  5. 第三阶段:解析 QuMagieCore.sh 启动脚本
  6. 第四阶段:解析 common.include
  7. 第五阶段:解析容器内 entrypoint 脚本
  8. 第六阶段:Python 侧调试——从 ModuleNotFoundError 到成功推理
  9. 第七阶段:独立容器复用宿主机 NPU
  10. 第八阶段:性能测试与遗留问题
  11. 关键结论与速查表
  12. 遗留问题与后续工作

1. 背景与目标

1.1 设备

  • 设备型号:QNAP TS416
  • SoC:Rockchip RK3568(NPU 算力约 1 TOPS)
  • 系统:QTS(QNAP 的 NAS 操作系统)
  • 容器运行时:Container Station,使用 system-docker 作为命令行接口
  • 已安装的 QPKG:QuMagieCore(QNAP 的 AI 相册核心组件,带有 AI 图像识别能力)

1.2 目标

在不影响 QuMagie 原有功能的前提下,复用设备的 NPU 资源,运行我们自己的 RKNN 模型(Python 接口),用于开发或测试目的。

1.3 挑战

  • QNAP 的 NPU 使用方式未公开文档化。
  • QuMagie 容器的启动脚本、模型路径、Python 环境都是未知的。
  • 需要弄清楚 NPU 是如何被容器化访问的,才能在新的独立容器中复用。

2. 环境信息

项目 值
SoC Rockchip RK3568
NPU 驱动版本 0.8.2
librknnrt.so 版本 2.3.0 (c949ad889d@2024-11-07T11:35:33)
rknn-toolkit-lite2 版本 2.0.0b0
Python 版本 3.8.10
镜像 qumagie-core-arm64:latest
容器名 aicore.container
容器 ID cef730460997
QPKG 根目录 /share/CACHEDEV1_DATA/.qpkg/QuMagieCore

3. 第一阶段:认识 QuMagie 容器的启动架构

3.1 查看运行中的容器

1
2
3
4
[admin@TS416 ~]# system-docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b8277b6cc823 qumagie-core-arm64:latest "/bin/bash" 3 minutes ago Up 3 minutes rknn-test
cef730460997 qumagie-core-arm64:latest "/root/daemon/entryp…" 3 weeks ago Up 3 weeks aicore.container

可见有两个容器:

  • rknn-test:之前实验时启动的临时容器。
  • aicore.container:QuMagie 的 AI 核心容器,已经运行了 3 周。

3.2 检查容器详情

1
system-docker inspect cef730460997

关键输出(节选):

1
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
{
"Id": "cef7304609970c521c499478ae006a51cec8f8b5983e273ff435be9a5fa2ccce",
"Created": "2026-08-31T15:29:14.96845285Z",
"Path": "/root/daemon/entrypoint.sh",
"State": {
"Status": "running",
"Pid": 2055
},
"Image": "sha256:ddf19ca7c584030390562464269a40b77bc922f90f14406a94a1eee02c7dc3bf",
"Name": "/aicore.container",
"HostConfig": {
"Binds": [
"/etc/localtime:/etc/localtime",
"/share/CACHEDEV1_DATA/.qpkg/QuMagieCore/daemon:/root/daemon",
"/share/CACHEDEV1_DATA/.qpkg/QuMagieCore/QuMagieCore/object:/tmp/iva",
"/share/CACHEDEV1_DATA/.qpkg/QuMagieCore/QuMagieCore/face:/tmp/face",
"/dev/bus/usb:/dev/bus/usb:ro"
],
"NetworkMode": "host",
"RestartPolicy": {
"Name": "always"
},
"Privileged": false,
"Devices": [
{
"PathOnHost": "/dev/dri/card0",
"PathInContainer": "/dev/dri/card0",
"CgroupPermissions": "rwm"
},
{
"PathOnHost": "/dev/dri/renderD128",
"PathInContainer": "/dev/dri/renderD128",
"CgroupPermissions": "rwm"
},
{
"PathOnHost": "/dev/dri/renderD128",
"PathInContainer": "/dev/dri/renderD129",
"CgroupPermissions": "rwm"
},
{
"PathOnHost": "/dev/rga",
"PathInContainer": "/dev/rga",
"CgroupPermissions": "rwm"
}
],
"DeviceCgroupRules": [
"c *:* rw"
],
"CpuPeriod": 100000,
"CpuQuota": 200000,
"Ulimits": [
{
"Name": "nofile",
"Hard": 65535,
"Soft": 65535
}
]
},
"Config": {
"Hostname": "TS416",
"Env": [
"QNAP_QPKG=QuMagieCore",
"NPU_DEVICE=aarch64",
"ENABLE_SEMANTIC_SEARCH=1",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/root/daemon/entrypoint.sh"
],
"Image": "qumagie-core-arm64:latest",
"Labels": {
"org.opencontainers.image.ref.name": "ubuntu",
"org.opencontainers.image.version": "20.04"
}
}
}

从 inspect 里能提取出的关键信息:

配置项 值 含义
网络模式 host 容器共享宿主机网络
启动命令 /root/daemon/entrypoint.sh 容器内 PID 1
环境变量 NPU_DEVICE=aarch64 非 RK3588 平台
环境变量 ENABLE_SEMANTIC_SEARCH=1 启用语义搜索
设备 /dev/dri/card0 GPU / DRM
设备 /dev/dri/renderD128 NPU / DRM render 节点
设备 /dev/dri/renderD128 → renderD129 RK3568 workaround(关键)
设备 /dev/rga 2D 图形加速
挂载 ${QPKG_ROOT}/daemon → /root/daemon QuMagie 应用文件
挂载 .../QuMagieCore/object → /tmp/iva 物体识别数据
挂载 .../QuMagieCore/face → /tmp/face 人脸识别数据
挂载 /dev/bus/usb → /dev/bus/usb:ro USB 设备(只读)
CPU period=100000, quota=200000 限 2 核
重启策略 always 挂了自动重启

3.3 需要注意的点

  • renderD128 被映射成 renderD129:这是 RK3568 的典型 workaround。因为 RK3568 只有 renderD128,但 Rockchip 的某些用户态库会硬编码访问 renderD129,所以需要”伪装”一个出来。
  • 网络是 host 模式:这意味着容器和宿主机共享 localhost,npu_transfer_proxy 可以被跨容器访问。
  • --device-cgroup-rule="c *:* rw":允许容器访问所有字符设备。

4. 第二阶段:进入容器,发现 NPU 相关进程

4.1 进入容器

1
2
[admin@TS416 ~]# system-docker exec -it cef730460997 bash
root@TS416:/#

4.2 查看容器内进程

1
2
3
4
5
6
7
8
9
10
11
root@TS416:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Aug31 ? 00:00:00 /bin/bash /root/daemon/entrypoint.sh
root 8 1 0 Aug31 ? 00:12:47 ./npu_transfer_proxy
root 16 1 0 Aug31 ? 00:00:02 ./inference
root 18 1 0 Aug31 ? 00:01:29 ./aic_manager inference FcServer_mt
root 19 1 0 Aug31 ? 00:01:30 ./FcServer_mt 3
root 28 19 0 Aug31 ? 00:00:00 [FcServer_mt] <defunct>
root 32 1 0 Aug31 ? 00:00:01 ./FcServer_mt 3
root 232 0 1 17:35 pts/0 00:00:00 bash
root 241 232 0 17:35 pts/0 00:00:00 ps -ef

4.3 进程解读

PID 进程 作用
1 /bin/bash /root/daemon/entrypoint.sh 容器主进程
8 ./npu_transfer_proxy NPU 用户态通信代理
16 ./inference AI 推理主程序(二进制)
18 ./aic_manager inference FcServer_mt AI 核心管理器
19 ./FcServer_mt 3 特征计算服务(主进程)
28 [FcServer_mt] <defunct> 已退出但未回收的子进程
32 ./FcServer_mt 3 aic_manager 拉起的第二个实例

关键发现:npu_transfer_proxy 是 NPU 访问的核心,且是单实例的。

4.4 查看容器内文件结构

1
2
3
4
5
root@TS416:/# ls
bin boot dev etc home lib libjpeg.so.62 media mnt opt proc root run sbin srv sys tmp usr var

root@TS416:/# ls /root/daemon/
entrypoint.sh faceLib quai quai_launcher.sh

4.5 检查 NPU 设备节点

1
2
3
4
5
6
root@TS416:/# ls -l /dev/dri/ /dev/rga
crw------- 1 root root 10, 60 Sep 24 18:34 /dev/rga

/dev/dri/:
total 0
crw------- 1 root root 226, 128 Sep 24 18:34 renderD128

注意:容器里只看到 renderD128,没有 renderD129(因为 inspect 显示 /dev/dri/renderD128 → /dev/dri/renderD129 是一个 workaround 映射,在本容器里的实际表现需要进一步确认)。


5. 第三阶段:解析 QuMagieCore.sh 启动脚本

5.1 脚本位置

1
2
3
DIR=$(/sbin/getcfg -f /etc/config/qpkg.conf QuMagieCore Install_Path)
echo $DIR
# 输出: /share/CACHEDEV1_DATA/.qpkg/QuMagieCore

5.2 关键函数:run_quai_container

1
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
run_quai_container()
{
stop_quai_container
prepare_quai_container

cp "${QPKG_ROOT}/docker_scripts/entrypoint_arm_64.sh" "${QPKG_ROOT}/daemon/entrypoint.sh"
chmod +x "${QPKG_ROOT}/daemon/entrypoint.sh"

CPU_QUOTA=$(get_cpu_quota)
log_usage "run aicore container with ${CPU_QUOTA}"

local npu_device=$(get_npu_device)
local enable_semantic_search=1
if [[ "${npu_device}" == "rk3588" ]]; then
enable_semantic_search=0
fi

${DOCKER} run -e "QNAP_QPKG=${QPKG_NAME}" \
--name="${AICORE_CONTAINER_NAME}" -d --restart=always \
$(mount_hailo_devices) \
$(mount_rknn_devices) \
-v /dev/bus/usb:/dev/bus/usb:ro \
--device-cgroup-rule="c *:* rw" \
-v /etc/localtime:/etc/localtime \
-v "${QPKG_ROOT}/daemon:/root/daemon" \
-v "${OBJ_DATA_ROOT}:/tmp/iva" \
-v "${FACE_DATA_ROOT}:/tmp/face" \
--security-opt="systempaths=unconfined" \
-e NPU_DEVICE=${npu_device} \
-e ENABLE_SEMANTIC_SEARCH=${enable_semantic_search} \
--net=host \
--cpu-period=100000 --cpu-quota="${CPU_QUOTA}" \
${IMAGE_REPOSITORY}:latest /root/daemon/entrypoint.sh

return $?
}

5.3 get_npu_device():判断 SoC 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
get_npu_device()
{
local device_compatible_node="/proc/device-tree/compatible"
if [[ ! -f "${device_compatible_node}" ]]; then
echo "aarch64"
return
fi

if [[ $(grep -ic rk3588 "${device_compatible_node}") -ge 1 ]]; then
echo "rk3588"
else
echo "aarch64"
fi
}

逻辑:

  • 有 rk3588 → 返回 rk3588(禁用语义搜索)
  • 其他 → 返回 aarch64(启用语义搜索)

我们的 TS416 返回 aarch64,所以 ENABLE_SEMANTIC_SEARCH=1,与 inspect 一致。


6. 第四阶段:解析 common.include

common.include 是 QuMagieCore.sh 引入的公共库,位于同一目录下。

6.1 关键变量定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Docker 命令
DOCKER="$(${GETCFG} -f ${CONF} container-station INSTALL_PATH)/bin/system-docker"

# QPKG 信息
QPKG_NAME="QuMagieCore"
QPKG_ROOT="$(${GETCFG} ${QPKG_NAME} Install_Path -f ${CONF})"

# 数据目录
DATA_ROOT="${QPKG_ROOT}/QuMagieCore"
FACE_DATA_ROOT="${DATA_ROOT}/face"
OBJ_DATA_ROOT="${DATA_ROOT}/object"

# 容器名
AICORE_CONTAINER_NAME="aicore.container"

6.2 mount_rknn_devices():RKNN 设备映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mount_rknn_devices()
{
local ret_devices=""

for device in /dev/dri/*
do
if [[ -c "${device}" ]]; then
ret_devices="${ret_devices} --device $device:$device"
fi
done

# Workaround for NPU models other than AI-642
if [[ -c "/dev/dri/renderD128" && ! -c "/dev/dri/renderD129" ]]; then
ret_devices="${ret_devices} --device /dev/dri/renderD128:/dev/dri/renderD129"
fi

if [[ -c "/dev/rga" ]]; then
ret_devices="${ret_devices} --device /dev/rga:/dev/rga"
fi

echo "${ret_devices}"
}

关键点:

  1. 遍历 /dev/dri/*,所有字符设备都挂进容器。
  2. 如果只有 renderD128,则额外把 renderD128 映射成 renderD129。这是 RK3568 独有的 workaround。
  3. /dev/rga 也一起挂进去。

6.3 get_cpu_quota():CPU 配额

1
2
3
4
5
6
7
8
9
10
11
12
13
get_cpu_quota()
{
local cpu_cores=$(grep -c processor /proc/cpuinfo)
local cpu_quota=$(($cpu_cores*100000))

if [ $cpu_cores -gt 1 ]; then
cpu_quota=$(($cpu_quota/2))
else
cpu_quota=50000
fi

echo "${cpu_quota}"
}

TS416 是 4 核,所以 cpu_quota = 4*100000/2 = 200000,与 inspect 一致。


7. 第五阶段:解析容器内 entrypoint 脚本

7.1 从宿主机获取 entrypoint_arm_64.sh

1
[admin@TS416 ~]# cat /share/CACHEDEV1_DATA/.qpkg/QuMagieCore/docker_scripts/entrypoint_arm_64.sh

7.2 脚本内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

ldconfig
cd /usr/local/lib/rknn_env/rknnlite/3rdparty/platform-tools/ntp/linux-aarch64
./npu_transfer_proxy &

cd /root/daemon/quai
python3 -m build_link
./inference &

cd /root/daemon/faceLib/bin/
chmod +x *

./aic_manager inference FcServer_mt &
./FcServer_mt 3

7.3 逐行解读

行 命令 作用
3 ldconfig 刷新动态库缓存,确保 librknnrt.so 可被找到
4-5 cd .../ntp/linux-aarch64 + ./npu_transfer_proxy & 启动 NPU 通信代理(后台)
7-8 cd /root/daemon/quai + python3 -m build_link 运行模型软链脚本
9 ./inference & 启动 AI 推理主程序(后台)
11-12 cd /root/daemon/faceLib/bin/ + chmod +x * 准备人脸特征计算目录
14 ./aic_manager inference FcServer_mt & 启动 AI 核心管理器(后台)
15 ./FcServer_mt 3 前台运行特征计算服务(容器主进程)

注意最后一行没有 &,所以 FcServer_mt 是前台进程,容器的主进程实际上是它。

原始的 build_link.py 被编译成了 build_link.pyc,用 uncompyle6 反编译:

1
2
pip3 install uncompyle6
uncompyle6 /path/to/build_link.pyc

反编译结果:

1
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
import platform, subprocess, os
DEVICE_COMPATIBLE_NODE = "/proc/device-tree/compatible"

def get_host():
system = platform.system()
machine = platform.machine()
os_machine = system + "-" + machine
if os_machine == "Linux-aarch64":
try:
with open(DEVICE_COMPATIBLE_NODE) as f:
device_compatible_str = f.read()
if "rk3588" in device_compatible_str:
host = "RK3588"
else:
if "rk3568" in device_compatible_str:
host = "RK356x"
else:
if "rk3566" in device_compatible_str:
host = "RK356x"
else:
host = "other_arm64"
except:
print("Read device node {} failed.".format(DEVICE_COMPATIBLE_NODE))
host = os_machine

else:
host = os_machine
return host


host = get_host()
if os.path.exists("/root/daemon/quai/model/qumagiecore.rknn"):
subprocess.run(["rm", "-f", "/root/daemon/quai/model/qumagiecore.rknn"])
elif host == "RK3588":
subprocess.run(["ln", "-s", "/root/daemon/quai/model/qumagiecore_rk3588.rknn",
"/root/daemon/quai/model/qumagiecore.rknn"])
else:
if host == "RK356x":
subprocess.run(["ln", "-s", "/root/daemon/quai/model/qumagiecore_rk356x.rknn",
"/root/daemon/quai/model/qumagiecore.rknn"])

结论:build_link.pyc 只是一个模型选择器:

  • 检测 SoC 类型
  • 删除旧软链
  • 根据 SoC 创建对应的软链(qumagiecore.rknn → qumagiecore_rk3588.rknn 或 qumagiecore_rk356x.rknn)

它跟 Python rknn 包完全无关,之前的猜测需要修正。


8. 第六阶段:Python 侧调试——从 ModuleNotFoundError 到成功推理

8.1 最初的尝试(错误)

1
from rknn.api import RKNN

报错:

1
ModuleNotFoundError: No module named 'rknn'

原因:

  • rknn 是 PC 端模型转换工具 rknn-toolkit2 的包名。
  • 板端推理用的是 rknn-toolkit-lite2,包名是 rknnlite。

8.2 发现两个 Python 环境

1
2
3
4
5
6
7
8
root@TS416:/usr/local/lib# ls
rknn_env rknn2_env

root@TS416:/usr/local/lib/rknn_env/rknnlite# cat VERSION
1.7.7b2

root@TS416:/usr/local/lib/rknn2_env/rknnlite# cat VERSION
2.0.0b0

系统里存在两套 RKNN Python 环境。

8.3 使用正确类名和导入

1
2
3
4
5
import sys
sys.path.insert(0, '/usr/local/lib/rknn2_env')
from rknnlite.api import RKNNLite

rknn = RKNNLite(verbose=True)
项 正确值 错误值
包名 rknnlite rknn
类名 RKNNLite RKNN
用途 板端推理 PC 端模型转换

8.4 undefined symbol 错误

用 rknn_env(1.7.7b2)时报错:

1
AttributeError: /usr/lib/librknnrt.so: undefined symbol: rknn_server_status

原因:/usr/lib/librknnrt.so 是 2.3.0 版本,与 1.7.7b2 的 Python 包不匹配。

改用 rknn2_env(2.0.0b0)后,版本匹配(虽然 Python 包是 2.0.0b0,运行时库是 2.3.0,但接口兼容),错误消失。

8.5 第一次尝试独立容器(失败)

1
2
3
4
5
6
7
8
9
10
11
system-docker run -it --rm \
--name my-npu-app \
--net=host \
--device /dev/dri/renderD128:/dev/dri/renderD128 \
--device /dev/rga:/dev/rga \
--device-cgroup-rule="c *:* rw" \
--security-opt="systempaths=unconfined" \
-v /share/MyNPUProject:/app \
-v /proc/device-tree/compatible:/proc/device-tree/compatible:ro \
qumagie-core-arm64:latest \
bash

进入后尝试 init_runtime():

1
2
3
4
5
!!! It is detected that some necessary files are missing in the container.
!!! When starting the container, please use the -v parameter to map the corresponding files in the host to the container.
The reference parameters of run container are as follows:
-v /dev/dri/renderD129:/dev/dri/renderD129
-v /proc/device-tree/compatible:/proc/device-tree/compatible

缺少两个东西:

  1. renderD129 的映射
  2. /proc/device-tree/compatible 的挂载

8.6 修正后的独立容器(成功)

完整命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
system-docker run -it --rm \
--name my-npu-test \
--net=host \
--device /dev/dri/renderD128:/dev/dri/renderD128 \
--device /dev/dri/renderD128:/dev/dri/renderD129 \
--device /dev/rga:/dev/rga \
--device-cgroup-rule="c *:* rw" \
--security-opt="systempaths=unconfined" \
-v /share/MyNPUProject:/app \
-v /share/CACHEDEV1_DATA/.qpkg/QuMagieCore/daemon/quai/model:/mnt/model:ro \
-v /proc/device-tree/compatible:/proc/device-tree/compatible:ro \
qumagie-core-arm64:latest \
bash

关键新增:

  • --device /dev/dri/renderD128:/dev/dri/renderD129
  • -v /proc/device-tree/compatible:/proc/device-tree/compatible:ro

8.7 完整推理脚本

1
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
import sys
sys.path.insert(0, '/usr/local/lib/rknn2_env')
from rknnlite.api import RKNNLite
import numpy as np
import time

MODEL = '/mnt/model/qumagiecore_rk356x.rknn'

rknn = RKNNLite(verbose=False)

ret = rknn.load_rknn(MODEL)
print('load_rknn:', ret)

ret = rknn.init_runtime()
print('init_runtime:', ret)

input_data = np.zeros((1, 3, 224, 224), dtype=np.uint8)
outputs = rknn.inference(inputs=[input_data])
print('inference ok')
for i, o in enumerate(outputs):
print(f' output[{i}]: shape={o.shape}, dtype={o.dtype}')

rknn.inference(inputs=[input_data])
t0 = time.time()
for _ in range(100):
rknn.inference(inputs=[input_data])
t1 = time.time()
print(f'avg: {(t1-t0)/100*1000:.2f} ms, FPS: {100/(t1-t0):.1f}')

rknn.release()
print('done')

8.8 成功输出

1
2
3
4
5
6
7
8
9
load_rknn: 0
I RKNN: [18:53:42.972] RKNN Runtime Information, librknnrt version: 2.3.0 (c949ad889d@2024-11-07T11:35:33)
I RKNN: [18:53:42.972] RKNN Driver Information, version: 0.8.2
I RKNN: [18:53:42.972] RKNN Model Information, version: 6, toolkit version: 2.0.0b0+9bab5682(compiler version: 2.0.0b0 (35a6907d79@2024-03-24T02:34:11)), target: RKNPU lite, target platform: rk3568, framework name: ONNX, framework layout: NCHW, model inference type: static_shape
init_runtime: 0
inference ok
output[0]: shape=(1, 420), dtype=float32
avg: 31.78 ms, FPS: 31.5
done

8.9 模型信息提取

从 RKNN 日志中可以读出模型的输入输出:

1
2
3
Feature Tensor Information Table
ID 1 ConvRelu input INT8 NC1HWC2 (1,3,224,224) ← 输入
ID 58 OutputOperator output INT8 UNDEFINED (1,420) ← 输出
项 值
输入 shape (1, 3, 224, 224)
输入 dtype INT8(RKNNLite 接收 uint8)
输出 shape (1, 420)
输出 dtype float32(RKNNLite 自动反量化)
模型类型 MobileNetV2 风格骨干网络
分类头 420 维(非 ImageNet 1000 类,是 QuMagie 自定义)

9. 第七阶段:独立容器复用宿主机 NPU

9.1 为什么可以复用

npu_transfer_proxy 是单实例守护进程,通过本地网络(localhost)与 AI 应用通信。容器使用 --net=host 后,容器内进程可以直接访问宿主机的 localhost,从而连接到 QuMagie 容器里的 npu_transfer_proxy。

架构图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──────────────────────────────────────┐
│ 宿主机 (TS416) │
│ │
│ ┌────────────────────────────────┐ │
│ │ aicore.container │ │
│ │ ├─ npu_transfer_proxy (:xxxx) │ │
│ │ ├─ inference │ │
│ │ └─ FcServer_mt │ │
│ └────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────┐ │
│ │ my-npu-test (--net=host) │ │
│ │ └─ python3 RKNNLite │ │
│ │ ↓ │ │
│ │ 通过 localhost 连接 │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘

9.2 验证共用关系

在宿主机上:

1
system-docker exec aicore.container ps -ef | grep npu_transfer_proxy

可以看到 npu_transfer_proxy 进程。在你的新容器里,它不会出现(因为不共享 PID namespace),但 RKNNLite 通过 localhost 能连上它。

9.3 注意事项

  • 不要在新容器里重复启动 npu_transfer_proxy,会冲突。
  • 性能是共享的:QuMagie 高负载时,你的推理会变慢。
  • 代理重启会断开连接:aicore.container 重启后,你的推理进程需要重新 init_runtime()。

10. 第八阶段:性能测试与遗留问题

10.1 速度测试结果

输入类型 平均耗时 FPS
全零 np.zeros((1,3,224,224), uint8) 31.78 ms 31.5
随机 np.random.randint(0,255,(1,3,224,224), uint8) 31.82 ms 31.4

结论:输入数据不影响速度,模型本身耗时约 31.8 ms。

10.2 与预期差距

Rockchip 官方基准测试中,MobileNetV2 在 RK3568 上可达 180 FPS(约 5.5 ms)。

当前速度慢了近 6 倍。

10.3 排查 NPU 频率(未完成)

尝试查看 NPU 频率:

1
2
3
4
5
root@TS416:/# cat /sys/class/devfreq/fdab0000.npu/cur_freq
cat: /sys/class/devfreq/fdab0000.npu/cur_freq: No such file or directory

root@TS416:/# cat /sys/kernel/debug/clk/clk_summary | grep -i npu
(空输出)

实际路径应为 /sys/class/devfreq/fde40000.npu/,这是 RK3568 的 NPU devfreq 节点,尚未验证。

10.4 可能的原因

原因 说明 状态
NPU 频率被限制 默认 governor 可能未拉到最高频 未排查
与 QuMagie 竞争 inference 进程持续占用 NPU 未排查
Python API 开销 ctypes 封装有额外开销 部分验证
模型量化质量 INT8 量化可能不够优化 无证据

10.5 可能的优化方向

  1. 调整 NPU 频率:

    1
    2
    3
    cat /sys/class/devfreq/fde40000.npu/available_frequencies
    echo userspace > /sys/class/devfreq/fde40000.npu/governor
    echo 800000 > /sys/class/devfreq/fde40000.npu/userspace/set_freq
  2. 暂停 QuMagie 容器,排除竞争:

    1
    2
    3
    system-docker stop aicore.container
    # 测试
    system-docker start aicore.container
  3. 使用 C/C++ API:如果 Python 开销是瓶颈,可以改用 C++ 接口,直接调用 librknnrt.so。


11. 关键结论与速查表

11.1 Python 环境速查

项 正确值 错误值
环境路径 /usr/local/lib/rknn2_env /usr/local/lib/rknn_env(旧版,版本不匹配)
包名 rknnlite rknn
类名 RKNNLite RKNN
VERSION 2.0.0b0 1.7.7b2
导入 from rknnlite.api import RKNNLite from rknn.api import RKNN

11.2 容器启动参数速查

参数 作用
--net=host 复用宿主机的 npu_transfer_proxy
--device /dev/dri/renderD128:/dev/dri/renderD128 NPU 设备
--device /dev/dri/renderD128:/dev/dri/renderD129 RK3568 workaround
--device /dev/rga:/dev/rga 2D 图形加速
--device-cgroup-rule="c *:* rw" 字符设备访问权限
--security-opt="systempaths=unconfined" 允许访问 /proc/device-tree
-v /proc/device-tree/compatible:/proc/device-tree/compatible:ro 让 rknnlite 识别 SoC
-v /path/to/model:/mnt/model:ro 挂载模型目录

11.3 一键推理命令(复制即用)

1
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
system-docker run --rm \
--name my-npu-test \
--net=host \
--device /dev/dri/renderD128:/dev/dri/renderD128 \
--device /dev/dri/renderD128:/dev/dri/renderD129 \
--device /dev/rga:/dev/rga \
--device-cgroup-rule="c *:* rw" \
--security-opt="systempaths=unconfined" \
-v /share/CACHEDEV1_DATA/.qpkg/QuMagieCore/daemon/quai/model:/mnt/model:ro \
-v /proc/device-tree/compatible:/proc/device-tree/compatible:ro \
qumagie-core-arm64:latest \
python3 -c "
import sys, time
sys.path.insert(0, '/usr/local/lib/rknn2_env')
from rknnlite.api import RKNNLite
import numpy as np

rknn = RKNNLite(verbose=False)
ret = rknn.load_rknn('/mnt/model/qumagiecore_rk356x.rknn')
print('load_rknn:', ret)

ret = rknn.init_runtime()
print('init_runtime:', ret)

input_data = np.zeros((1, 3, 224, 224), dtype=np.uint8)
outputs = rknn.inference(inputs=[input_data])
print('inference ok, output shape:', outputs[0].shape)

t0 = time.time()
for _ in range(100):
rknn.inference(inputs=[input_data])
t1 = time.time()
print(f'avg: {(t1-t0)/100*1000:.2f} ms, FPS: {100/(t1-t0):.1f}')

rknn.release()
print('done')
"

11.4 运行自定义模型

  1. 将自己的 .rknn 模型(必须 target_platform=rk3568)放到宿主机:

    1
    cp your_model.rknn /share/CACHEDEV1_DATA/.qpkg/QuMagieCore/daemon/quai/model/
  2. 修改上面的命令:

    • load_rknn('/mnt/model/your_model.rknn')
    • 根据你的模型输入 shape 修改 np.zeros((1, 3, H, W))

12. 遗留问题与后续工作

12.1 未解决的问题

问题 现状 后续方向
推理速度慢(31.8 ms) 与官方 5.5 ms 差 6 倍 检查 NPU 频率、排除 QuMagie 竞争
NPU 频率节点未知 尝试的路径不对 找 /sys/class/devfreq/fde40000.npu/
Python API 开销 未量化 对比 C/C++ API 性能
与 QuMagie 共存 共享代理,资源竞争 高负载时考虑停 QuMagie

12.2 待验证的优化

1
2
3
4
5
6
7
8
# 1. 查看 NPU 可用频率
cat /sys/class/devfreq/fde40000.npu/available_frequencies

# 2. 设置为最高频
echo userspace > /sys/class/devfreq/fde40000.npu/governor
echo 800000 > /sys/class/devfreq/fde40000.npu/userspace/set_freq

# 3. 重新测速

12.3 长期方案

  • 构建独立的 Docker 镜像,包含自己的 rknn-toolkit-lite2 和 npu_transfer_proxy。
  • 在需要独占 NPU 时,临时停止 aicore.container。
  • 生产环境考虑 C/C++ 实现推理核心,减少 Python 开销。

附录 A:关键文件路径速查

文件 路径
QPKG 控制脚本 ${QPKG_ROOT}/QuMagieCore.sh
公共库 ${QPKG_ROOT}/common.include
容器 entrypoint ${QPKG_ROOT}/daemon/entrypoint.sh
原始 entrypoint ${QPKG_ROOT}/docker_scripts/entrypoint_arm_64.sh
模型目录 ${QPKG_ROOT}/daemon/quai/model/
RKNN 环境 1 /usr/local/lib/rknn_env/
RKNN 环境 2 /usr/local/lib/rknn2_env/
系统 RKNN 库 /usr/lib/librknnrt.so
NPU 代理 /usr/local/lib/rknn_env/rknnlite/3rdparty/platform-tools/ntp/linux-aarch64/npu_transfer_proxy

附录 B:术语表

术语 说明
RKNN Rockchip Neural Network,Rockchip 的 NPU 推理框架
RKNNLite 板端轻量推理库,Python 包名 rknnlite
RKNN-Toolkit2 PC 端模型转换工具,Python 包名 rknn
npu_transfer_proxy NPU 用户态通信代理,单实例守护进程
renderD128/129 DRM render 节点,NPU 通过它访问
RGA Rockchip Graphics Accelerator,2D 图形加速
QPKG QNAP Package,QNAP 的应用包
Container Station QNAP 的容器管理平台

本文由 AI 辅助整理,基于 2026-09-24 的实际设备调试会话。
所有命令、报错和输出均来自终端记录,未做修改。
如需转载,请保留本声明。