Repository navigation
[CoreAI] Add multi-function export and shared runtime loading - #24476
Merged
Merged
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24476
Note: Links to docs will display an error until the docs builds have been completed. This comment was automatically generated by Dr. CI and updates every 15 minutes. |
metascroy
force-pushed
the
coreai-multifunction
branch
from
November 3, 2026 18:59
d07d65a to
f67bf14
Compare
metascroy
force-pushed
the
coreai-rename-packaging
branch
from
November 3, 2026 18:59
cfdb000 to
73ded2d
Compare
SS-JIA
approved these changes
Nov 3, 2026
CoreAIPartitioner(multifunction=True) lowers every Core AI partition of every method into one Core AI model with one function per partition, named <method>_<partition index> (forward_0, prefill_0, and forward_1 after a graph break). CoreAIBackend.preprocess_multimethod converts all partitions with one TorchConverter, saves or AOT-compiles once, embeds the bundle once, and writes one manifest per delegate naming its function and ordered I/O. Weights shared between methods are stored once, and because function bindings are not part of the runtime cache key, the delegates share one cache entry: the first load specializes and the rest restore from the same bookmark. No runtime or manifest format change is needed. Every partition must carry identical compile specs, and input_enumerations is not supported with the option yet. Without the option, lowering is unchanged.
Delegates of a multi-function export reference one Core AI bundle, but each delegate still loaded its own model and its own copy of the weights. The new boolean backend option coreai_share_multifunction_model (default false) makes them share one loaded model per process: the first delegate acquires it and the others reuse it until the last one is destroyed. Multi-function manifests now carry "multifunction": true so the runtime can tell them apart. Setting the option on any other delegate fails init with InvalidArgument. Sharing is scoped to the cache directory (identified by the open directory's device and inode, so symlinks count as the same directory) and to the cache key, which already includes the compute-unit specialization. Different cache directories or compute units load separate models. Delegates sharing a model run one at a time through a per-model lock. The SDK does not document concurrent execution on one model as safe, and without the lock coreai_multithread_test produced wrong outputs. Init now makes one acquire call and every handle holds an ETCoreAILoadedModel; the three runtime options share one runtime_option helper.
acquire_loaded_model held one process-wide mutex through acquire_cached_model, which can run a full specialization, so a cold shared load of one model blocked shared loads of every other model and cache directory. Shared loads now record in-flight keys: a load of a key already being acquired waits for that acquisition, while other keys load in parallel. A failed acquisition leaves no entry and wakes its waiters, one of which then acquires the model itself. Non-shared loads are unchanged.
metascroy
force-pushed
the
coreai-rename-packaging
branch
from
November 4, 2026 00:11
5d03055 to
884d214
Compare
metascroy
force-pushed
the
coreai-multifunction
branch
from
November 4, 2026 00:11
2b55aa0 to
216529d
Compare
metascroy
added a commit
that referenced
this pull request
Nov 4, 2026
…24525) Makes Core AI exports that use `input_enumerations` load and run, including with multi-function export. Stacked on #24522. ## Problem Every enumerated-shape PTE failed at `load_method`: ``` Core AI execution error: Model has no function named 'main' (0x23) ``` `AIProgram.set_static_shape_config` erases the delegate's dynamic `main` and replaces it with one static clone per shape (`main_s31_4`, `main_s31_16`, ...). The manifest still named `main`, and the runtime binds exactly that name. On the real SDK, a specialized model exposes only the clones, each with a static input descriptor. ## Export An enumerated delegate's manifest lists its per-shape functions instead of one `function`: ```json "enumerated_functions": [ {"function": "main_s31_4", "input_shapes": [[4, 8]]}, {"function": "main_s31_16", "input_shapes": [[16, 8]]}, {"function": "main_s31_32", "input_shapes": [[32, 8]]} ] ``` `input_shapes` gives every input's shape in `input_names` order, the same order ExecuTorch passes arguments in. Delegates without enumerations keep `function: "main"`. If an enumerated value makes a derived boundary dim zero or negative (`s0 - 1` at `s0=1`), export fails with an error naming the input, the dim and the value. ## Runtime - **Manifest parsing:** a manifest must have exactly one of `function` and `enumerated_functions`. Enumerated entries must have distinct names, one shape per input, positive integer dims, and no two entries with the same shapes. Enumerated delegates carry an explicit `enumerated` flag. - **Init:** binds one session per listed function. Any bind failure fails init. - **Execute:** validates the arguments once, then picks the function whose shapes equal the inputs. This happens inside the per-model execution lock. Inputs that match none fail with `InvalidArgument` before anything runs, and the log names the shapes received. - **Sharing:** all functions come from one model, so they share weights, one specialization and one cache entry. - **Cost:** the number of functions is the product of the enumerated value counts. On an M4 Max, a warm load took 7 ms with 1 function, 41 ms with 16 and 248 ms with 64 (documented in the README). ## Multi-function export `CoreAIPartitioner(multifunction=True)` accepts `input_enumerations`. Give each method its own partitioner to enumerate only some methods, or different values per method: ```python partitioner={ "decode": [CoreAIPartitioner(multifunction=True)], "prefill": [CoreAIPartitioner(multifunction=True, input_enumerations=[{0: [64, 128]}])], } ``` - **Naming:** each enumerated partition expands into its own functions in the one shared model, `prefill_0_<key>`, plus `prefill_1_<key>` past a graph break. Unenumerated partitions stay single functions (`decode_0`). - **Binding order:** every partition is bound before any is specialized, because specializing a partition erases its function. - **Compile specs:** `input_enumerations` may differ between methods. Every other compile spec (AOT config, deployment floor) must match. - **Errors:** export fails with a clear error when some methods omit `multifunction=True`, naming those methods, and when two delegates would generate the same function name. - **Runtime sharing:** works with `coreai_share_multifunction_model`; every delegate and per-shape function shares one loaded model. ## Shared-load robustness This PR also tightens #24476's per-key shared loading: - A scope guard clears a load's in-flight key, publishes the model and wakes waiters on every exit, so a key can't stay marked as loading. - `WaiterAcquiresAfterConcurrentSharedLoadFails` confirms the second load is actually waiting before the first one fails. It uses a test-only `BeforeSharedLoadWait` fault point. ## Test plan **Python.** All 171 CoreAI tests pass. - `test_shape_config.py`: `test_manifest_lists_one_function_per_shape`, `test_unenumerated_lowering_keeps_main`, `test_derived_dim_that_reaches_zero_is_rejected`. The real-runtime test `test_runs_each_enumerated_shape_and_rejects_others` runs each shape and checks rejection. - `test_multifunction.py`: - `test_enumerated_method_lists_per_shape_functions`, `test_graph_break_enumerates_every_partition`. - `test_other_compile_specs_must_still_match`, `test_mixed_partitioners_name_the_missing_flag`, `test_duplicate_function_names_are_rejected`. - The real-runtime test `test_enumerated_shapes_dispatch_by_input_shape` covers multi-method with a graph break. **Native.** All 6 CTest targets pass. - `ParsesEnumeratedFunctions`, `RejectsInvalidEnumeratedFunctions` (16 malformed cases, plus both or neither field present). - `EnumeratedFunctionsDispatchByInputShape`: checks which function ran, the resized output, and that an unmatched shape runs nothing. - `EnumeratedFunctionBindFailureFailsInit`. - The sharing tests, including `WaiterAcquiresAfterConcurrentSharedLoadFails`, passed in 5 of 5 runs. **Real SDK, M4 Max:** - **Single method:** rows 4/16/32 match eager (max error ≤1.8e-7) on cold and warm loads. Rows 8 is rejected with `InvalidArgument`. - **Multi-method, C++ runner:** a static `forward` plus an enumerated `prefill`, with a graph break. That's 4 delegates and 8 functions in one model. With sharing off and on, cold and warm, 100 of 100 calls pass in each configuration (worst max error 1.4e-6), and an unmatched shape is rejected with `0x12`. - **AOT (`aimodelc`, `h16c`):** the enumerated functions compile and run (max error ≤1.8e-7). ## Compatibility PTEs exported with `input_enumerations` before this change never loaded, so nothing that worked before breaks. Every other export keeps the `function` manifest field.
Adds a `coreai_expect_frequent_reshapes` runtime option that exposes the Core AI SDK's `SpecializationOptions.expectFrequentReshapes`. Stacked on #24476. ## Usage ```cpp BackendOptions<2> options; options.set_option("coreai_expect_frequent_reshapes", true); options.set_option("coreai_compute_unit", "prefer_gpu"); // optional; combines with it LoadBackendOptionsMap map; map.set_options("CoreAIBackend", options.view()); module.load(map); ``` Bool, default `false`. Set it for models whose input shapes change between calls. For AOT exports, the matching export-time setting is `AOTCompileConfig(expect_frequent_reshapes=True)`, which already existed. ## Why this option Checked against the Xcode 27 SDK by type-checking each member through `import CoreAI`. Of `SpecializationOptions`, the public surface is `.default`, `.cpuOnly`, `init(preferredComputeUnitKind:)`, read-only `allowedComputeUnitKinds` / `preferredComputeUnitKind`, and read/write `expectFrequentReshapes`. Everything except `expectFrequentReshapes` is already covered by `coreai_compute_unit`. The framework exports other symbols (`.interpreterOnly`, `init(allowedComputeUnitKinds:preferredComputeUnitKind:)`, `enableEncodingFunctions`, ...), but none of them are usable from `import CoreAI`. The SDK has no doc comment for `expectFrequentReshapes`. The README describes it by its name only. ## How it works * `CoreAIBackend::init` reads the option with the shared `runtime_option` helper. A non-boolean value fails init with `InvalidArgument`. * `ETCoreAICreateModelLoader` takes the flag next to the compute unit. The Swift loader sets `expectFrequentReshapes` on the `SpecializationOptions` chosen for that compute unit. * No cache-key change is needed. The specialization identifier already encodes `expectFrequentReshapes`, so each value gets its own specialization, bookmark and shared model (`coreai_share_multifunction_model` only shares between delegates that agree on it). * Cache eviction creates its loader with the flag off, since eviction targets the bookmark and the specialization doesn't matter. ## Test plan * `CoreAIDelegateTest.ExpectFrequentReshapesOptionReachesLoader`: absent / `false` / `true` reach the loader factory, alongside `coreai_compute_unit`. * `CoreAIDelegateTest.OptionsRejectWrongTypes`: an integer value fails init. * `SwiftBridgeTests.testComputeUnitsMapToSDKSpecializationOptions`: every compute unit, with and without reshapes, maps to the expected `SpecializationOptions`, and all 10 combinations have distinct identifiers. * Real SDK (M4 Max, multi-function PTE with sharing on): with the option, the model specializes under its own cache key, a warm load restores it, and outputs match eager. Without it, the default key is used. That gives 2 bookmarks in one cache directory. * All 6 native CTest targets pass.
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an opt-in way to export several ExecuTorch methods (and graph-broken partitions) as one Core AI model with multiple functions, so methods that share weights ship and cache them once, plus an opt-in runtime option so those delegates also share one loaded model in memory.
Stacked on #24474.
Usage
Each Core AI partition becomes one function named
<method>_<partition index>: hereforward_0andprefill_0, and a graph break addsforward_1,prefill_1, and so on. Graph breaks are allowed; partitions running on CPU stay in ExecuTorch as usual.To also share one loaded model at runtime, set the backend option when loading:
How it works
CoreAIBackend.preprocess_multimethodreceives every method's partitions at once.TorchConverter(one entrypoint per partition), callsto_coreai()once, and saves (or AOT-compiles withcoreai-build) once.NamedDataStoreonce, under a singleexport_hash.function,input_namesandoutput_names; all manifests point at the same bundle."multifunction": true(optional boolean; absent means false).coreai_share_multifunction_model, bool, defaultfalse):coreai_multithread_testproduced wrong outputs.InvalidArgument.preprocessand the multi-method path now share the conversion and delivery helpers, and the existing tests still pass.input_enumerationsis combined with the option. Enumerated shapes rename functions internally and need their own design.Results
Two-method model (8 ×
Linear(1024, 1024), fp32;forwardwith 1 row,prefillwith 16 rows), run on an M4 Max with a standalone C++ runner:multifunction=Truemultifunction=True+ graph break (4 functions)Shared weights are stored once: in a direct converter check, adding a second function to an 8.4 MB model added 2.9 KB.
Runtime sharing, measured as resident memory after one run per delegate, for K delegates of the same 33.6 MB model:
coreai_share_multifunction_model=trueWith sharing on, 2, 4 and 8 threads × 50 concurrent runs produced exact outputs, in about the same total time as unshared on this model.
Test plan
backends/apple/coreai/test/test_multifunction.py:input_enumerations.export_hash,path, files, data-store keys) withforward_0/prefill_0, and the combined bundle is about the size of a single method's.forward_0,forward_1,prefill_0,prefill_1in one bundle.mainmodel per delegate (as today).coreai-build(one build, sharedarchs) and with the realcoreai-build.ParsesOptionalMultifunctionFlag: flag absent, true, false, and non-boolean values.ShareOptionRequiresMultifunctionExport,OptionsRejectWrongTypes.ShareOptionSharesOneModelUntilLastDelegate,SharedModelsAreScopedToCacheDirectory(separate directories don't share; a symlink alias does).SharedModelSerializesConcurrentExecution,ConcurrentFirstSharedLoadsAcquireOnce,FailedSharedAcquisitionLeavesNoEntry.SharedLoadsOfDifferentModelsRunConcurrently,WaiterAcquiresAfterConcurrentSharedLoadFails.coreai_multithread_testadds cold and warm rounds on a multi-function stress fixture with sharing on (real SDK).coreai_fp32377 passed,coreai_fp16368 passed, 0 failures.