把 MiniCPM5-2B 跑进骁龙 NPU:AidLux 端侧大模型部署实录

ghhrffh 2026-09-19 23:14:14

 

环境:犀牛派 X1(Qualcomm QCS8550)+ AidLux Ubuntu 22.04结果:MiniCPM5-2B W4A16 全程跑在 Hexagon NPU 上,实测约28 tokens/s,完全离线

一、前言

之前一直觉得"端侧大模型"是个噱头——要么是 CPU 上硬扛,慢到没法用;要么是拿个 0.5B 的小模型意思一下。

这次在犀牛派 X1 上把MiniCPM5-2B跑通了,而且是实实在在跑在Hexagon NPU上:用高通的 QNN 2.48 后端,AidLux 官方的 AidGen SDK 做推理引擎。中文对话流畅,速度约 28 tokens/s,全程不联网。

这篇文章把整个流程记录下来,包括几个卡了我很久的坑。

二、硬件与系统环境

项目

配置

开发板

犀牛派 X1(RhinoPi-X1)

SoC

Qualcomm QCS8550(kalama 平台)

NPU

Hexagon DSP,dsp_arch v73

内存

14 GiB + 7.4 GiB Swap

存储

104 GB(可用约 85 GB)

系统

Ubuntu 22.04.2 LTS(AidLux 2.0,内核 5.15.148-android13)

版本清单:

aidgen-sdk            2.4.3.166      # 推理引擎主库

aidgen-qnn248         2.4.3.166      # QNN 2.48 后端插件

aidlux-aistack-base   1.3.1.169      # 被安装器降级过,见坑 2

aid-mms               1.2.61         # 模型下载工具

qairt/qnn248          /usr/local/lib/aidlux/qairt/qnn248

libaidgen.so.2.4.3    /usr/local/lib

三、为什么走 QNN 而不是 llama.cpp

AidLux 的 ModelFarm 里同一个模型通常有多个后端可选。以 Qwen3.5 系列为例:

$ mms list -a Qwen3.5

Model                 Precision  Chipset  SOC                      Backend

Qwen3.5-0.8B-llamacpp Q4_0       qcs8550  Qualcomm QCS8550         LlamaCPP

Qwen3.5-2B-llamacpp   Q4_0       qcs8550  Qualcomm QCS8550         LlamaCPP

llama.cpp版本走 CPU/GPU,兼容性好、部署简单、跨平台,缺点是把活儿全压在 CPU 上——这块板子的 6 个核心跑 2B 模型,速度很难和 NPU 相比。

而 QNN 版本是为这颗 SoC 的 NPU 专门编译量化的:权重 W4A16(4bit 权重 + 16bit 激活),模型被打包成.aidem格式,由 QNN HTP 后端加载到 Hexagon DSP 上执行。代价是强绑定——模型包只能用于指定芯片 + 指定 QNN 版本,换块板子就得换包。

如果你只是想快速体验,llama.cpp 更省事;想吃满硬件性能,就必须走 QNN 这条路。

四、步骤一:确认设备状态

动手前先确认三件事:架构、内存、NPU 是否暴露。

$ uname -m

aarch64

$ cat /sys/devices/soc0/machine

QCS_KAILUA

$ free -h

              total        used        free      shared  buff/cache   available

Mem:           14Gi       4.5Gi       9.1Gi        48Mi       1.1Gi       9.6Gi

Swap:          7.4Gi          0B       7.4Gi

NPU 相关的设备节点可以在/usr/local/lib/aidlux/qairt/下找到,只要装了aidgen-qnn248这个包,运行库就是齐的:

$ ls -la /usr/local/lib/libaidgen*

libaidgen.so -> libaidgen.so.2

libaidgen.so.2 -> libaidgen.so.2.4.3

libaidgen.so.2.4.3

libaidgen_qnn248.so -> libaidgen_qnn248.so.2

libaidgen_qnn248.so.2.4.3

内存建议留 8 GB 以上:2B 模型的权重虽然只有 1.66 GB,但 QNN 还会额外申请 DMA 缓冲区做中间张量,加上桌面环境本身的开销,内存紧张时加载会直接失败(见坑 4)。

