ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

高通跃龙IQ-9100工业平台的开发经验分享(3): weights-packing-CLI与PythonAPI对比

高通跃龙IQ-9100工业平台的开发经验分享(3): weights-packing-CLI与PythonAPI对比

设备: 高通跃龙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 MB224.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_weights343.0 MB-49.3%

但 DLC 只是中间格式。最终部署到设备上的是 context binary,由qnn-context-binary-generator从 DLC 编译而来。

2.2qnn-context-binary-generator的编译模式差异

这个 CLI 工具在单图模式和多图模式下的 weight packing 行为完全不同

编译模式输入 DLCcontext binary 大小是否打包 4-bit
单图未打包 676.4 MB337 MB✅ 内部自动打包
单图已打包 343.0 MB编译失败(Error 1002)N/A
多图(prompt+token, weight sharing)未打包 DLCs672.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),包含graphscontextdevices等部分。其中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 config

CLI 工具的 JSON parser没有实现完整的 HTP 后端扩展 schemagraphscontextdevices中的扩展配置键被识别为 “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=Trueprompt/token 图共享权重❌ 静默忽略
float_bitwidth=16浮点回退使用 fp16✅ 支持

Python API 中这些参数通过HtpGraphConfigHtpContextConfig等 Python 对象传递,在底层被正确映射到 QNN C API 的对应接口。

五、A/B 测试对比

5.1 单 Part 对比(Part 2 of 11)

编译方法Part 2 大小vs 云编译
CLI(默认,不打包)672.6 MB
CLI +weights_packing: trueJSON 配置672.6 MB2×(被忽略)
Python API +weights_packing=True337.3 MB≈1×
云编译参考值~340 MB

5.2 全量编译对比

Python API 编译全部 10 个 Transformer parts + LUT 嵌入(11-split 方案):

Part内容Python API 打包CLI 未打包缩减
1 (LUT)嵌入层1039.5 MB1039.5 MB0%
2–10Transformer 层(每份 3 层)337.3 MB × 9672.6 MB × 9-49.9%
111 层 + LM Head635.1 MB1266.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_modeldsp_archcores
  • 内存配置(mem_type
  • 性能配置(perf_profilerpc_control_latency

以下类别的键不被支持(报 “Unknown Key”):

  • graphs下的weights_packingoptimization_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 环境要求

项目要求
Python3.10(QAIRT SDK 的.pyd原生绑定要求)
QAIRT SDK2.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
与云编译大小差异0.2%
编译时间类似~16 分钟

结论

  1. QAIRT SDK 的 CLI 工具qnn-context-binary-generator在多图编译模式下不执行 4-bit 权重打包。这不是配置问题——即使在 JSON 配置文件中显式设置weights_packing: true,也会被 parser 忽略。

  2. CLI 的 JSON parser 只实现了 HTP 后端扩展 schema 的一个子集weights_packingweight_sharing_enabledoptimization_type等关键配置项被报为 “Unknown Key” 后静默跳过,工具以默认值继续执行并正常退出。

  3. QAIRT Python API 是多图 context binary 编译的正确路径。它通过NativeExecutor直接调用 QNN C API,所有 HTP 后端扩展参数都被正确执行。产出的 context binary 与云编译在大小和推理质量上等价。

  4. 本地编译可以完全替代 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%)。

  5. 对于 QAIRT SDK 的 LLM 部署流程,建议统一使用 Python API 进行编译,避免 CLI 工具的多图模式限制和 parser 不完整问题。

返回列表