blob: 397415c85116bbb906537980be6428ef3b3c6845 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""GRES device chunks on a dynamic node.
A dynamic node (`slurmd -Z`) reads its own gres.conf and registers the chunk
layout, but the controller has no gres.conf entry to rebuild it from after a
restart.
"""
import re
import pytest
import atf
pytestmark = pytest.mark.slow
CHUNK_GPUS = 4
BAD_COUNT = 3
DYN_SIZES = "2"
dyn_node = "dynnode1"
@pytest.fixture(scope="module", autouse=True)
def setup():
# slurmd parses ChunkSizes/ChunkOrder, slurmctld allocates by them 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",
)
# Derive the cap from the configured nodes, or a config with more of them
# than a hardcoded value refuses to start and looks like a chunk failure.
# Room for the dynamic node.
configured = atf.get_nodes(live=False, quiet=True, fatal=True)
atf.require_config_parameter("MaxNodeCount", len(set(configured) - {"DEFAULT"}) + 1)
atf.require_config_parameter("TreeWidth", 65533)
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CPU")
atf.require_config_parameter_includes("GresTypes", "gpu")
for i in range(CHUNK_GPUS):
atf.require_tty(i)
atf.require_config_parameter(
"NodeName",
f"{dyn_node} Name=gpu Type=chunked "
f"File=/dev/tty[0-{CHUNK_GPUS - 1}] ChunkSizes={DYN_SIZES}",
source="gres",
)
atf.require_slurm_running()
def _gres_chunks(node):
output = atf.run_command_output(
f"scontrol show node -d {node}", fatal=True, quiet=True
)
match = re.search(
r"GresChunks=gpu(?::\w+)?:sizes=([^;,\s]+);order=([^;,\s]+)", output
)
return (match.group(1), match.group(2)) if match else (None, None)
def _wait_for_node_idle(node):
"""wait_for_node_state() fails while the node is still arriving."""
for _ in atf.timer():
state = atf.get_nodes(quiet=True).get(node, {}).get("state")
if state and "IDLE" in state and "DRAIN" not in state:
return True
return False
def _start_dynamic_slurmd():
"""Start the dynamic slurmd and wait for it to register IDLE.
No -b: after a slurmctld restart the node's last response is from the
state file, so a slurmd reporting a fresh boot would be marked
"unexpectedly rebooted" and set DOWN.
"""
atf.start_slurmd(dyn_node, f"-Z --conf 'Gres=gpu:chunked:{CHUNK_GPUS} CPUs=8'")
assert _wait_for_node_idle(dyn_node), f"{dyn_node} never registered IDLE"
def _stop_dynamic_slurmd():
"""Stop the dynamic slurmd, False if none was running."""
if not atf.slurmd_pids(dyn_node):
return False
atf.stop_slurmd(dyn_node)
return True
@pytest.fixture(scope="module")
def dynamic_node(setup):
_start_dynamic_slurmd()
yield dyn_node
_stop_dynamic_slurmd()
atf.run_command(
f"scontrol delete nodename={dyn_node}",
user=atf.properties["slurm-user"],
fatal=True,
quiet=True,
)
def test_dynamic_node_registers_chunks(dynamic_node):
assert _gres_chunks(dynamic_node) == (
DYN_SIZES,
"0/1/2/3",
), f"Expected {dynamic_node} to report its registered GresChunks"
def test_dynamic_node_enforces_chunk_sizes(dynamic_node):
job_id = atf.submit_job_sbatch(
f"-w {dynamic_node} --gres=gpu:chunked:2 --wrap 'true'", fatal=True
)
assert atf.wait_for_job_state(
job_id, "COMPLETED"
), "A configured chunk size should run on a dynamic node"
error = atf.run_command_error(
f"sbatch -w {dynamic_node} --gres=gpu:chunked:{BAD_COUNT} --wrap 'true'",
xfail=True,
fatal=True,
)
assert "Requested node configuration is not available" in error, error
def test_dynamic_node_chunks_survive_slurmctld_restart(dynamic_node):
"""Chunk state is restored from the node state file.
The dynamic slurmd is stopped first, so it cannot re-register, and the
controller has no gres.conf entry for the node to rebuild from.
"""
expected = _gres_chunks(dynamic_node)
assert _stop_dynamic_slurmd(), f"Expected a running slurmd for {dynamic_node}"
atf.restart_slurmctld(quiet=True)
restored = _gres_chunks(dynamic_node)
# Put the node back, so the module-scoped fixture still holds afterwards
_start_dynamic_slurmd()
assert (
restored == expected
), f"GresChunks after a slurmctld restart was {restored}, expected {expected}"