先看结论与判断条件
- 输入契约的最小单位不是一个 shape 字符串,而是名称、维度语义、动态轴、dtype、layout、量化参数、归一化规则和调用端缓冲区的组合。
- 静态维度相等不代表输入正确,NHWC 与 NCHW、RGB 与 BGR、像素域与归一化域都可能在不崩溃的情况下产生错误结果。
- 动态维度必须声明允许变化的轴、上下限、填充与截断规则,不能把未知维度解释为任意输入均可接受。
- 模型元数据、应用预处理代码和运行时实际张量描述需要三方对照,任何一方缺席都会让发布结论失去可复核性。
- 设备测试负责证明候选应用在真实运行时上的装载与拒绝语义,单一设备通过不能外推到全部 API、ABI 和厂商环境。
- 门禁输出必须绑定模型摘要、应用候选、运行时和契约版本;没有当前项目回执时,只能陈述方法和待验证项,不能声称兼容通过。
发布门禁要拦住接口漂移,而不是等崩溃报警
移动端模型能被运行时打开,不等于应用送入的张量满足模型语义。shape 说明维度结构,dtype 决定每个元素的编码与字节宽度,layout 解释各维度分别代表批次、高度、宽度还是通道。三者任何一项错位,都可能在分配缓冲区时没有异常,却让像素、序列或特征落入错误位置。真正的发布门禁应在候选进入生产前发现这类静默错误,而不是把线上输出异常当作第一条证据。
输入问题通常跨越模型文件、预处理代码和运行时适配层。模型声明需要四维浮点输入,应用可能从相机得到带行跨度的字节缓冲区,再由 Kotlin、Swift、C++ 或 GPU 管线完成裁剪、旋转、颜色转换和归一化。只检查模型文件无法证明调用端按相同规则生产字节;只检查业务代码也无法确认新模型仍接受旧约定。门禁必须把两侧声明变成同一种可比较记录。
可执行的停线条件应当具体。输入名称或索引找不到、静态维度不等、动态轴没有策略、元素类型不匹配、缓冲区长度计算溢出、layout 未声明、颜色顺序或归一化参数来源不明,都应让候选停在待修复状态。若平台运行时只能在装载后提供部分描述,静态门禁可以标记待设备核验,但不能把未知字段自动填成默认值并继续发布。
| 对象 | 必须记录 | 核验动作 | 失败处置 |
|---|---|---|---|
| 模型声明 | 名称、rank、shape、dtype | 读取模型或元数据 | 缺字段即停线 |
| 维度语义 | 动态轴与每轴含义 | 比较契约与调用代码 | 未知轴进入待确认 |
| 字节布局 | layout、通道顺序、行跨度 | 重算偏移和容量 | 不一致拒绝候选 |
| 数值变换 | 缩放、均值、标准差、量化参数 | 比对元数据与预处理 | 来源不明不得放行 |
| 运行证据 | 应用候选、模型摘要、设备与结果 | 设备端契约测试 | 回执缺失只记未验证 |
先建立唯一且可版本化的输入契约
一份能进入发布流程的输入契约,应为每个输入张量登记稳定名称或位置、rank、各轴含义、静态维度、动态维度约束、dtype、layout、通道顺序、量化比例与零点、归一化参数、允许的缓冲区来源以及失败策略。多输入模型还需要说明输入之间的关联,例如图像尺寸与掩码尺寸是否必须相同,序列长度与 attention mask 是否共享有效区间。字段必须服务运行判断,不能只复制工具界面的展示文本。
契约自身也需要身份。建议将字段按固定顺序序列化后计算 contractDigest,并把它与模型文件摘要、关联元数据摘要、应用候选身份和运行时版本放入同一发布记录。模型字节不变但预处理规则改变时,contractDigest 应变化;应用代码升级而模型不变时,也需要重新比较调用端声明。这样可以明确区分模型更新、代码更新和契约修订,避免报告与候选错配。
动态维度不能用一个负数或通配符草率表示。契约至少要说明哪些轴允许变化、合法下限与上限、维度之间的约束、超限时拒绝还是截断、填充使用什么值,以及长度变化是否影响内存预算。没有这些信息,调用端只能知道尺寸未固定,却不知道什么输入才是合法输入。工程上应把每条动态规则写成可执行条件,并让测试样例覆盖边界内、边界值和越界输入。
| 字段 | 模型侧证据 | 应用侧证据 | 放行判断 |
|---|---|---|---|
| shape | 模型描述或元数据 | 缓冲区构造参数 | 维度及语义均一致 |
| dtype | 运行时张量类型 | 写入元素类型 | 类型与字节宽度一致 |
| layout | 导出约定或元数据 | 索引和通道搬运逻辑 | 轴顺序有明确映射 |
| 动态轴 | 模型允许的未知维度 | 范围和分配策略 | 每个动态轴有约束 |
| 归一化 | 元数据或训练约定 | 预处理实现 | 参数来源可追溯 |
shape 校验必须解释每个轴,而不只是比较数字
两个张量都显示为四维,并不能说明它们能够互换。常见图像输入可能分别采用 batch、height、width、channel 或 batch、channel、height、width;序列模型的二维输入可能是 batch 与 token,也可能把多个特征拼在末轴。门禁应保存 axisSemantics,并据此生成尺寸计算和索引规则。若只能获得数字而无法确定轴含义,正确状态是等待导出说明或调用代码证据,而不是根据经验猜测。
动态 shape 需要把模型能力和产品策略分开。运行时允许某个轴动态变化,只说明图可以接受不同尺寸,并不保证移动设备在任意尺寸下拥有足够内存,也不说明业务结果在不同裁剪比例下可接受。发布契约可设置比模型能力更窄的产品范围,并明确越界时返回输入错误。这样的限制属于工程策略,需要项目测试支撑,不能伪装成平台文档给出的通用上限。
图像方向、裁剪和缩放会改变 shape 语义。相机缓冲区可能先按传感器方向产生,再由预处理旋转为模型方向;若代码先分配目标缓冲区却忘记交换宽高,最终元素数量仍可能相同,内容位置却完全错误。文本模型也会遇到类似问题,截断方向、特殊标记和 padding 位置会改变有效序列。门禁样例应使用可辨识的小型输入,核对每个轴和边界位置,而不是只喂随机数据观察是否崩溃。
| 现象 | 可能原因 | 静态检查 | 设备检查 |
|---|---|---|---|
| 元素数相同但输出异常 | 轴顺序或宽高互换 | 比较 axisSemantics | 使用方向可辨输入 |
| 短输入正常长输入失败 | 动态轴范围或内存策略缺失 | 检查上下限和乘积 | 覆盖边界与越界 |
| 多输入模型偶发错误 | 相关输入尺寸未联动 | 验证跨输入约束 | 同步变更输入组合 |
| 更新后仅部分设备异常 | 运行时或内存路径不同 | 绑定候选与运行时 | 扩大设备矩阵 |
| 模型装载成功但首轮失败 | 缓冲区 shape 与模型描述不符 | 重算容量 | 记录首轮绑定结果 |
dtype 与量化参数决定缓冲区中的真实数值
dtype 不只是 API 中的枚举值,它决定每个元素占用的字节数、数值范围和转换方式。应用把浮点数组写入需要整数输入的模型时,有些绑定层会直接报错,有些适配代码可能先发生隐式转换。后者更危险,因为流程继续运行,却可能使用错误的比例和舍入策略。门禁应从运行时或模型描述读取目标类型,再检查调用端实际分配与写入的元素类型,禁止依靠语言容器名称推断底层字节。
量化输入还要绑定 scale、zeroPoint、是否对称、每张量还是每通道量化,以及原始值域。把像素直接转换为整数不等于正确量化;调用端必须按模型约定把实数映射到量化域,并处理截断和舍入。若模型元数据没有这些参数,团队应从受控导出记录获得依据,并在契约中标明来源。没有可追溯参数时,静态代码看起来合理也不能成为放行证据。
缓冲区容量应使用受控整数计算。rank、各维度和 dtype 宽度相乘前要检查维度为正、动态值已解析、乘法不会溢出,并确认底层缓冲区至少拥有所需字节。对于带行跨度或像素跨度的相机输入,逻辑元素数与源缓冲区字节数不是同一个概念,需要先按平台格式读取,再写入紧凑模型缓冲区。直接把源地址交给运行时,可能把填充字节当成模型数据。
| 检查项 | 错误理解 | 正确证据 | 失败结果 |
|---|---|---|---|
| 元素类型 | 数组能写入即可 | 模型 dtype 与写入类型 | 类型不符停线 |
| 量化参数 | 整数模型只需强转 | scale、zeroPoint 与值域 | 参数缺失待确认 |
| 容量 | 维度乘积就是字节数 | 元素数乘类型宽度 | 容量不足拒绝 |
| 源跨度 | 相机平面必然紧凑 | rowStride 与 pixelStride | 先重排再绑定 |
| 转换失败 | 运行时会自动修复 | 显式错误和测试回执 | 禁止静默回退 |
layout 和预处理错误往往不会触发异常
layout 是输入契约中最容易被遗漏的字段。NHWC 与 NCHW 可以拥有相同的元素总数,甚至在某些尺寸下显示相同数字集合,但线性内存中的索引完全不同。若调用端按通道优先写入、模型按通道末尾读取,运行时通常不会知道业务语义错了。门禁需要把 layout 写成明确枚举,并用索引函数或小型基准样例核对首元素、边界元素和相邻通道的位置。
颜色顺序、缩放区间、均值、标准差、裁剪方式和插值算法都属于输入契约,而不是可以忽略的 UI 细节。LiteRT model metadata 可携带输入输出描述、归一化参数、标签和关联文件,这使元数据成为重要证据来源。不过元数据存在不代表应用实际使用它;发布检查仍要把元数据值与预处理实现对照,并确认新候选没有继续读取旧配置或硬编码常量。
移动图像管线还可能在 CPU、GPU、相机框架和原生层之间切换。相同算法名称在颜色空间、舍入、边界填充和纹理坐标上可能存在实现差异。静态契约负责规定可接受语义,设备测试负责观察当前候选的实际结果。没有当前设备回执时,可以确认字段完整和代码路径可追踪,但不能声称不同后端已经产生一致输出。
跨运行时要统一契约语言,同时保留平台差异
Google AI Edge LiteRT 在端侧加载模型,并可通过不同 delegate 执行推理。门禁不应假设 delegate 会修复输入错误;它首先接收调用端提供的张量和缓冲区,再决定可执行的图分区。契约层可以使用统一字段描述 shape、dtype、layout 和预处理,但运行回执必须记录实际 LiteRT 版本、选用路径和候选应用,避免把框架的一般能力写成某个项目已经通过。
ONNX Runtime Mobile 在 Android 与 iOS 装载模型,同时受模型算子、移动包体和执行提供程序约束。对输入门禁而言,模型能够被该运行时解析,只覆盖格式和运行能力的一部分。应用仍要核对输入名称、类型、维度与预处理,并把执行提供程序变化列为需要回归的条件。运行时支持不会自动提供模型签名、授权或防回滚能力,这些责任不应混入输入契约结论。
Apple Core ML 负责模型集成、编译、装载和预测。Core ML custom layers 还说明自定义层可以由 App 内 Swift、Objective-C 或 Metal 实现,并通过约定接口与模型绑定。输入到达模型之前,业务代码和自定义实现可能继续转换数据,因此门禁需要记录 Core ML 模型描述,也要检查自定义层与调用端接收的形状和类型。保护自定义代码不能证明模型资产、框架运行时或预测结果同时受到保护。
| 运行环境 | 可读取证据 | 仍需应用确认 | 不能推出 |
|---|---|---|---|
| LiteRT | 张量描述与元数据 | 预处理和实际缓冲区 | delegate 自动修复输入 |
| ONNX Runtime Mobile | 输入名称、类型和维度 | 执行路径与转换代码 | 装载成功即业务正确 |
| Core ML | 模型描述与预测接口 | 调用端与自定义层变换 | 框架能力等于项目通过 |
| 统一契约层 | 规范化字段与摘要 | 平台字段映射 | 抹平平台语义差异 |
| 设备回执 | 当前候选运行结果 | 矩阵覆盖和失败样例 | 单机结果代表全部环境 |
设备测试要覆盖正常输入、边界输入和拒绝语义
Android instrumented tests 适合验证依赖真实 Android 运行时、组件和系统 API 的行为。输入契约测试应安装当前应用候选,加载与发布记录同摘要的模型,并从应用真实预处理入口构造张量。测试回执至少绑定候选身份、模型摘要、契约摘要、设备环境、运行时和用例结果。把独立脚本对模型文件的检查结果当成 App 已通过,会遗漏打包、桥接和实际调用路径。
用例需要分成三组。正常组验证代表性输入能够按照契约完成绑定;边界组覆盖动态维度下限、上限、空内容处理、最大合法容量和多输入关联;拒绝组故意提供错误 dtype、错误 rank、越界尺寸、未知 layout 或不完整归一化配置,确认应用在进入推理前返回可识别错误。只验证成功路径无法证明门禁真正会拦截错误输入。
单一设备通过不能代表完整 API、ABI 和厂商矩阵。发布判断应根据产品设备分布选择覆盖范围,并把未覆盖环境标明为限制。若某个平台只在特定后端暴露问题,回执要记录该路径,而不是笼统写成模型兼容。没有真实项目数据时,本文只能给出测试设计和停线规则,不能提供任何候选的性能、兼容率或通过结论。
- 测试回执绑定应用候选、模型摘要和契约摘要
- 从真实预处理入口构造输入张量
- 正常、边界和拒绝用例分别取证
- 错误输入在推理前产生明确失败
- 记录实际运行时与执行路径
- 未覆盖 API、ABI 和设备环境明确列为限制
用只读校验器把输入清单变成发布阻断条件
下面的 Python 校验器读取公开示例形式的 JSON 清单,比较模型声明与应用声明中的输入名称、shape、dtype、layout、归一化参数和缓冲区字节数。它对文件路径没有写操作,也不接触模型权重;任何字段缺失、维度非法、类型未知、契约不一致或容量不足都会以非零状态结束。代码把输入错误转成可重复门禁,便于在构建流水线中阻止错误候选继续流转。
校验器只验证清单内部的一致性,不能替代对模型文件的真实解析,也不能证明应用运行时按照清单执行。生产流程应由可信工具生成模型侧声明,由构建代码生成应用侧声明,再把通过的结果与设备测试回执绑定。若团队手工维护两份内容相同的 JSON,脚本只能证明两份手工记录一致,不能证明记录与二进制真实状态一致。
准备端侧模型的商业加固评估时,应提交当前应用候选、模型摘要、输入契约、预处理实现说明、目标运行时和设备回归范围,再从御盾中央平台发起申请。评估目标是明确哪些业务代码、模型资产和原生算子需要保护,以及保护后如何保持输入契约;在真实候选与回执到位前,不宣称模型保密、兼容通过、性能改善或攻击阻断。
- 模型侧清单由可信解析工具生成
- 应用侧清单从当前候选构建配置生成
- shape、dtype、layout 和归一化逐字段比较
- 缓冲区容量使用元素数和类型宽度重算
- 错误输入存在可达的非零失败路径
- 静态结果与设备测试回执共同绑定候选
from pathlib import Path
import json
import math
import sys
DTYPE_BYTES = {"float32": 4, "float16": 2, "int32": 4, "int8": 1, "uint8": 1}
ALLOWED_LAYOUTS = {"NHWC", "NCHW", "NC", "NT"}
def load_manifest(path_text):
path = Path(path_text)
if not path.is_file():
raise SystemExit(2)
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data.get("inputs"), list) or not data["inputs"]:
raise SystemExit(2)
return data
def normalize(entry):
required = ["name", "shape", "dtype", "layout", "normalization", "bufferBytes"]
if any(field not in entry for field in required):
raise SystemExit(2)
if entry["dtype"] not in DTYPE_BYTES or entry["layout"] not in ALLOWED_LAYOUTS:
raise SystemExit(2)
if not isinstance(entry["shape"], list) or any(not isinstance(value, int) or value <= 0 for value in entry["shape"]):
raise SystemExit(2)
expected_bytes = math.prod(entry["shape"]) * DTYPE_BYTES[entry["dtype"]]
if not isinstance(entry["bufferBytes"], int) or entry["bufferBytes"] < expected_bytes:
raise SystemExit(2)
return {field: entry[field] for field in required}
if len(sys.argv) != 3:
raise SystemExit(2)
model = load_manifest(sys.argv[1])
application = load_manifest(sys.argv[2])
model_inputs = {item["name"]: normalize(item) for item in model["inputs"]}
app_inputs = {item["name"]: normalize(item) for item in application["inputs"]}
if model_inputs.keys() != app_inputs.keys():
raise SystemExit(2)
for name, expected in model_inputs.items():
actual = app_inputs[name]
for field in ["shape", "dtype", "layout", "normalization"]:
if actual[field] != expected[field]:
raise SystemExit(2)
print(json.dumps({"status": "pass", "inputs": sorted(model_inputs)}, ensure_ascii=False))事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 模型元数据可以携带输入输出描述、归一化参数、标签和关联文件。 | LiteRT model metadata 描述了模型元数据及关联文件能够表达的输入输出信息。 | 元数据存在不证明应用实际读取并正确执行其中的预处理规则。 |
| LiteRT 在端侧加载模型,并可通过不同 delegate 执行推理。 | Google AI Edge LiteRT 说明端侧模型加载和 delegate 执行框架。 | 框架能够执行模型不代表输入 layout、dtype 或业务预处理已经正确。 |
| ONNX Runtime Mobile 在 Android 与 iOS 装载模型,并受到算子、包体和执行提供程序约束。 | ONNX Runtime Mobile 给出移动运行时、模型和执行路径的相关约束。 | 运行时支持不提供模型签名、业务授权、防回滚或输入契约通过结论。 |
| Core ML 覆盖模型集成、编译、装载和预测等平台流程。 | Apple Core ML 描述 Apple 平台上的模型集成与预测能力。 | 平台能力不能替代当前应用候选的输入缓冲区和预处理回归。 |
| Core ML 自定义层由 App 内代码实现,并通过约定接口与模型依赖绑定。 | Core ML custom layers 说明 Swift、Objective-C 或 Metal 自定义层与模型的集成方式。 | 自定义层可被调用不证明其输入转换正确,也不证明模型资产或预测结果受到保护。 |
| 依赖真实 Android 运行时、组件和系统 API 的输入语义应通过设备端测试验证。 | Android instrumented tests 说明设备端测试用于验证依赖 Android 运行环境的行为。 | 单一设备通过不能代表完整 API、ABI 和厂商矩阵。 |
| 输入契约应同时绑定模型摘要、应用候选、运行时与预处理版本。 | 工程判断:只绑定模型文件无法确认调用端实际构造的张量与当前候选一致。 | 契约绑定提高可追溯性,不单独证明性能、输出质量或安全强度。 |
| 静态字段比较与设备拒绝用例应作为两类独立发布证据。 | 工程判断:静态检查发现声明漂移,设备测试验证真实桥接、缓冲区和失败语义。 | 两类证据都必须来自当前候选;历史结果不能自动沿用到新模型或新应用版本。 |
工程常见问题
模型能够成功装载,是否说明输入张量契约已经正确?
不能。装载通常只证明格式和部分运行时条件满足,应用仍可能使用错误的 shape 语义、dtype、layout、颜色顺序或归一化参数。需要静态契约比较和真实调用路径测试。
shape 的数字完全相同,为什么还要检查 layout?
因为相同数字可以代表不同轴顺序。NHWC 与 NCHW 的元素总数可能相同,但线性内存索引不同,运行时未必报错,模型读取到的通道和空间位置却会错位。
动态维度在清单中写成任意值是否足够?
不够。应声明允许变化的轴、合法上下限、轴间约束、填充与截断规则、容量预算和越界失败行为,否则调用端无法判断某个具体输入是否合法。
量化模型只要 dtype 写成 int8 就能正确调用吗?
不能。还要核对 scale、zeroPoint、原始值域、舍入和截断规则,并确认应用写入的底层元素类型与缓冲区字节数符合模型约定。
模型元数据已经包含归一化参数,还需要检查应用代码吗?
需要。元数据说明模型侧约定,但应用可能没有读取元数据,仍使用旧配置或硬编码常量。发布门禁要比较元数据、预处理实现和运行时实际张量描述。
申请端侧 AI 模型加固评估前需要准备哪些输入资料?
准备当前应用候选、模型及关联文件摘要、完整输入契约、预处理实现说明、目标运行时、原生或自定义算子清单和设备回归范围,再通过御盾中央平台发起申请。