跳到主内容
快讯直播
AI智模界
教程

把端侧小模型塞进骁龙智能眼镜:导出、量化与端侧实测

假设手里有一个自研小模型 PrismML,参数量在亚十亿量级,输入是文本 token 或一小段音频特征,目标是让它在一副骁龙平台的智能眼镜上离线常驻推理。这件事的难点不在模型本身,而在"从 PyTorch 到眼镜上的二进制资产"这条链路上:导出格式对不对、量化掉不掉点、云端跑出来的延迟和真机差多少、眼镜上电池能撑多久。下面按顺序走一遍。

做完能得到什么

走完这套流程,你会拿到四样东西:

1. 一个经过量化、编译成 QNN 资产、可以直接推到设备上的模型文件;

2. 云端 profile 报告:逐层耗时、峰值内存、估算推理延迟;

3. 端侧实测数据:预热后的稳态延迟分位数、连续跑十分钟后的热衰减曲线、功耗区间;

4. 一套可重复执行的脚本,模型改了以后能一键重跑。

前置条件清单

  • 模型侧:PrismML 已经能从 PyTorch 或 ONNX 导出,输入输出 shape 固定,没有依赖运行时的动态控制流。
  • 账号侧:Qualcomm AI Hub 账号与 API AI 词典:Token">Token,具体申请入口以官网当前页面为准。
  • 本地环境:Linux 机器、Python 环境、能装 qai-hub 包;另外准备一份 adb 可用的设备连接。
  • 设备侧:一副(或一台同代平台的参考设备)出现在 AI Hub 设备列表里的骁龙设备。眼镜形态的算力、内存、散热都弱于手机参考机,两边的数据都要测,别只测一台。
  • 校准数据:几百到一两千条有代表性的真实输入。数量不是关键,覆盖度才是。
  • 端侧运行时:设备上要有对应平台的 QNN / QAIRT 运行时,以及 qnn-net-run、qnn-profile-viewer 这类命令行工具,具体获取方式以官方 SDK 文档为准。

第 1 步:把模型导出成可编译的形式

先做一次"干净导出"。固定 batch 为 1,序列长度做成若干固定档位(比如短、中、长三档各编译一份),比留一个动态维度更省事,端侧表现也更稳定。

```python

import torch

import torch.nn as nn

class Wrapper(nn.Module):

"""把预处理之外的纯计算部分包起来,方便导出"""

def __init__(self, backbone):

super().__init__()

self.backbone = backbone

def forward(self, input_ids, attention_mask):

return self.backbone(input_ids, attention_mask)

model = Wrapper(prismml_backbone).eval()

dummy_ids = torch.randint(0, 32000, (1, 64), dtype=torch.long)

dummy_mask = torch.ones(1, 64, dtype=torch.long)

torch.onnx.export(

model,

(dummy_ids, dummy_mask),

"prismml_seq64.onnx",

input_names=["input_ids", "attention_mask"],

output_names=["logits"],

opset_version=17, # 以转换工具当前支持的版本为准

do_constant_folding=True,

)

```

导出后先验证数值一致性:同一份输入,PyTorch 和 ONNX Runtime 的输出做余弦相似度或最大绝对误差对比。这一步不做,后面精度掉了根本分不清是量化的问题还是导出的问题。

导出完顺手做一次算子体检,看看有没有会掉到 CPU 上的算子(常见的是自定义 attention、动态 shape 的 reshape、非常规激活函数)。掉 CPU 的算子会带来跨后端拷贝,延迟抖动会很明显。

第 2 步:配置 AI Hub 并确认设备名

```bash

pip install qai-hub

export QAI_HUB_API_TOKEN="你的 token"

qai-hub configure --api_token "$QAI_HUB_API_TOKEN"

```

列出可选设备,记下你要用的那台的准确名称,后面所有 job 都用这个字符串:

```python

import qai_hub as hub

for d in hub.get_devices():

print(d.name, getattr(d, "os", ""), getattr(d, "attributes", ""))

```

命令行侧也有列设备的子命令,具体名称以 AI Hub 文档为准。

第 3 步:先编译浮点版本,建立基线

不要一上来就量化。先编译一个浮点版本,拿到"未量化"的延迟和内存基线,后面才知道量化省了多少、掉了多少。

