dsv4.1: remaining model and runtime integration (#38798)

Co-authored-by: BBuf <1182563586@qq.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Xiaoyu Zhang <xiaoyu.zhang@radixark.ai>
Co-authored-by: Yuwei An <ayw.sirius19@gmail.com>
Co-authored-by: Khoa Pham <khoa.pham@radixark.ai>
Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com>
Co-authored-by: Zhichen Zeng <zczeng@uw.edu>
Co-authored-by: Ke Bao <ispobaoke@gmail.com>
This commit is contained in:
Liangsheng Yin
2026-09-18 02:55:30 -07:00
committed by GitHub
co-authored by BBuf Claude Opus 5 Xiaoyu Zhang Yuwei An Khoa Pham Yuhao Yang Zhichen Zeng Ke Bao
parent 1b200ffaaa
commit a6cf05817f
103 changed files with 8807 additions and 723 deletions
@@ -2436,6 +2436,12 @@ Combining `--enable-response-store` with `--disaggregation-mode=prefill` or `dec
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>`None`</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>`None`</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Type: int</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Type: int</td>
</tr> </tr>
<tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}>`--cuda-graph-max-seq-len-prefill`</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Longest sequence a prefill CUDA graph replay admits; longer batches run eager prefill. Folds into `cuda_graph_config[prefill].max_seq_len`.</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>`None`</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Type: int</td>
</tr>
<tr> <tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}>`--cuda-graph-bs-decode`</td> <td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}>`--cuda-graph-bs-decode`</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Explicit list of batch sizes to capture for the decode CUDA graph.</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Explicit list of batch sizes to capture for the decode CUDA graph.</td>
@@ -369,7 +369,7 @@ export const config = {
"--mem-fraction-static 0.8", "--mem-fraction-static 0.8",
"--speculative-algorithm DSPARK", "--speculative-algorithm DSPARK",
"--speculative-dspark-block-size 5", "--speculative-dspark-block-size 5",
"--cuda-graph-max-bs 64", "--cuda-graph-max-bs-decode 64",
"--cuda-graph-backend-prefill breakable", "--cuda-graph-backend-prefill breakable",
"--cuda-graph-max-bs-prefill 4096", "--cuda-graph-max-bs-prefill 4096",
"--reasoning-parser auto", "--reasoning-parser auto",
@@ -32,7 +32,12 @@ from .moe import (
silu_and_mul_contig_post_quant, silu_and_mul_contig_post_quant,
silu_and_mul_masked_post_quant, silu_and_mul_masked_post_quant,
) )
from .topk import plan_topk_v2, topk_transform_paged, topk_transform_paged_v2 from .topk import (
plan_topk_v2,
topk_transform_paged,
topk_transform_paged_v2,
topk_transform_ragged_v2,
)
from .utils import make_name from .utils import make_name
__all__ = [ __all__ = [
@@ -56,6 +61,7 @@ __all__ = [
"triton_create_paged_compress_data", "triton_create_paged_compress_data",
"topk_transform_paged", "topk_transform_paged",
"topk_transform_paged_v2", "topk_transform_paged_v2",
"topk_transform_ragged_v2",
"plan_topk_v2", "plan_topk_v2",
"hash_topk", "hash_topk",
"mega_moe_pre_dispatch", "mega_moe_pre_dispatch",
@@ -0,0 +1,151 @@
"""Fused ratio-2 decode pair-pooling, bitwise identical to the torch pool_pairs
path; a rounding difference here can change the indexer's top-k selection."""
from typing import Tuple
import torch
import triton
import triton.language as tl
from triton.language.extra import libdevice
@triton.jit
def _c2_decode_pool_kernel(
kv_ptr, # [n, D] fp32
score_ptr, # [n, D] fp32
pos_ptr, # [n] int64
raw_out_loc_ptr, # [n] int32/int64
out_loc_ptr, # [n] int32/int64
req_ptr, # [n] int64
state_kv_ptr, # [R, D] fp32, in/out
state_score_ptr, # [R, D] fp32, in/out
pooled_ptr, # [n, D] fp32, out
group_pos_ptr, # [n] int64, out
slots_ptr, # [n] int64, out
pad_row,
RING_SIZE: tl.constexpr,
STATE_KV_STRIDE: tl.constexpr,
STATE_SCORE_STRIDE: tl.constexpr,
D: tl.constexpr,
BLOCK_D: tl.constexpr,
):
row = tl.program_id(0)
pos = tl.load(pos_ptr + row)
raw_loc = tl.load(raw_out_loc_ptr + row)
out_loc = tl.load(out_loc_ptr + row)
req = tl.load(req_ptr + row)
# Raw location 0 is the padded-graph-row sentinel, and its req_pool_idx 0 may
# be a live request, so such a row's pair state goes to the spare row.
odd = (pos % 2) == 1
if RING_SIZE:
r = tl.where(
(raw_loc == 0) | (pos == 0),
pad_row,
req * RING_SIZE + (pos - 1) % RING_SIZE,
)
else:
r = tl.where(raw_loc == 0, pad_row, req)
offs = tl.arange(0, BLOCK_D)
mask = offs < D
kv = tl.load(kv_ptr + row * D + offs, mask=mask, other=0.0)
score = tl.load(score_ptr + row * D + offs, mask=mask, other=0.0)
p_kv = tl.load(state_kv_ptr + r * STATE_KV_STRIDE + offs, mask=mask, other=0.0)
p_score = tl.load(
state_score_ptr + r * STATE_SCORE_STRIDE + offs, mask=mask, other=0.0
)
if RING_SIZE:
# Each live request has one decode row. Pad rows never modify the ring.
write_row = req * RING_SIZE + pos % RING_SIZE
tl.store(
state_kv_ptr + write_row * STATE_KV_STRIDE + offs,
kv,
mask=mask & (raw_loc != 0),
)
tl.store(
state_score_ptr + write_row * STATE_SCORE_STRIDE + offs,
score,
mask=mask & (raw_loc != 0),
)
else:
tl.store(
state_kv_ptr + r * STATE_KV_STRIDE + offs,
tl.where(odd, p_kv, kv),
mask=mask,
)
tl.store(
state_score_ptr + r * STATE_SCORE_STRIDE + offs,
tl.where(odd, p_score, score),
mask=mask,
)
# libdevice.exp, not tl.exp: the approximate exponential changes the latent.
m = tl.maximum(p_score, score)
e0 = libdevice.exp(p_score - m)
e1 = libdevice.exp(score - m)
denom = e0 + e1
# The + 0.0 below prevents FMA contraction: torch rounds both products first.
# libdevice.div_rn matches torch division; Triton's / is an approximate reciprocal.
t0 = p_kv * libdevice.div_rn(e0, denom)
t1 = kv * libdevice.div_rn(e1, denom)
t0 = t0 + 0.0
t1 = t1 + 0.0
pooled = t0 + t1
tl.store(pooled_ptr + row * D + offs, pooled, mask=mask)
# One program per row, so these are written exactly once each; no guard.
tl.store(group_pos_ptr + row, tl.where(odd, pos - 1, pos))
tl.store(slots_ptr + row, tl.where(out_loc >= 0, out_loc, 0))
def c2_decode_pool(
kv: torch.Tensor,
score: torch.Tensor,
pos: torch.Tensor,
raw_out_loc: torch.Tensor,
out_loc: torch.Tensor,
req: torch.Tensor,
state_kv: torch.Tensor,
state_score: torch.Tensor,
pad_row: int,
*,
ring_size: int = 0,
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Ratio-2 decode pair pooling; updates `state_kv` / `state_score` in place.
With ring_size > 0 the state halves may be views of an interleaved
CompressStatePool ring, one row per request otherwise; pad_row is the
padded-graph-row sentinel and is never written.
"""
assert kv.is_contiguous() and score.is_contiguous()
assert state_kv.stride(1) == state_score.stride(1) == 1
assert kv.dtype == torch.float32 and score.dtype == torch.float32
n, D = kv.shape
pooled = torch.empty_like(kv)
group_pos = torch.empty_like(pos)
slots = torch.empty(n, dtype=out_loc.dtype, device=out_loc.device)
_c2_decode_pool_kernel[(n,)](
kv,
score,
pos,
raw_out_loc,
out_loc,
req,
state_kv,
state_score,
pooled,
group_pos,
slots,
pad_row,
RING_SIZE=ring_size,
STATE_KV_STRIDE=state_kv.stride(0),
STATE_SCORE_STRIDE=state_score.stride(0),
D=D,
BLOCK_D=triton.next_power_of_2(D),
num_warps=4,
)
return pooled, group_pos, slots
@@ -0,0 +1,160 @@
"""SM100 small-batch paged attention with the heads on the MMA N dimension; the
caller applies the inverse RoPE to the result."""
from typing import Optional
import torch
import triton
import triton.language as tl
from .kv_layout import KVLayout
LAYOUT = KVLayout.V4
MAX_BATCH = 8
NUM_HEADS = 16
HEAD_DIM = 512
SOFTMAX_SCALE = HEAD_DIM**-0.5
def can_use_swapab_attention(
q: torch.Tensor,
kv: torch.Tensor,
extra_kv: Optional[torch.Tensor],
num_heads: int,
head_dim_v: int,
softmax_scale: float,
) -> bool:
"""The caller adds the SM100 and single-query forward-mode gates."""
return (
0 < q.shape[0] <= MAX_BATCH
and num_heads == NUM_HEADS
and q.dtype == torch.bfloat16
and q.shape[-1] == HEAD_DIM
and head_dim_v == HEAD_DIM
and softmax_scale == SOFTMAX_SCALE
and kv.shape[-1] == LAYOUT.bytes_per_token
and (extra_kv is None or extra_kv.shape[-1] == LAYOUT.bytes_per_token)
)
@triton.jit
def _combine(
PART,
MAX,
SUM,
SINK,
OUT,
NT: tl.constexpr,
ST: tl.constexpr,
H: tl.constexpr,
BD: tl.constexpr,
):
b, h, tile = tl.program_id(0), tl.program_id(1), tl.program_id(2)
t, d = tl.arange(0, ST), tile * BD + tl.arange(0, BD)
den = tl.load(SUM + (b * NT + t) * H + h, t < NT, 0)
mx = tl.load(MAX + (b * NT + t) * H + h, t < NT, 0)
mx = tl.where(den > 0, mx, -float("inf"))
sink = tl.load(SINK + h)
m = tl.maximum(tl.max(mx, 0), sink)
m = tl.where(tl.abs(m) == float("inf"), 0.0, m)
factor = tl.exp(mx - m)
denominator = tl.sum(den * factor, 0) + tl.exp(sink - m)
vals = tl.load(
PART + ((b * NT + t[:, None]) * H + h) * 512 + d[None, :],
t[:, None] < NT,
0,
)
out = tl.sum(vals * factor[:, None], 0) / denominator
out = tl.where((denominator > 0) & (sink != float("inf")), out, 0.0)
tl.store(OUT + (b * H + h) * 512 + d, out)
def swapab_attention(
q,
kv,
indices,
lengths,
sink,
extra_kv=None,
extra_indices=None,
extra_lengths=None,
):
"""V4-layout attention on 16 heads; `extra_*` is a second slot range appended
to each request's keys, and the attention sink is folded in exactly once."""
from .decode_attention_sm100_gluon import partial_gluon
block = 64
b, h, d = q.shape[0], q.shape[-2], q.shape[-1]
assert q.ndim in (3, 4) and (q.ndim == 3 or q.shape[1] == 1)
assert 0 < b <= MAX_BATCH and h == NUM_HEADS and d == HEAD_DIM
assert q.dtype == torch.bfloat16 and q.stride(-1) == 1
assert kv.shape[-1] == LAYOUT.bytes_per_token
assert kv.dtype in (torch.uint8, torch.float8_e4m3fn)
assert indices.stride(-1) == 1 and lengths.is_contiguous()
assert sink.stride(0) == 1 and sink.numel() >= h
nk = indices.shape[-1]
ne = 0 if extra_indices is None else extra_indices.shape[-1]
assert block in (32, 64, 128) and nk > 0
if extra_kv is None:
assert extra_indices is None and extra_lengths is None
extra_kv, extra_indices, extra_lengths = kv, indices, lengths
else:
assert extra_kv.shape[-1] == LAYOUT.bytes_per_token
assert extra_kv.dtype in (torch.uint8, torch.float8_e4m3fn)
assert extra_indices.stride(-1) == 1 and extra_lengths.is_contiguous()
kv, extra_kv = kv.view(torch.uint8), extra_kv.view(torch.uint8)
kt = triton.cdiv(nk, block)
nt = kt + triton.cdiv(ne, block)
partial = torch.empty((b, nt, h, 512), dtype=torch.float32, device=q.device)
maximum = torch.empty((b, nt, h), dtype=torch.float32, device=q.device)
sums = torch.empty_like(maximum)
out = torch.empty((b, h, 512), dtype=q.dtype, device=q.device)
partial_gluon[(b, nt)](
q,
kv,
extra_kv,
indices,
extra_indices,
lengths,
extra_lengths,
partial,
maximum,
sums,
QS=q.stride(0),
QH=q.stride(-2),
IS=indices.stride(0),
EIS=extra_indices.stride(0),
KP=kv.shape[1],
KS=kv.stride(0),
EP=extra_kv.shape[1],
ES=extra_kv.stride(0),
NK=nk,
NE=ne,
NT=nt,
KT=kt,
BT=block,
H=h,
SCALE=SOFTMAX_SCALE,
KTOKENS=kv.shape[0] * kv.shape[1],
ETOKENS=extra_kv.shape[0] * extra_kv.shape[1],
COMPENSATE=True,
SWAP_AB=True,
DATA_BYTES=LAYOUT.data_bytes,
SCALE_BYTES=LAYOUT.scale_bytes,
TILE=LAYOUT.tile_size,
num_warps=4,
)
bd = 64 if ne else 512
_combine[(b, h, triton.cdiv(512, bd))](
partial,
maximum,
sums,
sink,
out,
NT=nt,
ST=triton.next_power_of_2(nt),
H=h,
BD=bd,
num_warps=4,
)
return out
@@ -0,0 +1,190 @@
"""Native 16-head Blackwell (tcgen05) MMA layouts for paged V4-layout attention."""
from triton.experimental import gluon
from triton.experimental.gluon import language as gl
from triton.experimental.gluon.language.nvidia.blackwell import (
TensorMemoryLayout,
allocate_tensor_memory,
fence_async_shared,
get_tmem_reg_layout,
mbarrier,
tcgen05_commit,
tcgen05_mma,
)
@gluon.jit
def _load_v4(
CACHE,
ids,
valid,
PAGE: gl.constexpr,
STRIDE: gl.constexpr,
KV_LAYOUT: gl.constexpr,
DATA_BYTES: gl.constexpr,
SCALE_BYTES: gl.constexpr,
TILE: gl.constexpr,
):
# V4 row: 448 fp8 nope + 64 bf16 rope = DATA_BYTES, plus one ue8m0 scale per
# TILE values in the page's scale rows.
d = gl.arange(0, 512, gl.SliceLayout(0, KV_LAYOUT))
base = (ids // PAGE).to(gl.int64)[:, None] * STRIDE
slot = (ids % PAGE)[:, None]
mask = valid[:, None] & (d[None, :] < 448)
bits = gl.load(CACHE + base + slot * DATA_BYTES + d[None, :], mask, 0)
fp8 = bits.to(gl.float8e4nv, bitcast=True).to(gl.float32)
exponent = gl.load(
CACHE + base + PAGE * DATA_BYTES + slot * SCALE_BYTES + d[None, :] // TILE,
mask,
0,
).to(gl.int32)
scale = gl.where(exponent == 0, 0x00400000, exponent << 23).to(
gl.float32, bitcast=True
)
rope_ptr = (CACHE + base + slot * DATA_BYTES + 448 + (d[None, :] - 448) * 2).to(
gl.pointer_type(gl.bfloat16)
)
rope = gl.load(rope_ptr, valid[:, None] & (d[None, :] >= 448), 0)
return gl.where(d[None, :] < 448, fp8 * scale, rope.to(gl.float32)).to(gl.bfloat16)
@gluon.jit
def partial_gluon(
Q,
K,
E,
IDX,
EI,
L,
EL,
PART,
MAX,
SUM,
QS: gl.constexpr,
QH: gl.constexpr,
IS: gl.constexpr,
EIS: gl.constexpr,
KP: gl.constexpr,
KS: gl.constexpr,
EP: gl.constexpr,
ES: gl.constexpr,
NK: gl.constexpr,
NE: gl.constexpr,
NT: gl.constexpr,
KT: gl.constexpr,
BT: gl.constexpr,
H: gl.constexpr,
SCALE: gl.constexpr,
KTOKENS: gl.constexpr,
ETOKENS: gl.constexpr,
COMPENSATE: gl.constexpr,
SWAP_AB: gl.constexpr,
DATA_BYTES: gl.constexpr,
SCALE_BYTES: gl.constexpr,
TILE: gl.constexpr,
):
gl.static_assert(SWAP_AB and H == 16 and (BT == 64 or BT == 128))
b, t = gl.program_id(0), gl.program_id(1)
kv_layout: gl.constexpr = gl.BlockedLayout([1, 8], [4, 8], [4, 1], [1, 0])
n = gl.arange(0, BT, gl.SliceLayout(1, kv_layout))
if t < KT:
at = t * BT + n
length = gl.load(L + b)
ids = gl.load(IDX + b * IS + at, at < NK, -1)
valid = (at < NK) & (at < length) & (ids >= 0) & (ids < KTOKENS)
kv = _load_v4(
K,
gl.maximum(ids, 0),
valid,
KP,
KS,
kv_layout,
DATA_BYTES,
SCALE_BYTES,
TILE,
)
else:
at = (t - KT) * BT + n
length = gl.load(EL + b)
ids = gl.load(EI + b * EIS + at, at < NE, -1)
valid = (at < NE) & (at < length) & (ids >= 0) & (ids < ETOKENS)
kv = _load_v4(
E,
gl.maximum(ids, 0),
valid,
EP,
ES,
kv_layout,
DATA_BYTES,
SCALE_BYTES,
TILE,
)
qh = gl.arange(0, H, gl.SliceLayout(1, kv_layout))
qd = gl.arange(0, 512, gl.SliceLayout(0, kv_layout))
q = gl.load(Q + b * QS + qh[:, None] * QH + qd[None, :])
q_smem = gl.allocate_shared_memory(
gl.bfloat16,
[H, 512],
gl.NVMMASharedLayout(swizzle_byte_width=128, element_bitwidth=16),
value=q,
)
kv_smem = gl.allocate_shared_memory(
gl.bfloat16,
[BT, 512],
gl.NVMMASharedLayout(swizzle_byte_width=128, element_bitwidth=16),
value=kv,
)
score_tmem = allocate_tensor_memory(
gl.float32, [BT, H], TensorMemoryLayout(block=(BT, H), col_stride=1)
)
bar = gl.allocate_shared_memory(gl.int64, [1], mbarrier.MBarrierLayout())
mbarrier.init(bar, count=1)
fence_async_shared()
tcgen05_mma(kv_smem, q_smem.permute((1, 0)), score_tmem, use_acc=False)
tcgen05_commit(bar)
mbarrier.wait(bar, phase=0)
score_layout: gl.constexpr = get_tmem_reg_layout(
gl.float32, (BT, H), TensorMemoryLayout(block=(BT, H), col_stride=1), 4
)
scores = score_tmem.load(score_layout) * SCALE
valid = gl.convert_layout(valid, gl.SliceLayout(1, score_layout))
scores = gl.where(valid[:, None], scores, -float("inf"))
mx = gl.max(scores, 0)
mx = gl.where(mx == -float("inf"), 0.0, mx)
prob = gl.exp(scores - mx[None, :])
denom = gl.sum(prob, 0)
p_hi = prob.to(gl.bfloat16)
p_smem = gl.allocate_shared_memory(
gl.bfloat16,
[BT, H],
gl.NVMMASharedLayout(swizzle_byte_width=32, element_bitwidth=16),
value=p_hi,
)
out_tmem = allocate_tensor_memory(
gl.float32, [512, H], TensorMemoryLayout(block=(128, H), col_stride=1)
)
fence_async_shared()
tcgen05_mma(kv_smem.permute((1, 0)), p_smem, out_tmem, use_acc=False)
tcgen05_commit(bar)
mbarrier.wait(bar, phase=1)
if COMPENSATE:
p_lo = (prob - p_hi.to(gl.float32)).to(gl.bfloat16)
p_smem.store(p_lo)
fence_async_shared()
tcgen05_mma(kv_smem.permute((1, 0)), p_smem, out_tmem, use_acc=True)
tcgen05_commit(bar)
mbarrier.wait(bar, phase=0)
mbarrier.invalidate(bar)
out_layout: gl.constexpr = get_tmem_reg_layout(
gl.float32, (512, H), TensorMemoryLayout(block=(128, H), col_stride=1), 4
)
value = out_tmem.load(out_layout)
hd_layout: gl.constexpr = gl.BlockedLayout([1, 4], [4, 8], [4, 1], [1, 0])
value_hd = gl.convert_layout(value.permute((1, 0)), hd_layout)
h = gl.arange(0, H, gl.SliceLayout(1, hd_layout))
d = gl.arange(0, 512, gl.SliceLayout(0, hd_layout))
gl.store(PART + ((b * NT + t) * H + h[:, None]) * 512 + d[None, :], value_hd)
stat_layout: gl.constexpr = gl.BlockedLayout([1], [32], [4], [0])
hs = gl.arange(0, H, stat_layout)
gl.store(MAX + (b * NT + t) * H + hs, gl.convert_layout(mx, stat_layout))
gl.store(SUM + (b * NT + t) * H + hs, gl.convert_layout(denom, stat_layout))
@@ -7,8 +7,11 @@ from triton.language.extra import libdevice
from sglang.kernels.ops.attention.dsv4.torch_quant import FP4_AMAX_FLOOR from sglang.kernels.ops.attention.dsv4.torch_quant import FP4_AMAX_FLOOR
INDEX_HEAD_DIM = 128
# One index-K slot: 64 packed e2m1 bytes and four ue8m0 block exponents. # One index-K slot: 64 packed e2m1 bytes and four ue8m0 block exponents.
INDEX_K_SLOT_BYTES = 64 + 4 INDEX_K_PAYLOAD_BYTES = tl.constexpr(64)
INDEX_K_SCALE_BYTES = tl.constexpr(4)
INDEX_K_SLOT_BYTES = INDEX_K_PAYLOAD_BYTES.value + INDEX_K_SCALE_BYTES.value
@triton.jit @triton.jit
@@ -326,3 +329,138 @@ def index_k_rope_pack(
num_warps=4, num_warps=4,
) )
return (payload, scale) if cache is None else None return (payload, scale) if cache is None else None
@triton.jit
def _e2m1_decode(code):
# code: uint 0..15 -> e2m1 value. exp = bits 2..1, mantissa = bit 0, sign = bit 3.
e = (code >> 1) & 3
m = (code & 1).to(tl.float32)
sub = m * 0.5
nor = (1.0 + m * 0.5) * tl.exp2((e - 1).to(tl.float32))
v = tl.where(e == 0, sub, nor)
return tl.where((code >> 3) == 1, -v, v)
@triton.jit
def _fp4_index_logits_kernel(
q_ptr, # [B, H, D] bf16, fq4 queries (already rope'd)
w_ptr, # [B, H] bf16 head weights (softmax scale folded in)
slots_ptr, # [B, L] int64 pool slots per (request, compressed position)
lens_ptr, # [B] int64 visible compressed positions per request
table_ptr, # [num_pages, page_size * 64 + page_size * 4] uint8
out_ptr, # [B, L] fp32 logits, -inf beyond lens
L,
page_size,
row_stride,
stride_qb,
stride_qh,
stride_wb,
H: tl.constexpr,
HALF_D: tl.constexpr, # D // 2 == 64 nibble-pairs per row
BLOCK_L: tl.constexpr,
):
b = tl.program_id(0)
lb = tl.program_id(1)
offs_l = lb * BLOCK_L + tl.arange(0, BLOCK_L)
offs_h = tl.arange(0, H)
offs_i = tl.arange(
0, HALF_D
) # byte index i holds elements 2i (low nibble), 2i+1 (high nibble)
n_vis = tl.load(lens_ptr + b)
valid = offs_l < tl.minimum(n_vis, L)
slot = tl.load(slots_ptr + b * L + offs_l, mask=offs_l < L, other=0).to(tl.int64)
page = slot // page_size
off = slot % page_size
row_base = page * row_stride
# K payload: [BLOCK_L, HALF_D] uint8
pay = tl.load(
table_ptr
+ row_base[:, None]
+ off[:, None] * INDEX_K_PAYLOAD_BYTES
+ offs_i[None, :],
mask=valid[:, None],
other=0,
)
low = _e2m1_decode(pay & 0x0F)
high = _e2m1_decode((pay >> 4) & 0x0F)
# e8m0 block scales: element j uses block j // 32 -> byte i uses block i // 16.
sc_idx = offs_i // 16
exps = tl.load(
table_ptr
+ row_base[:, None]
+ page_size * INDEX_K_PAYLOAD_BYTES
+ off[:, None] * INDEX_K_SCALE_BYTES
+ sc_idx[None, :],
mask=valid[:, None],
other=127,
)
scale = tl.exp2(exps.to(tl.float32) - 127.0)
k_low = (low * scale).to(tl.bfloat16) # [BLOCK_L, HALF_D] elements 2i
k_high = (high * scale).to(tl.bfloat16) # elements 2i+1
# queries: even / odd elements, [H, HALF_D] bf16
q_even = tl.load(
q_ptr + b * stride_qb + offs_h[:, None] * stride_qh + 2 * offs_i[None, :]
)
q_odd = tl.load(
q_ptr + b * stride_qb + offs_h[:, None] * stride_qh + 2 * offs_i[None, :] + 1
)
acc = tl.dot(q_even, tl.trans(k_low)) # [H, BLOCK_L] fp32
acc += tl.dot(q_odd, tl.trans(k_high))
# reference rounding points: bf16 dot -> relu -> * bf16 weight -> bf16 -> sum -> bf16
s = acc.to(tl.bfloat16).to(tl.float32)
s = tl.maximum(s, 0.0)
w = tl.load(w_ptr + b * stride_wb + offs_h).to(tl.float32)
s = (s * w[:, None]).to(tl.bfloat16).to(tl.float32)
logit = tl.sum(s, axis=0).to(tl.bfloat16).to(tl.float32)
logit = tl.where(valid, logit, float("-inf"))
tl.store(out_ptr + b * L + offs_l, logit, mask=offs_l < L)
def fp4_index_logits_decode(
q: torch.Tensor,
weights: torch.Tensor,
slots: torch.Tensor,
lens: torch.Tensor,
table: torch.Tensor,
page_size: int,
) -> torch.Tensor:
"""Decode index logits from the fp4 index-K pool. q [B, H, 128] bf16, weights
[B, H], slots [B, L] int64, lens [B] int64, table = the layer's index-K page
buffer (uint8, 2D). Returns [B, L] fp32 logits, -inf at positions >= lens,
rounded as the torch reference does."""
assert q.dtype == torch.bfloat16 and q.shape[-1] == INDEX_HEAD_DIM
B, H, _ = q.shape
L = slots.shape[1]
assert table.dtype == torch.uint8 and table.dim() == 2
q = q.contiguous()
weights = weights.to(torch.bfloat16).contiguous()
slots = slots.contiguous()
out = torch.empty((B, L), dtype=torch.float32, device=q.device)
if L == 0:
return out
BLOCK_L = 64
grid = (B, triton.cdiv(L, BLOCK_L))
_fp4_index_logits_kernel[grid](
q,
weights,
slots,
lens.to(torch.int64).contiguous(),
table,
out,
L,
page_size,
table.stride(0),
q.stride(0),
q.stride(1),
weights.stride(0),
H=H,
HALF_D=INDEX_HEAD_DIM // 2,
BLOCK_L=BLOCK_L,
num_warps=4,
)
return out
@@ -400,6 +400,7 @@ class BuildCausalSwaPageIndices:
seq_lens_casual: torch.Tensor, seq_lens_casual: torch.Tensor,
swa_window: int, swa_window: int,
page_index_aligned_size: int, page_index_aligned_size: int,
swa_replay_start: Optional[torch.Tensor] = None,
) -> torch.Tensor: ) -> torch.Tensor:
return build_causal_swa_page_indices( return build_causal_swa_page_indices(
req_to_token=req_to_token, req_to_token=req_to_token,
@@ -408,6 +409,7 @@ class BuildCausalSwaPageIndices:
seq_lens_casual=seq_lens_casual, seq_lens_casual=seq_lens_casual,
swa_window=swa_window, swa_window=swa_window,
page_index_aligned_size=page_index_aligned_size, page_index_aligned_size=page_index_aligned_size,
swa_replay_start=swa_replay_start,
) )
@classmethod @classmethod
@@ -420,6 +422,7 @@ class BuildCausalSwaPageIndices:
seq_lens_casual: torch.Tensor, seq_lens_casual: torch.Tensor,
swa_window: int, swa_window: int,
page_index_aligned_size: int, page_index_aligned_size: int,
swa_replay_start: Optional[torch.Tensor] = None,
) -> torch.Tensor: ) -> torch.Tensor:
return build_causal_swa_page_indices_triton( return build_causal_swa_page_indices_triton(
req_to_token=req_to_token, req_to_token=req_to_token,
@@ -428,9 +431,41 @@ class BuildCausalSwaPageIndices:
seq_lens_casual=seq_lens_casual, seq_lens_casual=seq_lens_casual,
swa_window=swa_window, swa_window=swa_window,
page_index_aligned_size=page_index_aligned_size, page_index_aligned_size=page_index_aligned_size,
swa_replay_start=swa_replay_start,
) )
def late_layer_tail_layout(
*,
extend_lens_cpu: list[int],
seq_lens_cpu: list[int],
tail_len: int,
device: torch.device,
) -> tuple[torch.Tensor, list[int], torch.Tensor]:
"""Tail rows of each prefill extend: its last min(tail_len, extend_len) tokens.
Returns (token indices into the extend, per-request tail lengths, per-row
absolute window floor)."""
tail_lens_cpu = [min(tail_len, n) for n in extend_lens_cpu]
if len(extend_lens_cpu) == 1:
n, t, s = extend_lens_cpu[0], tail_lens_cpu[0], seq_lens_cpu[0]
floor = torch.full((t,), s - t, dtype=torch.int32, device=device)
return torch.arange(n - t, n, device=device), tail_lens_cpu, floor
# One H2D copy for the three length vectors; launch count does not grow with bs.
lens = torch.tensor([extend_lens_cpu, tail_lens_cpu, seq_lens_cpu], device=device)
extend_lens, tail_lens, seq_lens = lens[0], lens[1], lens[2]
total = sum(tail_lens_cpu)
req = torch.repeat_interleave(
torch.arange(len(tail_lens_cpu), device=device), tail_lens, output_size=total
)
offs = (
torch.arange(total, device=device)
- (torch.cumsum(tail_lens, 0) - tail_lens)[req]
)
token_indices = (torch.cumsum(extend_lens, 0) - tail_lens)[req] + offs
floor = (seq_lens - tail_lens)[req].to(torch.int32)
return token_indices, tail_lens_cpu, floor
def build_causal_swa_page_indices( def build_causal_swa_page_indices(
*, *,
req_to_token: torch.Tensor, req_to_token: torch.Tensor,
@@ -439,14 +474,20 @@ def build_causal_swa_page_indices(
seq_lens_casual: torch.Tensor, seq_lens_casual: torch.Tensor,
swa_window: int, swa_window: int,
page_index_aligned_size: int, page_index_aligned_size: int,
swa_replay_start: Optional[torch.Tensor] = None,
) -> torch.Tensor: ) -> torch.Tensor:
"""Window slots each query attends to, -1 where empty. swa_replay_start floors
each row's window at that absolute position; None is the plain causal window."""
device = seq_lens_casual.device device = seq_lens_casual.device
pos_causal = seq_lens_casual - 1 pos_causal = seq_lens_casual - 1
num_qo_tokens = seq_lens_casual.size(0) num_qo_tokens = seq_lens_casual.size(0)
offsets = pos_causal.unsqueeze(1) - torch.arange( offsets = pos_causal.unsqueeze(1) - torch.arange(
swa_window, dtype=torch.int32, device=device swa_window, dtype=torch.int32, device=device
).unsqueeze(0) ).unsqueeze(0)
if swa_replay_start is None:
invalid_offset_mask = offsets < 0 invalid_offset_mask = offsets < 0
else:
invalid_offset_mask = offsets < swa_replay_start.to(offsets.dtype).unsqueeze(1)
offsets.masked_fill_(invalid_offset_mask, 0) offsets.masked_fill_(invalid_offset_mask, 0)
raw_indices = req_to_token[req_pool_indices_repeated[:, None], offsets] raw_indices = req_to_token[req_pool_indices_repeated[:, None], offsets]
assert raw_indices.shape == (num_qo_tokens, swa_window) assert raw_indices.shape == (num_qo_tokens, swa_window)
@@ -470,10 +511,12 @@ def _causal_swa_page_indices_kernel(
full_to_swa_ptr, full_to_swa_ptr,
req_pool_ptr, req_pool_ptr,
seq_lens_ptr, seq_lens_ptr,
swa_replay_start_ptr,
out_ptr, out_ptr,
rt_stride, rt_stride,
swa_window, swa_window,
padded_width, padded_width,
HAS_SWA_REPLAY_START: tl.constexpr,
BLOCK_K: tl.constexpr, BLOCK_K: tl.constexpr,
): ):
row = tl.program_id(0) row = tl.program_id(0)
@@ -481,12 +524,16 @@ def _causal_swa_page_indices_kernel(
rp = tl.load(req_pool_ptr + row).to(tl.int64) rp = tl.load(req_pool_ptr + row).to(tl.int64)
base = req_to_token_ptr + rp * rt_stride base = req_to_token_ptr + rp * rt_stride
out_base = out_ptr + row.to(tl.int64) * padded_width out_base = out_ptr + row.to(tl.int64) * padded_width
if HAS_SWA_REPLAY_START:
floor = tl.load(swa_replay_start_ptr + row).to(tl.int64)
else:
floor = tl.zeros((), dtype=tl.int64)
for k0 in range(0, padded_width, BLOCK_K): for k0 in range(0, padded_width, BLOCK_K):
k = k0 + tl.arange(0, BLOCK_K) k = k0 + tl.arange(0, BLOCK_K)
kmask = k < padded_width kmask = k < padded_width
off = pos - k.to(tl.int64) off = pos - k.to(tl.int64)
valid = (k < swa_window) & (off >= 0) & kmask valid = (k < swa_window) & (off >= floor) & kmask
full_loc = tl.load(base + tl.where(valid, off, 0), mask=valid, other=-1).to( full_loc = tl.load(base + tl.where(valid, off, 0), mask=valid, other=-1).to(
tl.int64 tl.int64
) )
@@ -502,6 +549,7 @@ def build_causal_swa_page_indices_triton(
seq_lens_casual: torch.Tensor, seq_lens_casual: torch.Tensor,
swa_window: int, swa_window: int,
page_index_aligned_size: int, page_index_aligned_size: int,
swa_replay_start: Optional[torch.Tensor] = None,
) -> torch.Tensor: ) -> torch.Tensor:
num_qo_tokens = seq_lens_casual.size(0) num_qo_tokens = seq_lens_casual.size(0)
padded_width = ( padded_width = (
@@ -513,15 +561,19 @@ def build_causal_swa_page_indices_triton(
device=seq_lens_casual.device, device=seq_lens_casual.device,
) )
BLOCK_K = 256 BLOCK_K = 256
has_swa_replay_start = swa_replay_start is not None
_causal_swa_page_indices_kernel[(num_qo_tokens,)]( _causal_swa_page_indices_kernel[(num_qo_tokens,)](
req_to_token, req_to_token,
full_to_swa_mapping, full_to_swa_mapping,
req_pool_indices_repeated, req_pool_indices_repeated,
seq_lens_casual, seq_lens_casual,
# Unused when HAS_SWA_REPLAY_START is False; any valid pointer will do.
swa_replay_start if has_swa_replay_start else seq_lens_casual,
out, out,
req_to_token.stride(0), req_to_token.stride(0),
swa_window, swa_window,
padded_width, padded_width,
HAS_SWA_REPLAY_START=has_swa_replay_start,
BLOCK_K=BLOCK_K, BLOCK_K=BLOCK_K,
) )
return out return out
+132 -15
View File
@@ -1,11 +1,12 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING, Optional, Tuple from typing import TYPE_CHECKING, Dict, Optional, Tuple
import torch import torch
import triton import triton
import triton.language as tl import triton.language as tl
from triton.language.extra import libdevice
from sglang.kernels.jit.utils import cache_once, is_arch_support_pdl, load_jit from sglang.kernels.jit.utils import cache_once, is_arch_support_pdl, load_jit
from sglang.kernels.kernel_api_logging import debug_kernel_api from sglang.kernels.kernel_api_logging import debug_kernel_api
@@ -90,8 +91,12 @@ def moe_fused_gate_jit(
def _router_triton_kernel( def _router_triton_kernel(
scores_ptr, # [M, N] raw logits, fp32/fp16/bf16 (upcast to fp32 on load) scores_ptr, # [M, N] raw logits, fp32/fp16/bf16 (upcast to fp32 on load)
bias_ptr, # [N] fp32/fp16/bf16 (upcast to fp32 on load) bias_ptr, # [N] fp32/fp16/bf16 (upcast to fp32 on load)
bias_alt_ptr,
input_ids_ptr,
num_token_non_padded_ptr,
out_weights_ptr, # [M, K] fp32 out_weights_ptr, # [M, K] fp32
out_indices_ptr, # [M, K] int32 out_indices_ptr, # [M, K] int32
out_packed_ptr, # [M, K] int32 (HAS_PACKED)
M, M,
routed_scaling_factor, routed_scaling_factor,
moe_softcapping, moe_softcapping,
@@ -106,17 +111,28 @@ def _router_triton_kernel(
EXPERTS_PER_GROUP: tl.constexpr, # N // N_GROUP EXPERTS_PER_GROUP: tl.constexpr, # N // N_GROUP
BLOCK_G: tl.constexpr, # >= N_GROUP, power of 2 BLOCK_G: tl.constexpr, # >= N_GROUP, power of 2
SCORING_FUNC: tl.constexpr, # 0 = sigmoid, 1 = sqrtsoftplus, 2 = softmax SCORING_FUNC: tl.constexpr, # 0 = sigmoid, 1 = sqrtsoftplus, 2 = softmax
SQRTSOFTPLUS_LOG1P: tl.constexpr, # sqrtsoftplus via log1p (V4.1 numerics)
HAS_SOFTCAP: tl.constexpr, # tanh softcapping (softmax only) HAS_SOFTCAP: tl.constexpr, # tanh softcapping (softmax only)
RENORMALIZE: tl.constexpr, RENORMALIZE: tl.constexpr,
APPLY_SCALE: tl.constexpr, # apply_routed_scaling_factor_on_output APPLY_SCALE: tl.constexpr, # apply_routed_scaling_factor_on_output
HAS_BIAS: tl.constexpr, HAS_BIAS: tl.constexpr,
HAS_TOKEN_BIAS: tl.constexpr,
BIAS_ALT_TOKEN_ID: tl.constexpr,
HAS_PADDING: tl.constexpr,
HAS_PACKED: tl.constexpr,
RENORMALIZE_EPSILON: tl.constexpr,
USE_PDL: tl.constexpr, USE_PDL: tl.constexpr,
stride_bias,
stride_bias_alt,
stride_input_ids,
stride_sm, stride_sm,
stride_sn, stride_sn,
stride_wm, stride_wm,
stride_wk, stride_wk,
stride_im, stride_im,
stride_ik, stride_ik,
stride_pm,
stride_pk,
) -> None: ) -> None:
# Row-tiled: each program handles BLOCK_M rows; all reductions run along the # Row-tiled: each program handles BLOCK_M rows; all reductions run along the
# expert (N) axis. Tiling rows keeps CTAs large enough to stay occupancy-bound # expert (N) axis. Tiling rows keeps CTAs large enough to stay occupancy-bound
@@ -136,12 +152,30 @@ def _router_triton_kernel(
# Plain softmax routing has no bias, so keep the zero value in registers # Plain softmax routing has no bias, so keep the zero value in registers
# rather than materializing and clearing a device tensor per call. # rather than materializing and clearing a device tensor per call.
if HAS_BIAS: if HAS_BIAS:
bias = tl.load(bias_ptr + offs_n, mask=mask_n, other=0.0).to(tl.float32) bias = tl.load(bias_ptr + offs_n * stride_bias, mask=mask_n, other=0.0).to(
tl.float32
)
else: else:
bias = tl.zeros([BLOCK_N], dtype=tl.float32) bias = tl.zeros([BLOCK_N], dtype=tl.float32)
if HAS_TOKEN_BIAS:
bias_alt = tl.load(
bias_alt_ptr + offs_n * stride_bias_alt, mask=mask_n, other=0.0
).to(tl.float32)
live_m = mask_m
if HAS_PADDING:
live_m = live_m & (offs_m < tl.load(num_token_non_padded_ptr))
row_bias = bias[None, :]
if HAS_TOKEN_BIAS:
input_ids = tl.load(
input_ids_ptr + offs_m * stride_input_ids, mask=live_m, other=0
)
row_bias = tl.where(
(input_ids == BIAS_ALT_TOKEN_ID)[:, None], bias_alt[None, :], row_bias
)
row_ptr = scores_ptr + offs_m[:, None] * stride_sm + offs_n[None, :] * stride_sn row_ptr = scores_ptr + offs_m[:, None] * stride_sm + offs_n[None, :] * stride_sn
mask2d = mask_m[:, None] & mask_n[None, :] mask2d = live_m[:, None] & mask_n[None, :]
scores = tl.load(row_ptr, mask=mask2d, other=0.0).to( scores = tl.load(row_ptr, mask=mask2d, other=0.0).to(
tl.float32 tl.float32
) # [BLOCK_M, BLOCK_N] ) # [BLOCK_M, BLOCK_N]
@@ -149,17 +183,21 @@ def _router_triton_kernel(
if SCORING_FUNC == 0: if SCORING_FUNC == 0:
# sigmoid(x) = 1 / (1 + exp(-x)); bias is for ranking only, weight is bias-free. # sigmoid(x) = 1 / (1 + exp(-x)); bias is for ranking only, weight is bias-free.
activated = tl.sigmoid(scores) activated = tl.sigmoid(scores)
biased = activated + bias[None, :] biased = activated + row_bias
elif SCORING_FUNC == 1: elif SCORING_FUNC == 1:
# sqrt(softplus(x)). log(1.0 + exp(x)) rounds to 0 below -16.64 and overflows if SQRTSOFTPLUS_LOG1P:
# above 88.7; Triton has no log1p, so recover it from log via z*log(u)/(u-1). # log1p preserves small positive scores for negative logits.
sp = tl.where(scores > 20.0, scores, libdevice.log1p(libdevice.exp(scores)))
activated = libdevice.sqrt(sp)
else:
# Open-coded log1p; reproduces the DeepSeek-V4 sqrtsoftplus numerics.
z = tl.exp(-tl.abs(scores)) z = tl.exp(-tl.abs(scores))
u = 1.0 + z u = 1.0 + z
exact = u == 1.0 exact = u == 1.0
log1p_z = tl.where(exact, z, z * tl.log(u) / tl.where(exact, 1.0, u - 1.0)) log1p_z = tl.where(exact, z, z * tl.log(u) / tl.where(exact, 1.0, u - 1.0))
sp = tl.maximum(scores, 0.0) + log1p_z sp = tl.maximum(scores, 0.0) + log1p_z
activated = tl.sqrt(sp) activated = tl.sqrt(sp)
biased = activated + bias[None, :] biased = activated + row_bias
else: else:
# softmax over the row: weight is the softmax probability (bias kept), with # softmax over the row: weight is the softmax probability (bias kept), with
# optional tanh softcapping. Ranking by the (softcapped, biased) logit is # optional tanh softcapping. Ranking by the (softcapped, biased) logit is
@@ -169,7 +207,7 @@ def _router_triton_kernel(
# tanh(z) = 2*sigmoid(2z) - 1 (avoids relying on tl.math.tanh availability). # tanh(z) = 2*sigmoid(2z) - 1 (avoids relying on tl.math.tanh availability).
z = logit / moe_softcapping z = logit / moe_softcapping
logit = moe_softcapping * (2.0 * tl.sigmoid(2.0 * z) - 1.0) logit = moe_softcapping * (2.0 * tl.sigmoid(2.0 * z) - 1.0)
biased = logit + bias[None, :] biased = logit + row_bias
biased = tl.where(mask_n[None, :], biased, -float("inf")) biased = tl.where(mask_n[None, :], biased, -float("inf"))
row_max = tl.max(biased, axis=1)[:, None] # [BLOCK_M, 1] row_max = tl.max(biased, axis=1)[:, None] # [BLOCK_M, 1]
exp_row = tl.where(mask_n[None, :], tl.exp(biased - row_max), 0.0) exp_row = tl.where(mask_n[None, :], tl.exp(biased - row_max), 0.0)
@@ -178,8 +216,11 @@ def _router_triton_kernel(
biased = tl.where(mask_n[None, :], biased, -float("inf")) # [BLOCK_M, BLOCK_N] biased = tl.where(mask_n[None, :], biased, -float("inf")) # [BLOCK_M, BLOCK_N]
# Map NaN -> a finite floor if SCORING_FUNC == 1 and SQRTSOFTPLUS_LOG1P:
biased = tl.where(biased == biased, biased, -1e30) # [BLOCK_M, BLOCK_N] # Rank NaNs above finite scores, matching torch.topk.
biased = tl.where(biased == biased, biased, float("inf"))
else:
biased = tl.where(biased == biased, biased, -1e30)
# Grouped routing (DeepSeek-V3 noaux_tc): per-group score = sum of the top-2 # Grouped routing (DeepSeek-V3 noaux_tc): per-group score = sum of the top-2
# biased values; keep TOPK_GROUP groups (lowest group id wins ties); mask the # biased values; keep TOPK_GROUP groups (lowest group id wins ties); mask the
@@ -214,9 +255,10 @@ def _router_triton_kernel(
selected_idx = tl.zeros([BLOCK_M, BLOCK_K], dtype=tl.int32) selected_idx = tl.zeros([BLOCK_M, BLOCK_K], dtype=tl.int32)
cur = biased # [BLOCK_M, BLOCK_N] cur = biased # [BLOCK_M, BLOCK_N]
remaining = tl.broadcast_to(mask_n[None, :], (BLOCK_M, BLOCK_N))
for k in tl.static_range(K_ROUTED): for k in tl.static_range(K_ROUTED):
max_val = tl.max(cur, axis=1)[:, None] # [BLOCK_M, 1] max_val = tl.max(cur, axis=1)[:, None] # [BLOCK_M, 1]
is_max = cur == max_val is_max = remaining & (cur == max_val)
lane_id = tl.where(is_max, offs_n[None, :], N + 1) # lowest expert id wins ties lane_id = tl.where(is_max, offs_n[None, :], N + 1) # lowest expert id wins ties
win_lane = tl.min(lane_id, axis=1)[:, None].to(tl.int32) # [BLOCK_M, 1] win_lane = tl.min(lane_id, axis=1)[:, None].to(tl.int32) # [BLOCK_M, 1]
win_activated = tl.sum( win_activated = tl.sum(
@@ -225,7 +267,8 @@ def _router_triton_kernel(
slot = offs_k[None, :] == k # [1, BLOCK_K] slot = offs_k[None, :] == k # [1, BLOCK_K]
selected_vals = tl.where(slot, win_activated, selected_vals) selected_vals = tl.where(slot, win_activated, selected_vals)
selected_idx = tl.where(slot, win_lane, selected_idx) selected_idx = tl.where(slot, win_lane, selected_idx)
cur = tl.where(offs_n[None, :] == win_lane, -float("inf"), cur) remaining = remaining & (offs_n[None, :] != win_lane)
cur = tl.where(remaining, cur, -float("inf"))
routed_sum = tl.sum(tl.where(mask_k_routed[None, :], selected_vals, 0.0), axis=1)[ routed_sum = tl.sum(tl.where(mask_k_routed[None, :], selected_vals, 0.0), axis=1)[
:, None :, None
@@ -244,10 +287,16 @@ def _router_triton_kernel(
tl.extra.cuda.gdc_launch_dependents() tl.extra.cuda.gdc_launch_dependents()
if RENORMALIZE: if RENORMALIZE:
if RENORMALIZE_EPSILON > 0.0:
norm = routed_sum + RENORMALIZE_EPSILON
else:
norm = tl.where(routed_sum > 0.0, routed_sum, 1.0) # [BLOCK_M, 1] norm = tl.where(routed_sum > 0.0, routed_sum, 1.0) # [BLOCK_M, 1]
selected_vals = selected_vals / norm selected_vals = selected_vals / norm
if APPLY_SCALE: if APPLY_SCALE:
selected_vals = selected_vals * routed_scaling_factor selected_vals = selected_vals * routed_scaling_factor
if HAS_PADDING:
selected_vals = tl.where(live_m[:, None], selected_vals, 0.0)
selected_idx = tl.where(live_m[:, None], selected_idx, -1)
out_w_ptr = ( out_w_ptr = (
out_weights_ptr + offs_m[:, None] * stride_wm + offs_k[None, :] * stride_wk out_weights_ptr + offs_m[:, None] * stride_wm + offs_k[None, :] * stride_wk
@@ -258,6 +307,25 @@ def _router_triton_kernel(
store_mask = mask_m[:, None] & mask_k_total[None, :] store_mask = mask_m[:, None] & mask_k_total[None, :]
tl.store(out_w_ptr, selected_vals, mask=store_mask) tl.store(out_w_ptr, selected_vals, mask=store_mask)
tl.store(out_i_ptr, selected_idx, mask=store_mask) tl.store(out_i_ptr, selected_idx, mask=store_mask)
if HAS_PACKED:
# Must stay bitwise identical to fused_pack_topk.
w_bits = selected_vals.to(tl.bfloat16).to(tl.int16, bitcast=True).to(tl.int32)
packed = (selected_idx << 16) | (w_bits & 0xFFFF)
out_p_ptr = (
out_packed_ptr + offs_m[:, None] * stride_pm + offs_k[None, :] * stride_pk
)
tl.store(out_p_ptr, packed, mask=store_mask)
_DUMMY_I32: Dict[torch.device, torch.Tensor] = {}
def _dummy_i32(device: torch.device) -> torch.Tensor:
# Placeholder pointer for kernel args whose constexpr flag is off.
t = _DUMMY_I32.get(device)
if t is None:
t = _DUMMY_I32[device] = torch.empty(1, dtype=torch.int32, device=device)
return t
@debug_kernel_api @debug_kernel_api
@@ -273,14 +341,29 @@ def moe_fused_gate(
moe_softcapping: float = 0.0, moe_softcapping: float = 0.0,
num_expert_group: int = 1, num_expert_group: int = 1,
topk_group: int = 1, topk_group: int = 1,
*,
bias_alt: Optional[torch.Tensor] = None,
input_ids: Optional[torch.Tensor] = None,
bias_alt_token_id: Optional[int] = None,
num_token_non_padded: Optional[torch.Tensor] = None,
renormalize_epsilon: float = 0.0,
packed_out: Optional[torch.Tensor] = None,
sqrtsoftplus_log1p: bool = False,
) -> Tuple[torch.Tensor, torch.Tensor]: ) -> Tuple[torch.Tensor, torch.Tensor]:
"""Triton fused router: scoring + bias + topk + (optional) renorm/scale. """Triton fused router: scoring + bias + topk + (optional) renorm/scale.
Mirrors the semantics of :func:`moe_fused_gate_jit` (the CUDA JIT kernel). Mirrors :func:`moe_fused_gate_jit` (the CUDA JIT kernel) for the shared
parameters; the keyword-only extras are Triton-only.
With ``num_expert_group > 1`` it performs DeepSeek-V3 grouped routing With ``num_expert_group > 1`` it performs DeepSeek-V3 grouped routing
(per-group top-2-sum group scores, keep ``topk_group`` groups, then top-k (per-group top-2-sum group scores, keep ``topk_group`` groups, then top-k
within). The first argument is named ``scores`` (raw GEMM logits) to match within). ``scores`` contains raw GEMM logits.
the existing call sites.
Rows past the device scalar ``num_token_non_padded`` return zero weights and -1 ids.
Positive ``renormalize_epsilon`` uses ``sum + epsilon`` instead of the zero-sum guard.
``sqrtsoftplus_log1p`` evaluates sqrtsoftplus through ``log1p`` and ranks NaNs first
(DeepSeek-V4.1); off, the DeepSeek-V4 formula and NaN order are kept.
``packed_out`` ([M, topk] int32, optional) receives the FlashInfer routed-MoE form
``(id << 16) | bf16_bits(weight)``, bitwise identical to ``fused_pack_topk``.
""" """
scoring_func_int = _SCORING_FUNC_MAP.get(scoring_func.lower()) scoring_func_int = _SCORING_FUNC_MAP.get(scoring_func.lower())
assert scoring_func_int is not None, ( assert scoring_func_int is not None, (
@@ -310,6 +393,15 @@ def moe_fused_gate(
"scores and bias must have same num_experts" "scores and bias must have same num_experts"
) )
assert topk > num_fused_shared_experts, "topk must be > num_fused_shared_experts" assert topk > num_fused_shared_experts, "topk must be > num_fused_shared_experts"
if input_ids is not None:
assert bias_alt is not None and bias_alt_token_id is not None
assert bias is not None and bias_alt.shape == bias.shape
assert input_ids.shape == (scores.size(0),)
if packed_out is not None:
assert packed_out.dtype == torch.int32, "packed_out must be int32"
assert packed_out.shape == (scores.size(0), topk), (
"packed_out must be [M, topk]"
)
if routed_scaling_factor is None: if routed_scaling_factor is None:
routed_scaling_factor = 1.0 routed_scaling_factor = 1.0
@@ -325,6 +417,11 @@ def moe_fused_gate(
and num_fused_shared_experts == 0 and num_fused_shared_experts == 0
and num_expert_group <= 1 and num_expert_group <= 1
and moe_softcapping == 0.0 and moe_softcapping == 0.0
and input_ids is None
and num_token_non_padded is None
and renormalize_epsilon == 0.0
and packed_out is None
and bias.stride(0) == 1
): ):
radix_args = ( radix_args = (
scores, scores,
@@ -348,6 +445,8 @@ def moe_fused_gate(
weights = torch.empty((M, K), dtype=torch.float32, device=scores.device) weights = torch.empty((M, K), dtype=torch.float32, device=scores.device)
indices = torch.empty((M, K), dtype=torch.int32, device=scores.device) indices = torch.empty((M, K), dtype=torch.int32, device=scores.device)
if M == 0:
return weights, indices
BLOCK_N = triton.next_power_of_2(N) # 256 -> 256, 384 -> 512 BLOCK_N = triton.next_power_of_2(N) # 256 -> 256, 384 -> 512
BLOCK_K = triton.next_power_of_2(K) # 6 -> 8, 8 -> 8 BLOCK_K = triton.next_power_of_2(K) # 6 -> 8, 8 -> 8
@@ -363,11 +462,18 @@ def moe_fused_gate(
grid = (triton.cdiv(M, BLOCK_M),) grid = (triton.cdiv(M, BLOCK_M),)
use_pdl = is_arch_support_pdl() use_pdl = is_arch_support_pdl()
extra = {"launch_pdl": True} if use_pdl else {} extra = {"launch_pdl": True} if use_pdl else {}
# Dynamo cannot analyze the kernel (PDL inline asm), so it writes back every
# pointer arg; aliasing an output as an unused arg's fallback clobbers it.
_unused_i32 = _dummy_i32(scores.device)
_router_triton_kernel[grid]( _router_triton_kernel[grid](
scores, scores,
bias if bias is not None else scores, bias if bias is not None else scores,
bias_alt,
input_ids,
num_token_non_padded,
weights, weights,
indices, indices,
packed_out if packed_out is not None else _unused_i32,
M, M,
float(routed_scaling_factor), float(routed_scaling_factor),
float(moe_softcapping), float(moe_softcapping),
@@ -382,17 +488,28 @@ def moe_fused_gate(
EXPERTS_PER_GROUP=experts_per_group, EXPERTS_PER_GROUP=experts_per_group,
BLOCK_G=BLOCK_G, BLOCK_G=BLOCK_G,
SCORING_FUNC=scoring_func_int, SCORING_FUNC=scoring_func_int,
SQRTSOFTPLUS_LOG1P=bool(sqrtsoftplus_log1p),
HAS_SOFTCAP=bool(moe_softcapping != 0.0), HAS_SOFTCAP=bool(moe_softcapping != 0.0),
RENORMALIZE=bool(renormalize), RENORMALIZE=bool(renormalize),
APPLY_SCALE=bool(apply_routed_scaling_factor_on_output), APPLY_SCALE=bool(apply_routed_scaling_factor_on_output),
HAS_BIAS=bias is not None, HAS_BIAS=bias is not None,
HAS_TOKEN_BIAS=input_ids is not None,
BIAS_ALT_TOKEN_ID=bias_alt_token_id,
HAS_PADDING=num_token_non_padded is not None,
HAS_PACKED=packed_out is not None,
RENORMALIZE_EPSILON=renormalize_epsilon,
USE_PDL=use_pdl, USE_PDL=use_pdl,
stride_bias=bias.stride(0) if bias is not None else 0,
stride_bias_alt=bias_alt.stride(0) if bias_alt is not None else 0,
stride_input_ids=input_ids.stride(0) if input_ids is not None else 0,
stride_sm=scores.stride(0), stride_sm=scores.stride(0),
stride_sn=scores.stride(1), stride_sn=scores.stride(1),
stride_wm=weights.stride(0), stride_wm=weights.stride(0),
stride_wk=weights.stride(1), stride_wk=weights.stride(1),
stride_im=indices.stride(0), stride_im=indices.stride(0),
stride_ik=indices.stride(1), stride_ik=indices.stride(1),
stride_pm=packed_out.stride(0) if packed_out is not None else 0,
stride_pk=packed_out.stride(1) if packed_out is not None else 0,
num_warps=num_warps, num_warps=num_warps,
**extra, **extra,
) )
@@ -0,0 +1,43 @@
"""Translate committed cache locations to the draft SWA pool."""
import torch
import triton
import triton.language as tl
@triton.jit
def _committed_swa_locations(
LOC,
MAP,
LENS,
OUT,
N: tl.constexpr,
WIDTH: tl.constexpr,
MAP_SIZE: tl.constexpr,
BLOCK: tl.constexpr,
):
i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
length = tl.load(LENS + i // WIDTH, i < N, other=0)
committed = (i < N) & (i % WIDTH < length)
loc = tl.load(LOC + i, committed, other=0).to(tl.int64)
# Preserve torch indexing for an unused negative location.
loc = tl.where(loc < 0, loc + MAP_SIZE, loc)
swa = tl.load(MAP + loc, committed, other=-1).to(tl.int32)
tl.store(OUT + i, swa, i < N)
def committed_swa_locations(cache_loc, full_to_swa_mapping, commit_lens, width):
assert cache_loc.numel() == commit_lens.numel() * width
out = torch.empty_like(cache_loc, dtype=torch.int32)
if cache_loc.numel():
_committed_swa_locations[(triton.cdiv(cache_loc.numel(), 256),)](
cache_loc,
full_to_swa_mapping,
commit_lens,
out,
cache_loc.numel(),
width,
full_to_swa_mapping.numel(),
256,
)
return out
@@ -584,12 +584,14 @@ class AcceptGreedy:
target_logits: torch.Tensor, target_logits: torch.Tensor,
verify_num_draft_tokens: int, verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None, cutoff_verify_lens: Optional[torch.Tensor] = None,
fused_argmax: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return accept_greedy( return accept_greedy(
candidates=candidates, candidates=candidates,
target_logits=target_logits, target_logits=target_logits,
verify_num_draft_tokens=verify_num_draft_tokens, verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens, cutoff_verify_lens=cutoff_verify_lens,
fused_argmax=fused_argmax,
) )
@classmethod @classmethod
@@ -600,12 +602,14 @@ class AcceptGreedy:
target_logits: torch.Tensor, target_logits: torch.Tensor,
verify_num_draft_tokens: int, verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None, cutoff_verify_lens: Optional[torch.Tensor] = None,
fused_argmax: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return accept_greedy_triton( return accept_greedy_triton(
candidates=candidates, candidates=candidates,
target_logits=target_logits, target_logits=target_logits,
verify_num_draft_tokens=verify_num_draft_tokens, verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens, cutoff_verify_lens=cutoff_verify_lens,
fused_argmax=fused_argmax,
) )
@@ -615,9 +619,10 @@ def accept_greedy(
target_logits: torch.Tensor, target_logits: torch.Tensor,
verify_num_draft_tokens: int, verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None, cutoff_verify_lens: Optional[torch.Tensor] = None,
fused_argmax: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
bs = candidates.shape[0] bs = candidates.shape[0]
target_predict = torch.argmax(target_logits, dim=-1).view( target_predict = _row_argmax(target_logits, fused=fused_argmax).view(
bs, verify_num_draft_tokens bs, verify_num_draft_tokens
) )
correct_len, bonus = compute_dflash_correct_drafts_and_bonus( correct_len, bonus = compute_dflash_correct_drafts_and_bonus(
@@ -661,15 +666,35 @@ def gather_row_bonus_triton(*, table: torch.Tensor, idx: torch.Tensor) -> torch.
return out return out
def _row_argmax(logits: torch.Tensor, fused: bool = False) -> torch.Tensor:
# torch.argmax uses one block per row; at few rows x wide vocab that is ~7x
# off the memory the reduction touches. The fused kernel does not reproduce
# torch.argmax's NaN selection, hence the opt-in.
if (
fused
and logits.is_cuda
and logits.dim() == 2
and logits.dtype == torch.float32
and logits.stride(1) == 1
and logits.shape[0] <= 64
and logits.shape[1] >= 4096
):
from sglang.kernels.ops.speculative.row_argmax import row_argmax
return row_argmax(logits)
return torch.argmax(logits, dim=-1)
def accept_greedy_triton( def accept_greedy_triton(
*, *,
candidates: torch.Tensor, candidates: torch.Tensor,
target_logits: torch.Tensor, target_logits: torch.Tensor,
verify_num_draft_tokens: int, verify_num_draft_tokens: int,
cutoff_verify_lens: Optional[torch.Tensor] = None, cutoff_verify_lens: Optional[torch.Tensor] = None,
fused_argmax: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
bs = candidates.shape[0] bs = candidates.shape[0]
target_predict = torch.argmax(target_logits, dim=-1).view( target_predict = _row_argmax(target_logits, fused=fused_argmax).view(
bs, verify_num_draft_tokens bs, verify_num_draft_tokens
) )
correct_len, bonus = compute_dflash_correct_drafts_and_bonus( correct_len, bonus = compute_dflash_correct_drafts_and_bonus(
@@ -384,9 +384,14 @@ class CommitKvProj:
*, *,
main_x: torch.Tensor, main_x: torch.Tensor,
wkv_linears: list[torch.nn.Module], wkv_linears: list[torch.nn.Module],
allow_strided_output: bool = False,
) -> list[torch.Tensor]: ) -> list[torch.Tensor]:
if main_x.is_cuda and _fused_commit_kv_proj_supported(wkv_linears=wkv_linears): if main_x.is_cuda and _fused_commit_kv_proj_supported(wkv_linears=wkv_linears):
return cls.triton(main_x=main_x, wkv_linears=wkv_linears) return cls.triton(
main_x=main_x,
wkv_linears=wkv_linears,
allow_strided_output=allow_strided_output,
)
return cls.torch(main_x=main_x, wkv_linears=wkv_linears) return cls.torch(main_x=main_x, wkv_linears=wkv_linears)
@classmethod @classmethod
@@ -404,8 +409,13 @@ class CommitKvProj:
*, *,
main_x: torch.Tensor, main_x: torch.Tensor,
wkv_linears: list[torch.nn.Module], wkv_linears: list[torch.nn.Module],
allow_strided_output: bool = False,
) -> list[torch.Tensor]: ) -> list[torch.Tensor]:
return commit_kv_proj_fused(main_x=main_x, wkv_linears=wkv_linears) return commit_kv_proj_fused(
main_x=main_x,
wkv_linears=wkv_linears,
allow_strided_output=allow_strided_output,
)
def commit_kv_proj( def commit_kv_proj(
@@ -420,11 +430,20 @@ def commit_kv_proj_fused(
*, *,
main_x: torch.Tensor, main_x: torch.Tensor,
wkv_linears: list[torch.nn.Module], wkv_linears: list[torch.nn.Module],
allow_strided_output: bool = False,
) -> list[torch.Tensor]: ) -> list[torch.Tensor]:
num_stages = len(wkv_linears) num_stages = len(wkv_linears)
stacked = _stacked_wkv_weight(wkv_linears=wkv_linears) stacked = _stacked_wkv_weight(wkv_linears=wkv_linears)
if stacked.fp8_scale is not None: if stacked.mxfp8_scale is not None:
kv_all = wkv_linears[0].quant_method.w8a8_mxfp8_linear(
input=main_x,
weight=stacked.weight,
weight_scale=stacked.mxfp8_scale,
input_scale=None,
bias=None,
)
elif stacked.fp8_scale is not None:
quant_method = wkv_linears[0].quant_method quant_method = wkv_linears[0].quant_method
kv_all = quant_method.w8a8_block_fp8_linear( kv_all = quant_method.w8a8_block_fp8_linear(
input=main_x, input=main_x,
@@ -438,15 +457,14 @@ def commit_kv_proj_fused(
kv_all = torch.nn.functional.linear(main_x, stacked.weight) kv_all = torch.nn.functional.linear(main_x, stacked.weight)
head_dim = kv_all.shape[-1] // num_stages head_dim = kv_all.shape[-1] // num_stages
return [ slices = list(kv_all.split(head_dim, dim=-1))
kv_all[:, i * head_dim : (i + 1) * head_dim].contiguous() return slices if allow_strided_output else [kv.contiguous() for kv in slices]
for i in range(num_stages)
]
class _StackedWkvWeight(msgspec.Struct): class _StackedWkvWeight(msgspec.Struct):
weight: torch.Tensor weight: torch.Tensor
fp8_scale: Optional[torch.Tensor] fp8_scale: Optional[torch.Tensor]
mxfp8_scale: Optional[torch.Tensor] = None
def _stacked_wkv_weight(*, wkv_linears: list[torch.nn.Module]) -> _StackedWkvWeight: def _stacked_wkv_weight(*, wkv_linears: list[torch.nn.Module]) -> _StackedWkvWeight:
@@ -500,6 +518,21 @@ def _build_stacked_wkv_weight(
) -> _StackedWkvWeight: ) -> _StackedWkvWeight:
if _block_quant_stack_applies(wkv_linears=wkv_linears): if _block_quant_stack_applies(wkv_linears=wkv_linears):
weight = torch.cat([linear.weight for linear in wkv_linears], dim=0) weight = torch.cat([linear.weight for linear in wkv_linears], dim=0)
backend = getattr(wkv_linears[0].quant_method, "mxfp8_dense_backend", None)
if (
backend is not None
and (backend.is_flashinfer_cutlass() or backend.is_flashinfer_cutedsl())
and all(
getattr(linear, "block_fp8_mxfp8_ready", False)
and linear.weight.shape[0] % 128 == 0
for linear in wkv_linears
)
):
# 128-row-aligned scale tiles concatenate without breaking the swizzle.
scale = torch.cat(
[linear.weight_scale_inv_swizzled.reshape(-1) for linear in wkv_linears]
)
return _StackedWkvWeight(weight=weight, fp8_scale=None, mxfp8_scale=scale)
if wkv_linears[0].weight_scale_inv.dtype == torch.int32: if wkv_linears[0].weight_scale_inv.dtype == torch.int32:
from sglang.srt.layers.quantization.fp8_utils import ( from sglang.srt.layers.quantization.fp8_utils import (
inverse_transform_scale_ue8m0, inverse_transform_scale_ue8m0,
@@ -78,6 +78,8 @@ def parse_cuda_graph_config(server_args: Any):
_set(Phase.DECODE, "max_bs", cfg.cuda_graph_max_bs_decode) _set(Phase.DECODE, "max_bs", cfg.cuda_graph_max_bs_decode)
if cfg.cuda_graph_max_bs_prefill is not None: if cfg.cuda_graph_max_bs_prefill is not None:
_set(Phase.PREFILL, "max_bs", cfg.cuda_graph_max_bs_prefill) _set(Phase.PREFILL, "max_bs", cfg.cuda_graph_max_bs_prefill)
if cfg.cuda_graph_max_seq_len_prefill is not None:
_set(Phase.PREFILL, "max_seq_len", cfg.cuda_graph_max_seq_len_prefill)
if cfg.cuda_graph_bs_decode is not None: if cfg.cuda_graph_bs_decode is not None:
_set(Phase.DECODE, "bs", cfg.cuda_graph_bs_decode) _set(Phase.DECODE, "bs", cfg.cuda_graph_bs_decode)
if cfg.cuda_graph_bs_prefill is not None: if cfg.cuda_graph_bs_prefill is not None:
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
_deepseek_v4_kv_cache_dtype, _deepseek_v4_kv_cache_dtype,
declare_resolution, declare_resolution,
model_config_of,
resolving_view, resolving_view,
run_post_process_pass, run_post_process_pass,
) )
@@ -244,6 +245,8 @@ def validate_deepseek_v4_cp(server_args: ServerArgs) -> None:
f"DeepSeekV4 CP supports moe_a2a_backend in {supported_a2a_backends}, " f"DeepSeekV4 CP supports moe_a2a_backend in {supported_a2a_backends}, "
f"got {cfg.moe_a2a_backend!r}." f"got {cfg.moe_a2a_backend!r}."
) )
if model_config_of(server_args).hf_config.model_type != "deepseek_v41":
# The CP-aware sparse prefill chunk cache is validated on V4.1 only.
logger.warning( logger.warning(
"Disabling SGLANG_OPT_FLASHMLA_SPARSE_PREFILL because DeepSeekV4 " "Disabling SGLANG_OPT_FLASHMLA_SPARSE_PREFILL because DeepSeekV4 "
"context parallelism is enabled." "context parallelism is enabled."
@@ -255,3 +258,126 @@ def validate_deepseek_v4_cp(server_args: ServerArgs) -> None:
f"dp_size={cfg.dp_size}, moe_dense_tp_size={cfg.moe_dense_tp_size}, " f"dp_size={cfg.dp_size}, moe_dense_tp_size={cfg.moe_dense_tp_size}, "
f"attn_cp_size={cfg.attn_cp_size}, ep_size={cfg.ep_size}, tp_size={cfg.tp_size}" f"attn_cp_size={cfg.attn_cp_size}, ep_size={cfg.ep_size}, tp_size={cfg.tp_size}"
) )
def validate_deepseek_v41_features(server_args: ServerArgs) -> None:
from sglang.kernels.ops.attention.dsv4.unified_kv_kernels.env_gate import (
is_unified_kv_triton,
)
cfg = resolving_view(server_args)
if model_config_of(server_args).hf_config.model_type != "deepseek_v41":
if cfg.enable_encoder_swa_bounded_replay:
raise ValueError(
"--enable-encoder-swa-bounded-replay requires DeepSeek-V4.1"
)
return
if cfg.enable_encoder_swa_bounded_replay:
from sglang.srt.model_executor.cuda_graph_config import Backend
incompatible = (
("non-CUDA hardware", not get_platform().is_cuda),
(
"prefill CUDA graphs",
cfg.cuda_graph_config.prefill.backend != Backend.DISABLED,
),
("DP attention", cfg.enable_dp_attention),
("context parallelism", cfg.attn_cp_size > 1),
("external cache linker", cfg.enable_unified_cache_external_linker),
("unified memory", cfg.enable_unified_memory),
("PD disaggregation", cfg.disaggregation_mode != "null"),
("mixed prefill/decode", cfg.enable_mixed_chunk),
("LoRA", cfg.enable_lora),
("radix sessions", cfg.enable_session_radix_cache),
)
for feature, enabled in incompatible:
if enabled:
raise ValueError(
f"--enable-encoder-swa-bounded-replay does not support {feature} yet"
)
if (
cfg.max_running_requests is None
or cfg.max_running_requests <= 0
or not cfg.chunked_prefill_size
or cfg.chunked_prefill_size < 128
):
raise ValueError(
"encoder SWA replay requires explicit --max-running-requests and --chunked-prefill-size >= 128"
)
unsupported = (
(
"speculative decoding other than DSpark",
cfg.speculative_algorithm is not None
and str(cfg.speculative_algorithm).upper() != "DSPARK",
),
("HiSparse", cfg.enable_hisparse),
("the unified KV layout", is_unified_kv_triton()),
# The trtllm-gen path has no uniform-FP8 pool for V4.1's ratio-1/2 layers.
("the trtllm DSv4 attention backend", cfg.dsv4_attn_backend == "trtllm"),
("two-batch overlap", cfg.enable_two_batch_overlap),
("pipeline parallelism", cfg.pp_size > 1),
)
for feature, enabled in unsupported:
if enabled:
raise ValueError(
f"DeepSeek-V4.1 does not support {feature} yet; disable it to "
"serve this model."
)
if cfg.disaggregation_mode != "null" and cfg.speculative_algorithm is not None:
from sglang.srt.speculative.ragged_verify import (
RaggedVerifyMode,
read_ragged_verify_mode,
)
if (
read_ragged_verify_mode() is not RaggedVerifyMode.STATIC
or cfg.disaggregation_transfer_backend != "mooncake"
or cfg.dp_size != 1
or cfg.enable_dp_attention
or cfg.attn_cp_size != 1
or cfg.dcp_size != 1
):
raise ValueError(
"DeepSeek-V4.1 DSpark PD requires static verify, Mooncake, "
"DP=1 and CP=1. Both servers must enable DSpark with the same "
"block size and TP size."
)
from sglang.srt.model_executor.cuda_graph_config import Backend, Phase, with_phase
prefill_graph = cfg.cuda_graph_config.prefill
if prefill_graph.backend != Backend.DISABLED and prefill_graph.max_seq_len is None:
# The captured low-ratio indexer scores a static context width; 16k
# keeps it inside the candidate window at under 1 ms per layer.
declare_resolution(
server_args,
"validate_deepseek_v41_features",
cuda_graph_config=with_phase(
cfg.cuda_graph_config, Phase.PREFILL, max_seq_len=16 * 1024
),
)
logger.warning(
"Setting cuda_graph_config[prefill].max_seq_len to 16384 for "
"DeepSeek-V4.1; longer contexts run eager prefill."
)
if cfg.enable_decoder_swa_bounded_replay:
from sglang.srt.model_executor.cuda_graph_config import Backend
# Late layers see a per-request tail slice, not the captured prefill shape.
incompatible = (
(
"the prefill CUDA graph",
cfg.cuda_graph_config.prefill.backend != Backend.DISABLED,
),
# input_ids_global is a DP-wide gather, so the tail slice cannot apply.
("DP attention", cfg.enable_dp_attention),
)
for feature, enabled in incompatible:
if enabled:
raise ValueError(
"--enable-decoder-swa-bounded-replay cannot be combined with "
f"{feature} yet; disable one of them."
)
@@ -96,6 +96,15 @@ class ExecFeatures(msgspec.Struct):
bool, bool,
"Enable returning indexer topk indices of layers with indexer with responses.", "Enable returning indexer topk indices of layers with indexer with responses.",
] = False ] = False
enable_encoder_swa_bounded_replay: A[
bool,
"DeepSeek-V4.1 encoder SWA bounded replay: cache Main KV and Indexer keys only, "
"rebuild request-owned SWA windows on prefix hits. Experimental; CUDA only.",
] = False
enable_decoder_swa_bounded_replay: A[
bool,
"DeepSeek-V4.1 decoder SWA bounded replay: after the last kv_source layer, run the remaining layers over only the last window_size tokens of a prefill. Main and indexer KV stay exact; nothing is replayed. Deterministic for a fixed prompt and chunk size.",
] = False
sampling_mask_max_tokens: A[ sampling_mask_max_tokens: A[
int, int,
"The maximum number of token IDs in a returned sampling mask. Requests " "The maximum number of token IDs in a returned sampling mask. Requests "
@@ -486,6 +495,11 @@ class ExecGraph(msgspec.Struct):
cuda_graph_max_bs_prefill: A[ cuda_graph_max_bs_prefill: A[
Optional[int], "Maximum batch size captured for the prefill cuda graph." Optional[int], "Maximum batch size captured for the prefill cuda graph."
] = None ] = None
cuda_graph_max_seq_len_prefill: A[
Optional[int],
"Longest sequence a prefill cuda graph replay admits; longer batches "
"run eager prefill. Folds into cuda_graph_config[prefill].max_seq_len.",
] = None
cuda_graph_bs_decode: A[ cuda_graph_bs_decode: A[
Optional[List[int]], Optional[List[int]],
"Explicit list of batch sizes to capture for the decode cuda graph.", "Explicit list of batch sizes to capture for the decode cuda graph.",
@@ -163,6 +163,23 @@ class Schedule(msgspec.Struct):
fallback=0.8, fallback=0.8,
), ),
] = None ] = None
# Recorded by the cache hook; the effective field answers the fallback when unset.
_swa_full_tokens_ratio_explicitly_set: A[
Optional[bool],
Arg(no_cli=True),
] = None
swa_prefix_tails: A[
Optional[int],
Arg(
help=(
"When the SWA KV pool is sized from the request cap (DeepSeek-V4 "
"family), how many radix-cached prefix tails it keeps room for. "
"Each tail is one sliding window plus one page. Default: 4 x "
"max_running_requests per attention-DP rank, 0 when the radix "
"cache is disabled."
),
),
] = None
disable_hybrid_swa_memory: A[ disable_hybrid_swa_memory: A[
bool, Arg(help="Disable the hybrid SWA memory pool.", resolvable=True) bool, Arg(help="Disable the hybrid SWA memory pool.", resolvable=True)
] = False ] = False
@@ -203,6 +203,13 @@ def handle_cache_compatibility(server_args: Any) -> None:
"both build a decode host pool." "both build a decode host pool."
) )
if cfg._swa_full_tokens_ratio_explicitly_set is None:
declare_resolution(
server_args,
"_handle_cache_compatibility",
_swa_full_tokens_ratio_explicitly_set=cfg.swa_full_tokens_ratio is not None,
)
# Validate the effective ratio: model branches may declare a reset # Validate the effective ratio: model branches may declare a reset
# (e.g. Step3p forces 1.0 under hierarchical cache) that supersedes # (e.g. Step3p forces 1.0 under hierarchical cache) that supersedes
# the user input before it ever takes effect. # the user input before it ever takes effect.
@@ -210,6 +217,9 @@ def handle_cache_compatibility(server_args: Any) -> None:
# claimed the field, and the value to range-check is the effective one. # claimed the field, and the value to range-check is the effective one.
if not (0 < resolution_result(server_args, "swa_full_tokens_ratio") <= 1.0): if not (0 < resolution_result(server_args, "swa_full_tokens_ratio") <= 1.0):
raise ValueError("--swa-full-tokens-ratio should be in range (0, 1.0].") raise ValueError("--swa-full-tokens-ratio should be in range (0, 1.0].")
prefix_tails = resolved_view(server_args).swa_prefix_tails
if prefix_tails is not None and prefix_tails < 0:
raise ValueError("--swa-prefix-tails should be a non-negative integer.")
def handle_unified_memory_pool(server_args: Any) -> None: def handle_unified_memory_pool(server_args: Any) -> None:
@@ -416,8 +416,11 @@ def handle_model_specific_adjustments(server_args: Any):
from sglang.srt.arg_groups.deepseek_v4_hook import ( from sglang.srt.arg_groups.deepseek_v4_hook import (
validate_deepseek_v4_cp, validate_deepseek_v4_cp,
validate_deepseek_v4_mega_moe_token_budget, validate_deepseek_v4_mega_moe_token_budget,
validate_deepseek_v41_features,
) )
# Before the CP validation: V4.1 rejects CP outright, the actionable message.
validate_deepseek_v41_features(server_args)
validate_deepseek_v4_cp(server_args) validate_deepseek_v4_cp(server_args)
validate_deepseek_v4_mega_moe_token_budget(server_args) validate_deepseek_v4_mega_moe_token_budget(server_args)
@@ -13,21 +13,44 @@ from sglang.srt.arg_groups.model_override_base import (
) )
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.runtime_context import get_platform from sglang.srt.runtime_context import get_platform
from sglang.srt.utils import is_flashinfer_available
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@_register_for("DeepseekV4ForCausalLM") @_register_for("DeepseekV4ForCausalLM")
def _deepseek_v4_overrides(server_args: Any, hf_config: Any) -> dict: def _deepseek_v4_overrides(server_args: Any, hf_config: Any) -> dict:
"""DeepSeek V4 attention/page/window/MoE-runner defaults (from """Attention, page and MoE defaults; the rest lives in deepseek_v4_hook."""
arg_groups/deepseek_v4_hook.py). The kv-cache dtype and NPU split-backend
writes, the max_running_requests fill and the validations stay in the
hook at its legacy slot."""
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
overrides: Dict[str, Any] = {"attention_backend": "dsv4"} overrides: Dict[str, Any] = {"attention_backend": "dsv4"}
# MXFP8 serves this checkpoint's 32-wide ue8m0 blocks on SM100/SM103;
# explicit backend choices, including Triton, take precedence.
quant = getattr(hf_config, "quantization_config", None) or {}
if (
getattr(hf_config, "model_type", None) == "deepseek_v41"
and cfg.device == "cuda"
and not get_platform().is_hip
and get_platform().is_sm100
and cfg.fp8_gemm_runner_backend == "auto"
and quant.get("quant_method") == "fp8"
and quant.get("weight_block_size") == [32, 32]
and quant.get("scale_fmt") == "ue8m0"
and is_flashinfer_available()
):
overrides["fp8_gemm_runner_backend"] = "flashinfer_cutedsl"
logger.info("Use flashinfer_cutedsl for DeepSeek-V4.1 MXFP8 dense GEMMs.")
# Left unset, the pool configurator sizes the SWA pool from the request cap.
if (
cfg.swa_full_tokens_ratio is None
and getattr(hf_config, "model_type", None) != "deepseek_v41"
):
overrides["swa_full_tokens_ratio"] = 0.1
logger.info(f"Setting swa_full_tokens_ratio to 0.1 for {model_arch}.")
page_size = 256 page_size = 256
if cfg.device == "npu": if cfg.device == "npu":
# NPU keeps the device-aware "dsv4" backend (the registry routes it to # NPU keeps the device-aware "dsv4" backend (the registry routes it to
@@ -43,10 +66,6 @@ def _deepseek_v4_overrides(server_args: Any, hf_config: Any) -> dict:
f"Use dsv4 attention backend for {model_arch}, setting page_size to {page_size}." f"Use dsv4 attention backend for {model_arch}, setting page_size to {page_size}."
) )
if cfg.swa_full_tokens_ratio is None:
overrides["swa_full_tokens_ratio"] = 0.1
logger.info(f"Setting swa_full_tokens_ratio to 0.1 for {model_arch}.")
if cfg.moe_runner_backend == "auto": if cfg.moe_runner_backend == "auto":
model_config = model_config_of(server_args) model_config = model_config_of(server_args)
# nvidia/DeepSeek-V4-Pro-NVFP4 uses the routed TRT-LLM runner. # nvidia/DeepSeek-V4-Pro-NVFP4 uses the routed TRT-LLM runner.
+12 -1
View File
@@ -1014,7 +1014,17 @@ def _flashinfer_allreduce_fusion_auto_enable(view: Any) -> dict:
single-node systems. Reads the mid-resolution enable_dp_attention / single-node systems. Reads the mid-resolution enable_dp_attention /
moe_a2a_backend (after the DeepSeek CP and a2a declarations), exactly moe_a2a_backend (after the DeepSeek CP and a2a declarations), exactly
like the legacy tail block.""" like the legacy tail block."""
model_arch = model_config_of(view).hf_config.architectures[0] hf_config = model_config_of(view).hf_config
model_arch = hf_config.architectures[0]
# V4.1 TP4 uses the custom push plane for decode and fused MoE finalize.
prefer_custom_dsv41 = (
getattr(hf_config, "model_type", None) == "deepseek_v41"
and getattr(hf_config, "hidden_size", None) == 5120
and get_platform().is_blackwell
and view.tp_size == 4
and view.nnodes == 1
and not view.disable_custom_all_reduce
)
if envs.SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION.get() and model_arch in { if envs.SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION.get() and model_arch in {
"Qwen3_5MoeForCausalLM", "Qwen3_5MoeForCausalLM",
"Qwen3_5MoeForConditionalGeneration", "Qwen3_5MoeForConditionalGeneration",
@@ -1032,6 +1042,7 @@ def _flashinfer_allreduce_fusion_auto_enable(view: Any) -> dict:
if ( if (
view.flashinfer_allreduce_fusion_backend is None view.flashinfer_allreduce_fusion_backend is None
and model_arch in _FLASHINFER_ALLREDUCE_FUSION_ARCHS and model_arch in _FLASHINFER_ALLREDUCE_FUSION_ARCHS
and not prefer_custom_dsv41
and (get_platform().is_sm90 or get_platform().is_sm100) and (get_platform().is_sm90 or get_platform().is_sm100)
and view.tp_size > 1 and view.tp_size > 1
and not view.enable_dp_attention and not view.enable_dp_attention
@@ -810,6 +810,7 @@ class TboForwardBatchPreparer:
# The child runs the same forward, so it keeps the parent's # The child runs the same forward, so it keeps the parent's
# sharding verdict; its counts above are already per-child. # sharding verdict; its counts above are already per-child.
attn_tp_sequence_sharded=batch.attn_tp_sequence_sharded, attn_tp_sequence_sharded=batch.attn_tp_sequence_sharded,
encoder_swa_replay=batch.encoder_swa_replay,
tbo_split_seq_index=None, tbo_split_seq_index=None,
tbo_parent_token_range=(start_token_index, end_token_index), tbo_parent_token_range=(start_token_index, end_token_index),
tbo_children=None, tbo_children=None,
+28
View File
@@ -103,8 +103,36 @@ class DeepSeekV4Config(PretrainedConfig):
compress_rope_theta: int = 40000 compress_rope_theta: int = 40000
compress_ratios: List[int] = field(default_factory=list) compress_ratios: List[int] = field(default_factory=list)
kv_source_layer_ids: List[int] = field(default_factory=list)
index_source_layer_ids: List[int] = field(default_factory=list)
candidate_source_layer_id: int = -1
candidate_topk_blocks: int = 0
candidate_block_size: int = 0
engram_layer_ids: List[int] = field(default_factory=list)
engram_num_embeddings: List[int] = field(default_factory=list)
engram_max_ngram_size: int = 1
engram_vocab_size: int = 0
engram_n_heads: int = 0
engram_head_dim: int = 0
engram_pad_token_id: int = 2
engram_compressed_vocab_size: int = 0
vision_n_layers: int = 0
vision_dim: int = 1024
vision_n_heads: int = 16
vision_inter_dim: int = 2816
vision_patch_size: int = 14
vision_rope_theta: float = 10000.0
vision_downsample_ratio: int = 3
vision_max_n_token: int = 1024
vision_min_pixels: int = 295936
vision_max_wh_ratio: Optional[int] = None
image_token_id: int = 129264
n_hash_layers: int = 3 n_hash_layers: int = 3
hc_mult: int = 4 hc_mult: int = 4
hc_pre_from_prev_sublayer: bool = False
q_head_norm: bool = True
hc_sinkhorn_iters: int = 20 hc_sinkhorn_iters: int = 20
hc_eps: float = 1e-6 hc_eps: float = 1e-6
+88
View File
@@ -0,0 +1,88 @@
"""Translate DeepSeek V4.1 HF configs to the runtime's flat config schema."""
from transformers import DeepseekV3Config, PretrainedConfig
_VISION_FIELDS = {
"num_hidden_layers": "vision_n_layers",
"hidden_size": "vision_dim",
"num_attention_heads": "vision_n_heads",
"intermediate_size": "vision_inter_dim",
"patch_size": "vision_patch_size",
"rope_theta": "vision_rope_theta",
"downsample_ratio": "vision_downsample_ratio",
"max_image_tokens": "vision_max_n_token",
"min_pixels": "vision_min_pixels",
"max_wh_ratio": "vision_max_wh_ratio",
}
def _config_dict(config):
return config.to_dict() if isinstance(config, PretrainedConfig) else dict(config)
def normalize_deepseek_v41_config(values):
values = dict(values)
text = values.pop("text_config", None)
vision = values.pop("vision_config", None)
if text is not None:
text = _config_dict(text)
text.pop("model_type", None)
values = {**text, **values}
if vision is not None:
vision = _config_dict(vision)
for source, target in _VISION_FIELDS.items():
if source in vision:
values.setdefault(target, vision[source])
if "model_type" in values:
values["model_type"] = "deepseek_v41"
if values.get("architectures") == ["DeepseekV41ForCausalLM"]:
values["architectures"] = ["DeepseekV4ForCausalLM"]
return values
class DeepseekV41Config(DeepseekV3Config):
# V3 accepts the V4.1 compression ratios; the native V4 config rejects 1/2.
model_type = "deepseek_v41"
vision_n_layers = 0
hc_pre_from_prev_sublayer = True
q_head_norm = False
kv_source_layer_ids = ()
index_source_layer_ids = ()
candidate_source_layer_id = -1
candidate_topk_blocks = 0
candidate_block_size = 0
engram_layer_ids = ()
engram_num_embeddings = ()
engram_max_ngram_size = 1
engram_vocab_size = 0
engram_n_heads = 0
engram_head_dim = 0
engram_pad_token_id = 2
engram_compressed_vocab_size = 0
def __init__(self, **kwargs):
kwargs = normalize_deepseek_v41_config(kwargs)
kwargs["model_type"] = "deepseek_v41"
super().__init__(**kwargs)
def to_dict(self):
values = super().to_dict()
values["model_type"] = "deepseek_v41"
return values
class DeepseekV41TextConfig(DeepseekV41Config):
model_type = "deepseek_v41_text"
# Transformers regenerates a dataclass initializer unless it is explicit.
__init__ = DeepseekV41Config.__init__
class DeepseekV41VisionConfig(PretrainedConfig):
model_type = "deepseek_v41_vision"
DEEPSEEK_V41_CONFIG_CLASSES = (
DeepseekV41Config,
DeepseekV41TextConfig,
DeepseekV41VisionConfig,
)
@@ -29,6 +29,7 @@ from sglang.srt.disaggregation.base.conn import (
from sglang.srt.disaggregation.utils import ( from sglang.srt.disaggregation.utils import (
DisaggregationMode, DisaggregationMode,
filter_kv_indices_for_cp_rank, filter_kv_indices_for_cp_rank,
get_dsv41_spec_layout,
) )
from sglang.srt.distributed import get_pp_group, get_world_group from sglang.srt.distributed import get_pp_group, get_world_group
from sglang.srt.environ import envs from sglang.srt.environ import envs
@@ -102,6 +103,7 @@ class PrefillServerInfo:
kv_cache_dtype: Optional[str] kv_cache_dtype: Optional[str]
follow_bootstrap_room: bool follow_bootstrap_room: bool
enable_dsa_cache_layer_split: bool = False enable_dsa_cache_layer_split: bool = False
dsv41_spec_layout: Optional[dict] = None
# PD true-retraction rebootstrap: the prefill's HTTP API port. The decode # PD true-retraction rebootstrap: the prefill's HTTP API port. The decode
# already knows the prefill host (the bootstrap_addr host), so it can POST # already knows the prefill host (the bootstrap_addr host), so it can POST
@@ -152,6 +154,8 @@ class CommonKVManager(BaseKVManager):
kv_status_msg_tag: Optional[bytes] = None kv_status_msg_tag: Optional[bytes] = None
kv_status_msg_carries_reason: bool = False kv_status_msg_carries_reason: bool = False
dsv41_spec_layout: Optional[dict] = None
# Used by decode when the prefill reported Failed without a reason frame. # Used by decode when the prefill reported Failed without a reason frame.
DEFAULT_PREFILL_FAILURE_REASON = ( DEFAULT_PREFILL_FAILURE_REASON = (
"Failed to get kvcache from prefill instance, it might be dead" "Failed to get kvcache from prefill instance, it might be dead"
@@ -166,6 +170,7 @@ class CommonKVManager(BaseKVManager):
): ):
self.kv_args = args self.kv_args = args
self.kv_cache_dtype_str = args.kv_cache_dtype_str self.kv_cache_dtype_str = args.kv_cache_dtype_str
self.dsv41_spec_layout = get_dsv41_spec_layout(args)
self.kv_item_lens_sum = sum(args.kv_item_lens) self.kv_item_lens_sum = sum(args.kv_item_lens)
self.state_item_lens_sum = sum(x for comp in args.state_item_lens for x in comp) self.state_item_lens_sum = sum(x for comp in args.state_item_lens for x in comp)
self.is_mla_backend = is_mla_backend self.is_mla_backend = is_mla_backend
@@ -918,6 +923,27 @@ class CommonKVManager(BaseKVManager):
f"Both servers must use the same --kv-cache-dtype value." f"Both servers must use the same --kv-cache-dtype value."
) )
local_layout = self.dsv41_spec_layout
if local_layout is not None or info.dsv41_spec_layout is not None:
if local_layout != info.dsv41_spec_layout:
mismatched_fields = sorted(
key
for key in (local_layout or {}).keys()
| (info.dsv41_spec_layout or {}).keys()
if (local_layout or {}).get(key)
!= (info.dsv41_spec_layout or {}).get(key)
)
raise RuntimeError(
"DeepSeek-V4.1 DSpark PD layout mismatch "
f"({', '.join(mismatched_fields)}): both servers must "
"enable DSpark with the same block size and target/draft KV "
"layout. Upgrade both servers together."
)
if info.attn_tp_size != self.attn_tp_size:
raise RuntimeError(
"DeepSeek-V4.1 DSpark PD requires the same TP size on both servers"
)
if self.dcp_size > 1: if self.dcp_size > 1:
if not (self.is_mla_backend or self.is_hybrid_mla_backend): if not (self.is_mla_backend or self.is_hybrid_mla_backend):
raise RuntimeError( raise RuntimeError(
@@ -1079,6 +1105,7 @@ class CommonKVManager(BaseKVManager):
"rank_port": self.rank_port, "rank_port": self.rank_port,
"page_size": self.kv_args.page_size, "page_size": self.kv_args.page_size,
"kv_cache_dtype": self.kv_cache_dtype_str, "kv_cache_dtype": self.kv_cache_dtype_str,
"dsv41_spec_layout": self.dsv41_spec_layout,
"load_balance_method": get_parallel().load_balance_method, "load_balance_method": get_parallel().load_balance_method,
"enable_dsa_cache_layer_split": get_parallel().enable_dsa_cache_layer_split, "enable_dsa_cache_layer_split": get_parallel().enable_dsa_cache_layer_split,
# Self-register the HTTP API port so the decode can derive the PD # Self-register the HTTP API port so the decode can derive the PD
@@ -1969,6 +1996,7 @@ class CommonKVBootstrapServer(BaseKVBootstrapServer):
self.dp_size = None self.dp_size = None
self.page_size = None self.page_size = None
self.kv_cache_dtype: Optional[str] = None self.kv_cache_dtype: Optional[str] = None
self.dsv41_spec_layout: Optional[dict] = None
self.follow_bootstrap_room: Optional[bool] = None self.follow_bootstrap_room: Optional[bool] = None
self.enable_dsa_cache_layer_split: Optional[bool] = None self.enable_dsa_cache_layer_split: Optional[bool] = None
self.prefill_http_port: Optional[int] = None self.prefill_http_port: Optional[int] = None
@@ -2039,6 +2067,14 @@ class CommonKVBootstrapServer(BaseKVBootstrapServer):
page_size = int(data["page_size"]) page_size = int(data["page_size"])
kv_cache_dtype = data["kv_cache_dtype"] kv_cache_dtype = data["kv_cache_dtype"]
prefill_http_port = data.get("prefill_http_port") prefill_http_port = data.get("prefill_http_port")
dsv41_spec_layout = data.get("dsv41_spec_layout")
if self._registered_count and self.dsv41_spec_layout != dsv41_spec_layout:
return web.Response(
text="DeepSeek-V4.1 DSpark PD layout differs across prefill ranks",
status=400,
)
self.dsv41_spec_layout = dsv41_spec_layout
if self.attn_tp_size is None: if self.attn_tp_size is None:
self.attn_tp_size = attn_tp_size self.attn_tp_size = attn_tp_size
@@ -2130,6 +2166,7 @@ class CommonKVBootstrapServer(BaseKVBootstrapServer):
pp_size=self.pp_size, pp_size=self.pp_size,
page_size=self.page_size, page_size=self.page_size,
kv_cache_dtype=self.kv_cache_dtype, kv_cache_dtype=self.kv_cache_dtype,
dsv41_spec_layout=self.dsv41_spec_layout,
follow_bootstrap_room=( follow_bootstrap_room=(
self.follow_bootstrap_room self.follow_bootstrap_room
if self.follow_bootstrap_room is not None if self.follow_bootstrap_room is not None
@@ -2138,7 +2175,10 @@ class CommonKVBootstrapServer(BaseKVBootstrapServer):
enable_dsa_cache_layer_split=bool(self.enable_dsa_cache_layer_split), enable_dsa_cache_layer_split=bool(self.enable_dsa_cache_layer_split),
prefill_http_port=self.prefill_http_port, prefill_http_port=self.prefill_http_port,
) )
return web.json_response(dataclasses.asdict(info), status=200) payload = dataclasses.asdict(info)
if info.dsv41_spec_layout is None:
payload.pop("dsv41_spec_layout")
return web.json_response(payload, status=200)
if not self._is_ready(): if not self._is_ready():
return web.Response( return web.Response(
@@ -87,6 +87,7 @@ from sglang.srt.mem_cache.base_prefix_cache import (
EvictParams, EvictParams,
) )
from sglang.srt.mem_cache.common import ( from sglang.srt.mem_cache.common import (
dsv41_dspark_needs_rebootstrap,
kv_to_page_indices, kv_to_page_indices,
page_align_floor, page_align_floor,
release_kv_cache, release_kv_cache,
@@ -674,6 +675,16 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
if not is_retracted and not is_rebootstrap and is_unadmitted_reject(req): if not is_retracted and not is_rebootstrap and is_unadmitted_reject(req):
self.scheduler.retire_unadmitted_request(req) self.scheduler.retire_unadmitted_request(req)
return return
if is_retracted and dsv41_dspark_needs_rebootstrap(
self.token_to_kv_pool_allocator
):
if req.output_ids:
req.pd_rebootstrap_forced_output_id = req.output_ids.pop()
req.pd_rebootstrap_in_progress = True
req.time_stats.set_retract_time()
is_retracted = False
is_rebootstrap = True
if self._check_if_req_exceed_kv_capacity(req): if self._check_if_req_exceed_kv_capacity(req):
return return
@@ -2837,6 +2848,10 @@ class SchedulerDisaggregationDecodeMixin:
# A finished request can still have one redundant forward in flight. # A finished request can still have one redundant forward in flight.
# Drain it before a prebuilt request seeds a potentially reused row. # Drain it before a prebuilt request seeds a potentially reused row.
self.schedule_stream.wait_stream(self.forward_stream) self.schedule_stream.wait_stream(self.forward_stream)
# The prebuilt batch never reaches the forward loop's prepare call.
self.ngram_embedding_manager.prepare_for_forward(
new_batch, chunked_req=self.chunked_req
)
new_batch.process_prebuilt(self.future_map) new_batch.process_prebuilt(self.future_map)
return new_batch return new_batch
+24
View File
@@ -24,6 +24,7 @@ from sglang.srt.disaggregation.base import KVPoll
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.runtime_context import ( from sglang.srt.runtime_context import (
get_disagg, get_disagg,
get_spec,
) )
from sglang.srt.utils import is_npu from sglang.srt.utils import is_npu
@@ -1674,6 +1675,29 @@ def setup_state_kv_args(
) )
def get_dsv41_spec_layout(kv_args: KVArgs) -> Optional[dict]:
"""Describe the positional transfer layout without pool capacities or pointers."""
ratios = getattr(kv_args, "mla_compression_ratios", None) or []
if 2 not in ratios or str(get_spec().speculative_algorithm).upper() != "DSPARK":
return None
from sglang.srt.disaggregation.base.conn import StateType
if kv_args.state_types.count(StateType.SWA) != 2:
raise RuntimeError(
"DeepSeek-V4.1 DSpark PD requires target and draft SWA state"
)
return {
"num_draft_tokens": get_spec().speculative_num_draft_tokens,
"compression_ratios": list(ratios),
"kv_layer_ids": list(kv_args.kv_layer_ids),
"kv_item_lens": list(kv_args.kv_item_lens),
"state_types": [state_type.value for state_type in kv_args.state_types],
"state_item_lens": [list(items) for items in kv_args.state_item_lens],
}
def prepare_abort(req: Req, error_message: str, status_code=None): def prepare_abort(req: Req, error_message: str, status_code=None):
from sglang.srt.managers.schedule_batch import FINISH_ABORT from sglang.srt.managers.schedule_batch import FINISH_ABORT
@@ -2269,6 +2269,7 @@ def _execute_server_warmup(server_args: ServerArgs):
bool(model_info.get("has_image_understanding", False)) bool(model_info.get("has_image_understanding", False))
and not get_disagg().language_only and not get_disagg().language_only
and not get_disagg().language_model_only and not get_disagg().language_model_only
and not get_exec().features.enable_encoder_swa_bounded_replay
and not is_mps() and not is_mps()
) )
if model_info["is_generation"]: if model_info["is_generation"]:
+15
View File
@@ -549,6 +549,11 @@ class Envs:
SGLANG_DSPARK_OPT_MARKOV_W2_BF16 = EnvBool(True) SGLANG_DSPARK_OPT_MARKOV_W2_BF16 = EnvBool(True)
SGLANG_DSPARK_OPT_MARKOV_W2_TP_SHARD = EnvBool(True) SGLANG_DSPARK_OPT_MARKOV_W2_TP_SHARD = EnvBool(True)
SGLANG_DSPARK_OPT_FUSED_GREEDY_MARKOV = EnvBool(False) SGLANG_DSPARK_OPT_FUSED_GREEDY_MARKOV = EnvBool(False)
# With the TP-sharded markov_w2, gather each step's vocab-parallel logits over
# the NVLink push collective (CustomAllReduceV2's multicast plane) instead of
# the NCCL ring. Only taken when the group's communicator has a multicast
# plane; off, or no such plane, keeps the NCCL all-gather.
SGLANG_DSPARK_NVLINK_VOCAB_GATHER = EnvBool(True)
SGLANG_DSPARK_ENABLE_MULTI_STREAM = EnvBool(True) SGLANG_DSPARK_ENABLE_MULTI_STREAM = EnvBool(True)
SGLANG_DSPARK_CONFIDENCE_RELAY_LAG_STEPS = EnvInt(2) SGLANG_DSPARK_CONFIDENCE_RELAY_LAG_STEPS = EnvInt(2)
@@ -1479,6 +1484,13 @@ class Envs:
# Quantize the SWA fp8 KV cache from bf16-rounded values (matches # Quantize the SWA fp8 KV cache from bf16-rounded values (matches
# trainer-side QAT and the DSA-CP path) instead of fp32 registers. # trainer-side QAT and the DSA-CP path) instead of fp32 registers.
SGLANG_DSV4_USE_BF16_KV_QUANT_SOURCE = EnvBool(False) SGLANG_DSV4_USE_BF16_KV_QUANT_SOURCE = EnvBool(False)
# Paged KV layout of the DeepSeek-V4 family pools: "v4" (584 B/token, every
# GPU), "v41" (the SM100 FlashMLA V4.1 formats: 528 B fp8 SWA cache, fp8 or
# fp4 compressed caches) or "auto" (v41 on SM100 when FlashMLA supports it).
SGLANG_DSV4_KV_LAYOUT = EnvStr("v4")
# Compressed-cache layout under "v41": "auto" (fp4 for the fp4-rounded
# ratio-1 / ratio-2 latents, fp8 for ratios 4 / 128), "fp8" or "fp4" for all.
SGLANG_DSV4_COMPRESSED_KV_LAYOUT = EnvStr("auto")
# unified_kv only: split the pool into an fp8 nope pool plus a parallel # unified_kv only: split the pool into an fp8 nope pool plus a parallel
# bf16 rope pool, 640 B/token instead of 1024. The unified pool takes no # bf16 rope pool, 640 B/token instead of 1024. The unified pool takes no
# dtype, so --kv-cache-dtype has no effect there and this switch is the # dtype, so --kv-cache-dtype has no effect there and this switch is the
@@ -1510,6 +1522,9 @@ class Envs:
SGLANG_OPT_USE_ONLINE_COMPRESS = EnvBool(False) SGLANG_OPT_USE_ONLINE_COMPRESS = EnvBool(False)
SGLANG_EXPERIMENTAL_ONLINE_C128_MTP = EnvBool(False) SGLANG_EXPERIMENTAL_ONLINE_C128_MTP = EnvBool(False)
SGLANG_DSV4_COMPRESS_STATE_DTYPE = EnvStr("float32") SGLANG_DSV4_COMPRESS_STATE_DTYPE = EnvStr("float32")
# Run the DeepSeek-V4.1 ratio-1/2 prefill indexer on the torch path instead
# of the DeepGEMM dense fp4 logits kernel (test oracle / fallback).
SGLANG_DSV41_TORCH_PREFILL_INDEXER = EnvBool(False)
SGLANG_FP8_PAGED_MQA_LOGITS_TORCH = EnvBool(False) SGLANG_FP8_PAGED_MQA_LOGITS_TORCH = EnvBool(False)
SGLANG_OPT_FLASHMLA_SPARSE_PREFILL = EnvBool(True) SGLANG_OPT_FLASHMLA_SPARSE_PREFILL = EnvBool(True)
@@ -21,6 +21,7 @@ from typing import List, Optional, Tuple
import torch import torch
import torch_npu import torch_npu
from sglang.kernels.ops.attention.dsv4.kv_layout import KVLayout
from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE
from sglang.srt.hardware_backend.npu.utils import is_npu_arch35 from sglang.srt.hardware_backend.npu.utils import is_npu_arch35
from sglang.srt.mem_cache.deepseek_v4_compress_state import CompressStatePool from sglang.srt.mem_cache.deepseek_v4_compress_state import CompressStatePool
@@ -291,6 +292,7 @@ class DSV4NPUTokenToKVPool(DeepSeekV4TokenToKVPool):
enable_memory_saver: bool, enable_memory_saver: bool,
global_page_size: int, global_page_size: int,
cls: type = DeepSeekV4SingleKVPool, cls: type = DeepSeekV4SingleKVPool,
kv_layout: KVLayout = KVLayout.V4,
) -> NPUDeepSeekV4SingleKVPool: ) -> NPUDeepSeekV4SingleKVPool:
# NPU does not use the HiSparse c4 device pool; fail loud if someone # NPU does not use the HiSparse c4 device pool; fail loud if someone
# enables it so the silent layout mismatch surfaces at init. # enables it so the silent layout mismatch surfaces at init.
@@ -298,6 +300,8 @@ class DSV4NPUTokenToKVPool(DeepSeekV4TokenToKVPool):
"enable_hisparse is not supported on the NPU DSV4 KV pool " "enable_hisparse is not supported on the NPU DSV4 KV pool "
f"(got c4 pool class {cls.__name__})." f"(got c4 pool class {cls.__name__})."
) )
# The V4.1 fp8 / fp4 page layouts are CUDA FlashMLA formats.
assert kv_layout is KVLayout.V4, f"NPU pools do not support {kv_layout}"
# Full/SWA use the global page size, C4 uses its native compressed page, # Full/SWA use the global page size, C4 uses its native compressed page,
# and C128 has an independent physical page size. # and C128 has an independent physical page size.
is_c4_pool = page_size * 4 == global_page_size is_c4_pool = page_size * 4 == global_page_size
@@ -382,6 +386,9 @@ class DSV4NPUTokenToKVPool(DeepSeekV4TokenToKVPool):
def get_contiguous_buf_infos(self) -> Tuple[List[int], List[int], List[int]]: def get_contiguous_buf_infos(self) -> Tuple[List[int], List[int], List[int]]:
"""Main PD buffers addressed by the full KV page id.""" """Main PD buffers addressed by the full KV page id."""
if self.c4_kv_pool is None:
# A draft pool whose layers are all uncompressed has no c4 buffers.
return [], [], []
indexer_pool = self._indexer_pool(4) indexer_pool = self._indexer_pool(4)
buffers = ( buffers = (
self.c4_kv_pool.kv_buffer self.c4_kv_pool.kv_buffer
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ from sglang.kernels.ops.attention.dsv4 import (
compress_forward, compress_forward,
compress_norm_rope_store, compress_norm_rope_store,
) )
from sglang.kernels.ops.attention.dsv4.kv_layout import KVLayout
from sglang.srt.environ import envs from sglang.srt.environ import envs
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -158,6 +159,7 @@ class CompressorBackendMixin:
bf16_store: bool = False, bf16_store: bool = False,
kv_scale_cache: Optional[torch.Tensor] = None, kv_scale_cache: Optional[torch.Tensor] = None,
rope_cache: Optional[tuple[torch.Tensor, torch.Tensor]] = None, rope_cache: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
kv_layout: KVLayout = KVLayout.V4,
fp8_2buff: bool = False, fp8_2buff: bool = False,
kv_cache_rope: Optional[torch.Tensor] = None, kv_cache_rope: Optional[torch.Tensor] = None,
) -> None: ) -> None:
@@ -215,6 +217,7 @@ class CompressorBackendMixin:
bf16_store=bf16_store, bf16_store=bf16_store,
kvcache_scale=kv_scale_cache, kvcache_scale=kv_scale_cache,
rope_cache=rope_cache, rope_cache=rope_cache,
layout=kv_layout,
# Derived once per forward by the backend; every C4 layer writes the # Derived once per forward by the backend; every C4 layer writes the
# same rows to the same slots. # same rows to the same slots.
fp4_k_write_metadata=( fp4_k_write_metadata=(
@@ -268,6 +271,7 @@ class CompressorBackendMixin:
) )
use_hip_fp4 = _is_hip and use_fp4_indexer use_hip_fp4 = _is_hip and use_fp4_indexer
bf16_store = False bf16_store = False
kv_layout = KVLayout.V4
kv_scale_cache = None kv_scale_cache = None
fp8_2buff = False fp8_2buff = False
kv_cache_rope = None kv_cache_rope = None
@@ -295,6 +299,8 @@ class CompressorBackendMixin:
assert compress_kv_pool is not None assert compress_kv_pool is not None
kv_cache = token_to_kv_pool.get_extra_key_buffer(layer_id) kv_cache = token_to_kv_pool.get_extra_key_buffer(layer_id)
page_size = token_to_kv_pool.get_extra_key_page_size(layer_id) page_size = token_to_kv_pool.get_extra_key_page_size(layer_id)
# The pool's page format (V4, or the V4.1 fp8 / fp4 layouts).
kv_layout = token_to_kv_pool.get_extra_key_layout(layer_id)
if hasattr(compress_kv_pool, "translate_loc_to_hisparse_device"): if hasattr(compress_kv_pool, "translate_loc_to_hisparse_device"):
out_loc = compress_kv_pool._translate_loc_to_hisparse_device(out_loc) out_loc = compress_kv_pool._translate_loc_to_hisparse_device(out_loc)
self._forward_compress_all_in_one( self._forward_compress_all_in_one(
@@ -316,6 +322,7 @@ class CompressorBackendMixin:
rope_cache=( rope_cache=(
(compressor.fp4_cos, compressor.fp4_sin) if use_hip_fp4 else None (compressor.fp4_cos, compressor.fp4_sin) if use_hip_fp4 else None
), ),
kv_layout=kv_layout,
fp8_2buff=fp8_2buff, fp8_2buff=fp8_2buff,
kv_cache_rope=( kv_cache_rope=(
None if kv_cache_rope is None else kv_cache_rope.view(dtype=torch.uint8) None if kv_cache_rope is None else kv_cache_rope.view(dtype=torch.uint8)
@@ -0,0 +1,266 @@
"""DeepSeek V4.1 ratio-1/2 compressors and indexers.
Only kv_source layers own compressed latents; later layers of the same ratio
share that storage.
"""
from __future__ import annotations
from typing import Optional, Tuple
import torch
from torch import nn
from sglang.kernels.ops.attention.dsv4 import linear_bf16_fp32
from sglang.kernels.ops.attention.dsv4.torch_quant import (
fake_quant_compressed_kv,
fake_quant_fp4,
)
from sglang.kernels.ops.layernorm.rmsnorm_fp32 import rmsnorm_fp32
from sglang.srt.layers.linear import ReplicatedLinear
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.utils import add_prefix
def _rope_fq4(x, freqs, rope_dim, *, compressed_kv=False):
if x.is_cuda and torch.version.cuda is not None and x.dtype == torch.bfloat16:
from sglang.kernels.ops.attention.dsv4.fp4_rope_fake_quant import (
rope_tail_fake_quant_fp4,
)
return rope_tail_fake_quant_fp4(x, freqs, rope_dim, compressed_kv=compressed_kv)
quant = fake_quant_compressed_kv if compressed_kv else fake_quant_fp4
return quant(rope_tail(x, freqs, rope_dim))
class RMSNorm(nn.Module):
"""fp32 statistics and fp32 weight multiply, cast back at the very end."""
def __init__(self, dim: int, eps: float):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x: torch.Tensor) -> torch.Tensor:
if (
x.is_cuda
and torch.version.cuda is not None
and x.dtype in (torch.bfloat16, torch.float32)
and self.weight.dtype in (torch.bfloat16, torch.float32)
and x.shape[-1] in (128, 512)
and x.is_contiguous()
and self.weight.is_contiguous()
):
return rmsnorm_fp32(x, self.weight, self.eps)
dtype = x.dtype
x = x.float()
x = x * torch.rsqrt(x.square().mean(-1, keepdim=True) + self.eps)
return (self.weight * x).to(dtype)
def token_req_indices(forward_batch, *, num_tokens=None) -> torch.Tensor:
req = forward_batch.req_pool_indices.to(torch.int64)
if forward_batch.forward_mode.is_decode():
return req
if forward_batch.forward_mode.is_target_verify():
return torch.repeat_interleave(
req, int(forward_batch.spec_info.draft_token_num), output_size=num_tokens
)
assert forward_batch.forward_mode.is_extend(), (
"the V4.1 torch attention path serves extend, target-verify and decode"
)
return torch.repeat_interleave(
req, forward_batch.extend_seq_lens.to(torch.int64), output_size=num_tokens
)
def rope_tail(
x: torch.Tensor, freqs: torch.Tensor, rope_dim: int, inverse: bool = False
) -> torch.Tensor:
"""Rotate the last rope_dim features of x with complex freqs [T, rope_dim // 2]."""
head, tail = x[..., :-rope_dim], x[..., -rope_dim:]
tc = torch.view_as_complex(tail.float().unflatten(-1, (-1, 2)).contiguous())
f = freqs.conj() if inverse else freqs
f = f.view(x.shape[0], *([1] * (x.ndim - 2)), rope_dim // 2)
rotated = torch.view_as_real(tc * f).flatten(-2).to(x.dtype)
return torch.cat([head, rotated], dim=-1)
def fused_low_ratio_compress_supported() -> bool:
"""The fused c1 / c2 / index-K decode kernels pack fp4 with
`cvt.rn.satfinite.e2m1x2`, an sm100+ instruction; the answer also fixes the
ratio-2 weight layout (`wkv_gate`, or `wkv` plus `wgate`)."""
if not torch.cuda.is_available() or torch.version.hip is not None:
return False
return torch.cuda.get_device_capability()[0] >= 10
class DeepseekV41Compressor(nn.Module):
"""Pool consecutive tokens into one pre-RoPE KV latent; bf16 weights, fp32
projection and softmax pooling, rounded back to bf16 in `finish`."""
def __init__(
self,
hidden_size: int,
head_dim: int,
compress_ratio: int,
eps: float,
*,
fused_compress: Optional[bool] = None,
):
super().__init__()
self.compress_ratio = compress_ratio
self.norm = RMSNorm(head_dim, eps)
# The loader concatenates ratio-2 wkv/wgate when it finds wkv_gate.weight;
# ratio 1 must retain wkv.weight because there is no gate half to load.
self.use_fused_compress = (
fused_low_ratio_compress_supported()
if fused_compress is None
else bool(fused_compress)
)
self.use_fused_gate = compress_ratio > 1 and self.use_fused_compress
if self.use_fused_gate:
self.wkv_gate = nn.Linear(
hidden_size, 2 * head_dim, bias=False, dtype=torch.bfloat16
)
else:
self.wkv = nn.Linear(
hidden_size, head_dim, bias=False, dtype=torch.bfloat16
)
if compress_ratio > 1:
self.wgate = nn.Linear(
hidden_size, head_dim, bias=False, dtype=torch.bfloat16
)
def project(self, x: torch.Tensor) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
if self.compress_ratio == 1:
return self.wkv(x), None
if self.use_fused_gate:
fused = self.project_fused(x)
head_dim = fused.shape[-1] // 2
return fused[..., :head_dim], fused[..., head_dim:]
# Two GEMMs, not one fused [2D, K] projection: c2_decode_pool reads kv and
# score as contiguous [n, D] rows; column slices of a fused output are not.
kv = linear_bf16_fp32(x, self.wkv.weight)
score = linear_bf16_fp32(x, self.wgate.weight)
return kv, score
def project_fused(self, x: torch.Tensor) -> torch.Tensor:
"""`[n, 2D]` fp32, `| kv | score |`."""
return linear_bf16_fp32(x, self.wkv_gate.weight)
def finish(self, kv: torch.Tensor) -> torch.Tensor:
return self.norm(kv.to(torch.bfloat16))
@staticmethod
def pool_pairs(kv2: torch.Tensor, score2: torch.Tensor) -> torch.Tensor:
"""kv2, score2 [n, 2, D] fp32 -> [n, D]"""
return (kv2 * score2.softmax(dim=1)).sum(dim=1)
def _small_weights_proj_max_m(n_heads: int, hidden_size: int) -> int:
# -1 means the device or checkpoint shape requires the linear fallback.
if not torch.cuda.is_available() or torch.version.hip is not None:
return -1
from sglang.kernels.ops.gemm.small_gemm_bf16 import MAX_M, can_use_n32k5120_gemm
return MAX_M if can_use_n32k5120_gemm(n_heads, hidden_size, 1) else -1
class DeepseekV41Indexer(nn.Module):
"""Scores compressed positions with a small fp4 side attention; only a
kv_source layer owns index keys. Projections are replicated across TP: every
rank scores with all heads, so the top-k needs no cross-rank reduction."""
def __init__(
self,
config,
layer_id: int,
head_dim: int,
quant_config: Optional[QuantizationConfig],
prefix: str,
):
super().__init__()
self.n_heads = config.index_n_heads
self.n_local_heads = self.n_heads
self.index_head_dim = config.index_head_dim
self.rope_head_dim = config.qk_rope_head_dim
self.index_topk = config.index_topk
self.owns_k = layer_id in config.kv_source_layer_ids
self.is_candidate_source = layer_id == config.candidate_source_layer_id
self.uses_candidates = 0 <= config.candidate_source_layer_id < layer_id
self.candidate_topk_blocks = config.candidate_topk_blocks
self.candidate_block_size = config.candidate_block_size
self.softmax_scale = self.index_head_dim**-0.5
self.wq_b = ReplicatedLinear(
config.q_lora_rank,
self.n_heads * self.index_head_dim,
bias=False,
quant_config=quant_config,
params_dtype=torch.bfloat16,
prefix=add_prefix("wq_b", prefix),
)
self.weights_proj = ReplicatedLinear(
config.hidden_size,
self.n_heads,
bias=False,
params_dtype=torch.bfloat16,
quant_config=None,
prefix=add_prefix("weights_proj", prefix),
)
# The decode GEMM matches tiny_gemm's reduction order, not cuBLAS's.
self.weights_proj_small_max_m = _small_weights_proj_max_m(
self.n_heads, config.hidden_size
)
if self.owns_k:
self.wk = nn.Linear(
head_dim, self.index_head_dim, bias=False, dtype=torch.bfloat16
)
self.k_norm = RMSNorm(self.index_head_dim, config.rms_norm_eps)
def forward_wk(self, latent: torch.Tensor) -> torch.Tensor:
from sglang.kernels.ops.gemm.small_gemm_bf16 import (
can_use_n128k512_gemm,
n128k512_gemm_bf16,
)
# The JIT kernel raises rather than falling back on an unsupported shape.
if can_use_n128k512_gemm(
self.index_head_dim, latent.shape[-1], latent.shape[0]
):
return n128k512_gemm_bf16(latent, self.wk.weight)
return self.wk(latent)
def index_keys(self, latent: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
"""Pre-RoPE latents [n, D] -> fp4-rounded index keys [n, index_head_dim]."""
k = self.k_norm(self.forward_wk(latent))
return _rope_fq4(k, freqs, self.rope_head_dim)
def queries(self, q_lora: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
q, _ = self.wq_b(q_lora)
q = q.view(q.shape[0], self.n_local_heads, self.index_head_dim)
return _rope_fq4(q, freqs, self.rope_head_dim)
def head_weights_raw(self, x: torch.Tensor) -> torch.Tensor:
"""`weights_proj(x)` before the scale, [tokens, n_heads] bf16."""
if 0 < x.shape[0] <= self.weights_proj_small_max_m and x.is_cuda:
from sglang.kernels.ops.gemm.small_gemm_bf16 import n32k5120_gemm_bf16
return n32k5120_gemm_bf16(x, self.weights_proj.weight)
w, _ = self.weights_proj(x)
return w
@property
def head_weight_scale(self) -> float:
return self.softmax_scale * self.n_heads**-0.5
def head_weights(self, x: torch.Tensor) -> torch.Tensor:
return self.head_weights_raw(x) * self.head_weight_scale
def scores(
self, q: torch.Tensor, k: torch.Tensor, weights: torch.Tensor
) -> torch.Tensor:
"""q [t, H, d], k [n, d], weights [t, H] -> [t, n], summed over all heads."""
s = torch.einsum("bhd,nd->bhn", q, k)
s = (s.relu() * weights.unsqueeze(-1)).sum(dim=1)
return s.float()
@@ -116,6 +116,11 @@ class PagedIndexerMetadata:
use_topk_v2: bool use_topk_v2: bool
force_deep_gemm_metadata: bool = False force_deep_gemm_metadata: bool = False
use_prefill_cuda_graph: bool = False use_prefill_cuda_graph: bool = False
# Indexer source compression ratio: 4 for c4, 1 or 2 for the dsv41 sources.
compress_ratio: int = 4
# Rows per logits chunk for the prefill CUDA graph low-ratio indexer; 0 plans
# all rows at once.
row_chunk: int = 0
deep_gemm_metadata: Any = field(init=False, repr=False) deep_gemm_metadata: Any = field(init=False, repr=False)
topk_metadata: torch.Tensor = field(init=False, repr=False) topk_metadata: torch.Tensor = field(init=False, repr=False)
nonpaged_plan: Optional[NonPagedIndexerPlan] = field( nonpaged_plan: Optional[NonPagedIndexerPlan] = field(
@@ -144,7 +149,18 @@ class PagedIndexerMetadata:
compressed_seq_lens = self.compressed_seq_lens.to(torch.int32) compressed_seq_lens = self.compressed_seq_lens.to(torch.int32)
if compressed_seq_lens.dim() == 1: if compressed_seq_lens.dim() == 1:
compressed_seq_lens = compressed_seq_lens.unsqueeze(-1) compressed_seq_lens = compressed_seq_lens.unsqueeze(-1)
if _IS_SM120 and compressed_seq_lens.shape[0] > _SM120_INDEXER_M_CHUNK: if self.row_chunk > 0:
self.deep_gemm_metadata = torch.stack(
[
get_paged_mqa_logits_metadata(
compressed_seq_lens[_s : _s + self.row_chunk],
self.compressed_page_size,
deep_gemm.get_num_sms(),
)
for _s in range(0, compressed_seq_lens.shape[0], self.row_chunk)
]
)
elif _IS_SM120 and compressed_seq_lens.shape[0] > _SM120_INDEXER_M_CHUNK:
# Chunk metadata is shared by all indexer layers in this forward. # Chunk metadata is shared by all indexer layers in this forward.
self.deep_gemm_metadata = [ self.deep_gemm_metadata = [
get_paged_mqa_logits_metadata( get_paged_mqa_logits_metadata(
@@ -173,6 +189,9 @@ class PagedIndexerMetadata:
self.topk_metadata = torch.empty((0,)) self.topk_metadata = torch.empty((0,))
assert self.page_size == 256, "the system hardcodes page_size=256" assert self.page_size == 256, "the system hardcodes page_size=256"
assert self.page_size % self.compress_ratio == 0, (
f"compress_ratio {self.compress_ratio} must divide page_size {self.page_size}"
)
@property @property
def max_seq_len(self) -> int: def max_seq_len(self) -> int:
@@ -182,6 +201,17 @@ class PagedIndexerMetadata:
def max_compressed_seq_len(self) -> int: def max_compressed_seq_len(self) -> int:
return self.page_table.shape[1] * self.compressed_page_size return self.page_table.shape[1] * self.compressed_page_size
def row_chunks(self):
num_rows = self.compressed_seq_lens.shape[0]
if self.row_chunk <= 0:
return [(slice(0, num_rows), self.deep_gemm_metadata)]
return [
(slice(start, min(start + self.row_chunk, num_rows)), plan)
for start, plan in zip(
range(0, num_rows, self.row_chunk), self.deep_gemm_metadata
)
]
def copy_(self, other: PagedIndexerMetadata): def copy_(self, other: PagedIndexerMetadata):
if is_hip(): if is_hip():
copy_fields = ["page_table", "compressed_seq_lens"] copy_fields = ["page_table", "compressed_seq_lens"]
@@ -196,6 +226,8 @@ class PagedIndexerMetadata:
check_eq_fields=[ check_eq_fields=[
"page_size", "page_size",
"compressed_page_size", "compressed_page_size",
"compress_ratio",
"row_chunk",
"force_deep_gemm_metadata", "force_deep_gemm_metadata",
"use_prefill_cuda_graph", "use_prefill_cuda_graph",
"use_topk_v2", "use_topk_v2",
@@ -57,3 +57,90 @@ def create_attention_graph_variants(hf_config) -> Optional[AttentionGraphVariant
) )
return DsaGraphVariants(index_topk) return DsaGraphVariants(index_topk)
return None return None
DSV41_CANDIDATE_FILTERED = "candidate_filtered"
@dataclass(frozen=True)
class Dsv41CandidateGraphVariants:
"""Candidate-indexer graphs keyed by the batch's longest request; a variant
below its limit skips low-ratio scoring or candidate filtering."""
# (label, max_seq_len it serves), ascending; the last label is the fallback.
graph_limits: tuple[tuple[str, int], ...]
capture_labels: tuple[str, ...]
verify_extra_tokens: int = 0
def select(self, forward_batch: ForwardBatch) -> str:
lengths = getattr(forward_batch, "seq_lens_cpu", None)
max_seq_len = None
if lengths is not None and lengths.device.type == "cpu" and lengths.numel() > 0:
max_seq_len = int(lengths.max())
if max_seq_len is None and self.verify_extra_tokens:
# Includes acceptance still in flight, without a GPU-to-CPU copy.
max_seq_len = getattr(
getattr(forward_batch, "spec_info", None),
"candidate_max_seq_len_upper_bound",
None,
)
if max_seq_len is not None:
max_seq_len += self.verify_extra_tokens
for variant, limit in self.graph_limits:
if max_seq_len <= limit:
return variant
return DSV41_CANDIDATE_FILTERED
def create_dsv41_candidate_graph_variants(
model_runner, capture_forward_mode, captured_req_width: int = 0
) -> Optional[Dsv41CandidateGraphVariants]:
import torch
from sglang.srt.model_executor.forward_batch_info import ForwardMode
from sglang.srt.utils import is_hip
text_config = model_runner.model_config.hf_text_config
dspark_target_verify = (
capture_forward_mode == ForwardMode.TARGET_VERIFY
and model_runner.spec_algorithm.is_dspark()
and not model_runner.is_draft_worker
and captured_req_width > 0
)
if not (
(capture_forward_mode == ForwardMode.DECODE or dspark_target_verify)
and model_runner.device == "cuda"
and not is_hip()
and torch.cuda.get_device_capability(model_runner.gpu_id)[0] >= 10
and getattr(text_config, "model_type", None) == "deepseek_v41"
and getattr(text_config, "candidate_source_layer_id", -1) >= 0
):
return None
span = text_config.candidate_topk_blocks * text_config.candidate_block_size
if span <= 0:
return None
ratios = set(text_config.compress_ratios) & {1, 2}
topk = text_config.index_topk
variants = []
# Verify needs per-query causal top-k, so it always keeps candidate filtering.
if topk > 0 and ratios and not dspark_target_verify:
variants.append(("candidate_all", topk * min(ratios)))
if ratios == {1, 2}:
variants.append(("candidate_c2_all", topk * 2))
variants.append(("candidate_unfiltered", span))
graph_limits = []
for variant, limit in variants:
graph_limits.append((variant, min(limit, span)))
if limit >= span:
break
logger.info(
"Candidate indexer graph limits: %s; use full filtering above %s.",
graph_limits,
span,
)
return Dsv41CandidateGraphVariants(
graph_limits=tuple(graph_limits),
capture_labels=tuple(v for v, _ in graph_limits) + (DSV41_CANDIDATE_FILTERED,),
# The verify backend adds this width to committed CPU lengths.
verify_extra_tokens=captured_req_width if dspark_target_verify else 0,
)
+18
View File
@@ -55,6 +55,9 @@ class InterleaveContextParallelMetadata(BaseContextParallelMetadata):
per_rank_actual_token: Optional[List[int]] = None per_rank_actual_token: Optional[List[int]] = None
max_rank_len: Optional[List[int]] = None max_rank_len: Optional[List[int]] = None
per_rank_logical_token: Optional[List[int]] = None per_rank_logical_token: Optional[List[int]] = None
# Tail row -> packed all-gather slot; local tail metadata rows include padding.
gather_index: Optional[torch.Tensor] = None
local_index: Optional[torch.Tensor] = None
class InterleaveCPStrategy(ContextParallelStrategy): class InterleaveCPStrategy(ContextParallelStrategy):
@@ -213,6 +216,9 @@ class InterleaveCPStrategy(ContextParallelStrategy):
gathered = x.new_empty((self.cp_size * physical_rank_len, *x.shape[1:])) gathered = x.new_empty((self.cp_size * physical_rank_len, *x.shape[1:]))
attn_cp_all_gather_into_tensor(gathered, padded_x.contiguous()) attn_cp_all_gather_into_tensor(gathered, padded_x.contiguous())
if metadata.gather_index is not None:
return gathered.index_select(0, metadata.gather_index)
# Equal per-rank lengths: one interleave copy restores the original # Equal per-rank lengths: one interleave copy restores the original
# token order; cheaper than the index_select fallback below. # token order; cheaper than the index_select fallback below.
actual = metadata.per_rank_actual_token actual = metadata.per_rank_actual_token
@@ -290,3 +296,15 @@ class InterleaveCPStrategy(ContextParallelStrategy):
k_nope = full_latent[..., :kv_lora_rank].unsqueeze(1) k_nope = full_latent[..., :kv_lora_rank].unsqueeze(1)
k_rope = full_latent[..., kv_lora_rank:].unsqueeze(1) k_rope = full_latent[..., kv_lora_rank:].unsqueeze(1)
return k_nope, k_rope return k_nope, k_rope
def interleave_rows_per_request(
extend_lens: List[int], cp_rank: int, cp_size: int
) -> List[int]:
"""Rows of each request a CP rank holds: global token index congruent to cp_rank."""
counts, start = [], 0
for n in extend_lens:
end = start + n
counts.append((end - 1 - cp_rank) // cp_size - (start - 1 - cp_rank) // cp_size)
start = end
return counts
@@ -939,6 +939,15 @@ def can_use_flashinfer_allreduce(
# Dynamo, so statically-off configs must short-circuit before reaching them # Dynamo, so statically-off configs must short-circuit before reaching them
# (same ordering rule as apply_flashinfer_allreduce_fusion). # (same ordering rule as apply_flashinfer_allreduce_fusion).
token_num, hidden_dim = input_.shape token_num, hidden_dim = input_.shape
# MNNVL hard-fails instead of falling back when the width is not float4-aligned
# (FlashInfer csrc/trtllm_mnnvl_allreduce.cu).
if (
workspace_manager.backend == "mnnvl"
and hidden_dim % (16 // input_.element_size()) != 0
):
return False
if torch.compiler.is_compiling(): if torch.compiler.is_compiling():
# Don't call into the flashinfer workspace object while tracing. The # Don't call into the flashinfer workspace object while tracing. The
# workspace was allocated for (max_token_num, hidden_dim, dtype) and # workspace was allocated for (max_token_num, hidden_dim, dtype) and
@@ -209,6 +209,9 @@ class LogitsProcessorOutput:
# The last hidden layers # The last hidden layers
hidden_states: Optional[torch.Tensor] = None hidden_states: Optional[torch.Tensor] = None
# Original flattened token indices when only a subset of hidden rows is captured.
hidden_states_token_indices: Optional[torch.Tensor] = None
## Part 2: This part will be assigned in python/sglang/srt/layers/sampler.py::Sampler ## Part 2: This part will be assigned in python/sglang/srt/layers/sampler.py::Sampler
# he log probs of output tokens, if SGLANG_RETURN_ORIGINAL_LOGPROB = True, will get the log probs before applying temperature. If False, will get the log probs before applying temperature. # he log probs of output tokens, if SGLANG_RETURN_ORIGINAL_LOGPROB = True, will get the log probs before applying temperature. If False, will get the log probs before applying temperature.
next_token_logprobs: Optional[torch.Tensor] = None next_token_logprobs: Optional[torch.Tensor] = None
@@ -1505,7 +1505,7 @@ class FusedMoE(torch.nn.Module):
self, self,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
topk_output: TopKOutput, topk_output: TopKOutput,
pre_quant_input: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, pre_quant_input: Optional[Tuple] = None,
): ):
if self._use_ascend_fuseep: if self._use_ascend_fuseep:
from sglang.srt.hardware_backend.npu.moe.fuseep import forward_fuseep from sglang.srt.hardware_backend.npu.moe.fuseep import forward_fuseep
@@ -1546,7 +1546,7 @@ class FusedMoE(torch.nn.Module):
self, self,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
topk_output: TopKOutput, topk_output: TopKOutput,
pre_quant_input: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, pre_quant_input: Optional[Tuple] = None,
): ):
origin_hidden_states_dim = hidden_states.shape[-1] origin_hidden_states_dim = hidden_states.shape[-1]
assert self.quant_method is not None assert self.quant_method is not None
@@ -1555,20 +1555,8 @@ class FusedMoE(torch.nn.Module):
dwdp_mgr = get_global_dwdp_manager() dwdp_mgr = get_global_dwdp_manager()
dwdp_mgr.wait_prefetch(self.layer_id) dwdp_mgr.wait_prefetch(self.layer_id)
dispatch_output = self.dispatcher.dispatch( dispatch_output = self._dispatch_with_pre_quant(
hidden_states=hidden_states, topk_output=topk_output hidden_states, topk_output, pre_quant_input
)
if (
pre_quant_input is not None
and dispatch_output.format.is_standard()
and dispatch_output.hidden_states_scale is None
):
# SGLANG_OPT_MOE_QUANT_ONCE: the standard dispatch was a pure
# passthrough, so the caller's pre-quantized (q, scale) pair still
# matches dispatch_output.hidden_states; attach it for the triton
# fused runner to skip its own activation quant.
dispatch_output = dispatch_output._replace(
hidden_states_pre_quant=pre_quant_input
) )
combine_input = self.run_moe_core( combine_input = self.run_moe_core(
@@ -1593,16 +1581,40 @@ class FusedMoE(torch.nn.Module):
return final_hidden_states return final_hidden_states
def _dispatch_with_pre_quant(
self,
hidden_states: torch.Tensor,
topk_output: TopKOutput,
pre_quant_input: Optional[Tuple],
) -> DispatchOutput:
dispatch_output = self.dispatcher.dispatch(
hidden_states=hidden_states, topk_output=topk_output
)
if (
pre_quant_input is not None
and dispatch_output.format.is_standard()
and dispatch_output.hidden_states_scale is None
):
# Dropping an Mxfp8RoutedInputPreQuant here would leave its side
# stream unjoined under CUDA-graph capture.
dispatch_output = dispatch_output._replace(
hidden_states_pre_quant=pre_quant_input
)
return dispatch_output
def forward_deferred_finalize( def forward_deferred_finalize(
self, hidden_states: torch.Tensor, topk_output: TopKOutput self,
hidden_states: torch.Tensor,
topk_output: TopKOutput,
pre_quant_input: Optional[Tuple] = None,
): ):
assert self.quant_method is not None assert self.quant_method is not None
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import ( from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
flashinfer_trtllm_deferred_finalize_context, flashinfer_trtllm_deferred_finalize_context,
) )
dispatch_output = self.dispatcher.dispatch( dispatch_output = self._dispatch_with_pre_quant(
hidden_states=hidden_states, topk_output=topk_output hidden_states, topk_output, pre_quant_input
) )
with flashinfer_trtllm_deferred_finalize_context(): with flashinfer_trtllm_deferred_finalize_context():
@@ -0,0 +1,48 @@
"""Scoped handoff of a decoder's HC post operands to deferred MoE finalize."""
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
from typing import Callable, Optional
import torch
@dataclass
class MhcPostFusion:
residual: torch.Tensor
post: Optional[torch.Tensor]
comb: Optional[torch.Tensor]
stats_stream: Optional[torch.cuda.Stream]
output: Optional[torch.Tensor] = None
pre: Optional[torch.Tensor] = None
norm_weight: Optional[torch.Tensor] = None
norm_eps: float = 0.0
normalized: Optional[torch.Tensor] = None
quantized: Optional[tuple[torch.Tensor, torch.Tensor]] = None
record_stats: Optional[
Callable[[], tuple[torch.Tensor, torch.Tensor, torch.Tensor]]
] = None
def materialize_stats(self):
# Record after the main parent; graph replay must keep the join on the
# main stream.
if self.record_stats is not None:
self.pre, self.post, self.comb = self.record_stats()
self.record_stats = None
_current: ContextVar[Optional[MhcPostFusion]] = ContextVar("moe_mhc_post", default=None)
def current_mhc_post_fusion():
return _current.get()
@contextmanager
def use_mhc_post_fusion(state):
token = _current.set(state)
try:
yield
finally:
_current.reset(token)
@@ -75,6 +75,10 @@ def flashinfer_trtllm_deferred_finalize_context(
_deferred_finalize_enabled.reset(token) _deferred_finalize_enabled.reset(token)
def is_deferred_finalize_enabled() -> bool:
return _deferred_finalize_enabled.get()
def finalize_flashinfer_trtllm_deferred_output( def finalize_flashinfer_trtllm_deferred_output(
deferred_output: FlashInferTrtllmDeferredFinalizeOutput, deferred_output: FlashInferTrtllmDeferredFinalizeOutput,
shared_output: torch.Tensor, shared_output: torch.Tensor,
@@ -68,11 +68,10 @@ class StandardDispatchOutput(NamedTuple):
hidden_states: torch.Tensor hidden_states: torch.Tensor
hidden_states_scale: Optional[torch.Tensor] hidden_states_scale: Optional[torch.Tensor]
topk_output: TopKOutput topk_output: TopKOutput
# SGLANG_OPT_MOE_QUANT_ONCE: optional pre-quantized (q, scale) pair for # Pre-quantized activation for ``hidden_states``, which itself stays bf16:
# ``hidden_states`` (per-token-group-128 fp8, q rows possibly padded to a # either a (q, scale) pair (per-token-group-128 fp8, q rows padded to a
# multiple of 4). Consumed by the standard->triton fused runner so it can # multiple of 4) or an ``Mxfp8RoutedInputPreQuant``.
# skip its own activation quant; ``hidden_states`` itself stays bf16. hidden_states_pre_quant: Optional[Tuple] = None
hidden_states_pre_quant: Optional[Tuple[torch.Tensor, torch.Tensor]] = None
@property @property
def format(self) -> DispatchOutputFormat: def format(self) -> DispatchOutputFormat:
+96 -32
View File
@@ -26,6 +26,7 @@ from typing import (
Protocol, Protocol,
Tuple, Tuple,
TypeGuard, TypeGuard,
Union,
runtime_checkable, runtime_checkable,
) )
@@ -235,6 +236,10 @@ class TopKConfig:
fused_shared_experts_scaling_factor: Optional[float] = None fused_shared_experts_scaling_factor: Optional[float] = None
output_format: Optional[TopKOutputFormat] = None output_format: Optional[TopKOutputFormat] = None
scoring_func: str = "softmax" scoring_func: str = "softmax"
# sqrtsoftplus through log1p with NaNs ranked first (DeepSeek-V4.1 routing).
sqrtsoftplus_log1p: bool = False
# Let the fused router also emit FlashInfer routed-MoE packed ids.
fused_gate_packed_ids: bool = False
# Draft-side MoE blocks set this False so they never write the target's # Draft-side MoE blocks set this False so they never write the target's
# process-global routed-experts capture buffer. # process-global routed-experts capture buffer.
allow_routed_experts_capture: bool = True allow_routed_experts_capture: bool = True
@@ -271,15 +276,10 @@ class TopKConfig:
class TopKOutputChecker: class TopKOutputChecker:
@staticmethod @staticmethod
def format_is_standard(topk_output: TopKOutput) -> TypeGuard[StandardTopKOutput]: def format_is_standard(
# ===== TO BE REFACTORED ==== topk_output: TopKOutput,
# The experimental fused topk+pack carrier only exists under the master switch. ) -> TypeGuard[Union[StandardTopKOutput, StandardTopKOutputPacked]]:
if _SGLANG_EXPERIMENTAL_LORA_OPTI: return isinstance(topk_output, (StandardTopKOutput, StandardTopKOutputPacked))
return isinstance(
topk_output, (StandardTopKOutput, StandardTopKOutputPacked)
)
# ===== END TO BE REFACTORED ====
return isinstance(topk_output, StandardTopKOutput)
@staticmethod @staticmethod
def format_is_triton_kernels( def format_is_triton_kernels(
@@ -325,11 +325,8 @@ class StandardTopKOutput(NamedTuple):
return TopKOutputFormat.STANDARD return TopKOutputFormat.STANDARD
# ===== TO BE REFACTORED ==== # Standard top-k output plus the FlashInfer routed-MoE packed ids that
# Experimental fused topk+pack (SGLANG_OPT_LORA_FUSED_TOPK_PACK) carrier: the FlashInfer # ``moe_fused_gate`` writes; a separate type keeps the 3-tuple unpack valid.
# routed-MoE packed topk produced fused in the gating kernel. Kept a SEPARATE type rather
# than a 4th StandardTopKOutput field so the OSS `a, b, _ = topk_output` 3-tuple unpack
# stays valid; only the gated experimental MoE dispatch reads .packed_topk_ids (getattr).
class StandardTopKOutputPacked(NamedTuple): class StandardTopKOutputPacked(NamedTuple):
topk_weights: torch.Tensor topk_weights: torch.Tensor
topk_ids: torch.Tensor topk_ids: torch.Tensor
@@ -341,9 +338,6 @@ class StandardTopKOutputPacked(NamedTuple):
return TopKOutputFormat.STANDARD return TopKOutputFormat.STANDARD
# ===== END TO BE REFACTORED ====
class TritonKernelTopKOutput(NamedTuple): class TritonKernelTopKOutput(NamedTuple):
"""Triton kernel top-k output format.""" """Triton kernel top-k output format."""
@@ -546,6 +540,8 @@ class TopK(BaseFusedOp):
fused_shared_experts_scaling_factor: Optional[float] = None, fused_shared_experts_scaling_factor: Optional[float] = None,
is_fp4_experts: bool = False, is_fp4_experts: bool = False,
allow_routed_experts_capture: bool = True, allow_routed_experts_capture: bool = True,
sqrtsoftplus_log1p: bool = False,
fused_gate_packed_ids: bool = False,
): ):
# NOTE: scoring_func is not used for now, but we keep it for future use # NOTE: scoring_func is not used for now, but we keep it for future use
# see https://github.com/sgl-project/sglang/pull/4505 for more details # see https://github.com/sgl-project/sglang/pull/4505 for more details
@@ -584,6 +580,8 @@ class TopK(BaseFusedOp):
fused_shared_experts_scaling_factor=fused_shared_experts_scaling_factor, fused_shared_experts_scaling_factor=fused_shared_experts_scaling_factor,
output_format=output_format, output_format=output_format,
scoring_func=scoring_func, scoring_func=scoring_func,
sqrtsoftplus_log1p=sqrtsoftplus_log1p,
fused_gate_packed_ids=fused_gate_packed_ids,
allow_routed_experts_capture=allow_routed_experts_capture, allow_routed_experts_capture=allow_routed_experts_capture,
) )
@@ -1387,10 +1385,13 @@ def biased_topk_jit_kernel_impl(
num_token_non_padded: Optional[torch.Tensor] = None, num_token_non_padded: Optional[torch.Tensor] = None,
expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None, expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None,
apply_routed_scaling_factor_on_output: Optional[bool] = False, apply_routed_scaling_factor_on_output: Optional[bool] = False,
packed_out: Optional[torch.Tensor] = None,
sqrtsoftplus_log1p: bool = False,
) -> Tuple[torch.Tensor, torch.Tensor]: ) -> Tuple[torch.Tensor, torch.Tensor]:
assert hidden_states.shape[0] == gating_output.shape[0], "Number of tokens mismatch" assert hidden_states.shape[0] == gating_output.shape[0], "Number of tokens mismatch"
if _use_aiter and scoring_func == "sqrtsoftplus" and num_fused_shared_experts == 0: if _use_aiter and scoring_func == "sqrtsoftplus" and num_fused_shared_experts == 0:
assert packed_out is None, "aiter topk_gating cannot emit packed ids"
from aiter import topk_gating from aiter import topk_gating
num_tokens = gating_output.shape[0] num_tokens = gating_output.shape[0]
@@ -1429,6 +1430,14 @@ def biased_topk_jit_kernel_impl(
renormalize=renormalize, renormalize=renormalize,
routed_scaling_factor=routed_scaling_factor, routed_scaling_factor=routed_scaling_factor,
apply_routed_scaling_factor_on_output=apply_routed_scaling_factor_on_output, apply_routed_scaling_factor_on_output=apply_routed_scaling_factor_on_output,
num_token_non_padded=(
num_token_non_padded
if _fused_gate_masks_padded_rows(scoring_func)
else None
),
# Optional FlashInfer routed-MoE packed ids, written in the same launch.
packed_out=packed_out,
sqrtsoftplus_log1p=sqrtsoftplus_log1p,
) )
topk_weights, topk_ids = ( topk_weights, topk_ids = (
topk_weights.to(torch.float32), topk_weights.to(torch.float32),
@@ -1586,6 +1595,31 @@ def _eplb_remap_enabled() -> bool:
) )
def _fused_gate_masks_padded_rows(scoring_func: str) -> bool:
# Sigmoid is excluded: a padding count bypasses moe_fused_gate's radix fast
# path, and HIP fills padded ids with 0, not -1.
return _is_cuda and not _use_aiter and scoring_func == "sqrtsoftplus"
def _fused_gate_emits_packed_ids(
scoring_func: str,
num_fused_shared_experts: int,
expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo],
routing_overridden: bool,
enabled: bool,
) -> bool:
# The pack is taken from the router's final values, so every condition past
# the caller's opt-in rules out a later rewrite of ids or weights.
return (
enabled
and _fused_gate_masks_padded_rows(scoring_func)
and get_moe_runner_backend().is_flashinfer_mxfp4()
and expert_location_dispatch_info is None
and num_fused_shared_experts == 0
and not routing_overridden
)
def _mask_topk_ids_padded_region( def _mask_topk_ids_padded_region(
topk_ids: torch.Tensor, topk_ids: torch.Tensor,
num_token_non_padded: Optional[torch.Tensor] = None, num_token_non_padded: Optional[torch.Tensor] = None,
@@ -2162,6 +2196,7 @@ def _post_process_topk_ids(
layer_id: int, layer_id: int,
num_token_non_padded: Optional[torch.Tensor] = None, num_token_non_padded: Optional[torch.Tensor] = None,
expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None, expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None,
padded_rows_masked: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
num_fused_shared_experts = topk_config.num_fused_shared_experts num_fused_shared_experts = topk_config.num_fused_shared_experts
use_per_rank_shared_slots = has_per_rank_fused_shared_slots( use_per_rank_shared_slots = has_per_rank_fused_shared_slots(
@@ -2204,8 +2239,16 @@ def _post_process_topk_ids(
# ExpertDistributionRecorder tracks only EPLB physical routed experts. # ExpertDistributionRecorder tracks only EPLB physical routed experts.
recorder_topk_ids = routed_cols recorder_topk_ids = routed_cols
else: else:
# A remap table indexed by -1 aliases its last entry, so only the
# identity-remap branch may drop the padded-row mask.
topk_ids = _biased_grouped_topk_postprocess( topk_ids = _biased_grouped_topk_postprocess(
topk_ids, expert_location_dispatch_info, num_token_non_padded topk_ids,
expert_location_dispatch_info,
(
None
if padded_rows_masked and expert_location_dispatch_info is None
else num_token_non_padded
),
) )
elif _is_hip: elif _is_hip:
# On AMD HIP the aiter MoE kernels do not handle topk_ids=-1 safely # On AMD HIP the aiter MoE kernels do not handle topk_ids=-1 safely
@@ -2374,8 +2417,19 @@ def select_experts(
scoring_func = topk_config.scoring_func scoring_func = topk_config.scoring_func
# Set by the fused-gating+pack branch below; None everywhere else. # Set by the fused-gating+pack branches below; None everywhere else.
packed_topk = None packed_topk = None
# True when the router itself masked rows >= num_token_non_padded.
padded_rows_masked = False
simulate_uniform_experts = envs.SGLANG_SIMULATE_UNIFORM_EXPERTS.get()
simulate_round_robin_experts = envs.SGLANG_SIMULATE_ROUND_ROBIN_EXPERTS.get()
if simulate_uniform_experts and simulate_round_robin_experts:
raise ValueError(
"SGLANG_SIMULATE_UNIFORM_EXPERTS and "
"SGLANG_SIMULATE_ROUND_ROBIN_EXPERTS are mutually exclusive"
)
routing_overridden = simulate_uniform_experts or simulate_round_robin_experts
( (
router_logits, router_logits,
@@ -2481,6 +2535,22 @@ def select_experts(
scoring_func == "sqrtsoftplus" or scoring_func == "sigmoid" scoring_func == "sqrtsoftplus" or scoring_func == "sigmoid"
): ):
_biased_topk = biased_topk_xpu if _is_xpu else biased_topk_jit_kernel_impl _biased_topk = biased_topk_xpu if _is_xpu else biased_topk_jit_kernel_impl
_packed_kwargs = {}
if _fused_gate_emits_packed_ids(
scoring_func,
num_fused_shared_experts,
expert_location_dispatch_info,
routing_overridden,
topk_config.fused_gate_packed_ids,
):
packed_topk = torch.empty(
(hidden_states.shape[0], top_k),
dtype=torch.int32,
device=hidden_states.device,
)
_packed_kwargs = dict(packed_out=packed_topk)
if topk_config.sqrtsoftplus_log1p:
_packed_kwargs["sqrtsoftplus_log1p"] = True
topk_weights, topk_ids = _biased_topk( topk_weights, topk_ids = _biased_topk(
hidden_states=hidden_states, hidden_states=hidden_states,
gating_output=router_logits, gating_output=router_logits,
@@ -2493,7 +2563,9 @@ def select_experts(
num_token_non_padded=num_token_non_padded, num_token_non_padded=num_token_non_padded,
expert_location_dispatch_info=expert_location_dispatch_info, expert_location_dispatch_info=expert_location_dispatch_info,
apply_routed_scaling_factor_on_output=apply_routed_scaling_factor_on_output, apply_routed_scaling_factor_on_output=apply_routed_scaling_factor_on_output,
**_packed_kwargs,
) )
padded_rows_masked = _fused_gate_masks_padded_rows(scoring_func)
elif ( elif (
get_moe_runner_backend().is_flashinfer_trtllm_routed() get_moe_runner_backend().is_flashinfer_trtllm_routed()
and scoring_func == "softmax" and scoring_func == "softmax"
@@ -2522,8 +2594,7 @@ def select_experts(
and correction_bias is None and correction_bias is None
and expert_location_dispatch_info is None and expert_location_dispatch_info is None
and num_fused_shared_experts == 0 and num_fused_shared_experts == 0
and not envs.SGLANG_SIMULATE_UNIFORM_EXPERTS.get() and not routing_overridden
and not envs.SGLANG_SIMULATE_ROUND_ROBIN_EXPERTS.get()
): ):
num_experts = router_logits.shape[-1] num_experts = router_logits.shape[-1]
if num_experts & (num_experts - 1) == 0 and num_experts <= 512: if num_experts & (num_experts - 1) == 0 and num_experts <= 512:
@@ -2569,15 +2640,7 @@ def select_experts(
renormalize=renormalize, renormalize=renormalize,
) )
simulate_uniform_experts = envs.SGLANG_SIMULATE_UNIFORM_EXPERTS.get() if routing_overridden:
simulate_round_robin_experts = envs.SGLANG_SIMULATE_ROUND_ROBIN_EXPERTS.get()
if simulate_uniform_experts and simulate_round_robin_experts:
raise ValueError(
"SGLANG_SIMULATE_UNIFORM_EXPERTS and "
"SGLANG_SIMULATE_ROUND_ROBIN_EXPERTS are mutually exclusive"
)
if simulate_uniform_experts or simulate_round_robin_experts:
# Benchmark-only: override gating with a balanced expert assignment (so # Benchmark-only: override gating with a balanced expert assignment (so
# dummy/random benchmark tokens don't skew MoE load) via a single fused # dummy/random benchmark tokens don't skew MoE load) via a single fused
# Triton kernel — one launch instead of the ~5-7 small elementwise ops it # Triton kernel — one launch instead of the ~5-7 small elementwise ops it
@@ -2600,6 +2663,8 @@ def select_experts(
token_shard_rank=token_shard_rank, token_shard_rank=token_shard_rank,
num_token_shards=num_token_shards, num_token_shards=num_token_shards,
) )
# The override rewrote every row, including the router-masked ones.
padded_rows_masked = False
topk_ids, topk_weights, recorder_topk_ids = _post_process_topk_ids( topk_ids, topk_weights, recorder_topk_ids = _post_process_topk_ids(
topk_ids=topk_ids, topk_ids=topk_ids,
@@ -2609,18 +2674,17 @@ def select_experts(
num_token_non_padded=num_token_non_padded, num_token_non_padded=num_token_non_padded,
layer_id=layer_id, layer_id=layer_id,
expert_location_dispatch_info=expert_location_dispatch_info, expert_location_dispatch_info=expert_location_dispatch_info,
padded_rows_masked=padded_rows_masked,
) )
get_global_expert_distribution_recorder().on_select_experts( get_global_expert_distribution_recorder().on_select_experts(
topk_ids=recorder_topk_ids topk_ids=recorder_topk_ids
) )
# ===== TO BE REFACTORED ====
if packed_topk is not None: if packed_topk is not None:
return StandardTopKOutputPacked( return StandardTopKOutputPacked(
topk_weights, topk_ids, router_logits, packed_topk topk_weights, topk_ids, router_logits, packed_topk
) )
# ===== END TO BE REFACTORED ====
return StandardTopKOutput(topk_weights, topk_ids, router_logits) return StandardTopKOutput(topk_weights, topk_ids, router_logits)
+17 -1
View File
@@ -516,6 +516,8 @@ class Fp8LinearMethod(LinearMethodBase):
self.w8a8_block_fp8_linear = None self.w8a8_block_fp8_linear = None
self.w8a8_mxfp8_linear = None self.w8a8_mxfp8_linear = None
self.mxfp8_dense_backend = None self.mxfp8_dense_backend = None
# Set by a model-owned startup hook after opting into prefill tuning.
self.mxfp8_prefill_autotune_min_tokens = None
if self.use_mxfp8 and not self.convert_mxfp8_to_block: if self.use_mxfp8 and not self.convert_mxfp8_to_block:
self.mxfp8_dense_backend = resolve_mxfp8_dense_gemm_backend() self.mxfp8_dense_backend = resolve_mxfp8_dense_gemm_backend()
self.w8a8_mxfp8_linear = dispatch_w8a8_mxfp8_linear() self.w8a8_mxfp8_linear = dispatch_w8a8_mxfp8_linear()
@@ -1154,6 +1156,19 @@ class Fp8LinearMethod(LinearMethodBase):
if mxfp8_view: if mxfp8_view:
backend = self.mxfp8_dense_backend backend = self.mxfp8_dense_backend
extra_kwargs = {} extra_kwargs = {}
if self.mxfp8_prefill_autotune_min_tokens is not None:
input_tensor = x[0] if isinstance(x, tuple) else x
num_tokens = input_tensor.numel() // input_tensor.shape[-1]
if num_tokens >= self.mxfp8_prefill_autotune_min_tokens:
from sglang.srt.batch_invariant_ops import (
is_batch_invariant_mode_enabled,
)
from sglang.srt.runtime_context import get_exec
extra_kwargs["pin_tactic"] = (
is_batch_invariant_mode_enabled()
or get_exec().deterministic.enable_deterministic_inference
)
if backend.is_flashinfer_cutlass() or backend.is_flashinfer_cutedsl(): if backend.is_flashinfer_cutlass() or backend.is_flashinfer_cutedsl():
weight_scale = layer.weight_scale_inv_swizzled weight_scale = layer.weight_scale_inv_swizzled
elif backend.is_flashinfer_trtllm(): elif backend.is_flashinfer_trtllm():
@@ -1172,7 +1187,7 @@ class Fp8LinearMethod(LinearMethodBase):
bias=bias, bias=bias,
**extra_kwargs, **extra_kwargs,
) )
return self.w8a8_mxfp8_linear( out = self.w8a8_mxfp8_linear(
input=x, input=x,
weight=layer.weight, weight=layer.weight,
weight_scale=weight_scale, weight_scale=weight_scale,
@@ -1180,6 +1195,7 @@ class Fp8LinearMethod(LinearMethodBase):
bias=bias, bias=bias,
**extra_kwargs, **extra_kwargs,
) )
return out
if self.block_quant: if self.block_quant:
if use_intel_amx_backend(layer): if use_intel_amx_backend(layer):
@@ -746,9 +746,13 @@ def can_serve_block_fp8_as_mxfp8(
def dispatch_block_fp8_mxfp8_linear(backend: Mxfp8DenseGemmBackend) -> Callable: def dispatch_block_fp8_mxfp8_linear(backend: Mxfp8DenseGemmBackend) -> Callable:
"""The MXFP8 linear for a block-fp8 weight served as MXFP8.""" """The MXFP8 linear for a block-fp8 weight served as MXFP8."""
if backend.is_flashinfer_cutlass(): if backend.is_flashinfer_cutlass():
return partial(flashinfer_mxfp8_blockscaled_linear, backend="cutlass") return partial(
flashinfer_mxfp8_blockscaled_linear, backend="cutlass", pin_tactic=True
)
if backend.is_flashinfer_cutedsl(): if backend.is_flashinfer_cutedsl():
return partial(flashinfer_mxfp8_blockscaled_linear, backend="cute-dsl") return partial(
flashinfer_mxfp8_blockscaled_linear, backend="cute-dsl", pin_tactic=True
)
return _unsupported_mxfp8_linear return _unsupported_mxfp8_linear
@@ -1473,9 +1477,14 @@ def flashinfer_mxfp8_blockscaled_linear(
bias: Optional[torch.Tensor] = None, bias: Optional[torch.Tensor] = None,
output_dtype: Optional[torch.dtype] = None, output_dtype: Optional[torch.dtype] = None,
backend: str = "cutlass", backend: str = "cutlass",
pin_tactic: bool = False,
) -> torch.Tensor: ) -> torch.Tensor:
"""MXFP8 dense linear via FlashInfer mm_mxfp8. `weight_scale` must be the layout """MXFP8 dense linear via FlashInfer mm_mxfp8. `weight_scale` must be the layout
the backend expects, prepared at load time.""" the backend expects, prepared at load time.
pin_tactic skips autotuning: tactics tuned per M bucket change the fp32
reduction order, breaking row-wise batch invariance.
"""
input_2d = input.view(-1, input.shape[-1]) input_2d = input.view(-1, input.shape[-1])
output_shape = [*input.shape[:-1], weight.shape[0]] output_shape = [*input.shape[:-1], weight.shape[0]]
@@ -1507,6 +1516,20 @@ def flashinfer_mxfp8_blockscaled_linear(
else: else:
weight_scale_t = weight_scale.t() if weight_scale.ndim == 2 else weight_scale weight_scale_t = weight_scale.t() if weight_scale.ndim == 2 else weight_scale
if pin_tactic:
from flashinfer.autotuner import autotune
with autotune(False, skip_ops={"mxfp8_gemm"}):
output = flashinfer_mm_mxfp8(
q_input,
weight.t(),
x_scale_u8,
weight_scale_t,
out_dtype=output_dtype,
use_8x4_sf_layout=False,
backend=backend,
)
else:
output = flashinfer_mm_mxfp8( output = flashinfer_mm_mxfp8(
q_input, q_input,
weight.t(), weight.t(),
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple
import torch import torch
from torch.nn import Module from torch.nn import Module
@@ -97,6 +97,21 @@ def _pad_intermediate_size(layer: Module) -> None:
) )
def routed_hidden_size(layer: Module) -> int:
"""Hidden size the routed GEMM1 expects (uint8 weights hold two fp4/row)."""
w13 = layer.w13_weight
return w13.shape[2] * 2 if w13.dtype == torch.uint8 else w13.shape[2]
class Mxfp8RoutedInputPreQuant(NamedTuple):
"""MXFP8 linear-layout quant of the routed MoE input. ``ready`` is recorded on
the producing stream; the consumer must wait on it before the routed MoE op."""
x_q: torch.Tensor
x_sf: torch.Tensor
ready: Optional[torch.cuda.Event]
class Mxfp4FlashinferTrtllmMoEMethod: class Mxfp4FlashinferTrtllmMoEMethod:
fuse_routed_scaling_factor_in_topk = True fuse_routed_scaling_factor_in_topk = True
@@ -311,6 +326,24 @@ class Mxfp4FlashinferTrtllmMoEMethod:
persistent=False, persistent=False,
) )
def quantize_routed_input(
self, hidden_states: torch.Tensor, hidden_size: int
) -> Tuple[torch.Tensor, torch.Tensor]:
"""MXFP8 quant of the routed input, with the scale in the linear
[tokens, hidden // 32] layout the routed MoE op requires."""
from sglang.srt.layers.quantization.fp8_utils import flashinfer_mxfp8_quantize
x_quant, x_scale = flashinfer_mxfp8_quantize(
hidden_states,
False,
alignment=hidden_size,
backend=_MXFP8_QUANTIZE_BACKEND,
)
x_scale = x_scale.view(torch.float8_e4m3fn).reshape(
*hidden_states.shape[:-1], -1
)
return x_quant, x_scale
def apply( def apply(
self, self,
layer: Module, layer: Module,
@@ -321,6 +354,7 @@ class Mxfp4FlashinferTrtllmMoEMethod:
hidden_states = dispatch_output.hidden_states hidden_states = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output topk_output = dispatch_output.topk_output
pre_quant = getattr(dispatch_output, "hidden_states_pre_quant", None)
w13 = layer.w13_weight w13 = layer.w13_weight
w2 = layer.w2_weight w2 = layer.w2_weight
@@ -328,7 +362,7 @@ class Mxfp4FlashinferTrtllmMoEMethod:
w2_scale = layer.w2_weight_scale_inv w2_scale = layer.w2_weight_scale_inv
intermediate_size = w2.shape[2] * 2 if w2.dtype == torch.uint8 else w2.shape[2] intermediate_size = w2.shape[2] * 2 if w2.dtype == torch.uint8 else w2.shape[2]
hidden_size = w13.shape[2] * 2 if w13.dtype == torch.uint8 else w13.shape[2] hidden_size = routed_hidden_size(layer)
num_local_experts = layer.num_local_experts num_local_experts = layer.num_local_experts
if w13_scale.dim() == 2: if w13_scale.dim() == 2:
@@ -336,17 +370,18 @@ class Mxfp4FlashinferTrtllmMoEMethod:
if w2_scale.dim() == 2: if w2_scale.dim() == 2:
w2_scale = w2_scale.reshape(num_local_experts, hidden_size, -1) w2_scale = w2_scale.reshape(num_local_experts, hidden_size, -1)
if TopKOutputChecker.format_is_standard(topk_output): if TopKOutputChecker.format_is_bypassed(topk_output):
topk_ids = topk_output.topk_ids
topk_weights = topk_output.topk_weights
elif TopKOutputChecker.format_is_bypassed(topk_output):
raise NotImplementedError( raise NotImplementedError(
"the old code in this branch is WRONG. e.g. it does not consider HashTopK, and may miss args" "the old code in this branch is WRONG. e.g. it does not consider HashTopK, and may miss args"
) )
else: if not TopKOutputChecker.format_is_standard(topk_output):
raise ValueError(f"Unsupported topk output format: {topk_output.format}") raise ValueError(f"Unsupported topk output format: {topk_output.format}")
topk_ids = topk_output.topk_ids
topk_weights = topk_output.topk_weights
precision = self.flashinfer_mxfp4_moe_precision precision = self.flashinfer_mxfp4_moe_precision
input_ready: Optional[torch.cuda.Event] = None
if precision == "bf16": if precision == "bf16":
assert hidden_states.dtype == torch.bfloat16 assert hidden_states.dtype == torch.bfloat16
x_quant = hidden_states x_quant = hidden_states
@@ -360,40 +395,48 @@ class Mxfp4FlashinferTrtllmMoEMethod:
value=0.0, value=0.0,
) )
elif precision == "default": elif precision == "default":
from sglang.srt.layers.quantization.fp8_utils import ( if isinstance(pre_quant, Mxfp8RoutedInputPreQuant):
flashinfer_mxfp8_quantize, assert pre_quant.x_q.shape[0] == hidden_states.shape[0]
) x_quant, x_scale, input_ready = pre_quant
else:
x_quant, x_scale = flashinfer_mxfp8_quantize( x_quant, x_scale = self.quantize_routed_input(
hidden_states, hidden_states, hidden_size
False,
alignment=hidden_size,
backend=_MXFP8_QUANTIZE_BACKEND,
)
x_scale = x_scale.view(torch.float8_e4m3fn).reshape(
*hidden_states.shape[:-1], -1
) )
else: else:
raise NotImplementedError(f"Unsupported mxfp4 moe precision: {precision}") raise NotImplementedError(f"Unsupported mxfp4 moe precision: {precision}")
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import ( from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
_make_deferred_finalize_output,
is_deferred_finalize_enabled,
trtllm_moe_enable_pdl, trtllm_moe_enable_pdl,
) )
num_tokens = x_quant.shape[0]
# Deferred finalize returns the permuted GEMM2 output plus the routing
# triple instead of the finalized [T, hidden] tensor.
defer_finalize = is_deferred_finalize_enabled()
symm_output = None
if not defer_finalize:
with use_symmetric_memory( with use_symmetric_memory(
get_tp_group(), disabled=not is_allocation_symmetric() get_tp_group(), disabled=not is_allocation_symmetric()
): ):
num_tokens = x_quant.shape[0]
out_hidden_size = ( out_hidden_size = (
x_quant.shape[-1] * 2 x_quant.shape[-1] * 2
if x_quant.dtype == torch.uint8 if x_quant.dtype == torch.uint8
else x_quant.shape[-1] else x_quant.shape[-1]
) )
symm_output = torch.empty( symm_output = torch.empty(
num_tokens, out_hidden_size, dtype=torch.bfloat16, device=x_quant.device num_tokens,
out_hidden_size,
dtype=torch.bfloat16,
device=x_quant.device,
) )
output = trtllm_fp4_block_scale_routed_moe( if input_ready is not None:
# The op launches the routing kernel, so the join must precede it.
torch.cuda.current_stream().wait_event(input_ready)
result = trtllm_fp4_block_scale_routed_moe(
topk_ids=(topk_ids, topk_weights), topk_ids=(topk_ids, topk_weights),
routing_bias=None, routing_bias=None,
hidden_states=x_quant, hidden_states=x_quant,
@@ -419,11 +462,15 @@ class Mxfp4FlashinferTrtllmMoEMethod:
local_num_experts=num_local_experts, local_num_experts=num_local_experts,
routed_scaling_factor=1.0, routed_scaling_factor=1.0,
routing_method_type=int(RoutingMethodType.TopK), routing_method_type=int(RoutingMethodType.TopK),
do_finalize=True, do_finalize=not defer_finalize,
tune_max_num_tokens=next_power_of_2(x_quant.shape[0]), tune_max_num_tokens=next_power_of_2(num_tokens),
output=symm_output, output=symm_output,
enable_pdl=trtllm_moe_enable_pdl(num_tokens), enable_pdl=trtllm_moe_enable_pdl(num_tokens),
)[0] )
if defer_finalize:
output = _make_deferred_finalize_output(result, top_k=topk_ids.shape[1])
else:
output = result[0]
return StandardCombineInput(hidden_states=output) return StandardCombineInput(hidden_states=output)
@@ -467,3 +514,58 @@ def maybe_fuse_routed_scale_and_shared_add(
if shared is not None: if shared is not None:
routed += shared routed += shared
return routed return routed
# Fused finalize + shared add + TP all-reduce
_fused_finalize_all_reduce_world_size: Optional[int] = None
_fused_finalize_all_reduce_probed = False
def _fused_finalize_all_reduce_comm_world_size() -> Optional[int]:
global _fused_finalize_all_reduce_world_size, _fused_finalize_all_reduce_probed
if not _fused_finalize_all_reduce_probed:
_fused_finalize_all_reduce_probed = True
from sglang.kernels.ops.communication import all_reduce_fusion
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
CustomAllReduceV2,
)
ca_comm = get_tp_group().ca_comm
if isinstance(ca_comm, CustomAllReduceV2) and not ca_comm.disabled:
all_reduce_fusion.register_comm(ca_comm.obj)
_fused_finalize_all_reduce_world_size = ca_comm.world_size
else:
log_info_on_rank0(
logger,
"Fused MoE finalize: TP group has no "
"CustomAllReduceV2 push plane; keeping the unfused finalize path",
)
return _fused_finalize_all_reduce_world_size
def should_use_fuse_finalize_all_reduce(
experts, num_tokens: int, hidden_dim: int
) -> bool:
"""Capability only; the batch-size policy cap lives at the call site. The
kernel never rescales, so the expert weights must carry the routed scaling."""
if not isinstance(experts.quant_method, Mxfp4FlashinferTrtllmMoEMethod):
return False
if experts.quant_method.flashinfer_mxfp4_moe_precision != "default":
return False
if not experts.should_fuse_routed_scaling_factor_in_topk:
return False
if num_tokens <= 0:
return False
from sglang.kernels.ops.communication import all_reduce_fusion
if not all_reduce_fusion.valid_cluster_sizes(hidden_dim):
return False
tp_group = get_tp_group()
if _fused_finalize_all_reduce_comm_world_size() != tp_group.world_size:
return False
# one push phase counter per row (the plane has num_sm of them)
if num_tokens > tp_group.ca_comm.config.num_push_blocks:
return False
return all_reduce_fusion.fits_push_slot(
tp_group.ca_comm.max_push_size, num_tokens, hidden_dim
)
@@ -2386,6 +2386,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
# DeepSeek-V4.1 engram, extend batches only: [bs, n - 1] int32 predecessors # DeepSeek-V4.1 engram, extend batches only: [bs, n - 1] int32 predecessors
# of each request's first extend token (NgramEmbeddingManager). # of each request's first extend token (NgramEmbeddingManager).
engram_history: Optional[torch.Tensor] = None engram_history: Optional[torch.Tensor] = None
encoder_swa_reset: Optional[List[bool]] = None
req_pool_indices: torch.Tensor = None # shape: [b], int64 req_pool_indices: torch.Tensor = None # shape: [b], int64
seq_lens: torch.Tensor = None # shape: [b], int64 seq_lens: torch.Tensor = None # shape: [b], int64
@@ -2723,6 +2724,26 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
self.seq_lens_cpu = seq_lens_cpu self.seq_lens_cpu = seq_lens_cpu
self.extend_num_tokens = extend_num_tokens self.extend_num_tokens = extend_num_tokens
if get_exec().features.enable_encoder_swa_bounded_replay:
for req in reqs:
if (
req.multimodal_inputs is not None
or req.input_embeds is not None
or req.positional_embed_overrides is not None
):
raise ValueError(
"encoder SWA replay currently supports token-only text requests"
)
if req.return_logprob and req.logprob_start_len not in (
-1,
len(req.origin_input_ids),
):
raise ValueError(
"encoder SWA replay cannot return cached prompt logprobs"
)
self.encoder_swa_reset = [
r.kv.req_pool_idx is None or r.is_retracted for r in reqs
]
# Allocate memory # Allocate memory
out_cache_loc, req_pool_indices_tensor, req_pool_indices_cpu = alloc_for_extend( out_cache_loc, req_pool_indices_tensor, req_pool_indices_cpu = alloc_for_extend(
self self
@@ -7,6 +7,7 @@ from sglang.srt.environ import envs
from sglang.srt.managers.prefill_delayer import PrefillDelayerSinglePassExecutor from sglang.srt.managers.prefill_delayer import PrefillDelayerSinglePassExecutor
from sglang.srt.runtime_context import ( from sglang.srt.runtime_context import (
get_disagg, get_disagg,
get_exec,
get_schedule, get_schedule,
) )
from sglang.srt.utils import get_bool_env_var, is_gfx95_supported, is_hip from sglang.srt.utils import get_bool_env_var, is_gfx95_supported, is_hip
@@ -667,6 +668,7 @@ class PrefillAdder:
self.log_host_hit_tokens = 0 self.log_host_hit_tokens = 0
self.log_storage_hit_tokens = 0 self.log_storage_hit_tokens = 0
self.log_input_tokens = 0 self.log_input_tokens = 0
self.log_replay_tokens = 0
self.reprocessed_log_input_tokens = 0 self.reprocessed_log_input_tokens = 0
if running_batch is not None: if running_batch is not None:
@@ -897,6 +899,14 @@ class PrefillAdder:
self.reprocessed_log_input_tokens += raw_extend_input_len self.reprocessed_log_input_tokens += raw_extend_input_len
def _account_prefill_cache_admission(self, req: Req, prefix_len: int) -> None: def _account_prefill_cache_admission(self, req: Req, prefix_len: int) -> None:
if get_exec().features.enable_encoder_swa_bounded_replay and (
req.kv.req_pool_idx is None or req.is_retracted
):
replay_tokens = min(prefix_len, 128)
self.log_replay_tokens += replay_tokens
self.rem_input_tokens -= replay_tokens
if self.rem_chunk_tokens is not None:
self.rem_chunk_tokens -= replay_tokens
if req.retracted_stain: if req.retracted_stain:
# Retraction attribution is intentionally omitted for now; discard # Retraction attribution is intentionally omitted for now; discard
# its lifecycle state so a later abort cannot report it as a drop. # its lifecycle state so a later abort cannot report it as a drop.
@@ -103,6 +103,7 @@ class PrefillStats:
log_host_hit_tokens: int = 0 log_host_hit_tokens: int = 0
log_storage_hit_tokens: int = 0 log_storage_hit_tokens: int = 0
num_pending_tokens: int = 0 num_pending_tokens: int = 0
log_replay_tokens: int = 0
@classmethod @classmethod
def from_adder( def from_adder(
@@ -114,6 +115,7 @@ class PrefillStats:
): ):
return cls( return cls(
log_input_tokens=adder.log_input_tokens, log_input_tokens=adder.log_input_tokens,
log_replay_tokens=adder.log_replay_tokens,
log_hit_tokens=adder.log_hit_tokens, log_hit_tokens=adder.log_hit_tokens,
reprocessed_log_input_tokens=adder.reprocessed_log_input_tokens, reprocessed_log_input_tokens=adder.reprocessed_log_input_tokens,
reprocessed_log_hit_tokens=adder.reprocessed_log_hit_tokens, reprocessed_log_hit_tokens=adder.reprocessed_log_hit_tokens,
@@ -660,7 +662,10 @@ class SchedulerMetricsReporter:
gap_latency = now - self.last_prefill_stats_tic gap_latency = now - self.last_prefill_stats_tic
self.last_prefill_stats_tic = now self.last_prefill_stats_tic = now
self.last_input_throughput = ( self.last_input_throughput = (
prefill_stats.log_input_tokens / gap_latency if gap_latency > 0 else 0.0 (prefill_stats.log_input_tokens + prefill_stats.log_replay_tokens)
/ gap_latency
if gap_latency > 0
else 0.0
) )
pool_stats = self.scheduler.pool_stats_observer.get_pool_stats() pool_stats = self.scheduler.pool_stats_observer.get_pool_stats()
@@ -685,6 +690,8 @@ class SchedulerMetricsReporter:
f"#pending-token: {prefill_stats.num_pending_tokens}, " f"#pending-token: {prefill_stats.num_pending_tokens}, "
) )
if prefill_stats.log_replay_tokens:
msg += f"#replay-token: {prefill_stats.log_replay_tokens}, "
if self.scheduler.disaggregation_mode == DisaggregationMode.PREFILL: if self.scheduler.disaggregation_mode == DisaggregationMode.PREFILL:
msg += f"#bootstrap-req: {len(self.scheduler.disagg_prefill_bootstrap_queue.queue)}, " msg += f"#bootstrap-req: {len(self.scheduler.disagg_prefill_bootstrap_queue.queue)}, "
msg += ( msg += (
@@ -728,7 +735,9 @@ class SchedulerMetricsReporter:
value=can_run_cuda_graph value=can_run_cuda_graph
) )
self.metrics_collector.increment_realtime_tokens( self.metrics_collector.increment_realtime_tokens(
prefill_compute_tokens=prefill_stats.log_input_tokens, prefill_compute_tokens=(
prefill_stats.log_input_tokens + prefill_stats.log_replay_tokens
),
prefill_cache_tokens=prefill_stats.log_hit_tokens, prefill_cache_tokens=prefill_stats.log_hit_tokens,
dp_cooperation_info=dp_cooperation_info, dp_cooperation_info=dp_cooperation_info,
) )
@@ -1257,6 +1257,28 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
) -> None: ) -> None:
"""Validates that the input token count and the requested token count doesn't exceed the model's context length.""" """Validates that the input token count and the requested token count doesn't exceed the model's context length."""
# FIXME: unify the length validation logic with the one in the scheduler. # FIXME: unify the length validation logic with the one in the scheduler.
if get_exec().features.enable_encoder_swa_bounded_replay:
if any(
value is not None
for value in (
obj.image_data,
obj.video_data,
obj.audio_data,
obj.input_embeds,
obj.positional_embed_overrides,
)
):
raise ValueError(
"encoder SWA replay currently supports token-only text requests"
)
if (
isinstance(obj, GenerateReqInput)
and obj.return_logprob
and obj.logprob_start_len not in (None, -1, len(input_ids))
):
raise ValueError(
"encoder SWA replay cannot return cached prompt logprobs"
)
_max_req_len = self.context_len _max_req_len = self.context_len
input_token_num = len(input_ids) if input_ids is not None else 0 input_token_num = len(input_ids) if input_ids is not None else 0
input_token_num += self.num_reserved_tokens input_token_num += self.num_reserved_tokens
+8
View File
@@ -642,6 +642,14 @@ class TpModelWorker(BaseTpWorker):
# update the consumer index of hicache to the running batch # update the consumer index of hicache to the running batch
self.set_hicache_consumer(batch.hicache_consumer_index) self.set_hicache_consumer(batch.hicache_consumer_index)
if get_exec().features.enable_encoder_swa_bounded_replay:
from sglang.srt.model_executor.encoder_swa_replay import (
run_encoder_swa_replay,
)
# Replay reads restored main/indexer KV before the normal extend.
run_encoder_swa_replay(self, batch)
forward_batch = ForwardBatch.init_new( forward_batch = ForwardBatch.init_new(
batch, batch,
self.model_runner, self.model_runner,
+18
View File
@@ -194,6 +194,19 @@ def _evict_until_allocatable(
return return
def dsv41_dspark_needs_rebootstrap(
token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator,
) -> bool:
"""V4.1's request-scoped pair ring and draft KV cannot use CPU tensor backup."""
if str(get_spec().speculative_algorithm).upper() != "DSPARK":
return False
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
pool = token_to_kv_pool_allocator.get_kvcache()
return isinstance(pool, DeepSeekV4TokenToKVPool) and 2 in pool.compression_ratios
def retraction_backup( def retraction_backup(
req: Req, req: Req,
tree_cache: BasePrefixCache, tree_cache: BasePrefixCache,
@@ -203,6 +216,11 @@ def retraction_backup(
) -> bool: ) -> bool:
"""Returns False when the host pool cannot hold the backup; the caller """Returns False when the host pool cannot hold the backup; the caller
aborts the request since its KV cannot be preserved.""" aborts the request since its KV cannot be preserved."""
if dsv41_dspark_needs_rebootstrap(token_to_kv_pool_allocator):
# Drain the in-flight verify before its slots can receive recomputed KV.
device = token_to_kv_pool_allocator.get_kvcache().device
torch.get_device_module(device).synchronize(device)
return True
if backend == "cpu_tensor": if backend == "cpu_tensor":
req.offload_kv_cache(req_to_token_pool, token_to_kv_pool_allocator) req.offload_kv_cache(req_to_token_pool, token_to_kv_pool_allocator)
return True return True
@@ -183,13 +183,8 @@ class CompressStatePool:
dtype=dtype, device=device, enable_memory_saver=enable_memory_saver dtype=dtype, device=device, enable_memory_saver=enable_memory_saver
) )
if not online: if not online:
if _is_hip and ratio == 128: if ratio == 2 or (_is_hip and ratio == 128):
# Request-scoped C128 state is addressed by req_pool_idx (or a # Request-scoped rings reset all rows; C4 only its -1 sentinel row.
# per-request ring). The pool is allocated with torch.empty(),
# so a cold server can otherwise read uninitialized partial
# states before a request slot has been written for the first
# time. Initialize all C128 rows to the empty-state sentinel;
# C4 keeps the historical last-row sentinel behavior.
self.kv_score_buffer.clear() self.kv_score_buffer.clear()
else: else:
self.kv_score_buffer[-1].clear() self.kv_score_buffer[-1].clear()
@@ -197,6 +192,11 @@ class CompressStatePool:
def transfer_indices(self, req_pool_idx: int, seq_len: int) -> np.ndarray: def transfer_indices(self, req_pool_idx: int, seq_len: int) -> np.ndarray:
"""PD transfer indices of this pool's state for one request.""" """PD transfer indices of this pool's state for one request."""
assert self.request_scoped, "page-scoped state travels with the SWA pages" assert self.request_scoped, "page-scoped state travels with the SWA pages"
if self.ratio == 2:
# Only an odd prefix leaves a pending half-pair for decode to read.
if seq_len % 2 == 0:
return np.empty((0,), dtype=np.int32)
return np.array([int(req_pool_idx)], dtype=np.int32)
return request_scoped_state_transfer_indices( return request_scoped_state_transfer_indices(
req_pool_idx, req_pool_idx,
seq_len, seq_len,
@@ -262,15 +262,16 @@ class CompressStatePool:
) -> torch.Tensor: ) -> torch.Tensor:
swa_pages = swa_loc // self.swa_page_size swa_pages = swa_loc // self.swa_page_size
state_loc = swa_pages * self.ring_size + (swa_loc % self.ring_size) state_loc = swa_pages * self.ring_size + (swa_loc % self.ring_size)
state_loc = torch.where(swa_loc < 0, -1, state_loc) # Not where(cond, -1, x): its scalar overload may stage a host tensor,
return state_loc # which a CUDA graph capture cannot run.
return state_loc.masked_fill_(swa_loc < 0, -1)
def translate_from_req_position_to_state_loc( def translate_from_req_position_to_state_loc(
self, req_pool_indices: torch.Tensor, positions: torch.Tensor self, req_pool_indices: torch.Tensor, positions: torch.Tensor
) -> torch.Tensor: ) -> torch.Tensor:
state_loc = req_pool_indices * self.ring_size + positions % self.ring_size state_loc = req_pool_indices * self.ring_size + positions % self.ring_size
state_loc = torch.where(positions < 0, -1, state_loc) # A negative position means "no slot"; it lands on the empty row -1.
return state_loc return state_loc.masked_fill_(positions < 0, -1)
def get_state_by_state_loc(self, state_loc: torch.Tensor) -> KVAndScore: def get_state_by_state_loc(self, state_loc: torch.Tensor) -> KVAndScore:
return self.kv_score_buffer[state_loc] return self.kv_score_buffer[state_loc]
@@ -2,7 +2,7 @@ from __future__ import annotations
import logging import logging
from contextlib import nullcontext from contextlib import nullcontext
from typing import List, NamedTuple, Optional, Sequence, Tuple from typing import List, Literal, NamedTuple, Optional, Sequence, Tuple, Union
import torch import torch
@@ -16,7 +16,10 @@ from sglang.kernels.ops.attention.dsv4 import (
index_buf_accessor as dsv4_index_buf_accessor, index_buf_accessor as dsv4_index_buf_accessor,
) )
from sglang.kernels.ops.attention.dsv4.index_buf_accessor import NopeFp8RopeBf16Pack from sglang.kernels.ops.attention.dsv4.index_buf_accessor import NopeFp8RopeBf16Pack
from sglang.kernels.ops.attention.dsv4.kv_layout import KVLayout from sglang.kernels.ops.attention.dsv4.kv_layout import (
KVLayout,
is_valid_kv_layout_pair,
)
from sglang.kernels.ops.attention.dsv4.unified_kv_kernels import layout from sglang.kernels.ops.attention.dsv4.unified_kv_kernels import layout
from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE
from sglang.srt.environ import envs from sglang.srt.environ import envs
@@ -41,11 +44,17 @@ def get_dsv4_indexer_bytes_per_token(index_head_dim: int, use_fp4_indexer: bool)
def get_compress_state_ring_size( def get_compress_state_ring_size(
compress_ratio: int, is_speculative: bool = False compress_ratio: int, is_speculative: bool = False, num_draft_tokens: int = 0
) -> int: ) -> int:
assert compress_ratio in [4, 128], f"Unsupported {compress_ratio = }" assert compress_ratio in [2, 4, 128], f"Unsupported {compress_ratio = }"
# Online C128 stores one (max, sum, kv) state per index; if compress_ratio == 2:
# speculative decoding requires the experimental online C128 MTP path. # Two positions are one pair, addressed by position % ring_size; a
# speculative ring must be wider than the draft window: pow2 >= 2 + drafts.
if not is_speculative:
return 2
return 1 << (num_draft_tokens + 1).bit_length()
# Online c128 keeps one (max, sum, kv) state per index instead of a 128-slot
# ring of raw tokens, so ring_size collapses to 1.
if compress_ratio == 128 and ONLINE_C128: if compress_ratio == 128 and ONLINE_C128:
if is_speculative and not envs.SGLANG_EXPERIMENTAL_ONLINE_C128_MTP.get(): if is_speculative and not envs.SGLANG_EXPERIMENTAL_ONLINE_C128_MTP.get():
raise AssertionError("online c128 does not support MTP") raise AssertionError("online c128 does not support MTP")
@@ -57,8 +66,8 @@ def get_compress_state_ring_size(
def get_compress_state_write_pad(compress_ratio: int, ring_size: int) -> int: def get_compress_state_write_pad(compress_ratio: int, ring_size: int) -> int:
# Draft-token capacity must match mtp_pad in c_plan.cuh; """Largest draft-token count this ring can serve; mirrors `mtp_pad` in
# a non-speculative ring has no write padding. `c_plan.cuh`, where the bound is derived."""
window_size = compress_ratio * (2 if compress_ratio == 4 else 1) window_size = compress_ratio * (2 if compress_ratio == 4 else 1)
return ring_size - window_size + 2 if ring_size > window_size else 0 return ring_size - window_size + 2 if ring_size > window_size else 0
@@ -69,6 +78,69 @@ def get_swa_ring_size(sliding_window: int, is_speculative: bool = False) -> int:
return sliding_window + spec_extra return sliding_window + spec_extra
def resolve_compressed_kv_layout(
kv_layout: KVLayout, compress_ratio: int, option: Optional[str] = None
) -> KVLayout:
"""Layout of one compress ratio's cache next to a ``kv_layout`` main cache.
The ratio-1/2 latents are already e2m1 with per-16 e4m3 scales, so ``V41_FP4``
is lossless for them; ratios 4 / 128 are not fp4-rounded and stay fp8."""
if option is not None:
option = option.lower()
assert option in (
"auto",
"fp8",
"fp4",
), f"unknown compressed KV layout {option!r}"
if option == "auto":
option = None
if kv_layout is KVLayout.V4:
assert option in (None, "fp8"), "the V4 main cache only pairs with V4 caches"
return KVLayout.V4
assert kv_layout is KVLayout.V41, f"{kv_layout} is not a main-cache layout"
if option == "fp8":
return KVLayout.V41
if option == "fp4":
return KVLayout.V41_FP4
return KVLayout.V41_FP4 if compress_ratio in (1, 2) else KVLayout.V41
def flashmla_supports_v41_kv_layouts() -> bool:
"""Whether the installed FlashMLA decode kernel reads the V41 / V41_FP4
formats; its docstring lists the bytes-per-token it detects."""
try:
from sgl_kernel.flash_mla import flash_mla_with_kvcache
except Exception:
return False
return "528" in (flash_mla_with_kvcache.__doc__ or "")
def select_dsv4_kv_layout() -> Tuple[KVLayout, Optional[str]]:
"""The (main-cache layout, compressed-cache option) for a new DeepSeek-V4
family pool; the V4.1 layouts exist only in SM100 / SM103 FlashMLA."""
mode = envs.SGLANG_DSV4_KV_LAYOUT.get().lower()
option = envs.SGLANG_DSV4_COMPRESSED_KV_LAYOUT.get().lower()
if mode == "v4":
return KVLayout.V4, None if option == "auto" else option
assert mode in ("v41", "auto"), f"unknown SGLANG_DSV4_KV_LAYOUT={mode!r}"
is_sm100 = (
torch.cuda.is_available()
and torch.version.cuda is not None
and torch.cuda.get_device_capability()[0] == 10
)
supported = flashmla_supports_v41_kv_layouts()
if mode == "auto":
if is_sm100 and supported:
return KVLayout.V41, option
return KVLayout.V4, None
assert is_sm100, "the V4.1 KV cache layouts need an SM100 / SM103 GPU"
if not supported:
logger.warning(
"SGLANG_DSV4_KV_LAYOUT=v41 but the installed FlashMLA does not advertise "
"the V4.1 KV cache formats; the attention kernel will reject the cache."
)
return KVLayout.V41, option
class DeepSeekV4SingleKVPool(KVCache): class DeepSeekV4SingleKVPool(KVCache):
# Paged FlashMLA main-KV format of this pool's rows. # Paged FlashMLA main-KV format of this pool's rows.
kv_layout: KVLayout = KVLayout.V4 kv_layout: KVLayout = KVLayout.V4
@@ -85,6 +157,7 @@ class DeepSeekV4SingleKVPool(KVCache):
enable_memory_saver: bool, enable_memory_saver: bool,
start_layer: Optional[int] = None, start_layer: Optional[int] = None,
end_layer: Optional[int] = None, end_layer: Optional[int] = None,
kv_layout: Union[str, KVLayout] = KVLayout.V4,
): ):
super().__init__( super().__init__(
size, size,
@@ -99,8 +172,11 @@ class DeepSeekV4SingleKVPool(KVCache):
self.qk_nope_head_dim = qk_nope_head_dim self.qk_nope_head_dim = qk_nope_head_dim
self.qk_rope_head_dim = qk_rope_head_dim self.qk_rope_head_dim = qk_rope_head_dim
# Paged FlashMLA layout of this pool's pages; see KVLayout.
self.kv_layout = KVLayout.parse(kv_layout)
self.scale_pad = 1 self.scale_pad = 1
self.quantize_block_size = 64 self.quantize_block_size = self.kv_layout.tile_size
# V4 keeps its 64 RoPE dims in bf16; the V4.1 layouts quantize them too.
self.rope_storage_dtype = torch.bfloat16 self.rope_storage_dtype = torch.bfloat16
self.k_with_scale_buffer_dtype = torch.int8 self.k_with_scale_buffer_dtype = torch.int8
self._create_buffers() self._create_buffers()
@@ -120,6 +196,9 @@ class DeepSeekV4SingleKVPool(KVCache):
] ]
def get_bytes_per_token(self) -> int: def get_bytes_per_token(self) -> int:
if self.kv_layout is not KVLayout.V4:
assert self.qk_nope_head_dim + self.qk_rope_head_dim == 512
return self.kv_layout.bytes_per_token
dim_per_token = ( dim_per_token = (
self.qk_nope_head_dim self.qk_nope_head_dim
+ self.qk_rope_head_dim * self.rope_storage_dtype.itemsize + self.qk_rope_head_dim * self.rope_storage_dtype.itemsize
@@ -131,13 +210,17 @@ class DeepSeekV4SingleKVPool(KVCache):
def create_buffer(self, *, num_pages: int): def create_buffer(self, *, num_pages: int):
bytes_per_token = self.get_bytes_per_token() bytes_per_token = self.get_bytes_per_token()
self.kv_cache_total_dim = bytes_per_token self.kv_cache_total_dim = bytes_per_token
bytes_per_page_non_padded = self.page_size * bytes_per_token self.bytes_per_page_padded = self.kv_layout.page_bytes(self.page_size)
self.bytes_per_page_padded = ceil_div(bytes_per_page_non_padded, 576) * 576
if self.kv_layout is KVLayout.V4:
assert bytes_per_token == 448 + 64 * 2 + 8, ( assert bytes_per_token == 448 + 64 * 2 + 8, (
"DSV4 KV layout: qk_nope_head_dim FP8 (448) + qk_rope_head_dim BF16 " "DSV4 KV layout: qk_nope_head_dim FP8 (448) + qk_rope_head_dim BF16 "
"(64*2) + nope FP8 scales + scale_pad = 584 bytes/token" "(64*2) + nope FP8 scales + scale_pad = 584 bytes/token"
) )
assert (
self.bytes_per_page_padded
== ceil_div(self.page_size * bytes_per_token, 576) * 576
)
assert self.store_dtype == torch.uint8 assert self.store_dtype == torch.uint8
return torch.zeros( return torch.zeros(
@@ -153,6 +236,10 @@ class DeepSeekV4SingleKVPool(KVCache):
loc: torch.Tensor, loc: torch.Tensor,
cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack, cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
): ):
assert self.kv_layout is KVLayout.V4, (
"the (fp8 nope, bf16 rope, 7 scales) pack is the V4 layout; "
f"a {self.kv_layout.value} pool is written through set_key_buffer_fused"
)
dsv4_index_buf_accessor.SetKAndS.execute( dsv4_index_buf_accessor.SetKAndS.execute(
pool=self, pool=self,
buf=self.kv_buffer[layer_id], buf=self.kv_buffer[layer_id],
@@ -165,13 +252,19 @@ class DeepSeekV4SingleKVPool(KVCache):
layer_id: int, layer_id: int,
loc: torch.Tensor, loc: torch.Tensor,
cache_k: torch.Tensor, cache_k: torch.Tensor,
freqs_cis: Optional[torch.Tensor] = None,
) -> None: ) -> None:
"""Quantize ``cache_k`` ``[n, 512]`` bf16 into this pool's layout at ``loc``.
``freqs_cis`` (V4.1 only) rotates the RoPE tail in-kernel, so the input is
the un-rotated latent and the fp4 / fp8 rounding happens once."""
return fused_store_cache( return fused_store_cache(
input=cache_k, input=cache_k,
cache=self.kv_buffer[layer_id], cache=self.kv_buffer[layer_id],
indices=loc, indices=loc,
page_size=self.page_size, page_size=self.page_size,
type="flashmla", type="flashmla",
layout=self.kv_layout,
freqs_cis=freqs_cis,
) )
def get_key_buffer(self, layer_id: int): def get_key_buffer(self, layer_id: int):
@@ -235,12 +328,14 @@ class DeepSeekV4UniformFP8KVPool(DeepSeekV4SingleKVPool):
layer_id: int, layer_id: int,
loc: torch.Tensor, loc: torch.Tensor,
cache_k: torch.Tensor, cache_k: torch.Tensor,
freqs_cis: Optional[torch.Tensor] = None,
) -> None: ) -> None:
"""Store normed/roped rows as e4m3 with the backend's fixed unit scale. """Store normed/roped rows as e4m3 with the backend's fixed unit scale.
uint8 views work around index_put not supporting FP8 dtypes. uint8 views work around index_put not supporting FP8 dtypes.
""" """
assert freqs_cis is None, "the uniform-FP8 pool takes finished (rotated) rows"
assert cache_k.dim() == 2 and cache_k.shape[1] == self.kv_cache_total_dim assert cache_k.dim() == 2 and cache_k.shape[1] == self.kv_cache_total_dim
self.kv_buffer[layer_id].view(torch.uint8).view(-1, self.kv_cache_total_dim)[ self.kv_buffer[layer_id].view(torch.uint8).view(-1, self.kv_cache_total_dim)[
loc.long() loc.long()
@@ -260,6 +355,7 @@ class HiSparseC4DevicePool(DeepSeekV4SingleKVPool):
enable_memory_saver: bool, enable_memory_saver: bool,
start_layer: int | None = None, start_layer: int | None = None,
end_layer: int | None = None, end_layer: int | None = None,
kv_layout: Union[str, KVLayout] = KVLayout.V4,
): ):
super().__init__( super().__init__(
size, size,
@@ -272,6 +368,11 @@ class HiSparseC4DevicePool(DeepSeekV4SingleKVPool):
enable_memory_saver, enable_memory_saver,
start_layer, start_layer,
end_layer, end_layer,
kv_layout=kv_layout,
)
# The HiSparse transfer kernels hardcode the V4 token layout.
assert self.kv_layout is KVLayout.V4, (
f"HiSparse C4 pools support the V4 layout only, got {self.kv_layout}"
) )
self.data_ptrs = torch.tensor( self.data_ptrs = torch.tensor(
@@ -318,9 +419,10 @@ class HiSparseC4DevicePool(DeepSeekV4SingleKVPool):
layer_id: int, layer_id: int,
loc: torch.Tensor, loc: torch.Tensor,
cache_k: torch.Tensor, cache_k: torch.Tensor,
freqs_cis: Optional[torch.Tensor] = None,
) -> None: ) -> None:
loc = self.translate_loc_to_hisparse_device(loc) loc = self.translate_loc_to_hisparse_device(loc)
return super().set_key_buffer_fused(layer_id, loc, cache_k) return super().set_key_buffer_fused(layer_id, loc, cache_k, freqs_cis)
def get_cpu_copy(self, indices, mamba_indices=None, req_pool_index=None): def get_cpu_copy(self, indices, mamba_indices=None, req_pool_index=None):
raise NotImplementedError("HiSparseC4DevicePool does not support get_cpu_copy") raise NotImplementedError("HiSparseC4DevicePool does not support get_cpu_copy")
@@ -331,6 +433,21 @@ class HiSparseC4DevicePool(DeepSeekV4SingleKVPool):
raise NotImplementedError("HiSparseC4DevicePool does not support load_cpu_copy") raise NotImplementedError("HiSparseC4DevicePool does not support load_cpu_copy")
# Low-ratio indexer-K pool page, in compressed slots: the DeepGEMM indexer reads
# K in blocks of at most 128 and sglang's JIT metadata builder asserts 64.
def dsv41_index_page_size() -> int:
from sglang.srt.layers.deep_gemm_wrapper.configurer import (
DEEPGEMM_PAGED_SPARSE_MQA_LOGITS,
)
if DEEPGEMM_PAGED_SPARSE_MQA_LOGITS:
return 128
return 64
DSV41_INDEX_PAGE_SIZE = dsv41_index_page_size()
class DeepSeekV4IndexerPool(KVCache): class DeepSeekV4IndexerPool(KVCache):
quant_block_size = 128 quant_block_size = 128
index_k_with_scale_buffer_dtype = torch.uint8 index_k_with_scale_buffer_dtype = torch.uint8
@@ -346,6 +463,7 @@ class DeepSeekV4IndexerPool(KVCache):
enable_memory_saver: bool, enable_memory_saver: bool,
start_layer: Optional[int] = None, start_layer: Optional[int] = None,
end_layer: Optional[int] = None, end_layer: Optional[int] = None,
use_fp4_indexer: Optional[bool] = None,
): ):
super().__init__( super().__init__(
size, size,
@@ -358,8 +476,12 @@ class DeepSeekV4IndexerPool(KVCache):
end_layer, end_layer,
) )
self.index_head_dim = index_head_dim self.index_head_dim = index_head_dim
self.use_fp4_indexer = get_exec().kernel.enable_deepseek_v4_fp4_indexer if use_fp4_indexer is None:
use_fp4_indexer = get_exec().kernel.enable_deepseek_v4_fp4_indexer
self.use_fp4_indexer = use_fp4_indexer
self.uses_aiter_fp4_layout = _is_hip and self.use_fp4_indexer self.uses_aiter_fp4_layout = _is_hip and self.use_fp4_indexer
# Low-ratio pools round to nearest even; c4 keeps threshold rounding.
self.index_k_rne = False
self._create_buffer() self._create_buffer()
@@ -500,8 +622,54 @@ class DeepSeekV4IndexerPool(KVCache):
cache=self.index_k_with_scale_buffer[layer_id - self.start_layer], cache=self.index_k_with_scale_buffer[layer_id - self.start_layer],
loc=loc, loc=loc,
page_size=self.page_size, page_size=self.page_size,
rne=self.index_k_rne,
) )
def get_index_k_fp4(
self, layer_id: int, slots: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Packed fp4 rows at `slots`: (payload int8 [n, 64], scales int32 [n]),
from the page layout [page_size * 64 payload | page_size * 4 scale]."""
assert self.use_fp4_indexer, "packed readback only applies to the fp4 layout"
buf = self.index_k_with_scale_buffer[layer_id - self.start_layer]
slots = slots.to(torch.int64)
p = self.page_size
page, off = (slots // p).unsqueeze(-1), slots % p
payload_cols = (off * 64).unsqueeze(-1) + torch.arange(64, device=buf.device)
scale_cols = (p * 64 + off * 4).unsqueeze(-1) + torch.arange(
4, device=buf.device
)
payload = buf[page, payload_cols].view(torch.int8) # [n, 64]
scales = buf[page, scale_cols].contiguous().view(torch.int32).squeeze(-1)
return payload, scales
def get_index_k_dequant(
self, layer_id: int, slots: Optional[torch.Tensor] = None
) -> torch.Tensor:
"""Dequantized bf16 [n, index_head_dim] index K; `slots` None reads the pool."""
from sglang.srt.layers.quantization.fp8 import DSV4_DEQUANT_FP4_TABLE
assert self.use_fp4_indexer, "dequant readback only applies to the fp4 layout"
buf = self.index_k_with_scale_buffer[layer_id - self.start_layer]
if slots is None:
slots = torch.arange(self.size, device=buf.device)
slots = slots.to(torch.int64)
# Page layout: see get_index_k_fp4.
p = self.page_size
page, off = (slots // p).unsqueeze(-1), slots % p
payload_cols = (off * 64).unsqueeze(-1) + torch.arange(64, device=buf.device)
scale_cols = (p * 64 + off * 4).unsqueeze(-1) + torch.arange(
4, device=buf.device
)
u = buf[page, payload_cols].view(torch.uint8) # [n, 64]
codes = torch.stack([u & 0x0F, (u >> 4) & 0x0F], dim=-1) # [n, 64, 2]
vals = DSV4_DEQUANT_FP4_TABLE.to(buf.device)[codes.long()].flatten(
1
) # [n, 128]
exps = buf[page, scale_cols].to(torch.int32) & 0xFF # [n, 4]
scales = torch.exp2(exps.float() - 127).repeat_interleave(32, dim=-1)
return (vals * scales).to(torch.bfloat16)
class _CompressedPoolConfig(NamedTuple): class _CompressedPoolConfig(NamedTuple):
kv_size: int kv_size: int
@@ -511,7 +679,9 @@ class _CompressedPoolConfig(NamedTuple):
class DeepSeekV4LayerItem(NamedTuple): class DeepSeekV4LayerItem(NamedTuple):
compress_ratio: int compress_ratio: Literal[0, 1, 2, 4, 128]
# Layer index inside compress_kv_pool. Ratios 1/2 share a pool layer across the
# kv_source layer that writes it and the layers that read it.
compress_layer_id: int compress_layer_id: int
compress_kv_pool: Optional[DeepSeekV4SingleKVPool] = None compress_kv_pool: Optional[DeepSeekV4SingleKVPool] = None
@@ -683,6 +853,11 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
enable_hisparse: bool = False, enable_hisparse: bool = False,
online_mtp_max_draft_tokens: int = 0, online_mtp_max_draft_tokens: int = 0,
num_req_slots: Optional[int] = None, num_req_slots: Optional[int] = None,
kv_source_layers: Sequence[int] = (),
full_size: Optional[int] = None,
is_draft_worker: bool = False,
kv_layout: Union[str, KVLayout] = KVLayout.V4,
compressed_kv_layout: Optional[str] = None,
): ):
super().__init__( super().__init__(
swa_size, swa_size,
@@ -694,6 +869,14 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
start_layer, start_layer,
end_layer, end_layer,
) )
# Layout of the SWA (main) cache; compressed caches follow
# resolve_compressed_kv_layout, so valid (main, extra) pairs form only here.
self.kv_layout = KVLayout.parse(kv_layout)
assert self.kv_layout in (
KVLayout.V4,
KVLayout.V41,
), f"{self.kv_layout} is only valid for a compressed (extra) cache"
self.compressed_kv_layout_option = compressed_kv_layout
c4_logical_size = c128_size * 32 c4_logical_size = c128_size * 32
logger.info( logger.info(
@@ -723,6 +906,11 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
self.uniform_fp8 = ( self.uniform_fp8 = (
not self._unified_kv not self._unified_kv
) and get_exec().kernel.dsv4_attn_backend == "trtllm" ) and get_exec().kernel.dsv4_attn_backend == "trtllm"
if self.uniform_fp8:
assert self.kv_layout is KVLayout.V4, (
"--dsv4-attn-backend trtllm keeps its own uniform 512-byte pages; "
f"it cannot be combined with SGLANG_DSV4_KV_LAYOUT={self.kv_layout.value}"
)
c4_ring_size = self.get_ring_size(4) c4_ring_size = self.get_ring_size(4)
if self._unified_kv: if self._unified_kv:
# Unified C4 state is request-addressed: one ring per req slot, # Unified C4 state is request-addressed: one ring per req slot,
@@ -738,7 +926,13 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
c128_state_pool_size = max( c128_state_pool_size = max(
c128_state_pool_size, self.num_req_slots * c128_ring_size c128_state_pool_size, self.num_req_slots * c128_ring_size
) )
# Only the ratios the model has anywhere get a pool config: the backend, PD
# state transfer and HiCache read the registries as "the ratios this model
# has", and a PP stage missing one keeps its empty pool so the PD wire aligns.
model_ratios = set(compression_ratios)
self.compressed_pool_configs = { self.compressed_pool_configs = {
ratio: config
for ratio, config in {
4: _CompressedPoolConfig( 4: _CompressedPoolConfig(
kv_size=c4_size, kv_size=c4_size,
state_size=c4_state_pool_size, state_size=c4_state_pool_size,
@@ -750,6 +944,8 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
state_size=c128_state_pool_size, state_size=c128_state_pool_size,
state_dtype=c128_state_dtype, state_dtype=c128_state_dtype,
), ),
}.items()
if ratio in model_ratios
} }
self.compression_ratios = compression_ratios self.compression_ratios = compression_ratios
self.online_mtp_max_draft_tokens = online_mtp_max_draft_tokens self.online_mtp_max_draft_tokens = online_mtp_max_draft_tokens
@@ -786,7 +982,51 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
stage_layer_num = len(stage_ratios) stage_layer_num = len(stage_ratios)
kv_pool_cls: type = DeepSeekV4SingleKVPool kv_pool_cls: type = DeepSeekV4SingleKVPool
if self._unified_kv: self.request_window = None
encoder_replay = get_exec().features.enable_encoder_swa_bounded_replay
# DSpark's draft shares the target's full-to-SWA mapping, so the target
# keeps its paged SWA allocator even under encoder replay.
self.needs_paged_swa_allocator = (
not encoder_replay
or is_draft_worker
or get_spec().speculative_algorithm is not None
)
if encoder_replay and not is_draft_worker:
from sglang.srt.mem_cache.dsv41_request_window import RequestWindow
def make_window_pool(size, layers):
return self._make_kv_pool(
size=size,
page_size=swa_page_size,
dtype=dtype,
layer_num=layers,
device=device,
enable_memory_saver=enable_memory_saver,
global_page_size=swa_page_size,
kv_layout=self.kv_layout,
)
self.swa_kv_pool = None
self.unified_kv_pool = None
from sglang.srt.runtime_context import get_schedule
chunk = get_schedule().chunked_prefill_size or 0
self.request_window = RequestWindow(
make_window_pool,
num_slots=self.num_req_slots,
layers=stage_layer_num,
page_size=swa_page_size,
capacity=self.sliding_window + (online_mtp_max_draft_tokens or 0),
workspace_rows=(self.num_req_slots + 1) * self.sliding_window
+ max(
chunk,
(self.num_req_slots + 1) * (1 + (online_mtp_max_draft_tokens or 0)),
),
)
elif self._unified_kv:
assert self.kv_layout is KVLayout.V4, (
"unified_kv keeps bf16 rows, not a paged FlashMLA layout"
)
self.swa_kv_pool = None self.swa_kv_pool = None
swa_ring_size = get_swa_ring_size( swa_ring_size = get_swa_ring_size(
self.sliding_window, get_spec().speculative_algorithm is not None self.sliding_window, get_spec().speculative_algorithm is not None
@@ -825,9 +1065,19 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
device=device, device=device,
enable_memory_saver=enable_memory_saver, enable_memory_saver=enable_memory_saver,
global_page_size=swa_page_size, global_page_size=swa_page_size,
kv_layout=self.kv_layout,
cls=kv_pool_cls, cls=kv_pool_cls,
) )
logger.info(
"DSV4 SWA storage: worker=%s, storage=%s, paged_allocator=%s",
"draft" if is_draft_worker else "target",
"request_window" if self.request_window is not None else "paged",
self.needs_paged_swa_allocator,
)
self.full_size = full_size
self.kv_source_layers = list(kv_source_layers)
self.sources_by_ratio = self._collect_sources_by_ratio()
self._init_compressed_pools( self._init_compressed_pools(
stage_ratios=stage_ratios, stage_ratios=stage_ratios,
page_size=page_size, page_size=page_size,
@@ -863,8 +1113,12 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
self.full_to_swa_index_mapping = full_to_swa_index_mapping self.full_to_swa_index_mapping = full_to_swa_index_mapping
def get_ring_size(self, compress_ratio: int) -> int: def get_ring_size(self, compress_ratio: int) -> int:
is_speculative = get_spec().speculative_algorithm is not None spec = get_spec()
return get_compress_state_ring_size(compress_ratio, is_speculative) return get_compress_state_ring_size(
compress_ratio,
spec.speculative_algorithm is not None,
spec.speculative_num_draft_tokens or 0,
)
def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor): def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor):
assert self.full_to_swa_index_mapping is not None assert self.full_to_swa_index_mapping is not None
@@ -912,14 +1166,31 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
data_ptrs.append(buf.data_ptr() + swa_pages * row_bytes) data_ptrs.append(buf.data_ptr() + swa_pages * row_bytes)
data_lens.append(compress_rows * row_bytes) data_lens.append(compress_rows * row_bytes)
item_lens.append(rows_per_page * row_bytes) item_lens.append(rows_per_page * row_bytes)
else: elif kv_pool is not None:
for buf in kv_pool.kv_buffer: for buf in kv_pool.kv_buffer:
append_page_buffer(buf) append_page_buffer(buf)
indexer_pool = self.index_pools.get(ratio) indexer_pool = self.index_pools.get(ratio)
if indexer_pool is not None: if indexer_pool is None:
continue
# The transfer addresses every buffer by FULL page id; ratio-1/2 index
# pools page at DSV41_INDEX_PAGE_SIZE, so one item is the run of index
# pages holding a FULL page's page_size // ratio slots.
index_pages_per_full_page = 1
if ratio in (1, 2):
slots_per_full_page = self.page_size // ratio
assert slots_per_full_page % indexer_pool.page_size == 0, (
f"ratio-{ratio} index pages of {indexer_pool.page_size} slots do not "
f"tile a FULL page of {slots_per_full_page} slots"
)
index_pages_per_full_page = (
slots_per_full_page // indexer_pool.page_size
)
for buf in indexer_pool.contiguous_page_row_buffers(): for buf in indexer_pool.contiguous_page_row_buffers():
append_page_buffer(buf) assert buf.ndim == 2, f"expected 2D buffer, got {buf.ndim}D"
data_ptrs.append(buf.data_ptr())
data_lens.append(buf.nbytes)
item_lens.append(buf[0].nbytes * index_pages_per_full_page)
return data_ptrs, data_lens, item_lens return data_ptrs, data_lens, item_lens
@@ -1011,7 +1282,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
data_lens: List[int] = [] data_lens: List[int] = []
item_lens: List[int] = [] item_lens: List[int] = []
if not self._unified_kv: if self.swa_kv_pool is not None:
for buf in self.swa_kv_pool.kv_buffer: for buf in self.swa_kv_pool.kv_buffer:
assert buf.ndim == 2, f"expected 2D buffer, got {buf.ndim}D" assert buf.ndim == 2, f"expected 2D buffer, got {buf.ndim}D"
data_ptrs.append(buf.data_ptr()) data_ptrs.append(buf.data_ptr())
@@ -1023,6 +1294,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
self.indexer_compress_state_pools, self.indexer_compress_state_pools,
]: ]:
for pool in pools: for pool in pools:
# Request-scoped state ships as C128_STATE, not with the SWA ring.
if pool is None or pool.request_scoped: if pool is None or pool.request_scoped:
continue continue
t = pool.kv_score_buffer.kv_score t = pool.kv_score_buffer.kv_score
@@ -1036,6 +1308,8 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
def get_request_state_buf_infos( def get_request_state_buf_infos(
self, self,
) -> Tuple[List[int], List[int], List[int]]: ) -> Tuple[List[int], List[int], List[int]]:
"""Request-scoped state: the c128 raw-token ring (or its single online row)
and the ratio-2 pending-pair ring. One item is one c128 page / pair ring."""
data_ptrs: List[int] = [] data_ptrs: List[int] = []
data_lens: List[int] = [] data_lens: List[int] = []
item_lens: List[int] = [] item_lens: List[int] = []
@@ -1046,6 +1320,9 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
assert t.ndim == 2, f"expected 2D buffer, got {t.ndim}D" assert t.ndim == 2, f"expected 2D buffer, got {t.ndim}D"
data_ptrs.append(t.data_ptr()) data_ptrs.append(t.data_ptr())
data_lens.append(t.nbytes) data_lens.append(t.nbytes)
if pool.ratio == 2:
item_lens.append(t[0].nbytes * pool.ring_size)
else:
item_lens.append(t[0].nbytes if ONLINE_C128 else t[0].nbytes * 128) item_lens.append(t[0].nbytes if ONLINE_C128 else t[0].nbytes * 128)
return data_ptrs, data_lens, item_lens return data_ptrs, data_lens, item_lens
@@ -1060,12 +1337,25 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
enable_hisparse: bool, enable_hisparse: bool,
kv_pool_cls: type, kv_pool_cls: type,
) -> None: ) -> None:
"""One KV pool (plus packed indexer-K pool) per compress ratio in this stage:
slot = full-pool loc // ratio, page = page_size // ratio, so pages line up."""
configs = self.compressed_pool_configs configs = self.compressed_pool_configs
layer_counts = {ratio: stage_ratios.count(ratio) for ratio in configs} layer_counts = {ratio: stage_ratios.count(ratio) for ratio in configs}
# Keep empty pools and allocation order for PP stages without a given ratio. # Keep empty pools and allocation order for PP stages without a given ratio.
self.kv_pools: dict[int, Optional[DeepSeekV4SingleKVPool]] = { self.kv_pools: dict[int, Optional[DeepSeekV4SingleKVPool]] = {
ratio: None for ratio in configs ratio: None for ratio in configs
} }
# The PD wire order stays C4, C128, then the ratio-1/2 kv_source layers.
low_ratio_sources = {
ratio: sources
for ratio, sources in getattr(self, "sources_by_ratio", {}).items()
if ratio in (1, 2)
}
if low_ratio_sources:
assert self.full_size is not None, (
"low compress ratios need the full pool size"
)
assert not self._unified_kv, "unified_kv has no low compress ratio layout"
if not self._unified_kv: if not self._unified_kv:
for ratio, config in configs.items(): for ratio, config in configs.items():
@@ -1084,6 +1374,19 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
enable_memory_saver=enable_memory_saver, enable_memory_saver=enable_memory_saver,
global_page_size=page_size, global_page_size=page_size,
cls=pool_cls, cls=pool_cls,
kv_layout=self.compressed_kv_layout(ratio),
)
for ratio, sources in low_ratio_sources.items():
self.kv_pools[ratio] = self._make_kv_pool(
size=self.full_size // ratio,
page_size=page_size // ratio,
dtype=dtype,
layer_num=len(sources),
device=device,
enable_memory_saver=enable_memory_saver,
global_page_size=page_size,
cls=kv_pool_cls,
kv_layout=self.compressed_kv_layout(ratio),
) )
self.index_pools: dict[int, DeepSeekV4IndexerPool] = { self.index_pools: dict[int, DeepSeekV4IndexerPool] = {
@@ -1099,11 +1402,24 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
for ratio, config in configs.items() for ratio, config in configs.items()
if config.indexer_size is not None if config.indexer_size is not None
} }
for ratio, sources in low_ratio_sources.items():
# Reserved FULL page 0 pushes real slots past full_size, and one index
# padding page is too small to cover that gap.
self.index_pools[ratio] = self._make_indexer_pool(
(self.full_size + page_size) // ratio,
DSV41_INDEX_PAGE_SIZE,
dtype,
self.indexer_head_dim,
len(sources),
device,
enable_memory_saver,
force_fp4=True,
)
# HiCache and hardware backends still access the per-ratio attributes. # HiCache and hardware backends still read these per-ratio attributes.
self.c4_kv_pool = self.kv_pools[4] self.c4_kv_pool = self.kv_pools.get(4)
self.c128_kv_pool = self.kv_pools[128] self.c128_kv_pool = self.kv_pools.get(128)
self.c4_indexer_kv_pool = self.index_pools[4] self.c4_indexer_kv_pool = self.index_pools.get(4)
def _make_kv_pool( def _make_kv_pool(
self, self,
@@ -1116,6 +1432,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
enable_memory_saver: bool, enable_memory_saver: bool,
global_page_size: int, global_page_size: int,
cls: type = DeepSeekV4SingleKVPool, cls: type = DeepSeekV4SingleKVPool,
kv_layout: KVLayout = KVLayout.V4,
) -> DeepSeekV4SingleKVPool: ) -> DeepSeekV4SingleKVPool:
"""Build a full / SWA / c4 / c128 single-KV pool. ``global_page_size`` """Build a full / SWA / c4 / c128 single-KV pool. ``global_page_size``
is the model-wide page_size (== ``page_size`` for the SWA pool, larger is the model-wide page_size (== ``page_size`` for the SWA pool, larger
@@ -1132,8 +1449,17 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
layer_num, layer_num,
device, device,
enable_memory_saver, enable_memory_saver,
kv_layout=kv_layout,
) )
def compressed_kv_layout(self, compress_ratio: int) -> KVLayout:
"""See :func:`resolve_compressed_kv_layout`."""
layout = resolve_compressed_kv_layout(
self.kv_layout, compress_ratio, self.compressed_kv_layout_option
)
assert is_valid_kv_layout_pair(self.kv_layout, layout)
return layout
def _make_indexer_pool( def _make_indexer_pool(
self, self,
size: int, size: int,
@@ -1143,10 +1469,25 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
layer_num: int, layer_num: int,
device: str, device: str,
enable_memory_saver: bool, enable_memory_saver: bool,
force_fp4: bool = False,
) -> DeepSeekV4IndexerPool: ) -> DeepSeekV4IndexerPool:
"""Build the c4 lightning-indexer K pool (packed CUDA layout). """Build the c4 lightning-indexer K pool (packed CUDA layout).
Overridden by :class:`DSV4NPUTokenToKVPool` to swap in the Overridden by :class:`DSV4NPUTokenToKVPool` to swap in the
dedicated-buffer NPU variant (int8 K + fp16 scale).""" dedicated-buffer NPU variant. ``force_fp4`` forces the fp4 low-ratio layout."""
if force_fp4:
pool = DeepSeekV4IndexerPool(
size,
page_size,
dtype,
index_head_dim,
layer_num,
device,
enable_memory_saver,
use_fp4_indexer=True,
)
# The dsv41 low-ratio indexer rounds to nearest even (reference rounding).
pool.index_k_rne = True
return pool
return DeepSeekV4IndexerPool( return DeepSeekV4IndexerPool(
size, size,
page_size, page_size,
@@ -1172,13 +1513,30 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
enable_memory_saver=enable_memory_saver, enable_memory_saver=enable_memory_saver,
ratio=ratio, ratio=ratio,
online=(ratio == 128 and ONLINE_C128), online=(ratio == 128 and ONLINE_C128),
request_scoped=ratio == 128, request_scoped=ratio in (2, 128),
swa_page_size=self.swa_page_size, swa_page_size=self.swa_page_size,
online_mtp_max_draft_tokens=( online_mtp_max_draft_tokens=(
self.online_mtp_max_draft_tokens if ratio == 128 else 0 self.online_mtp_max_draft_tokens if ratio == 128 else 0
), ),
) )
def _make_pair_state_pool(self, enable_memory_saver: bool) -> CompressStatePool:
"""Ratio-2 pending-pair state: one position ring per request slot, holding
the fp32 (kv, score) of an even token until its odd partner arrives."""
ring_size = self.get_ring_size(2)
return CompressStatePool(
size=self.num_req_slots * ring_size,
ring_size=ring_size,
overlap=False,
head_dim=self.qk_nope_head_dim + self.qk_rope_head_dim,
dtype=torch.float32,
device=self.device,
enable_memory_saver=enable_memory_saver,
ratio=2,
request_scoped=True,
online=False,
)
def _init_paged_compress_states(self, enable_memory_saver: bool): def _init_paged_compress_states(self, enable_memory_saver: bool):
total_L = len(self.compression_ratios) total_L = len(self.compression_ratios)
self.compress_state_pools: List[Optional[CompressStatePool]] = [None] * total_L self.compress_state_pools: List[Optional[CompressStatePool]] = [None] * total_L
@@ -1188,7 +1546,15 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
for idx in range(self._stage_start, self._stage_end): for idx in range(self._stage_start, self._stage_end):
ratio = self.compression_ratios[idx] ratio = self.compression_ratios[idx]
if ratio == 0: if ratio in (0, 1):
continue
if ratio == 2:
# Only a kv_source layer compresses; later ratio-2 layers read it.
if idx in self.sources_by_ratio.get(2, []):
self.compress_state_pools[idx] = self._make_pair_state_pool(
enable_memory_saver
)
continue continue
self.compress_state_pools[idx] = self._make_compress_state_pool( self.compress_state_pools[idx] = self._make_compress_state_pool(
@@ -1204,6 +1570,36 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
enable_memory_saver=enable_memory_saver, enable_memory_saver=enable_memory_saver,
) )
def _collect_sources_by_ratio(self) -> dict[int, List[int]]:
"""Layers owning compressed storage: all of ratios 4/128, kv_sources of 1/2."""
stage = range(self._stage_start, self._stage_end)
for idx in stage:
ratio = self.compression_ratios[idx]
if ratio not in (0, 1, 2, 4, 128):
raise ValueError(f"Unsupported compression ratio: {ratio}")
sources_by_ratio: dict[int, List[int]] = {}
for ratio in (4, 128, 1, 2):
if ratio in (1, 2):
layers = [
l
for l in self.kv_source_layers
if l in stage and self.compression_ratios[l] == ratio
]
else:
layers = [l for l in stage if self.compression_ratios[l] == ratio]
if layers:
sources_by_ratio[ratio] = layers
return sources_by_ratio
def source_layer_of(self, layer_id: int) -> int:
"""The layer owning this layer's compressed storage: itself for ratios 4/128,
the nearest preceding kv_source layer for ratios 1/2."""
ratio = self.compression_ratios[layer_id]
sources = [l for l in self.sources_by_ratio[ratio] if l <= layer_id]
assert sources, f"layer {layer_id} (ratio {ratio}) has no kv_source layer"
return max(sources)
def _init_compressed_layer_mapping(self): def _init_compressed_layer_mapping(self):
layer_counts = {0: 0, **{ratio: 0 for ratio in self.kv_pools}} layer_counts = {0: 0, **{ratio: 0 for ratio in self.kv_pools}}
total_L = len(self.compression_ratios) total_L = len(self.compression_ratios)
@@ -1213,12 +1609,17 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
ratio = self.compression_ratios[idx] ratio = self.compression_ratios[idx]
if ratio not in layer_counts: if ratio not in layer_counts:
raise ValueError(f"Unsupported compression ratio: {ratio}") raise ValueError(f"Unsupported compression ratio: {ratio}")
if ratio in (1, 2):
sources = self.sources_by_ratio[ratio]
compress_layer_id = sources.index(self.source_layer_of(idx))
else:
compress_layer_id = layer_counts[ratio]
layer_counts[ratio] += 1
self.layer_mapping[idx] = DeepSeekV4LayerItem( self.layer_mapping[idx] = DeepSeekV4LayerItem(
compress_ratio=ratio, compress_ratio=ratio,
compress_layer_id=layer_counts[ratio], compress_layer_id=compress_layer_id,
compress_kv_pool=self.kv_pools.get(ratio), compress_kv_pool=self.kv_pools.get(ratio),
) )
layer_counts[ratio] += 1
def wait_layer_transfer(self, layer_id: int) -> None: def wait_layer_transfer(self, layer_id: int) -> None:
if self.layer_transfer_counter is not None: if self.layer_transfer_counter is not None:
@@ -1228,7 +1629,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
self.wait_layer_transfer(layer_id) self.wait_layer_transfer(layer_id)
compress_state_pool = self.compress_state_pools[layer_id] compress_state_pool = self.compress_state_pools[layer_id]
assert compress_state_pool is not None, ( assert compress_state_pool is not None, (
"Only c4/c128 layers have attention states." "Only c4/c128 layers and ratio-2 kv_source layers have attention states."
) )
return compress_state_pool return compress_state_pool
@@ -1292,23 +1693,20 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
return pools[0].transfer_indices(req_pool_idx, seq_len) return pools[0].transfer_indices(req_pool_idx, seq_len)
def clear_request_scoped_state(self, req_pool_idx: int) -> None: def clear_request_scoped_state(self, req_pool_idx: int) -> None:
"""Reset request-scoped state for one req slot.""" """Reset one req slot's C128 ring and ratio-2 pending-pair state."""
for pool in self.compress_state_pools: for pool in self.compress_state_pools:
if pool is None or not pool.request_scoped: if pool is None or not pool.request_scoped:
continue continue
state = pool.kv_score_buffer.kv_score if pool.ratio == 128 and ONLINE_C128:
if ONLINE_C128: row = pool.kv_score_buffer.kv_score[req_pool_idx]
row = state[req_pool_idx]
head_dim = row.shape[-1] // 3 head_dim = row.shape[-1] // 3
row[:head_dim].fill_(float("-inf")) row[:head_dim].fill_(float("-inf"))
row[head_dim:].zero_() row[head_dim:].zero_()
else: continue
start = req_pool_idx * pool.ring_size start = req_pool_idx * pool.ring_size
rows = state[start : start + pool.ring_size] pool.kv_score_buffer[start : start + pool.ring_size].clear()
half = rows.shape[-1] // 2
rows[:, :half].zero_()
rows[:, half:].fill_(float("-inf"))
def clear_unaccepted_c128_draft_states( def clear_unaccepted_c128_draft_states(
self, self,
@@ -1349,8 +1747,40 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
return layer_id - self._stage_start return layer_id - self._stage_start
def get_swa_raw_buffer(self, layer_id: int) -> torch.Tensor: def get_swa_raw_buffer(self, layer_id: int) -> torch.Tensor:
if self.request_window is not None:
return self.request_window.buffer(self._swa_local_layer_id(layer_id))
return self.swa_kv_pool.kv_buffer[self._swa_local_layer_id(layer_id)] return self.swa_kv_pool.kv_buffer[self._swa_local_layer_id(layer_id)]
def get_swa_key_buffer(self, layer_id: int) -> torch.Tensor:
self.wait_layer_transfer(layer_id)
if self.request_window is not None:
return self.get_swa_raw_buffer(layer_id).view(
self.request_window.state.dtype
)
return self.swa_kv_pool.get_key_buffer(self._swa_local_layer_id(layer_id))
def set_swa_key_buffer(
self,
layer_id: int,
loc: torch.Tensor,
cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
) -> None:
assert self.kv_layout is KVLayout.V4, (
"the (fp8 nope, bf16 rope, 7 scales) pack is the V4 layout; "
f"a {self.kv_layout.value} pool is written through the fused setters"
)
if self.request_window is not None:
dsv4_index_buf_accessor.SetKAndS.execute(
pool=self.request_window.state,
buf=self.get_swa_raw_buffer(layer_id),
loc=loc,
nope_fp8_rope_bf16_pack=cache_nope_fp8_rope_bf16_pack,
)
else:
self.swa_kv_pool.set_key_buffer(
self._swa_local_layer_id(layer_id), loc, cache_nope_fp8_rope_bf16_pack
)
def get_extra_key_page_size(self, layer_id: int) -> int: def get_extra_key_page_size(self, layer_id: int) -> int:
_, _, compress_kv_pool = self.layer_mapping[layer_id] _, _, compress_kv_pool = self.layer_mapping[layer_id]
assert compress_kv_pool is not None assert compress_kv_pool is not None
@@ -1369,12 +1799,16 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
return compress_kv_pool.kv_cache_total_dim return compress_kv_pool.kv_cache_total_dim
def get_swa_key_layout(self) -> KVLayout: def get_swa_key_layout(self) -> KVLayout:
return self.swa_kv_pool.kv_layout # swa_kv_pool is None under the request window and unified_kv.
return self.kv_layout
def get_swa_key_bytes_per_token(self) -> int: def get_swa_key_bytes_per_token(self) -> int:
"""Last dim of the ``(pages, page_size, 1, bytes)`` view the attention """Last dim of the ``(pages, page_size, 1, bytes)`` view the attention
kernel detects the SWA cache's format from.""" kernel detects the SWA cache's format from."""
if self.uniform_fp8:
# The trtllm uniform-FP8 pool has no paged FlashMLA layout: 512 B/token.
return self.swa_kv_pool.kv_cache_total_dim return self.swa_kv_pool.kv_cache_total_dim
return self.kv_layout.bytes_per_token
def get_extra_key_buffer(self, layer_id: int) -> torch.Tensor | None: def get_extra_key_buffer(self, layer_id: int) -> torch.Tensor | None:
self.wait_layer_transfer(layer_id) self.wait_layer_transfer(layer_id)
@@ -1401,6 +1835,25 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
) )
return pool return pool
def get_low_ratio_index_k_dequant(
self, layer_id: int, slots: Optional[torch.Tensor] = None
) -> torch.Tensor:
"""Index-K rows at `slots` from the layer's latent source."""
compress_ratio, compress_layer_id, _ = self.layer_mapping[layer_id]
return self._indexer_pool(compress_ratio).get_index_k_dequant(
compress_layer_id, slots
)
def get_low_ratio_index_k_fp4(
self, layer_id: int, slots: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Packed fp4 index-K rows at `slots`: (payload int8 [n, 64], ue8m0 scales
packed int32 [n]), the input layout of quantize_fp4_indexer_tensor."""
compress_ratio, compress_layer_id, _ = self.layer_mapping[layer_id]
return self._indexer_pool(compress_ratio).get_index_k_fp4(
compress_layer_id, slots
)
def get_index_k_page_size(self, compress_ratio: int = 4) -> int: def get_index_k_page_size(self, compress_ratio: int = 4) -> int:
return self._indexer_pool(compress_ratio).page_size return self._indexer_pool(compress_ratio).page_size
@@ -1473,12 +1926,14 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
swa_loc: torch.Tensor, swa_loc: torch.Tensor,
cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack, cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
) -> None: ) -> None:
self.swa_kv_pool.set_key_buffer( self.set_swa_key_buffer(layer_id, swa_loc, cache_nope_fp8_rope_bf16_pack)
self._swa_local_layer_id(layer_id), swa_loc, cache_nope_fp8_rope_bf16_pack
)
def get_swa_key_buffer_radix(self, layer_id: int) -> torch.Tensor: def get_swa_key_buffer_radix(self, layer_id: int) -> torch.Tensor:
self.wait_layer_transfer(layer_id) self.wait_layer_transfer(layer_id)
if self.request_window is not None:
return self.get_swa_raw_buffer(layer_id).view(
self.request_window.state.dtype
)
return self.swa_kv_pool.get_key_buffer(self._swa_local_layer_id(layer_id)) return self.swa_kv_pool.get_key_buffer(self._swa_local_layer_id(layer_id))
def set_swa_key_buffer_radix_fused( def set_swa_key_buffer_radix_fused(
@@ -1487,8 +1942,13 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
swa_loc: torch.Tensor, swa_loc: torch.Tensor,
cache_k: torch.Tensor, cache_k: torch.Tensor,
) -> None: ) -> None:
return self.swa_kv_pool.set_key_buffer_fused( return fused_store_cache(
self._swa_local_layer_id(layer_id), swa_loc, cache_k input=cache_k,
cache=self.get_swa_raw_buffer(layer_id),
indices=swa_loc,
page_size=self.swa_page_size,
type="flashmla",
layout=self.kv_layout,
) )
def set_swa_key_buffer_radix_fused_norm_rope( def set_swa_key_buffer_radix_fused_norm_rope(
@@ -1528,8 +1988,9 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
freqs_cis=freqs_cis, freqs_cis=freqs_cis,
positions=positions, positions=positions,
out_loc=swa_loc, out_loc=swa_loc,
kvcache=self.swa_kv_pool.kv_buffer[self._swa_local_layer_id(layer_id)], kvcache=self.get_swa_raw_buffer(layer_id),
page_size=self.swa_kv_pool.page_size, page_size=self.swa_page_size,
layout=self.kv_layout,
) )
def set_unified_key_buffer_radix_fused_norm_rope( def set_unified_key_buffer_radix_fused_norm_rope(
@@ -1565,10 +2026,20 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
layer_id: int, layer_id: int,
loc: torch.Tensor, loc: torch.Tensor,
cache_k: torch.Tensor, cache_k: torch.Tensor,
freqs_cis: Optional[torch.Tensor] = None,
) -> None: ) -> None:
"""Write ``cache_k`` ``[n, 512]`` bf16 into the layer's compressed cache.
For an fp4 (``V41_FP4``) cache pass the *un-quantized* latent, plus
``freqs_cis`` if it is not rotated yet: the kernel rounds to e2m1 once."""
_, compress_layer_id, compress_kv_pool = self.layer_mapping[layer_id] _, compress_layer_id, compress_kv_pool = self.layer_mapping[layer_id]
assert compress_kv_pool is not None assert compress_kv_pool is not None
return compress_kv_pool.set_key_buffer_fused(compress_layer_id, loc, cache_k) if freqs_cis is not None:
assert compress_kv_pool.kv_layout is KVLayout.V41_FP4, (
"in-kernel RoPE is for the fp4 cache; fp8 caches take the finished value"
)
return compress_kv_pool.set_key_buffer_fused(
compress_layer_id, loc, cache_k, freqs_cis
)
def set_index_k_fused( def set_index_k_fused(
self, self,
@@ -0,0 +1,269 @@
from typing import Optional
import msgspec
import torch
from sglang.kernels.ops.attention.dsv4.kv_layout import KVLayout
from sglang.srt.model_executor.runner_utils.capture_mode import get_is_capture_mode
class WindowLayout(msgspec.Struct, frozen=True):
req: torch.Tensor
pos: torch.Tensor
write_loc: torch.Tensor
indices: torch.Tensor
lengths: torch.Tensor
history_req: torch.Tensor
history_pos: torch.Tensor
history_loc: torch.Tensor
history_valid: torch.Tensor
commit_mask: torch.Tensor
size: int
def copy_(self, other: "WindowLayout") -> None:
# Captured copy kernels read these tensors by address, so a graph replay
# must refresh their contents in place, not rebind the object.
assert self.size == other.size, (self.size, other.size)
self.req.copy_(other.req)
self.pos.copy_(other.pos)
self.write_loc.copy_(other.write_loc)
self.indices.copy_(other.indices)
self.lengths.copy_(other.lengths)
self.history_req.copy_(other.history_req)
self.history_pos.copy_(other.history_pos)
self.history_loc.copy_(other.history_loc)
self.history_valid.copy_(other.history_valid)
self.commit_mask.copy_(other.commit_mask)
def _first_row_offsets(
req: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
n = req.numel()
offset = torch.arange(n, device=req.device)
starts = torch.ones(n, dtype=torch.bool, device=req.device)
starts[1:] = req[1:] != req[:-1]
group = starts.cumsum(0) - 1
group_first = torch.cummax(torch.where(starts, offset, 0), dim=0).values
ends = torch.ones(n, dtype=torch.bool, device=req.device)
ends[:-1] = starts[1:]
group_last = torch.cummin(
torch.where(ends, offset, n - 1).flip(0), dim=0
).values.flip(0)
return group, group_first, group_last
def window_layout(
req,
pos,
*,
window: int = 128,
capacity: int = 256,
floor: Optional[torch.Tensor] = None,
num_groups: Optional[int] = None,
):
n = pos.numel()
if n == 0:
raise ValueError("request-window layout needs at least one query")
req = req.to(torch.int64)
pos = pos.to(torch.int64)
device = pos.device
groups = n if num_groups is None else int(num_groups)
offset = torch.arange(n, device=device)
group, group_first, group_last = _first_row_offsets(req)
first_pos = pos - (offset - group_first)
history_rows = groups * window
write_loc = (history_rows + offset).to(torch.int32)
lookback = torch.arange(window, device=device)
seen_pos = pos[:, None] - lookback
old = seen_pos < first_pos[:, None]
old_loc = group[:, None] * window + (seen_pos - (first_pos[:, None] - window))
new_loc = history_rows + group_first[:, None] + seen_pos - first_pos[:, None]
valid = seen_pos >= 0
if floor is not None:
floor = floor.to(torch.int64)
valid &= seen_pos >= floor[:, None]
indices = torch.where(valid, torch.where(old, old_loc, new_loc), -1).to(torch.int32)
lengths = valid.sum(-1).to(torch.int32)
g_req = torch.zeros(groups, dtype=torch.int64, device=device).scatter_(
0, group, req
)
g_first = torch.zeros(groups, dtype=torch.int64, device=device).scatter_(
0, group, first_pos
)
g_live = torch.zeros(groups, dtype=torch.bool, device=device).scatter_(
0, group, torch.ones_like(group, dtype=torch.bool)
)
history_pos = (g_first[:, None] - window + lookback[None, :]).flatten()
history_valid = (history_pos >= 0) & g_live.repeat_interleave(window)
if floor is not None:
g_floor = torch.zeros(groups, dtype=torch.int64, device=device).scatter_(
0, group, floor
)
history_valid &= history_pos >= g_floor.repeat_interleave(window)
history_req = g_req.repeat_interleave(window)
history_loc = torch.arange(history_rows, device=device)
commit_mask = (group_last - offset) < capacity
return WindowLayout(
req,
pos,
write_loc,
indices,
lengths,
history_req,
history_pos,
history_loc,
history_valid,
commit_mask,
history_rows + n,
)
def copy_packed_tokens(src, dst, src_loc, dst_loc, *, page_size, layout=KVLayout.V4):
"""Move tokens between paged buffers of ``layout``: a data row and a scale row."""
if not src_loc.numel():
return
src_loc, dst_loc = src_loc.long(), dst_loc.long()
for width, base in (
(layout.data_bytes, 0),
(layout.scale_bytes, page_size * layout.data_bytes),
):
cols = torch.arange(width, device=src.device)
values = src[
src_loc[:, None] // page_size,
base + (src_loc[:, None] % page_size) * width + cols,
]
dst[
dst_loc[:, None] // page_size,
base + (dst_loc[:, None] % page_size) * width + cols,
] = values
def _capturing() -> bool:
return torch.cuda.is_available() and torch.cuda.is_current_stream_capturing()
class RequestWindow:
def __init__(
self,
pool_factory,
*,
num_slots,
layers,
page_size,
capacity,
workspace_rows: Optional[int] = None,
):
self.capacity = ((capacity + page_size - 1) // page_size) * page_size
self.page_size = page_size
self.pool_factory = pool_factory
self.num_slots = num_slots
self.rows = num_slots * self.capacity
self.state = pool_factory(self.rows + page_size, layers)
self.zero_row = self.rows
self.sink_row = self.rows + 1
self.tags = torch.full(
(layers, self.rows + page_size),
-1,
dtype=torch.int64,
device=self.state.kv_buffer[0].device,
)
self.workspace = None
if workspace_rows:
self._ensure_workspace(workspace_rows)
self.layout = None
self.prepared = None
def _ensure_workspace(self, rows: int) -> None:
if self.workspace is not None and self.workspace.size >= rows:
return
assert not _capturing(), "request-window workspace must be sized before capture"
size = ((rows + self.page_size - 1) // self.page_size) * self.page_size
self.workspace = self.pool_factory(size, 1)
def reset(self, slots):
loc = slots.to(torch.int64)[:, None] * self.capacity + torch.arange(
self.capacity, device=slots.device
)
self.tags[:, loc.flatten()] = -1
self.prepared = None
def activate(self, layout):
if self.layout is layout:
return
self.layout = layout
self.prepared = None
if self.workspace is None:
self._ensure_workspace(layout.size)
elif self.workspace.size < layout.size:
# Captured graphs hold the workspace address; growing it strands them.
raise RuntimeError(
f"request-window workspace too small: {self.workspace.size} rows "
f"for a layout of {layout.size}"
)
def initialize_dummy_history(self):
layout = self.layout
self.tags.fill_(-1)
loc = layout.history_req * self.capacity + layout.history_pos % self.capacity
for buf in self.state.kv_buffer:
buf.zero_()
self.tags[:, loc] = layout.history_pos
self.prepared = None
def _history_src(self, layout):
return torch.where(
layout.history_valid,
layout.history_req * self.capacity + layout.history_pos % self.capacity,
self.zero_row,
)
def buffer(self, layer):
# The runner's capture scope includes eager warmups before CUDA capture
# starts, so the phase is part of the key: leaving the scope revalidates.
in_capture = get_is_capture_mode() or _capturing()
prepared_key = (layer, in_capture)
if self.prepared != prepared_key:
layout = self.layout
if layout is None:
raise RuntimeError("request-window metadata was not activated")
src = self._history_src(layout)
if not in_capture:
valid = layout.history_valid
if not torch.equal(
self.tags[layer, src][valid], layout.history_pos[valid]
):
raise RuntimeError(
"SWA history is missing: replay or window ownership is invalid"
)
copy_packed_tokens(
self.state.kv_buffer[layer],
self.workspace.kv_buffer[0],
src,
layout.history_loc,
page_size=self.page_size,
layout=self.state.kv_layout,
)
self.prepared = prepared_key
return self.workspace.kv_buffer[0]
def commit(self, layer):
layout = self.layout
dst = torch.where(
layout.commit_mask,
layout.req * self.capacity + layout.pos % self.capacity,
self.sink_row,
)
copy_packed_tokens(
self.buffer(layer),
self.state.kv_buffer[layer],
layout.write_loc,
dst,
page_size=self.page_size,
layout=self.state.kv_layout,
)
self.tags[layer, dst] = layout.pos
@@ -66,6 +66,12 @@ class PoolName(str, Enum):
INDEXER = "indexer" INDEXER = "indexer"
# TODO(hzh0425): Current DeepSeek V4 pool naming is verbose; will be normalized to # TODO(hzh0425): Current DeepSeek V4 pool naming is verbose; will be normalized to
# 'COMPRESSED_KV / COMPRESSED_INDEXER / COMPRESSED_STATE' in the next PR. # 'COMPRESSED_KV / COMPRESSED_INDEXER / COMPRESSED_STATE' in the next PR.
DEEPSEEK_V4_C1 = "deepseek_v4_c1"
DEEPSEEK_V4_C1_INDEXER = "deepseek_v4_c1_indexer"
DEEPSEEK_V4_C1_INDEXER_SCALE = "deepseek_v4_c1_indexer_scale"
DEEPSEEK_V4_C2 = "deepseek_v4_c2"
DEEPSEEK_V4_C2_INDEXER = "deepseek_v4_c2_indexer"
DEEPSEEK_V4_C2_INDEXER_SCALE = "deepseek_v4_c2_indexer_scale"
DEEPSEEK_V4_C4 = "deepseek_v4_c4" DEEPSEEK_V4_C4 = "deepseek_v4_c4"
DEEPSEEK_V4_C4_INDEXER = "deepseek_v4_c4_indexer" DEEPSEEK_V4_C4_INDEXER = "deepseek_v4_c4_indexer"
# FP4 indexer splits the indexer cache into separate payload/scale buffers, # FP4 indexer splits the indexer cache into separate payload/scale buffers,
@@ -113,7 +113,7 @@ def _resolve_deepseek_v4_layer_mappings(
) -> _DeepSeekV4LayerMappings: ) -> _DeepSeekV4LayerMappings:
transfer_layer_num = kvcache.end_layer - kvcache.start_layer transfer_layer_num = kvcache.end_layer - kvcache.start_layer
full = {layer: layer for layer in range(transfer_layer_num)} full = {layer: layer for layer in range(transfer_layer_num)}
swa = {} if getattr(kvcache, "_unified_kv", False) else full.copy() swa = full.copy() if kvcache.swa_kv_pool is not None else {}
c4, c128, c4_state_global_layers = {}, {}, [] c4, c128, c4_state_global_layers = {}, {}, []
for local_layer, item in enumerate( for local_layer, item in enumerate(
@@ -483,6 +483,13 @@ def _dsv4_compressed_region_buffers(kvcache: Any, ratio: int) -> tuple[list, int
return pool.kv_buffer, pool.bytes_per_page_padded return pool.kv_buffer, pool.bytes_per_page_padded
def _dsv4_page_aligned_only(pool: Any) -> bool:
"""Whether a pool may only move whole pages: the token-granular copy
(``transfer_cache_dsv4_mla``) hardcodes the V4 data/scale row split."""
layout = getattr(pool, "kv_layout", None)
return layout is not None and layout.value != "v4"
@dataclass(frozen=True) @dataclass(frozen=True)
class _IndexerRegion: class _IndexerRegion:
"""One page-contiguous indexer buffer group to mirror on the host.""" """One page-contiguous indexer buffer group to mirror on the host."""
@@ -578,6 +585,85 @@ def _dsv4_indexer_regions(kvcache: Any, page_size: int) -> list[_IndexerRegion]:
] ]
def _dsv4_low_ratio_entries(
kvcache: Any, page_size: int, num_host_pages: int, transfer_layer_num: int
):
"""Mirror each shared source once, in FULL-page units. Prefixes end on an even
page boundary, so ratio-2's request-scoped ring is rebuilt, not cached."""
import torch
entries = []
for ratio, names in (
(
1,
(
PoolName.DEEPSEEK_V4_C1,
PoolName.DEEPSEEK_V4_C1_INDEXER,
PoolName.DEEPSEEK_V4_C1_INDEXER_SCALE,
),
),
(
2,
(
PoolName.DEEPSEEK_V4_C2,
PoolName.DEEPSEEK_V4_C2_INDEXER,
PoolName.DEEPSEEK_V4_C2_INDEXER_SCALE,
),
),
):
sources = getattr(kvcache, "sources_by_ratio", {}).get(ratio, [])
if not sources:
continue
kv_pool = kvcache.kv_pools[ratio]
index_pool = kvcache.index_pools[ratio]
assert page_size % ratio == 0
slots_per_page = page_size // ratio
assert slots_per_page % index_pool.page_size == 0
index_pages_per_full_page = slots_per_page // index_pool.page_size
layer_mapping = {
source - kvcache.start_layer: index for index, source in enumerate(sources)
}
regions = [(names[0], kv_pool, kv_pool.kv_buffer)]
if index_pool.index_k_with_scale_buffer is not None:
index_regions = [(names[1], index_pool.index_k_with_scale_buffer)]
else:
index_regions = [
(names[1], index_pool.index_k_payload_buffer),
(names[2], index_pool.index_k_scale_buffer),
]
for name, buffers in index_regions:
# Drop only the padding rows past the FULL page address space.
rows = []
for buffer in buffers:
full_pages = buffer.shape[0] // index_pages_per_full_page
rows.append(
buffer[: full_pages * index_pages_per_full_page]
.view(torch.uint8)
.reshape(full_pages, -1)
)
regions.append((name, index_pool, rows))
for name, device_pool, buffers in regions:
entries.append(
build_pool_entry(
name=name,
host_pool=DeepSeekV4PagedHostPool(
pool_name=str(name),
device_buffers=buffers,
item_bytes=buffers[0].shape[1] * buffers[0].element_size(),
num_host_pages=num_host_pages,
slot_page_size=page_size,
layout=get_memory().hicache_mem_layout,
allocator_type=_get_allocator_type(),
page_aligned_only=True,
),
device_pool=device_pool,
layer_mapping=layer_mapping,
transfer_layer_num=transfer_layer_num,
)
)
return entries
def _dsv4_rope_sibling( def _dsv4_rope_sibling(
kvcache: Any, ratio: int kvcache: Any, ratio: int
) -> Optional[tuple[PoolName, list, int]]: ) -> Optional[tuple[PoolName, list, int]]:
@@ -651,10 +737,10 @@ def build_deepseek_v4_hicache_stack(
full_layer_mapping = layer_mappings.full full_layer_mapping = layer_mappings.full
is_unified_kv = getattr(kvcache, "_unified_kv", False) is_unified_kv = getattr(kvcache, "_unified_kv", False)
has_paged_swa = not is_unified_kv and kvcache.swa_kv_pool is not None
mtp_swa_device_buffers = [] mtp_swa_device_buffers = []
if is_unified_kv: if not has_paged_swa:
# unified_kv keeps the SWA ring inside the unified pool and never offloads it, # Unified KV and encoder replay rebuild SWA state; keep it out of host cache.
# so there is no separate SWA host pool to map.
swa_layer_mapping = {} swa_layer_mapping = {}
else: else:
if len(kvcache.swa_kv_pool.kv_buffer) != transfer_layer_num: if len(kvcache.swa_kv_pool.kv_buffer) != transfer_layer_num:
@@ -707,7 +793,7 @@ def build_deepseek_v4_hicache_stack(
), ),
] ]
if not is_unified_kv: if has_paged_swa:
swa_host_pool = DeepSeekV4PagedHostPool( swa_host_pool = DeepSeekV4PagedHostPool(
pool_name=str(PoolName.SWA), pool_name=str(PoolName.SWA),
device_buffers=[ device_buffers=[
@@ -719,6 +805,7 @@ def build_deepseek_v4_hicache_stack(
slot_page_size=kvcache.swa_page_size, slot_page_size=kvcache.swa_page_size,
layout=get_memory().hicache_mem_layout, layout=get_memory().hicache_mem_layout,
allocator_type=_get_allocator_type(), allocator_type=_get_allocator_type(),
page_aligned_only=_dsv4_page_aligned_only(kvcache.swa_kv_pool),
) )
swa_attn_allocator = params.token_to_kv_pool_allocator.swa_attn_allocator swa_attn_allocator = params.token_to_kv_pool_allocator.swa_attn_allocator
entries.append( entries.append(
@@ -749,7 +836,8 @@ def build_deepseek_v4_hicache_stack(
slot_page_size=page_size, slot_page_size=page_size,
layout=get_memory().hicache_mem_layout, layout=get_memory().hicache_mem_layout,
allocator_type=_get_allocator_type(), allocator_type=_get_allocator_type(),
page_aligned_only=is_unified_kv, page_aligned_only=is_unified_kv
or _dsv4_page_aligned_only(kvcache.c4_kv_pool),
) )
entries.append( entries.append(
build_pool_entry( build_pool_entry(
@@ -855,7 +943,8 @@ def build_deepseek_v4_hicache_stack(
slot_page_size=c128_slot_page_size, slot_page_size=c128_slot_page_size,
layout=get_memory().hicache_mem_layout, layout=get_memory().hicache_mem_layout,
allocator_type=_get_allocator_type(), allocator_type=_get_allocator_type(),
page_aligned_only=is_unified_kv, page_aligned_only=is_unified_kv
or _dsv4_page_aligned_only(kvcache.c128_kv_pool),
) )
# C128 state pool is intentionally not registered with hicache. # C128 state pool is intentionally not registered with hicache.
# page_size=256 % 128 == 0, so state pool is not consumed on load. # page_size=256 % 128 == 0, so state pool is not consumed on load.
@@ -897,6 +986,10 @@ def build_deepseek_v4_hicache_stack(
if c128_rope_entry is not None: if c128_rope_entry is not None:
entries.append(c128_rope_entry) entries.append(c128_rope_entry)
entries.extend(
_dsv4_low_ratio_entries(kvcache, page_size, num_host_pages, transfer_layer_num)
)
host_pool_group = HostPoolGroup(entries) host_pool_group = HostPoolGroup(entries)
cache_controller = HybridCacheController( cache_controller = HybridCacheController(
params.token_to_kv_pool_allocator, params.token_to_kv_pool_allocator,
@@ -1459,10 +1552,12 @@ class _DeepSeekV4Strategy(StackStrategy):
def matches(self, kvcache, components): def matches(self, kvcache, components):
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
return isinstance(kvcache, DeepSeekV4TokenToKVPool) and components in ( if not isinstance(kvcache, DeepSeekV4TokenToKVPool):
return False
return components in (
{ComponentType.FULL, ComponentType.SWA}, {ComponentType.FULL, ComponentType.SWA},
{ComponentType.FULL, ComponentType.SWA, ComponentType.C128}, {ComponentType.FULL, ComponentType.SWA, ComponentType.C128},
) ) or (components == {ComponentType.FULL} and kvcache.swa_kv_pool is None)
def build_direct_linker_pool_group(self, *, kvcache, params, page_size): def build_direct_linker_pool_group(self, *, kvcache, params, page_size):
from sglang.srt.mem_cache.hybrid_cache.linker_pool_assembler import ( from sglang.srt.mem_cache.hybrid_cache.linker_pool_assembler import (
@@ -1513,6 +1608,12 @@ class _DeepSeekV4Strategy(StackStrategy):
# The *_ROPE entries only resolve under unified fp8 kv; entry_map filters # The *_ROPE entries only resolve under unified fp8 kv; entry_map filters
# them out everywhere else. # them out everywhere else.
_sidecar_srcs = [ _sidecar_srcs = [
(PoolName.DEEPSEEK_V4_C1, PoolName.KV),
(PoolName.DEEPSEEK_V4_C1_INDEXER, PoolName.KV),
(PoolName.DEEPSEEK_V4_C1_INDEXER_SCALE, PoolName.KV),
(PoolName.DEEPSEEK_V4_C2, PoolName.KV),
(PoolName.DEEPSEEK_V4_C2_INDEXER, PoolName.KV),
(PoolName.DEEPSEEK_V4_C2_INDEXER_SCALE, PoolName.KV),
(PoolName.DEEPSEEK_V4_C4, PoolName.KV), (PoolName.DEEPSEEK_V4_C4, PoolName.KV),
(PoolName.DEEPSEEK_V4_C4_ROPE, PoolName.KV), (PoolName.DEEPSEEK_V4_C4_ROPE, PoolName.KV),
(PoolName.DEEPSEEK_V4_C4_INDEXER, PoolName.KV), (PoolName.DEEPSEEK_V4_C4_INDEXER, PoolName.KV),
@@ -39,6 +39,7 @@ from sglang.srt.environ import envs
from sglang.srt.hardware_backend.mlx.runtime import use_mlx from sglang.srt.hardware_backend.mlx.runtime import use_mlx
from sglang.srt.managers.mm_schedule import init_mm_embedding_cache from sglang.srt.managers.mm_schedule import init_mm_embedding_cache
from sglang.srt.mem_cache.cache_init_params import CacheInitParams from sglang.srt.mem_cache.cache_init_params import CacheInitParams
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
from sglang.srt.mem_cache.memory_pool import MHATokenToKVPool from sglang.srt.mem_cache.memory_pool import MHATokenToKVPool
from sglang.srt.mem_cache.registry import TreeCacheBuildContext, create_tree_cache from sglang.srt.mem_cache.registry import TreeCacheBuildContext, create_tree_cache
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
@@ -233,7 +234,11 @@ def build_kv_cache(
) )
# Hybrid memory pool # Hybrid memory pool
is_hybrid_swa = tp_worker.is_hybrid_swa token_to_kv_pool = tp_worker.model_runner.token_to_kv_pool
is_hybrid_swa = tp_worker.is_hybrid_swa and (
not isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
or token_to_kv_pool.needs_paged_swa_allocator
)
is_hybrid_ssm = uses_ssm_state(tp_worker.model_runner.model_config) is_hybrid_ssm = uses_ssm_state(tp_worker.model_runner.model_config)
is_dsa = is_deepseek_dsa(model_config.hf_config) is_dsa = is_deepseek_dsa(model_config.hf_config)
@@ -56,7 +56,10 @@ from sglang.srt.mem_cache.allocator.unified_hybrid_swa import (
from sglang.srt.mem_cache.allocator.unified_mamba import ( from sglang.srt.mem_cache.allocator.unified_mamba import (
UnifiedMambaTokenToKVPoolAllocator, UnifiedMambaTokenToKVPoolAllocator,
) )
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool from sglang.srt.mem_cache.deepseek_v4_memory_pool import (
DeepSeekV4TokenToKVPool,
select_dsv4_kv_layout,
)
from sglang.srt.mem_cache.hisparse_memory_pool import HiSparseDSATokenToKVPool from sglang.srt.mem_cache.hisparse_memory_pool import HiSparseDSATokenToKVPool
from sglang.srt.mem_cache.memory_pool import ( from sglang.srt.mem_cache.memory_pool import (
DSATokenToKVPool, DSATokenToKVPool,
@@ -1237,6 +1240,7 @@ class KVCacheConfigurator:
if is_dsv4_model: if is_dsv4_model:
token_to_kv_pool = self._build_dsv4_kv_pool( token_to_kv_pool = self._build_dsv4_kv_pool(
max_running_requests=sizes.max_running_requests, max_running_requests=sizes.max_running_requests,
full_max_total_num_tokens=sizes.full_max_total_num_tokens,
swa_max_total_num_tokens=sizes.swa_max_total_num_tokens, swa_max_total_num_tokens=sizes.swa_max_total_num_tokens,
c4_max_total_num_tokens=sizes.c4_max_total_num_tokens, c4_max_total_num_tokens=sizes.c4_max_total_num_tokens,
c128_max_total_num_tokens=sizes.c128_max_total_num_tokens, c128_max_total_num_tokens=sizes.c128_max_total_num_tokens,
@@ -1340,6 +1344,7 @@ class KVCacheConfigurator:
self, self,
*, *,
max_running_requests: int, max_running_requests: int,
full_max_total_num_tokens: int,
swa_max_total_num_tokens: Optional[int], swa_max_total_num_tokens: Optional[int],
c4_max_total_num_tokens: int, c4_max_total_num_tokens: int,
c128_max_total_num_tokens: int, c128_max_total_num_tokens: int,
@@ -1361,8 +1366,10 @@ class KVCacheConfigurator:
compression_ratios = [ compression_ratios = [
COMPRESS_RATIO_NEXTN_LAYER COMPRESS_RATIO_NEXTN_LAYER
] * self.layer_info.num_effective_layers ] * self.layer_info.num_effective_layers
kv_source_layers = []
else: else:
compression_ratios = self.model_config.compress_ratios compression_ratios = self.model_config.compress_ratios
kv_source_layers = list(self.model_config.hf_config.kv_source_layer_ids)
# NPU keeps its PA_ND KV-pool subclass, while Compressor state sizing # NPU keeps its PA_ND KV-pool subclass, while Compressor state sizing
# follows the same fixed ring ownership as GPU. Do not replace the # follows the same fixed ring ownership as GPU. Do not replace the
@@ -1374,8 +1381,13 @@ class KVCacheConfigurator:
) )
pool_cls = DSV4NPUTokenToKVPool pool_cls = DSV4NPUTokenToKVPool
kv_layout_kwargs = {}
else: else:
pool_cls = DeepSeekV4TokenToKVPool pool_cls = DeepSeekV4TokenToKVPool
kv_layout, compressed_kv_layout = select_dsv4_kv_layout()
kv_layout_kwargs = dict(
kv_layout=kv_layout, compressed_kv_layout=compressed_kv_layout
)
token_to_kv_pool = pool_cls( token_to_kv_pool = pool_cls(
max_num_reqs=max_running_requests, max_num_reqs=max_running_requests,
@@ -1404,6 +1416,10 @@ class KVCacheConfigurator:
end_layer=self.layer_info.end_layer, end_layer=self.layer_info.end_layer,
enable_hisparse=get_memory().enable_hisparse, enable_hisparse=get_memory().enable_hisparse,
online_mtp_max_draft_tokens=(max_speculative_num_draft_tokens() or 0), online_mtp_max_draft_tokens=(max_speculative_num_draft_tokens() or 0),
kv_source_layers=kv_source_layers,
full_size=full_max_total_num_tokens,
**({"is_draft_worker": self.is_draft_worker} if not _is_npu else {}),
**kv_layout_kwargs,
) )
if not self.is_draft_worker and token_to_kv_pool._unified_kv: if not self.is_draft_worker and token_to_kv_pool._unified_kv:
# The draft pool has no C4 layers and shares this req pool, so only # The draft pool has no C4 layers and shares this req pool, so only
@@ -2071,7 +2087,19 @@ class KVCacheConfigurator:
need_sort=need_sort, need_sort=need_sort,
) )
else: else:
if self.is_hybrid_swa and sizes.full_max_total_num_tokens == 0: if (
isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
and not token_to_kv_pool.needs_paged_swa_allocator
):
token_to_kv_pool_allocator = PagedTokenToKVPoolAllocator(
sizes.full_max_total_num_tokens,
page_size=get_schedule().page_size,
dtype=self.kv_cache_dtype,
device=self.device,
kvcache=token_to_kv_pool,
need_sort=need_sort,
)
elif self.is_hybrid_swa and sizes.full_max_total_num_tokens == 0:
token_to_kv_pool_allocator = PureSWATokenToKVPoolAllocator( token_to_kv_pool_allocator = PureSWATokenToKVPoolAllocator(
sizes.swa_max_total_num_tokens, sizes.swa_max_total_num_tokens,
page_size=get_schedule().page_size, page_size=get_schedule().page_size,
@@ -2141,7 +2169,10 @@ class KVCacheConfigurator:
else: else:
assert self.is_draft_worker assert self.is_draft_worker
if self.is_hybrid_swa: if self.is_hybrid_swa and (
not isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
or token_to_kv_pool.needs_paged_swa_allocator
):
if isinstance( if isinstance(
token_to_kv_pool_allocator, token_to_kv_pool_allocator,
DeepSeekV4HiSparseTokenToKVPoolAllocator, DeepSeekV4HiSparseTokenToKVPoolAllocator,
@@ -71,6 +71,7 @@ from sglang.srt.mem_cache.allocator.unified_mamba import (
UnifiedMambaTokenToKVPoolAllocator, UnifiedMambaTokenToKVPoolAllocator,
) )
from sglang.srt.mem_cache.base_swa_memory_pool import BaseSWAKVPool from sglang.srt.mem_cache.base_swa_memory_pool import BaseSWAKVPool
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
from sglang.srt.runtime_context import get_parallel from sglang.srt.runtime_context import get_parallel
@@ -162,6 +163,10 @@ class KVIndexTranslator:
self._swa_write_loc_from_full = ( self._swa_write_loc_from_full = (
token_to_kv_pool.translate_loc_from_full_to_swa token_to_kv_pool.translate_loc_from_full_to_swa
if isinstance(token_to_kv_pool, BaseSWAKVPool) if isinstance(token_to_kv_pool, BaseSWAKVPool)
and (
not isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
or token_to_kv_pool.request_window is None
)
else None else None
) )
@@ -814,6 +814,12 @@ class MooncakeStore(HiCacheStorage, MooncakeBaseStore):
elif pool_name in ( elif pool_name in (
PoolName.INDEXER, PoolName.INDEXER,
PoolName.DRAFT_INDEXER, PoolName.DRAFT_INDEXER,
PoolName.DEEPSEEK_V4_C1,
PoolName.DEEPSEEK_V4_C1_INDEXER,
PoolName.DEEPSEEK_V4_C1_INDEXER_SCALE,
PoolName.DEEPSEEK_V4_C2,
PoolName.DEEPSEEK_V4_C2_INDEXER,
PoolName.DEEPSEEK_V4_C2_INDEXER_SCALE,
PoolName.DEEPSEEK_V4_C4, PoolName.DEEPSEEK_V4_C4,
PoolName.DEEPSEEK_V4_C4_ROPE, PoolName.DEEPSEEK_V4_C4_ROPE,
PoolName.DEEPSEEK_V4_C4_INDEXER, PoolName.DEEPSEEK_V4_C4_INDEXER,
@@ -84,6 +84,7 @@ ALLOWED_KEYS_PER_PHASE = {
"max_context_size", "max_context_size",
"full_prefill_max_req", "full_prefill_max_req",
"full_prefill_prefix_chunk_tokens", "full_prefill_prefix_chunk_tokens",
"max_seq_len",
), ),
} }
@@ -113,6 +114,9 @@ class PhaseConfig:
# chunk variants and chooses the smallest one covering a batch. None uses # chunk variants and chooses the smallest one covering a batch. None uses
# the scheduler's aggregate chunked_prefill_size token budget. # the scheduler's aggregate chunked_prefill_size token budget.
full_prefill_prefix_chunk_tokens: Optional[int] = None full_prefill_prefix_chunk_tokens: Optional[int] = None
# Prefill only: a batch whose longest sequence exceeds this replays eagerly, and
# backends that capture context-wide work size it. None defers to token buckets.
max_seq_len: Optional[int] = None
def default_prefill_backend() -> str: def default_prefill_backend() -> str:
@@ -0,0 +1,77 @@
from copy import copy
import torch
def run_encoder_swa_replay(worker, batch):
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
)
runner = worker.model_runner
window = runner.token_to_kv_pool.request_window
if window is None or not batch.forward_mode.is_extend_without_speculative():
return
for i, reset in enumerate(batch.encoder_swa_reset):
if not reset:
continue
slot = batch.req_pool_indices[i : i + 1]
window.reset(slot)
end = batch.prefix_lens[i]
if not end:
continue
if end % 2:
raise ValueError(
"encoder SWA replay requires an even cached-prefix boundary"
)
start = max(0, end - 128)
req = batch.reqs[i]
replay = copy(batch)
replay.reqs = [req]
replay.input_ids = torch.tensor(
list(req.full_untruncated_fill_ids[start:end]),
dtype=torch.int64,
device=runner.device,
)
replay.prefill_input_ids_cpu = None
replay.req_pool_indices = slot
replay.req_pool_indices_cpu = batch.req_pool_indices_cpu[i : i + 1]
replay.prefix_lens = [start]
replay.extend_lens = [end - start]
replay.extend_num_tokens = end - start
replay.seq_lens = torch.tensor([end], dtype=torch.int64, device=runner.device)
replay.seq_lens_cpu = torch.tensor([end], dtype=torch.int64)
replay.seq_lens_sum = end
replay.orig_seq_lens = replay.seq_lens
replay.out_cache_loc = runner.req_to_token_pool.req_to_token[
slot[0], start:end
].long()
replay.return_logprob = False
replay.top_logprobs_nums = None
replay.token_ids_logprobs = None
replay.extend_logprob_start_lens = [end - start]
replay.extend_input_logprob_token_ids = None
replay.is_prefill_only = True
replay.spec_info = None
replay.sampling_info = None
replay.has_grammar = False
replay.multimodal_inputs = [None]
replay.engram_history = None
hasher = runner.model.model.engram_hasher
if hasher is not None:
n = hasher.max_ngram_size - 1
ids = list(req.full_untruncated_fill_ids[max(0, start - n) : start])
replay.engram_history = torch.tensor(
[[0] * (n - len(ids)) + ids],
dtype=torch.int32,
device=runner.device,
)
fb = ForwardBatch.init_new(
replay,
runner,
capture_hidden_mode=CaptureHiddenMode.NULL,
return_hidden_states_before_norm=False,
)
fb.encoder_swa_replay = True
runner.forward(fb)
@@ -698,6 +698,7 @@ class ForwardBatch(ForwardBatchDeepSeekMHAMixin):
# For ngram embedding # For ngram embedding
ngram_embedding_info: Optional[NgramEmbeddingInfo] = None ngram_embedding_info: Optional[NgramEmbeddingInfo] = None
encoder_swa_replay: bool = False
# DeepSeek-V4.1 engram, extend only: the n - 1 tokens before each request's # DeepSeek-V4.1 engram, extend only: the n - 1 tokens before each request's
# first extend token, oldest first, [bs, n - 1] int32 (see EngramHasher). # first extend token, oldest first, [bs, n - 1] int32 (see EngramHasher).
@@ -33,7 +33,9 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _unsupported_derived_weight_cache_error() -> Optional[str]: def _unsupported_derived_weight_cache_error(
model: Optional[torch.nn.Module] = None,
) -> Optional[str]:
"""Reject online weight updates that derived-weight caches cannot survive. """Reject online weight updates that derived-weight caches cannot survive.
The HPC-Ops bf16xfp32 GEMM caches the fp32 weight split; in-place loader The HPC-Ops bf16xfp32 GEMM caches the fp32 weight split; in-place loader
@@ -41,6 +43,18 @@ def _unsupported_derived_weight_cache_error() -> Optional[str]:
old weights. The check is startup-determined and rank-uniform, so an old weights. The check is startup-determined and rank-uniform, so an
update never proceeds on some workers while rejected on others. update never proceeds on some workers while rejected on others.
""" """
if model is not None and any(
getattr(module, "_hc_attn_tf32_parts", None) is not None
or getattr(module, "_hc_ffn_tf32_parts", None) is not None
for module in model.modules()
):
return (
"Online weight updates are not supported while compensated mHC "
"weight splits are active: captured CUDA graphs retain these derived "
"weights. Restart with SGLANG_OPT_DEEPGEMM_HC_PRENORM=0 to use "
"online weight updates."
)
from sglang.kernels.ops.attention.dsv4.gemm import hpc_bf16xfp32_gemm_enabled from sglang.kernels.ops.attention.dsv4.gemm import hpc_bf16xfp32_gemm_enabled
if hpc_bf16xfp32_gemm_enabled(): if hpc_bf16xfp32_gemm_enabled():
@@ -148,7 +162,7 @@ class WeightUpdater:
) -> tuple[bool, str]: ) -> tuple[bool, str]:
"""Update engine weights in-place from the disk.""" """Update engine weights in-place from the disk."""
self._assert_weight_cache_inactive("update_weights_from_disk") self._assert_weight_cache_inactive("update_weights_from_disk")
error = _unsupported_derived_weight_cache_error() error = _unsupported_derived_weight_cache_error(self.get_model())
if error is not None: if error is not None:
return False, error return False, error
@@ -238,7 +252,7 @@ class WeightUpdater:
shape: the shape of the parameter to be updated. shape: the shape of the parameter to be updated.
""" """
self._assert_weight_cache_inactive("update_weights_from_distributed") self._assert_weight_cache_inactive("update_weights_from_distributed")
error = _unsupported_derived_weight_cache_error() error = _unsupported_derived_weight_cache_error(self.get_model())
if error is not None: if error is not None:
return False, error return False, error
@@ -322,7 +336,7 @@ class WeightUpdater:
named_tensors: List[Tuple[str, Union[torch.Tensor, LocalSerializedTensor]]], named_tensors: List[Tuple[str, Union[torch.Tensor, LocalSerializedTensor]]],
load_format: Optional[str] = None, load_format: Optional[str] = None,
): ):
error = _unsupported_derived_weight_cache_error() error = _unsupported_derived_weight_cache_error(self.get_model())
if error is not None: if error is not None:
return False, error return False, error
@@ -388,7 +402,7 @@ class WeightUpdater:
def update_weights_from_ipc(self: WeightUpdater, recv_req): def update_weights_from_ipc(self: WeightUpdater, recv_req):
"""Update weights from IPC for checkpoint-engine integration.""" """Update weights from IPC for checkpoint-engine integration."""
self._assert_weight_cache_inactive("update_weights_from_ipc") self._assert_weight_cache_inactive("update_weights_from_ipc")
error = _unsupported_derived_weight_cache_error() error = _unsupported_derived_weight_cache_error(self.get_model())
if error is not None: if error is not None:
return False, error return False, error
@@ -804,29 +804,13 @@ class HybridSWAPoolConfigurator(MemoryPoolConfigurator):
return self._solve_pool_sizes(max_total_num_tokens, page_size) return self._solve_pool_sizes(max_total_num_tokens, page_size)
class SWAChunkCapPoolConfigurator(HybridSWAPoolConfigurator): def compute_swa_request_cap(*, page_size: int, window: int, attn_dp_size: int) -> int:
"""Hybrid SWA configurator with the SWA pool sized from a fixed token cap. """Worst-case SWA slots the scheduler holds live at max_running_requests."""
When max_running_requests is explicit, the SWA pool's worst-case
footprint is bounded per request. The SWA pool is sized tightly from that
cap and the freed memory is redirected to the full pool, instead of sizing
both pools by swa_full_tokens_ratio.
"""
def __init__(self, kvc: KVCacheConfigurator):
self.kv_cache_dtype_str = kvc.kv_cache_dtype_str
super().__init__(kvc)
assert self._full_layers_num > 0
page_size = kvc.page_size
window = kvc.sliding_window_size
draft_tokens = get_spec().speculative_num_draft_tokens or 1 draft_tokens = get_spec().speculative_num_draft_tokens or 1
eviction_interval = max(1, envs.SGLANG_SWA_EVICTION_INTERVAL.get()) eviction_interval = max(1, envs.SGLANG_SWA_EVICTION_INTERVAL.get())
""" # __________[padding][eviction_interval][window]
__________[padding][eviction_interval][window] # Padding to make sure eviction point is page-aligned.
Padding to make sure eviction point is page-aligned.
"""
trailing_tokens = window + eviction_interval * draft_tokens + page_size trailing_tokens = window + eviction_interval * draft_tokens + page_size
if get_spec().speculative_algorithm is None: if get_spec().speculative_algorithm is None:
decode_alloc = page_size decode_alloc = page_size
@@ -844,20 +828,41 @@ class SWAChunkCapPoolConfigurator(HybridSWAPoolConfigurator):
decode_alloc = 2 * get_alloc_len_per_decode() decode_alloc = 2 * get_alloc_len_per_decode()
per_request = trailing_tokens + decode_alloc per_request = trailing_tokens + decode_alloc
num_reqs = get_schedule().max_running_requests // kvc.ps.attn_dp_size num_reqs = get_schedule().max_running_requests // attn_dp_size
if get_disagg().disaggregation_mode == "decode": if get_disagg().disaggregation_mode == "decode":
self._swa_cap = ( return (
per_request * num_reqs per_request * num_reqs
+ (window + page_size) * get_disagg().disaggregation_decode_extra_slots + (window + page_size) * get_disagg().disaggregation_decode_extra_slots
) )
else: else:
chunks_in_flight = 1 if get_schedule().disable_overlap_schedule else 2 chunks_in_flight = 1 if get_schedule().disable_overlap_schedule else 2
self._swa_cap = ( return (
per_request * num_reqs per_request * num_reqs
+ chunks_in_flight * get_schedule().chunked_prefill_size + chunks_in_flight * get_schedule().chunked_prefill_size
+ page_size + page_size
) )
class SWAChunkCapPoolConfigurator(HybridSWAPoolConfigurator):
"""Hybrid SWA configurator with the SWA pool sized from a fixed token cap.
When max_running_requests is explicit, the SWA pool's worst-case
footprint is bounded per request. The SWA pool is sized tightly from that
cap and the freed memory is redirected to the full pool, instead of sizing
both pools by swa_full_tokens_ratio.
"""
def __init__(self, kvc: KVCacheConfigurator):
self.kv_cache_dtype_str = kvc.kv_cache_dtype_str
super().__init__(kvc)
assert self._full_layers_num > 0
self._swa_cap = compute_swa_request_cap(
page_size=kvc.page_size,
window=kvc.sliding_window_size,
attn_dp_size=kvc.ps.attn_dp_size,
)
@staticmethod @staticmethod
def is_applicable(kvc: KVCacheConfigurator) -> bool: def is_applicable(kvc: KVCacheConfigurator) -> bool:
"""True when SWAChunkCache can be sized from explicit max requests.""" """True when SWAChunkCache can be sized from explicit max requests."""
@@ -915,6 +920,18 @@ class SWAChunkCapPoolConfigurator(HybridSWAPoolConfigurator):
) )
# Used when --swa-full-tokens-ratio is at its default and cap mode is unusable.
DSV4_DEFAULT_SWA_FULL_TOKENS_RATIO = 0.1
def _operator_swa_full_tokens_ratio() -> Optional[float]:
"""The operator's --swa-full-tokens-ratio, or None when it was not given."""
schedule = get_schedule()
if not schedule._swa_full_tokens_ratio_explicitly_set:
return None
return schedule.swa_full_tokens_ratio
@dataclass @dataclass
class _DSV4PoolSizes: class _DSV4PoolSizes:
full_max_total_num_tokens: int full_max_total_num_tokens: int
@@ -940,6 +957,28 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
self.qk_nope_head_dim = cfg.qk_nope_head_dim self.qk_nope_head_dim = cfg.qk_nope_head_dim
self.qk_rope_head_dim = cfg.qk_rope_head_dim self.qk_rope_head_dim = cfg.qk_rope_head_dim
self.indexer_head_dim = cfg.index_head_dim self.indexer_head_dim = cfg.index_head_dim
self.attn_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
from sglang.kernels.ops.attention.dsv4.unified_kv_kernels.env_gate import (
is_unified_kv_fp8,
is_unified_kv_triton,
)
from sglang.srt.mem_cache.deepseek_v4_memory_pool import (
dsv4_unified_row_bytes,
)
# Resolve the unified-kv gate before any sizing so the two cannot drift.
self._unified = is_unified_kv_triton()
self._unified_fp8 = is_unified_kv_fp8()
# Row width across both unified pools: 1024 B bf16, 640 B fp8.
self._unified_row_bytes = dsv4_unified_row_bytes(
self.qk_nope_head_dim, self.qk_rope_head_dim, self._unified_fp8
)
if self._unified:
# Unified_kv stores the whole latent: one bf16 row, or fp8 nope + bf16 rope.
self.kv_bytes = self._unified_row_bytes
else:
# One FlashMLA-layout latent slot, in bytes.
self.kv_bytes = self.qk_nope_head_dim + self.qk_rope_head_dim * 2 + 8
# HIP takes the FP4-accurate byte count here. The NVIDIA FP4 path # HIP takes the FP4-accurate byte count here. The NVIDIA FP4 path
# keeps the FP8 estimate. # keeps the FP8 estimate.
self.indexer_bytes_per_token = get_dsv4_indexer_bytes_per_token( self.indexer_bytes_per_token = get_dsv4_indexer_bytes_per_token(
@@ -958,10 +997,17 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
f"local={len(self.compression_ratios)}/{len(cfg.compress_ratios)}" f"local={len(self.compression_ratios)}/{len(cfg.compress_ratios)}"
) )
self.swa_page_size = cfg.window_size self.swa_page_size = cfg.window_size
self.operator_swa_ratio = _operator_swa_full_tokens_ratio()
self.swa_ratio = (
self.operator_swa_ratio
if self.operator_swa_ratio is not None
else DSV4_DEFAULT_SWA_FULL_TOKENS_RATIO
)
self.sliding_window_size = kvc.sliding_window_size self.sliding_window_size = kvc.sliding_window_size
self.swa_ratio = get_schedule().swa_full_tokens_ratio self.page_size = kvc.page_size
self.is_speculative = get_spec().speculative_algorithm is not None self.is_speculative = get_spec().speculative_algorithm is not None
self.online_c128_mtp_max_draft_tokens = max_speculative_num_draft_tokens() or 0 self.online_c128_mtp_max_draft_tokens = max_speculative_num_draft_tokens() or 0
self.attn_dp_size = kvc.ps.attn_dp_size
self.requested_max_running_requests_per_worker = ( self.requested_max_running_requests_per_worker = (
get_schedule().max_running_requests // kvc.ps.attn_dp_size get_schedule().max_running_requests // kvc.ps.attn_dp_size
if get_schedule().max_running_requests is not None if get_schedule().max_running_requests is not None
@@ -987,31 +1033,21 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
self.num_layers_total = len(self.compression_ratios) self.num_layers_total = len(self.compression_ratios)
self.num_layers_ca4 = sum(1 for r in self.compression_ratios if r == 4) self.num_layers_ca4 = sum(1 for r in self.compression_ratios if r == 4)
self.num_layers_ca128 = sum(1 for r in self.compression_ratios if r == 128) self.num_layers_ca128 = sum(1 for r in self.compression_ratios if r == 128)
# The low-ratio indexer pools are built with force_fp4=True
# Unified-KV uses a different physical layout than the non-unified V4 path: # (deepseek_v4_memory_pool), so they are fp4 whatever dtype c4 uses.
# * one row carries the full latent -- 1024 B bf16, or 640 B under low_ratio_index_bytes = get_dsv4_indexer_bytes_per_token(
# SGLANG_DSV4_UNIFIED_KV_FP8 (512 B fp8 nope + 128 B bf16 rope) -- not self.indexer_head_dim, use_fp4_indexer=True
# that path's 584-byte fp8(nope) + bf16(rope) + scales cell. )
# * SWA is a fixed per-request ring (num_req_slots * ring_size), self.low_ratio_bytes_per_full_token = sum(
# independent of full_token, so it is a fixed *bias* rather than a (self.kv_bytes + low_ratio_index_bytes) / cfg.compress_ratios[l]
# per-token term. Gate on the same switch the pool itself uses so the for l in cfg.hf_config.kv_source_layer_ids
# sizing and the allocation never drift apart. if kvc.layer_info.start_layer <= l < kvc.layer_info.end_layer
from sglang.kernels.ops.attention.dsv4.unified_kv_kernels.env_gate import ( and cfg.compress_ratios[l] in (1, 2)
is_unified_kv_fp8,
is_unified_kv_triton,
) )
from sglang.srt.mem_cache.deepseek_v4_memory_pool import ( from sglang.srt.mem_cache.deepseek_v4_memory_pool import (
dsv4_unified_row_bytes, dsv4_unified_row_bytes,
) )
self._unified = is_unified_kv_triton()
self._unified_fp8 = is_unified_kv_fp8()
self.attn_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
# Row width across both pools: 1024 B bf16, 640 B fp8. Read from the pool
# module so sizing can't drift from the allocation.
self._unified_row_bytes = dsv4_unified_row_bytes(
self.qk_nope_head_dim, self.qk_rope_head_dim, self._unified_fp8
)
# swa_page_size is the model's sliding window (cfg.window_size). # swa_page_size is the model's sliding window (cfg.window_size).
self._swa_ring_size = get_swa_ring_size(self.swa_page_size, self.is_speculative) self._swa_ring_size = get_swa_ring_size(self.swa_page_size, self.is_speculative)
self._spec_infl = 1.0 self._spec_infl = 1.0
@@ -1044,8 +1080,47 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
max_speculative_num_draft_tokens() or 0 max_speculative_num_draft_tokens() or 0
) )
from sglang.srt.runtime_context import get_exec
self.encoder_replay = get_exec().features.enable_encoder_swa_bounded_replay
self.paged_draft_layers = 0
if self.encoder_replay and kvc.spec_algorithm.is_dspark():
self.paged_draft_layers = int(
kvc.spec_aux_config.dflash_draft_num_layers or 0
)
assert self.paged_draft_layers > 0, "DSpark draft layer count is required"
self.request_window_bytes = 0
if self.encoder_replay:
slots = self.requested_max_running_requests_per_worker + 1
capacity = ceil_align(
self.sliding_window_size + self.online_c128_mtp_max_draft_tokens,
self.page_size,
)
layers = self.num_layers_total
scratch = (
max(
get_schedule().chunked_prefill_size,
slots * max(128, self.online_c128_mtp_max_draft_tokens),
)
+ slots * 128
+ self.page_size
)
self.request_window_bytes = (
(slots * capacity + self.page_size) * layers * (self.kv_bytes + 16)
+ 4 * scratch * (self.kv_bytes + 16)
+ slots * 3 * 16 * self.attn_head_dim * 8
)
if not self.paged_draft_layers:
self.swa_ratio = 0
self.swa_prefix_tails = self._resolve_swa_prefix_tails()
self.swa_cap_tokens = (
0
if self.encoder_replay and not self.paged_draft_layers
else self._resolve_swa_cap_tokens()
)
self.bytes_per_swa_token = self._get_bytes_per_swa_token()
self.bytes_per_full_token = self._get_bytes_per_full_token() self.bytes_per_full_token = self._get_bytes_per_full_token()
if self.is_speculative: if self.is_speculative and not self.encoder_replay:
# Reserve memory for the speculative draft worker by inflating # Reserve memory for the speculative draft worker by inflating
# per-token bytes by (target+draft)/target. Equivalent to dflash's # per-token bytes by (target+draft)/target. Equivalent to dflash's
# scale_kv_cell_size_per_token_for_dflash but applied to # scale_kv_cell_size_per_token_for_dflash but applied to
@@ -1054,6 +1129,7 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
target_layers = self.num_layers_total target_layers = self.num_layers_total
self._spec_infl = (target_layers + draft_layers) / target_layers self._spec_infl = (target_layers + draft_layers) / target_layers
self.bytes_per_full_token *= self._spec_infl self.bytes_per_full_token *= self._spec_infl
self.bytes_per_swa_token *= self._spec_infl
# Online c128 keeps a single in-progress (max, sum, kv) state per index # Online c128 keeps a single in-progress (max, sum, kv) state per index
# and assumes a strict forward-only schedule. Speculative decode (MTP) # and assumes a strict forward-only schedule. Speculative decode (MTP)
@@ -1105,72 +1181,133 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
f"get_compress_state_ring_size()." f"get_compress_state_ring_size()."
) )
def _get_bytes_per_full_token(self) -> float: def _resolve_swa_prefix_tails(self) -> int:
if self._unified: """Cached prefix tails cap mode keeps addressable: a prefix is reusable only
# Unified_kv stores the whole latent: one bf16 pool, or an fp8 nope while its last sliding_window tokens still hold SWA slots."""
# pool plus a bf16 rope pool. kv_bytes also prices the compressed prefix_tails = get_schedule().swa_prefix_tails
# c4/c128 rows below, which live in the same pool(s). if prefix_tails is not None:
kv_bytes = self._unified_row_bytes return prefix_tails
else: if get_memory().disable_radix_cache:
kv_bytes = self.qk_nope_head_dim + self.qk_rope_head_dim * 2 + 8 # Nothing is kept for reuse, so the request cap alone bounds the pool.
return 0
max_running_requests = self.requested_max_running_requests_per_worker
return 4 * max_running_requests if max_running_requests is not None else 0
attn_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim def _resolve_swa_cap_tokens(self) -> Optional[int]:
c4_state_dtype_size, c128_state_dtype_size = ( """SWA slots to reserve in cap mode, None to keep ratio sizing. Cap mode
_get_dsv4_compress_state_dtype_sizes() budgets from the request cap plus radix headroom, not full_tokens."""
if self.operator_swa_ratio is not None:
return None
if self._unified:
# Ring mode: SWA is a fixed per-request ring, with no paged pool to size.
return None
max_running_requests = self.requested_max_running_requests_per_worker
if max_running_requests is None or self.sliding_window_size is None:
return None
chunked_prefill_size = get_schedule().chunked_prefill_size
if self.disaggregation_mode != "decode" and (
chunked_prefill_size is None or chunked_prefill_size <= 0
):
return None
cap = compute_swa_request_cap(
page_size=self.page_size,
window=self.sliding_window_size,
attn_dp_size=self.attn_dp_size,
) )
c4_state_bytes = 2 * 2 * attn_head_dim * c4_state_dtype_size headroom = self.swa_prefix_tails * (self.sliding_window_size + self.page_size)
return ceil_align(cap + headroom, self.page_size)
def _get_bytes_per_swa_token(self) -> float:
"""Bytes one SWA slot costs across the stage. c4_state_pool_size = swa_tokens
/ swa_page_size * ring, so c4 compress state is priced per SWA slot too."""
if self.encoder_replay:
# Target SWA lives in the request window; only the draft owns paged SWA
# bytes, and its layers carry no compressed state.
return self.kv_bytes * self.paged_draft_layers
c4_state_dtype_size, _ = _get_dsv4_compress_state_dtype_sizes()
c4_state_bytes = 2 * 2 * self.attn_head_dim * c4_state_dtype_size
c4_indexer_state_bytes = 2 * 2 * self.indexer_head_dim * c4_state_dtype_size
c4_state_ratio = self.c4_ring_size / self.swa_page_size
return (
self.kv_bytes * self.num_layers_total
+ c4_state_ratio
* (c4_state_bytes + c4_indexer_state_bytes)
* self.num_layers_ca4
)
def _get_bytes_per_full_token(self) -> float:
_, c128_state_dtype_size = _get_dsv4_compress_state_dtype_sizes()
# Online c128 stores (max, sum, kv) per slot (3*head_dim) instead of # Online c128 stores (max, sum, kv) per slot (3*head_dim) instead of
# raw (kv, score) (2*head_dim). Combined with ring_size=1 this still # raw (kv, score) (2*head_dim). Combined with ring_size=1 this still
# nets a large reduction (~3/256x) but the per-slot bytes go up. # nets a large reduction (~3/256x) but the per-slot bytes go up.
c128_online = envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get() c128_online = envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get()
c128_state_bytes = ( c128_state_bytes = (
(3 if c128_online else 2 * 1) * attn_head_dim * c128_state_dtype_size (3 if c128_online else 2 * 1) * self.attn_head_dim * c128_state_dtype_size
) )
c4_indexer_state_bytes = 2 * 2 * self.indexer_head_dim * c4_state_dtype_size
c4_state_ratio = self.c4_ring_size / self.swa_page_size
# C128 state is request-scoped and is finalized after # C128 state is request-scoped and is finalized after
# max_running_requests is known, so it should not scale with # max_running_requests is known, so it should not scale with
# full-token capacity here. # full-token capacity here.
c128_state_ratio = 0 c128_state_ratio = 0
# Cap mode and ring mode both move the SWA pool and the c4 state that
# follows it out of the coefficient and into fixed bytes.
swa_ratio = (
0 if self._unified or self.swa_cap_tokens is not None else self.swa_ratio
)
c4_frac = 1 / (4 * self.c4_shrink_factor) c4_frac = 1 / (4 * self.c4_shrink_factor)
return ( return (
# Ring mode: SWA is a fixed per-request pool (see _fixed_swa_bytes). swa_ratio * self.bytes_per_swa_token
( + self.low_ratio_bytes_per_full_token
0.0 + c4_frac * self.kv_bytes * self.num_layers_ca4
if self._unified + 1 / 128 * self.kv_bytes * self.num_layers_ca128
else self.swa_ratio * kv_bytes * self.num_layers_total
)
+ c4_frac * kv_bytes * self.num_layers_ca4
+ 1 / 128 * kv_bytes * self.num_layers_ca128
+ 1 / 4 * self.indexer_bytes_per_token * self.num_layers_ca4 + 1 / 4 * self.indexer_bytes_per_token * self.num_layers_ca4
# Ring mode: C4 state is per-request too (see _fixed_c4_state_bytes).
+ (
0.0
if self._unified
else self.swa_ratio
* c4_state_ratio
* c4_state_bytes
* self.num_layers_ca4
)
+ c128_state_ratio * c128_state_bytes * self.num_layers_ca128 + c128_state_ratio * c128_state_bytes * self.num_layers_ca128
+ (
0.0
if self._unified
else self.swa_ratio
* c4_state_ratio
* c4_indexer_state_bytes
* self.num_layers_ca4
) )
def _get_swa_fixed_bytes(self) -> float:
"""Bias bytes the SWA pool takes in cap mode; 0 when sizing by ratio."""
paged_bytes = (
0
if self.swa_cap_tokens is None
else self.swa_cap_tokens * self.bytes_per_swa_token
) )
return self.request_window_bytes + paged_bytes
def _get_swa_tokens(self, full_token: int, page_size: int) -> int:
# swa_cap_tokens was already page-aligned at resolve time.
if self.swa_cap_tokens is None:
return int(full_token * self.swa_ratio) // page_size * page_size
return self.swa_cap_tokens
def _compute_dsv4_sizes(self, full_token: int, page_size: int) -> _DSV4PoolSizes: def _compute_dsv4_sizes(self, full_token: int, page_size: int) -> _DSV4PoolSizes:
full_token = full_token // page_size * page_size full_token = full_token // page_size * page_size
swa_tokens = int(full_token * self.swa_ratio) // page_size * page_size swa_tokens = self._get_swa_tokens(full_token, page_size)
if self.swa_cap_tokens is None:
# Only ratio sizing can under-size a request: cap mode sizes from the
# request floor, and encoder replay deliberately runs swa_tokens == 0.
if not self._unified: if not self._unified:
# Ring mode: the paged SWA pool is vestigial, so its floor does not apply. self.validate_swa_pool_size(
self.validate_swa_pool_size(swa_tokens, self.sliding_window_size, page_size) swa_tokens, self.sliding_window_size, page_size
)
source = "explicit" if self.operator_swa_ratio is not None else "default"
mode = (
"ring (paged swa_tokens vestigial)"
if self._unified
else f"ratio ({source})"
)
logger.info(
f"DSV4 SWA sizing: mode={mode}, swa_tokens={swa_tokens}, "
f"swa_full_tokens_ratio={self.swa_ratio}"
)
else:
logger.info(
f"DSV4 SWA sizing: mode=cap, swa_tokens={swa_tokens}, "
f"request_cap+headroom={self.swa_cap_tokens}, "
f"prefix_tails={self.swa_prefix_tails}"
)
return _DSV4PoolSizes( return _DSV4PoolSizes(
full_max_total_num_tokens=full_token, full_max_total_num_tokens=full_token,
swa_max_total_num_tokens=swa_tokens, swa_max_total_num_tokens=swa_tokens,
@@ -1195,18 +1332,17 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
return 0 return 0
_, c128_state_dtype_size = _get_dsv4_compress_state_dtype_sizes() _, c128_state_dtype_size = _get_dsv4_compress_state_dtype_sizes()
attn_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
num_req_slots = self._get_num_req_slots(max_running_requests) num_req_slots = self._get_num_req_slots(max_running_requests)
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get(): if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
state_rows = num_req_slots + self.c128_ring_size + 1 state_rows = num_req_slots + self.c128_ring_size + 1
state_rows *= 1 + self.online_c128_mtp_max_draft_tokens state_rows *= 1 + self.online_c128_mtp_max_draft_tokens
state_last_dim = 3 * attn_head_dim state_last_dim = 3 * self.attn_head_dim
else: else:
state_pool_size = num_req_slots * self.c128_ring_size state_pool_size = num_req_slots * self.c128_ring_size
state_rows = state_pool_size + self.c128_ring_size + 1 state_rows = state_pool_size + self.c128_ring_size + 1
state_rows = ceil_div(state_rows, 128) * 128 state_rows = ceil_div(state_rows, 128) * 128
state_last_dim = 2 * attn_head_dim state_last_dim = 2 * self.attn_head_dim
return ( return (
state_rows * state_last_dim * c128_state_dtype_size * self.num_layers_ca128 state_rows * state_last_dim * c128_state_dtype_size * self.num_layers_ca128
@@ -1313,14 +1449,23 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
max_running_requests_per_worker max_running_requests_per_worker
) )
available_bytes_for_tokens = max( swa_fixed_bytes = self._get_swa_fixed_bytes()
available_bytes fixed_bytes = (
- c128_state_fixed_bytes c128_state_fixed_bytes
- swa_ring_fixed_bytes + swa_fixed_bytes
- c4_state_fixed_bytes, + swa_ring_fixed_bytes
0, + c4_state_fixed_bytes
) )
available_bytes_for_tokens = max(available_bytes - fixed_bytes, 0)
full_token = int(available_bytes_for_tokens / self.bytes_per_full_token) full_token = int(available_bytes_for_tokens / self.bytes_per_full_token)
if full_token <= 0 and self.swa_cap_tokens is not None:
raise RuntimeError(
f"The DSV4 SWA pool cap ({self.swa_cap_tokens} tokens, "
f"{swa_fixed_bytes / (1 << 30):.2f} GB) leaves no room for the full "
f"KV pool within the available {available_bytes / (1 << 30):.2f} GB. "
f"Reduce --max-running-requests, lower --swa-prefix-tails "
f"or SGLANG_SWA_EVICTION_INTERVAL, or increase --mem-fraction-static."
)
sizes = self._compute_dsv4_sizes(full_token, page_size) sizes = self._compute_dsv4_sizes(full_token, page_size)
logger.info( logger.info(
@@ -1329,6 +1474,7 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
f"bytes_per_full_token={self.bytes_per_full_token:.2f}, " f"bytes_per_full_token={self.bytes_per_full_token:.2f}, "
f"available_bytes={available_bytes / (1 << 30):.2f} GB, " f"available_bytes={available_bytes / (1 << 30):.2f} GB, "
f"c128_state_fixed={c128_state_fixed_bytes / (1 << 30):.2f} GB, " f"c128_state_fixed={c128_state_fixed_bytes / (1 << 30):.2f} GB, "
f"swa_fixed={swa_fixed_bytes / (1 << 30):.2f} GB, "
f"swa_ring_fixed={swa_ring_fixed_bytes / (1 << 30):.2f} GB, " f"swa_ring_fixed={swa_ring_fixed_bytes / (1 << 30):.2f} GB, "
f"c4_state_fixed={c4_state_fixed_bytes / (1 << 30):.2f} GB, " f"c4_state_fixed={c4_state_fixed_bytes / (1 << 30):.2f} GB, "
f"full_token={sizes.full_max_total_num_tokens}" f"full_token={sizes.full_max_total_num_tokens}"
@@ -635,7 +635,11 @@ class BaseRunner(ABC):
spec_algorithm=mr.spec_algorithm, spec_algorithm=mr.spec_algorithm,
spec_info=spec_info, spec_info=spec_info,
capture_hidden_mode=capture_hidden_mode, capture_hidden_mode=capture_hidden_mode,
num_token_non_padded=buffers.num_token_non_padded, # Maintained only under expert parallelism; None elsewhere so routing
# does not mask every row against a never-filled zero count.
num_token_non_padded=(
buffers.num_token_non_padded if enable_num_token_non_padded() else None
),
global_forward_mode=capture_forward_mode, global_forward_mode=capture_forward_mode,
lora_ids=lora_ids, lora_ids=lora_ids,
) )
@@ -649,6 +653,8 @@ class BaseRunner(ABC):
forward_batch = mr.prepare_dummy_forward_batch(forward_batch) forward_batch = mr.prepare_dummy_forward_batch(forward_batch)
mr.attn_backend.init_forward_metadata(forward_batch) mr.attn_backend.init_forward_metadata(forward_batch)
if get_exec().features.enable_encoder_swa_bounded_replay:
mr.token_to_kv_pool.request_window.initialize_dummy_history()
def run_once(): def run_once():
# Reused dummy batches may carry DP-local lazy caches from a prior # Reused dummy batches may carry DP-local lazy caches from a prior
@@ -52,6 +52,7 @@ from sglang.srt.layers.attention.dsa.utils import is_dsa_enable_prefill_cp
from sglang.srt.layers.attention.graph_variants import ( from sglang.srt.layers.attention.graph_variants import (
AttentionGraphVariants, AttentionGraphVariants,
create_attention_graph_variants, create_attention_graph_variants,
create_dsv41_candidate_graph_variants,
) )
from sglang.srt.layers.cp.utils import is_mla_cp_enabled from sglang.srt.layers.cp.utils import is_mla_cp_enabled
from sglang.srt.layers.dp_attention import ( from sglang.srt.layers.dp_attention import (
@@ -300,6 +301,9 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
self.attention_graph_variants: Optional[AttentionGraphVariants] = ( self.attention_graph_variants: Optional[AttentionGraphVariants] = (
create_attention_graph_variants(model_runner.model_config.hf_config) create_attention_graph_variants(model_runner.model_config.hf_config)
or create_dsv41_candidate_graph_variants(
model_runner, self.capture_forward_mode, self.captured_req_width
)
) )
# --- bucket sizes --------------------------------------------- # --- bucket sizes ---------------------------------------------
@@ -965,7 +969,11 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
spec_algorithm=self.model_runner.spec_algorithm, spec_algorithm=self.model_runner.spec_algorithm,
spec_info=spec_info, spec_info=spec_info,
capture_hidden_mode=self.capture_hidden_mode, capture_hidden_mode=self.capture_hidden_mode,
num_token_non_padded=buffers.num_token_non_padded, # Maintained only under expert parallelism; None elsewhere so routing
# does not mask every row against a never-filled zero count.
num_token_non_padded=(
buffers.num_token_non_padded if enable_num_token_non_padded() else None
),
attn_tp_sequence_sharded=attn_tp_sharded, attn_tp_sequence_sharded=attn_tp_sharded,
global_forward_mode=self.capture_forward_mode, global_forward_mode=self.capture_forward_mode,
lora_ids=lora_ids, lora_ids=lora_ids,
@@ -342,7 +342,7 @@ def maybe_flashinfer_autotune_speculative_draft(
def maybe_flashinfer_autotune_extend( def maybe_flashinfer_autotune_extend(
runner: BaseRunner, *, decode_num_tokens: int runner: BaseRunner, *, decode_num_tokens: int
) -> None: ) -> None:
"""Also autotune one EXTEND-shaped dummy forward. """Also autotune kernels at the prefill token ceiling.
The decode-shaped autotune only covers token counts up to the decode The decode-shaped autotune only covers token counts up to the decode
batch size, so larger prefill/extend batches fall outside the tuned batch size, so larger prefill/extend batches fall outside the tuned
@@ -351,14 +351,27 @@ def maybe_flashinfer_autotune_extend(
untuned at >=8k tokens on sm100). One extra forward at the largest untuned at >=8k tokens on sm100). One extra forward at the largest
per-rank extend token count tunes all buckets up to it. per-rank extend token count tunes all buckets up to it.
""" """
if not envs.SGLANG_FLASHINFER_AUTOTUNE_EXTEND.get():
return
mr = runner.model_runner mr = runner.model_runner
# Prefer the per-rank scheduler buffer while preserving the legacy ceiling # Prefer the per-rank scheduler buffer while preserving the legacy ceiling
# when chunked prefill is disabled. # when chunked prefill is disabled.
num_tokens = max_prefill_buffer_tokens() or get_schedule().max_prefill_tokens num_tokens = max_prefill_buffer_tokens() or get_schedule().max_prefill_tokens
if num_tokens <= (decode_num_tokens or 0): if num_tokens <= (decode_num_tokens or 0):
return # decode-shaped autotune already covered these buckets return # decode-shaped autotune already covered these buckets
# DSpark's dummy forward is TARGET_VERIFY-shaped and misses large prefill GEMMs.
prefill_autotune = getattr(mr.model, "autotune_prefill_kernels", None)
wants_prefill_autotune = getattr(mr.model, "wants_prefill_autotune", None)
if wants_prefill_autotune is not None and not wants_prefill_autotune():
# Entering the autotune context loads / saves the tactic cache and syncs
# ranks, so a model that has nothing to tune must decline before it.
prefill_autotune = None
if prefill_autotune is not None and mr.is_generation and not mr.is_draft_worker:
with flashinfer_autotune_context(mr, run_lm_head=False):
tuned = prefill_autotune(num_tokens, dtype=mr.dtype)
if tuned:
return
if not envs.SGLANG_FLASHINFER_AUTOTUNE_EXTEND.get():
return
is_pd_prefill_target = ( is_pd_prefill_target = (
get_disagg().disaggregation_mode == "prefill" and not mr.is_draft_worker get_disagg().disaggregation_mode == "prefill" and not mr.is_draft_worker
) )
@@ -84,6 +84,7 @@ from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode, CaptureHiddenMode,
ForwardBatch, ForwardBatch,
ForwardMode, ForwardMode,
NgramEmbeddingInfo,
PPProxyTensors, PPProxyTensors,
compute_local_num_token_non_padded, compute_local_num_token_non_padded,
enable_num_token_non_padded, enable_num_token_non_padded,
@@ -283,6 +284,8 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
buffer population, attention metadata init, and output slicing. buffer population, attention metadata init, and output slicing.
""" """
_backend_can_run_prefill_cuda_graph = None
def __init__(self, model_runner: ModelRunner): def __init__(self, model_runner: ModelRunner):
if get_schedule().enable_mixed_chunk: if get_schedule().enable_mixed_chunk:
backend = get_exec().graph.cuda_graph_config.prefill.backend backend = get_exec().graph.cuda_graph_config.prefill.backend
@@ -291,6 +294,9 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
f"graph backend; got '{backend}'." f"graph backend; got '{backend}'."
) )
super().__init__(model_runner) super().__init__(model_runner)
self._backend_can_run_prefill_cuda_graph = getattr(
model_runner.attn_backend, "can_run_prefill_cuda_graph", None
)
# --- model flags ---------------------------------------------- # --- model flags ----------------------------------------------
self.quant_config = getattr(model_runner.model, "quant_config", None) self.quant_config = getattr(model_runner.model, "quant_config", None)
self.is_multimodal = model_runner.model_config.is_multimodal self.is_multimodal = model_runner.model_config.is_multimodal
@@ -1332,6 +1338,9 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
is None is None
): ):
return False return False
backend_can_run = self._backend_can_run_prefill_cuda_graph
if backend_can_run is not None and not backend_can_run(forward_batch):
return False
# Multi-req replay is supported by body-capture backends via the # Multi-req replay is supported by body-capture backends via the
# layer_model.forward monkey-patch in replay(): the captured graph runs # layer_model.forward monkey-patch in replay(): the captured graph runs
# the transformer stack, then the outer model.forward runs # the transformer stack, then the outer model.forward runs
@@ -1497,6 +1506,15 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
return_pooled_hidden_states=self.capture_return_pooled_hidden_states, return_pooled_hidden_states=self.capture_return_pooled_hidden_states,
max_seq_len_override=self.max_context_size, max_seq_len_override=self.max_context_size,
) )
ngram_manager = self.model_runner.ngram_embedding_manager
if ngram_manager.enabled:
forward_batch.ngram_embedding_info = NgramEmbeddingInfo.create(
ngram_manager.table,
bs,
self.device,
column_starts=0,
req_lens=shape_inputs["extend_seq_lens"],
)
self.tbo_plugin.capture_one_batch_size(forward_batch, num_tokens=num_tokens) self.tbo_plugin.capture_one_batch_size(forward_batch, num_tokens=num_tokens)
return forward_batch, self.model_runner.attn_backend return forward_batch, self.model_runner.attn_backend
@@ -1827,6 +1845,9 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
), ),
max_seq_len_override=self.max_context_size, max_seq_len_override=self.max_context_size,
) )
# The n-gram hasher runs outside the graph and reads this at replay.
static_forward_batch.ngram_embedding_info = forward_batch.ngram_embedding_info
static_forward_batch.engram_history = forward_batch.engram_history
if self._is_full_backend: if self._is_full_backend:
forward_batch.next_token_logits_buffer = ( forward_batch.next_token_logits_buffer = (
static_forward_batch.next_token_logits_buffer static_forward_batch.next_token_logits_buffer
@@ -1901,6 +1922,26 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
return static_forward_batch return static_forward_batch
def _fill_input_embeds_slot(self, args, layer_kwargs, static_num_tokens: int):
"""A text-only batch would otherwise replay the captured input_embeds."""
ie_idx = self._input_embeds_arg_idx
ie = layer_kwargs.get("input_embeds")
if ie is None and ie_idx is not None and len(args) > ie_idx:
ie = args[ie_idx]
if ie is None:
input_ids = layer_kwargs.get("input_ids")
if input_ids is None and len(args) > 0:
input_ids = args[0]
embed = getattr(self.model_runner.model, "get_input_embeddings", None)
assert input_ids is not None and embed is not None, (
"prefill CUDA graph replay needs input_embeds for the static "
"slot, and the model exposes no get_input_embeddings()"
)
ie = embed()(input_ids)
self.buffer_registry.get_slot("input_embeds").slice_for(1, static_num_tokens)[
: ie.shape[0]
].copy_(ie)
def _execute_body_capture( def _execute_body_capture(
self, self,
forward_batch: ForwardBatch, forward_batch: ForwardBatch,
@@ -1913,7 +1954,6 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
# BCG / Full: replay the captured body, run the LM head + # BCG / Full: replay the captured body, run the LM head +
# logits_processor eagerly. # logits_processor eagerly.
full_path = self._is_full_backend full_path = self._is_full_backend
ie_idx = self._input_embeds_arg_idx
def replay_layer_forward(*args, **layer_kwargs): def replay_layer_forward(*args, **layer_kwargs):
# The captured body graph reads activations from the static # The captured body graph reads activations from the static
@@ -1925,16 +1965,7 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
# Copy them into the slot before replay so the graph sees the # Copy them into the slot before replay so the graph sees the
# current request's embeddings (mirrors main's BCG closure). # current request's embeddings (mirrors main's BCG closure).
if self.buffer_registry.has_slot("input_embeds"): if self.buffer_registry.has_slot("input_embeds"):
ie = layer_kwargs.get("input_embeds") self._fill_input_embeds_slot(args, layer_kwargs, static_num_tokens)
if ie is None and ie_idx is not None and len(args) > ie_idx:
ie = args[ie_idx]
if ie is None:
# Otherwise the graph replays the previous batch's embeddings.
input_ids = args[0] if args else layer_kwargs["input_ids"]
ie = self.model_runner.model.get_input_embeddings()(input_ids)
self.buffer_registry.get_slot("input_embeds").slice_for(
1, static_num_tokens
)[: ie.shape[0]].copy_(ie)
hs = self.backend.replay(shape_key, static_forward_batch, **kwargs) hs = self.backend.replay(shape_key, static_forward_batch, **kwargs)
return _slice_output_rows(hs, raw_num_tokens) if full_path else hs return _slice_output_rows(hs, raw_num_tokens) if full_path else hs
@@ -26,5 +26,5 @@ class ShapeKey:
stream_idx: Optional[int] = None stream_idx: Optional[int] = None
# LoRA or prefill-prefix variant; None selects the default. # LoRA or prefill-prefix variant; None selects the default.
variant_label: Optional[str] = None variant_label: Optional[str] = None
# Independent attention variant; None selects the default. # Independent attention variant (DSA dense/sparse, candidate_*); None is default.
attention_variant: Optional[str] = None attention_variant: Optional[str] = None
@@ -72,6 +72,13 @@ def get_capture_attention_variant() -> Optional[str]:
return _capture_attention_variant return _capture_attention_variant
def skip_low_ratio_indexer(compress_ratio: int) -> bool:
"""Whether the captured candidate variant selects every position for this ratio."""
return _capture_attention_variant == "candidate_all" or (
_capture_attention_variant == "candidate_c2_all" and compress_ratio == 2
)
def _set_capture_attention_variant(variant: Optional[str]) -> None: def _set_capture_attention_variant(variant: Optional[str]) -> None:
global _capture_attention_variant global _capture_attention_variant
_capture_attention_variant = variant _capture_attention_variant = variant
+197 -9
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import logging import logging
from contextlib import contextmanager, nullcontext from contextlib import contextmanager, nullcontext
from functools import cached_property
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
import torch import torch
@@ -92,6 +93,7 @@ from sglang.srt.layers.moe import (
get_moe_a2a_backend, get_moe_a2a_backend,
get_moe_runner_backend, get_moe_runner_backend,
post_experts_all_reduce, post_experts_all_reduce,
should_skip_post_experts_all_reduce,
should_use_flashinfer_cutlass_moe_fp4_allgather, should_use_flashinfer_cutlass_moe_fp4_allgather,
) )
from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class
@@ -121,7 +123,11 @@ from sglang.srt.layers.quantization.fp8_utils import (
view_aiter_fused_rms_transposed_fp8_scale, view_aiter_fused_rms_transposed_fp8_scale,
) )
from sglang.srt.layers.quantization.mxfp4_flashinfer_trtllm_moe import ( from sglang.srt.layers.quantization.mxfp4_flashinfer_trtllm_moe import (
Mxfp4FlashinferTrtllmMoEMethod,
Mxfp8RoutedInputPreQuant,
maybe_fuse_routed_scale_and_shared_add, maybe_fuse_routed_scale_and_shared_add,
routed_hidden_size,
should_use_fuse_finalize_all_reduce,
) )
from sglang.srt.layers.radix_attention import RadixAttention from sglang.srt.layers.radix_attention import RadixAttention
from sglang.srt.layers.rotary_embedding import get_rope_wrapper from sglang.srt.layers.rotary_embedding import get_rope_wrapper
@@ -180,6 +186,7 @@ from sglang.srt.models.deepseek_common.utils import (
quant_blocks_shared_experts_fusion, quant_blocks_shared_experts_fusion,
tiny_router_gemm_max_tokens, tiny_router_gemm_max_tokens,
) )
from sglang.srt.multimodal.dsv41.vl_routing import vision_topk
from sglang.srt.runtime_context import ( from sglang.srt.runtime_context import (
attention_backends, attention_backends,
get_device, get_device,
@@ -456,6 +463,7 @@ class MoEGate(nn.Module):
prefix: str = "", prefix: str = "",
is_hash_moe: bool = False, is_hash_moe: bool = False,
is_deepseek_v4: bool = False, is_deepseek_v4: bool = False,
vl_correction_bias: bool = False,
): ):
super().__init__() super().__init__()
self.is_deepseek_v4 = is_deepseek_v4 self.is_deepseek_v4 = is_deepseek_v4
@@ -488,6 +496,12 @@ class MoEGate(nn.Module):
self.e_score_correction_bias = nn.Parameter(correction_bias) self.e_score_correction_bias = nn.Parameter(correction_bias)
else: else:
self.e_score_correction_bias = None self.e_score_correction_bias = None
self.e_score_correction_bias_vl = None
if vl_correction_bias:
self.e_score_correction_bias_vl = nn.Parameter(
torch.empty(config.n_routed_experts, dtype=torch.float32),
requires_grad=False,
)
if _is_cpu and _is_cpu_amx_available: if _is_cpu and _is_cpu_amx_available:
self.quant_method = PackWeightMethod(weight_names=["weight"]) self.quant_method = PackWeightMethod(weight_names=["weight"])
self.tiny_router_gemm_max_tokens = tiny_router_gemm_max_tokens( self.tiny_router_gemm_max_tokens = tiny_router_gemm_max_tokens(
@@ -538,6 +552,11 @@ class MoEGate(nn.Module):
return logits return logits
# 96 rows of 5120 bf16 fit the 1 MiB CustomAllReduceV2 push slot the whole
# [T, hidden] view is staged through.
_FUSED_FINALIZE_ALL_REDUCE_MAX_TOKENS = 96
class DeepseekV2MoE(nn.Module): class DeepseekV2MoE(nn.Module):
def __init__( def __init__(
self, self,
@@ -546,8 +565,10 @@ class DeepseekV2MoE(nn.Module):
quant_config: Optional[QuantizationConfig] = None, quant_config: Optional[QuantizationConfig] = None,
prefix: str = "", prefix: str = "",
alt_stream: Optional[torch.cuda.Stream] = None, alt_stream: Optional[torch.cuda.Stream] = None,
routed_quant_stream: Optional[torch.cuda.Stream] = None,
is_nextn: bool = False, is_nextn: bool = False,
is_deepseek_v4: bool = False, is_deepseek_v4: bool = False,
vl_correction_bias: bool = False,
): ):
super().__init__() super().__init__()
self.tp_size = get_parallel().tp_size self.tp_size = get_parallel().tp_size
@@ -585,7 +606,14 @@ class DeepseekV2MoE(nn.Module):
self.config = config self.config = config
self.layer_id = layer_id self.layer_id = layer_id
self.alt_stream = alt_stream self.alt_stream = alt_stream
self.routed_quant_stream = routed_quant_stream
self.is_nextn = is_nextn self.is_nextn = is_nextn
self._fuse_finalize_all_reduce = (
is_deepseek_v4
and getattr(config, "hc_pre_from_prev_sublayer", False)
and get_platform().is_blackwell
and self.tp_size == 4
)
n_hash_layers = getattr(config, "num_hash_layers", 0) n_hash_layers = getattr(config, "num_hash_layers", 0)
self.is_hash = layer_id < n_hash_layers and not (is_deepseek_v4 and is_nextn) self.is_hash = layer_id < n_hash_layers and not (is_deepseek_v4 and is_nextn)
@@ -608,6 +636,7 @@ class DeepseekV2MoE(nn.Module):
prefix=add_prefix("gate", prefix), prefix=add_prefix("gate", prefix),
is_hash_moe=self.is_hash, is_hash_moe=self.is_hash,
is_deepseek_v4=is_deepseek_v4, is_deepseek_v4=is_deepseek_v4,
vl_correction_bias=vl_correction_bias,
) )
# scaling factor for fused shared experts on AMD-platform. # scaling factor for fused shared experts on AMD-platform.
@@ -681,6 +710,12 @@ class DeepseekV2MoE(nn.Module):
topk_kwargs.update( topk_kwargs.update(
use_grouped_topk=False, use_grouped_topk=False,
scoring_func=config.scoring_func, scoring_func=config.scoring_func,
sqrtsoftplus_log1p=(
getattr(config, "model_type", None) == "deepseek_v41"
),
fused_gate_packed_ids=(
getattr(config, "model_type", None) == "deepseek_v41"
),
is_fp4_experts=getattr(quant_config, "is_fp4_experts", False), is_fp4_experts=getattr(quant_config, "is_fp4_experts", False),
apply_routed_scaling_factor_on_output=( apply_routed_scaling_factor_on_output=(
True True
@@ -951,6 +986,13 @@ class DeepseekV2MoE(nn.Module):
else self._maybe_quant_moe_input_once(hidden_states) else self._maybe_quant_moe_input_once(hidden_states)
) )
self.alt_stream.wait_stream(current_stream) self.alt_stream.wait_stream(current_stream)
should_quant_routed_input_mxfp8 = (
not use_flashinfer_trtllm_bypass
and pre_quant_input is None
and self._should_quant_routed_input_mxfp8(hidden_states)
)
if should_quant_routed_input_mxfp8:
self.routed_quant_stream.wait_stream(current_stream)
has_shared_output = ( has_shared_output = (
hidden_states.shape[0] > 0 and self.num_fused_shared_experts == 0 hidden_states.shape[0] > 0 and self.num_fused_shared_experts == 0
) )
@@ -959,6 +1001,7 @@ class DeepseekV2MoE(nn.Module):
if get_exec().moe.enable_eplb and not self.is_nextn if get_exec().moe.enable_eplb and not self.is_nextn
else None else None
) )
# router_logits: (num_tokens, n_experts) # router_logits: (num_tokens, n_experts)
router_logits = self.gate(hidden_states, gemm_output_zero_allocator) router_logits = self.gate(hidden_states, gemm_output_zero_allocator)
if use_flashinfer_trtllm_bypass: if use_flashinfer_trtllm_bypass:
@@ -973,6 +1016,14 @@ class DeepseekV2MoE(nn.Module):
if getattr(self, "is_hash", False) if getattr(self, "is_hash", False)
else {} else {}
) )
if self.gate.e_score_correction_bias_vl is not None:
topk_output = vision_topk(
self,
router_logits,
input_ids_global,
num_token_non_padded=num_token_non_padded,
)
else:
topk_output = self.topk( topk_output = self.topk(
hidden_states, hidden_states,
router_logits, router_logits,
@@ -980,7 +1031,29 @@ class DeepseekV2MoE(nn.Module):
expert_location_dispatch_info=dispatch_info, expert_location_dispatch_info=dispatch_info,
**topk_kwargs, **topk_kwargs,
) )
deferred_finalize = ( # Issued after the router so the main chain stays on the main stream at replay.
routed_pre_quant_input = pre_quant_input
if should_quant_routed_input_mxfp8:
with torch.cuda.stream(self.routed_quant_stream):
x_q, x_sf = self.experts.quant_method.quantize_routed_input(
hidden_states, routed_hidden_size(self.experts)
)
ready = self.routed_quant_stream.record_event()
routed_pre_quant_input = Mxfp8RoutedInputPreQuant(x_q, x_sf, ready)
# The mHC post-split consumes the reduced row without an RMSNorm.
use_fused_finalize_all_reduce = (
self._fuse_finalize_all_reduce
and has_shared_output
and hidden_states.shape[-1] == 5120
and not self._shared_expert_tp1
and self.tp_size > 1
and hidden_states.shape[0] <= _FUSED_FINALIZE_ALL_REDUCE_MAX_TOKENS
and not should_skip_post_experts_all_reduce(is_tp_path=True)
and should_use_fuse_finalize_all_reduce(
self.experts, hidden_states.shape[0], hidden_states.shape[-1]
)
)
deferred_finalize = use_fused_finalize_all_reduce or (
has_shared_output has_shared_output
and not self._shared_expert_tp1 and not self._shared_expert_tp1
and topk_output.format == TopKOutputFormat.BYPASSED and topk_output.format == TopKOutputFormat.BYPASSED
@@ -988,13 +1061,13 @@ class DeepseekV2MoE(nn.Module):
) )
if deferred_finalize: if deferred_finalize:
final_hidden_states = self.experts.forward_deferred_finalize( final_hidden_states = self.experts.forward_deferred_finalize(
hidden_states, topk_output hidden_states, topk_output, pre_quant_input=routed_pre_quant_input
) )
elif use_flashinfer_trtllm_bypass: elif use_flashinfer_trtllm_bypass:
final_hidden_states = self.experts.forward_impl(hidden_states, topk_output) final_hidden_states = self.experts.forward_impl(hidden_states, topk_output)
elif pre_quant_input is not None: elif routed_pre_quant_input is not None:
final_hidden_states = self.experts( final_hidden_states = self.experts(
hidden_states, topk_output, pre_quant_input=pre_quant_input hidden_states, topk_output, pre_quant_input=routed_pre_quant_input
) )
else: else:
final_hidden_states = self.experts(hidden_states, topk_output) final_hidden_states = self.experts(hidden_states, topk_output)
@@ -1007,6 +1080,7 @@ class DeepseekV2MoE(nn.Module):
final_hidden_states *= self.routed_scaling_factor final_hidden_states *= self.routed_scaling_factor
# Shared expert on alt stream, issued AFTER the main (routed) branch. See note above. # Shared expert on alt stream, issued AFTER the main (routed) branch. See note above.
# Only the quant-once fp8 pair is shared with it; the routed MXFP8 pre-quant is not.
with torch.cuda.stream(self.alt_stream): with torch.cuda.stream(self.alt_stream):
shared_output = self._forward_shared_experts( shared_output = self._forward_shared_experts(
hidden_states, hidden_states,
@@ -1014,15 +1088,82 @@ class DeepseekV2MoE(nn.Module):
pre_quant_input=pre_quant_input, pre_quant_input=pre_quant_input,
) )
# The routed-input pre-quant was already joined inside the routed MoE apply.
current_stream.wait_stream(self.alt_stream) current_stream.wait_stream(self.alt_stream)
all_reduce_done = False
if deferred_finalize: if deferred_finalize:
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import ( from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
finalize_flashinfer_trtllm_deferred_output, finalize_flashinfer_trtllm_deferred_output,
) )
deferred = final_hidden_states
if (
use_fused_finalize_all_reduce
and deferred.gemm2_out.shape[1] == hidden_states.shape[-1]
):
from sglang.kernels.ops.communication.all_reduce_fusion import (
moe_finalize_all_reduce,
)
from sglang.srt.layers.moe.mhc_post_fusion import (
current_mhc_post_fusion,
)
mhc = current_mhc_post_fusion()
if mhc is not None:
from sglang.kernels.ops.communication.all_reduce_mhc import (
moe_finalize_all_reduce_mhc,
)
# Join the coefficients before the fused epilogue reads them.
mhc.materialize_stats()
if mhc.stats_stream is not None:
current_stream.wait_stream(mhc.stats_stream)
args = (
deferred.gemm2_out,
deferred.expanded_idx_to_permuted_idx,
deferred.expert_weights,
deferred.top_k,
shared_output,
mhc.residual,
mhc.post,
mhc.comb,
)
if mhc.norm_weight is not None:
from sglang.kernels.ops.communication.all_reduce_mhc import (
moe_finalize_all_reduce_mhc_quant,
)
final_hidden_states, mhc.output, mhc.normalized, q, sf = (
moe_finalize_all_reduce_mhc_quant(
*args,
mhc.pre,
mhc.norm_weight,
mhc.norm_eps,
world_size=self.tp_size,
)
)
mhc.quantized = (q, sf)
else:
final_hidden_states, mhc.output = moe_finalize_all_reduce_mhc(
*args, world_size=self.tp_size
)
else:
final_hidden_states = moe_finalize_all_reduce(
deferred.gemm2_out,
deferred.expanded_idx_to_permuted_idx,
deferred.expert_weights,
deferred.top_k,
shared_output,
world_size=self.tp_size,
hidden_dim=hidden_states.shape[-1],
# Routing metadata must be ready before it is consumed.
prefetch_metadata=False,
)
all_reduce_done = True
else:
final_hidden_states = finalize_flashinfer_trtllm_deferred_output( final_hidden_states = finalize_flashinfer_trtllm_deferred_output(
final_hidden_states, deferred,
shared_output, shared_output,
) )
else: else:
@@ -1033,6 +1174,7 @@ class DeepseekV2MoE(nn.Module):
self.routed_scaling_factor, self.routed_scaling_factor,
) )
if not all_reduce_done:
final_hidden_states = post_experts_all_reduce(final_hidden_states) final_hidden_states = post_experts_all_reduce(final_hidden_states)
# TP1 shared experts are replicated, so add them after all-reduce to # TP1 shared experts are replicated, so add them after all-reduce to
# avoid summing the same shared output once per TP rank. # avoid summing the same shared output once per TP rank.
@@ -1088,6 +1230,14 @@ class DeepseekV2MoE(nn.Module):
if getattr(self, "is_hash", False) if getattr(self, "is_hash", False)
else {} else {}
) )
if self.gate.e_score_correction_bias_vl is not None:
topk_output = vision_topk(
self,
router_logits,
input_ids_global,
num_token_non_padded=num_token_non_padded,
)
else:
topk_output = self.topk( topk_output = self.topk(
hidden_states, hidden_states,
router_logits, router_logits,
@@ -1108,7 +1258,6 @@ class DeepseekV2MoE(nn.Module):
def _pre_combine_hook( def _pre_combine_hook(
dispatcher: BaseDispatcher, combine_input: CombineInput dispatcher: BaseDispatcher, combine_input: CombineInput
): ):
nonlocal shared_output nonlocal shared_output
self.alt_stream.wait_stream(torch.cuda.current_stream()) self.alt_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(self.alt_stream): with torch.cuda.stream(self.alt_stream):
@@ -1341,7 +1490,6 @@ class DeepseekV2MoE(nn.Module):
def _post_dispatch_hook( def _post_dispatch_hook(
dispatcher: BaseDispatcher, dispatch_output: DispatchOutput dispatcher: BaseDispatcher, dispatch_output: DispatchOutput
): ):
combine_overlap_args, down_gemm_overlap_args, meta_overlap_args = ( combine_overlap_args, down_gemm_overlap_args, meta_overlap_args = (
compute_overlap_args(dispatch_output, self.alt_stream) compute_overlap_args(dispatch_output, self.alt_stream)
) )
@@ -1359,7 +1507,6 @@ class DeepseekV2MoE(nn.Module):
def _pre_combine_hook( def _pre_combine_hook(
dispatcher: BaseDispatcher, combine_input: CombineInput dispatcher: BaseDispatcher, combine_input: CombineInput
): ):
nonlocal shared_output nonlocal shared_output
if ( if (
@@ -1397,7 +1544,6 @@ class DeepseekV2MoE(nn.Module):
def _post_dispatch_hook( def _post_dispatch_hook(
dispatcher: BaseDispatcher, dispatch_output: DispatchOutput dispatcher: BaseDispatcher, dispatch_output: DispatchOutput
): ):
combine_overlap_args, down_gemm_overlap_args, meta_overlap_args = ( combine_overlap_args, down_gemm_overlap_args, meta_overlap_args = (
compute_overlap_args(dispatch_output, self.alt_stream) compute_overlap_args(dispatch_output, self.alt_stream)
) )
@@ -1595,6 +1741,48 @@ class DeepseekV2MoE(nn.Module):
q, s = sglang_per_token_group_quant_fp8_row_padded(hidden_states, 128) q, s = sglang_per_token_group_quant_fp8_row_padded(hidden_states, 128)
return q, s return q, s
@cached_property
def _routed_mxfp8_prequant_static_enabled(self) -> bool:
return self._compute_routed_mxfp8_prequant_enabled()[0]
def _compute_routed_mxfp8_prequant_enabled(self) -> Tuple[bool, str]:
from sglang.srt.layers.moe.token_dispatcher.standard import StandardDispatcher
if not _is_cuda:
return False, "not CUDA"
if self.routed_quant_stream is None:
return False, "no routed_quant_stream"
if self._enable_a2a_moe or self._fuse_shared_experts_inside_sbo:
return False, "a2a MoE or SBO shared-expert fusion"
if not get_moe_runner_backend().is_flashinfer_mxfp4():
return False, "MoE runner backend not flashinfer_mxfp4"
experts = self.experts
if not isinstance(experts, FusedMoE):
return False, "experts not FusedMoE"
quant_method = experts.quant_method
if not isinstance(quant_method, Mxfp4FlashinferTrtllmMoEMethod):
return False, "experts quant method not Mxfp4FlashinferTrtllmMoEMethod"
if quant_method.flashinfer_mxfp4_moe_precision != "default":
return False, "flashinfer_mxfp4_moe_precision not default (no MXFP8 quant)"
# The pre-quant must describe exactly the tensor apply() receives: the
# standard dispatcher passes hidden_states through, the fp4 all-gather does not.
if not isinstance(experts.dispatcher, StandardDispatcher):
return False, "dispatcher not StandardDispatcher"
if should_use_flashinfer_cutlass_moe_fp4_allgather():
return False, "flashinfer cutlass fp4 all-gather dispatch"
return True, "ok"
def _should_quant_routed_input_mxfp8(self, hidden_states: torch.Tensor) -> bool:
return (
# Capture-only: graph-pool tensors need no record_stream.
torch.cuda.is_current_stream_capturing()
# The piecewise TC graph's MoE op drops pre_quant_input.
and not is_in_tc_piecewise_cuda_graph()
and hidden_states.shape[0] > 0
and hidden_states.dtype == torch.bfloat16
and self._routed_mxfp8_prequant_static_enabled
)
def op_gate(self, state): def op_gate(self, state):
if state.hidden_states_mlp_input.shape[0] > 0: if state.hidden_states_mlp_input.shape[0] > 0:
# router_logits: (num_tokens, n_experts) # router_logits: (num_tokens, n_experts)
File diff suppressed because it is too large Load Diff
+186 -13
View File
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import copy
import logging import logging
from typing import Iterable, List, Optional, Tuple from typing import Iterable, List, Optional, Tuple
@@ -17,6 +18,7 @@ from sglang.kernels.ops.speculative.dspark.dspark_draft_model import (
CommitKvProj, CommitKvProj,
) )
from sglang.srt.configs.deepseek_v4 import DeepSeekV4Config from sglang.srt.configs.deepseek_v4 import DeepSeekV4Config
from sglang.srt.distributed.device_communicators.vocab_gather import make_vocab_gather
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.layers.dp_attention import is_dp_attention_enabled from sglang.srt.layers.dp_attention import is_dp_attention_enabled
from sglang.srt.layers.layernorm import RMSNorm from sglang.srt.layers.layernorm import RMSNorm
@@ -39,6 +41,7 @@ from sglang.srt.models.deepseek_v4 import (
DeepseekV4DecoderLayer, DeepseekV4DecoderLayer,
DeepseekV4ForCausalLM, DeepseekV4ForCausalLM,
MqaAttentionBase, MqaAttentionBase,
_apply_wo_a_bf16_matmul,
_dequant_fp8_wo_a_streaming, _dequant_fp8_wo_a_streaming,
hc_head_torch, hc_head_torch,
make_hc_head_params, make_hc_head_params,
@@ -189,6 +192,22 @@ class DSparkAttention(MqaAttentionBase):
q = self.q_norm(q) q = self.q_norm(q)
q, _ = self.wq_b(q) q, _ = self.wq_b(q)
q = q.view(-1, self.n_local_heads, self.head_dim) q = q.view(-1, self.n_local_heads, self.head_dim)
if not self.q_head_norm:
if self._use_fast_kernel and not _is_npu:
fused_rope_inplace(
q[..., -self.rope_head_dim :],
None,
self.freqs_cis,
positions=positions,
)
else:
apply_rotary_emb(
q[..., -self.rope_head_dim :], self.freqs_cis[positions]
)
if q_out is None:
return q
q_out.copy_(q)
return q_out
if self._use_fast_kernel: if self._use_fast_kernel:
if q_out is None: if q_out is None:
q_out = torch.empty_like(q) q_out = torch.empty_like(q)
@@ -236,7 +255,6 @@ class DSparkAttention(MqaAttentionBase):
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
forward_batch: ForwardBatch, forward_batch: ForwardBatch,
) -> torch.Tensor: ) -> torch.Tensor:
if _is_npu and forward_batch.forward_mode.is_idle(): if _is_npu and forward_batch.forward_mode.is_idle():
return torch.zeros_like(hidden_states) return torch.zeros_like(hidden_states)
@@ -336,7 +354,13 @@ class DSparkAttention(MqaAttentionBase):
) )
wo_a = self.wo_a.weight.view(self.n_local_groups, self.o_lora_rank, -1) wo_a = self.wo_a.weight.view(self.n_local_groups, self.o_lora_rank, -1)
if self._use_fast_kernel: if self._use_fast_kernel:
o = torch.einsum("bgd,grd->bgr", o, wo_a) o = _apply_wo_a_bf16_matmul(
o,
wo_a,
is_decode=forward_batch.forward_mode.is_decode(),
is_target_verify=forward_batch.forward_mode.is_target_verify(),
fast_path=self.is_dsv41,
)
else: else:
o = torch.einsum("bgd,grd->bgr", o.float(), wo_a.float()).to(q.dtype) o = torch.einsum("bgd,grd->bgr", o.float(), wo_a.float()).to(q.dtype)
out, _ = self.wo_b(o.reshape(o.shape[0], o.shape[1] * o.shape[2])) out, _ = self.wo_b(o.reshape(o.shape[0], o.shape[1] * o.shape[2]))
@@ -363,10 +387,13 @@ class MarkovW2ShardGeometry(msgspec.Struct, frozen=True):
class DSparkV4MarkovHead(nn.Module): class DSparkV4MarkovHead(nn.Module):
markov_head_type = "vanilla" markov_head_type = "vanilla"
def __init__(self, *, vocab_size: int, markov_rank: int) -> None: def __init__(
self, *, vocab_size: int, markov_rank: int, is_dsv41: bool = False
) -> None:
super().__init__() super().__init__()
self.vocab_size = int(vocab_size) self.vocab_size = int(vocab_size)
self.markov_rank = int(markov_rank) self.markov_rank = int(markov_rank)
self._is_dsv41 = bool(is_dsv41)
if self.markov_rank <= 0: if self.markov_rank <= 0:
raise ValueError( raise ValueError(
f"DSparkV4MarkovHead requires markov_rank > 0, got {self.markov_rank}." f"DSparkV4MarkovHead requires markov_rank > 0, got {self.markov_rank}."
@@ -418,6 +445,15 @@ class DSparkV4MarkovHead(nn.Module):
"Disable SGLANG_DSPARK_OPT_MARKOV_W2_TP_SHARD." "Disable SGLANG_DSPARK_OPT_MARKOV_W2_TP_SHARD."
) )
self._shard_group = shard_group self._shard_group = shard_group
self._vocab_gather = make_vocab_gather(
shard_group,
local_width=per_partition,
prefer_nvlink=self._is_dsv41
and envs.SGLANG_DSPARK_NVLINK_VOCAB_GATHER.get(),
)
if shard_group.rank == 0:
cls_name = type(self._vocab_gather).__name__
logger.info("DSpark markov_w2 vocab gather: %s", cls_name)
self._tp_shard = MarkovW2ShardGeometry( self._tp_shard = MarkovW2ShardGeometry(
tp_size=tp_size, tp_size=tp_size,
org_vocab_start=int(lm_head.shard_indices.org_vocab_start_index), org_vocab_start=int(lm_head.shard_indices.org_vocab_start_index),
@@ -469,13 +505,40 @@ class DSparkV4MarkovHead(nn.Module):
else: else:
bias = F.linear(latent.float(), weight_local) bias = F.linear(latent.float(), weight_local)
step_local = BuildStepLocal.execute(bias=bias, base_local=base_local) step_local = BuildStepLocal.execute(bias=bias, base_local=base_local)
if shard.tp_size > 1: full = self._vocab_gather(step_local)
assert self._shard_group is not None
full = self._shard_group.all_gather(step_local, dim=-1)
else:
full = step_local
return full[..., : self.vocab_size] return full[..., : self.vocab_size]
@property
def supports_sharded_greedy(self) -> bool:
return (
self._is_dsv41 and self._tp_shard is not None and self._opt_markov_w2_bf16
)
def sample_block_greedy_fused(self, base_logits, *, first_prev_tokens):
if not self.supports_sharded_greedy or not base_logits.is_cuda:
return None
from sglang.kernels.ops.speculative.dspark.sharded_greedy import (
sharded_greedy_step,
)
shard = self._tp_shard
weight = self.markov_w2.weight[shard.org_vocab_start : shard.org_vocab_end]
prev = first_prev_tokens.long()
tokens = []
for step in range(base_logits.shape[1]):
latent = self.get_prev_embeddings(prev)
# Preserve the same BF16 GEMM rounding before the FP32 logits add.
bias = F.linear(latent.to(weight.dtype), weight)
prev = sharded_greedy_step(
bias,
base_logits[:, step],
group=self._shard_group,
vocab_start=shard.org_vocab_start,
gather=self._vocab_gather.gather_stacked,
)
tokens.append(prev)
return torch.stack(tokens, dim=1)
def forward(self, token_ids: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: def forward(self, token_ids: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
embed = self.get_prev_embeddings(token_ids) embed = self.get_prev_embeddings(token_ids)
logits = self.project_bias(embed) logits = self.project_bias(embed)
@@ -527,6 +590,22 @@ def build_dspark_v4_confidence_head(
) )
def _dspark_stage_config(config: DeepSeekV4Config) -> DeepSeekV4Config:
n_routed = int(getattr(config, "dspark_n_routed_experts", 0) or 0)
n_active = int(getattr(config, "dspark_num_experts_per_tok", 0) or 0)
has_vision = int(getattr(config, "vision_n_layers", 0) or 0) > 0
if not (n_routed or n_active or has_vision):
return config
stage_config = copy.copy(config)
if n_routed:
stage_config.n_routed_experts = n_routed
if n_active:
stage_config.num_experts_per_tok = n_active
if has_vision:
stage_config.vision_n_layers = 0
return stage_config
class DSparkV4Stage(DeepseekV4DecoderLayer): class DSparkV4Stage(DeepseekV4DecoderLayer):
def __init__( def __init__(
self, self,
@@ -538,14 +617,18 @@ class DSparkV4Stage(DeepseekV4DecoderLayer):
quant_config: Optional[QuantizationConfig] = None, quant_config: Optional[QuantizationConfig] = None,
prefix: str = "", prefix: str = "",
alt_streams: Optional[List[torch.cuda.Stream]] = None, alt_streams: Optional[List[torch.cuda.Stream]] = None,
hc_stats_stream: Optional[torch.cuda.Stream] = None,
moe_routed_quant_stream: Optional[torch.cuda.Stream] = None,
) -> None: ) -> None:
super().__init__( super().__init__(
config=config, config=_dspark_stage_config(config),
layer_id=layer_id, layer_id=layer_id,
quant_config=quant_config, quant_config=quant_config,
prefix=prefix, prefix=prefix,
is_nextn=True, is_nextn=True,
alt_streams=alt_streams, alt_streams=alt_streams,
hc_stats_stream=hc_stats_stream,
moe_routed_quant_stream=moe_routed_quant_stream,
) )
self.stage_id = stage_id self.stage_id = stage_id
self.dim = config.hidden_size self.dim = config.hidden_size
@@ -566,6 +649,11 @@ class DSparkV4Stage(DeepseekV4DecoderLayer):
if stage_id == num_stages - 1: if stage_id == num_stages - 1:
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
if self.hc_pre_from_prev_sublayer:
# V4.1 collapses the head with the last FFN's pre-mix; the
# checkpoint carries no hc_head_* tensors for the stages.
self.hc_head_fn = self.hc_head_base = self.hc_head_scale = None
else:
( (
self.hc_head_fn, self.hc_head_fn,
self.hc_head_base, self.hc_head_base,
@@ -615,7 +703,12 @@ class DSparkV4Stage(DeepseekV4DecoderLayer):
positions: torch.Tensor, positions: torch.Tensor,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
forward_batch: ForwardBatch, forward_batch: ForwardBatch,
) -> torch.Tensor: prev_pre: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
if self.hc_pre_from_prev_sublayer:
return self._forward_hc_pre_from_prev(
positions, hidden_states, forward_batch, prev_pre
)
residual = hidden_states residual = hidden_states
x, post, comb = self._hc_pre_block( x, post, comb = self._hc_pre_block(
hidden_states, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base hidden_states, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base
@@ -632,7 +725,49 @@ class DSparkV4Stage(DeepseekV4DecoderLayer):
x = self.post_attention_layernorm(x) x = self.post_attention_layernorm(x)
x = self._run_ffn(x, forward_batch) x = self._run_ffn(x, forward_batch)
x = self._hc_post_block(x, residual, post, comb) x = self._hc_post_block(x, residual, post, comb)
return x return x, None
def _forward_hc_pre_from_prev(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
forward_batch: ForwardBatch,
prev_pre: Optional[torch.Tensor],
) -> Tuple[torch.Tensor, torch.Tensor]:
stats_stream = self._get_hc_stats_stream(hidden_states, forward_batch)
residual = hidden_states
x = self._hc_combine(
hidden_states, prev_pre, self.input_layernorm, stats_stream
)
with self.self_attn.maybe_use_decode_attn_tp(forward_batch):
x = self.self_attn(positions, x, forward_batch)
attn_pre, attn_post, attn_comb = self._hc_mix_stats(
hidden_states,
self.hc_attn_fn,
self.hc_attn_scale,
self.hc_attn_base,
stats_stream,
)
if stats_stream is not None:
torch.cuda.current_stream().wait_stream(stats_stream)
hidden_states = self.hc_post(x, residual, attn_post, attn_comb)
residual = hidden_states
x = self._hc_combine(
hidden_states, attn_pre, self.post_attention_layernorm, stats_stream
)
x = self._run_ffn(x, forward_batch)
ffn_pre, ffn_post, ffn_comb = self._hc_mix_stats(
hidden_states,
self.hc_ffn_fn,
self.hc_ffn_scale,
self.hc_ffn_base,
stats_stream,
)
if stats_stream is not None:
torch.cuda.current_stream().wait_stream(stats_stream)
hidden_states = self.hc_post(x, residual, ffn_post, ffn_comb)
return hidden_states, ffn_pre
def _run_ffn(self, x: torch.Tensor, forward_batch: ForwardBatch) -> torch.Tensor: def _run_ffn(self, x: torch.Tensor, forward_batch: ForwardBatch) -> torch.Tensor:
shape = x.shape shape = x.shape
@@ -714,6 +849,21 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
self.alt_streams: Optional[List[torch.cuda.Stream]] = ( self.alt_streams: Optional[List[torch.cuda.Stream]] = (
[torch.cuda.Stream()] if use_multi_stream else None [torch.cuda.Stream()] if use_multi_stream else None
) )
self.moe_routed_quant_stream = (
torch.cuda.Stream()
if use_multi_stream
and torch.version.cuda is not None
and getattr(config, "hc_pre_from_prev_sublayer", False)
else None
)
self.hc_stats_stream = (
torch.cuda.Stream()
if use_multi_stream
and torch.version.cuda is not None
and get_platform().is_blackwell
and getattr(config, "hc_pre_from_prev_sublayer", False)
else None
)
self.stages = nn.ModuleList( self.stages = nn.ModuleList(
[ [
DSparkV4Stage( DSparkV4Stage(
@@ -725,6 +875,8 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
quant_config=quant_config, quant_config=quant_config,
prefix=add_prefix(f"stages.{stage_id}", prefix), prefix=add_prefix(f"stages.{stage_id}", prefix),
alt_streams=self.alt_streams, alt_streams=self.alt_streams,
hc_stats_stream=self.hc_stats_stream,
moe_routed_quant_stream=self.moe_routed_quant_stream,
) )
for stage_id in range(self.num_stages) for stage_id in range(self.num_stages)
] ]
@@ -732,6 +884,7 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
self.markov_head = DSparkV4MarkovHead( self.markov_head = DSparkV4MarkovHead(
vocab_size=int(config.vocab_size), vocab_size=int(config.vocab_size),
markov_rank=int(dspark_config.markov_rank), markov_rank=int(dspark_config.markov_rank),
is_dsv41=getattr(config, "model_type", None) == "deepseek_v41",
) )
self.confidence_head = build_dspark_v4_confidence_head( self.confidence_head = build_dspark_v4_confidence_head(
config=config, markov_rank=int(dspark_config.markov_rank) config=config, markov_rank=int(dspark_config.markov_rank)
@@ -739,6 +892,9 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
self.hc_mult = int(config.hc_mult) self.hc_mult = int(config.hc_mult)
self.norm_eps = float(config.rms_norm_eps) self.norm_eps = float(config.rms_norm_eps)
self.hc_eps = float(config.hc_eps) self.hc_eps = float(config.hc_eps)
self.hc_pre_from_prev_sublayer = bool(
getattr(config, "hc_pre_from_prev_sublayer", False)
)
if self.uses_own_vocab_modules: if self.uses_own_vocab_modules:
self.embed_tokens = VocabParallelEmbedding( self.embed_tokens = VocabParallelEmbedding(
@@ -791,6 +947,12 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
kvs = CommitKvProj.execute( kvs = CommitKvProj.execute(
main_x=main_x, main_x=main_x,
wkv_linears=[stage.self_attn.wkv for stage in self.stages], wkv_linears=[stage.self_attn.wkv for stage in self.stages],
# The FlashMLA writer reads an explicit KV row stride, so views are fine.
allow_strided_output=(
get_platform().is_blackwell
and not is_unified_kv_triton()
and not pool.uniform_fp8
),
) )
# Under unified_kv the swa_kv_pool is None; the caller passes a unified # Under unified_kv the swa_kv_pool is None; the caller passes a unified
# ring loc (state_slot * ring + pos % ring, -1 for uncommitted) so the # ring loc (state_slot * ring + pos % ring, -1 for uncommitted) so the
@@ -836,12 +998,20 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
if input_embeds is None: if input_embeds is None:
input_embeds = self.forward_embed(input_ids) input_embeds = self.forward_embed(input_ids)
x = input_embeds x = input_embeds
pre = None
for stage in self.stages: for stage in self.stages:
x = stage(positions, x, forward_batch) x, pre = stage(positions, x, forward_batch, pre)
if self.hc_pre_from_prev_sublayer:
from sglang.kernels.ops.layernorm.mhc import hc_combine
x = hc_combine(x.flatten(1).float(), pre, self.hc_mult, x.dtype)
return LogitsProcessorOutput(next_token_logits=None, hidden_states=x) return LogitsProcessorOutput(next_token_logits=None, hidden_states=x)
def collapse_hc_head(self, x: torch.Tensor) -> torch.Tensor: def collapse_hc_head(self, x: torch.Tensor) -> torch.Tensor:
if self.hc_pre_from_prev_sublayer:
assert x.dim() == 2, "V4.1 draft hidden states leave forward() collapsed"
return x
last = self.stages[-1] last = self.stages[-1]
return hc_head_torch( return hc_head_torch(
x, x,
@@ -853,7 +1023,6 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
) )
def compute_base_logits(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: def compute_base_logits(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
x_post_hc = self.collapse_hc_head(x) x_post_hc = self.collapse_hc_head(x)
return self._logits_from_x_post_hc(x_post_hc), x_post_hc return self._logits_from_x_post_hc(x_post_hc), x_post_hc
@@ -1008,6 +1177,8 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
stage_id, rest = parts[1], parts[2] stage_id, rest = parts[1], parts[2]
if rest.startswith("markov_head."): if rest.startswith("markov_head."):
rest = rest.replace("markov_head.embed.", "markov_head.markov_w1.", 1)
rest = rest.replace("markov_head.head.", "markov_head.markov_w2.", 1)
return f"markov_head.{rest[len('markov_head.') :]}" return f"markov_head.{rest[len('markov_head.') :]}"
if rest.startswith("confidence_head."): if rest.startswith("confidence_head."):
@@ -1024,6 +1195,8 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
mapped_rest = mapped_rest.replace(".w2.", ".down_proj.") mapped_rest = mapped_rest.replace(".w2.", ".down_proj.")
mapped_rest = mapped_rest.replace(".w3.", ".up_proj.") mapped_rest = mapped_rest.replace(".w3.", ".up_proj.")
mapped_rest = mapped_rest.replace(".gate.tid2eid", ".topk.tid2eid") mapped_rest = mapped_rest.replace(".gate.tid2eid", ".topk.tid2eid")
if mapped_rest.endswith(".gate.bias_vl"):
return None
mapped_rest = mapped_rest.replace(".gate.bias", ".gate.e_score_correction_bias") mapped_rest = mapped_rest.replace(".gate.bias", ".gate.e_score_correction_bias")
mapped_rest = mapped_rest.replace(".scale", ".weight_scale_inv") mapped_rest = mapped_rest.replace(".scale", ".weight_scale_inv")
return f"stages.{stage_id}.{mapped_rest}" return f"stages.{stage_id}.{mapped_rest}"
@@ -35,6 +35,7 @@ from sglang.srt.models.deepseek_v4 import (
DeepseekV4DecoderLayer, DeepseekV4DecoderLayer,
DeepseekV4ForCausalLM, DeepseekV4ForCausalLM,
_is_npu, _is_npu,
wo_a_fp8_gemm_enabled,
) )
from sglang.srt.runtime_context import get_parallel from sglang.srt.runtime_context import get_parallel
from sglang.srt.utils import add_prefix from sglang.srt.utils import add_prefix
@@ -220,6 +221,7 @@ class DeepseekV4ForCausalLMNextN(DeepseekV4ForCausalLM):
self.tp_size = get_parallel().tp_size self.tp_size = get_parallel().tp_size
self.pp_group = get_pp_group() self.pp_group = get_pp_group()
self.quant_config = quant_config self.quant_config = quant_config
self.wo_a_fp8 = wo_a_fp8_gemm_enabled(quant_config)
self.determine_num_fused_shared_experts() self.determine_num_fused_shared_experts()
self.model = DeepseekV4ModelNextN( self.model = DeepseekV4ModelNextN(
@@ -0,0 +1 @@
"""Vision tower, image preprocessing and VL expert routing for DeepSeek-V4.1."""
@@ -0,0 +1,106 @@
import torch
import torch.nn.functional as F
from sglang.srt.layers.moe.topk import (
_RENORMALIZE_SUM_EPSILON,
StandardTopKOutput,
StandardTopKOutputPacked,
_mask_topk_ids_padded_region,
_zero_topk_weights_padded_region,
)
from sglang.srt.layers.moe.utils import has_per_rank_fused_shared_slots
from sglang.srt.utils import is_cuda
def _scale_fused_shared_weights(weights, num_fused_shared_experts, scaling_factor):
# Standard EP replicates the fused shared expert on every rank and all-reduces,
# so the shared columns carry a 1/ep_size factor.
if num_fused_shared_experts and scaling_factor is not None:
weights[:, -num_fused_shared_experts:] *= scaling_factor
return weights
def vision_topk(moe, logits, input_ids, num_token_non_padded=None):
config = moe.topk.topk_config
num_fused_shared_experts = config.num_fused_shared_experts
if num_fused_shared_experts:
# This path bypasses _post_process_topk_ids, which appends the per-rank slots.
assert not has_per_rank_fused_shared_slots(num_fused_shared_experts), (
"VL routing does not support per-rank fused shared slots"
)
if is_cuda():
from sglang.kernels.ops.moe.moe_fused_gate import moe_fused_gate
from sglang.srt.layers.moe.utils import get_moe_runner_backend
# Same admission as _fused_gate_emits_packed_ids: the shared-expert slots
# rescaled below would rewrite weights after the router.
packed_topk = None
if (
num_fused_shared_experts == 0
and get_moe_runner_backend().is_flashinfer_mxfp4()
):
packed_topk = torch.empty(
(logits.shape[0], config.top_k), dtype=torch.int32, device=logits.device
)
weights, indices = moe_fused_gate(
logits,
moe.gate.e_score_correction_bias,
topk=config.top_k,
scoring_func="sqrtsoftplus",
num_fused_shared_experts=num_fused_shared_experts,
bias_alt=moe.gate.e_score_correction_bias_vl,
input_ids=input_ids,
bias_alt_token_id=moe.config.image_token_id,
renormalize=config.renormalize and config.top_k > 1,
renormalize_epsilon=_RENORMALIZE_SUM_EPSILON,
routed_scaling_factor=config.routed_scaling_factor,
apply_routed_scaling_factor_on_output=config.apply_routed_scaling_factor_on_output,
num_token_non_padded=num_token_non_padded,
packed_out=packed_topk,
sqrtsoftplus_log1p=True,
)
weights = _scale_fused_shared_weights(
weights,
num_fused_shared_experts,
config.fused_shared_experts_scaling_factor,
)
if packed_topk is not None:
return StandardTopKOutputPacked(weights, indices, logits, packed_topk)
return StandardTopKOutput(weights, indices, logits)
scores = F.softplus(logits.float()).sqrt()
if input_ids is None:
bias = moe.gate.e_score_correction_bias
else:
bias = torch.where(
(input_ids == moe.config.image_token_id)[:, None],
moe.gate.e_score_correction_bias_vl,
moe.gate.e_score_correction_bias,
)
# The shared slots appended below use the same layout as biased_grouped_topk_gpu.
topk_routed = config.top_k - num_fused_shared_experts
indices = (scores + bias).topk(topk_routed, dim=-1).indices
weights = scores.gather(-1, indices)
routed_sum = weights.sum(-1, keepdim=True, dtype=torch.float32)
if num_fused_shared_experts:
shared_ids = logits.shape[-1] + torch.arange(
num_fused_shared_experts, device=indices.device, dtype=indices.dtype
)
indices = torch.cat(
[indices, shared_ids.expand(indices.shape[0], -1)],
dim=-1,
)
weights = F.pad(weights, (0, num_fused_shared_experts))
weights[:, topk_routed:] = routed_sum / config.routed_scaling_factor
if config.renormalize and config.top_k > 1:
weights = weights / (routed_sum + _RENORMALIZE_SUM_EPSILON)
if config.apply_routed_scaling_factor_on_output:
weights = weights * config.routed_scaling_factor
weights = _scale_fused_shared_weights(
weights, num_fused_shared_experts, config.fused_shared_experts_scaling_factor
)
weights, indices = weights.float(), indices.int()
if num_token_non_padded is not None:
_mask_topk_ids_padded_region(indices, num_token_non_padded)
_zero_topk_weights_padded_region(weights, num_token_non_padded)
return StandardTopKOutput(weights, indices, logits)
@@ -49,6 +49,8 @@ class DFlashVerifyInput(SpecInput):
# Committed/live lengths before the verify caller temporarily expands # Committed/live lengths before the verify caller temporarily expands
# batch.seq_lens_cpu to the target-attention KV lengths. # batch.seq_lens_cpu to the target-attention KV lengths.
live_seq_lens_cpu: Optional[torch.Tensor] = None live_seq_lens_cpu: Optional[torch.Tensor] = None
# Conservative request-lifetime bound for candidate graph dispatch.
candidate_max_seq_len_upper_bound: Optional[int] = None
def __post_init__(self): def __post_init__(self):
super().__init__(spec_input_type=SpecInputType.DFLASH_VERIFY) super().__init__(spec_input_type=SpecInputType.DFLASH_VERIFY)
@@ -57,9 +57,6 @@ class DsparkDraftSampler:
self.sample_from_anchor = bool(model.sample_from_anchor) self.sample_from_anchor = bool(model.sample_from_anchor)
self.query_token_num = self.gamma if self.sample_from_anchor else self.gamma + 1 self.query_token_num = self.gamma if self.sample_from_anchor else self.gamma + 1
max_bs = int(max_bs) max_bs = int(max_bs)
# Resolved once: this sampler runs inside cuda-graph capture, so the
# branch below is baked into the captured graph anyway.
self._fused_greedy = envs.SGLANG_DSPARK_OPT_FUSED_GREEDY_MARKOV.get()
if out is not None: if out is not None:
assert out.shape == (max_bs * self.gamma,) and out.dtype == torch.int64 assert out.shape == (max_bs * self.gamma,) and out.dtype == torch.int64
self.out = out self.out = out
@@ -129,11 +126,11 @@ class DsparkDraftSampler:
# Gated/RNN subclasses return None (hidden-state-dependent bias); fall # Gated/RNN subclasses return None (hidden-state-dependent bias); fall
# through to the block sampler below. # through to the block sampler below.
draft_tokens = None draft_tokens = None
if ( fused_greedy = getattr(self.markov_head, "supports_sharded_greedy", False) or (
not self.folded_sampling envs.SGLANG_DSPARK_OPT_FUSED_GREEDY_MARKOV.get()
and self._fused_greedy
and isinstance(self.markov_head, VanillaMarkov) and isinstance(self.markov_head, VanillaMarkov)
): )
if not self.folded_sampling and fused_greedy:
draft_tokens = self.markov_head.sample_block_greedy_fused( draft_tokens = self.markov_head.sample_block_greedy_fused(
base_logits, first_prev_tokens=anchor base_logits, first_prev_tokens=anchor
) )
@@ -198,6 +195,9 @@ def _resolve_folded_sampling(
return False return False
if mode == DsparkFoldedSampling.FORCE: if mode == DsparkFoldedSampling.FORCE:
return True return True
# The V4.1 TP head reduces compact argmax summaries in the greedy graph.
if getattr(model.markov_head, "supports_sharded_greedy", False):
return False
vocab = int(model.lm_head.org_vocab_size) vocab = int(model.lm_head.org_vocab_size)
noise_bytes = max_bs * vocab * 4 noise_bytes = max_bs * vocab * 4
logits_bytes = max_bs * gamma * vocab * _base_logits_dtype(model).itemsize logits_bytes = max_bs * gamma * vocab * _base_logits_dtype(model).itemsize
@@ -121,6 +121,24 @@ class TargetHiddenKvInjector:
state_slot=state_slot, state_slot=state_slot,
final_pos=final_pos, final_pos=final_pos,
) )
elif (
cache_loc.is_cuda
and cache_loc.is_contiguous()
and commit_lens is not None
and cache_loc_2d is not None
and commit_lens.is_contiguous()
and cache_loc.numel() == cache_loc_2d.numel()
):
from sglang.kernels.ops.speculative.dspark.commit_swa import (
committed_swa_locations,
)
swa_loc = committed_swa_locations(
cache_loc,
pool.full_to_swa_index_mapping,
commit_lens,
cache_loc_2d.shape[1],
)
else: else:
swa_loc = pool.translate_loc_from_full_to_swa(cache_loc).to(torch.int32) swa_loc = pool.translate_loc_from_full_to_swa(cache_loc).to(torch.int32)
if commit_lens is not None and cache_loc_2d is not None: if commit_lens is not None and cache_loc_2d is not None:
@@ -78,6 +78,30 @@ class TargetVerifyResult(msgspec.Struct, frozen=True):
can_run_cuda_graph: bool can_run_cuda_graph: bool
def candidate_request_length_bound(
reqs, pending_verify_tokens: int = 0
) -> Optional[int]:
"""Bound committed positions without reading asynchronous acceptance results.
The overlap loop can hold one unprocessed result, so reserve its full width;
the runner adds the current verify width. Aborted/embedding/multimodal requests
return None: their visible token IDs may not track cache positions."""
if not reqs:
return None
longest = 0
for req in reqs:
budget = req.sampling_params.max_new_tokens
if (
not isinstance(budget, int)
or budget < 0
or getattr(req, "to_finish", None) is not None
or getattr(req, "input_embeds", None) is not None
or getattr(req, "multimodal_inputs", None) is not None
):
return None
longest = max(longest, len(req.origin_input_ids) + budget)
return longest + pending_verify_tokens
class TargetVerifyExecutor: class TargetVerifyExecutor:
def __init__( def __init__(
self, self,
@@ -92,6 +116,15 @@ class TargetVerifyExecutor:
simulate_acc_len: float = 0.0, simulate_acc_len: float = 0.0,
) -> None: ) -> None:
self.target_worker = target_worker self.target_worker = target_worker
# candidate_max_seq_len_upper_bound only feeds the V4.1 candidate graphs.
self._target_is_dsv41 = (
getattr(
target_worker.model_runner.model_config.hf_text_config,
"model_type",
None,
)
== "deepseek_v41"
)
self.gamma = int(gamma) self.gamma = int(gamma)
self.verify_num_draft_tokens = verify_num_draft_tokens self.verify_num_draft_tokens = verify_num_draft_tokens
self.model_runner = model_runner self.model_runner = model_runner
@@ -135,6 +168,7 @@ class TargetVerifyExecutor:
gamma=self.gamma, gamma=self.gamma,
verify_num_draft_tokens=self.verify_num_draft_tokens, verify_num_draft_tokens=self.verify_num_draft_tokens,
cutoff_layout=layout, cutoff_layout=layout,
fused_argmax=self._target_is_dsv41,
) )
if self._simulate_acc_len > 0: if self._simulate_acc_len > 0:
correct_len = self._simulated_correct_len( correct_len = self._simulated_correct_len(
@@ -296,6 +330,10 @@ class TargetVerifyExecutor:
seq_lens_cpu_backup, seq_lens_cpu_backup,
seq_lens_sum_backup, seq_lens_sum_backup,
) -> TargetVerifyResult: ) -> TargetVerifyResult:
if verify_input.live_seq_lens_cpu is None and self._target_is_dsv41:
verify_input.candidate_max_seq_len_upper_bound = (
candidate_request_length_bound(batch.reqs, self.verify_num_draft_tokens)
)
verify_forward_batch, _ = verify_input.prepare_for_verify( verify_forward_batch, _ = verify_input.prepare_for_verify(
batch, self.target_worker batch, self.target_worker
) )
@@ -479,6 +517,7 @@ class CommitInjectCtx(msgspec.Struct):
block_pos_offsets: torch.Tensor block_pos_offsets: torch.Tensor
resolve_pool: object resolve_pool: object
resolve_req_to_token: object resolve_req_to_token: object
kv_injector: Optional[TargetHiddenKvInjector] = None
class AcceptOuts(msgspec.Struct): class AcceptOuts(msgspec.Struct):
@@ -499,9 +538,11 @@ class DsparkVerifyEpilogue:
device, device,
tp_sync: SpecTpSync, tp_sync: SpecTpSync,
commit_ctx: Optional[CommitInjectCtx] = None, commit_ctx: Optional[CommitInjectCtx] = None,
fused_argmax: bool = False,
) -> None: ) -> None:
self.max_bs = int(max_bs) self.max_bs = int(max_bs)
self.stride = int(verify_num_draft_tokens) self.stride = int(verify_num_draft_tokens)
self._fused_argmax = bool(fused_argmax)
self.gamma = self.stride - 1 self.gamma = self.stride - 1
self.commit_ctx = commit_ctx self.commit_ctx = commit_ctx
self._tp_sync = tp_sync self._tp_sync = tp_sync
@@ -530,9 +571,13 @@ class DsparkVerifyEpilogue:
) )
self.strided_logits: Optional[torch.Tensor] = None self.strided_logits: Optional[torch.Tensor] = None
self.strided_hidden: Optional[torch.Tensor] = None self.strided_hidden: Optional[torch.Tensor] = None
self._static_step_state: Optional[tuple[int, bool]] = None
def capture_hook(self, runner, out, forward_batch, num_tokens) -> None: def capture_hook(self, runner, out, forward_batch, num_tokens) -> None:
if runner.model_runner.is_draft_worker or not runner.ragged_verify_mode: if (
runner.model_runner.is_draft_worker
or not forward_batch.forward_mode.is_target_verify()
):
return return
if ( if (
not isinstance(out, LogitsProcessorOutput) not isinstance(out, LogitsProcessorOutput)
@@ -540,6 +585,9 @@ class DsparkVerifyEpilogue:
or out.hidden_states is None or out.hidden_states is None
): ):
return return
if not runner.ragged_verify_mode:
self._static_epilogue(out, forward_batch)
return
self( self(
compact_logits=out.next_token_logits, compact_logits=out.next_token_logits,
compact_hidden=out.hidden_states, compact_hidden=out.hidden_states,
@@ -550,6 +598,7 @@ class DsparkVerifyEpilogue:
) )
def begin_step(self, verify_lens, armed: bool) -> None: def begin_step(self, verify_lens, armed: bool) -> None:
self._static_step_state = None
if verify_lens is None: if verify_lens is None:
self.verify_lens_buf.zero_() self.verify_lens_buf.zero_()
else: else:
@@ -559,6 +608,49 @@ class DsparkVerifyEpilogue:
self.verify_lens_buf[bs:].zero_() self.verify_lens_buf[bs:].zero_()
self.inject_gate_buf.fill_(1 if armed else 0) self.inject_gate_buf.fill_(1 if armed else 0)
def begin_static_step(self, bs: int, armed: bool) -> None:
state = (bs, armed)
if self._static_step_state == state:
return
self.verify_lens_buf[:bs].fill_(self.stride)
self.verify_lens_buf[bs:].zero_()
self.inject_gate_buf.fill_(int(armed))
self._static_step_state = state
def _static_epilogue(self, out, forward_batch) -> None:
bs = forward_batch.batch_size
verify_lens = self.verify_lens_buf[:bs]
candidates = forward_batch.input_ids.view(bs, self.stride)
commit_lens = self._accept(
candidates=candidates,
logits=out.next_token_logits,
draft_tokens=candidates[:, 1:].contiguous(),
seq_lens=forward_batch.seq_lens,
)
if not self.folds_commit:
return
# Same staged locations as target verify; padded and fallback rows skip KV.
gated_commit_lens = (
torch.minimum(commit_lens, verify_lens.to(torch.int32))
* self.inject_gate_buf
)
cache_loc = forward_batch.out_cache_loc
state_slot = None
if is_unified_kv_triton():
state_slot = (
forward_batch.req_pool_indices.view(-1, 1)
.expand(bs, self.stride)
.reshape(-1)
)
self.commit_ctx.kv_injector.inject_target_hidden(
target_hidden=out.hidden_states,
cache_loc=cache_loc,
cache_loc_2d=cache_loc.view(bs, self.stride),
positions=forward_batch.positions,
commit_lens=gated_commit_lens,
state_slot=state_slot,
)
def read_accept(self, bs: int) -> AcceptOuts: def read_accept(self, bs: int) -> AcceptOuts:
return AcceptOuts( return AcceptOuts(
correct_len=self.correct_len_buf[:bs], correct_len=self.correct_len_buf[:bs],
@@ -610,7 +702,23 @@ class DsparkVerifyEpilogue:
self.strided_hidden = self._ensure_out(self.strided_hidden, compact_hidden) self.strided_hidden = self._ensure_out(self.strided_hidden, compact_hidden)
verify_lens = self.verify_lens_buf[:bs] verify_lens = self.verify_lens_buf[:bs]
self._scatter(compact_logits, compact_hidden, verify_lens, bs) self._scatter(compact_logits, compact_hidden, verify_lens, bs)
commit_lens = self._accept(input_ids, seq_lens, verify_lens, bs) candidates = torch.zeros(
(bs * self.stride, 1), dtype=input_ids.dtype, device=input_ids.device
)
scatter_compact_to_strided_into(
compact=input_ids.view(-1, 1),
verify_lens=verify_lens,
out=candidates,
stride=self.stride,
fill_value=0,
)
commit_lens = self._accept(
candidates=candidates.view(bs, self.stride),
logits=self.strided_logits[: bs * self.stride],
draft_tokens=self.draft_tokens_buf[: bs * self.gamma].view(bs, self.gamma),
seq_lens=seq_lens,
cutoff_verify_lens=verify_lens,
)
if self.folds_commit: if self.folds_commit:
self._commit_inject( self._commit_inject(
commit_lens, verify_lens, seq_lens, req_pool_indices, bs commit_lens, verify_lens, seq_lens, req_pool_indices, bs
@@ -632,22 +740,16 @@ class DsparkVerifyEpilogue:
fill_value=0.0, fill_value=0.0,
) )
def _accept(self, input_ids, seq_lens, verify_lens, bs: int) -> torch.Tensor: def _accept(
candidates = torch.zeros( self, *, candidates, logits, draft_tokens, seq_lens, cutoff_verify_lens=None
(bs * self.stride, 1), dtype=input_ids.dtype, device=input_ids.device ) -> torch.Tensor:
) bs = candidates.shape[0]
scatter_compact_to_strided_into(
compact=input_ids.view(-1, 1),
verify_lens=verify_lens,
out=candidates,
stride=self.stride,
fill_value=0,
)
correct_len, bonus, cap_trim_lens = accept_greedy_triton( correct_len, bonus, cap_trim_lens = accept_greedy_triton(
candidates=candidates.view(bs, self.stride), candidates=candidates,
target_logits=self.strided_logits[: bs * self.stride], target_logits=logits,
verify_num_draft_tokens=self.stride, verify_num_draft_tokens=self.stride,
cutoff_verify_lens=verify_lens, cutoff_verify_lens=cutoff_verify_lens,
fused_argmax=self._fused_argmax,
) )
self._tp_sync.sync(SpecTpSyncSite.DSPARK_ACCEPT_GRAPH, correct_len) self._tp_sync.sync(SpecTpSyncSite.DSPARK_ACCEPT_GRAPH, correct_len)
self._tp_sync.sync(SpecTpSyncSite.DSPARK_ACCEPT_GRAPH, bonus) self._tp_sync.sync(SpecTpSyncSite.DSPARK_ACCEPT_GRAPH, bonus)
@@ -658,7 +760,7 @@ class DsparkVerifyEpilogue:
prefix_lens=seq_lens[:bs], prefix_lens=seq_lens[:bs],
) )
out_tokens = BuildOutTokens.execute( out_tokens = BuildOutTokens.execute(
draft_tokens=self.draft_tokens_buf[: bs * self.gamma].view(bs, self.gamma), draft_tokens=draft_tokens,
correct_len=correct_len, correct_len=correct_len,
bonus=bonus, bonus=bonus,
verify_num_draft_tokens=self.stride, verify_num_draft_tokens=self.stride,
@@ -719,6 +821,7 @@ def accept_draft_tokens(
gamma: int, gamma: int,
verify_num_draft_tokens: int, verify_num_draft_tokens: int,
cutoff_layout: Optional[RaggedVerifyLayout] = None, cutoff_layout: Optional[RaggedVerifyLayout] = None,
fused_argmax: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
greedy_mask = draft_block.greedy_mask greedy_mask = draft_block.greedy_mask
cutoff_verify_lens = None if cutoff_layout is None else cutoff_layout.verify_lens cutoff_verify_lens = None if cutoff_layout is None else cutoff_layout.verify_lens
@@ -729,6 +832,7 @@ def accept_draft_tokens(
target_logits=target_logits, target_logits=target_logits,
verify_num_draft_tokens=verify_num_draft_tokens, verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens, cutoff_verify_lens=cutoff_verify_lens,
fused_argmax=fused_argmax,
) )
bs, gamma_rows, vocab = draft_block.corrected_logits.shape bs, gamma_rows, vocab = draft_block.corrected_logits.shape
draft_probs = SoftmaxTemp.execute( draft_probs = SoftmaxTemp.execute(
@@ -753,6 +857,7 @@ def accept_draft_tokens(
target_logits=target_logits, target_logits=target_logits,
verify_num_draft_tokens=verify_num_draft_tokens, verify_num_draft_tokens=verify_num_draft_tokens,
cutoff_verify_lens=cutoff_verify_lens, cutoff_verify_lens=cutoff_verify_lens,
fused_argmax=fused_argmax,
) )
sampling_len, sampling_bonus, sampling_trim = AcceptSampling.execute( sampling_len, sampling_bonus, sampling_trim = AcceptSampling.execute(
candidates=candidates, candidates=candidates,
@@ -304,8 +304,23 @@ class DSparkWorkerV2(BaseSpecWorker):
dp_moe_sync=self._draft_is_moe and get_parallel().enable_dp_attention, dp_moe_sync=self._draft_is_moe and get_parallel().enable_dp_attention,
) )
self._verify_epilogue = None self._verify_epilogue = None
target_is_dsv41 = (
getattr(
self.target_worker.model_runner.model_config.hf_text_config,
"model_type",
None,
)
== "deepseek_v41"
)
static_epilogue_supported = (
target_is_dsv41
and self._verify_planner.mode_value == "static"
and self._draft_is_moe
and not get_parallel().enable_dp_attention
and self.ps.pp_size == 1
)
if ( if (
self._verify_planner.is_compact_mode (self._verify_planner.is_compact_mode or static_epilogue_supported)
and self._decode_graph_allowed and self._decode_graph_allowed
and is_cuda() and is_cuda()
): ):
@@ -314,6 +329,7 @@ class DSparkWorkerV2(BaseSpecWorker):
verify_num_draft_tokens=self.verify_num_draft_tokens, verify_num_draft_tokens=self.verify_num_draft_tokens,
device=self.device, device=self.device,
tp_sync=self._tp_sync, tp_sync=self._tp_sync,
fused_argmax=target_is_dsv41,
commit_ctx=CommitInjectCtx( commit_ctx=CommitInjectCtx(
draft_model=self.draft_model, draft_model=self.draft_model,
block_pos_offsets=self._block_pos_offsets, block_pos_offsets=self._block_pos_offsets,
@@ -321,6 +337,7 @@ class DSparkWorkerV2(BaseSpecWorker):
resolve_req_to_token=lambda: ( resolve_req_to_token=lambda: (
self.model_runner.req_to_token_pool.req_to_token self.model_runner.req_to_token_pool.req_to_token
), ),
kv_injector=self._kv_injector,
), ),
) )
self.model_runner.capture_tail_hooks.append( self.model_runner.capture_tail_hooks.append(
@@ -512,7 +529,11 @@ class DSparkWorkerV2(BaseSpecWorker):
batch: ScheduleBatch, batch: ScheduleBatch,
on_publish=None, on_publish=None,
grammar_barrier=None, grammar_barrier=None,
*,
pp_proxy_tensors=None,
) -> GenerationBatchResult: ) -> GenerationBatchResult:
# The non-overlap scheduler passes this keyword even when PP=1.
assert pp_proxy_tensors is None, "DSpark does not support pipeline parallelism"
if batch.forward_mode.is_extend() or batch.is_extend_in_batch: if batch.forward_mode.is_extend() or batch.is_extend_in_batch:
self._verify_planner.note_non_decode_step() self._verify_planner.note_non_decode_step()
self._observers.note_prefill_step() self._observers.note_prefill_step()
@@ -590,9 +611,17 @@ class DSparkWorkerV2(BaseSpecWorker):
final_pos = torch.repeat_interleave( final_pos = torch.repeat_interleave(
(draft_seq_lens + ctx_lens - 1).to(torch.int64), repeats (draft_seq_lens + ctx_lens - 1).to(torch.int64), repeats
) )
cache_loc = batch.out_cache_loc
token_indices = logits_output.hidden_states_token_indices
if token_indices is not None:
cache_loc = cache_loc[token_indices]
positions = positions[token_indices]
if state_slot is not None:
state_slot = state_slot[token_indices]
final_pos = final_pos[token_indices]
self._kv_injector.inject_target_hidden( self._kv_injector.inject_target_hidden(
target_hidden=logits_output.hidden_states, target_hidden=logits_output.hidden_states,
cache_loc=batch.out_cache_loc, cache_loc=cache_loc,
positions=positions, positions=positions,
state_slot=state_slot, state_slot=state_slot,
final_pos=final_pos, final_pos=final_pos,
@@ -600,6 +629,7 @@ class DSparkWorkerV2(BaseSpecWorker):
) )
# Avoid copying large hidden-state buffers to CPU in overlap scheduling. # Avoid copying large hidden-state buffers to CPU in overlap scheduling.
logits_output.hidden_states = None logits_output.hidden_states = None
logits_output.hidden_states_token_indices = None
batch_output.next_draft_input = make_next_draft_input( batch_output.next_draft_input = make_next_draft_input(
bonus_tokens=next_token_ids, bonus_tokens=next_token_ids,
@@ -780,6 +810,11 @@ class DSparkWorkerV2(BaseSpecWorker):
inject_gate=fold_eligible, inject_gate=fold_eligible,
) )
else: else:
if (
self._verify_epilogue is not None
and self._verify_planner.mode_value == "static"
):
self._verify_epilogue.begin_static_step(bs, fold_eligible)
target_verify = self._verify_executor.run_non_compact( target_verify = self._verify_executor.run_non_compact(
batch=batch, batch=batch,
draft_input=draft_input, draft_input=draft_input,
@@ -804,7 +839,11 @@ class DSparkWorkerV2(BaseSpecWorker):
grammar_mask.apply(logits_output.next_token_logits) grammar_mask.apply(logits_output.next_token_logits)
epilogue = self._verify_executor.verify_epilogue epilogue = self._verify_executor.verify_epilogue
folded_accept = fold_eligible and run_compact and can_run_cuda_graph folded_accept = (
fold_eligible
and can_run_cuda_graph
and (run_compact or self._verify_planner.mode_value == "static")
)
accept = self._verify_executor.accept_and_finalize( accept = self._verify_executor.accept_and_finalize(
folded_accept=folded_accept, folded_accept=folded_accept,
bs=bs, bs=bs,
@@ -817,6 +856,11 @@ class DSparkWorkerV2(BaseSpecWorker):
prefix_lens=prefix_lens, prefix_lens=prefix_lens,
draft_tokens=draft_tokens, draft_tokens=draft_tokens,
) )
self.model_runner.ngram_embedding_manager.update_after_verify(
verify_ids_2d=verify_ids_2d,
req_pool_indices=batch.req_pool_indices,
commit_lens=accept.commit_lens,
)
if batch.return_logprob: if batch.return_logprob:
compute_spec_logprobs( compute_spec_logprobs(
batch, batch,
@@ -91,6 +91,7 @@ from sglang.srt.configs import (
XllmConfig, XllmConfig,
) )
from sglang.srt.configs.deepseek_ocr import DeepseekVLV2Config from sglang.srt.configs.deepseek_ocr import DeepseekVLV2Config
from sglang.srt.configs.deepseek_v41 import DEEPSEEK_V41_CONFIG_CLASSES
from sglang.srt.configs.internvl import InternVLChatConfig from sglang.srt.configs.internvl import InternVLChatConfig
from sglang.srt.utils import get_bool_env_var, logger, lru_cache_frozenset from sglang.srt.utils import get_bool_env_var, logger, lru_cache_frozenset
from sglang.srt.utils.runai_utils import ObjectStorageModel, is_runai_obj_uri from sglang.srt.utils.runai_utils import ObjectStorageModel, is_runai_obj_uri
@@ -192,9 +193,28 @@ try:
class _DeepseekV4ConfigAlias(_HFDeepseekV3Config): class _DeepseekV4ConfigAlias(_HFDeepseekV3Config):
model_type = "deepseek_v4" model_type = "deepseek_v4"
hc_pre_from_prev_sublayer = False
# V4 normalizes each attention query head (weightless rmsnorm) before RoPE.
q_head_norm = True
kv_source_layer_ids = ()
index_source_layer_ids = ()
candidate_source_layer_id = -1
candidate_topk_blocks = 0
candidate_block_size = 0
engram_layer_ids = ()
engram_num_embeddings = ()
engram_max_ngram_size = 1
engram_vocab_size = 0
engram_n_heads = 0
engram_head_dim = 0
engram_pad_token_id = 2
engram_compressed_vocab_size = 0
_CONFIG_REGISTRY["deepseek_v32"] = _DeepseekV32ConfigAlias _CONFIG_REGISTRY["deepseek_v32"] = _DeepseekV32ConfigAlias
_CONFIG_REGISTRY["deepseek_v4"] = _DeepseekV4ConfigAlias _CONFIG_REGISTRY["deepseek_v4"] = _DeepseekV4ConfigAlias
_CONFIG_REGISTRY.update(
{cls.model_type: cls for cls in DEEPSEEK_V41_CONFIG_CLASSES}
)
# For kimi_k25_eagle3 # For kimi_k25_eagle3
class _KimiK2ConfigAlias(_HFDeepseekV3Config): class _KimiK2ConfigAlias(_HFDeepseekV3Config):
@@ -19,6 +19,10 @@ from typing import Optional
from transformers import PretrainedConfig from transformers import PretrainedConfig
from transformers.models.auto.modeling_auto import MODEL_FOR_CAUSAL_LM_MAPPING_NAMES from transformers.models.auto.modeling_auto import MODEL_FOR_CAUSAL_LM_MAPPING_NAMES
from sglang.srt.configs.deepseek_v41 import (
DeepseekV41Config,
normalize_deepseek_v41_config,
)
from sglang.srt.configs.model_config_parser_registry import ( from sglang.srt.configs.model_config_parser_registry import (
ModelConfigParserBase, ModelConfigParserBase,
get_model_config_parser, get_model_config_parser,
@@ -179,6 +183,8 @@ class HfModelConfigParser(ModelConfigParserBase):
_set_architectures(config, "DeepseekOCRForCausalLM") _set_architectures(config, "DeepseekOCRForCausalLM")
config = DeepseekVLV2Config.from_pretrained(model, revision=revision) config = DeepseekVLV2Config.from_pretrained(model, revision=revision)
_apply_deepseek_ocr_overrides(config, model) _apply_deepseek_ocr_overrides(config, model)
elif isinstance(config, DeepseekV41Config):
config._name_or_path = model
elif config.model_type in _CONFIG_REGISTRY: elif config.model_type in _CONFIG_REGISTRY:
model_type = config.model_type model_type = config.model_type
if model_type == "deepseek_vl_v2" and is_ocr: if model_type == "deepseek_vl_v2" and is_ocr:
@@ -315,6 +321,8 @@ def get_config(
) )
if model_override_args: if model_override_args:
if isinstance(config, DeepseekV41Config):
model_override_args = normalize_deepseek_v41_config(model_override_args)
# A plain update() setattrs a dict-valued override straight onto the # A plain update() setattrs a dict-valued override straight onto the
# config, so '{"text_config": {...}}' on a VLM would replace the whole # config, so '{"text_config": {...}}' on a VLM would replace the whole
# sub-config with a dict and break attribute access downstream. # sub-config with a dict and break attribute access downstream.
@@ -282,6 +282,10 @@ class TinyDSV4ModelConfig:
index_topk=DSV4_INDEX_TOPK, index_topk=DSV4_INDEX_TOPK,
num_hidden_layers=len(compression_ratios), num_hidden_layers=len(compression_ratios),
compress_ratios=list(compression_ratios), compress_ratios=list(compression_ratios),
# Ratio 1/2 layers are their own kv_source (one-layer fixtures).
kv_source_layer_ids=[
i for i, ratio in enumerate(compression_ratios) if ratio in (1, 2)
],
) )
self.hf_config.get_text_config = lambda: self.hf_config self.hf_config.get_text_config = lambda: self.hf_config
self.hf_text_config = self.hf_config self.hf_text_config = self.hf_config
@@ -411,6 +415,10 @@ class MockDSV4ModelRunner:
device=device, device=device,
enable_memory_saver=False, enable_memory_saver=False,
compression_ratios=list(compression_ratios), compression_ratios=list(compression_ratios),
kv_source_layers=model_config.hf_config.kv_source_layer_ids,
# Full locs are the identity-mapped SWA locs below, so the c1/c2
# latent pools (slot = loc // ratio) only need to span swa_size.
full_size=swa_size,
) )
# Register identity full->swa mapping over swa_size full locs. # Register identity full->swa mapping over swa_size full locs.
identity = torch.arange(swa_size, dtype=torch.int64, device=device) identity = torch.arange(swa_size, dtype=torch.int64, device=device)
@@ -1106,20 +1114,20 @@ def prepare_dsv4_runner_inputs(
# reference needs to build that metadata itself. Stash the current batch # reference needs to build that metadata itself. Stash the current batch
# so `_pure_torch_dsv4_combined_reference` knows which one to use. # so `_pure_torch_dsv4_combined_reference` knows which one to use.
fixture._current_batch = batch # type: ignore[attr-defined] fixture._current_batch = batch # type: ignore[attr-defined]
if case.compress_ratio in (4, 128): if case.compress_ratio in (1, 2, 4, 128):
_populate_extra_kv_cache(fixture, layer_id=0, num_entries=_DSV4_EXTRA_ENTRIES) _populate_extra_kv_cache(fixture, layer_id=0, num_entries=_DSV4_EXTRA_ENTRIES)
def _seed_c4_if_needed( def _seed_c4_if_needed(
fixture: DSV4AttentionFixture, *, num_entries: int = _DSV4_EXTRA_ENTRIES fixture: DSV4AttentionFixture, *, num_entries: int | None = None
) -> None: ) -> None:
"""For compress_ratio=4, seed the C4 metadata the exercised path consumes """Seed `c4_sparse_page_indices` (dense extend) or `c4_sparse_raw_indices`
(the C4Indexer would normally populate it; the compact fixture skips the (sparse prefill); the compact fixture skips the indexer that fills them.
indexer): `c4_sparse_page_indices` for the dense extend path,
`c4_sparse_raw_indices` for sparse prefill. No-op for other compress_ratios.
""" """
if fixture.case.compress_ratio != 4: if fixture.case.compress_ratio not in (1, 2, 4):
return return
if num_entries is None:
num_entries = getattr(fixture, "extra_entries", _DSV4_EXTRA_ENTRIES)
if fixture.seed_c4_for_sparse_prefill: if fixture.seed_c4_for_sparse_prefill:
_seed_c4_sparse_prefill_indices(fixture, num_entries=num_entries) _seed_c4_sparse_prefill_indices(fixture, num_entries=num_entries)
else: else:
@@ -1138,7 +1146,7 @@ def run_dsv4_fixture_eager(fixture: DSV4AttentionFixture) -> torch.Tensor:
full_kv_locs_per_req = _populate_swa_kv_cache( full_kv_locs_per_req = _populate_swa_kv_cache(
fixture, max_context_len=max_context_len, device=runner.device fixture, max_context_len=max_context_len, device=runner.device
) )
if case.compress_ratio in (4, 128): if case.compress_ratio in (1, 2, 4, 128):
_populate_extra_kv_cache(fixture, layer_id=0, num_entries=_DSV4_EXTRA_ENTRIES) _populate_extra_kv_cache(fixture, layer_id=0, num_entries=_DSV4_EXTRA_ENTRIES)
q_input, _ = fixture.actual_module.project(fixture.input_hidden) q_input, _ = fixture.actual_module.project(fixture.input_hidden)
with torch.no_grad(), forward_context(ForwardContext(attn_backend=fixture.backend)): with torch.no_grad(), forward_context(ForwardContext(attn_backend=fixture.backend)):
@@ -1202,7 +1210,7 @@ def expected_dsv4_output_from_inputs(
runner = fixture.runner runner = fixture.runner
max_context_len = runner.req_to_token_pool.req_to_token.shape[1] max_context_len = runner.req_to_token_pool.req_to_token.shape[1]
q_input, _ = fixture.actual_module.project(inputs["input_hidden"]) q_input, _ = fixture.actual_module.project(inputs["input_hidden"])
if case.compress_ratio in (4, 128): if case.compress_ratio in (1, 2, 4, 128):
return _pure_torch_dsv4_combined_reference(fixture, q_input).float() return _pure_torch_dsv4_combined_reference(fixture, q_input).float()
full_kv_locs_per_req = _full_kv_locs_per_req( full_kv_locs_per_req = _full_kv_locs_per_req(
case, max_context_len=max_context_len, device=runner.device case, max_context_len=max_context_len, device=runner.device
@@ -1319,7 +1327,7 @@ def _pure_torch_dsv4_combined_reference(
swa_indices = md.swa_page_indices # [num_q, padded_window], full-pool locs swa_indices = md.swa_page_indices # [num_q, padded_window], full-pool locs
swa_topk_lengths = md.swa_topk_lengths # [num_q] swa_topk_lengths = md.swa_topk_lengths # [num_q]
if case.compress_ratio in (4, 128): if case.compress_ratio in (1, 2, 4, 128):
extra_indices, extra_topk_lengths = _extra_metadata_indices( extra_indices, extra_topk_lengths = _extra_metadata_indices(
md, case.compress_ratio md, case.compress_ratio
) )
@@ -1443,9 +1451,9 @@ def _seed_c4_sparse_prefill_indices(
lens = (md.positions_casual + 1) // ratio lens = (md.positions_casual + 1) // ratio
max_len = int(lens.max().item()) max_len = int(lens.max().item())
pool = fixture.runner.token_to_kv_pool pool = fixture.runner.token_to_kv_pool
c4_page_size = pool.get_extra_key_page_size(layer_id=0) c_page_size = pool.get_extra_key_page_size(layer_id=0)
assert max_len <= min(num_entries, c4_page_size), ( assert max_len <= min(num_entries, c_page_size), (
f"case attends {max_len} c4 entries; only {min(num_entries, c4_page_size)} populated" f"case attends {max_len} c{ratio} entries; only {min(num_entries, c_page_size)} populated"
) )
assert (md.page_table[:, 0] == 0).all(), ( assert (md.page_table[:, 0] == 0).all(), (
"sparse seeding requires the raw==physical identity (first page 0)" "sparse seeding requires the raw==physical identity (first page 0)"
@@ -1504,7 +1512,7 @@ def run_dsv4_target_verify_attention_case(
testcase.assertEqual(fixture.backend.max_context_len, max_context_len) testcase.assertEqual(fixture.backend.max_context_len, max_context_len)
_populate_swa_kv_cache(fixture, max_context_len=max_context_len, device=device) _populate_swa_kv_cache(fixture, max_context_len=max_context_len, device=device)
if case.compress_ratio in (4, 128): if case.compress_ratio in (1, 2, 4, 128):
_populate_extra_kv_cache(fixture, layer_id=0, num_entries=_DSV4_EXTRA_ENTRIES) _populate_extra_kv_cache(fixture, layer_id=0, num_entries=_DSV4_EXTRA_ENTRIES)
_prepare_target_verify_batch(fixture.forward_batch, case, device) _prepare_target_verify_batch(fixture.forward_batch, case, device)
@@ -1614,27 +1622,20 @@ def run_dsv4_compress_attention_case(
dtype: torch.dtype = torch.bfloat16, dtype: torch.dtype = torch.bfloat16,
device: str = "cuda", device: str = "cuda",
) -> None: ) -> None:
"""Math-faithful test for the SWA + C4 (compress_ratio=4) / SWA + C128 """SWA + compressed-cache path (compress ratios 1, 2, 4, 128) through
(compress_ratio=128) path through `DeepseekV4AttnBackend.forward`. `DeepseekV4AttnBackend.forward` against a pure-PyTorch reference that reads the
same cache bytes and metadata indices. `sparse_prefill` pins
Pre-writes random packed K into both the SWA cache and the extra `SGLANG_OPT_FLASHMLA_SPARSE_PREFILL`; the C4 seeding dispatches on the same flag.
(C4/C128) cache via the production pack+set paths, lets
`init_forward_metadata` populate the compression metadata, manually seeds
the C4 metadata the exercised path consumes (see `_seed_c4_if_needed`; the
un-run indexer would otherwise leave it at `-1` / uninitialized), then
dispatches `forward(compress_ratio=case.compress_ratio)` and compares
against an independent pure-PyTorch SWA + extra reference that reads the
SAME cache bytes and metadata indices.
`sparse_prefill` pins `SGLANG_OPT_FLASHMLA_SPARSE_PREFILL`, selecting the
dense `flash_mla_with_kvcache` extend path or `_forward_prefill_sparse`;
the C4 seeding dispatches on the same flag.
""" """
assert case.compress_ratio in ( assert case.compress_ratio in (1, 2, 4, 128), (
4, f"DSV4 compact runner requires compress_ratio in (1, 2, 4, 128); "
128, f"got {case.compress_ratio}"
), ( )
f"DSV4 compact runner requires compress_ratio in (4, 128); got {case.compress_ratio}" # The sparse-prefill seeding attends (pos + 1) // ratio entries per query, so
# the low ratios need more populated entries than the default 32.
if case.compress_ratio in (1, 2):
extra_entries = max(
extra_entries, max(case.seq_lens) // case.compress_ratio + 1
) )
if sparse_prefill: if sparse_prefill:
assert case.forward_mode.is_extend_without_speculative(), ( assert case.forward_mode.is_extend_without_speculative(), (
@@ -1648,6 +1649,7 @@ def run_dsv4_compress_attention_case(
compression_ratios=[case.compress_ratio], compression_ratios=[case.compress_ratio],
) )
fixture.seed_c4_for_sparse_prefill = sparse_prefill fixture.seed_c4_for_sparse_prefill = sparse_prefill
fixture.extra_entries = extra_entries # type: ignore[attr-defined]
runner = fixture.runner runner = fixture.runner
max_context_len = runner.req_to_token_pool.req_to_token.shape[1] max_context_len = runner.req_to_token_pool.req_to_token.shape[1]
@@ -126,6 +126,26 @@ class TestDSV4AttentionBackendCorrectness(CustomTestCase):
extend_lens=(16,), extend_lens=(16,),
compress_ratio=128, compress_ratio=128,
), ),
DSV4AttentionCase(
name="dsv4_c2_extend",
backend="dsv4",
forward_mode=ForwardMode.EXTEND,
num_heads=64,
page_size=DSV4_PAGE_SIZE,
# Odd lengths: the ratio-2 causal count (pos + 1) // 2 rounds down.
prefix_lens=(33,),
extend_lens=(7,),
compress_ratio=2,
),
DSV4AttentionCase(
name="dsv4_c2_decode",
backend="dsv4",
forward_mode=ForwardMode.DECODE,
num_heads=64,
page_size=DSV4_PAGE_SIZE,
prefix_lens=(65,),
compress_ratio=2,
),
DSV4AttentionCase( DSV4AttentionCase(
name="dsv4_c128_decode", name="dsv4_c128_decode",
backend="dsv4", backend="dsv4",
@@ -533,6 +553,7 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
backend.model_runner = SimpleNamespace( backend.model_runner = SimpleNamespace(
spec_algorithm=SpeculativeAlgorithm.DFLASH spec_algorithm=SpeculativeAlgorithm.DFLASH
) )
backend.token_to_kv_pool = SimpleNamespace(request_window=None)
backend.forward_metadata = DSV4Metadata( backend.forward_metadata = DSV4Metadata(
self._make_core_metadata(0), indexer_metadata=None self._make_core_metadata(0), indexer_metadata=None
) )
@@ -577,6 +598,7 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
backend.model_runner = SimpleNamespace( backend.model_runner = SimpleNamespace(
spec_algorithm=SpeculativeAlgorithm.DFLASH spec_algorithm=SpeculativeAlgorithm.DFLASH
) )
backend.token_to_kv_pool = SimpleNamespace(request_window=None)
backend.forward_metadata = DSV4Metadata( backend.forward_metadata = DSV4Metadata(
self._make_core_metadata(0), indexer_metadata=None self._make_core_metadata(0), indexer_metadata=None
) )
@@ -794,7 +816,8 @@ class TestDSV4SwaOutCacheLocResolution(CustomTestCase):
backend = object.__new__(DeepseekV4AttnBackend) backend = object.__new__(DeepseekV4AttnBackend)
backend.forward_metadata = None backend.forward_metadata = None
backend.token_to_kv_pool = SimpleNamespace( backend.token_to_kv_pool = SimpleNamespace(
translate_loc_from_full_to_swa=lambda loc: mapping[loc] translate_loc_from_full_to_swa=lambda loc: mapping[loc],
request_window=None,
) )
return backend return backend
@@ -175,7 +175,9 @@ def _make_backend(
dsv4_prefill_backend: str = "auto", dsv4_prefill_backend: str = "auto",
) -> DeepseekV4AttnBackend: ) -> DeepseekV4AttnBackend:
backend = DeepseekV4AttnBackend.__new__(DeepseekV4AttnBackend) backend = DeepseekV4AttnBackend.__new__(DeepseekV4AttnBackend)
backend.forward_metadata = SimpleNamespace(sparse_prefill_cache=None) backend.forward_metadata = SimpleNamespace(
sparse_prefill_cache=None, late_layer_tail=None
)
backend.req_to_token = req_to_token backend.req_to_token = req_to_token
backend.sparse_prefill_workspace = SparsePrefillWorkspace(device) backend.sparse_prefill_workspace = SparsePrefillWorkspace(device)
backend.softmax_scale = 512**-0.5 backend.softmax_scale = 512**-0.5
@@ -1,6 +1,8 @@
import unittest import unittest
from types import SimpleNamespace from types import SimpleNamespace
import torch
from sglang.srt.arg_groups.overrides import resolution_result from sglang.srt.arg_groups.overrides import resolution_result
from sglang.srt.arg_groups.speculative_hook import ( from sglang.srt.arg_groups.speculative_hook import (
_handle_dspark, _handle_dspark,
@@ -120,5 +122,37 @@ class TestDsparkDpAttentionMoeA2aGate(CustomTestCase):
_handle_dspark(server_args) _handle_dspark(server_args)
class TestDsparkFoldedSamplingDefault(CustomTestCase):
def test_sharded_greedy_default_and_sampling_override(self):
from sglang.srt.environ import DsparkFoldedSampling, envs
from sglang.srt.speculative.dspark_components.dspark_draft_sampler import (
_resolve_folded_sampling,
)
model = SimpleNamespace(
lm_head=SimpleNamespace(org_vocab_size=128, weight=torch.empty(1)),
markov_head=SimpleNamespace(supports_sharded_greedy=True),
)
args = dict(
model=model,
gamma=5,
max_bs=64,
device="cpu",
tp_rank=0,
available_memory_gb=16,
)
with envs.SGLANG_DSPARK_FOLDED_SAMPLING.override(
DsparkFoldedSampling.AUTO.value
):
self.assertFalse(_resolve_folded_sampling(**args))
model.markov_head.supports_sharded_greedy = False
self.assertTrue(_resolve_folded_sampling(**args))
model.markov_head.supports_sharded_greedy = True
with envs.SGLANG_DSPARK_FOLDED_SAMPLING.override(
DsparkFoldedSampling.FORCE.value
):
self.assertTrue(_resolve_folded_sampling(**args))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -86,6 +86,7 @@ def _prefill_queue(sched):
def _decode_queue(sched): def _decode_queue(sched):
q = SimpleNamespace( q = SimpleNamespace(
scheduler=sched, scheduler=sched,
token_to_kv_pool_allocator=MagicMock(),
retracted_queue=[], retracted_queue=[],
pending_reqs=[], pending_reqs=[],
_check_if_req_exceed_kv_capacity=MagicMock(return_value=False), _check_if_req_exceed_kv_capacity=MagicMock(return_value=False),
@@ -906,7 +906,7 @@ def _buf_infos(*ptrs):
def _make_dsv4_target(*, unified, mapping=None): def _make_dsv4_target(*, unified, mapping=None):
pool = object.__new__(DeepSeekV4TokenToKVPool) pool = object.__new__(DeepSeekV4TokenToKVPool)
pool.compression_ratios = [0, 4, 128] pool.compression_ratios = [0, 2, 1, 4, 128]
pool._unified_kv = unified pool._unified_kv = unified
pool.page_size = 256 pool.page_size = 256
pool.sliding_window = 128 pool.sliding_window = 128
@@ -7,6 +7,7 @@ SM90 / SM100 / SM120.
""" """
import unittest import unittest
from types import SimpleNamespace
from unittest import mock from unittest import mock
import torch import torch
@@ -267,11 +268,7 @@ class TestMxfp8LinearBackends(_LinearBackendCheck):
is_backend_supported.assert_called_once_with("cute-dsl", 107) is_backend_supported.assert_called_once_with("cute-dsl", 107)
class TestBlockFp8AsMxfp8Linear(_LinearBackendCheck): def _build_block32_layer(n: int, k: int, keep_plain_weight_layout: bool = False):
"""A 32-wide-K ue8m0 block-fp8 weight served through the MXFP8 GEMMs."""
@staticmethod
def _build_layer(n: int, k: int, keep_plain_weight_layout: bool = False):
quant_config = Fp8Config( quant_config = Fp8Config(
is_checkpoint_fp8_serialized=True, is_checkpoint_fp8_serialized=True,
activation_scheme="dynamic", activation_scheme="dynamic",
@@ -286,6 +283,12 @@ class TestBlockFp8AsMxfp8Linear(_LinearBackendCheck):
load_linear_weights(layer, weight=w_fp8, weight_scale_inv=scale_e8m0) load_linear_weights(layer, weight=w_fp8, weight_scale_inv=scale_e8m0)
return layer, w_dequant return layer, w_dequant
class TestBlockFp8AsMxfp8Linear(_LinearBackendCheck):
"""A 32-wide-K ue8m0 block-fp8 weight served through the MXFP8 GEMMs."""
_build_layer = staticmethod(_build_block32_layer)
def _run(self, backend: str): def _run(self, backend: str):
self._check_backend( self._check_backend(
backend, _block32_backends(), BLOCK32_SHAPES, self._build_layer backend, _block32_backends(), BLOCK32_SHAPES, self._build_layer
@@ -339,6 +342,120 @@ class TestBlockFp8AsMxfp8Linear(_LinearBackendCheck):
plain_layer.quant_method.apply(plain_layer, Mxfp8SwizzledInput(q, s)) plain_layer.quant_method.apply(plain_layer, Mxfp8SwizzledInput(q, s))
@unittest.skipUnless(
"flashinfer_cutedsl" in _block32_backends(),
"block-fp8-as-MXFP8 prefill tuning needs the FlashInfer CuTe-DSL kernel",
)
class TestBlockFp8AsMxfp8PrefillAutotune(_LinearBackendCheck):
"""The startup hook that tunes those layers for the prefill M buckets."""
def setUp(self):
super().setUp()
patcher = mock.patch.object(
fp8_utils,
"FP8_GEMM_RUNNER_BACKEND",
Fp8GemmRunnerBackend.FLASHINFER_CUTEDSL,
)
patcher.start()
self.addCleanup(patcher.stop)
torch.manual_seed(7)
@staticmethod
def _ready_layer(n: int, k: int, keep_plain_weight_layout: bool = False):
layer, _ = _build_block32_layer(n, k, keep_plain_weight_layout)
layer.quant_method.process_weights_after_loading(layer)
return layer
def test_model_hook_deduplicates_ready_block_fp8_weights(self):
from sglang.srt.models.deepseek_v4 import DeepseekV4ForCausalLM
layers = torch.nn.ModuleList()
methods = []
for _ in range(2):
layer = self._ready_layer(128, 128)
methods.append(layer.quant_method)
layer.quant_method.apply = mock.Mock()
layers.append(layer)
# An unprepared layer intentionally has no swizzled scale buffer.
fallback = self._ready_layer(128, 128, keep_plain_weight_layout=True)
layers.append(fallback)
model = SimpleNamespace(
config=SimpleNamespace(model_type="deepseek_v41"), model=layers
)
count = DeepseekV4ForCausalLM.autotune_prefill_kernels(
model, 4096, dtype=torch.bfloat16
)
self.assertEqual(count, 1)
methods[0].apply.assert_called_once()
self.assertEqual(methods[0].apply.call_args.args[1].shape, (4096, 128))
methods[1].apply.assert_not_called()
for method in methods:
self.assertEqual(method.mxfp8_prefill_autotune_min_tokens, 4096)
self.assertIsNone(fallback.quant_method.mxfp8_prefill_autotune_min_tokens)
def test_block_fp8_dispatch_keeps_decode_and_determinism_pinned(self):
layer = self._ready_layer(128, 128)
method = layer.quant_method
method.mxfp8_prefill_autotune_min_tokens = 4096
call = mock.Mock(return_value=torch.empty(0))
method.w8a8_mxfp8_linear = call
for rows, invariant, deterministic, expected in (
(6, False, False, None),
(4096, False, False, False),
(4096, True, False, True),
(4096, False, True, True),
):
with self.subTest(
rows=rows, invariant=invariant, deterministic=deterministic
):
with (
mock.patch(
"sglang.srt.batch_invariant_ops.is_batch_invariant_mode_enabled",
return_value=invariant,
),
mock.patch(
"sglang.srt.runtime_context.get_exec",
return_value=SimpleNamespace(
deterministic=SimpleNamespace(
enable_deterministic_inference=deterministic
)
),
),
):
method.apply(layer, torch.empty(rows, 128, device="cuda"))
self.assertEqual(call.call_args.kwargs.get("pin_tactic"), expected)
def test_prefill_tuning_leaves_decode_bit_identical(self):
"""Tuning the prefill buckets must not move the decode tactic: below the
stamped min_tokens the output has to stay bit-for-bit what it was."""
from flashinfer.autotuner import autotune
from sglang.srt.models.deepseek_v4 import DeepseekV4ForCausalLM
runtime_patch = mock.patch(
"sglang.srt.runtime_context.get_exec",
return_value=SimpleNamespace(
deterministic=SimpleNamespace(enable_deterministic_inference=False)
),
)
runtime_patch.start()
self.addCleanup(runtime_patch.stop)
layer = self._ready_layer(1792, 5120)
method = layer.quant_method
x = torch.randn(6, 5120, device="cuda", dtype=torch.bfloat16)
original = method.apply(layer, x)
model = SimpleNamespace(
config=SimpleNamespace(model_type="deepseek_v41"),
model=torch.nn.ModuleList([layer]),
)
with autotune(True):
DeepseekV4ForCausalLM.autotune_prefill_kernels(
model, 4096, dtype=torch.bfloat16
)
self.assertEqual(method.mxfp8_prefill_autotune_min_tokens, 4096)
torch.testing.assert_close(method.apply(layer, x), original, rtol=0, atol=0)
@unittest.skipIf(get_device_sm() < 90, "FP8 GEMM backends require SM90+") @unittest.skipIf(get_device_sm() < 90, "FP8 GEMM backends require SM90+")
class TestModeloptFp8PerTensorLinear(_LinearBackendCheck): class TestModeloptFp8PerTensorLinear(_LinearBackendCheck):
"""Per-tensor FP8 (ModelOptFp8LinearMethod, static scales) on the auto """Per-tensor FP8 (ModelOptFp8LinearMethod, static scales) on the auto
@@ -0,0 +1,114 @@
"""A TP-sharded MXFP4 trtllm-gen MoE whose per-rank intermediate size needs
padding must sum to the unsharded experts' output."""
import unittest
from contextlib import nullcontext
from types import SimpleNamespace
from unittest.mock import Mock, patch
import torch
from sglang.srt.layers.moe.token_dispatcher import StandardDispatchOutput
from sglang.srt.layers.moe.topk import StandardTopKOutput
from sglang.srt.layers.quantization import mxfp4_flashinfer_trtllm_moe as mxfp4
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=60, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
def make_layer(weights):
layer = torch.nn.Module()
names = (
"w13_weight",
"w2_weight",
"w13_weight_scale_inv",
"w2_weight_scale_inv",
)
for name, tensor in zip(names, weights):
layer.register_parameter(name, torch.nn.Parameter(tensor, requires_grad=False))
layer.num_experts = weights[0].shape[0]
layer.num_local_experts = layer.num_experts
layer.moe_ep_rank = 0
return layer
def make_weights(intermediate, hidden=256, device="cpu"):
experts = 8
def fp4_packed(*shape):
return torch.randint(-128, 128, shape, dtype=torch.int8, device=device)
def e8m0_scales(*shape):
return torch.randint(-6, -3, shape, device=device).float().exp2()
return (
fp4_packed(experts, 2 * intermediate, hidden // 2),
fp4_packed(experts, hidden, intermediate // 2),
e8m0_scales(experts, 2 * intermediate, hidden // 32),
e8m0_scales(experts, hidden, intermediate // 32),
)
class TestMxfp4TrtllmPadding(CustomTestCase):
@unittest.skipUnless(
torch.cuda.is_available() and torch.cuda.get_device_capability()[0] == 10,
"Requires Blackwell",
)
def test_tp4_matches_unsharded_experts(self):
torch.manual_seed(42)
weights = make_weights(2304, hidden=5120, device="cuda")
def prepare(tensors):
layer = make_layer(tensors)
method = object.__new__(mxfp4.Mxfp4FlashinferTrtllmMoEMethod)
method._fp8 = Mock()
method.prefix = "test.experts"
method.flashinfer_mxfp4_moe_precision = "default"
method.process_weights_after_loading(layer)
method.create_moe_runner(layer, SimpleNamespace(swiglu_limit=10.0))
return method, layer
full = prepare([tensor.clone() for tensor in weights])
shards = []
for rank in range(4):
start = rank * 576
end = start + 576
w13, w2, s13, s2 = weights
shard = (
torch.cat(
(w13[:, start:end], w13[:, 2304 + start : 2304 + end]), dim=1
),
w2[..., start // 2 : end // 2].contiguous(),
torch.cat(
(s13[:, start:end], s13[:, 2304 + start : 2304 + end]), dim=1
),
s2[..., start // 32 : end // 32].contiguous(),
)
shards.append(prepare(shard))
with (
patch.object(mxfp4, "get_tp_group", return_value=None),
patch.object(mxfp4, "is_allocation_symmetric", return_value=False),
patch.object(mxfp4, "use_symmetric_memory", return_value=nullcontext()),
):
for tokens in (1, 64):
with self.subTest(tokens=tokens):
x = torch.randn(tokens, 5120, dtype=torch.bfloat16, device="cuda")
logits = torch.randn(tokens, 8, device="cuda")
scores, ids = logits.softmax(-1).topk(6, dim=-1)
topk = StandardTopKOutput(scores, ids.to(torch.int32), logits)
dispatch = StandardDispatchOutput(x, None, topk)
reference = full[0].apply(full[1], dispatch).hidden_states.float()
actual = sum(
method.apply(layer, dispatch).hidden_states.float()
for method, layer in shards
)
rmse = torch.linalg.norm(actual - reference) / torch.linalg.norm(
reference
)
self.assertLess(rmse.item(), 0.01)
if __name__ == "__main__":
unittest.main()
@@ -531,5 +531,26 @@ class TestDSV4NonPagedIndexer(CustomTestCase):
self.assertEqual(call.kwargs, {"clean_logits": False, "max_seqlen_k": 128}) self.assertEqual(call.kwargs, {"clean_logits": False, "max_seqlen_k": 128})
class TestCandidateIndexerGating(CustomTestCase):
def test_candidate_indexer_gating(self):
from sglang.srt.layers.attention.dsv4 import candidate_indexer
def platform(sm):
return patch.object(
candidate_indexer, "get_platform", lambda: SimpleNamespace(device_sm=sm)
)
flag = "sglang.srt.layers.deep_gemm_wrapper.configurer.DEEPGEMM_PAGED_SPARSE_MQA_LOGITS"
# V4 models have no candidate source; Hopper selects through masks inline.
with platform(100), patch(flag, True):
self.assertIsNone(candidate_indexer.make_candidate_indexer(0, 8))
with platform(90), patch(flag, False):
self.assertIsNone(candidate_indexer.make_candidate_indexer(2048, 8))
# Blackwell without DeepGEMM's sparse logits fails instead of falling back.
with platform(100), patch(flag, False):
with self.assertRaises(RuntimeError):
candidate_indexer.make_candidate_indexer(2048, 8)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -529,6 +529,8 @@ class TestDecodePrebuilt(unittest.TestCase):
scheduler.policy = MagicMock() scheduler.policy = MagicMock()
scheduler.schedule_stream = MagicMock() scheduler.schedule_stream = MagicMock()
scheduler.forward_stream = MagicMock() scheduler.forward_stream = MagicMock()
scheduler.ngram_embedding_manager = MagicMock()
scheduler.chunked_req = None
return scheduler return scheduler
def test_waiting_queue_is_sorted_before_prebuilt_selection(self): def test_waiting_queue_is_sorted_before_prebuilt_selection(self):
@@ -5,11 +5,17 @@ from unittest.mock import MagicMock, patch
import torch import torch
from sglang.kernels.ops.attention.dsv4.kv_layout import (
KVLayout,
is_valid_kv_layout_pair,
)
from sglang.srt.mem_cache.allocator.swa import SWATokenToKVPoolAllocator
from sglang.srt.mem_cache.deepseek_v4_memory_pool import ( from sglang.srt.mem_cache.deepseek_v4_memory_pool import (
DeepSeekV4SingleKVPool, DeepSeekV4SingleKVPool,
DeepSeekV4TokenToKVPool, DeepSeekV4TokenToKVPool,
_CompressedPoolConfig, _CompressedPoolConfig,
) )
from sglang.srt.runtime_context import get_context
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase from sglang.test.test_utils import CustomTestCase
@@ -25,6 +31,8 @@ class TestDSV4CompressedPools(CustomTestCase):
pool = DeepSeekV4TokenToKVPool.__new__(DeepSeekV4TokenToKVPool) pool = DeepSeekV4TokenToKVPool.__new__(DeepSeekV4TokenToKVPool)
pool._unified_kv = unified pool._unified_kv = unified
pool.uniform_fp8 = False pool.uniform_fp8 = False
pool.kv_layout = KVLayout.V4
pool.compressed_kv_layout_option = None
pool.compressed_pool_configs = { pool.compressed_pool_configs = {
4: _CompressedPoolConfig( 4: _CompressedPoolConfig(
256, 64, torch.bfloat16, indexer_size=1024 256, 64, torch.bfloat16, indexer_size=1024
@@ -176,5 +184,151 @@ class TestDSV4CompressedPools(CustomTestCase):
pool.get_index_k_page_size(128) pool.get_index_k_page_size(128)
HEAD_DIM = 512
ROPE_DIM = 64
PAGE_SIZE = 256
FULL_SIZE = 4 * PAGE_SIZE
class TestV41KVPoolLayouts(CustomTestCase):
"""A V4.1-layout pool hands the attention kernel page-aligned buffers and
picks the compressed layout each ratio asks for."""
def setUp(self):
super().setUp()
override = get_context().override_server_args(page_size=PAGE_SIZE)
override.install()
self.addCleanup(override.restore)
def make_pool(self, ratios, kv_source_layers, kv_layout, compressed=None, **sizes):
return DeepSeekV4TokenToKVPool(
max_num_reqs=16,
swa_size=FULL_SIZE,
c4_size=sizes.get("c4_size", 0),
c128_size=sizes.get("c128_size", 0),
c4_state_pool_size=sizes.get("c4_state_pool_size", 0),
c128_state_pool_size=sizes.get("c128_state_pool_size", 0),
page_size=PAGE_SIZE,
swa_page_size=PAGE_SIZE,
dtype=torch.float8_e4m3fn,
c4_state_dtype=torch.float32,
c128_state_dtype=torch.float32,
qk_nope_head_dim=HEAD_DIM - ROPE_DIM,
qk_rope_head_dim=ROPE_DIM,
indexer_head_dim=128,
layer_num=len(ratios),
device="cpu",
enable_memory_saver=False,
compression_ratios=ratios,
kv_source_layers=kv_source_layers,
full_size=FULL_SIZE,
kv_layout=kv_layout,
compressed_kv_layout=compressed,
)
def assert_kernel_requirements(self, pool, layout):
"""Pages start on the kernel's alignment, and its
(num_pages, page_size, 1, bytes_per_token) view walks one token per row."""
for buf in pool.kv_buffer:
self.assertEqual(buf.stride(0) % layout.page_align, 0)
bpt = layout.bytes_per_token
view = buf[:, : pool.page_size * bpt].view(
buf.shape[0], pool.page_size, 1, bpt
)
self.assertEqual(view.stride(1), bpt)
self.assertEqual(view.stride(0), pool.bytes_per_page_padded)
def test_v41_pool_buffers(self):
for option, expect in ((None, KVLayout.V41_FP4), ("fp8", KVLayout.V41)):
with self.subTest(compressed=option):
pool = self.make_pool([0, 0, 2, 1, 1], [2, 3], KVLayout.V41, option)
self.assert_kernel_requirements(pool.swa_kv_pool, KVLayout.V41)
self.assertEqual(pool.get_swa_key_bytes_per_token(), 528)
for ratio in (1, 2):
layer_id = pool.sources_by_ratio[ratio][0]
self.assertIs(pool.get_extra_key_layout(layer_id), expect)
self.assertEqual(
pool.get_extra_key_bytes_per_token(layer_id),
expect.bytes_per_token,
)
self.assertTrue(is_valid_kv_layout_pair(pool.kv_layout, expect))
self.assert_kernel_requirements(pool.kv_pools[ratio], expect)
# A pool of the fp4 layout cannot be the main cache.
with self.assertRaises(AssertionError):
self.make_pool([0], [], KVLayout.V41_FP4)
def test_v41_pool_with_c4_c128(self):
pool = self.make_pool(
[0, 4, 128],
[],
KVLayout.V41,
c4_size=PAGE_SIZE,
c128_size=PAGE_SIZE,
c4_state_pool_size=16,
c128_state_pool_size=16,
)
for ratio in (4, 128):
self.assertEqual(pool.kv_pools[ratio].page_size, PAGE_SIZE // ratio)
# The 2-token c128 page is the only production page that pads.
self.assertEqual(pool.kv_pools[128].bytes_per_page_padded, 1536)
class TestPagedDSparkWithEncoderReplay(CustomTestCase):
def setUp(self):
super().setUp()
override = get_context().override_server_args(
enable_encoder_swa_bounded_replay=True,
speculative_algorithm="DSPARK",
speculative_num_draft_tokens=6,
speculative_dspark_block_size=5,
page_size=256,
max_running_requests=2,
chunked_prefill_size=256,
)
override.install()
self.addCleanup(override.restore)
def make_pool(self, *, draft):
return DeepSeekV4TokenToKVPool(
max_num_reqs=2,
num_req_slots=3,
swa_size=1024,
c4_size=0,
c128_size=0,
c4_state_pool_size=0,
c128_state_pool_size=0,
page_size=256,
swa_page_size=256,
dtype=torch.float8_e4m3fn,
c4_state_dtype=torch.float32,
c128_state_dtype=torch.bfloat16,
qk_nope_head_dim=448,
qk_rope_head_dim=64,
indexer_head_dim=128,
layer_num=3,
device="cpu",
enable_memory_saver=False,
compression_ratios=[0, 0, 0],
online_mtp_max_draft_tokens=6,
full_size=2048,
is_draft_worker=draft,
)
def test_target_window_and_draft_paged_storage_share_allocator_mapping(self):
target = self.make_pool(draft=False)
draft = self.make_pool(draft=True)
allocator = SWATokenToKVPoolAllocator(
2048, 1024, 256, torch.float8_e4m3fn, "cpu", target, False
)
draft.register_mapping(allocator.full_to_swa_index_mapping)
allocator.full_to_swa_index_mapping[256:512] = torch.arange(768, 1024)
self.assertEqual(
draft.translate_loc_from_full_to_swa(
torch.tensor([256, 300, 511])
).tolist(),
[768, 812, 1023],
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -48,6 +48,7 @@ class TestUnifiedRadixHiCacheDispatch(unittest.TestCase):
) )
kvcache = _mock_kvcache(DeepSeekV4TokenToKVPool) kvcache = _mock_kvcache(DeepSeekV4TokenToKVPool)
kvcache.swa_kv_pool = MagicMock()
strategy = _select_strategy(kvcache, {FULL, SWA}) strategy = _select_strategy(kvcache, {FULL, SWA})
self.assertIsInstance(strategy, _DeepSeekV4Strategy) self.assertIsInstance(strategy, _DeepSeekV4Strategy)
@@ -141,6 +142,7 @@ class TestUnifiedRadixHiCacheDispatch(unittest.TestCase):
for cls in (SWAKVPool, DeepSeekV4TokenToKVPool): for cls in (SWAKVPool, DeepSeekV4TokenToKVPool):
kvcache = _mock_kvcache(cls) kvcache = _mock_kvcache(cls)
kvcache.swa_kv_pool = MagicMock()
with self.assertRaises(AssertionError) as cm: with self.assertRaises(AssertionError) as cm:
_select_strategy(kvcache, {FULL}) _select_strategy(kvcache, {FULL})
self.assertIn("No matching HiCache strategy", str(cm.exception)) self.assertIn("No matching HiCache strategy", str(cm.exception))
@@ -6,9 +6,10 @@ reduction holds only if ranks also enter tuning with the same cache, so these
cover that gate and the digest it decides on. cover that gate and the digest it decides on.
""" """
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci, register_cuda_ci
register_cpu_ci(est_time=52, suite="base-a-test-cpu") register_cpu_ci(est_time=57, suite="base-a-test-cpu")
register_cuda_ci(est_time=25, stage="base-b-kernel-unit", runner_config="1-gpu-large")
import json import json
import multiprocessing import multiprocessing
@@ -16,11 +17,15 @@ import os
import tempfile import tempfile
import traceback import traceback
import unittest import unittest
from contextlib import nullcontext
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import Mock, patch
import torch
import torch.distributed as dist import torch.distributed as dist
from sglang.srt.model_executor.runner import flashinfer_autotune as autotune
from sglang.srt.model_executor.runner.flashinfer_autotune import ( from sglang.srt.model_executor.runner.flashinfer_autotune import (
_autotune_cache_digest, _autotune_cache_digest,
_autotune_tactic_sync_group, _autotune_tactic_sync_group,
@@ -160,5 +165,113 @@ class TestDropDivergedAutotuneCache(CustomTestCase):
) )
class TestModelPrefillAutotune(CustomTestCase):
"""Model kernel warmup must cover prefill without a speculative dummy batch."""
def setUp(self):
self.hook = Mock(return_value=1)
self.mr = SimpleNamespace(
model=SimpleNamespace(autotune_prefill_kernels=self.hook),
is_generation=True,
is_draft_worker=False,
dtype=torch.bfloat16,
)
self.runner = SimpleNamespace(model_runner=self.mr)
# No dummy-buffer or attention APIs: this path must not build a
# TARGET_VERIFY batch or mutate request/KV state.
for target, kwargs in (
("max_prefill_buffer_tokens", {"return_value": 65536}),
(
"flashinfer_autotune_context",
{"side_effect": lambda *a, **k: nullcontext()},
),
):
p = patch.object(autotune, target, **kwargs)
setattr(self, target, p.start())
self.addCleanup(p.stop)
p = patch.object(
autotune.envs.SGLANG_FLASHINFER_AUTOTUNE_EXTEND, "get", return_value=False
)
p.start()
self.addCleanup(p.stop)
def test_declining_model_never_enters_the_autotune_context(self):
self.mr.model.wants_prefill_autotune = lambda: False
autotune.maybe_flashinfer_autotune_extend(self.runner, decode_num_tokens=384)
self.hook.assert_not_called()
self.flashinfer_autotune_context.assert_not_called()
def test_extend_pass_is_opt_in(self):
# A draft worker keeps its own warmup; a model without the hook opts out.
for draft, has_hook in ((True, True), (False, False)):
with self.subTest(draft=draft, has_hook=has_hook):
self.mr.is_draft_worker = draft
if not has_hook:
del self.mr.model.autotune_prefill_kernels
autotune.maybe_flashinfer_autotune_extend(
self.runner, decode_num_tokens=384
)
self.hook.assert_not_called()
self.flashinfer_autotune_context.assert_not_called()
@unittest.skipUnless(torch.cuda.is_available(), "FlashInfer requires CUDA")
class TestAutotuneCachePhases(CustomTestCase):
"""Loaded target tactics survive draft warmup, unless cache reuse is off."""
def test_target_and_draft_cache_reuse(self):
from flashinfer.autotuner import AutoTuner, _collect_metadata
tuner = AutoTuner.get()
tuner.clear_cache()
self.addCleanup(tuner.clear_cache)
runner = SimpleNamespace(
device="cuda",
forward_stream=torch.cuda.Stream(),
tp_group=SimpleNamespace(world_size=1),
)
with tempfile.TemporaryDirectory() as directory:
target, draft = (
Path(directory) / name for name in ("target.json", "draft.json")
)
for path, key, tactic in (
(target, "target_prefill", 7),
(draft, "draft_decode", 3),
):
path.write_text(
json.dumps(
{"_metadata": _collect_metadata(), key: ["TestRunner", tactic]}
)
)
with (
patch.object(
autotune,
"flashinfer_autotune_cache_path",
side_effect=[target, draft, draft],
),
patch.object(
autotune, "get_flashinfer_autotune_skip_ops", return_value=set()
),
autotune.envs.SGLANG_FLASHINFER_AUTOTUNE_CACHE.override(True),
):
with autotune.flashinfer_autotune_context(runner, run_lm_head=False):
self.assertEqual(
tuner._file_configs["target_prefill"], ("TestRunner", 7)
)
# No profiling: this models a restart that loads tactics from disk.
self.assertFalse(tuner.profiling_cache)
with autotune.flashinfer_autotune_context(runner, run_lm_head=False):
pass
saved = json.loads(draft.read_text())
self.assertEqual(saved["target_prefill"], ["TestRunner", 7])
self.assertEqual(saved["draft_decode"], ["TestRunner", 3])
with (
autotune.envs.SGLANG_FLASHINFER_AUTOTUNE_CACHE.override(False),
autotune.flashinfer_autotune_context(runner, run_lm_head=False),
):
self.assertNotIn("target_prefill", tuner._file_configs)
self.assertNotIn("draft_decode", tuner._file_configs)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -0,0 +1,61 @@
"""Weight-update entry points refuse a model carrying compensated-mHC derived
weight caches before any weight is written."""
import unittest
from types import SimpleNamespace
from unittest.mock import Mock, patch
import torch
from sglang.srt.model_executor.model_runner_components.weight_updater import (
WeightUpdater,
_unsupported_derived_weight_cache_error,
)
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
class TestCompensatedMhcUpdateGuard(CustomTestCase):
def test_all_update_entries_reject_before_writes(self):
for field in ("_hc_attn_tf32_parts", "_hc_ffn_tf32_parts"):
for method, args in (
("update_weights_from_tensor", ([], "direct")),
("update_weights_from_distributed", ([], [], [], "unused")),
("update_weights_from_disk", ("unused", "auto")),
("update_weights_from_ipc", (SimpleNamespace(),)),
):
with self.subTest(field=field, method=method, args=args):
model = torch.nn.Sequential(torch.nn.Linear(1, 1))
original = model[0].weight.detach().clone()
setattr(model[0], field, (torch.ones(1), torch.zeros(1)))
model.load_weights = Mock()
updater = SimpleNamespace(
get_model=lambda: model, _assert_weight_cache_inactive=Mock()
)
with patch(
"sglang.srt.model_executor.model_runner_components.weight_updater.default_weight_loader"
) as loader:
ok, message = getattr(WeightUpdater, method)(updater, *args)
self.assertFalse(ok)
self.assertIn("compensated mHC", message)
loader.assert_not_called()
model.load_weights.assert_not_called()
torch.testing.assert_close(
model[0].weight, original, rtol=0, atol=0
)
def test_models_without_derived_splits_keep_update_support(self):
model = torch.nn.Sequential(torch.nn.Linear(1, 1))
model[0]._hc_attn_tf32_parts = model[0]._hc_ffn_tf32_parts = None
with patch(
"sglang.kernels.ops.attention.dsv4.gemm.hpc_bf16xfp32_gemm_enabled",
return_value=False,
):
self.assertIsNone(_unsupported_derived_weight_cache_error(model))
self.assertIsNone(_unsupported_derived_weight_cache_error())
if __name__ == "__main__":
unittest.main()
@@ -1152,6 +1152,9 @@ class TestSWAPoolFloor(CustomTestCase):
cfg.c4_ring_size = 8 cfg.c4_ring_size = 8
cfg.c4_shrink_factor = 1 cfg.c4_shrink_factor = 1
cfg._unified = unified cfg._unified = unified
cfg.operator_swa_ratio = None
cfg.swa_cap_tokens = None
cfg.swa_prefix_tails = 0
return cfg._compute_dsv4_sizes(max_tokens, page_size) return cfg._compute_dsv4_sizes(max_tokens, page_size)
def test_dsv4_rejects_single_page_pool(self): def test_dsv4_rejects_single_page_pool(self):
@@ -1214,6 +1217,11 @@ class TestSWAPoolFloor(CustomTestCase):
cfg.disaggregation_mode = None cfg.disaggregation_mode = None
cfg.disaggregation_decode_extra_slots = 0 cfg.disaggregation_decode_extra_slots = 0
cfg._unified = True cfg._unified = True
cfg.operator_swa_ratio = None
cfg.swa_cap_tokens = None
cfg.swa_prefix_tails = 0
cfg.request_window_bytes = 0
cfg.bytes_per_swa_token = 0.0
cfg._unified_fp8 = False cfg._unified_fp8 = False
# object.__new__ skips __init__; bf16 unified row is 2B * latent # object.__new__ skips __init__; bf16 unified row is 2B * latent
cfg._unified_row_bytes = cfg.attn_head_dim * 2 cfg._unified_row_bytes = cfg.attn_head_dim * 2
@@ -1231,6 +1239,50 @@ class TestSWAPoolFloor(CustomTestCase):
+ cfg._get_c128_state_fixed_bytes(max_running_requests) + cfg._get_c128_state_fixed_bytes(max_running_requests)
) )
def test_dsv4_paged_dspark_budget_reserves_window_and_draft_layers(self):
from sglang.srt.model_executor.pool_configurator import DSV4PoolConfigurator
_publish_config(
self,
enable_encoder_swa_bounded_replay=True,
speculative_algorithm="DSPARK",
speculative_num_draft_tokens=6,
speculative_dspark_block_size=5,
page_size=256,
max_running_requests=2,
chunked_prefill_size=256,
)
cfg = SimpleNamespace(
qk_nope_head_dim=448,
qk_rope_head_dim=64,
index_head_dim=128,
context_len=131072,
compress_ratios=[0, 0] + [2] * 18 + [1] * 20,
window_size=128,
hf_config=SimpleNamespace(kv_source_layer_ids=[2, 8, 14, 20]),
)
spec = SimpleNamespace(is_dspark=lambda: True, is_none=lambda: False)
kvc = SimpleNamespace(
kv_cache_dtype_str="fp8_e4m3",
model_config=cfg,
layer_info=SimpleNamespace(start_layer=0, end_layer=40),
ps=SimpleNamespace(pp_size=1, attn_dp_size=1),
sliding_window_size=128,
page_size=256,
spec_algorithm=spec,
spec_aux_config=SimpleNamespace(dflash_draft_num_layers=3),
)
planner = DSV4PoolConfigurator(kvc)
self.assertEqual(planner.bytes_per_swa_token, 3 * 584)
budget = 256 * 1024 * 1024
sizes = planner.calculate_pool_sizes(budget, 256)
self.assertEqual(sizes.swa_max_total_num_tokens, planner.swa_cap_tokens)
self.assertLessEqual(
sizes.full_max_total_num_tokens * planner.bytes_per_full_token
+ planner._get_swa_fixed_bytes(),
budget,
)
def test_dsv4_unified_c4_state_not_token_scaled(self): def test_dsv4_unified_c4_state_not_token_scaled(self):
# Unified-KV sizes the c4 state ring from max_running_requests in # Unified-KV sizes the c4 state ring from max_running_requests in
# finalize_with_max_running_requests, so it must not scale here. # finalize_with_max_running_requests, so it must not scale here.
@@ -55,6 +55,7 @@ class TestDeepseekV4RoPEPolicy(CustomTestCase):
o_lora_rank=8, o_lora_rank=8,
rms_norm_eps=1e-6, rms_norm_eps=1e-6,
compress_ratios=[compress_ratio], compress_ratios=[compress_ratio],
q_head_norm=True,
rope_theta=10_000, rope_theta=10_000,
compress_rope_theta=160_000, compress_rope_theta=160_000,
max_position_embeddings=128, max_position_embeddings=128,

Some files were not shown because too many files have changed in this diff Show More