Skip to content

MORI-IO nixl-style cpp bench script - #504

Open
amirakb89 wants to merge 7 commits into
ROCm:mainfrom
amirakb89:io-nixlstyle-cpp-bench
Open

MORI-IO nixl-style cpp bench script#504
amirakb89 wants to merge 7 commits into
ROCm:mainfrom
amirakb89:io-nixlstyle-cpp-bench

Conversation

@amirakb89

@amirakb89 amirakb89 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

bench(io): nixlbench-matching C++ engine benchmark

Adds tests/cpp/io/bench_engine.cpp, a native C++ MORI-IO transfer benchmark
whose measurement loop matches nixlbench
(NVIDIA NIXL's xferbench), so MORI-IO and NIXL RDMA numbers are directly
comparable on the same fabric with no Python-interpreter overhead in the way.

Applies directly to main. The only files touched are the new benchmark, its
CMake target, and docs/MORI-IO-BENCHMARK.md; no existing source is modified,
and the Python benchmark is untouched.

What "nixl-matching" means here

  • One whole-loop timer around the entire iteration loop (nixlbench's
    total_timer), not the per-iteration time.time() sum the Python bench uses,
    which silently drops the inter-iteration gap.
  • Latency per single transfer: total_us / (iters x batch), matching nixl's
    avg_latency = total_duration / (per_thread_iter x batch_size).
  • Bandwidth msg x batch x iters / 1e9 / (total_us / 1e6) — GB = 10^9, as in
    nixl.
  • Inline spin-poll completion in the benchmark thread, mirroring nixl's
    getXferStatus scan, so no condition-variable wakeup latency lands in the
    measurement.
  • Strict stop-and-wait: one request outstanding at a time, equivalent to
    nixl --pipeline_depth 1.
  • Warmup excluded, and memory registered once during setup — the buffer
    is sized to the largest request in the whole sweep and reused across every
    size point, so no allocation or registration happens inside the timed loop.

Also included

  • --backend rdma|xgmi, so the same harness covers intra-node GPU-to-GPU
    transfers, sized by --num-streams / --num-events.
  • Built-in correctness check after the sweep, matching the Python benchmark,
    which always validates. Both ranks seed a rank- and offset-dependent pattern
    and exchange one FNV-1a checksum per transferred slot, so the check covers
    every transferred byte while staying O(batch) on the wire. --skip-validate
    opts out and is safe to pass on a single rank, since the exchange is a
    collective and an opted-out rank contributes an empty vector rather than
    returning early.
  • Rendezvous over mori::application::SocketBootstrapNetwork rather than a
    second hand-rolled socket setup, so connect/accept retries, timeouts and
    barriers follow the library's policy.
  • Python-parity flags so runs can be matched arg-for-arg against
    tests/python/io/benchmark.py: size and batch sweeps (--all, --all-batch,
    --buffer-size, --sweep-start/--sweep-max/--sweep-step),
    --enable-batch-transfer / --disable-batch-transfer, chunking, session
    fast path, per-side memory type, --target-dev-offset, and the QP tuning
    knobs.
  • The buffer and its memory registration are owned by a scope guard and failures
    throw rather than calling std::exit(), so the MR is dropped before the pages
    it covers are released on every exit path.
  • Unusable arguments are rejected up front instead of being read out of bounds,
    looped on forever or reported as a NaN, and -h/--help prints a grouped flag
    list.

Results

Broadcom Thor2 bnxt_re RoCE, 2 nodes, gfx950. VRAM->VRAM WRITE, 1 QP / 1
worker thread
, chunking off, --batch-contiguous, session enabled. Bandwidth
in GB/s. Single run per point — small deltas are directional.

Harness 1 MiB x 1 4 KiB x 128 512 B x 128
Python benchmark.py 22.59 14.79 2.60
C++ bench_engine 24.64 16.46 2.98

The C++ tool reads +9.1% / +11.3% / +14.6% higher, and the gap widening as
messages shrink is what the methodology difference predicts: at 1 MiB the
transfer dominates, while at 512 B each iteration is mostly harness, so the
whole-loop timer, the absent interpreter overhead and the spin-poll instead of a
condition-variable wakeup all take a larger share. These are measurement
differences, not a claim that the transport got faster.

All C++ runs above passed the built-in validation.

Reproduce

cmake -B build -DBUILD_IO=ON -DBUILD_TESTS=ON -DBUILD_EXAMPLES=OFF
cmake --build build --target bench_engine -j
export LD_LIBRARY_PATH=$PWD/build/src/io:$PWD/build/src/application:$LD_LIBRARY_PATH

Start rank 1 (target) first, then rank 0, which drives the transfers and
prints the table. Both ranks take the same workload arguments:

# TARGET (rank 1) — first
build/tests/cpp/bench_engine --rank 1 --master-ip <RANK0_IP> --self-ip <RANK1_IP> \
    --port 18527 --mem-type gpu --op write --enable-sess \
    --num-qp-per-transfer 1 --num-worker-threads 1 --transfer-batch-size 1 \
    --all --sweep-start 1048576 --sweep-max 1048576 --iters 500 --warmup-iters 50

# INITIATOR (rank 0) — second, prints results
build/tests/cpp/bench_engine --rank 0 --master-ip <RANK0_IP> --self-ip <RANK0_IP> \
    --port 18527 --mem-type gpu --op write --enable-sess \
    --num-qp-per-transfer 1 --num-worker-threads 1 --transfer-batch-size 1 \
    --all --sweep-start 1048576 --sweep-max 1048576 --iters 500 --warmup-iters 50

bench_engine --help lists every flag, and docs/MORI-IO-BENCHMARK.md has the
full walkthrough plus the Python-to-C++ flag mapping.

Test plan

  • Builds and links against libmori_io + libmori_application
    (-DBUILD_EXAMPLES=OFF skips the MPI-only examples target).
  • 2-node VRAM WRITE run, batch 1 and batched, sane BW and latency, built-in
    validation passing across read and write, contiguous and strided offsets,
    chunking on and off, with a zero-iteration negative control confirming the
    check fails when nothing moves.
  • Reviewer sanity-check on a second fabric (CX7/mlx5).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c755fd822e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/cpp/io/bench_engine.cpp Outdated
// Target: nothing to drive. RDMA one-sided ops complete without target CPU.
// Just hold memory registered until the initiator says it's done.
Barrier(sock); // wait for initiator to finish the whole sweep
if (cpuMem) HIP_CHECK(hipHostFree(buf)); else HIP_CHECK(hipFree(buf));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Deregister memory before freeing its backing buffer

Both this target exit and the initiator exit free buf while localMem remains registered with the RDMA backend; the initiator may also still have a session referencing it. Once a transfer has caused the region to be registered with a NIC, freeing the HIP allocation first leaves the backend with a registration for invalid storage and defers cleanup until engine destruction, which can cause teardown failures or stale DMA mappings. Destroy the session where applicable and call engine.DeregisterMemory(localMem) before hipHostFree/hipFree on both paths.

Useful? React with 👍 / 👎.

Comment thread tests/python/io/benchmark.py Outdated
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from tests.python.utils import TorchDistContext
import numpy as np

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Declare or remove the new NumPy dependency

In an environment that previously had the documented benchmark prerequisites but not NumPy, the script now fails at startup on this import before it can parse any arguments. NumPy is not declared in pyproject.toml or requirements-build.txt, so users following the existing installation flow are not guaranteed to have it; add it to the applicable runtime/setup requirements or retain an implementation using the previously supported list inputs.

Useful? React with 👍 / 👎.

Comment thread tests/cpp/io/bench_engine.cpp Outdated
Comment on lines +78 to +94
static int TcpListenAccept(uint16_t port) {
int lfd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(lfd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
perror("bind");
std::exit(1);
}
listen(lfd, 1);
int fd = accept(lfd, nullptr, nullptr);
close(lfd);
return fd;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we reuse MORI's existing networking abstractions instead of introducing another raw-socket rendezvous implementation here?
mori::application::TCPContext and SocketBootstrapNetwork already provide connection lifecycle management, send/receive primitives, barriers, timeouts, and cleanup. This benchmark also links mori_application, so duplicating these responsibilities with raw file descriptors creates a second implementation with different retry, timeout, and error-handling behavior.
Please consider using the existing bootstrap layer, or extracting a small reusable rendezvous helper from it. That would also avoid manual close()/std::exit() paths and make resource ownership explicit.

Comment thread tests/cpp/io/bench_engine.cpp Outdated
@amirakb89 amirakb89 closed this Jul 30, 2026
@amirakb89 amirakb89 reopened this Jul 30, 2026
@amirakb89
amirakb89 force-pushed the io-nixlstyle-cpp-bench branch 3 times, most recently from 60f0ddc to 10c71ea Compare July 31, 2026 21:02
@maning00

maning00 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Before continuing the review, I’d like to request two structural changes:

  1. Preserve the existing RDMA path
    The current implementation refactors the regular RdmaBatchReadWrite path to share the new build/post logic. At this stage, prepared transfers should remain opt-in and isolated from the existing path, without adding overhead to normal Read/Write/BatchReadWrite calls.
    The existing RdmaBatchReadWrite implementation should therefore remain unchanged, while the prepared API should use a separate build/post path invoked only by --prepare-once. Only after prepared transfers have been thoroughly validated and shown not to regress the existing RDMA path should we consider refactoring the regular path to share this implementation.

  2. Split this PR into two focused PRs

    • Baseline C++ benchmark: benchmark executable, CMake integration, rendezvous, XGMI support, validation, and documentation, without prepared-transfer or RDMA core changes.
    • Prepared transfers: prepared-transfer API and implementation, dedicated tests, --prepare-once benchmark integration, and benchmark results. This implementation should remain isolated from the existing RDMA path.

Please also rebase after #444 is merged so its commits are not duplicated, and reorganize the commit history around the final logical changes. This will make the performance-sensitive core changes easier to review and verify independently.

amirakb89 pushed a commit to amirakb89/mori that referenced this pull request Aug 3, 2026
Rounds out the C++ benchmark so it stands on its own without touching the
IO core, addressing the review on ROCm#504:

- --backend xgmi alongside rdma, sized by --num-streams/--num-events.
- Correctness check after the sweep, matching the Python benchmark, which
  always validates. Both ranks seed a rank- and offset-dependent pattern
  and exchange one FNV-1a checksum per transferred slot, so the check
  covers every transferred byte while staying O(batch) on the wire.
  --skip-validate opts out and is safe to pass to a single rank, since the
  exchange is a collective and an opted-out rank contributes an empty
  vector rather than returning early.
- Rendezvous now runs over mori::application::SocketBootstrapNetwork
  rather than a second hand-rolled socket setup, so connect/accept
  retries, timeouts and barriers follow the library's policy.
- The buffer and its memory registration are owned by a scope guard, and
  failures throw instead of calling std::exit(), so the MR is dropped
  before the pages it covers are released on every exit path.
- Unusable arguments are rejected up front instead of being read out of
  bounds, looped on forever or reported as a NaN: a value-taking flag now
  requires a value, --rank must be 0 or 1 since it indexes the two-rank
  bootstrap, --iters and the message size must be non-zero, an empty sweep
  is refused rather than indexing past the plan, and an unrecognised --op
  or memory type no longer falls back silently to write/GPU.
- -h/--help prints a grouped flag list, as bench_umbp_micro does, and
  file-local helpers sit in anonymous namespaces to match the neighbouring
  tests/cpp/io/test_engine.cpp. HIP_CHECK is local and throws, following
  examples/collective/intra_node rather than the library's
  HIP_RUNTIME_CHECK, which exits the process and would leave the MR behind.
- numpy is declared in requirements-build.txt; the Python benchmark
  imports it to marshal batch descriptors.

Prepared (build-once, post-many) transfers are left out deliberately:
they need changes inside the RDMA executor, so they are proposed
separately on top of this.

Co-authored-by: Cursor <cursoragent@cursor.com>
@amirakb89
amirakb89 force-pushed the io-nixlstyle-cpp-bench branch from 10c71ea to f953941 Compare August 3, 2026 22:18
amirakb89 and others added 4 commits August 3, 2026 19:12
bench_engine is a pure-C++ cross-node MORI-IO benchmark whose measurement loop
matches nixlbench (NVIDIA NIXL xferbench): a single whole-loop timer, inline
spin-poll completion in the bench thread, GB=1e9 throughput, warmup excluded,
and pipeline depth (--inflight) equivalent to nixl --pipeline_depth. This makes
MORI-IO and NIXL RDMA numbers directly comparable on the same fabric.

Ports the MORI Python benchmark feature set: batching (strided default +
--batch-contiguous), --enable-batch-transfer, chunking (--chunk-bytes/--max-chunks),
session fast-path (--enable-sess), --busy-wait, --poll_cq_mode, --mem-type gpu|cpu,
--post-batch-size, and the max-send-wr/cqe/msg-sge QP knobs. Flag defaults match
the Python bench (enable-sess and busy-wait off by default).

Rendezvous is a tiny length-prefixed TCP socket exchanging msgpack-packed
EngineDesc/MemoryDesc; rank 0 initiator drives one-sided WRITE/READ, rank 1
target only holds memory registered.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Match benchmark.py: size/batch sweeps (--all, --all-batch,
--buffer-size, --sweep-start-size/--sweep-max-size/--sweep-step),
per-side memory type (--initiator-mem-type/--target-mem-type),
--target-dev-offset for cross-rail transfers, and --log-level.
- latency is now per single transfer (total / (iters*batch)), matching
  nixl's avg_latency = total_duration/(per_thread_iter*batch_size); BW
  unchanged (msg*batch*iters over one whole-loop timer).
- completion is always an inline spin (like nixl's getXferStatus scan);
  drop the --busy-wait flag, which only affected untimed warmup and would
  otherwise make latency non-comparable to nixl. Warmup now spins too.
- batching self-activates on --transfer-batch-size > 1 (one N-descriptor
  batch request); drop the redundant --enable-batch-transfer gate that
  silently made batch sweeps a no-op by default.
- simplify dead cfg.host ternary; document that --inflight > 1 reuses the
  same buffer window (fine for an unvalidated perf bench, matching nixl).
Restore the Python bench's batch/single submission toggle in the C++
nixl-style engine bench, and add C++ run docs to MORI-IO-BENCHMARK.md.

- --enable-batch-transfer / --disable-batch-transfer (default ON):
  - ON  => one N-descriptor batch request (BatchWrite/BatchRead), the
    nixl-equivalent path (nixl always batches).
  - OFF => N individual single-transfer submissions per iteration at
    contiguous offsets i*msg, matching Python's run_single_once.
  Default is ON (vs Python's OFF) so the out-of-the-box run and the
  --all-batch sweep stay nixl-comparable; this also fixes the old sweep
  no-op (OFF now scales with N instead of doing one msg-byte write).
- Strict stop-and-wait (nixl --pipeline_depth 1): one request outstanding
  at a time. perSlot statuses live in a flat status array (TransferStatus
  is non-copyable); a request completes only when all its sub-transfers
  finish, mirroring Python waiting on the whole status_list.
- Metrics unchanged/consistent across modes: both move curBatch transfers
  of msg per iter, so total_bytes = msg*batch*iters and per-transfer
  latency = total_us/(iters*batch).
- docs/MORI-IO-BENCHMARK.md: new "C++ Benchmark (nixlbench-matching)"
  section (build, 2-node run, sweep modes, Python->C++ flag mapping) and
  updated the --enable-batch-transfer row to describe ON vs OFF.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@amirakb89
amirakb89 force-pushed the io-nixlstyle-cpp-bench branch from f953941 to 803f9cb Compare August 4, 2026 02:54
amirakb89 pushed a commit to amirakb89/mori that referenced this pull request Aug 4, 2026
Rounds out the C++ benchmark so it stands on its own without touching the
IO core, addressing the review on ROCm#504:

- --backend xgmi alongside rdma, sized by --num-streams/--num-events.
- Correctness check after the sweep, matching the Python benchmark, which
  always validates. Both ranks seed a rank- and offset-dependent pattern
  and exchange one FNV-1a checksum per transferred slot, so the check
  covers every transferred byte while staying O(batch) on the wire.
  --skip-validate opts out and is safe to pass to a single rank, since the
  exchange is a collective and an opted-out rank contributes an empty
  vector rather than returning early.
- Rendezvous now runs over mori::application::SocketBootstrapNetwork
  rather than a second hand-rolled socket setup, so connect/accept
  retries, timeouts and barriers follow the library's policy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rounds out the C++ benchmark so it stands on its own without touching the
IO core, addressing the review on ROCm#504:

- --backend xgmi alongside rdma, sized by --num-streams/--num-events.
- Correctness check after the sweep, matching the Python benchmark, which
  always validates. Both ranks seed a rank- and offset-dependent pattern
  and exchange one FNV-1a checksum per transferred slot, so the check
  covers every transferred byte while staying O(batch) on the wire.
  --skip-validate opts out and is safe to pass to a single rank, since the
  exchange is a collective and an opted-out rank contributes an empty
  vector rather than returning early.
- Rendezvous now runs over mori::application::SocketBootstrapNetwork
  rather than a second hand-rolled socket setup, so connect/accept
  retries, timeouts and barriers follow the library's policy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@amirakb89
amirakb89 force-pushed the io-nixlstyle-cpp-bench branch from 803f9cb to 963616d Compare August 4, 2026 03:45
Akbarzadeh and others added 2 commits August 6, 2026 17:57
- --num-initiator-dev/--num-target-dev: one forked process per GPU, initiator i <-> target i
- --backend fabric: BackendType::FABRIC, GPU memory only
- --xgmi-single-process: both GPUs in one process, no rendezvous (Python's default xgmi mode)
- --host/--src-gpu/--dst-gpu: Python spellings of --master-ip/--gpu/--target-dev-offset
- extract RunSweep() so both drivers share the timed loop; every rank now runs one
  identical collective sequence, so no role-dependent path can hang its peer
- floored modulo for a negative --target-dev-offset; BenchBuffer restores its device
  before hipFree; --xgmi-single-process rejects --rank 1

Verified on 2 nodes x 8 MI355X: single device unchanged (24.55 GB/s @1MiB),
8 pairs aggregate 314 GB/s (7.9x), CPU+GPU and read+write all validate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build the native bench_engine on both internode nodes and run a GPU write
sweep (1->32 MiB, step 1 MiB) at batch 1 and batch 64, single-device,
200 iters. The existing MORI-IO CI only exercised the Python harness; this
adds coverage for the C++ benchmark path.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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