MORI-IO: CPU hot-path improvements (profile-guided) - #518
Conversation
MORI IO improvements guided by prof removed spinlock fixes cmake fix improving fixes
65dad6c to
13f6fcd
Compare
There was a problem hiding this comment.
Pull request overview
This PR reduces CPU overhead in MORI’s RDMA I/O hot paths (CQ polling, submission tracking, and telemetry) by removing per-iteration locking/allocations and making logging/timing near-zero-cost when disabled.
Changes:
- Replaces the RDMA submission ledger’s per-WR unordered_map churn with a fixed-size ring buffer keyed by
recordId. - Avoids per-spin endpoint snapshot rebuilds by caching the snapshot and refreshing only when an epoch counter changes; also removes per-poll notif-context locking by publishing a stable pointer once.
- Makes logging macros and
ScopedTimerfaster on hot paths via call-site logger caching and conditional timestamp sampling; addsMORI_LIKELY/MORI_UNLIKELYhelpers and new C++ tests.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
include/mori/utils/mori_log.hpp |
Adds ModuleLogger locking, call-site logger caching, and sampled ScopedTimer behavior. |
include/mori/core/utils/utils.hpp |
Fixes warpSize macro collision under host compilation; adds MORI_LIKELY/UNLIKELY. |
src/io/rdma/ledger.cpp |
Implements allocation-free ring-based SubmissionLedger. |
src/io/rdma/common.hpp |
Updates SubmissionLedger API/storage and adds EndpointRuntime::notifCtx cache. |
src/io/rdma/backend_impl.hpp |
Adds snapshot type, epoch accessor, and epoch counter declaration. |
src/io/rdma/backend_impl.cpp |
Publishes notif context pointer, implements epoch-gated snapshot rebuild, wires maxSqDepth into ledger. |
tests/cpp/utils/test_logging.cpp |
Adds multithreaded stress tests for logging/thread-safety and first-touch behavior. |
tests/cpp/io/test_engine.cpp |
Adds wraparound/reuse coverage for the ledger ring behavior. |
tests/cpp/CMakeLists.txt |
Adds test_logging target and CTest registration. |
CMakeLists.txt |
Adds compiler-flag probing and warning suppression for deprecated fmt literal operator in spdlog consumers. |
Suppressed comments (1)
include/mori/utils/mori_log.hpp:430
- ScopedTimer::ElapsedSeconds() now uses start_ even when the timer is disabled; since start_ is only set when enabled_, this can return a huge nonsensical duration if ElapsedSeconds() is called while DEBUG logging is off.
double ElapsedSeconds() const {
return std::chrono::duration<double>(Clock::now() - start_).count();
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/io/rdma/ledger.cpp:25
std::abort()is used in this file, but<cstdlib>is not included. Relying on indirect includes forstd::abortis non-portable and can break builds depending on header ordering/toolchain.
#include <algorithm>
#include "mori/io/logging.hpp"
#include "src/io/rdma/common.hpp"
include/mori/utils/mori_log.hpp:450
MORI_TIMER/MORI_FUNCTION_TIMERcurrently expand to multiple top-level statements and inject_mori_timer_loggerinto the caller scope. This is fragile (e.g., breaksif (...) MORI_TIMER(...); else ...and can cause redeclaration errors if used more than once in a scope). Consider making each macro a single declaration statement and avoid introducing extra identifiers.
#define MORI_TIMER(name, module) \
static const std::shared_ptr<spdlog::logger> _mori_timer_logger = \
::mori::ModuleLogger::GetInstance().GetLogger(module); \
::mori::ScopedTimer timer_instance(name, _mori_timer_logger.get())
#define MORI_FUNCTION_TIMER(module) \
static const std::shared_ptr<spdlog::logger> _mori_timer_logger = \
::mori::ModuleLogger::GetInstance().GetLogger(module); \
::mori::ScopedTimer timer_instance(__PRETTY_FUNCTION__, _mori_timer_logger.get())
maning00
left a comment
There was a problem hiding this comment.
Thanks for the profiling and optimization work. I'm requesting changes because I don't think the current end-to-end evidence justifies the scope and risk of this hot-path rewrite.
At the moment, only mw-chunkon shows a measurable improvement (~2-7% in parts of the sweep). mw-chunkoff is mostly within noise, while hi-iops is slightly worse. Since the stated goal is reducing CPU overhead, bandwidth/latency alone are also insufficient: the PR does not report CPU utilization, cycles/op, or how much CPU capacity is actually released, and there is no per-change ablation.
There are also correctness/build blockers that must be addressed independently of performance:
- The ledger ring's capacity argument is invalid and can abort while the live-record count remains below
maxSqDepth. - Including
utils.hppfrommori_log.hppleaks thewarpSizemacro into HIP translation units and breaks valid header/include orders. - The logger synchronization work conflicts with and partially duplicates the fix already merged in #528.
Before reconsidering this PR, please:
- Rebase onto the latest
mainand retain the logger fix already merged there. - Fix the correctness/build blockers above.
- Preferably drop the ring rewrite and narrow the PR to the low-risk snapshot/notif-context/timer changes.
- Provide an ablation against the latest
main, including direct CPU-overhead metrics and repeated results on pre-selected representative workloads.
For the full hot-path rewrite, I would expect a material and repeatable benefit—roughly >=10% end-to-end improvement on a representative workload, or >=20% lower CPU cost at equal throughput—with no meaningful regressions elsewhere. If that cannot be demonstrated, I would prefer closing this PR rather than merging additional hot-path complexity for a marginal gain. A smaller follow-up PR containing only independently justified changes would be welcome.
Summary
Profile-guided reduction of per-operation CPU overhead on the RDMA IO path. A
CPU flame-graph of the completion/submission path surfaced several avoidable
costs on the busy-poll and submit hot paths: per-poll locking, per-spin heap
allocation + snapshot rebuilds, per-op hash-map churn in the submission ledger,
and always-on telemetry timing. This change removes all of them with no change
to the public API, ABI, or wire protocol.
The net effect is lower CPU cost per transfer, which shows up as higher
throughput (and correspondingly lower latency) in the CPU-overhead-bound
regimes, with no regressions elsewhere.
Extracted from #506 with minor adaptions
What changed
Lockless notif-context resolution in the CQ poller.
ProcessOneCqe()previously tookNotifManager::muon every poll just tolook up the endpoint's
QpNotifContext. The context is now published once atregistration (
EndpointRuntime::notifCtx, anatomic<void*>withrelease/acquire) and read locklessly in the poll loop. The backing map is
node-based, so the cached pointer stays valid across rehashes.
Epoch-gated endpoint snapshot (no per-spin lock/alloc).
The POLLING
MainLooprebuilt the endpoint snapshot every spin — taking ashared_lockand heap-allocating a vector on each iteration of a tightbusy-poll. A monotonic
endpointsEpoch_counter is bumped when the endpointset changes; the loop now caches the snapshot and only rebuilds when the
epoch moves.
SnapshotEndpointRuntimes()fills a caller-owned vector toavoid the per-call allocation.
Allocation-free submission ledger (ring instead of hash map).
SubmissionLedgerreplaced itsstd::unordered_map<uint64_t, SubmissionRecord>— which allocated/erased a node per WR — with a fixedpower-of-two ring indexed by
recordId & capMask_. Admission control boundslive records by
maxSqDepth, and the ring is sized strictly above that (withslack + floor), so a slot never aliases a live record.
Insert/ReleaseByCqe/ReleaseOrphaned*are now allocation-free; thesqDepthatomic update was also moved outside the lock.Sampled (near-zero-cost when off) telemetry timers.
ScopedTimer/MORI_TIMER/MORI_FUNCTION_TIMERnow capture timestampsonly when the module's debug logging is actually enabled, gated with
MORI_UNLIKELY. When telemetry is off the timers compile down to apredicted-not-taken branch, so they no longer cost anything on the hot path.
MORI_LIKELY/MORI_UNLIKELYbranch hints.Added portable
__builtin_expectwrappers(
include/mori/core/utils/utils.hpp) used on the cold paths above. Alsofixed a
warpSizemacro collision so the header parses under hostcompilation.
Benchmark setup
p20-05(initiator) ↔p20-14(target).across all ranks and reps).
34f17d69, "improved" = this change (13f6fcd9).avgBW/maxBWin GB/s (higher is better);avgLatin µs (lower isbetter).
ΔBW%positive = faster;ΔLat%negative = faster.Preset configurations
All presets are driven through
tools/run_telemetry_ab_cluster.sh→tests/python/io/benchmark.py. Common to all three:--op-type write,--enable-sess,--enable-batch-transfer,--num-initiator-dev 8,--num-target-dev 8, telemetry OFF, 8 initiator ranks.--disable-chunking)--disable-chunking)--num-worker-threads)--num-qp-per-transfer)--transfer-batch-size)--all--all--buffer-size 512--iters)What each stresses:
maximizing per-signaled-WR ledger-mutex round-trips and record churn.
the shared ledger/telemetry state; the most CPU-overhead-sensitive regime.
per transfer batch, maximizing completions/sec (completion-bound peak IOPS).
Exact benchmark argument vectors used for this dataset:
mw-chunkoff
2 workers, 4 QP, chunking OFF,
--iters 128. Less CPU-bound; results are withinrun-to-run noise — marginally positive at small sizes, flat elsewhere.
mw-chunkon
4 workers, 4 QP, chunking ON,
--iters 256. The CPU-overhead-bound regime andthe clearest win: +2–7% throughput (and matching latency reduction) across
almost all sizes; converges to the network limit only at the largest sizes.
hi-iops
512 B messages, 8 workers, 8 QP, batch 256, chunking OFF,
--iters 512.Completion-bound peak-IOPS probe; flat (within noise).
Takeaways
+2–7% bandwidth and a matching ~2–6% average-latency reduction across
the mid-size band the workload spends most time in.
regimes are less gated on host-CPU overhead.
Testing
tests/cpp/io/test_engine.cpp: added coverage for the ledger ring behavior.tests/cpp/utils/test_logging.cpp: new tests for the sampledScopedTimer.Risk / compatibility
maxSqDepthrounded up to a power of two, min 256 slots); relevant only with very large
maxSqDepth× many QPs.stable across rehash (documented at the publish site).
https://cursor.com/dashboard/shared-canvases?shareId=canvas-eghrnfdog48yKE5SGxxfUXeM