From b172a368c410e07a23f7e1d6c4e47ac461bb1506 Mon Sep 17 00:00:00 2001 From: Vijay Suryakant Kapse Date: Thu, 1 Oct 2026 18:40:15 -0400 Subject: [PATCH 1/4] [CoreML] Cache the quantizer's pattern graphs instead of rebuilding them 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 #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. --- .../quantizer/_coreml_quantizer_utils.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/backends/apple/coreml/quantizer/_coreml_quantizer_utils.py b/backends/apple/coreml/quantizer/_coreml_quantizer_utils.py index e7389c0135f..e6e81cd94e1 100644 --- a/backends/apple/coreml/quantizer/_coreml_quantizer_utils.py +++ b/backends/apple/coreml/quantizer/_coreml_quantizer_utils.py @@ -3,6 +3,8 @@ # Use of this source code is governed by a BSD-3-clause license that can be # found in the LICENSE.txt file or at https://opensource.org/licenses/BSD-3-Clause +import copy as _copy +import functools as _functools import itertools as _itertools from typing import ( Callable as _Callable, @@ -283,6 +285,34 @@ def forward( return _get_aten_graph_module(Pattern(), example_inputs) +def _cached_pattern(fn: _Callable) -> _Callable: + """ + Memoize a pattern-graph getter. + + Each getter builds its ``example_inputs`` internally at fixed shapes and takes + only hashable arguments, so its result is a pure function of those arguments. + Without this, annotation re-exports the same handful of pattern graphs for every + model it annotates: a single ``prepare_pt2e`` on a one-conv module performs 405 + pattern builds of which only 9 are distinct, and that dominates its runtime. + + 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: roughly 0.7 ms against 62 ms per pattern. + """ + cache = _functools.lru_cache(maxsize=None)(fn) + + @_functools.wraps(fn) + def wrapper(*args, **kwargs): + return _copy.deepcopy(cache(*args, **kwargs)) + + wrapper.cache_clear = cache.cache_clear + wrapper.cache_info = cache.cache_info + return wrapper + + +@_cached_pattern def get_binary_op_act_pattern( binary_op: _Callable, act_fn: _Optional[_Callable] = None, @@ -322,6 +352,7 @@ def forward(self, input_1, input_2): return _get_aten_graph_module(Pattern(), example_inputs) +@_cached_pattern def get_conv_pattern( conv_dim: int, act_fn: _Optional[_Callable] = None, act_in_place: bool = False ) -> _torch.nn.Module: @@ -347,6 +378,7 @@ def get_conv_pattern( ) +@_cached_pattern def get_conv_bn_pattern( conv_dim: int, act_fn: _Optional[_Callable] = None, act_in_place: bool = False ) -> _torch.nn.Module: @@ -376,6 +408,7 @@ def get_conv_bn_pattern( ) +@_cached_pattern def get_linear_pattern( act_fn: _Optional[_Callable] = None, act_in_place: bool = False ) -> _torch.nn.Module: @@ -395,6 +428,7 @@ def get_linear_pattern( return _get_weighted_mod_pattern(_F.linear, example_inputs, act_fn, act_in_place) +@_cached_pattern def get_linear_bn_pattern( act_fn: _Optional[_Callable] = None, act_in_place: bool = False ) -> _torch.nn.Module: From 56075d45da1bd8fdae06fde8acf08c06274a8e69 Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Fri, 9 Oct 2026 16:01:10 -0700 Subject: [PATCH 2/4] [CoreML][CI] Temporarily enable static-int8 operators for PR #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. --- .github/workflows/test-backend-coreml.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-backend-coreml.yml b/.github/workflows/test-backend-coreml.yml index fca10b619ce..422bf4eb328 100644 --- a/.github/workflows/test-backend-coreml.yml +++ b/.github/workflows/test-backend-coreml.yml @@ -41,17 +41,10 @@ jobs: contents: read with: backend: coreml - # The heavier coreml_static_int8 macOS matrix saturates the shared - # macOS runner pool, so run it only on the nightly schedule. Every-commit - # events (push, pull_request) run the lighter coreml flow only. - flows: >- - ${{ github.event_name == 'schedule' - && '["coreml", "coreml_static_int8"]' - || '["coreml"]' }} - # The operators suite under coreml_static_int8 has never finished inside the - # 180 minute limit, so it is cancelled every night after burning about three - # macOS runner hours for no result. See issue #21623. - exclude: '[{"flow": "coreml_static_int8", "suite": "operators"}]' + # TEMPORARY: measure static-int8 operator runtime for PR #23323 / issue #21623. + # Remove this override before landing; retain the normal coreml jobs. + flows: '["coreml", "coreml_static_int8"]' + exclude: '[{"flow": "coreml_static_int8", "suite": "models"}]' ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} timeout: 180 run-macos: true From 7561cf7d7cbeb288cf58aa2fb5e010e87244b770 Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Mon, 12 Oct 2026 16:55:32 -0700 Subject: [PATCH 3/4] [CoreML][CI] Restore original workflow after timing experiment 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 d635299820ea8affe3a34f3fa6d80167ddafbd41. Diff whitespace check passed. --- .github/workflows/test-backend-coreml.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-backend-coreml.yml b/.github/workflows/test-backend-coreml.yml index 422bf4eb328..fca10b619ce 100644 --- a/.github/workflows/test-backend-coreml.yml +++ b/.github/workflows/test-backend-coreml.yml @@ -41,10 +41,17 @@ jobs: contents: read with: backend: coreml - # TEMPORARY: measure static-int8 operator runtime for PR #23323 / issue #21623. - # Remove this override before landing; retain the normal coreml jobs. - flows: '["coreml", "coreml_static_int8"]' - exclude: '[{"flow": "coreml_static_int8", "suite": "models"}]' + # The heavier coreml_static_int8 macOS matrix saturates the shared + # macOS runner pool, so run it only on the nightly schedule. Every-commit + # events (push, pull_request) run the lighter coreml flow only. + flows: >- + ${{ github.event_name == 'schedule' + && '["coreml", "coreml_static_int8"]' + || '["coreml"]' }} + # The operators suite under coreml_static_int8 has never finished inside the + # 180 minute limit, so it is cancelled every night after burning about three + # macOS runner hours for no result. See issue #21623. + exclude: '[{"flow": "coreml_static_int8", "suite": "operators"}]' ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} timeout: 180 run-macos: true From a212d8328099eb84a0eed978100cc5df2380078b Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Mon, 12 Oct 2026 17:01:56 -0700 Subject: [PATCH 4/4] [CoreML][CI] Re-enable nightly static-int8 operator coverage 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. --- .github/workflows/test-backend-coreml.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test-backend-coreml.yml b/.github/workflows/test-backend-coreml.yml index fca10b619ce..0f270de0e8c 100644 --- a/.github/workflows/test-backend-coreml.yml +++ b/.github/workflows/test-backend-coreml.yml @@ -48,10 +48,6 @@ jobs: ${{ github.event_name == 'schedule' && '["coreml", "coreml_static_int8"]' || '["coreml"]' }} - # The operators suite under coreml_static_int8 has never finished inside the - # 180 minute limit, so it is cancelled every night after burning about three - # macOS runner hours for no result. See issue #21623. - exclude: '[{"flow": "coreml_static_int8", "suite": "operators"}]' ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} timeout: 180 run-macos: true