高通跃龙IQ-9075平台的开发记录(2): genie-图名称规则与常见问题

高通跃龙IQ-9075平台的开发记录(2): genie-图名称规则与常见问题

设备: 高通跃龙 IQ-9075 EVK(SA8775P,Hexagon v73)
运行时: Qualcomm Genie(QAIRT 2.42 附带示例源码 / 设备侧 Genie 1.14.0)
模型: Qwen2.5-7B-Instruct(本地编译)
依据: Qualcomm AI Hub Models 导出代码;QAIRT SDK 中 Geniequalla引擎源码;板上实测

本地编译 Qwen2.5-7B 时,context binary 里的图名称(graph name)曾写成类似qwen2_5_7b_instruct_prompt_2_of_8。模型能加载,输出却是乱码。后来对照官方导出约定,并翻了 Genie 源码,才搞清:图名称既参与AR/CL 回退解析,又决定分片在管线里的顺序。下根据现象、文档约定、源码行为、实测踩坑经历来写。

一、现象

本地编出的图名称缺少ar/cl段:

qwen2_5_7b_instruct_prompt_2_of_8 qwen2_5_7b_instruct_token_2_of_8

官方流水线生成的名称类似:

prompt_ar128_cl4096_2_of_8 token_ar1_cl4096_2_of_8

错误命名下,推理从第一个 token 起就不对。这和soc_model缺失时的乱码不同:后者常见「第一个 token 还像样、后面越跑越偏」;图名称相关问题时,开头就不对。

另一次把模型拆成 11 份后,加载阶段直接报:

token_ar1_cl4096_10_of_11 : input_ids - Tensor not found token_ar1_cl4096_9_of_11 : logits - Tensor not found

图已经按ar/cl命名,但分片编号的字典序把「第 10 份」排到了「第 2 份」前面,管线入口/出口对错了图。

二、官方导出规则

Qualcomm AI Hub Models 在共享 LLM 导出逻辑里,把命名写成硬规则约定。

qai_hub_models/models/_shared/llm/export.py

# Names follow the ar{seq_len}_cl{ctx_len} convention required by Genie.instantiations:list[tuple[str,int,int]]=[(f"ar{sl}_cl{cl}",sl,cl)forclincontext_lengthsforslinsequence_lengths]

同文件在提交各 split 编译时再次注明:ar.../cl...对 Genie 有语义。

qai_hub_models/models/_shared/llm/model.py中的命名函数:

defget_qnn_context_graph_name(self,split_index:int,num_splits:int)->str:""" Sequence length (ar...) and context length (cl...) in graph name are semantically important to Genie """ifself.sequence_length==1:instantiation_type=LLMInstantiationType.TOKEN_GENERATORelse:instantiation_type=LLMInstantiationType.PROMPT_PROCESSORreturn(f"{instantiation_type.value}_ar{self.sequence_length}"f"_cl{self.context_length}_{split_index+1}_of_{num_splits}")

因此完整格式是:

{type}_ar{N}_cl{M}_{K}_of_{T}
字段含义常见取值
type实例类型prompt(并行处理一段输入)或token(逐 token 生成)
ar{N}该图一次吃多少 tokenprompt 常用ar128;token 生成常用ar1
cl{M}上下文长度cl4096,与 Genie 配置里的context.size对齐
{K}_of_{T}第几片 / 共几片3_of_6

sequence_length == 1时走 token 图,否则走 prompt 图。这和 Transformer 推理里「prefill / decode」两套图是同一套分工。

三、Genie 源码里图名称怎么用

QAIRT 2.42 附带的 Genie 示例源码在:

examples/Genie/Genie/src/qualla/engines/qnn-htp/

下面两处直接决定「名称错了会怎样」。

3.1 AR / CL:张量优先,图名称兜底

nsp-graph.cpp里,GraphVariant构造时会算n_tokens(AR)和ctx_size(CL):

ctx_size=determineGraphContextSize(defaultGroup);n_tokens=determineGraphInputSize(defaultGroup);

