feat(short-conv): shared ShortConvAttnBackend for ZAYA1 CCA + LFM2 short conv (#29867)
This commit is contained in:
@@ -288,6 +288,7 @@ def attn_backend_wrapper(runner: "ModelRunner", full_attn_backend: "AttentionBac
|
|||||||
|
|
||||||
check_environments()
|
check_environments()
|
||||||
initialize_linear_attn_config(runner.server_args)
|
initialize_linear_attn_config(runner.server_args)
|
||||||
|
hybrid_backend_cls = HybridLinearAttnBackend
|
||||||
if runner.hybrid_gdn_config is not None:
|
if runner.hybrid_gdn_config is not None:
|
||||||
if is_blackwell():
|
if is_blackwell():
|
||||||
assert (
|
assert (
|
||||||
@@ -303,7 +304,46 @@ def attn_backend_wrapper(runner: "ModelRunner", full_attn_backend: "AttentionBac
|
|||||||
logger.info(f"Using hybrid linear attention backend for hybrid GDN models.")
|
logger.info(f"Using hybrid linear attention backend for hybrid GDN models.")
|
||||||
linear_attn_backend = GDNAttnBackend(runner)
|
linear_attn_backend = GDNAttnBackend(runner)
|
||||||
elif runner.mamba2_config is not None:
|
elif runner.mamba2_config is not None:
|
||||||
linear_attn_backend = Mamba2AttnBackend(runner)
|
from sglang.srt.configs.lfm2 import Lfm2Config
|
||||||
|
from sglang.srt.configs.lfm2_moe import Lfm2MoeConfig
|
||||||
|
from sglang.srt.configs.lfm2_vl import Lfm2VlConfig
|
||||||
|
from sglang.srt.configs.zaya import ZayaConfig
|
||||||
|
|
||||||
|
# Short-conv hybrids (ZAYA1 CCA, LFM2 short conv) share a conv-state
|
||||||
|
# sidecar that owns the per-request state plumbing and is invoked by
|
||||||
|
# the model via conv_state_metadata (never as a full-vs-linear
|
||||||
|
# alternative). Other mamba2 models keep the full Mamba2 SSM backend.
|
||||||
|
short_conv_cfgs = (
|
||||||
|
ZayaConfig,
|
||||||
|
Lfm2Config,
|
||||||
|
Lfm2MoeConfig,
|
||||||
|
Lfm2VlConfig,
|
||||||
|
)
|
||||||
|
if isinstance(runner.mamba2_config, short_conv_cfgs):
|
||||||
|
if is_npu():
|
||||||
|
# The model conv layers call
|
||||||
|
# get_attn_backend().conv_state_metadata() unconditionally,
|
||||||
|
# but the Ascend hybrid/mamba backend has no such method.
|
||||||
|
# Fail here (before model execution) with a clear message
|
||||||
|
# rather than an AttributeError deep in the first conv layer.
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Short-conv hybrid models (ZAYA1 CCA, LFM2 / LFM2-MoE) "
|
||||||
|
"are not yet supported on NPU: the conv-state sidecar "
|
||||||
|
"(ShortConvAttnBackend.conv_state_metadata) has no Ascend "
|
||||||
|
"implementation. Add an Ascend conv-state backend before "
|
||||||
|
"serving these models on NPU."
|
||||||
|
)
|
||||||
|
from sglang.srt.layers.attention.hybrid_linear_attn_backend import (
|
||||||
|
ShortConvHybridAttnBackend,
|
||||||
|
)
|
||||||
|
from sglang.srt.layers.attention.linear.short_conv_backend import (
|
||||||
|
ShortConvAttnBackend,
|
||||||
|
)
|
||||||
|
|
||||||
|
linear_attn_backend = ShortConvAttnBackend(runner)
|
||||||
|
hybrid_backend_cls = ShortConvHybridAttnBackend
|
||||||
|
else:
|
||||||
|
linear_attn_backend = Mamba2AttnBackend(runner)
|
||||||
elif runner.kimi_linear_config is not None:
|
elif runner.kimi_linear_config is not None:
|
||||||
linear_attn_backend = KDAAttnBackend(runner)
|
linear_attn_backend = KDAAttnBackend(runner)
|
||||||
elif runner.hybrid_lightning_config is not None:
|
elif runner.hybrid_lightning_config is not None:
|
||||||
@@ -325,7 +365,7 @@ def attn_backend_wrapper(runner: "ModelRunner", full_attn_backend: "AttentionBac
|
|||||||
full_attn_layers = [0]
|
full_attn_layers = [0]
|
||||||
else:
|
else:
|
||||||
full_attn_layers = cfg.full_attention_layer_ids
|
full_attn_layers = cfg.full_attention_layer_ids
|
||||||
return HybridLinearAttnBackend(
|
return hybrid_backend_cls(
|
||||||
full_attn_backend, linear_attn_backend, full_attn_layers
|
full_attn_backend, linear_attn_backend, full_attn_layers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1096,3 +1096,31 @@ class HybridLinearAttnBackend(AttentionBackend):
|
|||||||
mamba_track_indices,
|
mamba_track_indices,
|
||||||
mamba_steps_to_track,
|
mamba_steps_to_track,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ShortConvHybridAttnBackend(HybridLinearAttnBackend):
|
||||||
|
"""HybridLinearAttnBackend variant for short-conv hybrid models (ZAYA1 CCA,
|
||||||
|
LFM2 short conv).
|
||||||
|
|
||||||
|
The linear sidecar is a :class:`ShortConvAttnBackend
|
||||||
|
<sglang.srt.layers.attention.linear.short_conv_backend.ShortConvAttnBackend>`
|
||||||
|
that owns the per-request conv-state plumbing. The model's conv module
|
||||||
|
reaches it via :meth:`conv_state_metadata` (``get_attn_backend()`` returns
|
||||||
|
this wrapper) and runs its own conv kernel against the returned handle, so
|
||||||
|
the model definition holds no pool access. The sidecar is never reached
|
||||||
|
through the full-vs-linear ``forward_decode`` / ``forward_extend`` dispatch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
full_attn_backend: AttentionBackend,
|
||||||
|
short_conv_backend: MambaAttnBackendBase,
|
||||||
|
full_attn_layers: list,
|
||||||
|
):
|
||||||
|
# Register short_conv_backend as the linear sidecar so it rides in
|
||||||
|
# attn_backend_list and inherits the metadata / cuda-graph fan-out.
|
||||||
|
super().__init__(full_attn_backend, short_conv_backend, full_attn_layers)
|
||||||
|
self.short_conv_backend = short_conv_backend
|
||||||
|
|
||||||
|
def conv_state_metadata(self, layer_id: int, forward_batch: ForwardBatch):
|
||||||
|
return self.short_conv_backend.conv_state_metadata(layer_id, forward_batch)
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# Copyright 2023-2026 SGLang Team
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
# ==============================================================================
|
||||||
|
"""Short-convolution attention backend.
|
||||||
|
|
||||||
|
Several hybrid models interleave a *causal short conv with per-request conv
|
||||||
|
state* (stored in the centralized ``MambaPool``) with softmax attention layers:
|
||||||
|
|
||||||
|
* **LFM2** (:class:`Lfm2ShortConv <sglang.srt.models.lfm2.Lfm2ShortConv>`) --
|
||||||
|
a depthwise gated short conv (``causal_conv1d_fn`` / ``causal_conv1d_update``)
|
||||||
|
as a standalone token mixer on its own conv layers.
|
||||||
|
* **ZAYA1** (:class:`CCA <sglang.srt.models.zaya.CCA>`) -- a two-stage grouped
|
||||||
|
conv plus a one-token ``prev_hs`` lag, preprocessing q/k for the layer's
|
||||||
|
softmax attention.
|
||||||
|
|
||||||
|
These share the *state plumbing* -- resolving the per-request slot indices, the
|
||||||
|
``has_initial_state`` prefix mask, the ``query_start_loc`` cu-seqlens, and the
|
||||||
|
cuda-graph static index buffers, all once per forward step -- but NOT the conv
|
||||||
|
kernel itself. ``ShortConvAttnBackend`` owns only the plumbing and hands it out
|
||||||
|
via :meth:`conv_state_metadata` as a :class:`ShortConvMetadata`; each model runs
|
||||||
|
its own conv kernel against that handle, so the model definition holds no pool
|
||||||
|
access.
|
||||||
|
|
||||||
|
The backend is a *sidecar*: it is invoked directly by the model (through
|
||||||
|
:class:`ShortConvHybridAttnBackend
|
||||||
|
<sglang.srt.layers.attention.hybrid_linear_attn_backend.ShortConvHybridAttnBackend>`),
|
||||||
|
never through the full-vs-linear ``forward_decode`` / ``forward_extend``
|
||||||
|
dispatch. Metadata + cuda-graph capture/replay come from
|
||||||
|
:class:`MambaAttnBackendBase`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, List, NamedTuple, Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.srt.layers.attention.hybrid_linear_attn_backend import (
|
||||||
|
MambaAttnBackendBase,
|
||||||
|
)
|
||||||
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||||
|
|
||||||
|
|
||||||
|
class ShortConvMetadata(NamedTuple):
|
||||||
|
"""Per-(layer, step) conv-state handle handed to a model's conv kernel.
|
||||||
|
|
||||||
|
``layer_cache`` exposes the per-layer pool views (``conv[0]`` = conv state,
|
||||||
|
``conv[1]`` = an optional second state such as ZAYA1's ``prev_hs``,
|
||||||
|
``temporal`` = SSM state, unused by pure short convs). The device tensors are
|
||||||
|
cuda-graph-static on the decode/replay path; the ``*_cpu`` host mirrors are
|
||||||
|
built once per step only for models whose extend path runs a host loop
|
||||||
|
(e.g. ZAYA1 v1) and are ``None`` on decode.
|
||||||
|
"""
|
||||||
|
|
||||||
|
layer_cache: Any
|
||||||
|
cache_indices: torch.Tensor
|
||||||
|
# cu-seqlens for the varlen prefill conv (device, int32). None on decode.
|
||||||
|
query_start_loc: Optional[torch.Tensor] = None
|
||||||
|
# Per-request "resumes a cached prefix" mask (device bool). None on decode.
|
||||||
|
has_initial_state: Optional[torch.Tensor] = None
|
||||||
|
# Host mirror of cache_indices for extend host loops. None on decode.
|
||||||
|
slot_ids_cpu: Optional[List[int]] = None
|
||||||
|
# Host mirror of has_initial_state for extend host loops. None on decode.
|
||||||
|
has_prefix_cpu: Optional[List[bool]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ShortConvAttnBackend(MambaAttnBackendBase):
|
||||||
|
"""Owns the short-conv per-request state plumbing (see module docstring)."""
|
||||||
|
|
||||||
|
# State IO is index-driven; no host seq-lens plumbing required from the
|
||||||
|
# runner. (The extend path reads ``extend_*_cpu`` off the batch, which is
|
||||||
|
# always populated for extend regardless of this flag.)
|
||||||
|
needs_cpu_seq_lens: bool = False
|
||||||
|
|
||||||
|
def __init__(self, model_runner: ModelRunner):
|
||||||
|
super().__init__(model_runner)
|
||||||
|
mamba_cache = self.req_to_token_pool.mamba_pool.mamba_cache
|
||||||
|
# conv[0] == conv_state: [n_layers, n_slots, conv_dim, conv_kernel - 1]
|
||||||
|
self.conv_states_shape = mamba_cache.conv[0].shape
|
||||||
|
|
||||||
|
# Per-step state, resolved ONCE per step in init_forward_metadata /
|
||||||
|
# init_forward_metadata_out_graph (never per conv layer). The extend host
|
||||||
|
# mirrors drive the extend loop; ``_cache_indices`` is the int64 slot
|
||||||
|
# index view shared by all conv layers within the step.
|
||||||
|
self._has_initial_state: Optional[torch.Tensor] = None
|
||||||
|
self._slot_ids_cpu: Optional[List[int]] = None
|
||||||
|
self._has_prefix_cpu: Optional[List[bool]] = None
|
||||||
|
self._cache_indices: Optional[torch.Tensor] = None
|
||||||
|
self._cache_indices_buf: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
def _reset_step_state(self):
|
||||||
|
self._has_initial_state = None
|
||||||
|
self._slot_ids_cpu = None
|
||||||
|
self._has_prefix_cpu = None
|
||||||
|
|
||||||
|
def _alloc_cache_indices_buf(self, max_bs: int):
|
||||||
|
# Persistent int64 index buffer, refilled in place per step so the
|
||||||
|
# captured (cuda or cpu) graph reads a stable address.
|
||||||
|
self._cache_indices_buf = torch.empty(
|
||||||
|
max_bs, dtype=torch.int64, device=self.device
|
||||||
|
)
|
||||||
|
|
||||||
|
def _refresh_cache_indices(self):
|
||||||
|
# Resolve the int64 slot-index view ONCE per step, shared by every conv
|
||||||
|
# layer. When a graph index buffer is allocated and large enough, refill
|
||||||
|
# it IN PLACE and hand out a view -- the captured graph then reads a
|
||||||
|
# stable address that this (pre-replay) hook keeps current, so it is
|
||||||
|
# cuda- and cpu-graph safe. Otherwise (eager, or bs beyond the buffer)
|
||||||
|
# a fresh cast is fine.
|
||||||
|
md = self.forward_metadata
|
||||||
|
idx = md.mamba_cache_indices if md is not None else None
|
||||||
|
buf = self._cache_indices_buf
|
||||||
|
if idx is None:
|
||||||
|
self._cache_indices = None
|
||||||
|
elif buf is not None and idx.shape[0] <= buf.shape[0]:
|
||||||
|
n = idx.shape[0]
|
||||||
|
buf[:n].copy_(idx)
|
||||||
|
self._cache_indices = buf[:n]
|
||||||
|
else:
|
||||||
|
self._cache_indices = idx.to(torch.long)
|
||||||
|
|
||||||
|
def init_cuda_graph_state(self, max_bs: int, max_num_tokens: int):
|
||||||
|
super().init_cuda_graph_state(max_bs, max_num_tokens)
|
||||||
|
self._alloc_cache_indices_buf(max_bs)
|
||||||
|
|
||||||
|
def init_cpu_graph_state(self, max_bs: int, max_num_tokens: int):
|
||||||
|
super().init_cpu_graph_state(max_bs, max_num_tokens)
|
||||||
|
self._alloc_cache_indices_buf(max_bs)
|
||||||
|
|
||||||
|
def init_forward_metadata(self, forward_batch: ForwardBatch):
|
||||||
|
# Eager path (also the CPU-graph replay path). Builds
|
||||||
|
# self.forward_metadata and runs the deferred mamba clear/COW ops.
|
||||||
|
super().init_forward_metadata(forward_batch)
|
||||||
|
self._reset_step_state()
|
||||||
|
self._refresh_cache_indices()
|
||||||
|
mode = forward_batch.forward_mode
|
||||||
|
if (
|
||||||
|
mode.is_extend()
|
||||||
|
and not mode.is_target_verify()
|
||||||
|
and not mode.is_draft_extend_v2()
|
||||||
|
):
|
||||||
|
self._has_initial_state = forward_batch.extend_prefix_lens > 0
|
||||||
|
if self._cache_indices is not None:
|
||||||
|
self._slot_ids_cpu = self._cache_indices.tolist()
|
||||||
|
self._has_prefix_cpu = [
|
||||||
|
int(p) > 0 for p in forward_batch.extend_prefix_lens_cpu
|
||||||
|
]
|
||||||
|
|
||||||
|
def init_forward_metadata_out_graph(
|
||||||
|
self, forward_batch: ForwardBatch, in_capture: bool = False
|
||||||
|
):
|
||||||
|
# Decode cuda-graph capture + replay path -- no extend prefix state.
|
||||||
|
super().init_forward_metadata_out_graph(forward_batch, in_capture)
|
||||||
|
self._reset_step_state()
|
||||||
|
self._refresh_cache_indices()
|
||||||
|
|
||||||
|
def init_forward_metadata_capture_cpu_graph(self, *args, **kwargs):
|
||||||
|
# Decode CPU-graph capture path. The base fills forward_metadata but not
|
||||||
|
# the int64 view; without this the conv layers would capture a ``None``
|
||||||
|
# index (crash / corrupt state). Replay goes through init_forward_metadata
|
||||||
|
# and refills the SAME buffer, so the captured cpu graph reads a stable
|
||||||
|
# address kept current at replay.
|
||||||
|
super().init_forward_metadata_capture_cpu_graph(*args, **kwargs)
|
||||||
|
self._reset_step_state()
|
||||||
|
self._refresh_cache_indices()
|
||||||
|
|
||||||
|
def conv_state_metadata(
|
||||||
|
self, layer_id: int, forward_batch: ForwardBatch
|
||||||
|
) -> ShortConvMetadata:
|
||||||
|
"""Return the conv-state handle for ``layer_id`` at the current step.
|
||||||
|
|
||||||
|
The per-step fields are already resolved on ``self.forward_metadata`` /
|
||||||
|
``self._*`` (in ``init_forward_metadata`` / ``_out_graph``);
|
||||||
|
``forward_batch`` is accepted for interface parity with the unit-test
|
||||||
|
mock and is not otherwise required here.
|
||||||
|
"""
|
||||||
|
layer_cache = self.req_to_token_pool.mamba2_layer_cache(layer_id)
|
||||||
|
md = self.forward_metadata
|
||||||
|
|
||||||
|
# Slot indices are cached ONCE per step in init_forward_metadata /
|
||||||
|
# init_forward_metadata_out_graph (int64). Hand back the cached view -- no
|
||||||
|
# per-layer recompute. Decode is cuda-graph-safe because that view is a
|
||||||
|
# persistent buffer refilled in place before each replay.
|
||||||
|
return ShortConvMetadata(
|
||||||
|
layer_cache=layer_cache,
|
||||||
|
cache_indices=self._cache_indices,
|
||||||
|
query_start_loc=md.query_start_loc,
|
||||||
|
has_initial_state=self._has_initial_state,
|
||||||
|
slot_ids_cpu=self._slot_ids_cpu,
|
||||||
|
has_prefix_cpu=self._has_prefix_cpu,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The short-conv layers are invoked via conv_state_metadata + the model's own
|
||||||
|
# conv kernel, never through the HybridLinearAttnBackend full-vs-linear
|
||||||
|
# dispatch. Mirror Mamba2AttnBackend and guard the routed entrypoints.
|
||||||
|
def forward_decode(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"ShortConvAttnBackend is invoked via conv_state_metadata; "
|
||||||
|
"it does not run through forward_decode."
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward_extend(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"ShortConvAttnBackend is invoked via conv_state_metadata; "
|
||||||
|
"it does not run through forward_extend."
|
||||||
|
)
|
||||||
@@ -96,6 +96,8 @@ def causal_conv1d_fn(
|
|||||||
x = x.contiguous()
|
x = x.contiguous()
|
||||||
bias = bias.contiguous() if bias is not None else None
|
bias = bias.contiguous() if bias is not None else None
|
||||||
|
|
||||||
|
if cache_indices is not None and cache_indices.dtype != torch.int32:
|
||||||
|
cache_indices = cache_indices.to(torch.int32)
|
||||||
causal_conv1d_fwd(
|
causal_conv1d_fwd(
|
||||||
x,
|
x,
|
||||||
weight,
|
weight,
|
||||||
@@ -162,6 +164,8 @@ def causal_conv1d_update(
|
|||||||
unsqueeze = x.dim() == 2
|
unsqueeze = x.dim() == 2
|
||||||
if unsqueeze:
|
if unsqueeze:
|
||||||
x = x.unsqueeze(-1)
|
x = x.unsqueeze(-1)
|
||||||
|
if conv_state_indices is not None and conv_state_indices.dtype != torch.int32:
|
||||||
|
conv_state_indices = conv_state_indices.to(torch.int32)
|
||||||
causal_conv1d_update_kernel(
|
causal_conv1d_update_kernel(
|
||||||
x,
|
x,
|
||||||
conv_state,
|
conv_state,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ from sglang.srt.layers.vocab_parallel_embedding import (
|
|||||||
VocabParallelEmbedding,
|
VocabParallelEmbedding,
|
||||||
)
|
)
|
||||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||||
from sglang.srt.model_executor.forward_context import get_req_to_token_pool
|
from sglang.srt.model_executor.forward_context import get_attn_backend
|
||||||
from sglang.srt.model_loader.weight_utils import (
|
from sglang.srt.model_loader.weight_utils import (
|
||||||
default_weight_loader,
|
default_weight_loader,
|
||||||
sharded_weight_loader,
|
sharded_weight_loader,
|
||||||
@@ -265,10 +265,11 @@ class Lfm2ShortConv(nn.Module):
|
|||||||
if forward_batch.forward_mode.is_idle():
|
if forward_batch.forward_mode.is_idle():
|
||||||
return hidden_states
|
return hidden_states
|
||||||
|
|
||||||
layer_cache = get_req_to_token_pool().mamba2_layer_cache(self.layer_idx)
|
# The backend owns the per-request conv-state plumbing (slot indices,
|
||||||
conv_state = layer_cache.conv[0]
|
# prefix mask, cu-seqlens, cuda-graph buffers); this layer just runs its
|
||||||
req_pool_indices = forward_batch.req_pool_indices
|
# depthwise conv against the returned handle.
|
||||||
mamba_indices = get_req_to_token_pool().get_mamba_indices(req_pool_indices)
|
meta = get_attn_backend().conv_state_metadata(self.layer_idx, forward_batch)
|
||||||
|
conv_state = meta.layer_cache.conv[0]
|
||||||
|
|
||||||
# Project and split into gates: B (pre-conv), C (post-conv), x (input)
|
# Project and split into gates: B (pre-conv), C (post-conv), x (input)
|
||||||
proj, _ = self.in_proj(hidden_states)
|
proj, _ = self.in_proj(hidden_states)
|
||||||
@@ -283,40 +284,18 @@ class Lfm2ShortConv(nn.Module):
|
|||||||
self.conv_weight,
|
self.conv_weight,
|
||||||
self.conv_bias,
|
self.conv_bias,
|
||||||
activation=None,
|
activation=None,
|
||||||
conv_state_indices=mamba_indices.to(torch.int32),
|
conv_state_indices=meta.cache_indices,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Prefill: multiple tokens, use varlen kernel
|
# Prefill: multiple tokens, use varlen kernel
|
||||||
T = hidden_states.shape[0]
|
|
||||||
Bx_t = Bx.transpose(0, 1).contiguous()
|
Bx_t = Bx.transpose(0, 1).contiguous()
|
||||||
|
|
||||||
# Build query_start_loc: [0, cumsum(seq_lens)...]
|
|
||||||
extend_start_loc = forward_batch.extend_start_loc
|
|
||||||
if extend_start_loc is not None and len(extend_start_loc) > 1:
|
|
||||||
query_start_loc = torch.cat(
|
|
||||||
[
|
|
||||||
extend_start_loc,
|
|
||||||
torch.tensor(
|
|
||||||
[T], dtype=torch.int32, device=hidden_states.device
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
cache_indices = mamba_indices.to(torch.int32)
|
|
||||||
has_initial_state = forward_batch.extend_prefix_lens > 0
|
|
||||||
else:
|
|
||||||
query_start_loc = torch.tensor(
|
|
||||||
[0, T], dtype=torch.int32, device=hidden_states.device
|
|
||||||
)
|
|
||||||
cache_indices = mamba_indices[:1].to(torch.int32)
|
|
||||||
has_initial_state = forward_batch.extend_prefix_lens[:1] > 0
|
|
||||||
|
|
||||||
conv_out = causal_conv1d_fn(
|
conv_out = causal_conv1d_fn(
|
||||||
Bx_t,
|
Bx_t,
|
||||||
self.conv_weight,
|
self.conv_weight,
|
||||||
self.conv_bias,
|
self.conv_bias,
|
||||||
query_start_loc=query_start_loc,
|
query_start_loc=meta.query_start_loc,
|
||||||
cache_indices=cache_indices,
|
cache_indices=meta.cache_indices,
|
||||||
has_initial_state=has_initial_state,
|
has_initial_state=meta.has_initial_state,
|
||||||
conv_states=conv_state,
|
conv_states=conv_state,
|
||||||
activation=None,
|
activation=None,
|
||||||
).transpose(0, 1)
|
).transpose(0, 1)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ from sglang.srt.layers.vocab_parallel_embedding import (
|
|||||||
VocabParallelEmbedding,
|
VocabParallelEmbedding,
|
||||||
)
|
)
|
||||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||||
from sglang.srt.model_executor.forward_context import get_req_to_token_pool
|
from sglang.srt.model_executor.forward_context import get_attn_backend
|
||||||
from sglang.srt.model_loader.weight_utils import (
|
from sglang.srt.model_loader.weight_utils import (
|
||||||
default_weight_loader,
|
default_weight_loader,
|
||||||
sharded_weight_loader,
|
sharded_weight_loader,
|
||||||
@@ -328,10 +328,11 @@ class Lfm2MoeShortConv(nn.Module):
|
|||||||
if forward_batch.forward_mode.is_idle():
|
if forward_batch.forward_mode.is_idle():
|
||||||
return hidden_states
|
return hidden_states
|
||||||
|
|
||||||
layer_cache = get_req_to_token_pool().mamba2_layer_cache(self.layer_idx)
|
# The backend owns the per-request conv-state plumbing (slot indices,
|
||||||
conv_state = layer_cache.conv[0]
|
# prefix mask, cu-seqlens, cuda-graph buffers); this layer just runs its
|
||||||
req_pool_indices = forward_batch.req_pool_indices
|
# depthwise conv against the returned handle.
|
||||||
mamba_indices = get_req_to_token_pool().get_mamba_indices(req_pool_indices)
|
meta = get_attn_backend().conv_state_metadata(self.layer_idx, forward_batch)
|
||||||
|
conv_state = meta.layer_cache.conv[0]
|
||||||
|
|
||||||
proj, _ = self.in_proj(hidden_states)
|
proj, _ = self.in_proj(hidden_states)
|
||||||
B_gate, C_gate, x = proj.chunk(3, dim=-1)
|
B_gate, C_gate, x = proj.chunk(3, dim=-1)
|
||||||
@@ -344,36 +345,17 @@ class Lfm2MoeShortConv(nn.Module):
|
|||||||
self.conv_weight,
|
self.conv_weight,
|
||||||
self.conv_bias,
|
self.conv_bias,
|
||||||
activation=None,
|
activation=None,
|
||||||
conv_state_indices=mamba_indices.to(torch.int32),
|
conv_state_indices=meta.cache_indices,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
T = hidden_states.shape[0]
|
|
||||||
Bx_t = Bx.transpose(0, 1).contiguous()
|
Bx_t = Bx.transpose(0, 1).contiguous()
|
||||||
|
|
||||||
# Build query_start_loc for variable-length sequences
|
|
||||||
# causal_conv1d_fn expects [start0, start1, ..., startN, T]
|
|
||||||
extend_start_loc = forward_batch.extend_start_loc
|
|
||||||
if extend_start_loc is not None and len(extend_start_loc) > 1:
|
|
||||||
# Multiple sequences: append T to extend_start_loc
|
|
||||||
# Allocate and fill to avoid torch.cat overhead
|
|
||||||
query_start_loc = extend_start_loc.new_empty(len(extend_start_loc) + 1)
|
|
||||||
query_start_loc[:-1] = extend_start_loc
|
|
||||||
query_start_loc[-1] = T
|
|
||||||
cache_indices = mamba_indices.to(torch.int32)
|
|
||||||
has_initial_state = forward_batch.extend_prefix_lens > 0
|
|
||||||
else:
|
|
||||||
# Single sequence: [0, T]
|
|
||||||
query_start_loc = hidden_states.new_tensor([0, T], dtype=torch.int32)
|
|
||||||
cache_indices = mamba_indices[:1].to(torch.int32)
|
|
||||||
has_initial_state = forward_batch.extend_prefix_lens[:1] > 0
|
|
||||||
|
|
||||||
conv_out = causal_conv1d_fn(
|
conv_out = causal_conv1d_fn(
|
||||||
Bx_t,
|
Bx_t,
|
||||||
self.conv_weight,
|
self.conv_weight,
|
||||||
self.conv_bias,
|
self.conv_bias,
|
||||||
query_start_loc=query_start_loc,
|
query_start_loc=meta.query_start_loc,
|
||||||
cache_indices=cache_indices,
|
cache_indices=meta.cache_indices,
|
||||||
has_initial_state=has_initial_state,
|
has_initial_state=meta.has_initial_state,
|
||||||
conv_states=conv_state,
|
conv_states=conv_state,
|
||||||
activation=None,
|
activation=None,
|
||||||
).transpose(0, 1)
|
).transpose(0, 1)
|
||||||
|
|||||||
+256
-257
@@ -31,9 +31,13 @@ for the full design notes):
|
|||||||
averaging across MoE layers) and MOD (mixture-of-depths skip expert).
|
averaging across MoE layers) and MOD (mixture-of-depths skip expert).
|
||||||
- Per-layer :class:`ResidualScaling` keeps the residual stream in fp32 with
|
- Per-layer :class:`ResidualScaling` keeps the residual stream in fp32 with
|
||||||
affine scale/bias both on the residual and on the post-mixer hidden states.
|
affine scale/bias both on the residual and on the post-mixer hidden states.
|
||||||
- Per-request CCA state (``conv_state`` + ``prev_hs``) is managed by
|
- Per-request CCA state (``conv_state`` + ``prev_hs``) lives in SGLang's
|
||||||
SGLang's centralized ``MambaPool`` inside ``HybridReqToTokenPool``,
|
centralized ``MambaPool`` inside ``HybridReqToTokenPool``. The per-request
|
||||||
accessed via ``get_req_to_token_pool().mamba2_layer_cache()``.
|
state plumbing (slot indices, prefix mask, cuda-graph buffers) is owned by
|
||||||
|
``ShortConvAttnBackend`` and reached via
|
||||||
|
``get_attn_backend().conv_state_metadata()``, so the model holds no pool
|
||||||
|
access; CCA runs its own conv (:func:`cca_extend` / :func:`cca_decode`)
|
||||||
|
against the returned handle.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -41,7 +45,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from typing import Optional
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
@@ -70,7 +74,7 @@ from sglang.srt.layers.vocab_parallel_embedding import (
|
|||||||
VocabParallelEmbedding,
|
VocabParallelEmbedding,
|
||||||
)
|
)
|
||||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
|
||||||
from sglang.srt.model_executor.forward_context import get_req_to_token_pool
|
from sglang.srt.model_executor.forward_context import get_attn_backend
|
||||||
from sglang.srt.model_loader.weight_utils import default_weight_loader
|
from sglang.srt.model_loader.weight_utils import default_weight_loader
|
||||||
from sglang.srt.runtime_context import get_parallel
|
from sglang.srt.runtime_context import get_parallel
|
||||||
from sglang.srt.utils import add_prefix, make_layers, set_weight_attrs
|
from sglang.srt.utils import add_prefix, make_layers, set_weight_attrs
|
||||||
@@ -78,14 +82,6 @@ from sglang.srt.utils import add_prefix, make_layers, set_weight_attrs
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Attribute names used to memoize the per-request MambaPool slot indices on the
|
|
||||||
# ForwardBatch. The req -> slot mapping is identical for every CCA layer in a
|
|
||||||
# step, so caching it here makes the lookup (and its GPU->CPU sync) run once per
|
|
||||||
# forward step instead of once per attention layer.
|
|
||||||
_MAMBA_INDICES_ATTR = "_zaya_mamba_indices"
|
|
||||||
_MAMBA_INDICES_CPU_ATTR = "_zaya_mamba_indices_cpu"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Residual scaling
|
# Residual scaling
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -141,6 +137,187 @@ def _apply_norm_with_fp32_residual(
|
|||||||
return norm(residual.to(target_dtype))
|
return norm(residual.to(target_dtype))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CCA conv-state kernels (v1 torch)
|
||||||
|
#
|
||||||
|
# ZAYA1-specific conv step: the CCA conv is a causal two-stage conv over
|
||||||
|
# ``qk = [W_q hs || W_k hs]`` plus a one-token ``prev_hs`` lag for val_proj2.
|
||||||
|
# The per-request conv state lives in the centralized MambaPool; the backend
|
||||||
|
# (ShortConvAttnBackend) hands out the slot indices + prefix flags and CCA runs
|
||||||
|
# these functions against them. ``conv_qk`` is the module's two-stage conv;
|
||||||
|
# both functions mutate ``conv_state`` / ``prev_hs_state`` in place and return
|
||||||
|
# ``(qk_out, v2_input)`` -- the conv output ``[T, in_out_ch]`` and the (shifted)
|
||||||
|
# ``val_proj2`` input ``[T, hidden_size]``.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def cca_extend(
|
||||||
|
qk: torch.Tensor,
|
||||||
|
hidden_states: torch.Tensor,
|
||||||
|
conv_qk: nn.Module,
|
||||||
|
conv_state: torch.Tensor,
|
||||||
|
prev_hs_state: torch.Tensor,
|
||||||
|
slot_ids: List[int],
|
||||||
|
has_prefix: List[bool],
|
||||||
|
extend_seq_lens_cpu: List[int],
|
||||||
|
total_padding: Optional[int] = None,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
"""Prefill / extend conv-state step (v1, pure torch).
|
||||||
|
|
||||||
|
Walks each request in the batch, applies ``conv_qk`` with the request's own
|
||||||
|
initial state (zeros on a fresh first chunk, the cached ``conv_state`` slot
|
||||||
|
otherwise), writes the updated ``conv_state`` / ``prev_hs_state`` back, and
|
||||||
|
returns the concatenated ``(qk_out, v2_input)`` in the original token layout.
|
||||||
|
|
||||||
|
``slot_ids`` is the host mirror of the per-request MambaPool slot indices and
|
||||||
|
``has_prefix[i]`` is ``True`` when request ``i`` resumes a cached prefix.
|
||||||
|
|
||||||
|
The Triton swap (:func:`cca_conv1d_fn`) removes this per-request loop.
|
||||||
|
"""
|
||||||
|
dtype = hidden_states.dtype
|
||||||
|
if total_padding is None:
|
||||||
|
total_padding = conv_state.shape[-1]
|
||||||
|
in_out_ch = qk.shape[-1]
|
||||||
|
hidden_size = hidden_states.shape[-1]
|
||||||
|
|
||||||
|
qk_out = torch.empty_like(qk)
|
||||||
|
v2_input = torch.empty_like(hidden_states)
|
||||||
|
|
||||||
|
# Fresh-prefill fast path: when no request has a cached prefix the per-request
|
||||||
|
# convs can be coalesced into a single packed convolution. Each request's
|
||||||
|
# segment is laid out as ``[total_padding zeros, S_i tokens]``.
|
||||||
|
all_fresh = bool(extend_seq_lens_cpu) and not any(has_prefix)
|
||||||
|
|
||||||
|
if all_fresh:
|
||||||
|
seq_lens = [int(s) for s in extend_seq_lens_cpu]
|
||||||
|
pad = total_padding
|
||||||
|
offsets_in = [0]
|
||||||
|
for s in seq_lens:
|
||||||
|
offsets_in.append(offsets_in[-1] + s + pad)
|
||||||
|
packed = qk.new_zeros((1, in_out_ch, offsets_in[-1]))
|
||||||
|
start = 0
|
||||||
|
for i, s in enumerate(seq_lens):
|
||||||
|
end = start + s
|
||||||
|
packed[0, :, offsets_in[i] + pad : offsets_in[i + 1]] = qk[
|
||||||
|
start:end
|
||||||
|
].transpose(0, 1)
|
||||||
|
start = end
|
||||||
|
|
||||||
|
packed_out = conv_qk(packed) # [1, C, offsets_in[-1] - pad]
|
||||||
|
|
||||||
|
start = 0
|
||||||
|
for i, s in enumerate(seq_lens):
|
||||||
|
end = start + s
|
||||||
|
a_i = offsets_in[i]
|
||||||
|
qk_out[start:end] = packed_out[0, :, a_i : a_i + s].transpose(0, 1)
|
||||||
|
new_state = packed[0, :, a_i + s : a_i + s + pad]
|
||||||
|
conv_state[slot_ids[i]] = new_state.to(conv_state.dtype)
|
||||||
|
|
||||||
|
hs_cur = hidden_states[start:end]
|
||||||
|
first = hidden_states.new_zeros((1, hidden_size))
|
||||||
|
v2_input[start:end] = torch.cat([first, hs_cur[:-1]], dim=0)
|
||||||
|
prev_hs_state[slot_ids[i]] = (
|
||||||
|
hs_cur[-1].unsqueeze(-1).to(prev_hs_state.dtype)
|
||||||
|
)
|
||||||
|
start = end
|
||||||
|
else:
|
||||||
|
start = 0
|
||||||
|
for i, seq_len in enumerate(extend_seq_lens_cpu):
|
||||||
|
end = start + int(seq_len)
|
||||||
|
slot = slot_ids[i]
|
||||||
|
prefix = bool(has_prefix[i])
|
||||||
|
|
||||||
|
qk_cur = qk[start:end].transpose(0, 1).unsqueeze(0) # [1, C, S_cur]
|
||||||
|
if prefix:
|
||||||
|
left_pad = conv_state[slot].unsqueeze(0).to(dtype)
|
||||||
|
else:
|
||||||
|
left_pad = qk_cur.new_zeros((1, in_out_ch, total_padding))
|
||||||
|
padded = torch.cat([left_pad, qk_cur], dim=-1)
|
||||||
|
|
||||||
|
out = conv_qk(padded) # [1, C, S_cur]
|
||||||
|
qk_out[start:end] = out.squeeze(0).transpose(0, 1)
|
||||||
|
|
||||||
|
new_state = padded[..., -total_padding:]
|
||||||
|
conv_state[slot] = new_state.squeeze(0).to(conv_state.dtype)
|
||||||
|
|
||||||
|
hs_cur = hidden_states[start:end]
|
||||||
|
if prefix:
|
||||||
|
first = prev_hs_state[slot].squeeze(-1).to(dtype).unsqueeze(0)
|
||||||
|
else:
|
||||||
|
first = hidden_states.new_zeros((1, hidden_size))
|
||||||
|
v2_input[start:end] = torch.cat([first, hs_cur[:-1]], dim=0)
|
||||||
|
|
||||||
|
prev_hs_state[slot] = hs_cur[-1].unsqueeze(-1).to(prev_hs_state.dtype)
|
||||||
|
start = end
|
||||||
|
|
||||||
|
return qk_out, v2_input
|
||||||
|
|
||||||
|
|
||||||
|
def cca_decode(
|
||||||
|
qk: torch.Tensor,
|
||||||
|
hidden_states: torch.Tensor,
|
||||||
|
conv_qk: nn.Module,
|
||||||
|
conv_state: torch.Tensor,
|
||||||
|
prev_hs_state: torch.Tensor,
|
||||||
|
mamba_indices: torch.Tensor,
|
||||||
|
total_padding: Optional[int] = None,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
"""Single-token decode conv-state step (v1, pure torch).
|
||||||
|
|
||||||
|
Gathers each request's cached ``conv_state`` / ``prev_hs_state`` via
|
||||||
|
``index_select``, runs ``conv_qk`` on the ``[T, C, total_padding + 1]``
|
||||||
|
window, and scatters the updated state back with ``index_copy_``. All ops are
|
||||||
|
on-device (``mamba_indices`` is a device ``long`` tensor), so this stays
|
||||||
|
CUDA-graph capturable. Returns ``(qk_out, prev_hs)`` where ``prev_hs`` is the
|
||||||
|
previous hidden state feeding ``val_proj2``.
|
||||||
|
|
||||||
|
The Triton swap is :func:`cca_conv1d_update`.
|
||||||
|
"""
|
||||||
|
dtype = hidden_states.dtype
|
||||||
|
if total_padding is None:
|
||||||
|
total_padding = conv_state.shape[-1]
|
||||||
|
|
||||||
|
left_pad = conv_state.index_select(0, mamba_indices).to(dtype)
|
||||||
|
cur = qk.unsqueeze(-1) # [T, C, 1]
|
||||||
|
padded = torch.cat([left_pad, cur], dim=-1) # [T, C, total_padding + 1]
|
||||||
|
out = conv_qk(padded) # [T, C, 1]
|
||||||
|
qk_out = out.squeeze(-1) # [T, C]
|
||||||
|
|
||||||
|
new_state = padded[..., -total_padding:]
|
||||||
|
conv_state.index_copy_(0, mamba_indices, new_state.to(conv_state.dtype))
|
||||||
|
|
||||||
|
# Read the previous hidden state (val_proj2 input) BEFORE overwriting the
|
||||||
|
# slot with the current token.
|
||||||
|
prev_hs = prev_hs_state.index_select(0, mamba_indices).squeeze(-1).to(dtype)
|
||||||
|
prev_hs_state.index_copy_(
|
||||||
|
0, mamba_indices, hidden_states.unsqueeze(-1).to(prev_hs_state.dtype)
|
||||||
|
)
|
||||||
|
return qk_out, prev_hs
|
||||||
|
|
||||||
|
|
||||||
|
# Fused kernel seam (TODO) -- perf swap for the v1 torch paths above. These
|
||||||
|
# mirror the ``causal_conv1d_fn`` / ``causal_conv1d_update`` contract but for
|
||||||
|
# CCA's two-stage *grouped* conv (conv_qk[0] depthwise + conv_qk[1] grouped
|
||||||
|
# per-head), which the stock depthwise ``causal_conv1d`` cannot express. Once
|
||||||
|
# implemented they replace the per-request loop in ``cca_extend`` and the
|
||||||
|
# separate gather/conv/scatter launches in ``cca_decode`` with a single
|
||||||
|
# index-driven kernel. Same ``(qk_out, v2_input)`` return contract.
|
||||||
|
|
||||||
|
|
||||||
|
def cca_conv1d_fn(*args, **kwargs):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Fused CCA prefill conv-with-state kernel not implemented yet; "
|
||||||
|
"the model uses cca_extend (v1 torch) in the meantime."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cca_conv1d_update(*args, **kwargs):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Fused CCA decode conv-with-state kernel not implemented yet; "
|
||||||
|
"the model uses cca_decode (v1 torch) in the meantime."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# CCA: Compressed Convolutional Attention QKV projection
|
# CCA: Compressed Convolutional Attention QKV projection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -401,58 +578,6 @@ class CCA(nn.Module):
|
|||||||
|
|
||||||
# ----- helpers ---------------------------------------------------------
|
# ----- helpers ---------------------------------------------------------
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_mamba_indices(forward_batch: ForwardBatch) -> torch.Tensor:
|
|
||||||
"""Per-request MambaPool slot indices as an int64 device tensor.
|
|
||||||
|
|
||||||
The req -> slot mapping depends only on ``forward_batch.req_pool_indices``,
|
|
||||||
which is constant for every CCA layer within one forward step. Computing
|
|
||||||
it inside each of the ~60 attention layers would issue one redundant
|
|
||||||
gather per layer, so it is computed once and memoized on the ForwardBatch
|
|
||||||
(whose lifetime is exactly one forward step). The lookup is pure on-device
|
|
||||||
work, so this stays compatible with CUDA graph capture on the decode path.
|
|
||||||
"""
|
|
||||||
cached = getattr(forward_batch, _MAMBA_INDICES_ATTR, None)
|
|
||||||
if cached is None:
|
|
||||||
cached = (
|
|
||||||
get_req_to_token_pool()
|
|
||||||
.get_mamba_indices(forward_batch.req_pool_indices)
|
|
||||||
.to(torch.long)
|
|
||||||
)
|
|
||||||
setattr(forward_batch, _MAMBA_INDICES_ATTR, cached)
|
|
||||||
return cached
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_mamba_indices_cpu(
|
|
||||||
forward_batch: ForwardBatch, mamba_indices: torch.Tensor
|
|
||||||
) -> list[int]:
|
|
||||||
"""Host mirror of :meth:`_get_mamba_indices`, memoized per forward step.
|
|
||||||
|
|
||||||
Only the extend/prefill path needs the indices on the host to drive its
|
|
||||||
per-request Python loop; the decode path indexes the pool entirely
|
|
||||||
on-device. Memoizing turns the previous one-``.tolist()``-sync-per-layer
|
|
||||||
behavior into a single GPU->CPU sync per forward step. This helper is
|
|
||||||
never reached on the decode path that CUDA graphs capture.
|
|
||||||
"""
|
|
||||||
cached = getattr(forward_batch, _MAMBA_INDICES_CPU_ATTR, None)
|
|
||||||
if cached is None:
|
|
||||||
cached = mamba_indices.tolist()
|
|
||||||
setattr(forward_batch, _MAMBA_INDICES_CPU_ATTR, cached)
|
|
||||||
return cached
|
|
||||||
|
|
||||||
def _get_pool_state(self, forward_batch: ForwardBatch):
|
|
||||||
"""Retrieve per-request CCA state from the centralized MambaPool.
|
|
||||||
|
|
||||||
``conv_state`` / ``prev_hs_state`` are layer-local pool views, but the
|
|
||||||
``mamba_indices`` req -> slot mapping is shared across layers and so is
|
|
||||||
memoized on the ForwardBatch (see :meth:`_get_mamba_indices`).
|
|
||||||
"""
|
|
||||||
layer_cache = get_req_to_token_pool().mamba2_layer_cache(self.layer_id)
|
|
||||||
conv_state = layer_cache.conv[0]
|
|
||||||
prev_hs_state = layer_cache.conv[1]
|
|
||||||
mamba_indices = self._get_mamba_indices(forward_batch)
|
|
||||||
return conv_state, prev_hs_state, mamba_indices
|
|
||||||
|
|
||||||
def _normalize_qk(
|
def _normalize_qk(
|
||||||
self, query: torch.Tensor, key: torch.Tensor
|
self, query: torch.Tensor, key: torch.Tensor
|
||||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
@@ -583,195 +708,6 @@ class CCA(nn.Module):
|
|||||||
value = self._slice_v_per_rank(value_full)
|
value = self._slice_v_per_rank(value_full)
|
||||||
return query, key, value
|
return query, key, value
|
||||||
|
|
||||||
def _forward_extend(
|
|
||||||
self,
|
|
||||||
hidden_states: torch.Tensor,
|
|
||||||
forward_batch: ForwardBatch,
|
|
||||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
||||||
"""Prefill / extend path.
|
|
||||||
|
|
||||||
Walks every request in the batch, applies the conv with each request's
|
|
||||||
own initial state (zero on first chunk, cached otherwise), writes the
|
|
||||||
updated state and ``prev_hs`` back into the centralized MambaPool, and
|
|
||||||
returns the concatenated q/k/v in the original token layout.
|
|
||||||
"""
|
|
||||||
dtype = hidden_states.dtype
|
|
||||||
T = hidden_states.shape[0]
|
|
||||||
|
|
||||||
q_raw, _ = self.linear_q(hidden_states) # [T, latent_q]
|
|
||||||
k_raw, _ = self.linear_k(hidden_states)
|
|
||||||
qk = torch.cat([q_raw, k_raw], dim=-1) # [T, in_out_ch]
|
|
||||||
|
|
||||||
query_pre = q_raw.view(T, self.num_q_heads, self.head_dim)
|
|
||||||
key_base = k_raw.view(T, self.num_k_heads, self.head_dim)
|
|
||||||
|
|
||||||
qk_out = torch.empty_like(qk)
|
|
||||||
v2_input = torch.empty_like(hidden_states)
|
|
||||||
|
|
||||||
conv_state, prev_hs_state, mamba_indices = self._get_pool_state(forward_batch)
|
|
||||||
# Host view of the slot indices to drive the per-request loop below.
|
|
||||||
# Memoized on the ForwardBatch, so the GPU->CPU sync runs once per forward
|
|
||||||
# step rather than once per attention layer (~60 syncs/step otherwise).
|
|
||||||
mamba_idx_cpu = self._get_mamba_indices_cpu(forward_batch, mamba_indices)
|
|
||||||
|
|
||||||
extend_seq_lens_cpu = forward_batch.extend_seq_lens_cpu
|
|
||||||
extend_prefix_lens_cpu = forward_batch.extend_prefix_lens_cpu
|
|
||||||
|
|
||||||
# Fresh-prefill fast path: when no request has a cached prefix the
|
|
||||||
# per-request convs (one launch each, ×60 attention layers ×B requests)
|
|
||||||
# can be coalesced into a single packed convolution. The conv chain is
|
|
||||||
# two ``kernel_size=2`` convs (effective receptive field = 3), so each
|
|
||||||
# request's S valid outputs are produced from the packed positions
|
|
||||||
# ``[a_i, a_i + S_i - 1]`` where the input segment for request i is
|
|
||||||
# ``[pad, pad, x_0, ..., x_{S-1}]`` of length ``S_i + total_padding``.
|
|
||||||
all_fresh = bool(extend_seq_lens_cpu) and not any(
|
|
||||||
int(p) > 0 for p in extend_prefix_lens_cpu
|
|
||||||
)
|
|
||||||
|
|
||||||
if all_fresh:
|
|
||||||
seq_lens = [int(s) for s in extend_seq_lens_cpu]
|
|
||||||
pad = self.total_padding
|
|
||||||
# Build packed buffer: per request -> [pad zeros, S_i tokens].
|
|
||||||
offsets_in = [0]
|
|
||||||
for s in seq_lens:
|
|
||||||
offsets_in.append(offsets_in[-1] + s + pad)
|
|
||||||
packed = qk.new_zeros((1, self.in_out_ch, offsets_in[-1]))
|
|
||||||
start = 0
|
|
||||||
for i, s in enumerate(seq_lens):
|
|
||||||
end = start + s
|
|
||||||
packed[0, :, offsets_in[i] + pad : offsets_in[i + 1]] = qk[
|
|
||||||
start:end
|
|
||||||
].transpose(0, 1)
|
|
||||||
start = end
|
|
||||||
|
|
||||||
packed_out = self._conv_qk_run(packed) # [1, C, offsets_in[-1] - pad]
|
|
||||||
|
|
||||||
start = 0
|
|
||||||
for i, s in enumerate(seq_lens):
|
|
||||||
end = start + s
|
|
||||||
a_i = offsets_in[i]
|
|
||||||
qk_out[start:end] = packed_out[0, :, a_i : a_i + s].transpose(0, 1)
|
|
||||||
new_state = packed[0, :, a_i + s : a_i + s + pad]
|
|
||||||
conv_state[mamba_idx_cpu[i]] = new_state.to(conv_state.dtype)
|
|
||||||
|
|
||||||
hs_cur = hidden_states[start:end]
|
|
||||||
first = hidden_states.new_zeros((1, self.hidden_size))
|
|
||||||
v2_input[start:end] = torch.cat([first, hs_cur[:-1]], dim=0)
|
|
||||||
prev_hs_state[mamba_idx_cpu[i]] = (
|
|
||||||
hs_cur[-1].unsqueeze(-1).to(prev_hs_state.dtype)
|
|
||||||
)
|
|
||||||
start = end
|
|
||||||
else:
|
|
||||||
start = 0
|
|
||||||
for i, seq_len in enumerate(extend_seq_lens_cpu):
|
|
||||||
end = start + int(seq_len)
|
|
||||||
mamba_idx = mamba_idx_cpu[i]
|
|
||||||
has_prefix = int(extend_prefix_lens_cpu[i]) > 0
|
|
||||||
|
|
||||||
qk_cur = qk[start:end].transpose(0, 1).unsqueeze(0) # [1, C, S_cur]
|
|
||||||
if has_prefix:
|
|
||||||
left_pad = conv_state[mamba_idx].unsqueeze(0).to(dtype)
|
|
||||||
else:
|
|
||||||
left_pad = qk_cur.new_zeros((1, self.in_out_ch, self.total_padding))
|
|
||||||
padded = torch.cat([left_pad, qk_cur], dim=-1)
|
|
||||||
|
|
||||||
out = self._conv_qk_run(padded) # [1, C, S_cur]
|
|
||||||
qk_out[start:end] = out.squeeze(0).transpose(0, 1)
|
|
||||||
|
|
||||||
new_state = padded[..., -self.total_padding :]
|
|
||||||
conv_state[mamba_idx] = new_state.squeeze(0).to(conv_state.dtype)
|
|
||||||
|
|
||||||
hs_cur = hidden_states[start:end]
|
|
||||||
if has_prefix:
|
|
||||||
first = prev_hs_state[mamba_idx].squeeze(-1).to(dtype).unsqueeze(0)
|
|
||||||
else:
|
|
||||||
first = hidden_states.new_zeros((1, self.hidden_size))
|
|
||||||
shifted = torch.cat([first, hs_cur[:-1]], dim=0)
|
|
||||||
v2_input[start:end] = shifted
|
|
||||||
|
|
||||||
prev_hs_state[mamba_idx] = (
|
|
||||||
hs_cur[-1].unsqueeze(-1).to(prev_hs_state.dtype)
|
|
||||||
)
|
|
||||||
|
|
||||||
start = end
|
|
||||||
|
|
||||||
query_conv = qk_out[:, : self.latent_q_dim].view(
|
|
||||||
T, self.num_q_heads, self.head_dim
|
|
||||||
)
|
|
||||||
key_conv = qk_out[:, self.latent_q_dim :].view(
|
|
||||||
T, self.num_k_heads, self.head_dim
|
|
||||||
)
|
|
||||||
|
|
||||||
query, key = self._add_grouped_qk_means(
|
|
||||||
query_conv, key_conv, query_pre, key_base
|
|
||||||
)
|
|
||||||
query, key = self._normalize_qk(query, key)
|
|
||||||
|
|
||||||
v1, _ = self.val_proj1(hidden_states)
|
|
||||||
v2, _ = self.val_proj2(v2_input)
|
|
||||||
value_full = torch.cat([v1, v2], dim=-1).view(
|
|
||||||
T, self.num_k_heads_full, self.head_dim
|
|
||||||
)
|
|
||||||
value = self._slice_v_per_rank(value_full)
|
|
||||||
return query, key, value
|
|
||||||
|
|
||||||
def _forward_decode(
|
|
||||||
self,
|
|
||||||
hidden_states: torch.Tensor,
|
|
||||||
forward_batch: ForwardBatch,
|
|
||||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
||||||
"""Single-token decode path for a whole batch.
|
|
||||||
|
|
||||||
Reads each request's cached conv state and ``prev_hs`` from the
|
|
||||||
centralized MambaPool via ``index_select``, runs the conv on the
|
|
||||||
small ``[T, C, total_padding+1]`` window, and writes back via
|
|
||||||
``index_copy_``.
|
|
||||||
"""
|
|
||||||
T = hidden_states.shape[0]
|
|
||||||
dtype = hidden_states.dtype
|
|
||||||
|
|
||||||
conv_state, prev_hs_state, mamba_indices = self._get_pool_state(forward_batch)
|
|
||||||
|
|
||||||
q_raw, _ = self.linear_q(hidden_states)
|
|
||||||
k_raw, _ = self.linear_k(hidden_states)
|
|
||||||
qk = torch.cat([q_raw, k_raw], dim=-1) # [T, C]
|
|
||||||
|
|
||||||
query_pre = q_raw.view(T, self.num_q_heads, self.head_dim)
|
|
||||||
key_base = k_raw.view(T, self.num_k_heads, self.head_dim)
|
|
||||||
|
|
||||||
left_pad = conv_state.index_select(0, mamba_indices).to(dtype)
|
|
||||||
cur = qk.unsqueeze(-1) # [T, C, 1]
|
|
||||||
padded = torch.cat([left_pad, cur], dim=-1) # [T, C, total_padding+1]
|
|
||||||
out = self._conv_qk_run(padded) # [T, C, 1]
|
|
||||||
qk_out = out.squeeze(-1) # [T, C]
|
|
||||||
|
|
||||||
new_state = padded[..., -self.total_padding :]
|
|
||||||
conv_state.index_copy_(0, mamba_indices, new_state.to(conv_state.dtype))
|
|
||||||
|
|
||||||
query_conv = qk_out[:, : self.latent_q_dim].view(
|
|
||||||
T, self.num_q_heads, self.head_dim
|
|
||||||
)
|
|
||||||
key_conv = qk_out[:, self.latent_q_dim :].view(
|
|
||||||
T, self.num_k_heads, self.head_dim
|
|
||||||
)
|
|
||||||
|
|
||||||
query, key = self._add_grouped_qk_means(
|
|
||||||
query_conv, key_conv, query_pre, key_base
|
|
||||||
)
|
|
||||||
query, key = self._normalize_qk(query, key)
|
|
||||||
|
|
||||||
prev_hs = prev_hs_state.index_select(0, mamba_indices).squeeze(-1).to(dtype)
|
|
||||||
v1, _ = self.val_proj1(hidden_states)
|
|
||||||
v2, _ = self.val_proj2(prev_hs)
|
|
||||||
value_full = torch.cat([v1, v2], dim=-1).view(
|
|
||||||
T, self.num_k_heads_full, self.head_dim
|
|
||||||
)
|
|
||||||
value = self._slice_v_per_rank(value_full)
|
|
||||||
prev_hs_state.index_copy_(
|
|
||||||
0, mamba_indices, hidden_states.unsqueeze(-1).to(prev_hs_state.dtype)
|
|
||||||
)
|
|
||||||
return query, key, value
|
|
||||||
|
|
||||||
def forward(
|
def forward(
|
||||||
self,
|
self,
|
||||||
hidden_states: torch.Tensor,
|
hidden_states: torch.Tensor,
|
||||||
@@ -779,6 +715,16 @@ class CCA(nn.Module):
|
|||||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||||
"""Project ``hidden_states`` into ``(q, k, v)`` honoring per-request state.
|
"""Project ``hidden_states`` into ``(q, k, v)`` honoring per-request state.
|
||||||
|
|
||||||
|
The per-request conv-state plumbing (slot gather/scatter, prefix mask,
|
||||||
|
cuda-graph buffers) is owned by :class:`ShortConvAttnBackend
|
||||||
|
<sglang.srt.layers.attention.linear.short_conv_backend.ShortConvAttnBackend>`,
|
||||||
|
reached via ``get_attn_backend().conv_state_metadata``; CCA runs its own
|
||||||
|
two-stage grouped conv (:func:`cca_extend` / :func:`cca_decode`) against
|
||||||
|
that handle, so this module holds no pool access. Those functions return
|
||||||
|
the conv output ``qk_out`` and the ``val_proj2`` input ``v2_input`` (the
|
||||||
|
shifted / previous hidden state), updating the ``conv_state`` /
|
||||||
|
``prev_hs`` pool slots in place.
|
||||||
|
|
||||||
``q`` / ``k`` are returned in fp32 (the normalize step keeps fp32 for
|
``q`` / ``k`` are returned in fp32 (the normalize step keeps fp32 for
|
||||||
stability); ``v`` is returned in the input dtype since the caller
|
stability); ``v`` is returned in the input dtype since the caller
|
||||||
casts everything back to ``hidden_states.dtype`` before rotary +
|
casts everything back to ``hidden_states.dtype`` before rotary +
|
||||||
@@ -798,10 +744,63 @@ class CCA(nn.Module):
|
|||||||
zero.view(0, self.num_k_heads, self.head_dim),
|
zero.view(0, self.num_k_heads, self.head_dim),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
T = hidden_states.shape[0]
|
||||||
|
q_raw, _ = self.linear_q(hidden_states) # [T, latent_q]
|
||||||
|
k_raw, _ = self.linear_k(hidden_states)
|
||||||
|
qk = torch.cat([q_raw, k_raw], dim=-1) # [T, in_out_ch]
|
||||||
|
|
||||||
|
query_pre = q_raw.view(T, self.num_q_heads, self.head_dim)
|
||||||
|
key_base = k_raw.view(T, self.num_k_heads, self.head_dim)
|
||||||
|
|
||||||
|
# The backend hands out the per-request conv-state handle (slot indices,
|
||||||
|
# prefix mask, cuda-graph buffers); CCA runs its own two-stage grouped
|
||||||
|
# conv against it and gets back the conv output + val_proj2 input, with
|
||||||
|
# the conv_state / prev_hs pool slots updated in place.
|
||||||
|
meta = get_attn_backend().conv_state_metadata(self.layer_id, forward_batch)
|
||||||
|
conv_state = meta.layer_cache.conv[0]
|
||||||
|
prev_hs_state = meta.layer_cache.conv[1]
|
||||||
if forward_batch.forward_mode.is_decode_or_idle():
|
if forward_batch.forward_mode.is_decode_or_idle():
|
||||||
return self._forward_decode(hidden_states, forward_batch)
|
qk_out, v2_input = cca_decode(
|
||||||
# EXTEND / MIXED / DLLM_EXTEND all share the prefill loop.
|
qk,
|
||||||
return self._forward_extend(hidden_states, forward_batch)
|
hidden_states,
|
||||||
|
self.conv_qk,
|
||||||
|
conv_state,
|
||||||
|
prev_hs_state,
|
||||||
|
meta.cache_indices,
|
||||||
|
self.total_padding,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
qk_out, v2_input = cca_extend(
|
||||||
|
qk,
|
||||||
|
hidden_states,
|
||||||
|
self.conv_qk,
|
||||||
|
conv_state,
|
||||||
|
prev_hs_state,
|
||||||
|
meta.slot_ids_cpu,
|
||||||
|
meta.has_prefix_cpu,
|
||||||
|
forward_batch.extend_seq_lens_cpu,
|
||||||
|
self.total_padding,
|
||||||
|
)
|
||||||
|
|
||||||
|
query_conv = qk_out[:, : self.latent_q_dim].view(
|
||||||
|
T, self.num_q_heads, self.head_dim
|
||||||
|
)
|
||||||
|
key_conv = qk_out[:, self.latent_q_dim :].view(
|
||||||
|
T, self.num_k_heads, self.head_dim
|
||||||
|
)
|
||||||
|
|
||||||
|
query, key = self._add_grouped_qk_means(
|
||||||
|
query_conv, key_conv, query_pre, key_base
|
||||||
|
)
|
||||||
|
query, key = self._normalize_qk(query, key)
|
||||||
|
|
||||||
|
v1, _ = self.val_proj1(hidden_states)
|
||||||
|
v2, _ = self.val_proj2(v2_input)
|
||||||
|
value_full = torch.cat([v1, v2], dim=-1).view(
|
||||||
|
T, self.num_k_heads_full, self.head_dim
|
||||||
|
)
|
||||||
|
value = self._slice_v_per_rank(value_full)
|
||||||
|
return query, key, value
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -127,19 +127,77 @@ class _MockReqToTokenPool:
|
|||||||
return req_pool_indices.to(torch.int32)
|
return req_pool_indices.to(torch.int32)
|
||||||
|
|
||||||
|
|
||||||
|
class _MockShortConvBackend:
|
||||||
|
"""Stand-in for ``ShortConvHybridAttnBackend`` in the CPU unit tests.
|
||||||
|
|
||||||
|
The CCA module reaches the conv-state plumbing via
|
||||||
|
``get_attn_backend().conv_state_metadata(...)`` and runs its own conv
|
||||||
|
kernel. This mock exposes that accessor over a ``_MockReqToTokenPool``,
|
||||||
|
mirroring ``ShortConvAttnBackend``: the req -> slot mapping (and, for extend,
|
||||||
|
its host ``.tolist()`` mirror) is resolved once per step and shared across
|
||||||
|
all conv layers, while the decode path stays entirely on-device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, pool: "_MockReqToTokenPool"):
|
||||||
|
self.req_to_token_pool = pool
|
||||||
|
self.token_to_kv_pool = None
|
||||||
|
# Per-forward-step memoization keyed on the ForwardBatch identity,
|
||||||
|
# mirroring ShortConvAttnBackend.init_forward_metadata.
|
||||||
|
self._step_indices = {} # id(forward_batch) -> device index tensor
|
||||||
|
self._step_slot_ids = {} # id(forward_batch) -> host list (extend only)
|
||||||
|
|
||||||
|
def _resolve_indices(self, forward_batch):
|
||||||
|
key = id(forward_batch)
|
||||||
|
indices = self._step_indices.get(key)
|
||||||
|
if indices is None:
|
||||||
|
indices = self.req_to_token_pool.get_mamba_indices(
|
||||||
|
forward_batch.req_pool_indices
|
||||||
|
).to(torch.long)
|
||||||
|
self._step_indices[key] = indices
|
||||||
|
return indices
|
||||||
|
|
||||||
|
def _resolve_slot_ids(self, forward_batch, indices):
|
||||||
|
key = id(forward_batch)
|
||||||
|
slot_ids = self._step_slot_ids.get(key)
|
||||||
|
if slot_ids is None:
|
||||||
|
slot_ids = indices.tolist()
|
||||||
|
self._step_slot_ids[key] = slot_ids
|
||||||
|
return slot_ids
|
||||||
|
|
||||||
|
def conv_state_metadata(self, layer_id, forward_batch):
|
||||||
|
from sglang.srt.layers.attention.linear.short_conv_backend import (
|
||||||
|
ShortConvMetadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
layer_cache = self.req_to_token_pool.mamba2_layer_cache(layer_id)
|
||||||
|
indices = self._resolve_indices(forward_batch) # already int64
|
||||||
|
if forward_batch.forward_mode.is_decode_or_idle():
|
||||||
|
return ShortConvMetadata(layer_cache=layer_cache, cache_indices=indices)
|
||||||
|
|
||||||
|
slot_ids = self._resolve_slot_ids(forward_batch, indices)
|
||||||
|
has_prefix = [int(p) > 0 for p in forward_batch.extend_prefix_lens_cpu]
|
||||||
|
return ShortConvMetadata(
|
||||||
|
layer_cache=layer_cache,
|
||||||
|
cache_indices=indices,
|
||||||
|
slot_ids_cpu=slot_ids,
|
||||||
|
has_prefix_cpu=has_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _mock_pool_context(pool: _MockReqToTokenPool):
|
def _mock_pool_context(pool: _MockReqToTokenPool):
|
||||||
"""Install a mock ``ForwardContext`` whose ``req_to_token_pool`` is ``pool``."""
|
"""Install a mock ``ForwardContext`` whose ``attn_backend`` exposes both
|
||||||
|
``req_to_token_pool`` and ``conv_state_metadata`` over ``pool``."""
|
||||||
from sglang.srt.model_executor.forward_context import (
|
from sglang.srt.model_executor.forward_context import (
|
||||||
ForwardContext,
|
ForwardContext,
|
||||||
set_forward_context,
|
set_forward_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
backend = SimpleNamespace(req_to_token_pool=pool, token_to_kv_pool=None)
|
backend = _MockShortConvBackend(pool)
|
||||||
ctx = ForwardContext(attn_backend=backend)
|
ctx = ForwardContext(attn_backend=backend)
|
||||||
prev = set_forward_context(ctx)
|
prev = set_forward_context(ctx)
|
||||||
try:
|
try:
|
||||||
yield pool
|
yield backend
|
||||||
finally:
|
finally:
|
||||||
set_forward_context(prev)
|
set_forward_context(prev)
|
||||||
|
|
||||||
@@ -457,17 +515,17 @@ class TestZayaCCA(CustomTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
pool = _CountingPool(pool_size=8, cca_config=config)
|
pool = _CountingPool(pool_size=8, cca_config=config)
|
||||||
with _mock_pool_context(pool):
|
with _mock_pool_context(pool) as backend:
|
||||||
fb = _fresh_fb()
|
fb = _fresh_fb()
|
||||||
cca0.forward(hs, fb)
|
cca0.forward(hs, fb)
|
||||||
cca2.forward(hs, fb)
|
cca2.forward(hs, fb)
|
||||||
|
|
||||||
# Two CCA layers, one forward step -> one shared lookup, both the
|
# Two CCA layers, one forward step -> one shared lookup, both the
|
||||||
# device tensor and its host mirror memoized on the ForwardBatch.
|
# device tensor and its host mirror memoized once per step on the
|
||||||
|
# backend (ShortConvAttnBackend does this in init_forward_metadata).
|
||||||
self.assertEqual(pool.get_mamba_indices_calls, 1)
|
self.assertEqual(pool.get_mamba_indices_calls, 1)
|
||||||
self.assertTrue(hasattr(fb, "_zaya_mamba_indices"))
|
self.assertIn(id(fb), backend._step_indices)
|
||||||
self.assertTrue(hasattr(fb, "_zaya_mamba_indices_cpu"))
|
self.assertEqual(backend._step_slot_ids[id(fb)], [0])
|
||||||
self.assertEqual(fb._zaya_mamba_indices_cpu, [0])
|
|
||||||
|
|
||||||
# A new forward step (fresh ForwardBatch) resolves the mapping again.
|
# A new forward step (fresh ForwardBatch) resolves the mapping again.
|
||||||
cca0.forward(hs, _fresh_fb())
|
cca0.forward(hs, _fresh_fb())
|
||||||
@@ -479,16 +537,20 @@ class TestZayaCCA(CustomTestCase):
|
|||||||
cca, config = _make_tiny_cca(seed=7)
|
cca, config = _make_tiny_cca(seed=7)
|
||||||
|
|
||||||
pool = _MockReqToTokenPool(pool_size=8, cca_config=config)
|
pool = _MockReqToTokenPool(pool_size=8, cca_config=config)
|
||||||
with _mock_pool_context(pool):
|
with _mock_pool_context(pool) as backend:
|
||||||
|
# Keep a reference to the extend batch so its id() cannot be recycled
|
||||||
|
# by the later decode batch (the mock keys its per-step memo on
|
||||||
|
# id(forward_batch); a GC'd-then-reused address would false-collide).
|
||||||
|
fb_extend = _make_forward_batch(
|
||||||
|
is_decode=False,
|
||||||
|
extend_seq_lens_cpu=[3],
|
||||||
|
extend_prefix_lens_cpu=[0],
|
||||||
|
req_pool_indices=[0],
|
||||||
|
input_ids=torch.arange(3, dtype=torch.int64),
|
||||||
|
)
|
||||||
cca.forward(
|
cca.forward(
|
||||||
torch.randn(3, config.hidden_size, dtype=torch.float32) * 0.1,
|
torch.randn(3, config.hidden_size, dtype=torch.float32) * 0.1,
|
||||||
_make_forward_batch(
|
fb_extend,
|
||||||
is_decode=False,
|
|
||||||
extend_seq_lens_cpu=[3],
|
|
||||||
extend_prefix_lens_cpu=[0],
|
|
||||||
req_pool_indices=[0],
|
|
||||||
input_ids=torch.arange(3, dtype=torch.int64),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
fb_decode = _make_forward_batch(
|
fb_decode = _make_forward_batch(
|
||||||
is_decode=True,
|
is_decode=True,
|
||||||
@@ -502,10 +564,11 @@ class TestZayaCCA(CustomTestCase):
|
|||||||
fb_decode,
|
fb_decode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Device indices are memoized, but the host ``.tolist()`` mirror is only
|
# Decode resolves device indices, but the host ``.tolist()`` mirror
|
||||||
# built by the extend path.
|
# is only built by the extend path -- so the decode step stays
|
||||||
self.assertTrue(hasattr(fb_decode, "_zaya_mamba_indices"))
|
# entirely on-device (CUDA-graph friendly).
|
||||||
self.assertFalse(hasattr(fb_decode, "_zaya_mamba_indices_cpu"))
|
self.assertIn(id(fb_decode), backend._step_indices)
|
||||||
|
self.assertNotIn(id(fb_decode), backend._step_slot_ids)
|
||||||
|
|
||||||
|
|
||||||
class TestZayaCCATensorParallel(CustomTestCase):
|
class TestZayaCCATensorParallel(CustomTestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user