Skip to content

[None][feat] Add a Mooncake store driver for the blob backend - #20473

Open
chuangz0 wants to merge 9 commits into
NVIDIA:mainfrom
chuangz0:feat/mooncake-store-unified
Open

chuangz0 wants to merge 9 commits into
NVIDIA:mainfrom
chuangz0:feat/mooncake-store-unified

Conversation

@chuangz0

@chuangz0 chuangz0 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR adds the Mooncake driver for RI-08D's blob backend, which is on main through #20884.

The factory that RI-02's TransferManager calls is left to a follow-up on top of #20266, whose BackendFactory shape (cleanup dispatcher, FactoryRejected) is still in review. Nothing is wired into the executor yet, so the path stays off until a runtime assembles it.

The initial profile follows the design: KVCM v2, TP1/DP1/PP1/CP1, one pool, host staging from the #19722 lender, and None hints only. Names like RI-08D are items of the Runtime Integration design; M1–M5 are milestones of the Store Backend design. Both documents are linked below.

What changed since the last review

The backend moved to main. #20884 (merged as b351c407bba) copied blob/backend.py, keys.py, store.py, the testing package and their tests verbatim from this PR's head 15c9491ba4a. It then added:

  • batch registration (RegistersRegionsTogether);
  • a cut of each call at a repeated object key;
  • MemoryDriver fixes and new conformance checks;
  • a README.

This PR was rebuilt on main 592bd6fd8d9 and now holds only the Mooncake driver, its setup and their tests. The review threads on the backend and the boundary have replies, and that code is unchanged on main in those respects.

New in this PR since that head:

  • drain makes no SDK call (thread from @chienchunhung). It used to revoke the put sessions left open, in a blocking RPC outside its budget, which could hold up drain(0.0) and with it the backend's revocation and the lender's hold. close, the owner's last step under main's rule, now does that revocation before it closes the SDK client. A test sets up a gate that would hold a revoke inside the SDK, and checks that drain returns at once without reaching it.
  • Who closes the driver. open_mooncake_driver says what main's README says: the caller that opened the driver closes it last. close adds that this happens off the executor loop's thread. The tests close their drivers that way.
  • A repeated object key is pinned over Mooncake. The backend cuts the call there, so no operation names a key twice; the driver keys a part's results by store key and relies on that.
  • NOT_READY follows what the store reports (thread from @nv-xtf). Main's RowStatus.NOT_READY now covers an object of the key that the store reports without a readable complete replica, with a get's slot untouched, and another put of the same driver, still running, that writes the key; RPC, transfer and integrity errors stay FAULT. README rule 4 now defers to RowStatus for these conditions. So Mooncake's first-read -703 and a put start's -705 with no complete object are NOT_READY under the rule, and the departure note is gone.
  • A put of a key an earlier put of this driver left open is DECLINED, with its own reason and counter (rows_declined_left_open_by_own_put), when no complete object stands behind the key (thread from @nv-xtf, agreed). The driver never writes such a key again, and a FAULT here would count the earlier put's outcome again on every later put of the key. Beyond a store's own refusal, RowStatus.DECLINED covers only this case: the driver will not write the key again, as an earlier put of its own may still hold the key or act on a new write of it.
  • A key whose first put end went unanswered is left open even when the retry ends the session (second thread from @nv-xtf). Mooncake's master matches a session end or revoke to an object by key and client alone, and an end answered RPC_FAIL or RPC_TIMEOUT may still run later: the SDK closes the timed-out connection, the retry goes out on another, and the master runs the first end whenever it arrives. If the object was then evicted and this driver put the key again, the late end would mark the new, unwritten object complete. This was reproduced on a real 0.3.13 master with a proxy holding the first end. The row stays READY; the key is never put again. A start answered RPC_FAIL or RPC_TIMEOUT still frees its key: leaving it open would bar every key whose start met a master outage from ever being put again, while a late start can only reserve an absent key (its next put is NOT_READY until put_start_discard_timeout_sec) or replace a put of the key unfinished past that timeout. The ledger and its names now say keys rather than sessions (_keys_left_open, put_keys_left_for_close, put_keys_left_to_master).

The previous head is kept as backup/pr20473-before-rebase-onto-20884 in the fork.

Design

Design item Where
RI-08D: "Assembly creates BlobBackend; TransferManager retains it" Not in this PR. The factory builds BlobBackend(RegionBinding(regions, driver, hold), wake, config) from main, over the driver open_mooncake_driver returns
RI-08D: the store owns batching, the driver, the pinned SDK, and access-end evidence The driver owns batching and its threads: a fixed pool of workers cuts parts and decides the order of gets and puts
RI-08D: the store owns SDK capacity The mooncake section of the store config: workers (default 4), rows_per_call (256), bytes_per_call (128 MiB), each counted per SDK call. Gets go first; once four gets have gone ahead of a waiting put, that put goes next. Puts take at most max(1, workers − 1) threads, so with two workers or more a get never waits behind puts alone
RI-08D: "initial Store accepts only None as its hint" Mooncake declares no hints, so main's backend refuses a hinted fetch before the driver is called
Store §3.2: resources come back only after access has ended An operation whose access cannot be proven ended keeps its region from being unregistered and makes drain answer False
Store §3.3: advisory cancel Cancel ends the rows no worker has taken; the SDK cannot stop a running call
Store §3.5: put is READY only for a complete, readable object, never on "already exists" alone A session put ends only the rows whose ranged write returned the full size. A key that already exists is READY only if batch_is_exist sees it committed
Store A.2: what a driver must answer blob/mooncake.py; see "Mooncake mapping" below
Store §5 M3: SDK mapping and real-service tests that reuse the round-trip cases test_mooncake_real.py; test_blob_backend_round_trip.py, with a real lender on GPU; main's driver_conformance suite run against MooncakeDriver over the fake SDK and over the real pool
Store §6: kv_sharing.backends: [{type: mooncake, config: <path>}], with store parameters in the store's own file blob/mooncake_setup.py reads a YAML file with blob (key prefix) and mooncake sections; no LLM args are added

Departures

Mooncake mapping

  • put calls the session API, one SDK call per step for a part of up to rows_per_call rows: batch_put_session_start, then batch_put_from_multi_buffer_ranges (one scatter for the part), then batch_put_session_end for the rows written whole. The rest get batch_put_session_revoke, except a row the target may still write into, whose session is left open (see "Put sessions" below).

    • The transfer engine's scatter wait() has no timeout. It returns only once every fragment has physically finished.
    • So once that call returns, its access has ended, even if it answered TRANSFER_FAIL, and a failed row is revoked only after its write has stopped.
    • With batch_put_from, by contrast, a transfer that times out after 60 s is revoked at once while RDMA writes may still be in flight. A late write could then land in space the master has already given to another object.
  • get is one batch_get_into per part.

    • A session get makes three SDK calls, and each return must reacquire the GIL.
    • Next to a thread running Python nonstop, a 32 × 1 MiB session get took 16 ms at p99, against 5.6 ms for batch_get_into.
  • Put sessions are per key and per client.

    • The driver never starts a session for a key another of its puts is writing now; that row is NOT_READY without asking the SDK. A key repeated within one view lands in two calls; a second put of it that runs alongside the first is NOT_READY, never a fault.
    • A key whose session calls did not all have a known outcome, or that may still hold a session, is kept in one ledger:
      • left to the master, never revoked by the driver, because the target may still write into the object: a scatter that raised or miscounted, or a row the scatter answered TRANSFER_FAIL. The master frees these after put_start_release_timeout_sec;
      • left for close to revoke: a session start that raised or miscounted, an end whose first answer was RPC_FAIL or RPC_TIMEOUT (even when the retry ended the session) or that raised or miscounted, a failed revoke, or a put stopped by a raise. They hold no lender memory, so drain leaves them; one whose revocation fails is freed by the master. A put stopped by a raise is unprovable, so close never comes and the master frees its sessions too.
    • A key left open is never put again, as a late end or revoke would act on the next put of it. A later put of it is READY while the master shows its object complete, and DECLINED otherwise. A start answered RPC_FAIL or RPC_TIMEOUT frees its key (see above).
  • Results:

    Mooncake answer Row result
    get: object not found (-704) MISS
    get, first read: no readable complete replica (-703), the slot untouched; put: a key another put of this driver is writing now, or an object that exists with no readable complete replica (-705 and batch_is_exist 0) NOT_READY
    put: a key an earlier put of this driver left open, with no readable complete object (batch_is_exist 0) DECLINED
    put start: pool full (-200) DECLINED
    put start: already exists (-705) and batch_is_exist 1 READY
    Lease expired (-707) Read once more. The reread is READY only if it reads the whole object again; anything else is FAULT, never MISS or NOT_READY, because the first read already wrote the slot
    put start -600 (the driver holds no session of the key, so it is something else, such as a cachelib master's 16 MiB − 16 B object cap), a wrong size, or any other code FAULT
  • Access-end evidence:

    • A put part's access ends when its ranged write returns an answer for every row, whatever the answers are, TRANSFER_FAIL included.
    • A get part's access ends when batch_get_into answers every row and no answer is TRANSFER_FAIL.
    • In every other case the operation turns access_unprovable: a TRANSFER_FAIL on a get, an exception, or a wrong answer count. It keeps its region from being unregistered and makes drain answer False. The backend keeps the attempt's slots for good.
  • Capabilities:

    • get, put and cancel. Cancel ends the rows no worker has taken; the SDK cannot stop a running call.
    • absent_is_miss: the master answers OBJECT_NOT_FOUND for a key it does not hold, before any transfer.
    • Host memory only (device_memory=False). Main's backend refuses a row in any other region with InvalidSubmission, and a lender with no such region with ConstructionRejected.
    • Rows of one span (multi_span=False): batch_get_into reads each object into one buffer.
    • No probe, and no row limit of its own (max_rows_per_op=None), since the driver cuts parts itself.
    • Regions are registered one at a time; the driver does not implement RegistersRegionsTogether.
  • Shutdown: drain cancels the rows no worker took, ends the worker threads and waits for the running parts, and makes no SDK call. close, once a drain returned True and every region is unregistered, revokes the sessions left for it and then closes the SDK client, even if the revocation raised. The revocation waits on the master, and closing holds the GIL through the unmount, so the owner calls close off the executor loop's thread.

Scope

Commit Production Tests
961ee1277ee Add the Mooncake store driver mooncake.py a fake SDK, mapping, session and scheduling tests, and the conformance suite over the fake SDK
ec7bd8d3742 Open a Mooncake store driver from a store config mooncake_setup.py configuration, setup and layering tests
ec265c74665 Run the blob backend over a real Mooncake pool opt-in tests (TRTLLM_MOONCAKE_REAL=1) against mooncake_master and a segment provider, plus the conformance suite on the real store
4d311477bb9 Round-trip KV blocks between two staging lenders through the blob backend GPU test with real KV cache managers, over the in-memory store and a real Mooncake pool
840590063e7 Revoke the Mooncake sessions left open in close, so drain makes no SDK call drain, close a revoke held inside the SDK; owners close their drivers
321fc530e8d Pin a repeated object key over Mooncake and say where NOT_READY departs comments a view naming one key twice
83496c94f31 Close the Mooncake client even when its revocation raises, and pin the order close the fake SDK answers nothing once closed, as 0.3.13 does, so the revoke-before-close order is pinned
aaeeb4cdede Widen NOT_READY to what the store reports, and decline a put of a key left open main's RowStatus wording (NOT_READY, DECLINED, the class and probe docstrings), README rule 4, the left-open reason in mooncake.py the left-open session tests expect DECLINED and its counter
a9f6a74c3cb Leave a Mooncake key open when its first put end went unanswered _end_sessions (the first end answer decides the key), the ledger renamed to keys, main's RowStatus.DECLINED wording the retried end leaves its key open; a timed-out start frees its key; a key left open, to the master or for close, is never put again

The first two commits add the Mooncake modules to blob/__init__.py; the first three list them in main's blob/README.md.

Production code: +1497 / −12 lines, of which main's store.py is +15 / −11, plus +12 / −3 in the README. Tests: +3177 / −2 lines. Each commit passes the CPU tests present at it.

Verification

  • CPU, at a9f6a74c3cb, over main's backend:
    • Blob tests: 647 passed, 170 skipped. This count includes main's own blob tests; the skips are the real-pool tests and conformance checks that a driver's capabilities make inapplicable. These ran with the package's heavy imports stubbed, not from a wheel; CI runs them in the full environment through l0_cpu.
    • The Mooncake files alone: 195 passed, 62 skipped.
    • 16 workers, 30 repeats: 19410 passed, 5100 skipped, no failures.
    • The left-open change was checked against 17 mutants of its logic: all but one fail a test. The survivor names the first rather than the final end answer in a fault's text.
    • The new drain/close tests were checked against mutants: revoking in drain again, revoking after the client is closed, and closing without the finally each fail them.
  • Real Mooncake over TCP, at a9f6a74c3cb: 28 passed, 20 skipped. The conformance suite on the real store now also runs main's extent check; the three batch-registration checks are skipped, as Mooncake registers one region at a time.
  • Conformance over the fake SDK: runs every check the driver's capabilities and the fake's scenario hooks allow (blocked get and put, a key another writer holds, a full store, a crash in put and get, a failing read, an unprovable get, a driver that leaves its operations unstarted).
  • Not re-run since the rebase:
    • Mutation testing. About 1,000 mutants ran over the production modules at the previous head. Every surviving mutant that changed a row result, an access end or when memory is released was given a test that kills it.
    • The GB200 RDMA runs. They passed on f2d736b023d.
  • GPU round trip (kv_cache/sharing, in l0_a10), written against main's backend: passed in CI's A10-PyTorch-2 stage (L0 pipeline 66862), on the same diff before the rebase onto 592bd6fd8d9. It moves each row on its own: the in-memory driver takes one row per operation on a thread of its own, and Mooncake cuts each operation into one-row parts for two workers.

Objects of the wrong size. Both cases are checked.

  • If the object is longer than the row, Mooncake refuses it with -600 before any transfer (real_client.cpp:5134-5141).
  • If it is shorter, Mooncake writes it into the head of the slot and returns its size (real_client.cpp:5169-5180). The driver reports FAULT, its access ends normally, and nothing outside the row is touched.

Performance

Driver-owned concurrency against the previous design, on CPU. These numbers were measured at 15c9491ba4a, before the rebase, against f2d736b023d, and have not been re-run. The backend part of what they measure is now main's; main has since moved the repeated-key check into the call cut, and nothing else on the measured path.

  • Setup, for both versions:
    • a fixed submission rate;
    • a fake SDK whose data calls sleep 1 ms with the GIL released;
    • each version's Mooncake defaults: 4 workers, 256 rows and 128 MiB per SDK call.
  • The executor loop runs about 1 ms of Python per iteration. It is measured two ways:
    • torch-like: it releases the GIL 4 times per iteration, as torch and CUDA calls do;
    • pure Python: it never releases the GIL, the worst case.
  • In each cell the previous version's number comes first.

Mean submission to settle, torch-like executor:

1 × 256 rows 4 × 256 rows 1 × 4096 rows
fetch 2.87 → 2.38 ms 6.56 → 3.83 ms 16.17 → 9.36 ms
all submissions 3.05 → 2.62 ms 6.42 → 4.85 ms 16.81 → 11.20 ms

The executor's own cost, pure-Python executor:

1 × 256 rows 4 × 256 rows 1 × 4096 rows
Submit call (fetch/publish) p99 4.8–5.7 → 0.27 ms 11.4–11.7 → 0.45 ms 8.6–9.2 → 1.4–1.5 ms
Executor time lost to the GIL per submission 0.83–1.04 → 0.65–0.80 ms 2.9–3.0 → 2.0–2.1 ms 12.3–12.8 → 5.2–5.9 ms
Executor stalls over 1 ms 2–5 → 0 33–35 → 1–2 129–133 → 6–8

Worker CPU per submission is less than half of what it was. Settle with the pure-Python executor stays as before for 1 × 256, improves for 1 × 4096 (p50 56.7–57.0 → 52.3–52.4 ms), and is slower for 4 × 256 (p50 20.3–20.6 → 24.8–25.0 ms); see "Known limitations".

Throughput on two GB200 nodes over RDMA, measured on f2d736b023d, not yet on this branch.

  • Setup: segments on the other node; 4 workers; 128 MiB per SDK call.
  • NIC use: one process uses the two NICs of the NUMA node where its staging memory lives; four ranks use all four NICs.
  • "Busy" means another thread runs Python nonstop.
  • The microbenchmark will follow in a separate PR.
setup main thread put GB/s get GB/s
one process, 4 MiB rows idle 89–91 94
one process, 4 MiB rows busy 15–19 31–43
one process, 1 MiB rows busy 12.5–14 24–27
four TP ranks on one node, NUMA-bound, 1 MiB rows, node total idle 160 179
same busy 52 95

A row is one KV block (32 tokens) across all layers of a layer group, holding this rank's heads. With TP the rows shrink, so realistic rows are 0.25–2.5 MiB: Llama-3.1-8B at TP4 1 MiB, Llama-3.3-70B at TP4 2.5 MiB, DeepSeek-V3 (MLA) 2.1 MiB.

Please confirm

  • @chienchunhung:
    • A driver call that cannot prove its access ended keeps its attempt's slots for good, and the binding ends in the profile's containment. RI-02 implements only executor-fatal, so today a get that answers TRANSFER_FAIL, raises, or miscounts stops the executor.
      • A fetch reaches this state on a TRANSFER_FAIL answer, an exception, or a wrong answer count from batch_get_into. SDK 0.3.13 answers TRANSFER_FAIL alike for a failed slice, a status-query error, a partly submitted batch and its 60 s timeout. In the last three cases the transfer may still be running, and the answer does not say which case it is (transfer_task.cpp:746-805, 813-905, 1239-1258).
      • A publish reaches it only on an exception or a wrong answer count from the ranged write, because that write returns only after every fragment has finished.
      • The alternative is a session get, whose access ends even on TRANSFER_FAIL, at two more SDK calls per part. This PR keeps batch_get_into and asks for a containment profile that is not executor-fatal for a store fetch, since the request can recompute instead.
    • A driver call (one put or get) has no time bound; the lease holder's deadlines bound how long a request waits, and shutdown labels name what is still held.
    • Mooncake declares no row limit, so main's driver_call_limit(region) returns None. One driver call is then a layer group of a submission, cut only where it names an object key twice, and a holder bounds what one unprovable call holds by the size of its leases.
  • @nv-xtf: a key whose first put end went unanswered is now left open even when the retry ends the session (a9f6a74c3cb); a start answered RPC_FAIL or RPC_TIMEOUT still frees its key. See the thread for why.

Known limitations

  • The data path does its per-part work under the GIL. The SDK holds the GIL while converting a call's lists, and each worker runs some Python per part. Next to a busy executor, store throughput drops sharply (see the GB200 table). A GIL-free data path (a batched Mooncake API with completion notification, or a C++ worker) could bring busy throughput close to idle; whether it is worth building should wait for the end-to-end TTFT measurement (Store M4). Passing numpy arrays to the SDK instead of lists does not help: the 0.3.13 binding takes list[int], so it converts an array element by element, measured 30 times slower than tolist().
  • An executor that never releases the GIL delays the first transfers. Such an executor leaves the store's workers waiting for CPython's 5 ms forced switch, so submission to settle can take one switch interval longer than with the previous design (4 × 256 rows: p50 20.5 → 24.8 ms). Yielding after a submission fixes this case only and slows the 4 × 256 fetch next to a torch-like executor, so nothing yields.
  • Fetch latency next to a sustained publish load. Gets go first, but fetches and publishes still share the GIL and the transfer engine.
  • Deployment.
    • Keep each rank on its GPU's NUMA node. TRT-LLM's NUMA-aware worker affinity (on by default, TLLM_NUMA_AWARE_WORKER_AFFINITY) does this.
    • One process tops out near 100 GB/s; four ranks on their own NUMA nodes reach 160–180 GB/s together.
  • No probe. The driver declares probe=False. batch_is_exist could back one later, but in 0.3.13 it answers 0 both for an absent key and for one still being written, takes no timeout, and renews the object's read lease on a hit.
  • Lender regions name no memory kind yet. Until the lender's MemoryRegion adds memory_kind, whoever builds the backend hands RegionBinding host-memory SlotRegions; the GPU round trip does this.
  • Other.
    • Session puts carry no checksum.
    • A key left open is never put again by this driver, so a key whose first end went unanswered is not refilled from this process once the store evicts it; other processes still can. The root fix is in Mooncake: an end or revoke that names the attempt it ends, as [Store] Fence stale put finalization with ReplicaID kvcache-ai/Mooncake#3180 did for RFC [RFC]: Replica-ID Validation for PutEnd / PutRevoke and GetReplicaList kvcache-ai/Mooncake#3030 (closed unmerged). Once a release has it, the rule can go.
    • The SDK is pinned to 0.3.13, and the access-end reasoning relies on that version's transfer engine.
    • MC_STORE_LOCAL_HOT_CACHE_SIZE other than empty or 0, TCP's legacy framing and the newer transfer engine are refused when the driver is opened.
    • The last worker to exit calls on_change for the rows still queued after it stopped counting. A drain returns True before that call only once every worker died of a BaseException that left no operation unprovable, which only an on_change raising against the protocol does; under BlobBackend the owner cannot close the driver before that call is recorded.
    • The store config is read into frozen dataclasses with strict type checks rather than Pydantic models; it does not become LLM args.
    • The real-Mooncake tests are opt-in and do not run in CI.

Notes

The base is main 592bd6fd8d9, which contains #20117, #20217 and #20884.

  • Contribution checklist reviewed: tests are included; there are no new dependencies and no new public LLM configuration fields; the new path is off unless a runtime assembles it.

🤖 Generated with Claude Code

Dev Engineer Review

MooncakeDriver adds a host-memory implementation of the StoreDriver boundary for Mooncake SDK 0.3.13. It supports batched transfers, session management, access tracking, cancellation, and shutdown. open_mooncake_driver validates configuration and opens the driver. The PR does not add a runtime factory or wire the driver into the executor.

drain does not call the SDK. close revokes eligible remaining sessions before closing the client. The driver retains access slots when it cannot prove an operation has ended. This can prevent a successful drain and block executor progress. The supported setup profile is limited; verify its configuration checks and assumptions against deployment needs.

QA Engineer Review

The changes add CPU-only fake-SDK driver and setup suites, opt-in real-Mooncake integration tests, and CUDA-gated blob-backend round trips. Driver tests cover status mapping, transfer limits and scheduling, cancellation, drain and close behavior, session cleanup, repeated keys, and unprovable access. Setup tests cover configuration, SDK selection, environment checks, networking, cleanup, and import boundaries. Real-pool tests cover declines, object-size handling, concurrent transfers, and duplicate publishes. Backend tests cover full and partial fetches.

Current test results are unavailable. The supplied author-reported results apply to earlier revisions and do not establish the current status. The changed tests were not found in the scanned CI or manual-QA lists. The Mooncake entries found in those lists refer to other tests. Coverage verdict: needs follow-up.

Per-File QA Perspective

  • tensorrt_llm/_torch/disaggregation/blob/README.md: Documents the driver, setup module, test requirements, and result classifications. Verify the documented verdict conditions against RowStatus behavior.
  • tensorrt_llm/_torch/disaggregation/blob/__init__.py: Clarifies driver and setup responsibilities and lazy SDK imports. Verify that package imports do not load mooncake.store prematurely.
  • tensorrt_llm/_torch/disaggregation/blob/mooncake.py: Adds driver transfer, scheduling, access-tracking, cancellation, and shutdown paths. Verify SDK result mapping and session cleanup, especially when access completion is unprovable.
  • tensorrt_llm/_torch/disaggregation/blob/mooncake_setup.py: Adds configuration parsing, SDK and environment validation, master checks, and driver setup. Verify accepted settings and cleanup after failures.
  • tests/test_common/mooncake_utils.py: Adds a subprocess-backed Mooncake segment provider with readiness and cleanup handling. No matching entry was found in the scanned CI or manual-QA lists.
  • tests/unittest/_torch/disaggregation/blob/fake_mooncake.py: Adds a scripted fake SDK for driver tests. Verify that fake session and buffer behavior represents the SDK cases under test. No matching entry was found in the scanned lists.
  • tests/unittest/_torch/disaggregation/blob/test_mooncake_driver.py: Covers driver conformance, transfer scheduling and limits, result mapping, access tracking, and lifecycle errors. No matching entry was found in the scanned lists.
  • tests/unittest/_torch/disaggregation/blob/test_mooncake_real.py: Adds opt-in tests for real-pool transfers, concurrency, and duplicate publishes. No matching entry was found in the scanned lists.
  • tests/unittest/_torch/disaggregation/blob/test_mooncake_setup.py: Covers configuration, SDK setup, networking, failure cleanup, and import boundaries. No matching entry was found in the scanned lists.
  • tests/unittest/_torch/executor/kv_cache/sharing/test_blob_backend_round_trip.py: Adds CUDA-gated full and partial fetch tests. No matching entry was found in the scanned lists.
  • tensorrt_llm/_torch/disaggregation/blob/store.py: Clarifies NOT_READY and DECLINED semantics. Verify that callers handle concurrent or unresolved puts as documented.

@chuangz0
chuangz0 force-pushed the feat/mooncake-store-unified branch from c537c01 to f1b1689 Compare October 16, 2026 13:06
revoked and the hold is released.
RuntimeError: A registration may be live; the hold is kept.
"""
for index, region in enumerate(self.regions):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This registers every region the lender exposes and treats each one as host memory. Nothing checks that, so a region backed by device memory (a pool lent in place) would be registered with the transfer engine as host memory. Please register only host regions that have slots, keep their indices, refuse construction when none qualifies, and add a test that a device region is never registered.

@chuangz0 chuangz0 Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. RegionBinding.served_region_indices keeps only regions with slots in memory the driver can access: HOST, or DEVICE when the driver declares device_memory (Mooncake does not). With none, register() releases the hold and raises ConstructionRejected. Tests: test_region_binding_registers_the_regions_with_slots_the_driver_can_access, test_a_binding_with_no_region_to_register_is_rejected_and_lets_the_hold_go.

f"{where}: addresses must be int64 [{rows}], got {addresses.dtype} "
f"{addresses.shape}"
)
if not 0 <= group.region_index < len(self._regions):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A row whose region_index names a region this backend did not register is a row it does not recognize. Please reject it here with InvalidSubmission instead of passing the driver an address in memory it never registered.

@chuangz0 chuangz0 Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: _checked_group_rows raises InvalidSubmission for a region not in served_region_indices (test_rows_in_a_region_the_backend_did_not_register_are_rejected).

# Submission, on the executor thread.

def _submit(self, view: View, hint: Hint, is_fetch: bool) -> BlobAttempt:
if hint is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rejecting every hint closes this class to drivers that declare hints. Could a fetch pass the hint through to driver.get(rows, hint) when the driver declares hints, and reject it otherwise? A publish has nowhere to put a hint, so rejecting a non-None destination there is right.

@chuangz0 chuangz0 Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: a fetch's hint goes to every get of a driver that declares hints; a fetch with a hint over any other driver raises InvalidSubmission before the driver is called, and a publish still takes only None (test_a_fetch_hint_reaches_every_get_of_a_driver_that_reads_hints, test_invalid_submissions_are_rejected_before_the_driver[hint]).

key=(self._name, "observe", type(error)),
)
return None, False, False
if count != len(batch): # outside the driver protocol: trust neither answer again

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

After a driver breaks its protocol (a wrong result count here, or a raise other than DriverRejected at line 549), the backend keeps accepting work. The holder then only sees attempts that never quiesce, and stops using the binding only when its retention limit trips. Please refuse every later submission with SubmissionRejected, with a fault text that names the violation.

@chuangz0 chuangz0 Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: a raise other than DriverRejected from put/get, a put/get that returns no handle, a query that raises, and a wrong result count are each recorded as a protocol violation. The call is held as unprovable (unless its access end was already recorded), and every later submission gets SubmissionRejected naming the violation. A submission being handed over when the violation is recorded hands no further call to the driver: the next call fails as a whole, the rest end CANCELLED, and none of them holds memory; calls already in the driver run on. Tests: test_any_other_raise_from_the_driver_holds_the_call_and_refuses_later_work, test_a_poll_with_the_wrong_row_count_faults_the_call_and_keeps_it_held, test_the_calls_after_a_protocol_violation_never_reach_the_driver, test_a_violation_told_during_the_hand_over_stops_the_rest_of_it, test_calls_cut_off_by_another_attempts_violation_end_their_access, test_a_protocol_violation_leaves_calls_already_in_the_driver_alone.

deadline = time.monotonic() + max(0.0, timeout)
if self._close_and_force_outcomes():
self._call_wake()
self._progress.join(max(0.0, deadline - time.monotonic()))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When the only work left is unprovable, the progress thread can never finish, so every shutdown waits out its whole budget. Could shutdown return as soon as no worker is inside the driver and every open batch is unprovable?

@chuangz0 chuangz0 Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: shutdown returns as soon as every attempt not yet quiesced holds a call whose access can never be proven to end, since from then on neither its label nor a region's can clear (_shutdown_answer_is_final). That also covers an attempt whose other calls are still running in the driver. Tests: test_shutdown_returns_once_every_open_attempt_has_an_unprovable_call, test_shutdown_returns_once_a_call_in_the_driver_turns_unprovable.

return f"cannot import mooncake.store ({error}). {hint}"


def _warn_unless_pinned_version() -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The code mapping and the access-end rules were read from 0.3.13. On another SDK they may not hold, and an early return from a put would let a revoke free space that a transfer is still writing. Please refuse other versions, with a test-only override if needed.

@chuangz0 chuangz0 Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: open_sdk refuses a mooncake.store not shipped at 0.3.13 by one of the pinned distributions (_require_pinned_builds, which also refuses when any installed build that lists the module's file is not one of those distributions at 0.3.13, as a second install over the same path can leave). No override was needed: the tests fake the installed distributions (test_only_pinned_sdk_builds_are_opened).

return self.store.batch_put_from_multi_buffer_ranges(*args)


def test_real_concurrent_publishes_of_the_same_keys_from_one_client(clients, request) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This holds the first scatter for at most 5 s, well under the master's put discard timeout (30 s by default). The case that matters is a first put stuck past that timeout: the second put of the same key from this client must still be refused, and batch_is_exist must stay 0.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added test_real_put_stuck_past_the_discard_timeout_keeps_its_keys: the master runs with --put_start_discard_timeout_sec=2 and the first scatter is held past it. The second put of the same keys from this client is skipped without asking the SDK, and batch_is_exist stays 0 until the first put ends.

SLOT_BYTES = 64 << 10


def _require_mooncake() -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please check that mooncake_master comes from the same pinned distribution as the client, and record both versions in the run log. Beyond the current cases, the real-pool run should also cover a TRANSFER_FAIL get (access stays unproven), a lease-expired reread, a master kill, freeze and restart, a store-side freeze during a put, and process exit while a call is blocked.

@chuangz0 chuangz0 Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Version part done: the master must be shipped by one of the pinned distributions and report 0.3.13, every installed pinned client build must be 0.3.13, and both versions are printed and recorded as a test-suite property. Of the extra cases, the real pool covers a put stuck past the discard timeout. A TRANSFER_FAIL get and a lease-expired reread are covered only over the fake SDK (test_mooncake_driver.py). Master kill/freeze/restart, a store-side freeze during a put, and process exit during a blocked call are not in this PR.

def make_driver(self) -> StoreDriver:
return MooncakeDriver(FakeMooncakeStore())

@pytest.mark.xfail(strict=True, reason=SHORT_OBJECT_LANDS)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This strict xfail, and the one at test_mooncake_real.py:228, depend on the suite's "destination unchanged" assertion for short objects. batch_get_into writes a shorter object into the head of the slot, so that assertion does not fit this store; both xfails should go when the suite drops it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: for a smaller object the suite now checks only that the row is a FAULT and that the canaries outside the slot are intact (test_an_object_smaller_than_its_row_is_a_fault); both xfails are gone.



@pytest.fixture(scope="session")
def blob_memory_store():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Loading the in-memory driver by file path ties this conftest to the test-tree layout. Once the driver suite and the in-memory driver are importable from the package, please import them instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: the in-memory driver and the driver suite moved to tensorrt_llm/_torch/disaggregation/testing/. The conftest's path loading is gone, and the round-trip test imports them from the package.

# Submission, on the executor thread.

def _submit(self, view: View, hint: Hint, is_fetch: bool) -> BlobAttempt:
if hint is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This rejects every non-None hint, but blob/store.py in this PR says the backend passes a hint only to a driver that declares hints and rejects it before dispatch for the others (Capabilities.hints, StoreDriver.get). As written, a driver that fetches across instances by hint can't be used under this backend. Could a fetch's hint go to every get of a driver that declares hints (the same hint for each get when a submission is split into several), keeping the InvalidSubmission rejection for drivers that don't, with a test for both cases? Publish can keep rejecting non-None hints, since put takes none.

@chuangz0 chuangz0 Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done, same change as the thread above: when a fetch is split, each get receives the same hint object; a fetch with a hint over a driver without hints raises InvalidSubmission before the driver is called, and publish keeps rejecting a non-None destination. Both cases are tested in test_a_fetch_hint_reaches_every_get_of_a_driver_that_reads_hints and test_invalid_submissions_are_rejected_before_the_driver[hint].

@chuangz0
chuangz0 force-pushed the feat/mooncake-store-unified branch 6 times, most recently from 7f29395 to f2d736b Compare October 22, 2026 10:14
@chuangz0
chuangz0 force-pushed the feat/mooncake-store-unified branch from f2d736b to 6b51a8c Compare October 23, 2026 10:34
nv-xtf added a commit to nv-xtf/TensorRT-LLM that referenced this pull request Oct 26, 2026
… of NVIDIA#20473

The backend now hands each call to the driver as one RowBatch and learns of it through on_change, with no queue or worker threads of its own; the copied files match 15c9491 apart from batch registration, cutting a call at a repeated key, the extent check and the example's cleanup. The tests, the README and the KVCR-shaped driver follow.

Signed-off-by: Tingfeng Xian <289617005+nv-xtf@users.noreply.github.com>
"""
...

def get(self, rows: Sequence[RowIO], hint: Hint = None) -> OpHandle:
"""Fetch ``rows``, from where ``hint`` says when given; may return before any byte moved.
def get(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If a put-end RPC fails, this revoke call can make drain(0.0) wait for the SDK. The driver contract requires drain to return within its time budget. While it waits, the backend retains the region registration and lender hold, even after all data access has ended.

Maybe we should keep session cleanup within the remaining drain budget, and add a test with a blocked revoke call to verify that drain(0.0) returns promptly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 282e8493d01 and a53e895d184: drain makes no SDK call any more, so drain(0.0) returns at once whatever the master does. The sessions left open hold no lender memory, so nothing needs them revoked before the regions and the hold go. close now tries the revocation once and then closes the SDK client, even if the revocation raised. Under main's rule close is the owner's last step, after shutdown returned no labels and release_hold(), and it already waits on the master for the unmount. A session whose revocation fails is freed by the master after put_start_release_timeout_sec. The ledger's FOR_DRAIN is now FOR_CLOSE. test_drain_returns_at_once_while_a_revocation_would_block holds a revoke inside the SDK and checks that drain returns without calling it and that close revokes afterwards. test_sessions_left_open_are_revoked_only_in_close covers a revocation that fails.

@chuangz0
chuangz0 force-pushed the feat/mooncake-store-unified branch from 15c9491 to 47456bd Compare October 27, 2026 08:34
@chuangz0 chuangz0 changed the title [None][feat] Add a Mooncake store backend for unified KV sharing [None][feat] Add a Mooncake store driver for the blob backend Oct 27, 2026
@chuangz0
chuangz0 marked this pull request as ready for review October 28, 2026 08:30
@chuangz0
chuangz0 requested review from a team as code owners October 28, 2026 08:30
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between aaeeb4c and a9f6a74.

Files selected for processing: 3
  • tensorrt_llm/_torch/disaggregation/blob/mooncake.py
  • tensorrt_llm/_torch/disaggregation/blob/store.py
  • tests/unittest/_torch/disaggregation/blob/test_mooncake_driver.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

Walkthrough

The change adds Mooncake configuration and SDK setup, a worker-based blob driver, and tests using a fake SDK, a real Mooncake pool, and the KV-cache blob backend. It also updates the documented RowStatus conditions.

Changes

Mooncake Blob Store

Layer / File(s) Summary
Configuration and SDK setup
tensorrt_llm/_torch/disaggregation/blob/mooncake_setup.py, tensorrt_llm/_torch/disaggregation/blob/__init__.py, tests/unittest/_torch/disaggregation/blob/test_mooncake_setup.py, tensorrt_llm/_torch/disaggregation/blob/README.md, tensorrt_llm/_torch/disaggregation/blob/store.py
Adds configuration parsing and validation, SDK build and environment checks, master availability checks, address and RDMA-device selection, and driver setup. Updates row-status documentation and Mooncake test instructions.
Driver lifecycle and scheduling
tensorrt_llm/_torch/disaggregation/blob/mooncake.py
Adds host-memory registration, row admission, worker scheduling, operation polling and cancellation, draining, and closing.
Put and get transfer handling
tensorrt_llm/_torch/disaggregation/blob/mooncake.py
Adds put-session and scatter handling, get reads and lease-expired rereads, per-row result mapping, and access-safety tracking.
Fake SDK and driver tests
tests/unittest/_torch/disaggregation/blob/fake_mooncake.py, tests/unittest/_torch/disaggregation/blob/test_mooncake_driver.py
Adds a scripted in-memory SDK fake and CPU-only tests for driver transfers, scheduling, session handling, lifecycle, and backend round trips.
Real-pool and backend integration tests
tests/test_common/mooncake_utils.py, tests/unittest/_torch/disaggregation/blob/test_mooncake_real.py, tests/unittest/_torch/executor/kv_cache/sharing/test_blob_backend_round_trip.py
Adds an opt-in Mooncake segment provider and real-pool tests, plus full and partial KV-cache blob backend fetch tests.

Priority: Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BlobBackend
  participant MooncakeDriver
  participant Worker
  participant MooncakeStore
  BlobBackend->>MooncakeDriver: submit put or get rows
  MooncakeDriver->>Worker: queue a bounded part
  Worker->>MooncakeStore: issue batch SDK calls
  MooncakeStore-->>Worker: return per-key answers
  Worker-->>MooncakeDriver: record row results and access state
  BlobBackend->>MooncakeDriver: poll operation
  MooncakeDriver-->>BlobBackend: return row results
Loading
Merge Risk: Minimal · up to a9f6a

This change adds a Mooncake blob driver that is not yet wired into the executor, so it has no effect on runtime behavior. Prior review concerns about session cleanup and row-status mapping appear to have been addressed. No unresolved merge-blocking issue was identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 35.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the addition of the Mooncake store driver for the blob backend.
Description check Passed The description explains the purpose, scope, design, testing, verification results, limitations, and checklist status. It provides the information required by the template, although it uses expanded s…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the purpose, scope, design, testing, verification results, limitations, and checklist status. It provides the information required by the template, although it uses expanded section headings.

Full details: Docstring Coverage

Explanation

Docstring coverage is 35.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 158 functions across 10 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@chuangz0

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #81167 [ run ] triggered by Bot. Commit: 36dce6d Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #81167 [ run ] completed with state SUCCESS. Commit: 36dce6d
/LLM/main/L0_MergeRequest_PR pipeline #66862 completed with status: 'FAILURE'

CI Report

⚠️ Resource Maintenance — Test Coverage Reduced:

Resource maintenance in LLM/main/L0_Test-x86_64-Single-GPU #9587: L40S-PyTorch-2 is temporarily paused due to Kubernetes agent provisioning and node issues.
Affected stages:
L40S-PyTorch-2

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

MooncakeDriver implements the StoreDriver protocol over a set-up
MooncakeDistributedStore from SDK 0.3.13, zero copy from and into the
lender's registered host slots. A put starts a put session per key,
writes every row in one scatter (batch_put_from_multi_buffer_ranges),
ends the sessions of rows written whole and revokes the rest; a get is
one batch_get_into. The module docstring cites the 0.3.13 sources each
rule was read from.

- put and get run on the executor loop's thread: a few comparisons
  whatever the batch's size (one span per row, at most 2 GiB a row,
  inside one live registration), then the operation is recorded and
  queued whole. Admitting one costs the same for any number of rows or
  parts.
- A fixed pool of worker threads, started with the driver and ending
  when it drains, cuts parts off the queued operations, at most
  rows_per_call rows and bytes_per_call bytes each, and makes each
  part's SDK calls. Gets go first; once GETS_BEFORE_PUT_IS_DUE gets have
  gone ahead of a waiting put, that put goes next once a put may run,
  and puts take at most workers - 1 threads, so a get never waits
  behind puts alone.
- on_change is called once an operation is done or turned unprovable,
  outside the driver's lock. A cancel ends the rows no worker took, and
  a drain those of every operation; the running parts run to their
  end. A worker an exception stops faults its part with access unproven
  and the others go on; once none is left, the rows no worker took
  fault and new operations are rejected.
- Access end differs by call. The put scatter waits, without a timeout,
  until every task it posted finished, so its return ends access
  whatever it answers. batch_get_into can answer TRANSFER_FAIL for a
  future that gave up while its transfer still runs, so a get ends
  access only when no row answers TRANSFER_FAIL. A raise or a wrong
  answer count leaves access unproven. An operation whose access cannot
  be proven keeps its region from being unregistered and makes drain
  answer False at once.
- Put sessions are per key and client. The driver never starts a
  session for a key one of its puts holds (that row is NOT_READY without
  asking the SDK) and ends only rows written whole. A session it cannot
  see ended or revoked stays in one ledger: left for drain to revoke
  (an end whose outcome is unknown, a start or put that raised or
  miscounted, a failed revoke), or left to the master when the target
  may still write into the object (TRANSFER_FAIL, a scatter that raised
  or miscounted). A key whose session is left open is never put again:
  it is READY when the master shows its object complete.
- Codes become row results here and nowhere else: OBJECT_NOT_FOUND is a
  MISS; a key with no complete replica, a key held by this driver's put
  and an uncommitted object are NOT_READY; a full pool is DECLINED; an
  existing object is READY once the master confirms it; a read whose
  lease expired is read once more, READY or FAULT; the rest are FAULTs
  with their codes. Skipped rows are counted by reason in stats() and
  named in the part's warning.
- Capabilities: absent_is_miss, host memory only, one span per row,
  cancel, no probe and no row limit of its own. close() closes the SDK
  client once a drain returned True and every region is unregistered.

fake_mooncake.py implements only the SDK calls the driver may make, with
0.3.13's answers over real bytes; test_mooncake_driver.py pins the code
mapping, sessions, access-end rules, scheduling, drain and close, a
backend round trip, and that fetch and publish build no store key or
RowIO per row and start no thread on the calling thread. The shared driver_conformance
checks run over the fake SDK with every scenario it can stage. An
exception escaping a thread fails the test; the tests that end a worker
thread on purpose assert it ended by SystemExit.

Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
A deployment describes its store in a config file of its own, while the
backend needs a MooncakeDriver over a set-up SDK client and a
BlobConfig. mooncake_setup turns the one into the other.

load_store_config reads the store config, a YAML file or the mapping
parsed from one: a blob section for the caller's reader (BlobConfig for
the blob backend), and a mooncake section read into MooncakeConfig,
which also sets the driver's workers, rows_per_call and bytes_per_call
(defaults measured on GB200 over RDMA). Unknown sections and keys, and
values of the wrong type, are refused with their section; so are an
unreadable file, a YAML error and a master address that is not one
host:port.

open_sdk refuses TCP's legacy framing, the local hot cache and the newer
transfer engine (MC_USE_TENT, MC_USE_TEV1), whose behaviour the driver's
rules were not read for; logs the Mooncake environment before setup;
imports mooncake.store only when called, and refuses it unless every
installed distribution that lists the module is a pinned 0.3.13 build;
waits for the master; resolves the RDMA devices and the local address;
and closes a client whose setup failed. open_mooncake_driver reads both
sections before it opens the SDK, closes the client again when the
driver cannot start its workers, and returns the driver with what the
caller's reader returned. A client in service is never closed here.

test_mooncake_setup.py covers config errors, setup arguments, device
detection, a master that comes up late, the refused settings and SDK
builds, a missing SDK, a half-open client, the opened driver working
through its client, and the blob package's import layering.

Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
The fake SDK shows what the driver asks for; only a real master shows
whether the SDK answers that way. test_mooncake_real.py runs the shared
driver_conformance checks over a real pool, one fresh client per check,
with the scenarios a real master can stage (a key another client is
still writing, a get or put held inside the SDK), and runs the backend
between real clients for what the fake models beyond them: a full pool
that declines rows, a get that lands a shorter object's bytes and
refuses a longer one, a second publish of keys whose sessions the same
client still holds, also once the first put is stuck past the master's
put_start_discard_timeout_sec, and four workers moving 2000 rows byte
for byte.

The clients give the pool no segment and keep no local buffer, so every
object sits in another process's segment (running_segment_provider in
test_common/mooncake_utils.py) and nothing falls back to the SDK's
copying path. The file runs only with TRTLLM_MOONCAKE_REAL=1 and fails
unless mooncake_master and the client are both a pinned 0.3.13 build;
it records both versions as test-suite properties. An exception
escaping a thread fails the test.

Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
… the blob backend

The CPU and real-pool tests drive the backend with host slots of their
own; none shows that what the staging lender lends is what the backend
moves. test_blob_backend_round_trip.py does it end to end on a GPU: one
real KV cache manager publishes a request's blocks through its lender's
staging slots, another fetches them into a request of its own, and a
test lease holder drives the lender as the runtime will (lend, wait for
the view, submit it as it stands, wait for quiescence, mark what
arrived, release).

- Pages come back byte for byte and resume_bounds covers the prompt's
  three whole blocks, over the in-memory store and, with
  TRTLLM_MOONCAKE_REAL=1, a real Mooncake pool. Each driver moves every
  row separately, several at once: the in-memory driver takes one row
  per operation, each on a thread of its own; Mooncake cuts each
  operation into one-row parts on two workers.
- A fetch whose later blocks fault marks only the delivered prefix, so
  resume_bounds stops at the first gap. Its in-memory driver ignores
  cancels: the rows run in any order, and the cancel a later row's
  fault forwards could otherwise end the first row before it started.

The test builds each backend itself, as a factory will: it opens the
driver, takes the lender's hold, registers the regions with
RegionBinding, starts BlobBackend, and gives the hold back once the
backend shut down. The lender's MemoryRegion names no memory kind, which
SlotRegion requires, so the test registers host-memory SlotRegions made
from the lender's regions, as a runtime must until the lender's regions
name their memory kind. An exception escaping a thread fails the test.

Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
… makes no SDK call

drain revoked the put sessions left open once it had drained, in one
batch_put_session_revoke outside its budget. A slow or hung revoke then
held up drain(0.0) on the backend's progress thread, and with it the
revocation of the registrations and the lender's hold, although no
access remained.

close, the owner's last step under the blob backend's rule, now tries
the revocation once before it closes the SDK client; close already waits
on the master for the unmount. These sessions hold no lender memory, so
nothing waits for them, and one whose revocation fails is freed by the
master after put_start_release_timeout_sec. drain makes no SDK call and
returns within its budget. The ledger's FOR_DRAIN becomes FOR_CLOSE, and
the stats key put_sessions_left_for_close.

close's docstring and open_mooncake_driver's say who closes: the caller
that opened the driver, last, off the executor loop's thread. The setup
and round-trip tests close their drivers that way, and the round trip
keeps drivers rather than SDK clients for its fallback close.

A new test holds a revoke inside the SDK and checks that drain returns
at once without calling it; the session tests check that close revokes
what drain left.

Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
…T_READY departs

The blob backend cuts a group at a repeated object key, so no operation
names a key twice; the driver keys a part's results by store key and
relies on that. A test over the fake SDK publishes and fetches a view
naming one key twice: two calls, no fault, both slots fetched.

0.3.13 cannot always confirm the hold that NOT_READY stands for: -703
also answers a complete replica it cannot read, batch_is_exist answers 0
for one too, and a session left open after a start that raised may not
exist. The driver keeps NOT_READY for them, since the usual cause is a
write in progress and a FAULT would stop every fetch that races a writer
and count toward isolation; _SKIP_RESULTS now says so.

Comments also say why a failed register_buffer certifies nothing was
registered, and when the last worker's on_change may come after a drain
returned True. The round trip's open_driver says how each driver moves
its rows one by one.

Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
…, and pin the order

What the review of the drain and close change found:

- close closes the SDK client in a finally, so a BaseException from the
  revocation no longer leaves the client open behind a driver that
  already counts as closed.
- Nothing pinned that close revokes before it closes the client. A
  closed 0.3.13 client answers every batch call with an empty list,
  which the driver would take as a failed revocation. The fake SDK now
  answers so too, and the session tests then fail if the order flips.
- close's docstring: the revocation waits on the master without the GIL;
  only closing the client holds it through the unmount.
- A put stopped by a raise is unprovable, so close never comes for its
  sessions; the class docstring says the master frees them.
- The NOT_READY note names its one lasting case: a key whose left-open
  session the master freed stays NOT_READY, as it is never put again.
- The worker-exit comment says when a drain returns True before the last
  worker's on_change, instead of when rows are left queued.
- test_drain_leaves_the_session_of_an_unprovable_put_to_the_master is
  now test_an_unprovable_put_leaves_its_session_to_the_master, since
  drain revokes nothing.
- The round trip's fallback close skips a driver a failed build never
  drained, so the clients after it still close, and its open_driver says
  Mooncake runs puts one at a time.

Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
…put of a key left open

What the review of the NOT_READY departure settled:

- RowStatus.NOT_READY now covers an object of the key that the store
  reports without a readable complete replica, as while a write has not
  finished or when no complete replica can be read, with the row memory
  of a get left untouched; and another put of the same driver, still
  running, that writes the key. RPC, transfer and integrity errors stay
  FAULT. README rule 4 says the same, and both say these statuses charge
  the row no fault rather than that nothing went wrong.
- So Mooncake's first-read -703 and a put start's -705 with no complete
  object are NOT_READY under the rule itself, and the departure note in
  mooncake.py goes.
- A put of a key whose session this driver left open, with no complete
  object behind it, is DECLINED with its own reason and counter
  (rows_declined_held_by_session_left_open), instead of NOT_READY under
  held_by_own_put. The driver never writes such a key again, and the put
  that left the session open already concluded FAULT, so a FAULT here
  would count that one failure again on every later put of the key.
  Beyond a store's own refusal, RowStatus.DECLINED covers only this
  case.
- held_by_own_put keeps its name and counter but now means only another
  put of this driver writing the key now.
- The probe docstring follows NOT_READY: a key a get would find
  NOT_READY is NOT_READY there too.

Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
@chuangz0
chuangz0 force-pushed the feat/mooncake-store-unified branch from 36dce6d to aaeeb4c Compare October 29, 2026 02:34
@chuangz0

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #81317 [ run ] triggered by Bot. Commit: aaeeb4c Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #81317 [ run ] completed with state FAILURE. Commit: aaeeb4c
/LLM/main/L0_MergeRequest_PR pipeline #67001 completed with status: 'FAILURE'

CI Report

⚠️ Resource Maintenance — Test Coverage Reduced:

Resource maintenance in LLM/main/L0_Test-x86_64-Single-GPU #10112: L40S-PyTorch-2 is temporarily paused due to Kubernetes agent provisioning and node issues.
Affected stages:
L40S-PyTorch-2

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

refused = []
for key, first in zip(keys, codes):
code = final[key]
if code == _Code.OK:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does a successful put-end retry guarantee that the original timed-out RPC can no longer execute? Otherwise, could a late end prematurely commit a new object after eviction and reuse of the same key by this client? If so, should we retain the no-reuse guard even after retry success?

…nswered

The master matches a session end or revoke to an object by key and client
alone, and an end answered RPC_FAIL or RPC_TIMEOUT may still run later,
after a retry on another connection ended the session. If the object is
then evicted and this driver puts the key again, the late end marks the
new, unwritten object complete. Reproduced on a real 0.3.13 master.

- A reserved key is now resolved only when the SDK refused its start or
  all its session calls had a known outcome and left no session. An end
  whose first answer was RPC_FAIL or RPC_TIMEOUT leaves the key open for
  close even when the retry ends the session; the row stays READY. A
  later put of the key is READY while the master shows its object
  complete, DECLINED once it does not.
- A start refused with RPC_FAIL or RPC_TIMEOUT still resolves its key;
  leaving such keys open would bar every key a master outage met from
  ever being put again. The class docstring says what a late start can
  do.
- Renames: _sessions_left_open to _keys_left_open, the per-part closed
  set to resolved, the skip reason held_by_session_left_open to
  left_open_by_own_put (counter rows_declined_left_open_by_own_put), and
  the stats put_sessions_left_* to put_keys_left_*.
- RowStatus.DECLINED: the driver will not write the key again, as an
  earlier put of its own may still hold the key or act on a new write of
  it.

Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

5 participants