Fix dual-chunk sparse fallback index overflow (#27361)

This commit is contained in:
Han Yu
2026-06-07 23:15:37 -07:00
committed by GitHub
parent 1aa5040c74
commit 3d2165a286
5 changed files with 597 additions and 205 deletions
@@ -6,7 +6,7 @@ unit-test suite, **organized by the action needed to address it**.
Anything failing that is not listed here should be treated as a regression.
Last updated: 2026-05-29
Last updated: 2026-06-05
## Reference runs
@@ -182,8 +182,6 @@ moment production is fixed; no test method invokes them today.
| Citation | Symptom | Trigger | Status |
|---|---|---|---|
| `dual_chunk_flashattention_backend.py:1110-1132` | `RuntimeError: The size of tensor a (4) must match the size of tensor b (5)` at `vertical_buffer.copy_()` | `vertical_size ≤ 5`: fallback `torch.arange(0, intra_K_size, max(1, intra_K_size/5))` returns up to 5 elements into `vertical_size=4` buffer when `intra_vertical_indices.nelement() == 0` | `[no test]` (`dual_chunk/README.md`); smoke helper `run_dual_chunk_sparse_sub_window_case` wired but not invoked |
| `_vertical_slash_sparse_attention` (`convert_vertical_slash_indexes` block math) | `cudaErrorIllegalAddress` deep inside the kernel | `vertical_size=8` with `seq_len ≥ 128`: unstated invariant that `vertical_size + slash_size >= chunk_len_blocks` | `[no test]` (same smoke helper) |
| Triton dense `DRAFT_EXTEND` (non-V2) | Eager fixture/reference mismatch on narrow accepted-token layouts | Test omitted | `[no test]` (`dense/README.md`) |
## C.6. DSA-specific structural gaps
@@ -19,6 +19,7 @@ Columns are runner modes; rows are kernel-path modes of the single
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Non-sparse | ✓ first-window, successor-chunk, inter-chunk extend/decode layouts + GQA decode | deferred: graph metadata for dual-chunk not scoped | deferred | deferred | blocked: `init_forward_metadata` asserts `is_prefill() or is_decode()` (`dual_chunk_flashattention_backend.py:179`); `TARGET_VERIFY` falls under `is_prefill()` but the wrapper hasn't been wired through | deferred | deferred | deferred | blocked: `DRAFT_EXTEND_V2` excluded from `is_prefill()` alias (see Production-Unsupported below) | deferred | deferred | — |
| Sparse all-column (`vertical_size`/`slash_size` chosen so every key in the first chunk is selected) | ✓ single-request first-chunk, multi-request first-chunk, page-boundary first-chunk | — | — | — | blocked: same `is_prefill` assertion | — | — | — | blocked: same | — | — | — |
| Sparse sub-window (`vertical_size=4`, `slash_size=4`, `seq_len=128`) | ✓ independent DCA top-k/split/fallback reference + torch sparse-kernel reference | — | — | — | — | — | — | — | — | — | — | — |
| Threshold-gated sparse (`sparse_attention_threshold=100`, seq_len=16 → gate disables sparse, falls back to dense) | ✓ verifies `current_orig_seq_len > threshold` gate semantics | — | — | — | — | — | — | — | — | — | — | — |
## Input And Config Coverage
@@ -33,6 +34,11 @@ Columns are runner modes; rows are kernel-path modes of the single
(≤16 tokens) so the dense reference remains valid.
- Multi-request sparse and page-boundary sparse variants exercise per-request
`cu_seqlens_*` slicing inside `_dual_chunk_flash_attn_prefill_func`.
- Sub-window sparse prefill uses `vertical_size=4`, `slash_size=4`, and
`seq_len=128` to verify the DCA-specific content-aware top-k split and
empty-stage fallback against an independent reference, then verifies the
sparse output against a torch sparse-kernel reference that consumes the
production block/column metadata.
- Threshold-gated sparse uses `sparse_attention_threshold=100` so a 16-token
prompt bypasses the sparse kernel and falls through to the dense chunk
flash path, exercising the gate semantics in the wrapper.
@@ -77,65 +83,8 @@ See `KNOWN_FAILURES.md` §1 for the full root cause + fix.
- Populate CUDA graph and PCG/BCG runner metadata after eager non-sparse
coverage is stable across more chunk layouts.
- **Sub-context-window sparse pruning reference (genuine follow-up)** —
The current "all-column" sparse cases match the dense reference exactly
because the chosen `vertical_size=16` + `slash_size=16` + `last_q=16`
configuration covers every column in the first chunk for `seq_len <= 16`.
A truly pruning case needs `seq_len >> vertical_size + slash_size` and a
reference that applies the same mask the kernel applies.
The blocker is that the production sparse-attention config
`("vertical_and_slash", v_size, s_size, threshold)` is **content-aware**:
per-head `v_idx` and `s_idx` are picked by top-k attention scores over
the last `last_q` queries, not from a fixed schedule
(`dual_chunk_flashattention_backend.py:_dual_chunk_flash_attn_prefill`).
An independent reference therefore has three paths:
1. **Mock the sparse-config lookup** — patch
`get_sparse_attention_config` or the per-layer top-k selection so the
fixture supplies known `v_idx` / `s_idx` tensors. Then write a
token-level reference that masks `attn_scores[q, k] = -inf` unless
`k in v_idx` or `(q - k) in s_idx` (with causal `k <= q`). This is the
cleanest path but needs a hook in `_dual_chunk_flash_attn_prefill_func`
that doesn't exist today.
2. **Replicate `convert_vertical_slash_indexes`** at block granularity in
pure-PyTorch, then iterate `(block_count, block_offset, column_count,
column_index)` to build a per-(query_block, key_block) mask matching
the kernel's selection. Faithful but tedious — the block math (M=64,
N=64) needs to be mirrored exactly.
3. **Statistical recovery check** — compute dense attention scores
`softmax(Q @ K^T)` per head, identify the top-k columns by score, and
verify the sparse kernel output approximates the dense output modulo
the dropped probability mass. Not strict `assert_close`; rejects only
gross divergences.
Option 1 is recommended. It requires either: (a) a new
`sparse_attention_config_override` kwarg threaded through
`DualChunkFlashAttentionBackend.__init__` that bypasses the content-aware
selection, or (b) monkeypatching `get_sparse_attention_config` on the
fixture's backend instance. Until that lands, the all-column sparse +
threshold-gated cases keep the kernel/wrapper integration covered but
the per-column sparse math is unverified.
**Production-side bugs surfaced while attempting Option 3
(smoke-test "sparse output != dense output"):** two issues block even a
smoke-only sub-window test today.
- `dual_chunk_flashattention_backend.py:1110-1122`: when a chunk's
`intra_vertical_indices.nelement() == 0`, the fallback appends
`torch.arange(0, intra_K_size, max(1, intra_K_size/5))`. With
`intra_K_size=48` this is `arange(0, 48, 9.6)` → 5 elements, but the
`vertical_buffer` is sized to `vertical_size` (=4 in a sub-window
config). The copy at line 1132 then raises
`RuntimeError: The size of tensor a (4) must match the size of
tensor b (5)`. The fallback should clip to `vertical_size` slots.
- With `vertical_size=8` to clear the overflow, the sparse kernel
crashes with `cudaErrorIllegalAddress` deep inside
`_vertical_slash_sparse_attention`, suggesting the
`convert_vertical_slash_indexes` block math has an unstated
invariant that `vertical_size + slash_size >= chunk_len_blocks` or
similar. Needs a kernel-side audit.
The smoke-test helper `run_dual_chunk_sparse_sub_window_case` is wired
through `common/attention_methods/dual_chunk_attention.py` for when
those production bugs are fixed; no test method invokes it today.
- **Broaden sub-window sparse coverage** — the current regression case covers
`prefix_lens=(0,)`, `extend_lens=(128,)`, and no GQA. Add
multi-request batches, nonzero prefixes, GQA, and more sparse config variants
once those paths need explicit sparse pruning coverage. The 64x64
vertical/slash converter remains covered at the sgl-kernel layer.
@@ -13,9 +13,11 @@ from sglang.test.kits.attention_unittest.attention_methods.dual_chunk_attention
DualChunkAttentionCase,
make_dual_chunk_cases,
make_dual_chunk_sparse_cases,
make_dual_chunk_sparse_sub_window_cases,
make_dual_chunk_sparse_threshold_gated_cases,
run_dual_chunk_attention_case,
run_dual_chunk_sparse_attention_case,
run_dual_chunk_sparse_sub_window_case,
run_dual_chunk_sparse_threshold_gated_case,
)
from sglang.test.kits.attention_unittest.runner_modes.cuda_graph_decode_runner import (
@@ -75,6 +77,9 @@ class TestDualChunkFlashAttentionBackendCorrectness(CustomTestCase):
SPARSE_THRESHOLD_GATED_CASES = make_dual_chunk_sparse_threshold_gated_cases(
"dual_chunk_flash_attn"
)
SPARSE_SUB_WINDOW_CASES = make_dual_chunk_sparse_sub_window_cases(
"dual_chunk_flash_attn"
)
# Replay prefix_lens must each be >= capture_prefix_len (= fill-value - 1).
# Dual-chunk's `get_cuda_graph_seq_len_fill_value()` returns 1, so capture
# uses prefix=0. We pick a 3-request batch with varied lengths to exercise
@@ -106,34 +111,10 @@ class TestDualChunkFlashAttentionBackendCorrectness(CustomTestCase):
with self.subTest(case=case.name, backend=case.backend):
run_dual_chunk_sparse_threshold_gated_case(self, case)
# Sub-context-window sparse pruning: BLOCKED on production-side
# edge cases.
#
# The `run_dual_chunk_sparse_sub_window_case` helper in
# `common/attention_methods/dual_chunk_attention.py` is left in
# place for when those production gaps are fixed, but no test
# method invokes it today. See `dual_chunk/README.md` →
# "Sub-context-window sparse pruning" for the engineering paths
# and the two production bugs surfaced while attempting to land
# this coverage:
#
# - `dual_chunk_flashattention_backend.py:1110-1122`: when a chunk's
# `intra_vertical_indices.nelement() == 0`, the fallback appends
# `torch.arange(0, intra_K_size, max(1, intra_K_size/5))` which
# can produce more elements than the `vertical_size`-slot buffer
# allows, raising `RuntimeError: The size of tensor a (4) must
# match the size of tensor b (5)`. Triggered by
# `vertical_size in [4, 5]` with `seq_len=128`.
# - With `vertical_size=8` to avoid the overflow above, the sparse
# kernel raises a `cudaErrorIllegalAddress` deep inside
# `_vertical_slash_sparse_attention`, suggesting the
# `convert_vertical_slash_indexes` block math expects different
# invariants than what a `vertical_size + slash_size < chunk_len`
# config supplies.
#
# The all-column + threshold-gated cases above keep the integration
# path covered; sub-window correctness needs production hardening
# before unit-test coverage is safe.
def test_sparse_dual_chunk_sub_window_cases(self):
for case in self.SPARSE_SUB_WINDOW_CASES:
with self.subTest(case=case.name, backend=case.backend):
run_dual_chunk_sparse_sub_window_case(self, case)
def test_runner_mode_cuda_graph_decode_cases(self):
for case in self.CUDA_GRAPH_DECODE_CASES: