Skip to content

coreml_static_int8 operators suite never finishes on macOS (hits the 180 min cap) #21623

Open

Description

@shoumikhin

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.

cc @kimishpatel @YifanShenSZ @cymbalrush @metascroy

Activity

  1. added
    module: ciIssues related to continuous integration
    module: coremlIssues related to Apple's Core ML delegation and code under backends/apple/coreml/
    on Aug 18, 2026
  2. vijay-kapse commented on Oct 1, 2026

    @vijay-kapse

    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:

    baseline                   : 28.78 s
    cache+copy call 1 (cold)   : 29.69 s
    cache+copy calls 2-4 (warm):  0.67 s   (0.52 s, 0.52 s, 0.96 s)   -> 43x
    identical node targets     : True
    identical graph code       : True
    

    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.

  3. vijay-kapse commented on Oct 2, 2026

    @vijay-kapse

    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.

  4. self-assigned this
    on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugmodule: ciIssues related to continuous integrationmodule: coremlIssues related to Apple's Core ML delegation and code under backends/apple/coreml/

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions