vLLM 0.29 removed AttentionSpec.indexes_kv_by_block_stride and reshaped KVCacheTensor from (size, shared_by) to an explicit layout: layers in L order plus layer_stride and block_stride. The block-stride decision now lives on one physical CacheConfig.kv_cache_layout that the engine core resolves once, and layout.is_block_outermost is the direct successor of the old per-spec flag. Diffusion backends subclass the Omni AttentionBackend ABC, so upstream get_supported_kv_cache_layouts never sees them and the engine core never resolves a layout for them. Add diffusion_kv/layout.py to pin it before get_kv_cache_configs runs, defaulting to LBNHC, which reproduces the indexes_kv_by_block_stride=False that every dense diffusion backend declared. The backend hook is kept as an enforced contract rather than deleted: a backend needing block-stride indexing selects a block-outermost layout, and a contradiction raises instead of mis-addressing K/V. Workers receive only a KVCacheConfig, so the resolved name travels on it (KVCacheConfig.kv_cache_layout) and is adopted into each worker CacheConfig before allocation. Without that, init_kv_cache raises in the worker because get_resolved_kv_cache_layout has nothing to return. init_kv_cache also lost attn_groups and cache_dtype in 0.29; the production caller in model_runner_backend and the test stubs move to the six-argument form. Tensor construction goes through one derivation built on the upstream compute_layout_strides helper, shared by production and tests, so a wrong stride cannot be masked by a parallel test-only copy. Also updates the sizing expectation: 0.29 subtracts the null block that BlockPool permanently holds back before estimating, so sixteen physical pages now yield fifteen usable ones. One review point is addressed only in part, deliberately. build_native_kv_cache_configs now takes indexes_kv_by_block_stride and propagates a block-outermost layout end to end when it is set, with a test covering control plane to worker adoption. Automatic discovery of that requirement from the workers is not added: 0.29 removed the field that used to carry it, every diffusion backend in tree declares False, and inventing an executor RPC for a capability nothing requests would ship untestable plumbing. Until a backend needs it, the seam is explicit and the mismatch raises at worker init rather than corrupting addressing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146Xu5Sgj4vXxLxvLjV5YPj Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
949 lines
42 KiB
Python
949 lines
42 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass, field, replace
|
|
from typing import Any
|
|
|
|
import torch
|
|
from vllm.config import VllmConfig, set_current_vllm_config
|
|
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
|
|
from vllm.utils.math_utils import cdiv
|
|
from vllm.v1.attention.backend import AttentionType
|
|
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
|
from vllm.v1.attention.backends.utils import get_dcp_local_seq_lens
|
|
from vllm.v1.attention.selector import get_attn_backend
|
|
from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheConfig
|
|
from vllm.v1.worker.gpu.attn_utils import build_slot_mappings_by_layer
|
|
from vllm.v1.worker.gpu.block_table import BlockTables
|
|
|
|
from vllm_omni.diffusion.attention.backends.utils.piecewise_attn import (
|
|
PagedPiecewisePlan,
|
|
build_paged_piecewise_plan,
|
|
)
|
|
from vllm_omni.diffusion.forward_context import override_paged_kv_adapter
|
|
from vllm_omni.platforms import current_omni_platform
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DiffusionPagedAttentionRowBinding:
|
|
"""Worker row and logical length installed for one allocation identity."""
|
|
|
|
row_index: int
|
|
max_seq_len: int
|
|
block_ids: tuple[tuple[int, ...], ...] = ()
|
|
|
|
|
|
DiffusionKVRowResolver = Callable[
|
|
[str, int | None, str | None],
|
|
DiffusionPagedAttentionRowBinding,
|
|
]
|
|
|
|
|
|
class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
|
|
"""Register a diffusion layer with vLLM's native cache machinery.
|
|
|
|
This object deliberately does *not* subclass ``vllm.Attention``. The
|
|
latter owns a second execution path and would bypass Omni's sequence
|
|
parallel pre/post hooks. The wrapper only supplies the small
|
|
``AttentionLayerBase`` contract needed by ``init_attn_backend`` and keeps
|
|
the platform-native attention implementation/cache view available to the
|
|
diffusion adapter.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
layer_name: str,
|
|
layer: Any,
|
|
spec: AttentionSpec,
|
|
vllm_config: VllmConfig,
|
|
device: torch.device,
|
|
ulysses_degree: int = 1,
|
|
) -> None:
|
|
if type(ulysses_degree) is not int or ulysses_degree <= 0:
|
|
raise ValueError(f"ulysses_degree must be a positive integer, got {ulysses_degree!r}")
|
|
num_heads = int(layer.num_heads)
|
|
num_kv_heads = int(spec.num_kv_heads)
|
|
if num_heads <= 0 or num_kv_heads <= 0 or num_heads % num_kv_heads != 0:
|
|
raise ValueError(
|
|
"Paged attention requires positive Q/KV heads with num_heads divisible by num_kv_heads: "
|
|
f"num_heads={num_heads}, num_kv_heads={num_kv_heads}"
|
|
)
|
|
if num_heads % ulysses_degree != 0 or num_kv_heads % ulysses_degree != 0:
|
|
raise ValueError(
|
|
"Paged attention requires Q/KV heads divisible by ulysses_degree: "
|
|
f"num_heads={num_heads}, num_kv_heads={num_kv_heads}, ulysses_degree={ulysses_degree}"
|
|
)
|
|
num_heads //= ulysses_degree
|
|
num_kv_heads //= ulysses_degree
|
|
|
|
attention_config = vllm_config.attention_config
|
|
previous_backend = attention_config.backend
|
|
previous_backend_per_kind = attention_config.backend_per_kind
|
|
try:
|
|
# This is a portable vLLM backend request. The active platform
|
|
# resolves it to its native implementation, such as FlashAttention
|
|
# on CUDA or AscendAttentionBackend on NPU.
|
|
attention_config.backend = AttentionBackendEnum.FLASH_ATTN
|
|
attention_config.backend_per_kind = {}
|
|
with set_current_vllm_config(vllm_config):
|
|
attn_backend = get_attn_backend(
|
|
head_size=spec.head_size,
|
|
dtype=vllm_config.model_config.dtype,
|
|
kv_cache_dtype=vllm_config.cache_config.cache_dtype,
|
|
attn_type=AttentionType.DECODER,
|
|
num_heads=num_heads,
|
|
)
|
|
finally:
|
|
attention_config.backend = previous_backend
|
|
attention_config.backend_per_kind = previous_backend_per_kind
|
|
attn_backend = current_omni_platform.get_diffusion_paged_kv_attn_backend(
|
|
attn_backend,
|
|
ulysses_degree=ulysses_degree,
|
|
)
|
|
# vLLM 0.29 carries the block-stride decision on the resolved physical
|
|
# ``CacheConfig.kv_cache_layout`` instead of the spec; the backend's
|
|
# requirement is enforced against that layout rather than copied here.
|
|
canonical_spec = replace(
|
|
spec,
|
|
num_kv_heads=num_kv_heads,
|
|
)
|
|
self.layer_name = layer_name
|
|
self.spec = canonical_spec
|
|
self.num_heads = num_heads
|
|
self.num_kv_heads = num_kv_heads
|
|
self.head_size = int(canonical_spec.head_size)
|
|
self.head_size_v = int(getattr(canonical_spec, "head_size_v", canonical_spec.head_size))
|
|
if self.head_size_v != self.head_size:
|
|
raise ValueError(
|
|
"Diffusion native paged attention requires equal key/value head sizes; "
|
|
f"got head_size={self.head_size}, head_size_v={self.head_size_v}"
|
|
)
|
|
self.softmax_scale = float(layer.softmax_scale)
|
|
self.attn_backend = attn_backend
|
|
self.kv_cache: torch.Tensor | None = None
|
|
# Native attention backends consume either device tensors or host
|
|
# scalar copies of these scales. The paged diffusion path is currently
|
|
# unquantized, so initialize both contracts to identity.
|
|
self._q_scale = torch.ones(1, device=device, dtype=torch.float32)
|
|
self._k_scale = torch.ones(1, device=device, dtype=torch.float32)
|
|
self._v_scale = torch.ones(1, device=device, dtype=torch.float32)
|
|
self._q_scale_float = 1.0
|
|
self._k_scale_float = 1.0
|
|
self._v_scale_float = 1.0
|
|
with set_current_vllm_config(vllm_config):
|
|
self.impl = self._create_native_impl(vllm_config)
|
|
|
|
def get_attn_backend(self):
|
|
return self.attn_backend
|
|
|
|
def _create_native_impl(self, vllm_config: VllmConfig):
|
|
impl_cls = self.attn_backend.get_impl_cls()
|
|
impl = impl_cls(
|
|
num_heads=self.num_heads,
|
|
head_size=self.head_size,
|
|
scale=self.softmax_scale,
|
|
num_kv_heads=self.num_kv_heads,
|
|
alibi_slopes=None,
|
|
sliding_window=None,
|
|
kv_cache_dtype=vllm_config.cache_config.cache_dtype,
|
|
logits_soft_cap=None,
|
|
attn_type=AttentionType.DECODER,
|
|
kv_sharing_target_layer_name=None,
|
|
)
|
|
if not callable(getattr(impl, "forward", None)):
|
|
raise RuntimeError(
|
|
f"Native paged-attention backend {self.attn_backend.get_name()!r} has no forward implementation"
|
|
)
|
|
needs_explicit_cache_update = (
|
|
not self.attn_backend.forward_includes_kv_cache_update
|
|
or current_omni_platform.requires_diffusion_paged_kv_prewrite()
|
|
)
|
|
if needs_explicit_cache_update and not callable(getattr(impl, "do_kv_cache_update", None)):
|
|
raise RuntimeError(
|
|
f"Native paged-attention backend {self.attn_backend.get_name()!r} cannot update the KV cache"
|
|
)
|
|
if hasattr(impl, "vllm_flash_attn_version") and impl.vllm_flash_attn_version is None:
|
|
raise RuntimeError(f"Native paged-attention kernel is unavailable for diffusion layer {self.layer_name!r}")
|
|
return impl
|
|
|
|
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> AttentionSpec:
|
|
del vllm_config
|
|
return self.spec
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DiffusionPagedAttentionRow:
|
|
"""One logical BlockTable row participating in a paged attention call."""
|
|
|
|
request_id: str
|
|
query_len: int
|
|
seq_len: int
|
|
kv_start_pos: int = 0
|
|
sequence_id: int | None = None
|
|
context_id: str | None = None
|
|
|
|
def __post_init__(self) -> None:
|
|
if type(self.request_id) is not str or not self.request_id:
|
|
raise ValueError("Paged attention request_id must be a non-empty string")
|
|
if (self.sequence_id is None) == (self.context_id is None):
|
|
raise ValueError("Paged attention row requires exactly one of sequence_id or context_id")
|
|
if self.sequence_id is not None and (type(self.sequence_id) is not int or self.sequence_id < 0):
|
|
raise ValueError("Paged attention sequence_id must be a non-negative integer")
|
|
if self.context_id is not None and (type(self.context_id) is not str or not self.context_id):
|
|
raise ValueError("Paged attention context_id must be a non-empty string")
|
|
if type(self.query_len) is not int or self.query_len <= 0:
|
|
raise ValueError("Paged attention query_len must be a positive integer")
|
|
if type(self.seq_len) is not int or self.seq_len <= 0:
|
|
raise ValueError("Paged attention seq_len must be a positive integer")
|
|
if type(self.kv_start_pos) is not int or self.kv_start_pos < 0:
|
|
raise ValueError("Paged attention kv_start_pos must be a non-negative integer")
|
|
if self.kv_start_pos + self.query_len > self.seq_len:
|
|
raise ValueError(
|
|
"Paged attention write span exceeds seq_len: "
|
|
f"start={self.kv_start_pos}, query_len={self.query_len}, seq_len={self.seq_len}"
|
|
)
|
|
|
|
@property
|
|
def identity(self) -> tuple[str, int | None, str | None]:
|
|
return (self.request_id, self.sequence_id, self.context_id)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DiffusionPagedAttentionMetadata:
|
|
"""Runner-owned row layouts for one request-level denoise loop."""
|
|
|
|
prefill_rows: tuple[DiffusionPagedAttentionRow, ...]
|
|
denoise_rows: tuple[DiffusionPagedAttentionRow, ...]
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.prefill_rows or not self.denoise_rows:
|
|
raise ValueError("Paged attention metadata requires both prefill and denoise rows")
|
|
prefill_identities = tuple(row.identity for row in self.prefill_rows)
|
|
denoise_identities = tuple(row.identity for row in self.denoise_rows)
|
|
if prefill_identities != denoise_identities:
|
|
raise ValueError(
|
|
"Paged attention prefill and denoise rows must use the same ordered identities: "
|
|
f"prefill={prefill_identities!r}, denoise={denoise_identities!r}"
|
|
)
|
|
|
|
def rows_for_step(self, step_idx: int | None) -> tuple[DiffusionPagedAttentionRow, ...]:
|
|
if type(step_idx) is not int or step_idx < 0:
|
|
raise ValueError(f"Paged attention requires a non-negative denoise step index, got {step_idx!r}")
|
|
return self.prefill_rows if step_idx == 0 else self.denoise_rows
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PreparedDiffusionPagedAttentionBatch:
|
|
"""Native metadata shared by all paged attention layers in one forward."""
|
|
|
|
rows: tuple[DiffusionPagedAttentionRow, ...]
|
|
row_indices: torch.Tensor
|
|
query_start_loc: torch.Tensor
|
|
seq_lens: torch.Tensor
|
|
positions: torch.Tensor
|
|
block_tables: tuple[torch.Tensor, ...]
|
|
slot_mappings: torch.Tensor
|
|
attn_metadata: dict[str, Any]
|
|
slot_mappings_by_layer: dict[str, torch.Tensor]
|
|
num_tokens: int
|
|
_owner: object = field(repr=False, compare=False)
|
|
_generation: int = field(repr=False, compare=False)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DiffusionPagedAttentionContext:
|
|
"""One layer's native inputs for an Omni paged-backend invocation."""
|
|
|
|
layer: DiffusionPagedAttentionLayerAdapter
|
|
query: torch.Tensor
|
|
key_write: torch.Tensor
|
|
value_write: torch.Tensor
|
|
slot_mapping: torch.Tensor
|
|
native_metadata: Any
|
|
piecewise_plan: PagedPiecewisePlan | None
|
|
piecewise_native_metadata: tuple[Any, ...]
|
|
query_token_shape: tuple[int, ...]
|
|
query_has_head_dims: bool
|
|
output_scatter_indices: torch.Tensor | None = None
|
|
|
|
def restore_output(self, output: torch.Tensor) -> torch.Tensor:
|
|
if self.output_scatter_indices is not None:
|
|
flat_output = output.reshape(-1, self.layer.num_heads, self.layer.head_size_v)
|
|
restored = output.new_zeros(
|
|
(*self.query_token_shape, self.layer.num_heads, self.layer.head_size_v)
|
|
).reshape(-1, self.layer.num_heads, self.layer.head_size_v)
|
|
restored.index_copy_(0, self.output_scatter_indices, flat_output)
|
|
if self.query_has_head_dims:
|
|
return restored.reshape(
|
|
*self.query_token_shape,
|
|
self.layer.num_heads,
|
|
self.layer.head_size_v,
|
|
)
|
|
return restored.reshape(
|
|
*self.query_token_shape,
|
|
self.layer.num_heads * self.layer.head_size_v,
|
|
)
|
|
if self.query_has_head_dims:
|
|
return output.reshape(*self.query_token_shape, self.layer.num_heads, self.layer.head_size_v)
|
|
return output.reshape(*self.query_token_shape, self.layer.num_heads * self.layer.head_size_v)
|
|
|
|
|
|
class DiffusionPagedAttentionRuntime:
|
|
"""Activate Runner-prepared rows lazily as the denoise loop advances."""
|
|
|
|
def __init__(
|
|
self,
|
|
adapter: DiffusionPagedAttentionAdapter,
|
|
metadata: DiffusionPagedAttentionMetadata,
|
|
) -> None:
|
|
self.adapter = adapter
|
|
self.metadata = metadata
|
|
self._active_phase: bool | None = None
|
|
self._activation: Any | None = None
|
|
self._closed = False
|
|
|
|
def ensure_active(self, step_idx: int | None) -> DiffusionPagedAttentionAdapter:
|
|
if self._closed:
|
|
raise RuntimeError("Paged attention runtime is already closed")
|
|
is_prefill = step_idx == 0
|
|
rows = self.metadata.rows_for_step(step_idx)
|
|
if self._active_phase == is_prefill:
|
|
return self.adapter
|
|
|
|
self._deactivate()
|
|
batch = self.adapter.prepare_batch(rows)
|
|
activation = self.adapter.activate(batch)
|
|
activation.__enter__()
|
|
self._activation = activation
|
|
self._active_phase = is_prefill
|
|
return self.adapter
|
|
|
|
def _deactivate(self) -> None:
|
|
activation = self._activation
|
|
self._activation = None
|
|
self._active_phase = None
|
|
if activation is not None:
|
|
activation.__exit__(None, None, None)
|
|
|
|
def close(self) -> None:
|
|
if self._closed:
|
|
return
|
|
self._deactivate()
|
|
self._closed = True
|
|
|
|
@contextmanager
|
|
def activate(self) -> Iterator[DiffusionPagedAttentionRuntime]:
|
|
try:
|
|
yield self
|
|
finally:
|
|
self.close()
|
|
|
|
|
|
class DiffusionPagedAttentionAdapter:
|
|
"""Translate diffusion rows into vLLM metadata for an Omni paged backend."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
vllm_config: VllmConfig,
|
|
device: torch.device,
|
|
kv_cache_config: KVCacheConfig,
|
|
block_tables: BlockTables,
|
|
attn_groups: list[list[Any]],
|
|
layers: Mapping[str, DiffusionPagedAttentionLayerAdapter],
|
|
resolve_row: DiffusionKVRowResolver,
|
|
) -> None:
|
|
if not layers:
|
|
raise ValueError("Paged attention requires at least one native attention layer")
|
|
if len(attn_groups) != len(kv_cache_config.kv_cache_groups):
|
|
raise ValueError(
|
|
"Paged attention group mismatch: "
|
|
f"builders={len(attn_groups)}, cache_groups={len(kv_cache_config.kv_cache_groups)}"
|
|
)
|
|
self.vllm_config = vllm_config
|
|
self.device = torch.device(device)
|
|
self.kv_cache_config = kv_cache_config
|
|
self.block_tables = block_tables
|
|
self.attn_groups = attn_groups
|
|
self.layers = dict(layers)
|
|
self.resolve_row = resolve_row
|
|
self._owner = object()
|
|
self._prepare_generation = 0
|
|
self._active_batch: PreparedDiffusionPagedAttentionBatch | None = None
|
|
self._active_piecewise_plan: PagedPiecewisePlan | None = None
|
|
self._active_piecewise_native_metadata: tuple[dict[str, Any], ...] | None = None
|
|
self._causal_by_group = self._resolve_group_causality()
|
|
self._reorder_batch_threshold = self._resolve_reorder_batch_threshold()
|
|
|
|
def _resolve_group_causality(self) -> dict[int, bool]:
|
|
causal_by_group: dict[int, bool] = {}
|
|
for group_index, cache_group in enumerate(self.kv_cache_config.kv_cache_groups):
|
|
layer_causality = {
|
|
not bool(getattr(self.layers[layer_name].spec, "non_causal", False))
|
|
for layer_name in cache_group.layer_names
|
|
}
|
|
if len(layer_causality) != 1:
|
|
raise ValueError(f"Paged attention cache group {group_index} mixes causal and non-causal layers")
|
|
causal_by_group[group_index] = layer_causality.pop()
|
|
return causal_by_group
|
|
|
|
def _resolve_reorder_batch_threshold(self) -> int | None:
|
|
thresholds = [
|
|
group.get_metadata_builder(0).reorder_batch_threshold
|
|
for group_index, groups in enumerate(self.attn_groups)
|
|
if self._causal_by_group[group_index]
|
|
for group in groups
|
|
]
|
|
concrete_thresholds = [threshold for threshold in thresholds if threshold is not None]
|
|
return min(concrete_thresholds, default=None)
|
|
|
|
def _validate_native_batch_order(self, rows: tuple[DiffusionPagedAttentionRow, ...]) -> None:
|
|
threshold = self._reorder_batch_threshold
|
|
if threshold is None or not any(self._causal_by_group.values()):
|
|
return
|
|
|
|
found_long_query = False
|
|
for row in rows:
|
|
if row.query_len > threshold:
|
|
found_long_query = True
|
|
elif found_long_query:
|
|
raise ValueError(
|
|
"Causal paged attention rows must place native decode/short-query rows before "
|
|
f"prefill/long-query rows (decode threshold={threshold})"
|
|
)
|
|
|
|
def _validate_row_capacity(
|
|
self,
|
|
rows: tuple[DiffusionPagedAttentionRow, ...],
|
|
row_indices: list[int],
|
|
) -> None:
|
|
native_num_blocks = self.block_tables.num_blocks.np
|
|
blocks_per_kv_block = self.block_tables.blocks_per_kv_block
|
|
for row, row_index in zip(rows, row_indices, strict=True):
|
|
if type(row_index) is not int or not 0 <= row_index < self.block_tables.max_num_reqs:
|
|
raise ValueError(f"Paged attention row {row.identity!r} resolved to invalid Worker row {row_index!r}")
|
|
for group_index, (block_size, block_multiplier) in enumerate(
|
|
zip(
|
|
self.block_tables.block_sizes,
|
|
blocks_per_kv_block,
|
|
strict=True,
|
|
)
|
|
):
|
|
required_blocks = cdiv(
|
|
row.seq_len,
|
|
block_size * self.block_tables.cp_size,
|
|
)
|
|
required_kernel_blocks = required_blocks * block_multiplier
|
|
installed_kernel_blocks = int(native_num_blocks[group_index, row_index])
|
|
if required_kernel_blocks > installed_kernel_blocks:
|
|
raise ValueError(
|
|
f"Paged attention row {row.identity!r} requires {required_blocks} blocks in "
|
|
f"cache group {group_index} for seq_len={row.seq_len}, but its installed Worker "
|
|
f"row contains only {installed_kernel_blocks // block_multiplier} blocks"
|
|
)
|
|
|
|
def _build_native_metadata(
|
|
self,
|
|
*,
|
|
query_lens: Sequence[int],
|
|
query_start_loc: torch.Tensor,
|
|
query_start_loc_cpu: torch.Tensor,
|
|
seq_lens: torch.Tensor,
|
|
seq_lens_cpu: torch.Tensor,
|
|
positions: torch.Tensor,
|
|
block_tables: Sequence[torch.Tensor],
|
|
slot_mappings: torch.Tensor,
|
|
causal: bool | Mapping[int, bool],
|
|
) -> dict[str, Any]:
|
|
dcp_local_seq_lens = None
|
|
if self.block_tables.cp_size > 1:
|
|
dcp_local_seq_lens = get_dcp_local_seq_lens(
|
|
seq_lens,
|
|
dcp_size=self.block_tables.cp_size,
|
|
dcp_rank=self.block_tables.cp_rank,
|
|
cp_kv_cache_interleave_size=self.block_tables.cp_interleave,
|
|
)
|
|
with set_current_vllm_config(self.vllm_config):
|
|
return current_omni_platform.build_diffusion_kv_attn_metadata(
|
|
attn_groups=self.attn_groups,
|
|
num_reqs=len(query_lens),
|
|
num_tokens=sum(query_lens),
|
|
query_start_loc_gpu=query_start_loc,
|
|
query_start_loc_cpu=query_start_loc_cpu,
|
|
max_query_len=max(query_lens),
|
|
seq_lens=seq_lens,
|
|
max_seq_len=max(int(seq_len) for seq_len in seq_lens_cpu),
|
|
block_tables=block_tables,
|
|
slot_mappings=slot_mappings,
|
|
kv_cache_config=self.kv_cache_config,
|
|
seq_lens_cpu=seq_lens_cpu,
|
|
seq_lens_cpu_upper_bound=seq_lens_cpu,
|
|
dcp_local_seq_lens=dcp_local_seq_lens,
|
|
positions=positions,
|
|
causal=causal,
|
|
)
|
|
|
|
def prepare_batch(
|
|
self,
|
|
rows: Sequence[DiffusionPagedAttentionRow],
|
|
) -> PreparedDiffusionPagedAttentionBatch:
|
|
rows = tuple(rows)
|
|
if not rows:
|
|
raise ValueError("Paged attention batch must contain at least one row")
|
|
if self._active_batch is not None:
|
|
raise RuntimeError("Cannot prepare paged attention metadata during an active forward")
|
|
if len(rows) > self.block_tables.max_num_reqs:
|
|
raise ValueError(
|
|
f"Paged attention batch has {len(rows)} rows; Worker capacity is {self.block_tables.max_num_reqs}"
|
|
)
|
|
|
|
identities = [row.identity for row in rows]
|
|
if len(set(identities)) != len(identities):
|
|
raise ValueError("Paged attention batch contains duplicate row identities")
|
|
row_bindings = [self.resolve_row(row.request_id, row.sequence_id, row.context_id) for row in rows]
|
|
row_indices_list = [binding.row_index for binding in row_bindings]
|
|
if len(set(row_indices_list)) != len(row_indices_list):
|
|
raise ValueError("Paged attention batch resolves multiple inputs to the same Worker row")
|
|
for row, binding in zip(rows, row_bindings, strict=True):
|
|
if type(binding.max_seq_len) is not int or binding.max_seq_len < 0:
|
|
raise ValueError(
|
|
f"Paged attention row {row.identity!r} resolved to invalid logical capacity {binding.max_seq_len!r}"
|
|
)
|
|
if row.seq_len > binding.max_seq_len:
|
|
raise ValueError(
|
|
f"Paged attention row {row.identity!r} uses seq_len={row.seq_len}, but its installed "
|
|
f"allocation has logical length {binding.max_seq_len}"
|
|
)
|
|
self._validate_native_batch_order(rows)
|
|
self._validate_row_capacity(rows, row_indices_list)
|
|
|
|
query_lens = [row.query_len for row in rows]
|
|
num_tokens = sum(query_lens)
|
|
if num_tokens > self.block_tables.max_num_batched_tokens:
|
|
raise ValueError(
|
|
f"Paged attention batch has {num_tokens} tokens; Worker capacity is "
|
|
f"{self.block_tables.max_num_batched_tokens}"
|
|
)
|
|
query_offsets = [0]
|
|
for query_len in query_lens:
|
|
query_offsets.append(query_offsets[-1] + query_len)
|
|
|
|
# Native BlockTables reuses persistent gather/slot buffers. Starting a
|
|
# new preparation invalidates every batch prepared before it, even if a
|
|
# later metadata builder raises.
|
|
self._prepare_generation += 1
|
|
generation = self._prepare_generation
|
|
row_indices = torch.tensor(row_indices_list, dtype=torch.int32, device=self.device)
|
|
query_start_loc_cpu = torch.tensor(query_offsets, dtype=torch.int32)
|
|
query_start_loc = query_start_loc_cpu.to(self.device)
|
|
seq_lens_cpu = torch.tensor([row.seq_len for row in rows], dtype=torch.int32)
|
|
seq_lens = seq_lens_cpu.to(self.device)
|
|
positions = torch.cat(
|
|
[
|
|
torch.arange(
|
|
row.kv_start_pos,
|
|
row.kv_start_pos + row.query_len,
|
|
dtype=torch.int64,
|
|
device=self.device,
|
|
)
|
|
for row in rows
|
|
]
|
|
)
|
|
|
|
block_tables = self.block_tables.gather_block_tables(
|
|
row_indices,
|
|
num_reqs_padded=len(rows),
|
|
)
|
|
slot_mappings = self.block_tables.compute_slot_mappings(
|
|
row_indices,
|
|
query_start_loc,
|
|
positions,
|
|
num_tokens_padded=num_tokens,
|
|
)
|
|
causal: bool | Mapping[int, bool]
|
|
causal_values = set(self._causal_by_group.values())
|
|
causal = causal_values.pop() if len(causal_values) == 1 else self._causal_by_group
|
|
attn_metadata = self._build_native_metadata(
|
|
query_lens=query_lens,
|
|
query_start_loc=query_start_loc,
|
|
query_start_loc_cpu=query_start_loc_cpu,
|
|
seq_lens=seq_lens,
|
|
seq_lens_cpu=seq_lens_cpu,
|
|
positions=positions,
|
|
block_tables=block_tables,
|
|
slot_mappings=slot_mappings,
|
|
causal=causal,
|
|
)
|
|
slot_mappings_by_layer = build_slot_mappings_by_layer(
|
|
slot_mappings,
|
|
self.kv_cache_config,
|
|
)
|
|
return PreparedDiffusionPagedAttentionBatch(
|
|
rows=rows,
|
|
row_indices=row_indices,
|
|
query_start_loc=query_start_loc,
|
|
seq_lens=seq_lens,
|
|
positions=positions,
|
|
block_tables=tuple(block_tables),
|
|
slot_mappings=slot_mappings,
|
|
attn_metadata=attn_metadata,
|
|
slot_mappings_by_layer=slot_mappings_by_layer,
|
|
num_tokens=num_tokens,
|
|
_owner=self._owner,
|
|
_generation=generation,
|
|
)
|
|
|
|
def invalidate_prepared_batches(self) -> None:
|
|
"""Invalidate native buffer views after BlockTable state changes."""
|
|
|
|
if self._active_batch is not None:
|
|
raise RuntimeError("Cannot change paged attention BlockTables during an active forward")
|
|
self._prepare_generation += 1
|
|
|
|
@contextmanager
|
|
def activate(
|
|
self,
|
|
batch: PreparedDiffusionPagedAttentionBatch,
|
|
) -> Iterator[DiffusionPagedAttentionAdapter]:
|
|
if batch._owner is not self._owner:
|
|
raise ValueError("Prepared paged attention batch belongs to a different adapter")
|
|
if batch._generation != self._prepare_generation:
|
|
raise ValueError("Prepared paged attention batch is stale after a newer batch preparation")
|
|
if self._active_batch is not None:
|
|
raise RuntimeError("Paged attention adapter already has an active forward")
|
|
self._active_batch = batch
|
|
self._active_piecewise_plan = None
|
|
self._active_piecewise_native_metadata = None
|
|
try:
|
|
with override_paged_kv_adapter(self), set_current_vllm_config(self.vllm_config):
|
|
yield self
|
|
finally:
|
|
self._active_piecewise_plan = None
|
|
self._active_piecewise_native_metadata = None
|
|
self._active_batch = None
|
|
|
|
@staticmethod
|
|
def _flatten_tensor(
|
|
tensor: torch.Tensor,
|
|
*,
|
|
num_heads: int,
|
|
head_size: int,
|
|
name: str,
|
|
) -> tuple[torch.Tensor, tuple[int, ...], bool]:
|
|
if tensor.ndim >= 3 and tensor.shape[-2:] == (num_heads, head_size):
|
|
return tensor.reshape(-1, num_heads, head_size), tuple(tensor.shape[:-2]), True
|
|
hidden_size = num_heads * head_size
|
|
if tensor.ndim >= 1 and tensor.shape[-1] == hidden_size:
|
|
return tensor.reshape(-1, num_heads, head_size), tuple(tensor.shape[:-1]), False
|
|
raise ValueError(
|
|
f"Paged attention {name} must end in ({num_heads}, {head_size}) or ({hidden_size},); "
|
|
f"got shape={tuple(tensor.shape)}"
|
|
)
|
|
|
|
@staticmethod
|
|
def _token_packing_indices(
|
|
token_shape: tuple[int, ...],
|
|
batch: PreparedDiffusionPagedAttentionBatch,
|
|
*,
|
|
name: str,
|
|
device: torch.device,
|
|
) -> torch.Tensor | None:
|
|
if len(token_shape) == 1:
|
|
if token_shape[0] == batch.num_tokens:
|
|
return None
|
|
raise ValueError(
|
|
f"Paged attention {name} token count must match the prepared write batch: "
|
|
f"tokens={token_shape[0]}, prepared={batch.num_tokens}"
|
|
)
|
|
if len(token_shape) == 2:
|
|
batch_size, tokens_per_row = token_shape
|
|
query_lens = tuple(row.query_len for row in batch.rows)
|
|
if batch_size != len(batch.rows) or any(query_len > tokens_per_row for query_len in query_lens):
|
|
raise ValueError(
|
|
f"Paged attention batched {name} layout must match prepared rows for the current write: "
|
|
f"shape={token_shape}, row_query_lens={query_lens}"
|
|
)
|
|
if all(query_len == tokens_per_row for query_len in query_lens):
|
|
return None
|
|
return torch.cat(
|
|
[
|
|
torch.arange(
|
|
row_index * tokens_per_row,
|
|
row_index * tokens_per_row + query_len,
|
|
dtype=torch.long,
|
|
device=device,
|
|
)
|
|
for row_index, query_len in enumerate(query_lens)
|
|
]
|
|
)
|
|
raise ValueError(
|
|
f"Paged attention {name} supports packed [T, ...] or uniform batched [B, T, ...] token layouts; "
|
|
f"got token shape={token_shape}"
|
|
)
|
|
|
|
def _get_piecewise_plan(
|
|
self,
|
|
full_attn_spans: list[list[tuple[int, int]]],
|
|
) -> PagedPiecewisePlan:
|
|
batch = self._active_batch
|
|
if batch is None:
|
|
raise RuntimeError("Piecewise paged attention requires an active prepared batch")
|
|
if any(row.kv_start_pos + row.query_len != row.seq_len for row in batch.rows):
|
|
invalid_rows = [row.identity for row in batch.rows if row.kv_start_pos + row.query_len != row.seq_len]
|
|
raise ValueError(
|
|
"Paged piecewise attention requires each query/write span to end at seq_len; "
|
|
f"invalid rows={invalid_rows!r}"
|
|
)
|
|
|
|
cached_plan = self._active_piecewise_plan
|
|
if cached_plan is not None:
|
|
frozen_spans = tuple(tuple(tuple(span) for span in row_spans) for row_spans in full_attn_spans)
|
|
if cached_plan.spans != frozen_spans:
|
|
raise ValueError("Paged piecewise attention metadata changed between layers in one active batch")
|
|
return cached_plan
|
|
|
|
plan = build_paged_piecewise_plan(
|
|
full_attn_spans,
|
|
query_offsets=[row.kv_start_pos for row in batch.rows],
|
|
query_lens=[row.query_len for row in batch.rows],
|
|
seq_lens=[row.seq_len for row in batch.rows],
|
|
device=self.device,
|
|
)
|
|
self._active_piecewise_plan = plan
|
|
return plan
|
|
|
|
def _get_piecewise_native_metadata(
|
|
self,
|
|
plan: PagedPiecewisePlan,
|
|
) -> tuple[dict[str, Any], ...]:
|
|
"""Build one native metadata object per segment and cache it by batch."""
|
|
|
|
batch = self._active_batch
|
|
if batch is None:
|
|
raise RuntimeError("Piecewise paged attention requires an active prepared batch")
|
|
cached_metadata = self._active_piecewise_native_metadata
|
|
if cached_metadata is not None:
|
|
return cached_metadata
|
|
|
|
metadata_by_segment: list[dict[str, Any]] = []
|
|
for packed_segment in plan.segments:
|
|
row_segments = packed_segment.row_segments
|
|
query_lens = [segment.q_end - segment.q_start for segment in row_segments]
|
|
seq_lens_values = [segment.kv_end for segment in row_segments]
|
|
query_offsets = [0]
|
|
for query_len in query_lens:
|
|
query_offsets.append(query_offsets[-1] + query_len)
|
|
query_start_loc_cpu = torch.tensor(query_offsets, dtype=torch.int32)
|
|
seq_lens_cpu = torch.tensor(seq_lens_values, dtype=torch.int32)
|
|
if packed_segment.query_range is None:
|
|
positions = batch.positions.index_select(0, packed_segment.query_indices)
|
|
slot_mappings = batch.slot_mappings.index_select(-1, packed_segment.query_indices)
|
|
else:
|
|
start, end = packed_segment.query_range
|
|
positions = batch.positions[start:end]
|
|
slot_mappings = batch.slot_mappings[..., start:end]
|
|
segment_metadata = self._build_native_metadata(
|
|
query_lens=query_lens,
|
|
query_start_loc=query_start_loc_cpu.to(self.device),
|
|
query_start_loc_cpu=query_start_loc_cpu,
|
|
seq_lens=seq_lens_cpu.to(self.device),
|
|
seq_lens_cpu=seq_lens_cpu,
|
|
positions=positions,
|
|
block_tables=batch.block_tables,
|
|
slot_mappings=slot_mappings,
|
|
causal=(row_segments[0].mode == "causal"),
|
|
)
|
|
# FA3's full-CUDA-graph metadata builder reuses one persistent
|
|
# scheduler buffer across builds. Piecewise attention prepares all
|
|
# segments before executing any of them, so each segment needs its
|
|
# own snapshot rather than a view overwritten by the next build.
|
|
snapshotted = set()
|
|
for native_metadata in segment_metadata.values():
|
|
metadata_id = id(native_metadata)
|
|
if metadata_id in snapshotted:
|
|
continue
|
|
snapshotted.add(metadata_id)
|
|
for field_name in ("scheduler_metadata", "prefix_scheduler_metadata"):
|
|
scheduler_metadata = getattr(native_metadata, field_name, None)
|
|
if isinstance(scheduler_metadata, torch.Tensor):
|
|
setattr(native_metadata, field_name, scheduler_metadata.clone())
|
|
metadata_by_segment.append(segment_metadata)
|
|
self._active_piecewise_native_metadata = tuple(metadata_by_segment)
|
|
return self._active_piecewise_native_metadata
|
|
|
|
def prepare_layer_context(
|
|
self,
|
|
layer_name: str,
|
|
query: torch.Tensor,
|
|
key: torch.Tensor,
|
|
value: torch.Tensor,
|
|
*,
|
|
omni_attn_metadata: Any | None = None,
|
|
) -> DiffusionPagedAttentionContext:
|
|
if self._active_batch is None:
|
|
raise RuntimeError("Paged attention forward must run inside adapter.activate(batch)")
|
|
try:
|
|
layer = self.layers[layer_name]
|
|
except KeyError as exc:
|
|
raise KeyError(f"Unknown diffusion paged attention layer {layer_name!r}") from exc
|
|
batch = self._active_batch
|
|
full_attn_spans = self._validate_omni_attn_metadata(omni_attn_metadata)
|
|
|
|
query_flat, query_token_shape, query_has_head_dims = self._flatten_tensor(
|
|
query,
|
|
num_heads=layer.num_heads,
|
|
head_size=layer.head_size,
|
|
name="query",
|
|
)
|
|
key_flat, key_token_shape, _ = self._flatten_tensor(
|
|
key,
|
|
num_heads=layer.num_kv_heads,
|
|
head_size=layer.head_size,
|
|
name="key",
|
|
)
|
|
value_flat, value_token_shape, _ = self._flatten_tensor(
|
|
value,
|
|
num_heads=layer.num_kv_heads,
|
|
head_size=layer.head_size_v,
|
|
name="value",
|
|
)
|
|
if key_token_shape != query_token_shape:
|
|
if len(key_token_shape) == 1:
|
|
raise ValueError(
|
|
"Paged attention key token count must match the query layout: "
|
|
f"query={query_token_shape}, key={key_token_shape}"
|
|
)
|
|
raise ValueError(
|
|
"Paged attention batched key layout must match prepared rows and the query layout: "
|
|
f"query={query_token_shape}, key={key_token_shape}"
|
|
)
|
|
if value_token_shape != query_token_shape:
|
|
if len(value_token_shape) == 1:
|
|
raise ValueError(
|
|
"Paged attention value token count must match the query layout: "
|
|
f"query={query_token_shape}, value={value_token_shape}"
|
|
)
|
|
raise ValueError(
|
|
"Paged attention batched value layout must match prepared rows and the query layout: "
|
|
f"query={query_token_shape}, value={value_token_shape}"
|
|
)
|
|
output_scatter_indices = self._token_packing_indices(
|
|
query_token_shape,
|
|
batch,
|
|
name="Q/K/V",
|
|
device=query.device,
|
|
)
|
|
if output_scatter_indices is not None:
|
|
query_flat = query_flat.index_select(0, output_scatter_indices)
|
|
key_flat = key_flat.index_select(0, output_scatter_indices)
|
|
value_flat = value_flat.index_select(0, output_scatter_indices)
|
|
if query.device != self.device or key.device != self.device or value.device != self.device:
|
|
raise ValueError(
|
|
f"Paged attention Q/K/V must be on {self.device}; "
|
|
f"got query={query.device}, key={key.device}, value={value.device}"
|
|
)
|
|
if query.dtype != key.dtype or query.dtype != value.dtype:
|
|
raise ValueError(f"Paged attention Q/K/V dtypes must match; got {query.dtype}, {key.dtype}, {value.dtype}")
|
|
expected_dtype = self.vllm_config.model_config.dtype
|
|
if query.dtype != expected_dtype:
|
|
raise ValueError(
|
|
f"Paged attention Q/K/V dtype must match model activation dtype {expected_dtype}; got {query.dtype}"
|
|
)
|
|
if not bool(getattr(layer.spec, "non_causal", False)):
|
|
non_suffix_rows = [row.identity for row in batch.rows if row.kv_start_pos + row.query_len != row.seq_len]
|
|
if non_suffix_rows:
|
|
raise ValueError(
|
|
"Causal paged attention requires each query/write span to end at seq_len; "
|
|
f"invalid rows={non_suffix_rows!r}"
|
|
)
|
|
|
|
slot_mapping = batch.slot_mappings_by_layer.get(layer_name)
|
|
if slot_mapping is None:
|
|
raise KeyError(f"No native slot mapping was built for diffusion layer {layer_name!r}")
|
|
slot_mapping = slot_mapping.reshape(-1)[: batch.num_tokens]
|
|
if slot_mapping.numel() != batch.num_tokens:
|
|
raise ValueError(
|
|
f"Paged attention slot mapping for {layer_name!r} has {slot_mapping.numel()} entries; "
|
|
f"expected {batch.num_tokens}"
|
|
)
|
|
|
|
# vLLM's CUDA writer consumes the source token strides directly, so
|
|
# preserve strided QKV-projection views instead of materializing an
|
|
# extra K/V copy here. Platform writers with stricter contracts (for
|
|
# example, Ascend PA scatter) make their inputs contiguous locally.
|
|
# The native CUDA writer still assumes heads and head dimensions are
|
|
# packed within each token; materialize unusual inner layouts only.
|
|
if key_flat.stride(-1) != 1 or (key_flat.shape[-2] > 1 and key_flat.stride(-2) != key_flat.shape[-1]):
|
|
key_flat = key_flat.contiguous()
|
|
if value_flat.stride(-1) != 1 or (value_flat.shape[-2] > 1 and value_flat.stride(-2) != value_flat.shape[-1]):
|
|
value_flat = value_flat.contiguous()
|
|
# The writer also consumes slot_mapping directly, so a span that starts
|
|
# inside a physical block naturally writes only the requested suffix.
|
|
try:
|
|
native_metadata = batch.attn_metadata[layer_name]
|
|
except KeyError as exc:
|
|
raise KeyError(f"No native attention metadata was built for diffusion layer {layer_name!r}") from exc
|
|
piecewise_plan: PagedPiecewisePlan | None = None
|
|
piecewise_native_metadata: tuple[Any, ...] = ()
|
|
if full_attn_spans is not None:
|
|
piecewise_plan = self._get_piecewise_plan(full_attn_spans)
|
|
native_metadata_by_segment = self._get_piecewise_native_metadata(piecewise_plan)
|
|
try:
|
|
piecewise_native_metadata = tuple(
|
|
segment_metadata[layer_name] for segment_metadata in native_metadata_by_segment
|
|
)
|
|
except KeyError as exc:
|
|
raise KeyError(
|
|
f"No piecewise native attention metadata was built for diffusion layer {layer_name!r}"
|
|
) from exc
|
|
return DiffusionPagedAttentionContext(
|
|
layer=layer,
|
|
query=query_flat.contiguous(),
|
|
key_write=key_flat,
|
|
value_write=value_flat,
|
|
slot_mapping=slot_mapping,
|
|
native_metadata=native_metadata,
|
|
piecewise_plan=piecewise_plan,
|
|
piecewise_native_metadata=piecewise_native_metadata,
|
|
query_token_shape=query_token_shape,
|
|
query_has_head_dims=query_has_head_dims,
|
|
output_scatter_indices=output_scatter_indices,
|
|
)
|
|
|
|
@staticmethod
|
|
def _validate_omni_attn_metadata(
|
|
metadata: Any | None,
|
|
) -> list[list[tuple[int, int]]] | None:
|
|
if metadata is None:
|
|
return None
|
|
full_attn_spans = getattr(metadata, "full_attn_spans", None)
|
|
attn_mask = getattr(metadata, "attn_mask", None)
|
|
unsupported_fields = [
|
|
field_name
|
|
for field_name in (
|
|
"joint_attn_mask",
|
|
"query_ranges",
|
|
"video_layout",
|
|
)
|
|
if getattr(metadata, field_name, None) is not None
|
|
]
|
|
if attn_mask is not None:
|
|
if full_attn_spans is None:
|
|
unsupported_fields.append("attn_mask")
|
|
elif not isinstance(attn_mask, torch.Tensor) or attn_mask.ndim != 4:
|
|
unsupported_fields.append("attn_mask (piecewise paging requires a 4D tensor)")
|
|
extra = getattr(metadata, "extra", None)
|
|
if extra:
|
|
unsupported_fields.append(f"extra={sorted(extra)}")
|
|
if unsupported_fields:
|
|
raise NotImplementedError(
|
|
"Diffusion paged attention cannot translate Omni attention metadata fields "
|
|
f"{unsupported_fields!r} to native FlashAttention metadata"
|
|
)
|
|
return full_attn_spans
|