```python

import qai_hub as hub

device = hub.Device("设备列表里的准确名称")

compile_job = hub.submit_compile_job(

model="prismml_seq64.onnx",

device=device,

input_specs=dict(

input_ids=((1, 64), "int32"),

attention_mask=((1, 64), "int32"),

),

options="--target_runtime qnn_context_binary",

name="prismml-fp-seq64",

)

print(compile_job.url)

compile_job.wait()

```

options 里的目标运行时字符串、以及是否要额外生成 context binary 的分离形式,都以 AI Hub 当前文档为准——这部分接口改动相对频繁,照抄旧博客容易踩坑。

第 4 步:训练后量化

端侧小模型默认走 PTQ(训练后量化)。常见组合:

  • w8a8:权重和激活都 8 位,压缩比和加速都明显,对激活分布敏感;
  • w8a16:权重量化、激活保持 16 位,对精度更友好,适合注意力类结构;
  • w4a16:权重 4 位,内存占用进一步下降,适合眼镜这种内存紧张的场景,但精度风险更高。

先把权重做逐通道量化(per-channel),激活做逐张量(per-tensor),跑一遍看精度。如果掉点集中在某几层,可以做混合精度:把敏感层(通常是第一层、最后一层、以及归一化前后)留在高精度,其余压到低位宽。

```python

校准数据:用真实场景里的输入,不要用随机张量

calib_data = [

dict(input_ids=ids, attention_mask=mask)

for ids, mask in real_batches # 几百到一两千条

]

quantize_job = hub.submit_quantize_job(

model="prismml_seq64.onnx",

calibration_data=calib_data,

weights_dtype="int8",

activations_dtype="int8",

name="prismml-ptq-w8a8",

)

quantize_job.wait()

print(quantize_job.url)

```

参数名和可选取值请对着 AI Hub 文档核对,不同时期命名会有出入。如果平台自带量化不满足需求,可以换成 AIMET 在本地做,再把量化后的模型传上去编译。

量化完必须做一次端到端精度回归:拿 50 到 200 条带标注的样本,对比浮点版和量化版的输出。分类任务看准确率,生成任务看人工抽检。只要掉点超过你设定的容忍线,就回退到混合精度。

第 5 步:用云端 profile 摸底

```python

profile_job = hub.submit_profile_job(

model=quantized_compiled_model,

device=device,

name="prismml-profile-w8a8",

)

profile_job.wait()

print(profile_job.url)

```

报告里重点看三个数:逐层耗时占比、峰值内存、估算推理延迟。有两个地方要留心:

第一,云端 profile 的设备往往是散热条件较好的参考机,眼镜上的稳态表现会更差。第二,报告给的是稳态估算,首帧还要加上模型加载和内存分配的时间。眼镜上如果每次唤醒都要重新加载,首帧延迟可能比稳态高一个量级,这时候要考虑常驻内存还是分包加载。

第 6 步:把编译产物推到设备上跑

```python

download_path = "artifacts/"

job = quantized_compile_job

job.download_artifacts(download_path)

```

推到设备,准备一份输入清单文件(每行一个输入文件路径):

```bash

adb push artifacts/ /data/local/tmp/prismml/

adb push inputs.txt /data/local/tmp/prismml/

adb shell "cd /data/local/tmp/prismml && \

./qnn-net-run \

--backend libQnnHtp.so \

--retrieve_context prismml_quantized.bin \

--input_list inputs.txt \

--profiling_level detailed \

--output_dir ./out"

```

具体的可执行文件名、后端库名、以及 context 的传入方式是 --retrieve_context 还是别的参数,以你所装的 QAIRT SDK 版本自带文档为准。跑完后把 profiling 日志拉回来:

```bash

adb pull /data/local/tmp/prismml/out ./device_out

qnn-profile-viewer --input_log ./device_out/qnn-profiling-data.log

```

第 7 步:测延迟、内存和功耗

延迟:先预热 10 到 20 次丢掉,再连续跑 200 次以上,记录 P50 和 P95。只看平均值会掩盖抖动。然后把不同序列长度档位各跑一遍,画一张"长度—延迟"曲线。

