|
| 1 | +# (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. |
| 2 | + |
| 3 | +# pyre-unsafe |
| 4 | + |
| 5 | +"""GPU-free interface tests for the composable send/recv framework. |
| 6 | +
|
| 7 | +Asserts the *contract* (types, signatures, transport abstraction, reserved |
| 8 | +stubs) without compiling or launching any kernel, so this runs in CI with no |
| 9 | +GPU. The real end-to-end path is exercised by ``test_minimal_sendrecv``. |
| 10 | +""" |
| 11 | + |
| 12 | +import dataclasses |
| 13 | +import inspect |
| 14 | +import unittest |
| 15 | +from typing import Any, cast |
| 16 | + |
| 17 | +from torch.utils._triton import has_triton |
| 18 | + |
| 19 | + |
| 20 | +class ContractTest(unittest.TestCase): |
| 21 | + def test_ctx_fields(self) -> None: |
| 22 | + from comms.dsl import Ctx |
| 23 | + |
| 24 | + names = {f.name for f in dataclasses.fields(Ctx)} |
| 25 | + for expected in ( |
| 26 | + "in_ptr", |
| 27 | + "out_ptr", |
| 28 | + "region_ptr", |
| 29 | + "tile_idx", |
| 30 | + "block_id", |
| 31 | + "flat_tid", |
| 32 | + "num_blocks", |
| 33 | + "peer", |
| 34 | + "recv_peer", |
| 35 | + "shard_idx", |
| 36 | + "in_strides", |
| 37 | + "out_strides", |
| 38 | + "extra", |
| 39 | + ): |
| 40 | + self.assertIn(expected, names) |
| 41 | + # Construct + extension point works. |
| 42 | + ctx = Ctx(in_ptr=1, extra={"scales": 7}) |
| 43 | + self.assertEqual(ctx.in_ptr, 1) |
| 44 | + self.assertEqual(ctx.extra["scales"], 7) |
| 45 | + |
| 46 | + def test_hook_aliases_importable(self) -> None: |
| 47 | + from comms.dsl import ConsumeFn, ProduceFn |
| 48 | + |
| 49 | + self.assertIsNotNone(ProduceFn) |
| 50 | + self.assertIsNotNone(ConsumeFn) |
| 51 | + |
| 52 | + def test_exports(self) -> None: |
| 53 | + import comms.dsl as fw |
| 54 | + |
| 55 | + for name in ( |
| 56 | + "Ctx", |
| 57 | + "NvlTransport", |
| 58 | + "nvl_rendezvous", |
| 59 | + "MeshTransport", |
| 60 | + "LinkKind", |
| 61 | + ): |
| 62 | + self.assertIn(name, fw.__all__) |
| 63 | + |
| 64 | + |
| 65 | +class TransportTest(unittest.TestCase): |
| 66 | + def test_rendezvous_signature(self) -> None: |
| 67 | + from comms.dsl import nvl_rendezvous |
| 68 | + |
| 69 | + params = list(inspect.signature(nvl_rendezvous).parameters) |
| 70 | + for expected in ("group", "device", "per_peer_bytes"): |
| 71 | + self.assertIn(expected, params) |
| 72 | + |
| 73 | + def test_nvl_link_kind(self) -> None: |
| 74 | + from comms.dsl import LinkKind, NvlTransport |
| 75 | + |
| 76 | + # handle is not touched by link_kind, so we can build this GPU-free. |
| 77 | + t = NvlTransport( |
| 78 | + handle=cast(Any, None), world_size=4, local_rank=0, per_peer_bytes=1024 |
| 79 | + ) |
| 80 | + self.assertIs(t.link_kind(1), LinkKind.NVLINK) |
| 81 | + |
| 82 | + def test_check_transfer_guards(self) -> None: |
| 83 | + import torch |
| 84 | + from comms.dsl import check_transfer, NvlTransport |
| 85 | + |
| 86 | + # per_peer_bytes=4096, fp32 -> 1024 elems per peer; 4 signal slots. |
| 87 | + t = NvlTransport( |
| 88 | + handle=cast(Any, None), |
| 89 | + world_size=2, |
| 90 | + local_rank=0, |
| 91 | + per_peer_bytes=4096, |
| 92 | + max_blocks_per_peer=4, |
| 93 | + ) |
| 94 | + # Valid transfer: no raise. |
| 95 | + check_transfer(t, numel=512, dtype=torch.float32, num_blocks=2) |
| 96 | + # numel exceeds per-peer capacity -> raise (would corrupt the next peer). |
| 97 | + with self.assertRaises(ValueError): |
| 98 | + check_transfer(t, numel=2000, dtype=torch.float32, num_blocks=2) |
| 99 | + # num_blocks exceeds signal slots -> raise (would OOB-write the pad). |
| 100 | + with self.assertRaises(ValueError): |
| 101 | + check_transfer(t, numel=512, dtype=torch.float32, num_blocks=8) |
| 102 | + # num_blocks must be >= 1. |
| 103 | + with self.assertRaises(ValueError): |
| 104 | + check_transfer(t, numel=512, dtype=torch.float32, num_blocks=0) |
| 105 | + # MeshTransport delegates max_blocks_per_peer to intra, so the |
| 106 | + # num_blocks guard still fires through a mesh. |
| 107 | + from comms.dsl import MeshTransport |
| 108 | + |
| 109 | + mesh = MeshTransport(intra=t) |
| 110 | + check_transfer(mesh, numel=512, dtype=torch.float32, num_blocks=2) |
| 111 | + with self.assertRaises(ValueError): |
| 112 | + check_transfer(mesh, numel=512, dtype=torch.float32, num_blocks=8) |
| 113 | + |
| 114 | + def test_ib_reserved(self) -> None: |
| 115 | + from comms.dsl import ib_rendezvous, IbTransport |
| 116 | + |
| 117 | + ib = IbTransport(world_size=8, per_peer_bytes=1024) |
| 118 | + with self.assertRaises(NotImplementedError): |
| 119 | + ib.endpoint(1, dtype=cast(Any, None)) |
| 120 | + with self.assertRaises(NotImplementedError): |
| 121 | + ib_rendezvous(cast(Any, None), cast(Any, None), 1024) |
| 122 | + |
| 123 | + def test_mesh_routes_by_domain(self) -> None: |
| 124 | + from comms.dsl import IbTransport, LinkKind, MeshTransport, NvlTransport |
| 125 | + |
| 126 | + intra = NvlTransport( |
| 127 | + handle=cast(Any, None), world_size=8, local_rank=0, per_peer_bytes=1024 |
| 128 | + ) |
| 129 | + # No inter transport yet -> everything is NVLINK. |
| 130 | + mesh = MeshTransport(intra=intra) |
| 131 | + self.assertIs(mesh.link_kind(3), LinkKind.NVLINK) |
| 132 | + |
| 133 | + # With an inter transport + domain size 4: peers 0-3 NVLINK, 4-7 IB. |
| 134 | + mesh2 = MeshTransport( |
| 135 | + intra=intra, |
| 136 | + inter=IbTransport(world_size=8, per_peer_bytes=1024), |
| 137 | + local_domain_size=4, |
| 138 | + ) |
| 139 | + self.assertIs(mesh2.link_kind(2), LinkKind.NVLINK) |
| 140 | + self.assertIs(mesh2.link_kind(5), LinkKind.IB) |
| 141 | + |
| 142 | + |
| 143 | +@unittest.skipUnless(has_triton(), "Triton not available") |
| 144 | +class TritonInterfaceTest(unittest.TestCase): |
| 145 | + def _arg_names(self, jit_fn: object) -> list[str]: |
| 146 | + names = getattr(jit_fn, "arg_names", None) |
| 147 | + if names is not None: |
| 148 | + return list(names) |
| 149 | + return list(inspect.signature(getattr(jit_fn, "fn", jit_fn)).parameters) |
| 150 | + |
| 151 | + def test_send_recv_are_jit(self) -> None: |
| 152 | + from comms.dsl.triton import recv_tiles as recv, send_tiles as send |
| 153 | + from triton.runtime.jit import JITFunction |
| 154 | + |
| 155 | + self.assertIsInstance(send, JITFunction) |
| 156 | + self.assertIsInstance(recv, JITFunction) |
| 157 | + for p in ("in_ptr", "produce", "put", "signal"): |
| 158 | + self.assertIn(p, self._arg_names(send)) |
| 159 | + for p in ("out_ptr", "consume", "get", "wait"): |
| 160 | + self.assertIn(p, self._arg_names(recv)) |
| 161 | + |
| 162 | + def test_nvl_and_ib_ops_present(self) -> None: |
| 163 | + from comms.dsl.triton import ib_ops, nvl_ops |
| 164 | + from triton.runtime.jit import JITFunction |
| 165 | + |
| 166 | + for ops in (nvl_ops, ib_ops): |
| 167 | + for name in ("put", "get", "signal", "wait"): |
| 168 | + self.assertIsInstance(getattr(ops, name), JITFunction) |
| 169 | + |
| 170 | + def test_hook_ctx_contract(self) -> None: |
| 171 | + # Hooks take a single Ctx aggregate; consume also gets the tile payload. |
| 172 | + from comms.dsl.triton import hooks |
| 173 | + from comms.dsl.triton.ctx import Ctx |
| 174 | + from triton.runtime.jit import JITFunction |
| 175 | + |
| 176 | + self.assertIsInstance(hooks.copy_produce, JITFunction) |
| 177 | + self.assertIsInstance(hooks.copy_consume, JITFunction) |
| 178 | + self.assertEqual(self._arg_names(hooks.copy_produce), ["ctx"]) |
| 179 | + self.assertEqual(self._arg_names(hooks.copy_consume), ["ctx", "regs"]) |
| 180 | + self.assertIsNotNone(Ctx) |
| 181 | + |
| 182 | + |
| 183 | +class CuteInterfaceTest(unittest.TestCase): |
| 184 | + def test_cute_stubs_reserved(self) -> None: |
| 185 | + from comms.dsl.cute import recv, send |
| 186 | + |
| 187 | + with self.assertRaises(NotImplementedError): |
| 188 | + send(None, None, 1) |
| 189 | + with self.assertRaises(NotImplementedError): |
| 190 | + recv(None, None, 1) |
0 commit comments