[Disagg][Qwen3.5] Fix heterogeneous attn-TP scatter transfer: GDN conv sub-block slice + GQA replicated-KV head map (#30997)

Co-authored-by: Xuwei Li <lixuwei.xy@gmail.com>
This commit is contained in:
YAMY
2026-07-16 02:31:37 +08:00
committed by GitHub
co-authored by Xuwei Li
parent dd2e4cdc99
commit 2d00e20a52
9 changed files with 367 additions and 61 deletions
@@ -677,6 +677,9 @@ class MambaPool:
)
self.mem_usage = mem_usage_bytes / GB
self.num_mamba_layers = num_mamba_layers
# Full (unsharded) conv sub-block dims for PD transfer across different
# attn_tp_size (GDN: [key_dim, key_dim, value_dim]); None otherwise.
self.conv_shard_groups = getattr(cache_params.shape, "conv_shard_groups", None)
def get_speculative_mamba2_params_all_layers(self) -> SpeculativeState:
assert isinstance(self.mamba_cache, self.SpeculativeState)
@@ -832,6 +835,43 @@ class MambaPool:
dim_per_tensor += [sliceable_dim] * self.num_mamba_layers
return dim_per_tensor
def get_state_conv_shard_groups(self):
"""Per-tensor conv sub-block dims, aligned element-wise with
get_state_dim_per_tensor().
For GDN, conv_state's sliceable axis is cat([query, key, value]) with
each sub-block head-sharded independently across attn-TP; the full
(unsharded) sub-block dims are returned so PD transfer across different
attn_tp_size can slice each sub-block. Returns None for temporal_state
(single head-sharded axis) and whenever no descriptor is available, so
those tensors keep the single contiguous slice.
"""
subdims_per_tensor = []
for field in vars(self.mamba_cache):
# Mirror the exclusions in get_state_dim_per_tensor so the returned
# sub-dims line up element-wise with the RDMA buffer list.
if field in (
"intermediate_ssm",
"intermediate_conv_window",
"replayssm_d",
"replayssm_k",
"replayssm_g",
):
continue
value = getattr(self.mamba_cache, field)
if value is None:
continue
tensors = value if isinstance(value, list) else [value]
for _ in tensors:
# Only conv_state carries a q/k/v decomposition.
subdims = (
list(self.conv_shard_groups)
if field == "conv" and self.conv_shard_groups is not None
else None
)
subdims_per_tensor += [subdims] * self.num_mamba_layers
return subdims_per_tensor
class HybridReqToTokenPool(ReqToTokenPool):
"""A memory pool that maps a request to its token locations."""
@@ -1022,6 +1062,9 @@ class HybridReqToTokenPool(ReqToTokenPool):
def get_state_dim_per_tensor(self):
return self.mamba_pool.get_state_dim_per_tensor()
def get_state_conv_shard_groups(self):
return self.mamba_pool.get_state_conv_shard_groups()
def get_mamba_ping_pong_other_idx(self, mamba_next_track_idx: int) -> int:
if self.mamba_ping_pong_track_buffer_size == 2:
return 1 - mamba_next_track_idx
@@ -2598,6 +2641,10 @@ class HybridLinearKVPool(KVCache):
"""Get the sliceable dimension size for each mamba state tensor."""
return self.mamba_pool.get_state_dim_per_tensor()
def get_state_conv_shard_groups(self):
"""Per-tensor conv sub-block dims (GDN) aligned with the state list."""
return self.mamba_pool.get_state_conv_shard_groups()
def maybe_get_custom_mem_pool(self):
return self.full_kv_pool.maybe_get_custom_mem_pool()