Skip to content

[DSA] Fix top-k v2 emitting invalid indices under tie overflow / inf scores (IMA in FA3 sparse decode) - #30645

Merged
Fridge003 merged 1 commit into
sgl-project:mainfrom
DarkSharpness:fix-dsv4-topk-v2-tie-overflow-ima
Jul 9, 2026
Merged

[DSA] Fix top-k v2 emitting invalid indices under tie overflow / inf scores (IMA in FA3 sparse decode)#30645
Fridge003 merged 1 commit into
sgl-project:mainfrom
DarkSharpness:fix-dsv4-topk-v2-tie-overflow-ima

Conversation

@DarkSharpness

@DarkSharpness DarkSharpness commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

(generated by claude)

Motivation

The nightly test_glm52_fp8.py perf test crashed with an illegal memory access on all three variants (run). The CUDA coredump shows the faulting kernel is FA3 sparse decode (FlashAttnFwdSm90, bs=1 decode right after an 8192-token prefill), and the faulting LDGSTS address is exactly kv_pool_base - 1152B (= one 576-elem bf16 DSA KV row) — i.e. FA3 dereferenced KV slot index -1.

The -1 comes from the fused top-k v2 kernel. FA3 reads exactly min(seq_len, topk) entries of the top-k output (dsa_cache_seqlens = seq_lens.clamp(max=topk)), so every one of those slots must hold a valid index. Two data-dependent extremes break that contract:

  1. Tie overflow: the collect pass keeps at most kMaxNumTie = 1024 candidates from the threshold bin. When the bin holds more (ReLU-degenerate indexer scores easily put thousands of positions on one exact value — the random-token perf workload hits this; short gsm8k requests never reach the sparse path, which is why accuracy passed) and topk - above_count > 1024, handle_tie pads the remaining slots with -1. Unit repro on H200: seq=8320, topk=2048, 100 winners + 8220 zeros → 924 invalid entries inside the valid prefix.
  2. inf / >65504 scores: coarse_bin_lower_bound computed NaN boundaries for bins touching the fp16 ±inf keys (their neighbors live in NaN key space), so the collect pass matched nothing; large-but-finite scores (>65504, which fp16-round into the inf bin) were dropped entirely.

Modifications

All in topk_impl.cuh (used by the v2 JIT kernel only; v1 is self-contained):

  • kMaxNumTie 1024 → 2048 (= kMaxTopK), with a static_assert pinning the invariant. 1536-style middle values don't close the hole: remain = topk - above reaches topk itself whenever above == 0 (all-equal / inf-heavy rows). The tie buffer is overlaid with the coarse histogram (dead once find_threshold publishes the bin; TieHandleSmem and tie_values stay side by side since they are live together), so per-block shared memory shrinks from ~24.9KB to ~18.9KB.
  • Exact radix tie-select split by tie count: radix_tie_select<1> for the common num_ties <= kBlockSize case (instruction-identical to the old code), radix_tie_select<2> only for the rare overflow case. Also fixes the tie emit and cluster cross-rank tie merge loops, which silently assumed num_ties <= kBlockSize.
  • inf-aware coarse_bin_lower_bound: ±inf ordered keys act as ±65536 so the midpoint lands exactly on ±65520 (the fp32→fp16 RN overflow threshold); NaN-space keys saturate. Boundaries stay monotone and never NaN. The finite fast path is hoisted above the special cases and range-checks both keys at once — verified bit-exact against the general path for every bin of kBits 10 and 12.
  • Backstop pad: unfillable slots (unreachable for NaN-free scores after the above) are padded with a valid in-range index instead of -1 — a duplicate token costs a little accuracy, never an IMA.

