test_roce_ring.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. #!/usr/bin/env python3
  2. """CPU checks for RoCEnante on a switchless ring (b12x.comm.roce_ring + scripts/ring_mesh/plan.py).
  3. - B12X_ROCE_PEER_HCA_MAPS: one ';'-separated map per rank is picked by rank, B12X_ROCE_PEER_HCA_MAP
  4. still wins, a wrong count refuses.
  5. - plan.py orients the ring from the fabric subnets (sparkring numbers it with physical port f0
  6. clockwise), refuses a cable that breaks that contract, and translates sparkring's per-rank
  7. paths into peer maps for the TP rank order. The fixtures are a real four-Spark ring whose TP
  8. order runs the other way round the cables (TP 0,1,2,3 = ring 0,3,2,1).
  9. - every map pairs path i at both ends on the same PCIe domain, and a neighbour's paths cross the
  10. cable (f0 at one end, f1 at the other).
  11. - in the image: the RoCEnante overlay selects the package from DSV41_ROCE_RING.
  12. No GPU, torch or network. usage: python3 tests/test_roce_ring.py
  13. """
  14. import importlib.util
  15. import os
  16. import unittest
  17. from pathlib import Path
  18. ROOT = Path(__file__).resolve().parents[1]
  19. def _load(name, *candidates):
  20. for c in candidates:
  21. if c.is_file():
  22. spec = importlib.util.spec_from_file_location(name, c)
  23. module = importlib.util.module_from_spec(spec)
  24. spec.loader.exec_module(module)
  25. return module
  26. raise FileNotFoundError(candidates)
  27. # the package is fetched at build (scripts/fetch_runtime.sh): in the image, or after
  28. # `scripts/fetch_runtime.sh runtime/fetched b12x` on a host
  29. paths = _load("roce_ring_paths", Path("/opt/b12x/b12x/comm/roce_ring/_path_config.py"),
  30. ROOT / "runtime/fetched/b12x/b12x/comm/roce_ring/_path_config.py")
  31. plan = _load("ring_mesh_plan", ROOT / "scripts/ring_mesh/plan.py")
  32. HOSTS = ["s1", "s2", "s3", "s4"] # TP rank order
  33. # netdev -> IPv4 per host; each DAC is its own /24, f0 of every node cabled to the f1 of the node before it in TP order
  34. ADDR = {
  35. "s1": {"enp1s0f0np0": "10.10.3.11/24", "enp1s0f1np1": "10.10.0.10/24", "enP2p1s0f0np0": "10.20.3.11/24", "enP2p1s0f1np1": "10.20.0.10/24"},
  36. "s2": {"enp1s0f0np0": "10.10.0.11/24", "enp1s0f1np1": "10.10.1.10/24", "enP2p1s0f0np0": "10.20.0.11/24", "enP2p1s0f1np1": "10.20.1.10/24"},
  37. "s3": {"enp1s0f0np0": "10.10.1.11/24", "enp1s0f1np1": "10.10.2.10/24", "enP2p1s0f0np0": "10.20.1.11/24", "enP2p1s0f1np1": "10.20.2.10/24"},
  38. "s4": {"enp1s0f0np0": "10.10.2.11/24", "enp1s0f1np1": "10.10.3.10/24", "enP2p1s0f0np0": "10.20.2.11/24", "enP2p1s0f1np1": "10.20.3.10/24"},
  39. }
  40. # sparkring rocenante_native_path_arguments() for that ring (ring rank -> "dest,function,rdma,gid,hops")
  41. NATIVE = {
  42. "0": ["1,0,rocep1s0f0,3,1", "1,1,roceP2p1s0f0,3,1", "2,0,rocep1s0f0,3,2", "2,1,roceP2p1s0f1,3,2", "3,0,rocep1s0f1,3,1", "3,1,roceP2p1s0f1,3,1"],
  43. "1": ["0,0,rocep1s0f1,3,1", "0,1,roceP2p1s0f1,3,1", "2,0,rocep1s0f0,3,1", "2,1,roceP2p1s0f0,3,1", "3,0,rocep1s0f0,3,2", "3,1,roceP2p1s0f1,3,2"],
  44. "2": ["0,0,rocep1s0f1,3,2", "0,1,roceP2p1s0f0,3,2", "1,0,rocep1s0f1,3,1", "1,1,roceP2p1s0f1,3,1", "3,0,rocep1s0f0,3,1", "3,1,roceP2p1s0f0,3,1"],
  45. "3": ["0,0,rocep1s0f0,3,1", "0,1,roceP2p1s0f0,3,1", "1,0,rocep1s0f1,3,2", "1,1,roceP2p1s0f0,3,2", "2,0,rocep1s0f1,3,1", "2,1,roceP2p1s0f1,3,1"],
  46. }
  47. MAPS = "1=1/3,2=0/3,3=0/2;0=0/2,2=1/3,3=1/2;0=1/2,1=0/2,3=1/3;0=1/3,1=0/3,2=0/2"
  48. def inventory(addr):
  49. return {h: {"ports": {nd: {"ipv4": ip} for nd, ip in ports.items()}} for h, ports in addr.items()}
  50. class PeerMapEnv(unittest.TestCase):
  51. def setUp(self):
  52. self.saved = {k: os.environ.pop(k, None) for k in (paths.PEER_HCA_MAP_ENV, paths.PEER_HCA_MAPS_ENV)}
  53. def tearDown(self):
  54. for k, v in self.saved.items():
  55. os.environ.pop(k, None)
  56. if v is not None:
  57. os.environ[k] = v
  58. def test_per_rank_selection(self):
  59. os.environ[paths.PEER_HCA_MAPS_ENV] = MAPS
  60. self.assertEqual(paths.peer_hca_map(4, 0, 4), ((-1, -1), (1, 3), (0, 3), (0, 2)))
  61. self.assertEqual(paths.peer_hca_map(4, 3, 4), ((1, 3), (0, 3), (0, 2), (-1, -1)))
  62. def test_single_map_wins(self):
  63. os.environ[paths.PEER_HCA_MAPS_ENV] = MAPS
  64. os.environ[paths.PEER_HCA_MAP_ENV] = "1=0/2,2=0/3,3=1/3"
  65. self.assertEqual(paths.peer_hca_map(4, 0, 4)[1], (0, 2))
  66. def test_wrong_count_refuses(self):
  67. os.environ[paths.PEER_HCA_MAPS_ENV] = MAPS.rsplit(";", 1)[0]
  68. with self.assertRaises(ValueError):
  69. paths.peer_hca_map(4, 0, 4)
  70. def test_two_hca_default_unchanged(self):
  71. self.assertEqual(paths.peer_hca_map(4, 1, 2), ((0, 1), (-1, -1), (0, 1), (0, 1)))
  72. class Plan(unittest.TestCase):
  73. def test_ring_order(self):
  74. self.assertEqual(plan.ring_order(inventory(ADDR), HOSTS), ["s1", "s4", "s3", "s2"])
  75. def test_miscabled_refuses(self):
  76. bad = {h: dict(p) for h, p in ADDR.items()}
  77. bad["s2"]["enp1s0f0np0"], bad["s2"]["enp1s0f1np1"] = bad["s2"]["enp1s0f1np1"], bad["s2"]["enp1s0f0np0"]
  78. with self.assertRaises(SystemExit):
  79. plan.ring_order(inventory(bad), HOSTS)
  80. def test_peer_maps(self):
  81. order = plan.ring_order(inventory(ADDR), HOSTS)
  82. self.assertEqual(plan.peer_maps(NATIVE, order, HOSTS), MAPS)
  83. def test_maps_pair_paths_consistently(self):
  84. m = [dict((int(p), tuple(map(int, v.split("/")))) for p, v in (e.split("=") for e in r.split(","))) for r in MAPS.split(";")]
  85. domain, port = (lambda h: h // 2), (lambda h: h % 2)
  86. for r in range(4):
  87. for p in m[r]:
  88. for i in (0, 1):
  89. self.assertEqual(domain(m[r][p][i]), domain(m[p][r][i]), (r, p, i))
  90. if (p - r) % 4 in (1, 3):
  91. self.assertNotEqual(port(m[r][p][i]), port(m[p][r][i]), (r, p, i))
  92. class ImageOverlay(unittest.TestCase):
  93. PYNCCL = Path("/sgl-workspace/sglang/python/sglang/srt/distributed/device_communicators/pynccl.py")
  94. @unittest.skipUnless(PYNCCL.is_file(), "image only")
  95. def test_overlay_selects_package(self):
  96. src = self.PYNCCL.read_text()
  97. self.assertIn('"b12x.comm.roce_ring" if os.environ.get("DSV41_ROCE_RING", "0") == "1" else "b12x.comm.roce"', src)
  98. self.assertNotIn("from b12x.comm import roce\n", src)
  99. if __name__ == "__main__":
  100. unittest.main(verbosity=2)