先看结论与判断条件
- CPU 基线必须来自同一模型候选、运行时版本、输入张量和后处理,不是另一构建或桌面推理结果。
- delegate 可用、创建成功、Prepare 成功和实际接管算子是不同状态,回执要分别记录。
- 部分图委托会在 CPU 与硬件之间传递张量,输出一致性测试必须覆盖真实分区而非只检查最终初始化码。
- 浮点、量化和排序输出需要分别定义绝对容差、相对容差、离散一致性与业务决策边界。
- 预处理、标签、tokenizer、归一化和关联文件属于模型身份,不能只比较主模型文件摘要。
- 不支持、委托失败和输出超差要有明确 CPU 回退或拒绝终态,并在同一设备矩阵中验证。
CPU 基线首先是一套固定实验条件
CPU 路径只有在输入和契约完全一致时才能充当比较基线。两次运行要使用同一个模型摘要、相同运行时版本、相同线程与确定性选项、相同输入张量字节、相同预处理参数以及相同后处理。若一边读取图片后再缩放,另一边直接使用缓存张量,最终差异无法归因给 delegate。
基线回执不保存真实用户内容,可以保存脱敏样本 ID、输入张量摘要、shape、dtype、量化参数和预处理版本。摘要证明两次测试引用相同输入,却不证明输入本身符合业务。样本集仍要覆盖正常、边界、空白、极值和业务关键类别,并通过数据治理审查后进入测试仓库。
CPU 输出也不是数学真值,它是项目选定的参考实现。运行时升级、不同优化内核或线程设置可能改变浮点舍入,因此基线必须绑定候选版本。若 CPU 自身输出与权威离线样本不一致,应先阻止比较,不可用一个错误基线给硬件路径签字。
| 身份项 | CPU 路径 | delegate 路径 | 不一致处理 |
|---|---|---|---|
| modelSha | 同一摘要 | 同一摘要 | 拒绝比较 |
| metadataSha | 同一摘要 | 同一摘要 | 拒绝比较 |
| inputDigest | 同一张量摘要 | 同一张量摘要 | 拒绝比较 |
| preprocessVersion | 同一版本 | 同一版本 | 拒绝比较 |
| postprocessVersion | 同一版本 | 同一版本 | 拒绝比较 |
| runtimeVersion | 记录实际版本 | 记录实际版本 | 分组而非混算 |
delegate 状态要从可用性追到实际分区
设备报告 delegate 可用,只说明可以尝试创建,不保证当前模型、算子、shape 或数据类型被接受。测试事件应分开 availabilityChecked、delegateCreated、prepareCompleted 和 invocationCompleted。任何阶段失败都要记录具体终态,并确认运行时是否按策略回退到 CPU,不能看到对象创建成功就标记硬件执行。
delegate 的 Prepare 阶段会分析执行图、选择节点并用 delegate kernel 替换分区。实际接管可能覆盖全图,也可能只覆盖部分算子。回执需要保存受控的 partitionSummary,如委托节点数量、分区数量和未委托原因类别;不必公开模型业务结构,但要足以确认测试确实经过目标路径。
部分图委托会在执行器之间产生张量转换和同步。即便最终输出在容差内,某些中间 shape、动态输入或连续多次调用仍可能暴露生命周期问题。矩阵应覆盖首次调用、重复调用、输入 shape 变化、应用后台再前台和解释器重建,分别确认 delegate 资源与分区状态。
| 状态 | 说明 | 可否声称已执行 | 失败终态 |
|---|---|---|---|
| available | 设备允许尝试 | 否 | 使用 CPU 或拒绝 |
| created | 对象创建完成 | 否 | 释放并回退 |
| prepared | 图完成分区 | 仍需分区证据 | 记录分区并继续 |
| invoked | 执行返回 | 还需输出比较 | 进入一致性门禁 |
| verified | 输出与元数据通过 | 仅限该样本环境 | 登记回执 |
模型身份必须包含元数据与关联文件
主模型摘要相同,不代表端到端输入输出一致。模型元数据可能描述归一化参数、输入输出语义、标签和关联文件;文本模型还可能依赖 tokenizer 与词表,视觉模型可能依赖颜色顺序和缩放规则。任何附件变化都会改变张量或后处理结果,应进入 modelBundleId。
modelBundleId 可以由主模型、元数据、标签、tokenizer、预处理配置和后处理配置摘要按固定顺序组成。CPU 与 delegate 测试事件都引用它,门禁先检查一致,再比较输出。若某个运行时忽略部分元数据,适配层必须显式应用同一参数,而不是让两条路径各自选择默认值。
标签文本变化可能不改变数值张量,却会改变最终业务结果;tokenizer 变化会在推理前改变输入。技术测试因此分两层:张量层比较原始输出,业务层比较后处理类别、排序或文本片段。两层都通过才可声明端到端一致,且声明只覆盖已执行样本。
| 资产 | 影响阶段 | 建议身份 | 遗漏风险 |
|---|---|---|---|
| 主模型 | 图与权重 | modelSha | 比较不同网络 |
| 模型元数据 | 输入输出解释 | metadataSha | 归一化漂移 |
| tokenizer 或词表 | 文本预处理 | tokenizerSha | 输入 token 不同 |
| 标签文件 | 后处理展示 | labelsSha | 类别文字错位 |
| 预处理配置 | 张量生成 | preprocessSha | 颜色或缩放不同 |
| 后处理配置 | 业务决策 | postprocessSha | 阈值或排序不同 |
容差要按输出语义定义,不能只写一个 epsilon
浮点输出比较通常同时需要绝对容差和相对容差。接近零的值主要依赖绝对容差,较大值可用相对差描述;任一元素只有满足项目定义的关系才算通过。容差来自模型验证、数值精度和业务边界,不应从网上抄一个通用值,也不能在看到失败后不断放宽直到通过。
量化输出要核对 dtype、scale 和 zeroPoint,再决定比较整数张量还是反量化结果。若量化参数不同,数值数组即使看起来接近也不属于同一解释。分类任务还要比较 topK 集合、排序和置信度决策;检测任务要匹配框、类别与后处理,不能把扁平数组最大差值作为唯一标准。
业务决策边界可能比张量差异更重要。两个输出都在数值容差内,但若恰好跨过审核阈值、权限判断或离线回退条件,最终行为仍不一致。测试应记录 rawTensorResult 和 businessResult 两个终态;高影响动作不使用模糊容差自动放行,而由确定性策略处理。
| 输出类型 | 主要比较 | 附加元数据 | 拒绝条件 |
|---|---|---|---|
| 浮点向量 | 绝对与相对容差 | shape、dtype | 任一元素超差 |
| 量化张量 | 整数或反量化值 | scale、zeroPoint | 量化参数不一致 |
| 分类结果 | 分数与排序 | 标签版本、topK | 关键类别变化 |
| 检测结果 | 框、类别、置信度 | 后处理版本 | 匹配策略失败 |
| 文本 token | token 序列与终止 | tokenizer 版本 | 词表或长度漂移 |
| 业务决策 | 确定性状态 | policyVersion | 高影响终态变化 |
回退路径也要参加一致性测试
设备不支持 delegate、Prepare 失败、调用失败或输出超差时,产品需要明确 fallbackPolicy。可以回到 CPU、关闭该功能或提示设备不满足条件,但不能静默返回空数组或沿用上一轮结果。每种原因映射到有限终态,UI 和业务层只消费这些终态,不解析厂商错误字符串。
回退到 CPU 后要重新运行同一输入,并确认解释器状态没有被失败 delegate 污染。若实现复用同一个 interpreter,测试要证明移除 delegate 后图与张量重新准备;更保守的路径是新建 CPU interpreter。无论哪种实现,都记录 fallbackReason、cpuInvocationResult 和最终输出比较。
生命周期变化会触发资源释放和重建。Activity 重建、应用后台、低内存、模型切换和连续任务取消后,下一次调用不能复用已失效 delegate 句柄。资源状态与请求 generation 绑定,迟到结果不得覆盖新页面;本篇只验证输出一致性与回退,完整会话写入边界由相邻生命周期内容处理。
设备矩阵按实际硬件路径和业务样本分层
矩阵行应记录实际设备型号、系统、ABI、运行时、delegate 实现和驱动可观察版本,列覆盖 modelBundleId、样本集、生命周期和 fallback 原因。设备计划中的名称不能代替执行回执,因为设备目录、稳定性和容量会变化;云设备实际分配结果和自有关键真机使用统一格式归档。
每台设备先跑 CPU 基线,再在同一候选中跑 delegate。若 CPU 基线未通过权威样本,该设备上的 delegate 比较标记 blocked,而不是用 delegate 与错误 CPU 结果互相证明。实际没有启用 delegate 的设备可以验证 CPU 回退,但不能登记为硬件输出一致。
instrumented test 适合访问 Android 应用上下文、加载真实模型资产、切换生命周期并采集路径事件。单台设备和单个输入通过不能代表完整模型与厂商矩阵。报告要列出未覆盖设备、样本和 delegate 状态,不以初始化成功率或平均差值替代逐样本门禁。
| 字段 | 用途 | 必需条件 | 缺失处理 |
|---|---|---|---|
| candidateId | 绑定 App 与模型候选 | 每次运行 | 拒绝样本 |
| deviceProfile | 记录实际环境 | 设备执行 | 标记未覆盖 |
| delegateState | 证明目标路径 | 硬件比较 | 不得声称启用 |
| partitionSummary | 说明图接管范围 | Prepare 后 | 拒绝硬件结论 |
| inputDigest | 绑定同一输入 | CPU 与 delegate | 拒绝比较 |
| comparisonResult | 记录张量与业务终态 | 每个样本 | 不得登记通过 |
用输出对照器先拒绝元数据不一致
对照事件可以只保存脱敏摘要和有限数值样本。顶层声明 projectAbsTolerance 与 projectRelTolerance,case 中分别保存 CPU、delegate 的 modelBundleId、inputDigest、shape、dtype、values 和 delegateState。检查器先验证所有元数据一致且 delegateState 为 invoked,再逐元素计算差异。
下面的 Python 代码从命令行读取对照文件。输入缺失时直接非零退出;元数据不一致、数组为空、长度不同、非有限数或任一元素同时超过绝对和相对容差时拒绝。容差必须由项目输入,脚本没有内置通用阈值,也不会因为大部分元素接近就忽略单点超差。
公开示例只比较一维浮点摘要,真实项目需按输出语义扩展量化、分类、检测与文本规则,并控制数据规模。它证明的是回执结构和比较算法可审计,不证明任何具体设备输出已经一致。实际完成状态仍需同一候选、完整样本和设备矩阵回执。
import json
import math
import sys
from pathlib import Path
META_FIELDS = ("modelBundleId", "inputDigest", "shape", "dtype")
def stop(message):
print(f"delegate comparison failed: {message}", file=sys.stderr)
raise SystemExit(2)
def load_cases(path_text):
path = Path(path_text)
if not path.is_file():
print("delegate comparison failed: input file is missing", file=sys.stderr)
raise SystemExit(2)
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
stop(f"cannot parse input: {exc}")
def validate(data):
absolute = data.get("projectAbsTolerance")
relative = data.get("projectRelTolerance")
cases = data.get("cases")
if not isinstance(absolute, (int, float)) or absolute < 0:
stop("absolute tolerance is invalid")
if not isinstance(relative, (int, float)) or relative < 0:
stop("relative tolerance is invalid")
if not isinstance(cases, list) or not cases:
stop("cases must be a non-empty list")
for index, case in enumerate(cases):
cpu = case.get("cpu", {})
delegated = case.get("delegate", {})
if delegated.get("delegateState") != "invoked":
stop(f"case {index} did not execute the delegate")
if any(cpu.get(field) != delegated.get(field) for field in META_FIELDS):
stop(f"case {index} metadata does not match")
left = cpu.get("values")
right = delegated.get("values")
if not isinstance(left, list) or not left or len(left) != len(right or []):
stop(f"case {index} output lengths do not match")
for position, (base, value) in enumerate(zip(left, right)):
if not all(isinstance(item, (int, float)) and math.isfinite(item) for item in (base, value)):
stop(f"case {index} position {position} is not finite")
difference = abs(base - value)
limit = max(absolute, relative * abs(base))
if difference > limit:
stop(f"case {index} position {position} exceeds tolerance")
print(f"delegate comparison passed: {len(cases)} cases")
if __name__ == "__main__":
if len(sys.argv) != 2:
stop("usage: compare_delegate.py comparison.json")
validate(load_cases(sys.argv[1]))发布门禁绑定模型包、设备和真实执行路径
发布前先固定 App 候选、modelBundleId、运行时和容差策略,再按设备矩阵生成 CPU 基线与 delegate 事件。门禁依次检查元数据、CPU 权威样本、delegate 实际状态、图分区、张量比较、业务终态和回退路径。任一步缺失都不允许把样本登记为 verified。
模型、元数据、tokenizer、delegate 库、加固配置或运行时变化后,应重新生成候选身份并重跑关键矩阵。历史样本可以用于趋势分析,不能替新候选签字。若输出超差,先定位输入、分区、量化和后处理,再决定回退或阻止发布,不通过扩大容差掩盖原因。
站内的 delegate 加固后回退文章可继续检查初始化和资源路径;准备评估商业 App 的端侧模型、硬件 delegate 与加固范围时,可通过页面行动按钮进入御盾中央平台提交候选包、模型包和目标设备。提交只启动评估,一致性结论仍以同一候选的实际执行回执为准。
- CPU 与 delegate 使用同一 modelBundleId 和 inputDigest。
- 分别记录可用、创建、Prepare、调用和验证状态。
- 图分区摘要证明目标 delegate 实际接管算子。
- 按输出语义定义容差、枚举和业务终态。
- 不支持、失败和超差路径都有明确 CPU 回退或拒绝。
- 候选变化后重跑真实设备与关键业务样本矩阵。
- 每份回执记录 runtime、delegate 实现和实际设备环境。
- 量化输出先核对 scale、zeroPoint 与 dtype 再比较。
- 张量门禁通过后继续核对业务后处理和决策终态。
- 未覆盖样本、设备与生命周期组合保持明确缺口状态。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android 端需要检查 delegate 可用性并保留 CPU 执行路径。 | LiteRT Android Java API 说明 delegate 可用性检查与 CPU 路径。 | 可用或初始化成功不证明输出一致、全图委托或性能可接受。 |
| delegate 可能只接管部分算子并产生跨执行器数据传递。 | LiteRT delegate architecture 说明图分区和不支持节点保留机制。 | 架构事实不预设任何设备的分区比例、性能收益或输出结果。 |
| Prepare 阶段决定图分区与 delegate kernel 替换。 | TfLiteDelegate API 说明 Prepare 与资源生命周期接口。 | 底层接口不保证具体厂商实现的错误处理、线程或数值一致性。 |
| 模型身份应包含元数据、标签和关联文件。 | LiteRT model metadata 说明输入输出描述、归一化、标签与关联文件。 | 元数据存在不证明适配层实际采用,也不能替代 tokenizer 与预处理摘要。 |
| 真实应用中的 delegate 生命周期和输出应在 Android 设备回归。 | Android instrumented tests 说明设备端测试可访问应用上下文和平台 API。 | 单一设备或样本通过不能代表全部 ABI、系统、厂商和模型输入。 |
| 设备计划必须保存实际执行型号与系统回执。 | Test Lab available devices 说明设备目录、稳定性和容量会变化。 | 云设备可用不代表覆盖业务关键厂商特性和全部 delegate 实现。 |
| 元数据不一致或 delegate 未实际执行时应拒绝输出比较。 | 工程判断:实验条件或目标路径不成立时,数值差异无法归因。 | 具体元数据字段和分区证据需要按所用运行时实现补充。 |
| 当前不能声称任何目标设备的 delegate 输出已与 CPU 一致。 | 项目证据尚未接入;需要同候选模型包、输入、设备和比较回执。 | 文章提供方法和检查代码,不替代目标项目测试结论。 |
工程常见问题
delegate 创建成功是否说明模型已经跑在硬件上?
不能。还要确认 Prepare 完成、实际图分区、调用成功,并保存分区摘要;创建成功只证明对象建立。
CPU 输出是否可以直接当作绝对正确答案?
CPU 是项目参考路径,也要绑定同一模型、运行时和权威样本。若 CPU 基线本身失败,该设备上的 delegate 比较应 blocked。
所有浮点输出能否共用一个容差?
不应。容差与数值范围、dtype、模型验证和业务决策有关,分类、检测、量化和文本还需要不同结构规则。
只比较主模型文件摘要为什么不够?
元数据、tokenizer、标签、归一化和后处理都会改变输入或最终业务结果,应共同组成 modelBundleId。
delegate 失败后自动回到 CPU 是否无需再测?
仍需验证失败解释器没有污染 CPU 路径、同一输入重新执行、结果正确且 fallbackReason 明确,不能只看应用未崩溃。
App 加固后为什么要重跑输出一致性?
加固可能改变模型装载、Native delegate、资源生命周期和后处理路径。候选身份变化后,旧设备回执不能证明新包行为。