Verification (H200, same hardware as the failing job)

  • Selected-score multisets match torch.topk exactly across random / fat-tie / all-equal / inf-heavy / inf-some / huge-finite(1e30) / -inf-heavy distributions, on all three kernel paths (register, streaming, cluster); no duplicate or out-of-range indices; the original IMA repro (including all-NaN) now yields only in-range indices.
  • compute-sanitizer memcheck and racecheck clean (racecheck also validates the smem overlay phase separation).
  • Marker benchmark (do_bench, CUDA-graph replay, cold L2) A/B vs main: healthy-distribution shapes within ±1% (cluster small-batch +~2%); the degenerate fat-tie path +5–8%, which is the cost of exact-ranking 2048 tie candidates instead of dropping half of them.

Checklist

🤖 Generated with Claude Code


CI States

Latest PR Test (Base): ❌ Run #29012052859
Latest PR Test (Extra): ❌ Run #29012052641

…scores

The fused top-k v2 kernel could leave -1 (or short) entries inside the
first min(seq_len, topk) output slots whenever the threshold bin held
more than kMaxNumTie candidates (e.g. ReLU-degenerate indexer scores) or
touched the fp16 +/-inf key space. Downstream FA3 sparse decode reads
exactly min(seq_len, topk) slots, so a -1 there dereferences
kv_pool_base - row_bytes and crashes with an illegal memory access
(nightly GLM-5.2-FP8 perf test, coredump faulting address = base - 1152).

- Raise kMaxNumTie to 2048 (= kMaxTopK): the tie stage may need to fill
  up to topk slots (above_count can be 0), so any smaller cap leaves a
  hole. Overlay the tie machinery with the dead coarse histogram so the
  bigger buffer shrinks the shared-memory footprint (24.9KB -> 18.9KB).
- Split the exact radix tie-select into a <1>-item common path
  (num_ties <= kBlockSize, identical to the old code) and a <2>-item
  overflow path, and fix the tie emit / cluster tie merge loops that
  silently assumed num_ties <= kBlockSize.
- Make coarse_bin_lower_bound inf-aware: treat the +/-inf ordered keys
  as +/-65536 (midpoint lands on the exact fp32->fp16 RN overflow
  threshold +/-65520) and saturate NaN-space keys, so thresholds at or
  next to the inf bin classify +/-inf and >65504 scores instead of
  collecting nothing. Hoist the finite fast path above the special
  cases (verified bit-exact over every bin for kBits 10 and 12).
- Pad unfillable slots with a valid in-range index instead of -1 as a
  last-ditch backstop (unreachable for NaN-free scores): duplicates cost
  accuracy, never an IMA.

Verified on H200: selected-score sets match torch.topk exactly across
random / fat-tie / all-equal / inf-heavy / >65504 / -inf cases on all
three kernel paths (register, streaming, cluster); compute-sanitizer
memcheck and racecheck clean; marker benchmark shows healthy paths
within noise and the degenerate fat-tie path +5-8% (it now exact-ranks
2048 tie candidates instead of dropping half).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

/tag-run-ci-label

@DarkSharpness

Copy link
Copy Markdown
Collaborator Author

/rerun-test test_glm52_fp8.py

@github-actions github-actions Bot added the run-ci label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Results for /rerun-test test_glm52_fp8.py:

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

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

