| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- #!/usr/bin/env python3
- """Plan the hardware-forwarded opposite-node paths that let RoCEnante run on a switchless ring.
- python3 scripts/ring_mesh/plan.py --sparkring ~/sparkring --out ring-mesh HEAD W1 W2 W3
- HEAD W1 W2 W3 are SSH names of the four Sparks in tensor-parallel rank order (the head, then
- WORKER_HOSTS). Each is inventoried over SSH with inventory.sh; the cabling is read from the
- fabric subnets (each DAC is its own /24), the ring is numbered the way sparkring's planner
- requires (physical port f0 clockwise), and FujitsuPolycom/sparkring's own planner
- (spark_transport/fabric/cx7_hairpin_diagonal/fabric.py, tested at f16b5f4) builds the RoCEnante
- selection: per rank two /32 routes to the opposite node via a neighbour, two hardware-only tc
- redirect rules for the traffic it forwards, and two source markers. Writes into --out:
- fabric.json sparkring topology document (the plan's input)
- mesh-up-<host>.sh idempotent apply for that host (routes, clsact, rules, markers)
- mesh-down-<host>.sh removes exactly what mesh-up created
- env.txt the EXTRA_CONTAINER_ENV additions, with the per-rank peer maps
- translated from sparkring's ring numbering to the TP rank order
- Nothing on the hosts is changed. See docs/switchless-ring.md for installation.
- """
- import argparse
- import ipaddress
- import json
- import shlex
- import subprocess
- import sys
- from pathlib import Path
- HERE = Path(__file__).resolve().parent
- # sparkring's source-bound map: physical port f0 is the clockwise direction, function 0 is the
- # first PCIe domain (rocep1s0f*) and function 1 the second (roceP2p1s0f*).
- PORTS = {
- ("clockwise", 0): "enp1s0f0np0",
- ("clockwise", 1): "enP2p1s0f0np0",
- ("counter_clockwise", 0): "enp1s0f1np1",
- ("counter_clockwise", 1): "enP2p1s0f1np1",
- }
- HCAS = ("rocep1s0f0", "rocep1s0f1", "roceP2p1s0f0", "roceP2p1s0f1") # B12X_ROCE_HCA order
- def inventory(host):
- out = subprocess.run(["ssh", "-o", "BatchMode=yes", host, "bash -s"], input=(HERE / "inventory.sh").read_bytes(),
- capture_output=True, timeout=60, check=True).stdout
- return json.loads(out.decode())
- def check_host(host, inv):
- """Problems that stop the mesh from working on this host (empty list when fine)."""
- bad = []
- for nd, p in inv["ports"].items():
- if p["state"] != "up" or not p["ipv4"]:
- bad.append(f"{host} {nd}: link {p['state']} ip {p['ipv4'] or '-'}")
- if p["mtu"] != 9000:
- bad.append(f"{host} {nd}: MTU {p['mtu']} (9000 expected)")
- if p["gid3_netdev"] != nd or not p["gid3"].endswith(_gid_tail(p["ipv4"])):
- bad.append(f"{host} {nd}: RoCEv2 GID index 3 does not map this port's IPv4")
- if p["tc_offload"] != "on":
- bad.append(f"{host} {nd}: hw-tc-offload {p['tc_offload']}")
- if p["flow_steering_mode"] != "hmfs":
- bad.append(f"{host} {nd}: flow_steering_mode {p['flow_steering_mode']} (sparkring tested hmfs)")
- return bad
- def _gid_tail(cidr):
- if not cidr:
- return "-"
- a, b, c, d = str(ipaddress.ip_interface(cidr).ip).split(".")
- return f"{int(a):02x}{int(b):02x}:{int(c):02x}{int(d):02x}"
- def ring_order(invs, hosts):
- """Hosts in sparkring ring order, starting at the TP rank 0 host: each f0 port reaches the next host's f1."""
- net = {}
- for h in hosts:
- for nd, p in invs[h]["ports"].items():
- net.setdefault(ipaddress.ip_interface(p["ipv4"]).network, []).append((h, nd))
- peer = {}
- for ends in net.values():
- if len(ends) != 2:
- raise SystemExit(f"fabric subnet shared by {len(ends)} ports, expected one DAC per /24: {ends}")
- (h1, n1), (h2, n2) = ends
- peer[(h1, n1)], peer[(h2, n2)] = (h2, n2), (h1, n1)
- order = [hosts[0]]
- while True:
- cur = order[-1]
- nxt = []
- for f in (0, 1):
- h, nd = peer.get((cur, PORTS[("clockwise", f)]), (None, None))
- if nd != PORTS[("counter_clockwise", f)]:
- raise SystemExit(f"{cur} {PORTS[('clockwise', f)]} reaches {h} {nd}; sparkring needs every f0 port "
- f"cabled to the next node's f1 port on the same PCIe domain")
- nxt.append(h)
- if nxt[0] != nxt[1]:
- raise SystemExit(f"{cur}: its two f0 functions reach different nodes {nxt}")
- if nxt[0] == hosts[0]:
- break
- order.append(nxt[0])
- if sorted(order) != sorted(hosts) or len(order) != 4:
- raise SystemExit(f"not a four-node ring: {order}")
- return order
- def topology(invs, order, state_root="/run/dsv41-mesh"):
- ranks = []
- for r, h in enumerate(order):
- ports = {}
- for d in ("clockwise", "counter_clockwise"):
- ports[d] = []
- for f in (0, 1):
- nd = PORTS[(d, f)]
- p = invs[h]["ports"][nd]
- ports[d].append({
- "function": f, "netdev": nd, "rdma_device": p["rdma"],
- "ipv4_cidr": str(ipaddress.ip_interface(p["ipv4"]).ip) + "/32", "mac": p["mac"],
- "peer_rank": (r + (1 if d == "clockwise" else -1)) % 4,
- "peer_direction": "counter_clockwise" if d == "clockwise" else "clockwise", "peer_function": f})
- ranks.append({"rank": r, "ssh_alias": h, "management_netdev": "enP7s7", "ports": ports})
- return {"schema": "sparkring-cx7-hardware-diagonal-fabric/v1", "status": "research-only", "group_id": 77,
- "socket_direct_functions": 2, "flow_label_base": 16383, "shared_diagonal_flow_label": True,
- "endpoint_route_strategy": "adjacent_gateway", "standard_ether_type": 2048, "marked_ether_type": 34997,
- "standard_udp_destination_port": 4791, "roce_gid_index": 3, "bounded_runtime_seconds": 7200,
- "expected_ethernet_mtu": 9000, "expected_roce_mtu": 4096,
- "orchestration": {"host_helper_path": "/UNSUPPORTED/external-fabric-orchestrator",
- "remote_state_root": state_root},
- "ranks": ranks}
- def peer_maps(native_args, order, hosts):
- """B12X_ROCE_PEER_HCA_MAPS for the TP rank order.
- native_args is sparkring's rocenante_native_path_arguments(plan): per ring rank, entries
- "dest,function,rdma_device,gid_index,hops". Path i of a peer is its function-i path, the same
- physical path at both ends, which is what the proxy pairs (local path i with the peer's path i).
- """
- tp_of = {h: hosts.index(h) for h in hosts}
- maps = [None] * 4
- for ring_rank, specs in native_args.items():
- paths = {}
- for spec in specs:
- dest, function, rdma, _gid, _hops = spec.split(",")
- paths.setdefault(tp_of[order[int(dest)]], {})[int(function)] = HCAS.index(rdma)
- maps[tp_of[order[int(ring_rank)]]] = ",".join(f"{p}={v[0]}/{v[1]}" for p, v in sorted(paths.items()))
- return ";".join(maps)
- def host_scripts(plan, fabric, host_rank):
- """(mesh-up, mesh-down) for one host from sparkring's RoCEnante plan."""
- up = ["#!/usr/bin/env bash",
- "# Hardware-forwarded opposite-node paths for RoCEnante (generated by scripts/ring_mesh/plan.py). Idempotent.",
- "set -euo pipefail"]
- down = ["#!/usr/bin/env bash", "# Removes exactly what mesh-up.sh creates.", "set -uo pipefail",
- "systemctl stop 'dsv41-mesh-marker@*' 2>/dev/null || true"]
- rules = [x for x in plan.tc_rules if x.intermediate_rank == host_rank]
- qdiscs = sorted({x.ingress_netdev for x in rules})
- for q in qdiscs:
- up.append(f"tc qdisc show dev {q} | grep -q clsact || tc qdisc add dev {q} clsact")
- for x in rules:
- add = fabric.tc_rule_command(x, add=True)
- up.append(f"tc filter show dev {x.ingress_netdev} ingress pref {x.preference} | grep -q flower || {shlex.join(add)}")
- up.append(f"tc -s filter show dev {x.ingress_netdev} ingress pref {x.preference} | grep -q in_hw "
- f"|| {{ echo 'rule pref {x.preference} on {x.ingress_netdev} is not in hardware'; exit 3; }}")
- down.append(f"tc filter del dev {x.ingress_netdev} ingress pref {x.preference} protocol 0x88b5 "
- f"handle {x.handle} flower 2>/dev/null || true")
- for q in qdiscs:
- down.append(f'[ -z "$(tc filter show dev {q} ingress; tc filter show dev {q} egress)" ] '
- f"&& tc qdisc del dev {q} clsact 2>/dev/null || true")
- for r in (x for x in plan.routes if x.source_rank == host_rank):
- up.append(f"ip route show exact {r.destination_ipv4}/32 | grep -q . || {shlex.join(fabric.route_command(r, add=True))}")
- down.append(f"ip route del {r.destination_ipv4}/32 2>/dev/null || true")
- devs = sorted({m.rdma_device for m in plan.markers if m.source_rank == host_rank})
- ports = sorted({m.udp_source_port for m in plan.markers})
- if ports != [65535]:
- raise SystemExit(f"unexpected marker UDP source ports {ports}; the marker unit assumes 65535")
- up.append(f"for d in {' '.join(devs)}; do systemctl start dsv41-mesh-marker@$d; done")
- up.append(f"sleep 2; for d in {' '.join(devs)}; do systemctl is-active --quiet dsv41-mesh-marker@$d "
- f"|| {{ echo marker $d not active; exit 4; }}; done")
- up.append("echo mesh-up OK")
- down.append("echo mesh-down done")
- return "\n".join(up) + "\n", "\n".join(down) + "\n"
- def main():
- ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
- ap.add_argument("hosts", nargs=4, help="SSH names in TP rank order (head first)")
- ap.add_argument("--sparkring", required=True, help="FujitsuPolycom/sparkring checkout (tested at f16b5f4)")
- ap.add_argument("--out", default="ring-mesh")
- a = ap.parse_args()
- sys.path.insert(0, str(Path(a.sparkring) / "spark_transport/fabric/cx7_hairpin_diagonal"))
- import fabric # sparkring's planner
- invs = {h: inventory(h) for h in a.hosts}
- problems = [p for h in a.hosts for p in check_host(h, invs[h])]
- if problems:
- raise SystemExit("hosts not ready for the mesh:\n " + "\n ".join(problems))
- order = ring_order(invs, a.hosts)
- out = Path(a.out)
- out.mkdir(parents=True, exist_ok=True)
- (out / "fabric.json").write_text(json.dumps(topology(invs, order), indent=2) + "\n", newline="\n")
- plan = fabric.build_rocenante_plan(fabric.build_plan(fabric.load_topology(out / "fabric.json")))
- for r, h in enumerate(order):
- up, down = host_scripts(plan, fabric, r)
- (out / f"mesh-up-{h}.sh").write_text(up, newline="\n")
- (out / f"mesh-down-{h}.sh").write_text(down, newline="\n")
- hairpin = min(int(p["hairpin_queue_size"] or 0) for h in a.hosts for p in invs[h]["ports"].values())
- cap = 262144 if hairpin >= 8192 else 81920
- maps = peer_maps(fabric.rocenante_native_path_arguments(plan), order, list(a.hosts))
- env = (f"DSV41_ROCE_RING=1 SGLANG_ROCE_ALLREDUCE=1 SGLANG_ROCE_MAX_SIZE={cap} DSV41_ROCE_GATHER={cap} "
- f"B12X_ROCE_HCA={','.join(HCAS)} B12X_ROCE_PEER_HCA_MAPS={maps} B12X_ROCE_TWO_WAVE_THRESHOLD_BYTES=0")
- (out / "env.txt").write_text(env + "\n", newline="\n")
- print(f"ring order (sparkring numbering): {' -> '.join(order)} -> {order[0]}")
- print(f"hairpin_queue_size (smallest): {hairpin} -> RoCE size cap {cap} bytes")
- print(f"wrote {out}/fabric.json, mesh-up-*.sh, mesh-down-*.sh, env.txt\nEXTRA_CONTAINER_ENV additions:\n {env}")
- if __name__ == "__main__":
- main()
|