Skip to main content
AI / ML
Rami Mouro·2026年6月29日·← 所有文章

本指南在 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,即提示加生成输出,编译进模型且运行时无法提高。参见上下文长度
总耗时约为 15 分钟的设备设置,外加一次性的源码构建(在 IQ8 上构建约 45 分钟,在性能更强的 aarch64 机器上更快)。
如何阅读本指南。 每条命令都以 ubuntu 用户身份在 IQ-8275 上运行。所有工作都位于一个目录 ~/iq8-gemma 中,并且每个代码块都以自己的 cd 开头,因此你可以在任意新终端中按顺序粘贴任何代码块,而无需记住当前处于哪个目录。粘贴前无需编辑任何内容。

你将做什么

  1. 设置 IQ8 设备并确认 FastRPC 存在。
  2. 下载公开的 NPU 模型,并读取它所需的确切 QAIRT 版本。
  3. 针对该 QAIRT 从 LiteRT-LM 构建 litert_lm_main,并应用两个 Linux 启用补丁。
  4. 组装运行目录并在 NPU 上运行模型。
  5. (可选)从 Ubuntu 原型迁移到 Qualcomm Linux 生产镜像。

前提条件

在下面的任何步骤生效之前,开发板需要启用其 Qualcomm 外设并安装 AI 运行时:FastRPC 用户态libcdsprpc.so)、QNN 库(libqnn-devqnn-toolssnpe-toolstensorflow-lite-qcom-apps)、qcom-libdmabufheap 以及 GStreamer QCOM 插件,外加固件更新和重启。 请先按照 IQ8 设备页面完成设置,然后再回到这里: 重启后,重新连接到开发板并确认 FastRPC 存在:
如果 /dev/fastrpc-* 不存在,说明内核缺少 FastRPC 支持。就此打住:这是 BSP 或镜像问题,无法在用户态修复。

获取模型并找到它所需的 QAIRT 版本

不要猜测版本。创建工作目录并将公开的 NPU 模型下载到其中(3.29 GB;-C - 可在连接中断后续传):
Qualcomm 的 .litertlm 嵌入了 QNN 上下文二进制文件,这些文件与其编译时所用的 QAIRT 版本严格锁定,设备上的运行时必须匹配。版本信息并未明示,而 LiteRT 的源码固定的是一个更新的版本,因此直接从文件中读取:
该模型需要 QAIRT 2.44.0.260225。下载这个确切的 SDK 并解压:

针对该 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 也无法解析:
补丁 2:HTP burst 模式。 这是每秒 16 个和约 28 个 token 之间的差异。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_main
  • libLiteRtDispatch_Qualcomm.so(位于 .../qualcomm/dispatch/ 目录树下)
  • libLiteRt.so(LiteRT 核心运行时库)

组装运行目录

将二进制文件、dispatch 插件、LiteRT 核心库、约束提供器和模型收集到 ~/iq8-gemma/runfind 调用会在 Bazel 放置构建输出的任何位置找到它们,所以这个代码块可以原样运行:
运行目录现在包含:
验证插件的共享库依赖都能解析。clang-18 构建链接的是 GNU 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 后端即可证明这一点:
既然它拒绝 CPU,却在 --backend npu 下仍能生成正确文本,执行就发生在 Hexagon NSP 上(HTP burst 模式,HtpPerformanceMode: 2,在日志中可见)。

性能

在 IQ-8275 上测量,公开模型,NPU 后端,两个 Linux 补丁均已应用,热机状态,CPU 调频器固定为 performance 说明:
  • burst 模式至关重要。 不应用补丁 2 时,从源码构建的版本解码约 16 tok/s;应用后为 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_0qnn_partition_1(transformer 被拆分到两个 HTP 上下文中)。权重是 w4a16(4 位权重,16 位激活):这就是一个约 20 亿参数的模型能装下并在 NSP 上快速运行的原因。 该容器还携带本纯文本指南从未涉及的计算图:一个视觉编码器及其适配器(签名 vision_280vision_adapter_280,将 2,520 个图像块转为 280 个软 token)和一个音频编码器及其适配器(audio_adapter,输入 816 个 mel 帧,输出 204 个软 token),外加图像结束和音频结束嵌入(eoieoa)。要使用它们,需要一个能组装交错的 token 和软 token 序列的多模态前端;litert_lm_main --input_prompt 只向模型传递文本,因此在下面的每次运行中,视觉和音频塔都处于闲置状态。

