Skip to content

Run as a nested job step when launched inside an existing allocation - #71

Open
bvanessen wants to merge 4 commits into
llnl:mainfrom
bvanessen:enable_nested_slurm_launch
Open

Run as a nested job step when launched inside an existing allocation#71
bvanessen wants to merge 4 commits into
llnl:mainfrom
bvanessen:enable_nested_slurm_launch

Conversation

@bvanessen

Copy link
Copy Markdown
Collaborator

When invoked from inside a scheduler allocation (salloc/sbatch/flux alloc), a blocking launch previously forced allocation-level flags (-N, --partition, ...) that made SLURM treat the srun as a request for a brand-new allocation instead of a job step in the current one.

CLI:

  • With no job-size flag (-N/-g/--gpumem-at-least), inherit the node count from the enclosing allocation via a new scheduler-agnostic probe (hpc_launcher.schedulers.num_nodes_in_current_allocation) that checks the Flux, SLURM, and LSF environment markers. Explicit flags still win; --local is excluded.
  • Add -c/--cpus-per-task to give a job/step an explicit CPU footprint (SLURM --cpus-per-task, Flux --cores-per-task/--cores-per-slot).

SLURM (blocking launch with SLURM_JOB_ID set):

  • Drop allocation-selection flags (--partition/--account/--reservation) with a warning; they are fixed by the enclosing allocation and would otherwise force a new one.
  • Reject a node request that exceeds the allocation upfront, instead of letting srun silently queue it as a second allocation.
  • Add --overlap by default so concurrent steps in one allocation don't hang on "step creation temporarily disabled, retrying (Requested nodes are busy)" (SLURM >= 20.11 gives steps exclusive CPUs). --overlap is omitted when --cpus-per-task (steps pack onto disjoint CPUs via the implied --exact) or --exclusive is given, and can be removed with -x ~--overlap.

Non-blocking (sbatch/flux batch) submissions are unchanged: submitting a batch job from inside an allocation deliberately creates a new job.

Adds tests/nested_job_step_test.py (17 tests, no scheduler binaries required) and documents the behavior in launch_cli.md and torchrun-hpc_cli.md.

When invoked from inside a scheduler allocation (salloc/sbatch/flux
alloc), a blocking launch previously forced allocation-level flags
(-N, --partition, ...) that made SLURM treat the srun as a request for
a brand-new allocation instead of a job step in the current one.

CLI:
- With no job-size flag (-N/-g/--gpumem-at-least), inherit the node
  count from the enclosing allocation via a new scheduler-agnostic
  probe (hpc_launcher.schedulers.num_nodes_in_current_allocation) that
  checks the Flux, SLURM, and LSF environment markers. Explicit flags
  still win; --local is excluded.
- Add -c/--cpus-per-task to give a job/step an explicit CPU footprint
  (SLURM --cpus-per-task, Flux --cores-per-task/--cores-per-slot).

SLURM (blocking launch with SLURM_JOB_ID set):
- Drop allocation-selection flags (--partition/--account/--reservation)
  with a warning; they are fixed by the enclosing allocation and would
  otherwise force a new one.
- Reject a node request that exceeds the allocation upfront, instead of
  letting srun silently queue it as a second allocation.
- Add --overlap by default so concurrent steps in one allocation don't
  hang on "step creation temporarily disabled, retrying (Requested
  nodes are busy)" (SLURM >= 20.11 gives steps exclusive CPUs).
  --overlap is omitted when --cpus-per-task (steps pack onto disjoint
  CPUs via the implied --exact) or --exclusive is given, and can be
  removed with -x ~--overlap.

Non-blocking (sbatch/flux batch) submissions are unchanged: submitting
a batch job from inside an allocation deliberately creates a new job.

Adds tests/nested_job_step_test.py (17 tests, no scheduler binaries
required) and documents the behavior in launch_cli.md and
torchrun-hpc_cli.md.
…e tests

Validating the nested-job-step feature on CTS-2 (Slurm) and El Capitan family
(Flux) surfaced several issues that kept concurrent steps from packing
onto one allocation, plus a CLI bug. All were confirmed and the fixes
re-validated on hardware.

Slurm (slurm.py):
- A step with a stated footprint gets --exact plus --mem=0 (share the
  job's memory instead of implicitly consuming all of it) and, for
  CPU-only steps, --gres=none (stop implicitly holding every GPU in the
  job). Without these, concurrent steps serialize on "step creation
  temporarily disabled, retrying (Requested nodes are busy)".

CTS-2 (cts2.py):
- Apply --mpibind=off / --gpu-bind=none only when creating an
  allocation, not to nested steps: together they disabled both
  mechanisms that export per-step CUDA_VISIBLE_DEVICES (mpibind SPANK
  plugin and Slurm's GPU binding), leaving the variable unset inside
  steps.

El Capitan family (el_capitan_family.py):
- Do not add --exclusive to nested Flux jobs; it forced each nested
  `flux run` to demand the whole node, serializing concurrent launches.
- Use Flux's own affinity plugins (-ompibind=off, -ogpu-affinity=
  per-task, -ocpu-affinity=per-task) for nested jobs and for any
  multi-GPU-per-task job: mpibind assigns GPUs by the NUMA locality of
  a task's cores rather than the granted resource set, which both caps
  a task at one GPU per domain and can hand two concurrent jobs the
  same GPU. Whole-node single-GPU-per-task launches keep mpibind.

CLI (configure.py):
- An explicit --gpus-per-proc 0 (CPU-only launch) is honored instead of
  being silently promoted to 1 on GPU systems; only an unset value now
  takes the default. Also guard the system-params info log when no
  active params exist.

Step resource reporting (schedulers/):
- Split the report script into slurm_step_resources.sh and
  flux_step_resources.sh over a shared step_resources_common.sh (CPU
  affinity, memory/NUMA, cgroup limits, GPU device-open tests, runtime
  view), with per-scheduler identity and verbose sections.

Tests:
- tests/slurm_step_resources_test.py + tests/run_slurm_step_packing.sh:
  from inside a 1-node CTS-2 allocation, pack 7 concurrent steps
  (2/1/1-GPU x 8 cores + 4 CPU-only x 16 cores), asserting distinct
  step IDs, overlapping lifetimes, disjoint CPU sets (SMT- and
  core-specialization-aware), cgroup-verified disjoint GPUs, and
  CUDA_VISIBLE_DEVICES consistency where exported.
- tests/flux_step_resources_test.py + tests/run_flux_step_packing.sh:
  the Flux equivalent, sized to the node's core count, judging GPU
  visibility via ROCR/CUDA_VISIBLE_DEVICES.
- nested_job_step_test.py: pin the new flag behavior (--exact/--mem=0/
  --gres=none, CTS-2 binding flags, El Cap --exclusive and affinity
  selection); resource_validation_test.py: pin --gpus-per-proc 0.
- Document in-allocation behavior updates in launch_cli.md.
@bvanessen
bvanessen marked this pull request as ready for review August 25, 2026 22:03
@bvanessen
bvanessen requested a review from tbennun August 25, 2026 22:03
The manual packing scripts (tests/run_{slurm,flux}_step_packing.sh) and
their pytest twins had grown into near-duplicates whose parsing and
verification logic had already been fixed in two places more than once.
Extending them to multi-node workloads made the duplication worse, so
the common structure now lives in shared modules and each scheduler
supplies only its policy.

Workloads (specs are nodes:gpus_per_task:cores_per_task, 1 task/node):
- 1-node (7 steps/jobs, as before): 2/1/1-GPU x 8 cores + 4 CPU-only
  (16 cores on Slurm/matrix; sized to the node on Flux).
- 2-node (8 steps/jobs): adds a node-spanning GPU step (1 GPU + 8 cores
  per task) and a spanning CPU step, mixed with the singles; sized so
  the worst-case per-node packing (4 GPUs) still fits.

Multi-node support required per-task report files (a spanning step's
tasks would interleave on shared stdout) and per-host verification:
CPU/GPU disjointness is now checked per node, since distinct nodes
legitimately reuse the same core and GPU numbering, and a spanning
step's tasks must agree on their step/job ID.

New shared pieces:
- tests/step_packing_common.sh: shell harness (temp dir, wrapper
  generation, launch/wait loops), parameterized by policy variables.
- tests/verify_step_packing.py: the single verifier, usable as a CLI
  (shell harness) and as a module (pytest). Policies: gpu-mode
  device-open (Slurm cgroup ground truth + CUDA_VISIBLE_DEVICES
  consistency) vs visible-env (Flux ROCR/CUDA_VISIBLE_DEVICES);
  cpu-mode exact (Slurm whole-core x SMT siblings) vs normalized-range
  (Flux, physical-core normalization with core-specialization
  tolerance). Reports all violations, not just the first.
- tests/step_packing_pytest.py: shared pytest harness plus a
  shared_tmp_path fixture.

The run_* wrappers and *_test.py files shrink to skip guards, workload
specs, and the policy knobs.

Fixes a real failure found on a 2-node tuolumne run: the work dir was
created under TMPDIR (/var/tmp), which is node-local on LC systems, so
a spanning job's second task failed with "step_wrapper.sh: No such
file or directory". The wrapper script and reports are now written to
a shared filesystem ($HOME by default, override with STEP_PACKING_DIR)
-- same fix in the pytest fixture, which skips cleanly where home is
not writable.

@tbennun tbennun left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

one comment

class SlurmScheduler(Scheduler):

@staticmethod
def in_slurm_allocation() -> bool:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why not make a generic in_allocation static method, so you have FluxScheduler.in_allocation() and slurm...

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.

2 participants