[Diffusion] Add paged KV cache support for HunyuanImage3 DiT on NPU and GPU (#6563)

Signed-off-by: Acerak01-fy <wfy2003324@163.com>
Signed-off-by: zwhzzz0821 <zwhzzz0821@users.noreply.github.com>
Signed-off-by: zwhzzz0821 <2831474076@qq.com>
Co-authored-by: zwhzzz0821 <zwhzzz0821@users.noreply.github.com>
Co-authored-by: zwhzzz0821 <2831474076@qq.com>
This commit is contained in:
authored and GitHub committed 2026-08-31 10:15:46 +00:00
1 parent 6f16f9546d
commit fe4a2a7477
28 files changed
+1789 -205

No files matched your search

@@ -11,6 +11,7 @@ Tests cover:
"""
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
import torch
@@ -466,6 +467,120 @@ class TestCurrentDiffusionConfig:
class TestAttentionInitUsesCurrentDiffusionConfig:
@pytest.mark.parametrize("mindiesd_available", [False, True])
def test_paged_npu_memory_profile_uses_sdpa_only_without_mindiesd(self, monkeypatch, mindiesd_available):
attention = Attention.__new__(Attention)
attention._scheduler_paged_kv = True
attention.paged_kv_cache_role = "primary"
attention.backend_pref = "FLASH_ATTN"
attention.attn_backend = SimpleNamespace(supports_piecewise_spans=True, get_name=lambda: "FLASH_ATTN")
flash_output = torch.ones(1)
sdpa_output = torch.zeros(1)
dense_flash = Mock(return_value=flash_output)
sdpa = Mock(return_value=sdpa_output)
attention.attention = SimpleNamespace(forward=dense_flash)
attention.sdpa_fallback = SimpleNamespace(forward=sdpa)
monkeypatch.setattr(layer_mod, "is_forward_context_available", lambda: True)
monkeypatch.setattr(
layer_mod,
"get_forward_context",
lambda: SimpleNamespace(in_diffusion_kv_memory_profile=True),
)
monkeypatch.setattr(layer_mod.current_omni_platform, "is_npu", lambda: True)
monkeypatch.setattr(
layer_mod.current_omni_platform,
"supports_diffusion_dense_flash_attention",
lambda: mindiesd_available,
)
result = attention._run_local_attention(
torch.zeros(1, dtype=torch.bfloat16),
torch.zeros(1, dtype=torch.bfloat16),
torch.zeros(1, dtype=torch.bfloat16),
AttentionMetadata(),
)
if mindiesd_available:
assert result is flash_output
dense_flash.assert_called_once()
sdpa.assert_not_called()
else:
assert result is sdpa_output
dense_flash.assert_not_called()
sdpa.assert_called_once()
def test_dense_flash_import_error_is_not_reclassified(self, monkeypatch):
attention = Attention.__new__(Attention)
attention._scheduler_paged_kv = False
attention.paged_kv_cache_role = "primary"
attention.backend_pref = "FLASH_ATTN"
attention.attn_backend = SimpleNamespace(supports_piecewise_spans=True, get_name=lambda: "FLASH_ATTN")
attention.attention = SimpleNamespace(
forward=Mock(side_effect=ModuleNotFoundError("No module named 'mindiesd'", name="mindiesd"))
)
attention.sdpa_fallback = SimpleNamespace(forward=Mock(side_effect=AssertionError("must not fall back")))
monkeypatch.setattr(layer_mod, "is_forward_context_available", lambda: False)
with pytest.raises(ModuleNotFoundError, match="mindiesd"):
attention._run_local_attention(
torch.zeros(1, dtype=torch.bfloat16),
torch.zeros(1, dtype=torch.bfloat16),
torch.zeros(1, dtype=torch.bfloat16),
AttentionMetadata(),
)
def test_paged_marker_does_not_change_dense_backend(self, monkeypatch):
class _FakeAttentionImpl:
def __init__(self, **kwargs):
self.kwargs = kwargs
def forward(self, query, key, value, attn_metadata=None):
return query
class _DenseBackend:
supports_paged_kv = False
@staticmethod
def get_name() -> str:
return "TORCH_SDPA"
@staticmethod
def get_impl_cls():
return _FakeAttentionImpl
calls = []
def _fake_get_attn_backend_for_role(*args, **kwargs):
calls.append((args, kwargs))
return _DenseBackend, None
monkeypatch.setattr(layer_mod, "get_attn_backend_for_role", _fake_get_attn_backend_for_role)
monkeypatch.setattr(layer_mod.SDPABackend, "get_impl_cls", staticmethod(lambda: _FakeAttentionImpl))
monkeypatch.setattr(layer_mod, "build_parallel_attention_strategy", lambda **kwargs: object())
monkeypatch.setattr(layer_mod, "is_forward_context_available", lambda: False)
od_config = SimpleNamespace(
diffusion_attention_config=AttentionConfig(),
diffusion_kv_mode=layer_mod.DiffusionKVCacheMode.DENSE_LEGACY,
parallel_config=SimpleNamespace(ring_degree=1),
diffusion_kv_cache_dtype=None,
diffusion_kv_cache_skip_step_indices=None,
diffusion_kv_cache_skip_layer_indices=None,
)
with set_current_diffusion_config(od_config):
attention = Attention(
num_heads=4,
head_size=64,
causal=False,
softmax_scale=1.0,
paged_kv_cache_role="primary",
)
assert len(calls) == 1
assert attention.attn_backend is _DenseBackend
assert attention.backend_pref is None
def test_attention_init_uses_current_diffusion_config_without_forward_context(self, monkeypatch):
class _FakeAttentionImpl:
def __init__(self, **kwargs):
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""End-to-end test for ``piecewise_attn`` (CPU).
Verify that running attention in segments (causal outside full-attn spans,
@@ -98,9 +98,12 @@ def test_paged_piecewise_runner_uses_packed_indices_and_kv_endpoints():
query = torch.arange(12 * 2 * 4, dtype=torch.float32).reshape(12, 2, 4)
key = query + 100
value = query + 200
calls = []
calls: list[object] = []
def run_segment(segment_query, segment_key, segment_value, segment_batch):
output_buffer = torch.empty_like(query)
def run_segment(segment_query, segment_key, segment_value, segment_batch, segment_output_buffer):
assert segment_output_buffer is None
calls.append(
(
[segment.mode for segment in segment_batch.row_segments],
@@ -119,9 +122,13 @@ def test_paged_piecewise_runner_uses_packed_indices_and_kv_endpoints():
plan,
plan.segments,
run_segment,
output_buffer=output_buffer,
)
assert output is output_buffer
torch.testing.assert_close(output, query)
assert all(segment.query_range is None for segment in plan.segments)
assert not plan.segments_cover_query_contiguously
assert calls == [
(["causal", "causal"], [2, 4], [0, 1, 6, 7]),
(["full", "full"], [5, 7], [2, 3, 4, 8, 9, 10]),
@@ -129,6 +136,161 @@ def test_paged_piecewise_runner_uses_packed_indices_and_kv_endpoints():
]
def test_paged_piecewise_runner_preserves_homogeneous_batch_without_scatter(monkeypatch: pytest.MonkeyPatch):
plan = build_paged_piecewise_plan(
[[(2, 5)], [(2, 5)]],
query_offsets=[0, 0],
query_lens=[6, 6],
seq_lens=[6, 6],
device=DEVICE,
)
assert plan.homogeneous_batch_shape == (2, 6)
query = torch.arange(12 * 2 * 4, dtype=torch.float32).reshape(12, 2, 4)
calls: list[torch.Tensor] = []
def fail_index_copy(*_args, **_kwargs):
raise AssertionError("homogeneous piecewise path must not use index_copy_")
monkeypatch.setattr(torch.Tensor, "index_copy_", fail_index_copy)
def run_segment(segment_query, segment_key, segment_value, _metadata, segment_output_buffer):
assert segment_key is None
assert segment_value is None
assert segment_output_buffer is None
calls.append(segment_query.clone())
return segment_query + 1
output_buffer = torch.empty_like(query)
output = run_paged_piecewise_plan(
query,
None,
None,
plan,
plan.segments,
run_segment,
output_buffer=output_buffer,
use_homogeneous_batch=True,
)
assert output is output_buffer
torch.testing.assert_close(output, query + 1)
assert [call.shape[0] for call in calls] == [4, 6, 2]
torch.testing.assert_close(calls[0], query[[0, 1, 6, 7]])
torch.testing.assert_close(calls[1], query[[2, 3, 4, 8, 9, 10]])
torch.testing.assert_close(calls[2], query[[5, 11]])
def test_paged_piecewise_runner_keeps_indexed_fallback_for_heterogeneous_layout():
plan = build_paged_piecewise_plan(
[[(2, 5)], [(1, 4)]],
query_offsets=[0, 0],
query_lens=[6, 6],
seq_lens=[6, 6],
device=DEVICE,
)
assert plan.homogeneous_batch_shape is None
query = torch.arange(12 * 2 * 4, dtype=torch.float32).reshape(12, 2, 4)
output = run_paged_piecewise_plan(
query,
None,
None,
plan,
plan.segments,
lambda segment_query, *_args: segment_query,
use_homogeneous_batch=True,
)
torch.testing.assert_close(output, query)
def test_paged_piecewise_runner_slices_contiguous_segments():
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)
key = query + 100
value = query + 200
expected_ranges = [(0, 2), (2, 5), (5, 6)]
calls: list[tuple[int, int]] = []
def run_segment(segment_query, segment_key, segment_value, _segment_batch, segment_output_buffer):
assert segment_output_buffer is None
expected_range = expected_ranges[len(calls)]
start, end = expected_range
calls.append(expected_range)
assert segment_query.untyped_storage().data_ptr() == query.untyped_storage().data_ptr()
assert segment_key.untyped_storage().data_ptr() == key.untyped_storage().data_ptr()
assert segment_value.untyped_storage().data_ptr() == value.untyped_storage().data_ptr()
torch.testing.assert_close(segment_query, query[start:end])
return segment_query
output = run_paged_piecewise_plan(query, key, value, plan, plan.segments, run_segment)
assert calls == expected_ranges
assert [segment.query_range for segment in plan.segments] == expected_ranges
assert plan.segments_cover_query_contiguously
torch.testing.assert_close(output, query)
def test_paged_piecewise_runner_writes_contiguous_segments_into_output_buffer():
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_buffer = torch.empty(6, 2, 3)
targets = []
def run_segment(segment_query, _key, _value, _metadata, segment_output_buffer):
assert segment_output_buffer is not None
assert segment_output_buffer.untyped_storage().data_ptr() == output_buffer.untyped_storage().data_ptr()
targets.append(segment_output_buffer)
return segment_output_buffer.copy_(segment_query[..., :3])
output = run_paged_piecewise_plan(
query,
query,
query,
plan,
plan.segments,
run_segment,
output_buffer=output_buffer,
)
assert output is output_buffer
assert [target.shape[0] for target in targets] == [2, 3, 1]
torch.testing.assert_close(output, query[..., :3])
def test_paged_piecewise_runner_returns_single_full_segment_directly():
plan = build_paged_piecewise_plan(
[[]],
query_offsets=[0],
query_lens=[6],
seq_lens=[6],
device=DEVICE,
)
query = torch.randn(6, 2, 4)
runner_output = query + 1
output = run_paged_piecewise_plan(
query,
query,
query,
plan,
plan.segments,
lambda *_args: runner_output,
)
assert output is runner_output
def test_paged_piecewise_runner_uses_runner_output_shape():
plan = build_paged_piecewise_plan(
[[(2, 5)]],
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
@@ -485,8 +485,16 @@ def test_valid_sequence_and_context_install_into_native_rows(monkeypatch: pytest
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)
assert (sequence_binding.row_index, sequence_binding.max_seq_len, sequence_binding.block_ids) == (
0,
9,
((1, 2, 3), (4, 5)),
)
assert (context_binding.row_index, context_binding.max_seq_len, context_binding.block_ids) == (
1,
5,
((6, 7), (8,)),
)
def test_block_table_mutations_invalidate_prepared_attention_batches(
+2 -2
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
@@ -36,7 +36,7 @@ def test_non_mapping_omni_kv_config_is_rejected() -> None:
OmniDiffusionConfig.from_kwargs(omni_kv_config="paged_scheduler")
def test_paged_scheduler_rejects_dense_legacy_kv_receive() -> None:
def test_paged_scheduler_rejects_dense_connector_kv_receive() -> None:
with pytest.raises(ValueError, match="does not support imported AR KV"):
OmniDiffusionConfig.from_kwargs(
diffusion_kv_mode="paged_scheduler",
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
from unittest.mock import Mock
@@ -290,6 +290,7 @@ def test_minus_one_max_model_len_is_auto_fitted_by_native_cache_sizing() -> None
dtype=torch.bfloat16,
non_causal=True,
)
model_limit = vllm_config.model_config.max_model_len
initialization.build_native_kv_cache_configs(
vllm_config,
@@ -298,4 +299,8 @@ def test_minus_one_max_model_len_is_auto_fitted_by_native_cache_sizing() -> None
)
assert vllm_config.model_config.original_max_model_len == -1
assert vllm_config.model_config.max_model_len == 256
# Sixteen physical pages provide ``16 * block_size`` tokens. The native
# backend chooses the block geometry, so keep this assertion backend
# independent instead of assuming the generic 16-token block size.
expected_max_model_len = min(model_limit, spec.block_size * 16)
assert vllm_config.model_config.max_model_len == expected_max_model_len
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
@@ -20,12 +20,16 @@ from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.diffusion_kv import paged_attention_adapter as adapter_module
from vllm_omni.diffusion.diffusion_kv.paged_attention_adapter import (
DiffusionPagedAttentionAdapter,
DiffusionPagedAttentionMetadata,
DiffusionPagedAttentionRow,
DiffusionPagedAttentionRowBinding,
DiffusionPagedAttentionRuntime,
)
from vllm_omni.diffusion.forward_context import (
get_forward_context,
override_paged_kv_adapter,
set_forward_context,
set_forward_context_denoise_step_idx,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
@@ -122,8 +126,8 @@ class _FakeNativeImpl:
def do_kv_cache_update(
self,
_layer,
key: torch.Tensor,
value: torch.Tensor,
key: torch.Tensor | None,
value: torch.Tensor | None,
_kv_cache,
slot_mapping: torch.Tensor,
) -> None:
@@ -155,6 +159,11 @@ def _make_adapter(
capacity: int = 16,
) -> tuple[DiffusionPagedAttentionAdapter, _FakeBlockTables, _FakeLayer, list[tuple]]:
events: list[tuple] = []
monkeypatch.setattr(
adapter_module.current_omni_platform,
"requires_diffusion_paged_kv_prewrite",
lambda: False,
)
block_tables = _FakeBlockTables()
block_tables.max_num_batched_tokens = capacity
block_tables.num_blocks.np.fill(math.ceil(capacity / block_tables.block_sizes[0]))
@@ -301,6 +310,47 @@ def test_paged_adapter_accepts_native_gqa_shapes(monkeypatch: pytest.MonkeyPatch
assert context.value_write.shape == (3, 8, 4)
def test_paged_adapter_preserves_strided_kv_for_platform_writer(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, _, _ = _make_adapter(monkeypatch)
batch = adapter.prepare_batch(
[DiffusionPagedAttentionRow(request_id="req-0", sequence_id=0, query_len=5, seq_len=5)]
)
packed_qkv = torch.randn(1, 5, 3 * 2 * 4)
query, key, value = packed_qkv.split(2 * 4, dim=-1)
query = query.reshape(1, 5, 2, 4)
key = key.reshape(1, 5, 2, 4)
value = value.reshape(1, 5, 2, 4)
assert not key.is_contiguous()
assert not value.is_contiguous()
with adapter.activate(batch):
context = adapter.prepare_layer_context("layer-0", query, key, value)
assert context.query.is_contiguous()
assert not context.key_write.is_contiguous()
assert not context.value_write.is_contiguous()
assert context.key_write.data_ptr() == key.data_ptr()
assert context.value_write.data_ptr() == value.data_ptr()
def test_paged_adapter_packs_noncontiguous_kv_head_layout(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, _, _ = _make_adapter(monkeypatch)
batch = adapter.prepare_batch(
[DiffusionPagedAttentionRow(request_id="req-0", sequence_id=0, query_len=5, seq_len=5)]
)
query = torch.randn(1, 5, 2, 4)
key = torch.randn(1, 5, 4, 2).transpose(-1, -2)
value = torch.randn(1, 5, 4, 2).transpose(-1, -2)
assert key.shape == (1, 5, 2, 4)
assert key.stride(-1) != 1
with adapter.activate(batch):
context = adapter.prepare_layer_context("layer-0", query, key, value)
assert context.key_write.is_contiguous()
assert context.value_write.is_contiguous()
def test_omni_paged_backend_defers_backend_owned_cache_update(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, layer, _ = _make_adapter(monkeypatch)
layer.attn_backend.forward_includes_kv_cache_update = True
@@ -350,10 +400,48 @@ def test_omni_piecewise_backend_defers_backend_owned_cache_update(monkeypatch: p
assert [metadata.query_start_loc_cpu.tolist() for metadata in segment_metadata] == [[0, 1], [0, 2], [0, 1]]
assert [metadata.positions.tolist() for metadata in segment_metadata] == [[0], [1, 2], [3]]
assert [metadata.slot_mappings.tolist() for metadata in segment_metadata] == [[[0]], [[1, 2]], [[3]]]
for event in events[1:]:
build_kwargs = event[1]
assert build_kwargs["positions"].untyped_storage().data_ptr() == batch.positions.untyped_storage().data_ptr()
assert (
build_kwargs["slot_mappings"].untyped_storage().data_ptr()
== batch.slot_mappings.untyped_storage().data_ptr()
)
def test_platform_prewrite_updates_once_then_piecewise_reads_cache(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, layer, _ = _make_adapter(monkeypatch)
layer.attn_backend.forward_includes_kv_cache_update = True
monkeypatch.setattr(
adapter_module.current_omni_platform,
"requires_diffusion_paged_kv_prewrite",
lambda: True,
)
adapter.resolve_row = lambda *_args: DiffusionPagedAttentionRowBinding(
row_index=2,
max_seq_len=16,
block_ids=((3, 7, 9, 11),),
)
row = DiffusionPagedAttentionRow(request_id="req-0", sequence_id=0, query_len=4, seq_len=4)
batch = adapter.prepare_batch([row])
qkv = torch.randn(1, 4, 2, 4)
metadata = SimpleNamespace(full_attn_spans=[[(1, 3)]], extra={})
with adapter.activate(batch):
context = adapter.prepare_layer_context("layer-0", qkv, qkv, qkv, omni_attn_metadata=metadata)
output = _run_omni_paged_backend(context)
assert torch.equal(output, qkv)
assert layer.native_events == ["update", "forward", "forward", "forward"]
assert len(layer.updates) == 1
assert all(call[1] is None and call[2] is None for call in layer.calls)
def test_omni_paged_backend_runs_hunyuan_piecewise_segments(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, layer, events = _make_adapter(monkeypatch)
# Exercise the Ascend-only homogeneous batch path. CUDA keeps its native
# output-buffer contract and is covered by the GPU adapter tests.
monkeypatch.setattr(adapter_module.current_omni_platform, "is_npu", lambda: True)
batch = adapter.prepare_batch(
[
DiffusionPagedAttentionRow(
@@ -450,6 +538,48 @@ def test_omni_paged_backend_runs_hunyuan_piecewise_segments(monkeypatch: pytest.
]
def test_piecewise_metadata_snapshots_reused_native_scheduler_buffer(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, _, _ = _make_adapter(monkeypatch)
shared_scheduler_metadata = torch.zeros(1, dtype=torch.int32)
build_id = 0
def build_metadata(**kwargs):
nonlocal build_id
shared_scheduler_metadata.fill_(build_id)
metadata = SimpleNamespace(
build_id=build_id,
causal=kwargs["causal"],
seq_lens=kwargs["seq_lens"].clone(),
query_start_loc_cpu=kwargs["query_start_loc_cpu"].clone(),
positions=kwargs["positions"].clone(),
slot_mappings=kwargs["slot_mappings"].clone(),
scheduler_metadata=shared_scheduler_metadata,
prefix_scheduler_metadata=None,
)
build_id += 1
return {"layer-0": metadata}
monkeypatch.setattr(adapter_module.current_omni_platform, "build_diffusion_kv_attn_metadata", build_metadata)
batch = adapter.prepare_batch(
[DiffusionPagedAttentionRow(request_id="req-0", sequence_id=0, query_len=4, seq_len=4)]
)
qkv = torch.randn(1, 4, 2, 4)
metadata = SimpleNamespace(full_attn_spans=[[(1, 3)]], extra={})
with adapter.activate(batch):
context = adapter.prepare_layer_context(
"layer-0",
qkv,
qkv,
qkv,
omni_attn_metadata=metadata,
)
segment_schedules = [item.scheduler_metadata for item in context.piecewise_native_metadata]
assert [item.item() for item in segment_schedules] == [1, 2, 3]
assert len({item.data_ptr() for item in segment_schedules}) == 3
def test_omni_paged_backend_treats_empty_full_spans_as_causal(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, layer, events = _make_adapter(monkeypatch)
batch = adapter.prepare_batch(
@@ -851,6 +981,28 @@ def test_forward_rejects_batched_layout_that_does_not_match_rows(monkeypatch: py
adapter.prepare_layer_context("layer-0", qkv, qkv, qkv)
def test_forward_packs_and_restores_right_padded_heterogeneous_rows(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, _, _ = _make_adapter(monkeypatch)
batch = adapter.prepare_batch(
[
DiffusionPagedAttentionRow(request_id="req-0", sequence_id=0, query_len=1, seq_len=1),
DiffusionPagedAttentionRow(request_id="req-1", sequence_id=1, query_len=3, seq_len=3),
]
)
qkv = torch.arange(2 * 3 * 2 * 4, dtype=torch.float32).reshape(2, 3, 2, 4)
with adapter.activate(batch):
context = adapter.prepare_layer_context("layer-0", qkv, qkv, qkv)
assert context.output_scatter_indices.tolist() == [0, 3, 4, 5]
torch.testing.assert_close(context.query, qkv.reshape(-1, 2, 4)[[0, 3, 4, 5]])
restored = context.restore_output(context.query)
assert restored.shape == qkv.shape
torch.testing.assert_close(restored[0, 0], qkv[0, 0])
assert torch.count_nonzero(restored[0, 1:]) == 0
torch.testing.assert_close(restored[1], qkv[1])
def test_forward_rejects_padded_full_kv(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, _, _ = _make_adapter(monkeypatch)
batch = adapter.prepare_batch(
@@ -878,6 +1030,41 @@ def test_forward_rejects_padded_full_kv(monkeypatch: pytest.MonkeyPatch) -> None
adapter.prepare_layer_context("layer-0", query, full_kv, full_kv)
def test_runner_runtime_switches_from_prefill_to_reusable_denoise_metadata(
monkeypatch: pytest.MonkeyPatch,
) -> None:
adapter, _, _, _ = _make_adapter(monkeypatch)
metadata = DiffusionPagedAttentionMetadata(
prefill_rows=(DiffusionPagedAttentionRow(request_id="req-0", sequence_id=0, query_len=6, seq_len=6),),
denoise_rows=(
DiffusionPagedAttentionRow(
request_id="req-0",
sequence_id=0,
query_len=2,
seq_len=5,
kv_start_pos=3,
),
),
)
runtime = DiffusionPagedAttentionRuntime(adapter, metadata)
with set_forward_context(paged_kv_runtime=runtime), runtime.activate():
set_forward_context_denoise_step_idx(0)
assert Attention._active_paged_kv_adapter() is adapter
assert adapter._active_batch.rows == metadata.prefill_rows
set_forward_context_denoise_step_idx(1)
assert Attention._active_paged_kv_adapter() is adapter
denoise_batch = adapter._active_batch
assert denoise_batch.rows == metadata.denoise_rows
set_forward_context_denoise_step_idx(2)
assert Attention._active_paged_kv_adapter() is adapter
assert adapter._active_batch is denoise_batch
assert adapter._active_batch is None
def test_causal_batch_rejects_non_suffix_write(monkeypatch: pytest.MonkeyPatch) -> None:
adapter, _, _, _ = _make_adapter(monkeypatch, non_causal=False)
row = DiffusionPagedAttentionRow(
@@ -992,7 +1179,8 @@ def test_layer_adapter_accepts_platform_native_backend_and_uses_rank_local_heads
monkeypatch: pytest.MonkeyPatch,
) -> None:
selected_backends = []
impl_cls = Mock(return_value=SimpleNamespace(forward=Mock()))
specialized_backends = []
impl_cls = Mock(return_value=SimpleNamespace(forward=Mock(), do_kv_cache_update=Mock()))
native_backend = SimpleNamespace(
get_name=lambda: "ASCEND",
indexes_kv_by_block_stride=lambda: True,
@@ -1003,16 +1191,28 @@ def test_layer_adapter_accepts_platform_native_backend_and_uses_rank_local_heads
original_backend_per_kind = {"full": object()}
config = SimpleNamespace(
attention_config=SimpleNamespace(backend=None, backend_per_kind=original_backend_per_kind),
parallel_config=SimpleNamespace(prefill_context_parallel_size=2),
model_config=SimpleNamespace(dtype=torch.float16),
cache_config=SimpleNamespace(cache_dtype="auto"),
)
def select_backend(**_kwargs):
selected_backends.append((config.attention_config.backend, config.attention_config.backend_per_kind))
selected_backends.append(
(
config.attention_config.backend,
config.attention_config.backend_per_kind,
config.parallel_config.prefill_context_parallel_size,
)
)
return native_backend
monkeypatch.setattr(adapter_module, "get_attn_backend", select_backend)
monkeypatch.setattr(adapter_module, "set_current_vllm_config", lambda _config: nullcontext())
monkeypatch.setattr(
adapter_module.current_omni_platform,
"get_diffusion_paged_kv_attn_backend",
lambda backend, *, ulysses_degree: specialized_backends.append((backend, ulysses_degree)) or backend,
)
layer = SimpleNamespace(num_heads=8, softmax_scale=0.125)
spec = FullAttentionSpec(
block_size=16,
@@ -1031,9 +1231,11 @@ def test_layer_adapter_accepts_platform_native_backend_and_uses_rank_local_heads
ulysses_degree=2,
)
assert selected_backends == [(adapter_module.AttentionBackendEnum.FLASH_ATTN, {})]
assert selected_backends == [(adapter_module.AttentionBackendEnum.FLASH_ATTN, {}, 2)]
assert specialized_backends == [(native_backend, 2)]
assert config.attention_config.backend is None
assert config.attention_config.backend_per_kind is original_backend_per_kind
assert config.parallel_config.prefill_context_parallel_size == 2
assert native_layer.num_heads == 4
assert native_layer.num_kv_heads == 2
assert native_layer.spec.num_kv_heads == 2
@@ -1094,6 +1296,51 @@ def test_omni_attention_wraps_paged_kernel_with_sp_hooks() -> None:
assert torch.equal(output, torch.full_like(original, 6))
def test_omni_attention_strips_paged_ulysses_padding_around_kernel() -> None:
events: list[tuple[str, tuple[int, ...]]] = []
class Strategy:
name = "ulysses"
def pre_attention(self, query, key, value, metadata):
return query, key, value, metadata, SimpleNamespace(joint_len=0, joint_strategy="front")
def post_attention(self, output, _ctx):
events.append(("post", tuple(output.shape)))
return output
class Adapter:
def prepare_layer_context(self, _layer_name, query, key, value, *, omni_attn_metadata):
assert omni_attn_metadata is None
events.append(("kernel_input", tuple(query.shape)))
assert query[:, -1].tolist() == [[[3.0]]]
assert torch.equal(query, key)
assert torch.equal(query, value)
return SimpleNamespace(query=query)
class Backend:
def forward_paged(self, context):
return context.query
layer = Attention.__new__(Attention)
nn.Module.__init__(layer)
layer.prefix = "layer-0"
layer.paged_kv_cache_role = "primary"
layer.attn_backend = SimpleNamespace(supports_paged_kv=True, get_name=lambda: "FLASH_ATTN")
layer.attention = Backend()
layer.use_ring = False
layer._no_parallel_strategy = object()
layer._get_active_parallel_strategy = lambda: Strategy()
tensor = torch.arange(5, dtype=torch.float32).reshape(1, 5, 1, 1)
with set_forward_context(), override_paged_kv_adapter(Adapter()):
get_forward_context().sp_padding_size = 1
output = layer._forward_impl(tensor, tensor, tensor)
assert events == [("kernel_input", (1, 4, 1, 1)), ("post", (1, 5, 1, 1))]
assert output.flatten().tolist() == [0.0, 1.0, 2.0, 3.0, 0.0]
def test_omni_attention_rejects_paged_request_for_backend_without_paged_support() -> None:
class Adapter:
def prepare_layer_context(self, *args, **kwargs):
@@ -1145,3 +1392,15 @@ def test_omni_attention_keeps_dense_kernel_without_active_adapter() -> None:
assert events == ["pre", "dense", "post"]
assert output is qkv
def test_omni_attention_rejects_scheduler_paged_forward_without_active_adapter() -> None:
layer = Attention.__new__(Attention)
nn.Module.__init__(layer)
layer._scheduler_paged_kv = True
layer.paged_kv_cache_role = "primary"
layer._get_active_parallel_strategy = lambda: object()
qkv = torch.zeros(1, 2, 2, 4)
with set_forward_context(), pytest.raises(RuntimeError, match="without an active Worker adapter"):
layer._forward_impl(qkv, qkv, qkv)
@@ -154,8 +154,17 @@ def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> N
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)
packed_qkv = torch.randn(
19,
3 * _NUM_HEADS * _HEAD_SIZE,
dtype=torch.float16,
device=device,
)
_, key_flat, value_flat = packed_qkv.split(_NUM_HEADS * _HEAD_SIZE, dim=-1)
key = key_flat.reshape(19, _NUM_HEADS, _HEAD_SIZE)
value = value_flat.reshape(19, _NUM_HEADS, _HEAD_SIZE)
assert not key.is_contiguous()
assert not value.is_contiguous()
omni_backend = FlashAttentionImpl(
num_heads=_NUM_HEADS,
head_size=_HEAD_SIZE,
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from contextlib import nullcontext
from types import SimpleNamespace
@@ -42,9 +42,10 @@ class _RunnerKVBackend:
self.kv_cache_config = config
def _attention(*, enabled: bool) -> Attention:
def _attention(*, enabled: bool, prefix: str = "image_attention") -> Attention:
attention = Attention.__new__(Attention)
nn.Module.__init__(attention)
attention.prefix = prefix
attention.paged_kv_cache_role = "primary" if enabled else None
attention.paged_kv_cache_dtype = torch.bfloat16
attention.num_kv_heads = 2
@@ -82,6 +83,23 @@ def test_runner_discovers_native_spec_from_loaded_attention() -> None:
assert spec.non_causal is True
def test_runner_uses_attention_prefix_as_canonical_layer_name() -> None:
runner = _runner(_attention(enabled=True, prefix="layers.0.self_attn.image_attn.attn"))
specs = runner.get_kv_cache_spec()
assert set(specs) == {"layers.0.self_attn.image_attn.attn"}
def test_runner_rejects_duplicate_attention_prefixes() -> None:
runner = _runner(_attention(enabled=True, prefix="shared"))
second_attention = _attention(enabled=True, prefix="shared")
runner.pipeline.second_image_attention = second_attention
with pytest.raises(RuntimeError, match="Duplicate canonical paged Diffusion Attention prefix 'shared'"):
runner.get_kv_cache_spec()
def test_runner_rejects_paged_mode_without_cache_enabled_attention() -> None:
runner = _runner(_attention(enabled=False))
@@ -42,7 +42,10 @@ def make_metadata(request_id: str = "req-0") -> DiffusionKVMetadata:
def make_runner(mode: DiffusionKVCacheMode) -> DiffusionModelRunner:
runner = object.__new__(DiffusionModelRunner)
runner.od_config = SimpleNamespace(diffusion_kv_mode=mode)
runner.od_config = SimpleNamespace(
diffusion_kv_mode=mode,
parallel_config=SimpleNamespace(cfg_parallel_size=1),
)
return runner
@@ -85,6 +88,51 @@ def test_new_request_data_carries_scheduler_allocation_atomically() -> None:
assert new_req.diffusion_kv_metadata is metadata
def test_runner_builds_prefill_and_denoise_rows_from_scheduler_metadata() -> None:
runner = make_runner(DiffusionKVCacheMode.PAGED_SCHEDULER)
metadata = DiffusionKVMetadata(
request_id="req-0",
allocation_generation=1,
sequences=(
DiffusionKVSequenceMetadata(0, 4, 2, 8, ([1, 2],)),
DiffusionKVSequenceMetadata(1, 5, 2, 9, ([3, 4],)),
),
)
attn_metadata = runner._build_paged_attention_metadata([metadata])
assert [
(row.request_id, row.sequence_id, row.kv_start_pos, row.query_len, row.seq_len)
for row in attn_metadata.prefill_rows
] == [("req-0", 0, 0, 8, 8), ("req-0", 1, 0, 9, 9)]
assert [
(row.request_id, row.sequence_id, row.kv_start_pos, row.query_len, row.seq_len)
for row in attn_metadata.denoise_rows
] == [("req-0", 0, 4, 2, 6), ("req-0", 1, 5, 2, 7)]
def test_runner_selects_only_local_cfg_parallel_row(monkeypatch: pytest.MonkeyPatch) -> None:
runner = make_runner(DiffusionKVCacheMode.PAGED_SCHEDULER)
runner.od_config.parallel_config.cfg_parallel_size = 2
monkeypatch.setattr(
"vllm_omni.diffusion.worker.diffusion_model_runner.get_classifier_free_guidance_rank",
lambda: 1,
)
metadata = DiffusionKVMetadata(
request_id="req-0",
allocation_generation=1,
sequences=(
DiffusionKVSequenceMetadata(0, 4, 2, 8, ([1, 2],)),
DiffusionKVSequenceMetadata(1, 5, 2, 9, ([3, 4],)),
),
)
attn_metadata = runner._build_paged_attention_metadata([metadata])
assert [row.sequence_id for row in attn_metadata.prefill_rows] == [1]
assert [row.sequence_id for row in attn_metadata.denoise_rows] == [1]
def test_paged_request_without_metadata_fails_before_request_forward() -> None:
runner = make_runner(DiffusionKVCacheMode.PAGED_SCHEDULER)
runner._execute_request_list = Mock()
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
@@ -270,6 +270,17 @@ def test_grouped_denoise_allows_sdpa_attention_backend():
pipeline._ensure_grouped_attention_backend_supported(2)
def test_scheduler_paged_step_execution_is_rejected():
pipeline = _pipeline()
pipeline.od_config.diffusion_kv_mode = hy3_module.DiffusionKVCacheMode.PAGED_SCHEDULER
state = _state("paged-step", 0)
with pytest.raises(ValueError, match="request-level execution only"):
pipeline.prepare_encode(state)
with pytest.raises(ValueError, match="request-level execution only"):
pipeline.denoise_step(InputBatch.make_batch([state]))
def test_step_scheduler_preserves_latent_dtype_for_mixed_progress_batches():
pipeline = _pipeline()
pipeline._pipeline = SimpleNamespace(prepare_extra_func_kwargs=lambda step, kwargs: {})
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Unit tests for ImageKVCacheManager.
Covers: cache → reuse flow, AR KV injection, CFG (sequential & parallel), SP, cross-request isolation.
@@ -37,12 +37,21 @@ class MockAttention(nn.Module):
super().__init__()
self.paged_kv_active = False
self.calls = []
self.paged_calls = []
self.paged_metadata = []
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))
from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available
if is_forward_context_available() and get_forward_context().paged_kv_adapter is not None:
self.paged_calls.append((query, key, value))
self.paged_metadata.append(attn_metadata)
if attn_metadata is not None and attn_metadata.joint_query is not None:
return torch.cat((attn_metadata.joint_query, query), dim=1)
return query
@@ -105,6 +114,7 @@ def _call_mgr(
shard_image_size=None,
gen_timestep_scatter_index=None,
position_ids=None,
full_attn_spans=None,
):
query = torch.randn(bs * q_len, NUM_HEADS, HEAD_DIM)
attn_mask = torch.zeros(bs, 1, seq_len, seq_len)
@@ -121,6 +131,7 @@ def _call_mgr(
shard_image_size=shard_image_size,
gen_timestep_scatter_index=gen_timestep_scatter_index,
position_ids=position_ids,
full_attn_spans=full_attn_spans,
)
@@ -132,6 +143,83 @@ def test_cache_manager_registers_attention_without_adding_dense_state() -> None:
assert mgr.state_dict() == {}
def test_scheduler_paged_kv_runs_piecewise_for_first_and_later_steps() -> None:
from vllm_omni.diffusion.forward_context import override_paged_kv_adapter, set_forward_context
mgr = _make_cache_mgr()
mgr.attn.paged_kv_active = True
bs = 2
prefix_lens = torch.tensor([[2], [4]], dtype=torch.long)
first_q_len = 12
first_k, first_v = _make_known_kv(bs * first_q_len, base=1.0)
full_attn_spans = [[(2, 12)], [(2, 12)]]
with set_forward_context(), override_paged_kv_adapter(object()):
_call_mgr(
mgr,
bs,
q_len=first_q_len,
seq_len=first_q_len,
key_flat=first_k,
value_flat=first_v,
first_step=True,
gen_timestep_scatter_index=prefix_lens,
full_attn_spans=full_attn_spans,
)
assert mgr.image_kv_cache_map is None
assert mgr.image_kv_cache_lens is None
assert len(mgr.attn.paged_calls) == 1
assert mgr.attn.paged_calls[0][0].shape == (bs, first_q_len, NUM_HEADS, HEAD_DIM)
assert mgr.attn.paged_calls[0][1].shape == (bs, first_q_len, NUM_KV_HEADS, HEAD_DIM)
assert mgr.attn.paged_calls[0][2].shape == (bs, first_q_len, NUM_KV_HEADS, HEAD_DIM)
assert mgr.attn.paged_metadata[0].full_attn_spans == full_attn_spans
current_q_len = IMAGE_TOKEN_LEN
current_k, current_v = _make_known_kv(bs * current_q_len, base=50.0)
_call_mgr(
mgr,
bs,
q_len=current_q_len,
seq_len=prefix_lens[-1].item() + current_q_len,
key_flat=current_k,
value_flat=current_v,
position_ids=torch.arange(4, 4 + current_q_len).repeat(bs, 1),
full_attn_spans=full_attn_spans,
)
assert len(mgr.attn.paged_calls) == 2
assert mgr.attn.paged_calls[1][0].shape == (bs, current_q_len, NUM_HEADS, HEAD_DIM)
assert mgr.attn.paged_calls[1][1].shape == (bs, current_q_len, NUM_KV_HEADS, HEAD_DIM)
assert mgr.attn.paged_calls[1][2].shape == (bs, current_q_len, NUM_KV_HEADS, HEAD_DIM)
assert mgr.attn.paged_metadata[1].full_attn_spans == full_attn_spans
def test_cache_manager_rejects_dense_imported_prefix_for_paged_attention() -> None:
from vllm_omni.diffusion.forward_context import override_paged_kv_adapter, set_forward_context
mgr = _make_cache_mgr()
mgr.attn.paged_kv_active = True
prefix_len = 4
prefix_key, prefix_value = _make_known_kv(prefix_len, base=100.0)
mgr._injected_ar_kv = [(prefix_key, prefix_value)]
current_key, current_value = _make_known_kv(3, base=1.0)
with set_forward_context(), override_paged_kv_adapter(object()):
with pytest.raises(NotImplementedError, match="does not support imported AR KV"):
_call_mgr(
mgr,
bs=1,
q_len=3,
seq_len=7,
key_flat=current_key,
value_flat=current_value,
first_step=True,
gen_timestep_scatter_index=torch.tensor([[0]]),
full_attn_spans=[[(4, 7)]],
)
@pytest.mark.parametrize(
("paged_kv_active", "expected_kv_heads"),
[(False, NUM_HEADS), (True, NUM_KV_HEADS)],
@@ -155,6 +243,7 @@ def test_gqa_kv_stays_compressed_for_paged_attention(
first_step=True,
num_image_tokens=0,
gen_timestep_scatter_index=_gen_timestep_index(1, 0),
full_attn_spans=[[(0, q_len)]] if paged_kv_active else None,
)
_, key_input, value_input = mgr.attn.calls[-1]
+21 -3
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from contextlib import contextmanager
from types import SimpleNamespace
@@ -533,7 +533,14 @@ def test_profile_run_executes_forward_without_scheduler_kv_validation(monkeypatc
return original_execute(*args, **kwargs)
runner._execute_request_list = execute
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
forward_context_calls = []
@contextmanager
def record_forward_context(*args, **kwargs):
forward_context_calls.append((args, kwargs))
yield
monkeypatch.setattr(model_runner_module, "set_forward_context", record_forward_context)
reset_peak_memory_stats = Mock()
monkeypatch.setattr(
model_runner_module.current_omni_platform,
@@ -549,6 +556,8 @@ def test_profile_run_executes_forward_without_scheduler_kv_validation(monkeypatc
assert runner.pipeline.forward_calls == 1
assert record_names == ["pipeline_memory_profile"]
assert len(forward_context_calls) == 1
assert forward_context_calls[0][1]["in_diffusion_kv_memory_profile"] is True
runner._validate_diffusion_kv_metadata.assert_not_called()
reset_peak_memory_stats.assert_not_called()
synchronize.assert_called_once_with()
@@ -575,7 +584,14 @@ def test_profile_run_executes_maximum_step_batch_without_resetting_peak(monkeypa
runner.pipeline.denoise_step = denoise_step
runner._validate_diffusion_kv_metadata = Mock(side_effect=AssertionError("profile must bypass admission"))
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
forward_context_calls = []
@contextmanager
def record_forward_context(*args, **kwargs):
forward_context_calls.append((args, kwargs))
yield
monkeypatch.setattr(model_runner_module, "set_forward_context", record_forward_context)
reset_peak_memory_stats = Mock()
monkeypatch.setattr(
model_runner_module.current_omni_platform,
@@ -592,6 +608,8 @@ def test_profile_run_executes_maximum_step_batch_without_resetting_peak(monkeypa
assert observed_batch_rows == [(2, 2)]
assert runner.state_cache == {}
assert runner.input_batch is None
assert len(forward_context_calls) == 1
assert forward_context_calls[0][1]["in_diffusion_kv_memory_profile"] is True
runner._validate_diffusion_kv_metadata.assert_not_called()
reset_peak_memory_stats.assert_not_called()
synchronize.assert_called_once_with()
@@ -0,0 +1,64 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import importlib.util
from types import SimpleNamespace
import pytest
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.mark.parametrize("available", [False, True])
def test_dense_flash_capability_tracks_mindiesd(monkeypatch, available):
pytest.importorskip("vllm_ascend")
from vllm_omni.platforms.npu.platform import NPUOmniPlatform
monkeypatch.setattr(
importlib.util,
"find_spec",
lambda name: object() if name == "mindiesd" and available else None,
)
assert NPUOmniPlatform.supports_diffusion_dense_flash_attention() is available
def test_paged_config_uses_ascend_kernel_block_size() -> None:
pytest.importorskip("vllm_ascend")
from vllm_ascend.attention.attention_v1 import AscendAttentionBackend
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
from vllm_omni.platforms.npu.platform import NPUOmniPlatform
vllm_config = SimpleNamespace(cache_config=SimpleNamespace(block_size=16))
NPUOmniPlatform.configure_diffusion_vllm_config(
vllm_config,
SimpleNamespace(diffusion_kv_mode=DiffusionKVCacheMode.PAGED_SCHEDULER),
)
assert vllm_config.cache_config.block_size == AscendAttentionBackend.get_supported_kernel_block_sizes()[0]
def test_strict_ulysses_paged_backend_bypasses_ascend_pcp() -> None:
pytest.importorskip("vllm_ascend")
from vllm_ascend.attention.attention_v1 import (
AscendAttentionBackend,
AscendAttentionBackendImpl,
AscendAttentionMetadataBuilder,
)
from vllm_omni.platforms.npu.platform import NPUOmniPlatform
assert (
NPUOmniPlatform.get_diffusion_paged_kv_attn_backend(
AscendAttentionBackend,
ulysses_degree=1,
)
is AscendAttentionBackend
)
backend = NPUOmniPlatform.get_diffusion_paged_kv_attn_backend(
AscendAttentionBackend,
ulysses_degree=2,
)
assert backend.get_impl_cls() is AscendAttentionBackendImpl
assert backend.get_builder_cls() is AscendAttentionMetadataBuilder
+8 -6
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Unit tests for OmniPlatform.record_device_event implementations."""
import pytest
@@ -122,8 +122,8 @@ class TestNPUOmniPlatformRecordDeviceEvent:
result = NPUOmniPlatform.record_device_event()
assert result is None
def test_synchronizes_stream_then_records_event(self, mocker):
"""NPU should sync stream first, then create and record an Event."""
def test_synchronizes_stream_then_records_generic_event(self, mocker):
"""NPU should return an event consumable by the generic torch.Stream."""
try:
from vllm_omni.platforms.npu.platform import NPUOmniPlatform
except ModuleNotFoundError:
@@ -133,14 +133,16 @@ class TestNPUOmniPlatformRecordDeviceEvent:
mock_stream = mocker.MagicMock()
mock_torch = mocker.MagicMock()
mock_torch.npu.current_stream.return_value = mock_stream
mock_torch.npu.Event.return_value = mock_event
mock_torch.Event.return_value = mock_event
mocker.patch("vllm_omni.platforms.npu.platform.torch", mock_torch)
result = NPUOmniPlatform.record_device_event()
# Stream should be synced first (HCCL ordering)
mock_stream.synchronize.assert_called_once()
# Then event should be created and recorded
mock_torch.npu.Event.assert_called_once()
# Then a backend-neutral event should be created and recorded. The
# worker waits on it through the public torch.Stream wrapper.
mock_torch.Event.assert_called_once()
mock_torch.npu.Event.assert_not_called()
mock_event.record.assert_called_once()
assert result is mock_event
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import os
from functools import partial
@@ -7,11 +7,7 @@ from functools import partial
import torch
from vllm.logger import init_logger
from vllm_omni.diffusion.attention.backends.abstract import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
)
from vllm_omni.diffusion.attention.backends.abstract import AttentionBackend, AttentionImpl, AttentionMetadata
from vllm_omni.diffusion.attention.backends.sdpa import _maybe_reshape_attn_mask
from vllm_omni.diffusion.attention.backends.utils.piecewise_attn import (
piecewise_attn,
@@ -285,7 +281,9 @@ class FlashAttentionImpl(AttentionImpl):
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.
and kernel details out of Omni's common ``Attention`` layer. CUDA uses
the native vLLM FlashAttention writer/kernel contract. Ascend writes
the complete K/V span once before its piecewise FIA calls.
"""
layer = getattr(paged_kv_context, "layer", None)
@@ -297,8 +295,18 @@ class FlashAttentionImpl(AttentionImpl):
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(
prewrite_kv = current_omni_platform.requires_diffusion_paged_kv_prewrite()
read_kv_from_cache = prewrite_kv and layer.attn_backend.forward_includes_kv_cache_update
# The GPU/default path preserves native cache-update ownership. Ascend
# prewrites through vLLM-Ascend's normal-layout writer so piecewise FIA
# segments do not repeatedly scatter the same layer K/V.
if not layer.attn_backend.forward_includes_kv_cache_update or prewrite_kv:
cache_update = getattr(native_impl, "do_kv_cache_update", None)
if not callable(cache_update):
raise RuntimeError(
f"Native attention implementation for {layer.layer_name!r} cannot update the KV cache"
)
cache_update(
layer,
paged_kv_context.key_write,
paged_kv_context.value_write,
@@ -308,33 +316,52 @@ class FlashAttentionImpl(AttentionImpl):
def run_native_attention(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
key: torch.Tensor | None,
value: torch.Tensor | None,
native_metadata,
output: torch.Tensor | None,
) -> torch.Tensor:
output = torch.empty(
(query.shape[0], layer.num_heads, layer.head_size_v),
dtype=query.dtype,
device=query.device,
)
if output is None:
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,
None if read_kv_from_cache else key,
None if read_kv_from_cache else value,
kv_cache,
native_metadata,
output,
)
if paged_kv_context.piecewise_plan is not None:
# Ascend FIA can execute identical CFG rows as one batched call
# per piece. Keep that batch layout through the piecewise runner
# so it can concatenate row-major results instead of emitting an
# indexed ScatterUpdate for every segment. The CUDA path keeps
# its existing output-buffer contract (including graph capture).
use_homogeneous_batch = (
current_omni_platform.is_npu() and paged_kv_context.piecewise_plan.homogeneous_batch_shape is not None
)
output = None
if not use_homogeneous_batch:
output = torch.empty(
(paged_kv_context.query.shape[0], layer.num_heads, layer.head_size_v),
dtype=paged_kv_context.query.dtype,
device=paged_kv_context.query.device,
)
output = run_paged_piecewise_plan(
paged_kv_context.query,
paged_kv_context.key_write,
paged_kv_context.value_write,
None if read_kv_from_cache else paged_kv_context.key_write,
None if read_kv_from_cache else paged_kv_context.value_write,
paged_kv_context.piecewise_plan,
paged_kv_context.piecewise_native_metadata,
run_native_attention,
output_buffer=output,
use_homogeneous_batch=use_homogeneous_batch,
)
else:
output = run_native_attention(
@@ -342,6 +369,7 @@ class FlashAttentionImpl(AttentionImpl):
paged_kv_context.key_write,
paged_kv_context.value_write,
paged_kv_context.native_metadata,
None,
)
return paged_kv_context.restore_output(output)
@@ -516,15 +544,8 @@ class FlashAttentionImpl(AttentionImpl):
value: torch.Tensor,
attn_metadata: AttentionMetadata = None,
) -> torch.Tensor:
try:
from mindiesd import attention_forward
except ImportError:
raise ImportError(
"FlashAttentionBackend NPU implementation requires MindIE-SD. "
"Please install MindIE-SD to enable NPU attention support. "
"For installation details, see https://gitcode.com/Ascend/MindIE-SD"
"Otherwise, use SDPA backend by setting DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA"
)
from mindiesd import attention_forward
# Opt-in mask-free paths (mirror the CUDA cu_seqlens behavior): the
# model marks extra["npu_attn_varlen"] and carries packed metadata, so
# the padding document is excluded without reading or materializing
@@ -633,15 +654,8 @@ class FlashAttentionImpl(AttentionImpl):
return None
seq_q, seq_k = resolved
try:
from mindiesd import attention_forward_varlen
except ImportError:
raise ImportError(
"FlashAttentionBackend NPU implementation requires MindIE-SD. "
"Please install MindIE-SD to enable NPU attention support. "
"For installation details, see https://gitcode.com/Ascend/MindIE-SD"
"Otherwise, use SDPA backend by setting DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA"
)
from mindiesd import attention_forward_varlen
q = query.squeeze(0) # [T, N, D] == TND
k = key.squeeze(0)
v = value.squeeze(0)
@@ -689,15 +703,8 @@ class FlashAttentionImpl(AttentionImpl):
_, seq_k = resolved
used_k = seq_k[0] # real document length (first cumulative end)
try:
from mindiesd import attention_forward
except ImportError:
raise ImportError(
"FlashAttentionBackend NPU implementation requires MindIE-SD. "
"Please install MindIE-SD to enable NPU attention support. "
"For installation details, see https://gitcode.com/Ascend/MindIE-SD"
"Otherwise, use SDPA backend by setting DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA"
)
from mindiesd import attention_forward
# mindiesd always takes BSND input regardless of `layout`; the arg
# selects the op-internal layout and laser only supports BNSD, so do
# NOT forward the model's qkv_layout ("BSND" for MiniMax-H3) here.
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
Piecewise attention for mixed causal / full (bidirectional) masks.
@@ -92,6 +92,7 @@ def piecewise_attn(
):
_check_homogeneous(full_attn_spans)
spans = full_attn_spans[0]
ranges: tuple[tuple[int, int, int], ...]
if query_ranges is None:
query_len = query.shape[1]
ranges = ((0, query_len, key.shape[1] - query_len),)
@@ -133,6 +134,11 @@ class PagedPiecewiseSegment:
row_segments: tuple[Segment, ...]
query_indices: torch.Tensor
query_range: tuple[int, int] | None
# Local [start, end) range shared by every row when the batch can retain
# its original [B, S] layout. The packed query_indices remain available
# for metadata construction and the heterogeneous fallback.
local_query_range: tuple[int, int] | None = None
@dataclass(frozen=True, slots=True)
@@ -142,10 +148,16 @@ class PagedPiecewisePlan:
spans: tuple[tuple[tuple[int, int], ...], ...]
segments: tuple[PagedPiecewiseSegment, ...]
num_query_tokens: int
segments_cover_query_contiguously: bool
# (batch_size, query_len) when all rows have the same local segment
# layout. Native FIA still receives flattened row-major tokens, but the
# runner can restore outputs with one batch concat instead of one indexed
# scatter per segment.
homogeneous_batch_shape: tuple[int, int] | None = None
PagedPiecewiseRunner = Callable[
[torch.Tensor, torch.Tensor, torch.Tensor, object],
[torch.Tensor, torch.Tensor | None, torch.Tensor | None, object, torch.Tensor | None],
torch.Tensor,
]
@@ -164,7 +176,9 @@ def build_paged_piecewise_plan(
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)
spans: tuple[tuple[tuple[int, int], ...], ...] = tuple(
tuple((span_start, span_end) for span_start, span_end 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):
@@ -188,46 +202,206 @@ def build_paged_piecewise_plan(
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 = []
query_indices: list[int] = []
local_ranges: list[tuple[int, int]] = []
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
local_ranges.append((local_start, local_end))
query_indices.extend(range(packed_offsets[row_index] + local_start, packed_offsets[row_index] + local_end))
query_range = None
if query_indices and query_indices == list(range(query_indices[0], query_indices[-1] + 1)):
query_range = (query_indices[0], query_indices[-1] + 1)
local_query_range = local_ranges[0] if len(set(local_ranges)) == 1 else None
packed_segments.append(
PagedPiecewiseSegment(
row_segments=tuple(row_segments),
query_indices=torch.tensor(query_indices, dtype=torch.long, device=device),
query_range=query_range,
local_query_range=local_query_range,
)
)
return PagedPiecewisePlan(spans, tuple(packed_segments), packed_offsets[-1])
covered = 0
for segment in packed_segments:
if segment.query_range is None or segment.query_range[0] != covered:
break
covered = segment.query_range[1]
segments_cover_query_contiguously = covered == packed_offsets[-1]
homogeneous_batch_shape: tuple[int, int] | None = None
if (
row_count > 1
and len(set(query_lens)) == 1
and all(segment.local_query_range is not None for segment in packed_segments)
):
# A common local range is sufficient even when rows have different
# global offsets or KV lengths; those values remain row-specific in
# native metadata.
homogeneous_batch_shape = (row_count, int(query_lens[0]))
return PagedPiecewisePlan(
spans=spans,
segments=tuple(packed_segments),
num_query_tokens=packed_offsets[-1],
segments_cover_query_contiguously=segments_cover_query_contiguously,
homogeneous_batch_shape=homogeneous_batch_shape,
)
def _run_homogeneous_paged_piecewise_plan(
query: torch.Tensor,
key: torch.Tensor | None,
value: torch.Tensor | None,
plan: PagedPiecewisePlan,
segment_metadata: Sequence[object],
segment_runner: PagedPiecewiseRunner,
output_buffer: torch.Tensor | None,
) -> torch.Tensor:
"""Run a homogeneous batch while retaining the old [B, S] layout.
The native FIA contract is flattened TND, so each segment is flattened
only for the kernel call. Results are reshaped back to [B, L] and
concatenated along the sequence dimension, which avoids the per-segment
``index_copy_`` used by heterogeneous packed rows.
"""
batch_shape = plan.homogeneous_batch_shape
assert batch_shape is not None
batch_size, query_len = batch_shape
if query.shape[0] != batch_size * query_len:
raise ValueError(
f"Homogeneous paged piecewise plan expects {batch_size * query_len} query tokens, got {query.shape[0]}"
)
query_rows = query.reshape(batch_size, query_len, *query.shape[1:])
key_rows = None if key is None else key.reshape(batch_size, query_len, *key.shape[1:])
value_rows = None if value is None else value.reshape(batch_size, query_len, *value.shape[1:])
segment_outputs: list[torch.Tensor] = []
for segment, metadata in zip(plan.segments, segment_metadata, strict=True):
local_range = segment.local_query_range
if local_range is None:
raise ValueError("Homogeneous paged piecewise execution requires a shared local segment range")
start, end = local_range
segment_query = query_rows[:, start:end].contiguous().reshape(-1, *query.shape[1:])
segment_key = None if key_rows is None else key_rows[:, start:end].contiguous().reshape(-1, *key.shape[1:])
segment_value = (
None if value_rows is None else value_rows[:, start:end].contiguous().reshape(-1, *value.shape[1:])
)
segment_output = segment_runner(segment_query, segment_key, segment_value, metadata, None)
expected_tokens = batch_size * (end - start)
if segment_output.shape[0] != expected_tokens:
raise ValueError(
f"Paged piecewise runner returned {segment_output.shape[0]} tokens "
f"for a {expected_tokens}-token homogeneous segment"
)
segment_outputs.append(segment_output.reshape(batch_size, end - start, *segment_output.shape[1:]))
if not segment_outputs:
result = torch.empty_like(query)
elif len(segment_outputs) == 1:
result = segment_outputs[0].reshape(plan.num_query_tokens, *segment_outputs[0].shape[2:])
else:
result_rows = torch.cat(segment_outputs, dim=1)
result = result_rows.reshape(plan.num_query_tokens, *result_rows.shape[2:])
if output_buffer is not None:
if output_buffer.shape != result.shape:
raise ValueError(
f"Paged piecewise output buffer shape {tuple(output_buffer.shape)} does not match "
f"homogeneous result shape {tuple(result.shape)}"
)
output_buffer.copy_(result)
return output_buffer
return result
def run_paged_piecewise_plan(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
key: torch.Tensor | None,
value: torch.Tensor | None,
plan: PagedPiecewisePlan,
segment_metadata: Sequence[object],
segment_runner: PagedPiecewiseRunner,
output_buffer: torch.Tensor | None = None,
*,
use_homogeneous_batch: bool = False,
) -> torch.Tensor:
"""Run and scatter piecewise attention over packed Q/K/V write tensors."""
"""Run piecewise attention with view fast paths for contiguous segments.
``use_homogeneous_batch`` is an opt-in for native backends that can keep
identical rows in the legacy batch layout. The default remains the
indexed packed path so existing GPU/heterogeneous contracts are unchanged.
"""
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]}")
if output_buffer is not None and output_buffer.shape[0] != plan.num_query_tokens:
raise ValueError(
f"Paged piecewise output buffer has {output_buffer.shape[0]} tokens; expected {plan.num_query_tokens}"
)
output = None
if use_homogeneous_batch and plan.homogeneous_batch_shape is not None:
return _run_homogeneous_paged_piecewise_plan(
query,
key,
value,
plan,
segment_metadata,
segment_runner,
output_buffer,
)
output = output_buffer
contiguous_outputs = []
for segment, metadata in zip(plan.segments, segment_metadata, strict=True):
indices = segment.query_indices
segment_output_buffer = None
if segment.query_range is None:
segment_query = query.index_select(0, indices)
segment_key = None if key is None else key.index_select(0, indices)
segment_value = None if value is None else value.index_select(0, indices)
else:
start, end = segment.query_range
segment_query = query[start:end]
segment_key = None if key is None else key[start:end]
segment_value = None if value is None else value[start:end]
if output is not None:
segment_output_buffer = output[start:end]
segment_output = segment_runner(
query.index_select(0, indices),
key.index_select(0, indices),
value.index_select(0, indices),
segment_query,
segment_key,
segment_value,
metadata,
segment_output_buffer,
)
if segment_output.shape[0] != indices.shape[0]:
raise ValueError(
f"Paged piecewise runner returned {segment_output.shape[0]} tokens "
f"for a {indices.shape[0]}-token segment"
)
if segment_output_buffer is not None and segment_output is not segment_output_buffer:
if segment_output.shape != segment_output_buffer.shape:
raise ValueError(
f"Paged piecewise runner returned shape {tuple(segment_output.shape)} "
f"for output buffer shape {tuple(segment_output_buffer.shape)}"
)
segment_output_buffer.copy_(segment_output)
segment_output = segment_output_buffer
if plan.segments_cover_query_contiguously:
if output_buffer is None:
contiguous_outputs.append(segment_output)
continue
if output is None:
output = segment_output.new_empty((query.shape[0], *segment_output.shape[1:]))
output.index_copy_(0, indices, segment_output)
if segment.query_range is None:
output.index_copy_(0, indices, segment_output)
else:
start, end = segment.query_range
output[start:end].copy_(segment_output)
if output_buffer is not None and plan.segments_cover_query_contiguously:
return output_buffer
if contiguous_outputs:
if len(contiguous_outputs) == 1:
return contiguous_outputs[0]
return torch.cat(contiguous_outputs, dim=0)
assert output is not None
return output
+111 -6
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# Copyright (c) Microsoft Corporation and Jiarui Fang
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team & Jiarui Fang
@@ -23,6 +23,7 @@ from vllm_omni.diffusion.attention.parallel.base import NoParallelAttention
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.diffusion_kv.config import DiffusionKVCacheMode
from vllm_omni.diffusion.distributed.parallel_state import get_sp_group
from vllm_omni.diffusion.forward_context import (
get_forward_context,
@@ -44,6 +45,8 @@ def _try_extract_layer_index(prefix: str) -> int | None:
class Attention(nn.Module):
_scheduler_paged_kv = False
def __init__(
self,
num_heads: int,
@@ -111,6 +114,44 @@ class Attention(nn.Module):
role_category=role_category,
allow_trtllm_default=allow_trtllm_default,
)
scheduler_paged_kv = (
config is not None
and getattr(config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
is DiffusionKVCacheMode.PAGED_SCHEDULER
)
self._scheduler_paged_kv = scheduler_paged_kv
if (
scheduler_paged_kv
and paged_kv_cache_role is not None
and spec is None
and not attn_backend_cls.supports_paged_kv
):
# FLASH_ATTN is an Omni selector, not a device-specific kernel.
# vLLM resolves it to CUDA FlashAttention on GPU and Ascend native
# attention on NPU. A platform may legitimately default dense
# attention to SDPA (for example when an optional diffusion FA
# package is absent), but a paged layer must advertise the
# capability so the Worker can register its native cache view.
# Resolve the selector only for this marked layer; unmarked dense
# attention keeps the platform default unchanged.
from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec
dense_backend_name = attn_backend_cls.get_name()
paged_attention_config = AttentionConfig(default=AttentionSpec(backend="FLASH_ATTN"))
attn_backend_cls, spec = get_attn_backend_for_role(
role=role,
head_size=head_size,
attention_config=paged_attention_config,
role_category=role_category,
allow_trtllm_default=allow_trtllm_default,
)
logger.info(
"Resolved marked paged diffusion attention role=%r to %r because the platform default %r "
"does not support Scheduler-owned KV",
role,
attn_backend_cls.get_name(),
dense_backend_name,
)
parallel_config = getattr(config, "parallel_config", None)
allgather_degree = getattr(parallel_config, "allgather_degree", 1)
# TODO: Move AllGather-KV compatibility into an AttentionBackend capability
@@ -321,6 +362,17 @@ class Attention(nn.Module):
# Get the appropriate parallel strategy based on SP active state
strategy = self._get_active_parallel_strategy()
paged_adapter = self._active_paged_kv_adapter()
in_kv_memory_profile = is_forward_context_available() and get_forward_context().in_diffusion_kv_memory_profile
if (
self._scheduler_paged_kv
and self.paged_kv_cache_role is not None
and paged_adapter is None
and not in_kv_memory_profile
):
raise RuntimeError(
"Scheduler-paged diffusion attention reached model forward without an active Worker adapter. "
"Only the startup KV memory profile may execute before paged KV initialization."
)
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(
@@ -343,9 +395,39 @@ class Attention(nn.Module):
# For Ring: Concat joint_q
query, key, value, attn_metadata, ctx = strategy.pre_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.
# Scheduler rows describe the logical sequence, while strict Ulysses
# may append synthetic tokens solely to make the image shard divisible.
# Remove those tokens after the all-to-all and put zero placeholders
# back before the reverse all-to-all.
paged_sp_padding = 0
paged_sp_padding_offset = query.shape[1]
if use_paged_attention and strategy is not self._no_parallel_strategy and strategy.name == "ulysses":
forward_ctx = get_forward_context() if is_forward_context_available() else None
paged_sp_padding = int(getattr(forward_ctx, "sp_padding_size", 0))
if paged_sp_padding:
joint_len = int(getattr(ctx, "joint_len", 0))
joint_strategy = str(getattr(ctx, "joint_strategy", "front"))
paged_sp_padding_offset = query.shape[1] - (joint_len if joint_strategy == "rear" else 0)
padding_start = paged_sp_padding_offset - paged_sp_padding
if padding_start < 0:
raise ValueError(
"Paged Ulysses padding exceeds the post-all-to-all sequence: "
f"padding={paged_sp_padding}, sequence={query.shape[1]}"
)
def _remove_paged_sp_padding(tensor: torch.Tensor) -> torch.Tensor:
return torch.cat(
(tensor[:, :padding_start], tensor[:, paged_sp_padding_offset:]),
dim=1,
).contiguous()
query = _remove_paged_sp_padding(query)
key = _remove_paged_sp_padding(key)
value = _remove_paged_sp_padding(value)
# 2. This is the shared GPU/NPU boundary. The Worker adapter prepares
# the native page-table context after SP has produced rank-local Q/K/V;
# backend resolution below selects CUDA or Ascend execution.
if use_paged_attention:
assert paged_adapter is not None
paged_kv_context = paged_adapter.prepare_layer_context(
@@ -363,6 +445,11 @@ class Attention(nn.Module):
else:
out = self._run_local_attention(query, key, value, attn_metadata)
if paged_sp_padding:
padding_start = paged_sp_padding_offset - paged_sp_padding
output_padding = out.new_zeros((out.shape[0], paged_sp_padding, *out.shape[2:]))
out = torch.cat((out[:, :padding_start], output_padding, out[:, padding_start:]), dim=1)
# 3. Post-processing (Reverse Communication)
# For Ulysses: AllToAll Output, and AllGather Joint Output
out = strategy.post_attention(out, ctx)
@@ -371,7 +458,7 @@ class Attention(nn.Module):
@staticmethod
def _active_paged_kv_adapter():
"""Return the opaque Worker adapter installed for this forward."""
"""Return the Worker adapter selected by Runner-owned metadata."""
if not is_forward_context_available():
return None
@@ -392,7 +479,25 @@ class Attention(nn.Module):
)
return self.sdpa_fallback.forward(query, key, value, attn_metadata)
# Fallback to standard attention
in_kv_memory_profile = is_forward_context_available() and get_forward_context().in_diffusion_kv_memory_profile
# NPU only: the startup KV-capacity profile needs tensor shapes, not a
# paged attention result. If dense MindIE-SD is absent, SDPA provides
# that profile forward. Formal GPU/NPU paged requests never use this
# branch because their Worker adapter is active.
if (
self._scheduler_paged_kv
and self.paged_kv_cache_role is not None
and in_kv_memory_profile
and current_omni_platform.is_npu()
and self.attn_backend.get_name() == "FLASH_ATTN"
and not current_omni_platform.supports_diffusion_dense_flash_attention()
):
logger.warning_once(
"The startup KV memory profile is using SDPA because MindIE-SD is unavailable. "
"Formal paged requests still use the platform-native paged attention backend."
)
return self.sdpa_fallback.forward(query, key, value, attn_metadata)
return self.attention.forward(query, key, value, attn_metadata)
def _assert_piecewise_compatible(self, attn_metadata: AttentionMetadata | None) -> None:
+2 -3
View File
@@ -1086,10 +1086,9 @@ class OmniDiffusionConfig:
"need_recv_cache", False
):
raise ValueError(
"paged_scheduler Diffusion KV does not support imported AR KV in Phase 1; "
"disable need_recv_cache until connector-aware admission is implemented"
"paged_scheduler Diffusion KV does not support imported AR KV; "
"disable need_recv_cache until connector-aware import is implemented"
)
self.master_port = self._resolve_master_port()
self.request_batch_max_wait_ms = float(self.request_batch_max_wait_ms or 0.0)
if not math.isfinite(self.request_batch_max_wait_ms) or self.request_batch_max_wait_ms < 0:
@@ -1,10 +1,10 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import copy
from collections.abc import Mapping, Sequence
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any
@@ -22,9 +22,9 @@ from vllm_omni.diffusion.diffusion_kv.metadata import DiffusionKVMetadata
from vllm_omni.diffusion.diffusion_kv.paged_attention_adapter import (
DiffusionPagedAttentionAdapter,
DiffusionPagedAttentionLayerAdapter,
DiffusionPagedAttentionRow,
DiffusionPagedAttentionMetadata,
DiffusionPagedAttentionRowBinding,
PreparedDiffusionPagedAttentionBatch,
DiffusionPagedAttentionRuntime,
)
from vllm_omni.platforms import current_omni_platform
@@ -32,13 +32,6 @@ 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
@@ -46,6 +39,13 @@ class _DiffusionKVRowInstall:
block_ids: tuple[tuple[int, ...], ...]
@dataclass(frozen=True)
class _DiffusionKVRequestState:
generation: int
snapshot: DiffusionKVSnapshot
row_installs: tuple[_DiffusionKVRowInstall, ...]
class DiffusionKVModelRunnerBackend:
"""Native paged-KV state and BlockTable operations for a model runner."""
@@ -503,7 +503,7 @@ class DiffusionKVModelRunnerBackend:
self._diffusion_kv_request_states[metadata.request_id] = _DiffusionKVRequestState(
generation=metadata.allocation_generation,
snapshot=snapshot,
row_token_lens=current_state.row_token_lens,
row_installs=current_state.row_installs,
)
return False
@@ -530,7 +530,7 @@ class DiffusionKVModelRunnerBackend:
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),
row_installs=tuple(installs),
)
return True
@@ -559,24 +559,11 @@ class DiffusionKVModelRunnerBackend:
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.
def activate_paged_attention_metadata(self, metadata: DiffusionPagedAttentionMetadata):
"""Activate Runner-owned request metadata for an internal denoise loop."""
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)
runtime = DiffusionPagedAttentionRuntime(self.get_paged_attention_adapter(), metadata)
return runtime, runtime.activate()
def _resolve_paged_attention_row(
self,
@@ -588,11 +575,12 @@ class DiffusionKVModelRunnerBackend:
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:
for install in request_state.row_installs:
if install.identity == identity:
return DiffusionPagedAttentionRowBinding(
row_index=row_index,
max_seq_len=token_len,
max_seq_len=install.token_len,
block_ids=install.block_ids,
)
raise RuntimeError(f"Diffusion KV request state is missing logical length for {identity!r}")
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
@@ -34,6 +34,7 @@ class DiffusionPagedAttentionRowBinding:
row_index: int
max_seq_len: int
block_ids: tuple[tuple[int, ...], ...] = ()
DiffusionKVRowResolver = Callable[
@@ -100,6 +101,10 @@ class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
finally:
attention_config.backend = previous_backend
attention_config.backend_per_kind = previous_backend_per_kind
attn_backend = current_omni_platform.get_diffusion_paged_kv_attn_backend(
attn_backend,
ulysses_degree=ulysses_degree,
)
canonical_spec = replace(
spec,
num_kv_heads=num_kv_heads,
@@ -152,9 +157,11 @@ class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
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)
):
needs_explicit_cache_update = (
not self.attn_backend.forward_includes_kv_cache_update
or current_omni_platform.requires_diffusion_paged_kv_prewrite()
)
if needs_explicit_cache_update and not callable(getattr(impl, "do_kv_cache_update", None)):
raise RuntimeError(
f"Native paged-attention backend {self.attn_backend.get_name()!r} cannot update the KV cache"
)
@@ -204,6 +211,30 @@ class DiffusionPagedAttentionRow:
return (self.request_id, self.sequence_id, self.context_id)
@dataclass(frozen=True)
class DiffusionPagedAttentionMetadata:
"""Runner-owned row layouts for one request-level denoise loop."""
prefill_rows: tuple[DiffusionPagedAttentionRow, ...]
denoise_rows: tuple[DiffusionPagedAttentionRow, ...]
def __post_init__(self) -> None:
if not self.prefill_rows or not self.denoise_rows:
raise ValueError("Paged attention metadata requires both prefill and denoise rows")
prefill_identities = tuple(row.identity for row in self.prefill_rows)
denoise_identities = tuple(row.identity for row in self.denoise_rows)
if prefill_identities != denoise_identities:
raise ValueError(
"Paged attention prefill and denoise rows must use the same ordered identities: "
f"prefill={prefill_identities!r}, denoise={denoise_identities!r}"
)
def rows_for_step(self, step_idx: int | None) -> tuple[DiffusionPagedAttentionRow, ...]:
if type(step_idx) is not int or step_idx < 0:
raise ValueError(f"Paged attention requires a non-negative denoise step index, got {step_idx!r}")
return self.prefill_rows if step_idx == 0 else self.denoise_rows
@dataclass(frozen=True)
class PreparedDiffusionPagedAttentionBatch:
"""Native metadata shared by all paged attention layers in one forward."""
@@ -236,13 +267,81 @@ class DiffusionPagedAttentionContext:
piecewise_native_metadata: tuple[Any, ...]
query_token_shape: tuple[int, ...]
query_has_head_dims: bool
output_scatter_indices: torch.Tensor | None = None
def restore_output(self, output: torch.Tensor) -> torch.Tensor:
if self.output_scatter_indices is not None:
flat_output = output.reshape(-1, self.layer.num_heads, self.layer.head_size_v)
restored = output.new_zeros(
(*self.query_token_shape, self.layer.num_heads, self.layer.head_size_v)
).reshape(-1, self.layer.num_heads, self.layer.head_size_v)
restored.index_copy_(0, self.output_scatter_indices, flat_output)
if self.query_has_head_dims:
return restored.reshape(
*self.query_token_shape,
self.layer.num_heads,
self.layer.head_size_v,
)
return restored.reshape(
*self.query_token_shape,
self.layer.num_heads * self.layer.head_size_v,
)
if self.query_has_head_dims:
return output.reshape(*self.query_token_shape, self.layer.num_heads, self.layer.head_size_v)
return output.reshape(*self.query_token_shape, self.layer.num_heads * self.layer.head_size_v)
class DiffusionPagedAttentionRuntime:
"""Activate Runner-prepared rows lazily as the denoise loop advances."""
def __init__(
self,
adapter: DiffusionPagedAttentionAdapter,
metadata: DiffusionPagedAttentionMetadata,
) -> None:
self.adapter = adapter
self.metadata = metadata
self._active_phase: bool | None = None
self._activation: Any | None = None
self._closed = False
def ensure_active(self, step_idx: int | None) -> DiffusionPagedAttentionAdapter:
if self._closed:
raise RuntimeError("Paged attention runtime is already closed")
is_prefill = step_idx == 0
rows = self.metadata.rows_for_step(step_idx)
if self._active_phase == is_prefill:
return self.adapter
self._deactivate()
batch = self.adapter.prepare_batch(rows)
activation = self.adapter.activate(batch)
activation.__enter__()
self._activation = activation
self._active_phase = is_prefill
return self.adapter
def _deactivate(self) -> None:
activation = self._activation
self._activation = None
self._active_phase = None
if activation is not None:
activation.__exit__(None, None, None)
def close(self) -> None:
if self._closed:
return
self._deactivate()
self._closed = True
@contextmanager
def activate(self) -> Iterator[DiffusionPagedAttentionRuntime]:
try:
yield self
finally:
self.close()
class DiffusionPagedAttentionAdapter:
"""Translate diffusion rows into vLLM metadata for an Omni paged backend."""
@@ -545,15 +644,16 @@ class DiffusionPagedAttentionAdapter:
)
@staticmethod
def _validate_token_layout(
def _token_packing_indices(
token_shape: tuple[int, ...],
batch: PreparedDiffusionPagedAttentionBatch,
*,
name: str,
) -> None:
device: torch.device,
) -> torch.Tensor | None:
if len(token_shape) == 1:
if token_shape[0] == batch.num_tokens:
return
return None
raise ValueError(
f"Paged attention {name} token count must match the prepared write batch: "
f"tokens={token_shape[0]}, prepared={batch.num_tokens}"
@@ -561,11 +661,23 @@ class DiffusionPagedAttentionAdapter:
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}"
if batch_size != len(batch.rows) or any(query_len > tokens_per_row for query_len in query_lens):
raise ValueError(
f"Paged attention batched {name} layout must match prepared rows for the current write: "
f"shape={token_shape}, row_query_lens={query_lens}"
)
if all(query_len == tokens_per_row for query_len in query_lens):
return None
return torch.cat(
[
torch.arange(
row_index * tokens_per_row,
row_index * tokens_per_row + query_len,
dtype=torch.long,
device=device,
)
for row_index, query_len in enumerate(query_lens)
]
)
raise ValueError(
f"Paged attention {name} supports packed [T, ...] or uniform batched [B, T, ...] token layouts; "
@@ -626,19 +738,39 @@ class DiffusionPagedAttentionAdapter:
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"),
)
if packed_segment.query_range is None:
positions = batch.positions.index_select(0, packed_segment.query_indices)
slot_mappings = batch.slot_mappings.index_select(-1, packed_segment.query_indices)
else:
start, end = packed_segment.query_range
positions = batch.positions[start:end]
slot_mappings = batch.slot_mappings[..., start:end]
segment_metadata = self._build_native_metadata(
query_lens=query_lens,
query_start_loc=query_start_loc_cpu.to(self.device),
query_start_loc_cpu=query_start_loc_cpu,
seq_lens=seq_lens_cpu.to(self.device),
seq_lens_cpu=seq_lens_cpu,
positions=positions,
block_tables=batch.block_tables,
slot_mappings=slot_mappings,
causal=(row_segments[0].mode == "causal"),
)
# FA3's full-CUDA-graph metadata builder reuses one persistent
# scheduler buffer across builds. Piecewise attention prepares all
# segments before executing any of them, so each segment needs its
# own snapshot rather than a view overwritten by the next build.
snapshotted = set()
for native_metadata in segment_metadata.values():
metadata_id = id(native_metadata)
if metadata_id in snapshotted:
continue
snapshotted.add(metadata_id)
for field_name in ("scheduler_metadata", "prefix_scheduler_metadata"):
scheduler_metadata = getattr(native_metadata, field_name, None)
if isinstance(scheduler_metadata, torch.Tensor):
setattr(native_metadata, field_name, scheduler_metadata.clone())
metadata_by_segment.append(segment_metadata)
self._active_piecewise_native_metadata = tuple(metadata_by_segment)
return self._active_piecewise_native_metadata
@@ -678,9 +810,36 @@ class DiffusionPagedAttentionAdapter:
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 key_token_shape != query_token_shape:
if len(key_token_shape) == 1:
raise ValueError(
"Paged attention key token count must match the query layout: "
f"query={query_token_shape}, key={key_token_shape}"
)
raise ValueError(
"Paged attention batched key layout must match prepared rows and the query layout: "
f"query={query_token_shape}, key={key_token_shape}"
)
if value_token_shape != query_token_shape:
if len(value_token_shape) == 1:
raise ValueError(
"Paged attention value token count must match the query layout: "
f"query={query_token_shape}, value={value_token_shape}"
)
raise ValueError(
"Paged attention batched value layout must match prepared rows and the query layout: "
f"query={query_token_shape}, value={value_token_shape}"
)
output_scatter_indices = self._token_packing_indices(
query_token_shape,
batch,
name="Q/K/V",
device=query.device,
)
if output_scatter_indices is not None:
query_flat = query_flat.index_select(0, output_scatter_indices)
key_flat = key_flat.index_select(0, output_scatter_indices)
value_flat = value_flat.index_select(0, output_scatter_indices)
if query.device != self.device or key.device != self.device or value.device != self.device:
raise ValueError(
f"Paged attention Q/K/V must be on {self.device}; "
@@ -711,10 +870,18 @@ class DiffusionPagedAttentionAdapter:
f"expected {batch.num_tokens}"
)
# vLLM's writer consumes slot_mapping directly, so a span that starts
# vLLM's CUDA writer consumes the source token strides directly, so
# preserve strided QKV-projection views instead of materializing an
# extra K/V copy here. Platform writers with stricter contracts (for
# example, Ascend PA scatter) make their inputs contiguous locally.
# The native CUDA writer still assumes heads and head dimensions are
# packed within each token; materialize unusual inner layouts only.
if key_flat.stride(-1) != 1 or (key_flat.shape[-2] > 1 and key_flat.stride(-2) != key_flat.shape[-1]):
key_flat = key_flat.contiguous()
if value_flat.stride(-1) != 1 or (value_flat.shape[-2] > 1 and value_flat.stride(-2) != value_flat.shape[-1]):
value_flat = value_flat.contiguous()
# The writer also consumes slot_mapping directly, so a span that starts
# inside a physical block naturally writes only the requested suffix.
key_flat = key_flat.contiguous()
value_flat = value_flat.contiguous()
try:
native_metadata = batch.attn_metadata[layer_name]
except KeyError as exc:
@@ -743,6 +910,7 @@ class DiffusionPagedAttentionAdapter:
piecewise_native_metadata=piecewise_native_metadata,
query_token_shape=query_token_shape,
query_has_head_dims=query_has_head_dims,
output_scatter_indices=output_scatter_indices,
)
@staticmethod
+21
View File
@@ -29,11 +29,19 @@ class ForwardContext:
vllm_config: VllmConfig | None = None
omni_diffusion_config: OmniDiffusionConfig | None = None
attn_metadata: dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None = None
# Runner-owned paged execution metadata/runtime. Attention resolves the
# active Worker adapter from it; model code must not construct BlockTable
# rows or activate the runtime directly.
paged_kv_runtime: object | 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
# Startup-only memory profiling runs before Scheduler-owned pages exist.
# Attention layers use this explicit scope to distinguish that probe from
# a malformed paged request whose Worker adapter was not activated.
in_diffusion_kv_memory_profile: bool = False
split_text_embed_in_sp: bool = False
denoise_step_idx: int | None = None
denoise_timestep: float | None = None
@@ -154,6 +162,8 @@ def create_forward_context(
vllm_config: VllmConfig | None = None,
omni_diffusion_config: OmniDiffusionConfig | None = None,
attn_metadata: dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None = None,
paged_kv_runtime: object | None = None,
in_diffusion_kv_memory_profile: bool = False,
split_text_embed_in_sp: bool = False,
denoise_step_idx: int | None = None,
):
@@ -161,6 +171,8 @@ def create_forward_context(
vllm_config=vllm_config,
omni_diffusion_config=omni_diffusion_config,
attn_metadata=attn_metadata,
paged_kv_runtime=paged_kv_runtime,
in_diffusion_kv_memory_profile=in_diffusion_kv_memory_profile,
split_text_embed_in_sp=split_text_embed_in_sp,
denoise_step_idx=denoise_step_idx,
)
@@ -186,6 +198,8 @@ def set_forward_context(
vllm_config: VllmConfig | None = None,
omni_diffusion_config: OmniDiffusionConfig | None = None,
attn_metadata: dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None = None,
paged_kv_runtime: object | None = None,
in_diffusion_kv_memory_profile: bool = False,
split_text_embed_in_sp: bool = False,
denoise_step_idx: int | None = None,
):
@@ -197,6 +211,8 @@ def set_forward_context(
vllm_config=vllm_config,
omni_diffusion_config=omni_diffusion_config,
attn_metadata=attn_metadata,
paged_kv_runtime=paged_kv_runtime,
in_diffusion_kv_memory_profile=in_diffusion_kv_memory_profile,
split_text_embed_in_sp=split_text_embed_in_sp,
denoise_step_idx=denoise_step_idx,
)
@@ -246,6 +262,11 @@ 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:
_forward_context.denoise_step_idx = step_idx
if step_idx is not None:
paged_kv_runtime = getattr(_forward_context, "paged_kv_runtime", None)
ensure_active = getattr(paged_kv_runtime, "ensure_active", None)
if callable(ensure_active):
ensure_active(step_idx)
def set_forward_context_denoise_timestep(timestep: float | None) -> None:
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import inspect
import logging
@@ -1069,18 +1069,98 @@ class ImageKVCacheManager(nn.Module):
)
return key, value
def forward(
def clear_legacy_prompt_kv_cache(self) -> None:
"""Drop model-owned dense KV state before Scheduler-paged execution."""
self.image_kv_cache_map = None
self.image_kv_cache_lens = None
def _forward_paged(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attention_mask: torch.Tensor | None = None,
*,
first_step: bool,
query_lens: list[int],
seq_lens: list[int],
shard_image_size: int | None,
full_attn_spans: list[list[tuple[int, int]]],
uncond_cfg_prefill: bool,
) -> torch.Tensor:
"""Run Hunyuan attention against Worker-managed Scheduler pages.
This model boundary is shared by GPU and NPU: it describes Hunyuan's
Q/K/V layout and mixed-attention spans, while the common Attention
layer resolves the actual CUDA or Ascend paged kernel.
"""
if uncond_cfg_prefill:
raise RuntimeError("Hunyuan negative-CFG prefill must run before paged row activation")
if self._injected_ar_kv is not None:
raise NotImplementedError("Hunyuan Scheduler-paged KV does not support imported AR KV")
if not query_lens or len(seq_lens) != len(query_lens):
raise ValueError("Hunyuan Scheduler-paged KV requires matching query and sequence lengths")
if len(full_attn_spans) != len(query_lens):
raise ValueError("Hunyuan Scheduler-paged KV requires one full-attention span row per sequence")
self.clear_legacy_prompt_kv_cache()
bs = len(query_lens)
q_len = query_lens[0]
seq_len = seq_lens[0]
assert query.shape[0] == bs * q_len, f"{query.shape[0]} != {bs * q_len}"
head_num_per_rank = query.shape[1]
kv_head_num_per_rank = key.shape[1]
head_dim = query.shape[2]
query = query.reshape(bs, q_len, head_num_per_rank, head_dim)
key = key.reshape(bs, q_len, kv_head_num_per_rank, head_dim)
value = value.reshape(bs, q_len, kv_head_num_per_rank, head_dim)
joint_text_query = joint_text_key = joint_text_value = None
if self.sp_size > 1 and first_step:
if shard_image_size is None or shard_image_size <= 0:
raise ValueError("Hunyuan paged Ulysses requires a positive local image shard size")
local_prompt_len = seq_len - shard_image_size
joint_query_len = query.shape[1] - shard_image_size
if local_prompt_len != joint_query_len:
raise ValueError(
"Hunyuan paged Ulysses prompt layout mismatch: "
f"key_prompt={local_prompt_len}, query_prompt={joint_query_len}"
)
joint_text_query = query[:, :joint_query_len]
joint_text_key = key[:, :local_prompt_len]
joint_text_value = value[:, :local_prompt_len]
query = query[:, joint_query_len:]
key = key[:, local_prompt_len:]
value = value[:, local_prompt_len:]
if joint_text_query is None:
attn_metadata = AttentionMetadata(full_attn_spans=full_attn_spans)
else:
attn_metadata = AttentionMetadata(
joint_query=joint_text_query,
joint_key=joint_text_key,
joint_value=joint_text_value,
joint_strategy="front",
full_attn_spans=full_attn_spans,
)
attn_output = self.attn(query, key, value, attn_metadata)
return attn_output.reshape(bs * q_len, head_num_per_rank, head_dim)
def _forward_dense_legacy(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attention_mask: torch.Tensor,
**kwargs,
) -> torch.Tensor:
self.image_token_len = kwargs.get("num_image_tokens")
"""Run the device-neutral, model-owned dense prompt-KV compatibility path."""
first_step = kwargs.get("first_step")
uncond_cfg_prefill = kwargs.get("uncond_cfg_prefill", False)
query_lens = kwargs.get("query_lens")
seq_lens = kwargs.get("seq_lens")
shard_image_size = kwargs.get("shard_image_size") if self.sp_size > 1 else None
@@ -1109,8 +1189,7 @@ class ImageKVCacheManager(nn.Module):
key = key[:, :0, :, :]
value = value[:, :0, :, :]
elif first_step:
self.image_kv_cache_map = None # reset first
self.image_kv_cache_lens = None
self.clear_legacy_prompt_kv_cache()
key, value = self._cache_prompt_kv(
key,
value,
@@ -1134,7 +1213,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 and not self.attn.is_paged_kv_active():
if not keep_kv_compressed:
key = repeat_kv(key, repeat_num)
value = repeat_kv(value, repeat_num)
if self.sp_size > 1:
@@ -1163,6 +1242,34 @@ class ImageKVCacheManager(nn.Module):
attn_output = attn_output.reshape(bs * q_len, head_num_per_rank, head_dim)
return attn_output
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attention_mask: torch.Tensor | None = None,
**kwargs,
) -> torch.Tensor:
self.image_token_len = kwargs.get("num_image_tokens")
if self.attn.is_paged_kv_active():
full_attn_spans = kwargs.get("full_attn_spans")
if full_attn_spans is None:
raise ValueError("Hunyuan Scheduler-paged KV requires full_attn_spans metadata")
return self._forward_paged(
query,
key,
value,
first_step=bool(kwargs.get("first_step")),
query_lens=kwargs.get("query_lens"),
seq_lens=kwargs.get("seq_lens"),
shard_image_size=kwargs.get("shard_image_size") if self.sp_size > 1 else None,
full_attn_spans=full_attn_spans,
uncond_cfg_prefill=kwargs.get("uncond_cfg_prefill", False),
)
if attention_mask is None:
raise ValueError("Hunyuan dense attention requires an attention mask")
return self._forward_dense_legacy(query, key, value, attention_mask, **kwargs)
@dataclass
class CausalMMOutputWithPast(CausalLMOutputWithPast):
@@ -2706,7 +2813,6 @@ class HunyuanImage3Text2ImagePipeline(DiffusionPipeline):
# List[List[...]] per-sample metadata indexed along the CFG batch dim
if isinstance(model_kwargs.get("full_attn_spans"), list):
model_kwargs["full_attn_spans"] = model_kwargs["full_attn_spans"][s.start : s.stop]
# custom_pos_emb: tuple of (cos, sin)
if "custom_pos_emb" in model_kwargs and model_kwargs["custom_pos_emb"] is not None:
cos, sin = model_kwargs["custom_pos_emb"]
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import copy
import logging
@@ -72,7 +72,6 @@ _STEP_OUTPUT_SIZE = "hunyuan_output_size"
_STEP_COT_TEXT_LIST = "hunyuan_cot_text_list"
_STEP_AR_KV = "hunyuan_ar_kv"
_STEP_PROMPT_KV = "hunyuan_prompt_kv"
_HUNYUAN_DEFAULT_OUTPUT_TYPE = "pil"
@@ -466,6 +465,11 @@ class HunyuanImage3Pipeline(
return self._pipeline
def _validate_step_request(self, state: "StepRequestState") -> None:
if self._uses_scheduler_paged_kv():
raise ValueError(
"HunyuanImage3 paged_scheduler currently supports request-level execution only; "
"disable step execution or use dense_legacy."
)
prompt = state.prompt
sampling = state.sampling
if prompt is None:
@@ -663,6 +667,12 @@ class HunyuanImage3Pipeline(
prefix_lens.append(int(prompt_kv[0]["lens"][branch].item()))
return prefix_lens
def _uses_scheduler_paged_kv(self) -> bool:
return (
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
is DiffusionKVCacheMode.PAGED_SCHEDULER
)
def _merge_step_model_inputs(
self,
states: list["StepRequestState"],
@@ -693,6 +703,7 @@ class HunyuanImage3Pipeline(
values,
row_branches,
prefix_lens or [None] * len(row_branches),
strict=True,
)
]
continue
@@ -1871,8 +1882,7 @@ class HunyuanImage3Pipeline(
row_branches = [branch for branch in range(cfg_factor) for _ in states]
return row_state_indexes, row_branches
@staticmethod
def _validate_step_group_states(states: list["StepRequestState"]) -> tuple[bool, int]:
def _validate_step_group_states(self, states: list["StepRequestState"]) -> tuple[bool, int]:
if not states:
raise ValueError("HunyuanImage3 denoise_step received an empty group.")
@@ -2035,6 +2045,11 @@ class HunyuanImage3Pipeline(
states = list(input_batch.states)
if not states:
raise ValueError("HunyuanImage3 denoise_step received an empty batch.")
if self._uses_scheduler_paged_kv():
raise ValueError(
"HunyuanImage3 paged_scheduler currently supports request-level execution only; "
"disable step execution or use dense_legacy."
)
self._ensure_grouped_attention_backend_supported(len(states))
outputs: dict[str, torch.Tensor] = {}
for group in self._split_step_groups(states):
+3 -1
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Build native vLLM configuration views for the Omni diffusion runtime."""
@@ -14,6 +14,7 @@ from vllm.transformers_utils.config import get_hf_text_config
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
from vllm_omni.platforms import current_omni_platform
def resolve_diffusion_max_model_len(od_config: OmniDiffusionConfig) -> int:
@@ -227,6 +228,7 @@ def configure_diffusion_vllm_config(vllm_config: VllmConfig, od_config: OmniDiff
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)
current_omni_platform.configure_diffusion_vllm_config(vllm_config, od_config)
return vllm_config
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
Diffusion Model Runner for vLLM-Omni.
@@ -37,9 +37,10 @@ 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 (
DiffusionPagedAttentionMetadata,
DiffusionPagedAttentionRow,
PreparedDiffusionPagedAttentionBatch,
)
from vllm_omni.diffusion.distributed.parallel_state import get_classifier_free_guidance_rank
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 (
@@ -420,11 +421,19 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
raise RuntimeError("Model must be loaded before collecting Diffusion KV cache specs")
cache_layers: dict[str, tuple[Attention, KVCacheSpec]] = {}
for layer_name, module in self.pipeline.named_modules():
for module_path, 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:
layer_name = module.prefix
if not isinstance(layer_name, str) or not layer_name:
raise RuntimeError(
"Paged Diffusion Attention must expose a non-empty canonical prefix; "
f"module_path={module_path!r}"
)
if layer_name in cache_layers:
raise RuntimeError(f"Duplicate canonical paged Diffusion Attention prefix {layer_name!r}")
cache_layers[layer_name] = (module, spec)
if not cache_layers:
raise RuntimeError(
@@ -456,18 +465,51 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
def refresh_diffusion_kv_block_table_layout(self) -> None:
self.diffusion_kv_backend.refresh_block_table_layout()
def prepare_paged_attention_batch(
def _build_paged_attention_metadata(
self,
rows: list[DiffusionPagedAttentionRow] | tuple[DiffusionPagedAttentionRow, ...],
) -> PreparedDiffusionPagedAttentionBatch:
"""Prepare model-specific query/write spans against installed Worker rows."""
metadata: list[DiffusionKVMetadata],
) -> DiffusionPagedAttentionMetadata:
"""Translate Scheduler sequence state into ordered request-level 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)
cfg_size = int(getattr(self.od_config.parallel_config, "cfg_parallel_size", 1) or 1)
cfg_rank = get_classifier_free_guidance_rank() if cfg_size > 1 else None
prefill_rows: list[DiffusionPagedAttentionRow] = []
denoise_rows: list[DiffusionPagedAttentionRow] = []
for request_metadata in metadata:
sequences = request_metadata.sequences
if cfg_rank is not None and len(sequences) > 1:
if len(sequences) != cfg_size:
raise ValueError(
"Paged CFG parallel execution requires one Scheduler sequence per CFG rank: "
f"request={request_metadata.request_id!r}, cfg_size={cfg_size}, rows={len(sequences)}"
)
sequences = (sequences[cfg_rank],)
for sequence in sequences:
active_seq_len = sequence.prefix_len + sequence.target_len
if active_seq_len > sequence.seq_len:
raise ValueError(
"Paged denoise span exceeds its Scheduler allocation: "
f"request={request_metadata.request_id!r}, sequence={sequence.sequence_id}, "
f"active={active_seq_len}, allocated={sequence.seq_len}"
)
prefill_rows.append(
DiffusionPagedAttentionRow(
request_id=request_metadata.request_id,
sequence_id=sequence.sequence_id,
query_len=sequence.seq_len,
seq_len=sequence.seq_len,
)
)
denoise_rows.append(
DiffusionPagedAttentionRow(
request_id=request_metadata.request_id,
sequence_id=sequence.sequence_id,
query_len=sequence.target_len,
seq_len=active_seq_len,
kv_start_pos=sequence.prefix_len,
)
)
return DiffusionPagedAttentionMetadata(tuple(prefill_rows), tuple(denoise_rows))
def clear_prompt_embed_cache(self) -> None:
"""Evict all cached text-encoder outputs (e.g. between training epochs).
@@ -623,6 +665,8 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
kv_prefetch_job: KVPrefetchJob | None = None,
record_name: str,
record_output_peak_memory: bool = True,
in_diffusion_kv_memory_profile: bool = False,
diffusion_kv_metadata: list[DiffusionKVMetadata] | None = None,
) -> BatchRunnerOutput:
assert self.pipeline is not None, "Model not loaded. Call load_model() first."
if not reqs:
@@ -655,7 +699,27 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
if is_primary and record_output_peak_memory:
current_omni_platform.reset_peak_memory_stats()
with set_forward_context(vllm_config=self.vllm_config, omni_diffusion_config=od_config):
paged_kv_runtime = None
paged_kv_context: AbstractContextManager[Any] = nullcontext()
if diffusion_kv_metadata is not None:
if len(diffusion_kv_metadata) != len(reqs):
raise ValueError(
"Diffusion KV metadata count must match the request batch: "
f"metadata={len(diffusion_kv_metadata)}, requests={len(reqs)}"
)
paged_metadata = self._build_paged_attention_metadata(diffusion_kv_metadata)
paged_kv_runtime, paged_kv_context = self.diffusion_kv_backend.activate_paged_attention_metadata(
paged_metadata
)
with (
set_forward_context(
vllm_config=self.vllm_config,
omni_diffusion_config=od_config,
paged_kv_runtime=paged_kv_runtime,
in_diffusion_kv_memory_profile=in_diffusion_kv_memory_profile,
),
paged_kv_context,
):
with record_function(record_name):
raw_outputs = self.pipeline.forward(batch)
outputs = _normalize_pipeline_outputs(
@@ -733,6 +797,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
require_request_batch_support=False,
kv_prefetch_job=kv_prefetch_job,
record_name="pipeline_forward",
diffusion_kv_metadata=[diffusion_kv_metadata] if diffusion_kv_metadata is not None else None,
)
output = runner_output.runner_outputs[0].result
assert output is not None
@@ -773,6 +838,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
scheduler_output,
validate_kv_metadata=False,
record_output_peak_memory=False,
in_diffusion_kv_memory_profile=True,
)
else:
runner_output = self._execute_request_list(
@@ -785,6 +851,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
# peak counters. Resetting them here would discard request
# preparation allocations and understate the budget.
record_output_peak_memory=False,
in_diffusion_kv_memory_profile=True,
)
current_omni_platform.synchronize()
finally:
@@ -824,6 +891,12 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
allow_single_output=False,
require_request_batch_support=True,
record_name="pipeline_forward_batch",
diffusion_kv_metadata=[
new_req.diffusion_kv_metadata
for new_req in scheduler_output.scheduled_new_reqs
if new_req.diffusion_kv_metadata is not None
]
or None,
)
finally:
if installed_request_ids:
@@ -984,6 +1057,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
*,
validate_kv_metadata: bool,
record_output_peak_memory: bool,
in_diffusion_kv_memory_profile: bool = False,
) -> BatchRunnerOutput:
"""Execute one step with explicit validation and profiling policy."""
@@ -1023,6 +1097,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
return self._execute_stepwise_core(
scheduler_output,
record_output_peak_memory=record_output_peak_memory,
in_diffusion_kv_memory_profile=in_diffusion_kv_memory_profile,
)
except Exception:
if installed_request_ids:
@@ -1034,6 +1109,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
scheduler_output: DiffusionSchedulerOutput,
*,
record_output_peak_memory: bool,
in_diffusion_kv_memory_profile: bool = False,
) -> BatchRunnerOutput:
"""Run the denoise step after metadata admission and row installation."""
@@ -1056,10 +1132,14 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
return BatchRunnerOutput.from_list(runner_output_list)
attn_metadata = {}
kv_backend = getattr(self, "diffusion_kv_backend", None)
paged_kv_runtime = kv_backend if getattr(kv_backend, "paged_attention_adapter", None) is not None else None
with set_forward_context(
vllm_config=self.vllm_config,
omni_diffusion_config=self.od_config,
attn_metadata=attn_metadata,
paged_kv_runtime=paged_kv_runtime,
in_diffusion_kv_memory_profile=in_diffusion_kv_memory_profile,
):
clear_pipeline_stage_durations(self.pipeline)
noise_pred = self.pipeline.denoise_step(input_batch, states=states)
@@ -249,11 +249,6 @@ 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),
+40 -6
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from contextlib import nullcontext
from enum import Enum
@@ -150,6 +150,12 @@ class OmniPlatform(Platform):
)
backend_cls.validate_available()
@classmethod
def supports_diffusion_dense_flash_attention(cls) -> bool:
"""Whether the platform's dense ``FLASH_ATTN`` dependencies exist."""
return True
@classmethod
def supports_torch_inductor(cls) -> bool:
"""Check if the platform supports torch.compile with inductor backend."""
@@ -195,17 +201,36 @@ class OmniPlatform(Platform):
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.
This default is the GPU/common path and uses vLLM's builder. NPU
overrides it to build Ascend attention metadata without making the
shared diffusion adapter import ``vllm_ascend``. ``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 get_diffusion_paged_kv_attn_backend(cls, attn_backend: type, *, ulysses_degree: int) -> type:
"""Specialize a native paged backend for diffusion execution."""
del ulysses_degree
return attn_backend
@classmethod
def requires_diffusion_paged_kv_prewrite(cls) -> bool:
"""Whether paged attention must write K/V before native execution.
The default GPU path keeps cache-update ownership in its native
attention call. Ascend overrides this because piecewise FIA should
write the complete K/V span once, then read it from cache for every
segment.
"""
return False
@classmethod
def init_diffusion_worker_vllm_config(
cls,
@@ -214,6 +239,15 @@ class OmniPlatform(Platform):
"""Initialize platform-specific state for diffusion worker VllmConfig."""
return None
@classmethod
def configure_diffusion_vllm_config(
cls,
vllm_config: Any,
od_config: Any,
) -> None:
"""Apply platform-specific native cache geometry for diffusion."""
return None
@classmethod
def init_diffusion_model_runner_runtime(
cls,
+84 -2
View File
@@ -1,7 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from contextlib import nullcontext
from functools import cache
from typing import Any
import torch
@@ -21,6 +22,28 @@ _DIFFUSION_PACKED_MODULES_MAPPING = {
}
@cache
def _get_strict_ulysses_paged_backend() -> type:
"""Return an Ascend backend that bypasses vLLM PCP dispatch."""
from vllm_ascend.attention.attention_v1 import (
AscendAttentionBackend,
AscendAttentionBackendImpl,
AscendAttentionMetadataBuilder,
)
class AscendStrictUlyssesPagedBackend(AscendAttentionBackend):
@staticmethod
def get_impl_cls() -> type:
return AscendAttentionBackendImpl
@staticmethod
def get_builder_cls() -> type:
return AscendAttentionMetadataBuilder
return AscendStrictUlyssesPagedBackend
class NPUOmniPlatform(OmniPlatform, NPUPlatform):
"""NPU/Ascend implementation of OmniPlatform.
@@ -77,9 +100,55 @@ class NPUOmniPlatform(OmniPlatform, NPUPlatform):
@classmethod
def init_diffusion_worker_vllm_config(cls, vllm_config: Any) -> None:
from vllm_ascend.ascend_config import init_ascend_config
from vllm_ascend.utils import adapt_patch
# Omni's custom DiffusionWorker does not pass through vLLM-Ascend's
# NPUWorker constructor, where worker-local patches are normally
# installed. In particular, AscendBlockTables needs the patched
# non-UVA buffer implementation on NPU.
adapt_patch()
init_ascend_config(vllm_config)
@classmethod
def configure_diffusion_vllm_config(cls, vllm_config: Any, od_config: Any) -> None:
"""Use the block geometry required by Ascend's native paged kernel."""
if getattr(od_config, "diffusion_kv_mode", None) is None:
return
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
if od_config.diffusion_kv_mode is not DiffusionKVCacheMode.PAGED_SCHEDULER:
return
from vllm_ascend.attention.attention_v1 import AscendAttentionBackend
supported_sizes = [
size for size in AscendAttentionBackend.get_supported_kernel_block_sizes() if type(size) is int and size > 0
]
if not supported_sizes:
raise RuntimeError("Ascend paged attention did not expose an integer kernel block size")
# vLLM's generic default is 16, while the Ascend FIA backend stores
# cache pages as 128-token blocks. Set the Manager geometry
# before KV specs are collected so Scheduler and Worker agree.
vllm_config.cache_config.block_size = supported_sizes[0]
@classmethod
def requires_diffusion_paged_kv_prewrite(cls) -> bool:
"""Write the full K/V span once before piecewise FIA segments."""
return True
@classmethod
def get_diffusion_paged_kv_attn_backend(cls, attn_backend: type, *, ulysses_degree: int) -> type:
"""Keep strict Ulysses paged FIA out of vLLM's PCP implementation."""
del cls
if ulysses_degree <= 1:
return attn_backend
from vllm_ascend.attention.attention_v1 import AscendAttentionBackend
if not isinstance(attn_backend, type) or not issubclass(attn_backend, AscendAttentionBackend):
return attn_backend
return _get_strict_ulysses_paged_backend()
@classmethod
def get_diffusion_kv_block_tables_cls(cls) -> type:
from vllm_ascend.worker.v2.block_table import AscendBlockTables
@@ -211,6 +280,14 @@ class NPUOmniPlatform(OmniPlatform, NPUPlatform):
logger.debug("Falling back to diffusion attention backend SDPA")
return DiffusionAttentionBackendEnum.TORCH_SDPA.get_path()
@classmethod
def supports_diffusion_dense_flash_attention(cls) -> bool:
"""Return whether MindIE-SD is installed for dense NPU FlashAttention."""
from importlib.util import find_spec
return find_spec("mindiesd") is not None
@classmethod
def supports_torch_inductor(cls) -> bool:
return False
@@ -245,7 +322,12 @@ class NPUOmniPlatform(OmniPlatform, NPUPlatform):
"""
try:
torch.npu.current_stream().synchronize()
event = torch.npu.Event()
# The async output worker uses the public ``torch.Stream`` API.
# With torch_npu 2.10, a native ``torch.npu.Event`` cannot be
# consumed by that wrapper stream (the reverse direction is
# supported), while ``torch.Event`` is dispatched to the active
# NPU backend and remains cross-stream compatible.
event = torch.Event()
event.record()
return event
except Exception: