blob: f0cf4f96a1bf1e586fd9310ece4de79f6e321b1d [file]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Client tooling against a heterogeneous job running with --stepmgr.
sbcast has to resolve --jobid=<leader> and --jobid=<leader>+<offset> through
the controller and send each component its own job_id, because the
<leader>+<offset> form is not resolvable on the target slurmd after reroute.
squeue --only-job-state has to report every component of the hetjob.
Both already have coverage against a plain hetjob elsewhere in the suite,
so this only adds the stepmgr combination.
"""
import re
import pytest
import atf
pytestmark = pytest.mark.slow
HET_OPTS = "--stepmgr -N1 --ntasks=1 -t2 --exclusive : -N1 --ntasks=1 -t2 --exclusive"
@pytest.fixture(scope="module", autouse=True)
def setup():
# The client tools upgrade last, so gating on sbcast >= 26.11 already
# implies slurmd and slurmctld are >= 26.11.
atf.require_version(
(26, 11),
component="bin/sbcast",
reason="Issue 50976: hetjob + stepmgr requires sbcast 26.11+",
)
atf.require_nodes(2)
atf.require_config_parameter_includes("PrologFlags", "Contain")
# Het jobs are only started by backfill; don't wait for its full cycle
atf.require_config_parameter_includes("SchedulerParameters", ("bf_interval", 1))
atf.require_config_parameter("SchedulerType", "sched/backfill")
atf.require_slurm_running()
if atf.get_config_parameter("SlurmdUser") != "root(0)":
pytest.skip("Issue 50976: sbcast requires SlurmdUser to be root")
def _start_hetjob():
"""Submit a running 2-component stepmgr hetjob and return its leader id."""
leader_job_id = atf.submit_job_sbatch(
f"-o /dev/null {HET_OPTS} --wrap='sleep infinity'", fatal=True
)
atf.wait_for_job_state(leader_job_id, "RUNNING", fatal=True)
return leader_job_id
def test_sbcast_hetjob_stepmgr():
"""sbcast reaches the leader, and each component by +<offset>."""
leader_job_id = _start_hetjob()
bcast_dir = f"bcast_{leader_job_id}"
payload = "sbcast_payload"
atf.make_bash_script(payload, "true\n")
# A relative destination resolves against BcastParameters' directory when
# one is configured, and against the invoking working directory otherwise.
atf.run_command(f"mkdir -p {bcast_dir}", fatal=True)
# The bare leader id fans out to every component; the +<offset> form
# targets one component and is the form that is not resolvable on the
# target slurmd once the request has been rerouted.
atf.run_command(
f"sbcast -f --jobid={leader_job_id} {payload} {bcast_dir}/file", fatal=True
)
jobs = atf.get_jobs(leader_job_id, fatal=True)
components = sorted(
atf.range_to_list(jobs[leader_job_id]["HetJobIdSet"]),
key=lambda job_id: jobs[job_id]["HetJobOffset"],
)
# --exclusive puts the components on different nodes, so the resolved job
# id and node list tell apart a request that reached its own component
# from one that collapsed onto the leader.
nodes = [jobs[job_id]["NodeList"] for job_id in components]
if nodes[0] == nodes[1]:
pytest.fail(
f"The components should be on different nodes for the routing"
f" assertions to be meaningful, got {nodes}"
)
for offset, job_id in enumerate(components):
result = atf.run_command(
f"sbcast -vv -f --jobid={leader_job_id}+{offset} {payload}"
f" {bcast_dir}/file_comp{offset}",
fatal=True,
)
assert re.search(rf"^sbcast: jobid\s+= {job_id}$", result["stderr"], re.M), (
f"sbcast --jobid={leader_job_id}+{offset} must resolve to component"
f" job {job_id}.\nstderr:\n{result['stderr']}"
)
node = jobs[job_id]["NodeList"]
assert re.search(rf"^sbcast: node_list\s+= {node}$", result["stderr"], re.M), (
f"sbcast --jobid={leader_job_id}+{offset} must target component"
f" job {job_id}'s node {node}.\nstderr:\n{result['stderr']}"
)
for name in ("file", "file_comp0", "file_comp1"):
atf.assert_file_contents(f"{bcast_dir}/{name}", "true", contains=True)
def test_squeue_only_job_state_hetjob_stepmgr():
"""squeue --only-job-state reports every component of a stepmgr hetjob."""
leader_job_id = _start_hetjob()
output = (
atf.run_command_output(
f"squeue --noheader --only-job-state --format='%i=%T' -j {leader_job_id}",
fatal=True,
)
.strip()
.splitlines()
)
assert f"{leader_job_id}+0=RUNNING" in output, (
f"Expected component 0 of hetjob {leader_job_id} in --only-job-state "
f"output, got {output}"
)
assert f"{leader_job_id}+1=RUNNING" in output, (
f"Expected component 1 of hetjob {leader_job_id} in --only-job-state "
f"output, got {output}"
)
assert (
len(output) == 2
), f"Expected exactly 2 hetjob components, got {len(output)}: {output}"