工程实践

云端 Mac 上的 Core ML 模型编译与应用包验收

云端 Mac 上的 Core ML 模型编译与应用包验收

一个图像分类模型在本地调试正常,归档也显示成功,交付后的应用却在首次推理时找不到资源。常见原因不是模型算法,而是模型没有进入目标、被脚本复制到错误目录,或同名旧版本仍留在应用包里。云端 Mac 流水线需要验证的因此不只是“能否构建”,还包括模型源码能否独立编译,以及最终交付包里到底装了什么。

把模型验收拆成两道门

第一道门直接处理 .mlmodel.mlpackage,排除模型格式、工具链和生成目录问题。第二道门在 Xcode 归档完成后检查 .app,确认编译后的 .mlmodelc 确实进入交付目标。两道门必须分开:若只运行 xcodebuild,失败时很难迅速判断问题来自模型编译、目标成员关系,还是资源复制阶段。

开始前固定开发者目录,并记录工具版本:

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
xcodebuild -version
xcrun --find coremlcompiler
swift --version

ArmVMS 上若安装了多个 Xcode 版本,应由任务显式设置 DEVELOPER_DIR,不要依赖交互式终端中的 xcode-select 状态。流水线和人工登录会话的环境变量可能不同,显式路径更容易复现。

模型验收的对象应是“指定 Xcode 工具链生成的指定应用包”,而不是某台机器上一次偶然成功的构建目录。

先独立编译 Core ML 模型

为每个模型准备独立输出目录,禁止多个任务共用同一个临时目录。下面的脚本同时兼容 .mlmodel.mlpackage 输入:

MODEL_PATH="${1:?model path required}"
OUTPUT_ROOT="${2:-$PWD/build/coreml}"
MODEL_NAME="$(basename "$MODEL_PATH")"
MODEL_NAME="${MODEL_NAME%.*}"
MODEL_OUTPUT="$OUTPUT_ROOT/$MODEL_NAME"

rm -rf "$MODEL_OUTPUT"
mkdir -p "$MODEL_OUTPUT"

xcrun coremlcompiler compile "$MODEL_PATH" "$MODEL_OUTPUT"

find "$MODEL_OUTPUT" -type f -print0 |
  sort -z |
  xargs -0 shasum -a 256 > "$MODEL_OUTPUT.files.sha256"

du -sk "$MODEL_OUTPUT"

命令成功后至少检查三件事:输出目录非空、文件清单可生成、大小不是零。不要把编译目录直接当长期缓存,因为它与 Xcode 版本、模型内容和编译器行为相关。若要复用,应把模型源码摘要、xcodebuild -version 输出和构建脚本版本共同组成缓存键。

给生成型模型增加输入检查

部分项目在构建前由 Python 或转换工具生成模型。此时要先验证输入文件,再调用 coremlcompiler。生成任务必须写入新目录,全部完成后再原子移动到约定路径,避免 Xcode 读到半成品。

建议保存以下字段作为清单:

字段 用途
模型相对路径 区分主应用与扩展使用的模型
源文件 SHA-256 判断输入是否真正变化
Xcode 版本 解释编译产物差异
编译后大小 发现意外附带资源
目标名称 检查 Target Membership

从归档里确认最终产物

独立编译通过后,再进行干净归档。为避免读取上一次任务的残留,给 DerivedData 和归档路径分配本次任务专属目录:

RUN_ROOT="$PWD/build/model-validation"
DERIVED_DATA="$RUN_ROOT/DerivedData"
ARCHIVE_PATH="$RUN_ROOT/App.xcarchive"

rm -rf "$RUN_ROOT"
mkdir -p "$RUN_ROOT"

xcodebuild \
  -workspace Example.xcworkspace \
  -scheme Example \
  -configuration Release \
  -destination "generic/platform=iOS" \
  -derivedDataPath "$DERIVED_DATA" \
  -archivePath "$ARCHIVE_PATH" \
  archive

