[Bugfix] Restore BAGEL CFG KV transfers (#5620)

Signed-off-by: lishunyang12 <lishunyang03@gmail.com>
Co-authored-by: lishunyang12 <lishunyang03@gmail.com>
This commit is contained in:
SYLARandlishunyang12 authored and GitHub committed 2026-08-01 15:06:05 +00:00
1 parent 185e3d6df6
commit 6f98c807d2
8 files changed
+160 -8

No files matched your search

@@ -0,0 +1,57 @@
from types import SimpleNamespace
import pytest
from vllm import SamplingParams
from vllm.v1.engine import EngineCoreRequest
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
from vllm_omni.engine.async_engine_utils import apply_omni_final_stage_metadata
from vllm_omni.engine.serialization import deserialize_additional_information
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_engine_request() -> EngineCoreRequest:
return EngineCoreRequest(
request_id="req",
prompt_token_ids=[1],
mm_features=None,
sampling_params=SamplingParams(max_tokens=1),
pooling_params=None,
arrival_time=0.0,
lora_request=None,
cache_salt=None,
data_parallel_rank=None,
)
def _request_omits_kv_transfer(*, force_kv_transfer: bool) -> tuple[bool, dict]:
tagged = apply_omni_final_stage_metadata(
_make_engine_request(),
final_stage_id=0,
force_kv_transfer=force_kv_transfer,
)
scheduler = OmniARScheduler.__new__(OmniARScheduler)
scheduler._omits_kv_transfer_cache = {}
request = SimpleNamespace(
request_id="req",
additional_information=tagged.additional_information,
)
result = scheduler._request_omits_kv_transfer_to_next_stage(request)
metadata = deserialize_additional_information(tagged.additional_information)
return result, metadata
def test_stage_zero_request_omits_kv_transfer():
omits_transfer, metadata = _request_omits_kv_transfer(force_kv_transfer=False)
assert omits_transfer
assert "omni_force_kv_transfer" not in metadata
def test_cfg_companion_forces_kv_transfer_without_downstream_payload():
omits_transfer, metadata = _request_omits_kv_transfer(force_kv_transfer=True)
assert not omits_transfer
assert metadata["omni_final_stage_id"] == 0
assert metadata["omni_force_kv_transfer"] is True
@@ -7,7 +7,9 @@ from vllm.v1.engine import EngineCoreRequest
from vllm_omni.distributed.omni_coordinator import ReplicaInfo, ReplicaStatus from vllm_omni.distributed.omni_coordinator import ReplicaInfo, ReplicaStatus
from vllm_omni.engine import OmniEngineCoreRequest from vllm_omni.engine import OmniEngineCoreRequest
from vllm_omni.engine.async_omni_engine import AsyncOmniEngine, StageRuntimeInfo from vllm_omni.engine.async_omni_engine import AsyncOmniEngine, StageRuntimeInfo
from vllm_omni.engine.serialization import deserialize_additional_information
from vllm_omni.engine.stage_pool import StagePool from vllm_omni.engine.stage_pool import StagePool
from vllm_omni.model_executor.stage_input_processors.bagel import ExpandedPrompt
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -107,6 +109,34 @@ def test_build_add_request_message_preserves_model_intermediate_buffer(mocker: M
assert torch.equal(info["hidden_states"]["tts"], hidden) assert torch.equal(info["hidden_states"]["tts"], hidden)
def test_cfg_companion_suppresses_payload_but_forces_kv_transfer(mocker: MockerFixture):
engine = object.__new__(AsyncOmniEngine)
params = SamplingParams(max_tokens=8)
engine.prompt_expand_func = lambda *_args: [
ExpandedPrompt(
prompt={"prompt": "negative"},
role="cfg_text",
request_id_suffix="__cfg_text",
)
]
engine.supported_tasks = ("generate",)
engine.input_processor = mocker.Mock()
engine.input_processor.process_inputs.return_value = _make_engine_core_request("req__cfg_text")
engine.request_queue = mocker.Mock()
engine._enqueue_cfg_companions(
parent_id="req",
original_prompt={"prompt": "positive"},
stage0_params=params,
sampling_params_list=[params],
)
message = engine.request_queue.sync_q.put.call_args.args[0]
metadata = deserialize_additional_information(message.prompt.additional_information)
assert metadata["omni_final_stage_id"] == 0
assert metadata["omni_force_kv_transfer"] is True
def test_build_add_request_message_with_resumable_streaming(mocker: MockerFixture): def test_build_add_request_message_with_resumable_streaming(mocker: MockerFixture):
engine = object.__new__(AsyncOmniEngine) engine = object.__new__(AsyncOmniEngine)
params = SamplingParams(max_tokens=8) params = SamplingParams(max_tokens=8)
+35
View File
@@ -17,6 +17,9 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
import torch import torch
from vllm_omni.distributed.omni_connectors.kv_transfer_manager import (
OmniKVTransferManager,
)
from vllm_omni.outputs import OmniConnectorOutput from vllm_omni.outputs import OmniConnectorOutput
from vllm_omni.worker.omni_connector_model_runner_mixin import ( from vllm_omni.worker.omni_connector_model_runner_mixin import (
OmniConnectorModelRunnerMixin, OmniConnectorModelRunnerMixin,
@@ -1198,6 +1201,38 @@ class TestRankAwareKVRouting(unittest.TestCase):
) )
host.shutdown_omni_connectors() host.shutdown_omni_connectors()
def test_single_rank_hooks_use_default_manager_key(self):
host = self._make_host(from_tp=1, to_tp=1, local_rank=0)
expected = ["omni_0_to_1_kv_cache_req"]
self.assertEqual(host.get_rank_aware_kv_send_keys("req", from_stage=0, to_stage=1), expected)
self.assertEqual(host.get_rank_aware_kv_keys("req", from_stage=0, to_stage=1), expected)
host.shutdown_omni_connectors()
def test_init_uses_effective_manager_tp2_topology(self):
model_config = _make_model_config(stage_id=0)
model_config.omni_kv_config = {
"need_send_cache": True,
"omni_from_stage": "0",
"omni_to_stage": "1",
"rank_mapping": {"from_tp": 2, "to_tp": 2},
}
with patch(
"vllm_omni.distributed.omni_connectors.kv_transfer_manager.get_local_tp_rank",
return_value=1,
):
manager = OmniKVTransferManager.from_model_config(model_config)
host = MixinHost()
host.init_omni_connectors(model_config=model_config, kv_transfer_manager=manager)
expected = ["req_0_0_1_1"]
self.assertEqual(host.get_rank_aware_kv_send_keys("req", from_stage=0, to_stage=1), expected)
self.assertEqual(host.get_rank_aware_kv_keys("req", from_stage=0, to_stage=1), expected)
self.assertEqual(host.get_kv_rank_mapping()["from_tp"], 2)
self.assertEqual(host.get_kv_rank_mapping()["to_tp"], 2)
host.shutdown_omni_connectors()
def test_send_keys_route_from_rank_gt_to_rank(self): def test_send_keys_route_from_rank_gt_to_rank(self):
host = self._make_host(from_tp=4, to_tp=2, local_rank=3) host = self._make_host(from_tp=4, to_tp=2, local_rank=3)
self.assertEqual(host.get_rank_aware_kv_send_keys("req", from_stage=0), ["req_0_0_3_1"]) self.assertEqual(host.get_rank_aware_kv_send_keys("req", from_stage=0), ["req_0_0_3_1"])
+2 -2
View File
@@ -147,7 +147,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
return None return None
def _request_omits_kv_transfer_to_next_stage(self, request: Request) -> bool: def _request_omits_kv_transfer_to_next_stage(self, request: Request) -> bool:
"""True when orchestrator will not run stage 1+ for this request (e.g. text-only). """True when this stage-zero-final request does not need downstream KV.
The result is cached per request to avoid repeated deserialization of The result is cached per request to avoid repeated deserialization of
additional_information on every scheduler tick. additional_information on every scheduler tick.
@@ -162,7 +162,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
result = False result = False
else: else:
info = deserialize_additional_information(payload) info = deserialize_additional_information(payload)
result = info.get("omni_final_stage_id") == 0 result = info.get("omni_final_stage_id") == 0 and not bool(info.get("omni_force_kv_transfer", False))
self._omits_kv_transfer_cache[rid] = result self._omits_kv_transfer_cache[rid] = result
return result return result
@@ -403,6 +403,7 @@ class OmniKVTransferManager:
self._prefetch_executor: ThreadPoolExecutor | None = ( self._prefetch_executor: ThreadPoolExecutor | None = (
ThreadPoolExecutor(max_workers=1, thread_name_prefix="kv-prefetch") if async_prefetch else None ThreadPoolExecutor(max_workers=1, thread_name_prefix="kv-prefetch") if async_prefetch else None
) )
self._prefetch_futures: dict[str, Any] = {} self._prefetch_futures: dict[str, Any] = {}
self._bg_copy_stream: current_omni_platform.Stream | None = None self._bg_copy_stream: current_omni_platform.Stream | None = None
@@ -415,6 +416,11 @@ class OmniKVTransferManager:
except Exception as e: except Exception as e:
logger.warning("Failed to eagerly initialize sender connector: %s", e) logger.warning("Failed to eagerly initialize sender connector: %s", e)
@property
def tp_topology(self) -> KVTPTopology:
"""Return the effective topology after config and runtime detection."""
return self._tp_topo
@property @property
def topo_config(self) -> _TransferTopoConfig: def topo_config(self) -> _TransferTopoConfig:
"""Lazily build the parallel topology snapshot on first access.""" """Lazily build the parallel topology snapshot on first access."""
+6 -1
View File
@@ -77,12 +77,17 @@ def upgrade_to_omni_request(
def apply_omni_final_stage_metadata( def apply_omni_final_stage_metadata(
request: EngineCoreRequest, request: EngineCoreRequest,
final_stage_id: int, final_stage_id: int,
*,
force_kv_transfer: bool = False,
) -> EngineCoreRequest: ) -> EngineCoreRequest:
"""Tag a request so the AR scheduler can skip DiT KV for stage zero.""" """Tag a request with its final stage and optional KV-transfer override."""
merged: dict[str, Any] = {} merged: dict[str, Any] = {}
if isinstance(request, OmniEngineCoreRequest) and request.additional_information is not None: if isinstance(request, OmniEngineCoreRequest) and request.additional_information is not None:
merged = deserialize_additional_information(request.additional_information) merged = deserialize_additional_information(request.additional_information)
merged["omni_final_stage_id"] = final_stage_id merged["omni_final_stage_id"] = final_stage_id
merged.pop("omni_force_kv_transfer", None)
if force_kv_transfer:
merged["omni_force_kv_transfer"] = True
payload = serialize_additional_information(merged) payload = serialize_additional_information(merged)
return OmniEngineCoreRequest.from_request( return OmniEngineCoreRequest.from_request(
request, request,
+3 -5
View File
@@ -798,11 +798,9 @@ class AsyncOmniEngine:
supported_tasks=self.supported_tasks, supported_tasks=self.supported_tasks,
) )
request.external_req_id = cid request.external_req_id = cid
# Companions are stage-0-final: without this worker-visible tag # Companions are stage-0-final for ordinary downstream payloads,
# the AR runner would still emit downstream connector payloads # but diffusion still needs their CFG KV caches.
# for them (the orchestrator-side final_stage_id=0 registration request = apply_omni_final_stage_metadata(request, 0, force_kv_transfer=True)
# alone does not reach the worker).
request = apply_omni_final_stage_metadata(request, 0)
# Registration of this companion on stage-0's output processor is # Registration of this companion on stage-0's output processor is
# deferred to Orchestrator._handle_add_companion, which routes # deferred to Orchestrator._handle_add_companion, which routes
@@ -122,6 +122,19 @@ class OmniConnectorModelRunnerMixin:
# -- heterogeneous TP rank support -- # -- heterogeneous TP rank support --
rank_cfg = self._parse_rank_mapping(model_config) rank_cfg = self._parse_rank_mapping(model_config)
if self._kv_transfer_manager is not None:
topology = getattr(self._kv_transfer_manager, "tp_topology", None)
effective_mapping = (
getattr(topology, "source_tp_size", None),
getattr(topology, "target_tp_size", None),
getattr(topology, "local_rank", None),
)
if all(isinstance(value, int) for value in effective_mapping):
rank_cfg = {
"from_tp": effective_mapping[0],
"to_tp": effective_mapping[1],
"local_rank": effective_mapping[2],
}
self._from_tp: int = rank_cfg["from_tp"] self._from_tp: int = rank_cfg["from_tp"]
self._to_tp: int = rank_cfg["to_tp"] self._to_tp: int = rank_cfg["to_tp"]
self._local_rank: int = rank_cfg["local_rank"] self._local_rank: int = rank_cfg["local_rank"]
@@ -1275,6 +1288,10 @@ class OmniConnectorModelRunnerMixin:
For heterogeneous TP receive, the local rank is the target rank and must For heterogeneous TP receive, the local rank is the target rank and must
fetch one or more source-rank shards keyed as ``from_rank -> to_rank``. fetch one or more source-rank shards keyed as ``from_rank -> to_rank``.
""" """
if self._from_tp <= 1 and self._to_tp <= 1:
resolved_to_stage = self._next_stage_id if to_stage is None else to_stage
return [f"omni_{from_stage}_to_{resolved_to_stage}_kv_cache_{req_id}"]
remote_ranks = self.get_kv_remote_ranks() remote_ranks = self.get_kv_remote_ranks()
return [ return [
self.get_kv_connector_key( self.get_kv_connector_key(
@@ -1307,6 +1324,10 @@ class OmniConnectorModelRunnerMixin:
chunk_id: int = 0, chunk_id: int = 0,
) -> list[str]: ) -> list[str]:
"""Build send-side connector keys for this rank's KV shard(s).""" """Build send-side connector keys for this rank's KV shard(s)."""
if self._from_tp <= 1 and self._to_tp <= 1:
resolved_to_stage = self._next_stage_id if to_stage is None else to_stage
return [f"omni_{from_stage}_to_{resolved_to_stage}_kv_cache_{req_id}"]
target_ranks = self.get_kv_target_ranks_for_send() target_ranks = self.get_kv_target_ranks_for_send()
return [ return [
self.get_kv_connector_key( self.get_kv_connector_key(