perf(ci): run the python test suite in parallel - #233
Merged
Conversation
CI spent 21-26 min per matrix job entirely in pytest. Running it under xdist (`-n logical`, one worker per vCPU) takes the full suite from 1083s to 271s at four workers locally. `--dist loadfile` lives in addopts so it also holds when `-n` is passed by hand. Fixes test_client_state_multiuser.py, which inherited its JWT secret from a file that happened to run earlier and errored on any worker that scheduled it first. Wall-clock and throughput benchmarks move behind the existing `slow` marker; their behavioral assertions stay in the default run.
Pfannkuchensack
requested review from
blessedcoolant and
lstein
as code owners
September 9, 2026 07:09
Seven files built real checkpoint extents to assert key names, dtypes and rank. `test_flux2_fp8mixed_keys.py` alone peaked at 14.9GB -- more than a CI runner has -- because it materialized 2.3 billion elements as float32 before casting to fp8. Extents are now capped, or expanded from one element where the layout has to stay exact, and materialized only where safetensors needs contiguous data. Peak memory: flux2 14.9GB, wan 5.05GB, minimax 2.41GB -> at or near the 0.86GB import baseline. Full suite at four workers: 13.3GB -> 11.5GB.
Two tests read a worker's instantaneous state and fail when a loaded machine finishes the work first. The cache worker count is now converged on with the file's existing `_wait_until`; the recovery test drops the `is_running` sample and keeps the assertions that actually establish recovery ran. Both failed on macOS CI under xdist and pass serially.
A worker leaking from an earlier test in the same process made the baseline count unreliable: one starting as another exits leaves the count unchanged, so the test failed on macOS CI under xdist. Set difference names this cache's own worker regardless of what else is winding down.
Nothing in this repository runs `-m slow`, so marking a test removes it from CI rather than moving it to a slower tier. Three guards go back to running: - Loop-scheduler linearity now measures CPU time, which a worker losing its core does not inflate, so the ratio no longer has to be a benchmark. Its absolute sibling budget stays slow. - bcrypt's work factor is read from the hash instead of timed; every other assertion in that file holds at cost factor 4. - test_flux2_scaled_fp8_keys.py drops to 0.90GB from 1.96GB by expanding one element to the exact captured extents -- capping breaks it, because the fused qkv is split into thirds. Each xdist worker now reports its peak RSS and a dead worker fails the run at once, so the next memory blow-up is a number in the log rather than a runner that vanishes mid-run. The extent helper moves to the state_dicts utils the four copies of it should have used.
Pfannkuchensack
force-pushed
the
perf/parallel-python-tests
branch
from
September 9, 2026 15:35
766c5e8 to
90a8979
Compare
Torch sizes its thread pools to the whole machine, so four workers on a four-vCPU runner ask for sixteen compute threads. The Windows CPU build crashed a worker in the fp8 tests under that contention, twice in two runs.
Slow is where tests that need a development machine live -- real accelerator hardware, or a quiet one for timing -- and they are run there with `-m slow`. The earlier wording read as if the marker discarded a test.
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
python teststook 27-32 minutes, essentially all of it the pytest step — install is cached at 17-31s. Measured on run 34303000072:run pytestwas 1246s on linux, 1388s on macOS, 1586s on windows. 7267 tests with no single hotspot; the 30 slowest sum to ~230s of 1083s. The job timeout had already been raised to 30 minutes (invoke-ai#9391) rather than the runtime addressed.Result
Six jobs green in ~10 minutes; the pytest step is 392-572s against 1246-1586s. Verified across two consecutive full runs (34373639774, both attempts).
Parallel execution
-n logical, not-n auto: xdist'sautoasks psutil for physical cores, half the vCPUs a standard runner exposes. psutil is a hard runtime dependency, so that branch always wins.OMP_NUM_THREADS=1/MKL_NUM_THREADS=1: torch otherwise sizes its thread pools to the whole machine, so four workers on a four-vCPU runner ask for sixteen compute threads. Without this the Windows CPU build took a worker down inside the fp8 tests, twice in two runs; with it, twelve consecutive jobs are green.--dist loadfilelives inaddopts, so it also applies when-nis passed by hand. It is inert without-n.--max-worker-restart=0: a dead worker fails the run immediately and names the test it was running, instead of sixteen silent restarts.uv.lockand this workflow join the change-detection globs; the lockfile now pins a package the test command depends on.--cov-reportflags leaveaddopts: without--covthey never enabled coverage, sofail_under = 85was inert too. The coverage guide now names the flags it promises output from.What parallel execution exposed
The suite could not fit in a runner
The first parallel run did not fail a test — the Linux runner died at 90% with
The runner has received a shutdown signal, twice at the same point, later as exit 143. Per-file peak RSS found the cause: seven files built real checkpoint-sized tensors to assert key names, dtypes and rank.test_flux2_fp8mixed_keys.pymaterialized 2.3 billion elements as float32 before casting to fp8 and peaked at ~15GB — more than the runner's 14.7GB available. Serial CI had been surviving this by a hair.test_flux2_fp8mixed_keys.pytest_wan_checkpoint_config.pytest_minimax_h3_lora_probe_independence.pytest_flux2_scaled_fp8_keys.pytest_z_image_scaled_fp8_keys.pytest_flux1_scaled_fp8_keys.pytest_anima_scaled_fp8_keys.py0.86 GB is the bare import baseline. Extents are capped through a shared
token_extentshelper where only names, dtypes, values and rank are asserted. Where the code under test decides by shape — the fused qkv intest_flux2_scaled_fp8_keys.pyis split into thirds, so 4 rows would not divide — one element is expanded to the exact captured layout instead, and materialized only where safetensors needs contiguous data.test_pid_chunked_equivalence.pykeeps its real sizes: the chunk boundary is the subject of the test.krea2/test_attention.pyis untouched because its footprint is the diffusers import, not its tensors.Order and timing dependencies
test_client_state_multiuser.pyinherited its JWT secret from whichever file ran earlier: alone it was 6 passed / 9 errors, including the cross-user client-state isolation checks. Itsadmin_tokenfixture now requestssetup_jwt_secretlike every sibling. A per-file sweep of all 437 test files in fresh processes found no other instance.is_running is True, which asserts only that the work had not finished yet; the assertions establishing that recovery ran are unchanged.Benchmarks, and what stayed protected
CI does not run
-m slow; that lane is for what needs a development machine, andAGENTS.mdandtests/AGENTS.mdnow say so plainly. A guard that only needed a steadier measurement does not belong there, so two were made deterministic rather than marked:$2prefix, length 60, round trip — holds at cost factor 4, so a passlib drift or an explicit lowbcrypt__roundswas invisible.Genuinely machine-dependent measurements moved behind the marker: the ten benchmarks in
test_performance.py,test_timing_attack_resistance_same_length(200 bcrypt verifications, 34s), the absolute scheduler budget, andtest_consumer_time_does_not_count_as_decoder_inactivity. The three concurrency-correctness tests fromtest_performance.pykeep running asTestConcurrentAuthOperations, without timing assertions.slowis the lane for tests that need a development machine — real accelerator hardware, or a quiet one for timing — and is run there with-m slow.test_consumer_time_does_not_count_as_decoder_inactivitybelongs in it for the second reason: its first frame is decoded under the same deadline the consumer then sleeps past, so it fails whenever FFmpeg startup is starved. Widening the window from 2s to 5s did not stop it failing at 16 and 24 workers. An injectable clock in the decoder would make the accounting observable without a real timer; that is out of scope here.Checks
-m slowon each touched file passes.ruff checkandruff format --checkon all changed files.main: a local HF mirror host rejected by the SSRF guard. Environment-specific.Follow-ups
test_image_index_service.pyis ~125s, almost all of it idle in the production retry backoff; injecting that schedule would cut the serial run too.keys_to_mock_state_dictinstate_dicts/utils.pystill allocates full extents withtorch.empty; it commits no pages today, but the first caller that writes to those tensors turns 4-5.6GB virtual into resident.