Files
vLLM/vllm/v1/worker/gpu/model_states/recoverssm.py
T
5567cc1b25 [Mamba] RecoverSSM align: keep a boundary-ending state in its block
A speculative window whose accepted tokens end exactly on a recurrent block
boundary selected the following block-table column for the committed state
(commit plan) and recorded it as the running column (postprocess). That
column can still be unallocated, so the next step reads an uninitialized
page. Select the block that holds the last committed token, matching the
(n - 1) // block convention used elsewhere in align mode. Applies to
Kimi-K3 KDA and to the Qwen GDN/PLE RecoverSSM paths that share the plan.

Fix and test from local-inference-lab/vllm a02f585d (Martin Vit, root cause
reported by astra); the GDN/PLE align tests now expect the same convention.

Signed-off-by: Jürgen Schmied <juergenschmied70@gmail.com>
Co-authored-by: Martin Vit <martin@voipmonitor.org>
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-27 11:12:40 +02:00

104 lines
3.4 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from typing import Any
import torch
from vllm.triton_utils import tl, triton
from vllm.v1.attention.backends.recoverssm_metadata import RecoverSSMMetadata
from vllm.v1.worker.utils import AttentionGroup
class RecoverSSMState:
"""Coordinates RecoverSSM metadata between attention and postprocessing."""
def __init__(self) -> None:
self._step: tuple[RecoverSSMMetadata, ...] | None = None
def record_step(
self,
attn_metadata: dict[str, Any],
attn_groups: list[list[AttentionGroup]],
*,
for_capture: bool,
) -> None:
if for_capture:
self._step = None
return
step: list[RecoverSSMMetadata] = []
for group_list in attn_groups:
for group in group_list:
metadata = attn_metadata[group.layer_names[0]]
if isinstance(metadata, RecoverSSMMetadata):
step.append(metadata)
self._step = tuple(step)
def commit_step(
self,
num_sampled: torch.Tensor | int,
idx_mapping: torch.Tensor,
*,
state_indices: torch.Tensor | None,
num_accepted_tokens: torch.Tensor,
) -> None:
step = self._step
self._step = None
if isinstance(num_sampled, int) or step is None:
return
for metadata in step:
postprocess_meta = metadata.commit_recoverssm_state(num_sampled)
if postprocess_meta is None:
continue
assert state_indices is not None
# RecoverSSM already restored the accepted state. Update its running
# column and reset the next-step copy bias to the neutral value.
_postprocess_recoverssm_align_kernel[(postprocess_meta.num_spec_decodes,)](
idx_mapping,
num_sampled,
postprocess_meta.request_indices,
postprocess_meta.num_computed_tokens,
state_indices,
num_accepted_tokens,
MAMBA_BLOCK_SIZE=postprocess_meta.block_size,
BLOCK_TABLE_WIDTH=postprocess_meta.block_table.shape[1],
)
@triton.heuristics(
{"HAS_REQUEST_INDICES": lambda args: args["request_indices_ptr"] is not None}
)
@triton.jit
def _postprocess_recoverssm_align_kernel(
idx_mapping_ptr,
num_sampled_ptr,
request_indices_ptr,
num_computed_ptr,
state_idx_ptr,
num_accepted_ptr,
HAS_REQUEST_INDICES: tl.constexpr,
MAMBA_BLOCK_SIZE: tl.constexpr,
BLOCK_TABLE_WIDTH: tl.constexpr,
):
spec_idx = tl.program_id(0)
batch_idx = spec_idx
if HAS_REQUEST_INDICES:
batch_idx = tl.load(request_indices_ptr + spec_idx)
req_state_idx = tl.load(idx_mapping_ptr + batch_idx)
if req_state_idx < 0:
return
num_sampled = tl.load(num_sampled_ptr + batch_idx)
num_computed = tl.load(num_computed_ptr + batch_idx)
# Match the commit plan: the running state lives in the block holding the
# last computed token, so the next step migrates it across a boundary.
tl.store(
state_idx_ptr + req_state_idx,
tl.minimum(
tl.maximum(num_computed + num_sampled - 1, 0) // MAMBA_BLOCK_SIZE,
BLOCK_TABLE_WIDTH - 1,
),
)
tl.store(num_accepted_ptr + req_state_idx, 1)