Skip to content

vllm.models.qwen4_exp.common.qsa_cache

Paged side-cache ownership and metadata for Qwen4Exp QSA.

Each QSA layer keeps a fixed circular buffer of raw index keys (the compressor state) and one compressed key. MRoPE models pack exact three-axis positions beside the raw keys; text models derive group positions from logical positions. The compressor state uses one block per request, while the compressed owner uses MLAAttentionSpec.tokens_per_state so its block table follows the main KV-cache lifecycle. Their physical tensor storage is shared by the generic cache-layout planner.

Classes:

Functions:

QSACompressedKeyCache

Bases: _QSAStateCache

Normed, group-first-RoPE key at one row per complete group.

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
class QSACompressedKeyCache(_QSAStateCache):
    """Normed, group-first-RoPE key at one row per complete group."""

    def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
        del vllm_config
        return MLAAttentionSpec(
            block_size=self.cache_config.block_size,
            num_kv_heads=1,
            head_size=self.head_size,
            dtype=self.dtype,
            tokens_per_state=self.compress_ratio,
        )

QSAForwardMetadata dataclass

Bases: AttentionMetadata

Common per-forward metadata for one QSA side cache.

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
@dataclass
class QSAForwardMetadata(AttentionMetadata):
    """Common per-forward metadata for one QSA side cache."""

    block_table: torch.Tensor
    slot_mapping: torch.Tensor
    seq_lens: torch.Tensor
    query_start_loc: torch.Tensor
    token_to_req: torch.Tensor
    logical_positions: torch.Tensor
    visible_blocks: torch.Tensor
    k_work_metadata: torch.Tensor
    num_actual_tokens: int
    num_decodes: int
    num_decode_tokens: int
    num_prefills: int
    num_prefill_tokens: int
    max_query_len: int
    decode_query_len: int
    max_seq_len: int
    storage_block_size: int
    compress_ratio: int

QSAKeyStateCache

Bases: _QSAStateCache

Raw BF16 key, optionally followed by exact int64 MRoPE positions.

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
class QSAKeyStateCache(_QSAStateCache):
    """Raw BF16 key, optionally followed by exact int64 MRoPE positions."""

    _BF16_PER_INT64 = 4
    _NUM_ROPE_AXES = 3

    def __init__(self, *, cache_rope_positions: bool = False, **kwargs) -> None:
        key_head_size = int(kwargs.pop("head_size"))
        self.key_head_size = key_head_size
        self.cache_rope_positions = bool(cache_rope_positions)
        self.rope_position_offset = (
            (key_head_size + self._BF16_PER_INT64 - 1) // self._BF16_PER_INT64
        ) * self._BF16_PER_INT64
        storage_head_size = key_head_size
        if self.cache_rope_positions:
            storage_head_size = self.rope_position_offset + (
                self._NUM_ROPE_AXES * self._BF16_PER_INT64
            )
        super().__init__(head_size=storage_head_size, **kwargs)

    def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
        super().bind_kv_cache(kv_cache)
        qsa_cache = self.kv_cache
        self.key_cache = qsa_cache[..., : self.key_head_size]
        if self.cache_rope_positions:
            position_tail = qsa_cache[..., self.rope_position_offset :]
            self.rope_position_cache = position_tail.view(torch.int64)
        else:
            self.rope_position_cache = None

    def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
        # Hold the open group's committed keys plus every row a speculative
        # step stores before acceptance is known, rounded up to whole groups so
        # the ring divides the attention block size (it joins the LCM that sets
        # the scheduler block size). Anything narrower lets a rejected draft row
        # overwrite a committed key the next step needs to close the group.
        span = self.compress_ratio + vllm_config.num_speculative_tokens
        capacity = self.compress_ratio * cdiv(span, self.compress_ratio)
        assert self.cache_config.block_size % capacity == 0, (
            f"QSA ring capacity {capacity} must divide the attention block "
            f"size {self.cache_config.block_size}"
        )
        return CircularBufferSpec(
            block_size=capacity,
            num_kv_heads=1,
            head_size=self.head_size,
            head_size_v=0,
            dtype=self.dtype,
        )

QSAMetadataBuilder

Bases: AttentionMetadataBuilder[QSAForwardMetadata]

Build QSA metadata from vLLM's cache-group-specific common metadata.

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
class QSAMetadataBuilder(AttentionMetadataBuilder[QSAForwardMetadata]):
    """Build QSA metadata from vLLM's cache-group-specific common metadata."""

    _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH

    def __init__(
        self,
        kv_cache_spec: AttentionSpec,
        layer_names: list[str],
        vllm_config: VllmConfig,
        device: torch.device,
    ) -> None:
        super().__init__(kv_cache_spec, layer_names, vllm_config, device)
        self._init_reorder_batch_threshold(1, supports_spec_as_decode=True)
        assert self.reorder_batch_threshold is not None
        self.is_circular_buffer = isinstance(kv_cache_spec, CircularBufferSpec)
        if isinstance(kv_cache_spec, MLAAttentionSpec):
            compress_ratio = kv_cache_spec.tokens_per_state
            assert isinstance(compress_ratio, int), (
                "QSA compression requires an integer tokens_per_state"
            )
            self.compress_ratio = compress_ratio
        else:
            self.compress_ratio = 1
        self.storage_block_size = kv_cache_spec.num_states
        max_tokens = vllm_config.scheduler_config.max_num_batched_tokens
        self.token_to_req_buffer = torch.empty(
            max_tokens, dtype=torch.int32, device=device
        )
        self.slot_mapping_buffer = torch.empty(
            max_tokens, dtype=torch.int64, device=device
        )
        self.logical_positions_buffer = torch.empty(
            max_tokens, dtype=torch.int64, device=device
        )
        self.visible_blocks_buffer = torch.empty(
            max_tokens, dtype=torch.int32, device=device
        )
        max_requests = vllm_config.scheduler_config.max_num_seqs
        self.request_capacity = max_requests
        if not self.is_circular_buffer and self.compress_ratio != 1:
            max_k_work = (
                max_tokens + (self.compress_ratio - 1) * max_requests
            ) // self.compress_ratio
            self.k_work_metadata_buffer = torch.empty(
                max_k_work, 2, dtype=torch.int32, device=device
            )
        else:
            self.k_work_metadata_buffer = torch.empty(
                0, 2, dtype=torch.int32, device=device
            )

    def build(
        self,
        common_prefix_len: int,
        common_attn_metadata: CommonAttentionMetadata,
        fast_build: bool = False,
    ) -> QSAForwardMetadata:
        del common_prefix_len, fast_build
        num_tokens = common_attn_metadata.num_actual_tokens
        decode_threshold = self.reorder_batch_threshold
        assert decode_threshold is not None
        num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
            split_decodes_and_prefills(
                common_attn_metadata,
                decode_threshold=decode_threshold,
                require_uniform=True,
            )
        )
        assert num_decodes + num_prefills == common_attn_metadata.num_reqs
        assert num_decode_tokens + num_prefill_tokens == num_tokens
        decode_query_len = 0
        if num_decodes > 0:
            query_lens_cpu = torch.diff(
                common_attn_metadata.query_start_loc_cpu[: num_decodes + 1]
            )
            nonzero_query_lens = query_lens_cpu[query_lens_cpu > 0]
            if nonzero_query_lens.numel() > 0:
                decode_query_len = int(nonzero_query_lens[0].item())
                assert torch.all(nonzero_query_lens == decode_query_len)
        build_k_work = not self.is_circular_buffer and self.compress_ratio != 1
        k_work_metadata = self.k_work_metadata_buffer
        request_capacity = None
        if build_k_work:
            num_requests = common_attn_metadata.query_start_loc.shape[0] - 1
            request_capacity = self.request_capacity
            max_num_work = (
                num_tokens + (self.compress_ratio - 1) * num_requests
            ) // self.compress_ratio
            k_work_metadata = self.k_work_metadata_buffer[:max_num_work]
        token_to_req, logical_positions, visible_blocks, slot_mapping = (
            build_qsa_metadata(
                common_attn_metadata,
                self.token_to_req_buffer,
                self.logical_positions_buffer,
                self.visible_blocks_buffer,
                self.slot_mapping_buffer,
                storage_block_size=self.storage_block_size,
                compress_ratio=self.compress_ratio,
                circular_buffer_size=(
                    self.kv_cache_spec.block_size if self.is_circular_buffer else 0
                ),
                k_work_metadata_buffer=k_work_metadata if build_k_work else None,
                request_capacity=request_capacity,
            )
        )
        return QSAForwardMetadata(
            block_table=common_attn_metadata.block_table_tensor,
            slot_mapping=slot_mapping,
            seq_lens=common_attn_metadata.seq_lens,
            query_start_loc=common_attn_metadata.query_start_loc,
            token_to_req=token_to_req,
            logical_positions=logical_positions,
            visible_blocks=visible_blocks,
            k_work_metadata=k_work_metadata,
            num_actual_tokens=num_tokens,
            num_decodes=num_decodes,
            num_decode_tokens=num_decode_tokens,
            num_prefills=num_prefills,
            num_prefill_tokens=num_prefill_tokens,
            max_query_len=common_attn_metadata.max_query_len,
            decode_query_len=decode_query_len,
            max_seq_len=common_attn_metadata.max_seq_len,
            storage_block_size=self.storage_block_size,
            compress_ratio=self.compress_ratio,
        )

