[Feat] Add native Mooncake KV transfer from AR to DiT (#7166)

Signed-off-by: asukaqaq-s <1311722138@qq.com>
Signed-off-by: Acerak01-fy <wfy2003324@163.com>
Co-authored-by: Acerak01-fy <wfy2003324@163.com>
Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
This commit is contained in:
authored and GitHub committed 2026-09-20 14:44:55 +00:00
1 parent 2d0b03f717
commit 102ba716bf
53 files changed
+2564 -204

No files matched your search

@@ -325,7 +325,7 @@ def _runner(
model_config=SimpleNamespace(max_model_len=max_len),
scheduler_config=SimpleNamespace(
max_num_batched_tokens=max_num_batched_tokens,
max_num_seqs=max_num_seqs,
max_num_seqs=max_num_seqs * max_rows_per_request,
),
parallel_config=SimpleNamespace(
decode_context_parallel_size=cp_size,
@@ -403,7 +403,7 @@ def test_initialize_failure_does_not_publish_partial_cache_state(
successful_init_cache = model_runner_backend_module.init_kv_cache
vllm_config = SimpleNamespace(
model_config=SimpleNamespace(max_model_len=16),
scheduler_config=SimpleNamespace(max_num_batched_tokens=32, max_num_seqs=1),
scheduler_config=SimpleNamespace(max_num_batched_tokens=32, max_num_seqs=2),
parallel_config=SimpleNamespace(
decode_context_parallel_size=1,
cp_kv_cache_interleave_size=1,
@@ -445,7 +445,7 @@ def test_initialize_failure_does_not_publish_partial_cache_state(
assert backend.block_tables is None
assert backend.paged_attention_adapter is None
assert backend._kv_cache_layer_adapters["layer-0"].kv_cache == "placeholder-0"
assert backend.vllm_config.scheduler_config.max_num_seqs == 1
assert backend.vllm_config.scheduler_config.max_num_seqs == 2
monkeypatch.setattr(model_runner_backend_module, "init_kv_cache", successful_init_cache)
backend.initialize_kv_cache(config)
@@ -58,6 +58,7 @@ def _od_config(**overrides):
gpu_memory_utilization=0.9,
kv_cache_memory_bytes=None,
max_num_seqs=1,
diffusion_kv_max_rows_per_request=1,
max_num_batched_tokens=64,
num_gpus=1,
)
@@ -1,16 +1,23 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from contextlib import nullcontext
from types import SimpleNamespace
from unittest import mock
import pytest
import torch
from vllm.config import KVTransferConfig
from vllm.v1.core.kv_cache_manager import KVCacheBlocks
from vllm.v1.outputs import KVConnectorOutput
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.diffusion_kv.kv_connector import (
KVTransferRegistrationError,
commit_kv_load,
parse_kv_transfer_config,
shutdown_kv_connector,
wait_for_kv_load,
)
from vllm_omni.diffusion.sched.base_scheduler import BaseScheduler
from vllm_omni.diffusion.vllm_config import create_diffusion_vllm_config
@@ -138,3 +145,205 @@ def test_shutdown_kv_connector_is_idempotent() -> None:
assert ensure_shutdown.call_count == 2
scheduler_connector.shutdown.assert_called_once()
def test_commit_aligns_cfg_transport_pages_without_extending_computed_prefix() -> None:
requests = tuple(
SimpleNamespace(
request_id=f"req/{sequence_id}",
kv_transfer_params={"num_transfer_tokens": 9},
num_tokens=16,
num_computed_tokens=0,
)
for sequence_id in range(2)
)
manager = SimpleNamespace(
get_blocks=lambda _request_id: KVCacheBlocks(([0, 1, 2, 3],)),
kv_cache_config=SimpleNamespace(kv_cache_groups=[SimpleNamespace(kv_cache_spec=SimpleNamespace(block_size=4))]),
)
connector = mock.Mock()
expected = commit_kv_load(connector, manager, requests, [7, 5])
assert expected == {"req/0", "req/1"}
calls = connector.update_state_after_alloc.call_args_list
assert [len(call.args[1].blocks[0]) for call in calls] == [3, 3]
assert [call.args[2] for call in calls] == [7, 5]
assert [request.num_computed_tokens for request in requests] == [7, 5]
def test_wait_without_pending_load_uses_vllm_post_forward_once() -> None:
connector = mock.Mock()
output = mock.Mock(invalid_block_ids=set())
active_connector = mock.Mock(kv_connector=connector)
active_connector.post_forward.return_value = output
scheduler_output = SimpleNamespace(
kv_transfer_request_ids=set(),
finished_req_ids={"finished"},
kv_finished_request_ids={"finished/diffusion-kv/0", "finished/diffusion-kv/1"},
)
result = wait_for_kv_load(active_connector, scheduler_output, timeout=1.0)
assert result is output
active_connector.pre_forward.assert_called_once_with(scheduler_output)
active_connector.post_forward.assert_called_once_with({"finished/diffusion-kv/0", "finished/diffusion-kv/1"})
connector.get_finished.assert_not_called()
def test_timeout_returns_partial_completion_without_releasing_pages():
active = mock.Mock()
active.kv_connector.get_finished.return_value = (set(), {"cfg0"})
active.kv_connector.get_block_ids_with_load_errors.return_value = set()
active.post_forward.return_value = KVConnectorOutput()
scheduled = SimpleNamespace(kv_transfer_request_ids={"cfg0", "cfg1"}, kv_finished_request_ids=set())
output = wait_for_kv_load(active, scheduled, timeout=0)
assert output.finished_recving == {"cfg0"}
active.kv_connector.request_finished.assert_not_called()
active.post_forward.assert_called_once_with(set())
@pytest.fixture
def cfg_registration():
requests = tuple(
SimpleNamespace(
request_id=f"cfg{i}",
status=None,
num_tokens=8,
kv_transfer_params={"num_transfer_tokens": 4, "transfer_id": "ticket", "do_remote_prefill": True},
)
for i in range(2)
)
manager = SimpleNamespace(
get_blocks=lambda _: KVCacheBlocks(([0, 1],)),
kv_cache_config=SimpleNamespace(kv_cache_groups=[SimpleNamespace(kv_cache_spec=SimpleNamespace(block_size=4))]),
)
return requests, manager
def test_cfg_validation_is_atomic_before_connector_registration(cfg_registration):
requests, manager = cfg_registration
requests[1].kv_transfer_params["num_transfer_tokens"] = 9
connector = mock.Mock()
with pytest.raises(KVTransferRegistrationError, match="boundary"):
commit_kv_load(connector, manager, requests, [4, 4])
connector.update_state_after_alloc.assert_not_called()
@pytest.mark.parametrize("fail_init", [False, True])
def test_sp_connector_init_snapshots_topology_and_restores_model_tp(monkeypatch, fail_init):
import vllm.distributed.parallel_state as parallel_state
import vllm_omni.diffusion.diffusion_kv.kv_connector as module
import vllm_omni.diffusion.distributed.parallel_state as omni_parallel
tp = SimpleNamespace(world_size=1, rank_in_group=0)
sp = SimpleNamespace(world_size=2, rank_in_group=1)
monkeypatch.setattr(parallel_state, "_TP", tp)
monkeypatch.setattr(omni_parallel, "get_sp_group", lambda: sp)
snapshot: dict[str, int] = {}
def initialize(*args):
snapshot.update(
rank=parallel_state.get_tensor_model_parallel_rank(),
size=parallel_state.get_tensor_model_parallel_world_size(),
)
if fail_init:
raise RuntimeError("initialization failed")
monkeypatch.setattr(module, "ensure_kv_transfer_initialized", initialize)
config = SimpleNamespace(
kv_transfer_config=SimpleNamespace(kv_connector="MooncakeConnector", engine_id="dit"),
parallel_config=SimpleNamespace(prefill_context_parallel_size=2),
)
with pytest.raises(RuntimeError, match="initialization failed") if fail_init else nullcontext():
module.init_worker_kv_connector(config, object())
assert parallel_state._TP is tp
assert snapshot == {"rank": 1, "size": 2}
def test_partial_mooncake_registration_is_replaced_by_empty_notifications(cfg_registration):
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector import (
MooncakeConnector,
MooncakeConnectorScheduler,
)
scheduler = object.__new__(MooncakeConnectorScheduler)
scheduler.is_kv_producer = False
scheduler.is_kv_consumer = True
scheduler._reqs_need_recv = {}
connector = object.__new__(MooncakeConnector)
connector.connector_scheduler = scheduler
requests, manager = cfg_registration
for request in requests:
request.kv_transfer_params.update(remote_engine_id="ar", remote_bootstrap_addr="http://localhost:8998")
def register(request, blocks, count):
scheduler._reqs_need_recv[request.request_id] = (request, blocks)
request.kv_transfer_params["do_remote_prefill"] = False
if request.request_id == "cfg1":
raise RuntimeError("partial registration")
connector.update_state_after_alloc = register
with pytest.raises(KVTransferRegistrationError, match="partial registration"):
commit_kv_load(connector, manager, requests, [4, 4])
assert set(scheduler._reqs_need_recv) == {"cfg0", "cfg1"}
assert all(blocks == [] for _, blocks in scheduler._reqs_need_recv.values())
metadata = scheduler.build_connector_meta(None)
# Even partial registration failure sends every row in one per-rank batch,
# preserving the producer's complete fan-out count for the shared ticket.
assert set(metadata.reqs_to_recv["ar"]) == {"cfg0", "cfg1"}
assert all(
meta.transfer_id == "ticket" and meta.local_block_ids == [] for meta in metadata.reqs_to_recv["ar"].values()
)
@pytest.mark.parametrize("timeout", [0, -1, float("inf"), "60"])
def test_native_transfer_timeout_must_be_positive_finite(timeout):
with pytest.raises(ValueError, match="transfer_timeout"):
parse_kv_transfer_config(
{
**KV_TRANSFER_CONFIG,
"kv_connector_extra_config": {"transfer_timeout": timeout},
}
)
def test_receive_completion_requires_all_ranks_across_polls():
from vllm_omni.diffusion.executor.abstract import DiffusionExecutor
from vllm_omni.diffusion.sched.interface import CachedRequestData, DiffusionSchedulerOutput
executor = SimpleNamespace(od_config=SimpleNamespace(num_gpus=2))
executor.collective_rpc = mock.Mock(
side_effect=[
[KVConnectorOutput(finished_recving={"cfg0", "cfg1"}), KVConnectorOutput(finished_recving={"cfg0"})],
[KVConnectorOutput(), KVConnectorOutput(finished_recving={"cfg1"})],
]
)
scheduled = DiffusionSchedulerOutput(
step_id=0,
scheduled_new_reqs=[],
scheduled_cached_reqs=CachedRequestData.make_empty(),
finished_req_ids=set(),
num_running_reqs=0,
num_waiting_reqs=0,
kv_connector_metadata=object(),
kv_transfer_request_ids={"cfg0", "cfg1"},
)
result = DiffusionExecutor.prepare_kv_for_forward(executor, scheduled)
assert result.finished_recving == {"cfg0"}
scheduled.kv_transfer_request_ids = set()
result = DiffusionExecutor.prepare_kv_for_forward(executor, scheduled)
assert result.finished_recving == {"cfg1"}
assert not executor._kv_receive_completed_ranks
def test_native_transfer_rejects_sleep_without_changing_legacy_sleep():
with pytest.raises(ValueError, match="registered pages must remain mapped"):
OmniDiffusionConfig.from_kwargs(
diffusion_kv_mode="paged_scheduler",
diffusion_kv_max_rows_per_request=2,
kv_transfer_config=dict(KV_TRANSFER_CONFIG),
enable_sleep_mode=True,
)
assert OmniDiffusionConfig.from_kwargs(enable_sleep_mode=True).enable_sleep_mode
@@ -61,13 +61,14 @@ class _MetadataScheduler(BaseScheduler):
def test_scheduler_copies_opaque_params_to_sequence_requests() -> None:
params = {"transfer_id": "xfer-req-0", "do_remote_prefill": True}
params = {"transfer_id": "xfer-req-0", "do_remote_prefill": True, "num_transfer_tokens": 3}
sequence = DiffusionKVRequest(
"req-0/diffusion-kv/0",
sequence_id=0,
prefix_len=4,
target_len=2,
seq_len=6,
prompt_token_ids=[10, 11, 12, 13],
)
request = OmniDiffusionRequest(
prompt="prompt",
@@ -83,6 +84,9 @@ def test_scheduler_copies_opaque_params_to_sequence_requests() -> None:
assert state.diffusion_kv_requests[0].kv_transfer_params == params
assert state.diffusion_kv_requests[0].kv_transfer_params is not params
assert sequence.prompt_token_ids == [10, 11, 12]
assert sequence.num_prompt_tokens == 3
assert sequence.seq_len == 6
def test_diffusion_kv_metadata_uses_native_cache_group_block_ids() -> None:
@@ -0,0 +1,204 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Exercise upstream Mooncake completion accounting without device/network I/O."""
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import msgspec
import pytest
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake import mooncake_connector as mc
from vllm.v1.core.sched.scheduler import Scheduler
from vllm.v1.outputs import KVConnectorOutput
from vllm_omni.diffusion.diffusion_kv.kv_connector import install_mooncake_cfg_fanout
from vllm_omni.worker.mixins import OmniWorkerMixin
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
@pytest.fixture
def producer(monkeypatch):
worker = object.__new__(mc.MooncakeConnectorWorker)
worker.shutdown = Mock() # No hardware constructor, sockets or listener thread.
worker.is_kv_consumer = False
worker.tp_rank, worker.tp_size = 0, 1
worker.transfer_topo = SimpleNamespace(handshake_target_ranks=lambda size: list(range(size)))
worker.reqs_need_send, worker.finished_sending_reqs = {}, set()
for name in (
"kv_caches_base_addr",
"block_len_per_layer",
"kv_block_len_per_layer",
"registered_layer_names",
"registered_layer_indices",
"registered_group_indices",
):
setattr(worker, name, [])
worker._get_transfer_regions = Mock(return_value=[])
worker._producer_cache_is_replicated = Mock(return_value=False)
worker._encoder = msgspec.msgpack.Encoder()
worker._sender_executor = None
worker._send_blocks = Mock(return_value=0)
worker.xfer_stats = Mock()
worker._build_transfer_params = AsyncMock(return_value=([1], [2], [16], [], None))
monkeypatch.setattr(mc, "_align_transfer_regions", lambda *_: ([], [], None))
monkeypatch.setattr(mc, "_validate_asymmetric_region_lengths", lambda **_: None)
monkeypatch.setattr(mc.envs, "VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT", 0.05)
connector = object.__new__(mc.MooncakeConnector)
connector.connector_worker = worker
return connector
async def register(worker, ticket):
worker.sender_loop = asyncio.get_running_loop()
for blocks in ([], [[10]]):
meta = mc.MooncakeConnectorMetadata()
meta.add_new_req(ticket, blocks, {"transfer_id": ticket}, load_remote_cache=False)
await worker.record_send_reqs(meta)
return worker.reqs_need_send[ticket]
def packet(rank, ranks, tickets, empty=False):
return mc.MooncakeXferMetadata(
remote_hostname="localhost",
remote_port=1234 + rank,
remote_tp_size=ranks,
remote_tp_rank=rank,
req_blocks={
f"{ticket}/{row}": (ticket, [] if empty else [[100 + rank * 10 + row]])
for ticket, rows in tickets.items()
for row in range(rows)
},
kv_caches_base_addr=[],
block_lens=[],
kv_block_lens=[],
)
async def send(worker, rank, ranks, tickets, empty=False):
sock = SimpleNamespace(send_multipart=AsyncMock())
await worker.send_kv_to_decode(str(rank).encode(), sock, packet(rank, ranks, tickets, empty))
return [
msgspec.msgpack.decode(c.args[0][1], type=mc.MooncakeXferResponse) for c in sock.send_multipart.call_args_list
]
@pytest.mark.asyncio
@pytest.mark.parametrize("rows", [1, 2])
@pytest.mark.parametrize("ranks", [2, 4])
@pytest.mark.parametrize("in_flight", [False, True])
async def test_source_pages_wait_for_every_rank_and_row(producer, rows, ranks, in_flight):
worker = producer.connector_worker
entered, release = asyncio.Event(), asyncio.Event()
original = worker._build_transfer_params
async def plan(ready, meta, *regions):
if in_flight and meta.remote_tp_rank == ranks - 1:
entered.set()
await release.wait()
return await original(ready, meta, *regions)
worker._build_transfer_params = plan
install_mooncake_cfg_fanout(producer)
adapted = worker._build_transfer_params
install_mooncake_cfg_fanout(producer)
assert worker._build_transfer_params is adapted # Do not multiply twice.
ticket = await register(worker, "request")
pending = asyncio.create_task(send(worker, ranks - 1, ranks, {"request": rows})) if in_flight else None
try:
if pending is not None:
await asyncio.wait_for(entered.wait(), 2)
for rank in range(ranks - 1):
await send(worker, rank, ranks, {"request": rows})
assert ticket.need_send == ranks * rows
assert ticket.sent == (rank + 1) * rows
assert worker.reqs_need_send["request"] is ticket
finished = await worker.fetch_finished_sending_reqs()
scheduler = SimpleNamespace(connector=None, requests={"request": object()}, _free_blocks=Mock())
Scheduler._update_from_kv_xfer_finished(scheduler, KVConnectorOutput(finished_sending=finished))
scheduler._free_blocks.assert_not_called()
release.set()
responses = (
await asyncio.wait_for(pending, 2) if pending else await send(worker, ranks - 1, ranks, {"request": rows})
)
assert sum(len(response.ok_reqs or []) for response in responses) == rows
assert all(not response.err_reqs for response in responses)
assert "request" not in worker.reqs_need_send
finished = await worker.fetch_finished_sending_reqs()
assert finished == {"request"}
Scheduler._update_from_kv_xfer_finished(scheduler, KVConnectorOutput(finished_sending=finished))
scheduler._free_blocks.assert_called_once()
assert not await worker.fetch_finished_sending_reqs()
finally:
release.set()
if pending is not None:
await asyncio.gather(pending, return_exceptions=True)
@pytest.mark.asyncio
@pytest.mark.parametrize("empty", [False, True])
@pytest.mark.parametrize("ranks", [1, 2])
async def test_mixed_tickets_and_empty_block_notifications(producer, empty, ranks):
worker = producer.connector_worker
if empty:
worker._build_transfer_params.return_value = ([], [], [], [], None)
install_mooncake_cfg_fanout(producer)
single, cfg = await register(worker, "single"), await register(worker, "cfg")
await asyncio.gather(*(send(worker, rank, ranks, {"single": 1, "cfg": 2}, empty) for rank in range(ranks)))
assert (single.need_send, single.sent) == (ranks, ranks)
assert (cfg.need_send, cfg.sent) == (2 * ranks, 2 * ranks)
assert await worker.fetch_finished_sending_reqs() == {"single", "cfg"}
assert not worker.reqs_need_send
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", ["row_count", "transport"])
async def test_failed_rank_does_not_report_source_complete(producer, failure):
worker = producer.connector_worker
install_mooncake_cfg_fanout(producer)
ticket = await register(worker, "request")
await send(worker, 0, 2, {"request": 2})
if failure == "transport":
worker._send_blocks.return_value = -1
responses = await send(worker, 1, 2, {"request": 1 if failure == "row_count" else 2})
assert any(response.err_reqs for response in responses)
assert ticket.sent == 2 and ticket.need_send == 4 and ticket.sending == 0
assert worker.reqs_need_send["request"] is ticket
assert not await worker.fetch_finished_sending_reqs()
# Preserve upstream timeout cleanup after all in-flight writes have stopped.
ticket.expire_time = 0
assert await worker.fetch_finished_sending_reqs() == {"request"}
assert not worker.reqs_need_send
def test_worker_installs_adapter_after_native_initialization(producer, monkeypatch):
import vllm.distributed.kv_transfer as kv_transfer
worker = producer.connector_worker
original = worker._build_transfer_params
class UpstreamWorker:
def initialize_from_config(self, config):
assert config == "config"
assert worker._build_transfer_params is original
class OmniWorker(OmniWorkerMixin, UpstreamWorker):
pass
monkeypatch.setattr(kv_transfer, "has_kv_transfer_group", lambda: True)
monkeypatch.setattr(kv_transfer, "get_kv_transfer_group", lambda: producer)
object.__new__(OmniWorker).initialize_from_config("config")
assert worker._build_transfer_params is not original
@pytest.mark.parametrize("role", ["other_connector", "scheduler", "consumer"])
def test_adapter_leaves_other_roles_untouched(producer, role):
worker = producer.connector_worker
original = worker._build_transfer_params
if role == "scheduler":
producer.connector_worker = None
elif role == "consumer":
worker.is_kv_consumer = True
install_mooncake_cfg_fanout(object() if role == "other_connector" else producer)
assert worker._build_transfer_params is original
@@ -94,7 +94,7 @@ class _FakeAttentionGroup:
return self.builder
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class _FakeNativeMetadata:
build_id: int
causal: bool
@@ -102,6 +102,7 @@ class _FakeNativeMetadata:
query_start_loc_cpu: torch.Tensor
positions: torch.Tensor
slot_mappings: torch.Tensor
max_num_splits: int = 0
class _FakeLayer:
@@ -288,6 +289,8 @@ def test_omni_paged_backend_consumes_context_and_restores_diffusion_shape(
assert output.shape == query.shape
assert torch.equal(output, query)
assert layer.calls[0][0].shape == (5, 2, 4)
assert layer.calls[0][1] is None
assert layer.calls[0][2] is None
assert layer.native_events == ["update", "forward"]
assert layer.calls[0][3] is events[0][2]
@@ -440,9 +443,7 @@ def test_platform_prewrite_updates_once_then_piecewise_reads_cache(monkeypatch:
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)
layer.impl.vllm_flash_attn_version = 2
batch = adapter.prepare_batch(
[
DiffusionPagedAttentionRow(
@@ -499,10 +500,10 @@ def test_omni_paged_backend_runs_hunyuan_piecewise_segments(monkeypatch: pytest.
assert torch.equal(layer.updates[0][0], key.reshape(12, 2, 4))
assert torch.equal(layer.updates[0][1], value.reshape(12, 2, 4))
assert [call[0].shape[0] for call in layer.calls] == [4, 6, 2]
assert [call[1].shape[0] for call in layer.calls] == [4, 6, 2]
assert [call[2].shape[0] for call in layer.calls] == [4, 6, 2]
assert all(call[1] is None and call[2] is None for call in layer.calls)
assert all(call[3] is event[2] for call, event in zip(layer.calls, events[1:], strict=True))
assert [metadata.build_id for metadata in context.piecewise_native_metadata] == [1, 2, 3]
assert [metadata.max_num_splits for metadata in context.piecewise_native_metadata] == [1, 1, 1]
# The first metadata build is the normal whole-query path. Piecewise calls
# then use causal [3, 5), full [5, 8), and causal [8, 9) segments.
@@ -539,6 +540,51 @@ def test_omni_paged_backend_runs_hunyuan_piecewise_segments(monkeypatch: pytest.
]
def test_homogeneous_piecewise_packs_strided_query_only_per_segment(
monkeypatch: pytest.MonkeyPatch,
) -> None:
adapter, _, _, _ = _make_adapter(monkeypatch)
batch = adapter.prepare_batch(
[
DiffusionPagedAttentionRow(
request_id="req-0",
sequence_id=0,
query_len=6,
seq_len=9,
kv_start_pos=3,
),
DiffusionPagedAttentionRow(
request_id="req-1",
sequence_id=1,
query_len=6,
seq_len=9,
kv_start_pos=3,
),
]
)
packed_qkv = torch.randn(2, 6, 3, 2, 4)
query, key, value = packed_qkv.unbind(dim=2)
assert not query.is_contiguous()
metadata = SimpleNamespace(
attn_mask=None,
full_attn_spans=[[(5, 8)], [(5, 8)]],
query_ranges=None,
extra={},
)
with adapter.activate(batch):
context = adapter.prepare_layer_context(
"layer-0",
query,
key,
value,
omni_attn_metadata=metadata,
)
assert not context.query.is_contiguous()
assert context.query.data_ptr() == query.data_ptr()
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)
@@ -1232,7 +1278,7 @@ def test_layer_adapter_accepts_platform_native_backend_and_uses_rank_local_heads
ulysses_degree=2,
)
assert selected_backends == [(adapter_module.AttentionBackendEnum.FLASH_ATTN, {}, 2)]
assert selected_backends == [(adapter_module.AttentionBackendEnum.FLASH_ATTN, {}, 1)]
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
@@ -43,8 +43,7 @@ class _SmokeDiffusionAttention(nn.Module):
self.softmax_scale = _HEAD_SIZE**-0.5
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required for native paged attention")
def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> None:
def _native_adapter(num_rows: int = 1):
device = torch.device("cuda", torch.accelerator.current_device_index())
vllm_config = VllmConfig(
cache_config=CacheConfig(
@@ -89,8 +88,8 @@ def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> N
canonical_spec = native_layer.spec
kv_cache_config = KVCacheConfig(
num_blocks=4,
kv_cache_tensors=[build_kv_cache_tensor(canonical_spec, 4, [_LAYER_NAME])],
num_blocks=4 * num_rows,
kv_cache_tensors=[build_kv_cache_tensor(canonical_spec, 4 * num_rows, [_LAYER_NAME])],
kv_cache_groups=[
KVCacheGroupSpec(
layer_names=[_LAYER_NAME],
@@ -120,14 +119,15 @@ def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> N
)
block_tables = BlockTables(
block_sizes=[_BLOCK_SIZE],
max_num_reqs=1,
max_num_batched_tokens=32,
max_num_reqs=num_rows,
max_num_batched_tokens=32 * num_rows,
max_num_blocks_per_group=[2],
device=device,
kernel_block_sizes=kernel_block_sizes,
)
block_tables.append_block_ids(0, ([3, 1],), overwrite=True)
for row in range(num_rows):
block_tables.append_block_ids(row, ([4 * row + 3, 4 * row + 1],), overwrite=True)
block_tables.apply_staged_writes()
adapter = DiffusionPagedAttentionAdapter(
vllm_config=vllm_config,
@@ -136,11 +136,17 @@ def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> N
block_tables=block_tables,
attn_groups=attn_groups,
layers={_LAYER_NAME: native_layer},
resolve_row=lambda _request_id, _sequence_id, _context_id: DiffusionPagedAttentionRowBinding(
row_index=0,
resolve_row=lambda _request_id, sequence_id, _context_id: DiffusionPagedAttentionRowBinding(
row_index=sequence_id,
max_seq_len=19,
),
)
return adapter, diffusion_layer, device
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required for native paged attention")
def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> None:
adapter, diffusion_layer, device = _native_adapter()
prefix_batch = adapter.prepare_batch(
[
DiffusionPagedAttentionRow(
@@ -258,3 +264,48 @@ def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> N
torch.testing.assert_close(prefix_output, prefix_reference, rtol=2e-2, atol=2e-2)
torch.testing.assert_close(suffix_output, suffix_reference, rtol=2e-2, atol=2e-2)
torch.testing.assert_close(piecewise_output, piecewise_reference, rtol=2e-2, atol=2e-2)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required for native paged attention")
def test_hunyuan_homogeneous_cfg_paged_attention_graph_replay() -> None:
"""Real paged kernels, strided QKV, mixed image spans and changed replay inputs."""
adapter, layer, device = _native_adapter(num_rows=2)
packed = torch.randn(2, 19, 3, _NUM_HEADS, _HEAD_SIZE, device=device, dtype=torch.float16)
query, key, value = packed.unbind(dim=2)
backend = FlashAttentionImpl(
num_heads=_NUM_HEADS, head_size=_HEAD_SIZE, softmax_scale=layer.softmax_scale, num_kv_heads=_NUM_HEADS
)
batch = adapter.prepare_batch(
[DiffusionPagedAttentionRow(request_id="cfg", sequence_id=i, query_len=19, seq_len=19) for i in range(2)]
)
metadata = AttentionMetadata(full_attn_spans=[[(5, 10)], [(5, 10)]])
with adapter.activate(batch):
context = adapter.prepare_layer_context(_LAYER_NAME, query, key, value, omni_attn_metadata=metadata)
assert context.piecewise_plan.homogeneous_batch_shape == (2, 19)
warmup = torch.cuda.Stream()
warmup.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(warmup):
for _ in range(3):
backend.forward_paged(context)
torch.cuda.current_stream().wait_stream(warmup)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
captured = backend.forward_paged(context)
output_ptr = captured.data_ptr()
mask = torch.ones(19, 19, device=device, dtype=torch.bool).tril()
mask[5:10, 5:10] = True
for _ in range(2):
packed.copy_(torch.randn_like(packed))
graph.replay()
actual = captured.clone()
eager = backend.forward_paged(context)
reference = F.scaled_dot_product_attention(
query.transpose(1, 2),
key.transpose(1, 2),
value.transpose(1, 2),
attn_mask=mask,
scale=layer.softmax_scale,
).transpose(1, 2)
torch.testing.assert_close(actual.reshape_as(reference), reference, rtol=2e-2, atol=2e-2)
torch.testing.assert_close(actual, eager, rtol=2e-2, atol=2e-2)
assert captured.data_ptr() == output_ptr
@@ -176,10 +176,11 @@ def test_worker_honors_explicit_kv_memory_budget(monkeypatch) -> None:
profile_request = object()
assert worker.determine_available_kv_memory(profile_request) == [4096]
worker.model_runner.profile_run.assert_called_once_with(profile_request)
worker.model_runner.profile_run.assert_not_called()
def test_worker_treats_zero_kv_memory_budget_as_profiled_auto_sizing(monkeypatch) -> None:
monkeypatch.setattr(diffusion_worker_module, "current_omni_platform", SimpleNamespace(empty_cache=Mock()))
worker = object.__new__(DiffusionWorker)
worker.rank = 0
worker.init_snapshot = SimpleNamespace(free_memory=900)
@@ -209,6 +210,7 @@ def test_worker_treats_zero_kv_memory_budget_as_profiled_auto_sizing(monkeypatch
@pytest.mark.parametrize("non_kv_cache_memory", [750, 800])
def test_worker_rejects_non_positive_profiled_kv_memory(monkeypatch, non_kv_cache_memory: int) -> None:
monkeypatch.setattr(diffusion_worker_module, "current_omni_platform", SimpleNamespace(empty_cache=Mock()))
worker = object.__new__(DiffusionWorker)
worker.rank = 0
worker.init_snapshot = SimpleNamespace(free_memory=900)
@@ -245,6 +247,7 @@ def test_worker_rejects_non_positive_profiled_kv_memory(monkeypatch, non_kv_cach
def test_worker_profiles_activation_headroom_instead_of_current_residency(monkeypatch) -> None:
monkeypatch.setattr(diffusion_worker_module, "current_omni_platform", SimpleNamespace(empty_cache=Mock()))
worker = object.__new__(DiffusionWorker)
worker.rank = 0
worker.init_snapshot = SimpleNamespace(free_memory=1200)
@@ -310,3 +313,31 @@ def test_rank_probe_gathers_local_failure_before_raising(monkeypatch) -> None:
)
assert gathered == [(False, "ValueError: local failure")]
def test_hunyuan_native_layer_identity_matches_ar_spec(monkeypatch):
import vllm_omni.diffusion.models.hunyuan_image3.hunyuan_image3_transformer as hy3
monkeypatch.setattr(hy3, "get_tensor_model_parallel_world_size", lambda: 1)
monkeypatch.setattr(hy3, "get_sequence_parallel_world_size", lambda: 1)
monkeypatch.setattr(hy3, "get_allgather_parallel_world_size", lambda: 1)
monkeypatch.setattr(hy3, "get_sequence_parallel_rank", lambda: 0)
monkeypatch.setattr(hy3, "QKVParallelLinear", lambda **_: nn.Identity())
monkeypatch.setattr(hy3, "RowParallelLinear", lambda **_: nn.Identity())
monkeypatch.setattr(hy3, "get_rope", lambda **_: nn.Identity())
# Only backend construction is fake; execute both HY3 constructors and the
# real runner spec-discovery path, including the actual Attention prefix.
def attention(**kwargs):
return _attention(enabled=kwargs.get("paged_kv_cache_role") is not None, prefix=kwargs["prefix"])
monkeypatch.setattr(hy3, "Attention", attention)
model = hy3.HunYuanAttention(
config=SimpleNamespace(num_key_value_heads=2, attention_head_dim=8),
hidden_size=16,
num_heads=2,
num_kv_heads=2,
prefix="layers.3.self_attn",
)
runner = _runner(model.image_attn.attn)
assert set(runner.get_kv_cache_spec()) == {"model.layers.3.self_attn.attn"}