feat: SM120 (Blackwell Desktop) support for DeepSeek-V4 inference (#24692)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
eeecho
2026-06-01 14:05:20 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent dfa1af99f5
commit 524ba10eda
11 changed files with 2104 additions and 21 deletions
@@ -56,6 +56,7 @@ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMo
from sglang.srt.speculative.eagle_utils import per_step_draft_out_cache_loc
from sglang.srt.speculative.spec_info import SpecInput
from sglang.srt.utils import ceil_align
from sglang.srt.utils.common import is_sm120_supported
if TYPE_CHECKING:
from sgl_kernel.flash_mla import FlashMLASchedMeta
@@ -63,6 +64,8 @@ if TYPE_CHECKING:
from sglang.srt.layers.radix_attention import RadixAttention
from sglang.srt.model_executor.model_runner import ModelRunner
_is_sm120 = is_sm120_supported()
logger = logging.getLogger(__name__)
SWA_WINDOW = 128
@@ -82,6 +85,8 @@ def _pad_last_dim(x: T, multiples_of: int = PAGE_INDEX_ALIGNED_SIZE) -> T:
def _create_flashmla_metadata():
if _is_sm120:
return None
import sgl_kernel.flash_mla as flash_mla
return flash_mla.get_mla_metadata()[0]
@@ -1042,24 +1047,42 @@ class DeepseekV4AttnBackend(
extra_indices.shape[-1] % 64 == 0
), f"{extra_indices.shape=}'s last dimension is not aligned to 64"
import sgl_kernel.flash_mla as flash_mla
if _is_sm120:
from sglang.srt.layers.attention.flash_mla_sm120 import (
flash_mla_with_kvcache_sm120,
)
o = flash_mla.flash_mla_with_kvcache(
q=q,
k_cache=swa_k_cache,
head_dim_v=self.head_dim_v,
block_table=None,
cache_seqlens=None,
tile_scheduler_metadata=flashmla_metadata,
softmax_scale=self.softmax_scale,
is_fp8_kvcache=True,
indices=swa_page_indices,
topk_length=swa_topk_lengths,
attn_sink=attn_sink,
extra_k_cache=extra_k_cache,
extra_indices_in_kvcache=extra_indices,
extra_topk_length=extra_topk_lengths,
)[0]
o = flash_mla_with_kvcache_sm120(
q=q,
k_cache=swa_k_cache,
head_dim_v=self.head_dim_v,
softmax_scale=self.softmax_scale,
indices=swa_page_indices,
topk_length=swa_topk_lengths,
attn_sink=attn_sink,
extra_k_cache=extra_k_cache,
extra_indices_in_kvcache=extra_indices,
extra_topk_length=extra_topk_lengths,
)[0]
else:
import sgl_kernel.flash_mla as flash_mla
o = flash_mla.flash_mla_with_kvcache(
q=q,
k_cache=swa_k_cache,
head_dim_v=self.head_dim_v,
block_table=None,
cache_seqlens=None,
tile_scheduler_metadata=flashmla_metadata,
softmax_scale=self.softmax_scale,
is_fp8_kvcache=True,
indices=swa_page_indices,
topk_length=swa_topk_lengths,
attn_sink=attn_sink,
extra_k_cache=extra_k_cache,
extra_indices_in_kvcache=extra_indices,
extra_topk_length=extra_topk_lengths,
)[0]
o = o.squeeze(1)
return o
@@ -20,6 +20,7 @@ from sglang.srt.layers.attention.dsv4.metadata import PagedIndexerMetadata
from sglang.srt.layers.linear import ReplicatedLinear
from sglang.srt.state_capturer.indexer_topk import get_global_indexer_capturer
from sglang.srt.utils import add_prefix, is_hip
from sglang.srt.utils.common import is_sm120_supported
if TYPE_CHECKING:
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
@@ -148,6 +149,74 @@ def _aiter_fp8_paged_mqa_logits(
return logits
def fp8_paged_mqa_logits_torch_sm120(
q_fp8: torch.Tensor,
kvcache_fp8: torch.Tensor,
weight: torch.Tensor,
seq_lens: torch.Tensor,
page_table: torch.Tensor,
deep_gemm_metadata: Any,
max_seq_len: int,
clean_logits: bool = True,
) -> torch.Tensor:
"""CUDA-graph-compatible FP8 paged MQA logits for SM120 (vectorized, no .item())."""
_ = deep_gemm_metadata
batch_size, _, num_heads, head_dim = q_fp8.shape
block_size = kvcache_fp8.shape[1]
device = q_fp8.device
assert head_dim == 128, "Vectorized torch impl hardcodes DSV4 indexer head_dim=128"
assert (
block_size == 64
), "Vectorized torch impl hardcodes block_size=64 cache layout"
assert q_fp8.shape == (batch_size, 1, num_heads, head_dim)
assert kvcache_fp8.shape[1:] == (block_size, 1, head_dim + 4)
assert weight.shape == (batch_size, num_heads)
if seq_lens.dim() > 1:
seq_lens = seq_lens.squeeze(-1)
assert seq_lens.shape == (batch_size,)
assert page_table.shape[0] == batch_size
assert clean_logits == False
max_pages = (max_seq_len + block_size - 1) // block_size
max_padded_seq = max_pages * block_size
kvcache_flat = kvcache_fp8.view(-1, block_size * (head_dim + 4))
SCALE_OFFSET = block_size * head_dim
page_ids = page_table[:, :max_pages]
kvcache_gathered = kvcache_flat[page_ids]
kv_value_raw = kvcache_gathered[..., :SCALE_OFFSET]
kv_scale_raw = kvcache_gathered[..., SCALE_OFFSET:]
kv_value = kv_value_raw.contiguous().view(dtype=FP8_DTYPE).to(torch.float32)
kv_value = kv_value.view(batch_size, max_padded_seq, head_dim)
kv_scale = kv_scale_raw.contiguous().view(dtype=torch.float32)
kv_scale = kv_scale.view(batch_size, max_padded_seq)
q = q_fp8[:, 0].to(torch.float32)
score = torch.bmm(kv_value, q.transpose(1, 2))
score = F.relu(score)
score = score * weight.unsqueeze(1)
score = score.sum(dim=2)
score = score * kv_scale
out_width = min(max_padded_seq, max_seq_len)
logits = score.new_full((batch_size, max_seq_len), float("-inf"))
logits[:, :out_width] = score[:, :out_width]
positions = torch.arange(max_seq_len, device=device)
invalid_mask = positions.unsqueeze(0) >= seq_lens.unsqueeze(1)
logits.masked_fill_(invalid_mask, float("-inf"))
return logits
def topk_transform_512_pytorch_vectorized(
scores: torch.Tensor,
seq_lens: torch.Tensor,
@@ -428,7 +497,10 @@ class C4IndexerBackendMixin:
elif envs.SGLANG_OPT_USE_AITER_INDEXER.get():
fn = _aiter_fp8_paged_mqa_logits
elif envs.SGLANG_FP8_PAGED_MQA_LOGITS_TORCH.get():
fn = fp8_paged_mqa_logits_torch
if is_sm120_supported():
fn = fp8_paged_mqa_logits_torch_sm120
else:
fn = fp8_paged_mqa_logits_torch
else:
from deep_gemm import fp8_paged_mqa_logits as fn
@@ -0,0 +1,252 @@
"""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 os
import torch
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)
# Default SM120 FlashMLA backend: "triton" (optimized) or "torch" (pure-PyTorch fallback).
# Controlled by SGLANG_SM120_TRITON_FLASHMLA env var (1=triton, 0=torch).
_sm120_default_backend = (
"triton" if os.environ.get("SGLANG_SM120_TRITON_FLASHMLA", "1") == "1" else "torch"
)
def flash_mla_with_kvcache_sm120(**kwargs):
"""SM120 FlashMLA sparse decode entry point.
Dispatches to the Triton kernel (default) 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 == "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)
@@ -0,0 +1,370 @@
"""SM120-optimized Triton FlashMLA sparse decode kernel — Tiled V2.
Replaces V1's serial token loop with a tiled vectorized approach:
1. BLOCK_T tokens loaded simultaneously via 2D gather (vs 1-at-a-time)
2. All BLOCK_T QK scores computed at once via vectorized mul-reduce
3. V accumulation via vectorized weighted sum across BLOCK_T tokens
4. Online softmax operates on tile-level maxima (fewer rescales)
Three typed views of the same paged buffer handle FP8/uint8/BF16 regions:
- float8_e4m3fn view -> nope FP8 values (direct load + dequant)
- uint8 view -> UE8M0 scale bytes (raw integer -> exp2 conversion)
- bfloat16 view -> rope BF16 values (direct load)
DSv4 page layout (per token, 576 bytes data + 8 bytes scales):
Data section: [0:448] FP8 nope | [448:576] BF16 rope (64 values = 128 bytes)
Scale section: [page_size*576 + offset*8 : +7] UE8M0 scales (7 groups of 64)
Target: RTX PRO 6000 (SM120, 188 SMs, 99KB SMEM, ~1.5 TB/s GDDR7, 96MB L2)
"""
import logging
from typing import Optional, Tuple
import torch
import triton
import triton.language as tl
logger = logging.getLogger(__name__)
LOG2E = tl.constexpr(1.4426950408889634)
# DSv4 KV cache layout constants
_NOPE_DIM = 448
_ROPE_DIM = 64
_D = _NOPE_DIM + _ROPE_DIM # 512
_TOKEN_DATA_STRIDE = 576 # bytes per token in data section
_SCALE_STRIDE = 8 # bytes per token in scale section
@triton.autotune(
configs=[
triton.Config({"BLOCK_T": 16}, num_warps=4, num_stages=2),
triton.Config({"BLOCK_T": 16}, num_warps=8, num_stages=2),
triton.Config({"BLOCK_T": 32}, num_warps=8, num_stages=2),
],
key=["topk_rounded"],
)
@triton.jit
def _tiled_sparse_decode_kernel(
# Q: [B, H, D] bf16
Q_ptr,
# Paged KV cache — three typed views of same underlying memory
cache_fp8_ptr, # float8_e4m3fn flat (1 byte/elem) — for nope
cache_uint8_ptr, # uint8 flat (1 byte/elem) — for scales
cache_bf16_ptr, # bfloat16 flat (2 bytes/elem) — for rope
# Indices: [B, topk] int32
indices_ptr,
# Valid lengths: [B] int32
topk_len_ptr,
# Output: [B, H, D] bf16 and LSE: [B, H] float32
O_ptr,
LSE_ptr,
# Scalars
sm_scale: tl.float32,
page_size: tl.int32,
page_bytes: tl.int64,
scale_section_off: tl.int64, # page_size * 576
H: tl.int32,
topk: tl.int32,
topk_rounded: tl.int32, # for autotune key
has_topk_len: tl.constexpr,
# Strides
stride_qb: tl.int32,
stride_qh: tl.int32,
stride_ob: tl.int32,
stride_oh: tl.int32,
stride_ib: tl.int32, # indices batch stride
# Constexprs
NOPE_PAD: tl.constexpr, # 512 (padded from 448)
ROPE_DIM: tl.constexpr, # 64
NOPE_DIM_RT: tl.int32, # 448 (runtime, for masking)
BLOCK_T: tl.constexpr, # tokens per tile (16 or 32)
):
"""Tiled sparse decode: vectorized gather + QK + softmax + V accumulation.
Grid: (B, H) — one block per (batch, head) pair.
Each block processes all topk tokens in tiles of BLOCK_T.
"""
bid = tl.program_id(0)
hid = tl.program_id(1)
# ---- Load Q for this (batch, head) ----
q_base = bid * stride_qb + hid * stride_qh
nope_offs = tl.arange(0, NOPE_PAD) # [512]
nope_mask = nope_offs < NOPE_DIM_RT # [512], True for [0:448]
rope_offs = tl.arange(0, ROPE_DIM) # [64]
q_nope = tl.load(Q_ptr + q_base + nope_offs, mask=nope_mask, other=0.0)
q_nope = q_nope.to(tl.float32) * sm_scale
q_rope = tl.load(Q_ptr + q_base + NOPE_DIM_RT + rope_offs)
q_rope = q_rope.to(tl.float32) * sm_scale
# ---- Valid token count ----
valid_topk = topk
if has_topk_len:
valid_topk = tl.load(topk_len_ptr + bid).to(tl.int32)
valid_topk = tl.minimum(valid_topk, topk)
# ---- Online softmax state (base-2 math for SM120 efficiency) ----
m_i: tl.float32 = -1e30
l_i: tl.float32 = 0.0
acc_nope = tl.zeros([NOPE_PAD], dtype=tl.float32)
acc_rope = tl.zeros([ROPE_DIM], dtype=tl.float32)
# ---- Precompute constant index vectors ----
group_ids = (nope_offs // 64).to(tl.int64) # [NOPE_PAD], scale group for each dim
t_offs = tl.arange(0, BLOCK_T) # [BLOCK_T], token offsets within tile
# ---- Process tokens in tiles of BLOCK_T ----
for tile_start in range(0, topk, BLOCK_T):
t_idx = tile_start + t_offs # [BLOCK_T], global token indices
t_in_bounds = t_idx < topk # bounds for index load
t_valid = t_idx < valid_topk # bounds for actual processing
# Load indices for this tile: [BLOCK_T]
raw_indices = tl.load(
indices_ptr + bid * stride_ib + t_idx,
mask=t_in_bounds,
other=-1,
)
idx_valid = t_valid & (raw_indices >= 0) # [BLOCK_T] mask
# Page addressing: [BLOCK_T] (clamp for safe addressing of invalid tokens)
safe_indices = tl.where(idx_valid, raw_indices, tl.zeros_like(raw_indices))
page_ids = (safe_indices // page_size).to(tl.int64)
page_offs_t = (safe_indices % page_size).to(tl.int64)
token_data_bases = page_ids * page_bytes + page_offs_t * 576 # [BLOCK_T] int64
# ---- Vectorized NOPE FP8 gather: [BLOCK_T, NOPE_PAD] ----
nope_addrs = token_data_bases[:, None] + nope_offs[None, :].to(tl.int64)
nope_2d_mask = idx_valid[:, None] & nope_mask[None, :]
kv_nope_fp8 = tl.load(
cache_fp8_ptr + nope_addrs,
mask=nope_2d_mask,
other=0.0,
)
# ---- Vectorized scale gather + dequant: [BLOCK_T, NOPE_PAD] ----
scale_bases = page_ids * page_bytes + scale_section_off + page_offs_t * 8
scale_addrs = scale_bases[:, None] + group_ids[None, :]
scale_raw = tl.load(
cache_uint8_ptr + scale_addrs,
mask=nope_2d_mask,
other=127,
)
scale_f32 = tl.math.exp2(scale_raw.to(tl.float32) - 127.0)
kv_nope = tl.where(nope_2d_mask, kv_nope_fp8.to(tl.float32) * scale_f32, 0.0)
# ---- Vectorized ROPE BF16 gather: [BLOCK_T, ROPE_DIM] ----
rope_byte_bases = token_data_bases + 448
rope_elem_bases = (rope_byte_bases // 2).to(tl.int64)
rope_addrs = rope_elem_bases[:, None] + rope_offs[None, :].to(tl.int64)
kv_rope = tl.load(
cache_bf16_ptr + rope_addrs,
mask=idx_valid[:, None],
other=0.0,
).to(tl.float32)
# ---- Vectorized QK scores: [BLOCK_T] ----
# scores[t] = dot(q_nope, kv_nope[t]) + dot(q_rope, kv_rope[t])
scores = tl.sum(q_nope[None, :] * kv_nope, axis=1) + tl.sum(
q_rope[None, :] * kv_rope, axis=1
)
scores = tl.where(idx_valid, scores, -1e30)
# ---- Online softmax update (base-2, tile-level) ----
scores_log2 = scores * LOG2E # [BLOCK_T]
tile_max = tl.max(scores_log2) # scalar
m_new = tl.maximum(m_i, tile_max)
alpha = tl.math.exp2(m_i - m_new) # rescale factor
p = tl.math.exp2(scores_log2 - m_new) # [BLOCK_T] attention weights
p = tl.where(idx_valid, p, 0.0) # zero out invalid
l_i = l_i * alpha + tl.sum(p)
# ---- Vectorized V accumulation (K=V in MLA) ----
# acc += sum_t(p[t] * kv[t, :]) for both nope and rope
acc_nope = acc_nope * alpha + tl.sum(p[:, None] * kv_nope, axis=0)
acc_rope = acc_rope * alpha + tl.sum(p[:, None] * kv_rope, axis=0)
m_i = m_new
# ---- Normalize output ----
safe_l = tl.where(l_i > 0.0, l_i, 1.0)
acc_nope = acc_nope / safe_l
acc_rope = acc_rope / safe_l
# LSE: convert from log2 back to natural log
lse = tl.where(l_i > 0.0, m_i / LOG2E + tl.math.log(safe_l), float("-inf"))
# ---- Store output ----
o_base = bid * stride_ob + hid * stride_oh
tl.store(O_ptr + o_base + nope_offs, acc_nope.to(tl.bfloat16), mask=nope_mask)
tl.store(O_ptr + o_base + NOPE_DIM_RT + rope_offs, acc_rope.to(tl.bfloat16))
tl.store(LSE_ptr + bid * H + hid, lse)
def _run_triton_sparse_decode(
q: torch.Tensor, # [B, 1, H, D] bf16
k_cache: torch.Tensor, # [num_pages, page_size, 1, bpt] float8
indices: torch.Tensor, # [B, ...] int32
topk_length: Optional[torch.Tensor],
softmax_scale: float,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Run the tiled Triton sparse decode kernel on one paged KV cache."""
B, _, H, D = q.shape
num_pages = k_cache.shape[0]
page_size = k_cache.shape[1]
page_bytes = k_cache.stride(0) # elements = bytes for float8
# Flatten indices to [B, topk]
flat_indices = indices.reshape(B, -1).contiguous()
topk = flat_indices.shape[1]
# Create three typed views of the flat cache memory.
# The KV cache may arrive as uint8 or float8_e4m3fn depending on the
# sglang version. Ensure each view has the correct dtype so Triton
# interprets the loaded values correctly (FP8 dequant vs raw integer).
total_elems = num_pages * page_bytes
raw_flat = k_cache.as_strided((total_elems,), (1,))
raw_uint8 = raw_flat.view(torch.uint8)
raw_fp8 = raw_uint8.view(torch.float8_e4m3fn)
raw_bf16 = raw_uint8.view(torch.bfloat16)
# Squeeze Q: [B, H, D]
q3 = q.squeeze(1)
if not q3.is_contiguous():
q3 = q3.contiguous()
out = torch.zeros(B, H, D, dtype=torch.bfloat16, device=q.device)
lse = torch.full((B, H), float("-inf"), dtype=torch.float32, device=q.device)
# Round topk for autotune key stability
topk_rounded = triton.next_power_of_2(topk)
grid = (B, H)
_tiled_sparse_decode_kernel[grid](
q3,
raw_fp8,
raw_uint8,
raw_bf16,
flat_indices,
(
topk_length
if topk_length is not None
else torch.empty(0, device=q.device, dtype=torch.int32)
),
out,
lse,
softmax_scale,
page_size,
int(page_bytes), # page_bytes (int64)
int(page_size * _TOKEN_DATA_STRIDE), # scale_section_off (int64)
H,
topk,
topk_rounded,
topk_length is not None,
q3.stride(0),
q3.stride(1),
out.stride(0),
out.stride(1),
flat_indices.stride(0),
NOPE_PAD=512,
ROPE_DIM=_ROPE_DIM,
NOPE_DIM_RT=_NOPE_DIM,
)
# Return [B, 1, H, D] and [B, 1, H]
return out.unsqueeze(1), lse.unsqueeze(1)
def _merge_partial_attn(
out1: torch.Tensor,
lse1: torch.Tensor,
out2: torch.Tensor,
lse2: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Merge two attention outputs using LSE-weighted combination.
out: [B, 1, H, D] bf16, lse: [B, 1, H] float32
"""
max_lse = torch.maximum(lse1, lse2)
w1 = torch.where(lse1 > -1e20, torch.exp(lse1 - max_lse), torch.zeros_like(lse1))
w2 = torch.where(lse2 > -1e20, torch.exp(lse2 - max_lse), torch.zeros_like(lse2))
total = (w1 + w2).clamp(min=1e-20)
merged = (
w1.unsqueeze(-1) * out1.float() + w2.unsqueeze(-1) * out2.float()
) / total.unsqueeze(-1)
merged_lse = max_lse + torch.log(total)
return merged.to(torch.bfloat16), merged_lse
def _apply_attn_sink(
out: torch.Tensor,
lse: torch.Tensor,
attn_sink: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Apply attention sink normalization.
The sink adds to the softmax denominator without contributing output,
effectively down-weighting all attention scores.
out: [B, 1, H, D] bf16, lse: [B, 1, H] f32, attn_sink: [H] f32
"""
sink_lse = attn_sink.view(1, 1, -1).expand_as(lse)
combined_lse = torch.logaddexp(lse, sink_lse)
w = torch.where(
lse > -1e20,
torch.exp(lse - combined_lse),
torch.zeros_like(lse),
)
return (out.float() * w.unsqueeze(-1)).to(torch.bfloat16), combined_lse
def flash_mla_sparse_decode_triton(
q: torch.Tensor,
k_cache: torch.Tensor,
indices: torch.Tensor,
topk_length: Optional[torch.Tensor],
attn_sink: Optional[torch.Tensor],
head_dim_v: int,
softmax_scale: float,
extra_k_cache: Optional[torch.Tensor] = None,
extra_indices: Optional[torch.Tensor] = None,
extra_topk_length: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""SM120-optimized sparse MLA decode using tiled Triton kernel.
Processes SWA and extra (c4/c128) caches separately via the same
Triton kernel, then merges results using LSE-weighted combination.
"""
if softmax_scale is None:
softmax_scale = q.shape[-1] ** (-0.5)
# Process main cache (SWA)
out, lse = _run_triton_sparse_decode(
q,
k_cache,
indices,
topk_length,
softmax_scale,
)
# Process extra cache (c4 / c128) if present
if extra_k_cache is not None and extra_indices is not None:
out_extra, lse_extra = _run_triton_sparse_decode(
q,
extra_k_cache,
extra_indices,
extra_topk_length,
softmax_scale,
)
out, lse = _merge_partial_attn(out, lse, out_extra, lse_extra)
# Apply attention sink
if attn_sink is not None:
out, lse = _apply_attn_sink(out, lse, attn_sink)
# Return format matching PyTorch fallback: (out, lse.permute(0,2,1))
return out, lse.permute(0, 2, 1)
@@ -18,6 +18,9 @@ def _compute_enable_deep_gemm():
sm_version = get_device_sm()
if (_is_cuda and sm_version < 90) or (_is_musa and sm_version < 31):
return False
# DeepGEMM requires TMEM/tcgen05 (SM100+datacenter), not available on SM120
if sm_version == 120:
return False
if not (_is_cuda or _is_musa):
return False
@@ -0,0 +1,454 @@
"""SM120-optimized Triton MXFP4 MoE kernel — CUDA graph compatible.
Replaces the PyTorch fallback (per-expert for-loop + full dequant + matmul)
with fused Triton kernels that:
1. Fuse FP4 dequant + GEMV (no intermediate BF16 weight materialization)
2. Process each (token, expert) slot independently — no data-dependent routing
3. Respect SM120 shared memory constraint (99 KB/block)
CUDA graph compatibility:
- No .unique(), .item(), .nonzero() — all routing is tensor-level
- Fixed grid dimensions (M*topk, N_blocks) per captured batch size
- All control flow is static or within Triton kernels
SM120 constraints:
- SMEM: 99 KB/block (vs SM100 228 KB)
- No TMEM/tcgen05 — uses mma.sync.aligned via Triton
- Max warps: 48/SM
- Registers: ~128/thread practical limit
"""
import logging
from typing import Optional
import torch
import triton
import triton.language as tl
logger = logging.getLogger(__name__)
@triton.jit
def _dequant_fp4_lut(nibble):
"""Decode a 4-bit FP4 E2M1 nibble to float32 using arithmetic."""
sign_bit = (nibble >> 3) & 1
exp_bits = (nibble >> 1) & 3
man_bit = nibble & 1
is_subnormal = exp_bits == 0
mantissa = 1.0 + man_bit.to(tl.float32) * 0.5
exponent = tl.math.exp2((exp_bits - 1).to(tl.float32))
val = tl.where(is_subnormal, man_bit.to(tl.float32) * 0.5, mantissa * exponent)
val = tl.where(sign_bit != 0, -val, val)
return val
# ── Per-slot GEMV kernel: processes one (token, expert) pair ──
@triton.autotune(
configs=[
triton.Config({"BLOCK_N": 64, "BLOCK_K": 64}, num_warps=4, num_stages=2),
triton.Config({"BLOCK_N": 32, "BLOCK_K": 64}, num_warps=4, num_stages=2),
triton.Config({"BLOCK_N": 64, "BLOCK_K": 128}, num_warps=4, num_stages=2),
triton.Config({"BLOCK_N": 128, "BLOCK_K": 64}, num_warps=8, num_stages=2),
],
key=["N", "K"],
)
@triton.jit
def _mxfp4_slot_gemv_kernel(
# Pointers
A_ptr, # [M_total, K] bf16 — source rows
B_packed_ptr, # [E, N, K//2] uint8 — packed FP4 expert weights
B_scale_ptr, # [E, N, K//32] float32 — weight scales
C_ptr, # [num_slots, N] bf16 — output
token_ids_ptr, # [num_slots] int32 — which A row for each slot
expert_ids_ptr, # [num_slots] int32 — which expert's B for each slot
# Dimensions
N: tl.int32,
K: tl.int32,
# A strides
stride_am: tl.int32,
# B strides (within an expert)
stride_bn: tl.int32,
stride_bk2: tl.int32,
# B_scale strides (within an expert)
stride_bsn: tl.int32,
stride_bsk32: tl.int32,
# Expert strides (between experts)
expert_b_stride: tl.int64,
expert_s_stride: tl.int64,
# C strides
stride_cm: tl.int32,
# Block sizes
BLOCK_N: tl.constexpr,
BLOCK_K: tl.constexpr,
):
"""Per-slot fused MXFP4 dequant + GEMV.
Grid: (num_slots, cdiv(N, BLOCK_N))
Each program computes one (token, expert) pair for a BLOCK_N slice of output.
"""
slot_id = tl.program_id(0)
n_block = tl.program_id(1)
token_id = tl.load(token_ids_ptr + slot_id).to(tl.int64)
expert_id = tl.load(expert_ids_ptr + slot_id).to(tl.int64)
offs_n = n_block * BLOCK_N + tl.arange(0, BLOCK_N)
n_mask = offs_n < N
acc = tl.zeros([BLOCK_N], dtype=tl.float32)
# Expert weight base pointers
b_base = expert_id * expert_b_stride
s_base = expert_id * expert_s_stride
a_base = token_id * stride_am
for k_start in range(0, K, BLOCK_K):
# ── Load packed B: [BLOCK_N, BLOCK_K//2] ──
offs_k2 = k_start // 2 + tl.arange(0, BLOCK_K // 2)
b_mask = n_mask[:, None] & (offs_k2[None, :] < K // 2)
b_packed = tl.load(
B_packed_ptr
+ b_base
+ offs_n[:, None] * stride_bn
+ offs_k2[None, :] * stride_bk2,
mask=b_mask,
other=0,
)
# ── FP4 dequant ──
b_u8 = b_packed.to(tl.int32)
val_lo = _dequant_fp4_lut(b_u8 & 0x0F) # even K indices
val_hi = _dequant_fp4_lut((b_u8 >> 4) & 0x0F) # odd K indices
# ── Load and apply scales: [BLOCK_N, BLOCK_K//2] ──
group_ids = tl.arange(0, BLOCK_K // 2) // 16 # 32 values per group, 2 per byte
s_mask = n_mask[:, None] & ((k_start // 32 + group_ids[None, :]) < K // 32)
scales = tl.load(
B_scale_ptr
+ s_base
+ offs_n[:, None] * stride_bsn
+ (k_start // 32 + group_ids[None, :]) * stride_bsk32,
mask=s_mask,
other=1.0,
)
val_lo = val_lo * scales
val_hi = val_hi * scales
# ── Load A even/odd: [BLOCK_K//2] each ──
offs_k_even = k_start + tl.arange(0, BLOCK_K // 2) * 2
offs_k_odd = offs_k_even + 1
a_even = tl.load(
A_ptr + a_base + offs_k_even,
mask=offs_k_even < K,
other=0.0,
).to(tl.float32)
a_odd = tl.load(
A_ptr + a_base + offs_k_odd,
mask=offs_k_odd < K,
other=0.0,
).to(tl.float32)
# ── Dot product: acc[n] += sum_k(a_even[k]*B_lo[n,k] + a_odd[k]*B_hi[n,k]) ──
acc += tl.sum(a_even[None, :] * val_lo, axis=1)
acc += tl.sum(a_odd[None, :] * val_hi, axis=1)
# ── Store output ──
tl.store(
C_ptr + slot_id * stride_cm + offs_n,
acc.to(tl.bfloat16),
mask=n_mask,
)
# ── Legacy per-expert GEMM kernel (kept for benchmarking) ──
@triton.autotune(
configs=[
triton.Config(
{"BLOCK_M": 32, "BLOCK_N": 64, "BLOCK_K": 64}, num_warps=4, num_stages=2
),
triton.Config(
{"BLOCK_M": 32, "BLOCK_N": 32, "BLOCK_K": 64}, num_warps=4, num_stages=2
),
triton.Config(
{"BLOCK_M": 64, "BLOCK_N": 32, "BLOCK_K": 64}, num_warps=4, num_stages=2
),
triton.Config(
{"BLOCK_M": 64, "BLOCK_N": 64, "BLOCK_K": 32}, num_warps=8, num_stages=2
),
triton.Config(
{"BLOCK_M": 16, "BLOCK_N": 64, "BLOCK_K": 64}, num_warps=4, num_stages=2
),
],
key=["M", "N", "K"],
)
@triton.jit
def _mxfp4_gemm_kernel(
# Pointers
A_ptr, # [M, K] bf16 activation
B_packed_ptr, # [N, K//2] uint8 packed FP4
B_scale_ptr, # [N, K//32] float32 scales
C_ptr, # [M, N] bf16 output
# Dimensions
M,
N,
K,
# Strides
stride_am,
stride_ak,
stride_bn,
stride_bk2,
stride_bsn,
stride_bsk32,
stride_cm,
stride_cn,
# Constexprs
BLOCK_M: tl.constexpr,
BLOCK_N: tl.constexpr,
BLOCK_K: tl.constexpr,
):
"""Fused MXFP4 dequant + GEMM: C = A @ dequant(B_packed, B_scale).T"""
pid_m = tl.program_id(0)
pid_n = tl.program_id(1)
offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
for k_start in range(0, K, BLOCK_K):
offs_k2 = k_start // 2 + tl.arange(0, BLOCK_K // 2)
b_mask = (offs_n[:, None] < N) & (offs_k2[None, :] < K // 2)
b_packed = tl.load(
B_packed_ptr + offs_n[:, None] * stride_bn + offs_k2[None, :] * stride_bk2,
mask=b_mask,
other=0,
)
b_u8 = b_packed.to(tl.int32)
val_lo = _dequant_fp4_lut(b_u8 & 0x0F)
val_hi = _dequant_fp4_lut((b_u8 >> 4) & 0x0F)
group_ids = tl.arange(0, BLOCK_K // 2) // 16
scales_per_byte = tl.load(
B_scale_ptr
+ offs_n[:, None] * stride_bsn
+ (k_start // 32 + group_ids[None, :]) * stride_bsk32,
mask=(offs_n[:, None] < N)
& ((k_start // 32 + group_ids[None, :]) < K // 32),
other=1.0,
)
val_lo = val_lo * scales_per_byte
val_hi = val_hi * scales_per_byte
offs_k_even = k_start + tl.arange(0, BLOCK_K // 2) * 2
offs_k_odd = offs_k_even + 1
a_even_mask = (offs_m[:, None] < M) & (offs_k_even[None, :] < K)
a_even = tl.load(
A_ptr + offs_m[:, None] * stride_am + offs_k_even[None, :] * stride_ak,
mask=a_even_mask,
other=0.0,
).to(tl.float32)
a_odd_mask = (offs_m[:, None] < M) & (offs_k_odd[None, :] < K)
a_odd = tl.load(
A_ptr + offs_m[:, None] * stride_am + offs_k_odd[None, :] * stride_ak,
mask=a_odd_mask,
other=0.0,
).to(tl.float32)
acc += tl.dot(a_even, tl.trans(val_lo))
acc += tl.dot(a_odd, tl.trans(val_hi))
c_mask = (offs_m[:, None] < M) & (offs_n[None, :] < N)
tl.store(
C_ptr + offs_m[:, None] * stride_cm + offs_n[None, :] * stride_cn,
acc.to(tl.bfloat16),
mask=c_mask,
)
def mxfp4_gemm_triton(
A: torch.Tensor,
B_packed: torch.Tensor,
B_scale: torch.Tensor,
K_full: int,
) -> torch.Tensor:
"""Triton fused MXFP4 dequant + GEMM: output = A @ dequant(B).T
Kept for standalone benchmarking. The MoE forward uses the slot kernel.
"""
M = A.shape[0]
N = B_packed.shape[0]
K = K_full
if B_scale.dtype == torch.float8_e8m0fnu:
B_scale = B_scale.to(torch.float32)
elif B_scale.dtype != torch.float32:
B_scale = B_scale.float()
C = torch.empty(M, N, dtype=torch.bfloat16, device=A.device)
A = A.contiguous()
B_packed = B_packed.contiguous()
B_scale = B_scale.contiguous()
grid = lambda meta: (
triton.cdiv(M, meta["BLOCK_M"]),
triton.cdiv(N, meta["BLOCK_N"]),
)
B_u8 = B_packed.view(torch.uint8)
_mxfp4_gemm_kernel[grid](
A,
B_u8,
B_scale,
C,
M,
N,
K,
A.stride(0),
A.stride(1),
B_u8.stride(0),
B_u8.stride(1),
B_scale.stride(0),
B_scale.stride(1),
C.stride(0),
C.stride(1),
)
return C
def mxfp4_moe_forward_triton(
hidden_states: torch.Tensor,
w13_packed: torch.Tensor,
w2_packed: torch.Tensor,
w13_scale: torch.Tensor,
w2_scale: torch.Tensor,
topk_ids: torch.Tensor,
topk_weights: torch.Tensor,
hidden_size: int,
intermediate_size: int,
inplace: bool = False,
routed_scaling_factor: Optional[float] = None,
clamp_limit: Optional[float] = None,
) -> torch.Tensor:
"""SM120-optimized MXFP4 MoE forward — CUDA graph compatible.
Uses per-slot GEMV kernels instead of per-expert Python loops.
Each (token, expert) slot is processed independently with a fixed grid,
eliminating .unique()/.item()/.nonzero() that break CUDA graph capture.
"""
import torch.nn.functional as F
M, K = hidden_states.shape
topk = topk_ids.shape[1]
I = intermediate_size
num_slots = M * topk
device = hidden_states.device
dtype = hidden_states.dtype
# ── Graph-safe routing: flatten topk assignments ──
# token_ids[slot] = which row of A (original token index)
# expert_ids[slot] = which expert's weights to use
# topk_ids may contain -1 for padded/filtered tokens; clamp to 0 for safe
# Triton loads, then zero out invalid slots' output after GEMM.
flat_expert_ids_raw = topk_ids.reshape(-1).contiguous() # [M*topk]
invalid_slot_mask = flat_expert_ids_raw < 0 # [M*topk]
flat_expert_ids = flat_expert_ids_raw.clamp(min=0) # safe for indexing
token_ids = (
torch.arange(M, device=device, dtype=torch.int32)
.unsqueeze(1)
.expand(M, topk)
.reshape(-1)
.contiguous()
) # [M*topk]
# ── Ensure scales are float32 ──
if w13_scale.dtype != torch.float32:
w13_scale = w13_scale.to(torch.float32)
if w2_scale.dtype != torch.float32:
w2_scale = w2_scale.to(torch.float32)
# ── GEMM1: gate_up projection ──
# hidden_states[token] @ w13[expert].T → [num_slots, 2*I]
intermediate = torch.empty(num_slots, 2 * I, dtype=dtype, device=device)
w13_u8 = w13_packed.view(torch.uint8) # [E, 2*I, K//2]
grid1 = lambda meta: (num_slots, triton.cdiv(2 * I, meta["BLOCK_N"]))
_mxfp4_slot_gemv_kernel[grid1](
hidden_states,
w13_u8,
w13_scale,
intermediate,
token_ids,
flat_expert_ids,
2 * I,
K,
hidden_states.stride(0),
w13_u8.stride(1),
w13_u8.stride(2),
w13_scale.stride(1),
w13_scale.stride(2),
w13_u8.stride(0),
w13_scale.stride(0),
intermediate.stride(0),
)
# ── SiLU activation (graph-safe vectorized ops) ──
gate = intermediate[:, :I].float()
up = intermediate[:, I:].float()
if clamp_limit is not None and clamp_limit > 0:
gate = torch.clamp(gate, max=clamp_limit)
up = torch.clamp(up, min=-clamp_limit, max=clamp_limit)
activated = (F.silu(gate) * up).to(dtype)
# ── GEMM2: down projection ──
# activated[slot] @ w2[expert].T → [num_slots, K]
down = torch.empty(num_slots, K, dtype=dtype, device=device)
# For GEMM2, A is the activated buffer — each slot reads its own row
slot_ids = torch.arange(num_slots, device=device, dtype=torch.int32)
w2_u8 = w2_packed.view(torch.uint8) # [E, K, I//2]
grid2 = lambda meta: (num_slots, triton.cdiv(K, meta["BLOCK_N"]))
_mxfp4_slot_gemv_kernel[grid2](
activated,
w2_u8,
w2_scale,
down,
slot_ids,
flat_expert_ids,
K,
I,
activated.stride(0),
w2_u8.stride(1),
w2_u8.stride(2),
w2_scale.stride(1),
w2_scale.stride(2),
w2_u8.stride(0),
w2_scale.stride(0),
down.stride(0),
)
# ── Zero out invalid slots (padded/filtered tokens with topk_ids == -1) ──
# Use multiplication instead of boolean indexing to stay CUDA-graph-safe
# (no GPU→CPU sync). valid_mask is 1.0 for valid slots, 0.0 for invalid.
valid_mask = (~invalid_slot_mask).unsqueeze(1).to(dtype) # [M*topk, 1]
down = down * valid_mask
# ── Weighted sum across topk slots (graph-safe) ──
flat_weights = topk_weights.reshape(-1).unsqueeze(1).to(dtype) # [M*topk, 1]
output = (down * flat_weights).view(M, topk, K).sum(dim=1)
if routed_scaling_factor is not None and routed_scaling_factor != 1.0:
output.mul_(routed_scaling_factor)
return output
@@ -9,7 +9,7 @@ from torch.nn import Module
from sglang.srt.layers.moe.moe_runner.marlin import MarlinMoeQuantInfo
from sglang.srt.layers.moe.utils import MoeRunnerBackend
from sglang.srt.utils import log_info_on_rank0, set_weight_attrs
from sglang.srt.utils.common import is_sm90_supported
from sglang.srt.utils.common import is_sm90_supported, is_sm120_supported
if TYPE_CHECKING:
from sglang.srt.layers.moe.token_dispatcher import CombineInput, DispatchOutput
@@ -42,6 +42,7 @@ class Mxfp4MarlinMoEMethod:
FusedMoeWeightScaleSupported,
)
layer._dsv4_mxfp4_backend = None # set in process_weights_after_loading
fp4_block_k = 32
w13_weight = torch.nn.Parameter(
@@ -108,10 +109,42 @@ class Mxfp4MarlinMoEMethod:
if getattr(layer, "_mega_moe_weights_built", False):
return
if not is_sm90_supported():
if not is_sm90_supported() and not is_sm120_supported():
raise RuntimeError(
"DeepSeekV4 MXFP4 Marlin fallback requires Hopper/SM90 or above."
)
# SM120: Skip Marlin repacking, keep original weight format
# for Triton dequant kernel (Marlin kernel produces NaN on SM120)
if is_sm120_supported():
from torch.nn import Parameter
log_info_on_rank0(
logger,
f"SM120 detected: using PyTorch MXFP4 MoE fallback "
f"(layer: {self.prefix})...",
)
# Keep weights in original packed int8 format
# Normalize scales to float32 for direct use in dequant
w13_s = layer.w13_weight_scale_inv.data
w2_s = layer.w2_weight_scale_inv.data
if w13_s.dtype == torch.float8_e8m0fnu:
pass # already in e8m0 format, will convert at runtime
elif w13_s.dtype in (torch.uint8, torch.int8):
layer.w13_weight_scale_inv = Parameter(
w13_s.view(torch.uint8)
.view(torch.float8_e8m0fnu)
.to(torch.float32),
requires_grad=False,
)
layer.w2_weight_scale_inv = Parameter(
w2_s.view(torch.uint8).view(torch.float8_e8m0fnu).to(torch.float32),
requires_grad=False,
)
# else: float32 scales are already usable directly
layer._dsv4_mxfp4_backend = "sm120_triton"
return
if not check_moe_marlin_supports_layer(layer, 32):
raise RuntimeError(
"Current DeepSeekV4 MoE layer does not satisfy Marlin constraints."
@@ -144,6 +177,43 @@ class Mxfp4MarlinMoEMethod:
if not TopKOutputChecker.format_is_standard(topk_output):
raise ValueError(f"Unsupported topk output format: {topk_output.format}")
# SM120: use Triton fused dequant+GEMM (Marlin kernel produces NaN on SM120)
if layer._dsv4_mxfp4_backend == "sm120_triton":
from sglang.srt.layers.moe.fused_moe_triton.mxfp4_moe_sm120_triton import (
mxfp4_moe_forward_triton,
)
hidden_states = dispatch_output.hidden_states
w13 = layer.w13_weight.data
w2 = layer.w2_weight.data
w13_scale = layer.w13_weight_scale_inv.data
w2_scale = layer.w2_weight_scale_inv.data
intermediate_size = w13.shape[1] // 2
hidden_size = w13.shape[2] * 2
output = mxfp4_moe_forward_triton(
hidden_states=hidden_states,
w13_packed=w13,
w2_packed=w2,
w13_scale=w13_scale,
w2_scale=w2_scale,
topk_ids=topk_output.topk_ids,
topk_weights=topk_output.topk_weights,
hidden_size=hidden_size,
intermediate_size=intermediate_size,
routed_scaling_factor=(
self.runner.config.routed_scaling_factor
if hasattr(self.runner, "config")
else None
),
clamp_limit=(
self.runner.config.swiglu_limit
if hasattr(self.runner, "config")
else None
),
)
return StandardCombineInput(hidden_states=output)
quant_info = MarlinMoeQuantInfo(
w13_qweight=layer.w13_weight,
w2_qweight=layer.w2_weight,
+14
View File
@@ -2066,6 +2066,20 @@ class ServerArgs:
validate_deepseek_v4_cp(self)
if is_sm120_supported():
if self.moe_runner_backend == "auto":
self.moe_runner_backend = "marlin"
logger.info(
"Use marlin as MoE runner backend on SM120 for DeepseekV4"
)
# SM120 lacks tcgen05/TMEM: disable features that depend on
# DeepGEMM or require >99KB SMEM (topk_v2).
envs.SGLANG_OPT_FP8_WO_A_GEMM.set(False)
envs.SGLANG_OPT_USE_TOPK_V2.set(False)
envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.set(False)
envs.SGLANG_OPT_DEEPGEMM_HC_PRENORM.set(False)
envs.SGLANG_FP8_PAGED_MQA_LOGITS_TORCH.set(True)
elif model_arch in ["GptOssForCausalLM"]:
# Set attention backend for GPT-OSS
if self.is_attention_backend_not_set():