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

372 lines
14 KiB
Python

# MSA (fmha_sm100) drop-in for the MiniMax-M3 main sparse-attention step.
#
# Replaces only step 3 of MiniMax sparse prefill/decode. The lightning indexer
# (steps 1-2) is unchanged and still produces `topk_idx`.
# NVIDIA Blackwell (SM100/sm_103) only; callers gate on `msa_available()`.
from __future__ import annotations
import functools
from typing import Optional
import torch
class MSAUnavailableError(RuntimeError):
"""Raised when fmha_sm100 cannot serve the MiniMax MSA path."""
@functools.lru_cache(maxsize=1)
def _load_fmha_sm100():
try:
from fmha_sm100 import fmha_sm100, fmha_sm100_plan
except Exception as err:
raise MSAUnavailableError(
"fmha_sm100 or fmha_sm100_plan is not importable"
) from err
if not callable(fmha_sm100) or not callable(fmha_sm100_plan):
raise MSAUnavailableError("fmha_sm100 exports must be callable")
return fmha_sm100, fmha_sm100_plan
def _run_fmha_sm100_plan(*args, **kwargs):
_, fmha_sm100_plan = _load_fmha_sm100()
try:
return fmha_sm100_plan(*args, **kwargs)
except (AttributeError, RuntimeError, TypeError) as err:
raise MSAUnavailableError("fmha_sm100_plan failed") from err
@functools.lru_cache(maxsize=1)
def msa_available() -> bool:
"""True iff the fmha_sm100 sparse kernels and plan API are usable here."""
try:
cap = torch.cuda.get_device_capability()
except Exception:
return False
# SM100 family: B200 (10,0) and B300 (10,3). fmha_sm100/jit.py emits both
# sm_100a and sm_103a; the kernels run on either.
if cap[0] != 10 or cap[1] not in (0, 3):
return False
try:
_load_fmha_sm100()
return True
except MSAUnavailableError:
return False
def _build_page_table(
req_to_token: torch.Tensor, # [max_reqs, max_kv_len], physical slot per logical pos
slot_ids: torch.Tensor, # [batch]
seq_lens: torch.Tensor, # [batch] total K length (prefix + chunk)
page_size: int,
) -> torch.Tensor:
"""Flattened physical page ids per request (MSA `kv_indices`).
sglang's paged allocator stores page_size contiguous physical slots per page, so the
physical page of logical position p is ``req_to_token[req, p] // page_size`` and is the
same for every p within a page. We read one slot per logical page to recover the table.
Vectorized (no per-request Python loop): pages are packed contiguously by request in the
same order MSA's planner expects (``kv_page_indptr = cumsum(ceil(seq_lens/page_size))``).
``searchsorted`` maps each packed page slot back to its request; one ``.item()`` recovers
the total page count (this runs eagerly, outside CUDA-graph capture).
"""
P = page_size
n_pages = (seq_lens.to(torch.int64) + (P - 1)) // P # [batch]
offsets = (
torch.cumsum(n_pages, 0) - n_pages
) # [batch] exclusive page offset per request
total = int(n_pages.sum().item())
idx = torch.arange(
total, device=req_to_token.device
) # packed page slot -> (req, page)
req = torch.searchsorted(
offsets + n_pages, idx, right=True
) # request id per packed slot
logical_first = (idx - offsets[req]) * P # first logical position of that page
rows = slot_ids[req].to(torch.int64)
return (req_to_token[rows, logical_first] // P).to(torch.int32)
def msa_sparse_prefill_main(
q: torch.Tensor, # [total_q, num_q_heads, head_dim]
k_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (slot-major NHD)
v_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim]
topk_idx: torch.Tensor, # [num_kv_heads, total_q, topk] (0-based, -1 pad) -- step1/2 output
req_to_token: torch.Tensor, # [max_reqs, max_kv_len]
slot_ids: torch.Tensor, # [batch]
cu_seqlens: torch.Tensor, # [batch+1] cumulative Q lengths
seq_lens: torch.Tensor, # [batch] total K length (prefix + chunk)
prefix_lens: torch.Tensor, # [batch]
block_size_k: int, # == page_size == 128 for M3
sm_scale: Optional[float] = None,
) -> torch.Tensor:
"""Drop-in for flash_prefill_with_gqa_share_sparse using MSA fmha_sm100.
Returns o [total_q, num_q_heads, head_dim].
"""
fmha_sm100, _ = _load_fmha_sm100()
max_slots, num_kv_heads, head_dim = k_cache.shape
num_q_heads = q.shape[1]
P = block_size_k
topk = topk_idx.shape[-1]
if max_slots % P != 0:
raise ValueError(f"max_slots={max_slots} not divisible by page_size={P}")
if sm_scale is None:
sm_scale = head_dim**-0.5
# Whole pool as MSA paged KV: [num_phys_pages, num_kv_heads, P, head_dim].
n_phys_pages = max_slots // P
k_paged = k_cache.view(n_phys_pages, P, num_kv_heads, head_dim).permute(0, 2, 1, 3)
v_paged = v_cache.view(n_phys_pages, P, num_kv_heads, head_dim).permute(0, 2, 1, 3)
# Per-request Q lengths (extend) and physical page table.
qo_segment_lens = (cu_seqlens[1:] - cu_seqlens[:-1]).to(torch.int32)
kv_indices = _build_page_table(req_to_token, slot_ids, seq_lens, P)
# topk_idx [Hkv, total_q, topk] -> kv_block_indexes [total_q, Hkv, topk].
kv_block_indexes = topk_idx.permute(1, 0, 2).contiguous().to(torch.int32)
plan = _run_fmha_sm100_plan(
qo_segment_lens,
seq_lens.to(torch.int32),
num_q_heads,
num_kv_heads=num_kv_heads,
page_size=P,
kv_block_num=topk,
causal=True,
qo_offset=prefix_lens.to(torch.int32),
)
o, _ = fmha_sm100(
q,
k_paged,
v_paged,
plan,
sm_scale=sm_scale,
kv_indices=kv_indices,
kv_block_indexes=kv_block_indexes,
)
return o
def build_msa_decode_meta(
k_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim]
req_to_token: torch.Tensor,
slot_ids: torch.Tensor, # [batch]
seq_lens: torch.Tensor, # [batch] cached KV length per request
num_q_heads: int,
block_size_k: int,
topk: int,
):
"""Per-forward MSA decode metadata (page table + fmha plan), shared across layers.
Within one decode forward every sparse layer has the same batch, seq_lens, page size
and topk, so the physical page table and the fmha_sm100 plan are identical for all of
them — only ``kv_block_indexes`` (the per-layer top-k selection) changes. Building these
once per forward instead of once per layer removes the dominant host-side overhead
(page-table build + the host-side ``fmha_sm100_plan``) from the 57-layer decode loop.
Used only by the standalone parity harnesses; the serving backend builds eager-decode
metadata via ``build_msa_decode_cg_plan`` + ``update_msa_decode_cg_meta`` instead.
"""
max_slots, num_kv_heads, _ = k_cache.shape
P = block_size_k
if max_slots % P != 0:
raise ValueError(f"max_slots={max_slots} not divisible by page_size={P}")
B = slot_ids.shape[0]
kv_indices = _build_page_table(req_to_token, slot_ids, seq_lens, P)
seq_lens_i32 = seq_lens.to(torch.int32)
plan = _run_fmha_sm100_plan(
torch.ones(B, dtype=torch.int32),
seq_lens_i32,
num_q_heads,
num_kv_heads=num_kv_heads,
page_size=P,
kv_block_num=topk,
causal=False,
qo_offset=seq_lens_i32 - 1, # decode query sits at the last cached position
)
return kv_indices, plan
# ---------------------------------------------------------------------------
# Eager-only MSA decode plan (NOT used under CUDA graph)
#
# WARNING: the fmha_sm100 sparse decode kernel is NOT cuda-graph-safe — captured
# and replayed it returns silently wrong results that compound across replays
# (~14% GSM8K loss on B200). The backend routes decode to the cuda-graph-safe
# Triton sparse path whenever decode runs under a CUDA graph (see
# MiniMaxSparseAttnBackend._use_msa_decode); this plan is reachable ONLY in eager
# decode (no decode CUDA graph), where there is no capture/replay. Do NOT wire it
# back into a captured graph — that reintroduces the ~14% regression.
#
# The build-once / replay-update structure below (refreshing the four length
# tensors ``{kv_segment_lens, kv_segment_offsets, kv_page_indptr, qo_offset}`` and
# the page table in place) is a leftover from the abandoned capture-once attempt;
# it is kept only because eager decode reuses one per-forward plan across layers.
# ---------------------------------------------------------------------------
_MSA_CG_LEN_KEYS = (
"kv_segment_lens",
"kv_segment_offsets",
"kv_page_indptr",
"qo_offset",
)
def _check_cg_plan_layout(plan) -> None:
"""Fail fast if fmha_sm100's plan layout drifted from what replay-update
assumes (dict at tuple index 3 holding the four length tensors) — these are
undocumented fmha_sm100 internals."""
if not (isinstance(plan, tuple) and len(plan) > 3 and isinstance(plan[3], dict)):
raise MSAUnavailableError(
"fmha_sm100_plan no longer returns a tuple with a metadata dict at index 3; "
"the MSA CUDA-graph decode path must be revalidated against this fmha_sm100 "
"version. Set SGLANG_DISABLE_MSA=1 to use the Triton path meanwhile."
)
missing = [k for k in _MSA_CG_LEN_KEYS if not torch.is_tensor(plan[3].get(k))]
if missing:
raise MSAUnavailableError(
f"fmha_sm100 plan is missing length tensors {missing}; the MSA CUDA-graph "
"decode path must be revalidated against this fmha_sm100 version. "
"Set SGLANG_DISABLE_MSA=1 to use the Triton path meanwhile."
)
def build_msa_decode_cg_plan(
num_q_heads: int,
num_kv_heads: int,
block_size_k: int,
topk: int,
batch_size: int,
device: Optional[torch.device] = None,
):
"""Persistent fmha_sm100 decode plan for one batch size (CUDA-graph stable).
Built once per captured batch size. The worklist is length-independent (it uses
``topk * page_size`` internally), so the reference KV length here only has to make
every topk block valid; the length-dependent tensors are overwritten each step by
``update_msa_decode_cg_meta``. Returns the plan tuple to pass to ``fmha_sm100``.
"""
P = block_size_k
ref_len = topk * P # length at which all topk blocks exist -> full worklist
qo = torch.ones(batch_size, dtype=torch.int32)
kv = torch.full((batch_size,), ref_len, dtype=torch.int32)
plan = _run_fmha_sm100_plan(
qo,
kv,
num_q_heads,
num_kv_heads=num_kv_heads,
page_size=P,
kv_block_num=topk,
causal=False,
qo_offset=kv - 1,
device=device,
)
_check_cg_plan_layout(plan)
return plan
def update_msa_decode_cg_meta(
plan,
kv_indices_buf: torch.Tensor, # persistent page-table buffer [batch * max_pages]
req_to_token: torch.Tensor,
slot_ids: torch.Tensor, # [batch]
seq_lens: torch.Tensor, # [batch] cached KV length per request
block_size_k: int,
topk: int,
num_q_heads: int,
num_kv_heads: int,
):
"""Refresh the persistent decode plan's length-dependent tensors + page table IN PLACE.
Host-side (calls fmha_sm100_plan and one ``.item()``); MUST run outside CUDA-graph
capture — i.e. only from ``init_forward_metadata_out_graph``. The captured graph then
reads the same plan-tensor and ``kv_indices_buf`` addresses on replay.
"""
P = block_size_k
B = seq_lens.shape[0]
seq_lens_i32 = seq_lens.to(torch.int32)
# Fresh plan for the real lengths; copy only its four length-dependent tensors into the
# persistent plan (same shapes — they depend on batch size, not length). The fresh
# worklist is identical to the persistent one (topk*P based) and is discarded.
# qo_offset is clamped: graph replay pads the batch with seq_len==0 slots
# (masked via kv_segment_lens==0, but seq_len-1 would be -1).
fresh = _run_fmha_sm100_plan(
torch.ones(B, dtype=torch.int32),
seq_lens_i32,
num_q_heads,
num_kv_heads=num_kv_heads,
page_size=P,
kv_block_num=topk,
causal=False,
qo_offset=(seq_lens_i32 - 1).clamp_min(0),
device=seq_lens.device,
)
_check_cg_plan_layout(fresh)
pd, fd = plan[3], fresh[3]
for k in _MSA_CG_LEN_KEYS:
pd[k].copy_(fd[k])
table = _build_page_table(req_to_token, slot_ids, seq_lens, P)
kv_indices_buf[: table.numel()].copy_(table)
def msa_sparse_decode_main(
q: torch.Tensor, # [batch, num_q_heads, head_dim] (1 query token per request)
k_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (slot-major NHD)
v_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim]
topk_idx: torch.Tensor, # [num_kv_heads, batch, topk] (0-based, -1 pad)
req_to_token: torch.Tensor, # [max_reqs, max_kv_len]
slot_ids: torch.Tensor, # [batch]
seq_lens: torch.Tensor, # [batch] cached KV length per request
block_size_k: int, # == page_size == 128
sm_scale: Optional[float] = None,
kv_indices: Optional[
torch.Tensor
] = None, # precomputed page table (per-forward cache)
plan=None, # precomputed fmha_sm100 plan (per-forward cache)
) -> torch.Tensor:
"""Drop-in for flash_decode_with_gqa_share_sparse using MSA fmha_sm100.
Each request is one decode query at absolute position seq_len-1 attending to its
cached KV through the topk selected 128-blocks. Returns o [batch, num_q_heads, head_dim].
``kv_indices`` / ``plan`` are shared across all sparse layers of a forward; the serving
backend builds them once via ``build_msa_decode_cg_plan`` + ``update_msa_decode_cg_meta``
(eager decode only) and passes them in. When omitted (only the standalone parity
harnesses) they are built here via ``build_msa_decode_meta``.
"""
fmha_sm100, _ = _load_fmha_sm100()
max_slots, num_kv_heads, head_dim = k_cache.shape
H = q.shape[1]
P = block_size_k
topk = topk_idx.shape[-1]
if max_slots % P != 0:
raise ValueError(f"max_slots={max_slots} not divisible by page_size={P}")
if sm_scale is None:
sm_scale = head_dim**-0.5
n_phys_pages = max_slots // P
k_paged = k_cache.view(n_phys_pages, P, num_kv_heads, head_dim).permute(0, 2, 1, 3)
v_paged = v_cache.view(n_phys_pages, P, num_kv_heads, head_dim).permute(0, 2, 1, 3)
if kv_indices is None or plan is None:
kv_indices, plan = build_msa_decode_meta(
k_cache, req_to_token, slot_ids, seq_lens, H, P, topk
)
kv_block_indexes = topk_idx.permute(1, 0, 2).contiguous().to(torch.int32)
o, _ = fmha_sm100(
q,
k_paged,
v_paged,
plan,
sm_scale=sm_scale,
kv_indices=kv_indices,
kv_block_indexes=kv_block_indexes,
)
return o