[AMD] Fix stale SWA ring buffer on radix prefix reuse for DeepSeek-V4 with unified_kv backend - #30339
Conversation
The unified_kv layout keeps SWA in a per-request ring (addressed by req_pool_idx * window + pos % window) that is not content-stable and is never stored in the radix tree. Reusing a cached prefix therefore reads another request's stale SWA, causing decode divergence. Cap the radix prefix match by the trailing sliding window so those tokens are re-prefilled into this request's ring. No-op for all other layouts (base returns 0); active only when SGLANG_HACK_FLASHMLA_BACKEND=unified_kv_triton on HIP with a sliding window. HiCache/UnifiedRadixCache is handled separately in sgl-project#29417.
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
/tag-and-rerun-ci |
|
@amd-bot ci-status |
CI Status for PR #30339Merge verdict: ⛔ Not ready — do not merge on green. Two independent problems: (1) PR CI is incomplete — the AMD pipeline is still running (3 Caution This PR's changed code is not exercised by any PR-CI test. The new logic in Caution PR CI is incomplete. AMD ( Changed files: Executed CI failure attribution: AMD: 3 executed failures (0 related) + 3 shards still pending · Others: 6 root failures (0 related) + fast-fail cascades collapsed. Every executed failure is on a code path the PR does not touch. AMD Executed Failures
Still pending (not tested): Other Executed Failures
Collapsed fast-fail / aggregator jobs (not independent failures): Details / what to do before merge
Generated by amd-bot using Claude Code CLI |
|
/rerun-failed-ci |
|
hi @HaiShaw , per discussion, I did some manual tests locally on an 8*MI355X server for this PR. Nightly test_deepseek_v4_pro_fp4.py with radix cache ENABLEDConfig = registered nightly test/registered/amd/test_deepseek_v4_pro_fp4.py env (
Conclusion: BEFORE, tail<W rows (esp. tail=32) diverge well above the >=W floor; AFTER, every row returns to the floor (0/64). Nightly test_deepseek_v4_pro_fp4.py with radix cache DISABLEDPls Note: the registered nightly suites launch with
|
|
gated by |
…ject/sglang#30339, drop GPU_MAX_HW_QUEUES, set max-running-requests to CUDA_GRAPH_MAX_BS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ject/sglang#30339, drop GPU_MAX_HW_QUEUES, set max-running-requests to CUDA_GRAPH_MAX_BS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… with unified_kv backend (sgl-project#30339) Co-authored-by: amd-danli103 <dan2.li@amd.com>
Motivation
The DeepSeek-V4 unified_kv backend under ROCm stores SWA KV in a per-request ring buffer, addressed by
req_pool_idx * window + pos % window. Unlike the compressed/global KV, this ring is not content-addressed and is never written into the radix tree.Radix prefix caching reuses matched prefix pages across requests. For the index-addressed compressed KV this is safe (content-stable). But the SWA ring slots covering the reused-prefix region still hold whatever the previous occupant of that
req_poolslot wrote — i.e. stale SWA from an unrelated request.When a request reuses a cached prefix and its decode sliding window (the trailing
window= 128 tokens) reaches back into that reused-prefix region, the SWA path reads stale ring contents, leading to wrong attention output.This affects only the unified_kv layout. The default index-addressed SWA pool (triton/tilelang backend) is content-stable and unaffected.
Modifications
BasePrefixCache.swa_reprefill_tail_tokens() -> int: new base method returning0(no-op for all layouts).SWARadixCache.swa_reprefill_tail_tokens(): override returningsliding_window_sizeonly when the unified_kv_triton backend is active on HIP (is_unified_kv_triton()), else0.schedule_batch.py,schedule_policy.py): cap the radix match length byinput_len - reprefill_tail, so the trailing sliding window is held back from prefix reuse and re-prefilled into this request's own ring. The decode window therefore reads freshly-written data.The scheduler cap is generic — it just calls
tree_cache.swa_reprefill_tail_tokens(), so any cache implementing the method benefits, and it is a strict no-op for every other layout/backend (base returns0).Scope / activation: the fix engages only when all of the following hold:
SGLANG_HACK_FLASHMLA_BACKEND=unified_kv_tritonon HIP and the model has a sliding window.ChunkCache/SWAChunkCache, which inherits the base0— and with no prefix reuse there is no stale-ring read to fix);--enable-hierarchical-cacheoff andSGLANG_ENABLE_UNIFIED_RADIX_TREEoff (these route toUnifiedRadixCache, owned by [AMD] Enable unified-KV HiCache on DeepSeek-V4 #29417) and the experimental C++ radix tree off;In every other configuration
swa_reprefill_tail_tokens()returns0and this is a complete no-op.Accuracy Tests
GSM8K does not surface this bug (that's also why we didn't notice this bug before):
The root cause only manifests when several conditions hold at the same time. It's this conjunction that makes the bug so easy to miss:
req_poolslot. The SWA ring is addressed byreq_pool_idx * W + pos % Wand is never stored in the radix tree, so it is not content-stable.So GSM8K structurally cannot surface this bug: it breaks (2) and rarely meets (3). Empirically it's unchanged before/after (0.945–0.950, within run-to-run noise) — a no-regression guard, not a detector.
Determinism harness (the actual detector)
At
temperature=0, prefix-cache reuse must not increase output divergence beyond the model's inherent non-determinism floor. We make this rigorous and self-calibrating: for each uncached tail length we send K=64 identical prompts and report how many fall outside the majority cluster.Result (dsv4 Pro, unified_kv backend, TP8/DP8, --page-size 256, radix on, MTP):
Conclusion: Within tail<W, which specific tail fires is scheduling-dependent; tail=32 reproduces most reliably. The headline is not any single cell but: every tail<W row can exceed the ≥W floor BEFORE, and all return to the floor AFTER.
How to reproduce?
python3 swa_ring_determinism.py 30001 /path/to/DeepSeek-V4-ProSpeed Tests and Profiling
The cap re-prefills the trailing sliding window (128) of an otherwise-cached prefix, rounded up to page granularity — i.e. bounded by window + page_size, a constant independent of context/prefix length.
Measured with
generated-shared-prefix(shared system prompt = 2048, 64 groups × 16 prompts = 1024 req, output = 256, TP8/DP8, unified_kv, radix on, page-size 256, concurrency 64; steady-state of 3 runs;--cache-reporton):Client:
python3 -m sglang.bench_serving --port 30001 --dataset-name generated-shared-prefix --gsp-num-groups 64 --gsp-prompts-per-group 16 --gsp-system-prompt-len 2048 --gsp-question-len 64 --gsp-output-len 256 --max-concurrency 64 --cache-reporttail ≥ W (
question_len=128) — cap is a no-op:tail < W (
question_len=64) — cap active (this is the fix's worst case):The drop in cached tokens (2.09M → 1.85M) confirms the fix path is actually exercised; even so the throughput cost is ≤2% and TTFT +~30 ms. In real traffic only a fraction of requests have
tail < W, so the aggregate overhead is smaller.Relationship with #29417 (HiCache for unified_kv)
This PR and #29417 are complementary and intentionally non-overlapping in ownership:
SWARadixCache(radix-only)UnifiedRadixCache(HiCache host-offload)SWARadixCacheoverrideUnifiedRadixCacheoverride (its own path)BasePrefixCachereturns0, soUnifiedRadixCacheinherits a no-op here and gets its own override in [AMD] Enable unified-KV HiCache on DeepSeek-V4 #29417 — no functional conflict.tree_cache.swa_reprefill_tail_tokens()), whichever merges first provides it; the other should drop its duplicate scheduler edit and keep only its cache-class override. @1am9trashChecklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ciCI States
Latest PR Test (Base): ❌ Run #28841909733
Latest PR Test (Extra): 🚫 Run #28847534328