Repository navigation
[Store] Decouple NoF data path from SPDK behind neutral interfaces - #3424
Conversation
|
I have not finished reviewing the overall design yet. For now, I only helped run a few NoF experiments in Docker. The following are the test results and the issues I encountered while setting up the environment. Test environment
1. NoF worker-pool smoke testI ran The SPDK initiator successfully connected to the RDMA target, and the workload completed without I/O failures. 2. User-buffer registration testI ran a focused test for the The test used two adjacent 4 KiB buffers inside the same 2 MiB hugepage. Both addresses were intentionally not 2 MiB aligned. The test performed the following operations:
I did not observe the previous 3. Store-level end-to-end testI also ran the repository's The test used:
It wrote 1,024 4 KiB objects and then read back and verified all 1,024 objects. This exercised the following path: Issues encounteredMissing yalantinglibs dependencyA clean
I temporarily injected the yalantinglibs include path globally to continue testing. It looks like the target should declare the dependency directly: target_link_libraries(
mooncake_nof_spdk
PRIVATE
${SPDK_STATIC_LIBS}
glog::glog
yalantinglibs::yalantinglibs)IOVA mode inside the containerThe initiator initially failed with: For these experiments, I used the following local test-only change: opts.iova_mode = "va";The previous Scope and limitationsThese are experimental results only, not a complete design review or approval of the PR. The tests used an SPDK memory bdev and a same-host RDMA connection. They did not cover:
The end-to-end run was short and included cold-start costs, so its throughput and latency numbers should be treated as functional evidence rather than representative performance measurements. |
fcczzz
left a comment
There was a problem hiding this comment.
The abstraction direction looks reasonable, and the new SPDK experiments provide useful evidence for the normal #3131 path. I still see lifecycle gaps around timeout handling and memory registration: a timed-out probe can recycle an in-flight callback context, and client teardown does not release its process-global SPDK registrations. The global registry also needs ownership accounting when multiple initiators register the same address. I think these should be addressed before merge.
aeb56bd to
8778afb
Compare
Split the monolithic SpdkWrapper singleton into two role-based interfaces under include/nof/: - NVMeoFInitiator: byte-oriented segment I/O behind an opaque NofSegmentHandle, with NofIOAdaptor callbacks and NofCapabilities - DmaBufferAllocator: hugepage DMA allocation with an object-identity alloc/free mirror contract (replaces the use_spdk_dma flag) All SPDK headers/types are confined to a pimpl implementation in the new mooncake_nof_spdk static library; SPDK/DPDK static libs no longer propagate as usage requirements of mooncake_store. SPDK-free NoF support code (NofPageRegistry, NofProbeContextPool, SpdkControllerConfig) lives in mooncake_nof_core, built in every configuration so their policies are unit-testable without SPDK. Details: - transfer_task: NofWorkerPool takes an injected initiator; io_count becomes shared_ptr<atomic<int64_t>> (fixes the worker stack-pointer lifetime hazard); a failed SubmitIO returns the sub-task to its pool; QoS caches block size instead of querying per poll loop - real_client: SPDK env initializes lazily (setup no longer fail-fasts); the DMA allocator is injected only when NoF is available, preserving VRAM allocation priority in non-NoF builds; register_buffer_internal registers user buffers with the initiator and rolls back the TE registration on failure (kvcache-ai#3131); teardown unregisters every buffer the client registered - spdk_initiator: refcounted SpdkEnvGuard pins the SPDK env from the first successful init until process exit — SPDK v23.01.1 rejects a non-NULL-opts spdk_env_init after spdk_env_fini with -EINVAL, which made every Acquire() fail after a client close/reopen cycle that never called RegisterMemory; init/fini stay serialized via the registry mutex. Process-global NofPageRegistry keeps per-2MB-page refcounts with -EBUSY external-page handling and pins the env; OpenSegment connects outside the controllers mutex; probe contexts are recycled only after results are read and quarantined on timeout until the stale callback runs; NVMe controller options come from SpdkControllerConfig; CPU affinity is saved/restored around spdk_env_init so rte_eal_init cannot pin the host process to one core - master probe path shares one function-static initiator (MasterService::DefaultProbeNofSegment, also the test-peer reset default) - MC_NOF_BACKEND selects a matched Initiator/Allocator pair at CreateNofRuntime(): "spdk" (default) or "none" (runtime-disable NoF in a USE_NOF build); unrecognized values warn and fall back - MC_NOF_IOVA_MODE=va opts into IOVA-as-VA for containers without physical-address access Tests: nof_page_registry_test, nof_probe_context_pool_test, nvmeof_initiator_test, nof_env_reacquire_test (close/reopen without RegisterMemory, the SpdkEnvGuard reacquire regression), and RealClientTest.TearDownWithoutUnregisterReleasesNofRegistrations.
8778afb to
c02b3c2
Compare
Resolve conflicts between the NoF/SPDK decoupling and mainline work: - Env-var structuration PRs (kvcache-ai#4166/kvcache-ai#4170/kvcache-ai#4203): keep the neutral NoF naming/refactor while adopting the client-owned config structs (NoFDebugConfig, NoFWorkerPoolConfig, FilereadWorkerPoolConfig); QoS byte-limit vars stay in transfer_task.cpp, matching main (open kvcache-ai#4171). - kvcache-ai#3665 embedded master + P2PHANDSHAKE metadata resolution compose with the added nof_initiator injection in Client::Create. - kvcache-ai#4070 TENT native API layered onto the refactored TransferSubmitter. - kvcache-ai#3717 (ShmHelper/receiver shm SPDK registration) is ported from the deleted SpdkWrapper singleton onto the NVMeoFInitiator interface and the process-global NofPageRegistry: registrations normalize to 2MB pages with per-page refcounts, -EBUSY pages are absorbed as external, and a failed Register owns no registration. ShmHelper's singleton is now deliberately leaked so exit-time cleanup can never touch a torn-down registry/env; the SpdkEnvGuard process pin makes the old construction-ordering hack unnecessary.
A failed NVMeoFInitiator::RegisterMemory no longer rolls back the transfer-engine registration and fails register_buffer. A buffer SPDK cannot translate (plain 4 KiB pages, device memory) can never serve NoF-over-RDMA I/O anyway, and hard-failing broke the supported plain-page / pure-RDMA registration contract in USE_NOF builds (the real_client_regular_page_register_test assertions and the BufferPool posix_memalign overflow path), as well as VRAM buffers registered for transfers that never touch NoF. The failure is now logged as a warning at registration time, matching the ShmHelper/shm paths' non-fatal policy; the kvcache-ai#3131 fix itself is unchanged (hugepage-backed buffers still register successfully). Also: - add RealClientTest.RegisterBufferSucceedsWhenNofRegistrationFails - drop the stray unused kMaxTenantQuotaEvictionRetries constant and the unused USE_HTTP transfer_metadata_plugin.h include from master_service.cpp - fix the NoF environment-table parameter count in the deployment guide and document the non-fatal semantics
|
The interface split looks reasonable, but I see three issues that should be addressed before merge:
spdk_mem_register() can return an error after marking the page registered or installing translation state. NofPageRegistry::Register() rolls back only previously successful pages, leaving the failing page untouched. A later retry can receive -EBUSY and incorrectly classify that page as externally owned. Please clean up the failing page too, retaining explicit bookkeeping when cleanup cannot be confirmed.
ReleaseRangeLocked() logs backend errors but removes the page record anyway, and Unregister() subsequently erases the owner record and returns OK. This bypasses the ShmHelper/RealClient quarantine checks and allows munmap() while DMA translation state may remain. Please propagate the failure and preserve enough state for safe cleanup or quarantine.
The new Client::Create() parameter defaults to nullptr, and that value is forwarded without creating a default runtime. Existing callers still compile, but no NoF worker pool is created even with USE_NOF=ON, so NoF transfers fail. Please preserve the existing default behavior while keeping an explicit way to disable NoF or inject another initiator. Please add regression coverage for registration failures after partial state changes, backend unregistration failures, and NoF transfers through the existing C++ creation API. |
|
Thanks @fcczzz for the thorough review. The remaining findings are addressed in the latest commit:
The tent CI failure is pre-existing breakage on main: #4314 added GrantLeasePolicy to BatchProbeKey but missed the call site in the master_service DSL scenario. The one-line fix is posted separately on PR #4729. Could you take another look when you have time? |
… guard Engine: ERR_ADDRESS_NOT_REGISTERED from an already-cleaned transport is no longer treated as fatal in unregisterLocalMemory(Batch). Transports share one metadata instance with one BufferDesc per transport, so after a partial failure a retry always finds sibling transports already cleaned; treating that as an error kept the engine's region record stuck forever. The never-registered contract is preserved: when no transport cleaned anything, NOT_REGISTERED is still returned (and the RdmaTransport batch aggregation reports it only when nothing was cleaned, while keeping the best-effort republish on all paths). spdk_initiator: SpdkEnvGuard's Registry()/ProcessPin() are now deliberately leaked like RegistryMutex(). A probe-only initiator never registers memory, so the NofRegistryBundle is first constructed by the ~Impl backstop during static destruction, after those function-local statics are already destroyed; its constructor's Acquire() touched destroyed statics (latent UB). spdk_env_fini never runs now in any build; the OS reclaims DPDK resources at exit. RealClient::setup_internal refuses to re-initialize while a previous teardown left un-released NoF registrations or quarantined shms: the fresh initiator's owner token cannot release them, so a later teardown would no-op unregister, erase the records, and falsely report success (or munmap a quarantined VA with a live translation). nvmeof_initiator.h: clarify that the non-STUCK failure contract covers only the failed call; an earlier, smaller registration being extended stays live and still needs its own UnregisterMemory. Regression coverage: multi-transport single and batch retries with an already-cleaned sibling transport, and the never-registered contract for both engine paths.
… guard The engine-level NOT_REGISTERED tolerance added in the previous commit is unsafe: the invariant "NOT_REGISTERED implies no live MR" is only guaranteed for RdmaTransport, whose pending_dereg gate distinguishes a confirmed-cleaned retry from a never-registered address. Transports without such a gate (e.g. EFA, which removes BufferDesc before ibv_dereg_mr and reports NOT_REGISTERED on retry even when the MR is still live) would have a live MR treated as cleaned, letting the engine erase its record and the caller free or remap the VA. Revert unregisterLocalMemory/unregisterLocalMemoryBatch to their fail-stop form. Multi-transport partial-failure retry therefore returns to the pre-PR safe behavior (record kept, re-registration refused); lifting it needs per-transport pending gates and batch tolerance for TCP/CXL/SHM, deferred to a follow-up. Keep the RdmaTransport-internal batch normalization: within one transport the pending_dereg gate does prove which members are confirmed cleaned, so a retry batch mixing pending and already-cleaned members can safely report success. Also narrow the setup_internal re-init guard: refuse re-init only when nof_buffer_cleanup_failed_ is set (a previous teardown actually failed to release a NoF registration) or a quarantined shm exists, instead of refusing whenever any register_buffer record survives teardown, which wrongly rejected non-NoF clients.
unregisterLocalMemoryBatch defers the metadata publish to the end of the batch, but members cleaned in the call have already dropped both their metadata entries and their pending_dereg_ records. If that deferred publish fails, a retry finds nothing: removeLocalMemoryBuffer reports NOT_REGISTERED, no pending record matches, the batch reports NOT_REGISTERED, and the engine (which treats NOT_REGISTERED as fatal) keeps the region record stuck and un-re-registerable. Collect the members cleaned in this call and re-seed their pending records when the deferred publish fails. A retry then falls through the pending gate, re-runs the MR cleanup as a no-op (the MRs are already gone; RdmaContext::unregisterMemoryRegion returns 0 for a missing MR), and re-attempts the publish at the end of the batch.
Three defensive fixes from the boundary review: - NofPageRegistry::Register now rejects nullptr/zero-size ranges up front instead of inserting a phantom record (or touching page zero through the injected backend). Production callers were already guarded via NofRangeIsRegistrable; this covers the registry's standalone test surface. - NofRangeIsRegistrable restores the 256TB address-window check from the old SpdkWrapper::IsRegistrableRange, so an out-of-window address is skipped up front instead of failing inside spdk_mem_register and landing in quarantine. - Serialize spdk_nvme_probe/spdk_nvme_detach process-wide via a shared leaked mutex. The pre-refactor SpdkWrapper singleton serialized them through its instance mutex; per-instance initiators dropped that guarantee, so an embedded master's probe could race a worker's first open or another instance's teardown. The lock is held only around the SPDK calls (lock order ctrlrs_mutex_ < NvmeCtrlrStateMutex), so a slow probe still cannot stall cache lookups.
|
This PR has gone through many rounds of iteration and self-review, and the remaining edge cases are now addressed:
I've re-reviewed the whole diff end-to-end and the remaining open items are pre-existing behaviors (multi-transport @fcczzz Could you take another look when you have a chance? |
fd7a2b0 to
382e951
Compare
The PR changed registered_buffer_sizes_ from unordered_map<void*, size_t> to unordered_map<void*, BufferRegistrationRecord>; the test merged from main (kvcache-ai#3431) still assigned a bare size_t. Use aggregate initialization.
|
Validation on head 991f655 confirmed that the NoF-over-RDMA runtime path works with SPDK v23.01.1 and a same-host RDMA malloc-bdev target. Pure-NoF Python API round trips, overlapping hugepage-slice registrations, client recreation, and concurrent batch workloads passed data verification. The focused tests also passed: 84/84, with no skips. One build blocker remains: the unmodified USE_NOF=ON, BUILD_UNIT_TESTS=ON build fails on the ambiguous construction Please commit that fix and confirm the full build on the resulting SHA. With that addressed, I have no remaining blocking concerns within the reviewed and tested scope. |
Under USE_NOF=ON + BUILD_UNIT_TESTS=ON the nested braced-init-list
NofMetric metric({{"client", "test"}}) is ambiguous; spell out the
std::map parameter type explicitly. Test-only change.
|
Thanks @fcczzz for your patience. Fixed — committed as |
fcczzz
left a comment
There was a problem hiding this comment.
Build and validation passed on my own machine. No blocking findings from me; please merge the latest main and resolve the conflicts.
…-spdk # Conflicts: # docs/source/getting_started/build.md
|
Latest main has been merged and conflicts resolved. |
|
@alogfans Hi, could you review the changes in rdma_transport.cpp? They affect generic RDMA unregister behavior—including partial failures, retries, chunked buffers, and batch cleanup—even when NoF is disabled. The focused tests passed, but I'd appreciate TE-owner confirmation of the cleanup and concurrency semantics before merging. |
|
I do not have a concrete code change to request at this point. However, I am holding approval because the changes in The focused tests and current CI are green, but I would like the Transfer Engine owner to confirm the cleanup and concurrency semantics before this PR is considered ready to merge. If that review finds no issue, no additional change is required from my side. |
|
Hi @ykwd. This PR seems to be good to all the reviewers involved. If you have a moment, could we get this merged? Thanks! |
ykwd
left a comment
There was a problem hiding this comment.
Thanks for this great work!
Main moved the NoF data path behind the NVMeoFInitiator interface (kvcache-ai#3424): NofTask is byte oriented, NofOperationState replaces SpdkNofOperationState, and DMA memory comes from the DMA allocator instead of SpdkWrapper. Port the padded layout onto that: segments are submitted as byte ranges, bounce buffers come from hugepage_memory_alloc, and the staged state keeps the first error detail. Keep main's SGL relaxation by passing the initiator's pointer alignment into the layout, so a DWORD aligned buffer still goes direct. Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
Description
Related RFC #3397
Decouple Mooncake Store's NoF (NVMe-oF) data path from SPDK behind two neutral interfaces, and fix #3131 along the way.
Before: all NoF traffic funneled into the
SpdkWrapperMeyers singleton (env init + qpair cache + I/O + DMA alloc + probe in one class). SPDK types leaked through its header, and 38 SPDK/DPDK static libs propagated as PUBLIC link requirements to client/master/bench.After:
include/nof/:NVMeoFInitiator(byte-oriented I/O behind an opaqueNofSegmentHandle) andDmaBufferAllocator.mooncake_nof_spdkstatic lib (pimpl — SPDK types confined to one.cpp, SPDK/DPDK libs linkedPRIVATE).CreateNofRuntime()— the only#ifdef USE_NOFsite naming SPDK types — and are injected viashared_ptr. The SPDK env initializes lazily on first use and is owned by a refcounted guard.register_buffernow also registers user buffers with the SPDK RDMA translation table (process-global registry, per-2MB-page refcount, TE rollback on failure) — fixes [Bug]: NoF 写入失败:spdk_rdma_get_translation: No translation for ptr是否是我配置的不对导致的这个问题? #3131. Requires SPDK ≥ 22.05; a CMake probe warns on older versions.No wire-protocol / RPC / Python ABI changes. Non-NoF builds keep the exact original behavior (allocator injected as
nullptr→ original protocol → VRAM →aligned_allocorder).flowchart TB subgraph B["Before — everything funnels into one singleton"] TS1["TransferSubmitter / worker pool"] -->|GetInstance ×6| SW["SpdkWrapper (Meyers singleton):<br/>env init · qpair cache · IO · DMA alloc · probe"] RC1["RealClient::setup"] -->|eager InitializeEnv| SW CB1["ClientBufferAllocator / utils"] -->|use_spdk_dma flag| SW MS1["Master heartbeat probe"] --> SW SW -.->|SPDK types in header,<br/>38 SPDK/DPDK libs PUBLIC| L1["leaks into client / master / bench"] end subgraph A["After — interface split + dependency injection"] RC2["RealClient::setup"] -->|CreateNofRuntime:<br/>only USE_NOF ifdef| F["NofRuntime factory"] F --> NI["NVMeoFInitiator «interface»"] F --> DA["DmaBufferAllocator «interface»"] NI -.pimpl.- SI["SpdkInitiator::Impl — SPDK confined<br/>to one .cpp, libs PRIVATE"] DA -.-> SA["SpdkDmaAllocator /<br/>SystemDmaAllocator"] TS2["TransferSubmitter / NofWorkerPool"] -->|injected shared_ptr| NI CB2["ClientBufferAllocator"] -->|injected; nullptr when NoF absent| DA MS2["Master heartbeat probe"] --> NI endLifetime and registration fixes riding along:
flowchart LR subgraph LIB["mooncake_nof_spdk — the only SPDK-aware code"] G["SpdkEnvGuard:<br/>lazy, refcounted,<br/>init/fini serialized"] R["NofPageRegistry: process-global,<br/>per-2MB-page refcount,<br/>-EBUSY → external page"] Q["ctrlr/ns/qpair cache:<br/>same endpoint → same qpair,<br/>connect outside mutex"] G --- R G --- Q end RB["RealClient::register_buffer"] -->|1. TE register OK| R R -->|2. RegisterMemory fail → roll back TE| RB W["NofWorkerPool"] -->|SubmitIO bytes /<br/>PollCompletion| QAlso fixed in passing:
io_countwas a raw pointer to a worker-thread stack variable (nowshared_ptr<atomic<int64_t>>); a failedSubmitIOleaked its sub-task pool slot (now returned); the probe context recycle race;OpenSegmentblocked a global mutex during NVMe-oF connect.Upgrade notes for existing NoF users
No breaking configuration or API changes: build flags (
-DUSE_NOF=ON), existing env vars (MC_NOF_*,MC_NVME_*,MC_STORE_NUMA_SOCKET_ID), transport strings, and the Python ABI are all unchanged. One optional env var is added. Behavioral differences to be aware of:MC_NOF_BACKEND(spdkby default, case-insensitive): selects the NoF backend pair. Set it tononeto disable NoF at runtime in aUSE_NOFbuild — one binary can be deployed on nodes without a working SPDK/hugepage environment. Unrecognized values fall back tospdkwith a warning. Note the master process honors it too (withnone, heartbeat probes fail and NoF segments are unmounted after the threshold).RealClient::setupused to initialize the SPDK env eagerly and fail when hugepages were missing — even for clients that never touch NoF. Setup now always succeeds; a broken SPDK env surfaces as an error on the first NoF operation instead. Keep configuring hugepages before NoF traffic, as the deployment guide says.register_bufferregisters with SPDK automatically — no extra call or parameter. The buffer must be block-size aligned (existing hard gate) and must live in hugepage-backed memory (2MB hugetlb). The pointer itself need not be 2MB-aligned — a non-aligned buffer is registered by its containing 2MB pages (logging aRegisterMemorywarning); but 4KB-backed malloc memory fails inside SPDK'sspdk_mem_registerwith paddr-misalignment-EINVAL.spdk_rdma_get_translation: No translation for ptr是否是我配置的不对导致的这个问题? #3131 fix needs SPDK ≥ 22.05 (spdk_mem_register). Older SPDK still builds (with a CMake warning) and behaves exactly as before — i.e. NoF-over-RDMA writes from unregistered user buffers keep failing with "No translation".Use cases
1. Pure-NoF write to the SSD pool (the #3131 scenario — now works over RDMA):
Requirements:
USE_NOF=ONbuild against SPDK ≥ 22.05; the buffer must be block-size aligned (existing hard gate) and must be hugepage-backed (2MB hugetlb). The pointer itself need not be 2MB-aligned —register_buffernormalizes it to its containing 2MB pages, logging aRegisterMemorywarning — but 4KB-backed malloc memory fails inside SPDK'sspdk_mem_registerwith paddr-misalignment-EINVAL.2. One binary, deployed everywhere — nodes without hugepages/SPDK no longer need a separate build:
3. Standard SSD-pool deployment (master + store service +
spdk_tgt_create+mooncake_ssd_register): unchanged — seedocs/source/deployment/ssd/nvmf-ssd-deployment-guide.md.4. Custom/test initiator injection (C++): any
NVMeoFInitiatorimplementation can be injected — directly intoNofWorkerPool, or via the new tail parameter ofClient::Create(...). The alignment gate, QoS, and worker loop run unchanged against it;nvmeof_initiator_testexercises the full pool this way without SPDK.What if the backend is not SPDK later?
That swap is the point of the split. A non-SPDK backend implements
NVMeoFInitiator(and optionallyDmaBufferAllocator) and plugs intoCreateNofRuntime()— the single place that names SPDK types. Callers (NofWorkerPool,RealClient, master probe) do not change. Backend differences are advertised throughNofCapabilities: e.g. a backend without an RDMA translation table returnsrequires_memory_registration = falseand itsRegisterMemoryis a no-opOK. The new test suite already runs the worker pool against a non-SPDKMockInitiator, proving the seam.Backend selection exists from day one as one env var choosing a matched Initiator+Allocator pair:
MC_NOF_BACKEND=spdk(default) ornone(runtime-disable NoF in aUSE_NOFbuild). Adding a backend is one enum value + one factory branch + one env value — deliberately no plugin machinery before a second backend exists.Module
mooncake-store)Type of Change
spdk_rdma_get_translation: No translation for ptr是否是我配置的不对导致的这个问题? #3131)How Has This Been Tested?
Test commands:
Test results:
USE_NOF=ONbuild and runtime NoF validation still pending — requires a machine with SPDK + an NVMe-oF target. Will run before marking ready (incl. non-2MB-aligned hugepage-slice registration and NoF-over-RDMA pure-NoF write from #3131's repro).Checklist
./scripts/code_format.shcode_format.sh --staged, whitespace/EOF checks)