设备: 高通跃龙IQ-9100 (IQ-9075, SA8775P同样适用, Hexagon v73, 双 CDSP)
模型: Qwen2.5-7B-Instruct (w4a16 混合精度量化,28 层 Transformer)
SDK: QAIRT 2.42.0.251225
日期: 2026-07-19
概述
QAIRT SDK 提供两条编译路径:CLI 工具(qnn-context-binary-generator)和 Python API(qairt.compile())。对于 LLM 部署中的多图 context binary 编译,这两条路径的行为存在关键差异——CLI 无法正确执行 4-bit 权重打包(weights_packing),导致产物体积膨胀 2 倍。
本文记录这一差异的发现过程、根因分析、以及如何通过 Python API 实现与云编译等价的本地编译。
一、问题:本地编译产物是云编译的 2 倍
在将 Qwen2.5-7B(w4a16 量化)本地编译为 HTP context binary 时,产物体积约 8.4 GB——而通过 Qualcomm AI Hub 云编译的同一模型仅 4.7 GB。
逐层对比:
| 指标 | 云编译 (6-split) | 本地编译 (11-split) |
|---|---|---|
| 每层 Transformer 大小 | 112.75 MB | 224.2 MB |
| 总量 | 4720.7 MB | ~8358.8 MB |
| 比率 | 1.0× | ~2.0× |
224.2 ÷ 112.75 ≈ 1.99。对于 w4a16 量化模型,精确 2 倍差异只有一个合理解释:4-bit 权重没有被打包——每个 4-bit 值占了一整个 byte。
二、QAIRT 工具链中的 4-bit 权重打包
2.1qairt-converter的--pack_4_bit_weights参数
QAIRT SDK 的模型转换器qairt-converter有一个参数:
--pack_4_bit_weights: Store 4-bit quantized weights in packed format in a single byte i.e. two 4-bit quantized tensors can be stored in one byte默认值为False。启用后,DLC(中间格式)体积减半:
| DLC 编译模式 | Part 2 大小 | 缩减 |
|---|---|---|
| 默认(不打包) | 676.4 MB | — |
--pack_4_bit_weights | 343.0 MB | -49.3% |
但 DLC 只是中间格式。最终部署到设备上的是 context binary,由qnn-context-binary-generator从 DLC 编译而来。
2.2qnn-context-binary-generator的编译模式差异
这个 CLI 工具在单图模式和多图模式下的 weight packing 行为完全不同:
| 编译模式 | 输入 DLC | context binary 大小 | 是否打包 4-bit |
|---|---|---|---|
| 单图 | 未打包 676.4 MB | 337 MB | ✅ 内部自动打包 |
| 单图 | 已打包 343.0 MB | 编译失败(Error 1002) | N/A |
| 多图(prompt+token, weight sharing) | 未打包 DLCs | 672.6 MB | ❌ 不打包 |
LLM 部署必须使用多图模式(prompt 图和 token 图共享权重),而在这个模式下 CLI 工具不执行 4-bit 权重打包。
更值得注意的是:如果输入已经打包好的 DLC,单图模式反而会报错(nullptr for graphsInput)。CLI 工具只能接受未打包的 DLC,然后在单图模式下自行打包,或在多图模式下跳过打包。
2.3 跨平台验证
为排除平台差异,在 Windows 和 Linux(WSL2 Ubuntu 24.04)上分别执行多图编译:
| 平台 | 多图 Part 2 大小 |
|---|---|
Windows (QnnHtp.dll) | 672.6 MB |
Linux (libQnnHtp.so) | 673 MB |
结果一致。多图模式不打包 4-bit 权重是qnn-context-binary-generator的固有行为,与操作系统无关。
三、CLI 配置文件的静默忽略问题
3.1 HTP 后端扩展 schema
QNN HTP 后端定义了一套 JSON 格式的扩展配置(htp_backend_ext_config.json),包含graphs、context、devices等部分。其中graphs下的weights_packing键用于控制是否打包 4-bit 权重:
{"graphs":[{"graph_names":["prompt_ar128_cl4096_2_of_11","token_ar1_cl4096_2_of_11"],"weights_packing":true}],"context":{"weight_sharing_enabled":true}}3.2 CLI parser 的实际行为
将上述配置通过--config_file传给qnn-context-binary-generator,得到:
[ ERROR ] Unknown Key = graphs/0/weights_packing passed in config [ ERROR ] Unknown Key = graphs/0/O passed in config [ ERROR ] Unknown Key = context/weight_sharing_enabled passed in configCLI 工具的 JSON parser没有实现完整的 HTP 后端扩展 schema。graphs、context、devices中的扩展配置键被识别为 “Unknown Key” 后静默忽略——工具继续以默认值运行,不会中止。
这意味着:
- 用户在配置文件中设置
weights_packing: true→ 被忽略 - 用户在配置文件中设置
weight_sharing_enabled: true→ 被忽略 - 用户在配置文件中设置
optimization_type(O) → 被忽略 - 工具输出 672.6 MB 的未打包 context binary,看起来"编译成功"
没有任何指示告诉用户这些配置项没有生效。“Unknown Key” 的 ERROR 级日志容易被淹没在其他输出中,且工具正常退出(exit code 0)。
四、Python API:直接调用 QNN C API
QAIRT SDK 同时提供 Python API(qairt包),其底层通过NativeExecutor直接调用 QNN C API,不经过 CLI 的 JSON parser。
4.1 关键代码
importqairtfromqairt.api.compiler.configimportCompileConfigfromqairt.api.common.backends.htp.configimportHtpGraphConfig,HtpContextConfig# 转换 ONNX → DLC(内存中的中间表示)prompt_model=qairt.convert(prompt_onnx,encodings=prompt_enc,float_bitwidth=16)token_model=qairt.convert(token_onnx,encodings=token_enc,float_bitwidth=16)# 编译 DLC → context binaryconfig=CompileConfig(backend="HTP",graph_custom_configs=[HtpGraphConfig(name=prompt_name,optimization_type=3,weights_packing=True),HtpGraphConfig(name=token_name,optimization_type=3,weights_packing=True),],context_custom_configs=[HtpContextConfig(weight_sharing_enabled=True)],)result=qairt.compile([prompt_model,token_model],config=config)result.save(output_bin)4.2 配置参数说明
| 参数 | 作用 | CLI 是否支持 |
|---|---|---|
weights_packing=True | 将 2 个 4-bit 权重打包到 1 byte | ❌ 静默忽略 |
optimization_type=3 | 最高优化级别 | ❌ 静默忽略 |
weight_sharing_enabled=True | prompt/token 图共享权重 | ❌ 静默忽略 |
float_bitwidth=16 | 浮点回退使用 fp16 | ✅ 支持 |
Python API 中这些参数通过HtpGraphConfig、HtpContextConfig等 Python 对象传递,在底层被正确映射到 QNN C API 的对应接口。
五、A/B 测试对比
5.1 单 Part 对比(Part 2 of 11)
| 编译方法 | Part 2 大小 | vs 云编译 |
|---|---|---|
| CLI(默认,不打包) | 672.6 MB | 2× |
CLI +weights_packing: trueJSON 配置 | 672.6 MB | 2×(被忽略) |
Python API +weights_packing=True | 337.3 MB | ≈1× |
| 云编译参考值 | ~340 MB | 1× |
5.2 全量编译对比
Python API 编译全部 10 个 Transformer parts + LUT 嵌入(11-split 方案):
| Part | 内容 | Python API 打包 | CLI 未打包 | 缩减 |
|---|---|---|---|---|
| 1 (LUT) | 嵌入层 | 1039.5 MB | 1039.5 MB | 0% |
| 2–10 | Transformer 层(每份 3 层) | 337.3 MB × 9 | 672.6 MB × 9 | -49.9% |
| 11 | 1 层 + LM Head | 635.1 MB | 1266.8 MB | -49.9% |
| 总计 | 4710.4 MB | ~8358 MB | -43.6% |
本地 Python API 编译 4710.4 MB vs 云编译 4720.7 MB,差异仅 0.2%。
六、与云编译参数的贡献度对比
AI Hub 云编译管线中有两个本地不可用的参数:
# ai-hub-models 源码中硬编码other_compile_options+=" --quantize_full_type w8a16 --quantize_io"--quantize_full_type w8a16:将未量化的嵌入层和 LM Head 从 fp16 量化为 w8a16--quantize_io:量化模型的输入/输出张量
这些参数的实际贡献:
| 因素 | 贡献比例 | 说明 |
|---|---|---|
weights_packing | ~98-99% | 2 个 4-bit 值打包到 1 byte |
--quantize_full_type w8a16 | ~1-2% | 仅影响嵌入层和 LM Head |
--quantize_io | <1% | 仅影响 I/O 张量 |
体积差异的主导因素是weights_packing,而这个功能通过 QAIRT Python API 在本地完全可用。
云编译独有的量化参数对最终二进制大小的贡献不到 2%——4710 MB vs 4720 MB 的 10 MB 差异即来源于此。
七、技术原因分析
7.1 为什么 CLI 的多图模式不打包
qnn-context-binary-generator的单图模式和多图模式在内部使用不同的编译路径:
- 单图模式:直接调用 HTP 编译器,编译器内部对 4-bit 权重执行打包
- 多图模式(
--weight_sharing_enabled):需要在多个图之间协调权重共享,使用了不同的内部流程。这个流程中,4-bit 权重打包步骤被跳过了
这是 CLI 工具在多图编译路径中的实现缺陷,不是 QNN HTP 编译器本身的限制——因为 Python API 调用的是同一个底层编译器,且能在多图模式下正确执行打包。
7.2 为什么 CLI 的 JSON parser 不识别扩展键
CLI 工具的--config_fileJSON parser 实现了 HTP 后端配置 schema 的一个子集。以下类别的键被支持:
- 顶层设备配置(
soc_model、dsp_arch、cores) - 内存配置(
mem_type) - 性能配置(
perf_profile、rpc_control_latency)
以下类别的键不被支持(报 “Unknown Key”):
graphs下的weights_packing、optimization_type(O)context下的weight_sharing_enabled
Python API 的NativeExecutor则在 Windows 上通过QnnHtp.dll(Linux 上通过libQnnHtp.so)直接调用 QNN C API,所有 HTP 后端扩展参数都被正确传递。
八、本地编译完整流程
以下是使用 Python API 编译 LLM context binary 的关键步骤:
8.1 环境要求
| 项目 | 要求 |
|---|---|
| Python | 3.10(QAIRT SDK 的.pyd原生绑定要求) |
| QAIRT SDK | 2.42+(路径示例:C:\Qualcomm\AIStack\QAIRT\2.42.0.251225) |
| 环境变量 | QNN_SDK_ROOT指向 SDK 根目录 |
| Python path | $QNN_SDK_ROOT/lib/python加入sys.path |
8.2 编译脚本核心逻辑
importsys,os QAIRT_ROOT=os.environ["QNN_SDK_ROOT"]sys.path.insert(0,os.path.join(QAIRT_ROOT,"lib","python"))importqairtfromqairt.api.compiler.configimportCompileConfigfromqairt.api.common.backends.htp.configimport(HtpGraphConfig,HtpContextConfig,HtpDeviceConfig,HtpDeviceCoreConfig,PerfProfile,)NUM_SPLITS=6forpart_idxinrange(1,NUM_SPLITS+1):prompt_name=f"prompt_ar128_cl4096_{part_idx}_of_{NUM_SPLITS}"token_name=f"token_ar1_cl4096_{part_idx}_of_{NUM_SPLITS}"# 1. 转换 ONNX → DLCprompt_model=qairt.convert(prompt_onnx,encodings=prompt_enc,float_bitwidth=16)token_model=qairt.convert(token_onnx,encodings=token_enc,float_bitwidth=16)# 2. 编译 → context binaryconfig=CompileConfig(backend="HTP",graph_custom_configs=[HtpGraphConfig(name=prompt_name,optimization_type=3,weights_packing=True,vtcm_size_in_mb=8),HtpGraphConfig(name=token_name,optimization_type=3,weights_packing=True,vtcm_size_in_mb=8),],context_custom_configs=[HtpContextConfig(weight_sharing_enabled=True)],device_custom_configs=[HtpDeviceConfig(soc_model=77,dsp_arch="v73",cores=[HtpDeviceCoreConfig(perf_profile=PerfProfile.BURST,rpc_control_latency=100)],)],)result=qairt.compile([prompt_model,token_model],config=config)result.save(f"qwen2_5_7b_instruct_part_{part_idx}_of_{NUM_SPLITS}.bin")8.3 编译耗时
6-split 全量编译在 Windows x86_64 上约16 分钟完成(含 ONNX→DLC 转换 + DLC→context binary 编译)。
九、影响评估
9.1 消除云编译依赖
在发现 Python API 的weights_packing之前,获取紧凑 context binary 的唯一途径是 Qualcomm AI Hub 云编译。这意味着:
- 需要 AI Hub 账号和 API token
- 需要上传数 GB 的 ONNX 模型到云端(在网络条件差的环境下,200 KB/s 上传速度意味着 17+ 小时)
- 需要等待云端排队和编译
- 编译配置受限于云 API 提供的选项
使用 Python API 后,整个编译流程在本地 Windows 机器上 16 分钟完成,不需要网络连接、不需要账号、不需要等待。
9.2 对不同 QAIRT 版本的适用性
weights_packing参数在 QAIRT 2.40+ 的 Python API 中可用。CLI 的 parser 限制在测试过的所有版本(2.35、2.40、2.42)中均存在。
十、CLI 与 Python API 对比总结
| 对比维度 | CLI (qnn-context-binary-generator) | Python API (qairt.compile()) |
|---|---|---|
| 4-bit weights_packing(多图模式) | ❌ 不执行 | ✅ 正确执行 |
| weight_sharing_enabled | ❌ JSON parser 忽略 | ✅ 正确传递 |
| optimization_type | ❌ JSON parser 忽略 | ✅ 正确传递 |
| soc_model / dsp_arch | ✅ 支持 | ✅ 支持 |
| 底层调用方式 | 内部实现 | 直接调用 QNN C API (QnnHtp.dll/libQnnHtp.so) |
| 多图 context binary 大小 | ~672 MB/part(膨胀) | ~337 MB/part(紧凑) |
| 总模型大小(7B, 6-split) | ~8.4 GB | ~4.7 GB |
| 与云编译大小差异 | 2× | 0.2% |
| 编译时间 | 类似 | ~16 分钟 |
结论
QAIRT SDK 的 CLI 工具
qnn-context-binary-generator在多图编译模式下不执行 4-bit 权重打包。这不是配置问题——即使在 JSON 配置文件中显式设置weights_packing: true,也会被 parser 忽略。CLI 的 JSON parser 只实现了 HTP 后端扩展 schema 的一个子集。
weights_packing、weight_sharing_enabled、optimization_type等关键配置项被报为 “Unknown Key” 后静默跳过,工具以默认值继续执行并正常退出。QAIRT Python API 是多图 context binary 编译的正确路径。它通过
NativeExecutor直接调用 QNN C API,所有 HTP 后端扩展参数都被正确执行。产出的 context binary 与云编译在大小和推理质量上等价。本地编译可以完全替代 AI Hub 云编译。Python API +
weights_packing=True+soc_model=77产出的 context binary 总量 4710 MB,与云编译的 4720 MB 仅差 0.2%(差异来自云端额外的--quantize_full_type w8a16和--quantize_io,贡献不到 2%)。对于 QAIRT SDK 的 LLM 部署流程,建议统一使用 Python API 进行编译,避免 CLI 工具的多图模式限制和 parser 不完整问题。