MORI-IO nixl-style cpp bench script - #504
Conversation
There was a problem hiding this comment.
💡 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".
| // 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)); |
There was a problem hiding this comment.
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 👍 / 👎.
| # 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
60f0ddc to
10c71ea
Compare
|
Before continuing the review, I’d like to request two structural changes:
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. |
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>
10c71ea to
f953941
Compare
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>
f953941 to
803f9cb
Compare
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>
803f9cb to
963616d
Compare
- --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>
bench(io): nixlbench-matching C++ engine benchmark
Adds
tests/cpp/io/bench_engine.cpp, a native C++ MORI-IO transfer benchmarkwhose measurement loop matches nixlbench
(NVIDIA NIXL's
xferbench), so MORI-IO and NIXL RDMA numbers are directlycomparable on the same fabric with no Python-interpreter overhead in the way.
Applies directly to
main. The only files touched are the new benchmark, itsCMake target, and
docs/MORI-IO-BENCHMARK.md; no existing source is modified,and the Python benchmark is untouched.
What "nixl-matching" means here
total_timer), not the per-iterationtime.time()sum the Python bench uses,which silently drops the inter-iteration gap.
total_us / (iters x batch), matching nixl'savg_latency = total_duration / (per_thread_iter x batch_size).msg x batch x iters / 1e9 / (total_us / 1e6)— GB = 10^9, as innixl.
getXferStatusscan, so no condition-variable wakeup latency lands in themeasurement.
nixl
--pipeline_depth 1.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-GPUtransfers, sized by
--num-streams/--num-events.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-validateopts 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.
mori::application::SocketBootstrapNetworkrather than asecond hand-rolled socket setup, so connect/accept retries, timeouts and
barriers follow the library's policy.
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, sessionfast path, per-side memory type,
--target-dev-offset, and the QP tuningknobs.
throw rather than calling
std::exit(), so the MR is dropped before the pagesit covers are released on every exit path.
looped on forever or reported as a NaN, and
-h/--helpprints a grouped flaglist.
Results
Broadcom Thor2
bnxt_reRoCE, 2 nodes, gfx950. VRAM->VRAM WRITE, 1 QP / 1worker thread, chunking off,
--batch-contiguous, session enabled. Bandwidthin GB/s. Single run per point — small deltas are directional.
benchmark.pybench_engineThe 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
Start rank 1 (target) first, then rank 0, which drives the transfers and
prints the table. Both ranks take the same workload arguments:
bench_engine --helplists every flag, anddocs/MORI-IO-BENCHMARK.mdhas thefull walkthrough plus the Python-to-C++ flag mapping.
Test plan
libmori_io+libmori_application(
-DBUILD_EXAMPLES=OFFskips the MPI-only examples target).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.