| ############################################################################ |
| # Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| ############################################################################ |
| """Tests for the partition MinNodes limit with task-count jobs.""" |
| |
| import os |
| |
| import pytest |
| |
| import atf |
| |
| test_name = os.path.splitext(os.path.basename(__file__))[0] |
| PARTITION_NAME = "min_nodes_part" |
| MIN_NODES = 2 |
| LOW_MIN_NODES = 1 |
| |
| |
| @pytest.fixture(scope="module", autouse=True) |
| def setup(): |
| atf.require_config_parameter("EnforcePartLimits", ["NO", None]) |
| atf.require_nodes(2, [("CPUs", 4), ("RealMemory", 1024)]) |
| atf.require_slurm_running() |
| |
| |
| @pytest.fixture(scope="function") |
| def partition(): |
| created = [] |
| |
| def _create(name, min_nodes): |
| nodes = list(atf.get_nodes().keys()) |
| node_range = atf.node_list_to_range(nodes[0:2]) |
| |
| atf.run_command( |
| f"scontrol create PartitionName={name} " |
| f"Nodes={node_range} MinNodes={min_nodes}", |
| fatal=True, |
| user=atf.properties["slurm-user"], |
| ) |
| created.append(name) |
| |
| yield _create |
| |
| atf.cancel_jobs(atf.properties["submitted-jobs"], quiet=True) |
| for name in created: |
| atf.run_command( |
| f"scontrol delete PartitionName={name}", |
| fatal=True, |
| user=atf.properties["slurm-user"], |
| ) |
| |
| |
| @pytest.fixture(scope="function") |
| def min_nodes_partition(partition): |
| partition(PARTITION_NAME, MIN_NODES) |
| |
| |
| @pytest.fixture(scope="function") |
| def unlimited_partition(partition): |
| partition(PARTITION_NAME, LOW_MIN_NODES) |
| |
| |
| def test_job_meets_min_nodes(min_nodes_partition): |
| """Verify the implicit max_nodes is not derived below the partition MinNodes""" |
| |
| job_id = atf.submit_job_sbatch( |
| f"-p {PARTITION_NAME} -n2 --ntasks-per-node=2 -J {test_name}" |
| ' -t 1 --wrap "hostname" -o /dev/null', |
| fatal=True, |
| ) |
| completed = atf.wait_for_job_state(job_id, "COMPLETED") |
| state = atf.get_job_parameter(job_id, "JobState", quiet=True) |
| reason = atf.get_job_parameter(job_id, "Reason", quiet=True) |
| |
| assert ( |
| completed |
| ), f"Job should have run on {MIN_NODES} nodes: state={state} reason={reason}" |
| |
| node_list = atf.node_range_to_list(atf.get_job_parameter(job_id, "NodeList")) |
| assert ( |
| len(node_list) == MIN_NODES |
| ), f"Job should allocate {MIN_NODES} node(s), got {len(node_list)}: {node_list}" |
| |
| |
| def test_raised_min_nodes_pends_until_job_updated(unlimited_partition): |
| """Verify a raised MinNodes pends a no-node-count job until NumNodes is set""" |
| |
| job_id = atf.submit_job_sbatch( |
| f"--hold -p {PARTITION_NAME} -n2 --ntasks-per-node=2 -J {test_name}" |
| ' -t 1 --wrap "hostname" -o /dev/null', |
| fatal=True, |
| ) |
| atf.run_command( |
| f"scontrol update PartitionName={PARTITION_NAME} MinNodes={MIN_NODES}", |
| fatal=True, |
| user=atf.properties["slurm-user"], |
| ) |
| atf.run_command(f"scontrol release {job_id}", fatal=True) |
| atf.wait_for_job_state( |
| job_id, "PENDING", desired_reason="PartitionNodeLimit", fatal=True |
| ) |
| |
| atf.run_command( |
| f"scontrol update jobid={job_id} NumNodes={MIN_NODES}", |
| fatal=True, |
| user=atf.properties["slurm-user"], |
| ) |
| atf.wait_for_job_state(job_id, "COMPLETED", fatal=True) |
| |
| node_list = atf.node_range_to_list(atf.get_job_parameter(job_id, "NodeList")) |
| assert ( |
| len(node_list) == MIN_NODES |
| ), f"Job should allocate {MIN_NODES} node(s), got {len(node_list)}: {node_list}" |