feat(short-conv): shared ShortConvAttnBackend for ZAYA1 CCA + LFM2 short conv (#29867)

This commit is contained in:
Cheng Wan
2026-07-01 20:08:00 -07:00
committed by GitHub
parent 70df09b833
commit b558cc1abb
8 changed files with 652 additions and 338 deletions
+83 -20
View File
@@ -127,19 +127,77 @@ class _MockReqToTokenPool:
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
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 (
ForwardContext,
set_forward_context,
)
backend = SimpleNamespace(req_to_token_pool=pool, token_to_kv_pool=None)
backend = _MockShortConvBackend(pool)
ctx = ForwardContext(attn_backend=backend)
prev = set_forward_context(ctx)
try:
yield pool
yield backend
finally:
set_forward_context(prev)
@@ -457,17 +515,17 @@ class TestZayaCCA(CustomTestCase):
)
pool = _CountingPool(pool_size=8, cca_config=config)
with _mock_pool_context(pool):
with _mock_pool_context(pool) as backend:
fb = _fresh_fb()
cca0.forward(hs, fb)
cca2.forward(hs, fb)
# 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.assertTrue(hasattr(fb, "_zaya_mamba_indices"))
self.assertTrue(hasattr(fb, "_zaya_mamba_indices_cpu"))
self.assertEqual(fb._zaya_mamba_indices_cpu, [0])
self.assertIn(id(fb), backend._step_indices)
self.assertEqual(backend._step_slot_ids[id(fb)], [0])
# A new forward step (fresh ForwardBatch) resolves the mapping again.
cca0.forward(hs, _fresh_fb())
@@ -479,16 +537,20 @@ class TestZayaCCA(CustomTestCase):
cca, config = _make_tiny_cca(seed=7)
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(
torch.randn(3, config.hidden_size, dtype=torch.float32) * 0.1,
_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),
),
fb_extend,
)
fb_decode = _make_forward_batch(
is_decode=True,
@@ -502,10 +564,11 @@ class TestZayaCCA(CustomTestCase):
fb_decode,
)
# Device indices are memoized, but the host ``.tolist()`` mirror is only
# built by the extend path.
self.assertTrue(hasattr(fb_decode, "_zaya_mamba_indices"))
self.assertFalse(hasattr(fb_decode, "_zaya_mamba_indices_cpu"))
# Decode resolves device indices, but the host ``.tolist()`` mirror
# is only built by the extend path -- so the decode step stays
# entirely on-device (CUDA-graph friendly).
self.assertIn(id(fb_decode), backend._step_indices)
self.assertNotIn(id(fb_decode), backend._step_slot_ids)
class TestZayaCCATensorParallel(CustomTestCase):