Skip to main content

cluster-network-topology

Discover, diagram, and diagnose the RDMA/GPU network topology of a GPU cluster (rails, NICs, GID/RoCE config, GPU<->NIC PCIe affinity), determine whether the fabric supports cross-rail communication, and localize a failure to a fabric tier (NIC / leaf / spine) using only unprivileged host-side probes. Picks up where `mori check` stops: `mori check` answers "is this host configured correctly?" and drives its peer over SSH, while this answers "what is the fabric, and which tier broke?" from inside a scheduler allocation where SSH to compute nodes is unavailable. Handles IPv4-mapped AND IPv6-ULA RoCE addressing and classifies at BOTH the IP and RDMA layers — a fabric can be IP-routable cross-rail yet rail-only for RDMA. Includes the addressing-plan decode that recovers rail/leaf/pod grouping without switch access, spine enumeration that settles shared-vs-partitioned upper tiers from an unprivileged shell, a confound register for the measurement tools themselves, and an sbatch harness for multi-node tests under S

Ir a la instalación

Datos de origen

Repositorio
rocm/mori
Última actividad en el origen
9 de septiembre de 2026 a las 08:06
Idioma detectado de SKILL.md
inglés
Estrellas
179
Forks
94

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Explorador de archivos
9 archivos

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
cluster-network-topology
description
Discover, diagram, and diagnose the RDMA/GPU network topology of a GPU cluster (rails, NICs, GID/RoCE config, GPU<->NIC PCIe affinity), determine whether the fabric supports cross-rail communication, and localize a failure to a fabric tier (NIC / leaf / spine) using only unprivileged host-side probes. Picks up where `mori check` stops: `mori check` answers "is this host configured correctly?" and drives its peer over SSH, while this answers "what is the fabric, and which tier broke?" from inside a scheduler allocation where SSH to compute nodes is unavailable. Handles IPv4-mapped AND IPv6-ULA RoCE addressing and classifies at BOTH the IP and RDMA layers — a fabric can be IP-routable cross-rail yet rail-only for RDMA. Includes the addressing-plan decode that recovers rail/leaf/pod grouping without switch access, spine enumeration that settles shared-vs-partitioned upper tiers from an unprivileged shell, a confound register for the measurement tools themselves, and an sbatch harness for multi-node tests under Slurm/Spur. Use when asked to "map the cluster", "draw how NICs are connected", "cross-rail diagram", "can we enable cross-rail RDMA", "get the switch topology", or to diagnose RDMA/RoCE QP-setup failures (e.g. rc=110 ETIMEDOUT on INIT->RTR), cross-rail unreachability, rail affinity, or EP / KV-transfer connectivity problems on AMD (ionic/bnxt/mlx5) or NVIDIA (mlx5) fabrics.
# Cluster network topology Companion to `mori check` (`tools/env_check.sh`) and the `deploy-mori` skill: those answer "is this host configured correctly?", this answers "what is the fabric, and which tier broke?". The scripts referenced throughout ship beside this file — `rail_detect.sh`, `probe_topology.sh`, `xrail_worker.sh`, `xrail_worker.sbatch`, `xrail_matrix.sh`, `xrail_matrix.sbatch`, `make_diagrams.py`, `make_report.py`. `rail_detect.sh` is the shared rail-discovery library the other scripts source: it auto-detects each device's global RoCEv2 GID index and address (IPv4-mapped or IPv6 ULA/GUA, skipping `fe80::` link-local), so the tools never disagree about the same machine. Run it alone to see what a node looks like: `bash rail_detect.sh --dump`. A "rail-optimized" GPU cluster gives each GPU its own NIC, and each NIC lives on its own isolated L2/L3 domain (a "rail"). Rails may or may not be routable to each other: - **Full-mesh fabric**: any NIC can reach any NIC (cross-rail works) at both IP and RDMA. - **Rail-only fabric**: a NIC can only reach the *same* rail on other nodes; cross-rail is unroutable. Symptom: same-rail RDMA works, cross-rail QP `INIT->RTR` fails (`ibv_modify_qp` returns `ETIMEDOUT`/110, or `ping` shows 100% loss cross-rail). - **IP-routable but RDMA rail-only** (seen in the wild): cross-rail *ICMP/IP* works (the rails are routed), but cross-rail *RoCEv2 RDMA* still fails. So **do not classify from ping alone** — confirm at the RDMA layer (Step 2). The goal of this skill is to (1) discover the layout, (2) classify the fabric at both layers, (3) **localize** a failure to a tier and say what could change it, and (4) produce a diagram + report + guidance for RDMA workloads. --- ## Measurement discipline (read before running anything) Three rules, each learned by getting it wrong. They cost more time to re-do than to follow. **1. Every negative needs a positive control from the same tool, in the same job, at the same instant.** A cross-rail `FAIL` means nothing on its own — the peer might be down, the server might not have bound, the GID index might be wrong. It means something next to a same-rail `OK` from the same binary, same allocation, same minute. Sweeps (service level, DSCP/traffic class, MTU) are the usual offenders: a sweep where *every* value fails and no control was run is indistinguishable from a broken harness and carries **zero** evidential weight. Build the control into the harness, not into a follow-up run. **2. Never let one tool be the sole source of a verdict.** Tools embed assumptions in their address handles, and a fabric-shaped conclusion drawn from one binary is really a conclusion about that binary. See the confound register in Step 2c — the canonical case is `ibv_rc_pingpong`, which hardcodes `hop_limit = 1` and never uses RDMA CM. Before reporting "the fabric cannot do X", reproduce X's failure with **at least two tools that build their path differently** (one hand-built AH, one RDMA CM). **3. Capture the cheap artifacts every run, whether or not you think you need them.** `ip -6 route show table all`, `ip -6 neigh`, `ip -o addr`, `/sys/class/net/*/statistics`, and the tool inventory cost milliseconds and are the first things you will wish you had when the verdict is questioned weeks later. **`ip route` is the single most often forgotten and most load-bearing artifact** — it decides whether a rail plane is on-link (pure L2) or routed, which in turn decides whether the `hop_limit` confound is fatal or harmless. If you get one allocation, get the routes. Corollary: **record the confounds you did not close** in the report itself. A verdict with a named, dated open question is usable; a verdict that quietly rests on an unexamined assumption is a liability. --- ## Step 1 — Discover the layout Prefer the bundled `probe_topology.sh` (auto-detects NICs, GIDs incl. IPv6-ULA rails, GPUs, GPU model, same-PCI-domain affinity; emits `topo_report.<host>.txt` plus a raw `topology_<host>_node.auto.{mmd,dot}`). Run it inside the container/host that owns the devices: ```bash ./probe_topology.sh # local node PEER_IPS='<rail0_ip> <rail1_ip> ...' ./probe_topology.sh --peer <host> # + IP reachability matrix GID_INDEX=1 ./probe_topology.sh # force a RoCE GID index (else auto) ``` `--peer` needs `PEER_IPS` (the peer's addresses in rail order) because deriving them would mean guessing the site's addressing plan; without it the section says so rather than printing an empty pass. It is **IP only** — prefer `xrail_worker.sbatch`, which discovers both sides itself and tests the RDMA layer too. If doing it by hand, collect these facts per node: | Fact | Where | |---|---| | RDMA devices | `ls /sys/class/infiniband` (or `ibv_devices`) | | Port state / link layer / rate | `/sys/class/infiniband/<dev>/ports/1/{state,rate}`, `ibv_devinfo` | | **RoCE GID + type + addr** | `/sys/class/infiniband/<dev>/ports/1/gids/<i>` and `.../gid_attrs/types/<i>`. Pick the **RoCEv2 global** entry — this may be IPv4-mapped (`::ffff:AABBCCDD`, tail = rail IP) **or a global IPv6, commonly a ULA `fc00::/7`** (each rail its own `/64`). **Skip `fe80::` link-local.** Note the GID **index** (RDMA apps need it: `NCCL_IB_GID_INDEX`, `MORI_IB_GID_INDEX`). | | netdev per NIC | `.../gid_attrs/ndevs/<i>` (e.g. `enP2p0s9`) | | Rail IP (v4 or v6) | IPv4 from the `::ffff:` tail; **for IPv6 rails** read `ip -o -6 addr show <ndev> scope global`. If a rail has **no IPv4 and only a `fe80::` GID**, look again — its routable address is usually an IPv6 **ULA at a higher GID index** (this is easy to miss). | | **MTU per netdev** | `cat /sys/class/net/<ndev>/mtu`. Rails are usually jumbo (9000); a mgmt NIC usually is not. A 1500-MTU path still *works*, so this is easy to miss while it quietly taxes throughput. | | **L3 shape** | `ip -6 route show table all`, `ip -4 route show table all`, `ip route show default`, `ip -6 neigh show`. See Step 3a — this is what tells you whether a rail is on-link or routed. | | NIC PCI bus + NUMA | `readlink -f /sys/class/infiniband/<dev>/device`; `.../device/numa_node` | | **Firmware + driver** | `cat /sys/class/infiniband/<dev>/fw_ver`; `modinfo <mod>` vs `/sys/module/<mod>/version`. Compare as a **tuple**, not lexically — a larger trailing build number can hide a smaller patch number (`1.117.1-a-63` is *older* than `1.117.5-a-58`). | | GPU PCI bus + model | `rocm-smi --showbus` / `rocm-smi --showproductname` (AMD, verify the actual model — don't infer from PCI DID) or `nvidia-smi --query-gpu=index,pci.bus_id,name` (NVIDIA) | **GPU↔NIC affinity.** PCI addresses are `domain:bus:dev.func` (e.g. `0002:00:01.0`). First **isolate the rail NICs** — drop the management/front-end NIC(s): the one on the default route (`ip route show default`) and any RDMA device with no global address. This matters on **single-PCI-domain** boxes where mgmt NICs are interspersed among the rails (e.g. mlx5 `eth0`/`eth1` sitting among `rdma0..7`); if you don't drop them the pairing shifts. Then **pair within the same PCI domain by ordinal**: sort the rail NICs and the GPUs in each domain by full PCI address and zip them (k-th GPU ↔ k-th NIC). A naive "nearest bus number" instead collapses on multi-domain boxes (the bus field is `00` for everything). On a rail-optimized box the two halves land on the two NUMA nodes / PCI domains (first-half GPUs with first-half NICs, second half with second half); on single-domain boxes take the ordinal over the rails. Note whether the **mgmt NIC is cross-socket** for half the GPUs — it usually is, and it matters if you later fall back to it (Step 5). **Script portability.** The probe must run where the devices live. Two gotchas the bundled script already handles, but watch for if you hand-roll it: - **No `/dev/fd`** in some container / scheduler-step namespaces → bash **process substitution** (`while read … < <(cmd)`) fails with `/dev/fd/63: No such file`. Read from a temp file instead. - Parse the **full** PCI address (domain included), per the affinity note above. --- ## Step 2 — Classify the fabric (both layers) Pick two nodes A and B. Test **per rail**, **both layers**, and **as a full N×N matrix** rather than a couple of spot checks — the shape of the matrix is the finding. ### 2a — The N×N matrix For an N-rail node, run all N² source-rail × destination-rail combinations at both layers. What you are looking for is the *pattern*: | Matrix shape | Meaning | |---|---| | All N² pass | Full mesh. | | **Perfect diagonal** (N pass, N²−N fail) | Rail-partitioned *for that tool*. The clean diagonal is itself the positive control — it proves the harness, the GID index, the ports and the peer are all fine. | | Ragged / asymmetric | Not a fabric property. Suspect the harness, a wedged server, a per-device config difference, or a sick NIC. Re-run before interpreting. | | Diagonal fails too | Wrong GID index, wrong port, peer down. Fix this before reading anything else. | **IP layer (quick proxy).** Bind to the rail's netdev. `ping -I <ifname>` sets `SO_BINDTODEVICE`, which genuinely forces egress out that device — so a cross-rail ping success is real evidence that L3 crosses planes. **Caveat: `-I` only forces the *outbound* path.** The return path is chosen by the far node and is not observed; a success therefore proves "there is a route out and *some* route back", not "the reverse traversed the same rail". ```bash # rail-aligned: same subnet on both nodes -> expect 0% loss ping -c2 -I <ethX_railN> <B_ip4_railN> ping -6 -c2 -I <ethX_railN> <B_ip6_railN> # cross-rail: source railN device to B's railM addr (M != N) -> loss if rail-only ping -6 -c2 -I <ethX_railN> <B_ip6_railM> ``` Confirm the packets really left the rail NIC rather than leaking to the default route: ```bash cat /sys/class/net/<ndev>/statistics/tx_packets # before ping -6 -c 20 -I <ndev> <peer cross-rail addr> cat /sys/class/net/<ndev>/statistics/tx_packets # after -> must rise by ~20 ``` **RDMA layer (authoritative).** ICMP passing cross-rail does **not** prove RDMA works. Server on B, client on A, using the chosen RoCEv2 GID index (`-g` / `-x`): ```bash # same-rail: server & client both on rail 0 B$ ibv_rc_pingpong -d <dev0> -g <gid> -p 18500 # server A$ ibv_rc_pingpong -d <dev0> -g <gid> -p 18500 <B_host> # client -> expect success # cross-rail: client on rail 0, server on rail 1 B$ ibv_rc_pingpong -d <dev1> -g <gid> -p 18501 # server A$ ibv_rc_pingpong -d <dev0> -g <gid> -p 18501 <B_host> # client -> hangs/times out if rail-only ``` The out-of-band handshake goes over the management network by hostname; the RDMA path follows `-d`/`-g`. Note `ibv_rc_pingpong` **exits after one connection**, so the server side must re-listen in a loop for a matrix run; the destination rail is selected by which server port you connect to, the source rail by the client's `-d`. Classification: - Same-rail RDMA OK **and** cross-rail RDMA fails ⇒ **rail-only for RDMA** (the important case) — regardless of whether ICMP crossed. But see 2c before writing it down. - Cross-rail RDMA also OK ⇒ full-mesh. - To see the real errno when a library asserts, enable the provider's debug (e.g. AMD ionic: `IONIC_DEBUG=1 IONIC_DEBUG_FILE=/tmp/ionic_dbg`; look for `modify qp ... state 1 -> 2 rc <errno>`, where `state 1->2` = INIT->RTR). ### 2b — The paired IP-vs-RDMA control Run **both layers against the same node pair, in the same job, back to back**. This is the single highest-value experiment in the whole skill, because the interesting fabrics are the ones where the two layers disagree, and a disagreement measured hours apart on different node pairs is not a disagreement — it is two unrelated observations. Emit one row per (src rail, dst rail) with both verdicts side by side: ``` rail0 -> rail0 : RDMA OK IP OK rail0 -> rail1 : RDMA FAIL IP OK <- this row is the finding ``` ### 2c — Confound register: the tool is part of the experiment Before a matrix result becomes a fabric verdict, check what the tool did to the path: | Tool | Path construction | Hop limit | Confound | |---|---|---|---| | `ibv_rc_pingpong` | hand-built AH, TCP out-of-band | source says **`grh.hop_limit = 1`** | 1 would permit **zero** router hops, which alone could reproduce a perfect diagonal with no fabric involvement. But the kernel overwrites it from the route, so in practice it is usually *not* in force — measured: pingpong succeeds across a 3-router path. Settle it by hop count (below), not by reading the source. | | `ib_write_bw` / `ib_write_lat` (perftest) | hand-built AH, TCP out-of-band | sets a high hop limit | Not subject to the above, but still never consults RDMA CM. Good second opinion. | | `ib_write_bw -R`, `rping`, `ucmatose` | **RDMA CM** (`rdma_resolve_addr`/`rdma_resolve_route`) | from the kernel route | Uses the same resolution path a real library uses. If cross-rail works here and not with a hand-built AH, the fabric is fine and your address handle was wrong. | | `ping -I` | kernel route + `SO_BINDTODEVICE` | kernel default (64) | Egress forced, **return path unobserved**. | **Closing the hop-limit confound cheaply.** You do not need a hop-limit sweep, and a sweep is hard to run correctly anyway (a failed run leaves the peer's server mid-timeout, so the next cell fails for harness reasons and the column fills with noise). Instead **count the router hops on the path the tool already succeeded on**. If the *same-rail* run passes and `mtr`/`traceroute` shows that same-rail crosses one or more routers, then a hop limit of 1 was demonstrably never in force — whatever the source says — and it cannot explain the cross-rail failures. One `mtr` run retires the whole question. **Minimum bar for a "cross-rail is dead" verdict:** the failure reproduces under at least two tools that build the path differently, each with a same-rail positive control in the same job, and `ip route` has been captured so the on-link-vs-routed question is settled. Prefer a second hand-built-AH tool over an RDMA CM tool if the CM tools cannot be made to source-bind — CM will not pick a cross-rail path on its own anyway (see 3c). ```bash # RDMA CM re-test — cheap, one 2-node job, and it is the test most often skipped B$ rping -s -a <B rail1 addr> A$ rping -c -a <B rail1 addr> -C 10 A$ ib_write_bw -R <B rail1 addr> # and a same-rail run as the control ``` ### 2d — Running multi-node tests under a scheduler (Slurm / Spur) You need coordinated processes on **two different** nodes. Pitfalls learned the hard way: - **Interactive `srun --overlap` from the login node lands on the first node only** (even with `-w` / `--nodelist` / `-N2 -n2`), and **direct SSH to compute nodes is often blocked** (publickey-only, key not installed). So drive it from a **batch job**, whose steps *do* spread across the allocation. This is also why `mori check`, which drives its mesh over SSH, cannot be used directly on such clusters. - **Spur-like schedulers:** `srun` may be **non-blocking** (returns on dispatch, so the batch script must wait on its own sentinel files or it falls off the end and kills the workers); `srun` may **not propagate the submitter's environment** (pass knobs via files in a shared run dir); `scontrol show hostnames` may be unsupported and `--ntasks-per-node` rejected. Don't expand the nodelist — where `srun` exists, launch `srun -N2 -n2 --overlap`; either way have the workers **self-organize by `$(hostname)`** (sort the names; lower = tester A, higher = target B), which is what makes the same worker correct under both fan-out models below. The batch script may run from a **spool copy**, so use `$SLURM_SUBMIT_DIR`, not `$BASH_SOURCE`.
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub