设备: 高通跃龙IQ-9100 EVK (SA8775P, Hexagon v73, 双 CDSP)
模型: Qwen2.5-7B-Instruct (w4a16 混合精度量化,28 层 Transformer)
SDK: QAIRT 2.42 (本地编译) + Genie 1.14.0 推理运行时
日期: 2026-07-23
一个缺失的编译参数导致了数周的调试——排除了 9 个错误假设之后,才找到真正的根因。
背景
我们正在把 Qwen2.5-7B-Instruct 大语言模型部署到高通 QCS9100 边缘设备上,用 Hexagon DSP (HTP) 加速推理。模型已经通过 Qualcomm AI Hub 云编译成功跑通——推理输出正确、连贯。
现在要做的是本地编译:用 QAIRT SDK 在 Windows 上直接把模型编译成 HTP 上下文二进制(context binary),不再依赖云端。
本地编译的产物加载到设备上、成功执行——没有任何报错。
但输出是乱码。
一、问题现象
Q: What is 2+3? A: The value of 2 + 3 is $\boxed{5}$. oreferalitiesmallalitiesmall...第一个 token 看起来是对的(“The value of 2 + 3 is 5”),然后迅速退化为无意义的重复字符。
中文同样:
Q: 请用中文解释什么是量子计算 A: 量子计算是一种利用量子力学原理进行... eferalitiesmall...开头几个 token 基本正确,随后完全走偏。
关键特征:
- 模型加载成功,没有任何错误日志
- 第一个 token 往往正确
- 后续 token 迅速发散为乱码
- 相同的设备、相同的 Genie 运行时,云编译版本输出完全正确
这不是"完全不能跑"的问题,而是"看起来在跑,但输出是错的"——往往比加载失败更难排查。
二、9 次失败的假设
在找到真正的根因之前,我们经历了 9 次尝试和排除。
2.1 假设 1:11-split 结构不对 → 改 6-split
本地编译最初用的是 11-split(每份 3 层 Transformer),云编译用的是 6-split(每份 6 层)。拆分方式不同可能导致图结构不兼容。
执行:按云编译的拆分策略重新导出 6-split ONNX,重新编译。
结果:之前出现的usesCrossAttention结构错误消失了,但乱码依旧。
收获:拆分方式确实需要和云编译一致(6-split),但这不是乱码的根因。
2.2 假设 2:QAIRT 版本混用 → 统一 2.42
之前用 QAIRT 2.40 做 converter、2.42 做 compiler。版本混用可能引入兼容性问题。
执行:统一使用 QAIRT 2.42 做 converter + compiler。
结果:DLC 文件大小与 2.40 converter 完全一致,乱码依旧。
2.3 假设 3:n-vocab 配置错误 → 改 131072
genie_config.json中n-vocab设为 152064(Qwen2.5 的实际词汇表大小),而云编译版本使用的是 131072。
执行:改n-vocab为 131072。
结果:模型内部始终输出 152064 个 logits,n-vocab只影响 tokenizer 的解码范围,对推理质量无影响。乱码依旧。
2.4 假设 4:Gather 编码被移除 → 手动恢复
split_onnx函数在拆分时会故意移除 Gather(嵌入层)的激活编码(因 AISW-152612 bug),这可能影响量化精度。
执行:手动在 Part 1 的.encodings文件中恢复嵌入层的编码。
结果:DLC 大小不变,乱码依旧。
2.5 假设 5:浮点回退没启用 → 用 qairt-quantizer
也许需要显式启用--enable_float_fallback,让不支持量化的操作回退到浮点计算。
执行:对所有 12 个 DLC 运行qairt-quantizer --enable_float_fallback。
结果:工具提示"Float Fallback is already applied to the provided DLC"——converter 在转换阶段已经自动处理了。乱码依旧。
2.6 假设 6:VTCM 大小不对 → 4 MB 改 8 MB
通过对比发现,云编译使用 8 MB VTCM(Vector Tightly-Coupled Memory),本地默认是 4 MB。VTCM 是 Hexagon DSP 上的高速缓存,大小可能影响计算精度。
执行:在HtpGraphConfig中设置vtcm_size_in_mb=8,重新编译。
结果:二进制大小从 4703 MB 微调到 4710 MB(更接近云的 4720 MB),但乱码依旧。
收获:VTCM 大小确实影响编译产物,但 7 MB 的差异背后还有更大的原因。
2.7 假设 7:AI Hub 云编译有特殊参数 → 尝试远程编译
也许云编译管线有一些本地无法复制的特殊步骤?
执行:尝试通过 AI Hub Python SDK 提交远程编译任务。
结果:上传速度约 200 KB/s,12 GB ONNX 文件需要约 17 小时上传。放弃。
2.8 假设 8:云编译的量化参数是关键 → 分析源码
AI Hub 云编译管线硬编码了三个额外参数:
other_compile_options+=" --quantize_full_type w8a16 --quantize_io"--quantize_full_type w8a16会将未量化的嵌入层和 LM Head 从 fp16 进一步量化为 w8a16。这些参数只在云 API 中可用,本地 SDK 没有对应选项。
分析 AIMET 编码文件后发现:
- 所有 Transformer 层的主要权重:int4 ✓
- RMSNorm gamma:int16 ✓
- I/O 激活:int16 ✓
- 未编码的初始化器:仅 ~0.28 MB
0.28 MB 的未编码权重不可能导致全模型乱码。这条路也走不通。
2.9 假设 9:二进制大小差异 → 反向追踪
到这一步,我们换了思路——不再猜测原因,而是回到最基本的可观测指标。
| 编译方式 | 总二进制大小 |
|---|---|
| 云编译 | ~4720 MB |
| 本地(soc_model=0, vtcm=4MB) | ~4703 MB |
| 本地(soc_model=0, vtcm=8MB) | ~4710 MB |
4703 → 4710 → 4720。差距在缩小,但始终没有完全对齐。
17 MB 的差异(4703 vs 4720)对于一个 4.7 GB 的模型来说只有 0.36%——看似微不足道。但在前面的 SMMU 调试经验中我们学到一个规则:
二进制大小是编译正确性的可靠代理指标。当本地和云的大小完全匹配时,输出质量才会匹配。
17 MB 的差距说明,编译器做了不同的事情。问题是:什么导致了不同?
三、突破:soc_model
原因在CompileConfig的一个容易被忽略的参数上。
QAIRT Python API 的编译配置中有一个HtpDeviceConfig,用于指定目标设备信息:
fromqairt.api.common.backends.htp.configimportHtpDeviceConfig HtpDeviceConfig(soc_model=77,# SA8775P (QCS9100)dsp_arch="v73",# Hexagon v73)我们之前的编译配置中没有设置device_custom_configs。查看 SDK 文档发现:
当
soc_model未指定时,默认为0(generic)。
soc_model=0意味着编译器不知道目标芯片是什么,只能使用通用优化策略。而soc_model=77告诉编译器目标是 SA8775P——QCS9100 的底层 SoC。
加上这一行,重新编译:
config=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)],)],)编译完成。二进制总大小:4720.7 MB。
与云编译的 4720 MB精确匹配。
部署到设备上:
Q: What is 2+3? A: 2 + 3 equals 5. Q: 请用中文解释什么是量子计算 A: 量子计算是一种利用量子力学原理进行计算的技术...(正确、连贯的中文回答)乱码消失。中英文推理全部正确。
四、根因分析
4.1soc_model=0到底做了什么
当编译器不知道目标 SoC 时,它无法针对具体硬件特性做优化,只能使用保守的通用策略。在 Hexagon HTP 编译器中,这导致了多个层面的差异:
1. Spill/Fill 缓冲区大小错误
| 指标 | soc_model=0 | soc_model=77 | 云编译 |
|---|---|---|---|
| Part 2 SpillFill (prompt) | 8,192,000 | ~1,835,008 | 1,835,008 |
通用编译分配了 8 MB 的 spill/fill 缓冲区,而针对 SA8775P 优化后只需要 ~1.8 MB。spill/fill 缓冲区用于在 VTCM(片上高速缓存)不够时溢出中间数据。错误的缓冲区大小意味着数据在 VTCM 和主存之间的搬运策略完全不同。
2. 操作调度不同
通用编译器不了解 Hexagon v73 的具体流水线特性(如 HVX 向量单元的宽度、VTCM bank 数量),无法对操作进行最优排序。这影响了中间结果的数值精度。
3. 自回归累积误差
前面两个问题在单次前向传播(prompt 阶段)中表现不明显——所以第一个 token 往往看起来正确。但在逐 token 生成(自回归阶段),每个 token 的输出都依赖前一个 token 的 KV cache。微小的数值偏差在 28 层 Transformer 中逐步放大,几个 token 后就完全偏离正轨。
这解释了为什么现象是"第一个 token 对,后面乱码"——这是自回归解码中误差累积的典型表现。
4.2 为什么二进制大小是线索
编译器的优化策略直接决定了 context binary 的结构:spill/fill buffer 的大小、操作的调度顺序、以及数据在不同存储层级之间的搬运方式。这些差异都会反映在最终二进制文件的大小上。
| 状态 | 二进制总大小 | vs 云编译 | 推理质量 |
|---|---|---|---|
| soc_model=0, vtcm=4MB | 4703 MB | -17 MB (-0.36%) | 乱码 |
| soc_model=0, vtcm=8MB | 4710 MB | -10 MB (-0.21%) | 乱码 |
| soc_model=77, vtcm=8MB | 4720.7 MB | +0.7 MB (+0.01%) | 正确 |
| 云编译 | ~4720 MB | baseline | 正确 |
17 MB 的差距看似微小,但它代表的是编译器在内部做出了不同的决策。当本地编译的大小与云编译精确匹配时,内部结构也一致——推理质量随之匹配。
五、为什么这个参数这么难发现
回顾整个调试过程,有几个因素让soc_model成为最后一个被检查的地方:
5.1 没有报错
soc_model=0是一个合法的默认值。编译器不会警告你"你没有指定目标芯片"——它只是默默地用通用策略编译,产出一个完全合法的 context binary。这个文件可以正常加载、正常执行、不触发任何错误日志。
5.2 差异太微小
4703 MB vs 4720 MB,0.36% 的差异。如果你没有一个已知正确的参考值(云编译的 4720 MB),这个数字看起来完全合理。
5.3 症状误导
"第一个 token 对,后面乱码"这个现象很容易被误解为:
- 量化精度问题(假设 8)
- 编码缺失(假设 4)
- KV cache 配置错误
- tokenizer 不匹配(假设 3)
这些方向都有一定的合理性,足以消耗大量时间去排查。
5.4 文档不突出
在 QAIRT SDK 文档中,soc_model是HtpDeviceConfig的众多参数之一。文档没有特别强调"不设置此参数会导致推理乱码"——因为在手机端(Snapdragon)的典型场景中,编译通常在设备上或通过 AI Hub 云端完成,soc_model会被自动设置。
本地离线编译是一个非典型场景,而在这个场景下,soc_model的默认值是致命的。
六、完整的正确编译配置
importqairtfromqairt.api.compiler.configimportCompileConfigfromqairt.api.common.backends.htp.configimport(HtpGraphConfig,HtpContextConfig,HtpDeviceConfig,HtpDeviceCoreConfig,PerfProfile,)# 转换 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)# 编译 DLC → context binaryconfig=CompileConfig(backend="HTP",graph_custom_configs=[HtpGraphConfig(name="prompt_ar128_cl4096_N_of_6",optimization_type=3,weights_packing=True,vtcm_size_in_mb=8),HtpGraphConfig(name="token_ar1_cl4096_N_of_6",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,# SA8775P — 必须指定dsp_arch="v73",# Hexagon v73 — 必须指定cores=[HtpDeviceCoreConfig(perf_profile=PerfProfile.BURST,rpc_control_latency=100)],)],)result=qairt.compile([prompt_model,token_model],config=config)result.save("qwen2_5_7b_instruct_part_N_of_6.bin")设备端的htp_backend_ext_config.json也需要对应设置:
{"devices":[{"soc_model":77,"dsp_arch":"v73","cores":[{"perf_profile":"burst","core_id":0,"rpc_control_latency":100}]}],"memory":{"mem_type":"shared_buffer"},"context":{"weight_sharing_enabled":true}}soc_model在两个地方都要设:
- 编译时(
CompileConfig):决定编译器如何优化 - 运行时(
htp_backend_ext_config.json):决定 Genie 如何初始化 HTP 后端
两处不一致同样可能产生问题。
七、soc_model速查表
| SoC | soc_model | dsp_arch | 典型设备 |
|---|---|---|---|
| SA8775P | 77 | v73 | QCS9100 / QCS9075, Thundercomm Ride |
| QCS8550 | (查 SDK) | v73 | Snapdragon 8 Gen 3 |
| SA8295P | (查 SDK) | v69 | 高通座舱平台 |
完整列表见 QAIRT SDK 文档docs/QNN/general/overview.html的 Supported Devices 表。
八、关键经验总结
8.1 默认值可以是致命的
soc_model=0不会报错、不会警告、不会中止编译。它产出一个看起来完全正常的文件,但推理结果是错的。在嵌入式 AI 部署中,“编译成功"不等于"编译正确”。
8.2 二进制大小是你最好的调试指标
在整个调试过程中,二进制大小是唯一一个稳定指向根因的信号。云编译产出 4720 MB,只要本地编译不是 4720 MB,就说明编译器做了不同的事情。
这个规则在多个场景中反复验证:
- 体积差 2 倍 → 4-bit weights 没打包(详见 SMMU 限制突破记录)
- 体积差 0.36% →
soc_model不对(本文) - 体积差 < 0.01% → 编译正确
8.3 第一个 token 正确 ≠ 模型正确
自回归 LLM 的 prompt 阶段(第一个 token)是单次前向传播,数值误差还没有机会累积。只有在逐 token 生成阶段,KV cache 中的误差才会被反复放大。
永远不要因为第一个 token 看起来对就停止排查。
8.4 建立一个已知正确的参考值
整个调试过程之所以能最终收敛,是因为我们有一个已知正确的参考值——云编译版本的 4720 MB 和其正确的推理输出。没有这个基准,很难判断本地编译的产物是否"足够接近"。
在部署新模型时,先用云编译或官方预编译版本建立基线,再尝试本地编译优化。
8.5 排除法的价值
虽然前 8 次假设都失败了,但每一次排除都缩小了搜索空间:
- 排除了拆分策略(假设 1)
- 排除了 SDK 版本(假设 2)
- 排除了配置参数(假设 3、4、5)
- 排除了量化差异(假设 6、8)
最终只剩下"编译器在底层做了不同的事情"这一个方向。soc_model就在这个方向的终点。
九、附录:9 次假设完整对照表
| # | 假设 | 执行动作 | 结果 | 关键排除证据 |
|---|---|---|---|---|
| 1 | 11-split 结构不兼容 | 改 6-split | 结构错误消失,乱码依旧 | split 策略影响结构,不影响数值 |
| 2 | QAIRT 版本混用 | 统一 2.42 | DLC 大小不变,乱码依旧 | converter 版本差异可忽略 |
| 3 | n-vocab 配置错误 | 改 131072 | 无效果 | 模型始终输出 152064 logits |
| 4 | Gather 编码被移除 | 手动恢复 | DLC 大小不变,乱码依旧 | 编码对嵌入层影响有限 |
| 5 | 浮点回退未启用 | qairt-quantizer | 已自动应用 | converter 默认处理 |
| 6 | VTCM 4 MB 太小 | 改 8 MB | 大小 4703→4710,乱码依旧 | 方向正确但不是根因 |
| 7 | 需要云端特殊步骤 | 远程编译 | 上传速度 200 KB/s,放弃 | N/A |
| 8 | 云端量化参数是关键 | 分析源码 | 未编码权重仅 0.28 MB | 贡献 <1%,不是主因 |
| 9 | 二进制大小差异 | 反向追踪 | 发现 soc_model=0 | 17 MB = 编译策略差异 |
一行配置的教训:
soc_model=77。这个参数在 QAIRT SDK 文档中没有被特别标注,在手机端等典型场景中会被自动设置,但在本地离线编译场景中,不设置它会导致推理结果完全错误且没有任何报错提示。