Repository navigation
feat(kimi-k3): support P/D disaggregation (re-land of #2154) - #2332
Conversation
🏷️ CI GuideRuns automatically on every eligible PR before approval:
Heavy model tests:
|
d42a86a to
d6ba6ce
Compare
|
The fix for the original V4 DPA8 + DSpark hang looks correct. _dp_draft_lockstep_active() now checks is_pipeline_parallel directly, so disabling deferred output no longer causes a producer to skip draft collectives while its DP peers enter them. Keeping deferred output However, disabling Scheduler.use_spec for all P/D producers introduces two additional cases that need attention:
I checked the lockstep behavior and the two cases above with CPU-only source/control-flow checks; these are not end-to-end GPU reproductions. The passing V4/DSpark accuracy jobs are useful, but the P/D benchmark and Mooncake RDMA smoke jobs are currently skipped. Before re-landing, I would also like to see the current PR head pass the original regression configuration: 1P1D, DPA8, DSpark, chunked prefill, and |
50e48a9 to
3759c4e
Compare
b861778
89e5bf9 to
fca8290
Compare
…rs that cannot carry it K3's cache is in two halves and the MLA base class describes one: the 24 full-attention layers live in the paged pool it walks, the 69 KDA layers in the slot-indexed recurrent-state pool `build_kv_cache_tensor` tags `per_request_state=True`. A request's KDA state is not derivable from its MLA blocks, so a transfer carrying only the latter leaves decode running most of the model on a zeroed state.
…ries none The producer skipped Phase 2 whenever either slot index was negative and returned success. Phase 1 has already moved the block regions by then, so on Kimi-K3 the decode side got all 24 MLA layers and a zeroed recurrent state for the other 69 -- and since that still decodes fluent text, nothing downstream could tell. Raise instead, but only where the two facts contradict each other: slot regions were registered, so there is per-request state to move, and no slot came with the transfer to move it from. Backends with nothing slot-indexed registered keep the skip.
Keep transferred state at the prompt boundary so the consumer processes the first token exactly once, avoiding duplicate KDA recurrent-state updates.
Use the final recurrent-state slot instead of the admission-time slot. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- derive MLA query width from per-forward batch metadata - fall back to eager execution when an exact CUDA graph shape is unavailable - return the actual staged decode token range - initialize remote first-decode MTP windows without prompt-token leakage - preserve first-decode metadata without an extra scheduler scan - add P/D handoff and shape regression coverage Co-authored-by: Cursor <cursoragent@cursor.com>
A P/D producer never speculates but keeps its drafter, so its DP dummy ran at q=mtp_k+1 while real decodes ran at q=1, mismatching `running_tokens` on unified steps. - `dummy_execution` uses the scheduler's spec width (q=1 on a producer). - Only a producer with a drafter passes `graph_shapes`; elsewhere a missing recording stays a KeyError. `decide` warns once on a miss. - Drop the dead spec-token overwrite in the non-deferred `prepare_input_ids` branch; assert `num_spec_step == 0`. - Tests for the above; fix stubs broken by the rebase.
02992bc to
e7f78e3
Compare
main's #2332 made ChunkedOffloadSchedulerBase set seq.offload_load_start_tokens = hbm right before it creates each load operation, so every chunked offload scheduler publishes a restored prefix into the HBM prefix index. mooncake_store's _new_load_operation override set the same value (seq.num_cached_tokens, which is that hbm) and is now redundant. The dense-scheduler test no longer asserts that the attribute is absent, since main sets it there on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's #2332 made ChunkedOffloadSchedulerBase set seq.offload_load_start_tokens = hbm right before it creates each load operation, so every chunked offload scheduler publishes a restored prefix into the HBM prefix index. mooncake_store's _new_load_operation override set the same value (seq.num_cached_tokens, which is that hbm) and is now redundant. The dense-scheduler test no longer asserts that the attribute is absent, since main sets it there on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's #2332 made ChunkedOffloadSchedulerBase set seq.offload_load_start_tokens = hbm right before it creates each load operation, so every chunked offload scheduler publishes a restored prefix into the HBM prefix index. mooncake_store's _new_load_operation override set the same value (seq.num_cached_tokens, which is that hbm) and is now redundant. The dense-scheduler test no longer asserts that the attribute is absent, since main sets it there on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's #2332 made ChunkedOffloadSchedulerBase set seq.offload_load_start_tokens = hbm right before it creates each load operation, so every chunked offload scheduler publishes a restored prefix into the HBM prefix index. mooncake_store's _new_load_operation override set the same value (seq.num_cached_tokens, which is that hbm) and is now redundant. The dense-scheduler test no longer asserts that the attribute is absent, since main sets it there on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rebase main dropped `total_tokens = batch.total_tokens_num` from prepare_input_ids (its own paths no longer read it); the mixed branch still did, so every mixed step raised NameError. The rebase merged cleanly -- the conflict was semantic. test_mixed_deferred_inputs_under_spec caught it. Also point the mixed shrink-guard notes at `dspark_scheduler.apply_q_bucket`, where main (#2332) moved the prefill guard `_dspark_apply_q_bucket` used to hold inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… stuck load visible Review round on 3a61b3d (#issuecomment-6098310910). Most of that review is already answered by the previous commit; this one covers what remained. `orphan()` took the slot of any outstanding load whose slot matched, even one never handed to a worker, so a torn-down request kept its slot until the reclaim window passed, or forever with reclamation off. The connector now tells the index when it puts a state leg on a request (`note_handed_off`, through the bound block manager). `orphan()` holds the slot only for a load in a worker's hands; any other outstanding load is abandoned and the caller frees the slot, as on the merge base. `dispatched == settled + outstanding` holds by construction, so it cannot see a load that never settles. `reclaim` now refreshes `stale_live_loads`: live loads outstanding past the reclaim window (300 s when reclamation is off). It warns when the count rises, and `stats()` exports it next to `loads_handed_off`. `loads_attempted` keeps main's meaning (counted at request time). `orphan_load_slots_reclaimed` is exported only by `checkpoint_funnel`, not twice. A failed load now only lowers the save frontier, never raises it: a frontier below the load's floor means the chunks in between were never stored. Also drops `hash_of` and `pending_loads`, which nothing called; brings the index docstring and the scheduler's completion comment up to date; and notes that a K3 P/D transfer (#2332) brings its own state, which is one more reason the tier's uncarried state leg is given back.
Resolve the mooncake_connector.py conflict by taking main's version. Main (#2332) closed the same write-done port race differently: the listener binds port 0 so the kernel assigns a port the socket already holds, and register_kv_caches waits on _await_listener, raising if the bind failed. That removes the window this branch's _bind_notification_socket retried around, and it also covers the producer's write listener, so the retry loop and NOTIFY_BIND_ATTEMPTS go. Rewrite tests/test_mooncake_notify_bind.py against main's mechanism: the published port is held by the listener and a write-done sent to it arrives, and a listener that cannot bind fails registration instead of leaving notification_port unset behind a dead thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's #2332 made ChunkedOffloadSchedulerBase set seq.offload_load_start_tokens = hbm right before it creates each load operation, so every chunked offload scheduler publishes a restored prefix into the HBM prefix index. mooncake_store's _new_load_operation override set the same value (seq.num_cached_tokens, which is that hbm) and is now redundant. The dense-scheduler test no longer asserts that the attribute is absent, since main sets it there on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rebase main dropped `total_tokens = batch.total_tokens_num` from prepare_input_ids (its own paths no longer read it); the mixed branch still did, so every mixed step raised NameError. The rebase merged cleanly -- the conflict was semantic. test_mixed_deferred_inputs_under_spec caught it. Also point the mixed shrink-guard notes at `dspark_scheduler.apply_q_bucket`, where main (#2332) moved the prefill guard `_dspark_apply_q_bucket` used to hold inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore (#2464) * fix(atomesh): keep process_is_running's answer inside the EXIT trap cleanup_processes runs from the EXIT trap whenever the launcher exits on an error. In a function the trap runs, a bare `return` returns the trap's $? -- the exit status -- instead of the status of the test before it, so process_is_running reported every live process as gone: the reap then called `wait` on a live LMCache MP server and blocked forever, and terminate_process_group returned without killing anything. Return the test's status explicitly. Error exits now do kill, so terminate_process_group no longer waits without bound after SIGKILL: a process in uninterruptible sleep (a GPU or RDMA teardown) outlives it, and `wait` would have held the launcher, and so the container, until the job's time limit. It gives up after 30 s with a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(offload): mooncake_store — keep the prefix cache in a Mooncake Store without LMCache A new offload connector, `mooncake_store` (ATOM_KV_OFFLOAD=mooncake_store), moves each PP stage's 256-token KV chunks straight between its GPU and a Mooncake Store whose owner processes hold the memory -- on the prefill node and, across two nodes, on the decode node. No LMCache engine, CPU L1 or lookup server is involved. - Data path: the existing BlockGPUConnector + DenseKVByteCodec pack a save's blocks into slots of a GPU buffer registered with the Store, and batch_put_from sends them from HBM (GPUDirect RDMA); a load is a batch_get_into into those slots followed by the unpack. The slot pool is bounded (save and load regions); `pool_device: cpu` puts it in pinned host memory instead. - Lookup: the scheduler asks the master(s) with one batch_is_exist over every PP stage's keys and takes the shortest contiguous prefix. batch_is_exist grants a read lease, so there are no pins to release; a hit older than 1 s is looked up again before its load is dispatched. - Keys: atomkv1-<namespace>/w<rank>of<world>/<blake2b-128 prefix-chained chunk digest>. The namespace covers the PAGE geometry, the PP layer split, the chunk size and the online quantization. The scheduler hashes each prompt once and ships the digests to the workers instead of the token ids. - Reuses ChunkedOffloadSchedulerBase (save/load scheduling, early block release, exact completion ids) unchanged; the only base change is a `_build_load_request` hook whose default builds the same request as before. Loaded prefixes are published into the HBM prefix index (offload_load_start_tokens), as lmcache_mp does. - Failure handling: get results must equal the chunk size (a foreign layout fails the load); a failed put fails the save with the source quiescent and reports its unread chunks source-safe so PP quorums drain; a transfer that outlived Mooncake's 60 s batch wait holds its slots for a bounded time; failed lookups back off. Each worker probes one chunk through the Store at startup. - NIC per rank from the PCI topology (one PCI-local NIC per GPU), per-NIC Store pools, MC_NUM_QP_PER_EP=1 enforced. Phase 1 covers the dense layout (GLM-5.2 / DeepSeek MLA+DSA, plain dense models) with DCP=1 on the offload side; other layouts are refused at startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(atomesh): start a Mooncake Store for the mooncake_store offload MOONCAKE_STORE=1 in the prefill role env makes the launcher start a Mooncake Store (masters with HTTP metadata, THP owner processes bound to their NUMA nodes) before the prefill servers, and hand the prefill workers ATOM_KV_OFFLOAD=mooncake_store with the Store's endpoints in ATOM_KV_OFFLOAD_EXTRA_CONFIG, plus MC_NUM_QP_PER_EP=1, MC_MAX_MR_SIZE and MC_TCP_BIND_ADDRESS. The decode gets MC_NUM_QP_PER_EP=1 for P->D. The Store mechanics are PR #2459's validated launcher (ports below the ephemeral range, page-cache drop, NUMA budget and THP compaction, readiness by pool capacity, per-NIC pools, decode-node owners for two nodes, metrics saved at shutdown, capacity check, liveness hooks) under backend-neutral MOONCAKE_STORE_* names, without the LMCache pieces. New: preflight of the image's Mooncake Store binaries, MOONCAKE_STORE_CONNECTOR_CONFIG (validated against the connector's keys), MOONCAKE_STORE_LEASE_TTL_MS, MOONCAKE_STORE_HOST_POOL_GIB, and refusals of a second offload connector in PREFILL_KV_TRANSFER_CONFIG, a case-set ATOM_KV_OFFLOAD and MC_MS_AUTO_DISC. numa_exec.py, drop_page_cache.py, numa_memory_budget.py and compact_huge_pages.py are byte-identical copies of PR #2459's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(atomesh): GLM-5.2 cpp4/dcp4 cells with the mooncake_store offload Dispatch-only suite `mooncake_store`: the nightly lmcache-1m cpp4/dcp4 cells with their prefix tier in a Mooncake Store and no LMCache. Single node (...-mcstore-1m-c72/c96/c112/c128): owners 768 + 768 GiB on rdma4-7. Two half nodes (...-mcstore-2n-1m-c96/c112): per-NIC pools of 4 x 240 GiB on each node's NUMA0. They need an image with Mooncake Store (e.g. rocm/atom-dev:mooncake-store-20261002) and have their own display_topology. The cpp4/dcp4 decode env gets the anchor the new cells use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(offload): pack and unpack mooncake_store windows in place in the GPU pool A window's slots are now handed out as one run of the transfer pool when the region has one, and a chunk that is a page multiple (every GLM-5.2 stage's) leaves no padding between slots, so the run is a chunk-major buffer. The dense codec packs a save window straight into it and unpacks a load window straight out of it -- one prepared block-ID upload, one kernel and one stream sync per window -- instead of going through the block GPU connector's 48 MB staging buffer with a copy per chunk and a stream handshake per group. On the first native c96 run every load spent 55-120 ms unpacking ~290 chunks against 22-35 ms getting them from the Store; each of those staged steps is a GIL round trip against the forward thread. Scattered slots (a region broken up by held-back slots) and a pool in host memory keep the staging path. `mooncake_store.direct_copy: false` turns the in-place path off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(offload): pin mooncake_store's in-place gate and its source-safe accounting Runs the real in-place gate across direct_copy, the pool's device and the codec's fused staging; checks direct_copy's default and type; and checks that an in-place save whose later window fails, and one that mixes in-place and staged windows, still report every chunk source-safe exactly once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(offload): mooncake_store's data path packs and unpacks in place by default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(offload): name the PP frontier warning after the shared offload scheduler The chunked scheduler now also backs mooncake_store, whose PP4 cells log this warning at startup; it named LMCache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(atomesh): MOONCAKE_STORE_COMPACT_TIMEOUT for nodes slow to compact CI run 37148703448 lost its Store on pit2-p03-g27: right after a fresh 69 GB image pull NUMA1 held 145 GiB of page cache, compacting it for the 768 GiB owner did not finish in the fixed 600 s, and the owner's segment came up partly in 4 KiB pages that the NICs refused to register (ENOMEM, owner abort). The compaction budget is now a setting, default unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(atomesh): state what was seen on pit2-p03-g27, not a guessed cause The comment blamed the slow compaction on page cache from a fresh image pull. The numbers do not support that: NUMA1 had 1342 GiB free for a 768 GiB owner, yet did not compact in 600 s, while NUMA0 on the same node, and both nodes of pit2-p03-g19 and -g23, took 34 s. Record those observations instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(atomesh): mooncake_store cells at c48 and c56 The suite started at c72. c48 and c56 complete the concurrency sweep the nightly lmcache-1m cells cover. The anchor moves to the c48 cell, so the existing cells generate exactly the same matrix entries as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): address the review of mooncake_store A four-way review with adversarial verification confirmed eight defects. Every one was reproduced before it was fixed. None changes the successful save/load path that the benchmarks measure. - A stale hit could hang a request. A hit older than a second is looked up again when its load is dispatched, to renew its read leases. When that lookup came back short, the load was dropped. By then the engine had already parked the request, and only the load's report wakes it, so the request held its blocks and a max_num_seqs slot forever. The load now always goes ahead. A chunk evicted in the meantime fails the worker's all-or-nothing get, and the request recomputes; load_failed lowers the save floor so those chunks are saved again. - The source read deadline started too late. A worker counted it from when its stage received the save. A downstream PP stage receives a save only after the forwards queued ahead of it, while the engine reclaims the source save_abandon_timeout_s after dispatch. Saves now carry their dispatch time, and the deadline counts from it. - The Store namespace missed the environment switches that rearrange the bytes inside a KV block: ATOM_MLA_PAGE_SIZE, ATOM_USE_TRITON_MLA, ATOM_USE_TRITON_MLA_SHUFFLE_KV and ATOM_USE_UNIFIED_ATTN. Servers that differ in them read each other's KV as their own layout. - mooncake_store.publish_loaded_prefix=false broke HBM hashing. Every block after a loaded prefix stayed unhashed, so the request's own suffix was never reused, and each suffix block logged an "Unhashed parent block" ERROR. The setting is removed, and a loaded prefix is always published. - The reconnect pause started too early. After a failed connect, the scheduler counted its pause from before the blocking setup, which can run for minutes. It now counts from the failure and grows with how long the setup blocked. - A multi with mooncake_store and another offload connector was refused only at its first KV poll. Offload backends now register as such, and _build_subconnectors refuses a second one before building either. - The launcher had three gaps: - MOONCAKE_STORE=1 with several prefill workers on one node (prefill_single_node with xP > 1, or packed_nodes) failed only after the first worker had started. It is now refused up front. - Each branch now installs its EXIT trap before its first server, so a failed start stops what started before it. - Two-node Store waits now allow for the other node's compaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(offload): evict a chunk's PP-stage objects as one Mooncake group Each PP stage puts its own object of a chunk, and the master evicted them one by one, oldest lease first. A round's cutoff could fall between a chunk's stages. The chunk was then a miss for every lookup, since a load needs all of them, while the other stages' objects still held memory. Every rank's object of a chunk now goes into one Mooncake group, {namespace}/group/{digest}, through ReplicateConfig.group_ids. The master evicts a group whole, by the lease its members share. A rank that re-puts a missing chunk joins the live group, because an existing key keeps its group. mooncake_store.chunk_groups (default true) turns it off. The scheduler now counts lookups whose ranks held prefixes of different lengths, and the rank objects found past the shared prefix, in [OFFLOAD-LOOKUP-STATS] once a minute. Checked against a real mooncake_master (0.3.13.post1, tcp) with the owner overfilled 6x by 400 four-object chunks. Without groups, 2-3 chunks were left with only some of their stage objects; with groups, none were. ATOM's own client put and read back grouped objects byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(offload): what uneven lookups count once chunks are grouped At c112 with chunk groups, 22 of 12,504 lookups found the stages holding prefixes of different lengths. No stage had a failed put, and 13 of them came before the master's first eviction. They are saves a slower stage was still writing: each stage puts a prompt's chunks tail first, so its prefix grows only when its head window lands. Say so where the counters are documented, so they are not read as eviction leftovers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): quarantine a slot an unsettled transfer may reach for good A slot whose last Store transfer gave up after Mooncake's 60 s batch wait (or whose call raised) went back into use after 300 s. Nothing bounds how late that RDMA work lands: Mooncake gives up without cancelling it, and a NIC whose completion queue stalls holds it indefinitely -- rdma2 of pit2-p03-g23 held 256 outstanding requests for the last 5 minutes of a c56 run. A late get could then overwrite the next chunk's bytes before its unpack, and a late put read another chunk's. Such slots are now quarantined for good, as a GPU copy that did not fence already was. close() unregistered and freed the pool even with quarantined or leased slots, before the Store client was torn down, and a startup probe whose put or get failed reached the same cleanup without settling its slots. The probe now settles its slots as saves and loads do, and a pool with a quarantined or leased slot stays registered and allocated for the life of the process; the client's teardown ends its RDMA work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(atomesh): refuse NaN and negative budgets, and trap EXIT before any start numa_memory_budget.py took --reserve-gib, --per-gpu-gib and each pin's GiB as any float: a NaN compares false with every node's memory and a negative amount cancels other pins, so either passed a plan that cannot fit, and --compact-timeout 0 stopped every compaction at once. They are now refused. The four single-server branches of the role dispatch (the prefill on node 0, the decode node, a prefill node of several, the decode of a prefill-single- node job) installed their EXIT trap only after start_prefill or start_decode returned. Both start the Store's masters and owners before their server, so a failure in between left them running. Every branch now traps first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): name stored formats, RoPE and overrides in the Store namespace; pace failed saves The namespace hashed the configured dtype names. AITER stores "fp8" as e4m3fnuz on gfx942 and e4m3fn on gfx950 -- one byte each, so a load's size check cannot tell them apart -- and a DSA index cache is fp8 under a bf16 KV cache too: an MI300X and an MI355X on one Store shared keys for different bytes. It now also names the GPU arch and the dtypes AITER resolves the KV and index caches to. Two servers on one model path also shared keys across different RoPE settings or any other --hf-overrides field (only 12 geometry fields were hashed), across checkpoint revisions, and across ATOM_FORCE_ATTN_TRITON, whose fp8 MHA KV uses one fixed scale instead of per-token scales. The namespace now covers the RoPE settings, the overrides, the HF config's _commit_hash and that switch. The RoPE settings come from a snapshot Config takes next to the HF geometry: llama.py adds a key to the live rope_parameters while a worker builds the model, so hashing them live would split the scheduler's namespace from its workers'. A failed save was retried on the next step, and a failure that repeats -- a full Store, a master that is down, a stage whose save slots are all quarantined, which is now for good -- re-packed the request's unsaved range on every PP stage every step. Saves now pause 1 s after a failure, doubling for each further one in a row up to 30 s, until a save is stored. The README states what per-NIC pools cost when servers place a rank on different NICs: a hit the worker then misses, once per prefix, never another layout's bytes. Asking each rank's own pool is a follow-up. The in-place load test now checks each window's unpack is waited for before its slots return to the pool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(atomesh): refuse numeric Store settings that are not numbers before anything starts MOONCAKE_STORE_COMPACT_TIMEOUT and the other numeric MOONCAKE_STORE_* settings went to the helpers, a Store process or bash arithmetic unchecked. A compaction timeout of 0 or below cancelled the compaction while startup went on, NaN or a negative NODE_RESERVE_GIB passed the NUMA budget, and a wait timeout bash cannot add aborted the wait without stopping the masters and owners it waited for. The preflight now refuses each that is not a number of its kind: the compaction timeout, node reserve, both wait timeouts, page-cache drop period, owner threads and MR size, master NUMA node and the eviction ratios. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(docker): build Mooncake with Mooncake Store (WITH_STORE=ON) The release image built Mooncake with WITH_STORE=OFF, so no nightly had what the mooncake_store offload needs: the mooncake.store client in the workers and the mooncake_master / mooncake_client processes the atomesh launcher starts under MOONCAKE_STORE=1. Its CI cells could run only on a hand-built image passed as atomesh_image. Turn WITH_STORE on (the Rust bindings stay off), and fail the build when the Store is not configured or its binaries or Python module are missing. Taken from #2455: the same Mooncake commit (v0.3.14-rc1) and flags built as rocm/atom-dev:mooncake-store-20261002 (docker-release 37074895608), the image every mooncake_store result in this PR ran on. #2455's lmcache_mooncake builder stage is not carried; nothing here loads it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(atomesh): the mooncake_store cells run on the release image With Mooncake built WITH_STORE=ON in the release image, the mooncake_store suite needs no image of its own. Drop the notes that told a dispatch to pass one as atomesh_image; the launcher still refuses an image without the Store before it starts anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(offload): keep the mooncake_store transfer pool in HBM only mooncake_store.pool_device=cpu put the transfer pool in pinned host memory, but the launcher's NUMA budget and compaction counted host pools only through a separate MOONCAKE_STORE_HOST_POOL_GIB that defaulted to 0. A case that switched the pool to the CPU without also setting that variable left its pools out of the budget, and PyTorch's pinned allocator rounds each pool up to a power of two: 2 GiB per worker at the default sizes. The CPU pool was never the path to run: it adds a copy each way between HBM and host memory, its 4 KiB pages run into the ionic NIC's ~3 GiB registration limit, and every result of this connector ran with the pool in HBM. Drop the setting and the launcher's host pool budget instead of keeping the two in step. A worker whose GPU memory the NIC cannot register already fails at startup, naming what it needs. The rule that one node runs one prefill worker stays: the case places the owners around that worker's GPUs, and another worker on the node's other GPUs would share a NIC with an owner or find no pool for its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(tests): format the launcher tests with black 26 The pre-check installs black 26.10.1, whose stable style hugs a call's parentheses around a multi-line string argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(offload): drop mooncake_store code and settings nothing uses - The in-place pack and unpack are one method. Window keys and the per-chunk source-safe report have one helper each. The window's block groups come from BlockGPUConnector._ranges_to_block_ids over the window's slice of the block table. - direct_copy, chunk_groups, startup_probe and lookup_batch_keys are gone; no deployment set them. In-place copies whenever the slots are one run, chunk groups and the startup probe stay on, and a lookup still batches 8192 keys. - Store pools are parsed once, in config.py, and a pool must name rdma. Pools given as JSON text are refused. nic.py only finds NICs. - The hash chain reads token ids with one np.asarray. Keys are byte-identical: old and new were compared on about 10^5 inputs (lists, array("i"), int32 and int64 arrays, chunk sizes 1-300, with and without a cache seed). - Removed: overrides that did nothing (_lookup_unpin, the transfer stats hooks, request_finished), the raw-token lookup and chunk_key that only tests used, the MooncakeStoreOffloadConnector alias, client fields nothing read, a group-id length check its only caller cannot fail, and the per-request error counters (the PROF lines carry status=; the counts stay in [OFFLOAD-STORE-STATS]). - Connect and lookup back-off share one pause timer; they never overlap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(atomesh): drop Store launcher settings no cell sets - MOONCAKE_STORE_CONNECTOR_CONFIG is gone, with its inline validator (a copy of the connector's key table that an ast test kept in sync). The workers get the launcher's connector JSON as is; on both cell shapes it is byte-identical to the old round trip. - No cell or workflow set OWNER_THREADS, OWNER_MAX_MR_SIZE, EVICTION_HIGH_WATERMARK, EVICTION_RATIO, NODE_RESERVE_GIB, MASTER_WAIT_TIMEOUT, LEASE_TTL_MS, OWNER_RDMA_DEVICES or PAGE_CACHE_DROP_SECONDS (all MOONCAKE_STORE_*). Each is now fixed at its old default; owners still take their NICs from each OWNERS entry. - Branches that the up-front refusals already make unreachable (a second Store start in one shell, decode owners in the Store's shell) are removed. The cross-node waits are WAIT_TIMEOUT + 2 x COMPACT_TIMEOUT instead of a count of the other node's NUMA nodes. - The launcher header and the cell comments keep one line per setting and what an operator needs. The measurements live in the PR and the README. - Tests: dropped source-text checks that running tests already cover, repeated role-prefix and default-port checks, and a duplicate owner-death test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(offload): let the shared scheduler name where a load starts main's #2332 made ChunkedOffloadSchedulerBase set seq.offload_load_start_tokens = hbm right before it creates each load operation, so every chunked offload scheduler publishes a restored prefix into the HBM prefix index. mooncake_store's _new_load_operation override set the same value (seq.num_cached_tokens, which is that hbm) and is now redundant. The dense-scheduler test no longer asserts that the attribute is absent, since main sets it there on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): end the save pause when a save is stored A failed mooncake_store save pauses every save for 1 s, doubling with each further failure in a row up to 30 s, and the pause is meant to last until a save is stored. A stored save reset only the failure count, though, and left the deadline in place. Saves run concurrently, so a save that was in flight when another one failed can report its success afterwards, and every new save stayed suppressed for the rest of a pause of up to 30 s while the Store was taking saves again. A stored save now clears the deadline as well as the count. A failure that reports after the success still sets a fresh 1 s pause. The new test runs two concurrent saves, one refused by the Store, and reports them in both orders: stored last, the failed save is retried at once; stored first, the failure's pause holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): name MiMo-V2's sliding-window RoPE base and V scale in the Store namespace The Store namespace hashes the RoPE settings K is cached with, from a snapshot `Config` takes, but the snapshot read only the generic names. MiMo-V2, which runs on the dense PAGE path, rotates K in its sliding-window layers with `swa_rope_theta` and multiplies V by `attention_value_scale` before the cache write (models/mimo_v2.py). Two configs that differ only in either value write different KV bytes for the same tokens under the same namespace, at the same chunk size, so a load's size check accepts the other one's chunks. Both fields are now in the snapshot. Since it no longer holds RoPE settings alone, it is renamed for what it covers, how a layer encodes K and V on their way into the cache: `snapshot_kv_encoding`, `Config.offload_kv_encoding` and the namespace's `kv_encoding` entry. The namespace changes, which only means a Store filled by an earlier build is not reused. The sweep went over every model on the dense path (llama and eagle3, qwen3 and qwen3_moe, the deepseek_v2 family, glm4_moe, glm5_next, gpt_oss, minimax_m2, mimo_v2 and its MTP, mixtral and mistral3, kimi_k25) for the K/V settings their attention reads from the HF config. MiMo-V2 is the only one with RoPE settings under a name of its own. Its `context_len` only sizes the RoPE table, since MiMo-V2 passes no scaling, so it changes no bytes and is left out. Switches that select a checkpoint's weights (`use_qk_norm`, `attention_bias`) change only with the checkpoint, and norm epsilons change bytes only within rounding, so they stay out as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): stamp a save's dispatch on the clock the reclaim runs on A mooncake_store worker stops starting GPU reads of a save's source shortly before the scheduler may reclaim the source blocks, `save_abandon_timeout_s` after dispatch on the scheduler's CLOCK_MONOTONIC. The scheduler stamped the dispatch with `time.time()`, and the worker turned that into an age against its own wall clock, clamped at zero. A backward wall-clock step between dispatch and receipt shortened that age, down to nothing, so a downstream PP stage that received the save seconds late started its read window at receipt. With a short timeout and a slow stage, it could still read blocks the scheduler had already handed to another request, and store their KV under this prompt's keys. The scheduler now stamps the dispatch with `time.monotonic()`, the clock its reclaim deadline runs on, and names that clock by the host's boot ID (`dispatch_clock`). The worker places the deadline on the same clock, with no transit to estimate. This is exact, because every process of an engine runs on one host: its PP stages exchange metadata over ipc:// sockets, its TP workers take their calls from a shared-memory queue, and engine_core_mgr refuses multi-node data parallelism combined with PP. On one host CLOCK_MONOTONIC is a single system-wide clock that no wall-clock step moves, so the age a worker reads is the age the reclaim counts, never less. A stamp from any other clock fails safe: the save is placed a whole read window before its receipt and reads nothing (a rate-limited warning names both boot IDs). That covers a boot ID that differs, as on another host, and one either side could not read. A stamp in the future is capped at its receipt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): drop the PP fence warning and pin the order that makes it moot Under pipeline parallelism the chunked offload scheduler logged at every start that saves "may include a chunk the producer fence does not cover". The fear was this: `schedule()` advances `num_cached_tokens` as it dispatches a chunk, and the save frontier reads that field, so a save can cover chunks no stage has finished. That is true, but the fence was never recorded "at dispatch". Every stage records it when it takes the save's metadata, and every stage takes metadata and forwards in dispatch order: - `Scheduler.schedule()` builds the connector metadata before it advances the frontier past the chunk it is dispatching (`build_connector_meta`, then `_advance_prefill_on_schedule`). The saves batch N carries never cover batch N's own chunk, only the chunks of batches 0..N-1. - The head runs `process_kvconnector_output(meta N)`, then `send_metadata`, then `forward(N)`, and it ran `forward(N-1)` before it scheduled N (`PPEngineCoreProc._pp_head_step`). A downstream stage takes batches off a single ZMQ PUSH/PULL socket and runs the metadata RPC before the forward of the same batch (`_downstream_busy_loop`). Connector-only and idle dispatches use the same socket and the same RPC queue. - Each TP worker runs its RPCs one at a time, in order, from one shared-memory queue (`AsyncIOProc.busy_loop`). `forward` and `start_load_kv` both run on that thread's current stream, and a downstream stage's activation receive is ordered on that stream too. So when a stage records the event for meta N, it has already enqueued, on the same stream, the forward of every chunk the meta's saves read. The mooncake_store save waits on that event before its first GPU read (`_copy_in_place` or `BlockGPUConnector.batched_from_gpu`). The warning is removed. The frontier comment in `build_connector_meta`, the dense fence comments and the README invariant now state the actual guarantee. A new CPU test drives the real Scheduler with PP=4 and the mooncake_store scheduler through the real `_pp_head_step` and `_downstream_busy_loop`. Each save reaches both stages while its chunk is still in flight (no stage returns a token), and the test checks that every stage enqueued the forward of every chunk a save covers before taking that save's metadata. With the metadata built after the advance instead, the test fails on the first save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): load nothing from the Store once a put may still write memory it lost A mooncake_store put whose transfer Mooncake gave up on after its 60 s batch wait (or one that raised) leaves its RDMA work posted. The branch already quarantines the source slot for good, because the abandoned work may still read it. The write can also still land on the far side, and that memory is no longer the put's. In Mooncake v0.3.14-rc1, `Client::FinalizeBatchPut` sends `BatchPutRevoke` for a failed transfer before `batch_put_from` returns, and the master's `PutRevoke` erases the processing replica, which hands its buffer straight back to the allocator for the next put of that size. The hold a master keeps on a put-start it discards (`put_start_release_timeout`) does not apply to a revoke. A late write therefore lands inside another key's object of the same size, which a load's size check accepts, and the load would install another prompt's KV. No ordering on the ATOM side can come before that reuse, so this fails closed. After such a put the worker records why and loads nothing more from the Store, checked before every get, and logs one error. A load needs every rank's chunks, so every load of the engine then fails and its request prefills, until the engine and its Store restart. Saves go on. A put that failed within the batch wait leaves nothing posted and changes nothing. Neither of the obvious alternatives closes the hole: - "Never treat the timed-out key as stored" holds already. The put reports TRANSFER_FAIL, the save fails, and Mooncake revokes that key. The exposure is to other keys. - A client reset runs only after the revoke, so it cannot come before the memory is reused. It also has to wait out the other copy threads' calls on the same client, which the same stall can hold for up to 60 s. An end-to-end per-chunk digest would keep loads going but changes the object format. Mooncake's own (`MOONCAKE_STORE_CHECKSUM=1`) copies each object off the GPU and runs a byte-wise CRC64 on the CPU, far below the 12 GB/s in-place load path. The root fix belongs in Mooncake: keep a revoked replica's memory out of the allocator the way a discarded put-start's is, or cancel the posted work before revoking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): distrust the Store only for a put Mooncake may still run The worker quarantined a put's slots and stopped loading from the Store for any exception from the client's put. But the put checks its keys are unique and builds the ReplicateConfig that groups them before it calls batch_put_from, and nothing is posted at that point. A duplicate key, or a binding that refuses group_ids, therefore turned off every load for the life of the process and blamed RDMA in the log. The client now raises CallRefused for anything it refuses before handing the call to Mooncake, for puts and gets alike. The worker releases the slots of a refused call and fails only that save or load; every other exception, from batch_put_from itself, still quarantines the slots and, for a put, distrusts the Store. A get that raises keeps quarantining its slots and nothing more: its late RDMA can only write those slots, never the owners' memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): refuse to start mooncake_store without a boot ID A worker reads a save's dispatch stamp only on the clock the stamp names, the boot ID of /proc/sys/kernel/random/boot_id. Where that file cannot be read, the scheduler stamped no clock and the worker had none to compare, so every save was treated as stamped on a foreign clock and read nothing. Each save then failed on its deadline, and the only sign was a rate-limited warning. The scheduler and the worker now refuse to start when the boot ID cannot be read, with an error that names the file. The worker's dispatch check no longer needs a case for its own clock being unnamed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): pace failed saves only when the base retries them The mooncake_store scheduler counted every failed save into its streak and pushed the save pause out after the base's _retry_or_retire_failed_save returned, whether the base had set up a retry or not. The base retires a failed save instead when its request has finished, its ID was reused, or nothing of it is tracked any more. So the failed last save of a finished request paused every live request's saves for 1 s, then 2 s, up to 30 s, and the streak kept growing although nothing was retried. The base now returns whether it set up a retry, and the scheduler paces only those. A retired failure leaves the pause and the streak as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): one failing Store pool no longer breaks every lookup With per-NIC pools the scheduler asks every pool for every key. A key one pool held was present, but any other key got the lowest code of the pools, and the lookup discarded its whole answer on any negative code. So with pools rdma0 and rdma1 and the rdma1 master down, the keys past the hit came back as errors, every lookup answered nothing, and the prefixes rdma0 did hold could not be loaded. Opening the clients had the same shape: when one pool failed to connect, every client was closed and all lookups paused. Each pool now has its own client, opened on the first lookup that asks it, and its own pause after a failed connect or lookup. A key is present if any pool holds it, absent if a pool answered and none holds it, and unanswered if no pool answered; a pool that fails or is paused therefore counts as absent for every key another pool answers. A lookup answers only what the unanswered keys cannot change: it computes the hit with those keys counted as absent and as present, and answers None ("no answer", retried later) when the two differ. An error past the hit can no longer void a lookup, and no key a pool failed to answer can make a hit longer than the pools that answered allow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): mark a Store a put may still write, for every client of it After a put Mooncake gave up on with its RDMA write possibly still posted, the worker stopped loading from the Store, but only through a flag in its own process. The master had already given the revoked owner memory to other puts, of any namespace, so the late write can corrupt an object that a load's size check accepts. The other engines sharing the Store never learned of it, and neither did an engine restarted against it (the README told users to restart only the engine); the scheduler of the same engine also kept dispatching loads that were bound to fail. The startup probe's put left the same hazard behind when it gave up. The worker now creates a marker, atomkv/store-tainted, outside every namespace, in its Store: on such a put, and before startup fails on such a probe put. Every worker looks for it at startup (loads stay off if it is there; a Store that cannot say refuses startup) and before each load, at most once a second, and a load whose look gets no answer fails. Every scheduler lookup asks for it in the same batch_is_exist as the chunks; a pool that carries it holds nothing for the lookups from then on and is not asked again, and a pool that cannot say answers nothing. A process that has seen the marker never trusts that Store again. Why a marker in the Store, and how it is kept: the pinned Mooncake (v0.3.14-rc1) has a hard pin, ReplicateConfig.with_hard_pin, which every eviction path of the master skips, and a put session (batch_put_session_start/end) that has the master reserve and commit an object with no transfer at all, so the marker gets there while the fabric that stalled the put is still stalled. The marker asks for 100 replicas: the master places at most one per segment, best effort, so a segment that unmounts (taking its own objects, late write included) leaves the marker in the others. A marker the Store does not take is retried with backoff up to 30 s, logged as an error each time, while the worker's own loads stay off. Only a restart of the Store's master and owners clears it, which is also what frees the memory the late write can reach. What can still lose it is a remove of the key or a remove_all, which leave the late write in place, and the loss of every segment that holds a replica; the README says to restart a tainted Store and never just empty it. Tying the Store's lifetime to the worker's in the launcher would not have been enough: the launcher already starts its workers once and the Store with them, and the gap was other engines and deployments outside it. Its comment claimed the Store outlived worker restarts, which it never does, and now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(offload): renew only the leases of the chunks a load reads The mooncake_store scheduler renews a hit's read leases right before it dispatches the load, since a request that waited for KV blocks outlives the 10 s lease its lookup took. It did that by running a whole lookup again: every chunk of the prompt on every rank, through the base's bookkeeping, only to log the answer and dispatch the load anyway. The chunks below the HBM frontier are not read, and neither are those past the transfer end. The lookup client now renews exactly the chunks the load reads, [hbm, transfer_end) of every rank, in one batch_is_exist per pool, and the scheduler logs how many of those objects the Store holds. The load still goes ahead whatever the answer: the engine parked the request when it admitted it, and only the load's report wakes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(offload): skip the Store lookup of a prompt too short to load Every new request cost the scheduler thread a blocking batch_is_exist, also when its prompt could never be loaded: a load must move at least OFFLOAD_MIN_LOAD_TOKENS, and no hit is longer than the prompt less one token, floored to a chunk (_loadable_hit). Such a prompt's lookup always ended in "too small" after admission. The mooncake_store scheduler now answers a miss for those prompts without asking the Store. What this gives up is the hit's save floor: such a prompt's save no longer skips the chunks the Store already holds, and their puts return at once (the master refuses an existing key before any transfer), off the scheduler thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): hold the mooncake_store transfer pool back from the KV budget The worker allocates its transfer slot pool, 1.25 GiB of HBM by default, in register_kv_caches, after the model runner has sized the KV cache. mooncake_store did not override kv_budget_reserve_bytes, so the runner reserved nothing for it: at a high --gpu-memory-utilization the pool could fail to allocate, or take the headroom meant for activations and CUDA graphs. The worker class now answers kv_budget_reserve_bytes from its config, which the factory reaches through the backend's registration, also inside a multi beside a P/D connector. The pool's allocation and the reserve come from one function, pool_allocation_bytes: both regions whole plus 2 MiB to align the base, so the runner holds back exactly what registration allocates, before the chunk size is known. A region that is not a whole number of slots leaves its tail unused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(atomesh): read MOONCAKE_STORE_WAIT_TIMEOUT in decimal The launcher validates MOONCAKE_STORE_WAIT_TIMEOUT with 10#, which accepts a leading zero, and then used the raw value in bash arithmetic, which reads one as octal. 0900 passed validation and then aborted the launcher under set -e ("value too great for base"), after the Store had started; 0600 silently waited 384 s, while the cross-node wait, computed in awk, used 600 s. Every wait now reads the value through mooncake_store_wait_timeout, which normalizes it with $((10#value)) once validation has passed: the prefill node's owner wait, the decode node's, and the cross-node wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(offload): one definition of an offload backend Three places decided which connectors are offload tiers: a hardcoded _OFFLOAD_CONNECTORS set in arg_utils, the offload=True flag on the factory's registrations (is_offload_backend), and the launcher's bash list, which refuses an offload connector in the P/D config before anything starts. A backend registered later would be refused as a second offload tier by the multi connector but let through by ATOM_KV_OFFLOAD's composition. arg_utils now resolves each configured name through the factory, by name or alias in any case, and asks is_offload_backend; a name the factory does not know is not an offload tier there, and building it fails later with the factory's own error. The launcher checks the config before ATOM is importable, so it keeps its list, and a test now holds that list equal to the factory's offload names and aliases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(atomesh): signal the page-cache dropper only while it is ours The launcher's page-cache dropper runs with --for 1800 and exits on its own, but stop_mooncake_store still sent TERM, then KILL, to its saved pid and process group at cleanup, hours later in a long job. By then the kernel may have given that pid to an unrelated process, which the cleanup would kill. The cleanup now signals the dropper only while its pid still runs drop_page_cache.py --every, read from /proc/<pid>/cmdline, and forgets the pid either way. Until the forked child execs, though, its cmdline is the launcher shell's, so a cleanup right after the start would take the dropper for another process and leave it running. The start therefore waits for the exec, two seconds at most. The launcher test stub keeps the dropper's command line, as python3 would. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(atomesh): refuse the Store in an aggregated cell, and trap before its start main's #2528 adds an aggregated mode to the launcher: one server, no P/D roles, a start_aggregated branch ahead of the P/D branches. Two things of this PR meet it there: - MOONCAKE_STORE starts the Store from start_prefill, and an aggregated cell has no prefill, so its server would run silently without the offload the case asked for. validate_mooncake_store_settings now refuses that combination before anything starts. - Every branch installs its EXIT trap before its first start (the rebase moved the aggregated branch's trap ahead of start_aggregated, so the rule holds there too). The launcher test now finds nine branches and counts start_aggregated as a start. The Store-env command-line test also stubs build_torch_profiler_args and ATOM_SOURCE_ENV, which #2528 added to start_prefill and start_decode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): look for the Store's taint marker before and after every get A worker looked for the taint marker at most once a second and trusted a "not there" answer for the rest of that second. If it looked at t=0, another client marked the Store at t=0.1 and a load ran at t=0.2, the load skipped the look, and its batch_get_into could install the same-sized object that the abandoned put corrupted. The marker exists to stop exactly that. _load_refusal now looks before every get (once per load window, right before its client.get) and never trusts an earlier answer. A load also looks once more after its last get: a mark placed while a get ran is one the look before it missed, and that get may have read what the given-up put wrote, so the load fails and nothing it unpacked is used. A load costs one batch_is_exist on one key per window, plus one. Once seen, the marker still holds for good, and a look that gets no answer still refuses that load. What no look can catch is a late write that lands, and is read by a load that ends, between Mooncake giving up the put and the mark landing; the README says so. Tests: a load right after another client marked the Store gets nothing and issues no get (fails with the one-second cache); a load whose only get ran while another client marked the Store fails (fails without the look after the get). Reported by Copilot in review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): hold back the Store's staging buffers from the KV budget A window whose slots are scattered goes through the block GPU connector, which keeps one staging buffer per save and load thread, allocated after the KV cache is sized. The reserve covered only the transfer pool, so the degraded path allocated into the headroom the forward pass was sized against. The worker now builds the connector with a bound known from settings alone, OFFLOAD_GPU_STAGING_MAX_BYTES or 48 MiB, and the reserve adds that bound for every save and load thread. A chunk larger than the bound refuses to start instead of exceeding what was held back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(atomesh): refuse Store ports that collide or leave the TCP range Owner i binds ATOMESH_MOONCAKE_OWNER_PORT + i with nothing keeping it clear of the masters' ports: with the defaults, the 29th owner of a shared pool took 26080, the metadata server's port, and failed to bind once the Store started. Only the last pool's master ports were range-checked. check_mooncake_store_ports now runs with the other Store settings, before the node starts anything. It checks every port the prefill node's Store binds (each pool's master, metadata and metrics ports, and each owner's) for range and uniqueness, and the decode owners' ports for range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): close a Store client whose setup failed MooncakeStoreClient raised on a failed setup before keeping the store object, so nothing closed it, and whatever setup had started stayed. With a master that is down, the scheduler reconnects every 30 s and left one such client per attempt. The client is now closed before the error is raised; a close that raises is logged, and the setup failure is still what the caller sees. Reported by valarLip in review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(atomesh): wait for the other node's compaction of every NUMA node A node waits for the other node's part of the Store for MOONCAKE_STORE_WAIT_TIMEOUT plus that node's compaction, which comes first there. The compaction takes up to MOONCAKE_STORE_COMPACT_TIMEOUT for each NUMA node its owners pin, one node at a time, but the wait allowed for two. With owners on four NUMA nodes (NPS4) the other node could give up during a normal compaction. mooncake_store_cross_node_timeout now takes the other node's owner setting and counts its distinct NUMA nodes: MOONCAKE_STORE_DECODE_OWNERS for the prefill node's wait, MOONCAKE_STORE_OWNERS for the decode node's. Reported by valarLip in review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(docker): build the Mooncake Store only where it is asked for Every image that installed Mooncake (INSTALL_MOONCAKE=1) was built with WITH_STORE=ON and the three Store checks. An image that only moves P->D KV through the TransferEngine then failed to build wherever a Store dependency (cachelib, ...) does not build on its base, and every image grew and built slower. INSTALL_MOONCAKE_STORE (default 0) now decides: off, Mooncake is configured WITH_STORE=OFF and none of the Store checks run, which is the build from before; on, the build is the Store build, and a missing binary or a mooncake.store that does not import still fails it. docker-release.yaml turns it on: the release images are what the mooncake_store connector and the atomesh mooncake_store suite run on. The launcher's image preflight names the switch. Checked by running the build step's shell with stub tools: off configures WITH_STORE=OFF and needs neither the binaries nor the module; on configures WITH_STORE=ON and fails without either. Reported by valarLip in review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(offload): bound the Store's pending saves by OFFLOAD_MAX_PENDING_SAVES The Store scheduler bounded running-plus-queued saves only when the connector config set max_pending_saves, while docs/environment_variables.md says OFFLOAD_MAX_PENDING_SAVES bounds them too. Setting it did nothing. It now bounds them when set, the connector config still taking precedence; with neither set, saves stay unbounded, which the docs now say. Also says what put_bytes in [OFFLOAD-STORE-STATS] counts: every chunk a put left in the Store, including those it already held, since Mooncake answers 0 for both and moves nothing for the latter. Reported by valarLip in review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(docker): build the Mooncake Store by default INSTALL_MOONCAKE_STORE now defaults to 1, so every image built from atom_release.dockerfile carries the Store (mooncake_master, mooncake_client and mooncake.store), not only the release images. Nothing runs it unless ATOM_KV_OFFLOAD=mooncake_store or the launcher's MOONCAKE_STORE=1 asks for it, and P->D KV moves through the same TransferEngine as without it. INSTALL_MOONCAKE_STORE=0 still configures WITH_STORE=OFF, for a base on which a Store dependency does not build. docker-release.yaml no longer passes the switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(atomesh): read the dropper's command line again while it execs A process's /proc/<pid>/cmdline reads empty while it execs: its new memory map has no arguments yet. The page-cache dropper execs more than once as it starts (setsid, then python3), so a check that landed in such a window took a running dropper for gone. Right after its start that failed test_cleanup_signals_the_page_cache_dropper_only_while_it_runs in CI; at cleanup it would have left the dropper running. mooncake_page_cache_dropper_running now reads the command line again, for up to half a second, while it is empty. Repeating that test six at a time on the dev host failed 13 of 360 runs before and 0 of 600 after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(pd): a producer's DP dummy stages no drafts #2332 sized a P/D producer's DP dummy at q=1 (num_spec_step=0) to match the producer's real q=1 decodes. Such a batch has no scheduled_spec_decode_tokens, but the deferred decode path stages a new sequence's draft columns whenever the processor's use_spec is set -- and a producer keeps use_spec, since its drafter writes context KV. The dummy is always a new sequence, so every idle DPA producer rank raised AttributeError when a peer ran the q=1 decode that surfaces a deferred T0. Stage drafts only when the batch carries them (num_spec_step > 0). test_a_producer_dp_dummy_stages_without_a_draft_table builds the dummy with dummy_execution and stages it; without the fix it raises the same AttributeError seen in CI. The flat-width test's batch now carries the num_spec_step its q=4 rows imply. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(pd): pin that new requests still get their draft columns Review follow-up. The flat-width test asserted only shape and counts, so a predicate that stopped staging drafts for consumers would stay green while MTP verified committed history instead of drafts. It now checks the staged draft values. The producer-dummy test no longer pins that ScheduledBatch omits the draft table, only that it carries no drafts. The comment joins the one it qualifies instead of following the loop-vs-scatter note. With the guard removed the producer-dummy test fails (AttributeError); with drafts never staged the flat-width test fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rebase main dropped `total_tokens = batch.total_tokens_num` from prepare_input_ids (its own paths no longer read it); the mixed branch still did, so every mixed step raised NameError. The rebase merged cleanly -- the conflict was semantic. test_mixed_deferred_inputs_under_spec caught it. Also point the mixed shrink-guard notes at `dspark_scheduler.apply_q_bucket`, where main (#2332) moved the prefill guard `_dspark_apply_q_bucket` used to hold inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rebase main dropped `total_tokens = batch.total_tokens_num` from prepare_input_ids (its own paths no longer read it); the mixed branch still did, so every mixed step raised NameError. The rebase merged cleanly -- the conflict was semantic. test_mixed_deferred_inputs_under_spec caught it. Also point the mixed shrink-guard notes at `dspark_scheduler.apply_q_bucket`, where main (#2332) moved the prefill guard `_dspark_apply_q_bucket` used to hold inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rebase main dropped `total_tokens = batch.total_tokens_num` from prepare_input_ids (its own paths no longer read it); the mixed branch still did, so every mixed step raised NameError. The rebase merged cleanly -- the conflict was semantic. test_mixed_deferred_inputs_under_spec caught it. Also point the mixed shrink-guard notes at `dspark_scheduler.apply_q_bucket`, where main (#2332) moved the prefill guard `_dspark_apply_q_bucket` used to hold inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rebase main dropped `total_tokens = batch.total_tokens_num` from prepare_input_ids (its own paths no longer read it); the mixed branch still did, so every mixed step raised NameError. The rebase merged cleanly -- the conflict was semantic. test_mixed_deferred_inputs_under_spec caught it. Also point the mixed shrink-guard notes at `dspark_scheduler.apply_q_bucket`, where main (#2332) moved the prefill guard `_dspark_apply_q_bucket` used to hold inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-submits #2154 with the root-cause fix for the hang #2315 reverted it over, and with answers to each concern #2315.
1. The hang
#2315's diagnosis was correct, and this PR implements it rather than working around it.
47d77d3turnedis_deferred_outoff on a P/D producer._dp_draft_lockstep_active()was reading that same flag to mean "pipeline parallelism is off". Under DP attention a producer processing an output-less middle prefill chunk therefore skipped the aligningpropose()while its idle DP peers entered the draft collectives and waited inDPMetadata.make's all_reduce. One rank short, the all_reduce never completed — 142 warmup requests sent, zero returned after 210 s.fix(k3-disagg): keep P/D producers in the DP draft lockstepmakes the predicate askis_pipeline_paralleldirectly. A P/D producer keeps its drafter loaded and still owes its peers the collectivespropose()carries, so it stays in the lockstep; only PP ranks are excluded, and now by name. The two flags had been coextensive until #2154 gaveis_deferred_outa second reason to be false; using one to test the other was the latent coupling, and naming the condition removes that class of bug rather than special-casing the producer.A second commit narrows the trigger itself.
fix(k3-disagg): limit the producer's deferred-output opt-out to recurrent state: the opt-out exists so the consumer applies the handoff token T0 exactly once, but only a recurrent state notices a second application — a paged write lands at T0's own position, so repeating it is idempotent.Family.has_recurrent_statestates this per family, and V4, MLA and MHA producers keep the deferred-output path they used before P/D handoff existed. The V4 configuration that hung is no longer on the modified path at all.Coverage against recurrence:
test_draft_lockstep_reads_pipeline_parallel_not_deferred_outputasserts_dp_draft_lockstep_activereads exactlyis_pipeline_paralleland nothing else, so a future stand-in for the flag fails here.test_only_recurrent_state_producers_give_up_deferred_outputsweeps every attention family against a producer config: V4, MLA and MHA keep deferred output, the hybrids and pure GDN give it up.test_every_family_is_coveredfails if a family is added without stating its answer, so the sweep cannot go stale.2. The fixes the rollback withdrew
#2315 noted that it was a full rollback, that removing listener readiness and save fencing can reintroduce failures outside Kimi-K3 including other Mooncake P/D and offload users, and that those fixes need to be reintroduced. That is what this PR does, and it reintroduces them behind the draft-lockstep fix rather than in front of it: the Mooncake listener bind/readiness handoff, the CUDA-stream fence before asynchronous offload saves, the offload load-start tracking that publishes restored prefixes, the asymmetric-only DCP guard on slot-region transfer, port 0 surviving endpoint parsing, the full first-decode draft width, and the Kimi-K3 P/D state handoff itself.
3. Validation
Full unit suite in the ROCm container,
tests/pluginexcluded (the image has nosglang.srt.layers):The failure sets are identical:
test_fused_compress_ragged(4),test_mega_mxfp4_method(1),test_mla_flydsl_gather_dispatch(2 failed + 4 errors),test_scheduler_metrics(7).test_accumulation.pydiffers only in which parameters fail, in both trees — it is flaky.test_scheduler_metricspasses 34/34 in isolation on both trees and fails only in full-suite context, on main as well. No regression is attributable to this branch; the passed-count difference is the tests this branch adds.