📌 上一篇回顾
在上篇中,我们发现本地编译的 7B 模型二进制体积(8.4 GB)是云编译(4.7 GB)的两倍,根本原因是多图编译模式下 CLI 工具无法启用 weights_packing,且 JSON 配置被静默忽略。
本篇 将展示如何通过 QAIRT Python API 突破这一限制,将二进制压缩到 4.7 GB。但压缩后仍面临 4 GB IOVA 上限的质疑——为什么 4.7 GB 模型能成功运行?答案藏在三个运行时Runtime机制中。

【接上一篇

1. 突破口:QAIRT Python API

在 CLI 工具走不通的情况下,转向 QAIRT SDK 提供的 Python API。

关键发现:Python API 的 NativeExecutor 直接调用 QNN C API,绕过了 CLI 的 JSON parser。 所有在 CLI 上被静默忽略的配置项,Python API 都能正确传递。

核心代码:

import qairt
from qairt.api.compiler.config import CompileConfig
from qairt.api.common.backends.http.config import HtpGraphConfig, HtpContextConfig

# 1. 转换 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)

# 2. 编译 DLC → context binary,启用 weights_packing
config = 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)

这里有几个要点:

  1. weights_packing=True 是关键——直接通过 Python 对象传给 QNN C API
  2. optimization_type=3 对应最高优化级别(在 CLI 中通过 --config_file 的 0 键设置,但同样被静默忽略)
  3. weight_sharing_enabled=True 使 prompt 和 token 图共享权重,避免重复映射
  4. float_bitwidth=16 确保浮点回退使用 fp16 而非 fp32

A/B 测试:显示模型 part 2 中的那一层是smmu 的限制最大,导致失败的关键, 其中Part 2的测试结果如下:

方法 Part 2 大小 缩减
CLI qnn-context-binary-generator(默认) 672.6 MB
CLI + weights_packing 672.6 MB 0%(被忽略)
Python API + weights_packing=True 337.3 MB 49.9%

2. 11-split 的尝试

首先用 11-split 打包模型(4.7 GB 总量)部署测试。解决了图名称排序问题(10/11 → T0/T1)后,Genie 能正确加载前 7 个 context,但 context 8、9、10 映射失败:

fastrpc memory map for fd: 117 with length: 352321536 failed with error: 0x1
Failed to map shared weights for contextId 8

模型能运行但输出乱码——后 3 个 Transformer 层和 LMHead 的权重缺失了。

11 份太多了。

3. 6-split 的逆转

关键转折来自云编译的 6-split 模型——同样 4.7 GB,但成功部署了。

测试结果:

测试 Prompt 结果
英文数学 “What is 2+3” “2 + 3 equals 5.” ✓
中文解释 “请用中文解释什么是量子计算” 正确、连贯的中文回答 ✓

推理输出完全正确。中英文均验证通过。

4. 为什么 4.7 GB 能在 4 GB 限制下工作?

答案在三个运行时机制中:

4.1. use-mmap: true —— 按需分页加载

"QnnHtp": {
    "use-mmap": true,
    "mmap-budget": 0
}

权重通过 memory-mapped file 加载,操作系统使用 on-demand page faulting——只有当前推理实际访问的页面才会被映射。4.7 GB 的权重不需要同时全部驻留在 IOVA 空间里。

4.2. weight_sharing_enabled: true —— 权重共享

"context": {
    "weight_sharing_enabled": true
}

每个 part 包含两个图(prompt + token),但它们共享同一份权重。没有权重共享的话,实际映射量翻倍;有了它,每份权重只需映射一次。

4.3. 非致命的映射失败 —— 运行时优雅降级

内核日志(dmesg)中仍然存在 failed to map buffer 警告:

qcom,fastrpc-cb ...: compute-cb@2: failed to map buffer, fd = 25
qcom,fastrpc-cb ...: compute-cb@3: failed to map buffer, fd = 35

但这些是非致命的。Genie 运行时优雅地处理了这些失败——可能是辅助缓冲区(profiling、secondary cache、或冗余映射)映射失败,但不影响核心推理路径。

应用层分配的显式缓冲区只有 155 MB(12 个 buffer),远低于 4 GB 限制:

[INFO] "Allocated total size = 162038272 across 12 buffers"

4.4 与 4B 模型的对比

指标 Qwen3-4B (w4a16) Qwen2.5-7B (混合精度)
总模型大小 3.0 GB (4 parts) 4.7 GB (6 parts)
显式分配缓冲区 328 MB / 8 buffers 155 MB / 12 buffers
内核映射失败 3-4 个非致命 4-5 个非致命
推理质量 正确 ✓ 正确 ✓

7B 模型的显式缓冲区分配(155 MB)反而比 4B 模型(328 MB)还少——说明 Genie 运行时对 7B 模型做了更积极的内存优化。

小结

本篇中,我们通过 Python API 成功启用了 weights_packing,将二进制从 8.4 GB 压缩到 4.7 GB,并发现 6-split 可以成功部署而 11-split 失败。更重要的是,我们揭示了三个运行时机制(use-mmapweight_sharing、非致命映射失败)如何让 4.7 GB 模型在 4 GB IOVA 限制下正常工作。

下一篇预告
部署过程中我们还遇到了图名称排序导致的张量找不到问题,以及soc_model 配置错误引发的乱码输出。我们将在下一篇中解决这些细节,并给出完整的本地编译流程(约 16 分钟)、三层方案总结、假设验证,以及最重要的经验:理论内存限制 ≠ 实际限制。
最终,我们将回答那个核心问题——这是软件限制,不是硬件限制。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