先看结论与判断条件

  • 输入契约的最小单位不是一个 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 位置会改变有效序列。门禁样例应使用可辨识的小型输入,核对每个轴和边界位置,而不是只喂随机数据观察是否崩溃。

shape 漂移的典型表现与定位证据
现象可能原因静态检查设备检查
元素数相同但输出异常轴顺序或宽高互换比较 axisSemantics使用方向可辨输入
短输入正常长输入失败动态轴范围或内存策略缺失检查上下限和乘积覆盖边界与越界
多输入模型偶发错误相关输入尺寸未联动验证跨输入约束同步变更输入组合
更新后仅部分设备异常运行时或内存路径不同绑定候选与运行时扩大设备矩阵
模型装载成功但首轮失败缓冲区 shape 与模型描述不符重算容量记录首轮绑定结果

dtype 与量化参数决定缓冲区中的真实数值

dtype 不只是 API 中的枚举值,它决定每个元素占用的字节数、数值范围和转换方式。应用把浮点数组写入需要整数输入的模型时,有些绑定层会直接报错,有些适配代码可能先发生隐式转换。后者更危险,因为流程继续运行,却可能使用错误的比例和舍入策略。门禁应从运行时或模型描述读取目标类型,再检查调用端实际分配与写入的元素类型,禁止依靠语言容器名称推断底层字节。

量化输入还要绑定 scale、zeroPoint、是否对称、每张量还是每通道量化,以及原始值域。把像素直接转换为整数不等于正确量化;调用端必须按模型约定把实数映射到量化域,并处理截断和舍入。若模型元数据没有这些参数,团队应从受控导出记录获得依据,并在契约中标明来源。没有可追溯参数时,静态代码看起来合理也不能成为放行证据。

缓冲区容量应使用受控整数计算。rank、各维度和 dtype 宽度相乘前要检查维度为正、动态值已解析、乘法不会溢出,并确认底层缓冲区至少拥有所需字节。对于带行跨度或像素跨度的相机输入,逻辑元素数与源缓冲区字节数不是同一个概念,需要先按平台格式读取,再写入紧凑模型缓冲区。直接把源地址交给运行时,可能把填充字节当成模型数据。

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 和归一化逐字段比较
  • 缓冲区容量使用元素数和类型宽度重算
  • 错误输入存在可达的非零失败路径
  • 静态结果与设备测试回执共同绑定候选
校验模型与应用输入清单的 Python 发布门禁
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 模型加固评估前需要准备哪些输入资料?

准备当前应用候选、模型及关联文件摘要、完整输入契约、预处理实现说明、目标运行时、原生或自定义算子清单和设备回归范围,再通过御盾中央平台发起申请。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: 移动 AI 应用的运行时安全边界