ARTICLE DETAIL

资讯详情

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

在 Android 系统上 QNN SDK 转换并运行模型示例:TaoToken 统一 Key 配置与验证

在 Android 系统上 QNN SDK 转换并运行模型示例:TaoToken 统一 Key 配置与验证 1. Android 端 QNN SDK 模型转换与运行从 ONNX 到 HTP 后端落地在 Android 手机上跑一个 Embedding 模型听起来像是把大象塞进冰箱但拆开看其实就是三步把 ONNX 转成 QNN 能吃的格式、把模型编译成和芯片绑定的序列化文件、用 qnn-net-run 在设备上验证输出。真正卡人的地方往往不在转换命令本身而在环境变量、量化精度、HTP 后端配置这些细节上。这篇就按我实际跑通 bge-base-zh-v1.5 的流程把 QNN SDK 转换、运行、排障串一遍同时把 TaoToken 统一 Key 的配置骨架嵌进去方便你在做移动端 AI 部署时模型通道和 API 通道都能一次验证通过。适合谁看已经在 Ubuntu 上装好 QNN SDK、手里有一台支持 HTP 的 Android 设备、想把 ONNX 模型落到端侧推理的开发者。如果你还没配 SDK建议先把QNN_SDK_ROOT和x86_64-linux-clang工具链跑通再回来。下面所有命令都可以直接复制路径按你自己的 SDK 版本替换即可。2. TaoToken 统一 Key 前置配置让模型通道和 API 通道共用一套凭证QNN 负责端侧推理TaoToken 负责云端模型调用两者在同一个 Android 项目里经常要并存。与其到处散落 Key不如用一份统一配置管理。TaoToken 的 API 入口是https://taotoken.net/api官网在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册后在控制台生成 Key 即可。我习惯把 Key 放在项目根目录的settings.json里Android 侧通过 Gradle 读取Python 转换脚本通过环境变量读取避免硬编码。下面是一个可复制的骨架{ taotoken: { base_url: https://taotoken.net/api, api_key: sk-你的统一Key, default_model: claude-sonnet-4-20250514, timeout_seconds: 60, retry: 2 }, qnn: { sdk_root: /opt/qnn/qaisw-v2.12.0, soc_id: 39, dsp_arch: v68, vtcm_mb: 8 } }如果你更习惯 TOMLconfig.toml等价写法如下字段名保持一致方便脚本统一解析[taotoken] base_url https://taotoken.net/api api_key sk-你的统一Key default_model claude-sonnet-4-20250514 timeout_seconds 60 retry 2 [qnn] sdk_root /opt/qnn/qaisw-v2.12.0 soc_id 39 dsp_arch v68 vtcm_mb 8注意api_key不要提交到 Git建议用.gitignore排除或者改用环境变量TAOTOKEN_API_KEY注入。Android 侧可以用BuildConfig字段在编译期注入避免明文进 APK。配置好之后先用一条最小请求确认通道可用再去做 QNN 转换这样出问题时能快速区分是网络问题还是模型问题curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: ping}], max_tokens: 16 }返回里能看到choices字段就说明 Key 和网络都正常。这一步花不了一分钟但能省掉后面大量「到底是模型转错了还是 Key 失效了」的排查时间。3. QNN 模型转换全流程ONNX 简化、量化数据、FP16 转换3.1 导出并简化 ONNX 模型以 bge-base-zh-v1.5 为例先用 optimum 导出 ONNX再用 onnxsim 固定输入 shape。固定 shape 对 QNN 很关键动态维度在 HTP 后端上支持有限。from optimum.onnxruntime import ORTModelForSequenceClassification from transformers import AutoTokenizer model_path BAAI/bge-base-zh-v1.5 save_directory ./bge-base-zh-v1.5.onnx model ORTModelForSequenceClassification.from_pretrained(model_path, exportTrue) tokenizer AutoTokenizer.from_pretrained(model_path) model.save_pretrained(save_directory) tokenizer.save_pretrained(save_directory)导出后执行简化把三个输入都固定成1x512onnxsim ./bge-model.onnx bge-simple-model.onnx \ --overwrite-input-shape input_ids:1,512 \ --overwrite-input-shape attention_mask:1,512 \ --overwrite-input-shape token_type_ids:1,5123.2 构建量化校准数据量化质量直接决定端侧精度。校准数据要尽量贴近真实业务分布这里用 C-MTEB/LCQMC 测试集取前 100 条每条按 max_length512 分词保存成 int32 的 raw 二进制文件。import os import numpy as np from datasets import load_dataset from transformers import AutoModel, AutoTokenizer def create_lcqmc_quant_data(): dataset load_dataset(C-MTEB/LCQMC, splittest) sentence dataset[sentence1] tokenizer AutoTokenizer.from_pretrained(./) calib_data_size 100 input_data tokenizer(sentence, paddingmax_length, truncationTrue, max_length512, return_tensorspt) input_ids input_data[input_ids].numpy().astype(np.int32)[:calib_data_size, :] attention_mask input_data[attention_mask].numpy().astype(np.int32)[:calib_data_size, :] token_type_ids input_data[token_type_ids].numpy().astype(np.int32)[:calib_data_size, :] calib_data_dir calib_data os.makedirs(calib_data_dir, exist_okTrue) path_lists [] path_file input_list.txt for arr, name in zip([input_ids, attention_mask, token_type_ids], [input_ids, attention_mask, token_type_ids]): for i in range(arr.shape[0]): file_path os.path.join(calib_data_dir, f{name}_{i}.raw) path_lists.append(file_path) arr[i].tofile(file_path) with open(path_file, w, encodingutf-8) as f: n input_ids.shape[0] for i in range(n): f.write(f{os.path.abspath(path_lists[i])} f{os.path.abspath(path_lists[i n])} f{os.path.abspath(path_lists[i 2 * n])}\n) print(create quant data finish.)注意input_list.txt里必须写绝对路径相对路径在 qnn-onnx-converter 阶段会直接报错这是最常见的坑之一。3.3 转换 ONNX 为 QNN 模型实际部署中我基本不用 W8A8 量化精度掉得太厉害。推荐直接用 FP16去掉--input_list加上--float_bw 16python ${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-onnx-converter \ -o ./bge_qnn_fp16.cpp \ -i ./bge-simple-model.onnx \ --float_bw 16转换后会得到三个文件.bin保存权重参数.cpp描述计算图结构.json辅助描述结构后续步骤不再使用。如果你确实要试量化W8A16 用--act_bw 16 --weight_bw 8但要有精度不达标的心理准备。3.4 生成动态库并编译 HTP 序列化文件先用 qnn-model-lib-generator 把 cpp 和 bin 打包成 so${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-model-lib-generator \ -c ./bge_qnn_fp16.cpp \ -b ./bge_qnn_fp16.bin \ -o rag_qnn_so生成的aarch64-android目录下的 so 可以直接在 Android 上跑但初始化慢。进一步用 qnn-context-binary-generator 编译成和 HTP 硬件绑定的 serialized bin加载速度会快很多${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-context-binary-generator \ --model ./rag_qnn_so/x86_64-linux-clang/libbge_qnn_fp16.so \ --backend ${QNN_SDK_ROOT}/lib/x86_64-linux-clang/libQnnHtp.so \ --output_dir ./bin \ --binary_file bge_qnn_fp16.serialized \ --config_file ./htp_backend_extension.jsonhtp_backend_extension.json内容如下注意shared_library_path和config_file_path都要写绝对路径{ backend_extensions: { shared_library_path: /opt/qnn/qaisw-v2.12.0/lib/x86_64-linux-clang/libQnnHtpNetRunExtensions.so, config_file_path: /opt/qnn/qaisw-v2.12.0/model/htp_device_config.json } }htp_device_config.json里定义图配置和设备信息soc_id 和 dsp_arch 要和你手机芯片匹配{ graphs: { vtcm_mb: 8, O: 3.0, fp16_relaxed_precision: 1, graph_names: [bge_qnn_fp16] }, devices: [ { device_id: 0, soc_id: 39, dsp_arch: v68, cores: [{core_id: 0, perf_profile: burst}] } ] }注意SDK 2.20 之后图配置改成数组格式旧版本用对象格式混用会报找不到 key 的错误。graph_names填 so 文件名去掉lib前缀和.so后缀的部分。4. Android 设备运行验证push 文件、设置环境变量、执行推理4.1 准备输入数据先生成一份真实输入用于验证端侧输出是否合理import os import numpy as np from transformers import AutoTokenizer def create_model_input_data(): tokenizer AutoTokenizer.from_pretrained(BAAI/bge-base-zh-v1.5) input_data ZhongGuo, nihao, 日本再见, good cat! real_data real_data os.makedirs(real_data, exist_okTrue) input_tensor_data tokenizer(input_data, paddingmax_length, truncationTrue, max_length512, return_tensorspt) paths [] for name in [input_ids, attention_mask, token_type_ids]: p os.path.join(real_data, f{name}.raw) input_tensor_data[name].numpy().astype(np.int32).tofile(p) paths.append(p ) with open(./input_data.txt, w, encodingutf-8) as f: f.writelines(paths)4.2 push 到设备并运行把模型、数据、可执行文件和依赖 so 全部推到/data/local/tmp/bgeadb shell mkdir -p /data/local/tmp/bge adb push ./bin/bge_qnn_fp16.serialized.bin /data/local/tmp/bge/ adb push ./real_data/* /data/local/tmp/bge/ adb push ./input_data.txt /data/local/tmp/bge/ adb push $QNN_SDK_ROOT/bin/aarch64-android/qnn-net-run /data/local/tmp/bge/ adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtp.so /data/local/tmp/bge/ adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtpNetRunExtensions.so /data/local/tmp/bge/ adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtpPrepare.so /data/local/tmp/bge/ adb push $QNN_SDK_ROOT/lib/hexagon-v68/unsigned/libQnnHtpV68Skel.so /data/local/tmp/bge/ adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtpV68Stub.so /data/local/tmp/bge/ adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnSystem.so /data/local/tmp/bge/进入设备设置环境变量并执行adb shell cd /data/local/tmp/bge chmod -R 777 /data/local/tmp/bge/* export LD_LIBRARY_PATH/data/local/tmp/bge/:/vendor/lib64/:$LD_LIBRARY_PATH export ADSP_LIBRARY_PATH/data/local/tmp/bge ./qnn-net-run --backend ./libQnnHtp.so \ --retrieve_context ./bge_qnn_fp16.serialized.bin \ --input_list ./input_data.txt \ --output_dir output成功的话output目录下会生成Result_0之类的输出文件里面是 float32 的推理结果。用--use_native_input_files参数可以让 int32 输入按原生类型读取精度会明显改善这个参数在 int 型输入场景下强烈建议加上。4.3 用 TaoToken 验证模型通道端侧推理跑通后如果你还需要调用云端模型做对比或兜底直接用前面配好的 Key 发一条请求即可。模型对话入口在https://taotoken.net/apiCoding Plan 适合长期编码和 Agent 场景接入文档里有完整的参数说明。验证时把端侧输出和云端输出做一次余弦相似度对比能快速判断量化是否损失过大。5. 本篇常见错误排查报错一input_list.txt路径找不到。九成是写了相对路径。qnn-onnx-converter 在转换时会切换工作目录必须用os.path.abspath写绝对路径。报错二Failed to load libQnnHtpV68Skel.so。检查ADSP_LIBRARY_PATH是否指向了 skel 所在目录以及 so 文件是否 push 完整。v68 对应 dsp_arch芯片架构不匹配会直接加载失败。报错三graph_names找不到 key。SDK 2.20 之后图配置是数组旧版本是对象。确认你的 SDK 版本数组格式写成graphs: [{...}]对象格式写成graphs: {...}。报错四推理结果全是 NaN 或精度极差。优先检查是否用了 W8A8 量化换成 FP16 重转其次确认输入 raw 文件是 int32 而不是 float32运行命令加上--use_native_input_files。报错五TaoToken 请求 401。检查Authorization头是否带了Bearer前缀Key 是否被.gitignore误删以及 base_url 是否写成了带 UTM 的官网地址而不是https://taotoken.net/api。6. 通道验证完成后的下一步端侧 QNN 模型跑通、云端 TaoToken Key 验证通过两条通道就算都打通了。接下来可以把settings.json里的配置抽成 Android 的BuildConfig字段在 App 启动时做一次健康检查端侧加载 serialized bin云端发一条 ping 请求。两者都返回正常再进入主流程这样线上出问题时能第一时间定位是模型加载失败还是网络凭证失效。API Keys 管理在控制台里可以随时轮换接入文档里有各语言的示例代码长期做编码 Agent 的话 Coding Plan 的额度模型会更划算。
返回列表