Repository navigation
Conversation
CLA Signature PassGinray, thanks for your pull request. All authors of the commits have signed the CLA. 👍 |
420233f to
5d32fc4
Compare
CLA Signature PassGinray, thanks for your pull request. All authors of the commits have signed the CLA. 👍 |
ce3daea to
90ad2c4
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
[AI Review]Summary. The direction is good: replacing the packed format with frame-native scatter WRITE is a net simplification. In its current form, though, I'd suggest addressing two issues before merging: a performance regression on the default ZMQ backend, and a failure mode where one uncertain transfer permanently disables a client ↔ storage-unit path with no recovery owner. A few reliability risks also need validation under realistic RL patterns, and the change would be much easier to review if split. Locally, Scope note. Against Should fix before merge1. Default ZMQ backend: GET responses are serialized twice. TransferQueue/transfer_queue/storage/simple_storage.py Lines 1187 to 1198 in 90ad2c4 TransferQueue/transfer_queue/storage/simple_storage.py Lines 1049 to 1050 in 90ad2c4 For GET this walks the whole payload twice on the single storage worker thread (non-contiguous tensors are copied by 2. One uncertain failure permanently disables a path, with no recovery. TransferQueue/transfer_queue/storage/payload_transfer/nixl.py Lines 343 to 347 in 90ad2c4
For memory safety, quarantining the one receive buffer is already sufficient: it is never reused, and later GETs use a fresh transfer_id and buffer, so a late WRITE can only land in the quarantined memory. GET is read-only, so it can also be retried after quarantine. Suggest dropping the per-target permanent failure in favor of a byte budget on quarantined memory (fail fast only when exceeded). On the sender side, either reset the remote agent or fail the job explicitly rather than leaving a half-working client. Reliability risks to validate3. On storage units, all stored data becomes registered (pinned) memory, retained per whole PUT. TransferQueue/transfer_queue/storage/simple_storage.py Lines 276 to 277 in 90ad2c4
The 1000-cycle validation runs PUT/GET/CLEAR in lockstep, which is the one pattern that hides this. (The previous path copied into one per-PUT bytearray, so retention granularity was the same, but the memory was not pinned.) Consider keeping zero-copy on the client GET path only and copying out on storage-side PUT commit, which also makes the storage cache effective. At minimum, document this and validate with many outstanding PUTs. 4. The sender may remove and re-add the remote agent on almost every PUT. TransferQueue/transfer_queue/storage/payload_transfer/nixl_ucx_runtime.py Lines 262 to 264 in 90ad2c4 TransferQueue/transfer_queue/storage/payload_transfer/nixl_ucx_runtime.py Lines 646 to 652 in 90ad2c4 With storage-side buffers held long-term (point 3), nearly every PUT presents new metadata. Whether this rebuilds UCX endpoints/QPs depends on NIXL, but "QP count stayed at 2" was measured with one buffer reused 999 times. Please check QP/FD counts with many un-cleared PUTs. If NIXL supports loading partial metadata incrementally for a known agent, the 5. O(n²) overlap scan under the global runtime lock. TransferQueue/transfer_queue/storage/payload_transfer/nixl_ucx_runtime.py Lines 516 to 527 in 90ad2c4 Jagged nested tensors are unbound into one frame per sample on PUT. Timing this scan alone: 0.22 s for 4096 frames and 0.86 s for 8192, all while holding the runtime 6. Pending PUT/GET entries never expire. Simplicity and maintainability
Interfaces
Test gaps
|
[AI Review] Follow-up: how common RDMA stacks manage registered memoryThis expands on point 3 above. Common RDMA stacks handle registered memory in roughly three ways, and none of them registers a fresh buffer per request and then lets the lifetime of stored data decide when that buffer is freed. 1. Communication libraries (MPI, UCX, NCCL, TensorPipe, brpc): library-owned bounce buffers, user memory for large messages. 2. Transfer engines (NIXL, Mooncake Transfer Engine, 3FS USRBIO): the application provides registered memory. 3. RDMA storage (Mooncake Store, FaRM, 3FS): one fixed pool registered at startup, allocated per object. Where this PR differs. On storage units, all three conditions are inverted:
The client GET lease is milder, since consumers usually release a batch quickly, but a caller who keeps one small slice still pins the whole GET buffer. There is already a precedent in this repo. The MooncakeStore read path (#175) follows the first pattern: a bounded pre-registered pool, one lease per read, an immediate copy into the caller's tensors, then release. The lease ends with the transfer, not with the data: TransferQueue/transfer_queue/storage/clients/mooncake_client.py Lines 583 to 598 in 90ad2c4 Suggestion.
|
## Motivation Bring the rdma branch up to date with main and verify that the upstream changes remain compatible with the NIXL-UCX payload path. ## Summary - Merge main changes into rdma through the synced base of this PR. - Integrate upstream updates to SimpleStorage, storage request retries, ZMQ socket handling, SSD offload, and Mooncake buffer management. - Fix merge regressions in SSD-backed NIXL payloads, ZMQ error handling, and storage arrival metrics. - Update the payload-transfer contract: `PayloadTransfer.put()` returns the transferred payload byte count, and `PayloadTransferTimeout` carries control-plane timeouts to the storage manager for retry handling. - Enable retry handling for NIXL control-plane timeouts; WRITE/runtime failures remain non-retryable. Conservative cancellation and transaction-phase retry guards are handled in follow-up #176. - Preserve the existing SimpleStorage APIs and optional NIXL-UCX backend. ## Validation The following checks were run against this merge to verify regression coverage and compatibility with the NIXL-UCX path. - Core regression suite: 702 passed, 10 skipped. - Focused tests for storage retries, accept probing, ZMQ socket pooling and shared contexts, Mooncake buffer management, and SSD offload passed. - ZMQ retry passed with an injected lost response; the retried request returned the expected payload. - Two-node NIXL-UCX PUT/GET/CLEAR and payload integrity checks passed over the RDMA transport path. - NIXL+SSD public API checks passed for `kv_put`, `kv_batch_get`, and `kv_clear`. - NIXL timeout/cancel/retry control-flow check passed with a simulated peer. - Verified that SSD-offloaded NIXL payloads no longer retain the shared receive buffer through small Tensor / NumPy views. - Verified ZMQ timeout retries emit only retry warnings and log one final error after exhaustion; peer errors are logged and wrapped only once. - Added regression coverage for ZMQ send / receive timeouts, key-not-found handling, and ZMQ / NIXL arrival metric semantics.
Signed-off-by: Yinlei Sun <ginray0215@gmail.com>
90ad2c4 to
517c7a4
Compare
|
Thanks for the detailed review. I have pushed the follow-up fixes for the two pre-merge findings and several related lifecycle issues.
Validation of the pushed code tree: On each of two servers, the focused NIXL lifecycle, metrics, payload-transfer, and SimpleStorageUnit suites passed (190 passed, 2 warnings), and Remaining scope: Client GET lease growth, partial-metadata reloads under varied buffer sizes, small-frame registration and lock contention, and large-payload copy cost still need workload-scale measurements. The quarantine budget stops new receives after its threshold; it is not a strict cap on a transfer already in flight. |
[AI Review]Reviewed The strategy boundary, frame-native scatter approach, storage-side PUT copy-out, and worker-owned deferred responses are reasonable. Several substantial historical findings are fixed. I would address the remaining lifecycle issues below before merging, and resolve the small-frame preparation bottleneck. Findings to address before merge1. [P1] Completed-WRITE cleanup failure can break native-agent-before-buffer teardownCompleted-WRITE cleanup, lines 647–665, interacting with close(), lines 430–456. The failed-WRITE path deliberately clears its local except Exception as exc:
self._retained_resources.append((handle, sources))
cleanup_failed = True
...
if cleanup_failed:
raise NixlWriteCompletedCleanupFailed(self._terminal_error)The Future stores the exception; its traceback retains the This ownership relationship is present in the official NIXL implementation: the handle stores its agent, and initialize_xfer passes the underlying self.agent. A reproduction using the actual PR runtime and a fake agent modeling that ownership produced: With another pending or quarantined receive, this violates the teardown invariant relied upon to prevent late WRITE access to freed/reused memory. The reproduction establishes the ownership-ordering defect; actual RDMA memory corruption was not tested. Please apply the same traceback-local cleanup discipline to this branch, preferably through one shared retention/raise exit. Add the missing combination test: DONE → handle cleanup failure → keep the Future/exception alive → close with an exposed receive → native agent must be destroyed before receive owners. The existing cleanup-failure, failed-Future-retention, and teardown-order tests cover these pieces separately. 2. [P2] Worker and proxy retain the last ZMQ GET response while idleWorker, lines 1075–1089 and proxy, lines 996–1006. Returning serialized frames fixes double serialization, but Using actual SimpleStorage worker/proxy threads and local ZMQ sockets, an 8 MiB GET left these response-frame references after the client received and released its result:
The two references may share backing memory; these numbers do not establish two independent 8 MiB allocations. They do show that the final batch's buffers survive until another response overwrites the locals. This affects the default ZMQ backend, particularly serialization copies, contiguous copies of non-contiguous tensors, and temporary SSD read results. Please drop the loop-local payload references after sending, or use a short-lived send scope. Add an idle-after-GET ownership test through the actual worker/proxy, in addition to counting serialization calls. 3. [P2] GET cancellation discards a confirmed remote terminal failureCancellation cleanup, lines 383–390, compared with normal response validation in _expect(), lines 710–723. If a GET is cancelled after COMMIT, cleanup waits for the completion response and deserializes it, but never calls A remote WRITE can fail terminally during this window. The client correctly quarantines its receive buffer and rethrows Reproduced with the actual protocol and the repository's FakeAgent: Please share completion validation/state updates between normal and cancellation paths. Preserve cancellation as the outward exception while retaining the confirmed terminal state. Add a cancelled-GET test whose completion contains 4. [P2, still outstanding] Frame preparation remains quadratic, with registration under the global lock_acquire_frame_source(), lines 537–550. Each writable frame scans leased receive buffers and all previously registered sources. For Calling the real source-acquisition method with FakeAgent and disjoint 8-byte frames:
These measurements exclude real MR registration cost and are not RDMA throughput measurements. Large frame counts are reachable: nested tensors are unbound, and storage GET returns per-sample tensor lists. The lock is released between frames, but each scan and native registration holds the same runtime lock. Per-peer executors therefore do not remove preparation-stage contention. Please coalesce small frames and/or avoid the quadratic overlap scan and excessive registrations. This remains a material issue for a performance-oriented PR. Historical review follow-upI checked the code and tests against the original review, memory-management follow-up, and author's fix summary.
Architecture, API, and maintainabilityKeep the existing strategy and socket-ownership boundary. Describe GET zero-copy at the correct API boundary. The transport can return MR-backed views, but public get_data() scatters and repacks results. For ordinary pure-tensor fields, _pack_field_values() uses Thus the optimization can remove an intermediate transport copy; the standard pure-tensor public GET still copies during batch assembly. Long-lived receive leases remain relevant to lower-level calls and containers that preserve views. Also, after PUT copy-out, ordinary stored GET sources no longer originate in the PUT receive MR. Please benchmark through the public API to establish the actual benefit of leases and receive-MR-as-send-source reuse. Reduce duplicated state handling. Findings 1 and 3 are examples of the same outcome being interpreted differently in separate cleanup paths. Make the pre-WRITE, posted-but-unconfirmed, and confirmed-complete phases explicit, and share their ownership/completion rules. Concrete deletion opportunities remain:
A lower-priority edge case: an expired GET COMMIT currently reports Validation
The following focused suites passed: 263 passed, 2 warnings in 80.00 s. Additional checks reproduced findings 1–3, measured the small-frame preparation path, and verified tensor reassembly copies. Native behavior used FakeAgent or an ownership model checked against official NIXL source. The ZMQ retention check used real worker/proxy threads and sockets. Real NIXL/UCX/RDMA hardware tests were not rerun locally; the author's two-node results remain separate supporting evidence. |
[AI Review] Follow-up: [P2] Receive deregistration failure permanently disables receives without a terminal errorAdditional finding on When receive-memory deregistration fails, _deregister_receive_buffer() correctly retains the buffer in quarantine, but only sets This affects client-side GET or storage-side PUT. It can arise when returning an uncached receive buffer, evicting an idle buffer, or releasing the last receive lease. A worker can remain partially usable while every new receive fails as an ordinary request error, defeating the new ReproductionThe following body can be added to @pytest.mark.parametrize("runtime", [0], indirect=True)
def test_receive_cleanup_terminal_gap(runtime):
instance, agent = runtime
instance.prepare_receive(PayloadDescriptor("first", 16, (16,)))
agent.deregister_error = True
instance.cancel_receive("first")
assert instance.diagnostics["quarantined_bytes"] == 16
assert instance._receive_cleanup_failed is True
assert instance.terminal_error is None
with pytest.raises(NixlError) as raised:
instance.prepare_receive(PayloadDescriptor("next", 16, (16,)))
assert not isinstance(raised.value, PayloadTransferFatalError)With the same injected failure, I also exercised the actual strategy's The storage-side loss occurs because handle_request() / _handle_put_prepare() only recognizes Suggested fix and regression coverage
Validation here uses the PR's real Python lifecycle/protocol code with fault-injected FakeAgent behavior; native NIXL/UCX deregistration failure was not exercised on RDMA hardware. |
Motivation
PR #163 introduced the
PayloadTransferabstraction and the optional NIXL-UCX Host-to-Host payload backend forSimpleStorage.This PR further optimizes and hardens the NIXL-UCX path for RL workloads. The main goals are to reduce avoidable Host-side copies, support large frame-native payloads, avoid blocking
SimpleStorageworkers during GET transfers, and make buffer / transfer lifecycle handling safer under concurrency and failure.This work follows the optimization plan outlined in the Payload Transfer roadmap.
Summary
receive_buffer_cache_mbper runtime.SimpleStorageworker to continue processing requests while the NIXL transfer completes.Design
ZMQ remains the control plane, while non-empty Host payloads use NIXL-UCX.
The implementation intentionally keeps the scope limited to the
SimpleStorageHost payload fast path rather than introducing a general-purpose NIXL transport or MR management subsystem.Compatibility
SimpleStoragepublic APIs remain unchanged.Validation
Validation was performed on the current PR head with both two-node NIXL-UCX tests and local RL integration tests.
rma(rc_verbs/hns_0:1), including PUT / GET / CLEAR, datatype and nested-object handling, receive-buffer lease reuse, same-peer / different-peer concurrency, and controlled peer failure.data_ptr()addresses, with matching GET contents and SHA-256 hashes. Existing receive MRs were reused as send sources without another registration. No payload-sized staging copy was observed; small metadata frames were still copied.Follow-up
Continue reviewing whether the current lifecycle and failure-handling design can be simplified without weakening correctness.
Further validate performance and memory behavior in larger-scale distributed RL workloads.
Consider adding a resource limit to the idle registered receive pool. This may depend on the results of integration with RL frameworks, and I have not yet decided on the right approach.Current status: implemented a bounded idle receive-buffer cache with
receive_buffer_cache_mb(default: 256 MiB per runtime); oversized transient buffers are released after their leases end.Connect the runtime diagnostics to the existing TransferQueue metrics / Prometheus path.Current status: payload runtime diagnostics are exported through the existing metrics path as
tq_storage_payload_*metrics.