Skip to content

feat(apple): Core ML improvements (compiled-model cache, ft_callback, iOS 15, requirements) - #1190

Open
anhappdev wants to merge 6 commits into
masterfrom
feat/coreml-quick-wins
Open

anhappdev wants to merge 6 commits into
masterfrom
feat/coreml-quick-wins

Conversation

@anhappdev

@anhappdev anhappdev commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The "Core ML quick wins" from #1188. None of them depend on Core AI. One commit per item:

  • Cache the compiled model. MLModel.compileModel used to run on every mlperf_backend_create. The compiled .mlmodelc now lives in Caches/mlperf_coreml_models (one entry per source path) and is reused while a fingerprint of the source files (path, size, mtime) still matches. An unreadable entry gets recompiled. If caching fails, the backend uses the freshly compiled model. The model URL is built with URL(fileURLWithPath:). URL(string:) returns nil on iOS 15/16 for paths that need percent-encoding (iOS 17+ encodes them silently).
  • Wire up ft_callback. It is called after the prediction when the benchmark sets custom_setting { id: "report-first-token" value: "true" }, and is off by default. Calling it for every benchmark is unsafe: non-token runs don't expect token latencies, and in Offline mode MlperfDriver puts sample.back().index where loadgen's FirstTokenComplete dereferences a SampleMetadata*. No Core ML benchmark sets the flag yet; it's plumbing for a future LLM path. A failed CoreMLExecutor creation now also logs the error.
  • Raise the xcframework minimum to iOS 15.0, to match the app's deployment target.
  • Remove the dead image_classification / image_classification_offline entries (v3_0 MobilenetEdgeTPU). image_classification is only the task_set of the _v2 tasks, not a task id.
  • Refresh models/requirements.txt. It now pins coremltools 9.0, torch 2.7.0 / torchvision 0.22.0 / timm 1.0.15, and tensorflow-macos 2.12.0 (the newest torch and TF that coremltools 9.0 is tested with), on Python 3.11. coremltools 7 changed the default ct.convert target to mlprogram, which breaks the TF scripts' .mlmodel, quantization_utils and NeuralNetworkBuilder calls. So mobilebert.py, mobiledet.py, mobilenet_edgetpu.py and mosaic.py now pass convert_to='neuralnetwork', which keeps their previous output.

Also fixed: when model loading failed, mlperf_backend_create left backendExists = true and leaked backend_data, so every later create was refused. The app aborts on a failed create anyway (LOG(FATAL) in external.cc), so this only affected callers that keep running, such as dev-utils.

Test plan

  • bazel build --config=ios //mobile_back_apple/cpp/backend_coreml:libcoremlbackend (Xcode 26.6): the device slice has minos 15.0 and links CryptoKit.
  • swiftc -typecheck -swift-version 6 for arm64-apple-ios15.0 and arm64-apple-macos13.1, swift-format lint --strict, clang-format 16 --dry-run --Werror, and markdownlint.
  • macOS harness around CoreMLExecutor, with .mlpackage and .mlmodel files under a path containing a space. Checked: cold compile, warm reuse (entry untouched), recompile after the source changes, recovery from a corrupted entry, direct .mlmodelc load (nothing cached), and a clear error for a missing file. Predictions were correct in every case.
  • Real v4_1/apple/mobilenetv4_fp32_NCHW.mlpackage (CPU & GPU, macOS VM): backend create took 266–428 ms before this change and 21–25 ms once cached. The cache entry is 62 MB.
  • Fresh Python 3.11 venv from requirements.txt: TF SavedModel to NeuralNetwork plus fp16 weights, and timm MobileNetV4 to ML Program plus W8 and the experimental W8A8 API, then predict.
  • Real devices on BrowserStack, all with this PR's test package (ios_tests-apple-1080):
    • iPhone 16 Pro, iOS 18 (CI apple-iPhone 16 Pro-18): All tests passed!. The device log shows each ML Program model loaded three times from one Library/Caches/mlperf_coreml_models/<sha256>/model.mlmodelc entry, so compiled models are reused across backend creates.
    • iPhone 13, iOS 15.5 (manual run): +14: All tests passed!, models served from the same cache, and no cache or CoreMLExecutor errors.
    • MobileBERT logs about 900 Espresso::overflow_error lines on iOS 15. The base commit 846c58d logs 928 on the same device model, so they come from the model, not from this PR.

Note: CI's test package doesn't start on iOS 15/16 for any PR. dyld can't find lib_TestingInterop.dylib or _Testing_Foundation.framework: Xcode 26 doesn't embed them, and on iOS 18 they load from elsewhere on the device. For the iOS 15 runs above I copied both from Xcode into Runner.app/Frameworks of the downloaded package. Fixing that in make flutter/ios/test-package is a separate change.

MLModel.compileModel ran on every mlperf_backend_create and the compiled
.mlmodelc was never kept. Keep it in Caches/mlperf_coreml_models, one entry
per source path, and reuse it while a fingerprint of the source files
(path, size, mtime) matches. An unreadable entry is recompiled; a failure
to cache falls back to the freshly compiled model.

Build the model URL with URL(fileURLWithPath:) instead of URL(string:),
which returns nil on iOS 15/16 for paths that need percent-encoding.
MLError now carries its message as localizedDescription.
mlperf_backend_issue_query ignored ft_callback, so time to first token
could not be measured on Core ML. Call it after the prediction when the
benchmark sets custom_setting report-first-token=true. It stays off by
default: non-token benchmarks don't expect token latencies, and in Offline
mode the driver passes a sample index where loadgen reads a pointer.

