You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
coreml_static_int8 operators suite never finishes on macOS (hits the 180 min cap) #21623
The coreml_static_int8 half of test-backend-macos (operators suite) has never completed. It runs to the 180 minute job cap and gets cancelled, so it produces no signal at all.
Measured on a recent main commit:
flow / suite
duration
coreml / models
12 min
coreml / operators
11 min
coreml_static_int8 / models
25 min
coreml_static_int8 / operators
181 min, cancelled at the cap
Both flows are built by the same _create_coreml_flow in backends/test/suite/flows/coreml.py. coreml_static_int8 is the identical models + operators suite with quantize=True and minimum_deployment_target=iOS17. So the cost is a full PT2E prepare / calibrate / convert pass on every single test case, not larger weights.
#21609 moved this flow to the nightly schedule, so it no longer consumes the shared macOS pool on every commit. That limits the damage but does not fix it: the nightly run will keep hitting the same cap, and the int8 flow will keep reporting nothing.
Possible directions, roughly in order of effort:
Shard the operators suite across several jobs so each one finishes inside the cap.
Reuse a single quantized artifact across cases where the operator under test allows it, instead of quantizing per case.
Trim the int8 case list to the operators where quantization actually changes the lowering.
Until one of those lands, the int8 operators coverage should be treated as absent rather than passing.
I profiled this on macOS and the premise holds — it is the PT2E pass — but the cost turns out
to be one specific, fixable thing rather than something the three directions above would
address. Numbers below are from an M4 (16 GB), torch 2.14.1 + coremltools 9.0, which matches
what CI pins (torch==2.14.0, COREMLTOOLS_VERSION="9.0").
Where the time goes. Replicating runner.py's stages per case:
case
flow
quantize
export+lower
conv2d
coreml
—
0.67 s
conv2d
coreml_static_int8
36.88 s
3.16 s
linear
coreml
—
0.93 s
linear
coreml_static_int8
46.76 s
3.77 s
95% of the int8 cost is the quantize stage. Breaking that stage down on Conv2d(3, 8, 3):
sub-step
time
share
torch.export(strict=True)
0.15 s
0.5%
CoreMLQuantizer(...) ctor
0.00 s
0.0%
prepare_pt2e()
28.29 s
99.4%
calibrate
0.02 s
0.1%
convert_pt2e()
0.01 s
0.0%
The actual cause.prepare_pt2e spends its time in TorchDynamo, because annotation
rebuilds its subgraph-match patterns from scratch. Instrumenting _get_aten_graph_module in backends/apple/coreml/quantizer/_coreml_quantizer_utils.py
for a single prepare_pt2e:
pattern graph builds : 405
time in them : 28.24 s (99.1% of prepare_pt2e)
mean per build : 0.070 s
distinct (pattern, shapes) : 9 -> 396 of 405 builds are duplicates (97.8%)
The same nine pattern graphs are exported 405 times per case. And it is not amortized across
cases — five consecutive prepare_pt2e calls in one process, with fresh models:
28.94 s, 32.24 s, 34.23 s, 35.40 s, 32.16 s
The five getters (get_conv_pattern, get_conv_bn_pattern, get_linear_pattern, get_linear_bn_pattern, get_binary_op_act_pattern) take only hashable arguments
(conv_dim: int, act_fn, act_in_place: bool) and build their example_inputs internally
at fixed shapes, so the result is a pure function of the arguments.
One trap worth flagging, because the obvious fix is wrong: a plain lru_cache on those
getters breaks on the second use with
AssertionError: Expecting the pattern graph to return a tuple
SubgraphMatcherWithNameNodeMap.__init__ calls _split_to_graph_and_name_node_map(pattern_gm),
which mutates the pattern it is handed, stripping name_node_map out of the return tuple.
So the cache has to hand out a fresh copy. That is still overwhelmingly worth it: building one
pattern costs 62.0 ms, deepcopy of it costs 0.7 ms, about 93x cheaper.
Measured with cache + copy, same model, output verified identical:
What that implies for the suite. I cannot run all 467 operator tests to completion here, so
this is arithmetic on the per-case numbers rather than a measurement: the per-case quantize cost
drops from ~29 s to ~0.5 s, with one ~29 s warmup per xdist worker. Total quantize work goes from
roughly 467 x 29 s to (workers x 29 s) + (467 x 0.5 s), which should put the suite near the coreml flow's 11 min rather than 181 min — comfortably inside the cap, without sharding,
trimming the case list, or reusing quantized artifacts.
Happy to send this as a PR against _coreml_quantizer_utils.py if that sounds right. It would
also speed up every other user of the CoreML quantizer, not just CI.
One caveat: all of the above is a single-process measurement on one machine, and the 181 min
figure in the issue is a CI job with -n auto. If someone can land the change behind the
nightly run, the real suite time would settle whether the arithmetic holds.
Correcting a number I got wrong above, before anyone spends time on it.
I originally wrote that a single prepare_pt2e performs "405 pattern builds of which only 9
are distinct, 396 duplicates (97.8%)". That characterization was wrong. It came from
keying my instrumentation on type(pattern).__qualname__, and every pattern class is defined
inside its builder function, so conv1d/conv2d/conv_transpose and the activation variants all
report the same qualname and collapsed together in my count.
Re-instrumented with the getters' actual arguments:
per prepare_pt2e
405 builds, all 405 distinct within the call
across 3 different models
1215 builds, 405 distinct overall
key set, linear model vs conv model
identical
So there is no duplication within a single annotate. The waste is entirely across models:
the pattern set is completely independent of the model being annotated, and every model after
the first rebuilds all 405 from scratch. For the 467-case operators suite that is roughly
189,000 builds where 405 would do.
Everything else in the writeup is unaffected and was measured directly rather than inferred:
the 405 builds being 99.1% of prepare_pt2e, the 95%/99.4% stage split, the five
non-amortizing consecutive calls, the 55x warm speedup, and the byte-identical quantized
output. The fix is the same, and if anything this is a cleaner justification for it — the
patterns are model-independent, which is exactly why memoizing them is safe.
The commit message and PR description are updated.
Activity
Drop the coreml_static_int8 operators job, which has never once finis…
vijay-kapse commentedon Oct 1, 2026
I profiled this on macOS and the premise holds — it is the PT2E pass — but the cost turns out
to be one specific, fixable thing rather than something the three directions above would
address. Numbers below are from an M4 (16 GB), torch 2.14.1 + coremltools 9.0, which matches
what CI pins (
torch==2.14.0,COREMLTOOLS_VERSION="9.0").Where the time goes. Replicating
runner.py's stages per case:coremlcoreml_static_int8coremlcoreml_static_int895% of the int8 cost is the quantize stage. Breaking that stage down on
Conv2d(3, 8, 3):torch.export(strict=True)CoreMLQuantizer(...)ctorprepare_pt2e()convert_pt2e()The actual cause.
prepare_pt2espends its time in TorchDynamo, because annotationrebuilds its subgraph-match patterns from scratch. Instrumenting
_get_aten_graph_moduleinbackends/apple/coreml/quantizer/_coreml_quantizer_utils.pyfor a single
prepare_pt2e:The same nine pattern graphs are exported 405 times per case. And it is not amortized across
cases — five consecutive
prepare_pt2ecalls in one process, with fresh models:The five getters (
get_conv_pattern,get_conv_bn_pattern,get_linear_pattern,get_linear_bn_pattern,get_binary_op_act_pattern) take only hashable arguments(
conv_dim: int,act_fn,act_in_place: bool) and build theirexample_inputsinternallyat fixed shapes, so the result is a pure function of the arguments.
One trap worth flagging, because the obvious fix is wrong: a plain
lru_cacheon thosegetters breaks on the second use with
SubgraphMatcherWithNameNodeMap.__init__calls_split_to_graph_and_name_node_map(pattern_gm),which mutates the pattern it is handed, stripping
name_node_mapout of the return tuple.So the cache has to hand out a fresh copy. That is still overwhelmingly worth it: building one
pattern costs 62.0 ms,
deepcopyof it costs 0.7 ms, about 93x cheaper.Measured with cache + copy, same model, output verified identical:
What that implies for the suite. I cannot run all 467 operator tests to completion here, so
this is arithmetic on the per-case numbers rather than a measurement: the per-case quantize cost
drops from ~29 s to ~0.5 s, with one ~29 s warmup per xdist worker. Total quantize work goes from
roughly 467 x 29 s to (workers x 29 s) + (467 x 0.5 s), which should put the suite near the
coremlflow's 11 min rather than 181 min — comfortably inside the cap, without sharding,trimming the case list, or reusing quantized artifacts.
Happy to send this as a PR against
_coreml_quantizer_utils.pyif that sounds right. It wouldalso speed up every other user of the CoreML quantizer, not just CI.
One caveat: all of the above is a single-process measurement on one machine, and the 181 min
figure in the issue is a CI job with
-n auto. If someone can land the change behind thenightly run, the real suite time would settle whether the arithmetic holds.
[CoreML] Cache the quantizer's pattern graphs instead of rebuilding t…
vijay-kapse commentedon Oct 2, 2026
Correcting a number I got wrong above, before anyone spends time on it.
I originally wrote that a single
prepare_pt2eperforms "405 pattern builds of which only 9are distinct, 396 duplicates (97.8%)". That characterization was wrong. It came from
keying my instrumentation on
type(pattern).__qualname__, and every pattern class is definedinside its builder function, so conv1d/conv2d/conv_transpose and the activation variants all
report the same qualname and collapsed together in my count.
Re-instrumented with the getters' actual arguments:
prepare_pt2eSo there is no duplication within a single annotate. The waste is entirely across models:
the pattern set is completely independent of the model being annotated, and every model after
the first rebuilds all 405 from scratch. For the 467-case operators suite that is roughly
189,000 builds where 405 would do.
Everything else in the writeup is unaffected and was measured directly rather than inferred:
the 405 builds being 99.1% of
prepare_pt2e, the 95%/99.4% stage split, the fivenon-amortizing consecutive calls, the 55x warm speedup, and the byte-identical quantized
output. The fix is the same, and if anything this is a cleaner justification for it — the
patterns are model-independent, which is exactly why memoizing them is safe.
The commit message and PR description are updated.