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>
781 lines
30 KiB
Python
781 lines
30 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from contextlib import nullcontext
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
from vllm.v1.kv_cache_interface import FullAttentionSpec
|
|
from vllm.v1.worker import block_table as native_block_table
|
|
|
|
from vllm_omni.diffusion.diffusion_kv import model_runner_backend as model_runner_backend_module
|
|
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
|
|
from vllm_omni.diffusion.diffusion_kv.metadata import (
|
|
DiffusionKVContextMetadata,
|
|
DiffusionKVMetadata,
|
|
DiffusionKVSequenceMetadata,
|
|
)
|
|
from vllm_omni.diffusion.diffusion_kv.model_runner_backend import DiffusionKVModelRunnerBackend
|
|
from vllm_omni.diffusion.sched.interface import CachedRequestData, DiffusionSchedulerOutput
|
|
from vllm_omni.diffusion.worker.diffusion_model_runner import DiffusionModelRunner
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
|
|
|
|
|
def _registration_backend(parallel_config: object):
|
|
model_config = SimpleNamespace(set_attention_geometry=Mock())
|
|
vllm_config = SimpleNamespace(
|
|
attention_config=SimpleNamespace(use_non_causal=False),
|
|
compilation_config=SimpleNamespace(static_forward_context={}),
|
|
model_config=model_config,
|
|
)
|
|
backend = DiffusionKVModelRunnerBackend(
|
|
vllm_config=vllm_config,
|
|
od_config=SimpleNamespace(parallel_config=parallel_config),
|
|
device=torch.device("cpu"),
|
|
)
|
|
return backend, vllm_config, model_config
|
|
|
|
|
|
def _parallel_config(**overrides):
|
|
values = dict(ulysses_degree=1, ring_degree=1, allgather_degree=1, ulysses_mode="strict")
|
|
values.update(overrides)
|
|
return SimpleNamespace(**values)
|
|
|
|
|
|
def test_cache_layer_registration_installs_adapters_with_ulysses_local_geometry(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
backend, vllm_config, model_config = _registration_backend(_parallel_config(ulysses_degree=2))
|
|
layer = SimpleNamespace(
|
|
num_heads=8,
|
|
softmax_scale=0.125,
|
|
skip_sequence_parallel=False,
|
|
attn_backend=SimpleNamespace(supports_paged_kv=True, get_name=lambda: "FLASH_ATTN"),
|
|
)
|
|
unrelated = object()
|
|
vllm_config.compilation_config.static_forward_context.update({"layer-0": layer, "unrelated": unrelated})
|
|
spec = FullAttentionSpec(
|
|
block_size=16,
|
|
num_kv_heads=4,
|
|
head_size=8,
|
|
dtype=torch.float16,
|
|
non_causal=True,
|
|
)
|
|
constructor_calls = []
|
|
|
|
class FakeLayerAdapter:
|
|
def __init__(self, *, layer_name, layer, spec, ulysses_degree, **_kwargs) -> None:
|
|
constructor_calls.append((layer_name, layer, spec, ulysses_degree))
|
|
self.num_heads = layer.num_heads // ulysses_degree
|
|
self.num_kv_heads = spec.num_kv_heads // ulysses_degree
|
|
self.head_size = spec.head_size
|
|
self.spec = replace(spec, num_kv_heads=self.num_kv_heads)
|
|
|
|
monkeypatch.setattr(model_runner_backend_module, "DiffusionPagedAttentionLayerAdapter", FakeLayerAdapter)
|
|
monkeypatch.setattr(model_runner_backend_module, "set_current_vllm_config", lambda _config: nullcontext())
|
|
|
|
specs = backend.register_kv_cache_layers({"layer-0": (layer, spec)})
|
|
|
|
installed = vllm_config.compilation_config.static_forward_context["layer-0"]
|
|
assert isinstance(installed, FakeLayerAdapter)
|
|
assert vllm_config.compilation_config.static_forward_context["unrelated"] is unrelated
|
|
assert specs["layer-0"].num_kv_heads == 2
|
|
assert constructor_calls == [("layer-0", layer, spec, 2)]
|
|
model_config.set_attention_geometry.assert_called_once_with(
|
|
num_heads=4,
|
|
num_kv_heads=2,
|
|
head_size=8,
|
|
)
|
|
assert vllm_config.attention_config.use_non_causal is True
|
|
|
|
|
|
def test_cache_layer_registration_rejects_backend_without_paged_support() -> None:
|
|
backend, _, _ = _registration_backend(_parallel_config())
|
|
layer = SimpleNamespace(
|
|
attn_backend=SimpleNamespace(supports_paged_kv=False, get_name=lambda: "SDPA"),
|
|
)
|
|
spec = FullAttentionSpec(
|
|
block_size=16,
|
|
num_kv_heads=4,
|
|
head_size=8,
|
|
dtype=torch.float16,
|
|
)
|
|
|
|
with pytest.raises(NotImplementedError, match="layer-0.*paged support.*SDPA"):
|
|
backend.register_kv_cache_layers({"layer-0": (layer, spec)})
|
|
|
|
|
|
def test_cache_layer_registration_resolves_platform_paged_hooks(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
backend, _, _ = _registration_backend(_parallel_config())
|
|
|
|
def unsupported_block_tables():
|
|
raise NotImplementedError("platform has no paged BlockTables")
|
|
|
|
monkeypatch.setattr(
|
|
model_runner_backend_module.current_omni_platform,
|
|
"get_diffusion_kv_block_tables_cls",
|
|
unsupported_block_tables,
|
|
)
|
|
|
|
with pytest.raises(NotImplementedError, match="no paged BlockTables"):
|
|
backend.register_kv_cache_layers({"layer-0": (object(), Mock(spec=FullAttentionSpec))})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("parallel_config", "message"),
|
|
[
|
|
(_parallel_config(ring_degree=2), "Ring"),
|
|
(_parallel_config(allgather_degree=2), "AllGather-KV"),
|
|
(_parallel_config(ulysses_degree=2, ulysses_mode="advanced_uaa"), "strict Ulysses"),
|
|
],
|
|
)
|
|
def test_cache_layer_registration_rejects_unsupported_sp_modes(parallel_config, message: str) -> None:
|
|
backend, _, _ = _registration_backend(parallel_config)
|
|
|
|
with pytest.raises(NotImplementedError, match=message):
|
|
backend._get_paged_attention_ulysses_degree(SimpleNamespace(skip_sequence_parallel=False))
|
|
|
|
|
|
def test_cache_layer_registration_ignores_sp_for_opted_out_layer() -> None:
|
|
backend, _, _ = _registration_backend(
|
|
_parallel_config(ulysses_degree=2, ring_degree=2, ulysses_mode="advanced_uaa")
|
|
)
|
|
|
|
assert backend._get_paged_attention_ulysses_degree(SimpleNamespace(skip_sequence_parallel=True)) == 1
|
|
|
|
|
|
class _FakeSpec:
|
|
def __init__(self, block_size: int) -> None:
|
|
self.block_size = block_size
|
|
self.capacity_calls: list[tuple[object, int]] = []
|
|
|
|
def max_num_blocks_per_req(self, vllm_config: object, max_len: int) -> int:
|
|
self.capacity_calls.append((vllm_config, max_len))
|
|
return math.ceil(max_len / self.block_size)
|
|
|
|
|
|
class _FakeStagedTable:
|
|
def __init__(self) -> None:
|
|
self.staged: list[tuple[int, tuple[int, ...]]] = []
|
|
self.clear_calls = 0
|
|
|
|
def clear_staged_writes(self) -> None:
|
|
self.staged.clear()
|
|
self.clear_calls += 1
|
|
|
|
|
|
class _FakeNumBlocks:
|
|
def __init__(self, num_groups: int, max_num_reqs: int) -> None:
|
|
self.np = np.zeros((num_groups, max_num_reqs), dtype=np.int32)
|
|
self.copy_calls = 0
|
|
|
|
def copy_to_uva(self) -> None:
|
|
self.copy_calls += 1
|
|
|
|
|
|
class _FakeBlockTables:
|
|
instances: list[_FakeBlockTables] = []
|
|
|
|
def __init__(self, **kwargs: object) -> None:
|
|
self.kwargs = kwargs
|
|
self.block_sizes = list(kwargs["block_sizes"])
|
|
self.max_num_reqs = int(kwargs["max_num_reqs"])
|
|
self.max_num_batched_tokens = int(kwargs["max_num_batched_tokens"])
|
|
self.cp_size = int(kwargs["cp_size"])
|
|
self.cp_rank = int(kwargs["cp_rank"])
|
|
self.cp_interleave = int(kwargs["cp_interleave"])
|
|
self.blocks_per_kv_block = [1] * len(self.block_sizes)
|
|
self.block_tables = [_FakeStagedTable() for _ in self.block_sizes]
|
|
self.num_blocks = _FakeNumBlocks(len(self.block_sizes), self.max_num_reqs)
|
|
self.append_calls: list[tuple[int, tuple[list[int], ...], bool]] = []
|
|
self.apply_calls = 0
|
|
self.layout_refreshes = 0
|
|
self.fail_on_append_call: int | None = None
|
|
self.fail_apply = False
|
|
self.committed: list[dict[int, tuple[int, ...]]] = [dict() for _ in self.block_sizes]
|
|
self.__class__.instances.append(self)
|
|
|
|
def append_block_ids(
|
|
self,
|
|
req_index: int,
|
|
new_block_ids: tuple[list[int], ...],
|
|
overwrite: bool,
|
|
) -> None:
|
|
call_number = len(self.append_calls) + 1
|
|
if self.fail_on_append_call == call_number:
|
|
raise RuntimeError("fake append failure")
|
|
copied_ids = tuple(list(group_ids) for group_ids in new_block_ids)
|
|
self.append_calls.append((req_index, copied_ids, overwrite))
|
|
for group_index, group_ids in enumerate(copied_ids):
|
|
self.num_blocks.np[group_index, req_index] = len(group_ids)
|
|
if group_ids:
|
|
self.block_tables[group_index].staged.append((req_index, tuple(group_ids)))
|
|
|
|
def apply_staged_writes(self) -> None:
|
|
self.apply_calls += 1
|
|
if self.fail_apply:
|
|
raise RuntimeError("fake apply failure")
|
|
for group_index, group_table in enumerate(self.block_tables):
|
|
for row, group_ids in group_table.staged:
|
|
self.committed[group_index][row] = group_ids
|
|
group_table.staged.clear()
|
|
self.num_blocks.copy_to_uva()
|
|
|
|
def init_block_table_layout_tensors(self) -> None:
|
|
self.layout_refreshes += 1
|
|
|
|
|
|
def _metadata(
|
|
request_id: str = "req-0",
|
|
generation: int = 1,
|
|
*,
|
|
seq_len: int = 9,
|
|
sequence_block_ids: tuple[list[int], ...] = ([1, 2, 3], [4, 5]),
|
|
contexts: tuple[DiffusionKVContextMetadata, ...] | None = None,
|
|
) -> DiffusionKVMetadata:
|
|
if contexts is None:
|
|
contexts = (
|
|
DiffusionKVContextMetadata(
|
|
context_id="text",
|
|
cache_role="cross_attention",
|
|
num_tokens=5,
|
|
block_ids=([6, 7], [8]),
|
|
),
|
|
)
|
|
return DiffusionKVMetadata(
|
|
request_id=request_id,
|
|
allocation_generation=generation,
|
|
sequences=(
|
|
DiffusionKVSequenceMetadata(
|
|
sequence_id=0,
|
|
prefix_len=4,
|
|
target_len=5,
|
|
seq_len=seq_len,
|
|
block_ids=sequence_block_ids,
|
|
context_ids=tuple(context.context_id for context in contexts),
|
|
),
|
|
),
|
|
contexts=contexts,
|
|
)
|
|
|
|
|
|
def _install_native_modules(monkeypatch: pytest.MonkeyPatch, events: list[tuple]) -> None:
|
|
_FakeBlockTables.instances.clear()
|
|
|
|
def get_block_table_width(max_num_blocks, block_size):
|
|
events.append(("width", max_num_blocks, block_size))
|
|
return max_num_blocks + 1
|
|
|
|
def init_backend(kv_cache_config, vllm_config, device):
|
|
events.append(("backend", kv_cache_config, vllm_config, device))
|
|
group = SimpleNamespace(get_metadata_builder=lambda _index: SimpleNamespace(reorder_batch_threshold=None))
|
|
return [[group], [group]], "cg-support", [4, 8]
|
|
|
|
# vLLM 0.29 dropped attn_groups and cache_dtype from init_kv_cache: the
|
|
# layout now rides on the resolved CacheConfig.kv_cache_layout instead.
|
|
def init_cache(
|
|
runner_kv_caches,
|
|
forward_context,
|
|
kv_cache_config,
|
|
device,
|
|
kernel_block_sizes,
|
|
vllm_config,
|
|
):
|
|
events.append(("cache", kv_cache_config, kernel_block_sizes))
|
|
runner_kv_caches.extend(["cache-0", "cache-1"])
|
|
return {"layer-0": "tensor-0", "layer-1": "tensor-1"}
|
|
|
|
monkeypatch.setattr(model_runner_backend_module, "init_attn_backend", init_backend)
|
|
monkeypatch.setattr(model_runner_backend_module, "init_kv_cache", init_cache)
|
|
monkeypatch.setattr(
|
|
model_runner_backend_module.current_omni_platform,
|
|
"get_diffusion_kv_block_tables_cls",
|
|
lambda: _FakeBlockTables,
|
|
)
|
|
monkeypatch.setattr(native_block_table, "get_block_table_width", get_block_table_width, raising=False)
|
|
|
|
|
|
def _runner(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
*,
|
|
max_rows_per_request: int = 3,
|
|
max_num_seqs: int = 2,
|
|
max_len: int = 16,
|
|
max_num_batched_tokens: int = 32,
|
|
cp_size: int = 1,
|
|
cp_rank: int = 0,
|
|
cp_interleave: int = 1,
|
|
) -> tuple[DiffusionKVModelRunnerBackend, _FakeBlockTables, list[tuple]]:
|
|
events: list[tuple] = []
|
|
_install_native_modules(monkeypatch, events)
|
|
od_config = SimpleNamespace(
|
|
diffusion_kv_mode=DiffusionKVCacheMode.PAGED_SCHEDULER,
|
|
diffusion_kv_max_rows_per_request=max_rows_per_request,
|
|
max_num_seqs=max_num_seqs,
|
|
)
|
|
vllm_config = SimpleNamespace(
|
|
model_config=SimpleNamespace(max_model_len=max_len),
|
|
scheduler_config=SimpleNamespace(
|
|
max_num_batched_tokens=max_num_batched_tokens,
|
|
max_num_seqs=max_num_seqs,
|
|
),
|
|
parallel_config=SimpleNamespace(
|
|
decode_context_parallel_size=cp_size,
|
|
cp_kv_cache_interleave_size=cp_interleave,
|
|
),
|
|
compilation_config=SimpleNamespace(static_forward_context={"layer-0": object(), "layer-1": object()}),
|
|
cache_config=SimpleNamespace(cache_dtype="auto"),
|
|
)
|
|
monkeypatch.setattr(
|
|
model_runner_backend_module,
|
|
"get_dcp_group",
|
|
lambda: SimpleNamespace(rank_in_group=cp_rank),
|
|
)
|
|
backend = DiffusionKVModelRunnerBackend(
|
|
vllm_config=vllm_config,
|
|
od_config=od_config,
|
|
device="cuda:1",
|
|
)
|
|
backend._kv_cache_layer_adapters = {
|
|
"layer-0": SimpleNamespace(spec=SimpleNamespace(non_causal=False), kv_cache=None),
|
|
"layer-1": SimpleNamespace(spec=SimpleNamespace(non_causal=False), kv_cache=None),
|
|
}
|
|
|
|
config = SimpleNamespace(
|
|
num_blocks=32,
|
|
kv_cache_groups=[
|
|
SimpleNamespace(layer_names=["layer-0"], kv_cache_spec=_FakeSpec(4)),
|
|
SimpleNamespace(layer_names=["layer-1"], kv_cache_spec=_FakeSpec(8)),
|
|
],
|
|
)
|
|
backend.initialize_kv_cache(config)
|
|
return backend, _FakeBlockTables.instances[-1], events
|
|
|
|
|
|
def test_initialize_builds_native_block_tables_from_rank_local_config(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, events = _runner(monkeypatch)
|
|
|
|
assert events[:2] == [("width", 4, 4), ("width", 2, 8)]
|
|
assert [event[0] for event in events[2:]] == ["backend", "cache"]
|
|
assert block_tables.kwargs == {
|
|
"block_sizes": [4, 8],
|
|
"max_num_reqs": 6,
|
|
"max_num_batched_tokens": 32,
|
|
"max_num_blocks_per_group": [5, 3],
|
|
"device": "cuda:1",
|
|
"kernel_block_sizes": [4, 8],
|
|
"cp_size": 1,
|
|
"cp_rank": 0,
|
|
"cp_interleave": 1,
|
|
}
|
|
assert runner.block_tables is block_tables
|
|
assert runner._diffusion_kv_free_rows == [5, 4, 3, 2, 1, 0]
|
|
assert runner.kv_caches == ["cache-0", "cache-1"]
|
|
assert runner.vllm_config.scheduler_config.max_num_seqs == 6
|
|
|
|
|
|
def test_initialize_propagates_native_context_parallel_layout(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_, block_tables, _ = _runner(
|
|
monkeypatch,
|
|
cp_size=2,
|
|
cp_rank=1,
|
|
cp_interleave=4,
|
|
)
|
|
|
|
assert block_tables.kwargs["cp_size"] == 2
|
|
assert block_tables.kwargs["cp_rank"] == 1
|
|
assert block_tables.kwargs["cp_interleave"] == 4
|
|
|
|
|
|
def test_initialize_failure_does_not_publish_partial_cache_state(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
events: list[tuple] = []
|
|
_install_native_modules(monkeypatch, events)
|
|
successful_init_cache = model_runner_backend_module.init_kv_cache
|
|
vllm_config = SimpleNamespace(
|
|
model_config=SimpleNamespace(max_model_len=16),
|
|
scheduler_config=SimpleNamespace(max_num_batched_tokens=32, max_num_seqs=1),
|
|
parallel_config=SimpleNamespace(
|
|
decode_context_parallel_size=1,
|
|
cp_kv_cache_interleave_size=1,
|
|
),
|
|
compilation_config=SimpleNamespace(static_forward_context={"layer-0": object(), "layer-1": object()}),
|
|
cache_config=SimpleNamespace(cache_dtype="auto"),
|
|
)
|
|
backend = DiffusionKVModelRunnerBackend(
|
|
vllm_config=vllm_config,
|
|
od_config=SimpleNamespace(
|
|
diffusion_kv_mode=DiffusionKVCacheMode.PAGED_SCHEDULER,
|
|
diffusion_kv_max_rows_per_request=2,
|
|
),
|
|
device="cuda:1",
|
|
)
|
|
backend._kv_cache_layer_adapters = {
|
|
"layer-0": SimpleNamespace(spec=SimpleNamespace(non_causal=False), kv_cache="placeholder-0"),
|
|
"layer-1": SimpleNamespace(spec=SimpleNamespace(non_causal=False), kv_cache="placeholder-1"),
|
|
}
|
|
config = SimpleNamespace(
|
|
num_blocks=32,
|
|
kv_cache_groups=[
|
|
SimpleNamespace(layer_names=["layer-0"], kv_cache_spec=_FakeSpec(4)),
|
|
SimpleNamespace(layer_names=["layer-1"], kv_cache_spec=_FakeSpec(8)),
|
|
],
|
|
)
|
|
|
|
def fail_init_cache(runner_kv_caches, *_args, **_kwargs):
|
|
runner_kv_caches.append("partial-cache")
|
|
backend._kv_cache_layer_adapters["layer-0"].kv_cache = "partial-binding"
|
|
raise RuntimeError("cache binding failed")
|
|
|
|
monkeypatch.setattr(model_runner_backend_module, "init_kv_cache", fail_init_cache)
|
|
with pytest.raises(RuntimeError, match="cache binding failed"):
|
|
backend.initialize_kv_cache(config)
|
|
|
|
assert backend.kv_cache_config is None
|
|
assert backend.kv_caches == []
|
|
assert backend.block_tables is None
|
|
assert backend.paged_attention_adapter is None
|
|
assert backend._kv_cache_layer_adapters["layer-0"].kv_cache == "placeholder-0"
|
|
assert backend.vllm_config.scheduler_config.max_num_seqs == 1
|
|
|
|
monkeypatch.setattr(model_runner_backend_module, "init_kv_cache", successful_init_cache)
|
|
backend.initialize_kv_cache(config)
|
|
assert backend.kv_cache_config is not None
|
|
assert backend.kv_caches == ["cache-0", "cache-1"]
|
|
assert backend.vllm_config.scheduler_config.max_num_seqs == 2
|
|
|
|
|
|
def test_initialize_rejects_config_for_different_registered_layers() -> None:
|
|
backend = DiffusionKVModelRunnerBackend(
|
|
vllm_config=SimpleNamespace(),
|
|
od_config=SimpleNamespace(),
|
|
device="cuda:1",
|
|
)
|
|
backend._kv_cache_layer_adapters = {
|
|
"expected-layer": SimpleNamespace(spec=SimpleNamespace(non_causal=False), kv_cache=None)
|
|
}
|
|
config = SimpleNamespace(
|
|
kv_cache_groups=[SimpleNamespace(layer_names=["other-layer"])],
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="layer mismatch"):
|
|
backend.initialize_kv_cache(config)
|
|
|
|
|
|
def test_valid_sequence_and_context_install_into_native_rows(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
|
|
assert runner.install_diffusion_kv_metadata(_metadata()) is True
|
|
|
|
assert block_tables.append_calls == [
|
|
(0, ([1, 2, 3], [4, 5]), True),
|
|
(1, ([6, 7], [8]), True),
|
|
]
|
|
assert block_tables.apply_calls == 1
|
|
assert runner.get_diffusion_kv_row("req-0", 0) == 0
|
|
assert runner.get_diffusion_kv_row("req-0", 0, "text") == 1
|
|
sequence_binding = runner._resolve_paged_attention_row("req-0", 0, None)
|
|
context_binding = runner._resolve_paged_attention_row("req-0", None, "text")
|
|
assert (sequence_binding.row_index, sequence_binding.max_seq_len, sequence_binding.block_ids) == (
|
|
0,
|
|
9,
|
|
((1, 2, 3), (4, 5)),
|
|
)
|
|
assert (context_binding.row_index, context_binding.max_seq_len, context_binding.block_ids) == (
|
|
1,
|
|
5,
|
|
((6, 7), (8,)),
|
|
)
|
|
|
|
|
|
def test_block_table_mutations_invalidate_prepared_attention_batches(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
runner, _, _ = _runner(monkeypatch)
|
|
assert runner.paged_attention_adapter is not None
|
|
runner.paged_attention_adapter.invalidate_prepared_batches = Mock()
|
|
|
|
runner.install_diffusion_kv_metadata(_metadata())
|
|
runner.paged_attention_adapter.invalidate_prepared_batches.assert_called_once_with()
|
|
|
|
runner.remove_diffusion_kv_requests(["req-0"])
|
|
assert runner.paged_attention_adapter.invalidate_prepared_batches.call_count == 2
|
|
|
|
runner.refresh_block_table_layout()
|
|
assert runner.paged_attention_adapter.invalidate_prepared_batches.call_count == 3
|
|
|
|
|
|
def test_cache_layer_registration_is_frozen_after_physical_initialization(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
runner, _, _ = _runner(monkeypatch)
|
|
|
|
with pytest.raises(RuntimeError, match="cannot be changed"):
|
|
runner.register_kv_cache_layers({})
|
|
|
|
|
|
def test_request_scoped_context_uses_one_row_across_sequences(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
first_sequence = _metadata().sequences[0]
|
|
metadata = replace(
|
|
_metadata(),
|
|
sequences=(
|
|
first_sequence,
|
|
replace(
|
|
first_sequence,
|
|
sequence_id=1,
|
|
block_ids=([9, 10, 11], [12, 13]),
|
|
),
|
|
),
|
|
)
|
|
|
|
assert runner.install_diffusion_kv_metadata(metadata) is True
|
|
|
|
assert block_tables.append_calls == [
|
|
(0, ([1, 2, 3], [4, 5]), True),
|
|
(1, ([9, 10, 11], [12, 13]), True),
|
|
(2, ([6, 7], [8]), True),
|
|
]
|
|
assert runner.get_diffusion_kv_row("req-0", 0, "text") == 2
|
|
assert runner.get_diffusion_kv_row("req-0", 1, "text") == 2
|
|
|
|
|
|
def test_row_block_count_uses_native_spec_semantics(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
spec = runner.kv_cache_config.kv_cache_groups[0].kv_cache_spec
|
|
spec.max_num_blocks_per_req = lambda _config, token_len: math.ceil(token_len / 8)
|
|
|
|
metadata = _metadata(
|
|
sequence_block_ids=([1, 2], [4, 5]),
|
|
contexts=(),
|
|
)
|
|
|
|
assert runner.install_diffusion_kv_metadata(metadata) is True
|
|
assert block_tables.append_calls == [(0, ([1, 2], [4, 5]), True)]
|
|
|
|
|
|
def test_repeated_native_block_ids_are_allowed(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
metadata = _metadata(
|
|
sequence_block_ids=([1, 1, 3], [4, 4]),
|
|
contexts=(),
|
|
)
|
|
|
|
assert runner.install_diffusion_kv_metadata(metadata) is True
|
|
assert block_tables.append_calls == [(0, ([1, 1, 3], [4, 4]), True)]
|
|
|
|
|
|
def test_generation_is_idempotent_stale_safe_and_conflict_safe(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
metadata = _metadata()
|
|
runner.install_diffusion_kv_metadata(metadata)
|
|
|
|
assert runner.install_diffusion_kv_metadata(metadata) is False
|
|
assert runner.install_diffusion_kv_metadata(replace(metadata, allocation_generation=2)) is False
|
|
assert block_tables.apply_calls == 1
|
|
|
|
with pytest.raises(ValueError, match="Stale Diffusion KV allocation generation"):
|
|
runner.install_diffusion_kv_metadata(metadata)
|
|
with pytest.raises(ValueError, match="Conflicting Diffusion KV allocation snapshot"):
|
|
runner.install_diffusion_kv_metadata(_metadata(generation=3, sequence_block_ids=([9, 2, 3], [4, 5])))
|
|
assert block_tables.apply_calls == 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("metadata", "message"),
|
|
[
|
|
(_metadata(sequence_block_ids=([1, 2, 3],)), "block groups"),
|
|
(_metadata(sequence_block_ids=([1, 2], [4, 5])), "expected 3"),
|
|
(_metadata(sequence_block_ids=([1, 2, 32], [4, 5])), "outside"),
|
|
(
|
|
_metadata(
|
|
seq_len=17,
|
|
sequence_block_ids=([1, 2, 3, 4, 5], [6, 7, 8]),
|
|
contexts=(),
|
|
),
|
|
"exceeds row capacity",
|
|
),
|
|
],
|
|
)
|
|
def test_invalid_group_count_block_count_range_and_row_capacity_are_atomic(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
metadata: DiffusionKVMetadata,
|
|
message: str,
|
|
) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
|
|
with pytest.raises(ValueError, match=message):
|
|
runner.install_diffusion_kv_metadata(metadata)
|
|
|
|
assert block_tables.append_calls == []
|
|
assert block_tables.apply_calls == 0
|
|
assert runner._diffusion_kv_identity_to_row == {}
|
|
assert runner._diffusion_kv_free_rows == [5, 4, 3, 2, 1, 0]
|
|
|
|
|
|
def test_invalid_later_context_does_not_stage_earlier_valid_row(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
bad_context = DiffusionKVContextMetadata(
|
|
context_id="text",
|
|
cache_role="cross_attention",
|
|
num_tokens=5,
|
|
block_ids=([6, 32], [8]),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="outside"):
|
|
runner.install_diffusion_kv_metadata(_metadata(contexts=(bad_context,)))
|
|
|
|
assert block_tables.append_calls == []
|
|
assert all(not group_table.staged for group_table in block_tables.block_tables)
|
|
|
|
|
|
def test_native_append_failure_rolls_back_staged_state_and_registry(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
block_tables.fail_on_append_call = 2
|
|
|
|
with pytest.raises(RuntimeError, match="fake append failure"):
|
|
runner.install_diffusion_kv_metadata(_metadata())
|
|
|
|
assert np.count_nonzero(block_tables.num_blocks.np) == 0
|
|
assert all(not group_table.staged for group_table in block_tables.block_tables)
|
|
assert runner._diffusion_kv_identity_to_row == {}
|
|
assert runner._diffusion_kv_free_rows == [5, 4, 3, 2, 1, 0]
|
|
|
|
|
|
def test_native_apply_failure_rolls_back_staged_state_and_registry(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
block_tables.fail_apply = True
|
|
|
|
with pytest.raises(RuntimeError, match="fake apply failure"):
|
|
runner.install_diffusion_kv_metadata(_metadata())
|
|
|
|
assert block_tables.apply_calls == 1
|
|
assert np.count_nonzero(block_tables.num_blocks.np) == 0
|
|
assert all(not group_table.staged for group_table in block_tables.block_tables)
|
|
assert all(group_table.clear_calls == 1 for group_table in block_tables.block_tables)
|
|
assert runner._diffusion_kv_identity_to_row == {}
|
|
assert runner._diffusion_kv_free_rows == [5, 4, 3, 2, 1, 0]
|
|
|
|
|
|
def test_adapter_and_global_row_capacity_are_enforced(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch, max_rows_per_request=2, max_num_seqs=1)
|
|
second_context = DiffusionKVContextMetadata(
|
|
context_id="image",
|
|
cache_role="cross_attention",
|
|
num_tokens=1,
|
|
block_ids=([9], [10]),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="adapter limit is 2"):
|
|
runner.install_diffusion_kv_metadata(_metadata(contexts=(_metadata().contexts[0], second_context)))
|
|
assert block_tables.append_calls == []
|
|
|
|
runner.install_diffusion_kv_metadata(_metadata())
|
|
append_count = len(block_tables.append_calls)
|
|
with pytest.raises(ValueError, match="only 0 rows are free"):
|
|
runner.install_diffusion_kv_metadata(_metadata("req-1"))
|
|
assert len(block_tables.append_calls) == append_count
|
|
|
|
|
|
def test_empty_and_duplicate_sequence_metadata_are_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
empty = DiffusionKVMetadata(request_id="req-0", allocation_generation=0, sequences=())
|
|
duplicate_sequence = _metadata().sequences[0]
|
|
duplicate = DiffusionKVMetadata(
|
|
request_id="req-0",
|
|
allocation_generation=0,
|
|
sequences=(duplicate_sequence, duplicate_sequence),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="at least one sequence"):
|
|
runner.install_diffusion_kv_metadata(empty)
|
|
with pytest.raises(ValueError, match="Duplicate diffusion KV row identity"):
|
|
runner.install_diffusion_kv_metadata(duplicate)
|
|
assert block_tables.append_calls == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("metadata", "message"),
|
|
[
|
|
(_metadata(generation=-1), "non-negative integer"),
|
|
(_metadata(request_id=""), "non-empty string"),
|
|
(_metadata(sequence_block_ids=([1, 2, True], [4, 5])), "non-integer block ID"),
|
|
],
|
|
)
|
|
def test_request_generation_and_block_id_types_are_validated(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
metadata: DiffusionKVMetadata,
|
|
message: str,
|
|
) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
|
|
with pytest.raises(ValueError, match=message):
|
|
runner.install_diffusion_kv_metadata(metadata)
|
|
|
|
assert block_tables.append_calls == []
|
|
assert runner._diffusion_kv_request_states == {}
|
|
|
|
|
|
def test_cleanup_is_idempotent_and_reuses_native_rows(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch, max_rows_per_request=2, max_num_seqs=1)
|
|
runner.install_diffusion_kv_metadata(_metadata())
|
|
|
|
assert runner.remove_diffusion_kv_requests(["missing"]) == 0
|
|
assert runner.remove_diffusion_kv_requests(["req-0", "req-0"]) == 2
|
|
assert block_tables.append_calls[-2:] == [(0, ([], []), True), (1, ([], []), True)]
|
|
assert block_tables.num_blocks.np[:, :2].tolist() == [[0, 0], [0, 0]]
|
|
assert runner.remove_diffusion_kv_requests(["req-0"]) == 0
|
|
with pytest.raises(KeyError, match="No native diffusion KV row"):
|
|
runner.get_diffusion_kv_row("req-0", 0)
|
|
|
|
runner.install_diffusion_kv_metadata(_metadata("req-1"))
|
|
assert runner.get_diffusion_kv_row("req-1", 0) == 0
|
|
assert runner.get_diffusion_kv_row("req-1", 0, "text") == 1
|
|
|
|
|
|
def test_cached_step_does_not_reinstall_metadata() -> None:
|
|
runner = object.__new__(DiffusionModelRunner)
|
|
runner.od_config = SimpleNamespace(diffusion_kv_mode=DiffusionKVCacheMode.PAGED_SCHEDULER)
|
|
runner.pipeline = object()
|
|
runner.install_diffusion_kv_metadata = Mock()
|
|
runner._supports_step_mode = Mock(return_value=False)
|
|
scheduler_output = DiffusionSchedulerOutput(
|
|
step_id=1,
|
|
scheduled_new_reqs=[],
|
|
scheduled_cached_reqs=CachedRequestData(request_ids=["req-0"]),
|
|
finished_req_ids=set(),
|
|
num_running_reqs=1,
|
|
num_waiting_reqs=0,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="does not support step execution"):
|
|
runner.execute_stepwise(scheduler_output)
|
|
runner.install_diffusion_kv_metadata.assert_not_called()
|
|
|
|
|
|
def test_dense_cleanup_and_wake_are_noops_but_paged_wake_refreshes(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
runner, block_tables, _ = _runner(monkeypatch)
|
|
runner.refresh_block_table_layout()
|
|
assert block_tables.layout_refreshes == 1
|
|
|
|
dense_backend = DiffusionKVModelRunnerBackend(
|
|
vllm_config=SimpleNamespace(),
|
|
od_config=SimpleNamespace(diffusion_kv_mode=DiffusionKVCacheMode.DENSE_LEGACY),
|
|
device="cuda:1",
|
|
)
|
|
dense_backend.block_tables = Mock()
|
|
assert dense_backend.remove_diffusion_kv_requests(["req-0"]) == 0
|
|
dense_backend.refresh_block_table_layout()
|
|
dense_backend.block_tables.init_block_table_layout_tensors.assert_not_called()
|
|
with pytest.raises(ValueError, match="Dense diffusion execution"):
|
|
dense_backend.install_diffusion_kv_metadata(_metadata())
|