执行路径,逐层拆解

LiteRT 计算图不是普通的 tflite 网络:它主要是一个 DISPATCH_OP,一个作为”运行这个预编译厂商计算图”占位符的自定义算子。当 NPU 加速器注册时,LiteRT 加载 libLiteRtDispatch_Qualcomm.so,后者将 QNN 上下文二进制文件交给 QNN HTP 后端。QNN 通过 FastRPC(经由 libcdsprpc.so/dev/fastrpc-cdsp 到 DSP 的远程过程调用传输)与 Hexagon NSP 通信;实际的矩阵乘法在 libQnnHtpV75Skel.so 内执行,这是加载 v75 NSP 上的 QNN “骨架”库,通过 ADSP_LIBRARY_PATH 找到。因此三样东西必须一致:主机 QNN 库、DSP skel 和模型内的上下文二进制文件,全部使用同一 QAIRT 版本。这就是版本步骤至关重要的原因。

为什么版本必须精确匹配

QNN 上下文二进制文件是针对一个 QAIRT 版本提前编译并序列化的:其计算图格式、算子包集合以及它期望的 skel ABI 都被固化其中。在不同的运行时上加载它,最好的情况也是反序列化被拒绝。版本信息没有事先文档说明,且 LiteRT 的 main 固定的是更新的 2.47,因此两者都不权威。文件内序列化的构建 ID(v2.44.0.260225…)才是权威,这就是我们用 strings 读取而不是相信固定版本的原因。

预填充与解码,以及 KV 缓存