QSAStateBackend

Bases: AttentionBackend

Key-only dummy backend for out-of-band QSA side-cache operations.

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
class QSAStateBackend(AttentionBackend):
    """Key-only dummy backend for out-of-band QSA side-cache operations."""

    supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
    # fp8 entries allow the optional e4m3 compressed indexer cache.
    supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
        "auto",
        "bfloat16",
        "fp8",
        "fp8_e4m3",
    ]

    @staticmethod
    def get_name() -> str:
        return "QWEN4_EXP_EXP_QSA_STATE"

    @staticmethod
    def get_impl_cls():
        raise NotImplementedError(
            "QSA state caches run out-of-band and have no attention impl"
        )

    @staticmethod
    def get_builder_cls() -> type[QSAMetadataBuilder]:
        return QSAMetadataBuilder

    @classmethod
    def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
        # QSA pages are packed beside the main KV pages within each block.
        return (KVCacheLayout.BLNHC, KVCacheLayout.BLHNC)

_QSAStateCache

Bases: Module, AttentionLayerBase

Methods:

  • bind_kv_cache

    Adapt the unified [B, H, N, C] view to QSA's [B, N, H, C].

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
class _QSAStateCache(nn.Module, AttentionLayerBase):
    supports_dcp = False

    def __init__(
        self,
        *,
        head_size: int,
        dtype: torch.dtype,
        cache_config: CacheConfig,
        prefix: str,
        vllm_config: VllmConfig,
        compress_ratio: int = 1,
    ) -> None:
        super().__init__()
        if head_size <= 0:
            raise ValueError("QSA cache head size must be positive")
        if compress_ratio <= 0:
            raise ValueError("QSA compression ratio must be positive")
        if cache_config.block_size % compress_ratio:
            raise ValueError(
                "QSA cache block size must be divisible by the compression ratio"
            )
        self.head_size = head_size
        self.dtype = dtype
        self.cache_config = cache_config
        self.prefix = prefix
        self.compress_ratio = compress_ratio
        self.kv_cache = torch.tensor([])

        static_context = vllm_config.compilation_config.static_forward_context
        if prefix in static_context:
            raise ValueError(f"Duplicate layer name: {prefix}")
        static_context[prefix] = self

    def forward(self) -> None: ...

    def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
        """Adapt the unified [B, H, N, C] view to QSA's [B, N, H, C]."""
        if kv_cache.ndim != 4 or kv_cache.shape[1] != 1:
            raise ValueError("QSA state cache must be [blocks, 1, states, width]")
        if kv_cache.dtype != self.dtype or kv_cache.shape[3] != self.head_size:
            raise ValueError(
                f"QSA state cache does not match its spec "
                f"(dtype {kv_cache.dtype} != {self.dtype} or width "
                f"{kv_cache.shape[3]} != {self.head_size})"
            )
        super().bind_kv_cache(kv_cache.transpose(1, 2))

    def get_attn_backend(self) -> type[AttentionBackend]:
        return QSAStateBackend

bind_kv_cache(kv_cache)

Adapt the unified [B, H, N, C] view to QSA's [B, N, H, C].

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
    """Adapt the unified [B, H, N, C] view to QSA's [B, N, H, C]."""
    if kv_cache.ndim != 4 or kv_cache.shape[1] != 1:
        raise ValueError("QSA state cache must be [blocks, 1, states, width]")
    if kv_cache.dtype != self.dtype or kv_cache.shape[3] != self.head_size:
        raise ValueError(
            f"QSA state cache does not match its spec "
            f"(dtype {kv_cache.dtype} != {self.dtype} or width "
            f"{kv_cache.shape[3]} != {self.head_size})"
        )
    super().bind_kv_cache(kv_cache.transpose(1, 2))

build_qsa_metadata_triton(common_attn_metadata, token_to_req_buffer, logical_positions_buffer, visible_blocks_buffer, slot_mapping_buffer, *, storage_block_size, compress_ratio, circular_buffer_size=0, k_work_metadata_buffer=None, request_capacity=None)

Build QSA side-cache and optional pre-indexer work metadata.

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
def build_qsa_metadata_triton(
    common_attn_metadata: CommonAttentionMetadata,
    token_to_req_buffer: torch.Tensor,
    logical_positions_buffer: torch.Tensor,
    visible_blocks_buffer: torch.Tensor,
    slot_mapping_buffer: torch.Tensor,
    *,
    storage_block_size: int,
    compress_ratio: int,
    circular_buffer_size: int = 0,
    k_work_metadata_buffer: torch.Tensor | None = None,
    request_capacity: int | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
    """Build QSA side-cache and optional pre-indexer work metadata."""
    num_tokens = common_attn_metadata.num_actual_tokens
    num_mapped_tokens = int(common_attn_metadata.query_start_loc_cpu[-1])
    token_to_req = token_to_req_buffer[:num_tokens]
    logical_positions = logical_positions_buffer[:num_tokens]
    visible_blocks = visible_blocks_buffer[:num_tokens]
    slot_mapping = slot_mapping_buffer[:num_tokens]
    num_reqs = common_attn_metadata.query_start_loc.shape[0] - 1
    assert num_reqs > 0

    if k_work_metadata_buffer is not None:
        if request_capacity is None:
            request_capacity = num_reqs
        assert request_capacity >= num_reqs
        # Pad for tl.arange while keeping the scan width stable across live batches.
        request_scan_size = 1 << int(math.ceil(math.log2(request_capacity)))
        max_num_work = k_work_metadata_buffer.shape[0]
    else:
        request_scan_size = 1
        max_num_work = 0

    if num_tokens == 0 and k_work_metadata_buffer is None:
        return token_to_req, logical_positions, visible_blocks, slot_mapping

    block_table = common_attn_metadata.block_table_tensor
    num_search_steps = int(math.ceil(math.log2(num_reqs)))
    work_search_steps = int(math.ceil(math.log2(num_reqs)))
    # The same grid covers token tiles and, for the compressed cache, work tiles.
    num_token_blocks = cdiv(num_tokens, 128)
    num_work_blocks = (
        cdiv(max_num_work, 256) if k_work_metadata_buffer is not None else 0
    )
    _build_qsa_metadata_kernel[(max(num_token_blocks, num_work_blocks, 1),)](
        common_attn_metadata.query_start_loc,
        common_attn_metadata.seq_lens,
        common_attn_metadata.slot_mapping,
        block_table,
        token_to_req,
        logical_positions,
        visible_blocks,
        slot_mapping,
        k_work_metadata_buffer,
        block_table.stride(0),
        block_table.stride(1),
        num_reqs,
        num_mapped_tokens,
        num_tokens,
        max_num_work,
        num_search_steps,
        work_search_steps,
        storage_block_size,
        compress_ratio,
        circular_buffer_size,
        block_table.shape[1],
        launch_pdl=_metadata_launch_pdl(),
        TOKEN_BLOCK_SIZE=128,
        REQUEST_SCAN_SIZE=request_scan_size,
        WORK_BLOCK_SIZE=256,
        num_warps=4,
    )
    if circular_buffer_size == 0 and compress_ratio == 1:
        slot_mapping = common_attn_metadata.slot_mapping[:num_tokens]
    return token_to_req, logical_positions, visible_blocks, slot_mapping

canonical_qsa_rope_positions(positions)

Return exact per-token positions as [tokens, 1, 3] int64 rows.

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
def canonical_qsa_rope_positions(positions: torch.Tensor) -> torch.Tensor:
    """Return exact per-token positions as ``[tokens, 1, 3]`` int64 rows."""

    if positions.ndim == 1:
        positions = positions.unsqueeze(0).expand(3, -1)
    elif positions.ndim != 2 or positions.shape[0] not in (1, 3):
        raise ValueError("QSA RoPE positions must be [tokens] or [1|3, tokens]")
    if positions.shape[0] == 1:
        positions = positions.expand(3, -1)
    return positions.transpose(0, 1).unsqueeze(1).to(torch.int64)

circular_qsa_slot_mapping(block_table, token_to_req, logical_positions, compressor_state_size, query_start_loc=None, out=None)

Map each request to its fixed physical block as a circular token ring.

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
def circular_qsa_slot_mapping(
    block_table: torch.Tensor,
    token_to_req: torch.Tensor,
    logical_positions: torch.Tensor,
    compressor_state_size: int,
    query_start_loc: torch.Tensor | None = None,
    out: torch.Tensor | None = None,
) -> torch.Tensor:
    """Map each request to its fixed physical block as a circular token ring."""

    if compressor_state_size <= 0:
        raise ValueError("QSA circular buffer size must be positive")
    if block_table.ndim != 2:
        raise ValueError("QSA block table must be two-dimensional")

    requests = token_to_req.to(device=block_table.device, dtype=torch.long)
    positions = logical_positions.to(device=block_table.device, dtype=torch.long)
    if not all(block_table.shape):
        slots = torch.full_like(positions, PAD_SLOT_ID)
    else:
        valid = (requests >= 0) & (requests < block_table.shape[0]) & (positions >= 0)
        safe_requests = requests.clamp(0, block_table.shape[0] - 1)
        physical_blocks = block_table[safe_requests, 0].long()
        valid &= physical_blocks >= 0
        slots = physical_blocks * compressor_state_size + positions.remainder(
            compressor_state_size
        )
        slots = torch.where(valid, slots, PAD_SLOT_ID)

    if query_start_loc is not None:
        if query_start_loc.ndim != 1 or query_start_loc.shape[0] < 2:
            raise ValueError("QSA query starts must contain a terminal offset")
        query_start_loc = query_start_loc.to(block_table.device)
        num_requests = query_start_loc.shape[0] - 1
        safe_requests = requests.clamp(0, num_requests - 1)
        request_ends = query_start_loc.index_select(0, safe_requests + 1)
        rows = torch.arange(slots.numel(), device=slots.device)
        keep = (
            (requests >= 0)
            & (requests < num_requests)
            & (rows + compressor_state_size >= request_ends)
        )
        slots = torch.where(keep, slots, PAD_SLOT_ID)

    slots = slots.to(torch.int64)
    if out is not None:
        out.fill_(PAD_SLOT_ID)
        out[: slots.numel()].copy_(slots)
        return out[: slots.numel()]
    return slots

compressed_qsa_slot_mapping(block_table, token_to_req, logical_positions, storage_block_size, compress_ratio, out=None)

Build boundary-only slots for an MLAAttentionSpec QSA cache.

Source code in vllm/models/qwen4_exp/common/qsa_cache.py
def compressed_qsa_slot_mapping(
    block_table: torch.Tensor,
    token_to_req: torch.Tensor,
    logical_positions: torch.Tensor,
    storage_block_size: int,
    compress_ratio: int,
    out: torch.Tensor | None = None,
) -> torch.Tensor:
    """Build boundary-only slots for an ``MLAAttentionSpec`` QSA cache."""

    if storage_block_size <= 0 or compress_ratio <= 0:
        raise ValueError("QSA block size and compression ratio must be positive")
    compressed_positions = torch.div(
        logical_positions.clamp_min(0), compress_ratio, rounding_mode="floor"
    )
    slots = _logical_to_physical_qsa_slots(
        block_table,
        token_to_req,
        compressed_positions,
        storage_block_size,
    )
    valid = (logical_positions >= 0) & (
        (logical_positions + 1).remainder(compress_ratio) == 0
    )
    slots = torch.where(valid, slots, PAD_SLOT_ID).to(torch.int64)
    if out is not None:
        out.fill_(PAD_SLOT_ID)
        out[: slots.numel()].copy_(slots)
        return out[: slots.numel()]
    return slots