[KDA] Support ReplaySSM ring-write in the fused chain-verify kernel (#36821)
Co-authored-by: luoyuan.luo <luoyuan.luo@antgroup.com>
This commit is contained in:
@@ -16,11 +16,17 @@ kernels. Requires ``T >= kernel_width - 1`` (the rolled conv state is then
|
||||
exactly the last ``kernel_width - 1`` input tokens, matching the reference
|
||||
kernel's store).
|
||||
|
||||
Numerics: deliberately bit-aligned with the unfused pair. The conv output is
|
||||
rounded to the activation dtype (bf16) before entering the recurrence —
|
||||
exactly what the unfused path does through its intermediate tensor — and all
|
||||
expressions mirror the reference kernels line by line, with the same
|
||||
num_warps so reduction order matches.
|
||||
ReplaySSM (``cache_ring``): instead of per-step [HV, V, K] fp32 state
|
||||
snapshots, stash each step's raw inputs (pre-l2norm k, pre-delta v, gate,
|
||||
beta) into the per-slot rings the commit-time exact fold replays
|
||||
(kda_replayssm_spec_decode.py) -- same CACHE_RING contract as the unfused
|
||||
fused_sigmoid_gating_delta_rule_update, so the two paths fill identical rings.
|
||||
|
||||
Numerics: aligned with the unfused pair. The conv output is rounded to the
|
||||
activation dtype (bf16) before entering the recurrence — exactly what the
|
||||
unfused path does through its intermediate tensor — and all expressions mirror
|
||||
the reference kernels line by line. Reduction order still splits differently
|
||||
where many V heads share one Q/K head, worth ~1 ulp on the output.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
@@ -83,6 +89,18 @@ def fused_kda_conv_gating_verify_kernel(
|
||||
SAVE_INTERMEDIATE_WINDOW: tl.constexpr,
|
||||
CACHE_INTERMEDIATE_STATES: tl.constexpr,
|
||||
USE_GDC: tl.constexpr = False,
|
||||
# ReplaySSM fused ring-write (spec verify): per-slot rings consumed by the
|
||||
# commit-time exact fold (kda_replayssm_spec_decode.py). Off -> dead code.
|
||||
replayssm_rawv=None, # [slots, HV, L, V] activation dtype
|
||||
replayssm_rawk=None, # [slots, H, L, K] activation dtype
|
||||
replayssm_g=None, # [slots, HV, L, K] fp32
|
||||
replayssm_beta=None, # [slots, HV, L] fp32
|
||||
stride_rawv_slot: tl.constexpr = 0,
|
||||
stride_rawk_slot: tl.constexpr = 0,
|
||||
stride_g_slot: tl.constexpr = 0,
|
||||
stride_beta_slot: tl.constexpr = 0,
|
||||
MAX_CACHE_LEN: tl.constexpr = 0,
|
||||
CACHE_RING: tl.constexpr = False,
|
||||
):
|
||||
# PDL: overlap prologue with the tail of the producer qkv-projection GEMM;
|
||||
# every global load (conv_state_indices, mixed_qkv, weights) happens after
|
||||
@@ -289,6 +307,53 @@ def fused_kda_conv_gating_verify_kernel(
|
||||
|
||||
b_beta = 1.0 / (1.0 + tl.exp(-b_b))
|
||||
|
||||
# ReplaySSM ring-write. Must sit here: b_k still pre-l2norm, b_v still
|
||||
# pre-delta, b_g/b_beta formed -- so the commit fold's replay is
|
||||
# bit-identical to the update below (mirrors the CACHE_RING block in
|
||||
# fused_sigmoid_gating_recurrent.py). rawk dedups via is_qk_owner
|
||||
# (per k-head); g/beta write once per v-head at i_v == 0. The
|
||||
# t < MAX_CACHE_LEN guard drops absorb-overflow steps instead of
|
||||
# smashing the next slot's ring.
|
||||
if CACHE_RING:
|
||||
if h0_idx >= 0 and t < MAX_CACHE_LEN:
|
||||
ring_slot = h0_idx.to(tl.int64)
|
||||
tl.store(
|
||||
replayssm_rawv
|
||||
+ ring_slot * stride_rawv_slot
|
||||
+ i_hv * MAX_CACHE_LEN * V
|
||||
+ t * V
|
||||
+ o_v,
|
||||
b_v.to(replayssm_rawv.dtype.element_ty),
|
||||
mask=mask_v,
|
||||
)
|
||||
if is_qk_owner:
|
||||
tl.store(
|
||||
replayssm_rawk
|
||||
+ ring_slot * stride_rawk_slot
|
||||
+ i_h * MAX_CACHE_LEN * K
|
||||
+ t * K
|
||||
+ o_k,
|
||||
b_k.to(replayssm_rawk.dtype.element_ty),
|
||||
mask=mask_k,
|
||||
)
|
||||
if i_v == 0:
|
||||
tl.store(
|
||||
replayssm_g
|
||||
+ ring_slot * stride_g_slot
|
||||
+ i_hv * MAX_CACHE_LEN * K
|
||||
+ t * K
|
||||
+ o_k,
|
||||
b_g,
|
||||
mask=mask_k,
|
||||
)
|
||||
tl.store(
|
||||
replayssm_beta
|
||||
+ ring_slot * stride_beta_slot
|
||||
+ i_hv * MAX_CACHE_LEN
|
||||
+ t,
|
||||
b_beta,
|
||||
)
|
||||
|
||||
if USE_QK_L2NORM_IN_KERNEL:
|
||||
b_q = b_q / (tl.sqrt(tl.sum(b_q * b_q) + 1e-6))
|
||||
b_k = b_k / (tl.sqrt(tl.sum(b_k * b_k) + 1e-6))
|
||||
@@ -358,15 +423,23 @@ def fused_kda_conv_gating_verify(
|
||||
softplus_beta: float = 1.0,
|
||||
softplus_threshold: float = 20.0,
|
||||
use_qk_l2norm_in_kernel: bool = True,
|
||||
# num_warps=4 is ~1.3x faster than the unfused pair in-graph; the output,
|
||||
# conv_state and conv-window caches stay bit-identical to the reference.
|
||||
# Only the fp32 intermediate-ssm rollback cache differs: the tl.sum
|
||||
# reduction-order delta (~1 ulp/step) compounds through the delta-rule
|
||||
# recurrence — measured ~6e-8 at T=4 standard gate (the production MTP
|
||||
# shape), ~1.5e-5 at T=4 safe gate, ~2e-3 at T=8 safe gate. num_warps=1
|
||||
# reproduces the reference reduction order exactly (all buffers
|
||||
# bit-identical) but is ~2.4x slower in-graph — numerics debugging only.
|
||||
# num_warps=4 is ~1.3x faster than the unfused pair in-graph; conv_state
|
||||
# and the conv-window cache stay bit-identical to the reference, the bf16
|
||||
# output within one ulp (the BV=4 tile reduces K in a different order).
|
||||
# The fp32 intermediate-ssm rollback cache carries that ~1 ulp/step delta
|
||||
# through the delta-rule recurrence — measured ~6e-8 at T=4 standard gate
|
||||
# (the production MTP shape), ~1.5e-5 at T=4 safe gate, ~2e-3 at T=8 safe
|
||||
# gate. num_warps=1 is ~2.4x slower in-graph — numerics debugging only.
|
||||
# The ReplaySSM ring values are bit-exact at any num_warps: they are
|
||||
# elementwise (conv FMA chain, gate, sigmoid), upstream of every tl.sum.
|
||||
num_warps: int = 4,
|
||||
# ReplaySSM fused ring-write; same parameter names as the unfused
|
||||
# fused_sigmoid_gating_delta_rule_update so ring_kwargs pass through both.
|
||||
cache_ring: bool = False,
|
||||
replayssm_rawv: Optional[torch.Tensor] = None,
|
||||
replayssm_rawk: Optional[torch.Tensor] = None,
|
||||
replayssm_g: Optional[torch.Tensor] = None,
|
||||
replayssm_beta: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
"""Chain-verify fast path. Returns ``o`` of shape [1, seq_len, HV, V],
|
||||
matching the unfused ``target_verify`` output layout."""
|
||||
@@ -420,6 +493,37 @@ def fused_kda_conv_gating_verify(
|
||||
if intermediate_states_buffer is not None:
|
||||
assert intermediate_states_buffer.is_contiguous()
|
||||
|
||||
if cache_ring:
|
||||
# Per-layer ring views (memory_pool.py KDA spec rings). The kernel uses
|
||||
# stride(0) as the slot pitch and packs within a slot from
|
||||
# MAX_CACHE_LEN and the head/dim extents, so inner dims must be packed.
|
||||
assert (
|
||||
replayssm_rawv is not None
|
||||
and replayssm_rawk is not None
|
||||
and replayssm_g is not None
|
||||
and replayssm_beta is not None
|
||||
), "cache_ring requires all four replayssm_* rings"
|
||||
max_cache_len = replayssm_rawv.shape[-2]
|
||||
assert tuple(replayssm_rawv.shape[1:]) == (HV, max_cache_len, V)
|
||||
assert tuple(replayssm_rawk.shape[1:]) == (H, max_cache_len, K)
|
||||
assert tuple(replayssm_g.shape[1:]) == (HV, max_cache_len, K)
|
||||
assert tuple(replayssm_beta.shape[1:]) == (HV, max_cache_len)
|
||||
assert replayssm_rawv.stride()[1:] == (max_cache_len * V, V, 1)
|
||||
assert replayssm_rawk.stride()[1:] == (max_cache_len * K, K, 1)
|
||||
assert replayssm_g.stride()[1:] == (max_cache_len * K, K, 1)
|
||||
assert replayssm_beta.stride()[1:] == (max_cache_len, 1)
|
||||
assert replayssm_rawv.dtype == mixed_qkv.dtype
|
||||
assert replayssm_rawk.dtype == mixed_qkv.dtype
|
||||
assert replayssm_g.dtype == torch.float32
|
||||
assert replayssm_beta.dtype == torch.float32
|
||||
stride_rawv_slot = replayssm_rawv.stride(0)
|
||||
stride_rawk_slot = replayssm_rawk.stride(0)
|
||||
stride_g_slot = replayssm_g.stride(0)
|
||||
stride_beta_slot = replayssm_beta.stride(0)
|
||||
else:
|
||||
max_cache_len = 0
|
||||
stride_rawv_slot = stride_rawk_slot = stride_g_slot = stride_beta_slot = 0
|
||||
|
||||
grid = (NV, B * HV)
|
||||
# PDL (sm90+): chain behind the producer qkv-projection GEMM and signal the
|
||||
# downstream o_norm / o_proj. Scheduling only — bit-exactness unaffected.
|
||||
@@ -480,6 +584,16 @@ def fused_kda_conv_gating_verify(
|
||||
USE_LOWER_BOUND=lower_bound is not None,
|
||||
SAVE_INTERMEDIATE_WINDOW=intermediate_conv_window is not None,
|
||||
CACHE_INTERMEDIATE_STATES=intermediate_states_buffer is not None,
|
||||
replayssm_rawv=replayssm_rawv,
|
||||
replayssm_rawk=replayssm_rawk,
|
||||
replayssm_g=replayssm_g,
|
||||
replayssm_beta=replayssm_beta,
|
||||
stride_rawv_slot=stride_rawv_slot,
|
||||
stride_rawk_slot=stride_rawk_slot,
|
||||
stride_g_slot=stride_g_slot,
|
||||
stride_beta_slot=stride_beta_slot,
|
||||
MAX_CACHE_LEN=max_cache_len,
|
||||
CACHE_RING=cache_ring,
|
||||
# num_warps=1 matches the reference kernels' reduction order exactly;
|
||||
# higher values must be re-validated for bit-exactness before use.
|
||||
num_warps=num_warps,
|
||||
|
||||
@@ -39,6 +39,7 @@ from sglang.srt.runtime_context import (
|
||||
get_disagg,
|
||||
get_exec,
|
||||
get_memory,
|
||||
get_platform,
|
||||
get_spec,
|
||||
)
|
||||
|
||||
@@ -988,6 +989,27 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
"KDA target_verify requires a speculative mamba cache "
|
||||
"(MambaPool.SpeculativeState); none found."
|
||||
)
|
||||
# ReplaySSM: the ring-write is fused into the verify kernel
|
||||
# (CACHE_RING) on both the fused chain-verify and the unfused triton
|
||||
# paths; commit replays the ring instead of reading per-step
|
||||
# snapshots. ring_kwargs stays empty for non-triton verify kernels,
|
||||
# which never see replayssm. Ragged layouts work natively on the
|
||||
# unfused path -- step_idx is the within-row step under varlen, so
|
||||
# row i writes ring[slot][0..verify_lens[i]) and commit folds at most
|
||||
# commit_lens of them (absorb overflow is bounded in-kernel).
|
||||
replayssm_rawk = replayssm_g = replayssm_beta = None
|
||||
ring_kwargs = {}
|
||||
if replayssm_on:
|
||||
replayssm_rawk = mamba_cache_params.replayssm_rawk
|
||||
replayssm_g = mamba_cache_params.replayssm_g
|
||||
replayssm_beta = mamba_cache_params.replayssm_beta
|
||||
ring_kwargs = dict(
|
||||
cache_ring=True,
|
||||
replayssm_rawv=replayssm_rawv,
|
||||
replayssm_rawk=replayssm_rawk,
|
||||
replayssm_g=replayssm_g,
|
||||
replayssm_beta=replayssm_beta,
|
||||
)
|
||||
intermediate_conv_window_cache = mamba_cache_params.intermediate_conv_window[0]
|
||||
intermediate_state_indices = self.verify_intermediate_state_indices
|
||||
|
||||
@@ -1047,6 +1069,9 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
retrieve_next_sibling=retrieve_next_sibling,
|
||||
retrieve_parent_token=retrieve_parent_token,
|
||||
replayssm_rawv=replayssm_rawv,
|
||||
replayssm_rawk=replayssm_rawk,
|
||||
replayssm_g=replayssm_g,
|
||||
replayssm_beta=replayssm_beta,
|
||||
):
|
||||
return self._fused_chain_verify_fn(
|
||||
mixed_qkv=mixed_qkv,
|
||||
@@ -1077,6 +1102,7 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
head_k_dim=layer.head_k_dim,
|
||||
head_v_dim=layer.head_v_dim,
|
||||
lower_bound=layer.lower_bound,
|
||||
**ring_kwargs,
|
||||
)
|
||||
dense_token_indices = None
|
||||
mixed_qkv_dense = mixed_qkv.view(batch_size, draft_token_num, -1)
|
||||
@@ -1135,22 +1161,6 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
k = k.unflatten(-1, (-1, layer.head_k_dim)).unsqueeze(0)
|
||||
v = v.unflatten(-1, (-1, layer.head_v_dim)).unsqueeze(0)
|
||||
|
||||
# ReplaySSM: the ring-write is fused into the triton verify kernel
|
||||
# (CACHE_RING). Ragged layouts work natively -- step_idx is the
|
||||
# within-row step under varlen, so row i writes
|
||||
# ring[slot][0..verify_lens[i]) and commit folds at most commit_lens
|
||||
# of them (absorb overflow is bounded in-kernel). ring_kwargs stays
|
||||
# empty for non-triton verify kernels, which never see replayssm.
|
||||
ring_kwargs = {}
|
||||
if replayssm_rawv is not None:
|
||||
ring_kwargs = dict(
|
||||
cache_ring=True,
|
||||
replayssm_rawv=replayssm_rawv,
|
||||
replayssm_rawk=mamba_cache_params.replayssm_rawk,
|
||||
replayssm_g=mamba_cache_params.replayssm_g,
|
||||
replayssm_beta=mamba_cache_params.replayssm_beta,
|
||||
)
|
||||
|
||||
core_attn_out = self.kernel_dispatcher.target_verify(
|
||||
A_log=layer.A_log,
|
||||
dt_bias=layer.dt_bias,
|
||||
@@ -1210,10 +1220,13 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
retrieve_next_sibling: Optional[torch.Tensor],
|
||||
retrieve_parent_token: Optional[torch.Tensor],
|
||||
replayssm_rawv: Optional[torch.Tensor],
|
||||
replayssm_rawk: Optional[torch.Tensor],
|
||||
replayssm_g: Optional[torch.Tensor],
|
||||
replayssm_beta: Optional[torch.Tensor],
|
||||
) -> bool:
|
||||
if self._fused_chain_verify_fn is None or not mixed_qkv.is_cuda:
|
||||
return False
|
||||
if replayssm_rawv is not None or any(
|
||||
if any(
|
||||
value is not None
|
||||
for value in (
|
||||
retrieve_next_token,
|
||||
@@ -1222,6 +1235,7 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
)
|
||||
):
|
||||
return False
|
||||
replayssm_on = replayssm_rawv is not None
|
||||
if draft_token_num < 3 or mixed_qkv.shape[0] % draft_token_num != 0:
|
||||
return False
|
||||
if (
|
||||
@@ -1240,6 +1254,15 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
|
||||
seq_len, dim = mixed_qkv.shape
|
||||
batch_size = seq_len // draft_token_num
|
||||
if replayssm_on and (
|
||||
batch_size != 1 or not (get_platform().is_sm90 or get_platform().is_sm100)
|
||||
):
|
||||
# The runtime still uses BV=4, not the benchmark's best-BV sweep:
|
||||
# fused+ring wins at B=1 but regresses from B=4 (B=2 at T=8) on
|
||||
# both enabled architectures. Keep the ring path conservative until
|
||||
# other batch/architecture combinations are measured. The snapshot
|
||||
# path and the separate CuTe path are unchanged.
|
||||
return False
|
||||
expected_dim = (
|
||||
2 * layer.num_q_heads * layer.head_k_dim
|
||||
+ layer.num_v_heads * layer.head_v_dim
|
||||
@@ -1273,7 +1296,21 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
layer.A_log.dtype != torch.float32
|
||||
or layer.dt_bias.dtype != torch.float32
|
||||
or ssm_states.dtype != torch.float32
|
||||
or intermediate_state_cache is None
|
||||
):
|
||||
return False
|
||||
if replayssm_on:
|
||||
if not self._replayssm_ring_ok(
|
||||
layer=layer,
|
||||
draft_token_num=draft_token_num,
|
||||
mixed_qkv=mixed_qkv,
|
||||
replayssm_rawv=replayssm_rawv,
|
||||
replayssm_rawk=replayssm_rawk,
|
||||
replayssm_g=replayssm_g,
|
||||
replayssm_beta=replayssm_beta,
|
||||
):
|
||||
return False
|
||||
elif (
|
||||
intermediate_state_cache is None
|
||||
or intermediate_state_cache.dtype != torch.float32
|
||||
):
|
||||
return False
|
||||
@@ -1283,7 +1320,7 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
or b.stride(-1) != 1
|
||||
or not conv_states.is_contiguous()
|
||||
or not ssm_states.is_contiguous()
|
||||
or not intermediate_state_cache.is_contiguous()
|
||||
or (not replayssm_on and not intermediate_state_cache.is_contiguous())
|
||||
):
|
||||
return False
|
||||
if (
|
||||
@@ -1301,10 +1338,15 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
or ssm_states.ndim != 4
|
||||
or tuple(ssm_states.shape[-3:])
|
||||
!= (layer.num_v_heads, layer.head_v_dim, layer.head_k_dim)
|
||||
or intermediate_state_cache.ndim != 5
|
||||
or intermediate_state_cache.shape[1] < draft_token_num
|
||||
or tuple(intermediate_state_cache.shape[-3:])
|
||||
!= (layer.num_v_heads, layer.head_v_dim, layer.head_k_dim)
|
||||
or (
|
||||
not replayssm_on
|
||||
and (
|
||||
intermediate_state_cache.ndim != 5
|
||||
or intermediate_state_cache.shape[1] < draft_token_num
|
||||
or tuple(intermediate_state_cache.shape[-3:])
|
||||
!= (layer.num_v_heads, layer.head_v_dim, layer.head_k_dim)
|
||||
)
|
||||
)
|
||||
):
|
||||
return False
|
||||
if (
|
||||
@@ -1324,15 +1366,76 @@ class KDAAttnBackend(MambaAttnBackendBase):
|
||||
b,
|
||||
conv_states,
|
||||
ssm_states,
|
||||
intermediate_state_cache,
|
||||
intermediate_conv_window_cache,
|
||||
cache_indices,
|
||||
intermediate_state_indices,
|
||||
)
|
||||
if layer.bias is not None:
|
||||
tensors += (layer.bias,)
|
||||
# Ring devices are validated in _replayssm_ring_ok.
|
||||
if not replayssm_on:
|
||||
tensors += (intermediate_state_cache,)
|
||||
return all(tensor.device == mixed_qkv.device for tensor in tensors)
|
||||
|
||||
@staticmethod
|
||||
def _replayssm_ring_ok(
|
||||
*,
|
||||
layer: RadixLinearAttention,
|
||||
draft_token_num: int,
|
||||
mixed_qkv: torch.Tensor,
|
||||
replayssm_rawv: torch.Tensor,
|
||||
replayssm_rawk: Optional[torch.Tensor],
|
||||
replayssm_g: Optional[torch.Tensor],
|
||||
replayssm_beta: Optional[torch.Tensor],
|
||||
) -> bool:
|
||||
"""Whether the per-layer ReplaySSM rings fit the fused ring-write.
|
||||
|
||||
Layouts follow memory_pool.py's KDA spec rings: rawv [slots, HV, L, V]
|
||||
and rawk [slots, H, L, K] in the activation dtype, g [slots, HV, L, K]
|
||||
fp32 (per-K KDA gate), beta [slots, HV, L] fp32. The kernel uses
|
||||
stride(0) as the slot pitch and assumes packed inner dims; anything
|
||||
else falls back to the unfused path, which handles it.
|
||||
"""
|
||||
if replayssm_rawk is None or replayssm_g is None or replayssm_beta is None:
|
||||
return False
|
||||
if (
|
||||
replayssm_rawv.ndim != 4
|
||||
or replayssm_rawk.ndim != 4
|
||||
or replayssm_g.ndim != 4
|
||||
or replayssm_beta.ndim != 3
|
||||
):
|
||||
return False
|
||||
H, HV = layer.num_q_heads, layer.num_v_heads
|
||||
K, V = layer.head_k_dim, layer.head_v_dim
|
||||
ring_len = replayssm_rawv.shape[-2]
|
||||
if ring_len < draft_token_num:
|
||||
return False
|
||||
if (
|
||||
tuple(replayssm_rawv.shape[1:]) != (HV, ring_len, V)
|
||||
or tuple(replayssm_rawk.shape[1:]) != (H, ring_len, K)
|
||||
or tuple(replayssm_g.shape[1:]) != (HV, ring_len, K)
|
||||
or tuple(replayssm_beta.shape[1:]) != (HV, ring_len)
|
||||
):
|
||||
return False
|
||||
if (
|
||||
replayssm_rawv.dtype != mixed_qkv.dtype
|
||||
or replayssm_rawk.dtype != mixed_qkv.dtype
|
||||
or replayssm_g.dtype != torch.float32
|
||||
or replayssm_beta.dtype != torch.float32
|
||||
):
|
||||
return False
|
||||
if (
|
||||
replayssm_rawv.stride()[1:] != (ring_len * V, V, 1)
|
||||
or replayssm_rawk.stride()[1:] != (ring_len * K, K, 1)
|
||||
or replayssm_g.stride()[1:] != (ring_len * K, K, 1)
|
||||
or replayssm_beta.stride()[1:] != (ring_len, 1)
|
||||
):
|
||||
return False
|
||||
return all(
|
||||
ring.device == mixed_qkv.device
|
||||
for ring in (replayssm_rawv, replayssm_rawk, replayssm_g, replayssm_beta)
|
||||
)
|
||||
|
||||
def _can_run_dspark_cutedsl_mtp(
|
||||
self,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user