diff --git a/AGENTS.md b/AGENTS.md
index e1ea961a..3de138b4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -52,6 +52,17 @@ Configs named `raw_rx_*` are RX-only — they initialize the RX path and wait fo
When determining throughput for a benchmark use the `mlnx_perf` utility in the background to view transmit and receive rates. Using application run time with packet counts is usually not accurate enough due to startup inconsistencies.
+### Platform profiles (Spark / IGX)
+
+The `*_spark*` sweep configs and the `run_spark_*` / `setup_spark_wire_loopback_netns.sh` scripts are **platform-parameterized**, selected by `BENCH_PLATFORM` (default `spark`):
+
+```bash
+BENCH_PLATFORM=igx ./examples/run_spark_bench.sh dpdk sweep # IGX Orin devkit
+./examples/run_spark_bench.sh dpdk sweep # DGX Spark (default)
+```
+
+`examples/bench_platform.sh` sources `examples/bench_platform_
.env` (one profile per platform) and fills `@VAR@` placeholders in the sweep config templates. A profile carries the per-platform values that differ between a GB10 Spark (unified memory → `kind: host_pinned`, isolcpus 16-19) and an IGX Orin devkit (discrete RTX 6000 Ada → `kind: device` for real GPUDirect, isolcpus 9-11, PCIe `0005:03:00.x`): memory kind, `num_bufs`, core map, DPDK port BDFs, wire-loopback netdevs/RDMA devices, and the RoCE flow-control depth cap. Add a platform by dropping in a new `bench_platform_.env`. The `daqiri_bench_socket_*_netns` configs are not templated (kernel sockets: `kind: host`, master 8, fixed netns IPs); only their per-pair cores differ, applied by the run script. Set `REPEATS=3` for publication-quality error bars.
+
## Formatting
`clang-format` is required for contributions (CONTRIBUTING.md):
@@ -112,6 +123,7 @@ The web docs live in `docs/` and are built with [MkDocs Material](https://squidf
- `docs/benchmarks/socket_benchmarking.md` — "Socket and RDMA Benchmarking" (TCP/UDP and RoCE/RDMA)
- `docs/benchmarks/raw_benchmarking.md` — "Raw Ethernet Benchmarking" (DPDK `raw_*` benches)
- `docs/benchmarks/performance-dgx-spark.md` — per-platform performance report for DGX Spark stream/protocol combinations (the long internal report lives outside the repo in `projects/daqiri-notes/`)
+ - `docs/benchmarks/performance-igx-orin.md` — the IGX Orin devkit counterpart (discrete RTX 6000 Ada, `kind: device` GPUDirect), produced by the same sweep scripts with `BENCH_PLATFORM=igx`
- `docs/stylesheets/extra.css` — custom theme overrides
**User-facing vocabulary:** the YAML schema uses `stream_type` (`raw`, `socket`, future `pcie`); for socket streams the transport is encoded in the endpoint URI scheme (`udp://`, `tcp://`, `roce://`) in `socket_config.local_addr`/`remote_addr`, **not** a separate `protocol` field. (`SocketProtocol` still exists internally, derived from the scheme.) **"Engine"** is the standard term for the specific library backing an implementation; it replaced the former "manager" and "backend" terms and is now used consistently across code (`src/engines//`, the `Engine` ABC, CMake `DAQIRI_ENGINE`), the API reference, tutorials, the landing page, and concept pages. The mapping: `stream_type: "raw"` is implemented by the `dpdk` engine; `stream_type: "socket"` with `udp://`/`tcp://` endpoints by the always-built `socket` engine; `stream_type: "socket"` with `roce://` endpoints by the `ibverbs` engine.
diff --git a/README.md b/README.md
index ca93a71e..409a6d5b 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,7 @@ Reference material for the DAQIRI codebase:
- [C++ API Usage](https://nvidia.github.io/daqiri/api-reference/cpp/) — C++ RX/TX workflows, buffer lifecycle, file writing, utilities, and status codes
- [Python API Usage](https://nvidia.github.io/daqiri/api-reference/python/) — Python bindings, workflow examples, enums, config classes, and helper functions
- [Performance: DGX Spark](https://nvidia.github.io/daqiri/benchmarks/performance-dgx-spark/) — Per-platform throughput, drop, and utilization numbers for stream/protocol combinations on DGX Spark
+- [Performance: IGX Orin](https://nvidia.github.io/daqiri/benchmarks/performance-igx-orin/) — The same sweep on an IGX Orin devkit (discrete RTX 6000 Ada, `kind: device` GPUDirect)
- [Contributing](CONTRIBUTING.md) — Contribution guidelines, coding standards, DCO sign-off
## Tutorials
diff --git a/docs/benchmarks/performance-dgx-spark.md b/docs/benchmarks/performance-dgx-spark.md
index ecabf556..5d4d1c75 100644
--- a/docs/benchmarks/performance-dgx-spark.md
+++ b/docs/benchmarks/performance-dgx-spark.md
@@ -246,6 +246,13 @@ export DAQIRI_BUILD_DIR=./build
export LD_LIBRARY_PATH=/opt/daqiri/lib:${LD_LIBRARY_PATH:-}
```
+The sweep scripts are platform-parameterized via `BENCH_PLATFORM` (default
+`spark`); these Spark numbers use the default. The same scripts run on an IGX
+Orin devkit with `BENCH_PLATFORM=igx`, which selects that platform's profile
+(`examples/bench_platform_igx.env`): a discrete GPU uses `kind: device` for real
+GPUDirect (vs Spark's `kind: host_pinned` over unified memory), with its own core
+map and PCIe addresses. See the "Platform profiles" note in `AGENTS.md`.
+
The base container does not ship the network tools the setup scripts and RoCE
baseline depend on; install them first, or
`scripts/setup_spark_wire_loopback_netns.sh` fails with `ip: command not found`:
@@ -300,4 +307,4 @@ Each run writes `bench-results/--/runs.csv`. See
per-transport details.
---
-**Previous:** [Raw Ethernet Benchmarking](raw_benchmarking.md)
+**Previous:** [Raw Ethernet Benchmarking](raw_benchmarking.md) | **Next:** [Performance: IGX Orin](performance-igx-orin.md)
diff --git a/docs/benchmarks/performance-igx-orin.md b/docs/benchmarks/performance-igx-orin.md
new file mode 100644
index 00000000..a3b2971f
--- /dev/null
+++ b/docs/benchmarks/performance-igx-orin.md
@@ -0,0 +1,360 @@
+---
+hide:
+ - navigation
+---
+
+# Performance: IGX Orin
+
+Measured C++-loopback throughput for each stream/protocol on a single IGX Orin
+devkit (discrete RTX 6000 Ada GPU), driven over a physical cabled loopback on one
+ConnectX-7. Numbers are from a Release build via `BENCH_PLATFORM=igx
+examples/run_spark_bench.sh`, 30 s per cell, mean of 3 reps.
+
+This page is the IGX-Orin counterpart to [Performance: DGX Spark](performance-dgx-spark.md);
+the two run the *same* sweep scripts, selected by `BENCH_PLATFORM`. The
+interesting differences are platform topology, not methodology: the IGX Orin
+pairs a discrete RTX 6000 Ada (so `kind: device` gives real GPUDirect, with the
+NIC DMAing straight into 49 GB of VRAM) with a weaker CPU — 12 uniform
+Cortex-A78AE cores at 1.97 GHz, only three isolated (`isolcpus=9-11`) — and a
+PCIe switch capped at a 128-byte Max Payload Size. The CPU is the headline: DGX
+Spark pins its benchmark cores onto a 3.9 GHz Cortex-X925 performance cluster, so
+each Spark bench core has roughly twice the clock and a much higher-IPC
+microarchitecture. The result: the offload-heavy bulk-transfer paths (DPDK raw,
+large RoCE messages) hit the same ~96–98 Gb/s NIC/PCIe ceiling as Spark — the NIC
+does that work, not the CPU — while the CPU-bound paths (kernel sockets,
+small-operation RoCE, small-packet DPDK) top out well below Spark in proportion
+to the per-core gap. A clean check: single-pair 1 MiB TCP runs 31.6 Gb/s on Spark
+versus 17.2 Gb/s here — a ~1.8× ratio that tracks the 3.9/1.97 GHz clock ratio
+almost exactly.
+
+For the loopback setup these numbers depend on and the per-transport
+benchmarking procedure, see [Socket and RDMA Benchmarking](socket_benchmarking.md)
+(the `dq_wire_*` network-namespace wire loopback used by RoCE and sockets) and
+[Raw Ethernet Benchmarking](raw_benchmarking.md) (the two-physical-port DPDK
+loopback). The exact commands are collected under [Reproduce](#reproduce) below.
+
+!!! note "Methodology"
+ Each cell is the mean of 3 independent 30 s reps (`REPEATS=3`). Run-to-run
+ spread is tiny on this platform — ≤0.4 Gb/s on the DPDK and RoCE sweeps,
+ ≤0.3 Gb/s on the sockets — so the tables show means without per-cell error
+ bars and call out the spread in prose instead. Every DPDK and RoCE cell was
+ drop-free; the per-core CPU-utilization figures are approximate (see the note
+ under the DPDK table).
+
+## System under test
+
+| Component | Detail |
+| --------- | ------ |
+| Platform | IGX Orin devkit, 12 cores (0–11), isolcpus `9-11` (only three isolated cores; `irqaffinity=0-8`, `nohz_full`/`rcu_nocbs` on 9–11) |
+| CPU | 12× Arm Cortex-A78AE @ 1.97 GHz (uniform, 3 clusters of 4). For contrast, DGX Spark pins bench cores on a 3.9 GHz Cortex-X925 cluster — ~2× clock and higher IPC, which is why the CPU-bound paths below trail Spark |
+| GPU | Discrete NVIDIA RTX 6000 Ada (49 GB), real GPUDirect via dmabuf → `kind: device` |
+| NIC | ConnectX-7, ports `eth0` ↔ `eth1` (`0005:03:00.0`/`.1`) cross-cabled (single-host loopback), MTU 9000 |
+| Build | Release (`-DCMAKE_BUILD_TYPE=Release`), `DAQIRI_ENGINE="dpdk ibverbs"` |
+| Loopback | Raw/DPDK uses the two physical ports directly; socket/RoCE use the `dq_wire_*` network-namespace wire loopback |
+| Core pinning | DPDK/RoCE (poll-mode) use the isolated cores: DPDK single-queue pollers 9/10, workers 0/1, master 8; multi-queue TX pollers 9/7, RX pollers 10/11 (the fourth poller borrows non-isolated core 7, since only three are isolated). Kernel sockets instead pin to **non-isolated** cores 1–4 (in the `irqaffinity=0-8` pool, normal tick/RCU, and off core 0 — the single NIC RX-softirq sink); isolated cores hurt syscall-heavy kernel networking. |
+| PCIe | Switch Max Payload Size capped at 128 B (devkit limitation), bounding bulk throughput near ~96–98 Gb/s |
+
+## Results Summary — native-shape peak (C++ loopback)
+
+Each transport at its best-case operation size. Raw/RoCE are single-stream;
+socket TCP/UDP scale (or don't) with the number of client/server pairs, so the
+four-pair aggregate is shown.
+
+| Stream / Protocol | Best case | Throughput | Drops | Notes |
+| ----------------- | --------- | ---------: | ----- | ----- |
+| Raw Ethernet / GPUDirect | 4 KB packet | **97.0 Gb/s** | 0 | 96.4 Gb/s at the 8 KB native shape; flat across batch size |
+| Socket / RoCE (SEND) | 8 MB message | **97.7 Gb/s** | 0 | Single QP, batch 1; collapses below 64 KB (see below) |
+| Socket / TCP | 8 KB × 4 pairs | **15.3 Gb/s** | ~0 | Large-message scaling capped by single-core RX softirq; kernel-TCP CPU-bound |
+| Socket / UDP | 8 KB × 4 pairs | **10.5 Gb/s** | ~0% loss | Receiver goodput; unpaced sender |
+
+Each transport is best read at its own native operation size (see the per-transport
+tables below). The headline story is the split between the two NIC-saturating
+paths (DPDK raw, large RoCE) at ~96–98 Gb/s and the CPU-bound paths (sockets,
+small-message RoCE) that the IGX Orin's smaller core complement holds far lower
+than Spark.
+
+## Raw Ethernet / GPUDirect (DPDK)
+
+Physical port-to-port loopback, GPU-resident payloads in RTX 6000 Ada VRAM
+(`kind: device` — the NIC DMAs straight into VRAM over the GPU↔NIC PCIe switch
+leg). Native 8 KB packets run at **96.4 Gb/s** drop-free across all batch sizes;
+the throughput peak is **97.0 Gb/s** at a 4 KB payload. Packet handling is
+CPU-bound on the poller cores. Throughput is flat across batch size.
+
+Achieved Gb/s measured at App RX (equal to App TX, since every cell is
+drop-free), unpaced, mean of 3 reps (run-to-run spread ≤0.4 Gb/s):
+
+
+
+
+
Payload
+
Batch size (packets per burst)
+
+
+
256
1024
4096
10240
+
+
+
+
8000 B
96.5
96.5
96.5
96.4
+
4096 B
96.7
97.0
97.0
96.6
+
1024 B
95.4
95.9
95.7
95.3
+
256 B
36.9
37.2
37.6
38.2
+
64 B
14.1
14.1
14.3
14.5
+
+
+
+At ≥1 KB the link saturates (~95–97 Gb/s) regardless of batch. Below that the
+path is packet-rate-bound: 256 B ~37.8 Gb/s (14.8 M pps), 64 B ~14.5 Gb/s
+(14.2 M pps) — a ~14–15 M pps single-queue ceiling. (Gb/s here is the L2 frame
+rate including the 64 B header, so pps ≈ Gb/s ÷ ((payload + 64) × 8).) These
+cells are flat across batch size. Because every cell is drop-free, the achieved
+rate is also the no-drop rate.
+
+Compared with DGX Spark (~98–105 Gb/s, ~20 M pps single-queue), the IGX Orin
+caps lower on both axes — the 128 B PCIe MPS limit holds the byte rate near
+~96–97 Gb/s, and the smaller cores hold the single-queue packet rate near
+~15 M pps.
+
+**CPU utilization.** Per-core busy% is unreliable to report on this platform: the
+poller cores (9–11) run with `nohz_full`/`rcu_nocbs`, whose tickless accounting
+under-counts busy time in `/proc/stat`, and the figure shifts run-to-run even at
+identical throughput. So we don't publish a precise per-core table here — the real
+evidence that the path is CPU-bound is the throughput itself: it stays flat across
+batch size (the poll-mode driver spins regardless of offered load) and the
+small-payload packet rate caps below Spark in proportion to the per-core clock
+gap. The GPU stays compute-idle (SM ~0%) — it is a DMA target for the payload, not
+a compute engine — while its memory controller shows light activity as the NIC
+writes into VRAM.
+
+### Multi-queue core scaling
+
+Unlike DGX Spark — where a second TX core lifts the native 8 KB shape from ~98 to
+~109 Gb/s — the IGX Orin shows **no multi-queue lift**: a single queue already
+saturates the ~96 Gb/s link, and the small-payload packet rate is not lifted by
+adding RX cores either. The matrix sweeps (TX cores, RX cores) over `(1,1)`,
+`(1,2)`, `(2,1)`, `(2,2)`.
+
+| Cell | TX pollers | RX pollers | Achieved Gb/s (8 KB) |
+| ---- | ---------- | ---------- | ------------: |
+| (1,1) | 9 | 10 | 96.4 |
+| (1,2) | 9 | 10,11 | 96.4 |
+| (2,1) | 9,7 | 10 | 95.4 |
+| (2,2) | 9,7 | 10,11 | 95.4 |
+
+The slight regression in the two-TX cells is the isolated-core budget: the IGX
+Orin has only three isolated cores (`isolcpus=9-11`), so the second TX poller
+borrows non-isolated core 7, where it contends with kernel/IRQ work. Sweeping
+each cell from 64 B to 8 KB confirms the cells stay clustered at every payload —
+the lines overlap rather than fanning out as they do on Spark:
+
+
+
+At small payloads (~14.5 Gb/s / ~14 M pps at 64 B) the path is packet-rate-bound
+but a second RX core does not raise the ceiling here, because the extra poller
+lands on the shared core 7/IRQ-contended budget rather than a clean isolated
+core. At large payloads the link is already saturated by one queue. Every cell is
+drop-free. Generated by `BENCH_PLATFORM=igx examples/run_spark_mq_bench.sh`
+(30 s per point) and `scripts/plot_mq_payload_sweep.py`.
+
+## Socket / RoCE
+
+RoCE SEND over the netns wire loopback, single queue-pair, batch 1, payloads in
+VRAM (`kind: device`). Throughput is App RX goodput, equal to App TX with 0
+drops. Large messages saturate the wire; small messages are bound by
+per-operation software overhead — and the IGX Orin's shallower flow-control
+window makes that overhead bite much earlier than on Spark.
+
+**Message-size sweep (single QP, batch 1, 0 drops), mean of 3 reps (spread ≤0.1 Gb/s):**
+
+| Message size | Gb/s |
+| ------------ | ---: |
+| 8 MB | **97.7** |
+| 1 MB | 97.6 |
+| 64 KB | 95.6 |
+| 8 KB | 12.6 |
+| 4 KB | 5.4 |
+
+Messages ≥64 KB hold ~96–98 Gb/s at the wire ceiling — matching Spark. Below
+that the path is operation-rate-bound (per-operation software overhead, not a
+stall). Here the IGX Orin diverges sharply from Spark: at 8 KB it reaches only
+12.6 Gb/s (Spark: 60.7) and at 4 KB only 5.4 Gb/s (Spark: 38.0). Two platform
+factors compound — the RoCE flow-control window is capped shallower on this HCA
+(`RDMA_RX_DEPTH_CAP=128` vs Spark's 512, so fewer operations stay in flight to
+amortize the per-op cost), and the smaller cores post/poll operations more
+slowly. The window is still drop-free; it is simply op-rate-bound far below the
+wire at small messages.
+
+**CPU utilization** (headline cell, 8 MB message, batch 1, unpaced, mean of 3 reps):
+
+| Core | Busy% | Note |
+| ------------------ | ----: | --------------------------------------------- |
+| Master (CPU 8) | ~2% | Orchestration only |
+| Client TX (CPU 10) | ~76% | Post-and-poll spin; rate-independent |
+| Server RX (CPU 0) | ~4% | HCA writes straight to memory; CPU uninvolved |
+
+The idle RX core is the expected RoCE RC signature — the HCA places incoming data
+directly into registered memory with no CPU involvement, exactly as on Spark. The
+GPU memory controller shows light activity (~6%) as the HCA DMAs message payloads
+into VRAM, while the GPU SM stays idle — the `kind: device` GPUDirect signature.
+
+## Socket / TCP
+
+Four one-way TCP client/server pairs over the netns wire loopback, each pair
+pinned to one non-isolated core (1–4). TCP self-paces via flow control, so App TX
+equals App RX with effectively no app-level loss. `message_size` is the per-send
+byte count of a stream (no datagram boundary, no fragmentation).
+
+Throughput in Gb/s (App TX = App RX), mean of 3 reps (spread ≤0.3 Gb/s):
+
+
+
+
+
Message size
+
Number of client/server pairs
+
+
+
1
2
4
+
+
+
+
1000 B
4.2
8.5
16.7
+
8000 B
15.9
16.8
15.3
+
1 MiB
17.2
15.6
14.6
+
+
+
+A single TCP stream tops out near **~17 Gb/s** — the per-core ceiling, far below
+Spark's ~97 Gb/s four-pair aggregate. That is host-side kernel-TCP CPU cost: a
+single 1 MiB pair runs 17.2 Gb/s here versus 31.6 Gb/s on Spark, a ~1.8× gap that
+matches the 1.97-vs-3.9 GHz per-core clock ratio almost exactly. The wire is
+identical; the cores are not.
+
+Two different scaling regimes appear as you add pairs:
+
+- **Small (1000 B) messages scale well** — 4.2 → 8.5 → 16.7 Gb/s over 1/2/4 pairs
+ (~linear). These are operation-rate-bound, so spreading them across more cores
+ helps. Pinning matters here: kernel sockets run on non-isolated cores 1–4 — the
+ isolated `nohz_full`/`rcu_nocbs` cores 9–11 used for DPDK/RoCE *halve* this
+ small-message scaling, because deferred timers/RCU starve the syscall path.
+- **Large (8 KB, 1 MiB) messages do not scale** — a single pair already nears the
+ ~17 Gb/s per-core ceiling, and adding pairs does not raise the aggregate (1 MiB
+ even drifts to 14.6 at 4 pairs). The cause is **receive-side, not the app
+ cores**: all NIC RX softirq processing serializes on a single core — measured,
+ every `NET_RX` softirq lands on cpu0, which runs ~70% in softirq while every
+ other core sees none. RSS/RPS is not spreading receive work, so the four flows'
+ copy-heavy receive paths share one softirq core. Lifting this would need
+ RX-softirq spreading (RPS / multi-queue RSS steered to distinct cores), not more
+ app cores — which is why moving the app threads around leaves the large-message
+ numbers essentially unchanged.
+
+## Socket / UDP
+
+Four one-way UDP client/server pairs, same one-core-per-pair pinning on the
+non-isolated cores 1–4. UDP has no
+flow control, so each sender runs flat-out and the receiver drops whatever it
+cannot drain — the loss column is an inherent property of unpaced UDP, not a
+fault. App RX is the delivered goodput; App-level loss is `(App TX − App RX) /
+App TX`.
+
+Each cell shows **receiver goodput in Gb/s** with the **app-level loss %** dimmed
+beneath it (mean of 3 reps):
+
+
+
+
+
Message size
+
Number of client/server pairs
+
+
+
1
2
4
+
+
+
+
1000 B
1.61% loss
0.00~100% loss
0.00~100% loss
+
8000 B
5.831% loss
8.920% loss
10.5~0% loss
+
+
+
+The sweep stops at 8000 B (single Ethernet frame). At 1000 B the multi-pair
+receiver collapses to near-total loss — the small-datagram receive rate on the
+IGX Orin's cores cannot keep up with multiple unpaced senders, so almost nothing
+is delivered. At 8000 B delivered goodput climbs with pair count to ~10.5 Gb/s
+(loss falling to ~0% at 4 pairs as the per-pair send rate drops below what the
+receiver drains). As on Spark, the wire itself is loss-free; the loss is
+host-side socket-buffer and receive-rate pressure, sharper here on the smaller
+cores.
+
+## Reproduce
+
+Run inside the project container (privileged, GPUs passed through, hugepages
+mounted), as root. Build with `-DCMAKE_BUILD_TYPE=Release` and
+`cmake --install build` so the bench loads the current `libdaqiri.so`. Select the
+IGX Orin profile with `BENCH_PLATFORM=igx` (the scripts default to `spark`); the
+profile (`examples/bench_platform_igx.env`) selects `kind: device`, the IGX core
+map, the `0005:03:00.x` PCIe addresses, and the shallower RoCE depth cap. See the
+"Platform profiles" note in `AGENTS.md`.
+
+```bash
+export BENCH_PLATFORM=igx
+export REPEATS=3 # mean of 3 reps, as published here
+export DAQIRI_BUILD_DIR=./build
+export LD_LIBRARY_PATH=/opt/daqiri/lib:${LD_LIBRARY_PATH:-}
+```
+
+The base container does not ship the network tools the setup scripts and RoCE
+baseline depend on; install them first, or
+`scripts/setup_spark_wire_loopback_netns.sh` fails with `ip: command not found`:
+
+```bash
+apt-get update
+apt-get install -y iproute2 iputils-ping ethtool iperf3 rdma-core ibverbs-utils perftest
+```
+
+These provide `ip`/`nstat` (`iproute2`), `ethtool`, and `ib_send_bw` (`perftest`).
+
+**Raw Ethernet / GPUDirect (DPDK)** drives the two physical ports directly, so
+the `dq_wire_*` namespaces must **not** be up — they capture the ports and
+hide them from DPDK. Tear them down first (no-op if they were never created).
+The RX physical port on the IGX Orin is `eth1` (p1 in the p0→p1 loopback):
+
+```bash
+./scripts/setup_spark_wire_loopback_netns.sh down # ensure netns is torn down
+export ETH_DST_ADDR=$(cat /sys/class/net/eth1/address)
+./examples/run_spark_bench.sh dpdk sweep
+```
+
+The **multi-queue core-scaling matrix and payload sweep** run on the same
+physical loopback (netns down). The four cells are generated from
+`examples/daqiri_bench_raw_tx_rx_spark_mq.yaml` at run time, so just export the
+rx-iface MAC as `ETH_DST_ADDR` (the script fills it into each generated config),
+then run the sweep and render the plot:
+
+```bash
+export ETH_DST_ADDR=$(cat /sys/class/net/eth1/address)
+./examples/run_spark_mq_bench.sh # 4 cells x payload sweep, 30 s each
+# render the line plot (needs matplotlib in a venv -- not a runtime dependency):
+./scripts/plot_mq_payload_sweep.py bench-results/-dpdk-mq/runs.csv \
+ docs/images/igx-mq-payload-sweep.svg \
+ "IGX Orin — DPDK multi-queue throughput vs payload"
+```
+
+**Socket / RoCE and sockets** cross the cable through the `dq_wire_client` →
+`dq_wire_server` namespaces. Bring the loopback up and confirm PHY counters move
+before running; tear it down when finished:
+
+```bash
+./scripts/setup_spark_wire_loopback_netns.sh up # create the namespaces
+./scripts/setup_spark_wire_loopback_netns.sh verify # confirm wire traffic
+./examples/run_spark_bench.sh rdma sweep
+./examples/run_spark_bench.sh socket-tcp sweep
+./examples/run_spark_bench.sh socket-udp sweep
+./scripts/setup_spark_wire_loopback_netns.sh down # tear down when done
+```
+
+Each run writes `bench-results/--/runs.csv`. See
+[Socket and RDMA Benchmarking](socket_benchmarking.md) and
+[Raw Ethernet Benchmarking](raw_benchmarking.md) for the namespace setup and
+per-transport details.
+
+---
+**Previous:** [Performance: DGX Spark](performance-dgx-spark.md)
diff --git a/docs/images/igx-mq-payload-sweep.svg b/docs/images/igx-mq-payload-sweep.svg
new file mode 100644
index 00000000..de8f7dc6
--- /dev/null
+++ b/docs/images/igx-mq-payload-sweep.svg
@@ -0,0 +1,1609 @@
+
+
+
diff --git a/examples/bench_platform.sh b/examples/bench_platform.sh
new file mode 100644
index 00000000..6679a011
--- /dev/null
+++ b/examples/bench_platform.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+# Platform selection for the DAQIRI bench suite.
+#
+# Source this from run_spark_bench.sh / run_spark_mq_bench.sh /
+# setup_spark_wire_loopback_netns.sh. It loads examples/bench_platform_
.env
+# (selected by $BENCH_PLATFORM) and exposes:
+# - all profile variables (BENCH_MEM_KIND, CORE_*, DPDK_*_PCI, WIRE_*, ...)
+# - bench_fill_placeholders -> stdout with every @VAR@ substituted
+#
+# Add a platform by dropping in a new bench_platform_.env next to this file.
+#
+# BENCH_PLATFORM defaults to "spark" (the documented reference platform). Set
+# BENCH_PLATFORM=igx for the IGX Orin devkit.
+
+_bench_platform_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BENCH_PLATFORM="${BENCH_PLATFORM:-spark}"
+_bench_profile="$_bench_platform_dir/bench_platform_${BENCH_PLATFORM}.env"
+
+if [[ ! -f "$_bench_profile" ]]; then
+ echo "ERROR: unknown BENCH_PLATFORM='$BENCH_PLATFORM' (no $_bench_profile)" >&2
+ echo " available:" "$(cd "$_bench_platform_dir" && ls bench_platform_*.env 2>/dev/null | sed -E 's/bench_platform_(.*)\.env/\1/' | tr '\n' ' ')" >&2
+ return 1 2>/dev/null || exit 1
+fi
+
+# shellcheck disable=SC1090
+source "$_bench_profile"
+echo "bench platform: $BENCH_PLATFORM (mem_kind=$BENCH_MEM_KIND, master=$CORE_MASTER) [set BENCH_PLATFORM= to change]" >&2
+
+# Substitute @VAR@ tokens in a config template, writing the result to stdout.
+# Every placeholder used by the config templates must be listed here.
+bench_fill_placeholders() {
+ local f="$1"
+ sed -E \
+ -e "s|@MEM_KIND@|${BENCH_MEM_KIND}|g" \
+ -e "s|@NUM_BUFS_TX@|${BENCH_NUM_BUFS_TX}|g" \
+ -e "s|@NUM_BUFS_RX@|${BENCH_NUM_BUFS_RX}|g" \
+ -e "s|@DPDK_TX_PCI@|${DPDK_TX_PCI}|g" \
+ -e "s|@DPDK_RX_PCI@|${DPDK_RX_PCI}|g" \
+ -e "s|@CORE_MASTER@|${CORE_MASTER}|g" \
+ -e "s|@CORE_DPDK_TXQ@|${CORE_DPDK_TXQ}|g" \
+ -e "s|@CORE_DPDK_RXQ@|${CORE_DPDK_RXQ}|g" \
+ -e "s|@CORE_DPDK_TXW@|${CORE_DPDK_TXW}|g" \
+ -e "s|@CORE_DPDK_RXW@|${CORE_DPDK_RXW}|g" \
+ -e "s|@CORE_MQ_TXQ0@|${CORE_MQ_TXQ0}|g" \
+ -e "s|@CORE_MQ_TXW0@|${CORE_MQ_TXW0}|g" \
+ -e "s|@CORE_MQ_TXQ1@|${CORE_MQ_TXQ1}|g" \
+ -e "s|@CORE_MQ_TXW1@|${CORE_MQ_TXW1}|g" \
+ -e "s|@CORE_MQ_RXQ0@|${CORE_MQ_RXQ0}|g" \
+ -e "s|@CORE_MQ_RXW0@|${CORE_MQ_RXW0}|g" \
+ -e "s|@CORE_MQ_RXQ1@|${CORE_MQ_RXQ1}|g" \
+ -e "s|@CORE_MQ_RXW1@|${CORE_MQ_RXW1}|g" \
+ -e "s|@CORE_ROCE_SRV_TXQ@|${CORE_ROCE_SRV_TXQ}|g" \
+ -e "s|@CORE_ROCE_SRV_RXQ@|${CORE_ROCE_SRV_RXQ}|g" \
+ -e "s|@CORE_ROCE_SRV_W@|${CORE_ROCE_SRV_W}|g" \
+ -e "s|@CORE_ROCE_CLI_TXQ@|${CORE_ROCE_CLI_TXQ}|g" \
+ -e "s|@CORE_ROCE_CLI_RXQ@|${CORE_ROCE_CLI_RXQ}|g" \
+ -e "s|@CORE_ROCE_CLI_W@|${CORE_ROCE_CLI_W}|g" \
+ "$f"
+}
diff --git a/examples/bench_platform_igx.env b/examples/bench_platform_igx.env
new file mode 100644
index 00000000..b672b837
--- /dev/null
+++ b/examples/bench_platform_igx.env
@@ -0,0 +1,63 @@
+# Platform profile: NVIDIA IGX Orin devkit (discrete RTX 6000 Ada + discrete CX-7).
+# Sourced by bench_platform.sh; consumed by run_spark_bench.sh,
+# run_spark_mq_bench.sh, and setup_spark_wire_loopback_netns.sh to fill the
+# @VAR@ placeholders in the bench config templates.
+#
+# IGX has a DISCRETE GPU and NIC on a shared PCIe switch (domain 0005, GPU<->NIC
+# PXB). Real GPUDirect (dmabuf) works, so kind: device makes the NIC DMA straight
+# to GPU VRAM -- on both DPDK raw and RoCE this is the difference between ~74/~38
+# Gb/s (host_pinned, NIC reads CPU DDR over PCIe capped by MPS=128) and ~96/~97
+# Gb/s. VRAM is 49 GB so the RX pool can be deep. Verified 2026-06-15.
+
+# Memory placement for GPU data buffers (DPDK raw + RoCE). kind: device == GPUDirect.
+BENCH_MEM_KIND="device"
+BENCH_NUM_BUFS_TX=51200
+BENCH_NUM_BUFS_RX=409600
+
+# DPDK physical ports (eth0 -> eth1 over-the-wire loopback).
+DPDK_TX_PCI="0005:03:00.0"
+DPDK_RX_PCI="0005:03:00.1"
+
+# netns wire-loopback netdevs / RDMA devices / IPs (sockets + RoCE).
+WIRE_CLIENT_IF="eth0"
+WIRE_SERVER_IF="eth1"
+WIRE_CLIENT_RDMA="mlx5_0"
+WIRE_SERVER_RDMA="mlx5_1"
+WIRE_CLIENT_IP="10.250.0.1"
+WIRE_SERVER_IP="10.250.0.2"
+WIRE_MTU=9000
+
+# Core map. IGX: 12 cores (0-11), isolcpus 9-11 (only 3 IRQ-free isolated cores;
+# irqaffinity=0-8). master 8; busy EAL pollers -> isolated 9/10/11; app workers
+# -> low cores 0-7. RoCE has 4 busy threads but only 3 isolated cores.
+CORE_MASTER=8
+# DPDK single-queue: queue pollers (EAL lcores) on isolated cores, workers on 0/1.
+CORE_DPDK_TXQ=9
+CORE_DPDK_RXQ=10
+CORE_DPDK_TXW=0
+CORE_DPDK_RXW=1
+# DPDK multi-queue (2,2): 3 isolated cores (9/10/11) carry pollers; TXq1 spills to
+# core 7; workers on 0-3. Leaves 4/5/6 free for CUDA helper threads.
+CORE_MQ_TXQ0=9; CORE_MQ_TXW0=0
+CORE_MQ_TXQ1=7; CORE_MQ_TXW1=1
+CORE_MQ_RXQ0=10; CORE_MQ_RXW0=2
+CORE_MQ_RXQ1=11; CORE_MQ_RXW1=3
+# RoCE netns: server/client queue cores + bench worker cores.
+CORE_ROCE_SRV_TXQ=9; CORE_ROCE_SRV_RXQ=2; CORE_ROCE_SRV_W=0
+CORE_ROCE_CLI_TXQ=10; CORE_ROCE_CLI_RXQ=3; CORE_ROCE_CLI_W=1
+# Socket pairs: NON-isolated cores in the irqaffinity=0-8 pool (normal tick/RCU).
+# Kernel sockets are not poll-mode, so the isolcpus/nohz_full/rcu_nocbs cores 9-11
+# (meant for DPDK/RoCE busy-spin) hurt them -- deferred timers/RCU starve the
+# syscall path, halving small-message scaling. Cores 1-4 also stay off core 0,
+# which is the single sink for all NIC RX softirqs (measured), so a socket pair
+# never contends with receive-softirq processing. (DPDK/RoCE keep cores 9-11.)
+SOCKET_PAIR_CORES=(1 2 3 4)
+
+# RoCE flow-control window: this CX-7/firmware throws CQ errors with the deep
+# rx_depth=512 the Spark run used (<=1MB cells), so cap at 128 (the known-good).
+RDMA_RX_DEPTH_CAP=128
+RDMA_TX_DEPTH_CAP=128
+
+# CSV display + per-core busy% sampling for the mq sweep (master, then each
+# queue's poller+worker, in this order): TXq0 TXw0 TXq1 TXw1 RXq0 RXw0 RXq1 RXw1.
+MQ_CPU_SAMPLE=("$CORE_MASTER" "$CORE_MQ_TXQ0" "$CORE_MQ_TXW0" "$CORE_MQ_TXQ1" "$CORE_MQ_TXW1" "$CORE_MQ_RXQ0" "$CORE_MQ_RXW0" "$CORE_MQ_RXQ1" "$CORE_MQ_RXW1")
diff --git a/examples/bench_platform_spark.env b/examples/bench_platform_spark.env
new file mode 100644
index 00000000..42ed9dc0
--- /dev/null
+++ b/examples/bench_platform_spark.env
@@ -0,0 +1,51 @@
+# Platform profile: DGX Spark (GB10 superchip).
+# Sourced by bench_platform.sh; consumed by run_spark_bench.sh,
+# run_spark_mq_bench.sh, and setup_spark_wire_loopback_netns.sh to fill the
+# @VAR@ placeholders in the bench config templates.
+#
+# GB10 has unified CPU/GPU memory (NVLink-C2C), so the NIC reaches data buffers
+# without a discrete-PCIe hop -- kind: host_pinned is the working GPUDirect path.
+
+# Memory placement for GPU data buffers (DPDK raw + RoCE).
+BENCH_MEM_KIND="host_pinned"
+BENCH_NUM_BUFS_TX=51200
+BENCH_NUM_BUFS_RX=51200
+
+# DPDK physical ports (p0 -> p1 over-the-wire loopback).
+DPDK_TX_PCI="0000:01:00.0"
+DPDK_RX_PCI="0002:01:00.1"
+
+# netns wire-loopback netdevs / RDMA devices / IPs (sockets + RoCE).
+WIRE_CLIENT_IF="enp1s0f0np0"
+WIRE_SERVER_IF="enP2p1s0f1np1"
+WIRE_CLIENT_RDMA="rocep1s0f0"
+WIRE_SERVER_RDMA="roceP2p1s0f1"
+WIRE_CLIENT_IP="10.250.0.1"
+WIRE_SERVER_IP="10.250.0.2"
+WIRE_MTU=9000
+
+# Core map. Spark: master 8; isolated X925 cores 16-19 (mq expands to 5-9,15-19).
+CORE_MASTER=8
+# DPDK single-queue: queue pollers (EAL lcores) + bench app workers.
+CORE_DPDK_TXQ=17
+CORE_DPDK_RXQ=18
+CORE_DPDK_TXW=16
+CORE_DPDK_RXW=19
+# DPDK multi-queue (2,2): per-queue poller + worker.
+CORE_MQ_TXQ0=16; CORE_MQ_TXW0=15
+CORE_MQ_TXQ1=19; CORE_MQ_TXW1=6
+CORE_MQ_RXQ0=18; CORE_MQ_RXW0=17
+CORE_MQ_RXQ1=9; CORE_MQ_RXW1=7
+# RoCE netns: server/client queue cores + bench worker cores.
+CORE_ROCE_SRV_TXQ=16; CORE_ROCE_SRV_RXQ=19; CORE_ROCE_SRV_W=19
+CORE_ROCE_CLI_TXQ=17; CORE_ROCE_CLI_RXQ=18; CORE_ROCE_CLI_W=18
+# Socket pairs: one isolated core per client/server pair (pairs 0-3).
+SOCKET_PAIR_CORES=(16 17 18 19)
+
+# RoCE flow-control window (deep window is fine on the Spark CX-7 firmware).
+RDMA_RX_DEPTH_CAP=512
+RDMA_TX_DEPTH_CAP=128
+
+# CSV display + per-core busy% sampling for the mq sweep (master, then each
+# queue's poller+worker, in this order): TXq0 TXw0 TXq1 TXw1 RXq0 RXw0 RXq1 RXw1.
+MQ_CPU_SAMPLE=("$CORE_MASTER" "$CORE_MQ_TXQ0" "$CORE_MQ_TXW0" "$CORE_MQ_TXQ1" "$CORE_MQ_TXW1" "$CORE_MQ_RXQ0" "$CORE_MQ_RXW0" "$CORE_MQ_RXQ1" "$CORE_MQ_RXW1")
diff --git a/examples/daqiri_bench_raw_tx_rx_spark.yaml b/examples/daqiri_bench_raw_tx_rx_spark.yaml
index 35815e9f..72991a50 100644
--- a/examples/daqiri_bench_raw_tx_rx_spark.yaml
+++ b/examples/daqiri_bench_raw_tx_rx_spark.yaml
@@ -1,26 +1,25 @@
-# DGX Spark (GB10) ready-to-run config for daqiri_bench_raw_gpudirect.
-# Templated version (with placeholders) is in
-# daqiri_bench_raw_tx_rx.yaml.
+# Over-the-wire loopback config template for daqiri_bench_raw_gpudirect, driven by
+# run_spark_bench.sh. The @TOKEN@ fields are filled per platform from
+# examples/bench_platform_
.env (BENCH_PLATFORM=spark|igx); see "Platform
+# profiles" in AGENTS.md. The fully-placeholdered base is daqiri_bench_raw_tx_rx.yaml.
#
-# Spark substitutions baked in here:
-# - PCIe addresses: this file is configured for an over-the-wire loopback --
-# tx_port (0000:01:00.0) and rx_port (0002:01:00.1) are different physical
-# ports (p0 / p1) cross-cabled by the chassis QSFP loop, so traffic crosses
-# the cable. To verify the port topology or set up a different loopback
-# (e.g. on-chip), see the "Port topology" section of the "DGX Spark" tab in
+# What the profile fills, and how the two reference platforms differ:
+# - @MEM_KIND@: host_pinned on DGX Spark (GB10 unified memory; nvidia_peermem
+# does not load, so host_pinned is the working GPUDirect path) vs device on
+# IGX Orin (discrete RTX 6000 Ada with real GPUDirect via dmabuf -- NIC DMAs
+# straight to VRAM, ~96 Gb/s vs ~74 with host_pinned).
+# - @DPDK_TX_PCI@ / @DPDK_RX_PCI@: the two cross-cabled physical ports (p0->p1
+# QSFP loop) so traffic crosses the wire. Spark 0000:01:00.0 / 0002:01:00.1;
+# IGX 0005:03:00.0 / 0005:03:00.1. See the "Port topology" section of
# docs/tutorials/system_configuration.md.
-# - kind: host_pinned, NOT device. GB10 has unified memory; nvidia_peermem
-# does not load and CUDA reports DMA_BUF_SUPPORTED=0. PR #41 made
-# host_pinned the working GPUDirect path on Spark (~94 Gbps unicast).
-# - cpu_core: DAQIRI queue threads use 17 (TX) / 18 (RX); the benchmark app
-# workers use the remaining big cores 16 (TX) / 19 (RX) so the app and
-# DAQIRI poll threads never share a core. All four are big-cluster X925
-# cores 16-19 isolated by the daqiri-tuning grub drop-in.
-# - master_core: 8 (a non-isolated big core; any 0-15 works).
-# - bench_tx ip_src/ip_dst: match the daqiri-tx / daqiri-rx nmcli profiles
-# (1.1.1.1/24 and 2.2.2.2/24, MTU 9000).
-# - eth_dst_addr: your rx_port's own MAC; fill per-system, e.g.:
-# cat /sys/class/net/enP2p1s0f1np1/address
+# - cpu_core / master_core: per-platform isolated-core map (Spark 16-19/master 8;
+# IGX 9-11/master 8). DAQIRI queue pollers and the bench app workers never
+# share a core.
+# - @NUM_BUFS_TX@ / @NUM_BUFS_RX@: buffer-pool depths (device memory on IGX lifts
+# the RX pool well above the host hugepage budget).
+# - bench_tx ip_src/ip_dst: 1.1.1.1/24 -> 2.2.2.2/24, MTU 9000.
+# - eth_dst_addr: rx_port's own MAC, filled by run_spark_bench.sh from
+# $ETH_DST_ADDR (cat /sys/class/net//address).
#
%YAML 1.2
---
@@ -28,32 +27,36 @@ daqiri:
cfg:
version: 1
stream_type: "raw"
- master_core: 8
+ master_core: @CORE_MASTER@
debug: false
log_level: "info"
loopback: ""
+ # Per-platform placeholder tokens are filled by examples/bench_platform.sh from
+ # the selected bench_platform_
.env (run via run_spark_bench.sh). kind is device
+ # on IGX (discrete GPU, GPUDirect to VRAM) and host_pinned on Spark (GB10
+ # unified memory); cores/PCI also differ per platform.
memory_regions:
- name: "Data_TX_GPU"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
- num_bufs: 51200
+ num_bufs: @NUM_BUFS_TX@
buf_size: 8064
- name: "Data_RX_GPU"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
- num_bufs: 51200
+ num_bufs: @NUM_BUFS_RX@
buf_size: 8064
interfaces:
- name: "tx_port"
- address: 0000:01:00.0
+ address: @DPDK_TX_PCI@
tx:
queues:
- name: "tx_q_0"
id: 0
batch_size: 10240
- cpu_core: 17
+ cpu_core: @CORE_DPDK_TXQ@
memory_regions:
- "Data_TX_GPU"
offloads:
@@ -62,13 +65,13 @@ daqiri:
# p1 -- a DIFFERENT physical port than tx_port (p0), cross-cabled to it, so
# traffic crosses the wire. The same physical port (e.g. 0002:01:00.0, also
# p0) would loop on-chip instead. Verify your p0/p1 mapping (see header).
- address: 0002:01:00.1
+ address: @DPDK_RX_PCI@
rx:
flow_isolation: true
queues:
- name: "rq_q_0"
id: 0
- cpu_core: 18
+ cpu_core: @CORE_DPDK_RXQ@
batch_size: 10240
memory_regions:
- "Data_RX_GPU"
@@ -84,11 +87,11 @@ daqiri:
bench_rx:
interface_name: "rx_port"
- cpu_core: 19
+ cpu_core: @CORE_DPDK_RXW@
bench_tx:
interface_name: "tx_port"
- cpu_core: 16
+ cpu_core: @CORE_DPDK_TXW@
batch_size: 10240
payload_size: 8000
header_size: 64
diff --git a/examples/daqiri_bench_raw_tx_rx_spark_mq.yaml b/examples/daqiri_bench_raw_tx_rx_spark_mq.yaml
index ceb31e20..e5fc146e 100644
--- a/examples/daqiri_bench_raw_tx_rx_spark_mq.yaml
+++ b/examples/daqiri_bench_raw_tx_rx_spark_mq.yaml
@@ -1,4 +1,8 @@
-# DGX Spark (GB10) multi-queue core-scaling base -- the (TX=2, RX=2) superset.
+# Multi-queue core-scaling base -- the (TX=2, RX=2) superset. PLATFORM TEMPLATE:
+# the @TOKEN@ fields (kind, cores, PCI, num_bufs) are filled by run_spark_mq_bench.sh
+# from examples/bench_platform_
.env (BENCH_PLATFORM=spark|igx). The detailed
+# core-cluster notes below describe the SPARK profile; IGX uses isolcpus 9-11 with
+# kind: device (discrete GPU GPUDirect). See "Platform profiles" in AGENTS.md.
#
# This is the single checked-in config for the multi-queue scaling sweep. The
# matrix cells (TX,RX) = (1,1),(1,2),(2,1),(2,2) are derived from this file at
@@ -49,42 +53,45 @@ daqiri:
cfg:
version: 1
stream_type: "raw"
- master_core: 8
+ master_core: @CORE_MASTER@
debug: false
log_level: "info"
loopback: ""
memory_regions:
+ # IGX discrete RTX 6000 Ada -> real GPUDirect: kind device for both TX and RX
+ # (NIC DMAs straight to VRAM). VRAM is 49 GB, so large pools are fine; RX gets
+ # the deep pool the ANO gpu-only config uses to sustain line rate drop-free.
- name: "Data_TX_GPU_0"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
- num_bufs: 51200
+ num_bufs: @NUM_BUFS_TX@
buf_size: 8064
- name: "Data_TX_GPU_1"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
- num_bufs: 51200
+ num_bufs: @NUM_BUFS_TX@
buf_size: 8064
- name: "Data_RX_GPU_0"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
- num_bufs: 51200
+ num_bufs: @NUM_BUFS_RX@
buf_size: 8064
- name: "Data_RX_GPU_1"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
- num_bufs: 51200
+ num_bufs: @NUM_BUFS_RX@
buf_size: 8064
interfaces:
- name: "tx_port"
- address: "0000:01:00.0" # quoted: PyYAML (gen_spark_mq_config.py) else reads the BDF as base-60
+ address: "@DPDK_TX_PCI@" # quoted: PyYAML (gen_spark_mq_config.py) else reads the BDF as base-60
tx:
queues:
- name: "tx_q_0"
id: 0
batch_size: 10240
- cpu_core: 16
+ cpu_core: @CORE_MQ_TXQ0@
memory_regions:
- "Data_TX_GPU_0"
offloads:
@@ -92,25 +99,25 @@ daqiri:
- name: "tx_q_1"
id: 1
batch_size: 10240
- cpu_core: 19
+ cpu_core: @CORE_MQ_TXQ1@
memory_regions:
- "Data_TX_GPU_1"
offloads:
- "tx_eth_src"
- name: "rx_port"
- address: "0002:01:00.1" # quoted: PyYAML (gen_spark_mq_config.py) else reads the BDF as base-60
+ address: "@DPDK_RX_PCI@" # quoted: PyYAML (gen_spark_mq_config.py) else reads the BDF as base-60
rx:
flow_isolation: true
queues:
- name: "rx_q_0"
id: 0
- cpu_core: 18
+ cpu_core: @CORE_MQ_RXQ0@
batch_size: 10240
memory_regions:
- "Data_RX_GPU_0"
- name: "rx_q_1"
id: 1
- cpu_core: 9
+ cpu_core: @CORE_MQ_RXQ1@
batch_size: 10240
memory_regions:
- "Data_RX_GPU_1"
@@ -135,15 +142,15 @@ daqiri:
bench_rx:
- interface_name: "rx_port"
queue_id: 0
- cpu_core: 17 # RX q0 worker (cluster A); its queue poller is on 18
+ cpu_core: @CORE_MQ_RXW0@ # RX q0 worker
- interface_name: "rx_port"
queue_id: 1
- cpu_core: 7 # RX q1 worker (cluster B); its queue poller is on 9
+ cpu_core: @CORE_MQ_RXW1@ # RX q1 worker
bench_tx:
- interface_name: "tx_port"
queue_id: 0
- cpu_core: 15 # TX q0 worker (cluster A); its queue poller is on 16
+ cpu_core: @CORE_MQ_TXW0@ # TX q0 worker
batch_size: 10240
payload_size: 8000
header_size: 64
@@ -154,7 +161,7 @@ bench_tx:
udp_dst_port: 4096
- interface_name: "tx_port"
queue_id: 1
- cpu_core: 6 # TX q1 worker (cluster 5-9); its poller is on 19 (cross-cluster)
+ cpu_core: @CORE_MQ_TXW1@ # TX q1 worker
batch_size: 10240
payload_size: 8000
header_size: 64
diff --git a/examples/daqiri_bench_rdma_tx_rx_spark_netns.yaml b/examples/daqiri_bench_rdma_tx_rx_spark_netns.yaml
index 6556aa18..3e678094 100644
--- a/examples/daqiri_bench_rdma_tx_rx_spark_netns.yaml
+++ b/examples/daqiri_bench_rdma_tx_rx_spark_netns.yaml
@@ -1,4 +1,10 @@
-# DGX Spark (GB10) RDMA bench — combined base for the netns wire loopback.
+# RDMA/RoCE bench — combined base for the netns wire loopback. PLATFORM TEMPLATE:
+# the @TOKEN@ fields (kind, cores) are filled by run_spark_bench.sh from
+# examples/bench_platform_
.env (BENCH_PLATFORM=spark|igx). On IGX use
+# kind: device (discrete GPU GPUDirect RDMA) -- NIC reads VRAM over the GPU<->NIC
+# PXB path and hits ~96 Gb/s on large messages vs ~38 with host_pinned (NIC reads
+# CPU DDR over PCIe, capped by MPS=128). Spark uses host_pinned (unified memory).
+# See "Platform profiles" in AGENTS.md.
#
# This is the single checked-in config for the RoCE wire-loopback sweep. It
# carries BOTH roles (the my_server / my_client interfaces, their memory regions,
@@ -30,28 +36,28 @@ daqiri:
cfg:
version: 1
stream_type: "socket"
- master_core: 8
+ master_core: @CORE_MASTER@
debug: false
log_level: "info"
memory_regions:
- name: "DATA_RX_GPU_SERVER"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
num_bufs: 128
buf_size: 10000000
- name: "DATA_TX_GPU_SERVER"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
num_bufs: 128
buf_size: 10000000
- name: "DATA_TX_GPU_CLIENT"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
num_bufs: 128
buf_size: 10000000
- name: "DATA_RX_GPU_CLIENT"
- kind: "host_pinned"
+ kind: "@MEM_KIND@"
affinity: 0
num_bufs: 128
buf_size: 10000000
@@ -68,32 +74,35 @@ daqiri:
queues:
- name: "Server_RX_Queue"
id: 0
- cpu_core: 19
+ cpu_core: @CORE_ROCE_SRV_RXQ@
batch_size: 1
tx:
queues:
- name: "Server_TX_Queue"
id: 0
- cpu_core: 16
+ cpu_core: @CORE_ROCE_SRV_TXQ@
batch_size: 1
- name: my_client
address: 10.250.0.1
socket_config:
mode: client
- remote_addr: "roce://10.250.0.2:4096"
+ # Current schema: a RoCE client gives only its own local_addr here; the
+ # peer endpoint lives in rdma_bench_client.server_address/server_port
+ # below. (The old remote_addr-in-socket_config form is now rejected.)
+ local_addr: "roce://10.250.0.1"
roce_config:
transport_mode: RC
tx:
queues:
- name: "Client_TX_Queue"
id: 0
- cpu_core: 17
+ cpu_core: @CORE_ROCE_CLI_TXQ@
batch_size: 1
rx:
queues:
- name: "Client_RX_Queue"
id: 0
- cpu_core: 18
+ cpu_core: @CORE_ROCE_CLI_RXQ@
batch_size: 1
rdma_bench_server:
@@ -102,7 +111,7 @@ rdma_bench_server:
# hands off through a ring, so co-locating the worker there livelocks at small
# message sizes (same failure mode as DPDK small-batch). Put the worker on the
# server's other isolated core (the RX queue core, 19).
- cpu_core: 19
+ cpu_core: @CORE_ROCE_SRV_W@
server_address: 10.250.0.2
server_port: 4096
message_size: 8000000
@@ -118,7 +127,7 @@ rdma_bench_server:
rdma_bench_client:
# Bench worker on a separate core from the mgr thread (Client_TX_Queue = 17);
# worker on the client's RX queue core (18). See the server note above.
- cpu_core: 18
+ cpu_core: @CORE_ROCE_CLI_W@
message_size: 8000000
# Flow-control windows (PR #144). Pre-post rx_depth receives before sending and
# bound the transmit side by tx_depth so small-message SENDs don't outrun the
diff --git a/examples/run_spark_bench.sh b/examples/run_spark_bench.sh
index 6b345060..f44c498e 100755
--- a/examples/run_spark_bench.sh
+++ b/examples/run_spark_bench.sh
@@ -52,6 +52,10 @@ fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="${DAQIRI_BUILD_DIR:-$SCRIPT_DIR/../build}"
+# Load the platform profile (BENCH_PLATFORM=spark|igx; default spark). Defines the
+# CORE_*/DPDK_*_PCI/BENCH_MEM_KIND/... values and bench_fill_placeholders().
+# shellcheck source=bench_platform.sh
+source "$SCRIPT_DIR/bench_platform.sh"
# Splits a combined both-role netns base into a single-role config (rdma + socket).
NETNS_GEN="$SCRIPT_DIR/../scripts/gen_spark_netns_config.py"
TS="$(date -u +%Y%m%dT%H%M%SZ)"
@@ -87,15 +91,14 @@ case "$BACKEND" in
PAIRS_HEADLINE=(1)
BASE_YAML="$SCRIPT_DIR/daqiri_bench_raw_tx_rx_spark.yaml"
BENCH_BIN="$BUILD_DIR/examples/daqiri_bench_raw_gpudirect"
- CPU_MASTER=8; CPU_TX=17; CPU_RX=18
+ CPU_MASTER=$CORE_MASTER; CPU_TX=$CORE_DPDK_TXQ; CPU_RX=$CORE_DPDK_RXQ
: "${ETH_DST_ADDR:?ETH_DST_ADDR must be set for dpdk backend (cat /sys/class/net//address)}"
# Resolve the tx_port (p0) / rx_port (p1) netdevs so each cell can assert wire
# transit via their *_phy SerDes counters -- the MLX5 bifurcated driver keeps
# these live even while the DPDK PMD owns the port, so a non-advancing
# rx_packets_phy flags the on-chip eswitch short-cut instead of a true cable
# loopback. Override DPDK_{TX,RX}_PCI / DPDK_{TX,RX}_NETDEV if auto-detect fails.
- DPDK_TX_PCI="${DPDK_TX_PCI:-0000:01:00.0}"
- DPDK_RX_PCI="${DPDK_RX_PCI:-0002:01:00.1}"
+ # DPDK_{TX,RX}_PCI come from the platform profile.
DPDK_TX_NETDEV="${DPDK_TX_NETDEV:-$(ls "/sys/bus/pci/devices/$DPDK_TX_PCI/net" 2>/dev/null | head -n1 || true)}"
DPDK_RX_NETDEV="${DPDK_RX_NETDEV:-$(ls "/sys/bus/pci/devices/$DPDK_RX_PCI/net" 2>/dev/null | head -n1 || true)}"
;;
@@ -112,7 +115,7 @@ case "$BACKEND" in
# the kernel's local routing table.
BASE_YAML="$SCRIPT_DIR/daqiri_bench_rdma_tx_rx_spark_netns.yaml"
BENCH_BIN="$BUILD_DIR/examples/daqiri_bench_rdma"
- CPU_MASTER=8; CPU_TX=17; CPU_RX=18
+ CPU_MASTER=$CORE_MASTER; CPU_TX=$CORE_ROCE_CLI_TXQ; CPU_RX=$CORE_ROCE_SRV_W
;;
# Single-frame UDP sizes (<= the ~8972 B MTU payload, so no IP fragmentation).
# 65507 is intentionally excluded: it fragments into ~8 packets and, under
@@ -137,7 +140,7 @@ case "$BACKEND" in
BENCH_BIN="$BUILD_DIR/examples/daqiri_bench_socket"
# Pair 0 pins to core 16 (see pair_core); report that core's busy% as the per-pair
# bottleneck. cpu_tx_pct and cpu_rx_pct therefore both refer to the pair-0 core.
- CPU_MASTER=8; CPU_TX=16; CPU_RX=16
+ CPU_MASTER=$CORE_MASTER; CPU_TX=${SOCKET_PAIR_CORES[0]}; CPU_RX=${SOCKET_PAIR_CORES[0]}
;;
socket-tcp)
# 1 MiB / 8000 / 1000 to mirror the published TCP matrix. The bench memsets a full
@@ -156,11 +159,18 @@ case "$BACKEND" in
BENCH_BIN="$BUILD_DIR/examples/daqiri_bench_socket"
# Pair 0 pins to core 16 (see pair_core); report that core's busy% as the per-pair
# bottleneck. cpu_tx_pct and cpu_rx_pct therefore both refer to the pair-0 core.
- CPU_MASTER=8; CPU_TX=16; CPU_RX=16
+ CPU_MASTER=$CORE_MASTER; CPU_TX=${SOCKET_PAIR_CORES[0]}; CPU_RX=${SOCKET_PAIR_CORES[0]}
;;
*) echo "Unknown backend: $BACKEND" >&2; exit 1 ;;
esac
+# Fill the platform @VAR@ placeholders (cores, mem kind, PCI, num_bufs) into a
+# concrete base once; every generator (dpdk sed, gen_spark_netns_config split,
+# socket sed) then operates on real values. yaml-cpp/PyYAML never see a token.
+FILLED_BASE="$OUT_DIR/base.$BACKEND.filled.yaml"
+bench_fill_placeholders "$BASE_YAML" > "$FILLED_BASE"
+BASE_YAML="$FILLED_BASE"
+
DROP_CURVE_TARGETS=(1 5 10 25 50 75 100 0) # 0 means unpaced (line rate)
# --------------------------------------------------------------------------
@@ -271,8 +281,12 @@ generate_yaml() {
# bite), large messages stay memory-bounded (where window depth is irrelevant).
local budget=1073741824 # 1 GiB pinned per memory region
local cap=$(( budget / payload )); (( cap < 1 )) && cap=1
- local rx_nb=512; (( rx_nb > cap )) && rx_nb=$cap
- local tx_nb=128; (( tx_nb > cap )) && tx_nb=$cap
+ # Flow-control window, capped per platform (RDMA_{RX,TX}_DEPTH_CAP): Spark's
+ # CX-7 firmware takes the deep rx_depth=512, but the IGX CX-7 throws CQ errors
+ # on <=1MB cells with 512, so its profile caps at 128. Also bounded by the
+ # per-region memory budget (cap) so large messages stay memory-bounded.
+ local rx_nb=$RDMA_RX_DEPTH_CAP; (( rx_nb > cap )) && rx_nb=$cap
+ local tx_nb=$RDMA_TX_DEPTH_CAP; (( tx_nb > cap )) && tx_nb=$cap
# Split the combined base per role, then apply the per-message-size window
# rewrite to each. Server -> $out, client -> ${out%.yaml}_client.yaml.
local role dst
@@ -306,7 +320,9 @@ generate_yaml() {
# the send rate to the receive rate (the sender cannot outrun the receiver it time-slices
# with), so each pair sits near the per-core ceiling and N pairs scale to ~N x that. This
# matches the reference four-pair methodology and keeps App TX ~= App RX with low loss.
-pair_core() { echo $(( 16 + ($1 % 4) )); }
+# One core per socket pair, from the platform profile's SOCKET_PAIR_CORES
+# (Spark 16-19; IGX 9/10/11/0). Wraps if more pairs than listed cores.
+pair_core() { echo "${SOCKET_PAIR_CORES[$(( $1 % ${#SOCKET_PAIR_CORES[@]} ))]}"; }
# Write the server/client YAML pair for socket pair `idx`: split the combined base
# per role, then substitute message_size, unique ports (SRV/CLI_PORT_BASE + idx),
@@ -316,18 +332,23 @@ generate_socket_yaml() {
local srv_port=$(( SRV_PORT_BASE + idx ))
local cli_port=$(( CLI_PORT_BASE + idx ))
local core; core="$(pair_core "$idx")"
+ # NOTE: the address-port patterns make the surrounding quote optional (\"?).
+ # gen_spark_netns_config.py round-trips through PyYAML, which emits the
+ # local_addr/remote_addr scheme strings UNQUOTED (udp://10.250.0.2:5001), so a
+ # quote-required pattern silently fails to substitute and every pair collides
+ # on the base port (TCP "address already in use", UDP all on one port).
python3 "$NETNS_GEN" "$BASE_YAML" --role server | \
sed -E \
-e "s|^( *message_size: ).*|\1$payload|g" \
- -e "s|^( *local_addr: \"[a-z]+://[0-9.]+:)[0-9]+(\")|\1$srv_port\2|" \
+ -e "s|^( *local_addr: \"?[a-z]+://[0-9.]+:)[0-9]+(\"?)|\1$srv_port\2|" \
-e "s|^( *server_port: ).*|\1$srv_port|" \
-e "s|^( *cpu_core: ).*|\1$core|" \
> "$server_out"
python3 "$NETNS_GEN" "$BASE_YAML" --role client | \
sed -E \
-e "s|^( *message_size: ).*|\1$payload|g" \
- -e "s|^( *local_addr: \"[a-z]+://[0-9.]+:)[0-9]+(\")|\1$cli_port\2|" \
- -e "s|^( *remote_addr: \"[a-z]+://[0-9.]+:)[0-9]+(\")|\1$srv_port\2|" \
+ -e "s|^( *local_addr: \"?[a-z]+://[0-9.]+:)[0-9]+(\"?)|\1$cli_port\2|" \
+ -e "s|^( *remote_addr: \"?[a-z]+://[0-9.]+:)[0-9]+(\"?)|\1$srv_port\2|" \
-e "s|^( *server_port: ).*|\1$srv_port|" \
-e "s|^( *cpu_core: ).*|\1$core|" \
> "$client_out"
diff --git a/examples/run_spark_mq_bench.sh b/examples/run_spark_mq_bench.sh
index 47876783..85eb3faf 100755
--- a/examples/run_spark_mq_bench.sh
+++ b/examples/run_spark_mq_bench.sh
@@ -71,6 +71,9 @@ set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="${DAQIRI_BUILD_DIR:-$SCRIPT_DIR/../build}"
+# Load the platform profile (BENCH_PLATFORM=spark|igx; default spark).
+# shellcheck source=bench_platform.sh
+source "$SCRIPT_DIR/bench_platform.sh"
BENCH_BIN="$BUILD_DIR/examples/daqiri_bench_raw_gpudirect"
RUN_SECONDS="${RUN_SECONDS:-30}"
PAYLOADS="${PAYLOADS:-64 256 1024 4096 8000}"
@@ -92,7 +95,9 @@ OUT_DIR="$SCRIPT_DIR/../bench-results/$TS-dpdk-mq"
mkdir -p "$OUT_DIR"
CSV="$OUT_DIR/runs.csv"
-echo "cell,tx_cores,rx_cores,payload,rep,gbps,pps,drops,cpu8,cpu16,cpu15,cpu19,cpu6,cpu18,cpu17,cpu9,cpu7" > "$CSV"
+# Per-core busy% columns are named from the platform's MQ_CPU_SAMPLE order.
+CPU_HDR="$(printf 'cpu%s,' "${MQ_CPU_SAMPLE[@]}" | sed 's/,$//')"
+echo "cell,tx_cores,rx_cores,payload,rep,gbps,pps,drops,$CPU_HDR" > "$CSV"
# Capture slow-moving environment state once per result set (mirrors
# run_spark_bench.sh). Best-effort -- skip if the helper is unavailable.
@@ -112,10 +117,16 @@ if [[ ! -f "$MQ_BASE" || ! -f "$MQ_GEN" ]]; then
exit 1
fi
+# Fill platform @VAR@ placeholders into a concrete base before gen_spark_mq_config
+# (PyYAML) ever parses it -- otherwise cores/kind/num_bufs tokens reach yaml-cpp.
+FILLED_MQ_BASE="$OUT_DIR/base.mq.filled.yaml"
+bench_fill_placeholders "$MQ_BASE" > "$FILLED_MQ_BASE"
+MQ_BASE="$FILLED_MQ_BASE"
+
# RX netdev whose *_phy counters prove traffic crossed the cable. rx_port is the
# PCIe address 0002:01:00.1; resolve its netdev name for ethtool -S. Override
# with RX_NETDEV if auto-detection fails.
-RX_PCI="0002:01:00.1"
+RX_PCI="$DPDK_RX_PCI" # from the platform profile
RX_NETDEV="${RX_NETDEV:-}"
if [[ -z "$RX_NETDEV" ]]; then
RX_NETDEV="$(ls "/sys/bus/pci/devices/$RX_PCI/net" 2>/dev/null | head -n1 || true)"
@@ -132,10 +143,9 @@ CELLS=(
"2t2r 2 2"
)
-# Cores to sample busy% for, in CSV column order: master, then each queue's
-# poller+worker pair (TX q0 16/15, TX q1 19/6, RX q0 18/17, RX q1 9/7).
-# Order must match the CSV header above.
-CPU_CORES=(8 16 15 19 6 18 17 9 7)
+# Cores to sample busy% for, in CSV column order (master, then each queue's
+# poller+worker pair). From the platform profile; must match the CSV header above.
+CPU_CORES=("${MQ_CPU_SAMPLE[@]}")
FAILURES=0
@@ -216,8 +226,8 @@ run_cell() {
# CSV display columns show the queue-poller cores: TX -> 16[,19], RX -> 18[,9]
# per queue count. '|' keeps multi-core lists in a single CSV field.
local tx_cores rx_cores
- [[ "$tx_count" == 2 ]] && tx_cores="16|19" || tx_cores="16"
- [[ "$rx_count" == 2 ]] && rx_cores="18|9" || rx_cores="18"
+ [[ "$tx_count" == 2 ]] && tx_cores="$CORE_MQ_TXQ0|$CORE_MQ_TXQ1" || tx_cores="$CORE_MQ_TXQ0"
+ [[ "$rx_count" == 2 ]] && rx_cores="$CORE_MQ_RXQ0|$CORE_MQ_RXQ1" || rx_cores="$CORE_MQ_RXQ0"
local run_dir="$OUT_DIR/$cell/p$payload/r$rep"
mkdir -p "$run_dir"
diff --git a/mkdocs.yml b/mkdocs.yml
index 5867d1de..892b2252 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -61,6 +61,7 @@ nav:
- Socket and RDMA Benchmarking: benchmarks/socket_benchmarking.md
- Raw Ethernet Benchmarking: benchmarks/raw_benchmarking.md
- "Performance: DGX Spark": benchmarks/performance-dgx-spark.md
+ - "Performance: IGX Orin": benchmarks/performance-igx-orin.md
- API Reference:
- API Guide: api-reference/index.md
- Configuration YAML Reference: api-reference/configuration.md
diff --git a/scripts/plot_mq_payload_sweep.py b/scripts/plot_mq_payload_sweep.py
index 173c3fb1..32341b59 100644
--- a/scripts/plot_mq_payload_sweep.py
+++ b/scripts/plot_mq_payload_sweep.py
@@ -1,18 +1,21 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
-"""Render the DGX Spark DPDK multi-queue payload sweep as a line plot.
+"""Render a DPDK multi-queue payload sweep as a line plot.
Reads a runs.csv produced by examples/run_spark_mq_bench.sh (columns:
cell,tx_cores,rx_cores,payload,gbps,pps,drops,cpu8,cpu16,cpu17,cpu18,cpu19)
and plots achieved Gb/s vs payload size, one line per (TX,RX) core cell.
Usage:
- scripts/plot_mq_payload_sweep.py [output.svg]
+ scripts/plot_mq_payload_sweep.py [output.svg] [chart title]
Default output: docs/images/spark-mq-payload-sweep.svg (relative to repo root).
-Requires matplotlib (not a runtime dependency of DAQIRI; install in a venv to
-regenerate, e.g. `python3 -m venv .venv && .venv/bin/pip install matplotlib`).
+The chart title defaults to the DGX Spark wording; pass a third argument to
+override it for another platform (e.g. "IGX Orin — DPDK multi-queue throughput
+vs payload"). Requires matplotlib (not a runtime dependency of DAQIRI; install
+in a venv to regenerate, e.g. `python3 -m venv .venv && .venv/bin/pip install
+matplotlib`).
"""
import csv
@@ -58,6 +61,8 @@ def main(argv):
else:
repo_root = Path(__file__).resolve().parent.parent
out_path = repo_root / "docs" / "images" / "spark-mq-payload-sweep.svg"
+ title = argv[3] if len(argv) >= 4 else \
+ "DGX Spark — DPDK multi-queue throughput vs payload"
series = load(csv_path)
@@ -76,7 +81,7 @@ def main(argv):
ax.set_xscale("log", base=2)
ax.set_xlabel("UDP payload size (bytes)")
ax.set_ylabel("Achieved throughput (Gb/s)")
- ax.set_title("DGX Spark — DPDK multi-queue throughput vs payload")
+ ax.set_title(title)
ax.set_ylim(bottom=0)
if payload_ticks:
ticks = sorted(payload_ticks)
diff --git a/scripts/setup_spark_wire_loopback_netns.sh b/scripts/setup_spark_wire_loopback_netns.sh
index 67f9ac0a..f8c757b6 100755
--- a/scripts/setup_spark_wire_loopback_netns.sh
+++ b/scripts/setup_spark_wire_loopback_netns.sh
@@ -55,23 +55,28 @@
CLIENT_NS=dq_wire_client
SERVER_NS=dq_wire_server
-CLIENT_IF=enp1s0f0np0 # p0 via PCI segment 0000:01:00.0 (carrier up)
-SERVER_IF=enP2p1s0f1np1 # p1 via PCI segment 0002:01:00.1 (carrier up) -- the
- # cabled peer of CLIENT_IF (p0<->p1 QSFP loopback). NOT
- # enP2p1s0f0np0, which is p0 again (same physical port).
-
-CLIENT_RDMA=rocep1s0f0 # rdma dev for enp1s0f0np0
-SERVER_RDMA=roceP2p1s0f1 # rdma dev for enP2p1s0f1np1
+# Interfaces / RDMA devices / IPs / MTU come from the platform profile
+# (examples/bench_platform_
.env, BENCH_PLATFORM=spark|igx). Spark cables
+# enp1s0f0np0<->enP2p1s0f1np1 (rocep1s0f0/roceP2p1s0f1); IGX cables eth0<->eth1
+# (mlx5_0/mlx5_1). Override any of these by exporting them before running.
+_setup_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# shellcheck source=../examples/bench_platform.sh
+source "$_setup_dir/../examples/bench_platform.sh"
+
+CLIENT_IF="${CLIENT_IF:-$WIRE_CLIENT_IF}"
+SERVER_IF="${SERVER_IF:-$WIRE_SERVER_IF}"
+CLIENT_RDMA="${CLIENT_RDMA:-$WIRE_CLIENT_RDMA}"
+SERVER_RDMA="${SERVER_RDMA:-$WIRE_SERVER_RDMA}"
# Leave these blank to auto-detect from the interfaces (recommended). Only set
# them if you need to override the peer MAC for some reason.
CLIENT_MAC=""
SERVER_MAC=""
-CLIENT_IP=10.250.0.1
-SERVER_IP=10.250.0.2
+CLIENT_IP="${WIRE_CLIENT_IP}"
+SERVER_IP="${WIRE_SERVER_IP}"
-MTU=9000 # colleague used 9082; 9000 matches the data-plane ports
+MTU="${WIRE_MTU}"
# =====================================================================
set -euo pipefail