Skip to content

Core: sweep every transformers model_type across a process pool - #8995

Merged
danielhanchen merged 2 commits into
mainfrom
parallel-compiler-sweep
Aug 16, 2026
Merged

Core: sweep every transformers model_type across a process pool#8995
danielhanchen merged 2 commits into
mainfrom
parallel-compiler-sweep

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Compiler full-model-sweep is the second most expensive step in Core at 415.7s, and it runs once per matrix leg, so it costs about 21 minutes of runner time per push.

Where the time goes

The step is 6 tests. One of them is essentially all of it:

84.84s call  test_compile_every_transformers_model_type
 0.02s call  test_compile_real_modeling_module[gemma3-Gemma3RMSNorm]
 0.01s call  test_compile_real_modeling_module[qwen3-Qwen3RMSNorm]
 0.01s call  test_compile_real_modeling_module[llama-LlamaRMSNorm]

That test walks every model_type the matrix's transformers ships and compiles each one, in a serial loop.

There is nothing to shave inside the loop. Splitting the cost over the 359 model_types that actually have a modeling module:

phase total mean
importing 2.6s 0.007s
compiling 81.8s 0.228s

97% is compilation. The only lever is running it on more than one core.

For the record, the step's own comment estimated about 110s (383 models at ~0.3s). That estimate is accurate; it just describes a faster machine than the runner. At 415.7s the runner is doing ~1.16s per model. There is no mystery overhead here, which is worth stating since I first went looking for one.

The change

Run the sweep over a process pool. The models are independent: each compile reads one transformers modeling module and writes its own cache file.

  • spawn, not fork. The workers start after torch is already loaded in the parent, and forking a loaded torch is a known hazard.
  • Each worker re-imports the shim, so it gets its own hermetic cache directory. Nothing in this test asserts on the cache path. The per-model file assertions live in test_compile_real_modeling_module, which is unchanged and still runs in process.
  • The per-model SIGALRM budget moves into the worker, in its own main thread, so it still bounds a single infinite-looping model_type. That guard exists because transformers >=5,<6 once hung for 30+ minutes.
  • Ordered imap, so the known / new-failure report keeps the same model order it had as a serial loop.
  • 4 workers, matching the runner's vCPU count. More would only add transformers imports and memory. Peak memory should if anything improve: 4 workers importing ~90 modeling modules each, instead of one process importing all 359.

Classification, the report, and both assertions (not new_failures, ok >= 200) are untouched.

Verification

Equally green is not enough for a hard gate, so I checked that every model_type gets the same verdict, not just the same totals:

serial    85.0s   ok=359
pooled    25.7s   ok=359   (4 workers, spawn)
diffs     0       <- across all 383 model_types

Whole step locally, before and after:

wall result
before 84.93s 5 passed, 1 skipped
after 26.13s 5 passed, 1 skipped

ok=359 skipped=24 known-broken=0 new-failures=0 (359 + 24 = 383).

3.25x locally. On the runner that maps to roughly 415.7s to ~130s per leg.

Caveat

This is validated on my machine, not on a GitHub runner, and it is a hard gate. Worth running staging CI on it before merging rather than taking the local numbers on faith.

test_compile_every_transformers_model_type walks every model_type the
matrix's transformers ships and compiles each one. It was a serial loop, it
was the second most expensive step in Core at 415.7s, and it runs once per
matrix leg, so it cost about 21 minutes of runner time per push.

The work is independent per model and it is compile bound, not import bound.
Measured over the 359 model_types that have a modeling module:

    importing   2.6s total   0.007s mean
    compiling  81.8s total   0.228s mean

So there is nothing to shave inside the loop; the only lever is running it on
more than one core.

Uses spawn rather than fork, since the workers start after torch is already
loaded in the parent. Each worker re-imports the shim and gets its own
hermetic cache directory. Nothing in this test asserts on the cache path; the
per-model file assertions are in test_compile_real_modeling_module, which is
unchanged and still runs in process. Ordered imap keeps the known and
new-failure report in the same model order as before.

Verified identical, not just equally green: every one of the 383 model_types
gets the same verdict serially and pooled, 0 differences.

    serial    85.0s   ok=359
    pooled    25.7s   ok=359   4 workers
    diffs     0

Whole step locally: 84.93s to 26.13s, 5 passed and 1 skipped either way.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 9aa64c396b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

A worker that dies mid-task (segfault, OOM killer) never sets its result, and
Pool.imap then blocks forever: python/cpython#66587, open since 2014.
_maintain_pool replaces the worker but the in-flight job's cache entry is
never set, so the parent waits on a condition nothing will signal.

The serial loop could not lose a worker -- a segfault killed pytest outright
and turned the leg red in seconds. Pooled, the same crash would sit until the
job's 35 minute timeout, which costs far more than running the sweep pooled
ever saves. Drive the iterator with next(timeout=600) and turn a stall into a
named AssertionError.

chunksize drops from 4 to 1, and that is required rather than incidental:
Pool.imap only returns an IMapIterator, the thing that has a timeout, when
chunksize == 1. For chunksize > 1 it returns a bare generator expression over
the chunks (Lib/multiprocessing/pool.py:396-420), which has no
next(timeout=...) at all. Per-task IPC is noise next to a ~1s compile:

    chunksize=4   26.13s
    chunksize=1   28.39s

both ok=359 skipped=24 known-broken=0 new-failures=0, matching serial.

Guard verified against a worker killed mid-task: fires after the budget with
the results collected so far, instead of hanging.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@danielhanchen
danielhanchen merged commit bcd2dfc into main Aug 16, 2026
12 checks passed
@danielhanchen
danielhanchen deleted the parallel-compiler-sweep branch August 16, 2026 14:40
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: e655a4f049

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant