Skip to content

#15 - Clean up of RoCE bench issues#223

Merged
RamyaGuru merged 18 commits into
mainfrom
roce-bench-unidir-and-profiling
Jul 13, 2026
Merged

#15 - Clean up of RoCE bench issues#223
RamyaGuru merged 18 commits into
mainfrom
roce-bench-unidir-and-profiling

Conversation

@RamyaGuru

Copy link
Copy Markdown
Collaborator

Clean up of various RoCE bench issues on DGX Spark:

  • Unidirectional standardization — RoCE was the only transport running
    bidirectionally; make it one-way by default (matching DPDK and sockets) so the
    numbers are apples-to-apples, and re-measure the perf report.
  • Idling in the workload bench — the RoCE workload receive path blocked on the
    GPU every message; recycle recv buffers with CUDA events so the receiver keeps
    reaping completions instead.
  • Fixed GEMM size for the workload bench — pin the GEMM dimension (and add a
    matching FFT-length knob) so the FLOP count per call is constant across the
    message-size sweep.
  • Spark netns port auto-detect — harden the wire-loopback setup to group ports
    by phys_port_name, robust to PCIe re-enumeration.

RamyaGuru and others added 12 commits July 8, 2026 10:05
Pin the GEMM dimension (n=1024, --workload-gemm-n) so the RoCE-vs-raw
comparison isolates transport from problem size. The prior batch sweep
confounded the two (n scaled with batch), which produced the misleading
"gap is pipelining depth" reading. The window-matched fixed-n data shows
the gap is real and receive-path/threading bound, not the GPU; adds the
GEMMs/s + SM% table, flags the batch sweep as superseded, and documents
the reproduce commands.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ramya Gurunathan <rgurunathan@nvidia.com>
Pin the square GEMM dimension (--workload-gemm-n) so the FLOP count per
call is fixed while the I/O unit is swept, isolating transport from
problem size; init logs the chosen GEMM/FFT shape and FLOP count, and
rejects a pinned n whose A operand exceeds the received working set.
Add --workload-sync-interval to vary GPU stream drain depth for
characterizing the single-threaded receive+compute ceiling. Thread both
through all bench mains and expose them via run_spark_bench.sh (GEMM_N,
SYNC_INTERVAL envs; post_process_gemm_n, post_process_sync CSV columns).

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ramya Gurunathan <rgurunathan@nvidia.com>
The --workload-sync-interval sweep (2->32, both precisions) shows no
measurable change in RoCE throughput, and fully-synchronous matches
deep-async. Sync cadence is not the ceiling; the RoCE path already
drains once per held-receive batch, masking the knob, so the fix is
structural (dedicated GPU-worker thread) rather than sync tuning.
Updates the fixed-size GEMM section accordingly.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Ramya Gurunathan <rgurunathan@nvidia.com>
…de flags

The receive-path GPU workload sized its GEMM from a --workload-batch-bytes
knob (deriving n = sqrt(batch/4)) and sub-divided each RoCE message into
chunk-sized slices, which conflated problem size with pipelining depth and
made the fixed-n RoCE-vs-raw comparison confusing to run (juggling
WORKLOAD_BATCH + PAYLOADS_OVERRIDE).

Pin the square GEMM side length directly instead:
- Rename --workload-gemm-n -> --workload-gemm-dim (default 1024); the compute
  working set is exactly n*n*elem_size, read from the front of each received
  I/O unit. GpuWorkload rejects a unit too small to hold the operand.
- Delete --workload-batch-bytes / parse_workload_batch_bytes and the
  derive-n-from-bytes branch. Each backend keeps its default reorder window
  (raw 1024 packets, socket 8 MB, RoCE whole message); RoCE now runs one
  fixed-dimension GEMM per received message.
- run_spark_bench.sh: GEMM_N -> GEMM_DIM (default 1024); drop WORKLOAD_BATCH
  and PAYLOADS_OVERRIDE. When a workload is active the payload sweep is
  restricted to the headline size (the small cells cannot hold the operand).
  CSV: post_process_gemm_n -> post_process_gemm_dim; drop post_process_batch.
- Docs: update AGENTS.md bench-flag summary and the DGX Spark perf report
  methodology/reproduce; remove the superseded batch-size-sweep section and
  mark the fixed-GEMM table pending refresh.

Example-only change; no library rebuild required.

Signed-off-by: Ramya Gurunathan <rgurunathan@nvidia.com>
Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
…drain

The RoCE workload bench read each recv buffer zero-copy and, to know when the
buffer was safe to free/repost, drained the whole GPU stream
(cudaStreamSynchronize) once per held-receive batch -- and run() also drained
every sync_interval GEMMs. On the single receive thread that drain parks the
thread on the GPU, so it stops reaping completions and reposting receives while
the GEMM runs. That serialization (not the GPU, buffers, or transport) is why
the fixed-n RoCE point ran ~2x slower than raw at the identical workload.

Replace the blocking drain with non-blocking, event-based recycling:
- GpuWorkload gains a lazily-created pool of cudaEvents (cudaEventDisableTiming)
  and record_event / event_done / wait_event / release_event, plus run_async
  (enqueue with no maybe_sync). Events complete in stream order, so an event
  stamped after a message's GEMM signals that the read of that recv buffer is
  done. The pool costs nothing for the run()+sync() callers that never record.
- rdma_bench holds each completion with its event in a FIFO, frees buffers as
  soon as event_done() reports the GEMM finished (reclaim_completed), and keeps
  reaping completions meanwhile. Outstanding GPU work is bounded by max_inflight
  (backpressure: block on the oldest event only at the cap), which replaces the
  periodic sync_interval drain as the measurement-honesty bound. Shutdown /
  connection loss drains all events fully before teardown.

DPDK/socket are unchanged (they copy into the pipeline's shared output buffer,
so overlapping GEMMs would need that buffer ring-buffered first); a comment in
raw_bench_common notes the prerequisite. --workload-sync-interval still governs
the DPDK/socket run()+sync() path; the RoCE path bounds work with events and
ignores it.

Example-only change; no library rebuild required.

Signed-off-by: Ramya Gurunathan <rgurunathan@nvidia.com>
Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
…iling knobs

The RoCE GPU-workload receive thread parked on the GPU every message -- a
blocking drain of held recv buffers on the idle path, plus an eager cap wait --
which starved the GPU and throttled the sender. Reclaim held recv buffers
non-blocking instead: drop the idle-path drain, reclaim before the last-resort
cap block, and only block when the event pool would otherwise starve. nsys
confirmed the per-message cudaEventSynchronize -- not the GPU, memory, or
transport -- was the cap.

Add the bench knobs used to root-cause and re-measure this:
- run_spark_bench.sh: UNIDIR (one-way RoCE, matching the DPDK and socket benches
  which are already one-way), NSYS/NSYS_BIN (wrap the rdma server in nsys),
  RUN_SECONDS override, MAX_INFLIGHT, and RDMA_BUDGET_GIB/RX_NB/TX_NB for the
  buffer-depth experiment.
- --workload-max-inflight bench flag and a larger CUDA event pool.

The residual RoCE<DPDK gap turned out to be the bidirectional benchmark, not the
pipeline: one-way RoCE holds line rate (FP32 ~103 Gb/s), matching DPDK.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
…on results

Replace the stale, pre-fix bidirectional numbers with a single same-session
table comparing DPDK and one-way RoCE (none / FFT / GEMM FP32); both hold ~line
rate under the GPU workload. Delete the obsolete "single-threaded RoCE stall"
narrative and the GEMMs/s + duty-cycle-SM analysis -- the gap was the
bidirectional benchmark, now removed. Drop the FP16 GEMM result, clarify both
workloads are FP32, and remove the misleading "native shape" wording (8 KB is a
chosen operating point, not the throughput peak). Fix the Reproduce commands to
set UNIDIR=1 for the RoCE runs.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
The RoCE netns bench was the only transport running bidirectionally (both roles
send + receive), which understated its one-way rate and made it inconsistent
with the DPDK bench (inherently TX->RX one-way) and the socket bench (explicitly
one-way). Flip the combined base config so the server is receive-only (runs the
GEMM) and the client is send-only, mirroring the socket netns base;
gen_spark_netns_config.py splits each role's section verbatim, so the split
configs inherit the one-way flags.

