犀牛派X1 - 入门保姆级教程连载02 - 从零开始玩转边缘 AI - 下篇

上一篇介绍了如何调用yolo的模型简单推理,这篇我们来个大招。

YOLO26l INT8 (QNN 2.36 / NPU) 测试说明 —— 犀牛派 X1 (QCS8550)

这篇文档是讲述如何在犀牛派 X1 上使用 QNN 2.36 HTP(NPU) 后端测试 YOLO26l W8A8 量化模型的完整流程,
包括:环境确认、单图基准测试、USB 摄像头实时检测、NPU 后端是否生效的验证方法、以及实测帧率水平。

1. 测试环境

项目 内容
主板 犀牛派 X1,SoC 为高通 QCS8550 (Kalama)
系统 Ubuntu 22.04 (AidLux),内核 5.15,Python 3.10
推理后端 QNN 2.36 (/usr/local/lib/aidlux/aidlite/qnn236/libQnnHtp.so),HTP V73
Python SDK pyaidlite 2.3.1 + opencv-python 4.13 + numpy 1.26 (已预装)
模型 cutoff_yolo26l_qcs8550_w8a8.qnn236.ctx.bin(mms 下载,INT8 W8A8,输入 640x640)
模型输出 split_xyxy 布局:[1,4,8400](框) + [1,80,8400](类别分数)

AidLite 的关键配置(camera_test.pyload_interpreter()):

config.framework_type  = aidlite.FrameworkType.TYPE_QNN     # QNN 后端
config.accelerate_type = aidlite.AccelerateType.TYPE_DSP    # DSP => HTP(NPU)
config.is_quantify_model = 1                                # INT8 量化模型

2. 目录结构

yolo26l_test/
├── README.md                 # 本文档
├── camera_test.py            # 主测试:摄像头实时检测 + NPU 验证 + 帧率统计
├── benchmark_image.py        # 单张图片基准测试(官方示例流程)
├── npu_monitor.sh            # 从另一个终端验证/观察 NPU 是否在工作
├── yolo.py / utils.py        # 模型封装与预处理/后处理(NMS 等)
├── bus.jpg                   # 官方测试图片
└── models/QCS8550/W8A8/
    └── cutoff_yolo26l_qcs8550_w8a8.qnn236.ctx.bin

由于之前写第一篇的时候把代码都贴论坛里面,导致字符太多。超出限制了。所以后期我代码放到github上,链接发布出来给大家分享。

3. 测试步骤

3.1 单图基准测试(先确认模型可用)

cd ~/yolo26l_test
python3 benchmark_image.py --invoke_nums 30

预期输出:bus.jpg 检出 5 个目标(1 个 bus + 4 个 person),标注图保存为 result.jpg,
并打印 30 次 invoke 的耗时统计。

3.2 摄像头实时检测(正式测试)

python3 camera_test.py                     # 自动探测 USB 摄像头,默认统计 300 帧
python3 camera_test.py --source 0 --show   # 指定 /dev/video0 并实时显示画面(需桌面)
python3 camera_test.py --frames 0          # 不限帧数,Ctrl+C 结束并打印统计
python3 camera_test.py --save out.mp4      # 同时保存标注视频

无摄像头时也可以用图片/视频模拟帧源验证全流程:

python3 camera_test.py --source bus.jpg --frames 300

3.3 验证是否真正调用了 NPU

脚本启动时会自动执行 check_npu_backend(),从三个角度验证,全部通过则打印
验证结论: 通过,推理运行在 NPU(HTP) 上:

  1. HTP 后端库映射:读取 /proc/self/maps,确认进程加载了
    libQnnHtp.solibQnnHtpV73Stub.so(QCS8550 的 NPU 为 HTP V73;
    如果只加载了 libQnnCpu.so 则说明跑在 CPU 上)。
  2. fastrpc 通道:进程持有 /dev/adsprpc-smd-secure 文件句柄,
    这是 ARM 侧与 CDSP/NPU 通信的 RPC 通道,只有真正下发到 NPU 才会打开。
  3. CDSP 服务进程:cdsprpcd 在运行中(NPU 推理的系统侧依赖)。

另开一个终端,可在推理进行中用监控脚本交叉验证:

脚本内容如下:

#!/bin/bash
# npu_monitor.sh —— 验证并观察 NPU(HTP/CDSP) 是否在被使用
#
# 用法:
#   ./npu_monitor.sh              # 自动寻找 camera_test.py 进程,检查一次
#   ./npu_monitor.sh <PID>        # 检查指定进程
#   ./npu_monitor.sh <PID> 30     # 每 2 秒检查一次,共 30 次
#
# 验证依据(满足即说明推理跑在 NPU 上):
#   1) 进程加载了 libQnnHtp.so 和 libQnnHtpV73Stub.so (QCS8550 对应 HTP V73)
#   2) 进程持有 /dev/adsprpc-smd-secure fd(ARM 与 CDSP/NPU 的 fastrpc 通道)
# 说明: 本平台上 cdsprpcd 的 CPU 占用即使 NPU 满负荷也接近 0(DSP 侧计算不计入),
#       因此 cdsprpcd %CPU 不能作为判据,仅打印供参考。

PID=$1
TIMES=${2:-1}
CLK_TCK=100

if [ -z "$PID" ]; then
    for p in $(pgrep -f "camera_test.py"); do
        if [ "$(cat /proc/$p/comm 2>/dev/null)" = "python3" ]; then
            PID=$p
            break
        fi
    done
    if [ -z "$PID" ]; then
        echo "未发现正在运行的 camera_test.py,请先启动测试,或用: $0 <PID> [次数]"
        exit 1
    fi
fi

echo "监控进程 PID=$PID"
echo "=========================================================="

for i in $(seq 1 "$TIMES"); do
    if [ ! -d "/proc/$PID" ]; then
        echo "[$i] 进程 $PID 已退出"
        break
    fi

    HTP=$(grep -o 'libQnnHtp[^ ]*\.so' "/proc/$PID/maps" 2>/dev/null | sort -u | tr '\n' ' ')
    FASTRPC=$(ls -l "/proc/$PID/fd" 2>/dev/null | grep -o 'adsprpc-smd[a-z-]*' | sort -u | tr '\n' ' ')

    echo "[$i] $(date +%H:%M:%S)"
    echo "    HTP 库    : ${HTP:-未加载!}"
    echo "    fastrpc fd: ${FASTRPC:-未打开!}"

    if echo "$HTP" | grep -q "libQnnHtp.so" && echo "$HTP" | grep -q "V73" && [ -n "$FASTRPC" ]; then
        echo "    结论      : NPU(HTP V73) 后端生效 ✔"
    else
        echo "    结论      : 未确认 NPU 后端,请检查模型加载配置 ✘"
    fi

    # 参考: cdsprpcd CPU(本平台上即使 NPU 工作中也通常保持 0)
    CPID=$(pidof cdsprpcd | awk '{print $1}')
    if [ -n "$CPID" ]; then
        PREV=$(awk '{print $14 + $15}' "/proc/$CPID/stat" 2>/dev/null)
        [ "$i" -lt "$TIMES" ] && sleep 2
        CUR=$(awk '{print $14 + $15}' "/proc/$CPID/stat" 2>/dev/null)
        if [ -n "$PREV" ] && [ -n "$CUR" ]; then
            CPU=$(awk "BEGIN {printf \"%.1f\", ($CUR - $PREV) * 100.0 / ($CLK_TCK * 2)}")
            echo "    (参考) cdsprpcd %CPU: $CPU"
        fi
    elif [ "$i" -lt "$TIMES" ]; then
        sleep 2
    fi
done
echo "=========================================================="

chmod +x ./npu_monitor.sh # 给丫执行权限,能够执行。
./npu_monitor.sh           # 自动找到 camera_test.py 进程检查一次
./npu_monitor.sh <PID> 30  # 对指定进程每 2 秒检查一次,共 30 次

也可以手动检查(把 <PID> 换成 camera_test.py 的进程号):

