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