Remove the now-redundant UNIDIR env knob and its awk send:/receive: rewrite from
run_spark_bench.sh -- one-way is the default. Drop UNIDIR=1 from the perf-doc
Reproduce commands and reword the methodology note accordingly. The RoCE result
numbers are re-measured in a follow-up commit.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
The setup script hardcoded CLIENT_IF/SERVER_IF (enp1s0f0np0 / enP2p1s0f1np1),
which no longer exist after PCIe re-enumeration on the DGX Spark. With dead
names, detect_macs aborts, the dq_wire_{client,server} namespaces are never
created, and the netns-based socket/RoCE benches (launched via `ip netns exec`)
silently have nowhere to run.

Make the cabled ports auto-detected and robust to enumeration drift:
- CLIENT_IF/SERVER_IF/CLIENT_RDMA/SERVER_RDMA default to blank, env-overridable.
- autodetect_ports() picks the two carrier-up RoCE-backed netdevs in init_net
  and derives the RDMA devices from sysfs (rdma_for_netdev); errors with
  guidance if not exactly two are found.
- down() now moves every non-lo netdev in each namespace back to init_net, so
  teardown no longer depends on a hardcoded interface name.
- resolve_ports() lets verify()/monitor() read the in-namespace netdev names
  after up() has moved the ports, falling back to auto-detect otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
autodetect_ports() required exactly 2 carrier-up RoCE netdevs, but Spark's single
CX-7 exposes each of its 2 physical ports over both PCIe segments (socket-direct),
so a cabled loopback lights carrier on up to 4 PFs at once. The count check then
aborts ("found 3/4"), and it could also pick two PCIe reps of the SAME physical
port -- the on-chip eswitch shortcut instead of an over-the-wire path.

Group carrier-up RoCE netdevs by phys_port_name (the hardware port label, stable
across PCIe re-enumeration and interface renames), keep one rep per physical port
(lowest BDF, matching the doc's canonical enp1s0f{0,1}np{0,1} names), and require
exactly 2 distinct ports. CLIENT_IF/SERVER_IF therefore always straddle p0 and p1
(over-the-wire). Falls back to per-netdev grouping when phys_port_name is
unavailable so the 2-port guard still holds. Refresh the header comments and the
manual-override example accordingly.

See docs/tutorials/system_configuration.md "Port topology: 4 PFs, 2 ports".

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
The FFT workload's 1-D C2C transform length was a hardcoded constant (1024),
while GEMM already had --workload-gemm-dim. Add a symmetric --workload-fft-len so
the FFT compute size is a benchmark knob, independent of the GEMM dimension:

- parse_workload_fft_len() mirrors parse_workload_gemm_dim; GpuWorkload::init()
  takes an fft_len arg (default 1024) and threads it into cufftPlan1d, replacing
  the kFftLen constant. The working set still fans out across as many batched
  length-N transforms as fit.
- Thread fft_len through every bench main (rdma, socket, raw gpudirect/hds) and
  rx_count_worker, exactly as gemm_dim is.
- run_spark_bench.sh gains an FFT_LEN env (default 1024) passed as
  --workload-fft-len for all backends.

Verified in-container: FFT_LEN=512 logs "fft shape = 1953 x C2C length-512".

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
Now that the RoCE bench is one-way, re-measure and refresh the perf report so all
three transports are apples-to-apples:

- Results Summary RoCE row and message-size sweep updated with one-way numbers
  (best case 106.0 Gb/s at 64 KB; 8 MB 105.0). Large messages rose vs the old
  bidirectional run (reverse-direction contention removed); the op-rate-bound
  small cells are the honest single-direction rate.
- CPU-utilization table: fix the receiver core. For rdma, cpu_rx measured the
  idle client RX core (18); the one-way receiver is the SERVER, whose RX poller +
  bench worker share core 19. Point CPU_RX at 19 and re-measure -- the receiver
  core is ~1.1% at the bare-loopback 8 MB cell, confirming the RoCE RC "HCA DMAs
  straight to memory" signature with the correct core.
- Reproduce: collapse the two redundant GPU-workload blocks into one (a
  workload-active sweep auto-pins to the summary-table cell, so it equals smoke);
  add a general smoke-vs-sweep explanation; document the FFT default and the new
  FFT_LEN / --workload-fft-len knob; drop the redundant explicit GEMM_DIM=1024.
- Rename "headline cell" -> "summary-table cell" throughout.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
@RamyaGuru
RamyaGuru marked this pull request as ready for review July 10, 2026 15:13
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three RoCE bench correctness issues on DGX Spark and hardens the loopback setup, producing re-measured, phy-verified 100 GbE numbers (correcting prior on-chip eswitch figures that exceeded the 100 GbE wire ceiling).

  • Unidirectional RoCE: sets send: false / receive: true on the server and vice versa in the netns YAML, matching DPDK and socket benches; the run_spark_bench.sh RoCE section now pins and measures the correct server-RX core (19, not the idle client-RX core 18).
  • CUDA-event recv recycling in rdma_bench.cpp: replaces the per-batch blocking flush with an event-pool–based approach — each recv buffer is stamped with a CUDA event after its GEMM is enqueued and freed non-blockingly once event_done() fires, keeping the single receive thread continuously reaping completions rather than parking on the GPU every message.
  • Fixed GEMM dimension: pins the square GEMM side length n (default 1024) via --workload-gemm-dim so the FLOP count (2·n³) is constant across the message-size sweep; --workload-fft-len adds the equivalent knob for the FFT path.
  • Spark netns port auto-detect: autodetect_ports() groups carrier-up RoCE netdevs by phys_port_name, picks the lowest-BDF representative per physical port, and requires exactly 2 to form a valid over-the-wire pair, surviving PCIe re-enumeration and interface renames.

Confidence Score: 5/5

Safe to merge. All BurstParams free paths in the new event-recycling receive code are correct, the autodetect logic is sound, and the one-way switch is consistent across YAML, script, and perf docs.

The CUDA-event recv recycling is the most complex new path: every completion is freed either immediately (event pool exhausted → sync + free) or by reclaim_completed() / drain_held_recv() once its event fires, with no missing-free or double-free scenarios. The max_inflight cap is correctly clamped to kMaxWorkloadInflight (63), one below the 64-event pool, so record_event() never starves in steady state. DCO sign-offs are present on all 21 commits.

No files require special attention.

Important Files Changed

Filename Overview
examples/rdma_bench.cpp Replaces the per-batch flush approach with CUDA-event recycling: each received buffer is stamped with an event and freed only once event_done() returns true, eliminating the per-message blocking stall. All BurstParams free paths are correctly handled — the ev==nullptr fallback syncs the stream and frees immediately; the normal path defers to reclaim_completed() / drain_held_recv(). max_inflight cap is correctly clamped to kMaxWorkloadInflight (< the event pool) so the pool never starves in steady state.
scripts/setup_spark_wire_loopback_netns.sh New autodetect_ports() groups carrier-up RoCE netdevs by phys_port_name, picks the lowest-BDF representative per physical port, and requires exactly 2 ports to produce a valid client/server pair. Robust to PCIe re-enumeration and interface renames.
examples/bench_workload.cu Adds parse_workload_gemm_dim/fft_len/sync_interval/max_inflight parsers; pins the GEMM side length so FLOP count per call is fixed; adds record_event/event_done/wait_event/release_event using a lazily-created 64-event pool.
examples/run_spark_bench.sh Adds GEMM_DIM, FFT_LEN, SYNC_INTERVAL, MAX_INFLIGHT, NSYS, RDMA_BUDGET_GIB, RDMA_RX/TX_NB env vars. Fixes socket port-collision, adds separate send/receive core pinning, RoCE one-way mode and wire-transit phy check.
examples/daqiri_bench_rdma_tx_rx_spark_netns.yaml Adds send: false / receive: true on the server and send: true / receive: false on the client, making RoCE one-way by default to match DPDK and socket benches.
docs/benchmarks/performance-dgx-spark.md Comprehensive refresh to phy-verified 100 GbE numbers: corrects RoCE/DPDK peaks, rewrites multi-queue section, updates TCP/UDP tables, replaces workload batch-size sweep with fixed-n GEMM comparison table.

Reviews (4): Last reviewed commit: "#15 - Pin socket send/receive to separat..." | Re-trigger Greptile

Comment thread examples/raw_bench_common.cpp
Address Greptile doc-sync review on PR #223:

- Bench usage strings listed only --workload-gemm-dim. Add the other workload
  flags each main actually parses: --workload-fft-len and --workload-sync-interval
  to all four, plus --workload-max-inflight to the rdma bench (RoCE event path).
- raw_benchmarking.md: note that --workload-gemm-dim / --workload-fft-len pin the
  GEMM side length / FFT transform length so the per-call FLOP count is fixed
  across the I/O sweep.
- run_spark_bench.sh: GEMM_DIM / FFT_LEN validation accepted 0 (^[0-9]+$) despite
  the "positive integer" message; tighten to ^[1-9][0-9]*$.

(The review's claim that the docs still document --workload-batch-bytes is stale --
that flag was already removed; no references remain in the tree.)

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
@RamyaGuru
RamyaGuru requested a review from cliffburdick July 10, 2026 15:36
| Raw Ethernet / GPUDirect | 4 KB packet | **105.5 ±0.9 Gb/s** | 0 | 98.5 Gb/s single-queue at the 8 KB native shape |
| Socket / RoCE (SEND) | 8 MB message | **102.2 ±0.3 Gb/s** | 0 | Single QP, batch 1 |
| Raw Ethernet / GPUDirect | 4 KB packet | **105.5 ±0.9 Gb/s** | 0 | 98.5 Gb/s single-queue at 8 KB |
| Socket / RoCE (SEND) | 64 KB message | **106.0 ±0.1 Gb/s** | 0 | Single QP, batch 1 |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just artificially say this is 100Gbps since it could be confusing to people without a footnote. The Spark networking is very strange so 106Gbps could look bad in some cases if people think it's a 200G link.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this was it :/ I had grabbed a second QSFP from the office that turned out to be 200 instead of 100 Gps, and think I must have been running with that cable for some time. I'm so sorry for the confusion! I refreshed all numbers in this doc with a vetted 100G link. It reproduced most of the old numbers, but this time saturated at wire rate properly instead of giving > 100 Gbps throughput values.
I think the old report was capping ~108 Gbps on the 200G link because I was measuring unidirectional traffic only p0 --> p1?

| Core | Busy% | Note |
| -------------------- | ----: | ----------------------------------------------- |
| Master (CPU 8) | 0.7% | Orchestration only |
| Client TX (CPU 17) | 74.8% | Post-and-poll spin; rate-independent |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does rate-independent mean here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think meaning that the CPU % for polling CPUs is not dependent on the data rate on the wire? But I removed that phrase because I don't believe it was something I actually tested for.

Comment thread examples/bench_workload.h Outdated

// Parse "--workload-fft-len N" from argv: the 1-D C2C transform length. The burst
// working set is fanned out across as many batched length-N transforms as fit, so
// N sets the per-transform cost (~5*N*log2(N) flops) while the batch count tracks

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That equation only works for powers of two, so we may want to restrict it or remove that comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Removed comment for now.


| Workload | DPDK (Raw / GPUDirect) | RoCE (RC) |
| -------- | ---------------------: | --------: |
| none (baseline) | 98.4 ±0.2 | 108.0 ±0.1 |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is surprising to me. Do we know why the packet reception went down by 5% when a workload was enabled? I would think given the memory bandwidth it would be unaffected.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a clear explanation yet. I removed speculative explainer text in the report. I think another data point on IGX or RTX Pro server would help me see if it has something to do with unified memory on the Spark that I'm not handling right?

- examples/bench_workload.h: the --workload-fft-len comment gave the FFT cost as
  ~5*N*log2(N) flops -- the radix-2 estimate, valid only for power-of-two N; cuFFT
  uses different algorithms (and costs) for other lengths. Drop the equation so the
  comment holds for any N (r3560531760).
- performance-dgx-spark.md: "rate-independent" was undefined jargon (r3560468420).
  Reword the RoCE TX and DPDK poller CPU notes to say the busy% comes from the
  post-and-poll / poll-mode spin loop rather than the throughput, and add a sentence
  spelling out that the core stays near this level whether the link runs at 10 or
  100 Gb/s.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
The DPDK bench path already asserted per-cell wire transit via *_phy SerDes
counters; the RoCE path did not, so an on-chip eswitch loopback could inflate a
RoCE number above the 100GbE line rate undetected. Add the same check to the RoCE
path: a netns-aware phy_counter reads the server rx_packets_phy and client
tx_packets_phy inside the dq_wire_{server,client} loopback namespaces around each
cell.

Also harden the threshold on all three checks (RoCE, DPDK, multi-queue). A bare
">0" let a stray background/control packet pass an on-chip run as "wire OK" -- a
+22 phy delta slipped through on a 23M-message run. Require the phy delta to reach
~half the message/packet count before certifying wire transit.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
Three bench-harness fixes surfaced while re-measuring the DGX Spark sockets and
multi-queue cells over the wire:

- run_spark_bench.sh: generate_socket_yaml's local_addr/remote_addr port rewrite
  required a quoted "tcp://..." URI, but gen_spark_netns_config.py emits it
  unquoted (PyYAML strips the quotes), so the substitution silently missed. Every
  multi-pair TCP/UDP cell then reused one port -- both servers bound the same port
  -> 'Address already in use', a dead pair, and a wedged sweep. Make the quote
  optional in all three URI sed patterns. (Overlaps the fix/socket-netns-rx work.)
- run_spark_mq_bench.sh: LD_LIBRARY_PATH pointed at $BUILD_DIR, but the per-engine
  sub-libs (libdaqiri_dpdk.so.0 etc.) live in $BUILD_DIR/src, so the bench failed
  with 'cannot open libdaqiri_dpdk.so.0'. Put $BUILD_DIR/src first.
- plot_mq_payload_sweep.py: predated REPEATS>1 and plotted every rep as its own
  point; average reps per (cell, payload) so the line has one point per x.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
The loopback cable is 100 GbE, so the report's >100 Gb/s figures could not have
crossed the wire -- they were on-chip eswitch hairpin (and/or a since-swapped
200 Gbps cable). Re-measured every cell over the phy-verified p0<->p1 wire loop
(rx_packets_phy confirmed to advance) and corrected the numbers:

- RoCE SEND: 106.0 -> 97.6 peak (8MB/1MB/64KB all ~97; small cells unchanged).
- DPDK sweep: 105.5@4KB -> ~98.8 peak; 1KB 92 -> 97 (stale low); best case now
  ~98.8 @ 4-8KB.
- Multi-queue: the '2nd TX core scales to 108.8' story was pure on-chip -- over
  the wire one queue already saturates (~97-99), and cores only help small,
  packet-rate-bound payloads via RX (64B 20->27, 256B 50->66). Section rewritten,
  payload-sweep plot re-rendered.
- GPU workload: RoCE none 108.0 -> 96.6 (now ~= DPDK 98.7); table + prose redone.
- Sockets (re-run after the port-collision fix): TCP 4-pair 97.2 -> 90.6,
  UDP 4-pair 29.8 -> 28.5.
- SUT table now states the 100 GbE link rate.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
…eams

The socket sweep co-pinned a pair's send (client) and receive (server) processes
on one core. A shared send/receive core ping-pongs a single stream and can wedge it
at half rate for an entire run -- the 8 KB (~1 MSS) cells were bimodal (~15 vs
~30 Gb/s, std +-8.6). Pin the two sides to SEPARATE isolated cores, kept in the
SAME CPU cluster (pairs 0-1 in 15-19, pairs 2-3 in 5-9): straddling GB10's two
big-core clusters makes every payload pay cross-cluster cache-coherence latency and
regresses large messages, so intra-cluster placement is required. Single-stream
variance drops sharply; the only cost is the 4-pair cells, whose eight cores must
span both clusters (the 5-9 pairs sit farther from the NIC), so they scale slightly
sub-linearly. Add SOCKET_NOPIN=1 (cpu_core -1, unpinned) for the pinned-vs-scheduler
comparison; not used in the report.

Refresh the TCP/UDP tables, summary rows, methodology prose, and the SUT
core-pinning row in performance-dgx-spark.md to the re-measured split numbers.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: rgurunathan <rgurunathan@nvidia.com>
| Socket / RoCE (SEND) | 64 KB message | **97.6 ±0.1 Gb/s** | 0 | Single QP, batch 1 |
| Socket / TCP | 1 MiB × 4 pairs | **90.6 ±1.5 Gb/s** | ~0 | Flow-controlled (App TX = App RX) |
| Socket / UDP | 8 KB × 4 pairs | **28.5 ±0.6 Gb/s** | ~54% loss | Receiver goodput; unpaced sender |
| Socket / TCP | 8 KB × 4 pairs | **87.3 ±2.2 Gb/s** | ~0 | Flow-controlled (App TX = App RX) |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These numbers are still backwards to me. We probably should investigate at some point

@RamyaGuru
RamyaGuru merged commit b3fc8d8 into main Jul 13, 2026
3 checks passed
chloecrozier added a commit that referenced this pull request Jul 14, 2026
@RamyaGuru
RamyaGuru deleted the roce-bench-unidir-and-profiling branch July 16, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants