blob: 70ee77247c77080d920bb8d29a9e976388bc314a [file] [edit]
############################################################################
# Copyright (C) 2026 Google LLC.
############################################################################
"""Test topology/block with TPU cluster layouts and segment allocation."""
import re
import pytest
import atf
NODE_COUNT = 16
TPU_YAML_16NODE_SEG = """tpu_types:
- type: tpu7x
pci_device_id: "0x0076"
shape_per_node: 2x2x1
shape_per_segment: 4x4x4
nodes_per_pod: 2304
- type: v6e
pci_device_id: "0x006f"
shape_per_node: 2x2
shape_per_segment: 16x16
nodes_per_pod: 64
"""
TPU_YAML_4NODE_SEG = """tpu_types:
- type: tpu7x
pci_device_id: "0x0076"
shape_per_node: 2x2x1
shape_per_segment: 2x2x4
nodes_per_pod: 2304
"""
GRES_CONF = """AutoDetect=off
Name=tpu Type=tpu7x Count=4
"""
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 11),
"bin/sbatch",
reason="Issue 51191: sbatch --layout and job_submit/tpu added in 26.11",
)
atf.require_version(
(26, 11),
"bin/scontrol",
reason="Issue 51191: scontrol shows Layout since 26.11",
)
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CPU")
atf.require_config_parameter_includes("JobSubmitPlugins", "tpu")
atf.require_config_parameter_includes("GresTypes", "tpu")
atf.require_config_parameter_includes("SchedulerParameters", ("bf_interval", 1))
atf.require_config_parameter_includes("SchedulerParameters", ("sched_interval", 1))
atf.require_config_file("tpu.yaml", TPU_YAML_16NODE_SEG)
atf.require_config_file("gres.conf", GRES_CONF)
atf.require_nodes(NODE_COUNT, [("Gres", "tpu:tpu7x:4"), ("CPUs", 8)])
# Only to wait for node registration (debug2)
atf.require_config_parameter("SlurmctldDebug", "debug2")
atf.require_slurm_running()
def _use_topology(topo_yaml, tpu_yaml=TPU_YAML_16NODE_SEG):
atf.require_config_file("topology.yaml", topo_yaml)
atf.require_config_file("tpu.yaml", tpu_yaml)
atf.restart_slurmctld(quiet=True)
# Wait for nodes registration
nodes = {f"node{n}" for n in range(1, NODE_COUNT + 1)}
for _ in atf.timer(poll_interval=0.5):
log = atf.get_log("slurmctld").rsplit("slurmctld version", 1)[-1]
registered = set(
re.findall(r"_slurm_rpc_node_registration complete for (\S+) ", log)
)
if nodes <= registered:
break
else:
pytest.fail(
f"Nodes {sorted(nodes - registered)} never registered after the restart"
)
def _submit_and_wait(layout_arg):
job_id = atf.submit_job_sbatch(
f"{layout_arg} --output=/dev/null --wrap='sleep infinity'",
fatal=True,
)
assert atf.wait_for_job_state(
job_id, "RUNNING"
), f"Job {job_id} ({layout_arg}) failed to start"
return job_id
def _nodes(first, last):
return {f"node{n}" for n in range(first, last + 1)}
def _allocated(job_id):
return set(atf.node_range_to_list(atf.get_job_parameter(job_id, "NodeList")))
def _check_job(job_id, shape, expected_nodes, valid_blocks):
assert (
atf.get_job_parameter(job_id, "Layout") == f"tpu7x={shape}"
), f"The layout of job {job_id} should be stored"
assert (
int(atf.get_job_parameter(job_id, "NumNodes")) == expected_nodes
), f"tpu7x={shape} should need {expected_nodes} nodes"
allocated = _allocated(job_id)
assert any(
allocated <= block for block in valid_blocks
), f"Expected tpu7x={shape} within one of {valid_blocks}, got {allocated}"
def test_subcube_cluster():
"""Sub-cube cluster (4 nodes): base blocks of size 2, aggregated block of size 4."""
topo_yaml = """- topology: topo-block
cluster_default: true
block:
block_sizes: [2, 4]
blocks:
- block: b0_2x2x2_0
nodes: node[1-2]
- block: b0_2x2x2_1
nodes: node[3-4]
"""
_use_topology(topo_yaml)
out = atf.run_command_output("scontrol show topology", fatal=True)
for expected in (
"BlockName=b0_2x2x2_0",
"BlockName=b0_2x2x2_1",
"BlockSize=2",
"BlockSize=4",
"AggregatedBlock=b0_2x2x2_[0-1]",
):
assert expected in out, f"Expected {expected} in the topology:\n{out}"
base = [_nodes(1, 2), _nodes(3, 4)]
for shape, expected_nodes, valid_blocks in [
("2x2x1", 1, base),
("2x2x2", 2, base),
("2x2x4", 4, [_nodes(1, 4)]),
]:
job_id = _submit_and_wait(f"--layout=tpu7x={shape}")
_check_job(job_id, shape, expected_nodes, valid_blocks)
atf.cancel_all_jobs(fatal=True, quiet=True)
def test_single_cube_ordered():
"""Single-cube cluster (16 nodes, ordered names): full hierarchy and sub-cube allocations."""
topo_yaml = """- topology: topo-block
cluster_default: true
block:
block_sizes: [2, 4, 8, 16]
blocks:
- block: b0_2x2x2_0
nodes: node[1-2]
- block: b0_2x2x2_1
nodes: node[3-4]
- block: b0_2x2x2_2
nodes: node[5-6]
- block: b0_2x2x2_3
nodes: node[7-8]
- block: b0_2x2x2_4
nodes: node[9-10]
- block: b0_2x2x2_5
nodes: node[11-12]
- block: b0_2x2x2_6
nodes: node[13-14]
- block: b0_2x2x2_7
nodes: node[15-16]
"""
_use_topology(topo_yaml)
out = atf.run_command_output("scontrol show topology", fatal=True)
for i in range(8):
assert (
f"BlockName=b0_2x2x2_{i}" in out
), f"Expected block b0_2x2x2_{i} in the topology:\n{out}"
for aggregated in ("0-1", "2-3", "4-5", "6-7", "0-3", "4-7", "0-7"):
assert (
f"AggregatedBlock=b0_2x2x2_[{aggregated}]" in out
), f"Expected aggregated block b0_2x2x2_[{aggregated}]:\n{out}"
def blocks(size):
return [_nodes(first, first + size - 1) for first in range(1, 17, size)]
for shape, expected_nodes in [
("2x2x1", 1),
("2x2x2", 2),
("2x2x4", 4),
("2x4x4", 8),
("4x4x4", 16),
]:
job_id = _submit_and_wait(f"--layout=tpu7x={shape}")
_check_job(job_id, shape, expected_nodes, blocks(max(expected_nodes, 2)))
atf.cancel_all_jobs(fatal=True, quiet=True)
def test_single_cube_unordered():
"""Single-cube cluster with unordered hostnames aggregates by block definition order."""
topo_yaml = """- topology: topo-block
cluster_default: true
block:
block_sizes: [2, 4, 8, 16]
blocks:
- block: b0_2x2x2_0
nodes: node[13,5]
- block: b0_2x2x2_1
nodes: node[10,2]
- block: b0_2x2x2_2
nodes: node[16,8]
- block: b0_2x2x2_3
nodes: node[4,12]
- block: b0_2x2x2_4
nodes: node[1,9]
- block: b0_2x2x2_5
nodes: node[6,14]
- block: b0_2x2x2_6
nodes: node[3,11]
- block: b0_2x2x2_7
nodes: node[15,7]
"""
_use_topology(topo_yaml)
out = atf.run_command_output("scontrol show topology", fatal=True)
for expected in (
"BlockName=b0_2x2x2_0 BlockIndex=0 Nodes=node[13,5] BlockSize=2",
"BlockName=b0_2x2x2_1 BlockIndex=1 Nodes=node[10,2] BlockSize=2",
"AggregatedBlock=b0_2x2x2_[0-1]",
"Nodes=node[2,5,10,13]",
"AggregatedBlock=b0_2x2x2_[0-7]",
"Nodes=node[1-16]",
):
assert expected in out, f"Expected {expected} in the topology:\n{out}"
valid_4node = [
{"node2", "node5", "node10", "node13"},
{"node4", "node8", "node12", "node16"},
{"node1", "node6", "node9", "node14"},
{"node3", "node7", "node11", "node15"},
]
job_id = _submit_and_wait("--layout=tpu7x=2x2x4")
allocated = _allocated(job_id)
assert (
allocated in valid_4node
), f"Expected 4-node job to land on an aggregated block {valid_4node}, got {allocated}"
def test_multi_segment_cluster():
"""Multi-segment requests allocate discrete segments via topology/block."""
topo_blocks = []
for seg in range(4):
for b in range(2):
n_start = seg * 4 + b * 2 + 1
n_end = n_start + 1
topo_blocks.append(
f" - block: b0_c{seg}_{b}\n nodes: node[{n_start}-{n_end}]"
)
topo_yaml = (
"- topology: topo-block\n"
" cluster_default: true\n"
" block:\n"
" block_sizes: [2, 4]\n"
" blocks:\n" + "\n".join(topo_blocks) + "\n"
)
_use_topology(topo_yaml, tpu_yaml=TPU_YAML_4NODE_SEG)
out = atf.run_command_output("scontrol show topology", fatal=True)
for expected in (
"AggregatedBlock=b0_c0_[0-1]",
"Nodes=node[1-4]",
"AggregatedBlock=b0_c3_[0-1]",
"Nodes=node[13-16]",
):
assert expected in out, f"Expected {expected} in the topology:\n{out}"
segments = [_nodes(first, first + 3) for first in range(1, 17, 4)]
for shape, expected_nodes in [("2x2x4", 4), ("2x2x8", 8), ("2x2x16", 16)]:
job_id = _submit_and_wait(f"--layout=tpu7x={shape}")
assert (
int(atf.get_job_parameter(job_id, "NumNodes")) == expected_nodes
), f"tpu7x={shape} should need {expected_nodes} nodes"
allocated = _allocated(job_id)
assert all(
segment <= allocated or not (segment & allocated) for segment in segments
), f"Expected tpu7x={shape} on whole segments {segments}, got {allocated}"
atf.cancel_all_jobs(fatal=True, quiet=True)