blob: bccccb0af8708bbf61bcc24d51f7f04ccf281d9f [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""GRES device chunks on GPUs that slurmd detects.
fake_gpus.conf stands in for a GPU library, so slurmd merges the detected
devices with the gres.conf records as AutoDetect does. The chunk settings can
only come from gres.conf, so they must survive that merge, and the link data
that detection adds must not decide the chunks.
"""
import pytest
import atf
GPUS = 4
# Links on the detected devices pair 1 and 2, across the boundary of the
# chunks of 2
FAKE_LINKS = ["-1,0,0,0", "0,-1,1,0", "0,1,-1,0", "0,0,0,-1"]
PAIRS = [{0, 1}, {2, 3}]
node_name = None
@pytest.fixture(scope="module", autouse=True)
def setup():
global node_name
# slurmd parses ChunkSizes/ChunkOrder and scontrol shows them
for component in ["sbin/slurmd", "bin/scontrol"]:
atf.require_version(
(26, 11),
component,
reason="Issue 51025: gres.conf ChunkSizes/ChunkOrder are new in 26.11",
)
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CPU")
atf.require_config_parameter_includes("GresTypes", "gpu")
atf.require_config_parameter_includes("SlurmdParameters", "config_overrides")
atf.require_nodes(1, [("CPUs", GPUS), ("Gres", f"gpu:{GPUS}")])
for i in range(GPUS):
atf.require_tty(i)
gpu_nodes = sorted(
name
for name, params in atf.get_nodes(live=False, quiet=True, fatal=True).items()
if name != "DEFAULT" and f"gpu:{GPUS}" in str(params.get("Gres", ""))
)
assert gpu_nodes, "Expected require_nodes() to configure a node with GPUs"
node_name = gpu_nodes[0]
atf.require_config_file(
"fake_gpus.conf",
"".join(
f"(null)|{GPUS}|0-{GPUS - 1}|{links}|/dev/tty{i}|(null)|nvidia_gpu_env\n"
for i, links in enumerate(FAKE_LINKS)
),
)
atf.require_config_file(
"gres.conf", _gres_conf(f"File=/dev/tty[0-{GPUS - 1}] ChunkSizes=2")
)
atf.require_slurm_running()
def _gres_conf(record):
return f"NodeName={node_name} Name=gpu {record}\n"
def test_chunk_config_survives_detection():
assert atf.wait_for_node_state(node_name, "IDLE", operator=atf.SetMatch.EQUAL), (
f"{node_name} should register IDLE (state="
f"{atf.get_node_parameter(node_name, 'state')}, "
f"reason={atf.get_node_parameter(node_name, 'reason')})"
)
log_file = atf.get_config_parameter("SlurmdLogFile").replace("%n", node_name)
assert (
atf.run_command_exit(
f"grep -q 'Adding fake system GPU data' {log_file}", user="root"
)
== 0
), f"Expected slurmd to merge the fake GPUs, see {log_file}"
output = atf.run_command_output(
f"scontrol show node -d {node_name}", fatal=True, quiet=True
)
assert (
"GresChunks=gpu:sizes=2;order=0/1/2/3" in output
), f"Expected the chunk settings of gres.conf to survive the merge: {output}"
@pytest.mark.parametrize("flags", ["", "--gres-flags=enforce-links"])
def test_detected_links_do_not_decide_chunks(flags):
"""Two jobs get the aligned pairs, not the linked devices 1 and 2."""
allocated = []
for _ in PAIRS:
job_id = atf.submit_job_sbatch(
f"-N1 -w {node_name} --gres=gpu:2 {flags} --wrap 'sleep infinity'",
fatal=True,
)
assert atf.wait_for_job_state(
job_id, "RUNNING"
), f"Job {job_id} should reach RUNNING"
allocated.append(atf.get_job_gres_indexes(job_id))
for indexes in allocated:
assert indexes in PAIRS, f"Expected one of {PAIRS}, got {indexes}"
assert allocated[0] != allocated[1], f"Two jobs share chunk {allocated[0]}"
def test_chunk_sizes_without_file_stops_slurmd():
"""A detected node still needs File on the record that sets ChunkSizes."""
gres_conf = f"{atf.properties['slurm-config-dir']}/gres.conf"
atf.stop_slurmd(node_name)
atf.run_command(
f"cat > {gres_conf}",
input=_gres_conf(f"Count={GPUS} ChunkSizes=2"),
user=atf.properties["slurm-user"],
fatal=True,
quiet=True,
)
result = atf.run_command(
f"timeout 15 {atf.properties['slurm-sbin-dir']}/slurmd -D -N {node_name}",
user="root",
xfail=True,
quiet=True,
)
# Put the node back before asserting, so a failure does not strand it
atf.run_command(
f"cat > {gres_conf}",
input=_gres_conf(f"File=/dev/tty[0-{GPUS - 1}] ChunkSizes=2"),
user=atf.properties["slurm-user"],
fatal=True,
quiet=True,
)
atf.start_slurmd(node_name, quiet=True)
assert result["exit_code"] == 1, result
assert "ChunkSizes requires File or MultipleFiles" in result["stderr"], result