Repository navigation
[CoreML] Cache the quantizer's pattern graphs instead of rebuilding them per model - #23323
vijay-kapse wants to merge 5 commits into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/23323
Note: Links to docs will display an error until the docs builds have been completed. ❌ 1 New Failure, 2 Unrelated FailuresAs of commit a212d83 with merge base d036d50 ( NEW FAILURE - The following job has failed:
BROKEN TRUNK - The following job failed but were present on the merge base:👉 Rebase onto the `viable/strict` branch to avoid these failures
UNSTABLE - The following job is marked as unstable, possibly due to flakiness on trunk:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
Hi @vijay-kapse! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
|
|
@pytorchbot label "release notes: apple" |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
…hem per model
The CoreML quantizer's annotation builds its subgraph-match patterns by exporting
small pattern modules, and it rebuilds every one of them for each model it
annotates. A single prepare_pt2e performs 405 pattern builds, and those builds are
99.1% of its runtime:
prepare_pt2e total : 28.51s
pattern graph builds : 405
time in them : 28.24s (99.1% of prepare_pt2e)
mean per build : 0.070s
The patterns do not depend on the model being annotated. Instrumenting the getters
with their actual arguments over three different models shows 1215 builds with only
405 distinct argument signatures, and the key set for a linear model is identical to
the key set for a conv model. So every model after the first rebuilds, from scratch,
patterns that are byte-for-byte the ones already built.
Nothing amortized it. Five consecutive prepare_pt2e calls in one process, each on a
fresh module, cost 28.94s, 32.24s, 34.23s, 35.40s, 32.16s.
The five getters take only hashable arguments and build their example_inputs
internally at fixed shapes, so each is a pure function of its arguments and can be
memoized.
The cached graph cannot be handed out directly. SubgraphMatcherWithNameNodeMap
splits name_node_map out of the pattern's return tuple in place, so a second
consumer of the same object fails with "Expecting the pattern graph to return a
tuple". Callers therefore get a copy, which is far cheaper than rebuilding: about
0.7 ms against 62 ms per pattern.
Measured with this change, prepare_pt2e plus convert_pt2e on an M4, alternating
conv and linear modules so the cache is exercised across model types:
case 1 (conv2d): 29.92s <- cold, builds the 405 patterns once
case 2 (conv2d): 0.53s
case 3 (linear): 0.54s
case 4 (conv2d): 0.55s
case 5 (linear): 0.54s
55x on warm calls. The quantized output is unchanged: the fully converted graphs
for a conv module and a linear module are byte identical before and after, both in
node targets and in printed code.
Reported in pytorch#21623, where the coreml_static_int8 operators suite runs to the 180
minute CI cap and is cancelled. Per-case quantize there drops from roughly 29s to
0.5s with one warmup per worker process.
b19165b to
b172a36
Compare
|
Correcting a number I got wrong above, before anyone spends time on it. I originally wrote that a single Re-instrumented with the getters' actual arguments:
So there is no duplication within a single annotate. The waste is entirely across models: Everything else in the writeup is unaffected and was measured directly rather than inferred: The commit message and PR description are updated. |
|
Testing CI on trunk |
…23323 Measure whether the pattern-cache optimization lets the previously timed-out operator suite complete within the existing 180-minute limit. Retain normal CoreML coverage and avoid an additional static-int8 models run. Remove this temporary commit before landing. Test Plan: Parsed the workflow YAML and verified the exact three-job matrix, unchanged timeout and PR ref, and no other semantic changes. Runtime measurement pending CI. Diff whitespace check passed; repository validator unavailable because PHP is missing.
|
|
|
Thanks for triggering CI. Three jobs went red and I do not think any of them is this PR — here is what I checked.
For contrast, in the scheduled trunk run This PR only touches |
|
I'm testing whether this change brings the coreml_static_int8 operators suite within the existing 180-minute CI timeout. The temporary workflow commit enables that suite for this measurement. Fresh CI run: https://github.com/pytorch/executorch/actions/runs/38280085324 I'll check the operators job's completion time separately from the unrelated coreml models job. |
The CI timing experiment is complete. Restore the original scheduling and exclusions so the temporary static-int8 override does not land with the quantizer optimization. Test Plan: Verified the complete restored tree is identical to the pre-experiment PR commit d635299. Diff whitespace check passed.
Pattern caching lets the previously timing-out operator suite complete within the existing limit. Restore nightly coverage while retaining the lighter CoreML-only policy for PRs and pushes. Test Plan: Verified YAML and the event matrices: scheduled runs include both static-int8 suites; PR, push and manual events remain CoreML-only. Diff whitespace check passed. Benchmark run https://github.com/pytorch/executorch/actions/runs/38280085324/job/114915056342 completed in 15m32s with pytest in 328.75s (382 passed, 20 failed, 65 skipped); completion confirms timing, not an all-passing suite. Repository validator could not start because PHP is missing.
|
Looks good to land once CI is green. Thanks for the contribution! |
Fixes the root cause behind #21623.
Problem
The CoreML quantizer builds its subgraph-match patterns by exporting small pattern modules,
and it rebuilds them for every model it annotates. Instrumenting
_get_aten_graph_modulefor a single
prepare_pt2eon a one-conv module:The patterns do not depend on the model being annotated. Instrumenting the getters with
their actual arguments across three different models gives 1215 builds with only 405 distinct
argument signatures, and the key set for a linear model is identical to the key set for a
conv model. Every model after the first rebuilds patterns that were already built.
Nothing amortizes it. Five consecutive
prepare_pt2ecalls in one process, each on a freshmodule:
For context on where that lands, the quantize stage is 95% of the per-case cost of the
coreml_static_int8flow, andprepare_pt2eis 99.4% of the quantize stage:torch.export(strict=True)CoreMLQuantizer(...)ctorprepare_pt2e()convert_pt2e()Change
Memoizes the five pattern getters. Each builds its
example_inputsinternally at fixedshapes and takes only hashable arguments (
conv_dim: int,act_fn,act_in_place: bool),so each is a pure function of its arguments.
The cached graph cannot be handed out directly, which is worth knowing because the
obvious
lru_cacheis incorrect here.SubgraphMatcherWithNameNodeMap.__init__calls_split_to_graph_and_name_node_map(pattern_gm), which splitsname_node_mapout of thepattern's return tuple in place; a second consumer of the same object then fails with:
So callers get a copy. That is still overwhelmingly worth it: building one pattern costs
62.0 ms,
deepcopyof it costs 0.7 ms, roughly 93x cheaper.Measured
prepare_pt2e+convert_pt2eon an M4, alternating conv and linear modules so the cache isexercised across model types:
55x on warm calls.
Output is unchanged
The fully converted graph for a conv module and for a linear module is byte identical before
and after this change, both in node targets and in
print_readable()output — produced byrunning the same script against the unpatched and patched file and diffing.
Effect on #21623
Per-case quantize in that suite drops from roughly 29 s to 0.5 s, with one ~29 s warmup per
xdist worker process. I could not run all 467 operator tests to completion locally, so the
suite-level figure is arithmetic rather than a measurement: total quantize work goes from
about 467 x 29 s to (workers x 29 s) + (467 x 0.5 s), which should bring the flow near the
coremlflow's 11 min instead of hitting the 180 min cap. A nightly run would confirm it.This also speeds up every other user of the CoreML quantizer, not only CI.
Environment: macOS 26.5 on an M4, torch 2.14.1, coremltools 9.0 (matching
COREMLTOOLS_VERSION="9.0"and thetorch==2.14.0pin).cc @kimishpatel @YifanShenSZ @cymbalrush @metascroy @nil-is-all