[Fix] Handle chunked paged MQA metadata in DSV4.1 eager forwards (#40637)
build-sglang-image / build (push) Successful in 32m15s

This commit is contained in:
Yuwei An
2026-09-23 13:35:55 +08:00
committed by minke.yu
parent 104218d9ed
commit ddf5207630
5 changed files with 281 additions and 25 deletions
@@ -752,6 +752,33 @@ class TestPagedIndexerMetadataChunking(CustomTestCase):
row chunks the indexer loops over; a mismatch would silently score rows
with another chunk's schedule."""
def test_capture_warmup_skips_dynamic_budget_but_eager_forward_uses_it(self):
metadata = SimpleNamespace(
use_prefill_cuda_graph=False,
compressed_seq_lens=SimpleNamespace(
is_cuda=True, device=SimpleNamespace(index=0)
),
max_compressed_seq_len=65536,
)
for capture_mode in (True, False):
with (
self.subTest(capture_mode=capture_mode),
patch(f"{_METADATA}.get_is_capture_mode", return_value=capture_mode),
patch("torch.cuda.is_current_stream_capturing", return_value=False),
patch(f"{_METADATA}.is_in_breakable_cuda_graph", return_value=False),
patch(f"{_METADATA}.is_in_tc_piecewise_cuda_graph", return_value=False),
patch(
f"{_METADATA}.mqa_logits_budget_bytes", return_value=4096
) as budget,
):
result = PagedIndexerMetadata._mqa_logits_budget(metadata, num_rows=256)
if capture_mode:
self.assertIsNone(result)
budget.assert_not_called()
else:
self.assertEqual(result, 4096)
budget.assert_called_once_with(device_index=0, allow_sync=True)
def _build(self, *, num_rows: int, budget, use_topk_v2: bool):
deep_gemm = SimpleNamespace(
get_num_sms=MagicMock(return_value=1),
@@ -806,6 +833,12 @@ class TestPagedIndexerMetadataChunking(CustomTestCase):
self.assertIsInstance(metadata.deep_gemm_metadata, list)
self.assertEqual(len(metadata.deep_gemm_metadata), len(chunks))
metadata_chunks = metadata.row_chunks()
self.assertEqual([rows for rows, _ in metadata_chunks], chunks)
for (_, actual_plan), expected_plan in zip(
metadata_chunks, metadata.deep_gemm_metadata
):
self.assertIs(actual_plan, expected_plan)
schedule_rows = [
call.args[0]
for call in deep_gemm.get_paged_mqa_logits_metadata.call_args_list
@@ -875,6 +908,92 @@ class TestChunkedTopKMatchesUnchunked(CustomTestCase):
self.assertTrue(torch.equal(run(rows_per_chunk), expected))
class TestChunkedCandidatePublisher(CustomTestCase):
def test_each_deep_gemm_call_receives_one_tensor_schedule(self):
from sglang.srt.layers.attention.dsv4 import candidate_indexer_deep_gemm as mod
num_rows, width = 5, 16
chunks = [slice(0, 2), slice(2, 4), slice(4, 5)]
plans = [torch.tensor([i], dtype=torch.uint8) for i in range(len(chunks))]
topk_plans = [torch.tensor([i], dtype=torch.int32) for i in range(len(chunks))]
metadata = SimpleNamespace(
compressed_seq_lens=torch.full((num_rows, 1), width, dtype=torch.int32),
page_table=torch.zeros((num_rows, 1), dtype=torch.int32),
deep_gemm_metadata=plans,
max_compressed_seq_len=width,
compressed_page_size=64,
topk_metadata_chunks=topk_plans,
use_topk_v2=True,
row_chunks=lambda: list(zip(chunks, plans)),
)
inputs = SimpleNamespace(
q_fp4=torch.zeros((num_rows, 1, 2, 64), dtype=torch.int8),
q_sf=torch.zeros((num_rows, 1, 2), dtype=torch.int32),
k_cache=torch.zeros((1, 64, 1, 68), dtype=torch.uint8),
weights=torch.zeros((num_rows, 2), dtype=torch.float32),
metadata=metadata,
request_ids=torch.arange(num_rows),
num_rows=num_rows,
)
page_indices = torch.full((num_rows, 4), -1, dtype=torch.int32)
raw_indices = torch.full_like(page_indices, -1)
indexer = object.__new__(mod.DeepGemmCandidateIndexer)
indexer.topk_blocks = 2
deep_gemm = MagicMock(
side_effect=lambda q, *_args: torch.zeros(
(q[0].shape[0], width), dtype=torch.float32
)
)
topk = MagicMock()
event = MagicMock()
stream = MagicMock()
with (
patch.object(mod, "deep_gemm_fp4_paged_mqa_logits", deep_gemm),
patch.object(mod, "topk_transform_paged_from_metadata", topk),
patch.object(
mod,
"candidate_row_lens",
side_effect=lambda lens, _topk: (
torch.ones_like(lens),
lens.clone(),
),
),
patch.object(
mod,
"amax_topk_blocks",
side_effect=lambda _logits, lens, _nblocks, topk_blocks: torch.zeros(
(lens.shape[0], topk_blocks), dtype=torch.int32
),
),
patch.object(
mod,
"sort_candidate_blocks",
side_effect=lambda blocks, *_args: blocks + 1,
),
patch.object(
mod,
"build_sparse_indexer_schedule",
return_value=torch.tensor([7], dtype=torch.uint8),
),
patch.object(mod.torch.cuda, "Event", return_value=event),
patch.object(mod.torch.cuda, "current_stream", return_value=stream),
):
table = indexer.publish_decode(inputs, page_indices, raw_indices)
self.assertEqual(deep_gemm.call_count, len(chunks))
for call, plan in zip(deep_gemm.call_args_list, plans):
self.assertIs(call.args[5], plan)
self.assertIsInstance(call.args[5], torch.Tensor)
self.assertEqual([call.kwargs["rows"] for call in topk.call_args_list], chunks)
for call, plan in zip(topk.call_args_list, topk_plans):
self.assertIs(call.kwargs["topk_metadata"], plan)
self.assertEqual(table.blocks.shape, (num_rows, indexer.topk_blocks))
self.assertEqual(table.phys_blocks.shape, table.blocks.shape)
self.assertEqual(table.valid_lens.shape, (num_rows,))
event.record.assert_called_once_with(stream)
class TestCandidateIndexerGating(CustomTestCase):
def test_candidate_indexer_gating(self):
from sglang.srt.layers.attention.dsv4 import candidate_indexer