生成分为两个阶段。**预填充(Prefill)**将整个提示一次性通过 transformer,以构建 KV 缓存(每层的键/值张量):它是计算密集型且高度并行的,因此按 token 计非常快。**解码(Decode)**随后逐个生成 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 版本一样。
结合下面描述的解码速度下降,接近限制运行的会话会比约 28 tok/s 的标称值明显更慢,因为每个解码步骤都要在更长的缓存上做注意力计算。

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 区域(dmesgno 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 并预热可以消除这部分。

故障排查

第一部分总结一句话

让运行时与模型匹配(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 是非致命的。

第二部分:Qualcomm Linux 上的生产部署

Ubuntu(第一部分)是快速原型验证的途径。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 从头开始;而是沿用第一部分的成果。模型、QAIRT 2.44、dispatch 到 QNN 到 FastRPC 到 Hexagon 的路径、--backend npu,以及第一部分的两个补丁(dispatch 目录加 burst)全部不变。从可用的 Ubuntu 二进制文件到可用的 QLI 运行,只有两个功能性增量——一个在构建时,一个在运行时——外加打包方式的变化(Yocto 镜像而非 apt): 注意 burst 补丁不是 QLI 特有的:它是任何从源码构建的 Linux 版本都需要的,包括 Ubuntu(就是第一部分中 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 小时)。核心越多,耗时按比例越少。 预估构建时间:
约 7 分钟的热构建数字是在一台 16 核 AMD EPYC 7763 上、已有 sstate-cachedownloads 的情况下实测的。冷构建数字是估计值:变量是核心数和下载速度,其他影响不大。请在构建之间保留 sstate-cachedownloads 目录(将 SSTATE_DIRDL_DIR 指向它们);这就是 7 分钟与 4 小时的差别。

设置构建树

安装 Docker(一次性),获取 kas-container,并以锁定的修订版拉取 QLI 2.0 各层:

构建 IQ-8275 镜像

构建目标是一个用冒号连接的 kas 配置片段列表:机器、镜像、内核、锁文件:
这会生成可刷写的包(约 927 MB):
其中包含:firehose 编程器(prog_firehose_ddr.elf)、分区表、SAIL 引导加载链,以及 rawprogram*.xml/patch*.xml——qdl 需要的一切。确认 AI 软件栈已包含在镜像中:
注意镜像附带的是 QAIRT 2.43;我们的模型需要 2.44,因此(与 Ubuntu 上一样)我们在下面自行部署 2.44。

刷写开发板(EDL 加 qdl)

将 IQ8 置于 EDL(紧急下载)模式,并用 qdl(Linux/macOS)或 qdl.exe(Windows)刷写。QLI 构建指南有完整的矩阵;简要路径:
断电退出 EDL;开发板启动 QLI 2.0。以 root 登录(此镜像的密码为 oelinux123),然后确认:
注意,尽管 machine 显示为 QCS8275设备树称该 SoC 为 qcs8300(它们是同一颗 v75 芯片;soc_id 675)。正是这个命名触发了我们接下来要修补的 QNN 缺陷。

QLI 还需一个补丁:SoC 配置修复(QnnDevice_create 14001)

第一部分的二进制文件在 Ubuntu 上能运行,但在 QLI 上初始化时报错 Failed to set up QNN managerQnnDevice_create … 14001。根本原因:LiteRT 的 QNN 后端在 aarch64 上会强制设置一个由在线检测到的 SoC 构建的 QnnHtpDevice_CustomConfig SOC 选项(htp_backend.cc)。Ubuntu 的 QNN 运行时容忍这种强制覆盖;QLI 的会拒绝。(甚至不是值错了:SoC 表将 QCS8275QCS8300 映射到同一个枚举值,v75,8 MB VTCM。QLI 只是不接受在此路径上的显式 SOC 覆盖。)解决办法是编译掉这个强制代码块,让 aarch64 自动检测。它位于 @litert external 中,因此要在 bazel fetch 之后bazel build 之前打补丁:
这个二进制文件是一个超集:去掉强制 SOC 配置在 Ubuntu 上没有影响,所以同一个二进制文件可以在两个操作系统上运行。(如果你只面向 QLI,可以从一开始就带着全部三个补丁构建。)

在 QLI 上部署运行时

QLI 免费提供 FastRPC 和 cDSP 固件,但它是没有 apt 的 Yocto rootfs。你需要部署镜像中没有的三样东西:litert_lm_main 及其 .so 文件(应用了 SoC 补丁的重新构建版)、QAIRT 2.44 SDK 和模型。无需添加 C++ 运行时:二进制文件链接 GNU libstdc++.so.6,rootfs 中已有。开发板有网络,可以直接拉取大文件:
然后将三个重新构建的产物 scprun/ 中:litert_lm_mainlibLiteRtDispatch_Qualcomm.solibGemmaModelConstraintProvider.so

在 NPU 上运行(外加唯一的运行时注意事项:ulimit -l

QLI 默认的 max locked memory 是 8 MBulimit -l 返回 8192);Ubuntu 的 root 是无限制的。FastRPC 要**固定(pin)**模型约 1.17 GB 的常驻权重缓冲区,远超 8 MB,因此 QNN 报告 Could not allocate persistent weights buffer! 并中止加载(第一次冷启动尝试甚至导致 cDSP 故障)。运行前先提高限制err 1002 权重映射消息就会退化为你在 Ubuntu 上看到的完全相同的无害回退:
调试期间的崩溃安全。 QLI 附带 qcom_scm.download_mode=1,因此 cDSP 或内核故障会将 SoC 置入 EDL ramdump 模式(USB 900e),需要物理断电重启才能恢复。运行 echo 0 > /sys/module/qcom_scm/parameters/download_mode,使故障时直接重启。(提高 ulimit -l 后就不会有故障;这只是一道保险。)
你会看到 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 重复版本匹配步骤,比较不同设备上的吞吐量。