随后定位应用包并导出模型清单:

APP_PATH="$(find "$ARCHIVE_PATH/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"

find "$APP_PATH" -type d -name '*.mlmodelc' -print |
  LC_ALL=C sort > "$RUN_ROOT/bundled-models.txt"

test -s "$RUN_ROOT/bundled-models.txt"

这里不要只匹配主应用根目录。模型可能属于扩展,也可能由资源包携带。检查脚本应记录完整相对路径,再按项目定义判断哪些路径允许出现。预期一个模型时发现两个同名目录,应立即查看主应用、扩展和依赖包是否各自复制了一份。

建立不依赖猜测的基线

模型体积受结构和编译工具影响,不能简单规定“增长超过某个统一百分比就失败”。更稳妥的做法是为每个模型保存审核后的基线,同时设置绝对增量与比例增量。小模型适合关注绝对值,大模型则更需要关注比例。

基线文件可以使用简单的制表符格式:

Classifier.mlmodelc 8421376
Embedding.mlmodelc  27156480

比较时还要处理新增、删除和重命名。新增模型不能自动视为错误,但必须要求变更记录说明用途;模型被删除则应阻断,除非清单也经过审核更新。重命名应按“删除旧项并新增新项”处理,避免历史数据被错误继承。

清单应作为构建产物保存,但不要上传模型源码之外的敏感配置。若模型由受控下载步骤取得,只记录摘要、逻辑名称和最终包内路径,不在日志中输出带令牌的下载地址。

常见失败与交付检查项

模型能独立编译却没有进入应用包,优先检查 Target Membership、Copy Bundle Resources、资源包依赖和条件化构建设置。Debug 正常而 Release 缺失时,比较两个配置的资源阶段与自定义脚本输入输出列表。

如果同名模型重复出现,先执行完整相对路径盘点,不要直接删除其中一个。主应用和扩展可能确实分别需要副本;真正的问题是未经设计的重复复制。若归档体积突然增加,还应检查生成目录是否被整体加入资源阶段,把原始模型、临时文件和编译产物一起装进应用。

提交前可按以下顺序验收:

  1. 固定 DEVELOPER_DIR 并记录 Xcode 版本。
  2. 在隔离目录独立运行 coremlcompiler
  3. 为模型源码和编译结果生成摘要清单。
  4. 使用全新的 DerivedData 完成 Release 归档。
  5. .xcarchive 内枚举全部 .mlmodelc
  6. 对照允许路径、预期数量和已审核大小基线。
  7. 保存清单、命令输出与归档标识,便于后续比较。

这套流程不会判断模型精度,也不能替代端到端推理测试。它解决的是更靠前、也更适合自动化的问题:模型是否可编译、是否被正确打包、交付内容是否发生未经解释的变化。把这些结论固定为机器可读清单后,运行时的模型缺失问题就能在归档阶段被发现。

常见问题

为什么不能只看 Xcode 构建是否成功?

构建成功只能说明编译链路没有返回错误,不能证明目标模型已进入最终应用包。仍需解开归档或导出产物,检查 mlmodelc 目录、文件数量和路径。

模型体积变化多少时应该阻断构建?

不要使用适用于所有模型的固定比例。先为每个模型保存已审核基线,再按团队允许的绝对增量与比例增量共同判断,并要求变更提交说明来源。

同名 Core ML 模型出现两份是否一定有问题?

通常应视为需要调查的信号。先确认它们是否属于不同目标或扩展;若最终进入同一应用包,应检查 Copy Bundle Resources、包依赖和生成脚本是否重复复制。

ArmVMS 云端 Mac

为下一次构建选择独享 Apple Silicon 物理节点

按天、周、月或季租用 ArmVMS M4 与 ArmVMS M4 Pro。配置、节点和附加项目在创建订单前逐项确认。

选择配置并创建订单