内存:adb shell dumpsys meminfo <包名> 看进程 RSS,同时记录 QNN 报告的峰值内存。眼镜上内存是硬约束,宁可留足余量。

热衰减:连续跑 5 到 10 分钟,每 30 秒采样一次延迟,看曲线什么时候开始抬升、抬升多少。眼镜的散热面积小,这一步的结果往往比峰值性能更有参考价值。

功耗:能用功率轨(配合 Snapdragon Profiler 之类的工具)就用功率轨,精度高、时间分辨率好。拿不到功率轨时,用电池电流做粗测:

```bash

固定条件:屏幕亮度固定、关闭 Wi-Fi 与不必要的传感器

adb shell dumpsys batterystats --reset

跑一轮固定次数(例如 1000 次)的推理

adb shell "cd /data/local/tmp/prismml && ./run_batch.sh 1000"

adb shell dumpsys batterystats --charged > batt.txt

```

也可以高频采样瞬时电流:

```bash

for i in $(seq 1 600); do

adb shell cat /sys/class/power_supply/battery/current_now

sleep 0.1

done > current_samples.txt

```

电流乘以电池电压得到功率,再按推理次数折算成每次推理的能量消耗(毫焦耳/次)。这个指标比纯功率更实用,因为它把"跑得多快"和"耗多少电"合在了一起。

把结果整理成一张表:

配置P50 延迟P95 延迟峰值内存单次能量相对浮点精度
浮点基线100%
w8a8
w8a16
w4a16

第 8 步:针对眼镜形态做取舍

眼镜和手机的约束不一样:电池小、散热差、佩戴时不能被感知到发热,而且很多场景是"按需唤醒"而不是持续推理。几个实用做法:

  • 分档调度:常驻的唤醒检测用极小模型或传统信号处理,识别到意图后再加载大一点的 PrismML。
  • 限制占空比:与其追求单次延迟压到多低,不如把每秒调用次数降下来,整体功耗收益更明显。
  • 温度降档:读设备温度节点,超过阈值自动切到短序列档位或降频运行。
  • 端云协同:本地只做置信度判断和简单意图,复杂的走云端,本地保留兜底能力。

常见坑与排错

编译失败,报不支持的算子。 先看是不是动态 shape 或控制流。把动态维度改成固定档位,把 Python 层的 if/for 改写成张量操作,通常能解决。

量化后精度掉得厉害。 九成出在校准数据上。用随机张量或只用单一场景的数据做校准,量化参数会偏。换成覆盖真实场景、长度分布接近线上的样本再试。

云端延迟很漂亮,眼镜上完全不是这个数。 检查三件事:热衰减、内存带宽竞争、以及是否每次推理都在重新加载模型。前两个靠限制占空比和降低位宽缓解,第三个靠常驻内存解决。

延迟抖动很大。 大概率是某些层跑在 CPU 上。用逐层 profile 定位,再看能不能换成等效的受支持算子。

功耗测出来忽高忽低。 先固定条件:屏幕、连接、传感器、环境温度。如果还是不稳,把测量时长拉长到几分钟取平均,别用几十秒的样本下结论。

首帧特别慢。 把模型加载、内存分配、首次算子初始化分开计时。眼镜端可以做成开机预热一次,后续走常驻路径。

前后处理成了瓶颈。 分词、重采样、归一化这些如果放在 CPU 上逐帧做,很容易吃掉可观的时间。能塞进模型图里的就塞进去,塞不进去的就用固定 buffer 复用,避免频繁分配。

下一步建议

1. 建立回归门禁:把精度、P95 延迟、单次能量三个指标做成脚本,模型或量化配置一改就自动重跑,超过阈值直接拦下来。

2. 试更低位宽:在 w8a16 稳定之后,针对权重探索 4 位甚至混合位宽,重点观察内存占用和精度两条曲线的交叉点。

3. 接入应用层:把 QNN 资产通过对应平台的推理框架集成进眼镜端应用,处理好多实例、生命周期和后台调度。

4. 做真实场景实测:实验室里的循环跑分和"用户戴一整天、间歇唤醒几百次"是两回事,后者才是决定能不能上线的数据。

5. 设计降级路径:为高温、低电量、内存紧张各准备一套降级配置,让设备在边界条件下仍然可用,而不是直接崩掉或卡死。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。