[DSV4] Chunk the indexer MQA logits by query rows under a free-memory budget (#39095)
Signed-off-by: Shiki Wu <shikiw@nvidia.com> Co-authored-by: Baizhou Zhang <sobereddiezhang@gmail.com> Co-authored-by: Yuwei An <ayw.sirius19@gmail.com>
This commit is contained in:
co-authored by
Baizhou Zhang
Yuwei An
parent
993d1fccba
commit
7b67a96640
@@ -1,4 +1,5 @@
|
||||
"""Contract tests for the DSA indexer's MQA-logits chunk budget.
|
||||
"""Contract tests for the MQA-logits chunk decision shared by the DSA and DSV4
|
||||
indexers.
|
||||
|
||||
On ROCm the `[num_q x num_k]` fp32 logits tensor goes to aiter's
|
||||
`fp8_mqa_logits`, which only compiles below 2 GiB, so the budget that decides
|
||||
@@ -8,34 +9,30 @@ The measured memory budget is stubbed: it is the only input the limit has to
|
||||
beat, and stubbing it keeps these tests on CPU.
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
torch = pytest.importorskip("torch")
|
||||
|
||||
from sglang.srt.layers.attention.dsa import dsa_indexer # noqa: E402
|
||||
from sglang.srt.layers.attention.mqa_logits_utils import ( # noqa: E402
|
||||
MQA_LOGITS_MAX_BYTES_ROCM,
|
||||
mqa_logits_should_chunk,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cpu_ci # noqa: E402
|
||||
|
||||
register_cpu_ci(est_time=9, suite="base-a-test-cpu")
|
||||
|
||||
CEILING = dsa_indexer.Indexer._MQA_LOGITS_MAX_BYTES_ROCM
|
||||
CEILING = MQA_LOGITS_MAX_BYTES_ROCM
|
||||
# More than any single logits tensor here needs, so it never decides a case.
|
||||
HUGE_MEM_BUDGET = 64 * 2**30
|
||||
|
||||
|
||||
def _decide(num_q, num_k, mem_budget=HUGE_MEM_BUDGET, is_hip=True):
|
||||
# __new__ skips an __init__ that needs a model config and a device.
|
||||
indexer = dsa_indexer.Indexer.__new__(dsa_indexer.Indexer)
|
||||
with (
|
||||
mock.patch.object(dsa_indexer, "_is_hip", is_hip),
|
||||
mock.patch.object(
|
||||
dsa_indexer.Indexer,
|
||||
"_get_mqa_logits_budget_bytes",
|
||||
return_value=mem_budget,
|
||||
),
|
||||
):
|
||||
return indexer._should_chunk_mqa_logits(num_q, num_k, 0)
|
||||
return mqa_logits_should_chunk(
|
||||
num_rows=num_q,
|
||||
num_cols=num_k,
|
||||
get_budget_bytes=lambda: mem_budget,
|
||||
rocm=is_hip,
|
||||
)
|
||||
|
||||
|
||||
def test_the_ceiling_is_the_largest_logits_aiter_still_takes():
|
||||
|
||||
@@ -11,19 +11,35 @@ from sglang.srt.layers.attention.dsa.dsa_topk_backend import DSATopKBackend
|
||||
from sglang.srt.layers.attention.dsv4.indexer import (
|
||||
FP8_DTYPE,
|
||||
C4IndexerBackendMixin,
|
||||
topk_transform_pytorch_vectorized,
|
||||
)
|
||||
from sglang.srt.layers.attention.dsv4.metadata import (
|
||||
NonPagedIndexerPlan,
|
||||
PagedIndexerMetadata,
|
||||
iter_row_chunks,
|
||||
plan_indexer_row_chunks,
|
||||
)
|
||||
from sglang.srt.layers.attention.mqa_logits_utils import (
|
||||
MQA_LOGITS_MAX_BYTES_ROCM,
|
||||
mqa_logits_budget_bytes,
|
||||
mqa_logits_row_bytes,
|
||||
mqa_logits_rows_per_chunk,
|
||||
mqa_logits_should_chunk,
|
||||
)
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
||||
from sglang.srt.runtime_context import get_parallel
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cpu_ci(est_time=11, suite="base-a-test-cpu")
|
||||
register_cpu_ci(est_time=17, suite="base-a-test-cpu")
|
||||
|
||||
_INDEXER = "sglang.srt.layers.attention.dsv4.indexer"
|
||||
_METADATA = "sglang.srt.layers.attention.dsv4.metadata"
|
||||
_MQA_UTILS = "sglang.srt.layers.attention.mqa_logits_utils"
|
||||
|
||||
# issue #35201: 372K raw tokens -> 92992 c4 columns, padded to 93184 by DeepGEMM.
|
||||
_ISSUE_C4_COLS = 92992
|
||||
_ISSUE_ALIGNED_COLS = 93184
|
||||
|
||||
|
||||
class TestDSV4PagedIndexerMetadata(CustomTestCase):
|
||||
@@ -247,8 +263,11 @@ class TestDSV4TopKDispatch(CustomTestCase):
|
||||
torch.empty((1, 1, 1)),
|
||||
)
|
||||
)
|
||||
backend._get_nonpaged_indexer_plan = MagicMock(return_value=object())
|
||||
backend._forward_nonpaged_indexer = MagicMock(return_value=logits)
|
||||
backend._get_nonpaged_indexer_plan = MagicMock(
|
||||
return_value=SimpleNamespace(query_rows=1, rows_per_chunk=None)
|
||||
)
|
||||
backend._gather_nonpaged_index_k = MagicMock(return_value=(object(), object()))
|
||||
backend._nonpaged_mqa_logits = MagicMock(return_value=logits)
|
||||
|
||||
indexer_capturer = MagicMock()
|
||||
with (
|
||||
@@ -353,7 +372,9 @@ class TestDSV4NonPagedIndexer(CustomTestCase):
|
||||
extend_start_loc=torch.tensor([0], dtype=torch.int32),
|
||||
extend_num_tokens=query_rows,
|
||||
)
|
||||
metadata = SimpleNamespace(nonpaged_plan=None, compressed_page_size=64)
|
||||
metadata = SimpleNamespace(
|
||||
nonpaged_plan=None, compressed_page_size=64, mqa_logits_budget_bytes=None
|
||||
)
|
||||
page_table = torch.tensor([[3, 1]], dtype=torch.int32).repeat(query_rows, 1)
|
||||
c4_seq_lens = torch.tensor([62, 63, 64, 65], dtype=torch.int32)
|
||||
|
||||
@@ -388,6 +409,50 @@ class TestDSV4NonPagedIndexer(CustomTestCase):
|
||||
with threshold.override(0):
|
||||
self.assertIsNone(build_plan())
|
||||
|
||||
def test_plan_row_chunking_follows_the_forward_budget(self):
|
||||
backend = SimpleNamespace(_can_use_nonpaged_indexer=lambda **_: True)
|
||||
backend.dsa_topk_backend = SimpleNamespace(is_sgl_kernel=lambda: True)
|
||||
c4_indexer = SimpleNamespace(use_fp4_indexer=False, index_topk=512)
|
||||
query_rows = 8192
|
||||
seq_len = 372_000
|
||||
batch = SimpleNamespace(
|
||||
seq_lens=torch.tensor([seq_len], dtype=torch.int32),
|
||||
seq_lens_cpu=[seq_len],
|
||||
extend_seq_lens_cpu=[query_rows],
|
||||
extend_seq_lens=torch.tensor([query_rows], dtype=torch.int32),
|
||||
extend_start_loc=torch.tensor([0], dtype=torch.int32),
|
||||
extend_num_tokens=query_rows,
|
||||
)
|
||||
c4_seq_lens = torch.full((query_rows,), seq_len // 4, dtype=torch.int32)
|
||||
page_table = torch.zeros((query_rows, 1), dtype=torch.int32)
|
||||
|
||||
def build_plan(budget):
|
||||
metadata = SimpleNamespace(
|
||||
nonpaged_plan=None,
|
||||
compressed_page_size=64,
|
||||
mqa_logits_budget_bytes=budget,
|
||||
)
|
||||
return C4IndexerBackendMixin._get_nonpaged_indexer_plan(
|
||||
backend,
|
||||
c4_indexer=c4_indexer,
|
||||
forward_batch=batch,
|
||||
indexer_metadata=metadata,
|
||||
page_table=page_table,
|
||||
c4_seq_lens=c4_seq_lens,
|
||||
query_rows=query_rows,
|
||||
)
|
||||
|
||||
# No budget measured this forward (small batch or graph): one call.
|
||||
self.assertIsNone(build_plan(None).rows_per_chunk)
|
||||
budget = 512 << 20
|
||||
plan = build_plan(budget)
|
||||
# 8192 rows x align256(93056) cols x 4 B is far over 512 MiB.
|
||||
self.assertIsNotNone(plan.rows_per_chunk)
|
||||
self.assertLess(plan.rows_per_chunk, query_rows)
|
||||
self.assertLessEqual(
|
||||
plan.rows_per_chunk * mqa_logits_row_bytes(plan.max_seqlen_k), budget
|
||||
)
|
||||
|
||||
def test_extreme_plan_metadata_is_bounded_and_fail_closed(self):
|
||||
backend = SimpleNamespace(_can_use_nonpaged_indexer=lambda **_: True)
|
||||
backend.dsa_topk_backend = SimpleNamespace(is_sgl_kernel=lambda: True)
|
||||
@@ -401,7 +466,9 @@ class TestDSV4NonPagedIndexer(CustomTestCase):
|
||||
extend_start_loc=torch.tensor([0], dtype=torch.int32),
|
||||
extend_num_tokens=query_rows,
|
||||
)
|
||||
metadata = SimpleNamespace(nonpaged_plan=None, compressed_page_size=64)
|
||||
metadata = SimpleNamespace(
|
||||
nonpaged_plan=None, compressed_page_size=64, mqa_logits_budget_bytes=None
|
||||
)
|
||||
page_table = torch.zeros((query_rows, 1), dtype=torch.int32)
|
||||
c4_seq_lens = torch.tensor(
|
||||
[124_997, 124_998, 124_999, 125_000], dtype=torch.int32
|
||||
@@ -439,7 +506,9 @@ class TestDSV4NonPagedIndexer(CustomTestCase):
|
||||
backend = SimpleNamespace(_can_use_nonpaged_indexer=can_use_nonpaged_indexer)
|
||||
backend.dsa_topk_backend = SimpleNamespace(is_sgl_kernel=lambda: True)
|
||||
c4_indexer = SimpleNamespace(use_fp4_indexer=False, index_topk=512)
|
||||
metadata = SimpleNamespace(nonpaged_plan=None, compressed_page_size=64)
|
||||
metadata = SimpleNamespace(
|
||||
nonpaged_plan=None, compressed_page_size=64, mqa_logits_budget_bytes=None
|
||||
)
|
||||
|
||||
def build_plan(query_rows):
|
||||
batch = SimpleNamespace(
|
||||
@@ -503,13 +572,18 @@ class TestDSV4NonPagedIndexer(CustomTestCase):
|
||||
deep_gemm = SimpleNamespace(fp8_mqa_logits=MagicMock(return_value=expected))
|
||||
|
||||
with patch.dict(sys.modules, {"deep_gemm": deep_gemm}):
|
||||
actual = C4IndexerBackendMixin._forward_nonpaged_indexer(
|
||||
q_indexer=q_indexer,
|
||||
weights=weights,
|
||||
kv = C4IndexerBackendMixin._gather_nonpaged_index_k(
|
||||
c4_indexer=c4_indexer,
|
||||
token_to_kv_pool=token_to_kv_pool,
|
||||
plan=plan,
|
||||
)
|
||||
actual = C4IndexerBackendMixin._nonpaged_mqa_logits(
|
||||
q_indexer=q_indexer,
|
||||
weights=weights,
|
||||
kv=kv,
|
||||
plan=plan,
|
||||
rows=slice(0, plan.query_rows),
|
||||
)
|
||||
|
||||
self.assertIs(actual, expected)
|
||||
token_to_kv_pool.get_index_k_scale_buffer.assert_called_once_with(
|
||||
@@ -531,6 +605,276 @@ class TestDSV4NonPagedIndexer(CustomTestCase):
|
||||
self.assertEqual(call.kwargs, {"clean_logits": False, "max_seqlen_k": 128})
|
||||
|
||||
|
||||
class TestMqaLogitsBudgetArithmetic(CustomTestCase):
|
||||
def test_row_bytes_follow_deepgemm_stride_alignment(self):
|
||||
# DeepGEMM pads the fp32 logits row stride to 1024 B, i.e. 256 columns.
|
||||
self.assertEqual(mqa_logits_row_bytes(1), 256 * 4)
|
||||
self.assertEqual(mqa_logits_row_bytes(256), 256 * 4)
|
||||
self.assertEqual(mqa_logits_row_bytes(257), 512 * 4)
|
||||
self.assertEqual(mqa_logits_row_bytes(_ISSUE_C4_COLS), _ISSUE_ALIGNED_COLS * 4)
|
||||
|
||||
def test_rows_per_chunk_keeps_one_chunk_inside_budget(self):
|
||||
row_bytes = mqa_logits_row_bytes(_ISSUE_C4_COLS)
|
||||
budget = 512 << 20
|
||||
# 4096 rows x 93184 cols x 4 B = 1.42 GiB > 512 MiB: must slice.
|
||||
rows = mqa_logits_rows_per_chunk(
|
||||
num_rows=4096, row_bytes=row_bytes, budget_bytes=budget
|
||||
)
|
||||
self.assertIsNotNone(rows)
|
||||
self.assertLess(rows, 4096)
|
||||
self.assertLessEqual(rows * row_bytes, budget)
|
||||
# The whole matrix fits: single call.
|
||||
self.assertIsNone(
|
||||
mqa_logits_rows_per_chunk(
|
||||
num_rows=64, row_bytes=row_bytes, budget_bytes=budget
|
||||
)
|
||||
)
|
||||
# A tight budget never yields a chunk larger than the budget, even when
|
||||
# that means fewer rows than a full page of queries.
|
||||
tight = 64 << 20
|
||||
rows = mqa_logits_rows_per_chunk(
|
||||
num_rows=4096, row_bytes=row_bytes, budget_bytes=tight
|
||||
)
|
||||
self.assertEqual(rows, tight // row_bytes)
|
||||
self.assertLessEqual(rows * row_bytes, tight)
|
||||
# Few query rows still chunk when they do not fit.
|
||||
self.assertEqual(
|
||||
mqa_logits_rows_per_chunk(
|
||||
num_rows=100, row_bytes=row_bytes, budget_bytes=40 * row_bytes
|
||||
),
|
||||
40,
|
||||
)
|
||||
# A budget below one row degrades to single-row chunks, never None.
|
||||
self.assertEqual(
|
||||
mqa_logits_rows_per_chunk(
|
||||
num_rows=4096, row_bytes=row_bytes, budget_bytes=1
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
def test_should_chunk_caps_the_budget_only_on_rocm(self):
|
||||
huge = 64 << 30
|
||||
# 16384 x 32768 x 4 B is exactly 2 GiB, aiter's compile-time ceiling.
|
||||
self.assertEqual(
|
||||
mqa_logits_should_chunk(
|
||||
num_rows=16384, num_cols=32768, get_budget_bytes=lambda: huge, rocm=True
|
||||
),
|
||||
(True, MQA_LOGITS_MAX_BYTES_ROCM),
|
||||
)
|
||||
self.assertEqual(
|
||||
mqa_logits_should_chunk(
|
||||
num_rows=16384,
|
||||
num_cols=32768,
|
||||
get_budget_bytes=lambda: huge,
|
||||
rocm=False,
|
||||
),
|
||||
(False, huge),
|
||||
)
|
||||
|
||||
def test_should_chunk_skips_small_matrices_without_querying_the_budget(self):
|
||||
get_budget = MagicMock(return_value=1)
|
||||
# 64 decode rows x 100K columns is far below the 8M-element threshold.
|
||||
self.assertEqual(
|
||||
mqa_logits_should_chunk(
|
||||
num_rows=64, num_cols=100_000, get_budget_bytes=get_budget, rocm=False
|
||||
),
|
||||
(False, 0),
|
||||
)
|
||||
get_budget.assert_not_called()
|
||||
|
||||
def test_plan_combines_sm120_cap_with_budget(self):
|
||||
budget = 512 << 20
|
||||
by_budget = mqa_logits_rows_per_chunk(
|
||||
num_rows=8192,
|
||||
row_bytes=mqa_logits_row_bytes(_ISSUE_C4_COLS),
|
||||
budget_bytes=budget,
|
||||
)
|
||||
cases = (
|
||||
# (num_rows, num_cols, budget_bytes, sm120_row_cap) -> rows_per_chunk
|
||||
((8192, 1024, None, None), None),
|
||||
((8192, 1024, None, 4096), 4096),
|
||||
((4096, 1024, None, 4096), None),
|
||||
((8192, _ISSUE_C4_COLS, budget, None), by_budget),
|
||||
((8192, _ISSUE_C4_COLS, budget, 4096), min(4096, by_budget)),
|
||||
)
|
||||
for (num_rows, num_cols, budget_bytes, cap), expected in cases:
|
||||
with self.subTest(num_rows=num_rows, num_cols=num_cols, cap=cap):
|
||||
self.assertEqual(
|
||||
plan_indexer_row_chunks(
|
||||
num_rows=num_rows,
|
||||
num_cols=num_cols,
|
||||
budget_bytes=budget_bytes,
|
||||
sm120_row_cap=cap,
|
||||
),
|
||||
expected,
|
||||
)
|
||||
|
||||
def test_iter_row_chunks_covers_rows_exactly_once(self):
|
||||
self.assertEqual(
|
||||
list(iter_row_chunks(num_rows=10, rows_per_chunk=4)),
|
||||
[slice(0, 4), slice(4, 8), slice(8, 10)],
|
||||
)
|
||||
self.assertEqual(
|
||||
list(iter_row_chunks(num_rows=10, rows_per_chunk=None)), [slice(0, 10)]
|
||||
)
|
||||
self.assertEqual(
|
||||
list(iter_row_chunks(num_rows=10, rows_per_chunk=64)), [slice(0, 10)]
|
||||
)
|
||||
|
||||
def test_static_budget_never_queries_free_memory(self):
|
||||
total = 80 << 30
|
||||
props = SimpleNamespace(total_memory=total)
|
||||
device_module = SimpleNamespace(
|
||||
get_device_properties=MagicMock(return_value=props)
|
||||
)
|
||||
schedule = SimpleNamespace(mem_fraction_static=0.9)
|
||||
with (
|
||||
envs.SGLANG_DSA_MQA_LOGITS_FREE_MEM_FRACTION.override(0.2),
|
||||
patch(f"{_MQA_UTILS}.get_device_module", return_value=device_module),
|
||||
patch(f"{_MQA_UTILS}.get_schedule", return_value=schedule),
|
||||
patch(f"{_MQA_UTILS}.is_hip", return_value=False),
|
||||
patch(f"{_MQA_UTILS}.is_xpu", return_value=False),
|
||||
patch(
|
||||
"torch.cuda.mem_get_info", return_value=(6 << 30, total)
|
||||
) as mem_get_info,
|
||||
):
|
||||
static = mqa_logits_budget_bytes(device_index=0, allow_sync=False)
|
||||
mem_get_info.assert_not_called()
|
||||
live = mqa_logits_budget_bytes(device_index=0, allow_sync=True)
|
||||
mem_get_info.assert_called_once_with(0)
|
||||
# static: 80 GiB x (1 - 0.9) x 0.2; live is further capped by 6 GiB free x 0.2.
|
||||
self.assertEqual(static, int(int(total * 0.1) * 0.2))
|
||||
self.assertEqual(live, int((6 << 30) * 0.2))
|
||||
|
||||
|
||||
class TestPagedIndexerMetadataChunking(CustomTestCase):
|
||||
"""The schedule list and the top-k plan list must be built over the exact
|
||||
row chunks the indexer loops over; a mismatch would silently score rows
|
||||
with another chunk's schedule."""
|
||||
|
||||
def _build(self, *, num_rows: int, budget, use_topk_v2: bool):
|
||||
deep_gemm = SimpleNamespace(
|
||||
get_num_sms=MagicMock(return_value=1),
|
||||
get_paged_mqa_logits_metadata=MagicMock(
|
||||
side_effect=lambda c4, *_: torch.zeros((2, 2), dtype=torch.int32)
|
||||
),
|
||||
)
|
||||
c4_seq_lens = torch.arange(1, num_rows + 1, dtype=torch.int32)
|
||||
page_table = torch.zeros(
|
||||
(num_rows, _ISSUE_ALIGNED_COLS // 64), dtype=torch.int32
|
||||
)
|
||||
with (
|
||||
patch.dict(sys.modules, {"deep_gemm": deep_gemm}),
|
||||
envs.SGLANG_FP8_PAGED_MQA_LOGITS_TORCH.override(False),
|
||||
envs.SGLANG_OPT_USE_JIT_INDEXER_METADATA.override(False),
|
||||
patch(f"{_METADATA}.is_hip", return_value=False),
|
||||
patch(f"{_METADATA}.is_xpu", return_value=False),
|
||||
patch(f"{_METADATA}._IS_SM120", False),
|
||||
patch.object(
|
||||
PagedIndexerMetadata, "_mqa_logits_budget", return_value=budget
|
||||
),
|
||||
patch(
|
||||
"sglang.kernels.ops.attention.dsv4.plan_topk_v2",
|
||||
side_effect=lambda seq_lens: seq_lens.new_zeros(
|
||||
(seq_lens.shape[0] + 1, 2)
|
||||
),
|
||||
) as plan_topk_v2,
|
||||
):
|
||||
metadata = PagedIndexerMetadata(
|
||||
page_size=256,
|
||||
compressed_page_size=64,
|
||||
page_table=page_table,
|
||||
compressed_seq_lens=c4_seq_lens,
|
||||
use_topk_v2=use_topk_v2,
|
||||
)
|
||||
return metadata, deep_gemm, plan_topk_v2
|
||||
|
||||
def test_budget_splits_schedules_and_topk_plans_over_the_same_rows(self):
|
||||
num_rows, budget = 4096, 512 << 20
|
||||
metadata, deep_gemm, plan_topk_v2 = self._build(
|
||||
num_rows=num_rows, budget=budget, use_topk_v2=True
|
||||
)
|
||||
expected_rows = mqa_logits_rows_per_chunk(
|
||||
num_rows=num_rows,
|
||||
row_bytes=mqa_logits_row_bytes(_ISSUE_ALIGNED_COLS),
|
||||
budget_bytes=budget,
|
||||
)
|
||||
self.assertEqual(metadata.rows_per_chunk, expected_rows)
|
||||
self.assertEqual(metadata.mqa_logits_budget_bytes, budget)
|
||||
chunks = list(iter_row_chunks(num_rows=num_rows, rows_per_chunk=expected_rows))
|
||||
self.assertGreater(len(chunks), 1)
|
||||
|
||||
self.assertIsInstance(metadata.deep_gemm_metadata, list)
|
||||
self.assertEqual(len(metadata.deep_gemm_metadata), len(chunks))
|
||||
schedule_rows = [
|
||||
call.args[0]
|
||||
for call in deep_gemm.get_paged_mqa_logits_metadata.call_args_list
|
||||
]
|
||||
torch.testing.assert_close(
|
||||
torch.cat(schedule_rows), metadata.compressed_seq_lens.unsqueeze(-1)
|
||||
)
|
||||
self.assertEqual(
|
||||
[r.shape[0] for r in schedule_rows], [c.stop - c.start for c in chunks]
|
||||
)
|
||||
|
||||
self.assertEqual(len(metadata.topk_metadata_chunks), len(chunks))
|
||||
# First call is the full-batch plan; the rest are one per chunk.
|
||||
plan_rows = [call.args[0] for call in plan_topk_v2.call_args_list]
|
||||
torch.testing.assert_close(plan_rows[0], metadata.compressed_seq_lens)
|
||||
torch.testing.assert_close(
|
||||
torch.cat(plan_rows[1:]), metadata.compressed_seq_lens
|
||||
)
|
||||
self.assertEqual(
|
||||
[r.shape[0] for r in plan_rows[1:]], [c.stop - c.start for c in chunks]
|
||||
)
|
||||
|
||||
def test_no_budget_keeps_the_single_call_shape(self):
|
||||
metadata, deep_gemm, plan_topk_v2 = self._build(
|
||||
num_rows=4096, budget=None, use_topk_v2=True
|
||||
)
|
||||
self.assertIsNone(metadata.rows_per_chunk)
|
||||
self.assertIsNone(metadata.topk_metadata_chunks)
|
||||
self.assertIsInstance(metadata.deep_gemm_metadata, torch.Tensor)
|
||||
deep_gemm.get_paged_mqa_logits_metadata.assert_called_once()
|
||||
plan_topk_v2.assert_called_once()
|
||||
|
||||
|
||||
class TestChunkedTopKMatchesUnchunked(CustomTestCase):
|
||||
"""Each row's top-k depends only on its own logits row, sequence length and
|
||||
page-table row, so scoring the batch in row chunks must select the same
|
||||
pages as one pass. This is the property the chunk loop relies on."""
|
||||
|
||||
def test_row_chunks_select_the_same_pages(self):
|
||||
torch.manual_seed(0)
|
||||
rows, width, topk, page_size = 37, 2048, 64, 64
|
||||
logits = torch.randn(rows, width, dtype=torch.float32)
|
||||
seq_lens = torch.randint(1, width, (rows,), dtype=torch.int32)
|
||||
page_table = torch.randint(
|
||||
0, 4096, (rows, width // page_size), dtype=torch.int32
|
||||
)
|
||||
|
||||
def run(rows_per_chunk):
|
||||
out = torch.full((rows, topk), -1, dtype=torch.int32)
|
||||
for rows_slice in iter_row_chunks(
|
||||
num_rows=rows, rows_per_chunk=rows_per_chunk
|
||||
):
|
||||
topk_transform_pytorch_vectorized(
|
||||
logits[rows_slice],
|
||||
seq_lens[rows_slice],
|
||||
page_table[rows_slice],
|
||||
out[rows_slice],
|
||||
page_size,
|
||||
None,
|
||||
)
|
||||
# Unsorted top-k: compare the selected sets row by row.
|
||||
return out.sort(dim=1).values
|
||||
|
||||
expected = run(None)
|
||||
for rows_per_chunk in (1, 7, 16, rows - 1):
|
||||
with self.subTest(rows_per_chunk=rows_per_chunk):
|
||||
self.assertTrue(torch.equal(run(rows_per_chunk), expected))
|
||||
|
||||
|
||||
class TestCandidateIndexerGating(CustomTestCase):
|
||||
def test_candidate_indexer_gating(self):
|
||||
from sglang.srt.layers.attention.dsv4 import candidate_indexer
|
||||
|
||||
Reference in New Issue
Block a user