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:
co-authored by
BBuf
Claude Opus 5
Xiaoyu Zhang
Yuwei An
Khoa Pham
Yuhao Yang
Zhichen Zeng
Ke Bao
parent
1b200ffaaa
commit
a6cf05817f
@@ -32,7 +32,12 @@ from .moe import (
|
||||
silu_and_mul_contig_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
|
||||
|
||||
__all__ = [
|
||||
@@ -56,6 +61,7 @@ __all__ = [
|
||||
"triton_create_paged_compress_data",
|
||||
"topk_transform_paged",
|
||||
"topk_transform_paged_v2",
|
||||
"topk_transform_ragged_v2",
|
||||
"plan_topk_v2",
|
||||
"hash_topk",
|
||||
"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
|
||||
|
||||
INDEX_HEAD_DIM = 128
|
||||
# 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
|
||||
@@ -326,3 +329,138 @@ def index_k_rope_pack(
|
||||
num_warps=4,
|
||||
)
|
||||
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,
|
||||
swa_window: int,
|
||||
page_index_aligned_size: int,
|
||||
swa_replay_start: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
return build_causal_swa_page_indices(
|
||||
req_to_token=req_to_token,
|
||||
@@ -408,6 +409,7 @@ class BuildCausalSwaPageIndices:
|
||||
seq_lens_casual=seq_lens_casual,
|
||||
swa_window=swa_window,
|
||||
page_index_aligned_size=page_index_aligned_size,
|
||||
swa_replay_start=swa_replay_start,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -420,6 +422,7 @@ class BuildCausalSwaPageIndices:
|
||||
seq_lens_casual: torch.Tensor,
|
||||
swa_window: int,
|
||||
page_index_aligned_size: int,
|
||||
swa_replay_start: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
return build_causal_swa_page_indices_triton(
|
||||
req_to_token=req_to_token,
|
||||
@@ -428,9 +431,41 @@ class BuildCausalSwaPageIndices:
|
||||
seq_lens_casual=seq_lens_casual,
|
||||
swa_window=swa_window,
|
||||
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(
|
||||
*,
|
||||
req_to_token: torch.Tensor,
|
||||
@@ -439,14 +474,20 @@ def build_causal_swa_page_indices(
|
||||
seq_lens_casual: torch.Tensor,
|
||||
swa_window: int,
|
||||
page_index_aligned_size: int,
|
||||
swa_replay_start: Optional[torch.Tensor] = None,
|
||||
) -> 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
|
||||
pos_causal = seq_lens_casual - 1
|
||||
num_qo_tokens = seq_lens_casual.size(0)
|
||||
offsets = pos_causal.unsqueeze(1) - torch.arange(
|
||||
swa_window, dtype=torch.int32, device=device
|
||||
).unsqueeze(0)
|
||||
invalid_offset_mask = offsets < 0
|
||||
if swa_replay_start is None:
|
||||
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)
|
||||
raw_indices = req_to_token[req_pool_indices_repeated[:, None], offsets]
|
||||
assert raw_indices.shape == (num_qo_tokens, swa_window)
|
||||
@@ -470,10 +511,12 @@ def _causal_swa_page_indices_kernel(
|
||||
full_to_swa_ptr,
|
||||
req_pool_ptr,
|
||||
seq_lens_ptr,
|
||||
swa_replay_start_ptr,
|
||||
out_ptr,
|
||||
rt_stride,
|
||||
swa_window,
|
||||
padded_width,
|
||||
HAS_SWA_REPLAY_START: tl.constexpr,
|
||||
BLOCK_K: tl.constexpr,
|
||||
):
|
||||
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)
|
||||
base = req_to_token_ptr + rp * rt_stride
|
||||
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):
|
||||
k = k0 + tl.arange(0, BLOCK_K)
|
||||
kmask = k < padded_width
|
||||
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(
|
||||
tl.int64
|
||||
)
|
||||
@@ -502,6 +549,7 @@ def build_causal_swa_page_indices_triton(
|
||||
seq_lens_casual: torch.Tensor,
|
||||
swa_window: int,
|
||||
page_index_aligned_size: int,
|
||||
swa_replay_start: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
num_qo_tokens = seq_lens_casual.size(0)
|
||||
padded_width = (
|
||||
@@ -513,15 +561,19 @@ def build_causal_swa_page_indices_triton(
|
||||
device=seq_lens_casual.device,
|
||||
)
|
||||
BLOCK_K = 256
|
||||
has_swa_replay_start = swa_replay_start is not None
|
||||
_causal_swa_page_indices_kernel[(num_qo_tokens,)](
|
||||
req_to_token,
|
||||
full_to_swa_mapping,
|
||||
req_pool_indices_repeated,
|
||||
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,
|
||||
req_to_token.stride(0),
|
||||
swa_window,
|
||||
padded_width,
|
||||
HAS_SWA_REPLAY_START=has_swa_replay_start,
|
||||
BLOCK_K=BLOCK_K,
|
||||
)
|
||||
return out
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import triton
|
||||
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.kernel_api_logging import debug_kernel_api
|
||||
@@ -90,8 +91,12 @@ def moe_fused_gate_jit(
|
||||
def _router_triton_kernel(
|
||||
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_alt_ptr,
|
||||
input_ids_ptr,
|
||||
num_token_non_padded_ptr,
|
||||
out_weights_ptr, # [M, K] fp32
|
||||
out_indices_ptr, # [M, K] int32
|
||||
out_packed_ptr, # [M, K] int32 (HAS_PACKED)
|
||||
M,
|
||||
routed_scaling_factor,
|
||||
moe_softcapping,
|
||||
@@ -106,17 +111,28 @@ def _router_triton_kernel(
|
||||
EXPERTS_PER_GROUP: tl.constexpr, # N // N_GROUP
|
||||
BLOCK_G: tl.constexpr, # >= N_GROUP, power of 2
|
||||
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)
|
||||
RENORMALIZE: tl.constexpr,
|
||||
APPLY_SCALE: tl.constexpr, # apply_routed_scaling_factor_on_output
|
||||
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,
|
||||
stride_bias,
|
||||
stride_bias_alt,
|
||||
stride_input_ids,
|
||||
stride_sm,
|
||||
stride_sn,
|
||||
stride_wm,
|
||||
stride_wk,
|
||||
stride_im,
|
||||
stride_ik,
|
||||
stride_pm,
|
||||
stride_pk,
|
||||
) -> None:
|
||||
# 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
|
||||
@@ -136,12 +152,30 @@ def _router_triton_kernel(
|
||||
# Plain softmax routing has no bias, so keep the zero value in registers
|
||||
# rather than materializing and clearing a device tensor per call.
|
||||
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:
|
||||
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
|
||||
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(
|
||||
tl.float32
|
||||
) # [BLOCK_M, BLOCK_N]
|
||||
@@ -149,17 +183,21 @@ def _router_triton_kernel(
|
||||
if SCORING_FUNC == 0:
|
||||
# sigmoid(x) = 1 / (1 + exp(-x)); bias is for ranking only, weight is bias-free.
|
||||
activated = tl.sigmoid(scores)
|
||||
biased = activated + bias[None, :]
|
||||
biased = activated + row_bias
|
||||
elif SCORING_FUNC == 1:
|
||||
# sqrt(softplus(x)). log(1.0 + exp(x)) rounds to 0 below -16.64 and overflows
|
||||
# above 88.7; Triton has no log1p, so recover it from log via z*log(u)/(u-1).
|
||||
z = tl.exp(-tl.abs(scores))
|
||||
u = 1.0 + z
|
||||
exact = 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
|
||||
activated = tl.sqrt(sp)
|
||||
biased = activated + bias[None, :]
|
||||
if SQRTSOFTPLUS_LOG1P:
|
||||
# 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))
|
||||
u = 1.0 + z
|
||||
exact = 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
|
||||
activated = tl.sqrt(sp)
|
||||
biased = activated + row_bias
|
||||
else:
|
||||
# softmax over the row: weight is the softmax probability (bias kept), with
|
||||
# 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).
|
||||
z = logit / moe_softcapping
|
||||
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"))
|
||||
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)
|
||||
@@ -178,8 +216,11 @@ def _router_triton_kernel(
|
||||
|
||||
biased = tl.where(mask_n[None, :], biased, -float("inf")) # [BLOCK_M, BLOCK_N]
|
||||
|
||||
# Map NaN -> a finite floor
|
||||
biased = tl.where(biased == biased, biased, -1e30) # [BLOCK_M, BLOCK_N]
|
||||
if SCORING_FUNC == 1 and SQRTSOFTPLUS_LOG1P:
|
||||
# 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
|
||||
# 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)
|
||||
|
||||
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):
|
||||
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
|
||||
win_lane = tl.min(lane_id, axis=1)[:, None].to(tl.int32) # [BLOCK_M, 1]
|
||||
win_activated = tl.sum(
|
||||
@@ -225,7 +267,8 @@ def _router_triton_kernel(
|
||||
slot = offs_k[None, :] == k # [1, BLOCK_K]
|
||||
selected_vals = tl.where(slot, win_activated, selected_vals)
|
||||
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)[
|
||||
:, None
|
||||
@@ -244,10 +287,16 @@ def _router_triton_kernel(
|
||||
tl.extra.cuda.gdc_launch_dependents()
|
||||
|
||||
if RENORMALIZE:
|
||||
norm = tl.where(routed_sum > 0.0, routed_sum, 1.0) # [BLOCK_M, 1]
|
||||
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]
|
||||
selected_vals = selected_vals / norm
|
||||
if APPLY_SCALE:
|
||||
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_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, :]
|
||||
tl.store(out_w_ptr, selected_vals, 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
|
||||
@@ -273,14 +341,29 @@ def moe_fused_gate(
|
||||
moe_softcapping: float = 0.0,
|
||||
num_expert_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]:
|
||||
"""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
|
||||
(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
|
||||
the existing call sites.
|
||||
within). ``scores`` contains raw GEMM logits.
|
||||
|
||||
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())
|
||||
assert scoring_func_int is not None, (
|
||||
@@ -310,6 +393,15 @@ def moe_fused_gate(
|
||||
"scores and bias must have same num_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:
|
||||
routed_scaling_factor = 1.0
|
||||
|
||||
@@ -325,6 +417,11 @@ def moe_fused_gate(
|
||||
and num_fused_shared_experts == 0
|
||||
and num_expert_group <= 1
|
||||
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 = (
|
||||
scores,
|
||||
@@ -348,6 +445,8 @@ def moe_fused_gate(
|
||||
|
||||
weights = torch.empty((M, K), dtype=torch.float32, 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_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),)
|
||||
use_pdl = is_arch_support_pdl()
|
||||
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](
|
||||
scores,
|
||||
bias if bias is not None else scores,
|
||||
bias_alt,
|
||||
input_ids,
|
||||
num_token_non_padded,
|
||||
weights,
|
||||
indices,
|
||||
packed_out if packed_out is not None else _unused_i32,
|
||||
M,
|
||||
float(routed_scaling_factor),
|
||||
float(moe_softcapping),
|
||||
@@ -382,17 +488,28 @@ def moe_fused_gate(
|
||||
EXPERTS_PER_GROUP=experts_per_group,
|
||||
BLOCK_G=BLOCK_G,
|
||||
SCORING_FUNC=scoring_func_int,
|
||||
SQRTSOFTPLUS_LOG1P=bool(sqrtsoftplus_log1p),
|
||||
HAS_SOFTCAP=bool(moe_softcapping != 0.0),
|
||||
RENORMALIZE=bool(renormalize),
|
||||
APPLY_SCALE=bool(apply_routed_scaling_factor_on_output),
|
||||
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,
|
||||
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_sn=scores.stride(1),
|
||||
stride_wm=weights.stride(0),
|
||||
stride_wk=weights.stride(1),
|
||||
stride_im=indices.stride(0),
|
||||
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,
|
||||
**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,
|
||||
verify_num_draft_tokens: int,
|
||||
cutoff_verify_lens: Optional[torch.Tensor] = None,
|
||||
fused_argmax: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
return accept_greedy(
|
||||
candidates=candidates,
|
||||
target_logits=target_logits,
|
||||
verify_num_draft_tokens=verify_num_draft_tokens,
|
||||
cutoff_verify_lens=cutoff_verify_lens,
|
||||
fused_argmax=fused_argmax,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -600,12 +602,14 @@ class AcceptGreedy:
|
||||
target_logits: torch.Tensor,
|
||||
verify_num_draft_tokens: int,
|
||||
cutoff_verify_lens: Optional[torch.Tensor] = None,
|
||||
fused_argmax: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
return accept_greedy_triton(
|
||||
candidates=candidates,
|
||||
target_logits=target_logits,
|
||||
verify_num_draft_tokens=verify_num_draft_tokens,
|
||||
cutoff_verify_lens=cutoff_verify_lens,
|
||||
fused_argmax=fused_argmax,
|
||||
)
|
||||
|
||||
|
||||
@@ -615,9 +619,10 @@ def accept_greedy(
|
||||
target_logits: torch.Tensor,
|
||||
verify_num_draft_tokens: int,
|
||||
cutoff_verify_lens: Optional[torch.Tensor] = None,
|
||||
fused_argmax: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
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
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
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(
|
||||
*,
|
||||
candidates: torch.Tensor,
|
||||
target_logits: torch.Tensor,
|
||||
verify_num_draft_tokens: int,
|
||||
cutoff_verify_lens: Optional[torch.Tensor] = None,
|
||||
fused_argmax: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
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
|
||||
)
|
||||
correct_len, bonus = compute_dflash_correct_drafts_and_bonus(
|
||||
|
||||
@@ -384,9 +384,14 @@ class CommitKvProj:
|
||||
*,
|
||||
main_x: torch.Tensor,
|
||||
wkv_linears: list[torch.nn.Module],
|
||||
allow_strided_output: bool = False,
|
||||
) -> list[torch.Tensor]:
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
@@ -404,8 +409,13 @@ class CommitKvProj:
|
||||
*,
|
||||
main_x: torch.Tensor,
|
||||
wkv_linears: list[torch.nn.Module],
|
||||
allow_strided_output: bool = False,
|
||||
) -> 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(
|
||||
@@ -420,11 +430,20 @@ def commit_kv_proj_fused(
|
||||
*,
|
||||
main_x: torch.Tensor,
|
||||
wkv_linears: list[torch.nn.Module],
|
||||
allow_strided_output: bool = False,
|
||||
) -> list[torch.Tensor]:
|
||||
num_stages = len(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
|
||||
kv_all = quant_method.w8a8_block_fp8_linear(
|
||||
input=main_x,
|
||||
@@ -438,15 +457,14 @@ def commit_kv_proj_fused(
|
||||
kv_all = torch.nn.functional.linear(main_x, stacked.weight)
|
||||
|
||||
head_dim = kv_all.shape[-1] // num_stages
|
||||
return [
|
||||
kv_all[:, i * head_dim : (i + 1) * head_dim].contiguous()
|
||||
for i in range(num_stages)
|
||||
]
|
||||
slices = list(kv_all.split(head_dim, dim=-1))
|
||||
return slices if allow_strided_output else [kv.contiguous() for kv in slices]
|
||||
|
||||
|
||||
class _StackedWkvWeight(msgspec.Struct):
|
||||
weight: torch.Tensor
|
||||
fp8_scale: Optional[torch.Tensor]
|
||||
mxfp8_scale: Optional[torch.Tensor] = None
|
||||
|
||||
|
||||
def _stacked_wkv_weight(*, wkv_linears: list[torch.nn.Module]) -> _StackedWkvWeight:
|
||||
@@ -500,6 +518,21 @@ def _build_stacked_wkv_weight(
|
||||
) -> _StackedWkvWeight:
|
||||
if _block_quant_stack_applies(wkv_linears=wkv_linears):
|
||||
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:
|
||||
from sglang.srt.layers.quantization.fp8_utils import (
|
||||
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)
|
||||
if cfg.cuda_graph_max_bs_prefill is not None:
|
||||
_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:
|
||||
_set(Phase.DECODE, "bs", cfg.cuda_graph_bs_decode)
|
||||
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 (
|
||||
_deepseek_v4_kv_cache_dtype,
|
||||
declare_resolution,
|
||||
model_config_of,
|
||||
resolving_view,
|
||||
run_post_process_pass,
|
||||
)
|
||||
@@ -244,14 +245,139 @@ def validate_deepseek_v4_cp(server_args: ServerArgs) -> None:
|
||||
f"DeepSeekV4 CP supports moe_a2a_backend in {supported_a2a_backends}, "
|
||||
f"got {cfg.moe_a2a_backend!r}."
|
||||
)
|
||||
logger.warning(
|
||||
"Disabling SGLANG_OPT_FLASHMLA_SPARSE_PREFILL because DeepSeekV4 "
|
||||
"context parallelism is enabled."
|
||||
)
|
||||
envs.SGLANG_OPT_FLASHMLA_SPARSE_PREFILL.set(False)
|
||||
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(
|
||||
"Disabling SGLANG_OPT_FLASHMLA_SPARSE_PREFILL because DeepSeekV4 "
|
||||
"context parallelism is enabled."
|
||||
)
|
||||
envs.SGLANG_OPT_FLASHMLA_SPARSE_PREFILL.set(False)
|
||||
logger.warning(
|
||||
f"Enable Context Parallel for DeepSeekV4, "
|
||||
f"strategy={cfg.cp_strategy}, "
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
"Enable returning indexer topk indices of layers with indexer with responses.",
|
||||
] = 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[
|
||||
int,
|
||||
"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[
|
||||
Optional[int], "Maximum batch size captured for the prefill cuda graph."
|
||||
] = 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[
|
||||
Optional[List[int]],
|
||||
"Explicit list of batch sizes to capture for the decode cuda graph.",
|
||||
|
||||
@@ -163,6 +163,23 @@ class Schedule(msgspec.Struct):
|
||||
fallback=0.8,
|
||||
),
|
||||
] = 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[
|
||||
bool, Arg(help="Disable the hybrid SWA memory pool.", resolvable=True)
|
||||
] = False
|
||||
|
||||
@@ -203,6 +203,13 @@ def handle_cache_compatibility(server_args: Any) -> None:
|
||||
"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
|
||||
# (e.g. Step3p forces 1.0 under hierarchical cache) that supersedes
|
||||
# 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.
|
||||
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].")
|
||||
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:
|
||||
|
||||
@@ -416,8 +416,11 @@ def handle_model_specific_adjustments(server_args: Any):
|
||||
from sglang.srt.arg_groups.deepseek_v4_hook import (
|
||||
validate_deepseek_v4_cp,
|
||||
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_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.runtime_context import get_platform
|
||||
from sglang.srt.utils import is_flashinfer_available
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@_register_for("DeepseekV4ForCausalLM")
|
||||
def _deepseek_v4_overrides(server_args: Any, hf_config: Any) -> dict:
|
||||
"""DeepSeek V4 attention/page/window/MoE-runner defaults (from
|
||||
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."""
|
||||
"""Attention, page and MoE defaults; the rest lives in deepseek_v4_hook."""
|
||||
cfg = resolving_view(server_args)
|
||||
|
||||
model_arch = hf_config.architectures[0]
|
||||
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
|
||||
if cfg.device == "npu":
|
||||
# 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}."
|
||||
)
|
||||
|
||||
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":
|
||||
model_config = model_config_of(server_args)
|
||||
# nvidia/DeepSeek-V4-Pro-NVFP4 uses the routed TRT-LLM runner.
|
||||
|
||||
@@ -1014,7 +1014,17 @@ def _flashinfer_allreduce_fusion_auto_enable(view: Any) -> dict:
|
||||
single-node systems. Reads the mid-resolution enable_dp_attention /
|
||||
moe_a2a_backend (after the DeepSeek CP and a2a declarations), exactly
|
||||
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 {
|
||||
"Qwen3_5MoeForCausalLM",
|
||||
"Qwen3_5MoeForConditionalGeneration",
|
||||
@@ -1032,6 +1042,7 @@ def _flashinfer_allreduce_fusion_auto_enable(view: Any) -> dict:
|
||||
if (
|
||||
view.flashinfer_allreduce_fusion_backend is None
|
||||
and model_arch in _FLASHINFER_ALLREDUCE_FUSION_ARCHS
|
||||
and not prefer_custom_dsv41
|
||||
and (get_platform().is_sm90 or get_platform().is_sm100)
|
||||
and view.tp_size > 1
|
||||
and not view.enable_dp_attention
|
||||
|
||||
@@ -810,6 +810,7 @@ class TboForwardBatchPreparer:
|
||||
# The child runs the same forward, so it keeps the parent's
|
||||
# sharding verdict; its counts above are already per-child.
|
||||
attn_tp_sequence_sharded=batch.attn_tp_sequence_sharded,
|
||||
encoder_swa_replay=batch.encoder_swa_replay,
|
||||
tbo_split_seq_index=None,
|
||||
tbo_parent_token_range=(start_token_index, end_token_index),
|
||||
tbo_children=None,
|
||||
|
||||
@@ -103,8 +103,36 @@ class DeepSeekV4Config(PretrainedConfig):
|
||||
|
||||
compress_rope_theta: int = 40000
|
||||
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
|
||||
hc_mult: int = 4
|
||||
hc_pre_from_prev_sublayer: bool = False
|
||||
q_head_norm: bool = True
|
||||
hc_sinkhorn_iters: int = 20
|
||||
hc_eps: float = 1e-6
|
||||
|
||||
@@ -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 (
|
||||
DisaggregationMode,
|
||||
filter_kv_indices_for_cp_rank,
|
||||
get_dsv41_spec_layout,
|
||||
)
|
||||
from sglang.srt.distributed import get_pp_group, get_world_group
|
||||
from sglang.srt.environ import envs
|
||||
@@ -102,6 +103,7 @@ class PrefillServerInfo:
|
||||
kv_cache_dtype: Optional[str]
|
||||
follow_bootstrap_room: bool
|
||||
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
|
||||
# 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_carries_reason: bool = False
|
||||
|
||||
dsv41_spec_layout: Optional[dict] = None
|
||||
|
||||
# Used by decode when the prefill reported Failed without a reason frame.
|
||||
DEFAULT_PREFILL_FAILURE_REASON = (
|
||||
"Failed to get kvcache from prefill instance, it might be dead"
|
||||
@@ -166,6 +170,7 @@ class CommonKVManager(BaseKVManager):
|
||||
):
|
||||
self.kv_args = args
|
||||
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.state_item_lens_sum = sum(x for comp in args.state_item_lens for x in comp)
|
||||
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."
|
||||
)
|
||||
|
||||
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 not (self.is_mla_backend or self.is_hybrid_mla_backend):
|
||||
raise RuntimeError(
|
||||
@@ -1079,6 +1105,7 @@ class CommonKVManager(BaseKVManager):
|
||||
"rank_port": self.rank_port,
|
||||
"page_size": self.kv_args.page_size,
|
||||
"kv_cache_dtype": self.kv_cache_dtype_str,
|
||||
"dsv41_spec_layout": self.dsv41_spec_layout,
|
||||
"load_balance_method": get_parallel().load_balance_method,
|
||||
"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
|
||||
@@ -1969,6 +1996,7 @@ class CommonKVBootstrapServer(BaseKVBootstrapServer):
|
||||
self.dp_size = None
|
||||
self.page_size = None
|
||||
self.kv_cache_dtype: Optional[str] = None
|
||||
self.dsv41_spec_layout: Optional[dict] = None
|
||||
self.follow_bootstrap_room: Optional[bool] = None
|
||||
self.enable_dsa_cache_layer_split: Optional[bool] = None
|
||||
self.prefill_http_port: Optional[int] = None
|
||||
@@ -2039,6 +2067,14 @@ class CommonKVBootstrapServer(BaseKVBootstrapServer):
|
||||
page_size = int(data["page_size"])
|
||||
kv_cache_dtype = data["kv_cache_dtype"]
|
||||
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:
|
||||
self.attn_tp_size = attn_tp_size
|
||||
@@ -2130,6 +2166,7 @@ class CommonKVBootstrapServer(BaseKVBootstrapServer):
|
||||
pp_size=self.pp_size,
|
||||
page_size=self.page_size,
|
||||
kv_cache_dtype=self.kv_cache_dtype,
|
||||
dsv41_spec_layout=self.dsv41_spec_layout,
|
||||
follow_bootstrap_room=(
|
||||
self.follow_bootstrap_room
|
||||
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),
|
||||
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():
|
||||
return web.Response(
|
||||
|
||||
@@ -87,6 +87,7 @@ from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
EvictParams,
|
||||
)
|
||||
from sglang.srt.mem_cache.common import (
|
||||
dsv41_dspark_needs_rebootstrap,
|
||||
kv_to_page_indices,
|
||||
page_align_floor,
|
||||
release_kv_cache,
|
||||
@@ -674,6 +675,16 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
|
||||
if not is_retracted and not is_rebootstrap and is_unadmitted_reject(req):
|
||||
self.scheduler.retire_unadmitted_request(req)
|
||||
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):
|
||||
return
|
||||
|
||||
@@ -2837,6 +2848,10 @@ class SchedulerDisaggregationDecodeMixin:
|
||||
# A finished request can still have one redundant forward in flight.
|
||||
# Drain it before a prebuilt request seeds a potentially reused row.
|
||||
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)
|
||||
|
||||
return new_batch
|
||||
|
||||
@@ -24,6 +24,7 @@ from sglang.srt.disaggregation.base import KVPoll
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.runtime_context import (
|
||||
get_disagg,
|
||||
get_spec,
|
||||
)
|
||||
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):
|
||||
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))
|
||||
and not get_disagg().language_only
|
||||
and not get_disagg().language_model_only
|
||||
and not get_exec().features.enable_encoder_swa_bounded_replay
|
||||
and not is_mps()
|
||||
)
|
||||
if model_info["is_generation"]:
|
||||
|
||||
@@ -549,6 +549,11 @@ class Envs:
|
||||
SGLANG_DSPARK_OPT_MARKOV_W2_BF16 = EnvBool(True)
|
||||
SGLANG_DSPARK_OPT_MARKOV_W2_TP_SHARD = EnvBool(True)
|
||||
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_CONFIDENCE_RELAY_LAG_STEPS = EnvInt(2)
|
||||
|
||||
@@ -1479,6 +1484,13 @@ class Envs:
|
||||
# Quantize the SWA fp8 KV cache from bf16-rounded values (matches
|
||||
# trainer-side QAT and the DSA-CP path) instead of fp32 registers.
|
||||
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
|
||||
# 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
|
||||
@@ -1510,6 +1522,9 @@ class Envs:
|
||||
SGLANG_OPT_USE_ONLINE_COMPRESS = EnvBool(False)
|
||||
SGLANG_EXPERIMENTAL_ONLINE_C128_MTP = EnvBool(False)
|
||||
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_OPT_FLASHMLA_SPARSE_PREFILL = EnvBool(True)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import List, Optional, Tuple
|
||||
import torch
|
||||
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.hardware_backend.npu.utils import is_npu_arch35
|
||||
from sglang.srt.mem_cache.deepseek_v4_compress_state import CompressStatePool
|
||||
@@ -291,6 +292,7 @@ class DSV4NPUTokenToKVPool(DeepSeekV4TokenToKVPool):
|
||||
enable_memory_saver: bool,
|
||||
global_page_size: int,
|
||||
cls: type = DeepSeekV4SingleKVPool,
|
||||
kv_layout: KVLayout = KVLayout.V4,
|
||||
) -> NPUDeepSeekV4SingleKVPool:
|
||||
# NPU does not use the HiSparse c4 device pool; fail loud if someone
|
||||
# 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 "
|
||||
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,
|
||||
# and C128 has an independent physical 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]]:
|
||||
"""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)
|
||||
buffers = (
|
||||
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_norm_rope_store,
|
||||
)
|
||||
from sglang.kernels.ops.attention.dsv4.kv_layout import KVLayout
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -158,6 +159,7 @@ class CompressorBackendMixin:
|
||||
bf16_store: bool = False,
|
||||
kv_scale_cache: Optional[torch.Tensor] = None,
|
||||
rope_cache: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
|
||||
kv_layout: KVLayout = KVLayout.V4,
|
||||
fp8_2buff: bool = False,
|
||||
kv_cache_rope: Optional[torch.Tensor] = None,
|
||||
) -> None:
|
||||
@@ -215,6 +217,7 @@ class CompressorBackendMixin:
|
||||
bf16_store=bf16_store,
|
||||
kvcache_scale=kv_scale_cache,
|
||||
rope_cache=rope_cache,
|
||||
layout=kv_layout,
|
||||
# Derived once per forward by the backend; every C4 layer writes the
|
||||
# same rows to the same slots.
|
||||
fp4_k_write_metadata=(
|
||||
@@ -268,6 +271,7 @@ class CompressorBackendMixin:
|
||||
)
|
||||
use_hip_fp4 = _is_hip and use_fp4_indexer
|
||||
bf16_store = False
|
||||
kv_layout = KVLayout.V4
|
||||
kv_scale_cache = None
|
||||
fp8_2buff = False
|
||||
kv_cache_rope = None
|
||||
@@ -295,6 +299,8 @@ class CompressorBackendMixin:
|
||||
assert compress_kv_pool is not None
|
||||
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)
|
||||
# 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"):
|
||||
out_loc = compress_kv_pool._translate_loc_to_hisparse_device(out_loc)
|
||||
self._forward_compress_all_in_one(
|
||||
@@ -316,6 +322,7 @@ class CompressorBackendMixin:
|
||||
rope_cache=(
|
||||
(compressor.fp4_cos, compressor.fp4_sin) if use_hip_fp4 else None
|
||||
),
|
||||
kv_layout=kv_layout,
|
||||
fp8_2buff=fp8_2buff,
|
||||
kv_cache_rope=(
|
||||
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
|
||||
force_deep_gemm_metadata: 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)
|
||||
topk_metadata: torch.Tensor = field(init=False, repr=False)
|
||||
nonpaged_plan: Optional[NonPagedIndexerPlan] = field(
|
||||
@@ -144,7 +149,18 @@ class PagedIndexerMetadata:
|
||||
compressed_seq_lens = self.compressed_seq_lens.to(torch.int32)
|
||||
if compressed_seq_lens.dim() == 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.
|
||||
self.deep_gemm_metadata = [
|
||||
get_paged_mqa_logits_metadata(
|
||||
@@ -173,6 +189,9 @@ class PagedIndexerMetadata:
|
||||
self.topk_metadata = torch.empty((0,))
|
||||
|
||||
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
|
||||
def max_seq_len(self) -> int:
|
||||
@@ -182,6 +201,17 @@ class PagedIndexerMetadata:
|
||||
def max_compressed_seq_len(self) -> int:
|
||||
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):
|
||||
if is_hip():
|
||||
copy_fields = ["page_table", "compressed_seq_lens"]
|
||||
@@ -196,6 +226,8 @@ class PagedIndexerMetadata:
|
||||
check_eq_fields=[
|
||||
"page_size",
|
||||
"compressed_page_size",
|
||||
"compress_ratio",
|
||||
"row_chunk",
|
||||
"force_deep_gemm_metadata",
|
||||
"use_prefill_cuda_graph",
|
||||
"use_topk_v2",
|
||||
|
||||
@@ -57,3 +57,90 @@ def create_attention_graph_variants(hf_config) -> Optional[AttentionGraphVariant
|
||||
)
|
||||
return DsaGraphVariants(index_topk)
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -55,6 +55,9 @@ class InterleaveContextParallelMetadata(BaseContextParallelMetadata):
|
||||
per_rank_actual_token: Optional[List[int]] = None
|
||||
max_rank_len: 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):
|
||||
@@ -213,6 +216,9 @@ class InterleaveCPStrategy(ContextParallelStrategy):
|
||||
gathered = x.new_empty((self.cp_size * physical_rank_len, *x.shape[1:]))
|
||||
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
|
||||
# token order; cheaper than the index_select fallback below.
|
||||
actual = metadata.per_rank_actual_token
|
||||
@@ -290,3 +296,15 @@ class InterleaveCPStrategy(ContextParallelStrategy):
|
||||
k_nope = full_latent[..., :kv_lora_rank].unsqueeze(1)
|
||||
k_rope = full_latent[..., kv_lora_rank:].unsqueeze(1)
|
||||
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
|
||||
# (same ordering rule as apply_flashinfer_allreduce_fusion).
|
||||
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():
|
||||
# Don't call into the flashinfer workspace object while tracing. The
|
||||
# workspace was allocated for (max_token_num, hidden_dim, dtype) and
|
||||
|
||||
@@ -209,6 +209,9 @@ class LogitsProcessorOutput:
|
||||
# The last hidden layers
|
||||
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
|
||||
# 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
|
||||
|
||||
@@ -1505,7 +1505,7 @@ class FusedMoE(torch.nn.Module):
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
topk_output: TopKOutput,
|
||||
pre_quant_input: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
||||
pre_quant_input: Optional[Tuple] = None,
|
||||
):
|
||||
if self._use_ascend_fuseep:
|
||||
from sglang.srt.hardware_backend.npu.moe.fuseep import forward_fuseep
|
||||
@@ -1546,7 +1546,7 @@ class FusedMoE(torch.nn.Module):
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
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]
|
||||
assert self.quant_method is not None
|
||||
@@ -1555,21 +1555,9 @@ class FusedMoE(torch.nn.Module):
|
||||
dwdp_mgr = get_global_dwdp_manager()
|
||||
dwdp_mgr.wait_prefetch(self.layer_id)
|
||||
|
||||
dispatch_output = self.dispatcher.dispatch(
|
||||
hidden_states=hidden_states, topk_output=topk_output
|
||||
dispatch_output = self._dispatch_with_pre_quant(
|
||||
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(
|
||||
dispatch_output=dispatch_output,
|
||||
@@ -1593,16 +1581,40 @@ class FusedMoE(torch.nn.Module):
|
||||
|
||||
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(
|
||||
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
|
||||
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
|
||||
flashinfer_trtllm_deferred_finalize_context,
|
||||
)
|
||||
|
||||
dispatch_output = self.dispatcher.dispatch(
|
||||
hidden_states=hidden_states, topk_output=topk_output
|
||||
dispatch_output = self._dispatch_with_pre_quant(
|
||||
hidden_states, topk_output, pre_quant_input
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def is_deferred_finalize_enabled() -> bool:
|
||||
return _deferred_finalize_enabled.get()
|
||||
|
||||
|
||||
def finalize_flashinfer_trtllm_deferred_output(
|
||||
deferred_output: FlashInferTrtllmDeferredFinalizeOutput,
|
||||
shared_output: torch.Tensor,
|
||||
|
||||
@@ -68,11 +68,10 @@ class StandardDispatchOutput(NamedTuple):
|
||||
hidden_states: torch.Tensor
|
||||
hidden_states_scale: Optional[torch.Tensor]
|
||||
topk_output: TopKOutput
|
||||
# SGLANG_OPT_MOE_QUANT_ONCE: optional pre-quantized (q, scale) pair for
|
||||
# ``hidden_states`` (per-token-group-128 fp8, q rows possibly padded to a
|
||||
# multiple of 4). Consumed by the standard->triton fused runner so it can
|
||||
# skip its own activation quant; ``hidden_states`` itself stays bf16.
|
||||
hidden_states_pre_quant: Optional[Tuple[torch.Tensor, torch.Tensor]] = None
|
||||
# Pre-quantized activation for ``hidden_states``, which itself stays bf16:
|
||||
# either a (q, scale) pair (per-token-group-128 fp8, q rows padded to a
|
||||
# multiple of 4) or an ``Mxfp8RoutedInputPreQuant``.
|
||||
hidden_states_pre_quant: Optional[Tuple] = None
|
||||
|
||||
@property
|
||||
def format(self) -> DispatchOutputFormat:
|
||||
|
||||
@@ -26,6 +26,7 @@ from typing import (
|
||||
Protocol,
|
||||
Tuple,
|
||||
TypeGuard,
|
||||
Union,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
@@ -235,6 +236,10 @@ class TopKConfig:
|
||||
fused_shared_experts_scaling_factor: Optional[float] = None
|
||||
output_format: Optional[TopKOutputFormat] = None
|
||||
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
|
||||
# process-global routed-experts capture buffer.
|
||||
allow_routed_experts_capture: bool = True
|
||||
@@ -271,15 +276,10 @@ class TopKConfig:
|
||||
|
||||
class TopKOutputChecker:
|
||||
@staticmethod
|
||||
def format_is_standard(topk_output: TopKOutput) -> TypeGuard[StandardTopKOutput]:
|
||||
# ===== TO BE REFACTORED ====
|
||||
# The experimental fused topk+pack carrier only exists under the master switch.
|
||||
if _SGLANG_EXPERIMENTAL_LORA_OPTI:
|
||||
return isinstance(
|
||||
topk_output, (StandardTopKOutput, StandardTopKOutputPacked)
|
||||
)
|
||||
# ===== END TO BE REFACTORED ====
|
||||
return isinstance(topk_output, StandardTopKOutput)
|
||||
def format_is_standard(
|
||||
topk_output: TopKOutput,
|
||||
) -> TypeGuard[Union[StandardTopKOutput, StandardTopKOutputPacked]]:
|
||||
return isinstance(topk_output, (StandardTopKOutput, StandardTopKOutputPacked))
|
||||
|
||||
@staticmethod
|
||||
def format_is_triton_kernels(
|
||||
@@ -325,11 +325,8 @@ class StandardTopKOutput(NamedTuple):
|
||||
return TopKOutputFormat.STANDARD
|
||||
|
||||
|
||||
# ===== TO BE REFACTORED ====
|
||||
# Experimental fused topk+pack (SGLANG_OPT_LORA_FUSED_TOPK_PACK) carrier: the FlashInfer
|
||||
# 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).
|
||||
# Standard top-k output plus the FlashInfer routed-MoE packed ids that
|
||||
# ``moe_fused_gate`` writes; a separate type keeps the 3-tuple unpack valid.
|
||||
class StandardTopKOutputPacked(NamedTuple):
|
||||
topk_weights: torch.Tensor
|
||||
topk_ids: torch.Tensor
|
||||
@@ -341,9 +338,6 @@ class StandardTopKOutputPacked(NamedTuple):
|
||||
return TopKOutputFormat.STANDARD
|
||||
|
||||
|
||||
# ===== END TO BE REFACTORED ====
|
||||
|
||||
|
||||
class TritonKernelTopKOutput(NamedTuple):
|
||||
"""Triton kernel top-k output format."""
|
||||
|
||||
@@ -546,6 +540,8 @@ class TopK(BaseFusedOp):
|
||||
fused_shared_experts_scaling_factor: Optional[float] = None,
|
||||
is_fp4_experts: bool = False,
|
||||
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
|
||||
# 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,
|
||||
output_format=output_format,
|
||||
scoring_func=scoring_func,
|
||||
sqrtsoftplus_log1p=sqrtsoftplus_log1p,
|
||||
fused_gate_packed_ids=fused_gate_packed_ids,
|
||||
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,
|
||||
expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None,
|
||||
apply_routed_scaling_factor_on_output: Optional[bool] = False,
|
||||
packed_out: Optional[torch.Tensor] = None,
|
||||
sqrtsoftplus_log1p: bool = False,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
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:
|
||||
assert packed_out is None, "aiter topk_gating cannot emit packed ids"
|
||||
from aiter import topk_gating
|
||||
|
||||
num_tokens = gating_output.shape[0]
|
||||
@@ -1429,6 +1430,14 @@ def biased_topk_jit_kernel_impl(
|
||||
renormalize=renormalize,
|
||||
routed_scaling_factor=routed_scaling_factor,
|
||||
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.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(
|
||||
topk_ids: torch.Tensor,
|
||||
num_token_non_padded: Optional[torch.Tensor] = None,
|
||||
@@ -2162,6 +2196,7 @@ def _post_process_topk_ids(
|
||||
layer_id: int,
|
||||
num_token_non_padded: Optional[torch.Tensor] = None,
|
||||
expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None,
|
||||
padded_rows_masked: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
num_fused_shared_experts = topk_config.num_fused_shared_experts
|
||||
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.
|
||||
recorder_topk_ids = routed_cols
|
||||
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, 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:
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# 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,
|
||||
@@ -2481,6 +2535,22 @@ def select_experts(
|
||||
scoring_func == "sqrtsoftplus" or scoring_func == "sigmoid"
|
||||
):
|
||||
_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(
|
||||
hidden_states=hidden_states,
|
||||
gating_output=router_logits,
|
||||
@@ -2493,7 +2563,9 @@ def select_experts(
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
expert_location_dispatch_info=expert_location_dispatch_info,
|
||||
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 (
|
||||
get_moe_runner_backend().is_flashinfer_trtllm_routed()
|
||||
and scoring_func == "softmax"
|
||||
@@ -2522,8 +2594,7 @@ def select_experts(
|
||||
and correction_bias is None
|
||||
and expert_location_dispatch_info is None
|
||||
and num_fused_shared_experts == 0
|
||||
and not envs.SGLANG_SIMULATE_UNIFORM_EXPERTS.get()
|
||||
and not envs.SGLANG_SIMULATE_ROUND_ROBIN_EXPERTS.get()
|
||||
and not routing_overridden
|
||||
):
|
||||
num_experts = router_logits.shape[-1]
|
||||
if num_experts & (num_experts - 1) == 0 and num_experts <= 512:
|
||||
@@ -2569,15 +2640,7 @@ def select_experts(
|
||||
renormalize=renormalize,
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
if simulate_uniform_experts or simulate_round_robin_experts:
|
||||
if routing_overridden:
|
||||
# Benchmark-only: override gating with a balanced expert assignment (so
|
||||
# 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
|
||||
@@ -2600,6 +2663,8 @@ def select_experts(
|
||||
token_shard_rank=token_shard_rank,
|
||||
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_ids,
|
||||
@@ -2609,18 +2674,17 @@ def select_experts(
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
layer_id=layer_id,
|
||||
expert_location_dispatch_info=expert_location_dispatch_info,
|
||||
padded_rows_masked=padded_rows_masked,
|
||||
)
|
||||
|
||||
get_global_expert_distribution_recorder().on_select_experts(
|
||||
topk_ids=recorder_topk_ids
|
||||
)
|
||||
|
||||
# ===== TO BE REFACTORED ====
|
||||
if packed_topk is not None:
|
||||
return StandardTopKOutputPacked(
|
||||
topk_weights, topk_ids, router_logits, packed_topk
|
||||
)
|
||||
# ===== END TO BE REFACTORED ====
|
||||
return StandardTopKOutput(topk_weights, topk_ids, router_logits)
|
||||
|
||||
|
||||
|
||||
@@ -516,6 +516,8 @@ class Fp8LinearMethod(LinearMethodBase):
|
||||
self.w8a8_block_fp8_linear = None
|
||||
self.w8a8_mxfp8_linear = 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:
|
||||
self.mxfp8_dense_backend = resolve_mxfp8_dense_gemm_backend()
|
||||
self.w8a8_mxfp8_linear = dispatch_w8a8_mxfp8_linear()
|
||||
@@ -1154,6 +1156,19 @@ class Fp8LinearMethod(LinearMethodBase):
|
||||
if mxfp8_view:
|
||||
backend = self.mxfp8_dense_backend
|
||||
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():
|
||||
weight_scale = layer.weight_scale_inv_swizzled
|
||||
elif backend.is_flashinfer_trtllm():
|
||||
@@ -1172,7 +1187,7 @@ class Fp8LinearMethod(LinearMethodBase):
|
||||
bias=bias,
|
||||
**extra_kwargs,
|
||||
)
|
||||
return self.w8a8_mxfp8_linear(
|
||||
out = self.w8a8_mxfp8_linear(
|
||||
input=x,
|
||||
weight=layer.weight,
|
||||
weight_scale=weight_scale,
|
||||
@@ -1180,6 +1195,7 @@ class Fp8LinearMethod(LinearMethodBase):
|
||||
bias=bias,
|
||||
**extra_kwargs,
|
||||
)
|
||||
return out
|
||||
|
||||
if self.block_quant:
|
||||
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:
|
||||
"""The MXFP8 linear for a block-fp8 weight served as MXFP8."""
|
||||
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():
|
||||
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
|
||||
|
||||
|
||||
@@ -1473,9 +1477,14 @@ def flashinfer_mxfp8_blockscaled_linear(
|
||||
bias: Optional[torch.Tensor] = None,
|
||||
output_dtype: Optional[torch.dtype] = None,
|
||||
backend: str = "cutlass",
|
||||
pin_tactic: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""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])
|
||||
output_shape = [*input.shape[:-1], weight.shape[0]]
|
||||
|
||||
@@ -1507,15 +1516,29 @@ def flashinfer_mxfp8_blockscaled_linear(
|
||||
else:
|
||||
weight_scale_t = weight_scale.t() if weight_scale.ndim == 2 else weight_scale
|
||||
|
||||
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,
|
||||
)
|
||||
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(
|
||||
q_input,
|
||||
weight.t(),
|
||||
x_scale_u8,
|
||||
weight_scale_t,
|
||||
out_dtype=output_dtype,
|
||||
use_8x4_sf_layout=False,
|
||||
backend=backend,
|
||||
)
|
||||
|
||||
if bias is not None:
|
||||
output += bias
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple
|
||||
|
||||
import torch
|
||||
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:
|
||||
fuse_routed_scaling_factor_in_topk = True
|
||||
|
||||
@@ -311,6 +326,24 @@ class Mxfp4FlashinferTrtllmMoEMethod:
|
||||
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(
|
||||
self,
|
||||
layer: Module,
|
||||
@@ -321,6 +354,7 @@ class Mxfp4FlashinferTrtllmMoEMethod:
|
||||
|
||||
hidden_states = dispatch_output.hidden_states
|
||||
topk_output = dispatch_output.topk_output
|
||||
pre_quant = getattr(dispatch_output, "hidden_states_pre_quant", None)
|
||||
|
||||
w13 = layer.w13_weight
|
||||
w2 = layer.w2_weight
|
||||
@@ -328,7 +362,7 @@ class Mxfp4FlashinferTrtllmMoEMethod:
|
||||
w2_scale = layer.w2_weight_scale_inv
|
||||
|
||||
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
|
||||
if w13_scale.dim() == 2:
|
||||
@@ -336,17 +370,18 @@ class Mxfp4FlashinferTrtllmMoEMethod:
|
||||
if w2_scale.dim() == 2:
|
||||
w2_scale = w2_scale.reshape(num_local_experts, hidden_size, -1)
|
||||
|
||||
if TopKOutputChecker.format_is_standard(topk_output):
|
||||
topk_ids = topk_output.topk_ids
|
||||
topk_weights = topk_output.topk_weights
|
||||
elif TopKOutputChecker.format_is_bypassed(topk_output):
|
||||
if TopKOutputChecker.format_is_bypassed(topk_output):
|
||||
raise NotImplementedError(
|
||||
"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}")
|
||||
|
||||
topk_ids = topk_output.topk_ids
|
||||
topk_weights = topk_output.topk_weights
|
||||
|
||||
precision = self.flashinfer_mxfp4_moe_precision
|
||||
input_ready: Optional[torch.cuda.Event] = None
|
||||
if precision == "bf16":
|
||||
assert hidden_states.dtype == torch.bfloat16
|
||||
x_quant = hidden_states
|
||||
@@ -360,40 +395,48 @@ class Mxfp4FlashinferTrtllmMoEMethod:
|
||||
value=0.0,
|
||||
)
|
||||
elif precision == "default":
|
||||
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
|
||||
)
|
||||
if isinstance(pre_quant, Mxfp8RoutedInputPreQuant):
|
||||
assert pre_quant.x_q.shape[0] == hidden_states.shape[0]
|
||||
x_quant, x_scale, input_ready = pre_quant
|
||||
else:
|
||||
x_quant, x_scale = self.quantize_routed_input(
|
||||
hidden_states, hidden_size
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported mxfp4 moe precision: {precision}")
|
||||
|
||||
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
|
||||
_make_deferred_finalize_output,
|
||||
is_deferred_finalize_enabled,
|
||||
trtllm_moe_enable_pdl,
|
||||
)
|
||||
|
||||
with use_symmetric_memory(
|
||||
get_tp_group(), disabled=not is_allocation_symmetric()
|
||||
):
|
||||
num_tokens = x_quant.shape[0]
|
||||
out_hidden_size = (
|
||||
x_quant.shape[-1] * 2
|
||||
if x_quant.dtype == torch.uint8
|
||||
else x_quant.shape[-1]
|
||||
)
|
||||
symm_output = torch.empty(
|
||||
num_tokens, out_hidden_size, dtype=torch.bfloat16, device=x_quant.device
|
||||
)
|
||||
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(
|
||||
get_tp_group(), disabled=not is_allocation_symmetric()
|
||||
):
|
||||
out_hidden_size = (
|
||||
x_quant.shape[-1] * 2
|
||||
if x_quant.dtype == torch.uint8
|
||||
else x_quant.shape[-1]
|
||||
)
|
||||
symm_output = torch.empty(
|
||||
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),
|
||||
routing_bias=None,
|
||||
hidden_states=x_quant,
|
||||
@@ -419,11 +462,15 @@ class Mxfp4FlashinferTrtllmMoEMethod:
|
||||
local_num_experts=num_local_experts,
|
||||
routed_scaling_factor=1.0,
|
||||
routing_method_type=int(RoutingMethodType.TopK),
|
||||
do_finalize=True,
|
||||
tune_max_num_tokens=next_power_of_2(x_quant.shape[0]),
|
||||
do_finalize=not defer_finalize,
|
||||
tune_max_num_tokens=next_power_of_2(num_tokens),
|
||||
output=symm_output,
|
||||
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)
|
||||
|
||||
@@ -467,3 +514,58 @@ def maybe_fuse_routed_scale_and_shared_add(
|
||||
if shared is not None:
|
||||
routed += shared
|
||||
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
|
||||
# of each request's first extend token (NgramEmbeddingManager).
|
||||
engram_history: Optional[torch.Tensor] = None
|
||||
encoder_swa_reset: Optional[List[bool]] = None
|
||||
|
||||
req_pool_indices: 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.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
|
||||
out_cache_loc, req_pool_indices_tensor, req_pool_indices_cpu = alloc_for_extend(
|
||||
self
|
||||
|
||||
@@ -7,6 +7,7 @@ from sglang.srt.environ import envs
|
||||
from sglang.srt.managers.prefill_delayer import PrefillDelayerSinglePassExecutor
|
||||
from sglang.srt.runtime_context import (
|
||||
get_disagg,
|
||||
get_exec,
|
||||
get_schedule,
|
||||
)
|
||||
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_storage_hit_tokens = 0
|
||||
self.log_input_tokens = 0
|
||||
self.log_replay_tokens = 0
|
||||
self.reprocessed_log_input_tokens = 0
|
||||
|
||||
if running_batch is not None:
|
||||
@@ -897,6 +899,14 @@ class PrefillAdder:
|
||||
self.reprocessed_log_input_tokens += raw_extend_input_len
|
||||
|
||||
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:
|
||||
# Retraction attribution is intentionally omitted for now; discard
|
||||
# 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_storage_hit_tokens: int = 0
|
||||
num_pending_tokens: int = 0
|
||||
log_replay_tokens: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_adder(
|
||||
@@ -114,6 +115,7 @@ class PrefillStats:
|
||||
):
|
||||
return cls(
|
||||
log_input_tokens=adder.log_input_tokens,
|
||||
log_replay_tokens=adder.log_replay_tokens,
|
||||
log_hit_tokens=adder.log_hit_tokens,
|
||||
reprocessed_log_input_tokens=adder.reprocessed_log_input_tokens,
|
||||
reprocessed_log_hit_tokens=adder.reprocessed_log_hit_tokens,
|
||||
@@ -660,7 +662,10 @@ class SchedulerMetricsReporter:
|
||||
gap_latency = now - self.last_prefill_stats_tic
|
||||
self.last_prefill_stats_tic = now
|
||||
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()
|
||||
@@ -685,6 +690,8 @@ class SchedulerMetricsReporter:
|
||||
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:
|
||||
msg += f"#bootstrap-req: {len(self.scheduler.disagg_prefill_bootstrap_queue.queue)}, "
|
||||
msg += (
|
||||
@@ -728,7 +735,9 @@ class SchedulerMetricsReporter:
|
||||
value=can_run_cuda_graph
|
||||
)
|
||||
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,
|
||||
dp_cooperation_info=dp_cooperation_info,
|
||||
)
|
||||
|
||||
@@ -1257,6 +1257,28 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
|
||||
) -> None:
|
||||
"""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.
|
||||
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
|
||||
input_token_num = len(input_ids) if input_ids is not None else 0
|
||||
input_token_num += self.num_reserved_tokens
|
||||
|
||||
@@ -642,6 +642,14 @@ class TpModelWorker(BaseTpWorker):
|
||||
# update the consumer index of hicache to the running batch
|
||||
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(
|
||||
batch,
|
||||
self.model_runner,
|
||||
|
||||
@@ -194,6 +194,19 @@ def _evict_until_allocatable(
|
||||
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(
|
||||
req: Req,
|
||||
tree_cache: BasePrefixCache,
|
||||
@@ -203,6 +216,11 @@ def retraction_backup(
|
||||
) -> bool:
|
||||
"""Returns False when the host pool cannot hold the backup; the caller
|
||||
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":
|
||||
req.offload_kv_cache(req_to_token_pool, token_to_kv_pool_allocator)
|
||||
return True
|
||||
|
||||
@@ -183,13 +183,8 @@ class CompressStatePool:
|
||||
dtype=dtype, device=device, enable_memory_saver=enable_memory_saver
|
||||
)
|
||||
if not online:
|
||||
if _is_hip and ratio == 128:
|
||||
# Request-scoped C128 state is addressed by req_pool_idx (or a
|
||||
# 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.
|
||||
if ratio == 2 or (_is_hip and ratio == 128):
|
||||
# Request-scoped rings reset all rows; C4 only its -1 sentinel row.
|
||||
self.kv_score_buffer.clear()
|
||||
else:
|
||||
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:
|
||||
"""PD transfer indices of this pool's state for one request."""
|
||||
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(
|
||||
req_pool_idx,
|
||||
seq_len,
|
||||
@@ -262,15 +262,16 @@ class CompressStatePool:
|
||||
) -> torch.Tensor:
|
||||
swa_pages = swa_loc // self.swa_page_size
|
||||
state_loc = swa_pages * self.ring_size + (swa_loc % self.ring_size)
|
||||
state_loc = torch.where(swa_loc < 0, -1, state_loc)
|
||||
return state_loc
|
||||
# Not where(cond, -1, x): its scalar overload may stage a host tensor,
|
||||
# which a CUDA graph capture cannot run.
|
||||
return state_loc.masked_fill_(swa_loc < 0, -1)
|
||||
|
||||
def translate_from_req_position_to_state_loc(
|
||||
self, req_pool_indices: torch.Tensor, positions: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
state_loc = req_pool_indices * self.ring_size + positions % self.ring_size
|
||||
state_loc = torch.where(positions < 0, -1, state_loc)
|
||||
return state_loc
|
||||
# A negative position means "no slot"; it lands on the empty row -1.
|
||||
return state_loc.masked_fill_(positions < 0, -1)
|
||||
|
||||
def get_state_by_state_loc(self, state_loc: torch.Tensor) -> KVAndScore:
|
||||
return self.kv_score_buffer[state_loc]
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import nullcontext
|
||||
from typing import List, NamedTuple, Optional, Sequence, Tuple
|
||||
from typing import List, Literal, NamedTuple, Optional, Sequence, Tuple, Union
|
||||
|
||||
import torch
|
||||
|
||||
@@ -16,7 +16,10 @@ from sglang.kernels.ops.attention.dsv4 import (
|
||||
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.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.srt.constants import GPU_MEMORY_TYPE_KV_CACHE
|
||||
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(
|
||||
compress_ratio: int, is_speculative: bool = False
|
||||
compress_ratio: int, is_speculative: bool = False, num_draft_tokens: int = 0
|
||||
) -> int:
|
||||
assert compress_ratio in [4, 128], f"Unsupported {compress_ratio = }"
|
||||
# Online C128 stores one (max, sum, kv) state per index;
|
||||
# speculative decoding requires the experimental online C128 MTP path.
|
||||
assert compress_ratio in [2, 4, 128], f"Unsupported {compress_ratio = }"
|
||||
if compress_ratio == 2:
|
||||
# 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 is_speculative and not envs.SGLANG_EXPERIMENTAL_ONLINE_C128_MTP.get():
|
||||
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:
|
||||
# Draft-token capacity must match mtp_pad in c_plan.cuh;
|
||||
# a non-speculative ring has no write padding.
|
||||
"""Largest draft-token count this ring can serve; mirrors `mtp_pad` in
|
||||
`c_plan.cuh`, where the bound is derived."""
|
||||
window_size = compress_ratio * (2 if compress_ratio == 4 else 1)
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
# Paged FlashMLA main-KV format of this pool's rows.
|
||||
kv_layout: KVLayout = KVLayout.V4
|
||||
@@ -85,6 +157,7 @@ class DeepSeekV4SingleKVPool(KVCache):
|
||||
enable_memory_saver: bool,
|
||||
start_layer: Optional[int] = None,
|
||||
end_layer: Optional[int] = None,
|
||||
kv_layout: Union[str, KVLayout] = KVLayout.V4,
|
||||
):
|
||||
super().__init__(
|
||||
size,
|
||||
@@ -99,8 +172,11 @@ class DeepSeekV4SingleKVPool(KVCache):
|
||||
self.qk_nope_head_dim = qk_nope_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.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.k_with_scale_buffer_dtype = torch.int8
|
||||
self._create_buffers()
|
||||
@@ -120,6 +196,9 @@ class DeepSeekV4SingleKVPool(KVCache):
|
||||
]
|
||||
|
||||
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 = (
|
||||
self.qk_nope_head_dim
|
||||
+ self.qk_rope_head_dim * self.rope_storage_dtype.itemsize
|
||||
@@ -131,13 +210,17 @@ class DeepSeekV4SingleKVPool(KVCache):
|
||||
def create_buffer(self, *, num_pages: int):
|
||||
bytes_per_token = self.get_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 = ceil_div(bytes_per_page_non_padded, 576) * 576
|
||||
self.bytes_per_page_padded = self.kv_layout.page_bytes(self.page_size)
|
||||
|
||||
assert bytes_per_token == 448 + 64 * 2 + 8, (
|
||||
"DSV4 KV layout: qk_nope_head_dim FP8 (448) + qk_rope_head_dim BF16 "
|
||||
"(64*2) + nope FP8 scales + scale_pad = 584 bytes/token"
|
||||
)
|
||||
if self.kv_layout is KVLayout.V4:
|
||||
assert bytes_per_token == 448 + 64 * 2 + 8, (
|
||||
"DSV4 KV layout: qk_nope_head_dim FP8 (448) + qk_rope_head_dim BF16 "
|
||||
"(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
|
||||
|
||||
return torch.zeros(
|
||||
@@ -153,6 +236,10 @@ class DeepSeekV4SingleKVPool(KVCache):
|
||||
loc: torch.Tensor,
|
||||
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(
|
||||
pool=self,
|
||||
buf=self.kv_buffer[layer_id],
|
||||
@@ -165,13 +252,19 @@ class DeepSeekV4SingleKVPool(KVCache):
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
freqs_cis: Optional[torch.Tensor] = 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(
|
||||
input=cache_k,
|
||||
cache=self.kv_buffer[layer_id],
|
||||
indices=loc,
|
||||
page_size=self.page_size,
|
||||
type="flashmla",
|
||||
layout=self.kv_layout,
|
||||
freqs_cis=freqs_cis,
|
||||
)
|
||||
|
||||
def get_key_buffer(self, layer_id: int):
|
||||
@@ -235,12 +328,14 @@ class DeepSeekV4UniformFP8KVPool(DeepSeekV4SingleKVPool):
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
freqs_cis: Optional[torch.Tensor] = None,
|
||||
) -> None:
|
||||
"""Store normed/roped rows as e4m3 with the backend's fixed unit scale.
|
||||
|
||||
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
|
||||
self.kv_buffer[layer_id].view(torch.uint8).view(-1, self.kv_cache_total_dim)[
|
||||
loc.long()
|
||||
@@ -260,6 +355,7 @@ class HiSparseC4DevicePool(DeepSeekV4SingleKVPool):
|
||||
enable_memory_saver: bool,
|
||||
start_layer: int | None = None,
|
||||
end_layer: int | None = None,
|
||||
kv_layout: Union[str, KVLayout] = KVLayout.V4,
|
||||
):
|
||||
super().__init__(
|
||||
size,
|
||||
@@ -272,6 +368,11 @@ class HiSparseC4DevicePool(DeepSeekV4SingleKVPool):
|
||||
enable_memory_saver,
|
||||
start_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(
|
||||
@@ -318,9 +419,10 @@ class HiSparseC4DevicePool(DeepSeekV4SingleKVPool):
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
freqs_cis: Optional[torch.Tensor] = None,
|
||||
) -> None:
|
||||
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):
|
||||
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")
|
||||
|
||||
|
||||
# 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):
|
||||
quant_block_size = 128
|
||||
index_k_with_scale_buffer_dtype = torch.uint8
|
||||
@@ -346,6 +463,7 @@ class DeepSeekV4IndexerPool(KVCache):
|
||||
enable_memory_saver: bool,
|
||||
start_layer: Optional[int] = None,
|
||||
end_layer: Optional[int] = None,
|
||||
use_fp4_indexer: Optional[bool] = None,
|
||||
):
|
||||
super().__init__(
|
||||
size,
|
||||
@@ -358,8 +476,12 @@ class DeepSeekV4IndexerPool(KVCache):
|
||||
end_layer,
|
||||
)
|
||||
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
|
||||
# Low-ratio pools round to nearest even; c4 keeps threshold rounding.
|
||||
self.index_k_rne = False
|
||||
|
||||
self._create_buffer()
|
||||
|
||||
@@ -500,8 +622,54 @@ class DeepSeekV4IndexerPool(KVCache):
|
||||
cache=self.index_k_with_scale_buffer[layer_id - self.start_layer],
|
||||
loc=loc,
|
||||
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):
|
||||
kv_size: int
|
||||
@@ -511,7 +679,9 @@ class _CompressedPoolConfig(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_kv_pool: Optional[DeepSeekV4SingleKVPool] = None
|
||||
|
||||
@@ -683,6 +853,11 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
enable_hisparse: bool = False,
|
||||
online_mtp_max_draft_tokens: int = 0,
|
||||
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__(
|
||||
swa_size,
|
||||
@@ -694,6 +869,14 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
start_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
|
||||
|
||||
logger.info(
|
||||
@@ -723,6 +906,11 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
self.uniform_fp8 = (
|
||||
not self._unified_kv
|
||||
) 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)
|
||||
if self._unified_kv:
|
||||
# Unified C4 state is request-addressed: one ring per req slot,
|
||||
@@ -738,18 +926,26 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
c128_state_pool_size = max(
|
||||
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 = {
|
||||
4: _CompressedPoolConfig(
|
||||
kv_size=c4_size,
|
||||
state_size=c4_state_pool_size,
|
||||
state_dtype=c4_state_dtype,
|
||||
indexer_size=c4_logical_size,
|
||||
),
|
||||
128: _CompressedPoolConfig(
|
||||
kv_size=c128_size,
|
||||
state_size=c128_state_pool_size,
|
||||
state_dtype=c128_state_dtype,
|
||||
),
|
||||
ratio: config
|
||||
for ratio, config in {
|
||||
4: _CompressedPoolConfig(
|
||||
kv_size=c4_size,
|
||||
state_size=c4_state_pool_size,
|
||||
state_dtype=c4_state_dtype,
|
||||
indexer_size=c4_logical_size,
|
||||
),
|
||||
128: _CompressedPoolConfig(
|
||||
kv_size=c128_size,
|
||||
state_size=c128_state_pool_size,
|
||||
state_dtype=c128_state_dtype,
|
||||
),
|
||||
}.items()
|
||||
if ratio in model_ratios
|
||||
}
|
||||
self.compression_ratios = compression_ratios
|
||||
self.online_mtp_max_draft_tokens = online_mtp_max_draft_tokens
|
||||
@@ -786,7 +982,51 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
stage_layer_num = len(stage_ratios)
|
||||
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
|
||||
swa_ring_size = get_swa_ring_size(
|
||||
self.sliding_window, get_spec().speculative_algorithm is not None
|
||||
@@ -825,9 +1065,19 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
device=device,
|
||||
enable_memory_saver=enable_memory_saver,
|
||||
global_page_size=swa_page_size,
|
||||
kv_layout=self.kv_layout,
|
||||
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(
|
||||
stage_ratios=stage_ratios,
|
||||
page_size=page_size,
|
||||
@@ -863,8 +1113,12 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
self.full_to_swa_index_mapping = full_to_swa_index_mapping
|
||||
|
||||
def get_ring_size(self, compress_ratio: int) -> int:
|
||||
is_speculative = get_spec().speculative_algorithm is not None
|
||||
return get_compress_state_ring_size(compress_ratio, is_speculative)
|
||||
spec = get_spec()
|
||||
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):
|
||||
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_lens.append(compress_rows * row_bytes)
|
||||
item_lens.append(rows_per_page * row_bytes)
|
||||
else:
|
||||
elif kv_pool is not None:
|
||||
for buf in kv_pool.kv_buffer:
|
||||
append_page_buffer(buf)
|
||||
|
||||
indexer_pool = self.index_pools.get(ratio)
|
||||
if indexer_pool is not None:
|
||||
for buf in indexer_pool.contiguous_page_row_buffers():
|
||||
append_page_buffer(buf)
|
||||
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():
|
||||
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
|
||||
|
||||
@@ -1011,7 +1282,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
data_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:
|
||||
assert buf.ndim == 2, f"expected 2D buffer, got {buf.ndim}D"
|
||||
data_ptrs.append(buf.data_ptr())
|
||||
@@ -1023,6 +1294,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
self.indexer_compress_state_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:
|
||||
continue
|
||||
t = pool.kv_score_buffer.kv_score
|
||||
@@ -1036,6 +1308,8 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
def get_request_state_buf_infos(
|
||||
self,
|
||||
) -> 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_lens: List[int] = []
|
||||
item_lens: List[int] = []
|
||||
@@ -1046,7 +1320,10 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
assert t.ndim == 2, f"expected 2D buffer, got {t.ndim}D"
|
||||
data_ptrs.append(t.data_ptr())
|
||||
data_lens.append(t.nbytes)
|
||||
item_lens.append(t[0].nbytes if ONLINE_C128 else t[0].nbytes * 128)
|
||||
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)
|
||||
return data_ptrs, data_lens, item_lens
|
||||
|
||||
def _init_compressed_pools(
|
||||
@@ -1060,12 +1337,25 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
enable_hisparse: bool,
|
||||
kv_pool_cls: type,
|
||||
) -> 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
|
||||
layer_counts = {ratio: stage_ratios.count(ratio) for ratio in configs}
|
||||
# Keep empty pools and allocation order for PP stages without a given ratio.
|
||||
self.kv_pools: dict[int, Optional[DeepSeekV4SingleKVPool]] = {
|
||||
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:
|
||||
for ratio, config in configs.items():
|
||||
@@ -1084,6 +1374,19 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
enable_memory_saver=enable_memory_saver,
|
||||
global_page_size=page_size,
|
||||
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] = {
|
||||
@@ -1099,11 +1402,24 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
for ratio, config in configs.items()
|
||||
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.
|
||||
self.c4_kv_pool = self.kv_pools[4]
|
||||
self.c128_kv_pool = self.kv_pools[128]
|
||||
self.c4_indexer_kv_pool = self.index_pools[4]
|
||||
# HiCache and hardware backends still read these per-ratio attributes.
|
||||
self.c4_kv_pool = self.kv_pools.get(4)
|
||||
self.c128_kv_pool = self.kv_pools.get(128)
|
||||
self.c4_indexer_kv_pool = self.index_pools.get(4)
|
||||
|
||||
def _make_kv_pool(
|
||||
self,
|
||||
@@ -1116,6 +1432,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
enable_memory_saver: bool,
|
||||
global_page_size: int,
|
||||
cls: type = DeepSeekV4SingleKVPool,
|
||||
kv_layout: KVLayout = KVLayout.V4,
|
||||
) -> DeepSeekV4SingleKVPool:
|
||||
"""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
|
||||
@@ -1132,8 +1449,17 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
layer_num,
|
||||
device,
|
||||
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(
|
||||
self,
|
||||
size: int,
|
||||
@@ -1143,10 +1469,25 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
layer_num: int,
|
||||
device: str,
|
||||
enable_memory_saver: bool,
|
||||
force_fp4: bool = False,
|
||||
) -> DeepSeekV4IndexerPool:
|
||||
"""Build the c4 lightning-indexer K pool (packed CUDA layout).
|
||||
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(
|
||||
size,
|
||||
page_size,
|
||||
@@ -1172,13 +1513,30 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
enable_memory_saver=enable_memory_saver,
|
||||
ratio=ratio,
|
||||
online=(ratio == 128 and ONLINE_C128),
|
||||
request_scoped=ratio == 128,
|
||||
request_scoped=ratio in (2, 128),
|
||||
swa_page_size=self.swa_page_size,
|
||||
online_mtp_max_draft_tokens=(
|
||||
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):
|
||||
total_L = len(self.compression_ratios)
|
||||
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):
|
||||
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
|
||||
|
||||
self.compress_state_pools[idx] = self._make_compress_state_pool(
|
||||
@@ -1204,6 +1570,36 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
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):
|
||||
layer_counts = {0: 0, **{ratio: 0 for ratio in self.kv_pools}}
|
||||
total_L = len(self.compression_ratios)
|
||||
@@ -1213,12 +1609,17 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
ratio = self.compression_ratios[idx]
|
||||
if ratio not in layer_counts:
|
||||
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(
|
||||
compress_ratio=ratio,
|
||||
compress_layer_id=layer_counts[ratio],
|
||||
compress_layer_id=compress_layer_id,
|
||||
compress_kv_pool=self.kv_pools.get(ratio),
|
||||
)
|
||||
layer_counts[ratio] += 1
|
||||
|
||||
def wait_layer_transfer(self, layer_id: int) -> None:
|
||||
if self.layer_transfer_counter is not None:
|
||||
@@ -1228,7 +1629,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
self.wait_layer_transfer(layer_id)
|
||||
compress_state_pool = self.compress_state_pools[layer_id]
|
||||
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
|
||||
|
||||
@@ -1292,23 +1693,20 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
return pools[0].transfer_indices(req_pool_idx, seq_len)
|
||||
|
||||
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:
|
||||
if pool is None or not pool.request_scoped:
|
||||
continue
|
||||
|
||||
state = pool.kv_score_buffer.kv_score
|
||||
if ONLINE_C128:
|
||||
row = state[req_pool_idx]
|
||||
if pool.ratio == 128 and ONLINE_C128:
|
||||
row = pool.kv_score_buffer.kv_score[req_pool_idx]
|
||||
head_dim = row.shape[-1] // 3
|
||||
row[:head_dim].fill_(float("-inf"))
|
||||
row[head_dim:].zero_()
|
||||
else:
|
||||
start = req_pool_idx * pool.ring_size
|
||||
rows = state[start : start + pool.ring_size]
|
||||
half = rows.shape[-1] // 2
|
||||
rows[:, :half].zero_()
|
||||
rows[:, half:].fill_(float("-inf"))
|
||||
continue
|
||||
|
||||
start = req_pool_idx * pool.ring_size
|
||||
pool.kv_score_buffer[start : start + pool.ring_size].clear()
|
||||
|
||||
def clear_unaccepted_c128_draft_states(
|
||||
self,
|
||||
@@ -1349,8 +1747,40 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
return layer_id - self._stage_start
|
||||
|
||||
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)]
|
||||
|
||||
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:
|
||||
_, _, compress_kv_pool = self.layer_mapping[layer_id]
|
||||
assert compress_kv_pool is not None
|
||||
@@ -1369,12 +1799,16 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
return compress_kv_pool.kv_cache_total_dim
|
||||
|
||||
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:
|
||||
"""Last dim of the ``(pages, page_size, 1, bytes)`` view the attention
|
||||
kernel detects the SWA cache's format from."""
|
||||
return self.swa_kv_pool.kv_cache_total_dim
|
||||
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.kv_layout.bytes_per_token
|
||||
|
||||
def get_extra_key_buffer(self, layer_id: int) -> torch.Tensor | None:
|
||||
self.wait_layer_transfer(layer_id)
|
||||
@@ -1401,6 +1835,25 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
)
|
||||
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:
|
||||
return self._indexer_pool(compress_ratio).page_size
|
||||
|
||||
@@ -1473,12 +1926,14 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
swa_loc: torch.Tensor,
|
||||
cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
|
||||
) -> None:
|
||||
self.swa_kv_pool.set_key_buffer(
|
||||
self._swa_local_layer_id(layer_id), swa_loc, cache_nope_fp8_rope_bf16_pack
|
||||
)
|
||||
self.set_swa_key_buffer(layer_id, swa_loc, cache_nope_fp8_rope_bf16_pack)
|
||||
|
||||
def get_swa_key_buffer_radix(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_radix_fused(
|
||||
@@ -1487,8 +1942,13 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
swa_loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
) -> None:
|
||||
return self.swa_kv_pool.set_key_buffer_fused(
|
||||
self._swa_local_layer_id(layer_id), swa_loc, cache_k
|
||||
return fused_store_cache(
|
||||
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(
|
||||
@@ -1528,8 +1988,9 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
freqs_cis=freqs_cis,
|
||||
positions=positions,
|
||||
out_loc=swa_loc,
|
||||
kvcache=self.swa_kv_pool.kv_buffer[self._swa_local_layer_id(layer_id)],
|
||||
page_size=self.swa_kv_pool.page_size,
|
||||
kvcache=self.get_swa_raw_buffer(layer_id),
|
||||
page_size=self.swa_page_size,
|
||||
layout=self.kv_layout,
|
||||
)
|
||||
|
||||
def set_unified_key_buffer_radix_fused_norm_rope(
|
||||
@@ -1565,10 +2026,20 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
freqs_cis: Optional[torch.Tensor] = 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]
|
||||
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(
|
||||
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"
|
||||
# TODO(hzh0425): Current DeepSeek V4 pool naming is verbose; will be normalized to
|
||||
# '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_INDEXER = "deepseek_v4_c4_indexer"
|
||||
# FP4 indexer splits the indexer cache into separate payload/scale buffers,
|
||||
|
||||
@@ -113,7 +113,7 @@ def _resolve_deepseek_v4_layer_mappings(
|
||||
) -> _DeepSeekV4LayerMappings:
|
||||
transfer_layer_num = kvcache.end_layer - kvcache.start_layer
|
||||
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 = {}, {}, []
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
class _IndexerRegion:
|
||||
"""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(
|
||||
kvcache: Any, ratio: int
|
||||
) -> Optional[tuple[PoolName, list, int]]:
|
||||
@@ -651,10 +737,10 @@ def build_deepseek_v4_hicache_stack(
|
||||
full_layer_mapping = layer_mappings.full
|
||||
|
||||
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 = []
|
||||
if is_unified_kv:
|
||||
# unified_kv keeps the SWA ring inside the unified pool and never offloads it,
|
||||
# so there is no separate SWA host pool to map.
|
||||
if not has_paged_swa:
|
||||
# Unified KV and encoder replay rebuild SWA state; keep it out of host cache.
|
||||
swa_layer_mapping = {}
|
||||
else:
|
||||
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(
|
||||
pool_name=str(PoolName.SWA),
|
||||
device_buffers=[
|
||||
@@ -719,6 +805,7 @@ def build_deepseek_v4_hicache_stack(
|
||||
slot_page_size=kvcache.swa_page_size,
|
||||
layout=get_memory().hicache_mem_layout,
|
||||
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
|
||||
entries.append(
|
||||
@@ -749,7 +836,8 @@ def build_deepseek_v4_hicache_stack(
|
||||
slot_page_size=page_size,
|
||||
layout=get_memory().hicache_mem_layout,
|
||||
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(
|
||||
build_pool_entry(
|
||||
@@ -855,7 +943,8 @@ def build_deepseek_v4_hicache_stack(
|
||||
slot_page_size=c128_slot_page_size,
|
||||
layout=get_memory().hicache_mem_layout,
|
||||
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.
|
||||
# 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:
|
||||
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)
|
||||
cache_controller = HybridCacheController(
|
||||
params.token_to_kv_pool_allocator,
|
||||
@@ -1459,10 +1552,12 @@ class _DeepSeekV4Strategy(StackStrategy):
|
||||
def matches(self, kvcache, components):
|
||||
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.C128},
|
||||
)
|
||||
) or (components == {ComponentType.FULL} and kvcache.swa_kv_pool is None)
|
||||
|
||||
def build_direct_linker_pool_group(self, *, kvcache, params, page_size):
|
||||
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
|
||||
# them out everywhere else.
|
||||
_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_ROPE, 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.managers.mm_schedule import init_mm_embedding_cache
|
||||
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.registry import TreeCacheBuildContext, create_tree_cache
|
||||
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
|
||||
@@ -233,7 +234,11 @@ def build_kv_cache(
|
||||
)
|
||||
|
||||
# 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_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 (
|
||||
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.memory_pool import (
|
||||
DSATokenToKVPool,
|
||||
@@ -1237,6 +1240,7 @@ class KVCacheConfigurator:
|
||||
if is_dsv4_model:
|
||||
token_to_kv_pool = self._build_dsv4_kv_pool(
|
||||
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,
|
||||
c4_max_total_num_tokens=sizes.c4_max_total_num_tokens,
|
||||
c128_max_total_num_tokens=sizes.c128_max_total_num_tokens,
|
||||
@@ -1340,6 +1344,7 @@ class KVCacheConfigurator:
|
||||
self,
|
||||
*,
|
||||
max_running_requests: int,
|
||||
full_max_total_num_tokens: int,
|
||||
swa_max_total_num_tokens: Optional[int],
|
||||
c4_max_total_num_tokens: int,
|
||||
c128_max_total_num_tokens: int,
|
||||
@@ -1361,8 +1366,10 @@ class KVCacheConfigurator:
|
||||
compression_ratios = [
|
||||
COMPRESS_RATIO_NEXTN_LAYER
|
||||
] * self.layer_info.num_effective_layers
|
||||
kv_source_layers = []
|
||||
else:
|
||||
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
|
||||
# follows the same fixed ring ownership as GPU. Do not replace the
|
||||
@@ -1374,8 +1381,13 @@ class KVCacheConfigurator:
|
||||
)
|
||||
|
||||
pool_cls = DSV4NPUTokenToKVPool
|
||||
kv_layout_kwargs = {}
|
||||
else:
|
||||
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(
|
||||
max_num_reqs=max_running_requests,
|
||||
@@ -1404,6 +1416,10 @@ class KVCacheConfigurator:
|
||||
end_layer=self.layer_info.end_layer,
|
||||
enable_hisparse=get_memory().enable_hisparse,
|
||||
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:
|
||||
# The draft pool has no C4 layers and shares this req pool, so only
|
||||
@@ -2071,7 +2087,19 @@ class KVCacheConfigurator:
|
||||
need_sort=need_sort,
|
||||
)
|
||||
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(
|
||||
sizes.swa_max_total_num_tokens,
|
||||
page_size=get_schedule().page_size,
|
||||
@@ -2141,7 +2169,10 @@ class KVCacheConfigurator:
|
||||
|
||||
else:
|
||||
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(
|
||||
token_to_kv_pool_allocator,
|
||||
DeepSeekV4HiSparseTokenToKVPoolAllocator,
|
||||
|
||||
@@ -71,6 +71,7 @@ from sglang.srt.mem_cache.allocator.unified_mamba import (
|
||||
UnifiedMambaTokenToKVPoolAllocator,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@@ -162,6 +163,10 @@ class KVIndexTranslator:
|
||||
self._swa_write_loc_from_full = (
|
||||
token_to_kv_pool.translate_loc_from_full_to_swa
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@@ -814,6 +814,12 @@ class MooncakeStore(HiCacheStorage, MooncakeBaseStore):
|
||||
elif pool_name in (
|
||||
PoolName.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_ROPE,
|
||||
PoolName.DEEPSEEK_V4_C4_INDEXER,
|
||||
|
||||
@@ -84,6 +84,7 @@ ALLOWED_KEYS_PER_PHASE = {
|
||||
"max_context_size",
|
||||
"full_prefill_max_req",
|
||||
"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
|
||||
# the scheduler's aggregate chunked_prefill_size token budget.
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
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
|
||||
# first extend token, oldest first, [bs, n - 1] int32 (see EngramHasher).
|
||||
|
||||
@@ -33,7 +33,9 @@ if TYPE_CHECKING:
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
if hpc_bf16xfp32_gemm_enabled():
|
||||
@@ -148,7 +162,7 @@ class WeightUpdater:
|
||||
) -> tuple[bool, str]:
|
||||
"""Update engine weights in-place from the 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:
|
||||
return False, error
|
||||
|
||||
@@ -238,7 +252,7 @@ class WeightUpdater:
|
||||
shape: the shape of the parameter to be updated.
|
||||
"""
|
||||
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:
|
||||
return False, error
|
||||
|
||||
@@ -322,7 +336,7 @@ class WeightUpdater:
|
||||
named_tensors: List[Tuple[str, Union[torch.Tensor, LocalSerializedTensor]]],
|
||||
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:
|
||||
return False, error
|
||||
|
||||
@@ -388,7 +402,7 @@ class WeightUpdater:
|
||||
def update_weights_from_ipc(self: WeightUpdater, recv_req):
|
||||
"""Update weights from IPC for checkpoint-engine integration."""
|
||||
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:
|
||||
return False, error
|
||||
|
||||
|
||||
@@ -804,6 +804,45 @@ class HybridSWAPoolConfigurator(MemoryPoolConfigurator):
|
||||
return self._solve_pool_sizes(max_total_num_tokens, page_size)
|
||||
|
||||
|
||||
def compute_swa_request_cap(*, page_size: int, window: int, attn_dp_size: int) -> int:
|
||||
"""Worst-case SWA slots the scheduler holds live at max_running_requests."""
|
||||
draft_tokens = get_spec().speculative_num_draft_tokens or 1
|
||||
eviction_interval = max(1, envs.SGLANG_SWA_EVICTION_INTERVAL.get())
|
||||
|
||||
# __________[padding][eviction_interval][window]
|
||||
# Padding to make sure eviction point is page-aligned.
|
||||
trailing_tokens = window + eviction_interval * draft_tokens + page_size
|
||||
if get_spec().speculative_algorithm is None:
|
||||
decode_alloc = page_size
|
||||
elif get_schedule().disable_overlap_schedule:
|
||||
# spec-v1: new_tokens_required_next_decode per request.
|
||||
decode_alloc = spec_decode_alloc_len_per_request(
|
||||
page_size=page_size,
|
||||
speculative_num_steps=get_spec().speculative_num_steps,
|
||||
speculative_eagle_topk=get_spec().speculative_eagle_topk,
|
||||
speculative_num_draft_tokens=get_spec().speculative_num_draft_tokens,
|
||||
)
|
||||
else:
|
||||
# spec-v2: the overlap allocator keeps 2 * alloc_len outstanding
|
||||
# (eagle_utils.eagle_prepare_for_decode: kv_committed_len + 2 * alloc_len).
|
||||
decode_alloc = 2 * get_alloc_len_per_decode()
|
||||
per_request = trailing_tokens + decode_alloc
|
||||
|
||||
num_reqs = get_schedule().max_running_requests // attn_dp_size
|
||||
if get_disagg().disaggregation_mode == "decode":
|
||||
return (
|
||||
per_request * num_reqs
|
||||
+ (window + page_size) * get_disagg().disaggregation_decode_extra_slots
|
||||
)
|
||||
else:
|
||||
chunks_in_flight = 1 if get_schedule().disable_overlap_schedule else 2
|
||||
return (
|
||||
per_request * num_reqs
|
||||
+ chunks_in_flight * get_schedule().chunked_prefill_size
|
||||
+ page_size
|
||||
)
|
||||
|
||||
|
||||
class SWAChunkCapPoolConfigurator(HybridSWAPoolConfigurator):
|
||||
"""Hybrid SWA configurator with the SWA pool sized from a fixed token cap.
|
||||
|
||||
@@ -818,45 +857,11 @@ class SWAChunkCapPoolConfigurator(HybridSWAPoolConfigurator):
|
||||
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
|
||||
eviction_interval = max(1, envs.SGLANG_SWA_EVICTION_INTERVAL.get())
|
||||
|
||||
"""
|
||||
__________[padding][eviction_interval][window]
|
||||
Padding to make sure eviction point is page-aligned.
|
||||
"""
|
||||
trailing_tokens = window + eviction_interval * draft_tokens + page_size
|
||||
if get_spec().speculative_algorithm is None:
|
||||
decode_alloc = page_size
|
||||
elif get_schedule().disable_overlap_schedule:
|
||||
# spec-v1: new_tokens_required_next_decode per request.
|
||||
decode_alloc = spec_decode_alloc_len_per_request(
|
||||
page_size=page_size,
|
||||
speculative_num_steps=get_spec().speculative_num_steps,
|
||||
speculative_eagle_topk=get_spec().speculative_eagle_topk,
|
||||
speculative_num_draft_tokens=get_spec().speculative_num_draft_tokens,
|
||||
)
|
||||
else:
|
||||
# spec-v2: the overlap allocator keeps 2 * alloc_len outstanding
|
||||
# (eagle_utils.eagle_prepare_for_decode: kv_committed_len + 2 * alloc_len).
|
||||
decode_alloc = 2 * get_alloc_len_per_decode()
|
||||
per_request = trailing_tokens + decode_alloc
|
||||
|
||||
num_reqs = get_schedule().max_running_requests // kvc.ps.attn_dp_size
|
||||
if get_disagg().disaggregation_mode == "decode":
|
||||
self._swa_cap = (
|
||||
per_request * num_reqs
|
||||
+ (window + page_size) * get_disagg().disaggregation_decode_extra_slots
|
||||
)
|
||||
else:
|
||||
chunks_in_flight = 1 if get_schedule().disable_overlap_schedule else 2
|
||||
self._swa_cap = (
|
||||
per_request * num_reqs
|
||||
+ chunks_in_flight * get_schedule().chunked_prefill_size
|
||||
+ page_size
|
||||
)
|
||||
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
|
||||
def is_applicable(kvc: KVCacheConfigurator) -> bool:
|
||||
@@ -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
|
||||
class _DSV4PoolSizes:
|
||||
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_rope_head_dim = cfg.qk_rope_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
|
||||
# keeps the FP8 estimate.
|
||||
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)}"
|
||||
)
|
||||
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.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.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 = (
|
||||
get_schedule().max_running_requests // kvc.ps.attn_dp_size
|
||||
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_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)
|
||||
|
||||
# Unified-KV uses a different physical layout than the non-unified V4 path:
|
||||
# * one row carries the full latent -- 1024 B bf16, or 640 B under
|
||||
# SGLANG_DSV4_UNIFIED_KV_FP8 (512 B fp8 nope + 128 B bf16 rope) -- not
|
||||
# that path's 584-byte fp8(nope) + bf16(rope) + scales cell.
|
||||
# * SWA is a fixed per-request ring (num_req_slots * ring_size),
|
||||
# independent of full_token, so it is a fixed *bias* rather than a
|
||||
# per-token term. Gate on the same switch the pool itself uses so the
|
||||
# sizing and the allocation never drift apart.
|
||||
from sglang.kernels.ops.attention.dsv4.unified_kv_kernels.env_gate import (
|
||||
is_unified_kv_fp8,
|
||||
is_unified_kv_triton,
|
||||
# The low-ratio indexer pools are built with force_fp4=True
|
||||
# (deepseek_v4_memory_pool), so they are fp4 whatever dtype c4 uses.
|
||||
low_ratio_index_bytes = get_dsv4_indexer_bytes_per_token(
|
||||
self.indexer_head_dim, use_fp4_indexer=True
|
||||
)
|
||||
self.low_ratio_bytes_per_full_token = sum(
|
||||
(self.kv_bytes + low_ratio_index_bytes) / cfg.compress_ratios[l]
|
||||
for l in cfg.hf_config.kv_source_layer_ids
|
||||
if kvc.layer_info.start_layer <= l < kvc.layer_info.end_layer
|
||||
and cfg.compress_ratios[l] in (1, 2)
|
||||
)
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import (
|
||||
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).
|
||||
self._swa_ring_size = get_swa_ring_size(self.swa_page_size, self.is_speculative)
|
||||
self._spec_infl = 1.0
|
||||
@@ -1044,8 +1080,47 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
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()
|
||||
if self.is_speculative:
|
||||
if self.is_speculative and not self.encoder_replay:
|
||||
# Reserve memory for the speculative draft worker by inflating
|
||||
# per-token bytes by (target+draft)/target. Equivalent to dflash's
|
||||
# scale_kv_cell_size_per_token_for_dflash but applied to
|
||||
@@ -1054,6 +1129,7 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
target_layers = self.num_layers_total
|
||||
self._spec_infl = (target_layers + draft_layers) / target_layers
|
||||
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
|
||||
# and assumes a strict forward-only schedule. Speculative decode (MTP)
|
||||
@@ -1105,72 +1181,133 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
f"get_compress_state_ring_size()."
|
||||
)
|
||||
|
||||
def _get_bytes_per_full_token(self) -> float:
|
||||
if self._unified:
|
||||
# Unified_kv stores the whole latent: one bf16 pool, or an fp8 nope
|
||||
# pool plus a bf16 rope pool. kv_bytes also prices the compressed
|
||||
# c4/c128 rows below, which live in the same pool(s).
|
||||
kv_bytes = self._unified_row_bytes
|
||||
else:
|
||||
kv_bytes = self.qk_nope_head_dim + self.qk_rope_head_dim * 2 + 8
|
||||
def _resolve_swa_prefix_tails(self) -> int:
|
||||
"""Cached prefix tails cap mode keeps addressable: a prefix is reusable only
|
||||
while its last sliding_window tokens still hold SWA slots."""
|
||||
prefix_tails = get_schedule().swa_prefix_tails
|
||||
if prefix_tails is not None:
|
||||
return prefix_tails
|
||||
if get_memory().disable_radix_cache:
|
||||
# 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
|
||||
c4_state_dtype_size, c128_state_dtype_size = (
|
||||
_get_dsv4_compress_state_dtype_sizes()
|
||||
def _resolve_swa_cap_tokens(self) -> Optional[int]:
|
||||
"""SWA slots to reserve in cap mode, None to keep ratio sizing. Cap mode
|
||||
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
|
||||
# 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.
|
||||
c128_online = envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get()
|
||||
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
|
||||
# max_running_requests is known, so it should not scale with
|
||||
# full-token capacity here.
|
||||
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)
|
||||
return (
|
||||
# Ring mode: SWA is a fixed per-request pool (see _fixed_swa_bytes).
|
||||
(
|
||||
0.0
|
||||
if self._unified
|
||||
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
|
||||
swa_ratio * self.bytes_per_swa_token
|
||||
+ self.low_ratio_bytes_per_full_token
|
||||
+ c4_frac * self.kv_bytes * self.num_layers_ca4
|
||||
+ 1 / 128 * self.kv_bytes * self.num_layers_ca128
|
||||
+ 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
|
||||
+ (
|
||||
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:
|
||||
full_token = full_token // page_size * page_size
|
||||
swa_tokens = int(full_token * self.swa_ratio) // page_size * page_size
|
||||
if not self._unified:
|
||||
# Ring mode: the paged SWA pool is vestigial, so its floor does not apply.
|
||||
self.validate_swa_pool_size(swa_tokens, self.sliding_window_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:
|
||||
self.validate_swa_pool_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(
|
||||
full_max_total_num_tokens=full_token,
|
||||
swa_max_total_num_tokens=swa_tokens,
|
||||
@@ -1195,18 +1332,17 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
return 0
|
||||
|
||||
_, 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)
|
||||
|
||||
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
||||
state_rows = num_req_slots + self.c128_ring_size + 1
|
||||
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:
|
||||
state_pool_size = num_req_slots * self.c128_ring_size
|
||||
state_rows = state_pool_size + self.c128_ring_size + 1
|
||||
state_rows = ceil_div(state_rows, 128) * 128
|
||||
state_last_dim = 2 * attn_head_dim
|
||||
state_last_dim = 2 * self.attn_head_dim
|
||||
|
||||
return (
|
||||
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
|
||||
)
|
||||
|
||||
available_bytes_for_tokens = max(
|
||||
available_bytes
|
||||
- c128_state_fixed_bytes
|
||||
- swa_ring_fixed_bytes
|
||||
- c4_state_fixed_bytes,
|
||||
0,
|
||||
swa_fixed_bytes = self._get_swa_fixed_bytes()
|
||||
fixed_bytes = (
|
||||
c128_state_fixed_bytes
|
||||
+ swa_fixed_bytes
|
||||
+ swa_ring_fixed_bytes
|
||||
+ 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)
|
||||
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)
|
||||
logger.info(
|
||||
@@ -1329,6 +1474,7 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
||||
f"bytes_per_full_token={self.bytes_per_full_token:.2f}, "
|
||||
f"available_bytes={available_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"c4_state_fixed={c4_state_fixed_bytes / (1 << 30):.2f} GB, "
|
||||
f"full_token={sizes.full_max_total_num_tokens}"
|
||||
|
||||
@@ -635,7 +635,11 @@ class BaseRunner(ABC):
|
||||
spec_algorithm=mr.spec_algorithm,
|
||||
spec_info=spec_info,
|
||||
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,
|
||||
lora_ids=lora_ids,
|
||||
)
|
||||
@@ -649,6 +653,8 @@ class BaseRunner(ABC):
|
||||
|
||||
forward_batch = mr.prepare_dummy_forward_batch(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():
|
||||
# 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 (
|
||||
AttentionGraphVariants,
|
||||
create_attention_graph_variants,
|
||||
create_dsv41_candidate_graph_variants,
|
||||
)
|
||||
from sglang.srt.layers.cp.utils import is_mla_cp_enabled
|
||||
from sglang.srt.layers.dp_attention import (
|
||||
@@ -300,6 +301,9 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
|
||||
|
||||
self.attention_graph_variants: Optional[AttentionGraphVariants] = (
|
||||
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 ---------------------------------------------
|
||||
@@ -965,7 +969,11 @@ class DecodeCudaGraphRunner(BaseCudaGraphRunner):
|
||||
spec_algorithm=self.model_runner.spec_algorithm,
|
||||
spec_info=spec_info,
|
||||
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,
|
||||
global_forward_mode=self.capture_forward_mode,
|
||||
lora_ids=lora_ids,
|
||||
|
||||
@@ -342,7 +342,7 @@ def maybe_flashinfer_autotune_speculative_draft(
|
||||
def maybe_flashinfer_autotune_extend(
|
||||
runner: BaseRunner, *, decode_num_tokens: int
|
||||
) -> 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
|
||||
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
|
||||
per-rank extend token count tunes all buckets up to it.
|
||||
"""
|
||||
if not envs.SGLANG_FLASHINFER_AUTOTUNE_EXTEND.get():
|
||||
return
|
||||
mr = runner.model_runner
|
||||
# Prefer the per-rank scheduler buffer while preserving the legacy ceiling
|
||||
# when chunked prefill is disabled.
|
||||
num_tokens = max_prefill_buffer_tokens() or get_schedule().max_prefill_tokens
|
||||
if num_tokens <= (decode_num_tokens or 0):
|
||||
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 = (
|
||||
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,
|
||||
ForwardBatch,
|
||||
ForwardMode,
|
||||
NgramEmbeddingInfo,
|
||||
PPProxyTensors,
|
||||
compute_local_num_token_non_padded,
|
||||
enable_num_token_non_padded,
|
||||
@@ -283,6 +284,8 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
|
||||
buffer population, attention metadata init, and output slicing.
|
||||
"""
|
||||
|
||||
_backend_can_run_prefill_cuda_graph = None
|
||||
|
||||
def __init__(self, model_runner: ModelRunner):
|
||||
if get_schedule().enable_mixed_chunk:
|
||||
backend = get_exec().graph.cuda_graph_config.prefill.backend
|
||||
@@ -291,6 +294,9 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
|
||||
f"graph backend; got '{backend}'."
|
||||
)
|
||||
super().__init__(model_runner)
|
||||
self._backend_can_run_prefill_cuda_graph = getattr(
|
||||
model_runner.attn_backend, "can_run_prefill_cuda_graph", None
|
||||
)
|
||||
# --- model flags ----------------------------------------------
|
||||
self.quant_config = getattr(model_runner.model, "quant_config", None)
|
||||
self.is_multimodal = model_runner.model_config.is_multimodal
|
||||
@@ -1332,6 +1338,9 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
|
||||
is None
|
||||
):
|
||||
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
|
||||
# layer_model.forward monkey-patch in replay(): the captured graph 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,
|
||||
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)
|
||||
return forward_batch, self.model_runner.attn_backend
|
||||
|
||||
@@ -1827,6 +1845,9 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
|
||||
),
|
||||
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:
|
||||
forward_batch.next_token_logits_buffer = (
|
||||
static_forward_batch.next_token_logits_buffer
|
||||
@@ -1901,6 +1922,26 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
|
||||
|
||||
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(
|
||||
self,
|
||||
forward_batch: ForwardBatch,
|
||||
@@ -1913,7 +1954,6 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
|
||||
# BCG / Full: replay the captured body, run the LM head +
|
||||
# logits_processor eagerly.
|
||||
full_path = self._is_full_backend
|
||||
ie_idx = self._input_embeds_arg_idx
|
||||
|
||||
def replay_layer_forward(*args, **layer_kwargs):
|
||||
# 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
|
||||
# current request's embeddings (mirrors main's BCG closure).
|
||||
if self.buffer_registry.has_slot("input_embeds"):
|
||||
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:
|
||||
# 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)
|
||||
self._fill_input_embeds_slot(args, layer_kwargs, static_num_tokens)
|
||||
hs = self.backend.replay(shape_key, static_forward_batch, **kwargs)
|
||||
return _slice_output_rows(hs, raw_num_tokens) if full_path else hs
|
||||
|
||||
|
||||
@@ -26,5 +26,5 @@ class ShapeKey:
|
||||
stream_idx: Optional[int] = None
|
||||
# LoRA or prefill-prefix variant; None selects the default.
|
||||
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
|
||||
|
||||
@@ -72,6 +72,13 @@ def get_capture_attention_variant() -> Optional[str]:
|
||||
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:
|
||||
global _capture_attention_variant
|
||||
_capture_attention_variant = variant
|
||||
|
||||
@@ -22,6 +22,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import contextmanager, nullcontext
|
||||
from functools import cached_property
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
import torch
|
||||
@@ -92,6 +93,7 @@ from sglang.srt.layers.moe import (
|
||||
get_moe_a2a_backend,
|
||||
get_moe_runner_backend,
|
||||
post_experts_all_reduce,
|
||||
should_skip_post_experts_all_reduce,
|
||||
should_use_flashinfer_cutlass_moe_fp4_allgather,
|
||||
)
|
||||
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,
|
||||
)
|
||||
from sglang.srt.layers.quantization.mxfp4_flashinfer_trtllm_moe import (
|
||||
Mxfp4FlashinferTrtllmMoEMethod,
|
||||
Mxfp8RoutedInputPreQuant,
|
||||
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.rotary_embedding import get_rope_wrapper
|
||||
@@ -180,6 +186,7 @@ from sglang.srt.models.deepseek_common.utils import (
|
||||
quant_blocks_shared_experts_fusion,
|
||||
tiny_router_gemm_max_tokens,
|
||||
)
|
||||
from sglang.srt.multimodal.dsv41.vl_routing import vision_topk
|
||||
from sglang.srt.runtime_context import (
|
||||
attention_backends,
|
||||
get_device,
|
||||
@@ -456,6 +463,7 @@ class MoEGate(nn.Module):
|
||||
prefix: str = "",
|
||||
is_hash_moe: bool = False,
|
||||
is_deepseek_v4: bool = False,
|
||||
vl_correction_bias: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self.is_deepseek_v4 = is_deepseek_v4
|
||||
@@ -488,6 +496,12 @@ class MoEGate(nn.Module):
|
||||
self.e_score_correction_bias = nn.Parameter(correction_bias)
|
||||
else:
|
||||
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:
|
||||
self.quant_method = PackWeightMethod(weight_names=["weight"])
|
||||
self.tiny_router_gemm_max_tokens = tiny_router_gemm_max_tokens(
|
||||
@@ -538,6 +552,11 @@ class MoEGate(nn.Module):
|
||||
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):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -546,8 +565,10 @@ class DeepseekV2MoE(nn.Module):
|
||||
quant_config: Optional[QuantizationConfig] = None,
|
||||
prefix: str = "",
|
||||
alt_stream: Optional[torch.cuda.Stream] = None,
|
||||
routed_quant_stream: Optional[torch.cuda.Stream] = None,
|
||||
is_nextn: bool = False,
|
||||
is_deepseek_v4: bool = False,
|
||||
vl_correction_bias: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self.tp_size = get_parallel().tp_size
|
||||
@@ -585,7 +606,14 @@ class DeepseekV2MoE(nn.Module):
|
||||
self.config = config
|
||||
self.layer_id = layer_id
|
||||
self.alt_stream = alt_stream
|
||||
self.routed_quant_stream = routed_quant_stream
|
||||
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)
|
||||
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),
|
||||
is_hash_moe=self.is_hash,
|
||||
is_deepseek_v4=is_deepseek_v4,
|
||||
vl_correction_bias=vl_correction_bias,
|
||||
)
|
||||
|
||||
# scaling factor for fused shared experts on AMD-platform.
|
||||
@@ -681,6 +710,12 @@ class DeepseekV2MoE(nn.Module):
|
||||
topk_kwargs.update(
|
||||
use_grouped_topk=False,
|
||||
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),
|
||||
apply_routed_scaling_factor_on_output=(
|
||||
True
|
||||
@@ -951,6 +986,13 @@ class DeepseekV2MoE(nn.Module):
|
||||
else self._maybe_quant_moe_input_once(hidden_states)
|
||||
)
|
||||
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 = (
|
||||
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
|
||||
else None
|
||||
)
|
||||
|
||||
# router_logits: (num_tokens, n_experts)
|
||||
router_logits = self.gate(hidden_states, gemm_output_zero_allocator)
|
||||
if use_flashinfer_trtllm_bypass:
|
||||
@@ -973,14 +1016,44 @@ class DeepseekV2MoE(nn.Module):
|
||||
if getattr(self, "is_hash", False)
|
||||
else {}
|
||||
)
|
||||
topk_output = self.topk(
|
||||
hidden_states,
|
||||
router_logits,
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
expert_location_dispatch_info=dispatch_info,
|
||||
**topk_kwargs,
|
||||
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(
|
||||
hidden_states,
|
||||
router_logits,
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
expert_location_dispatch_info=dispatch_info,
|
||||
**topk_kwargs,
|
||||
)
|
||||
# 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 = (
|
||||
)
|
||||
deferred_finalize = use_fused_finalize_all_reduce or (
|
||||
has_shared_output
|
||||
and not self._shared_expert_tp1
|
||||
and topk_output.format == TopKOutputFormat.BYPASSED
|
||||
@@ -988,13 +1061,13 @@ class DeepseekV2MoE(nn.Module):
|
||||
)
|
||||
if 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:
|
||||
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(
|
||||
hidden_states, topk_output, pre_quant_input=pre_quant_input
|
||||
hidden_states, topk_output, pre_quant_input=routed_pre_quant_input
|
||||
)
|
||||
else:
|
||||
final_hidden_states = self.experts(hidden_states, topk_output)
|
||||
@@ -1007,6 +1080,7 @@ class DeepseekV2MoE(nn.Module):
|
||||
final_hidden_states *= self.routed_scaling_factor
|
||||
|
||||
# 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):
|
||||
shared_output = self._forward_shared_experts(
|
||||
hidden_states,
|
||||
@@ -1014,17 +1088,84 @@ class DeepseekV2MoE(nn.Module):
|
||||
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)
|
||||
|
||||
all_reduce_done = False
|
||||
if deferred_finalize:
|
||||
from sglang.srt.layers.moe.moe_runner.flashinfer_trtllm import (
|
||||
finalize_flashinfer_trtllm_deferred_output,
|
||||
)
|
||||
|
||||
final_hidden_states = finalize_flashinfer_trtllm_deferred_output(
|
||||
final_hidden_states,
|
||||
shared_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(
|
||||
deferred,
|
||||
shared_output,
|
||||
)
|
||||
else:
|
||||
final_hidden_states = maybe_fuse_routed_scale_and_shared_add(
|
||||
self.experts,
|
||||
@@ -1033,7 +1174,8 @@ class DeepseekV2MoE(nn.Module):
|
||||
self.routed_scaling_factor,
|
||||
)
|
||||
|
||||
final_hidden_states = post_experts_all_reduce(final_hidden_states)
|
||||
if not all_reduce_done:
|
||||
final_hidden_states = post_experts_all_reduce(final_hidden_states)
|
||||
# TP1 shared experts are replicated, so add them after all-reduce to
|
||||
# avoid summing the same shared output once per TP rank.
|
||||
if self._shared_expert_tp1:
|
||||
@@ -1088,13 +1230,21 @@ class DeepseekV2MoE(nn.Module):
|
||||
if getattr(self, "is_hash", False)
|
||||
else {}
|
||||
)
|
||||
topk_output = self.topk(
|
||||
hidden_states,
|
||||
router_logits,
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
expert_location_dispatch_info=dispatch_info,
|
||||
**topk_kwargs,
|
||||
)
|
||||
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(
|
||||
hidden_states,
|
||||
router_logits,
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
expert_location_dispatch_info=dispatch_info,
|
||||
**topk_kwargs,
|
||||
)
|
||||
else:
|
||||
pre_quant_input = None
|
||||
shared_output = None
|
||||
@@ -1108,7 +1258,6 @@ class DeepseekV2MoE(nn.Module):
|
||||
def _pre_combine_hook(
|
||||
dispatcher: BaseDispatcher, combine_input: CombineInput
|
||||
):
|
||||
|
||||
nonlocal shared_output
|
||||
self.alt_stream.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(self.alt_stream):
|
||||
@@ -1341,7 +1490,6 @@ class DeepseekV2MoE(nn.Module):
|
||||
def _post_dispatch_hook(
|
||||
dispatcher: BaseDispatcher, dispatch_output: DispatchOutput
|
||||
):
|
||||
|
||||
combine_overlap_args, down_gemm_overlap_args, meta_overlap_args = (
|
||||
compute_overlap_args(dispatch_output, self.alt_stream)
|
||||
)
|
||||
@@ -1359,7 +1507,6 @@ class DeepseekV2MoE(nn.Module):
|
||||
def _pre_combine_hook(
|
||||
dispatcher: BaseDispatcher, combine_input: CombineInput
|
||||
):
|
||||
|
||||
nonlocal shared_output
|
||||
|
||||
if (
|
||||
@@ -1397,7 +1544,6 @@ class DeepseekV2MoE(nn.Module):
|
||||
def _post_dispatch_hook(
|
||||
dispatcher: BaseDispatcher, dispatch_output: DispatchOutput
|
||||
):
|
||||
|
||||
combine_overlap_args, down_gemm_overlap_args, meta_overlap_args = (
|
||||
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)
|
||||
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):
|
||||
if state.hidden_states_mlp_input.shape[0] > 0:
|
||||
# router_logits: (num_tokens, n_experts)
|
||||
|
||||
+1520
-105
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
|
||||
@@ -17,6 +18,7 @@ from sglang.kernels.ops.speculative.dspark.dspark_draft_model import (
|
||||
CommitKvProj,
|
||||
)
|
||||
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.layers.dp_attention import is_dp_attention_enabled
|
||||
from sglang.srt.layers.layernorm import RMSNorm
|
||||
@@ -39,6 +41,7 @@ from sglang.srt.models.deepseek_v4 import (
|
||||
DeepseekV4DecoderLayer,
|
||||
DeepseekV4ForCausalLM,
|
||||
MqaAttentionBase,
|
||||
_apply_wo_a_bf16_matmul,
|
||||
_dequant_fp8_wo_a_streaming,
|
||||
hc_head_torch,
|
||||
make_hc_head_params,
|
||||
@@ -189,6 +192,22 @@ class DSparkAttention(MqaAttentionBase):
|
||||
q = self.q_norm(q)
|
||||
q, _ = self.wq_b(q)
|
||||
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 q_out is None:
|
||||
q_out = torch.empty_like(q)
|
||||
@@ -236,7 +255,6 @@ class DSparkAttention(MqaAttentionBase):
|
||||
hidden_states: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
) -> torch.Tensor:
|
||||
|
||||
if _is_npu and forward_batch.forward_mode.is_idle():
|
||||
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)
|
||||
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:
|
||||
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]))
|
||||
@@ -363,10 +387,13 @@ class MarkovW2ShardGeometry(msgspec.Struct, frozen=True):
|
||||
class DSparkV4MarkovHead(nn.Module):
|
||||
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__()
|
||||
self.vocab_size = int(vocab_size)
|
||||
self.markov_rank = int(markov_rank)
|
||||
self._is_dsv41 = bool(is_dsv41)
|
||||
if self.markov_rank <= 0:
|
||||
raise ValueError(
|
||||
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."
|
||||
)
|
||||
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(
|
||||
tp_size=tp_size,
|
||||
org_vocab_start=int(lm_head.shard_indices.org_vocab_start_index),
|
||||
@@ -469,13 +505,40 @@ class DSparkV4MarkovHead(nn.Module):
|
||||
else:
|
||||
bias = F.linear(latent.float(), weight_local)
|
||||
step_local = BuildStepLocal.execute(bias=bias, base_local=base_local)
|
||||
if shard.tp_size > 1:
|
||||
assert self._shard_group is not None
|
||||
full = self._shard_group.all_gather(step_local, dim=-1)
|
||||
else:
|
||||
full = step_local
|
||||
full = self._vocab_gather(step_local)
|
||||
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]:
|
||||
embed = self.get_prev_embeddings(token_ids)
|
||||
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):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -538,14 +617,18 @@ class DSparkV4Stage(DeepseekV4DecoderLayer):
|
||||
quant_config: Optional[QuantizationConfig] = None,
|
||||
prefix: str = "",
|
||||
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:
|
||||
super().__init__(
|
||||
config=config,
|
||||
config=_dspark_stage_config(config),
|
||||
layer_id=layer_id,
|
||||
quant_config=quant_config,
|
||||
prefix=prefix,
|
||||
is_nextn=True,
|
||||
alt_streams=alt_streams,
|
||||
hc_stats_stream=hc_stats_stream,
|
||||
moe_routed_quant_stream=moe_routed_quant_stream,
|
||||
)
|
||||
self.stage_id = stage_id
|
||||
self.dim = config.hidden_size
|
||||
@@ -566,11 +649,16 @@ class DSparkV4Stage(DeepseekV4DecoderLayer):
|
||||
|
||||
if stage_id == num_stages - 1:
|
||||
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
(
|
||||
self.hc_head_fn,
|
||||
self.hc_head_base,
|
||||
self.hc_head_scale,
|
||||
) = make_hc_head_params(config.hc_mult, config.hidden_size)
|
||||
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_base,
|
||||
self.hc_head_scale,
|
||||
) = make_hc_head_params(config.hc_mult, config.hidden_size)
|
||||
|
||||
def _build_self_attn(
|
||||
self,
|
||||
@@ -615,7 +703,12 @@ class DSparkV4Stage(DeepseekV4DecoderLayer):
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
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
|
||||
x, post, comb = self._hc_pre_block(
|
||||
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._run_ffn(x, forward_batch)
|
||||
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:
|
||||
shape = x.shape
|
||||
@@ -714,6 +849,21 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
|
||||
self.alt_streams: Optional[List[torch.cuda.Stream]] = (
|
||||
[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(
|
||||
[
|
||||
DSparkV4Stage(
|
||||
@@ -725,6 +875,8 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
|
||||
quant_config=quant_config,
|
||||
prefix=add_prefix(f"stages.{stage_id}", prefix),
|
||||
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)
|
||||
]
|
||||
@@ -732,6 +884,7 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
|
||||
self.markov_head = DSparkV4MarkovHead(
|
||||
vocab_size=int(config.vocab_size),
|
||||
markov_rank=int(dspark_config.markov_rank),
|
||||
is_dsv41=getattr(config, "model_type", None) == "deepseek_v41",
|
||||
)
|
||||
self.confidence_head = build_dspark_v4_confidence_head(
|
||||
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.norm_eps = float(config.rms_norm_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:
|
||||
self.embed_tokens = VocabParallelEmbedding(
|
||||
@@ -791,6 +947,12 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
|
||||
kvs = CommitKvProj.execute(
|
||||
main_x=main_x,
|
||||
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
|
||||
# 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:
|
||||
input_embeds = self.forward_embed(input_ids)
|
||||
x = input_embeds
|
||||
pre = None
|
||||
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)
|
||||
|
||||
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]
|
||||
return hc_head_torch(
|
||||
x,
|
||||
@@ -853,7 +1023,6 @@ class DeepseekV4ForCausalLMDSpark(nn.Module):
|
||||
)
|
||||
|
||||
def compute_base_logits(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
|
||||
x_post_hc = self.collapse_hc_head(x)
|
||||
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]
|
||||
|
||||
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.') :]}"
|
||||
|
||||
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(".w3.", ".up_proj.")
|
||||
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(".scale", ".weight_scale_inv")
|
||||
return f"stages.{stage_id}.{mapped_rest}"
|
||||
|
||||
@@ -35,6 +35,7 @@ from sglang.srt.models.deepseek_v4 import (
|
||||
DeepseekV4DecoderLayer,
|
||||
DeepseekV4ForCausalLM,
|
||||
_is_npu,
|
||||
wo_a_fp8_gemm_enabled,
|
||||
)
|
||||
from sglang.srt.runtime_context import get_parallel
|
||||
from sglang.srt.utils import add_prefix
|
||||
@@ -220,6 +221,7 @@ class DeepseekV4ForCausalLMNextN(DeepseekV4ForCausalLM):
|
||||
self.tp_size = get_parallel().tp_size
|
||||
self.pp_group = get_pp_group()
|
||||
self.quant_config = quant_config
|
||||
self.wo_a_fp8 = wo_a_fp8_gemm_enabled(quant_config)
|
||||
self.determine_num_fused_shared_experts()
|
||||
|
||||
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
|
||||
# batch.seq_lens_cpu to the target-attention KV lengths.
|
||||
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):
|
||||
super().__init__(spec_input_type=SpecInputType.DFLASH_VERIFY)
|
||||
|
||||
@@ -57,9 +57,6 @@ class DsparkDraftSampler:
|
||||
self.sample_from_anchor = bool(model.sample_from_anchor)
|
||||
self.query_token_num = self.gamma if self.sample_from_anchor else self.gamma + 1
|
||||
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:
|
||||
assert out.shape == (max_bs * self.gamma,) and out.dtype == torch.int64
|
||||
self.out = out
|
||||
@@ -129,11 +126,11 @@ class DsparkDraftSampler:
|
||||
# Gated/RNN subclasses return None (hidden-state-dependent bias); fall
|
||||
# through to the block sampler below.
|
||||
draft_tokens = None
|
||||
if (
|
||||
not self.folded_sampling
|
||||
and self._fused_greedy
|
||||
fused_greedy = getattr(self.markov_head, "supports_sharded_greedy", False) or (
|
||||
envs.SGLANG_DSPARK_OPT_FUSED_GREEDY_MARKOV.get()
|
||||
and isinstance(self.markov_head, VanillaMarkov)
|
||||
):
|
||||
)
|
||||
if not self.folded_sampling and fused_greedy:
|
||||
draft_tokens = self.markov_head.sample_block_greedy_fused(
|
||||
base_logits, first_prev_tokens=anchor
|
||||
)
|
||||
@@ -198,6 +195,9 @@ def _resolve_folded_sampling(
|
||||
return False
|
||||
if mode == DsparkFoldedSampling.FORCE:
|
||||
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)
|
||||
noise_bytes = max_bs * vocab * 4
|
||||
logits_bytes = max_bs * gamma * vocab * _base_logits_dtype(model).itemsize
|
||||
|
||||
@@ -121,6 +121,24 @@ class TargetHiddenKvInjector:
|
||||
state_slot=state_slot,
|
||||
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:
|
||||
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:
|
||||
|
||||
@@ -78,6 +78,30 @@ class TargetVerifyResult(msgspec.Struct, frozen=True):
|
||||
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:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -92,6 +116,15 @@ class TargetVerifyExecutor:
|
||||
simulate_acc_len: float = 0.0,
|
||||
) -> None:
|
||||
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.verify_num_draft_tokens = verify_num_draft_tokens
|
||||
self.model_runner = model_runner
|
||||
@@ -135,6 +168,7 @@ class TargetVerifyExecutor:
|
||||
gamma=self.gamma,
|
||||
verify_num_draft_tokens=self.verify_num_draft_tokens,
|
||||
cutoff_layout=layout,
|
||||
fused_argmax=self._target_is_dsv41,
|
||||
)
|
||||
if self._simulate_acc_len > 0:
|
||||
correct_len = self._simulated_correct_len(
|
||||
@@ -296,6 +330,10 @@ class TargetVerifyExecutor:
|
||||
seq_lens_cpu_backup,
|
||||
seq_lens_sum_backup,
|
||||
) -> 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(
|
||||
batch, self.target_worker
|
||||
)
|
||||
@@ -479,6 +517,7 @@ class CommitInjectCtx(msgspec.Struct):
|
||||
block_pos_offsets: torch.Tensor
|
||||
resolve_pool: object
|
||||
resolve_req_to_token: object
|
||||
kv_injector: Optional[TargetHiddenKvInjector] = None
|
||||
|
||||
|
||||
class AcceptOuts(msgspec.Struct):
|
||||
@@ -499,9 +538,11 @@ class DsparkVerifyEpilogue:
|
||||
device,
|
||||
tp_sync: SpecTpSync,
|
||||
commit_ctx: Optional[CommitInjectCtx] = None,
|
||||
fused_argmax: bool = False,
|
||||
) -> None:
|
||||
self.max_bs = int(max_bs)
|
||||
self.stride = int(verify_num_draft_tokens)
|
||||
self._fused_argmax = bool(fused_argmax)
|
||||
self.gamma = self.stride - 1
|
||||
self.commit_ctx = commit_ctx
|
||||
self._tp_sync = tp_sync
|
||||
@@ -530,9 +571,13 @@ class DsparkVerifyEpilogue:
|
||||
)
|
||||
self.strided_logits: 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:
|
||||
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
|
||||
if (
|
||||
not isinstance(out, LogitsProcessorOutput)
|
||||
@@ -540,6 +585,9 @@ class DsparkVerifyEpilogue:
|
||||
or out.hidden_states is None
|
||||
):
|
||||
return
|
||||
if not runner.ragged_verify_mode:
|
||||
self._static_epilogue(out, forward_batch)
|
||||
return
|
||||
self(
|
||||
compact_logits=out.next_token_logits,
|
||||
compact_hidden=out.hidden_states,
|
||||
@@ -550,6 +598,7 @@ class DsparkVerifyEpilogue:
|
||||
)
|
||||
|
||||
def begin_step(self, verify_lens, armed: bool) -> None:
|
||||
self._static_step_state = None
|
||||
if verify_lens is None:
|
||||
self.verify_lens_buf.zero_()
|
||||
else:
|
||||
@@ -559,6 +608,49 @@ class DsparkVerifyEpilogue:
|
||||
self.verify_lens_buf[bs:].zero_()
|
||||
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:
|
||||
return AcceptOuts(
|
||||
correct_len=self.correct_len_buf[:bs],
|
||||
@@ -610,7 +702,23 @@ class DsparkVerifyEpilogue:
|
||||
self.strided_hidden = self._ensure_out(self.strided_hidden, compact_hidden)
|
||||
verify_lens = self.verify_lens_buf[: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:
|
||||
self._commit_inject(
|
||||
commit_lens, verify_lens, seq_lens, req_pool_indices, bs
|
||||
@@ -632,22 +740,16 @@ class DsparkVerifyEpilogue:
|
||||
fill_value=0.0,
|
||||
)
|
||||
|
||||
def _accept(self, input_ids, seq_lens, verify_lens, bs: int) -> torch.Tensor:
|
||||
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,
|
||||
)
|
||||
def _accept(
|
||||
self, *, candidates, logits, draft_tokens, seq_lens, cutoff_verify_lens=None
|
||||
) -> torch.Tensor:
|
||||
bs = candidates.shape[0]
|
||||
correct_len, bonus, cap_trim_lens = accept_greedy_triton(
|
||||
candidates=candidates.view(bs, self.stride),
|
||||
target_logits=self.strided_logits[: bs * self.stride],
|
||||
candidates=candidates,
|
||||
target_logits=logits,
|
||||
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, bonus)
|
||||
@@ -658,7 +760,7 @@ class DsparkVerifyEpilogue:
|
||||
prefix_lens=seq_lens[:bs],
|
||||
)
|
||||
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,
|
||||
bonus=bonus,
|
||||
verify_num_draft_tokens=self.stride,
|
||||
@@ -719,6 +821,7 @@ def accept_draft_tokens(
|
||||
gamma: int,
|
||||
verify_num_draft_tokens: int,
|
||||
cutoff_layout: Optional[RaggedVerifyLayout] = None,
|
||||
fused_argmax: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
greedy_mask = draft_block.greedy_mask
|
||||
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,
|
||||
verify_num_draft_tokens=verify_num_draft_tokens,
|
||||
cutoff_verify_lens=cutoff_verify_lens,
|
||||
fused_argmax=fused_argmax,
|
||||
)
|
||||
bs, gamma_rows, vocab = draft_block.corrected_logits.shape
|
||||
draft_probs = SoftmaxTemp.execute(
|
||||
@@ -753,6 +857,7 @@ def accept_draft_tokens(
|
||||
target_logits=target_logits,
|
||||
verify_num_draft_tokens=verify_num_draft_tokens,
|
||||
cutoff_verify_lens=cutoff_verify_lens,
|
||||
fused_argmax=fused_argmax,
|
||||
)
|
||||
sampling_len, sampling_bonus, sampling_trim = AcceptSampling.execute(
|
||||
candidates=candidates,
|
||||
|
||||
@@ -304,8 +304,23 @@ class DSparkWorkerV2(BaseSpecWorker):
|
||||
dp_moe_sync=self._draft_is_moe and get_parallel().enable_dp_attention,
|
||||
)
|
||||
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 (
|
||||
self._verify_planner.is_compact_mode
|
||||
(self._verify_planner.is_compact_mode or static_epilogue_supported)
|
||||
and self._decode_graph_allowed
|
||||
and is_cuda()
|
||||
):
|
||||
@@ -314,6 +329,7 @@ class DSparkWorkerV2(BaseSpecWorker):
|
||||
verify_num_draft_tokens=self.verify_num_draft_tokens,
|
||||
device=self.device,
|
||||
tp_sync=self._tp_sync,
|
||||
fused_argmax=target_is_dsv41,
|
||||
commit_ctx=CommitInjectCtx(
|
||||
draft_model=self.draft_model,
|
||||
block_pos_offsets=self._block_pos_offsets,
|
||||
@@ -321,6 +337,7 @@ class DSparkWorkerV2(BaseSpecWorker):
|
||||
resolve_req_to_token=lambda: (
|
||||
self.model_runner.req_to_token_pool.req_to_token
|
||||
),
|
||||
kv_injector=self._kv_injector,
|
||||
),
|
||||
)
|
||||
self.model_runner.capture_tail_hooks.append(
|
||||
@@ -512,7 +529,11 @@ class DSparkWorkerV2(BaseSpecWorker):
|
||||
batch: ScheduleBatch,
|
||||
on_publish=None,
|
||||
grammar_barrier=None,
|
||||
*,
|
||||
pp_proxy_tensors=None,
|
||||
) -> 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:
|
||||
self._verify_planner.note_non_decode_step()
|
||||
self._observers.note_prefill_step()
|
||||
@@ -590,9 +611,17 @@ class DSparkWorkerV2(BaseSpecWorker):
|
||||
final_pos = torch.repeat_interleave(
|
||||
(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(
|
||||
target_hidden=logits_output.hidden_states,
|
||||
cache_loc=batch.out_cache_loc,
|
||||
cache_loc=cache_loc,
|
||||
positions=positions,
|
||||
state_slot=state_slot,
|
||||
final_pos=final_pos,
|
||||
@@ -600,6 +629,7 @@ class DSparkWorkerV2(BaseSpecWorker):
|
||||
)
|
||||
# Avoid copying large hidden-state buffers to CPU in overlap scheduling.
|
||||
logits_output.hidden_states = None
|
||||
logits_output.hidden_states_token_indices = None
|
||||
|
||||
batch_output.next_draft_input = make_next_draft_input(
|
||||
bonus_tokens=next_token_ids,
|
||||
@@ -780,6 +810,11 @@ class DSparkWorkerV2(BaseSpecWorker):
|
||||
inject_gate=fold_eligible,
|
||||
)
|
||||
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(
|
||||
batch=batch,
|
||||
draft_input=draft_input,
|
||||
@@ -804,7 +839,11 @@ class DSparkWorkerV2(BaseSpecWorker):
|
||||
grammar_mask.apply(logits_output.next_token_logits)
|
||||
|
||||
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(
|
||||
folded_accept=folded_accept,
|
||||
bs=bs,
|
||||
@@ -817,6 +856,11 @@ class DSparkWorkerV2(BaseSpecWorker):
|
||||
prefix_lens=prefix_lens,
|
||||
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:
|
||||
compute_spec_logprobs(
|
||||
batch,
|
||||
|
||||
@@ -91,6 +91,7 @@ from sglang.srt.configs import (
|
||||
XllmConfig,
|
||||
)
|
||||
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.utils import get_bool_env_var, logger, lru_cache_frozenset
|
||||
from sglang.srt.utils.runai_utils import ObjectStorageModel, is_runai_obj_uri
|
||||
@@ -192,9 +193,28 @@ try:
|
||||
|
||||
class _DeepseekV4ConfigAlias(_HFDeepseekV3Config):
|
||||
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_v4"] = _DeepseekV4ConfigAlias
|
||||
_CONFIG_REGISTRY.update(
|
||||
{cls.model_type: cls for cls in DEEPSEEK_V41_CONFIG_CLASSES}
|
||||
)
|
||||
|
||||
# For kimi_k25_eagle3
|
||||
class _KimiK2ConfigAlias(_HFDeepseekV3Config):
|
||||
|
||||
@@ -19,6 +19,10 @@ from typing import Optional
|
||||
from transformers import PretrainedConfig
|
||||
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 (
|
||||
ModelConfigParserBase,
|
||||
get_model_config_parser,
|
||||
@@ -179,6 +183,8 @@ class HfModelConfigParser(ModelConfigParserBase):
|
||||
_set_architectures(config, "DeepseekOCRForCausalLM")
|
||||
config = DeepseekVLV2Config.from_pretrained(model, revision=revision)
|
||||
_apply_deepseek_ocr_overrides(config, model)
|
||||
elif isinstance(config, DeepseekV41Config):
|
||||
config._name_or_path = model
|
||||
elif config.model_type in _CONFIG_REGISTRY:
|
||||
model_type = config.model_type
|
||||
if model_type == "deepseek_vl_v2" and is_ocr:
|
||||
@@ -315,6 +321,8 @@ def get_config(
|
||||
)
|
||||
|
||||
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
|
||||
# config, so '{"text_config": {...}}' on a VLM would replace the whole
|
||||
# sub-config with a dict and break attribute access downstream.
|
||||
|
||||
@@ -282,6 +282,10 @@ class TinyDSV4ModelConfig:
|
||||
index_topk=DSV4_INDEX_TOPK,
|
||||
num_hidden_layers=len(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_text_config = self.hf_config
|
||||
@@ -411,6 +415,10 @@ class MockDSV4ModelRunner:
|
||||
device=device,
|
||||
enable_memory_saver=False,
|
||||
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.
|
||||
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
|
||||
# so `_pure_torch_dsv4_combined_reference` knows which one to use.
|
||||
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)
|
||||
|
||||
|
||||
def _seed_c4_if_needed(
|
||||
fixture: DSV4AttentionFixture, *, num_entries: int = _DSV4_EXTRA_ENTRIES
|
||||
fixture: DSV4AttentionFixture, *, num_entries: int | None = None
|
||||
) -> None:
|
||||
"""For compress_ratio=4, seed the C4 metadata the exercised path consumes
|
||||
(the C4Indexer would normally populate it; the compact fixture skips the
|
||||
indexer): `c4_sparse_page_indices` for the dense extend path,
|
||||
`c4_sparse_raw_indices` for sparse prefill. No-op for other compress_ratios.
|
||||
"""Seed `c4_sparse_page_indices` (dense extend) or `c4_sparse_raw_indices`
|
||||
(sparse prefill); the compact fixture skips the indexer that fills them.
|
||||
"""
|
||||
if fixture.case.compress_ratio != 4:
|
||||
if fixture.case.compress_ratio not in (1, 2, 4):
|
||||
return
|
||||
if num_entries is None:
|
||||
num_entries = getattr(fixture, "extra_entries", _DSV4_EXTRA_ENTRIES)
|
||||
if fixture.seed_c4_for_sparse_prefill:
|
||||
_seed_c4_sparse_prefill_indices(fixture, num_entries=num_entries)
|
||||
else:
|
||||
@@ -1138,7 +1146,7 @@ def run_dsv4_fixture_eager(fixture: DSV4AttentionFixture) -> torch.Tensor:
|
||||
full_kv_locs_per_req = _populate_swa_kv_cache(
|
||||
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)
|
||||
q_input, _ = fixture.actual_module.project(fixture.input_hidden)
|
||||
with torch.no_grad(), forward_context(ForwardContext(attn_backend=fixture.backend)):
|
||||
@@ -1202,7 +1210,7 @@ def expected_dsv4_output_from_inputs(
|
||||
runner = fixture.runner
|
||||
max_context_len = runner.req_to_token_pool.req_to_token.shape[1]
|
||||
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()
|
||||
full_kv_locs_per_req = _full_kv_locs_per_req(
|
||||
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_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(
|
||||
md, case.compress_ratio
|
||||
)
|
||||
@@ -1443,9 +1451,9 @@ def _seed_c4_sparse_prefill_indices(
|
||||
lens = (md.positions_casual + 1) // ratio
|
||||
max_len = int(lens.max().item())
|
||||
pool = fixture.runner.token_to_kv_pool
|
||||
c4_page_size = pool.get_extra_key_page_size(layer_id=0)
|
||||
assert max_len <= min(num_entries, c4_page_size), (
|
||||
f"case attends {max_len} c4 entries; only {min(num_entries, c4_page_size)} populated"
|
||||
c_page_size = pool.get_extra_key_page_size(layer_id=0)
|
||||
assert max_len <= min(num_entries, c_page_size), (
|
||||
f"case attends {max_len} c{ratio} entries; only {min(num_entries, c_page_size)} populated"
|
||||
)
|
||||
assert (md.page_table[:, 0] == 0).all(), (
|
||||
"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)
|
||||
|
||||
_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)
|
||||
|
||||
_prepare_target_verify_batch(fixture.forward_batch, case, device)
|
||||
@@ -1614,28 +1622,21 @@ def run_dsv4_compress_attention_case(
|
||||
dtype: torch.dtype = torch.bfloat16,
|
||||
device: str = "cuda",
|
||||
) -> None:
|
||||
"""Math-faithful test for the SWA + C4 (compress_ratio=4) / SWA + C128
|
||||
(compress_ratio=128) path through `DeepseekV4AttnBackend.forward`.
|
||||
|
||||
Pre-writes random packed K into both the SWA cache and the extra
|
||||
(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.
|
||||
"""SWA + compressed-cache path (compress ratios 1, 2, 4, 128) through
|
||||
`DeepseekV4AttnBackend.forward` against a pure-PyTorch reference that reads the
|
||||
same cache bytes and metadata indices. `sparse_prefill` pins
|
||||
`SGLANG_OPT_FLASHMLA_SPARSE_PREFILL`; the C4 seeding dispatches on the same flag.
|
||||
"""
|
||||
assert case.compress_ratio in (
|
||||
4,
|
||||
128,
|
||||
), (
|
||||
f"DSV4 compact runner requires compress_ratio in (4, 128); got {case.compress_ratio}"
|
||||
assert case.compress_ratio in (1, 2, 4, 128), (
|
||||
f"DSV4 compact runner requires compress_ratio in (1, 2, 4, 128); "
|
||||
f"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:
|
||||
assert case.forward_mode.is_extend_without_speculative(), (
|
||||
f"sparse prefill only serves extend; got {case.forward_mode}"
|
||||
@@ -1648,6 +1649,7 @@ def run_dsv4_compress_attention_case(
|
||||
compression_ratios=[case.compress_ratio],
|
||||
)
|
||||
fixture.seed_c4_for_sparse_prefill = sparse_prefill
|
||||
fixture.extra_entries = extra_entries # type: ignore[attr-defined]
|
||||
runner = fixture.runner
|
||||
max_context_len = runner.req_to_token_pool.req_to_token.shape[1]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user