五、步骤二:安装 AidGen 推理运行时

5.1 换源(关键前置)

AidLux 默认的源指向开发环境,实测 TLS 握手会失败。先换成正式源:

sudo cp -n /etc/apt/aid_sources.list /etc/apt/aid_sources.list.before-minicpm5

echo 'https://appcenter.aidlux.com' | sudo tee /etc/apt/aid_sources.list

5.2 安装 SDK

sudo aid-pkg update

sudo aid-pkg install aidgen-sdk --without-progress

sudo aid-pkg install aidgen-qnn248 --without-progress

依赖关系是自动解析的,aidgen-qnn248会依赖aidgen-sdk,aidgen-sdk又会依赖aidlms-sdk和aidlux-aistack-base。

安装过程中你会看到这样一行 warning:

dpkg: warning: downgrading aidlux-aistack-base from 1.3.1.175 to 1.3.1.169

这是正常现象——官方安装器把系统里的aidlux-aistack-base从 1.3.1.175 降到了 1.3.1.169,属于 SDK 侧要求的版本对齐。装完后本模型验证可运行。如果降级后你还有别的应用依赖新版,记得手动装回去。

验证安装:

aid-pkg installed | grep aidgen

# aidgen-qnn248    2.4.3.166

# aidgen-sdk       2.4.3.166

六、步骤三:用 MMS 下载量化模型包

AidLux 提供mms(ModelFarm)命令行工具来下载官方模型。

6.1 先登录

mms login

会弹出浏览器或让你输入账号密码。注意:mms list不登录也能用,但mms get必须登录,否则返回MMS login required。

6.2 查找目标模型

$ mms list -a MiniCPM5

Model           Precision  Chipset  SOC                      Backend

--------------  ---------  -------  ------------------------  -------

MiniCPM5-1B     W4A16      qcs8550  Qualcomm QCS8550         QNN2.36

MiniCPM5-2B     W4A16      qcs8550  Qualcomm QCS8550         QNN2.48

...

三个参数必须严格对上:模型名 / 精度 / 芯片 / 后端版本。

6.3 下载

mms get \

  -m MiniCPM5-2B \

  -p W4A16 \

  -c qcs8550 \

  -b QNN2.48 \

  -d /home/aidlux/models/minicpm5-2b

下载约 1.66 GB,得到qnn248_qcs8550_cl8192.zip(cl8192= context length 8192)。

6.4 ⚠️ 一个必须知道的坑

mms get失败时仍然返回退出码 0。

如果你写脚本用$?判断下载是否成功,会被这个行为坑惨。比如我尝试下载 Qwen3.5-4B 时:

$ mms get -m Qwen3.5-4B -p W4A16 -c qcs8550 -b QNN2.48 -d ./package

Error: model not found. Invalid chipset for -c: qcs8550

$ echo $?

0

命令"成功"退出了,但目录里什么都没有。所以下载完必须检查文件是否真的存在:

ls -lh /home/aidlux/models/minicpm5-2b/qnn248_qcs8550_cl8192.zip

顺带一提,那个 Qwen3.5-4B 在官网页面上明确列着 QCS8550 / QNN2.48 / W4A16,但mms list里查不到,且页面的下载链接字段是空的,is_contact和is_commercial都是 true。官网展示 ≠ 你能下载,页面上的参数不能当作可下载的依据,一切以mms list为准。

七、步骤四:读懂模型包结构

解压后目录长这样:

rnn248_qcs8550_cl8192/

├── aidgen_config.json             # AidGen 引擎入口配置

├── minicpm5-2b-htp.json           # QNN HTP 后端详细参数

├── htp_backend_ext_config.json    # DSP 扩展配置(性能档位)

├── aidgen_chat_template.txt       # 官方对话模板示例

├── minicpm5-2b-tokenizer.json     # 词表(约 9.4 MB)

├── minicpm5-2b_qnn248_qcs8550_cl8192_1_of_4.serialized.bin.aidem   # 546 MB