🚀 8-gpu-b200 (1 test): ❌ View workflow run

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes and refactors the TopK kernel implementation for DeepSeek v4. Key changes include adding a fast path to coarse_bin_lower_bound, increasing kMaxNumTie to 2048 to avoid downstream illegal memory accesses, refactoring handle_tie and radix_tie_select to support multi-item register layouts, and overlaying shared memory structures to control footprint size. The review feedback highlights two important issues: first, a potential uninitialized memory read in radix_tie_select when topk is 0, which can be resolved with an early return in handle_tie; second, a strict aliasing violation in to_finite_val due to pointer-based reinterpretation, which should be replaced with __ushort_as_half.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@@ -208,15 +244,11 @@ struct TopKConfig {
static_assert(kNumWarps == kWarpSize);

if (num_ties <= topk) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If topk is 0 (which happens when above_count >= global_topk), handle_tie is still called. If num_ties > kWarpSize * 4, it will enter radix_tie_select with topk_remain = 0. In radix_tie_select, since topk_remain is 0, the condition above < topk_remain is never met, meaning no thread writes to smem->match. This causes threads to read uninitialized/garbage values from smem->match and perform atomic operations/scatters based on garbage threshold bins. Adding an early return if (topk == 0) return; at the beginning of handle_tie completely avoids this issue and saves unnecessary work.

    if (topk == 0) return;
    if (num_ties <= topk) {

const auto to_val = [](uint32_t okey) -> float {
// ordered16 -> fp16 value (inverse of the transform in extract_coarse_bin);
// finite keys only.
const auto to_finite_val = [](uint32_t okey) -> float {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using reinterpret_cast to cast a uint16_t* to const fp16_t* and dereferencing it violates strict aliasing rules in C++ and can lead to undefined behavior. Since hb is a uint16_t and fp16_t is __half, you can use the standard CUDA/HIP intrinsic __ushort_as_half(hb) to safely perform the bit-reinterpret cast without pointer dereferencing.

@kpham-sgl

Copy link
Copy Markdown
Collaborator

/rerun-test test_glm52_fp8.py

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Results for /rerun-test test_glm52_fp8.py:

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

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

🚀 8-gpu-b200 (1 test): ❌ View workflow run

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

@Fridge003

Copy link
Copy Markdown
Collaborator

/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_nvfp4_dp_mtp.py test/registered/models_e2e/test_dsa_glm52_nvfp4_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_hisparse.py test/registered/cuda_graph/piecewise/test_pcg_glm52_fp4.py test/registered/kernels/test_dsa_indexer.py test/registered/jit/deepseek_v4/test_topk_v2.py

@github-actions

github-actions Bot commented Jul 9, 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_nvfp4_dp_mtp.py test/registered/models_e2e/test_dsa_glm52_nvfp4_tp_mtp.py test/registered/models_e2e/test_dsa_glm52_hisparse.py test/registered/cuda_graph/piecewise/test_pcg_glm52_fp4.py test/registered/kernels/test_dsa_indexer.py test/registered/jit/deepseek_v4/test_topk_v2.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 (3 tests): ✅ View workflow run

cd test/ && python3 registered/models_e2e/test_dsa_glm52_nvfp4_dp_mtp.py
cd test/ && python3 registered/models_e2e/test_dsa_glm52_nvfp4_tp_mtp.py
cd test/ && python3 registered/cuda_graph/piecewise/test_pcg_glm52_fp4.py

🚀 1-gpu-h100 (2 tests): ✅ View workflow run

cd test/ && python3 registered/kernels/test_dsa_indexer.py
cd test/ && python3 registered/jit/deepseek_v4/test_topk_v2.py

@Fridge003
Fridge003 merged commit bda1dc0 into sgl-project:main Jul 9, 2026
95 of 110 checks passed
Fridge003 pushed a commit that referenced this pull request Jul 9, 2026
…indices under tie overflow / inf scores (IMA in FA3 sparse decode) (#30645) (#30698)

Co-authored-by: DarkSharpness <76582120+DarkSharpness@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@hnyls2002

Copy link
Copy Markdown
Collaborator

@DarkSharpness @Fridge003 @b8zhong

This PR broke the lint...

Chronostasys pushed a commit to MindLab-Research/sglang that referenced this pull request Aug 24, 2026
…indices under tie overflow / inf scores (IMA in FA3 sparse decode) (sgl-project#30645) (sgl-project#30698)

Co-authored-by: DarkSharpness <76582120+DarkSharpness@users.noreply.github.com>
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
Chronostasys pushed a commit to MindLab-Research/sglang that referenced this pull request Aug 24, 2026
…scores (IMA in FA3 sparse decode) (sgl-project#30645)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants