先看结论与判断条件
- 运行时裁剪改变的是模型与应用候选之间的兼容契约,模型文件未变化也不能沿用裁剪前的通过结论。
- 模型所需算子清单必须来自最终交付模型,转换前的框架节点、训练代码或人工估计都不能替代成品扫描。
- 算子名称相同不代表一定兼容,还要比较 domain、opset、数据类型、张量形状、属性组合和运行时内核版本。
- delegate 只接管其支持的子图,验收必须记录实际分区、主运行时回退和跨执行器边界,不能只看硬件后端已初始化。
- 自定义算子与 Core ML 自定义层属于应用代码和模型共同维护的接口,版本、权重、形状推导、线程与数值行为都要绑定。
- 静态清单只能给出发布前阻断,最终放行仍需要同一模型、同一运行时构建、同一应用候选和代表设备上的业务回执。
先把裁剪理解为兼容契约变更
移动端为了控制包体,常会移除未使用的算子内核、执行提供程序或辅助组件。这个动作不会改变模型文件,却会改变应用候选能够解释和执行的模型集合。原来在完整运行时中能够推理的模型,进入裁剪构建后可能在装载阶段拒绝、在图准备阶段找不到内核,也可能直到某条业务分支首次触发时才暴露缺口。因此,裁剪后的验收对象必须是模型、运行时构建和应用候选的组合,不能只给模型单独盖章。
ONNX Runtime Mobile 明确把移动部署与模型所需算子、运行时包体以及执行提供程序联系起来。Google AI Edge LiteRT 也把端侧模型加载和不同 delegate 的执行路径放在同一运行时体系中。这些资料支持一个基础事实:兼容性依赖模型需求与当前运行时能力是否相交。它们不证明任何具体候选已经通过,更不提供模型签名、授权、防回滚或业务准确率结论。
工程上应建立可复算的 compatibilityId,它至少绑定模型摘要、算子清单摘要、运行时版本、裁剪配置摘要、delegate 配置、自定义实现摘要和应用候选摘要。测试报告只对这一组输入有效。只要模型重新转换、运行时升级、裁剪白名单变化、delegate 切换或自定义实现更新,就要重新计算影响面。这样做不是增加文档负担,而是避免把旧报告误贴到新组合上。
| 对象 | 需要固定的身份 | 常见失败阶段 | 不能推出的结论 |
|---|---|---|---|
| 模型 | 文件摘要、格式、opset 与算子清单 | 解析、图准备或特定分支 | 文件存在不等于可完整执行 |
| 运行时 | 版本、构建摘要与允许算子 | 内核解析和数据类型匹配 | 版本相同不等于裁剪内容相同 |
| delegate | 后端、配置与实际分区 | 子图委派、回退和边界复制 | 初始化成功不等于目标子图已接管 |
| 自定义实现 | 代码、权重与接口版本 | 形状推导、计算或线程调用 | 注册成功不等于数值结果正确 |
| 应用候选 | 包摘要、ABI 与调用路径 | 资源装载、路由和业务断言 | 单次演示不覆盖所有真实分支 |
模型需求清单必须来自最终交付文件
算子需求不能从训练代码或转换前计算图抄写。转换器可能融合节点、替换算子、插入量化相关节点,也可能因目标格式和优化级别生成不同图结构。真正需要进入门禁的是最终交付模型扫描结果,并记录扫描工具版本、模型摘要、domain、opset、算子类型、数据类型、张量形状和关键属性。若模型包还包含多个子模型或按设备选择的变体,每个变体都要拥有独立清单。
只有算子名称的集合仍然不够。例如同名算子可能受 opset 语义、输入类型、动态形状或属性组合影响,运行时也可能只编译了部分类型内核。验收表应把“算子已登记”与“当前签名有可用内核”分开。前者是名称匹配,后者要求运行时能力清单能回答具体 domain、版本和类型组合。遇到无法静态判定的动态形状或控制流,要标记为设备执行必测项,而不是默认通过。
模型清单还应保存出现位置和业务可达性。一个罕见分支中的算子即使通常不运行,仍然可能在某类输入、语言、设备或错误恢复路径下触发。删除它的依据不能只是日常日志没有出现。更稳妥的做法是保留全部静态需求,再用业务路径矩阵标记哪些节点已经动态覆盖,哪些仍需构造输入验证。未覆盖与不需要是两种不同状态。
| 字段 | 回答的问题 | 获取方式 | 缺失后的处理 |
|---|---|---|---|
| 模型摘要 | 清单对应哪份字节 | 对最终文件计算摘要 | 整份清单作废 |
| domain 与算子 | 节点属于哪套语义 | 扫描最终计算图 | 不得只按名称放行 |
| opset 或规范版本 | 应按哪一版规则解释 | 读取模型声明 | 标记版本未知并阻断 |
| 数据类型与形状 | 现有内核能否接收 | 读取节点签名与推导结果 | 转入设备必测或阻断 |
| 属性与出现位置 | 是否触发特殊实现 | 保存节点属性和图路径 | 不能证明内核适配 |
| 业务覆盖状态 | 真实路径是否执行过 | 绑定测试用例回执 | 记录为未覆盖而非通过 |
运行时能力清单要从真实构建生成
运行时允许列表不应来自通用产品文档,因为同一版本可以有完整构建、移动裁剪构建和团队自定义构建。门禁需要读取实际进入应用候选的运行时产物,导出已注册算子、内核签名、执行提供程序、ABI 和构建选项,并对结果计算摘要。若工具链无法从二进制可靠导出,就应在构建阶段生成机器可读清单,并让清单与产物进入同一来源证明。
匹配过程至少分三层。第一层检查模型中的 domain 和算子是否存在;第二层检查 opset、类型、形状或属性约束是否落在内核支持范围;第三层检查应用实际启用的执行路径是否包含所需组件。任何一层无法判断,都应输出明确的 unknown,而不是把不存在的证据解释为兼容。发布系统可以允许人工复核 unknown,但人工结论仍要写明依据、适用候选和到期条件。
裁剪规则本身也要版本化。仅保存一份算子白名单会丢失生成器版本、默认内核、平台条件和自动扩展规则。建议保存原始规则、规范化规则、生成工具版本、输入模型集合以及最终能力清单,并分别计算摘要。这样才能解释一次运行时升级后,算子清单相同却出现行为差异的原因,也能发现构建过程中未预期的能力漂移。
delegate 分区和回退必须单独验收
LiteRT delegate architecture 说明 delegate 可以只接管计算图的一部分,未支持节点仍由主运行时执行,并可能在执行器边界发生数据传递。看到 GPU、NPU 或其他 delegate 初始化成功,只能证明后端对象可创建,不能证明目标模型的关键子图已经被接管。验收应导出实际分区结果,记录哪些节点进入 delegate、哪些留在主图,以及边界张量的类型和形状。
回退路径要区分预期回退和异常回退。预期回退是架构允许某些节点留在主运行时,并已经纳入包体与性能预算;异常回退则是本应委派的节点因版本、类型或设备能力不匹配而返回主执行器。二者都可能得到正确输出,但发布判断不同。前者需要证明主运行时保留了对应内核,后者需要调查配置或设备差异,不能用“结果还能出来”掩盖部署偏离。
跨执行器边界还会改变内存、线程和延迟形态,但公开资料不能替具体设备给出收益结论。工程验收可以记录分区数量、边界数量、实际后端、回退原因和业务输出,再与同候选的基线比较。没有同设备、同输入和同热身条件时,只能记录执行事实,不应宣称裁剪或委派带来性能提升。
| 状态 | 观察事实 | 发布判断 | 需要的后续证据 |
|---|---|---|---|
| 完整委派 | 目标子图全部进入后端 | 继续做数值与设备回归 | 输出对照和代表设备回执 |
| 预期部分委派 | 白名单节点进入后端,其余留在主图 | 核对主运行时内核未被裁掉 | 分区清单和回退路径测试 |
| 异常回退 | 预期节点返回主执行器 | 不能按目标部署放行 | 版本、类型和设备能力诊断 |
| 委派失败 | 后端创建或图准备失败 | 阻断该设备组合 | 失败码、配置和候选身份 |
| 结果偏差 | 执行完成但业务断言不一致 | 阻断并定位数值边界 | 固定输入、基线和容差规则 |
自定义算子和自定义层是双边接口
当标准运行时没有所需内核时,团队可能通过自定义算子、delegate 扩展或 Core ML 自定义层补齐能力。Core ML custom layers 说明自定义层实现位于应用代码中,并与模型依赖绑定。MLCustomLayer protocol 进一步规定初始化、权重、输出形状、CPU 计算和可选 GPU 编码等接口。由此可见,自定义层兼容性不是模型文件或代码文件单边属性,而是两者在具体版本上的接口契约。
清单应记录自定义标识、接口版本、实现摘要、权重文件摘要、支持的数据类型、形状规则、线程假设、CPU 路径和可选加速路径。应用代码经过优化、混淆或保护后,注册名称、构造方式、资源路径和调用约定都要保持。资料只能说明协议要求,不能证明任意代码变换后数值与线程行为必然一致,因此自定义实现必须进入同候选的动态回归。
失败处理要避免静默替换。如果自定义实现缺失、版本不符或权重加载失败,应用应产生可诊断的错误并停止使用该模型组合,不能悄悄切到未登记实现。若产品设计允许回退到标准模型或服务端路径,回退目标也要有独立身份、授权规则和业务提示。兼容门禁只判断执行条件,不能代替模型来源、用户授权或隐私策略。
| 契约项 | 模型侧 | 应用侧 | 验收动作 |
|---|---|---|---|
| 标识与版本 | 层或算子声明 | 注册表和实现版本 | 逐项匹配并拒绝未知版本 |
| 权重 | 权重名称与形状 | 加载器和文件摘要 | 核对完整性与加载结果 |
| 形状 | 输入输出张量约束 | 输出形状推导 | 覆盖动态尺寸和边界输入 |
| 计算路径 | 数值语义 | CPU 与可选加速实现 | 固定输入对照输出 |
| 线程与生命周期 | 调用时序假设 | 对象创建和资源释放 | 重复调用与后台切换测试 |
| 失败语义 | 不可执行条件 | 错误与回退策略 | 确认不会静默使用未知实现 |
元数据和关联文件也会改变可执行条件
LiteRT model metadata 说明模型元数据可以描述输入输出、归一化参数、标签和关联文件。算子清单匹配并不代表调用协议匹配。如果应用仍按旧尺寸、旧颜色顺序、旧 tokenizer 或旧标签解释输入输出,运行时可能正常执行,业务结果却已经失真。因此兼容包要把模型、元数据、预处理、后处理和关联文件作为同一交付集合。
建议为文件集合生成 packageDigest,并在能力清单中记录每个成员的路径、摘要和用途。模型更新出现新算子时,必须同时确认元数据是否变化;运行时升级若改变输入绑定或动态形状处理,也要重新核对调用代码。只校验主模型文件会留下合法文件混搭的空间,特别是后台逐文件更新或失败重试时。
元数据核对属于接口一致性,不是安全能力证明。它不能说明模型来源可信、文件经过授权,也不能证明输出适合具体业务。工程上应把来源校验、算子兼容、接口一致和业务验收拆成不同门禁。它们可以共享同一个 packageDigest 和候选标识,但每一列都要保存独立证据,避免一次模型加载成功被扩大成全部通过。
| 成员 | 影响的接口 | 静态检查 | 动态检查 |
|---|---|---|---|
| 主模型 | 图、权重和算子 | 摘要、格式与需求清单 | 装载和业务分支执行 |
| 模型元数据 | 张量说明和归一化 | 字段、版本与摘要 | 调用参数与实际读取确认 |
| Tokenizer 或词表 | 文本到标识的映射 | 文件集合和版本 | 固定语料编码对照 |
| 标签与后处理 | 输出解释和阈值 | 顺序、语言和规则摘要 | 固定输出的业务解释 |
| 自定义权重 | 自定义层参数 | 名称、形状与摘要 | 初始化和计算路径 |
| 运行配置 | delegate 与回退 | 配置摘要和允许列表 | 实际分区与失败语义 |
用只读脚本先做算子集合阻断
静态门禁适合在构建或发布前快速发现确定性缺口。输入可以是模型扫描器导出的需求清单和运行时构建导出的能力清单。脚本要先核对文件结构与模型摘要,再比较标准算子、自定义实现和允许回退节点。缺少标准内核且没有登记自定义实现的节点必须失败;声明回退但主运行时同样没有内核的节点也必须失败。
下面的 Python 校验器只读取两份 JSON,不接触模型权重、客户数据或内部路径。它要求需求项带有 domain、operator、opset 和 types,能力项声明内置签名、自定义实现及允许回退集合。输出把每个节点分为 builtin、custom 或 fallback,并对未知节点返回非零状态。真实工程还应加入形状、属性与版本约束,但不能在缺少解析能力时默认兼容。
这段代码的结论边界很明确:通过表示清单层面存在一条登记的执行路径,不表示目标设备一定能创建 delegate,也不表示动态形状、自定义权重、数值输出、线程行为或业务结果通过。静态结果应绑定输入文件摘要,随后由设备回执补充运行证据。任何人工豁免都应写明候选、原因、到期和补测要求。
from pathlib import Path
import hashlib
import json
import sys
if len(sys.argv) != 3:
raise SystemExit(2)
requirements_path = Path(sys.argv[1])
capabilities_path = Path(sys.argv[2])
if not requirements_path.is_file() or not capabilities_path.is_file():
raise SystemExit(2)
requirements_raw = requirements_path.read_bytes()
capabilities_raw = capabilities_path.read_bytes()
requirements = json.loads(requirements_raw.decode("utf-8"))
capabilities = json.loads(capabilities_raw.decode("utf-8"))
if not isinstance(requirements.get("operators"), list):
raise SystemExit(2)
if not isinstance(capabilities.get("builtin"), list):
raise SystemExit(2)
custom = set(capabilities.get("custom", []))
fallback = set(capabilities.get("fallback", []))
builtin = set()
for item in capabilities["builtin"]:
fields = (item.get("domain"), item.get("operator"), item.get("opset"), tuple(item.get("types", [])))
if not all(fields[:3]) or not fields[3]:
raise SystemExit(2)
builtin.add(fields)
results = []
missing = []
for node in requirements["operators"]:
key = (node.get("domain"), node.get("operator"), node.get("opset"), tuple(node.get("types", [])))
node_id = str(node.get("nodeId", ""))
if not node_id or not all(key[:3]) or not key[3]:
raise SystemExit(2)
if node_id in custom:
route = "custom"
elif node_id in fallback and key in builtin:
route = "fallback"
elif key in builtin:
route = "builtin"
else:
route = "missing"
missing.append(node_id)
results.append({"nodeId": node_id, "route": route})
report = {"requirementsSha256": hashlib.sha256(requirements_raw).hexdigest(), "capabilitiesSha256": hashlib.sha256(capabilities_raw).hexdigest(), "results": results, "missing": sorted(missing)}
print(json.dumps(report, ensure_ascii=False, indent=2))
if missing:
raise SystemExit(3)设备回归要覆盖真实组合和失败分支
静态匹配通过后,应从实际发布范围选择设备、系统版本、ABI 和加速后端,安装同一应用候选并装载同一模型包。每次回执至少记录应用摘要、模型 packageDigest、运行时能力摘要、设备类别、实际 delegate、图分区摘要、输入用例、输出断言和失败日志摘要。缺少这些身份字段的截图或口头结论无法证明测试对象。
用例既要覆盖正常主路径,也要覆盖动态形状、批量变化、语言差异、空输入、边界尺寸、自定义层、delegate 不可用和主动回退。运行时裁剪最危险的情况不是启动即失败,而是罕见路径缺少内核。因此测试设计应把静态算子位置映射到可触发的业务输入,并允许对尚未覆盖的节点明确保留 unknown 状态。
版本升级需要双向矩阵。新模型对旧应用候选要么被明确拒绝,要么有证据证明兼容;旧模型对新运行时也要验证保留窗口内的回退路径。任何模型、运行时、delegate、自定义实现或元数据变化都应重新计算矩阵。矩阵不是要求穷举所有设备,而是让抽样依据、未覆盖边界和发布风险可见。
| 维度 | 代表组合 | 关键观察 | 失败后的决定 |
|---|---|---|---|
| 运行时版本 | 当前版与计划升级版 | 能力摘要和装载结果 | 区分模型缺口与运行时变化 |
| delegate | 启用、不可用和主动关闭 | 实际分区与回退原因 | 未知回退不得按目标部署放行 |
| 设备能力 | 代表 ABI 与加速后端 | 内核选择和错误语义 | 限定设备范围或修正构建 |
| 模型变体 | 主模型与设备变体 | 算子清单和输出断言 | 为每个变体独立登记 |
| 自定义实现 | 标准路径与自定义层路径 | 初始化、形状和数值对照 | 缺失实现时明确阻断 |
| 失败恢复 | 加载失败与回退目标 | 用户状态和模型身份 | 拒绝静默使用未知模型 |
发布判断必须写清事实、限制和下一步
一份可执行的兼容报告应分开列出静态事实、设备观察、工程判断和未验证项。静态事实包括模型与运行时清单是否匹配;设备观察包括实际 delegate、分区、回退和业务断言;工程判断说明抽样为何足以支持当前发布范围;未验证项则明确动态形状、设备类别或业务分支的缺口。只有前三部分互相绑定且没有阻断项时,才能对特定组合登记通过。
兼容通过也有严格边界。它不证明模型文件保密,不证明授权服务正确,不证明更新链防回滚,也不证明任何保护产品的效果。ONNX Runtime Mobile、Google AI Edge LiteRT、Core ML custom layers 等一手资料支持平台接口与运行结构,不是当前项目的实测回执。没有同一候选上的真实记录,就应使用待验证、未覆盖或不适用,不写性能数字、客户结论和攻击阻断承诺。
准备评估时,团队应提交最终模型摘要、算子需求清单、实际运行时能力清单、delegate 策略、自定义实现版本、完整模型包清单和目标设备范围。御盾中央平台的申请入口用于提交接入需求和约束,具体保护范围仍以项目评审和同候选证据为准。先把这些材料整理齐全,通常比反复更换裁剪白名单更快找到真正缺口。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| ONNX 移动部署的兼容关系受到模型算子、移动运行时包和执行提供程序共同约束。 | ONNX Runtime Mobile 说明 Android 和 iOS 上的模型装载、算子要求与移动运行时构建关系。 | 资料不证明当前模型已通过,也不提供模型签名、授权、防回滚或业务输出保证。 |
| LiteRT 提供端侧模型加载与通过不同 delegate 执行推理的运行结构。 | Google AI Edge LiteRT 描述端侧推理运行时及其模型执行入口。 | 平台能力说明不代表任意裁剪构建包含目标模型的全部算子内核。 |
| 模型元数据可以携带输入输出描述、归一化参数、标签和关联文件。 | LiteRT model metadata 描述模型元数据字段和关联文件用途。 | 元数据完整不证明模型来源可信、算子兼容或业务输出可接受。 |
| delegate 可以只接管部分节点,未支持节点仍由主图执行并形成执行器边界。 | LiteRT delegate architecture 描述子图委派、未支持节点和跨执行器数据传递。 | 架构事实不预设具体设备上的分区结果、回退原因或性能收益。 |
| Core ML 自定义层实现位于应用代码中,并与模型中的自定义层依赖绑定。 | Core ML custom layers 说明 Swift、Objective-C 或 Metal 实现与模型集成方式。 | 应用代码存在不证明自定义层在变换后保持数值、线程或资源行为。 |
| Core ML 自定义层协议包含初始化、权重、输出形状、CPU 计算和可选 GPU 编码接口。 | MLCustomLayer protocol 列出自定义层实现需要遵循的运行接口。 | 协议符合性不能替代同一候选上的真实输入输出和生命周期测试。 |
| 裁剪后兼容结论应绑定模型摘要、运行时能力摘要、delegate 配置和应用候选。 | 工程判断:这些输入任一变化都可能改变可用内核、图分区、自定义实现或回退路径。 | 身份绑定提供可追溯性,不自动证明设备兼容、性能或业务正确性。 |
| 静态算子集合匹配只能作为发布前门禁,不能替代设备动态回归。 | 工程判断:动态形状、属性组合、delegate 创建、自定义权重和罕见业务分支需要运行证据。 | 本文不给出任何具体项目的通过结论、设备覆盖结论或性能数字。 |
工程常见问题
模型在完整运行时能运行,裁剪后为什么还要重新验收?
裁剪会改变实际编译进应用的内核、执行提供程序和回退能力。模型文件即使不变,模型与新运行时构建的兼容组合已经变化,旧报告不能直接继承。
只比较算子名称是否足以判断兼容?
不足。还要比较 domain、opset、数据类型、张量形状、属性组合和内核版本。名称存在但具体签名没有实现时,图准备或特定输入仍可能失败。
delegate 初始化成功是否说明硬件加速已经生效?
不能。需要读取实际图分区,确认哪些节点进入 delegate、哪些节点回退主运行时以及回退原因。初始化成功只覆盖后端对象创建。
自定义算子已经注册,为什么还要做输入输出对照?
注册只证明标识可以解析。权重、形状推导、数据类型、线程生命周期、CPU 或加速实现仍可能与模型不一致,需要固定输入和业务断言验证。
静态校验脚本通过后能否直接发布?
不能。静态通过表示清单中存在登记的执行路径,仍需在目标设备上验证模型装载、实际 delegate、回退、自定义层、动态形状和关键业务分支。
申请移动 AI 运行时兼容评估前要准备什么?
准备最终模型与摘要、算子需求清单、实际运行时能力清单、delegate 策略、自定义实现版本、模型关联文件、应用候选和目标设备范围,再通过御盾中央平台提交申请。