[JIT Kernel] DeepSeek-V4 DSA indexer: faster top-k + page-table transform (runtime k <= 2048) (#26788)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.benchmark import marker
|
||||
from sglang.jit_kernel.dsv4.topk import (
|
||||
plan_topk_v2,
|
||||
topk_transform_512,
|
||||
topk_transform_512_v2,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(
|
||||
est_time=120, stage="base-b-kernel-benchmark", runner_config="1-gpu-large"
|
||||
)
|
||||
|
||||
# Compressed page size used by the DSA indexer (real value is 256 // 4 = 64).
|
||||
PAGE_SIZE = 64
|
||||
|
||||
|
||||
def _make_inputs(batch_size: int, seq_len: int, k: int):
|
||||
torch.random.manual_seed(42)
|
||||
scores = torch.randn(batch_size, seq_len, dtype=torch.float32, device="cuda")
|
||||
seq_lens = torch.full((batch_size,), seq_len, dtype=torch.int32, device="cuda")
|
||||
num_pages = (seq_len + PAGE_SIZE - 1) // PAGE_SIZE
|
||||
page_table = (
|
||||
torch.arange(num_pages, dtype=torch.int32, device="cuda")
|
||||
.unsqueeze(0)
|
||||
.expand(batch_size, -1)
|
||||
.contiguous()
|
||||
)
|
||||
out = torch.empty(batch_size, k, dtype=torch.int32, device="cuda")
|
||||
return scores, seq_lens, page_table, out
|
||||
|
||||
|
||||
def _make_p1_table(batch_size: int, seq_len: int):
|
||||
# flashinfer / torch do a per-token (page_size=1) gather, so they need a
|
||||
# (batch, seq) table (one entry per position) rather than the page-size-64 one.
|
||||
src_page_table = (
|
||||
torch.arange(seq_len, dtype=torch.int32, device="cuda")
|
||||
.unsqueeze(0)
|
||||
.expand(batch_size, -1)
|
||||
.contiguous()
|
||||
)
|
||||
lengths = torch.full((batch_size,), seq_len, dtype=torch.int32, device="cuda")
|
||||
return src_page_table, lengths
|
||||
|
||||
|
||||
def _build_fn(provider: str, batch_size: int, seq_len: int, k: int):
|
||||
scores, seq_lens, page_table, out = _make_inputs(batch_size, seq_len, k)
|
||||
N = PAGE_SIZE
|
||||
|
||||
def fn(scores, seq_lens, page_table):
|
||||
if provider == "jit_v1":
|
||||
topk_transform_512(scores, seq_lens, page_table, out, N)
|
||||
return out
|
||||
elif provider == "jit_v2":
|
||||
topk_transform_512_v2(scores, seq_lens, page_table, out, N, metadata)
|
||||
return out
|
||||
elif provider == "flashinfer":
|
||||
from flashinfer import top_k_page_table_transform
|
||||
|
||||
return top_k_page_table_transform(scores, page_table, seq_lens, k)
|
||||
elif provider == "torch":
|
||||
idx = scores.topk(k, dim=-1).indices # (batch, k) int64
|
||||
return torch.gather(page_table, 1, idx)
|
||||
else:
|
||||
raise ValueError(f"unknown provider {provider}")
|
||||
|
||||
if provider in ("flashinfer", "torch"):
|
||||
page_table, seq_lens = _make_p1_table(batch_size, seq_len)
|
||||
if provider == "jit_v2":
|
||||
metadata = plan_topk_v2(seq_lens)
|
||||
return fn, (scores, seq_lens, page_table)
|
||||
|
||||
|
||||
@marker.parametrize("k", [512, 1024, 2048], [512])
|
||||
@marker.parametrize("seq_len", [2**x for x in range(10, 19)], [4096, 65536])
|
||||
@marker.parametrize("batch_size", [2**x for x in range(13)], [1, 128, 1024])
|
||||
@marker.benchmark("provider", ["jit_v1", "jit_v2", "flashinfer", "torch"])
|
||||
def benchmark(seq_len: int, batch_size: int, k: int, provider: str):
|
||||
if k > seq_len:
|
||||
marker.skip("k cannot be larger than seq_len")
|
||||
if k == 2048 and provider == "jit_v1":
|
||||
marker.skip("jit_v1 does not support k=2048")
|
||||
|
||||
fn, input_args = _build_fn(provider, batch_size, seq_len, k)
|
||||
return marker.do_bench(fn, input_args=input_args, memory_args=input_args[:2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
benchmark.run()
|
||||
@@ -0,0 +1,299 @@
|
||||
"""Correctness tests for the DeepSeek-V4 (DSA indexer) JIT top-k transform v2.
|
||||
|
||||
The v2 kernel selects the per-row top-k of ``scores`` (ragged ``seq_lens``) and
|
||||
writes the page-table transform of the selected raw indices into the output. We
|
||||
validate against ``torch.topk`` with a small tolerance for boundary ties (the
|
||||
fp16 coarse histogram can swap elements of equal score).
|
||||
|
||||
Coverage is organized around the kernel's dispatch so every template and its
|
||||
boundaries are exercised:
|
||||
|
||||
template per-row seq reached when
|
||||
-------- ---------- ------------
|
||||
trivial seq <= k
|
||||
Register2 k < seq <= 8192 max_seq <= 8192 (level 0)
|
||||
Register4 8192 < seq <= 16384 max_seq <= 16384 (level 1)
|
||||
Streaming 16384 < seq <= floor max_seq > 16384, non-cluster (level 2)
|
||||
Cluster seq > floor(=65536) max_seq > floor and batch <= 128
|
||||
|
||||
and two cluster dispatch shapes: the fused small-batch kernel (batch <= 30) and
|
||||
the persistent-pool + main kernel (30 < batch <= 128). Boundary seq lengths
|
||||
(8192/8193, 16384/16385, 65535/65536/65537) and batch sizes (30/31, 128/129) are
|
||||
included explicitly, across k in {512,1024,2048} and identity/perm page tables.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.dsv4.topk import plan_topk_v2, topk_transform_512_v2
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=90, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
PAGE_SIZE = 64 # c4 page size = 256 // 4
|
||||
PAGE_BITS = PAGE_SIZE.bit_length() - 1
|
||||
PAGE_MASK = PAGE_SIZE - 1
|
||||
MAX_PERMIT_ERROR = 5
|
||||
FLOOR = 65536 # kClusterFloor
|
||||
|
||||
# (batch, seq) chosen to land on each template and each dispatch boundary.
|
||||
FIXED_CONFIGS = [
|
||||
# --- trivial (seq <= k) ---
|
||||
(8, 256), # trivial for every k
|
||||
(16, 1024), # trivial for k>=1024
|
||||
# --- Register2 (level 0: max_seq <= 8192) ---
|
||||
(8, 4096),
|
||||
(8, 8192), # reg2 upper boundary
|
||||
(128, 8192),
|
||||
(300, 8192), # batch > 128, still level 0
|
||||
# --- Register4 (level 1: 8192 < max_seq <= 16384) ---
|
||||
(8, 8193), # just over reg2
|
||||
(64, 16384), # reg4 upper boundary
|
||||
(256, 16384), # batch > 128
|
||||
# --- Streaming (level 2: max_seq > 16384, non-cluster) ---
|
||||
(8, 16385), # just over reg4 (small batch, seq < floor => non-cluster)
|
||||
(4, 32768),
|
||||
(16, 65535), # just under floor
|
||||
(4, 65536), # at floor (seq == floor => non-cluster)
|
||||
(100, 65536),
|
||||
# --- Cluster, fused small-batch kernel (batch <= 30, max_seq > floor) ---
|
||||
(1, 65537), # single row just over floor
|
||||
(2, 131072),
|
||||
(8, 98304),
|
||||
(30, 131072), # batch == pool boundary
|
||||
# --- Cluster, persistent pool + main kernel (30 < batch <= 128) ---
|
||||
(31, 131072), # just over small-batch
|
||||
(40, 262144), # N > pool of 30 => round-robin
|
||||
(64, 196608),
|
||||
(128, 131072), # cluster batch upper boundary
|
||||
# --- batch > 128 => non-cluster streaming even at long ctx ---
|
||||
(129, 131072),
|
||||
(200, 262144),
|
||||
]
|
||||
|
||||
|
||||
def _assert_topk_close(scores_cpu, ref_raw, our_raw, bs, seq_lens, k):
|
||||
"""Set-compare our top-k raw indices vs torch's, tolerating equal-score ties."""
|
||||
bad = 0
|
||||
for i in range(bs):
|
||||
L = int(seq_lens[i])
|
||||
ref, our = set(ref_raw[i]), set(our_raw[i])
|
||||
more, less = our - ref, ref - our
|
||||
if more or less:
|
||||
mv = sorted(scores_cpu[i, list(more)].tolist())
|
||||
lv = sorted(scores_cpu[i, list(less)].tolist())
|
||||
if mv != lv: # not merely a tie swap -> genuine error
|
||||
bad += len(more)
|
||||
print(
|
||||
f"b={i} L={L} k={k}: more={list(more)[:4]} less={list(less)[:4]} mv={mv[:3]} lv={lv[:3]}"
|
||||
)
|
||||
assert len(our) == min(
|
||||
k, L
|
||||
), f"b={i} L={L} k={k}: {len(our)} valid != {min(k, L)}"
|
||||
assert bad <= MAX_PERMIT_ERROR, f"{bad=} > {MAX_PERMIT_ERROR}"
|
||||
|
||||
|
||||
def _make_page_table(batch, num_pages, mode, device, per_row=False):
|
||||
if mode == "identity":
|
||||
pt = torch.arange(num_pages, dtype=torch.int32, device=device)
|
||||
full = pt.unsqueeze(0).expand(batch, -1).contiguous()
|
||||
inv = pt.unsqueeze(0).expand(batch, -1).cpu()
|
||||
return full, inv
|
||||
# permutation (optionally a distinct permutation per row)
|
||||
rows = batch if per_row else 1
|
||||
full = torch.stack(
|
||||
[torch.randperm(num_pages, device=device) for _ in range(rows)]
|
||||
).to(torch.int32)
|
||||
inv = torch.empty_like(full)
|
||||
ar = torch.arange(num_pages, dtype=torch.int32, device=device)
|
||||
for r in range(rows):
|
||||
inv[r, full[r].long()] = ar
|
||||
if not per_row:
|
||||
full = full.expand(batch, -1).contiguous()
|
||||
inv = inv.expand(batch, -1)
|
||||
return full, inv.cpu()
|
||||
|
||||
|
||||
def _invert(out_row, inv_row):
|
||||
"""Undo page_to_indices for one row's page indices (drop -1 padding)."""
|
||||
return [
|
||||
(int(inv_row[v >> PAGE_BITS]) << PAGE_BITS) | (v & PAGE_MASK)
|
||||
for v in out_row
|
||||
if v != -1
|
||||
]
|
||||
|
||||
|
||||
def _reference(scores, seq_lens, k):
|
||||
"""torch.topk reference indices per row (trivial rows -> all positions)."""
|
||||
ref = []
|
||||
for i in range(scores.shape[0]):
|
||||
L = int(seq_lens[i])
|
||||
if L <= k:
|
||||
ref.append(list(range(L)))
|
||||
else:
|
||||
ref.append(
|
||||
torch.topk(scores[i, :L], k, sorted=False).indices.cpu().tolist()
|
||||
)
|
||||
return ref
|
||||
|
||||
|
||||
def _run(scores, seq_lens, page_table, inv_cpu, k):
|
||||
batch = scores.shape[0]
|
||||
out = torch.full((batch, k), -1, dtype=torch.int32, device=scores.device)
|
||||
metadata = plan_topk_v2(seq_lens)
|
||||
topk_transform_512_v2(scores, seq_lens, page_table, out, PAGE_SIZE, metadata)
|
||||
torch.cuda.synchronize()
|
||||
out_cpu = out.cpu().tolist()
|
||||
return [_invert(out_cpu[i], inv_cpu[i]) for i in range(batch)]
|
||||
|
||||
|
||||
def _run_raw(scores, seq_lens, page_table, k):
|
||||
"""Run the kernel and return its optional raw (pre-transform) top-k index
|
||||
output per row, dropping -1 padding -- the selected positions themselves,
|
||||
NOT the page-table transform of them."""
|
||||
batch = scores.shape[0]
|
||||
out = torch.full((batch, k), -1, dtype=torch.int32, device=scores.device)
|
||||
raw = torch.full((batch, k), -1, dtype=torch.int32, device=scores.device)
|
||||
metadata = plan_topk_v2(seq_lens)
|
||||
topk_transform_512_v2(scores, seq_lens, page_table, out, PAGE_SIZE, metadata, raw)
|
||||
torch.cuda.synchronize()
|
||||
raw_cpu = raw.cpu().tolist()
|
||||
return [[v for v in raw_cpu[i] if v != -1] for i in range(batch)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("page_mode", ["identity", "perm"])
|
||||
@pytest.mark.parametrize("k", [512, 1024, 2048])
|
||||
@pytest.mark.parametrize("batch,seq", FIXED_CONFIGS)
|
||||
@torch.inference_mode()
|
||||
def test_topk_v2(batch: int, seq: int, k: int, page_mode: str) -> None:
|
||||
torch.manual_seed(batch * 100003 + seq * 7 + k)
|
||||
device = "cuda"
|
||||
# Pad the row stride to a multiple of 4 (16-byte vectorized load) while keeping
|
||||
# the exact seq_len -- this also exercises the scalar-tail path for odd seq.
|
||||
width = (seq + 3) & ~3
|
||||
scores = torch.randn(batch, width, dtype=torch.float32, device=device)[:, :seq]
|
||||
seq_lens = torch.full((batch,), seq, dtype=torch.int32, device=device)
|
||||
num_pages = (seq + PAGE_SIZE - 1) // PAGE_SIZE
|
||||
page_table, inv_cpu = _make_page_table(batch, num_pages, page_mode, device)
|
||||
|
||||
our_raw = _run(scores, seq_lens, page_table, inv_cpu, k)
|
||||
ref_raw = _reference(scores, seq_lens, k)
|
||||
_assert_topk_close(scores.cpu(), ref_raw, our_raw, batch, seq_lens.cpu(), k)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("k", [512, 1024, 2048])
|
||||
@pytest.mark.parametrize(
|
||||
"batch,shape",
|
||||
[
|
||||
(20, "small_batch"), # fused small-batch kernel (<= pool of 30)
|
||||
(64, "persistent"), # persistent pool + main kernel
|
||||
(128, "persistent"), # cluster batch boundary
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("per_row_pt", [False, True])
|
||||
@torch.inference_mode()
|
||||
def test_topk_v2_ragged(batch: int, shape: str, k: int, per_row_pt: bool) -> None:
|
||||
"""Ragged lengths spanning trivial..cluster in one launch, both dispatch shapes.
|
||||
|
||||
``per_row_pt`` gives each row a distinct page-table permutation, exercising
|
||||
the per-batch page_table indexing (batch_id stride) rather than a shared one.
|
||||
"""
|
||||
torch.manual_seed(7777 + batch + k + int(per_row_pt))
|
||||
device = "cuda"
|
||||
seq = 262144
|
||||
scores = torch.randn(batch, seq, dtype=torch.float32, device=device)
|
||||
# span every path; guarantee at least one > floor row so cluster dispatch fires
|
||||
buckets = [max(1, k // 2), k, 4096, 12000, 40000, 65536, 98304, 262144]
|
||||
g = torch.Generator(device="cpu").manual_seed(batch + k)
|
||||
lengths = torch.tensor(
|
||||
[
|
||||
buckets[int(torch.randint(0, len(buckets), (1,), generator=g))]
|
||||
for _ in range(batch)
|
||||
],
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
lengths[0] = max(1, k // 2) # a trivial row
|
||||
lengths[1] = 262144 # a long (cluster) row
|
||||
num_pages = (seq + PAGE_SIZE - 1) // PAGE_SIZE
|
||||
page_table, inv_cpu = _make_page_table(
|
||||
batch, num_pages, "perm", device, per_row=per_row_pt
|
||||
)
|
||||
|
||||
our_raw = _run(scores, lengths, page_table, inv_cpu, k)
|
||||
ref_raw = _reference(scores, lengths, k)
|
||||
_assert_topk_close(scores.cpu(), ref_raw, our_raw, batch, lengths.cpu(), k)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("page_mode", ["identity", "perm"])
|
||||
@pytest.mark.parametrize(
|
||||
"batch,seq",
|
||||
[
|
||||
(8, 256), # trivial
|
||||
(8, 4096), # register
|
||||
(4, 131072), # fused small-batch cluster
|
||||
(64, 131072), # persistent cluster + main<3> epilogue
|
||||
(256, 131072), # non-cluster streaming
|
||||
],
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_topk_v2_raw_indices(batch: int, seq: int, page_mode: str) -> None:
|
||||
"""The optional raw-index output must be the pre-transform position of each
|
||||
transformed output slot (out[j] == page_to_indices(raw[j])), and -1 aligns."""
|
||||
k = 512
|
||||
torch.manual_seed(batch * 131 + seq)
|
||||
device = "cuda"
|
||||
width = (seq + 3) & ~3
|
||||
scores = torch.randn(batch, width, dtype=torch.float32, device=device)[:, :seq]
|
||||
seq_lens = torch.full((batch,), seq, dtype=torch.int32, device=device)
|
||||
num_pages = (seq + PAGE_SIZE - 1) // PAGE_SIZE
|
||||
page_table, inv_cpu = _make_page_table(batch, num_pages, page_mode, device)
|
||||
out = torch.full((batch, k), -1, dtype=torch.int32, device=device)
|
||||
raw = torch.full((batch, k), -1, dtype=torch.int32, device=device)
|
||||
|
||||
metadata = plan_topk_v2(seq_lens)
|
||||
topk_transform_512_v2(scores, seq_lens, page_table, out, PAGE_SIZE, metadata, raw)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
out_cpu, raw_cpu = out.cpu().tolist(), raw.cpu().tolist()
|
||||
for i in range(batch):
|
||||
for j in range(k):
|
||||
o, r = out_cpu[i][j], raw_cpu[i][j]
|
||||
if o == -1:
|
||||
assert r == -1, f"b={i} j={j}: out=-1 but raw={r}"
|
||||
else:
|
||||
inv = (int(inv_cpu[i][o >> PAGE_BITS]) << PAGE_BITS) | (o & PAGE_MASK)
|
||||
assert r == inv, f"b={i} j={j}: raw={r} != inverse(out)={inv}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("k", [512, 1024, 2048])
|
||||
@pytest.mark.parametrize("batch,seq", FIXED_CONFIGS)
|
||||
@torch.inference_mode()
|
||||
def test_topk_v2_output_indices(batch: int, seq: int, k: int) -> None:
|
||||
"""Validate the raw (pre-transform) index output DIRECTLY against torch.topk.
|
||||
|
||||
Unlike ``test_topk_v2`` -- which checks the page-transformed output and inverts
|
||||
it through the page table -- this exercises the selected indices themselves, so
|
||||
it isolates the top-k selection from the page-table transform. A permuted page
|
||||
table is used so raw != out, catching any bug that leaks transformed page
|
||||
indices into the raw buffer. Covers every dispatch template/boundary.
|
||||
"""
|
||||
torch.manual_seed(batch * 100003 + seq * 7 + k + 1)
|
||||
device = "cuda"
|
||||
width = (seq + 3) & ~3
|
||||
scores = torch.randn(batch, width, dtype=torch.float32, device=device)[:, :seq]
|
||||
seq_lens = torch.full((batch,), seq, dtype=torch.int32, device=device)
|
||||
num_pages = (seq + PAGE_SIZE - 1) // PAGE_SIZE
|
||||
page_table, _ = _make_page_table(batch, num_pages, "perm", device)
|
||||
|
||||
our_raw = _run_raw(scores, seq_lens, page_table, k)
|
||||
ref_raw = _reference(scores, seq_lens, k)
|
||||
_assert_topk_close(scores.cpu(), ref_raw, our_raw, batch, seq_lens.cpu(), k)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
Reference in New Issue
Block a user