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
461 lines
15 KiB
Python
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"]))
|