├── ..._2_of_4.serialized.bin.aidem        # 368 MB

├── ..._3_of_4.serialized.bin.aidem        # 368 MB

├── ..._4_of_4.serialized.bin.aidem        # 646 MB

└── prefix-kvcache/

    ├── kv-cache.primary.qnn-htp         # 预编译 KV Cache(22 KB)

    └── dialog.json

几个关键点:

  • 权重被切成 4 片,加载时要按顺序全部传给引擎,缺一片就报错。
  • .aidem是加密/受保护的权重格式,不能直接拿去做别的用途,只能通过 AidGen SDK 读。
  • prefix-kvcache是省时间的关键:里面存了系统提示词的 KV Cache 前缀,加载时直接复用,省掉每次重算 prompt 的开销。

aidgen_config.json内容很简洁,主要就是告诉引擎去哪儿找权重:

{

  "backend_type": "genie",

  "prefix_path": "prefix-kvcache/kv-cache.primary.qnn-htp",

  "model": {

    "path": [

      "minicpm5-2b_qnn248_qcs8550_cl8192_1_of_4.serialized.bin.aidem",

      "minicpm5-2b_qnn248_qcs8550_cl8192_2_of_4.serialized.bin.aidem",

      "minicpm5-2b_qnn248_qcs8550_cl8192_3_of_4.serialized.bin.aidem",

      "minicpm5-2b_qnn248_qcs8550_cl8192_4_of_4.serialized.bin.aidem"

    ]

  }

}

真正的性能参数在minicpm5-2b-htp.json里:

{

  "context": { "size": 8192, "n-vocab": 130560, "bos-token": -1, "eos-token": [1, 130073] },

  "sampler": { "seed": 42, "temp": 1, "top-p": 0.95, "top-k": 20, "greedy": false },

  "engine": {

    "n-threads": 3,

    "backend": {

      "type": "QnnHtp",

      "QnnHtp": {

        "kv-dim": 128,

        "use-mmap": true,

        "cpu-mask": "0xe0"

      }

    },

    "model": {

      "positional-encoding": { "type": "rope", "rope-dim": 64, "rope-theta": 5000000 }

    }

  }

}

注意"cpu-mask": "0xe0"—— 二进制1110_0000,即只使用5 / 6 / 7 号核心,也就是 SoC 上频率最高的那三个大核;配合n-threads: 3,把推理线程锁在大核上,避免被小核拖慢。这是端侧推理很典型的调优手法。

这块板子上 Linux 能看到的核心频率分布,也能印证这个设计意图:

cpu0 ~ cpu2 : 2016000 Hz   # 小核

cpu3 ~ cpu4 : 2803200 Hz   # 中核

cpu5        : 3187200 Hz   # 超大核

八、步骤五:写一个最小的 C++ 推理程序

AidGen SDK 提供的是 C++ 头文件接口(aidlux/aidgen/aidgen.hpp),核心就三个类:Context(引擎上下文)、Generator(生成器)、Tokenizer。

核心代码chat.cpp(为便于阅读,这里省略了语音输入部分):

#include "aidlux/aidgen/aidgen.hpp"

#include <iostream>

#include <string>

#include <cstdio>

using namespace aplux::aidgen;

