Repository navigation
CI follow-up: cache source-built wheels and extensions, bump CI containers, trim slow GPU tests - #2836
Conversation
…r test to unit A failing rank blocked in megatron_worker_teardown's barrier never reported, so the 30 s failure grace period could not start and the job waited for the pytest timeout instead. Also doc/comment fixes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
- gpu job: pin and cache mamba_ssm / causal-conv1d / fast-hadamard-transform wheels per container image (~10 min of compilation per run); noxfile builds a wheel only if the wheelhouse lacks it - Bump pytorch 26.05/26.07 -> 26.09, trtllm 1.3.0rc20 -> rc29, vllm v0.30.0 -> v0.31.0 (legacy v0.20.0 kept); nemo 26.08 is already the newest public. The tensorrt:26.05 images used by the ONNX/diffusers examples stay (torch-tensorrt needs libnvinfer.so.10) - benchmark.py: tolerate TensorRT builds without NetworkDefinitionCreationFlag.EXPLICIT_BATCH (removed in 11) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
tests/gpu_megatron/_extensions/test_torch_extensions.py was identical to tests/gpu/_extensions/; the megatron conftest already prebuilds the extensions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
|
/claude review |
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2836 +/- ##
==========================================
+ Coverage 76.90% 78.80% +1.89%
==========================================
Files 658 658
Lines 74023 74029 +6
==========================================
+ Hits 56930 58335 +1405
+ Misses 17093 15694 -1399
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The nightly gpu job never found its extension cache in a month of runs: actions/cache saves only when the job succeeds, and the nightly was failing or timing out every night, so the main-branch cache was never populated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
There was a problem hiding this comment.
Claude review — CI follow-up PR
Scope: full review (the trigger comment was a bare /claude review). 11 changed files, all reviewed: modelopt/onnx/quantization/autotune/benchmark.py, noxfile.py, the three workflow files, and the five test files. Note that a two-dot git diff origin/main HEAD in the shallow CI checkout surfaces unrelated main-only changes (the external-draft removal, find_final_norm_path); I scoped the review to the 11 files GitHub reports as this PR's, and ignored that noise.
Findings — CRITICAL: 1, IMPORTANT: 4, SUGGESTION: 3
Most impactful
1. The wheel cache probably doesn't work at all (CRITICAL, noxfile.py:131). The workflow caches /root/.cache/wheelhouse and never sets MODELOPT_WHEELHOUSE, so the noxfile falls back to ~/.cache/wheelhouse. In a GitHub Actions container job HOME is remapped to /github/home, not the image's /root — so the restore fills a directory nothing reads, the glob always misses, and all three packages recompile on every run. That is precisely the ~10 min this PR exists to remove. The sibling .github/actions/cache-extensions avoids this by writing the literal path into $GITHUB_ENV rather than relying on ~; doing the same here is a one-line fix. A single echo "$HOME" in the gpu job confirms or refutes it before merge.
2. A partial wheelhouse can be frozen into the cache key permanently (IMPORTANT, gpu_tests.yml:104). always() is justified for a failing test, but a timeout can also land between wheel builds. Saving a one-of-two wheelhouse, then skipping the save on every subsequent run because cache-hit == 'true', leaves the missing wheel rebuilt forever — and the key only changes when noxfile.py does, so it cannot self-heal. Suggested either a completeness marker or run_id-suffixed keys with restore-keys.
3. The container bump collides with the repo's own cppimport note (IMPORTANT, gpu_tests.yml:47). noxfile.py still says tests/gpu/_extensions/test_onnx_extensions.py "fails for newer containers until cppimport#98", and that test is an unconditional assert with no skip marker. 26.05 → 26.09 is squarely "newer". Either the note is stale and should be deleted in this PR, or the test needs a guard. The PR body says the workflows have not yet been run on the new images, so this is unverified either way.
4. Teardown failures are now silently swallowed on the failure path (IMPORTANT, distributed/utils.py:200-210). The early put is the right fix for the reported hang, but the finally block still assembles a [teardown] … traceback that the if not reported guard then discards. That diagnostic is most valuable exactly in the gpu_megatron scenario this PR targets, where the failing rank's own teardown barrier is the thing that blocks.
5. The vLLM pin the noxfile says must stay in sync is now out of sync (IMPORTANT, example_tests.yml:151). Workflows go to v0.31.0; examples/vllm_serve/Dockerfile and its README stay at 0.30.0, so CI validates a version users building the shipped Dockerfile don't get.
Also raised three non-blocking SUGGESTIONs: glob prefix-collision and a bare IndexError in _install_built_from_source, a thin 5 s timing margin in the new pool test, and a 2024 copyright year on a file added in 2026.
Assessment
The pool fix itself is correct and well-targeted — reporting before teardown is the right resolution for a teardown barrier that blocks against a still-running peer, and the new test genuinely fails without it and terminates fast rather than hanging CI. The EXPLICIT_BATCH guard is right and consistent with the other create_network call site. The dedupe of the symlinked megatron extension test is sound given the conftest already prebuilds the extensions.
Risk: medium, and concentrated in the CI plumbing rather than the library. Nothing here can affect user-facing behavior — benchmark.py is the only modelopt/ change and it only widens TensorRT version tolerance. But the headline benefit of the PR (the wheel cache) looks likely to be a no-op as written, and two of the container bumps are unverified against the images. Running the gpu job on this branch would resolve findings 1 and 3 together and is worth doing before merge.
mode="max-autotune" enables inductor CUDA graphs, whose recording intermittently fails in training (cudaStreamCaptureStatusInvalidated, 'graph recording observed an input tensor deallocate'); it failed test_llama_eagle3 on 5 of the last 10 nightlies and on the pytorch:26.09 dispatch run. max-autotune-no-cudagraphs keeps the autotuning. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
Their setup.py hardcodes -std=c++17, which fails against the torch headers of pytorch:26.09 ('ATen requires C++20'). Patch the standard when building the wheel; verified to compile and import against torch 2.13 headers.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
…illegal-memory-access flake test_fp8_real_quantize (twice, 2-GPU) and test_layerwise_calibrate_vlm_text_forward_loop[True] (PR CI, 1 GPU) failed with the intermittent CUDA illegal memory access that skip_flaky_on_blackwell already covers for this file; test_megatron.py alone passed on the same 2-GPU runners in all 4 tries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
…p the new Blackwell skips On the 1-GPU PR runners the branch hit CUDA illegal memory accesses in random test_megatron.py tests (3 of 7 full-suite runs), never with main's conftest (0 of 2 bisect runs, and none on main). Destroying each test's process groups in the teardown is the only functional difference, so revert it; the 2-GPU full suite takes the same time without it (43:36 vs 44:21). The skips added for the same crashes in 3ae2c85 go too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
yeyu-nvidia
left a comment
There was a problem hiding this comment.
Reviewed the hf_eagle.py hunk you asked about. The change is right and I'd land it — flaky nightlies beat unmeasured throughput, and max-autotune-no-cudagraphs is the correct knob: same Triton autotuning, no graph capture.
Two observations, neither blocking.
1. Two comments in the same file now describe a constraint that no longer applies
hf_eagle.py:762 and :910 both carry:
# Init rotary_emb outside _eagle_forward so inv_freq is not captured by CUDAGraph.
self.eagle_module._maybe_init_rope(device=...)Those hoists exist because _eagle_forward was capturing CUDA graphs. With graphs off, the stated reason is gone while the code stays — and the next person to read it will either think graphs are still in play, or move the call back in without realising it was load-bearing for a reason that may return.
I would not move the calls; the hoist is harmless and keeps the door open if graphs are re-enabled. But a word in each comment saying the capture is currently disabled and this guards re-enabling would stop the rationale going stale. Your call whether that belongs in this PR or a follow-up.
2. suppress_errors = True means we would not have heard about this from a user
_activate_torch_compile sets torch._dynamo.config.suppress_errors = True four lines above the change. A compile-time failure degrades to eager silently; what made these failures visible is that cudagraph recording blows up at runtime rather than at compile. So the only reason this surfaced at all is the nightly, and only as a hard crash — a quieter regression in the same block would be invisible.
Not something to fix here. Worth knowing that this file has no signal between "works" and "crashes the test".
On the unmeasured speed impact
Honest to state it, and I would not gate on it. If it does get measured later, the comparison worth having is EAGLE3 training step time with max-autotune vs max-autotune-no-cudagraphs on a real target rather than the tiny CI model — graph capture pays off on many-small-kernel workloads, and EAGLE3's draft forward at training shapes is mostly large GEMMs, so my expectation is the delta is small. If it turns out not to be, the better fix is probably making capture robust (static input buffers) rather than reverting, since the recording failure is a lifetime problem, not an autotuning one.
I did not review the rest of the PR — CI infrastructure, the worker-pool fix, and the quantization changes are outside what you asked me for and outside what I can usefully check.
There was a problem hiding this comment.
Approving on the basis of the modelopt/torch/speculative/plugins/hf_eagle.py change only — the one @kevalmorabia97 asked me to look at. I have not reviewed the CI infrastructure, the worker-pool fix or the quantization changes in this PR.
max-autotune → max-autotune-no-cudagraphs is the right call and the right knob: identical Triton autotuning, no graph capture. Trading unmeasured throughput for a nightly that stops failing 5 runs in 10 is a good trade, and the comment records why.
Detail in my earlier comment: #2836 (review) — two non-blocking notes, the stale CUDAGraph rationale now left behind at hf_eagle.py:762 and :910, and suppress_errors = True four lines above meaning this block has no signal between working and crashing the nightly.
There was a problem hiding this comment.
Approving on the basis of the hf_eagle.py change only, which is what @kevalmorabia97 asked me to look at. max-autotune -> max-autotune-no-cudagraphs is the right knob: same Triton autotuning, no graph capture. Detail in #2836 (review)
…emb init is hoisted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
|
@yeyu-nvidia Thanks. Addressed note 1 in cb1cac9: both rotary_emb hoist comments in |
A pool that lives for the whole session turns one poisoned job into many errors. In the first CI run of this PR, test_lora_then_quantize hit the known Blackwell "illegal memory access" in an NCCL broadcast. A CUDA error of that kind is sticky, so the next jobs in the same workers failed too, and because _reset_shared_pools ran a job in the poisoned pool at the start of every module, every test of every later module errored, including the ones that use no pool (3 failed, 180 errors). With a pool per module such a flake cost one module. DistributedWorkerPool.run now retires a pool after any failed job (#2836), so get_live_pool hands the next test a new one and _reset_shared_pools skips a retired pool. What is left is a reset job that itself fails or finds a dead worker: _reset_shared_pools no longer lets that error the module. run() retires the pool, and the next test that needs one gets a new pool. Testing: the Megatron lane needs GPUs, so this PR's gpu_megatron job is the check. The retirement it relies on is covered on CPU by test_a_failed_job_retires_the_pool_even_when_every_rank_replied in tests/gpu/torch/utils/test_distributed_worker_pool.py. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A pool that lives for the whole session turns one poisoned job into many errors. In the first CI run of this PR, test_lora_then_quantize hit the known Blackwell "illegal memory access" in an NCCL broadcast. A CUDA error of that kind is sticky, so the next jobs in the same workers failed too, and because _reset_shared_pools ran a job in the poisoned pool at the start of every module, every test of every later module errored, including the ones that use no pool (3 failed, 180 errors). With a pool per module such a flake cost one module. DistributedWorkerPool.run now retires a pool after any failed job (#2836), so get_live_pool hands the next test a new one and _reset_shared_pools skips a retired pool. What is left is a reset job that itself fails or finds a dead worker: _reset_shared_pools no longer lets that error the module. run() retires the pool, and the next test that needs one gets a new pool. Testing: the Megatron lane needs GPUs, so this PR's gpu_megatron job is the check. The retirement it relies on is covered on CPU by test_a_failed_job_retires_the_pool_even_when_every_rank_replied in tests/gpu/torch/utils/test_distributed_worker_pool.py. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A pool that lives for the whole session turns one poisoned job into many errors. In the first CI run of this PR, test_lora_then_quantize hit the known Blackwell "illegal memory access" in an NCCL broadcast. A CUDA error of that kind is sticky, so the next jobs in the same workers failed too, and because _reset_shared_pools ran a job in the poisoned pool at the start of every module, every test of every later module errored, including the ones that use no pool (3 failed, 180 errors). With a pool per module such a flake cost one module. DistributedWorkerPool.run now retires a pool after any failed job (#2836), so get_live_pool hands the next test a new one and _reset_shared_pools skips a retired pool. What is left is a reset job that itself fails or finds a dead worker: _reset_shared_pools no longer lets that error the module. run() retires the pool, and the next test that needs one gets a new pool. Testing: the Megatron lane needs GPUs, so this PR's gpu_megatron job is the check. The retirement it relies on is covered on CPU by test_a_failed_job_retires_the_pool_even_when_every_rank_replied in tests/gpu/torch/utils/test_distributed_worker_pool.py. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A pool that lives for the whole session turns one poisoned job into many errors. In the first CI run of this PR, test_lora_then_quantize hit the known Blackwell "illegal memory access" in an NCCL broadcast. A CUDA error of that kind is sticky, so the next jobs in the same workers failed too, and because _reset_shared_pools ran a job in the poisoned pool at the start of every module, every test of every later module errored, including the ones that use no pool (3 failed, 180 errors). With a pool per module such a flake cost one module. DistributedWorkerPool.run now retires a pool after any failed job (#2836), so get_live_pool hands the next test a new one and _reset_shared_pools skips a retired pool. What is left is a reset job that itself fails or finds a dead worker: _reset_shared_pools no longer lets that error the module. run() retires the pool, and the next test that needs one gets a new pool. Testing: the Megatron lane needs GPUs, so this PR's gpu_megatron job is the check. The retirement it relies on is covered on CPU by test_a_failed_job_retires_the_pool_even_when_every_rank_replied in tests/gpu/torch/utils/test_distributed_worker_pool.py. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A pool that lives for the whole session turns one poisoned job into many errors. In the first CI run of this PR, test_lora_then_quantize hit the known Blackwell "illegal memory access" in an NCCL broadcast. A CUDA error of that kind is sticky, so the next jobs in the same workers failed too, and because _reset_shared_pools ran a job in the poisoned pool at the start of every module, every test of every later module errored, including the ones that use no pool (3 failed, 180 errors). With a pool per module such a flake cost one module. DistributedWorkerPool.run now retires a pool after any failed job (#2836), so get_live_pool hands the next test a new one and _reset_shared_pools skips a retired pool. What is left is a reset job that itself fails or finds a dead worker: _reset_shared_pools no longer lets that error the module. run() retires the pool, and the next test that needs one gets a new pool. Testing: the Megatron lane needs GPUs, so this PR's gpu_megatron job is the check. The retirement it relies on is covered on CPU by test_a_failed_job_retires_the_pool_even_when_every_rank_replied in tests/gpu/torch/utils/test_distributed_worker_pool.py. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…#2845) ### What does this PR do? Type of change: CI (unit-test runtime) The unit `linux` job gates every GPU and example workflow (their pr-gate jobs wait for it), and its tests are CPU-bound but run serially. This runs them under pytest-xdist on the runner's four vCPUs. The `linux` job and the multi-version matrix get `PYTEST_ADDOPTS=-n 4 --dist loadfile` and `OMP_NUM_THREADS=1`: one worker per vCPU (`-n auto` counts physical cores), whole files per worker, and one thread per worker, because with torch's default threads the runner is oversubscribed and slower than serial (1,481 s and 41 failures in my local runs). What else changes so the suite passes under xdist: - `noxfile.py`: the second pytest process that runs the `manual` tests clears `PYTEST_ADDOPTS`, so it stays serial. Those tests leak global state into each other, and a worker would run them in some order. - `tests/_test_utils/torch/quantization/onnx_export.py`: `TEST_MODELS` was a set of classes, whose iteration order differs between processes, so workers collected different test ids. It is a tuple now. - `test_hf_dflash2.py::test_overfits_a_single_batch` takes about 30 s on one thread on main (53 s before #2836 halved its loop to 30 steps; that version hit the 60 s default call timeout in the nightly on main on 2026-10-20). Its tiny model is built in bf16, and a CPU without native bf16 support emulates bf16 matmuls (about 0.9 s per step, 0.1 s in float32). The model is float32 now, which takes the test to about 4 s on one thread, so it needs no timeout override; the loop and both assertions are unchanged. - `test_full_algorithms_vision.py::test_search_constraints` assigned `ConstraintsFunc._get_true_latency` and never restored it. Serial runs never noticed, because `test_evaluate_constraints.py` sorts first. Under xdist a worker can get the files in the other order, and the Python 3.10 and 3.14 legs failed two tests after it. It uses `monkeypatch` now. - `tests/conftest.py`: the unit per-test cap is 120 s on every platform. `test_dist.py::test_data_parallel` takes 8-15 s serial and 25-35 s with four workers sharing four vCPUs, and went over 60 s on the slowest runner. A hung test is still reported, after 2 min instead of 1. Why: on current main the `linux` job has a median of 12.9 min (27 runs of other PRs since #2823 merged), and a pull request's GPU run waits a median 14.5 min for it (125 runs of 33 PRs). ### Time improvement (measured in CI) Unit `linux` job: serial on current main (27 runs of other PRs since #2823 merged) against the runs of this series with `-n 4`. | | before (serial) | with this PR | |---|---|---| | pytest | 630 s median (355-709 s) | 479 s in this PR's own run; 294 s median over the 11 runs of the series (166-479 s) | | whole job | 12.9 min median (8.8-14.8) | 10.9 min in this PR's own run; 7.0 min median over the 11 runs (5.0-10.9) | | unit workflow: `linux`, then the multi-version and Windows legs | 24.1 min median (12 runs) | 17.2 min median (the 7 runs that ran every leg) | - The runs of #2846 and above also carry the sysmon core of #2846 (about 22% off the unit suite on its own, measured locally), so the 294 s median is xdist plus sysmon; this PR's own run is the xdist-only number. It was the slowest of the 11 (166-479 s for nearly the same tests: runner variance), so it understates the gain. Locally on 4 CPUs with coverage as in CI, the base took 825 s serial and 332-344 s with this configuration (3 runs). - The GPU, Example and Regression workflows hold their jobs until the unit `linux` check is done (`pr-gate / wait-checks`, at least 5 min). That wait is a median 14.5 min (IQR 12.9-15.8) on 125 GPU runs of 33 PRs since #2823; in this PR's own GPU run it was 9.7 min (its `linux` job took 10.9 min). With a 7 min `linux` job instead of 12.9 min, a GPU run requested right after a push starts about 6 min earlier. That figure is derived from the job times: the later runs of the series were requested after their unit check had finished (the wait sat at its 5.2 min floor in 7 of the 9 GPU runs), so they cannot show it. Method: every PR of the series has its own CI run, so "before" is the run of the PR(s) below that lack the change and "after" the runs that carry it. Times come from the job logs (`--durations`, or the gap between consecutive result lines). Where runner speed matters, the tests that no PR of the series touches scale each run, and the raw and the scaled medians are both shown. One test varies by 15-30% from run to run on its own, so smaller differences are not evidence. ### Usage Nothing user-facing. Locally: `OMP_NUM_THREADS=1 pytest tests/unit -n 4 --dist loadfile`. ### Testing - Full `tests/unit` on 4 CPUs with coverage as in CI, on the base before #2823: serial 825 s, this configuration 332-344 s in three repeated runs (4,724 passed, 0 failed). - This PR's own CI run: the `linux` job took 10.9 min (479 s of pytest) against the 12.9 min serial median; see the time section above for the later runs of the series. - `tests/unit` without `puzzletron` (it needs `hydra`, absent from my local environment) in reversed and in shuffled file order shows no other order dependence; `test_search_constraints` followed by `test_evaluate_constraints.py` in one process failed before the fix and passes after it. - `test_hf_dflash2.py` (33 tests) on one CPU thread: 13.5 s, of which `test_overfits_a_single_batch` takes about 4 s (30 s with the bf16 model on main; the next slowest test takes about 1.3 s). - Pre-commit (all hooks) passes. ### Before your PR is "*Ready for review*" Make sure you read and follow [Contributor guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md) and your commits are signed (`git commit -s -S`). Make sure you read and follow the [Security Best Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors) (e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(..., weights_only=False)`, `pickle`, etc.). - Is this change backward compatible?: N/A (CI and tests) - If you copied code from any other sources or added a new PIP dependency, did you follow guidance in `CONTRIBUTING.md`: no new dependency (`pytest-xdist` is already in the `dev-test` extra on main) - Did you write any new necessary tests?: N/A (two existing tests fixed or sped up so they pass under xdist) - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: N/A (CI and tests) - Did you get Claude approval on this PR?: N/A (optional, not run) ### Additional Information Slice 1 of 11 of the GPU-CI speed-up work, split out of the reference draft #2730 (not for merge; it holds the whole series with its measurements). Merge in order; each PR stands on its own and passes CI without the later ones. 1. #2845 [OMNIML-6178] [1/11] Run the unit tests in parallel with pytest-xdist **(this PR)** 2. #2846 [OMNIML-6178] [2/11] Use the sysmon coverage core in CI 3. #2847 [OMNIML-6179] [3/11] Run manual-marked tests on nightly and on-demand GPU runs 4. #2848 [OMNIML-6181] [4/11] Give gpu_megatron's slow cold starts more time 5. #2849 [OMNIML-6179] [5/11] Make the ONNX tests cheaper: CPU-only ones to the unit lane, small models 6. #2850 [OMNIML-6179] [6/11] Use local tokenizers and calibration text in GPU tests, shrink puzzletron data 7. #2851 [OMNIML-6180] [7/11] Trim the llm_eval, llm_qat, EAGLE and megatron_bridge example tests 8. #2852 [OMNIML-6180] [8/11] Trim the DFlash regression test 9. #2853 [OMNIML-6180] [9/11] Trim the torch_onnx example lane 10. #2854 [OMNIML-6181] [10/11] Make the distributed worker pool fail fast when a worker dies 11. #2855 [OMNIML-6181] [11/11] Share one Megatron worker pool across the gpu_megatron modules <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Improved consistency and performance of automated test runs across platforms and Python configurations through controlled parallel execution. * Preserved predictable model-check ordering and isolated temporary test changes. * Increased unit-test time limits to better support process-spawning and slower runs. * Improved test reliability by explicitly controlling numeric precision and ensuring serial execution for manual CPU tests. * No end-user functionality or product behavior changed. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A pool that lives for the whole session turns one poisoned job into many errors. In the first CI run of this PR, test_lora_then_quantize hit the known Blackwell "illegal memory access" in an NCCL broadcast. A CUDA error of that kind is sticky, so the next jobs in the same workers failed too, and because _reset_shared_pools ran a job in the poisoned pool at the start of every module, every test of every later module errored, including the ones that use no pool (3 failed, 180 errors). With a pool per module such a flake cost one module. DistributedWorkerPool.run now retires a pool after any failed job (#2836), so get_live_pool hands the next test a new one and _reset_shared_pools skips a retired pool. What is left is a reset job that itself fails or finds a dead worker: _reset_shared_pools no longer lets that error the module. run() retires the pool, and the next test that needs one gets a new pool. Testing: the Megatron lane needs GPUs, so this PR's gpu_megatron job is the check. The retirement it relies on is covered on CPU by test_a_failed_job_retires_the_pool_even_when_every_rank_replied in tests/gpu/torch/utils/test_distributed_worker_pool.py. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A pool that lives for the whole session turns one poisoned job into many errors. In the first CI run of this PR, test_lora_then_quantize hit the known Blackwell "illegal memory access" in an NCCL broadcast. A CUDA error of that kind is sticky, so the next jobs in the same workers failed too, and because _reset_shared_pools ran a job in the poisoned pool at the start of every module, every test of every later module errored, including the ones that use no pool (3 failed, 180 errors). With a pool per module such a flake cost one module. DistributedWorkerPool.run now retires a pool after any failed job (#2836), so get_live_pool hands the next test a new one and _reset_shared_pools skips a retired pool. What is left is a reset job that itself fails or finds a dead worker: _reset_shared_pools no longer lets that error the module. run() retires the pool, and the next test that needs one gets a new pool. Testing: the Megatron lane needs GPUs, so this PR's gpu_megatron job is the check. The retirement it relies on is covered on CPU by test_a_failed_job_retires_the_pool_even_when_every_rank_replied in tests/gpu/torch/utils/test_distributed_worker_pool.py. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A pool that lives for the whole session turns one poisoned job into many errors. In the first CI run of this PR, test_lora_then_quantize hit the known Blackwell "illegal memory access" in an NCCL broadcast. A CUDA error of that kind is sticky, so the next jobs in the same workers failed too, and because _reset_shared_pools ran a job in the poisoned pool at the start of every module, every test of every later module errored, including the ones that use no pool (3 failed, 180 errors). With a pool per module such a flake cost one module. DistributedWorkerPool.run now retires a pool after any failed job (#2836), so get_live_pool hands the next test a new one and _reset_shared_pools skips a retired pool. What is left is a reset job that itself fails or finds a dead worker: _reset_shared_pools no longer lets that error the module. run() retires the pool, and the next test that needs one gets a new pool. Testing: the Megatron lane needs GPUs, so this PR's gpu_megatron job is the check. The retirement it relies on is covered on CPU by test_a_failed_job_retires_the_pool_even_when_every_rank_replied in tests/gpu/torch/utils/test_distributed_worker_pool.py. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… a worker dies (#2854) ### What does this PR do? Type of change: CI (test reliability); changes `tests/_test_utils` only A worker that exits without reporting (an illegal memory access aborts the process, or an OOM kill) was only noticed when the result timeout ran out: 600 s for a job, 120 s for the start. Until then the parent waits on a queue nobody writes to, and the other workers wait in the job, typically in a collective, for their dead peer. A start that fails, for example on a bad backend, also waited out the whole ready timeout and left the workers it had spawned behind. `DistributedWorkerPool` now checks the workers' liveness once a second while it waits. A dead worker is noticed within a second and fails the run, or the start, naming its rank and exit code (about 4 s after the death, which includes the 3 s the pool already waits for the other workers' stack dumps). If another rank had already reported an exception, its traceback is kept in that error. The pool is marked `broken` and the other workers are killed with their stacks printed, the way a hung job or a failed rank already ends in main (`get_live_pool` then hands out a new pool). A start that fails kills the workers it had spawned. `ready_timeout` is a constructor argument (120 s by default, as before), so a test can reach the start timeout without waiting for it. This builds on the hang handling of #2832 (`broken`, `get_live_pool`, the stack dump) and does not change it. ### Time improvement (measured locally, not in CI) No lane time changes in a passing run, and no worker died in this series' CI runs, so CI cannot show it. What changes is how long a dead worker costs. CPU, gloo, two ranks, the pool test's own job (rank 0 exits with code 13, rank 1 keeps waiting), seconds from `pool.run(...)` (or the constructor) to the exception, on main and on this PR: | scenario | before | after | |---|---|---| | a rank dies mid-job, its peer is stuck in a wait, `job_timeout` 30 s (as in the test) | 33.1 s, `queue.Empty` | 4.0 s, `Worker process(es) died (rank, exit code): [(0, 13)]` | | the same with the default `job_timeout` of 600 s | 603.1 s, `queue.Empty` | 4.0 s | | the same, the peer waits in `dist.barrier()` | 33.3 s (the 30 s `failure_grace`) | 4.3 s | | the start fails (`backend="not-a-backend"`), default `ready_timeout` of 120 s | 120.1 s, `queue.Empty` | 5.0 s | | rank 0 dies at the start, rank 1 hangs in the rendezvous | 120.0 s, one live worker left behind | 5.0 s, no worker left | The after time is the 1 s liveness poll plus the 3 s the pool waits for the other workers' stack dumps before it kills them. At the production timeouts (120 s for the start, 600 s for a job) a dead worker therefore costs 4-5 s instead of 2-10 minutes of a CI job: the test fails at once and the next one gets a fresh pool from `get_live_pool`, instead of waiting for a queue nobody writes to. Cost: the four new cases add about 27 s to the `gpu` lane: the pool test module takes 43 s in this PR's run and 17 s in #2849's run, which has the same base (main) and lacks them (both from the job logs). Method: a small script (not part of this PR) builds the pool the way the repo's test does and times each scenario with `time.monotonic()` around the call, once per commit, on a CPU host. ### Usage ```python pool = DistributedWorkerPool(world_size=2, backend="nccl", ready_timeout=300) # new argument, default 120 ``` ### Testing `tests/gpu/torch/utils/test_distributed_worker_pool.py` runs the pool with gloo on CPU (it extends main's three tests; the new cases take 24 s together): - a rank that exits with code 13 while its peer waits fails the run in about 5 s and kills the peer. The test sets `job_timeout = 30`, so a regression fails there with `queue.Empty` instead of at the test's own cap. With the liveness check disabled it fails exactly that way. - the same test with a second job: rank 0 raises an exception and rank 1 exits with code 13 two seconds later, after rank 0 has reported. The error carries the exit code and rank 0's traceback. Without the change it carries only the exit code, and the case fails. - a start that fails raises and leaves no worker, both when every worker dies (a bad backend) and when the wait times out while they are still starting (`ready_timeout=0.01`). With `_abort()` removed from the failed-start path, the second case fails ("workers left behind"); the first passes either way, since all its workers die on their own, so it covers the liveness wiring only. Main's three tests (as reworked by #2836) still pass, and the three new cases fail against main's unmodified pool. Pre-commit passes. This PR is rebased onto #2836, which also edited the pool (reporting a failed rank before its teardown, retiring a pool after any failed job); the changes are in different places. The default start timeout stays 120 s here; #2848 raises it to 300 s, so whichever of the two merges second resolves that one line. ### Before your PR is "*Ready for review*" Make sure you read and follow [Contributor guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md) and your commits are signed (`git commit -s -S`). Make sure you read and follow the [Security Best Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors) (e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(..., weights_only=False)`, `pickle`, etc.). - Is this change backward compatible?: N/A (CI and tests) - If you copied code from any other sources or added a new PIP dependency, did you follow guidance in `CONTRIBUTING.md`: no new dependency - Did you write any new necessary tests?: ✅ 2 test functions (4 cases) in `tests/gpu/torch/utils/test_distributed_worker_pool.py`, run on CPU with gloo; each fails without its fix - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: N/A (CI and tests) - Did you get Claude approval on this PR?: N/A (optional, not run) ### Additional Information Slice 10 of 11 of the GPU-CI speed-up work, split out of the reference draft #2730 (not for merge; it holds the whole series with its measurements). Slices 5, 8 and 10 (#2849, #2852, #2854) target main and merge independently; the other open slices form a stack and merge bottom-up. Each PR stands on its own and passes CI without the later ones. The times above are from the series' CI runs before 2026-10-21, when all slices formed one linear stack; a PR number in a method note names that PR's run from then. 1. #2845 [OMNIML-6178] [1/11] Run the unit tests in parallel with pytest-xdist (merged) 2. #2846 [OMNIML-6178] [2/11] Use the sysmon coverage core in CI 3. #2847 [OMNIML-6179] [3/11] Run manual-marked tests on nightly and on-demand GPU runs 4. #2848 [OMNIML-6181] [4/11] Give gpu_megatron's slow cold starts more time 5. #2849 [OMNIML-6179] [5/11] Make the ONNX tests cheaper: CPU-only ones to the unit lane, small models (targets main) 6. #2850 [OMNIML-6179] [6/11] Use local tokenizers and calibration text in GPU tests, shrink puzzletron data 7. #2851 [OMNIML-6180] [7/11] Trim the llm_eval, llm_qat, EAGLE and megatron_bridge example tests 8. #2852 [OMNIML-6180] [8/11] Trim the DFlash regression test (targets main) 9. #2853 [OMNIML-6180] [9/11] Trim the torch_onnx example lane 10. #2854 [OMNIML-6181] [10/11] Make the distributed worker pool fail fast when a worker dies (targets main) **(this PR)** 11. #2855 [OMNIML-6181] [11/11] Share one Megatron worker pool across the gpu_megatron modules <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Distributed worker pools now detect workers that exit during startup or job execution and report failures instead of waiting for the full job timeout. * Worker startup supports a configurable readiness timeout with prompt timeout handling. * Failed or timed-out initialization now cleans up spawned worker processes, preventing orphaned processes and leaving the pool in a consistent state. * Job result retrieval now responds promptly when a worker fails. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A pool that lives for the whole session turns one poisoned job into many errors. In the first CI run of this PR, test_lora_then_quantize hit the known Blackwell "illegal memory access" in an NCCL broadcast. A CUDA error of that kind is sticky, so the next jobs in the same workers failed too, and because _reset_shared_pools ran a job in the poisoned pool at the start of every module, every test of every later module errored, including the ones that use no pool (3 failed, 180 errors). With a pool per module such a flake cost one module. DistributedWorkerPool.run now retires a pool after any failed job (#2836), so get_live_pool hands the next test a new one and _reset_shared_pools skips a retired pool. What is left is a reset job that itself fails or finds a dead worker: _reset_shared_pools no longer lets that error the module. run() retires the pool, and the next test that needs one gets a new pool. Testing: the Megatron lane needs GPUs, so this PR's gpu_megatron job is the check. The retirement it relies on is covered on CPU by test_a_failed_job_retires_the_pool_even_when_every_rank_replied in tests/gpu/torch/utils/test_distributed_worker_pool.py. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…2848) ### What does this PR do? Type of change: CI (test reliability) `DistributedWorkerPool` waited 120 s for its workers to import the Megatron/TE/modelopt stack and report ready. The start takes 75-77 s in 14 modules of the PR run I read and has a p90 of 94-98 s, so slow hosts time out and the whole module errors: 15 jobs (903 job-minutes) in two weeks, 15 of the 63 failed gpu_megatron PR jobs in that period. It waits 300 s. An earlier version of this PR also gave `test_mamba_search_space` more time. Main's #2836 has since set its cap to 150 s, and that holds: in 58 gpu_megatron jobs from the last six days the test took 64-132 s (median 73 s, none above 150 s), so nothing is left to change there. ### Time improvement (measured in CI) None in a passing run: this PR raises one limit that only failing runs reach. - In the 9 gpu_megatron runs of this series, the worker-pool start took 75-77 s before #2846 and 18-27 s after it, against the old 120 s ready timeout. No run came near the limit, so the series cannot show the failures this PR prevents (the sysmon core of #2846 also takes most starts out of reach of the old ready timeout). - What it prevents, from the CI history of the two weeks before the series: the ready timeout failed 15 jobs (903 job-minutes), 15 of the 63 failed gpu_megatron PR jobs. Each failed job costs the author a re-run of the lane, 20-45 min of job time in this series. Method: every PR of the series has its own CI run, so "before" is the run of the PR(s) below that lack the change and "after" the runs that carry it. Times come from the job logs (`--durations`, or the gap between consecutive result lines). Where runner speed matters, the tests that no PR of the series touches scale each run, and the raw and the scaled medians are both shown. One test varies by 15-30% from run to run on its own, so smaller differences are not evidence. ### Usage Nothing user-facing. ### Testing Pre-commit passes. Not run on a GPU here: the effect is on cold-cache runs, so this PR's gpu_megatron job can only show that nothing regresses. ### Before your PR is "*Ready for review*" Make sure you read and follow [Contributor guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md) and your commits are signed (`git commit -s -S`). Make sure you read and follow the [Security Best Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors) (e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(..., weights_only=False)`, `pickle`, etc.). - Is this change backward compatible?: N/A (CI and tests) - If you copied code from any other sources or added a new PIP dependency, did you follow guidance in `CONTRIBUTING.md`: no new dependency - Did you write any new necessary tests?: N/A (a timeout in the shared test utilities) - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: N/A (CI and tests) - Did you get Claude approval on this PR?: N/A (optional, not run) ### Additional Information Slice 4 of 11 of the GPU-CI speed-up work, split out of the reference draft #2730 (not for merge; it holds the whole series with its measurements). Slices 1, 2, 5, 8, 9 and 10 (#2845, #2846, #2849, #2852, #2853, #2854) have merged. The remaining slices (#2847, #2848, #2850, #2851, #2855) are independent PRs on main: each applies to main on its own, and merging all of them onto main in different orders gives the same tree. The times above are from the series' CI runs before 2026-10-21, when all slices formed one linear stack; a PR number in a method note names that PR's run from then. 1. #2845 [OMNIML-6178] [1/11] Run the unit tests in parallel with pytest-xdist (merged) 2. #2846 [OMNIML-6178] [2/11] Use the sysmon coverage core in CI (merged) 3. #2847 [OMNIML-6179] [3/11] Run manual-marked tests on nightly and on-demand GPU runs 4. #2848 [OMNIML-6181] [4/11] Give gpu_megatron's slow cold starts more time **(this PR)** 5. #2849 [OMNIML-6179] [5/11] Make the ONNX tests cheaper: CPU-only ones to the unit lane, small models (merged) 6. #2850 [OMNIML-6179] [6/11] Use local tokenizers and calibration text in GPU tests, shrink puzzletron data 7. #2851 [OMNIML-6180] [7/11] Trim the llm_eval, llm_qat, EAGLE and megatron_bridge example tests 8. #2852 [OMNIML-6180] [8/11] Trim the DFlash regression test (merged) 9. #2853 [OMNIML-6180] [9/11] Trim the torch_onnx example lane (merged) 10. #2854 [OMNIML-6181] [10/11] Make the distributed worker pool fail fast when a worker dies (merged) 11. #2855 [OMNIML-6181] [11/11] Share one Megatron worker pool across the gpu_megatron modules <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Increased the wait time for distributed worker initialization to improve reliability during startup. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…egatron modules (#2855) ### What does this PR do? Type of change: CI (test runtime); changes `tests/gpu_megatron` and `tests/_test_utils` only Each of the 15 gpu_megatron modules that use `dist_workers` started its own worker pool: about 19 of the lane's 36 minutes. Fourteen logged module starts took 75-77 s each (the GDN module 179 s: 76 s pool plus 103 s Triton JIT), plus 3.6-10 s of shutdown, and the start has a p90 of 94-98 s. The 76 s is not distributed init (an in-process init takes 0.1 s): the spawned child unpickles `megatron_worker_teardown` by reference, which imports the heavy `tests/gpu_megatron/conftest.py` (transformers, megatron.core, all of modelopt and its plugins, TE) before it reaches `init_process_group`. The same import chain takes pytest itself 89 s to start here against 17 s in the gpu lane, whose session-scoped pool, with a light teardown function, starts in 1.7 s. Module scope was chosen when a spawn cost about 10 s ("avoids cross-test state contamination"); the git history shows no contamination incident, and the cost has grown 7.6x. `_pool_cache` is now session-scoped, so the pools are keyed by world size and shared by every module. The `dist_workers` fixtures stay function-scoped and get their pool from `get_live_pool`, which replaces a pool that a hang killed. What isolation the module scope gave is kept explicitly: - `megatron_worker_teardown`, after every job: synchronizes CUDA first (an asynchronous CUDA error then surfaces in the test that caused it), destroys Megatron's model-parallel state as before, and now also the process groups it leaves behind (about 20 NCCL groups per dispatch, each with a communicator and a watchdog thread; `test_megatron.py` alone dispatches about 110 times). A pool that lives through the whole session needs it more than a per-module pool did. - `_reset_shared_pools`, an autouse module-scoped fixture: when a module starts, it resets the process-global torch settings that helpers like `set_seed` change (cudnn and TF32 flags, deterministic algorithms, RNG) to what a fresh worker starts with (`reset_worker_state`; the workers record those settings right after they start), in the pools that already exist. It is not part of `_get_pool`, which runs once per test and would add a job to each. - `get_live_pool` and the pool's abort handling: a job that times out, is interrupted, loses a worker **or fails on any rank** retires the pool, and the next test gets a fresh one, for the cost of one pool start. `DistributedWorkerPool.run` has retired a pool after a plain failed job since #2836, which matters more with a session-wide pool: a CUDA error such as the known Blackwell illegal memory access is sticky, the workers stay poisoned, and every later test would fail with it. `_reset_shared_pools` no longer lets a failed reset error the module's tests (they may not use a pool at all). Measured saving on top of #2846's sysmon core: 216 s of the gpu_megatron lane (1,038 s to 822 s; see the time section). Without sysmon a pool start costs 76 s, and the saving would be about 17 min (an estimate; the series was not run that way). One pool start instead of 15 also means fewer chances to hit the ready timeout. ### Time improvement (measured in CI) gpu_megatron lane, pytest time: | main (#2845) | + #2846 | slices 1-10 (the earlier base) | + this PR | |---|---|---|---| | 2,240 s | 1,212 s | 1,038 s (1,029 s in #2852) | **822 s** | This PR on its own: 1,038 s -> 822 s, **-216 s (-21%)**, against the two earlier runs (#2852: 1,029 s, #2854: 1,038 s). This PR's run was on a slightly slower runner (the heavy `call` phases of the tests it does not change took 6% longer than in #2854), so the gain is not runner luck. The setups of 12 s or more, which are the pool starts and the module-level compiles, fall from 16 (489 s) to 3 (246 s): the pool starts once instead of in each of 14 modules, and what remains is the extension build (124 s), the GDN module's Triton JIT (105 s) and one 18 s pool start. With sysmon (#2846) a pool start takes 18-27 s instead of 75-77 s, so each avoided start saves less than the 76 s this PR was designed around. Without #2846 the saving would be 14 starts of 76 s plus the 3.6-10 s shutdowns, about 17 min; that is an estimate, because the series was never run without sysmon on this PR. This is one CI run of the final head: the two runs before it failed on the sticky CUDA error and on the missing `gc.collect()` described below. Method: every PR of the series has its own CI run, so "before" is the run of the PR(s) below that lack the change and "after" the runs that carry it. Times come from the job logs (`--durations`, or the gap between consecutive result lines). Where runner speed matters, the tests that no PR of the series touches scale each run, and the raw and the scaled medians are both shown. One test varies by 15-30% from run to run on its own, so smaller differences are not evidence. ### Usage Nothing user-facing (test fixtures). ### Testing - `reset_worker_state` has a gloo test next to the other pool tests (it runs on CPU): flags and seed changed in a job are back at their spawn-time values afterwards; it fails when the helper forgets one of the settings. - The Megatron lane needs GPUs, so this PR's gpu_megatron job is the check: 822 s of pytest on the final head, against 1,038 s for the earlier base, #2854's run (381 passed, 129 skipped, as before). - The first CI run of this PR failed in gpu_megatron: the Blackwell illegal-memory-access flake hit `test_lora_then_quantize` and poisoned the shared pool, and every later module errored (3 failed, 180 errors), because the pool was reused after a failed job and `_reset_shared_pools` ran a job in it at each module start. The second commit retired a pool after any failed job and made a failed reset harmless; with it the lane ended with 1 failure instead of 183, in `test_forward_with_one_lora[lora_config1]` (the same CUDA error). Re-running the job on that commit failed at the same test again, so it was not a random flake: the teardown of this PR, unlike the draft's, did not collect garbage after destroying the groups, and the draft passed the lane three times. The third commit adds that `gc.collect()`; the lane then passes. The cause is not established (the finished test's Megatron and TE modules can still reference the destroyed groups, and their finalizers then run at an arbitrary point of the next job). #2836 has since landed the retirement in `run()` for its own reasons (with its own gloo test), so the second commit now only makes a failed reset harmless; main's teardown has neither the process-group loop nor the collection, and a session-wide pool needs both. - Pre-commit passes. ### Before your PR is "*Ready for review*" Make sure you read and follow [Contributor guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md) and your commits are signed (`git commit -s -S`). Make sure you read and follow the [Security Best Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors) (e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(..., weights_only=False)`, `pickle`, etc.). - Is this change backward compatible?: N/A (CI and tests) - If you copied code from any other sources or added a new PIP dependency, did you follow guidance in `CONTRIBUTING.md`: no new dependency - Did you write any new necessary tests?: ✅ 1 test function in `tests/gpu/torch/utils/test_distributed_worker_pool.py` (gloo, CPU) for the new helper; the session pool itself is exercised by the whole gpu_megatron lane - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: N/A (CI and tests) - Did you get Claude approval on this PR?: N/A (optional, not run) ### Additional Information Slice 11 of 11 of the GPU-CI speed-up work, split out of the reference draft #2730 (not for merge; it holds the whole series with its measurements). Slices 1, 2, 4, 5, 6, 7, 8, 9 and 10 (#2845, #2846, #2848, #2849, #2850, #2851, #2852, #2853, #2854) have merged. The two remaining slices (#2847, #2855) are independent PRs on main: each applies to main on its own. The times above are from the series' CI runs before 2026-10-21, when all slices formed one linear stack; a PR number in a method note names that PR's run from then. 1. #2845 [OMNIML-6178] [1/11] Run the unit tests in parallel with pytest-xdist (merged) 2. #2846 [OMNIML-6178] [2/11] Use the sysmon coverage core in CI (merged) 3. #2847 [OMNIML-6179] [3/11] Run manual-marked tests on nightly and on-demand GPU runs 4. #2848 [OMNIML-6181] [4/11] Give gpu_megatron's slow cold starts more time (merged) 5. #2849 [OMNIML-6179] [5/11] Make the ONNX tests cheaper: CPU-only ones to the unit lane, small models (merged) 6. #2850 [OMNIML-6179] [6/11] Use local tokenizers and calibration text in GPU tests, shrink puzzletron data (merged) 7. #2851 [OMNIML-6180] [7/11] Trim the llm_eval, llm_qat, EAGLE and megatron_bridge example tests (merged) 8. #2852 [OMNIML-6180] [8/11] Trim the DFlash regression test (merged) 9. #2853 [OMNIML-6180] [9/11] Trim the torch_onnx example lane (merged) 10. #2854 [OMNIML-6181] [10/11] Make the distributed worker pool fail fast when a worker dies (merged) 11. #2855 [OMNIML-6181] [11/11] Share one Megatron worker pool across the gpu_megatron modules **(this PR)** <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Tests** * Distributed GPU test workers now restore their initial PyTorch settings and random seed between test modules. * Worker teardown now synchronizes CUDA, removes non-default process groups, and cleans up Python and GPU resources. * Worker pools are shared across test modules and shut down at the end of the test session for more consistent test execution. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
What does this PR do?
Type of change: CI / tests (follow-up to #2832)
Related: #2870 (independent, targets
main) runs GPU CI only for PRs targetingmain.gpu_megatron, teardown starts withdist.barrier(), which blocked against a still-running peer, so the parent never heard of the failure and the 30 s grace period could not start (it waited for the pytest timeout). New test fails without the fix. Also the remaining Fix post-merge unit CI and the gpu_megatron nightly timeout #2832 review nits (fixture-scope note, comments) and the sub-millisecondget_live_pooltest moved totests/unit.gpujob:mamba_ssm,causal-conv1dandfast-hadamard-transformwere compiled from sdist on every run (~10 min of the job, e.g. 4 + 6 min on 10-16). They are now pinned innoxfile.pyand built into a wheelhouse thatactions/cachekeeps per container image (key includes thenoxfile.pyhash); a wheel is only built when the wheelhouse lacks it. A plain wheelhouse rather than pip's or uv's cache: pip does not cache wheels it builds from a local source tree (the C++20-patched sdist) or reliably from a git URL, its cache lives underHOME(/github/homein container jobs), and the GPU sessions use the container's own pip (venv_backend="none"), not uv. A directory of wheels keyed on the image andnoxfile.pyis explicit and is saved only once complete.gpujob never found itstorch_extensionscache (205-350 s of extension compile each night;gpu_megatronhit about half the nights).actions/cachesaves only when the job succeeds, and the nightly was failing or timing out every night, so main's cache scope was never populated (PR runs save into their ownpull-request/Nscope, which the nightly cannot read). The extension cache and the wheelhouse are now restored withactions/cache/restoreand saved withactions/cache/saveunderalways()(new.github/actions/save-extensions).test_qwen_vluses (rc21-rc23 importMultimodalConfig, whichtensorrt_llm.llmapidoes not export yet; rc24+ importfla, which the image lacks). NeMo 26.08 is already the newest public image. vLLM stays at v0.30.0: v0.31.0 breaks the vLLM plugin (test_kimi_gemm_rs_ar_bypasses_enabled_fake_quantizers:gemm_rs_arhas no.apply;test_flashinfer_metadata_builder_patch_stashes_common_metadata:FlashInferMetadataBuilder.cache_config;test_tiny_llama_quantizetimes out) and needs plugin changes by its owners. Thegpu_vllmjob onv0.20.0(legacy FusedMoE registration, vLLM < 0.24) is dropped; v0.30.0 is the only vLLM job, and its timeout goes from 15 to 30 min: on PRs (1 GPU) its tests take ~10 min plus ~5 min to pull the image, so it timed out twice with all tests passing; the 2-GPU nightly also runs the TP2 tests and needs ~22 min.gpu_vllmpast its 15 min limit). The key now also includes the job name: example and GPU jobs on the same image shared one key, and an example job that builds no extensions finished first and saved an empty cache.gpuandgpu_megatronthen restored it as an exact hit, recompiled on every run (test_cuda_exttook 35-46 s in everygpurun of this PR), and could never save their own (Unable to reserve cache ... another job may be creating this cache). This also affectsmain. The save step now skips when no.sowas built, so a run cancelled before building anything cannot claim the key with an empty cache. Thetensorrt:26.05images used by the ONNX/diffusers examples stay:torch-tensorrt(<2.13) needslibnvinfer.so.10, which newer images drop.setup.pyhardcodes-std=c++17but the 26.09 torch headers need C++20 (ATen requires C++20), so the wheels are built with the standard patched to C++20 (checked to compile and import against torch 2.13 headers).auto_deploy(removed in rc29, so the next bump would otherwise break): ModelOpt's NVFP4 dequantize/export paths (andgpu_trtllmcollection) failed withModuleNotFoundError. The cutlass-swizzled block-scale conversion is a tensor permutation, now implemented in ModelOpt as the inverse ofswizzle_nvfp4_scales(same math as TensorRT-LLM's helper; bit-identical to rc20's on 6 shapes) with a CPU round-trip test.implicit_gemm_cuda.pypassed-std=c++17, which fails against the 26.09 torch headers (165 GPU test failures); torch'sload()now picks the standard.persistent_materializationpatchedFSDPParamGroup.unshard/reshardon the class, so while one layer was materialized every FSDP module skipped unshard; a full forward then ran on sharded DTensor params (test_layerwise_calibrate_fsdp2failed on torch 2.14 with'FSDPParam' object has no attribute '_unsharded_param'). Now patched only on the param groups inside the layer, and only when the layer itself is FSDP-wrapped (otherwise nothing was gathered for its FSDP submodules, so they must still unshard). CPU/gloo regression test fails without the fix; a second test covers a sharedfully_shard([a, b])group, nested contexts, a pre-existing instance override and a non-FSDP layer (fails without the group dedup or the FSDP-wrapped check).hf_eagle.pycompiled_eagle_forwardwithmode="max-autotune", which enables inductor CUDA graphs; recording them intermittently fails (cudaStreamCaptureStatusInvalidated/ "graph recording observed an input tensor deallocate"). It failedtest_llama_eagle3on 5 of the last 10 nightlies and on thepytorch:26.09dispatch run. Nowmax-autotune-no-cudagraphs(same autotuning, no graphs). Training-speed impact not measured.pytorch:26.09runner), pushing the MSE-sweep export and calibration tests past the 120 s timeout. It now uses the config that was fastest on B300 (BLOCKS_PER_PROGRAM=64, 8 warps; on Ada all three were within ~8% from 256² to 16384x8192). Cold first call drops from 13.6 s to 5.4 s locally, which also helps users' first MSE calibration in each process.xfail(raises=RuntimeError)markers intest_megatron_preprocess_data.py; they xpassed in all 151 PR runs and every nightly that reached them over the last week (one of them never touches HF).tests/gpu_megatron/_extensions/test_torch_extensions.py, an exact copy oftests/gpu/_extensions/(the megatron conftest already prebuilds the extensions).manual(~40 s each, ~4 min of thegpujob). Run them with--run-manual.gpu_vllm's new TP2 tests (Sync the amax of vLLM's tensor-parallel linears across TP ranks #2842) failed because vLLM needs 160 MiB of/dev/shmand the container had Docker's default 64 MiB; the GPU test container now gets--shm-size=2gb, like the example jobs.test_distill_llm_sft_hf_chat[True](Megatron-Bridge: GGML PTQ, QAD and fake-quant export for large MoE and VLM models #2873, packed SFT on a tiny MoE at TP2) failed on the sm_120 runner with "No dot product attention backend is available": the tiny model's head_dim of 2 leaves only TE's unfused attention, which the 2-GPU sm_120 runner rejects for packed input (it passes on Ada); the packed case now uses head_dim 8 so the flash and cuDNN backends also qualify. Both only run on the 2-GPU nightly, not in single-GPU PR CI.gpu_megatrontimeout 60 -> 75 min: PR runs (1 GPU) take ~42 min, but the nightly runs every test on 2 GPUs: 46.5 min on Oct 19, then cancelled at 60 min with 99% done on Oct 20, and at 89% in this branch's dispatch run.test_megatron.pytests (3 of 7 full-suite runs:test_dsa_kv_cache_quant,test_fp8_real_quantize,test_layerwise_calibrate_vlm_text_forward_loop), never with main's conftest (0 of 2 bisect runs, none on main). Reverted (6bc0119); the 2-GPU full suite takes the same time without it (43:36 vs 44:21), so the 60→75 min job timeout covers it.main(Oct 20):test_hf_dflash2.py::test_overfits_a_single_batchtook 51-61 s on most unit runners (2-7 s on others) and hit its 60 s timeout, also in main's nightly; both accuracies pass 0.9 by step ~21, so it now runs 30 steps instead of 60 (8 s on one CPU thread, was 16 s).test_mamba_search_spacetimeout 120 -> 150 s: across ~135 PRgpu_megatronjobs on Oct 16-17 it usually took 64-76 s, ~15% took 85-103 s on slow runners, and it reached 112, 119 and 123 s (twice; one timed out here). Nightlies were ~72 s.Testing
queue.Emptyafter the 30 s job timeout) and passes with it; CPU unit tests that use the shared pool pass.pip wheelper missing package, warm cache runs none and installs the cached wheels.pytorch:26.09passed; Unit tests passed. PR CI on 9d9ab38: everything green exceptgpu(2 FP8-sweep timeouts, fixed above);gpu_megatronpassed in 32.5 min (60 min limit); the torch-extension cache was restored.tests/gpu/torch/quantization/test_nvfp4_fp8_sweep_kernel.pyand the accelerate MSE tests pass locally (Ada, torch 2.14); thehf_ptq.pyexport case was not run locally.gpu_vllm(all 190 tests passed, then hit the old 15 min limit) andgpu_megatron(test_mamba_search_spaceat 123 s, timeout raised above). Thegpujob restored the wheelhouse and installed all three wheels without compiling.tests/unit/torch/quantization/test_utils.pypasses on CPU; GPU FSDP2/layerwise/export tests pass innemo:26.08(56 passed, 1 skipped).nemo:26.08the six puzzletron models skip as manual andtest_mamba_search_spacepasses with the new marker.gpu7 MB,gpu_megatron5 MB,gpu_trtllm5 MB,gpu_vllm2 MB; before,gpuandgpu_megatrononly ever had a 0 MB one). Re-runninggpu_trtllmrestored its cache:test_cuda_ext33.2 s -> 3.1 s and the pytest step 202 s -> 79 s.torch._inductor.list_mode_optionsconfirmsmax-autotunesetstriton.cudagraphsand the new mode does not;tests/unit/torch/speculative(339) andtests/gpu/torch/speculative(14) pass.test_distill_llm_sft_hf_chat[True]; in GPU testsgpuandgpu_trtllmpassed and bothgpu_vllmTP2 tests passed, butgpu_vllmhit its 20 min limit at 51% of tests (raised to 30 min below) andgpu_megatronhit its 60 min limit at 89% (raised to 75 min below). Re-dispatched GPU tests on f25ff98:gpu,gpu_trtllmandgpu_vllmpassed (gpu_vllm211 passed in 15 min);gpu_megatron402 passed and failed onlytest_fp8_real_quantize(the Blackwell flake above).Looked at but left alone: the 11 unified-export cases (each hits a different fuse-flag branch). Shared worker pools across megatron modules and a preloaded forkserver (importing modelopt initializes CUDA) did not work out.
CONTRIBUTING.md: N/A🤖 Generated with Claude Code