blob: f8befd462f1dd40354ea3089372f1b3df9df94ab [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""An untracked GPU drain opens no accounting event.
gres/gpu is not in AccountingStorageTRES by default. Draining one still
marks the node DEGRADED, and sacctmgr records no drained-TRES event for it.
Once a tracked TRES is drained too, the event counts only the tracked one,
but its Reason and TresReference still name the untracked GPU.
"""
import re
import pytest
import atf
pytestmark = pytest.mark.slow
NODE = "node100"
PORT = 61622
REASON = "untracked_gpu"
GPU_A = "GPU-aaaaaaaa-0000-0000-0000-000000000000"
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 11),
component="bin/sacctmgr",
reason="Ticket 25180: drained-TRES events added in 26.11",
)
atf.require_version(
(26, 11),
component="sbin/slurmd",
reason="Ticket 25180: GRES device slots keyed by UUID added in 26.11",
)
atf.require_accounting()
atf.require_config_parameter_excludes("AccountingStorageTRES", "gres/gpu")
atf.require_config_parameter("MaxNodeCount", 4)
atf.require_config_parameter("TreeWidth", 65533)
atf.require_config_parameter_includes("SlurmctldParameters", "cloud_reg_addrs")
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CPU")
atf.require_config_parameter_includes("GresTypes", "gpu")
atf.require_tty(0)
atf.require_slurm_running()
def _one_gpu_node(dynamic_slurmds):
atf.require_config_file("gres.conf", "Autodetect=full\n")
atf.require_config_file("fake_gpus.conf", atf.fake_gpus_conf((0, GPU_A)))
atf.restart_slurm(clean=True, quiet=True)
dynamic_slurmds(NODE, f"Gres=gpu:a100:1 Port={PORT}")
def test_untracked_gpu_drain_opens_no_event(dynamic_slurmds):
"""Draining gres/gpu with tracking left off records no event for that drain."""
_one_gpu_node(dynamic_slurmds)
atf.update_tres_drain(NODE, "gres/gpu", "Index=0", reason=REASON, fatal=True)
assert atf.wait_for_node_state(
NODE, "DEGRADED"
), "the node should be DEGRADED even though the GPU is not tracked"
# Once slurmctld has sent slurmdbd everything queued, any event the
# drain opened would be in the database.
atf.wait_for_dbd_agent_drained(fatal=True)
events = atf.get_node_events(NODE) or ""
assert REASON not in events, events
# States=Degraded selects only a drained-TRES event. An untracked GPU
# must not open one, even under some other reason.
degraded = atf.run_command_output(
f"sacctmgr -n -P list events all_time Event=Node Nodes={NODE} "
"States=Degraded format=nodename,reason,tres,state",
user=atf.properties["slurm-user"],
fatal=True,
quiet=True,
)
assert not (
degraded or ""
).strip(), f"an untracked GPU drain opened a drained-TRES event:\n{degraded}"
def test_event_names_untracked_gpu_beside_tracked_cpu(dynamic_slurmds):
"""An event opened by a tracked drain still names the untracked one.
CPUs are always tracked, so draining one opens the event. Only the CPU is
counted in its TRES, but its Reason and TresReference list the GPU too.
"""
_one_gpu_node(dynamic_slurmds)
atf.update_tres_drain(NODE, "gres/gpu", "Index=0", reason="gpu_why", fatal=True)
atf.update_tres_drain(NODE, "cpu", "Index=0", reason="cpu_why", fatal=True)
for _ in atf.timer(fatal=True):
reason = atf.get_open_node_event_reason(NODE)
if reason == "cpu_why,gpu_why":
break
tres = atf.get_open_node_event_tres(NODE) or ""
assert re.search(
r"(?:^|,)cpu=1(?:,|$)", tres
), f"the drained CPU should be counted: {atf.get_node_events(NODE)!r}"
assert (
"gres/gpu" not in tres
), f"the untracked GPU should not be counted: {atf.get_node_events(NODE)!r}"
assert atf.get_open_node_event_tres_reference(NODE) == (
f"cpu(IDX:0),gres/gpu:a100(IDX:0)(UUID:{GPU_A})"
), f"the event should name both TRES: {atf.get_node_events(NODE)!r}"