Commit Graph

7458 Commits

Author SHA1 Message Date
17cbfa31c9 E1 lane-stats instrumentation + 2a reserve-at-admission (default-off)
Two CP HiCache observability/admission additions, both behind default-off env flags (behavior-neutral until enabled).

E1 instrumentation (SGLANG_CP_HICACHE_LANE_STATS_S=<sec>, default 0):
- environ.py: flag. hiradix_cache.py: _cp_maybe_log_lane_stats() (rank-0 per-owner-lane L2 occupancy + imbalance + hot-prefix histogram) wired into check_hicache_events; per-cause drop counters in _warn_cp_hicache_fallback.
- Used for E1: verdict = lane imbalance is a non-issue (1.02-1.03 across full fill->evict->refill, zero host_reservation_failed).

2a reserve-at-admission (SGLANG_CP_HICACHE_RESERVE_ADMISSION, default off):
- hiradix_cache.py: cp_host_backup_admission_budget() (min-over-ranks available+evictable host tokens from the replicated radix snapshot) + _cp_host_evictable_tokens_by_rank.
- schedule_policy.py: PrefillAdder rem_l2_backup_tokens field + worst-lane footprint debit + budget_state gate (OTHER).
- scheduler.py: pass cp_size to PrefillAdder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:03:14 +00:00
7de882b622 2b.0: adopt l2_pooling pooled-L2 mechanics (default-off)
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>
2026-06-20 11:57:25 +00:00
8f1e85a992 CP HiCache: replicated-clock SLRU eviction, collective-safe (Phase 1)
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>
2026-06-19 19:35:21 +00:00
8d73919d02 Gate CP HiCache write-backup on hard deficit, not the free-room watermark
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>
2026-06-19 12:14:36 +00:00
laoyao0822
96158fa110 Bound overlap prefill to one pending CP batch
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)
2026-06-19 06:14:20 +00:00
512fe92a83 Fix CP HiCache catch_up_all_layers fallback on chunked-prefill final chunk
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>
2026-06-18 11:54:24 +00:00
a4ec2236ef Fix spec-decode grammar mask crash under xgrammar 0.2.1 (strict tvm-ffi binding)
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>
2026-06-18 07:22:26 +00:00
40738f59cf Cache EnvField reads to avoid per-call os.getenv in the hot path
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>
2026-06-17 20:06:55 +00:00
820790b97f Remove CP HiCache debug instrumentation
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>
2026-06-17 19:59:01 +00:00
2212963a6c Harden OpenAI tool-call/chat-template + reasoning parsing
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>
2026-06-17 19:58:42 +00:00
e23168e7f5 Fix CP shared-KV bs=1 cache-hit zero-prefix corruption; spans are sole compose input
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>
2026-06-17 18:25:22 +00:00
b34e7cb932 Add SGLANG_CP_TRANSFER_LOG: decouple CP shared-KV transfer-partition dumps from debug flag
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>
2026-06-17 16:29:31 +00:00
268bcf78da Dump: add 'fh' mode (final_hidden only, fresh prefills allowed) for cache-bust vs cache-hit
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>
2026-06-17 16:10:41 +00:00
d6aac79eb1 Lean tensor dump: residual trajectory only (attn_in/attn_out + q_all), drop kv/MoE
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>
2026-06-17 15:09:39 +00:00
9031a294b3 Complete per-forward tensor dump: all-layer trajectory + final hidden, flush per (rid,rank)
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>
2026-06-17 14:35:43 +00:00
ed42a6dcbc Honor x-request-id as rid + env-gated NSA tensor dump for L1-vs-L2 reload diff
- 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>
2026-06-17 14:05:54 +00:00
e4e0784387 CP HiCache trace: content-key in fwd_hash for L1-hit vs L2-reload differential
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>
2026-06-17 11:20:54 +00:00
25c2d6d606 CP HiCache trace: cross-rank loc-divergence confirm (visible_locs_hash + victim_set + release_draw)
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>
2026-06-17 08:29:54 +00:00
1d54b0b7ab CP HiCache trace: reload per-page owner-check (the make-or-break invariant)
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>
2026-06-17 04:52:17 +00:00
50d1c2fc5a CP HiCache trace: compose gather-output + span-owner + descriptor + MoE-stage hashes
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>
2026-06-17 04:03:47 +00:00
a447ae8317 CP HiCache trace: NSA indexer-K round-trip hash + forward-side attn/MoE hashes
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>
2026-06-17 03:05:30 +00:00
e3cfba8441 CP HiCache trace: key KV-value hashes by host-slot fingerprint + zero-KV flag
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>
2026-06-17 02:29:00 +00:00
5ed20f493a CP HiCache trace: add KV-value round-trip fingerprint (backup vs reload)
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>
2026-06-17 02:04:05 +00:00
38ef664b4c Add env-gated CP HiCache KV round-trip corruption tracing
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>
2026-06-17 01:14:31 +00:00
34763d92b1 Cache CP shared-KV flattened request row ids per forward
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>
2026-06-13 13:19:46 +00:00
2c7353d061 Fix spec_v2 CP draft extend hidden-state shape mismatch
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>
2026-06-13 09:06:00 +00:00
laoyao0822
1d168d061f Preserve pending CP HiCache backups through final insert
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
2026-06-13 03:44:23 +08:00
laoyao0822
ceb5345410 Account decode handoff queues in DP load snapshots
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.
2026-06-13 02:35:10 +08:00
laoyao0822
69ca7045ea Preserve chunked request affinity state
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.
2026-06-13 02:34:55 +08:00
laoyao0822
ee843a946b Keep chunked CP prefills solo during bs>1 admission
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.
2026-06-13 01:28:35 +08:00
laoyao0822
254d667853 Keep CP batch slot descriptors on IPC-compatible runtime
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.
2026-06-13 01:15:33 +08:00
laoyao0822
e0c388076e Keep CP compose on CUDA IPC after syh rebasing
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
2026-06-13 01:02:56 +08:00
laoyao0822
4d5c7f32d6 Keep CP shared-KV prefetch warnings actionable
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)
2026-06-13 00:59:33 +08:00
laoyao0822
cd4412a4b8 Reuse IPC descriptors across CP shared-KV layers
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)
2026-06-13 00:58:47 +08:00
laoyao0822
bafb55044b Keep CP current IPC staging proportional to touched pages
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)
2026-06-13 00:58:47 +08:00
laoyao0822
142e7a5a64 Keep CP shared-KV fast paths off dense current collectives
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)
2026-06-13 00:58:47 +08:00
laoyao0822
2387787ebc Fail fast when CP compose would hide dense fallback
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.
2026-06-12 23:57:40 +08:00
cf46defddd Harden the affinity wiring per opus review (F-1, F-2)
- 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>
2026-06-12 08:07:36 +00:00
6a1e862f48 Group cache-hit prefills into dense batches (SGLANG_CP_PREFILL_AFFINITY_GROUP)
专题 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>
2026-06-12 07:44:39 +00:00
54c056af83 Let tail chunks co-batch with cache-hit requests (SGLANG_CP_PREFILL_MIX_CHUNKED)
专题 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>
2026-06-12 06:38:47 +00:00
a75164ef48 Demote the P/D source-fingerprint mismatch to a warning
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>
2026-06-12 05:15:11 +00:00
58f2350738 Give prefill inflight transfers a liveness bound
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>
2026-06-12 04:55:35 +00:00
d66758d929 Run the symm staging fill on byte views (no fp8 index_copy kernel)
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>
2026-06-12 04:02:19 +00:00
9c6cf278b0 Seed the IPC agreement before prefetch hit/miss can diverge
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>
2026-06-11 22:56:31 +00:00
1923fcd67e Wire the symm current exchange into the CP shared-KV prefetchers
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>
2026-06-11 22:39:47 +00:00
d63fbd4d79 Fold the symm compose into one barrier-gated mega gather
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>
2026-06-11 22:09:29 +00:00
e8e5edb230 Shrink the symm compose region to a compact current-page staging
Peers only ever read the CURRENT pages of a rank's compose output — the
prefix comes straight from the IPC-registered KV pool — so the symm
region does not need to hold the whole dense buffer (pool-bound ~2.5 GB
double-buffered slab).  It now holds one round of current pages in
merged-span order (extend-cap-bound, ~58-100 MB), and dense buffers
become purely rank-local (plain allocations or the optional local arena;
COMPOSE_SYMM no longer requires COMPOSE_ARENA).

Exchange per compose call: publish my written current pages
dense[page] -> staging[slot i] (slot = the page's batch current index,
identical on every rank, so peers address each other's staging with no
per-batch handshake), cp_symm_barrier, gather peers'
staging[writer][slot] -> dense[page] via the existing src!=dst page
gather.  Reuse safety keeps the parity-half distance-2 argument, now on
the staging.  Capacity sizing comes from the admission caps
(max_total_extend_tokens / max_batch_requests) with a pool-derived
fallback and the SYMM_HEAP_MB override; overflow fails fast
(batch-logical, hence rank-uniform).

Idea credit: laoyao0822's touched-pages-proportional staging
(906ecbe5d4), rebound onto our barrier-gated, group-agreed transport.

Validated on g0033 8xH200: 151 unit tests; 8-rank GPU byte-exactness
vs compose_v2 across 8 layers (arena on and off, parity halves
exercised); benchmark path e (real protocol) byte-exact, current-page
exchange 0.196 ms vs 0.354 ms compact-AR isolated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:50:34 +00:00
laoyao0822
f74f0a557f Keep CP pending-split probes collective-free
Radix insert can reach the pending-backup/stale-tail split probe on only a subset of CP ranks. The previous opportunistic ack drain called writing_check(), which may issue a scalar write-visibility collective while other ranks are polling inflight transfer state with a different tensor shape. That ordering mismatch matches the observed Gloo 4-vs-2 abort after CP_HICACHE_FALLBACK insert_deferred_pending_backup_split logs.