Also log the error when CoreMLExecutor creation fails.
The app already targets iOS 15.0; the backend still declared 13.1.
image_classification and image_classification_offline (v3_0
MobilenetEdgeTPU) are not task ids in tasks.pbtxt, so these entries were
never used. image_classification is only the task_set of the _v2 tasks.
Pin coremltools 9.0 with the newest torch (2.7.0) and TensorFlow (2.12.0,
via tensorflow-macos) it is tested with, on Python 3.11.

coremltools 7 changed the ct.convert default to mlprogram. The TF scripts
build NeuralNetwork models (.mlmodel, quantization_utils,
NeuralNetworkBuilder), so pass convert_to='neuralnetwork' explicitly.

Document the compiled-model cache and the custom settings the backend reads.
mlperf_backend_create set backendExists before loading the model and
returned nullptr on failure without clearing it or freeing backend_data,
so every later create failed with "Only one backend instance should exist
at a time". Set the flag only once the backend is ready, and free the data
on failure.
@github-actions

Copy link
Copy Markdown

MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅

@sonarqubecloud

Copy link
Copy Markdown

@anhappdev anhappdev changed the title feat(apple): Core ML quick wins (compiled-model cache, ft_callback, iOS 15, requirements) feat(apple): Core ML improvements (compiled-model cache, ft_callback, iOS 15, requirements) Oct 25, 2026
@anhappdev
anhappdev marked this pull request as ready for review October 27, 2026 03:47
@anhappdev
anhappdev requested a review from a team as a code owner October 27, 2026 03:47
@freedomtan

Copy link
Copy Markdown
Contributor

@anhappdev check Core ML's built-in caching mechanism works or not. Or, in other words, does app-level cache pay off or not. If model compilation time is a concern, we can just load compiled model (.modelc/).

@anhappdev

Copy link
Copy Markdown
Collaborator Author

@anhappdev check Core ML's built-in caching mechanism works or not. Or, in other words, does app-level cache pay off or not. If model compilation time is a concern, we can just load compiled model (.modelc/).

@freedomtan The problem is that the app explicitly compiles the model everytime it runs the benchmark, so we need to fix the logic there.
let compiledModelURL = try? MLModel.compileModel(at: url)

anhappdev added a commit that referenced this pull request Oct 27, 2026
…#1191)

## Problem

The Xcode Cloud "Deploy to TestFlight | Archive - iOS" run reports about
30 warnings on every build (31 on #1160 and #1164, 29 on #1190). None of
them come from our code:

- About 13 linker warnings `Search path
'.../Metal.xctoolchain/usr/lib/swift/iphoneos' not found`, one per Swift
pod plus the app.
- About 15 compiler warnings in Flutter plugin sources under
`.pub-cache`: firebase_auth (deprecated APIs, unused variables,
`keyWindow`), firebase_core (pointer types), firebase_storage
(non-exhaustive switch, retroactive `Error` conformance), share_plus
(`keyWindow`), device_info_plus (integer precision).
- `libtool: warning: 'dummy.o' has no symbols`.

## Cause

CocoaPods adds `"${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}"` to
every Swift pod's `LIBRARY_SEARCH_PATHS`, and the url_launcher_ios and
shared_preferences_foundation podspecs add
`$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/`, which also reaches
`Pods-Runner`. On Xcode Cloud the Metal toolchain comes first in
`TOOLCHAINS`, so `TOOLCHAIN_DIR` points into `Metal.xctoolchain`. That
toolchain has no `usr/lib/swift`, so every link step warns.

## Change

In `flutter/ios/Podfile` `post_install`:

- Remove the `$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)` entry
(either spelling, and the old `DT_TOOLCHAIN_DIR` form) from the pod
xcconfigs. The app targets iOS 15, which ships the Swift runtime in
`/usr/lib/swift`, and `/usr/lib/swift` and `$(SDKROOT)/usr/lib/swift`
stay in the search paths. This replaces the Xcode 15 `DT_TOOLCHAIN_DIR`
→ `TOOLCHAIN_DIR` rewrite, which no podspec here needs any more.
- Set `GCC_WARN_INHIBIT_ALL_WARNINGS` and `SWIFT_SUPPRESS_WARNINGS` on
pod targets. CocoaPods' `inhibit_all_warnings!` only covers Objective-C
files, so it would leave the firebase_storage Swift warnings.

Only third-party pod targets are affected. Warnings in our own Runner
code still show up.

## Verification

Local builds of the `Pods-Runner` target (all plugin and Firebase pods,
`-sdk iphoneos`, Release, Xcode 26.6, CocoaPods 1.17.0). The Metal
toolchain is installed and put first in `TOOLCHAINS` to match Xcode
Cloud, which makes `TOOLCHAIN_DIR` point into `Metal.xctoolchain` there
too:

| Podfile | `TOOLCHAINS` | Result | Warnings |
|---|---|---|---|
| `master` | Metal first (like Xcode Cloud) | BUILD SUCCEEDED | 29: 12
`search path ... Metal.xctoolchain ... not found`, 15 plugin warnings,
`dummy.o`, Crashlytics duplicate `-lc++` |
| this PR | Metal first (like Xcode Cloud) | BUILD SUCCEEDED | 0 |
| this PR | default | BUILD SUCCEEDED | 0 |

The full app link (Runner plus the bazel-built backends) wasn't built
locally. The Xcode Cloud archive on this PR is the end-to-end check.
@freedomtan

Copy link
Copy Markdown
Contributor

To avoid compiling every time, we can simply compile a model offline.
.mlpackage .mlmodel -> .mlmodelc/ on either Linux or macOS machine. Provide .mlmodelc. from our server/cloud side.

@freedomtan to check it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants