先看结论与判断条件
- 下载、真实性验证、完整性验证、兼容检查、运行时编译、活动切换和旧版清理必须是可独立失败的阶段。
- 可信清单应绑定版本、过期时间、全部文件摘要、运行时要求和发布证据,不能只校验主模型文件。
- 下载内容始终落在非活动临时目录,只有全部验证与预热通过后才生成不可变候选版本。
- 活动版本宜用同一文件系统内的短小指针原子替换,推理请求先固定一次快照,不能边读边切换。
- 失败回退应保留上一已知可用版本并限制重复尝试,避免服务端持续下发同一坏候选造成启动循环。
- 旧版本清理与启用解耦,只有确认没有进程或请求持有旧快照后才进入容量回收。
把模型更新定义成状态机而不是一次下载
移动端更新模型的风险不在于文件是否能下载,而在于多个阶段可能只完成一半。网络中断会留下截断文件,应用被系统终止会打断编译,清单和关联资源可能来自不同发布批次,切换时另一个线程仍在加载旧版本。若代码只用“文件存在”判断可用,半成品就可能进入推理路径。工程判断是把更新明确拆成发现、授权、下载、验证、兼容检查、编译或准备、候选冻结、原子启用和清理,每个状态都有唯一入口与失败出口。
状态机需要持久化,但持久化的不是模糊布尔值。至少记录目标版本、清单摘要、临时目录、已完成文件、候选目录、失败阶段、失败类别和最后尝试时间。应用重启后只能从可证明的阶段恢复:已下载文件重新做摘要校验,编译结果重新确认与候选匹配,未完成的活动切换则读取当前指针而不是猜测。任何无法解释的状态都回到旧活动版本,并把临时内容隔离等待下一次受控重试。
用户触发、后台任务和推送通知可能同时启动更新,必须由单一协调器串行化同一模型族的激活。锁只保护本地临界区,不能代替版本策略;服务端仍要防止把较旧版本、过期清单或不兼容候选发给客户端。放行证据应显示每次状态转换的输入摘要和结果,而不是只记录“更新成功”,这样故障时才能区分网络、供应链、兼容、编译还是切换问题。
| 状态 | 允许写入的位置 | 进入下一状态的条件 | 失败动作 |
|---|---|---|---|
| 发现候选 | 内存或受控元数据 | 版本策略与清单入口可接受 | 保留活动版本并延迟重试 |
| 下载中 | 非活动临时目录 | 全部声明文件传输完成 | 保留可验证分片或隔离临时目录 |
| 已验证 | 不可变候选目录 | 签名、版本、过期与摘要全部通过 | 候选标记拒绝且不可启用 |
| 准备运行 | 候选专属编译或缓存目录 | 编译、元数据与试加载通过 | 丢弃准备结果并记录失败阶段 |
| 活动切换 | 短小活动指针 | 原子替换完成并可重新读取 | 继续使用旧指针 |
| 稳定观察 | 活动与上一版本并存 | 真实业务探针和错误预算满足 | 原子切回上一已知可用版本 |
可信清单必须先于模型文件进入决策
客户端不能从下载 URL 或文件名推断版本与可信度。The Update Framework specification 提供的更新思路包括验证角色签名、版本、过期时间和一致快照,用于处理回滚、冻结与元数据混搭风险。移动模型不一定直接采用完整 TUF 实现,但工程边界相同:先得到可验证的可信元数据,再依据元数据选择目标文件;不能先下载任意文件,最后只比较一个由同一不可信响应提供的摘要。
清单至少要绑定模型族、发布版本、目标平台、运行时要求、主模型及全部关联文件摘要、文件长度、兼容 schema、发布时间和过期时间。客户端还要保存已接受的最高版本或等价回滚状态,拒绝无授权的旧元数据。设备时间不可靠时,过期判断需要受控时间来源或服务端挑战,不能因为本地时间异常就永久绕过过期检查。任何签名或版本验证失败都应在下载之前停止。
一致性还涉及多文件发布。主模型、tokenizer、标签、归一化配置和业务规则必须来自同一候选清单,不能在缓存命中时随意混用上次文件。清单更新采用新的不可变版本目录,下载器按摘要寻址并在所有文件完成后冻结目录。工程判断是把“候选完整”定义为清单中每个必需成员都存在且摘要匹配,而不是主模型达到预期大小。
| 清单字段 | 解决的问题 | 客户端检查 | 不能替代 |
|---|---|---|---|
| 版本与序列 | 识别未授权回滚 | 高于或等于本地允许下限 | 业务授权和灰度策略 |
| 过期时间 | 限制冻结旧元数据 | 在可信时间边界内有效 | 持续在线可用性 |
| 文件摘要与长度 | 发现截断、替换和混搭 | 逐文件流式计算并比较 | 发布者身份验证 |
| 目标平台与运行时 | 阻止明显不兼容候选 | 平台、架构、运行时版本匹配 | 真实设备试加载 |
| 关联文件集合 | 保持 tokenizer 与模型一致 | 必需成员完整且无未知替换 | 业务输入输出回归 |
| 签名与角色 | 确认元数据授权来源 | 使用受控信任根验证 | 声明内容本身必然正确 |
供应链证明绑定候选身份但不替代运行测试
SLSA Provenance v1.1 说明构建证明可以绑定产物主体、构建者、构建类型、外部参数和依赖材料。对移动模型发布,候选清单可引用模型文件摘要及对应 provenance,发布服务在下发前确认声明主体与实际候选一致。这样评审者能够回答模型由哪个受信构建流程产生、使用了哪些外部输入,而不是只看到一个无法追溯的二进制文件。
in-toto Attestation Statement v1 定义把产物摘要与有类型的声明负载放在同一 statement 中的通用结构。格式解决的是报告与产物如何绑定,不自动保证内容真实。客户端或发布服务仍需验证签名者、声明类型、策略字段和摘要,拒绝把另一个版本的测试报告附到当前模型。工程判断是移动端只接收最小化的放行结论与必要引用,完整供应链材料可由发布后台保存,但二者必须指向同一候选摘要。
provenance 与 attestation 不能证明目标设备上编译成功、输入输出语义正确或性能满足预算。构建来源可接受后,客户端还要执行平台兼容检查、运行时编译或解释器加载、元数据解析和代表性探针。项目没有真实构建证明时,应把来源状态标为未接入,继续做摘要与兼容门禁,但不能把普通 HTTPS 下载扩写成供应链验证已经完成。
| 证据层 | 回答的问题 | 绑定对象 | 明确边界 |
|---|---|---|---|
| 可信清单 | 哪些文件和版本被授权发布 | 清单签名与文件摘要 | 不证明构建过程正确 |
| SLSA provenance | 候选如何由构建流程产生 | 产物主体与构建材料 | 不证明设备运行语义 |
| in-toto statement | 声明负载如何绑定产物 | subject 摘要与 predicate 类型 | 格式不保证陈述真实 |
| 客户端兼容检查 | 候选是否适配当前运行环境 | 设备条件与候选清单 | 静态检查不替代加载 |
| 运行时试加载 | 编译、解析和基本入口是否成功 | 设备、运行时与候选版本 | 探针不代表完整业务回归 |
| 稳定观察 | 真实请求是否满足错误预算 | 活动版本与发布窗口 | 短期成功不代表永久安全 |
全部文件先进入临时目录并逐项验证
下载目录与活动目录必须物理分离。每个候选使用不可猜测但可审计的临时目录,文件下载时使用临时扩展名,写入完成后刷新文件内容与目录元数据,再进行长度和摘要校验。客户端只接受可信清单列出的相对路径,拒绝绝对路径、父目录跳转、符号链接和重复成员,避免关联文件越过候选根目录或覆盖活动指针。
摘要应在流式写入或完成后从磁盘重新计算,而不是信任网络库返回的校验值。若支持分片续传,每片验证只能证明片段未损坏,最终仍要对完整文件计算清单指定摘要。失败文件保留与否取决于是否能够安全复用,任何没有明确摘要身份的残片都不能被下一次请求直接当成已完成。存储空间不足时先停止候选准备,不应删除当前活动版本来换取下载空间。
LiteRT model metadata 说明模型元数据可以携带输入输出描述、归一化参数、标签和关联文件。这个事实意味着更新校验不能只覆盖主模型;tokenizer、词表、标签、预处理参数和其他关联资产都可能改变推理语义。候选完整性表应逐项列出用途、摘要、长度和兼容版本,未知额外文件默认不进入运行目录,缺少必需成员则整个候选失败。
- 下载目录与活动目录完全分离
- 清单路径经过规范化且不能越过候选根目录
- 每个文件从落盘内容重新计算摘要
- 分片完成后仍执行完整文件摘要
- 主模型和全部关联资产属于同一清单
- 空间不足不会删除唯一可用活动版本
兼容检查和运行时准备必须在切换之前
Download and compile Core ML models 描述动态 Core ML 模型先下载到本地、编译,再从编译产物初始化的路径。工程上应把编译输出视为候选派生产物,而不是下载成功的附带结果。输入模型摘要、编译工具或系统版本、编译目录和初始化结果都要与候选版本绑定;只有从编译产物成功创建模型并通过受控探针,候选才进入可切换状态。
其他运行时也有各自兼容条件,包括算子集合、delegate、硬件架构、量化方式、输入 shape 和元数据 schema。静态清单可以提前排除已知不兼容组合,真实设备试加载负责捕捉运行时差异。探针应使用公开安全、固定且不包含用户数据的输入,检查输出结构、有限数值和必要业务不变量;它不能证明全部模型质量,却能阻止无法加载或接口明显变化的候选进入活动路径。
Manage Core AI model caching 说明模型缓存可能随源模型、OS 版本、存储压力和清理策略失效或重建。该语义不能外推到所有 Core ML 或 Android 运行时,但提示团队不要把已有缓存当成永久有效证据。候选启用记录应区分源模型、编译产物和运行时缓存,缓存缺失时能够安全重建,缓存命中时也要确认其来源版本与当前候选一致。
| 门禁 | 输入 | 通过条件 | 失败处理 |
|---|---|---|---|
| 平台约束 | 系统、架构、运行时版本 | 满足清单声明范围 | 不下载或隔离候选 |
| 文件完整性 | 全部文件与关联资产 | 长度和摘要逐项一致 | 拒绝整个候选 |
| 运行时编译 | 源模型与编译环境 | 生成可识别编译产物 | 保留旧活动版本 |
| 模型初始化 | 候选运行产物 | 实例创建且入口可调用 | 记录运行时错误类别 |
| 固定探针 | 无用户数据的受控输入 | 输出结构和不变量符合预期 | 候选标记不兼容 |
| 缓存对应 | 源摘要、系统与缓存身份 | 命中条件与当前候选一致 | 安全重建而非误用旧缓存 |
活动指针只在全部门禁后原子替换
原子启用不等于把整个大目录瞬间复制到活动位置。更可靠的做法是候选目录一旦验证通过就变为不可变,活动状态只保存一个很小的版本指针。新指针先写入同一文件系统中的临时文件,刷新内容与父目录后,再用原子重命名替换活动指针。推理请求开始时读取一次指针并固定对应目录,整个请求生命周期不再跟随指针变化。
同一文件系统是关键边界,跨卷移动可能退化为复制和删除,不能假定具有原子语义。进程内还要协调并发:激活线程替换指针不应关闭已有模型实例,新的请求读取新版本,旧请求继续持有旧快照直到完成。多进程应用需要使用所有进程都能观察到的指针与版本租约,并在应用重启时重新读取指针和验证候选目录。
下面示例验证 manifest 中全部文件的相对路径、长度和 SHA-256,确认候选版本后,把 JSON 指针写到同目录临时文件并使用 os.replace 原子替换。它不验证发布签名,也没有实现平台模型编译,因此只能解释“完整候选到活动指针”的最后一步。真实应用还要在调用函数前完成可信清单验证、兼容测试和持久化刷新策略,并把 base_dir 限定在应用私有数据目录。
- 候选目录验证完成后保持不可变
- 指针临时文件和活动指针位于同一文件系统
- 替换前刷新指针内容和必要目录元数据
- 每个推理请求固定一次版本快照
- 旧实例不会在处理中被强制关闭
- 应用重启后从活动指针恢复而非猜测版本
from pathlib import Path
import hashlib
import json
import os
import sys
if len(sys.argv) != 3:
raise SystemExit("usage: activate.py model_root manifest.json")
model_root = Path(sys.argv[1]).resolve()
manifest_path = Path(sys.argv[2]).resolve()
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
version = manifest.get("version")
files = manifest.get("files")
if not version or not isinstance(files, list) or not files:
raise SystemExit("manifest has no versioned file set")
candidate = (model_root / "candidates" / version).resolve()
if candidate.parent != (model_root / "candidates").resolve():
raise SystemExit("candidate path escaped the model root")
for entry in files:
relative = Path(entry.get("path", ""))
expected = entry.get("sha256", "")
if relative.is_absolute() or ".." in relative.parts:
raise SystemExit(f"unsafe manifest path: {relative}")
target = (candidate / relative).resolve()
if candidate not in target.parents or not target.is_file():
raise SystemExit(f"candidate file missing: {relative}")
if target.stat().st_size != entry.get("length"):
raise SystemExit(f"candidate length mismatch: {relative}")
actual = hashlib.sha256(target.read_bytes()).hexdigest()
if actual != expected:
raise SystemExit(f"candidate digest mismatch: {relative}")
pointer = model_root / "active.json"
staged = model_root / "active.json.next"
staged.write_text(json.dumps({"version": version}), encoding="utf-8")
with staged.open("rb") as handle:
os.fsync(handle.fileno())
os.replace(staged, pointer)
print(f"activated verified model version {version}")失败回退与坏版本抑制要提前设计
最安全的失败动作通常是继续使用上一已知可用版本,而不是立即删除候选或退回应用内置模型。上一版本必须在启用新版本后保留一段稳定观察期,且其文件、编译产物和必要关联资产保持完整。若新版本在真实请求中出现加载失败、输出不变量破坏或错误预算越界,回退只需把活动指针原子切回已验证版本,新请求使用旧版本,正在执行的请求按原快照完成。
坏版本需要抑制,否则服务端持续返回同一候选会造成下载、编译、失败和回退循环。客户端可记录候选摘要、失败阶段、错误类别、设备兼容条件和下一次允许重试时间,只有清单版本、候选摘要、运行环境或服务端策略发生可解释变化后才重试。记录不应包含模型内容或用户输入,服务端也不应把客户端单次失败自动扩大成所有设备不兼容。
回退本身也受版本策略约束。TUF 风格的回滚保护用于拒绝未授权旧元数据,不应阻止客户端在本地两个已授权候选之间执行安全回退。工程判断是分别维护“可接受发布版本集合”和“当前活动版本”,回退目标必须仍在授权集合、未过期策略允许范围内且具有本地完整证据。没有目标版本文件或证据时,不能只修改字符串指针。
| 失败阶段 | 旧版本状态 | 候选处理 | 下一次重试条件 |
|---|---|---|---|
| 清单验证 | 保持活动 | 拒绝且不下载 | 可信元数据或信任策略变化 |
| 文件下载 | 保持活动 | 保留可验证分片或隔离 | 网络恢复且候选摘要未变 |
| 摘要校验 | 保持活动 | 标记候选不可用 | 清单或文件摘要变化 |
| 编译或试加载 | 保持活动 | 记录设备兼容失败 | 运行环境或候选发生变化 |
| 切换指针 | 旧指针继续有效 | 重新读取当前活动状态 | 本地存储状态恢复 |
| 真实请求观察 | 原子切回旧版 | 坏版本进入抑制列表 | 新候选或明确策略解除 |
旧版本清理要晚于引用释放和证据归档
清理与启用如果在同一临界区执行,最容易把回退能力和在途请求一起删除。新版本切换后,旧模型实例可能仍被当前请求、后台任务或另一个进程持有。版本目录应有租约或引用记录,清理器只处理不再活动、没有有效租约、已过稳定观察期且不属于保留回退集合的候选。无法确认引用状态时宁可延迟清理,也不能凭目录修改时间推断无人使用。
缓存需要单独建账。Manage Core AI model caching 指出缓存可能因源模型、系统版本、存储压力或清理策略失效,因此缓存不应承担唯一模型副本或回退证据的职责。清理时先区分可信源文件、运行时编译产物、可重建缓存和下载残片;存储压力下优先移除可重建、非活动且无租约的内容,唯一已知可用版本应受到保护。
发布记录最终应绑定清单摘要、全部文件摘要、供应链声明引用、目标设备条件、编译或加载回执、活动指针变更、回退演练和清理边界。结论只能写到真实证据覆盖的范围,例如指定候选在列出设备与状态机路径上通过,不能宣称所有设备永久无故障。准备评估时,可通过御盾中央平台提交模型清单、关联文件、运行时矩阵和更新状态机,由双方先固定候选身份与验收范围。
- 活动版本和上一已知可用版本不会被容量清理
- 跨线程与跨进程引用都有可判断租约
- 源模型、编译产物、缓存和残片分类清楚
- 清理失败不会影响当前推理路径
- 回退演练完成后再缩短旧版保留窗口
- 发布与清理记录都绑定同一候选摘要
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 动态 Core ML 模型需要下载到本地、编译,并从编译产物初始化。 | Download and compile Core ML models 描述了下载、编译和加载动态 Core ML 模型的官方路径。 | 该流程不自动定义发布者认证、版本授权、完整关联文件或回滚策略。 |
| 更新客户端需要处理签名、版本、过期和一致快照等元数据验证。 | The Update Framework specification 定义了面向回滚、冻结和元数据混搭风险的更新验证角色与流程。 | TUF 是通用更新框架,不直接决定移动模型的业务授权、灰度和性能门禁。 |
| 构建证明可以把产物主体与构建者、构建类型、外部参数和依赖材料关联。 | SLSA Provenance v1.1 描述 provenance 的主体和构建相关字段。 | provenance 只能说明记录的构建过程,不能单独证明目标设备运行安全或模型质量。 |
| 供应链声明可以用产物摘要绑定有类型的声明负载。 | in-toto Attestation Statement v1 定义 subject 与 predicate 的通用 statement 结构。 | 声明格式不保证内容真实,仍需验证签名者、策略、声明类型和实际摘要。 |
| 模型缓存可能随源模型、系统版本、存储压力和清理策略失效或重建。 | Manage Core AI model caching 描述 Core AI 模型专用化与缓存管理的相关语义。 | Core AI 缓存语义不能直接外推到全部 Core ML、LiteRT 或其他移动运行时。 |
| 模型元数据可能包含输入输出说明、归一化参数、标签和关联文件。 | LiteRT model metadata 描述模型 metadata 与关联文件可承载的内容类型。 | 元数据存在不等于内容可信,主模型摘要通过也不能覆盖未列出的关联资产。 |
| 活动切换应发生在全部文件验证和运行时准备之后,并只替换短小活动指针。 | 工程判断:不可变候选目录加同文件系统原子指针替换,可以缩小半成品进入推理路径的窗口。 | 具体文件系统、跨进程可见性、持久化和原子语义必须由目标平台项目测试确认。 |
| 失败回退、坏版本抑制和旧版本清理必须与新版本启用解耦。 | 工程判断:分离状态可以保留上一已知可用版本,并避免同一坏候选持续触发更新循环。 | 回退窗口、错误预算、租约和存储策略需要真实业务与设备证据,本文不提供通用数值。 |
工程常见问题
模型文件 SHA-256 正确后能否立即启用?
不能。摘要只证明文件与可信清单中的目标一致,还要验证清单授权、版本和过期状态、全部关联资产、平台兼容、运行时编译或加载及固定探针。
为什么不能直接把新模型覆盖到当前活动路径?
覆盖过程中进程终止、存储不足或并发读取会留下混合状态。应先在非活动目录完成候选,再原子替换短小指针,让在途请求继续持有旧快照。
模型主文件验证通过后,tokenizer 和标签可以继续用旧版吗?
不能默认混用。关联文件可能改变输入处理和输出解释,必须由同一可信清单声明并逐项校验。只有显式兼容策略和业务回归支持时才能复用。
SLSA provenance 或 in-toto 声明能否替代设备端回归?
不能。它们把构建过程或声明负载与产物身份绑定,但不证明特定设备能编译、加载并正确执行模型。设备兼容和业务探针仍需独立完成。
新版本激活后应立即删除旧版本吗?
不应立即删除。旧版本需要覆盖稳定观察和安全回退,在途请求或其他进程也可能仍持有引用。只有租约释放且清理条件满足后才回收。
申请移动端模型更新安全评估前需要准备什么?
准备可信清单、模型和关联文件、构建证明引用、运行时矩阵、更新状态机、失败回退方案及代表性候选,再通过御盾中央平台提交申请。