@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,810 +1,807 @@
|
|
|
1
|
-
# Core ML Swift
|
|
1
|
+
# Core ML in Swift: extended patterns
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Companion to `../SKILL.md`. Baseline iOS 26 and Swift 6.3; lower floors are
|
|
4
|
+
called out per API, and most of this works back to iOS 14 when the newer calls
|
|
5
|
+
are left out.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
|
|
9
|
+
- [An actor that owns models](#an-actor-that-owns-models)
|
|
10
|
+
- [Generated classes](#generated-classes)
|
|
11
|
+
- [A hand-written feature provider](#a-hand-written-feature-provider)
|
|
12
|
+
- [Predictions in async code](#predictions-in-async-code)
|
|
13
|
+
- [Batch inference](#batch-inference)
|
|
14
|
+
- [Stateful sessions with MLState (iOS 18+)](#stateful-sessions-with-mlstate-ios-18)
|
|
15
|
+
- [Pixel buffers](#pixel-buffers)
|
|
16
|
+
- [MLMultiArray](#mlmultiarray)
|
|
17
|
+
- [MLTensor (iOS 18+)](#mltensor-ios-18)
|
|
18
|
+
- [Vision and Core ML together](#vision-and-core-ml-together)
|
|
19
|
+
- [NaturalLanguage models](#naturallanguage-models)
|
|
20
|
+
- [MLComputePlan in detail (iOS 17.4+)](#mlcomputeplan-in-detail-ios-174)
|
|
21
|
+
- [Lifecycle: warm up and release](#lifecycle-warm-up-and-release)
|
|
22
|
+
- [Errors](#errors)
|
|
23
|
+
- [Tests with Swift Testing](#tests-with-swift-testing)
|
|
24
|
+
- [Memory practices](#memory-practices)
|
|
25
|
+
|
|
26
|
+
## An actor that owns models
|
|
27
|
+
|
|
28
|
+
An actor serializes loading, guarantees one instance per model, and keeps
|
|
29
|
+
compiled models on disk between launches.
|
|
28
30
|
|
|
29
31
|
```swift
|
|
30
32
|
import CoreML
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
enum ModelStoreError: Error {
|
|
35
|
+
case missing(String)
|
|
36
|
+
case predictionFailed(String)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
actor ModelStore {
|
|
40
|
+
static let shared = ModelStore()
|
|
41
|
+
|
|
42
|
+
private var loaded: [String: MLModel] = [:]
|
|
43
|
+
private let compiledFolder: URL
|
|
35
44
|
|
|
36
45
|
init() {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
).first!
|
|
40
|
-
cacheDirectory = appSupport.appendingPathComponent("CompiledModels", isDirectory: true)
|
|
41
|
-
try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
|
|
46
|
+
compiledFolder = URL.applicationSupportDirectory.appending(path: "CompiledModels", directoryHint: .isDirectory)
|
|
47
|
+
try? FileManager.default.createDirectory(at: compiledFolder, withIntermediateDirectories: true)
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
func
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
50
|
+
func prepare(_ name: String, settings: sending MLModelConfiguration = MLModelConfiguration()) async throws {
|
|
51
|
+
_ = try await model(name, settings: settings)
|
|
52
|
+
}
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
return model
|
|
54
|
+
func predict(_ name: String, from input: sending MLFeatureProvider) async throws -> sending MLFeatureProvider {
|
|
55
|
+
nonisolated(unsafe) let engine = try await model(name)
|
|
56
|
+
return try await engine.prediction(from: input)
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
func
|
|
56
|
-
|
|
59
|
+
func release(_ name: String) {
|
|
60
|
+
loaded[name] = nil
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
func
|
|
60
|
-
|
|
63
|
+
func releaseEverything() {
|
|
64
|
+
loaded.removeAll()
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
private func
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if FileManager.default.fileExists(atPath: cachedURL.path) {
|
|
67
|
-
return cachedURL
|
|
67
|
+
private func model(_ name: String, settings: sending MLModelConfiguration = MLModelConfiguration()) async throws -> MLModel {
|
|
68
|
+
if let ready = loaded[name] {
|
|
69
|
+
return ready
|
|
68
70
|
}
|
|
71
|
+
let location = try await compiledLocation(for: name)
|
|
72
|
+
let fresh = try await MLModel.load(contentsOf: location, configuration: settings)
|
|
73
|
+
loaded[name] = fresh
|
|
74
|
+
return fresh
|
|
75
|
+
}
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
private func compiledLocation(for key: String) async throws -> URL {
|
|
78
|
+
let files = FileManager.default
|
|
79
|
+
let cached = compiledFolder.appending(path: key + ".mlmodelc")
|
|
80
|
+
if files.fileExists(atPath: cached.path()) {
|
|
81
|
+
return cached
|
|
73
82
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
guard let packageURL = Bundle.main.url(forResource: name, withExtension: "mlpackage") else {
|
|
77
|
-
throw ModelManagerError.modelNotFound(name)
|
|
83
|
+
if let shipped = Bundle.main.url(forResource: key, withExtension: "mlmodelc") {
|
|
84
|
+
return shipped
|
|
78
85
|
}
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
// Move compiled model to persistent cache
|
|
82
|
-
if FileManager.default.fileExists(atPath: cachedURL.path) {
|
|
83
|
-
try FileManager.default.removeItem(at: cachedURL)
|
|
86
|
+
guard let package = Bundle.main.url(forResource: key, withExtension: "mlpackage") else {
|
|
87
|
+
throw ModelStoreError.missing(key)
|
|
84
88
|
}
|
|
85
|
-
try
|
|
86
|
-
|
|
89
|
+
let temporary = try await MLModel.compileModel(at: package)
|
|
90
|
+
try? files.removeItem(at: cached)
|
|
91
|
+
try files.moveItem(at: temporary, to: cached)
|
|
92
|
+
return cached
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
|
-
|
|
90
|
-
enum ModelManagerError: Error {
|
|
91
|
-
case modelNotFound(String)
|
|
92
|
-
case predictionFailed(String)
|
|
93
|
-
}
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
Lookup order: a previously compiled copy in Application Support, then a
|
|
98
|
+
`.mlmodelc` shipped in the bundle, then compiling a bundled `.mlpackage` and
|
|
99
|
+
moving the result into the cache. Anything else is `missing`.
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
`MLModel` is not `Sendable`, so the store never hands one out: returning it to
|
|
102
|
+
a main-actor caller fails to compile under Swift 6. Callers warm a model with
|
|
103
|
+
`prepare` and run it with `predict`. Inputs, outputs and the configuration are
|
|
104
|
+
`sending`, so the caller passes a freshly built value and owns the result. The
|
|
105
|
+
`nonisolated(unsafe)` copy in `predict` lets the cached model reach the async
|
|
106
|
+
prediction call, which may run concurrently on one model (see "Predictions in
|
|
107
|
+
async code" below). For downloaded
|
|
108
|
+
models, put the version in the cache file name so a new version compiles once.
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
### Driving it from SwiftUI
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
isLoading = true
|
|
111
|
-
defer { isLoading = false }
|
|
112
|
+
Most examples below feed one image feature, so they share a tiny helper:
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
config.computeUnits = .all
|
|
114
|
+
```swift
|
|
115
|
+
import CoreML
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
func photoInput(_ frame: CVPixelBuffer) throws -> MLFeatureProvider {
|
|
118
|
+
try MLDictionaryFeatureProvider(dictionary: ["photo": MLFeatureValue(pixelBuffer: frame)])
|
|
119
|
+
}
|
|
120
|
+
```
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
])
|
|
123
|
-
let output = try model.prediction(from: input)
|
|
122
|
+
```swift
|
|
123
|
+
import Observation
|
|
124
|
+
import CoreML
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
@MainActor
|
|
127
|
+
@Observable
|
|
128
|
+
final class DishRecognizer {
|
|
129
|
+
var dish = ""
|
|
130
|
+
var certainty = 0.0
|
|
131
|
+
var working = false
|
|
132
|
+
var failure: String?
|
|
133
|
+
|
|
134
|
+
func recognize(_ picture: CGImage) async {
|
|
135
|
+
working = true
|
|
136
|
+
defer { working = false }
|
|
137
|
+
do {
|
|
138
|
+
let frame = try makeFrame(from: picture, width: 224, height: 224)
|
|
139
|
+
let answer = try await ModelStore.shared.predict("DishClassifier", from: photoInput(frame))
|
|
140
|
+
dish = answer.featureValue(for: "classLabel")?.stringValue ?? ""
|
|
141
|
+
certainty = answer.featureValue(for: "classLabelProbs")?.dictionaryValue[dish]?.doubleValue ?? 0
|
|
129
142
|
} catch {
|
|
130
|
-
|
|
143
|
+
failure = error.localizedDescription
|
|
131
144
|
}
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
147
|
```
|
|
135
148
|
|
|
136
|
-
|
|
149
|
+
`makeFrame(from:width:height:)` is defined in the pixel buffer section below.
|
|
150
|
+
The frame is created inside `recognize`, which is what allows its feature
|
|
151
|
+
provider to be sent to the store. The dictionary lookup reads the probability of the winning
|
|
152
|
+
label out of the `classLabelProbs` feature.
|
|
137
153
|
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
## Generated classes
|
|
155
|
+
|
|
156
|
+
For a model file named `SceneTagger`, Xcode generates `SceneTagger`,
|
|
157
|
+
`SceneTaggerInput` and `SceneTaggerOutput`.
|
|
140
158
|
|
|
141
159
|
```swift
|
|
142
160
|
import CoreML
|
|
161
|
+
import Vision
|
|
143
162
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
let config = MLModelConfiguration()
|
|
149
|
-
config.computeUnits = .all
|
|
150
|
-
|
|
151
|
-
let classifier = try MyImageClassifier(configuration: config)
|
|
152
|
-
let input = MyImageClassifierInput(image: pixelBuffer)
|
|
153
|
-
let output = try classifier.prediction(input: input)
|
|
163
|
+
func bestDish(_ frame: CVPixelBuffer, using classifier: SceneTagger) throws -> (name: String, probability: Double) {
|
|
164
|
+
let verdict = try classifier.prediction(input: SceneTaggerInput(image: frame))
|
|
165
|
+
return (verdict.classLabel, verdict.classLabelProbs[verdict.classLabel] ?? 0)
|
|
166
|
+
}
|
|
154
167
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return (topLabel, topConfidence)
|
|
168
|
+
func legacyVisionModel(for classifier: SceneTagger) throws -> VNCoreMLModel {
|
|
169
|
+
try VNCoreMLModel(for: classifier.model)
|
|
158
170
|
}
|
|
159
171
|
```
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
```swift
|
|
164
|
-
// Get the underlying MLModel from a generated class
|
|
165
|
-
let classifier = try MyImageClassifier(configuration: config)
|
|
166
|
-
let mlModel = classifier.model
|
|
167
|
-
|
|
168
|
-
// Useful for Vision integration
|
|
169
|
-
let vnModel = try VNCoreMLModel(for: mlModel)
|
|
170
|
-
```
|
|
173
|
+
The generated class exposes the underlying `MLModel` as `.model`, which is
|
|
174
|
+
what `VNCoreMLModel(for:)` and `CoreMLModelContainer(model:)` expect.
|
|
171
175
|
|
|
172
|
-
##
|
|
176
|
+
## A hand-written feature provider
|
|
173
177
|
|
|
174
|
-
Implement `MLFeatureProvider` when
|
|
178
|
+
Implement `MLFeatureProvider` when the input is assembled in code:
|
|
175
179
|
|
|
176
180
|
```swift
|
|
177
181
|
import CoreML
|
|
178
182
|
|
|
179
|
-
final class
|
|
180
|
-
let
|
|
181
|
-
let
|
|
183
|
+
final class GlareCheckInput: MLFeatureProvider {
|
|
184
|
+
let frame: CVPixelBuffer
|
|
185
|
+
let sensitivity: Double
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
init(frame: CVPixelBuffer, sensitivity: Double = 0.5) {
|
|
188
|
+
self.frame = frame
|
|
189
|
+
self.sensitivity = sensitivity
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
case "image":
|
|
190
|
-
return MLFeatureValue(pixelBuffer: image)
|
|
191
|
-
case "confidence_threshold":
|
|
192
|
-
return MLFeatureValue(double: confidenceThreshold)
|
|
193
|
-
default:
|
|
194
|
-
return nil
|
|
195
|
-
}
|
|
192
|
+
var featureNames: Set<String> {
|
|
193
|
+
["photo", "sensitivity"]
|
|
196
194
|
}
|
|
197
195
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
func featureValue(for requested: String) -> MLFeatureValue? {
|
|
197
|
+
if requested == "sensitivity" {
|
|
198
|
+
return MLFeatureValue(double: sensitivity)
|
|
199
|
+
}
|
|
200
|
+
return requested == "photo" ? MLFeatureValue(pixelBuffer: frame) : nil
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
let
|
|
206
|
-
|
|
204
|
+
func hasGlare(_ frame: CVPixelBuffer, model: MLModel) async throws -> Bool {
|
|
205
|
+
let verdict = try await model.prediction(from: GlareCheckInput(frame: frame))
|
|
206
|
+
return verdict.featureValue(for: "glare")?.int64Value == 1
|
|
207
|
+
}
|
|
207
208
|
```
|
|
208
209
|
|
|
209
|
-
|
|
210
|
+
Unknown feature names return `nil`.
|
|
211
|
+
|
|
212
|
+
## Predictions in async code
|
|
210
213
|
|
|
211
|
-
|
|
212
|
-
preprocessing, and caller coordination off the main actor, then call prediction
|
|
213
|
-
without `await`.
|
|
214
|
+
Keep loading, preprocessing and coordination off the main actor.
|
|
214
215
|
|
|
215
|
-
|
|
216
|
+
- iOS 17 and later: `prediction(from:options:) async throws` is the natural
|
|
217
|
+
call in async code and may run concurrently on one model. The compiler
|
|
218
|
+
picks it inside `async` functions, so write `try await`.
|
|
219
|
+
- The synchronous `prediction(from:)` is for synchronous contexts and for
|
|
220
|
+
deployment targets below iOS 17.
|
|
216
221
|
|
|
217
222
|
```swift
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
import CoreML
|
|
224
|
+
|
|
225
|
+
actor InferenceService {
|
|
226
|
+
nonisolated(unsafe) private let engine: MLModel
|
|
220
227
|
|
|
221
|
-
init(
|
|
222
|
-
self.
|
|
228
|
+
init(engine: MLModel) {
|
|
229
|
+
self.engine = engine
|
|
223
230
|
}
|
|
224
231
|
|
|
225
|
-
func
|
|
226
|
-
try
|
|
232
|
+
func run(_ features: sending MLFeatureProvider) async throws -> sending MLFeatureProvider {
|
|
233
|
+
try await engine.prediction(from: features)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func runBlocking(_ features: MLFeatureProvider) throws -> MLFeatureProvider {
|
|
237
|
+
try engine.prediction(from: features)
|
|
227
238
|
}
|
|
228
239
|
}
|
|
229
240
|
```
|
|
230
241
|
|
|
231
|
-
|
|
242
|
+
`runBlocking` is a synchronous actor method, so the synchronous overload
|
|
243
|
+
applies there. Calling the async overload hands the non-`Sendable` model and
|
|
244
|
+
input to a nonisolated method, which Swift 6 rejects for plain actor state
|
|
245
|
+
("sending 'self.engine' risks causing data races"). The model is therefore
|
|
246
|
+
stored `nonisolated(unsafe)`, relying on the async API being safe to call
|
|
247
|
+
concurrently, and `run` takes and returns `sending` values.
|
|
248
|
+
|
|
249
|
+
### Streaming frames
|
|
232
250
|
|
|
233
251
|
```swift
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
])
|
|
244
|
-
let output = try model.prediction(from: input)
|
|
245
|
-
let label = output.featureValue(for: "classLabel")?.stringValue ?? "unknown"
|
|
246
|
-
continuation.yield(label)
|
|
247
|
-
}
|
|
248
|
-
continuation.finish()
|
|
249
|
-
} catch {
|
|
250
|
-
continuation.finish(throwing: error)
|
|
252
|
+
import CoreML
|
|
253
|
+
|
|
254
|
+
func liveLabels(from frames: AsyncStream<CameraSample>, model: sending MLModel) -> AsyncThrowingStream<String, Error> {
|
|
255
|
+
let (labels, sink) = AsyncThrowingStream<String, Error>.makeStream()
|
|
256
|
+
let worker = Task {
|
|
257
|
+
do {
|
|
258
|
+
for await sample in frames {
|
|
259
|
+
let verdict = try await model.prediction(from: photoInput(sample.buffer))
|
|
260
|
+
sink.yield(verdict.featureValue(for: "classLabel")?.stringValue ?? "")
|
|
251
261
|
}
|
|
262
|
+
sink.finish()
|
|
263
|
+
} catch {
|
|
264
|
+
sink.finish(throwing: error)
|
|
252
265
|
}
|
|
253
266
|
}
|
|
267
|
+
sink.onTermination = { _ in worker.cancel() }
|
|
268
|
+
return labels
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
struct CameraSample: @unchecked Sendable {
|
|
272
|
+
let buffer: CVPixelBuffer
|
|
254
273
|
}
|
|
255
274
|
```
|
|
256
275
|
|
|
257
|
-
|
|
276
|
+
`CVPixelBuffer` is not `Sendable`; the wrapper is acceptable only when nothing
|
|
277
|
+
touches the buffer after it is handed over. The model parameter is `sending`
|
|
278
|
+
for the same reason: the worker task takes it over, and the caller must not use
|
|
279
|
+
that instance again. The stream comes from `makeStream()` rather than the
|
|
280
|
+
closure initializer, because a `Task` started inside that closure cannot
|
|
281
|
+
capture the model under Swift 6.
|
|
282
|
+
|
|
283
|
+
## Batch inference
|
|
258
284
|
|
|
259
285
|
```swift
|
|
260
286
|
import CoreML
|
|
261
287
|
|
|
262
|
-
func
|
|
263
|
-
let
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let features = batchOutput.features(at: i)
|
|
274
|
-
let label = features.featureValue(for: "classLabel")?.stringValue ?? "unknown"
|
|
275
|
-
let probs = features.featureValue(for: "classLabelProbs")?.dictionaryValue ?? [:]
|
|
276
|
-
let confidence = (probs[label] as? NSNumber)?.doubleValue ?? 0
|
|
277
|
-
results.append((label, confidence))
|
|
278
|
-
}
|
|
279
|
-
return results
|
|
288
|
+
func labelBatch(_ frames: [CVPixelBuffer], model: MLModel) throws -> [(String, Double)] {
|
|
289
|
+
let rows = try frames.map(photoInput)
|
|
290
|
+
let results = try model.predictions(fromBatch: MLArrayBatchProvider(array: rows))
|
|
291
|
+
var pairs: [(String, Double)] = []
|
|
292
|
+
for index in 0..<results.count {
|
|
293
|
+
let row = results.features(at: index)
|
|
294
|
+
let top = row.featureValue(for: "classLabel")?.stringValue ?? ""
|
|
295
|
+
let table = row.featureValue(for: "classLabelProbs")?.dictionaryValue
|
|
296
|
+
pairs.append((top, (table?[top] as? NSNumber)?.doubleValue ?? 0))
|
|
297
|
+
}
|
|
298
|
+
return pairs
|
|
280
299
|
}
|
|
281
|
-
```
|
|
282
300
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
let output = try model.predictions(fromBatch: batchInputs)
|
|
288
|
-
|
|
289
|
-
// Explicit prediction options
|
|
290
|
-
let options = MLPredictionOptions()
|
|
291
|
-
let outputWithOptions = try model.predictions(from: batchInputs, options: options)
|
|
301
|
+
func labelBatchWithOptions(_ batch: MLBatchProvider, model: MLModel) throws -> MLBatchProvider {
|
|
302
|
+
let options = MLPredictionOptions()
|
|
303
|
+
return try model.predictions(from: batch, options: options)
|
|
304
|
+
}
|
|
292
305
|
```
|
|
293
306
|
|
|
294
|
-
|
|
295
|
-
|
|
307
|
+
The two correct spellings: `predictions(fromBatch:)` without options and
|
|
308
|
+
`predictions(from:options:)` with them. `predictions(from:)` alone is not the
|
|
309
|
+
options-free batch call.
|
|
296
310
|
|
|
297
|
-
## Stateful
|
|
311
|
+
## Stateful sessions with MLState (iOS 18+)
|
|
298
312
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
where each prediction depends on previous context.
|
|
313
|
+
Text generation, audio classification and time-series models carry context
|
|
314
|
+
from one call to the next. `MLState` holds that context.
|
|
302
315
|
|
|
303
316
|
```swift
|
|
304
317
|
import CoreML
|
|
305
318
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
private var state: MLState?
|
|
319
|
+
enum ListeningError: Error {
|
|
320
|
+
case notListening
|
|
321
|
+
}
|
|
310
322
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
323
|
+
actor ChirpDetector {
|
|
324
|
+
private let engine: MLModel
|
|
325
|
+
private var memory: MLState?
|
|
314
326
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
state = model.makeState()
|
|
327
|
+
init(engine: MLModel) {
|
|
328
|
+
self.engine = engine
|
|
318
329
|
}
|
|
319
330
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
guard let state else {
|
|
323
|
-
throw ClassifierError.noActiveSession
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
let input = try MLDictionaryFeatureProvider(dictionary: [
|
|
327
|
-
"audio_features": MLFeatureValue(multiArray: audioFeatures)
|
|
328
|
-
])
|
|
329
|
-
|
|
330
|
-
let output = try model.prediction(from: input, using: state)
|
|
331
|
-
return output.featureValue(for: "label")?.stringValue ?? "unknown"
|
|
331
|
+
func startListening() {
|
|
332
|
+
memory = engine.makeState()
|
|
332
333
|
}
|
|
333
334
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
func detect(_ window: MLMultiArray) throws -> String? {
|
|
336
|
+
guard let memory else { throw ListeningError.notListening }
|
|
337
|
+
let input = try MLDictionaryFeatureProvider(dictionary: ["samples": MLFeatureValue(multiArray: window)])
|
|
338
|
+
let heard = try engine.prediction(from: input, using: memory)
|
|
339
|
+
return heard.featureValue(for: "species")?.stringValue
|
|
337
340
|
}
|
|
338
|
-
}
|
|
339
341
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
+
func stopListening() {
|
|
343
|
+
memory = nil
|
|
344
|
+
}
|
|
342
345
|
}
|
|
343
346
|
```
|
|
344
347
|
|
|
345
|
-
|
|
348
|
+
Rules:
|
|
346
349
|
|
|
347
|
-
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
explicit reset method -- just discard the old state and create fresh.
|
|
359
|
-
- **Memory:** State holds model-specific internal buffers. Release it when the
|
|
360
|
-
session ends to free memory.
|
|
350
|
+
- One prediction at a time per state. Being `Sendable` means a state may be
|
|
351
|
+
handed to another task or actor; overlapping calls on it are still unsafe,
|
|
352
|
+
and its buffers stay untouched until the current call returns.
|
|
353
|
+
- In plain serial code, such as the synchronous actor method above, call
|
|
354
|
+
`prediction(from:using:)` without `await`. In async code, or when you need
|
|
355
|
+
`MLPredictionOptions`, use `prediction(from:using:options:) async` (iOS 18);
|
|
356
|
+
still one call in flight per state.
|
|
357
|
+
- Concurrent sequences, such as several audio channels, get one
|
|
358
|
+
`makeState()` each.
|
|
359
|
+
- There is no reset call. Throw the state away and make a new one.
|
|
360
|
+
- A state holds buffers specific to its model; drop it once the session is over.
|
|
361
361
|
|
|
362
|
-
##
|
|
362
|
+
## Pixel buffers
|
|
363
363
|
|
|
364
|
-
|
|
364
|
+
A throwing version of the conversion helper:
|
|
365
365
|
|
|
366
366
|
```swift
|
|
367
|
-
import CoreVideo
|
|
368
367
|
import CoreGraphics
|
|
368
|
+
import CoreVideo
|
|
369
369
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
]
|
|
370
|
+
enum FrameError: Error {
|
|
371
|
+
case bufferAllocation
|
|
372
|
+
case drawingContext
|
|
373
|
+
}
|
|
375
374
|
|
|
376
|
-
|
|
377
|
-
let
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
throw
|
|
386
|
-
}
|
|
375
|
+
func makeFrame(from picture: CGImage, width: Int, height: Int) throws -> CVPixelBuffer {
|
|
376
|
+
let attributes = [
|
|
377
|
+
kCVPixelBufferCGImageCompatibilityKey: true,
|
|
378
|
+
kCVPixelBufferCGBitmapContextCompatibilityKey: true
|
|
379
|
+
] as CFDictionary
|
|
380
|
+
var created: CVPixelBuffer?
|
|
381
|
+
let format = kCVPixelFormatType_32ARGB
|
|
382
|
+
guard CVPixelBufferCreate(nil, width, height, format, attributes, &created) == kCVReturnSuccess,
|
|
383
|
+
let frame = created else {
|
|
384
|
+
throw FrameError.bufferAllocation
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
CVPixelBufferLockBaseAddress(frame, [])
|
|
388
|
+
defer { CVPixelBufferUnlockBaseAddress(frame, []) }
|
|
389
|
+
|
|
390
|
+
let base = CVPixelBufferGetBaseAddress(frame)
|
|
391
|
+
let stride = CVPixelBufferGetBytesPerRow(frame)
|
|
392
|
+
let layout = CGImageAlphaInfo.noneSkipFirst.rawValue
|
|
393
|
+
let rgb = CGColorSpaceCreateDeviceRGB()
|
|
394
|
+
guard let canvas = CGContext(data: base, width: width, height: height, bitsPerComponent: 8,
|
|
395
|
+
bytesPerRow: stride, space: rgb, bitmapInfo: layout) else {
|
|
396
|
+
throw FrameError.drawingContext
|
|
397
|
+
}
|
|
398
|
+
canvas.draw(picture, in: CGRect(origin: .zero, size: CGSize(width: width, height: height)))
|
|
399
|
+
return frame
|
|
400
|
+
}
|
|
401
|
+
```
|
|
387
402
|
|
|
388
|
-
|
|
389
|
-
|
|
403
|
+
From a `CIImage`, create a `kCVPixelFormatType_32BGRA` buffer and render into it
|
|
404
|
+
with `CIContext.render(_:to:)`:
|
|
390
405
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
height: height,
|
|
395
|
-
bitsPerComponent: 8,
|
|
396
|
-
bytesPerRow: CVPixelBufferGetBytesPerRow(buffer),
|
|
397
|
-
space: CGColorSpaceCreateDeviceRGB(),
|
|
398
|
-
bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
|
|
399
|
-
) else {
|
|
400
|
-
throw ImageError.contextCreationFailed
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
404
|
-
return buffer
|
|
405
|
-
}
|
|
406
|
+
```swift
|
|
407
|
+
import CoreImage
|
|
408
|
+
import CoreVideo
|
|
406
409
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
+
func makeFrame(from image: CIImage, renderer: CIContext) -> CVPixelBuffer? {
|
|
411
|
+
var created: CVPixelBuffer?
|
|
412
|
+
CVPixelBufferCreate(kCFAllocatorDefault, Int(image.extent.width), Int(image.extent.height),
|
|
413
|
+
kCVPixelFormatType_32BGRA, nil, &created)
|
|
414
|
+
guard let frame = created else { return nil }
|
|
415
|
+
renderer.render(image, to: frame)
|
|
416
|
+
return frame
|
|
410
417
|
}
|
|
411
418
|
```
|
|
412
419
|
|
|
413
|
-
|
|
414
|
-
|
|
420
|
+
Center-crop before scaling when the model expects a square and the photo is
|
|
421
|
+
not: `image.cropped(to:)` with a square rect centered on `image.extent`, then
|
|
422
|
+
scale to the model's side length. Per-channel normalization is shown in the
|
|
423
|
+
tensor section.
|
|
415
424
|
|
|
416
|
-
## MLMultiArray
|
|
425
|
+
## MLMultiArray
|
|
417
426
|
|
|
418
427
|
```swift
|
|
419
428
|
import CoreML
|
|
420
429
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
430
|
+
func planarImageTensor() throws -> MLMultiArray {
|
|
431
|
+
let planes = try MLMultiArray(shape: [1, 3, 256, 256], dataType: .float32)
|
|
432
|
+
for offset in 0..<planes.count {
|
|
433
|
+
planes[offset] = NSNumber(value: Float(offset % 255) / 255)
|
|
434
|
+
}
|
|
435
|
+
return planes
|
|
436
|
+
}
|
|
426
437
|
|
|
427
|
-
|
|
428
|
-
let
|
|
438
|
+
func features(from values: [Float]) throws -> MLFeatureValue {
|
|
439
|
+
let array = try MLMultiArray(values)
|
|
440
|
+
return MLFeatureValue(multiArray: array)
|
|
441
|
+
}
|
|
429
442
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
let pointer = multiArray.dataPointer.assumingMemoryBound(to: Float.self)
|
|
433
|
-
return Array(UnsafeBufferPointer(start: pointer, count: multiArray.count))
|
|
443
|
+
func pixelValue(_ planes: MLMultiArray, channel: Int, row: Int, column: Int) -> Float {
|
|
444
|
+
planes[[0, channel, row, column] as [NSNumber]].floatValue
|
|
434
445
|
}
|
|
435
446
|
|
|
436
|
-
|
|
447
|
+
func floats(from array: MLMultiArray) -> [Float] {
|
|
448
|
+
MLShapedArray<Float>(array).scalars
|
|
449
|
+
}
|
|
437
450
|
```
|
|
438
451
|
|
|
439
|
-
|
|
452
|
+
Linear subscripts (`planes[offset]`) walk the whole buffer; index arrays
|
|
453
|
+
address one element. Converting through `MLShapedArray<Float>` avoids raw
|
|
454
|
+
pointer work; if you do use `dataPointer.assumingMemoryBound(to: Float.self)`
|
|
455
|
+
with `UnsafeBufferPointer`, keep the array alive for the whole access.
|
|
456
|
+
|
|
457
|
+
Tokenized text arrives as an `[Int32]` of token ids shaped `[1, sequence]`;
|
|
458
|
+
audio features as `[1, frames, bins]`. Both follow the same create, fill,
|
|
459
|
+
wrap-in-`MLFeatureValue(multiArray:)` pattern.
|
|
460
|
+
|
|
461
|
+
## MLTensor (iOS 18+)
|
|
440
462
|
|
|
441
463
|
```swift
|
|
442
464
|
import CoreML
|
|
443
465
|
|
|
444
|
-
|
|
445
|
-
let
|
|
446
|
-
let
|
|
447
|
-
let
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
let
|
|
451
|
-
let
|
|
466
|
+
func tensorTour() async -> (MLShapedArray<Float>, MLMultiArray) {
|
|
467
|
+
let readings = MLTensor([1.5, 3.0, 4.5, 6.0, 7.5, 9.0] as [Float])
|
|
468
|
+
let blank = MLTensor(zeros: [2, 3], scalarType: Float.self)
|
|
469
|
+
let unit = MLTensor(ones: [2, 3], scalarType: Float.self)
|
|
470
|
+
|
|
471
|
+
let square = readings.reshaped(to: [2, 3])
|
|
472
|
+
let row = readings.expandingShape(at: 0)
|
|
473
|
+
let summed = square + unit + blank
|
|
474
|
+
let average = readings.mean()
|
|
475
|
+
let winner = readings.argmax()
|
|
476
|
+
let odds = row.softmax(alongAxis: -1)
|
|
477
|
+
let joined = MLTensor(concatenating: [square, summed], alongAxis: 0)
|
|
478
|
+
|
|
479
|
+
_ = (average, winner, joined)
|
|
480
|
+
let materialized = await odds.shapedArray(of: Float.self)
|
|
481
|
+
return (materialized, MLMultiArray(materialized))
|
|
482
|
+
}
|
|
452
483
|
|
|
453
|
-
|
|
454
|
-
let
|
|
455
|
-
let
|
|
456
|
-
|
|
484
|
+
func imageNetNormalized(_ pixels: MLTensor) -> MLTensor {
|
|
485
|
+
let mean = MLTensor([0.485, 0.456, 0.406] as [Float]).reshaped(to: [1, 3, 1, 1])
|
|
486
|
+
let spread = MLTensor([0.229, 0.224, 0.225] as [Float]).reshaped(to: [1, 3, 1, 1])
|
|
487
|
+
return (pixels - mean) / spread
|
|
488
|
+
}
|
|
457
489
|
|
|
458
|
-
|
|
459
|
-
|
|
490
|
+
func tensorFromArray(_ array: MLMultiArray) -> MLTensor {
|
|
491
|
+
MLTensor(MLShapedArray<Float>(array))
|
|
492
|
+
}
|
|
493
|
+
```
|
|
460
494
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
495
|
+
`expandingShape(at: 0)` turns shape `[6]` into `[1, 6]`. Values only exist
|
|
496
|
+
after `await shapedArray(of:)`. Keep to operations Apple documents: no
|
|
497
|
+
`MLTensor(multiArray)`, no `std()` or `standardDeviation()`, no peeking into
|
|
498
|
+
the lazy storage and no blocking read, unless Apple's documentation names the
|
|
499
|
+
exact API and its availability. A standard deviation can be built from documented parts:
|
|
465
500
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
let b = MLTensor([3.0, 4.0])
|
|
469
|
-
let concatenated = MLTensor(concatenating: [a, b], alongAxis: 0)
|
|
501
|
+
```swift
|
|
502
|
+
import CoreML
|
|
470
503
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
let stdTensor = MLTensor(std)
|
|
475
|
-
return (tensor - meanTensor) / stdTensor
|
|
504
|
+
func spread(of values: MLTensor) -> MLTensor {
|
|
505
|
+
let centered = values - values.mean()
|
|
506
|
+
return (centered * centered).mean().squareRoot()
|
|
476
507
|
}
|
|
477
508
|
```
|
|
478
509
|
|
|
479
|
-
## Vision
|
|
510
|
+
## Vision and Core ML together
|
|
480
511
|
|
|
481
|
-
###
|
|
512
|
+
### Swift-native Vision (iOS 18+)
|
|
482
513
|
|
|
483
514
|
```swift
|
|
484
|
-
import Vision
|
|
485
515
|
import CoreML
|
|
516
|
+
import Observation
|
|
517
|
+
import Vision
|
|
486
518
|
|
|
487
|
-
@MainActor
|
|
488
519
|
@Observable
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
struct Detection: Identifiable {
|
|
520
|
+
@MainActor
|
|
521
|
+
final class ShelfDetector {
|
|
522
|
+
struct Finding: Identifiable {
|
|
494
523
|
let id = UUID()
|
|
495
|
-
let
|
|
496
|
-
let
|
|
497
|
-
let
|
|
524
|
+
let name: String
|
|
525
|
+
let score: Float
|
|
526
|
+
let box: NormalizedRect
|
|
498
527
|
}
|
|
499
528
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
529
|
+
var findings: [Finding] = []
|
|
530
|
+
private let detector: MLModel
|
|
531
|
+
|
|
532
|
+
init(detector: MLModel) {
|
|
533
|
+
self.detector = detector
|
|
534
|
+
}
|
|
503
535
|
|
|
536
|
+
func scan(_ photo: CGImage) async {
|
|
504
537
|
do {
|
|
505
|
-
let
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
let results = try await request.perform(on: image)
|
|
512
|
-
detections = results.compactMap { observation in
|
|
513
|
-
guard let object = observation as? RecognizedObjectObservation,
|
|
514
|
-
let topLabel = object.labels.first else { return nil }
|
|
515
|
-
return Detection(
|
|
516
|
-
label: topLabel.identifier,
|
|
517
|
-
confidence: topLabel.confidence,
|
|
518
|
-
boundingBox: object.boundingBox
|
|
519
|
-
)
|
|
538
|
+
let request = CoreMLRequest(model: try CoreMLModelContainer(model: detector))
|
|
539
|
+
let raw = try await request.perform(on: photo)
|
|
540
|
+
findings = raw.compactMap { item in
|
|
541
|
+
guard let object = item as? RecognizedObjectObservation,
|
|
542
|
+
let best = object.labels.first else { return nil }
|
|
543
|
+
return Finding(name: best.identifier, score: best.confidence, box: object.boundingBox)
|
|
520
544
|
}
|
|
521
545
|
} catch {
|
|
522
|
-
|
|
546
|
+
findings = []
|
|
523
547
|
}
|
|
524
548
|
}
|
|
525
549
|
}
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
```swift
|
|
529
|
-
func classifyImage(_ image: CGImage) async throws -> [(label: String, confidence: Float)] {
|
|
530
|
-
let classifier = try MyImageClassifier(configuration: .init())
|
|
531
|
-
let request = CoreMLRequest(model: .init(classifier.model))
|
|
532
550
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
551
|
+
func topClasses(in photo: CGImage, model: MLModel) async throws -> [(String, Float)] {
|
|
552
|
+
let request = CoreMLRequest(model: try CoreMLModelContainer(model: model))
|
|
553
|
+
return try await request.perform(on: photo)
|
|
554
|
+
.compactMap { $0 as? ClassificationObservation }
|
|
555
|
+
.map { ($0.identifier, $0.confidence) }
|
|
538
556
|
}
|
|
539
557
|
```
|
|
540
558
|
|
|
541
|
-
|
|
559
|
+
With a generated class, pass `generated.model` into `CoreMLModelContainer`.
|
|
560
|
+
|
|
561
|
+
### Legacy Vision (before iOS 18)
|
|
542
562
|
|
|
543
563
|
```swift
|
|
544
|
-
import Vision
|
|
545
564
|
import CoreML
|
|
565
|
+
import Vision
|
|
546
566
|
|
|
547
|
-
|
|
548
|
-
let
|
|
549
|
-
config.computeUnits = .all
|
|
550
|
-
|
|
551
|
-
let detector = try MyObjectDetector(configuration: config)
|
|
552
|
-
let vnModel = try VNCoreMLModel(for: detector.model)
|
|
567
|
+
actor LegacyShelfDetector {
|
|
568
|
+
private let visionModel: VNCoreMLModel
|
|
553
569
|
|
|
554
|
-
|
|
555
|
-
|
|
570
|
+
init(model: MLModel) throws {
|
|
571
|
+
visionModel = try VNCoreMLModel(for: model)
|
|
572
|
+
}
|
|
556
573
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
574
|
+
func objects(in photo: CGImage) throws -> [(label: String, confidence: Float, box: CGRect)] {
|
|
575
|
+
let request = VNCoreMLRequest(model: visionModel)
|
|
576
|
+
request.imageCropAndScaleOption = .scaleFill
|
|
577
|
+
try VNImageRequestHandler(cgImage: photo).perform([request])
|
|
578
|
+
let found = request.results as? [VNRecognizedObjectObservation] ?? []
|
|
579
|
+
return found.compactMap { object in
|
|
580
|
+
object.labels.first.map { ($0.identifier, $0.confidence, object.boundingBox) }
|
|
581
|
+
}
|
|
582
|
+
}
|
|
562
583
|
}
|
|
563
|
-
```
|
|
564
584
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
let classifier = try MyImageClassifier(configuration: .init())
|
|
568
|
-
let vnModel = try VNCoreMLModel(for: classifier.model)
|
|
569
|
-
let request = VNCoreMLRequest(model: vnModel)
|
|
585
|
+
func legacyTopFive(in photo: CGImage, model: MLModel) throws -> [VNClassificationObservation] {
|
|
586
|
+
let request = VNCoreMLRequest(model: try VNCoreMLModel(for: model))
|
|
570
587
|
request.imageCropAndScaleOption = .centerCrop
|
|
571
|
-
|
|
572
|
-
let
|
|
573
|
-
return
|
|
574
|
-
try handler.perform([request])
|
|
575
|
-
guard let results = request.results as? [VNClassificationObservation] else { return [] }
|
|
576
|
-
return results.prefix(5).map { ($0.identifier, $0.confidence) }
|
|
577
|
-
}.value
|
|
588
|
+
try VNImageRequestHandler(cgImage: photo).perform([request])
|
|
589
|
+
let ranked = request.results as? [VNClassificationObservation] ?? []
|
|
590
|
+
return Array(ranked.prefix(5))
|
|
578
591
|
}
|
|
579
592
|
```
|
|
580
593
|
|
|
581
|
-
|
|
594
|
+
The legacy handler blocks while it works, so it runs inside an actor (or a
|
|
595
|
+
`Task.detached` whose captures are all `Sendable`) rather than on the main
|
|
596
|
+
actor. Vision observation classes are not `Sendable`; convert them to plain
|
|
597
|
+
values before they leave the actor. `.scaleFill` stretches the whole image into the model's input; `.centerCrop`
|
|
598
|
+
keeps the middle square, which suits most classifiers.
|
|
599
|
+
|
|
600
|
+
## NaturalLanguage models
|
|
582
601
|
|
|
583
|
-
|
|
602
|
+
Text classifiers trained with Create ML load through `NLModel`:
|
|
584
603
|
|
|
585
604
|
```swift
|
|
605
|
+
import CoreML
|
|
586
606
|
import NaturalLanguage
|
|
587
607
|
|
|
588
|
-
func
|
|
589
|
-
let
|
|
590
|
-
let
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
let hypotheses = nlModel.predictedLabelHypotheses(for: text, maximumCount: 1)
|
|
594
|
-
let confidence = hypotheses[label] ?? 0
|
|
595
|
-
return (label, confidence)
|
|
608
|
+
func ticketCategory(for message: String, modelURL: URL) throws -> (String, Double)? {
|
|
609
|
+
let classifier = try NLModel(contentsOf: modelURL)
|
|
610
|
+
guard let category = classifier.predictedLabel(for: message) else { return nil }
|
|
611
|
+
let score = classifier.predictedLabelHypotheses(for: message, maximumCount: 1)[category] ?? 0
|
|
612
|
+
return (category, score)
|
|
596
613
|
}
|
|
597
614
|
```
|
|
598
615
|
|
|
599
|
-
## MLComputePlan
|
|
616
|
+
## MLComputePlan in detail (iOS 17.4+)
|
|
617
|
+
|
|
618
|
+
A logging pass over every operation of an ML Program, run with the compute
|
|
619
|
+
units you intend to ship:
|
|
600
620
|
|
|
601
621
|
```swift
|
|
602
622
|
import CoreML
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
let
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
623
|
+
import OSLog
|
|
624
|
+
|
|
625
|
+
func auditDispatch(of packageURL: URL, units: MLComputeUnits) async throws {
|
|
626
|
+
let log = Logger(subsystem: "app.inference", category: "compute-plan")
|
|
627
|
+
let settings = MLModelConfiguration()
|
|
628
|
+
settings.computeUnits = units
|
|
629
|
+
let plan = try await MLComputePlan.load(contentsOf: packageURL, configuration: settings)
|
|
630
|
+
guard case .program(let program) = plan.modelStructure else {
|
|
631
|
+
log.info("Per-operation dispatch is reported only for ML Program models")
|
|
612
632
|
return
|
|
613
633
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
634
|
+
guard let entry = program.functions["main"] else {
|
|
635
|
+
log.info("No main function in the program")
|
|
636
|
+
return
|
|
637
|
+
}
|
|
638
|
+
for op in entry.block.operations {
|
|
639
|
+
let device = plan.deviceUsage(for: op).map { "\($0.preferred)" } ?? "unknown"
|
|
640
|
+
let weight = plan.estimatedCost(of: op)?.weight ?? 0
|
|
641
|
+
log.debug("\(op.operatorName, privacy: .public) on \(device, privacy: .public) weight \(weight)")
|
|
623
642
|
}
|
|
624
643
|
}
|
|
625
644
|
```
|
|
626
645
|
|
|
627
|
-
|
|
646
|
+
Reading the result:
|
|
628
647
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
| CPU | Fallback for unsupported operations | Investigate if many ops fall here |
|
|
634
|
-
|
|
635
|
-
If many critical operations fall back to CPU, try `.cpuAndGPU` compute units,
|
|
636
|
-
check for unsupported ANE operations, or re-convert with a different deployment target.
|
|
648
|
+
- **Neural Engine**: the fastest and most efficient place; nothing to do.
|
|
649
|
+
- **GPU**: runs through Metal; good for large matrix work.
|
|
650
|
+
- **CPU**: the fallback for operations the accelerators cannot run. A handful
|
|
651
|
+
is normal; many, especially heavy ones, deserve investigation.
|
|
637
652
|
|
|
638
|
-
|
|
653
|
+
If critical operations land on the CPU: try `.cpuAndGPU`, look up which
|
|
654
|
+
operations the Neural Engine does not support, or convert again with a
|
|
655
|
+
different minimum deployment target.
|
|
639
656
|
|
|
640
|
-
|
|
657
|
+
## Lifecycle: warm up and release
|
|
641
658
|
|
|
642
659
|
```swift
|
|
643
|
-
|
|
660
|
+
import SwiftUI
|
|
661
|
+
import Observation
|
|
662
|
+
import CoreML
|
|
663
|
+
|
|
644
664
|
@Observable
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
private
|
|
665
|
+
@MainActor
|
|
666
|
+
final class InferenceLifecycle {
|
|
667
|
+
private(set) var ready = false
|
|
648
668
|
|
|
649
|
-
func
|
|
650
|
-
|
|
651
|
-
let config = MLModelConfiguration()
|
|
652
|
-
config.computeUnits = .all
|
|
653
|
-
_ = try await modelManager.model(named: "MainClassifier", configuration: config)
|
|
654
|
-
isModelReady = true
|
|
655
|
-
} catch {
|
|
656
|
-
isModelReady = false
|
|
657
|
-
}
|
|
669
|
+
func warmUp() async {
|
|
670
|
+
ready = (try? await ModelStore.shared.prepare("DishClassifier")) != nil
|
|
658
671
|
}
|
|
659
672
|
|
|
660
|
-
func
|
|
661
|
-
await
|
|
662
|
-
|
|
673
|
+
func enteredBackground() async {
|
|
674
|
+
await ModelStore.shared.releaseEverything()
|
|
675
|
+
ready = false
|
|
663
676
|
}
|
|
664
677
|
}
|
|
665
678
|
|
|
666
|
-
|
|
667
|
-
@
|
|
668
|
-
|
|
669
|
-
@State private var modelState = AppModelState()
|
|
670
|
-
@Environment(\.scenePhase) private var scenePhase
|
|
679
|
+
struct PantryApp: App {
|
|
680
|
+
@State private var lifecycle = InferenceLifecycle()
|
|
681
|
+
@Environment(\.scenePhase) private var phase
|
|
671
682
|
|
|
672
683
|
var body: some Scene {
|
|
673
684
|
WindowGroup {
|
|
674
|
-
|
|
675
|
-
.environment(
|
|
676
|
-
.task { await
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
+
Text(lifecycle.ready ? "Ready" : "Loading")
|
|
686
|
+
.environment(lifecycle)
|
|
687
|
+
.task { await lifecycle.warmUp() }
|
|
688
|
+
}
|
|
689
|
+
.onChange(of: phase) { _, newPhase in
|
|
690
|
+
Task {
|
|
691
|
+
switch newPhase {
|
|
692
|
+
case .background: await lifecycle.enteredBackground()
|
|
693
|
+
case .active: await lifecycle.warmUp()
|
|
694
|
+
default: break
|
|
685
695
|
}
|
|
696
|
+
}
|
|
686
697
|
}
|
|
687
698
|
}
|
|
688
699
|
}
|
|
689
700
|
```
|
|
690
701
|
|
|
691
|
-
|
|
702
|
+
In a real app this type carries `@main`; it is omitted here so the snippet
|
|
703
|
+
stays self-contained.
|
|
704
|
+
|
|
705
|
+
## Errors
|
|
692
706
|
|
|
693
707
|
```swift
|
|
694
708
|
import CoreML
|
|
695
709
|
|
|
696
|
-
func
|
|
697
|
-
let
|
|
698
|
-
|
|
699
|
-
|
|
710
|
+
func guardedPrediction(_ features: MLFeatureProvider, resource: String) async -> MLFeatureProvider? {
|
|
711
|
+
guard let location = Bundle.main.url(forResource: resource, withExtension: "mlmodelc") else {
|
|
712
|
+
return nil
|
|
713
|
+
}
|
|
700
714
|
do {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
return nil
|
|
704
|
-
}
|
|
705
|
-
let model = try await MLModel.load(contentsOf: url, configuration: config)
|
|
706
|
-
return try model.prediction(from: input)
|
|
715
|
+
let engine = try await MLModel.load(contentsOf: location)
|
|
716
|
+
return try await engine.prediction(from: features)
|
|
707
717
|
} catch {
|
|
708
|
-
print("Model error: \(error)")
|
|
709
718
|
return nil
|
|
710
719
|
}
|
|
711
720
|
}
|
|
712
721
|
```
|
|
713
722
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
| Error | Cause | Fix |
|
|
723
|
+
| Symptom | Usual cause | Fix |
|
|
717
724
|
|---|---|---|
|
|
718
|
-
|
|
|
719
|
-
| Compilation
|
|
720
|
-
| Input shape mismatch |
|
|
721
|
-
|
|
|
722
|
-
|
|
|
725
|
+
| Model file not found | Wrong path, or the file is not a member of the target | Check target membership and the resource name |
|
|
726
|
+
| Compilation fails | Corrupt `.mlpackage` or operations the OS does not support | Export again from coremltools |
|
|
727
|
+
| Input shape mismatch | Image size or tensor shape differs from the model's description | Match `modelDescription.inputDescriptionsByName` |
|
|
728
|
+
| Memory exhausted | The device cannot hold the model | Smaller or compressed model, or `.cpuOnly` as a last resort |
|
|
729
|
+
| Unexpected CPU fallback | Operations unsupported on the chosen device | Stay on `.all` and inspect `MLComputePlan` |
|
|
723
730
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
lazy-buffer access, or synchronous extraction unless Apple documents that exact
|
|
727
|
-
API and availability.
|
|
731
|
+
MLTensor code should use only operations confirmed in Apple's documentation
|
|
732
|
+
(see the tensor section); invented helpers compile nowhere.
|
|
728
733
|
|
|
729
|
-
## Testing
|
|
734
|
+
## Tests with Swift Testing
|
|
730
735
|
|
|
731
736
|
```swift
|
|
732
|
-
import Testing
|
|
733
737
|
import CoreML
|
|
738
|
+
import Testing
|
|
734
739
|
|
|
735
|
-
struct
|
|
736
|
-
|
|
737
|
-
let
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
740
|
+
struct DishClassifierTests {
|
|
741
|
+
private func model(units: MLComputeUnits) async throws -> MLModel {
|
|
742
|
+
let url = try #require(Bundle.main.url(forResource: "DishClassifier", withExtension: "mlmodelc"))
|
|
743
|
+
let settings = MLModelConfiguration()
|
|
744
|
+
settings.computeUnits = units
|
|
745
|
+
return try await MLModel.load(contentsOf: url, configuration: settings)
|
|
741
746
|
}
|
|
742
747
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
let model = try MyImageClassifier(configuration: config)
|
|
748
|
-
let input = try createTestInput(width: 224, height: 224)
|
|
749
|
-
let output = try model.prediction(input: input)
|
|
750
|
-
|
|
751
|
-
#expect(!output.classLabel.isEmpty)
|
|
752
|
-
#expect(output.classLabelProbs.values.allSatisfy { $0 >= 0 && $0 <= 1 })
|
|
748
|
+
private func blankFrame() throws -> CVPixelBuffer {
|
|
749
|
+
var created: CVPixelBuffer?
|
|
750
|
+
CVPixelBufferCreate(kCFAllocatorDefault, 224, 224, kCVPixelFormatType_32ARGB, nil, &created)
|
|
751
|
+
return try #require(created)
|
|
753
752
|
}
|
|
754
753
|
|
|
755
|
-
@Test func
|
|
756
|
-
let
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
let model = try MyImageClassifier(configuration: config)
|
|
760
|
-
let input = try createTestInput(width: 224, height: 224)
|
|
761
|
-
|
|
762
|
-
_ = try model.prediction(input: input) // Warm up
|
|
763
|
-
|
|
764
|
-
let start = ContinuousClock.now
|
|
765
|
-
for _ in 0..<10 {
|
|
766
|
-
_ = try model.prediction(input: input)
|
|
767
|
-
}
|
|
768
|
-
let avgMs = (ContinuousClock.now - start) / 10
|
|
754
|
+
@Test func loadsWithInputs() async throws {
|
|
755
|
+
let model = try await model(units: .cpuOnly)
|
|
756
|
+
#expect(!model.modelDescription.inputDescriptionsByName.isEmpty)
|
|
757
|
+
}
|
|
769
758
|
|
|
770
|
-
|
|
759
|
+
@Test func producesProbabilities() async throws {
|
|
760
|
+
let model = try await model(units: .cpuOnly)
|
|
761
|
+
let verdict = try await model.prediction(from: photoInput(try blankFrame()))
|
|
762
|
+
let winner = verdict.featureValue(for: "classLabel")?.stringValue ?? ""
|
|
763
|
+
let probabilities = verdict.featureValue(for: "classLabelProbs")?.dictionaryValue.values.map { $0.doubleValue } ?? []
|
|
764
|
+
#expect(!winner.isEmpty)
|
|
765
|
+
#expect(probabilities.allSatisfy { (0...1).contains($0) })
|
|
771
766
|
}
|
|
772
|
-
}
|
|
773
767
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
768
|
+
@Test func staysUnderLatencyBudget() async throws {
|
|
769
|
+
let model = try await model(units: .all)
|
|
770
|
+
let probe = try photoInput(try blankFrame())
|
|
771
|
+
_ = try await model.prediction(from: probe)
|
|
772
|
+
let clock = ContinuousClock()
|
|
773
|
+
let rounds = 10
|
|
774
|
+
let elapsed = try await clock.measure {
|
|
775
|
+
for _ in 0..<rounds {
|
|
776
|
+
_ = try await model.prediction(from: probe)
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
#expect(elapsed / rounds < .milliseconds(50))
|
|
782
780
|
}
|
|
783
|
-
return buffer
|
|
784
781
|
}
|
|
785
782
|
```
|
|
786
783
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
784
|
+
Load and output tests pin `.cpuOnly` for repeatable results. The latency test
|
|
785
|
+
uses `.all`, discards one warm-up call, then averages ten timed runs against a
|
|
786
|
+
50 ms budget; set the budget from the product requirement and run it on
|
|
787
|
+
device, not in the simulator.
|
|
788
|
+
|
|
789
|
+
## Memory practices
|
|
790
|
+
|
|
791
|
+
- Unload when `scenePhase` becomes `.background` and reload in the foreground;
|
|
792
|
+
the system reclaims memory from background apps aggressively.
|
|
793
|
+
- Choose compute units by context: `.all` normally; `.cpuOnly` only when
|
|
794
|
+
profiling or policy shows contention, thermal limits, energy pressure,
|
|
795
|
+
deterministic testing or a real background-execution need. Whether a
|
|
796
|
+
backgrounded app gets the accelerators at all is decided by the system: its
|
|
797
|
+
execution mode, whether it is suspended, heat, battery and competing work.
|
|
798
|
+
- Ship or cache compiled `.mlmodelc`: it loads faster and needs less transient
|
|
799
|
+
memory than compiling at run time. Compile downloads once, move them out of
|
|
800
|
+
the temporary directory, key the cache by version, never recompile the same
|
|
801
|
+
model on every launch.
|
|
802
|
+
- Measure on real hardware, beginning with the device that has the least
|
|
803
|
+
RAM: model load, the first call, sustained calls, trips to the background
|
|
804
|
+
and back, and behavior under memory pressure.
|
|
805
|
+
- One instance per model, shared through an actor.
|
|
806
|
+
- Release large `MLArrayBatchProvider` values promptly; they hold every input
|
|
807
|
+
in the batch.
|