The 2b.1b adopt-then-strip left the entire option-A commit-quorum apparatus
orphaned: B1's writing_check ReduceOp.MIN frontier (mark_object_committed) is
the all-ranks-done consensus, so the per-(rank,layer,payload) gather quorum it
replaced is information-redundant (design sec 2.4 Corollary). This GC removes
the apparatus and collapses the CpSharedL2PageAllocator commit model to exactly
{_ranges_by_object, _committed_objects, _free_by_payload} -- precisely what
placement_digest() hashes -- so the cross-rank digest assert now covers the
whole model and the dead quorum is structurally unrepresentable.
Removed (all proven dead under B1, confirmed by a full tree caller-sweep +
opus adversarial review = SHIP):
- allocator: commit_layer, _has_full_commit, _expected_layers_for_object_payload,
adopt_reserved_range, set_object_required_payloads; fields _commits_by_object,
_expected_ranks, _adopted_ranges, _required_payloads_by_object,
_expected_layers_by_object, _required_payloads, _expected_layers; ctor args
expected_ranks/expected_layers/required_payloads; module fns
broadcast_cp_shared_l2_decision, gather_cp_shared_l2_{preflight,commits,commit}.
reserve/split_committed_object/_drop_object simplified to ranges+committed-bit.
- cache_controller: the 3 dead _commit_cp_shared_l2_* fns, 3 imports, all 5
option-A ctor params (cp_shared_l2_{cpu_group,source_rank,broadcast_fn,
preflight_fn,commit_fn}), the per-object commit-contract builder/setter/call.
- hiradix: allocator construction updated (3 args dropped); live _split_node
caller and _get_cp_shared_l2_rank_and_group@167 intact.
- tests: split tests migrated to mark_object_committed; 4 dead-symbol tests removed.
Lost __init__ payload-has-slab validation is covered by reserve()'s own
ValueError (fail-loud at first write). Default (flag-off) path unchanged: the
allocator is never constructed when enable_cp_shared_physical_l2_hicache=False.
Net -580 LOC. Validated: 87/87 pool suite (syh-dev-new, torch) + 31/31 core
classes locally + edited-module import smoke. Out of scope (flagged separately):
the CpSharedL2NodeMetadata.{required_payloads,committed_payload_layers} vestige.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The B1 write-through: every CP rank runs the IDENTICAL deterministic CpSharedL2PageAllocator.reserve over the replicated event stream -> identical placement with NO broadcast/gather; commit rides the writing_check ReduceOp.MIN frontier (mark_object_committed) instead of a per-(rank,layer,payload) gather quorum.
cache_controller.py: _reserve_write_cp_shared_l2 stripped to every-rank reserve (dropped preflight#2/rank0-gate/broadcast#3/adopt/missing-payloads; per-object contract kept MF3; idempotent abort-on-partial SF3); _submit_write_cp_layer_states drops the 3 gather-commit calls (per-layer D2H + all-payload done-gate MF1 + per-node ack kept); submit_write_cp_all_layer drops both fallback gathers (zero-owned keeps its ack MF2).
hiradix_cache.py: 3-way merge of l2_pooling shared-L2 init (clean, 0-conflict; Phase-1 clock + E1 + reserve-budget preserved) builds the allocator/slab when the flag is on and passes it to HiCacheController WITHOUT collective fns. The #5 deadlock removed: shared_l2_capacity reserve-miss now SKIPS the backup (reactive, rank-uniform) instead of _evict_host_for_physical_slots(synchronize_across_ranks=True) -- the deterministic shared-pool evict is 2b.2. mark_object_committed wired at _commit_pending_backup (the MIN commit). New _cp_assert_placement_replicated (SGLANG_CP_HICACHE_PLACEMENT_ASSERT) in check_hicache_events: MIN/MAX-reduce placement_digest across the CP group, fail-loud on divergence (Theorem 1 runtime gate; rank-uniform entry, cannot deadlock).
environ.py: SGLANG_CP_HICACHE_PLACEMENT_ASSERT (EnvBool, default off).
Tests: 8-rank reserve-determinism (identical placement across 8 ranks + divergence-detectable) + full pool suite 91/91 + import smoke on g0033 syh-dev-new. TWO opus adversarial reviews both SHIP (cache_controller strip + hiradix wiring).
NOTES: default-off (--enable-cp-shared-physical-l2-hicache); validated for write_through policy (prod default). Capacity-miss currently SKIPS (B1 shared-pool evict = 2b.2) so the flag must stay off in real runs until 2b.2. Dead gather island (3 fns + 3 imports, opus-confirmed inert) pending GC follow-up. Design+proof: docs_internal/cp_hicache_2b_pooled_l2_b1_design.md sec 2.4/9.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add CpSharedL2PageAllocator.mark_object_committed(object_key) -- the B1 commit path that marks an object committed directly, bypassing the per-(rank,layer,payload) commit_layer/_has_full_commit quorum. Under B1 the all-ranks-done consensus is the writing_check ReduceOp.MIN frontier (the same barrier that releases the node write lock), so every rank calls this with the same object_key at the MIN commit point and the committed set transitions rank-uniformly (feeds placement_digest).
Resolves opus-review MF4: split_committed_object stays coherent for a B1-committed object (no per-layer commit facts) -- it adds children to _committed_objects and the empty _commits_by_object is harmless because B1 never calls _has_full_commit.
7 unit tests (pure-Python): commit-without-quorum, idempotent, unknown-raises, stat bump, digest reflects committed set + lockstep equality, split-of-marked-object coherent, release-after-mark frees pages. Full suite 88/88 green on g0033.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add CpSharedL2PageAllocator.placement_digest() -- a deterministic, order-independent SHA-256 of the full replicated allocator state (free list + per-object ranges + committed set). This is the B1 proof-obligation gate from the 2b design (sec 2.4, Theorem 1): the write-through (2b.1) cross-rank assert hashes this each tick and EQ-checks it across CP ranks, turning placement-determinism into a fail-loud runtime invariant -- any reserve/release fired on a subset of ranks or with a per-rank arg changes the digest.
6 unit tests (pure-Python, no torch): determinism, changes-on-reserve, identical-op-sequence => identical-digest, divergent-op => different-digest, reserve+release restores digest (free-list coalescing), placement-order sensitivity. Full suite 81/81 green on g0033 syh-dev-new.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt the ownerless shared-physical-L2 mechanics from origin/l2_pooling onto the Phase-1 branch, behind --enable-cp-shared-physical-l2-hicache (default off -> behavior-neutral). Foundation for the B1 collective-free allocator (2b.1/2b.2) and the L3 durable floor (Phase 3); no coordination yet.
- cp_shared_l2_pool.py (new): CpSharedL2PageAllocator (deterministic first-fit, capacity charged once, no x cp_size), hugetlbfs-2M slab primitives, position-indexed object ranges, commit-quorum data model.
- memory_pool_host.py: SharedHostTensor* slab orchestration (3-way merged; fix-output's net change to this file is blank-line-only -> additions-only, no content lost).
- server_args.py: flags + _handle_cp_shared_physical_l2_hicache_validation (3-way merged clean with Phase-1 assert + fix-output EnvField).
- test_cp_shared_l2_pool.py: 75 unit tests (allocator/slab/quorum), green on g0033 syh-dev-new; envs stub provides SGLANG_REQ_WAITING_TIMEOUT (read by Phase-1 validation).
Design + B1 proof: docs_internal/cp_hicache_2b_pooled_l2_b1_design.md (sec 2.4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make CP shared-KV HiCache eviction collective-free and scan-resistant by
removing every per-rank wall-clock input from the eviction/admission decisions
(any such input desyncs the must-be-replicated victim set / batch across the 8
CP ranks and can deadlock the collective-coupled writeback/load-back).
- Replicated logical clock: TreeNode.next_access_time() (a process-global
monotonic counter, mirrors mamba/swa's get_last_access_time) replaces
time.monotonic() as the source of last_access_time at every radix bump site
(radix_cache __init__/match/insert; hiradix CP match/insert/_insert_helper_host;
reset() zeroes it). creation_time/pin_expiry intentionally stay wall-clock.
Because match/insert events are replicated (reqs broadcast from rank 0 over the
replicated tree), last_access_time is now identical on every rank. Unique ints
also give a strict total order (no LRU tie ambiguity). No duration arithmetic
reads last_access_time, so non-CP LRU is unchanged; mamba/swa use their own
TreeNode and are untouched.
- CpReplicatedSLRUStrategy = (is_protected[hit>=2], last_access_time, node.id):
scan-resistant (cold one-shots stay probationary, evicted before reused/
protected prefixes), recency-within-segment ages out cold (not LFU), node.id
is the deterministic total-order tiebreak (heaps never compare TreeNodes).
Overridden for CP so it reaches every get_priority eviction site; the host
write-admission key _cp_host_evict_key is switched from FIFO-by-id to it.
- Co-fixes for the same per-rank-wall-clock class: pin_prefix is forbidden under
CP (its pin_expiry victim-eligibility check is per-rank wall-clock; SLRU
scan-resistance covers the benefit); the affinity head_age_s admission input is
disabled (=0.0, relying on the replicated head_defer_count bound); and a
server_args fail-fast guards CP shared-KV against SGLANG_REQ_WAITING_TIMEOUT>0
(its waiting-queue pruning is per-rank wall-clock).
Tests: test_evict_policy.py adds TestCpReplicatedSLRUStrategy (scan-resistance,
recency, total-order, full ordering) -- 30 passed in the dev-cu13 container;
test_radix_cache_unit.py adds logical-clock determinism/total-order tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A long cold-request workload flooded the prefill log (~177MB / 32K lines in
8min, all 8 CP ranks) with cp_host_reservation_plan_insufficient +
prepare_write_backup_reservation_failed even though the host pool was <42%
used and the writes physically fit.
Root cause: _free_room_deficit folds the proactive 20% free-room target
(hicache_host_free_room_ratio) into the admission deficit once a lane dips
below the 10% trigger. _evict_cp_host_for_write_admission then failed -- and
evicted nothing -- whenever it could not reclaim that full 20% target, which
is impossible while the residual is locked/pending. The lane never drained,
so every subsequent backup re-issued the same impossible demand and re-logged
on every tick. The reservation failure is itself a graceful skip, so there is
no crash; the symptom is the log storm.
Gate write-backup admission on the HARD deficit -- max(0, required-available)
over the target and draft lanes (draft_available is 2**62 with no draft pool,
a no-op) -- and evict toward the watermark best-effort, admitting whenever the
write itself fits. This drains the stuck lane and backs the request up instead
of skipping+flooding. The proactive watermark stays as the eviction target
(deficit_by_owner unchanged); it is no longer a hard admission gate. The
decision is computed from rank-replicated state on the collective-free reserve
path, so every CP rank makes the same admit/skip choice (opus-reviewed
rank-safe).
Also rate-limit the four host-reservation fallback warnings (once per 10s per
fallback_name, with a suppressed count) so any genuine exhaustion degrades
quietly instead of producing a log storm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CP shared-KV batch planning needs the immediately previous prepared batch to remain visible as a virtual prefix, but launching another batch before processing that result can leave multiple prepared plans racing against radix insertion. The event loop now processes the previous result after planning the current batch and before launching it, preserving one-batch lookback without accumulating deeper overlap state.
Constraint: CP HiCache prepared batch views are inserted during process_batch_result, not at forward launch.
Rejected: Process previous result before planning current batch | loses visibility of the previous pending prepared plan needed by current planning.
Confidence: medium
Scope-risk: moderate
Directive: Do not increase non-PP overlap depth for CP shared-KV without adding pending-radix reservation semantics.
Tested: Remote cjy-glm5-new pytest test/registered/unit/disaggregation/test_overlap_disagg_prefill_event_loop.py test/registered/unit/disaggregation/test_overlap_disagg_decode_event_loop.py
Not-tested: Full E2E latency impact of reduced overlap depth.
(cherry picked from commit 7df8723eee8203e9e334b229178e3f8bac61396a)
Under overlap scheduling a chunked request's final-chunk write-backup prepare
read a stale `is_chunked` (>0): that per-tick counter is decremented in
process_batch_result, which the overlap loop runs AFTER the run_batch prepare
hook. So prepare floored `backup_end` to a page boundary (the intermediate-chunk
rule) and dropped the now-complete final tail page, while the final non-chunked
insert builds the radix node at full unaligned length. The exact-equality attach
predicate (prepared.logical_len == len(value)) then failed by
(num_tokens-1) mod page_size, dropped the per-layer overlap backup, and forced
the serial all-layer catch-up (~89% of large chunked requests in prod).
Decouple the floor decision from the stale counter: the scheduler marks
`req.cp_backup_is_intermediate_chunk = (req is self.chunked_req)` in
`_prepare_hicache_write_backups_before_forward` (the live chunked_req identity
is the authoritative "will be chunked further" signal, set this tick before
run_batch), and the prepare candidate builder floors on that flag instead of
`is_chunked`. Intermediate chunks still floor; only the misclassified final
chunk now backs up its full tail and attaches the overlap backup.
Tests: update the chunked prepare test to the new flag; add a regression test
that a final chunk with stale is_chunked reserves the full tail; add an offline
repro that drives the real prepare/insert/probe and sweeps unaligned tails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EAGLE verify's traverse_tree.dfs recurses with dfs(retrieve_next_token[curr], ...),
so curr (the fill_next_token_bitmask/accept_token index) degrades from a Python int
to a 0-dim torch tensor at recursion depth >= 2. xgrammar's Python signature is
unchanged across 0.1.27 -> 0.2.1, but the binding swapped pybind11 -> apache-tvm-ffi:
the old binding silently coerced the 0-dim tensor to int, the new one rejects it with
"Expected int but got ffi.Tensor", crashing the decode scheduler on grammar-constrained
(tool-call / structured-output) requests with draft depth >= 2.
Wrap the propagated indices in int(), matching upstream (PR #21722):
- accept_token(int(draft_tokens[curr]))
- dfs(int(retrieve_next_token[curr]), ...)
- dfs(int(retrieve_next_sibling[curr]), ...)
Version-agnostic: correct on both 0.1.27 and 0.2.1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EnvField.get() re-read os.environ (os.getenv + parse) on every call. The
attention/compose hot path reads many flags per layer / per compose
(nsa_backend ~11/layer, cp_shared_kv_runtime ~12/compose) -- thousands of
os.getenv + Python-method calls per forward for values that are fixed startup
config.
Cache the parsed value on first read; invalidate on every mutation through the
API (set / override / clear). The only env vars mutated at runtime to pass
information are NOT EnvFields (SGLANG_TMP_NCCL_COMM_VALUE is read by C getenv;
SGLANG_RUN_ID / SGLANG_DP_RANK via raw os.environ), so the cache does not affect
them. Convert the two direct os.environ["SGLANG_*"]=... writes in server_args
(SGLANG_VLM_CACHE_SIZE_MB, SGLANG_ENABLE_DETERMINISTIC_INFERENCE) to envs.X.set()
so they invalidate the cache.
Verified: cache + set/override/clear invalidation behavioral test passes;
331 mem_cache/parser/layers unit tests pass (5 pre-existing CUDA-device-assert
failures unrelated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CP shared-KV cache-hit corruption bug is fixed and verified end-to-end, so
drop the per-layer/per-request trace probes added during the investigation.
Each probe was a Python call plus an UNCACHED os.getenv (EnvField.get() re-reads
os.environ on every call) in the hot path even when disabled -- ~1000 per
forward from the 11 per-layer deepseek probes alone -- plus a latent risk that
an accidentally-set SGLANG_NSA_DUMP_DIR would dump tensors in-forward and tank
throughput.
Delete cp_hicache_trace.py and every reference to it:
- deepseek_v2: 11 per-layer fwd_hash probes + the final dump-flush block
- nsa_backend: the per-compose NSA tensor-dump block
- cp_shared_kv_runtime: 5 imports + the if _cptrace_enabled(...) compose blocks
- cache_controller / hiradix_cache / allocator / memory_pool_host: the
cptrace/khash/knz/rng round-trip + lifecycle hashes
- environ: the SGLANG_CP_HICACHE_KV_TRACE and SGLANG_NSA_DUMP_DIR flags
Kept: SGLANG_DEBUG_CP_SHARED_KV / SGLANG_CP_TRANSFER_LOG transfer logging and the
x-request-id passthrough (independent of the trace module). Verified: imports
clean, 235 mem_cache + reasoning unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
serving_chat:
- Tolerate a malformed historical tool_call `arguments` string (a valid JSON
document followed by trailing content) instead of 400-ing the whole
multi-turn request: salvage the leading JSON document via raw_decode, else
keep the raw string. (A 112-message tool-history request was rejected with
orjson "unexpected content after document".)
- Catch TypeError (not only jinja2.TemplateError) from the chat-template
render so a `tojson` filter on a Jinja Undefined becomes a clean 400 instead
of a 500 (upstream #20700 / 5e9bd21979).
reasoning_parser:
- Strip only LEADING think-start marker tokens; a global replace would delete a
`<think>` token that legitimately appears inside reasoning content. Preserve
model-generated whitespace in reasoning/normal text (drop .strip()/.rstrip())
(upstream #24251 / dac78768f0).
- Add Glm45 detector tests: leading-only strip, token-inside-content preserved,
repeated leading markers, streaming trailing whitespace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A bs=1 cache-hit prefill produced deterministically wrong output (e.g. "0.5,0.5,0.5").
Root cause: regression a24111a5f4 changed the bs=1 cache-hit call sites to pass
prefix_slot_spans=[] (get_or_build_batch_slot_spans want_prefix=False) together with
prefix_pages>0. In materialize_prefix_and_reuse_current_kv_page_slots and its index twin,
the guard `if prefix_slot_spans is not None:` let the empty list shadow the prefix_pages
fallback -> prefix_spans=[] -> the cached prefix dense slots were never materialized
(both IPC and local paths gate on `if prefix_spans:`) -> attention read zero KV over the
entire reused prefix. The indexer compose dropped its prefix the same way. bs=1 only;
bs>1 (non-empty per-request spans) and fresh prefill (no prefix) were unaffected.
Fix: make the canonical per-request slot-span list the sole description of the
prefix/current regions and fail loud on a missing list. Remove all four
span-reconstruction fallbacks: prefix_slot_span (singular, dead), prefix_pages->span
(the bug), current_slot_spans=None->span (dead), and the prefetcher's copy. Both twins
now require prefix_slot_spans + current_slot_spans (raise on None; [] = genuine
no-prefix). All call sites build canonical spans via want_prefix=True (cached per-forward,
so a24111a5f4's per-layer-CPU goal is preserved by the cache, not by skipping the build);
the MLA current-only path also uses want_prefix=True to keep want_prefix uniform and the
span cache thrash-free.
Tests: migrate unit tests off the removed prefix_pages param (drop it where spans were
already passed; supply canonical spans otherwise); fix two source-string/captured-kwarg
assertions; add test_materialize_prefix_requires_explicit_prefix_slot_spans (None->raise,
explicit spans -> non-zero prefix). test_cp_shared_kv_runtime.py: 158 passed on the
g0033 container.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SGLANG_DEBUG_CP_SHARED_KV also disables tai IPC materialize -> NSA index fail-fast at
warmup (unusable in this config). Add a logging-only flag that emits the sender/worker
transfer-partition dumps (main-KV pages/positions vs NSA-state pages/positions) without
that side effect, and lift the 64-log cap, so we can diff the state-vs-main-KV partition
for a cache-hit vs cache-miss.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rid 'dumpfh-' -> dump ONLY the model's final hidden (last token, post-CP-gather = the
first-token source), allowed on FRESH prefills too -> the cache-bust-vs-cache-hit
discriminator (prefill-side vs decode-side). rid 'dump-' keeps the lean per-layer
trajectory (cache-hit only). Flush moved after the CP gather. dump_diff generalized to
compare any two phases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The full-stage+kv dump (1.7GB/file, synchronous in-forward) hung/crashed a scheduler
child. Trim to the residual-stream trajectory (attn_in/attn_out per layer) + q_all +
final_hidden (~tens of MB/forward) — enough to localize the first divergent layer (Stage 1).
Drop dense-space kv_cache/topk (not comparable anyway) and the MoE substages. Dumps go to
local NVMe (/ssd) to avoid beegfs stall.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dump_tensors now ACCUMULATES per-(layer,stage) into a forward buffer (every fwd_hash
stage at every layer + nsa q_all/attn_out/topk all layers, kv_cache at a layer spread);
dump_flush writes one file per (rid,rank) at model end with the full trajectory + final
hidden + positions + seq/prefix metadata. Gated to cache-hit extends only (skip fresh
full prefills) so --repeat 2 dumps just the L1-hit + L2-reload (small). dump_diff joins
A(rep1) vs C by idx, relerr per token-order stage -> first divergent layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- http_server: chat + generate endpoints read x-request-id header into rid (the
Rust PD router drops the client body rid but forwards the header), so the
client id reaches forward_batch.rids for exact cross-send join.
- cp_hicache_trace.dump_tensors + SGLANG_NSA_DUMP_DIR: torch.save q/composed-KV/
selection/attn_out at layer 0 for rids starting 'dump-' (extend forwards), to
diff L1-hit vs L2-reload by relative error (beats fp-nondeterminism that
defeats binary hashes). Wired into nsa_backend flashmla_sparse path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a per-request content fingerprint (hash of extend input-ids + seq_len) to
fwd_hash so the SAME request forwarded from an L1-hit (known-good) and an
L2-reload (suspect) can be JOINED across the log without rid (the Rust PD
gateway strips the client rid). Gated to bs<=1 forwards (the join is only
meaningful single-request, and this skips the c=24 flood forwards so the
level-3 log stays small). The analyzer joins by ck and reports the first
DETERMINISTIC stage (topk/attn) that diverges = corruption localized.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The decisive level-1 instrumentation for the cross-rank logical-loc
divergence hypothesis. Prior traces all keyed PER-RANK (host pools aren't
comparable across ranks) -- which structurally hid the one invariant that
matters: the visible LOGICAL-loc sequence for a reloaded node must be
bit-identical on every CP rank, or the cross-rank gather reads token j from
its owner rank at a slot that rank filled with a different token.
- visible_locs_hash (cache_controller.load_cp): per-node, per-rank khash of
the visible logical locs, emitted for EVERY node incl. zero-owned so the
analyzer can diff across ranks by node_id. Divergence = root cause.
- victim_set (hiradix _evict_cp_load_back_owner_lanes): per-rank device
load-back eviction victim ids. Device eviction is keyed on wall-clock
last_access_time (per-rank, NOT replicated -- unlike host eviction's
deterministic (priority, node.id)); divergent victims is the trigger.
- release_draw (allocator.alloc_pages_with_owners): fires when a lane's free
bucket is exhausted and the deferred-free release bucket is tapped -- the
use-after-free/aliasing secondary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two metadata agents verified that all cache-hit-extend metadata (positions,
page_table, cu_seqlens, topk offset, MoE row-selection) is correct and
prefix-aware. The one reload-specific, unguarded thing is the per-page OWNER
replay: load_cp builds selected_logical_locs = node_device_indices[owned_positions]
and explicitly SKIPS re-validating that those pages are owned by this rank (perf;
the allocator's owner assert is gated behind debug_mode). A per-page owner
permutation (correct bytes, wrong logical page) passes the aggregate host-keyed
value-hash but makes attention read the right bytes for the wrong positions ->
reload-only repeated-token garbage.
Add owner_check (level 1): at each reloaded node, owner_for_logical_pages(
selected_logical_locs // page_size) must all == cp_rank; log bad_owner count +
valid/padded/tail_pad. bad_owner>0 = the per-page owner/padding permutation =
root cause, which the aggregate byte-hash cannot detect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both cached KV components round-trip byte-perfect, so the bug is downstream in the
cross-rank COMPOSE/GATHER on a reload forward. Add level-3 hashes targeting the
active suspect = materialize_prefix_and_reuse_current_kv_page_slots (reloaded
prefix gathered via modulo-owner IPC descriptors + fresh current via page_inverse
staging; the abutting prefix|current boundary is the suspect):
- gather_out: the live composed dense KV the attention actually reads (mixed_locs
gather), with prefix_pages/current_pages; nz==0 = zero/uninitialized composed KV
(the observed 0|0|0), self-contained.
- span_owner: per prefix/current span -> owner ranks (modulo) + physical pages +
logical pages + per-span hash/nz, to verify the prefix span maps to modulo
owners and catch a boundary conflation/off-by-one.
- ipc_desc: owner/src/logical-page ranges in build_cp_shared_kv_ipc_page_descriptors
(the modulo-owner prefix gather chokepoint).
- MoE stages (forward_deepep, via fwd_hash, valid-row local tensors only, never the
a2a-permuted intermediate): moe_postsel, router_logits, topk_ids/topk_w,
experts_out, moe_out_compact, moe_out_restored -> brackets select/router/topk/
dispatch-combine/shared-add/row-restore.
(The symm ComposePlan is dormant/unwired, so excluded.) All level 3, eager-extend
only, try/except-guarded, helpers khash/knz/rng. Analyzer flags zero composed KV +
zero spans + MoE-stage zeros.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Main KV round-trips byte-perfect yet output is garbage, so add the two
unverified components:
1. NSA INDEXER-K round-trip (level 2): hash the device index_k_with_scale_buffer
at backup (_backup_indexer_from_device_per_layer) and reload (NSA
load_to_device_per_layer, after _load_indexer), keyed by host-slot fingerprint
+ layer, with khash+nz. The indexer selects which tokens attention attends
(top-k); if it corrupts on reload -> wrong selection -> garbage even with
correct main KV.
2. FORWARD-side per-layer hashes (level 3, eager extend path only, cuda-graph
guarded): attn-input, attn-output (pre-residual), topk_indices (the indexer's
selection output -- direct consumer of the indexer-K), and MoE-input, in the
DeepseekV2/GlmMoeDsa decoder layer forward. Localizes where a reload forward
diverges: topk diverges => indexer-K cache; attn-out diverges (topk ok) =>
main KV/page mapping; moe-in diverges (attn-out ok) => residual/MoE.
Analyzer compares indexer reload vs backup (host_fp keyed) + flags zero/degenerate
hidden states per stage. Level 3 (SGLANG_CP_HICACHE_KV_TRACE=3) captures everything.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first KV-value pass couldn't compare (reload hashes keyed by the merged
load op's node id, backup hashes per node -> no join). Fix: emit the host-slot
range fingerprint at both backup and reload (host slots are the stable identity
across node id / splits / merge), and the analyzer matches on
(rank, layer, host_fp). Also add a nonzero-byte count (nz): real KV is never
all-zero, so an nz==0 slice is uninitialized/zero KV -- a direct, self-contained
corruption flag that needs no cross-stage matching. This targets the observed
'0|0|0...' garbage on large-cached reloads (store never filled the backed-up
pages, or the round-trip lost them). Gated at SGLANG_CP_HICACHE_KV_TRACE=2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The index trace proved per-node addressing is self-consistent, so the
corruption must be in the KV VALUES, not the bookkeeping. Add a full-tensor
position-weighted fingerprint (khash) of the actual device KV at two points,
both keyed by (node_id, layer_id) in owned-position order so they compare
directly:
- backup_kv_hash: device KV at backup, taken on the DEFAULT stream (outside the
write_stream block) so it is the correct POST-STORE value, not the racy
write_stream copy's view.
- reload_kv_hash: device KV after the H2D load (on load_stream, in-order after
the copy).
If a reload hash matches no backup hash for that node+layer, the round-trip
delivered wrong KV -- catching BOTH a per-layer store-vs-copy ordering race
(backup-correct != reloaded-stale) AND transfer corruption, in one run. Since
the compose/attention is identical fresh-vs-reload and stateless-per-forward,
all-ranks byte-correct shards => correct output, so this per-rank value check is
complete for the round-trip. Gated at SGLANG_CP_HICACHE_KV_TRACE=2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Heavily-forked CP HiCache produces KV corruption only after an L1->L2->L1
round-trip (eviction + reload), scaling with cached volume. Static reading and
upstream-diff are exhausted, so trace the data flow and check four invariants
directly, gated by SGLANG_CP_HICACHE_KV_TRACE (0=off, 1=structural lifecycle,
2=+compose/free/ack), off by default and cheap when off.
Trace points (keyed by node_id; rid_map ties client rid->node_id):
- backup_reserve / backup_d2h: host<->phys slots written (H2/H4)
- split: prefix+suffix partition vs parent (H1)
- evict: write_pending at device free (H4)
- reload_node: host slots read on reload (H2)
- reload_assign: reloaded device locs -> forward
- dev_free / write_ack: device free vs backup-complete ordering (H4)
- compose / remap: per-forward row-id cache reuse + unmapped count (H3)
New mem_cache/cp_hicache_trace.py (cptrace/rng helper; values rendered
space-free for offline parsing). The companion analyzer joins these by
node_id; the decisive, request-independent check is H2: any node whose reload
host-slot fingerprint differs from its backup fingerprint is the corruption
source.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build_flattened_request_row_ids ran once per layer in the per-layer
prefill attention loop and rebuilt the indexer-seq-len-derived row ids
on the GPU via repeat_interleave with a *device* repeats tensor, which
forces a device sync (it reads sum(repeats) to size the output) and
serializes the launch thread every layer.
The row ids depend only on indexer_seq_lens_cpu, which is identical
across every layer of a forward pass. Add
get_cp_shared_kv_flattened_request_row_ids, which builds them once and
caches on the ForwardBatch (keyed by expected flattened length + device
so a different batch shape or the draft-vs-target pass never reuses a
stale tensor), and build on CPU then move once to drop the sync.
In a TP-5 warm cachebench trace this collapsed 1817 wrapper calls to 46
actual builds (97.5% fewer) and dropped ipc_materialize visibly.
GSM8K 200q x2 = 0.955 / 0.950, 0 invalid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EAGLEWorkerV2's target prefill always captured the global FULL hidden,
so the bs>1 NSA CP shared-KV draft extend received a global hidden whose
row count (real total tokens) matched neither the per-rank CP-local count
nor the MLP-sync CP-aligned padded count, tripping
[CP_SHARED_KV_FAIL_FAST][draft_batch_gt1_spec_hidden_shape_mismatch]
under SGLANG_ENABLE_SPEC_V2=1.
Mirror the legacy EAGLEWorker contract: add _can_use_cp_draft_shared_kv
and, when CP draft shared-KV applies, capture the CP-local hidden side
channel (CaptureHiddenMode.NULL + capture_draft_hidden_states) instead of
FULL. The v2 consumer already prefers draft_hidden_states when present
(commit 5e22279670 added the consumer but not this producer side).
Fixes the shape at the source rather than loosening the NextN fail-fast.
CP-off keeps FULL; non-CP / bs=1 unaffected; legacy v1 path untouched.
Validated on g0033 PD (prefill + 2-node decode): bs>1 up to 17 incl. the
former-crashing bs=6, 0 non-400 errors, GSM8K 0.965 / 0 invalid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deferred non-chunked inserts can happen while a CP HiCache per-layer backup is still in flight. Rolling back the unattached prepared backup at unfinished-request time drops the only reservation that final insertion can attach, so cache_finished_req has to start a post-forward backup and catch up all layers synchronously.\n\nKeep the prepared backup for non-chunked deferred inserts and continue rolling it back for chunked middle inserts, where the suffix may be extended by later chunks and the old backup would cover the wrong range. Document the failure mode so future changes do not rediscover the same fallback path.\n\nConstraint: CP HiCache radix split cannot mutate in-flight backup nodes.\nConstraint: Chunked prefill middle inserts still need rollback because their backup range is not final.\nRejected: Always rollback unattached backups | causes post-forward catch_up_all_layers for non-chunked deferred inserts.\nRejected: Always preserve unattached backups | can attach stale backup ranges for chunked middle inserts.\nConfidence: high\nScope-risk: narrow\nDirective: Do not clear req.cp_hicache_prepared_backup on non-chunked deferred insert without proving final insert no longer needs it.\nTested: remote cjy-glm5-new py_compile radix_cache.py\nTested: remote cjy-glm5-new pytest test_cp_hicache_metadata.py::{nonchunked preserve,chunked rollback} => 2 passed\nNot-tested: live ETE replay after restarting prefill with this exact commit
Decode DP dispatch was collapsing onto a few ranks because the controller only randomizes among exact minimum load pairs. The load snapshot undercounted decode handoff work: pending prefill-info requests were absent, and DecodeRequest wrappers in prealloc/transfer queues were skipped because their rid lives on .req.
This makes scheduler load accounting unwrap DecodeRequest items and include pending decode requests, so TOTAL_TOKENS sees queued handoff backlog instead of repeatedly treating busy ranks as empty.
Constraint: Do not mask imbalance with synthetic per-request token penalties; dispatch should be driven by accurate observed load.
Rejected: Add req*4000 or other queue penalties | heuristic, workload-dependent, and hides the accounting bug.
Confidence: medium
Scope-risk: moderate
Directive: Any new decode handoff queue must be included in get_load() or DP routing can regress to stale/min-load collapse.
Tested: Remote cjy-glm5-new: PYTHONPATH=python python -m pytest -q test/registered/unit/observability/test_scheduler_metrics_load.py test/registered/unit/managers/test_prefill_adder.py -> 27 passed.
Not-tested: Fresh decode ETE distribution after service restart.
The affinity scheduler needs to know whether the current batch is led by a chunked request. The rebase carried call sites that referenced an old private field name, while PrefillAdder only retained a boolean flag, causing startup failure before scheduling could run.
Constraint: CP bs>1 affinity must classify a chunked-led batch without reopening chunked-tail mixing behavior.
Rejected: Recreate the old private _chunked_req_in_batch attribute | keeps a stale name and hides the public state transition in PrefillAdder.
Confidence: high
Scope-risk: narrow
Directive: Keep chunked_req_in_batch and has_chunked_req_in_batch updated together when adding new chunked admission paths.
Tested: Remote cjy-glm5-new: PYTHONPATH=python python -m pytest -q test/registered/unit/managers/test_prefill_adder.py -> 26 passed; combined run with scheduler load accounting tests -> 27 passed.
Not-tested: Full ETE restart after this commit alone.
Revert the tail-chunk co-batching gate from 54c056af because allowing a continued chunk to share the next CP bs>1 batch reopens the mixed chunk/page-tail scheduler risks we are currently avoiding. Keep the independent real-prefix budget accounting so chunked requests still contribute their carried prefix to CP cached-token and buffer estimates.\n\nConstraint: Chunked-prefill requests must remain solo until the CP split/page-tail contract is revalidated for mixed batches.\nRejected: Full revert of 54c056af | it would also drop true-prefix budget accounting and under-estimate cache/buffer pressure for admitted chunks.\nConfidence: high\nScope-risk: moderate\nDirective: Do not reintroduce tail-chunk co-batching without tests covering page-tail split, CP buffer admission, and ETE chunked+cache-hit replay.\nTested: Local py_compile for environ.py, schedule_policy.py, cp_shared_kv_compose.py, test_prefill_adder.py, test_cp_shared_kv_compose_v2_8rank.py.\nTested: Remote cjy-glm5-new PYTHONPATH=python pytest -q test/registered/unit/managers/test_prefill_adder.py -> 25 passed.\nTested: Remote cjy-glm5-new PYTHONPATH=python pytest -q test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py -> 157 passed, 2 subtests passed.\nNot-tested: Full mixed replay with chunked-prefill traffic after service restart.
The syh rebase kept callers that expect reusable batch slot spans, while the restored CUDA IPC runtime lacked the helper and indexer still passed a symm-only writer-rank argument. Restore the batch-scoped span cache and remove the stale symm argument so the production compose path stays on CUDA IPC with exact per-request spans.\n\nConstraint: Production main-stream compose should use CUDA IPC, not symm writer-rank routing.\nRejected: Re-enable symm writer-rank parameters | benchmark showed no main-stream win and callers fail against IPC runtime contracts.\nConfidence: high\nScope-risk: narrow\nDirective: Keep slot-span metadata batch-scoped; do not rebuild Python span descriptors per layer without benchmarking.\nTested: Local py_compile for cp_shared_kv_runtime.py, cp_shared_kv_prefetch.py, nsa_indexer.py, nsa_backend.py.\nTested: Remote cjy-glm5-new pytest test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py -> 157 passed, 2 subtests passed.\nNot-tested: Full ETE prefill/decode runtime after restarting services.
The syh branch still passed symm writer metadata into the RAGGED MLA
partial-current compose path. After rebasing the CUDA IPC staging path,
those keyword arguments no longer belong to the runtime contract and would
raise at runtime before reaching the IPC compose implementation.
Constraint: Main-stream CP shared-KV compose should use the measured CUDA IPC staging path; symm remains an experimental/benchmark path.
Rejected: Keep writer-rank arguments behind the symm env | the runtime signature has been restored to the IPC contract and the env-gated symm compose is not the production default.
Confidence: high
Scope-risk: narrow
Tested: python -m py_compile nsa_backend.py cp_shared_kv_runtime.py cp_shared_kv_prefetch.py nsa_indexer.py
Not-tested: Local pytest blocked by missing orjson in this environment; remote CUDA/ETE not run in this step
Expected no-prefetch paths were polluting production logs: no cache prefix, tiny/first-layer windows, and FP8 RAGGED top-k were being reported as fallback warnings. The prefetch contract now treats zero-prefix and first-layer misses as normal skips, while preserving warnings for non-zero misaligned prefixes and real consume misses after the first layer. The same change keeps RAGGED cache-hit prefetch eligible and records the CE/IPM prefetch contract in the plan doc.
Constraint: FP8 sparse prefill uses RAGGED top-k, but CP shared-KV prefix materialization is still page-slot based
Constraint: Layer 0 has no previous attention-window hook that can have prefetched the layer
Rejected: Warn whenever a prefetcher is absent | no-cache and too-short requests are expected synchronous paths and make logs unusable
Confidence: high
Scope-risk: moderate
Directive: Keep CP_SHARED_KV_FALLBACK warnings for unexpected contract failures only; use debug logs for expected skip paths
Tested: Local py_compile for cp_shared_kv_prefetch.py, nsa_indexer.py, nsa_backend.py
Tested: Remote cjy-glm5-new targeted regression: 3 passed, 21 warnings
Tested: Remote cjy-glm5-new full test_cp_shared_kv_runtime.py: 156 passed, 21 warnings, 2 subtests passed
Not-tested: New ETE run after prefill restart to confirm log volume reduction in production traffic
(cherry picked from commit e08e321e5929fdbb30102ec0b19c6ff0ecac7e7e)
CP shared-KV slot remaps already have forward-batch lifetime, but the IPC materialize path rebuilt owner/source/dense descriptor tensors on every layer. Cache prefix/current IPC descriptors on the token and paged slot-remap objects, keyed by layout, spans, device, descriptor kind, and prefix capacity, so all model layers can reuse the same request/batch-plan descriptors.
Constraint: Small-extend cache-hit workloads showed descriptor setup could exceed the all-reduce baseline before any IPC kernel work ran.
Rejected: Global descriptor cache | slot-remap lifetime is safer and avoids stale entries across request/batch-plan changes.
Rejected: Cache without physical page capacity | prefix descriptors encode capacity-invalid pages and must miss when capacity changes.
Confidence: high
Scope-risk: moderate
Directive: Do not reuse descriptors across different slot_logical_pages identity, CP layout, spans, device, or prefix capacity; stale descriptors can alias dense slots across requests.
Tested: Local py_compile; local git diff --check; remote g0034 cjy-glm5-new targeted descriptor tests 2 passed; remote full test_cp_shared_kv_runtime.py 146 passed, 21 warnings, 2 subtests passed.
Not-tested: Full ETE throughput/accuracy after descriptor cache; CUDA service benchmark still needed to quantify speedup.
(cherry picked from commit addd1ca1571e41458315d15304a0e841682fe8fa)
Cache-hit bs>1 current reuse can create very large dense attention buffers
while touching only a small set of current pages. The previous SGLang runtime
asked tai-kernel for a staging buffer sized like the full dense tensor, which
caused CUDA OOM before the current IPC fast path could run.
Switch token and index current IPC helpers to descriptor-compact staging: publish
the dense destination pages into compact staging slots and materialize peers
from compact source page ids back to the original dense destination pages.
Document the failure mode and the compact-staging contract so the dense-sized
contract is not reintroduced.
Constraint: CUDA + SGLANG_CP_SHARED_KV_USE_TAI_MATERIALIZE=1 must fail fast instead of silently falling back to current-slot all_reduce
Rejected: Let staging allocation failure fall back to all_reduce | hides the bug and restores the expensive collective path
Rejected: Size staging by the full dense tensor | reproduces the 965MB staging OOM on long-prefix cache-hit batches
Confidence: high
Scope-risk: moderate
Directive: Current IPC helper source ids are compact staging ids; destination ids remain dense slot pages
Tested: Remote cjy-glm5-new PYTHONPATH=python:/mnt/beegfs/cjy/tai-kernel/python python -m pytest -q test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py -> 144 passed, 2 subtests passed
Tested: Local py_compile cp_shared_kv_runtime.py
Not-tested: Full ETE service restart with production traffic after this commit
(cherry picked from commit 906ecbe5d4f08b73242e98e2b628e26516d5b04a)
CP shared-KV cache-hit batches should compose long prefix pages and short current pages through page-slot IPC instead of falling back to dense all_reduce. Wire the runtime and prefetch consume paths to the TAI current-staging helpers, fail fast when the configured CUDA fast path cannot run, and document the bs>1 cache-hit benchmark evidence.
Constraint: bs>1 prefill must preserve the page-slot contract across fp8/bf16 and zero-lane current tails.
Rejected: Silent all_reduce fallback | hides correctness and performance regressions in production.
Confidence: medium
Scope-risk: moderate
Directive: Any future fallback in CP shared-KV CUDA fast paths must be explicit warning/fail-fast and covered by runtime tests.
Tested: Local py_compile cp_shared_kv_runtime.py and cp_shared_kv_prefetch.py; remote PYTHONPATH=python pytest -q test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py (144 passed, 21 warnings, 2 subtests passed); remote TAI IPC benchmark fp8 bs>1 cache-hit matrix recorded in docs.
Not-tested: Full ETE mixed replay after replacing all current collectives with IPC.
(cherry picked from commit 8aa3b4ce59e0ebef5da5b0d07499a5f1d9785997)
CP shared-KV bs>1 compose must not silently fall back to dense full-buffer collectives when CUDA TAI materialize is expected. The fallback masks both correctness contract drift and severe synchronization/communication regressions, especially while comparing the symm path with the older IPC path.\n\nThis keeps CPU/unit-test fallback available, but makes production CUDA+TAI runs raise an explicit compose_v2 fail-fast for token-KV and index dense fallback. It also records the symm-vs-IPC comparison contract so barrier and collective counts are evaluated alongside elapsed time.\n\nConstraint: Production cache-hit-heavy bs>1 paths must expose unexpected dense collectives instead of silently taking them.\nRejected: Cherry-pick the old IPC branch wholesale | it conflicts with the symm compose design and would mix two transport protocols before benchmarking.\nRejected: Allow dense fallback with warning only | warning can be missed and still corrupts performance conclusions.\nConfidence: medium\nScope-risk: moderate\nDirective: Do not re-enable dense full fallback in CUDA+TAI compose paths without a benchmark proving it is intentional and a correctness test covering cache-hit bs>1.\nTested: python -m py_compile for cp_shared_kv_runtime.py and test_cp_shared_kv_runtime.py; git diff --check.\nNot-tested: Remote container pytest/ETE; local pytest is not reliable in this workspace because dependencies such as orjson are missing.
- A chunked-led batch now seeds affinity_batch_warm_led from the CHUNK's
class instead of the first loop candidate: a 65536 first chunk is
cold-led (later colds are free density, caps bound them); latching
from a warm follower inverted cold-led-admits-everything and could
spuriously STOP the scan, under-filling the forward.
- The policy additionally requires no LoRA and no L3 hicache-storage:
their pre-classification continues make "first classified candidate"
drift from the true FCFS head, mis-routing the defer accounting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
专题 S4 (design: docs_internal/perf/prefill-compute-intensity-plan.md S4,
amended). Under FCFS a cold request joining a warm-led batch turns a
1-2s cache-hit forward into a 5-10s one, splitting the warm work into
the 新-cache-新 pattern. The policy prevents exactly that one thing:
- WARM candidates always admit (into a cold-led batch they are free
density — the cold extend dominates the forward anyway).
- COLD admits into an empty or cold-led batch (small colds co-batch
today; the FCFS head always starts a batch so the queue keeps moving).
- COLD into a WARM-led batch is skipped, bounded by a per-pass window
(W=16 skips), a head defer count (K=3 passes) and an age bound
(T=5s). On any bound the scan STOPS instead of force-admitting: the
cold waits for the same forward either way, but leads its own clean
batch next pass instead of polluting this one.
The skip is strictly post-match / pre-admit (after init_next_round_input,
before add_one_req): no lock, no allocation, no budget mutation to
unwind, and re-matching a skipped candidate next pass is exactly what
the scan already does after a cap rejection. Classification is the
in-scan match result (device prefix + host hit vs a 64-token floor) —
under FCFS+L2 no pre-scan signal exists, so this adds zero matching
work for inspected candidates. Disabled wholesale under priority
scheduling (the skip must not reorder across priority classes).
Three amendments vs the design draft, reasoned in the decision-table
docstring: cold+cold-led admits (STOP would regress today's small-cold
co-batching); starved heads STOP rather than force-admit (clean batch
boundaries at identical latency); priority interaction handled by
disabling rather than per-request comparison.
Decision logic is a pure function with table + bounds unit tests
(28/28 adder suite green). Default OFF.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
专题 S1 (design: docs_internal/perf/prefill-compute-intensity-plan.md
S1.0-S1.11). A batch containing a chunked prefill request has been
forced to bs=1 by the CP gate, so every chunk of a long prompt
monopolizes a forward while short-extend cache-hit continuations queue
— the direct cause of the replay TTFT tail (p90 19.3s / p99 49.2s at
91.8% cache hit). Yet mixed chunk batches already occur today (a
freshly-chunked request keeps earlier-admitted small requests), proving
the CP forward path is mixed-chunk-safe; only admission was asymmetric.
Three changes, the first flag-independent:
- add_chunked_req now seeds the budget with the chunk's TRUE prefix
(was 0), so the CP cached tally and the buffer estimator's mqa_logits
k_rows see the chunk's footprint before any later request is admitted
(landmine D1).
- New SGLANG_CP_PREFILL_MIX_CHUNKED (default OFF): with it on, the gate
admits requests after a chunked one and lets the existing CP caps
(extend / cached / buffer, now correctly seeded) bound the batch — a
FULL chunk still ends the scan by consuming the chunk-clamped extend
cap; only a tail chunk leaves headroom. A chunked prefix that is not
page-aligned (rare sub-page final-chunk tail) keeps its batch solo
(the CP page-aligned split would fail-fast otherwise).
- The symm staging capacity identity (admission extend cap + request
slack == staging pages) is asserted when the flag is on, locking the
coupling the design relies on (plan doc S1.4 I2).
Tests: 4 new adder units (budget seeding; tail chunk admits followers;
full chunk solo by budget; non-aligned prefix solo); the 8-rank
byte-exactness scenario gains a chunk-shaped request (2048-token
page-aligned carried prefix + 512 extend) — all four phases
(legacy/v2/symm/prefetch) byte-identical on g0033. Known pre-existing
cross-file pollution noted in problems.md P16.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fingerprint hashes the whole source tree, so ANY code difference
between the prefill and decode trees tripped it — and it raised inside
try_ensure_parallel_info on the DECODE scheduler's event loop, crashing
the decode cluster. In practice that means a prefill-only restart with
a one-line fix (today: the inflight liveness fix) kills decode.
The checks that actually guard the KV transfer contract (page size,
kv cache dtype) remain hard errors; the fingerprint is now a warning
that still surfaces both hashes and source roots.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
E2e caught one request wedged FOREVER in disagg_prefill_inflight_queue
(probes 21s apart with zero traffic both showed #inflight-req: 1, no
reap/timeout warnings ever logged). Mechanism, established by reading
the full state machine: prefill Success is set locally by the transfer
worker on the LAST chunk; if the decode peer is torn down between the
handshake and the prefill's final send(), add_transfer_request silently
drops the chunk (no transfer destinations) — Success becomes
unreachable. The only external rescue, the decode ABORT notification,
is best-effort (silently swallowed on send error, no-op if it races the
room registration), there is no prefill-side heartbeat of decode
sessions, and the sender's only timeout covers Bootstrapping — the
inflight queue itself has no liveness bound. The orphan pins the
request's KV pages and rides every poll collective.
Two fixes, both reaped through the existing Failed branch via the
CP/TP MIN-reduce poll consensus (Failed=0 wins, so one rank concluding
flips every rank together — rank-uniform by construction):
- add_transfer_request: a room with no transfer destinations that is
NOT already Success (the dummy-rank handshake marking) now concludes
Failed loudly instead of dropping the chunk silently.
- Inflight residency timeout: entries stuck in a non-terminal poll
state past SGLANG_DISAGGREGATION_INFLIGHT_TIMEOUT (default 300s,
matching the sibling BOOTSTRAP/WAITING timeouts) get sender.abort()
and reap on the next poll. Covers what the hardening cannot: lost
ABORT datagrams, decode crashes.
Known sibling gaps left for follow-up: the decode transfer queue has
no Transferring liveness bound, and an abort that matches no queue is
still a silent no-op (much narrower race than first thought — work
requests are ordered before control requests within a tick).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First e2e launch crashed every CP rank at the symm token fill:
index_copy_cuda is not implemented for Float8_e4m3fn — the production
KV pool dtype, which the 8-rank test missed by building its pools as
uint8. The fill is a whole-token-row copy, so it is dtype-agnostic:
both the staging span and the current rows now go through uint8 views.
The 8-rank test now builds the KV pools and current rows as
float8_e4m3fn (payloads constructed as bytes, compared as bytes) so
the production dtype is what every phase exercises.
Validated on g0033: all four 8-rank byte-exactness phases under fp8.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opus review of the symm prefetch wiring found a latent group deadlock:
the prefetch hit path never touched _agreed_tai_ipc_peer_ptrs, so the
first use of a layer's pool tensor — which issues a capability MIN
all-reduce plus an IPC handle all-gather — only happened on the sync
fallback. With every rank hitting from batch one (the prefetcher runs
a layer ahead), a later lone-rank miss would be the only rank issuing
those collectives. Both consumes now seed the (idempotent, per-pool-
tensor-cached) agreement at entry, before any per-rank early return;
the index consume gets the pool buffer from its indexer call site.
Unreachable in today's config (prefetch env off) but a blocker for
ever enabling it.
Also per review: the [pool|staging] combined pointer-table cache now
keys per pool table instead of holding one — the token and index pool
tables alternate every layer and evicted each other, so the cache
never hit; and a debug-gated check that current locs all map into the
staging page inverse (index_copy_ has no skip semantics for stray -1).
Validated on g0033: 151 unit tests; all four 8-rank byte-exactness
phases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bs=1 MLA/index prefetchers replaced their consume-side trailing-range
NCCL all-reduce with the staging exchange: fill current rows straight into
this round's staging span (token-KV collapses to one cached index_copy;
the index fill kernel is just pointed at the staging page inverse),
cp_symm_barrier, then gather ALL current pages — this rank's own included
— from the stagings into the prefetched dense buffer. The symm+prefetcher
FAIL_FAST is gone.
Rank-uniformity moves with it: staging registration now also happens in
maybe_create (batch-logical gates, before any per-rank miss can diverge),
because with a prefetcher active the sync compose runs only on per-rank
misses and its lazy collective registration would hang. A hit/miss
divergence itself stays barrier-safe — both the prefetch consume and the
sync-compose fallback execute exactly one begin_round + barrier per
(layer, kind), and the counting barrier is shape-free (unlike the AR pair
it replaces, which would shape-mismatch).
Found by the new index test phase: the fill/remap kernel family skips
page id 0 as the SGLang dummy page, so a 0-based first staging slot was
never written. The staging layout now reserves row 0 (slot of current
page i = i + 1) for every kind, matching the convention instead of
depending on per-kernel behavior.
Launch-path cost: per-(kind,parity) peer pointer tables and the
[pool|staging] concatenations are precomputed/cached (identity pinned by
holding the pool-table reference); all prefetch descriptors, staging row
indices, and mixed_locs are built once per batch.
Validated on g0033 8xH200: 151 unit tests; 8-rank byte-exactness for
token sync symm (8 layers), index sync symm (4 layers, new phase), and
MLA + index prefetch consume_prefix_with_current vs the legacy sync
compose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The publish-variant staging exchange measured ~equal to the default
compact-current AR (90.4 vs 86.5 ms/batch on the traced scenario): the
publish copy and the barrier serialized behind the 0.65 ms prefix
gather ate the transport win the isolated current exchange showed
(0.196 vs 0.354 ms). Fix the structure instead of the copy: current
rows are now written straight INTO the staging — the fill kernels take
their write destinations solely from page_inverse, so a per-batch
staging-remapped page inverse on the plan retargets them with zero
kernel changes — then cp_symm_barrier, then ONE slot-dense gather
covers prefix pages (pool pointers) and ALL current pages (staging
pointers, including this rank's own) through a concatenated 2*cp
pointer table where current slots carry owner = cp_size + writer and
src = staging slot. No publish copy, no prefix pre-gather, no second
gather.
The fused fill's loc outputs are dense-geometry-bound, so the token-KV
path computes mixed_locs/staging row indices once per batch (they are
layer-invariant) and the per-layer fill collapses to a single
index_copy_ into the zeroed staging span.
Benchmark (g0033 8xH200, byte-exact, idle-checked): 62.8 ms/batch vs
84.4 default Step A (-26%) and 60.8 ideal; publish variant was 88.0.
151 unit tests; 8-rank GPU byte-exactness vs v2 across 8 layers,
arena on and off.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>