This keeps the split probe local and conservative: it leaves ready write acks queued and lets the globally ordered scheduler/cache-event path drain them. The regression test locks the contract by making any collective from the pending-split probe fail.

Constraint: CP radix insert/pending-split probing is not globally ordered across ranks.
Rejected: Drain ready write acks from the split probe | can interleave with scheduler poll collectives and corrupt distributed collective ordering.
Confidence: high
Scope-risk: narrow
Directive: Do not call writing_check() or any CP/TP collective from radix pending-split probes; drain write acks only from globally ordered paths.
Tested: Remote cjy container: PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_cp_hicache_metadata.py => 126 passed, 21 warnings.
Tested: Remote cjy container: python -m py_compile python/sglang/srt/mem_cache/hiradix_cache.py => PY_COMPILE_OK.
Not-tested: Full ETE prefill/decode replay after this change.
Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-06-11 21:13:54 +00:00
a24111a5f4 Cut per-layer CPU on the prefill launch path: validators, plans, spans
From the nsys CPU-gap attribution (launch thread, one 78-layer forward:
374ms API time; 642 cudaStreamSynchronize blocking 89.5ms and overlapping
122ms of the 505ms GPU idle; ~44ms pure-Python before concat_mla_absorb_q):

- memory_pool_host: skip validate_page_aligned_token_indices on CUDA
  tensors in _get_indexer_page_indices and _prepare_load_page_indices —
  torch.any/torch.equal there cost a queue-deep cudaStreamSynchronize per
  layer-group submit (~0.42ms each, ~12.7ms/forward measured). Same
  construction-based-invariant guard the CacheController pair check
  already documents; CPU/test tensors stay validated.
- nsa_indexer: per-batch _CpRaggedIndexPlan replaces the per-F-layer
  rebuild of the O(total-q-tokens) topk offset list and the 6-7 int32
  ragged descriptor tensors (segment records, kv_lens/q_starts/q_lens/
  k_bases/q_bases/current_bases). All inputs are batch metadata; the plan
  is anchored on the forward batch with a content key over cp_index.
- nsa_indexer forward_indexer: read seq_lens_cpu instead of a device
  seq_lens[i].item() per request per layer (one stream sync each).
- cp_shared_kv_runtime: get_or_build_batch_slot_spans caches the
  layer-invariant prefix/current slot spans per batch (the builders read
  logical_pages only for its shape); nsa_backend x3 + nsa_indexer call
  sites switched.

Microbenchmark (idle H200, traced batch shape bs=12 / 44.6K q tokens,
test/manual/bench_cpu_gap_fixes.py, equality-checked): validator path
197.1us -> 59.2us per submit under a busy queue (x3.3); ragged plan
3238.6us -> 36.2us per layer (x90, ~128ms launch-thread time per forward
at 40 F-layers); slot spans 20.1us -> 0.5us (x41). Layer suites A/B vs
HEAD: identical failure set (5 pre-existing CPU-tensor indexer tests),
no regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:01:00 +00:00
b356773d2f Harden symm compose per review: prefetch gate, round parity, hot-path CPU
Addresses the opus review of the Step A/B compose stack:

- FAIL FAST on symm + prefetcher coexistence: the MLA/index prefetchers
  issue their own per-span collectives and bypass the symm exchange, so
  letting them coexist would make COMPOSE_SYMM a silent no-op on every
  prefetch hit (measured "parity" that never ran). maybe_build_current_
  page_writer_ranks now raises when a prefetcher is attached or
  SGLANG_CP_SHARED_KV_ENABLE_MLA_PREFETCH is set.
- Arena parity now derives from a monotonic round epoch instead of raw
  layer_id: EAGLE draft layers reuse decoder layer ids, so id-parity would
  stop alternating halves (draft L0 -> next forward target L0 lands on the
  same half) and repeated-id rounds would keep appending into one half.
  begin_round(layer_id, kind) starts a new round (other half, offsets
  reset) when the id changes OR a kind repeats; regression test included.
- Per-batch writer-ranks cache on the forward batch and a presence flag in
  the plan key: previously the ~bs x current-pages writer list was rebuilt
  AND tuple-hashed on every layer per call site, pure launch-path waste.
- Rank-uniformity invariant documented at _symm_exchange_current_pages
  (any per-rank gate must go through a group agreement first) and an
  actionable arena-overflow message naming SGLANG_CP_SHARED_KV_SYMM_HEAP_MB.

Deferred follow-ups (documented in the design doc): factor the two near-
clone v2 helpers, single barrier per F-layer (~308us/batch), persistent
device-side peer-ptr tables for the gather.

182 targeted tests + 8-rank GPU byte-exactness (v2 and symm) re-validated
on g0034 syh-dev-new.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:26:27 +00:00