Files
sgl-project--sglang/test/registered/jit/test_fused_store_index_cache.py
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

461 lines
15 KiB
Python

"""
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_amd_ci, 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, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
register_amd_ci(est_time=24, suite="nightly-amd-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"]))