int mainintargc,char**argv {

    set_log_level(LogLevel::ERROR);

    ContextProperties props;

    props.stream = true;              // 流式输出

    props.enable_profiler = true;     // 打开性能统计,用来测速

    props.enable_prompt_cache = false;

    auto ctx = Context::create_instance("aidgen_config.json", props, BackendType::QNN248);

    if (!ctx) return 1;

    auto ec = ctx->initialize();

    if (ec != ErrorCode::SUCCESS) {

        std::cerr << "Context initialization failed: " << int(ec) << '\n';

        return 1;

    }

    auto gen = Generator::create_instance(ctx);

    if (!gen) return 1;

    ec = gen->initialize();

    if (ec != ErrorCode::SUCCESS) {

        std::cerr << "Model loading failed: " << int(ec) << '\n';

        return 1;

    }

    std::string input;

    bool first = true;

    while (true) {

        std::cout << "\n你:" << std::flush;

        if (!std::getline(std::cin, input) || input == "/quit") break;

        if (input.empty()) continue;

        // 关键:多轮对话必须手动 reset,否则上下文会串

        if (!first && gen->reset() != ErrorCode::SUCCESS) return 1;

        first = false;

        // 严格按模型自带模板构造提示词

        std::string prompt = "<s> <|im_start|>user\n" + input +

            "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n";

        bool failed = false, ended = false;

        std::cout << "模型:" << std::flush;

        ec = gen->run(prompt, [&](const GenEvent& event, void*) {

            if (event.state == GenState::ERROR || event.state == GenState::ABORT) failed = true;

            if (event.state == GenState::END) ended = true;

            std::cout << event.text << std::flush;   // 流式打印

        });

        std::cout << '\n';

        if (ec != ErrorCode::SUCCESS || failed || !ended) {

            std::cerr << "Generation failed or incomplete: " << int(ec) << '\n';

            return 1;

        }

        ProfileData perf;

        if (gen->get_profiler(perf) == ErrorCode::SUCCESS)

            std::cerr << "[速度 " << perf.generator.generate_tps << " tokens/s,生成 "

                      << perf.generator.generated_token_count << " tokens]\n";

    }

    return gen->finalize() == ErrorCode::SUCCESS ? 0 : 1;

}

编译(注意-Wl,-rpath,少了这一步运行时会找不到libaidgen.so.2):

cd /home/aidlux/models/minicpm5-2b

g++ -std=c++17 -O2 chat.cpp -o minicpm-chat \

    -L/usr/local/lib -Wl,-rpath,/usr/local/lib -laidgen -pthread

配套的启动脚本chat.sh很简单,就是切到模型目录(因为aidgen_config.json里的路径是相对路径):

#!/usr/bin/env bash

set -euo pipefail

cd /home/aidlux/models/minicpm5-2b/qnn248_qcs8550_cl8192

exec ../minicpm-chat "$@"

运行:

bash /home/aidlux/models/minicpm5-2b/chat.sh

  • 九、步骤六:跑通验证

启动后会看到引擎申请 DMA 缓冲区并加载权重:

[info] [license.cpp:650] the device is licensed, with license ID: aid****

BufferAllocator.cpp:100] Using DMA-BUF heap named: system

[INFO] "Using create From Binary"

[INFO] "Allocated total size = 213287424 across 8 buffers"

Allocated total size = 213287424—— 运行期额外申请了约213 MB的 DMA 缓冲(8 个 buffer),这部分是除权重之外的内存开销。

然后是对话:

你:请介绍一下你自己

模型:我是 MiniCPM5-2B,一个轻量级的中文大语言模型……

[速度 28.18 tokens/s,生成 35 tokens]

实测数据:

指标

数值

生成速度

约 28.18 tokens/s

单次生成量

35 tokens

权重体积

1.66 GB(zip)/ 约 1.9 GB(解压)

运行期 DMA 缓冲

213 MB

上下文长度

8192

精度

W4A16

后端

QNN 2.48 / Hexagon HTP v73

连续提问也验证过:1+1返回 2,中国首都是哪里返回北京,程序正常退出(退出码 0)。

十、踩坑记录

坑 1:AidLux 开发源 TLS 失败

aid-pkg update报 TLS 连接错误,换成正式源https://appcenter.aidlux.com后解决。记得备份原文件。

坑 2:安装器偷偷降级了系统包

aidlux-aistack-base从 1.3.1.175 降到 1.3.1.169。不影响本模型,但如果你还有别的应用依赖新版,降级后可能出问题,需要手动恢复。

坑 3:mms get失败也返回 0

见 6.4 节。这是最阴的一个坑——脚本里判断退出码完全不可靠,必须检查产物文件是否存在。

坑 4:同一个模型只能跑一个实例(err 1002)

这个是踩得最惨的。桌面窗口开着聊天没关,我再开一个终端跑命令行版,直接崩:

[INFO]  "Allocated total size = 213287424 across 8 buffers"

[ERROR] "Could not create context from binary for context index = 2 : err 1002"

[ERROR] "Create From Binary FAILED!"

[E][implqnn248.cpp:1008][AIDGEN:Backend:QNN248] Failed to create the dialog

Model loading failed: -20

err 1002是 NPU 侧的资源创建失败——NPU 上下文已经被占用(或者内存不足)。排查方法:

pgrep -af minicpm-chat     # 看是不是还挂着旧进程

free -h                    # 看可用内存

杀掉残留进程后重新启动即可:

pkill -f minicpm-chat

结论:minicpm-chat是单实例资源模型,同一时间只能有一个进程持有 NPU 上下文。桌面快捷方式和终端命令行不要同时开。

坑 5:多轮对话上下文会串

gen->run()是累积式的,不会自动清空历史。如果上一句问了"1+1",下一句问"那 2+2 呢",模型会带着前面的 KV 状态继续生成。

解决办法就是每轮之前调一次gen->reset():

if (!first && gen->reset() != ErrorCode::SUCCESS) return 1;

我这边做成了每个问题互相独立的模式——简单、可预测,不会出现上下文越滚越长导致速度下降的问题。如果你要做真正的多轮对话,就别 reset,但要注意 context size 8192 的上限。

坑 6:提示词模板不能自己乱写

MiniCPM5 是思考模型,模板必须严格按官方aidgen_chat_template.txt来:

<s><|im_start|>user

{你的问题}<|im_end|>

<|im_start|>assistant

<think>

</think>

注意最后的<think>\n\n</think>\n——这是非思考模式的写法,用一个空的 think 块让模型直接出答案。

如果你漏掉,或者自己随便写个User: xxx\nAssistant:,模型要么输出乱码,要么把思考过程全吐出来,速度也会因为多生成一堆 token 而暴跌。

坑 7:编译时忘记 rpath

只写-laidgen不写-Wl,-rpath,/usr/local/lib,编译能过,但运行时:

error while loading shared libraries: libaidgen.so.2: cannot open shared object file

因为/usr/local/lib不一定在ld.so的默认搜索路径里。加上 rpath 最省事。

十一、加餐一:离线中文语音输入

打字麻烦,加了个语音输入:在聊天窗口输入/voice,按回车开始录音,再按回车结束,识别结果会显示出来,确认后发送。

用的是Vosk 小型中文模型(vosk-model-small-cn-0.22,43 MB),本地离线识别,不联网、不上传、不保存录音。

核心是parec按 16 kHz / 单声道拉流喂给识别器:

command = ['parec', '--raw', '--format=s16le', '--rate=16000', '--channels=1']

command.append('--device=' + source)

这里有两个实测发现的细节:

1. 麦克风端口要选对。犀牛派 X1 的红色 LINE IN 口,在 PulseAudio 里实际对应headset-mic端口,另外两个端口(speaker-mic、handset-mic)接同样的麦克风都采不到声音。所以代码里做了强制端口切换:

DEFAULT_QAL_PORT = 'headset-mic'

# 通过 pactl set-source-port regular0 headset-mic 切换

2. 板载没有麦克风。官方音频规格只有 3.5mm Line in / Line out,而且 Line in 是线路电平输入,需要带前置放大的麦克风,普通驻极体麦克风直插基本没信号。最省事的是插一个USB 麦克风/耳麦,免驱,系统直接识别。

脚本会优先挑名称或描述里带USB的录音设备。另外还加了静音电平检测——如果录完全程电平接近 0,直接提示"没有采到声音"并说明需要外接麦克风,而不是让你对着一句"识别失败"发呆。

十二、加餐二:桌面快捷方式

每次敲命令太麻烦,加个.desktop文件放到~/Desktop/:

[Desktop Entry]

Type=Application

Name=MiniCPM5 本地聊天

Comment=MiniCPM5-2B QCS8550 NPU

Exec=/usr/bin/gnome-terminal --wait --title=MiniCPM5-2B -- /bin/bash /home/aidlux/models/minicpm5-2b/desktop-chat.sh

Icon=utilities-terminal

Terminal=false

Categories=Utility;

desktop-chat.sh做了一件事:出错时不让窗口立刻消失,方便看错误信息。

#!/usr/bin/env bash

printf '正在加载 MiniCPM5-2B,请稍候…\n'

bash /home/aidlux/models/minicpm5-2b/chat.sh

status=$?

if [ "$status" -ne 0 ]; then

    printf '\n模型启动或运行失败(退出码 %s),请保留上面的错误信息。\n' "$status"

    read -r -p '按回车关闭窗口…'

fi

exit "$status"

这个很有必要——双击图标启动的终端窗口如果程序崩了会瞬间关闭,你根本来不及看err 1002之类的错误。

十三、总结

整体流程其实就六步:

  1. 确认设备(架构 / 内存 / NPU 运行库)
  2. 换正式源,装aidgen-sdk+aidgen-qnn248
  3. mms login后按 模型名/精度/芯片/后端 四要素下载模型包
  4. 解压,理解.aidem分片 +aidgen_config.json的结构
  5. 用 AidGen C++ 接口写个几十行的推理程序
  6. 编译时带上 rpath,跑起来

几点体会:

  • QNN 路线性能确实好,2B 模型 28 tokens/s 在嵌入式设备上完全可用了,而且功耗远低于 CPU 推理。
  • 绑定很硬,模型包和芯片、QNN 版本强绑定,换个平台就得重新找包,这是端侧目前的常态。
  • 官方的坑得自己趟:mms get假成功、安装器降级系统包、NPU 单实例互斥,这些文档里都不会写。
  • 最大的坑其实是那个 err 1002,排查了半天才发现是旧进程没退。写端侧应用一定要做好资源生命周期管理。

后面想试试给模型挂上工具调用(function calling),让它能真正干点活,而不只是聊天。如果有进展再写一篇。

本文部署记录于 2026-09-19,环境为犀牛派 X1 + AidLux 2.0(Ubuntu 22.04)/ QCS8550 / QNN 2.48。

 

...全文
206 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
内容概要:本文系统研究了基于雅可比伪逆迭代的双臂协同轨迹规划与跟踪误差收敛性问题,结合机械臂正向与逆向运动学建模、蒙特卡洛法分析工作空间、笛卡尔空间内的圆弧轨迹生成等关键技术,利用Matlab实现了双臂系统的协同控制与轨迹跟踪仿真。重点探讨了雅可比矩阵伪逆法在解决冗余自由度机械臂逆运动学问题中的应用,深入分析了轨迹跟踪过程中误差的收敛特性,并通过仿真实验验证了该方法在复杂任务场景下的有效性、稳定性与鲁棒性,为多臂机器人在工业自动化、精密操作等领域的协同作业提供了理论依据与技术支持。; 适合人群:具备机器人学、自动控制理论基础及Matlab编程能力的科研人员与工程技术人员,特别适合从事机械臂控制、轨迹规划、多机器人协同等方向的硕士、博士研究生及相关领域研究人员。; 使用场景及目标:①掌握基于雅可比伪逆法的机械臂逆运动学求解原理与实现方法;②理解双臂系统在笛卡尔空间中协同轨迹规划的机制;③分析并优化轨迹跟踪过程中的误差收敛性能;④为多机器人系统在复杂动态环境下的高精度协同控制提供算法设计与仿真验证支持。; 阅读建议:建议读者结合文中提供的Matlab代码进行实践操作,按照文档目录循序渐进地学习,重点关注算法实现细节、仿真参数设置与结果分析部分,同时可进一步探索将智能优化算法或深度学习方法引入轨迹规划与误差补偿的研究方向。

7,832

社区成员

发帖
与我相关
我的任务
社区描述
本论坛以AI、IoT、PC 、XR、Auto等核心板块组成,为开发者提供便捷及高效的学习和交流平台。 高通开发者专区主页:https://qualcomm.csdn.net/
物联网人工智能开源 企业社区 北京·东城区
社区管理员
  • csdnsqst0050
  • chipseeker
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