AR(一次处理的 token 数)优先从 IO 张量推断:

  • input_ids→ 用元素个数(注释写明形状[1,1,1,AR-N]
  • 否则试inputs_embeds、attention_mask、past_key 输出维、logits 等

张量推不出来时,才从图名称用正则抠数字:

// Attempting to parse input token length from graph name.staticconststd::regexpattern(R"((ar|AR)_?(\d+))");// AR_32 / ar32 / ...if(std::regex_search(graph_name,matches,pattern)&&matches.size()==3)returnstd::stoi(matches[2].str());

CL(上下文长度)同样优先看 attention_mask、past_key_in/out 等;失败后再解析名称:

staticconststd::regexpattern(R"((cl|CL)_?(\d+))");// cl_1024 / CL1024 / ...

若名称里仍没有cl,代码会扫名称中所有数字,取最大值;只有该值大于CONTEXT_SAFE_LIMIT(定义为501)才当作 CL:

#defineCONTEXT_SAFE_LIMIT501// Safe limit is there to ensure In AR-N, N number or// any other smaller number in graph name shouldn't be get selected as context length.

也就是说:名称写成..._prompt_2_of_8时,出现的数字是 2、5、7、8 一类,最大值远小于 501,不会被误当成上下文长度,函数直接返回-1。后面nsp-model.cpp对「检测不到 CL」的 variant 还有补救逻辑,但配置里的context.size与「加载到的 max-CL」对不上时,会直接报错退出:

if(max_ctx_size<m_ctx_size&&!isLongContextEnabled()){State::error(fmt::format("Config specifies context->size={}, but loaded max-CL={}",m_ctx_size,max_ctx_size));returnfalse;}

所以 AI Hub 注释里「ar/cl对 Genie 语义重要」和源码并不矛盾:正常图靠张量就能定 AR/CL;一旦某片图 IO 不完整、或检测路径走到名称回退,名称里有没有规范的ar/cl就决定成败。官方导出一律写进名称,是为了和 Genie 的约定对齐,避免踩回退路径。

3.2 分片顺序:std::set字典序

同目录nsp-model.cppstd::map<variant_spec, std::set<std::string>>收集同 AR/CL 下的各分片图名,再按集合顺序赋idx

std::map<std::pair<int32_t,int32_t>,std::set<std::string>>graph_names;// ...// Graph names are sorted by default (std::set<>), so iterate by splitfor(auto&graph_name:graphs){__INFO("Inserting graph {} as idx {} for AR-{} CL-{}",graph_name,idx,variant,ctx_size);m_nsp_graphs[idx++].addGraph(m_graph_map.at(graph_name));}

std::set<std::string>按字典序排序。注释写明:靠这个顺序当作 split 下标。

初始化校验还要求:

  • 第一个split(m_nsp_graphs.front())上要有input_ids(或约定的 embedding 输入)
  • 最后一个split(m_nsp_graphs.back())上要有logits(或对应输出)

找不到就推{graph_name, tensor_name, "Tensor not found"}——与板上看到的报错一致。

于是当 split ≥ 10 时:

字典序: ..._10_of_11 ← idx 0(被当成第一片,期望有 input_ids) ..._11_of_11 ..._2_of_11 ← 真正带 input_ids 的第一片 ... ..._9_of_11 ← 被当成最后一片(期望有 logits)

ASCII 里'1' < '2',所以_10_排在_2_前面。这不是 Genie「故意」乱序,而是字符串排序的自然结果。

日志里也会打印类似:

Inserting graph token_ar1_cl4096_... as idx ... for AR-... CL-... qnn-htp: Graphs loaded ((AR-n, CL-x): #splits): ...

可用这些行核对实际 AR/CL 和分片顺序。

四、两类踩坑与处理

4.1 缺少ar/cl:能加载,输出乱

本地早期 ONNX 图名未按官方格式设置,编进 binary 后 Genie 仍可能靠部分张量推出 AR;CL 回退又吃不到合法大数字,variant 分组和缓存配置容易偏。表现是输出从一开始就不对。

处理:在 ONNX / 编译阶段写成prompt_ar128_cl4096_K_of_Ttoken_ar1_cl4096_K_of_T,与genie_config.json里的context.size一致。

4.2 分片 ≥ 10:字典序打乱管线

报错形如..._10_of_11 : input_ids - Tensor not found。名称里的ar/cl已经对了,错在K_of_T的排序。

处理过三种做法:

  1. 零填充:导出时用_02_of_11代替_2_of_11,字典序与数值序一致。
  2. 只改编号的等长补丁:例如把已编好的 binary 里_10_改成_T0_'T' > '9'),让第 10、11 片排到单位数后面。这只动分片编号,不动ar/cl
  3. 把 split 压到 ≤ 9:Qwen2.5-7B 的 28 层用 6-split(每份约几层)就够,从根上避开字典序问题。最终采用这条。

4.3 想事后改语义字段:改不动

发现名称不规范后,曾对 context binary 做字符串级修改:

做法结果
换名称并改 uint32 长度前缀Create From Binary FAILED,偏移对不上
换名称、用空字节垫齐长度Unable to retrieve graph handle
等长替换(下划线填充)Unable to retrieve graph handle

QNN 发布说明里对 HTP context / LoRA 等路径提过 checksum 类问题;结合上述失败现象,可以认为 binary 对内容敏感,不能指望改几个字符修好语义字段ar/cl/图类型前缀这类变更,只能从 ONNX 或 DLC 起重编。
(只改_10__T0_这类等长编号补丁,有时能过加载;那是另一条、更窄的路径,且仍不如直接 6-split 干净。)

编译时可用context_enable_graphs=<graph_name>(Python API 的HtpGraphConfig(name=...)或 CLI--qnn_options)把图名写进 binary;编完用qnn-context-binary-info核对实际名称。

五、编译时怎么设对

ONNX 阶段(每个 split 的 prompt / token 各一份):

prompt_model.graph.name=f"prompt_ar128_cl4096_{part_idx}_of_{num_splits}"token_model.graph.name=f"token_ar1_cl4096_{part_idx}_of_{num_splits}"

num_splits ≥ 10时对part_idxzfill,或干脆不要拆那么多份。

编译阶段get_qnn_context_graph_name/context_enable_graphs使用同一套字符串,避免 ONNX 名与 enable 列表不一致。

运行前genie_config.jsoncontext.size与名称里的cl{M}、以及实际图推得的 max-CL 一致;加载日志里看Inserting graph ... for AR-... CL-...

六、小结

问题表现源码/约定里对应什么怎么处理
ar/cl加载或输出异常、乱码名称是 AR/CL 的回退解析源;官方导出强制写入type_arN_clM_K_of_T重编
split ≥ 10input_ids/logitsTensor not foundstd::set字典序当 split 下标零填充、少拆分,或慎用编号补丁
事后改语义名Create From Binary / 取不到 graph handlebinary 对内容敏感从 ONNX/DLC 重编

图名称不是给人看的标签:它参与 Genie 对 AR/CL 的兜底推断,并用字符串排序决定分片管线。本地编译时把它和拆分数、上下文长度放在同一套配置里规划,能少走很多弯路。

参考

  • 高通预编译模型库:qai_hub_models/models/_shared/llm/model.pyget_qnn_context_graph_name)、export.pyar{seq}_cl{ctx}约定)
  • QAIRT 2.42 Genie 源码:
    • examples/Genie/Genie/src/qualla/engines/qnn-htp/nsp-graph.cpp(张量推断 AR/CL、名称正则回退、CONTEXT_SAFE_LIMIT
    • examples/Genie/Genie/src/qualla/engines/qnn-htp/nsp-model.cppstd::set排序分片、首尾图校验、context.size与 max-CL)
  • 相关实测记录:FastRPC SMMU 限制突破记录(11-split 字典序与编号补丁)