[Diffusion][HunyuanImage3] Add request paged prefix caching (#7719)
Signed-off-by: zwhzzz0821 <2831474076@qq.com>
This commit is contained in:
1 parent
9a1a05facd
commit
205aa01c7b
49 files changed
+2889
-127
No files matched your search
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Small diffusion identity helpers alongside vLLM's native KV cache utilities.
|
||||
|
||||
Content hashing runs before Scheduler admission, only with prefix caching on.
|
||||
Requests carry native MultiModalFeatureSpec / PlaceholderRange values; native
|
||||
generate_block_hash_extra_keys handles block intersections, with no per-token
|
||||
extra-key expansion or intermediate dependency objects.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from vllm.multimodal.hasher import MultiModalHasher
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
|
||||
def _normalize_identity(value: object) -> object:
|
||||
# MultiModalHasher handles tensor bytes (including BF16). Its generic
|
||||
# recursive serialization alone does not canonicalize nested dict order
|
||||
# or distinguish every container/scalar boundary. A typed JSON envelope
|
||||
# supplies those guarantees and rejects the upstream pickle fallback.
|
||||
if value is None:
|
||||
return ["none"]
|
||||
if isinstance(value, np.generic):
|
||||
scalar = value.item()
|
||||
if isinstance(scalar, np.generic):
|
||||
# Extended-precision scalars have no lossless Python scalar;
|
||||
# item() returns another NumPy scalar instead of making progress.
|
||||
raise TypeError(f"Unsupported prefix-cache identity scalar dtype: {value.dtype}")
|
||||
return _normalize_identity(scalar)
|
||||
if isinstance(value, bool):
|
||||
return ["bool", value]
|
||||
if isinstance(value, int):
|
||||
return ["int", str(value)]
|
||||
if isinstance(value, float):
|
||||
return ["float", value.hex()]
|
||||
if isinstance(value, str):
|
||||
return ["str", value]
|
||||
if isinstance(value, bytes):
|
||||
return ["bytes", value.hex()]
|
||||
if isinstance(value, torch.Tensor):
|
||||
if value.layout != torch.strided or value.is_quantized:
|
||||
raise TypeError("Prefix-cache identity requires a dense, non-quantized tensor")
|
||||
tensor = value.detach().cpu().contiguous()
|
||||
# A byte view also covers dtypes NumPy cannot represent directly.
|
||||
data = tensor.reshape(-1).view(torch.uint8).numpy()
|
||||
return ["tensor", str(tensor.dtype), list(tensor.shape), MultiModalHasher.hash_kwargs("sha256", data=data)]
|
||||
if isinstance(value, np.ndarray):
|
||||
if value.dtype.hasobject or value.dtype.fields is not None:
|
||||
raise TypeError("Prefix-cache identity does not support object or structured arrays")
|
||||
return [
|
||||
"array",
|
||||
value.dtype.str,
|
||||
list(value.shape),
|
||||
MultiModalHasher.hash_kwargs("sha256", data=np.ascontiguousarray(value)),
|
||||
]
|
||||
if isinstance(value, dict):
|
||||
if any(not isinstance(key, str) for key in value):
|
||||
raise TypeError("Prefix-cache identity dictionaries require string keys")
|
||||
return ["dict", [[key, _normalize_identity(value[key])] for key in sorted(value)]]
|
||||
if isinstance(value, list | tuple):
|
||||
return ["sequence", [_normalize_identity(item) for item in value]]
|
||||
raise TypeError(f"Unsupported prefix-cache identity value: {type(value).__name__}")
|
||||
|
||||
|
||||
def hash_prefix_cache_value(value: object) -> bytes:
|
||||
"""Deterministic, type-framed identity using vLLM's multimodal hasher."""
|
||||
|
||||
encoded = json.dumps(_normalize_identity(value), ensure_ascii=True, separators=(",", ":"))
|
||||
return bytes.fromhex(MultiModalHasher.hash_kwargs("sha256", identity=encoded))
|
||||
|
||||
|
||||
def get_cache_namespace(model_namespace: str, sampling: OmniDiffusionSamplingParams) -> str:
|
||||
"""Isolate model semantics and the adapter actually activated by Worker.
|
||||
|
||||
Native LoRA block keys use lora_name, but diffusion also supports a scale
|
||||
and identifies loaded adapters by lora_int_id. Keep that difference here.
|
||||
Seeds are NOT global identity inputs: models attach random state only to
|
||||
the multimodal features whose computation depends on it.
|
||||
"""
|
||||
|
||||
if not isinstance(model_namespace, str) or not model_namespace:
|
||||
raise ValueError("Model cache namespace must be non-empty")
|
||||
lora = sampling.lora_request
|
||||
adapter = None if lora is None else (lora.lora_int_id, float(sampling.lora_scale))
|
||||
return hash_prefix_cache_value(("diffusion-prefix-identity-v2", model_namespace, adapter)).hex()
|
||||
@@ -1,11 +1,13 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheManager
|
||||
from vllm.utils.hashing import get_hash_fn_by_name
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheBlocks, KVCacheManager
|
||||
from vllm.v1.core.kv_cache_utils import KVCacheBlock
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
|
||||
from vllm_omni.core.sched.utils import free_kv_blocks_in_physical_order
|
||||
@@ -31,6 +33,8 @@ class DiffusionKVCacheManager:
|
||||
scheduler_block_size: int,
|
||||
hash_block_size: int,
|
||||
max_in_flight_tokens: int | None = None,
|
||||
enable_prefix_caching: bool = False,
|
||||
prefix_caching_hash_algo: str = "sha256",
|
||||
) -> None:
|
||||
if max_model_len <= 0:
|
||||
raise ValueError(f"max_model_len must be positive, got {max_model_len}")
|
||||
@@ -40,15 +44,18 @@ class DiffusionKVCacheManager:
|
||||
raise ValueError("scheduler_block_size and hash_block_size must be positive")
|
||||
if max_in_flight_tokens is not None and max_in_flight_tokens <= 0:
|
||||
raise ValueError(f"max_in_flight_tokens must be positive when provided, got {max_in_flight_tokens}")
|
||||
self._hash_function = get_hash_fn_by_name(prefix_caching_hash_algo)
|
||||
self.native_manager = KVCacheManager(
|
||||
kv_cache_config=kv_cache_config,
|
||||
max_model_len=max_model_len,
|
||||
scheduler_block_size=scheduler_block_size,
|
||||
hash_block_size=hash_block_size,
|
||||
max_in_flight_tokens=max_in_flight_tokens,
|
||||
enable_caching=False,
|
||||
enable_caching=enable_prefix_caching,
|
||||
)
|
||||
self.max_model_len = max_model_len
|
||||
self.hash_block_size = hash_block_size
|
||||
self.enable_prefix_caching = enable_prefix_caching
|
||||
# Native vLLM may reserve a null block, so an idle BlockPool does not
|
||||
# necessarily report ``kv_cache_config.num_blocks`` free blocks.
|
||||
self._empty_pool_num_free_blocks = self.native_manager.block_pool.get_num_free_blocks()
|
||||
@@ -57,31 +64,87 @@ class DiffusionKVCacheManager:
|
||||
self._internal_request_ids: set[str] = set()
|
||||
self._next_allocation_generation = 1
|
||||
|
||||
def _get_empty_pool_required_blocks(self, requests: Sequence[DiffusionKVRequest]) -> int:
|
||||
"""Return the native full-sequence reservation for one public request.
|
||||
def _prepare_block_hashes(self, requests: Sequence[DiffusionKVRequest]) -> None:
|
||||
if not self.enable_prefix_caching:
|
||||
return
|
||||
for request in requests:
|
||||
request.build_block_hashes(self.hash_block_size, self._hash_function)
|
||||
|
||||
vLLM performs this calculation inside ``allocate_slots`` when
|
||||
``full_sequence_must_fit`` is enabled. Diffusion CFG adds one semantic
|
||||
requirement that native requests do not have: every execution sequence
|
||||
must fit atomically. Sum the native per-sequence result so a request
|
||||
that can never fit is rejected independently of current pool usage.
|
||||
def _clip_computed_blocks(
|
||||
self,
|
||||
blocks: KVCacheBlocks,
|
||||
*,
|
||||
hit_len: int,
|
||||
cached_prefix_len: int,
|
||||
) -> KVCacheBlocks:
|
||||
if cached_prefix_len == hit_len:
|
||||
return blocks
|
||||
clipped_groups: list[list[KVCacheBlock]] = []
|
||||
for group_blocks, cache_group in zip(
|
||||
blocks.blocks,
|
||||
self.native_manager.kv_cache_config.kv_cache_groups,
|
||||
strict=True,
|
||||
):
|
||||
block_size = int(cache_group.kv_cache_spec.block_size)
|
||||
if cached_prefix_len % block_size:
|
||||
raise RuntimeError(
|
||||
"Diffusion prefix hit is not aligned to a native cache group: "
|
||||
f"cached_prefix_len={cached_prefix_len}, block_size={block_size}"
|
||||
)
|
||||
keep_blocks = cached_prefix_len // block_size
|
||||
if keep_blocks > len(group_blocks):
|
||||
raise RuntimeError(
|
||||
"Native prefix lookup returned too few blocks for the common CFG boundary: "
|
||||
f"required={keep_blocks}, returned={len(group_blocks)}"
|
||||
)
|
||||
clipped_groups.append(list(group_blocks[:keep_blocks]))
|
||||
return self.native_manager.create_kv_cache_blocks(tuple(clipped_groups))
|
||||
|
||||
@staticmethod
|
||||
def _unique_blocks(block_sets: Sequence[KVCacheBlocks]) -> list[KVCacheBlock]:
|
||||
unique: dict[int, KVCacheBlock] = {}
|
||||
for blocks in block_sets:
|
||||
for group in blocks.blocks:
|
||||
for block in group:
|
||||
if not block.is_null:
|
||||
unique[id(block)] = block
|
||||
return list(unique.values())
|
||||
|
||||
def _get_empty_pool_required_blocks(
|
||||
self,
|
||||
requests: Sequence[DiffusionKVRequest],
|
||||
computed_blocks: Sequence[KVCacheBlocks],
|
||||
cached_prefix_len: int,
|
||||
) -> int:
|
||||
"""Return the minimum physical capacity for one public request.
|
||||
|
||||
Cached CFG rows may point to the same physical blocks. Count those hit
|
||||
blocks once, then ask the native coordinator how many additional blocks
|
||||
each row needs for its suffix. This gives an admission bound that still
|
||||
works for warm requests and does not over-count shared prefix pages.
|
||||
"""
|
||||
|
||||
coordinator = self.native_manager.coordinator
|
||||
empty_blocks = self.native_manager.empty_kv_cache_blocks.blocks
|
||||
return sum(
|
||||
coordinator.get_num_blocks_to_allocate(
|
||||
unique_hit_blocks = len(self._unique_blocks(computed_blocks))
|
||||
suffix_blocks = 0
|
||||
for request, hit in zip(requests, computed_blocks, strict=True):
|
||||
native_required = coordinator.get_num_blocks_to_allocate(
|
||||
request_id=request.request_id,
|
||||
num_tokens=request.num_tokens,
|
||||
new_computed_blocks=empty_blocks,
|
||||
new_computed_blocks=hit.blocks,
|
||||
num_encoder_tokens=0,
|
||||
total_computed_tokens=0,
|
||||
num_local_computed_tokens=0,
|
||||
total_computed_tokens=cached_prefix_len,
|
||||
num_local_computed_tokens=cached_prefix_len,
|
||||
num_tokens_main_model=request.num_tokens,
|
||||
apply_admission_cap=True,
|
||||
)
|
||||
for request in requests
|
||||
)
|
||||
evictable_hits = sum(block.ref_cnt == 0 and not block.is_null for group in hit.blocks for block in group)
|
||||
suffix_blocks += max(native_required - evictable_hits, 0)
|
||||
# ``get_num_blocks_to_allocate`` includes evictable hit pages because
|
||||
# the live free-capacity check must first remove them from the free
|
||||
# queue. They are already part of the pool's physical capacity, so do
|
||||
# not count them a second time in this empty-pool bound.
|
||||
return unique_hit_blocks + suffix_blocks
|
||||
|
||||
def has_request(self, public_request_id: str) -> bool:
|
||||
return public_request_id in self._requests
|
||||
@@ -123,7 +186,25 @@ class DiffusionKVCacheManager:
|
||||
f"seq_len={request.seq_len}, max_model_len={self.max_model_len}"
|
||||
)
|
||||
|
||||
required_blocks = self._get_empty_pool_required_blocks(requests)
|
||||
self._prepare_block_hashes(requests)
|
||||
hit_results = [self.native_manager.get_computed_blocks(request) for request in requests]
|
||||
cached_prefix_len = min(result[1] for result in hit_results)
|
||||
computed_blocks = [
|
||||
self._clip_computed_blocks(
|
||||
result[0],
|
||||
hit_len=result[1],
|
||||
cached_prefix_len=cached_prefix_len,
|
||||
)
|
||||
for result in hit_results
|
||||
]
|
||||
for request, result in zip(requests, hit_results, strict=True):
|
||||
request.shared_prefix_boundary = result[2]
|
||||
|
||||
required_blocks = self._get_empty_pool_required_blocks(
|
||||
requests,
|
||||
computed_blocks,
|
||||
cached_prefix_len,
|
||||
)
|
||||
if required_blocks > self._empty_pool_num_free_blocks:
|
||||
raise DiffusionKVAdmissionError(
|
||||
f"Diffusion KV request {public_request_id!r} cannot fit even when the block pool is empty: "
|
||||
@@ -133,11 +214,20 @@ class DiffusionKVCacheManager:
|
||||
|
||||
allocated: list[DiffusionKVRequest] = []
|
||||
sequence_metadata: list[DiffusionKVSequenceMetadata] = []
|
||||
pinned_blocks = self._unique_blocks(computed_blocks)
|
||||
if pinned_blocks:
|
||||
# All CFG lookups happen before allocation so they can share one
|
||||
# execution boundary. Pin every distinct hit until each native
|
||||
# allocation has taken its own reference; otherwise an earlier
|
||||
# branch's miss allocation could evict a later branch's hit.
|
||||
self.native_manager.block_pool.touch(pinned_blocks)
|
||||
try:
|
||||
for request in requests:
|
||||
for request, request_computed_blocks in zip(requests, computed_blocks, strict=True):
|
||||
blocks = self.native_manager.allocate_slots(
|
||||
request,
|
||||
num_new_tokens=request.seq_len,
|
||||
num_new_tokens=request.seq_len - cached_prefix_len,
|
||||
num_new_computed_tokens=cached_prefix_len,
|
||||
new_computed_blocks=request_computed_blocks,
|
||||
delay_cache_blocks=True,
|
||||
full_sequence_must_fit=True,
|
||||
)
|
||||
@@ -146,18 +236,23 @@ class DiffusionKVCacheManager:
|
||||
allocated.clear()
|
||||
return None
|
||||
allocated.append(request)
|
||||
request.num_computed_tokens = cached_prefix_len
|
||||
sequence_metadata.append(
|
||||
DiffusionKVSequenceMetadata(
|
||||
sequence_id=request.sequence_id,
|
||||
prefix_len=request.prefix_len,
|
||||
target_len=request.target_len,
|
||||
seq_len=request.seq_len,
|
||||
block_ids=blocks.get_block_ids(),
|
||||
block_ids=self.native_manager.get_block_ids(request.request_id),
|
||||
cached_prefix_len=cached_prefix_len,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
self._rollback(allocated)
|
||||
raise
|
||||
finally:
|
||||
if pinned_blocks:
|
||||
self.native_manager.block_pool.free_blocks(reversed(pinned_blocks))
|
||||
|
||||
metadata = DiffusionKVMetadata(
|
||||
request_id=public_request_id,
|
||||
@@ -170,6 +265,23 @@ class DiffusionKVCacheManager:
|
||||
self._internal_request_ids.update(internal_ids)
|
||||
return metadata
|
||||
|
||||
def publish_request(self, public_request_id: str) -> None:
|
||||
"""Publish successfully materialized, block-aligned stable prefixes."""
|
||||
|
||||
if not self.enable_prefix_caching:
|
||||
return
|
||||
try:
|
||||
requests = self._requests[public_request_id]
|
||||
except KeyError as exc:
|
||||
raise KeyError(f"Diffusion KV request {public_request_id!r} is not allocated") from exc
|
||||
for request in requests:
|
||||
publishable_prefix_len = min(
|
||||
request.prefix_len // self.hash_block_size * self.hash_block_size,
|
||||
len(request.block_hashes) * self.hash_block_size,
|
||||
)
|
||||
if publishable_prefix_len > 0:
|
||||
self.native_manager.cache_blocks(request, publishable_prefix_len)
|
||||
|
||||
def get_metadata(self, public_request_id: str) -> DiffusionKVMetadata:
|
||||
try:
|
||||
return self._metadata[public_request_id]
|
||||
@@ -181,6 +293,8 @@ class DiffusionKVCacheManager:
|
||||
self._metadata.pop(public_request_id, None)
|
||||
for request in reversed(requests):
|
||||
free_kv_blocks_in_physical_order(self.native_manager, request)
|
||||
request.num_computed_tokens = 0
|
||||
request.shared_prefix_boundary = 0
|
||||
self._internal_request_ids.discard(request.request_id)
|
||||
|
||||
def close(self) -> None:
|
||||
@@ -190,3 +304,5 @@ class DiffusionKVCacheManager:
|
||||
def _rollback(self, requests: Sequence[DiffusionKVRequest]) -> None:
|
||||
for request in reversed(requests):
|
||||
free_kv_blocks_in_physical_order(self.native_manager, request)
|
||||
request.num_computed_tokens = 0
|
||||
request.shared_prefix_boundary = 0
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Scheduler-to-Worker allocation payloads for diffusion KV.
|
||||
|
||||
@@ -40,6 +40,7 @@ class DiffusionKVSequenceMetadata:
|
||||
target_len: int
|
||||
seq_len: int
|
||||
block_ids: tuple[list[int], ...]
|
||||
cached_prefix_len: int = 0
|
||||
context_ids: tuple[str, ...] = ()
|
||||
num_computed_tokens: int = 0
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
@@ -154,6 +154,7 @@ class DiffusionKVModelRunnerBackend:
|
||||
|
||||
if attention_geometry is not None:
|
||||
num_heads, num_kv_heads, head_size = attention_geometry
|
||||
assert callable(set_attention_geometry)
|
||||
try:
|
||||
set_attention_geometry(
|
||||
num_heads=num_heads,
|
||||
@@ -396,6 +397,15 @@ class DiffusionKVModelRunnerBackend:
|
||||
for sequence in metadata.sequences:
|
||||
if type(sequence.sequence_id) is not int or sequence.sequence_id < 0:
|
||||
raise ValueError(f"Diffusion KV sequence_id must be a non-negative integer: {sequence.sequence_id!r}")
|
||||
if (
|
||||
type(sequence.cached_prefix_len) is not int
|
||||
or sequence.cached_prefix_len < 0
|
||||
or sequence.cached_prefix_len > sequence.prefix_len
|
||||
):
|
||||
raise ValueError(
|
||||
"Diffusion KV cached_prefix_len must lie within the stable prefix: "
|
||||
f"cached={sequence.cached_prefix_len!r}, prefix={sequence.prefix_len!r}"
|
||||
)
|
||||
sequence_identity = (metadata.request_id, sequence.sequence_id, None)
|
||||
sequence_install = self._validate_row(
|
||||
identity=sequence_identity,
|
||||
@@ -410,6 +420,7 @@ class DiffusionKVModelRunnerBackend:
|
||||
sequence.prefix_len,
|
||||
sequence.target_len,
|
||||
sequence.seq_len,
|
||||
sequence.cached_prefix_len,
|
||||
sequence_install.block_ids,
|
||||
sequence.context_ids,
|
||||
)
|
||||
@@ -593,7 +604,7 @@ class DiffusionKVModelRunnerBackend:
|
||||
)
|
||||
raise RuntimeError(f"Diffusion KV request state is missing logical length for {identity!r}")
|
||||
|
||||
def remove_diffusion_kv_requests(self, request_ids: list[str | tuple[str, int]]) -> int:
|
||||
def remove_diffusion_kv_requests(self, request_ids: Sequence[str | tuple[str, int]]) -> int:
|
||||
"""Retire Worker rows without logically freeing Scheduler-owned blocks."""
|
||||
if (
|
||||
not is_scheduler_paged_kv_mode(
|
||||
|
||||
@@ -84,14 +84,18 @@ class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
|
||||
attention_config = vllm_config.attention_config
|
||||
previous_backend = attention_config.backend
|
||||
previous_backend_per_kind = attention_config.backend_per_kind
|
||||
previous_pcp_size = vllm_config.parallel_config.prefill_context_parallel_size
|
||||
parallel_config = vllm_config.parallel_config
|
||||
previous_pcp_size = parallel_config.prefill_context_parallel_size
|
||||
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 = {}
|
||||
vllm_config.parallel_config.prefill_context_parallel_size = 1
|
||||
# Diffusion maps SP to PCP for MoE, not for attention. Omni's
|
||||
# Ulysses hooks gather the sequence and shard heads before this
|
||||
# native kernel, so backend selection must not request PCP.
|
||||
parallel_config.prefill_context_parallel_size = 1
|
||||
with set_current_vllm_config(vllm_config):
|
||||
attn_backend = get_attn_backend(
|
||||
head_size=spec.head_size,
|
||||
@@ -103,7 +107,7 @@ class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
|
||||
finally:
|
||||
attention_config.backend = previous_backend
|
||||
attention_config.backend_per_kind = previous_backend_per_kind
|
||||
vllm_config.parallel_config.prefill_context_parallel_size = previous_pcp_size
|
||||
parallel_config.prefill_context_parallel_size = previous_pcp_size
|
||||
attn_backend = current_omni_platform.get_diffusion_paged_kv_attn_backend(
|
||||
attn_backend,
|
||||
ulysses_degree=ulysses_degree,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from vllm.v1.core.kv_cache_utils import BlockHash
|
||||
from vllm.multimodal.inputs import MultiModalFeatureSpec
|
||||
from vllm.v1.core.kv_cache_utils import BlockHash, generate_block_hash_extra_keys, hash_block_tokens
|
||||
from vllm.v1.request import RequestStatus
|
||||
|
||||
|
||||
@@ -51,8 +52,10 @@ class DiffusionKVRequest:
|
||||
An empty ``block_hashes`` sequence means the prefix has no canonical cache
|
||||
identity yet. Such a request may use native request-local page allocation,
|
||||
but consumers must not publish it through ``KVCacheManager.cache_blocks``.
|
||||
Prefix publication becomes valid only after model preprocessing supplies
|
||||
one canonical hash for every cacheable full block.
|
||||
With prefix caching enabled, Engine invokes model input preparation to
|
||||
attach token IDs and native multimodal identities / positions. The Manager
|
||||
builds one canonical hash for every cacheable full block before lookup;
|
||||
publication requires those hashes and successfully materialized KV.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -64,6 +67,9 @@ class DiffusionKVRequest:
|
||||
target_len: int,
|
||||
seq_len: int,
|
||||
block_hashes: Sequence[BlockHash] = (),
|
||||
cache_token_ids: Sequence[int] = (),
|
||||
mm_features: Sequence[MultiModalFeatureSpec] = (),
|
||||
cache_namespace: str = "diffusion-primary-v1",
|
||||
kv_contexts: Sequence[DiffusionKVContext] = (),
|
||||
kv_transfer_params: dict[str, Any] | None = None,
|
||||
prompt_token_ids: list[int] | None = None,
|
||||
@@ -83,6 +89,15 @@ class DiffusionKVRequest:
|
||||
"prefix_len + target_len must not exceed seq_len: "
|
||||
f"prefix_len={prefix_len}, target_len={target_len}, seq_len={seq_len}"
|
||||
)
|
||||
token_ids = tuple(cache_token_ids)
|
||||
if token_ids and len(token_ids) < prefix_len:
|
||||
raise ValueError(
|
||||
f"cache_token_ids must cover the reusable prefix: tokens={len(token_ids)}, prefix_len={prefix_len}"
|
||||
)
|
||||
if any(type(token_id) is not int for token_id in token_ids):
|
||||
raise TypeError("cache_token_ids must contain only integers")
|
||||
if not isinstance(cache_namespace, str) or not cache_namespace:
|
||||
raise ValueError("cache_namespace must be a non-empty string")
|
||||
|
||||
contexts = tuple(kv_contexts)
|
||||
if any(not isinstance(context, DiffusionKVContext) for context in contexts):
|
||||
@@ -96,6 +111,15 @@ class DiffusionKVRequest:
|
||||
self.prefix_len = prefix_len
|
||||
self.target_len = target_len
|
||||
self.kv_contexts = contexts
|
||||
self.cache_token_ids = token_ids
|
||||
self.mm_features = list(mm_features)
|
||||
self.cache_namespace = cache_namespace
|
||||
# Native generate_block_hash_extra_keys consumes this Request surface.
|
||||
# Diffusion adapter ID + scale are already isolated in cache_namespace;
|
||||
# there are no separate LoRA-name, salt, or prompt-embedding inputs.
|
||||
self.lora_request = None
|
||||
self.cache_salt = None
|
||||
self.prompt_embeds = None
|
||||
|
||||
# Native vLLM Request surface. Keep this list intentionally small and
|
||||
# cover it with real-KVCacheManager conformance tests.
|
||||
@@ -115,6 +139,70 @@ class DiffusionKVRequest:
|
||||
self.kv_transfer_params = kv_transfer_params
|
||||
self.prompt_token_ids = prompt_token_ids
|
||||
|
||||
def _validate_mm_features(self) -> None:
|
||||
# Native block traversal expects sorted, non-overlapping placeholders.
|
||||
# Do not silently accept nested/overlapping ranges: the traversal can
|
||||
# skip a feature and incorrectly reuse KV. Combine such dependencies
|
||||
# in the feature identifier instead.
|
||||
previous_end = 0
|
||||
for feature in self.mm_features:
|
||||
if not isinstance(feature, MultiModalFeatureSpec):
|
||||
raise TypeError("mm_features must contain native MultiModalFeatureSpec values")
|
||||
position = feature.mm_position
|
||||
end = position.offset + position.length
|
||||
if position.offset < previous_end or position.length <= 0 or end > self.seq_len:
|
||||
raise ValueError("mm_features must have valid sorted, non-overlapping positions within seq_len")
|
||||
if not isinstance(feature.identifier, str) or not feature.identifier:
|
||||
raise ValueError("mm_features must have non-empty content identifiers")
|
||||
previous_end = end
|
||||
|
||||
def build_block_hashes(
|
||||
self,
|
||||
hash_block_size: int,
|
||||
hash_function: Callable[[object], bytes],
|
||||
) -> None:
|
||||
"""Build native chained hashes for complete reusable-prefix blocks."""
|
||||
|
||||
if self.block_hashes:
|
||||
self.skip_reading_prefix_cache = False
|
||||
return
|
||||
if hash_block_size <= 0:
|
||||
raise ValueError(f"hash_block_size must be positive, got {hash_block_size}")
|
||||
num_cache_tokens = self.prefix_len // hash_block_size * hash_block_size
|
||||
if num_cache_tokens == 0 or not self.cache_token_ids:
|
||||
return
|
||||
if len(self.cache_token_ids) < num_cache_tokens:
|
||||
raise ValueError(
|
||||
"cache_token_ids do not cover the block-aligned reusable prefix: "
|
||||
f"tokens={len(self.cache_token_ids)}, required={num_cache_tokens}"
|
||||
)
|
||||
self._validate_mm_features()
|
||||
|
||||
# Keep the first hash deterministic without mutating vLLM's process-wide
|
||||
# ``NONE_HASH``. A diffusion manager can be created more than once in
|
||||
# one process (for example in tests or worker reinitialization); using
|
||||
# the global random root would make requests built by different manager
|
||||
# instances impossible to match.
|
||||
parent_hash: BlockHash | None = BlockHash(hash_function(("diffusion-prefix-root-v1", self.cache_namespace)))
|
||||
hashes: list[BlockHash] = []
|
||||
mm_index = 0
|
||||
for start in range(0, num_cache_tokens, hash_block_size):
|
||||
end = start + hash_block_size
|
||||
# Reuse upstream MM intersection/offset logic directly. Keep our
|
||||
# stable-prefix bound: native get_request_block_hasher hashes the
|
||||
# entire token axis, including diffusion's mutable target region.
|
||||
extra_keys, mm_index = generate_block_hash_extra_keys(self, start, end, mm_index)
|
||||
parent_hash = hash_block_tokens(
|
||||
hash_function=hash_function,
|
||||
parent_block_hash=parent_hash,
|
||||
curr_block_token_ids=self.cache_token_ids[start:end],
|
||||
extra_keys=extra_keys,
|
||||
)
|
||||
hashes.append(parent_hash)
|
||||
|
||||
self.block_hashes = hashes
|
||||
self.skip_reading_prefix_cache = not hashes
|
||||
|
||||
@property
|
||||
def seq_len(self) -> int:
|
||||
"""Complete first-step sequence length and allocation boundary."""
|
||||
|
||||
Reference in new issue
Block a user