|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | +"""Worker-side helpers for tests that reuse one ``MpiPoolSession``. |
| 4 | +
|
| 5 | +Used by the automatic session-reuse layer (``test_common/session_reuse.py``). |
| 6 | +This is test infrastructure only and must not be used on production inference |
| 7 | +paths. Heavy imports are done lazily so importing this module never triggers a |
| 8 | +circular import during package initialization. |
| 9 | +""" |
| 10 | + |
| 11 | + |
| 12 | +def _barrier_run(fn): |
| 13 | + """Runs inside a worker; module-level so it is picklable. |
| 14 | +
|
| 15 | + The leading barrier is what makes ``submit_sync_per_worker`` exactly-once: |
| 16 | + a worker that picked up one of the ``n_workers`` submitted tasks blocks |
| 17 | + here until every other worker holds a task of its own, so no worker can |
| 18 | + drain a second one (pigeonhole). The workers' ``MPI_COMM_WORLD`` is the |
| 19 | + spawned worker world (the parent process is not a member), so the barrier |
| 20 | + spans exactly the pool's ``n_workers`` ranks. |
| 21 | + """ |
| 22 | + from mpi4py import MPI |
| 23 | + |
| 24 | + MPI.COMM_WORLD.barrier() |
| 25 | + return (MPI.COMM_WORLD.Get_rank(), fn()) |
| 26 | + |
| 27 | + |
| 28 | +def submit_sync_per_worker(mpi_session, fn, timeout: float = 60.0) -> list: |
| 29 | + """Run ``fn`` exactly ONCE on every worker; return results ordered by rank. |
| 30 | +
|
| 31 | + ``MpiPoolSession.submit_sync`` enqueues ``n_workers`` tasks, but |
| 32 | + ``MPIPoolExecutor`` gives no one-task-per-worker guarantee: a fast task can |
| 33 | + be drained twice by one idle worker, leaving another untouched. Each task |
| 34 | + therefore opens with a barrier across the worker world (see |
| 35 | + ``_barrier_run``), which pins the tasks one-per-worker. |
| 36 | +
|
| 37 | + This doubles as the pool health probe: completing the barrier proves every |
| 38 | + worker is alive AND that the worker world's collectives still function — |
| 39 | + the thing tests actually depend on. The wait is bounded by ``timeout``: a |
| 40 | + wedged worker (e.g. stuck in a collective after the previous test's |
| 41 | + executor died mid-shutdown) never completes the barrier, so an unbounded |
| 42 | + ``result()`` would hang the whole session. A timeout is reported as probe |
| 43 | + failure, making the caller retire the pool and spawn a fresh one. |
| 44 | + """ |
| 45 | + import concurrent.futures |
| 46 | + |
| 47 | + futures = mpi_session.submit(_barrier_run, fn) |
| 48 | + done, not_done = concurrent.futures.wait(futures, timeout=timeout) |
| 49 | + if not_done: |
| 50 | + raise RuntimeError( |
| 51 | + f"health probe timed out after {timeout}s: " |
| 52 | + f"{len(not_done)}/{len(futures)} worker tasks never returned" |
| 53 | + ) |
| 54 | + by_rank = dict(f.result() for f in done) # .result() re-raises worker errors |
| 55 | + missing = set(range(mpi_session.n_workers)) - set(by_rank) |
| 56 | + if missing: # unreachable given the barrier; guards scheduler surprises |
| 57 | + raise RuntimeError(f"no task ran on worker ranks {sorted(missing)}") |
| 58 | + return [result for _, result in sorted(by_rank.items())] |
| 59 | + |
| 60 | + |
| 61 | +def reset_worker_torch_compile_state() -> None: |
| 62 | + """Reset per-worker torch.compile / Dynamo state (runs inside each worker). |
| 63 | +
|
| 64 | + Dynamo's recompile counter is process-global and per-code-object. When |
| 65 | + worker processes are reused across LLMs (shared ``MpiPoolSession``), each |
| 66 | + ``torch_compile`` case recompiles the same ``model.forward`` code object |
| 67 | + under new guards; the count accumulates and eventually trips |
| 68 | + ``recompile_limit`` (16), which is a HARD failure under ``fullgraph=True`` |
| 69 | + (``FailOnRecompileLimitHit``) and aborts the whole MPI job. Resetting |
| 70 | + between cases makes each LLM start from a clean compile cache, like a fresh |
| 71 | + process. Run on every worker via ``submit_sync_per_worker``. |
| 72 | + """ |
| 73 | + import torch |
| 74 | + |
| 75 | + torch._dynamo.reset() |
0 commit comments