Files
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

467 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""SM120 FlashMLA sparse decode implementation.
On SM120 (Blackwell Desktop / RTX PRO 6000) the flash_mla CUDA kernel
is not available, so this module provides alternative implementations:
- A fused Triton kernel (default, ``SGLANG_SM120_TRITON_FLASHMLA=1``)
- A pure-PyTorch fallback (``SGLANG_SM120_TRITON_FLASHMLA=0``)
The FP8 KV cache uses a page-internal layout where NOPE+ROPE data has
stride (nope_dim + rope_dim*2) per token, and scales are stored in a
separate region at the end of each page.
"""
import logging
import math
import torch
import triton
import triton.language as tl
from sglang.srt.environ import envs
logger = logging.getLogger(__name__)
# Page layout constants for DSv4-Flash (MODEL1):
# nope_dim = 448, rope_dim = 64, quantize_block_size = 64
# nope_rope_stride = 448 + 64*2 = 576 bytes per token
# scale_stride = ceil(448/64) + 1 = 8 bytes per token (7 scales + 1 pad)
# bytes_per_token = 448 + 128 + 8 = 584
# page_bytes = ceil_div(page_size * 584, 576) * 576
_NOPE_DIM = 448
_ROPE_DIM = 64
_NOPE_ROPE_STRIDE = _NOPE_DIM + _ROPE_DIM * 2 # 576
_TILE_SIZE = 64
_NUM_TILES = _NOPE_DIM // _TILE_SIZE # 7
_SCALE_STRIDE = _NUM_TILES + 1 # 8 (7 scales + 1 pad)
_D = _NOPE_DIM + _ROPE_DIM # 512
def _gather_and_dequant(k_cache, indices, page_size):
"""Gather KV entries from the paged buffer using correct page-internal addressing.
Args:
k_cache: (num_pages, page_size, 1, bytes_per_token) float8_e4m3fn
Non-contiguous view of the raw page buffer.
indices: (...) int32/int64, token-level indices. -1 = invalid.
page_size: tokens per page (256)
Returns:
kv: (..., _D) bfloat16, dequantized KV vectors
"""
idx_shape = indices.shape
flat_idx = indices.reshape(-1) # (N,)
N = flat_idx.shape[0]
device = k_cache.device
# Page-level addressing
page_bytes = k_cache.stride(0) # actual byte stride between pages
pages = flat_idx // page_size
offsets = flat_idx % page_size
# Clamp invalid indices
safe_pages = pages.clamp(min=0)
safe_offsets = offsets.clamp(min=0)
# Access raw buffer as uint8 — use as_strided to get full page view
num_pages = k_cache.shape[0]
raw_pages = k_cache.as_strided(
(num_pages, page_bytes),
(page_bytes, 1),
).view(
torch.uint8
) # (num_pages, page_bytes) uint8
# Note: float8_e4m3fn and uint8 are both 1 byte, view is safe
# Compute byte offsets within each page
# NOPE: page[safe_page, safe_offset * 576 + 0:448]
# ROPE: page[safe_page, safe_offset * 576 + 448:576]
# SCALES: page[safe_page, page_size * 576 + safe_offset * 8 + 0:7]
nope_base = safe_offsets * _NOPE_ROPE_STRIDE # (N,)
nope_offsets = nope_base.unsqueeze(-1) + torch.arange(
_NOPE_DIM, device=device, dtype=torch.long
) # (N, 448)
rope_base = nope_base + _NOPE_DIM # (N,)
rope_offsets = rope_base.unsqueeze(-1) + torch.arange(
_ROPE_DIM * 2, device=device, dtype=torch.long
) # (N, 128)
scale_section_offset = page_size * _NOPE_ROPE_STRIDE # 147456
scale_base = scale_section_offset + safe_offsets * _SCALE_STRIDE # (N,)
scale_offsets = scale_base.unsqueeze(-1) + torch.arange(
_NUM_TILES, device=device, dtype=torch.long
) # (N, 7)
# Gather bytes per page — use advanced indexing
# raw_pages[safe_pages, nope_offsets] → (N, 448)
page_idx_nope = safe_pages.unsqueeze(-1).expand_as(nope_offsets)
nope_bytes = raw_pages[page_idx_nope, nope_offsets] # (N, 448) uint8
page_idx_rope = safe_pages.unsqueeze(-1).expand_as(rope_offsets)
rope_bytes = raw_pages[page_idx_rope, rope_offsets] # (N, 128) uint8
page_idx_scale = safe_pages.unsqueeze(-1).expand_as(scale_offsets)
scale_bytes = raw_pages[page_idx_scale, scale_offsets] # (N, 7) uint8
# Reinterpret dtypes
nope_fp8 = nope_bytes.view(torch.float8_e4m3fn) # (N, 448)
rope_bf16 = rope_bytes.contiguous().view(torch.bfloat16) # (N, 64)
scale_e8m0 = scale_bytes.view(torch.float8_e8m0fnu) # (N, 7)
# Dequantize: nope_tile * scale_tile → bf16 (vectorized)
result = torch.empty(N, _D, dtype=torch.bfloat16, device=device)
result[:, :_NOPE_DIM] = (
(
nope_fp8.view(N, _NUM_TILES, _TILE_SIZE).float()
* scale_e8m0.view(N, _NUM_TILES, 1).float()
)
.view(N, _NOPE_DIM)
.to(torch.bfloat16)
)
result[:, _NOPE_DIM:] = rope_bf16
return result.reshape(*idx_shape, _D)
def _sm120_sparse_decode_fwd(
q,
k_cache,
indices,
topk_length,
attn_sink,
head_dim_v,
softmax_scale,
extra_k_cache=None,
extra_indices=None,
extra_topk_length=None,
):
B, s_q, H_q, D_qk = q.shape
num_pages, page_size, H_k, bpt = k_cache.shape
topk = indices.shape[-1]
invalid_mask = indices < 0
safe_indices = indices.clamp(min=0)
if topk_length is not None:
topk_range = torch.arange(topk, device=topk_length.device).view(1, 1, topk)
invalid_mask = invalid_mask | (topk_range >= topk_length.view(B, 1, 1))
# Gather and dequantize using page-aware addressing
gathered_kv = _gather_and_dequant(k_cache, safe_indices, page_size)
if extra_k_cache is not None and extra_indices is not None:
extra_topk = extra_indices.shape[-1]
extra_page_size = extra_k_cache.shape[1]
extra_invalid = extra_indices < 0
extra_safe = extra_indices.clamp(min=0)
if extra_topk_length is not None:
extra_range = torch.arange(
extra_topk, device=extra_topk_length.device
).view(1, 1, extra_topk)
extra_invalid = extra_invalid | (
extra_range >= extra_topk_length.view(B, 1, 1)
)
extra_kv = _gather_and_dequant(extra_k_cache, extra_safe, extra_page_size)
gathered_kv = torch.cat([gathered_kv, extra_kv], dim=2)
invalid_mask = torch.cat([invalid_mask, extra_invalid], dim=2)
gathered_kv[invalid_mask] = 0.0
q_f = q.float()
kv_f = gathered_kv.float()
kv_d = kv_f.shape[-1]
if D_qk != kv_d:
q_f = q_f[..., :kv_d]
scores = torch.einsum("bshd,bstd->bsht", q_f, kv_f) * softmax_scale
scores.masked_fill_(invalid_mask.unsqueeze(2).expand_as(scores), float("-inf"))
lse = torch.logsumexp(scores, dim=-1)
if attn_sink is not None:
lse_for_out = torch.logsumexp(
torch.stack([lse, attn_sink.view(1, 1, H_q).expand_as(lse)], dim=0), dim=0
)
else:
lse_for_out = lse.clone()
lonely = lse == float("-inf")
lse_for_out[lonely] = float("inf")
weights = torch.exp(scores - lse_for_out.unsqueeze(-1))
out = torch.einsum("bsht,bstv->bshv", weights, kv_f[..., :head_dim_v])
out[lonely.unsqueeze(-1).expand_as(out)] = 0.0
return out.to(torch.bfloat16), lse.permute(0, 2, 1)
# SM120 FlashMLA: default FlashInfer (CUTLASS SM120 sparse MLA decode).
# Override with SGLANG_SM120_FLASHMLA_BACKEND=triton|torch to force fallback.
_sm120_default_backend = envs.SGLANG_SM120_FLASHMLA_BACKEND.get()
def flash_mla_with_kvcache_sm120(**kwargs):
"""SM120 FlashMLA sparse decode entry point.
Dispatches to FlashInfer (default if available), Triton, or PyTorch fallback.
"""
q = kwargs["q"]
k_cache = kwargs["k_cache"]
indices = kwargs["indices"]
topk_length = kwargs.get("topk_length")
attn_sink = kwargs.get("attn_sink")
head_dim_v = kwargs["head_dim_v"]
softmax_scale = kwargs.get("softmax_scale")
if softmax_scale is None:
softmax_scale = q.shape[-1] ** (-0.5)
extra_k_cache = kwargs.get("extra_k_cache")
extra_indices = kwargs.get("extra_indices_in_kvcache")
extra_topk_length = kwargs.get("extra_topk_length")
if _sm120_default_backend == "flashinfer":
return _flash_mla_flashinfer(
q,
k_cache,
indices,
topk_length,
attn_sink,
head_dim_v,
softmax_scale,
extra_k_cache,
extra_indices,
extra_topk_length,
)
if _sm120_default_backend == "triton":
from sglang.srt.layers.attention.flash_mla_sm120_triton import (
flash_mla_sparse_decode_triton,
)
out, lse = flash_mla_sparse_decode_triton(
q,
k_cache,
indices,
topk_length,
attn_sink,
head_dim_v,
softmax_scale,
extra_k_cache,
extra_indices,
extra_topk_length,
)
return (out, lse)
out, lse = _sm120_sparse_decode_fwd(
q,
k_cache,
indices,
topk_length,
attn_sink,
head_dim_v,
softmax_scale,
extra_k_cache,
extra_indices,
extra_topk_length,
)
return (out, lse)
# --- Page-split utilities: pbs=256 → pbs=64 ---
# SGLang SWA KV cache footer layout per 256-token page:
# [data: 256 * 576 bytes] [scale: 256 * 8 bytes] [padding]
# FlashInfer decode_dsv4 expects per 64-token page:
# [data: 64 * 576 bytes] [scale: 64 * 8 bytes] [padding to 37440]
_PBS_SRC = 256 # SGLang physical page size
_PBS_DST = 64 # FlashInfer page_block_size
_NOPE_ROPE_STRIDE = 576 # bytes per token for nope+rope
_SCALE_STRIDE = 8 # bytes per token for scale (7 + 1 pad)
_BYTES_PER_DST_PAGE = (
_PBS_DST * _NOPE_ROPE_STRIDE + _PBS_DST * _SCALE_STRIDE
) # 64*576 + 64*8 = 37376 + 512 = 37888
# Padded to 576 alignment
_BYTES_PER_DST_PAGE_PADDED = math.ceil(_BYTES_PER_DST_PAGE / 576) * 576 # 37440
@triton.jit
def _page_split_kernel(
src_ptr,
dst_ptr,
N_pages,
src_stride0: tl.constexpr,
dst_stride0: tl.constexpr,
DATA_PER_SUB: tl.constexpr, # 64 * 576 = 36864
SCALE_PER_SUB: tl.constexpr, # 64 * 8 = 512
SRC_SCALE_OFF: tl.constexpr, # 256 * 576 = 147456
DST_SCALE_OFF: tl.constexpr, # 64 * 576 = 36864
RATIO: tl.constexpr, # 4
BLOCK_SIZE: tl.constexpr,
):
"""Fused page-split: copy data+scale for all sub-pages in one kernel."""
pid = tl.program_id(0)
page_idx = pid // RATIO
sub = pid % RATIO
if page_idx >= N_pages:
return
src_base = src_ptr + page_idx * src_stride0
dst_base = dst_ptr + (page_idx * RATIO + sub) * dst_stride0
# Copy data region: DATA_PER_SUB bytes from src offset sub*DATA_PER_SUB
data_src_off = sub * DATA_PER_SUB
for start in tl.range(0, DATA_PER_SUB, BLOCK_SIZE):
offs = start + tl.arange(0, BLOCK_SIZE)
mask = offs < DATA_PER_SUB
vals = tl.load(src_base + data_src_off + offs, mask=mask)
tl.store(dst_base + offs, vals, mask=mask)
# Copy scale region: SCALE_PER_SUB bytes
scale_src_off = SRC_SCALE_OFF + sub * SCALE_PER_SUB
for start in tl.range(0, SCALE_PER_SUB, BLOCK_SIZE):
offs = start + tl.arange(0, BLOCK_SIZE)
mask = offs < SCALE_PER_SUB
vals = tl.load(src_base + scale_src_off + offs, mask=mask)
tl.store(dst_base + DST_SCALE_OFF + offs, vals, mask=mask)
def _split_kv_pages_to_64(kv_u8: torch.Tensor, src_pbs: int) -> torch.Tensor:
"""Split pbs=N footer-format pages into pbs=64 footer-format pages.
Uses a fused Triton kernel to do all sub-page copies in a single launch
instead of 8 separate copy kernels (4 sub-pages × 2 regions).
"""
assert src_pbs % _PBS_DST == 0 and src_pbs >= _PBS_DST
if src_pbs == _PBS_DST:
return kv_u8
N = kv_u8.shape[0]
ratio = src_pbs // _PBS_DST
num_dst_pages = N * ratio
from sglang.srt.runtime_context import get_resources
# Pre-allocated grow-only buffer for page-split output per device.
dev = kv_u8.device
buffers = get_resources().buffers
key = f"flash_mla_sm120_split:{dev}"
buf = buffers.get(key)
if buf is None or buf.shape[0] < num_dst_pages:
buf = torch.empty(
num_dst_pages,
_BYTES_PER_DST_PAGE_PADDED,
dtype=torch.uint8,
device=dev,
)
buffers[key] = buf
out = buf[:num_dst_pages]
# Get raw 2D view of source
src_2d = kv_u8
if src_2d.ndim == 4:
src_stride0 = src_2d.stride(0)
src_2d = torch.as_strided(src_2d, (N, src_stride0), (src_stride0, 1))
else:
src_stride0 = src_2d.stride(0)
grid = (N * ratio,)
_page_split_kernel[grid](
src_2d,
out,
N,
src_stride0,
_BYTES_PER_DST_PAGE_PADDED,
_PBS_DST * _NOPE_ROPE_STRIDE, # DATA_PER_SUB = 36864
_PBS_DST * _SCALE_STRIDE, # SCALE_PER_SUB = 512
src_pbs * _NOPE_ROPE_STRIDE, # SRC_SCALE_OFF = 147456
_PBS_DST * _NOPE_ROPE_STRIDE, # DST_SCALE_OFF = 36864
ratio, # RATIO = 4
1024, # BLOCK_SIZE
)
bpt = _NOPE_ROPE_STRIDE + _SCALE_STRIDE # 584
return out.as_strided(
(num_dst_pages, _PBS_DST, 1, bpt),
(_BYTES_PER_DST_PAGE_PADDED, bpt, bpt, 1),
)
def _flash_mla_flashinfer(
q,
k_cache,
indices,
topk_length,
attn_sink,
head_dim_v,
softmax_scale,
extra_k_cache,
extra_indices,
extra_topk_length,
):
"""FlashInfer SM120 sparse MLA via sparse_mla_sm120_decode_dsv4.
SGLang SWA pool uses page_size=256 (footer format: 256*576 bytes data + 256*8 bytes scale).
FlashInfer decode_dsv4 fast path requires page_block_size=64 (footer: 64*576 + 64*8).
We split 256-token pages into 4 virtual 64-token pages.
Token indices are invariant under page-split (identity mapping).
"""
from flashinfer.mla._sparse_mla_sm120 import sparse_mla_sm120_decode_dsv4
B, _, H, D = q.shape # (batch, 1, num_heads, head_dim)
dev = q.device
# --- Page-split: convert pbs=N kv_cache to pbs=64 view ---
kv_u8 = k_cache.view(torch.uint8) if k_cache.dtype != torch.uint8 else k_cache
src_pbs = k_cache.shape[1] if k_cache.ndim >= 3 else _PBS_SRC
kv_64 = _split_kv_pages_to_64(kv_u8, src_pbs) if src_pbs != _PBS_DST else kv_u8
extra_kv_u8 = (
extra_k_cache.view(torch.uint8)
if extra_k_cache is not None and extra_k_cache.dtype != torch.uint8
else extra_k_cache
)
extra_kv_64 = extra_kv_u8
# Indices: no remapping needed (page-split preserves token addressing).
idx = indices.squeeze(1) if indices.dim() == 3 else indices
extra_idx = (
extra_indices.squeeze(1)
if extra_indices is not None and extra_indices.dim() == 3
else extra_indices
)
output = torch.empty(B, H, head_dim_v, dtype=torch.bfloat16, device=dev)
out_lse = torch.empty(B, H, dtype=torch.float32, device=dev)
# Pre-allocate split-K scratch for decode-dsv4 fast path.
topk = idx.shape[-1]
extra_topk = extra_idx.shape[-1] if extra_idx is not None else 0
_BI = 64
num_splits = (topk + _BI - 1) // _BI + (
(extra_topk + _BI - 1) // _BI if extra_topk > 0 else 0
)
mid_out = torch.empty(
B, H, num_splits, head_dim_v, dtype=torch.bfloat16, device=dev
)
mid_lse = torch.empty(B, H, num_splits, dtype=torch.float32, device=dev)
sparse_mla_sm120_decode_dsv4(
q=q.squeeze(1) if q.ndim == 4 else q,
kv_cache=kv_64,
indices=idx,
mid_out=mid_out,
mid_lse=mid_lse,
output=output,
out_lse=out_lse,
sm_scale=softmax_scale,
topk_length=topk_length,
attn_sink=attn_sink,
extra_kv_cache=extra_kv_64,
extra_indices=extra_idx,
extra_topk_length=extra_topk_length,
)
return (output.unsqueeze(1), None)