From 8b42029a86e79b1ed2ce85d89b76ffb34329aafc Mon Sep 17 00:00:00 2001 From: Denis Leshchev Date: Mon, 15 Jun 2026 12:53:17 -0400 Subject: [PATCH 1/4] #184 - Parameterize the bench suite for Spark and IGX via platform profiles The DGX Spark sweep configs/scripts were hardcoded for Spark (GB10), so running the same suite on the IGX Orin devkit (discrete RTX 6000 Ada + ConnectX-7) meant editing files in place. Make the suite select a target platform with BENCH_PLATFORM (default spark) instead. - examples/bench_platform.sh sources examples/bench_platform_

.env and fills @VAR@ placeholders in the sweep config templates. - bench_platform_{spark,igx}.env carry the per-platform values: GPU memory kind, num_bufs, core map, DPDK port BDFs, wire-loopback netdevs/RDMA devices, and the RoCE flow-control depth cap. - The three *_spark* sweep configs become platform-neutral templates; the run_spark_* scripts and setup_spark_wire_loopback_netns.sh source the helper and fill a concrete base before any generator/PyYAML parses it. The key per-platform difference is GPU memory placement: Spark's unified memory uses kind: host_pinned, while IGX's discrete GPU uses kind: device for real GPUDirect (DPDK raw ~96 vs ~74 Gb/s, RoCE large-message ~96 vs ~38 Gb/s -- the NIC reads VRAM over the GPU<->NIC PXB path instead of CPU DDR over PCIe). Spark-filled output is byte-identical to the previous hardcoded config; IGX validated end-to-end. Also fixed two run_spark_bench.sh issues surfaced on IGX: the socket per-pair port substitution required quotes that gen_spark_netns_config.py (PyYAML) strips, so every pair collided on the base port; and the RoCE client config used socket_config.remote_addr, which the current schema rejects (peer now lives in rdma_bench_client.server_address). Co-Authored-By: Claude Opus 4.8 Signed-off-by: Denis Leshchev --- AGENTS.md | 11 +++ docs/benchmarks/performance-dgx-spark.md | 7 ++ examples/bench_platform.sh | 59 ++++++++++++++++ examples/bench_platform_igx.env | 58 ++++++++++++++++ examples/bench_platform_spark.env | 51 ++++++++++++++ examples/daqiri_bench_raw_tx_rx_spark.yaml | 67 ++++++++++--------- examples/daqiri_bench_raw_tx_rx_spark_mq.yaml | 47 +++++++------ .../daqiri_bench_rdma_tx_rx_spark_netns.yaml | 35 ++++++---- examples/run_spark_bench.sh | 45 +++++++++---- examples/run_spark_mq_bench.sh | 26 ++++--- scripts/setup_spark_wire_loopback_netns.sh | 25 ++++--- 11 files changed, 336 insertions(+), 95 deletions(-) create mode 100644 examples/bench_platform.sh create mode 100644 examples/bench_platform_igx.env create mode 100644 examples/bench_platform_spark.env diff --git a/AGENTS.md b/AGENTS.md index e1ea961a..43429b2b 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): diff --git a/docs/benchmarks/performance-dgx-spark.md b/docs/benchmarks/performance-dgx-spark.md index ecabf556..b4e1b9a4 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`: 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..7cf2f9f8 --- /dev/null +++ b/examples/bench_platform_igx.env @@ -0,0 +1,58 @@ +# 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: 3 isolated cores + core 0 (only 3 isolated available). +SOCKET_PAIR_CORES=(9 10 11 0) + +# 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/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 From 3f9b4e5ebb8dbe6802874efa0bc32b494967cce0 Mon Sep 17 00:00:00 2001 From: Denis Leshchev Date: Mon, 15 Jun 2026 14:49:00 -0400 Subject: [PATCH 2/4] #184 - Add IGX Orin performance report Add docs/benchmarks/performance-igx-orin.md, the IGX Orin devkit counterpart to the DGX Spark performance report, produced by the same sweep scripts with BENCH_PLATFORM=igx. Single-rep (REPEATS=1) C++-loopback numbers for DPDK raw (kind: device GPUDirect), RoCE SEND, TCP, and UDP, with an "Single-rep figures" callout in lieu of error bars. Headline: the offload-heavy bulk paths (DPDK raw, RoCE >=64 KB) hit the same ~96-98 Gb/s NIC/PCIe ceiling as Spark, while the CPU-bound paths trail Spark in proportion to the per-core CPU gap -- IGX's 1.97 GHz Cortex-A78AE cores vs the 3.9 GHz Cortex-X925 cluster the Spark bench pins onto. The single-pair 1 MiB TCP ratio (17.8 vs 31.6 = 1.78x) tracks the clock ratio, confirming the wire is not the limit. - scripts/plot_mq_payload_sweep.py: parameterize the chart title (optional 3rd arg) so it is not hardcoded "DGX Spark"; render docs/images/igx-mq-payload-sweep.svg. - Wire the page into mkdocs.yml nav, the README Documentation table, and the AGENTS.md docs layout list; add a Spark->IGX cross-link on the Spark page. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Denis Leshchev --- AGENTS.md | 1 + README.md | 1 + docs/benchmarks/performance-dgx-spark.md | 2 +- docs/benchmarks/performance-igx-orin.md | 344 +++++ docs/images/igx-mq-payload-sweep.svg | 1610 ++++++++++++++++++++++ mkdocs.yml | 1 + scripts/plot_mq_payload_sweep.py | 15 +- 7 files changed, 1968 insertions(+), 6 deletions(-) create mode 100644 docs/benchmarks/performance-igx-orin.md create mode 100644 docs/images/igx-mq-payload-sweep.svg diff --git a/AGENTS.md b/AGENTS.md index 43429b2b..3de138b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,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 b4e1b9a4..5d4d1c75 100644 --- a/docs/benchmarks/performance-dgx-spark.md +++ b/docs/benchmarks/performance-dgx-spark.md @@ -307,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..11e29071 --- /dev/null +++ b/docs/benchmarks/performance-igx-orin.md @@ -0,0 +1,344 @@ +--- +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). + +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.8 Gb/s here — a 1.78× 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 "Single-rep figures" + Unlike the DGX Spark page (mean ± std over 3 reps), the tables below are from + a single 30 s rep per cell (`REPEATS=1`), so no error bars are shown. Re-run + with `REPEATS=3` for publication-quality spread; every cell here was + drop-free and run-to-run variation on this platform has been within a couple + percent in earlier sweeps. + +## 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 | Each direction has a busy-spin queue poller and an app worker on separate cores. DPDK single-queue: pollers 9/10, workers 0/1, master 8. Multi-queue: TX pollers 9/7, RX pollers 10/11 — with only three isolated cores, the fourth poller borrows non-isolated core 7. | +| 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.8 Gb/s** | ~0 | Does not scale past ~17 Gb/s; kernel-TCP CPU-bound | +| Socket / UDP | 8 KB × 4 pairs | **9.3 Gb/s** | ~1% 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, single rep: + + + + + + + + + + + + + + + + + + +
PayloadBatch size (packets per burst)
2561024409610240
8000 B96.596.496.496.4
4096 B96.797.096.996.6
1024 B95.395.995.795.3
256 B36.737.537.037.8
64 B14.214.214.414.6
+ +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** (headline cell, 8000 B / batch 10240, single-queue, unpaced; +from the equivalent `(1,1)` multi-queue cell): + +| Core | Busy% | Note | +| ----------------------- | ----: | ------------------------------------- | +| Master (CPU 8) | ~28% | Orchestration only | +| TX queue poller (CPU 9) | ~70% | Poll-mode spin; rate-independent | +| RX queue poller (CPU 10)| ~70% | Poll-mode spin; rate-independent | + +The app workers run on their own cores (TX 0, RX 1) alongside these pollers. The +GPU stays compute-idle (SM ~0%) — it is a DMA target for the payload, not a +compute engine — though 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.5 | +| (1,2) | 9 | 10,11 | 96.4 | +| (2,1) | 9,7 | 10 | 95.4 | +| (2,2) | 9,7 | 10,11 | 95.5 | + +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: + +![DPDK multi-queue throughput vs UDP payload size on IGX Orin, one line per (TX,RX) core count](../images/igx-mq-payload-sweep.svg) + +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), single rep:** + +| Message size | Gb/s | +| ------------ | ---: | +| 8 MB | **97.7** | +| 1 MB | 97.6 | +| 64 KB | 95.6 | +| 8 KB | 12.8 | +| 4 KB | 5.5 | + +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.8 Gb/s (Spark: 60.7) and at 4 KB only 5.5 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, single rep): + +| Core | Busy% | Note | +| ------------------ | ----: | --------------------------------------------- | +| Master (CPU 8) | ~2% | Orchestration only | +| Client TX (CPU 10) | ~77% | Post-and-poll spin; rate-independent | +| Server-side core | ~76% | Worker core; busy-polls the receive CQ | + +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 core. 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), single rep: + + + + + + + + + + + + + + + + +
Message sizeNumber of client/server pairs
124
1000 B3.67.112.4
8000 B14.416.915.8
1 MiB17.817.215.6
+ +Kernel TCP on the IGX Orin tops out near **~17 Gb/s**, far below Spark's ~97 Gb/s +four-pair aggregate. Small (1000 B) messages are per-operation-bound and scale +with pair count (more cores doing syscalls), but at 8 KB and 1 MiB a single pair +already reaches the ~17 Gb/s ceiling and adding pairs causes contention rather +than scaling — 1 MiB even regresses from 17.8 (1 pair) to 15.6 (4 pairs). The +limit is host-side kernel-TCP CPU cost: a single 1 MiB pair runs 17.8 Gb/s here +versus 31.6 Gb/s on Spark, a 1.78× gap that matches the 1.97-vs-3.9 GHz per-core +clock ratio almost exactly — the wire is identical, the cores are not. + +## Socket / UDP + +Four one-way UDP client/server pairs, same one-core-per-pair pinning. 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 (single rep): + + + + + + + + + + + + + + + +
Message sizeNumber of client/server pairs
124
1000 B1.32% loss0.01~100% loss0.01~100% loss
8000 B5.428% loss8.121% loss9.31% 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 ~9.3 Gb/s +(loss falling to ~1% 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 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..368865f2 --- /dev/null +++ b/docs/images/igx-mq-payload-sweep.svg @@ -0,0 +1,1610 @@ + + + + + + + + 2026-06-15T14:36:06.019934 + image/svg+xml + + + Matplotlib v3.5.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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) From 75ef398f48a66ee760b1d597fdc38d425163da08 Mon Sep 17 00:00:00 2001 From: Denis Leshchev Date: Tue, 16 Jun 2026 10:06:26 -0400 Subject: [PATCH 3/4] #184 - Refresh IGX Orin performance report with 3-rep data Re-ran the full IGX Orin suite with REPEATS=3 (BENCH_PLATFORM=igx) and replaced the single-rep figures with 3-rep means. The numbers confirm the single-rep draft almost exactly (run-to-run spread <=0.4 Gb/s on DPDK/RoCE, <=0.3 on sockets), so the narrative is unchanged. Notable corrections from the cleaner data: - RoCE server RX core is ~4% busy (idle), not ~76% -- the expected RoCE RC signature (HCA writes straight to memory), matching Spark. The single-rep draft mis-attributed a busy worker core. - DPDK CPU-utilization table replaced with an honest note: per-core busy% is unreliable on the nohz_full/rcu_nocbs isolated cores (tickless /proc/stat accounting under-counts and shifts run-to-run at identical throughput), so we don't publish precise per-core figures -- the throughput-vs-Spark gap is the real CPU-bound evidence. - Regenerated docs/images/igx-mq-payload-sweep.svg from rep-averaged data. Methodology callout updated to REPEATS=3; Reproduce section exports REPEATS=3. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Denis Leshchev --- docs/benchmarks/performance-igx-orin.md | 92 +++--- docs/images/igx-mq-payload-sweep.svg | 375 ++++++++++++------------ 2 files changed, 233 insertions(+), 234 deletions(-) diff --git a/docs/benchmarks/performance-igx-orin.md b/docs/benchmarks/performance-igx-orin.md index 11e29071..8e425da8 100644 --- a/docs/benchmarks/performance-igx-orin.md +++ b/docs/benchmarks/performance-igx-orin.md @@ -8,7 +8,7 @@ hide: 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). +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 @@ -33,12 +33,13 @@ benchmarking procedure, see [Socket and RDMA Benchmarking](socket_benchmarking.m [Raw Ethernet Benchmarking](raw_benchmarking.md) (the two-physical-port DPDK loopback). The exact commands are collected under [Reproduce](#reproduce) below. -!!! note "Single-rep figures" - Unlike the DGX Spark page (mean ± std over 3 reps), the tables below are from - a single 30 s rep per cell (`REPEATS=1`), so no error bars are shown. Re-run - with `REPEATS=3` for publication-quality spread; every cell here was - drop-free and run-to-run variation on this platform has been within a couple - percent in earlier sweeps. +!!! 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 @@ -63,7 +64,7 @@ four-pair aggregate is shown. | ----------------- | --------- | ---------: | ----- | ----- | | 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.8 Gb/s** | ~0 | Does not scale past ~17 Gb/s; kernel-TCP CPU-bound | +| Socket / TCP | 8 KB × 4 pairs | **15.7 Gb/s** | ~0 | Does not scale past ~17 Gb/s; kernel-TCP CPU-bound | | Socket / UDP | 8 KB × 4 pairs | **9.3 Gb/s** | ~1% loss | Receiver goodput; unpaced sender | Each transport is best read at its own native operation size (see the per-transport @@ -81,7 +82,7 @@ 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, single rep: +drop-free), unpaced, mean of 3 reps (run-to-run spread ≤0.4 Gb/s): @@ -94,11 +95,11 @@ drop-free), unpaced, single rep: - - - - - + + + + +
8000 B96.596.496.496.4
4096 B96.797.096.996.6
1024 B95.395.995.795.3
256 B36.737.537.037.8
64 B14.214.214.414.6
8000 B96.596.596.596.4
4096 B96.797.097.096.6
1024 B95.495.995.795.3
256 B36.937.237.638.2
64 B14.114.114.314.5
@@ -114,18 +115,15 @@ 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** (headline cell, 8000 B / batch 10240, single-queue, unpaced; -from the equivalent `(1,1)` multi-queue cell): - -| Core | Busy% | Note | -| ----------------------- | ----: | ------------------------------------- | -| Master (CPU 8) | ~28% | Orchestration only | -| TX queue poller (CPU 9) | ~70% | Poll-mode spin; rate-independent | -| RX queue poller (CPU 10)| ~70% | Poll-mode spin; rate-independent | - -The app workers run on their own cores (TX 0, RX 1) alongside these pollers. The -GPU stays compute-idle (SM ~0%) — it is a DMA target for the payload, not a -compute engine — though its memory controller shows light activity as the NIC +**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 @@ -138,10 +136,10 @@ adding RX cores either. The matrix sweeps (TX cores, RX cores) over `(1,1)`, | Cell | TX pollers | RX pollers | Achieved Gb/s (8 KB) | | ---- | ---------- | ---------- | ------------: | -| (1,1) | 9 | 10 | 96.5 | +| (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.5 | +| (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 @@ -166,37 +164,38 @@ 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), single rep:** +**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.8 | -| 4 KB | 5.5 | +| 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.8 Gb/s (Spark: 60.7) and at 4 KB only 5.5 Gb/s (Spark: 38.0). Two platform +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, single rep): +**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) | ~77% | Post-and-poll spin; rate-independent | -| Server-side core | ~76% | Worker core; busy-polls the receive CQ | +| Client TX (CPU 10) | ~76% | Post-and-poll spin; rate-independent | +| Server RX (CPU 0) | ~4% | HCA writes straight to memory; CPU uninvolved | -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. +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 @@ -205,7 +204,7 @@ pinned to one core. TCP self-paces via flow control, so App TX equals App RX wit 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), single rep: +Throughput in Gb/s (App TX = App RX), mean of 3 reps (spread ≤0.3 Gb/s): @@ -218,9 +217,9 @@ Throughput in Gb/s (App TX = App RX), single rep: - - - + + +
1000 B3.67.112.4
8000 B14.416.915.8
1 MiB17.817.215.6
1000 B3.67.112.1
8000 B14.716.615.7
1 MiB17.816.415.4
@@ -228,7 +227,7 @@ Kernel TCP on the IGX Orin tops out near **~17 Gb/s**, far below Spark's ~97 Gb/ four-pair aggregate. Small (1000 B) messages are per-operation-bound and scale with pair count (more cores doing syscalls), but at 8 KB and 1 MiB a single pair already reaches the ~17 Gb/s ceiling and adding pairs causes contention rather -than scaling — 1 MiB even regresses from 17.8 (1 pair) to 15.6 (4 pairs). The +than scaling — 1 MiB even regresses from 17.8 (1 pair) to 15.4 (4 pairs). The limit is host-side kernel-TCP CPU cost: a single 1 MiB pair runs 17.8 Gb/s here versus 31.6 Gb/s on Spark, a 1.78× gap that matches the 1.97-vs-3.9 GHz per-core clock ratio almost exactly — the wire is identical, the cores are not. @@ -242,7 +241,7 @@ 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 (single rep): +beneath it (mean of 3 reps): @@ -255,8 +254,8 @@ beneath it (single rep): - - + +
1000 B1.32% loss0.01~100% loss0.01~100% loss
8000 B5.428% loss8.121% loss9.31% loss
1000 B1.31% loss0.00~100% loss0.00~100% loss
8000 B5.427% loss8.021% loss9.31% loss
@@ -281,6 +280,7 @@ map, the `0005:03:00.x` PCIe addresses, and the shallower RoCE depth cap. See th ```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:-} ``` diff --git a/docs/images/igx-mq-payload-sweep.svg b/docs/images/igx-mq-payload-sweep.svg index 368865f2..de8f7dc6 100644 --- a/docs/images/igx-mq-payload-sweep.svg +++ b/docs/images/igx-mq-payload-sweep.svg @@ -6,7 +6,7 @@ - 2026-06-15T14:36:06.019934 + 2026-06-16T10:03:35.551903 image/svg+xml @@ -42,16 +42,16 @@ z +" clip-path="url(#p382dddaae4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.5; stroke-width: 0.4; stroke-linecap: square"/> - - + @@ -117,11 +117,11 @@ z +" clip-path="url(#p382dddaae4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.5; stroke-width: 0.4; stroke-linecap: square"/> - + @@ -188,11 +188,11 @@ z +" clip-path="url(#p382dddaae4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.5; stroke-width: 0.4; stroke-linecap: square"/> - + @@ -246,11 +246,11 @@ z +" clip-path="url(#p382dddaae4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.5; stroke-width: 0.4; stroke-linecap: square"/> - + @@ -299,11 +299,11 @@ z +" clip-path="url(#p382dddaae4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.5; stroke-width: 0.4; stroke-linecap: square"/> - + @@ -737,16 +737,16 @@ z +" clip-path="url(#p382dddaae4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.5; stroke-width: 0.4; stroke-linecap: square"/> - - + @@ -758,18 +758,18 @@ L -3.5 0 - + - + - + @@ -777,18 +777,18 @@ L 493.2 219.55451 - + - + - + @@ -796,18 +796,18 @@ L 493.2 171.469021 - + - + - + @@ -815,18 +815,18 @@ L 493.2 123.383531 - + - + - + @@ -834,18 +834,18 @@ L 493.2 75.298041 - + - + - + @@ -1058,14 +1058,14 @@ z - + - - - - - - - + + + + + + - + - - - - - - - + + + + + + - + - - - - - - - + + + + + + - + - - - - - - - + + + + + + @@ -1190,16 +1190,9 @@ L 493.2 25.44 " style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1442,7 +1441,7 @@ L 70.05 53.216562 L 80.05 53.216562 " style="fill: none; stroke: #1f77b4; stroke-width: 1.8; stroke-linecap: square"/> - + @@ -1513,7 +1512,7 @@ L 70.05 67.894687 L 80.05 67.894687 " style="fill: none; stroke: #ff7f0e; stroke-width: 1.8; stroke-linecap: square"/> - + @@ -1544,7 +1543,7 @@ L 70.05 82.572812 L 80.05 82.572812 " style="fill: none; stroke: #2ca02c; stroke-width: 1.8; stroke-linecap: square"/> - + @@ -1574,7 +1573,7 @@ L 70.05 97.250937 L 80.05 97.250937 " style="fill: none; stroke: #d62728; stroke-width: 1.8; stroke-linecap: square"/> - + @@ -1603,7 +1602,7 @@ L 80.05 97.250937 - + From c86f365a63909e69dc07d5bd42bd5eb3c52f82f3 Mon Sep 17 00:00:00 2001 From: Denis Leshchev Date: Tue, 16 Jun 2026 13:07:08 -0400 Subject: [PATCH 4/4] #184 - Pin IGX socket benchmarks to non-isolated cores; refresh socket results The IGX socket pairs were pinned to the isolated cores (9 10 11 0) that DPDK/RoCE use for poll-mode busy-spin. That tuning is wrong for kernel sockets: the nohz_full/rcu_nocbs cores defer timers/RCU and starve the syscall path, and core 0 is the single sink for all NIC RX softirqs. Re-pin SOCKET_PAIR_CORES to the non-isolated cores (1 2 3 4) in the irqaffinity=0-8 pool and re-run TCP+UDP at REPEATS=3. Effect: small-message TCP now scales ~linearly (1000 B: 4.2/8.5/16.7 over 1/2/4 pairs vs 3.6/7.1/12.1 before) and UDP improves across the board (8 KB 4-pair 10.5 vs 9.3 Gb/s). Large-message TCP is essentially unchanged because it is receive-side bound, not app-core bound. Report update: refreshed the TCP/UDP tables and added the measured root cause for the large-message TCP ceiling -- all NIC RX softirq serializes on a single core (cpu0 runs ~70% in softirq, every other core sees no NET_RX), so the four flows' copy-heavy receive paths share one softirq core. RSS/RPS is not spreading receive work; lifting it would need RX-softirq spreading, not more app cores. Documented the socket-vs-poll-mode core split in the System-under-test table. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Denis Leshchev --- docs/benchmarks/performance-igx-orin.md | 62 ++++++++++++++++--------- examples/bench_platform_igx.env | 9 +++- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/docs/benchmarks/performance-igx-orin.md b/docs/benchmarks/performance-igx-orin.md index 8e425da8..a3b2971f 100644 --- a/docs/benchmarks/performance-igx-orin.md +++ b/docs/benchmarks/performance-igx-orin.md @@ -24,7 +24,7 @@ large RoCE messages) hit the same ~96–98 Gb/s NIC/PCIe ceiling as Spark — th 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.8 Gb/s here — a 1.78× ratio that tracks the 3.9/1.97 GHz clock ratio +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 @@ -51,7 +51,7 @@ loopback). The exact commands are collected under [Reproduce](#reproduce) below. | 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 | Each direction has a busy-spin queue poller and an app worker on separate cores. DPDK single-queue: pollers 9/10, workers 0/1, master 8. Multi-queue: TX pollers 9/7, RX pollers 10/11 — with only three isolated cores, the fourth poller borrows non-isolated core 7. | +| 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) @@ -64,8 +64,8 @@ four-pair aggregate is shown. | ----------------- | --------- | ---------: | ----- | ----- | | 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.7 Gb/s** | ~0 | Does not scale past ~17 Gb/s; kernel-TCP CPU-bound | -| Socket / UDP | 8 KB × 4 pairs | **9.3 Gb/s** | ~1% loss | Receiver goodput; unpaced sender | +| 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 @@ -200,9 +200,9 @@ into VRAM, while the GPU SM stays idle — the `kind: device` GPUDirect signatur ## Socket / TCP Four one-way TCP client/server pairs over the netns wire loopback, each pair -pinned to one core. 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). +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): @@ -217,24 +217,40 @@ Throughput in Gb/s (App TX = App RX), mean of 3 reps (spread ≤0.3 Gb/s): - 1000 B3.67.112.1 - 8000 B14.716.615.7 - 1 MiB17.816.415.4 + 1000 B4.28.516.7 + 8000 B15.916.815.3 + 1 MiB17.215.614.6 -Kernel TCP on the IGX Orin tops out near **~17 Gb/s**, far below Spark's ~97 Gb/s -four-pair aggregate. Small (1000 B) messages are per-operation-bound and scale -with pair count (more cores doing syscalls), but at 8 KB and 1 MiB a single pair -already reaches the ~17 Gb/s ceiling and adding pairs causes contention rather -than scaling — 1 MiB even regresses from 17.8 (1 pair) to 15.4 (4 pairs). The -limit is host-side kernel-TCP CPU cost: a single 1 MiB pair runs 17.8 Gb/s here -versus 31.6 Gb/s on Spark, a 1.78× gap that matches the 1.97-vs-3.9 GHz per-core -clock ratio almost exactly — the wire is identical, the cores are not. +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. UDP has no +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) / @@ -254,16 +270,16 @@ beneath it (mean of 3 reps): - 1000 B1.31% loss0.00~100% loss0.00~100% loss - 8000 B5.427% loss8.021% loss9.31% loss + 1000 B1.61% loss0.00~100% loss0.00~100% loss + 8000 B5.831% loss8.920% loss10.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 ~9.3 Gb/s -(loss falling to ~1% at 4 pairs as the per-pair send rate drops below what the +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. diff --git a/examples/bench_platform_igx.env b/examples/bench_platform_igx.env index 7cf2f9f8..b672b837 100644 --- a/examples/bench_platform_igx.env +++ b/examples/bench_platform_igx.env @@ -45,8 +45,13 @@ 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: 3 isolated cores + core 0 (only 3 isolated available). -SOCKET_PAIR_CORES=(9 10 11 0) +# 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).