sgl-project--sglang
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
506 行
19 KiB
Python
506 行
19 KiB
Python
"""SM120 FlashMLA sparse decode unit tests.
|
|
|
|
Validates the SM120-specific FlashMLA implementation that replaces the upstream
|
|
`flash_mla` CUDA kernel (unavailable on SM120 / RTX PRO 6000):
|
|
|
|
- ``_gather_and_dequant``: byte-precise dequant of paged FP8 + BF16 + UE8M0 KV
|
|
cache. Covers the dtype-reinterpretation surface that caused the historic
|
|
uint8 garbled-output regression (see progress doc §4).
|
|
- ``_sm120_sparse_decode_fwd``: pure-PyTorch reference path.
|
|
- ``flash_mla_sparse_decode_triton``: tiled Triton kernel.
|
|
- ``_apply_attn_sink`` / ``_merge_partial_attn``: post-processing helpers.
|
|
- ``flash_mla_with_kvcache_sm120``: entry-point dispatch on
|
|
``SGLANG_SM120_TRITON_FLASHMLA`` selects torch/triton paths and both yield
|
|
matching output.
|
|
|
|
DSv4 cache layout (per page):
|
|
data section: page_size * 576 bytes = 64 tokens * (448 nope + 128 rope)
|
|
scale section: page_size * 8 bytes = 64 tokens * (7 UE8M0 scales + 1 pad)
|
|
total bytes: page_size * 584 = stride(0) of k_cache
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention import flash_mla_sm120 as fmod
|
|
from sglang.srt.layers.attention.flash_mla_sm120 import (
|
|
_D,
|
|
_NOPE_DIM,
|
|
_NOPE_ROPE_STRIDE,
|
|
_NUM_TILES,
|
|
_ROPE_DIM,
|
|
_SCALE_STRIDE,
|
|
_TILE_SIZE,
|
|
_gather_and_dequant,
|
|
_sm120_sparse_decode_fwd,
|
|
flash_mla_with_kvcache_sm120,
|
|
)
|
|
from sglang.srt.layers.attention.flash_mla_sm120_triton import (
|
|
_apply_attn_sink,
|
|
_merge_partial_attn,
|
|
flash_mla_sparse_decode_triton,
|
|
)
|
|
from sglang.test.ci.ci_register import register_cuda_ci
|
|
from sglang.test.test_utils import CustomTestCase
|
|
|
|
register_cuda_ci(est_time=45, stage="base-b", runner_config="1-gpu-large")
|
|
|
|
|
|
# Per-token byte layout
|
|
_BYTES_PER_TOKEN = _NOPE_ROPE_STRIDE + _SCALE_STRIDE # 576 + 8 = 584
|
|
|
|
_IS_SM120 = torch.cuda.is_available() and torch.cuda.get_device_capability() == (12, 0)
|
|
|
|
|
|
def _build_kvcache(
|
|
num_pages: int,
|
|
page_size: int,
|
|
*,
|
|
device: torch.device,
|
|
seed: int = 0,
|
|
):
|
|
"""Build a synthetic FP8/BF16/UE8M0 KV cache.
|
|
|
|
Returns the (num_pages, page_size, 1, bpt) FP8-viewed cache plus the raw
|
|
nope FP8 (float), rope BF16 (float), and UE8M0 scale (float) reference
|
|
tensors for verification.
|
|
"""
|
|
g = torch.Generator(device="cpu").manual_seed(seed)
|
|
bpt = _BYTES_PER_TOKEN # 584 — also satisfies k_cache.stride(0) requirement
|
|
raw = torch.zeros(num_pages, page_size, bpt, dtype=torch.uint8, device=device)
|
|
|
|
# ---- Nope FP8 region: per-token bytes [0:448] ----
|
|
# Stay in [0, 0x6F] to avoid NaN/Inf in float8_e4m3fn.
|
|
nope_bytes = torch.randint(
|
|
0, 0x70, (num_pages, page_size, _NOPE_DIM), generator=g, dtype=torch.uint8
|
|
).to(device)
|
|
raw[:, :, :_NOPE_DIM] = nope_bytes
|
|
|
|
# ---- Rope BF16 region: per-token bytes [448:576] = 64 bf16 values ----
|
|
rope_bf16_vals = (
|
|
torch.randn((num_pages, page_size, _ROPE_DIM), generator=g, dtype=torch.float32)
|
|
.clamp(-2.0, 2.0)
|
|
.to(torch.bfloat16)
|
|
.to(device)
|
|
)
|
|
# View bf16 as 2 bytes per value -> 128 bytes per token rope region
|
|
rope_as_uint8 = rope_bf16_vals.contiguous().view(
|
|
torch.uint8
|
|
) # (num_pages, page_size, 128)
|
|
raw[:, :, _NOPE_DIM : _NOPE_DIM + _ROPE_DIM * 2] = rope_as_uint8
|
|
|
|
# ---- Scale section: starts at page_size * 576 ----
|
|
# 7 UE8M0 bytes per token + 1 pad. Keep exponents in a sane range
|
|
# (UE8M0 byte 'b' decodes to 2**(b-127)), so 120..130 gives ~[1/128, 8].
|
|
scale_bytes = torch.randint(
|
|
120,
|
|
131,
|
|
(num_pages, page_size, _NUM_TILES),
|
|
generator=g,
|
|
dtype=torch.uint8,
|
|
).to(device)
|
|
# raw is (num_pages, page_size, bpt) with bpt=584; data ends at 576, scale region 576..584
|
|
# Per-token scale offset relative to scale section: token_idx * 8.
|
|
# Note: the scale region in raw memory is at the END of the page after the
|
|
# data section. Since raw shape is (num_pages, page_size, bpt), where
|
|
# bpt=584, scale lives in raw[:, :, _NOPE_ROPE_STRIDE : _NOPE_ROPE_STRIDE+8].
|
|
# BUT the gather code expects layout
|
|
# raw_pages: (num_pages, page_bytes)
|
|
# scale_section_offset = page_size * 576
|
|
# i.e. data for ALL tokens first, then scale for ALL tokens. So we need
|
|
# the flat per-page layout: [tok0_data(576), tok1_data, ..., tokN-1_data,
|
|
# tok0_scale(8), tok1_scale, ...].
|
|
# We constructed raw as (num_pages, page_size, bpt) which interleaves data
|
|
# and scales per token. Build a fresh buffer with the correct flat order.
|
|
flat = torch.zeros(num_pages, page_size * bpt, dtype=torch.uint8, device=device)
|
|
# Data: per-token 576 bytes contiguous
|
|
flat_data = raw[:, :, :_NOPE_ROPE_STRIDE].reshape(
|
|
num_pages, page_size * _NOPE_ROPE_STRIDE
|
|
)
|
|
flat[:, : page_size * _NOPE_ROPE_STRIDE] = flat_data
|
|
# Scales: per-token 8 bytes (only first 7 written)
|
|
scale_block = torch.zeros(
|
|
num_pages, page_size, _SCALE_STRIDE, dtype=torch.uint8, device=device
|
|
)
|
|
scale_block[:, :, :_NUM_TILES] = scale_bytes
|
|
flat[:, page_size * _NOPE_ROPE_STRIDE :] = scale_block.reshape(
|
|
num_pages, page_size * _SCALE_STRIDE
|
|
)
|
|
|
|
# View as (num_pages, page_size, 1, bpt) float8_e4m3fn
|
|
k_cache = flat.view(num_pages, page_size, 1, bpt).view(torch.float8_e4m3fn)
|
|
# The Triton kernel expects k_cache.stride(0) == page_size * bpt.
|
|
assert k_cache.stride(0) == page_size * bpt
|
|
|
|
# Reference dequant per token (matches what _gather_and_dequant should produce):
|
|
nope_fp8 = nope_bytes.view(
|
|
torch.float8_e4m3fn
|
|
).float() # (num_pages, page_size, 448)
|
|
scale_e8m0 = scale_bytes.view(
|
|
torch.float8_e8m0fnu
|
|
).float() # (num_pages, page_size, 7)
|
|
nope_dequant = (
|
|
nope_fp8.view(num_pages, page_size, _NUM_TILES, _TILE_SIZE)
|
|
* scale_e8m0.view(num_pages, page_size, _NUM_TILES, 1)
|
|
).view(
|
|
num_pages, page_size, _NOPE_DIM
|
|
) # float32
|
|
ref_per_token = torch.cat(
|
|
[nope_dequant.to(torch.bfloat16), rope_bf16_vals], dim=-1
|
|
) # (num_pages, page_size, 512) bf16
|
|
return k_cache, ref_per_token
|
|
|
|
|
|
def _build_q_indices(
|
|
batch_size: int,
|
|
num_heads: int,
|
|
topk: int,
|
|
num_pages: int,
|
|
page_size: int,
|
|
*,
|
|
device: torch.device,
|
|
seed: int = 1,
|
|
):
|
|
g = torch.Generator(device="cpu").manual_seed(seed)
|
|
q = (
|
|
torch.randn((batch_size, 1, num_heads, _D), generator=g, dtype=torch.float32)
|
|
.clamp(-1.5, 1.5)
|
|
.to(torch.bfloat16)
|
|
.to(device)
|
|
)
|
|
# Each batch picks `topk` random valid token-level indices into the pool
|
|
pool_size = num_pages * page_size
|
|
indices = torch.zeros((batch_size, 1, topk), dtype=torch.int32, device=device)
|
|
for b in range(batch_size):
|
|
perm = torch.randperm(pool_size, generator=g)[:topk]
|
|
indices[b, 0] = perm.to(device=device, dtype=torch.int32)
|
|
return q, indices
|
|
|
|
|
|
@unittest.skipUnless(_IS_SM120, "SM120 (compute capability 12.0) required")
|
|
class TestGatherAndDequant(CustomTestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not torch.cuda.is_available():
|
|
raise unittest.SkipTest("CUDA required")
|
|
cls.device = torch.device("cuda")
|
|
|
|
def test_basic_dequant_matches_manual(self):
|
|
"""Per-token output equals nope_fp8 * scale concatenated with rope_bf16."""
|
|
num_pages, page_size = 3, 64
|
|
k_cache, ref_per_token = _build_kvcache(
|
|
num_pages, page_size, device=self.device, seed=42
|
|
)
|
|
# Pick 1 index per page (positions 0, 32, 63)
|
|
token_ids = torch.tensor(
|
|
[[0, 1 * page_size + 32, 2 * page_size + 63]],
|
|
dtype=torch.int32,
|
|
device=self.device,
|
|
)
|
|
out = _gather_and_dequant(k_cache, token_ids, page_size)
|
|
self.assertEqual(out.shape, (1, 3, _D))
|
|
# Expected entries
|
|
expected = torch.stack(
|
|
[
|
|
ref_per_token[0, 0],
|
|
ref_per_token[1, 32],
|
|
ref_per_token[2, 63],
|
|
],
|
|
dim=0,
|
|
).unsqueeze(0)
|
|
# bf16 dequant: allow up to 1 ULP per element. Scales are integral
|
|
# powers of 2 so the only loss is the fp8 -> bf16 mantissa rounding.
|
|
torch.testing.assert_close(out, expected, atol=1e-2, rtol=1e-2)
|
|
|
|
def test_dequant_handles_full_page_range(self):
|
|
"""All token positions in a single page produce correct output."""
|
|
num_pages, page_size = 2, 64
|
|
k_cache, ref_per_token = _build_kvcache(
|
|
num_pages, page_size, device=self.device, seed=7
|
|
)
|
|
token_ids = torch.arange(page_size, dtype=torch.int32, device=self.device).view(
|
|
1, page_size
|
|
)
|
|
out = _gather_and_dequant(k_cache, token_ids, page_size)
|
|
torch.testing.assert_close(
|
|
out, ref_per_token[0].unsqueeze(0), atol=1e-2, rtol=1e-2
|
|
)
|
|
|
|
|
|
@unittest.skipUnless(_IS_SM120, "SM120 (compute capability 12.0) required")
|
|
class TestSparseDecodeTritonVsTorch(CustomTestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not torch.cuda.is_available():
|
|
raise unittest.SkipTest("CUDA required")
|
|
cls.device = torch.device("cuda")
|
|
|
|
def _run(
|
|
self,
|
|
batch_size: int = 2,
|
|
num_heads: int = 4,
|
|
topk: int = 64,
|
|
page_size: int = 64,
|
|
num_pages: int = 4,
|
|
seed: int = 11,
|
|
with_topk_length: bool = True,
|
|
with_attn_sink: bool = False,
|
|
):
|
|
k_cache, _ = _build_kvcache(num_pages, page_size, device=self.device, seed=seed)
|
|
q, indices = _build_q_indices(
|
|
batch_size,
|
|
num_heads,
|
|
topk,
|
|
num_pages,
|
|
page_size,
|
|
device=self.device,
|
|
seed=seed + 100,
|
|
)
|
|
topk_length = None
|
|
if with_topk_length:
|
|
topk_length = torch.tensor(
|
|
[topk // 2, topk] if batch_size == 2 else [topk] * batch_size,
|
|
dtype=torch.int32,
|
|
device=self.device,
|
|
)
|
|
attn_sink = None
|
|
if with_attn_sink:
|
|
attn_sink = torch.full(
|
|
(num_heads,), -1.5, dtype=torch.float32, device=self.device
|
|
)
|
|
softmax_scale = _D ** (-0.5)
|
|
# Production passes config.v_head_dim == _D (512). Both impls return
|
|
# (B, 1, H, _D); the Triton kernel always computes the full nope+rope.
|
|
head_dim_v = _D
|
|
|
|
# PyTorch reference
|
|
ref_out, ref_lse = _sm120_sparse_decode_fwd(
|
|
q,
|
|
k_cache,
|
|
indices,
|
|
topk_length,
|
|
attn_sink,
|
|
head_dim_v=head_dim_v,
|
|
softmax_scale=softmax_scale,
|
|
)
|
|
|
|
# Triton
|
|
tri_out, tri_lse = flash_mla_sparse_decode_triton(
|
|
q,
|
|
k_cache,
|
|
indices,
|
|
topk_length,
|
|
attn_sink,
|
|
head_dim_v=head_dim_v,
|
|
softmax_scale=softmax_scale,
|
|
)
|
|
|
|
# Outputs both bf16, allow drift from online softmax base-2 vs base-e
|
|
# math + bf16 mantissa precision (~7 bits). 5e-2 abs/rel is generous
|
|
# but accounts for accumulated rounding across topk tokens.
|
|
torch.testing.assert_close(
|
|
tri_out.to(torch.float32),
|
|
ref_out.to(torch.float32),
|
|
atol=5e-2,
|
|
rtol=5e-2,
|
|
)
|
|
return ref_out, tri_out, ref_lse, tri_lse
|
|
|
|
def test_triton_vs_torch_basic(self):
|
|
self._run()
|
|
|
|
def test_triton_vs_torch_no_topk_length(self):
|
|
self._run(with_topk_length=False)
|
|
|
|
def test_triton_vs_torch_with_attn_sink(self):
|
|
self._run(with_attn_sink=True)
|
|
|
|
def test_triton_vs_torch_small_topk(self):
|
|
self._run(topk=32, num_heads=2)
|
|
|
|
def test_triton_vs_torch_negative_indices_are_masked(self):
|
|
"""Indices < 0 are 'invalid' and must contribute zero to output."""
|
|
k_cache, _ = _build_kvcache(4, 64, device=self.device, seed=3)
|
|
q, indices = _build_q_indices(2, 4, 32, 4, 64, device=self.device, seed=99)
|
|
# Half of each batch's indices set to -1
|
|
indices[:, :, 16:] = -1
|
|
topk_length = torch.tensor([32, 32], dtype=torch.int32, device=self.device)
|
|
|
|
ref_out, _ = _sm120_sparse_decode_fwd(
|
|
q, k_cache, indices, topk_length, None, _D, _D**-0.5
|
|
)
|
|
tri_out, _ = flash_mla_sparse_decode_triton(
|
|
q, k_cache, indices, topk_length, None, _D, _D**-0.5
|
|
)
|
|
torch.testing.assert_close(
|
|
tri_out.to(torch.float32),
|
|
ref_out.to(torch.float32),
|
|
atol=5e-2,
|
|
rtol=5e-2,
|
|
)
|
|
|
|
|
|
@unittest.skipUnless(_IS_SM120, "SM120 (compute capability 12.0) required")
|
|
class TestApplyAttnSink(CustomTestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not torch.cuda.is_available():
|
|
raise unittest.SkipTest("CUDA required")
|
|
cls.device = torch.device("cuda")
|
|
|
|
def test_sink_zero_means_full_dampen_to_half(self):
|
|
"""attn_sink == lse implies sink contributes equal weight: output halves."""
|
|
B, H, D = 1, 2, 8
|
|
lse = torch.zeros(B, 1, H, device=self.device, dtype=torch.float32)
|
|
attn_sink = torch.zeros(H, device=self.device, dtype=torch.float32)
|
|
out = torch.ones(B, 1, H, D, device=self.device, dtype=torch.bfloat16)
|
|
new_out, new_lse = _apply_attn_sink(out, lse, attn_sink)
|
|
# combined_lse = logaddexp(0, 0) = log(2). w = exp(-log2) = 0.5.
|
|
torch.testing.assert_close(
|
|
new_out.float(),
|
|
torch.full_like(new_out.float(), 0.5),
|
|
atol=2e-3,
|
|
rtol=2e-3,
|
|
)
|
|
torch.testing.assert_close(
|
|
new_lse,
|
|
torch.full_like(new_lse, 0.6931472), # log(2)
|
|
atol=1e-5,
|
|
rtol=1e-5,
|
|
)
|
|
|
|
def test_sink_dead_lse_stays_zero(self):
|
|
"""lse == -inf (no valid tokens) -> output stays zero (weight=0)."""
|
|
B, H, D = 1, 2, 4
|
|
lse = torch.full(
|
|
(B, 1, H), float("-inf"), device=self.device, dtype=torch.float32
|
|
)
|
|
attn_sink = torch.zeros(H, device=self.device, dtype=torch.float32)
|
|
out = torch.ones(B, 1, H, D, device=self.device, dtype=torch.bfloat16)
|
|
new_out, _ = _apply_attn_sink(out, lse, attn_sink)
|
|
torch.testing.assert_close(
|
|
new_out.float(), torch.zeros_like(new_out.float()), atol=0, rtol=0
|
|
)
|
|
|
|
|
|
@unittest.skipUnless(_IS_SM120, "SM120 (compute capability 12.0) required")
|
|
class TestMergePartialAttn(CustomTestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not torch.cuda.is_available():
|
|
raise unittest.SkipTest("CUDA required")
|
|
cls.device = torch.device("cuda")
|
|
|
|
def test_equal_lse_arithmetic_mean(self):
|
|
"""lse1 == lse2 -> merged output is the arithmetic mean of out1, out2."""
|
|
B, H, D = 1, 2, 4
|
|
lse = torch.zeros(B, 1, H, device=self.device, dtype=torch.float32)
|
|
out1 = torch.ones(B, 1, H, D, device=self.device, dtype=torch.bfloat16)
|
|
out2 = torch.full((B, 1, H, D), 3.0, device=self.device, dtype=torch.bfloat16)
|
|
merged, merged_lse = _merge_partial_attn(out1, lse, out2, lse)
|
|
torch.testing.assert_close(
|
|
merged.float(),
|
|
torch.full_like(merged.float(), 2.0),
|
|
atol=1e-2,
|
|
rtol=1e-2,
|
|
)
|
|
torch.testing.assert_close(
|
|
merged_lse,
|
|
torch.full_like(merged_lse, 0.6931472), # log(2)
|
|
atol=1e-5,
|
|
rtol=1e-5,
|
|
)
|
|
|
|
def test_one_dead_branch_passes_through(self):
|
|
"""If lse2 == -inf, merged equals out1."""
|
|
B, H, D = 1, 1, 4
|
|
lse1 = torch.zeros(B, 1, H, device=self.device, dtype=torch.float32)
|
|
lse2 = torch.full(
|
|
(B, 1, H), float("-inf"), device=self.device, dtype=torch.float32
|
|
)
|
|
out1 = torch.full((B, 1, H, D), 2.5, device=self.device, dtype=torch.bfloat16)
|
|
out2 = torch.full((B, 1, H, D), 99.0, device=self.device, dtype=torch.bfloat16)
|
|
merged, merged_lse = _merge_partial_attn(out1, lse1, out2, lse2)
|
|
torch.testing.assert_close(merged.float(), out1.float(), atol=1e-3, rtol=1e-3)
|
|
torch.testing.assert_close(merged_lse, lse1, atol=1e-5, rtol=1e-5)
|
|
|
|
|
|
@unittest.skipUnless(_IS_SM120, "SM120 (compute capability 12.0) required")
|
|
class TestEntryPointDispatch(CustomTestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not torch.cuda.is_available():
|
|
raise unittest.SkipTest("CUDA required")
|
|
cls.device = torch.device("cuda")
|
|
|
|
def test_torch_backend_matches_triton_backend(self):
|
|
"""SGLANG_SM120_TRITON_FLASHMLA toggles backend; both return matching out."""
|
|
k_cache, _ = _build_kvcache(4, 64, device=self.device, seed=5)
|
|
q, indices = _build_q_indices(1, 4, 32, 4, 64, device=self.device, seed=13)
|
|
topk_length = torch.tensor([32], dtype=torch.int32, device=self.device)
|
|
|
|
kwargs = dict(
|
|
q=q,
|
|
k_cache=k_cache,
|
|
indices=indices,
|
|
topk_length=topk_length,
|
|
attn_sink=None,
|
|
head_dim_v=_D,
|
|
softmax_scale=_D**-0.5,
|
|
)
|
|
|
|
with mock.patch.object(fmod, "_sm120_default_backend", "torch"):
|
|
out_torch, _ = flash_mla_with_kvcache_sm120(**kwargs)
|
|
with mock.patch.object(fmod, "_sm120_default_backend", "triton"):
|
|
out_triton, _ = flash_mla_with_kvcache_sm120(**kwargs)
|
|
|
|
torch.testing.assert_close(
|
|
out_torch.to(torch.float32),
|
|
out_triton.to(torch.float32),
|
|
atol=5e-2,
|
|
rtol=5e-2,
|
|
)
|
|
|
|
def test_flashinfer_backend_matches_triton(self):
|
|
"""FlashInfer SM120 sparse MLA decode matches Triton reference."""
|
|
import importlib
|
|
|
|
if importlib.util.find_spec("flashinfer.sparse_mla_sm120") is None:
|
|
self.skipTest("FlashInfer SM120 sparse MLA not available")
|
|
|
|
k_cache, _ = _build_kvcache(4, 64, device=self.device, seed=5)
|
|
q, indices = _build_q_indices(1, 4, 32, 4, 64, device=self.device, seed=13)
|
|
topk_length = torch.tensor([32], dtype=torch.int32, device=self.device)
|
|
|
|
kwargs = dict(
|
|
q=q,
|
|
k_cache=k_cache,
|
|
indices=indices,
|
|
topk_length=topk_length,
|
|
attn_sink=None,
|
|
head_dim_v=_D,
|
|
softmax_scale=_D**-0.5,
|
|
)
|
|
|
|
with mock.patch.object(fmod, "_sm120_default_backend", "triton"):
|
|
out_triton, _ = flash_mla_with_kvcache_sm120(**kwargs)
|
|
with mock.patch.object(fmod, "_sm120_default_backend", "flashinfer"):
|
|
out_fi, _ = flash_mla_with_kvcache_sm120(**kwargs)
|
|
|
|
torch.testing.assert_close(
|
|
out_fi.to(torch.float32),
|
|
out_triton.to(torch.float32),
|
|
atol=5e-2,
|
|
rtol=5e-2,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(unittest.main())
|