Skip to content

[DSA] Fold page-table into fused top-k v2 (decode): drop page_size=1 expansion - #30274

Merged
Fridge003 merged 9 commits into
sgl-project:mainfrom
DarkSharpness:dsa-topk-v2
Jul 7, 2026
Merged

Fridge003 merged 9 commits into
sgl-project:mainfrom
DarkSharpness:dsa-topk-v2

Conversation

@DarkSharpness

@DarkSharpness DarkSharpness commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator

(generated by claude)

Summary

Integrates the DeepSeek-V4 top-k v2 JIT kernel (topk_transform_512_v2, runtime k≤2048, added in #26788) into the shared DeepSeek-V3.2 / GLM DSA sparse-attention path, and removes the page_size=1 page-table overhead it previously required. Python/Triton only — the JIT kernel file is unchanged. Three incremental commits:

1. Fold the page-table into fused top-k v2 (decode).
The legacy top-k transform expanded to a page_size=1 page table. The v2 kernel supports arbitrary page size, so _try_topk_transform_v2_paged (in dsa_topk_backend.py) feeds it the compact page_size=64 real_page_table (the indexer's own block table) directly. Engaged behind the existing SGLANG_OPT_USE_TOPK_V2 flag for the decode-shaped PAGED, fp32, 1-row/batch case; everything else (prefill RAGGED, multi-query, non-fp32) falls through to the legacy path unchanged. A one-time INFO log reports engagement for routing observability.

2. Preprocess the v2 plan once per forward.
The cluster-threshold plan (plan_topk_v2) is now computed once per forward batch into DSAMetadata.topk_v2_plan (eager + capture), refreshed in-place with .copy_() on CUDA-graph replay, and reused across layers instead of recomputed per top-k call.

3. Drop the wide page_size=1 page table in fused decode CUDA graphs.
The decode CUDA graph statically allocated a [max_num_tokens, max_ctx_len] page_size=1 table. With fused top-k (default) it is dead weight — attention reads topk_indices, the indexer reads the compact real_page_table. The new dsa_drop_wide_page_table gate (cuda ∧ ¬hip ∧ page_size>1 ∧ no-hisparse ∧ no-spec ∧ fuse_topk) skips the wide allocation and materializes only the compact real_page_table, filled straight from req_to_token by the fused metadata triton kernels (their page_table_1 write is made optional). At 160K context this shrinks the static table 64× (2560 vs 163840 int32 cols/row). Non-fused / hisparse / HIP-indexer / spec (MTP precompute, target-verify, draft-extend) paths keep the wide table and are untouched.

Verification

  • B200 unit: the three fused metadata triton kernels produce byte-identical real_page_table with page_table_1=None vs the wide-table path.
  • B200 wiring: the drop-path decode-graph real_page_table is byte-identical (same shape + values) to the legacy derive-from-wide result, verified up to 160K context.
  • v2 correctness: v2 selection ≡ v1 ≡ torch.topk and the fold yields identical physical KV slots to page_size=1, through the real DSATopKBackend.topk_transform, up to 1M context; CUDA-graph capture/replay safe.
  • E2E: DeepSeek-V3.2 GSM8K (TP8) — validated via the registered 8-gpu-h200 test.

Compatibility

GLM DSA (index_topk) shares this exact code path, so it is covered without extra code. All new behavior is gated; default configs on non-DSA / non-CUDA / spec paths are byte-unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MQgtB3k9vngYs9MyAu7BSo


CI States

Latest PR Test (Base): ❌ Run #28817867682
Latest PR Test (Extra): ❌ Run #28817866983

…expansion

The DeepSeek-V3.2 / GLM DSA sparse-attention decode path selects the per-row
top-k of the indexer logits and maps the selected positions through a page
table. The legacy fused path (fast_topk_transform_fused / flashinfer
top_k_page_table_transform) consumes a page_size=1 page table
(attn_metadata.page_table_1) that is `max_seqlen_k` wide per row -- a large
buffer that must be captured in the prefill & decode CUDA graphs.

The DeepSeek-V4 top-k v2 JIT kernel (topk_transform_512_v2) fuses selection +
transform and consumes an arbitrary power-of-two page size, so it can read the
indexer's own compact `real_page_table` (page_size = pool page size, typically
64) directly and fold the transform:
    slot = real_page_table[row, p // page_size] * page_size + (p % page_size)
This is arithmetically identical to gathering the page_size=1 table, so the
selected physical KV slots are unchanged, while the page-table buffer the
transform reads is `page_size`x smaller (64x for the common case).

Add `_try_topk_transform_v2_paged`, engaged behind the existing
`SGLANG_OPT_USE_TOPK_V2` flag (default on) only for the decode-shaped PAGED
case the kernel is defined for (no ragged row_starts, no batch_idx_list remap,
one fp32 score row per batch aligned 1:1 with the page table, 0<topk<=2048).
Every other case (prefill RAGGED / multi-query PAGED / non-fp32) returns None
and falls through to the existing implementation unchanged. Shared by
DeepSeek-V3.2 and GLM DSA (both route through DSATopKBackend.topk_transform).

Verified on B200: the folded path (real_page_table, ps=64) yields the IDENTICAL
physical KV slots as the same kernel run with page_table_1 (ps=1), through the
real DSATopKBackend.topk_transform entrypoint, across decode shapes up to 1M
context; v2 selection matches v1/torch; helper is CUDA-graph capture/replay
safe. Full-model DeepSeek-V3.2 GSM8K e2e pending (671B model, run via CI
test/registered/8-gpu-models/test_deepseek_v32_indexcache.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQgtB3k9vngYs9MyAu7BSo
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@DarkSharpness

DarkSharpness commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

(generated by claude)

/rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py

@github-actions

github-actions Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py:

🚀 8-gpu-h200 (1 test): ✅ View workflow run

cd test/ && python3 registered/8-gpu-models/test_deepseek_v32_indexcache.py

@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

(generated by claude)

/rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py

@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

(generated by claude)

/rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py

@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

/rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py

@github-actions

github-actions Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py:

🚀 8-gpu-h200 (1 test): ✅ View workflow run

cd test/ && python3 registered/8-gpu-models/test_deepseek_v32_indexcache.py

root and others added 2 commits July 6, 2026 09:40
Previously `_try_topk_transform_v2_paged` called `plan_topk_v2(lengths)` on
every layer's top-k transform. The plan (cluster-threshold metadata) depends
only on the decode seq lengths, which are identical across all layers of a
forward, so compute it once.

Mirror the `paged_mqa_schedule_metadata` lifecycle: a new `DSAMetadata.
topk_v2_plan` field computed once at eager metadata build and at CUDA-graph
capture, and refreshed in-place (`copy_`, preserving the captured data_ptr) on
graph replay via `_refresh_topk_v2_plan`. The refresh self-gates to the decode
shape (one score row per batch) the fold engages on, and to
`SGLANG_OPT_USE_TOPK_V2`. The helper now consumes `attn_metadata.topk_v2_plan`
and only falls back to an inline `plan_topk_v2` when it is absent / shaped for a
different batch.

Verified on B200: through the real DSATopKBackend.topk_transform entrypoint,
both the precomputed-plan reuse and the inline fallback yield the identical
(exact) folded physical KV slots as the page_size=1 path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQgtB3k9vngYs9MyAu7BSo
The decode-family CUDA graph statically allocated a
[max_num_tokens, max_ctx_len] page_size=1 table (req_to_token width). With
fused top-k (default) it is dead weight: attention reads topk_indices and the
indexer reads the compact page_size=64 real_page_table. Only non-fused,
hisparse, HIP-indexer, and spec (MTP precompute / target-verify / draft-extend)
paths consume it.

Gate `dsa_drop_wide_page_table` (cuda ∧ ¬hip ∧ page_size>1 ∧ no-hisparse ∧
no-spec ∧ fuse_topk) skips allocating the wide table and instead materializes
only the compact real_page_table, filled straight from req_to_token by the
fused metadata triton kernels (page_table_1 write made optional). At 160K ctx
this shrinks the static table 64x (2560 vs 163840 int32 cols/row).

Verified on B200: the three fused metadata kernels produce byte-identical
real_page_table with page_table_1=None, and the decode-graph wiring yields a
real_page_table byte-identical (same shape+values) to the legacy
derive-from-wide-table result up to 160K context.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQgtB3k9vngYs9MyAu7BSo
@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

/rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py

@github-actions

github-actions Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py:

🚀 8-gpu-h200 (1 test): ✅ View workflow run

cd test/ && python3 registered/8-gpu-models/test_deepseek_v32_indexcache.py

The [DSA] fold-engaged INFO log was added only to prove routing in CI; that is
confirmed, so drop it (and the now-unused logging import/global). Also drops a
redundant forward-ref quote on a type annotation (ruff).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQgtB3k9vngYs9MyAu7BSo
@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

/rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py

@github-actions

github-actions Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-test test/registered/8-gpu-models/test_deepseek_v32_indexcache.py:

🚀 8-gpu-h200 (1 test): ✅ View workflow run

cd test/ && python3 registered/8-gpu-models/test_deepseek_v32_indexcache.py

root and others added 2 commits July 6, 2026 10:43
…ernels

Add a docstring to fused_dsa_decode_metadata describing the optional
page_table_1 (pass None to write only the compact real_page_table; requires
real_page_size>1), and comments at the three kernel store guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQgtB3k9vngYs9MyAu7BSo
Reworks the fused decode top-k so correctness no longer depends on a soft
try/None fallback that could deref a dropped page_table_1.

- topk_transform now dispatches decode-shaped PAGED to the v2 kernel as a
  deterministic decision on the work shape (SGLANG_OPT_USE_TOPK_V2 + PAGED +
  no row_starts/batch_idx_list + topk<=2048 + 1-row/batch). Once dispatched it
  commits: no falling back to the legacy page_size=1 path (which, under the
  page-table fold, may have no page_size=1 table). The legacy branch asserts
  page_table_1 is present.
- _try_topk_transform_v2_paged -> _topk_transform_v2_paged: its preconditions
  are now invariants that assert (raise) rather than return None. The DeepGEMM
  indexer emits contiguous fp32 16B-aligned scores, so drop the pad-to-4; the
  plan is preprocessed per forward, so drop the inline recompute and assert it
  is present (a missing plan is a caller bug, not something to paper over).
- dsa_drop_wide_page_table now also requires SGLANG_OPT_USE_TOPK_V2 and
  dsa_index_topk<=2048, matching exactly when decode routes to v2 -- so the
  page_size=1 table is dropped only when nothing reads it.

Verified on B200: folded selection matches a torch top-k reference for
topk=512/2048, and a missing/mismatched plan raises.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQgtB3k9vngYs9MyAu7BSo
@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

/rerun-test test/registered/models_e2e/test_dsa_glm52_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_dp_mtp.py test/registered/models_e2e/test_dsa_glm52_hisparse.py

@github-actions

github-actions Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-test test/registered/models_e2e/test_dsa_glm52_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_dp_mtp.py test/registered/models_e2e/test_dsa_glm52_hisparse.py:

🚀 8-gpu-h200 (3 tests): ❌ View workflow run

cd test/ && python3 registered/models_e2e/test_dsa_glm52_tp_mtp.py
cd test/ && python3 registered/models_e2e/test_dsa_glm52_dp_mtp.py
cd test/ && python3 registered/models_e2e/test_dsa_glm52_hisparse.py

@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

/rerun-test test/registered/models_e2e/test_dsa_glm52_nvfp4_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_nvfp4_dp_mtp.py

@github-actions

github-actions Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-test test/registered/models_e2e/test_dsa_glm52_nvfp4_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_nvfp4_dp_mtp.py:

🚀 4-gpu-b200 (2 tests): ❌ View workflow run

cd test/ && python3 registered/models_e2e/test_dsa_glm52_nvfp4_tp_mtp.py
cd test/ && python3 registered/models_e2e/test_dsa_glm52_nvfp4_dp_mtp.py

….2 MTP crash)

GLM 5.2 MTP core-dumped with CUDBG_EXCEPTION_WARP_ILLEGAL_ADDRESS in
topk_small_batch_kernel<true> (our v2 kernel). It reproduces only for the
spec verify / draft-extend path: small effective batch (bs * num_draft_tokens)
plus the wide CUDA-graph score buffer (width = max context) makes the host pick
the small-batch cluster launch, and that path faults under the multi-query MTP
graphs. Non-spec decode (large batch -> main kernel) is unaffected and is
validated at GSM8K 0.955; the v2 fold is only e2e-validated for single-query
decode. The kernel is robust to NaN/Inf/large-score/wide-buffer/large-seqlen in
isolation (verified on B200), so the fault is a CUDA-graph/PDL/cluster-launch
interaction not reproducible out-of-graph.

Temporary fix: gate v2 dispatch off for target-verify / draft-extend
(allow_topk_v2 on DSAIndexerMetadata, set from the forward mode). Those fall
back to the legacy page_size=1 transform, which is safe -- the page-table fold
already excludes spec, so page_table_1 is present. Decode/prefill are unchanged.
TODO(dsa-topk-v2): re-enable for MTP once the small-batch kernel is fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQgtB3k9vngYs9MyAu7BSo
@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

/rerun-test test/registered/models_e2e/test_dsa_glm52_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_dp_mtp.py test/registered/models_e2e/test_dsa_glm52_hisparse.py test/registered/models_e2e/test_dsa_glm52_nvfp4_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_nvfp4_dp_mtp.py

@github-actions

github-actions Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-test test/registered/models_e2e/test_dsa_glm52_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_dp_mtp.py test/registered/models_e2e/test_dsa_glm52_hisparse.py test/registered/models_e2e/test_dsa_glm52_nvfp4_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_nvfp4_dp_mtp.py:

🚀 8-gpu-h200 (3 tests): ✅ View workflow run

cd test/ && python3 registered/models_e2e/test_dsa_glm52_tp_mtp.py
cd test/ && python3 registered/models_e2e/test_dsa_glm52_dp_mtp.py
cd test/ && python3 registered/models_e2e/test_dsa_glm52_hisparse.py

🚀 4-gpu-b200 (2 tests): ✅ View workflow run

cd test/ && python3 registered/models_e2e/test_dsa_glm52_nvfp4_tp_mtp.py
cd test/ && python3 registered/models_e2e/test_dsa_glm52_nvfp4_dp_mtp.py

@Fridge003
Fridge003 merged commit be70bfb into sgl-project:main Jul 7, 2026
93 of 107 checks passed
DarkSharpness pushed a commit that referenced this pull request Jul 7, 2026
The fused v2 PAGED dispatch (from #30274) asserts that DSAMetadata carries
a preprocessed topk_v2_plan instead of silently recomputing it per layer.
The equivalence test builds DSAMetadata by hand without the plan, so
test_topk_fused_backends_equivalence started failing on trunk for every PR
(e.g. #30275's base-b-test-1-gpu-large job). Build the plan the same way
init_forward_metadata / _build_forward_metadata_cuda_graph do; the
sgl-kernel-vs-flashinfer equivalence check now actually covers the v2
fused path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chronostasys pushed a commit to MindLab-Research/sglang that referenced this pull request Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants