[Test] Consolidate kernel tests under plural kernels tree (#39966)
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""Boundary tests for the packed indices in the DSV4 prefill write plan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kernels.deepseek_v4.common import (
|
||||
make_legacy_context,
|
||||
make_paged_context,
|
||||
to_seq_extend,
|
||||
)
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=20, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestCompressWritePlanBounds(CustomTestCase):
|
||||
def test_64k_prefill_preserves_last_token(self):
|
||||
"""65536 tokens fit uint16 indices; the last token must not wrap or vanish."""
|
||||
for cr in (4, 128):
|
||||
paged = make_paged_context(
|
||||
bs=16, compress_ratio=cr, num_swa_pages_per_req=16
|
||||
)
|
||||
legacy = make_legacy_context(bs=16, compress_ratio=cr)
|
||||
seq_lens, extend_lens, num_q = to_seq_extend([(4096, 4096)] * 16)
|
||||
for ctx, on_gpu in ((paged, False), (paged, True), (legacy, False)):
|
||||
with self.subTest(cr=cr, paged=ctx is paged, on_gpu=on_gpu):
|
||||
device = "cuda" if on_gpu else "cpu"
|
||||
plan = ctx.make_prefill_plan(
|
||||
seq_lens.to(device), extend_lens.to(device), num_q
|
||||
)
|
||||
c = plan.plan_c.cpu().view(torch.int32).view(-1, 4)
|
||||
valid_c = c[:, 0] != -1
|
||||
ids = c[valid_c, 1].bitwise_and(0xFFFF).sort().values
|
||||
torch.testing.assert_close(
|
||||
ids, torch.arange(cr - 1, num_q, cr, dtype=torch.int32)
|
||||
)
|
||||
w = plan.plan_w.cpu().view(torch.int32).view(-1, 2)
|
||||
last = w[w[:, 0] == 65535]
|
||||
if cr == 4:
|
||||
self.assertEqual(len(last), 1)
|
||||
self.assertEqual(int(last[0, 1]), ctx.state_loc(15, 4095))
|
||||
else:
|
||||
# Non-overlapping C128 consumed the complete final block;
|
||||
# no raw tail remains to persist into the state ring.
|
||||
self.assertEqual(len(w[w[:, 0] != -1]), 0)
|
||||
|
||||
def test_prefill_rejects_uint16_index_overflow(self):
|
||||
for ctx in (
|
||||
make_paged_context(bs=16, compress_ratio=4, num_swa_pages_per_req=17),
|
||||
make_legacy_context(bs=16, compress_ratio=4),
|
||||
):
|
||||
seq_lens, extend_lens, num_q = to_seq_extend(
|
||||
[(4096, 4096)] * 15 + [(4097, 4097)]
|
||||
)
|
||||
with self.assertRaisesRegex(RuntimeError, "plan_compress_prefill"):
|
||||
ctx.make_prefill_plan(seq_lens, extend_lens, num_q)
|
||||
|
||||
def test_prefill_rejects_packed_invalid_sentinel(self):
|
||||
# A 65536-request, one-token-per-request batch makes the last packed
|
||||
# (batch_id, ragged_id) equal (65535, 65535), the invalid write sentinel.
|
||||
ctx = make_legacy_context(bs=65536, compress_ratio=4)
|
||||
seq_lens, extend_lens, num_q = to_seq_extend([(1, 1)] * 65536)
|
||||
with self.assertRaisesRegex(RuntimeError, "plan_compress_prefill"):
|
||||
ctx.make_prefill_plan(seq_lens, extend_lens, num_q)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+3
-3
@@ -11,10 +11,10 @@
|
||||
#
|
||||
# Test command:
|
||||
# python3 -m pytest -q \
|
||||
# test/registered/jit/test_deepseek_v4_compress_state_runtime_shapes.py
|
||||
# test/registered/kernels/ops/attention/test_deepseek_v4_compress_state_runtime_shapes.py
|
||||
#
|
||||
# Runtime-shape benchmark command:
|
||||
# python3 test/registered/jit/test_deepseek_v4_compress_state_runtime_shapes.py \
|
||||
# python3 test/registered/kernels/ops/attention/test_deepseek_v4_compress_state_runtime_shapes.py \
|
||||
# --benchmark \
|
||||
# --shape-source runtime \
|
||||
# --warmup 20 \
|
||||
@@ -22,7 +22,7 @@
|
||||
# --csv /data00/eval_results/operator_bench/runtime_shape_bench.csv
|
||||
#
|
||||
# Synthetic Flash/Pro shape benchmark command:
|
||||
# python3 test/registered/jit/test_deepseek_v4_compress_state_runtime_shapes.py \
|
||||
# python3 test/registered/kernels/ops/attention/test_deepseek_v4_compress_state_runtime_shapes.py \
|
||||
# --benchmark \
|
||||
# --shape-source preset \
|
||||
# --shape-presets all \
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""KPool fused metadata must retain live tails and refresh captured buffers."""
|
||||
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.attention.dsa_kpool_metadata.verify import (
|
||||
fused_dsa_target_verify_metadata,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestKPoolMetadataFusion(CustomTestCase):
|
||||
def test_verify_replay_boundaries_and_request_remapping(self):
|
||||
device = "cuda"
|
||||
bs, next_n, width, topk, pool_size = 4, 6, 16384, 2048, 4
|
||||
seq = torch.tensor([1, 61, 2047, 8191], device=device, dtype=torch.int64)
|
||||
req = torch.tensor([3, 1, 6, 0], device=device, dtype=torch.int64)
|
||||
table = torch.arange(8 * width, device=device, dtype=torch.int32).view(8, width)
|
||||
|
||||
def empty(*shape):
|
||||
return torch.full(shape, -1, device=device, dtype=torch.int32)
|
||||
|
||||
buffers = dict(
|
||||
cache_seqlens=empty(bs),
|
||||
cu_seqlens_k=empty(bs + 1),
|
||||
page_table_1=empty(bs * next_n, width),
|
||||
seqlens_expanded=empty(bs * next_n),
|
||||
dsa_cache_seqlens=empty(bs * next_n),
|
||||
dsa_cu_seqlens_k=empty(bs * next_n + 1),
|
||||
real_page_table=empty(bs * next_n, width // 64),
|
||||
paged_mqa_ctx_lens_2d=empty(bs, next_n),
|
||||
)
|
||||
addresses = {key: value.data_ptr() for key, value in buffers.items()}
|
||||
|
||||
def refresh():
|
||||
fused_dsa_target_verify_metadata(
|
||||
seq_lens=seq,
|
||||
req_pool_indices=req,
|
||||
req_to_token=table,
|
||||
bs=bs,
|
||||
max_seqlen_k=width,
|
||||
dsa_index_topk=topk,
|
||||
real_page_size=64,
|
||||
next_n=next_n,
|
||||
index_kpool=pool_size,
|
||||
**buffers,
|
||||
)
|
||||
|
||||
refresh()
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
refresh()
|
||||
for lengths, requests in [
|
||||
([2, 63, 2048, 8193], [0, 6, 1, 3]),
|
||||
([64, 128, 2051, 8190], [5, 2, 7, 4]),
|
||||
([0, 3, 2045, 9000], [7, 0, 3, 2]),
|
||||
]:
|
||||
seq.copy_(torch.tensor(lengths, device=device))
|
||||
req.copy_(torch.tensor(requests, device=device))
|
||||
graph.replay()
|
||||
expanded = (
|
||||
seq[:, None] + torch.arange(1, next_n + 1, device=device)
|
||||
).flatten()
|
||||
expected = torch.minimum(expanded, topk + expanded % pool_size).int()
|
||||
torch.testing.assert_close(buffers["seqlens_expanded"], expanded.int())
|
||||
torch.testing.assert_close(buffers["dsa_cache_seqlens"], expected)
|
||||
torch.testing.assert_close(
|
||||
buffers["dsa_cu_seqlens_k"][1:], expected.cumsum(0).int()
|
||||
)
|
||||
torch.testing.assert_close(buffers["cache_seqlens"], (seq + next_n).int())
|
||||
torch.testing.assert_close(
|
||||
buffers["paged_mqa_ctx_lens_2d"],
|
||||
(seq + next_n).int()[:, None].expand(bs, next_n),
|
||||
)
|
||||
expected_pages = table[req].repeat_interleave(next_n, dim=0)
|
||||
row_lens = (seq + next_n).repeat_interleave(next_n)
|
||||
live = torch.arange(width, device=device)[None, :] < row_lens[:, None]
|
||||
torch.testing.assert_close(
|
||||
buffers["page_table_1"][live], expected_pages[live]
|
||||
)
|
||||
real_live = (
|
||||
torch.arange(0, width, 64, device=device)[None, :] < row_lens[:, None]
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
buffers["real_page_table"][real_live],
|
||||
(expected_pages[:, ::64] // 64)[real_live],
|
||||
)
|
||||
self.assertEqual(
|
||||
addresses, {key: value.data_ptr() for key, value in buffers.items()}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Fused KPool replay and MTP sibling copies preserve captured buffer identity."""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.dsa_metadata_kit import (
|
||||
BS,
|
||||
NEXT_N,
|
||||
ROUNDS,
|
||||
addresses,
|
||||
apply_metadata,
|
||||
assert_metadata_equal,
|
||||
inputs,
|
||||
make_backend,
|
||||
)
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestDSAMetadataReplay(CustomTestCase):
|
||||
def test_fusion_matches_ordinary_metadata(self):
|
||||
for mode in (
|
||||
ForwardMode.DECODE,
|
||||
ForwardMode.TARGET_VERIFY,
|
||||
ForwardMode.DRAFT_EXTEND_V2,
|
||||
):
|
||||
with self.subTest(mode=mode):
|
||||
seq, req = inputs(*ROUNDS[0])
|
||||
fused = make_backend(mode, seq, req)
|
||||
ordinary = make_backend(mode, seq, req, fusion=False)
|
||||
pointers = addresses(fused.forward_metadata)
|
||||
for lengths, requests in ROUNDS:
|
||||
seq.copy_(torch.tensor(lengths, device="cuda"))
|
||||
req.copy_(torch.tensor(requests, device="cuda"))
|
||||
spec = None
|
||||
if mode.is_draft_extend_v2():
|
||||
spec = SimpleNamespace(
|
||||
num_accept_tokens=torch.tensor(
|
||||
[1, 2, 5, NEXT_N], device="cuda", dtype=torch.int32
|
||||
)
|
||||
)
|
||||
apply_metadata(fused, mode, seq, req, spec)
|
||||
apply_metadata(ordinary, mode, seq, req, spec)
|
||||
assert_metadata_equal(
|
||||
self, fused.forward_metadata, ordinary.forward_metadata
|
||||
)
|
||||
self.assertEqual(pointers, addresses(fused.forward_metadata))
|
||||
|
||||
def test_precomputed_verify_retains_live_tail(self):
|
||||
mode = ForwardMode.TARGET_VERIFY
|
||||
seq, req = inputs(*ROUNDS[0])
|
||||
fused = make_backend(mode, seq, req)
|
||||
ordinary = make_backend(mode, seq, req, fusion=False)
|
||||
pointers = addresses(fused.forward_metadata)
|
||||
for lengths, requests in ROUNDS[1:]:
|
||||
seq.copy_(torch.tensor(lengths, device="cuda"))
|
||||
req.copy_(torch.tensor(requests, device="cuda"))
|
||||
precomputed = fused._precompute_replay_metadata(
|
||||
BS, req, seq, seq.cpu(), mode
|
||||
)
|
||||
fused.init_forward_metadata_replay_cuda_graph_from_precomputed(
|
||||
BS, precomputed, mode
|
||||
)
|
||||
apply_metadata(ordinary, mode, seq, req)
|
||||
assert_metadata_equal(
|
||||
self, fused.forward_metadata, ordinary.forward_metadata
|
||||
)
|
||||
self.assertEqual(pointers, addresses(fused.forward_metadata))
|
||||
|
||||
def test_precomputed_and_sibling_copy_refresh_derived_metadata(self):
|
||||
mode = ForwardMode.DECODE
|
||||
seq, req = inputs(*ROUNDS[0])
|
||||
source = make_backend(mode, seq, req)
|
||||
sibling = make_backend(mode, seq, req)
|
||||
ordinary = make_backend(mode, seq, req, fusion=False)
|
||||
pointers = addresses(sibling.forward_metadata)
|
||||
for lengths, requests in ROUNDS[1:]:
|
||||
seq.copy_(torch.tensor(lengths, device="cuda"))
|
||||
req.copy_(torch.tensor(requests, device="cuda"))
|
||||
precomputed = source._precompute_replay_metadata(
|
||||
BS, req, seq, seq.cpu(), mode
|
||||
)
|
||||
source.init_forward_metadata_replay_cuda_graph_from_precomputed(
|
||||
BS, precomputed, mode
|
||||
)
|
||||
# An eligible sibling must reuse the derived results, not silently
|
||||
# fall through to the full recomputation path.
|
||||
with patch.object(
|
||||
sibling,
|
||||
"init_forward_metadata_replay_cuda_graph_from_precomputed",
|
||||
side_effect=AssertionError("unexpected sibling fallback"),
|
||||
):
|
||||
sibling._copy_replay_metadata_from_sibling(
|
||||
source, BS, precomputed, mode
|
||||
)
|
||||
apply_metadata(ordinary, mode, seq, req)
|
||||
assert_metadata_equal(
|
||||
self, source.forward_metadata, ordinary.forward_metadata
|
||||
)
|
||||
assert_metadata_equal(
|
||||
self, sibling.forward_metadata, ordinary.forward_metadata
|
||||
)
|
||||
self.assertEqual(pointers, addresses(sibling.forward_metadata))
|
||||
self.assertIsNot(
|
||||
sibling.forward_metadata.kpool_write_plan,
|
||||
source.forward_metadata.kpool_write_plan,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,94 @@
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.attention.dsv4.elementwise import fused_rope_inplace
|
||||
from sglang.kernels.ops.attention.dsv4.q_rope_store import q_rope_store
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=40, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestQRopeStore(CustomTestCase):
|
||||
def test_exact_output_and_padding(self):
|
||||
torch.manual_seed(911)
|
||||
freqs = torch.polar(
|
||||
torch.ones(8192, 32, device="cuda"), torch.randn(8192, 32, device="cuda")
|
||||
)
|
||||
for rows in (1, 2, 5, 6, 8):
|
||||
for heads in (8, 16, 32):
|
||||
for dtype in (torch.int32, torch.int64):
|
||||
q = torch.randn(
|
||||
rows, heads + 1, 512, device="cuda", dtype=torch.bfloat16
|
||||
)[:, :heads]
|
||||
padding = torch.full(
|
||||
(rows, 64, 512), 7.0, device="cuda", dtype=q.dtype
|
||||
)
|
||||
output = padding[:, :heads]
|
||||
positions = torch.randint(
|
||||
0, 8192, (rows,), device="cuda", dtype=dtype
|
||||
)
|
||||
original = q.clone()
|
||||
expected = q.clone()
|
||||
fused_rope_inplace(expected[..., 448:], None, freqs, positions)
|
||||
q_rope_store(q, output, freqs, positions)
|
||||
torch.testing.assert_close(output, expected, rtol=0, atol=0)
|
||||
torch.testing.assert_close(q, original, rtol=0, atol=0)
|
||||
self.assertTrue((padding[:, heads:] == 7).all().item())
|
||||
|
||||
def test_large_prefill_exact_output_and_padding(self):
|
||||
torch.manual_seed(911)
|
||||
freqs = torch.polar(
|
||||
torch.ones(8192, 32, device="cuda"), torch.randn(8192, 32, device="cuda")
|
||||
)
|
||||
for rows in (4096, 4097, 65536):
|
||||
for dtype in (torch.int32, torch.int64):
|
||||
with self.subTest(rows=rows, dtype=dtype):
|
||||
q = torch.randn(rows, 17, 512, device="cuda", dtype=torch.bfloat16)[
|
||||
:, :16
|
||||
]
|
||||
original = q.clone()
|
||||
expected = q.clone()
|
||||
padding = torch.full(
|
||||
(rows, 64, 512), 7.0, device="cuda", dtype=q.dtype
|
||||
)
|
||||
positions = torch.randint(
|
||||
0, 8192, (rows,), device="cuda", dtype=dtype
|
||||
)
|
||||
fused_rope_inplace(expected[..., 448:], None, freqs, positions)
|
||||
q_rope_store(q, padding[:, :16], freqs, positions)
|
||||
torch.testing.assert_close(
|
||||
padding[:, :16], expected, rtol=0, atol=0
|
||||
)
|
||||
torch.testing.assert_close(q, original, rtol=0, atol=0)
|
||||
self.assertTrue((padding[:, 16:] == 7).all().item())
|
||||
|
||||
def _check_graph_replay(self, rows):
|
||||
q = torch.randn(rows, 16, 512, device="cuda", dtype=torch.bfloat16)
|
||||
output = torch.zeros(rows, 64, 512, device="cuda", dtype=q.dtype)[:, :16]
|
||||
freqs = torch.polar(
|
||||
torch.ones(8192, 32, device="cuda"), torch.randn(8192, 32, device="cuda")
|
||||
)
|
||||
positions = torch.arange(rows, device="cuda") % 8192
|
||||
q_rope_store(q, output, freqs, positions)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
q_rope_store(q, output, freqs, positions)
|
||||
for _ in range(3):
|
||||
q.normal_()
|
||||
positions.random_(0, 8192)
|
||||
graph.replay()
|
||||
expected = q.clone()
|
||||
fused_rope_inplace(expected[..., 448:], None, freqs, positions)
|
||||
torch.testing.assert_close(output, expected, rtol=0, atol=0)
|
||||
|
||||
def test_graph_replay(self):
|
||||
self._check_graph_replay(6)
|
||||
|
||||
def test_large_prefill_graph_replay(self):
|
||||
self._check_graph_replay(4097)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,167 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.srt.runtime_context import get_platform
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=25, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not torch.cuda.is_available()
|
||||
or torch.version.cuda is None
|
||||
or not get_platform().is_blackwell,
|
||||
reason="the FlashMLA split-KV schedule is Blackwell-only here",
|
||||
)
|
||||
|
||||
H_Q, D_QK, D_V = 64, 512, 512
|
||||
# The only cache format this build's sparse decode takes for both caches, and a
|
||||
# page size that keeps page * bytes_per_token a multiple of 576.
|
||||
BYTES_PER_TOKEN, PAGE = 584, 288
|
||||
BLOCK_SIZE_N, FIXED_OVERHEAD = 64, 5
|
||||
|
||||
|
||||
def _cache():
|
||||
n = 8192 // PAGE + 16
|
||||
return torch.randint(
|
||||
0, 200, (n, PAGE, 1, BYTES_PER_TOKEN), device="cuda", dtype=torch.uint8
|
||||
)
|
||||
|
||||
|
||||
def _call(kv, b, s_q, topk, topk_length, *, extra=None, meta=None):
|
||||
import sgl_kernel.flash_mla as flash_mla
|
||||
|
||||
g = torch.Generator(device="cuda").manual_seed(b * 7 + s_q * 13 + topk)
|
||||
q = torch.randn(
|
||||
(b, s_q, H_Q, D_QK), device="cuda", dtype=torch.bfloat16, generator=g
|
||||
)
|
||||
indices = torch.randint(
|
||||
0, 5120, (b, s_q, topk), device="cuda", dtype=torch.int32, generator=g
|
||||
)
|
||||
sink = torch.randn((H_Q,), device="cuda", dtype=torch.float32, generator=g)
|
||||
kwargs = {}
|
||||
if extra is not None:
|
||||
extra_kv, extra_indices, extra_topk_length = extra
|
||||
kwargs = dict(
|
||||
extra_k_cache=extra_kv,
|
||||
extra_indices_in_kvcache=extra_indices,
|
||||
extra_topk_length=extra_topk_length,
|
||||
)
|
||||
sched = flash_mla.FlashMLASchedMeta()
|
||||
if meta is not None:
|
||||
sched.tile_scheduler_metadata, sched.num_splits = meta
|
||||
out, lse = flash_mla.flash_mla_with_kvcache(
|
||||
q,
|
||||
kv,
|
||||
None,
|
||||
None,
|
||||
D_V,
|
||||
sched,
|
||||
indices=indices,
|
||||
is_fp8_kvcache=True,
|
||||
softmax_scale=0.1,
|
||||
causal=False,
|
||||
topk_length=topk_length,
|
||||
attn_sink=sink,
|
||||
**kwargs,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
return out, lse, sched
|
||||
|
||||
|
||||
def _ours(
|
||||
like_meta, like_splits, topk_length, topk, *, extra_topk_length=None, extra_topk=0
|
||||
):
|
||||
from sglang.kernels.ops.attention.dsv4.flashmla_sched_meta import (
|
||||
flashmla_sched_meta,
|
||||
)
|
||||
|
||||
meta = torch.empty_like(like_meta)
|
||||
splits = torch.empty_like(like_splits)
|
||||
flashmla_sched_meta(
|
||||
meta,
|
||||
splits,
|
||||
topk_length=topk_length,
|
||||
extra_topk_length=extra_topk_length,
|
||||
block_size_n=BLOCK_SIZE_N,
|
||||
fixed_overhead_num_blocks=FIXED_OVERHEAD,
|
||||
topk=topk,
|
||||
extra_topk=extra_topk,
|
||||
)
|
||||
return meta, splits
|
||||
|
||||
|
||||
def _lengths(b, topk, mode, seed):
|
||||
g = torch.Generator(device="cuda").manual_seed(seed)
|
||||
if mode == "full":
|
||||
return torch.full((b,), topk, device="cuda", dtype=torch.int32)
|
||||
if mode == "ones":
|
||||
return torch.ones((b,), device="cuda", dtype=torch.int32)
|
||||
if mode == "zeros":
|
||||
return torch.zeros((b,), device="cuda", dtype=torch.int32)
|
||||
lengths = torch.randint(
|
||||
0, topk + 1, (b,), device="cuda", dtype=torch.int32, generator=g
|
||||
)
|
||||
if mode == "mixed":
|
||||
lengths[0] = 0
|
||||
lengths[-1] = topk
|
||||
return lengths
|
||||
|
||||
|
||||
# FlashMLA's DecodingSchedMeta ends in a `_pad` word it never writes, so the
|
||||
# reference carries whatever torch::empty left there.
|
||||
DEFINED = slice(0, 7)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("b", [1, 6, 64])
|
||||
@pytest.mark.parametrize("s_q", [1, 6])
|
||||
@pytest.mark.parametrize("topk", [2048])
|
||||
@pytest.mark.parametrize("mode", ["full", "random", "zeros", "ones", "mixed"])
|
||||
def test_matches_flashmla_schedule(b: int, s_q: int, topk: int, mode: str):
|
||||
kv = _cache()
|
||||
topk_length = _lengths(b, topk, mode, b * 1000 + s_q * 37 + topk + len(mode))
|
||||
_, _, sched = _call(kv, b, s_q, topk, topk_length)
|
||||
if sched.tile_scheduler_metadata is None:
|
||||
pytest.skip("FlashMLA did not split the KV for this shape")
|
||||
meta, splits = _ours(
|
||||
sched.tile_scheduler_metadata, sched.num_splits, topk_length, topk
|
||||
)
|
||||
assert torch.equal(meta[:, DEFINED], sched.tile_scheduler_metadata[:, DEFINED])
|
||||
assert torch.equal(splits, sched.num_splits)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("b", [1, 8])
|
||||
@pytest.mark.parametrize("topk,extra_topk", [(512, 512), (2048, 512), (512, 2048)])
|
||||
def test_extra_cache_schedule(b: int, topk: int, extra_topk: int):
|
||||
kv, extra_kv = _cache(), _cache()
|
||||
s_q = 1
|
||||
g = torch.Generator(device="cuda").manual_seed(b + topk + extra_topk)
|
||||
topk_length = _lengths(b, topk, "random", b + topk)
|
||||
extra_topk_length = torch.randint(
|
||||
1, extra_topk + 1, (b,), device="cuda", dtype=torch.int32, generator=g
|
||||
)
|
||||
extra_indices = torch.randint(
|
||||
0, 4096, (b, s_q, extra_topk), device="cuda", dtype=torch.int32, generator=g
|
||||
)
|
||||
extra = (extra_kv, extra_indices, extra_topk_length)
|
||||
ref_out, ref_lse, sched = _call(kv, b, s_q, topk, topk_length, extra=extra)
|
||||
if sched.tile_scheduler_metadata is None:
|
||||
pytest.skip("FlashMLA did not split the KV for this shape")
|
||||
meta, splits = _ours(
|
||||
sched.tile_scheduler_metadata,
|
||||
sched.num_splits,
|
||||
topk_length,
|
||||
topk,
|
||||
extra_topk_length=extra_topk_length,
|
||||
extra_topk=extra_topk,
|
||||
)
|
||||
assert torch.equal(meta[:, DEFINED], sched.tile_scheduler_metadata[:, DEFINED])
|
||||
assert torch.equal(splits, sched.num_splits)
|
||||
out, lse, _ = _call(kv, b, s_q, topk, topk_length, extra=extra, meta=(meta, splits))
|
||||
assert torch.equal(out.view(torch.int16), ref_out.view(torch.int16))
|
||||
assert torch.equal(lse.view(torch.int32), ref_lse.view(torch.int32))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__]))
|
||||
@@ -0,0 +1,322 @@
|
||||
"""KDA backend dispatch and ReplaySSM verify -> commit -> verify parity."""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.attention.fla.fused_kda_conv_recurrent_verify import (
|
||||
fused_kda_conv_gating_verify,
|
||||
)
|
||||
from sglang.srt.layers.attention.hybrid_linear_attn_backend import (
|
||||
HybridLinearAttnBackend,
|
||||
)
|
||||
from sglang.srt.layers.attention.linear.kda_backend import (
|
||||
KDAAttnBackend,
|
||||
KDAKernelDispatcher,
|
||||
)
|
||||
from sglang.srt.layers.attention.linear.utils import LinearAttnKernelBackend
|
||||
from sglang.srt.mem_cache.memory_pool import MambaPool
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
||||
from sglang.srt.runtime_context import override_platform
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=90, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
# One bf16 ulp: the fused and unfused kernels reduce K in different orders.
|
||||
_OUTPUT_TOL = dict(rtol=2**-7, atol=1e-7)
|
||||
# Fold and snapshot recurrence differ by ~1 fp32 ulp; a wrong-step commit
|
||||
# moves elements by >= 1e-3, so 1e-5 still catches it.
|
||||
_SNAPSHOT_ORACLE_TOL = dict(rtol=0, atol=1e-5)
|
||||
|
||||
|
||||
class TestKDAFusedVerifyBackend(CustomTestCase):
|
||||
def _make_case(self, batch_size=1, heads=2, v_heads=4, lower_bound=-5.0):
|
||||
torch.manual_seed(36821)
|
||||
steps, head_dim, num_layers = 4, 128, 2
|
||||
num_slots = batch_size + 3
|
||||
dim = (2 * heads + v_heads) * head_dim
|
||||
|
||||
def randn(*shape, dtype=torch.bfloat16):
|
||||
return torch.randn(*shape, device="cuda", dtype=dtype) * 0.2
|
||||
|
||||
layers = [
|
||||
SimpleNamespace(
|
||||
layer_id=i,
|
||||
num_q_heads=heads,
|
||||
num_k_heads=heads,
|
||||
num_v_heads=v_heads,
|
||||
head_q_dim=head_dim,
|
||||
head_k_dim=head_dim,
|
||||
head_v_dim=head_dim,
|
||||
q_dim=heads * head_dim,
|
||||
k_dim=heads * head_dim,
|
||||
v_dim=v_heads * head_dim,
|
||||
conv_weights=randn(dim, 4),
|
||||
bias=randn(dim),
|
||||
A_log=randn(v_heads, dtype=torch.float32),
|
||||
dt_bias=randn(v_heads * head_dim, dtype=torch.float32),
|
||||
lower_bound=lower_bound,
|
||||
)
|
||||
for i in range(num_layers)
|
||||
]
|
||||
state = MambaPool.SpeculativeState(
|
||||
conv=[randn(num_layers, num_slots, 3, dim)],
|
||||
temporal=randn(
|
||||
num_layers,
|
||||
num_slots,
|
||||
v_heads,
|
||||
head_dim,
|
||||
head_dim,
|
||||
dtype=torch.float32,
|
||||
),
|
||||
intermediate_ssm=None,
|
||||
intermediate_conv_window=[randn(num_layers, batch_size, steps, 3, dim)],
|
||||
replayssm_rawv=randn(num_layers, num_slots, v_heads, 16, head_dim),
|
||||
replayssm_rawk=randn(num_layers, num_slots, heads, 16, head_dim),
|
||||
replayssm_g=randn(
|
||||
num_layers, num_slots, v_heads, 16, head_dim, dtype=torch.float32
|
||||
),
|
||||
replayssm_beta=randn(
|
||||
num_layers, num_slots, v_heads, 16, dtype=torch.float32
|
||||
),
|
||||
)
|
||||
# Physical slots differ from scratch rows; reverse them to catch callers
|
||||
# accidentally committing by request index instead of mamba slot.
|
||||
slots = torch.arange(batch_size + 1, 1, -1, device="cuda", dtype=torch.int32)
|
||||
batch = SimpleNamespace(
|
||||
forward_mode=ForwardMode.TARGET_VERIFY,
|
||||
spec_info=SimpleNamespace(draft_token_num=steps, ragged_verify_layout=None),
|
||||
)
|
||||
rounds = [
|
||||
[
|
||||
(
|
||||
randn(batch_size * steps, dim),
|
||||
randn(1, batch_size * steps, v_heads * head_dim),
|
||||
randn(1, batch_size * steps, v_heads),
|
||||
)
|
||||
for _ in layers
|
||||
]
|
||||
for _ in range(2)
|
||||
]
|
||||
return layers, state, slots, batch, rounds
|
||||
|
||||
def _make_backend(self, template, slots, steps, *, fused, ring=True):
|
||||
state = MambaPool.SpeculativeState(
|
||||
conv=[template.conv[0].clone()],
|
||||
temporal=template.temporal.clone(),
|
||||
intermediate_conv_window=[template.intermediate_conv_window[0].clone()],
|
||||
intermediate_ssm=(
|
||||
None
|
||||
if ring
|
||||
else template.temporal.new_zeros(
|
||||
template.temporal.shape[0],
|
||||
slots.numel(),
|
||||
steps,
|
||||
*template.temporal.shape[2:],
|
||||
)
|
||||
),
|
||||
**{
|
||||
name: getattr(template, name).clone() if ring else None
|
||||
for name in (
|
||||
"replayssm_rawv",
|
||||
"replayssm_rawk",
|
||||
"replayssm_g",
|
||||
"replayssm_beta",
|
||||
)
|
||||
},
|
||||
)
|
||||
# Only the model/pool setup is a fixture. Verify, dispatch, ring fold and
|
||||
# conv rollback below all use the production backend and GPU kernels.
|
||||
backend = KDAAttnBackend.__new__(KDAAttnBackend)
|
||||
backend.req_to_token_pool = SimpleNamespace(
|
||||
mamba2_layer_cache=state.at_layer_idx,
|
||||
get_speculative_mamba2_params_all_layers=lambda: state,
|
||||
mamba_pool=SimpleNamespace(replayssm_is_kda=ring),
|
||||
)
|
||||
backend.forward_metadata = SimpleNamespace(
|
||||
query_start_loc=torch.arange(
|
||||
slots.numel() + 1, device="cuda", dtype=torch.int32
|
||||
)
|
||||
* steps,
|
||||
mamba_cache_indices=slots,
|
||||
retrieve_next_token=None,
|
||||
retrieve_next_sibling=None,
|
||||
retrieve_parent_token=None,
|
||||
)
|
||||
backend.verify_intermediate_state_indices = torch.arange(
|
||||
slots.numel(), device="cuda", dtype=torch.int32
|
||||
)
|
||||
backend.accept_lens_pool = None
|
||||
backend.kernel_dispatcher = KDAKernelDispatcher(
|
||||
LinearAttnKernelBackend.TRITON,
|
||||
LinearAttnKernelBackend.TRITON,
|
||||
LinearAttnKernelBackend.TRITON,
|
||||
)
|
||||
backend._fused_chain_verify_fn = (
|
||||
Mock(wraps=fused_kda_conv_gating_verify) if fused else None
|
||||
)
|
||||
hybrid = HybridLinearAttnBackend.__new__(HybridLinearAttnBackend)
|
||||
hybrid.linear_attn_backend = backend
|
||||
return backend, hybrid, state
|
||||
|
||||
@staticmethod
|
||||
def _verify(backend, layers, batch, inputs):
|
||||
return [
|
||||
backend.forward_extend(layer, batch, mixed, a, b)
|
||||
for layer, (mixed, a, b) in zip(layers, inputs)
|
||||
]
|
||||
|
||||
def test_verify_commit_verify(self):
|
||||
# B=1 exercises the enabled path. Platform override makes the dispatch
|
||||
# testable on any CUDA CI runner; it does not replace a GPU kernel.
|
||||
for platform, (heads, v_heads, lower_bound), num_accept_tokens in (
|
||||
({"is_sm90": True}, (2, 2, None), 1),
|
||||
({"is_sm90": True}, (2, 2, None), 2),
|
||||
({"is_sm90": True}, (2, 2, None), 4),
|
||||
({"is_sm90": True}, (2, 4, -5.0), 1),
|
||||
({"is_sm90": True}, (2, 4, -5.0), 2),
|
||||
({"is_sm90": True}, (2, 4, -5.0), 4),
|
||||
({"is_sm90": False, "is_sm100": True}, (2, 4, -5.0), 2),
|
||||
):
|
||||
with (
|
||||
self.subTest(
|
||||
platform=platform,
|
||||
heads=heads,
|
||||
v_heads=v_heads,
|
||||
lower_bound=lower_bound,
|
||||
num_accept_tokens=num_accept_tokens,
|
||||
),
|
||||
override_platform(**platform),
|
||||
):
|
||||
layers, initial, slots, batch, rounds = self._make_case(
|
||||
heads=heads, v_heads=v_heads, lower_bound=lower_bound
|
||||
)
|
||||
fused, fused_hybrid, fused_state = self._make_backend(
|
||||
initial, slots, 4, fused=True
|
||||
)
|
||||
reference, ref_hybrid, ref_state = self._make_backend(
|
||||
initial, slots, 4, fused=False
|
||||
)
|
||||
snapshots, _, snapshot_state = self._make_backend(
|
||||
initial, slots, 4, fused=False, ring=False
|
||||
)
|
||||
out_fused = self._verify(fused, layers, batch, rounds[0])
|
||||
out_ref = self._verify(reference, layers, batch, rounds[0])
|
||||
self._verify(snapshots, layers, batch, rounds[0])
|
||||
for actual, expected in zip(out_fused, out_ref):
|
||||
torch.testing.assert_close(actual, expected, **_OUTPUT_TOL)
|
||||
for state in (fused_state, ref_state):
|
||||
torch.testing.assert_close(
|
||||
state.temporal, initial.temporal, rtol=0, atol=0
|
||||
)
|
||||
|
||||
last_steps = torch.full_like(slots, num_accept_tokens - 1)
|
||||
for hybrid in (fused_hybrid, ref_hybrid):
|
||||
hybrid.update_mamba_state_after_mtp_verify(
|
||||
last_correct_step_indices=last_steps,
|
||||
mamba_track_indices=None,
|
||||
mamba_steps_to_track=None,
|
||||
model=None,
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
fused_state.temporal, ref_state.temporal, rtol=0, atol=0
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
fused_state.conv[0], ref_state.conv[0], rtol=0, atol=0
|
||||
)
|
||||
# Independent snapshot oracle: equality between two ring
|
||||
# arms alone would miss a shared no-op / wrong-step commit.
|
||||
expected_ssm = initial.temporal.clone()
|
||||
expected_ssm[:, slots.long()] = snapshot_state.intermediate_ssm[
|
||||
:, :, num_accept_tokens - 1
|
||||
]
|
||||
torch.testing.assert_close(
|
||||
fused_state.temporal, expected_ssm, **_SNAPSHOT_ORACLE_TOL
|
||||
)
|
||||
expected_conv = initial.conv[0].clone()
|
||||
for i, (mixed, _, _) in enumerate(rounds[0]):
|
||||
history = torch.cat(
|
||||
(
|
||||
initial.conv[0][i, slots.long()],
|
||||
mixed.view(1, 4, -1)[:, :num_accept_tokens],
|
||||
),
|
||||
dim=1,
|
||||
)
|
||||
expected_conv[i, slots.long()] = history[:, -3:]
|
||||
torch.testing.assert_close(
|
||||
fused_state.conv[0], expected_conv, rtol=0, atol=0
|
||||
)
|
||||
|
||||
out_fused = self._verify(fused, layers, batch, rounds[1])
|
||||
out_ref = self._verify(reference, layers, batch, rounds[1])
|
||||
for actual, expected in zip(out_fused, out_ref):
|
||||
torch.testing.assert_close(actual, expected, **_OUTPUT_TOL)
|
||||
self.assertEqual(fused._fused_chain_verify_fn.call_count, 4)
|
||||
|
||||
def test_ring_dispatch_falls_back(self):
|
||||
# B=2 and the measured regression sizes on the enabled architectures,
|
||||
# plus B=1 on an architecture without ring measurements.
|
||||
sm90 = {"is_sm90": True, "is_sm100": False}
|
||||
sm100 = {"is_sm90": False, "is_sm100": True}
|
||||
other = {"is_sm90": False, "is_sm100": False}
|
||||
for platform, batch_size in (
|
||||
(sm90, 2),
|
||||
(sm90, 4),
|
||||
(sm90, 16),
|
||||
(sm90, 64),
|
||||
(sm100, 2),
|
||||
(sm100, 4),
|
||||
(sm100, 16),
|
||||
(other, 1),
|
||||
(other, 4),
|
||||
):
|
||||
with (
|
||||
self.subTest(platform=platform, batch_size=batch_size),
|
||||
override_platform(**platform),
|
||||
):
|
||||
layers, initial, slots, batch, rounds = self._make_case(batch_size)
|
||||
backend, _, state = self._make_backend(initial, slots, 4, fused=True)
|
||||
reference, _, ref_state = self._make_backend(
|
||||
initial, slots, 4, fused=False
|
||||
)
|
||||
out = self._verify(backend, layers, batch, rounds[0])
|
||||
ref = self._verify(reference, layers, batch, rounds[0])
|
||||
backend._fused_chain_verify_fn.assert_not_called()
|
||||
for actual, expected in zip(out, ref):
|
||||
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
|
||||
for name in (
|
||||
"replayssm_rawv",
|
||||
"replayssm_rawk",
|
||||
"replayssm_g",
|
||||
"replayssm_beta",
|
||||
):
|
||||
torch.testing.assert_close(
|
||||
getattr(state, name), getattr(ref_state, name), rtol=0, atol=0
|
||||
)
|
||||
|
||||
def test_snapshot_dispatch_is_unchanged(self):
|
||||
for platform in (
|
||||
{"is_sm90": True, "is_sm100": False},
|
||||
{"is_sm90": False, "is_sm100": True},
|
||||
{"is_sm90": False, "is_sm100": False},
|
||||
):
|
||||
with self.subTest(platform=platform), override_platform(**platform):
|
||||
layers, initial, slots, batch, rounds = self._make_case(batch_size=4)
|
||||
backend, _, _ = self._make_backend(
|
||||
initial, slots, 4, fused=True, ring=False
|
||||
)
|
||||
reference, _, _ = self._make_backend(
|
||||
initial, slots, 4, fused=False, ring=False
|
||||
)
|
||||
out = self._verify(backend, layers, batch, rounds[0])
|
||||
ref = self._verify(reference, layers, batch, rounds[0])
|
||||
self.assertEqual(backend._fused_chain_verify_fn.call_count, 2)
|
||||
for actual, expected in zip(out, ref):
|
||||
torch.testing.assert_close(actual, expected, **_OUTPUT_TOL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,183 @@
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.attention.fla.kda import chunk_kda
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=180, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
CHUNK_SIZE = 64
|
||||
|
||||
_BACKENDS = {"triton": chunk_kda}
|
||||
HELION_AVAILABLE = True
|
||||
try:
|
||||
import helion # noqa: F401
|
||||
except ModuleNotFoundError as error:
|
||||
# A broken install (transitive import failure) must stay loud; only the
|
||||
# absent package downgrades the run to triton-only.
|
||||
if error.name != "helion":
|
||||
raise
|
||||
HELION_AVAILABLE = False
|
||||
if HELION_AVAILABLE:
|
||||
from sglang.kernels.ops.attention.helion.kda_prefill import (
|
||||
chunk_kda as helion_chunk_kda,
|
||||
)
|
||||
|
||||
_BACKENDS["helion"] = helion_chunk_kda
|
||||
|
||||
|
||||
def _make_varlen_inputs(seed, lens, num_heads=2, head_dim=128):
|
||||
"""Packed varlen KDA inputs: [1, sum(lens), H, D] plus a zero fp32 state pool."""
|
||||
generator = torch.Generator(device="cuda").manual_seed(seed)
|
||||
total = sum(lens)
|
||||
|
||||
def randn(*shape, dtype=torch.bfloat16):
|
||||
return torch.randn(*shape, generator=generator, device="cuda", dtype=dtype)
|
||||
|
||||
q = randn(1, total, num_heads, head_dim)
|
||||
k = randn(1, total, num_heads, head_dim)
|
||||
v = (0.1 * randn(1, total, num_heads, head_dim, dtype=torch.float32)).to(
|
||||
torch.bfloat16
|
||||
)
|
||||
gate = randn(1, total, num_heads, head_dim)
|
||||
beta = torch.sigmoid(randn(1, total, num_heads, dtype=torch.float32)).to(
|
||||
torch.bfloat16
|
||||
)
|
||||
a_log = randn(num_heads, dtype=torch.float32)
|
||||
dt_bias = randn(num_heads * head_dim, dtype=torch.float32)
|
||||
state = torch.zeros(
|
||||
len(lens), num_heads, head_dim, head_dim, device="cuda", dtype=torch.float32
|
||||
)
|
||||
cu_seqlens = torch.tensor(
|
||||
[0, *torch.tensor(lens).cumsum(0).tolist()], dtype=torch.int32, device="cuda"
|
||||
)
|
||||
return q, k, v, gate, beta, a_log, dt_bias, state, cu_seqlens
|
||||
|
||||
|
||||
def _run_chunk_kda(
|
||||
chunk_kda_fn, q, k, v, gate, beta, a_log, dt_bias, state, cu_seqlens, **kwargs
|
||||
):
|
||||
return chunk_kda_fn(
|
||||
# chunk_kda writes in place (the attention output lands in v, the gate
|
||||
# cumsum in g); hand every run fresh copies so runs stay independent.
|
||||
q=q.clone(),
|
||||
k=k.clone(),
|
||||
v=v.clone(),
|
||||
g=gate.clone(),
|
||||
beta=beta.clone(),
|
||||
scale=q.shape[-1] ** -0.5,
|
||||
initial_state=state,
|
||||
initial_state_indices=torch.arange(
|
||||
state.shape[0], device="cuda", dtype=torch.int32
|
||||
),
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
cu_seqlens=cu_seqlens,
|
||||
A_log=a_log,
|
||||
dt_bias=dt_bias,
|
||||
lower_bound=-5.0,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class TestKdaTrackState(CustomTestCase):
|
||||
def test_helion_backend_ran(self):
|
||||
"""Visibility hook: without helion installed the snapshot check above
|
||||
runs triton-only and the Helion track configs go untested — surface
|
||||
that as an explicit skip instead of a silent pass."""
|
||||
if not HELION_AVAILABLE:
|
||||
self.skipTest("helion is not installed; triton backend only")
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_track_state_snapshots_fp32_accumulator(self):
|
||||
"""Bug regression: the mamba radix track path snapshots the SSM state at
|
||||
the last chunk boundary of unaligned sequences into the fp32 state pool.
|
||||
It used to read the per-chunk states `h` (activation dtype, bf16), so a
|
||||
prefix-cache hit restored a bf16-rounded state while a cache miss kept
|
||||
fp32. `track_state` must carry the in-kernel fp32 accumulator: identical
|
||||
to the fp32 final state of a run truncated at the boundary, and strictly
|
||||
more precise than the bf16 `h` row for the same boundary.
|
||||
"""
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("requires CUDA")
|
||||
for backend, chunk_kda_fn in _BACKENDS.items():
|
||||
# num_heads=2 exercises the Helion small-head track config; 16
|
||||
# crosses _PREFILL_SMALL_HEAD_THRESHOLD (12) to exercise the
|
||||
# large-head varlen track config that real models take.
|
||||
for num_heads in (2, 16):
|
||||
with self.subTest(backend=backend, num_heads=num_heads):
|
||||
self._check_track_state(chunk_kda_fn, num_heads)
|
||||
|
||||
def _check_track_state(self, chunk_kda_fn, num_heads):
|
||||
# seq0: 100 tokens, unaligned -> snapshot at the 64-token boundary
|
||||
# (start of chunk 1). seq1: 64 tokens, aligned -> not tracked.
|
||||
lens = [100, 64]
|
||||
q, k, v, gate, beta, a_log, dt_bias, state, cu_seqlens = _make_varlen_inputs(
|
||||
0, lens, num_heads=num_heads
|
||||
)
|
||||
num_heads, head_dim = q.shape[2], q.shape[3]
|
||||
|
||||
track_state = torch.full(
|
||||
(len(lens), num_heads, head_dim, head_dim),
|
||||
float("nan"),
|
||||
device="cuda",
|
||||
dtype=torch.float32,
|
||||
)
|
||||
track_chunk_idx = torch.tensor([1, -1], dtype=torch.int32, device="cuda")
|
||||
_, h = _run_chunk_kda(
|
||||
chunk_kda_fn,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
gate,
|
||||
beta,
|
||||
a_log,
|
||||
dt_bias,
|
||||
state,
|
||||
cu_seqlens,
|
||||
output_intermediate_states=True,
|
||||
track_state=track_state,
|
||||
track_chunk_idx=track_chunk_idx,
|
||||
)
|
||||
|
||||
# The untracked row must stay untouched; the tracked row must be finite.
|
||||
self.assertTrue(torch.all(torch.isnan(track_state[1])))
|
||||
self.assertFalse(torch.any(torch.isnan(track_state[0])))
|
||||
|
||||
# Reference: truncate seq0 at the boundary; the pool's fp32 row then
|
||||
# receives the in-place final state for the same prefix — the
|
||||
# established fp32 path the snapshot must agree with.
|
||||
ref_state = torch.zeros(
|
||||
1, num_heads, head_dim, head_dim, device="cuda", dtype=torch.float32
|
||||
)
|
||||
ref_cu_seqlens = torch.tensor([0, CHUNK_SIZE], dtype=torch.int32, device="cuda")
|
||||
_run_chunk_kda(
|
||||
chunk_kda_fn,
|
||||
q[:, :CHUNK_SIZE],
|
||||
k[:, :CHUNK_SIZE],
|
||||
v[:, :CHUNK_SIZE],
|
||||
gate[:, :CHUNK_SIZE],
|
||||
beta[:, :CHUNK_SIZE],
|
||||
a_log,
|
||||
dt_bias,
|
||||
ref_state,
|
||||
ref_cu_seqlens,
|
||||
)
|
||||
torch.testing.assert_close(track_state[0], ref_state[0], rtol=1e-5, atol=1e-5)
|
||||
|
||||
# The guard: h packs one row per (seq, chunk); row 1 is seq0's state at
|
||||
# the boundary, rounded to bf16. If the snapshot were re-routed through
|
||||
# h, it could not match the fp32 reference above.
|
||||
self.assertTrue(
|
||||
torch.equal(h[0, 1].float(), track_state[0].to(torch.bfloat16).float()),
|
||||
"h row should be exactly the bf16 rounding of the fp32 snapshot",
|
||||
)
|
||||
self.assertFalse(
|
||||
torch.equal(track_state[0], track_state[0].to(torch.bfloat16).float()),
|
||||
"test inputs must make bf16 rounding lossy",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,140 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Correctness tests for native SM90 SubBlock Sage FP8 attention."""
|
||||
|
||||
import math
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=45, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
def _native_sm90_sage_available() -> bool:
|
||||
try:
|
||||
import spas_sage_attn._qattn as qattn
|
||||
except (ImportError, OSError):
|
||||
return False
|
||||
return hasattr(
|
||||
qattn,
|
||||
"qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_sm90",
|
||||
)
|
||||
|
||||
|
||||
requires_native_sm90_sage = unittest.skipUnless(
|
||||
torch.cuda.is_available()
|
||||
and torch.cuda.get_device_capability() == (9, 0)
|
||||
and _native_sm90_sage_available(),
|
||||
"requires SM90 and a compiled SpargeAttention installation",
|
||||
)
|
||||
|
||||
|
||||
def _masked_reference(q, k, v, index, counts, scale, key_block_size=128):
|
||||
logits = torch.einsum("bqhd,bkhd->bhqk", q.float(), k.float()) * scale
|
||||
mask = torch.zeros_like(logits, dtype=torch.bool)
|
||||
for b in range(q.shape[0]):
|
||||
for h in range(q.shape[2]):
|
||||
for qb in range(index.shape[2]):
|
||||
q_slice = slice(qb * 64, min((qb + 1) * 64, q.shape[1]))
|
||||
for slot in range(int(counts[b, h, qb])):
|
||||
kb = int(index[b, h, qb, slot])
|
||||
k_slice = slice(
|
||||
kb * key_block_size,
|
||||
min((kb + 1) * key_block_size, k.shape[1]),
|
||||
)
|
||||
mask[b, h, q_slice, k_slice] = True
|
||||
logits.masked_fill_(~mask, -float("inf"))
|
||||
p = torch.softmax(logits, dim=-1)
|
||||
return torch.einsum("bhqk,bkhd->bqhd", p, v.float()).to(torch.bfloat16)
|
||||
|
||||
|
||||
def _cosine(a, b):
|
||||
return float(
|
||||
torch.nn.functional.cosine_similarity(
|
||||
a.float().flatten(), b.float().flatten(), dim=0
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@requires_native_sm90_sage
|
||||
class TestSubBlockSageFp8NativeSm90(CustomTestCase):
|
||||
def test_full_budget_ragged_tail_reproduces_dense(self):
|
||||
from sglang.kernels.ops.attention.subblock_sage_fp8_sm90 import (
|
||||
subblock_sage_fp8_sm90_attention,
|
||||
)
|
||||
|
||||
torch.manual_seed(19)
|
||||
seq_len = 1024 + 37
|
||||
heads = 4
|
||||
shape = (1, seq_len, heads, 128)
|
||||
q = torch.randn(shape, device="cuda", dtype=torch.bfloat16)
|
||||
k = torch.randn_like(q)
|
||||
v = torch.randn_like(q)
|
||||
scale = 1.0 / math.sqrt(128)
|
||||
|
||||
query_blocks = math.ceil(seq_len / 64)
|
||||
key_blocks = math.ceil(seq_len / 128)
|
||||
index = (
|
||||
torch.arange(key_blocks, device="cuda", dtype=torch.int32)
|
||||
.view(1, 1, 1, key_blocks)
|
||||
.expand(1, heads, query_blocks, key_blocks)
|
||||
.contiguous()
|
||||
)
|
||||
output = subblock_sage_fp8_sm90_attention(q, k, v, index, key_blocks, scale)
|
||||
reference = torch.nn.functional.scaled_dot_product_attention(
|
||||
q.transpose(1, 2),
|
||||
k.transpose(1, 2),
|
||||
v.transpose(1, 2),
|
||||
scale=scale,
|
||||
).transpose(1, 2)
|
||||
|
||||
self.assertTrue(torch.isfinite(output.float()).all())
|
||||
self.assertGreater(_cosine(output, reference), 0.998)
|
||||
|
||||
def test_sparse_k128_plan_and_variable_counts(self):
|
||||
from sglang.kernels.ops.attention.subblock_sage_fp8_sm90 import (
|
||||
subblock_sage_fp8_sm90_attention,
|
||||
)
|
||||
|
||||
torch.manual_seed(23)
|
||||
seq_len = 1024 + 37
|
||||
heads = 4
|
||||
shape = (1, seq_len, heads, 128)
|
||||
q = torch.randn(shape, device="cuda", dtype=torch.bfloat16)
|
||||
k = torch.randn_like(q)
|
||||
v = torch.randn_like(q)
|
||||
scale = 1.0 / math.sqrt(128)
|
||||
|
||||
query_blocks = math.ceil(seq_len / 64)
|
||||
key_blocks = math.ceil(seq_len / 128)
|
||||
width = 4
|
||||
index = torch.stack(
|
||||
[
|
||||
torch.roll(torch.arange(key_blocks), shifts=query_block)[:width]
|
||||
for query_block in range(query_blocks)
|
||||
]
|
||||
)
|
||||
index = (
|
||||
index.to(device="cuda", dtype=torch.int32)
|
||||
.view(1, 1, query_blocks, width)
|
||||
.expand(1, heads, query_blocks, width)
|
||||
.contiguous()
|
||||
)
|
||||
counts = (
|
||||
((torch.arange(query_blocks, device="cuda", dtype=torch.int32) % width) + 1)
|
||||
.view(1, 1, query_blocks)
|
||||
.expand(1, heads, query_blocks)
|
||||
.contiguous()
|
||||
)
|
||||
|
||||
output = subblock_sage_fp8_sm90_attention(q, k, v, index, width, scale, counts)
|
||||
reference = _masked_reference(q, k, v, index, counts, scale)
|
||||
|
||||
self.assertTrue(torch.isfinite(output.float()).all())
|
||||
self.assertGreater(_cosine(output, reference), 0.997)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=3)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""A token must retain its bits when batch size changes its receiving TP rank.
|
||||
|
||||
Run with ``python test_deterministic_reduce_scatter.py --num-gpu 4``.
|
||||
This exercises the collective used before DeepEP MoE without a model or DeepEP.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
from sglang.srt.distributed import parallel_state as ps
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kernels.utils import multigpu_pytest_main
|
||||
|
||||
register_cuda_ci(est_time=45, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def group():
|
||||
rank = int(os.environ["RANK"])
|
||||
world_size = int(os.environ["WORLD_SIZE"])
|
||||
local_rank = int(os.environ["LOCAL_RANK"])
|
||||
# Match --enable-deterministic-inference's CUDA collective settings.
|
||||
os.environ["SGLANG_ENABLE_DETERMINISTIC_INFERENCE"] = "1"
|
||||
os.environ["NCCL_ALGO"] = "allreduce:tree"
|
||||
nchannels = str(envs.SGLANG_DETERMINISTIC_NCCL_NCHANNELS.get())
|
||||
os.environ["NCCL_MIN_NCHANNELS"] = nchannels
|
||||
os.environ["NCCL_MAX_NCHANNELS"] = nchannels
|
||||
torch.cuda.set_device(local_rank)
|
||||
ps.set_custom_all_reduce(False)
|
||||
ps.init_distributed_environment(
|
||||
world_size=world_size,
|
||||
rank=rank,
|
||||
local_rank=local_rank,
|
||||
distributed_init_method="env://",
|
||||
)
|
||||
ps.initialize_model_parallel(tensor_model_parallel_size=world_size)
|
||||
yield ps.get_tp_group()
|
||||
ps.destroy_model_parallel()
|
||||
ps.destroy_distributed_environment()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16, torch.float32])
|
||||
@pytest.mark.parametrize("use_graph", [False, True])
|
||||
def test_batch_and_destination_invariance(group, dtype, use_graph):
|
||||
torch.manual_seed(42 + group.rank_in_group)
|
||||
# Non-integer contributions expose changes in floating-point sum order.
|
||||
token = torch.randn(2048, device="cuda", dtype=dtype)
|
||||
reference = None
|
||||
for local_tokens in (1, 2, 3, 8, 32, 128):
|
||||
input_ = token.repeat(local_tokens * group.world_size, 1)
|
||||
original = input_.clone()
|
||||
output = torch.empty((local_tokens, 2048), device="cuda", dtype=dtype)
|
||||
if use_graph:
|
||||
with group.graph_capture() as capture:
|
||||
for _ in range(3):
|
||||
group.reduce_scatter_tensor(output, input_)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph, stream=capture.stream):
|
||||
group.reduce_scatter_tensor(output, input_)
|
||||
graph.replay()
|
||||
else:
|
||||
group.reduce_scatter_tensor(output, input_)
|
||||
|
||||
# Compare all destinations to catch rank-dependent reduction order.
|
||||
all_outputs = torch.empty_like(input_)
|
||||
dist.all_gather_into_tensor(all_outputs, output, group=group.device_group)
|
||||
if reference is None:
|
||||
reference = all_outputs[0].clone()
|
||||
torch.testing.assert_close(
|
||||
all_outputs, reference.expand_as(all_outputs), atol=0, rtol=0
|
||||
)
|
||||
torch.testing.assert_close(input_, original, atol=0, rtol=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
multigpu_pytest_main(__name__, __file__, num_gpus=(4,))
|
||||
@@ -7,12 +7,12 @@ NCCL all-gather for a sweep of token counts, hidden widths, and the
|
||||
Usage::
|
||||
|
||||
# Run on the default world sizes (2, 4, 8 GPUs):
|
||||
python test/registered/jit/test_symm_mem_all_gather.py
|
||||
python test/registered/kernels/ops/communication/test_symm_mem_all_gather.py
|
||||
# Pick a specific world size (or comma-separated list):
|
||||
python test/registered/jit/test_symm_mem_all_gather.py --num-gpu 4
|
||||
python test/registered/jit/test_symm_mem_all_gather.py --num-gpu 2,4,8
|
||||
python test/registered/kernels/ops/communication/test_symm_mem_all_gather.py --num-gpu 4
|
||||
python test/registered/kernels/ops/communication/test_symm_mem_all_gather.py --num-gpu 2,4,8
|
||||
# Extra pytest args (forwarded to each torchrun worker):
|
||||
python test/registered/jit/test_symm_mem_all_gather.py -k 16384
|
||||
python test/registered/kernels/ops/communication/test_symm_mem_all_gather.py -k 16384
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""CUDA graph executable reuse across capture sizes and rejected updates."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.model_executor.runner_backend import cuda_graph_dedup_mixin as dedup
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
@unittest.skipUnless(torch.cuda.is_available(), "requires CUDA")
|
||||
class TestCudaGraphDedup(CustomTestCase):
|
||||
@staticmethod
|
||||
def capture(size, *, extra_node=False):
|
||||
inputs = torch.zeros(size, device="cuda")
|
||||
outputs = torch.empty_like(inputs)
|
||||
graph = torch.cuda.CUDAGraph(keep_graph=True)
|
||||
with torch.cuda.graph(graph):
|
||||
torch.add(inputs, 1, out=outputs)
|
||||
if extra_node:
|
||||
outputs.mul_(2)
|
||||
return graph, inputs, outputs
|
||||
|
||||
def test_grid_sizes_share_one_executable(self):
|
||||
registry = dedup.DedupedCudaGraphRegistry()
|
||||
self.addCleanup(registry.close)
|
||||
captures = [self.capture(size) for size in (4096, 8192)]
|
||||
self.assertEqual(
|
||||
dedup.graph_signature(captures[0][0].raw_cuda_graph()),
|
||||
dedup.graph_signature(captures[1][0].raw_cuda_graph()),
|
||||
)
|
||||
with patch.object(
|
||||
registry, "instantiate", wraps=registry.instantiate
|
||||
) as instantiate:
|
||||
graphs = [registry.register(capture[0]) for capture in captures]
|
||||
self.assertEqual(instantiate.call_count, 1)
|
||||
self.assertEqual(registry.stats(), (2, 1))
|
||||
self.assertEqual(graphs[0].group.current_raw_graph, graphs[1].raw_graph)
|
||||
registry.seal()
|
||||
registry.seal()
|
||||
for value in (3, 7):
|
||||
for graph, (_, inputs, outputs) in zip(graphs, captures):
|
||||
inputs.fill_(value)
|
||||
graph.replay()
|
||||
torch.cuda.synchronize()
|
||||
self.assertTrue(
|
||||
torch.equal(outputs, torch.full_like(outputs, value + 1))
|
||||
)
|
||||
|
||||
def test_rejected_registration_preserves_live_executable(self):
|
||||
registry = dedup.DedupedCudaGraphRegistry()
|
||||
self.addCleanup(registry.close)
|
||||
original, inputs, outputs = self.capture(4096)
|
||||
incompatible, _, _ = self.capture(4096, extra_node=True)
|
||||
self.addCleanup(incompatible.reset)
|
||||
graph = registry.register(original)
|
||||
signature = dedup.graph_signature(graph.raw_graph)
|
||||
with (
|
||||
patch.object(dedup, "graph_signature", return_value=signature),
|
||||
self.assertRaisesRegex(AssertionError, "register update failed"),
|
||||
):
|
||||
registry.register(incompatible)
|
||||
self.assertEqual(registry.stats(), (1, 1))
|
||||
self.assertEqual(graph.group.current_raw_graph, graph.raw_graph)
|
||||
inputs.fill_(11)
|
||||
graph.replay()
|
||||
torch.cuda.synchronize()
|
||||
self.assertTrue(torch.equal(outputs, torch.full_like(outputs, 12)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Blackwell coverage for the FLUX.2 gated residual normalization fast path."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
import sglang.multimodal_gen.runtime.models.dits.flux_2 as flux2
|
||||
from sglang.kernels.ops.diffusion import residual_gate_add
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
|
||||
def _is_blackwell() -> bool:
|
||||
return torch.cuda.is_available() and torch.cuda.get_device_capability()[0] >= 10
|
||||
|
||||
|
||||
@unittest.skipUnless(_is_blackwell(), "FLUX.2 gated residual fusion requires SM100+")
|
||||
class TestFlux2GatedResnorm(CustomTestCase):
|
||||
def test_gated_residual_norm_modulate_is_bit_exact(self):
|
||||
torch.manual_seed(0)
|
||||
hidden = 6144
|
||||
shape = (1, 64, hidden)
|
||||
residual = torch.randn(shape, device="cuda", dtype=torch.bfloat16)
|
||||
update = torch.randn_like(residual)
|
||||
params = torch.randn(1, 1, 3 * hidden, device="cuda").bfloat16()
|
||||
gate, scale, shift = params.chunk(3, dim=-1)
|
||||
norm = nn.LayerNorm(hidden, elementwise_affine=False, eps=1e-6, device="cuda")
|
||||
|
||||
expected_residual = residual_gate_add(residual, update, gate)
|
||||
expected = norm(expected_residual) * (1 + scale) + shift
|
||||
actual, actual_residual = flux2._flux2_gated_resnorm(
|
||||
norm, residual, update, gate, scale, shift
|
||||
)
|
||||
|
||||
self.assertIsInstance(
|
||||
flux2._defer_gated_residual(residual, update, gate), tuple
|
||||
)
|
||||
self.assertTrue(torch.equal(actual_residual, expected_residual))
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
|
||||
def test_gated_residual_defer_rejects_unsupported_inputs(self):
|
||||
torch.manual_seed(0)
|
||||
for batch, dtype in ((1, torch.float16), (2, torch.bfloat16)):
|
||||
residual = torch.randn(batch, 17, 6144, device="cuda", dtype=dtype)
|
||||
update = torch.randn_like(residual)
|
||||
gate = torch.randn(batch, 1, 6144, device="cuda", dtype=dtype)
|
||||
expected = residual_gate_add(residual, update, gate)
|
||||
actual = flux2._defer_gated_residual(residual, update, gate)
|
||||
|
||||
self.assertIsInstance(actual, torch.Tensor)
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
|
||||
residual = torch.randn(1, 17, 6144, device="cuda", dtype=torch.bfloat16)
|
||||
update = torch.randn_like(residual)
|
||||
gate = torch.randn(1, 1, 6144, device="cuda", dtype=torch.bfloat16)
|
||||
with patch("torch.compiler.is_compiling", return_value=True):
|
||||
actual = flux2._defer_gated_residual(residual, update, gate)
|
||||
|
||||
self.assertIsInstance(actual, torch.Tensor)
|
||||
self.assertTrue(torch.equal(actual, residual_gate_add(residual, update, gate)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,131 @@
|
||||
"""LongCat normalization parity and graph-safe fusion dispatch."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
from diffusers.models.normalization import AdaLayerNormZero, AdaLayerNormZeroSingle
|
||||
|
||||
import sglang.multimodal_gen.runtime.models.dits.longcat_image as longcat
|
||||
from sglang.kernels.ops.diffusion import BitExactFusionGate, modulate_scale_shift
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=45, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestLongCatNormModulation(CustomTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.original_gate = longcat._LONGCAT_LN_MOD
|
||||
longcat._LONGCAT_LN_MOD = BitExactFusionGate("test", per_signature=True)
|
||||
torch.manual_seed(42)
|
||||
|
||||
def tearDown(self):
|
||||
longcat._LONGCAT_LN_MOD = self.original_gate
|
||||
super().tearDown()
|
||||
|
||||
def require_cuda(self):
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("CUDA required")
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_adaln_checkpoint_and_output_parity(self):
|
||||
for device, dtype, dim, seq in [
|
||||
("cpu", torch.float32, 64, 17),
|
||||
("cuda", torch.bfloat16, 3072, 512),
|
||||
("cuda", torch.bfloat16, 3072, 4608),
|
||||
]:
|
||||
if device == "cuda" and not torch.cuda.is_available():
|
||||
continue
|
||||
for reference_cls, candidate_cls in [
|
||||
(AdaLayerNormZero, longcat._LongCatAdaLayerNormZero),
|
||||
(AdaLayerNormZeroSingle, longcat._LongCatAdaLayerNormZeroSingle),
|
||||
]:
|
||||
with self.subTest(device=device, seq=seq, cls=reference_cls.__name__):
|
||||
reference = reference_cls(dim).to(device=device, dtype=dtype)
|
||||
candidate = candidate_cls(dim).to(device=device, dtype=dtype)
|
||||
candidate.load_state_dict(reference.state_dict(), strict=True)
|
||||
x = torch.randn(1, seq, dim, device=device, dtype=dtype)
|
||||
emb = torch.randn(1, dim, device=device, dtype=dtype)
|
||||
expected, actual = reference(x, emb=emb), candidate(x, emb=emb)
|
||||
for a, b in zip(expected, actual, strict=True):
|
||||
self.assertTrue(torch.equal(a, b))
|
||||
if device == "cuda":
|
||||
self.assertTrue(longcat._LONGCAT_LN_MOD.verified)
|
||||
self.assertFalse(longcat._LONGCAT_LN_MOD.disabled)
|
||||
|
||||
def inputs(self, seq=4096):
|
||||
self.require_cuda()
|
||||
x = torch.randn(1, seq, 3072, device="cuda", dtype=torch.bfloat16)
|
||||
modulation = torch.randn(1, 6 * 3072, device="cuda", dtype=torch.bfloat16)
|
||||
shift, scale, *_ = modulation.chunk(6, dim=-1)
|
||||
norm = torch.nn.LayerNorm(3072, elementwise_affine=False, eps=1e-6).cuda()
|
||||
return norm, x, scale, shift
|
||||
|
||||
def test_grad_enabled_uses_differentiable_reference(self):
|
||||
norm, x, scale, shift = self.inputs(seq=17)
|
||||
with torch.inference_mode():
|
||||
longcat._longcat_norm_modulate(norm, x, scale, shift)
|
||||
self.assertTrue(longcat._LONGCAT_LN_MOD.verified)
|
||||
leaves = [t.detach().clone().requires_grad_() for t in (x, scale, shift)]
|
||||
refs = [t.detach().clone().requires_grad_() for t in leaves]
|
||||
with patch.object(longcat.diffusion_ops, "fused_layernorm_modulate") as fused:
|
||||
actual = longcat._longcat_norm_modulate(norm, *leaves)
|
||||
actual.float().sum().backward()
|
||||
fused.assert_not_called()
|
||||
expected = norm(refs[0]) * (1 + refs[1][:, None]) + refs[2][:, None]
|
||||
expected.float().sum().backward()
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
for a, b in zip(leaves, refs, strict=True):
|
||||
self.assertIsNotNone(a.grad)
|
||||
self.assertTrue(torch.equal(a.grad, b.grad))
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_changed_inputs_are_used_by_graph_replay(self):
|
||||
norm, x, scale, shift = self.inputs()
|
||||
longcat._longcat_norm_modulate(norm, x, scale, shift)
|
||||
self.assertTrue(longcat._LONGCAT_LN_MOD.verified)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
actual = longcat._longcat_norm_modulate(norm, x, scale, shift)
|
||||
x.add_(0.25)
|
||||
scale.neg_()
|
||||
shift.mul_(0.5)
|
||||
graph.replay()
|
||||
expected = norm(x) * (1 + scale[:, None]) + shift[:, None]
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_unverified_capture_uses_eager_reference(self):
|
||||
norm, x, scale, shift = self.inputs(seq=17)
|
||||
expected = modulate_scale_shift(norm(x), scale, shift)
|
||||
with patch.object(longcat.diffusion_ops, "fused_layernorm_modulate") as fused:
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
actual = longcat._longcat_norm_modulate(norm, x, scale, shift)
|
||||
graph.replay()
|
||||
fused.assert_not_called()
|
||||
self.assertFalse(longcat._LONGCAT_LN_MOD.verified)
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_mismatch_disables_fusion_and_returns_reference(self):
|
||||
norm, x, scale, shift = self.inputs(seq=17)
|
||||
expected = norm(x) * (1 + scale[:, None]) + shift[:, None]
|
||||
with patch.object(
|
||||
longcat.diffusion_ops,
|
||||
"fused_layernorm_modulate",
|
||||
return_value=torch.zeros_like(x),
|
||||
):
|
||||
actual = longcat._longcat_norm_modulate(norm, x, scale, shift)
|
||||
self.assertTrue(longcat._LONGCAT_LN_MOD.disabled)
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
with patch.object(longcat.diffusion_ops, "fused_layernorm_modulate") as fused:
|
||||
actual = longcat._longcat_norm_modulate(norm, x, scale, shift)
|
||||
fused.assert_not_called()
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,78 @@
|
||||
"""MXFP8 producers against ``flashinfer.mxfp8_quantize`` of the bf16 tensor the
|
||||
unfused kernel stores: payload and the swizzled E8M0 scale buffer, padding
|
||||
included, byte for byte."""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.diffusion import (
|
||||
can_use_mxfp8_swizzled,
|
||||
can_use_silu_mul_mxfp8,
|
||||
indexed_scale_shift_bf16_,
|
||||
indexed_scale_shift_mxfp8_,
|
||||
mxfp8_quantize_swizzled,
|
||||
silu_mul_mxfp8,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=20, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
ROWS, HIDDEN = 333, 5376 # rows and scale columns both need padding
|
||||
|
||||
|
||||
def _assert_matches_flashinfer(
|
||||
got: tuple[torch.Tensor, torch.Tensor], bf16: torch.Tensor
|
||||
) -> None:
|
||||
import flashinfer
|
||||
|
||||
q, s = flashinfer.mxfp8_quantize(bf16.contiguous(), True)
|
||||
assert torch.equal(got[0].view(torch.uint8), q.view(torch.uint8))
|
||||
assert torch.equal(got[1].view(torch.uint8), s.view(torch.uint8))
|
||||
|
||||
|
||||
def test_quantize_swizzled_is_byte_exact() -> None:
|
||||
g = torch.Generator(device="cuda").manual_seed(1)
|
||||
x = (torch.randn((ROWS, HIDDEN), device="cuda", generator=g) * 3).to(torch.bfloat16)
|
||||
x[0, :32] = 0 # an all-zero block takes the minimum exponent
|
||||
assert can_use_mxfp8_swizzled(x)
|
||||
_assert_matches_flashinfer(mxfp8_quantize_swizzled(x), x)
|
||||
|
||||
|
||||
def test_silu_mul_mxfp8_is_byte_exact() -> None:
|
||||
g = torch.Generator(device="cuda").manual_seed(2)
|
||||
x = (torch.randn((ROWS, 2 * HIDDEN), device="cuda", generator=g) * 2).to(
|
||||
torch.bfloat16
|
||||
)
|
||||
assert can_use_silu_mul_mxfp8(x)
|
||||
ref = torch.nn.functional.silu(x[:, :HIDDEN]) * x[:, HIDDEN:]
|
||||
_assert_matches_flashinfer(silu_mul_mxfp8(x), ref)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("keep_bf16", [True, False])
|
||||
def test_indexed_scale_shift_mxfp8_is_byte_exact(keep_bf16: bool) -> None:
|
||||
g = torch.Generator(device="cuda").manual_seed(3)
|
||||
x = torch.randn((ROWS, HIDDEN), device="cuda", generator=g).to(torch.bfloat16)
|
||||
shift = torch.randn((3, HIDDEN), device="cuda", generator=g).to(torch.bfloat16)
|
||||
scale = torch.randn((3, HIDDEN), device="cuda", generator=g).to(torch.bfloat16)
|
||||
indices = torch.randint(0, 3, (ROWS,), device="cuda", generator=g)
|
||||
ref = indexed_scale_shift_bf16_(x.clone(), shift, scale, indices)
|
||||
kept, q, s = indexed_scale_shift_mxfp8_(
|
||||
x, shift, scale, indices, keep_bf16=keep_bf16
|
||||
)
|
||||
_assert_matches_flashinfer((q, s), ref)
|
||||
assert (kept is x and torch.equal(x, ref)) if keep_bf16 else kept is None
|
||||
|
||||
|
||||
def test_predicates_reject_unsupported_input() -> None:
|
||||
assert not can_use_mxfp8_swizzled(
|
||||
torch.randn(4, 64, device="cuda", dtype=torch.float16)
|
||||
)
|
||||
assert not can_use_silu_mul_mxfp8(
|
||||
torch.randn(4, 96, device="cuda", dtype=torch.bfloat16)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -0,0 +1,48 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.diffusion import (
|
||||
can_use_fused_inplace_qknorm_rope,
|
||||
fused_inplace_qknorm_rope,
|
||||
fused_qknorm_rope_out_of_place,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=20, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
|
||||
def test_out_of_place_qknorm_rope_matches_inplace_and_keeps_inputs() -> None:
|
||||
"""The out-of-place variant (strided fused-qkv views in, contiguous copies
|
||||
out) is bit-equal to the in-place kernel and leaves its inputs untouched;
|
||||
VDN-H3's linear branch reads the raw q/k after it."""
|
||||
T, H, D, R = 512, 4, 128, 96
|
||||
if not can_use_fused_inplace_qknorm_rope(
|
||||
D, R, True, torch.bfloat16, torch.bfloat16, True
|
||||
):
|
||||
pytest.skip("fused qknorm+rope JIT kernel unavailable")
|
||||
g = torch.Generator(device="cpu").manual_seed(0)
|
||||
qkv = torch.randn(T, 3 * H * D, generator=g).to("cuda", torch.bfloat16)
|
||||
q = qkv[:, : H * D].view(T, H, D)
|
||||
k = qkv[:, H * D : 2 * H * D].view(T, H, D)
|
||||
qw = (torch.rand(D, generator=g) + 0.5).to("cuda", torch.bfloat16)
|
||||
kw = (torch.rand(D, generator=g) + 0.5).to("cuda", torch.bfloat16)
|
||||
freqs = torch.randn(T, R // 2, generator=g).to("cuda")
|
||||
cache = torch.cat((freqs.cos(), freqs.sin()), -1).to(torch.bfloat16).contiguous()
|
||||
pos = torch.arange(T, device="cuda")
|
||||
kwargs = dict(
|
||||
is_neox=True, eps=1e-5, head_dim=D, rope_dim=R, round_norm_before_rope=True
|
||||
)
|
||||
q_ref, k_ref = q.clone(), k.clone()
|
||||
fused_inplace_qknorm_rope(q_ref, k_ref, qw, kw, cache, pos, **kwargs)
|
||||
q_out = torch.empty(T, H, D, device="cuda", dtype=torch.bfloat16)
|
||||
k_out = torch.empty_like(q_out)
|
||||
before = qkv.clone()
|
||||
fused_qknorm_rope_out_of_place(q, k, q_out, k_out, qw, kw, cache, pos, **kwargs)
|
||||
assert torch.equal(qkv, before)
|
||||
assert torch.equal(q_out, q_ref) and torch.equal(k_out, k_ref)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -0,0 +1,141 @@
|
||||
"""SANA-WM conv post-processing parity, rounding, and graph replay."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
import sglang.multimodal_gen.runtime.models.dits.sana_wm_components as wm
|
||||
from sglang.kernels.ops.diffusion import BitExactFusionGate
|
||||
from sglang.kernels.ops.diffusion.activation.sana_conv_post_triton import (
|
||||
can_use_fused_bias_glu,
|
||||
can_use_fused_bias_silu,
|
||||
fused_bias_glu,
|
||||
fused_bias_silu,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=45, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestSanaWMConvPost(CustomTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("CUDA required")
|
||||
self.original_gate = wm._SANA_WM_CONV_POST
|
||||
wm._SANA_WM_CONV_POST = BitExactFusionGate("test", per_signature=True)
|
||||
self.addCleanup(setattr, wm, "_SANA_WM_CONV_POST", self.original_gate)
|
||||
torch.manual_seed(42)
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_post_kernels_both_layouts_and_bias_modes(self):
|
||||
for shape in [(3, 34, 17, 23), (2, 13440, 22, 40), (2, 34, 1, 1)]:
|
||||
for layout in [torch.contiguous_format, torch.channels_last]:
|
||||
x = torch.randn(shape, device="cuda", dtype=torch.bfloat16).to(
|
||||
memory_format=layout
|
||||
)
|
||||
bias = torch.randn(shape[1], device="cuda", dtype=x.dtype)
|
||||
biased = x + bias[None, :, None, None]
|
||||
self.assertTrue(torch.equal(fused_bias_silu(x, bias), F.silu(biased)))
|
||||
for b, z in [(bias, biased), (None, x)]:
|
||||
a, g = z.chunk(2, dim=1)
|
||||
actual = fused_bias_glu(x, b)
|
||||
self.assertTrue(torch.equal(actual, a * F.silu(g)))
|
||||
self.assertTrue(actual.is_contiguous(memory_format=layout))
|
||||
# Use a real spatial slice; width-one tensors remain contiguous.
|
||||
sliced = torch.empty(2, 34, 7, 10, device="cuda", dtype=x.dtype)[:, :, :, ::2]
|
||||
self.assertFalse(can_use_fused_bias_silu(sliced, bias))
|
||||
self.assertFalse(can_use_fused_bias_glu(sliced, None))
|
||||
self.assertFalse(can_use_fused_bias_glu(x.float(), None))
|
||||
|
||||
@staticmethod
|
||||
def reference(module, x):
|
||||
z = module.depth_conv(F.silu(module.inverted_conv.conv(x)))
|
||||
a, g = z.chunk(2, dim=1)
|
||||
return a * F.silu(g)
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_native_convolution_and_depthwise_bias_rounding(self):
|
||||
for channels, hidden, shape in [
|
||||
(32, 96, (3, 32, 17, 23)),
|
||||
(2240, 6720, (14, 2240, 22, 40)),
|
||||
]:
|
||||
module = wm.GLUMBConvTemp(channels, hidden).cuda().bfloat16()
|
||||
for seed in (0, 1):
|
||||
torch.manual_seed(seed)
|
||||
x = torch.randn(shape, device="cuda", dtype=torch.bfloat16)
|
||||
self.assertTrue(
|
||||
torch.equal(module._spatial_glu(x), self.reference(module, x))
|
||||
)
|
||||
self.assertTrue(wm._SANA_WM_CONV_POST.verified)
|
||||
self.assertFalse(wm._SANA_WM_CONV_POST.disabled)
|
||||
# Guard the numerical distinction which forbids bias extraction.
|
||||
z = module.inverted_conv(x)
|
||||
conv = module.depth_conv.conv
|
||||
split = F.conv2d(z, conv.weight, None, padding=1, groups=conv.groups)
|
||||
split = split + conv.bias[None, :, None, None]
|
||||
self.assertFalse(torch.equal(conv(z), split))
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_graph_replay_updates_inputs_and_streaming_tail(self):
|
||||
module = wm.GLUMBConvTemp(32, 96).cuda().bfloat16()
|
||||
# Exercise a nonzero temporal convolution instead of its zero init.
|
||||
module.t_conv.weight.normal_(std=0.01)
|
||||
x = torch.randn(2, 3 * 7 * 11, 32, device="cuda", dtype=torch.bfloat16)
|
||||
tail = torch.randn(2, 32, 1, 77, device="cuda", dtype=x.dtype)
|
||||
module(x, (3, 7, 11), ffn_tail=tail, save_ffn_tail=True)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
actual, actual_tail = module(
|
||||
x, (3, 7, 11), ffn_tail=tail, save_ffn_tail=True
|
||||
)
|
||||
x.add_(0.25)
|
||||
tail.mul_(0.5)
|
||||
graph.replay()
|
||||
with patch.object(wm._SANA_WM_CONV_POST, "disabled", True):
|
||||
expected, expected_tail = module(
|
||||
x, (3, 7, 11), ffn_tail=tail, save_ffn_tail=True
|
||||
)
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
self.assertTrue(torch.equal(actual_tail, expected_tail))
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_unverified_capture_and_mismatch_use_reference(self):
|
||||
module = wm.GLUMBConvTemp(32, 96).cuda().bfloat16()
|
||||
x = torch.randn(3, 32, 17, 23, device="cuda", dtype=torch.bfloat16)
|
||||
expected = self.reference(module, x)
|
||||
with patch.object(wm.diffusion_ops, "fused_bias_silu") as fused:
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
actual = module._spatial_glu(x)
|
||||
graph.replay()
|
||||
fused.assert_not_called()
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
with patch.object(
|
||||
wm.diffusion_ops, "fused_bias_glu", return_value=torch.zeros_like(expected)
|
||||
):
|
||||
actual = module._spatial_glu(x)
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
self.assertTrue(wm._SANA_WM_CONV_POST.disabled)
|
||||
with patch.object(wm.diffusion_ops, "fused_bias_silu") as fused:
|
||||
actual = module._spatial_glu(x)
|
||||
fused.assert_not_called()
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
|
||||
def test_grad_enabled_uses_differentiable_reference(self):
|
||||
module = wm.GLUMBConvTemp(32, 96).cuda().bfloat16()
|
||||
x = torch.randn(
|
||||
2, 32, 7, 11, device="cuda", dtype=torch.bfloat16, requires_grad=True
|
||||
)
|
||||
with patch.object(wm.diffusion_ops, "fused_bias_silu") as fused:
|
||||
actual = module._spatial_glu(x)
|
||||
actual.float().sum().backward()
|
||||
fused.assert_not_called()
|
||||
self.assertIsNotNone(x.grad)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Exact streaming GDN output/state parity without materialized reversed video."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
|
||||
import sglang.multimodal_gen.runtime.models.dits.sana_wm_components as wm
|
||||
from sglang.kernels.ops.diffusion import BitExactFusionGate
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=45, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestSanaWMReverseScan(CustomTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("CUDA required")
|
||||
original = wm._SANA_WM_GDN_REVERSE
|
||||
wm._SANA_WM_GDN_REVERSE = BitExactFusionGate("test", per_signature=True)
|
||||
self.addCleanup(setattr, wm, "_SANA_WM_GDN_REVERSE", original)
|
||||
torch.manual_seed(42)
|
||||
|
||||
def inputs(self, frames, transposed=False, frame_beta=False):
|
||||
b, h, d, s = 1, 20, 112, 880
|
||||
shape = (b, h, frames * s, d) if transposed else (b, h, d, frames * s)
|
||||
xs = [torch.randn(shape, device="cuda") * 0.03 for _ in range(5)]
|
||||
if transposed:
|
||||
xs = [x.transpose(-1, -2) for x in xs]
|
||||
beta_shape = (b, h, frames) if frame_beta else (b, h, frames, s)
|
||||
return (
|
||||
*xs,
|
||||
torch.rand(beta_shape, device="cuda") * 0.02,
|
||||
torch.rand(b, h, frames, device="cuda") * 0.5 + 0.4,
|
||||
)
|
||||
|
||||
def assert_nested_equal(self, a, b):
|
||||
if isinstance(a, tuple):
|
||||
for x, y in zip(a, b, strict=True):
|
||||
self.assert_nested_equal(x, y)
|
||||
else:
|
||||
self.assertTrue(torch.equal(a, b))
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_multiple_chunks_outputs_and_carried_states(self):
|
||||
for transposed in (False, True):
|
||||
for frame_beta in (False, True):
|
||||
reference_state = candidate_state = (None, None)
|
||||
reference_cam = candidate_cam = None
|
||||
for frames in (1, 4, 3, 2):
|
||||
q, k, v, qr, kr, beta, decay = self.inputs(
|
||||
frames, transposed, frame_beta
|
||||
)
|
||||
with patch.object(wm._SANA_WM_GDN_REVERSE, "disabled", True):
|
||||
a, reference_state = wm._gdn_scan_cached(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
qr,
|
||||
kr,
|
||||
beta,
|
||||
decay,
|
||||
init_state_kv=reference_state[0],
|
||||
init_state_z=reference_state[1],
|
||||
)
|
||||
c, reference_cam = wm._single_path_delta_scan_cached(
|
||||
qr, kr, v, beta, decay, init_state_kv=reference_cam
|
||||
)
|
||||
b, candidate_state = wm._gdn_scan_cached(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
qr,
|
||||
kr,
|
||||
beta,
|
||||
decay,
|
||||
init_state_kv=candidate_state[0],
|
||||
init_state_z=candidate_state[1],
|
||||
)
|
||||
d, candidate_cam = wm._single_path_delta_scan_cached(
|
||||
qr, kr, v, beta, decay, init_state_kv=candidate_cam
|
||||
)
|
||||
self.assert_nested_equal(
|
||||
(a, reference_state, c, reference_cam),
|
||||
(b, candidate_state, d, candidate_cam),
|
||||
)
|
||||
self.assertFalse(wm._SANA_WM_GDN_REVERSE.disabled)
|
||||
self.assertTrue(wm._SANA_WM_GDN_REVERSE.verified)
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_changed_graph_inputs_and_initial_states(self):
|
||||
q, k, v, qr, kr, beta, decay = self.inputs(3, True)
|
||||
state = torch.randn(1, 20, 112, 112, device="cuda") * 0.001
|
||||
z = torch.randn(1, 20, 112, 1, device="cuda") * 0.001
|
||||
|
||||
def run():
|
||||
return (
|
||||
wm._gdn_scan_cached(
|
||||
q, k, v, qr, kr, beta, decay, init_state_kv=state, init_state_z=z
|
||||
),
|
||||
wm._single_path_delta_scan_cached(
|
||||
qr, kr, v, beta, decay, init_state_kv=state
|
||||
),
|
||||
)
|
||||
|
||||
run()
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
actual = run()
|
||||
q.add_(0.01)
|
||||
qr.mul_(0.7)
|
||||
beta.mul_(0.8)
|
||||
state.add_(0.001)
|
||||
z.neg_()
|
||||
graph.replay()
|
||||
with patch.object(wm._SANA_WM_GDN_REVERSE, "disabled", True):
|
||||
expected = run()
|
||||
self.assert_nested_equal(actual, expected)
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_unverified_capture_and_mismatch_fallback(self):
|
||||
_, _, v, qr, kr, beta, decay = self.inputs(2)
|
||||
|
||||
def reference():
|
||||
return wm._single_path_delta_scan_backward_reference(qr, kr, v, beta, decay)
|
||||
|
||||
expected = reference()
|
||||
with patch.object(wm, "_sana_wm_reverse_scan_impl") as fast:
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
actual = wm._sana_wm_reverse_scan(
|
||||
qr, kr, v, beta, decay, reference=reference
|
||||
)
|
||||
graph.replay()
|
||||
fast.assert_not_called()
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
with patch.object(
|
||||
wm, "_sana_wm_reverse_scan_impl", return_value=torch.ones_like(expected)
|
||||
):
|
||||
actual = wm._sana_wm_reverse_scan(
|
||||
qr, kr, v, beta, decay, reference=reference
|
||||
)
|
||||
self.assertTrue(wm._SANA_WM_GDN_REVERSE.disabled)
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_synthetic_zero_update_keeps_nonfinite_query_behavior(self):
|
||||
q, k, v, qr, kr, beta, decay = self.inputs(1)
|
||||
qr[..., 0] = float("nan")
|
||||
q[..., 1] = float("inf")
|
||||
expected = wm._gdn_scan_backward_reference(q, k, v, qr, kr, beta, decay, 1e-6)
|
||||
actual = wm._sana_wm_reverse_scan_impl(qr, kr, v, beta, decay, q, k)
|
||||
for a, b in zip(actual, expected, strict=True):
|
||||
torch.testing.assert_close(a, b, rtol=0, atol=0, equal_nan=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Fused VDN-H3 delta-rule factors against the eager Cholesky chain: both fp32 paths are
|
||||
held to the same cond(I + A)-dominated error band vs fp64, on model-shaped inputs."""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.diffusion import can_use_vdn_delta_factors, vdn_delta_factors
|
||||
from sglang.multimodal_gen.runtime.models.dits import minimax_h3_vdn as vdn
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=60, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
HEAD_DIM = 128
|
||||
|
||||
|
||||
def _inputs(
|
||||
frames: int, heads: int, tokens: int, beta_scale: float = 1.0, seed: int = 0
|
||||
):
|
||||
g = torch.Generator(device="cuda").manual_seed(seed)
|
||||
k = torch.nn.functional.normalize(
|
||||
torch.nn.functional.silu(
|
||||
torch.randn(frames, heads, tokens, HEAD_DIM, device="cuda", generator=g)
|
||||
),
|
||||
dim=-1,
|
||||
)
|
||||
v = torch.nn.functional.silu(
|
||||
torch.randn(frames, heads, tokens, HEAD_DIM, device="cuda", generator=g)
|
||||
)
|
||||
beta = (
|
||||
torch.sigmoid(torch.randn(frames, heads, tokens, device="cuda", generator=g))
|
||||
* beta_scale
|
||||
)
|
||||
alpha = torch.rand(frames, heads, HEAD_DIM, device="cuda", generator=g) * 0.9 + 0.1
|
||||
alpha[0] = 1.0
|
||||
A = (k * beta.unsqueeze(-1)).transpose(-1, -2) @ k
|
||||
A = 0.5 * (A + A.transpose(-1, -2))
|
||||
B = (v * beta.unsqueeze(-1)).transpose(-1, -2) @ k
|
||||
return A.float().contiguous(), B.float().contiguous(), alpha.float().contiguous()
|
||||
|
||||
|
||||
def _rel(x: torch.Tensor, ref: torch.Tensor) -> float:
|
||||
return ((x.double() - ref).norm() / ref.norm()).item()
|
||||
|
||||
|
||||
def _fp64(A, B, alpha):
|
||||
inv = torch.linalg.inv(
|
||||
torch.eye(HEAD_DIM, device=A.device, dtype=torch.float64) + A.double()
|
||||
)
|
||||
return alpha.double().unsqueeze(-1) * inv, B.double() @ inv
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frames,heads,tokens", [(1, 1, 16), (5, 3, 64), (11, 7, 1008)])
|
||||
@pytest.mark.parametrize("beta_scale", [1.0, 50.0])
|
||||
def test_matches_eager_and_fp64(frames, heads, tokens, beta_scale):
|
||||
A, B, alpha = _inputs(frames, heads, tokens, beta_scale)
|
||||
assert can_use_vdn_delta_factors(A, B, alpha)
|
||||
t_ref, j_ref = _fp64(A, B, alpha)
|
||||
t_eager, j_eager = vdn.delta_factor_apply(
|
||||
"vdn_solve", alpha, A, B, tokens_per_frame=tokens
|
||||
)
|
||||
t_fused, j_fused = vdn_delta_factors(A, B, alpha)
|
||||
assert t_fused.shape == A.shape and j_fused.shape == B.shape
|
||||
assert t_fused.dtype is torch.float32 and j_fused.dtype is torch.float32
|
||||
assert torch.isfinite(t_fused).all() and torch.isfinite(j_fused).all()
|
||||
# same accuracy class as the Cholesky chain (both fp32, cond-dominated)
|
||||
assert _rel(t_fused, t_ref) <= 1.5 * _rel(t_eager, t_ref) + 1e-7
|
||||
assert _rel(j_fused, j_ref) <= 1.5 * _rel(j_eager, j_ref) + 1e-7
|
||||
assert _rel(t_fused, t_ref) < 1e-5 and _rel(j_fused, j_ref) < 3e-5
|
||||
# elementwise the two fp32 paths differ by a few 1e-5 on ill-conditioned inputs (cancellation)
|
||||
torch.testing.assert_close(t_fused, t_eager, rtol=1e-4, atol=1e-5)
|
||||
torch.testing.assert_close(j_fused, j_eager, rtol=1e-4, atol=1e-4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rule", ["vdn_solve", "vdn_scaled"])
|
||||
def test_delta_factor_apply_fused_path(rule):
|
||||
A, B, alpha = _inputs(4, 2, 48)
|
||||
eager = vdn.delta_factor_apply(rule, alpha, A, B, tokens_per_frame=48, fused=False)
|
||||
fused = vdn.delta_factor_apply(rule, alpha, A, B, tokens_per_frame=48, fused=True)
|
||||
for x, y in zip(fused, eager):
|
||||
torch.testing.assert_close(x, y, rtol=2e-5, atol=2e-5)
|
||||
|
||||
|
||||
def test_sana_scaled_ignores_fused():
|
||||
A, B, alpha = _inputs(2, 2, 32)
|
||||
eager = vdn.delta_factor_apply(
|
||||
"sana_scaled", alpha, A, B, tokens_per_frame=32, fused=False
|
||||
)
|
||||
fused = vdn.delta_factor_apply(
|
||||
"sana_scaled", alpha, A, B, tokens_per_frame=32, fused=True
|
||||
)
|
||||
for x, y in zip(fused, eager):
|
||||
assert torch.equal(x, y)
|
||||
|
||||
|
||||
def _storage_offset_copy(t: torch.Tensor) -> torch.Tensor:
|
||||
# contiguous, but one element past a 16-byte boundary
|
||||
flat = torch.empty(t.numel() + 1, dtype=t.dtype, device=t.device)
|
||||
out = flat[1:].view(t.shape)
|
||||
out.copy_(t)
|
||||
assert out.is_contiguous() and out.data_ptr() % 16 != 0
|
||||
return out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("which", ["A", "B", "alpha"])
|
||||
def test_storage_offset_input_matches_aligned(which):
|
||||
"""A contiguous input with a storage offset must not fault in the float4 loads."""
|
||||
A, B, alpha = _inputs(3, 2, 64)
|
||||
ref = vdn_delta_factors(A, B, alpha)
|
||||
inputs = {"A": A, "B": B, "alpha": alpha}
|
||||
inputs[which] = _storage_offset_copy(inputs[which])
|
||||
assert can_use_vdn_delta_factors(inputs["A"], inputs["B"], inputs["alpha"])
|
||||
out = vdn_delta_factors(inputs["A"], inputs["B"], inputs["alpha"])
|
||||
for got, want in zip(out, ref):
|
||||
assert torch.equal(got, want)
|
||||
|
||||
|
||||
def test_can_use_rejects_unsupported():
|
||||
A, B, alpha = _inputs(2, 2, 32)
|
||||
assert can_use_vdn_delta_factors(A, B, alpha)
|
||||
assert not can_use_vdn_delta_factors(
|
||||
A[..., :64, :64].contiguous(),
|
||||
B[..., :64, :64].contiguous(),
|
||||
alpha[..., :64].contiguous(),
|
||||
)
|
||||
assert not can_use_vdn_delta_factors(A.bfloat16(), B, alpha)
|
||||
assert not can_use_vdn_delta_factors(A.transpose(-1, -2), B, alpha)
|
||||
assert not can_use_vdn_delta_factors(A, B, alpha[..., :1].expand_as(alpha))
|
||||
assert not can_use_vdn_delta_factors(A.cpu(), B.cpu(), alpha.cpu())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,139 @@
|
||||
"""VDN-H3 linear-branch kernels against the eager chains they replace: the data
|
||||
movers bit-exact, the three activation kernels within one bf16 ulp."""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.diffusion import (
|
||||
can_use_vdn_frame_stats_prep,
|
||||
can_use_vdn_gather_linear_state,
|
||||
can_use_vdn_linear_epilogue,
|
||||
can_use_vdn_silu_l2norm,
|
||||
can_use_vdn_temporal_conv_act,
|
||||
vdn_frame_stats_prep,
|
||||
vdn_linear_epilogue,
|
||||
vdn_silu_l2norm,
|
||||
vdn_temporal_conv_act,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.models.dits.minimax_h3_vdn import (
|
||||
VDNHybridAttentionArchConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.dits import minimax_h3_vdn as vdn
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
FRAMES, TOKENS, HEADS, HEAD_DIM = 6, 24, 3, 32
|
||||
|
||||
|
||||
def _ulp_close(got: torch.Tensor, ref: torch.Tensor) -> bool:
|
||||
scale = max(1.0, ref.float().abs().max().item())
|
||||
return (got.float() - ref.float()).abs().max().item() <= 2e-2 * scale
|
||||
|
||||
|
||||
def test_temporal_conv_act_matches_eager_chain() -> None:
|
||||
g = torch.Generator(device="cpu").manual_seed(4)
|
||||
x = torch.randn(FRAMES, TOKENS, HEADS * HEAD_DIM, generator=g).to(
|
||||
"cuda", torch.bfloat16
|
||||
)
|
||||
w = (torch.randn(HEADS * HEAD_DIM, 5, generator=g) * 0.4).to("cuda", torch.bfloat16)
|
||||
assert can_use_vdn_temporal_conv_act(x, HEADS, HEAD_DIM)
|
||||
ref = vdn._activate(vdn._temporal_shift(x, w).reshape(-1, HEADS, HEAD_DIM), True)
|
||||
assert _ulp_close(vdn_temporal_conv_act(x, w, HEADS, HEAD_DIM, True), ref)
|
||||
frame_major = vdn_temporal_conv_act(x, w, HEADS, HEAD_DIM, True, frame_major=True)
|
||||
assert (
|
||||
frame_major.shape == (FRAMES, HEADS, TOKENS, HEAD_DIM)
|
||||
and frame_major.is_contiguous()
|
||||
)
|
||||
assert torch.equal(
|
||||
frame_major, ref.view(FRAMES, TOKENS, HEADS, HEAD_DIM).permute(0, 2, 1, 3)
|
||||
) or _ulp_close(
|
||||
frame_major, ref.view(FRAMES, TOKENS, HEADS, HEAD_DIM).permute(0, 2, 1, 3)
|
||||
)
|
||||
|
||||
|
||||
def test_silu_l2norm_reads_strided_qkv_views() -> None:
|
||||
g = torch.Generator(device="cpu").manual_seed(4)
|
||||
tokens = torch.randn(FRAMES * TOKENS, 3 * HEADS * HEAD_DIM, generator=g).to(
|
||||
"cuda", torch.bfloat16
|
||||
)
|
||||
strided = tokens[:, : HEADS * HEAD_DIM].view(FRAMES * TOKENS, HEADS, HEAD_DIM)
|
||||
assert can_use_vdn_silu_l2norm(strided)
|
||||
got = vdn_silu_l2norm(strided, True)
|
||||
assert got.is_contiguous() and _ulp_close(got, vdn._activate(strided, True))
|
||||
got_v = vdn_silu_l2norm(strided, False)
|
||||
assert _ulp_close(got_v, torch.nn.functional.silu(strided))
|
||||
frame_major = vdn_silu_l2norm(strided, True, per_frame=TOKENS)
|
||||
assert frame_major.shape == (FRAMES, HEADS, TOKENS, HEAD_DIM)
|
||||
assert torch.equal(
|
||||
got.view(FRAMES, TOKENS, HEADS, HEAD_DIM).permute(0, 2, 1, 3), frame_major
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
vdn_silu_l2norm(strided, True, per_frame=TOKENS + 1)
|
||||
|
||||
|
||||
def test_frame_stats_prep_is_bit_exact() -> None:
|
||||
g = torch.Generator(device="cpu").manual_seed(4)
|
||||
key = torch.randn(FRAMES * TOKENS, HEADS, HEAD_DIM, generator=g).to(
|
||||
"cuda", torch.bfloat16
|
||||
)
|
||||
value = torch.randn(FRAMES * TOKENS, HEADS, HEAD_DIM, generator=g).to(
|
||||
"cuda", torch.bfloat16
|
||||
)
|
||||
beta = torch.rand(FRAMES * TOKENS, HEADS, generator=g).to("cuda", torch.bfloat16)
|
||||
assert can_use_vdn_frame_stats_prep(key, value)
|
||||
k16, k32, kb32, vb = vdn_frame_stats_prep(key, value, beta, FRAMES, TOKENS)
|
||||
kf = key.view(FRAMES, TOKENS, HEADS, HEAD_DIM).permute(0, 2, 1, 3)
|
||||
vf = value.view(FRAMES, TOKENS, HEADS, HEAD_DIM).permute(0, 2, 1, 3)
|
||||
bf = beta.view(FRAMES, TOKENS, HEADS).permute(0, 2, 1)
|
||||
assert torch.equal(k16, kf.contiguous())
|
||||
assert torch.equal(k32, kf.float().contiguous())
|
||||
assert torch.equal(kb32, (kf.float() * bf.unsqueeze(-1).float()).contiguous())
|
||||
assert torch.equal(vb, (vf * bf.unsqueeze(-1).to(vf.dtype)).contiguous())
|
||||
|
||||
|
||||
def test_linear_epilogue_matches_eager_chain() -> None:
|
||||
g = torch.Generator(device="cpu").manual_seed(4)
|
||||
readout = torch.randn(FRAMES, HEADS, TOKENS, HEAD_DIM, generator=g).to(
|
||||
"cuda", torch.bfloat16
|
||||
)
|
||||
weight = (1 + 0.1 * torch.randn(HEAD_DIM, generator=g)).to("cuda", torch.bfloat16)
|
||||
gate = torch.rand(FRAMES * TOKENS, HEADS, HEAD_DIM, generator=g).to(
|
||||
"cuda", torch.bfloat16
|
||||
)
|
||||
assert can_use_vdn_linear_epilogue(readout)
|
||||
got = vdn_linear_epilogue(readout, weight, gate, 1e-6)
|
||||
assert _ulp_close(got, vdn.linear_epilogue(readout, weight, gate, 1e-6))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bridge", ["alpha", "none"])
|
||||
@pytest.mark.parametrize("with_text_state", [False, True])
|
||||
def test_gather_linear_state_matches_eager(bridge: str, with_text_state: bool) -> None:
|
||||
g = torch.Generator(device="cpu").manual_seed(5)
|
||||
frames, heads, dim = 9, 2, 32
|
||||
hybrid = VDNHybridAttentionArchConfig(chunk=3, radius=1, anchor_frames="none")
|
||||
bounds = hybrid.window_bounds(frames)
|
||||
prefix = torch.randn(frames, heads, dim, dim, generator=g).cuda()
|
||||
suffix = torch.randn(frames, heads, dim, dim, generator=g).cuda()
|
||||
alpha = (torch.rand(frames, heads, dim, generator=g) * 0.5 + 0.5).cuda()
|
||||
text = torch.randn(heads, dim, dim, generator=g).cuda() if with_text_state else None
|
||||
assert can_use_vdn_gather_linear_state(prefix)
|
||||
kwargs = dict(bridge=bridge, text_state=text, out_dtype=torch.float32)
|
||||
ref = vdn.gather_linear_state(prefix, suffix, alpha, bounds, fused=False, **kwargs)
|
||||
got = vdn.gather_linear_state(prefix, suffix, alpha, bounds, **kwargs)
|
||||
torch.testing.assert_close(got, ref, atol=1e-5, rtol=1e-5)
|
||||
|
||||
|
||||
def test_predicates_reject_unsupported_inputs() -> None:
|
||||
fp16 = torch.randn(4, 2, 32, device="cuda", dtype=torch.float16)
|
||||
assert not can_use_vdn_silu_l2norm(fp16)
|
||||
odd = torch.randn(4, 2, 48, device="cuda", dtype=torch.bfloat16)
|
||||
assert not can_use_vdn_silu_l2norm(odd)
|
||||
with pytest.raises(ValueError):
|
||||
vdn_silu_l2norm(odd, True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -0,0 +1,345 @@
|
||||
"""Tests for KV checksum integration in PD disaggregation."""
|
||||
|
||||
import unittest
|
||||
import zlib
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.disaggregation.checksum import (
|
||||
KvChecksumComputer,
|
||||
is_health_check_req,
|
||||
)
|
||||
from sglang.srt.disaggregation.decode import SchedulerDisaggregationDecodeMixin
|
||||
from sglang.srt.disaggregation.prefill import SchedulerDisaggregationPrefillMixin
|
||||
from sglang.srt.disaggregation.utils import MetadataBuffers
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=15, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
def _ref_strided_adler32(tensors, indices, strides) -> int:
|
||||
parts = []
|
||||
for tensor, idx, stride in zip(tensors, indices, strides):
|
||||
raw = tensor.cpu().contiguous().flatten().view(torch.uint8)
|
||||
for i in idx.cpu().tolist():
|
||||
parts.append(raw[i * stride : (i + 1) * stride].numpy().tobytes())
|
||||
return zlib.adler32(b"".join(parts))
|
||||
|
||||
|
||||
def _make_buf(size=4, *, kv_checksum_enabled=True, output_dsa_topk_indices_dim=0):
|
||||
return MetadataBuffers(
|
||||
size=size,
|
||||
hidden_size=16,
|
||||
hidden_states_dtype=torch.float32,
|
||||
max_sampling_mask_tokens=128,
|
||||
output_dsa_topk_indices_dim=output_dsa_topk_indices_dim,
|
||||
kv_checksum_enabled=kv_checksum_enabled,
|
||||
)
|
||||
|
||||
|
||||
class TestMetadataBuffers(unittest.TestCase):
|
||||
def test_extends_aux_with_kv_checksum(self):
|
||||
for sampling_mask in (False, True):
|
||||
for seed_dim in (0, 32):
|
||||
with (
|
||||
self.subTest(sampling_mask=sampling_mask, seed_dim=seed_dim),
|
||||
envs.SGLANG_ENABLE_DISAGG_SAMPLING_MASK.override(sampling_mask),
|
||||
):
|
||||
buf = _make_buf(size=8, output_dsa_topk_indices_dim=seed_dim)
|
||||
disabled = _make_buf(
|
||||
size=8,
|
||||
kv_checksum_enabled=False,
|
||||
output_dsa_topk_indices_dim=seed_dim,
|
||||
)
|
||||
ptrs, data_lens, item_lens = buf.get_buf_infos()
|
||||
base_ptrs, base_data_lens, base_item_lens = disabled.get_buf_infos()
|
||||
self.assertEqual(len(ptrs), len(base_ptrs) + 1)
|
||||
self.assertEqual(data_lens[:-1], base_data_lens)
|
||||
self.assertEqual(item_lens[:-1], base_item_lens)
|
||||
self.assertEqual(ptrs[-1], buf.kv_checksum.data_ptr())
|
||||
self.assertEqual(data_lens[-1], buf.kv_checksum.nbytes)
|
||||
self.assertEqual(item_lens[-1], buf.kv_checksum[0].nbytes)
|
||||
self.assertEqual(item_lens[-1], item_lens[-2])
|
||||
self.assertEqual(len(buf.get_buf(2)), len(disabled.get_buf(2)))
|
||||
|
||||
def test_set_get_kv_checksum_roundtrip(self):
|
||||
buf = _make_buf()
|
||||
buf.set_kv_checksum(SimpleNamespace(metadata_buffer_index=1), 0xDEADBEEF)
|
||||
self.assertEqual(buf.get_kv_checksum(1), 0xDEADBEEF)
|
||||
self.assertEqual(buf.get_kv_checksum(0), 0)
|
||||
|
||||
|
||||
class TestKvChecksumComputerConfig(unittest.TestCase):
|
||||
def test_flattens_nested_state_descriptor_components(self):
|
||||
computer = KvChecksumComputer(
|
||||
torch.device("cpu"),
|
||||
kv_data_ptrs=[11, 22],
|
||||
kv_item_lens=[33, 44],
|
||||
state_data_ptrs=[[55, 66], [77]],
|
||||
state_item_lens=[[88, 99], [111]],
|
||||
)
|
||||
|
||||
self.assertEqual(computer._state_data_ptrs, [55, 66, 77])
|
||||
self.assertEqual(computer._state_item_lens, [88, 99, 111])
|
||||
|
||||
|
||||
class TestKvChecksumHealthCheck(unittest.TestCase):
|
||||
def test_detects_health_check_request(self):
|
||||
self.assertTrue(is_health_check_req(SimpleNamespace(rid="HEALTH_CHECK_1")))
|
||||
self.assertFalse(is_health_check_req(SimpleNamespace(rid="user_req")))
|
||||
self.assertFalse(is_health_check_req(SimpleNamespace(rid=None)))
|
||||
|
||||
|
||||
def _make_kv(num_layers, num_pages, page_elems, dtype=torch.float16):
|
||||
return [
|
||||
torch.randn(num_pages, page_elems, dtype=dtype, device="cuda:0")
|
||||
for _ in range(2 * num_layers)
|
||||
]
|
||||
|
||||
|
||||
def _make_computer(kv, item_len, state=None, state_item_lens=None):
|
||||
return KvChecksumComputer(
|
||||
torch.device("cuda:0"),
|
||||
kv_data_ptrs=[t.data_ptr() for t in kv],
|
||||
kv_item_lens=[item_len] * len(kv),
|
||||
state_data_ptrs=[t.data_ptr() for t in (state or [])],
|
||||
state_item_lens=state_item_lens or [],
|
||||
)
|
||||
|
||||
|
||||
class TestKvChecksumComputer(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("CUDA not available")
|
||||
self.device = torch.device("cuda:0")
|
||||
|
||||
def test_kv_only_matches_reference(self):
|
||||
kv = _make_kv(num_layers=4, num_pages=32, page_elems=128, dtype=torch.bfloat16)
|
||||
idx = torch.tensor([3, 7, 8, 15, 31], dtype=torch.int64, device=self.device)
|
||||
item_len = 128 * 2
|
||||
value = _make_computer(kv, item_len).compute(idx)
|
||||
expected = _ref_strided_adler32(kv, [idx] * len(kv), [item_len] * len(kv))
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
def test_kv_corruption_detected(self):
|
||||
kv = _make_kv(num_layers=2, num_pages=16, page_elems=64)
|
||||
idx = torch.tensor([5], dtype=torch.int64, device=self.device)
|
||||
computer = _make_computer(kv, 64 * 2)
|
||||
v1 = computer.compute(idx)
|
||||
kv[0][5, 0] += 1
|
||||
self.assertNotEqual(v1, computer.compute(idx))
|
||||
|
||||
def test_kv_plus_state_matches_reference(self):
|
||||
kv = _make_kv(num_layers=2, num_pages=8, page_elems=32)
|
||||
state = [
|
||||
torch.randn(4, 16, dtype=torch.float16, device=self.device)
|
||||
for _ in range(2)
|
||||
]
|
||||
kv_idx = torch.tensor([0, 1, 2], dtype=torch.int64, device=self.device)
|
||||
state_idx = torch.tensor([1], dtype=torch.int64, device=self.device)
|
||||
kv_len, state_lens = 32 * 2, [16 * 2, 16 * 2]
|
||||
computer = _make_computer(kv, kv_len, state, state_lens)
|
||||
value = computer.compute(kv_idx, state_idx)
|
||||
expected = _ref_strided_adler32(
|
||||
kv + state,
|
||||
[kv_idx] * len(kv) + [state_idx] * len(state),
|
||||
[kv_len] * len(kv) + state_lens,
|
||||
)
|
||||
self.assertEqual(value, expected)
|
||||
state[0][1, 0] += 1
|
||||
self.assertNotEqual(value, computer.compute(kv_idx, state_idx))
|
||||
|
||||
def test_nested_state_components_match_reference(self):
|
||||
kv = _make_kv(num_layers=1, num_pages=8, page_elems=32)
|
||||
state_components = [
|
||||
[torch.randn(4, 16, dtype=torch.float16, device=self.device)],
|
||||
[
|
||||
torch.randn(4, 8, dtype=torch.float16, device=self.device),
|
||||
torch.randn(4, 12, dtype=torch.float16, device=self.device),
|
||||
],
|
||||
]
|
||||
kv_idx = torch.tensor([0, 3, 7], dtype=torch.int64, device=self.device)
|
||||
state_idx = torch.tensor([1, 2], dtype=torch.int64, device=self.device)
|
||||
state_tensors = [tensor for comp in state_components for tensor in comp]
|
||||
kv_len = 32 * 2
|
||||
state_lens = [[16 * 2], [8 * 2, 12 * 2]]
|
||||
computer = KvChecksumComputer(
|
||||
self.device,
|
||||
kv_data_ptrs=[t.data_ptr() for t in kv],
|
||||
kv_item_lens=[kv_len] * len(kv),
|
||||
state_data_ptrs=[
|
||||
[tensor.data_ptr() for tensor in comp] for comp in state_components
|
||||
],
|
||||
state_item_lens=state_lens,
|
||||
)
|
||||
|
||||
value = computer.compute(kv_idx, state_idx)
|
||||
expected = _ref_strided_adler32(
|
||||
kv + state_tensors,
|
||||
[kv_idx] * len(kv) + [state_idx] * len(state_tensors),
|
||||
[kv_len] * len(kv) + [item for comp in state_lens for item in comp],
|
||||
)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
|
||||
class _FakeScheduler(SchedulerDisaggregationDecodeMixin):
|
||||
def __init__(self, computer, req_to_token):
|
||||
self.kv_checksum_computer = computer
|
||||
self.waiting_queue = []
|
||||
self.token_to_kv_pool_allocator = SimpleNamespace(
|
||||
page_size=1, get_kvcache=lambda: SimpleNamespace()
|
||||
)
|
||||
self.req_to_token_pool = SimpleNamespace(req_to_token=req_to_token)
|
||||
self.tree_cache = None
|
||||
self.output_streamer = SimpleNamespace(stream_output=self.stream_output)
|
||||
self.metrics_reporter = SimpleNamespace(enable_metrics=True)
|
||||
self.metrics_collector = Mock()
|
||||
self.streamed_aborts = []
|
||||
|
||||
def stream_output(self, reqs, return_logprob):
|
||||
self.streamed_aborts.extend(reqs)
|
||||
|
||||
|
||||
class _FakePrefillScheduler(SchedulerDisaggregationPrefillMixin):
|
||||
def __init__(self):
|
||||
self.kv_checksum_computer = object()
|
||||
self.disagg_metadata_buffers = SimpleNamespace(set_kv_checksum=Mock())
|
||||
|
||||
|
||||
def _make_req(expected_chksum, num_input_tokens, rid="r0"):
|
||||
return SimpleNamespace(
|
||||
rid=rid,
|
||||
bootstrap_room=12345,
|
||||
kv=SimpleNamespace(req_pool_idx=0),
|
||||
origin_input_ids=list(range(num_input_tokens)),
|
||||
fill_ids=list(range(num_input_tokens)),
|
||||
expected_kv_checksum=expected_chksum,
|
||||
return_logprob=False,
|
||||
)
|
||||
|
||||
|
||||
class TestPrefillHealthCheckChecksum(unittest.TestCase):
|
||||
def test_health_check_clears_metadata_checksum(self):
|
||||
sched = _FakePrefillScheduler()
|
||||
req = _make_req(0xDEADBEEF, 1, rid="HEALTH_CHECK_1")
|
||||
with patch.object(
|
||||
SchedulerDisaggregationPrefillMixin,
|
||||
"_send_kv_chunk",
|
||||
lambda *args, **kwargs: None,
|
||||
):
|
||||
sched.send_kv_chunk(req, last_chunk=True)
|
||||
sched.disagg_metadata_buffers.set_kv_checksum.assert_called_once_with(req, 0)
|
||||
|
||||
|
||||
class TestGetNewPrebuiltBatchChecksum(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("CUDA not available")
|
||||
self.device = torch.device("cuda:0")
|
||||
self.num_pages = 8
|
||||
self.kv = _make_kv(num_layers=2, num_pages=self.num_pages, page_elems=32)
|
||||
self.item_len = 32 * 2
|
||||
self.req_to_token = torch.arange(
|
||||
self.num_pages, dtype=torch.int64, device=self.device
|
||||
).view(1, self.num_pages)
|
||||
self.true_chksum = _ref_strided_adler32(
|
||||
self.kv,
|
||||
[torch.arange(self.num_pages, dtype=torch.int64, device=self.device)]
|
||||
* len(self.kv),
|
||||
[self.item_len] * len(self.kv),
|
||||
)
|
||||
|
||||
def _make_sched(self, computer=None):
|
||||
if computer is _SENTINEL:
|
||||
computer = _make_computer(self.kv, self.item_len)
|
||||
return _FakeScheduler(computer, self.req_to_token)
|
||||
|
||||
def _run_once(self, sched, batch_ret=None):
|
||||
running_batch = SimpleNamespace()
|
||||
with patch.object(
|
||||
SchedulerDisaggregationDecodeMixin,
|
||||
"_get_new_prebuilt_batch",
|
||||
lambda s, rb: batch_ret,
|
||||
):
|
||||
return sched.get_new_prebuilt_batch(running_batch)
|
||||
|
||||
def test_match_keeps_req(self):
|
||||
sched = self._make_sched(_SENTINEL)
|
||||
sched.waiting_queue = [_make_req(self.true_chksum, self.num_pages)]
|
||||
self._run_once(sched)
|
||||
self.assertEqual(len(sched.waiting_queue), 1)
|
||||
self.assertEqual(sched.streamed_aborts, [])
|
||||
|
||||
def test_mismatch_aborts(self):
|
||||
sched = self._make_sched(_SENTINEL)
|
||||
req = _make_req(0xDEADBEEF, self.num_pages)
|
||||
sched.waiting_queue = [req]
|
||||
with (
|
||||
envs.SGLANG_IS_IN_CI.override(False),
|
||||
patch("sglang.srt.disaggregation.decode.prepare_abort") as mock_abort,
|
||||
patch("sglang.srt.disaggregation.decode.release_kv_cache") as mock_release,
|
||||
):
|
||||
self._run_once(sched)
|
||||
self._run_once(sched)
|
||||
self.assertEqual(sched.waiting_queue, [])
|
||||
self.assertEqual(sched.streamed_aborts, [req])
|
||||
mock_abort.assert_called_once()
|
||||
mock_release.assert_called_once()
|
||||
sched.metrics_collector.increment_transfer_failed_reqs.assert_called_once_with()
|
||||
|
||||
def test_mismatch_raises_in_ci(self):
|
||||
sched = self._make_sched(_SENTINEL)
|
||||
sched.waiting_queue = [_make_req(0xDEADBEEF, self.num_pages)]
|
||||
with (
|
||||
envs.SGLANG_IS_IN_CI.override(True),
|
||||
patch("sglang.srt.disaggregation.decode.prepare_abort") as mock_abort,
|
||||
self.assertRaisesRegex(RuntimeError, "KV checksum mismatch"),
|
||||
):
|
||||
self._run_once(sched)
|
||||
mock_abort.assert_not_called()
|
||||
sched.metrics_collector.increment_transfer_failed_reqs.assert_not_called()
|
||||
|
||||
def test_health_check_skips_checksum(self):
|
||||
sched = self._make_sched(_SENTINEL)
|
||||
req = _make_req(0xDEADBEEF, self.num_pages, rid="HEALTH_CHECK_1")
|
||||
sched.waiting_queue = [req]
|
||||
with (
|
||||
patch("sglang.srt.disaggregation.decode.prepare_abort") as mock_abort,
|
||||
patch("sglang.srt.disaggregation.decode.release_kv_cache") as mock_release,
|
||||
):
|
||||
self._run_once(sched)
|
||||
self.assertEqual(sched.waiting_queue, [req])
|
||||
self.assertEqual(sched.streamed_aborts, [])
|
||||
mock_abort.assert_not_called()
|
||||
mock_release.assert_not_called()
|
||||
|
||||
def test_retract_re_verifies(self):
|
||||
sched = self._make_sched(_SENTINEL)
|
||||
req = _make_req(self.true_chksum, self.num_pages)
|
||||
sched.waiting_queue = [req]
|
||||
self._run_once(sched)
|
||||
self._run_once(sched)
|
||||
self.assertEqual(sched.waiting_queue, [req])
|
||||
self.assertEqual(sched.streamed_aborts, [])
|
||||
|
||||
def test_disabled_delegates_to_batch_builder(self):
|
||||
sched = self._make_sched(computer=None)
|
||||
sched.waiting_queue = [_make_req(0xABCD, 4)]
|
||||
sentinel = object()
|
||||
self.assertIs(self._run_once(sched, batch_ret=sentinel), sentinel)
|
||||
|
||||
def test_zero_expected_skips_checksum(self):
|
||||
sched = self._make_sched(_SENTINEL)
|
||||
sched.waiting_queue = [_make_req(0, self.num_pages)]
|
||||
self._run_once(sched)
|
||||
self.assertEqual(len(sched.waiting_queue), 1)
|
||||
|
||||
|
||||
_SENTINEL = object()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,141 @@
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.elementwise.fast_topk import fast_topk
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
def _check_topk_values(score, lengths, indices, topk, row_starts):
|
||||
"""fast_topk leaves order and tie-breaking unspecified,
|
||||
so compare the sorted top-k values rather than index sets."""
|
||||
for b in range(score.shape[0]):
|
||||
start = int(row_starts[b]) if row_starts is not None else 0
|
||||
length = int(lengths[b])
|
||||
section = score[b, start : start + length]
|
||||
row = indices[b]
|
||||
if length <= topk:
|
||||
# naive path: identity indices, then -1 fill
|
||||
assert torch.equal(
|
||||
row[:length].cpu(), torch.arange(length, dtype=torch.int32)
|
||||
)
|
||||
assert (row[length:] == -1).all()
|
||||
continue
|
||||
assert (row >= 0).all(), "long rows must fill every slot"
|
||||
picked = section[row.long()]
|
||||
expected = torch.topk(section, topk).values
|
||||
assert torch.equal(
|
||||
picked.sort(descending=True).values, expected.sort(descending=True).values
|
||||
), f"row {b}: top-{topk} value multiset mismatch"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topk", [512, 2048])
|
||||
@pytest.mark.parametrize(
|
||||
"batch,length",
|
||||
[
|
||||
(1, 4096),
|
||||
(7, 3000),
|
||||
(33, 32768),
|
||||
(128, 2050),
|
||||
],
|
||||
)
|
||||
def test_fast_topk_long_rows(topk, batch, length):
|
||||
torch.manual_seed(0)
|
||||
score = torch.randn(batch, length, dtype=torch.float32, device="cuda")
|
||||
lengths = torch.full((batch,), length, dtype=torch.int32, device="cuda")
|
||||
|
||||
indices = fast_topk(score, lengths, topk)
|
||||
_check_topk_values(score, lengths, indices, topk, None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topk", [512, 2048])
|
||||
def test_fast_topk_short_and_mixed_rows(topk):
|
||||
torch.manual_seed(0)
|
||||
max_len = topk + 128
|
||||
batch = 8
|
||||
score = torch.randn(batch, max_len, dtype=torch.float32, device="cuda")
|
||||
# rows shorter than k (naive path), exactly k, and longer than k
|
||||
lens = [1, topk // 3, topk - 1, topk, topk + 1, topk + 7, 17, max_len]
|
||||
lengths = torch.tensor(lens[:batch], dtype=torch.int32, device="cuda")
|
||||
|
||||
indices = fast_topk(score, lengths, topk)
|
||||
_check_topk_values(score, lengths, indices, topk, None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topk", [512, 2048])
|
||||
def test_fast_topk_ragged_with_row_starts(topk):
|
||||
torch.manual_seed(0)
|
||||
batch, width = 16, 8192
|
||||
score = torch.randn(batch, width, dtype=torch.float32, device="cuda")
|
||||
row_starts = torch.randint(0, 2048, (batch,), dtype=torch.int32, device="cuda")
|
||||
lengths = torch.randint(1, 2048, (batch,), dtype=torch.int32, device="cuda")
|
||||
lengths = torch.minimum(lengths, width - row_starts).to(torch.int32)
|
||||
# ensure some rows are longer than k
|
||||
lengths[0] = min(width - int(row_starts[0]), topk + 100)
|
||||
|
||||
indices = fast_topk(score, lengths, topk, row_starts=row_starts)
|
||||
_check_topk_values(score, lengths, indices, topk, row_starts)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topk", [512, 2048])
|
||||
def test_fast_topk_row_stride(topk):
|
||||
torch.manual_seed(0)
|
||||
batch, length = 8, 4096
|
||||
base = torch.randn(batch, 2 * length, dtype=torch.float32, device="cuda")
|
||||
score = base[:, :length] # stride(0) == 2*length, stride(1) == 1
|
||||
lengths = torch.full((batch,), length, dtype=torch.int32, device="cuda")
|
||||
|
||||
indices = fast_topk(score, lengths, topk)
|
||||
_check_topk_values(score, lengths, indices, topk, None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topk", [512, 2048])
|
||||
@pytest.mark.parametrize(
|
||||
"fill",
|
||||
[
|
||||
"binary", # only 0s and 1s: extreme duplication at the threshold bin
|
||||
"few_levels", # a handful of distinct levels incl. negatives
|
||||
"constant", # whole rows of one value
|
||||
],
|
||||
)
|
||||
def test_fast_topk_duplicate_heavy(topk, fill):
|
||||
torch.manual_seed(0)
|
||||
batch, length = 16, 8192
|
||||
if fill == "binary":
|
||||
score = torch.randint(0, 2, (batch, length), dtype=torch.float32, device="cuda")
|
||||
elif fill == "few_levels":
|
||||
levels = torch.tensor([-5.0, -1.0, 0.0, 0.5, 2.0], device="cuda")
|
||||
score = levels[torch.randint(0, 5, (batch, length), device="cuda")]
|
||||
else:
|
||||
score = torch.full((batch, length), 3.25, dtype=torch.float32, device="cuda")
|
||||
lengths = torch.full((batch,), length, dtype=torch.int32, device="cuda")
|
||||
|
||||
indices = fast_topk(score, lengths, topk)
|
||||
_check_topk_values(score, lengths, indices, topk, None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topk", [512, 2048])
|
||||
def test_fast_topk_negative_and_zero(topk):
|
||||
torch.manual_seed(0)
|
||||
batch, length = 8, 16384
|
||||
score = torch.randn(batch, length, dtype=torch.float32, device="cuda") * 100
|
||||
score[:, : length // 3] = 0.0 # long zero prefix
|
||||
score[:, length // 3 : length // 2] = -1e30 # very negative block
|
||||
lengths = torch.full((batch,), length, dtype=torch.int32, device="cuda")
|
||||
|
||||
indices = fast_topk(score, lengths, topk)
|
||||
_check_topk_values(score, lengths, indices, topk, None)
|
||||
|
||||
|
||||
def test_fast_topk_unsupported_k():
|
||||
score = torch.randn(2, 4096, dtype=torch.float32, device="cuda")
|
||||
lengths = torch.full((2,), 4096, dtype=torch.int32, device="cuda")
|
||||
with pytest.raises(RuntimeError, match="topk"):
|
||||
fast_topk(score, lengths, 1024)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,161 @@
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.kernels.ops.elementwise.hc_combine import hc_combine
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
HC_COUNT = 4
|
||||
HIDDEN_SIZE = 2560
|
||||
|
||||
|
||||
def _reference_hc_combine(
|
||||
block_output: torch.Tensor,
|
||||
residual: torch.Tensor,
|
||||
normed_residual: torch.Tensor,
|
||||
inject_weight: torch.Tensor,
|
||||
hc: int,
|
||||
hs: int,
|
||||
compute_dtype: torch.dtype = torch.float32,
|
||||
) -> torch.Tensor:
|
||||
"""Eager reference mirroring ``GatedResidual._combine_compute``;
|
||||
``compute_dtype=torch.float64`` is the near-exact reference."""
|
||||
R = residual.to(compute_dtype).unflatten(-1, (hc, hs))
|
||||
gates = 2 * torch.sigmoid(
|
||||
F.linear(normed_residual.to(compute_dtype), inject_weight.to(compute_dtype))
|
||||
/ hc
|
||||
)
|
||||
injection = block_output.to(compute_dtype).unsqueeze(-2) * gates.unsqueeze(-1)
|
||||
return (R + injection).flatten(-2)
|
||||
|
||||
|
||||
def _make_inputs(
|
||||
num_tokens: int, dtype: torch.dtype, hc: int = HC_COUNT, hs: int = HIDDEN_SIZE
|
||||
):
|
||||
torch.manual_seed(0)
|
||||
block_output = torch.randn(num_tokens, hs, dtype=dtype, device="cuda")
|
||||
residual = torch.randn(num_tokens, hc * hs, dtype=dtype, device="cuda")
|
||||
normed_residual = torch.randn(num_tokens, hc * hs, dtype=dtype, device="cuda")
|
||||
inject_weight = torch.randn(hc, hc * hs, dtype=dtype, device="cuda") * 0.02
|
||||
return block_output, residual, normed_residual, inject_weight
|
||||
|
||||
|
||||
# Worst case over M in {1, 7, 128, 8192} against the fp64 reference on B300 (sm103):
|
||||
# bf16 max rel err 7.8e-3 (1 ulp at a binade edge), fp16 below 1e-3 (1 ulp = 9.8e-4);
|
||||
# the residual is fp32 reordering flipping the final rounding.
|
||||
_TOLERANCES = {
|
||||
torch.bfloat16: dict(rtol=1e-2, atol=5e-3),
|
||||
torch.float16: dict(rtol=1e-3, atol=1e-3),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize("num_tokens", [1, 7, 128, 8192])
|
||||
def test_hc_combine_correctness(dtype, num_tokens):
|
||||
block_output, residual, normed_residual, inject_weight = _make_inputs(
|
||||
num_tokens, dtype
|
||||
)
|
||||
|
||||
out = hc_combine(
|
||||
block_output,
|
||||
residual,
|
||||
normed_residual,
|
||||
inject_weight,
|
||||
HC_COUNT,
|
||||
HIDDEN_SIZE,
|
||||
)
|
||||
expected = _reference_hc_combine(
|
||||
block_output,
|
||||
residual,
|
||||
normed_residual,
|
||||
inject_weight,
|
||||
HC_COUNT,
|
||||
HIDDEN_SIZE,
|
||||
compute_dtype=torch.float64,
|
||||
).to(dtype)
|
||||
|
||||
torch.testing.assert_close(out, expected, **_TOLERANCES[dtype])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
def test_hc_combine_out_param(dtype):
|
||||
block_output, residual, normed_residual, inject_weight = _make_inputs(64, dtype)
|
||||
out = torch.empty_like(residual)
|
||||
|
||||
result = hc_combine(
|
||||
block_output,
|
||||
residual,
|
||||
normed_residual,
|
||||
inject_weight,
|
||||
HC_COUNT,
|
||||
HIDDEN_SIZE,
|
||||
out=out,
|
||||
)
|
||||
expected = _reference_hc_combine(
|
||||
block_output,
|
||||
residual,
|
||||
normed_residual,
|
||||
inject_weight,
|
||||
HC_COUNT,
|
||||
HIDDEN_SIZE,
|
||||
compute_dtype=torch.float64,
|
||||
).to(dtype)
|
||||
|
||||
assert result.data_ptr() == out.data_ptr()
|
||||
torch.testing.assert_close(result, expected, **_TOLERANCES[dtype])
|
||||
|
||||
|
||||
def test_hc_combine_3d_input():
|
||||
dtype = torch.bfloat16
|
||||
block_output, residual, normed_residual, inject_weight = _make_inputs(32, dtype)
|
||||
block_output = block_output.reshape(4, 8, HIDDEN_SIZE)
|
||||
residual = residual.reshape(4, 8, HC_COUNT * HIDDEN_SIZE)
|
||||
normed_residual = normed_residual.reshape(4, 8, HC_COUNT * HIDDEN_SIZE)
|
||||
|
||||
out = hc_combine(
|
||||
block_output,
|
||||
residual,
|
||||
normed_residual,
|
||||
inject_weight,
|
||||
HC_COUNT,
|
||||
HIDDEN_SIZE,
|
||||
)
|
||||
expected = _reference_hc_combine(
|
||||
block_output,
|
||||
residual,
|
||||
normed_residual,
|
||||
inject_weight,
|
||||
HC_COUNT,
|
||||
HIDDEN_SIZE,
|
||||
compute_dtype=torch.float64,
|
||||
).to(dtype)
|
||||
|
||||
assert out.shape == residual.shape
|
||||
torch.testing.assert_close(out, expected, **_TOLERANCES[dtype])
|
||||
|
||||
|
||||
def test_hc_combine_bad_hidden_size():
|
||||
dtype = torch.bfloat16
|
||||
block_output, residual, normed_residual, inject_weight = _make_inputs(
|
||||
4,
|
||||
dtype,
|
||||
hc=4,
|
||||
hs=1000, # 4 * 1000 = 4000, not a multiple of 2048
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="2048"):
|
||||
hc_combine(
|
||||
block_output,
|
||||
residual,
|
||||
normed_residual,
|
||||
inject_weight,
|
||||
4,
|
||||
1000,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,269 @@
|
||||
import os
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from sglang.srt.layers.quantization.unquant import UnquantizedEmbeddingMethod
|
||||
from sglang.srt.layers.vocab_parallel_embedding import (
|
||||
VocabParallelEmbeddingShardIndices,
|
||||
)
|
||||
from sglang.srt.models import qwen4_exp as qwen4_exp_module
|
||||
from sglang.srt.models.qwen4_exp import (
|
||||
Qwen4ExpPinnedHostEmbedding,
|
||||
Qwen4ExpPLELayer,
|
||||
)
|
||||
from sglang.srt.utils import set_weight_attrs
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=45, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not torch.cuda.is_available(), reason="CUDA is required for this test."
|
||||
)
|
||||
|
||||
|
||||
def _make_source_embedding(
|
||||
*,
|
||||
dtype=torch.bfloat16,
|
||||
embedding_dim=7,
|
||||
vocab_start=0,
|
||||
vocab_end=8,
|
||||
org_vocab_size=8,
|
||||
tp_size=1,
|
||||
num_added_embeddings=0,
|
||||
):
|
||||
local_rows = vocab_end - vocab_start
|
||||
weight = nn.Parameter(
|
||||
torch.empty((local_rows, embedding_dim), dtype=dtype, device="cuda"),
|
||||
requires_grad=False,
|
||||
)
|
||||
set_weight_attrs(
|
||||
weight,
|
||||
{
|
||||
"input_dim": 1,
|
||||
"output_dim": 0,
|
||||
"weight_loader": lambda *_args, **_kwargs: None,
|
||||
},
|
||||
)
|
||||
shard_indices = VocabParallelEmbeddingShardIndices(
|
||||
padded_org_vocab_start_index=vocab_start,
|
||||
padded_org_vocab_end_index=vocab_end,
|
||||
padded_added_vocab_start_index=org_vocab_size,
|
||||
padded_added_vocab_end_index=org_vocab_size,
|
||||
org_vocab_start_index=vocab_start,
|
||||
org_vocab_end_index=vocab_end,
|
||||
added_vocab_start_index=org_vocab_size,
|
||||
added_vocab_end_index=org_vocab_size,
|
||||
)
|
||||
return SimpleNamespace(
|
||||
weight=weight,
|
||||
quant_config=None,
|
||||
enable_tp=True,
|
||||
use_attn_tp_group=False,
|
||||
tp_size=tp_size,
|
||||
num_embeddings=org_vocab_size + num_added_embeddings,
|
||||
org_vocab_size=org_vocab_size,
|
||||
padding_size=1,
|
||||
num_added_embeddings=num_added_embeddings,
|
||||
use_presharded_weights=False,
|
||||
org_vocab_size_padded=org_vocab_size,
|
||||
num_embeddings_padded=org_vocab_size + num_added_embeddings,
|
||||
shard_indices=shard_indices,
|
||||
embedding_dim=embedding_dim,
|
||||
weight_scale=None,
|
||||
quant_method=UnquantizedEmbeddingMethod(),
|
||||
num_embeddings_per_partition=local_rows,
|
||||
num_org_embeddings_per_partition=local_rows,
|
||||
num_added_embeddings_per_partition=0,
|
||||
)
|
||||
|
||||
|
||||
def _load_rows(offloaded, rows, *, pinned=True):
|
||||
pointer = offloaded.weight.data_ptr()
|
||||
offloaded.weight_loader(offloaded.weight, rows)
|
||||
assert offloaded.weight.data_ptr() == pointer
|
||||
assert offloaded.weight.is_pinned() == pinned
|
||||
assert offloaded.weight.weight_loader.__self__ is offloaded
|
||||
assert offloaded.quant_method is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("input_dtype", [torch.int32, torch.int64])
|
||||
@pytest.mark.parametrize("embedding_dim", [7, 64, 257])
|
||||
def test_qwen4_ple_pinned_gather_tp1(input_dtype, embedding_dim):
|
||||
source = _make_source_embedding(embedding_dim=embedding_dim)
|
||||
offloaded = Qwen4ExpPinnedHostEmbedding(source)
|
||||
rows = torch.arange(8 * embedding_dim, dtype=torch.bfloat16, device="cuda").reshape(
|
||||
8, embedding_dim
|
||||
)
|
||||
_load_rows(offloaded, rows)
|
||||
|
||||
ids = torch.tensor([[0, 7, 3], [4, 1, 6]], dtype=input_dtype, device="cuda")
|
||||
expected = rows.index_select(0, ids.long().flatten()).reshape(
|
||||
*ids.shape, embedding_dim
|
||||
)
|
||||
actual = offloaded(ids)
|
||||
|
||||
assert actual.shape == expected.shape
|
||||
assert actual.is_contiguous()
|
||||
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
|
||||
|
||||
|
||||
def test_qwen4_ple_pinned_gather_shard_boundaries_and_out_buffer():
|
||||
embedding_dim = 13
|
||||
source = _make_source_embedding(
|
||||
embedding_dim=embedding_dim,
|
||||
vocab_start=4,
|
||||
vocab_end=8,
|
||||
org_vocab_size=8,
|
||||
tp_size=2,
|
||||
)
|
||||
offloaded = Qwen4ExpPinnedHostEmbedding(source)
|
||||
rows = torch.arange(8 * embedding_dim, dtype=torch.bfloat16, device="cuda").reshape(
|
||||
8, embedding_dim
|
||||
)
|
||||
_load_rows(offloaded, rows)
|
||||
|
||||
ids = torch.tensor([[-1, 3, 4], [7, 8, 100]], device="cuda")
|
||||
output = torch.full(
|
||||
(*ids.shape, embedding_dim),
|
||||
torch.nan,
|
||||
dtype=torch.bfloat16,
|
||||
device="cuda",
|
||||
)
|
||||
actual = offloaded.gather(ids, out=output)
|
||||
expected = torch.zeros_like(output)
|
||||
expected[0, 2] = rows[4]
|
||||
expected[1, 0] = rows[7]
|
||||
|
||||
assert actual.data_ptr() == output.data_ptr()
|
||||
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
|
||||
|
||||
|
||||
def test_qwen4_ple_pinned_gather_empty_input():
|
||||
offloaded = Qwen4ExpPinnedHostEmbedding(_make_source_embedding())
|
||||
_load_rows(offloaded, torch.zeros((8, 7), dtype=torch.bfloat16, device="cuda"))
|
||||
ids = torch.empty((0, 3), dtype=torch.int64, device="cuda")
|
||||
actual = offloaded.gather(ids)
|
||||
assert actual.shape == (0, 3, 7)
|
||||
assert actual.numel() == 0
|
||||
|
||||
|
||||
def test_qwen4_ple_pinned_embedding_rejects_unsupported_weights():
|
||||
with pytest.raises(TypeError, match="requires bfloat16"):
|
||||
Qwen4ExpPinnedHostEmbedding(_make_source_embedding(dtype=torch.float16))
|
||||
with pytest.raises(NotImplementedError, match="added vocabulary"):
|
||||
Qwen4ExpPinnedHostEmbedding(_make_source_embedding(num_added_embeddings=1))
|
||||
|
||||
|
||||
def test_qwen4_ple_prefetch_buffer_lifecycle(monkeypatch):
|
||||
layer = Qwen4ExpPLELayer.__new__(Qwen4ExpPLELayer)
|
||||
nn.Module.__init__(layer)
|
||||
layer.ple_embed_dim = 7
|
||||
layer.ple_embedding = SimpleNamespace(
|
||||
ngram_embedding=Qwen4ExpPinnedHostEmbedding(
|
||||
_make_source_embedding(embedding_dim=layer.ple_embed_dim)
|
||||
)
|
||||
)
|
||||
layer._graph_prefetch_buffers = {}
|
||||
layer._eager_prefetch_buffer = None
|
||||
lookup_ids = torch.empty((0,), dtype=torch.int64, device="cuda")
|
||||
|
||||
monkeypatch.setattr(qwen4_exp_module, "get_is_capture_mode", lambda: False)
|
||||
eager_large = layer._get_prefetch_buffer(8, lookup_ids)
|
||||
eager_small = layer._get_prefetch_buffer(3, lookup_ids)
|
||||
assert eager_small.data_ptr() == eager_large.data_ptr()
|
||||
assert layer._eager_prefetch_buffer.shape == (8, layer.ple_embed_dim)
|
||||
|
||||
eager_grown = layer._get_prefetch_buffer(12, lookup_ids)
|
||||
eager_grown_small = layer._get_prefetch_buffer(4, lookup_ids)
|
||||
assert eager_grown_small.data_ptr() == eager_grown.data_ptr()
|
||||
assert layer._eager_prefetch_buffer.shape == (12, layer.ple_embed_dim)
|
||||
|
||||
monkeypatch.setattr(qwen4_exp_module, "get_is_capture_mode", lambda: True)
|
||||
graph_three = layer._get_prefetch_buffer(3, lookup_ids)
|
||||
graph_five = layer._get_prefetch_buffer(5, lookup_ids)
|
||||
graph_three_reused = layer._get_prefetch_buffer(3, lookup_ids)
|
||||
assert graph_three_reused.data_ptr() == graph_three.data_ptr()
|
||||
assert graph_five.data_ptr() != graph_three.data_ptr()
|
||||
assert set(layer._graph_prefetch_buffers) == {3, 5}
|
||||
|
||||
|
||||
def _file_backend_supported() -> bool:
|
||||
from sglang.srt.models.qwen4_exp_ple_table import device_uses_host_page_tables
|
||||
|
||||
return (
|
||||
torch.cuda.is_available()
|
||||
and device_uses_host_page_tables(torch.cuda.current_device()) is True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _file_backend_supported(),
|
||||
reason="the file backend needs pageable host memory reachable through host page tables",
|
||||
)
|
||||
@pytest.mark.parametrize("embedding_dim", [7, 160])
|
||||
def test_qwen4_ple_file_backend_matches_pinned(embedding_dim):
|
||||
with tempfile.TemporaryDirectory() as table_dir:
|
||||
pinned = Qwen4ExpPinnedHostEmbedding(
|
||||
_make_source_embedding(embedding_dim=embedding_dim)
|
||||
)
|
||||
filed = Qwen4ExpPinnedHostEmbedding(
|
||||
_make_source_embedding(embedding_dim=embedding_dim),
|
||||
backend="file",
|
||||
table_dir=table_dir,
|
||||
)
|
||||
assert pinned._file_prefetcher is None and filed._file_prefetcher is not None
|
||||
(name,) = os.listdir(table_dir)
|
||||
assert "rows0-8" in name # this rank's vocabulary shard
|
||||
rows = torch.arange(
|
||||
8 * embedding_dim, dtype=torch.bfloat16, device="cuda"
|
||||
).reshape(8, embedding_dim)
|
||||
_load_rows(pinned, rows)
|
||||
_load_rows(filed, rows, pinned=False)
|
||||
ids = torch.tensor([[0, 7, 3], [4, 1, 6]], dtype=torch.int64, device="cuda")
|
||||
torch.testing.assert_close(filed(ids), pinned(ids), rtol=0, atol=0)
|
||||
# A prefill-sized gather goes through the page-cache hint path.
|
||||
big = torch.randint(0, 8, (4096,), device="cuda")
|
||||
torch.testing.assert_close(
|
||||
filed(big), rows.index_select(0, big), rtol=0, atol=0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _file_backend_supported(),
|
||||
reason="the file backend needs pageable host memory reachable through host page tables",
|
||||
)
|
||||
def test_qwen4_ple_file_backend_fp8_table():
|
||||
embedding_dim = 160
|
||||
with tempfile.TemporaryDirectory() as table_dir:
|
||||
filed = Qwen4ExpPinnedHostEmbedding(
|
||||
_make_source_embedding(
|
||||
embedding_dim=embedding_dim, dtype=torch.float8_e4m3fn
|
||||
),
|
||||
backend="file",
|
||||
table_dir=table_dir,
|
||||
)
|
||||
assert filed.weight.dtype == torch.float8_e4m3fn
|
||||
rows = (
|
||||
torch.arange(8 * embedding_dim, dtype=torch.float32, device="cuda").reshape(
|
||||
8, embedding_dim
|
||||
)
|
||||
/ 64
|
||||
).to(torch.float8_e4m3fn)
|
||||
_load_rows(filed, rows, pinned=False)
|
||||
ids = torch.tensor([[0, 7, 3]], dtype=torch.int64, device="cuda")
|
||||
expected = (
|
||||
rows.index_select(0, ids.flatten())
|
||||
.to(torch.bfloat16)
|
||||
.reshape(1, 3, embedding_dim)
|
||||
)
|
||||
torch.testing.assert_close(filed(ids), expected, rtol=0, atol=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,84 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.srt.layers.hc_mix_triton import (
|
||||
_FUSED_MIX_MAX_ROWS,
|
||||
fused_hc_mix,
|
||||
fused_hc_mix_supported,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
HC_COUNT = 4
|
||||
HIDDEN_SIZE = 2560
|
||||
LOWRANK = 320
|
||||
|
||||
|
||||
def _reference_mix(
|
||||
hyper_input_normed: torch.Tensor,
|
||||
w_down: torch.Tensor,
|
||||
w_up: torch.Tensor,
|
||||
hc: int,
|
||||
hs: int,
|
||||
compute_dtype: torch.dtype = torch.float64,
|
||||
) -> torch.Tensor:
|
||||
"""Mirrors GatedResidual._mix_compute in hyperconnection.py."""
|
||||
x = hyper_input_normed.to(compute_dtype)
|
||||
t = F.silu(F.linear(x, w_down.to(compute_dtype)) / hc)
|
||||
u = torch.sigmoid(F.linear(t, w_up.to(compute_dtype)))
|
||||
return (u.unflatten(-1, (hc, hs)) * x.unflatten(-1, (hc, hs))).mean(dim=-2)
|
||||
|
||||
|
||||
def _make_inputs(num_tokens: int, dtype: torch.dtype):
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(num_tokens, HC_COUNT * HIDDEN_SIZE, dtype=dtype, device="cuda")
|
||||
w_down = (
|
||||
torch.randn(LOWRANK, HC_COUNT * HIDDEN_SIZE, dtype=dtype, device="cuda") * 0.02
|
||||
)
|
||||
w_up = (
|
||||
torch.randn(HC_COUNT * HIDDEN_SIZE, LOWRANK, dtype=dtype, device="cuda") * 0.02
|
||||
)
|
||||
return x, w_down, w_up
|
||||
|
||||
|
||||
_TOLERANCES = {
|
||||
torch.bfloat16: dict(rtol=1e-2, atol=5e-3),
|
||||
torch.float16: dict(rtol=2e-3, atol=1e-3),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize("num_tokens", [1, 4, 7, _FUSED_MIX_MAX_ROWS])
|
||||
def test_fused_hc_mix_matches_reference(dtype, num_tokens):
|
||||
x, w_down, w_up = _make_inputs(num_tokens, dtype)
|
||||
assert fused_hc_mix_supported(x, w_down, w_up)
|
||||
out = fused_hc_mix(x, w_down, w_up, HC_COUNT, HIDDEN_SIZE)
|
||||
ref = _reference_mix(x, w_down, w_up, HC_COUNT, HIDDEN_SIZE)
|
||||
torch.testing.assert_close(out.to(torch.float64), ref, **_TOLERANCES[dtype])
|
||||
|
||||
|
||||
def test_fused_hc_mix_no_less_accurate_than_eager():
|
||||
"""The fused kernel (fp32 accumulation throughout) must not be farther
|
||||
from the fp64 reference than the eager bf16 chain it replaces."""
|
||||
x, w_down, w_up = _make_inputs(8, torch.bfloat16)
|
||||
ref = _reference_mix(x, w_down, w_up, HC_COUNT, HIDDEN_SIZE)
|
||||
fused = fused_hc_mix(x, w_down, w_up, HC_COUNT, HIDDEN_SIZE)
|
||||
eager = _reference_mix(
|
||||
x, w_down, w_up, HC_COUNT, HIDDEN_SIZE, compute_dtype=torch.bfloat16
|
||||
)
|
||||
fused_err = (fused.to(torch.float64) - ref).abs().max()
|
||||
eager_err = (eager.to(torch.float64) - ref).abs().max()
|
||||
assert fused_err <= eager_err * 1.5 + 1e-6
|
||||
|
||||
|
||||
def test_fused_hc_mix_gate_rejects_prefill_rows():
|
||||
x, w_down, w_up = _make_inputs(_FUSED_MIX_MAX_ROWS + 1, torch.bfloat16)
|
||||
assert not fused_hc_mix_supported(x, w_down, w_up)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -10,7 +10,7 @@ semaphore window cycling.
|
||||
|
||||
Usage::
|
||||
|
||||
python test/registered/jit/kimi_k3/test_ar_fusion.py # relaunches under torchrun (8 GPUs)
|
||||
python test/registered/kernels/ops/kimi_k3/test_ar_fusion.py # relaunches under torchrun (8 GPUs)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.kvcache.hisparse import (
|
||||
HiSparseSpecState,
|
||||
copy_cache_planned_mla,
|
||||
load_cache_to_device_buffer_spec_mla,
|
||||
)
|
||||
from sglang.srt.utils import is_npu, is_xpu
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not torch.cuda.is_available() or is_npu() or is_xpu(),
|
||||
reason="HiSparse speculative swap tests require a CUDA GPU.",
|
||||
)
|
||||
|
||||
DEVICE = "cuda"
|
||||
TOKEN_SCALE = 1_000_003
|
||||
|
||||
|
||||
class _SwapState(NamedTuple):
|
||||
device_buffer_tokens: torch.Tensor
|
||||
device_buffer_locs: torch.Tensor
|
||||
host_cache_locs: torch.Tensor
|
||||
host_cache: torch.Tensor
|
||||
device_buffer: torch.Tensor
|
||||
swap_state: HiSparseSpecState
|
||||
|
||||
|
||||
def _make_cache_index(num_reqs: int, hot_buffer_size: int) -> torch.Tensor:
|
||||
hash_size = 1 << (2 * hot_buffer_size - 1).bit_length()
|
||||
cache_index = torch.full(
|
||||
(num_reqs, 2, hash_size), -1, dtype=torch.int64, device=DEVICE
|
||||
)
|
||||
tokens = torch.arange(hot_buffer_size, dtype=torch.int64, device=DEVICE)
|
||||
hash_slots = ((tokens * 2654435761) & (hash_size - 1)).to(torch.long)
|
||||
packed_entries = (tokens << 32) | tokens
|
||||
cache_index[:, 0, hash_slots] = packed_entries
|
||||
return cache_index
|
||||
|
||||
|
||||
def _make_state(
|
||||
*,
|
||||
num_reqs: int,
|
||||
hot_buffer_size: int,
|
||||
page_size: int,
|
||||
scratch_size: int,
|
||||
seq_len: int,
|
||||
item_words: int,
|
||||
metadata_occurrences: int,
|
||||
) -> _SwapState:
|
||||
buffer_size = hot_buffer_size + page_size
|
||||
device_buffer_tokens = torch.full(
|
||||
(num_reqs, buffer_size), -1, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
device_buffer_tokens[:, :hot_buffer_size] = torch.arange(
|
||||
hot_buffer_size, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
|
||||
physical_tokens_per_req = buffer_size + scratch_size
|
||||
request_bases = (
|
||||
torch.arange(num_reqs, dtype=torch.int32, device=DEVICE).view(-1, 1)
|
||||
* physical_tokens_per_req
|
||||
)
|
||||
device_buffer_locs = (
|
||||
request_bases
|
||||
+ torch.arange(buffer_size, dtype=torch.int32, device=DEVICE).view(1, -1)
|
||||
).contiguous()
|
||||
scratch_locs = (
|
||||
request_bases
|
||||
+ buffer_size
|
||||
+ torch.arange(scratch_size, dtype=torch.int32, device=DEVICE).view(1, -1)
|
||||
).contiguous()
|
||||
|
||||
host_cache_locs = torch.arange(seq_len, dtype=torch.int64, device=DEVICE)
|
||||
host_cache_locs = host_cache_locs.view(1, -1).repeat(num_reqs, 1).contiguous()
|
||||
host_cache = torch.empty((seq_len, item_words), dtype=torch.int64, pin_memory=True)
|
||||
host_cache.copy_(
|
||||
torch.arange(seq_len, dtype=torch.int64).view(-1, 1) * TOKEN_SCALE
|
||||
+ torch.arange(item_words, dtype=torch.int64).view(1, -1)
|
||||
)
|
||||
|
||||
device_buffer = torch.full(
|
||||
(num_reqs * physical_tokens_per_req, item_words),
|
||||
-1,
|
||||
dtype=torch.int64,
|
||||
device=DEVICE,
|
||||
)
|
||||
hot_locs = device_buffer_locs[:, :hot_buffer_size].to(torch.long)
|
||||
device_buffer[hot_locs] = host_cache[:hot_buffer_size].to(DEVICE)
|
||||
|
||||
scratch_state = torch.full(
|
||||
(num_reqs + 1, max(4 * num_reqs, 5 * metadata_occurrences)),
|
||||
-1,
|
||||
dtype=torch.int32,
|
||||
device=DEVICE,
|
||||
)
|
||||
scratch_state[0].zero_()
|
||||
swap_state = HiSparseSpecState(
|
||||
cache_index=_make_cache_index(num_reqs, hot_buffer_size),
|
||||
cache_policy=torch.zeros(
|
||||
(num_reqs + 1, hot_buffer_size),
|
||||
dtype=torch.int32,
|
||||
device=DEVICE,
|
||||
),
|
||||
scratch_locs=scratch_locs,
|
||||
scratch_state=scratch_state,
|
||||
)
|
||||
return _SwapState(
|
||||
device_buffer_tokens=device_buffer_tokens,
|
||||
device_buffer_locs=device_buffer_locs,
|
||||
host_cache_locs=host_cache_locs,
|
||||
host_cache=host_cache,
|
||||
device_buffer=device_buffer,
|
||||
swap_state=swap_state,
|
||||
)
|
||||
|
||||
|
||||
def _run_swap(
|
||||
*,
|
||||
top_k_tokens: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
state: _SwapState,
|
||||
out: torch.Tensor | None = None,
|
||||
req_pool_indices: torch.Tensor | None = None,
|
||||
num_real_reqs: torch.Tensor | None = None,
|
||||
miss_src: torch.Tensor | None = None,
|
||||
miss_dst: torch.Tensor | None = None,
|
||||
miss_count: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
if out is None:
|
||||
out = torch.full_like(top_k_tokens, -1)
|
||||
else:
|
||||
out.fill_(-1)
|
||||
num_reqs = top_k_tokens.size(0)
|
||||
if req_pool_indices is None:
|
||||
req_pool_indices = torch.arange(num_reqs, dtype=torch.int64, device=DEVICE)
|
||||
if num_real_reqs is None:
|
||||
num_real_reqs = torch.tensor([num_reqs], dtype=torch.int32, device=DEVICE)
|
||||
load_cache_to_device_buffer_spec_mla(
|
||||
top_k_tokens=top_k_tokens,
|
||||
device_buffer_tokens=state.device_buffer_tokens,
|
||||
host_cache_locs=state.host_cache_locs,
|
||||
device_buffer_locs=state.device_buffer_locs,
|
||||
host_cache=state.host_cache,
|
||||
device_buffer=state.device_buffer,
|
||||
top_k_device_locs=out,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
state=state.swap_state,
|
||||
num_real_reqs=num_real_reqs,
|
||||
miss_src=miss_src,
|
||||
miss_dst=miss_dst,
|
||||
miss_count=miss_count,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _assert_output_matches_tokens(
|
||||
state: _SwapState, out: torch.Tensor, tokens: torch.Tensor
|
||||
) -> None:
|
||||
actual = state.device_buffer[out.to(torch.long)]
|
||||
expected = tokens.to(torch.int64).unsqueeze(-1) * TOKEN_SCALE + torch.arange(
|
||||
state.device_buffer.size(-1), dtype=torch.int64, device=DEVICE
|
||||
)
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
|
||||
class TestHiSparseSpec(CustomTestCase):
|
||||
def test_deduplicates_repeated_misses_and_copies_full_items(self) -> None:
|
||||
hot_size, page_size = 4096, 64
|
||||
num_steps, top_k, item_words = 4, 2048, 72
|
||||
total_occurrences = num_steps * top_k
|
||||
state = _make_state(
|
||||
num_reqs=1,
|
||||
hot_buffer_size=hot_size,
|
||||
page_size=page_size,
|
||||
scratch_size=hot_size,
|
||||
seq_len=16384,
|
||||
item_words=item_words,
|
||||
metadata_occurrences=total_occurrences,
|
||||
)
|
||||
|
||||
miss_count = 196
|
||||
hits = torch.arange(top_k - miss_count, dtype=torch.int32, device=DEVICE)
|
||||
shared_misses = hot_size + torch.arange(
|
||||
miss_count, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
step = torch.cat((hits, shared_misses))
|
||||
top_k_tokens = step.view(1, 1, -1).repeat(1, num_steps, 1)
|
||||
seq_lens = torch.full((num_steps,), 16384, dtype=torch.int32, device=DEVICE)
|
||||
|
||||
out = _run_swap(top_k_tokens=top_k_tokens, seq_lens=seq_lens, state=state)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
_assert_output_matches_tokens(state, out, top_k_tokens)
|
||||
self.assertEqual(int(state.swap_state.scratch_state[0, 0].item()), miss_count)
|
||||
repeated_miss_locs = out[0, :, -miss_count:]
|
||||
self.assertTrue(torch.all(repeated_miss_locs == repeated_miss_locs[0]).item())
|
||||
|
||||
def test_copies_782_cross_step_unique_misses(self) -> None:
|
||||
hot_size, page_size = 4096, 64
|
||||
num_steps, top_k, item_words = 4, 2048, 72
|
||||
total_occurrences = num_steps * top_k
|
||||
state = _make_state(
|
||||
num_reqs=1,
|
||||
hot_buffer_size=hot_size,
|
||||
page_size=page_size,
|
||||
scratch_size=hot_size,
|
||||
seq_len=16384,
|
||||
item_words=item_words,
|
||||
metadata_occurrences=total_occurrences,
|
||||
)
|
||||
|
||||
steps = []
|
||||
next_miss = hot_size
|
||||
for step_idx, miss_count in enumerate((196, 196, 195, 195)):
|
||||
hits = torch.roll(
|
||||
torch.arange(hot_size, dtype=torch.int32, device=DEVICE),
|
||||
step_idx * 137,
|
||||
)[: top_k - miss_count]
|
||||
misses = torch.arange(
|
||||
next_miss,
|
||||
next_miss + miss_count,
|
||||
dtype=torch.int32,
|
||||
device=DEVICE,
|
||||
)
|
||||
next_miss += miss_count
|
||||
steps.append(torch.cat((hits, misses)))
|
||||
top_k_tokens = torch.stack(steps).unsqueeze(0).contiguous()
|
||||
seq_lens = torch.full((num_steps,), 16384, dtype=torch.int32, device=DEVICE)
|
||||
out = _run_swap(
|
||||
top_k_tokens=top_k_tokens,
|
||||
seq_lens=seq_lens,
|
||||
state=state,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
_assert_output_matches_tokens(state, out, top_k_tokens)
|
||||
self.assertEqual(int(state.swap_state.scratch_state[0, 0].item()), 782)
|
||||
|
||||
def test_records_union_plan_for_shared_layer_io(self) -> None:
|
||||
hot_size, page_size = 4096, 64
|
||||
num_steps, top_k, item_words = 4, 2048, 72
|
||||
total_occurrences = num_steps * top_k
|
||||
state = _make_state(
|
||||
num_reqs=1,
|
||||
hot_buffer_size=hot_size,
|
||||
page_size=page_size,
|
||||
scratch_size=hot_size,
|
||||
seq_len=16384,
|
||||
item_words=item_words,
|
||||
metadata_occurrences=total_occurrences,
|
||||
)
|
||||
|
||||
steps = []
|
||||
next_miss = hot_size
|
||||
for step_idx, step_miss_count in enumerate((196, 196, 195, 195)):
|
||||
hits = torch.roll(
|
||||
torch.arange(hot_size, dtype=torch.int32, device=DEVICE),
|
||||
step_idx * 137,
|
||||
)[: top_k - step_miss_count]
|
||||
misses = torch.arange(
|
||||
next_miss,
|
||||
next_miss + step_miss_count,
|
||||
dtype=torch.int32,
|
||||
device=DEVICE,
|
||||
)
|
||||
next_miss += step_miss_count
|
||||
steps.append(torch.cat((hits, misses)))
|
||||
top_k_tokens = torch.stack(steps).unsqueeze(0).contiguous()
|
||||
seq_lens = torch.full((num_steps,), 16384, dtype=torch.int32, device=DEVICE)
|
||||
miss_src = torch.full(
|
||||
(1, total_occurrences), -1, dtype=torch.int64, device=DEVICE
|
||||
)
|
||||
miss_dst = torch.full(
|
||||
(1, total_occurrences), -1, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
miss_count = torch.full((1,), -1, dtype=torch.int32, device=DEVICE)
|
||||
|
||||
_run_swap(
|
||||
top_k_tokens=top_k_tokens,
|
||||
seq_lens=seq_lens,
|
||||
state=state,
|
||||
miss_src=miss_src,
|
||||
miss_dst=miss_dst,
|
||||
miss_count=miss_count,
|
||||
)
|
||||
shared_layer_buffer = torch.full_like(state.device_buffer, -1)
|
||||
copy_cache_planned_mla(
|
||||
miss_src=miss_src,
|
||||
miss_dst=miss_dst,
|
||||
miss_count=miss_count,
|
||||
num_real_reqs=torch.ones(1, dtype=torch.int32, device=DEVICE),
|
||||
host_cache=state.host_cache,
|
||||
device_buffer=shared_layer_buffer,
|
||||
item_size_bytes=state.host_cache.stride(0)
|
||||
* state.host_cache.element_size(),
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
self.assertEqual(int(miss_count.item()), 782)
|
||||
count = int(miss_count.item())
|
||||
src = miss_src[0, :count].to(torch.long)
|
||||
dst = miss_dst[0, :count].to(torch.long)
|
||||
torch.testing.assert_close(
|
||||
shared_layer_buffer[dst], state.host_cache[src.cpu()].to(DEVICE)
|
||||
)
|
||||
torch.testing.assert_close(shared_layer_buffer[dst], state.device_buffer[dst])
|
||||
|
||||
def test_padded_request_clears_stale_plan_count(self) -> None:
|
||||
hot_size, page_size = 4096, 64
|
||||
num_steps, top_k = 4, 2048
|
||||
total_occurrences = num_steps * top_k
|
||||
state = _make_state(
|
||||
num_reqs=2,
|
||||
hot_buffer_size=hot_size,
|
||||
page_size=page_size,
|
||||
scratch_size=hot_size,
|
||||
seq_len=8192,
|
||||
item_words=1,
|
||||
metadata_occurrences=total_occurrences,
|
||||
)
|
||||
top_k_tokens = torch.arange(top_k, dtype=torch.int32, device=DEVICE).view(
|
||||
1, 1, -1
|
||||
)
|
||||
top_k_tokens = top_k_tokens.repeat(2, num_steps, 1).contiguous()
|
||||
seq_lens = torch.full((2 * num_steps,), 8192, dtype=torch.int32, device=DEVICE)
|
||||
miss_src = torch.full(
|
||||
(2, total_occurrences), -1, dtype=torch.int64, device=DEVICE
|
||||
)
|
||||
miss_dst = torch.full(
|
||||
(2, total_occurrences), -1, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
miss_count = torch.full((2,), 123, dtype=torch.int32, device=DEVICE)
|
||||
|
||||
_run_swap(
|
||||
top_k_tokens=top_k_tokens,
|
||||
seq_lens=seq_lens,
|
||||
state=state,
|
||||
num_real_reqs=torch.ones(1, dtype=torch.int32, device=DEVICE),
|
||||
miss_src=miss_src,
|
||||
miss_dst=miss_dst,
|
||||
miss_count=miss_count,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
self.assertEqual(int(miss_count[1].item()), 0)
|
||||
|
||||
def test_resolves_all_speculative_extra_page_slots_without_host_io(self) -> None:
|
||||
hot_size, page_size = 4096, 64
|
||||
num_steps, top_k = 4, 2048
|
||||
total_occurrences = num_steps * top_k
|
||||
seq_len = 8192
|
||||
state = _make_state(
|
||||
num_reqs=1,
|
||||
hot_buffer_size=hot_size,
|
||||
page_size=page_size,
|
||||
scratch_size=hot_size,
|
||||
seq_len=seq_len,
|
||||
item_words=72,
|
||||
metadata_occurrences=total_occurrences,
|
||||
)
|
||||
|
||||
draft_tokens = torch.arange(
|
||||
seq_len - num_steps, seq_len, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
extra_offsets = torch.tensor([0, 7, 31, 63], device=DEVICE)
|
||||
extra_locs = state.device_buffer_locs[0, hot_size + extra_offsets].to(
|
||||
torch.long
|
||||
)
|
||||
state.device_buffer_tokens[0, hot_size + extra_offsets] = draft_tokens
|
||||
state.device_buffer[extra_locs] = state.host_cache[
|
||||
draft_tokens.to(device="cpu", dtype=torch.long)
|
||||
].to(DEVICE)
|
||||
state.host_cache_locs[0, draft_tokens.to(torch.long)] = -1
|
||||
|
||||
hits = torch.arange(top_k - 1, dtype=torch.int32, device=DEVICE)
|
||||
top_k_tokens = torch.stack(
|
||||
[torch.cat((hits, draft_tokens[step : step + 1])) for step in range(4)]
|
||||
).unsqueeze(0)
|
||||
seq_lens = draft_tokens + 1
|
||||
|
||||
out = _run_swap(top_k_tokens=top_k_tokens, seq_lens=seq_lens, state=state)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
_assert_output_matches_tokens(state, out, top_k_tokens)
|
||||
torch.testing.assert_close(out[0, :, -1].to(torch.long), extra_locs)
|
||||
self.assertEqual(int(state.swap_state.scratch_state[0, 0].item()), 0)
|
||||
|
||||
def test_full_union_overflow_preserves_all_8192_outputs(self) -> None:
|
||||
hot_size, page_size = 4096, 64
|
||||
num_steps, top_k = 4, 2048
|
||||
total_occurrences = num_steps * top_k
|
||||
state = _make_state(
|
||||
num_reqs=1,
|
||||
hot_buffer_size=hot_size,
|
||||
page_size=page_size,
|
||||
scratch_size=total_occurrences - hot_size,
|
||||
seq_len=16384,
|
||||
item_words=72,
|
||||
metadata_occurrences=total_occurrences,
|
||||
)
|
||||
top_k_tokens = (
|
||||
hot_size + torch.arange(total_occurrences, dtype=torch.int32, device=DEVICE)
|
||||
).view(1, num_steps, top_k)
|
||||
seq_lens = torch.full((num_steps,), 16384, dtype=torch.int32, device=DEVICE)
|
||||
|
||||
miss_src = torch.full(
|
||||
(1, total_occurrences), -1, dtype=torch.int64, device=DEVICE
|
||||
)
|
||||
miss_dst = torch.full(
|
||||
(1, total_occurrences), -1, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
miss_count = torch.full((1,), -1, dtype=torch.int32, device=DEVICE)
|
||||
out = _run_swap(
|
||||
top_k_tokens=top_k_tokens,
|
||||
seq_lens=seq_lens,
|
||||
state=state,
|
||||
miss_src=miss_src,
|
||||
miss_dst=miss_dst,
|
||||
miss_count=miss_count,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
_assert_output_matches_tokens(state, out, top_k_tokens)
|
||||
self.assertEqual(torch.unique(out).numel(), total_occurrences)
|
||||
self.assertEqual(
|
||||
int(state.swap_state.scratch_state[0, 0].item()), total_occurrences
|
||||
)
|
||||
self.assertEqual(int(miss_count.item()), total_occurrences)
|
||||
self.assertTrue(miss_src.ge(0).all().item())
|
||||
self.assertTrue(miss_dst.ge(0).all().item())
|
||||
|
||||
def test_packed_ring_supports_glm52_native_context_length(self) -> None:
|
||||
hot_size, page_size = 4096, 64
|
||||
num_steps, top_k = 4, 2048
|
||||
total_occurrences = num_steps * top_k
|
||||
seq_len = 1_048_648
|
||||
state = _make_state(
|
||||
num_reqs=1,
|
||||
hot_buffer_size=hot_size,
|
||||
page_size=page_size,
|
||||
scratch_size=hot_size,
|
||||
seq_len=seq_len,
|
||||
item_words=1,
|
||||
metadata_occurrences=total_occurrences,
|
||||
)
|
||||
top_k_tokens = torch.arange(top_k, dtype=torch.int32, device=DEVICE).view(
|
||||
1, 1, -1
|
||||
)
|
||||
top_k_tokens = top_k_tokens.repeat(1, num_steps, 1)
|
||||
high_token = seq_len - 1
|
||||
top_k_tokens[:, :, -1] = high_token
|
||||
seq_lens = torch.full((num_steps,), seq_len, dtype=torch.int32, device=DEVICE)
|
||||
|
||||
out = _run_swap(top_k_tokens=top_k_tokens, seq_lens=seq_lens, state=state)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
_assert_output_matches_tokens(state, out, top_k_tokens)
|
||||
self.assertTrue(out.ge(0).all().item())
|
||||
|
||||
# The first call admits the high token into the packed hash. The
|
||||
# second call must resolve it as a hot hit rather than truncating the
|
||||
# packed int64 entry and repeating Host-to-GPU IO.
|
||||
out = _run_swap(top_k_tokens=top_k_tokens, seq_lens=seq_lens, state=state)
|
||||
torch.cuda.synchronize()
|
||||
_assert_output_matches_tokens(state, out, top_k_tokens)
|
||||
self.assertEqual(int(state.swap_state.scratch_state[0, 0].item()), 0)
|
||||
|
||||
def test_cuda_graph_replay_preserves_valid_locations(self) -> None:
|
||||
hot_size, page_size = 4096, 64
|
||||
num_steps, top_k = 4, 2048
|
||||
total_occurrences = num_steps * top_k
|
||||
state = _make_state(
|
||||
num_reqs=1,
|
||||
hot_buffer_size=hot_size,
|
||||
page_size=page_size,
|
||||
scratch_size=hot_size,
|
||||
seq_len=65536,
|
||||
item_words=72,
|
||||
metadata_occurrences=total_occurrences,
|
||||
)
|
||||
top_k_tokens = torch.arange(top_k, dtype=torch.int32, device=DEVICE).repeat(
|
||||
num_steps, 1
|
||||
)
|
||||
for step, miss_count in enumerate((164, 102, 61, 20)):
|
||||
top_k_tokens[step, -miss_count:] = torch.arange(
|
||||
8192 + step * top_k,
|
||||
8192 + step * top_k + miss_count,
|
||||
dtype=torch.int32,
|
||||
device=DEVICE,
|
||||
)
|
||||
top_k_tokens = top_k_tokens.unsqueeze(0).contiguous()
|
||||
seq_lens = torch.tensor(
|
||||
[65533, 65534, 65535, 65536], dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
|
||||
_run_swap(top_k_tokens=top_k_tokens, seq_lens=seq_lens, state=state)
|
||||
torch.cuda.synchronize()
|
||||
graph_out = torch.full_like(top_k_tokens, -1)
|
||||
req_pool_indices = torch.arange(1, dtype=torch.int64, device=DEVICE)
|
||||
num_real_reqs = torch.tensor([1], dtype=torch.int32, device=DEVICE)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
_run_swap(
|
||||
top_k_tokens=top_k_tokens,
|
||||
seq_lens=seq_lens,
|
||||
state=state,
|
||||
out=graph_out,
|
||||
req_pool_indices=req_pool_indices,
|
||||
num_real_reqs=num_real_reqs,
|
||||
)
|
||||
|
||||
for _ in range(4):
|
||||
graph.replay()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
_assert_output_matches_tokens(state, graph_out, top_k_tokens)
|
||||
self.assertTrue(graph_out.ge(0).all().item())
|
||||
|
||||
def test_rejects_invalid_step_shape_before_compilation(self) -> None:
|
||||
state = _make_state(
|
||||
num_reqs=1,
|
||||
hot_buffer_size=4096,
|
||||
page_size=64,
|
||||
scratch_size=4096,
|
||||
seq_len=8192,
|
||||
item_words=1,
|
||||
metadata_occurrences=8192,
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "2-4 steps"):
|
||||
_run_swap(
|
||||
top_k_tokens=torch.zeros(
|
||||
(1, 1, 2048), dtype=torch.int32, device=DEVICE
|
||||
),
|
||||
seq_lens=torch.tensor([8192], dtype=torch.int32, device=DEVICE),
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,87 @@
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.layernorm.grouped_gemma_rmsnorm import grouped_gemma_rmsnorm
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
def _reference_grouped_gemma_rmsnorm(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
group_size: int,
|
||||
eps: float,
|
||||
compute_dtype: torch.dtype = torch.float32,
|
||||
) -> torch.Tensor:
|
||||
"""Mirrors GroupedGemmaRMSNorm.forward (hyperconnection.py); keep them in sync."""
|
||||
x_float = x.to(compute_dtype)
|
||||
hidden = x_float.shape[-1]
|
||||
x_grouped = x_float.reshape(*x_float.shape[:-1], hidden // group_size, group_size)
|
||||
variance = x_grouped.pow(2).mean(dim=-1, keepdim=True)
|
||||
x_norm = (x_grouped * torch.rsqrt(variance + eps)).flatten(-2)
|
||||
return x_norm * (1.0 + weight.to(compute_dtype))
|
||||
|
||||
|
||||
# Tolerances are at the output-dtype quantization floor, measured against the
|
||||
# fp64 reference on 4xB300 (sm103): bf16 max rel err 3.9e-3 (1 ulp), fp16
|
||||
# 4.9e-4 (0.5 ulp). The kernel computes in fp32 like the eager reference.
|
||||
_TOLERANCES = {
|
||||
torch.bfloat16: dict(rtol=5e-3, atol=5e-3),
|
||||
torch.float16: dict(rtol=1e-3, atol=1e-3),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens,hidden_size,group_size",
|
||||
[
|
||||
(1, 10240, 2560), # production shape (HC 4 x 2560)
|
||||
(7, 10240, 2560),
|
||||
(128, 10240, 2560),
|
||||
(33, 1024, 512),
|
||||
(5, 512, 512), # single group == plain gemma rmsnorm
|
||||
(1024, 2048, 1024),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("eps", [1e-6, 1e-5])
|
||||
def test_grouped_gemma_rmsnorm_correctness(
|
||||
dtype, num_tokens, hidden_size, group_size, eps
|
||||
):
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(num_tokens, hidden_size, dtype=dtype, device="cuda")
|
||||
weight = torch.randn(hidden_size, dtype=dtype, device="cuda") * 0.2
|
||||
|
||||
out = grouped_gemma_rmsnorm(x, weight, group_size, eps)
|
||||
expected = _reference_grouped_gemma_rmsnorm(
|
||||
x, weight, group_size, eps, compute_dtype=torch.float64
|
||||
).to(dtype)
|
||||
|
||||
torch.testing.assert_close(out, expected, **_TOLERANCES[dtype])
|
||||
|
||||
|
||||
def test_grouped_gemma_rmsnorm_out_param():
|
||||
x = torch.randn(64, 10240, dtype=torch.bfloat16, device="cuda")
|
||||
weight = torch.randn(10240, dtype=torch.bfloat16, device="cuda") * 0.2
|
||||
out = torch.empty_like(x)
|
||||
|
||||
result = grouped_gemma_rmsnorm(x, weight, 2560, 1e-6, out=out)
|
||||
expected = _reference_grouped_gemma_rmsnorm(
|
||||
x, weight, 2560, 1e-6, compute_dtype=torch.float64
|
||||
).to(x.dtype)
|
||||
|
||||
assert result.data_ptr() == out.data_ptr()
|
||||
torch.testing.assert_close(result, expected, **_TOLERANCES[x.dtype])
|
||||
|
||||
|
||||
def test_grouped_gemma_rmsnorm_bad_group_size():
|
||||
x = torch.randn(4, 10240, dtype=torch.bfloat16, device="cuda")
|
||||
weight = torch.zeros(10240, dtype=torch.bfloat16, device="cuda")
|
||||
with pytest.raises(RuntimeError, match="group_size"):
|
||||
grouped_gemma_rmsnorm(x, weight, 1000, 1e-6)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,153 @@
|
||||
"""MXFP8 epilogues stay bitwise identical to FlashInfer's standalone quantizer."""
|
||||
|
||||
import sys
|
||||
|
||||
import flashinfer
|
||||
import pytest
|
||||
import torch
|
||||
from flashinfer import mxfp8_quantize
|
||||
|
||||
from sglang.kernels.ops.attention.dsv4.wo_a_bf16 import (
|
||||
_quantize_partial,
|
||||
_wo_a_reduce,
|
||||
wo_a_bf16_small_batch,
|
||||
wo_a_bf16_small_batch_mxfp8,
|
||||
)
|
||||
from sglang.kernels.ops.layernorm.mxfp8_epilogue import rmsnorm_mxfp8
|
||||
from sglang.srt.runtime_context import get_platform
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not torch.cuda.is_available()
|
||||
or torch.version.cuda is None
|
||||
or not get_platform().is_blackwell,
|
||||
reason="the MXFP8 reference quantizer is Blackwell-only",
|
||||
)
|
||||
|
||||
DEVICE = "cuda"
|
||||
|
||||
HIDDEN = 5120
|
||||
STREAMS = 4
|
||||
|
||||
|
||||
@pytest.mark.parametrize("m", [1, 2, 5, 6, 8])
|
||||
@pytest.mark.parametrize("scale", [1e-3, 1.0, 1e3])
|
||||
@pytest.mark.parametrize("backend", ["cuda", "cute-dsl"])
|
||||
def test_bitwise_identical_to_norm_then_quantize(m: int, scale: float, backend: str):
|
||||
from sglang.kernels.ops.layernorm.hc_combine_norm import (
|
||||
hc_combine_norm,
|
||||
hc_combine_norm_mxfp8,
|
||||
)
|
||||
from sglang.srt.layers.quantization.fp8_utils import flashinfer_mxfp8_quantize
|
||||
|
||||
g = torch.Generator(device="cuda").manual_seed(m * 31 + int(scale * 1000))
|
||||
x = (
|
||||
torch.randn(
|
||||
(m, STREAMS * HIDDEN), device="cuda", dtype=torch.bfloat16, generator=g
|
||||
)
|
||||
* scale
|
||||
)
|
||||
pre = torch.randn(
|
||||
(m, STREAMS), device="cuda", dtype=torch.bfloat16, generator=g
|
||||
).contiguous()
|
||||
w = torch.randn((HIDDEN,), device="cuda", dtype=torch.bfloat16, generator=g)
|
||||
eps = 1e-6
|
||||
|
||||
y_ref = hc_combine_norm(x, pre, w, eps)
|
||||
q_ref, sf_ref = flashinfer_mxfp8_quantize(y_ref, True, 32, backend)
|
||||
y, q, sf = hc_combine_norm_mxfp8(x, pre, w, eps)
|
||||
|
||||
assert torch.equal(y, y_ref)
|
||||
assert torch.equal(
|
||||
q.reshape(-1).view(torch.uint8), q_ref.reshape(-1).view(torch.uint8)
|
||||
)
|
||||
assert sf.shape == sf_ref.reshape(-1).shape
|
||||
assert torch.equal(sf, sf_ref.reshape(-1))
|
||||
|
||||
|
||||
def check(x, w, got):
|
||||
y, q, sf = got
|
||||
expected = flashinfer.norm.rmsnorm(x, w, 1e-6)
|
||||
eq, esf = flashinfer.mxfp8_quantize(expected, is_sf_swizzled_layout=True)
|
||||
assert torch.equal(y.view(torch.int16), expected.view(torch.int16))
|
||||
assert torch.equal(q.view(torch.uint8), eq.view(torch.uint8))
|
||||
m = x.shape[0]
|
||||
g = torch.arange(40, device=x.device)
|
||||
row = torch.arange(m, device=x.device)[:, None]
|
||||
offsets = (g // 4) * 512 + ((row % 32) * 4 + row // 32) * 4 + g % 4
|
||||
assert torch.equal(sf[offsets], esf.flatten()[offsets])
|
||||
pad = torch.ones_like(sf, dtype=torch.bool)
|
||||
pad[offsets] = False
|
||||
assert torch.count_nonzero(sf[pad]) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("m", [1, 5, 6, 8])
|
||||
@pytest.mark.parametrize("stride", [1280, 1792])
|
||||
def test_dynamic_graph(m, stride):
|
||||
torch.manual_seed(941)
|
||||
x = torch.randn((m, stride), device="cuda", dtype=torch.bfloat16)[:, :1280]
|
||||
w = torch.randn(1280, device="cuda", dtype=torch.bfloat16)
|
||||
for _ in range(3):
|
||||
check(x, w, rmsnorm_mxfp8(x, w, 1e-6))
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
got = rmsnorm_mxfp8(x, w, 1e-6)
|
||||
for scale in [0, 1e-4, 1, 100]:
|
||||
x.copy_(torch.randn_like(x) * scale)
|
||||
w.copy_(torch.randn_like(w))
|
||||
graph.replay()
|
||||
check(x, w, got)
|
||||
|
||||
|
||||
def test_wo_a_partial_quant_matches_quantize():
|
||||
torch.manual_seed(0)
|
||||
for rows in range(2, 9):
|
||||
for magnitude in (0.0, 1e-37, 1e-7, 1.0, 448.0, 1e10):
|
||||
partial = torch.randn(8, rows, 2, 1024, device=DEVICE) * magnitude
|
||||
bf16 = torch.empty(rows, 2048, dtype=torch.bfloat16, device=DEVICE)
|
||||
_wo_a_reduce[(rows * 8,)](partial, bf16, rows * 2048, num_warps=4)
|
||||
expected_q, expected_s = mxfp8_quantize(bf16, True, alignment=32)
|
||||
actual_q, actual_s = _quantize_partial(partial)
|
||||
torch.testing.assert_close(
|
||||
actual_q.view(torch.uint8),
|
||||
expected_q.view(torch.uint8),
|
||||
rtol=0,
|
||||
atol=0,
|
||||
)
|
||||
torch.testing.assert_close(actual_s, expected_s, rtol=0, atol=0)
|
||||
|
||||
x = torch.randn(rows, 64, 512, device=DEVICE, dtype=torch.bfloat16)[
|
||||
:, :16
|
||||
].view(rows, 2, 4096)
|
||||
wo_a = torch.randn(2, 1024, 4096, device=DEVICE, dtype=torch.bfloat16) * 0.02
|
||||
bf16 = wo_a_bf16_small_batch(x, wo_a).flatten(1)
|
||||
q, s = wo_a_bf16_small_batch_mxfp8(x, wo_a)
|
||||
expected_q, expected_s = mxfp8_quantize(bf16, True, alignment=32)
|
||||
torch.testing.assert_close(
|
||||
q.view(torch.uint8), expected_q.view(torch.uint8), rtol=0, atol=0
|
||||
)
|
||||
torch.testing.assert_close(s, expected_s, rtol=0, atol=0)
|
||||
|
||||
partial = torch.randn(8, 6, 2, 1024, device=DEVICE)
|
||||
_quantize_partial(partial)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
q, s = _quantize_partial(partial)
|
||||
for _ in range(3):
|
||||
partial.normal_()
|
||||
# The replay must regenerate scale padding as well as live rows.
|
||||
s.fill_(255)
|
||||
graph.replay()
|
||||
bf16 = torch.empty(6, 2048, dtype=torch.bfloat16, device=DEVICE)
|
||||
_wo_a_reduce[(48,)](partial, bf16, 6 * 2048, num_warps=4)
|
||||
eq, es = mxfp8_quantize(bf16, True, alignment=32)
|
||||
torch.testing.assert_close(
|
||||
q.view(torch.uint8), eq.view(torch.uint8), rtol=0, atol=0
|
||||
)
|
||||
torch.testing.assert_close(s, es, rtol=0, atol=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__]))
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Tests for Adler-32 GPU checksum against Python zlib.adler32."""
|
||||
|
||||
import unittest
|
||||
import zlib
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.memory.adler32 import (
|
||||
adler32_checksum,
|
||||
adler32_regions_checksum,
|
||||
adler32_strided_checksum,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=15, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
def _ref_adler32(tensor: torch.Tensor) -> int:
|
||||
return zlib.adler32(tensor.cpu().contiguous().view(torch.uint8).numpy().tobytes())
|
||||
|
||||
|
||||
def _ref_strided_adler32(tensors, indices, strides) -> int:
|
||||
parts = []
|
||||
for tensor, idx, stride in zip(tensors, indices, strides):
|
||||
raw = tensor.cpu().contiguous().flatten().view(torch.uint8)
|
||||
for i in idx.cpu().tolist():
|
||||
parts.append(raw[i * stride : (i + 1) * stride].numpy().tobytes())
|
||||
return zlib.adler32(b"".join(parts))
|
||||
|
||||
|
||||
class TestAdler32(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("CUDA not available")
|
||||
torch.manual_seed(42)
|
||||
|
||||
def _check_whole(self, tensor):
|
||||
self.assertEqual(adler32_checksum(tensor), _ref_adler32(tensor))
|
||||
|
||||
def test_whole_small(self):
|
||||
self._check_whole(torch.tensor([1.0, 2, 3, 4], device="cuda"))
|
||||
|
||||
def test_whole_single_element(self):
|
||||
self._check_whole(torch.tensor([42.0], device="cuda"))
|
||||
|
||||
def test_whole_dtypes(self):
|
||||
for dtype, shape in [
|
||||
(torch.bfloat16, (1024, 128)),
|
||||
(torch.float16, (512, 64)),
|
||||
(torch.float32, (4096, 256)),
|
||||
]:
|
||||
self._check_whole(torch.randn(*shape, dtype=dtype, device="cuda"))
|
||||
|
||||
def _check_strided(self, tensors, indices, strides):
|
||||
actual = adler32_strided_checksum(
|
||||
[t.data_ptr() for t in tensors], strides, indices
|
||||
)
|
||||
expected = _ref_strided_adler32(tensors, indices, strides)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_strided_single_tensor(self):
|
||||
t = torch.randn(100, 64, device="cuda")
|
||||
idx = torch.tensor([0, 5, 10, 50, 99], dtype=torch.int64, device="cuda")
|
||||
self._check_strided([t], [idx], [64 * 4])
|
||||
|
||||
def test_strided_multi_tensor_different_strides(self):
|
||||
t1 = torch.randn(50, 32, dtype=torch.float16, device="cuda")
|
||||
t2 = torch.randn(80, 64, dtype=torch.float16, device="cuda")
|
||||
idx1 = torch.tensor([0, 10, 49], dtype=torch.int64, device="cuda")
|
||||
idx2 = torch.tensor([30, 79], dtype=torch.int64, device="cuda")
|
||||
self._check_strided([t1, t2], [idx1, idx2], [32 * 2, 64 * 2])
|
||||
|
||||
def test_strided_many_items(self):
|
||||
t = torch.randn(1000, 128, dtype=torch.bfloat16, device="cuda")
|
||||
idx = torch.arange(1000, dtype=torch.int64, device="cuda")
|
||||
self._check_strided([t], [idx], [128 * 2])
|
||||
|
||||
def test_regions(self):
|
||||
first = torch.randint(
|
||||
0, 256, (5 * 1024 * 1024,), dtype=torch.uint8, device="cuda"
|
||||
)
|
||||
second = torch.randint(0, 256, (12345,), dtype=torch.uint8, device="cuda")
|
||||
actual = adler32_regions_checksum(
|
||||
[first.data_ptr(), second.data_ptr()],
|
||||
[first.numel(), second.numel()],
|
||||
first.device,
|
||||
)
|
||||
expected = zlib.adler32(
|
||||
first.cpu().numpy().tobytes() + second.cpu().numpy().tobytes()
|
||||
)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Tests for the DeepEP v2 contiguous-layout scatter kernel."""
|
||||
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.moe.ep_moe_kernels import ep_scatter_from_psum
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
DEVICE = "cuda"
|
||||
HIDDEN = 256
|
||||
SCALE_HIDDEN = HIDDEN // 128
|
||||
# Match the scatter kernel's BLOCK_E alignment.
|
||||
ALIGN = 128
|
||||
|
||||
|
||||
class TestDeepEPv2ContigScatter(CustomTestCase):
|
||||
"""BF16 and FP8 scatter must preserve routed rows and mask invalid experts."""
|
||||
|
||||
# Local expert ids per (token, slot); -1 marks a route to a remote expert,
|
||||
# and 7 is out of this rank's expert range.
|
||||
RECV_TOPK = [
|
||||
[0, -1],
|
||||
[0, -1],
|
||||
[0, 1],
|
||||
[1, 7],
|
||||
[-1, -1],
|
||||
]
|
||||
NUM_LOCAL_EXPERTS = 2
|
||||
|
||||
def _run(self, dtype, with_scale):
|
||||
num_recv = len(self.RECV_TOPK)
|
||||
recv_x = (
|
||||
torch.arange(num_recv * HIDDEN, dtype=torch.float32, device=DEVICE).reshape(
|
||||
num_recv, HIDDEN
|
||||
)
|
||||
% 100
|
||||
).to(dtype)
|
||||
recv_topk = torch.tensor(self.RECV_TOPK, dtype=torch.int64, device=DEVICE)
|
||||
psum = torch.tensor(
|
||||
[ALIGN * (e + 1) for e in range(self.NUM_LOCAL_EXPERTS)],
|
||||
dtype=torch.int32,
|
||||
device=DEVICE,
|
||||
)
|
||||
all_tokens = int(psum[-1].item())
|
||||
|
||||
recv_x_scale = None
|
||||
output_tensor_scale = None
|
||||
if with_scale:
|
||||
recv_x_scale = torch.arange(
|
||||
num_recv * SCALE_HIDDEN, dtype=torch.float32, device=DEVICE
|
||||
).reshape(num_recv, SCALE_HIDDEN)
|
||||
output_tensor_scale = torch.zeros(
|
||||
(all_tokens, SCALE_HIDDEN), dtype=torch.float32, device=DEVICE
|
||||
)
|
||||
|
||||
output_tensor = torch.zeros((all_tokens, HIDDEN), device=DEVICE, dtype=dtype)
|
||||
m_indices = torch.empty(all_tokens, device=DEVICE, dtype=torch.int32)
|
||||
output_index = torch.empty_like(recv_topk)
|
||||
expert_start_loc = torch.empty_like(psum)
|
||||
|
||||
ep_scatter_from_psum(
|
||||
recv_x,
|
||||
recv_x_scale,
|
||||
recv_topk,
|
||||
psum,
|
||||
expert_start_loc,
|
||||
output_tensor,
|
||||
output_tensor_scale,
|
||||
m_indices,
|
||||
output_index,
|
||||
)
|
||||
return recv_x, recv_x_scale, output_tensor, output_tensor_scale, output_index
|
||||
|
||||
def _check(self, dtype, with_scale):
|
||||
recv_x, recv_x_scale, out, out_scale, output_index = self._run(
|
||||
dtype, with_scale
|
||||
)
|
||||
index = output_index.tolist()
|
||||
|
||||
for token, slots in enumerate(self.RECV_TOPK):
|
||||
for slot, expert in enumerate(slots):
|
||||
dest = index[token][slot]
|
||||
if not 0 <= expert < self.NUM_LOCAL_EXPERTS:
|
||||
# -1 suppresses this route in the post-permute gather.
|
||||
self.assertEqual(dest, -1, msg=f"{token=} {slot=} {expert=}")
|
||||
continue
|
||||
self.assertTrue(
|
||||
ALIGN * expert <= dest < ALIGN * (expert + 1),
|
||||
msg=f"{token=} {slot=} {expert=} {dest=}",
|
||||
)
|
||||
torch.testing.assert_close(out[dest].float(), recv_x[token].float())
|
||||
if with_scale:
|
||||
torch.testing.assert_close(out_scale[dest], recv_x_scale[token])
|
||||
|
||||
accepted = [
|
||||
index[t][s]
|
||||
for t, slots in enumerate(self.RECV_TOPK)
|
||||
for s, e in enumerate(slots)
|
||||
if 0 <= e < self.NUM_LOCAL_EXPERTS
|
||||
]
|
||||
self.assertEqual(len(set(accepted)), len(accepted))
|
||||
|
||||
def test_scatter_bf16_without_scales(self):
|
||||
self._check(torch.bfloat16, with_scale=False)
|
||||
|
||||
def test_scatter_fp8_with_scales(self):
|
||||
self._check(torch.float8_e4m3fn, with_scale=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,186 @@
|
||||
"""The unified Triton router, admitted on ROCm and for single-group routing.
|
||||
|
||||
Pins that the router selects what the torch reference selects, and that the
|
||||
shared expert appears exactly once -- two places can emit it (this router, or
|
||||
_post_process_topk_ids), and if both do, the id is written twice and evicts a
|
||||
real routed expert while the model keeps producing plausible logits.
|
||||
|
||||
The reference is `biased_grouped_topk_impl`, not `select_experts` with the flag
|
||||
off: on ROCm that is the aiter path, which casts the correction bias down to the
|
||||
gating dtype, and GLM-5.2 keeps that bias where bf16 cannot separate neighbours.
|
||||
It reorders routing on its own.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=20, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
register_amd_ci(est_time=20, suite="jit-kernel-unit-test-amd")
|
||||
|
||||
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a GPU")
|
||||
|
||||
E, TOPK_ROUTED, SHARED, SCALE = 256, 8, 1, 2.5
|
||||
HIDDEN = 512
|
||||
|
||||
|
||||
def _jit_routed(monkeypatch, logits, hidden, bias, groups):
|
||||
"""Routed ids from select_experts with the unified router on, sorted."""
|
||||
monkeypatch.setenv("SGLANG_OPT_USE_JIT_KERNEL_GROUPED_TOPK", "1")
|
||||
from sglang.srt.layers.moe.topk import TopKConfig, select_experts
|
||||
|
||||
cfg = TopKConfig(
|
||||
top_k=TOPK_ROUTED + SHARED,
|
||||
renormalize=True,
|
||||
use_grouped_topk=True,
|
||||
num_expert_group=groups,
|
||||
num_fused_shared_experts=SHARED,
|
||||
topk_group=1,
|
||||
scoring_func="sigmoid",
|
||||
correction_bias=bias,
|
||||
routed_scaling_factor=SCALE,
|
||||
apply_routed_scaling_factor_on_output=False,
|
||||
)
|
||||
ids = select_experts(hidden, logits, cfg).topk_ids.long()
|
||||
return ids, ids[ids < E].view(ids.shape[0], TOPK_ROUTED).sort(-1).values
|
||||
|
||||
|
||||
def _reference_routed(logits, hidden, bias, groups):
|
||||
"""Routed ids from the torch reference, sorted.
|
||||
|
||||
It overwrites the last slot with the shared id, so the routed experts are
|
||||
what survives below E.
|
||||
"""
|
||||
from sglang.srt.layers.moe.topk import biased_grouped_topk_impl
|
||||
|
||||
_, ids = biased_grouped_topk_impl(
|
||||
hidden_states=hidden,
|
||||
gating_output=logits,
|
||||
correction_bias=bias,
|
||||
topk=TOPK_ROUTED + SHARED,
|
||||
renormalize=True,
|
||||
num_expert_group=groups,
|
||||
topk_group=1,
|
||||
num_fused_shared_experts=SHARED,
|
||||
routed_scaling_factor=SCALE,
|
||||
)
|
||||
ids = ids.long()
|
||||
return ids[ids < E].view(ids.shape[0], TOPK_ROUTED).sort(-1).values
|
||||
|
||||
|
||||
def _inputs(tokens, dev="cuda"):
|
||||
torch.manual_seed(0)
|
||||
hidden = torch.randn(tokens, HIDDEN, dtype=torch.bfloat16, device=dev)
|
||||
# A narrow band at a large offset, as GLM-5.2's bias is: near-equal values
|
||||
# are what a router has to keep apart.
|
||||
bias = (7.0 + 0.04 * torch.randn(E, device=dev)).float()
|
||||
# fp32 logits: in bf16 the two round the sigmoid differently and near-equal
|
||||
# rows would flip for that reason alone.
|
||||
logits = torch.randn(tokens, E, dtype=torch.float32, device=dev)
|
||||
return hidden, bias, logits
|
||||
|
||||
|
||||
def _groups(request_groups: int) -> int:
|
||||
"""Skip single-group cases where the gate does not admit them.
|
||||
|
||||
biased_grouped_topk_gpu admits one group on ROCm only; CUDA still requires
|
||||
num_expert_group > 1, so a groups=1 case there never reaches the router and
|
||||
the test would assert against a path it did not exercise.
|
||||
"""
|
||||
from sglang.srt.layers.moe import topk as topk_mod
|
||||
|
||||
if request_groups == 1 and not topk_mod._is_hip:
|
||||
pytest.skip("single-group routing is admitted on ROCm only")
|
||||
return request_groups
|
||||
|
||||
|
||||
@pytest.mark.parametrize("groups", [1, 8])
|
||||
@pytest.mark.parametrize("tokens", [6, 48, 256])
|
||||
def test_jit_router_selects_what_the_reference_selects(monkeypatch, tokens, groups):
|
||||
groups = _groups(groups)
|
||||
hidden, bias, logits = _inputs(tokens)
|
||||
|
||||
want = _reference_routed(logits, hidden, bias, groups)
|
||||
_, got = _jit_routed(monkeypatch, logits, hidden, bias, groups)
|
||||
|
||||
score = logits.sigmoid() + bias
|
||||
for r in (want != got).any(-1).nonzero().flatten().tolist():
|
||||
only_want = sorted(set(want[r].tolist()) - set(got[r].tolist()))
|
||||
only_got = sorted(set(got[r].tolist()) - set(want[r].tolist()))
|
||||
# Exact ties may break either way; nothing else may.
|
||||
for x, y in zip(only_want, only_got):
|
||||
assert score[r, x].item() == score[r, y].item(), (
|
||||
f"row {r}: reference took {x} (score {score[r, x].item():.9f}) "
|
||||
f"but the router took {y} (score {score[r, y].item():.9f})"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("groups", [1, 8])
|
||||
def test_shared_expert_appears_exactly_once(monkeypatch, groups):
|
||||
groups = _groups(groups)
|
||||
hidden, bias, logits = _inputs(48)
|
||||
ids, routed = _jit_routed(monkeypatch, logits, hidden, bias, groups)
|
||||
|
||||
assert ids.shape[-1] == TOPK_ROUTED + SHARED
|
||||
shared = (ids >= E).sum(-1)
|
||||
assert torch.equal(shared, torch.full_like(shared, SHARED)), (
|
||||
f"shared expert appears {shared.tolist()} times a row, expected {SHARED}"
|
||||
)
|
||||
assert (routed[:, 1:] != routed[:, :-1]).all(), "a routed expert repeats"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_aiter", [True, False])
|
||||
def test_router_is_asked_for_the_total_width(monkeypatch, use_aiter):
|
||||
"""The width handed to the kernel, for both callers.
|
||||
|
||||
select_experts passes `num_routed_topk if _use_aiter else top_k`; the kernel
|
||||
wants the total either way. Whichever GPU runs the suite fixes `_use_aiter`
|
||||
and can only exercise one half, so pin the arithmetic here.
|
||||
"""
|
||||
from sglang.srt.layers.moe import topk as topk_mod
|
||||
|
||||
seen = {}
|
||||
|
||||
class _Captured(Exception):
|
||||
pass
|
||||
|
||||
def _capture(scores, bias, topk, **kwargs):
|
||||
seen["topk"] = topk
|
||||
raise _Captured
|
||||
|
||||
monkeypatch.setenv("SGLANG_OPT_USE_JIT_KERNEL_GROUPED_TOPK", "1")
|
||||
monkeypatch.setattr(topk_mod, "_use_aiter", use_aiter, raising=False)
|
||||
monkeypatch.setattr(
|
||||
"sglang.kernels.ops.moe.moe_fused_gate.moe_fused_gate", _capture
|
||||
)
|
||||
|
||||
hidden, bias, logits = _inputs(4)
|
||||
# The aiter caller hands over routed-only; everyone else the total.
|
||||
topk_in = TOPK_ROUTED if use_aiter else TOPK_ROUTED + SHARED
|
||||
with pytest.raises(_Captured):
|
||||
topk_mod.biased_grouped_topk_gpu(
|
||||
hidden_states=hidden,
|
||||
gating_output=logits,
|
||||
correction_bias=bias,
|
||||
topk=topk_in,
|
||||
renormalize=True,
|
||||
# 8 groups, not 1: the arithmetic under test is the same either
|
||||
# way, and only this value reaches the router on both platforms.
|
||||
num_expert_group=8,
|
||||
topk_group=1,
|
||||
num_fused_shared_experts=SHARED,
|
||||
routed_scaling_factor=SCALE,
|
||||
)
|
||||
|
||||
assert seen["topk"] == TOPK_ROUTED + SHARED, (
|
||||
f"_use_aiter={use_aiter}: caller passed topk={topk_in}, kernel was asked "
|
||||
f"for {seen['topk']} slots, expected {TOPK_ROUTED + SHARED} "
|
||||
f"(routed {TOPK_ROUTED} + shared {SHARED})"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,273 @@
|
||||
"""Fused QSA indexer-prep kernels must match the eager indexer path bit-for-bit,
|
||||
up to rare last-ulp RMSNorm flips (see assert_bit_comparable)."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
from sglang.srt.layers.attention.qsa.kernel import (
|
||||
average_pool_qsa_keys,
|
||||
expand_qsa_block_indices,
|
||||
torch_expand_qsa_block_indices,
|
||||
)
|
||||
from sglang.srt.layers.attention.qsa.qsa_indexer import QSAIndexer
|
||||
from sglang.srt.layers.rotary_embedding.mrope import MRotaryEmbedding
|
||||
|
||||
# MRotaryEmbedding reads the exec config bag at init; publish a minimal
|
||||
# process context for the bare pytest process.
|
||||
from sglang.srt.runtime_context import publish
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
publish(ServerArgs(model_path="dummy"), role="test")
|
||||
|
||||
HEAD_DIM = 128
|
||||
NUM_Q_HEADS = 4
|
||||
RATIO = 4
|
||||
HIDDEN = 2560
|
||||
EPS = 1e-6
|
||||
|
||||
|
||||
def _make_config():
|
||||
return SimpleNamespace(
|
||||
indexer_n_heads=NUM_Q_HEADS,
|
||||
indexer_kv_heads=1,
|
||||
indexer_head_dim=HEAD_DIM,
|
||||
indexer_budget=2048,
|
||||
indexer_compress_ratio=RATIO,
|
||||
hidden_size=HIDDEN,
|
||||
rms_norm_eps=EPS,
|
||||
)
|
||||
|
||||
|
||||
def _make_rotary(mrope_section, mrope_interleaved, device, dtype=torch.bfloat16):
|
||||
return MRotaryEmbedding(
|
||||
head_size=HEAD_DIM,
|
||||
rotary_dim=HEAD_DIM,
|
||||
max_position_embeddings=32768,
|
||||
base=1000000,
|
||||
is_neox_style=True,
|
||||
dtype=dtype,
|
||||
mrope_section=mrope_section,
|
||||
mrope_interleaved=mrope_interleaved,
|
||||
)
|
||||
|
||||
|
||||
def _make_indexer(rotary, device, dtype=torch.bfloat16):
|
||||
# Build under the model dtype like ModelRunner does; device-only .to()
|
||||
# afterwards so the fp32 cos_sin_cache buffer keeps its dtype.
|
||||
prev_dtype = torch.get_default_dtype()
|
||||
torch.set_default_dtype(dtype)
|
||||
try:
|
||||
indexer = QSAIndexer(
|
||||
_make_config(), layer_id=0, quant_config=None, rotary_emb=rotary
|
||||
)
|
||||
indexer.to(device=device)
|
||||
finally:
|
||||
torch.set_default_dtype(prev_dtype)
|
||||
with torch.no_grad():
|
||||
out_features = (NUM_Q_HEADS + 1) * HEAD_DIM
|
||||
indexer.index_qk_proj.weight.data.copy_(
|
||||
torch.randn(out_features, HIDDEN, device=device, dtype=dtype) * 0.02
|
||||
)
|
||||
for norm in (indexer.q_layernorm, indexer.k_layernorm):
|
||||
w = torch.randn(HEAD_DIM, device=device, dtype=dtype) * 0.1
|
||||
norm._weight_loader(norm.weight, w)
|
||||
return indexer
|
||||
|
||||
|
||||
class FakePool:
|
||||
"""Minimal stand-in for the QSA KV pool buffers used by the indexer."""
|
||||
|
||||
index_state_dtype = torch.bfloat16
|
||||
|
||||
def __init__(self, num_slots, num_compressed, device, dtype=torch.bfloat16):
|
||||
self.key_state = torch.zeros(num_slots, 1, HEAD_DIM, dtype=dtype, device=device)
|
||||
self.qsa_rope_position_buffer = torch.zeros(
|
||||
num_slots, 3, dtype=torch.int64, device=device
|
||||
)
|
||||
self.compressed = torch.zeros(
|
||||
num_compressed, 1, HEAD_DIM, dtype=dtype, device=device
|
||||
)
|
||||
|
||||
def get_qsa_key_state_buffer(self, layer_id):
|
||||
return self.key_state
|
||||
|
||||
def set_qsa_key_state_buffer(self, layer_id, loc, token_k):
|
||||
self.key_state[loc.long()] = token_k.to(self.key_state.dtype)
|
||||
|
||||
def set_qsa_rope_position_buffer(self, loc, positions):
|
||||
positions = positions.long()
|
||||
if positions.ndim == 1:
|
||||
positions = positions.unsqueeze(0).expand(3, -1)
|
||||
self.qsa_rope_position_buffer[loc.long()] = positions.transpose(0, 1)
|
||||
|
||||
def get_qsa_rope_position_buffer(self, loc):
|
||||
return self.qsa_rope_position_buffer[loc.long()]
|
||||
|
||||
def get_qsa_compressed_k_buffer(self, layer_id):
|
||||
return self.compressed
|
||||
|
||||
def set_qsa_compressed_k_buffer(self, layer_id, loc, compressed_k):
|
||||
self.compressed[loc.long()] = compressed_k.to(self.compressed.dtype)
|
||||
|
||||
|
||||
def assert_bit_comparable(actual, expected, max_frac=1e-5, max_abs=0.02):
|
||||
"""Eager RMSNorm (flashinfer CuTe DSL) reduces in an unreproducible order,
|
||||
so ~1 row in 30k flips by 1-2 bf16 ulp; max_frac and max_abs bound that."""
|
||||
diff = (actual.float() - expected.float()).abs()
|
||||
mismatches = int((diff > 0).sum())
|
||||
allowed = max(16, int(max_frac * actual.numel()))
|
||||
assert mismatches <= allowed, f"{mismatches} mismatched elements"
|
||||
if mismatches:
|
||||
peak = diff.max().item()
|
||||
assert peak <= max_abs, f"largest deviation {peak} exceeds {max_abs}"
|
||||
|
||||
|
||||
def _eager_compress_reference(indexer, pool, group_locs, write_locs):
|
||||
"""The pre-fusion compression chain, via the indexer's own helpers."""
|
||||
key_groups = pool.get_qsa_key_state_buffer(0)[group_locs.long()]
|
||||
pooled = average_pool_qsa_keys(key_groups)
|
||||
rope_positions = indexer._rope_from_matrix(
|
||||
pool.get_qsa_rope_position_buffer(group_locs[:, 0])
|
||||
)
|
||||
normalized = indexer.normalize_compressed_keys(pooled, rope_positions)
|
||||
pool.set_qsa_compressed_k_buffer(0, write_locs, normalized)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_groups", [1, 5, 2000])
|
||||
@pytest.mark.parametrize(
|
||||
"mrope_section, mrope_interleaved",
|
||||
[([24, 20, 20], True), ([24, 20, 20], False), (None, False)],
|
||||
)
|
||||
def test_fused_compress_matches_eager(num_groups, mrope_section, mrope_interleaved):
|
||||
device = torch.device("cuda")
|
||||
dtype = torch.bfloat16
|
||||
torch.manual_seed(num_groups)
|
||||
rotary = _make_rotary(mrope_section, mrope_interleaved, device, dtype)
|
||||
indexer = _make_indexer(rotary, device, dtype)
|
||||
|
||||
pool_ref = FakePool(8192, 4096, device, dtype)
|
||||
pool_new = FakePool(8192, 4096, device, dtype)
|
||||
pool_new.key_state.copy_(
|
||||
pool_ref.key_state.copy_(
|
||||
torch.randn(8192, 1, HEAD_DIM, device=device, dtype=dtype)
|
||||
)
|
||||
)
|
||||
positions = torch.randint(0, 30000, (8192, 3), device=device)
|
||||
pool_new.qsa_rope_position_buffer.copy_(positions)
|
||||
pool_ref.qsa_rope_position_buffer.copy_(positions)
|
||||
|
||||
# Random groups; slot 0 doubles as the CUDA-graph dummy write target, so
|
||||
# allow repeats there too.
|
||||
group_locs = torch.randint(0, 8192, (num_groups, RATIO), device=device).to(
|
||||
torch.int32
|
||||
)
|
||||
write_locs = torch.randperm(4096, device=device)[:num_groups].to(torch.int32)
|
||||
|
||||
_eager_compress_reference(indexer, pool_ref, group_locs, write_locs)
|
||||
indexer._fused_compress_store(pool_new, group_locs, write_locs)
|
||||
|
||||
assert_bit_comparable(pool_new.compressed, pool_ref.compressed)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.int32, torch.int64])
|
||||
def test_expand_block_indices_int_inputs(dtype):
|
||||
device = torch.device("cuda")
|
||||
torch.manual_seed(0)
|
||||
rows, block_topk, token_topk, ratio = 37, 512, 2048, 4
|
||||
query_positions = torch.randint(0, 8000, (rows,), dtype=dtype, device=device)
|
||||
sequence_lengths = (
|
||||
query_positions + torch.randint(1, 9, (rows,), dtype=dtype, device=device)
|
||||
).to(dtype)
|
||||
# Production contract: top-k only selects blocks inside [0, seq_len//4),
|
||||
# so no selected block ever masks out against sequence_lengths.
|
||||
counts = torch.randint(0, block_topk + 1, (rows,))
|
||||
block_indices = torch.full((rows, block_topk), -1, dtype=torch.int32)
|
||||
seq_lens_host = sequence_lengths.cpu()
|
||||
for r in range(rows):
|
||||
limit = max(int(seq_lens_host[r]) // ratio, 1)
|
||||
count = min(int(counts[r]), limit)
|
||||
if count:
|
||||
block_indices[r, :count] = torch.randperm(limit)[:count].to(torch.int32)
|
||||
block_indices = block_indices.to(device)
|
||||
out = expand_qsa_block_indices(
|
||||
block_indices, query_positions, sequence_lengths, ratio, token_topk
|
||||
)
|
||||
ref = torch_expand_qsa_block_indices(
|
||||
block_indices.cpu(),
|
||||
query_positions.cpu(),
|
||||
sequence_lengths.cpu(),
|
||||
ratio,
|
||||
token_topk,
|
||||
)
|
||||
assert torch.equal(out.cpu(), ref)
|
||||
|
||||
|
||||
def test_decode_selection_equivalent():
|
||||
"""Last-ulp norm flips must not change the selected blocks:
|
||||
scores are fp32 sums of 128-dim dots, so a 1-ulp flip only matters on exact ties."""
|
||||
from sglang.srt.layers.attention.qsa.kernel import qsa_fast_topk
|
||||
from sglang.srt.layers.attention.qsa.mqa import torch_qsa_mqa_decode
|
||||
|
||||
device = torch.device("cuda")
|
||||
dtype = torch.bfloat16
|
||||
torch.manual_seed(7)
|
||||
rotary = _make_rotary([24, 20, 20], True, device, dtype)
|
||||
indexer = _make_indexer(rotary, device, dtype)
|
||||
|
||||
batch, max_pages, page_size = 4, 32, 64
|
||||
max_model_len = max_pages * page_size
|
||||
hidden = torch.randn(batch, HIDDEN, device=device, dtype=dtype)
|
||||
positions = (
|
||||
torch.arange(8000, 8000 + batch, device=device)
|
||||
.unsqueeze(0)
|
||||
.expand(3, -1)
|
||||
.contiguous()
|
||||
)
|
||||
qk, _ = indexer.index_qk_proj(hidden)
|
||||
|
||||
# Eager index q.
|
||||
q_ref = indexer.q_layernorm(qk[:, : NUM_Q_HEADS * HEAD_DIM].reshape(-1, HEAD_DIM))
|
||||
q_ref = q_ref.reshape(batch, NUM_Q_HEADS, HEAD_DIM)
|
||||
q_ref = indexer.apply_rope(positions, q_ref)
|
||||
|
||||
# Fused index q.
|
||||
pool = FakePool(64, 4096, device, dtype)
|
||||
cache_loc = torch.arange(1, batch + 1, device=device)
|
||||
q_new, _, stored = indexer.project_qk(
|
||||
hidden, positions, pool=pool, cache_loc=cache_loc
|
||||
)
|
||||
assert stored
|
||||
|
||||
compressed_cache = torch.randn(
|
||||
64, page_size, 1, HEAD_DIM, device=device, dtype=dtype
|
||||
)
|
||||
page_table = torch.arange(max_pages, dtype=torch.int32, device=device).repeat(
|
||||
batch, 1
|
||||
)
|
||||
context_lens = torch.full((batch,), 1500, dtype=torch.int32, device=device)
|
||||
|
||||
def select(q):
|
||||
logits = torch_qsa_mqa_decode(
|
||||
q, compressed_cache, page_table, context_lens, max_model_len
|
||||
)
|
||||
row_starts = torch.zeros_like(context_lens)
|
||||
return qsa_fast_topk(logits, row_starts, context_lens, topk=512)
|
||||
|
||||
idx_ref = select(q_ref)
|
||||
idx_new = select(q_new[:, :NUM_Q_HEADS].contiguous())
|
||||
for row in range(batch):
|
||||
ref_set = set(idx_ref[row][idx_ref[row] >= 0].tolist())
|
||||
new_set = set(idx_new[row][idx_new[row] >= 0].tolist())
|
||||
assert ref_set == new_set, f"row {row}: selection mismatch"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,192 @@
|
||||
"""Regression test for the QSA strided sparse-decode scratch zero-fill.
|
||||
|
||||
Poison the packed scratch with NaN, gather with the strided layout used by
|
||||
`_forward_trtllm_sparse`, and require that (a) valid rows are copied exactly and
|
||||
(b) every slot in [valid_count, stride) is zero, so the paged decode kernel can never
|
||||
multiply masked probabilities into stale NaN/Inf bytes. Also checks the compact
|
||||
(FA2 fallback) layout is unchanged. Intended for test/registered/kernels/ops/qsa/.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
from sglang.srt.layers.attention.qsa.sparse_attn import (
|
||||
qwen_sparse_fa2_cu_seqlens_triton,
|
||||
qwen_sparse_kv_extraction_compact_triton,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float8_e4m3fn])
|
||||
def test_strided_gather_zero_fills_tail(dtype):
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required")
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda")
|
||||
batch, topk, page, heads, dim = 3, 2051, 64, 2, 256
|
||||
pages_per_row = (topk + page - 1) // page
|
||||
stride = pages_per_row * page
|
||||
pool_rows = 8192
|
||||
k_pool = torch.randn(pool_rows, heads, dim, device=device, dtype=torch.bfloat16).to(
|
||||
dtype
|
||||
)
|
||||
v_pool = torch.randn(pool_rows, heads, dim, device=device, dtype=torch.bfloat16).to(
|
||||
dtype
|
||||
)
|
||||
seq_lens = torch.tensor([733, 109, 2500], device=device, dtype=torch.int32)
|
||||
req_to_token = (
|
||||
torch.randperm(pool_rows, device=device)[: batch * 2600]
|
||||
.reshape(batch, 2600)
|
||||
.to(torch.int32)
|
||||
)
|
||||
req_indices = torch.arange(batch, device=device, dtype=torch.int32)
|
||||
# top-k rows: the first min(seq_len, topk) logical positions, then -1 padding
|
||||
indices = torch.full((batch, topk), -1, device=device, dtype=torch.int32)
|
||||
for b in range(batch):
|
||||
n = min(int(seq_lens[b]), topk)
|
||||
indices[b, :n] = torch.arange(n, device=device, dtype=torch.int32)
|
||||
cu_strided = torch.arange(batch + 1, device=device, dtype=torch.int32) * stride
|
||||
# the scratch is always in the compute dtype (bf16); an FP8 pool is dequantized on the way in
|
||||
packed_k = torch.full(
|
||||
(batch * stride, heads, dim), float("nan"), device=device, dtype=torch.bfloat16
|
||||
)
|
||||
packed_v = packed_k.clone()
|
||||
|
||||
qwen_sparse_kv_extraction_compact_triton(
|
||||
k_pool,
|
||||
v_pool,
|
||||
req_to_token,
|
||||
req_indices,
|
||||
indices,
|
||||
seq_lens,
|
||||
cu_strided,
|
||||
packed_k,
|
||||
packed_v,
|
||||
batch,
|
||||
topk,
|
||||
zero_fill_cols=stride,
|
||||
)
|
||||
pk, pv = (
|
||||
packed_k.float().view(batch, stride, heads, dim),
|
||||
packed_v.float().view(batch, stride, heads, dim),
|
||||
)
|
||||
assert torch.isfinite(pk).all() and torch.isfinite(pv).all()
|
||||
for b in range(batch):
|
||||
n = min(int(seq_lens[b]), topk)
|
||||
slots = req_to_token[b, :n].long()
|
||||
torch.testing.assert_close(pk[b, :n], k_pool[slots].to(torch.bfloat16).float())
|
||||
torch.testing.assert_close(pv[b, :n], v_pool[slots].to(torch.bfloat16).float())
|
||||
assert (pk[b, n:] == 0).all() and (pv[b, n:] == 0).all()
|
||||
|
||||
|
||||
def test_compact_gather_unchanged():
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required")
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda")
|
||||
batch, topk, heads, dim = 2, 2051, 2, 256
|
||||
k_pool = torch.randn(4096, heads, dim, device=device, dtype=torch.bfloat16)
|
||||
v_pool = torch.randn(4096, heads, dim, device=device, dtype=torch.bfloat16)
|
||||
seq_lens = torch.tensor([300, 50], device=device, dtype=torch.int32)
|
||||
req_to_token = torch.arange(batch * 512, device=device, dtype=torch.int32).reshape(
|
||||
batch, 512
|
||||
)
|
||||
req_indices = torch.arange(batch, device=device, dtype=torch.int32)
|
||||
indices = torch.full((batch, topk), -1, device=device, dtype=torch.int32)
|
||||
for b in range(batch):
|
||||
indices[b, : int(seq_lens[b])] = torch.arange(
|
||||
int(seq_lens[b]), device=device, dtype=torch.int32
|
||||
)
|
||||
counts = torch.empty(batch, device=device, dtype=torch.int32)
|
||||
cu_k = torch.empty(batch + 1, device=device, dtype=torch.int32)
|
||||
qwen_sparse_fa2_cu_seqlens_triton(seq_lens, indices, counts, cu_k, batch, topk)
|
||||
assert cu_k.tolist() == [0, 300, 350]
|
||||
packed_k = torch.full(
|
||||
(batch * topk, heads, dim), float("nan"), device=device, dtype=torch.bfloat16
|
||||
)
|
||||
packed_v = packed_k.clone()
|
||||
qwen_sparse_kv_extraction_compact_triton(
|
||||
k_pool,
|
||||
v_pool,
|
||||
req_to_token,
|
||||
req_indices,
|
||||
indices,
|
||||
seq_lens,
|
||||
cu_k,
|
||||
packed_k,
|
||||
packed_v,
|
||||
batch,
|
||||
topk,
|
||||
)
|
||||
torch.testing.assert_close(packed_k[:300], k_pool[req_to_token[0, :300].long()])
|
||||
torch.testing.assert_close(packed_k[300:350], k_pool[req_to_token[1, :50].long()])
|
||||
# compact layout leaves the region past the packed rows untouched (still NaN)
|
||||
assert torch.isnan(packed_k[350:]).all()
|
||||
|
||||
|
||||
def test_strided_gather_addresses_pool_beyond_int32_elements():
|
||||
"""Slots past 2^31 / (heads * dim) must be addressed with 64-bit offsets.
|
||||
|
||||
An FP8 KV pool on one GB300 holds ~7.6M tokens for Qwen3.8-Flash-Next (2 kv heads x 256),
|
||||
so slot indices above 4,194,304 occur in production; int32 element offsets wrap there.
|
||||
"""
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required")
|
||||
if torch.cuda.get_device_properties(0).total_memory < 6 * 1024**3:
|
||||
pytest.skip("needs ~2.5 GB of device memory")
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda")
|
||||
heads, dim = 2, 256
|
||||
threshold = (1 << 31) // (heads * dim) # 4,194,304
|
||||
pool_rows = threshold + 4096
|
||||
k_pool = torch.zeros(
|
||||
pool_rows, heads, dim, device=device, dtype=torch.float8_e4m3fn
|
||||
)
|
||||
v_pool = torch.zeros(
|
||||
pool_rows, heads, dim, device=device, dtype=torch.float8_e4m3fn
|
||||
)
|
||||
hi = torch.arange(threshold + 64, threshold + 64 + 300, device=device)
|
||||
k_pool[hi] = torch.randn(300, heads, dim, device=device, dtype=torch.bfloat16).to(
|
||||
torch.float8_e4m3fn
|
||||
)
|
||||
v_pool[hi] = torch.randn(300, heads, dim, device=device, dtype=torch.bfloat16).to(
|
||||
torch.float8_e4m3fn
|
||||
)
|
||||
batch, topk, page = 1, 2051, 64
|
||||
stride = ((topk + page - 1) // page) * page
|
||||
seq_lens = torch.tensor([300], device=device, dtype=torch.int32)
|
||||
req_to_token = torch.zeros(batch, 512, device=device, dtype=torch.int32)
|
||||
req_to_token[0, :300] = hi.to(torch.int32)
|
||||
indices = torch.full((batch, topk), -1, device=device, dtype=torch.int32)
|
||||
indices[0, :300] = torch.arange(300, device=device, dtype=torch.int32)
|
||||
cu_strided = torch.arange(batch + 1, device=device, dtype=torch.int32) * stride
|
||||
packed_k = torch.full(
|
||||
(batch * stride, heads, dim), float("nan"), device=device, dtype=torch.bfloat16
|
||||
)
|
||||
packed_v = packed_k.clone()
|
||||
qwen_sparse_kv_extraction_compact_triton(
|
||||
k_pool,
|
||||
v_pool,
|
||||
req_to_token,
|
||||
torch.zeros(1, device=device, dtype=torch.int32),
|
||||
indices,
|
||||
seq_lens,
|
||||
cu_strided,
|
||||
packed_k,
|
||||
packed_v,
|
||||
batch,
|
||||
topk,
|
||||
zero_fill_cols=stride,
|
||||
)
|
||||
torch.testing.assert_close(packed_k[:300], k_pool[hi].to(torch.bfloat16))
|
||||
torch.testing.assert_close(packed_v[:300], v_pool[hi].to(torch.bfloat16))
|
||||
assert (packed_k[300:] == 0).all() and (packed_v[300:] == 0).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.quantization.fp8_utils import unshuffle_aiter_fp8_weight
|
||||
from sglang.srt.utils import is_hip
|
||||
from sglang.test.ci.ci_register import register_amd_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_amd_ci(est_time=5, stage="jit-kernel-unit", runner_config="amd")
|
||||
|
||||
|
||||
@unittest.skipUnless(is_hip(), "requires ROCm AITER")
|
||||
class TestAiterFp8Utils(CustomTestCase):
|
||||
def test_unshuffle_weight_round_trip(self):
|
||||
from aiter.ops.shuffle import shuffle_weight
|
||||
|
||||
for shape in ((32, 64), (2, 32, 64)):
|
||||
with self.subTest(shape=shape):
|
||||
logical = (
|
||||
torch.arange(
|
||||
torch.Size(shape).numel(), device="cuda", dtype=torch.float32
|
||||
)
|
||||
.remainder(7)
|
||||
.to(torch.float8_e4m3fn)
|
||||
.reshape(shape)
|
||||
)
|
||||
shuffled = shuffle_weight(logical, layout=(16, 16))
|
||||
|
||||
torch.testing.assert_close(
|
||||
unshuffle_aiter_fp8_weight(shuffled), logical
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,125 @@
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from sglang.srt.speculative.dflash_worker_v2 import _DominoDraftSampler
|
||||
from sglang.srt.speculative.domino_utils import _domino_gru_cell, domino_greedy_rollout
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
@unittest.skipUnless(torch.cuda.is_available(), "CUDA is required")
|
||||
class TestDFlashDominoRollout(CustomTestCase):
|
||||
def setUp(self):
|
||||
torch.manual_seed(0)
|
||||
self.embedding = nn.Embedding(31, 8, device="cuda", dtype=torch.bfloat16)
|
||||
self.prefix_gru = nn.GRU(8, 4, batch_first=True, bias=False).cuda().bfloat16()
|
||||
self.embed_proj = (
|
||||
nn.Sequential(
|
||||
nn.Linear(12, 5, bias=False), nn.SiLU(), nn.Linear(5, 31, bias=False)
|
||||
)
|
||||
.cuda()
|
||||
.bfloat16()
|
||||
)
|
||||
self.lm_head_weight = torch.randn(31, 8, device="cuda", dtype=torch.bfloat16)
|
||||
self.hidden = torch.randn(3, 16, 8, device="cuda", dtype=torch.bfloat16)
|
||||
self.bonus_tokens = torch.tensor([1, 4, 9], device="cuda")
|
||||
|
||||
def rollout(self, hidden, bonus_tokens, pool_size=5, shift_label=True):
|
||||
return domino_greedy_rollout(
|
||||
draft_hidden=hidden,
|
||||
bonus_tokens=bonus_tokens,
|
||||
target_embedding=self.embedding,
|
||||
lm_head_weight=self.lm_head_weight,
|
||||
prefix_gru=self.prefix_gru,
|
||||
embed_proj=self.embed_proj,
|
||||
vocab_size=31,
|
||||
shift_label=shift_label,
|
||||
candidate_pool_size=pool_size,
|
||||
)
|
||||
|
||||
def test_gru_feedback_matches_sequence(self):
|
||||
embeddings = self.embedding(torch.tensor([[1, 2, 3], [4, 5, 6]], device="cuda"))
|
||||
_, expected = self.prefix_gru(embeddings)
|
||||
state = torch.zeros(2, 4, device="cuda", dtype=torch.bfloat16)
|
||||
for step in embeddings.unbind(dim=1):
|
||||
state = _domino_gru_cell(self.prefix_gru, step, state)
|
||||
torch.testing.assert_close(state, expected[0], rtol=0.02, atol=0.002)
|
||||
|
||||
def test_candidate_pool_boundaries(self):
|
||||
for shift_label in (True, False):
|
||||
with self.subTest(shift_label=shift_label):
|
||||
full = self.rollout(self.hidden, self.bonus_tokens, 0, shift_label)
|
||||
for pool_size in (31, 32):
|
||||
actual = self.rollout(
|
||||
self.hidden, self.bonus_tokens, pool_size, shift_label
|
||||
)
|
||||
torch.testing.assert_close(actual, full, rtol=0, atol=0)
|
||||
first_hidden = self.hidden[:, 0 if shift_label else 1]
|
||||
expected_first = (first_hidden @ self.lm_head_weight.T).argmax(dim=-1)
|
||||
for block_size in (2, 16):
|
||||
limited = self.rollout(
|
||||
self.hidden[:, :block_size], self.bonus_tokens, 1, shift_label
|
||||
)
|
||||
self.assertEqual(limited.shape, (3, block_size - 1))
|
||||
torch.testing.assert_close(limited[:, 0], expected_first)
|
||||
if block_size > 2:
|
||||
torch.testing.assert_close(
|
||||
limited[:, 1:], limited[:, 1:2].expand_as(limited[:, 1:])
|
||||
)
|
||||
|
||||
def test_batch_matches_individual_requests(self):
|
||||
for pool_size in (0, 5):
|
||||
with self.subTest(pool_size=pool_size):
|
||||
batched = self.rollout(self.hidden, self.bonus_tokens, pool_size)
|
||||
individual = torch.cat(
|
||||
[
|
||||
self.rollout(hidden[None], bonus[None], pool_size)
|
||||
for hidden, bonus in zip(self.hidden, self.bonus_tokens)
|
||||
]
|
||||
)
|
||||
torch.testing.assert_close(batched, individual, rtol=0, atol=0)
|
||||
|
||||
def test_sampler_replays_with_new_inputs(self):
|
||||
sampler = _DominoDraftSampler(
|
||||
target_embedding=self.embedding,
|
||||
lm_head_weight=self.lm_head_weight,
|
||||
prefix_gru=self.prefix_gru,
|
||||
embed_proj=self.embed_proj,
|
||||
vocab_size=31,
|
||||
block_size=16,
|
||||
shift_label=True,
|
||||
max_bs=3,
|
||||
candidate_pool_size=5,
|
||||
)
|
||||
block_ids = torch.zeros(3, 16, device="cuda", dtype=torch.long)
|
||||
block_ids[:, 0].copy_(self.bonus_tokens)
|
||||
|
||||
def sample():
|
||||
sampler(self.hidden.flatten(0, 1), block_ids.flatten())
|
||||
|
||||
warmup = torch.cuda.Stream()
|
||||
warmup.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(warmup):
|
||||
sample()
|
||||
torch.cuda.current_stream().wait_stream(warmup)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
sample()
|
||||
|
||||
for _ in range(2):
|
||||
self.hidden.copy_(torch.randn_like(self.hidden))
|
||||
block_ids[:, 0].copy_(torch.randint(31, (3,), device="cuda"))
|
||||
expected = self.rollout(self.hidden, block_ids[:, 0])
|
||||
graph.replay()
|
||||
torch.cuda.synchronize()
|
||||
torch.testing.assert_close(
|
||||
sampler.out.view(3, 15), expected, rtol=0, atol=0
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,195 @@
|
||||
"""2D (token-block) launches of the KV index-copy kernels must be
|
||||
bit-identical to the historical 1D launches, with no unwritten or
|
||||
overwritten bytes (sentinel-checked over the full buffers), and the draft
|
||||
kernel's output must match a Python reference."""
|
||||
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.kernels.ops.attention.utils import (
|
||||
create_flashinfer_kv_indices_triton,
|
||||
kv_indices_num_token_blocks,
|
||||
)
|
||||
from sglang.kernels.ops.speculative.cache_locs import (
|
||||
generate_draft_decode_kv_indices,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=25, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
register_amd_ci(est_time=25, stage="jit-kernel-unit", runner_config="amd")
|
||||
|
||||
SENTINEL = 0x7EADBEEF
|
||||
POOL_LEN = 262_144
|
||||
LENSETS = [
|
||||
[0, 1, 511, 512, 513, 8191, 8192, 8193],
|
||||
[100_000, 33, 4096],
|
||||
[1000] * 7 + [100_000],
|
||||
]
|
||||
|
||||
|
||||
def _npo2(x: int) -> int:
|
||||
return max(1, 1 << (max(1, x) - 1).bit_length())
|
||||
|
||||
|
||||
def _draft_inputs(seqs, topk, steps, device, idx_dtype=torch.int64):
|
||||
bs = len(seqs)
|
||||
req_pool = torch.arange(bs, dtype=idx_dtype, device=device)
|
||||
r2t = torch.randint(
|
||||
0, 6_000_000, (bs + 1, POOL_LEN), dtype=torch.int32, device=device
|
||||
)
|
||||
lens = torch.tensor(seqs, dtype=idx_dtype, device=device)
|
||||
width = topk * (max(seqs) + steps) + 64
|
||||
return bs, req_pool, r2t, lens, width, bs * topk
|
||||
|
||||
|
||||
def _run_draft(kern_inputs, topk, steps, page_size, nb, kw):
|
||||
bs, req_pool, r2t, lens, width, tot = kern_inputs
|
||||
dev = lens.device
|
||||
kv_i = torch.full((steps, bs * width), SENTINEL, dtype=torch.int32, device=dev)
|
||||
kv_p = torch.full((steps, tot + 1), SENTINEL, dtype=torch.int32, device=dev)
|
||||
# positions is per draft token in production (bs * topk entries); the
|
||||
# kernel reads positions[:bs * topk] for the kv_indptr prefix sums.
|
||||
positions = torch.repeat_interleave(lens, topk)
|
||||
generate_draft_decode_kv_indices[(steps * nb, bs, topk)](
|
||||
req_pool,
|
||||
r2t,
|
||||
lens,
|
||||
kv_i,
|
||||
kv_p,
|
||||
positions,
|
||||
POOL_LEN,
|
||||
kv_i.shape[1],
|
||||
kv_p.shape[1],
|
||||
_npo2(bs),
|
||||
_npo2(steps),
|
||||
_npo2(tot),
|
||||
page_size,
|
||||
**kw,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
return kv_i, kv_p
|
||||
|
||||
|
||||
class TestSpecKvIndicesGrid(CustomTestCase):
|
||||
def test_draft_grid_equivalence(self):
|
||||
torch.manual_seed(0)
|
||||
for seqs in LENSETS:
|
||||
for topk, page_size in [(1, 1), (4, 1), (4, 16)]:
|
||||
for steps, idx_dtype in [
|
||||
(2, torch.int64),
|
||||
(3, torch.int32),
|
||||
(4, torch.int64),
|
||||
]:
|
||||
inputs = _draft_inputs(seqs, topk, steps, "cuda", idx_dtype)
|
||||
ref = _run_draft(inputs, topk, steps, page_size, 1, {})
|
||||
for nb in [
|
||||
1,
|
||||
kv_indices_num_token_blocks(POOL_LEN, steps * len(seqs) * topk),
|
||||
triton.cdiv(POOL_LEN, 8192),
|
||||
]:
|
||||
out = _run_draft(
|
||||
inputs, topk, steps, page_size, nb, {"NUM_STEPS": steps}
|
||||
)
|
||||
self.assertTrue(torch.equal(ref[0], out[0]), (seqs, topk, nb))
|
||||
self.assertTrue(torch.equal(ref[1], out[1]), (seqs, topk, nb))
|
||||
|
||||
def test_draft_reference(self):
|
||||
torch.manual_seed(1)
|
||||
seqs, steps = [100_000, 33, 4096, 16], 3
|
||||
for topk, page_size in [(1, 1), (4, 1), (4, 16)]:
|
||||
inputs = _draft_inputs(seqs, topk, steps, "cuda")
|
||||
bs, _, r2t, _, _, tot = inputs
|
||||
nb = kv_indices_num_token_blocks(POOL_LEN, steps * bs * topk)
|
||||
kv_i, kv_p = _run_draft(
|
||||
inputs, topk, steps, page_size, nb, {"NUM_STEPS": steps}
|
||||
)
|
||||
for it in range(steps):
|
||||
iters = it + 1
|
||||
for s in range(bs):
|
||||
ln = seqs[s]
|
||||
for k in range(topk):
|
||||
off = sum(seqs[:s]) * topk + s * iters * topk + k * (ln + iters)
|
||||
self.assertTrue(
|
||||
torch.equal(r2t[s, :ln], kv_i[it, off : off + ln]),
|
||||
(it, s, k, topk, page_size),
|
||||
)
|
||||
if page_size == 1 or topk == 1:
|
||||
src = ln + k * steps
|
||||
else:
|
||||
last = ln % page_size
|
||||
pages = -(-(last + steps) // page_size)
|
||||
src = (
|
||||
ln // page_size * page_size
|
||||
+ k * pages * page_size
|
||||
+ last
|
||||
)
|
||||
self.assertTrue(
|
||||
torch.equal(
|
||||
r2t[s, src : src + iters],
|
||||
kv_i[it, off + ln : off + ln + iters],
|
||||
),
|
||||
(it, s, k, topk, page_size),
|
||||
)
|
||||
positions = [ln for ln in seqs for _ in range(topk)]
|
||||
for z in range(1, tot + 1):
|
||||
self.assertEqual(
|
||||
int(kv_p[it, z]),
|
||||
sum(positions[:z]) + z * iters,
|
||||
(it, z, topk, page_size),
|
||||
)
|
||||
|
||||
def test_flat_grid_equivalence(self):
|
||||
torch.manual_seed(0)
|
||||
dev = "cuda"
|
||||
for seqs in LENSETS:
|
||||
for use_start, entry_page_size in [(False, 1), (True, 1), (False, 16)]:
|
||||
bs = len(seqs)
|
||||
req_pool = torch.arange(bs, dtype=torch.int64, device=dev)
|
||||
r2t = torch.randint(
|
||||
0, 6_000_000, (bs + 1, POOL_LEN), dtype=torch.int32, device=dev
|
||||
)
|
||||
lens = torch.tensor(
|
||||
seqs,
|
||||
dtype=torch.int32 if use_start else torch.int64,
|
||||
device=dev,
|
||||
)
|
||||
indptr = torch.zeros(bs + 1, dtype=torch.int32, device=dev)
|
||||
indptr[1:] = torch.cumsum(lens, 0).to(torch.int32)
|
||||
start = (
|
||||
torch.full((bs,), 7, dtype=torch.int32, device=dev)
|
||||
if use_start
|
||||
else None
|
||||
)
|
||||
n = int(indptr[-1]) + (7 * bs if use_start else 0) + 8
|
||||
outs = []
|
||||
for grid, parallel in [
|
||||
((bs,), False),
|
||||
((bs, 1), True),
|
||||
((bs, kv_indices_num_token_blocks(POOL_LEN, bs)), True),
|
||||
((bs, triton.cdiv(POOL_LEN, 8192)), True),
|
||||
]:
|
||||
kv_i = torch.full((n,), SENTINEL, dtype=torch.int32, device=dev)
|
||||
create_flashinfer_kv_indices_triton[grid](
|
||||
r2t,
|
||||
req_pool,
|
||||
lens,
|
||||
indptr,
|
||||
start,
|
||||
kv_i,
|
||||
r2t.shape[1],
|
||||
ENTRY_PAGE_SIZE=entry_page_size,
|
||||
TOKEN_BLOCK_PARALLEL=parallel,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
outs.append(kv_i)
|
||||
for kv_i in outs[1:]:
|
||||
self.assertTrue(
|
||||
torch.equal(outs[0], kv_i), (seqs, use_start, entry_page_size)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,81 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.mamba.mamba_state_scatter_triton import (
|
||||
fused_commit_track_indices,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
def _reference(accept_index, accept_lens, seq_lens, draft_token_num, track_interval):
|
||||
"""Mirrors the eager branch of spec_utils._verify_commit_step_indices."""
|
||||
bs = accept_lens.shape[0]
|
||||
offset = torch.arange(
|
||||
0,
|
||||
bs * draft_token_num,
|
||||
step=draft_token_num,
|
||||
dtype=accept_lens.dtype,
|
||||
device=accept_lens.device,
|
||||
)
|
||||
req_idx = torch.arange(bs, dtype=torch.int64, device=accept_lens.device)
|
||||
last = accept_index[req_idx, (accept_lens - 1).to(torch.int64)] - offset
|
||||
if track_interval <= 0:
|
||||
return last, None
|
||||
pre = seq_lens
|
||||
post = seq_lens + accept_lens
|
||||
mask = pre // track_interval != post // track_interval
|
||||
point = post // track_interval * track_interval
|
||||
ith = torch.clamp(point - pre - 1, min=0).to(torch.int64)
|
||||
cand = accept_index[req_idx, ith] - offset
|
||||
track = torch.where(mask, cand, torch.full_like(cand, -1))
|
||||
return last, track
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bs", [1, 3, 48, 257])
|
||||
@pytest.mark.parametrize("track_interval", [0, 64])
|
||||
@pytest.mark.parametrize("tree_depth", [4, 3])
|
||||
def test_verify_commit_steps_matches_eager(bs, track_interval, tree_depth):
|
||||
"""The fused kernel must match eager on both outputs near tracking boundaries
|
||||
and when accept_index rows (max_tree_depth) are narrower than draft_token_num."""
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("needs CUDA")
|
||||
torch.manual_seed(bs + track_interval + tree_depth)
|
||||
device = "cuda"
|
||||
draft_token_num = 4
|
||||
accept_lens = torch.randint(
|
||||
1, tree_depth + 1, (bs,), device=device, dtype=torch.int32
|
||||
)
|
||||
tree_nodes = torch.argsort(torch.rand(bs, draft_token_num, device=device), dim=1)[
|
||||
:, :tree_depth
|
||||
]
|
||||
accept_index = (
|
||||
torch.arange(bs, device=device, dtype=torch.int64).unsqueeze(1)
|
||||
* draft_token_num
|
||||
+ tree_nodes
|
||||
).to(torch.int32)
|
||||
# Cluster seq lens around tracking boundaries to exercise the crossing.
|
||||
seq_lens = torch.randint(60, 70, (bs,), device=device, dtype=torch.int64)
|
||||
|
||||
exp_last, exp_track = _reference(
|
||||
accept_index, accept_lens, seq_lens, draft_token_num, track_interval
|
||||
)
|
||||
got_last, got_track = fused_commit_track_indices(
|
||||
accept_index,
|
||||
accept_lens,
|
||||
seq_lens if track_interval > 0 else None,
|
||||
draft_token_num,
|
||||
track_interval,
|
||||
)
|
||||
assert torch.equal(got_last, exp_last)
|
||||
if track_interval > 0:
|
||||
assert torch.equal(got_track, exp_track)
|
||||
else:
|
||||
assert got_track is None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
Reference in New Issue
Block a user