[AMD] Strict bit-exact SWA HiCache for DeepSeek-V4 with unified_kv: SWA-window + c4/indexer state riding across L1/L2/L3 - #32214
Open
amd-danli103 wants to merge 43 commits into
Conversation
amd-danli103
requested review from
Fridge003,
Ying1123,
hnyls2002,
ispobock,
merrymercy and
xiezhq-hermann
as code owners
July 23, 2026 13:24
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
amd-danli103
requested review from
HaiShaw,
Qiaolin-Yu,
alphabetc1,
hanming-lu,
hebiao064,
hzh0425 and
yizhang2077
as code owners
July 23, 2026 13:24
21 tasks
The fused transfer keeps reading the host tile after it returns, so a concurrent promote or L3 fetch corrupts the device rows. The launches it saved are not worth an async contract on the byte-exact path.
One textual conflict: upstream added RadixKey.cache_salt at the in-batch prefix-caching match site, the same call this branch passes for_reuse=True to. Both kept.
One textual conflict: upstream reorganised environ.py into banner-delimited sections and replaced the `# CUDA Graph` comment this branch had appended its flag in front of. SGLANG_UNIFIED_KV_BIT_EXACT_HICACHE moves into the "Radix and sparse KV caches" section next to SGLANG_ENABLE_UNIFIED_RADIX_TREE rather than staying at the tail of the Mamba section.
amd-danli103
force-pushed
the
feat/unified-kv-swa-hicache
branch
from
August 18, 2026 04:10
0486f40 to
eccbb57
Compare
Upstream added a buffer-only host memory mode for HiCache (sgl-project#34798), which rewrote three places in swa_component.py. Two textual conflicts: - prepare_prefetch: upstream replaced the inline window-page math with full_window_pages and now allows a sub-window fetch at the root or in buffer-only mode. Taken as the non-strict branch verbatim; the strict ring-paged branch above it is unchanged. - build_hicache_transfers: upstream renamed sw_pages to num_pages. The strict-aware stride keeps the new name. One silent merge that needed a fix: upstream's new mid-tree graft guard in _commit_prefetch drops a window whose window_require_pages is below full_window_pages. That count is in strides, and the strict pool's stride is the ring (one ring, where full_window_pages counts two page_size pages), so merged as-is the guard would have dropped every strict window grafted mid-tree. The requirement is now derived from the same stride, which is identical to full_window_pages whenever the flag is off. Test fakes gain root_node, sliding_window_size and full_window_pages, and the _sync_trailing_keys tests now pass a controller stand-in: upstream consults the per-pool page size there to shrink a buffer the hit undershot, so a bare None self no longer works.
Two textual conflicts, both resolved as a union: - deepseek_v4.py: upstream rewrote the comment above `attn_k` for the new fused qk-norm-rope verify path (SGLANG_OPT_FUSED_QK_NORM_ROPE_VERIFY, default on), which hands back a non-None kv on target-verify instead of storing inside the fused kernel. The unfused path this branch was written against already returned a non-None kv there, so the SWA capture hook's `kv is not None` gate keeps its meaning; took upstream's comment and left the hook above it. - hybrid_pool_assembler.py: upstream added get_serving to the runtime_context import this branch had appended SpeculativeAlgorithm next to. Both kept. Upstream also moved DSAIndexerPoolHost out of memory_pool_host.py and migrated this file's hicache_size / mem_layout / write_policy / io_backend reads to get_memory(); neither touches the strict path (all of this branch's additions there live in DeepSeekV4PagedHostPool, and the projected values are the same ones ServerArgs carries). Signed-off-by: amd-danli103 <danli103@amd.com>
One textual conflict, in prepare_prefetch: upstream's HiCache host pool refactor (sgl-project#36232) replaced the direct _swa_kv_pool_host.alloc + evict_host + retry with host_pool_group.alloc(pool=PoolName.SWA, reclaim=...). That allocation is shared with the strict ring-paged branch above it, so the strict path moves onto the group call as well: every builder wires ComponentType.SWA from host_pool_group.get_pool(PoolName.SWA), so the component's _swa_kv_pool_host and the group's SWA entry are the same object and the group's alloc is the old alloc/reclaim/retry verbatim. Both branches keep their own num_tokens. The matching free in free_host_values merged without a conflict and resolves to that same pool. The strict prefetch test fake gains a host_pool_group that allocs from the pool the component already holds, mirroring how the assembler wires them. Signed-off-by: amd-danli103 <danli103@amd.com>
One textual conflict: sgl-project#33091 swapped the device_swa_evict_fn lambda for the _evict_swa_for_device_alloc helper and dropped the function-local EvictParams import this branch appended its write_through guard next to. Guard stays, import goes. Signed-off-by: amd-danli103 <danli103@amd.com>
amd-danli103
force-pushed
the
feat/unified-kv-swa-hicache
branch
from
August 26, 2026 11:46
c3d0eb0 to
d156531
Compare
Conflicts in assembler/swa_component/tree_core/server_args: keep 32214 strict SWA HiCache paths and main APIs (FP4 indexer, host_lock, layer_mappings). hicache_swa_offload_page_stride moved to arg_groups. Signed-off-by: amd-danli103 <danli103@amd.com>
Branching-point caching (sgl-project#34565) made finalize_match_result_in_tree_core align the full-KV hit to a page, which the SimpleNamespace tree_core in these tests does not carry. Signed-off-by: amd-danli103 <danli103@amd.com>
…/restore sgl-project#38192 already isolates the compressor read path per request. HiCache write/restore still used swa_loc, so a restored window landed on the wrong C4 rows. Switch the four capture/restore helpers to translate_from_req_position_to_state_loc. Signed-off-by: amd-danli103 <danli103@amd.com>
…capacity Slack is ceil(ISL/(page*stride))+1 at per-rank batch 1, with ISL min(200k, context_length) and page the resolved tree page size, not 16 * max_running_requests. 1.5x-of-durable bounds the derived value while an explicit SGLANG_SWA_HICACHE_STATE_STAGING_PAGES is taken as-is, and the DRAM guard now weighs the coupled c4/indexer pools. Signed-off-by: amd-danli103 <danli103@amd.com>
…ol_idx A second cleanup_after_caching_req silently replaced the upstream one, so out-of-window SWA slots were never freed. Also req.req_pool_idx moved to req.kv in the last main merge. Signed-off-by: amd-danli103 <danli103@amd.com>
Only strict SWA can let a device-only validator outrun the host-gated boundary. Other trees skip the clamp instead of relying on min() being an identity. Signed-off-by: amd-danli103 <danli103@amd.com>
Ring mode pages full attention only and never writes full_to_swa_index_mapping, so every page rep resolved to the padding slot and free_page_ids kept handing page 0 back until available_size ran past size on the first grouped teardown.
…stream The check re-read dev[state_locs] after a host sync, by which point the compressor forward had rewritten the same (r, pos % ring) rows, so it compared the host tile against post-forward state and failed whenever the forward got there first.
restore_swa_windows writes the per-request ring from the scheduler stream while the previous forward is still writing the same r * ring + pos % ring rows, so the restored boundary got overwritten. Wait on load_fence_stream, the same fence start_loading already applies to load-back H2D for this hazard.
…joint free EAGLE +1 on a page-aligned SWA branch emits token-adjacent tails that still share a page; merging them in free_kv_row matches the existing comment and stops decode warmup from tripping _page_disjoint.
Three textual conflicts: - deepseek_v4_backend_hip_radix.py: upstream (sgl-project#38947) replaced the get_swa_out_cache_loc / get_unified_swa_loc docstrings with one-line comments. This branch had only reworded them, so upstream wins and the reword leaves the diff. - deepseek_v4_memory_pool.py: same PR renamed get_c128_state_buf_infos to get_request_state_buf_infos, which this branch had appended get_swa_state_coupling_infos in front of. Both kept, new name taken; the two call sites merged clean on upstream's side. - swa_component.py: upstream commented the host_lru lookup in redistribute_on_node_split, which this branch had hoisted above the strict ring-page split. Nothing used it in between, so the hoist goes and upstream's placement stands.
ServerArgs stopped being a dataclass upstream, so __dataclass_fields__ is gone; msgspec.structs.fields still carries the arg_groups default.
…covers it Ours returned before clear_full_to_swa_mapping; the landed version clears the mapping first, which test_direct_free_clears_mapping_in_ring_mode pins. allocator/swa.py is now byte-identical to main.
amd-danli103
force-pushed
the
feat/unified-kv-swa-hicache
branch
from
September 11, 2026 06:37
110b435 to
a24122d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
DeepSeek-V4's
unified_kvbackend on ROCm packs three KV families in one pool: the content-addressed compressed KV (C4/C128), the per-request SWA ring (addressed byreq_pool_idx * window + pos % window), and the compressor's c4 / indexer overlap state. Only the compressed KV is content-stable; the SWA ring and the c4/indexer overlap state are not, and are never written into the radix tree.Two prior PRs made reuse of such prefixes safe but only approximately correct:
swa_reprefill_tail_tokens()) rather than restored.Re-prefill recomputes the trailing window (and, implicitly, the c4/indexer overlap state for that region) instead of reproducing the exact values the original prefill wrote. That is enough to prevent gross stale-ring errors and to keep GSM8K-level accuracy within noise — but it is not bit-exact: a partial re-prefill cannot reconstruct the original SWA window and c4/indexer overlap state byte-for-byte, so the reused output can differ from a full fresh compute at the bit level.
This PR closes that gap. Instead of re-prefilling an approximation, it captures and offloads the true values of the SWA window and the c4/indexer overlap state across L1→L2→L3, and restores them exactly on a hierarchical hit — making unified_kv SWA prefix reuse strict and bit-exact. A hard reuse gate re-prefills only when the truth is unavailable (never a stale/approximate read).
This is an opt-in gated by
SGLANG_UNIFIED_KV_BIT_EXACT_HICACHE(default off), and it engages only when all of the following hold at once:unified_kv_tritonattention backend is chosen (SGLANG_HACK_FLASHMLA_BACKEND=unified_kv_triton);--enable-hierarchical-cache).In every other case, the feature is a strict no-op, no pool is allocated, no capture, no restore; behavior is byte-identical to
main.Modifications
Organized as three layers:
1. Substrate (Commit 1–5):
SGLANG_UNIFIED_KV_BIT_EXACT_HICACHEflag +--hicache-swa-offload-page-stridearg (environ.py,arg_groups/fields/memory.py).memory_pool_host.py).deepseek_v4_memory_pool.py,hybrid_cache/hybrid_pool_assembler.py).hicache_storage.py,storage/nixl/hicache_nixl.py).hybrid_pool_assembler.py): with the strict flag on, raise a clearValueErrorunless--hicache-write-policy write_through, before any host pool is pinned.2. Capture (Commit 6–8):
deepseek_v4_backend_hip_radix.py,model_runner.py,deepseek_v4.py).compress_hip.py,compressor_v2.py).3. Reuse/restore + strict gate (Commit 9–10):
buf_looffset on a hierarchical hit; strict reuse gate caps the match and re-prefills any tail that isn't provably bit-exact, never a stale read; commit-coupling guard keeps window+state+full-KV committed atomically (swa_component.py,unified_radix_cache.py,base_prefix_cache.py,schedule_batch.py,schedule_policy.py).4. Follow-ups on the strict path:
translate_from_req_position_to_state_loc) instead ofswa_loc, matching the per-request compressor read path from [AMD][DSV4] Reland unified-KV pool sizing and SWA ring accounting, fully gated #38192 — the old addressing landed a restored window on the wrong C4 rows (compress_hip.py).ceil(ISL/(page*stride)) + 1windows at per-rank batch 1,ISL = min(200k, context_length)— instead of scaling with cache capacity; bounded at 1.5x durable, overridable bySGLANG_SWA_HICACHE_STATE_STAGING_PAGES(hybrid_pool_assembler.py).Note to reviewers
Scope. Off by default: no pool is allocated, no capture or restore runs.
deepseek_v4_backend_hip_radix.pyandcompress_hip.pyare ROCm-only by construction. In the common files, the L3 key namespace and theunified_swa_hicachepool branch are gated on the flag at startup; the rest is either a new symbol that re-routes nothing (BindCapturedSWAHost,MatchPrefixParams.for_reuse, the new arg/env fields, the new host-pool methods) or a call whose callee returns early unless strict mode and the SWA host pool are both live.The one match-path change that could otherwise have altered another configuration — clamping the FULL device anchor to the host-gated boundary on a cross-request reuse match — is opt-in per component (
TreeComponent.device_anchor_needs_reuse_clamp,Falseeverywhere except strict SWA), so a non-strict tree matches exactly what it matched before.test_reuse_does_not_clamp_when_no_component_asks_for_itpins that.This PR can be split into three cumulative sub-PRs on request — substrate / capture / reuse (C1–5 / C6–8 / C9–10) — each gated by the same flag and a no-op by default.
Accuracy Tests
Two detectors (like #30339): a byte-exact harness is the real detector; GSM8K is only a no-regression guard.
1. Unit Tests (6 test files, 194 UTs total)
How:
cd test && python3 -m pytest -q srt/mem_cache/test_swa_*.pyExpected:
194 passed— byte-exact swa+c4_state/c4_indexer_state capture/restore across L1→L2→L3, strict-gate reject, commit-coupling guard, dirty-read protection, L3-only restore.No regression on the shared unified-cache suite.
python3 -m pytest -q registered/unit/mem_cachegives the same result on this branch as on the merge-base (2115 passed / 1351 skipped / 401 subtests; the 6 failures are pre-existing in our container — 5 need a built rust ext, 1 is a HIP JIT compile failure).2. E2E (DeepSeek-V4 unified_kv on ROCm, HiCache + strict flag)
How: launch with the gates on and
SGLANG_SWA_DBG_CHECKSUM=1(emits a per-window checksum vs a fresh device re-read), trigger prefix reuse, grep the log.Trigger reuse — send the same long prompt N rounds;
cached_tokensclimbs 0 → large:Expected (grep
server.log):[C4-STATE-DBG]state ride bit-exact /[LB-DEV]device landing byte-exact /[SWA-DBG]restore verified[LB-DEV]+[C4-STATE-DBG]> 0 ([SWA-DBG]is 0 on this path — the prefill checksum is not persisted across a process restart, so the window's byte-exact verification is carried by[LB-DEV]device-landing).MISMATCH|reuse_reject|BIND-MISS|AssertionError|Tracebackprefetch .*matched=[0-9]+ loaded=[0-9]+matched=0 loaded>0L3-only restore validated (cold restart, empty L1/L2):
prefetch matched=0 loaded=2304, the first request immediately reportscached_tokens=2304,[LB-DEV]+[C4-STATE-DBG]byte-exact all pass, zeroMISMATCH.gsm8k eval:
Two arms, each run once per server profile — off (SGLANG_UNIFIED_KV_BIT_EXACT_HICACHE=0) and dev (=1) — with the cache flushed (/flush_cache) before each pass and temperature=0, so the only variable is the strict flag. GSM8K is a no-regression guard (the byte-exact harness above is the real correctness detector); the acceptance criterion is that dev is not below off and invalid ≈ 0.
For off (SGLANG_UNIFIED_KV_BIT_EXACT_HICACHE=0) and dev (=1) , all above tests we got acc ~0.94(invalid ≈ 0).
For off, we got 0.933, and for dev, we got 0.942(invalid 0).
Speed Tests and Profiling
Both arms HiCache on,
DBG=0,--hicache-ratio 1.0(single variable = strict flag); customer clients. Just take some of the configs as example.The prefill cost is the deterministic price of strict correctness (positional H2D of the SWA window + c4/indexer state on reuse, not a content-addressed page copy), applied only with the flag on.
Optional in-repo reproduction.
The table above comes from the customer perf clients. For a self-contained check that needs no external scripts, the stock bench_serving client reproduces the strict overhead directly in-repo (it is a different, smaller workload, so absolute numbers differ). Launch the same server twice with DBG=0 — once SGLANG_UNIFIED_KV_BIT_EXACT_HICACHE=1 (dev), once =0 (off) — then run against each:
Compare peak output token throughput: dev trails off by a small, consistent margin (measured off ≈ 1536 tok/s vs dev ≈ 1472 tok/s, ≈ −4%), the same direction as the customer-client table. Note that at this concurrency/input length the Median/P99 TTFT is dominated by request queueing (~50–100 s) and is not a reliable reuse detector — prefix-reuse correctness is verified separately by the byte-exact harness and the climbing cached_tokens above, not by this client's TTFT.
Checklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ciCI States
Latest PR Test (Base): ❌ Run #34570730373
Latest PR Test (Extra): ❌ Run #34570730288
Latest PR Test (AMD ROCm 10): ❌ Run #34570730197