import sys from unittest.mock import MagicMock, patch import pytest import ray from ray._private.accelerators import TPUAcceleratorManager, tpu from ray.util.tpu import SlicePlacementGroup def test_get_current_pod_name_smoke(): with patch( "ray._private.accelerators.tpu.TPUAcceleratorManager.get_current_node_tpu_name", return_value="my-tpu", ): name = ray.util.tpu.get_current_pod_name() assert name == "my-tpu" def test_empty_get_current_pod_name_returns_none(): with patch( "ray._private.accelerators.tpu.TPUAcceleratorManager.get_current_node_tpu_name", return_value="", ): name = ray.util.tpu.get_current_pod_name() assert name is None @pytest.mark.parametrize( "test_case", [ # (number_chips_per_host, parsed accl_type, expected_worker_count) (4, "v2-4", 1), (4, "v3-32", 4), (4, "v4-8", 1), (4, "v4-16", 2), (8, "v5litepod-4", 1), (8, "v5litepod-8", 1), (8, "v5litepod-16", 2), (8, "v5litepod-32", 4), (4, "v5p-4", 1), (4, "v5p-8", 1), (4, "v5p-16", 2), (4, "v6e-4", 1), (8, "v6e-8", 1), (4, "v6e-8", 2), (8, "v6e-16", 2), (4, "v7x-8", 1), (4, "v7x-16", 2), ], ) @patch("glob.glob") def test_worker_count(mock_glob, test_case): num_devices, accelerator_type, expected_worker_count = test_case mock_glob.return_value = ["/dev/accel" + str(x) for x in range(num_devices)] TPUAcceleratorManager.get_current_node_num_accelerators.cache_clear() with patch( "ray._private.accelerators.tpu.TPUAcceleratorManager." "get_current_node_tpu_pod_type", return_value=accelerator_type, ): worker_count = ray.util.tpu.get_current_pod_worker_count() assert worker_count == expected_worker_count @patch("glob.glob") def test_num_tpu_chips(mock_glob): mock_glob.return_value = [ "/dev/accel0", "/dev/accel1", "/dev/accel2", "/dev/accel3", ] TPUAcceleratorManager.get_current_node_num_accelerators.cache_clear() num_tpu_chips = ray.util.tpu.get_num_tpu_chips_on_node() assert num_tpu_chips == 4 @pytest.mark.parametrize( "test_case", [ # (accelerator_type, accelerator_topology, expected_result) ("v2-16", "4x4", True), ("v2-256", "16x16", True), ("v2-4", "2x2", False), ("v3-16", "4x4", True), ("v3-1024", "32x32", True), ("v3-4", "4x16", False), ("v4-4", "2x2x1", True), ("v4-32", "2x4x4", True), ("v4-2048", "8x8x16", True), ("v4-4", "16x16x16", False), ("v5p-128", "4x4x4", True), ("v5p-4096", "16x16x16", True), ("v5p-12288", "16x16x24", True), ("v5p-4", "24x24x24", False), ("v5litepod-16", "2x8", True), ("v5litepod-256", "16x16", True), ("v5litepod-4", "2x2", True), ("v6e-8", "2x4", True), ("v6e-16", "4x4", True), ("v6e-64", "8x8", True), ("v6e-4", "4x16", False), ("tpu7x-16", "2x2x2", True), ("tpu7x-64", "2x4x4", True), ("v7x-8", "4x4", False), ], ) @patch("glob.glob") def test_is_valid_tpu_accelerator_topology(_mock_glob, test_case): """Test valid TPU accelerator topologies.""" accelerator_type, accelerator_topology, expected_result = test_case actual_result = TPUAcceleratorManager.is_valid_tpu_accelerator_topology( accelerator_type, accelerator_topology ) assert actual_result == expected_result def test_get_current_node_labels_env_only(monkeypatch): # Simulate GKE TPU environment variables monkeypatch.setenv("TPU_NAME", "tpu-worker-group-2") monkeypatch.setenv("TPU_WORKER_ID", "0") monkeypatch.setenv("TPU_ACCELERATOR_TYPE", "v6e-16") monkeypatch.setenv("TPU_TOPOLOGY", "4x4") tpu_labels = TPUAcceleratorManager.get_current_node_accelerator_labels() assert tpu_labels["ray.io/tpu-slice-name"] == "tpu-worker-group-2" assert tpu_labels["ray.io/tpu-worker-id"] == "0" assert tpu_labels["ray.io/tpu-topology"] == "4x4" assert tpu_labels["ray.io/tpu-pod-type"] == "v6e-16" def test_get_current_node_tpu_topology_from_metadata(): tpu_env_string = "TPU_ACCELERATOR:v6e.\nTOPOLOGY: '2x2x4'\nTPU_HOST_BOUNDS:0,1,1,2" with patch( "ray._private.accelerators.tpu._get_tpu_metadata", return_value=tpu_env_string ): topology = TPUAcceleratorManager.get_current_node_tpu_topology() assert topology == "2x2x4" @pytest.mark.parametrize( "topology, accelerator_type, expected_pod_type, should_raise", [ ("2x4", "TPU-V6E", "v6e-8", False), ("2x2x2", "TPU-V4", "v4-16", False), ("4x8", "TPU-V3", "v3-64", False), ("2x2x1", "TPU-V5P", "v5p-8", False), ("4x4", "TPU-V5P", "v5p-32", False), ("8x16", "TPU-V6E", "v6e-128", False), ("", "TPU-V3", None, False), ("4x", "TPU-V3", None, True), ("2x2x2", "TPU-V7X", "v7x-16", False), ], ) def test_infer_tpu_pod_type_from_topology( topology, accelerator_type, expected_pod_type, should_raise ): if should_raise: with pytest.raises(ValueError): tpu.infer_tpu_pod_type_from_topology(topology, accelerator_type) else: actual_result = tpu.infer_tpu_pod_type_from_topology(topology, accelerator_type) assert actual_result == expected_pod_type @pytest.fixture def ray_start_cpu(): address_info = ray.init(num_cpus=1) yield address_info ray.shutdown() @pytest.fixture def ray_tpu_cluster(ray_start_cluster): """ Simulates a Ray cluster with two multi-host TPU v4-16 slices. """ pod_type = "v4-16" topology = "2x2x2" cluster = ray_start_cluster slice_0_env_common = { "TPU_NAME": "test-slice-0", "TPU_ACCELERATOR_TYPE": pod_type, "TPU_TOPOLOGY": topology, } slice_0_head_labels = { "ray.io/tpu-slice-name": "test-slice-0", "ray.io/tpu-worker-id": "0", "ray.io/tpu-pod-type": pod_type, "ray.io/tpu-topology": topology, } slice_0_worker_labels = { "ray.io/tpu-slice-name": "test-slice-0", "ray.io/tpu-worker-id": "1", "ray.io/tpu-pod-type": pod_type, "ray.io/tpu-topology": topology, } cluster.add_node( num_cpus=2, resources={"TPU": 4, f"TPU-{pod_type}-head": 1}, env_vars={**slice_0_env_common, "TPU_WORKER_ID": "0"}, labels=slice_0_head_labels, ) cluster.add_node( num_cpus=2, resources={"TPU": 4}, env_vars={**slice_0_env_common, "TPU_WORKER_ID": "1"}, labels=slice_0_worker_labels, ) slice_1_env_common = { "TPU_NAME": "test-slice-1", "TPU_ACCELERATOR_TYPE": pod_type, "TPU_TOPOLOGY": topology, } slice_1_head_labels = { "ray.io/tpu-slice-name": "test-slice-1", "ray.io/tpu-worker-id": "0", "ray.io/tpu-pod-type": pod_type, "ray.io/tpu-topology": topology, } slice_1_worker_labels = { "ray.io/tpu-slice-name": "test-slice-1", "ray.io/tpu-worker-id": "1", "ray.io/tpu-pod-type": pod_type, "ray.io/tpu-topology": topology, } cluster.add_node( num_cpus=2, resources={"TPU": 4, f"TPU-{pod_type}-head": 1}, env_vars={**slice_1_env_common, "TPU_WORKER_ID": "0"}, labels=slice_1_head_labels, ) cluster.add_node( num_cpus=2, resources={"TPU": 4}, env_vars={**slice_1_env_common, "TPU_WORKER_ID": "1"}, labels=slice_1_worker_labels, ) ray.init(address=cluster.address) yield cluster ray.shutdown() @pytest.fixture def ray_v6e_tpu_cluster(ray_start_cluster): """ Simulates a Ray cluster with two v6e-8 slices (2x4 topology). """ pod_type = "v6e-8" topology = "2x4" cluster = ray_start_cluster for i in range(2): env_common = { "TPU_NAME": f"test-v6e-slice-{i}", "TPU_ACCELERATOR_TYPE": pod_type, "TPU_TOPOLOGY": topology, } head_labels = { "ray.io/tpu-slice-name": f"test-v6e-slice-{i}", "ray.io/tpu-worker-id": "0", "ray.io/tpu-pod-type": pod_type, "ray.io/tpu-topology": topology, } # A single-host v6e-8 has 8 chips on one node cluster.add_node( num_cpus=4, resources={"TPU": 8, f"TPU-{pod_type}-head": 1}, env_vars={**env_common, "TPU_WORKER_ID": "0"}, labels=head_labels, ) ray.init(address=cluster.address) yield cluster ray.shutdown() def test_fetch_tpu_slice_name_from_pg(ray_tpu_cluster): """Tests that the slice name can be fetched from a PG.""" tpu_head_pg = ray.util.placement_group(bundles=[{"TPU-v4-16-head": 1}]) ray.get(tpu_head_pg.ready()) expected_unique_slice_names = {"test-slice-0", "test-slice-1"} slice_name = tpu.fetch_tpu_slice_name_from_pg(tpu_head_pg) assert slice_name in expected_unique_slice_names ray.util.remove_placement_group(tpu_head_pg) def test_reserve_tpu_slice(ray_tpu_cluster): """Tests that a TPU slice can be successfully reserved.""" reserved_name_0, hg_pg_0 = tpu.reserve_tpu_slice( topology="2x2x2", accelerator_type="TPU-V4" ) reserved_name_1, hg_pg_1 = tpu.reserve_tpu_slice( topology="2x2x2", accelerator_type="TPU-V4" ) # Ensure the placement groups reserving the TPU slice using the head worker are valid. assert hg_pg_0 is not None, "Expected placement group for slice 0, got None" assert hg_pg_1 is not None, "Expected placement group for slice 1, got None" assert ( reserved_name_0 != reserved_name_1 ), f"Expected to reserve two different slices, but got the same name: {reserved_name_0}" expected_unique_slice_names = {"test-slice-0", "test-slice-1"} actual_reserved_names = {reserved_name_0, reserved_name_1} assert actual_reserved_names == expected_unique_slice_names, ( f"Got unexpected slice names. Expected {expected_unique_slice_names}, " f"but got {actual_reserved_names}" ) def test_slice_placement_group(ray_tpu_cluster): """Test that single TPU slice can be successfully reserved.""" slice_placement_group = ray.util.tpu.slice_placement_group( topology="2x2x2", accelerator_version="v4", ) assert slice_placement_group.chips_per_host == 4 assert slice_placement_group.num_hosts == 2 assert slice_placement_group.placement_group.bundle_count == 2 assert slice_placement_group.placement_group.bundle_specs == [ {"TPU": 4, "CPU": 1.0}, {"TPU": 4, "CPU": 1.0}, ] def test_multi_slice_placement_group(ray_tpu_cluster): """Test that multiple whole TPU slices can be successfully reserved""" multi_slice_placement_group = ray.util.tpu.slice_placement_group( topology="2x2x2", accelerator_version="v4", num_slices=2, ) assert multi_slice_placement_group.placement_group.bundle_count == 4 assert multi_slice_placement_group.num_hosts == 4 assert multi_slice_placement_group.placement_group.bundle_specs == [ {"TPU": 4, "CPU": 1.0}, # slice 1, host 1 {"TPU": 4, "CPU": 1.0}, # slice 1, host 2 {"TPU": 4, "CPU": 1.0}, # slice 2, host 1 {"TPU": 4, "CPU": 1.0}, # slice 2, host 2 ] @patch("ray.util.tpu.placement_group") @patch("ray.util.tpu.remove_placement_group") @patch("ray.util.tpu.reserve_tpu_slice") def test_slice_placement_group_partial_failure_cleanup( mock_reserve, mock_remove_pg, mock_create_pg ): """ Verifies that if a multi-slice request fails halfway through, the TPU head placement groups are cleaned up to prevent leaks. """ fake_head_pg_1 = MagicMock(name="head_pg_1") mock_reserve.side_effect = [("slice_1", fake_head_pg_1), None] with pytest.raises(RuntimeError, match="Failed to reserve TPU slice"): SlicePlacementGroup(topology="2x2x2", accelerator_version="v4", num_slices=2) # Validate that 2 TPU util attempted to reserve two slices, failed, and # correctly cleaned up the hanging TPU head placement groups. assert mock_reserve.call_count == 2 mock_remove_pg.assert_called_once_with(fake_head_pg_1) mock_create_pg.assert_not_called() @pytest.mark.parametrize( "accelerator_type, expected_version", [ # type with "TPU-" prefix ("TPU-V4", "v4"), ("TPU-v4", "v4"), ("TPU-V6E", "v6e"), ("TPU-v5p", "v5p"), ("TPU-V7X", "v7x"), # Only the TPU version - no parsing necessary. ("v4", "v4"), ("v3", "v3"), ("v6e", "v6e"), ("v5litepod", "v5litepod"), ("v7x", "v7x"), ], ) def test_get_tpu_version_valid(accelerator_type, expected_version): assert ray.util.tpu.get_tpu_version_from_type(accelerator_type) == expected_version @pytest.mark.parametrize( "invalid_type", [ "A100", # GPU type "random-invalid-type", # Random string "TPU-invalid", # TPU prefix "", # Empty string ], ) def test_get_tpu_version_invalid(invalid_type): with pytest.raises(ValueError, match="Invalid accelerator_type"): ray.util.tpu.get_tpu_version_from_type(invalid_type) @pytest.mark.parametrize( "topology, accelerator_type, num_workers, resources_per_worker, expected_slices", [ # "2x2x1" has 4 chips, for 4 workers with TPU: 1 each we expect num_slices=1. ("2x2x1", "TPU-V4", 4, {"TPU": 1}, 1), # "2x2x1" has 4 chips, for 8 workers with TPU: 1 each we expect num_slices=2. ("2x2x1", "v4", 8, {"TPU": 1}, 2), # "2x2x2" has 8 chips and 2 hosts, defaulting to 1 TPU worker per host # and requesting 4 workers, we expect num_slices=2. ("2x2x2", "TPU-V4", 4, None, 2), # "2x2x4" has 16 chips and 4 hosts, defaulting to 1 TPU worker per host # and requesting 4 workers, we expect num_slices=1. ("2x2x4", "TPU-V4", 4, None, 1), # 0 workers requested -> fallback to 1 slice. ("2x2x1", "v4", 0, None, 1), # Invalid topology -> fallback to 1 slice. ("", "v4", 4, {"TPU": 1}, 1), ("2x2x1", "", 4, {"TPU": 1}, 1), ], ) def test_get_tpu_num_slices_for_workers( topology, accelerator_type, num_workers, resources_per_worker, expected_slices ): num_slices = ray.util.tpu.get_tpu_num_slices_for_workers( topology=topology, accelerator_type=accelerator_type, num_workers=num_workers, resources_per_worker=resources_per_worker, ) assert num_slices == expected_slices def _make_mock_tpu_node( alive, pod_type, slice_name, worker_id, tpu_chips=4, node_id=None ): """Helper to mock a Ray Node dictionary returned by ray.nodes().""" if node_id is None: node_id = f"node_{slice_name}_{worker_id}" return { "NodeID": node_id, "Alive": alive, "Labels": { "ray.io/tpu-pod-type": pod_type, "ray.io/tpu-slice-name": slice_name, "ray.io/tpu-worker-id": str(worker_id), }, "Resources": {"TPU": tpu_chips}, } @pytest.mark.parametrize( "topology, accelerator_type, mock_nodes, mock_avail_resources, expected_ready", [ # 1 fully intact and available v4 slice (2 physical hosts). ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-1", 0, node_id="A"), _make_mock_tpu_node(True, "v4-16", "slice-1", 1, node_id="B"), ], { "A": {"TPU": 4}, "B": {"TPU": 4}, }, 1, ), # 1 fully intact slice, but one node is using 2 TPUs (unavailable) -> 0 ready slices. ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-1", 0, node_id="A"), _make_mock_tpu_node(True, "v4-16", "slice-1", 1, node_id="B"), ], { "A": {"TPU": 2}, "B": {"TPU": 4}, }, 0, ), # Fractured slice (missing a physical host) -> 0 ready slices. ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-1", 0, node_id="A"), # Worker 1 is missing ], { "A": {"TPU": 4}, }, 0, ), # Correct number of hosts, but missing the head node (rank 0) -> 0 ready slices. ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-1", 1, node_id="A"), _make_mock_tpu_node(True, "v4-16", "slice-1", 2, node_id="B"), ], { "A": {"TPU": 4}, "B": {"TPU": 4}, }, 0, ), # Fractured slice (one physical host is dead) -> 0 ready slices. ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-1", 0, node_id="A"), _make_mock_tpu_node(False, "v4-16", "slice-1", 1, node_id="B"), ], { "A": {"TPU": 4}, }, 0, ), # 2 slices total: one intact & available, one fractured -> 1 ready slice. ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-A", 0, node_id="A0"), _make_mock_tpu_node(True, "v4-16", "slice-A", 1, node_id="A1"), _make_mock_tpu_node(True, "v4-16", "slice-B", 0, node_id="B0"), # slice-B worker 1 is missing ], { "A0": {"TPU": 4}, "A1": {"TPU": 4}, "B0": {"TPU": 4}, }, 1, ), # 1 fully intact and available v6e 2x4 slice (single-host). ( "2x4", "v6e", [ _make_mock_tpu_node( True, "v6e-8", "slice-1", 0, tpu_chips=8, node_id="A" ), ], { "A": {"TPU": 8}, }, 1, ), # 1 fully intact and available v6e 2x4 slice (2 physical hosts). ( "2x4", "v6e", [ _make_mock_tpu_node( True, "v6e-8", "slice-1", 0, tpu_chips=4, node_id="A" ), _make_mock_tpu_node( True, "v6e-8", "slice-1", 1, tpu_chips=4, node_id="B" ), ], { "A": {"TPU": 4}, "B": {"TPU": 4}, }, 1, ), # 2 fully intact v6e slices. ( "4x4", "v6e", [ _make_mock_tpu_node(True, "v6e-16", "slice-1", 0, node_id="S1_0"), _make_mock_tpu_node(True, "v6e-16", "slice-1", 1, node_id="S1_1"), _make_mock_tpu_node(True, "v6e-16", "slice-1", 2, node_id="S1_2"), _make_mock_tpu_node(True, "v6e-16", "slice-1", 3, node_id="S1_3"), _make_mock_tpu_node(True, "v6e-16", "slice-2", 0, node_id="S2_0"), _make_mock_tpu_node(True, "v6e-16", "slice-2", 1, node_id="S2_1"), _make_mock_tpu_node(True, "v6e-16", "slice-2", 2, node_id="S2_2"), _make_mock_tpu_node(True, "v6e-16", "slice-2", 3, node_id="S2_3"), ], { "S1_0": {"TPU": 4}, "S1_1": {"TPU": 4}, "S1_2": {"TPU": 4}, "S1_3": {"TPU": 4}, "S2_0": {"TPU": 4}, "S2_1": {"TPU": 4}, "S2_2": {"TPU": 4}, "S2_3": {"TPU": 4}, }, 2, ), ], ) @patch("ray.is_initialized", return_value=True) @patch("ray._private.state.available_resources_per_node") @patch("ray.nodes") def test_get_num_ready_tpu_slices_calculation( mock_nodes_call, mock_avail_resources_call, mock_is_initialized, topology, accelerator_type, mock_nodes, mock_avail_resources, expected_ready, ): """Test that the TPU slice readiness utility correctly calculates the number of ready slices in different mocked scenarios, including idle resource verification.""" mock_nodes_call.return_value = mock_nodes mock_avail_resources_call.return_value = mock_avail_resources actual_ready = ray.util.tpu.get_num_ready_tpu_slices( topology=topology, accelerator_type=accelerator_type, ) assert actual_ready == expected_ready @pytest.mark.parametrize( "topology, accelerator_type, mock_nodes, expected_intact", [ # 1 fully intact v4 slice (2 physical hosts). ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-1", 0, node_id="A"), _make_mock_tpu_node(True, "v4-16", "slice-1", 1, node_id="B"), ], 1, ), # Fractured slice (missing a physical host) -> 0 intact slices. ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-1", 0, node_id="A"), ], 0, ), # Missing head node (rank 0) -> 0 intact slices. ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-1", 1, node_id="A"), _make_mock_tpu_node(True, "v4-16", "slice-1", 2, node_id="B"), ], 0, ), # One physical host is dead -> 0 intact slices. ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-1", 0, node_id="A"), _make_mock_tpu_node(False, "v4-16", "slice-1", 1, node_id="B"), ], 0, ), # 2 slices: one intact, one fractured -> 1 intact slice. ( "2x2x2", "v4", [ _make_mock_tpu_node(True, "v4-16", "slice-A", 0, node_id="A0"), _make_mock_tpu_node(True, "v4-16", "slice-A", 1, node_id="A1"), _make_mock_tpu_node(True, "v4-16", "slice-B", 0, node_id="B0"), ], 1, ), # 2 fully intact v6e slices. ( "4x4", "v6e", [ _make_mock_tpu_node(True, "v6e-16", "slice-1", 0, node_id="S1_0"), _make_mock_tpu_node(True, "v6e-16", "slice-1", 1, node_id="S1_1"), _make_mock_tpu_node(True, "v6e-16", "slice-1", 2, node_id="S1_2"), _make_mock_tpu_node(True, "v6e-16", "slice-1", 3, node_id="S1_3"), _make_mock_tpu_node(True, "v6e-16", "slice-2", 0, node_id="S2_0"), _make_mock_tpu_node(True, "v6e-16", "slice-2", 1, node_id="S2_1"), _make_mock_tpu_node(True, "v6e-16", "slice-2", 2, node_id="S2_2"), _make_mock_tpu_node(True, "v6e-16", "slice-2", 3, node_id="S2_3"), ], 2, ), ], ) @patch("ray.is_initialized", return_value=True) @patch("ray.nodes") def test_get_num_tpu_slices_calculation( mock_nodes_call, mock_is_initialized, topology, accelerator_type, mock_nodes, expected_intact, ): """Test that the intact TPU slice utility counts slices based purely on physical integrity (all hosts alive, correct chip count) without checking whether they are idle.""" mock_nodes_call.return_value = mock_nodes actual_intact = ray.util.tpu.get_num_tpu_slices( topology=topology, accelerator_type=accelerator_type, ) assert actual_intact == expected_intact @patch("ray.is_initialized", return_value=False) def test_get_num_tpu_slices_uninitialized(mock_is_initialized): """Test that the utility gracefully handles an uninitialized Ray context.""" assert ray.util.tpu.get_num_tpu_slices("2x2x2", "v4") == 0 def test_get_num_ready_tpu_slices(ray_tpu_cluster): """ Tests the get_num_ready_tpu_slices utility against a real Ray cluster. The ray_tpu_cluster fixture provisions two v4-16 slices (2x2x2 topology). """ ready_slices = ray.util.tpu.get_num_ready_tpu_slices( topology="2x2x2", accelerator_type="v4" ) assert ready_slices == 2 @patch("ray.is_initialized", return_value=False) def test_get_num_ready_tpu_slices_uninitialized(mock_is_initialized): """Test that the utility gracefully handles an uninitialized Ray context.""" assert ray.util.tpu.get_num_ready_tpu_slices("2x2x2", "v4") == 0 @pytest.mark.parametrize( "node_dict, expected_slice_name", [ (_make_mock_tpu_node(True, "v4-16", "slice-1", 0), "slice-1"), (_make_mock_tpu_node(True, "v6e-8", "slice-A", 1), "slice-A"), (_make_mock_tpu_node(True, "v4-16", "", 0), ""), ({"Alive": True, "Labels": {}}, None), # Missing TPU slice name ({"Alive": True}, None), # Missing Node labels dict ], ) def test_get_tpu_slice_name_from_node(node_dict, expected_slice_name): """Tests that the utility correctly extracts the TPU slice name from a node dictionary.""" assert ray.util.tpu.get_tpu_slice_name_from_node(node_dict) == expected_slice_name @patch("ray.is_initialized", return_value=True) @patch("ray.nodes") def test_get_tpu_nodes_for_slice(mock_nodes_call, mock_is_initialized): """Tests that the utility correctly filters alive nodes for a specific slice.""" mock_nodes = [ _make_mock_tpu_node(True, "v4-16", "slice-A", 0), _make_mock_tpu_node(True, "v4-16", "slice-A", 1), _make_mock_tpu_node(False, "v4-16", "slice-A", 2), # Dead node _make_mock_tpu_node(True, "v4-16", "slice-B", 0), # Wrong slice ] mock_nodes_call.return_value = mock_nodes # Call ray.nodes() to fetch from GCS nodes_a = ray.util.tpu.get_tpu_nodes_for_slice("slice-A") assert len(nodes_a) == 2 assert nodes_a[0]["Labels"][ray._raylet.RAY_NODE_TPU_WORKER_ID_KEY] == "0" assert nodes_a[1]["Labels"][ray._raylet.RAY_NODE_TPU_WORKER_ID_KEY] == "1" assert mock_nodes_call.call_count == 1 # Pass cached nodes directly nodes_b = ray.util.tpu.get_tpu_nodes_for_slice("slice-B", nodes=mock_nodes) assert len(nodes_b) == 1 assert nodes_b[0]["Labels"][ray._raylet.RAY_NODE_TPU_SLICE_NAME_KEY] == "slice-B" assert mock_nodes_call.call_count == 1 # Call count remains 1 # Use non-existent slice nodes_c = ray.util.tpu.get_tpu_nodes_for_slice("slice-C", nodes=mock_nodes) assert len(nodes_c) == 0 @patch("ray.is_initialized", return_value=False) def test_get_tpu_nodes_for_slice_uninitialized(mock_is_initialized): """Test that the utility gracefully handles an uninitialized Ray context.""" assert ray.util.tpu.get_tpu_nodes_for_slice("slice-A") == [] def test_get_tpu_worker_resources_chips_per_vm_override(): """Test that chips_per_vm correctly overrides the default resource calculations.""" # Default behavior: v6e 2x4 defaults to a single 8-chip host num_workers, resources = ray.util.tpu.get_tpu_worker_resources( topology="2x4", accelerator_type="v6e" ) assert num_workers == 1 assert resources["TPU"] == 8 # Override behavior: v6e 2x4 forced to 4 chips per VM (2 hosts) num_workers_override, resources_override = ray.util.tpu.get_tpu_worker_resources( topology="2x4", accelerator_type="v6e", chips_per_vm=4 ) assert num_workers_override == 2 assert resources_override["TPU"] == 4 def test_slice_placement_group_chips_per_vm_override(ray_v6e_tpu_cluster): """Test that SlicePlacementGroup respects chips_per_vm for host calculation.""" # Default behavior (1 VM with 8 chips) default_pg = SlicePlacementGroup(topology="2x4", accelerator_version="v6e") assert default_pg.chips_per_host == 8 assert default_pg.num_hosts == 1 assert default_pg.num_bundles == 1 assert default_pg.bundle_resources["TPU"] == 8 # User-specified override behavior (2 VMs with 4 chips each) override_pg = SlicePlacementGroup( topology="2x4", accelerator_version="v6e", chips_per_vm=4 ) assert override_pg.chips_per_host == 4 assert override_pg.num_hosts == 2 assert override_pg.num_bundles == 2 assert override_pg.bundle_resources["TPU"] == 4 def test_user_bundle_label_selector_merged(ray_tpu_cluster): """Verifies that user-passed bundle_label_selector is merged with dynamic TPU labels.""" user_selectors = [{"env": "prod"}, {"env": "test"}] # 2x2x2 v4 = 2 hosts = 2 bundles slice_pg = SlicePlacementGroup( topology="2x2x2", accelerator_version="v4", bundle_label_selector=user_selectors ) assert len(slice_pg._bundle_label_selector) == 2 # Verify slice 0 assert slice_pg._bundle_label_selector[0]["env"] == "prod" assert ray._raylet.RAY_NODE_TPU_SLICE_NAME_KEY in slice_pg._bundle_label_selector[0] # Verify slice 1 assert slice_pg._bundle_label_selector[1]["env"] == "test" assert ray._raylet.RAY_NODE_TPU_SLICE_NAME_KEY in slice_pg._bundle_label_selector[1] def test_user_bundle_label_selector_collision_dynamic_wins(ray_v6e_tpu_cluster): """Verifies that dynamic TPU labels take precedence on collision.""" user_selectors = [{ray._raylet.RAY_NODE_TPU_SLICE_NAME_KEY: "user-requested-slice"}] # v6e-8 is single host (1 bundle) slice_pg = SlicePlacementGroup( topology="2x4", accelerator_version="v6e", bundle_label_selector=user_selectors ) assert len(slice_pg._bundle_label_selector) == 1 # The dynamic value should win (it generates test-v6e-slice-N) actual_val = slice_pg._bundle_label_selector[0][ ray._raylet.RAY_NODE_TPU_SLICE_NAME_KEY ] assert actual_val != "user-requested-slice" assert "test-v6e-slice-" in actual_val def test_user_bundle_label_selector_length_mismatch_raises(): """Verifies that providing wrong length of selector list raises ValueError.""" user_selectors = [{"env": "prod"}] # Only 1 provided but 2x2x2 v4 has 2 hosts with pytest.raises(ValueError, match="bundle_label_selector length"): SlicePlacementGroup( topology="2x2x2", accelerator_version="v4", bundle_label_selector=user_selectors, ) def test_release_head_pgs_idempotent(ray_tpu_cluster): """Verifies that release_head_pgs() is idempotent.""" slice_pg = SlicePlacementGroup(topology="2x2x2", accelerator_version="v4") assert len(slice_pg.head_placement_groups) == 1 slice_pg.release_head_pgs() assert len(slice_pg.head_placement_groups) == 0 # Call again, should not raise slice_pg.release_head_pgs() assert len(slice_pg.head_placement_groups) == 0 def test_shutdown_idempotent(ray_tpu_cluster): """Verifies that shutdown() is idempotent.""" slice_pg = SlicePlacementGroup(topology="2x2x2", accelerator_version="v4") slice_pg.shutdown() assert slice_pg.placement_group is None assert len(slice_pg.head_placement_groups) == 0 # Call again, should not raise slice_pg.shutdown() def test_shutdown_safe_after_construction_failure(): """Verifies that shutdown() is safe to call on a partially-constructed instance.""" with patch( "ray.util.tpu.SlicePlacementGroup._reserve_slice", side_effect=RuntimeError("Test failure"), ): with pytest.raises(RuntimeError, match="Test failure"): SlicePlacementGroup(topology="2x2x2", accelerator_version="v4") # If the above didn't crash or leak resources, we are good. # We can also manually construct a partial instance and call shutdown. partial_pg = SlicePlacementGroup.__new__(SlicePlacementGroup) partial_pg._head_pgs = [] partial_pg._placement_group = None # Should not raise even though it's missing attributes partial_pg.shutdown() def test_release_head_pgs_after_ready_then_shutdown(ray_tpu_cluster): """Validates Slice PG lifecycle: wait until ready, release head PGs, then shutdown.""" slice_pg = SlicePlacementGroup(topology="2x2x2", accelerator_version="v4") # Wait for ready ray.get(slice_pg.placement_group.ready()) slice_pg.release_head_pgs() assert len(slice_pg.head_placement_groups) == 0 slice_pg.shutdown() assert slice_pg.placement_group is None def test_chips_per_vm_zero_raises_value_error(): """Verifies that passing chips_per_vm=0 explicitly raises a ValueError instead of silently using the topology default.""" with pytest.raises(ValueError): SlicePlacementGroup( topology="2x2x2", accelerator_version="v4", chips_per_vm=0, ) # Also verify when custom resources already include a "TPU" key with pytest.raises(ValueError): SlicePlacementGroup( topology="2x2x2", accelerator_version="v4", resources_per_bundle={"TPU": 4}, chips_per_vm=0, ) if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))