[GDN] Support ReplaySSM Ring Spec-Verify (#28695)
Co-authored-by: luoyuan.luo <luoyuan.luo@antgroup.com> Co-authored-by: vincentzed <207368749+vincentzed@users.noreply.github.com>
This commit is contained in:
co-authored by
luoyuan.luo
vincentzed
parent
d1c2a1de08
commit
c41c573ce9
File diff suppressed because it is too large
Load Diff
@@ -94,10 +94,16 @@ class MambaAttnBackendBase(AttentionBackend):
|
|||||||
)
|
)
|
||||||
# The ring cursor is a per-slot decode counter shared by all GDN layers;
|
# The ring cursor is a per-slot decode counter shared by all GDN layers;
|
||||||
# manage it once here (snapshot, hand to layers, advance mod L), not per-layer.
|
# manage it once here (snapshot, hand to layers, advance mod L), not per-layer.
|
||||||
|
# Gate on the linear_replayssm FLAG, not on cursor-tensor presence: the
|
||||||
|
# spec-verify ring (--enable-gdn-replayssm-spec) shares the write_pos
|
||||||
|
# allocation but owns it exclusively via commit_gdn_replayssm_spec
|
||||||
|
# (advance-by-accept-count once per verify step). Advancing it here as
|
||||||
|
# well inserts one phantom/stale ring entry per step and cumulatively
|
||||||
|
# poisons the reconstruction (degenerate repetition at 10k+ tokens).
|
||||||
mamba_pool = getattr(self.req_to_token_pool, "mamba_pool", None)
|
mamba_pool = getattr(self.req_to_token_pool, "mamba_pool", None)
|
||||||
write_pos_buf = (
|
write_pos_buf = (
|
||||||
getattr(mamba_pool, "replayssm_write_pos", None)
|
mamba_pool.replayssm_write_pos
|
||||||
if mamba_pool is not None
|
if mamba_pool is not None and mamba_pool.enable_linear_replayssm
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
if write_pos_buf is not None:
|
if write_pos_buf is not None:
|
||||||
@@ -323,12 +329,20 @@ class MambaAttnBackendBase(AttentionBackend):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _replayssm_enabled(self) -> bool:
|
def _replayssm_enabled(self) -> bool:
|
||||||
"""True iff --enable-linear-replayssm allocated the ring cursor
|
"""True iff --enable-linear-replayssm is on for this pool.
|
||||||
(MambaPool.replayssm_write_pos doubles as the on/off gate)."""
|
|
||||||
|
Gate on the FLAG, not on ``replayssm_write_pos is not None``: the
|
||||||
|
spec-verify ring (--enable-gdn-replayssm-spec) also allocates the
|
||||||
|
cursor tensor but owns it exclusively via commit_gdn_replayssm_spec.
|
||||||
|
The decode-ring metadata machinery gated here (per-bs static cursor
|
||||||
|
buffers, the per-replay snapshot + advance-by-one in _replay_metadata,
|
||||||
|
and the decode-kernel ring rerouting downstream) must stay fully
|
||||||
|
dormant for the spec ring.
|
||||||
|
"""
|
||||||
mamba_pool = getattr(self.req_to_token_pool, "mamba_pool", None)
|
mamba_pool = getattr(self.req_to_token_pool, "mamba_pool", None)
|
||||||
if mamba_pool is None:
|
if mamba_pool is None:
|
||||||
return False
|
return False
|
||||||
return getattr(mamba_pool, "replayssm_write_pos", None) is not None
|
return bool(mamba_pool.enable_linear_replayssm)
|
||||||
|
|
||||||
def _replayssm_track_flush_mask(
|
def _replayssm_track_flush_mask(
|
||||||
self, seq_lens_cpu: torch.Tensor, bs: int
|
self, seq_lens_cpu: torch.Tensor, bs: int
|
||||||
@@ -544,7 +558,10 @@ class MambaAttnBackendBase(AttentionBackend):
|
|||||||
static_ff.copy_(force_flush_dev)
|
static_ff.copy_(force_flush_dev)
|
||||||
else:
|
else:
|
||||||
static_ff.zero_()
|
static_ff.zero_()
|
||||||
if not in_capture:
|
# Defense in depth: the decode-ring advance is only meaningful for
|
||||||
|
# decode/idle forwards (mirrors the eager path's gating). A
|
||||||
|
# TARGET_VERIFY replay must never advance the cursor.
|
||||||
|
if not in_capture and forward_mode.is_decode_or_idle():
|
||||||
L = mamba_pool.linear_replayssm_cache_len
|
L = mamba_pool.linear_replayssm_cache_len
|
||||||
# Advance only valid (non-padded) slots; a forced flush empties
|
# Advance only valid (non-padded) slots; a forced flush empties
|
||||||
# the ring -> next write_pos 0, like the natural L-1 wrap.
|
# the ring -> next write_pos 0, like the natural L-1 wrap.
|
||||||
|
|||||||
@@ -565,22 +565,62 @@ class GDNAttnBackend(MambaAttnBackendBase):
|
|||||||
value = value.view(1, actual_seq_len, layer.num_v_heads, layer.head_v_dim)
|
value = value.view(1, actual_seq_len, layer.num_v_heads, layer.head_v_dim)
|
||||||
|
|
||||||
if is_target_verify:
|
if is_target_verify:
|
||||||
core_attn_out = self.kernel_dispatcher.target_verify(
|
# ReplaySSM spec-verify (Part B of #28511): when the per-slot ring is
|
||||||
A_log=layer.A_log,
|
# allocated (--enable-gdn-replayssm-spec, GDN + linear-chain topk<=1),
|
||||||
dt_bias=layer.dt_bias,
|
# reconstruct the verify output for the whole draft window from the
|
||||||
q=query,
|
# frozen checkpoint (`temporal`) + the per-slot circular (d, k, g) ring
|
||||||
k=key,
|
# instead of the recurrent verify that snapshots a full state per draft
|
||||||
v=value,
|
# token. The cursors are advanced once per decode step by the worker
|
||||||
a=a,
|
# (commit_gdn_replayssm_spec in spec_utils). GDN-only: KDA (per-K gate)
|
||||||
b=b,
|
# routes through kda_backend and never reaches here; we additionally
|
||||||
ssm_states=ssm_states,
|
# guard on `not replayssm_is_kda` for safety. Falls back to the
|
||||||
cache_indices=cache_indices,
|
# recurrent verify when the ring is absent.
|
||||||
query_start_loc=query_start_loc,
|
mamba_pool = self.req_to_token_pool.mamba_pool
|
||||||
intermediate_states_buffer=intermediate_state_cache,
|
use_replayssm_spec = (
|
||||||
intermediate_state_indices=intermediate_state_indices,
|
mamba_cache_params.replayssm_d is not None
|
||||||
cache_steps=forward_batch.spec_info.draft_token_num,
|
and getattr(mamba_pool, "replayssm_cache_base", None) is not None
|
||||||
retrieve_parent_token=retrieve_parent_token,
|
and not getattr(mamba_pool, "replayssm_is_kda", False)
|
||||||
)
|
)
|
||||||
|
if use_replayssm_spec:
|
||||||
|
core_attn_out = self._replayssm_target_verify(
|
||||||
|
layer=layer,
|
||||||
|
query=query,
|
||||||
|
key=key,
|
||||||
|
value=value,
|
||||||
|
a=a,
|
||||||
|
b=b,
|
||||||
|
mamba_pool=mamba_pool,
|
||||||
|
layer_cache=mamba_cache_params,
|
||||||
|
cache_indices=cache_indices,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
draft_token_num=forward_batch.spec_info.draft_token_num,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# The recurrent fallback needs the per-draft snapshots, which
|
||||||
|
# the pool gates OFF under --enable-gdn-replayssm-spec (the
|
||||||
|
# same flag that makes `use_replayssm_spec` true above), so
|
||||||
|
# this branch is unreachable with a None buffer by
|
||||||
|
# construction -- keep it loud rather than silently frozen.
|
||||||
|
assert intermediate_state_cache is not None, (
|
||||||
|
"recurrent target_verify fallback requires intermediate_ssm, "
|
||||||
|
"which is not allocated under --enable-gdn-replayssm-spec"
|
||||||
|
)
|
||||||
|
core_attn_out = self.kernel_dispatcher.target_verify(
|
||||||
|
A_log=layer.A_log,
|
||||||
|
dt_bias=layer.dt_bias,
|
||||||
|
q=query,
|
||||||
|
k=key,
|
||||||
|
v=value,
|
||||||
|
a=a,
|
||||||
|
b=b,
|
||||||
|
ssm_states=ssm_states,
|
||||||
|
cache_indices=cache_indices,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
intermediate_states_buffer=intermediate_state_cache,
|
||||||
|
intermediate_state_indices=intermediate_state_indices,
|
||||||
|
cache_steps=forward_batch.spec_info.draft_token_num,
|
||||||
|
retrieve_parent_token=retrieve_parent_token,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
g, beta = fused_gdn_gating(layer.A_log, a, b, layer.dt_bias)
|
g, beta = fused_gdn_gating(layer.A_log, a, b, layer.dt_bias)
|
||||||
core_attn_out, last_recurrent_state, h = self.kernel_dispatcher.extend(
|
core_attn_out, last_recurrent_state, h = self.kernel_dispatcher.extend(
|
||||||
@@ -612,3 +652,92 @@ class GDNAttnBackend(MambaAttnBackendBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return core_attn_out
|
return core_attn_out
|
||||||
|
|
||||||
|
def _replayssm_target_verify(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
layer: RadixLinearAttention,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
a: torch.Tensor,
|
||||||
|
b: torch.Tensor,
|
||||||
|
mamba_pool: MambaPool,
|
||||||
|
layer_cache: "MambaPool.SpeculativeState",
|
||||||
|
cache_indices: torch.Tensor,
|
||||||
|
query_start_loc: torch.Tensor,
|
||||||
|
draft_token_num: int,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""ReplaySSM GDN spec-verify (Part B of #28511).
|
||||||
|
|
||||||
|
Reconstructs the verify output for the whole draft window from the frozen
|
||||||
|
checkpoint (``temporal``) + the per-slot circular ``(d, k, g)`` ring, and
|
||||||
|
appends this window's drafts to the rings (chunked ``d`` for output
|
||||||
|
reconstruction; raw ``v`` / pre-norm ``k`` / fp32 ``beta`` for the
|
||||||
|
closed-loop exact fold that replays the recurrent update into the fp32
|
||||||
|
checkpoint at flush). The rings are PER-LAYER
|
||||||
|
(sliced via ``mamba2_layer_cache``), while the cursors (write_pos,
|
||||||
|
cache_base, is_flush) are PER-SLOT pool attributes shared by all GDN layers
|
||||||
|
of the step; the cursors persist across steps and are advanced once per step
|
||||||
|
by the worker (commit_gdn_replayssm_spec) -- here we only read them and
|
||||||
|
write this step's ring entries. GDN has K == V, so ``temporal``
|
||||||
|
([slots, HV, K, V]) is consumed directly as the kernel's [slots, HV, V, K]
|
||||||
|
checkpoint.
|
||||||
|
"""
|
||||||
|
from sglang.kernels.ops.attention.fla.gdn_replayssm_spec_decode import (
|
||||||
|
gdn_replayssm_spec_decode,
|
||||||
|
)
|
||||||
|
|
||||||
|
H, K = layer.num_k_heads, layer.head_k_dim
|
||||||
|
HV, V = layer.num_v_heads, layer.head_v_dim
|
||||||
|
# q/k/v may be [1, seq, *] (fallback split) or [seq, *] (fused split);
|
||||||
|
# derive the packed token count from numel so both layouts flatten.
|
||||||
|
seq_len = query.numel() // (H * K)
|
||||||
|
q = query.reshape(seq_len, H, K)
|
||||||
|
k = key.reshape(seq_len, H, K)
|
||||||
|
v = value.reshape(seq_len, HV, V)
|
||||||
|
a = a.reshape(seq_len, HV)
|
||||||
|
b = b.reshape(seq_len, HV)
|
||||||
|
d_cache = layer_cache.replayssm_d # [slots, HV, L, V]
|
||||||
|
max_cache_len = d_cache.shape[-2] # ring length L
|
||||||
|
out = q.new_empty(seq_len, HV, V)
|
||||||
|
gdn_replayssm_spec_decode(
|
||||||
|
q=q,
|
||||||
|
k=k,
|
||||||
|
v=v,
|
||||||
|
a=a,
|
||||||
|
b=b,
|
||||||
|
A_log=layer.A_log,
|
||||||
|
dt_bias=layer.dt_bias,
|
||||||
|
checkpoint_state=layer_cache.temporal,
|
||||||
|
d_cache=d_cache,
|
||||||
|
k_cache=layer_cache.replayssm_k,
|
||||||
|
g_cache=layer_cache.replayssm_g,
|
||||||
|
# Closed-loop exact-fold rings: raw v / raw pre-norm k / fp32 beta.
|
||||||
|
# The flush replays these through the recurrent update (bit-identical
|
||||||
|
# to the recurrent baseline) instead of folding `d` open-loop.
|
||||||
|
rawv_cache=layer_cache.replayssm_rawv,
|
||||||
|
rawk_cache=layer_cache.replayssm_rawk,
|
||||||
|
beta_cache=layer_cache.replayssm_beta,
|
||||||
|
out=out,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
ssm_state_indices=cache_indices,
|
||||||
|
# Per-slot cursors live on the pool (shared across all GDN layers),
|
||||||
|
# NOT in forward_metadata: the verify kernel reads/writes them
|
||||||
|
# block-keyed via ssm_state_indices and must NOT advance write_pos
|
||||||
|
# (the worker does that after acceptance), so the decode-path
|
||||||
|
# forward_metadata.replayssm_write_pos snapshot is not used here.
|
||||||
|
write_pos=mamba_pool.replayssm_write_pos,
|
||||||
|
cache_base=mamba_pool.replayssm_cache_base,
|
||||||
|
is_flush=mamba_pool.replayssm_is_flush,
|
||||||
|
max_cache_len=max_cache_len,
|
||||||
|
max_spec_len=draft_token_num,
|
||||||
|
scale=K**-0.5,
|
||||||
|
use_qk_l2norm_in_kernel=True,
|
||||||
|
# SGLang marks invalid/padding requests with a negative mamba slot
|
||||||
|
# index (valid slots start at 0), so the kernel's "null block"
|
||||||
|
# sentinel is -1, not the vLLM default of 0.
|
||||||
|
null_block_id=-1,
|
||||||
|
)
|
||||||
|
# Match the recurrent target_verify output shape (== value.shape).
|
||||||
|
return out.reshape(value.shape)
|
||||||
|
|||||||
@@ -723,6 +723,14 @@ class KVCacheConfigurator:
|
|||||||
enable_linear_replayssm=self.server_args.enable_linear_replayssm,
|
enable_linear_replayssm=self.server_args.enable_linear_replayssm,
|
||||||
linear_replayssm_cache_len=self.server_args.linear_replayssm_cache_len,
|
linear_replayssm_cache_len=self.server_args.linear_replayssm_cache_len,
|
||||||
mamba_envelope_layout=self.server_args.enable_page_major_kv_layout,
|
mamba_envelope_layout=self.server_args.enable_page_major_kv_layout,
|
||||||
|
# ReplaySSM spec-verify is GDN-only: activate the pool machinery
|
||||||
|
# (rings + cursors + the intermediate_ssm gate) only for GDN-hybrid
|
||||||
|
# models, so any other mamba-ish model (Mamba2/Nemotron, lightning,
|
||||||
|
# ...) run with the flag set stays byte-identical to flag-off.
|
||||||
|
enable_gdn_replayssm_spec=(
|
||||||
|
self.server_args.enable_gdn_replayssm_spec
|
||||||
|
and self.hybrid_gdn_config is not None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return req_to_token_pool
|
return req_to_token_pool
|
||||||
|
|
||||||
|
|||||||
@@ -329,9 +329,20 @@ class MambaPool:
|
|||||||
# replayssm_d: [num_layers, num_slots, HV, L, V]
|
# replayssm_d: [num_layers, num_slots, HV, L, V]
|
||||||
# replayssm_k: [num_layers, num_slots, H, L, K]
|
# replayssm_k: [num_layers, num_slots, H, L, K]
|
||||||
# replayssm_g: [num_layers, num_slots, HV, L] (fp32)
|
# replayssm_g: [num_layers, num_slots, HV, L] (fp32)
|
||||||
|
# replayssm_rawv: [num_layers, num_slots, HV, L, V] (conv/activation dtype)
|
||||||
|
# replayssm_rawk: [num_layers, num_slots, H, L, K] (conv/activation dtype)
|
||||||
|
# replayssm_beta: [num_layers, num_slots, HV, L] (fp32)
|
||||||
|
# The raw rings + beta exist only under --enable-gdn-replayssm-spec: the
|
||||||
|
# closed-loop exact fold sequentially replays them through the recurrent
|
||||||
|
# update at flush -- bit-identical to the recurrent baseline -- instead
|
||||||
|
# of folding the chunked `d` records open-loop (which accumulates error
|
||||||
|
# across flushes). See fla/gdn_replayssm_spec_decode.py.
|
||||||
replayssm_d: Optional[torch.Tensor] = None
|
replayssm_d: Optional[torch.Tensor] = None
|
||||||
replayssm_k: Optional[torch.Tensor] = None
|
replayssm_k: Optional[torch.Tensor] = None
|
||||||
replayssm_g: Optional[torch.Tensor] = None
|
replayssm_g: Optional[torch.Tensor] = None
|
||||||
|
replayssm_rawv: Optional[torch.Tensor] = None
|
||||||
|
replayssm_rawk: Optional[torch.Tensor] = None
|
||||||
|
replayssm_beta: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
def at_layer_idx(self, layer: int):
|
def at_layer_idx(self, layer: int):
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
@@ -357,7 +368,10 @@ class MambaPool:
|
|||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class SpeculativeState(State):
|
class SpeculativeState(State):
|
||||||
intermediate_ssm: torch.Tensor
|
# None under --enable-gdn-replayssm-spec: the spec ring owns rollback
|
||||||
|
# (verify writes ring records, commit moves cursors), so the per-draft
|
||||||
|
# full-state snapshots are never produced or consumed.
|
||||||
|
intermediate_ssm: Optional[torch.Tensor]
|
||||||
intermediate_conv_window: List[torch.Tensor]
|
intermediate_conv_window: List[torch.Tensor]
|
||||||
|
|
||||||
def _allocate_deduplicated_conv_window(
|
def _allocate_deduplicated_conv_window(
|
||||||
@@ -414,6 +428,7 @@ class MambaPool:
|
|||||||
enable_linear_replayssm: bool = False,
|
enable_linear_replayssm: bool = False,
|
||||||
linear_replayssm_cache_len: int = 16,
|
linear_replayssm_cache_len: int = 16,
|
||||||
envelope_layout: bool = False,
|
envelope_layout: bool = False,
|
||||||
|
enable_gdn_replayssm_spec: bool = False,
|
||||||
):
|
):
|
||||||
conv_state_shape = cache_params.shape.conv
|
conv_state_shape = cache_params.shape.conv
|
||||||
temporal_state_shape = cache_params.shape.temporal
|
temporal_state_shape = cache_params.shape.temporal
|
||||||
@@ -429,6 +444,13 @@ class MambaPool:
|
|||||||
self.debug_memory_pool = envs.SGLANG_DEBUG_MEMORY_POOL.get()
|
self.debug_memory_pool = envs.SGLANG_DEBUG_MEMORY_POOL.get()
|
||||||
self.enable_linear_replayssm = enable_linear_replayssm
|
self.enable_linear_replayssm = enable_linear_replayssm
|
||||||
self.linear_replayssm_cache_len = linear_replayssm_cache_len
|
self.linear_replayssm_cache_len = linear_replayssm_cache_len
|
||||||
|
# ReplaySSM spec-verify (Part B of #28511) REUSES the linear_replayssm ring
|
||||||
|
# (replayssm_d/k/g + write_pos) and ADDS two per-slot cursors
|
||||||
|
# (replayssm_cache_base + replayssm_is_flush). Enabling the spec-verify path
|
||||||
|
# therefore implies the ring, so the d/k/g + write_pos allocation gates on
|
||||||
|
# `_replayssm_on` (either flag). GDN-only is enforced upstream + below.
|
||||||
|
self.enable_gdn_replayssm_spec = enable_gdn_replayssm_spec
|
||||||
|
_replayssm_on = enable_linear_replayssm or enable_gdn_replayssm_spec
|
||||||
|
|
||||||
# for disagg with nvlink
|
# for disagg with nvlink
|
||||||
self.enable_custom_mem_pool, self.custom_mem_pool, _ = (
|
self.enable_custom_mem_pool, self.custom_mem_pool, _ = (
|
||||||
@@ -504,22 +526,34 @@ class MambaPool:
|
|||||||
|
|
||||||
# GDN ReplaySSM ring buffers (slice 1a). Allocated only when the
|
# GDN ReplaySSM ring buffers (slice 1a). Allocated only when the
|
||||||
# flag is on; otherwise left as None so the legacy State is
|
# flag is on; otherwise left as None so the legacy State is
|
||||||
# byte-identical. temporal_state_shape == (HV, V, K).
|
# byte-identical. temporal_state_shape == (HV, V, K). Either the decode
|
||||||
|
# ring (--enable-linear-replayssm) or the spec-verify ring
|
||||||
|
# (--enable-gdn-replayssm-spec) shares this allocation.
|
||||||
replayssm_d = replayssm_k = replayssm_g = None
|
replayssm_d = replayssm_k = replayssm_g = None
|
||||||
if enable_linear_replayssm:
|
replayssm_rawv = replayssm_rawk = replayssm_beta = None
|
||||||
|
if _replayssm_on:
|
||||||
hv, v_dim, k_dim = temporal_state_shape
|
hv, v_dim, k_dim = temporal_state_shape
|
||||||
h_k = getattr(cache_params.shape, "num_k_heads_per_tp", hv)
|
h_k = getattr(cache_params.shape, "num_k_heads_per_tp", hv)
|
||||||
L = linear_replayssm_cache_len
|
L = linear_replayssm_cache_len
|
||||||
num_slots = size + 1
|
num_slots = size + 1
|
||||||
# Ring records live in the SSM dtype (bf16/fp32) except g (fp32).
|
# Ring dtype. DECODE ring (--enable-linear-replayssm): records
|
||||||
|
# follow the SSM dtype -- its flush folds `d` directly into the
|
||||||
|
# state. SPEC-verify ring (--enable-gdn-replayssm-spec): d/k feed
|
||||||
|
# ONLY the one-shot output reconstruction (the closed-loop exact
|
||||||
|
# fold replays the raw rings for state instead), so their
|
||||||
|
# quantization noise stays below the bf16 output cast; keep them
|
||||||
|
# in the conv/activation dtype instead of the (fp32-enforced)
|
||||||
|
# SSM dtype to halve the ring traffic. g stays fp32 everywhere
|
||||||
|
# (exact-fold input). The two flags are mutually exclusive.
|
||||||
|
ring_dtype = conv_dtype if enable_gdn_replayssm_spec else ssm_dtype
|
||||||
replayssm_d = torch.zeros(
|
replayssm_d = torch.zeros(
|
||||||
size=(num_mamba_layers, num_slots, hv, L, v_dim),
|
size=(num_mamba_layers, num_slots, hv, L, v_dim),
|
||||||
dtype=ssm_dtype,
|
dtype=ring_dtype,
|
||||||
device=device,
|
device=device,
|
||||||
)
|
)
|
||||||
replayssm_k = torch.zeros(
|
replayssm_k = torch.zeros(
|
||||||
size=(num_mamba_layers, num_slots, h_k, L, k_dim),
|
size=(num_mamba_layers, num_slots, h_k, L, k_dim),
|
||||||
dtype=ssm_dtype,
|
dtype=ring_dtype,
|
||||||
device=device,
|
device=device,
|
||||||
)
|
)
|
||||||
# The log-decay gate ring (fp32): per-head SCALAR for the GDN
|
# The log-decay gate ring (fp32): per-head SCALAR for the GDN
|
||||||
@@ -535,6 +569,42 @@ class MambaPool:
|
|||||||
dtype=torch.float32,
|
dtype=torch.float32,
|
||||||
device=device,
|
device=device,
|
||||||
)
|
)
|
||||||
|
# Closed-loop exact-fold rings (spec-verify only). Raw v / raw
|
||||||
|
# pre-norm k live in the conv (activation) dtype -- they are born
|
||||||
|
# there, so storage round-trips losslessly -- beta in fp32. The
|
||||||
|
# flush replays these through the recurrent update sequentially
|
||||||
|
# (bit-identical to the recurrent baseline) instead of folding
|
||||||
|
# the chunked `d` records open-loop.
|
||||||
|
if enable_gdn_replayssm_spec:
|
||||||
|
# Backstop for the spec-verify ring invariants; this pool
|
||||||
|
# is sized with the final adaptive-aware draft maximum.
|
||||||
|
if L & (L - 1) != 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"spec-verify ring length must be a power of two, got {L}"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
speculative_num_draft_tokens is not None
|
||||||
|
and L < 2 * speculative_num_draft_tokens
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"spec-verify ring too small: {L} < "
|
||||||
|
f"2 * {speculative_num_draft_tokens} (early-flush margin)"
|
||||||
|
)
|
||||||
|
replayssm_rawv = torch.zeros(
|
||||||
|
size=(num_mamba_layers, num_slots, hv, L, v_dim),
|
||||||
|
dtype=conv_dtype,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
replayssm_rawk = torch.zeros(
|
||||||
|
size=(num_mamba_layers, num_slots, h_k, L, k_dim),
|
||||||
|
dtype=conv_dtype,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
replayssm_beta = torch.zeros(
|
||||||
|
size=(num_mamba_layers, num_slots, hv, L),
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
|
||||||
if speculative_num_draft_tokens is not None:
|
if speculative_num_draft_tokens is not None:
|
||||||
if _is_npu:
|
if _is_npu:
|
||||||
@@ -546,18 +616,30 @@ class MambaPool:
|
|||||||
)
|
)
|
||||||
# Cache intermediate SSM states per draft token during target verify
|
# Cache intermediate SSM states per draft token during target verify
|
||||||
# Shape: [num_layers, size + 1, speculative_num_draft_tokens, HV, K, V]
|
# Shape: [num_layers, size + 1, speculative_num_draft_tokens, HV, K, V]
|
||||||
intermediate_ssm_state_cache = torch.zeros(
|
#
|
||||||
size=(
|
# ReplaySSM spec-verify owns rollback via the ring + cursors (the
|
||||||
num_mamba_layers,
|
# verify kernel never writes per-draft snapshots; the commit never
|
||||||
spec_state_size + 1,
|
# reads them), so this buffer -- the dominant spec scratch, ~46x
|
||||||
speculative_num_draft_tokens,
|
# the conv state -- is dead weight there and is skipped. The conv
|
||||||
temporal_state_shape[0],
|
# intermediate windows below STAY (conv rollback consumes them).
|
||||||
temporal_state_shape[1],
|
# The recurrent-verify fallback cannot be reached under the flag
|
||||||
temporal_state_shape[2],
|
# (GDN + linear chain + triton enforced in server_args; the
|
||||||
),
|
# backend asserts loudly if it ever is).
|
||||||
dtype=ssm_dtype,
|
if enable_gdn_replayssm_spec:
|
||||||
device="cuda",
|
intermediate_ssm_state_cache = None
|
||||||
)
|
else:
|
||||||
|
intermediate_ssm_state_cache = torch.zeros(
|
||||||
|
size=(
|
||||||
|
num_mamba_layers,
|
||||||
|
spec_state_size + 1,
|
||||||
|
speculative_num_draft_tokens,
|
||||||
|
temporal_state_shape[0],
|
||||||
|
temporal_state_shape[1],
|
||||||
|
temporal_state_shape[2],
|
||||||
|
),
|
||||||
|
dtype=ssm_dtype,
|
||||||
|
device="cuda",
|
||||||
|
)
|
||||||
# Cache intermediate conv windows (last K-1 inputs) per draft token
|
# Cache intermediate conv windows (last K-1 inputs) per draft token
|
||||||
# during target verify.
|
# during target verify.
|
||||||
#
|
#
|
||||||
@@ -625,13 +707,21 @@ class MambaPool:
|
|||||||
replayssm_d=replayssm_d,
|
replayssm_d=replayssm_d,
|
||||||
replayssm_k=replayssm_k,
|
replayssm_k=replayssm_k,
|
||||||
replayssm_g=replayssm_g,
|
replayssm_g=replayssm_g,
|
||||||
|
replayssm_rawv=replayssm_rawv,
|
||||||
|
replayssm_rawk=replayssm_rawk,
|
||||||
|
replayssm_beta=replayssm_beta,
|
||||||
|
)
|
||||||
|
intermediate_ssm_gb = (
|
||||||
|
get_tensor_size_bytes(intermediate_ssm_state_cache) / GB
|
||||||
|
if intermediate_ssm_state_cache is not None
|
||||||
|
else 0.0
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Mamba Cache is allocated. "
|
f"Mamba Cache is allocated. "
|
||||||
f"max_mamba_cache_size: {size}, "
|
f"max_mamba_cache_size: {size}, "
|
||||||
f"conv_state size: {get_tensor_size_bytes(conv_state) / GB:.2f}GB, "
|
f"conv_state size: {get_tensor_size_bytes(conv_state) / GB:.2f}GB, "
|
||||||
f"ssm_state size: {get_tensor_size_bytes(temporal_state) / GB:.2f}GB "
|
f"ssm_state size: {get_tensor_size_bytes(temporal_state) / GB:.2f}GB "
|
||||||
f"intermediate_ssm_state_cache size: {get_tensor_size_bytes(intermediate_ssm_state_cache) / GB:.2f}GB "
|
f"intermediate_ssm_state_cache size: {intermediate_ssm_gb:.2f}GB "
|
||||||
# Report the deduplicated PHYSICAL conv-window buffers (the view
|
# Report the deduplicated PHYSICAL conv-window buffers (the view
|
||||||
# over-reports its logical, un-deduplicated size).
|
# over-reports its logical, un-deduplicated size).
|
||||||
f"intermediate_conv_window_cache size: {get_tensor_size_bytes(self._intermediate_conv_window_phys) / GB:.2f}GB "
|
f"intermediate_conv_window_cache size: {get_tensor_size_bytes(self._intermediate_conv_window_phys) / GB:.2f}GB "
|
||||||
@@ -643,6 +733,9 @@ class MambaPool:
|
|||||||
replayssm_d=replayssm_d,
|
replayssm_d=replayssm_d,
|
||||||
replayssm_k=replayssm_k,
|
replayssm_k=replayssm_k,
|
||||||
replayssm_g=replayssm_g,
|
replayssm_g=replayssm_g,
|
||||||
|
replayssm_rawv=replayssm_rawv,
|
||||||
|
replayssm_rawk=replayssm_rawk,
|
||||||
|
replayssm_beta=replayssm_beta,
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Mamba Cache is allocated. "
|
f"Mamba Cache is allocated. "
|
||||||
@@ -650,26 +743,48 @@ class MambaPool:
|
|||||||
f"conv_state size: {get_tensor_size_bytes(conv_state) / GB:.2f}GB, "
|
f"conv_state size: {get_tensor_size_bytes(conv_state) / GB:.2f}GB, "
|
||||||
f"ssm_state size: {get_tensor_size_bytes(temporal_state) / GB:.2f}GB "
|
f"ssm_state size: {get_tensor_size_bytes(temporal_state) / GB:.2f}GB "
|
||||||
)
|
)
|
||||||
if enable_linear_replayssm:
|
if _replayssm_on:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"GDN ReplaySSM ring buffers allocated (L="
|
f"GDN ReplaySSM ring buffers allocated (L="
|
||||||
f"{linear_replayssm_cache_len}): "
|
f"{linear_replayssm_cache_len}): "
|
||||||
f"d={get_tensor_size_bytes(replayssm_d) / GB:.3f}GB, "
|
f"d={get_tensor_size_bytes(replayssm_d) / GB:.3f}GB, "
|
||||||
f"k={get_tensor_size_bytes(replayssm_k) / GB:.3f}GB, "
|
f"k={get_tensor_size_bytes(replayssm_k) / GB:.3f}GB, "
|
||||||
f"g={get_tensor_size_bytes(replayssm_g) / GB:.3f}GB "
|
f"g={get_tensor_size_bytes(replayssm_g) / GB:.3f}GB "
|
||||||
|
+ (
|
||||||
|
f"rawv={get_tensor_size_bytes(replayssm_rawv) / GB:.3f}GB, "
|
||||||
|
f"rawk={get_tensor_size_bytes(replayssm_rawk) / GB:.3f}GB, "
|
||||||
|
f"beta={get_tensor_size_bytes(replayssm_beta) / GB:.3f}GB "
|
||||||
|
if enable_gdn_replayssm_spec
|
||||||
|
else ""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# Gate granularity of the linear-attn layers (drives the kernel's
|
# Gate granularity of the linear-attn layers (drives the kernel's
|
||||||
# IS_KDA path + the g_cache layout). Read by the backend metadata to
|
# IS_KDA path + the g_cache layout). Read by the backend metadata to
|
||||||
# decide the per-K (KDA) vs scalar (GDN) flush/advance handling.
|
# decide the per-K (KDA) vs scalar (GDN) flush/advance handling.
|
||||||
self.replayssm_is_kda = bool(
|
self.replayssm_is_kda = bool(_replayssm_on and cache_params.is_kda)
|
||||||
enable_linear_replayssm and cache_params.is_kda
|
|
||||||
)
|
|
||||||
# Persistent per-slot decode-position cursor for ReplaySSM. Shared
|
# Persistent per-slot decode-position cursor for ReplaySSM. Shared
|
||||||
# across all linear-attn layers; advanced once per decode forward by
|
# across all linear-attn layers; advanced once per decode forward by
|
||||||
# the backend metadata build. Index 0..size; reset on slot (re)alloc.
|
# the backend metadata build (decode ring) or once per verify step by
|
||||||
|
# the worker (spec-verify ring). Index 0..size; reset on slot (re)alloc.
|
||||||
self.replayssm_write_pos = (
|
self.replayssm_write_pos = (
|
||||||
torch.zeros((size + 1,), dtype=torch.int32, device=device)
|
torch.zeros((size + 1,), dtype=torch.int32, device=device)
|
||||||
if enable_linear_replayssm
|
if _replayssm_on
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
# ReplaySSM spec-verify (Part B of #28511) extra per-slot cursors. The
|
||||||
|
# circular ring's rolling origin (cache_base) + the per-slot flush flag
|
||||||
|
# (is_flush). Block-keyed (indexed by the physical mamba slot), shared by
|
||||||
|
# all GDN layers of one verify step; advanced by commit_gdn_replayssm_spec.
|
||||||
|
# Only allocated for the spec-verify ring (the decode ring does not use
|
||||||
|
# a circular buffer); None otherwise.
|
||||||
|
self.replayssm_cache_base = (
|
||||||
|
torch.zeros((size + 1,), dtype=torch.int32, device=device)
|
||||||
|
if enable_gdn_replayssm_spec
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
self.replayssm_is_flush = (
|
||||||
|
torch.zeros((size + 1,), dtype=torch.int8, device=device)
|
||||||
|
if enable_gdn_replayssm_spec
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
mem_usage_bytes = self.mamba_cache.mem_usage_bytes()
|
mem_usage_bytes = self.mamba_cache.mem_usage_bytes()
|
||||||
@@ -804,6 +919,12 @@ class MambaPool:
|
|||||||
]
|
]
|
||||||
if self.replayssm_write_pos is not None:
|
if self.replayssm_write_pos is not None:
|
||||||
self.replayssm_write_pos[dst_indices] = 0
|
self.replayssm_write_pos[dst_indices] = 0
|
||||||
|
# ReplaySSM spec-verify ring: a copied checkpoint has no pending ring
|
||||||
|
# entries, so its rolling origin + flush flag reset alongside write_pos.
|
||||||
|
if self.replayssm_cache_base is not None:
|
||||||
|
self.replayssm_cache_base[dst_indices] = 0
|
||||||
|
if self.replayssm_is_flush is not None:
|
||||||
|
self.replayssm_is_flush[dst_indices] = 0
|
||||||
|
|
||||||
def get_cpu_copy(self, indices):
|
def get_cpu_copy(self, indices):
|
||||||
current_platform.synchronize()
|
current_platform.synchronize()
|
||||||
@@ -814,17 +935,46 @@ class MambaPool:
|
|||||||
temporal_cpu = self.mamba_cache.temporal[:, indices].to(
|
temporal_cpu = self.mamba_cache.temporal[:, indices].to(
|
||||||
"cpu", non_blocking=True
|
"cpu", non_blocking=True
|
||||||
)
|
)
|
||||||
|
# ReplaySSM spec-verify ring: round-trip the per-slot cursors with the
|
||||||
|
# checkpoint so a restored slot reconstructs exactly. Only the spec ring
|
||||||
|
# adds the 3rd tuple element; every other config keeps the legacy 2-tuple
|
||||||
|
# so those paths stay byte-identical.
|
||||||
|
if self.replayssm_cache_base is not None:
|
||||||
|
cursors_cpu = (
|
||||||
|
self.replayssm_write_pos[indices].to("cpu", non_blocking=True),
|
||||||
|
self.replayssm_cache_base[indices].to("cpu", non_blocking=True),
|
||||||
|
self.replayssm_is_flush[indices].to("cpu", non_blocking=True),
|
||||||
|
)
|
||||||
|
current_platform.synchronize()
|
||||||
|
return conv_cpu, temporal_cpu, cursors_cpu
|
||||||
current_platform.synchronize()
|
current_platform.synchronize()
|
||||||
return conv_cpu, temporal_cpu
|
return conv_cpu, temporal_cpu
|
||||||
|
|
||||||
def load_cpu_copy(self, mamba_cache_cpu, indices):
|
def load_cpu_copy(self, mamba_cache_cpu, indices):
|
||||||
conv_cpu, temporal_cpu = mamba_cache_cpu
|
# Accept both the legacy 2-tuple (conv, temporal) and the 3-tuple that also
|
||||||
|
# carries the ReplaySSM spec-verify cursors.
|
||||||
|
if len(mamba_cache_cpu) == 3:
|
||||||
|
conv_cpu, temporal_cpu, cursors_cpu = mamba_cache_cpu
|
||||||
|
else:
|
||||||
|
conv_cpu, temporal_cpu = mamba_cache_cpu
|
||||||
|
cursors_cpu = None
|
||||||
current_platform.synchronize()
|
current_platform.synchronize()
|
||||||
for i, conv in enumerate(self.mamba_cache.conv):
|
for i, conv in enumerate(self.mamba_cache.conv):
|
||||||
conv[:, indices] = conv_cpu[i].to(conv.device, non_blocking=True)
|
conv[:, indices] = conv_cpu[i].to(conv.device, non_blocking=True)
|
||||||
self.mamba_cache.temporal[:, indices] = temporal_cpu.to(
|
self.mamba_cache.temporal[:, indices] = temporal_cpu.to(
|
||||||
self.mamba_cache.temporal.device, non_blocking=True
|
self.mamba_cache.temporal.device, non_blocking=True
|
||||||
)
|
)
|
||||||
|
if cursors_cpu is not None and self.replayssm_cache_base is not None:
|
||||||
|
wp_cpu, cb_cpu, fl_cpu = cursors_cpu
|
||||||
|
self.replayssm_write_pos[indices] = wp_cpu.to(
|
||||||
|
self.replayssm_write_pos.device, non_blocking=True
|
||||||
|
)
|
||||||
|
self.replayssm_cache_base[indices] = cb_cpu.to(
|
||||||
|
self.replayssm_cache_base.device, non_blocking=True
|
||||||
|
)
|
||||||
|
self.replayssm_is_flush[indices] = fl_cpu.to(
|
||||||
|
self.replayssm_is_flush.device, non_blocking=True
|
||||||
|
)
|
||||||
current_platform.synchronize()
|
current_platform.synchronize()
|
||||||
|
|
||||||
def get_contiguous_buf_infos(self):
|
def get_contiguous_buf_infos(self):
|
||||||
@@ -841,7 +991,14 @@ class MambaPool:
|
|||||||
continue
|
continue
|
||||||
# Skip GDN ReplaySSM ring buffers: they are derived/transient decode
|
# Skip GDN ReplaySSM ring buffers: they are derived/transient decode
|
||||||
# scratch, not part of the persistent transferable state.
|
# scratch, not part of the persistent transferable state.
|
||||||
if field in ("replayssm_d", "replayssm_k", "replayssm_g"):
|
if field in (
|
||||||
|
"replayssm_d",
|
||||||
|
"replayssm_k",
|
||||||
|
"replayssm_g",
|
||||||
|
"replayssm_rawv",
|
||||||
|
"replayssm_rawk",
|
||||||
|
"replayssm_beta",
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
value = getattr(self.mamba_cache, field)
|
value = getattr(self.mamba_cache, field)
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -882,6 +1039,9 @@ class MambaPool:
|
|||||||
"replayssm_d",
|
"replayssm_d",
|
||||||
"replayssm_k",
|
"replayssm_k",
|
||||||
"replayssm_g",
|
"replayssm_g",
|
||||||
|
"replayssm_rawv",
|
||||||
|
"replayssm_rawk",
|
||||||
|
"replayssm_beta",
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
value = getattr(self.mamba_cache, field)
|
value = getattr(self.mamba_cache, field)
|
||||||
@@ -964,6 +1124,7 @@ class HybridReqToTokenPool(ReqToTokenPool):
|
|||||||
enable_linear_replayssm: bool = False,
|
enable_linear_replayssm: bool = False,
|
||||||
linear_replayssm_cache_len: int = 16,
|
linear_replayssm_cache_len: int = 16,
|
||||||
mamba_envelope_layout: bool = False,
|
mamba_envelope_layout: bool = False,
|
||||||
|
enable_gdn_replayssm_spec: bool = False,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
size=size,
|
size=size,
|
||||||
@@ -990,6 +1151,7 @@ class HybridReqToTokenPool(ReqToTokenPool):
|
|||||||
enable_linear_replayssm=enable_linear_replayssm,
|
enable_linear_replayssm=enable_linear_replayssm,
|
||||||
linear_replayssm_cache_len=linear_replayssm_cache_len,
|
linear_replayssm_cache_len=linear_replayssm_cache_len,
|
||||||
mamba_envelope_layout=mamba_envelope_layout,
|
mamba_envelope_layout=mamba_envelope_layout,
|
||||||
|
enable_gdn_replayssm_spec=enable_gdn_replayssm_spec,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _init_mamba_pool(
|
def _init_mamba_pool(
|
||||||
@@ -1005,6 +1167,7 @@ class HybridReqToTokenPool(ReqToTokenPool):
|
|||||||
enable_linear_replayssm: bool = False,
|
enable_linear_replayssm: bool = False,
|
||||||
linear_replayssm_cache_len: int = 16,
|
linear_replayssm_cache_len: int = 16,
|
||||||
mamba_envelope_layout: bool = False,
|
mamba_envelope_layout: bool = False,
|
||||||
|
enable_gdn_replayssm_spec: bool = False,
|
||||||
):
|
):
|
||||||
self.mamba_pool = self.mamba_pool_cls(
|
self.mamba_pool = self.mamba_pool_cls(
|
||||||
size=mamba_size,
|
size=mamba_size,
|
||||||
@@ -1018,6 +1181,7 @@ class HybridReqToTokenPool(ReqToTokenPool):
|
|||||||
enable_linear_replayssm=enable_linear_replayssm,
|
enable_linear_replayssm=enable_linear_replayssm,
|
||||||
linear_replayssm_cache_len=linear_replayssm_cache_len,
|
linear_replayssm_cache_len=linear_replayssm_cache_len,
|
||||||
envelope_layout=mamba_envelope_layout,
|
envelope_layout=mamba_envelope_layout,
|
||||||
|
enable_gdn_replayssm_spec=enable_gdn_replayssm_spec,
|
||||||
)
|
)
|
||||||
self.mamba_allocator = MambaSlotAllocator(
|
self.mamba_allocator = MambaSlotAllocator(
|
||||||
size=mamba_size,
|
size=mamba_size,
|
||||||
@@ -1116,6 +1280,12 @@ class HybridReqToTokenPool(ReqToTokenPool):
|
|||||||
# (the post-prefill state that prefill wrote into this slot).
|
# (the post-prefill state that prefill wrote into this slot).
|
||||||
if self.mamba_pool.replayssm_write_pos is not None:
|
if self.mamba_pool.replayssm_write_pos is not None:
|
||||||
self.mamba_pool.replayssm_write_pos[req.mamba_pool_idx] = 0
|
self.mamba_pool.replayssm_write_pos[req.mamba_pool_idx] = 0
|
||||||
|
# ReplaySSM spec-verify ring: an empty ring also resets the
|
||||||
|
# circular origin + flush flag so the first verify step on this
|
||||||
|
# freshly-prefilled slot reconstructs from the checkpoint alone.
|
||||||
|
if self.mamba_pool.replayssm_cache_base is not None:
|
||||||
|
self.mamba_pool.replayssm_cache_base[req.mamba_pool_idx] = 0
|
||||||
|
self.mamba_pool.replayssm_is_flush[req.mamba_pool_idx] = 0
|
||||||
mamba_indices.append(req.mamba_pool_idx)
|
mamba_indices.append(req.mamba_pool_idx)
|
||||||
if self.enable_mamba_extra_buffer:
|
if self.enable_mamba_extra_buffer:
|
||||||
if req.mamba_ping_pong_track_buffer is None:
|
if req.mamba_ping_pong_track_buffer is None:
|
||||||
|
|||||||
@@ -2164,6 +2164,16 @@ class ServerArgs:
|
|||||||
int,
|
int,
|
||||||
"Ring-buffer length L for ReplaySSM linear-attn decode. The full recurrent state is flushed to HBM every L decode steps.",
|
"Ring-buffer length L for ReplaySSM linear-attn decode. The full recurrent state is flushed to HBM every L decode steps.",
|
||||||
] = 16
|
] = 16
|
||||||
|
# ReplaySSM spec-verify (Part B of RFC #28511): GDN linear-chain target-verify
|
||||||
|
# via a per-slot circular (d, k, g) ring + periodic flush instead of per-draft
|
||||||
|
# full-state snapshots. GDN only; linear-chain (topk <= 1) only. Reuses the
|
||||||
|
# `linear_replayssm` ring (replayssm_d/k/g + write_pos) and adds two per-slot
|
||||||
|
# cursors (cache_base, is_flush); the ring length reuses
|
||||||
|
# `linear_replayssm_cache_len`.
|
||||||
|
enable_gdn_replayssm_spec: A[
|
||||||
|
bool,
|
||||||
|
"Enable the ReplaySSM GDN spec-verify kernel (Part B of RFC #28511): a per-slot circular (d, k, g) ring + periodic flush replacing the recurrent verify's per-draft full-state snapshots. GDN only, linear-chain (--speculative-eagle-topk in {None, 1}) only. Reuses --linear-replayssm-cache-len for the ring length.",
|
||||||
|
] = False
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Hierarchical cache
|
# Hierarchical cache
|
||||||
@@ -2998,6 +3008,9 @@ class ServerArgs:
|
|||||||
|
|
||||||
handle_speculative_decoding(self)
|
handle_speculative_decoding(self)
|
||||||
|
|
||||||
|
# Needs the draft-token count derived just above.
|
||||||
|
self._validate_gdn_replayssm_spec_ring()
|
||||||
|
|
||||||
# Validate the CuteDSL A2A token budget now that num_tokens_per_req is final.
|
# Validate the CuteDSL A2A token budget now that num_tokens_per_req is final.
|
||||||
self._validate_cutedsl_a2a_token_budget()
|
self._validate_cutedsl_a2a_token_budget()
|
||||||
|
|
||||||
@@ -5307,6 +5320,110 @@ class ServerArgs:
|
|||||||
f"{self.linear_replayssm_cache_len}."
|
f"{self.linear_replayssm_cache_len}."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ReplaySSM spec-verify (Part B of #28511): GDN-only, linear-chain target
|
||||||
|
# verify. Reuses the `linear_replayssm` ring (replayssm_d/k/g + write_pos)
|
||||||
|
# plus two extra per-slot cursors (cache_base, is_flush) and the chunked
|
||||||
|
# (I+A)^-1 reconstruction verify kernel. The intra-window interaction uses a
|
||||||
|
# strictly-lower causal mask, so it is valid ONLY for a linear draft chain
|
||||||
|
# (speculative_eagle_topk in {None, 1}, i.e. NEXTN / MTP); EAGLE tree verify
|
||||||
|
# (topk > 1) must fall back to the recurrent verify. GDN-only is enforced at
|
||||||
|
# runtime (KDA routes through kda_backend, which never enters this path; the
|
||||||
|
# pool gate also checks `not cache_params.is_kda`). The ring length reuses
|
||||||
|
# --linear-replayssm-cache-len (no separate flag).
|
||||||
|
if self.enable_gdn_replayssm_spec:
|
||||||
|
if self.speculative_eagle_topk not in (None, 1):
|
||||||
|
raise ValueError(
|
||||||
|
"--enable-gdn-replayssm-spec requires a linear draft chain "
|
||||||
|
"(--speculative-eagle-topk in {None, 1}); the chunked verify "
|
||||||
|
"kernel uses a strictly-lower causal mask and is invalid for "
|
||||||
|
"EAGLE tree verify. Got "
|
||||||
|
f"--speculative-eagle-topk={self.speculative_eagle_topk!r}."
|
||||||
|
)
|
||||||
|
if decode != "triton":
|
||||||
|
raise ValueError(
|
||||||
|
"--enable-gdn-replayssm-spec requires the Triton linear-attn "
|
||||||
|
"decode backend, got "
|
||||||
|
f"--linear-attn-decode-backend={decode!r}."
|
||||||
|
)
|
||||||
|
if self.enable_mamba_extra_buffer():
|
||||||
|
# The spec-verify path does not yet implement the device-side
|
||||||
|
# force-flush needed to keep `temporal` consistent with the ring at
|
||||||
|
# radix mamba-track boundaries, so it is incompatible with
|
||||||
|
# extra_buffer (radix prefix caching).
|
||||||
|
raise ValueError(
|
||||||
|
"--enable-gdn-replayssm-spec is not yet compatible with mamba "
|
||||||
|
"extra_buffer (radix prefix caching); use --disable-radix-cache "
|
||||||
|
"or --mamba-radix-cache-strategy no_buffer."
|
||||||
|
)
|
||||||
|
if self.disaggregation_mode != "null":
|
||||||
|
raise ValueError(
|
||||||
|
"--enable-gdn-replayssm-spec is not supported under PD "
|
||||||
|
"disaggregation yet (follow-up). Got "
|
||||||
|
f"--disaggregation-mode={self.disaggregation_mode!r}."
|
||||||
|
)
|
||||||
|
if self.linear_replayssm_cache_len < 1:
|
||||||
|
raise ValueError(
|
||||||
|
"--linear-replayssm-cache-len must be >= 1, got "
|
||||||
|
f"{self.linear_replayssm_cache_len}."
|
||||||
|
)
|
||||||
|
if self.enable_linear_replayssm:
|
||||||
|
raise ValueError(
|
||||||
|
"--enable-gdn-replayssm-spec and --enable-linear-replayssm are "
|
||||||
|
"mutually exclusive: they share the ring storage but drive it "
|
||||||
|
"with incompatible cursor protocols (per-decode-forward vs "
|
||||||
|
"per-verify-commit advance)."
|
||||||
|
)
|
||||||
|
ring_len = self.linear_replayssm_cache_len
|
||||||
|
if ring_len & (ring_len - 1) != 0:
|
||||||
|
raise ValueError(
|
||||||
|
"--linear-replayssm-cache-len must be a power of two for the "
|
||||||
|
f"circular spec-verify ring, got {ring_len}."
|
||||||
|
)
|
||||||
|
# ring_len >= 2 * max drafts is checked in
|
||||||
|
# _validate_gdn_replayssm_spec_ring() (draft tokens not derived yet).
|
||||||
|
# Closed-loop exact fold: the flush replays raw ring inputs through
|
||||||
|
# the recurrent update into the checkpoint, bit-identical to the
|
||||||
|
# recurrent baseline -- which keeps its state in fp32. A 16-bit
|
||||||
|
# checkpoint would re-quantize the exactly-folded state every flush
|
||||||
|
# and become the dominant residual error source, so require fp32.
|
||||||
|
if self.mamba_ssm_dtype is None:
|
||||||
|
logger.info(
|
||||||
|
"--enable-gdn-replayssm-spec: setting --mamba-ssm-dtype "
|
||||||
|
"float32 (the closed-loop exact fold requires the fp32 SSM "
|
||||||
|
"checkpoint for recurrent-parity)."
|
||||||
|
)
|
||||||
|
self.mamba_ssm_dtype = "float32"
|
||||||
|
elif self.mamba_ssm_dtype != "float32":
|
||||||
|
raise ValueError(
|
||||||
|
"--enable-gdn-replayssm-spec requires --mamba-ssm-dtype "
|
||||||
|
f"float32, got {self.mamba_ssm_dtype!r}. The closed-loop "
|
||||||
|
"exact fold keeps the committed state bit-identical to the "
|
||||||
|
"recurrent baseline, which is only meaningful against the "
|
||||||
|
"fp32 checkpoint; a 16-bit checkpoint would re-quantize it "
|
||||||
|
"every flush."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_gdn_replayssm_spec_ring(self):
|
||||||
|
"""Enforce ring_len >= 2 * max draft tokens for the spec-verify ring.
|
||||||
|
|
||||||
|
Early-flush margin: write_pos + spec_len <= ring_len must hold on every
|
||||||
|
verify step (see _advance_gdn_spec_cursors_kernel). Runs after
|
||||||
|
handle_speculative_decoding() so the (adaptive-aware) max is final;
|
||||||
|
MambaPool re-checks at ring allocation as a backstop.
|
||||||
|
"""
|
||||||
|
if not self.enable_gdn_replayssm_spec:
|
||||||
|
return
|
||||||
|
max_drafts = self.max_speculative_num_draft_tokens
|
||||||
|
if max_drafts is None:
|
||||||
|
return
|
||||||
|
ring_len = self.linear_replayssm_cache_len
|
||||||
|
if ring_len < 2 * max_drafts:
|
||||||
|
raise ValueError(
|
||||||
|
"--linear-replayssm-cache-len must be >= 2 * the maximum "
|
||||||
|
"speculative draft-token count for the spec-verify ring "
|
||||||
|
f"(early-flush margin), got {ring_len} < {2 * max_drafts}."
|
||||||
|
)
|
||||||
|
|
||||||
def _handle_legacy_cp_arguments(self):
|
def _handle_legacy_cp_arguments(self):
|
||||||
legacy_mode_to_strategy = {
|
legacy_mode_to_strategy = {
|
||||||
"in-seq-split": "zigzag",
|
"in-seq-split": "zigzag",
|
||||||
|
|||||||
@@ -696,6 +696,73 @@ def commit_mamba_states_after_verify(
|
|||||||
model_runner = target_worker.model_runner
|
model_runner = target_worker.model_runner
|
||||||
if mambaish_config(model_runner.model_config) is None:
|
if mambaish_config(model_runner.model_config) is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# ReplaySSM spec-verify path (Part B of #28511): the accepted drafts already
|
||||||
|
# live in the per-slot circular ring (written during verify). Instead of
|
||||||
|
# scattering an intermediate full SSM state into `temporal`, advance the
|
||||||
|
# block-keyed cursors by the accepted count (the ring owns the SSM state; the
|
||||||
|
# verify/flush kernel folds it into `temporal` periodically). The CONV state
|
||||||
|
# still needs its usual accept-rollback, so we keep the conv-window scatter and
|
||||||
|
# skip only the SSM scatter. GDN-only + linear-chain (topk<=1) -- the runtime
|
||||||
|
# ring is allocated only then; KDA never allocates the cursors.
|
||||||
|
req_pool = model_runner.req_to_token_pool
|
||||||
|
mamba_pool = getattr(req_pool, "mamba_pool", None)
|
||||||
|
if (
|
||||||
|
mamba_pool is not None
|
||||||
|
and getattr(mamba_pool, "replayssm_cache_base", None) is not None
|
||||||
|
and not getattr(mamba_pool, "replayssm_is_kda", False)
|
||||||
|
):
|
||||||
|
if batch.forward_mode.is_idle() or accept_index.numel() == 0:
|
||||||
|
return
|
||||||
|
from sglang.kernels.ops.attention.fla.gdn_replayssm_spec_decode import (
|
||||||
|
commit_gdn_replayssm_spec,
|
||||||
|
)
|
||||||
|
from sglang.kernels.ops.mamba.mamba_state_scatter_triton import (
|
||||||
|
fused_conv_window_scatter_with_mask,
|
||||||
|
)
|
||||||
|
|
||||||
|
spec_state = req_pool.get_speculative_mamba2_params_all_layers()
|
||||||
|
bs = accept_lens.shape[0]
|
||||||
|
state_batch_indices = req_pool.get_mamba_indices(batch.req_pool_indices)
|
||||||
|
# Advance the per-slot circular cursors by the accepted count (incl. the
|
||||||
|
# bonus token). max_cache_len = ring length L = replayssm_d.shape[-2].
|
||||||
|
commit_gdn_replayssm_spec(
|
||||||
|
write_pos=mamba_pool.replayssm_write_pos,
|
||||||
|
cache_base=mamba_pool.replayssm_cache_base,
|
||||||
|
is_flush=mamba_pool.replayssm_is_flush,
|
||||||
|
num_accepted=accept_lens, # [bs], includes the bonus token
|
||||||
|
state_batch_indices=state_batch_indices,
|
||||||
|
max_cache_len=spec_state.replayssm_d.shape[-2],
|
||||||
|
max_spec_len=draft_token_num,
|
||||||
|
null_block_id=-1, # SGLang: valid slots >= 0, padding == -1
|
||||||
|
)
|
||||||
|
# Roll back / commit the conv state to the last accepted draft step
|
||||||
|
# (same logic as the recurrent commit, but conv-only).
|
||||||
|
accept_indices_offset = torch.arange(
|
||||||
|
0,
|
||||||
|
bs * draft_token_num,
|
||||||
|
step=draft_token_num,
|
||||||
|
dtype=accept_lens.dtype,
|
||||||
|
device=accept_lens.device,
|
||||||
|
)
|
||||||
|
req_idx = torch.arange(bs, dtype=torch.int64, device=accept_lens.device)
|
||||||
|
last_correct_step_indices = (
|
||||||
|
accept_index[req_idx, (accept_lens - 1).to(torch.int64)]
|
||||||
|
- accept_indices_offset
|
||||||
|
)
|
||||||
|
fused_conv_window_scatter_with_mask(
|
||||||
|
spec_state.conv[0],
|
||||||
|
spec_state.intermediate_conv_window[0],
|
||||||
|
state_batch_indices,
|
||||||
|
last_correct_step_indices,
|
||||||
|
)
|
||||||
|
# NOTE: radix mamba prefix-caching (mamba_track / extra_buffer) would need
|
||||||
|
# a device-side force-flush so `temporal` reflects the ring before a
|
||||||
|
# snapshot; not wired for Part B (server_args forbids extra_buffer with
|
||||||
|
# --enable-gdn-replayssm-spec), so the per-track scatters are intentionally
|
||||||
|
# skipped here.
|
||||||
|
return
|
||||||
|
|
||||||
attn_backend = model_runner.attn_backend
|
attn_backend = model_runner.attn_backend
|
||||||
|
|
||||||
bs = accept_lens.shape[0]
|
bs = accept_lens.shape[0]
|
||||||
|
|||||||
Reference in New Issue
Block a user