Skip to content

[OMNIML-6179] Run the HF checkpoint multi-rank tests on the shared CPU worker pool - #2923

Merged
shengliangxu merged 2 commits into
mainfrom
shengliangx/ci-hf-ckpt-unit
Oct 22, 2026
Merged

shengliangxu merged 2 commits into
mainfrom
shengliangx/ci-hf-ckpt-unit

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: CI (test runtime, tests only)

The six ensure_local_checkpoint multi-rank cases are gloo/CPU tests with a fake HfApi, but they lived in tests/gpu and each one spawned a fresh 2-process job (spawn_multiprocess_job). The spawn re-imports torch, transformers and modelopt in both children: every case took 11.2-11.8 s in the gpu job of a pull request (13.7-14.3 s on the nightly's 2-GPU runner), 69 s and 85 s per run, to test a few lines of rank-0 logic.

The file moves to tests/unit/torch/utils/, next to the module's other unit tests, and runs on the session-scoped cpu_dist_workers pool that other multi-rank unit tests already share (CONTRIBUTING: "reuse the session-scoped cpu_dist_workers fixture for multi-rank CPU tests"). Cases and assertions are unchanged. A case takes 0.01-0.02 s once the pool is up.

The pool's workers outlive a case, so each job now undoes what it changes instead of relying on a throwaway process: snapshot_download, HfApi and os.path.isdir are patched with mock.patch.object, HF_HUB_OFFLINE (which tests/unit/conftest.py sets for every unit test) is restored with mock.patch.dict(os.environ), and the process group a job creates is destroyed. The default group is never touched.

Time improvement (measured)

gpu job, per-case time from the result-line gaps of two CI logs of the base:

run before (per case) before (6 cases) after
1-GPU pull-request run (#2850, gpu job) 11.2-11.8 s 69 s 0 s (the file leaves the lane)
2-GPU dispatch run with --run-manual, the nightly's setup (run 39668011982) 13.7-14.3 s 85 s 0 s

unit job: the six cases take 0.01-0.02 s each on a warm pool (CPU host, torch 2.14.1). The first test of an xdist worker that touches the pool pays the pool start (about 7 s on this host), but that cost is already paid by the six existing pool-using files, so the unit job gains about 0.1 s.

Memory (the pool question from the xdist work): this is a seventh file that uses cpu_dist_workers; the six existing ones are test_fsdp2_parallel_export.py, test_autoquant.py, test_autoquant_shapley.py, quantization/test_dist.py, test_quantize_cpu.py and quantization/test_utils.py. A pool is one per xdist worker (two gloo processes, about 0.62 GB each after their first job), so the ceiling is still four pools. The jobs add about 5 MiB to each pool process (623-624 MiB after the six jobs, against 619 MiB after the imports of a typical existing user). The only way this file could raise the unit job's peak is by giving a pool to an xdist worker that would otherwise have none, at most about 1.2-1.4 GB on that worker, which is unlikely with six other users. A separate change to the unit job's xdist grouping (every file with a cpu_dist_workers test goes to one xdist group, chosen from item.fixturenames) would pick this file up with no change.

Usage

Nothing user-facing.

Testing

All on CPU with CUDA_VISIBLE_DEVICES="", in the CI-like venv (Python 3.12, torch 2.14.1 CPU, xdist 3.8.0) unless noted:

  • The moved file alone: 6 passed, also on torch 2.10. Warm pool (other pool users ran first): 0.01-0.02 s per case.
  • tests/unit/torch/utils under -n 4 --dist loadfile: 371 passed.
  • That directory plus the other cpu_dist_workers users (quantization/test_dist.py, export/test_fsdp2_parallel_export.py, quantization/test_utils.py) in one process in default, reversed and two shuffled file orders (seeds 11 and 12): 403 passed each.
  • A probe runs the six jobs on one persistent 2-rank pool and compares the workers' state before and after (HF_HUB_OFFLINE, snapshot_download, HfApi, os.path.isdir, the default group and the process-group registry): identical on torch 2.10 and 2.14.1. The same probe on the old job bodies, which patch globals and never restore them, reports HF_HUB_OFFLINE lost, the two names still patched, os.path.isdir patched on rank 1 and extra process groups left behind.
  • The cases still catch regressions: with the rank-0 check in ensure_local_checkpoint replaced by if True, 3 of the 6 fail; with the "this rank cannot see" check removed, 1 fails.
  • Pre-commit passes (ruff, mypy, license, bandit).

This PR's own unit job on a hosted runner passed. The gpu workflow was skipped by path gating (the file only leaves tests/gpu), so the saving shows in CI on the first gpu run after this merges; the old times come from the CI logs above. Not run: Windows (cpu_dist_workers skips there).

Before your PR is "Ready for review"

Make sure you read and follow Contributor guidelines and your commits are signed (git commit -s -S).

Make sure you read and follow the Security Best Practices (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 (existing tests moved; no new cases)
  • Did you update Changelog?: N/A (CI and tests)
  • Did you get Claude approval on this PR?: ✅ (/claude review approved d836289; its one suggestion is applied in 9ce78ae)

Additional Information

Found by a survey of the GPU lanes for per-case process starts (the pattern #2855 removed from gpu_megatron). Jira: OMNIML-6179, under the CI runtime epic OMNIML-6177.

Summary by CodeRabbit

  • Tests
    • Improved the reliability and isolation of distributed checkpoint test runs. Test environments are now cleaned up after each run, including when a run exits early or encounters an error, and rank-specific path checks are more precisely scoped. These changes improve test consistency without changing end-user behavior.

@copy-pr-bot

copy-pr-bot Bot commented Oct 21, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between d836289 and 9ce78ae.

📒 Files selected for processing: 1
  • tests/unit/torch/utils/test_hf_checkpoint_utils_dist.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

📝 Walkthrough

The distributed checkpoint tests now use the shared cpu_dist_workers fixture instead of spawning a separate two-process job. The helper scopes patched functions and environment changes, and destroys created process groups.

Changes: Distributed checkpoint tests

Layer / File(s) Summary
Shared workers and scoped test state
tests/unit/torch/utils/test_hf_checkpoint_utils_dist.py
The tests use the shared worker fixture instead of spawning dedicated process jobs. The helper restores patched functions and environment state, and destroys created process groups. The local-path test scopes its rank-1 visibility patch and expected-error assertion.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Other

Suggested reviewers: kevalmorabia97

Merge Risk: ⚪ Minimal · up to 9ce78

The checkpoint tests now use shared CPU workers with scoped state cleanup. Source inspection found no actionable merge risk.

Pre-merge checks: Passed 5 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 5 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: moving HF checkpoint multi-rank tests to the shared CPU worker pool.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed PASS. The PR changes only tests/gpu/torch/utils/test_hf_checkpoint_utils_dist.py, renamed to tests/unit/torch/utils/test_hf_checkpoint_utils_dist.py. It makes no changes to the modelopt package,…
Full details: Security Anti-Patterns

Explanation

PASS. The PR changes only tests/gpu/torch/utils/test_hf_checkpoint_utils_dist.py, renamed to tests/unit/torch/utils/test_hf_checkpoint_utils_dist.py. It makes no changes to the modelopt package, examples, pyproject.toml, or requirements.txt, so none of the specified security anti-patterns apply.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 30730c1

@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.65%. Comparing base (f983619) to head (9ce78ae).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2923      +/-   ##
==========================================
- Coverage   79.66%   79.65%   -0.01%     
==========================================
  Files         659      659              
  Lines       74400    74408       +8     
==========================================
  Hits        59272    59272              
- Misses      15128    15136       +8     
Flag Coverage Δ
unit 60.87% <ø> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The six ensure_local_checkpoint cases are gloo/CPU tests with a fake HfApi, but each one spawned a
fresh 2-process job from tests/gpu (spawn_multiprocess_job), paying about 11 s of imports per case
on a GPU runner (6 cases, about 69 s of the gpu job on a pull request, about 85 s nightly). They
move next to the module's other unit tests and run on the session-scoped cpu_dist_workers pool,
where a case takes 0.01-0.02 s once the pool is up.

The workers outlive a case, so each job now undoes what it changes: snapshot_download, HfApi and
os.path.isdir are patched with mock.patch.object, HF_HUB_OFFLINE is restored with
mock.patch.dict(os.environ), and the group a job creates is destroyed. Cases and assertions are
unchanged.

Testing: the moved file alone, the whole tests/unit/torch/utils directory under xdist, and that
directory together with the other cpu_dist_workers users in default, reversed and shuffled file
order, on torch 2.10 and 2.14 (CPU only); a probe that runs the six jobs on one pool and compares
the workers' state before and after (HF_HUB_OFFLINE, the patched names, the process-group
registry) passes, and fails on the old job bodies.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu
shengliangxu force-pushed the shengliangx/ci-hf-ckpt-unit branch from 30730c1 to d836289 Compare October 22, 2026 06:56
@shengliangxu
shengliangxu marked this pull request as ready for review October 22, 2026 06:57
@shengliangxu
shengliangxu requested a review from a team as a code owner October 22, 2026 06:57
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment thread tests/unit/torch/utils/test_hf_checkpoint_utils_dist.py Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review passed — no blocking issues found. LGTM

Scope: full review. 1 file changed (48 additions, 28 deletions): tests/gpu/torch/utils/test_hf_checkpoint_utils_dist.py → tests/unit/torch/utils/test_hf_checkpoint_utils_dist.py. Reviewed the whole moved file, plus ensure_local_checkpoint/_make_local/_snapshot_download in modelopt/torch/utils/plugins/hf_checkpoint_utils.py, DistributedWorkerPool/get_live_pool/default_worker_teardown in tests/_test_utils/torch/distributed/utils.py, and the cpu_dist_workers fixture and timeout mapping in tests/unit/conftest.py / tests/conftest.py. No modelopt/ or examples/ source changed, so no mode-registration, state-dict or export surface is in play.

Findings: CRITICAL: 0, IMPORTANT: 0, SUGGESTION: 1

What I checked and what holds

  • Collective symmetry on a shared pool. This is the real risk of the move: a persistent pool can be poisoned by a job that leaves ranks at different points in the collective sequence. All six cases are symmetric. dist.new_group(group_ranks, ...) is called by both ranks with identical arguments before the member check; for group_ranks=None both ranks enter ensure_local_checkpoint, which issues its own new_group on both; for [0, 1] both are members; for [1] the non-member returns before any further collective and the single member broadcasts inside a one-rank subgroup, so the default group's sequence stays aligned either way.
  • Failure paths are symmetric too. With fail=True, rank 0's OSError is caught inside ensure_local_checkpoint and sent through broadcast_object_list, so both ranks complete the same collectives and then both raise RuntimeError; pytest.raises absorbs it on both, so the pool is not marked broken. Same for hidden_from_rank1: rank 1's "this rank cannot see" fires after the broadcast, on the os.path.isdir check.
  • Cleanup is complete relative to the fixture's documented contract ("Jobs must leave the process group intact and undo any global state they set"). snapshot_download, HfApi and os.path.isdir move from unrestored global assignment to mock.patch.object; HF_HUB_OFFLINE to mock.patch.dict(os.environ) (whose clear()/update() restoration does propagate through os.environ to the real environment); every subgroup the job creates is destroyed in a finally, and ensure_local_checkpoint already destroys the one it makes itself. The default group is never touched. default_worker_teardown offers nothing to reuse here (the CPU pool passes teardown_fn=None, and that helper only resets AcceleratorState), so the self-cleaning jobs are the right mechanism rather than a parallel one.
  • Hermeticity under tests/unit. Popping HF_HUB_OFFLINE sends _snapshot_download down its online branch, but that branch touches only HfApi().repo_info and snapshot_download, both patched — no path reaches the network, so the unit suite stays offline.
  • A hang still fails fast. tests/conftest.py gives tests/unit a 120 s default cap, well inside the pool's 600 s job_timeout; pytest-timeout fires first, run aborts the workers, and get_live_pool rebuilds for the next test, so a regression in the very behavior these tests guard costs one test rather than the lane.
  • Calling convention and assertions. partial(fn, tmp_path, ...) plus the pool's fn(rank, world_size, *args) reconstructs the original (..., rank, size) signature unchanged; workers write the downloaded-by-* markers before reporting, so the post-run tmp_path.glob assertions are well ordered. Cases and assertions are otherwise identical to the tests/gpu version, and the author's mutation check (3 of 6 fail with the rank-0 check stubbed to if True) backs that they still bite.
  • No basename collision with the sibling test_hf_checkpoint_utils.py, and the importorskip-after-module-import ordering matches what that already-in-tests/unit sibling does, so it is a pre-existing convention rather than something this move introduces.

The one suggestion is non-blocking: the finally guard leans on destroy_process_group early-returning for the NON_GROUP_MEMBER sentinel a non-member rank receives, which the adjacent comment doesn't mention.

Risk: low. Tests-only, no public surface, no behavior change for users; the move is well matched to CONTRIBUTING's "reuse the session-scoped cpu_dist_workers fixture for multi-rank CPU tests", and the net effect is removing ~69-85 s of per-case process spawning from the gpu lane.

🤖 Generated with Claude Code

@meenchen meenchen left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot review (gpt-6.1-sol) — DM the bot to share feedback.

LGTM: the test migration preserves all six cases and assertions, with cleanup appropriate for persistent workers; justified existing-test edits require human sign-off.

Needs action:

  • Sign off on the justified migration in tests/unit/torch/utils/test_hf_checkpoint_utils_dist.py: CPU/gloo cases reuse the existing pool without reducing coverage.

No action needed:

  • Mock contexts restore patched globals and environment; explicit groups are destroyed without touching the default group.
  • Tests were inspected, not executed during this review.

A rank outside the group gets the NON_GROUP_MEMBER sentinel from new_group,
which destroy_process_group happens to ignore. Skip it explicitly instead of
relying on that.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>

@meenchen meenchen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot review (gpt-6.1-sol) — DM the bot to share feedback.

LGTM: the prior cleanup suggestion is resolved, and all six cases retain their assertions; the justified existing-test migration requires human sign-off.

Needs action:

  • Sign off on the justified test migration in tests/unit/torch/utils/test_hf_checkpoint_utils_dist.py: CPU/gloo cases reuse the shared pool without reducing coverage.

No action needed:

  • ✔️ Resolved since the last review: cleanup explicitly skips both the default group and the non-member sentinel using isinstance(group, dist.ProcessGroup).
  • Mock contexts restore patched globals and environment variables; collective ordering and process-group cleanup remain appropriate for persistent workers.
  • Tests were inspected, not executed during this review.

@shengliangxu
shengliangxu merged commit e74587b into main Oct 22, 2026
48 checks passed
@shengliangxu
shengliangxu deleted the shengliangx/ci-hf-ckpt-unit branch October 22, 2026 17:56
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-22 17:56 UTC

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.

3 participants