[Diffusion] Add native paged KV backend for diffusion workers (#6102)

Signed-off-by: Acerak01-fy <wfy2003324@163.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
Wang fuyinandHongsheng Liu authored and GitHub committed 2026-08-22 21:45:45 +08:00
1 parent c2daa471c3
commit 72ee535fcd
34 files changed
+4595 -71

No files matched your search

+11 -1
View File
@@ -1090,7 +1090,10 @@ def test_diffusion_config_preserves_existing_coercion_hooks():
def test_diffusion_config_from_kwargs_reuses_legacy_normalization(monkeypatch):
from vllm_omni.platforms import current_omni_platform
monkeypatch.setenv("DIFFUSION_CACHE_BACKEND", "TEA_CACHE")
monkeypatch.setattr(current_omni_platform, "is_cuda", lambda: True)
cfg = omni_config_module._DiffusionConfigProjection.from_kwargs(
diffusion_attention_backend="flash_attn",
@@ -1100,6 +1103,7 @@ def test_diffusion_config_from_kwargs_reuses_legacy_normalization(monkeypatch):
kv_cache_skip_layers=[2],
static_lora_scale=0.25,
diffusion_kv_mode="paged_scheduler",
diffusion_kv_max_rows_per_request=2,
diffusers_load_kwargs=None,
diffusers_call_kwargs=None,
)
@@ -1116,7 +1120,10 @@ def test_diffusion_config_from_kwargs_reuses_legacy_normalization(monkeypatch):
assert cfg.diffusers_call_kwargs == {}
def test_from_pipeline_config_normalizes_diffusion_config_aliases_from_engine_args(tmp_path):
def test_from_pipeline_config_normalizes_diffusion_config_aliases_from_engine_args(tmp_path, monkeypatch):
from vllm_omni.platforms import current_omni_platform
monkeypatch.setattr(current_omni_platform, "is_cuda", lambda: True)
deploy_path = tmp_path / "dreamzero_diffusion_aliases.yaml"
deploy_path.write_text(
"\n".join(
@@ -1128,6 +1135,7 @@ def test_from_pipeline_config_normalizes_diffusion_config_aliases_from_engine_ar
" diffusion_attention_backend: flash_attn",
" fa_deterministic: true",
" diffusion_kv_mode: paged_scheduler",
" diffusion_kv_max_rows_per_request: 2",
]
)
)
@@ -1141,6 +1149,7 @@ def test_from_pipeline_config_normalizes_diffusion_config_aliases_from_engine_ar
assert stage.diffusion_config.diffusion_attention_config.default.backend == "flash_attn"
assert stage.diffusion_config.fa_deterministic is True
assert stage.diffusion_config.diffusion_kv_mode is DiffusionKVCacheMode.PAGED_SCHEDULER
assert stage.diffusion_config.diffusion_kv_max_rows_per_request == 2
def test_from_pipeline_config_rejects_reserved_diffusion_kv_mode(tmp_path):
@@ -1183,6 +1192,7 @@ def test_diffusion_config_field_classification_covers_current_fields():
"prompt_embed_cache_size",
"diffusion_kv_cache_dtype",
"diffusion_kv_mode",
"diffusion_kv_max_rows_per_request",
} <= omni_config_module._DIFFUSION_ONLY_CONFIG_FIELDS
assert {
"revision",
@@ -23,8 +23,10 @@ import vllm_omni.diffusion.attention.backends.utils.piecewise_attn as piecewise_
from vllm_omni.diffusion.attention.backends.abstract import QueryRange
from vllm_omni.diffusion.attention.backends.utils.piecewise_attn import (
Segment,
build_paged_piecewise_plan,
build_segments,
piecewise_attn,
run_paged_piecewise_plan,
)
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
@@ -84,6 +86,72 @@ def test_build_segments_full_span_retains_global_kv_end():
assert segments == [Segment(q_start=4, q_end=6, kv_end=7, mode="full")]
def test_paged_piecewise_runner_uses_packed_indices_and_kv_endpoints():
full_attn_spans = [[(2, 5)], [(4, 7)]]
plan = build_paged_piecewise_plan(
full_attn_spans,
query_offsets=[0, 2],
query_lens=[6, 6],
seq_lens=[6, 8],
device=DEVICE,
)
query = torch.arange(12 * 2 * 4, dtype=torch.float32).reshape(12, 2, 4)
key = query + 100
value = query + 200
calls = []
def run_segment(segment_query, segment_key, segment_value, segment_batch):
calls.append(
(
[segment.mode for segment in segment_batch.row_segments],
[segment.kv_end for segment in segment_batch.row_segments],
segment_batch.query_indices.tolist(),
)
)
torch.testing.assert_close(segment_key, segment_query + 100)
torch.testing.assert_close(segment_value, segment_query + 200)
return segment_query
output = run_paged_piecewise_plan(
query,
key,
value,
plan,
plan.segments,
run_segment,
)
torch.testing.assert_close(output, query)
assert calls == [
(["causal", "causal"], [2, 4], [0, 1, 6, 7]),
(["full", "full"], [5, 7], [2, 3, 4, 8, 9, 10]),
(["causal", "causal"], [6, 8], [5, 11]),
]
def test_paged_piecewise_runner_uses_runner_output_shape():
plan = build_paged_piecewise_plan(
[[(2, 5)]],
query_offsets=[0],
query_lens=[6],
seq_lens=[6],
device=DEVICE,
)
query = torch.arange(6 * 2 * 4, dtype=torch.float32).reshape(6, 2, 4)
output = run_paged_piecewise_plan(
query,
query,
query,
plan,
plan.segments,
lambda segment_query, *_args: segment_query[..., :3],
)
assert output.shape == (6, 2, 3)
torch.testing.assert_close(output, query[..., :3])
@pytest.mark.parametrize("global_spans", SPAN_CASES)
@pytest.mark.parametrize("q_range", Q_RANGE_CASES)
@pytest.mark.parametrize("batch_size", BATCH_CASES)
@@ -0,0 +1,772 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM 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]
def init_cache(
runner_kv_caches,
forward_context,
kv_cache_config,
attn_groups,
device,
cache_dtype,
kernel_block_sizes,
vllm_config,
):
events.append(("cache", kv_cache_config, attn_groups, 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) == (0, 9)
assert (context_binding.row_index, context_binding.max_seq_len) == (1, 5)
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())
@@ -3,6 +3,7 @@
import pytest
from vllm_omni.config.omni_config import _DiffusionConfigProjection
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
@@ -39,6 +40,8 @@ def test_paged_scheduler_rejects_dense_legacy_kv_receive() -> None:
with pytest.raises(ValueError, match="does not support imported AR KV"):
OmniDiffusionConfig.from_kwargs(
diffusion_kv_mode="paged_scheduler",
diffusion_kv_max_rows_per_request=1,
max_num_batched_tokens=1,
omni_kv_config={"need_recv_cache": True},
)
@@ -47,6 +50,33 @@ def test_paged_scheduler_does_not_depend_on_model_registry() -> None:
config = OmniDiffusionConfig.from_kwargs(
model_class_name="FutureDiffusionModel",
diffusion_kv_mode="paged_scheduler",
diffusion_kv_max_rows_per_request=1,
max_num_batched_tokens=1,
)
assert config.diffusion_kv_mode is DiffusionKVCacheMode.PAGED_SCHEDULER
@pytest.mark.parametrize("config_cls", [OmniDiffusionConfig, _DiffusionConfigProjection])
def test_paged_scheduler_mode_is_platform_agnostic(config_cls) -> None:
config = config_cls.from_kwargs(
diffusion_kv_mode="paged_scheduler",
diffusion_kv_max_rows_per_request=1,
)
assert config.diffusion_kv_mode is DiffusionKVCacheMode.PAGED_SCHEDULER
def test_paged_scheduler_requires_a_worker_row_limit() -> None:
with pytest.raises(ValueError, match="requires diffusion_kv_max_rows_per_request"):
OmniDiffusionConfig.from_kwargs(diffusion_kv_mode="paged_scheduler")
with pytest.raises(ValueError, match="requires diffusion_kv_max_rows_per_request"):
_DiffusionConfigProjection.from_kwargs(diffusion_kv_mode="paged_scheduler")
@pytest.mark.parametrize("invalid_limit", [0, -1, True, 1.5])
def test_worker_row_limit_must_be_a_positive_integer(invalid_limit: object) -> None:
with pytest.raises((TypeError, ValueError)):
OmniDiffusionConfig.from_kwargs(diffusion_kv_max_rows_per_request=invalid_limit)
with pytest.raises((TypeError, ValueError)):
_DiffusionConfigProjection.from_kwargs(diffusion_kv_max_rows_per_request=invalid_limit)
@@ -22,6 +22,7 @@ class _Executor:
self.specs = [{"layer0": object()}, {"layer0": object()}]
self.memory = [1024, 1024]
self.configs = None
self.resolved_max_model_len = None
self.profile_requests = None
def get_kv_cache_specs(self):
@@ -31,8 +32,9 @@ class _Executor:
self.profile_requests = profile_requests
return self.memory
def set_kv_cache_configs(self, configs) -> None:
def set_kv_cache_configs(self, configs, resolved_max_model_len) -> None:
self.configs = configs
self.resolved_max_model_len = resolved_max_model_len
def _od_config(**overrides):
@@ -55,6 +57,7 @@ def _od_config(**overrides):
),
gpu_memory_utilization=0.9,
kv_cache_memory_bytes=None,
max_num_seqs=1,
max_num_batched_tokens=64,
num_gpus=1,
)
@@ -64,9 +67,9 @@ def _od_config(**overrides):
def test_control_plane_builds_worker_and_scheduler_configs(monkeypatch) -> None:
executor = _Executor()
od_config = _od_config(num_gpus=2)
od_config = _od_config(num_gpus=2, max_model_len=-1)
vllm_config = SimpleNamespace(
model_config=SimpleNamespace(max_model_len=4096),
model_config=SimpleNamespace(max_model_len=256),
max_in_flight_tokens=128,
)
worker_configs = [object(), object()]
@@ -91,6 +94,7 @@ def test_control_plane_builds_worker_and_scheduler_configs(monkeypatch) -> None:
build.assert_called_once_with(vllm_config, executor.specs, executor.memory)
assert result == (scheduler_kv_cache_config, 16, 16, vllm_config)
assert executor.configs == worker_configs
assert executor.resolved_max_model_len == vllm_config.model_config.max_model_len
assert executor.profile_requests is profile_requests
@@ -0,0 +1,54 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
from importlib import import_module, util
import pytest
import torch
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.gpu]
def test_native_block_tables_slot_mapping_handles_non_aligned_tail() -> None:
if not torch.cuda.is_available():
pytest.skip("CUDA is required for the native BlockTables slot-mapping smoke test")
module_name = "vllm.v1.worker.gpu.block_table"
try:
module_spec = util.find_spec(module_name)
except ModuleNotFoundError as exc:
if exc.name is None or not module_name.startswith(f"{exc.name}."):
raise
pytest.skip("installed vLLM does not provide GPU BlockTables")
if module_spec is None:
pytest.skip("installed vLLM does not provide GPU BlockTables")
block_table_module = import_module(module_name)
block_tables_cls = getattr(block_table_module, "BlockTables", None)
if block_tables_cls is None or not hasattr(block_tables_cls, "compute_slot_mappings"):
pytest.skip("installed vLLM does not provide the required BlockTables API")
device = torch.device("cuda", torch.accelerator.current_device_index())
block_tables = block_tables_cls(
block_sizes=[4],
max_num_reqs=1,
max_num_batched_tokens=16,
max_num_blocks_per_group=[4],
device=device,
kernel_block_sizes=[4],
)
block_tables.append_block_ids(0, ([1, 2, 3],), overwrite=True)
block_tables.apply_staged_writes()
idx_mapping = torch.tensor([0], dtype=torch.int32, device=device)
query_start_loc = torch.tensor([0, 9], dtype=torch.int32, device=device)
positions = torch.arange(9, dtype=torch.int64, device=device)
slot_mappings = block_tables.compute_slot_mappings(
idx_mapping,
query_start_loc,
positions,
num_tokens_padded=9,
)
torch.accelerator.synchronize(device)
assert slot_mappings.cpu().tolist() == [[4, 5, 6, 7, 8, 9, 10, 11, 12]]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,251 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
from types import SimpleNamespace
import pytest
import torch
from torch import nn
from torch.nn import functional as F
from vllm.config import AttentionConfig, CacheConfig, VllmConfig, set_current_vllm_config
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec, KVCacheTensor
from vllm.v1.worker.gpu.attn_utils import init_attn_backend, init_kv_cache
from vllm.v1.worker.gpu.block_table import BlockTables
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
from vllm_omni.diffusion.attention.backends.flash_attn import FlashAttentionImpl
from vllm_omni.diffusion.diffusion_kv.paged_attention_adapter import (
DiffusionPagedAttentionAdapter,
DiffusionPagedAttentionLayerAdapter,
DiffusionPagedAttentionRow,
DiffusionPagedAttentionRowBinding,
)
from vllm_omni.diffusion.vllm_config import _DiffusionVllmModelConfig
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.gpu]
_LAYER_NAME = "model.layers.0.attn"
_NUM_HEADS = 2
_HEAD_SIZE = 64
_BLOCK_SIZE = 16
class _SmokeDiffusionAttention(nn.Module):
def __init__(self) -> None:
super().__init__()
self.num_heads = _NUM_HEADS
self.num_kv_heads = _NUM_HEADS
self.head_size = _HEAD_SIZE
self.softmax_scale = _HEAD_SIZE**-0.5
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required for native paged attention")
def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> None:
device = torch.device("cuda", torch.accelerator.current_device_index())
vllm_config = VllmConfig(
cache_config=CacheConfig(
block_size=_BLOCK_SIZE,
cache_dtype="float16",
enable_prefix_caching=False,
),
attention_config=AttentionConfig(use_non_causal=True),
)
model_config = _DiffusionVllmModelConfig(
model=None,
dtype=torch.float16,
max_model_len=32,
original_max_model_len=32,
hf_config=SimpleNamespace(model_type="diffusion_paged_attention_smoke"),
)
vllm_config.model_config = model_config
model_config.set_attention_geometry(
num_heads=_NUM_HEADS,
num_kv_heads=_NUM_HEADS,
head_size=_HEAD_SIZE,
)
vllm_config.compilation_config.static_forward_context.clear()
diffusion_layer = _SmokeDiffusionAttention().to(device)
spec = FullAttentionSpec(
block_size=_BLOCK_SIZE,
num_kv_heads=_NUM_HEADS,
head_size=_HEAD_SIZE,
dtype=torch.float16,
non_causal=True,
)
with set_current_vllm_config(vllm_config):
native_layer = DiffusionPagedAttentionLayerAdapter(
layer_name=_LAYER_NAME,
layer=diffusion_layer,
spec=spec,
vllm_config=vllm_config,
device=device,
)
vllm_config.compilation_config.static_forward_context[_LAYER_NAME] = native_layer
canonical_spec = native_layer.spec
kv_cache_config = KVCacheConfig(
num_blocks=4,
kv_cache_tensors=[
KVCacheTensor(
size=canonical_spec.page_size_bytes * 4,
shared_by=[_LAYER_NAME],
)
],
kv_cache_groups=[
KVCacheGroupSpec(
layer_names=[_LAYER_NAME],
kv_cache_spec=canonical_spec,
)
],
)
with set_current_vllm_config(vllm_config):
attn_groups, _, kernel_block_sizes = init_attn_backend(
kv_cache_config,
vllm_config,
device,
)
runner_kv_caches: list[torch.Tensor | list[torch.Tensor]] = []
init_kv_cache(
runner_kv_caches,
vllm_config.compilation_config.static_forward_context,
kv_cache_config,
attn_groups,
device,
vllm_config.cache_config.cache_dtype,
kernel_block_sizes,
vllm_config,
)
block_tables = BlockTables(
block_sizes=[_BLOCK_SIZE],
max_num_reqs=1,
max_num_batched_tokens=32,
max_num_blocks_per_group=[2],
device=device,
kernel_block_sizes=kernel_block_sizes,
)
block_tables.append_block_ids(0, ([3, 1],), overwrite=True)
block_tables.apply_staged_writes()
adapter = DiffusionPagedAttentionAdapter(
vllm_config=vllm_config,
device=device,
kv_cache_config=kv_cache_config,
block_tables=block_tables,
attn_groups=attn_groups,
layers={_LAYER_NAME: native_layer},
resolve_row=lambda _request_id, _sequence_id, _context_id: DiffusionPagedAttentionRowBinding(
row_index=0,
max_seq_len=19,
),
)
prefix_batch = adapter.prepare_batch(
[
DiffusionPagedAttentionRow(
request_id="req-0",
sequence_id=0,
query_len=17,
seq_len=17,
)
]
)
torch.manual_seed(1)
query = torch.randn(19, _NUM_HEADS, _HEAD_SIZE, dtype=torch.float16, device=device)
key = torch.randn_like(query)
value = torch.randn_like(query)
omni_backend = FlashAttentionImpl(
num_heads=_NUM_HEADS,
head_size=_HEAD_SIZE,
softmax_scale=diffusion_layer.softmax_scale,
num_kv_heads=_NUM_HEADS,
)
with adapter.activate(prefix_batch):
prefix_context = adapter.prepare_layer_context(_LAYER_NAME, query[:17], key[:17], value[:17])
prefix_output = omni_backend.forward_paged(prefix_context)
prefix_slot_mappings = prefix_batch.slot_mappings.clone()
suffix_batch = adapter.prepare_batch(
[
DiffusionPagedAttentionRow(
request_id="req-0",
sequence_id=0,
query_len=2,
seq_len=19,
kv_start_pos=17,
)
]
)
with adapter.activate(suffix_batch):
suffix_context = adapter.prepare_layer_context(_LAYER_NAME, query[17:], key[17:], value[17:])
suffix_output = omni_backend.forward_paged(suffix_context)
suffix_slot_mappings = suffix_batch.slot_mappings.clone()
piecewise_batch = adapter.prepare_batch(
[
DiffusionPagedAttentionRow(
request_id="req-0",
sequence_id=0,
query_len=19,
seq_len=19,
)
]
)
mixed_mask = torch.ones(19, 19, dtype=torch.bool, device=device).tril()
mixed_mask[5:10, 5:10] = True
mixed_metadata = AttentionMetadata(
attn_mask=mixed_mask.unsqueeze(0).unsqueeze(0),
full_attn_spans=[[(5, 10)]],
)
with adapter.activate(piecewise_batch):
piecewise_context = adapter.prepare_layer_context(
_LAYER_NAME,
query,
key,
value,
omni_attn_metadata=mixed_metadata,
)
piecewise_output = omni_backend.forward_paged(piecewise_context)
prefix_reference = (
F.scaled_dot_product_attention(
query[:17].transpose(0, 1).unsqueeze(0),
key[:17].transpose(0, 1).unsqueeze(0),
value[:17].transpose(0, 1).unsqueeze(0),
is_causal=False,
scale=diffusion_layer.softmax_scale,
)
.squeeze(0)
.transpose(0, 1)
)
suffix_reference = (
F.scaled_dot_product_attention(
query[17:].transpose(0, 1).unsqueeze(0),
key.transpose(0, 1).unsqueeze(0),
value.transpose(0, 1).unsqueeze(0),
is_causal=False,
scale=diffusion_layer.softmax_scale,
)
.squeeze(0)
.transpose(0, 1)
)
piecewise_reference = (
F.scaled_dot_product_attention(
query.transpose(0, 1).unsqueeze(0),
key.transpose(0, 1).unsqueeze(0),
value.transpose(0, 1).unsqueeze(0),
attn_mask=mixed_mask.unsqueeze(0).unsqueeze(0),
scale=diffusion_layer.softmax_scale,
)
.squeeze(0)
.transpose(0, 1)
)
torch.accelerator.synchronize(device)
assert prefix_slot_mappings.cpu().tolist() == [list(range(3 * _BLOCK_SIZE, 4 * _BLOCK_SIZE)) + [_BLOCK_SIZE]]
assert suffix_slot_mappings.cpu().tolist() == [[_BLOCK_SIZE + 1, _BLOCK_SIZE + 2]]
torch.testing.assert_close(prefix_output, prefix_reference, rtol=2e-2, atol=2e-2)
torch.testing.assert_close(suffix_output, suffix_reference, rtol=2e-2, atol=2e-2)
torch.testing.assert_close(piecewise_output, piecewise_reference, rtol=2e-2, atol=2e-2)
@@ -25,6 +25,23 @@ class _Backend:
return True
class _RunnerKVBackend:
def __init__(self) -> None:
self.kv_cache_config = None
def register_kv_cache_layers(self, layers):
return {layer_name: spec for layer_name, (_layer, spec) in layers.items()}
def initialize_kv_cache(self, config) -> None:
configured_layers = {layer_name for group in config.kv_cache_groups for layer_name in group.layer_names}
if configured_layers != {"image_attention"}:
raise ValueError(
"Rank-local Diffusion KVCacheConfig layer mismatch: "
f"expected=['image_attention'], configured={sorted(configured_layers)}"
)
self.kv_cache_config = config
def _attention(*, enabled: bool) -> Attention:
attention = Attention.__new__(Attention)
nn.Module.__init__(attention)
@@ -47,6 +64,7 @@ def _runner(attention: Attention) -> DiffusionModelRunner:
pipeline = nn.Module()
pipeline.image_attention = attention
runner.pipeline = pipeline
runner.diffusion_kv_backend = _RunnerKVBackend()
return runner
@@ -102,14 +120,21 @@ def test_worker_selects_its_rank_local_config() -> None:
worker = object.__new__(DiffusionWorker)
worker.rank = 1
worker.od_config = SimpleNamespace(num_gpus=2)
worker.vllm_config = SimpleNamespace(
model_config=SimpleNamespace(max_model_len=None),
cache_config=SimpleNamespace(num_gpu_blocks=None),
)
worker._maybe_get_memory_pool_context = lambda _tag: nullcontext()
worker.model_runner = SimpleNamespace(set_kv_cache_config=lambda config: setattr(worker, "installed", config))
configs = [object(), object()]
configs = [SimpleNamespace(num_blocks=4), SimpleNamespace(num_blocks=8)]
worker.set_kv_cache_configs(configs)
worker.set_kv_cache_configs(configs, 64)
assert worker.installed is configs[1]
assert worker.vllm_config.model_config.max_model_len == 64
assert worker.vllm_config.cache_config.num_gpu_blocks == 8
with pytest.raises(ValueError, match="rank count mismatch"):
worker.set_kv_cache_configs(configs[:1])
worker.set_kv_cache_configs(configs[:1], 64)
def test_worker_honors_explicit_kv_memory_budget(monkeypatch) -> None:
@@ -19,8 +19,8 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_TRANSFORMER_MODULE = "vllm_omni.diffusion.models.hunyuan_image3.hunyuan_image3_transformer"
NUM_HEADS = 4
NUM_KV_HEADS = 2
NUM_HEADS = 32
NUM_KV_HEADS = 8
HEAD_DIM = 16
IMAGE_TOKEN_LEN = 8
SUFFIX_TOKEN_LEN = 3
@@ -35,8 +35,14 @@ SCALING = 1.0 / math.sqrt(HEAD_DIM)
class MockAttention(nn.Module):
def __init__(self, num_heads, head_size, causal=False, softmax_scale=None, num_kv_heads=None, **kwargs):
super().__init__()
self.paged_kv_active = False
self.calls = []
def is_paged_kv_active(self):
return self.paged_kv_active
def forward(self, query, key, value, attn_metadata=None, **kwargs):
self.calls.append((query, key, value))
return query
@@ -126,6 +132,37 @@ def test_cache_manager_registers_attention_without_adding_dense_state() -> None:
assert mgr.state_dict() == {}
@pytest.mark.parametrize(
("paged_kv_active", "expected_kv_heads"),
[(False, NUM_HEADS), (True, NUM_KV_HEADS)],
)
def test_gqa_kv_stays_compressed_for_paged_attention(
paged_kv_active: bool,
expected_kv_heads: int,
) -> None:
mgr = _make_cache_mgr()
mgr.attn.paged_kv_active = paged_kv_active
q_len = 3
key, value = _make_known_kv(q_len)
_call_mgr(
mgr,
bs=1,
q_len=q_len,
seq_len=q_len,
key_flat=key,
value_flat=value,
first_step=True,
num_image_tokens=0,
gen_timestep_scatter_index=_gen_timestep_index(1, 0),
)
_, key_input, value_input = mgr.attn.calls[-1]
expected_shape = (1, q_len, expected_kv_heads, HEAD_DIM)
assert key_input.shape == expected_shape
assert value_input.shape == expected_shape
# ============================================================
# Test 1: No AR KV — basic cache → reuse
# ============================================================
@@ -149,10 +149,18 @@ class TestCreateDefaultDiffusion:
assert ea["enforce_eager"] is True
assert ea["lora_path"] == "/tmp/lora"
def test_diffusion_kv_mode_roundtrip(self):
od = _roundtrip_diffusion_config(model="x", diffusion_kv_mode="paged_scheduler")
def test_diffusion_kv_mode_roundtrip(self, monkeypatch):
from vllm_omni.platforms import current_omni_platform
monkeypatch.setattr(current_omni_platform, "is_cuda", lambda: True)
od = _roundtrip_diffusion_config(
model="x",
diffusion_kv_mode="paged_scheduler",
diffusion_kv_max_rows_per_request=2,
)
assert od.diffusion_kv_mode is DiffusionKVCacheMode.PAGED_SCHEDULER
assert od.diffusion_kv_max_rows_per_request == 2
def test_diffusion_kv_sizing_fields_roundtrip(self, monkeypatch):
monkeypatch.setattr(OmniDiffusionConfig, "_resolve_master_port", lambda self: 29500)
@@ -12,6 +12,7 @@ import pytest
from vllm_omni.diffusion import diffusion_engine as diffusion_engine_module
from vllm_omni.diffusion.data import DiffusionOutput
from vllm_omni.diffusion.diffusion_engine import DiffusionEngine, DiffusionExecutionMode
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.sched import DiffusionRequestStatus, RequestScheduler
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
@@ -88,6 +89,50 @@ def test_emit_step_outputs_finalizes_finished_request_without_stream() -> None:
assert engine.scheduler.get_request_state(request_id) is None
def _make_kv_cleanup_engine(mode: DiffusionKVCacheMode) -> tuple[DiffusionEngine, Mock]:
engine = DiffusionEngine.__new__(DiffusionEngine)
engine.od_config = SimpleNamespace(diffusion_kv_mode=mode)
cleanup = Mock()
engine.executor = SimpleNamespace(remove_diffusion_kv_requests=cleanup)
return engine, cleanup
def test_paged_terminal_output_clears_worker_rows() -> None:
engine, cleanup = _make_kv_cleanup_engine(DiffusionKVCacheMode.PAGED_SCHEDULER)
engine._finalize_finished_request = lambda request_id, *_args: request_id
engine._put_output = lambda *_args: None
engine._emit_finished_outputs({"req-0", "req-1"})
cleanup.assert_called_once()
assert set(cleanup.call_args.args[0]) == {"req-0", "req-1"}
def test_paged_abort_clears_worker_rows_before_scheduler_free() -> None:
engine, cleanup = _make_kv_cleanup_engine(DiffusionKVCacheMode.PAGED_SCHEDULER)
engine.scheduler = SimpleNamespace(
get_request_state=lambda _request_id: None,
finish_requests=Mock(side_effect=AssertionError("unknown request must not be logically freed")),
)
engine._abort_requests(["req-idle", "req-idle"])
cleanup.assert_called_once_with(["req-idle"])
engine.scheduler.finish_requests.assert_not_called()
def test_dense_terminal_and_abort_paths_skip_worker_row_cleanup() -> None:
engine, cleanup = _make_kv_cleanup_engine(DiffusionKVCacheMode.DENSE_LEGACY)
engine._finalize_finished_request = lambda request_id, *_args: request_id
engine._put_output = lambda *_args: None
engine.scheduler = SimpleNamespace(get_request_state=lambda _request_id: None)
engine._emit_finished_outputs({"req-0"})
engine._abort_requests(["req-idle"])
cleanup.assert_not_called()
def test_init_accepts_custom_scheduler(monkeypatch: pytest.MonkeyPatch) -> None:
od_config = SimpleNamespace(
custom_pipeline_args=None,
@@ -11,6 +11,7 @@ import torch
import vllm_omni.diffusion.worker.diffusion_model_runner as model_runner_module
from tests.helpers.mark import hardware_test
from vllm_omni.diffusion.data import DiffusionOutput
from vllm_omni.diffusion.diffusion_kv.model_runner_backend import DiffusionKVModelRunnerBackend
from vllm_omni.diffusion.worker.diffusion_model_runner import DiffusionModelRunner
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch, split_diffusion_output_by_request
@@ -193,6 +194,11 @@ def _make_runner(cache_backend, cache_backend_name: str, enable_cache_dit_summar
parallel_config=SimpleNamespace(use_hsdp=False),
streaming_output=False,
)
runner.diffusion_kv_backend = DiffusionKVModelRunnerBackend(
vllm_config=runner.vllm_config,
od_config=runner.od_config,
device=runner.device,
)
runner.kv_transfer_manager = SimpleNamespace(
receive_kv_cache=lambda req, target_device=None: None,
receive_multi_kv_cache=lambda req, cfg_kv_collect_func=None, target_device=None: None,
@@ -40,6 +40,12 @@ class TestPlatformDiffusionHooks:
result = OmniPlatform.get_diffusion_model_runner_cls()
assert result == "vllm_omni.diffusion.worker.diffusion_model_runner.DiffusionModelRunner"
def test_get_diffusion_kv_block_tables_cls_default(self):
"""Test the default paged-KV BlockTables implementation."""
from vllm.v1.worker.gpu.block_table import BlockTables
assert OmniPlatform.get_diffusion_kv_block_tables_cls() is BlockTables
def test_oot_enum_exists(self):
"""Test OOT is a valid platform enum value."""
assert OmniPlatformEnum.OOT.value == "oot"
@@ -17,6 +17,8 @@ import vllm_omni.diffusion.worker.diffusion_model_runner as model_runner_module
from tests.helpers.mark import hardware_test
from vllm_omni.diffusion.data import DiffusionOutput
from vllm_omni.diffusion.diffusion_engine import DiffusionEngine
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
from vllm_omni.diffusion.diffusion_kv.metadata import DiffusionKVMetadata
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.comm import RingComm, SeqAllToAll4D
from vllm_omni.diffusion.distributed.parallel_state import (
@@ -306,11 +308,14 @@ def _make_vllm_config():
)
def _make_runner():
def _make_runner(
diffusion_kv_mode: DiffusionKVCacheMode = DiffusionKVCacheMode.DENSE_LEGACY,
):
runner = object.__new__(DiffusionModelRunner)
runner.vllm_config = _make_vllm_config()
runner.od_config = SimpleNamespace(
cache_backend=None,
diffusion_kv_mode=diffusion_kv_mode,
parallel_config=SimpleNamespace(use_hsdp=False),
streaming_output=False,
)
@@ -330,6 +335,7 @@ def _make_distributed_runner(mode: str, device: torch.device):
runner.vllm_config = _make_vllm_config()
runner.od_config = SimpleNamespace(
cache_backend=None,
diffusion_kv_mode=DiffusionKVCacheMode.DENSE_LEGACY,
parallel_config=SimpleNamespace(use_hsdp=False),
streaming_output=False,
)
@@ -502,6 +508,81 @@ def test_step_profiler_reports_denoise_step_as_diffuse():
class TestRunner:
"""DiffusionModelRunner.execute_stepwise"""
@pytest.mark.parametrize(
("diffusion_kv_mode", "should_cleanup"),
[
(DiffusionKVCacheMode.DENSE_LEGACY, False),
(DiffusionKVCacheMode.PAGED_SCHEDULER, True),
],
)
def test_scheduler_finished_cleanup_only_runs_for_paged_kv(
self,
mocker: MockerFixture,
diffusion_kv_mode: DiffusionKVCacheMode,
should_cleanup: bool,
):
runner = _make_runner(diffusion_kv_mode)
cleanup = mocker.patch.object(runner, "remove_diffusion_kv_requests")
expected_output = object()
mocker.patch.object(runner, "_execute_stepwise_core", return_value=expected_output)
scheduler_output = DiffusionSchedulerOutput(
step_id=1,
scheduled_new_reqs=[],
scheduled_cached_reqs=CachedRequestData.make_empty(),
finished_req_ids={"finished-request"},
num_running_reqs=0,
num_waiting_reqs=0,
)
output = DiffusionModelRunner.execute_stepwise(runner, scheduler_output)
assert output is expected_output
if should_cleanup:
cleanup.assert_called_once_with(["finished-request"])
else:
cleanup.assert_not_called()
@pytest.mark.parametrize(
("diffusion_kv_mode", "should_cleanup"),
[
(DiffusionKVCacheMode.DENSE_LEGACY, False),
(DiffusionKVCacheMode.PAGED_SCHEDULER, True),
],
)
def test_terminal_cleanup_only_runs_for_paged_kv(
self,
monkeypatch: pytest.MonkeyPatch,
mocker: MockerFixture,
diffusion_kv_mode: DiffusionKVCacheMode,
should_cleanup: bool,
):
runner = _make_runner(diffusion_kv_mode)
cleanup = mocker.patch.object(runner, "remove_diffusion_kv_requests")
install = mocker.patch.object(runner, "install_diffusion_kv_metadata")
req = _make_step_request()
scheduler_output = _make_scheduler_output(req, step_id=0)
if should_cleanup:
scheduler_output.scheduled_new_reqs[0].diffusion_kv_metadata = DiffusionKVMetadata(
request_id=req.request_id,
allocation_generation=1,
sequences=(),
)
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
first_output = DiffusionModelRunner.execute_stepwise(runner, scheduler_output)
assert first_output.get_request_output(req.request_id).finished is False
cleanup.assert_not_called()
output = DiffusionModelRunner.execute_stepwise(runner, _make_cached_scheduler_output(step_id=1))
assert output.get_request_output(req.request_id).finished is True
if should_cleanup:
install.assert_called_once_with(scheduler_output.scheduled_new_reqs[0].diffusion_kv_metadata)
cleanup.assert_called_once_with([req.request_id])
else:
install.assert_not_called()
cleanup.assert_not_called()
def test_completes_request_and_clears_state(self, monkeypatch):
runner = _make_runner()
req = _make_step_request()
+6
View File
@@ -678,6 +678,7 @@ class _DiffusionConfigProjection:
cache_config: Any = field(default_factory=dict)
enable_cache_dit_summary: bool = False
diffusion_kv_mode: DiffusionKVCacheMode = DiffusionKVCacheMode.DENSE_LEGACY
diffusion_kv_max_rows_per_request: int | None = Field(default=None, ge=1, strict=True)
enable_prompt_embed_cache: bool = False
prompt_embed_cache_size: int = Field(default=32, ge=1)
enable_session_state_manager: bool = False
@@ -816,6 +817,11 @@ class _DiffusionConfigProjection:
)
self.diffusion_kv_mode = parse_diffusion_kv_cache_mode(self.diffusion_kv_mode)
if (
self.diffusion_kv_mode is DiffusionKVCacheMode.PAGED_SCHEDULER
and self.diffusion_kv_max_rows_per_request is None
):
raise ValueError("paged_scheduler requires diffusion_kv_max_rows_per_request to be set")
self.diffusion_kv_cache_skip_step_indices = parse_kv_cache_skip_selector(self.diffusion_kv_cache_skip_steps)
self.diffusion_kv_cache_skip_layer_indices = parse_kv_cache_skip_selector(self.diffusion_kv_cache_skip_layers)
@@ -15,6 +15,11 @@ class AttentionBackend(ABC):
accept_output_buffer: bool = False
supports_piecewise_spans: bool = False
# A backend that supports this capability can consume the opaque paged-KV
# context prepared by the diffusion Worker data plane. Keeping the
# capability on the backend class prevents a paged request from silently
# falling back to dense attention on an incompatible implementation.
supports_paged_kv: bool = False
# The backend can represent a contiguous valid K/V prefix by slicing the
# tensors instead of materializing a padding mask. Models may use this to
# avoid a slower masked-attention plan when tail padding is not semantic.
@@ -221,6 +226,18 @@ class AttentionImpl(ABC, Generic[T]):
else:
raise NotImplementedError(f"No forward implementation for platform: {current_omni_platform}")
def forward_paged(self, paged_kv_context: Any) -> torch.Tensor:
"""Execute one Worker-prepared paged-KV attention call.
The context is intentionally opaque to the common attention layer.
Backends opt in by setting ``supports_paged_kv`` on their backend
class and implementing this method. Dense callers continue to use
``forward`` unchanged.
"""
del paged_kv_context
raise NotImplementedError(f"{type(self).__name__} does not support paged KV attention")
def forward_cuda(
self,
query: torch.Tensor,
@@ -15,6 +15,7 @@ from vllm_omni.diffusion.attention.backends.abstract import (
from vllm_omni.diffusion.attention.backends.sdpa import _maybe_reshape_attn_mask
from vllm_omni.diffusion.attention.backends.utils.piecewise_attn import (
piecewise_attn,
run_paged_piecewise_plan,
)
from vllm_omni.diffusion.config import get_current_diffusion_config_or_none
from vllm_omni.platforms import current_omni_platform
@@ -25,6 +26,7 @@ logger = init_logger(__name__)
class FlashAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
supports_piecewise_spans: bool = True
supports_paged_kv: bool = True
@classmethod
def supports_packed_mask_free(cls) -> bool:
@@ -246,6 +248,73 @@ class FlashAttentionImpl(AttentionImpl):
# (b s) h d -> b s h d
return out.reshape(batch_size, q_len, *out.shape[1:])
def forward_paged(self, paged_kv_context) -> torch.Tensor:
"""Run platform-native paged attention through the Omni backend.
``DiffusionPagedAttentionAdapter`` owns BlockTables and prepares the
rank-local native cache context. The adapter no longer performs the
attention call itself; this method is the backend-owned execution
boundary. Its native layer wrapper keeps vLLM version-specific cache
and kernel details out of Omni's common ``Attention`` layer.
"""
layer = getattr(paged_kv_context, "layer", None)
if layer is None:
raise TypeError("paged_kv_context must expose a native diffusion attention layer")
kv_cache = getattr(layer, "kv_cache", None)
if kv_cache is None:
raise RuntimeError(f"Native KV cache is not bound for diffusion layer {layer.layer_name!r}")
native_impl = getattr(layer, "impl", None)
if native_impl is None:
raise RuntimeError(f"Native attention implementation is not bound for diffusion layer {layer.layer_name!r}")
if not layer.attn_backend.forward_includes_kv_cache_update:
native_impl.do_kv_cache_update(
layer,
paged_kv_context.key_write,
paged_kv_context.value_write,
kv_cache,
paged_kv_context.slot_mapping,
)
def run_native_attention(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
native_metadata,
) -> torch.Tensor:
output = torch.empty(
(query.shape[0], layer.num_heads, layer.head_size_v),
dtype=query.dtype,
device=query.device,
)
return native_impl.forward(
layer,
query,
key,
value,
kv_cache,
native_metadata,
output,
)
if paged_kv_context.piecewise_plan is not None:
output = run_paged_piecewise_plan(
paged_kv_context.query,
paged_kv_context.key_write,
paged_kv_context.value_write,
paged_kv_context.piecewise_plan,
paged_kv_context.piecewise_native_metadata,
run_native_attention,
)
else:
output = run_native_attention(
paged_kv_context.query,
paged_kv_context.key_write,
paged_kv_context.value_write,
paged_kv_context.native_metadata,
)
return paged_kv_context.restore_output(output)
def forward_cuda(
self,
query: torch.Tensor,
@@ -15,6 +15,8 @@ Per segment:
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal, NamedTuple
import torch
@@ -123,3 +125,109 @@ def piecewise_attn(
if len(outputs) == 1:
return outputs[0]
return torch.cat(outputs, dim=1)
@dataclass(frozen=True, slots=True)
class PagedPiecewiseSegment:
"""One aligned segment across rows in a packed paged batch."""
row_segments: tuple[Segment, ...]
query_indices: torch.Tensor
@dataclass(frozen=True, slots=True)
class PagedPiecewisePlan:
"""Piecewise segment batches for one packed paged-attention batch."""
spans: tuple[tuple[tuple[int, int], ...], ...]
segments: tuple[PagedPiecewiseSegment, ...]
num_query_tokens: int
PagedPiecewiseRunner = Callable[
[torch.Tensor, torch.Tensor, torch.Tensor, object],
torch.Tensor,
]
def build_paged_piecewise_plan(
full_attn_spans: Sequence[Sequence[tuple[int, int]]],
query_offsets: Sequence[int],
query_lens: Sequence[int],
seq_lens: Sequence[int],
*,
device: torch.device | str | None = None,
) -> PagedPiecewisePlan:
"""Build packed indices for corresponding piecewise segments in each row."""
row_count = len(full_attn_spans)
if row_count == 0 or not (row_count == len(query_offsets) == len(query_lens) == len(seq_lens)):
raise ValueError("Paged piecewise inputs must have one entry per row")
spans = tuple(tuple(tuple(span) for span in row_spans) for row_spans in full_attn_spans)
packed_offsets = [0]
for row_spans, query_offset, query_len, seq_len in zip(spans, query_offsets, query_lens, seq_lens, strict=True):
previous_end = 0
for start, end in row_spans:
if start < previous_end or not 0 <= start < end <= seq_len:
raise ValueError("Paged piecewise spans must be sorted, non-overlapping, and within the sequence")
previous_end = end
if query_offset < 0 or query_len <= 0 or query_offset + query_len > seq_len:
raise ValueError("Paged piecewise query range must be within the sequence")
packed_offsets.append(packed_offsets[-1] + query_len)
segments_by_row = tuple(
tuple(build_segments(row_spans, query_offset, query_len))
for row_spans, query_offset, query_len in zip(spans, query_offsets, query_lens, strict=True)
)
if len({len(row_segments) for row_segments in segments_by_row}) != 1:
raise ValueError("Paged piecewise rows must produce aligned segments")
packed_segments = []
for row_segments in zip(*segments_by_row, strict=True):
if len({segment.mode for segment in row_segments}) != 1:
raise ValueError("Paged piecewise rows must use the same attention mode per segment")
query_indices = []
for row_index, (segment, query_offset) in enumerate(zip(row_segments, query_offsets, strict=True)):
local_start = segment.q_start - query_offset
local_end = segment.q_end - query_offset
query_indices.extend(range(packed_offsets[row_index] + local_start, packed_offsets[row_index] + local_end))
packed_segments.append(
PagedPiecewiseSegment(
row_segments=tuple(row_segments),
query_indices=torch.tensor(query_indices, dtype=torch.long, device=device),
)
)
return PagedPiecewisePlan(spans, tuple(packed_segments), packed_offsets[-1])
def run_paged_piecewise_plan(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
plan: PagedPiecewisePlan,
segment_metadata: Sequence[object],
segment_runner: PagedPiecewiseRunner,
) -> torch.Tensor:
"""Run and scatter piecewise attention over packed Q/K/V write tensors."""
if query.shape[0] != plan.num_query_tokens:
raise ValueError(f"Paged piecewise plan has {plan.num_query_tokens} query tokens, got {query.shape[0]}")
output = None
for segment, metadata in zip(plan.segments, segment_metadata, strict=True):
indices = segment.query_indices
segment_output = segment_runner(
query.index_select(0, indices),
key.index_select(0, indices),
value.index_select(0, indices),
metadata,
)
if output is None:
output = segment_output.new_empty((query.shape[0], *segment_output.shape[1:]))
output.index_copy_(0, indices, segment_output)
assert output is not None
return output
+58 -7
View File
@@ -24,7 +24,11 @@ from vllm_omni.diffusion.attention.parallel.ring import RingParallelAttention
from vllm_omni.diffusion.attention.selector import get_attn_backend_for_role
from vllm_omni.diffusion.config import get_current_diffusion_config_or_none
from vllm_omni.diffusion.distributed.parallel_state import get_sp_group
from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available
from vllm_omni.diffusion.forward_context import (
get_forward_context,
get_ulysses_mode,
is_forward_context_available,
)
from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
@@ -73,6 +77,11 @@ class Attention(nn.Module):
self.role = role
self.role_category = role_category
self.qkv_layout = qkv_layout
# ``prefix`` is also the stable layer identity used by vLLM's native
# KV-cache metadata. Keep it on the Omni layer so the active paged
# adapter can dispatch the already-resharded Q/K/V to the matching
# native cache tensor without replacing this Omni execution path.
self.prefix = prefix
if paged_kv_cache_role == "":
raise ValueError("paged_kv_cache_role must be non-empty when provided")
self.paged_kv_cache_role = paged_kv_cache_role
@@ -311,19 +320,48 @@ class Attention(nn.Module):
) -> torch.Tensor:
# Get the appropriate parallel strategy based on SP active state
strategy = self._get_active_parallel_strategy()
paged_adapter = self._active_paged_kv_adapter()
use_paged_attention = paged_adapter is not None and self.paged_kv_cache_role is not None
if use_paged_attention and not getattr(self.attn_backend, "supports_paged_kv", False):
raise NotImplementedError(
f"Diffusion paged KV requires an Omni backend with paged support; "
f"selected {self.attn_backend.get_name()}"
)
if use_paged_attention and strategy is not self._no_parallel_strategy:
strategy_name = strategy.name
if self.use_ring or strategy_name == "ring":
raise NotImplementedError(
"paged Scheduler KV is not supported with Ring attention; use strict Ulysses or no SP"
)
if strategy_name == "allgather_kv":
raise NotImplementedError("paged Scheduler KV is not supported with AllGather-KV sequence parallelism")
if strategy_name == "ulysses" and get_ulysses_mode(default="strict") != "strict":
raise NotImplementedError("paged Scheduler KV currently supports only strict Ulysses")
# 1. Prepare inputs (Communication / Resharding)
# For Ulysses: AllToAll Q/K/V; Slicing joint_q/k/v
# For Ring: Concat joint_q
query, key, value, attn_metadata, ctx = strategy.pre_attention(query, key, value, attn_metadata)
attn_metadata = self._with_kv_cache_dtype(attn_metadata)
# 2. Kernel Execution (Computation)
if self.use_ring and strategy is not self._no_parallel_strategy:
out = self._run_ring_attention(query, key, value, attn_metadata)
# 2. Kernel execution stays inside the selected Omni backend. The
# Worker adapter only prepares the native page-table context after
# SP has produced rank-local Q/K/V tensors.
if use_paged_attention:
assert paged_adapter is not None
paged_kv_context = paged_adapter.prepare_layer_context(
self.prefix,
query,
key,
value,
omni_attn_metadata=attn_metadata,
)
out = self.attention.forward_paged(paged_kv_context)
else:
out = self._run_local_attention(query, key, value, attn_metadata)
attn_metadata = self._with_kv_cache_dtype(attn_metadata)
if self.use_ring and strategy is not self._no_parallel_strategy:
out = self._run_ring_attention(query, key, value, attn_metadata)
else:
out = self._run_local_attention(query, key, value, attn_metadata)
# 3. Post-processing (Reverse Communication)
# For Ulysses: AllToAll Output, and AllGather Joint Output
@@ -331,6 +369,19 @@ class Attention(nn.Module):
return out
@staticmethod
def _active_paged_kv_adapter():
"""Return the opaque Worker adapter installed for this forward."""
if not is_forward_context_available():
return None
return getattr(get_forward_context(), "paged_kv_adapter", None)
def is_paged_kv_active(self) -> bool:
"""Return whether this layer will use Scheduler-managed paged KV."""
return self.paged_kv_cache_role is not None and self._active_paged_kv_adapter() is not None
def _run_local_attention(self, query, key, value, attn_metadata):
self._assert_piecewise_compatible(attn_metadata)
+12
View File
@@ -714,6 +714,9 @@ class OmniDiffusionConfig:
# Local Diffusion KV ownership and cache-layout mode.
diffusion_kv_mode: DiffusionKVCacheMode = DiffusionKVCacheMode.DENSE_LEGACY
# Maximum number of native BlockTable rows one public request can own
# (sequences plus independent contexts). The model adapter defines it.
diffusion_kv_max_rows_per_request: int | None = None
# Optional override for the diffusion model runner class (import path).
# Precedence in the worker: this override > the runner declared by the
@@ -990,6 +993,15 @@ class OmniDiffusionConfig:
if not isinstance(self.diffusion_compile_dynamic, bool):
raise TypeError(f"diffusion_compile_dynamic must be a bool, got {type(self.diffusion_compile_dynamic)!r}")
self.diffusion_kv_mode = parse_diffusion_kv_cache_mode(self.diffusion_kv_mode)
if self.diffusion_kv_max_rows_per_request is not None and (
type(self.diffusion_kv_max_rows_per_request) is not int or self.diffusion_kv_max_rows_per_request <= 0
):
raise ValueError("diffusion_kv_max_rows_per_request must be a positive integer when set")
if (
self.diffusion_kv_mode is DiffusionKVCacheMode.PAGED_SCHEDULER
and self.diffusion_kv_max_rows_per_request is None
):
raise ValueError("paged_scheduler requires diffusion_kv_max_rows_per_request to be set")
if self.kv_cache_memory_bytes is not None and self.kv_cache_memory_bytes < 0:
raise ValueError("kv_cache_memory_bytes must be non-negative")
if not 0.0 < self.gpu_memory_utilization <= 1.0:
+22 -2
View File
@@ -29,7 +29,7 @@ from vllm_omni.diffusion.data import (
DiffusionRequestAbortedError,
OmniDiffusionConfig,
)
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode, is_scheduler_paged_kv_mode
from vllm_omni.diffusion.diffusion_kv.initialization import initialize_diffusion_kv_control_plane
from vllm_omni.diffusion.executor.abstract import DiffusionExecutor
from vllm_omni.diffusion.io_support import (
@@ -634,12 +634,25 @@ class DiffusionEngine:
if not task.future.done():
task.future.set_exception(exc)
def _remove_diffusion_kv_requests(self, request_ids: Iterable[str]) -> None:
"""Clear terminal Worker rows while Scheduler owns the allocations."""
od_config = getattr(self, "od_config", None)
if od_config is None or not is_scheduler_paged_kv_mode(
getattr(od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
):
return
unique_request_ids = list(dict.fromkeys(request_ids))
if unique_request_ids:
self.executor.remove_diffusion_kv_requests(unique_request_ids)
def _emit_finished_outputs(
self,
finished_ids: set[str],
runner_output: BaseRunnerOutput | None = None,
missing_result_error: str = "Diffusion execution finished without a final output",
) -> None:
self._remove_diffusion_kv_requests(finished_ids)
for rid in finished_ids:
if runner_output is not None:
_output = runner_output.get_request_output(rid)
@@ -659,6 +672,8 @@ class DiffusionEngine:
self._emit_finished_outputs(finished_ids, runner_output)
return
self._remove_diffusion_kv_requests(finished_ids)
delivered_finished_req_ids: set[str] = set()
# finished_ids may have some requests that are not scheduler in this round.
@@ -852,6 +867,7 @@ class DiffusionEngine:
sched_output = self.scheduler.schedule()
if sched_output.is_empty:
if target_request_id in sched_output.finished_req_ids:
self._remove_diffusion_kv_requests([target_request_id])
return self._finalize_finished_request(target_request_id)
if not self.scheduler.has_requests():
raise RuntimeError("Diffusion scheduler has no runnable requests.")
@@ -881,6 +897,7 @@ class DiffusionEngine:
if not isinstance(runner_output, RunnerOutput) and not len(runner_output) == 1:
raise ValueError("Sync func should receive one result at one time")
if target_request_id in finished_req_ids:
self._remove_diffusion_kv_requests([target_request_id])
req_output = runner_output.get_request_output(target_request_id)
output = self._finalize_finished_request(
target_request_id,
@@ -1232,8 +1249,11 @@ class DiffusionEngine:
def _abort_requests(self, request_ids: str | Iterable[str]) -> None:
request_ids = [request_ids] if isinstance(request_ids, str) else list(request_ids)
request_ids = list(dict.fromkeys(request_ids))
for request_id in dict.fromkeys(request_ids):
self._remove_diffusion_kv_requests(request_ids)
for request_id in request_ids:
if self.scheduler.get_request_state(request_id) is not None:
self.scheduler.finish_requests(request_id, DiffusionRequestStatus.FINISHED_ABORTED)
@@ -37,3 +37,8 @@ def parse_diffusion_kv_cache_mode(value: object) -> DiffusionKVCacheMode:
"use 'dense_legacy' or 'paged_scheduler'"
)
return mode
def is_scheduler_paged_kv_mode(mode: DiffusionKVCacheMode) -> bool:
"""Return whether a parsed cache mode uses Scheduler-owned paging."""
return mode is DiffusionKVCacheMode.PAGED_SCHEDULER
@@ -73,7 +73,13 @@ def initialize_diffusion_kv_control_plane(
scheduler_kv_cache_config,
vllm_config,
)
executor.set_kv_cache_configs(worker_configs)
# Native sizing may auto-fit ``max_model_len=-1`` against the profiled
# memory budget. Forward that final value so every Worker sizes its
# physical BlockTables from the same bound as the Scheduler.
resolved_max_model_len = vllm_config.model_config.max_model_len
if type(resolved_max_model_len) is not int or resolved_max_model_len <= 0:
raise ValueError("native Diffusion KV sizing must resolve a positive max_model_len")
executor.set_kv_cache_configs(worker_configs, resolved_max_model_len)
return (
scheduler_kv_cache_config,
scheduler_block_size,
@@ -0,0 +1,647 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import copy
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Any
import torch
from vllm.config import VllmConfig, set_current_vllm_config
from vllm.distributed import get_dcp_group
from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheConfig, KVCacheSpec
from vllm.v1.worker import block_table as native_block_table
from vllm.v1.worker.gpu.attn_utils import init_attn_backend, init_kv_cache
from vllm.v1.worker.gpu.block_table import BlockTables
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode, is_scheduler_paged_kv_mode
from vllm_omni.diffusion.diffusion_kv.metadata import DiffusionKVMetadata
from vllm_omni.diffusion.diffusion_kv.paged_attention_adapter import (
DiffusionPagedAttentionAdapter,
DiffusionPagedAttentionLayerAdapter,
DiffusionPagedAttentionRow,
DiffusionPagedAttentionRowBinding,
PreparedDiffusionPagedAttentionBatch,
)
from vllm_omni.platforms import current_omni_platform
DiffusionKVIdentity = tuple[str, int | None, str | None]
DiffusionKVSnapshot = tuple[object, ...]
@dataclass(frozen=True)
class _DiffusionKVRequestState:
generation: int
snapshot: DiffusionKVSnapshot
row_token_lens: tuple[tuple[DiffusionKVIdentity, int], ...]
@dataclass(frozen=True)
class _DiffusionKVRowInstall:
identity: DiffusionKVIdentity
token_len: int
block_ids: tuple[tuple[int, ...], ...]
class DiffusionKVModelRunnerBackend:
"""Native paged-KV state and BlockTable operations for a model runner."""
def __init__(
self,
*,
vllm_config: VllmConfig,
od_config: OmniDiffusionConfig,
device: torch.device,
) -> None:
self.vllm_config = vllm_config
self.od_config = od_config
self.device = device
self.kv_cache_config: KVCacheConfig | None = None
self.kv_caches: list[torch.Tensor | list[torch.Tensor]] = []
self.block_tables: BlockTables | None = None
self.paged_attention_adapter: DiffusionPagedAttentionAdapter | None = None
self._kv_cache_layer_adapters: dict[str, DiffusionPagedAttentionLayerAdapter] = {}
self._diffusion_kv_identity_to_row: dict[DiffusionKVIdentity, int] = {}
self._diffusion_kv_free_rows: list[int] = []
self._diffusion_kv_request_states: dict[str, _DiffusionKVRequestState] = {}
def register_kv_cache_layers(
self,
layers: Mapping[str, tuple[Any, KVCacheSpec]],
) -> dict[str, KVCacheSpec]:
"""Register native adapters and return their canonical cache specs."""
if self.kv_cache_config is not None:
raise RuntimeError("Diffusion KV cache layers cannot be changed after physical initialization")
if layers:
current_omni_platform.get_diffusion_kv_block_tables_cls()
if not callable(current_omni_platform.build_diffusion_kv_attn_metadata):
raise NotImplementedError(
"Diffusion paged KV requires platform-native BlockTables and attention metadata hooks"
)
forward_context = self.vllm_config.compilation_config.static_forward_context
previous_adapters = self._kv_cache_layer_adapters
for layer_name, (layer, spec) in layers.items():
if not isinstance(spec, AttentionSpec):
raise TypeError(
f"Diffusion KV layer {layer_name!r} produced unsupported spec {type(spec).__name__}; "
"only native attention specs are supported"
)
if not layer.attn_backend.supports_paged_kv:
raise NotImplementedError(
f"Diffusion paged KV layer {layer_name!r} requires an Omni backend with paged support; "
f"selected {layer.attn_backend.get_name()}"
)
existing = forward_context.get(layer_name)
if existing is not None and existing is not layer and existing is not previous_adapters.get(layer_name):
raise RuntimeError(f"Diffusion KV layer name {layer_name!r} collides with the native forward context")
previous_use_non_causal = self.vllm_config.attention_config.use_non_causal
self.vllm_config.attention_config.use_non_causal = any(
bool(getattr(spec, "non_causal", False)) for _, spec in layers.values()
)
adapters: dict[str, DiffusionPagedAttentionLayerAdapter] = {}
try:
with set_current_vllm_config(self.vllm_config):
for layer_name, (layer, spec) in layers.items():
adapters[layer_name] = DiffusionPagedAttentionLayerAdapter(
layer_name=layer_name,
layer=layer,
spec=spec,
vllm_config=self.vllm_config,
device=self.device,
ulysses_degree=self._get_paged_attention_ulysses_degree(layer),
)
except Exception:
self.vllm_config.attention_config.use_non_causal = previous_use_non_causal
raise
geometries = {(adapter.num_heads, adapter.num_kv_heads, adapter.head_size) for adapter in adapters.values()}
if len(geometries) > 1:
self.vllm_config.attention_config.use_non_causal = previous_use_non_causal
raise ValueError(
"Native paged attention metadata builders require one rank-local attention geometry; "
f"got {sorted(geometries)!r}"
)
attention_geometry = next(iter(geometries)) if geometries else None
set_attention_geometry = (
getattr(self.vllm_config.model_config, "set_attention_geometry", None)
if attention_geometry is not None
else None
)
if attention_geometry is not None and not callable(set_attention_geometry):
self.vllm_config.attention_config.use_non_causal = previous_use_non_causal
raise RuntimeError(
"Diffusion native paged attention requires a model config that exposes set_attention_geometry"
)
updated_forward_context = dict(forward_context)
for layer_name in set(layers) | set(previous_adapters):
entry = updated_forward_context.get(layer_name)
layer_entry = layers.get(layer_name)
if entry is previous_adapters.get(layer_name) or (layer_entry is not None and entry is layer_entry[0]):
updated_forward_context.pop(layer_name, None)
updated_forward_context.update(adapters)
if attention_geometry is not None:
num_heads, num_kv_heads, head_size = attention_geometry
try:
set_attention_geometry(
num_heads=num_heads,
num_kv_heads=num_kv_heads,
head_size=head_size,
)
except Exception:
self.vllm_config.attention_config.use_non_causal = previous_use_non_causal
raise
forward_context.clear()
forward_context.update(updated_forward_context)
self._kv_cache_layer_adapters = adapters
return {layer_name: adapter.spec for layer_name, adapter in adapters.items()}
def _get_paged_attention_ulysses_degree(self, layer: Any) -> int:
if bool(getattr(layer, "skip_sequence_parallel", False)):
return 1
parallel_config = getattr(self.od_config, "parallel_config", None)
ulysses_degree = int(getattr(parallel_config, "ulysses_degree", 1))
ring_degree = int(getattr(parallel_config, "ring_degree", 1))
allgather_degree = int(getattr(parallel_config, "allgather_degree", 1))
ulysses_mode = str(getattr(parallel_config, "ulysses_mode", "strict"))
if ring_degree > 1:
raise NotImplementedError(
"Diffusion paged attention does not support Ring or hybrid Ulysses+Ring attention"
)
if allgather_degree > 1:
raise NotImplementedError("Diffusion paged attention does not support AllGather-KV sequence parallelism")
if ulysses_degree > 1 and ulysses_mode != "strict":
raise NotImplementedError(
f"Diffusion paged attention currently supports only strict Ulysses; got ulysses_mode={ulysses_mode!r}"
)
return ulysses_degree
def _get_max_rows_per_request(self) -> int:
max_rows = getattr(self.od_config, "diffusion_kv_max_rows_per_request", None)
if type(max_rows) is not int or max_rows <= 0:
raise ValueError(
"paged_scheduler requires a positive integer diffusion_kv_max_rows_per_request adapter limit"
)
return max_rows
def _get_max_model_len(self) -> int:
max_len = getattr(self.vllm_config.model_config, "max_model_len", None)
if type(max_len) is not int or max_len <= 0:
raise ValueError("paged_scheduler requires a positive model_config.max_model_len")
return max_len
def initialize_kv_cache(self, kv_cache_config: KVCacheConfig) -> None:
"""Allocate and bind native attention KV tensors for this rank."""
if self.kv_cache_config is not None:
raise RuntimeError("Diffusion native KV cache is already initialized")
if not self._kv_cache_layer_adapters:
raise RuntimeError("Diffusion KV cache layers must be registered before physical initialization")
configured_layers = {
layer_name for group in kv_cache_config.kv_cache_groups for layer_name in group.layer_names
}
registered_layers = set(self._kv_cache_layer_adapters)
if configured_layers != registered_layers:
raise ValueError(
"Rank-local Diffusion KVCacheConfig layer mismatch: "
f"expected={sorted(registered_layers)}, configured={sorted(configured_layers)}"
)
kv_cache_config = copy.deepcopy(kv_cache_config)
max_len = self._get_max_model_len()
max_rows_per_request = self._get_max_rows_per_request()
scheduler_config = self.vllm_config.scheduler_config
max_num_seqs = getattr(scheduler_config, "max_num_seqs", None)
if type(max_num_seqs) is not int or max_num_seqs <= 0:
raise ValueError("paged_scheduler requires a positive scheduler_config.max_num_seqs")
kv_cache_groups = kv_cache_config.kv_cache_groups
if not kv_cache_groups:
raise ValueError("paged_scheduler requires at least one native KV cache group")
block_sizes = [group.kv_cache_spec.block_size for group in kv_cache_groups]
if any(type(block_size) is not int or block_size <= 0 for block_size in block_sizes):
raise ValueError("native diffusion KV cache groups must have positive integer block sizes")
unaligned_max_num_blocks_per_group = [
group.kv_cache_spec.max_num_blocks_per_req(self.vllm_config, max_len) for group in kv_cache_groups
]
if any(type(capacity) is not int or capacity <= 0 for capacity in unaligned_max_num_blocks_per_group):
raise ValueError("native diffusion KV cache groups must have positive row capacities")
max_num_blocks_per_group = [
native_block_table.get_block_table_width(capacity, block_size)
for capacity, block_size in zip(unaligned_max_num_blocks_per_group, block_sizes, strict=True)
]
max_num_reqs = max_num_seqs * max_rows_per_request
max_num_batched_tokens = getattr(scheduler_config, "max_num_batched_tokens", None)
if type(max_num_batched_tokens) is not int or max_num_batched_tokens <= 0:
raise ValueError("scheduler_config.max_num_batched_tokens must be a positive integer")
parallel_config = self.vllm_config.parallel_config
cp_size = parallel_config.decode_context_parallel_size
cp_rank = get_dcp_group().rank_in_group if cp_size > 1 else 0
cp_interleave = parallel_config.cp_kv_cache_interleave_size
# Native metadata builders size per-row buffers from max_num_seqs.
# A diffusion public request can occupy several sequence/context rows.
scheduler_config.max_num_seqs = max_num_reqs
kv_caches: list[torch.Tensor | list[torch.Tensor]] = []
previous_adapter_caches = {
layer_name: adapter.kv_cache for layer_name, adapter in self._kv_cache_layer_adapters.items()
}
try:
with set_current_vllm_config(self.vllm_config):
attn_groups, _, kernel_block_sizes = init_attn_backend(
kv_cache_config,
self.vllm_config,
self.device,
)
block_tables_cls = current_omni_platform.get_diffusion_kv_block_tables_cls()
block_tables = block_tables_cls(
block_sizes=block_sizes,
max_num_reqs=max_num_reqs,
max_num_batched_tokens=max_num_batched_tokens,
max_num_blocks_per_group=max_num_blocks_per_group,
device=self.device,
kernel_block_sizes=kernel_block_sizes,
cp_size=cp_size,
cp_rank=cp_rank,
cp_interleave=cp_interleave,
)
paged_attention_adapter = DiffusionPagedAttentionAdapter(
vllm_config=self.vllm_config,
device=self.device,
kv_cache_config=kv_cache_config,
block_tables=block_tables,
attn_groups=attn_groups,
layers=self._kv_cache_layer_adapters,
resolve_row=self._resolve_paged_attention_row,
)
init_kv_cache(
kv_caches,
self.vllm_config.compilation_config.static_forward_context,
kv_cache_config,
attn_groups,
self.device,
self.vllm_config.cache_config.cache_dtype,
kernel_block_sizes,
self.vllm_config,
)
except Exception:
scheduler_config.max_num_seqs = max_num_seqs
for layer_name, previous_cache in previous_adapter_caches.items():
self._kv_cache_layer_adapters[layer_name].kv_cache = previous_cache
raise
self.kv_caches = kv_caches
self.block_tables = block_tables
self.paged_attention_adapter = paged_attention_adapter
self._diffusion_kv_identity_to_row.clear()
self._diffusion_kv_free_rows = list(range(max_num_reqs - 1, -1, -1))
self._diffusion_kv_request_states.clear()
self.kv_cache_config = kv_cache_config
def _validate_row(
self,
*,
identity: DiffusionKVIdentity,
token_len: int,
block_ids: tuple[list[int], ...],
seen_identities: set[DiffusionKVIdentity],
) -> _DiffusionKVRowInstall:
if identity in seen_identities:
raise ValueError(f"Duplicate diffusion KV row identity: {identity!r}")
seen_identities.add(identity)
if type(token_len) is not int or token_len < 0:
raise ValueError(f"Diffusion KV row {identity!r} has invalid token length {token_len!r}")
assert self.kv_cache_config is not None
groups = self.kv_cache_config.kv_cache_groups
if not isinstance(block_ids, tuple) or len(block_ids) != len(groups):
actual_groups = len(block_ids) if isinstance(block_ids, (tuple, list)) else "invalid"
raise ValueError(f"Diffusion KV row {identity!r} has {actual_groups} block groups; expected {len(groups)}")
max_len = self._get_max_model_len()
normalized_groups: list[tuple[int, ...]] = []
for group_index, (group_block_ids, kv_cache_group) in enumerate(zip(block_ids, groups, strict=True)):
if not isinstance(group_block_ids, list):
raise ValueError(f"Diffusion KV row {identity!r} group {group_index} block IDs must be a list")
spec = kv_cache_group.kv_cache_spec
expected_count = spec.max_num_blocks_per_req(self.vllm_config, token_len)
if len(group_block_ids) != expected_count:
raise ValueError(
f"Diffusion KV row {identity!r} group {group_index} has {len(group_block_ids)} blocks; "
f"expected {expected_count} for {token_len} tokens"
)
row_capacity = spec.max_num_blocks_per_req(self.vllm_config, max_len)
if len(group_block_ids) > row_capacity:
raise ValueError(
f"Diffusion KV row {identity!r} group {group_index} exceeds row capacity {row_capacity}"
)
normalized_ids: list[int] = []
for block_id in group_block_ids:
if type(block_id) is not int:
raise ValueError(
f"Diffusion KV row {identity!r} group {group_index} has non-integer block ID {block_id!r}"
)
if not 0 <= block_id < self.kv_cache_config.num_blocks:
raise ValueError(
f"Diffusion KV row {identity!r} group {group_index} block ID {block_id} is outside "
f"[0, {self.kv_cache_config.num_blocks})"
)
normalized_ids.append(block_id)
normalized_groups.append(tuple(normalized_ids))
return _DiffusionKVRowInstall(
identity=identity,
token_len=token_len,
block_ids=tuple(normalized_groups),
)
def _prepare_install(
self,
metadata: DiffusionKVMetadata,
) -> tuple[list[_DiffusionKVRowInstall], DiffusionKVSnapshot]:
if self.block_tables is None or self.kv_cache_config is None:
raise RuntimeError("paged_scheduler native KV cache is not initialized")
if type(metadata.request_id) is not str or not metadata.request_id:
raise ValueError("Diffusion KV metadata request_id must be a non-empty string")
if type(metadata.allocation_generation) is not int or metadata.allocation_generation < 0:
raise ValueError("Diffusion KV allocation_generation must be a non-negative integer")
if type(self.kv_cache_config.num_blocks) is not int or self.kv_cache_config.num_blocks <= 0:
raise ValueError("native diffusion KV cache config must provide a positive num_blocks")
if not metadata.sequences:
raise ValueError(f"Diffusion KV request {metadata.request_id!r} must contain at least one sequence")
seen_identities: set[DiffusionKVIdentity] = set()
installs: list[_DiffusionKVRowInstall] = []
sequence_snapshots: list[object] = []
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}")
sequence_identity = (metadata.request_id, sequence.sequence_id, None)
sequence_install = self._validate_row(
identity=sequence_identity,
token_len=sequence.seq_len,
block_ids=sequence.block_ids,
seen_identities=seen_identities,
)
installs.append(sequence_install)
sequence_snapshots.append(
(
sequence.sequence_id,
sequence.prefix_len,
sequence.target_len,
sequence.seq_len,
sequence_install.block_ids,
sequence.context_ids,
)
)
context_snapshots: list[object] = []
for context in metadata.contexts:
if type(context.context_id) is not str or not context.context_id:
raise ValueError("Diffusion KV context_id must be a non-empty string")
context_identity = (metadata.request_id, None, context.context_id)
context_install = self._validate_row(
identity=context_identity,
token_len=context.num_tokens,
block_ids=context.block_ids,
seen_identities=seen_identities,
)
installs.append(context_install)
context_snapshots.append(
(
context.context_id,
context.cache_role,
context.num_tokens,
context_install.block_ids,
)
)
max_rows_per_request = self._get_max_rows_per_request()
if len(installs) > max_rows_per_request:
raise ValueError(
f"Diffusion KV request {metadata.request_id!r} requires {len(installs)} rows; "
f"adapter limit is {max_rows_per_request}"
)
snapshot: DiffusionKVSnapshot = (
metadata.request_id,
tuple(sequence_snapshots),
tuple(context_snapshots),
)
return installs, snapshot
def _rollback_staged_writes(self, rows: list[int], previous_num_blocks: object | None) -> None:
block_tables = self.block_tables
if block_tables is None:
return
for group_table in getattr(block_tables, "block_tables", ()):
clear_staged_writes = getattr(group_table, "clear_staged_writes", None)
if clear_staged_writes is not None:
clear_staged_writes()
if previous_num_blocks is None:
return
native_num_blocks = getattr(block_tables, "num_blocks", None)
if native_num_blocks is None or not hasattr(native_num_blocks, "np"):
return
native_num_blocks.np[:, rows] = previous_num_blocks
copy_to_uva = getattr(native_num_blocks, "copy_to_uva", None)
if copy_to_uva is not None:
copy_to_uva()
def _apply_rows(self, rows: list[int], installs: list[_DiffusionKVRowInstall]) -> None:
assert self.block_tables is not None
if self.paged_attention_adapter is not None:
self.paged_attention_adapter.invalidate_prepared_batches()
native_num_blocks = getattr(self.block_tables, "num_blocks", None)
previous_num_blocks = None
if native_num_blocks is not None and hasattr(native_num_blocks, "np"):
previous_num_blocks = native_num_blocks.np[:, rows].copy()
try:
for row, install in zip(rows, installs, strict=True):
self.block_tables.append_block_ids(
row,
tuple(list(group_ids) for group_ids in install.block_ids),
overwrite=True,
)
self.block_tables.apply_staged_writes()
except Exception:
self._rollback_staged_writes(rows, previous_num_blocks)
raise
def install_diffusion_kv_metadata(self, metadata: DiffusionKVMetadata) -> bool:
"""Install one Scheduler allocation snapshot into native Worker rows."""
if not is_scheduler_paged_kv_mode(
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
):
raise ValueError("Dense diffusion execution must not install Diffusion KV metadata")
installs, snapshot = self._prepare_install(metadata)
current_state = self._diffusion_kv_request_states.get(metadata.request_id)
if current_state is not None:
if metadata.allocation_generation < current_state.generation:
raise ValueError(
f"Stale Diffusion KV allocation generation {metadata.allocation_generation} for request "
f"{metadata.request_id!r}; installed generation is {current_state.generation}"
)
if snapshot != current_state.snapshot:
raise ValueError(
f"Conflicting Diffusion KV allocation snapshot for active request {metadata.request_id!r}; "
"remove the request before replacing its rows"
)
if metadata.allocation_generation == current_state.generation:
return False
self._diffusion_kv_request_states[metadata.request_id] = _DiffusionKVRequestState(
generation=metadata.allocation_generation,
snapshot=snapshot,
row_token_lens=current_state.row_token_lens,
)
return False
existing_identities = [
install.identity for install in installs if install.identity in self._diffusion_kv_identity_to_row
]
if existing_identities:
raise RuntimeError(f"Diffusion KV row registry contains orphaned identities: {existing_identities!r}")
if len(installs) > len(self._diffusion_kv_free_rows):
raise ValueError(
f"Diffusion KV request {metadata.request_id!r} requires {len(installs)} rows, but only "
f"{len(self._diffusion_kv_free_rows)} rows are free"
)
rows = list(reversed(self._diffusion_kv_free_rows[-len(installs) :])) if installs else []
if rows:
self._apply_rows(rows, installs)
if installs:
del self._diffusion_kv_free_rows[-len(installs) :]
self._diffusion_kv_identity_to_row.update(
(install.identity, row) for install, row in zip(installs, rows, strict=True)
)
self._diffusion_kv_request_states[metadata.request_id] = _DiffusionKVRequestState(
generation=metadata.allocation_generation,
snapshot=snapshot,
row_token_lens=tuple((install.identity, install.token_len) for install in installs),
)
return True
def get_diffusion_kv_row(
self,
request_id: str,
sequence_id: int | None,
context_id: str | None = None,
) -> int:
"""Resolve a Scheduler allocation identity to its native table row."""
if (
not is_scheduler_paged_kv_mode(
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
)
or self.block_tables is None
):
raise RuntimeError("paged_scheduler native BlockTables are not initialized")
identity = (request_id, None, context_id) if context_id is not None else (request_id, sequence_id, None)
try:
return self._diffusion_kv_identity_to_row[identity]
except KeyError as exc:
raise KeyError(f"No native diffusion KV row is installed for {identity!r}") from exc
def get_paged_attention_adapter(self) -> DiffusionPagedAttentionAdapter:
if self.paged_attention_adapter is None:
raise RuntimeError("paged_scheduler native attention adapter is not initialized")
return self.paged_attention_adapter
def prepare_paged_attention_batch(
self,
rows: Sequence[DiffusionPagedAttentionRow],
) -> PreparedDiffusionPagedAttentionBatch:
"""Build native page-table metadata for one Diffusion forward.
The scheduler allocation payload describes capacity, while the model
integration supplies the current ``query_len``/``kv_start_pos`` span
for each row. Keeping this boundary explicit prevents the Worker from
guessing model-specific text/image layout.
"""
return self.get_paged_attention_adapter().prepare_batch(rows)
def activate_paged_attention(self, batch: PreparedDiffusionPagedAttentionBatch):
"""Return the context manager that exposes a prepared batch to Omni Attention."""
return self.get_paged_attention_adapter().activate(batch)
def _resolve_paged_attention_row(
self,
request_id: str,
sequence_id: int | None,
context_id: str | None,
) -> DiffusionPagedAttentionRowBinding:
row_index = self.get_diffusion_kv_row(request_id, sequence_id, context_id)
identity = (request_id, None, context_id) if context_id is not None else (request_id, sequence_id, None)
request_state = self._diffusion_kv_request_states.get(request_id)
if request_state is not None:
for installed_identity, token_len in request_state.row_token_lens:
if installed_identity == identity:
return DiffusionPagedAttentionRowBinding(
row_index=row_index,
max_seq_len=token_len,
)
raise RuntimeError(f"Diffusion KV request state is missing logical length for {identity!r}")
def remove_diffusion_kv_requests(self, request_ids: list[str]) -> int:
"""Retire Worker rows without logically freeing Scheduler-owned blocks."""
if (
not is_scheduler_paged_kv_mode(
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
)
or self.block_tables is None
):
return 0
request_id_set = set(request_ids)
identities_and_rows = [
(identity, row)
for identity, row in self._diffusion_kv_identity_to_row.items()
if identity[0] in request_id_set
]
rows = [row for _, row in identities_and_rows]
if rows:
assert self.kv_cache_config is not None
num_groups = len(self.kv_cache_config.kv_cache_groups)
installs = [
_DiffusionKVRowInstall(
identity=identity,
token_len=0,
block_ids=tuple(tuple() for _ in range(num_groups)),
)
for identity, _ in identities_and_rows
]
self._apply_rows(rows, installs)
for identity, _ in identities_and_rows:
del self._diffusion_kv_identity_to_row[identity]
for request_id in request_id_set:
self._diffusion_kv_request_states.pop(request_id, None)
self._diffusion_kv_free_rows.extend(rows)
self._diffusion_kv_free_rows.sort(reverse=True)
return len(rows)
def refresh_block_table_layout(self) -> None:
"""Refresh native pointer tensors after a CuMem KV-cache wake-up."""
if (
not is_scheduler_paged_kv_mode(
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
)
or self.block_tables is None
):
return
if self.paged_attention_adapter is not None:
self.paged_attention_adapter.invalidate_prepared_batches()
self.block_tables.init_block_table_layout_tensors()
@@ -0,0 +1,778 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM 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
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
canonical_spec = replace(
spec,
num_kv_heads=num_kv_heads,
indexes_kv_by_block_stride=attn_backend.indexes_kv_by_block_stride(),
)
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"
)
if not self.attn_backend.forward_includes_kv_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 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
def restore_output(self, output: torch.Tensor) -> torch.Tensor:
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 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 _validate_token_layout(
token_shape: tuple[int, ...],
batch: PreparedDiffusionPagedAttentionBatch,
*,
name: str,
) -> None:
if len(token_shape) == 1:
if token_shape[0] == batch.num_tokens:
return
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) and all(query_len == tokens_per_row for query_len in query_lens):
return
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}"
)
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)
metadata_by_segment.append(
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=batch.positions.index_select(0, packed_segment.query_indices),
block_tables=batch.block_tables,
slot_mappings=batch.slot_mappings.index_select(-1, packed_segment.query_indices),
causal=(row_segments[0].mode == "causal"),
)
)
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",
)
self._validate_token_layout(query_token_shape, batch, name="query")
self._validate_token_layout(key_token_shape, batch, name="key")
self._validate_token_layout(value_token_shape, batch, name="value")
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 writer consumes slot_mapping directly, so a span that starts
# inside a physical block naturally writes only the requested suffix.
key_flat = key_flat.contiguous()
value_flat = value_flat.contiguous()
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,
)
@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
+14 -3
View File
@@ -142,13 +142,24 @@ class DiffusionExecutor(ABC):
raise TypeError("determine_available_kv_memory must return list[int]")
return result
def set_kv_cache_configs(self, kv_cache_configs: list[KVCacheConfig]) -> None:
"""Send each native rank-local config back to all Workers."""
def set_kv_cache_configs(self, kv_cache_configs: list[KVCacheConfig], resolved_max_model_len: int) -> None:
"""Send rank-local configs and the resolved model length to all Workers."""
# The default control-plane RPC mode executes on every rank and has
# rank 0 return the gathered rank statuses, so failures on nonzero
# ranks are not silently dropped.
self.collective_rpc("set_kv_cache_configs", args=(kv_cache_configs,))
self.collective_rpc("set_kv_cache_configs", args=(kv_cache_configs, resolved_max_model_len))
def remove_diffusion_kv_requests(self, request_ids: list[str]) -> None:
"""Clear request rows on every Worker after Scheduler retirement."""
unique_request_ids = list(dict.fromkeys(request_ids))
if not unique_request_ids:
return
self.collective_rpc(
"remove_diffusion_kv_requests",
args=(unique_request_ids,),
)
@abstractmethod
def shutdown(self) -> None:
+30 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from contextlib import contextmanager
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import torch
import vllm.ir
@@ -26,6 +26,11 @@ class ForwardContext:
vllm_config: VllmConfig | None = None
omni_diffusion_config: OmniDiffusionConfig | None = None
attn_metadata: dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None = None
# Active Worker-side paged KV adapter. The adapter is installed only for
# the duration of a paged forward; dense forwards leave this as ``None``.
# Keep the field opaque here to avoid coupling the common context module to
# the diffusion_kv implementation.
paged_kv_adapter: Any | None = None
split_text_embed_in_sp: bool = False
denoise_step_idx: int | None = None
denoise_timestep: float | None = None
@@ -209,6 +214,30 @@ def set_forward_context(
yield
@contextmanager
def override_paged_kv_adapter(adapter: Any | None):
"""Temporarily expose a Worker paged-KV adapter to Omni Attention.
This is deliberately a small context override instead of a second global
forward context. The model runner owns the outer context and the adapter
only replaces one opaque field while its prepared native metadata is live.
"""
if _forward_context is None:
# Unit-level adapter users can prepare/activate metadata without an
# Omni model forward context. In that case there is nothing to
# override and the adapter's explicit ``forward`` API remains usable.
yield
return
previous = _forward_context.paged_kv_adapter
_forward_context.paged_kv_adapter = adapter
try:
yield
finally:
_forward_context.paged_kv_adapter = previous
def set_forward_context_denoise_step_idx(step_idx: int | None) -> None:
"""Set the current diffusion denoise step on the active ForwardContext."""
if _forward_context is not None:
@@ -1134,7 +1134,7 @@ class ImageKVCacheManager(nn.Module):
joint_text_query = query[:, :0, :, :]
joint_text_key, joint_text_value = self._reuse_prompt_kv(key, value, seq_len, bs, shard_image_size)
if not keep_kv_compressed:
if not keep_kv_compressed and not self.attn.is_paged_kv_active():
key = repeat_kv(key, repeat_num)
value = repeat_kv(value, repeat_num)
if self.sp_size > 1:
+61 -1
View File
@@ -5,7 +5,7 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any
import torch
@@ -80,6 +80,15 @@ class _DiffusionVllmModelConfig:
logits_processors: tuple[str, ...] = ()
use_mla: bool = False
is_moe: bool = False
# Native vLLM attention layers/builders read these fields directly. The
# diffusion model does not have a full vLLM ModelConfig, so keep the
# narrow compatibility surface here instead of manufacturing a second
# model configuration object in the Worker backend.
is_mm_prefix_lm: bool = False
rswa_window: int | None = None
_attention_num_heads: int | None = field(default=None, init=False, repr=False)
_attention_num_kv_heads: int | None = field(default=None, init=False, repr=False)
_attention_head_size: int | None = field(default=None, init=False, repr=False)
# Needed for models that bundle things like LogitsProcessors, e.g., SenseNova
head_dtype: torch.dtype | None = None
@@ -91,6 +100,56 @@ class _DiffusionVllmModelConfig:
def is_nvfp4_quantized(self) -> bool:
return self.quantization == "modelopt_fp4"
def set_attention_geometry(
self,
*,
num_heads: int,
num_kv_heads: int,
head_size: int,
) -> None:
"""Record rank-local geometry needed by native metadata builders.
Diffusion attention modules are loaded outside vLLM's model loader, so
their dimensions are not available when this lightweight config is
created. The paged Worker backend fills them in after model loading
and before calling ``init_attn_backend``.
"""
values = {
"num_heads": num_heads,
"num_kv_heads": num_kv_heads,
"head_size": head_size,
}
invalid = {name: value for name, value in values.items() if type(value) is not int or value <= 0}
if invalid:
raise ValueError(f"Diffusion attention geometry must contain positive integers: {invalid!r}")
self._attention_num_heads = num_heads
self._attention_num_kv_heads = num_kv_heads
self._attention_head_size = head_size
def _require_attention_geometry(self) -> tuple[int, int, int]:
geometry = (
self._attention_num_heads,
self._attention_num_kv_heads,
self._attention_head_size,
)
if any(value is None for value in geometry):
raise RuntimeError("Diffusion attention geometry is unavailable before paged cache layer registration")
num_heads, num_kv_heads, head_size = geometry
assert num_heads is not None and num_kv_heads is not None and head_size is not None
return num_heads, num_kv_heads, head_size
def get_num_attention_heads(self, parallel_config: Any) -> int:
del parallel_config
return self._require_attention_geometry()[0]
def get_num_kv_heads(self, parallel_config: Any) -> int:
del parallel_config
return self._require_attention_geometry()[1]
def get_head_size(self) -> int:
return self._require_attention_geometry()[2]
@property
def is_diffusion(self) -> bool:
# This config is also installed as the current VllmConfig for the
@@ -164,6 +223,7 @@ def configure_diffusion_vllm_config(vllm_config: VllmConfig, od_config: OmniDiff
vllm_config.cache_config.kv_cache_memory_bytes = getattr(od_config, "kv_cache_memory_bytes", None)
# Prefix hashes/publication are intentionally deferred to the next PR.
vllm_config.cache_config.enable_prefix_caching = False
vllm_config.scheduler_config.max_num_seqs = int(od_config.max_num_seqs)
max_num_batched_tokens = getattr(od_config, "max_num_batched_tokens", None)
if max_num_batched_tokens is not None:
vllm_config.scheduler_config.max_num_batched_tokens = int(max_num_batched_tokens)
@@ -24,6 +24,7 @@ from vllm.logger import init_logger
from vllm.utils.mem_utils import DeviceMemoryProfiler, GiB_bytes
from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheSpec
from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.cache.cachedit import cache_summary
from vllm_omni.diffusion.cache.prompt_embed_cache import (
install_prompt_embed_cache,
@@ -34,6 +35,11 @@ from vllm_omni.diffusion.compile import regionally_compile
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
from vllm_omni.diffusion.diffusion_kv.metadata import DiffusionKVMetadata
from vllm_omni.diffusion.diffusion_kv.model_runner_backend import DiffusionKVModelRunnerBackend
from vllm_omni.diffusion.diffusion_kv.paged_attention_adapter import (
DiffusionPagedAttentionRow,
PreparedDiffusionPagedAttentionBatch,
)
from vllm_omni.diffusion.forward_context import set_forward_context
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import (
@@ -144,9 +150,13 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
self.prompt_embed_cache: Any | None = None
self.input_batch: InputBatch | None = None
self.model_memory_usage = 0
# Rank-local native config generated by the Engine. Physical tensors
# and BlockTable binding remain the responsibility of the paged
# data-plane initialization path.
self.diffusion_kv_backend = DiffusionKVModelRunnerBackend(
vllm_config=vllm_config,
od_config=od_config,
device=device,
)
# Compatibility view for callers that inspect the installed config;
# physical ownership remains in ``diffusion_kv_backend``.
self.kv_cache_config: KVCacheConfig | None = None
# Cache for per-request stepwise state.
@@ -381,41 +391,60 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
def get_kv_cache_spec(self) -> dict[str, KVCacheSpec]:
"""Collect native specs from cache-enabled loaded attention modules."""
from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
if self.od_config.diffusion_kv_mode is not DiffusionKVCacheMode.PAGED_SCHEDULER:
return {}
if self.pipeline is None:
raise RuntimeError("Model must be loaded before collecting Diffusion KV cache specs")
specs: dict[str, KVCacheSpec] = {}
cache_layers: dict[str, tuple[Attention, KVCacheSpec]] = {}
for layer_name, module in self.pipeline.named_modules():
if not isinstance(module, Attention):
continue
spec = module.get_kv_cache_spec(self.vllm_config)
if spec is not None:
specs[layer_name] = spec
if not specs:
cache_layers[layer_name] = (module, spec)
if not cache_layers:
raise RuntimeError(
"paged_scheduler Diffusion KV found no cache-enabled Attention modules "
f"in {type(self.pipeline).__name__}"
)
return specs
return self.diffusion_kv_backend.register_kv_cache_layers(cache_layers)
def set_kv_cache_config(self, kv_cache_config: KVCacheConfig) -> None:
"""Retain the Engine-generated rank-local config for the data plane."""
"""Physically initialize the Engine-generated rank-local config."""
configured_layers = {
layer_name for group in kv_cache_config.kv_cache_groups for layer_name in group.layer_names
}
expected_layers = set(self.get_kv_cache_spec())
if configured_layers != expected_layers:
raise ValueError(
"Rank-local Diffusion KVCacheConfig layer mismatch: "
f"expected={sorted(expected_layers)}, configured={sorted(configured_layers)}"
)
self.kv_cache_config = kv_cache_config
self.diffusion_kv_backend.initialize_kv_cache(kv_cache_config)
self.kv_cache_config = self.diffusion_kv_backend.kv_cache_config
def install_diffusion_kv_metadata(self, metadata: DiffusionKVMetadata) -> bool:
return self.diffusion_kv_backend.install_diffusion_kv_metadata(metadata)
def get_diffusion_kv_row(
self,
request_id: str,
sequence_id: int | None,
context_id: str | None = None,
) -> int:
return self.diffusion_kv_backend.get_diffusion_kv_row(request_id, sequence_id, context_id)
def remove_diffusion_kv_requests(self, request_ids: list[str]) -> int:
return self.diffusion_kv_backend.remove_diffusion_kv_requests(request_ids)
def refresh_diffusion_kv_block_table_layout(self) -> None:
self.diffusion_kv_backend.refresh_block_table_layout()
def prepare_paged_attention_batch(
self,
rows: list[DiffusionPagedAttentionRow] | tuple[DiffusionPagedAttentionRow, ...],
) -> PreparedDiffusionPagedAttentionBatch:
"""Prepare model-specific query/write spans against installed Worker rows."""
return self.diffusion_kv_backend.prepare_paged_attention_batch(rows)
def activate_paged_attention(self, batch: PreparedDiffusionPagedAttentionBatch):
"""Expose a prepared paged batch to Omni Attention for one forward."""
return self.diffusion_kv_backend.activate_paged_attention(batch)
def clear_prompt_embed_cache(self) -> None:
"""Evict all cached text-encoder outputs (e.g. between training epochs).
@@ -664,17 +693,25 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
request_id=req.request_id,
metadata=diffusion_kv_metadata,
)
runner_output = self._execute_request_list(
[req],
od_config=self.od_config,
allow_single_output=True,
require_request_batch_support=False,
kv_prefetch_job=kv_prefetch_job,
record_name="pipeline_forward",
)
output = runner_output.runner_outputs[0].result
assert output is not None
return output
installed_request = False
if diffusion_kv_metadata is not None:
self.install_diffusion_kv_metadata(diffusion_kv_metadata)
installed_request = True
try:
runner_output = self._execute_request_list(
[req],
od_config=self.od_config,
allow_single_output=True,
require_request_batch_support=False,
kv_prefetch_job=kv_prefetch_job,
record_name="pipeline_forward",
)
output = runner_output.runner_outputs[0].result
assert output is not None
return output
finally:
if installed_request:
self.remove_diffusion_kv_requests([req.request_id])
def profile_run(self, requests: list[OmniDiffusionRequest]) -> None:
"""Run the maximum per-rank request batch for memory profiling.
@@ -746,14 +783,23 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
request_id=new_req.req.request_id,
metadata=new_req.diffusion_kv_metadata,
)
reqs = [nr.req for nr in scheduler_output.scheduled_new_reqs]
return self._execute_request_list(
reqs,
od_config=od_config,
allow_single_output=False,
require_request_batch_support=True,
record_name="pipeline_forward_batch",
)
installed_request_ids: list[str] = []
try:
for new_req in scheduler_output.scheduled_new_reqs:
if new_req.diffusion_kv_metadata is not None:
self.install_diffusion_kv_metadata(new_req.diffusion_kv_metadata)
installed_request_ids.append(new_req.request_id)
reqs = [nr.req for nr in scheduler_output.scheduled_new_reqs]
return self._execute_request_list(
reqs,
od_config=od_config,
allow_single_output=False,
require_request_batch_support=True,
record_name="pipeline_forward_batch",
)
finally:
if installed_request_ids:
self.remove_diffusion_kv_requests(installed_request_ids)
# ------------------------------------------------------------------
# Step-wise execution
@@ -884,6 +930,40 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
if self.od_config.cache_backend not in (None, "none"):
raise ValueError("Step mode does not support cache_backend yet.")
# Scheduler metadata is installed only for newly admitted requests;
# cached requests continue to use the row installed on their first
# step. Paged rows are retired here for scheduler-finished requests so
# a subsequent wave can reuse the native table slots. Dense execution
# must not issue Worker KV cleanup side effects.
if (
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
is DiffusionKVCacheMode.PAGED_SCHEDULER
and scheduler_output.finished_req_ids
):
self.remove_diffusion_kv_requests(list(scheduler_output.finished_req_ids))
installed_request_ids: list[str] = []
try:
for new_req in scheduler_output.scheduled_new_reqs:
if new_req.diffusion_kv_metadata is not None:
self.install_diffusion_kv_metadata(new_req.diffusion_kv_metadata)
installed_request_ids.append(new_req.request_id)
return self._execute_stepwise_core(
scheduler_output,
record_output_peak_memory=record_output_peak_memory,
)
except Exception:
if installed_request_ids:
self.remove_diffusion_kv_requests(installed_request_ids)
raise
def _execute_stepwise_core(
self,
scheduler_output: DiffusionSchedulerOutput,
*,
record_output_peak_memory: bool,
) -> BatchRunnerOutput:
"""Run the denoise step after metadata admission and row installation."""
use_hsdp = self.od_config.parallel_config.use_hsdp
grad_context = torch.no_grad() if use_hsdp else torch.inference_mode()
with grad_context:
@@ -1005,7 +1085,16 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
state.peak_memory_mb,
)
terminal_request_ids = [
runner_output.request_id for runner_output in runner_output_list if runner_output.finished
]
self._update_states_after(states, input_batch, pipeline_interrupted)
if (
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
is DiffusionKVCacheMode.PAGED_SCHEDULER
and terminal_request_ids
):
self.remove_diffusion_kv_requests(terminal_request_ids)
return BatchRunnerOutput.from_list(runner_output_list)
+32 -3
View File
@@ -188,6 +188,11 @@ class DiffusionWorker:
# requests, which only carry their request_id in subsequent ticks.
self._step_lora_state: dict[str, tuple[LoRARequest | None, float]] = {}
self.stage_id = getattr(od_config, "stage_id", 0)
if self.od_config.diffusion_kv_mode is DiffusionKVCacheMode.PAGED_SCHEDULER:
logger.warning_once(
"paged_scheduler initializes native paged KV storage, but no production diffusion model uses the "
"paged-attention adapter yet; model attention remains on the dense path."
)
self.init_device()
# Create model runner — one decision chain, in precedence order:
# 1. explicit od_config.diffusion_model_runner_cls (user override),
@@ -431,16 +436,38 @@ class DiffusionWorker:
)
]
def set_kv_cache_configs(self, kv_cache_configs: list[KVCacheConfig]) -> None:
"""Install this rank's native config without allocating tensors yet."""
def set_kv_cache_configs(
self,
kv_cache_configs: list[KVCacheConfig],
resolved_max_model_len: int,
) -> None:
"""Select this rank's config and initialize its physical KV pages."""
assert self.model_runner is not None
assert self.vllm_config is not None
if len(kv_cache_configs) != self.od_config.num_gpus:
raise ValueError(
"Diffusion KVCacheConfig rank count mismatch: "
f"expected={self.od_config.num_gpus}, got={len(kv_cache_configs)}"
)
self.model_runner.set_kv_cache_config(kv_cache_configs[self.rank])
if not 0 <= self.rank < len(kv_cache_configs):
raise ValueError(f"Diffusion Worker rank {self.rank} has no rank-local KVCacheConfig")
if type(resolved_max_model_len) is not int or resolved_max_model_len <= 0:
raise ValueError("resolved Diffusion KV max_model_len must be a positive integer")
# Native cache sizing may resolve an explicit ``-1`` model-length
# sentinel to the capacity that actually fits the profiled pool.
self.vllm_config.model_config.max_model_len = resolved_max_model_len
kv_cache_config = kv_cache_configs[self.rank]
self.vllm_config.cache_config.num_gpu_blocks = kv_cache_config.num_blocks
with self._maybe_get_memory_pool_context("kv_cache"):
self.model_runner.set_kv_cache_config(kv_cache_config)
def remove_diffusion_kv_requests(self, request_ids: list[str]) -> int:
"""Clear Worker-local rows without freeing Scheduler-owned blocks."""
assert self.model_runner is not None, "Model runner not initialized"
return self.model_runner.remove_diffusion_kv_requests(request_ids)
def init_lora_manager(self) -> None:
"""Initialize the LoRA manager for this worker."""
@@ -730,6 +757,8 @@ class DiffusionWorker:
allocator = CuMemAllocator.get_instance()
allocator.wake_up(tags)
current_omni_platform.synchronize()
if self.model_runner is not None and (tags is None or "kv_cache" in tags):
self.model_runner.refresh_diffusion_kv_block_table_layout()
if len(self._sleep_saved_buffers) and self.model_runner is not None:
model = self.model_runner.pipeline
for name, buffer in model.named_buffers():
+22
View File
@@ -184,6 +184,28 @@ class OmniPlatform(Platform):
"""
return "vllm_omni.diffusion.worker.diffusion_model_runner.DiffusionModelRunner"
@classmethod
def get_diffusion_kv_block_tables_cls(cls) -> type:
"""Return the platform's native paged-KV BlockTables implementation."""
from vllm.v1.worker.gpu.block_table import BlockTables
return BlockTables
@classmethod
def build_diffusion_kv_attn_metadata(cls, **kwargs: Any) -> dict[str, Any]:
"""Build native attention metadata for the diffusion paged path.
The common CUDA/ROCm/XPU path uses vLLM's builder. Platforms with a
backend-specific metadata contract can override this hook without
making the diffusion adapter import their optional runtime package.
``seq_lens_cpu`` is an adapter-only convenience value and is removed
before calling the upstream builder.
"""
from vllm.v1.worker.gpu.attn_utils import build_attn_metadata
kwargs.pop("seq_lens_cpu", None)
return build_attn_metadata(**kwargs)
@classmethod
def init_diffusion_worker_vllm_config(
cls,
+21
View File
@@ -80,6 +80,27 @@ class NPUOmniPlatform(OmniPlatform, NPUPlatform):
init_ascend_config(vllm_config)
@classmethod
def get_diffusion_kv_block_tables_cls(cls) -> type:
from vllm_ascend.worker.v2.block_table import AscendBlockTables
return AscendBlockTables
@classmethod
def build_diffusion_kv_attn_metadata(cls, **kwargs: Any) -> dict[str, Any]:
"""Build the Ascend metadata required by the native NPU backend."""
from vllm_ascend.attention.attention_v1 import AscendAttentionState
from vllm_ascend.worker.v2.attn_utils import build_attn_metadata
kwargs = dict(kwargs)
seq_lens_cpu = kwargs.pop("seq_lens_cpu")
kwargs["seq_lens_np"] = seq_lens_cpu.detach().cpu().numpy()
# The diffusion adapter always supplies a paged cache and the current
# K/V write span. ChunkedPrefill is Ascend's cache-backed FIA state for
# both multi-token updates and single-token updates in this path.
kwargs["attn_state"] = AscendAttentionState.ChunkedPrefill
return build_attn_metadata(**kwargs)
@classmethod
def init_diffusion_model_runner_runtime(cls, vllm_config: Any, od_config: Any, device: torch.device) -> None:
from vllm_ascend.ascend_forward_context import set_mc2_mask, set_mc2_tokens_capacity