[CI] Move JIT kernel tests + benchmarks to test/registered/jit; add in-package guard (#27644)
This commit is contained in:
@@ -1,260 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Tuple, Union
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.jit_kernel.benchmark.bench_activation import register_cuda_ci
|
||||
from sglang.jit_kernel.dsv4 import compress_forward
|
||||
from sglang.jit_kernel.tests.deepseek_v4.common import (
|
||||
LegacyContext,
|
||||
PagedContext,
|
||||
make_legacy_context,
|
||||
make_paged_context,
|
||||
make_state_pool,
|
||||
to_seq_extend,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=30, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
Context = Union[LegacyContext, PagedContext]
|
||||
|
||||
# c128 input row layout: | kv | score | each [head_dim]
|
||||
HEAD_DIM = 512
|
||||
RATIO = 128
|
||||
ATOL = 5e-3
|
||||
RTOL = 5e-3
|
||||
|
||||
|
||||
def _gt_compress(
|
||||
kv_score_input_cpu: torch.Tensor, # [num_q, head_dim*2]
|
||||
ape_cpu: torch.Tensor, # [128, head_dim]
|
||||
P: int,
|
||||
head_dim: int,
|
||||
) -> torch.Tensor:
|
||||
"""fp64 reference for compress event at ragged position ``P`` (P % 128 == 127)."""
|
||||
lo = P - (RATIO - 1)
|
||||
kv = kv_score_input_cpu[lo : P + 1, :head_dim].double()
|
||||
sc = kv_score_input_cpu[lo : P + 1, head_dim:].double()
|
||||
return ((kv * (sc + ape_cpu.double()).softmax(dim=0)).sum(dim=0)).float()
|
||||
|
||||
|
||||
def _make_inputs(
|
||||
num_q: int, head_dim: int, seed: int
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
g = torch.Generator(device="cpu").manual_seed(seed)
|
||||
kv_score_input_cpu = torch.randn(
|
||||
num_q, head_dim * 2, generator=g, dtype=torch.float32
|
||||
)
|
||||
ape_cpu = torch.randn(RATIO, head_dim, generator=g, dtype=torch.float32)
|
||||
return kv_score_input_cpu, ape_cpu
|
||||
|
||||
|
||||
def _run_prefill(
|
||||
ctx: Context,
|
||||
pool: torch.Tensor,
|
||||
kv_score_input: torch.Tensor,
|
||||
ape: torch.Tensor,
|
||||
seq_lens_cpu: torch.Tensor,
|
||||
extend_lens_cpu: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
num_q = int(extend_lens_cpu.sum().item())
|
||||
plan = ctx.make_prefill_plan(seq_lens_cpu, extend_lens_cpu, num_q)
|
||||
return compress_forward(
|
||||
pool,
|
||||
kv_score_input,
|
||||
ape,
|
||||
plan,
|
||||
head_dim=ctx.head_dim,
|
||||
compress_ratio=RATIO,
|
||||
)
|
||||
|
||||
|
||||
def _run_decode(
|
||||
ctx: Context,
|
||||
pool: torch.Tensor,
|
||||
kv_score_input: torch.Tensor,
|
||||
ape: torch.Tensor,
|
||||
seq_lens_gpu: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
plan = ctx.make_decode_plan(seq_lens_gpu)
|
||||
return compress_forward(
|
||||
pool,
|
||||
kv_score_input,
|
||||
ape,
|
||||
plan,
|
||||
head_dim=ctx.head_dim,
|
||||
compress_ratio=RATIO,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Tests
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["legacy", "paged"])
|
||||
@pytest.mark.parametrize("seq_len", [128, 256, 512])
|
||||
def test_prefill_no_context(mode: str, seq_len: int) -> None:
|
||||
"""Single-shot prefill, no prefix. Every compress event must match fp64 GT."""
|
||||
if mode == "legacy":
|
||||
ctx: Context = make_legacy_context(
|
||||
bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM
|
||||
)
|
||||
else:
|
||||
ctx = make_paged_context(bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM)
|
||||
|
||||
seq_lens_cpu, extend_lens_cpu, num_q = to_seq_extend([(seq_len, seq_len)])
|
||||
kv_in_cpu, ape_cpu = _make_inputs(num_q, ctx.head_dim, seed=seq_len)
|
||||
|
||||
pool = make_state_pool(ctx.num_pages, RATIO, ctx.head_dim)
|
||||
out = _run_prefill(
|
||||
ctx, pool, kv_in_cpu.cuda(), ape_cpu.cuda(), seq_lens_cpu, extend_lens_cpu
|
||||
)
|
||||
|
||||
# Compact prefill output: row per compress plan, in CPU-planner order.
|
||||
for plan_id, P in enumerate(range(RATIO - 1, seq_len, RATIO)):
|
||||
gt = _gt_compress(kv_in_cpu, ape_cpu, P=P, head_dim=ctx.head_dim)
|
||||
triton.testing.assert_close(out[plan_id].cpu(), gt, atol=ATOL, rtol=RTOL)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["legacy", "paged"])
|
||||
@pytest.mark.parametrize("prefix_len", [0, 128, 256])
|
||||
def test_prefill_then_decode(mode: str, prefix_len: int) -> None:
|
||||
"""Prefill ``prefix_len`` tokens, then decode through to the next 128 boundary."""
|
||||
seq_len = prefix_len + RATIO # one full compress chunk after prefix
|
||||
|
||||
if mode == "legacy":
|
||||
ctx: Context = make_legacy_context(
|
||||
bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM
|
||||
)
|
||||
else:
|
||||
ctx = make_paged_context(bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM)
|
||||
|
||||
kv_full_cpu, ape_cpu = _make_inputs(
|
||||
seq_len, ctx.head_dim, seed=seq_len + prefix_len
|
||||
)
|
||||
pool = make_state_pool(ctx.num_pages, RATIO, ctx.head_dim)
|
||||
|
||||
if prefix_len > 0:
|
||||
seq_lens_cpu, extend_lens_cpu, _ = to_seq_extend([(prefix_len, prefix_len)])
|
||||
_run_prefill(
|
||||
ctx,
|
||||
pool,
|
||||
kv_full_cpu[:prefix_len].cuda(),
|
||||
ape_cpu.cuda(),
|
||||
seq_lens_cpu,
|
||||
extend_lens_cpu,
|
||||
)
|
||||
|
||||
final_out = None
|
||||
for k in range(RATIO):
|
||||
cur_seq_len = prefix_len + k + 1
|
||||
seq_lens_gpu = torch.tensor([cur_seq_len], dtype=torch.int64, device="cuda")
|
||||
kv_step = kv_full_cpu[prefix_len + k : prefix_len + k + 1].cuda()
|
||||
out = _run_decode(ctx, pool, kv_step, ape_cpu.cuda(), seq_lens_gpu)
|
||||
if cur_seq_len % RATIO == 0:
|
||||
final_out = out
|
||||
|
||||
P = seq_len - 1
|
||||
gt = _gt_compress(kv_full_cpu, ape_cpu, P=P, head_dim=ctx.head_dim)
|
||||
assert final_out is not None
|
||||
triton.testing.assert_close(final_out[0].cpu(), gt, atol=ATOL, rtol=RTOL)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["legacy", "paged"])
|
||||
@pytest.mark.parametrize("prefix_len", [128, 256])
|
||||
def test_prefill_then_extend(mode: str, prefix_len: int) -> None:
|
||||
"""Prefill once, then a second prefill that extends across one compress event.
|
||||
|
||||
First prefill ends at a 128-boundary so the second prefill starts fresh.
|
||||
"""
|
||||
extend_len = RATIO
|
||||
seq_len = prefix_len + extend_len
|
||||
|
||||
if mode == "legacy":
|
||||
ctx: Context = make_legacy_context(
|
||||
bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM
|
||||
)
|
||||
else:
|
||||
ctx = make_paged_context(bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM)
|
||||
|
||||
kv_full_cpu, ape_cpu = _make_inputs(seq_len, ctx.head_dim, seed=prefix_len)
|
||||
pool = make_state_pool(ctx.num_pages, RATIO, ctx.head_dim)
|
||||
|
||||
seq_lens_cpu, extend_lens_cpu, _ = to_seq_extend([(prefix_len, prefix_len)])
|
||||
_run_prefill(
|
||||
ctx,
|
||||
pool,
|
||||
kv_full_cpu[:prefix_len].cuda(),
|
||||
ape_cpu.cuda(),
|
||||
seq_lens_cpu,
|
||||
extend_lens_cpu,
|
||||
)
|
||||
|
||||
seq_lens_cpu, extend_lens_cpu, _ = to_seq_extend([(seq_len, extend_len)])
|
||||
out = _run_prefill(
|
||||
ctx,
|
||||
pool,
|
||||
kv_full_cpu[prefix_len:].cuda(),
|
||||
ape_cpu.cuda(),
|
||||
seq_lens_cpu,
|
||||
extend_lens_cpu,
|
||||
)
|
||||
|
||||
P = seq_len - 1
|
||||
gt = _gt_compress(kv_full_cpu, ape_cpu, P=P, head_dim=ctx.head_dim)
|
||||
# Single compress event in this extend; compact plan_id 0.
|
||||
triton.testing.assert_close(out[0].cpu(), gt, atol=ATOL, rtol=RTOL)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["legacy", "paged"])
|
||||
def test_prefill_multibatch(mode: str) -> None:
|
||||
"""Multi-batch prefill, each batch ending at a different chunk count."""
|
||||
seq_extend = [(128, 128), (256, 256), (384, 384)]
|
||||
bs = len(seq_extend)
|
||||
if mode == "legacy":
|
||||
ctx: Context = make_legacy_context(
|
||||
bs=bs, compress_ratio=RATIO, head_dim=HEAD_DIM
|
||||
)
|
||||
else:
|
||||
ctx = make_paged_context(bs=bs, compress_ratio=RATIO, head_dim=HEAD_DIM)
|
||||
|
||||
seq_lens_cpu, extend_lens_cpu, num_q = to_seq_extend(seq_extend)
|
||||
kv_in_cpu, ape_cpu = _make_inputs(num_q, ctx.head_dim, seed=99)
|
||||
pool = make_state_pool(ctx.num_pages, RATIO, ctx.head_dim)
|
||||
out = _run_prefill(
|
||||
ctx, pool, kv_in_cpu.cuda(), ape_cpu.cuda(), seq_lens_cpu, extend_lens_cpu
|
||||
)
|
||||
|
||||
# Compact: walk batches in order, then positions in order; matches the
|
||||
# CPU planner's emit order for plan_c.
|
||||
base = 0
|
||||
plan_id = 0
|
||||
for b, (seq, ext) in enumerate(seq_extend):
|
||||
for j in range(ext):
|
||||
P = j # prefix=0
|
||||
if (P + 1) % RATIO != 0:
|
||||
continue
|
||||
gt = _gt_compress(
|
||||
kv_in_cpu[base : base + ext],
|
||||
ape_cpu,
|
||||
P=P,
|
||||
head_dim=ctx.head_dim,
|
||||
)
|
||||
triton.testing.assert_close(
|
||||
out[plan_id].cpu(),
|
||||
gt,
|
||||
atol=ATOL,
|
||||
rtol=RTOL,
|
||||
)
|
||||
plan_id += 1
|
||||
base += ext
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -1,337 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Tuple, Union
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.jit_kernel.benchmark.bench_activation import register_cuda_ci
|
||||
from sglang.jit_kernel.dsv4 import compress_forward
|
||||
from sglang.jit_kernel.tests.deepseek_v4.common import (
|
||||
LegacyContext,
|
||||
PagedContext,
|
||||
make_legacy_context,
|
||||
make_paged_context,
|
||||
make_state_pool,
|
||||
to_seq_extend,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=30, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
Context = Union[LegacyContext, PagedContext]
|
||||
|
||||
# c4 input row layout: | kv_overlap | kv | score_overlap | score |
|
||||
HEAD_DIM = 512
|
||||
RATIO = 4
|
||||
WINDOW = 8 # = 2 * RATIO (overlap + current)
|
||||
ATOL = 5e-3
|
||||
RTOL = 5e-3
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# fp64 ground truth (single compress event over a 8-token window).
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _gt_compress(
|
||||
kv_score_input_cpu: torch.Tensor, # [num_q, head_dim*4]
|
||||
ape_cpu: torch.Tensor, # [8, head_dim]
|
||||
P: int,
|
||||
head_dim: int,
|
||||
) -> torch.Tensor:
|
||||
"""fp64 reference for compress event at ragged position ``P``.
|
||||
|
||||
Tokens at positions [P-7..P-4] contribute their *overlap* halves, tokens
|
||||
at [P-3..P] contribute their *fresh* halves. Bias[0..3] for overlap,
|
||||
bias[4..7] for fresh. When P < 7, the overlap is masked (kv=0, score=-inf)
|
||||
so the softmax sees only the 4 fresh tokens.
|
||||
"""
|
||||
if P < 7:
|
||||
kv_ov = torch.zeros(4, head_dim, dtype=torch.float64)
|
||||
sc_ov = torch.full((4, head_dim), float("-inf"), dtype=torch.float64)
|
||||
else:
|
||||
kv_ov = kv_score_input_cpu[P - 7 : P - 3, :head_dim].double()
|
||||
sc_ov = kv_score_input_cpu[P - 7 : P - 3, 2 * head_dim : 3 * head_dim].double()
|
||||
kv_fr = kv_score_input_cpu[P - 3 : P + 1, head_dim : 2 * head_dim].double()
|
||||
sc_fr = kv_score_input_cpu[P - 3 : P + 1, 3 * head_dim :].double()
|
||||
kv = torch.cat([kv_ov, kv_fr], dim=0)
|
||||
sc = torch.cat([sc_ov, sc_fr], dim=0) + ape_cpu.double()
|
||||
return ((kv * sc.softmax(dim=0)).sum(dim=0)).float()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Driver
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _run_prefill(
|
||||
ctx: Context,
|
||||
pool: torch.Tensor,
|
||||
kv_score_input: torch.Tensor,
|
||||
ape: torch.Tensor,
|
||||
seq_lens_cpu: torch.Tensor,
|
||||
extend_lens_cpu: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
num_q = int(extend_lens_cpu.sum().item())
|
||||
plan = ctx.make_prefill_plan(seq_lens_cpu, extend_lens_cpu, num_q)
|
||||
return compress_forward(
|
||||
pool,
|
||||
kv_score_input,
|
||||
ape,
|
||||
plan,
|
||||
head_dim=ctx.head_dim,
|
||||
compress_ratio=RATIO,
|
||||
)
|
||||
|
||||
|
||||
def _run_decode(
|
||||
ctx: Context,
|
||||
pool: torch.Tensor,
|
||||
kv_score_input: torch.Tensor,
|
||||
ape: torch.Tensor,
|
||||
seq_lens_gpu: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
plan = ctx.make_decode_plan(seq_lens_gpu)
|
||||
return compress_forward(
|
||||
pool,
|
||||
kv_score_input,
|
||||
ape,
|
||||
plan,
|
||||
head_dim=ctx.head_dim,
|
||||
compress_ratio=RATIO,
|
||||
)
|
||||
|
||||
|
||||
def _make_inputs(
|
||||
num_q: int, head_dim: int, seed: int
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
g = torch.Generator(device="cpu").manual_seed(seed)
|
||||
kv_score_input_cpu = torch.randn(
|
||||
num_q, head_dim * 4, generator=g, dtype=torch.float32
|
||||
)
|
||||
ape_cpu = torch.randn(WINDOW, head_dim, generator=g, dtype=torch.float32)
|
||||
return kv_score_input_cpu, ape_cpu
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Tests
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["legacy", "paged"])
|
||||
@pytest.mark.parametrize("seq_len", [4, 8, 32, 256, 1024])
|
||||
def test_prefill_no_context(mode: str, seq_len: int) -> None:
|
||||
"""Prefill once, no prefix. Every compress event must match fp64 GT."""
|
||||
if mode == "legacy":
|
||||
ctx: Context = make_legacy_context(
|
||||
bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM
|
||||
)
|
||||
else:
|
||||
ctx = make_paged_context(bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM)
|
||||
|
||||
seq_lens_cpu, extend_lens_cpu, num_q = to_seq_extend([(seq_len, seq_len)])
|
||||
kv_in_cpu, ape_cpu = _make_inputs(num_q, ctx.head_dim, seed=seq_len)
|
||||
|
||||
pool = make_state_pool(ctx.num_pages, RATIO, ctx.head_dim)
|
||||
out = _run_prefill(
|
||||
ctx, pool, kv_in_cpu.cuda(), ape_cpu.cuda(), seq_lens_cpu, extend_lens_cpu
|
||||
)
|
||||
|
||||
# Compact prefill output: row per compress plan, in CPU-planner order
|
||||
# (batch-major, position-ascending).
|
||||
for plan_id, P in enumerate(range(RATIO - 1, seq_len, RATIO)):
|
||||
gt = _gt_compress(kv_in_cpu, ape_cpu, P=P, head_dim=ctx.head_dim)
|
||||
triton.testing.assert_close(out[plan_id].cpu(), gt, atol=ATOL, rtol=RTOL)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["legacy", "paged"])
|
||||
@pytest.mark.parametrize("prefix_len", [4, 256])
|
||||
def test_prefill_then_decode(mode: str, prefix_len: int) -> None:
|
||||
"""Prefill once, then decode 4 more tokens through one compress boundary."""
|
||||
extend_decode = 4
|
||||
seq_len = prefix_len + extend_decode
|
||||
|
||||
if mode == "legacy":
|
||||
ctx: Context = make_legacy_context(
|
||||
bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM
|
||||
)
|
||||
else:
|
||||
ctx = make_paged_context(bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM)
|
||||
|
||||
kv_full_cpu, ape_cpu = _make_inputs(
|
||||
seq_len, ctx.head_dim, seed=seq_len + prefix_len
|
||||
)
|
||||
pool = make_state_pool(ctx.num_pages, RATIO, ctx.head_dim)
|
||||
|
||||
# Prefill the prefix.
|
||||
seq_lens_cpu, extend_lens_cpu, _ = to_seq_extend([(prefix_len, prefix_len)])
|
||||
_run_prefill(
|
||||
ctx,
|
||||
pool,
|
||||
kv_full_cpu[:prefix_len].cuda(),
|
||||
ape_cpu.cuda(),
|
||||
seq_lens_cpu,
|
||||
extend_lens_cpu,
|
||||
)
|
||||
|
||||
# Decode `extend_decode` tokens one at a time.
|
||||
final_out = None
|
||||
for k in range(extend_decode):
|
||||
cur_seq_len = prefix_len + k + 1
|
||||
seq_lens_gpu = torch.tensor([cur_seq_len], dtype=torch.int64, device="cuda")
|
||||
kv_step = kv_full_cpu[prefix_len + k : prefix_len + k + 1].cuda()
|
||||
out = _run_decode(ctx, pool, kv_step, ape_cpu.cuda(), seq_lens_gpu)
|
||||
if cur_seq_len % RATIO == 0:
|
||||
final_out = out
|
||||
|
||||
# Check the trailing compress: position P = seq_len - 1 = prefix + 3.
|
||||
P = seq_len - 1
|
||||
gt = _gt_compress(kv_full_cpu, ape_cpu, P=P, head_dim=ctx.head_dim)
|
||||
assert final_out is not None
|
||||
triton.testing.assert_close(final_out[0].cpu(), gt, atol=ATOL, rtol=RTOL)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["legacy", "paged"])
|
||||
@pytest.mark.parametrize("prefix_len", [256, 512, 768])
|
||||
def test_prefill_then_extend(mode: str, prefix_len: int) -> None:
|
||||
"""Prefill once, then prefill an extend that crosses one compress event.
|
||||
|
||||
The first prefill ends at a swa_page boundary (only relevant for paged),
|
||||
so the second prefill's overlap must be read out of the buffer.
|
||||
"""
|
||||
extend_len = 4
|
||||
|
||||
if mode == "legacy":
|
||||
ctx: Context = make_legacy_context(
|
||||
bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM
|
||||
)
|
||||
else:
|
||||
ctx = make_paged_context(bs=1, compress_ratio=RATIO, head_dim=HEAD_DIM)
|
||||
|
||||
seq_len = prefix_len + extend_len
|
||||
kv_full_cpu, ape_cpu = _make_inputs(seq_len, ctx.head_dim, seed=prefix_len)
|
||||
pool = make_state_pool(ctx.num_pages, RATIO, ctx.head_dim)
|
||||
|
||||
# First prefill: seq=prefix, ext=prefix.
|
||||
seq_lens_cpu, extend_lens_cpu, _ = to_seq_extend([(prefix_len, prefix_len)])
|
||||
_run_prefill(
|
||||
ctx,
|
||||
pool,
|
||||
kv_full_cpu[:prefix_len].cuda(),
|
||||
ape_cpu.cuda(),
|
||||
seq_lens_cpu,
|
||||
extend_lens_cpu,
|
||||
)
|
||||
|
||||
# Second prefill: seq=prefix+extend, ext=extend, prefix=prefix_len.
|
||||
seq_lens_cpu, extend_lens_cpu, num_q = to_seq_extend([(seq_len, extend_len)])
|
||||
out = _run_prefill(
|
||||
ctx,
|
||||
pool,
|
||||
kv_full_cpu[prefix_len:].cuda(),
|
||||
ape_cpu.cuda(),
|
||||
seq_lens_cpu,
|
||||
extend_lens_cpu,
|
||||
)
|
||||
|
||||
P = seq_len - 1
|
||||
gt = _gt_compress(kv_full_cpu, ape_cpu, P=P, head_dim=ctx.head_dim)
|
||||
# Single compress event in this extend; compact plan_id 0.
|
||||
triton.testing.assert_close(out[0].cpu(), gt, atol=ATOL, rtol=RTOL)
|
||||
|
||||
|
||||
def test_paged_buffer_intermediate() -> None:
|
||||
"""Paged-only: after a multi-page prefill, verify the trailing 4 tokens of
|
||||
every swa_page sit in the correct state-pool slots.
|
||||
|
||||
These slots are what radix-cache resume reads when prefix-matching from a
|
||||
swa_page boundary, so they MUST match the original token data.
|
||||
"""
|
||||
ctx = make_paged_context(
|
||||
bs=1,
|
||||
compress_ratio=RATIO,
|
||||
head_dim=HEAD_DIM,
|
||||
swa_page_size=256,
|
||||
ring_size=8,
|
||||
num_swa_pages_per_req=8,
|
||||
)
|
||||
seq_len = 1024 # 4 swa_pages
|
||||
seq_lens_cpu, extend_lens_cpu, num_q = to_seq_extend([(seq_len, seq_len)])
|
||||
kv_in_cpu, ape_cpu = _make_inputs(num_q, ctx.head_dim, seed=42)
|
||||
|
||||
pool = make_state_pool(ctx.num_pages, RATIO, ctx.head_dim)
|
||||
_run_prefill(
|
||||
ctx, pool, kv_in_cpu.cuda(), ape_cpu.cuda(), seq_lens_cpu, extend_lens_cpu
|
||||
)
|
||||
|
||||
pool_cpu = pool.cpu()
|
||||
# For each swa_page boundary, the trailing `RATIO` tokens must have been
|
||||
# written. The state slot for token at position p is
|
||||
# `state_loc(0, p) = (p // swa_page_size) * ring_size + p % ring_size`.
|
||||
for swa_page_end in range(ctx.swa_page_size, seq_len + 1, ctx.swa_page_size):
|
||||
for offset in range(RATIO):
|
||||
p = swa_page_end - RATIO + offset
|
||||
sl = ctx.state_loc(0, p)
|
||||
page_idx = sl // RATIO
|
||||
slot_idx = sl % RATIO
|
||||
actual = pool_cpu[page_idx, slot_idx]
|
||||
# Token-row layout: the c4 prefill write copies the full
|
||||
# head_dim*4 row from kv_input verbatim into the state pool.
|
||||
expected = kv_in_cpu[p]
|
||||
triton.testing.assert_close(
|
||||
actual,
|
||||
expected,
|
||||
atol=ATOL,
|
||||
rtol=RTOL,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["legacy", "paged"])
|
||||
def test_prefill_multibatch(mode: str) -> None:
|
||||
"""Multi-batch prefill, both modes."""
|
||||
seq_extend = [(8, 8), (256, 256), (260, 260), (1023, 1023)]
|
||||
bs = len(seq_extend)
|
||||
if mode == "legacy":
|
||||
ctx: Context = make_legacy_context(
|
||||
bs=bs, compress_ratio=RATIO, head_dim=HEAD_DIM
|
||||
)
|
||||
else:
|
||||
ctx = make_paged_context(bs=bs, compress_ratio=RATIO, head_dim=HEAD_DIM)
|
||||
|
||||
seq_lens_cpu, extend_lens_cpu, num_q = to_seq_extend(seq_extend)
|
||||
kv_in_cpu, ape_cpu = _make_inputs(num_q, ctx.head_dim, seed=99)
|
||||
pool = make_state_pool(ctx.num_pages, RATIO, ctx.head_dim)
|
||||
out = _run_prefill(
|
||||
ctx, pool, kv_in_cpu.cuda(), ape_cpu.cuda(), seq_lens_cpu, extend_lens_cpu
|
||||
)
|
||||
|
||||
# Compact: walk batches in order, then positions in order; matches the
|
||||
# CPU planner's emit order for plan_c.
|
||||
base = 0
|
||||
plan_id = 0
|
||||
for b, (seq, ext) in enumerate(seq_extend):
|
||||
for j in range(ext):
|
||||
P = j # prefix=0 here
|
||||
if (P + 1) % RATIO != 0:
|
||||
continue
|
||||
gt = _gt_compress(
|
||||
kv_in_cpu[base : base + ext],
|
||||
ape_cpu,
|
||||
P=P,
|
||||
head_dim=ctx.head_dim,
|
||||
)
|
||||
triton.testing.assert_close(
|
||||
out[plan_id].cpu(),
|
||||
gt,
|
||||
atol=ATOL,
|
||||
rtol=RTOL,
|
||||
)
|
||||
plan_id += 1
|
||||
base += ext
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -1,228 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.dsv4 import (
|
||||
CompressorDecodePlan,
|
||||
compress_norm_rope_store,
|
||||
fused_q_indexer_rope_hadamard_fp4_quant,
|
||||
)
|
||||
from sglang.jit_kernel.hadamard import hadamard_transform
|
||||
from sglang.srt.layers.attention.dsv4.fp4_indexer import (
|
||||
quantize_fp4_indexer_tensor,
|
||||
store_fp4_index_k_cache,
|
||||
)
|
||||
from sglang.srt.layers.deepseek_v4_rope import (
|
||||
apply_rotary_emb_triton,
|
||||
precompute_freqs_cis,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=60, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=60, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
HEAD_DIM = 128
|
||||
FP4_DIM = HEAD_DIM // 2
|
||||
GROUP_SIZE = 32
|
||||
SCALE_GROUPS = HEAD_DIM // GROUP_SIZE
|
||||
SCALE_BYTES = 4
|
||||
PAGE_SIZE = 64
|
||||
E2M1_MAX = 6.0
|
||||
|
||||
|
||||
def _ceil_ue8m0_exp_ref(x: torch.Tensor) -> torch.Tensor:
|
||||
bits = x.to(torch.float32).contiguous().view(torch.int32)
|
||||
exp = (bits >> 23) & 0xFF
|
||||
mantissa = bits & 0x7FFFFF
|
||||
exp = exp + (mantissa != 0).to(torch.int32)
|
||||
return exp.clamp(1, 254)
|
||||
|
||||
|
||||
def _fp4_e2m1_code_ref(x: torch.Tensor) -> torch.Tensor:
|
||||
ax = torch.minimum(x.abs(), torch.tensor(E2M1_MAX, device=x.device))
|
||||
idx = torch.zeros_like(ax, dtype=torch.uint8)
|
||||
for threshold in (0.25, 0.75, 1.25, 1.75, 2.5, 3.5, 5.0):
|
||||
idx += (ax > threshold).to(torch.uint8)
|
||||
sign = ((x < 0) & (idx != 0)).to(torch.uint8) * 8
|
||||
return idx | sign
|
||||
|
||||
|
||||
def _ref_quantize_fp4_indexer(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
x = x.contiguous().view(-1, HEAD_DIM).float()
|
||||
groups = x.view(-1, SCALE_GROUPS, GROUP_SIZE)
|
||||
scale_raw = (groups.abs().amax(dim=-1) / E2M1_MAX).clamp_min(1.0e-4)
|
||||
scale_exp = _ceil_ue8m0_exp_ref(scale_raw)
|
||||
scale = (scale_exp << 23).contiguous().view(torch.float32)
|
||||
|
||||
scaled = (groups / scale.unsqueeze(-1)).view(-1, HEAD_DIM)
|
||||
code = _fp4_e2m1_code_ref(scaled)
|
||||
packed = (code[:, 0::2].to(torch.int16) | (code[:, 1::2].to(torch.int16) << 4)).to(
|
||||
torch.uint8
|
||||
)
|
||||
|
||||
packed_sf = scale_exp[:, 0].clone()
|
||||
for group_id in range(1, SCALE_GROUPS):
|
||||
packed_sf |= scale_exp[:, group_id] << (8 * group_id)
|
||||
return packed, packed_sf
|
||||
|
||||
|
||||
def _ref_store_fp4_index_cache(
|
||||
x_fp4: torch.Tensor,
|
||||
x_sf: torch.Tensor,
|
||||
loc: torch.Tensor,
|
||||
num_pages: int,
|
||||
) -> torch.Tensor:
|
||||
expected = torch.zeros(
|
||||
num_pages,
|
||||
PAGE_SIZE * (FP4_DIM + SCALE_BYTES),
|
||||
device=x_fp4.device,
|
||||
dtype=torch.uint8,
|
||||
)
|
||||
sf_shifts = torch.arange(0, 32, 8, device=x_fp4.device, dtype=torch.int32)
|
||||
for token_id in range(x_fp4.shape[0]):
|
||||
cache_loc = int(loc[token_id].item())
|
||||
page = cache_loc // PAGE_SIZE
|
||||
offset = cache_loc % PAGE_SIZE
|
||||
expected[page, offset * FP4_DIM : (offset + 1) * FP4_DIM] = x_fp4[token_id]
|
||||
sf_start = PAGE_SIZE * FP4_DIM + offset * SCALE_BYTES
|
||||
expected[page, sf_start : sf_start + SCALE_BYTES] = (
|
||||
(x_sf[token_id] >> sf_shifts) & 0xFF
|
||||
).to(torch.uint8)
|
||||
return expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [1, 7, 96])
|
||||
def test_quantize_fp4_indexer_tensor(num_tokens: int) -> None:
|
||||
torch.manual_seed(num_tokens)
|
||||
x = torch.randn(num_tokens, HEAD_DIM, device="cuda", dtype=torch.bfloat16)
|
||||
x[0, :8] = torch.tensor(
|
||||
[-8.0, -6.0, -3.0, -1.5, 0.0, 0.5, 2.0, 8.0],
|
||||
device="cuda",
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
|
||||
x_fp4, x_sf = quantize_fp4_indexer_tensor(x)
|
||||
ref_fp4, ref_sf = _ref_quantize_fp4_indexer(x)
|
||||
|
||||
torch.testing.assert_close(x_fp4.view(torch.uint8), ref_fp4)
|
||||
torch.testing.assert_close(x_sf, ref_sf)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [1, 16, 96])
|
||||
def test_fp4_index_cache_store_layout(num_tokens: int) -> None:
|
||||
torch.manual_seed(num_tokens)
|
||||
num_pages = max(1, (num_tokens + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||
x = torch.randn(num_tokens, HEAD_DIM, device="cuda", dtype=torch.bfloat16)
|
||||
loc = torch.randperm(num_pages * PAGE_SIZE, device="cuda")[:num_tokens].to(
|
||||
torch.int64
|
||||
)
|
||||
cache = torch.zeros(
|
||||
num_pages,
|
||||
PAGE_SIZE * (FP4_DIM + SCALE_BYTES),
|
||||
device="cuda",
|
||||
dtype=torch.uint8,
|
||||
)
|
||||
|
||||
store_fp4_index_k_cache(x, cache, loc, page_size=PAGE_SIZE)
|
||||
|
||||
ref_fp4, ref_sf = _ref_quantize_fp4_indexer(x)
|
||||
expected = _ref_store_fp4_index_cache(ref_fp4, ref_sf, loc, num_pages)
|
||||
torch.testing.assert_close(cache, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [1, 16, 96])
|
||||
def test_fp4_fused_norm_rope_store_layout(num_tokens: int) -> None:
|
||||
torch.manual_seed(num_tokens + 100)
|
||||
num_pages = max(1, (num_tokens + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||
compress_ratio = 4
|
||||
kv = torch.randn(num_tokens, HEAD_DIM, device="cuda", dtype=torch.bfloat16)
|
||||
norm_weight = torch.randn(HEAD_DIM, device="cuda", dtype=torch.bfloat16)
|
||||
seq_lens = (
|
||||
torch.arange(1, num_tokens + 1, device="cuda", dtype=torch.int64)
|
||||
* compress_ratio
|
||||
)
|
||||
req_pool_indices = torch.arange(num_tokens, device="cuda", dtype=torch.int64)
|
||||
plan = CompressorDecodePlan.generate_legacy(
|
||||
compress_ratio, req_pool_indices, seq_lens
|
||||
)
|
||||
loc = torch.arange(num_tokens, device="cuda", dtype=torch.int32)
|
||||
freqs_cis = precompute_freqs_cis(
|
||||
64, int(seq_lens.max().item()) + 1, 0, 10000, 1, 32, 1
|
||||
).to("cuda")
|
||||
cache = torch.zeros(
|
||||
num_pages,
|
||||
PAGE_SIZE * (FP4_DIM + SCALE_BYTES),
|
||||
device="cuda",
|
||||
dtype=torch.uint8,
|
||||
)
|
||||
|
||||
compress_norm_rope_store(
|
||||
kv.clone(),
|
||||
plan,
|
||||
norm_weight=norm_weight,
|
||||
norm_eps=1.0e-6,
|
||||
freq_cis=freqs_cis,
|
||||
out_loc=loc,
|
||||
kvcache=cache,
|
||||
page_size=PAGE_SIZE,
|
||||
use_fp4=True,
|
||||
)
|
||||
|
||||
ref = kv.float()
|
||||
ref = ref * torch.rsqrt((ref * ref).sum(dim=-1, keepdim=True) / HEAD_DIM + 1.0e-6)
|
||||
ref = ref * norm_weight.float()
|
||||
freqs = torch.view_as_real(freqs_cis).flatten(-2)[
|
||||
(seq_lens - compress_ratio).long()
|
||||
]
|
||||
rope = ref[:, 64:].reshape(num_tokens, 32, 2)
|
||||
freqs = freqs.reshape(num_tokens, 32, 2)
|
||||
rope_out = torch.empty_like(rope)
|
||||
rope_out[..., 0] = rope[..., 0] * freqs[..., 0] - rope[..., 1] * freqs[..., 1]
|
||||
rope_out[..., 1] = rope[..., 0] * freqs[..., 1] + rope[..., 1] * freqs[..., 0]
|
||||
ref[:, 64:] = rope_out.reshape(num_tokens, 64)
|
||||
ref = hadamard_transform(ref.contiguous(), scale=HEAD_DIM**-0.5)
|
||||
ref_fp4, ref_sf = _ref_quantize_fp4_indexer(ref)
|
||||
|
||||
expected = _ref_store_fp4_index_cache(
|
||||
ref_fp4,
|
||||
ref_sf,
|
||||
loc.to(torch.int64),
|
||||
num_pages,
|
||||
)
|
||||
torch.testing.assert_close(cache, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 5, 17])
|
||||
def test_fp4_fused_q_indexer_rope_hadamard_quant(batch_size: int) -> None:
|
||||
torch.manual_seed(batch_size + 200)
|
||||
num_heads = 8
|
||||
rope_dim = 64
|
||||
weight_scale = HEAD_DIM**-0.5 * num_heads**-0.5
|
||||
q = torch.randn(
|
||||
batch_size, num_heads, HEAD_DIM, device="cuda", dtype=torch.bfloat16
|
||||
)
|
||||
weight = torch.randn(batch_size, num_heads, device="cuda", dtype=torch.bfloat16)
|
||||
positions = (torch.arange(batch_size, device="cuda", dtype=torch.int32) * 7) % 63
|
||||
freqs_cis = precompute_freqs_cis(rope_dim, 64, 0, 10000, 1, 32, 1).to("cuda")
|
||||
|
||||
(q_fp4, q_sf), weights_out = fused_q_indexer_rope_hadamard_fp4_quant(
|
||||
q, weight, weight_scale, freqs_cis, positions
|
||||
)
|
||||
|
||||
ref = q.clone()
|
||||
apply_rotary_emb_triton(ref[..., -rope_dim:], freqs_cis, positions=positions)
|
||||
ref = hadamard_transform(ref.contiguous(), scale=HEAD_DIM**-0.5)
|
||||
ref_fp4, ref_sf = _ref_quantize_fp4_indexer(ref.view(-1, HEAD_DIM))
|
||||
ref_fp4 = ref_fp4.view(batch_size, num_heads, FP4_DIM)
|
||||
ref_sf = ref_sf.view(batch_size, num_heads)
|
||||
|
||||
torch.testing.assert_close(q_fp4.view(torch.uint8), ref_fp4)
|
||||
torch.testing.assert_close(q_sf, ref_sf)
|
||||
torch.testing.assert_close(weights_out.squeeze(-1), weight.float() * weight_scale)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -1,142 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.modelopt_quant import (
|
||||
ModelOptFp8Config,
|
||||
ModelOptFp8LinearMethod,
|
||||
)
|
||||
from sglang.srt.layers.quantization.fp8_kernel import static_quant_fp8
|
||||
from sglang.srt.layers.quantization.fp8_utils import (
|
||||
cutlass_fp8_supported,
|
||||
input_to_float8,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=20, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=80, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.bfloat16
|
||||
MAX_FP8_DIFF = 5e-4
|
||||
TEST_CASES = [
|
||||
pytest.param(19, 150, 80, id="misaligned_projection_shape"),
|
||||
pytest.param(512, 3072, 4096, id="flux2_added_kv_projection_shape"),
|
||||
]
|
||||
|
||||
|
||||
def _modelopt_fp8_supported() -> bool:
|
||||
return torch.cuda.is_available() and cutlass_fp8_supported()
|
||||
|
||||
|
||||
def _calc_diff(x: torch.Tensor, y: torch.Tensor) -> float:
|
||||
x, y = x.double(), y.double()
|
||||
denominator = (x * x + y * y).sum()
|
||||
if denominator == 0:
|
||||
return 0.0
|
||||
sim = 2 * (x * y).sum() / denominator
|
||||
return (1 - sim).item()
|
||||
|
||||
|
||||
def _dequantize_fp8_input(qinput: torch.Tensor, x_scale: torch.Tensor) -> torch.Tensor:
|
||||
return qinput.to(torch.float32) * x_scale.to(torch.float32)
|
||||
|
||||
|
||||
def _dequantize_fp8_weight(
|
||||
weight: torch.Tensor, weight_scale: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
if weight_scale.ndim == 0 or weight_scale.numel() == 1:
|
||||
scale = weight_scale.to(torch.float32)
|
||||
else:
|
||||
scale = weight_scale.to(torch.float32).reshape(-1, 1).t()
|
||||
return weight.to(torch.float32) * scale
|
||||
|
||||
|
||||
def _build_layer(
|
||||
weight_q: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
input_scale: torch.Tensor,
|
||||
) -> tuple[torch.nn.Module, ModelOptFp8LinearMethod]:
|
||||
output_size, input_size = weight_q.shape
|
||||
method = ModelOptFp8LinearMethod(
|
||||
ModelOptFp8Config(is_checkpoint_fp8_serialized=True)
|
||||
)
|
||||
layer = torch.nn.Module()
|
||||
method.create_weights(
|
||||
layer=layer,
|
||||
input_size_per_partition=input_size,
|
||||
output_partition_sizes=[output_size],
|
||||
input_size=input_size,
|
||||
output_size=output_size,
|
||||
params_dtype=DTYPE,
|
||||
weight_loader=lambda *args, **kwargs: None,
|
||||
)
|
||||
layer = layer.to(device=DEVICE)
|
||||
|
||||
layer.weight.data.copy_(weight_q)
|
||||
layer.weight_scale.data.copy_(weight_scale.reshape_as(layer.weight_scale))
|
||||
layer.input_scale.data.copy_(input_scale.reshape_as(layer.input_scale))
|
||||
method.process_weights_after_loading(layer)
|
||||
return layer, method
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _modelopt_fp8_supported(),
|
||||
reason="Diffusion ModelOpt FP8 scaled mm correctness requires CUDA FP8 support",
|
||||
)
|
||||
@pytest.mark.parametrize("m,n,k", TEST_CASES)
|
||||
def test_checkpoint_processing(m: int, n: int, k: int) -> None:
|
||||
generator = torch.Generator(device=DEVICE)
|
||||
generator.manual_seed(20260410 + m + n + k)
|
||||
|
||||
weight = torch.randn((n, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
weight_q, weight_scale = input_to_float8(weight)
|
||||
input_scale = torch.tensor(1.0, device=DEVICE, dtype=torch.float32)
|
||||
|
||||
layer, _ = _build_layer(weight_q, weight_scale, input_scale)
|
||||
|
||||
assert tuple(layer.weight.shape) == (k, n)
|
||||
assert tuple(layer.weight.stride()) == (1, k)
|
||||
assert layer.weight.dtype == torch.float8_e4m3fn
|
||||
assert layer.input_scale.ndim == 0
|
||||
assert tuple(layer.weight_scale.shape) == (n, 1)
|
||||
|
||||
expected_weight = weight_q.t().to(torch.float32) * weight_scale.to(torch.float32)
|
||||
actual_weight = _dequantize_fp8_weight(layer.weight, layer.weight_scale)
|
||||
torch.testing.assert_close(actual_weight, expected_weight, atol=0.0, rtol=0.0)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _modelopt_fp8_supported(),
|
||||
reason="Diffusion ModelOpt FP8 scaled mm correctness requires CUDA FP8 support",
|
||||
)
|
||||
@pytest.mark.parametrize("m,n,k", TEST_CASES)
|
||||
def test_shape_correctness(m: int, n: int, k: int) -> None:
|
||||
generator = torch.Generator(device=DEVICE)
|
||||
generator.manual_seed(20260410 + m + n + k)
|
||||
|
||||
x = torch.randn((m, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
weight = torch.randn((n, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
weight_q, weight_scale = input_to_float8(weight)
|
||||
_, input_scale = input_to_float8(x)
|
||||
|
||||
layer, method = _build_layer(weight_q, weight_scale, input_scale)
|
||||
|
||||
qinput, x_scale = static_quant_fp8(
|
||||
x.contiguous(),
|
||||
layer.input_scale,
|
||||
repeat_scale=method.cutlass_fp8_supported,
|
||||
)
|
||||
expected = torch.matmul(
|
||||
_dequantize_fp8_input(qinput, x_scale),
|
||||
_dequantize_fp8_weight(layer.weight, layer.weight_scale),
|
||||
)
|
||||
|
||||
actual = method.apply(layer, x)
|
||||
diff = _calc_diff(actual, expected.to(dtype=DTYPE))
|
||||
assert diff < MAX_FP8_DIFF, f"{m=}, {n=}, {k=}, {diff=:.6f}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,460 +0,0 @@
|
||||
import sys
|
||||
|
||||
import flashinfer
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.nvfp4 import cutlass_scaled_fp4_mm, scaled_fp4_quant
|
||||
from sglang.multimodal_gen.runtime.layers.quantization import (
|
||||
modelopt_quant as diffusion_modelopt_quant,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.modelopt_quant import (
|
||||
ModelOptFp4Config,
|
||||
ModelOptFp4LinearMethod,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.srt.layers.quantization.modelopt_quant import pad_nvfp4_weight
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
# B200-only correctness coverage for diffusion NVFP4 scaled mm.
|
||||
register_cuda_ci(est_time=15, suite="base-b-kernel-unit-1-gpu-b200")
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.bfloat16
|
||||
BLOCK_SIZE = 16
|
||||
FLOAT4_E2M1_MAX = 6.0
|
||||
FLOAT8_E4M3_MAX = torch.finfo(torch.float8_e4m3fn).max
|
||||
FP4_VALUE_LUT = (0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0)
|
||||
DEEPGEMM_FP4_MAX_DIFF = 0.02
|
||||
TEST_CASES = [
|
||||
pytest.param(19, 150, 80, id="padding_regression"),
|
||||
pytest.param(512, 6144, 128, id="flux2_projection_shape"),
|
||||
]
|
||||
FLUX2_PROJECTION_SHAPE = (512, 6144, 128)
|
||||
|
||||
|
||||
def _nvfp4_supported() -> bool:
|
||||
return torch.cuda.is_available() and torch.cuda.get_device_capability() >= (10, 0)
|
||||
|
||||
|
||||
def _make_global_scale(x: torch.Tensor) -> torch.Tensor:
|
||||
max_abs = torch.amax(x.abs()).clamp_min_(1e-6)
|
||||
return (FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / max_abs).to(torch.float32)
|
||||
|
||||
|
||||
def _calc_diff(x: torch.Tensor, y: torch.Tensor) -> float:
|
||||
x, y = x.double(), y.double()
|
||||
denominator = (x * x + y * y).sum()
|
||||
if denominator == 0:
|
||||
return 0.0
|
||||
sim = 2 * (x * y).sum() / denominator
|
||||
return (1 - sim).item()
|
||||
|
||||
|
||||
def _swap_fp4_nibbles(packed: torch.Tensor) -> torch.Tensor:
|
||||
return ((packed >> 4) | (packed << 4)).contiguous()
|
||||
|
||||
|
||||
def _fp4_lut(device: torch.device) -> torch.Tensor:
|
||||
return torch.tensor(FP4_VALUE_LUT, dtype=torch.float32, device=device)
|
||||
|
||||
|
||||
def _unpack_fp4_bytes(packed: torch.Tensor) -> torch.Tensor:
|
||||
assert packed.dtype == torch.uint8
|
||||
lut = _fp4_lut(packed.device)
|
||||
|
||||
def _decode(nibbles: torch.Tensor) -> torch.Tensor:
|
||||
values = lut[(nibbles & 0x7).to(torch.long)]
|
||||
return torch.where((nibbles & 0x8) != 0, -values, values)
|
||||
|
||||
low = _decode(packed & 0x0F)
|
||||
high = _decode((packed & 0xF0) >> 4)
|
||||
return torch.stack((low, high), dim=-1).reshape(
|
||||
packed.shape[0], packed.shape[1] * 2
|
||||
)
|
||||
|
||||
|
||||
def _swizzled_to_linear(
|
||||
scales_swizzled: torch.Tensor,
|
||||
rows: int,
|
||||
cols: int,
|
||||
) -> torch.Tensor:
|
||||
scales_swizzled = scales_swizzled.view(torch.float8_e4m3fn)
|
||||
row_tiles = (rows + 128 - 1) // 128
|
||||
tile_cols = BLOCK_SIZE * 4
|
||||
col_tiles = (cols + tile_cols - 1) // tile_cols
|
||||
tmp = scales_swizzled.reshape(1, row_tiles, col_tiles, 32, 4, 4)
|
||||
tmp = tmp.permute(0, 1, 4, 3, 2, 5)
|
||||
linear = tmp.reshape(row_tiles * 128, col_tiles * tile_cols // BLOCK_SIZE)
|
||||
return linear[:rows, : cols // BLOCK_SIZE]
|
||||
|
||||
|
||||
def _dequantize_nvfp4(
|
||||
packed: torch.Tensor,
|
||||
scales_swizzled: torch.Tensor,
|
||||
global_scale: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
rows, packed_cols = packed.shape
|
||||
cols = packed_cols * 2
|
||||
unpacked = _unpack_fp4_bytes(packed).reshape(rows, cols // BLOCK_SIZE, BLOCK_SIZE)
|
||||
scales_linear = _swizzled_to_linear(scales_swizzled, rows, cols).to(torch.float32)
|
||||
return (unpacked * (scales_linear / global_scale).unsqueeze(-1)).reshape(rows, cols)
|
||||
|
||||
|
||||
def _quantize_weight_for_checkpoint(
|
||||
weight: torch.Tensor, weight_global_scale: torch.Tensor
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
weight_fp4, weight_scale_linear = flashinfer.fp4_quantize(
|
||||
weight,
|
||||
weight_global_scale,
|
||||
is_sf_swizzled_layout=False,
|
||||
)
|
||||
if weight_scale_linear.dtype == torch.uint8:
|
||||
weight_scale_linear = weight_scale_linear.view(torch.float8_e4m3fn)
|
||||
return weight_fp4, weight_scale_linear.contiguous()
|
||||
|
||||
|
||||
def _set_diffusion_fp4_backend(
|
||||
monkeypatch: pytest.MonkeyPatch, backend: str | None
|
||||
) -> None:
|
||||
if backend is None:
|
||||
monkeypatch.delenv(
|
||||
"SGLANG_DIFFUSION_FLASHINFER_FP4_GEMM_BACKEND", raising=False
|
||||
)
|
||||
else:
|
||||
monkeypatch.setenv("SGLANG_DIFFUSION_FLASHINFER_FP4_GEMM_BACKEND", backend)
|
||||
|
||||
current_platform.__class__.get_modelopt_flashinfer_fp4_backend.cache_clear()
|
||||
current_platform.__class__.get_modelopt_fp4_gemm_op.cache_clear()
|
||||
diffusion_modelopt_quant._get_fp4_gemm_op.cache_clear()
|
||||
|
||||
|
||||
def _build_layer(
|
||||
weight_fp4: torch.Tensor,
|
||||
weight_scale_linear: torch.Tensor,
|
||||
input_global_scale: torch.Tensor,
|
||||
weight_global_scale: torch.Tensor,
|
||||
*,
|
||||
weight_scale_device: torch.device | str | None = None,
|
||||
checkpoint_weight_scale_layout: str = "linear",
|
||||
) -> tuple[ModelOptFp4LinearMethod, torch.nn.Module]:
|
||||
output_size, input_size_half = weight_fp4.shape
|
||||
input_size = input_size_half * 2
|
||||
method = ModelOptFp4LinearMethod(
|
||||
ModelOptFp4Config(
|
||||
is_checkpoint_nvfp4_serialized=True,
|
||||
group_size=BLOCK_SIZE,
|
||||
swap_weight_nibbles=True,
|
||||
checkpoint_weight_scale_layout=checkpoint_weight_scale_layout,
|
||||
)
|
||||
)
|
||||
layer = torch.nn.Module()
|
||||
method.create_weights(
|
||||
layer,
|
||||
input_size_per_partition=input_size,
|
||||
output_partition_sizes=[output_size],
|
||||
input_size=input_size,
|
||||
output_size=output_size,
|
||||
params_dtype=DTYPE,
|
||||
weight_loader=lambda *args, **kwargs: None,
|
||||
)
|
||||
layer = layer.to(device=DEVICE)
|
||||
|
||||
checkpoint_weight = _swap_fp4_nibbles(weight_fp4)
|
||||
layer.weight.data.copy_(checkpoint_weight)
|
||||
layer.input_scale.data.copy_(
|
||||
(1.0 / input_global_scale).reshape_as(layer.input_scale)
|
||||
)
|
||||
layer.weight_scale_2.data.copy_(
|
||||
(1.0 / weight_global_scale).reshape_as(layer.weight_scale_2)
|
||||
)
|
||||
layer.weight_scale.data.copy_(weight_scale_linear)
|
||||
if weight_scale_device is not None:
|
||||
layer.weight_scale = torch.nn.Parameter(
|
||||
layer.weight_scale.detach().to(weight_scale_device), requires_grad=False
|
||||
)
|
||||
|
||||
method.process_weights_after_loading(layer)
|
||||
|
||||
_, flashinfer_backend = current_platform.get_modelopt_fp4_gemm_op()
|
||||
if flashinfer_backend == "trtllm":
|
||||
expected_weight, _ = pad_nvfp4_weight(
|
||||
weight_fp4, n_alignment=128, k_alignment=0
|
||||
)
|
||||
expected_scale = (
|
||||
_swizzled_to_linear(weight_scale_linear, output_size, input_size)
|
||||
if checkpoint_weight_scale_layout == "swizzled"
|
||||
else weight_scale_linear
|
||||
)
|
||||
if expected_scale.shape[0] != expected_weight.shape[0]:
|
||||
pad_n = expected_weight.shape[0] - expected_scale.shape[0]
|
||||
expected_scale = torch.nn.functional.pad(expected_scale, (0, 0, 0, pad_n))
|
||||
|
||||
expected_padding_cols = 0
|
||||
if expected_scale.shape[1] % 4 != 0:
|
||||
padded_scale_k = ((expected_scale.shape[1] + 4 - 1) // 4) * 4
|
||||
pad_scale_k = padded_scale_k - expected_scale.shape[1]
|
||||
expected_scale = torch.nn.functional.pad(
|
||||
expected_scale, (0, pad_scale_k, 0, 0)
|
||||
)
|
||||
pad_weight_k = pad_scale_k * 8
|
||||
expected_weight = torch.nn.functional.pad(
|
||||
expected_weight, (0, pad_weight_k, 0, 0)
|
||||
)
|
||||
expected_padding_cols = pad_weight_k
|
||||
|
||||
expected_weight = flashinfer.shuffle_matrix_a(
|
||||
expected_weight.view(torch.uint8), 128
|
||||
)
|
||||
expected_scale = (
|
||||
flashinfer.shuffle_matrix_sf_a(expected_scale.view(torch.uint8), 128)
|
||||
.reshape(expected_scale.shape)
|
||||
.view(torch.float8_e4m3fn)
|
||||
)
|
||||
|
||||
assert torch.equal(layer.weight, expected_weight)
|
||||
assert torch.equal(
|
||||
layer.weight_scale_interleaved.view(torch.uint8),
|
||||
expected_scale.view(torch.uint8),
|
||||
)
|
||||
assert layer.weights_padding_cols == expected_padding_cols
|
||||
else:
|
||||
expected_weight, expected_padding_cols = pad_nvfp4_weight(weight_fp4)
|
||||
expected_scale_shape = (
|
||||
((output_size + 128 - 1) // 128) * 128,
|
||||
(((input_size // BLOCK_SIZE) + 4 - 1) // 4) * 4,
|
||||
)
|
||||
|
||||
assert torch.equal(layer.weight, expected_weight)
|
||||
assert layer.weight_scale_interleaved.shape == expected_scale_shape
|
||||
assert layer.weight_scale_interleaved.dtype == torch.float8_e4m3fn
|
||||
assert layer.weights_padding_cols == expected_padding_cols
|
||||
torch.testing.assert_close(
|
||||
layer.alpha,
|
||||
(1.0 / (input_global_scale * weight_global_scale)).to(torch.float32),
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
layer.input_scale_inv,
|
||||
input_global_scale.to(torch.float32),
|
||||
)
|
||||
return method, layer
|
||||
|
||||
|
||||
def _resolve_mode(mode: str):
|
||||
if mode == "jit_cutlass":
|
||||
return scaled_fp4_quant, cutlass_scaled_fp4_mm, None
|
||||
if mode == "flashinfer2":
|
||||
return flashinfer.fp4_quantize, flashinfer.mm_fp4, "cudnn"
|
||||
if mode == "flashinfer_trtllm":
|
||||
return flashinfer.fp4_quantize, flashinfer.mm_fp4, "trtllm"
|
||||
raise ValueError(f"Unknown mode: {mode}")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(),
|
||||
reason="Diffusion NVFP4 scaled mm correctness requires Blackwell GPUs",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"backend", [None, "flashinfer_trtllm"], ids=["default", "flashinfer_trtllm"]
|
||||
)
|
||||
@pytest.mark.parametrize("m,n,k", TEST_CASES)
|
||||
def test_checkpoint_processing(
|
||||
monkeypatch: pytest.MonkeyPatch, backend: str | None, m: int, n: int, k: int
|
||||
) -> None:
|
||||
_set_diffusion_fp4_backend(monkeypatch, backend)
|
||||
generator = torch.Generator(device=DEVICE)
|
||||
generator.manual_seed(20260404 + m + n + k)
|
||||
|
||||
weight = torch.randn((n, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
input_global_scale = torch.tensor(512.0, device=DEVICE, dtype=torch.float32)
|
||||
weight_global_scale = _make_global_scale(weight)
|
||||
weight_fp4, weight_scale_linear = _quantize_weight_for_checkpoint(
|
||||
weight, weight_global_scale
|
||||
)
|
||||
|
||||
_build_layer(
|
||||
weight_fp4, weight_scale_linear, input_global_scale, weight_global_scale
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(),
|
||||
reason="Diffusion NVFP4 scaled mm correctness requires Blackwell GPUs",
|
||||
)
|
||||
@pytest.mark.parametrize("mode", ["jit_cutlass", "flashinfer2"])
|
||||
def test_flux2_shape_correctness(mode: str) -> None:
|
||||
m, n, k = FLUX2_PROJECTION_SHAPE
|
||||
quantize_op, gemm_op, gemm_backend = _resolve_mode(mode)
|
||||
generator = torch.Generator(device=DEVICE)
|
||||
generator.manual_seed(20260404 + m + n + k)
|
||||
|
||||
x = torch.randn((m, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
weight = torch.randn((n, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
input_global_scale = _make_global_scale(x)
|
||||
weight_global_scale = _make_global_scale(weight)
|
||||
alpha = (1.0 / (input_global_scale * weight_global_scale)).to(torch.float32)
|
||||
|
||||
x_fp4, x_scale_swizzled = quantize_op(x, input_global_scale)
|
||||
weight_fp4, weight_scale_swizzled = quantize_op(weight, weight_global_scale)
|
||||
if x_scale_swizzled.dtype == torch.uint8:
|
||||
x_scale_swizzled = x_scale_swizzled.view(torch.float8_e4m3fn)
|
||||
if weight_scale_swizzled.dtype == torch.uint8:
|
||||
weight_scale_swizzled = weight_scale_swizzled.view(torch.float8_e4m3fn)
|
||||
|
||||
expected = torch.matmul(
|
||||
_dequantize_nvfp4(x_fp4, x_scale_swizzled, input_global_scale),
|
||||
_dequantize_nvfp4(weight_fp4, weight_scale_swizzled, weight_global_scale).t(),
|
||||
)
|
||||
|
||||
if gemm_backend is None:
|
||||
actual = gemm_op(
|
||||
x_fp4,
|
||||
weight_fp4,
|
||||
x_scale_swizzled,
|
||||
weight_scale_swizzled,
|
||||
alpha,
|
||||
DTYPE,
|
||||
)
|
||||
else:
|
||||
actual = gemm_op(
|
||||
x_fp4,
|
||||
weight_fp4.t(),
|
||||
x_scale_swizzled,
|
||||
weight_scale_swizzled.t(),
|
||||
alpha,
|
||||
DTYPE,
|
||||
backend=gemm_backend,
|
||||
)
|
||||
|
||||
diff = _calc_diff(actual, expected.to(dtype=DTYPE))
|
||||
assert diff < DEEPGEMM_FP4_MAX_DIFF, f"{mode=}, {m=}, {n=}, {k=}, {diff=:.6f}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(),
|
||||
reason="Diffusion NVFP4 scaled mm correctness requires Blackwell GPUs",
|
||||
)
|
||||
def test_flux2_shape_correctness_flashinfer_trtllm(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_set_diffusion_fp4_backend(monkeypatch, "flashinfer_trtllm")
|
||||
|
||||
m, n, k = FLUX2_PROJECTION_SHAPE
|
||||
generator = torch.Generator(device=DEVICE)
|
||||
generator.manual_seed(20260404 + m + n + k + 17)
|
||||
|
||||
x = torch.randn((m, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
weight = torch.randn((n, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
input_global_scale = _make_global_scale(x)
|
||||
weight_global_scale = _make_global_scale(weight)
|
||||
weight_fp4, weight_scale_linear = _quantize_weight_for_checkpoint(
|
||||
weight, weight_global_scale
|
||||
)
|
||||
|
||||
method, layer = _build_layer(
|
||||
weight_fp4, weight_scale_linear, input_global_scale, weight_global_scale
|
||||
)
|
||||
actual = method.apply(layer, x)
|
||||
|
||||
x_fp4, x_scale_swizzled = flashinfer.fp4_quantize(x, input_global_scale)
|
||||
weight_fp4_ref, weight_scale_swizzled = flashinfer.fp4_quantize(
|
||||
weight, weight_global_scale
|
||||
)
|
||||
if x_scale_swizzled.dtype == torch.uint8:
|
||||
x_scale_swizzled = x_scale_swizzled.view(torch.float8_e4m3fn)
|
||||
if weight_scale_swizzled.dtype == torch.uint8:
|
||||
weight_scale_swizzled = weight_scale_swizzled.view(torch.float8_e4m3fn)
|
||||
|
||||
expected = torch.matmul(
|
||||
_dequantize_nvfp4(x_fp4, x_scale_swizzled, input_global_scale),
|
||||
_dequantize_nvfp4(
|
||||
weight_fp4_ref, weight_scale_swizzled, weight_global_scale
|
||||
).t(),
|
||||
)
|
||||
|
||||
diff = _calc_diff(actual, expected.to(dtype=DTYPE))
|
||||
assert diff < DEEPGEMM_FP4_MAX_DIFF, f"{m=}, {n=}, {k=}, {diff=:.6f}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(),
|
||||
reason="Diffusion NVFP4 scaled mm correctness requires Blackwell GPUs",
|
||||
)
|
||||
def test_flux2_swizzled_scale_checkpoint_flashinfer_trtllm_matches_cudnn(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_set_diffusion_fp4_backend(monkeypatch, "flashinfer_trtllm")
|
||||
|
||||
m, n, k = FLUX2_PROJECTION_SHAPE
|
||||
generator = torch.Generator(device=DEVICE)
|
||||
generator.manual_seed(20260517 + m + n + k)
|
||||
|
||||
x = torch.randn((m, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
weight = torch.randn((n, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
input_global_scale = _make_global_scale(x)
|
||||
weight_global_scale = _make_global_scale(weight)
|
||||
alpha = (1.0 / (input_global_scale * weight_global_scale)).to(torch.float32)
|
||||
|
||||
x_fp4, x_scale_swizzled = flashinfer.fp4_quantize(x, input_global_scale)
|
||||
weight_fp4, weight_scale_swizzled = flashinfer.fp4_quantize(
|
||||
weight, weight_global_scale
|
||||
)
|
||||
if x_scale_swizzled.dtype == torch.uint8:
|
||||
x_scale_swizzled = x_scale_swizzled.view(torch.float8_e4m3fn)
|
||||
if weight_scale_swizzled.dtype == torch.uint8:
|
||||
weight_scale_swizzled = weight_scale_swizzled.view(torch.float8_e4m3fn)
|
||||
|
||||
method, layer = _build_layer(
|
||||
weight_fp4,
|
||||
weight_scale_swizzled,
|
||||
input_global_scale,
|
||||
weight_global_scale,
|
||||
checkpoint_weight_scale_layout="swizzled",
|
||||
)
|
||||
actual = method.apply(layer, x)
|
||||
|
||||
expected = flashinfer.mm_fp4(
|
||||
x_fp4,
|
||||
weight_fp4.t(),
|
||||
x_scale_swizzled,
|
||||
weight_scale_swizzled.t(),
|
||||
alpha,
|
||||
DTYPE,
|
||||
backend="cudnn",
|
||||
)
|
||||
|
||||
diff = _calc_diff(actual, expected)
|
||||
assert diff < DEEPGEMM_FP4_MAX_DIFF, f"{m=}, {n=}, {k=}, {diff=:.6f}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(),
|
||||
reason="Diffusion NVFP4 scaled mm correctness requires Blackwell GPUs",
|
||||
)
|
||||
def test_checkpoint_processing_flashinfer_trtllm_cpu_weight_scale(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_set_diffusion_fp4_backend(monkeypatch, "flashinfer_trtllm")
|
||||
|
||||
m, n, k = FLUX2_PROJECTION_SHAPE
|
||||
generator = torch.Generator(device=DEVICE)
|
||||
generator.manual_seed(20260413 + m + n + k)
|
||||
|
||||
weight = torch.randn((n, k), device=DEVICE, dtype=DTYPE, generator=generator)
|
||||
input_global_scale = torch.tensor(512.0, device=DEVICE, dtype=torch.float32)
|
||||
weight_global_scale = _make_global_scale(weight)
|
||||
weight_fp4, weight_scale_linear = _quantize_weight_for_checkpoint(
|
||||
weight, weight_global_scale
|
||||
)
|
||||
|
||||
_build_layer(
|
||||
weight_fp4,
|
||||
weight_scale_linear,
|
||||
input_global_scale,
|
||||
weight_global_scale,
|
||||
weight_scale_device="cpu",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,133 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.test.ci.ci_register import register_amd_ci
|
||||
|
||||
register_amd_ci(est_time=30, suite="jit-kernel-unit-test-amd")
|
||||
|
||||
DEVICE = "cuda"
|
||||
D = 5120
|
||||
EPS = 1e-6
|
||||
|
||||
|
||||
def _ref_rms_norm(x_f32, weight, eps):
|
||||
var = x_f32.pow(2).mean(-1, keepdim=True)
|
||||
return x_f32 * torch.rsqrt(var + eps)
|
||||
|
||||
|
||||
def _ref_fused_residual_norm_ss(
|
||||
residual, x, gate, weight, bias, scale, shift, norm_type, eps
|
||||
):
|
||||
ref_res = residual.float() + x.float() * (gate.float() if gate is not None else 1)
|
||||
ref_res_bf16 = ref_res.to(torch.bfloat16)
|
||||
if norm_type == "layer":
|
||||
normed = F.layer_norm(ref_res_bf16.float(), (D,), weight, bias, eps)
|
||||
else:
|
||||
normed = _ref_rms_norm(ref_res_bf16.float(), weight, eps) * weight.float()
|
||||
y = (normed * (1.0 + scale.float()) + shift.float()).to(torch.bfloat16)
|
||||
return y, ref_res_bf16
|
||||
|
||||
|
||||
def _ref_norm_ss(x, weight, bias, scale, shift, norm_type, eps):
|
||||
if norm_type == "layer":
|
||||
normed = F.layer_norm(x.float(), (D,), weight, bias, eps)
|
||||
else:
|
||||
normed = _ref_rms_norm(x.float(), weight, eps) * weight.float()
|
||||
return (normed * (1.0 + scale.float()) + shift.float()).to(torch.bfloat16)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cuda_setup():
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required")
|
||||
if not hasattr(torch.version, "hip") or not torch.version.hip:
|
||||
pytest.skip("ROCm/HIP required for FlyDSL kernels")
|
||||
torch.manual_seed(42)
|
||||
|
||||
|
||||
FUSED_CASES = [
|
||||
("rms", 1, 16),
|
||||
("rms", 2, 16),
|
||||
("layer", 2, 16),
|
||||
("rms", 1, 90000),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("norm_type,B,L", FUSED_CASES)
|
||||
def test_fused_residual_norm_scale_shift(norm_type, B, L):
|
||||
from sglang.jit_kernel.diffusion.flydsl.fused_residual_norm import (
|
||||
flydsl_fused_residual_norm_scale_shift,
|
||||
)
|
||||
|
||||
residual = torch.randn(B, L, D, device=DEVICE, dtype=torch.bfloat16)
|
||||
x = torch.randn(B, L, D, device=DEVICE, dtype=torch.bfloat16)
|
||||
gate = torch.randn(B, 1, D, device=DEVICE, dtype=torch.bfloat16)
|
||||
weight = torch.randn(D, device=DEVICE, dtype=torch.float32)
|
||||
bias = (
|
||||
torch.randn(D, device=DEVICE, dtype=torch.float32)
|
||||
if norm_type == "layer"
|
||||
else None
|
||||
)
|
||||
scale = torch.randn(B, 1, D, device=DEVICE, dtype=torch.bfloat16)
|
||||
shift = torch.randn(B, 1, D, device=DEVICE, dtype=torch.bfloat16)
|
||||
|
||||
y, res_out = flydsl_fused_residual_norm_scale_shift(
|
||||
residual,
|
||||
x,
|
||||
gate,
|
||||
weight,
|
||||
bias,
|
||||
scale,
|
||||
shift,
|
||||
norm_type,
|
||||
EPS,
|
||||
)
|
||||
y_ref, res_ref = _ref_fused_residual_norm_ss(
|
||||
residual,
|
||||
x,
|
||||
gate,
|
||||
weight,
|
||||
bias,
|
||||
scale,
|
||||
shift,
|
||||
norm_type,
|
||||
EPS,
|
||||
)
|
||||
torch.testing.assert_close(res_out, res_ref, atol=5e-2, rtol=5e-2)
|
||||
torch.testing.assert_close(y, y_ref, atol=1.0, rtol=5e-2)
|
||||
|
||||
|
||||
NSS_CASES = [
|
||||
("rms", 2, 16),
|
||||
("layer", 2, 16),
|
||||
("rms", 1, 90000),
|
||||
("layer", 1, 90000),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("norm_type,B,L", NSS_CASES)
|
||||
def test_norm_scale_shift(norm_type, B, L):
|
||||
from sglang.jit_kernel.diffusion.flydsl.fused_residual_norm import (
|
||||
flydsl_norm_scale_shift,
|
||||
)
|
||||
|
||||
x = torch.randn(B, L, D, device=DEVICE, dtype=torch.bfloat16)
|
||||
weight = torch.randn(D, device=DEVICE, dtype=torch.float32)
|
||||
bias = (
|
||||
torch.randn(D, device=DEVICE, dtype=torch.float32)
|
||||
if norm_type == "layer"
|
||||
else None
|
||||
)
|
||||
scale = torch.randn(B, 1, D, device=DEVICE, dtype=torch.bfloat16)
|
||||
shift = torch.randn(B, 1, D, device=DEVICE, dtype=torch.bfloat16)
|
||||
|
||||
y = flydsl_norm_scale_shift(x, weight, bias, scale, shift, norm_type, EPS)
|
||||
y_ref = _ref_norm_ss(x, weight, bias, scale, shift, norm_type, EPS)
|
||||
torch.testing.assert_close(y, y_ref, atol=1.0, rtol=5e-2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,252 +0,0 @@
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from einops import rearrange
|
||||
from torch import Tensor
|
||||
|
||||
from sglang.jit_kernel.diffusion.cutedsl.scale_residual_norm_scale_shift import (
|
||||
fused_norm_scale_shift,
|
||||
fused_scale_residual_norm_scale_shift,
|
||||
validate_scale_shift,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=28, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
DEVICE = "cuda"
|
||||
SHAPE_MAP = {
|
||||
"1": lambda B, S, F, D: (1,),
|
||||
"D": lambda B, S, F, D: (D,),
|
||||
"1D": lambda B, S, F, D: (1, D),
|
||||
"BD": lambda B, S, F, D: (B, D),
|
||||
"11D": lambda B, S, F, D: (1, 1, D),
|
||||
"B1D": lambda B, S, F, D: (B, 1, D),
|
||||
"1SD": lambda B, S, F, D: (1, S, D),
|
||||
"BSD": lambda B, S, F, D: (B, S, D),
|
||||
"BF1D": lambda B, S, F, D: (B, F, 1, D),
|
||||
}
|
||||
SHAPES = [
|
||||
# (B, S, F, D)
|
||||
(1, 115200, 1, 3072), # Hunyuan
|
||||
(1, 32760, 1, 1536), # Wan
|
||||
(1, 6, 1, 3072), # Qwen
|
||||
(1, 1024, 8, 3072),
|
||||
(4, 512, 16, 3072),
|
||||
]
|
||||
DTYPES = [torch.float16, torch.bfloat16, torch.float32]
|
||||
NORM_TYPES = ["layer", "rms"]
|
||||
AFFINE_MODES = ["D", "NAT"]
|
||||
INDEX_MODES = ["BSD", "1", "1SD", "BD", "B1D", "D", "1D", "11D", "BF1D"]
|
||||
|
||||
|
||||
def _tol(dtype: torch.dtype):
|
||||
return 1e-5 if dtype == torch.float32 else 5e-2
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cuda_setup():
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required")
|
||||
torch.cuda.manual_seed(0)
|
||||
|
||||
|
||||
def _apply_scale_shift(y: Tensor, scale: Tensor, shift: Tensor) -> Tensor:
|
||||
if scale.ndim == 4:
|
||||
num_frame = scale.shape[1]
|
||||
return rearrange(
|
||||
rearrange(y, "b (f l) d -> b f l d", f=num_frame) * (1 + scale) + shift,
|
||||
"b f l d -> b (f l) d",
|
||||
)
|
||||
else:
|
||||
scale = rearrange(scale, "b d -> b 1 d") if scale.ndim == 2 else scale
|
||||
shift = rearrange(shift, "b d -> b 1 d") if shift.ndim == 2 else shift
|
||||
return y * (1 + scale) + shift
|
||||
|
||||
|
||||
def fused_norm_scale_shift_ref(
|
||||
x: Tensor,
|
||||
weight: Optional[Tensor],
|
||||
bias: Optional[Tensor],
|
||||
scale: Tensor,
|
||||
shift: Tensor,
|
||||
norm_type: str,
|
||||
eps: float,
|
||||
) -> Tensor:
|
||||
original_dtype = x.dtype
|
||||
x, weight, bias, scale, shift = (
|
||||
v.float() if v is not None else v for v in [x, weight, bias, scale, shift]
|
||||
)
|
||||
if norm_type == "layer":
|
||||
norm = torch.layer_norm(x, x.shape[-1:], eps=eps, weight=weight, bias=bias)
|
||||
else:
|
||||
norm = torch.rms_norm(x, x.shape[-1:], eps=eps, weight=weight)
|
||||
return _apply_scale_shift(norm, scale, shift).to(original_dtype)
|
||||
|
||||
|
||||
def fused_scale_residual_norm_scale_shift_ref(
|
||||
residual: Tensor,
|
||||
x: Tensor,
|
||||
gate: Optional[Tensor] | int,
|
||||
weight: Optional[Tensor],
|
||||
bias: Optional[Tensor],
|
||||
scale: Tensor,
|
||||
shift: Tensor,
|
||||
norm_type: str,
|
||||
eps: float,
|
||||
):
|
||||
original_dtype = x.dtype
|
||||
residual, x, gate, weight, bias, scale, shift = (
|
||||
v.float() if isinstance(v, Tensor) else v
|
||||
for v in [residual, x, gate, weight, bias, scale, shift]
|
||||
)
|
||||
if isinstance(gate, int):
|
||||
x = residual + gate * x
|
||||
else:
|
||||
if gate.ndim == 4:
|
||||
num_frame = gate.shape[1]
|
||||
x_fld = rearrange(x, "b (f l) d -> b f l d", f=num_frame)
|
||||
x = residual + rearrange(x_fld * gate, "b f l d -> b (f l) d")
|
||||
else:
|
||||
gate = rearrange(gate, "b d -> b 1 d") if gate.ndim == 2 else gate
|
||||
x = residual + gate * x
|
||||
if norm_type == "layer":
|
||||
norm = torch.layer_norm(x, x.shape[-1:], eps=eps, weight=weight, bias=bias)
|
||||
else:
|
||||
norm = torch.rms_norm(x, x.shape[-1:], eps=eps, weight=weight)
|
||||
y_ref = _apply_scale_shift(norm, scale, shift)
|
||||
return y_ref.to(original_dtype), x.to(original_dtype)
|
||||
|
||||
|
||||
def _make_tensor(index_mode: str, shape: Tuple, dtype: torch.dtype):
|
||||
if index_mode == "NAT":
|
||||
return None
|
||||
return torch.randn(*SHAPE_MAP[index_mode](*shape), device=DEVICE, dtype=dtype)
|
||||
|
||||
|
||||
def test_validate_scale_shift_rejects_non_divisible_frames():
|
||||
with pytest.raises(ValueError, match=r"S\(10\) must be divisible by F\(4\)"):
|
||||
validate_scale_shift(
|
||||
torch.empty((1, 4, 1, 256), device=DEVICE, dtype=torch.float16),
|
||||
1,
|
||||
10,
|
||||
256,
|
||||
)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def run_norm_scale_shift(
|
||||
shape=SHAPES[0],
|
||||
dtype=DTYPES[0],
|
||||
affine_dtype=DTYPES[0],
|
||||
scale_dtype=DTYPES[0],
|
||||
shift_dtype=DTYPES[0],
|
||||
norm_type=NORM_TYPES[0],
|
||||
affine_mode=AFFINE_MODES[0],
|
||||
scale_mode="BSD",
|
||||
shift_mode="BSD",
|
||||
eps=1e-5,
|
||||
):
|
||||
x = _make_tensor("BSD", shape, dtype)
|
||||
weight = _make_tensor(affine_mode, shape, affine_dtype)
|
||||
bias = _make_tensor(affine_mode, shape, affine_dtype)
|
||||
scale = _make_tensor(scale_mode, shape, scale_dtype)
|
||||
shift = _make_tensor(shift_mode, shape, shift_dtype)
|
||||
y_dev = fused_norm_scale_shift(x, weight, bias, scale, shift, norm_type, eps)
|
||||
y_ref = fused_norm_scale_shift_ref(x, weight, bias, scale, shift, norm_type, eps)
|
||||
torch.testing.assert_close(y_dev, y_ref, atol=_tol(dtype), rtol=_tol(dtype))
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def run_scale_resi_norm_scale_shift(
|
||||
shape=SHAPES[0],
|
||||
dtype=DTYPES[0],
|
||||
affine_dtype=DTYPES[0],
|
||||
scale_dtype=DTYPES[0],
|
||||
shift_dtype=DTYPES[0],
|
||||
norm_type=NORM_TYPES[0],
|
||||
affine_mode=AFFINE_MODES[0],
|
||||
gate_mode="B1D",
|
||||
scale_mode="BSD",
|
||||
shift_mode="BSD",
|
||||
eps=1e-5,
|
||||
):
|
||||
residual = _make_tensor("BSD", shape, dtype)
|
||||
x = _make_tensor("BSD", shape, dtype)
|
||||
gate = _make_tensor(gate_mode, shape, dtype)
|
||||
weight = _make_tensor(affine_mode, shape, affine_dtype)
|
||||
bias = _make_tensor(affine_mode, shape, affine_dtype)
|
||||
scale = _make_tensor(scale_mode, shape, scale_dtype)
|
||||
shift = _make_tensor(shift_mode, shape, shift_dtype)
|
||||
y_dev, res_dev = fused_scale_residual_norm_scale_shift(
|
||||
residual, x, gate, weight, bias, scale, shift, norm_type, eps
|
||||
)
|
||||
y_ref, res_ref = fused_scale_residual_norm_scale_shift_ref(
|
||||
residual, x, gate, weight, bias, scale, shift, norm_type, eps
|
||||
)
|
||||
torch.testing.assert_close(y_dev, y_ref, atol=_tol(dtype), rtol=_tol(dtype))
|
||||
torch.testing.assert_close(res_dev, res_ref, atol=_tol(dtype), rtol=_tol(dtype))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("norm_type", NORM_TYPES)
|
||||
class TestFusedNormScaleShift:
|
||||
@pytest.mark.parametrize("shape", SHAPES)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_shape_dtype(self, shape, dtype, norm_type):
|
||||
run_norm_scale_shift(shape=shape, dtype=dtype, norm_type=norm_type)
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_dtype_0(self, dtype, norm_type):
|
||||
run_norm_scale_shift(affine_dtype=dtype, norm_type=norm_type)
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_dtype_1(self, dtype, norm_type):
|
||||
run_norm_scale_shift(scale_dtype=dtype, shift_dtype=dtype, norm_type=norm_type)
|
||||
|
||||
@pytest.mark.parametrize("affine_mode", AFFINE_MODES)
|
||||
def test_normtype_affine(self, affine_mode, norm_type):
|
||||
run_norm_scale_shift(affine_mode=affine_mode, norm_type=norm_type)
|
||||
|
||||
@pytest.mark.parametrize("index_mode", INDEX_MODES)
|
||||
def test_index_mode(self, index_mode, norm_type):
|
||||
run_norm_scale_shift(
|
||||
scale_mode=index_mode, shift_mode=index_mode, norm_type=norm_type
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("norm_type", NORM_TYPES)
|
||||
class TestFusedScaleResidualNormScaleShift:
|
||||
@pytest.mark.parametrize("shape", SHAPES)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_shape_dtype(self, shape, dtype, norm_type):
|
||||
run_scale_resi_norm_scale_shift(shape=shape, dtype=dtype, norm_type=norm_type)
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_dtype_0(self, dtype, norm_type):
|
||||
run_scale_resi_norm_scale_shift(affine_dtype=dtype, norm_type=norm_type)
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_dtype_1(self, dtype, norm_type):
|
||||
run_scale_resi_norm_scale_shift(
|
||||
scale_dtype=dtype, shift_dtype=dtype, norm_type=norm_type
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("affine_mode", AFFINE_MODES)
|
||||
def test_normtype_affine(self, affine_mode, norm_type):
|
||||
run_scale_resi_norm_scale_shift(affine_mode=affine_mode, norm_type=norm_type)
|
||||
|
||||
@pytest.mark.parametrize("index_mode", INDEX_MODES)
|
||||
def test_scale_shift_index_mode(self, index_mode, norm_type):
|
||||
run_scale_resi_norm_scale_shift(
|
||||
scale_mode=index_mode, shift_mode=index_mode, norm_type=norm_type
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("index_mode", INDEX_MODES)
|
||||
def test_gate_index_mode(self, index_mode, norm_type):
|
||||
run_scale_resi_norm_scale_shift(gate_mode=index_mode, norm_type=norm_type)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,104 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.jit_kernel.diffusion.group_norm_silu import apply_group_norm_silu
|
||||
from sglang.jit_kernel.diffusion.triton.group_norm_silu import triton_group_norm_silu
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=8, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPES = [torch.float16, torch.bfloat16, torch.float32]
|
||||
TEST_CASES = [
|
||||
pytest.param((2, 64, 32, 32), 32, id="image_2d"),
|
||||
pytest.param((1, 64, 4, 16, 16), 32, id="video_3d"),
|
||||
pytest.param((4, 128), 32, id="token_2d"),
|
||||
]
|
||||
LARGE_TILE_CASE = ((1, 128, 20, 256, 256), 32)
|
||||
|
||||
|
||||
def _tol(dtype: torch.dtype) -> tuple[float, float]:
|
||||
if dtype == torch.float32:
|
||||
return 1e-5, 1e-5
|
||||
if dtype == torch.bfloat16:
|
||||
return 7e-2, 2e-2
|
||||
return 3e-3, 3e-3
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cuda_setup():
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required")
|
||||
torch.cuda.manual_seed(0)
|
||||
|
||||
|
||||
def _reference(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
bias: torch.Tensor,
|
||||
num_groups: int,
|
||||
eps: float = 1e-5,
|
||||
) -> torch.Tensor:
|
||||
return F.silu(F.group_norm(x, num_groups, weight=weight, bias=bias, eps=eps))
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
@pytest.mark.parametrize("shape,num_groups", TEST_CASES)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_triton_group_norm_silu(
|
||||
shape: tuple[int, ...], num_groups: int, dtype: torch.dtype
|
||||
) -> None:
|
||||
channels = shape[1]
|
||||
x = torch.randn(shape, device=DEVICE, dtype=dtype)
|
||||
weight = torch.randn(channels, device=DEVICE, dtype=dtype)
|
||||
bias = torch.randn(channels, device=DEVICE, dtype=dtype)
|
||||
|
||||
actual = triton_group_norm_silu(x, weight, bias, num_groups=num_groups)
|
||||
expected = _reference(x, weight, bias, num_groups)
|
||||
|
||||
atol, rtol = _tol(dtype)
|
||||
torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
@pytest.mark.parametrize("shape,num_groups", TEST_CASES[:2])
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
def test_apply_group_norm_silu(
|
||||
shape: tuple[int, ...],
|
||||
num_groups: int,
|
||||
dtype: torch.dtype,
|
||||
) -> None:
|
||||
norm = nn.GroupNorm(num_groups, shape[1], eps=1e-5, affine=True).to(
|
||||
device=DEVICE, dtype=dtype
|
||||
)
|
||||
activation = nn.SiLU()
|
||||
hidden_states = torch.randn(shape, device=DEVICE, dtype=dtype)
|
||||
|
||||
actual = apply_group_norm_silu(hidden_states, norm, activation)
|
||||
expected = activation(norm(hidden_states))
|
||||
|
||||
atol, rtol = _tol(dtype)
|
||||
torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def test_triton_group_norm_silu_large_tile_bf16() -> None:
|
||||
shape, num_groups = LARGE_TILE_CASE
|
||||
x = torch.randn(shape, device=DEVICE, dtype=torch.bfloat16)
|
||||
weight = torch.randn(shape[1], device=DEVICE, dtype=torch.bfloat16)
|
||||
bias = torch.randn(shape[1], device=DEVICE, dtype=torch.bfloat16)
|
||||
|
||||
actual = triton_group_norm_silu(x, weight, bias, num_groups=num_groups)
|
||||
expected = _reference(x, weight, bias, num_groups)
|
||||
|
||||
atol, rtol = _tol(torch.bfloat16)
|
||||
torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,153 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=44, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=176, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.bfloat16
|
||||
MAX_SEQ_LEN = 131072
|
||||
ROPE_BASE = 10000.0
|
||||
ATOL = 8e-2
|
||||
RTOL = 1e-2
|
||||
|
||||
|
||||
def create_cos_sin_cache(
|
||||
rotary_dim: int,
|
||||
max_position: int = MAX_SEQ_LEN,
|
||||
base: float = ROPE_BASE,
|
||||
) -> torch.Tensor:
|
||||
inv_freq = 1.0 / (
|
||||
base
|
||||
** (
|
||||
torch.arange(0, rotary_dim, 2, dtype=torch.float32, device=DEVICE)
|
||||
/ rotary_dim
|
||||
)
|
||||
)
|
||||
t = torch.arange(max_position, dtype=torch.float32, device=DEVICE)
|
||||
freqs = torch.einsum("i,j->ij", t, inv_freq)
|
||||
return torch.cat((freqs.cos(), freqs.sin()), dim=-1)
|
||||
|
||||
|
||||
def split_qknorm_rope(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
is_neox: bool,
|
||||
) -> None:
|
||||
from flashinfer.rope import apply_rope_with_cos_sin_cache_inplace
|
||||
|
||||
from sglang.jit_kernel.norm import fused_inplace_qknorm
|
||||
|
||||
fused_inplace_qknorm(q, k, q_weight, k_weight)
|
||||
apply_rope_with_cos_sin_cache_inplace(
|
||||
positions=positions.long(),
|
||||
query=q.view(q.shape[0], -1),
|
||||
key=k.view(k.shape[0], -1),
|
||||
head_size=q.shape[-1],
|
||||
cos_sin_cache=cos_sin_cache,
|
||||
is_neox=is_neox,
|
||||
)
|
||||
|
||||
|
||||
def fused_qknorm_rope(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
is_neox: bool,
|
||||
) -> None:
|
||||
from sglang.jit_kernel.diffusion.qknorm_rope import fused_inplace_qknorm_rope
|
||||
|
||||
fused_inplace_qknorm_rope(
|
||||
q,
|
||||
k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
cos_sin_cache,
|
||||
positions,
|
||||
is_neox=is_neox,
|
||||
rope_dim=cos_sin_cache.shape[-1],
|
||||
)
|
||||
|
||||
|
||||
BS_LIST = [2**n for n in range(13)]
|
||||
BS_LIST += [x + 1 for x in BS_LIST]
|
||||
BS_LIST = get_ci_test_range(BS_LIST, [1, 9, 129, 257, 2049, 4097])
|
||||
HEADS_LIST = get_ci_test_range([8, 16, 24, 32], [8, 24])
|
||||
HEAD_DIM_LIST = get_ci_test_range([64, 128, 256], [64, 128, 256])
|
||||
IS_NEOX_LIST = [False, True]
|
||||
POSITION_DTYPES = [torch.int32, torch.int64]
|
||||
ROPE_DIM_CHOICES = {
|
||||
64: [64],
|
||||
128: [64, 128],
|
||||
256: [64, 128, 256],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_size,num_heads,head_dim,is_neox,position_dtype",
|
||||
list(
|
||||
itertools.product(
|
||||
BS_LIST,
|
||||
HEADS_LIST,
|
||||
HEAD_DIM_LIST,
|
||||
IS_NEOX_LIST,
|
||||
POSITION_DTYPES,
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_qknorm_rope(
|
||||
batch_size: int,
|
||||
num_heads: int,
|
||||
head_dim: int,
|
||||
is_neox: bool,
|
||||
position_dtype: torch.dtype,
|
||||
) -> None:
|
||||
rope_dims = ROPE_DIM_CHOICES[head_dim]
|
||||
for rope_dim in rope_dims:
|
||||
if is_neox:
|
||||
elems_per_thread = head_dim // 32
|
||||
rotary_lanes = rope_dim // elems_per_thread
|
||||
if rotary_lanes < 2 or rotary_lanes & (rotary_lanes - 1):
|
||||
continue
|
||||
|
||||
q = torch.randn(batch_size, num_heads, head_dim, device=DEVICE, dtype=DTYPE)
|
||||
k = torch.randn(batch_size, num_heads, head_dim, device=DEVICE, dtype=DTYPE)
|
||||
q_weight = torch.randn(head_dim, device=DEVICE, dtype=DTYPE)
|
||||
k_weight = torch.randn(head_dim, device=DEVICE, dtype=DTYPE)
|
||||
positions = torch.randint(
|
||||
0, MAX_SEQ_LEN, (batch_size,), device=DEVICE, dtype=position_dtype
|
||||
)
|
||||
cos_sin_cache = create_cos_sin_cache(rope_dim)
|
||||
|
||||
q_ref, k_ref = q.clone(), k.clone()
|
||||
q_fused, k_fused = q.clone(), k.clone()
|
||||
|
||||
split_qknorm_rope(
|
||||
q_ref, k_ref, q_weight, k_weight, cos_sin_cache, positions, is_neox
|
||||
)
|
||||
fused_qknorm_rope(
|
||||
q_fused, k_fused, q_weight, k_weight, cos_sin_cache, positions, is_neox
|
||||
)
|
||||
|
||||
# The split baseline mixes a separate BF16 qknorm kernel with FlashInfer RoPE,
|
||||
# which differs from the fused path by about one BF16 rounding step on H200.
|
||||
triton.testing.assert_close(q_ref, q_fused, atol=ATOL, rtol=RTOL)
|
||||
triton.testing.assert_close(k_ref, k_fused, atol=ATOL, rtol=RTOL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,226 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.jit_kernel.diffusion.triton.norm import norm_infer
|
||||
from sglang.jit_kernel.diffusion.triton.scale_shift import (
|
||||
fuse_layernorm_scale_shift_gate_select01_kernel,
|
||||
fuse_residual_layernorm_scale_shift_gate_select01_kernel,
|
||||
)
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=15, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPES = get_ci_test_range(
|
||||
[torch.float16, torch.bfloat16, torch.float32], [torch.float16, torch.bfloat16]
|
||||
)
|
||||
BATCH_SIZES = get_ci_test_range([1, 2, 4], [1, 2])
|
||||
SEQ_LENS = get_ci_test_range([6, 33, 128, 257], [6, 128])
|
||||
HIDDEN_SIZES = get_ci_test_range([512, 1024, 1536, 3072], [512, 3072])
|
||||
EPS = 1e-6
|
||||
|
||||
|
||||
def _tol(dtype: torch.dtype) -> tuple[float, float]:
|
||||
if dtype == torch.float32:
|
||||
return 1e-5, 1e-5
|
||||
return 5e-2, 5e-2
|
||||
|
||||
|
||||
def _make_modulation_tensors(batch_size: int, hidden_size: int, dtype: torch.dtype):
|
||||
scale0 = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=dtype)
|
||||
shift0 = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=dtype)
|
||||
gate0 = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=dtype)
|
||||
scale1 = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=dtype)
|
||||
shift1 = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=dtype)
|
||||
gate1 = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=dtype)
|
||||
return scale0, shift0, gate0, scale1, shift1, gate1
|
||||
|
||||
|
||||
def _baseline_select01_modulation(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor | None,
|
||||
bias: torch.Tensor | None,
|
||||
scale0: torch.Tensor,
|
||||
shift0: torch.Tensor,
|
||||
gate0: torch.Tensor,
|
||||
scale1: torch.Tensor,
|
||||
shift1: torch.Tensor,
|
||||
gate1: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
eps: float,
|
||||
):
|
||||
normalized = norm_infer(
|
||||
x.view(-1, x.shape[-1]),
|
||||
weight,
|
||||
bias,
|
||||
eps=eps,
|
||||
is_rms_norm=False,
|
||||
).view_as(x)
|
||||
return _apply_select01_modulation(
|
||||
normalized, scale0, shift0, gate0, scale1, shift1, gate1, index
|
||||
)
|
||||
|
||||
|
||||
def _baseline_residual_select01_modulation(
|
||||
x: torch.Tensor,
|
||||
residual: torch.Tensor,
|
||||
residual_gate: torch.Tensor,
|
||||
weight: torch.Tensor | None,
|
||||
bias: torch.Tensor | None,
|
||||
scale0: torch.Tensor,
|
||||
shift0: torch.Tensor,
|
||||
gate0: torch.Tensor,
|
||||
scale1: torch.Tensor,
|
||||
shift1: torch.Tensor,
|
||||
gate1: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
eps: float,
|
||||
):
|
||||
residual_out = residual + residual_gate * x
|
||||
normalized = norm_infer(
|
||||
residual_out.view(-1, residual_out.shape[-1]),
|
||||
weight,
|
||||
bias,
|
||||
eps=eps,
|
||||
is_rms_norm=False,
|
||||
).view_as(residual_out)
|
||||
output, gate_out = _apply_select01_modulation(
|
||||
normalized, scale0, shift0, gate0, scale1, shift1, gate1, index
|
||||
)
|
||||
return output, residual_out, gate_out
|
||||
|
||||
|
||||
def _apply_select01_modulation(
|
||||
x: torch.Tensor,
|
||||
scale0: torch.Tensor,
|
||||
shift0: torch.Tensor,
|
||||
gate0: torch.Tensor,
|
||||
scale1: torch.Tensor,
|
||||
shift1: torch.Tensor,
|
||||
gate1: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
):
|
||||
idx = index.bool().unsqueeze(-1)
|
||||
scale = torch.where(idx, scale1.unsqueeze(1), scale0.unsqueeze(1))
|
||||
shift = torch.where(idx, shift1.unsqueeze(1), shift0.unsqueeze(1))
|
||||
gate = torch.where(idx, gate1.unsqueeze(1), gate0.unsqueeze(1))
|
||||
return x * (1 + scale) + shift, gate
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cuda_setup():
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required")
|
||||
torch.cuda.manual_seed(0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("batch_size", BATCH_SIZES)
|
||||
@pytest.mark.parametrize("seq_len", SEQ_LENS)
|
||||
@pytest.mark.parametrize("hidden_size", HIDDEN_SIZES)
|
||||
def test_fused_layernorm_scale_shift_gate_select01(
|
||||
dtype, batch_size, seq_len, hidden_size
|
||||
):
|
||||
x = torch.randn(batch_size, seq_len, hidden_size, device=DEVICE, dtype=dtype)
|
||||
weight = torch.randn(hidden_size, device=DEVICE, dtype=dtype)
|
||||
bias = torch.randn(hidden_size, device=DEVICE, dtype=dtype)
|
||||
index = torch.randint(0, 2, (batch_size, seq_len), device=DEVICE, dtype=torch.int32)
|
||||
scale0, shift0, gate0, scale1, shift1, gate1 = _make_modulation_tensors(
|
||||
batch_size, hidden_size, dtype
|
||||
)
|
||||
|
||||
out_ref, gate_ref = _baseline_select01_modulation(
|
||||
x,
|
||||
weight,
|
||||
bias,
|
||||
scale0,
|
||||
shift0,
|
||||
gate0,
|
||||
scale1,
|
||||
shift1,
|
||||
gate1,
|
||||
index,
|
||||
EPS,
|
||||
)
|
||||
out_fused, gate_fused = fuse_layernorm_scale_shift_gate_select01_kernel(
|
||||
x.contiguous(),
|
||||
weight=weight,
|
||||
bias=bias,
|
||||
scale0=scale0,
|
||||
shift0=shift0,
|
||||
gate0=gate0,
|
||||
scale1=scale1,
|
||||
shift1=shift1,
|
||||
gate1=gate1,
|
||||
index=index,
|
||||
eps=EPS,
|
||||
)
|
||||
|
||||
atol, rtol = _tol(dtype)
|
||||
triton.testing.assert_close(out_ref, out_fused, atol=atol, rtol=rtol)
|
||||
triton.testing.assert_close(gate_ref, gate_fused, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("batch_size", BATCH_SIZES)
|
||||
@pytest.mark.parametrize("seq_len", SEQ_LENS)
|
||||
@pytest.mark.parametrize("hidden_size", HIDDEN_SIZES)
|
||||
def test_fused_residual_layernorm_scale_shift_gate_select01(
|
||||
dtype, batch_size, seq_len, hidden_size
|
||||
):
|
||||
x = torch.randn(batch_size, seq_len, hidden_size, device=DEVICE, dtype=dtype)
|
||||
residual = torch.randn_like(x)
|
||||
residual_gate = torch.randn_like(x)
|
||||
weight = torch.randn(hidden_size, device=DEVICE, dtype=dtype)
|
||||
bias = torch.randn(hidden_size, device=DEVICE, dtype=dtype)
|
||||
index = torch.randint(0, 2, (batch_size, seq_len), device=DEVICE, dtype=torch.int32)
|
||||
scale0, shift0, gate0, scale1, shift1, gate1 = _make_modulation_tensors(
|
||||
batch_size, hidden_size, dtype
|
||||
)
|
||||
|
||||
out_ref, residual_ref, gate_ref = _baseline_residual_select01_modulation(
|
||||
x,
|
||||
residual,
|
||||
residual_gate,
|
||||
weight,
|
||||
bias,
|
||||
scale0,
|
||||
shift0,
|
||||
gate0,
|
||||
scale1,
|
||||
shift1,
|
||||
gate1,
|
||||
index,
|
||||
EPS,
|
||||
)
|
||||
out_fused, residual_fused, gate_fused = (
|
||||
fuse_residual_layernorm_scale_shift_gate_select01_kernel(
|
||||
x.contiguous(),
|
||||
residual=residual.contiguous(),
|
||||
residual_gate=residual_gate.contiguous(),
|
||||
weight=weight,
|
||||
bias=bias,
|
||||
scale0=scale0,
|
||||
shift0=shift0,
|
||||
gate0=gate0,
|
||||
scale1=scale1,
|
||||
shift1=shift1,
|
||||
gate1=gate1,
|
||||
index=index,
|
||||
eps=EPS,
|
||||
)
|
||||
)
|
||||
|
||||
atol, rtol = _tol(dtype)
|
||||
triton.testing.assert_close(out_ref, out_fused, atol=atol, rtol=rtol)
|
||||
triton.testing.assert_close(residual_ref, residual_fused, atol=atol, rtol=rtol)
|
||||
triton.testing.assert_close(gate_ref, gate_fused, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,193 +0,0 @@
|
||||
"""Numerical correctness for fused varlen pack/scatter Triton kernels.
|
||||
|
||||
Bit-exact comparison against the equivalent PyTorch ops (index_select,
|
||||
zeros + index_copy_) across bf16/fp16 and several shape/mask cases.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.diffusion.triton.varlen_pack_pad import (
|
||||
build_inv_indices,
|
||||
fused_pack_qkv,
|
||||
fused_scatter_to_padded,
|
||||
)
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=10, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=60, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPES = get_ci_test_range([torch.bfloat16, torch.float16], [torch.bfloat16])
|
||||
# (bs, s_txt, s_img, num_heads, head_dim, valid_txt_lens) tuples
|
||||
SHAPES = get_ci_test_range(
|
||||
[
|
||||
# name, bs, s_txt, s_img, H, D, valid_txt_lens
|
||||
("small_c2", 2, 64, 128, 4, 64, [32, 48]),
|
||||
("prod_c2", 2, 256, 1024, 24, 128, [128, 200]),
|
||||
("all_valid_b1", 1, 64, 128, 4, 64, [64]),
|
||||
("all_valid_b4", 4, 64, 128, 4, 64, [64, 64, 64, 64]),
|
||||
("c8_prod", 8, 256, 4096, 24, 128, [128, 200, 256, 100, 50, 256, 256, 50]),
|
||||
# one batch with zero valid text tokens (image side still valid)
|
||||
("zero_txt_one_batch", 2, 64, 128, 4, 64, [0, 32]),
|
||||
# bs=1 with no text validity (only image rows packed)
|
||||
("bs1_zero_txt", 1, 64, 128, 4, 64, [0]),
|
||||
],
|
||||
[
|
||||
("small_c2", 2, 64, 128, 4, 64, [32, 48]),
|
||||
("prod_c2", 2, 256, 1024, 24, 128, [128, 200]),
|
||||
("all_valid_b4", 4, 64, 128, 4, 64, [64, 64, 64, 64]),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _build_mask(bs, s_txt, s_img, valid_txt_lens):
|
||||
s = s_txt + s_img
|
||||
mask = torch.zeros(bs, s, dtype=torch.bool, device=DEVICE)
|
||||
for b, vt in enumerate(valid_txt_lens):
|
||||
mask[b, :vt] = True
|
||||
mask[b, s_txt:] = True
|
||||
return mask
|
||||
|
||||
|
||||
def _ref_pack(q, k, v, indices):
|
||||
bs, seq = q.shape[:2]
|
||||
flat = lambda t: t.reshape(bs * seq, *t.shape[2:])
|
||||
return (
|
||||
flat(q).index_select(0, indices),
|
||||
flat(k).index_select(0, indices),
|
||||
flat(v).index_select(0, indices),
|
||||
)
|
||||
|
||||
|
||||
def _ref_scatter(out_unpad, indices, bs, seq):
|
||||
n_valid = indices.shape[0]
|
||||
_, num_heads, head_dim = out_unpad.shape
|
||||
flat = torch.zeros(
|
||||
bs * seq, num_heads, head_dim, dtype=out_unpad.dtype, device=DEVICE
|
||||
)
|
||||
flat.index_copy_(0, indices, out_unpad)
|
||||
return flat.view(bs, seq, num_heads, head_dim)
|
||||
|
||||
|
||||
def _build_meta(mask):
|
||||
bs, seq = mask.shape
|
||||
indices = mask.reshape(-1).nonzero(as_tuple=False).flatten()
|
||||
inv_indices = build_inv_indices(indices, bs * seq)
|
||||
return indices, inv_indices
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize(
|
||||
"shape", SHAPES, ids=lambda s: s[0] if isinstance(s, tuple) else str(s)
|
||||
)
|
||||
def test_pack_matches_index_select(dtype, shape):
|
||||
_, bs, s_txt, s_img, num_heads, head_dim, valid_txt_lens = shape
|
||||
torch.manual_seed(0)
|
||||
s = s_txt + s_img
|
||||
mask = _build_mask(bs, s_txt, s_img, valid_txt_lens)
|
||||
indices, _ = _build_meta(mask)
|
||||
|
||||
q = torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
k = torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
v = torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
|
||||
q_ref, k_ref, v_ref = _ref_pack(q, k, v, indices)
|
||||
q_fused, k_fused, v_fused = fused_pack_qkv(q, k, v, indices)
|
||||
|
||||
# bit-exact: pack is pure gather, no math
|
||||
assert torch.equal(q_ref, q_fused)
|
||||
assert torch.equal(k_ref, k_fused)
|
||||
assert torch.equal(v_ref, v_fused)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize(
|
||||
"shape", SHAPES, ids=lambda s: s[0] if isinstance(s, tuple) else str(s)
|
||||
)
|
||||
def test_scatter_matches_index_copy(dtype, shape):
|
||||
_, bs, s_txt, s_img, num_heads, head_dim, valid_txt_lens = shape
|
||||
torch.manual_seed(1)
|
||||
s = s_txt + s_img
|
||||
mask = _build_mask(bs, s_txt, s_img, valid_txt_lens)
|
||||
indices, inv_indices = _build_meta(mask)
|
||||
n_valid = indices.shape[0]
|
||||
|
||||
out_unpad = torch.randn(n_valid, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
out_ref = _ref_scatter(out_unpad, indices, bs, s)
|
||||
out_fused = fused_scatter_to_padded(out_unpad, inv_indices, bs, s)
|
||||
|
||||
# bit-exact: scatter is pure copy + zero-fill
|
||||
assert torch.equal(out_ref, out_fused)
|
||||
# Padding rows must be exactly zero
|
||||
invalid = ~mask
|
||||
if invalid.any():
|
||||
assert out_fused[invalid].abs().max().item() == 0.0
|
||||
|
||||
|
||||
def test_pack_handles_non_contiguous_input():
|
||||
"""Helper must accept non-contiguous Q/K/V (auto .contiguous() inside)."""
|
||||
torch.manual_seed(2)
|
||||
bs, s_txt, s_img, num_heads, head_dim = 2, 64, 128, 4, 64
|
||||
s = s_txt + s_img
|
||||
mask = _build_mask(bs, s_txt, s_img, [32, 48])
|
||||
indices, _ = _build_meta(mask)
|
||||
|
||||
# Build non-contiguous tensors via permute
|
||||
qkv_pre = torch.randn(
|
||||
bs, num_heads, s, head_dim, dtype=torch.bfloat16, device=DEVICE
|
||||
)
|
||||
q = qkv_pre.permute(0, 2, 1, 3)
|
||||
k = torch.randn_like(qkv_pre).permute(0, 2, 1, 3)
|
||||
v = torch.randn_like(qkv_pre).permute(0, 2, 1, 3)
|
||||
assert not q.is_contiguous()
|
||||
|
||||
q_ref, k_ref, v_ref = _ref_pack(
|
||||
q.contiguous(), k.contiguous(), v.contiguous(), indices
|
||||
)
|
||||
q_fused, k_fused, v_fused = fused_pack_qkv(q, k, v, indices)
|
||||
assert torch.equal(q_ref, q_fused)
|
||||
assert torch.equal(k_ref, k_fused)
|
||||
assert torch.equal(v_ref, v_fused)
|
||||
|
||||
|
||||
def test_build_inv_indices_matches_manual():
|
||||
"""build_inv_indices output should match the manual full+scatter form."""
|
||||
torch.manual_seed(3)
|
||||
bs, s = 2, 32
|
||||
mask = torch.bernoulli(torch.full((bs, s), 0.6, device=DEVICE)).to(torch.bool)
|
||||
indices = mask.reshape(-1).nonzero(as_tuple=False).flatten()
|
||||
n_valid = indices.shape[0]
|
||||
|
||||
manual = torch.full((bs * s,), -1, dtype=torch.int32, device=DEVICE)
|
||||
if n_valid > 0:
|
||||
manual[indices.long()] = torch.arange(n_valid, dtype=torch.int32, device=DEVICE)
|
||||
|
||||
built = build_inv_indices(indices, bs * s)
|
||||
assert torch.equal(built, manual)
|
||||
|
||||
|
||||
def test_empty_valid_set_handled():
|
||||
"""All-False mask: pack returns empty tensors; scatter writes all zeros."""
|
||||
bs, s, num_heads, head_dim = 2, 16, 4, 64
|
||||
mask = torch.zeros(bs, s, dtype=torch.bool, device=DEVICE)
|
||||
indices = mask.reshape(-1).nonzero(as_tuple=False).flatten()
|
||||
inv_indices = build_inv_indices(indices, bs * s)
|
||||
assert indices.numel() == 0
|
||||
|
||||
q = torch.randn(bs, s, num_heads, head_dim, dtype=torch.bfloat16, device=DEVICE)
|
||||
q_unpad, k_unpad, v_unpad = fused_pack_qkv(q, q.clone(), q.clone(), indices)
|
||||
assert q_unpad.shape == (0, num_heads, head_dim)
|
||||
assert k_unpad.shape == (0, num_heads, head_dim)
|
||||
assert v_unpad.shape == (0, num_heads, head_dim)
|
||||
|
||||
out_padded = fused_scatter_to_padded(q_unpad, inv_indices, bs, s)
|
||||
assert out_padded.shape == (bs, s, num_heads, head_dim)
|
||||
assert out_padded.abs().max().item() == 0.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,156 +0,0 @@
|
||||
"""End-to-end equivalence between USPAttention varlen path and SDPA reference.
|
||||
|
||||
Compares the production varlen path (``build_varlen_mask_meta`` +
|
||||
``fused_pack_qkv`` + ``flash_attn_varlen_func`` + ``fused_scatter_to_padded``)
|
||||
against ``torch.nn.functional.scaled_dot_product_attention`` with a broadcast
|
||||
key mask, for inputs the gating in ``USPAttention.forward`` would accept.
|
||||
|
||||
Verifies the documented contract:
|
||||
* Valid (non-masked) query rows match SDPA within FA-vs-SDPA tolerance.
|
||||
* Masked query rows are exactly zero in the varlen path (differs from
|
||||
SDPA, which produces deterministic attention output at those rows).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.jit_kernel.diffusion.triton.varlen_pack_pad import (
|
||||
fused_pack_qkv,
|
||||
fused_scatter_to_padded,
|
||||
)
|
||||
from sglang.jit_kernel.flash_attention import flash_attn_varlen_func
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.multimodal_gen.runtime.layers.attention.backends import (
|
||||
flash_attn as _fa_backend,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.attention.layer import (
|
||||
build_varlen_mask_meta,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=15, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=60, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPES = get_ci_test_range([torch.bfloat16, torch.float16], [torch.bfloat16])
|
||||
# (name, bs, s_txt, s_img, num_heads, head_dim, valid_txt_lens)
|
||||
SHAPES = get_ci_test_range(
|
||||
[
|
||||
("small_c2", 2, 64, 128, 4, 64, [32, 48]),
|
||||
("prod_c2", 2, 256, 1024, 24, 128, [128, 200]),
|
||||
("all_valid_b1", 1, 64, 128, 4, 64, [64]),
|
||||
("zero_txt_one_batch", 2, 64, 128, 4, 64, [0, 32]),
|
||||
],
|
||||
[
|
||||
("small_c2", 2, 64, 128, 4, 64, [32, 48]),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _build_mask(bs, s_txt, s_img, valid_txt_lens):
|
||||
s = s_txt + s_img
|
||||
mask = torch.zeros(bs, s, dtype=torch.bool, device=DEVICE)
|
||||
for b, vt in enumerate(valid_txt_lens):
|
||||
mask[b, :vt] = True
|
||||
mask[b, s_txt:] = True
|
||||
return mask
|
||||
|
||||
|
||||
def _sdpa_with_key_mask(q, k, v, key_mask, softmax_scale):
|
||||
"""Reference: SDPA with a ``[B, S]`` key mask broadcast to ``[B, 1, 1, S]``."""
|
||||
q_ = q.transpose(1, 2)
|
||||
k_ = k.transpose(1, 2)
|
||||
v_ = v.transpose(1, 2)
|
||||
mask = key_mask.to(dtype=q.dtype)[:, None, None, :]
|
||||
mask = (mask - 1.0) * torch.finfo(q.dtype).max
|
||||
out = F.scaled_dot_product_attention(
|
||||
q_,
|
||||
k_,
|
||||
v_,
|
||||
attn_mask=mask,
|
||||
dropout_p=0.0,
|
||||
is_causal=False,
|
||||
scale=softmax_scale,
|
||||
)
|
||||
return out.transpose(1, 2)
|
||||
|
||||
|
||||
def _varlen_path(q, k, v, key_mask, softmax_scale):
|
||||
"""Production varlen path matching USPAttention.forward masked branch."""
|
||||
bs, seq = q.shape[0], q.shape[1]
|
||||
meta = build_varlen_mask_meta(key_mask)
|
||||
indices = meta["indices"]
|
||||
if indices.shape[0] == 0:
|
||||
return torch.zeros_like(q)
|
||||
q_unpad, k_unpad, v_unpad = fused_pack_qkv(q, k, v, indices)
|
||||
out_unpad = flash_attn_varlen_func(
|
||||
q=q_unpad,
|
||||
k=k_unpad,
|
||||
v=v_unpad,
|
||||
cu_seqlens_q=meta["cu_seqlens"],
|
||||
cu_seqlens_k=meta["cu_seqlens"],
|
||||
max_seqlen_q=meta["max_seqlen"],
|
||||
max_seqlen_k=meta["max_seqlen"],
|
||||
softmax_scale=softmax_scale,
|
||||
causal=False,
|
||||
ver=_fa_backend.fa_ver,
|
||||
)
|
||||
return fused_scatter_to_padded(out_unpad, meta["inv_indices"], bs, seq)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize(
|
||||
"shape", SHAPES, ids=lambda s: s[0] if isinstance(s, tuple) else str(s)
|
||||
)
|
||||
def test_varlen_path_matches_sdpa_on_valid_rows(dtype, shape):
|
||||
"""Valid rows: varlen output ≈ SDPA output within FA tolerance."""
|
||||
_, bs, s_txt, s_img, num_heads, head_dim, valid_txt_lens = shape
|
||||
torch.manual_seed(0)
|
||||
s = s_txt + s_img
|
||||
softmax_scale = head_dim**-0.5
|
||||
mask = _build_mask(bs, s_txt, s_img, valid_txt_lens)
|
||||
q = torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
k = torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
v = torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
|
||||
out_sdpa = _sdpa_with_key_mask(q, k, v, mask, softmax_scale)
|
||||
out_varlen = _varlen_path(q, k, v, mask, softmax_scale)
|
||||
|
||||
valid = mask[..., None, None].expand_as(out_sdpa)
|
||||
rtol = 1e-2 if dtype == torch.bfloat16 else 5e-3
|
||||
atol = 5e-2 if dtype == torch.bfloat16 else 1e-2
|
||||
torch.testing.assert_close(
|
||||
out_sdpa[valid],
|
||||
out_varlen[valid],
|
||||
rtol=rtol,
|
||||
atol=atol,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize(
|
||||
"shape", SHAPES, ids=lambda s: s[0] if isinstance(s, tuple) else str(s)
|
||||
)
|
||||
def test_varlen_path_zeros_masked_rows(dtype, shape):
|
||||
"""Masked rows: varlen path produces exact zeros (documented contract)."""
|
||||
_, bs, s_txt, s_img, num_heads, head_dim, valid_txt_lens = shape
|
||||
torch.manual_seed(1)
|
||||
s = s_txt + s_img
|
||||
softmax_scale = head_dim**-0.5
|
||||
mask = _build_mask(bs, s_txt, s_img, valid_txt_lens)
|
||||
q = torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
k = torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
v = torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
|
||||
out_varlen = _varlen_path(q, k, v, mask, softmax_scale)
|
||||
|
||||
invalid = ~mask
|
||||
if invalid.any():
|
||||
assert (out_varlen[invalid] == 0).all(), "masked rows must be zero-filled"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,70 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from sglang.jit_kernel.kv_canary import consts
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=5, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
|
||||
_CONSTS_CUH: Path = (
|
||||
Path(__file__).resolve().parents[2] / "csrc" / "kv_canary" / "consts.cuh"
|
||||
)
|
||||
|
||||
|
||||
def _camel_to_upper_snake(name: str) -> str:
|
||||
return re.sub(r"([A-Z])", r"_\1", name).lstrip("_").upper()
|
||||
|
||||
|
||||
def _decode(expr: str) -> int:
|
||||
expr = expr.strip().rstrip("UuLl")
|
||||
if "<<" in expr:
|
||||
return 1 << int(expr.split("<<")[1].strip())
|
||||
return int(expr, 0)
|
||||
|
||||
|
||||
def _parse_constexpr_ints(source: str) -> dict[str, int]:
|
||||
pattern = re.compile(r"constexpr\s+(?:[\w:]+)\s+(k[A-Za-z]\w*)\s*=\s*([^;]+);")
|
||||
return {name: _decode(rhs) for name, rhs in pattern.findall(source)}
|
||||
|
||||
|
||||
def _parse_enum_class(source: str, enum_name: str) -> dict[str, int]:
|
||||
pattern = re.compile(
|
||||
r"enum\s+class\s+" + re.escape(enum_name) + r"\s*:\s*[^\{]+\{([^}]+)\}"
|
||||
)
|
||||
body = pattern.search(source).group(1)
|
||||
member_re = re.compile(r"(k[A-Za-z]\w*)\s*=\s*([^,]+)")
|
||||
return {name: _decode(rhs) for name, rhs in member_re.findall(body)}
|
||||
|
||||
|
||||
def test_int_consts_sync() -> None:
|
||||
cpp = _parse_constexpr_ints(_CONSTS_CUH.read_text(encoding="utf-8"))
|
||||
cpp_normalized = {_camel_to_upper_snake(n[1:]): v for n, v in cpp.items()}
|
||||
py = {
|
||||
n: v
|
||||
for n, v in vars(consts).items()
|
||||
if isinstance(v, int) and not isinstance(v, bool) and not n.startswith("_")
|
||||
}
|
||||
assert cpp_normalized == py
|
||||
|
||||
|
||||
def test_enums_sync() -> None:
|
||||
cuh = _CONSTS_CUH.read_text(encoding="utf-8")
|
||||
for enum_name in ("RealKvHashMode", "FailReason"):
|
||||
cpp_members = _parse_enum_class(cuh, enum_name)
|
||||
py_enum = getattr(consts, enum_name)
|
||||
cpp_normalized = {
|
||||
_camel_to_upper_snake(n[1:]): v for n, v in cpp_members.items()
|
||||
}
|
||||
py_normalized = {m.name: int(m.value) for m in py_enum}
|
||||
assert cpp_normalized == py_normalized
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -1,440 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.kv_canary import consts
|
||||
from sglang.jit_kernel.kv_canary.verify import (
|
||||
CanaryLaunchTag,
|
||||
VerifyOrWriteContext,
|
||||
VerifyPlan,
|
||||
launch_canary_verify_kernel,
|
||||
)
|
||||
from sglang.jit_kernel.kv_canary.write import WritePlan
|
||||
from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
|
||||
FakeViolationLog,
|
||||
assert_canary_buf_equal,
|
||||
assert_canary_state_equal,
|
||||
make_canary_buf,
|
||||
make_canary_buf_pair,
|
||||
make_log_pair,
|
||||
make_verify_plan,
|
||||
make_verify_plan_pair,
|
||||
make_write_plan_pair,
|
||||
stamp_clean_chain,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._differential import (
|
||||
_assert_plans_byte_equal,
|
||||
_run_both_plan,
|
||||
_run_both_verify,
|
||||
_run_both_write,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._fixtures import (
|
||||
dummy_pseudo_tensors,
|
||||
empty_extras,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=60, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
_DEVICE = torch.device("cuda")
|
||||
|
||||
|
||||
def _build_verify_plan_5_entries(
|
||||
*, device: torch.device
|
||||
) -> tuple[VerifyPlan, VerifyPlan]:
|
||||
num_slots = 16
|
||||
cuda_buf, ref_buf = make_canary_buf_pair(
|
||||
num_slots=num_slots, slot_stride_bytes=32, device=_DEVICE
|
||||
)
|
||||
|
||||
plan_cuda, plan_ref = make_verify_plan_pair(
|
||||
slot_indices=[0, 1, 2, 3, 4],
|
||||
positions=[0, 1, 2, 3, 4],
|
||||
prev_slot_indices=[-1, 0, 1, 2, 3],
|
||||
capacity=8,
|
||||
device=device,
|
||||
)
|
||||
return plan_cuda, plan_ref
|
||||
|
||||
|
||||
def _build_write_fixtures(
|
||||
*, device: torch.device
|
||||
) -> tuple[WritePlan, WritePlan, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
num_tokens = 5
|
||||
plan_cuda, plan_ref = make_write_plan_pair(
|
||||
write_offsets=[0, num_tokens],
|
||||
seed_slot_indices=[-1],
|
||||
num_valid_reqs=1,
|
||||
req_capacity=4,
|
||||
device=device,
|
||||
)
|
||||
input_ids = torch.tensor([10, 20, 30, 40, 50], dtype=torch.int64, device=device)
|
||||
positions = torch.tensor([0, 1, 2, 3, 4], dtype=torch.int64, device=device)
|
||||
out_cache_loc = torch.tensor([0, 1, 2, 3, 4], dtype=torch.int64, device=device)
|
||||
return plan_cuda, plan_ref, input_ids, positions, out_cache_loc
|
||||
|
||||
|
||||
def _build_plan_fixtures(
|
||||
*, device: torch.device, int64_req_to_token: bool = False
|
||||
) -> tuple[
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
]:
|
||||
bs = 3
|
||||
max_reqs = 4
|
||||
max_seq_len = 16
|
||||
req_pool_indices = torch.tensor([1, 2, 3], dtype=torch.int64, device=device)
|
||||
prefix_lens = torch.tensor([0, 4, 8], dtype=torch.int64, device=device)
|
||||
extend_seq_lens = torch.tensor([5, 1, 1], dtype=torch.int64, device=device)
|
||||
rp_axis = torch.arange(max_reqs, device=device, dtype=torch.int32).unsqueeze(1)
|
||||
pos_axis = torch.arange(max_seq_len, device=device, dtype=torch.int32).unsqueeze(0)
|
||||
req_to_token_int32 = (rp_axis * max_seq_len + pos_axis).contiguous()
|
||||
if int64_req_to_token:
|
||||
req_to_token = req_to_token_int32.to(torch.int64)
|
||||
else:
|
||||
req_to_token = req_to_token_int32
|
||||
return req_pool_indices, prefix_lens, extend_seq_lens, req_to_token
|
||||
|
||||
|
||||
def test_verify_byte_equal_across_repeated_launches_10x() -> None:
|
||||
num_launches = 10
|
||||
plan_cuda, plan_ref = _build_verify_plan_5_entries(device=_DEVICE)
|
||||
|
||||
snapshot_rings: list[torch.Tensor] = []
|
||||
snapshot_write_indices: list[torch.Tensor] = []
|
||||
snapshot_bufs: list[torch.Tensor] = []
|
||||
|
||||
for _ in range(num_launches):
|
||||
cuda_buf, ref_buf = make_canary_buf_pair(
|
||||
num_slots=16, slot_stride_bytes=32, device=_DEVICE
|
||||
)
|
||||
cuda_log, ref_log = make_log_pair(capacity=64, device=_DEVICE)
|
||||
|
||||
_run_both_verify(
|
||||
cuda_canary_buf=cuda_buf,
|
||||
ref_canary_buf=ref_buf,
|
||||
plan_cuda=plan_cuda,
|
||||
plan_ref=plan_ref,
|
||||
cuda_log=cuda_log,
|
||||
ref_log=ref_log,
|
||||
real_kv_sources_cuda=(),
|
||||
real_kv_sources_ref=(),
|
||||
real_kv_hash_mode=consts.RealKvHashMode.NONE,
|
||||
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
|
||||
)
|
||||
|
||||
assert_canary_buf_equal(buf_a=cuda_buf, buf_b=ref_buf)
|
||||
assert_canary_state_equal(log_a=cuda_log, log_b=ref_log)
|
||||
|
||||
snapshot_rings.append(cuda_log.ring.clone())
|
||||
snapshot_write_indices.append(cuda_log.write_index.clone())
|
||||
snapshot_bufs.append(cuda_buf.clone())
|
||||
|
||||
for i in range(1, num_launches):
|
||||
assert torch.equal(
|
||||
snapshot_rings[0], snapshot_rings[i]
|
||||
), f"violation_ring differs between launch 0 and {i}"
|
||||
assert torch.equal(
|
||||
snapshot_write_indices[0], snapshot_write_indices[i]
|
||||
), f"violation_write_index differs between launch 0 and {i}"
|
||||
assert torch.equal(
|
||||
snapshot_bufs[0], snapshot_bufs[i]
|
||||
), f"canary_buf differs between launch 0 and {i}"
|
||||
|
||||
|
||||
def test_write_byte_equal_across_repeated_launches_10x() -> None:
|
||||
num_launches = 10
|
||||
plan_cuda, plan_ref, input_ids, positions, out_cache_loc = _build_write_fixtures(
|
||||
device=_DEVICE
|
||||
)
|
||||
|
||||
snapshot_bufs: list[torch.Tensor] = []
|
||||
snapshot_rings: list[torch.Tensor] = []
|
||||
snapshot_counters: list[torch.Tensor] = []
|
||||
|
||||
for _ in range(num_launches):
|
||||
cuda_buf, ref_buf = make_canary_buf_pair(
|
||||
num_slots=16, slot_stride_bytes=32, device=_DEVICE
|
||||
)
|
||||
cuda_log, ref_log = make_log_pair(capacity=64, device=_DEVICE)
|
||||
pseudo_tok, pseudo_pos = dummy_pseudo_tensors(input_ids.shape[0])
|
||||
|
||||
_run_both_write(
|
||||
cuda_canary_buf=cuda_buf,
|
||||
ref_canary_buf=ref_buf,
|
||||
plan_cuda=plan_cuda,
|
||||
plan_ref=plan_ref,
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
enable_write_verify_inputs=False,
|
||||
expected_input_tokens=pseudo_tok,
|
||||
expected_input_positions=pseudo_pos,
|
||||
cuda_log=cuda_log,
|
||||
ref_log=ref_log,
|
||||
real_kv_sources_cuda=(),
|
||||
real_kv_sources_ref=(),
|
||||
real_kv_hash_mode=consts.RealKvHashMode.NONE,
|
||||
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
|
||||
)
|
||||
|
||||
assert_canary_buf_equal(buf_a=cuda_buf, buf_b=ref_buf)
|
||||
assert_canary_state_equal(log_a=cuda_log, log_b=ref_log)
|
||||
|
||||
snapshot_bufs.append(cuda_buf.clone())
|
||||
snapshot_rings.append(cuda_log.ring.clone())
|
||||
snapshot_counters.append(cuda_log.slot_run_counter.clone())
|
||||
|
||||
for i in range(1, num_launches):
|
||||
assert torch.equal(
|
||||
snapshot_bufs[0], snapshot_bufs[i]
|
||||
), f"canary_buf differs between launch 0 and {i}"
|
||||
assert torch.equal(
|
||||
snapshot_rings[0], snapshot_rings[i]
|
||||
), f"violation_ring differs between launch 0 and {i}"
|
||||
assert torch.equal(
|
||||
snapshot_counters[0], snapshot_counters[i]
|
||||
), f"slot_run_counter differs between launch 0 and {i}"
|
||||
|
||||
|
||||
def test_plan_byte_equal_across_repeated_launches_10x() -> None:
|
||||
num_launches = 10
|
||||
req_pool_indices, prefix_lens, extend_seq_lens, req_to_token = _build_plan_fixtures(
|
||||
device=_DEVICE
|
||||
)
|
||||
|
||||
snapshot_slots: list[torch.Tensor] = []
|
||||
snapshot_positions: list[torch.Tensor] = []
|
||||
snapshot_prevs: list[torch.Tensor] = []
|
||||
snapshot_write_offsets: list[torch.Tensor] = []
|
||||
|
||||
for _ in range(num_launches):
|
||||
triton_v = VerifyPlan.allocate(
|
||||
verify_capacity=64, device=_DEVICE
|
||||
).zero_for_testing_()
|
||||
triton_w = WritePlan.allocate(
|
||||
write_req_capacity=8, device=_DEVICE
|
||||
).zero_for_testing_()
|
||||
ref_v = VerifyPlan.allocate(
|
||||
verify_capacity=64, device=_DEVICE
|
||||
).zero_for_testing_()
|
||||
ref_w = WritePlan.allocate(
|
||||
write_req_capacity=8, device=_DEVICE
|
||||
).zero_for_testing_()
|
||||
|
||||
_run_both_plan(
|
||||
triton_verify=triton_v,
|
||||
triton_write=triton_w,
|
||||
ref_verify=ref_v,
|
||||
ref_write=ref_w,
|
||||
req_pool_indices=req_pool_indices,
|
||||
prefix_lens=prefix_lens,
|
||||
extend_seq_lens=extend_seq_lens,
|
||||
req_to_token=req_to_token,
|
||||
extras=empty_extras(),
|
||||
swa_window_size=0,
|
||||
full_to_swa_index_mapping=None,
|
||||
)
|
||||
|
||||
_assert_plans_byte_equal(
|
||||
triton_verify=triton_v,
|
||||
triton_write=triton_w,
|
||||
ref_verify=ref_v,
|
||||
ref_write=ref_w,
|
||||
)
|
||||
|
||||
n_verify = int(triton_v.verify_num_valid[0].item())
|
||||
snapshot_slots.append(triton_v.verify_slot_indices[:n_verify].clone())
|
||||
snapshot_positions.append(triton_v.verify_expected_positions[:n_verify].clone())
|
||||
snapshot_prevs.append(triton_v.verify_prev_slot_indices[:n_verify].clone())
|
||||
snapshot_write_offsets.append(triton_w.write_offsets.clone())
|
||||
|
||||
for i in range(1, num_launches):
|
||||
assert torch.equal(
|
||||
snapshot_slots[0], snapshot_slots[i]
|
||||
), f"verify_slot_indices differs between launch 0 and {i}"
|
||||
assert torch.equal(
|
||||
snapshot_positions[0], snapshot_positions[i]
|
||||
), f"verify_expected_positions differs between launch 0 and {i}"
|
||||
assert torch.equal(
|
||||
snapshot_prevs[0], snapshot_prevs[i]
|
||||
), f"verify_prev_slot_indices differs between launch 0 and {i}"
|
||||
assert torch.equal(
|
||||
snapshot_write_offsets[0], snapshot_write_offsets[i]
|
||||
), f"write_offsets differs between launch 0 and {i}"
|
||||
|
||||
|
||||
def test_verify_multi_launch_100x_counter_linear() -> None:
|
||||
num_launches = 100
|
||||
|
||||
plan_cuda = make_verify_plan(
|
||||
slot_indices=[0],
|
||||
positions=[0],
|
||||
prev_slot_indices=[-1],
|
||||
capacity=4,
|
||||
device=_DEVICE,
|
||||
)
|
||||
|
||||
cuda_log = FakeViolationLog.allocate(capacity=64, device=_DEVICE)
|
||||
|
||||
for _ in range(num_launches):
|
||||
cuda_buf = make_canary_buf(num_slots=16, slot_stride_bytes=32, device=_DEVICE)
|
||||
launch_canary_verify_kernel(
|
||||
context=VerifyOrWriteContext(
|
||||
canary_buf=cuda_buf,
|
||||
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
|
||||
violation_ring=cuda_log.ring,
|
||||
violation_write_index=cuda_log.write_index,
|
||||
slot_run_counter=cuda_log.slot_run_counter,
|
||||
kernel_run_counter=cuda_log.kernel_run_counter,
|
||||
enable_chain_position_assert=cuda_log.enable_chain_position_assert,
|
||||
real_kv_sources=(),
|
||||
real_kv_hash_mode=consts.RealKvHashMode.NONE,
|
||||
),
|
||||
plan=plan_cuda,
|
||||
check_verify_expected_token=True,
|
||||
)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
|
||||
assert (
|
||||
int(cuda_log.kernel_run_counter[0].item()) == num_launches
|
||||
), f"kernel_run_counter expected {num_launches}, got {cuda_log.kernel_run_counter[0].item()}"
|
||||
assert int(cuda_log.slot_run_counter[0].item()) == num_launches, (
|
||||
f"slot_run_counter expected {num_launches} (1 active entry x 100 launches), "
|
||||
f"got {cuda_log.slot_run_counter[0].item()}"
|
||||
)
|
||||
|
||||
|
||||
def test_verify_check_disabled_byte_equal() -> None:
|
||||
"""check_verify_expected_token True vs False produce equivalent violation logs on a clean plan."""
|
||||
plan_true_cuda, plan_true_ref = _build_verify_plan_5_entries(device=_DEVICE)
|
||||
plan_false_cuda, plan_false_ref = _build_verify_plan_5_entries(device=_DEVICE)
|
||||
|
||||
chain_slot_indices = [0, 1, 2, 3, 4]
|
||||
chain_tokens = [10, 20, 30, 40, 50]
|
||||
chain_positions = [0, 1, 2, 3, 4]
|
||||
|
||||
cuda_buf_true, ref_buf_true = make_canary_buf_pair(
|
||||
num_slots=16, slot_stride_bytes=32, device=_DEVICE
|
||||
)
|
||||
stamp_clean_chain(
|
||||
cuda_buf=cuda_buf_true,
|
||||
ref_buf=ref_buf_true,
|
||||
slot_indices=chain_slot_indices,
|
||||
tokens=chain_tokens,
|
||||
positions=chain_positions,
|
||||
)
|
||||
cuda_buf_false, ref_buf_false = make_canary_buf_pair(
|
||||
num_slots=16, slot_stride_bytes=32, device=_DEVICE
|
||||
)
|
||||
stamp_clean_chain(
|
||||
cuda_buf=cuda_buf_false,
|
||||
ref_buf=ref_buf_false,
|
||||
slot_indices=chain_slot_indices,
|
||||
tokens=chain_tokens,
|
||||
positions=chain_positions,
|
||||
)
|
||||
cuda_log_true, ref_log_true = make_log_pair(capacity=64, device=_DEVICE)
|
||||
cuda_log_false, ref_log_false = make_log_pair(capacity=64, device=_DEVICE)
|
||||
|
||||
_run_both_verify(
|
||||
cuda_canary_buf=cuda_buf_true,
|
||||
ref_canary_buf=ref_buf_true,
|
||||
plan_cuda=plan_true_cuda,
|
||||
plan_ref=plan_true_ref,
|
||||
cuda_log=cuda_log_true,
|
||||
ref_log=ref_log_true,
|
||||
real_kv_sources_cuda=(),
|
||||
real_kv_sources_ref=(),
|
||||
real_kv_hash_mode=consts.RealKvHashMode.NONE,
|
||||
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
|
||||
check_verify_expected_token=True,
|
||||
)
|
||||
_run_both_verify(
|
||||
cuda_canary_buf=cuda_buf_false,
|
||||
ref_canary_buf=ref_buf_false,
|
||||
plan_cuda=plan_false_cuda,
|
||||
plan_ref=plan_false_ref,
|
||||
cuda_log=cuda_log_false,
|
||||
ref_log=ref_log_false,
|
||||
real_kv_sources_cuda=(),
|
||||
real_kv_sources_ref=(),
|
||||
real_kv_hash_mode=consts.RealKvHashMode.NONE,
|
||||
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
|
||||
check_verify_expected_token=False,
|
||||
)
|
||||
|
||||
assert int(cuda_log_true.write_index[0].item()) == 0
|
||||
assert int(cuda_log_false.write_index[0].item()) == 0
|
||||
assert torch.equal(cuda_log_true.ring, cuda_log_false.ring)
|
||||
assert torch.equal(cuda_log_true.write_index, cuda_log_false.write_index)
|
||||
assert torch.equal(cuda_log_true.slot_run_counter, cuda_log_false.slot_run_counter)
|
||||
assert torch.equal(
|
||||
cuda_log_true.kernel_run_counter, cuda_log_false.kernel_run_counter
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("per_req_present", [False, True])
|
||||
def test_plan_per_req_present_or_absent(per_req_present: bool) -> None:
|
||||
max_reqs = 4
|
||||
max_seq_len = 16
|
||||
rp_axis = torch.arange(max_reqs, device=_DEVICE, dtype=torch.int32).unsqueeze(1)
|
||||
pos_axis = torch.arange(max_seq_len, device=_DEVICE, dtype=torch.int32).unsqueeze(0)
|
||||
req_to_token = (rp_axis * max_seq_len + pos_axis).contiguous()
|
||||
|
||||
if per_req_present:
|
||||
req_pool_indices = torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE)
|
||||
prefix_lens = torch.tensor([3, 5], dtype=torch.int64, device=_DEVICE)
|
||||
extend_seq_lens = torch.tensor([1, 1], dtype=torch.int64, device=_DEVICE)
|
||||
else:
|
||||
req_pool_indices = torch.tensor([0], dtype=torch.int64, device=_DEVICE)
|
||||
prefix_lens = torch.tensor([0], dtype=torch.int64, device=_DEVICE)
|
||||
extend_seq_lens = torch.tensor([0], dtype=torch.int64, device=_DEVICE)
|
||||
|
||||
triton_v = VerifyPlan.allocate(
|
||||
verify_capacity=64, device=_DEVICE
|
||||
).zero_for_testing_()
|
||||
triton_w = WritePlan.allocate(
|
||||
write_req_capacity=8, device=_DEVICE
|
||||
).zero_for_testing_()
|
||||
ref_v = VerifyPlan.allocate(verify_capacity=64, device=_DEVICE).zero_for_testing_()
|
||||
ref_w = WritePlan.allocate(write_req_capacity=8, device=_DEVICE).zero_for_testing_()
|
||||
|
||||
_run_both_plan(
|
||||
triton_verify=triton_v,
|
||||
triton_write=triton_w,
|
||||
ref_verify=ref_v,
|
||||
ref_write=ref_w,
|
||||
req_pool_indices=req_pool_indices,
|
||||
prefix_lens=prefix_lens,
|
||||
extend_seq_lens=extend_seq_lens,
|
||||
req_to_token=req_to_token,
|
||||
extras=empty_extras(),
|
||||
swa_window_size=0,
|
||||
full_to_swa_index_mapping=None,
|
||||
)
|
||||
|
||||
_assert_plans_byte_equal(
|
||||
triton_verify=triton_v,
|
||||
triton_write=triton_w,
|
||||
ref_verify=ref_v,
|
||||
ref_write=ref_w,
|
||||
)
|
||||
|
||||
if not per_req_present:
|
||||
assert int(triton_v.verify_num_valid[0].item()) == 0
|
||||
bs = int(req_pool_indices.shape[0])
|
||||
assert int(triton_w.write_offsets[bs].item()) == 0
|
||||
|
||||
if per_req_present:
|
||||
assert int(triton_v.verify_num_valid[0].item()) == 8
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -1,842 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.kv_canary import consts
|
||||
from sglang.jit_kernel.kv_canary.plan import launch_canary_plan_kernels
|
||||
from sglang.jit_kernel.kv_canary.plan_ref import (
|
||||
launch_canary_plan_kernels_torch_reference,
|
||||
)
|
||||
from sglang.jit_kernel.kv_canary.verify import (
|
||||
CanaryLaunchTag,
|
||||
RealKvSource,
|
||||
VerifyOrWriteContext,
|
||||
VerifyPlan,
|
||||
launch_canary_verify_kernel,
|
||||
)
|
||||
from sglang.jit_kernel.kv_canary.verify_ref import (
|
||||
launch_canary_verify_kernel_torch_reference,
|
||||
)
|
||||
from sglang.jit_kernel.kv_canary.write import WritePlan, launch_canary_write_kernel
|
||||
from sglang.jit_kernel.kv_canary.write_ref import (
|
||||
launch_canary_write_kernel_torch_reference,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
|
||||
FakeViolationLog,
|
||||
assert_canary_buf_equal,
|
||||
assert_canary_state_equal,
|
||||
make_canary_buf,
|
||||
make_real_kv_sources,
|
||||
stamp_clean_chain,
|
||||
write_slot_fields,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._fixtures import (
|
||||
clone_real_kv_sources,
|
||||
empty_extras,
|
||||
make_req_to_token,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
|
||||
_DEVICE = torch.device("cuda")
|
||||
|
||||
|
||||
def _run_pipeline(
|
||||
*,
|
||||
real: bool,
|
||||
req_pool_indices: torch.Tensor,
|
||||
prefix_lens: torch.Tensor,
|
||||
extend_seq_lens: torch.Tensor,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
out_cache_loc: torch.Tensor,
|
||||
req_to_token: torch.Tensor,
|
||||
canary_buf: torch.Tensor,
|
||||
log: FakeViolationLog,
|
||||
extras: tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor],
|
||||
swa_window_size: int,
|
||||
full_to_swa_index_mapping: Optional[torch.Tensor],
|
||||
kernel_kind: CanaryLaunchTag,
|
||||
enable_write_verify_inputs: bool,
|
||||
expected_input_tokens: torch.Tensor,
|
||||
expected_input_positions: torch.Tensor,
|
||||
real_kv_sources: tuple[RealKvSource, ...],
|
||||
real_kv_hash_mode: consts.RealKvHashMode,
|
||||
verify_capacity: int,
|
||||
write_req_capacity: int,
|
||||
req_to_verify_expected_tokens: Optional[torch.Tensor] = None,
|
||||
req_to_verify_expected_tokens_valid_lens: Optional[torch.Tensor] = None,
|
||||
kv_token_id_vs_position_offset: int = 0,
|
||||
check_verify_expected_token: bool = True,
|
||||
) -> tuple[VerifyPlan, WritePlan]:
|
||||
_ = extras
|
||||
plan_v = VerifyPlan.allocate(verify_capacity=verify_capacity, device=_DEVICE)
|
||||
plan_w = WritePlan.allocate(write_req_capacity=write_req_capacity, device=_DEVICE)
|
||||
|
||||
# Existing pipeline tests that supply a pool but no per-req lens want "bound by full
|
||||
# row width" semantics. Synthesise that bound here so callers don't have to.
|
||||
if (
|
||||
req_to_verify_expected_tokens is not None
|
||||
and req_to_verify_expected_tokens_valid_lens is None
|
||||
):
|
||||
req_to_verify_expected_tokens_valid_lens = torch.full(
|
||||
(int(req_pool_indices.shape[0]),),
|
||||
int(req_to_verify_expected_tokens.shape[1]),
|
||||
dtype=torch.int64,
|
||||
device=req_pool_indices.device,
|
||||
)
|
||||
|
||||
plan_fn = (
|
||||
launch_canary_plan_kernels
|
||||
if real
|
||||
else launch_canary_plan_kernels_torch_reference
|
||||
)
|
||||
plan_fn(
|
||||
verify_plan_out=plan_v,
|
||||
write_plan_out=plan_w,
|
||||
req_pool_indices=req_pool_indices,
|
||||
prefix_lens=prefix_lens,
|
||||
extend_seq_lens=extend_seq_lens,
|
||||
req_to_token=req_to_token,
|
||||
swa_window_size=swa_window_size,
|
||||
full_to_swa_index_mapping=full_to_swa_index_mapping,
|
||||
verify_capacity=verify_capacity,
|
||||
req_to_verify_expected_tokens=req_to_verify_expected_tokens,
|
||||
req_to_verify_expected_tokens_valid_lens=req_to_verify_expected_tokens_valid_lens,
|
||||
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
|
||||
)
|
||||
|
||||
if real:
|
||||
context = VerifyOrWriteContext(
|
||||
canary_buf=canary_buf,
|
||||
kernel_kind=kernel_kind,
|
||||
violation_ring=log.ring,
|
||||
violation_write_index=log.write_index,
|
||||
slot_run_counter=log.slot_run_counter,
|
||||
kernel_run_counter=log.kernel_run_counter,
|
||||
enable_chain_position_assert=log.enable_chain_position_assert,
|
||||
real_kv_sources=real_kv_sources,
|
||||
real_kv_hash_mode=real_kv_hash_mode,
|
||||
)
|
||||
launch_canary_write_kernel(
|
||||
context=context,
|
||||
plan=plan_w,
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
enable_write_input_assert=enable_write_verify_inputs,
|
||||
expected_input_tokens=expected_input_tokens,
|
||||
expected_input_positions=expected_input_positions,
|
||||
)
|
||||
launch_canary_verify_kernel(
|
||||
context=context,
|
||||
plan=plan_v,
|
||||
check_verify_expected_token=check_verify_expected_token,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
else:
|
||||
launch_canary_write_kernel_torch_reference(
|
||||
context=VerifyOrWriteContext(
|
||||
canary_buf=canary_buf,
|
||||
kernel_kind=kernel_kind,
|
||||
violation_ring=log.ring,
|
||||
violation_write_index=log.write_index,
|
||||
slot_run_counter=log.slot_run_counter,
|
||||
kernel_run_counter=log.kernel_run_counter,
|
||||
enable_chain_position_assert=log.enable_chain_position_assert,
|
||||
real_kv_sources=real_kv_sources,
|
||||
real_kv_hash_mode=real_kv_hash_mode,
|
||||
),
|
||||
plan=plan_w,
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
enable_write_input_assert=enable_write_verify_inputs,
|
||||
expected_input_tokens=expected_input_tokens,
|
||||
expected_input_positions=expected_input_positions,
|
||||
)
|
||||
launch_canary_verify_kernel_torch_reference(
|
||||
context=VerifyOrWriteContext(
|
||||
canary_buf=canary_buf,
|
||||
kernel_kind=kernel_kind,
|
||||
violation_ring=log.ring,
|
||||
violation_write_index=log.write_index,
|
||||
slot_run_counter=log.slot_run_counter,
|
||||
kernel_run_counter=log.kernel_run_counter,
|
||||
enable_chain_position_assert=log.enable_chain_position_assert,
|
||||
real_kv_sources=real_kv_sources,
|
||||
real_kv_hash_mode=real_kv_hash_mode,
|
||||
),
|
||||
plan=plan_v,
|
||||
check_verify_expected_token=check_verify_expected_token,
|
||||
)
|
||||
|
||||
return plan_v, plan_w
|
||||
|
||||
|
||||
def _run_both_and_assert_pipeline_equal(
|
||||
*,
|
||||
req_pool_indices: torch.Tensor,
|
||||
prefix_lens: torch.Tensor,
|
||||
extend_seq_lens: torch.Tensor,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
out_cache_loc: torch.Tensor,
|
||||
req_to_token: torch.Tensor,
|
||||
num_slots: int,
|
||||
extras: tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor],
|
||||
swa_window_size: int = 0,
|
||||
full_to_swa_index_mapping: Optional[torch.Tensor] = None,
|
||||
kernel_kind: CanaryLaunchTag = CanaryLaunchTag.HEAD_K_FULL,
|
||||
enable_write_verify_inputs: bool = False,
|
||||
expected_input_tokens: Optional[torch.Tensor] = None,
|
||||
expected_input_positions: Optional[torch.Tensor] = None,
|
||||
real_kv_sources_real: tuple[RealKvSource, ...] = (),
|
||||
real_kv_sources_ref: tuple[RealKvSource, ...] = (),
|
||||
real_kv_hash_mode: consts.RealKvHashMode = consts.RealKvHashMode.NONE,
|
||||
ring_capacity: int = 64,
|
||||
verify_capacity: int = 256,
|
||||
write_req_capacity: int = 16,
|
||||
assert_ring_equal: bool = True,
|
||||
initial_canary_buf: Optional[torch.Tensor] = None,
|
||||
req_to_verify_expected_tokens: Optional[torch.Tensor] = None,
|
||||
kv_token_id_vs_position_offset: int = 0,
|
||||
check_verify_expected_token: bool = True,
|
||||
) -> tuple[
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
FakeViolationLog,
|
||||
FakeViolationLog,
|
||||
VerifyPlan,
|
||||
WritePlan,
|
||||
VerifyPlan,
|
||||
WritePlan,
|
||||
]:
|
||||
# The kernel rejects non-None expected_* tensors when enable_write_verify_inputs=False
|
||||
# (sanity check to catch caller bugs), so only synthesise zero placeholders in the
|
||||
# branch that will actually assert against them.
|
||||
if enable_write_verify_inputs:
|
||||
total_tokens = int(input_ids.shape[0])
|
||||
if expected_input_tokens is None:
|
||||
expected_input_tokens = torch.zeros(
|
||||
total_tokens, dtype=torch.int64, device=_DEVICE
|
||||
)
|
||||
if expected_input_positions is None:
|
||||
expected_input_positions = torch.zeros(
|
||||
total_tokens, dtype=torch.int64, device=_DEVICE
|
||||
)
|
||||
|
||||
if initial_canary_buf is None:
|
||||
buf_real = make_canary_buf(num_slots=num_slots, device=_DEVICE)
|
||||
else:
|
||||
buf_real = initial_canary_buf.clone()
|
||||
buf_ref = buf_real.clone()
|
||||
log_real = FakeViolationLog.allocate(capacity=ring_capacity, device=_DEVICE)
|
||||
log_ref = FakeViolationLog.allocate(capacity=ring_capacity, device=_DEVICE)
|
||||
|
||||
shared: dict[str, Any] = dict(
|
||||
req_pool_indices=req_pool_indices,
|
||||
prefix_lens=prefix_lens,
|
||||
extend_seq_lens=extend_seq_lens,
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
req_to_token=req_to_token,
|
||||
extras=extras,
|
||||
swa_window_size=swa_window_size,
|
||||
full_to_swa_index_mapping=full_to_swa_index_mapping,
|
||||
kernel_kind=kernel_kind,
|
||||
enable_write_verify_inputs=enable_write_verify_inputs,
|
||||
expected_input_tokens=expected_input_tokens,
|
||||
expected_input_positions=expected_input_positions,
|
||||
real_kv_hash_mode=real_kv_hash_mode,
|
||||
verify_capacity=verify_capacity,
|
||||
write_req_capacity=write_req_capacity,
|
||||
req_to_verify_expected_tokens=req_to_verify_expected_tokens,
|
||||
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
|
||||
check_verify_expected_token=check_verify_expected_token,
|
||||
)
|
||||
|
||||
plan_v_real, plan_w_real = _run_pipeline(
|
||||
real=True,
|
||||
canary_buf=buf_real,
|
||||
log=log_real,
|
||||
real_kv_sources=real_kv_sources_real,
|
||||
**shared,
|
||||
)
|
||||
plan_v_ref, plan_w_ref = _run_pipeline(
|
||||
real=False,
|
||||
canary_buf=buf_ref,
|
||||
log=log_ref,
|
||||
real_kv_sources=real_kv_sources_ref,
|
||||
**shared,
|
||||
)
|
||||
|
||||
assert_canary_buf_equal(buf_a=buf_real, buf_b=buf_ref)
|
||||
if assert_ring_equal:
|
||||
assert_canary_state_equal(log_a=log_real, log_b=log_ref)
|
||||
else:
|
||||
assert torch.equal(log_real.write_index, log_ref.write_index)
|
||||
assert torch.equal(log_real.slot_run_counter, log_ref.slot_run_counter)
|
||||
assert torch.equal(log_real.kernel_run_counter, log_ref.kernel_run_counter)
|
||||
|
||||
return (
|
||||
buf_real,
|
||||
buf_ref,
|
||||
log_real,
|
||||
log_ref,
|
||||
plan_v_real,
|
||||
plan_w_real,
|
||||
plan_v_ref,
|
||||
plan_w_ref,
|
||||
)
|
||||
|
||||
|
||||
def _t(values: list[int]) -> torch.Tensor:
|
||||
return torch.tensor(values, dtype=torch.int64, device=_DEVICE)
|
||||
|
||||
|
||||
def _linear_r2t(*, max_reqs: int = 4, max_seq_len: int = 16) -> torch.Tensor:
|
||||
return make_req_to_token(
|
||||
kind="linear", max_reqs=max_reqs, max_seq_len=max_seq_len, device=_DEVICE
|
||||
)
|
||||
|
||||
|
||||
def _zero_no_write_inputs() -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""``(input_ids, positions, out_cache_loc)`` zero placeholders for extend_seq_lens=0 tests."""
|
||||
zeros = torch.zeros(1, dtype=torch.int64, device=_DEVICE)
|
||||
return zeros.clone(), zeros.clone(), zeros.clone()
|
||||
|
||||
|
||||
def _contiguous_out_cache_loc(
|
||||
*, req_pool_idx: int, start: int, count: int, max_seq_len: int = 16
|
||||
) -> torch.Tensor:
|
||||
return _t([req_pool_idx * max_seq_len + start + i for i in range(count)])
|
||||
|
||||
|
||||
def _stamp_linear_prefix(
|
||||
*,
|
||||
initial_buf: torch.Tensor,
|
||||
initial_ref: torch.Tensor,
|
||||
req_pool_idx: int,
|
||||
prefix_len: int,
|
||||
tokens: list[int],
|
||||
max_seq_len: int = 16,
|
||||
) -> None:
|
||||
"""Stamp clean chain for slots ``[rp*max_seq_len + 0 .. + prefix_len)`` at positions ``0..prefix_len``."""
|
||||
stamp_clean_chain(
|
||||
cuda_buf=initial_buf,
|
||||
ref_buf=initial_ref,
|
||||
slot_indices=[req_pool_idx * max_seq_len + pos for pos in range(prefix_len)],
|
||||
tokens=tokens,
|
||||
positions=list(range(prefix_len)),
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_basic_5_step_single_req() -> None:
|
||||
"""Single req, prefix_len=0, extend_seq_len=5: basic plan→write→verify byte-equal."""
|
||||
_run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([0]),
|
||||
extend_seq_lens=_t([5]),
|
||||
input_ids=_t([10, 20, 30, 40, 50]),
|
||||
positions=_t([0, 1, 2, 3, 4]),
|
||||
out_cache_loc=_contiguous_out_cache_loc(req_pool_idx=1, start=0, count=5),
|
||||
req_to_token=_linear_r2t(),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
swa_window_size=0,
|
||||
full_to_swa_index_mapping=None,
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_multi_req_mixed_extend_decode() -> None:
|
||||
"""bs=3: pure extend req, decode req (prefix+1 extend), and padding sentinel row."""
|
||||
max_seq_len = 16
|
||||
_run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1, 2, 0]),
|
||||
prefix_lens=_t([0, 5, 0]),
|
||||
extend_seq_lens=_t([4, 1, 0]),
|
||||
input_ids=_t([11, 12, 13, 14, 21]),
|
||||
positions=_t([0, 1, 2, 3, 5]),
|
||||
out_cache_loc=_t(
|
||||
[
|
||||
1 * max_seq_len + 0,
|
||||
1 * max_seq_len + 1,
|
||||
1 * max_seq_len + 2,
|
||||
1 * max_seq_len + 3,
|
||||
2 * max_seq_len + 5,
|
||||
]
|
||||
),
|
||||
req_to_token=_linear_r2t(max_reqs=8, max_seq_len=max_seq_len),
|
||||
num_slots=128,
|
||||
extras=empty_extras(),
|
||||
swa_window_size=0,
|
||||
full_to_swa_index_mapping=None,
|
||||
write_req_capacity=4,
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_swa_window() -> None:
|
||||
"""SWA window=4, prefix_len=6: verify covers window [2,6), write covers extend tokens."""
|
||||
max_seq_len = 16
|
||||
max_reqs = 4
|
||||
full_to_swa_index_mapping = torch.arange(
|
||||
max_reqs * max_seq_len + 1, dtype=torch.int64, device=_DEVICE
|
||||
)
|
||||
|
||||
_run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([6]),
|
||||
extend_seq_lens=_t([2]),
|
||||
input_ids=_t([100, 101]),
|
||||
positions=_t([6, 7]),
|
||||
out_cache_loc=_t(
|
||||
[
|
||||
full_to_swa_index_mapping[1 * max_seq_len + 6].item(),
|
||||
full_to_swa_index_mapping[1 * max_seq_len + 7].item(),
|
||||
]
|
||||
),
|
||||
req_to_token=_linear_r2t(max_reqs=max_reqs, max_seq_len=max_seq_len),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
swa_window_size=4,
|
||||
full_to_swa_index_mapping=full_to_swa_index_mapping,
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_sweep_no_write() -> None:
|
||||
"""All extend_seq_lens=0: write_step is no-op, verify sweeps prefix, buf unchanged."""
|
||||
prefix_len = 4
|
||||
input_ids, positions, out_cache_loc = _zero_no_write_inputs()
|
||||
|
||||
initial_buf = make_canary_buf(num_slots=64, device=_DEVICE)
|
||||
initial_ref = initial_buf.clone()
|
||||
_stamp_linear_prefix(
|
||||
initial_buf=initial_buf,
|
||||
initial_ref=initial_ref,
|
||||
req_pool_idx=1,
|
||||
prefix_len=prefix_len,
|
||||
tokens=[100 + pos for pos in range(prefix_len)],
|
||||
)
|
||||
|
||||
buf_real, buf_ref, log_real, log_ref, plan_v_real, plan_w_real, _, _ = (
|
||||
_run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([prefix_len]),
|
||||
extend_seq_lens=_t([0]),
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
req_to_token=_linear_r2t(),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
swa_window_size=0,
|
||||
full_to_swa_index_mapping=None,
|
||||
initial_canary_buf=initial_buf,
|
||||
)
|
||||
)
|
||||
|
||||
assert int(plan_v_real.verify_num_valid[0].item()) == prefix_len
|
||||
assert int(plan_w_real.write_num_valid_reqs[0].item()) == 1
|
||||
assert int(plan_w_real.write_offsets[1].item()) == 0
|
||||
assert torch.equal(buf_real, initial_buf)
|
||||
assert torch.equal(buf_ref, initial_buf)
|
||||
assert int(log_real.write_index[0].item()) == 0
|
||||
assert int(log_ref.write_index[0].item()) == 0
|
||||
assert int(log_real.slot_run_counter[0].item()) == prefix_len
|
||||
assert int(log_ref.slot_run_counter[0].item()) == prefix_len
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"real_kv_hash_mode",
|
||||
[
|
||||
consts.RealKvHashMode.NONE,
|
||||
consts.RealKvHashMode.PARTIAL,
|
||||
consts.RealKvHashMode.ALL,
|
||||
],
|
||||
)
|
||||
def test_pipeline_real_kv_mode(real_kv_hash_mode: consts.RealKvHashMode) -> None:
|
||||
"""real_kv_hash_mode OFF/PARTIAL/ALL: real and ref use cloned sources to prevent ALL-mode hash aliasing."""
|
||||
sources_real = make_real_kv_sources(count=2, num_slots=64, device=_DEVICE)
|
||||
sources_ref = clone_real_kv_sources(sources_real)
|
||||
|
||||
_run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([0]),
|
||||
extend_seq_lens=_t([3]),
|
||||
input_ids=_t([5, 6, 7]),
|
||||
positions=_t([0, 1, 2]),
|
||||
out_cache_loc=_contiguous_out_cache_loc(req_pool_idx=1, start=0, count=3),
|
||||
req_to_token=_linear_r2t(),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
real_kv_sources_real=sources_real,
|
||||
real_kv_sources_ref=sources_ref,
|
||||
real_kv_hash_mode=real_kv_hash_mode,
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_pseudo_mode_on_match() -> None:
|
||||
"""enable_write_verify_inputs=ON, expected==actual: zero violations, buf byte-equal."""
|
||||
input_ids = _t([1, 2, 3, 4])
|
||||
positions = _t([0, 1, 2, 3])
|
||||
|
||||
_, _, log_real, log_ref, _, _, _, _ = _run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([0]),
|
||||
extend_seq_lens=_t([4]),
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=_contiguous_out_cache_loc(req_pool_idx=1, start=0, count=4),
|
||||
req_to_token=_linear_r2t(),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
enable_write_verify_inputs=True,
|
||||
expected_input_tokens=input_ids.clone(),
|
||||
expected_input_positions=positions.clone(),
|
||||
)
|
||||
|
||||
assert int(log_real.write_index[0].item()) == 0
|
||||
assert int(log_ref.write_index[0].item()) == 0
|
||||
|
||||
|
||||
def test_pipeline_pseudo_mode_on_token_mismatch_then_verify_clean() -> None:
|
||||
"""enable_write_verify_inputs=ON, expected tokens all wrong: write records N violations."""
|
||||
n_tokens = 3
|
||||
positions = _t([0, 1, 2])
|
||||
|
||||
_, _, log_real, log_ref, _, _, _, _ = _run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([0]),
|
||||
extend_seq_lens=_t([3]),
|
||||
input_ids=_t([10, 20, 30]),
|
||||
positions=positions,
|
||||
out_cache_loc=_contiguous_out_cache_loc(req_pool_idx=1, start=0, count=3),
|
||||
req_to_token=_linear_r2t(),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
enable_write_verify_inputs=True,
|
||||
expected_input_tokens=_t([99, 99, 99]),
|
||||
expected_input_positions=positions.clone(),
|
||||
ring_capacity=64,
|
||||
)
|
||||
|
||||
write_violations = int(log_real.write_index[0].item())
|
||||
assert (
|
||||
write_violations == n_tokens
|
||||
), f"expected {n_tokens} write violations, got {write_violations}"
|
||||
|
||||
|
||||
def test_pipeline_empty_batch() -> None:
|
||||
"""bs=1 with req_pool_idx=0 (padding): write and verify are no-op, kernel_run_counter == 2 (write+verify)."""
|
||||
input_ids, positions, out_cache_loc = _zero_no_write_inputs()
|
||||
|
||||
_, _, log_real, log_ref, _, _, _, _ = _run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([0]),
|
||||
prefix_lens=_t([0]),
|
||||
extend_seq_lens=_t([0]),
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
req_to_token=_linear_r2t(),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
)
|
||||
|
||||
assert int(log_real.kernel_run_counter[0].item()) == 2
|
||||
assert int(log_ref.kernel_run_counter[0].item()) == 2
|
||||
assert int(log_real.write_index[0].item()) == 0
|
||||
|
||||
|
||||
def test_pipeline_negative_slot_swa_out_of_window() -> None:
|
||||
"""SWA: some out_cache_loc entries map to -1 (out-of-window); write_step skips them, buf unchanged."""
|
||||
max_seq_len = 16
|
||||
max_reqs = 4
|
||||
|
||||
full_to_swa_index_mapping = torch.arange(
|
||||
max_reqs * max_seq_len + 1, dtype=torch.int64, device=_DEVICE
|
||||
)
|
||||
full_to_swa_index_mapping[1 * max_seq_len + 6] = -1
|
||||
full_to_swa_index_mapping[1 * max_seq_len + 7] = -1
|
||||
|
||||
_run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([6]),
|
||||
extend_seq_lens=_t([4]),
|
||||
input_ids=_t([100, 101, 102, 103]),
|
||||
positions=_t([6, 7, 8, 9]),
|
||||
out_cache_loc=_t([-1, -1, 1 * max_seq_len + 8, 1 * max_seq_len + 9]),
|
||||
req_to_token=_linear_r2t(max_reqs=max_reqs, max_seq_len=max_seq_len),
|
||||
num_slots=128,
|
||||
extras=empty_extras(),
|
||||
swa_window_size=4,
|
||||
full_to_swa_index_mapping=full_to_swa_index_mapping,
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_ring_overflow_via_real_plan() -> None:
|
||||
"""Verify detects >capacity violations when prev_hash is pre-corrupted; write_index byte-equal, ring relaxed."""
|
||||
max_seq_len = 16
|
||||
max_reqs = 4
|
||||
req_to_token = _linear_r2t(max_reqs=max_reqs, max_seq_len=max_seq_len)
|
||||
n_slots = 8
|
||||
|
||||
req_pool_indices = _t([1])
|
||||
prefix_lens = _t([n_slots])
|
||||
extend_seq_lens = _t([0])
|
||||
input_ids, positions, out_cache_loc = _zero_no_write_inputs()
|
||||
|
||||
num_slots = max_reqs * max_seq_len
|
||||
|
||||
# Step 1: pre-pollute canary_buf slots [0..n_slots) with wrong prev_hash so verify fires n_slots violations.
|
||||
buf_real = make_canary_buf(num_slots=num_slots, device=_DEVICE)
|
||||
buf_ref = make_canary_buf(num_slots=num_slots, device=_DEVICE)
|
||||
for slot_idx in range(n_slots):
|
||||
full_slot = 1 * max_seq_len + slot_idx
|
||||
for buf in (buf_real, buf_ref):
|
||||
write_slot_fields(
|
||||
canary_buf=buf,
|
||||
slot_idx=full_slot,
|
||||
token=slot_idx + 1,
|
||||
position=slot_idx,
|
||||
prev_hash=0x1234_DEAD_BEEF_0000 + slot_idx,
|
||||
real_kv_hash=0,
|
||||
)
|
||||
|
||||
# Step 2: run real pipeline (plan + no write + verify); overflow ring capacity=4 with all n_slots violations.
|
||||
ring_capacity = 4
|
||||
log_real = FakeViolationLog.allocate(capacity=ring_capacity, device=_DEVICE)
|
||||
log_ref = FakeViolationLog.allocate(capacity=ring_capacity, device=_DEVICE)
|
||||
plan_v_real = VerifyPlan.allocate(verify_capacity=256, device=_DEVICE)
|
||||
plan_w_real = WritePlan.allocate(write_req_capacity=4, device=_DEVICE)
|
||||
plan_v_ref = VerifyPlan.allocate(verify_capacity=256, device=_DEVICE)
|
||||
plan_w_ref = WritePlan.allocate(write_req_capacity=4, device=_DEVICE)
|
||||
|
||||
launch_canary_plan_kernels(
|
||||
verify_plan_out=plan_v_real,
|
||||
write_plan_out=plan_w_real,
|
||||
req_pool_indices=req_pool_indices,
|
||||
prefix_lens=prefix_lens,
|
||||
extend_seq_lens=extend_seq_lens,
|
||||
req_to_token=req_to_token,
|
||||
swa_window_size=0,
|
||||
full_to_swa_index_mapping=None,
|
||||
verify_capacity=int(plan_v_real.verify_slot_indices.shape[0]),
|
||||
req_to_verify_expected_tokens=None,
|
||||
req_to_verify_expected_tokens_valid_lens=None,
|
||||
kv_token_id_vs_position_offset=0,
|
||||
)
|
||||
launch_canary_plan_kernels_torch_reference(
|
||||
verify_plan_out=plan_v_ref,
|
||||
write_plan_out=plan_w_ref,
|
||||
req_pool_indices=req_pool_indices,
|
||||
prefix_lens=prefix_lens,
|
||||
extend_seq_lens=extend_seq_lens,
|
||||
req_to_token=req_to_token,
|
||||
swa_window_size=0,
|
||||
full_to_swa_index_mapping=None,
|
||||
verify_capacity=int(plan_v_ref.verify_slot_indices.shape[0]),
|
||||
req_to_verify_expected_tokens=None,
|
||||
req_to_verify_expected_tokens_valid_lens=None,
|
||||
kv_token_id_vs_position_offset=0,
|
||||
)
|
||||
|
||||
launch_canary_verify_kernel(
|
||||
context=VerifyOrWriteContext(
|
||||
canary_buf=buf_real,
|
||||
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
|
||||
violation_ring=log_real.ring,
|
||||
violation_write_index=log_real.write_index,
|
||||
slot_run_counter=log_real.slot_run_counter,
|
||||
kernel_run_counter=log_real.kernel_run_counter,
|
||||
enable_chain_position_assert=log_real.enable_chain_position_assert,
|
||||
real_kv_sources=(),
|
||||
real_kv_hash_mode=consts.RealKvHashMode.NONE,
|
||||
),
|
||||
plan=plan_v_real,
|
||||
check_verify_expected_token=True,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
launch_canary_verify_kernel_torch_reference(
|
||||
context=VerifyOrWriteContext(
|
||||
canary_buf=buf_ref,
|
||||
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
|
||||
violation_ring=log_ref.ring,
|
||||
violation_write_index=log_ref.write_index,
|
||||
slot_run_counter=log_ref.slot_run_counter,
|
||||
kernel_run_counter=log_ref.kernel_run_counter,
|
||||
enable_chain_position_assert=log_ref.enable_chain_position_assert,
|
||||
real_kv_sources=(),
|
||||
real_kv_hash_mode=consts.RealKvHashMode.NONE,
|
||||
),
|
||||
plan=plan_v_ref,
|
||||
check_verify_expected_token=True,
|
||||
)
|
||||
|
||||
# Step 3: write_index byte-equal; ring contents relaxed (atomic order not guaranteed under overflow).
|
||||
assert torch.equal(log_real.write_index, log_ref.write_index)
|
||||
assert int(log_real.write_index[0].item()) == n_slots
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kernel_kind", [CanaryLaunchTag.HEAD_K_FULL, CanaryLaunchTag.SWEEP_V_SWA]
|
||||
)
|
||||
def test_pipeline_kernel_kind_propagates(kernel_kind: CanaryLaunchTag) -> None:
|
||||
"""Different CanaryLaunchTag values: violation ring's kernel_kind field matches on both sides."""
|
||||
max_seq_len = 16
|
||||
input_ids, positions, out_cache_loc = _zero_no_write_inputs()
|
||||
|
||||
initial_buf = make_canary_buf(num_slots=64, device=_DEVICE)
|
||||
write_slot_fields(
|
||||
canary_buf=initial_buf,
|
||||
slot_idx=1 * max_seq_len,
|
||||
token=7,
|
||||
position=99,
|
||||
prev_hash=0,
|
||||
real_kv_hash=0,
|
||||
)
|
||||
|
||||
_, _, log_real, log_ref, _, _, _, _ = _run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([1]),
|
||||
extend_seq_lens=_t([0]),
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
req_to_token=_linear_r2t(max_seq_len=max_seq_len),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
kernel_kind=kernel_kind,
|
||||
initial_canary_buf=initial_buf,
|
||||
)
|
||||
|
||||
assert int(log_real.write_index[0].item()) == 1
|
||||
assert int(log_ref.write_index[0].item()) == 1
|
||||
assert int(log_real.ring[0, consts.VIOLATION_FIELD_KERNEL_KIND].item()) == int(
|
||||
kernel_kind
|
||||
)
|
||||
assert int(log_ref.ring[0, consts.VIOLATION_FIELD_KERNEL_KIND].item()) == int(
|
||||
kernel_kind
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_token_mismatch_detected_via_pool() -> None:
|
||||
"""plan-pool gather + verify-token check: stamped wrong token id raises VERIFY_TOKEN_MISMATCH."""
|
||||
max_seq_len = 16
|
||||
max_reqs = 4
|
||||
prefix_len = 4
|
||||
input_ids, positions, out_cache_loc = _zero_no_write_inputs()
|
||||
|
||||
expected_tokens = [1000 + pos for pos in range(prefix_len)]
|
||||
pool = torch.full((max_reqs, max_seq_len), -999, dtype=torch.int32, device=_DEVICE)
|
||||
for pos, token in enumerate(expected_tokens):
|
||||
pool[1, pos] = token
|
||||
|
||||
stored_tokens = [token + 1 for token in expected_tokens]
|
||||
|
||||
initial_buf = make_canary_buf(num_slots=64, device=_DEVICE)
|
||||
initial_ref = initial_buf.clone()
|
||||
_stamp_linear_prefix(
|
||||
initial_buf=initial_buf,
|
||||
initial_ref=initial_ref,
|
||||
req_pool_idx=1,
|
||||
prefix_len=prefix_len,
|
||||
tokens=stored_tokens,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
|
||||
_, _, log_real, log_ref, _, _, _, _ = _run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([prefix_len]),
|
||||
extend_seq_lens=_t([0]),
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
req_to_token=_linear_r2t(max_reqs=max_reqs, max_seq_len=max_seq_len),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
swa_window_size=0,
|
||||
full_to_swa_index_mapping=None,
|
||||
initial_canary_buf=initial_buf,
|
||||
req_to_verify_expected_tokens=pool,
|
||||
kv_token_id_vs_position_offset=0,
|
||||
check_verify_expected_token=True,
|
||||
)
|
||||
|
||||
assert int(log_real.write_index[0].item()) == prefix_len
|
||||
assert int(log_ref.write_index[0].item()) == prefix_len
|
||||
# Ring rows may land in any order; collect stored/expected pairs and compare as sets.
|
||||
observed_pairs: set[tuple[int, int]] = set()
|
||||
for row_idx in range(prefix_len):
|
||||
fail_bits = int(
|
||||
log_real.ring[row_idx, consts.VIOLATION_FIELD_FAIL_REASON_BITS].item()
|
||||
)
|
||||
assert fail_bits & int(
|
||||
consts.FailReason.VERIFY_TOKEN_MISMATCH
|
||||
), f"row {row_idx}: VERIFY_TOKEN_MISMATCH bit missing in {fail_bits:#b}"
|
||||
stored = int(log_real.ring[row_idx, consts.VIOLATION_FIELD_STORED_TOKEN].item())
|
||||
expected = int(
|
||||
log_real.ring[row_idx, consts.VIOLATION_FIELD_EXPECTED_TOKEN].item()
|
||||
)
|
||||
observed_pairs.add((stored, expected))
|
||||
expected_pairs = {(stored_tokens[i], expected_tokens[i]) for i in range(prefix_len)}
|
||||
assert observed_pairs == expected_pairs
|
||||
|
||||
|
||||
def test_pipeline_eagle_offset_plus_1_byte_equal() -> None:
|
||||
"""plan-pool + offset=+1 full pipeline: stamped tokens match pool[rp, pos+1], no violations CUDA vs ref byte-equal."""
|
||||
max_seq_len = 16
|
||||
max_reqs = 4
|
||||
prefix_len = 4
|
||||
input_ids, positions, out_cache_loc = _zero_no_write_inputs()
|
||||
|
||||
stored_tokens = [2000 + pos for pos in range(prefix_len)]
|
||||
pool = torch.full((max_reqs, max_seq_len), -999, dtype=torch.int32, device=_DEVICE)
|
||||
for pos in range(prefix_len):
|
||||
# offset=+1 means kernel gathers from pool[rp, pos + 1], so place stored_tokens[pos] there.
|
||||
pool[1, pos + 1] = stored_tokens[pos]
|
||||
|
||||
initial_buf = make_canary_buf(num_slots=64, device=_DEVICE)
|
||||
initial_ref = initial_buf.clone()
|
||||
_stamp_linear_prefix(
|
||||
initial_buf=initial_buf,
|
||||
initial_ref=initial_ref,
|
||||
req_pool_idx=1,
|
||||
prefix_len=prefix_len,
|
||||
tokens=stored_tokens,
|
||||
max_seq_len=max_seq_len,
|
||||
)
|
||||
|
||||
_, _, log_real, log_ref, _, _, _, _ = _run_both_and_assert_pipeline_equal(
|
||||
req_pool_indices=_t([1]),
|
||||
prefix_lens=_t([prefix_len]),
|
||||
extend_seq_lens=_t([0]),
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
req_to_token=_linear_r2t(max_reqs=max_reqs, max_seq_len=max_seq_len),
|
||||
num_slots=64,
|
||||
extras=empty_extras(),
|
||||
swa_window_size=0,
|
||||
full_to_swa_index_mapping=None,
|
||||
initial_canary_buf=initial_buf,
|
||||
req_to_verify_expected_tokens=pool,
|
||||
kv_token_id_vs_position_offset=1,
|
||||
check_verify_expected_token=True,
|
||||
)
|
||||
|
||||
assert int(log_real.write_index[0].item()) == 0
|
||||
assert int(log_ref.write_index[0].item()) == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -1,222 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.tests.kv_canary._differential import _run_both_plan
|
||||
from sglang.jit_kernel.tests.kv_canary._fixtures import (
|
||||
allocate_plan_pair,
|
||||
derive_plan_capacity,
|
||||
make_lut,
|
||||
make_padding_mask,
|
||||
make_req_to_token,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._fuzz_driver import (
|
||||
FUZZ_SEEDS_PR,
|
||||
run_fuzz_combo,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._invariants import PlanInvariants
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
|
||||
_DEVICE = torch.device("cuda")
|
||||
|
||||
_FUZZ_ITER_PER_SEED = 50
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, kw_only=True)
|
||||
class PlanFuzzInputs:
|
||||
req_pool_indices: torch.Tensor
|
||||
prefix_lens: torch.Tensor
|
||||
extend_seq_lens: torch.Tensor
|
||||
req_to_token: torch.Tensor
|
||||
swa_window_size: int
|
||||
full_to_swa_index_mapping: Optional[torch.Tensor]
|
||||
verify_capacity: int
|
||||
write_req_capacity: int
|
||||
req_to_verify_expected_tokens: Optional[torch.Tensor]
|
||||
kv_token_id_vs_position_offset: int
|
||||
|
||||
|
||||
def _draw_random_plan_inputs(rng: random.Random) -> PlanFuzzInputs:
|
||||
bs = rng.randint(1, 16)
|
||||
max_seq_len = rng.choice([8, 16, 64, 128, 256])
|
||||
swa_enabled = rng.random() < 0.5
|
||||
swa_window_size = (
|
||||
rng.choice([4, 16, 64, max_seq_len, max(2, max_seq_len // 3)])
|
||||
if swa_enabled
|
||||
else 0
|
||||
)
|
||||
swa_window_size = min(swa_window_size, max_seq_len)
|
||||
lut_kind = (
|
||||
rng.choice(["identity", "shift", "permutation", "with_oob"])
|
||||
if swa_enabled
|
||||
else None
|
||||
)
|
||||
rtt_kind = rng.choice(["linear", "sparse_permuted"])
|
||||
padding_kind = rng.choice(["none", "trailing", "interleaved"])
|
||||
capacity_kind = rng.choice(["loose", "tight_match", "under_by_one"])
|
||||
|
||||
max_reqs = max(bs + 2, 4)
|
||||
pool_size = max_reqs * max_seq_len
|
||||
rtt = make_req_to_token(
|
||||
kind=rtt_kind,
|
||||
max_reqs=max_reqs,
|
||||
max_seq_len=max_seq_len,
|
||||
device=_DEVICE,
|
||||
rng=rng,
|
||||
)
|
||||
padding_mask = make_padding_mask(bs=bs, kind=padding_kind, rng=rng)
|
||||
req_pool_indices_list: list[int] = []
|
||||
prefix_lens_list: list[int] = []
|
||||
extend_seq_lens_list: list[int] = []
|
||||
for r in range(bs):
|
||||
if padding_mask[r]:
|
||||
req_pool_indices_list.append(0)
|
||||
prefix_lens_list.append(0)
|
||||
extend_seq_lens_list.append(0)
|
||||
else:
|
||||
req_pool_indices_list.append(rng.randint(1, max_reqs - 1))
|
||||
prefix_lens_list.append(rng.randint(0, max_seq_len - 1))
|
||||
extend_seq_lens_list.append(rng.randint(1, max(1, max_seq_len // 4)))
|
||||
req_pool_indices = torch.tensor(
|
||||
req_pool_indices_list, dtype=torch.int64, device=_DEVICE
|
||||
)
|
||||
prefix_lens = torch.tensor(prefix_lens_list, dtype=torch.int64, device=_DEVICE)
|
||||
extend_seq_lens = torch.tensor(
|
||||
extend_seq_lens_list, dtype=torch.int64, device=_DEVICE
|
||||
)
|
||||
|
||||
total_verify = 0
|
||||
for rpi, pfx in zip(req_pool_indices_list, prefix_lens_list):
|
||||
if rpi == 0:
|
||||
continue
|
||||
if swa_window_size > 0:
|
||||
window_start = max(0, pfx - swa_window_size)
|
||||
total_verify += max(0, pfx - window_start)
|
||||
else:
|
||||
total_verify += pfx
|
||||
|
||||
verify_capacity, write_req_capacity = derive_plan_capacity(
|
||||
kind=capacity_kind,
|
||||
total_verify=total_verify,
|
||||
extras_count=0,
|
||||
bs=bs,
|
||||
)
|
||||
|
||||
full_to_swa: Optional[torch.Tensor]
|
||||
if swa_window_size > 0 and lut_kind is not None:
|
||||
full_to_swa = make_lut(
|
||||
kind=lut_kind, pool_size=pool_size, device=_DEVICE, rng=rng
|
||||
)
|
||||
else:
|
||||
full_to_swa = None
|
||||
|
||||
expected_pool_present = rng.random() < 0.5
|
||||
kv_token_id_vs_position_offset = rng.choice([0, 1])
|
||||
expected_pool: Optional[torch.Tensor]
|
||||
if expected_pool_present:
|
||||
pool_max_context_len = rng.choice(
|
||||
[
|
||||
max(1, max_seq_len // 4),
|
||||
max(1, max_seq_len // 2),
|
||||
max_seq_len,
|
||||
]
|
||||
)
|
||||
expected_pool = torch.randint(
|
||||
low=0,
|
||||
high=50000,
|
||||
size=(max_reqs, pool_max_context_len),
|
||||
dtype=torch.int32,
|
||||
device=_DEVICE,
|
||||
)
|
||||
else:
|
||||
expected_pool = None
|
||||
|
||||
return PlanFuzzInputs(
|
||||
req_pool_indices=req_pool_indices,
|
||||
prefix_lens=prefix_lens,
|
||||
extend_seq_lens=extend_seq_lens,
|
||||
req_to_token=rtt,
|
||||
swa_window_size=swa_window_size,
|
||||
full_to_swa_index_mapping=full_to_swa,
|
||||
verify_capacity=verify_capacity,
|
||||
write_req_capacity=write_req_capacity,
|
||||
req_to_verify_expected_tokens=expected_pool,
|
||||
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
|
||||
)
|
||||
|
||||
|
||||
def _run_one(inputs: PlanFuzzInputs) -> tuple:
|
||||
triton_v, triton_w, ref_v, ref_w = allocate_plan_pair(
|
||||
verify_capacity=inputs.verify_capacity,
|
||||
write_req_capacity=inputs.write_req_capacity,
|
||||
)
|
||||
_run_both_plan(
|
||||
triton_verify=triton_v,
|
||||
triton_write=triton_w,
|
||||
ref_verify=ref_v,
|
||||
ref_write=ref_w,
|
||||
req_pool_indices=inputs.req_pool_indices,
|
||||
prefix_lens=inputs.prefix_lens,
|
||||
extend_seq_lens=inputs.extend_seq_lens,
|
||||
req_to_token=inputs.req_to_token,
|
||||
extras=(
|
||||
torch.empty(0, dtype=torch.int64, device=_DEVICE),
|
||||
torch.empty(0, dtype=torch.int64, device=_DEVICE),
|
||||
torch.empty(0, dtype=torch.int64, device=_DEVICE),
|
||||
torch.zeros(1, dtype=torch.int32, device=_DEVICE),
|
||||
),
|
||||
swa_window_size=inputs.swa_window_size,
|
||||
full_to_swa_index_mapping=inputs.full_to_swa_index_mapping,
|
||||
req_to_verify_expected_tokens=inputs.req_to_verify_expected_tokens,
|
||||
kv_token_id_vs_position_offset=inputs.kv_token_id_vs_position_offset,
|
||||
)
|
||||
PlanInvariants.assert_all(
|
||||
verify_plan=triton_v,
|
||||
write_plan=triton_w,
|
||||
req_pool_indices=inputs.req_pool_indices,
|
||||
prefix_lens=inputs.prefix_lens,
|
||||
extend_seq_lens=inputs.extend_seq_lens,
|
||||
swa_window_size=inputs.swa_window_size,
|
||||
extras_slot_indices=torch.empty(0, dtype=torch.int64, device=_DEVICE),
|
||||
extras_positions=torch.empty(0, dtype=torch.int64, device=_DEVICE),
|
||||
extras_prev_slot_indices=torch.empty(0, dtype=torch.int64, device=_DEVICE),
|
||||
extras_count=0,
|
||||
)
|
||||
return triton_v, triton_w
|
||||
|
||||
|
||||
def _summarize(inputs: PlanFuzzInputs) -> str:
|
||||
return (
|
||||
f"bs={int(inputs.req_pool_indices.shape[0])} "
|
||||
f"swa={inputs.swa_window_size} "
|
||||
f"verify_cap={inputs.verify_capacity} write_cap={inputs.write_req_capacity} "
|
||||
f"has_lut={inputs.full_to_swa_index_mapping is not None} "
|
||||
f"has_pool={inputs.req_to_verify_expected_tokens is not None} "
|
||||
f"offset={inputs.kv_token_id_vs_position_offset}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", FUZZ_SEEDS_PR)
|
||||
def test_plan_fuzz_full_combo(seed: int) -> None:
|
||||
"""Multi-dim plan fuzzer: random LUT/rtt/padding/capacity/swa × N iters, byte-equal."""
|
||||
run_fuzz_combo(
|
||||
seed,
|
||||
draw_fn=_draw_random_plan_inputs,
|
||||
run_one_fn=_run_one,
|
||||
summarize_fn=_summarize,
|
||||
n_iter=_FUZZ_ITER_PER_SEED,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,278 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.kv_canary.scatter_req_token_ids import (
|
||||
_SCATTER_BATCH_BLOCK,
|
||||
launch_scatter_req_token_ids_kernel,
|
||||
scatter_req_token_ids_torch_reference,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=10, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
|
||||
_DEVICE = torch.device("cuda")
|
||||
|
||||
|
||||
def _build_pool(*, max_reqs: int, max_context_len: int) -> torch.Tensor:
|
||||
return torch.zeros((max_reqs, max_context_len), dtype=torch.int32, device=_DEVICE)
|
||||
|
||||
|
||||
def _build_offsets(lens: list[int]) -> torch.Tensor:
|
||||
cumsum = [0]
|
||||
for n in lens:
|
||||
cumsum.append(cumsum[-1] + n)
|
||||
return torch.tensor(cumsum, dtype=torch.int64, device=_DEVICE)
|
||||
|
||||
|
||||
def _build_flat(seqs: list[list[int]]) -> torch.Tensor:
|
||||
flat: list[int] = []
|
||||
for s in seqs:
|
||||
flat.extend(s)
|
||||
return torch.tensor(flat, dtype=torch.int64, device=_DEVICE)
|
||||
|
||||
|
||||
class TestScatterReqTokenIds(CustomTestCase):
|
||||
def test_scatter_byte_equal_basic(self) -> None:
|
||||
"""Triton output matches the PyTorch reference for a small mixed batch."""
|
||||
seqs = [[10, 20, 30], [40, 50], [60, 70, 80, 90]]
|
||||
lens = [len(s) for s in seqs]
|
||||
rp = [3, 1, 5]
|
||||
|
||||
flat = _build_flat(seqs)
|
||||
offsets = _build_offsets(lens)
|
||||
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
|
||||
triton_pool = _build_pool(max_reqs=8, max_context_len=16)
|
||||
ref_pool = _build_pool(max_reqs=8, max_context_len=16)
|
||||
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=triton_pool,
|
||||
)
|
||||
scatter_req_token_ids_torch_reference(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=ref_pool,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
self.assertTrue(torch.equal(triton_pool, ref_pool))
|
||||
# Spot-check: req in slot 3 holds [10,20,30,0,0,...] etc.
|
||||
self.assertEqual(triton_pool[3, :3].tolist(), [10, 20, 30])
|
||||
self.assertEqual(triton_pool[1, :2].tolist(), [40, 50])
|
||||
self.assertEqual(triton_pool[5, :4].tolist(), [60, 70, 80, 90])
|
||||
|
||||
def test_scatter_empty_batch_no_op(self) -> None:
|
||||
"""Empty input (num_tokens == 0) returns without touching the pool."""
|
||||
flat = torch.empty(0, dtype=torch.int64, device=_DEVICE)
|
||||
offsets = torch.zeros(1, dtype=torch.int64, device=_DEVICE)
|
||||
req_pool_indices = torch.empty(0, dtype=torch.int64, device=_DEVICE)
|
||||
pool = _build_pool(max_reqs=8, max_context_len=16)
|
||||
pool_before = pool.clone()
|
||||
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=pool,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
self.assertTrue(torch.equal(pool, pool_before))
|
||||
|
||||
def test_scatter_single_req(self) -> None:
|
||||
"""One req with a full row of tokens writes byte-equal to the reference."""
|
||||
seqs = [list(range(10))]
|
||||
lens = [10]
|
||||
rp = [2]
|
||||
|
||||
flat = _build_flat(seqs)
|
||||
offsets = _build_offsets(lens)
|
||||
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
|
||||
triton_pool = _build_pool(max_reqs=4, max_context_len=16)
|
||||
ref_pool = _build_pool(max_reqs=4, max_context_len=16)
|
||||
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=triton_pool,
|
||||
)
|
||||
scatter_req_token_ids_torch_reference(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=ref_pool,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
self.assertTrue(torch.equal(triton_pool, ref_pool))
|
||||
|
||||
def test_scatter_truncates_at_max_context_len(self) -> None:
|
||||
"""Tokens past the pool's max_context_len are silently dropped (no row spill)."""
|
||||
# Two reqs; first req longer than max_context_len. Second req must remain
|
||||
# uncorrupted.
|
||||
seqs = [list(range(20)), [777, 888, 999]]
|
||||
lens = [len(s) for s in seqs]
|
||||
rp = [1, 2]
|
||||
max_context_len = 8
|
||||
|
||||
flat = _build_flat(seqs)
|
||||
offsets = _build_offsets(lens)
|
||||
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
|
||||
pool = _build_pool(max_reqs=4, max_context_len=max_context_len)
|
||||
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=pool,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
self.assertEqual(
|
||||
pool[1, :max_context_len].tolist(), list(range(max_context_len))
|
||||
)
|
||||
self.assertEqual(pool[2, :3].tolist(), [777, 888, 999])
|
||||
|
||||
def test_scatter_mixed_empty_and_nonempty_reqs(self) -> None:
|
||||
"""Middle req has length 0 between two non-empty reqs: pool rows are byte-equal and untouched rows stay zero."""
|
||||
seqs = [[1, 2], [], [3, 4, 5]]
|
||||
lens = [len(s) for s in seqs]
|
||||
rp = [2, 4, 6]
|
||||
|
||||
flat = _build_flat(seqs)
|
||||
offsets = _build_offsets(lens)
|
||||
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
|
||||
triton_pool = _build_pool(max_reqs=8, max_context_len=8)
|
||||
ref_pool = _build_pool(max_reqs=8, max_context_len=8)
|
||||
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=triton_pool,
|
||||
)
|
||||
scatter_req_token_ids_torch_reference(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=ref_pool,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
self.assertTrue(torch.equal(triton_pool, ref_pool))
|
||||
# Middle req contributes nothing; its pool row stays zero.
|
||||
zero_row = torch.zeros(8, dtype=torch.int32, device=_DEVICE)
|
||||
self.assertTrue(torch.equal(triton_pool[4], zero_row))
|
||||
# First and third reqs are written to their respective rows.
|
||||
self.assertEqual(triton_pool[2, :2].tolist(), [1, 2])
|
||||
self.assertEqual(triton_pool[6, :3].tolist(), [3, 4, 5])
|
||||
|
||||
def test_scatter_random_byte_equal(self) -> None:
|
||||
"""Randomized fuzz across bs, seq lengths, and req pool indices."""
|
||||
rng = random.Random(0)
|
||||
max_reqs = 64
|
||||
max_context_len = 32
|
||||
|
||||
for _ in range(8):
|
||||
bs = rng.randint(1, 16)
|
||||
lens = [rng.randint(0, max_context_len) for _ in range(bs)]
|
||||
# All distinct req pool indices in [1, max_reqs)
|
||||
rp = rng.sample(range(1, max_reqs), k=bs)
|
||||
seqs = [[rng.randint(0, 1 << 30) for _ in range(n)] for n in lens]
|
||||
|
||||
flat = _build_flat(seqs)
|
||||
offsets = _build_offsets(lens)
|
||||
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
|
||||
triton_pool = _build_pool(
|
||||
max_reqs=max_reqs, max_context_len=max_context_len
|
||||
)
|
||||
ref_pool = _build_pool(max_reqs=max_reqs, max_context_len=max_context_len)
|
||||
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=triton_pool,
|
||||
)
|
||||
scatter_req_token_ids_torch_reference(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=ref_pool,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
self.assertTrue(torch.equal(triton_pool, ref_pool))
|
||||
|
||||
|
||||
class TestScatterInputValidation(CustomTestCase):
|
||||
"""Cover the strict input checks in launch_scatter_req_token_ids_kernel."""
|
||||
|
||||
def test_raises_on_2d_flat_in(self) -> None:
|
||||
"""A 2-D flat_in tensor triggers a ValueError before any kernel launch."""
|
||||
flat = torch.zeros((2, 2), dtype=torch.int64, device=_DEVICE)
|
||||
offsets = torch.tensor([0, 1, 2], dtype=torch.int64, device=_DEVICE)
|
||||
req_pool_indices = torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE)
|
||||
pool = _build_pool(max_reqs=4, max_context_len=4)
|
||||
with self.assertRaises(ValueError):
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=pool,
|
||||
)
|
||||
|
||||
def test_raises_on_wrong_dtype_pool(self) -> None:
|
||||
"""A pool_out with non-int32 dtype triggers a TypeError."""
|
||||
flat = torch.tensor([10, 20], dtype=torch.int64, device=_DEVICE)
|
||||
offsets = torch.tensor([0, 2], dtype=torch.int64, device=_DEVICE)
|
||||
req_pool_indices = torch.tensor([1], dtype=torch.int64, device=_DEVICE)
|
||||
pool = torch.zeros((4, 4), dtype=torch.int64, device=_DEVICE)
|
||||
with self.assertRaises(TypeError):
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=pool,
|
||||
)
|
||||
|
||||
def test_raises_on_offsets_len_mismatch(self) -> None:
|
||||
"""offsets.shape[0] must equal bs + 1; mismatch triggers a ValueError."""
|
||||
flat = torch.tensor([10, 20], dtype=torch.int64, device=_DEVICE)
|
||||
# bs = 2 but offsets has length 2 instead of 3.
|
||||
offsets = torch.tensor([0, 2], dtype=torch.int64, device=_DEVICE)
|
||||
req_pool_indices = torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE)
|
||||
pool = _build_pool(max_reqs=4, max_context_len=4)
|
||||
with self.assertRaises(ValueError):
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=pool,
|
||||
)
|
||||
|
||||
def test_raises_on_bs_plus_one_exceeds_batch_block(self) -> None:
|
||||
"""bs+1 must fit in _SCATTER_BATCH_BLOCK; exceeding it triggers a ValueError."""
|
||||
bs = _SCATTER_BATCH_BLOCK
|
||||
flat = torch.empty(0, dtype=torch.int64, device=_DEVICE)
|
||||
offsets = torch.zeros(bs + 1, dtype=torch.int64, device=_DEVICE)
|
||||
req_pool_indices = torch.zeros(bs, dtype=torch.int64, device=_DEVICE)
|
||||
pool = _build_pool(max_reqs=4, max_context_len=4)
|
||||
with self.assertRaises(ValueError):
|
||||
launch_scatter_req_token_ids_kernel(
|
||||
flat_in=flat,
|
||||
offsets=offsets,
|
||||
req_pool_indices=req_pool_indices,
|
||||
pool_out=pool,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,64 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sglang.jit_kernel.benchmark.kv_canary.utils import (
|
||||
MAX_EXTEND_TOKENS_PER_FORWARD,
|
||||
build_fast_matrix_cases,
|
||||
build_full_matrix_cases,
|
||||
cases_to_x_vals,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
|
||||
register_cpu_ci(est_time=3, suite="base-a-test-cpu")
|
||||
|
||||
|
||||
def test_fast_matrix_cases_include_e2e_decode_and_chunked_prefill_scenarios() -> None:
|
||||
cases = build_fast_matrix_cases()
|
||||
scenarios = {case.scenario for case in cases}
|
||||
|
||||
assert {
|
||||
"e2e_decode_steady",
|
||||
"e2e_decode_tail",
|
||||
"e2e_prefill_chunk_first",
|
||||
"e2e_prefill_chunk_second",
|
||||
"e2e_prefill_chunk_mid",
|
||||
"e2e_prefill_chunk_last",
|
||||
} <= scenarios
|
||||
|
||||
|
||||
def test_extend_cases_are_bounded_to_scheduler_chunk_size() -> None:
|
||||
cases = build_full_matrix_cases()
|
||||
bad_cases = [
|
||||
case
|
||||
for case in cases
|
||||
if case.mode == "extend"
|
||||
and case.bs * case.extend_len > MAX_EXTEND_TOKENS_PER_FORWARD
|
||||
]
|
||||
|
||||
assert bad_cases == []
|
||||
|
||||
|
||||
def test_cases_to_x_vals_includes_scenario_axis() -> None:
|
||||
case = build_fast_matrix_cases()[0]
|
||||
|
||||
x_vals = cases_to_x_vals([case])
|
||||
|
||||
assert x_vals == [
|
||||
(
|
||||
case.scenario,
|
||||
case.bs,
|
||||
case.prefix_len,
|
||||
case.mode,
|
||||
case.extend_len,
|
||||
case.pool_kind,
|
||||
case.real_kv_kind,
|
||||
case.hash_mode,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -1,214 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.kv_canary import consts
|
||||
from sglang.jit_kernel.kv_canary.verify import (
|
||||
CanaryLaunchTag,
|
||||
RealKvSource,
|
||||
VerifyPlan,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
|
||||
FakeViolationLog,
|
||||
make_canary_buf,
|
||||
make_log_pair,
|
||||
make_verify_plan_pair,
|
||||
stamp_clean_chain,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._differential import _run_both_verify
|
||||
from sglang.jit_kernel.tests.kv_canary._fixtures import (
|
||||
clone_real_kv_sources,
|
||||
make_real_kv_sources,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._fuzz_driver import (
|
||||
FUZZ_SEEDS_PR,
|
||||
run_fuzz_combo,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._invariants import VerifyInvariants
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
|
||||
_DEVICE = torch.device("cuda")
|
||||
|
||||
_FUZZ_ITER_PER_SEED = 30
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, kw_only=True)
|
||||
class VerifyFuzzInputs:
|
||||
cuda_canary_buf: torch.Tensor
|
||||
ref_canary_buf: torch.Tensor
|
||||
plan_cuda: VerifyPlan
|
||||
plan_ref: VerifyPlan
|
||||
kernel_kind: CanaryLaunchTag
|
||||
real_kv_sources_cuda: tuple[RealKvSource, ...]
|
||||
real_kv_sources_ref: tuple[RealKvSource, ...]
|
||||
real_kv_hash_mode: consts.RealKvHashMode
|
||||
ring_capacity: int
|
||||
check_verify_expected_token: bool
|
||||
|
||||
|
||||
def _draw_random_verify_inputs(rng: random.Random) -> VerifyFuzzInputs:
|
||||
hash_mode = rng.choice(
|
||||
[
|
||||
consts.RealKvHashMode.NONE,
|
||||
consts.RealKvHashMode.PARTIAL,
|
||||
consts.RealKvHashMode.ALL,
|
||||
]
|
||||
)
|
||||
src_count = rng.choice([1, 2, 4])
|
||||
page_size = rng.choice([1, 16])
|
||||
bytes_per = rng.choice([16, 64, 128])
|
||||
kernel_kind = rng.choice(list(CanaryLaunchTag))
|
||||
plan_size = rng.randint(0, 32)
|
||||
num_slots = max(plan_size + 8, 16)
|
||||
ring_capacity = rng.choice([16, 64, 256])
|
||||
|
||||
sources_cuda = make_real_kv_sources(
|
||||
count=src_count,
|
||||
num_bytes_per_token=bytes_per,
|
||||
page_size=page_size,
|
||||
num_slots=num_slots,
|
||||
device=_DEVICE,
|
||||
rng=rng,
|
||||
)
|
||||
sources_ref = clone_real_kv_sources(sources_cuda)
|
||||
|
||||
cuda_buf = make_canary_buf(
|
||||
num_slots=num_slots, slot_stride_bytes=32, device=_DEVICE
|
||||
)
|
||||
ref_buf = cuda_buf.clone()
|
||||
|
||||
slot_universe = list(range(1, num_slots))
|
||||
rng.shuffle(slot_universe)
|
||||
slot_indices = slot_universe[:plan_size]
|
||||
tokens = [rng.randint(0, 0xFFFFFFFF) for _ in range(plan_size)]
|
||||
positions = list(range(plan_size))
|
||||
prev_slot_indices: list[int] = []
|
||||
for i in range(plan_size):
|
||||
if i == 0:
|
||||
prev_slot_indices.append(-1)
|
||||
else:
|
||||
prev_slot_indices.append(slot_indices[i - 1])
|
||||
|
||||
if hash_mode == consts.RealKvHashMode.NONE and plan_size > 0:
|
||||
stamp_clean_chain(
|
||||
cuda_buf=cuda_buf,
|
||||
ref_buf=ref_buf,
|
||||
slot_indices=slot_indices,
|
||||
tokens=tokens,
|
||||
positions=positions,
|
||||
)
|
||||
|
||||
# Inject prev_slot == TOKEN_TO_KV_SLOT_PADDING into ~15% of entries so the differential
|
||||
# harness exercises the chain-check-skip branch (added for SWA-evicted ancestor handling).
|
||||
# Done AFTER stamp_clean_chain so the stored prev_hash on those slots is still chain-clean;
|
||||
# the kernel must rely on prev_slot==padding (not on stored hash) to decide whether to skip.
|
||||
for i in range(plan_size):
|
||||
if rng.random() < 0.15:
|
||||
prev_slot_indices[i] = consts.TOKEN_TO_KV_SLOT_PADDING
|
||||
|
||||
check_verify_expected_token = rng.random() < 0.5
|
||||
expected_input_ids: list[int] = []
|
||||
for i in range(plan_size):
|
||||
# Always pick a value; with check=False the kernel must not deref this column.
|
||||
if rng.random() < 0.3:
|
||||
expected_input_ids.append(-1)
|
||||
elif rng.random() < 0.5:
|
||||
expected_input_ids.append(int(tokens[i]))
|
||||
else:
|
||||
mutated = (int(tokens[i]) ^ 0x1) & 0xFFFFFFFF
|
||||
expected_input_ids.append(mutated)
|
||||
|
||||
plan_cuda, plan_ref = make_verify_plan_pair(
|
||||
slot_indices=slot_indices,
|
||||
positions=positions,
|
||||
prev_slot_indices=prev_slot_indices,
|
||||
expected_input_ids=expected_input_ids if plan_size > 0 else None,
|
||||
capacity=max(plan_size, 1),
|
||||
device=_DEVICE,
|
||||
)
|
||||
|
||||
return VerifyFuzzInputs(
|
||||
cuda_canary_buf=cuda_buf,
|
||||
ref_canary_buf=ref_buf,
|
||||
plan_cuda=plan_cuda,
|
||||
plan_ref=plan_ref,
|
||||
kernel_kind=kernel_kind,
|
||||
real_kv_sources_cuda=sources_cuda,
|
||||
real_kv_sources_ref=sources_ref,
|
||||
real_kv_hash_mode=hash_mode,
|
||||
ring_capacity=ring_capacity,
|
||||
check_verify_expected_token=check_verify_expected_token,
|
||||
)
|
||||
|
||||
|
||||
def _run_one(inputs: VerifyFuzzInputs) -> None:
|
||||
cuda_buf_before = inputs.cuda_canary_buf.clone()
|
||||
cuda_log, ref_log = make_log_pair(capacity=inputs.ring_capacity, device=_DEVICE)
|
||||
log_before = FakeViolationLog.allocate(
|
||||
capacity=inputs.ring_capacity, device=_DEVICE
|
||||
)
|
||||
_run_both_verify(
|
||||
cuda_canary_buf=inputs.cuda_canary_buf,
|
||||
ref_canary_buf=inputs.ref_canary_buf,
|
||||
plan_cuda=inputs.plan_cuda,
|
||||
plan_ref=inputs.plan_ref,
|
||||
cuda_log=cuda_log,
|
||||
ref_log=ref_log,
|
||||
real_kv_sources_cuda=inputs.real_kv_sources_cuda,
|
||||
real_kv_sources_ref=inputs.real_kv_sources_ref,
|
||||
real_kv_hash_mode=inputs.real_kv_hash_mode,
|
||||
kernel_kind=inputs.kernel_kind,
|
||||
assert_equal=False,
|
||||
check_verify_expected_token=inputs.check_verify_expected_token,
|
||||
)
|
||||
assert int(cuda_log.kernel_run_counter[0].item()) == int(
|
||||
ref_log.kernel_run_counter[0].item()
|
||||
)
|
||||
assert int(cuda_log.slot_run_counter[0].item()) == int(
|
||||
ref_log.slot_run_counter[0].item()
|
||||
)
|
||||
assert int(cuda_log.write_index[0].item()) == int(ref_log.write_index[0].item())
|
||||
VerifyInvariants.assert_all(
|
||||
canary_buf_before=cuda_buf_before,
|
||||
canary_buf_after=inputs.cuda_canary_buf,
|
||||
log_before=log_before,
|
||||
log_after=cuda_log,
|
||||
plan=inputs.plan_cuda,
|
||||
kernel_kind=inputs.kernel_kind,
|
||||
)
|
||||
|
||||
|
||||
def _summarize(inputs: VerifyFuzzInputs) -> str:
|
||||
n_active = int(inputs.plan_cuda.verify_num_valid[0].item())
|
||||
return (
|
||||
f"plan_size={n_active} kind={inputs.kernel_kind.name} "
|
||||
f"hash_mode={inputs.real_kv_hash_mode.name} "
|
||||
f"sources={len(inputs.real_kv_sources_cuda)} "
|
||||
f"ring={inputs.ring_capacity} "
|
||||
f"check_token={inputs.check_verify_expected_token}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", FUZZ_SEEDS_PR)
|
||||
def test_verify_fuzz_full_combo(seed: int) -> None:
|
||||
"""Multi-dim verify fuzzer: random hash mode × kernel kind × page × bytes × N iters, byte-equal."""
|
||||
run_fuzz_combo(
|
||||
seed,
|
||||
draw_fn=_draw_random_verify_inputs,
|
||||
run_one_fn=_run_one,
|
||||
summarize_fn=_summarize,
|
||||
n_iter=_FUZZ_ITER_PER_SEED,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,256 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.kv_canary import consts
|
||||
from sglang.jit_kernel.kv_canary.verify import (
|
||||
CanaryLaunchTag,
|
||||
RealKvSource,
|
||||
)
|
||||
from sglang.jit_kernel.kv_canary.write import WritePlan
|
||||
from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
|
||||
FakeViolationLog,
|
||||
make_canary_buf,
|
||||
make_log_pair,
|
||||
make_write_plan_pair,
|
||||
stamp_pair,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._differential import _run_both_write
|
||||
from sglang.jit_kernel.tests.kv_canary._fixtures import (
|
||||
clone_real_kv_sources,
|
||||
make_real_kv_sources,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._fuzz_driver import (
|
||||
FUZZ_SEEDS_PR,
|
||||
run_fuzz_combo,
|
||||
)
|
||||
from sglang.jit_kernel.tests.kv_canary._invariants import WriteInvariants
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
|
||||
_DEVICE = torch.device("cuda")
|
||||
|
||||
_FUZZ_ITER_PER_SEED = 30
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, kw_only=True)
|
||||
class WriteFuzzInputs:
|
||||
cuda_canary_buf: torch.Tensor
|
||||
ref_canary_buf: torch.Tensor
|
||||
plan_cuda: WritePlan
|
||||
plan_ref: WritePlan
|
||||
input_ids: torch.Tensor
|
||||
positions: torch.Tensor
|
||||
out_cache_loc: torch.Tensor
|
||||
kernel_kind: CanaryLaunchTag
|
||||
enable_write_verify_inputs: bool
|
||||
expected_input_tokens: torch.Tensor
|
||||
expected_input_positions: torch.Tensor
|
||||
real_kv_sources_cuda: tuple[RealKvSource, ...]
|
||||
real_kv_sources_ref: tuple[RealKvSource, ...]
|
||||
real_kv_hash_mode: consts.RealKvHashMode
|
||||
ring_capacity: int
|
||||
|
||||
|
||||
def _draw_random_write_inputs(rng: random.Random) -> WriteFuzzInputs:
|
||||
enable_write_verify_inputs = rng.choice([False, True])
|
||||
hash_mode = rng.choice(
|
||||
[
|
||||
consts.RealKvHashMode.NONE,
|
||||
consts.RealKvHashMode.PARTIAL,
|
||||
consts.RealKvHashMode.ALL,
|
||||
]
|
||||
)
|
||||
src_count = rng.choice([1, 2, 4])
|
||||
page_size = rng.choice([1, 16])
|
||||
bytes_per = rng.choice([16, 64, 128])
|
||||
kernel_kind = rng.choice(list(CanaryLaunchTag))
|
||||
ring_capacity = rng.choice([16, 64, 256])
|
||||
|
||||
n_reqs = rng.randint(1, 4)
|
||||
per_req_tokens: list[int] = [rng.randint(1, 5) for _ in range(n_reqs)]
|
||||
total_tokens = sum(per_req_tokens)
|
||||
num_slots = max(total_tokens + 8, 16)
|
||||
|
||||
sources_cuda = make_real_kv_sources(
|
||||
count=src_count,
|
||||
num_bytes_per_token=bytes_per,
|
||||
page_size=page_size,
|
||||
num_slots=num_slots,
|
||||
device=_DEVICE,
|
||||
rng=rng,
|
||||
)
|
||||
sources_ref = clone_real_kv_sources(sources_cuda)
|
||||
|
||||
cuda_buf = make_canary_buf(
|
||||
num_slots=num_slots, slot_stride_bytes=32, device=_DEVICE
|
||||
)
|
||||
ref_buf = cuda_buf.clone()
|
||||
|
||||
write_offsets: list[int] = [0]
|
||||
running = 0
|
||||
for t in per_req_tokens:
|
||||
running += t
|
||||
write_offsets.append(running)
|
||||
slot_pool = list(range(1, num_slots))
|
||||
rng.shuffle(slot_pool)
|
||||
seed_slot_indices: list[int] = []
|
||||
for _ in range(n_reqs):
|
||||
if rng.random() < 0.4 or len(slot_pool) <= total_tokens:
|
||||
seed_slot_indices.append(-1)
|
||||
else:
|
||||
seed_slot_indices.append(slot_pool.pop())
|
||||
|
||||
out_cache_loc_list: list[int] = []
|
||||
for _ in range(total_tokens):
|
||||
if not slot_pool:
|
||||
out_cache_loc_list.append(-1)
|
||||
else:
|
||||
out_cache_loc_list.append(slot_pool.pop())
|
||||
|
||||
plan_cuda, plan_ref = make_write_plan_pair(
|
||||
write_offsets=write_offsets,
|
||||
seed_slot_indices=seed_slot_indices,
|
||||
num_valid_reqs=n_reqs,
|
||||
device=_DEVICE,
|
||||
)
|
||||
|
||||
input_ids = torch.tensor(
|
||||
[rng.randint(-(1 << 31), (1 << 31) - 1) for _ in range(total_tokens)],
|
||||
dtype=torch.int64,
|
||||
device=_DEVICE,
|
||||
)
|
||||
# Per-chain sequential positions so the write kernel's chain-step position assert holds.
|
||||
# For chains with a real seed slot, stamp the seed with (chain_start_position - 1) so the
|
||||
# first chain entry's position == seed.position + 1.
|
||||
chain_start_positions: list[int] = [rng.randint(0, 1024) for _ in range(n_reqs)]
|
||||
positions_list: list[int] = []
|
||||
for r in range(n_reqs):
|
||||
start = chain_start_positions[r]
|
||||
positions_list.extend(start + i for i in range(per_req_tokens[r]))
|
||||
seed_slot = seed_slot_indices[r]
|
||||
if seed_slot >= 0:
|
||||
stamp_pair(
|
||||
(cuda_buf, ref_buf),
|
||||
slot_idx=seed_slot,
|
||||
token=0,
|
||||
position=start - 1,
|
||||
prev_hash=0,
|
||||
)
|
||||
positions = torch.tensor(positions_list, dtype=torch.int64, device=_DEVICE)
|
||||
out_cache_loc = torch.tensor(out_cache_loc_list, dtype=torch.int64, device=_DEVICE)
|
||||
expected_input_tokens = input_ids.clone()
|
||||
expected_input_positions = positions.clone()
|
||||
if enable_write_verify_inputs:
|
||||
candidate_indices = [
|
||||
idx for idx, slot in enumerate(out_cache_loc_list) if slot >= 0
|
||||
]
|
||||
rng.shuffle(candidate_indices)
|
||||
mismatch_count = rng.randint(0, len(candidate_indices))
|
||||
for idx in candidate_indices[:mismatch_count]:
|
||||
if rng.choice([False, True]):
|
||||
expected_input_tokens[idx] = expected_input_tokens[idx] + 1
|
||||
else:
|
||||
expected_input_positions[idx] = expected_input_positions[idx] + 1
|
||||
|
||||
return WriteFuzzInputs(
|
||||
cuda_canary_buf=cuda_buf,
|
||||
ref_canary_buf=ref_buf,
|
||||
plan_cuda=plan_cuda,
|
||||
plan_ref=plan_ref,
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
kernel_kind=kernel_kind,
|
||||
enable_write_verify_inputs=enable_write_verify_inputs,
|
||||
expected_input_tokens=expected_input_tokens,
|
||||
expected_input_positions=expected_input_positions,
|
||||
real_kv_sources_cuda=sources_cuda,
|
||||
real_kv_sources_ref=sources_ref,
|
||||
real_kv_hash_mode=hash_mode,
|
||||
ring_capacity=ring_capacity,
|
||||
)
|
||||
|
||||
|
||||
def _run_one(inputs: WriteFuzzInputs) -> None:
|
||||
cuda_buf_before = inputs.cuda_canary_buf.clone()
|
||||
cuda_log, ref_log = make_log_pair(capacity=inputs.ring_capacity, device=_DEVICE)
|
||||
log_before = FakeViolationLog.allocate(
|
||||
capacity=inputs.ring_capacity, device=_DEVICE
|
||||
)
|
||||
_run_both_write(
|
||||
cuda_canary_buf=inputs.cuda_canary_buf,
|
||||
ref_canary_buf=inputs.ref_canary_buf,
|
||||
plan_cuda=inputs.plan_cuda,
|
||||
plan_ref=inputs.plan_ref,
|
||||
input_ids=inputs.input_ids,
|
||||
positions=inputs.positions,
|
||||
out_cache_loc=inputs.out_cache_loc,
|
||||
enable_write_verify_inputs=inputs.enable_write_verify_inputs,
|
||||
expected_input_tokens=inputs.expected_input_tokens,
|
||||
expected_input_positions=inputs.expected_input_positions,
|
||||
cuda_log=cuda_log,
|
||||
ref_log=ref_log,
|
||||
real_kv_sources_cuda=inputs.real_kv_sources_cuda,
|
||||
real_kv_sources_ref=inputs.real_kv_sources_ref,
|
||||
real_kv_hash_mode=inputs.real_kv_hash_mode,
|
||||
kernel_kind=inputs.kernel_kind,
|
||||
assert_equal=False,
|
||||
)
|
||||
assert torch.equal(
|
||||
inputs.cuda_canary_buf, inputs.ref_canary_buf
|
||||
), "CUDA vs ref canary_buf diverged"
|
||||
assert int(cuda_log.write_index[0].item()) == int(ref_log.write_index[0].item())
|
||||
assert int(cuda_log.slot_run_counter[0].item()) == int(
|
||||
ref_log.slot_run_counter[0].item()
|
||||
)
|
||||
assert int(cuda_log.kernel_run_counter[0].item()) == int(
|
||||
ref_log.kernel_run_counter[0].item()
|
||||
)
|
||||
WriteInvariants.assert_all(
|
||||
canary_buf_before=cuda_buf_before,
|
||||
canary_buf_after=inputs.cuda_canary_buf,
|
||||
plan=inputs.plan_cuda,
|
||||
input_ids=inputs.input_ids,
|
||||
positions=inputs.positions,
|
||||
out_cache_loc=inputs.out_cache_loc,
|
||||
enable_write_verify_inputs=inputs.enable_write_verify_inputs,
|
||||
expected_input_tokens=inputs.expected_input_tokens,
|
||||
expected_input_positions=inputs.expected_input_positions,
|
||||
log_before=log_before,
|
||||
log_after=cuda_log,
|
||||
)
|
||||
|
||||
|
||||
def _summarize(inputs: WriteFuzzInputs) -> str:
|
||||
n_active = int(inputs.plan_cuda.write_num_valid_reqs[0].item())
|
||||
total = int(inputs.plan_cuda.write_offsets[n_active].item())
|
||||
return (
|
||||
f"n_reqs={n_active} total_tokens={total} kind={inputs.kernel_kind.name} "
|
||||
f"pseudo={inputs.enable_write_verify_inputs} hash_mode={inputs.real_kv_hash_mode.name} "
|
||||
f"sources={len(inputs.real_kv_sources_cuda)}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", FUZZ_SEEDS_PR)
|
||||
def test_write_fuzz_full_combo(seed: int) -> None:
|
||||
"""Multi-dim write fuzzer: random pseudo/hash/kernel/page/source × N iters, byte-equal."""
|
||||
run_fuzz_combo(
|
||||
seed,
|
||||
draw_fn=_draw_random_write_inputs,
|
||||
run_one_fn=_run_one,
|
||||
summarize_fn=_summarize,
|
||||
n_iter=_FUZZ_ITER_PER_SEED,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,205 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.jit_kernel.activation import (
|
||||
SUPPORTED_ACTIVATIONS,
|
||||
relu2,
|
||||
run_activation,
|
||||
)
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=20, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=30, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
OPS = SUPPORTED_ACTIVATIONS
|
||||
DTYPES = [torch.float16, torch.bfloat16, torch.float32]
|
||||
SHAPES = get_ci_test_range(
|
||||
full_range=[
|
||||
(7, 16),
|
||||
(83, 1024),
|
||||
(3, 5, 16),
|
||||
(2, 3, 512),
|
||||
(1, 17, 4096),
|
||||
*[(2**x, 2048) for x in range(0, 15, 2)],
|
||||
*[(2**x, 65536) for x in range(0, 5, 2)],
|
||||
],
|
||||
ci_range=[(7, 16), (2, 3, 512)],
|
||||
)
|
||||
|
||||
|
||||
def _reference(op_name: str, x: torch.Tensor) -> torch.Tensor:
|
||||
d = x.shape[-1] // 2
|
||||
lhs = x[..., :d].float()
|
||||
rhs = x[..., d:]
|
||||
if op_name == "silu":
|
||||
act = F.silu(lhs)
|
||||
elif op_name == "gelu":
|
||||
act = F.gelu(lhs, approximate="none")
|
||||
else:
|
||||
act = F.gelu(lhs, approximate="tanh")
|
||||
return act.to(dtype=x.dtype) * rhs
|
||||
|
||||
|
||||
def _tolerances(dtype: torch.dtype) -> tuple[float, float]:
|
||||
if dtype == torch.float32:
|
||||
return 1e-4, 1e-4
|
||||
return 1e-2, 1e-2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op_name", OPS)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("shape", SHAPES)
|
||||
def test_activation_correctness(
|
||||
op_name: str, dtype: torch.dtype, shape: tuple[int, ...]
|
||||
) -> None:
|
||||
x = torch.randn(shape, dtype=dtype, device="cuda")
|
||||
out = run_activation(op_name, x, None)
|
||||
expected = _reference(op_name, x)
|
||||
atol, rtol = _tolerances(dtype)
|
||||
torch.testing.assert_close(out, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op_name", OPS)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("shape", SHAPES)
|
||||
def test_activation_out_param(
|
||||
op_name: str, dtype: torch.dtype, shape: tuple[int, ...]
|
||||
) -> None:
|
||||
x = torch.randn(shape, dtype=dtype, device="cuda")
|
||||
out = torch.empty(shape[:-1] + (shape[-1] // 2,), dtype=dtype, device="cuda")
|
||||
result = run_activation(op_name, x, out)
|
||||
assert result is out
|
||||
expected = _reference(op_name, x)
|
||||
atol, rtol = _tolerances(dtype)
|
||||
torch.testing.assert_close(out, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
FILTER_SHAPES = get_ci_test_range(
|
||||
full_range=[(83, 1024), (256, 2048), (1024, 4096)],
|
||||
ci_range=[(83, 1024)],
|
||||
)
|
||||
EXPERT_STEPS = [1, 16]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op_name", OPS)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("shape", FILTER_SHAPES)
|
||||
@pytest.mark.parametrize("expert_step", EXPERT_STEPS)
|
||||
def test_activation_filter_expert(
|
||||
op_name: str,
|
||||
dtype: torch.dtype,
|
||||
shape: tuple[int, int],
|
||||
expert_step: int,
|
||||
) -> None:
|
||||
"""expert_ids[token // expert_step] == -1 must leave the output row untouched."""
|
||||
num_tokens = shape[0]
|
||||
x = torch.randn(shape, dtype=dtype, device="cuda")
|
||||
# Pre-fill out with a sentinel so we can detect untouched rows.
|
||||
sentinel = float("nan")
|
||||
out = torch.full(
|
||||
shape[:-1] + (shape[-1] // 2,),
|
||||
sentinel,
|
||||
dtype=dtype,
|
||||
device="cuda",
|
||||
)
|
||||
|
||||
num_groups = (num_tokens + expert_step - 1) // expert_step
|
||||
expert_ids = torch.randint(
|
||||
low=0, high=8, size=(num_groups,), dtype=torch.int32, device="cuda"
|
||||
)
|
||||
skip_mask = torch.rand(num_groups, device="cuda") < 0.4
|
||||
expert_ids[skip_mask] = -1
|
||||
|
||||
result = run_activation(op_name, x, out, expert_ids, expert_step)
|
||||
assert result is out
|
||||
|
||||
token_skip = skip_mask[torch.arange(num_tokens, device="cuda") // expert_step]
|
||||
expected = _reference(op_name, x)
|
||||
atol, rtol = _tolerances(dtype)
|
||||
|
||||
kept = ~token_skip
|
||||
if kept.any():
|
||||
torch.testing.assert_close(out[kept], expected[kept], atol=atol, rtol=rtol)
|
||||
if token_skip.any():
|
||||
assert torch.isnan(
|
||||
out[token_skip]
|
||||
).all(), "filter_expert kernel touched rows whose expert_id is -1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op_name", OPS)
|
||||
def test_activation_filter_expert_all_skipped(op_name: str) -> None:
|
||||
"""If every expert id is -1, the output must be left entirely untouched."""
|
||||
shape = (32, 512)
|
||||
x = torch.randn(shape, dtype=torch.bfloat16, device="cuda")
|
||||
out = torch.full(
|
||||
shape[:-1] + (shape[-1] // 2,),
|
||||
float("nan"),
|
||||
dtype=torch.bfloat16,
|
||||
device="cuda",
|
||||
)
|
||||
expert_ids = torch.full((shape[0],), -1, dtype=torch.int32, device="cuda")
|
||||
run_activation(op_name, x, out, expert_ids, 1)
|
||||
assert torch.isnan(out).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op_name", OPS)
|
||||
def test_activation_filter_expert_none_skipped(op_name: str) -> None:
|
||||
"""No -1 in expert_ids must yield bit-identical output to the unfiltered path."""
|
||||
shape = (64, 512)
|
||||
dtype = torch.bfloat16
|
||||
x = torch.randn(shape, dtype=dtype, device="cuda")
|
||||
expert_ids = torch.zeros((shape[0],), dtype=torch.int32, device="cuda")
|
||||
out_filtered = run_activation(op_name, x, None, expert_ids, 1)
|
||||
out_unfiltered = run_activation(op_name, x, None)
|
||||
torch.testing.assert_close(out_filtered, out_unfiltered, atol=0.0, rtol=0.0)
|
||||
|
||||
|
||||
UNARY_SHAPES = get_ci_test_range(
|
||||
full_range=[
|
||||
(7, 16),
|
||||
(83, 1024),
|
||||
(3, 5, 16),
|
||||
(2, 3, 512),
|
||||
(1, 17, 4096),
|
||||
*[(2**x, 2048) for x in range(0, 15, 2)],
|
||||
],
|
||||
ci_range=[(7, 16), (2, 3, 512)],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("shape", UNARY_SHAPES)
|
||||
def test_relu2_correctness(dtype: torch.dtype, shape: tuple[int, ...]) -> None:
|
||||
x = torch.randn(shape, dtype=dtype, device="cuda")
|
||||
out = relu2(x)
|
||||
expected = F.relu(x.float()).pow(2).to(dtype=dtype)
|
||||
atol, rtol = _tolerances(dtype)
|
||||
torch.testing.assert_close(out, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("shape", UNARY_SHAPES)
|
||||
def test_relu2_out_param(dtype: torch.dtype, shape: tuple[int, ...]) -> None:
|
||||
x = torch.randn(shape, dtype=dtype, device="cuda")
|
||||
out = torch.empty(shape, dtype=dtype, device="cuda")
|
||||
result = relu2(x, out)
|
||||
assert result is out
|
||||
expected = F.relu(x.float()).pow(2).to(dtype=dtype)
|
||||
atol, rtol = _tolerances(dtype)
|
||||
torch.testing.assert_close(out, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
def test_relu2_negative_inputs_zeroed() -> None:
|
||||
"""All-negative input must produce an all-zero output."""
|
||||
x = -torch.rand((64, 512), dtype=torch.bfloat16, device="cuda") - 1e-3
|
||||
out = relu2(x)
|
||||
assert torch.count_nonzero(out) == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,41 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.add_constant import add_constant
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=45, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=180, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [1, 2, 127, 128, 1024, 1025, 4096, 4097])
|
||||
@pytest.mark.parametrize("constant", [0, 1, 7, 1024, -3])
|
||||
def test_add_constant(size: int, constant: int) -> None:
|
||||
src = torch.arange(0, size, dtype=torch.int32, device="cuda")
|
||||
dst = add_constant(src, constant)
|
||||
assert torch.all(dst == src + constant)
|
||||
|
||||
|
||||
def test_add_constant_unaligned_input() -> None:
|
||||
src = torch.arange(0, 4098, dtype=torch.int32, device="cuda")[1:]
|
||||
dst = add_constant(src, 7)
|
||||
assert torch.all(dst == src + 7)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [2**20, 2**20 + 3])
|
||||
def test_add_constant_large_aligned_input(size: int) -> None:
|
||||
src = torch.arange(0, size, dtype=torch.int32, device="cuda")
|
||||
dst = add_constant(src, -3)
|
||||
assert torch.all(dst == src - 3)
|
||||
|
||||
|
||||
def test_add_constant_large_unaligned_input() -> None:
|
||||
src = torch.arange(0, 2**20 + 4, dtype=torch.int32, device="cuda")[1:]
|
||||
dst = add_constant(src, 7)
|
||||
assert torch.all(dst == src + 7)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,169 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.awq_dequantize import awq_dequantize as jit_awq_dequantize
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=9, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
try:
|
||||
from sgl_kernel import awq_dequantize as aot_awq_dequantize
|
||||
|
||||
AOT_AVAILABLE = True
|
||||
except ImportError:
|
||||
AOT_AVAILABLE = False
|
||||
|
||||
|
||||
def reverse_awq_order(t: torch.Tensor):
|
||||
bits = 4
|
||||
AWQ_REVERSE_ORDER = [0, 4, 1, 5, 2, 6, 3, 7]
|
||||
reverse_order_tensor = torch.arange(
|
||||
t.shape[-1],
|
||||
dtype=torch.int32,
|
||||
device=t.device,
|
||||
)
|
||||
reverse_order_tensor = reverse_order_tensor.view(-1, 32 // bits)
|
||||
reverse_order_tensor = reverse_order_tensor[:, AWQ_REVERSE_ORDER]
|
||||
reverse_order_tensor = reverse_order_tensor.view(-1)
|
||||
|
||||
t = t[:, reverse_order_tensor] & 0xF
|
||||
return t
|
||||
|
||||
|
||||
# qweights - [R , C // 8], int32
|
||||
# scales - [R // G, C ], float16
|
||||
# zeros - [R // G, C // 8], int32
|
||||
def awq_dequantize_torch(
|
||||
qweight: torch.Tensor, scales: torch.Tensor, qzeros: torch.Tensor, group_size: int
|
||||
) -> torch.Tensor:
|
||||
if group_size == -1:
|
||||
group_size = qweight.shape[0]
|
||||
|
||||
bits = 4
|
||||
shifts = torch.arange(0, 32, bits, device=qzeros.device)
|
||||
|
||||
iweights = torch.bitwise_right_shift(qweight[:, :, None], shifts[None, None, :]).to(
|
||||
torch.int8
|
||||
)
|
||||
|
||||
iweights = iweights.view(iweights.shape[0], -1)
|
||||
|
||||
zeros = torch.bitwise_right_shift(qzeros[:, :, None], shifts[None, None, :]).to(
|
||||
torch.int8
|
||||
)
|
||||
zeros = zeros.view(qzeros.shape[0], -1)
|
||||
zeros = reverse_awq_order(zeros)
|
||||
|
||||
iweights = reverse_awq_order(iweights)
|
||||
|
||||
iweights = torch.bitwise_and(iweights, (2**bits) - 1)
|
||||
zeros = torch.bitwise_and(zeros, (2**bits) - 1)
|
||||
|
||||
scales = scales.repeat_interleave(group_size, dim=0)
|
||||
zeros = zeros.repeat_interleave(group_size, dim=0)
|
||||
return (iweights - zeros) * scales
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"qweight_row,qweight_col,is_bf16_act",
|
||||
list(
|
||||
itertools.product(
|
||||
[128, 256, 512, 1024, 3584],
|
||||
[16, 32, 64, 128, 448],
|
||||
[True, False],
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_awq_dequantize_jit_vs_torch(
|
||||
qweight_row: int, qweight_col: int, is_bf16_act: bool
|
||||
):
|
||||
device = torch.device("cuda")
|
||||
qweight = torch.randint(
|
||||
0,
|
||||
torch.iinfo(torch.int32).max,
|
||||
(qweight_row, qweight_col),
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
group_size = qweight_row
|
||||
scales_row = qweight_row // group_size
|
||||
scales_col = qweight_col * 8
|
||||
|
||||
if is_bf16_act:
|
||||
scales = torch.rand(scales_row, scales_col, dtype=torch.bfloat16, device=device)
|
||||
else:
|
||||
scales = torch.rand(scales_row, scales_col, dtype=torch.float16, device=device)
|
||||
|
||||
qzeros = torch.randint(
|
||||
0,
|
||||
torch.iinfo(torch.int32).max,
|
||||
(scales_row, qweight_col),
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
|
||||
# Run both implementations
|
||||
torch_out = awq_dequantize_torch(qweight, scales, qzeros, group_size)
|
||||
jit_out = jit_awq_dequantize(qweight, scales, qzeros)
|
||||
|
||||
# Compare results (approximate due to different computation paths)
|
||||
torch.testing.assert_close(
|
||||
torch_out.to(torch.float32), jit_out.to(torch.float32), rtol=1e-3, atol=1e-5
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"qweight_row,qweight_col,is_bf16_act",
|
||||
list(
|
||||
itertools.product(
|
||||
[128, 256, 512, 1024, 3584],
|
||||
[16, 32, 64, 128, 448],
|
||||
[True, False],
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_awq_dequantize_jit_vs_aot(
|
||||
qweight_row: int, qweight_col: int, is_bf16_act: bool
|
||||
):
|
||||
if not AOT_AVAILABLE:
|
||||
pytest.skip("sgl_kernel AOT not available")
|
||||
|
||||
device = torch.device("cuda")
|
||||
qweight = torch.randint(
|
||||
0,
|
||||
torch.iinfo(torch.int32).max,
|
||||
(qweight_row, qweight_col),
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
group_size = qweight_row
|
||||
scales_row = qweight_row // group_size
|
||||
scales_col = qweight_col * 8
|
||||
|
||||
if is_bf16_act:
|
||||
scales = torch.rand(scales_row, scales_col, dtype=torch.bfloat16, device=device)
|
||||
else:
|
||||
scales = torch.rand(scales_row, scales_col, dtype=torch.float16, device=device)
|
||||
|
||||
qzeros = torch.randint(
|
||||
0,
|
||||
torch.iinfo(torch.int32).max,
|
||||
(scales_row, qweight_col),
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
|
||||
# Run both implementations
|
||||
aot_out = aot_awq_dequantize(qweight, scales, qzeros)
|
||||
jit_out = jit_awq_dequantize(qweight, scales, qzeros)
|
||||
|
||||
# Bitwise equality
|
||||
torch.testing.assert_close(jit_out, aot_out, rtol=0, atol=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,125 +0,0 @@
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from sgl_kernel.scalar_type import scalar_types
|
||||
|
||||
from sglang.jit_kernel.awq_marlin_repack import (
|
||||
awq_marlin_moe_repack as jit_awq_marlin_moe_repack,
|
||||
)
|
||||
from sglang.srt.layers.quantization.utils import pack_cols, quantize_weights
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=10, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def _has_aot_awq_marlin_moe_repack() -> bool:
|
||||
return hasattr(torch.ops.sgl_kernel, "awq_marlin_moe_repack") and hasattr(
|
||||
torch.ops.sgl_kernel.awq_marlin_moe_repack, "default"
|
||||
)
|
||||
|
||||
|
||||
AOT_AVAILABLE = _has_aot_awq_marlin_moe_repack()
|
||||
|
||||
|
||||
def awq_pack(
|
||||
q_w: torch.Tensor,
|
||||
num_bits: int,
|
||||
size_k: int,
|
||||
size_n: int,
|
||||
):
|
||||
assert q_w.shape == (size_k, size_n)
|
||||
|
||||
if num_bits == 4:
|
||||
interleave = np.array([0, 2, 4, 6, 1, 3, 5, 7])
|
||||
elif num_bits == 8:
|
||||
interleave = np.array([0, 2, 1, 3])
|
||||
else:
|
||||
raise Exception("num_bits must be 4 or 8, got {}".format(num_bits))
|
||||
|
||||
q_w = q_w.reshape((-1, len(interleave)))[:, interleave].ravel()
|
||||
q_w = q_w.reshape((-1, size_n)).contiguous()
|
||||
|
||||
return pack_cols(q_w, num_bits, size_k, size_n)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_bits", [4])
|
||||
@pytest.mark.parametrize("num_experts", [2, 4, 8])
|
||||
@pytest.mark.parametrize("k_tiles,n_tiles", [(1, 1), (2, 2), (4, 4)])
|
||||
@pytest.mark.parametrize("group_size", [16, 32])
|
||||
def test_awq_marlin_moe_repack_jit_vs_aot(
|
||||
num_bits, num_experts, k_tiles, n_tiles, group_size
|
||||
):
|
||||
if not AOT_AVAILABLE:
|
||||
pytest.skip("sgl_kernel AOT not available")
|
||||
|
||||
tile_k, tile_n = 16, 64
|
||||
size_k = k_tiles * tile_k
|
||||
size_n = n_tiles * tile_n
|
||||
pack_factor = 32 // num_bits
|
||||
|
||||
# Create per-expert AWQ-packed weights
|
||||
b_q_weight = torch.empty(
|
||||
(num_experts, size_k, size_n // pack_factor),
|
||||
dtype=torch.int32,
|
||||
device="cuda",
|
||||
)
|
||||
for e in range(num_experts):
|
||||
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
|
||||
w_ref, q_w, s, zp = quantize_weights(
|
||||
b_weight, scalar_types.uint4, group_size, zero_points=True
|
||||
)
|
||||
b_q_weight[e] = awq_pack(q_w, num_bits, size_k, size_n)
|
||||
|
||||
perm = torch.empty((num_experts, 0), dtype=torch.int32, device="cuda")
|
||||
|
||||
out_jit = jit_awq_marlin_moe_repack(b_q_weight, perm, size_k, size_n, num_bits)
|
||||
out_aot = torch.ops.sgl_kernel.awq_marlin_moe_repack.default(
|
||||
b_q_weight, perm, size_k, size_n, num_bits
|
||||
)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# Bitwise equality
|
||||
torch.testing.assert_close(out_jit, out_aot, rtol=0, atol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_bits", [4])
|
||||
@pytest.mark.parametrize("num_experts", [2, 4])
|
||||
@pytest.mark.parametrize("k_tiles,n_tiles", [(1, 1), (2, 2)])
|
||||
@pytest.mark.parametrize("group_size", [16, 32])
|
||||
def test_awq_marlin_moe_repack_shape(
|
||||
num_bits, num_experts, k_tiles, n_tiles, group_size
|
||||
):
|
||||
tile_k, tile_n = 16, 64
|
||||
size_k = k_tiles * tile_k
|
||||
size_n = n_tiles * tile_n
|
||||
pack_factor = 32 // num_bits
|
||||
|
||||
# Create per-expert AWQ-packed weights
|
||||
b_q_weight = torch.empty(
|
||||
(num_experts, size_k, size_n // pack_factor),
|
||||
dtype=torch.int32,
|
||||
device="cuda",
|
||||
)
|
||||
for e in range(num_experts):
|
||||
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
|
||||
w_ref, q_w, s, zp = quantize_weights(
|
||||
b_weight, scalar_types.uint4, group_size, zero_points=True
|
||||
)
|
||||
b_q_weight[e] = awq_pack(q_w, num_bits, size_k, size_n)
|
||||
|
||||
perm = torch.empty((num_experts, 0), dtype=torch.int32, device="cuda")
|
||||
|
||||
out = jit_awq_marlin_moe_repack(b_q_weight, perm, size_k, size_n, num_bits)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
assert out.is_cuda and out.dtype == torch.int32
|
||||
expected_shape = (num_experts, size_k // 16, size_n * (num_bits // 2))
|
||||
assert list(out.shape) == list(expected_shape)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,111 +0,0 @@
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from sgl_kernel.scalar_type import scalar_types
|
||||
|
||||
from sglang.jit_kernel.awq_marlin_repack import (
|
||||
awq_marlin_repack as jit_awq_marlin_repack,
|
||||
)
|
||||
from sglang.srt.layers.quantization.utils import pack_cols, quantize_weights
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_marlin_utils import get_weight_perm, marlin_weights
|
||||
|
||||
register_cuda_ci(est_time=10, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def _has_aot_awq_marlin_repack() -> bool:
|
||||
return hasattr(torch.ops.sgl_kernel, "awq_marlin_repack") and hasattr(
|
||||
torch.ops.sgl_kernel.awq_marlin_repack, "default"
|
||||
)
|
||||
|
||||
|
||||
AOT_AVAILABLE = _has_aot_awq_marlin_repack()
|
||||
|
||||
|
||||
def awq_pack(
|
||||
q_w: torch.Tensor,
|
||||
num_bits: int,
|
||||
size_k: int,
|
||||
size_n: int,
|
||||
):
|
||||
assert q_w.shape == (size_k, size_n)
|
||||
|
||||
if num_bits == 4:
|
||||
interleave = np.array([0, 2, 4, 6, 1, 3, 5, 7])
|
||||
elif num_bits == 8:
|
||||
interleave = np.array([0, 2, 1, 3])
|
||||
else:
|
||||
raise Exception("num_bits must be 4 or 8, got {}".format(num_bits))
|
||||
|
||||
q_w = q_w.reshape((-1, len(interleave)))[:, interleave].ravel()
|
||||
q_w = q_w.reshape((-1, size_n)).contiguous()
|
||||
|
||||
return pack_cols(q_w, num_bits, size_k, size_n)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_bits", [4, 8])
|
||||
@pytest.mark.parametrize("k_tiles,n_tiles", [(1, 1), (2, 2), (4, 4)])
|
||||
@pytest.mark.parametrize("group_size", [16, 32])
|
||||
def test_awq_marlin_repack_jit_vs_aot(num_bits, k_tiles, n_tiles, group_size):
|
||||
if not AOT_AVAILABLE:
|
||||
pytest.skip("sgl_kernel AOT not available")
|
||||
|
||||
tile_k, tile_n = 16, 64
|
||||
size_k = k_tiles * tile_k
|
||||
size_n = n_tiles * tile_n
|
||||
|
||||
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
|
||||
|
||||
w_ref, q_w, s, zp = quantize_weights(
|
||||
b_weight, scalar_types.uint4, group_size, zero_points=True
|
||||
)
|
||||
|
||||
q_w_awq = awq_pack(q_w, num_bits, size_k, size_n)
|
||||
|
||||
out_jit = jit_awq_marlin_repack(q_w_awq, size_k, size_n, num_bits)
|
||||
out_aot = torch.ops.sgl_kernel.awq_marlin_repack.default(
|
||||
q_w_awq, size_k, size_n, num_bits
|
||||
)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# Bitwise equality
|
||||
torch.testing.assert_close(out_jit, out_aot, rtol=0, atol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_bits", [4, 8])
|
||||
@pytest.mark.parametrize("k_tiles,n_tiles", [(1, 1), (2, 2)])
|
||||
@pytest.mark.parametrize("group_size", [16, 32])
|
||||
def test_awq_marlin_repack_correct(num_bits, k_tiles, n_tiles, group_size):
|
||||
tile_k, tile_n = 16, 64
|
||||
size_k = k_tiles * tile_k
|
||||
size_n = n_tiles * tile_n
|
||||
pack_factor = 32 // num_bits
|
||||
|
||||
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
|
||||
|
||||
w_ref, q_w, s, zp = quantize_weights(
|
||||
b_weight, scalar_types.uint4, group_size, zero_points=True
|
||||
)
|
||||
|
||||
q_w_awq = awq_pack(q_w, num_bits, size_k, size_n)
|
||||
|
||||
weight_perm = get_weight_perm(num_bits)
|
||||
q_w_marlin = marlin_weights(q_w, size_k, size_n, num_bits, weight_perm)
|
||||
|
||||
out_gpu = jit_awq_marlin_repack(q_w_awq, size_k, size_n, num_bits)
|
||||
assert out_gpu.is_cuda and out_gpu.dtype == torch.int32
|
||||
|
||||
expected_cols = size_n * tile_k // pack_factor
|
||||
assert list(out_gpu.shape) == [size_k // tile_k, expected_cols]
|
||||
|
||||
torch.cuda.synchronize()
|
||||
|
||||
torch.testing.assert_close(out_gpu, q_w_marlin)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,46 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.clamp_position import clamp_position_cuda
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=12, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def _reference_clamp_position(seq_lens):
|
||||
return torch.clamp(seq_lens - 1, min=0).to(seq_lens.dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [1, 2, 127, 128, 255, 256, 1024, 4097])
|
||||
@pytest.mark.parametrize("dtype", [torch.int32, torch.int64])
|
||||
class TestClampPosition:
|
||||
def test_normal(self, size: int, dtype: torch.dtype) -> None:
|
||||
seq_lens = torch.randint(1, 10000, (size,), dtype=dtype, device="cuda")
|
||||
expected = _reference_clamp_position(seq_lens)
|
||||
result = clamp_position_cuda(seq_lens)
|
||||
assert torch.equal(result, expected)
|
||||
|
||||
def test_zeros(self, size: int, dtype: torch.dtype) -> None:
|
||||
seq_lens = torch.zeros(size, dtype=dtype, device="cuda")
|
||||
expected = _reference_clamp_position(seq_lens)
|
||||
result = clamp_position_cuda(seq_lens)
|
||||
assert torch.equal(result, expected)
|
||||
|
||||
def test_ones(self, size: int, dtype: torch.dtype) -> None:
|
||||
seq_lens = torch.ones(size, dtype=dtype, device="cuda")
|
||||
expected = _reference_clamp_position(seq_lens)
|
||||
result = clamp_position_cuda(seq_lens)
|
||||
assert torch.equal(result, expected)
|
||||
|
||||
def test_mixed(self, size: int, dtype: torch.dtype) -> None:
|
||||
seq_lens = torch.randint(0, 10000, (size,), dtype=dtype, device="cuda")
|
||||
expected = _reference_clamp_position(seq_lens)
|
||||
result = clamp_position_cuda(seq_lens)
|
||||
assert torch.equal(result, expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,175 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=17, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def torch_concat_mla_k(
|
||||
k: torch.Tensor, k_nope: torch.Tensor, k_rope: torch.Tensor
|
||||
) -> None:
|
||||
"""Reference PyTorch implementation for concat_mla_k."""
|
||||
# k_nope: [num_tokens, num_heads, nope_head_dim]
|
||||
# k_rope: [num_tokens, 1, rope_head_dim]
|
||||
# k: [num_tokens, num_heads, nope_head_dim + rope_head_dim]
|
||||
nope_head_dim = k_nope.shape[-1]
|
||||
k[:, :, :nope_head_dim] = k_nope
|
||||
# Broadcast k_rope across all heads
|
||||
k[:, :, nope_head_dim:] = k_rope.expand(-1, k.shape[1], -1)
|
||||
|
||||
|
||||
def torch_concat_mla_absorb_q(
|
||||
a: torch.Tensor, b: torch.Tensor, out: torch.Tensor
|
||||
) -> None:
|
||||
"""Reference PyTorch implementation for concat_mla_absorb_q."""
|
||||
# a: [dim_0, dim_1, a_last_dim]
|
||||
# b: [dim_0, dim_1, b_last_dim]
|
||||
# out: [dim_0, dim_1, a_last_dim + b_last_dim]
|
||||
a_last_dim = a.shape[-1]
|
||||
out[:, :, :a_last_dim] = a
|
||||
out[:, :, a_last_dim:] = b
|
||||
|
||||
|
||||
def sgl_kernel_concat_mla_k(
|
||||
k: torch.Tensor, k_nope: torch.Tensor, k_rope: torch.Tensor
|
||||
) -> None:
|
||||
"""AOT compiled sgl_kernel implementation."""
|
||||
from sgl_kernel import concat_mla_k
|
||||
|
||||
concat_mla_k(k, k_nope, k_rope)
|
||||
|
||||
|
||||
def sgl_kernel_concat_mla_absorb_q(
|
||||
a: torch.Tensor, b: torch.Tensor, out: torch.Tensor
|
||||
) -> None:
|
||||
"""AOT compiled sgl_kernel implementation."""
|
||||
from sgl_kernel import concat_mla_absorb_q
|
||||
|
||||
result = concat_mla_absorb_q(a, b) # AOT returns output
|
||||
out.copy_(result) # Copy to provided tensor for comparison
|
||||
|
||||
|
||||
def jit_concat_mla_k(
|
||||
k: torch.Tensor, k_nope: torch.Tensor, k_rope: torch.Tensor
|
||||
) -> None:
|
||||
"""JIT compiled implementation."""
|
||||
from sglang.jit_kernel.concat_mla import concat_mla_k
|
||||
|
||||
concat_mla_k(k, k_nope, k_rope)
|
||||
|
||||
|
||||
def jit_concat_mla_absorb_q(
|
||||
a: torch.Tensor, b: torch.Tensor, out: torch.Tensor
|
||||
) -> None:
|
||||
"""JIT compiled implementation - wrapper for test compatibility."""
|
||||
from sglang.jit_kernel.concat_mla import concat_mla_absorb_q
|
||||
|
||||
result = concat_mla_absorb_q(a, b)
|
||||
out.copy_(result)
|
||||
|
||||
|
||||
# Constants matching the kernel
|
||||
NUM_LOCAL_HEADS = 128
|
||||
QK_NOPE_HEAD_DIM = 128
|
||||
QK_ROPE_HEAD_DIM = 64
|
||||
K_HEAD_DIM = QK_NOPE_HEAD_DIM + QK_ROPE_HEAD_DIM
|
||||
|
||||
A_LAST_DIM = 512
|
||||
B_LAST_DIM = 64
|
||||
OUT_LAST_DIM = A_LAST_DIM + B_LAST_DIM
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.bfloat16
|
||||
|
||||
# Test configurations
|
||||
NUM_TOKENS_LIST = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", NUM_TOKENS_LIST)
|
||||
def test_concat_mla_k_jit_vs_torch(num_tokens: int) -> None:
|
||||
"""Test JIT kernel against PyTorch reference."""
|
||||
k_jit = torch.empty(
|
||||
num_tokens, NUM_LOCAL_HEADS, K_HEAD_DIM, device=DEVICE, dtype=DTYPE
|
||||
)
|
||||
k_torch = torch.empty(
|
||||
num_tokens, NUM_LOCAL_HEADS, K_HEAD_DIM, device=DEVICE, dtype=DTYPE
|
||||
)
|
||||
|
||||
k_nope = torch.randn(
|
||||
num_tokens, NUM_LOCAL_HEADS, QK_NOPE_HEAD_DIM, device=DEVICE, dtype=DTYPE
|
||||
)
|
||||
k_rope = torch.randn(num_tokens, 1, QK_ROPE_HEAD_DIM, device=DEVICE, dtype=DTYPE)
|
||||
|
||||
torch_concat_mla_k(k_torch, k_nope, k_rope)
|
||||
jit_concat_mla_k(k_jit, k_nope, k_rope)
|
||||
|
||||
triton.testing.assert_close(k_jit, k_torch, atol=0, rtol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", NUM_TOKENS_LIST)
|
||||
def test_concat_mla_k_jit_vs_aot(num_tokens: int) -> None:
|
||||
"""Test JIT kernel against AOT kernel for bitwise equivalence."""
|
||||
k_jit = torch.empty(
|
||||
num_tokens, NUM_LOCAL_HEADS, K_HEAD_DIM, device=DEVICE, dtype=DTYPE
|
||||
)
|
||||
k_aot = torch.empty(
|
||||
num_tokens, NUM_LOCAL_HEADS, K_HEAD_DIM, device=DEVICE, dtype=DTYPE
|
||||
)
|
||||
|
||||
k_nope = torch.randn(
|
||||
num_tokens, NUM_LOCAL_HEADS, QK_NOPE_HEAD_DIM, device=DEVICE, dtype=DTYPE
|
||||
)
|
||||
k_rope = torch.randn(num_tokens, 1, QK_ROPE_HEAD_DIM, device=DEVICE, dtype=DTYPE)
|
||||
|
||||
sgl_kernel_concat_mla_k(k_aot, k_nope, k_rope)
|
||||
jit_concat_mla_k(k_jit, k_nope, k_rope)
|
||||
|
||||
triton.testing.assert_close(k_jit, k_aot, atol=0, rtol=0)
|
||||
|
||||
|
||||
DIM_0_LIST = [1, 2, 4, 8, 16, 32]
|
||||
DIM_1_LIST = [1, 2, 4, 8, 16, 128]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dim_0,dim_1",
|
||||
list(itertools.product(DIM_0_LIST, DIM_1_LIST)),
|
||||
)
|
||||
def test_concat_mla_absorb_q_jit_vs_torch(dim_0: int, dim_1: int) -> None:
|
||||
"""Test JIT kernel against PyTorch reference."""
|
||||
a = torch.randn(dim_0, dim_1, A_LAST_DIM, device=DEVICE, dtype=DTYPE)
|
||||
b = torch.randn(dim_0, dim_1, B_LAST_DIM, device=DEVICE, dtype=DTYPE)
|
||||
out_jit = torch.empty(dim_0, dim_1, OUT_LAST_DIM, device=DEVICE, dtype=DTYPE)
|
||||
out_torch = torch.empty(dim_0, dim_1, OUT_LAST_DIM, device=DEVICE, dtype=DTYPE)
|
||||
|
||||
torch_concat_mla_absorb_q(a, b, out_torch)
|
||||
jit_concat_mla_absorb_q(a, b, out_jit)
|
||||
|
||||
triton.testing.assert_close(out_jit, out_torch, atol=0, rtol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dim_0,dim_1",
|
||||
list(itertools.product(DIM_0_LIST, DIM_1_LIST)),
|
||||
)
|
||||
def test_concat_mla_absorb_q_jit_vs_aot(dim_0: int, dim_1: int) -> None:
|
||||
"""Test JIT kernel against AOT kernel for bitwise equivalence."""
|
||||
a = torch.randn(dim_0, dim_1, A_LAST_DIM, device=DEVICE, dtype=DTYPE)
|
||||
b = torch.randn(dim_0, dim_1, B_LAST_DIM, device=DEVICE, dtype=DTYPE)
|
||||
out_jit = torch.empty(dim_0, dim_1, OUT_LAST_DIM, device=DEVICE, dtype=DTYPE)
|
||||
out_aot = torch.empty(dim_0, dim_1, OUT_LAST_DIM, device=DEVICE, dtype=DTYPE)
|
||||
|
||||
sgl_kernel_concat_mla_absorb_q(a, b, out_aot)
|
||||
jit_concat_mla_absorb_q(a, b, out_jit)
|
||||
|
||||
triton.testing.assert_close(out_jit, out_aot, atol=0, rtol=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,239 +0,0 @@
|
||||
"""
|
||||
Correctness test for the JIT custom all-reduce (v2) kernel.
|
||||
|
||||
The test compares the JIT custom all-reduce output against NCCL all-reduce
|
||||
for various tensor sizes and dtypes, in both eager and CUDA-graph modes.
|
||||
|
||||
Usage:
|
||||
python -m pytest test_jit_custom_all_reduce.py -v
|
||||
|
||||
This file doubles as the torchrun worker script. The test class launches
|
||||
torchrun --nproc_per_node=N <this_file>
|
||||
and asserts that all worker processes exit successfully.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
import sglang.srt.distributed.parallel_state as ps
|
||||
from sglang.jit_kernel.all_reduce import (
|
||||
AllReduceAlgo,
|
||||
_jit_custom_all_reduce_pull_module,
|
||||
_jit_custom_all_reduce_push_module,
|
||||
)
|
||||
from sglang.jit_kernel.tests.utils import multiprocess_main, multiprocess_test
|
||||
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
|
||||
CustomAllReduceV2,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(
|
||||
est_time=300,
|
||||
suite="base-b-kernel-unit-8-gpu-h200",
|
||||
)
|
||||
register_cuda_ci(
|
||||
est_time=300,
|
||||
suite="nightly-kernel-8-gpu-h200",
|
||||
nightly=True,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test parameters (shared between test class and worker)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TEST_SIZES = [
|
||||
16,
|
||||
32,
|
||||
512,
|
||||
1024,
|
||||
1024 + 16, # weird case
|
||||
4 * 1024,
|
||||
32 * 1024,
|
||||
256 * 1024,
|
||||
2 * 1024 * 1024, # 2M elements
|
||||
4 * 1024 * 1024, # 4M elements
|
||||
]
|
||||
TEST_DTYPES = [torch.float16, torch.bfloat16, torch.float32]
|
||||
SHOTS = [
|
||||
AllReduceAlgo.ONE_SHOT_PULL,
|
||||
AllReduceAlgo.ONE_SHOT_PUSH,
|
||||
AllReduceAlgo.TWO_SHOT_PULL,
|
||||
]
|
||||
USE_GRAPH_OPTIONS = [True, False]
|
||||
TEST_CONFIG = itertools.product(TEST_SIZES, TEST_DTYPES, SHOTS, USE_GRAPH_OPTIONS)
|
||||
TEST_LAYERS = 4
|
||||
TEST_LOOP = 16
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test class (runs via pytest, launches torchrun subprocesses)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _compile_one(dtype: torch.dtype, world_size: int):
|
||||
_jit_custom_all_reduce_push_module(dtype, world_size)
|
||||
_jit_custom_all_reduce_pull_module(dtype, world_size)
|
||||
|
||||
|
||||
def _precompile_kernels() -> None:
|
||||
# NOTE: even when device count < 8, we should be able to compile all
|
||||
process_map: Dict[Tuple[torch.dtype, int], mp.Process] = {}
|
||||
COMPILE_SPACE = itertools.product(TEST_DTYPES, [2, 3, 4, 5, 6, 7, 8])
|
||||
mp.set_start_method("spawn")
|
||||
for config in COMPILE_SPACE:
|
||||
process_map[config] = mp.Process(target=_compile_one, args=config)
|
||||
for process in process_map.values():
|
||||
process.start()
|
||||
for (dtype, world_size), process in process_map.items():
|
||||
process.join()
|
||||
if process.exitcode != 0:
|
||||
raise RuntimeError(f"Custom All Reduce {world_size=} {dtype=} failed")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nproc", [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
def test_custom_allreduce(nproc: int) -> None:
|
||||
if nproc == 1: # NOTE: special case to speed up tests
|
||||
return _precompile_kernels()
|
||||
|
||||
device_count = torch.cuda.device_count()
|
||||
if device_count < nproc:
|
||||
pytest.skip(
|
||||
f"Requires at least {nproc} GPUs, but only {device_count} available"
|
||||
)
|
||||
multiprocess_test(__file__, nproc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Worker logic (executed by each torchrun process)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def init_distributed():
|
||||
"""Initialize distributed groups via torchrun env vars.
|
||||
|
||||
Returns (rank, device, cpu_group, nccl_group, comm).
|
||||
"""
|
||||
local_rank = int(os.environ["LOCAL_RANK"])
|
||||
world_size = int(os.environ["WORLD_SIZE"])
|
||||
rank = local_rank
|
||||
device = torch.device(f"cuda:{rank}")
|
||||
torch.cuda.set_device(device)
|
||||
|
||||
dist.init_process_group(backend="gloo")
|
||||
ps._WORLD = coord = ps.init_world_group(
|
||||
ranks=list(range(world_size)),
|
||||
local_rank=local_rank,
|
||||
backend="nccl",
|
||||
)
|
||||
|
||||
cpu_group = coord.cpu_group
|
||||
nccl_group = coord.device_group
|
||||
assert nccl_group is not None
|
||||
|
||||
max_size = max(TEST_SIZES) * 4
|
||||
comm = CustomAllReduceV2(cpu_group, device, max_size, max_size)
|
||||
if comm.disabled:
|
||||
raise RuntimeError("JIT CustomAllReduceV2 is disabled on this system")
|
||||
|
||||
return rank, device, cpu_group, nccl_group, comm
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def worker_test(
|
||||
device: torch.device,
|
||||
nccl_group: dist.ProcessGroup,
|
||||
comm: CustomAllReduceV2,
|
||||
size: int,
|
||||
dtype: torch.dtype,
|
||||
use_graph: bool,
|
||||
algo: AllReduceAlgo,
|
||||
) -> Optional[RuntimeError]:
|
||||
comm.override_algo = algo
|
||||
|
||||
def get_run_graph_fn():
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
graph_inp = torch.zeros((TEST_LAYERS, size), dtype=dtype, device=device)
|
||||
out_jits = []
|
||||
with comm.capture():
|
||||
with torch.cuda.graph(graph):
|
||||
for i in range(TEST_LAYERS):
|
||||
out_jits.append(comm.custom_all_reduce(graph_inp[i]))
|
||||
out_jit = torch.stack(out_jits)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
def run_graph(x: torch.Tensor) -> torch.Tensor:
|
||||
graph_inp.copy_(x)
|
||||
graph.replay()
|
||||
return out_jit.clone()
|
||||
|
||||
return run_graph
|
||||
|
||||
def get_run_eager_fn():
|
||||
def run_eager(x: torch.Tensor) -> torch.Tensor:
|
||||
eager_inp = x.clone()
|
||||
out_eagers = []
|
||||
for i in range(TEST_LAYERS):
|
||||
out_eagers.append(comm.custom_all_reduce(eager_inp[i]))
|
||||
torch.cuda.synchronize()
|
||||
return torch.stack(out_eagers)
|
||||
|
||||
return run_eager
|
||||
|
||||
run_fn = get_run_graph_fn() if use_graph else get_run_eager_fn()
|
||||
num_errors = 0
|
||||
for _ in range(TEST_LOOP):
|
||||
# NOTE: 15 * 8 < 128, which is the precision limit for bf16
|
||||
inp = torch.randint(0, 16, (TEST_LAYERS, size), dtype=dtype, device=device)
|
||||
assert comm.should_custom_ar(inp[0])
|
||||
out_ref = inp.clone()
|
||||
dist.all_reduce(out_ref, group=nccl_group)
|
||||
out_jit = run_fn(inp)
|
||||
num_errors += not torch.all(out_jit == out_ref)
|
||||
if num_errors > 0:
|
||||
return RuntimeError(
|
||||
f"Test failed for {size=}, {dtype=}, {algo=}, "
|
||||
f"{use_graph=} with {num_errors} errors. "
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def worker_main() -> None:
|
||||
"""Entry point for each torchrun worker process."""
|
||||
rank, device, cpu_group, nccl_group, comm = init_distributed()
|
||||
|
||||
torch.cuda.set_stream(torch.cuda.Stream())
|
||||
|
||||
logging.disable(logging.INFO) # Suppress internal logging for cleaner test output
|
||||
items = list(enumerate(TEST_CONFIG))
|
||||
for i, (size, dtype, algo, use_graph) in items:
|
||||
error = worker_test(device, nccl_group, comm, size, dtype, use_graph, algo)
|
||||
if error is not None:
|
||||
print(
|
||||
f"Worker {rank} failed for {size=}, {dtype=}, "
|
||||
f"{algo=}, {use_graph=}, iteration={i}\n"
|
||||
f"Error: {error}"
|
||||
)
|
||||
# communicate the result to rank 0 for logging
|
||||
result = torch.tensor([int(error is not None)])
|
||||
dist.all_reduce(result, group=cpu_group)
|
||||
failed = bool(result.item())
|
||||
if failed:
|
||||
raise RuntimeError(
|
||||
f"Test failed on rank {rank} for config: "
|
||||
f"{size=}, {dtype=}, {algo=}, {use_graph=}"
|
||||
)
|
||||
|
||||
comm.close()
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
multiprocess_main(__file__, worker_main)
|
||||
@@ -1,312 +0,0 @@
|
||||
"""Tests for CuTe DSL fused sigmoid gating delta rule kernel (GDN)."""
|
||||
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
try:
|
||||
import cuda.bindings.driver as cuda_driver
|
||||
import cutlass # noqa: F401
|
||||
from cutlass.cute.runtime import from_dlpack
|
||||
|
||||
from sglang.jit_kernel import cutedsl_gdn
|
||||
|
||||
CUTEDSL_AVAILABLE = True
|
||||
except ImportError:
|
||||
CUTEDSL_AVAILABLE = False
|
||||
cutedsl_gdn = None
|
||||
|
||||
try:
|
||||
from sglang.srt.layers.attention.fla.fused_sigmoid_gating_recurrent import (
|
||||
fused_sigmoid_gating_delta_rule_update,
|
||||
)
|
||||
|
||||
TRITON_AVAILABLE = True
|
||||
except ImportError:
|
||||
TRITON_AVAILABLE = False
|
||||
|
||||
register_cuda_ci(est_time=5, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def run_triton_kernel(A_log, dt_bias, q, k, v, a, b, initial_state, indices, scale):
|
||||
return fused_sigmoid_gating_delta_rule_update(
|
||||
A_log=A_log,
|
||||
a=a,
|
||||
dt_bias=dt_bias,
|
||||
softplus_beta=1.0,
|
||||
softplus_threshold=20.0,
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
b=b,
|
||||
initial_state_source=initial_state,
|
||||
initial_state_indices=indices,
|
||||
scale=scale,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
cu_seqlens=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CUTEDSL_AVAILABLE, reason="CuTe DSL not available")
|
||||
@pytest.mark.skipif(not TRITON_AVAILABLE, reason="Triton kernel not available")
|
||||
@pytest.mark.skip(
|
||||
reason=(
|
||||
"Temporary CI workaround: CuTe DSL GDN precision is currently unstable "
|
||||
"against the Triton reference and needs follow-up investigation."
|
||||
)
|
||||
)
|
||||
@pytest.mark.parametrize("B", [16, 128])
|
||||
def test_cutedsl_gdn_precision(B: int):
|
||||
"""Test precision of CuTe DSL GDN kernel against Triton reference."""
|
||||
torch.manual_seed(2025)
|
||||
T, H, K, V, HV = 1, 16, 128, 128, 32
|
||||
scale = K**-0.5
|
||||
|
||||
A_log = torch.randn(HV, dtype=torch.float32, device="cuda")
|
||||
dt_bias = torch.randn(HV, dtype=torch.bfloat16, device="cuda")
|
||||
a = torch.randn(B, T, HV, dtype=torch.bfloat16, device="cuda")
|
||||
b = torch.randn(B, T, HV, dtype=torch.bfloat16, device="cuda")
|
||||
q = torch.randn(B, T, H, K, dtype=torch.bfloat16, device="cuda")
|
||||
k = torch.randn(B, T, H, K, dtype=torch.bfloat16, device="cuda")
|
||||
v = torch.randn(B, T, HV, V, dtype=torch.bfloat16, device="cuda")
|
||||
indices = torch.arange(B, dtype=torch.int32, device="cuda")
|
||||
state_cutedsl = torch.randn(B, HV, K, V, dtype=torch.float32, device="cuda")
|
||||
state_triton = state_cutedsl.clone().reshape(-1).contiguous()
|
||||
|
||||
# Warmup compilation
|
||||
_ = cutedsl_gdn.cutedsl_fused_sigmoid_gating_delta_rule_update(
|
||||
A_log, dt_bias, q, k, v, a, b, state_cutedsl.clone(), indices, scale=scale
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# Fresh state for actual test
|
||||
state_cutedsl = torch.randn(B, HV, K, V, dtype=torch.float32, device="cuda")
|
||||
state_triton = state_cutedsl.clone().reshape(-1).contiguous()
|
||||
|
||||
out_cutedsl = cutedsl_gdn.cutedsl_fused_sigmoid_gating_delta_rule_update(
|
||||
A_log, dt_bias, q, k, v, a, b, state_cutedsl, indices, scale=scale
|
||||
)
|
||||
out_triton = run_triton_kernel(
|
||||
A_log, dt_bias, q, k, v, a, b, state_triton, indices, scale
|
||||
)
|
||||
|
||||
# Check precision: diff > 0.1 must be < 1% of elements
|
||||
abs_diff = (out_triton.float() - out_cutedsl.float()).abs()
|
||||
max_diff = abs_diff.max().item()
|
||||
mean_diff = abs_diff.mean().item()
|
||||
fail_rate = (abs_diff > 0.1).float().mean().item() * 100
|
||||
has_nan = torch.isnan(out_cutedsl).any() or torch.isinf(out_cutedsl).any()
|
||||
|
||||
kernel_type = "SmallBatch" if B < 32 else "LargeBatch"
|
||||
print(
|
||||
f"\n B={B} ({kernel_type}): max_diff={max_diff:.2e}, mean_diff={mean_diff:.2e}, fail_rate={fail_rate:.2f}%"
|
||||
)
|
||||
|
||||
assert not has_nan, "Output contains NaN/Inf"
|
||||
assert fail_rate < 1.0, f"Fail rate {fail_rate:.2f}% >= 1%"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
True,
|
||||
reason="Skip the performance test because the speedup ratio is highly unstable in the CI environment. ",
|
||||
)
|
||||
@pytest.mark.skipif(not CUTEDSL_AVAILABLE, reason="CuTe DSL not available")
|
||||
@pytest.mark.skipif(not TRITON_AVAILABLE, reason="Triton kernel not available")
|
||||
@pytest.mark.parametrize("B", [1, 128])
|
||||
def test_cutedsl_gdn_performance(B: int):
|
||||
"""Benchmark CuTe DSL GDN kernel against Triton reference."""
|
||||
torch.manual_seed(2025)
|
||||
T, H, K, V, HV = 1, 16, 128, 128, 32
|
||||
N = B
|
||||
scale = K**-0.5
|
||||
is_varlen = True
|
||||
warmup, bench_iters, run_iters = 10, 100, 10
|
||||
|
||||
A_log = torch.randn(HV, dtype=torch.float32, device="cuda")
|
||||
dt_bias = torch.randn(HV, dtype=torch.bfloat16, device="cuda")
|
||||
indices = torch.arange(N, dtype=torch.int32, device="cuda")
|
||||
state_cutedsl = torch.randn(N, HV, K, V, dtype=torch.float32, device="cuda")
|
||||
state_triton = state_cutedsl.reshape(-1).contiguous()
|
||||
cu_seqlens = torch.zeros(N + 1, dtype=torch.int32, device="cuda")
|
||||
o_cutedsl = torch.zeros(1, N, HV, V, dtype=torch.bfloat16, device="cuda")
|
||||
|
||||
# Prepare tensors for multiple runs
|
||||
q_list, k_list, v_list, a_list, b_list = [], [], [], [], []
|
||||
q_tensor_list, k_tensor_list, v_tensor_list, a_tensor_list, b_tensor_list = (
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
)
|
||||
q_triton, k_triton, v_triton, a_triton, b_triton = [], [], [], [], []
|
||||
|
||||
for ri in range(run_iters):
|
||||
torch.manual_seed(2025 + ri)
|
||||
q_i = torch.randn(1, N, H, K, dtype=torch.bfloat16, device="cuda")
|
||||
k_i = torch.randn(1, N, H, K, dtype=torch.bfloat16, device="cuda")
|
||||
v_i = torch.randn(1, N, HV, V, dtype=torch.bfloat16, device="cuda")
|
||||
a_i = torch.randn(N, HV, dtype=torch.bfloat16, device="cuda")
|
||||
b_i = torch.randn(N, HV, dtype=torch.bfloat16, device="cuda")
|
||||
|
||||
q_list.append(q_i)
|
||||
k_list.append(k_i)
|
||||
v_list.append(v_i)
|
||||
a_list.append(a_i)
|
||||
b_list.append(b_i)
|
||||
q_tensor_list.append(from_dlpack(q_i, assumed_align=16))
|
||||
k_tensor_list.append(from_dlpack(k_i, assumed_align=16))
|
||||
v_tensor_list.append(from_dlpack(v_i, assumed_align=16))
|
||||
a_tensor_list.append(from_dlpack(a_i, assumed_align=16))
|
||||
b_tensor_list.append(from_dlpack(b_i, assumed_align=16))
|
||||
q_triton.append(q_i.transpose(0, 1).contiguous())
|
||||
k_triton.append(k_i.transpose(0, 1).contiguous())
|
||||
v_triton.append(v_i.transpose(0, 1).contiguous())
|
||||
a_triton.append(a_i.unsqueeze(1).contiguous())
|
||||
b_triton.append(b_i.unsqueeze(1).contiguous())
|
||||
|
||||
A_log_t = from_dlpack(A_log, assumed_align=16)
|
||||
dt_bias_t = from_dlpack(dt_bias, assumed_align=16)
|
||||
h0_t = from_dlpack(state_cutedsl, assumed_align=16)
|
||||
idx_t = from_dlpack(indices, assumed_align=16)
|
||||
o_t = from_dlpack(o_cutedsl, assumed_align=16)
|
||||
cu_t = from_dlpack(cu_seqlens, assumed_align=16)
|
||||
|
||||
torch_stream = torch.cuda.Stream()
|
||||
stream = cuda_driver.CUstream(torch_stream.cuda_stream)
|
||||
|
||||
# Compile kernels
|
||||
compiled = cutedsl_gdn._get_compiled_kernel(N, H, HV, K, V, N, N < 32, is_varlen)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
for ri in range(run_iters):
|
||||
_ = run_triton_kernel(
|
||||
A_log,
|
||||
dt_bias,
|
||||
q_triton[ri],
|
||||
k_triton[ri],
|
||||
v_triton[ri],
|
||||
a_triton[ri],
|
||||
b_triton[ri],
|
||||
state_triton,
|
||||
indices,
|
||||
scale,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
def run_cutedsl():
|
||||
for ri in range(run_iters):
|
||||
compiled(
|
||||
cu_t,
|
||||
q_tensor_list[ri],
|
||||
k_tensor_list[ri],
|
||||
v_tensor_list[ri],
|
||||
a_tensor_list[ri],
|
||||
b_tensor_list[ri],
|
||||
A_log_t,
|
||||
dt_bias_t,
|
||||
h0_t,
|
||||
idx_t,
|
||||
o_t,
|
||||
stream,
|
||||
)
|
||||
|
||||
def run_triton():
|
||||
for ri in range(run_iters):
|
||||
_ = run_triton_kernel(
|
||||
A_log,
|
||||
dt_bias,
|
||||
q_triton[ri],
|
||||
k_triton[ri],
|
||||
v_triton[ri],
|
||||
a_triton[ri],
|
||||
b_triton[ri],
|
||||
state_triton,
|
||||
indices,
|
||||
scale,
|
||||
)
|
||||
|
||||
# Warmup
|
||||
with torch.cuda.stream(torch_stream):
|
||||
run_cutedsl()
|
||||
torch.cuda.synchronize()
|
||||
run_triton()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# Capture CUDA graphs
|
||||
graph_triton = torch.cuda.CUDAGraph()
|
||||
graph_cutedsl = torch.cuda.CUDAGraph()
|
||||
try:
|
||||
with torch.cuda.graph(graph_triton):
|
||||
run_triton()
|
||||
with torch.cuda.graph(graph_cutedsl, stream=torch_stream):
|
||||
run_cutedsl()
|
||||
torch.cuda.synchronize()
|
||||
except Exception:
|
||||
graph_triton = graph_cutedsl = None
|
||||
|
||||
# Warmup with graphs
|
||||
for _ in range(warmup):
|
||||
if graph_cutedsl:
|
||||
graph_cutedsl.replay()
|
||||
else:
|
||||
with torch.cuda.stream(torch_stream):
|
||||
run_cutedsl()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
if graph_triton:
|
||||
graph_triton.replay()
|
||||
else:
|
||||
run_triton()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# Benchmark
|
||||
triton_times, cutedsl_times = [], []
|
||||
for _ in range(bench_iters):
|
||||
start, end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(
|
||||
enable_timing=True
|
||||
)
|
||||
start.record()
|
||||
if graph_triton:
|
||||
graph_triton.replay()
|
||||
else:
|
||||
run_triton()
|
||||
end.record()
|
||||
torch.cuda.synchronize()
|
||||
triton_times.append(start.elapsed_time(end))
|
||||
|
||||
start, end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(
|
||||
enable_timing=True
|
||||
)
|
||||
with torch.cuda.stream(torch_stream):
|
||||
start.record()
|
||||
if graph_cutedsl:
|
||||
graph_cutedsl.replay()
|
||||
else:
|
||||
run_cutedsl()
|
||||
end.record()
|
||||
torch.cuda.synchronize()
|
||||
cutedsl_times.append(start.elapsed_time(end))
|
||||
|
||||
triton_mean = np.mean(triton_times) / run_iters * 1000
|
||||
triton_std = np.std(triton_times) / run_iters * 1000
|
||||
cutedsl_mean = np.mean(cutedsl_times) / run_iters * 1000
|
||||
cutedsl_std = np.std(cutedsl_times) / run_iters * 1000
|
||||
speedup = triton_mean / cutedsl_mean
|
||||
|
||||
kernel_type = "SmallBatch" if B < 32 else "LargeBatch"
|
||||
print(
|
||||
f"\n B={B} ({kernel_type}): Triton={triton_mean:.2f}±{triton_std:.2f}μs, CuTeDSL={cutedsl_mean:.2f}±{cutedsl_std:.2f}μs, speedup={speedup:.2f}x"
|
||||
)
|
||||
|
||||
min_speedup = 1.0 if B < 32 else 1.15
|
||||
assert speedup >= min_speedup, f"Speedup {speedup:.2f}x < {min_speedup}x for B={B}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,96 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=10, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def sglang_jit_fused_add_rmsnorm(
|
||||
input: torch.Tensor,
|
||||
residual: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
eps: float,
|
||||
*,
|
||||
cast_x_before_out_mul: bool = False,
|
||||
) -> None:
|
||||
from sglang.jit_kernel.norm import fused_add_rmsnorm
|
||||
|
||||
fused_add_rmsnorm(
|
||||
input, residual, weight, eps, cast_x_before_out_mul=cast_x_before_out_mul
|
||||
)
|
||||
|
||||
|
||||
def flashinfer_fused_add_rmsnorm(
|
||||
input: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, eps: float
|
||||
) -> None:
|
||||
from flashinfer.norm import fused_add_rmsnorm
|
||||
|
||||
fused_add_rmsnorm(input, residual, weight, eps=eps)
|
||||
|
||||
|
||||
def forward_native_hf_reference(
|
||||
x: torch.Tensor, residual: torch.Tensor, w: torch.Tensor, eps: float
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
sum_fp32 = x.to(torch.float32) + residual.to(torch.float32)
|
||||
residual_out = sum_fp32.to(x.dtype)
|
||||
variance = sum_fp32.pow(2).mean(-1, keepdim=True)
|
||||
out = w * (sum_fp32 * torch.rsqrt(variance + eps)).to(x.dtype)
|
||||
return out, residual_out
|
||||
|
||||
|
||||
BS_LIST = [2**n for n in range(0, 14)]
|
||||
BS_LIST += [x + 1 + i for i, x in enumerate(BS_LIST)]
|
||||
BS_LIST = get_ci_test_range(BS_LIST, [1, 9, 256, 4109])
|
||||
HIDDEN_SIZE_LIST = get_ci_test_range(
|
||||
[512, 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192],
|
||||
[512, 2048, 8192],
|
||||
)
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.bfloat16
|
||||
EPS = torch.finfo(torch.bfloat16).eps
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_size,hidden_size,cast_x_before_out_mul",
|
||||
list(itertools.product(BS_LIST, HIDDEN_SIZE_LIST, [False, True])),
|
||||
)
|
||||
def test_fused_add_rmsnorm(
|
||||
batch_size: int, hidden_size: int, cast_x_before_out_mul: bool
|
||||
) -> None:
|
||||
torch.manual_seed(0)
|
||||
input = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=DTYPE)
|
||||
residual = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=DTYPE)
|
||||
weight = torch.randn(hidden_size, device=DEVICE, dtype=DTYPE)
|
||||
|
||||
input_sglang = input.clone()
|
||||
residual_sglang = residual.clone()
|
||||
sglang_jit_fused_add_rmsnorm(
|
||||
input_sglang,
|
||||
residual_sglang,
|
||||
weight,
|
||||
EPS,
|
||||
cast_x_before_out_mul=cast_x_before_out_mul,
|
||||
)
|
||||
|
||||
if cast_x_before_out_mul:
|
||||
out_ref, residual_ref = forward_native_hf_reference(
|
||||
input, residual, weight, EPS
|
||||
)
|
||||
else:
|
||||
input_ref = input.clone()
|
||||
residual_ref_buf = residual.clone()
|
||||
flashinfer_fused_add_rmsnorm(input_ref, residual_ref_buf, weight, EPS)
|
||||
out_ref, residual_ref = input_ref, residual_ref_buf
|
||||
|
||||
torch.testing.assert_close(input_sglang, out_ref, atol=1e-2, rtol=1e-2)
|
||||
torch.testing.assert_close(residual_sglang, residual_ref, atol=1e-2, rtol=1e-2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,459 +0,0 @@
|
||||
"""
|
||||
Test for fused_store_index_k_cache kernel.
|
||||
|
||||
Design Notes:
|
||||
1. torch.cuda.synchronize() needed after TVM FFI kernel call.
|
||||
2. _split_buffer used buf[:, :vb].reshape(-1) which COPIES data for
|
||||
non-contiguous slices → reference buffer stayed all-zeros.
|
||||
Fix: use flat byte-offset indexing.
|
||||
3. act_quant may use a different quantization scheme → generous tolerance.
|
||||
4. FP8 E4M3 1-ULP rounding differences between CUDA hardware cast
|
||||
(__nv_fp8_e4m3) and PyTorch .to(float8_e4m3fn) at tie-break points.
|
||||
Adjacent FP8 representable values at the high end differ by up to 32
|
||||
in float space (e.g. 288, 320, 352, ..., 448).
|
||||
Need to compare dequantized values with FP8-appropriate tolerance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
try:
|
||||
from sglang.jit_kernel.fused_store_index_cache import (
|
||||
can_use_dsa_fused_store,
|
||||
fused_store_index_k_cache,
|
||||
)
|
||||
|
||||
HAS_FUSED = True
|
||||
except ImportError:
|
||||
HAS_FUSED = False
|
||||
|
||||
try:
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
_is_hip = is_hip()
|
||||
except ImportError:
|
||||
_is_hip = False
|
||||
|
||||
try:
|
||||
from sglang.srt.layers.quantization.fp8_kernel import is_fp8_fnuz
|
||||
|
||||
_is_fp8_fnuz = is_fp8_fnuz()
|
||||
except ImportError:
|
||||
_is_fp8_fnuz = False
|
||||
|
||||
register_cuda_ci(est_time=24, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
PAGE_SIZE = 64
|
||||
HEAD_DIM = 128
|
||||
FP8_E4M3_MAX = 448.0
|
||||
FP8_DTYPE = torch.float8_e4m3fn
|
||||
BYTES_PER_TOKEN = 128 + 4 # 128 fp8 bytes + 4 scale bytes
|
||||
BYTES_PER_PAGE = PAGE_SIZE * BYTES_PER_TOKEN
|
||||
|
||||
|
||||
def _skip_if_unavailable(page_size: int = PAGE_SIZE):
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required")
|
||||
if _is_hip:
|
||||
pytest.skip("Fused store kernel is CUDA-specific")
|
||||
if _is_fp8_fnuz:
|
||||
pytest.skip("Fused store path disabled for FP8 FNUZ")
|
||||
if not hasattr(torch, "float8_e4m3fn"):
|
||||
pytest.skip("torch.float8_e4m3fn not available")
|
||||
if not HAS_FUSED:
|
||||
pytest.skip("fused_store_index_cache not importable")
|
||||
if not can_use_dsa_fused_store(torch.bfloat16, torch.int64, page_size):
|
||||
pytest.skip("JIT kernel unavailable / failed to compile")
|
||||
|
||||
|
||||
def _num_pages(loc: torch.Tensor, page_size: int, extra: int = 1) -> int:
|
||||
return int(loc.max().item()) // page_size + 1 + extra
|
||||
|
||||
|
||||
def _make_buffer(num_pages: int, page_size: int = PAGE_SIZE) -> torch.Tensor:
|
||||
return torch.zeros(
|
||||
(num_pages, page_size * BYTES_PER_TOKEN),
|
||||
dtype=torch.uint8,
|
||||
device="cuda",
|
||||
)
|
||||
|
||||
|
||||
def _read_token_from_buffer(
|
||||
buf: torch.Tensor,
|
||||
token_idx: int,
|
||||
page_size: int = PAGE_SIZE,
|
||||
) -> Tuple[torch.Tensor, float]:
|
||||
"""
|
||||
Read a single token's fp8 values and scale from the paged buffer
|
||||
using flat byte offsets.
|
||||
"""
|
||||
page = token_idx // page_size
|
||||
offset = token_idx % page_size
|
||||
page_bytes = page_size * BYTES_PER_TOKEN
|
||||
|
||||
buf_flat = buf.reshape(-1)
|
||||
|
||||
val_start = page * page_bytes + offset * 128
|
||||
fp8_bytes = buf_flat[val_start : val_start + 128]
|
||||
fp8_vals = fp8_bytes.view(FP8_DTYPE).float()
|
||||
|
||||
scale_start = page * page_bytes + 128 * page_size + offset * 4
|
||||
scale_bytes = buf_flat[scale_start : scale_start + 4]
|
||||
scale = scale_bytes.view(torch.float32).item()
|
||||
|
||||
return fp8_vals, scale
|
||||
|
||||
|
||||
def _write_token_to_buffer(
|
||||
buf: torch.Tensor,
|
||||
token_idx: int,
|
||||
fp8_data: torch.Tensor,
|
||||
scale: float,
|
||||
page_size: int = PAGE_SIZE,
|
||||
) -> None:
|
||||
"""
|
||||
Write a single token's fp8 values and scale into the paged buffer
|
||||
using flat byte offsets on buf.reshape(-1) (which is a true view
|
||||
since buf is contiguous).
|
||||
"""
|
||||
page = token_idx // page_size
|
||||
offset = token_idx % page_size
|
||||
page_bytes = page_size * BYTES_PER_TOKEN
|
||||
|
||||
buf_flat = buf.reshape(-1)
|
||||
|
||||
val_start = page * page_bytes + offset * 128
|
||||
buf_flat[val_start : val_start + 128] = fp8_data.view(torch.uint8)
|
||||
|
||||
scale_start = page * page_bytes + 128 * page_size + offset * 4
|
||||
scale_t = torch.tensor([scale], dtype=torch.float32, device=buf.device)
|
||||
buf_flat[scale_start : scale_start + 4] = scale_t.view(torch.uint8)
|
||||
|
||||
|
||||
def _gather_tokens(
|
||||
buf: torch.Tensor,
|
||||
loc: torch.Tensor,
|
||||
page_size: int = PAGE_SIZE,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
N = loc.shape[0]
|
||||
fp8_f32 = torch.empty((N, HEAD_DIM), dtype=torch.float32, device=buf.device)
|
||||
scales = torch.empty((N,), dtype=torch.float32, device=buf.device)
|
||||
for i in range(N):
|
||||
idx = int(loc[i].item())
|
||||
vals, s = _read_token_from_buffer(buf, idx, page_size)
|
||||
fp8_f32[i] = vals
|
||||
scales[i] = s
|
||||
return fp8_f32, scales
|
||||
|
||||
|
||||
# Reference kernel
|
||||
def _reference_quantize_and_store(
|
||||
key_bf16: torch.Tensor,
|
||||
loc: torch.Tensor,
|
||||
num_pages: int,
|
||||
page_size: int = PAGE_SIZE,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Reference kernel of the fused kernel's quantization:
|
||||
abs_max = max(|row|)
|
||||
scale = max(1e-4, abs_max) / 448
|
||||
fp8_val = clip(val / scale, -448, 448) -> cast to fp8
|
||||
"""
|
||||
N = key_bf16.shape[0]
|
||||
key_f32 = key_bf16.float()
|
||||
buf = _make_buffer(num_pages, page_size)
|
||||
|
||||
for i in range(N):
|
||||
row = key_f32[i]
|
||||
abs_max = row.abs().max().item()
|
||||
scale = max(1e-4, abs_max) / FP8_E4M3_MAX
|
||||
inv_scale = 1.0 / scale
|
||||
quantized = (row * inv_scale).clamp(-FP8_E4M3_MAX, FP8_E4M3_MAX)
|
||||
quantized_fp8 = quantized.to(FP8_DTYPE)
|
||||
|
||||
idx = int(loc[i].item())
|
||||
_write_token_to_buffer(buf, idx, quantized_fp8, scale, page_size)
|
||||
|
||||
return buf
|
||||
|
||||
|
||||
def _import_act_quant():
|
||||
try:
|
||||
from sglang.srt.layers.attention.dsa.triton_kernel import act_quant
|
||||
|
||||
return act_quant
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _ref_store_via_act_quant(
|
||||
key_bf16: torch.Tensor,
|
||||
loc: torch.Tensor,
|
||||
num_pages: int,
|
||||
page_size: int = PAGE_SIZE,
|
||||
block_size: int = 128,
|
||||
scale_fmt: Optional[str] = None,
|
||||
) -> Optional[torch.Tensor]:
|
||||
act_quant = _import_act_quant()
|
||||
if act_quant is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
k_fp8, k_scale = act_quant(key_bf16, block_size, scale_fmt)
|
||||
except TypeError:
|
||||
k_fp8, k_scale = act_quant(key_bf16, block_size)
|
||||
|
||||
if k_fp8.dim() == 3 and k_fp8.shape[1] == 1:
|
||||
k_fp8 = k_fp8.squeeze(1)
|
||||
if k_scale is not None and k_scale.dim() == 3 and k_scale.shape[1] == 1:
|
||||
k_scale = k_scale.squeeze(1)
|
||||
k_scale = k_scale.view(-1).float()
|
||||
|
||||
buf = _make_buffer(num_pages, page_size)
|
||||
N = key_bf16.shape[0]
|
||||
for i in range(N):
|
||||
idx = int(loc[i].item())
|
||||
_write_token_to_buffer(
|
||||
buf, idx, k_fp8[i].to(FP8_DTYPE), k_scale[i].item(), page_size
|
||||
)
|
||||
return buf
|
||||
|
||||
|
||||
# TEST 1: Fused kernel vs. its own algorithm (pure-Python reference)
|
||||
#
|
||||
# NOTE on FP8 rounding:
|
||||
# CUDA hardware fp8 cast (__nv_fp8_e4m3) and PyTorch .to(float8_e4m3fn)
|
||||
# may round differently at tie-break points. This causes up to 1-ULP
|
||||
# differences in the FP8 codes. In FP8 E4M3, adjacent representable
|
||||
# values at the high end differ by up to 32 in float space (e.g.
|
||||
# 288 vs 320). After dequantization (fp8_float * scale), the error
|
||||
# from 1-ULP is: scale * ulp ≈ (abs_max/448) * 32 ≈ 0.07 * abs_max.
|
||||
# For randn inputs (abs_max ≈ 3-4), this is about 0.2-0.3.
|
||||
#
|
||||
# We therefore compare dequantized values with tolerances that
|
||||
# accommodate 1-ULP FP8 rounding, NOT byte-exact fp8 codes.
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens,base_index",
|
||||
[(1, 0), (32, 0), (64, 0), (128, 64), (257, 65), (512, 0)],
|
||||
)
|
||||
def test_fused_kernel_matches_own_algorithm(num_tokens: int, base_index: int):
|
||||
"""Compare fused CUDA kernel against a pure-Python implementation
|
||||
of the *same* quantization formula."""
|
||||
_skip_if_unavailable()
|
||||
device = torch.device("cuda")
|
||||
|
||||
key = torch.randn((num_tokens, HEAD_DIM), device=device, dtype=torch.bfloat16)
|
||||
loc = (
|
||||
base_index + torch.randperm(num_tokens, device=device, dtype=torch.int64)
|
||||
).contiguous()
|
||||
num_pages = _num_pages(loc, PAGE_SIZE)
|
||||
|
||||
# Reference kernel
|
||||
ref_buf = _reference_quantize_and_store(key, loc, num_pages)
|
||||
|
||||
# Fused kernel
|
||||
out_buf = _make_buffer(num_pages)
|
||||
fused_store_index_k_cache(key, out_buf, loc, page_size=PAGE_SIZE)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
out_f, out_s = _gather_tokens(out_buf, loc)
|
||||
ref_f, ref_s = _gather_tokens(ref_buf, loc)
|
||||
|
||||
# 1) Scales must match tightly (same f32 formula, no rounding ambiguity)
|
||||
torch.testing.assert_close(out_s, ref_s, rtol=1e-5, atol=1e-7)
|
||||
|
||||
# 2) Most FP8 codes should match; allow rare 1-ULP differences.
|
||||
# 1-ULP at FP8 E4M3 high end = 32 in float space.
|
||||
mismatch = out_f != ref_f
|
||||
mismatch_frac = mismatch.float().mean().item()
|
||||
assert mismatch_frac < 0.01, (
|
||||
f"Too many FP8 code mismatches: {mismatch_frac:.2%} "
|
||||
f"(expected < 1% from rounding tie-breaks)"
|
||||
)
|
||||
|
||||
# 3) Where codes differ, the difference should be exactly 1 ULP.
|
||||
# In FP8 E4M3: if the float-cast value is V, the adjacent value
|
||||
# differs by ~V * 0.1 (relative) at most.
|
||||
if mismatch.any():
|
||||
diff = (out_f[mismatch] - ref_f[mismatch]).abs()
|
||||
rel_diff = diff / ref_f[mismatch].abs().clamp(min=1e-6)
|
||||
# 1-ULP relative difference for E4M3 is at most ~12.5% (2^-3)
|
||||
assert rel_diff.max().item() <= 0.15, (
|
||||
f"FP8 code difference exceeds 1-ULP: max relative diff = "
|
||||
f"{rel_diff.max().item():.4f}"
|
||||
)
|
||||
|
||||
# 4) Dequantized values should be close.
|
||||
# Max error from 1-ULP: scale * fp8_ulp ≈ (abs_max/448) * 32
|
||||
# For randn abs_max ≈ 3-4: max_err ≈ 0.21 - 0.29
|
||||
out_deq = out_f * out_s.unsqueeze(-1)
|
||||
ref_deq = ref_f * ref_s.unsqueeze(-1)
|
||||
torch.testing.assert_close(out_deq, ref_deq, rtol=0.15, atol=0.5)
|
||||
|
||||
|
||||
# TEST 2: Cross-check against act_quant
|
||||
@pytest.mark.parametrize("scale_fmt", [None, "fp32"])
|
||||
def test_fused_kernel_vs_act_quant_semantic(scale_fmt: Optional[str]):
|
||||
"""Both fused kernel and act_quant should approximately reconstruct
|
||||
the original bf16 values."""
|
||||
_skip_if_unavailable()
|
||||
device = torch.device("cuda")
|
||||
|
||||
num_tokens = 257
|
||||
base_index = 65
|
||||
key = torch.randn((num_tokens, HEAD_DIM), device=device, dtype=torch.bfloat16)
|
||||
loc = (
|
||||
base_index + torch.randperm(num_tokens, device=device, dtype=torch.int64)
|
||||
).contiguous()
|
||||
num_pages = _num_pages(loc, PAGE_SIZE)
|
||||
|
||||
ref_buf = _ref_store_via_act_quant(key, loc, num_pages, scale_fmt=scale_fmt)
|
||||
if ref_buf is None:
|
||||
pytest.skip("act_quant not available")
|
||||
|
||||
out_buf = _make_buffer(num_pages)
|
||||
fused_store_index_k_cache(key, out_buf, loc, page_size=PAGE_SIZE)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
out_f, out_s = _gather_tokens(out_buf, loc)
|
||||
ref_f, ref_s = _gather_tokens(ref_buf, loc)
|
||||
|
||||
out_deq = out_f * out_s.unsqueeze(-1)
|
||||
ref_deq = ref_f * ref_s.unsqueeze(-1)
|
||||
orig_f32 = key.float()
|
||||
|
||||
# Fused kernel should reconstruct original within FP8 precision
|
||||
torch.testing.assert_close(
|
||||
out_deq,
|
||||
orig_f32,
|
||||
rtol=0.15,
|
||||
atol=5e-2,
|
||||
msg="Fused kernel dequantized values don't approximate original",
|
||||
)
|
||||
|
||||
# act_quant may use a very different scale policy.
|
||||
try:
|
||||
torch.testing.assert_close(
|
||||
ref_deq,
|
||||
orig_f32,
|
||||
rtol=0.25,
|
||||
atol=0.5,
|
||||
msg="act_quant dequantized values don't approximate original",
|
||||
)
|
||||
except AssertionError:
|
||||
nonzero_frac = (ref_deq.abs() > 1e-6).float().mean().item()
|
||||
if nonzero_frac < 0.5:
|
||||
pytest.fail(
|
||||
f"act_quant output looks mostly zero ({nonzero_frac:.1%} nonzero)."
|
||||
)
|
||||
else:
|
||||
pytest.skip(
|
||||
f"act_quant uses a very different quantization scheme "
|
||||
f"(scale_fmt={scale_fmt}). Fused kernel validated independently."
|
||||
)
|
||||
|
||||
torch.testing.assert_close(
|
||||
out_deq,
|
||||
ref_deq,
|
||||
rtol=0.3,
|
||||
atol=0.5,
|
||||
msg="Fused and act_quant dequantized values diverge too much",
|
||||
)
|
||||
|
||||
|
||||
# TEST 3: Roundtrip reconstruction
|
||||
@pytest.mark.parametrize("num_tokens", [1, 64, 257])
|
||||
def test_roundtrip_reconstruction(num_tokens: int):
|
||||
_skip_if_unavailable()
|
||||
device = torch.device("cuda")
|
||||
|
||||
key = torch.randn((num_tokens, HEAD_DIM), device=device, dtype=torch.bfloat16)
|
||||
loc = torch.arange(num_tokens, device=device, dtype=torch.int64)
|
||||
num_pages = _num_pages(loc, PAGE_SIZE)
|
||||
|
||||
buf = _make_buffer(num_pages)
|
||||
fused_store_index_k_cache(key, buf, loc, page_size=PAGE_SIZE)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
fp8_f32, scales = _gather_tokens(buf, loc)
|
||||
reconstructed = fp8_f32 * scales.unsqueeze(-1)
|
||||
original = key.float()
|
||||
|
||||
torch.testing.assert_close(reconstructed, original, rtol=0.15, atol=5e-2)
|
||||
|
||||
per_row_energy = reconstructed.abs().sum(dim=-1)
|
||||
orig_energy = original.abs().sum(dim=-1)
|
||||
mask = orig_energy > 0.1
|
||||
assert (
|
||||
per_row_energy[mask] > 0.01
|
||||
).all(), "Some tokens have zero reconstruction — kernel may not be writing output"
|
||||
|
||||
|
||||
# TEST 4: Boundary conditions
|
||||
def test_single_token():
|
||||
_skip_if_unavailable()
|
||||
device = torch.device("cuda")
|
||||
|
||||
key = torch.randn((1, HEAD_DIM), device=device, dtype=torch.bfloat16)
|
||||
loc = torch.tensor([0], device=device, dtype=torch.int64)
|
||||
|
||||
buf = _make_buffer(1)
|
||||
fused_store_index_k_cache(key, buf, loc, page_size=PAGE_SIZE)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
fp8_f32, scales = _gather_tokens(buf, loc)
|
||||
reconstructed = fp8_f32 * scales.unsqueeze(-1)
|
||||
torch.testing.assert_close(reconstructed, key.float(), rtol=0.15, atol=5e-2)
|
||||
|
||||
|
||||
# TEST 5: Zero input conditions
|
||||
def test_zero_input():
|
||||
_skip_if_unavailable()
|
||||
device = torch.device("cuda")
|
||||
|
||||
key = torch.zeros((4, HEAD_DIM), device=device, dtype=torch.bfloat16)
|
||||
loc = torch.arange(4, device=device, dtype=torch.int64)
|
||||
|
||||
buf = _make_buffer(1)
|
||||
fused_store_index_k_cache(key, buf, loc, page_size=PAGE_SIZE)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
fp8_f32, scales = _gather_tokens(buf, loc)
|
||||
|
||||
expected_scale = 1e-4 / FP8_E4M3_MAX
|
||||
torch.testing.assert_close(
|
||||
scales,
|
||||
torch.full_like(scales, expected_scale),
|
||||
rtol=1e-5,
|
||||
atol=1e-10,
|
||||
)
|
||||
assert (fp8_f32 == 0).all()
|
||||
|
||||
|
||||
# TEST 6: Sanity check — verify reference itself writes non-zero data
|
||||
def test_reference_writes_nonzero():
|
||||
_skip_if_unavailable()
|
||||
device = torch.device("cuda")
|
||||
|
||||
key = torch.randn((8, HEAD_DIM), device=device, dtype=torch.bfloat16)
|
||||
loc = torch.arange(8, device=device, dtype=torch.int64)
|
||||
|
||||
buf = _reference_quantize_and_store(key, loc, num_pages=1)
|
||||
|
||||
fp8_f32, scales = _gather_tokens(buf, loc)
|
||||
deq = fp8_f32 * scales.unsqueeze(-1)
|
||||
|
||||
assert deq.abs().sum().item() > 0, "Reference buffer is all zeros — error!"
|
||||
torch.testing.assert_close(deq, key.float(), rtol=0.15, atol=5e-2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,238 +0,0 @@
|
||||
"""Tests for fused sigmoid gating delta rule MTP kernel (GDN target_verify).
|
||||
|
||||
Compares the fused kernel `fused_sigmoid_gating_delta_rule_update` against
|
||||
the reference two-step implementation:
|
||||
1. g, beta = fused_gdn_gating(A_log, a, b, dt_bias)
|
||||
2. o = fused_recurrent_gated_delta_rule_update(q, k, v, g, beta, ...)
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
try:
|
||||
from sglang.srt.layers.attention.fla.fused_gdn_gating import fused_gdn_gating
|
||||
from sglang.srt.layers.attention.fla.fused_recurrent import (
|
||||
fused_recurrent_gated_delta_rule_update,
|
||||
)
|
||||
from sglang.srt.layers.attention.fla.fused_sigmoid_gating_recurrent import (
|
||||
fused_sigmoid_gating_delta_rule_update,
|
||||
)
|
||||
|
||||
KERNELS_AVAILABLE = True
|
||||
except ImportError:
|
||||
KERNELS_AVAILABLE = False
|
||||
|
||||
register_cuda_ci(est_time=6, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def _make_tensors(N, T, H, HV, K, V, device="cuda", seed=2025):
|
||||
"""Create input tensors for GDN target_verify."""
|
||||
torch.manual_seed(seed)
|
||||
A_log = torch.randn(HV, dtype=torch.float32, device=device)
|
||||
dt_bias = torch.randn(HV, dtype=torch.bfloat16, device=device)
|
||||
a = torch.randn(1, N * T, HV, dtype=torch.bfloat16, device=device)
|
||||
b = torch.randn(1, N * T, HV, dtype=torch.bfloat16, device=device)
|
||||
q = torch.randn(1, N * T, H, K, dtype=torch.bfloat16, device=device)
|
||||
k = torch.randn(1, N * T, H, K, dtype=torch.bfloat16, device=device)
|
||||
v = torch.randn(1, N * T, HV, V, dtype=torch.bfloat16, device=device)
|
||||
indices = torch.arange(N, dtype=torch.int32, device=device)
|
||||
initial_state = torch.randn(N, HV, K, V, dtype=torch.float, device=device)
|
||||
cu_seqlens = torch.arange(0, N * T + 1, T, dtype=torch.int32, device=device)
|
||||
return A_log, dt_bias, a, b, q, k, v, initial_state, indices, cu_seqlens
|
||||
|
||||
|
||||
def run_reference(
|
||||
A_log,
|
||||
dt_bias,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a,
|
||||
b,
|
||||
initial_state_source,
|
||||
initial_state_indices,
|
||||
cu_seqlens,
|
||||
disable_state_update=True,
|
||||
intermediate_states_buffer=None,
|
||||
intermediate_state_indices=None,
|
||||
cache_steps=None,
|
||||
retrieve_parent_token=None,
|
||||
):
|
||||
"""Reference: fused_gdn_gating + fused_recurrent_gated_delta_rule_update."""
|
||||
# fused_gdn_gating expects 2D [seq_len, HV]
|
||||
a_2d = a.view(-1, a.shape[-1])
|
||||
b_2d = b.view(-1, b.shape[-1])
|
||||
g, beta = fused_gdn_gating(A_log, a_2d, b_2d, dt_bias)
|
||||
# fused_recurrent expects 3D [B, T, HV]
|
||||
g = g.view(a.shape)
|
||||
beta = beta.view(b.shape)
|
||||
|
||||
# fused_recurrent requires intermediate_state_indices when cu_seqlens is used
|
||||
if cu_seqlens is not None and intermediate_state_indices is None:
|
||||
N = len(cu_seqlens) - 1
|
||||
intermediate_state_indices = torch.arange(N, dtype=torch.int32, device=q.device)
|
||||
|
||||
return fused_recurrent_gated_delta_rule_update(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
g=g,
|
||||
beta=beta,
|
||||
initial_state_source=initial_state_source,
|
||||
initial_state_indices=initial_state_indices,
|
||||
cu_seqlens=cu_seqlens,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
disable_state_update=disable_state_update,
|
||||
intermediate_states_buffer=intermediate_states_buffer,
|
||||
intermediate_state_indices=intermediate_state_indices,
|
||||
cache_steps=cache_steps,
|
||||
retrieve_parent_token=retrieve_parent_token,
|
||||
)
|
||||
|
||||
|
||||
def run_fused_mtp(
|
||||
A_log,
|
||||
dt_bias,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a,
|
||||
b,
|
||||
initial_state_source,
|
||||
initial_state_indices,
|
||||
cu_seqlens,
|
||||
disable_state_update=True,
|
||||
intermediate_states_buffer=None,
|
||||
intermediate_state_indices=None,
|
||||
cache_steps=None,
|
||||
retrieve_parent_token=None,
|
||||
):
|
||||
"""Fused: fused_sigmoid_gating_delta_rule_update."""
|
||||
return fused_sigmoid_gating_delta_rule_update(
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
a=a,
|
||||
b=b,
|
||||
initial_state_source=initial_state_source,
|
||||
initial_state_indices=initial_state_indices,
|
||||
cu_seqlens=cu_seqlens,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
softplus_beta=1.0,
|
||||
softplus_threshold=20.0,
|
||||
is_kda=False,
|
||||
disable_state_update=disable_state_update,
|
||||
intermediate_states_buffer=intermediate_states_buffer,
|
||||
intermediate_state_indices=intermediate_state_indices,
|
||||
cache_steps=cache_steps,
|
||||
retrieve_parent_token=retrieve_parent_token,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not KERNELS_AVAILABLE, reason="Kernel not available")
|
||||
@pytest.mark.parametrize("N", [1, 8, 16])
|
||||
@pytest.mark.parametrize("T", [1, 4, 8])
|
||||
def test_fused_gdn_mtp_precision(N: int, T: int):
|
||||
"""Compare fused MTP output against reference."""
|
||||
H, HV, K, V = 16, 32, 128, 128
|
||||
|
||||
A_log, dt_bias, a, b, q, k, v, state, indices, cu_seqlens = _make_tensors(
|
||||
N, T, H, HV, K, V
|
||||
)
|
||||
|
||||
state_ref = state.clone()
|
||||
state_fused = state.clone()
|
||||
|
||||
out_ref = run_reference(
|
||||
A_log,
|
||||
dt_bias,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a,
|
||||
b,
|
||||
state_ref,
|
||||
indices,
|
||||
cu_seqlens,
|
||||
disable_state_update=True,
|
||||
)
|
||||
out_fused = run_fused_mtp(
|
||||
A_log,
|
||||
dt_bias,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a,
|
||||
b,
|
||||
state_fused,
|
||||
indices,
|
||||
cu_seqlens,
|
||||
disable_state_update=True,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(out_ref, out_fused, rtol=1e-2, atol=1e-2)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not KERNELS_AVAILABLE, reason="Kernels not available")
|
||||
@pytest.mark.parametrize("N", [1, 16, 128])
|
||||
def test_mtp_single_step_decode(N: int):
|
||||
"""Verify MTP kernel matches reference for T=1 (decode scenario)."""
|
||||
T = 1
|
||||
H, HV, K, V = 16, 32, 128, 128
|
||||
|
||||
A_log, dt_bias, a, b, q, k, v, state, indices, cu_seqlens = _make_tensors(
|
||||
N, T, H, HV, K, V
|
||||
)
|
||||
|
||||
state_ref = state.clone()
|
||||
state_fused = state.clone()
|
||||
|
||||
out_ref = run_reference(
|
||||
A_log,
|
||||
dt_bias,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a,
|
||||
b,
|
||||
state_ref,
|
||||
indices,
|
||||
cu_seqlens,
|
||||
disable_state_update=False,
|
||||
)
|
||||
out_fused = run_fused_mtp(
|
||||
A_log,
|
||||
dt_bias,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a,
|
||||
b,
|
||||
state_fused,
|
||||
indices,
|
||||
cu_seqlens,
|
||||
disable_state_update=False,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(out_ref, out_fused, rtol=1e-2, atol=1e-2)
|
||||
|
||||
# Also verify states match after update
|
||||
state_diff = (state_ref.float() - state_fused.float()).abs()
|
||||
state_max_diff = state_diff.max().item()
|
||||
state_fail_rate = (state_diff > 0.1).float().mean().item() * 100
|
||||
print(
|
||||
f" single_step state N={N}: max_diff={state_max_diff:.2e}, "
|
||||
f"fail_rate={state_fail_rate:.2f}%"
|
||||
)
|
||||
assert state_fail_rate < 0.01, f"State mismatch: fail_rate={state_fail_rate:.2f}%"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,194 +0,0 @@
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from sgl_kernel.scalar_type import scalar_types
|
||||
|
||||
from sglang.jit_kernel.gptq_marlin import gptq_marlin_gemm
|
||||
from sglang.srt.layers.quantization.marlin_utils import (
|
||||
check_marlin_supported,
|
||||
marlin_make_workspace,
|
||||
)
|
||||
from sglang.srt.layers.quantization.marlin_utils_fp4 import (
|
||||
apply_fp4_marlin_linear,
|
||||
nvfp4_marlin_process_global_scale,
|
||||
prepare_nvfp4_layer_for_marlin,
|
||||
)
|
||||
from sglang.srt.utils.common import is_sm80_supported, is_sm90_supported
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_marlin_utils import (
|
||||
awq_marlin_quantize,
|
||||
make_nvfp4_weight_and_ref,
|
||||
marlin_quantize,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=13, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
MNK_FACTORS = [
|
||||
(1, 1, 1),
|
||||
(1, 4, 8),
|
||||
(13, 17, 67),
|
||||
(257, 13, 11),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("k_chunk", [128])
|
||||
@pytest.mark.parametrize("n_chunk", [64, 256])
|
||||
@pytest.mark.parametrize("quant_type", [scalar_types.uint4, scalar_types.uint4b8])
|
||||
@pytest.mark.parametrize("group_size", [-1, 128])
|
||||
@pytest.mark.parametrize("mnk_factors", MNK_FACTORS)
|
||||
@pytest.mark.parametrize("act_order", [False, True])
|
||||
def test_gptq_marlin_gemm(
|
||||
k_chunk,
|
||||
n_chunk,
|
||||
quant_type,
|
||||
group_size,
|
||||
mnk_factors,
|
||||
act_order,
|
||||
):
|
||||
m_factor, n_factor, k_factor = mnk_factors
|
||||
has_zp = quant_type in [scalar_types.uint4, scalar_types.uint8]
|
||||
|
||||
size_m = m_factor
|
||||
size_k = k_chunk * k_factor
|
||||
size_n = n_chunk * n_factor
|
||||
|
||||
if act_order:
|
||||
if group_size == -1:
|
||||
return
|
||||
if group_size == size_k:
|
||||
return
|
||||
if has_zp:
|
||||
return
|
||||
|
||||
if size_k % group_size != 0:
|
||||
return
|
||||
|
||||
a_input = torch.randn((size_m, size_k), dtype=torch.float16, device="cuda")
|
||||
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
|
||||
|
||||
if has_zp:
|
||||
w_ref, marlin_q_w, marlin_s, marlin_zp = awq_marlin_quantize(
|
||||
b_weight, quant_type, group_size
|
||||
)
|
||||
g_idx = None
|
||||
sort_indices = None
|
||||
marlin_s2 = None
|
||||
else:
|
||||
w_ref, marlin_q_w, marlin_s, g_idx, sort_indices, _ = marlin_quantize(
|
||||
b_weight, quant_type, group_size, act_order
|
||||
)
|
||||
marlin_zp = None
|
||||
marlin_s2 = None
|
||||
|
||||
workspace = marlin_make_workspace(w_ref.device)
|
||||
|
||||
output = gptq_marlin_gemm(
|
||||
a_input,
|
||||
None,
|
||||
marlin_q_w,
|
||||
marlin_s,
|
||||
marlin_s2,
|
||||
marlin_zp,
|
||||
g_idx,
|
||||
sort_indices,
|
||||
workspace,
|
||||
quant_type,
|
||||
a_input.shape[0],
|
||||
b_weight.shape[1],
|
||||
a_input.shape[1],
|
||||
is_k_full=True,
|
||||
use_atomic_add=False,
|
||||
use_fp32_reduce=False,
|
||||
is_zp_float=False,
|
||||
)
|
||||
|
||||
output_ref = torch.matmul(a_input, w_ref)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# JIT kernel should produce approximately correct results vs torch.matmul
|
||||
max_diff = torch.mean(torch.abs(output - output_ref)) / torch.mean(
|
||||
torch.abs(output_ref)
|
||||
)
|
||||
assert max_diff < 0.04
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Skip, test pass locally but compiling takes too long in CI")
|
||||
@pytest.mark.skipif(
|
||||
not (is_sm80_supported() or is_sm90_supported()),
|
||||
reason="NVFP4 Marlin fallback tests require CUDA SM8X/SM9X",
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
def test_nvfp4_marlin_support_and_scale_transforms_sm80_sm90(dtype):
|
||||
major, minor = torch.cuda.get_device_capability()
|
||||
capability = major * 10 + minor
|
||||
assert check_marlin_supported(
|
||||
scalar_types.float4_e2m1f,
|
||||
group_size=16,
|
||||
has_zp=False,
|
||||
device_capability=capability,
|
||||
)
|
||||
|
||||
global_scale = torch.tensor(1.0, dtype=dtype, device="cuda")
|
||||
actual_global_scale = nvfp4_marlin_process_global_scale(global_scale)
|
||||
assert actual_global_scale.is_cuda
|
||||
assert actual_global_scale.ndim == 1
|
||||
assert actual_global_scale.numel() == 1
|
||||
if dtype == torch.float16:
|
||||
assert actual_global_scale.item() == 128.0
|
||||
else:
|
||||
assert actual_global_scale.item() == 2.0**119
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Skip, test pass locally but compiling takes too long in CI")
|
||||
@pytest.mark.skipif(
|
||||
not (is_sm80_supported() or is_sm90_supported()),
|
||||
reason="NVFP4 Marlin dense numeric test requires CUDA SM80, SM86, or SM90",
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
def test_nvfp4_marlin_dense_matches_dequant_reference(dtype):
|
||||
torch.manual_seed(0)
|
||||
|
||||
size_m = 17
|
||||
size_k = 256
|
||||
size_n = 192
|
||||
group_size = 16
|
||||
|
||||
a_input = torch.randn((size_m, size_k), dtype=dtype, device="cuda") / 10
|
||||
fp4_weight, scales, global_scale, weight_ref = make_nvfp4_weight_and_ref(
|
||||
size_n, size_k, dtype, group_size=group_size
|
||||
)
|
||||
|
||||
layer = torch.nn.Module()
|
||||
layer.quant_config = SimpleNamespace(group_size=group_size)
|
||||
layer.output_size_per_partition = size_n
|
||||
layer.input_size_per_partition = size_k
|
||||
layer.params_dtype = dtype
|
||||
layer.weight = torch.nn.Parameter(fp4_weight, requires_grad=False)
|
||||
layer.weight_scale = torch.nn.Parameter(scales, requires_grad=False)
|
||||
layer.weight_global_scale = torch.nn.Parameter(
|
||||
global_scale.reshape(1), requires_grad=False
|
||||
)
|
||||
prepare_nvfp4_layer_for_marlin(layer)
|
||||
|
||||
output = apply_fp4_marlin_linear(
|
||||
a_input,
|
||||
layer.weight,
|
||||
layer.weight_scale,
|
||||
layer.weight_global_scale,
|
||||
layer.workspace,
|
||||
size_n,
|
||||
size_k,
|
||||
use_fp32_reduce=True,
|
||||
)
|
||||
|
||||
output_ref = torch.matmul(a_input, weight_ref.T)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
torch.testing.assert_close(output, output_ref, rtol=0.04, atol=0.04)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,96 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from sgl_kernel.scalar_type import scalar_types
|
||||
|
||||
from sglang.jit_kernel.gptq_marlin_repack import gptq_marlin_repack
|
||||
from sglang.srt.layers.quantization.utils import (
|
||||
gptq_quantize_weights,
|
||||
pack_rows,
|
||||
sort_weights,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_marlin_utils import get_weight_perm, marlin_weights
|
||||
|
||||
register_cuda_ci(est_time=16, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
MARLIN_K_CHUNKS = [128]
|
||||
MARLIN_N_CHUNKS = [64, 256]
|
||||
|
||||
MNK_FACTORS = [
|
||||
(1, 1, 1),
|
||||
(1, 4, 8),
|
||||
(1, 7, 5),
|
||||
(13, 17, 67),
|
||||
(26, 37, 13),
|
||||
(67, 13, 11),
|
||||
(257, 13, 11),
|
||||
(658, 13, 11),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("k_chunk", MARLIN_K_CHUNKS)
|
||||
@pytest.mark.parametrize("n_chunk", MARLIN_N_CHUNKS)
|
||||
@pytest.mark.parametrize("quant_type", [scalar_types.uint4b8])
|
||||
@pytest.mark.parametrize("group_size", [-1, 32, 64, 128])
|
||||
@pytest.mark.parametrize("act_order", [False, True])
|
||||
@pytest.mark.parametrize("mnk_factors", MNK_FACTORS)
|
||||
def test_gptq_marlin_repack(
|
||||
k_chunk, n_chunk, quant_type, group_size, act_order, mnk_factors
|
||||
):
|
||||
m_factor, n_factor, k_factor = mnk_factors
|
||||
|
||||
size_k = k_chunk * k_factor
|
||||
size_n = n_chunk * n_factor
|
||||
|
||||
# Filter act_order
|
||||
if act_order:
|
||||
if group_size == -1:
|
||||
return
|
||||
if group_size == size_k:
|
||||
return
|
||||
|
||||
# Normalize group_size
|
||||
if group_size == -1:
|
||||
group_size = size_k
|
||||
assert group_size <= size_k
|
||||
|
||||
if size_k % group_size != 0:
|
||||
pytest.skip("size_k must be divisible by group_size")
|
||||
|
||||
# Create input
|
||||
b_weight = torch.randn((size_k, size_n), dtype=torch.float16, device="cuda")
|
||||
|
||||
# Quantize (and apply act_order if provided)
|
||||
w_ref, q_w, s, g_idx, rand_perm = gptq_quantize_weights(
|
||||
b_weight, quant_type, group_size, act_order
|
||||
)
|
||||
|
||||
q_w_gptq = pack_rows(q_w, quant_type.size_bits, size_k, size_n)
|
||||
|
||||
# For act_order, sort the "weights" and "g_idx" so that group ids are
|
||||
# increasing
|
||||
sort_indices = torch.empty(0, dtype=torch.int, device=b_weight.device)
|
||||
if act_order:
|
||||
q_w, g_idx, sort_indices = sort_weights(q_w, g_idx)
|
||||
|
||||
marlin_layout_perm = get_weight_perm(quant_type.size_bits)
|
||||
q_w_marlin_ref = marlin_weights(
|
||||
q_w, size_k, size_n, quant_type.size_bits, marlin_layout_perm
|
||||
)
|
||||
|
||||
# Run JIT repack kernel
|
||||
jit_output = gptq_marlin_repack(
|
||||
q_w_gptq, sort_indices, size_k, size_n, quant_type.size_bits
|
||||
)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# JIT should match the reference (computed from CPU marlin_weights)
|
||||
torch.testing.assert_close(jit_output, q_w_marlin_ref)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,210 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.grouped_topk import grouped_topk as jit_grouped_topk
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.srt.layers.moe.topk import biased_grouped_topk_impl
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
CORRECTNESS_CASES = get_ci_test_range(
|
||||
full_range=list(
|
||||
itertools.product(
|
||||
[1, 17, 128],
|
||||
[16, 32, 64, 128, 192, 256, 384, 512],
|
||||
[1, 2, 3, 4, 5, 6, 7, 8],
|
||||
)
|
||||
),
|
||||
ci_range=[
|
||||
(1, 16, 3), # smallest non-power-of-two topk
|
||||
(17, 128, 6), # Nemotron-3-Nano shape that exposed the bug
|
||||
(128, 192, 8), # Hunyuan-3 shape, power-of-two topk sanity case
|
||||
(33, 512, 7), # largest expert-count tier with non-power-of-two topk
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _make_inputs(num_tokens: int, num_experts: int, seed: int):
|
||||
torch.manual_seed(seed)
|
||||
hidden_states = torch.empty((num_tokens, 1), dtype=torch.float32, device="cuda")
|
||||
gating_output = torch.randn(
|
||||
(num_tokens, num_experts), dtype=torch.float32, device="cuda"
|
||||
)
|
||||
correction_bias = torch.randn(num_experts, dtype=torch.float32, device="cuda") * 0.1
|
||||
return hidden_states, gating_output, correction_bias
|
||||
|
||||
|
||||
def _scatter_by_expert(
|
||||
weights: torch.Tensor, ids: torch.Tensor, num_experts: int
|
||||
) -> torch.Tensor:
|
||||
dense = torch.zeros(
|
||||
(weights.shape[0], num_experts), dtype=torch.float32, device=weights.device
|
||||
)
|
||||
dense.scatter_(1, ids.long(), weights)
|
||||
return dense
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens,num_experts,topk", CORRECTNESS_CASES)
|
||||
def test_grouped_topk_renormalize_matches_reference(
|
||||
num_tokens: int, num_experts: int, topk: int
|
||||
) -> None:
|
||||
hidden_states, gating_output, correction_bias = _make_inputs(
|
||||
num_tokens, num_experts, seed=1000 + num_experts * 10 + topk
|
||||
)
|
||||
scaling_factor = 2.826 if (num_experts, topk) == (192, 8) else 1.0
|
||||
|
||||
topk_weights, topk_ids = jit_grouped_topk(
|
||||
gating_output,
|
||||
correction_bias,
|
||||
1,
|
||||
1,
|
||||
topk,
|
||||
True,
|
||||
scaling_factor,
|
||||
)
|
||||
ref_weights, ref_ids = biased_grouped_topk_impl(
|
||||
hidden_states,
|
||||
gating_output,
|
||||
correction_bias,
|
||||
topk,
|
||||
True,
|
||||
1,
|
||||
1,
|
||||
routed_scaling_factor=scaling_factor,
|
||||
apply_routed_scaling_factor_on_output=True,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
torch.testing.assert_close(
|
||||
_scatter_by_expert(topk_weights, topk_ids, num_experts),
|
||||
_scatter_by_expert(ref_weights, ref_ids, num_experts),
|
||||
rtol=1e-5,
|
||||
atol=1e-6,
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
topk_weights.sum(dim=-1),
|
||||
torch.full((num_tokens,), scaling_factor, dtype=torch.float32, device="cuda"),
|
||||
rtol=1e-5,
|
||||
atol=1e-6,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topk", [3, 5, 6, 7])
|
||||
def test_grouped_topk_non_power_of_two_renormalize(topk: int) -> None:
|
||||
hidden_states, gating_output, correction_bias = _make_inputs(
|
||||
num_tokens=64, num_experts=128, seed=2000 + topk
|
||||
)
|
||||
|
||||
topk_weights, topk_ids = jit_grouped_topk(
|
||||
gating_output,
|
||||
correction_bias,
|
||||
1,
|
||||
1,
|
||||
topk,
|
||||
True,
|
||||
1.0,
|
||||
)
|
||||
ref_weights, ref_ids = biased_grouped_topk_impl(
|
||||
hidden_states,
|
||||
gating_output,
|
||||
correction_bias,
|
||||
topk,
|
||||
True,
|
||||
1,
|
||||
1,
|
||||
routed_scaling_factor=1.0,
|
||||
apply_routed_scaling_factor_on_output=True,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
torch.testing.assert_close(
|
||||
_scatter_by_expert(topk_weights, topk_ids, 128),
|
||||
_scatter_by_expert(ref_weights, ref_ids, 128),
|
||||
rtol=1e-5,
|
||||
atol=1e-6,
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
topk_weights.sum(dim=-1),
|
||||
torch.ones((64,), dtype=torch.float32, device="cuda"),
|
||||
rtol=1e-5,
|
||||
atol=1e-6,
|
||||
)
|
||||
|
||||
|
||||
def test_grouped_topk_negative_choice_scores_match_reference() -> None:
|
||||
hidden_states, gating_output, correction_bias = _make_inputs(
|
||||
num_tokens=64, num_experts=128, seed=23758
|
||||
)
|
||||
correction_bias.fill_(-2.0)
|
||||
|
||||
topk_weights, topk_ids = jit_grouped_topk(
|
||||
gating_output,
|
||||
correction_bias,
|
||||
1,
|
||||
1,
|
||||
6,
|
||||
True,
|
||||
1.0,
|
||||
)
|
||||
ref_weights, ref_ids = biased_grouped_topk_impl(
|
||||
hidden_states,
|
||||
gating_output,
|
||||
correction_bias,
|
||||
6,
|
||||
True,
|
||||
1,
|
||||
1,
|
||||
routed_scaling_factor=1.0,
|
||||
apply_routed_scaling_factor_on_output=True,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
torch.testing.assert_close(
|
||||
_scatter_by_expert(topk_weights, topk_ids, 128),
|
||||
_scatter_by_expert(ref_weights, ref_ids, 128),
|
||||
rtol=1e-5,
|
||||
atol=1e-6,
|
||||
)
|
||||
|
||||
|
||||
def test_grouped_topk_without_renormalize_matches_reference() -> None:
|
||||
hidden_states, gating_output, correction_bias = _make_inputs(
|
||||
num_tokens=64, num_experts=128, seed=3006
|
||||
)
|
||||
|
||||
topk_weights, topk_ids = jit_grouped_topk(
|
||||
gating_output,
|
||||
correction_bias,
|
||||
1,
|
||||
1,
|
||||
6,
|
||||
False,
|
||||
1.0,
|
||||
)
|
||||
ref_weights, ref_ids = biased_grouped_topk_impl(
|
||||
hidden_states,
|
||||
gating_output,
|
||||
correction_bias,
|
||||
6,
|
||||
False,
|
||||
1,
|
||||
1,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
torch.testing.assert_close(
|
||||
_scatter_by_expert(topk_weights, topk_ids, 128),
|
||||
_scatter_by_expert(ref_weights, ref_ids, 128),
|
||||
rtol=1e-5,
|
||||
atol=1e-6,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,428 +0,0 @@
|
||||
import math
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from scipy.linalg import hadamard
|
||||
|
||||
from sglang.jit_kernel.hadamard import (
|
||||
hadamard_transform,
|
||||
hadamard_transform_12n,
|
||||
hadamard_transform_20n,
|
||||
hadamard_transform_28n,
|
||||
hadamard_transform_40n,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=128, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=512, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
# Exact M×N Hadamard matrices (±1 entries) copied from
|
||||
# python/sglang/jit_kernel/csrc/fast-hadamard-transform/code_gen.py.
|
||||
# These are non-power-of-2 Hadamard matrices constructed via Paley/Williamson methods.
|
||||
# "+" = +1, "-" = -1. Used by the _12n/_20n/_28n/_40n kernel variants.
|
||||
|
||||
_HAD_12_STR = """
|
||||
+-++++++++++
|
||||
--+-+-+-+-+-
|
||||
+++-++----++
|
||||
+---+--+-++-
|
||||
+++++-++----
|
||||
+-+---+--+-+
|
||||
++--+++-++--
|
||||
+--++---+--+
|
||||
++----+++-++
|
||||
+--+-++---+-
|
||||
++++----+++-
|
||||
+-+--+-++---
|
||||
"""
|
||||
|
||||
_HAD_20_STR = """
|
||||
+----+----++--++-++-
|
||||
-+----+---+++---+-++
|
||||
--+----+---+++-+-+-+
|
||||
---+----+---+++++-+-
|
||||
----+----++--++-++-+
|
||||
-+++++-----+--+++--+
|
||||
+-+++-+---+-+--+++--
|
||||
++-++--+---+-+--+++-
|
||||
+++-+---+---+-+--+++
|
||||
++++-----++--+-+--++
|
||||
--++-+-++-+-----++++
|
||||
---++-+-++-+---+-+++
|
||||
+---++-+-+--+--++-++
|
||||
++---++-+----+-+++-+
|
||||
-++---++-+----+++++-
|
||||
-+--+--++-+----+----
|
||||
+-+-----++-+----+---
|
||||
-+-+-+---+--+----+--
|
||||
--+-+++------+----+-
|
||||
+--+--++------+----+
|
||||
"""
|
||||
|
||||
_HAD_28_STR = """
|
||||
+------++----++-+--+-+--++--
|
||||
-+-----+++-----+-+--+-+--++-
|
||||
--+-----+++---+-+-+----+--++
|
||||
---+-----+++---+-+-+-+--+--+
|
||||
----+-----+++---+-+-+++--+--
|
||||
-----+-----++++--+-+--++--+-
|
||||
------++----++-+--+-+--++--+
|
||||
--++++-+-------++--+++-+--+-
|
||||
---++++-+-----+-++--+-+-+--+
|
||||
+---+++--+----++-++--+-+-+--
|
||||
++---++---+----++-++--+-+-+-
|
||||
+++---+----+----++-++--+-+-+
|
||||
++++--------+-+--++-++--+-+-
|
||||
-++++--------+++--++--+--+-+
|
||||
-+-++-++--++--+--------++++-
|
||||
+-+-++--+--++--+--------++++
|
||||
-+-+-++--+--++--+----+---+++
|
||||
+-+-+-++--+--+---+---++---++
|
||||
++-+-+-++--+------+--+++---+
|
||||
-++-+-+-++--+------+-++++---
|
||||
+-++-+---++--+------+-++++--
|
||||
-++--++-+-++-+++----++------
|
||||
+-++--++-+-++-+++-----+-----
|
||||
++-++---+-+-++-+++-----+----
|
||||
-++-++-+-+-+-+--+++-----+---
|
||||
--++-++++-+-+----+++-----+--
|
||||
+--++-+-++-+-+----+++-----+-
|
||||
++--++-+-++-+-+----++------+
|
||||
"""
|
||||
|
||||
_HAD_40_STR = """
|
||||
+-------------------+-------------------
|
||||
++-++----+-+-++++--+++-++----+-+-++++--+
|
||||
+++-++----+-+-++++--+++-++----+-+-++++--
|
||||
+-++-++----+-+-++++-+-++-++----+-+-++++-
|
||||
+--++-++----+-+-+++++--++-++----+-+-++++
|
||||
++--++-++----+-+-+++++--++-++----+-+-+++
|
||||
+++--++-++----+-+-+++++--++-++----+-+-++
|
||||
++++--++-++----+-+-+++++--++-++----+-+-+
|
||||
+++++--++-++----+-+-+++++--++-++----+-+-
|
||||
+-++++--++-++----+-++-++++--++-++----+-+
|
||||
++-++++--++-++----+-++-++++--++-++----+-
|
||||
+-+-++++--++-++----++-+-++++--++-++----+
|
||||
++-+-++++--++-++----++-+-++++--++-++----
|
||||
+-+-+-++++--++-++---+-+-+-++++--++-++---
|
||||
+--+-+-++++--++-++--+--+-+-++++--++-++--
|
||||
+---+-+-++++--++-++-+---+-+-++++--++-++-
|
||||
+----+-+-++++--++-+++----+-+-++++--++-++
|
||||
++----+-+-++++--++-+++----+-+-++++--++-+
|
||||
+++----+-+-++++--++-+++----+-+-++++--++-
|
||||
+-++----+-+-++++--+++-++----+-+-++++--++
|
||||
+--------------------+++++++++++++++++++
|
||||
++-++----+-+-++++--+--+--++++-+-+----++-
|
||||
+++-++----+-+-++++-----+--++++-+-+----++
|
||||
+-++-++----+-+-++++--+--+--++++-+-+----+
|
||||
+--++-++----+-+-++++-++--+--++++-+-+----
|
||||
++--++-++----+-+-+++--++--+--++++-+-+---
|
||||
+++--++-++----+-+-++---++--+--++++-+-+--
|
||||
++++--++-++----+-+-+----++--+--++++-+-+-
|
||||
+++++--++-++----+-+------++--+--++++-+-+
|
||||
+-++++--++-++----+-+-+----++--+--++++-+-
|
||||
++-++++--++-++----+---+----++--+--++++-+
|
||||
+-+-++++--++-++----+-+-+----++--+--++++-
|
||||
++-+-++++--++-++------+-+----++--+--++++
|
||||
+-+-+-++++--++-++----+-+-+----++--+--+++
|
||||
+--+-+-++++--++-++---++-+-+----++--+--++
|
||||
+---+-+-++++--++-++--+++-+-+----++--+--+
|
||||
+----+-+-++++--++-++-++++-+-+----++--+--
|
||||
++----+-+-++++--++-+--++++-+-+----++--+-
|
||||
+++----+-+-++++--++----++++-+-+----++--+
|
||||
+-++----+-+-++++--++-+--++++-+-+----++--
|
||||
"""
|
||||
|
||||
|
||||
def _parse_hadamard_str(s):
|
||||
"""Parse a ±1 string matrix definition into a numpy array."""
|
||||
s = s.strip().replace("+", "1").replace("-", "-1").split()
|
||||
return np.stack(
|
||||
[np.fromstring(" ".join(s[i]), dtype=np.int32, sep=" ") for i in range(len(s))]
|
||||
)
|
||||
|
||||
|
||||
# Parsed M×M special Hadamard matrices, keyed by M (the "multiple").
|
||||
# Copied from python/sglang/jit_kernel/csrc/fast-hadamard-transform/code_gen.py
|
||||
# (had_12_paley, had_20_will, had_28_will, had_40_tpal)
|
||||
_SPECIAL_MATRICES = {
|
||||
12: _parse_hadamard_str(_HAD_12_STR),
|
||||
20: _parse_hadamard_str(_HAD_20_STR),
|
||||
28: _parse_hadamard_str(_HAD_28_STR),
|
||||
40: _parse_hadamard_str(_HAD_40_STR),
|
||||
}
|
||||
|
||||
|
||||
def hadamard_transform_ref(x, scale=1.0):
|
||||
"""Reference impl for the general (power-of-2) hadamard_transform.
|
||||
|
||||
Pads dim to the next power of 2, multiplies by the full H matrix
|
||||
via F.linear, then truncates back to the original dim.
|
||||
"""
|
||||
x_shape = x.shape
|
||||
dim = x.shape[-1]
|
||||
x = x.reshape(-1, dim)
|
||||
log_dim = math.ceil(math.log2(dim)) if dim > 0 else 0
|
||||
dim_padded = 2**log_dim if dim > 0 else 1
|
||||
if dim != dim_padded:
|
||||
x = F.pad(x, (0, dim_padded - dim))
|
||||
H = torch.tensor(hadamard(dim_padded, dtype=float), dtype=x.dtype, device=x.device)
|
||||
out = F.linear(x, H)
|
||||
out = out * scale
|
||||
return out[..., :dim].reshape(*x_shape)
|
||||
|
||||
|
||||
def hadamard_transform_mn_ref(x, multiple, scale=1.0):
|
||||
"""Reference impl for the M×N hadamard variants (_12n, _20n, _28n, _40n).
|
||||
|
||||
The kernel computes (H_M ⊗ H_N) · x via two steps:
|
||||
1) H_N (power-of-2 Hadamard) along the N dimension
|
||||
2) H_M (special ±1 matrix) along the M dimension
|
||||
where dim = M * N, M = `multiple`, N = power of 2.
|
||||
"""
|
||||
x_shape = x.shape
|
||||
dim = x.shape[-1]
|
||||
x = x.reshape(-1, dim)
|
||||
|
||||
# The kernel requires dim % (4*M) == 0 (for vectorized memory access).
|
||||
# See python/sglang/jit_kernel/hadamard.py: pad_multiple = 4 * 12 / 4 * 20 / etc.
|
||||
pad_multiple = 4 * multiple
|
||||
if dim % pad_multiple != 0:
|
||||
pad_size = pad_multiple - dim % pad_multiple
|
||||
x = F.pad(x, (0, pad_size))
|
||||
dim_padded = dim + pad_size
|
||||
else:
|
||||
dim_padded = dim
|
||||
|
||||
# N = dim_padded / M, must be a power of 2
|
||||
n = dim_padded // multiple
|
||||
log_n = int(math.log2(n))
|
||||
assert 2**log_n == n, f"n={n} is not a power of 2"
|
||||
|
||||
batch = x.shape[0]
|
||||
x = x.reshape(batch, multiple, n) # (batch, M, N)
|
||||
|
||||
# Step 1: apply H_N (standard power-of-2 Hadamard) along the N dimension
|
||||
H_n = torch.tensor(hadamard(n, dtype=float), dtype=x.dtype, device=x.device)
|
||||
x = torch.einsum("bmn,kn->bmk", x, H_n)
|
||||
|
||||
# Step 2: apply H_M (special ±1 matrix) along the M dimension
|
||||
H_m = torch.tensor(
|
||||
_SPECIAL_MATRICES[multiple].astype(float), dtype=x.dtype, device=x.device
|
||||
)
|
||||
x = torch.einsum("bmn,km->bkn", x, H_m)
|
||||
|
||||
x = x.reshape(batch, -1) * scale
|
||||
return x[..., : x_shape[-1]].reshape(*x_shape)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize(
|
||||
"dim",
|
||||
# Power-of-2 dims from sgl-kernel/tests/test_hadamard.py (old AOT test)
|
||||
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768],
|
||||
)
|
||||
def test_hadamard_transform(dim, dtype):
|
||||
device = "cuda"
|
||||
|
||||
# Tolerances from sgl-kernel/tests/test_hadamard.py (old AOT test)
|
||||
if dtype == torch.float32:
|
||||
rtol, atol = 3e-4, 3e-3
|
||||
elif dtype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
else: # float16
|
||||
rtol, atol = 3e-3, 5e-3
|
||||
|
||||
torch.random.manual_seed(0)
|
||||
batch_size = 15
|
||||
|
||||
x = torch.randn(batch_size, dim, device=device, dtype=dtype)
|
||||
scale = 1.0 / math.sqrt(dim)
|
||||
|
||||
out = hadamard_transform(x, scale=scale)
|
||||
# Compute reference in float32 from a detached copy to avoid precision loss
|
||||
out_ref = hadamard_transform_ref(x.detach().clone().float(), scale=scale)
|
||||
|
||||
torch.testing.assert_close(out.float(), out_ref, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize(
|
||||
"dim",
|
||||
# Non-power-of-2 dims to test the padding path
|
||||
# (137 from sgl-kernel/tests/test_hadamard.py, 500/1000 added for coverage)
|
||||
[137, 500, 1000],
|
||||
)
|
||||
def test_hadamard_transform_non_power_of_two(dim, dtype):
|
||||
device = "cuda"
|
||||
|
||||
if dtype == torch.float32:
|
||||
rtol, atol = 3e-4, 3e-3
|
||||
elif dtype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
else:
|
||||
rtol, atol = 3e-3, 5e-3
|
||||
|
||||
torch.random.manual_seed(42)
|
||||
batch_size = 15
|
||||
|
||||
x = torch.randn(batch_size, dim, device=device, dtype=dtype)
|
||||
scale = 1.0 / math.sqrt(dim)
|
||||
|
||||
out = hadamard_transform(x, scale=scale)
|
||||
out_ref = hadamard_transform_ref(x.detach().clone().float(), scale=scale)
|
||||
|
||||
torch.testing.assert_close(out.float(), out_ref, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
def test_hadamard_transform_3d_input(dtype):
|
||||
device = "cuda"
|
||||
|
||||
if dtype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
else:
|
||||
rtol, atol = 3e-3, 5e-3
|
||||
|
||||
torch.random.manual_seed(0)
|
||||
|
||||
x = torch.randn(4, 8, 256, device=device, dtype=dtype)
|
||||
scale = 1.0 / math.sqrt(256)
|
||||
|
||||
out = hadamard_transform(x, scale=scale)
|
||||
assert out.shape == x.shape
|
||||
|
||||
out_ref = hadamard_transform_ref(x.detach().clone().float(), scale=scale)
|
||||
torch.testing.assert_close(out.float(), out_ref, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
def test_hadamard_transform_scale_one(dtype):
|
||||
device = "cuda"
|
||||
|
||||
if dtype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
else:
|
||||
rtol, atol = 3e-3, 5e-3
|
||||
|
||||
torch.random.manual_seed(0)
|
||||
|
||||
x = torch.randn(8, 64, device=device, dtype=dtype)
|
||||
|
||||
out = hadamard_transform(x, scale=1.0)
|
||||
out_ref = hadamard_transform_ref(x.detach().clone().float(), scale=1.0)
|
||||
|
||||
torch.testing.assert_close(out.float(), out_ref, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
# Test dimensions for M×N variants: dim = M * N where N = 2^k.
|
||||
# M = 12/20/28/40 are the non-power-of-2 Hadamard sizes registered in
|
||||
# python/sglang/jit_kernel/hadamard.py (Hadamard12NKernel, ..., Hadamard40NKernel).
|
||||
# range(2,9) gives N = 4,8,...,256 so dims cover a practical range.
|
||||
_12N_DIMS = [12 * (2**k) for k in range(2, 9)] # 48, 96, ... , 3072
|
||||
_20N_DIMS = [20 * (2**k) for k in range(2, 9)] # 80, 160, ... , 5120
|
||||
_28N_DIMS = [28 * (2**k) for k in range(2, 9)] # 112, 224, ... , 7168
|
||||
_40N_DIMS = [40 * (2**k) for k in range(2, 9)] # 160, 320, ... , 10240
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize("dim", _12N_DIMS)
|
||||
def test_hadamard_transform_12n(dim, dtype):
|
||||
device = "cuda"
|
||||
|
||||
if dtype == torch.float32:
|
||||
rtol, atol = 3e-4, 3e-3
|
||||
elif dtype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
else:
|
||||
rtol, atol = 3e-3, 5e-3
|
||||
|
||||
torch.random.manual_seed(0)
|
||||
batch_size = 15
|
||||
|
||||
x = torch.randn(batch_size, dim, device=device, dtype=dtype)
|
||||
scale = 1.0 / math.sqrt(dim)
|
||||
|
||||
out = hadamard_transform_12n(x, scale=scale)
|
||||
out_ref = hadamard_transform_mn_ref(x.detach().clone().float(), 12, scale=scale)
|
||||
|
||||
torch.testing.assert_close(out.float(), out_ref, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize("dim", _20N_DIMS)
|
||||
def test_hadamard_transform_20n(dim, dtype):
|
||||
device = "cuda"
|
||||
|
||||
if dtype == torch.float32:
|
||||
rtol, atol = 3e-4, 3e-3
|
||||
elif dtype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
else:
|
||||
rtol, atol = 3e-3, 5e-3
|
||||
|
||||
torch.random.manual_seed(0)
|
||||
batch_size = 15
|
||||
|
||||
x = torch.randn(batch_size, dim, device=device, dtype=dtype)
|
||||
scale = 1.0 / math.sqrt(dim)
|
||||
|
||||
out = hadamard_transform_20n(x, scale=scale)
|
||||
out_ref = hadamard_transform_mn_ref(x.detach().clone().float(), 20, scale=scale)
|
||||
|
||||
torch.testing.assert_close(out.float(), out_ref, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize("dim", _28N_DIMS)
|
||||
def test_hadamard_transform_28n(dim, dtype):
|
||||
device = "cuda"
|
||||
|
||||
if dtype == torch.float32:
|
||||
rtol, atol = 3e-4, 3e-3
|
||||
elif dtype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
else:
|
||||
rtol, atol = 3e-3, 5e-3
|
||||
|
||||
torch.random.manual_seed(0)
|
||||
batch_size = 15
|
||||
|
||||
x = torch.randn(batch_size, dim, device=device, dtype=dtype)
|
||||
scale = 1.0 / math.sqrt(dim)
|
||||
|
||||
out = hadamard_transform_28n(x, scale=scale)
|
||||
out_ref = hadamard_transform_mn_ref(x.detach().clone().float(), 28, scale=scale)
|
||||
|
||||
torch.testing.assert_close(out.float(), out_ref, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize("dim", _40N_DIMS)
|
||||
def test_hadamard_transform_40n(dim, dtype):
|
||||
device = "cuda"
|
||||
|
||||
if dtype == torch.float32:
|
||||
rtol, atol = 3e-4, 3e-3
|
||||
elif dtype == torch.bfloat16:
|
||||
rtol, atol = 1e-2, 5e-2
|
||||
else:
|
||||
rtol, atol = 3e-3, 5e-3
|
||||
|
||||
torch.random.manual_seed(0)
|
||||
batch_size = 15
|
||||
|
||||
x = torch.randn(batch_size, dim, device=device, dtype=dtype)
|
||||
scale = 1.0 / math.sqrt(dim)
|
||||
|
||||
out = hadamard_transform_40n(x, scale=scale)
|
||||
out_ref = hadamard_transform_mn_ref(x.detach().clone().float(), 40, scale=scale)
|
||||
|
||||
torch.testing.assert_close(out.float(), out_ref, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__]))
|
||||
@@ -1,247 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.memory_pool import MHATokenToKVPool, MLATokenToKVPool
|
||||
from sglang.srt.mem_cache.memory_pool_host import (
|
||||
ALLOC_MEMORY_FUNCS,
|
||||
MHATokenToKVPoolHost,
|
||||
MLATokenToKVPoolHost,
|
||||
alloc_with_pin_memory,
|
||||
)
|
||||
from sglang.srt.utils import is_cuda, is_hip, is_npu, is_xpu
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=10, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not torch.cuda.is_available()
|
||||
or is_npu()
|
||||
or is_xpu()
|
||||
or not (is_cuda() or is_hip()),
|
||||
reason="HiCache JIT tests require CUDA/ROCm.",
|
||||
)
|
||||
|
||||
DEVICE = "cuda"
|
||||
PAGE_SIZE = 1 if is_hip() else 16
|
||||
NUM_LAYERS = 2
|
||||
POOL_SIZE = PAGE_SIZE * 8
|
||||
MHA_ELEMENT_DIMS = [128, 256, 512, 1024]
|
||||
MLA_ELEMENT_DIMS = [576]
|
||||
LAYOUTS = ["layer_first", "page_first"]
|
||||
|
||||
|
||||
def _token_indices_for_pages(
|
||||
pages: torch.Tensor, page_size: int = PAGE_SIZE, device: str = DEVICE
|
||||
) -> torch.Tensor:
|
||||
parts = [
|
||||
torch.arange(
|
||||
int(page) * page_size,
|
||||
(int(page) + 1) * page_size,
|
||||
device=device,
|
||||
dtype=torch.int64,
|
||||
)
|
||||
for page in pages.tolist()
|
||||
]
|
||||
return torch.cat(parts, dim=0)
|
||||
|
||||
|
||||
def _pinned_host_pool(host_pool_cls, **kwargs):
|
||||
original_alloc = ALLOC_MEMORY_FUNCS[DEVICE]
|
||||
ALLOC_MEMORY_FUNCS[DEVICE] = alloc_with_pin_memory
|
||||
try:
|
||||
return host_pool_cls(
|
||||
host_to_device_ratio=2.0,
|
||||
host_size=0,
|
||||
page_size=PAGE_SIZE,
|
||||
pin_memory=True,
|
||||
device="cpu",
|
||||
**kwargs,
|
||||
)
|
||||
finally:
|
||||
ALLOC_MEMORY_FUNCS[DEVICE] = original_alloc
|
||||
|
||||
|
||||
def _copy_tensor_with_offset(tensor: torch.Tensor, offset: int) -> None:
|
||||
data = torch.arange(
|
||||
tensor.numel(), device=tensor.device, dtype=tensor.dtype
|
||||
).view_as(tensor)
|
||||
tensor.copy_(data + offset)
|
||||
|
||||
|
||||
def _run_transfer_roundtrip_mha(layout: str, element_dim: int) -> None:
|
||||
device_pool = MHATokenToKVPool(
|
||||
size=POOL_SIZE,
|
||||
page_size=PAGE_SIZE,
|
||||
head_num=element_dim // 128,
|
||||
head_dim=128,
|
||||
dtype=torch.bfloat16,
|
||||
layer_num=NUM_LAYERS,
|
||||
device=DEVICE,
|
||||
enable_memory_saver=False,
|
||||
)
|
||||
host_pool = _pinned_host_pool(
|
||||
MHATokenToKVPoolHost,
|
||||
device_pool=device_pool,
|
||||
layout=layout,
|
||||
)
|
||||
assert (
|
||||
host_pool.can_use_jit
|
||||
), f"Expected JIT HiCache kernel for MHA dim={element_dim}"
|
||||
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
_copy_tensor_with_offset(device_pool.k_buffer[layer_id], layer_id)
|
||||
_copy_tensor_with_offset(device_pool.v_buffer[layer_id], layer_id + 100)
|
||||
|
||||
device_pages = torch.tensor([1, 2, 3], device=DEVICE, dtype=torch.int64)
|
||||
host_pages = torch.tensor([0, 1, 2], device=DEVICE, dtype=torch.int64)
|
||||
device_indices = _token_indices_for_pages(device_pages)
|
||||
host_indices = _token_indices_for_pages(host_pages)
|
||||
|
||||
host_pool.backup_from_device_all_layer(
|
||||
device_pool, host_indices, device_indices, "kernel"
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
for host_page, device_page in zip(host_pages.tolist(), device_pages.tolist()):
|
||||
host_start = host_page * PAGE_SIZE
|
||||
device_start = device_page * PAGE_SIZE
|
||||
assert torch.equal(
|
||||
host_pool.k_data_refs[layer_id][
|
||||
host_start : host_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
device_pool.k_buffer[layer_id][
|
||||
device_start : device_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
)
|
||||
assert torch.equal(
|
||||
host_pool.v_data_refs[layer_id][
|
||||
host_start : host_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
device_pool.v_buffer[layer_id][
|
||||
device_start : device_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
)
|
||||
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
device_pool.k_buffer[layer_id].zero_()
|
||||
device_pool.v_buffer[layer_id].zero_()
|
||||
|
||||
load_pages = torch.tensor([4, 5, 6], device=DEVICE, dtype=torch.int64)
|
||||
load_indices = _token_indices_for_pages(load_pages)
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
host_pool.load_to_device_per_layer(
|
||||
device_pool, host_indices, load_indices, layer_id, "kernel"
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
for host_page, device_page in zip(host_pages.tolist(), load_pages.tolist()):
|
||||
host_start = host_page * PAGE_SIZE
|
||||
device_start = device_page * PAGE_SIZE
|
||||
assert torch.equal(
|
||||
device_pool.k_buffer[layer_id][
|
||||
device_start : device_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
host_pool.k_data_refs[layer_id][
|
||||
host_start : host_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
)
|
||||
assert torch.equal(
|
||||
device_pool.v_buffer[layer_id][
|
||||
device_start : device_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
host_pool.v_data_refs[layer_id][
|
||||
host_start : host_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
)
|
||||
|
||||
|
||||
def _run_transfer_roundtrip_mla(layout: str, element_dim: int) -> None:
|
||||
device_pool = MLATokenToKVPool(
|
||||
size=POOL_SIZE,
|
||||
page_size=PAGE_SIZE,
|
||||
kv_lora_rank=element_dim - 64,
|
||||
qk_rope_head_dim=64,
|
||||
dtype=torch.bfloat16,
|
||||
layer_num=NUM_LAYERS,
|
||||
device=DEVICE,
|
||||
enable_memory_saver=False,
|
||||
)
|
||||
host_pool = _pinned_host_pool(
|
||||
MLATokenToKVPoolHost,
|
||||
device_pool=device_pool,
|
||||
layout=layout,
|
||||
)
|
||||
assert (
|
||||
host_pool.can_use_jit
|
||||
), f"Expected JIT HiCache kernel for MLA dim={element_dim}"
|
||||
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
_copy_tensor_with_offset(device_pool.kv_buffer[layer_id], layer_id)
|
||||
|
||||
device_pages = torch.tensor([1, 2, 3], device=DEVICE, dtype=torch.int64)
|
||||
host_pages = torch.tensor([0, 1, 2], device=DEVICE, dtype=torch.int64)
|
||||
device_indices = _token_indices_for_pages(device_pages)
|
||||
host_indices = _token_indices_for_pages(host_pages)
|
||||
|
||||
host_pool.backup_from_device_all_layer(
|
||||
device_pool, host_indices, device_indices, "kernel"
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
for host_page, device_page in zip(host_pages.tolist(), device_pages.tolist()):
|
||||
host_start = host_page * PAGE_SIZE
|
||||
device_start = device_page * PAGE_SIZE
|
||||
assert torch.equal(
|
||||
host_pool.data_refs[layer_id][
|
||||
host_start : host_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
device_pool.kv_buffer[layer_id][
|
||||
device_start : device_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
)
|
||||
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
device_pool.kv_buffer[layer_id].zero_()
|
||||
|
||||
load_pages = torch.tensor([4, 5, 6], device=DEVICE, dtype=torch.int64)
|
||||
load_indices = _token_indices_for_pages(load_pages)
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
host_pool.load_to_device_per_layer(
|
||||
device_pool, host_indices, load_indices, layer_id, "kernel"
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
for layer_id in range(NUM_LAYERS):
|
||||
for host_page, device_page in zip(host_pages.tolist(), load_pages.tolist()):
|
||||
host_start = host_page * PAGE_SIZE
|
||||
device_start = device_page * PAGE_SIZE
|
||||
assert torch.equal(
|
||||
device_pool.kv_buffer[layer_id][
|
||||
device_start : device_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
host_pool.data_refs[layer_id][
|
||||
host_start : host_start + PAGE_SIZE
|
||||
].cpu(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", LAYOUTS)
|
||||
@pytest.mark.parametrize("element_dim", MHA_ELEMENT_DIMS)
|
||||
def test_hicache_transfer_mha(layout: str, element_dim: int) -> None:
|
||||
_run_transfer_roundtrip_mha(layout, element_dim)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", LAYOUTS)
|
||||
@pytest.mark.parametrize("element_dim", MLA_ELEMENT_DIMS)
|
||||
def test_hicache_transfer_mla(layout: str, element_dim: int) -> None:
|
||||
_run_transfer_roundtrip_mla(layout, element_dim)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,426 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.hisparse import (
|
||||
load_cache_to_device_buffer_dsv4_mla,
|
||||
load_cache_to_device_buffer_mla,
|
||||
transfer_cache_dsv4_mla,
|
||||
)
|
||||
from sglang.srt.utils import is_cuda, is_hip, is_npu, is_xpu
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=10, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not torch.cuda.is_available()
|
||||
or is_npu()
|
||||
or is_xpu()
|
||||
or not (is_cuda() or is_hip()),
|
||||
reason="HiSparse JIT tests require CUDA/ROCm.",
|
||||
)
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.float32
|
||||
KV_DIM = 8
|
||||
HOT_BUFFER_SIZE = 4
|
||||
PADDED_BUFFER_SIZE = HOT_BUFFER_SIZE + 1
|
||||
HOST_CACHE_SIZE = 16
|
||||
DEVICE_CACHE_SIZE = 16
|
||||
ITEM_SIZE_BYTES = KV_DIM * torch.empty((), dtype=DTYPE).element_size()
|
||||
DSV4_PAGE_SIZE = 64
|
||||
DSV4_VALUE_BYTES = 576
|
||||
DSV4_SCALE_BYTES = 8
|
||||
DSV4_ITEM_BYTES = DSV4_VALUE_BYTES + DSV4_SCALE_BYTES
|
||||
DSV4_PAGE_BYTES = ((DSV4_ITEM_BYTES * DSV4_PAGE_SIZE + 575) // 576) * 576
|
||||
DSV4_SCALE_OFFSET = DSV4_VALUE_BYTES * DSV4_PAGE_SIZE
|
||||
|
||||
|
||||
def _host_cache() -> torch.Tensor:
|
||||
host_cache = torch.empty(
|
||||
(HOST_CACHE_SIZE, 1, KV_DIM), dtype=DTYPE, device="cpu", pin_memory=True
|
||||
)
|
||||
host_cache.copy_(torch.arange(host_cache.numel(), dtype=DTYPE).view_as(host_cache))
|
||||
return host_cache
|
||||
|
||||
|
||||
def _dsv4_token_pattern(seed: int) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
value = (
|
||||
(torch.arange(DSV4_VALUE_BYTES, dtype=torch.int16) + seed)
|
||||
.remainder(256)
|
||||
.to(torch.uint8)
|
||||
)
|
||||
scale = (
|
||||
(torch.arange(DSV4_SCALE_BYTES, dtype=torch.int16) + seed + 17)
|
||||
.remainder(256)
|
||||
.to(torch.uint8)
|
||||
)
|
||||
return value, scale
|
||||
|
||||
|
||||
def _write_dsv4_token(cache: torch.Tensor, loc: int, seed: int) -> None:
|
||||
page = loc // DSV4_PAGE_SIZE
|
||||
offset = loc % DSV4_PAGE_SIZE
|
||||
value, scale = _dsv4_token_pattern(seed)
|
||||
cache[page, offset * DSV4_VALUE_BYTES : (offset + 1) * DSV4_VALUE_BYTES].copy_(
|
||||
value.to(cache.device)
|
||||
)
|
||||
scale_start = DSV4_SCALE_OFFSET + offset * DSV4_SCALE_BYTES
|
||||
cache[page, scale_start : scale_start + DSV4_SCALE_BYTES].copy_(
|
||||
scale.to(cache.device)
|
||||
)
|
||||
|
||||
|
||||
def _read_dsv4_token(cache: torch.Tensor, loc: int) -> torch.Tensor:
|
||||
page = loc // DSV4_PAGE_SIZE
|
||||
offset = loc % DSV4_PAGE_SIZE
|
||||
value = cache[page, offset * DSV4_VALUE_BYTES : (offset + 1) * DSV4_VALUE_BYTES]
|
||||
scale_start = DSV4_SCALE_OFFSET + offset * DSV4_SCALE_BYTES
|
||||
scale = cache[page, scale_start : scale_start + DSV4_SCALE_BYTES]
|
||||
return torch.cat([value, scale])
|
||||
|
||||
|
||||
def _dsv4_ptrs(cache: torch.Tensor) -> torch.Tensor:
|
||||
return torch.tensor([cache.data_ptr()], dtype=torch.uint64, device=DEVICE)
|
||||
|
||||
|
||||
def _run_kernel(
|
||||
*,
|
||||
top_k_tokens: torch.Tensor,
|
||||
device_buffer_tokens: torch.Tensor,
|
||||
host_cache_locs: torch.Tensor,
|
||||
device_buffer_locs: torch.Tensor,
|
||||
host_cache: torch.Tensor,
|
||||
device_buffer: torch.Tensor,
|
||||
lru_slots: torch.Tensor,
|
||||
seq_len: int | None = None,
|
||||
seq_lens: torch.Tensor | None = None,
|
||||
seq_lens_dtype: torch.dtype = torch.int32,
|
||||
req_pool_indices: torch.Tensor | None = None,
|
||||
num_real_reqs: int | None = None,
|
||||
) -> torch.Tensor:
|
||||
batch_size = top_k_tokens.shape[0]
|
||||
if req_pool_indices is None:
|
||||
req_pool_indices = torch.arange(batch_size, dtype=torch.int64, device=DEVICE)
|
||||
if seq_lens is None:
|
||||
seq_lens = torch.full(
|
||||
(batch_size,), seq_len, dtype=seq_lens_dtype, device=DEVICE
|
||||
)
|
||||
if num_real_reqs is None:
|
||||
num_real_reqs = batch_size
|
||||
|
||||
out = torch.full_like(top_k_tokens, -1)
|
||||
load_cache_to_device_buffer_mla(
|
||||
top_k_tokens=top_k_tokens,
|
||||
device_buffer_tokens=device_buffer_tokens,
|
||||
host_cache_locs=host_cache_locs,
|
||||
device_buffer_locs=device_buffer_locs,
|
||||
host_cache=host_cache,
|
||||
device_buffer=device_buffer,
|
||||
top_k_device_locs=out,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
lru_slots=lru_slots,
|
||||
item_size_bytes=ITEM_SIZE_BYTES,
|
||||
num_top_k=top_k_tokens.shape[1],
|
||||
hot_buffer_size=HOT_BUFFER_SIZE,
|
||||
page_size=1,
|
||||
block_size=256,
|
||||
num_real_reqs=torch.tensor([num_real_reqs], dtype=torch.int32, device=DEVICE),
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
return out
|
||||
|
||||
|
||||
def _make_state(
|
||||
device_buffer_locs_rows: list[list[int]],
|
||||
device_buffer_tokens_rows: list[list[int]],
|
||||
newest_tokens: list[int],
|
||||
):
|
||||
host_cache = _host_cache()
|
||||
device_buffer = torch.full(
|
||||
(DEVICE_CACHE_SIZE, 1, KV_DIM), -1, dtype=DTYPE, device=DEVICE
|
||||
)
|
||||
device_buffer_locs = torch.tensor(
|
||||
device_buffer_locs_rows, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
device_buffer_tokens = torch.tensor(
|
||||
device_buffer_tokens_rows, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
lru_slots = (
|
||||
torch.arange(HOT_BUFFER_SIZE, dtype=torch.int16, device=DEVICE)
|
||||
.view(1, -1)
|
||||
.repeat(device_buffer_locs.shape[0], 1)
|
||||
)
|
||||
host_cache_locs = (
|
||||
torch.arange(HOST_CACHE_SIZE, dtype=torch.int64, device=DEVICE)
|
||||
.view(1, -1)
|
||||
.repeat(device_buffer_locs.shape[0], 1)
|
||||
)
|
||||
|
||||
# Slots 0..3 participate in LRU; slot 4 is the reserved newest slot.
|
||||
for rid, newest_token in enumerate(newest_tokens):
|
||||
for slot, token in enumerate(device_buffer_tokens_rows[rid][:HOT_BUFFER_SIZE]):
|
||||
if token >= 0:
|
||||
device_buffer[device_buffer_locs[rid, slot]].copy_(
|
||||
host_cache[token].to(DEVICE, non_blocking=True)
|
||||
)
|
||||
device_buffer[device_buffer_locs[rid, HOT_BUFFER_SIZE]].copy_(
|
||||
host_cache[newest_token].to(DEVICE, non_blocking=True)
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
return {
|
||||
"host_cache": host_cache,
|
||||
"device_buffer": device_buffer,
|
||||
"device_buffer_locs": device_buffer_locs,
|
||||
"device_buffer_tokens": device_buffer_tokens,
|
||||
"lru_slots": lru_slots,
|
||||
"host_cache_locs": host_cache_locs,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_hip(), reason="DSV4 paged-layout HiSparse test is CUDA-only.")
|
||||
def test_transfer_cache_dsv4_mla_copies_paged_token() -> None:
|
||||
src_cache = torch.zeros((2, DSV4_PAGE_BYTES), dtype=torch.uint8, device=DEVICE)
|
||||
dst_cache = torch.zeros(
|
||||
(2, DSV4_PAGE_BYTES), dtype=torch.uint8, device="cpu", pin_memory=True
|
||||
)
|
||||
src_loc = DSV4_PAGE_SIZE + 6
|
||||
dst_loc = DSV4_PAGE_SIZE + 1
|
||||
_write_dsv4_token(src_cache, src_loc, seed=41)
|
||||
|
||||
transfer_cache_dsv4_mla(
|
||||
src_ptrs=_dsv4_ptrs(src_cache),
|
||||
dst_ptrs=_dsv4_ptrs(dst_cache),
|
||||
src_indices=torch.tensor([src_loc], dtype=torch.int64, device=DEVICE),
|
||||
dst_indices=torch.tensor([dst_loc], dtype=torch.int64, device=DEVICE),
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
assert torch.equal(
|
||||
_read_dsv4_token(dst_cache, dst_loc).to(DEVICE),
|
||||
_read_dsv4_token(src_cache, src_loc),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_hip(), reason="DSV4 paged-layout HiSparse test is CUDA-only.")
|
||||
def test_dsv4_swap_in_reads_paged_host_layout() -> None:
|
||||
host_cache = torch.zeros(
|
||||
(2, DSV4_PAGE_BYTES), dtype=torch.uint8, device="cpu", pin_memory=True
|
||||
)
|
||||
device_buffer = torch.zeros((2, DSV4_PAGE_BYTES), dtype=torch.uint8, device=DEVICE)
|
||||
host_loc = DSV4_PAGE_SIZE + 1
|
||||
swap_loc = DSV4_PAGE_SIZE + 12
|
||||
_write_dsv4_token(host_cache, host_loc, seed=41)
|
||||
|
||||
top_k_tokens = torch.tensor([[3]], dtype=torch.int32, device=DEVICE)
|
||||
device_buffer_tokens = torch.full(
|
||||
(1, PADDED_BUFFER_SIZE), -1, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
host_cache_locs = torch.zeros((1, 8), dtype=torch.int64, device=DEVICE)
|
||||
host_cache_locs[0, 3] = host_loc
|
||||
device_buffer_locs = torch.tensor(
|
||||
[[swap_loc, swap_loc + 1, swap_loc + 2, swap_loc + 3, swap_loc + 4]],
|
||||
dtype=torch.int32,
|
||||
device=DEVICE,
|
||||
)
|
||||
lru_slots = torch.arange(HOT_BUFFER_SIZE, dtype=torch.int16, device=DEVICE).view(
|
||||
1, -1
|
||||
)
|
||||
out = torch.full_like(top_k_tokens, -1)
|
||||
|
||||
load_cache_to_device_buffer_dsv4_mla(
|
||||
top_k_tokens=top_k_tokens,
|
||||
device_buffer_tokens=device_buffer_tokens,
|
||||
host_cache_locs=host_cache_locs,
|
||||
device_buffer_locs=device_buffer_locs,
|
||||
host_cache=host_cache,
|
||||
device_buffer=device_buffer,
|
||||
top_k_device_locs=out,
|
||||
req_pool_indices=torch.tensor([0], dtype=torch.int64, device=DEVICE),
|
||||
seq_lens=torch.tensor([8], dtype=torch.int32, device=DEVICE),
|
||||
lru_slots=lru_slots,
|
||||
item_size_bytes=DSV4_ITEM_BYTES,
|
||||
num_top_k=1,
|
||||
hot_buffer_size=HOT_BUFFER_SIZE,
|
||||
page_size=1,
|
||||
block_size=256,
|
||||
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
assert out.item() == swap_loc
|
||||
assert torch.equal(
|
||||
_read_dsv4_token(device_buffer, swap_loc),
|
||||
_read_dsv4_token(host_cache, host_loc).to(DEVICE),
|
||||
)
|
||||
|
||||
|
||||
def _long_case():
|
||||
# One-request baseline used by the stateful cases below:
|
||||
# req 0 LRU slots : [0, 1, 2, 3]
|
||||
# req 0 cached tokens : slot0->1, slot1->4, slot2->2, slot3->5
|
||||
# req 0 physical locs : slot0->9, slot1->7, slot2->3, slot3->5
|
||||
# req 0 newest slot : slot4/newest -> token 7 at physical loc 11
|
||||
return _make_state([[9, 7, 3, 5, 11]], [[1, 4, 2, 5, -1]], [7])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seq_lens_dtype", [torch.int32, torch.int64])
|
||||
def test_load_cache_to_device_buffer_fast_path(seq_lens_dtype: torch.dtype) -> None:
|
||||
host_cache = _host_cache()
|
||||
device_buffer = torch.arange(
|
||||
DEVICE_CACHE_SIZE * KV_DIM, dtype=DTYPE, device=DEVICE
|
||||
).view(DEVICE_CACHE_SIZE, 1, KV_DIM)
|
||||
device_buffer_before = device_buffer.clone()
|
||||
device_buffer_locs = torch.tensor(
|
||||
[[13, 9, 5, 1, 15]], dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
device_buffer_tokens = torch.tensor(
|
||||
[[10, 11, 12, 13, -1]], dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
device_buffer_tokens_before = device_buffer_tokens.clone()
|
||||
lru_slots = torch.tensor([[0, 1, 2, 3]], dtype=torch.int16, device=DEVICE)
|
||||
lru_slots_before = lru_slots.clone()
|
||||
|
||||
# Short-sequence layout:
|
||||
# token position 0 -> physical loc 13
|
||||
# token position 1 -> physical loc 9
|
||||
# token position 2 -> physical loc 5
|
||||
#
|
||||
# seq_len <= HOT_BUFFER_SIZE should skip host loads and LRU mutations,
|
||||
# so top_k_tokens acts like direct indexing into device_buffer_locs.
|
||||
out = _run_kernel(
|
||||
top_k_tokens=torch.tensor([[2, 0, 1]], dtype=torch.int32, device=DEVICE),
|
||||
device_buffer_tokens=device_buffer_tokens,
|
||||
host_cache_locs=torch.arange(
|
||||
HOST_CACHE_SIZE, dtype=torch.int64, device=DEVICE
|
||||
).view(1, -1),
|
||||
device_buffer_locs=device_buffer_locs,
|
||||
host_cache=host_cache,
|
||||
device_buffer=device_buffer,
|
||||
lru_slots=lru_slots,
|
||||
seq_len=3,
|
||||
seq_lens_dtype=seq_lens_dtype,
|
||||
)
|
||||
|
||||
assert torch.equal(out.cpu(), torch.tensor([[5, 13, 9]], dtype=torch.int32))
|
||||
assert torch.equal(device_buffer_tokens.cpu(), device_buffer_tokens_before.cpu())
|
||||
assert torch.equal(lru_slots.cpu(), lru_slots_before.cpu())
|
||||
assert torch.equal(device_buffer.cpu(), device_buffer_before.cpu())
|
||||
|
||||
|
||||
def test_load_cache_to_device_buffer_hits_newest_and_updates_lru() -> None:
|
||||
state = _long_case()
|
||||
|
||||
# Query [4, 2, 7]:
|
||||
# 4 hits slot1 -> loc 7
|
||||
# 2 hits slot2 -> loc 3
|
||||
# 7 is the newest token -> reserved newest loc 11
|
||||
#
|
||||
# Hits move to the MRU tail, so [0, 1, 2, 3] becomes [0, 3, 1, 2].
|
||||
out = _run_kernel(
|
||||
top_k_tokens=torch.tensor([[4, 2, 7]], dtype=torch.int32, device=DEVICE),
|
||||
seq_len=8,
|
||||
**state,
|
||||
)
|
||||
|
||||
assert torch.equal(out.cpu(), torch.tensor([[7, 3, 11]], dtype=torch.int32))
|
||||
assert torch.equal(
|
||||
state["device_buffer_tokens"].cpu(),
|
||||
torch.tensor([[1, 4, 2, 5, -1]], dtype=torch.int32),
|
||||
)
|
||||
assert torch.equal(
|
||||
state["lru_slots"].cpu(), torch.tensor([[0, 3, 1, 2]], dtype=torch.int16)
|
||||
)
|
||||
|
||||
|
||||
def test_load_cache_to_device_buffer_miss_uses_updated_lru_slot() -> None:
|
||||
state = _long_case()
|
||||
|
||||
# Step 1: touch tokens [4, 2], so LRU becomes [0, 3, 1, 2].
|
||||
# Step 2: query token 6, which is a miss.
|
||||
# The kernel should reuse the new LRU head slot0, whose physical loc is 9.
|
||||
# This round has no regular hits, so the freshly loaded miss slot ends up at the tail.
|
||||
_run_kernel(
|
||||
top_k_tokens=torch.tensor([[4, 2]], dtype=torch.int32, device=DEVICE),
|
||||
seq_len=8,
|
||||
**state,
|
||||
)
|
||||
out = _run_kernel(
|
||||
top_k_tokens=torch.tensor([[6]], dtype=torch.int32, device=DEVICE),
|
||||
seq_len=8,
|
||||
**state,
|
||||
)
|
||||
|
||||
assert torch.equal(out.cpu(), torch.tensor([[9]], dtype=torch.int32))
|
||||
assert torch.equal(
|
||||
state["device_buffer_tokens"].cpu(),
|
||||
torch.tensor([[6, 4, 2, 5, -1]], dtype=torch.int32),
|
||||
)
|
||||
assert torch.equal(
|
||||
state["lru_slots"].cpu(), torch.tensor([[3, 1, 2, 0]], dtype=torch.int16)
|
||||
)
|
||||
assert torch.equal(state["device_buffer"][9].cpu(), state["host_cache"][6])
|
||||
|
||||
|
||||
def test_load_cache_to_device_buffer_batched_with_padding() -> None:
|
||||
state = _make_state(
|
||||
[
|
||||
[9, 7, 3, 5, 11],
|
||||
[12, 10, 8, 6, 14],
|
||||
[15, 4, 2, 1, 13],
|
||||
],
|
||||
[
|
||||
[1, 4, 2, 5, -1],
|
||||
[0, 1, 2, 3, -1],
|
||||
[9, 8, 7, 6, -1],
|
||||
],
|
||||
[7, 4, 5],
|
||||
)
|
||||
padded_tokens_before = state["device_buffer_tokens"][2].clone()
|
||||
padded_lru_before = state["lru_slots"][2].clone()
|
||||
|
||||
# req 0: long path
|
||||
# cached tokens/locs : 1@9, 4@7, 2@3, 5@5, newest 7@11
|
||||
# query [4, 6, 7] : hit loc 7, miss into slot0/loc 9, newest loc 11
|
||||
# LRU update : remaining evictables [2, 3], then miss [0], then hit [1]
|
||||
# : [0, 1, 2, 3] -> [2, 3, 0, 1]
|
||||
#
|
||||
# req 1: fast path
|
||||
# seq_len = 3 <= HOT_BUFFER_SIZE, so [2, 1, 0] maps directly to locs [8, 10, 12]
|
||||
#
|
||||
# req 2: padded block
|
||||
# num_real_reqs = 2 means this row must be ignored entirely.
|
||||
out = _run_kernel(
|
||||
top_k_tokens=torch.tensor(
|
||||
[[4, 6, 7], [2, 1, 0], [9, 8, 7]], dtype=torch.int32, device=DEVICE
|
||||
),
|
||||
seq_lens=torch.tensor([8, 3, 8], dtype=torch.int32, device=DEVICE),
|
||||
num_real_reqs=2,
|
||||
**state,
|
||||
)
|
||||
|
||||
assert torch.equal(
|
||||
out.cpu(),
|
||||
torch.tensor([[7, 9, 11], [8, 10, 12], [-1, -1, -1]], dtype=torch.int32),
|
||||
)
|
||||
assert torch.equal(
|
||||
state["device_buffer_tokens"][:2].cpu(),
|
||||
torch.tensor([[6, 4, 2, 5, -1], [0, 1, 2, 3, -1]], dtype=torch.int32),
|
||||
)
|
||||
assert torch.equal(
|
||||
state["lru_slots"][:2].cpu(),
|
||||
torch.tensor([[2, 3, 0, 1], [0, 1, 2, 3]], dtype=torch.int16),
|
||||
)
|
||||
assert torch.equal(
|
||||
state["device_buffer_tokens"][2].cpu(), padded_tokens_before.cpu()
|
||||
)
|
||||
assert torch.equal(state["lru_slots"][2].cpu(), padded_lru_before.cpu())
|
||||
assert torch.equal(state["device_buffer"][9].cpu(), state["host_cache"][6])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,104 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.mla_kv_pack_quantize_fp8 import mla_kv_pack_quantize_fp8
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=60, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
DEVICE = "cuda"
|
||||
|
||||
SHAPES = get_ci_test_range(
|
||||
[(128, 64, 128), (64, 32, 64)],
|
||||
[(128, 64, 128)],
|
||||
)
|
||||
NUM_HEADS = get_ci_test_range([8, 16, 32, 64], [16, 32])
|
||||
BATCH_SIZES = get_ci_test_range(
|
||||
[1, 4, 17, 64, 257, 1024, 4096, 16384],
|
||||
[1, 64, 1024, 16384],
|
||||
)
|
||||
|
||||
|
||||
def _ref(k_nope, k_pe, v, k_scale_inv, v_scale_inv, fp8_dtype):
|
||||
s, h, qk_nope = k_nope.shape
|
||||
qk_rope = k_pe.shape[-1]
|
||||
v_head = v.shape[-1]
|
||||
if k_pe.dim() == 3:
|
||||
k_pe = k_pe.squeeze(1)
|
||||
|
||||
k_bf16 = torch.empty(
|
||||
(s, h, qk_nope + qk_rope), dtype=k_nope.dtype, device=k_nope.device
|
||||
)
|
||||
k_bf16[..., :qk_nope] = k_nope
|
||||
k_bf16[..., qk_nope:] = k_pe.unsqueeze(1).expand(-1, h, -1)
|
||||
|
||||
k_fp8 = (k_bf16.float() * k_scale_inv).to(fp8_dtype)
|
||||
v_fp8 = (v.float() * v_scale_inv).to(fp8_dtype)
|
||||
return k_fp8, v_fp8
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize("shape", SHAPES)
|
||||
@pytest.mark.parametrize("num_heads", NUM_HEADS)
|
||||
@pytest.mark.parametrize("batch_size", BATCH_SIZES)
|
||||
def test_correctness(dtype, shape, num_heads, batch_size):
|
||||
qk_nope, qk_rope, v_head = shape
|
||||
|
||||
torch.manual_seed(0)
|
||||
k_nope = torch.randn((batch_size, num_heads, qk_nope), dtype=dtype, device=DEVICE)
|
||||
k_pe = torch.randn((batch_size, 1, qk_rope), dtype=dtype, device=DEVICE)
|
||||
v = torch.randn((batch_size, num_heads, v_head), dtype=dtype, device=DEVICE)
|
||||
|
||||
k_scale_inv = 0.7
|
||||
v_scale_inv = 1.3
|
||||
|
||||
k_fp8, v_fp8 = mla_kv_pack_quantize_fp8(
|
||||
k_nope, k_pe, v, k_scale_inv=k_scale_inv, v_scale_inv=v_scale_inv
|
||||
)
|
||||
|
||||
k_ref, v_ref = _ref(k_nope, k_pe, v, k_scale_inv, v_scale_inv, torch.float8_e4m3fn)
|
||||
|
||||
torch.testing.assert_close(k_fp8.float(), k_ref.float(), rtol=1e-2, atol=0.5)
|
||||
torch.testing.assert_close(v_fp8.float(), v_ref.float(), rtol=1e-2, atol=0.5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
def test_strided_inputs(dtype):
|
||||
s, h = 16, 32
|
||||
qk_nope, qk_rope, v_head = 128, 64, 128
|
||||
|
||||
full = torch.randn(
|
||||
(s, h, qk_nope * 2), dtype=dtype, device=DEVICE, requires_grad=False
|
||||
)
|
||||
k_nope = full[..., qk_nope:]
|
||||
assert k_nope.stride(-1) == 1
|
||||
|
||||
k_pe = torch.randn((s, 1, qk_rope), dtype=dtype, device=DEVICE)
|
||||
v = torch.randn((s, h, v_head), dtype=dtype, device=DEVICE)
|
||||
|
||||
k_fp8, v_fp8 = mla_kv_pack_quantize_fp8(k_nope, k_pe, v)
|
||||
k_ref, v_ref = _ref(k_nope, k_pe, v, 1.0, 1.0, torch.float8_e4m3fn)
|
||||
torch.testing.assert_close(k_fp8.float(), k_ref.float(), rtol=1e-2, atol=0.5)
|
||||
torch.testing.assert_close(v_fp8.float(), v_ref.float(), rtol=1e-2, atol=0.5)
|
||||
|
||||
|
||||
def test_kpe_2d_accepted():
|
||||
s, h = 8, 16
|
||||
qk_nope, qk_rope, v_head = 128, 64, 128
|
||||
dtype = torch.bfloat16
|
||||
|
||||
k_nope = torch.randn((s, h, qk_nope), dtype=dtype, device=DEVICE)
|
||||
k_pe = torch.randn((s, qk_rope), dtype=dtype, device=DEVICE)
|
||||
v = torch.randn((s, h, v_head), dtype=dtype, device=DEVICE)
|
||||
|
||||
k_fp8, v_fp8 = mla_kv_pack_quantize_fp8(k_nope, k_pe, v)
|
||||
k_ref, v_ref = _ref(k_nope, k_pe.unsqueeze(1), v, 1.0, 1.0, torch.float8_e4m3fn)
|
||||
torch.testing.assert_close(k_fp8.float(), k_ref.float(), rtol=1e-2, atol=0.5)
|
||||
torch.testing.assert_close(v_fp8.float(), v_ref.float(), rtol=1e-2, atol=0.5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,349 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.jit_kernel.moe_align import moe_align_block_size
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=28, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def ceil_div(a, b):
|
||||
return (a + b - 1) // b
|
||||
|
||||
|
||||
@triton.jit
|
||||
def moe_align_block_size_stage1(
|
||||
topk_ids_ptr,
|
||||
tokens_cnts_ptr,
|
||||
num_experts: tl.constexpr,
|
||||
numel: tl.constexpr,
|
||||
tokens_per_thread: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(0)
|
||||
start_idx = pid * tokens_per_thread
|
||||
off_c = (pid + 1) * num_experts
|
||||
|
||||
for i in range(tokens_per_thread):
|
||||
if start_idx + i < numel:
|
||||
idx = tl.load(topk_ids_ptr + start_idx + i)
|
||||
token_cnt = tl.load(tokens_cnts_ptr + off_c + idx)
|
||||
tl.store(tokens_cnts_ptr + off_c + idx, token_cnt + 1)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def moe_align_block_size_stage2(
|
||||
tokens_cnts_ptr,
|
||||
num_experts: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(0)
|
||||
last_cnt = 0
|
||||
for i in range(1, num_experts + 1):
|
||||
token_cnt = tl.load(tokens_cnts_ptr + i * num_experts + pid)
|
||||
last_cnt = last_cnt + token_cnt
|
||||
tl.store(tokens_cnts_ptr + i * num_experts + pid, last_cnt)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def moe_align_block_size_stage3(
|
||||
total_tokens_post_pad_ptr,
|
||||
tokens_cnts_ptr,
|
||||
cumsum_ptr,
|
||||
num_experts: tl.constexpr,
|
||||
block_size: tl.constexpr,
|
||||
):
|
||||
last_cumsum = 0
|
||||
off_cnt = num_experts * num_experts
|
||||
for i in range(1, num_experts + 1):
|
||||
token_cnt = tl.load(tokens_cnts_ptr + off_cnt + i - 1)
|
||||
last_cumsum = last_cumsum + tl.cdiv(token_cnt, block_size) * block_size
|
||||
tl.store(cumsum_ptr + i, last_cumsum)
|
||||
tl.store(total_tokens_post_pad_ptr, last_cumsum)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def moe_align_block_size_stage4(
|
||||
topk_ids_ptr,
|
||||
sorted_token_ids_ptr,
|
||||
expert_ids_ptr,
|
||||
tokens_cnts_ptr,
|
||||
cumsum_ptr,
|
||||
num_experts: tl.constexpr,
|
||||
block_size: tl.constexpr,
|
||||
numel: tl.constexpr,
|
||||
tokens_per_thread: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(0)
|
||||
start_idx = tl.load(cumsum_ptr + pid)
|
||||
end_idx = tl.load(cumsum_ptr + pid + 1)
|
||||
|
||||
for i in range(start_idx, end_idx, block_size):
|
||||
tl.store(expert_ids_ptr + i // block_size, pid)
|
||||
|
||||
start_idx = pid * tokens_per_thread
|
||||
off_t = pid * num_experts
|
||||
|
||||
for i in range(start_idx, tl.minimum(start_idx + tokens_per_thread, numel)):
|
||||
expert_id = tl.load(topk_ids_ptr + i)
|
||||
token_cnt = tl.load(tokens_cnts_ptr + off_t + expert_id)
|
||||
rank_post_pad = token_cnt + tl.load(cumsum_ptr + expert_id)
|
||||
tl.store(sorted_token_ids_ptr + rank_post_pad, i)
|
||||
tl.store(tokens_cnts_ptr + off_t + expert_id, token_cnt + 1)
|
||||
|
||||
|
||||
def moe_align_block_size_triton(
|
||||
topk_ids: torch.Tensor,
|
||||
num_experts: int,
|
||||
block_size: int,
|
||||
sorted_token_ids: torch.Tensor,
|
||||
expert_ids: torch.Tensor,
|
||||
num_tokens_post_pad: torch.Tensor,
|
||||
) -> None:
|
||||
numel = topk_ids.numel()
|
||||
grid = (num_experts,)
|
||||
tokens_cnts = torch.zeros(
|
||||
(num_experts + 1, num_experts), dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
cumsum = torch.zeros((num_experts + 1,), dtype=torch.int32, device=topk_ids.device)
|
||||
tokens_per_thread = ceil_div(numel, num_experts)
|
||||
|
||||
moe_align_block_size_stage1[grid](
|
||||
topk_ids,
|
||||
tokens_cnts,
|
||||
num_experts,
|
||||
numel,
|
||||
tokens_per_thread,
|
||||
)
|
||||
moe_align_block_size_stage2[grid](
|
||||
tokens_cnts,
|
||||
num_experts,
|
||||
)
|
||||
moe_align_block_size_stage3[(1,)](
|
||||
num_tokens_post_pad,
|
||||
tokens_cnts,
|
||||
cumsum,
|
||||
num_experts,
|
||||
block_size,
|
||||
)
|
||||
moe_align_block_size_stage4[grid](
|
||||
topk_ids,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
tokens_cnts,
|
||||
cumsum,
|
||||
num_experts,
|
||||
block_size,
|
||||
numel,
|
||||
tokens_per_thread,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"block_size,num_tokens,topk,num_experts,pad_sorted_token_ids",
|
||||
list(
|
||||
itertools.product(
|
||||
[32, 64, 128, 256], # block_size
|
||||
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096], # num_tokens
|
||||
[1, 2, 4, 8, 16, 32, 64], # topk
|
||||
[64, 160, 256, 257, 260, 264], # num_experts
|
||||
[True, False], # pad_sorted_token_ids
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_moe_align_block_size_compare_implementations(
|
||||
block_size, num_tokens, topk, num_experts, pad_sorted_token_ids
|
||||
):
|
||||
topk_ids = torch.argsort(torch.rand(num_tokens, num_experts, device="cuda"), dim=1)[
|
||||
:, :topk
|
||||
]
|
||||
|
||||
max_num_tokens_padded = topk_ids.numel() + (num_experts + 1) * (block_size - 1)
|
||||
if topk_ids.numel() < num_experts + 1:
|
||||
max_num_tokens_padded = topk_ids.numel() * block_size
|
||||
|
||||
sorted_ids_cuda = torch.empty(
|
||||
(max_num_tokens_padded,), dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
if not pad_sorted_token_ids:
|
||||
sorted_ids_cuda.fill_(topk_ids.numel())
|
||||
max_num_m_blocks = max_num_tokens_padded // block_size
|
||||
expert_ids_cuda = torch.zeros(
|
||||
(max_num_m_blocks,), dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
num_tokens_post_pad_cuda = torch.empty(
|
||||
(1), dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
cumsum_buffer = torch.empty(
|
||||
num_experts + 2, dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
|
||||
sorted_ids_triton = torch.empty_like(sorted_ids_cuda)
|
||||
sorted_ids_triton.fill_(topk_ids.numel())
|
||||
expert_ids_triton = torch.zeros_like(expert_ids_cuda)
|
||||
num_tokens_post_pad_triton = torch.empty_like(num_tokens_post_pad_cuda)
|
||||
|
||||
moe_align_block_size(
|
||||
topk_ids,
|
||||
num_experts + 1,
|
||||
block_size,
|
||||
sorted_ids_cuda,
|
||||
expert_ids_cuda,
|
||||
num_tokens_post_pad_cuda,
|
||||
cumsum_buffer,
|
||||
pad_sorted_token_ids,
|
||||
)
|
||||
|
||||
moe_align_block_size_triton(
|
||||
topk_ids,
|
||||
num_experts + 1,
|
||||
block_size,
|
||||
sorted_ids_triton,
|
||||
expert_ids_triton,
|
||||
num_tokens_post_pad_triton,
|
||||
)
|
||||
|
||||
assert torch.allclose(expert_ids_cuda, expert_ids_triton, atol=0, rtol=0), (
|
||||
f"Expert IDs mismatch for block_size={block_size}, "
|
||||
f"num_tokens={num_tokens}, topk={topk}\n"
|
||||
f"CUDA expert_ids: {expert_ids_cuda}\n"
|
||||
f"Triton expert_ids: {expert_ids_triton}"
|
||||
)
|
||||
|
||||
assert torch.allclose(
|
||||
num_tokens_post_pad_cuda, num_tokens_post_pad_triton, atol=0, rtol=0
|
||||
), (
|
||||
f"Num tokens post pad mismatch for block_size={block_size}, "
|
||||
f"num_tokens={num_tokens}, topk={topk}\n"
|
||||
f"CUDA num_tokens_post_pad: {num_tokens_post_pad_cuda}\n"
|
||||
f"Triton num_tokens_post_pad: {num_tokens_post_pad_triton}"
|
||||
)
|
||||
|
||||
# Select an expert to check
|
||||
expert_idx = expert_ids_cuda.max().item()
|
||||
|
||||
# Get the first and last block id where expert_ids_cuda == expert_idx
|
||||
matching_indices = torch.where(expert_ids_cuda == expert_idx)[0]
|
||||
block_sorted_start = matching_indices[0].item() * block_size
|
||||
block_sorted_end = min(
|
||||
(matching_indices[-1].item() + 1) * block_size,
|
||||
num_tokens_post_pad_cuda.item(),
|
||||
)
|
||||
|
||||
selected_sorted_ids_cuda = sorted_ids_cuda[
|
||||
block_sorted_start:block_sorted_end
|
||||
].sort()[0]
|
||||
selected_sorted_ids_triton = sorted_ids_triton[
|
||||
block_sorted_start:block_sorted_end
|
||||
].sort()[0]
|
||||
|
||||
assert torch.allclose(
|
||||
selected_sorted_ids_cuda,
|
||||
selected_sorted_ids_triton,
|
||||
atol=0,
|
||||
rtol=0,
|
||||
), (
|
||||
f"Sorted IDs mismatch for block_size={block_size}, "
|
||||
f"num_tokens={num_tokens}, topk={topk}\n"
|
||||
f"CUDA sorted_ids: {selected_sorted_ids_cuda}\n"
|
||||
f"Triton sorted_ids: {selected_sorted_ids_triton}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"block_size,num_tokens,topk,num_experts",
|
||||
list(
|
||||
itertools.product(
|
||||
[64, 128], # block_size
|
||||
[1, 8, 32, 256], # num_tokens
|
||||
[8], # topk
|
||||
[
|
||||
1025,
|
||||
2048,
|
||||
4095,
|
||||
], # num_experts (>1024 to exercise v2 kernel, max 4095 real experts)
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_moe_align_block_size_v2_large_num_experts(
|
||||
block_size, num_tokens, topk, num_experts
|
||||
):
|
||||
"""Test moe_align_block_size v2 kernel for >1024 experts against Triton reference."""
|
||||
topk_ids = torch.randint(
|
||||
0, num_experts, (num_tokens, topk), dtype=torch.int32, device="cuda"
|
||||
)
|
||||
|
||||
max_num_tokens_padded = topk_ids.numel() + (num_experts + 1) * (block_size - 1)
|
||||
if topk_ids.numel() < num_experts + 1:
|
||||
max_num_tokens_padded = topk_ids.numel() * block_size
|
||||
|
||||
sorted_ids_cuda = torch.empty(
|
||||
(max_num_tokens_padded,), dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
sorted_ids_cuda.fill_(topk_ids.numel())
|
||||
max_num_m_blocks = max_num_tokens_padded // block_size
|
||||
expert_ids_cuda = torch.zeros(
|
||||
(max_num_m_blocks,), dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
num_tokens_post_pad_cuda = torch.empty(
|
||||
(1), dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
cumsum_buffer = torch.empty(
|
||||
num_experts + 2, dtype=torch.int32, device=topk_ids.device
|
||||
)
|
||||
|
||||
sorted_ids_triton = torch.empty_like(sorted_ids_cuda)
|
||||
sorted_ids_triton.fill_(topk_ids.numel())
|
||||
expert_ids_triton = torch.zeros_like(expert_ids_cuda)
|
||||
num_tokens_post_pad_triton = torch.empty_like(num_tokens_post_pad_cuda)
|
||||
|
||||
moe_align_block_size(
|
||||
topk_ids,
|
||||
num_experts + 1,
|
||||
block_size,
|
||||
sorted_ids_cuda,
|
||||
expert_ids_cuda,
|
||||
num_tokens_post_pad_cuda,
|
||||
cumsum_buffer,
|
||||
True,
|
||||
)
|
||||
|
||||
moe_align_block_size_triton(
|
||||
topk_ids,
|
||||
num_experts + 1,
|
||||
block_size,
|
||||
sorted_ids_triton,
|
||||
expert_ids_triton,
|
||||
num_tokens_post_pad_triton,
|
||||
)
|
||||
|
||||
assert torch.equal(num_tokens_post_pad_cuda, num_tokens_post_pad_triton), (
|
||||
f"Num tokens post pad mismatch: CUDA={num_tokens_post_pad_cuda.item()}, "
|
||||
f"Triton={num_tokens_post_pad_triton.item()}"
|
||||
)
|
||||
|
||||
ntp = num_tokens_post_pad_cuda.item()
|
||||
num_blocks = ntp // block_size
|
||||
|
||||
assert torch.equal(expert_ids_cuda[:num_blocks], expert_ids_triton[:num_blocks]), (
|
||||
f"Expert IDs mismatch for block_size={block_size}, "
|
||||
f"num_tokens={num_tokens}, topk={topk}, num_experts={num_experts}"
|
||||
)
|
||||
|
||||
# Compare sorted_token_ids per expert block (order within block may differ)
|
||||
for b in range(num_blocks):
|
||||
s, e = b * block_size, (b + 1) * block_size
|
||||
block_cuda = sorted_ids_cuda[s:e].sort().values
|
||||
block_triton = sorted_ids_triton[s:e].sort().values
|
||||
assert torch.equal(block_cuda, block_triton), (
|
||||
f"Block {b} sorted_ids mismatch for num_experts={num_experts}, "
|
||||
f"num_tokens={num_tokens}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__]))
|
||||
@@ -1,168 +0,0 @@
|
||||
# Temporarily adapted from https://github.com/vllm-project/vllm/blob/main/tests/lora/test_moe_lora_align_sum.py, will optimize in future refactor
|
||||
import random
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# IMPORT PREBUILT KERNEL
|
||||
# ---------------------------------------------------------
|
||||
from sglang.jit_kernel.moe_lora_align import moe_lora_align_block_size
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=28, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def round_up(x, base):
|
||||
return ((x + base - 1) // base) * base
|
||||
|
||||
|
||||
def CEILDIV(x, y):
|
||||
return (x + y - 1) // y
|
||||
|
||||
|
||||
def sample_data(num_experts, max_loras, num_tokens, topk_num):
|
||||
# 1. Generate TopK IDs (Flattened tokens)
|
||||
topk_ids = torch.zeros((num_tokens, topk_num), dtype=torch.int32)
|
||||
for i in range(num_tokens):
|
||||
pool = list(range(num_experts))
|
||||
random.shuffle(pool)
|
||||
for j in range(topk_num):
|
||||
topk_ids[i, j] = pool[j]
|
||||
|
||||
# 2. Generate Random Requests (Segments)
|
||||
# We split num_tokens into random chunks to simulate a batch of requests
|
||||
remaining_tokens = num_tokens
|
||||
seg_lens = []
|
||||
while remaining_tokens > 0:
|
||||
# Random length between 1 and remaining
|
||||
length = random.randint(1, min(32, remaining_tokens))
|
||||
if remaining_tokens - length < 0:
|
||||
length = remaining_tokens
|
||||
seg_lens.append(length)
|
||||
remaining_tokens -= length
|
||||
|
||||
# Ensure we cover the full range exactly (cleanup last segment)
|
||||
if sum(seg_lens) < num_tokens:
|
||||
seg_lens.append(num_tokens - sum(seg_lens))
|
||||
|
||||
# 3. Build seg_indptr [0, len1, len1+len2, ...]
|
||||
seg_indptr = torch.cumsum(
|
||||
torch.tensor([0] + seg_lens, dtype=torch.int32), dim=0
|
||||
).to(dtype=torch.int32)
|
||||
|
||||
# 4. Assign a LoRA ID to each Request
|
||||
num_reqs = len(seg_lens)
|
||||
req_to_lora = torch.randint(0, max_loras, (num_reqs,), dtype=torch.int32)
|
||||
|
||||
return (topk_ids.to("cuda"), seg_indptr.to("cuda"), req_to_lora.to("cuda"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [100, 200, 1024, 4096])
|
||||
@pytest.mark.parametrize("topk_num", [6])
|
||||
@pytest.mark.parametrize("num_experts", [64, 128, 256, 512])
|
||||
@pytest.mark.parametrize("max_loras", [2, 32])
|
||||
@pytest.mark.parametrize("block_size", [16])
|
||||
def test_moe_lora_align_block_size(
|
||||
num_tokens, topk_num, num_experts, max_loras, block_size
|
||||
):
|
||||
# sample data
|
||||
random.seed(1)
|
||||
torch.manual_seed(1)
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA is not available, skipping moe_lora_align_block_size test.")
|
||||
# UPDATED: Get the new 3-step mapping tensors
|
||||
topk_ids, seg_indptr, req_to_lora = sample_data(
|
||||
num_experts, max_loras, num_tokens, topk_num
|
||||
)
|
||||
|
||||
# compute paddings
|
||||
max_num_tokens_padded = topk_ids.numel() + num_experts * (block_size - 1)
|
||||
max_num_tokens_padded = round_up(max_num_tokens_padded, block_size)
|
||||
max_num_m_blocks = CEILDIV(max_num_tokens_padded, block_size)
|
||||
|
||||
# init output tensors
|
||||
sorted_token_ids = torch.full(
|
||||
(max_loras * max_num_tokens_padded,),
|
||||
topk_ids.numel(),
|
||||
dtype=torch.int32,
|
||||
device="cuda",
|
||||
)
|
||||
expert_ids = torch.full(
|
||||
(max_loras * max_num_m_blocks,), num_experts, dtype=torch.int32, device="cuda"
|
||||
)
|
||||
num_tokens_post_pad = torch.zeros((max_loras,), dtype=torch.int32, device="cuda")
|
||||
adapter_enabled = torch.ones((max_loras + 1,), dtype=torch.int32, device="cuda")
|
||||
lora_ids = torch.arange(max_loras, dtype=torch.int32, device="cuda")
|
||||
|
||||
# UPDATED: Call kernel with new signature
|
||||
moe_lora_align_block_size(
|
||||
topk_ids,
|
||||
seg_indptr, # Arg 2: Pointers
|
||||
req_to_lora, # Arg 3: Request Map
|
||||
num_experts,
|
||||
block_size,
|
||||
max_loras,
|
||||
max_num_tokens_padded,
|
||||
max_num_m_blocks,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_pad,
|
||||
adapter_enabled,
|
||||
lora_ids,
|
||||
None,
|
||||
)
|
||||
|
||||
# verify values
|
||||
expert_ids = expert_ids.view(max_loras, -1)
|
||||
sorted_token_ids = sorted_token_ids.view(max_loras, -1, block_size)
|
||||
|
||||
# Reconstruct token-level ownership for verification logic
|
||||
# We expand req_to_lora back to [num_tokens] on CPU just to check correctness
|
||||
# This proves the kernel (which used the compressed format) produced the right result
|
||||
cpu_seg_indptr = seg_indptr.cpu()
|
||||
cpu_req_to_lora = req_to_lora.cpu()
|
||||
token_ownership = torch.zeros(num_tokens, dtype=torch.int32)
|
||||
|
||||
for r in range(len(cpu_req_to_lora)):
|
||||
start = cpu_seg_indptr[r]
|
||||
end = cpu_seg_indptr[r + 1]
|
||||
token_ownership[start:end] = cpu_req_to_lora[r]
|
||||
|
||||
token_ownership = token_ownership.to("cuda")
|
||||
|
||||
for lora_idx in range(max_loras):
|
||||
# Count how many tokens actually belong to this LoRA
|
||||
expected_count = (token_ownership == lora_idx).sum().item()
|
||||
|
||||
# Verify the kernel processed a reasonable number of tokens (sanity check)
|
||||
# Note: num_tokens_post_pad includes padding, so it might be larger than expected_count
|
||||
assert num_tokens_post_pad[lora_idx].item() >= expected_count * topk_num
|
||||
|
||||
for token_idx in range(sorted_token_ids.size(1)):
|
||||
block = sorted_token_ids[lora_idx][token_idx]
|
||||
# Valid indices are those less than total numel
|
||||
indices = block[block != topk_ids.numel()]
|
||||
|
||||
if indices.numel() > 0:
|
||||
# 1. Verify routing: Does the token actually route to this expert?
|
||||
expert_id = expert_ids[lora_idx][token_idx]
|
||||
assert torch.all(topk_ids.view(-1)[indices] == expert_id)
|
||||
|
||||
# 2. Verify ownership: Did the kernel grab the correct tokens for this LoRA?
|
||||
# The indices in 'sorted_token_ids' point to the flattened [token, topk] array.
|
||||
# We divide by topk_num to get the original token index.
|
||||
original_token_indices = indices // topk_num
|
||||
|
||||
# Check that all tokens in this block truly belong to 'lora_idx'
|
||||
actual_owners = token_ownership[original_token_indices]
|
||||
assert torch.all(
|
||||
actual_owners == lora_idx
|
||||
), f"Kernel put tokens from LoRA {actual_owners} into block for LoRA {lora_idx}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__]))
|
||||
@@ -1,615 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from sgl_kernel.scalar_type import scalar_types
|
||||
|
||||
from sglang.jit_kernel.moe_wna16_marlin import moe_wna16_marlin_gemm
|
||||
from sglang.srt.layers.moe.fused_moe_triton import moe_align_block_size
|
||||
from sglang.srt.layers.moe.fused_moe_triton.fused_marlin_moe import fused_marlin_moe
|
||||
from sglang.srt.layers.quantization.marlin_utils_fp4 import (
|
||||
prepare_moe_nvfp4_layer_for_marlin,
|
||||
)
|
||||
from sglang.srt.utils.common import is_sm80_supported, is_sm90_supported
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_marlin_utils import (
|
||||
awq_marlin_quantize,
|
||||
make_nvfp4_weight_and_ref,
|
||||
marlin_quantize,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=10, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def _has_aot_moe_wna16_marlin_gemm() -> bool:
|
||||
return hasattr(torch.ops.sgl_kernel, "moe_wna16_marlin_gemm") and hasattr(
|
||||
torch.ops.sgl_kernel.moe_wna16_marlin_gemm, "default"
|
||||
)
|
||||
|
||||
|
||||
AOT_AVAILABLE = _has_aot_moe_wna16_marlin_gemm()
|
||||
|
||||
|
||||
def stack_and_dev(tensors: list[torch.Tensor]):
|
||||
dev = tensors[0].device
|
||||
return torch.stack(tensors, dim=0).to(dev)
|
||||
|
||||
|
||||
def _get_scalar_type(num_bits: int, has_zp: bool):
|
||||
if has_zp:
|
||||
assert num_bits == 4
|
||||
return scalar_types.uint4
|
||||
else:
|
||||
return scalar_types.uint4b8 if num_bits == 4 else scalar_types.uint8b128
|
||||
|
||||
|
||||
def _setup_moe_weights(e, n, k, quant_type, group_size, act_order, dtype):
|
||||
"""Set up quantized MoE weights for a single gate (e experts, output n, input k)."""
|
||||
has_zp = quant_type in [scalar_types.uint4, scalar_types.uint8]
|
||||
|
||||
w = torch.randn((e, n, k), device="cuda", dtype=dtype) / 20
|
||||
|
||||
w_ref_l = []
|
||||
qweight_l = []
|
||||
scales_l = []
|
||||
zeros_l = []
|
||||
g_idx_l = []
|
||||
sort_indices_l = []
|
||||
|
||||
for i in range(e):
|
||||
if has_zp:
|
||||
w_ref, qweight, scales, zeros = awq_marlin_quantize(
|
||||
w[i].transpose(1, 0), quant_type, group_size
|
||||
)
|
||||
w_ref_l.append(w_ref.T)
|
||||
qweight_l.append(qweight)
|
||||
scales_l.append(scales)
|
||||
zeros_l.append(zeros)
|
||||
else:
|
||||
test_perm = torch.randperm(k)
|
||||
w_ref, qweight, scales, g_idx, sort_indices, _ = marlin_quantize(
|
||||
w[i].transpose(1, 0), quant_type, group_size, act_order, test_perm
|
||||
)
|
||||
w_ref_l.append(w_ref.T)
|
||||
qweight_l.append(qweight)
|
||||
scales_l.append(scales)
|
||||
g_idx_l.append(g_idx)
|
||||
sort_indices_l.append(sort_indices)
|
||||
|
||||
w_ref = stack_and_dev(w_ref_l)
|
||||
qweight = stack_and_dev(qweight_l).contiguous()
|
||||
scales = stack_and_dev(scales_l)
|
||||
g_idx = stack_and_dev(g_idx_l) if g_idx_l else None
|
||||
sort_indices = stack_and_dev(sort_indices_l) if sort_indices_l else None
|
||||
zeros = stack_and_dev(zeros_l) if zeros_l else None
|
||||
|
||||
return w_ref, qweight, scales, zeros, g_idx, sort_indices
|
||||
|
||||
|
||||
def _run_single_gemm(
|
||||
fn,
|
||||
a,
|
||||
c,
|
||||
qweight,
|
||||
scales,
|
||||
zeros,
|
||||
g_idx,
|
||||
sort_indices,
|
||||
workspace,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
topk_weights,
|
||||
quant_type,
|
||||
block_size_m,
|
||||
topk,
|
||||
size_m,
|
||||
size_n,
|
||||
size_k,
|
||||
mul_topk_weights,
|
||||
is_k_full,
|
||||
use_atomic_add,
|
||||
):
|
||||
return fn(
|
||||
a,
|
||||
c,
|
||||
qweight,
|
||||
None, # b_bias
|
||||
scales,
|
||||
None, # global_scale
|
||||
zeros,
|
||||
g_idx,
|
||||
sort_indices,
|
||||
workspace,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
topk_weights,
|
||||
moe_block_size=block_size_m,
|
||||
top_k=topk,
|
||||
mul_topk_weights=mul_topk_weights,
|
||||
is_ep=False,
|
||||
b_q_type=quant_type,
|
||||
size_m=size_m,
|
||||
size_n=size_n,
|
||||
size_k=size_k,
|
||||
is_k_full=is_k_full,
|
||||
use_atomic_add=use_atomic_add,
|
||||
use_fp32_reduce=True,
|
||||
is_zp_float=False,
|
||||
)
|
||||
|
||||
|
||||
def _run_single_gemm_aot(
|
||||
a,
|
||||
c,
|
||||
qweight,
|
||||
scales,
|
||||
zeros,
|
||||
g_idx,
|
||||
sort_indices,
|
||||
workspace,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
topk_weights,
|
||||
quant_type,
|
||||
block_size_m,
|
||||
topk,
|
||||
size_m,
|
||||
size_n,
|
||||
size_k,
|
||||
mul_topk_weights,
|
||||
is_k_full,
|
||||
use_atomic_add,
|
||||
):
|
||||
return torch.ops.sgl_kernel.moe_wna16_marlin_gemm.default(
|
||||
a,
|
||||
c,
|
||||
qweight,
|
||||
None, # b_bias
|
||||
scales,
|
||||
None, # global_scale
|
||||
zeros,
|
||||
g_idx,
|
||||
sort_indices,
|
||||
workspace,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
topk_weights,
|
||||
moe_block_size=block_size_m,
|
||||
top_k=topk,
|
||||
mul_topk_weights=mul_topk_weights,
|
||||
is_ep=False,
|
||||
b_q_type_id=quant_type.id,
|
||||
size_m=size_m,
|
||||
size_n=size_n,
|
||||
size_k=size_k,
|
||||
is_k_full=is_k_full,
|
||||
use_atomic_add=use_atomic_add,
|
||||
use_fp32_reduce=True,
|
||||
is_zp_float=False,
|
||||
)
|
||||
|
||||
|
||||
def generate_test_cases():
|
||||
m_list = [1, 123]
|
||||
n_list = [128, 1024]
|
||||
k_list = [256]
|
||||
e_list = [4]
|
||||
topk_list = [2]
|
||||
dtype_list = [torch.float16, torch.bfloat16]
|
||||
group_size_list = [128]
|
||||
act_order_list = [False, True]
|
||||
quant_type_list = [scalar_types.uint4, scalar_types.uint4b8]
|
||||
|
||||
all_combinations = itertools.product(
|
||||
m_list,
|
||||
n_list,
|
||||
k_list,
|
||||
e_list,
|
||||
topk_list,
|
||||
dtype_list,
|
||||
group_size_list,
|
||||
act_order_list,
|
||||
quant_type_list,
|
||||
)
|
||||
|
||||
def is_valid(m, n, k, e, topk, dtype, group_size, act_order, quant_type):
|
||||
has_zp = quant_type in [scalar_types.uint4, scalar_types.uint8]
|
||||
if act_order:
|
||||
if group_size == -1 or group_size == k:
|
||||
return False
|
||||
if has_zp:
|
||||
return False
|
||||
if group_size > 0 and k % group_size != 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
return [case for case in all_combinations if is_valid(*case)]
|
||||
|
||||
|
||||
TEST_CASES = generate_test_cases()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"m,n,k,e,topk,dtype,group_size,act_order,quant_type",
|
||||
TEST_CASES,
|
||||
ids=[
|
||||
f"m{c[0]}_n{c[1]}_k{c[2]}_e{c[3]}_t{c[4]}_{c[5].__name__ if hasattr(c[5], '__name__') else str(c[5]).split('.')[-1]}_g{c[6]}_act{c[7]}_{c[8]}"
|
||||
for c in TEST_CASES
|
||||
],
|
||||
)
|
||||
def test_moe_wna16_marlin_gemm(
|
||||
m, n, k, e, topk, dtype, group_size, act_order, quant_type
|
||||
):
|
||||
if not AOT_AVAILABLE:
|
||||
pytest.skip("sgl_kernel moe_wna16_marlin_gemm AOT op not available")
|
||||
|
||||
torch.manual_seed(0)
|
||||
|
||||
has_zp = quant_type in [scalar_types.uint4, scalar_types.uint8]
|
||||
|
||||
a = torch.randn((m, k), device="cuda", dtype=dtype) / 10
|
||||
|
||||
# Set up quantized weights for first gemm (gate_up: output 2*n, input k)
|
||||
w_ref1, qweight1, scales1, zeros1, g_idx1, sort_indices1 = _setup_moe_weights(
|
||||
e, 2 * n, k, quant_type, group_size, act_order, dtype
|
||||
)
|
||||
|
||||
# Compute block_size_m
|
||||
for block_size_m in [8, 16, 32, 48, 64]:
|
||||
if m * topk / e / block_size_m < 0.9:
|
||||
break
|
||||
|
||||
# Align tokens
|
||||
score = torch.randn((m, e), device="cuda", dtype=dtype)
|
||||
score_softmax = torch.softmax(score, dim=-1, dtype=torch.float32)
|
||||
topk_weights, topk_ids = torch.topk(score_softmax, topk)
|
||||
|
||||
sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
|
||||
topk_ids, block_size_m, e
|
||||
)
|
||||
|
||||
# Workspace
|
||||
sms = torch.cuda.get_device_properties("cuda").multi_processor_count
|
||||
max_workspace_size = (max(2 * n, k) // 64) * (
|
||||
sorted_token_ids.size(0) // block_size_m
|
||||
)
|
||||
max_workspace_size = min(max_workspace_size, sms * 4)
|
||||
workspace = torch.zeros(
|
||||
max_workspace_size, dtype=torch.int, device="cuda", requires_grad=False
|
||||
)
|
||||
|
||||
use_atomic_add = (
|
||||
dtype == torch.half or torch.cuda.get_device_capability("cuda")[0] >= 9
|
||||
)
|
||||
|
||||
scalar_type = _get_scalar_type(4, has_zp)
|
||||
|
||||
# --- Run JIT kernel ---
|
||||
c_jit = torch.empty((m * topk, 2 * n), dtype=dtype, device="cuda")
|
||||
c_jit = _run_single_gemm(
|
||||
moe_wna16_marlin_gemm,
|
||||
a,
|
||||
c_jit,
|
||||
qweight1,
|
||||
scales1,
|
||||
zeros1,
|
||||
g_idx1,
|
||||
sort_indices1,
|
||||
workspace,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
topk_weights,
|
||||
scalar_type,
|
||||
block_size_m,
|
||||
topk,
|
||||
m,
|
||||
2 * n,
|
||||
k,
|
||||
False,
|
||||
True,
|
||||
use_atomic_add,
|
||||
)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# --- Check bitwise equality with AOT kernel ---
|
||||
c_aot = torch.empty((m * topk, 2 * n), dtype=dtype, device="cuda")
|
||||
c_aot = _run_single_gemm_aot(
|
||||
a,
|
||||
c_aot,
|
||||
qweight1,
|
||||
scales1,
|
||||
zeros1,
|
||||
g_idx1,
|
||||
sort_indices1,
|
||||
workspace,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
topk_weights,
|
||||
scalar_type,
|
||||
block_size_m,
|
||||
topk,
|
||||
m,
|
||||
2 * n,
|
||||
k,
|
||||
False,
|
||||
True,
|
||||
use_atomic_add,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
torch.testing.assert_close(c_jit, c_aot, rtol=0, atol=0)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Skip, test pass locally but compiling takes too long in CI")
|
||||
@pytest.mark.skipif(
|
||||
not (is_sm80_supported() or is_sm90_supported()),
|
||||
reason="Non-gated NVFP4 Marlin fallback test requires CUDA SM8X/SM9X",
|
||||
)
|
||||
def test_fused_marlin_moe_non_gated_relu2():
|
||||
torch.manual_seed(0)
|
||||
|
||||
m = 17
|
||||
n = 128
|
||||
k = 256
|
||||
e = 4
|
||||
topk = 2
|
||||
dtype = torch.float16
|
||||
group_size = 128
|
||||
quant_type = scalar_types.uint4b8
|
||||
|
||||
hidden_states = torch.randn((m, k), device="cuda", dtype=dtype) / 10
|
||||
w_ref1, qweight1, scales1, zeros1, g_idx1, sort_indices1 = _setup_moe_weights(
|
||||
e, n, k, quant_type, group_size, False, dtype
|
||||
)
|
||||
w_ref2, qweight2, scales2, zeros2, g_idx2, sort_indices2 = _setup_moe_weights(
|
||||
e, k, n, quant_type, group_size, False, dtype
|
||||
)
|
||||
|
||||
router_logits = torch.randn((m, e), device="cuda", dtype=dtype)
|
||||
score_softmax = torch.softmax(router_logits, dim=-1, dtype=torch.float32)
|
||||
topk_weights, topk_ids = torch.topk(score_softmax, topk)
|
||||
|
||||
output = fused_marlin_moe(
|
||||
hidden_states=hidden_states,
|
||||
w1=qweight1,
|
||||
w2=qweight2,
|
||||
w1_scale=scales1,
|
||||
w2_scale=scales2,
|
||||
gating_output=router_logits,
|
||||
topk_weights=topk_weights,
|
||||
topk_ids=topk_ids,
|
||||
g_idx1=g_idx1,
|
||||
g_idx2=g_idx2,
|
||||
sort_indices1=sort_indices1,
|
||||
sort_indices2=sort_indices2,
|
||||
w1_zeros=zeros1,
|
||||
w2_zeros=zeros2,
|
||||
num_bits=4,
|
||||
is_k_full=True,
|
||||
routed_scaling_factor=1.0,
|
||||
activation="relu2",
|
||||
is_gated=False,
|
||||
)
|
||||
|
||||
output_ref = torch.zeros_like(hidden_states)
|
||||
for token_idx in range(m):
|
||||
for route_idx in range(topk):
|
||||
expert_id = topk_ids[token_idx, route_idx]
|
||||
intermediate = hidden_states[token_idx] @ w_ref1[expert_id].T
|
||||
intermediate = torch.square(torch.relu(intermediate))
|
||||
routed = intermediate @ w_ref2[expert_id].T
|
||||
output_ref[token_idx] += routed * topk_weights[token_idx, route_idx]
|
||||
|
||||
torch.cuda.synchronize()
|
||||
torch.testing.assert_close(output, output_ref, rtol=0.04, atol=0.04)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Skip, test pass locally but compiling takes too long in CI")
|
||||
@pytest.mark.skipif(
|
||||
not (is_sm80_supported() or is_sm90_supported()),
|
||||
reason="NVFP4 Marlin MoE padding test requires CUDA SM8X/SM9X",
|
||||
)
|
||||
def test_fused_marlin_moe_nvfp4_non_gated_padded_intermediate_launches():
|
||||
torch.manual_seed(0)
|
||||
|
||||
m = 17
|
||||
intermediate_size = 192
|
||||
hidden_size = 256
|
||||
e = 4
|
||||
topk = 2
|
||||
dtype = torch.bfloat16
|
||||
nvfp4_group_size = 16
|
||||
|
||||
layer = torch.nn.Module()
|
||||
layer.quant_config = SimpleNamespace(group_size=nvfp4_group_size)
|
||||
layer.moe_runner_config = SimpleNamespace(is_gated=False)
|
||||
layer.params_dtype = dtype
|
||||
layer.intermediate_size_per_partition = intermediate_size
|
||||
layer.w13_weight = torch.nn.Parameter(
|
||||
torch.randint(
|
||||
0,
|
||||
256,
|
||||
(e, intermediate_size, hidden_size // 2),
|
||||
device="cuda",
|
||||
dtype=torch.uint8,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.w2_weight = torch.nn.Parameter(
|
||||
torch.randint(
|
||||
0,
|
||||
256,
|
||||
(e, hidden_size, intermediate_size // 2),
|
||||
device="cuda",
|
||||
dtype=torch.uint8,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.w13_weight_scale = torch.nn.Parameter(
|
||||
torch.rand(
|
||||
(e, intermediate_size, hidden_size // nvfp4_group_size),
|
||||
device="cuda",
|
||||
dtype=dtype,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.w2_weight_scale = torch.nn.Parameter(
|
||||
torch.rand(
|
||||
(e, hidden_size, intermediate_size // nvfp4_group_size),
|
||||
device="cuda",
|
||||
dtype=dtype,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.w13_weight_scale_2 = torch.nn.Parameter(
|
||||
torch.ones((e,), device="cuda", dtype=dtype), requires_grad=False
|
||||
)
|
||||
layer.w2_weight_scale_2 = torch.nn.Parameter(
|
||||
torch.ones((e,), device="cuda", dtype=dtype), requires_grad=False
|
||||
)
|
||||
prepare_moe_nvfp4_layer_for_marlin(layer)
|
||||
|
||||
assert layer.w13_weight.shape[1] * 16 == 256
|
||||
assert layer.w2_weight.shape[1] * 16 == 256
|
||||
|
||||
hidden_states = torch.randn((m, hidden_size), device="cuda", dtype=dtype) / 10
|
||||
|
||||
score = torch.randn((m, e), device="cuda", dtype=dtype)
|
||||
score_softmax = torch.softmax(score, dim=-1, dtype=torch.float32)
|
||||
topk_weights, topk_ids = torch.topk(score_softmax, topk)
|
||||
|
||||
out = fused_marlin_moe(
|
||||
hidden_states=hidden_states,
|
||||
w1=layer.w13_weight,
|
||||
w2=layer.w2_weight,
|
||||
w1_scale=layer.w13_weight_scale,
|
||||
w2_scale=layer.w2_weight_scale,
|
||||
gating_output=score,
|
||||
topk_weights=topk_weights,
|
||||
topk_ids=topk_ids,
|
||||
w1_global_scale=layer.w13_weight_scale_2,
|
||||
w2_global_scale=layer.w2_weight_scale_2,
|
||||
workspace=layer.workspace,
|
||||
num_bits=4,
|
||||
is_k_full=True,
|
||||
routed_scaling_factor=1.0,
|
||||
activation="relu2",
|
||||
is_gated=False,
|
||||
)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
assert out.shape == (m, hidden_size)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Skip, test pass locally but compiling takes too long in CI")
|
||||
@pytest.mark.skipif(
|
||||
not (is_sm80_supported() or is_sm90_supported()),
|
||||
reason="NVFP4 Marlin MoE numeric test requires CUDA SM80, SM86, or SM90",
|
||||
)
|
||||
def test_fused_marlin_moe_nvfp4_non_gated_matches_dequant_reference():
|
||||
torch.manual_seed(0)
|
||||
|
||||
m = 17
|
||||
intermediate_size = 192
|
||||
hidden_size = 256
|
||||
e = 4
|
||||
topk = 2
|
||||
dtype = torch.bfloat16
|
||||
group_size = 16
|
||||
routed_scaling_factor = 1.0
|
||||
|
||||
w13_packed_l, w13_scales_l, w13_gscale_l, w13_ref_l = [], [], [], []
|
||||
w2_packed_l, w2_scales_l, w2_gscale_l, w2_ref_l = [], [], [], []
|
||||
for _ in range(e):
|
||||
packed, scales, gscale, ref = make_nvfp4_weight_and_ref(
|
||||
intermediate_size, hidden_size, dtype, group_size=group_size
|
||||
)
|
||||
w13_packed_l.append(packed)
|
||||
w13_scales_l.append(scales)
|
||||
w13_gscale_l.append(gscale)
|
||||
w13_ref_l.append(ref)
|
||||
|
||||
packed, scales, gscale, ref = make_nvfp4_weight_and_ref(
|
||||
hidden_size, intermediate_size, dtype, group_size=group_size
|
||||
)
|
||||
w2_packed_l.append(packed)
|
||||
w2_scales_l.append(scales)
|
||||
w2_gscale_l.append(gscale)
|
||||
w2_ref_l.append(ref)
|
||||
|
||||
layer = torch.nn.Module()
|
||||
layer.quant_config = SimpleNamespace(group_size=group_size)
|
||||
layer.moe_runner_config = SimpleNamespace(is_gated=False)
|
||||
layer.params_dtype = dtype
|
||||
layer.intermediate_size_per_partition = intermediate_size
|
||||
layer.w13_weight = torch.nn.Parameter(
|
||||
torch.stack(w13_packed_l), requires_grad=False
|
||||
)
|
||||
layer.w2_weight = torch.nn.Parameter(torch.stack(w2_packed_l), requires_grad=False)
|
||||
layer.w13_weight_scale = torch.nn.Parameter(
|
||||
torch.stack(w13_scales_l), requires_grad=False
|
||||
)
|
||||
layer.w2_weight_scale = torch.nn.Parameter(
|
||||
torch.stack(w2_scales_l), requires_grad=False
|
||||
)
|
||||
layer.w13_weight_scale_2 = torch.nn.Parameter(
|
||||
torch.stack(w13_gscale_l), requires_grad=False
|
||||
)
|
||||
layer.w2_weight_scale_2 = torch.nn.Parameter(
|
||||
torch.stack(w2_gscale_l), requires_grad=False
|
||||
)
|
||||
prepare_moe_nvfp4_layer_for_marlin(layer)
|
||||
|
||||
# Scale activations down so relu² doesn't blow up intermediate magnitudes;
|
||||
# this keeps output values small so tighter element-wise tolerance is realistic.
|
||||
hidden_states = torch.randn((m, hidden_size), device="cuda", dtype=dtype) / 20
|
||||
router_logits = torch.randn((m, e), device="cuda", dtype=dtype)
|
||||
score_softmax = torch.softmax(router_logits, dim=-1, dtype=torch.float32)
|
||||
topk_weights, topk_ids = torch.topk(score_softmax, topk)
|
||||
|
||||
output = fused_marlin_moe(
|
||||
hidden_states=hidden_states,
|
||||
w1=layer.w13_weight,
|
||||
w2=layer.w2_weight,
|
||||
w1_scale=layer.w13_weight_scale,
|
||||
w2_scale=layer.w2_weight_scale,
|
||||
gating_output=router_logits,
|
||||
topk_weights=topk_weights,
|
||||
topk_ids=topk_ids,
|
||||
w1_global_scale=layer.w13_weight_scale_2,
|
||||
w2_global_scale=layer.w2_weight_scale_2,
|
||||
workspace=layer.workspace,
|
||||
num_bits=4,
|
||||
is_k_full=True,
|
||||
routed_scaling_factor=routed_scaling_factor,
|
||||
activation="relu2",
|
||||
is_gated=False,
|
||||
)
|
||||
|
||||
w13_ref = torch.stack(w13_ref_l)
|
||||
w2_ref = torch.stack(w2_ref_l)
|
||||
output_ref = torch.zeros_like(hidden_states)
|
||||
for token_idx in range(m):
|
||||
for route_idx in range(topk):
|
||||
expert_id = topk_ids[token_idx, route_idx]
|
||||
intermediate = hidden_states[token_idx] @ w13_ref[expert_id].T
|
||||
intermediate = torch.square(torch.relu(intermediate))
|
||||
routed = intermediate @ w2_ref[expert_id].T
|
||||
output_ref[token_idx] += routed * topk_weights[token_idx, route_idx]
|
||||
output_ref *= routed_scaling_factor
|
||||
|
||||
torch.cuda.synchronize()
|
||||
torch.testing.assert_close(output, output_ref, rtol=0.05, atol=0.25)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,153 +0,0 @@
|
||||
import random
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.mxfp8 import (
|
||||
es_sm100_mxfp8_blockscaled_grouped_quant,
|
||||
es_sm100_mxfp8_blockscaled_moe_grouped_gemm,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=5, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def align(val: int, alignment: int = 128) -> int:
|
||||
return int((val + alignment - 1) // alignment * alignment)
|
||||
|
||||
|
||||
# Copy from: https://github.com/deepseek-ai/DeepGEMM/blob/main/deep_gemm/utils.py
|
||||
def calc_diff(x, y):
|
||||
x, y = x.double(), y.double()
|
||||
denominator = (x * x + y * y).sum()
|
||||
sim = 2 * (x * y).sum() / denominator
|
||||
return 1 - sim
|
||||
|
||||
|
||||
def is_sm100_supported(device=None) -> bool:
|
||||
return (torch.cuda.get_device_capability(device)[0] == 10) and (
|
||||
torch.version.cuda >= "12.8"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not is_sm100_supported(),
|
||||
reason="test_mxfp8_moe at jit kernen is only supported on sm100",
|
||||
)
|
||||
@pytest.mark.parametrize("num_experts", [8, 16, 32, 64])
|
||||
@pytest.mark.parametrize("out_dtype", [torch.half, torch.bfloat16])
|
||||
def test_es_sm100_mxfp8_blockscaled_grouped_mm(num_experts, out_dtype):
|
||||
device = "cuda"
|
||||
alignment = 128
|
||||
n_g = random.randint(1, 64) * alignment
|
||||
k_g = random.randint(1, 64) * alignment
|
||||
|
||||
expert_offset = 0
|
||||
expert_offsets = []
|
||||
aux_expert_offset = 0
|
||||
aux_expert_offsets = []
|
||||
a_blockscale_offset = 0
|
||||
a_blockscale_offsets = []
|
||||
b_blockscale_offset = 0
|
||||
b_blockscale_offsets = []
|
||||
a_list = []
|
||||
b_list = []
|
||||
ref_d_list = []
|
||||
tokens_per_expert = []
|
||||
|
||||
for g in range(num_experts):
|
||||
m_g = random.randint(1, 512)
|
||||
tokens_per_expert.append(m_g)
|
||||
expert_offsets.append(expert_offset)
|
||||
expert_offset += m_g
|
||||
aux_expert_offsets.append(aux_expert_offset)
|
||||
aux_expert_offset += n_g
|
||||
a_blockscale_offsets.append(a_blockscale_offset)
|
||||
a_blockscale_offset += align(m_g, 128)
|
||||
b_blockscale_offsets.append(b_blockscale_offset)
|
||||
b_blockscale_offset += n_g # n_g already align to 128
|
||||
|
||||
a = torch.normal(
|
||||
0.0, std=1.0, size=(m_g, k_g), device=device, dtype=out_dtype
|
||||
) # (M, K):(K, 1)
|
||||
b = torch.normal(
|
||||
0.0, std=1.0, size=(n_g, k_g), device=device, dtype=out_dtype
|
||||
) # (N, K):(K, 1)
|
||||
|
||||
a_list.append(a)
|
||||
b_list.append(b)
|
||||
ref_d = a @ b.T
|
||||
ref_d_list.append(ref_d)
|
||||
a = torch.concat(a_list, dim=0)
|
||||
b = torch.concat(b_list, dim=0)
|
||||
|
||||
_expert_offsets = torch.tensor(expert_offsets).to(device=device, dtype=torch.int32)
|
||||
_aux_expert_offsets = torch.tensor(aux_expert_offsets).to(
|
||||
device=device, dtype=torch.int32
|
||||
)
|
||||
_a_blockscale_offsets = torch.tensor(a_blockscale_offsets).to(
|
||||
device=device, dtype=torch.int32
|
||||
)
|
||||
_b_blockscale_offsets = torch.tensor(b_blockscale_offsets).to(
|
||||
device=device, dtype=torch.int32
|
||||
)
|
||||
|
||||
a_quant = torch.zeros_like(a, dtype=torch.float8_e4m3fn, device=device)
|
||||
a_scale_factor = torch.zeros(
|
||||
(a_blockscale_offset, k_g // 32), dtype=torch.uint8, device=device
|
||||
)
|
||||
|
||||
b_quant = torch.zeros_like(b, dtype=torch.float8_e4m3fn, device=device)
|
||||
b_scale_factor = torch.zeros(
|
||||
(num_experts * n_g, k_g // 32), dtype=torch.uint8, device=device
|
||||
)
|
||||
tokens_per_expert = torch.tensor(tokens_per_expert).to(
|
||||
device=device, dtype=torch.int32
|
||||
)
|
||||
workspace = torch.empty((1024, 1024, 1024), dtype=torch.uint8, device=device)
|
||||
|
||||
es_sm100_mxfp8_blockscaled_grouped_quant(
|
||||
a,
|
||||
tokens_per_expert,
|
||||
_expert_offsets,
|
||||
_a_blockscale_offsets,
|
||||
a_quant,
|
||||
a_scale_factor,
|
||||
)
|
||||
es_sm100_mxfp8_blockscaled_grouped_quant(
|
||||
b,
|
||||
torch.ones_like(tokens_per_expert) * n_g,
|
||||
_aux_expert_offsets,
|
||||
_b_blockscale_offsets,
|
||||
b_quant,
|
||||
b_scale_factor,
|
||||
)
|
||||
|
||||
b_quant = b_quant.view(num_experts, n_g, k_g)
|
||||
b_scale_factor = b_scale_factor.view(num_experts, n_g, k_g // 32)
|
||||
d = es_sm100_mxfp8_blockscaled_moe_grouped_gemm(
|
||||
b_quant,
|
||||
a_quant,
|
||||
b_scale_factor,
|
||||
a_scale_factor,
|
||||
_expert_offsets,
|
||||
_a_blockscale_offsets,
|
||||
tokens_per_expert,
|
||||
workspace,
|
||||
a.dtype,
|
||||
)
|
||||
|
||||
for g in range(num_experts):
|
||||
baseline = ref_d_list[g]
|
||||
actual = d[expert_offsets[g] : (expert_offsets[g] + tokens_per_expert[g])]
|
||||
diff = calc_diff(actual, baseline)
|
||||
assert diff < 0.001
|
||||
print(
|
||||
f"m_g={baseline.shape[0]} n_g={n_g} k_g={k_g} num_experts={num_experts}, out_dtype={out_dtype}, diff={diff:.5f}: OK"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__]))
|
||||
@@ -1,135 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.ngram_embedding import (
|
||||
compute_n_gram_ids,
|
||||
compute_n_gram_ids_decode,
|
||||
update_token_table,
|
||||
update_token_table_decode,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
|
||||
def _make_ngram_params(ne_n: int, ne_k: int, vocab_size: int):
|
||||
ne_weights = torch.zeros([ne_n - 1, ne_k, ne_n], dtype=torch.int32)
|
||||
ne_mods = torch.zeros([ne_n - 1, ne_k], dtype=torch.int32)
|
||||
exclusive_sums = torch.zeros([(ne_n - 1) * ne_k + 1], dtype=torch.int32)
|
||||
|
||||
for n in range(2, ne_n + 1):
|
||||
for k in range(ne_k):
|
||||
config_id = (n - 2) * ne_k + k
|
||||
mod = 65537 + 2 * config_id
|
||||
ne_mods[n - 2][k] = mod
|
||||
exclusive_sums[config_id + 1] = exclusive_sums[config_id] + mod
|
||||
for delta in range(ne_n):
|
||||
ne_weights[n - 2][k][delta] = pow(vocab_size, delta, mod)
|
||||
|
||||
return (
|
||||
ne_weights.cuda(),
|
||||
ne_mods.cuda(),
|
||||
exclusive_sums.cuda(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 2, 17, 128, 1024])
|
||||
def test_compute_n_gram_ids_decode_matches_general(batch_size: int) -> None:
|
||||
ne_n = 8
|
||||
ne_k = 2
|
||||
vocab_size = 32000
|
||||
max_context_len = 1024
|
||||
max_running_reqs = batch_size + 8
|
||||
num_configs = (ne_n - 1) * ne_k
|
||||
|
||||
ne_weights, ne_mods, exclusive_sums = _make_ngram_params(ne_n, ne_k, vocab_size)
|
||||
ne_token_table = torch.randint(
|
||||
0,
|
||||
vocab_size,
|
||||
(max_running_reqs, max_context_len),
|
||||
dtype=torch.int32,
|
||||
device="cuda",
|
||||
)
|
||||
row_indices = torch.randperm(max_running_reqs, device="cuda")[:batch_size].to(
|
||||
torch.int64
|
||||
)
|
||||
column_starts = torch.randint(
|
||||
0, max_context_len, (batch_size,), dtype=torch.int32, device="cuda"
|
||||
)
|
||||
tokens = torch.empty(batch_size, dtype=torch.int32, device="cuda")
|
||||
exclusive_req_len_sums = torch.arange(
|
||||
batch_size + 1, dtype=torch.int32, device="cuda"
|
||||
)
|
||||
n_gram_ids_general = torch.empty(
|
||||
(batch_size, num_configs), dtype=torch.int32, device="cuda"
|
||||
)
|
||||
n_gram_ids_decode = torch.empty_like(n_gram_ids_general)
|
||||
|
||||
compute_n_gram_ids(
|
||||
ne_n=ne_n,
|
||||
ne_k=ne_k,
|
||||
ne_weights=ne_weights,
|
||||
ne_mods=ne_mods,
|
||||
exclusive_ne_embedder_size_sums=exclusive_sums,
|
||||
tokens=tokens,
|
||||
exclusive_req_len_sums=exclusive_req_len_sums,
|
||||
ne_token_table=ne_token_table,
|
||||
row_indices=row_indices,
|
||||
column_starts=column_starts,
|
||||
n_gram_ids=n_gram_ids_general,
|
||||
)
|
||||
compute_n_gram_ids_decode(
|
||||
ne_n=ne_n,
|
||||
ne_k=ne_k,
|
||||
ne_weights=ne_weights,
|
||||
ne_mods=ne_mods,
|
||||
exclusive_ne_embedder_size_sums=exclusive_sums,
|
||||
ne_token_table=ne_token_table,
|
||||
row_indices=row_indices,
|
||||
column_starts=column_starts,
|
||||
n_gram_ids=n_gram_ids_decode,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(n_gram_ids_decode, n_gram_ids_general, atol=0, rtol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 2, 17, 128, 1024])
|
||||
def test_update_token_table_decode_matches_general(batch_size: int) -> None:
|
||||
max_context_len = 4096
|
||||
max_running_reqs = batch_size + 8
|
||||
tokens = torch.arange(batch_size, dtype=torch.int32, device="cuda") + 100
|
||||
row_indices = torch.randperm(max_running_reqs, device="cuda")[:batch_size].to(
|
||||
torch.int64
|
||||
)
|
||||
column_starts = torch.randint(
|
||||
0, max_context_len, (batch_size,), dtype=torch.int32, device="cuda"
|
||||
)
|
||||
req_lens = torch.ones(batch_size, dtype=torch.int32, device="cuda")
|
||||
|
||||
token_table_general = torch.full(
|
||||
(max_running_reqs, max_context_len), -1, dtype=torch.int32, device="cuda"
|
||||
)
|
||||
token_table_decode = token_table_general.clone()
|
||||
|
||||
update_token_table(
|
||||
tokens=tokens,
|
||||
ne_token_table=token_table_general,
|
||||
row_indices=row_indices,
|
||||
column_starts=column_starts,
|
||||
req_lens=req_lens,
|
||||
ignore_tokens=None,
|
||||
)
|
||||
update_token_table_decode(
|
||||
tokens=tokens,
|
||||
ne_token_table=token_table_decode,
|
||||
row_indices=row_indices,
|
||||
column_starts=column_starts,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(token_table_decode, token_table_general, atol=0, rtol=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,137 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.nvfp4 import (
|
||||
cutlass_fp4_group_mm,
|
||||
scaled_fp4_experts_quant,
|
||||
scaled_fp4_quant,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=5, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
FLOAT4_E2M1_MAX = 6.0
|
||||
FLOAT8_E4M3_MAX = torch.finfo(torch.float8_e4m3fn).max
|
||||
|
||||
|
||||
def _nvfp4_supported() -> bool:
|
||||
return torch.cuda.is_available() and torch.cuda.get_device_capability() >= (10, 0)
|
||||
|
||||
|
||||
def _round_up(x: int, y: int) -> int:
|
||||
return ((x + y - 1) // y) * y
|
||||
|
||||
|
||||
def _build_expert_offsets(
|
||||
m_per_expert: list[int], device: torch.device
|
||||
) -> torch.Tensor:
|
||||
offsets = [0]
|
||||
for m in m_per_expert:
|
||||
offsets.append(offsets[-1] + m)
|
||||
return torch.tensor(offsets, dtype=torch.int32, device=device)
|
||||
|
||||
|
||||
def _build_blockscale_offsets(
|
||||
m_per_expert: list[int], device: torch.device
|
||||
) -> torch.Tensor:
|
||||
offsets = [0]
|
||||
for m in m_per_expert:
|
||||
offsets.append(offsets[-1] + _round_up(m, 128))
|
||||
return torch.tensor(offsets, dtype=torch.int32, device=device)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(), reason="NVFP4 requires compute capability >= 10.0"
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
def test_nvfp4_blockwise_moe_grouped_mm(dtype: torch.dtype) -> None:
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda")
|
||||
|
||||
num_experts = 4
|
||||
m_per_expert = [33, 17, 48, 29]
|
||||
n = 256
|
||||
k = 128
|
||||
|
||||
expert_offsets_full = _build_expert_offsets(m_per_expert, device)
|
||||
blockscale_offsets_full = _build_blockscale_offsets(m_per_expert, device)
|
||||
|
||||
total_m = int(expert_offsets_full[-1].item())
|
||||
a = torch.randn((total_m, k), device=device, dtype=dtype) * 0.1
|
||||
b = torch.randn((num_experts, n, k), device=device, dtype=dtype) * 0.1
|
||||
|
||||
a_global_scale = torch.empty((num_experts,), device=device, dtype=torch.float32)
|
||||
for i in range(num_experts):
|
||||
start = int(expert_offsets_full[i].item())
|
||||
end = int(expert_offsets_full[i + 1].item())
|
||||
amax = a[start:end].abs().max().to(torch.float32)
|
||||
a_global_scale[i] = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / amax
|
||||
|
||||
b_global_scale = torch.empty((num_experts,), device=device, dtype=torch.float32)
|
||||
for i in range(num_experts):
|
||||
bmax = b[i].abs().max().to(torch.float32)
|
||||
b_global_scale[i] = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / bmax
|
||||
|
||||
a_fp4, a_blockscale = scaled_fp4_experts_quant(
|
||||
a,
|
||||
a_global_scale,
|
||||
expert_offsets_full,
|
||||
blockscale_offsets_full,
|
||||
topk=1,
|
||||
)
|
||||
|
||||
b_fp4 = torch.empty((num_experts, n, k // 2), device=device, dtype=torch.uint8)
|
||||
b_blockscale = torch.empty(
|
||||
(num_experts, _round_up(n, 128), _round_up(k // 16, 4)),
|
||||
device=device,
|
||||
dtype=torch.float8_e4m3fn,
|
||||
)
|
||||
for i in range(num_experts):
|
||||
b_fp4_i, b_scale_i = scaled_fp4_quant(b[i], b_global_scale[i])
|
||||
b_fp4[i].copy_(b_fp4_i)
|
||||
b_blockscale[i].copy_(b_scale_i)
|
||||
|
||||
alphas = (1.0 / (a_global_scale * b_global_scale)).to(torch.float32)
|
||||
|
||||
params = {
|
||||
"ab_strides": torch.full((num_experts,), k, dtype=torch.int64, device=device),
|
||||
"c_strides": torch.full((num_experts,), n, dtype=torch.int64, device=device),
|
||||
"problem_sizes": torch.tensor(
|
||||
[[m, n, k] for m in m_per_expert], dtype=torch.int32, device=device
|
||||
),
|
||||
"expert_offsets": expert_offsets_full[:-1].contiguous(),
|
||||
"blockscale_offsets": blockscale_offsets_full[:-1].contiguous(),
|
||||
"a_ptrs": torch.empty((num_experts,), dtype=torch.int64, device=device),
|
||||
"b_ptrs": torch.empty((num_experts,), dtype=torch.int64, device=device),
|
||||
"out_ptrs": torch.empty((num_experts,), dtype=torch.int64, device=device),
|
||||
"a_scales_ptrs": torch.empty((num_experts,), dtype=torch.int64, device=device),
|
||||
"b_scales_ptrs": torch.empty((num_experts,), dtype=torch.int64, device=device),
|
||||
"alpha_ptrs": torch.empty((num_experts,), dtype=torch.int64, device=device),
|
||||
"layout_sfa": torch.empty((num_experts, 5), dtype=torch.int64, device=device),
|
||||
"layout_sfb": torch.empty((num_experts, 5), dtype=torch.int64, device=device),
|
||||
}
|
||||
|
||||
out = cutlass_fp4_group_mm(
|
||||
a_fp4,
|
||||
b_fp4,
|
||||
a_blockscale,
|
||||
b_blockscale,
|
||||
alphas,
|
||||
dtype,
|
||||
params,
|
||||
)
|
||||
|
||||
ref = torch.empty((total_m, n), device=device, dtype=dtype)
|
||||
for i in range(num_experts):
|
||||
start = int(expert_offsets_full[i].item())
|
||||
end = int(expert_offsets_full[i + 1].item())
|
||||
ref[start:end] = torch.matmul(a[start:end], b[i].t())
|
||||
|
||||
torch.testing.assert_close(out, ref, atol=1e-1, rtol=1e-1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,152 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.nvfp4 import cutlass_scaled_fp4_mm, scaled_fp4_quant
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=5, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def _nvfp4_supported() -> bool:
|
||||
return torch.cuda.is_available() and torch.cuda.get_device_capability() >= (10, 0)
|
||||
|
||||
|
||||
DTYPES = [torch.float16, torch.bfloat16]
|
||||
SHAPES = [
|
||||
(128, 128, 64),
|
||||
(128, 128, 128),
|
||||
(256, 128, 64),
|
||||
(128, 256, 128),
|
||||
(150, 128, 64),
|
||||
]
|
||||
|
||||
FLOAT4_E2M1_MAX = 6.0
|
||||
FLOAT8_E4M3_MAX = torch.finfo(torch.float8_e4m3fn).max
|
||||
|
||||
K_E2M1_TO_FLOAT = [
|
||||
0.0,
|
||||
0.5,
|
||||
1.0,
|
||||
1.5,
|
||||
2.0,
|
||||
3.0,
|
||||
4.0,
|
||||
6.0,
|
||||
]
|
||||
|
||||
|
||||
def e2m1_to_fp32(int4_value: int) -> float:
|
||||
sign_bit = int4_value & 0x8
|
||||
int4_abs_value = int4_value & 0x7
|
||||
float_result = K_E2M1_TO_FLOAT[int4_abs_value]
|
||||
return -float_result if sign_bit else float_result
|
||||
|
||||
|
||||
def break_fp4_bytes(a: torch.Tensor) -> torch.Tensor:
|
||||
assert a.dtype == torch.uint8
|
||||
m, n = a.shape
|
||||
a = a.flatten()
|
||||
high_half_byte = (a & 0xF0) >> 4
|
||||
low_half_byte = a & 0x0F
|
||||
f_h = torch.tensor([e2m1_to_fp32(x) for x in high_half_byte], device=a.device)
|
||||
f_l = torch.tensor([e2m1_to_fp32(x) for x in low_half_byte], device=a.device)
|
||||
return torch.stack((f_l, f_h), dim=-1).reshape(m, n * 2)
|
||||
|
||||
|
||||
def convert_swizzled_to_linear(
|
||||
a_sf_swizzled: torch.Tensor, m: int, k: int, block_size: int
|
||||
) -> torch.Tensor:
|
||||
sf_m, sf_k = a_sf_swizzled.shape
|
||||
del sf_m, sf_k
|
||||
m_tiles = (m + 128 - 1) // 128
|
||||
f = block_size * 4
|
||||
k_tiles = (k + f - 1) // f
|
||||
tmp = torch.reshape(a_sf_swizzled, (1, m_tiles, k_tiles, 32, 4, 4))
|
||||
tmp = torch.permute(tmp, (0, 1, 4, 3, 2, 5))
|
||||
out = tmp.reshape(m_tiles * 128, k_tiles * f // block_size)
|
||||
return out[0:m, 0 : k // block_size]
|
||||
|
||||
|
||||
def dequantize_to_dtype(
|
||||
tensor_fp4: torch.Tensor,
|
||||
tensor_sf: torch.Tensor,
|
||||
global_scale: torch.Tensor,
|
||||
block_size: int = 16,
|
||||
) -> torch.Tensor:
|
||||
assert tensor_fp4.dtype == torch.uint8
|
||||
m, packed_k = tensor_fp4.shape
|
||||
k = packed_k * 2
|
||||
tensor_f32 = break_fp4_bytes(tensor_fp4)
|
||||
tensor_f32 = tensor_f32.reshape(m, k // block_size, block_size)
|
||||
tensor_sf = tensor_sf.view(torch.float8_e4m3fn)
|
||||
tensor_sf = convert_swizzled_to_linear(tensor_sf, m, k, block_size)
|
||||
tensor_sf_dtype = tensor_sf.to(torch.float32) / global_scale
|
||||
return (tensor_f32 * tensor_sf_dtype.unsqueeze(-1)).reshape(m, k)
|
||||
|
||||
|
||||
def get_ref_results(
|
||||
a_fp4: torch.Tensor,
|
||||
b_fp4: torch.Tensor,
|
||||
a_sf: torch.Tensor,
|
||||
b_sf: torch.Tensor,
|
||||
a_global_scale: torch.Tensor,
|
||||
b_global_scale: torch.Tensor,
|
||||
block_size: int,
|
||||
) -> torch.Tensor:
|
||||
a_in_dtype = dequantize_to_dtype(a_fp4, a_sf, a_global_scale, block_size=block_size)
|
||||
b_in_dtype = dequantize_to_dtype(b_fp4, b_sf, b_global_scale, block_size=block_size)
|
||||
return torch.matmul(a_in_dtype, b_in_dtype.t())
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(), reason="NVFP4 requires compute capability >= 10.0"
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("shape", SHAPES)
|
||||
def test_nvfp4_gemm(dtype: torch.dtype, shape: tuple[int, int, int]) -> None:
|
||||
m, n, packed_k = shape
|
||||
k = packed_k * 2
|
||||
block_size = 16
|
||||
|
||||
a_dtype = torch.randn((m, k), dtype=dtype, device="cuda")
|
||||
b_dtype = torch.randn((n, k), dtype=dtype, device="cuda")
|
||||
|
||||
a_global_scale = (
|
||||
(FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX) / torch.amax(a_dtype.flatten(), dim=-1)
|
||||
).to(torch.float32)
|
||||
b_global_scale = (
|
||||
(FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX) / torch.amax(b_dtype.flatten(), dim=-1)
|
||||
).to(torch.float32)
|
||||
|
||||
alpha = 1.0 / (a_global_scale * b_global_scale)
|
||||
|
||||
a_fp4, a_scale_interleaved = scaled_fp4_quant(a_dtype, a_global_scale)
|
||||
b_fp4, b_scale_interleaved = scaled_fp4_quant(b_dtype, b_global_scale)
|
||||
|
||||
expected_out = get_ref_results(
|
||||
a_fp4,
|
||||
b_fp4,
|
||||
a_scale_interleaved,
|
||||
b_scale_interleaved,
|
||||
a_global_scale,
|
||||
b_global_scale,
|
||||
block_size,
|
||||
)
|
||||
|
||||
out = cutlass_scaled_fp4_mm(
|
||||
a_fp4,
|
||||
b_fp4,
|
||||
a_scale_interleaved,
|
||||
b_scale_interleaved,
|
||||
alpha,
|
||||
dtype,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(out, expected_out.to(dtype=dtype), atol=1e-1, rtol=1e-1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,225 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.nvfp4 import (
|
||||
scaled_fp4_grouped_quant,
|
||||
scaled_fp4_quant,
|
||||
silu_and_mul_scaled_fp4_grouped_quant,
|
||||
)
|
||||
|
||||
try:
|
||||
from sgl_kernel import silu_and_mul as _sgl_silu_and_mul
|
||||
except Exception:
|
||||
_sgl_silu_and_mul = None
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=5, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def _nvfp4_supported() -> bool:
|
||||
return torch.cuda.is_available() and torch.cuda.get_device_capability() >= (10, 0)
|
||||
|
||||
|
||||
def _silu_and_mul_reference(x: torch.Tensor) -> torch.Tensor:
|
||||
if _sgl_silu_and_mul is not None:
|
||||
return _sgl_silu_and_mul(x)
|
||||
k = x.shape[-1] // 2
|
||||
return torch.nn.functional.silu(x[:, :, :k]) * x[:, :, k:]
|
||||
|
||||
|
||||
DTYPES = [torch.float16, torch.bfloat16]
|
||||
SHAPES = [(128, 64), (128, 128), (256, 64), (256, 128)]
|
||||
PAD_SHAPES = [
|
||||
(90, 64),
|
||||
(150, 64),
|
||||
(128, 48),
|
||||
(128, 80),
|
||||
]
|
||||
|
||||
FLOAT4_E2M1_MAX = 6.0
|
||||
FLOAT8_E4M3_MAX = torch.finfo(torch.float8_e4m3fn).max
|
||||
BLOCK_SIZE = 16
|
||||
|
||||
E2M1_TO_FLOAT32 = [
|
||||
0.0,
|
||||
0.5,
|
||||
1.0,
|
||||
1.5,
|
||||
2.0,
|
||||
3.0,
|
||||
4.0,
|
||||
6.0,
|
||||
0.0,
|
||||
-0.5,
|
||||
-1.0,
|
||||
-1.5,
|
||||
-2.0,
|
||||
-3.0,
|
||||
-4.0,
|
||||
-6.0,
|
||||
]
|
||||
|
||||
|
||||
def cast_from_fp4(x: torch.Tensor, m: int, n: int) -> torch.Tensor:
|
||||
v_2nd = (x & 0xF).to(torch.long)
|
||||
v_1st = ((x >> 4) & 0xF).to(torch.long)
|
||||
c = torch.stack((v_2nd, v_1st), dim=-1).flatten()
|
||||
lut = torch.tensor(E2M1_TO_FLOAT32, device=x.device, dtype=torch.float32)
|
||||
return lut[c].reshape(m, n)
|
||||
|
||||
|
||||
def cast_to_fp4(x: torch.Tensor) -> torch.Tensor:
|
||||
sign = torch.sign(x)
|
||||
x = torch.abs(x)
|
||||
x[(x >= 0.0) & (x <= 0.25)] = 0.0
|
||||
x[(x > 0.25) & (x < 0.75)] = 0.5
|
||||
x[(x >= 0.75) & (x <= 1.25)] = 1.0
|
||||
x[(x > 1.25) & (x < 1.75)] = 1.5
|
||||
x[(x >= 1.75) & (x <= 2.5)] = 2.0
|
||||
x[(x > 2.5) & (x < 3.5)] = 3.0
|
||||
x[(x >= 3.5) & (x <= 5.0)] = 4.0
|
||||
x[x > 5.0] = 6.0
|
||||
return x * sign
|
||||
|
||||
|
||||
def get_reciprocal(x):
|
||||
if isinstance(x, torch.Tensor):
|
||||
return torch.where(x == 0, torch.tensor(0.0, dtype=x.dtype), 1.0 / x)
|
||||
return 0.0 if x == 0 else 1.0 / x
|
||||
|
||||
|
||||
def ref_nvfp4_quant(x: torch.Tensor, global_scale: torch.Tensor):
|
||||
assert global_scale.dtype == torch.float32
|
||||
assert x.ndim == 2
|
||||
m, n = x.shape
|
||||
x = torch.reshape(x, (m, n // BLOCK_SIZE, BLOCK_SIZE))
|
||||
vec_max = torch.max(torch.abs(x), dim=-1, keepdim=True)[0].to(torch.float32)
|
||||
scale = global_scale * (vec_max * get_reciprocal(FLOAT4_E2M1_MAX))
|
||||
scale = scale.to(torch.float8_e4m3fn).to(torch.float32)
|
||||
output_scale = get_reciprocal(scale * get_reciprocal(global_scale))
|
||||
|
||||
scaled_x = x.to(torch.float32) * output_scale
|
||||
clipped_x = torch.clamp(scaled_x, -6.0, 6.0).reshape(m, n)
|
||||
return cast_to_fp4(clipped_x), scale.squeeze(-1)
|
||||
|
||||
|
||||
def recover_swizzled_scales(scale: torch.Tensor, m: int, n: int) -> torch.Tensor:
|
||||
rounded_m = ((m + 128 - 1) // 128) * 128
|
||||
scale_n = n // BLOCK_SIZE
|
||||
rounded_n = ((scale_n + 4 - 1) // 4) * 4
|
||||
tmp = torch.reshape(scale, (1, rounded_m // 128, rounded_n // 4, 32, 4, 4))
|
||||
tmp = torch.permute(tmp, (0, 1, 4, 3, 2, 5))
|
||||
result = torch.reshape(tmp, (rounded_m, rounded_n)).to(torch.float32)
|
||||
return result[:m, :scale_n]
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(), reason="NVFP4 requires compute capability >= 10.0"
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("shape", SHAPES)
|
||||
def test_quantize_to_fp4(dtype: torch.dtype, shape: tuple[int, int]) -> None:
|
||||
torch.manual_seed(42)
|
||||
m, n = shape
|
||||
|
||||
x = torch.randn((m, n), dtype=dtype, device="cuda")
|
||||
tensor_amax = torch.abs(x).max().to(torch.float32)
|
||||
global_scale = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / tensor_amax
|
||||
out_ref, scale_ref = ref_nvfp4_quant(x, global_scale)
|
||||
|
||||
out, out_scale = scaled_fp4_quant(x, global_scale)
|
||||
scale_ans = recover_swizzled_scales(out_scale, m, n)
|
||||
out_ans = cast_from_fp4(out, m, n)
|
||||
|
||||
torch.testing.assert_close(out_ans, out_ref)
|
||||
torch.testing.assert_close(scale_ans, scale_ref)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(), reason="NVFP4 requires compute capability >= 10.0"
|
||||
)
|
||||
@pytest.mark.parametrize("shape", PAD_SHAPES)
|
||||
def test_quantize_to_fp4_padded(shape: tuple[int, int]) -> None:
|
||||
torch.manual_seed(42)
|
||||
m, n = shape
|
||||
x = torch.randn((m, n), dtype=torch.float16, device="cuda")
|
||||
|
||||
tensor_amax = torch.abs(x).max().to(torch.float32)
|
||||
global_scale = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / tensor_amax
|
||||
out_ref, scale_ref = ref_nvfp4_quant(x, global_scale)
|
||||
|
||||
out, out_scale = scaled_fp4_quant(x, global_scale)
|
||||
scale_ans = recover_swizzled_scales(out_scale, m, n)
|
||||
out_ans = cast_from_fp4(out, m, n)
|
||||
|
||||
torch.testing.assert_close(out_ans, out_ref)
|
||||
torch.testing.assert_close(scale_ans, scale_ref)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(), reason="NVFP4 requires compute capability >= 10.0"
|
||||
)
|
||||
@pytest.mark.parametrize("shape", [(2, 128, 512), (2, 100, 128)])
|
||||
def test_quantize_to_fp4_grouped(shape: tuple[int, int, int]) -> None:
|
||||
torch.manual_seed(42)
|
||||
l, m, k = shape
|
||||
|
||||
x = torch.randn((l, m, k), dtype=torch.bfloat16, device="cuda")
|
||||
mask = torch.randint(1, max(2, m // 2), (l,), dtype=torch.int32, device="cuda")
|
||||
tensor_amax = x.abs().amax(dim=(1, 2)).to(torch.float32)
|
||||
x_sf_global = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / tensor_amax
|
||||
|
||||
output, output_scales = scaled_fp4_grouped_quant(x, x_sf_global, mask)
|
||||
output = output.permute(2, 0, 1)
|
||||
padded_m = ((m + 128 - 1) // 128) * 128
|
||||
output_scales = output_scales.permute(5, 2, 4, 0, 1, 3).view(l, padded_m, -1)
|
||||
|
||||
for i in range(l):
|
||||
a_fp4, a_scale_interleaved = scaled_fp4_quant(x[i], x_sf_global[i])
|
||||
torch.testing.assert_close(a_fp4[: mask[i]], output[i][: mask[i]])
|
||||
scale_ref = recover_swizzled_scales(a_scale_interleaved, m, k)
|
||||
scale_ans = recover_swizzled_scales(output_scales[i], m, k)
|
||||
torch.testing.assert_close(scale_ref[: mask[i]], scale_ans[: mask[i]])
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _nvfp4_supported(), reason="NVFP4 requires compute capability >= 10.0"
|
||||
)
|
||||
@pytest.mark.parametrize("shape", [(4, 96, 256), (8, 128, 512)])
|
||||
def test_silu_and_mul_quantize_to_fp4_grouped(shape: tuple[int, int, int]) -> None:
|
||||
torch.manual_seed(42)
|
||||
l, m, k = shape
|
||||
|
||||
x = torch.randn((l, m, k * 2), dtype=torch.bfloat16, device="cuda")
|
||||
mask = torch.randint(1, max(2, m // 2), (l,), dtype=torch.int32, device="cuda")
|
||||
|
||||
ref_y = _silu_and_mul_reference(x)
|
||||
|
||||
tensor_amax = ref_y.abs().amax(dim=(1, 2)).to(torch.float32)
|
||||
y_sf_global = FLOAT8_E4M3_MAX * FLOAT4_E2M1_MAX / tensor_amax
|
||||
|
||||
ref_output, ref_output_scales = scaled_fp4_grouped_quant(ref_y, y_sf_global, mask)
|
||||
output, output_scales = silu_and_mul_scaled_fp4_grouped_quant(x, y_sf_global, mask)
|
||||
|
||||
output = output.permute(2, 0, 1)
|
||||
ref_output = ref_output.permute(2, 0, 1)
|
||||
|
||||
padded_m = ((m + 128 - 1) // 128) * 128
|
||||
output_scales = output_scales.permute(5, 2, 4, 0, 1, 3).view(l, padded_m, -1)
|
||||
ref_output_scales = ref_output_scales.permute(5, 2, 4, 0, 1, 3).view(
|
||||
l, padded_m, -1
|
||||
)
|
||||
|
||||
for i in range(l):
|
||||
torch.testing.assert_close(ref_output[i, : mask[i]], output[i, : mask[i]])
|
||||
scale_ref = recover_swizzled_scales(ref_output_scales[i], m, k)
|
||||
scale_ans = recover_swizzled_scales(output_scales[i], m, k)
|
||||
torch.testing.assert_close(scale_ref[: mask[i]], scale_ans[: mask[i]])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,91 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.per_tensor_quant_fp8 import per_tensor_quant_fp8
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=16, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
try:
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
_is_hip = is_hip()
|
||||
except ImportError:
|
||||
_is_hip = False
|
||||
|
||||
fp8_type_ = torch.float8_e4m3fnuz if _is_hip else torch.float8_e4m3fn
|
||||
|
||||
|
||||
def sglang_scaled_fp8_quant(
|
||||
input: torch.Tensor,
|
||||
scale: Optional[torch.Tensor] = None,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
fp8_type_: torch.dtype = torch.float8_e4m3fn
|
||||
output = torch.empty_like(input, device=input.device, dtype=fp8_type_)
|
||||
is_static = True
|
||||
if scale is None:
|
||||
scale = torch.zeros(1, device=input.device, dtype=torch.float32)
|
||||
is_static = False
|
||||
per_tensor_quant_fp8(input, output, scale, is_static)
|
||||
|
||||
return output, scale
|
||||
|
||||
|
||||
def torch_scaled_fp8_quant(tensor, inv_scale):
|
||||
finfo = torch.finfo(torch.float8_e4m3fn)
|
||||
scale = inv_scale.reciprocal()
|
||||
qweight = (tensor.to(torch.float32) * scale).clamp(min=finfo.min, max=finfo.max)
|
||||
qweight = qweight.to(torch.float8_e4m3fn)
|
||||
return qweight
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens,hidden_dim",
|
||||
list(itertools.product([128, 256, 512], [512, 2048, 4096])),
|
||||
)
|
||||
def test_jit_per_tensor_quant_compare_implementations(
|
||||
num_tokens: int,
|
||||
hidden_dim: int,
|
||||
):
|
||||
device = torch.device("cuda")
|
||||
x = torch.rand((num_tokens, hidden_dim), dtype=torch.float16, device=device)
|
||||
|
||||
sglang_out, sglang_scale = sglang_scaled_fp8_quant(x)
|
||||
torch_out = torch_scaled_fp8_quant(x, sglang_scale)
|
||||
|
||||
torch.testing.assert_close(
|
||||
sglang_out.float(), torch_out.float(), rtol=1e-3, atol=1e-3
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(4, 8, 64), (2, 16, 128), (19260817, 1, 1)])
|
||||
def test_jit_per_tensor_quant_supports_3d(shape):
|
||||
device = torch.device("cuda")
|
||||
x = torch.rand(shape, dtype=torch.bfloat16, device=device)
|
||||
out = torch.empty_like(x, device=x.device, dtype=fp8_type_)
|
||||
scale = torch.zeros(1, device=x.device, dtype=torch.float32)
|
||||
|
||||
per_tensor_quant_fp8(x, out, scale, is_static=False)
|
||||
|
||||
x_2d = x.flatten(0, -2)
|
||||
out_ref_2d = torch_scaled_fp8_quant(x_2d, scale)
|
||||
out_ref = out_ref_2d.reshape(shape)
|
||||
|
||||
torch.testing.assert_close(out.float(), out_ref.float(), rtol=1e-3, atol=1e-3)
|
||||
|
||||
scale = torch.rand(1, dtype=torch.float32, device=device)
|
||||
sglang_out, _ = sglang_scaled_fp8_quant(x, scale)
|
||||
torch_out = torch_scaled_fp8_quant(x, scale)
|
||||
|
||||
torch.testing.assert_close(
|
||||
sglang_out.float(), torch_out.float(), rtol=1e-3, atol=1e-3
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,210 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
_is_hip = is_hip()
|
||||
fp8_type_ = torch.float8_e4m3fnuz if _is_hip else torch.float8_e4m3fn
|
||||
|
||||
from sgl_kernel.test_utils import (
|
||||
assert_all_close_or_tiny_diff,
|
||||
create_per_token_group_quant_test_data,
|
||||
)
|
||||
|
||||
from sglang.jit_kernel.per_token_group_quant_8bit import (
|
||||
per_token_group_quant_8bit as sglang_per_token_group_quant_8bit,
|
||||
)
|
||||
from sglang.srt.layers.quantization.fp8_kernel import (
|
||||
create_per_token_group_quant_fp8_output_scale,
|
||||
)
|
||||
from sglang.srt.layers.quantization.fp8_kernel import (
|
||||
per_token_group_quant_8bit as triton_per_token_group_quant_8bit,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=16, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
configs = list(
|
||||
itertools.product(
|
||||
[1, 4, 16, 64, 127, 128, 512, 1024, 4096, 8192], # num_tokens
|
||||
[128, 256, 384, 512, 1024, 1536, 1664, 2048, 4096, 7168, 16384], # hidden_dim
|
||||
[16, 32, 64, 128], # group_size
|
||||
[None], # num_ranks
|
||||
[fp8_type_], # dtype
|
||||
[
|
||||
dict(
|
||||
column_major_scales=False,
|
||||
scale_tma_aligned=False,
|
||||
scale_ue8m0=False,
|
||||
fuse_silu_and_mul=False,
|
||||
masked_layout_mode=None,
|
||||
),
|
||||
dict(
|
||||
column_major_scales=True,
|
||||
scale_tma_aligned=False,
|
||||
scale_ue8m0=False,
|
||||
fuse_silu_and_mul=False,
|
||||
masked_layout_mode=None,
|
||||
),
|
||||
dict(
|
||||
column_major_scales=True,
|
||||
scale_tma_aligned=True,
|
||||
scale_ue8m0=False,
|
||||
fuse_silu_and_mul=False,
|
||||
masked_layout_mode=None,
|
||||
),
|
||||
dict(
|
||||
column_major_scales=True,
|
||||
scale_tma_aligned=True,
|
||||
scale_ue8m0=True,
|
||||
fuse_silu_and_mul=False,
|
||||
masked_layout_mode=None,
|
||||
),
|
||||
],
|
||||
)
|
||||
) + list(
|
||||
itertools.product(
|
||||
[1, 4, 1 * 8, 4 * 8, 64 * 8, 256 * 8, 768 * 8],
|
||||
[2048],
|
||||
[128],
|
||||
[8, 16, 32, 48],
|
||||
[fp8_type_],
|
||||
[
|
||||
dict(
|
||||
column_major_scales=True,
|
||||
scale_tma_aligned=True,
|
||||
scale_ue8m0=True,
|
||||
fuse_silu_and_mul=True,
|
||||
masked_layout_mode=None,
|
||||
),
|
||||
dict(
|
||||
column_major_scales=True,
|
||||
scale_tma_aligned=True,
|
||||
scale_ue8m0=True,
|
||||
fuse_silu_and_mul=True,
|
||||
masked_layout_mode="balanced",
|
||||
),
|
||||
dict(
|
||||
column_major_scales=True,
|
||||
scale_tma_aligned=True,
|
||||
scale_ue8m0=True,
|
||||
fuse_silu_and_mul=True,
|
||||
masked_layout_mode="imbalanced",
|
||||
),
|
||||
dict(
|
||||
column_major_scales=True,
|
||||
scale_tma_aligned=True,
|
||||
scale_ue8m0=True,
|
||||
fuse_silu_and_mul=True,
|
||||
masked_layout_mode="extreme",
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens, hidden_dim, group_size, num_ranks, dst_dtype, flags", configs
|
||||
)
|
||||
def test_per_token_group_quant_with_column_major(
|
||||
num_tokens,
|
||||
hidden_dim,
|
||||
group_size,
|
||||
num_ranks,
|
||||
dst_dtype,
|
||||
flags,
|
||||
):
|
||||
arch_major, _ = torch.cuda.get_device_capability(torch.cuda.current_device())
|
||||
if flags["scale_ue8m0"] and (arch_major <= 9):
|
||||
pytest.skip("Only Blackwell need ue8m0 fusion")
|
||||
return
|
||||
|
||||
if (flags["scale_ue8m0"] and (group_size != 128)) or (
|
||||
(dst_dtype == torch.int8) and flags["column_major_scales"]
|
||||
):
|
||||
pytest.skip()
|
||||
return
|
||||
|
||||
x, masked_m = create_per_token_group_quant_test_data(
|
||||
num_tokens=num_tokens, hidden_dim=hidden_dim, num_ranks=num_ranks, flags=flags
|
||||
)
|
||||
|
||||
execute_kwargs = dict(
|
||||
x=x,
|
||||
masked_m=masked_m,
|
||||
group_size=group_size,
|
||||
eps=1e-10,
|
||||
dst_dtype=dst_dtype,
|
||||
**{k: v for k, v in flags.items() if k not in ["masked_layout_mode"]},
|
||||
)
|
||||
|
||||
def _postprocess(x_q, x_s):
|
||||
if masked_m is not None:
|
||||
print(f"Mask tokens after {masked_m} to be zero")
|
||||
for i in range(len(masked_m)):
|
||||
x_q[i, masked_m[i] :, :] = 0
|
||||
x_s[i, masked_m[i] :, :] = 0
|
||||
return x_q, x_s
|
||||
|
||||
x_q_triton, x_s_triton = _postprocess(
|
||||
*triton_per_token_group_quant_8bit(**execute_kwargs)
|
||||
)
|
||||
|
||||
fuse_silu_and_mul = False
|
||||
out_shape = (*x.shape[:-1], x.shape[-1] // (2 if fuse_silu_and_mul else 1))
|
||||
|
||||
fp8_dtype = torch.float8_e4m3fn
|
||||
fp8_max = torch.finfo(fp8_dtype).max
|
||||
fp8_min = -fp8_max
|
||||
x_q = torch.empty(out_shape, device=x.device, dtype=fp8_dtype)
|
||||
x_s = create_per_token_group_quant_fp8_output_scale(
|
||||
x_shape=out_shape,
|
||||
device=x.device,
|
||||
group_size=group_size,
|
||||
column_major_scales=False,
|
||||
scale_tma_aligned=False,
|
||||
scale_ue8m0=False,
|
||||
)
|
||||
|
||||
execute_kwargs = dict(
|
||||
input=x,
|
||||
output_q=x_q,
|
||||
output_s=x_s,
|
||||
group_size=group_size,
|
||||
eps=1e-10,
|
||||
fp8_max=fp8_max,
|
||||
fp8_min=fp8_min,
|
||||
)
|
||||
x_q_sglang, x_s_sglang = _postprocess(
|
||||
*sglang_per_token_group_quant_8bit(**execute_kwargs)
|
||||
)
|
||||
|
||||
try:
|
||||
assert_all_close_or_tiny_diff(x_q_triton, x_q_sglang)
|
||||
torch.testing.assert_close(
|
||||
x_s_triton.contiguous(),
|
||||
x_s_sglang.contiguous(),
|
||||
rtol=1e-3,
|
||||
atol=1e-5,
|
||||
msg=lambda message: message + f" {x_s_triton=} {x_s_sglang=}",
|
||||
)
|
||||
except AssertionError:
|
||||
print(
|
||||
f"{x.shape=} {x_q_triton.shape=} {x_s_triton.shape=} {x_q_sglang.shape=} {x_s_sglang.shape=}"
|
||||
)
|
||||
print(f"{x=}")
|
||||
print(f"{masked_m=}")
|
||||
print(f"{x_q_triton=}")
|
||||
print(f"{x_s_triton=}")
|
||||
print(f"{x_q_sglang=}")
|
||||
print(f"{x_s_sglang=}")
|
||||
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,494 +0,0 @@
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.jit_kernel.rope import rotary_embedding
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=18, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def burn_kernel(out_ptr, iters: tl.constexpr):
|
||||
pid = tl.program_id(0)
|
||||
x = tl.full((), pid + 1, dtype=tl.uint32)
|
||||
|
||||
a = tl.full((), 1664525, dtype=tl.uint32)
|
||||
c = tl.full((), 1013904223, dtype=tl.uint32)
|
||||
sh = tl.full((), 13, dtype=tl.uint32)
|
||||
|
||||
for _ in range(iters):
|
||||
x = x * a + c
|
||||
x = x ^ (x >> sh)
|
||||
|
||||
if pid == 0:
|
||||
tl.store(out_ptr, x)
|
||||
|
||||
|
||||
def triton_burn(ms: float, grid=(256,)):
|
||||
iters = int(ms * 20000)
|
||||
out = torch.empty((), device="cuda", dtype=torch.uint32)
|
||||
burn_kernel[grid](out, iters=iters)
|
||||
return out
|
||||
|
||||
|
||||
def create_test_inputs(
|
||||
head_size, batch_size, seq_len, device, dtype, num_q_heads, num_kv_heads
|
||||
):
|
||||
"""Create test inputs."""
|
||||
total_tokens = batch_size * seq_len
|
||||
|
||||
query = torch.randn(
|
||||
batch_size, seq_len, num_q_heads, head_size, dtype=dtype, device=device
|
||||
)
|
||||
key = torch.randn(
|
||||
batch_size, seq_len, num_kv_heads, head_size, dtype=dtype, device=device
|
||||
)
|
||||
|
||||
pos_ids = torch.randint(
|
||||
0, min(seq_len * 2, 100), (total_tokens,), dtype=torch.long, device=device
|
||||
)
|
||||
|
||||
query = query.view(total_tokens, num_q_heads, head_size)
|
||||
key = key.view(total_tokens, num_kv_heads, head_size)
|
||||
|
||||
return query, key, pos_ids
|
||||
|
||||
|
||||
def create_cos_sin_cache(rotary_dim, max_position_embeddings, base, dtype, device):
|
||||
"""Create cos/sin cache for rotary embedding."""
|
||||
max_pos = max_position_embeddings
|
||||
extended_max_pos = max(max_pos, 100)
|
||||
cos_sin_cache = torch.zeros(
|
||||
extended_max_pos, rotary_dim, dtype=dtype, device=device
|
||||
)
|
||||
|
||||
inv_freq = 1.0 / (
|
||||
base
|
||||
** (
|
||||
torch.arange(0, rotary_dim, 2, dtype=torch.float32, device=device)
|
||||
/ rotary_dim
|
||||
)
|
||||
)
|
||||
t = torch.arange(extended_max_pos, dtype=torch.float32, device=device)
|
||||
freqs = torch.outer(t, inv_freq)
|
||||
cos_cache = torch.cos(freqs).to(dtype)
|
||||
sin_cache = torch.sin(freqs).to(dtype)
|
||||
|
||||
cos_sin_cache[:, : rotary_dim // 2] = cos_cache
|
||||
cos_sin_cache[:, rotary_dim // 2 :] = sin_cache
|
||||
|
||||
return cos_sin_cache
|
||||
|
||||
|
||||
# vLLM torch native
|
||||
def _apply_rotary_emb(
|
||||
x: torch.Tensor,
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
is_neox_style: bool,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Args:
|
||||
x: [num_tokens, num_heads, head_size]
|
||||
cos: [num_tokens, head_size // 2]
|
||||
sin: [num_tokens, head_size // 2]
|
||||
is_neox_style: Whether to use the Neox-style or GPT-J-style rotary
|
||||
positional embeddings.
|
||||
"""
|
||||
cos = cos.unsqueeze(-2).to(x.dtype)
|
||||
sin = sin.unsqueeze(-2).to(x.dtype)
|
||||
if is_neox_style:
|
||||
x1, x2 = torch.chunk(x, 2, dim=-1)
|
||||
else:
|
||||
x1 = x[..., ::2]
|
||||
x2 = x[..., 1::2]
|
||||
o1 = x1 * cos - x2 * sin
|
||||
o2 = x2 * cos + x1 * sin
|
||||
if is_neox_style:
|
||||
return torch.cat((o1, o2), dim=-1)
|
||||
else:
|
||||
return torch.stack((o1, o2), dim=-1).flatten(-2)
|
||||
|
||||
|
||||
class RotaryEmbedding(torch.nn.Module):
|
||||
# Reference: https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/layers/rotary_embedding.py
|
||||
def __init__(
|
||||
self,
|
||||
head_size: int,
|
||||
rotary_dim: int,
|
||||
max_position_embeddings: int,
|
||||
base: int,
|
||||
is_neox_style: bool,
|
||||
dtype: torch.dtype,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.head_size = head_size
|
||||
self.rotary_dim = rotary_dim
|
||||
self.max_position_embeddings = max_position_embeddings
|
||||
self.base = base
|
||||
self.is_neox_style = is_neox_style
|
||||
self.dtype = dtype
|
||||
|
||||
cache = self._compute_cos_sin_cache()
|
||||
self.cos_sin_cache: torch.Tensor
|
||||
self.register_buffer("cos_sin_cache", cache, persistent=False)
|
||||
|
||||
def _compute_inv_freq(self, base: Union[int, float]) -> torch.Tensor:
|
||||
inv_freq = 1.0 / (
|
||||
base
|
||||
** (
|
||||
torch.arange(0, self.rotary_dim, 2, dtype=torch.float) / self.rotary_dim
|
||||
)
|
||||
)
|
||||
return inv_freq
|
||||
|
||||
def _compute_cos_sin_cache(self) -> torch.Tensor:
|
||||
"""Compute the cos and sin cache."""
|
||||
inv_freq = self._compute_inv_freq(self.base)
|
||||
t = torch.arange(self.max_position_embeddings, dtype=torch.float)
|
||||
|
||||
freqs = torch.einsum("i,j -> ij", t, inv_freq)
|
||||
cos = freqs.cos()
|
||||
sin = freqs.sin()
|
||||
cache = torch.cat((cos, sin), dim=-1)
|
||||
return cache
|
||||
|
||||
def forward_native(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
query: torch.Tensor,
|
||||
key: Optional[torch.Tensor] = None,
|
||||
offsets: Optional[torch.Tensor] = None,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""A PyTorch-native implementation of forward()."""
|
||||
|
||||
if offsets is not None:
|
||||
positions = positions + offsets
|
||||
|
||||
positions = positions.flatten()
|
||||
num_tokens = positions.shape[0]
|
||||
cos_sin = self.cos_sin_cache.index_select(0, positions)
|
||||
|
||||
cos, sin = cos_sin.chunk(2, dim=-1)
|
||||
|
||||
query_shape = query.shape
|
||||
query = query.view(num_tokens, -1, self.head_size)
|
||||
query_rot = query[..., : self.rotary_dim]
|
||||
query_pass = query[..., self.rotary_dim :]
|
||||
query_rot = _apply_rotary_emb(query_rot, cos, sin, self.is_neox_style)
|
||||
query = torch.cat((query_rot, query_pass), dim=-1).reshape(query_shape)
|
||||
|
||||
# Modification: convert to the correct dtype
|
||||
query = query.to(self.dtype)
|
||||
|
||||
if key is not None:
|
||||
key_shape = key.shape
|
||||
key = key.view(num_tokens, -1, self.head_size)
|
||||
key_rot = key[..., : self.rotary_dim]
|
||||
key_pass = key[..., self.rotary_dim :]
|
||||
key_rot = _apply_rotary_emb(key_rot, cos, sin, self.is_neox_style)
|
||||
key = torch.cat((key_rot, key_pass), dim=-1).reshape(key_shape)
|
||||
|
||||
key = key.to(self.dtype)
|
||||
|
||||
return query, key
|
||||
|
||||
|
||||
def get_torch_rotary_embedding(
|
||||
head_size, rotary_dim, max_position_embeddings, base, is_neox_style, dtype, device
|
||||
):
|
||||
"""Initialize Torch Native RotaryEmbedding based on vLLM implementation."""
|
||||
return RotaryEmbedding(
|
||||
head_size=head_size,
|
||||
rotary_dim=rotary_dim,
|
||||
max_position_embeddings=max_position_embeddings,
|
||||
base=base,
|
||||
is_neox_style=is_neox_style,
|
||||
dtype=dtype,
|
||||
).to(device)
|
||||
|
||||
|
||||
def get_sgl_rotary_embedding(
|
||||
head_size, rotary_dim, max_position_embeddings, base, is_neox_style, dtype, device
|
||||
):
|
||||
"""Initialize SglKernelRotaryEmbedding."""
|
||||
try:
|
||||
from sgl_kernel.testing.rotary_embedding import SglKernelRotaryEmbedding
|
||||
except ImportError:
|
||||
pytest.skip(
|
||||
"SglKernelRotaryEmbedding is not available. Test case can be removed."
|
||||
)
|
||||
|
||||
return SglKernelRotaryEmbedding(
|
||||
head_size=head_size,
|
||||
rotary_dim=rotary_dim,
|
||||
max_position_embeddings=max_position_embeddings,
|
||||
base=base,
|
||||
is_neox_style=is_neox_style,
|
||||
dtype=dtype,
|
||||
).to(device)
|
||||
|
||||
|
||||
def compare_results(jit_out, sgl_out, dtype):
|
||||
"""Compare results between JIT and SGL implementations."""
|
||||
if jit_out is None:
|
||||
assert sgl_out is None
|
||||
return
|
||||
|
||||
assert sgl_out is not None
|
||||
|
||||
# Check for NaN values
|
||||
assert not torch.isnan(jit_out).any(), "NaN in JIT results"
|
||||
assert not torch.isnan(sgl_out).any(), "NaN in SGL results"
|
||||
|
||||
# Compare results
|
||||
atol = 4e-2 if dtype != torch.float32 else 1e-5
|
||||
rtol = 4e-2 if dtype != torch.float32 else 1e-5
|
||||
|
||||
torch.testing.assert_close(jit_out, sgl_out, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"head_size, rotary_dim, max_position_embeddings, base, is_neox_style, dtype, device, batch_size, seq_len, num_q_heads, num_kv_heads",
|
||||
[
|
||||
# GPT-OSS cases
|
||||
*[
|
||||
(64, 64, 4096, 8000, True, torch.bfloat16, "cuda", bs, sl, 8, 8)
|
||||
for bs, sl in [(1, 1), (32, 1), (128, 1), (512, 1), (2, 512), (4, 4096)]
|
||||
],
|
||||
# Other cases
|
||||
(64, 64, 32, 8000, True, torch.bfloat16, "cuda", 32, 32, 1, 1),
|
||||
(256, 128, 4096, 10000, True, torch.bfloat16, "cuda", 2, 512, 4, 2),
|
||||
(512, 128, 311, 10000, True, torch.bfloat16, "cuda", 3, 39, 4, 2),
|
||||
(128, 128, 2048, 10000, False, torch.bfloat16, "cuda", 2, 512, 32, 8),
|
||||
(128, 128, 2048, 10000, False, torch.bfloat16, "cuda", 2, 512, 16, 4),
|
||||
(512, 128, 311, 10000, False, torch.bfloat16, "cuda", 3, 39, 4, 2),
|
||||
(64, 64, 32, 8000, True, torch.float32, "cuda", 32, 32, 1, 1),
|
||||
(256, 128, 4096, 10000, True, torch.float32, "cuda", 2, 512, 4, 2),
|
||||
(512, 128, 311, 10000, True, torch.float32, "cuda", 3, 39, 4, 2),
|
||||
(128, 128, 2048, 10000, False, torch.float32, "cuda", 2, 512, 32, 8),
|
||||
(128, 128, 2048, 10000, False, torch.float32, "cuda", 2, 512, 16, 4),
|
||||
(512, 128, 311, 10000, False, torch.float32, "cuda", 3, 39, 4, 2),
|
||||
# Additional test cases for different head sizes and dtypes
|
||||
(64, 32, 1024, 10000, True, torch.float16, "cuda", 16, 64, 8, 4),
|
||||
(128, 64, 2048, 10000, True, torch.float16, "cuda", 8, 128, 16, 8),
|
||||
(256, 128, 4096, 10000, True, torch.float16, "cuda", 4, 256, 8, 4),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"key_is_none",
|
||||
[True, False],
|
||||
)
|
||||
def test_correctness(
|
||||
head_size,
|
||||
rotary_dim,
|
||||
max_position_embeddings,
|
||||
base,
|
||||
is_neox_style,
|
||||
dtype,
|
||||
device,
|
||||
batch_size,
|
||||
seq_len,
|
||||
num_q_heads,
|
||||
num_kv_heads,
|
||||
key_is_none,
|
||||
):
|
||||
"""Test correctness of JIT rotary embedding implementation."""
|
||||
# Create inputs and caches
|
||||
query, key, pos_ids = create_test_inputs(
|
||||
head_size, batch_size, seq_len, device, dtype, num_q_heads, num_kv_heads
|
||||
)
|
||||
cos_sin_cache = create_cos_sin_cache(
|
||||
rotary_dim, max_position_embeddings, base, dtype, device
|
||||
)
|
||||
|
||||
# Initialize torch kernel
|
||||
torch_rotary_emb = get_torch_rotary_embedding(
|
||||
head_size,
|
||||
rotary_dim,
|
||||
max_position_embeddings,
|
||||
base,
|
||||
is_neox_style,
|
||||
dtype,
|
||||
device,
|
||||
)
|
||||
torch_rotary_emb.cos_sin_cache = cos_sin_cache
|
||||
r = torch.randn_like(query)
|
||||
|
||||
# Apply rotary embeddings
|
||||
query_jit, key_jit = query.clone(), key.clone()
|
||||
query_torch, key_torch = query.clone(), key.clone()
|
||||
stream_jit = torch.get_device_module("cuda").Stream()
|
||||
stream_kernel = torch.get_device_module("cuda").Stream()
|
||||
|
||||
if key_is_none:
|
||||
key_jit = None
|
||||
key_torch = None
|
||||
triton_burn(100.0, grid=(1024,))
|
||||
|
||||
r_jit, r_torch = r.clone(), r.clone()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
with torch.cuda.stream(stream_jit):
|
||||
# Test if rotary_embedding runs on stream_jit
|
||||
triton_burn(100.0, grid=(1024,))
|
||||
query_jit = query_jit + r_jit
|
||||
query_jit_out, key_jit_out = rotary_embedding(
|
||||
positions=pos_ids,
|
||||
query=query_jit,
|
||||
key=key_jit,
|
||||
head_size=head_size,
|
||||
cos_sin_cache=cos_sin_cache,
|
||||
is_neox=is_neox_style,
|
||||
)
|
||||
|
||||
with torch.cuda.stream(stream_kernel):
|
||||
triton_burn(100.0, grid=(1024,))
|
||||
query_torch = query_torch + r_torch
|
||||
query_torch_out, key_torch_out = torch_rotary_emb.forward_native(
|
||||
positions=pos_ids, query=query_torch, key=key_torch
|
||||
)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
compare_results(query_jit_out, query_torch_out, dtype)
|
||||
compare_results(key_jit_out, key_torch_out, dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"head_size, rotary_dim, max_position_embeddings, base, is_neox_style, dtype, device, batch_size, seq_len, num_q_heads, num_kv_heads",
|
||||
[
|
||||
# Small scale
|
||||
(64, 64, 4096, 8000, True, torch.bfloat16, "cuda", 1, 1, 8, 8),
|
||||
(64, 64, 4096, 8000, True, torch.bfloat16, "cuda", 4, 16, 8, 8),
|
||||
# Medium scale
|
||||
(64, 64, 4096, 8000, True, torch.bfloat16, "cuda", 8, 64, 8, 8),
|
||||
(64, 64, 4096, 8000, True, torch.bfloat16, "cuda", 16, 128, 8, 8),
|
||||
# Large scale
|
||||
(64, 64, 4096, 8000, True, torch.bfloat16, "cuda", 32, 512, 8, 8),
|
||||
(64, 64, 4096, 8000, True, torch.bfloat16, "cuda", 64, 1024, 8, 8),
|
||||
],
|
||||
)
|
||||
def test_performance(
|
||||
head_size: int,
|
||||
rotary_dim: int,
|
||||
max_position_embeddings: int,
|
||||
base: int,
|
||||
is_neox_style,
|
||||
dtype,
|
||||
device,
|
||||
batch_size,
|
||||
seq_len,
|
||||
num_q_heads,
|
||||
num_kv_heads,
|
||||
):
|
||||
"""Performance test comparing JIT and SGL implementations with accuracy validation."""
|
||||
# Create inputs and caches
|
||||
query, key, pos_ids = create_test_inputs(
|
||||
head_size, batch_size, seq_len, device, dtype, num_q_heads, num_kv_heads
|
||||
)
|
||||
cos_sin_cache = create_cos_sin_cache(
|
||||
rotary_dim, max_position_embeddings, base, dtype, device
|
||||
)
|
||||
|
||||
# Initialize SGL kernel
|
||||
sgl_rotary_emb = get_sgl_rotary_embedding(
|
||||
head_size,
|
||||
rotary_dim,
|
||||
max_position_embeddings,
|
||||
base,
|
||||
is_neox_style,
|
||||
dtype,
|
||||
device,
|
||||
)
|
||||
sgl_rotary_emb.cos_sin_cache = cos_sin_cache
|
||||
|
||||
warmup = 3
|
||||
|
||||
# Warmup runs
|
||||
for _ in range(warmup):
|
||||
query_warm, key_warm = query.clone(), key.clone()
|
||||
rotary_embedding(
|
||||
positions=pos_ids,
|
||||
query=query_warm,
|
||||
key=key_warm,
|
||||
head_size=head_size,
|
||||
cos_sin_cache=cos_sin_cache,
|
||||
is_neox=is_neox_style,
|
||||
)
|
||||
|
||||
query_sgl_warm, key_sgl_warm = query.clone(), key.clone()
|
||||
sgl_rotary_emb.forward_cuda(
|
||||
positions=pos_ids, query=query_sgl_warm, key=key_sgl_warm
|
||||
)
|
||||
|
||||
iteration = 100
|
||||
|
||||
# Time JIT implementation
|
||||
torch.cuda.synchronize()
|
||||
start_time = time.time()
|
||||
for _ in range(iteration):
|
||||
query_jit, key_jit = query.clone(), key.clone()
|
||||
rotary_embedding(
|
||||
positions=pos_ids,
|
||||
query=query_jit,
|
||||
key=key_jit,
|
||||
head_size=head_size,
|
||||
cos_sin_cache=cos_sin_cache,
|
||||
is_neox=is_neox_style,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
jit_time = (time.time() - start_time) / iteration
|
||||
|
||||
# Time SGL implementation
|
||||
torch.cuda.synchronize()
|
||||
start_time = time.time()
|
||||
for _ in range(iteration):
|
||||
query_sgl, key_sgl = query.clone(), key.clone()
|
||||
sgl_rotary_emb.forward_cuda(positions=pos_ids, query=query_sgl, key=key_sgl)
|
||||
torch.cuda.synchronize()
|
||||
sgl_time = (time.time() - start_time) / iteration
|
||||
|
||||
# Accuracy validation during performance test
|
||||
# Run one more time to get outputs for comparison
|
||||
query_jit_final, key_jit_final = query.clone(), key.clone()
|
||||
query_sgl_final, key_sgl_final = query.clone(), key.clone()
|
||||
|
||||
query_jit_out, key_jit_out = rotary_embedding(
|
||||
positions=pos_ids,
|
||||
query=query_jit_final,
|
||||
key=key_jit_final,
|
||||
head_size=head_size,
|
||||
cos_sin_cache=cos_sin_cache,
|
||||
is_neox=is_neox_style,
|
||||
)
|
||||
|
||||
query_sgl_out, key_sgl_out = sgl_rotary_emb.forward_cuda(
|
||||
positions=pos_ids, query=query_sgl_final, key=key_sgl_final
|
||||
)
|
||||
|
||||
# Validate accuracy
|
||||
compare_results(query_jit_out, query_sgl_out, dtype)
|
||||
compare_results(key_jit_out, key_sgl_out, dtype)
|
||||
|
||||
# Print results
|
||||
total_tokens = batch_size * seq_len
|
||||
print(
|
||||
f"\nPerformance Test - Batch={batch_size}, SeqLen={seq_len}, Tokens={total_tokens}"
|
||||
)
|
||||
print(f"JIT: {jit_time*1000:.9f}ms, SGL: {sgl_time*1000:.9f}ms")
|
||||
if sgl_time > 0:
|
||||
speedup = sgl_time / jit_time if jit_time > 0 else float("inf")
|
||||
print(f"Speedup (SGL/JIT): {speedup:.2f}x")
|
||||
|
||||
assert jit_time >= 0 and sgl_time >= 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,101 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=37, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=148, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def sglang_aot_qknorm(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
) -> None:
|
||||
from sgl_kernel import rmsnorm
|
||||
|
||||
head_dim = q.shape[-1]
|
||||
q = q.view(-1, head_dim)
|
||||
k = k.view(-1, head_dim)
|
||||
rmsnorm(q, q_weight, out=q)
|
||||
rmsnorm(k, k_weight, out=k)
|
||||
|
||||
|
||||
def sglang_jit_qknorm(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
) -> None:
|
||||
from sglang.jit_kernel.norm import fused_inplace_qknorm
|
||||
|
||||
fused_inplace_qknorm(q, k, q_weight, k_weight)
|
||||
|
||||
|
||||
def flashinfer_qknorm(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
) -> None:
|
||||
from flashinfer.norm import rmsnorm
|
||||
|
||||
rmsnorm(q, q_weight, out=q)
|
||||
rmsnorm(k, k_weight, out=k)
|
||||
|
||||
|
||||
@torch.compile()
|
||||
def torch_impl_qknorm(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
eps: float = 1e-6,
|
||||
) -> None:
|
||||
q_mean = q.float().pow(2).mean(dim=-1, keepdim=True)
|
||||
k_mean = k.float().pow(2).mean(dim=-1, keepdim=True)
|
||||
q_norm = (q_mean + eps).rsqrt()
|
||||
k_norm = (k_mean + eps).rsqrt()
|
||||
q.copy_(q.float() * q_norm * q_weight.float())
|
||||
k.copy_(k.float() * k_norm * k_weight.float())
|
||||
|
||||
|
||||
BS_LIST = [2**n for n in range(0, 14)]
|
||||
BS_LIST += [x + 1 + i for i, x in enumerate(BS_LIST)]
|
||||
BS_LIST = get_ci_test_range(BS_LIST, [1, 9, 256, 4109])
|
||||
N_K_LIST = get_ci_test_range([2, 4], [2, 4])
|
||||
N_Q_LIST = get_ci_test_range([8, 16], [8, 16])
|
||||
HEAD_DIM_LIST = get_ci_test_range([64, 128, 256, 512, 1024], [64, 256, 1024])
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.bfloat16
|
||||
|
||||
# NOTE(dark): sgl_kernel use flashinfer template, which is bitwise identical to flashinfer impl.
|
||||
# However, sgl-jit-kernel, flashinfer, torch_impl, may have small numerical differences.
|
||||
# so we allow a small rel/abs tolerance in correctness test.
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_size,n_k,n_q,head_dim",
|
||||
list(itertools.product(BS_LIST, N_K_LIST, N_Q_LIST, HEAD_DIM_LIST)),
|
||||
)
|
||||
def test_qknorm(batch_size: int, n_k: int, n_q: int, head_dim: int) -> None:
|
||||
q = torch.randn(batch_size, n_q, head_dim, device=DEVICE, dtype=DTYPE)
|
||||
k = torch.randn(batch_size, n_k, head_dim, device=DEVICE, dtype=DTYPE)
|
||||
q_weight = torch.randn(head_dim, device=DEVICE, dtype=DTYPE)
|
||||
k_weight = torch.randn(head_dim, device=DEVICE, dtype=DTYPE)
|
||||
q_k_aot = (q.clone(), k.clone())
|
||||
q_k_jit = (q.clone(), k.clone())
|
||||
sglang_aot_qknorm(q_k_aot[0], q_k_aot[1], q_weight, k_weight)
|
||||
sglang_jit_qknorm(q_k_jit[0], q_k_jit[1], q_weight, k_weight)
|
||||
triton.testing.assert_close(q_k_aot[0], q_k_jit[0], atol=1e-2, rtol=1e-2)
|
||||
triton.testing.assert_close(q_k_aot[1], q_k_jit[1], atol=1e-2, rtol=1e-2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,83 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=15, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def sglang_jit_qknorm_across_heads(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
) -> None:
|
||||
from sglang.jit_kernel.norm import fused_inplace_qknorm_across_heads
|
||||
|
||||
fused_inplace_qknorm_across_heads(q, k, q_weight, k_weight)
|
||||
|
||||
|
||||
def sglang_aot_qknorm_across_heads(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
) -> None:
|
||||
from sgl_kernel import rmsnorm
|
||||
|
||||
rmsnorm(q, q_weight, out=q)
|
||||
rmsnorm(k, k_weight, out=k)
|
||||
|
||||
|
||||
@torch.compile()
|
||||
def torch_impl_qknorm_across_heads(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
eps: float = 1e-6,
|
||||
) -> None:
|
||||
q_mean = q.float().pow(2).mean(dim=-1, keepdim=True)
|
||||
k_mean = k.float().pow(2).mean(dim=-1, keepdim=True)
|
||||
q_norm = (q_mean + eps).rsqrt()
|
||||
k_norm = (k_mean + eps).rsqrt()
|
||||
q.copy_(q.float() * q_norm * q_weight.float())
|
||||
k.copy_(k.float() * k_norm * k_weight.float())
|
||||
|
||||
|
||||
BS_LIST = [2**n for n in range(0, 14)]
|
||||
BS_LIST += [x + 1 + i for i, x in enumerate(BS_LIST)]
|
||||
BS_LIST = get_ci_test_range(BS_LIST, [1, 9, 256, 4109])
|
||||
HIDDEN_DIM_LIST = get_ci_test_range([512, 1024, 2048, 4096], [512, 2048, 4096])
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.bfloat16
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_size,hidden_dim",
|
||||
list(itertools.product(BS_LIST, HIDDEN_DIM_LIST)),
|
||||
)
|
||||
def test_qknorm_across_heads(batch_size: int, hidden_dim: int) -> None:
|
||||
q = torch.randn(batch_size, hidden_dim, device=DEVICE, dtype=DTYPE)
|
||||
k = torch.randn(batch_size, hidden_dim, device=DEVICE, dtype=DTYPE)
|
||||
q_weight = torch.randn(hidden_dim, device=DEVICE, dtype=DTYPE)
|
||||
k_weight = torch.randn(hidden_dim, device=DEVICE, dtype=DTYPE)
|
||||
|
||||
q_k_jit = (q.clone(), k.clone())
|
||||
q_k_aot = (q.clone(), k.clone())
|
||||
|
||||
sglang_jit_qknorm_across_heads(q_k_jit[0], q_k_jit[1], q_weight, k_weight)
|
||||
sglang_aot_qknorm_across_heads(q_k_aot[0], q_k_aot[1], q_weight, k_weight)
|
||||
|
||||
triton.testing.assert_close(q_k_jit[0], q_k_aot[0], atol=1e-2, rtol=1e-2)
|
||||
triton.testing.assert_close(q_k_jit[1], q_k_aot[1], atol=1e-2, rtol=1e-2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,86 +0,0 @@
|
||||
# Adapted from https://github.com/flashinfer-ai/flashinfer/blob/main/tests/test_sampling.py
|
||||
# and /sgl-workspace/sglang/sgl-kernel/tests/test_sampling.py
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import sgl_kernel
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=6, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 99, 989])
|
||||
@pytest.mark.parametrize("vocab_size", [111, 32000, 128256])
|
||||
@pytest.mark.parametrize("k", [10, 100, 500])
|
||||
def test_top_k_renorm_probs(batch_size, vocab_size, k):
|
||||
"""Test top_k_renorm_probs kernel for correctness.
|
||||
|
||||
This test validates that the kernel correctly:
|
||||
1. Identifies the top-k probabilities
|
||||
2. Masks out non-top-k values
|
||||
3. Renormalizes the remaining probabilities to sum to 1
|
||||
"""
|
||||
if k > vocab_size:
|
||||
pytest.skip("k should be less than vocab_size")
|
||||
torch.manual_seed(42)
|
||||
pre_norm_prob = torch.rand(batch_size, vocab_size, device="cuda:0")
|
||||
normalized_prob = pre_norm_prob / pre_norm_prob.sum(dim=-1, keepdim=True)
|
||||
sorted_prob, _ = torch.sort(normalized_prob, descending=True)
|
||||
pivot = sorted_prob[:, k - 1]
|
||||
mask = (normalized_prob >= pivot.unsqueeze(-1)).int()
|
||||
renorm_prob_ground_truth = normalized_prob.clone()
|
||||
renorm_prob_ground_truth[mask == 0] = 0
|
||||
renorm_prob_ground_truth = renorm_prob_ground_truth / renorm_prob_ground_truth.sum(
|
||||
dim=-1, keepdim=True
|
||||
)
|
||||
|
||||
renorm_prob = sgl_kernel.top_k_renorm_prob(normalized_prob, k)
|
||||
for i in range(batch_size):
|
||||
torch.testing.assert_close(
|
||||
renorm_prob_ground_truth[i],
|
||||
renorm_prob[i],
|
||||
rtol=1e-3,
|
||||
atol=1e-3,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 99, 989])
|
||||
@pytest.mark.parametrize("vocab_size", [111, 32000, 128256])
|
||||
@pytest.mark.parametrize("p", [0.1, 0.5, 0.9])
|
||||
def test_top_p_renorm_probs(batch_size, vocab_size, p):
|
||||
"""Test top_p_renorm_probs kernel for correctness.
|
||||
|
||||
This test validates that the kernel correctly:
|
||||
1. Computes the cumulative probability distribution
|
||||
2. Identifies tokens in the top-p threshold
|
||||
3. Masks out tokens outside the threshold
|
||||
4. Renormalizes the remaining probabilities to sum to 1
|
||||
"""
|
||||
torch.manual_seed(42)
|
||||
pre_norm_prob = torch.rand(batch_size, vocab_size, device="cuda:0")
|
||||
normalized_prob = pre_norm_prob / pre_norm_prob.sum(dim=-1, keepdim=True)
|
||||
sorted_prob, indices = torch.sort(normalized_prob, descending=False)
|
||||
cdf = torch.cumsum(sorted_prob, dim=-1)
|
||||
mask = torch.zeros(batch_size, vocab_size, dtype=torch.int32, device="cuda:0")
|
||||
mask.scatter_add_(1, indices, (cdf >= (1 - p)).int())
|
||||
renorm_prob_ground_truth = normalized_prob.clone()
|
||||
renorm_prob_ground_truth[mask == 0] = 0
|
||||
renorm_prob_ground_truth = renorm_prob_ground_truth / renorm_prob_ground_truth.sum(
|
||||
dim=-1, keepdim=True
|
||||
)
|
||||
|
||||
renorm_prob = sgl_kernel.top_p_renorm_prob(normalized_prob, p)
|
||||
torch.testing.assert_close(
|
||||
renorm_prob_ground_truth,
|
||||
renorm_prob,
|
||||
rtol=1e-3,
|
||||
atol=1e-3,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__]))
|
||||
@@ -1,69 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.resolve_future_token_ids import resolve_future_token_ids_cuda
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=9, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
def _reference_resolve(input_ids, future_map):
|
||||
"""Reference implementation using plain torch."""
|
||||
result = input_ids.clone()
|
||||
result[:] = torch.where(
|
||||
result < 0,
|
||||
future_map[torch.clamp(-result, min=0)],
|
||||
result,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [1, 2, 127, 128, 255, 256, 1024, 4097])
|
||||
@pytest.mark.parametrize("dtype", [torch.int32, torch.int64])
|
||||
class TestResolveFutureTokenIds:
|
||||
def test_all_negative(self, size: int, dtype: torch.dtype) -> None:
|
||||
map_size = 8192
|
||||
future_map = torch.randint(0, 50000, (map_size,), dtype=dtype, device="cuda")
|
||||
# Negative indices in range [-map_size+1, -1]
|
||||
input_ids = -torch.randint(1, map_size, (size,), dtype=dtype, device="cuda")
|
||||
|
||||
expected = _reference_resolve(input_ids, future_map)
|
||||
resolve_future_token_ids_cuda(input_ids, future_map)
|
||||
assert torch.equal(input_ids, expected)
|
||||
|
||||
def test_all_non_negative(self, size: int, dtype: torch.dtype) -> None:
|
||||
map_size = 16
|
||||
future_map = torch.randint(0, 50000, (map_size,), dtype=dtype, device="cuda")
|
||||
input_ids = torch.randint(0, 50000, (size,), dtype=dtype, device="cuda")
|
||||
|
||||
expected = input_ids.clone()
|
||||
resolve_future_token_ids_cuda(input_ids, future_map)
|
||||
assert torch.equal(input_ids, expected)
|
||||
|
||||
def test_mixed(self, size: int, dtype: torch.dtype) -> None:
|
||||
map_size = 8192
|
||||
future_map = torch.randint(0, 50000, (map_size,), dtype=dtype, device="cuda")
|
||||
# Mix of negative and non-negative
|
||||
input_ids = torch.randint(
|
||||
-map_size + 1, 50000, (size,), dtype=dtype, device="cuda"
|
||||
)
|
||||
|
||||
expected = _reference_resolve(input_ids, future_map)
|
||||
resolve_future_token_ids_cuda(input_ids, future_map)
|
||||
assert torch.equal(input_ids, expected)
|
||||
|
||||
def test_zeros(self, size: int, dtype: torch.dtype) -> None:
|
||||
map_size = 16
|
||||
future_map = torch.randint(0, 50000, (map_size,), dtype=dtype, device="cuda")
|
||||
input_ids = torch.zeros(size, dtype=dtype, device="cuda")
|
||||
|
||||
expected = input_ids.clone()
|
||||
resolve_future_token_ids_cuda(input_ids, future_map)
|
||||
assert torch.equal(input_ids, expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,107 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=45, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=240, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
|
||||
EPS = 1e-6
|
||||
DEVICE = "cuda"
|
||||
DTYPES = [torch.float16, torch.bfloat16]
|
||||
|
||||
|
||||
def sglang_jit_rmsnorm(
|
||||
input: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
*,
|
||||
output: torch.Tensor | None = None,
|
||||
eps: float = EPS,
|
||||
) -> None:
|
||||
from sglang.jit_kernel.norm import rmsnorm
|
||||
|
||||
rmsnorm(input, weight, out=output, eps=eps)
|
||||
|
||||
|
||||
def flashinfer_rmsnorm(
|
||||
input: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
*,
|
||||
output: torch.Tensor,
|
||||
eps: float = EPS,
|
||||
) -> None:
|
||||
from flashinfer.norm import rmsnorm
|
||||
|
||||
rmsnorm(input, weight, out=output, eps=eps)
|
||||
|
||||
|
||||
BS_LIST = [2**n for n in range(0, 14)]
|
||||
BS_LIST += [x + 1 + i for i, x in enumerate(BS_LIST)]
|
||||
BS_LIST = get_ci_test_range(BS_LIST, [1, 9, 256, 4109])
|
||||
SUPPORTED_HIDDEN_SIZE_LIST = get_ci_test_range(
|
||||
[64, 128, 256, 512, *range(1024, 8192 + 1, 1024), 2304, 2560, 12288, 16384],
|
||||
[256, 1024, 16384],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_size,hidden_size",
|
||||
list(itertools.product(BS_LIST, SUPPORTED_HIDDEN_SIZE_LIST)),
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("specify_out", [True, False])
|
||||
def test_rmsnorm(
|
||||
batch_size: int, hidden_size: int, dtype: torch.dtype, specify_out: bool
|
||||
) -> None:
|
||||
input = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=dtype)
|
||||
weight = torch.randn(hidden_size, device=DEVICE, dtype=dtype)
|
||||
|
||||
input_flashinfer = input.clone()
|
||||
output_flashinfer = torch.empty_like(input)
|
||||
flashinfer_rmsnorm(input_flashinfer, weight, output=output_flashinfer)
|
||||
|
||||
if specify_out:
|
||||
output_sglang = torch.empty_like(input)
|
||||
sglang_jit_rmsnorm(input, weight, output=output_sglang)
|
||||
else:
|
||||
output_sglang = input.clone()
|
||||
sglang_jit_rmsnorm(output_sglang, weight, output=output_sglang)
|
||||
|
||||
torch.testing.assert_close(output_sglang, output_flashinfer, atol=1e-2, rtol=1e-2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hidden_size", [64, 128, 256, 512, 8192, 8704, 16384])
|
||||
def test_rmsnorm_hidden_size_support(hidden_size: int) -> None:
|
||||
from sglang.jit_kernel.norm import _is_supported_rmsnorm_hidden_size
|
||||
|
||||
assert _is_supported_rmsnorm_hidden_size(hidden_size)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("hidden_size", "expected"),
|
||||
[
|
||||
(64, "RMSNormWarpKernel"),
|
||||
(128, "RMSNormWarpKernel"),
|
||||
(256, "RMSNormWarpKernel"),
|
||||
(512, "RMSNormHalfKernel"),
|
||||
(1536, "RMSNormKernel"),
|
||||
(2048, "RMSNormHalfKernel"),
|
||||
(2304, "RMSNormKernel"), # NOTE: not 512 aligned
|
||||
(8192, "RMSNormHalfKernel"),
|
||||
(8704, "RMSNormHalfKernel"),
|
||||
(16384, "RMSNormHalfKernel"),
|
||||
],
|
||||
)
|
||||
def test_rmsnorm_kernel_dispatch(hidden_size: int, expected: str) -> None:
|
||||
from sglang.jit_kernel.norm import _rmsnorm_kernel_class
|
||||
|
||||
assert _rmsnorm_kernel_class(hidden_size) == expected
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,136 +0,0 @@
|
||||
"""Tests for the JIT rmsnorm_hf kernel (HF LlamaRMSNorm semantics)."""
|
||||
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.rmsnorm_hf import (
|
||||
is_supported_rmsnorm_hf_hidden_size,
|
||||
rmsnorm_hf,
|
||||
)
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
EPS = 1e-5
|
||||
DEVICE = "cuda"
|
||||
DTYPES = [torch.float16, torch.bfloat16]
|
||||
|
||||
|
||||
def hf_rmsnorm_reference(x: torch.Tensor, w: torch.Tensor, eps: float) -> torch.Tensor:
|
||||
"""HF LlamaRMSNorm: normalize fp32, cast normalized x to dtype, then multiply weight."""
|
||||
x_fp32 = x.to(torch.float32)
|
||||
variance = x_fp32.pow(2).mean(-1, keepdim=True)
|
||||
x_normed = x_fp32 * torch.rsqrt(variance + eps)
|
||||
return w * x_normed.to(x.dtype)
|
||||
|
||||
|
||||
def sgl_rmsnorm_reference(x: torch.Tensor, w: torch.Tensor, eps: float) -> torch.Tensor:
|
||||
"""Old sgl_kernel.rmsnorm semantics — weight multiply in fp32, cast at the end."""
|
||||
x_fp32 = x.to(torch.float32)
|
||||
variance = x_fp32.pow(2).mean(-1, keepdim=True)
|
||||
x_normed = x_fp32 * torch.rsqrt(variance + eps)
|
||||
return (x_normed * w.to(torch.float32)).to(x.dtype)
|
||||
|
||||
|
||||
BS_LIST = get_ci_test_range(
|
||||
[1, 2, 4, 7, 16, 64, 128, 512, 1024, 4096],
|
||||
[1, 16, 1024],
|
||||
)
|
||||
HIDDEN_SIZE_LIST = get_ci_test_range(
|
||||
# Warp-kernel shapes (q/k RMSNorm head dims) + CTA-kernel shapes.
|
||||
[32, 64, 96, 128, 256, 512, 1024, 2048, 3072, 4096, 8192, 16384],
|
||||
[128, 512, 4096, 16384],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_size,hidden_size",
|
||||
list(itertools.product(BS_LIST, HIDDEN_SIZE_LIST)),
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_rmsnorm_hf_correctness(
|
||||
batch_size: int, hidden_size: int, dtype: torch.dtype
|
||||
) -> None:
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(batch_size, hidden_size, device=DEVICE, dtype=dtype)
|
||||
w = torch.randn(hidden_size, device=DEVICE, dtype=dtype)
|
||||
out = rmsnorm_hf(x, w, EPS)
|
||||
ref = hf_rmsnorm_reference(x, w, EPS)
|
||||
# Loose atol — the kernel's block-reduce order differs from PyTorch's
|
||||
# `mean`, producing ~1 fp16 ULP of drift on some shapes.
|
||||
# The SGL-semantics regression guard below is what catches the cast-order
|
||||
# bug this PR fixes; it's reduction-order-invariant.
|
||||
torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_rmsnorm_hf_out_param(dtype: torch.dtype) -> None:
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(8, 4096, device=DEVICE, dtype=dtype)
|
||||
w = torch.randn(4096, device=DEVICE, dtype=dtype)
|
||||
out = torch.empty_like(x)
|
||||
result = rmsnorm_hf(x, w, EPS, out=out)
|
||||
assert result.data_ptr() == out.data_ptr()
|
||||
torch.testing.assert_close(
|
||||
out, hf_rmsnorm_reference(x, w, EPS), atol=1e-2, rtol=1e-2
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_rmsnorm_hf_matches_hf_not_sgl(dtype: torch.dtype) -> None:
|
||||
"""Regression guard: kernel must follow HF (cast-before-mul), not the old
|
||||
sgl_kernel.rmsnorm semantics (fp32-mul-then-cast). Reduction-order drift
|
||||
prevents a bit-exact assert against HF, so instead assert the kernel is
|
||||
strictly closer to HF than to the SGL reference."""
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(64, 4096, device=DEVICE, dtype=dtype)
|
||||
w = torch.randn(4096, device=DEVICE, dtype=dtype)
|
||||
out = rmsnorm_hf(x, w, EPS).float()
|
||||
hf_ref = hf_rmsnorm_reference(x, w, EPS).float()
|
||||
sgl_ref = sgl_rmsnorm_reference(x, w, EPS).float()
|
||||
assert (sgl_ref - hf_ref).abs().max() > 0, "inputs don't exercise the difference"
|
||||
diff_hf = (out - hf_ref).abs().max().item()
|
||||
diff_sgl = (out - sgl_ref).abs().max().item()
|
||||
assert (
|
||||
diff_hf < diff_sgl
|
||||
), f"kernel closer to SGL than HF (hf={diff_hf}, sgl={diff_sgl})"
|
||||
|
||||
|
||||
def test_rmsnorm_hf_empty_input() -> None:
|
||||
"""Empty input must short-circuit: the C++ launcher rejects num_tokens=0."""
|
||||
x = torch.empty(0, 4096, device=DEVICE, dtype=torch.float16)
|
||||
w = torch.randn(4096, device=DEVICE, dtype=torch.float16)
|
||||
out = rmsnorm_hf(x, w, EPS)
|
||||
assert out.shape == x.shape and out.numel() == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("hidden_size", "expected"),
|
||||
[
|
||||
(16, False),
|
||||
(32, True),
|
||||
(64, True),
|
||||
(96, True),
|
||||
(128, True),
|
||||
(256, True),
|
||||
(288, True),
|
||||
(384, True),
|
||||
(500, False),
|
||||
(512, True),
|
||||
(3072, True),
|
||||
(4096, True),
|
||||
(8192, True),
|
||||
(4097, False),
|
||||
],
|
||||
)
|
||||
def test_is_supported_hidden_size(hidden_size: int, expected: bool) -> None:
|
||||
assert is_supported_rmsnorm_hf_hidden_size(hidden_size) is expected
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,257 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=64, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=256, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
DEVICE = "cuda"
|
||||
DTYPE = torch.bfloat16
|
||||
MAX_SEQ_LEN = 131072 # common seq length
|
||||
ROPE_BASE = 10000.0
|
||||
CACHE_SIZE = 1024 * 128
|
||||
|
||||
|
||||
def create_cos_sin_cache(
|
||||
rotary_dim: int,
|
||||
max_position: int = MAX_SEQ_LEN,
|
||||
base: float = ROPE_BASE,
|
||||
) -> torch.Tensor:
|
||||
"""Create cos/sin cache compatible with SGLang layout: [max_pos, rotary_dim]."""
|
||||
inv_freq = 1.0 / (
|
||||
base
|
||||
** (
|
||||
torch.arange(0, rotary_dim, 2, dtype=torch.float32, device=DEVICE)
|
||||
/ rotary_dim
|
||||
)
|
||||
)
|
||||
t = torch.arange(max_position, dtype=torch.float32, device=DEVICE)
|
||||
freqs = torch.einsum("i,j->ij", t, inv_freq)
|
||||
cos = freqs.cos()
|
||||
sin = freqs.sin()
|
||||
cache = torch.cat((cos, sin), dim=-1) # [max_pos, rotary_dim]
|
||||
return cache
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Implementation wrappers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sglang_jit_rope(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
is_neox: bool,
|
||||
) -> None:
|
||||
from sglang.jit_kernel.rope import apply_rope_inplace
|
||||
|
||||
apply_rope_inplace(q, k, cos_sin_cache, positions, is_neox=is_neox)
|
||||
|
||||
|
||||
def flashinfer_rope(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
is_neox: bool,
|
||||
) -> None:
|
||||
from flashinfer.rope import apply_rope_with_cos_sin_cache_inplace
|
||||
|
||||
head_size = q.shape[-1]
|
||||
# flashinfer expects [nnz, num_heads * head_size]
|
||||
q_2d = q.view(q.shape[0], -1)
|
||||
k_2d = k.view(k.shape[0], -1)
|
||||
apply_rope_with_cos_sin_cache_inplace(
|
||||
positions=positions,
|
||||
query=q_2d,
|
||||
key=k_2d,
|
||||
head_size=head_size,
|
||||
cos_sin_cache=cos_sin_cache,
|
||||
is_neox=is_neox,
|
||||
)
|
||||
|
||||
|
||||
def torch_impl_rope(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
is_neox: bool,
|
||||
) -> None:
|
||||
# TODO: implement a pure-PyTorch reference for extra coverage
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test parameters
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BS_LIST = [2**x for x in range(12)]
|
||||
BS_LIST += [x + 1 for x in BS_LIST] # odd sizes to stress non-aligned paths
|
||||
BS_LIST = get_ci_test_range(BS_LIST, [1, 129, 2048, 2049])
|
||||
NUM_KV_HEADS_LIST = get_ci_test_range([1, 2, 8], [1, 8])
|
||||
GQA_RATIO = get_ci_test_range([1, 4, 8], [1, 8])
|
||||
ROPE_DIM_LIST = get_ci_test_range([64, 128, 256, 512], [64, 256])
|
||||
IS_NEOX_LIST = [False, True]
|
||||
DTYPE_LIST = get_ci_test_range(
|
||||
[torch.bfloat16, torch.float16], [torch.bfloat16, torch.float16]
|
||||
)
|
||||
PARTIAL_ROPE_DIM_LIST = get_ci_test_range([64, 80, 96, 128], [64, 96])
|
||||
HEAD_DIM_LIST = get_ci_test_range([64, 128, 256], [64, 256])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", BS_LIST)
|
||||
@pytest.mark.parametrize("gqa_ratio", GQA_RATIO)
|
||||
@pytest.mark.parametrize("num_kv_heads", NUM_KV_HEADS_LIST)
|
||||
@pytest.mark.parametrize("rope_dim", ROPE_DIM_LIST)
|
||||
@pytest.mark.parametrize("is_neox", IS_NEOX_LIST)
|
||||
@pytest.mark.parametrize("dtype", DTYPE_LIST)
|
||||
def test_rope(
|
||||
batch_size: int,
|
||||
gqa_ratio: int,
|
||||
num_kv_heads: int,
|
||||
rope_dim: int,
|
||||
is_neox: bool,
|
||||
dtype: torch.dtype,
|
||||
) -> None:
|
||||
num_qo_heads = num_kv_heads * gqa_ratio
|
||||
q = torch.randn(batch_size, num_qo_heads, rope_dim, device=DEVICE, dtype=dtype)
|
||||
k = torch.randn(batch_size, num_kv_heads, rope_dim, device=DEVICE, dtype=dtype)
|
||||
positions = torch.randint(
|
||||
0, MAX_SEQ_LEN, (batch_size,), device=DEVICE, dtype=torch.int64
|
||||
)
|
||||
cos_sin_cache = create_cos_sin_cache(rope_dim)
|
||||
|
||||
q_fi, k_fi = q.clone(), k.clone()
|
||||
q_jit, k_jit = q.clone(), k.clone()
|
||||
|
||||
flashinfer_rope(q_fi, k_fi, cos_sin_cache, positions, is_neox)
|
||||
sglang_jit_rope(q_jit, k_jit, cos_sin_cache, positions, is_neox)
|
||||
|
||||
atol = rtol = 1e-2
|
||||
triton.testing.assert_close(q_fi, q_jit, atol=atol, rtol=rtol)
|
||||
triton.testing.assert_close(k_fi, k_jit, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.int32, torch.int64])
|
||||
def test_rope_position_dtypes(dtype: torch.dtype) -> None:
|
||||
"""Ensure both int32 and int64 position tensors work correctly."""
|
||||
batch_size, num_qo_heads, num_kv_heads, rope_dim = 16384, 16, 2, 128
|
||||
is_neox = True
|
||||
|
||||
q = torch.randn(batch_size, num_qo_heads, rope_dim, device=DEVICE, dtype=DTYPE)
|
||||
k = torch.randn(batch_size, num_kv_heads, rope_dim, device=DEVICE, dtype=DTYPE)
|
||||
positions = torch.randint(0, MAX_SEQ_LEN, (batch_size,), device=DEVICE, dtype=dtype)
|
||||
cos_sin_cache = create_cos_sin_cache(rope_dim)
|
||||
|
||||
q_fi, k_fi = q.clone(), k.clone()
|
||||
q_jit, k_jit = q.clone(), k.clone()
|
||||
|
||||
flashinfer_rope(q_fi, k_fi, cos_sin_cache, positions.long(), is_neox)
|
||||
sglang_jit_rope(q_jit, k_jit, cos_sin_cache, positions, is_neox)
|
||||
|
||||
atol = rtol = 1e-2
|
||||
triton.testing.assert_close(q_fi, q_jit, atol=atol, rtol=rtol)
|
||||
triton.testing.assert_close(k_fi, k_jit, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", BS_LIST)
|
||||
@pytest.mark.parametrize("is_neox", IS_NEOX_LIST)
|
||||
@pytest.mark.parametrize("rope_dim", PARTIAL_ROPE_DIM_LIST)
|
||||
@pytest.mark.parametrize("head_dim", HEAD_DIM_LIST)
|
||||
def test_partial_rope(batch_size: int, is_neox: bool, rope_dim: int, head_dim: int):
|
||||
if head_dim < rope_dim:
|
||||
pytest.skip("Invalid config: head_dim must be >= rope_dim.")
|
||||
num_qo_heads, num_kv_heads = 8, 2
|
||||
|
||||
q = torch.randn(batch_size, num_qo_heads, head_dim, device=DEVICE, dtype=DTYPE)
|
||||
k = torch.randn(batch_size, num_kv_heads, head_dim, device=DEVICE, dtype=DTYPE)
|
||||
positions = torch.randint(0, MAX_SEQ_LEN, (batch_size,), device=DEVICE)
|
||||
cos_sin_cache = create_cos_sin_cache(rope_dim)
|
||||
|
||||
q_fi, k_fi = q.clone(), k.clone()
|
||||
q_jit, k_jit = q.clone(), k.clone()
|
||||
rope = ..., slice(rope_dim) # NOTE: flashinfer by default apply to first rope_dim
|
||||
|
||||
flashinfer_rope(q_fi, k_fi, cos_sin_cache, positions.long(), is_neox)
|
||||
sglang_jit_rope(q_jit[rope], k_jit[rope], cos_sin_cache, positions, is_neox)
|
||||
|
||||
atol = rtol = 1e-2
|
||||
triton.testing.assert_close(q_fi, q_jit, atol=atol, rtol=rtol)
|
||||
triton.testing.assert_close(k_fi, k_jit, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", BS_LIST)
|
||||
@pytest.mark.parametrize("gqa_ratio", GQA_RATIO)
|
||||
@pytest.mark.parametrize("num_kv_heads", NUM_KV_HEADS_LIST)
|
||||
@pytest.mark.parametrize("rope_dim", ROPE_DIM_LIST)
|
||||
@pytest.mark.parametrize("is_neox", IS_NEOX_LIST)
|
||||
def test_fused_rope_store(
|
||||
batch_size: int,
|
||||
gqa_ratio: int,
|
||||
num_kv_heads: int,
|
||||
rope_dim: int,
|
||||
is_neox: bool,
|
||||
) -> None:
|
||||
"""Test fused RoPE + KV cache store against separate RoPE + manual store."""
|
||||
from sglang.jit_kernel.rope import apply_rope_inplace_with_kvcache
|
||||
|
||||
num_qo_heads = num_kv_heads * gqa_ratio
|
||||
dtype = DTYPE
|
||||
|
||||
q = torch.randn(batch_size, num_qo_heads, rope_dim, device=DEVICE, dtype=dtype)
|
||||
k = torch.randn(batch_size, num_kv_heads, rope_dim, device=DEVICE, dtype=dtype)
|
||||
v = torch.randn(batch_size, num_kv_heads, rope_dim, device=DEVICE, dtype=dtype)
|
||||
positions = torch.randint(
|
||||
0, MAX_SEQ_LEN, (batch_size,), device=DEVICE, dtype=torch.int64
|
||||
)
|
||||
out_loc = torch.randperm(CACHE_SIZE, device=DEVICE, dtype=torch.int64)[:batch_size]
|
||||
cos_sin_cache = create_cos_sin_cache(rope_dim)
|
||||
|
||||
row_size = num_kv_heads * rope_dim
|
||||
k_cache_ref = torch.zeros(CACHE_SIZE, row_size, device=DEVICE, dtype=dtype)
|
||||
v_cache_ref = torch.zeros(CACHE_SIZE, row_size, device=DEVICE, dtype=dtype)
|
||||
k_cache_fused = torch.zeros(CACHE_SIZE, row_size, device=DEVICE, dtype=dtype)
|
||||
v_cache_fused = torch.zeros(CACHE_SIZE, row_size, device=DEVICE, dtype=dtype)
|
||||
|
||||
# --- reference: separate RoPE then manual scatter ---
|
||||
q_ref, k_ref = q.clone(), k.clone()
|
||||
flashinfer_rope(q_ref, k_ref, cos_sin_cache, positions, is_neox)
|
||||
k_cache_ref[out_loc] = k_ref.view(batch_size, -1)
|
||||
v_cache_ref[out_loc] = v.view(batch_size, -1)
|
||||
|
||||
# --- fused kernel ---
|
||||
q_fused, k_fused = q.clone(), k.clone()
|
||||
v_fused = v.clone()
|
||||
apply_rope_inplace_with_kvcache(
|
||||
q_fused,
|
||||
k_fused,
|
||||
v_fused,
|
||||
k_cache_fused,
|
||||
v_cache_fused,
|
||||
cos_sin_cache,
|
||||
positions,
|
||||
out_loc,
|
||||
is_neox=is_neox,
|
||||
)
|
||||
|
||||
atol = rtol = 1e-2
|
||||
# q should match RoPE-only result
|
||||
triton.testing.assert_close(q_ref, q_fused, atol=atol, rtol=rtol)
|
||||
# k_cache should contain the rotated k
|
||||
triton.testing.assert_close(
|
||||
k_cache_ref[out_loc], k_cache_fused[out_loc], atol=atol, rtol=rtol
|
||||
)
|
||||
# v_cache should be an exact copy
|
||||
assert torch.all(v_cache_ref[out_loc] == v_cache_fused[out_loc]), "v_cache mismatch"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,126 +0,0 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.set_mla_kv_buffer import (
|
||||
can_use_set_mla_kv_buffer,
|
||||
set_mla_kv_buffer,
|
||||
)
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
|
||||
DEVICE = "cuda"
|
||||
CACHE_SIZE = 4096
|
||||
|
||||
# (nope_dim, rope_dim) pairs: standard MLA, MLA scale buffer, FP8 nope-extended layout.
|
||||
SHAPES = get_ci_test_range(
|
||||
[(512, 64), (512, 32), (256, 64), (128, 64), (528, 64)],
|
||||
[(512, 64), (528, 64)],
|
||||
)
|
||||
BATCH_SIZES = get_ci_test_range([1, 7, 64, 257, 1024], [1, 64, 1024])
|
||||
|
||||
|
||||
def _ref(kv_buffer, loc, cache_k_nope, cache_k_rope):
|
||||
nope_dim = cache_k_nope.shape[-1]
|
||||
n_loc = loc.shape[0]
|
||||
src_nope = cache_k_nope.reshape(n_loc, -1)
|
||||
src_rope = cache_k_rope.reshape(n_loc, -1)
|
||||
kv_view = kv_buffer.view(kv_buffer.shape[0], -1)
|
||||
kv_view[loc.long(), :nope_dim] = src_nope
|
||||
kv_view[loc.long(), nope_dim : nope_dim + src_rope.shape[-1]] = src_rope
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize("shape", SHAPES)
|
||||
@pytest.mark.parametrize("batch_size", BATCH_SIZES)
|
||||
def test_set_mla_kv_buffer_correctness(dtype, shape, batch_size):
|
||||
nope_dim, rope_dim = shape
|
||||
total_dim = nope_dim + rope_dim
|
||||
|
||||
cache_k_nope = torch.randn((batch_size, 1, nope_dim), dtype=dtype, device=DEVICE)
|
||||
cache_k_rope = torch.randn((batch_size, 1, rope_dim), dtype=dtype, device=DEVICE)
|
||||
kv_buffer = torch.randn((CACHE_SIZE, 1, total_dim), dtype=dtype, device=DEVICE)
|
||||
kv_ref = kv_buffer.clone()
|
||||
|
||||
loc = torch.randperm(CACHE_SIZE, device=DEVICE)[:batch_size]
|
||||
|
||||
set_mla_kv_buffer(kv_buffer, loc, cache_k_nope, cache_k_rope)
|
||||
_ref(kv_ref, loc, cache_k_nope, cache_k_rope)
|
||||
|
||||
assert torch.equal(kv_buffer, kv_ref)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("loc_dtype", [torch.int32, torch.int64])
|
||||
def test_set_mla_kv_buffer_loc_dtypes(loc_dtype):
|
||||
nope_dim, rope_dim = 512, 64
|
||||
batch_size = 128
|
||||
dtype = torch.bfloat16
|
||||
|
||||
cache_k_nope = torch.randn((batch_size, 1, nope_dim), dtype=dtype, device=DEVICE)
|
||||
cache_k_rope = torch.randn((batch_size, 1, rope_dim), dtype=dtype, device=DEVICE)
|
||||
kv_buffer = torch.randn(
|
||||
(CACHE_SIZE, 1, nope_dim + rope_dim), dtype=dtype, device=DEVICE
|
||||
)
|
||||
kv_ref = kv_buffer.clone()
|
||||
|
||||
loc = torch.randperm(CACHE_SIZE, device=DEVICE)[:batch_size].to(loc_dtype)
|
||||
|
||||
set_mla_kv_buffer(kv_buffer, loc, cache_k_nope, cache_k_rope)
|
||||
_ref(kv_ref, loc, cache_k_nope, cache_k_rope)
|
||||
|
||||
assert torch.equal(kv_buffer, kv_ref)
|
||||
|
||||
|
||||
def test_set_mla_kv_buffer_uint8_byte_layout():
|
||||
"""FP8 DSA byte-layout: cache_k_nope is uint8 with [fp8(512) | scales(16)] = 528,
|
||||
cache_k_rope is uint8 [128]; total payload = 656 bytes."""
|
||||
nope_bytes, rope_bytes = 528, 128
|
||||
batch_size = 64
|
||||
dtype = torch.uint8
|
||||
|
||||
cache_k_nope = torch.randint(
|
||||
0, 256, (batch_size, 1, nope_bytes), dtype=dtype, device=DEVICE
|
||||
)
|
||||
cache_k_rope = torch.randint(
|
||||
0, 256, (batch_size, 1, rope_bytes), dtype=dtype, device=DEVICE
|
||||
)
|
||||
kv_buffer = torch.randint(
|
||||
0, 256, (CACHE_SIZE, 1, nope_bytes + rope_bytes), dtype=dtype, device=DEVICE
|
||||
)
|
||||
kv_ref = kv_buffer.clone()
|
||||
|
||||
loc = torch.randperm(CACHE_SIZE, device=DEVICE)[:batch_size]
|
||||
|
||||
set_mla_kv_buffer(kv_buffer, loc, cache_k_nope, cache_k_rope)
|
||||
_ref(kv_ref, loc, cache_k_nope, cache_k_rope)
|
||||
|
||||
assert torch.equal(kv_buffer, kv_ref)
|
||||
|
||||
|
||||
def test_set_mla_kv_buffer_empty_loc():
|
||||
nope_dim, rope_dim = 512, 64
|
||||
dtype = torch.bfloat16
|
||||
cache_k_nope = torch.empty((0, 1, nope_dim), dtype=dtype, device=DEVICE)
|
||||
cache_k_rope = torch.empty((0, 1, rope_dim), dtype=dtype, device=DEVICE)
|
||||
kv_buffer = torch.randn(
|
||||
(CACHE_SIZE, 1, nope_dim + rope_dim), dtype=dtype, device=DEVICE
|
||||
)
|
||||
kv_before = kv_buffer.clone()
|
||||
|
||||
loc = torch.empty((0,), dtype=torch.int64, device=DEVICE)
|
||||
set_mla_kv_buffer(kv_buffer, loc, cache_k_nope, cache_k_rope)
|
||||
|
||||
assert torch.equal(kv_buffer, kv_before)
|
||||
|
||||
|
||||
def test_can_use_set_mla_kv_buffer():
|
||||
assert can_use_set_mla_kv_buffer(1024, 128) # bf16 (512,64)
|
||||
assert can_use_set_mla_kv_buffer(528, 128) # fp8 byte layout
|
||||
assert not can_use_set_mla_kv_buffer(13, 8) # not multiple of 4
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,133 +0,0 @@
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.kvcache import can_use_store_cache, store_cache
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=28, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
register_amd_ci(est_time=55, suite="jit-kernel-unit-test-amd")
|
||||
|
||||
BS_LIST = [2**n for n in range(0, 15)]
|
||||
BS_LIST += [x + 1 + i for i, x in enumerate(BS_LIST)]
|
||||
BS_LIST = get_ci_test_range(BS_LIST, [1, 9, 256, 16399])
|
||||
HIDDEN_DIMS = get_ci_test_range(
|
||||
[64, 128, 256, 512, 1024, 96, 98, 100], [64, 512, 1024, 98]
|
||||
)
|
||||
CACHE_SIZE = 1024 * 1024
|
||||
DTYPE = torch.bfloat16
|
||||
DEVICE = "cuda"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_size,element_dim",
|
||||
list(itertools.product(BS_LIST, HIDDEN_DIMS)),
|
||||
)
|
||||
def test_store_cache(batch_size: int, element_dim: int) -> None:
|
||||
k = torch.randn((batch_size, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
v = torch.randn((batch_size, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
k_cache = torch.randn((CACHE_SIZE, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
v_cache = torch.randn((CACHE_SIZE, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
indices = torch.randperm(CACHE_SIZE, device=DEVICE)[:batch_size]
|
||||
|
||||
# AOT store cache
|
||||
store_cache(k, v, k_cache, v_cache, indices)
|
||||
|
||||
assert torch.all(k_cache[indices] == k)
|
||||
assert torch.all(v_cache[indices] == v)
|
||||
|
||||
|
||||
# Smaller subset for targeted tests below
|
||||
REPR_BS = get_ci_test_range([1, 7, 128], [1, 128])
|
||||
REPR_DIMS = get_ci_test_range([64, 128, 512, 1024, 96], [64, 1024, 96])
|
||||
SMALL_CACHE = 4096
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
|
||||
@pytest.mark.parametrize(
|
||||
"batch_size,element_dim",
|
||||
list(itertools.product(REPR_BS, REPR_DIMS)),
|
||||
)
|
||||
def test_store_cache_dtypes(
|
||||
batch_size: int, element_dim: int, dtype: torch.dtype
|
||||
) -> None:
|
||||
k = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE)
|
||||
v = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE)
|
||||
k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
|
||||
v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
|
||||
indices = torch.randperm(SMALL_CACHE, device=DEVICE)[:batch_size]
|
||||
|
||||
store_cache(k, v, k_cache, v_cache, indices)
|
||||
|
||||
assert torch.all(k_cache[indices] == k)
|
||||
assert torch.all(v_cache[indices] == v)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_size,element_dim",
|
||||
list(itertools.product(REPR_BS, REPR_DIMS)),
|
||||
)
|
||||
def test_store_cache_int32_indices(batch_size: int, element_dim: int) -> None:
|
||||
k = torch.randn((batch_size, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
v = torch.randn((batch_size, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE)
|
||||
# int32 indices exercise a different CUDA template instantiation than default int64
|
||||
indices = torch.randperm(SMALL_CACHE, device=DEVICE)[:batch_size].to(torch.int32)
|
||||
|
||||
store_cache(k, v, k_cache, v_cache, indices)
|
||||
|
||||
assert torch.all(k_cache[indices.long()] == k)
|
||||
assert torch.all(v_cache[indices.long()] == v)
|
||||
|
||||
|
||||
def _valid_num_splits(element_dim: int, dtype: torch.dtype) -> list:
|
||||
"""Return the list of valid num_split values for a given element_dim/dtype."""
|
||||
row_bytes = element_dim * dtype.itemsize
|
||||
splits = [1]
|
||||
if row_bytes % (2 * 128) == 0:
|
||||
splits.append(2)
|
||||
if row_bytes % (4 * 128) == 0:
|
||||
splits.append(4)
|
||||
return splits
|
||||
|
||||
|
||||
_NUM_SPLIT_CASES = [
|
||||
(_dim, _ns, _dtype)
|
||||
for _dtype in [torch.float16, torch.bfloat16, torch.float32]
|
||||
for _dim in REPR_DIMS
|
||||
for _ns in _valid_num_splits(_dim, _dtype)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("element_dim,num_split,dtype", _NUM_SPLIT_CASES)
|
||||
def test_store_cache_num_split(
|
||||
element_dim: int, num_split: int, dtype: torch.dtype
|
||||
) -> None:
|
||||
batch_size = 128
|
||||
k = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE)
|
||||
v = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE)
|
||||
k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
|
||||
v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
|
||||
indices = torch.randperm(SMALL_CACHE, device=DEVICE)[:batch_size]
|
||||
|
||||
# Verify each num_split kernel path (1, 2, 4) produces correct results
|
||||
store_cache(k, v, k_cache, v_cache, indices, num_split=num_split)
|
||||
|
||||
assert torch.all(k_cache[indices] == k)
|
||||
assert torch.all(v_cache[indices] == v)
|
||||
|
||||
|
||||
def test_can_use_store_cache() -> None:
|
||||
assert can_use_store_cache(128)
|
||||
assert can_use_store_cache(256)
|
||||
assert can_use_store_cache(1024)
|
||||
assert can_use_store_cache(2048)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,183 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
try:
|
||||
import tabulate
|
||||
except Exception:
|
||||
tabulate = None
|
||||
|
||||
from sglang.jit_kernel.timestep_embedding import (
|
||||
timestep_embedding as timestep_embedding_cuda,
|
||||
)
|
||||
from sglang.jit_kernel.utils import get_ci_test_range
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=16, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
|
||||
|
||||
CORRECTNESS_BATCH_SIZES = get_ci_test_range(
|
||||
[1, 2, 8, 128, 256, 512, 1536, 2048, 4096, 11008, 16384],
|
||||
[1, 128, 2048, 16384],
|
||||
)
|
||||
CORRECTNESS_DIMS = get_ci_test_range(
|
||||
[32, 128, 256, 512, 1536, 2048, 4096, 8192],
|
||||
[32, 512, 8192],
|
||||
)
|
||||
DIFFUSERS_BATCH_SIZES = get_ci_test_range(
|
||||
[1, 2, 8, 128, 256, 512, 1536, 2048, 16384],
|
||||
[1, 512, 16384],
|
||||
)
|
||||
DIFFUSERS_DIMS = get_ci_test_range([32, 256, 512, 1536, 8192], [32, 512, 8192])
|
||||
DTYPES = get_ci_test_range(
|
||||
[torch.float16, torch.bfloat16, torch.float32],
|
||||
[torch.float16, torch.bfloat16],
|
||||
)
|
||||
SCALES = get_ci_test_range([1, 0.01], [1, 0.01])
|
||||
|
||||
|
||||
def get_timestep_embedding_reference(
|
||||
timesteps: torch.Tensor,
|
||||
dim: int,
|
||||
*,
|
||||
flip_sin_to_cos: bool = False,
|
||||
downscale_freq_shift: float = 1,
|
||||
scale: float = 1,
|
||||
max_period: int = 10000,
|
||||
):
|
||||
assert len(timesteps.shape) == 1, "Timesteps should be a 1d-array"
|
||||
|
||||
timesteps = timesteps.to(torch.float32)
|
||||
half_dim = dim // 2
|
||||
exponent = -torch.log(
|
||||
torch.tensor(max_period, dtype=torch.float32, device=timesteps.device)
|
||||
) * torch.arange(
|
||||
start=0, end=half_dim, dtype=torch.float32, device=timesteps.device
|
||||
)
|
||||
exponent = exponent / (half_dim - downscale_freq_shift)
|
||||
|
||||
emb = torch.exp(exponent)
|
||||
emb = timesteps[:, None].float() * emb[None, :]
|
||||
|
||||
emb = scale * emb
|
||||
|
||||
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
|
||||
if flip_sin_to_cos:
|
||||
emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1)
|
||||
if dim % 2 == 1:
|
||||
emb = torch.nn.functional.pad(emb, (0, 1, 0, 0))
|
||||
return emb
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", CORRECTNESS_BATCH_SIZES)
|
||||
@pytest.mark.parametrize("dim", CORRECTNESS_DIMS)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_timestep_embedding_correctness_with_sgld(batch_size, dim, dtype):
|
||||
device = "cuda"
|
||||
t = torch.randint(low=0, high=1000, size=(batch_size,), device=device).to(dtype)
|
||||
torch_output = get_timestep_embedding_reference(
|
||||
t, dim, flip_sin_to_cos=True, downscale_freq_shift=0
|
||||
)
|
||||
cuda_output = timestep_embedding_cuda(
|
||||
t, dim, flip_sin_to_cos=True, downscale_freq_shift=0
|
||||
)
|
||||
torch.testing.assert_close(torch_output, cuda_output, atol=1e-3, rtol=1e-3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", DIFFUSERS_BATCH_SIZES)
|
||||
@pytest.mark.parametrize("dim", DIFFUSERS_DIMS)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("flip_sin_to_cos", [False, True])
|
||||
@pytest.mark.parametrize("downscale_freq_shift", [0, 1])
|
||||
@pytest.mark.parametrize("scale", SCALES)
|
||||
def test_timestep_embedding_correctness_with_diffusers(
|
||||
batch_size, dim, flip_sin_to_cos, downscale_freq_shift, scale, dtype
|
||||
):
|
||||
device = "cuda"
|
||||
t = torch.randint(low=0, high=1000, size=(batch_size,), device=device).to(dtype)
|
||||
torch_output = get_timestep_embedding_reference(
|
||||
t,
|
||||
dim,
|
||||
flip_sin_to_cos=flip_sin_to_cos,
|
||||
downscale_freq_shift=downscale_freq_shift,
|
||||
scale=scale,
|
||||
max_period=10000,
|
||||
)
|
||||
cuda_output = timestep_embedding_cuda(
|
||||
t,
|
||||
dim,
|
||||
flip_sin_to_cos=flip_sin_to_cos,
|
||||
downscale_freq_shift=downscale_freq_shift,
|
||||
scale=scale,
|
||||
max_period=10000,
|
||||
)
|
||||
torch.testing.assert_close(torch_output, cuda_output, atol=1e-3, rtol=1e-3)
|
||||
|
||||
|
||||
def test_timestep_embedding_perf():
|
||||
if os.environ.get("SGLANG_RUN_JIT_KERNEL_PERF_TESTS") != "1":
|
||||
pytest.skip("Perf test disabled by default")
|
||||
if tabulate is None:
|
||||
pytest.skip("Optional dependency 'tabulate' is not installed")
|
||||
|
||||
NUM_BATCH = [1, 2, 8, 63, 256, 512, 613, 1024, 1536]
|
||||
NUM_DIM = [32, 64, 128, 256, 512, 1024, 2048, 4096]
|
||||
|
||||
def perf_kernel_fn(kernel_fn: callable, *args, **kwargs):
|
||||
warmup_times = 4
|
||||
repeat_times = 20
|
||||
start = torch.cuda.Event(enable_timing=True)
|
||||
end = torch.cuda.Event(enable_timing=True)
|
||||
|
||||
for _ in range(warmup_times):
|
||||
output_fn = kernel_fn(*args, **kwargs)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
start.record()
|
||||
for _ in range(repeat_times):
|
||||
output_fn = kernel_fn(*args, **kwargs)
|
||||
end.record()
|
||||
end.synchronize()
|
||||
return start.elapsed_time(end) / repeat_times
|
||||
|
||||
device = "cuda"
|
||||
results = []
|
||||
|
||||
cuda_speedups = []
|
||||
for B in NUM_BATCH:
|
||||
for dim in NUM_DIM:
|
||||
t = torch.linspace(0, max(100000, B), steps=B, device=device).to(
|
||||
torch.float32
|
||||
)
|
||||
time_torch = perf_kernel_fn(get_timestep_embedding_reference, t, dim)
|
||||
time_cuda = perf_kernel_fn(timestep_embedding_cuda, t, dim)
|
||||
speedup_cuda = time_torch / time_cuda
|
||||
|
||||
results.append(
|
||||
{
|
||||
"Batch Size": B,
|
||||
"Dimension": dim,
|
||||
"Torch Time (ms)": time_torch,
|
||||
"CUDA Time (ms)": time_cuda,
|
||||
"Speedup (CUDA)": speedup_cuda,
|
||||
}
|
||||
)
|
||||
cuda_speedups.append(speedup_cuda)
|
||||
|
||||
print("=== Timestep Embedding Benchmark Results ===")
|
||||
print(
|
||||
tabulate.tabulate(
|
||||
results,
|
||||
headers="keys",
|
||||
tablefmt="fancy_grid",
|
||||
floatfmt=(".0f", ".0f", ".6f", ".6f", ".5f"),
|
||||
)
|
||||
)
|
||||
print(f"Average Speedup(cuda): {np.mean(cuda_speedups):.4f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -1,168 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import triton
|
||||
|
||||
from sglang.jit_kernel.all_reduce import fused_parallel_qknorm
|
||||
from sglang.jit_kernel.tests.test_custom_all_reduce import multiprocess_test
|
||||
from sglang.jit_kernel.tests.utils import multiprocess_main
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(
|
||||
est_time=300,
|
||||
suite="base-b-kernel-unit-8-gpu-h200",
|
||||
)
|
||||
register_cuda_ci(
|
||||
est_time=300,
|
||||
suite="nightly-kernel-8-gpu-h200",
|
||||
nightly=True,
|
||||
)
|
||||
|
||||
|
||||
Q_K_DIMS = [(6144, 1024)]
|
||||
EPS = 1e-6
|
||||
BATCH_SIZES = [2**n for n in range(0, 14)]
|
||||
DTYPES = [torch.float16, torch.bfloat16, torch.float32]
|
||||
TEST_CONFIG = list(itertools.product(Q_K_DIMS, BATCH_SIZES, DTYPES))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nproc", [2, 4, 8])
|
||||
def test_tp_qknorm(nproc: int) -> None:
|
||||
device_count = torch.cuda.device_count()
|
||||
if device_count < nproc:
|
||||
pytest.skip(
|
||||
f"Requires at least {nproc} GPUs, but only {device_count} available"
|
||||
)
|
||||
multiprocess_test(__file__, nproc)
|
||||
|
||||
|
||||
def init_distributed():
|
||||
import sglang.srt.distributed.parallel_state as ps
|
||||
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
|
||||
CustomAllReduceV2,
|
||||
)
|
||||
|
||||
local_rank = int(os.environ["LOCAL_RANK"])
|
||||
world_size = int(os.environ["WORLD_SIZE"])
|
||||
rank = local_rank
|
||||
device = torch.device(f"cuda:{rank}")
|
||||
torch.cuda.set_device(device)
|
||||
|
||||
dist.init_process_group(backend="gloo")
|
||||
ps._WORLD = coord = ps.init_world_group(
|
||||
ranks=list(range(world_size)),
|
||||
local_rank=local_rank,
|
||||
backend="nccl",
|
||||
)
|
||||
|
||||
cpu_group = coord.cpu_group
|
||||
nccl_group = coord.device_group
|
||||
assert nccl_group is not None
|
||||
|
||||
max_pull_size = 0
|
||||
max_push_size = 8 * max(BATCH_SIZES)
|
||||
comm = CustomAllReduceV2(cpu_group, device, max_pull_size, max_push_size)
|
||||
if comm.disabled:
|
||||
raise RuntimeError("JIT CustomAllReduceV2 is disabled on this system")
|
||||
|
||||
return rank, world_size, device, cpu_group, nccl_group, comm
|
||||
|
||||
|
||||
def _all_gather_cat(x: torch.Tensor, group: dist.ProcessGroup) -> torch.Tensor:
|
||||
gathered = [torch.empty_like(x) for _ in range(dist.get_world_size(group=group))]
|
||||
dist.all_gather(gathered, x, group=group)
|
||||
return torch.cat(gathered, dim=-1)
|
||||
|
||||
|
||||
def _rmsnorm_ref(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
|
||||
x_fp32 = x.float()
|
||||
scale = (x_fp32.pow(2).mean(dim=-1, keepdim=True) + eps).rsqrt()
|
||||
return (x_fp32 * scale * weight.float()).to(x.dtype)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def worker_test(
|
||||
rank: int,
|
||||
world_size: int,
|
||||
device: torch.device,
|
||||
nccl_group: dist.ProcessGroup,
|
||||
comm,
|
||||
q_k_dim: tuple[int, int],
|
||||
batch_size: int,
|
||||
dtype: torch.dtype,
|
||||
) -> Optional[RuntimeError]:
|
||||
q_dim, k_dim = q_k_dim
|
||||
local_q_dim = q_dim // world_size
|
||||
local_k_dim = k_dim // world_size
|
||||
|
||||
q = torch.randn(batch_size, local_q_dim, device=device, dtype=dtype)
|
||||
k = torch.randn(batch_size, local_k_dim, device=device, dtype=dtype)
|
||||
q_weight = torch.randn(local_q_dim, device=device, dtype=dtype)
|
||||
k_weight = torch.randn(local_k_dim, device=device, dtype=dtype)
|
||||
|
||||
q_ref = _all_gather_cat(q, nccl_group)
|
||||
k_ref = _all_gather_cat(k, nccl_group)
|
||||
q_weight_ref = _all_gather_cat(q_weight.unsqueeze(0), nccl_group).squeeze(0)
|
||||
k_weight_ref = _all_gather_cat(k_weight.unsqueeze(0), nccl_group).squeeze(0)
|
||||
|
||||
q_expected = _rmsnorm_ref(q_ref, q_weight_ref, EPS)
|
||||
k_expected = _rmsnorm_ref(k_ref, k_weight_ref, EPS)
|
||||
q_expected = q_expected[:, rank * local_q_dim : (rank + 1) * local_q_dim]
|
||||
k_expected = k_expected[:, rank * local_k_dim : (rank + 1) * local_k_dim]
|
||||
|
||||
fused_parallel_qknorm(
|
||||
comm.obj,
|
||||
q,
|
||||
k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
EPS,
|
||||
)
|
||||
|
||||
try:
|
||||
triton.testing.assert_close(q, q_expected, atol=1e-2, rtol=1e-2)
|
||||
triton.testing.assert_close(k, k_expected, atol=1e-2, rtol=1e-2)
|
||||
except AssertionError as err:
|
||||
return RuntimeError(
|
||||
f"TP QKNorm mismatch for {batch_size=}, {dtype=}, {world_size=}, {rank=}: {err}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def worker_main() -> None:
|
||||
rank, world_size, device, cpu_group, nccl_group, comm = init_distributed()
|
||||
torch.cuda.set_stream(torch.cuda.Stream())
|
||||
|
||||
for q_k_dim, batch_size, dtype in TEST_CONFIG:
|
||||
error = worker_test(
|
||||
rank,
|
||||
world_size,
|
||||
device,
|
||||
nccl_group,
|
||||
comm,
|
||||
q_k_dim,
|
||||
batch_size,
|
||||
dtype,
|
||||
)
|
||||
result = torch.tensor([int(error is not None)])
|
||||
dist.all_reduce(result, group=cpu_group)
|
||||
if error is not None:
|
||||
print(str(error))
|
||||
if bool(result.item()):
|
||||
raise RuntimeError(
|
||||
f"TP QKNorm test failed for {q_k_dim=}, {batch_size=}, {dtype=}, {world_size=}"
|
||||
)
|
||||
|
||||
print(f"Rank {rank} passed all tests.")
|
||||
comm.close()
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
multiprocess_main(__file__, worker_main)
|
||||
Reference in New Issue
Block a user