AI / ML
本指南在 Dragonwing IQ-8275 (QCS8275) 的 Hexagon NPU 上运行 Google 的 Gemma-4 E2B。您将针对模型编译时使用的完全相同的 Qualcomm AI 运行时,从源码构建 Google 的 LiteRT-LM 运行时,然后直接在 NPU 上运行公开的、Apache-2.0 许可的
.litertlm,解码速度约为每秒 28 个 token。每条命令都可以直接复制粘贴。
- 目标设备: IQ-8275 EVK,Ubuntu 24.04 (aarch64),Hexagon v75 NSP。
- 模型:
litert-community/gemma-4-E2B-it-litert-lm,具体为gemma-4-E2B-it_qualcomm_qcs8275.litertlm(3.29 GB)。该文件仅支持 NPU。 - 范围:文本输入,文本输出。
.litertlm中也包含视觉和音频塔,但本文不会用到它们:每条命令都以文本提示驱动litert_lm_main。接入图像或音频输入超出了本指南的范围。 - 上下文窗口:4096 个 token,包括提示和生成的输出,编译进模型且无法在运行时提高。参见上下文长度。
如何阅读本指南。 每条命令都以
ubuntu 用户身份在 IQ-8275 上运行。所有工作都位于一个目录 ~/iq8-gemma 中,并且每个代码块都以自己的 cd 开头,因此您可以按顺序将任何代码块粘贴到任何新终端中,而无需跟踪当前所在目录。粘贴前无需编辑任何内容。您将完成的工作
- 设置 IQ8 设备并确认 FastRPC 存在。
- 下载公开的 NPU 模型并读取它所需的确切 QAIRT 版本。
- 针对该 QAIRT 从 LiteRT-LM 构建
litert_lm_main,并应用两个 Linux 启用补丁。 - 组装运行目录并在 NPU 上运行模型。
- (可选)从 Ubuntu 原型迁移到 Qualcomm Linux 生产镜像。
前提条件
在执行以下任何操作之前,开发板需要启用 Qualcomm 外设并安装 AI 运行时:FastRPC 用户空间(libcdsprpc.so)、QNN 库(libqnn-dev、qnn-tools、snpe-tools、tensorflow-lite-qcom-apps)、qcom-libdmabufheap、GStreamer QCOM 插件,以及固件更新和重启。
请先按照 IQ8 设备页面完成设置,然后再回到这里:
重启后,重新连接开发板并确认 FastRPC 存在:
/dev/fastrpc-* 不存在,则内核缺少 FastRPC 支持。到此为止:这是 BSP 或镜像问题,不是您能在用户空间修复的。
获取模型并找出它所需的 QAIRT 版本
不要猜测版本。创建工作目录并将公开的 NPU 模型下载到其中(3.29 GB;-C - 可在连接中断时续传):
.litertlm 内嵌了 QNN 上下文二进制文件,它们与编译时所用的 QAIRT 版本锁定,设备端运行时必须匹配。该版本没有预先公布,而 LiteRT 的源码固定的是一个更新的版本,因此直接从文件中读取:
针对该 QAIRT 构建 litert_lm_main
构建工具链
Pre Requisites 不会安装编译器或 Bazel,因此需要添加它们(LiteRT-LM 使用 clang-18 构建;生成的二进制文件链接 GNU libstdc++):
检出固定了您的 QAIRT 版本的 LiteRT-LM 提交
LITERT_QAIRT_SDK 允许 Bazel 使用本地 SDK,但工作区的 strip_prefix 必须与固定版本的目录布局匹配,因此要检出 2.44 分支线上的提交。最新的此类提交是 cbf463d97fa3(它将 LiteRT d865fd82 固定到 QAIRT 2.44.0.260225;下一个提交跳到了 2.46)。
应用两个 Linux 启用补丁
LiteRT-LM 将两处 Qualcomm 设置置于#if defined(__ANDROID__) 之后,因此在桌面或嵌入式 Linux aarch64 上它们会被静默跳过。两个补丁都是一行代码,添加 || defined(__linux__)。
补丁 1:dispatch 库目录。 LiteRT-LM 仅在 __ANDROID__ 或 __EMSCRIPTEN__ 下推导查找 libLiteRtDispatch_Qualcomm.so 的目录。没有这个补丁,NPU 加速器永远不会注册,DISPATCH_OP 保持未解析状态:
CreateLiteRtNpuOptions() 仅在 #if defined(__ANDROID__) 下调用 SetHtpPerformanceMode(kBurst) 和 SetLogLevel(kOff)(源码中有一条 TODO … Bug: 498622107 承认了这一点)。在 Linux 上这些调用被跳过,因此 dispatch 插件获得 HtpPerformanceMode::kDefault:DSP 永远不会自行提升到 burst,解码以约每秒 16 个 token 运行,同时 QNN 调试日志刷屏 stdout。为 Linux 启用该代码块:
sed,是因为该文件中还有其他不能触碰的裸 #if defined(__ANDROID__) 行。)
构建
~/iq8-gemma/LiteRT-LM/bazel-bin/ 下):
runtime/engine/litert_lm_mainlibLiteRtDispatch_Qualcomm.so(位于.../qualcomm/dispatch/目录树下)libLiteRt.so(LiteRT 核心运行时库)
组装运行目录
将二进制文件、dispatch 插件、LiteRT 核心库、约束提供程序和模型收集到~/iq8-gemma/run 中。find 调用会定位 Bazel 放置的构建输出,因此此代码块可直接使用:
libstdc++,它已随 build-essential 安装,因此这条命令应该没有任何输出:
-stdlib=libc++ 构建,则需要 sudo apt-get install -y libc++1 libc++abi1;这里的默认构建使用 libstdc++,所以不需要。)
在 NPU 上运行
此代码块将LD_LIBRARY_PATH 指向运行目录和匹配的 QAIRT 主机库,将 ADSP_LIBRARY_PATH 指向 Hexagon v75 skel,然后以 root 身份运行(FastRPC 和 cDSP 需要)。$HOME 和 $PWD 路径会在 sudo 之前由 shell 展开,因此无需编辑即可运行:
litert_lm_main 默认打印基准测试信息。应用补丁 2 后,您会在运行早期看到 Set HTP performance mode: 2,并且 QNN 调试日志会安静下来:这就是 burst 模式生效了。)
确认它真的运行在 NPU 上
该模型仅支持 NPU:它没有 CPU 图。请求 CPU 后端可以证明这一点:--backend npu 下生成正确的文本,那么执行就是在 Hexagon NSP 上(HTP burst 模式,HtpPerformanceMode: 2,可在日志中看到)。
性能
在 IQ-8275 上测量,公开模型,NPU 后端,应用了两个 Linux 补丁,已预热,CPU 调速器固定为performance:
说明:
- Burst 模式至关重要。 不应用补丁 2 时,从源码构建的版本解码约为每秒 16 tok;应用后为 25 到 29。如果您看到约 16 和刷屏的 QNN 日志,说明补丁 2 没有生效。
- 解码速度随输出增长而下降。 每个解码步骤都要对整个 KV 缓存做注意力计算,因此 200 token 的回答平均约 28 tok/s,800 token 的约 25;最开始的 token(短上下文)最快。这不是热问题:SoC 全程温度约 43 °C。
- 固定 CPU 调速器以获得稳定的数字(启动后首次推理时 DSP 仍会自行提升时钟):
- 不同提示长度的预填充 tok/s 不可比较。 对于单行提示,约 1,240 的数字主要由固定开销主导,因此其本身没有意义。
底层原理:实际发生了什么
值得理解,因为上面的每一步都对应此技术栈中的一层。.litertlm 是一个容器,不是 tflite 文件
该文件以魔术字节 LITERTLM 开头。它内部打包了运行时所需的一切:SentencePiece 分词器、模型元数据(聊天模板、EOS/EOA token、使其仅支持 NPU 的后端约束)、LiteRT 模型图,以及对 NPU 而言最重要的部分——预编译的 QNN 上下文二进制文件。对于此模型,它们是两个图,qnn_partition_0 和 qnn_partition_1(transformer 被拆分到两个 HTP 上下文中)。权重为 w4a16(4 位权重,16 位激活):这就是一个约 2B 参数的模型能装进 NSP 并快速运行的原因。
容器还携带了本纯文本指南永远不会触及的图:一个视觉编码器及其适配器(签名 vision_280 和 vision_adapter_280,将 2,520 个图像块转换为 280 个软 token)和一个音频编码器及其适配器(audio_adapter,输入 816 个 mel 帧,输出 204 个软 token),以及图像结束和音频结束嵌入(eoi、eoa)。要使用它们需要一个能组装交错的 token 和软 token 序列的多模态前端;litert_lm_main --input_prompt 只会向模型提供文本,因此在下面的每次运行中,视觉和音频塔都处于闲置状态。
执行路径,逐层分解
DISPATCH_OP,一个作为”运行这个预编译供应商图”占位符的自定义算子。当 NPU 加速器注册时,LiteRT 加载 libLiteRtDispatch_Qualcomm.so,它将 QNN 上下文二进制文件交给 QNN HTP 后端。QNN 通过 FastRPC(经由 libcdsprpc.so 和 /dev/fastrpc-cdsp 到 DSP 的远程过程调用传输)与 Hexagon NSP 通信;实际的矩阵乘法在 libQnnHtpV75Skel.so 内部执行,这是加载在 v75 NSP 上、通过 ADSP_LIBRARY_PATH 找到的 QNN “骨架”。因此三样东西必须一致:主机 QNN 库、DSP skel 和模型内的上下文二进制文件,全部为同一个 QAIRT 版本。这就是版本步骤重要的原因。
为什么版本必须完全匹配
QNN 上下文二进制文件是针对一个 QAIRT 版本提前编译并序列化的:其图格式、算子包集合以及它期望的 skel ABI 都被固化在内。在不同的运行时上加载它,最好的情况是反序列化被拒绝。该版本没有预先记录,而 LiteRT 的main 固定的是更新的 2.47,因此两者都不权威。文件内序列化的构建 ID(v2.44.0.260225…)才是权威,这就是我们用 strings 读取它而不是相信固定版本的原因。
预填充与解码,以及 KV 缓存
生成分为两个阶段。预填充将整个提示通过 transformer 运行一次以构建 KV 缓存(每层的键/值张量):它是计算密集型且高度并行的,因此每个 token 的速度很快。解码随后逐个生成 token,每一步都要对不断增长的 KV 缓存做注意力计算:它受内存带宽限制(每个 token 都要将 4 位权重流经 NSP),这就是解码(Ubuntu 上约 28 tok/s,QLI 上约 32)每个 token 远慢于预填充,并且是真正限制交互延迟的数字的原因。上下文长度
提示加生成的输出必须容纳在 4096 个 token 内。 该上限编译在模型中,而非在运行时选择:QNN 上下文二进制文件及其外围的 tflite 图都以静态形状构建,因此没有任何标志可以提高它,更长的窗口意味着重新编译模型。这些图直接说明了限制:
而且只有一个预填充签名
prefill_128,因此长提示是以 128-token 分块而不是一次性预填充的。在满 4096 时,int8 KV 缓存为 36 MiB,再加上约 1.17 GB 的权重。
Hugging Face 模型卡称 Gemma-4 E2B 架构”最高可支持 32k 上下文长度”。那是架构,不是这个文件:CPU 和 GPU 构建以 2048 发布,而这个
qualcomm_qcs8275 构建以 4096 编译。请从您实际部署的文件中读取限制,就像从中读取 QAIRT 版本一样。Burst 模式:为什么构建在打补丁前只有 16 tok/s
Hexagon NSP 运行在 DCVS(动态时钟和电压缩放)之下:不加干预时它以低时钟空闲,只有在持续负载下才提升。QNN 暴露了一个性能模式来覆盖它。HtpPerformanceMode::kBurst 使运行时投票将 DSP 提升到最高时钟并保持(外加 RPC 轮询以降低 FastRPC 延迟)。LiteRT-LM 的 NPU 执行器确实会请求 burst,但仅在 #if defined(__ANDROID__) 内(补丁 2)。在 Linux 上,该代码块被编译掉后,dispatch 插件报告 Failed to parse qnn options … Null Qualcomm options,回退到 HtpPerformanceMode::kDefault,DSP 以其惰性的默认时钟运行:解码约 16 tok/s。应用补丁 2 后,日志显示 Set HTP performance mode: 2;解码跃升至 25 到 29。这一个门控是整个技术栈中最大的性能杠杆,远大于此处的任何其他因素。
err 1002 权重缓冲区消息
在图初始化期间,您会看到 fastrpc memory map for fd: … length: 1172307968 failed … err 1002。这是 QNN 试图通过 FastRPC 一次性将约 1.17 GB 的持久权重缓冲区映射到 cDSP 的 IOMMU 中。原厂 Ubuntu BSP 没有预留大型 FastRPC DMA 区域(dmesg:no reserved DMA memory for FASTRPC,CMA 仅约 164 MB),因此该单次映射请求被拒绝。它是非致命的:QNN 回退到另一条路径将权重送到 NSP,图仍在 v75 上执行(无论哪种方式模型都能生成正确的文本)。它是否损失了解码吞吐量,很难与上面的 KV 缓存长度衰减区分开;在此 BSP 上,开启 burst 模式且该消息存在时,解码为 25 到 29 tok/s。
为什么解码随回答变长而变慢
解码受内存带宽限制,并且随序列变长每个 token 变慢:每一步都对整个 KV 缓存做注意力计算,而缓存随每个输出的 token 增长。因此 200 token 的回答平均约 28 tok/s,而 800 token 的平均约 25。最开始的 token(短上下文)最快。启动后的首次推理还有一个小的 DCVS 上速过程;将 CPU 调速器固定为performance 并预热可以消除这部分。
故障排除
第 1 部分一句话总结
让运行时与模型匹配(QAIRT 2.44.0.260225,从文件中读取),在固定该版本的提交处构建 LiteRT-LM(带两个 Linux 启用补丁:dispatch 目录加 HTP burst 模式),部署匹配的 QAIRT 主机库和 Hexagon v75 skel,然后以--backend npu 运行。Burst 模式是将 16 tok/s 的构建变成约 28 tok/s 的关键;吓人的 err 1002 是非致命的。
第 2 部分:在 Qualcomm Linux 上投入生产
Ubuntu(第 1 部分)是快速原型的方式。Qualcomm Linux (QLI) 2.0 是您真正会在这些开发板上交付的基于 Yocto 的嵌入式操作系统:一个您自己构建和控制的从源码构建的镜像,内置 Qualcomm AI 技术栈。相同的模型、相同的 QAIRT 2.44、相同的--backend npu,但 QLI 要求两个 Ubuntu 没有的小而具体的代价:一个重新构建补丁(修复 QNN 14001 的 SoC 配置修复)和一行运行时命令(ulimit -l unlimited)。因此流程是:构建镜像、刷写、用 SoC 补丁重新构建二进制文件,然后部署和运行。
与 Ubuntu 的不同之处:
- QLI 原生附带 FastRPC 用户空间、cDSP 固件和 QNN 运行时(没有
apt;它在镜像中)。您不需要运行Pre Requisites。 - rootfs 是 Yocto 镜像而非 Debian,因此您需要将 LiteRT-LM 二进制文件、QAIRT 2.44 和模型部署到其上(scp 或数据分区),而不是
apt install。 - 您需要在 Linux PC 上自己构建操作系统镜像,然后刷写到开发板。
相对于 Ubuntu 构建的变化(QLI 增量)
您不需要为 QLI 从头开始;您可以沿用第 1 部分的工作。模型、QAIRT 2.44、dispatch 到 QNN 到 FastRPC 到 Hexagon 的路径、--backend npu,以及第 1 部分的两个补丁(dispatch 目录加 burst)都保持不变。从可用的 Ubuntu 二进制文件到可用的 QLI 运行,只有两个功能性增量,一个在构建时,一个在运行时,外加打包方式的变化(Yocto 镜像而非 apt):
注意 burst 补丁不是 QLI 特有的:它是任何从源码构建的 Linux 版本都需要的,包括 Ubuntu(它是第 1 部分中 16 到 28 tok/s 的修复)。真正 QLI 特有的增量只是 SoC 守卫重新构建和
ulimit -l 这一行。同一个在 Ubuntu 上约 28 tok/s 的二进制文件,加上这一个额外补丁重新构建后,在 QLI 上约为 32。
构建主机:要求和预期
您不在 IQ8 上构建镜像。 您在 Linux PC(“构建主机”)上构建,然后将结果刷写到开发板。所有内容都通过kas-container 在容器内运行,因此主机的唯一依赖是 Docker。
构建主机前提条件:
机器推荐: 这是多核工作站物有所值的一项工作。Threadripper 或 EPYC(32 到 64 核)约 1 到 2 小时就能完成冷构建;同样的构建在典型的 8 核笔记本上要花一整个下午(约 8 到 10 小时)。更多核心意味着成比例减少的实际耗时。
预计构建时间:
设置构建树
安装 Docker(一次),获取kas-container,并按锁定的修订版拉取 QLI 2.0 层:
构建 IQ-8275 镜像
构建目标是一个用冒号连接的 kas 配置片段列表:机器、镜像、内核、锁文件:prog_firehose_ddr.elf)、分区表、SAIL 引导加载链,以及 rawprogram*.xml/patch*.xml,即 qdl 需要的一切。确认 AI 技术栈在镜像中:
刷写开发板(EDL 加 qdl)
将 IQ8 置于 EDL(emergency download,紧急下载)模式,并使用qdl(Linux/macOS)或 qdl.exe(Windows)刷写。QLI 构建指南有完整的矩阵;简要路径:
root 登录(此镜像上的密码为 oelinux123),然后确认:
qcs8300,尽管 machine 读出 QCS8275(它们是同一个 v75 部件;soc_id 675)。这个命名正是触发我们下一步要修补的 QNN 缺陷的原因。
QLI 还需一个补丁:SoC 配置修复(QnnDevice_create 14001)
第 1 部分的二进制文件在 Ubuntu 上可以运行,但在 QLI 上会在初始化时报 Failed to set up QNN manager 或 QnnDevice_create … 14001 而失败。根本原因:LiteRT 的 QNN 后端在 aarch64 上会强制设置一个由在线检测到的 SoC 构建的 QnnHtpDevice_CustomConfig SOC 选项(htp_backend.cc)。Ubuntu 的 QNN 运行时容忍这种强制覆盖;QLI 的会拒绝。(它甚至不是错误的值:SoC 表将 QCS8275 和 QCS8300 都映射到同一个枚举,v75 和 8 MB VTCM。QLI 只是不接受此路径上的显式 SOC 覆盖。)解决办法是通过编译时排除强制块,让 aarch64 自动检测。它位于 @litert 外部依赖中,因此要在 bazel fetch 之后、bazel build 之前打补丁:
在 QLI 上部署运行时
QLI 免费提供 FastRPC 和 cDSP 固件,但它是没有apt 的 Yocto rootfs。您需要部署镜像不附带的三样东西:litert_lm_main 及其 .so 文件(带 SoC 补丁的重新构建版本)、QAIRT 2.44 SDK 和模型。无需添加 C++ 运行时:二进制文件链接 GNU libstdc++.so.6,rootfs 中已有。开发板有网络,因此可以直接拉取大文件:
scp 到 run/ 中:litert_lm_main、libLiteRtDispatch_Qualcomm.so 和 libGemmaModelConstraintProvider.so。
在 NPU 上运行(以及唯一的运行时坑:ulimit -l)
QLI 的默认 max locked memory 为 8 MB(ulimit -l 返回 8192);Ubuntu 的 root 是无限制的。FastRPC 会固定模型约 1.17 GB 的持久权重缓冲区,这远超 8 MB,因此 QNN 报告 Could not allocate persistent weights buffer! 且加载中止(首次冷启动尝试实际上导致了 cDSP 故障)。运行前提高它,err 1002 权重映射就会退化为您在 Ubuntu 上看到的完全相同的无害回退:
HtpPerformanceMode: 2、没有 14001、正确的生成文本,长时间运行时:
原型与生产的对比:回报
同一个模型、同一个 NPU、同一个 QAIRT,在同一块物理开发板上测量,先是 Ubuntu 原型,然后重新刷写为 QLI 2.0(调速器performance,已预热):
原型到生产之路的点睛之笔:QLI 2.0 不是降级,它更快也更稳定。 与 Ubuntu 不同,它即使在 800-token 的生成中也能保持约 32 tok/s,而不是下滑到约 25。可能的原因正是它作为生产目标的特点:精简的单一用途镜像使 CPU 和调度器的竞争少得多,因此 NSP 的 DCVS 能保持其时钟。您放弃了
apt 的便利,付出了两个 QLI 特有的代价(一个 SoC 配置的重新构建补丁和一行运行时命令 ulimit -l),换来的是一个可复现、版本锁定、从源码构建的镜像,它运行该模型比您做原型的机器更好。
后续步骤
- 换成您自己的提示,或将
litert_lm_main包装在一个小型本地 API 之后,构建设备端助手。 - 尝试为 v75 NSP 构建的其他 LiteRT-LM 模型,部署前先从文件中读取每个模型所需的 QAIRT 版本。
- 对于生产路径,从一开始就纳入 SoC 配置补丁,并将您的运行目录烘焙进 Yocto 镜像。
- 通过对其他 Dragonwing 器件的 NSP 重复版本匹配步骤,比较不同器件上的吞吐量。

