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>
350 lines
14 KiB
Python
350 lines
14 KiB
Python
# 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
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
|
|
|
KV_TRANSFER_CONFIG = {
|
|
"kv_connector": "MooncakeConnector",
|
|
"kv_role": "kv_consumer",
|
|
"engine_id": "dit-engine-1",
|
|
"kv_connector_extra_config": {"mooncake_protocol": "tcp"},
|
|
}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _fixed_master_port(monkeypatch) -> None:
|
|
monkeypatch.setattr(OmniDiffusionConfig, "_resolve_master_port", lambda _self: 29500)
|
|
|
|
|
|
def test_config_roundtrip_to_kv_vllm_config() -> None:
|
|
od_config = OmniDiffusionConfig.from_kwargs(
|
|
diffusion_kv_mode="paged_scheduler",
|
|
diffusion_kv_max_rows_per_request=1,
|
|
max_model_len=64,
|
|
kv_transfer_config=dict(KV_TRANSFER_CONFIG),
|
|
)
|
|
|
|
assert isinstance(od_config.kv_transfer_config, KVTransferConfig)
|
|
assert od_config.kv_transfer_config.engine_id == "dit-engine-1"
|
|
vllm_config = create_diffusion_vllm_config(torch.device("cpu"), od_config)
|
|
assert vllm_config.kv_transfer_config is od_config.kv_transfer_config
|
|
|
|
|
|
def test_parse_requires_explicit_engine_id() -> None:
|
|
payload = dict(KV_TRANSFER_CONFIG)
|
|
payload.pop("engine_id")
|
|
with pytest.raises(ValueError, match="non-empty engine_id"):
|
|
parse_kv_transfer_config(payload)
|
|
|
|
|
|
def test_diffusion_projection_rejects_missing_engine_id_before_materialize() -> None:
|
|
"""Structured resolve must not let upstream auto-mint engine_id first."""
|
|
from vllm_omni.config.omni_config import _DiffusionConfigProjection
|
|
|
|
payload = dict(KV_TRANSFER_CONFIG)
|
|
payload.pop("engine_id")
|
|
with pytest.raises(ValueError, match="non-empty engine_id"):
|
|
_DiffusionConfigProjection.from_kwargs(kv_transfer_config=payload)
|
|
|
|
|
|
def test_diffusion_projection_preserves_explicit_engine_id() -> None:
|
|
from vllm_omni.config.omni_config import _DiffusionConfigProjection
|
|
|
|
projection = _DiffusionConfigProjection.from_kwargs(kv_transfer_config=dict(KV_TRANSFER_CONFIG))
|
|
assert isinstance(projection.kv_transfer_config, KVTransferConfig)
|
|
assert projection.kv_transfer_config.engine_id == "dit-engine-1"
|
|
|
|
od_config = OmniDiffusionConfig.from_kwargs(
|
|
diffusion_kv_mode="paged_scheduler",
|
|
diffusion_kv_max_rows_per_request=1,
|
|
kv_transfer_config=projection.kv_transfer_config,
|
|
)
|
|
assert od_config.kv_transfer_config is not None
|
|
assert od_config.kv_transfer_config.engine_id == "dit-engine-1"
|
|
|
|
|
|
class _ConcreteScheduler(BaseScheduler):
|
|
def update_from_output(self, sched_output, output) -> set[str]:
|
|
del sched_output, output
|
|
return set()
|
|
|
|
|
|
def test_scheduler_assembles_kv_stub_and_shuts_it_down() -> None:
|
|
scheduler = _ConcreteScheduler()
|
|
fake_connector = mock.Mock()
|
|
od_config = OmniDiffusionConfig.from_kwargs(
|
|
diffusion_kv_mode="paged_scheduler",
|
|
diffusion_kv_max_rows_per_request=1,
|
|
max_model_len=64,
|
|
kv_transfer_config=dict(KV_TRANSFER_CONFIG),
|
|
)
|
|
kv_vllm_config = mock.Mock()
|
|
kv_vllm_config.model_config.max_model_len = 64
|
|
kv_vllm_config.max_in_flight_tokens = 64
|
|
|
|
with (
|
|
mock.patch(
|
|
"vllm_omni.diffusion.diffusion_kv.kv_connector.KVConnectorFactory.create_connector",
|
|
return_value=fake_connector,
|
|
),
|
|
mock.patch("vllm_omni.diffusion.sched.base_scheduler.DiffusionKVCacheManager"),
|
|
):
|
|
scheduler.initialize(
|
|
od_config,
|
|
kv_cache_config=mock.sentinel.kv_cache_config,
|
|
scheduler_block_size=16,
|
|
hash_block_size=16,
|
|
kv_vllm_config=kv_vllm_config,
|
|
)
|
|
|
|
assert scheduler.kv_connector is fake_connector
|
|
with mock.patch("vllm_omni.diffusion.diffusion_kv.kv_connector.shutdown_kv_connector") as shutdown:
|
|
scheduler.close()
|
|
shutdown.assert_called_once_with(scheduler_connector=fake_connector)
|
|
|
|
|
|
def test_native_config_requires_paged_scheduler() -> None:
|
|
with pytest.raises(ValueError, match="requires diffusion_kv_mode='paged_scheduler'"):
|
|
OmniDiffusionConfig.from_kwargs(kv_transfer_config=dict(KV_TRANSFER_CONFIG))
|
|
|
|
|
|
def test_kv_and_legacy_transfer_configs_are_exclusive() -> None:
|
|
with pytest.raises(ValueError, match="exactly one KV transfer path"):
|
|
OmniDiffusionConfig.from_kwargs(
|
|
kv_transfer_config=dict(KV_TRANSFER_CONFIG),
|
|
omni_kv_config={"need_recv_cache": True},
|
|
)
|
|
|
|
|
|
def test_shutdown_kv_connector_is_idempotent() -> None:
|
|
scheduler_connector = mock.Mock()
|
|
with mock.patch("vllm_omni.diffusion.diffusion_kv.kv_connector.ensure_kv_transfer_shutdown") as ensure_shutdown:
|
|
shutdown_kv_connector(scheduler_connector=scheduler_connector)
|
|
shutdown_kv_connector()
|
|
|
|
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
|