fabric_ring.py 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. """NCCL and arx settings for a rank that mentat placed on a ring of boxes.
  2. Runs at interpreter start (fabric_ring.pth) and does nothing unless mentat
  3. gave this process MENTAT_FABRIC_LAYOUT=ring, which it does for each rank of a
  4. TP=RING4 claim. On a ring each box cables one port to the previous rank and
  5. the other to the next, so a rank sends to next and receives from prev on
  6. different ports, and the diagonal ranks share no cable. This sets:
  7. - NCCL_IB_HCA to both PCIe roots' functions of both ports, NCCL_ALGO=Ring so
  8. NCCL only talks to neighbours, and NCCL_GRAPH_FILE to a ring whose channels
  9. receive on the ports toward prev and send on the ports toward next. NCCL
  10. cannot infer that wiring, since it assumes every NIC reaches every peer.
  11. - the GID from each port's own address, since each cable has its own subnet
  12. and no one GID index fits every device.
  13. - VLLM_ARX_RING, ARX_RING_PREV_HCAS and ARX_RING_NEXT_HCAS for arx.
  14. NCCL reads its environment on first use, so this must run before vLLM
  15. imports it.
  16. """
  17. import os
  18. def _port_devices(iface: str) -> list[str]:
  19. """RDMA devices behind the port that carries iface, one per PCIe root.
  20. Each ConnectX-7 port is two PCI functions on two roots with the same
  21. bus:device.function (0000:01:00.1 and 0002:01:00.1), and each function
  22. has its own RDMA device. Sorted by PCI address, which is also the order
  23. libibverbs and NCCL list them in.
  24. """
  25. pci = os.path.basename(os.path.realpath(f"/sys/class/net/{iface}/device"))
  26. bdf = pci.split(":", 1)[1]
  27. devs = []
  28. for fn in sorted(os.listdir("/sys/bus/pci/devices")):
  29. if fn.split(":", 1)[1] != bdf:
  30. continue
  31. ib = f"/sys/bus/pci/devices/{fn}/infiniband"
  32. if os.path.isdir(ib):
  33. devs += [(fn, d) for d in os.listdir(ib)]
  34. return [d for _, d in sorted(devs)]
  35. def _graph_xml(nccl_index: dict[str, int], prev: list[str], nxt: list[str], nchannels: int) -> str:
  36. """A ring graph for one GPU per node: channel c receives on prev[c % 2]
  37. and sends on next[c % 2], alternating PCIe roots across channels."""
  38. chans = "".join(
  39. f'<channel><net dev="{nccl_index[prev[c % 2]]}"/><gpu dev="0"/>'
  40. f'<net dev="{nccl_index[nxt[c % 2]]}"/></channel>'
  41. for c in range(nchannels)
  42. )
  43. # The speeds and path types are what NCCL computes for its own ring on
  44. # these boxes. It picks protocols and chunk sizes from them, which set
  45. # the order its reductions sum in.
  46. return (
  47. '<graphs version="1">'
  48. f'<graph id="0" pattern="4" crossnic="1" nchannels="{nchannels}" speedintra="0.24" '
  49. 'speedinter="0.24" latencyinter="0" typeintra="LOC" typeinter="P2C" samechannels="1">'
  50. f"{chans}</graph></graphs>\n"
  51. )
  52. def _setup() -> None:
  53. if os.environ.get("MENTAT_FABRIC_LAYOUT") != "ring":
  54. return
  55. prev = _port_devices(os.environ["MENTAT_FABRIC_PREV_IFACE"])
  56. nxt = _port_devices(os.environ["MENTAT_FABRIC_NEXT_IFACE"])
  57. if len(prev) != 2 or len(nxt) != 2:
  58. raise RuntimeError(f"fabric_ring: expected two RDMA devices per port, got prev={prev} next={nxt}")
  59. # On a switch both "ports" can be the same one; list each device once.
  60. devs = sorted(set(prev + nxt), key=lambda d: os.path.realpath(f"/sys/class/infiniband/{d}/device"))
  61. index = {d: i for i, d in enumerate(devs)}
  62. nchannels = int(os.environ.get("NCCL_MAX_NCHANNELS") or 8)
  63. path = f"/tmp/nccl-ring-graph.{os.getpid()}.xml"
  64. with open(path, "w") as f:
  65. f.write(_graph_xml(index, prev, nxt, nchannels))
  66. os.environ.update({
  67. "NCCL_IB_HCA": "=" + ",".join(devs),
  68. "NCCL_IB_MERGE_NICS": "0",
  69. "NCCL_ALGO": "Ring",
  70. "NCCL_CROSS_NIC": "1",
  71. "NCCL_GRAPH_FILE": path,
  72. "NCCL_IB_ADDR_FAMILY": "AF_INET",
  73. "NCCL_IB_ROCE_VERSION_NUM": "2",
  74. "VLLM_ARX_RING": "1",
  75. "ARX_RING_PREV_HCAS": ",".join(prev),
  76. "ARX_RING_NEXT_HCAS": ",".join(nxt),
  77. })
  78. os.environ.pop("NCCL_IB_GID_INDEX", None)
  79. _setup()