grep -o 'libQnnHtp[^ ]*\.so' /proc/<PID>/maps | sort -u     # 应有 libQnnHtp.so + libQnnHtpV73Stub.so
ls -l /proc/<PID>/fd | grep adsprpc                         # 应有 /dev/adsprpc-smd-secure

注意:本平台上 cdsprpcd 的 CPU 占用即使 NPU 满负荷也接近 0
(计算发生在 DSP 固件侧,不计入 ARM 进程),不能用它判断 NPU 是否工作。

4. 实测结果(本机实测,供参考)

单图 invoke 基准(benchmark_image.py,30 次):

  • 平均 6.59 ms/次,最小 6.51 ms —— 纯推理上限约 150 FPS

帧源实时模式(camera_test.py --source bus.jpg --frames 300,810x1080 帧,300 帧统计):

阶段 平均耗时
预处理 letterbox+归一化 (CPU) 5.09 ms
NPU 推理 invoke 7.18 ms
后处理 置信度过滤+NMS (CPU) 1.16 ms
端到端单帧 13.43 ms ⇒ 约 74.5 FPS

USB 摄像头实测(GEMBIRD AX2311 UVC 摄像头,/dev/video2,640x480,300 帧):

指标 数值
NPU 推理 invoke 7.9~8.2 ms(上限约 125 FPS)
端到端单帧(预处理+推理+后处理) 约 14.9 ms ⇒ 约 67 FPS
系统整体帧率 约 8.2 FPS(受摄像头取流限制)

结论:

  • YOLO26l INT8 在 QCS8550 NPU 上纯推理约 6.6~8.2 ms(120~150 FPS),
    串行流水线端到端约 67~75 FPS,检测管线本身远超实时需求。
  • 最终整体帧率取决于摄像头:测试用的这款廉价 UVC 摄像头实际只能输出约
    8 FPS(纯 cap.read() 不含任何处理也是 8.2 FPS,换 320x240 也一样,
    驱动也未开放曝光调节),属于摄像头硬件/光线限制,与 NPU 无关。
    换用能稳定输出 30 FPS 的摄像头,整体即可达到 30 FPS 满帧。

5. 常见问题

5.1 摄像头打不开 / 提示"未找到可用的 USB 摄像头"

/dev/video0/dev/video1 是高通相机子系统节点(cam-req-mgrcam_sync),
不是 USB 摄像头;/dev/video32/33 是视频解码器。UVC 摄像头插入后才会
出现新的 /dev/videoX。排查:

lsusb                       # 应能看到摄像头设备;若只有 root hub 说明系统没识别到
sudo dmesg -w               # 保持运行,然后重新插拔摄像头,观察是否枚举
v4l2-ctl --list-devices     # 查看 UVC 设备节点

lsusb 看不到设备:换一个 USB 口(优先蓝色 USB3 口)、确认摄像头在其他电脑上
可用、检查转接线/供电。识别到设备后用 v4l2-ctl --list-devices 找到节点编号,
再用 python3 camera_test.py --source <编号> 指定。

5.2 提示模型加载失败

确认模型文件存在且完整(约 26 MB):

ls -l models/QCS8550/W8A8/cutoff_yolo26l_qcs8550_w8a8.qnn236.ctx.bin
md5sum models/QCS8550/W8A8/cutoff_yolo26l_qcs8550_w8a8.qnn236.ctx.bin

5.3 如何对比 CPU 与 NPU 速度

camera_test.pyload_interpreter()
config.accelerate_type 改为 aidlite.AccelerateType.TYPE_CPU 再跑一次,
对比 invoke 耗时即可直观看到 NPU 的加速比(此时 NPU 验证会提示未生效,属预期)。

现在调用后端QNN是真的丝滑,就是自己需要调整一下iou的threshold 来调整NMS最小非近邻的值,来减少框框的数量,不然乱糟糟~

我写了帮助文档进去。可以用-h调用:

可以调大一些参数:

每次执行完成会有统计结果:

我执行的时候保存了视频。

可以后期作为检测结果检查回放。

如果有兴趣复现操作和学习用的朋友可以直接从我的github链接下载完整代码和说明文档。
Github仓库链接: https://github.com/yoyojacky/rhinox1_yolo26l_test

3 个赞