[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:
53 files changed
+2564
-204
No files matched your search
@@ -161,6 +161,7 @@ nav:
|
||||
- Cache-DiT: design/feature/cache_dit.md
|
||||
- TeaCache: design/feature/teacache.md
|
||||
- Diffusion Continuous Batching: design/feature/diffusion_continuous_batching.md
|
||||
- Mooncake KV Transfer: design/feature/mooncake-kv-transfer.md
|
||||
- Infrastructure and Performance:
|
||||
- Prometheus Metrics: design/metrics.md
|
||||
- Speech Generation Performance Optimizations: design/qwen3_omni_tts_performance_optimization.md
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Mooncake KV Transfer
|
||||
|
||||
## PR summary
|
||||
|
||||
This change adds an opt-in AR-to-DiT KV handoff using vLLM's native
|
||||
`KVTransferConfig`, Scheduler-owned diffusion KV pages, and
|
||||
`MooncakeConnector`. The existing `OmniKVTransferManager` path remains the
|
||||
fallback when `kv_transfer_config` is absent.
|
||||
|
||||
The orchestrator creates one transfer ticket per request and resolves the
|
||||
bound AR replica's Mooncake endpoint after generation. DiT allocates the
|
||||
destination pages, waits for every worker rank to finish loading, and releases
|
||||
the source and destination pages only after the connector lifecycle completes.
|
||||
Request registration failures are rolled back before Worker dispatch. Receive
|
||||
timeouts and cancellation terminate the request without recycling in-flight
|
||||
pages: the scheduler retains them until every CFG row has completed on every
|
||||
rank. Late completion is polled only while such requests remain. If a peer
|
||||
never completes, its pages remain reserved until shutdown; other requests can
|
||||
continue using the remaining capacity. Invalid pages, missing Worker ranks,
|
||||
and failures without a proven rollback still stop the replica before teardown.
|
||||
The example sets `transfer_timeout: 60.0` seconds explicitly. Cancellation
|
||||
queued during a synchronous receive is processed when that receive completes
|
||||
or times out; it does not interrupt the remote write.
|
||||
|
||||
## Reference configuration
|
||||
|
||||
| Stage | Replicas | Devices | Parallelism | KV cache |
|
||||
| --- | ---: | --- | --- | ---: |
|
||||
| AR | 2 | 0-3 | TP=2 per replica | 2 GiB per worker |
|
||||
| DiT | 2 | 4-7 | TP=2 per replica, EP enabled | 1 GiB per worker |
|
||||
|
||||
The acceptance workload uses 512x512 images, two denoising steps, guidance
|
||||
scale 2.0, and `max_num_seqs=1`.
|
||||
|
||||
For a four-GPU debug run, set both stages to one replica and assign AR TP=2 to
|
||||
devices `0,1` and DiT TP=2 to devices `2,3`.
|
||||
|
||||
## Experiment
|
||||
|
||||
The final comparison used one node with eight NVIDIA RTX PRO 6000 Blackwell
|
||||
Server Edition GPUs, TCP Mooncake transport, the same 50-prompt/seed T2I
|
||||
dataset, concurrency 8, and two independent 100-request runs per
|
||||
implementation. Each side completed 200/200 requests without an OOM or request
|
||||
failure.
|
||||
|
||||
| Implementation | Run 1 (req/s) | Run 2 (req/s) | Mean (req/s) |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `OmniKVTransferManager` | 4.4103 | 4.4497 | 4.4300 |
|
||||
| Native `MooncakeConnector` | 4.4457 | 4.4699 | 4.4578 |
|
||||
|
||||
Native throughput is **0.63% higher** than the legacy mean. The absolute
|
||||
performance difference is below the 1% acceptance threshold.
|
||||
|
||||
For accuracy, a non-zero transferred prefix and the resulting attention output
|
||||
were compared bit-for-bit with the legacy manager path:
|
||||
|
||||
```text
|
||||
prefix_bits_equal True
|
||||
attention_bits_equal True
|
||||
max_abs_diff 0.0
|
||||
```
|
||||
|
||||
The one-AR/one-DiT TP=2 debug topology also completed 20/20 sequential
|
||||
requests. A separate real-TCP transport check transferred both CFG rows from a
|
||||
TP=1 producer to a strict-Ulysses SP=2 consumer with exact values on both
|
||||
target ranks.
|
||||
|
||||
## Scope and limitations
|
||||
|
||||
- The fully validated end-to-end topology is 2x AR TP=2 plus 2x DiT TP=2.
|
||||
- Heterogeneous TP=1 to SP=2 reshaping is transport-tested, but arbitrary mixed
|
||||
TP/CFG/SP end-to-end combinations have not been exhaustively validated.
|
||||
- CFG-parallel end-to-end deployment is not part of this acceptance run.
|
||||
- Native AR-to-DiT transfer currently requires `async_chunk: false`.
|
||||
- TCP is covered by the acceptance run. The supplementary single-node RDMA
|
||||
measurements use a separate short workload; they do not establish RDMA
|
||||
support for multi-node deployment, which remains out of scope.
|
||||
- The native path currently validates the two-stage `AR(0) -> DiT(1)` graph. Other stage graphs are rejected during startup until
|
||||
their producer/consumer edge mapping is implemented.
|
||||
- Sleep mode is disabled for the native paged path because registered KV pages
|
||||
must remain mapped for the connector lifetime.
|
||||
|
||||
- Ascend NPU adaptation and its performance validation are maintained in a
|
||||
separate follow-up PR; the GPU acceptance results above do not validate NPU.
|
||||
- The homogeneous piecewise CUDA batching optimization is retained. A native
|
||||
paged-attention CUDA graph capture/replay regression covers two CFG rows,
|
||||
strided QKV, mixed causal/image spans, changed inputs, and output stability.
|
||||
It must be run on CUDA hardware; CPU unit tests do not establish graph or
|
||||
end-to-end performance acceptance.
|
||||
@@ -62,6 +62,24 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
_DEPLOY_DIR = Path(__file__).parents[2] / "vllm_omni" / "deploy"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("async_chunk", [False, True])
|
||||
def test_native_kv_transfer_requires_completed_ar_stage(async_chunk):
|
||||
from types import SimpleNamespace
|
||||
|
||||
pipeline = SimpleNamespace(stages=(), model_type="test")
|
||||
deploy = DeployConfig(
|
||||
async_chunk=async_chunk,
|
||||
stages=[
|
||||
StageDeployConfig(stage_id=0, engine_extras={"kv_transfer_config": {"kv_connector": "MooncakeConnector"}})
|
||||
],
|
||||
)
|
||||
if async_chunk:
|
||||
with pytest.raises(ValueError, match="requires async_chunk=False"):
|
||||
omni_config_module._validate_async_chunk_support(pipeline, deploy)
|
||||
else:
|
||||
omni_config_module._validate_async_chunk_support(pipeline, deploy)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stable_test_platform(monkeypatch):
|
||||
from vllm_omni import platforms
|
||||
@@ -921,6 +939,7 @@ def test_sub_config_fields_match_structured_scopes():
|
||||
assert {f.name for f in fields(OmniStageConnectorConfig)} == {
|
||||
"async_chunk",
|
||||
"omni_kv_config",
|
||||
"kv_transfer_config",
|
||||
"stage_connector",
|
||||
"output_connectors",
|
||||
"input_connectors",
|
||||
|
||||
@@ -7,6 +7,7 @@ window fills with stale entries after K completions.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -15,6 +16,7 @@ import pytest
|
||||
# Request / RequestStatus are bound in this module. Ruff isort would reorder them.
|
||||
# isort: off
|
||||
import vllm_omni # noqa: F401 - import for side effects (patch vLLM)
|
||||
from vllm.v1.request import RequestStatus
|
||||
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
|
||||
|
||||
# isort: on
|
||||
@@ -72,3 +74,85 @@ def test_free_request_is_safe_without_a_chunk_transfer_adapter():
|
||||
sched._free_request(request) # must not raise
|
||||
|
||||
sched._free_input_coordinator_request.assert_called_once_with("req-1")
|
||||
|
||||
|
||||
def test_native_mooncake_finish_uses_confirmed_token_boundary() -> None:
|
||||
sched = _make_scheduler(chunk_transfer_adapter=None)
|
||||
sched.vllm_config = SimpleNamespace(kv_transfer_config=SimpleNamespace(kv_connector="MooncakeConnector"))
|
||||
observed = {}
|
||||
|
||||
def connector_finished(request):
|
||||
observed["status"] = request.status
|
||||
observed["num_computed_tokens"] = request.num_computed_tokens
|
||||
return True, None
|
||||
|
||||
sched._connector_finished = connector_finished
|
||||
request = _make_finished_request("req-native")
|
||||
request.client_index = 0
|
||||
request.status = RequestStatus.FINISHED_STOPPED
|
||||
request.kv_transfer_params = {
|
||||
"do_remote_decode": True,
|
||||
"transfer_id": "xfer-req-native",
|
||||
}
|
||||
request.num_computed_tokens = 13
|
||||
request.num_output_placeholders = 1
|
||||
request.num_prompt_tokens = 10
|
||||
|
||||
kv_params, _ = sched._free_request(request)
|
||||
|
||||
assert observed == {
|
||||
"status": RequestStatus.FINISHED_LENGTH_CAPPED,
|
||||
"num_computed_tokens": 12,
|
||||
}
|
||||
assert request.status == RequestStatus.FINISHED_STOPPED
|
||||
assert request.num_computed_tokens == 13
|
||||
assert kv_params == {
|
||||
"transfer_id": "xfer-req-native",
|
||||
"num_transfer_tokens": 12,
|
||||
}
|
||||
|
||||
|
||||
def test_native_mooncake_recycles_ar_pages_in_physical_order() -> None:
|
||||
sched = OmniARScheduler.__new__(OmniARScheduler)
|
||||
sched.vllm_config = SimpleNamespace(kv_transfer_config=SimpleNamespace(kv_connector="MooncakeConnector"))
|
||||
blocks = [SimpleNamespace(block_id=7), SimpleNamespace(block_id=2), SimpleNamespace(block_id=4)]
|
||||
sched.kv_cache_manager = SimpleNamespace(
|
||||
enable_caching=False,
|
||||
pop_blocks_for_free=MagicMock(return_value=blocks),
|
||||
block_pool=SimpleNamespace(free_blocks=MagicMock()),
|
||||
)
|
||||
sched.defer_block_free = False
|
||||
sched.processed_step_seq = 3
|
||||
request = SimpleNamespace(last_sched_seq=3)
|
||||
|
||||
sched._free_request_blocks(request)
|
||||
|
||||
sched.kv_cache_manager.block_pool.free_blocks.assert_called_once()
|
||||
freed = sched.kv_cache_manager.block_pool.free_blocks.call_args.args[0]
|
||||
assert [block.block_id for block in freed] == [2, 4, 7]
|
||||
|
||||
|
||||
def test_native_mooncake_deferred_ar_pages_drain_in_physical_order() -> None:
|
||||
from collections import deque
|
||||
|
||||
sched = OmniARScheduler.__new__(OmniARScheduler)
|
||||
sched.vllm_config = SimpleNamespace(kv_transfer_config=SimpleNamespace(kv_connector="MooncakeConnector"))
|
||||
blocks = [SimpleNamespace(block_id=7), SimpleNamespace(block_id=2), SimpleNamespace(block_id=4)]
|
||||
sched.kv_cache_manager = SimpleNamespace(
|
||||
enable_caching=False,
|
||||
pop_blocks_for_free=MagicMock(return_value=blocks),
|
||||
block_pool=SimpleNamespace(free_blocks=MagicMock()),
|
||||
)
|
||||
sched.defer_block_free = True
|
||||
sched.processed_step_seq = 2
|
||||
sched.sched_step_seq = 3
|
||||
sched.deferred_frees = deque()
|
||||
request = SimpleNamespace(last_sched_seq=3)
|
||||
|
||||
sched._free_request_blocks(request)
|
||||
assert [block.block_id for block in sched.deferred_frees[0][1]] == [7, 4, 2]
|
||||
|
||||
sched.processed_step_seq = 3
|
||||
sched._drain_deferred_frees()
|
||||
freed = list(sched.kv_cache_manager.block_pool.free_blocks.call_args.args[0])
|
||||
assert [block.block_id for block in freed] == [2, 4, 7]
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
|
||||
@@ -362,6 +362,20 @@ def test_prepared_model_inputs_match_local_tokenization(
|
||||
local_inputs = pipeline.prepare_model_inputs(**common_kwargs)
|
||||
prepared_inputs = pipeline.prepare_model_inputs(**common_kwargs, prepared_layout=prepared_layout)
|
||||
|
||||
if with_reference_image:
|
||||
# Preparation runs without a forward context. Coverage must come from
|
||||
# the scheduler metadata copied onto this request, including every CFG row.
|
||||
reused = pipeline.prepare_model_inputs(
|
||||
**common_kwargs, prepared_layout=prepared_layout, kv_computed_tokens=(5,) * len(prefix_lens)
|
||||
)
|
||||
assert reused["cond_vae_images"] is None and reused["cond_vit_images"] is None
|
||||
partial = pipeline.prepare_model_inputs(
|
||||
**common_kwargs,
|
||||
prepared_layout=prepared_layout,
|
||||
kv_computed_tokens=(5,) * (len(prefix_lens) - 1) + (4,),
|
||||
)
|
||||
assert partial["cond_vae_images"] is not None and partial["cond_vit_images"] is not None
|
||||
|
||||
comparable_fields = (
|
||||
"input_ids",
|
||||
"position_ids",
|
||||
@@ -474,3 +488,33 @@ def test_paged_preprocess_attaches_layout_and_scheduler_kv_requests(monkeypatch)
|
||||
(12, 17, 32)
|
||||
]
|
||||
assert len(tokenizer.calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transfer_tokens, expected", [(9, [7, 5]), (4, [4, 4])])
|
||||
def test_cfg_rows_preserve_reusable_token_ids_and_native_transfer_boundary(transfer_tokens, expected):
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import prepare_kv_requests
|
||||
|
||||
tokenizer, image_processor = _components([12, 14])
|
||||
request = _request(guidance_scale=5.0)
|
||||
layout = _prepare(request, tokenizer, image_processor)
|
||||
layout.tokenizer_output.think_recaption_end_pos = [[7], [5]]
|
||||
layout.tokenizer_output.uncond_cfg_start_pos = [[None], [5]]
|
||||
rows = build_hunyuan_diffusion_kv_requests(request, layout)
|
||||
assert [row.prompt_token_ids for row in rows] == [
|
||||
layout.tokenizer_output.tokens[0, :7].tolist(),
|
||||
layout.tokenizer_output.tokens[1, :5].tolist(),
|
||||
]
|
||||
prepare_kv_requests(rows, {"num_transfer_tokens": transfer_tokens})
|
||||
assert [row.num_prompt_tokens for row in rows] == expected
|
||||
assert all(row.kv_transfer_params["num_transfer_tokens"] == transfer_tokens for row in rows)
|
||||
layout.tokenizer_output.uncond_cfg_start_pos = [[5]]
|
||||
with pytest.raises(ValueError, match="one boundary per CFG row"):
|
||||
build_hunyuan_diffusion_kv_requests(request, layout)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("computed, expected", [((8, 8), True), ((8, 5), False), ((0, 0), False), ((), False)])
|
||||
def test_native_image_reuse_requires_coverage_in_every_cfg_row(computed, expected):
|
||||
from vllm_omni.diffusion.models.hunyuan_image3.request_layout import native_kv_covers_cond_images
|
||||
|
||||
output = TokenizerEncodeOutput(joint_image_slices=[[slice(2, 8)], [slice(2, 6)]])
|
||||
assert native_kv_covers_cond_images(output, computed) is expected
|
||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -143,6 +144,104 @@ def test_cache_manager_registers_attention_without_adding_dense_state() -> None:
|
||||
assert mgr.state_dict() == {}
|
||||
|
||||
|
||||
def test_cache_manager_initializes_with_quantization_skip_layers(monkeypatch):
|
||||
import vllm_omni.diffusion.attention.layer as layer
|
||||
from vllm_omni.diffusion.config import set_current_diffusion_config
|
||||
from vllm_omni.diffusion.models.hunyuan_image3.hunyuan_image3_transformer import ImageKVCacheManager
|
||||
|
||||
config = SimpleNamespace(
|
||||
diffusion_attention_config=None,
|
||||
parallel_config=SimpleNamespace(ring_degree=1),
|
||||
diffusion_kv_cache_dtype="auto",
|
||||
diffusion_kv_cache_skip_layer_indices={3},
|
||||
)
|
||||
monkeypatch.setattr(layer, "get_attn_backend_for_role", lambda **_: (layer.SDPABackend, None))
|
||||
monkeypatch.setattr(layer, "build_parallel_attention_strategy", lambda **_: object())
|
||||
with patched_mgr_env(), set_current_diffusion_config(config):
|
||||
# Keep the real Attention constructor and its skip-layer validation.
|
||||
with patch(f"{_TRANSFORMER_MODULE}.Attention", layer.Attention):
|
||||
mgr = ImageKVCacheManager(
|
||||
num_heads=NUM_HEADS,
|
||||
num_kv_heads=NUM_KV_HEADS,
|
||||
head_dim=HEAD_DIM,
|
||||
scaling=SCALING,
|
||||
prefix="model.layers.3.self_attn",
|
||||
)
|
||||
assert mgr.attn.layer_idx == 3
|
||||
assert not mgr.attn._should_apply_kv_cache_quant()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("first_step", [True, False])
|
||||
def test_native_connector_keeps_imported_prefix_in_paged_attention(first_step) -> 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
|
||||
query_len, prefix_len = IMAGE_TOKEN_LEN, 19
|
||||
key, value = _make_known_kv(2 * query_len)
|
||||
spans = [[(prefix_len, prefix_len + query_len)]] * 2
|
||||
with (
|
||||
set_forward_context(),
|
||||
override_paged_kv_adapter(object()),
|
||||
patch.object(mgr, "_forward_dense_legacy", side_effect=AssertionError("dense KV path")),
|
||||
patch(f"{_TRANSFORMER_MODULE}.repeat_kv", side_effect=AssertionError("KV head expansion")),
|
||||
patch(f"{_TRANSFORMER_MODULE}.get_paged_kv_computed_tokens", return_value=(prefix_len, prefix_len)),
|
||||
):
|
||||
_call_mgr(
|
||||
mgr,
|
||||
2,
|
||||
query_len,
|
||||
prefix_len + query_len,
|
||||
key,
|
||||
value,
|
||||
first_step=first_step,
|
||||
full_attn_spans=spans,
|
||||
)
|
||||
assert len(mgr.attn.paged_calls) == 1
|
||||
assert mgr.attn.paged_calls[0][1].shape == (2, query_len, NUM_KV_HEADS, HEAD_DIM)
|
||||
assert mgr.attn.paged_metadata[0].attn_mask is None
|
||||
assert mgr.attn.paged_metadata[0].full_attn_spans == spans
|
||||
assert mgr.image_kv_cache_map is None
|
||||
assert not hasattr(mgr, "_materialize_paged_prefix")
|
||||
|
||||
|
||||
def test_legacy_tensor_prefix_still_uses_dense_cache_and_reuse() -> None:
|
||||
mgr = _make_cache_mgr()
|
||||
prefix_len, prompt_len = 3, 2
|
||||
imported = _make_known_kv(prefix_len, base=10.0)
|
||||
mgr._injected_ar_kv = [imported]
|
||||
first_len = prompt_len + IMAGE_TOKEN_LEN
|
||||
key, value = _make_known_kv(first_len, base=20.0)
|
||||
with (
|
||||
patch.object(mgr, "_forward_paged", side_effect=AssertionError("legacy entered paged path")),
|
||||
):
|
||||
_call_mgr(
|
||||
mgr,
|
||||
1,
|
||||
first_len,
|
||||
prefix_len + first_len,
|
||||
key,
|
||||
value,
|
||||
first_step=True,
|
||||
gen_timestep_scatter_index=_gen_timestep_index(1, prompt_len),
|
||||
)
|
||||
assert mgr.image_kv_cache_map is not None
|
||||
assert mgr.image_kv_cache_map[0].shape[1] == prefix_len + prompt_len
|
||||
next_key, next_value = _make_known_kv(IMAGE_TOKEN_LEN, base=30.0)
|
||||
_call_mgr(
|
||||
mgr,
|
||||
1,
|
||||
IMAGE_TOKEN_LEN,
|
||||
prefix_len + first_len,
|
||||
next_key,
|
||||
next_value,
|
||||
position_ids=torch.arange(prefix_len + prompt_len, prefix_len + first_len).unsqueeze(0),
|
||||
)
|
||||
assert len(mgr.attn.calls) == 2
|
||||
assert not mgr.attn.paged_calls
|
||||
assert mgr.attn.calls[-1][1].shape == (1, prefix_len + first_len, NUM_HEADS, HEAD_DIM)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -474,12 +474,13 @@ async def test_cancelled_future_does_not_kill_busy_loop():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_native_kv_reservation_error_wakes_stream_without_killing_busy_loop():
|
||||
@pytest.mark.parametrize("error_type", [ValueError, RuntimeError])
|
||||
async def test_native_kv_reservation_error_wakes_stream_without_killing_busy_loop(error_type):
|
||||
loop = asyncio.get_running_loop()
|
||||
engine = _make_engine_with_loop(loop)
|
||||
|
||||
def fail_reservation(*args, **kwargs):
|
||||
raise RuntimeError("native allocation bug")
|
||||
raise error_type("native allocation bug")
|
||||
|
||||
engine.scheduler._diffusion_kv_manager = SimpleNamespace(
|
||||
has_request=lambda request_id: False,
|
||||
|
||||
@@ -431,7 +431,13 @@ def test_update_states_carries_prepared_layout() -> None:
|
||||
request.prepared_layout = prepared_layout
|
||||
scheduler_output = SimpleNamespace(
|
||||
finished_req_ids=set(),
|
||||
scheduled_new_reqs=[SimpleNamespace(request_id=request.request_id, req=request)],
|
||||
scheduled_new_reqs=[
|
||||
SimpleNamespace(
|
||||
request_id=request.request_id,
|
||||
req=request,
|
||||
diffusion_kv_metadata=None,
|
||||
)
|
||||
],
|
||||
scheduled_cached_reqs=SimpleNamespace(request_ids=[]),
|
||||
)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import torch
|
||||
import vllm.v1.core.single_type_kv_cache_manager as native_kv_managers
|
||||
from pytest_mock import MockerFixture
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec
|
||||
from vllm.v1.outputs import KVConnectorOutput
|
||||
|
||||
from tests.helpers.kv_layout import build_kv_cache_tensor
|
||||
from vllm_omni.diffusion.data import DiffusionOutput, DiffusionRequestAbortedError
|
||||
@@ -136,6 +137,55 @@ def _attach_diffusion_kv(request: OmniDiffusionRequest, *, seq_len: int = 8) ->
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("scheduler_cls", [RequestScheduler, StepScheduler])
|
||||
@pytest.mark.parametrize("failure", ["timeout", "registration"])
|
||||
async def test_single_native_kv_failure_reaches_output_stream(mocker, scheduler_cls, failure):
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import KVTransferRegistrationError
|
||||
|
||||
scheduler = scheduler_cls()
|
||||
_initialize_paged_scheduler(scheduler, max_num_seqs=1)
|
||||
connector = mocker.Mock()
|
||||
connector.get_num_new_matched_tokens.return_value = (4, True)
|
||||
connector.request_finished.return_value = (False, None)
|
||||
scheduler._kv_connector = connector
|
||||
request = _make_request("failed")
|
||||
_attach_diffusion_kv(request)
|
||||
request.diffusion_kv_requests[0].prompt_token_ids = [1, 2, 3, 4]
|
||||
request.kv_transfer_params = {"num_transfer_tokens": 4, "do_remote_prefill": True}
|
||||
scheduler.add_request(request)
|
||||
if failure == "registration":
|
||||
mocker.patch(
|
||||
"vllm_omni.diffusion.sched.base_scheduler.commit_kv_load",
|
||||
side_effect=KVTransferRegistrationError("registration failed"),
|
||||
)
|
||||
engine = object.__new__(DiffusionEngine)
|
||||
engine.scheduler = scheduler
|
||||
engine.od_config = SimpleNamespace(diffusion_kv_mode=DiffusionKVCacheMode.PAGED_SCHEDULER)
|
||||
engine.execution_mode = (
|
||||
DiffusionExecutionMode.STEP_BATCH if scheduler_cls is StepScheduler else DiffusionExecutionMode.REQUEST_BATCH
|
||||
)
|
||||
engine.abort_queue = queue.Queue()
|
||||
engine._cv = threading.Condition()
|
||||
engine.main_loop = asyncio.get_running_loop()
|
||||
stream = asyncio.Queue()
|
||||
engine._out_streams = {"failed": stream}
|
||||
engine.executor = mocker.Mock()
|
||||
engine.executor.prepare_kv_for_forward.return_value = KVConnectorOutput()
|
||||
scheduled = scheduler.schedule()
|
||||
engine._prepare_kv_for_forward(scheduled)
|
||||
assert scheduled.scheduled_request_ids == []
|
||||
output = BatchRunnerOutput.from_list([])
|
||||
finished = scheduler.update_from_output(scheduled, output)
|
||||
engine._emit_outputs(finished, scheduled.scheduled_request_ids, output)
|
||||
terminal = await asyncio.wait_for(stream.get(), timeout=1)
|
||||
assert terminal.finished
|
||||
assert terminal.error == ("Timed out receiving diffusion KV" if failure == "timeout" else "registration failed")
|
||||
assert scheduler.get_request_state("failed") is None
|
||||
# Reporting failure must not recycle pages that the sender can still write.
|
||||
assert scheduler._diffusion_kv_manager.has_request("failed") == (failure == "timeout")
|
||||
|
||||
|
||||
class _StubScheduler:
|
||||
def __init__(self, request: OmniDiffusionRequest, output) -> None:
|
||||
self._request = request
|
||||
@@ -602,16 +652,22 @@ class TestRequestScheduler:
|
||||
assert sched_output.finished_req_ids == {"impossible"}
|
||||
assert _new_ids(sched_output) == ["schedulable"]
|
||||
|
||||
def test_diffusion_kv_internal_allocation_error_finishes_request(self, monkeypatch) -> None:
|
||||
def test_diffusion_kv_internal_allocation_error_is_request_scoped(self, monkeypatch) -> None:
|
||||
_initialize_paged_scheduler(self.scheduler)
|
||||
request = _make_request("native-error")
|
||||
_attach_diffusion_kv(request)
|
||||
self.scheduler.add_request(request)
|
||||
manager = self.scheduler._diffusion_kv_manager
|
||||
assert manager is not None
|
||||
reserve_request = manager.reserve_request
|
||||
next_request = _make_request("after-error")
|
||||
_attach_diffusion_kv(next_request)
|
||||
self.scheduler.add_request(next_request)
|
||||
|
||||
def raise_native_error(*args, **kwargs):
|
||||
raise ValueError("native allocation bug")
|
||||
if args[0] == "native-error":
|
||||
raise ValueError("native allocation bug")
|
||||
return reserve_request(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
manager,
|
||||
@@ -619,14 +675,150 @@ class TestRequestScheduler:
|
||||
raise_native_error,
|
||||
)
|
||||
|
||||
sched_output = self.scheduler.schedule()
|
||||
|
||||
output = self.scheduler.schedule()
|
||||
state = self.scheduler.get_request_state("native-error")
|
||||
assert state is not None
|
||||
assert state.status == DiffusionRequestStatus.FINISHED_ERROR
|
||||
assert state.error == "native allocation bug"
|
||||
assert sched_output.finished_req_ids == {"native-error"}
|
||||
assert sched_output.scheduled_request_ids == []
|
||||
assert output.finished_req_ids == {"native-error"}
|
||||
assert _new_ids(output) == ["after-error"]
|
||||
assert not manager.has_request("native-error")
|
||||
|
||||
@pytest.mark.parametrize("action", ["abort", "timeout", "preempt"])
|
||||
def test_diffusion_kv_loading_defers_free_and_blocks_preempt(self, mocker: MockerFixture, action: str) -> None:
|
||||
_initialize_paged_scheduler(self.scheduler, num_blocks=5)
|
||||
connector = mocker.Mock()
|
||||
connector.get_num_new_matched_tokens.return_value = (4, True)
|
||||
connector.request_finished.return_value = (False, None)
|
||||
self.scheduler._kv_connector = connector
|
||||
request = _make_request("loading")
|
||||
request.diffusion_kv_requests = tuple(
|
||||
DiffusionKVRequest(
|
||||
f"loading/diffusion-kv/{i}",
|
||||
sequence_id=i,
|
||||
prefix_len=4,
|
||||
target_len=4,
|
||||
seq_len=8,
|
||||
prompt_token_ids=[1, 2, 3, 4],
|
||||
)
|
||||
for i in range(2)
|
||||
)
|
||||
request.kv_transfer_params = {"num_transfer_tokens": 4, "do_remote_prefill": True}
|
||||
manager = self.scheduler._diffusion_kv_manager
|
||||
assert manager is not None
|
||||
pool = manager.native_manager.block_pool
|
||||
initial_free_blocks = pool.get_num_free_blocks()
|
||||
self.scheduler.add_request(request)
|
||||
scheduled = self.scheduler.schedule()
|
||||
internal_ids = {f"loading/diffusion-kv/{i}" for i in range(2)}
|
||||
assert scheduled.kv_transfer_request_ids == internal_ids
|
||||
assert pool.get_num_free_blocks() == initial_free_blocks - 4
|
||||
metadata = manager.get_metadata("loading")
|
||||
state = self.scheduler.get_request_state("loading")
|
||||
free_request = mocker.spy(manager, "free_request")
|
||||
|
||||
if action == "abort":
|
||||
self.scheduler.finish_requests("loading", DiffusionRequestStatus.FINISHED_ABORTED)
|
||||
elif action == "timeout":
|
||||
assert self.scheduler.fail_incomplete_kv_loads(internal_ids) == {"loading"}
|
||||
assert state.error == "Timed out receiving diffusion KV"
|
||||
if action != "preempt":
|
||||
assert state.is_finished()
|
||||
assert self.scheduler.pop_request_state("loading") is state
|
||||
with pytest.raises(ValueError, match="already active"):
|
||||
self.scheduler.add_request(request)
|
||||
assert self.scheduler.get_diffusion_kv_cleanup_targets(["loading"]) == []
|
||||
|
||||
# Empty, unrelated, and partial completion retain both CFG rows even
|
||||
# after the frontend has consumed and popped the terminal request.
|
||||
for finished in (set(), {"other/diffusion-kv/0"}, {"loading/diffusion-kv/0"}):
|
||||
self.scheduler.update_kv_connector_output(KVConnectorOutput(finished_recving=finished))
|
||||
assert "loading" in self.scheduler._kv_loading_request_ids
|
||||
assert self.scheduler.preempt_request("loading") is False
|
||||
assert not self.scheduler.completed_kv_drains()
|
||||
assert manager.has_request("loading")
|
||||
assert manager.get_metadata("loading") == metadata
|
||||
assert pool.get_num_free_blocks() == initial_free_blocks - 4
|
||||
free_request.assert_not_called()
|
||||
connector.request_finished.assert_not_called()
|
||||
|
||||
# CFG completion may arrive in different polls, after rank aggregation.
|
||||
self.scheduler.update_kv_connector_output(KVConnectorOutput(finished_recving={"loading/diffusion-kv/1"}))
|
||||
assert "loading" not in self.scheduler._kv_loading_request_ids
|
||||
if action == "preempt":
|
||||
assert self.scheduler.preempt_request("loading") is True
|
||||
assert state.status == DiffusionRequestStatus.PREEMPTED
|
||||
assert manager.get_metadata("loading") == metadata
|
||||
self.scheduler.finish_requests("loading", DiffusionRequestStatus.FINISHED_ABORTED)
|
||||
else:
|
||||
assert self.scheduler.completed_kv_drains() == {"loading"}
|
||||
assert self.scheduler.get_diffusion_kv_cleanup_targets(["loading"]) == [
|
||||
("loading", metadata.allocation_generation)
|
||||
]
|
||||
self.scheduler.release_kv_drains({"loading"})
|
||||
assert not self.scheduler._running and not self.scheduler._waiting
|
||||
assert self.scheduler._kv_finished_request_ids == internal_ids
|
||||
assert connector.request_finished.call_count == 2
|
||||
free_request.assert_called_once_with("loading")
|
||||
assert not manager.has_request("loading")
|
||||
assert pool.get_num_free_blocks() == initial_free_blocks
|
||||
|
||||
@pytest.mark.parametrize("cancel", [False, True])
|
||||
def test_engine_timeout_keeps_serving_and_reclaims_late_receive(self, mocker, cancel):
|
||||
_initialize_paged_scheduler(self.scheduler, num_blocks=16, max_num_seqs=2)
|
||||
connector = mocker.Mock()
|
||||
connector.get_num_new_matched_tokens.return_value = (4, True)
|
||||
connector.request_finished.return_value = (False, None)
|
||||
self.scheduler._kv_connector = connector
|
||||
for rid in ("slow", "ready"):
|
||||
req = _make_request(rid)
|
||||
_attach_diffusion_kv(req)
|
||||
req.diffusion_kv_requests[0].prompt_token_ids = [1, 2, 3, 4]
|
||||
req.kv_transfer_params = {"num_transfer_tokens": 4, "do_remote_prefill": True}
|
||||
self.scheduler.add_request(req)
|
||||
scheduled = self.scheduler.schedule()
|
||||
engine = object.__new__(DiffusionEngine)
|
||||
engine.scheduler = self.scheduler
|
||||
engine.od_config = SimpleNamespace(diffusion_kv_mode=DiffusionKVCacheMode.PAGED_SCHEDULER)
|
||||
engine.abort_queue = queue.Queue()
|
||||
engine._cv = threading.Condition()
|
||||
if cancel:
|
||||
engine.abort_queue.put("slow")
|
||||
engine.executor = mocker.Mock()
|
||||
engine.executor.prepare_kv_for_forward.side_effect = [
|
||||
KVConnectorOutput(finished_recving={"ready/diffusion-kv/0"}),
|
||||
KVConnectorOutput(finished_recving={"slow/diffusion-kv/0"}),
|
||||
]
|
||||
fail_engine = mocker.patch.object(engine, "_fail_engine")
|
||||
engine._prepare_kv_for_forward(scheduled)
|
||||
assert scheduled.scheduled_request_ids == ["ready"]
|
||||
assert scheduled.finished_req_ids == {"slow"}
|
||||
expected_status = DiffusionRequestStatus.FINISHED_ABORTED if cancel else DiffusionRequestStatus.FINISHED_ERROR
|
||||
assert self.scheduler.get_request_state("slow").status == expected_status
|
||||
assert self.scheduler._diffusion_kv_manager.has_request("slow")
|
||||
engine._remove_diffusion_kv_requests(["slow"])
|
||||
engine.executor.remove_diffusion_kv_requests.assert_not_called()
|
||||
self.scheduler.pop_request_state("slow")
|
||||
generation = self.scheduler._kv_request_generations["slow"]
|
||||
engine._prepare_kv_for_forward(self.scheduler.schedule())
|
||||
engine.executor.remove_diffusion_kv_requests.assert_called_once_with([("slow", generation)])
|
||||
assert not self.scheduler._diffusion_kv_manager.has_request("slow")
|
||||
assert self.scheduler._diffusion_kv_manager.has_request("ready")
|
||||
fail_engine.assert_not_called()
|
||||
|
||||
def test_slow_kv_match_does_not_block_later_request(self, mocker):
|
||||
_initialize_paged_scheduler(self.scheduler, max_num_seqs=2)
|
||||
connector = mocker.Mock()
|
||||
connector.get_num_new_matched_tokens.side_effect = lambda req, _: (
|
||||
(None, False) if req.request_id.startswith("slow/") else (0, False)
|
||||
)
|
||||
self.scheduler._kv_connector = connector
|
||||
for rid in ("slow", "ready"):
|
||||
request = _make_request(rid)
|
||||
_attach_diffusion_kv(request)
|
||||
self.scheduler.add_request(request)
|
||||
output = self.scheduler.schedule()
|
||||
assert _new_ids(output) == ["ready"]
|
||||
assert list(self.scheduler._waiting) == ["slow"]
|
||||
|
||||
def test_diffusion_kv_preemption_retains_allocation(self) -> None:
|
||||
_initialize_paged_scheduler(self.scheduler, num_blocks=3)
|
||||
|
||||
@@ -835,11 +835,18 @@ class TestWorkerProcRpcRankStatus:
|
||||
|
||||
def test_execute_rpc_returns_rank_status_envelope(self, monkeypatch):
|
||||
proc = self._make_worker_proc()
|
||||
cpu_group = object()
|
||||
|
||||
monkeypatch.setattr(torch.distributed, "is_initialized", lambda: True)
|
||||
monkeypatch.setattr(torch.distributed, "get_world_size", lambda: 2)
|
||||
monkeypatch.setattr(
|
||||
diffusion_worker_module,
|
||||
"get_world_group",
|
||||
lambda: SimpleNamespace(cpu_group=cpu_group),
|
||||
)
|
||||
|
||||
def _all_gather_object(out, local):
|
||||
def _all_gather_object(out, local, *, group):
|
||||
assert group is cpu_group
|
||||
out[0] = local
|
||||
out[1] = {
|
||||
"rank": 1,
|
||||
|
||||
@@ -2395,13 +2395,20 @@ def test_dist_stage_runtime_applies_local_dp_to_stage_config(typed):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_replicas", [1, 2, 3])
|
||||
def test_typed_diffusion_replicas_share_one_config_between_planning_and_launch(mocker, num_replicas):
|
||||
@pytest.mark.parametrize("kv_owner", [None, "connector_config", "diffusion_config"])
|
||||
def test_typed_diffusion_replicas_share_one_config_between_planning_and_launch(mocker, num_replicas, kv_owner):
|
||||
from vllm_omni.config.config_factory import StageConfigFactory
|
||||
from vllm_omni.engine import stage_runtime as runtime_module
|
||||
|
||||
stage = StageConfigFactory.create_typed_default_diffusion(
|
||||
"generic-diffusion", {"model_class_name": "QwenImagePipeline"}
|
||||
).stage_configs[0]
|
||||
if kv_owner is not None:
|
||||
from vllm.config import KVTransferConfig
|
||||
|
||||
getattr(stage, kv_owner).kv_transfer_config = KVTransferConfig(
|
||||
kv_connector="MooncakeConnector", kv_role="kv_consumer", engine_id="dit"
|
||||
)
|
||||
devices = ",".join(str(i) for i in range(num_replicas))
|
||||
stage.runtime_config.devices = devices
|
||||
stage.runtime_config.num_replicas = num_replicas
|
||||
@@ -2432,11 +2439,114 @@ def test_typed_diffusion_replicas_share_one_config_between_planning_and_launch(m
|
||||
for i, plan in enumerate(replicas):
|
||||
assert plan.metadata.runtime_cfg is plan.stage_cfg.runtime_config
|
||||
assert plan.stage_cfg.runtime_config.devices == str(i)
|
||||
if num_replicas > 1:
|
||||
if num_replicas > 1 or kv_owner is not None:
|
||||
assert plan.stage_cfg is not stage
|
||||
if kv_owner is not None:
|
||||
from vllm_omni.engine.stage_init_utils import build_engine_args_dict_from_omni_stage_config
|
||||
|
||||
assert getattr(stage, kv_owner).kv_transfer_config.engine_id == "dit"
|
||||
engine_args = build_engine_args_dict_from_omni_stage_config(plan.stage_cfg, "dummy-model")
|
||||
assert engine_args["kv_transfer_config"].engine_id == f"dit-s0-r{i}"
|
||||
assert runtime._initialize_local_diffusion_replica(plan, stage_init_timeout=1) is client
|
||||
assert launch.call_args.kwargs["stage_config"] is plan.stage_cfg
|
||||
assert launch.call_args.kwargs["metadata"] is plan.metadata
|
||||
assert launch.call_args.kwargs["use_inline"] is (num_replicas == 1)
|
||||
|
||||
assert launch.call_count == 2 * num_replicas
|
||||
|
||||
|
||||
@pytest.mark.parametrize("typed", [False, True], ids=["legacy", "typed"])
|
||||
@pytest.mark.parametrize("num_replicas", [1, 2])
|
||||
def test_native_kv_producer_replica_identity_and_bootstrap_are_isolated(mocker, typed, num_replicas):
|
||||
from vllm.config import KVTransferConfig
|
||||
|
||||
from vllm_omni.config.omni_config import VllmOmniARStageConfig
|
||||
from vllm_omni.config.stage_config import StagePipelineConfig
|
||||
from vllm_omni.engine import stage_runtime as runtime_module
|
||||
|
||||
kv_config = KVTransferConfig(
|
||||
kv_connector="MooncakeConnector",
|
||||
kv_role="kv_producer",
|
||||
engine_id="ar",
|
||||
kv_ip="127.0.0.1",
|
||||
kv_connector_extra_config={"bootstrap_port": 9100},
|
||||
)
|
||||
if typed:
|
||||
stage = VllmOmniARStageConfig(stage_pipeline_config=StagePipelineConfig(stage_id=0, model_stage="ar"))
|
||||
stage.connector_config.kv_transfer_config = kv_config
|
||||
runtime_cfg = stage.runtime_config
|
||||
else:
|
||||
runtime_cfg = OmegaConf.create({"devices": "0,1", "env": {"KEEP": "value"}})
|
||||
stage = types.SimpleNamespace(stage_id=0, engine_args={"kv_transfer_config": kv_config}, runtime=runtime_cfg)
|
||||
metadata = _make_llm_metadata(0)
|
||||
mocker.patch.object(
|
||||
runtime_module,
|
||||
"extract_legacy_stage_metadata",
|
||||
side_effect=lambda cfg: types.SimpleNamespace(**{**metadata.__dict__, "runtime_cfg": cfg.runtime}),
|
||||
)
|
||||
runtime_cfg.devices = "0,1"
|
||||
runtime_cfg.env = {"KEEP": "value"}
|
||||
runtime = StageRuntime(
|
||||
stage_configs=[stage],
|
||||
model="dummy-model",
|
||||
config_path="dummy-config",
|
||||
stage_init_timeout=1,
|
||||
async_chunk=False,
|
||||
)
|
||||
mocker.patch.object(runtime_module, "get_stage_connector_spec", return_value={})
|
||||
mocker.patch.object(runtime_module, "resolve_omni_kv_config_for_stage", return_value=(None, None, None))
|
||||
for builder in ("build_engine_args_dict", "build_engine_args_dict_from_omni_stage_config"):
|
||||
mocker.patch.object(runtime_module, builder, return_value={})
|
||||
mocker.patch.object(
|
||||
runtime_module,
|
||||
"build_vllm_config",
|
||||
return_value=(types.SimpleNamespace(kv_transfer_config=kv_config), object),
|
||||
)
|
||||
|
||||
for _ in range(2):
|
||||
plans = runtime._build_logical_stage_init_plans(None, [num_replicas], {})
|
||||
for i, replica in enumerate(plans[0].replicas):
|
||||
config = replica.stage_vllm_config.kv_transfer_config
|
||||
assert config.engine_id == f"ar-s0-r{i}"
|
||||
assert config.kv_connector_extra_config["bootstrap_addr"] == f"http://127.0.0.1:{9100 + i}"
|
||||
assert dict(replica.metadata.runtime_cfg.env) == {
|
||||
"KEEP": "value",
|
||||
"VLLM_MOONCAKE_BOOTSTRAP_PORT": str(9100 + i),
|
||||
}
|
||||
if typed:
|
||||
assert replica.metadata.runtime_cfg is replica.stage_cfg.runtime_config
|
||||
assert kv_config.engine_id == "ar"
|
||||
assert kv_config.kv_connector_extra_config == {"bootstrap_port": 9100}
|
||||
assert dict(runtime_cfg.env) == {"KEEP": "value"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"roles, sources, async_chunk, valid",
|
||||
[
|
||||
(["kv_producer", "kv_consumer"], [0], False, True),
|
||||
(["kv_producer", "kv_consumer"], [0], True, False),
|
||||
([None, "kv_consumer"], [0], False, False),
|
||||
(["kv_producer", None], [0], False, False),
|
||||
([None, "kv_producer", "kv_consumer"], [1], False, False),
|
||||
(["kv_producer", "kv_consumer"], [1], False, False),
|
||||
([None, None], [0], False, True),
|
||||
],
|
||||
)
|
||||
def test_native_kv_topology_rejects_silent_legacy_fallback(roles, sources, async_chunk, valid):
|
||||
plans = []
|
||||
for stage_id, role in enumerate(roles):
|
||||
diffusion = stage_id == len(roles) - 1
|
||||
config = types.SimpleNamespace(kv_role=role) if role else None
|
||||
replica = types.SimpleNamespace(
|
||||
metadata=types.SimpleNamespace(stage_type="diffusion" if diffusion else "llm", engine_input_source=sources),
|
||||
stage_vllm_config=None if diffusion else types.SimpleNamespace(kv_transfer_config=config),
|
||||
stage_cfg=types.SimpleNamespace(engine_args={"kv_transfer_config": config}),
|
||||
)
|
||||
plans.append(types.SimpleNamespace(stage_id=stage_id, replicas=[replica]))
|
||||
runtime = object.__new__(StageRuntime)
|
||||
runtime._async_chunk = async_chunk
|
||||
if valid:
|
||||
runtime._validate_native_kv_topology(plans)
|
||||
else:
|
||||
with pytest.raises(ValueError, match="two-stage"):
|
||||
runtime._validate_native_kv_topology(plans)
|
||||
@@ -22,6 +22,7 @@ from vllm.v1.engine.exceptions import EngineDeadError
|
||||
from vllm.v1.metrics.stats import IterationStats
|
||||
|
||||
from vllm_omni.engine import OmniEngineCoreOutput
|
||||
from vllm_omni.engine.errors import NativeKVHandoffError
|
||||
from vllm_omni.engine.messages import (
|
||||
AbortRequestMessage,
|
||||
AbortResultMessage,
|
||||
@@ -1414,6 +1415,105 @@ async def test_handle_streaming_update_unknown_request_is_dropped() -> None:
|
||||
assert "req-unknown" not in orchestrator.request_states
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("native_kv", [False, True])
|
||||
async def test_add_request_attaches_native_kv_ticket_before_dispatch(mocker, native_kv) -> None:
|
||||
sampling = SamplingParams(extra_args={"keep": "sampling"})
|
||||
prompt = SimpleNamespace(sampling_params=SamplingParams(extra_args={"keep": "prompt"}))
|
||||
source = StagePool(
|
||||
0,
|
||||
FakeStageClient(),
|
||||
stage_vllm_config=SimpleNamespace(
|
||||
kv_transfer_config=SimpleNamespace(kv_role="kv_producer") if native_kv else None
|
||||
),
|
||||
)
|
||||
target = StagePool(1, FakeStageClient(stage_type="diffusion", final_output=True))
|
||||
orchestrator = Orchestrator(
|
||||
request_async_queue=asyncio.Queue(),
|
||||
output_async_queue=asyncio.Queue(),
|
||||
rpc_async_queue=asyncio.Queue(),
|
||||
stage_pools=[source, target],
|
||||
)
|
||||
|
||||
async def check_dispatch(request_id, req_state, request, **kwargs):
|
||||
assert req_state.native_kv_transfer_id == ("xfer-req" if native_kv else None)
|
||||
for params, preserved in ((sampling, "sampling"), (request.sampling_params, "prompt")):
|
||||
assert params.extra_args["keep"] == preserved
|
||||
if native_kv:
|
||||
assert params.extra_args["kv_transfer_params"] == {
|
||||
"transfer_id": "xfer-req",
|
||||
"do_remote_decode": True,
|
||||
"do_remote_prefill": False,
|
||||
}
|
||||
else:
|
||||
assert "kv_transfer_params" not in params.extra_args
|
||||
return 0
|
||||
|
||||
dispatch = mocker.patch.object(source, "submit_initial", side_effect=check_dispatch)
|
||||
await orchestrator._handle_add_request(
|
||||
StageSubmissionMessage(
|
||||
type="add_request",
|
||||
request_id="req",
|
||||
prompt=prompt,
|
||||
original_prompt={},
|
||||
output_prompt_text=None,
|
||||
sampling_params_list=[sampling, OmniDiffusionSamplingParams()],
|
||||
final_stage_id=1,
|
||||
preprocess_ms=0,
|
||||
request_timestamp=0,
|
||||
enqueue_ts=0,
|
||||
)
|
||||
)
|
||||
dispatch.assert_awaited_once()
|
||||
|
||||
|
||||
def test_native_handoff_uses_bound_replica_and_reports_missing_binding(mocker) -> None:
|
||||
orchestrator = object.__new__(Orchestrator)
|
||||
orchestrator.stage_pools = [mocker.Mock()]
|
||||
orchestrator.stage_pools[0].get_bound_client.return_value = None
|
||||
req_state = SimpleNamespace(native_kv_transfer_id="xfer-req")
|
||||
output = SimpleNamespace(kv_transfer_params={"num_transfer_tokens": 4})
|
||||
|
||||
with pytest.raises(NativeKVHandoffError, match="bound AR replica"):
|
||||
orchestrator._diffusion_submit_kwargs("req", 0, SimpleNamespace(engine_input_source=[0]), req_state, output)
|
||||
orchestrator.stage_pools[0].get_bound_client.assert_called_once_with("req")
|
||||
# The pool default points at replica 0; this request actually used replica 1.
|
||||
orchestrator.stage_pools[0].stage_vllm_config = SimpleNamespace(
|
||||
kv_transfer_config=SimpleNamespace(engine_id="ar-0")
|
||||
)
|
||||
config = SimpleNamespace(engine_id="ar-1", kv_connector_extra_config={"bootstrap_addr": "http://host:8999"})
|
||||
orchestrator.stage_pools[0].get_bound_client.return_value = SimpleNamespace(
|
||||
vllm_config=SimpleNamespace(kv_transfer_config=config)
|
||||
)
|
||||
params = orchestrator._diffusion_submit_kwargs(
|
||||
"req", 0, SimpleNamespace(engine_input_source=[0]), req_state, output
|
||||
)
|
||||
assert params["kv_transfer_params"]["remote_engine_id"] == "ar-1"
|
||||
assert params["kv_transfer_params"]["remote_bootstrap_addr"] == "http://host:8999"
|
||||
assert "remote_engine_id" not in output.kv_transfer_params
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_native_handoff_failure_is_request_scoped(mocker):
|
||||
orchestrator = object.__new__(Orchestrator)
|
||||
fail = mocker.patch.object(orchestrator, "_fail_request_client_error", new_callable=mocker.AsyncMock)
|
||||
|
||||
def lost_binding():
|
||||
raise NativeKVHandoffError("bound AR replica is unavailable")
|
||||
|
||||
assert not await orchestrator._dispatch_or_fail_request(
|
||||
lost_binding, req_id="req", stage_id=1, operation="inter-stage forward"
|
||||
)
|
||||
fail.assert_awaited_once_with(
|
||||
"req",
|
||||
1,
|
||||
"bound AR replica is unavailable",
|
||||
status_code=502,
|
||||
error_type="NativeKVHandoffError",
|
||||
release_owners=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_handle_streaming_update_passes_prompt_text_to_stage_pool() -> None:
|
||||
class RecordingPool:
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -190,7 +190,7 @@ def test_legacy_default_stage_build_accepts_engine_adapter_metadata(monkeypatch)
|
||||
)[0]
|
||||
assert "api_key" not in stage_dict["engine_args"]
|
||||
stage_cfg = create_config(stage_dict)
|
||||
metadata = SimpleNamespace(stage_id=0, cfg_kv_collect_func=None)
|
||||
metadata = SimpleNamespace(stage_id=0, cfg_kv_collect_func=None, default_sampling_params=None)
|
||||
monkeypatch.setattr(stage_init_utils.current_omni_platform, "get_device_count", lambda: 1)
|
||||
|
||||
config = stage_init_utils.build_diffusion_config("unused", stage_cfg, metadata)
|
||||
|
||||
@@ -271,6 +271,7 @@ class _ParallelEngineOverrides(_ParallelConfigEngineOverrides, total=False):
|
||||
|
||||
class _ConnectorEngineOverrides(TypedDict, total=False):
|
||||
omni_kv_config: dict[str, Any]
|
||||
kv_transfer_config: KVTransferConfig | dict[str, Any]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -332,6 +333,8 @@ def _first_defined(*values: Any) -> Any:
|
||||
|
||||
def _validate_async_chunk_support(pipeline: PipelineConfig, deploy: DeployConfig) -> None:
|
||||
has_inter_stage_edges = any(stage.input_sources for stage in pipeline.stages)
|
||||
if deploy.async_chunk and any(stage.engine_extras.get("kv_transfer_config") for stage in deploy.stages):
|
||||
raise ValueError("Native AR-to-DiT KV transfer requires async_chunk=False.")
|
||||
if (
|
||||
deploy.async_chunk
|
||||
and has_inter_stage_edges
|
||||
@@ -605,6 +608,7 @@ class OmniStageConnectorConfig:
|
||||
|
||||
async_chunk: bool = False
|
||||
omni_kv_config: dict[str, Any] | None = None
|
||||
kv_transfer_config: KVTransferConfig | None = None
|
||||
stage_connector: dict[str, Any] = field(
|
||||
default_factory=lambda: {
|
||||
"name": "SharedMemoryConnector",
|
||||
@@ -2078,6 +2082,7 @@ def _build_connector_config(
|
||||
return cast(Any, OmniStageConnectorConfig)(
|
||||
async_chunk=resolve_stage_async_chunk(deploy, stage_deploy),
|
||||
omni_kv_config=_copy_value(engine.get("omni_kv_config")),
|
||||
kv_transfer_config=_copy_value(engine.get("kv_transfer_config")),
|
||||
output_connectors=_copy_value(output_connectors) if output_connectors else None,
|
||||
input_connectors=_copy_value(input_connectors) if input_connectors else None,
|
||||
)
|
||||
|
||||
@@ -23,7 +23,10 @@ from vllm.v1.request import Request, RequestStatus, StreamingUpdate
|
||||
from vllm.v1.spec_decode.metrics import SpecDecodingStats
|
||||
|
||||
from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin
|
||||
from vllm_omni.core.sched.utils import omni_routed_experts_for_request
|
||||
from vllm_omni.core.sched.utils import (
|
||||
free_kv_blocks_in_physical_order,
|
||||
omni_routed_experts_for_request,
|
||||
)
|
||||
from vllm_omni.engine import OmniEngineCoreOutput
|
||||
from vllm_omni.engine.serialization import deserialize_additional_information
|
||||
|
||||
@@ -127,6 +130,26 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
# Output placeholders are zero when async scheduling isn't used
|
||||
return request.num_computed_tokens - request.num_output_placeholders
|
||||
|
||||
def _uses_native_mooncake_connector(self) -> bool:
|
||||
kv_config = getattr(self.vllm_config, "kv_transfer_config", None)
|
||||
return getattr(kv_config, "kv_connector", None) == "MooncakeConnector"
|
||||
|
||||
def _free_request_blocks(self, request: Request) -> None:
|
||||
"""Keep native Mooncake pages coalescible without changing vLLM APIs."""
|
||||
|
||||
if not self._uses_native_mooncake_connector() or self.kv_cache_manager.enable_caching:
|
||||
super()._free_request_blocks(request)
|
||||
return
|
||||
if not self.defer_block_free or request.last_sched_seq <= self.processed_step_seq:
|
||||
free_kv_blocks_in_physical_order(self.kv_cache_manager, request)
|
||||
return
|
||||
blocks = self.kv_cache_manager.pop_blocks_for_free(request)
|
||||
if blocks:
|
||||
# vLLM's deferred-free drain reverses this list before returning
|
||||
# it to BlockPool, so store the inverse of the desired order.
|
||||
blocks.sort(key=lambda block: block.block_id, reverse=True)
|
||||
self.deferred_frees.append((self.sched_step_seq, blocks))
|
||||
|
||||
def _resolve_kv_connector_type(self) -> str:
|
||||
"""Connector backend name for the ``kv_wait_s`` label, or ``unknown``."""
|
||||
connector_cfg = self._get_omni_kv_config_value("connector_config")
|
||||
@@ -755,9 +778,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
if req_id in self.waiting_for_transfer_free:
|
||||
req = self.requests.get(req_id)
|
||||
if req:
|
||||
self.kv_cache_manager.free(req)
|
||||
if req_id in self.requests:
|
||||
del self.requests[req_id]
|
||||
self._free_blocks(req)
|
||||
if req_id in self.transfer_triggered_requests:
|
||||
self.transfer_triggered_requests.remove(req_id)
|
||||
self.active_kv_transfers.discard(req_id)
|
||||
@@ -1041,7 +1062,36 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
||||
getattr(self, "_inflight_prefills", set()).discard(request)
|
||||
|
||||
# 1. Standard cleanup parts from base _free_request
|
||||
connector_delay_free_blocks, kv_xfer_params = self._connector_finished(request)
|
||||
status = getattr(request, "status", None)
|
||||
transfer_params = getattr(request, "kv_transfer_params", None)
|
||||
native_transfer = (
|
||||
transfer_params
|
||||
and transfer_params.get("do_remote_decode")
|
||||
and getattr(getattr(self.vllm_config, "kv_transfer_config", None), "kv_connector", None)
|
||||
== "MooncakeConnector"
|
||||
)
|
||||
if native_transfer and status == RequestStatus.FINISHED_STOPPED:
|
||||
request.status = RequestStatus.FINISHED_LENGTH_CAPPED
|
||||
num_computed_tokens = None
|
||||
if native_transfer:
|
||||
# vLLM clips the block table with
|
||||
# get_block_ids_for_computed_tokens(). Exclude Omni's optimistic
|
||||
# async output placeholders from the physical transfer boundary.
|
||||
num_computed_tokens = request.num_computed_tokens
|
||||
request.num_computed_tokens = self._get_confirmed_num_computed_tokens(request)
|
||||
try:
|
||||
connector_delay_free_blocks, kv_xfer_params = self._connector_finished(request)
|
||||
finally:
|
||||
if num_computed_tokens is not None:
|
||||
request.num_computed_tokens = num_computed_tokens
|
||||
if status is not None:
|
||||
request.status = status
|
||||
if native_transfer and connector_delay_free_blocks:
|
||||
kv_xfer_params = {
|
||||
**(kv_xfer_params or {}),
|
||||
"transfer_id": transfer_params["transfer_id"],
|
||||
"num_transfer_tokens": self._get_confirmed_num_computed_tokens(request),
|
||||
}
|
||||
|
||||
# EC Connector: mirror the KV hook (upstream v0.28 _free_request).
|
||||
# The contract requires firing before the encoder cache is freed so
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"""Shared utilities for omni schedulers."""
|
||||
|
||||
import numpy as np
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheManager
|
||||
from vllm.v1.outputs import RoutedExpertsLists
|
||||
from vllm.v1.request import Request
|
||||
|
||||
|
||||
def omni_routed_experts_for_request(routed_experts: RoutedExpertsLists, request) -> np.ndarray | None:
|
||||
@@ -21,3 +23,19 @@ def omni_routed_experts_for_request(routed_experts: RoutedExpertsLists, request)
|
||||
mask = np.isin(routed_experts.slot_mapping, list(slot_set))
|
||||
data = routed_experts.routing_data[mask]
|
||||
return data if data.size > 0 else None
|
||||
|
||||
|
||||
def free_kv_blocks_in_physical_order(manager: KVCacheManager, request: Request) -> None:
|
||||
"""Return non-cached request blocks in increasing physical order.
|
||||
|
||||
vLLM's block pool deliberately honors the eviction-priority order passed
|
||||
to ``free_blocks``. For a non-caching P/D pool there is no LRU priority to
|
||||
preserve, so physical order lets native connectors coalesce adjacent
|
||||
source/destination pages on the next allocation.
|
||||
"""
|
||||
|
||||
if manager.enable_caching:
|
||||
manager.free(request)
|
||||
return
|
||||
blocks = manager.pop_blocks_for_free(request)
|
||||
manager.block_pool.free_blocks(sorted(blocks, key=lambda block: block.block_id))
|
||||
@@ -0,0 +1,82 @@
|
||||
pipeline: hunyuan_image_3_moe
|
||||
async_chunk: false
|
||||
trust_remote_code: true
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
is_comprehension: true
|
||||
# Return AR text (including CoT) alongside the final image.
|
||||
final_output: true
|
||||
final_output_type: text
|
||||
num_replicas: 2
|
||||
devices: "0,1,2,3"
|
||||
tensor_parallel_size: 2
|
||||
max_num_seqs: 1
|
||||
max_num_batched_tokens: 32768
|
||||
kv_cache_memory_bytes: 2147483648
|
||||
gpu_memory_utilization: 0.95
|
||||
enforce_eager: true
|
||||
moe_backend: triton
|
||||
env:
|
||||
MC_TCP_ENABLE_CONNECTION_POOL: "1"
|
||||
MC_TCP_SLICE_SIZE: "4194304"
|
||||
VLLM_USE_FLASHINFER_SAMPLER: "0"
|
||||
hf_overrides:
|
||||
rope_parameters:
|
||||
mrope_section: [0, 32, 32]
|
||||
rope_type: default
|
||||
kv_transfer_config:
|
||||
kv_connector: MooncakeConnector
|
||||
kv_role: kv_producer
|
||||
engine_id: hunyuan-image3-ar
|
||||
kv_ip: 127.0.0.1
|
||||
kv_connector_extra_config:
|
||||
mooncake_protocol: tcp
|
||||
bootstrap_port: 8998
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
max_tokens: 8192
|
||||
detokenize: true
|
||||
skip_special_tokens: false
|
||||
include_stop_str_in_output: true
|
||||
|
||||
- stage_id: 1
|
||||
num_replicas: 2
|
||||
devices: "4,5,6,7"
|
||||
max_num_seqs: 1
|
||||
max_num_batched_tokens: 16384
|
||||
max_model_len: 12288
|
||||
kv_cache_memory_bytes: 1073741824
|
||||
enforce_eager: true
|
||||
# Registered Mooncake KV pages must remain mapped for the connector lifetime.
|
||||
enable_sleep_mode: false
|
||||
moe_backend: triton
|
||||
env:
|
||||
MC_TCP_ENABLE_CONNECTION_POOL: "1"
|
||||
MC_TCP_SLICE_SIZE: "4194304"
|
||||
PYTORCH_CUDA_ALLOC_CONF: expandable_segments:True
|
||||
distributed_executor_backend: mp
|
||||
diffusion_kv_mode: paged_scheduler
|
||||
diffusion_kv_max_rows_per_request: 2
|
||||
kv_transfer_config:
|
||||
kv_connector: MooncakeConnector
|
||||
kv_role: kv_consumer
|
||||
engine_id: hunyuan-image3-dit
|
||||
kv_connector_extra_config:
|
||||
mooncake_protocol: tcp
|
||||
# Seconds to wait before failing a request; in-flight pages remain reserved.
|
||||
transfer_timeout: 60.0
|
||||
parallel_config:
|
||||
tensor_parallel_size: 2
|
||||
enable_expert_parallel: true
|
||||
default_sampling_params:
|
||||
num_inference_steps: 2
|
||||
guidance_scale: 2.0
|
||||
height: 512
|
||||
width: 512
|
||||
|
||||
edges:
|
||||
- from: 0
|
||||
to: 1
|
||||
window_size: -1
|
||||
max_inflight: 1
|
||||
@@ -311,11 +311,17 @@ class FlashAttentionImpl(AttentionImpl[AttentionMetadata]):
|
||||
if native_impl is None:
|
||||
raise RuntimeError(f"Native attention implementation is not bound for diffusion layer {layer.layer_name!r}")
|
||||
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
|
||||
forward_updates_cache = layer.attn_backend.forward_includes_kv_cache_update
|
||||
# Match vLLM's split update/attention contract. Once K/V has been
|
||||
# written by do_kv_cache_update(), decoder attention consumes the
|
||||
# paged cache and does not need segment-local K/V tensors. In
|
||||
# particular, this avoids packing the same K/V projection view once
|
||||
# for every piecewise segment.
|
||||
read_kv_from_cache = not forward_updates_cache or prewrite_kv
|
||||
# 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:
|
||||
if not forward_updates_cache or prewrite_kv:
|
||||
cache_update = getattr(native_impl, "do_kv_cache_update", None)
|
||||
if not callable(cache_update):
|
||||
raise RuntimeError(
|
||||
@@ -353,14 +359,11 @@ class FlashAttentionImpl(AttentionImpl[AttentionMetadata]):
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
# Identical CFG rows can execute as one batched call per piece.
|
||||
# Keep that batch layout through the piecewise runner so Q/K/V use
|
||||
# strided slices instead of indexed gathers and results can be
|
||||
# concatenated in row-major order.
|
||||
use_homogeneous_batch = paged_kv_context.piecewise_plan.homogeneous_batch_shape is not None
|
||||
output = None
|
||||
if not use_homogeneous_batch:
|
||||
output = torch.empty(
|
||||
|
||||
@@ -1225,6 +1225,8 @@ class OmniDiffusionConfig:
|
||||
if self.diffusion_kv_mode is not DiffusionKVCacheMode.PAGED_SCHEDULER:
|
||||
raise ValueError("native kv_transfer_config requires diffusion_kv_mode='paged_scheduler'")
|
||||
self.kv_transfer_config = parse_kv_transfer_config(self.kv_transfer_config)
|
||||
if self.enable_sleep_mode:
|
||||
raise ValueError("Native KV transfer does not support sleep mode: registered pages must remain mapped")
|
||||
|
||||
self.master_port = self._resolve_master_port()
|
||||
self.request_batch_max_wait_ms = float(self.request_batch_max_wait_ms or 0.0)
|
||||
|
||||
@@ -603,7 +603,11 @@ class DiffusionEngine:
|
||||
|
||||
self._wait_for_admission_if_needed_locked()
|
||||
|
||||
sched_output = self.scheduler.schedule()
|
||||
try:
|
||||
sched_output = self.scheduler.schedule()
|
||||
except Exception as exc:
|
||||
self._fail_engine(exc)
|
||||
return
|
||||
self._scheduler_num_waiting_reqs = max(int(sched_output.num_waiting_reqs), 0)
|
||||
|
||||
self._emit_request_started_outputs(sched_output)
|
||||
@@ -613,8 +617,17 @@ class DiffusionEngine:
|
||||
continue
|
||||
|
||||
try:
|
||||
runner_output: BaseRunnerOutput = self.execute_fn(sched_output) # pyright: ignore[reportAssignmentType]
|
||||
self._prepare_kv_for_forward(sched_output)
|
||||
runner_output: BaseRunnerOutput = (
|
||||
self.execute_fn(sched_output)
|
||||
if sched_output.scheduled_request_ids
|
||||
else BatchRunnerOutput.from_list([])
|
||||
)
|
||||
worker_execution_completed = True
|
||||
except Exception as exc:
|
||||
if self._closed:
|
||||
return
|
||||
worker_execution_completed = False
|
||||
logger.error(
|
||||
"Execution failed for diffusion requests %s", sched_output.scheduled_request_ids, exc_info=True
|
||||
)
|
||||
@@ -632,8 +645,23 @@ class DiffusionEngine:
|
||||
|
||||
self._process_aborts_queue()
|
||||
self._process_rpc_queue()
|
||||
finished_req_ids = self.scheduler.update_from_output(sched_output, runner_output)
|
||||
self._emit_outputs(finished_req_ids, sched_output.scheduled_request_ids, runner_output)
|
||||
try:
|
||||
finished_req_ids = self.scheduler.update_from_output(sched_output, runner_output)
|
||||
self._emit_outputs(
|
||||
finished_req_ids,
|
||||
sched_output.scheduled_request_ids,
|
||||
runner_output,
|
||||
worker_execution_completed=worker_execution_completed,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._fail_engine(exc)
|
||||
return
|
||||
|
||||
if not sched_output.scheduled_request_ids and self.scheduler._kv_draining_requests:
|
||||
# Only exceptional in-flight receives need idle polling. New
|
||||
# requests / aborts wake this wait immediately.
|
||||
with self._cv:
|
||||
self._cv.wait(timeout=0.01)
|
||||
|
||||
# Engine is stopping: fail any RPCs still queued so callers don't hang.
|
||||
self._fail_pending_rpcs(RuntimeError("DiffusionEngine is shutting down."))
|
||||
@@ -755,15 +783,80 @@ class DiffusionEngine:
|
||||
return
|
||||
unique_request_ids = list(dict.fromkeys(request_ids))
|
||||
if unique_request_ids:
|
||||
self.executor.remove_diffusion_kv_requests(unique_request_ids)
|
||||
scheduler = getattr(self, "scheduler", None)
|
||||
target_builder = getattr(scheduler, "get_diffusion_kv_cleanup_targets", None)
|
||||
cleanup_targets = target_builder(unique_request_ids) if target_builder is not None else unique_request_ids
|
||||
try:
|
||||
if cleanup_targets:
|
||||
self.executor.remove_diffusion_kv_requests(cleanup_targets)
|
||||
except Exception as exc:
|
||||
self._fail_engine(exc)
|
||||
raise
|
||||
|
||||
def _prepare_kv_for_forward(self, sched_output: DiffusionSchedulerOutput) -> None:
|
||||
if getattr(sched_output, "kv_connector_metadata", None) is None:
|
||||
return
|
||||
try:
|
||||
output = self.executor.prepare_kv_for_forward(sched_output)
|
||||
assert output is not None
|
||||
self.scheduler.update_kv_connector_output(output)
|
||||
if not self.abort_queue.empty():
|
||||
self._process_aborts_queue()
|
||||
incomplete = sched_output.kv_transfer_request_ids - (output.finished_recving or set())
|
||||
sched_output.finished_req_ids.update(self.scheduler.fail_incomplete_kv_loads(incomplete))
|
||||
# Timed-out/cancelled requests must never reach model execution.
|
||||
terminal = {
|
||||
rid
|
||||
for rid in sched_output.scheduled_request_ids
|
||||
if (state := self.scheduler.get_request_state(rid)) is not None and state.is_finished()
|
||||
}
|
||||
if terminal:
|
||||
sched_output.finished_req_ids.update(terminal)
|
||||
sched_output.scheduled_new_reqs = [
|
||||
req for req in sched_output.scheduled_new_reqs if req.request_id not in terminal
|
||||
]
|
||||
sched_output.scheduled_cached_reqs.request_ids = [
|
||||
rid for rid in sched_output.scheduled_cached_reqs.request_ids if rid not in terminal
|
||||
]
|
||||
# is_empty / started-output handling may have already cached it.
|
||||
sched_output.__dict__.pop("scheduled_request_ids", None)
|
||||
drained = self.scheduler.completed_kv_drains()
|
||||
if drained:
|
||||
self._remove_diffusion_kv_requests(drained)
|
||||
self.scheduler.release_kv_drains(drained)
|
||||
except Exception as exc:
|
||||
self._fail_engine(exc)
|
||||
raise
|
||||
|
||||
def _fail_engine(self, exc: Exception) -> None:
|
||||
if getattr(self, "_shutdown_complete", False):
|
||||
return
|
||||
logger.error("Diffusion engine failed; stopping workers before releasing KV pages", exc_info=exc)
|
||||
with self._cv:
|
||||
self._closed = True
|
||||
if self.stop_event is not None:
|
||||
self.stop_event.set()
|
||||
streams = list(self._out_streams.values())
|
||||
self._cv.notify_all()
|
||||
for stream in streams:
|
||||
self._put_queue_output(stream, DiffusionOutput.from_exception(exc))
|
||||
self._fail_pending_rpcs(exc)
|
||||
try:
|
||||
self.executor.shutdown()
|
||||
finally:
|
||||
try:
|
||||
self.scheduler.close()
|
||||
finally:
|
||||
self._shutdown_complete = True
|
||||
|
||||
def _emit_finished_outputs(
|
||||
self,
|
||||
finished_ids: set[str],
|
||||
runner_output: BaseRunnerOutput | None = None,
|
||||
missing_result_error: str = "Diffusion execution finished without a final output",
|
||||
worker_cleaned_ids: set[str] | None = None,
|
||||
) -> None:
|
||||
self._remove_diffusion_kv_requests(finished_ids)
|
||||
self._remove_diffusion_kv_requests(finished_ids - (worker_cleaned_ids or set()))
|
||||
for rid in finished_ids:
|
||||
if runner_output is not None:
|
||||
_output = runner_output.get_request_output(rid)
|
||||
@@ -777,13 +870,21 @@ class DiffusionEngine:
|
||||
finished_ids: set[str],
|
||||
scheduled_request_ids: list[str],
|
||||
runner_output: BaseRunnerOutput,
|
||||
*,
|
||||
worker_execution_completed: bool = False,
|
||||
) -> None:
|
||||
"""Emit output chunks for every request through the unified output stream."""
|
||||
finished_ids = set(finished_ids)
|
||||
worker_cleaned_ids = finished_ids.intersection(scheduled_request_ids) if worker_execution_completed else set()
|
||||
if self.execution_mode != DiffusionExecutionMode.STEP_BATCH:
|
||||
self._emit_finished_outputs(finished_ids, runner_output)
|
||||
self._emit_finished_outputs(
|
||||
finished_ids,
|
||||
runner_output,
|
||||
worker_cleaned_ids=worker_cleaned_ids,
|
||||
)
|
||||
return
|
||||
|
||||
self._remove_diffusion_kv_requests(finished_ids)
|
||||
self._remove_diffusion_kv_requests(finished_ids - worker_cleaned_ids)
|
||||
|
||||
delivered_finished_req_ids: set[str] = set()
|
||||
|
||||
@@ -975,7 +1076,11 @@ class DiffusionEngine:
|
||||
# keep scheduling and executing until the target request is finished
|
||||
while True:
|
||||
self._process_aborts_queue()
|
||||
sched_output = self.scheduler.schedule()
|
||||
try:
|
||||
sched_output = self.scheduler.schedule()
|
||||
except Exception as exc:
|
||||
self._fail_engine(exc)
|
||||
raise
|
||||
if sched_output.is_empty:
|
||||
if target_request_id in sched_output.finished_req_ids:
|
||||
self._remove_diffusion_kv_requests([target_request_id])
|
||||
@@ -986,12 +1091,19 @@ class DiffusionEngine:
|
||||
|
||||
# NOTE: add_req_and_wait_for_response() is synchronous, will be only called
|
||||
# within _dummy_run, only one request will be scheduled
|
||||
request_id = sched_output.scheduled_request_ids[0]
|
||||
request_id = target_request_id
|
||||
try:
|
||||
runner_output: BaseRunnerOutput = self.execute_fn(sched_output) # pyright: ignore[reportAssignmentType]
|
||||
self._prepare_kv_for_forward(sched_output)
|
||||
runner_output: BaseRunnerOutput = (
|
||||
self.execute_fn(sched_output)
|
||||
if sched_output.scheduled_request_ids
|
||||
else BatchRunnerOutput.from_list([])
|
||||
)
|
||||
except EngineDeadError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
if self._closed:
|
||||
raise
|
||||
logger.error("Execution failed for diffusion request %s", request_id, exc_info=True)
|
||||
runner_output = RunnerOutput(
|
||||
request_id=request_id,
|
||||
@@ -1005,7 +1117,11 @@ class DiffusionEngine:
|
||||
finished_req_ids = self.scheduler.update_from_output(sched_output, runner_output)
|
||||
|
||||
# sync func should receive one result
|
||||
if not isinstance(runner_output, RunnerOutput) and not len(runner_output) == 1:
|
||||
if (
|
||||
sched_output.scheduled_request_ids
|
||||
and not isinstance(runner_output, RunnerOutput)
|
||||
and len(runner_output) != 1
|
||||
):
|
||||
raise ValueError("Sync func should receive one result at one time")
|
||||
if target_request_id in finished_req_ids:
|
||||
self._remove_diffusion_kv_requests([target_request_id])
|
||||
@@ -1100,7 +1216,8 @@ class DiffusionEngine:
|
||||
executes one request from the collective wave.
|
||||
|
||||
Hunyuan is currently the only model integrated with
|
||||
``paged_scheduler``; 1024x1024, enabled CFG, and the maximum advertised
|
||||
``paged_scheduler``. The stage's configured default resolution (or
|
||||
1024x1024 when absent), enabled CFG, and the maximum advertised
|
||||
reference-image count exercise its first-step activation peak.
|
||||
Admission compares each preprocessed request's CFG count and tokenized
|
||||
sequence/target shape with the resulting per-request profile envelope.
|
||||
@@ -1117,9 +1234,12 @@ class DiffusionEngine:
|
||||
model_class_name = self.od_config.model_class_name
|
||||
if model_class_name is None:
|
||||
raise RuntimeError("Diffusion KV profiling requires a resolved model_class_name")
|
||||
additional_config = getattr(self.od_config, "additional_config", {}) or {}
|
||||
profile_height = additional_config.get("diffusion_kv_profile_height", 1024)
|
||||
profile_width = additional_config.get("diffusion_kv_profile_width", 1024)
|
||||
request = self._make_dummy_request(
|
||||
height=1024,
|
||||
width=1024,
|
||||
height=profile_height,
|
||||
width=profile_width,
|
||||
guidance_scale=5.0,
|
||||
num_image_inputs=get_dummy_run_num_image_inputs(model_class_name),
|
||||
)
|
||||
@@ -1335,7 +1455,11 @@ class DiffusionEngine:
|
||||
|
||||
def _process_aborts_queue(self) -> None:
|
||||
with self._cv:
|
||||
self._drain_abort_queue()
|
||||
try:
|
||||
self._drain_abort_queue()
|
||||
except Exception as exc:
|
||||
self._fail_engine(exc)
|
||||
raise
|
||||
|
||||
def _drain_abort_queue(self) -> None:
|
||||
if self.abort_queue.empty():
|
||||
@@ -1352,11 +1476,10 @@ class DiffusionEngine:
|
||||
request_ids = [request_ids] if isinstance(request_ids, str) else list(request_ids)
|
||||
request_ids = list(dict.fromkeys(request_ids))
|
||||
|
||||
self._remove_diffusion_kv_requests(request_ids)
|
||||
|
||||
for request_id in request_ids:
|
||||
if self.scheduler.get_request_state(request_id) is not None:
|
||||
self.scheduler.finish_requests(request_id, DiffusionRequestStatus.FINISHED_ABORTED)
|
||||
self._remove_diffusion_kv_requests(request_ids)
|
||||
|
||||
def _finalize_finished_request(
|
||||
self,
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Diffusion-side assembly facade built on the upstream Mooncake KV connector.
|
||||
|
||||
The configured ``kv_connector`` (currently ``MooncakeConnector``) is created
|
||||
through vLLM's native ``KVConnectorFactory``. PR0 only validates configuration
|
||||
and creates the Scheduler/Worker connector objects; it does not subclass or
|
||||
reimplement Mooncake's transfer runtime. Real page sizing, cache registration,
|
||||
admission, and remote loading belong to the follow-up landing PR.
|
||||
"""
|
||||
"""Assemble native vLLM KV connectors for Scheduler-owned diffusion pages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
from collections import Counter
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import torch
|
||||
from vllm.config import KVTransferConfig, VllmConfig
|
||||
from vllm.distributed.kv_transfer.kv_connector.factory import KVConnectorFactory
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.base import KVConnectorRole
|
||||
from vllm.distributed.kv_transfer.kv_transfer_state import ensure_kv_transfer_initialized, ensure_kv_transfer_shutdown
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheBlocks
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
from vllm.v1.outputs import KVConnectorOutput
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.base import KVConnectorBase_V1
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheManager
|
||||
from vllm.v1.worker.gpu.kv_connector import ActiveKVConnector
|
||||
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.diffusion_kv.request import DiffusionKVRequest
|
||||
from vllm_omni.diffusion.sched.interface import DiffusionSchedulerOutput
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class KVTransferRegistrationError(ValueError):
|
||||
"""A failed handoff whose destination pages have not been dispatched."""
|
||||
|
||||
|
||||
def mint_transfer_id(request_id: str) -> str:
|
||||
"""Return the stable ticket shared by one AR -> Diffusion handoff."""
|
||||
|
||||
@@ -110,6 +115,8 @@ def parse_kv_transfer_config(value: object | None) -> KVTransferConfig | None:
|
||||
|
||||
def create_scheduler_kv_connector(
|
||||
od_config: OmniDiffusionConfig,
|
||||
kv_cache_config: KVCacheConfig | None = None,
|
||||
vllm_config: VllmConfig | None = None,
|
||||
) -> KVConnectorBase_V1 | None:
|
||||
"""Create a Scheduler-role connector when native config is present."""
|
||||
|
||||
@@ -121,14 +128,15 @@ def create_scheduler_kv_connector(
|
||||
f"Diffusion native kv_transfer_config must be KVTransferConfig, got {type(kv_transfer_config)!r}"
|
||||
)
|
||||
|
||||
vllm_config = _build_native_vllm_config(od_config)
|
||||
if kv_cache_config is None or vllm_config is None:
|
||||
raise ValueError("KV transfer requires paged_scheduler and a native cache configuration")
|
||||
connector = KVConnectorFactory.create_connector(
|
||||
config=vllm_config,
|
||||
role=KVConnectorRole.SCHEDULER,
|
||||
kv_cache_config=_empty_kv_cache_config(),
|
||||
kv_cache_config=kv_cache_config,
|
||||
)
|
||||
logger.info(
|
||||
"Created KV connector stub (SCHEDULER role): connector=%s engine_id=%s",
|
||||
"Created KV connector (SCHEDULER role): connector=%s engine_id=%s",
|
||||
kv_transfer_config.kv_connector,
|
||||
kv_transfer_config.engine_id,
|
||||
)
|
||||
@@ -140,9 +148,27 @@ def init_worker_kv_connector(vllm_config: VllmConfig, kv_cache_config: KVCacheCo
|
||||
|
||||
if vllm_config.kv_transfer_config is None:
|
||||
return
|
||||
ensure_kv_transfer_initialized(vllm_config, kv_cache_config)
|
||||
import vllm.distributed.parallel_state as parallel_state
|
||||
|
||||
from vllm_omni.diffusion.distributed.parallel_state import get_sp_group
|
||||
|
||||
# vLLM 0.29 Mooncake snapshots tp_rank/tp_size in its Worker constructor;
|
||||
# consumer receive/region mapping use those saved values, not get_tp_group.
|
||||
# Restrict this compatibility bridge to that connector and restore the
|
||||
# model's TP group even when construction fails. Never remap model TP for
|
||||
# the connector's entire lifetime (it would change model collectives).
|
||||
tp_group = parallel_state._TP
|
||||
parallel_config = getattr(vllm_config, "parallel_config", None)
|
||||
if getattr(parallel_config, "prefill_context_parallel_size", 1) > 1 and tp_group.world_size == 1:
|
||||
if vllm_config.kv_transfer_config.kv_connector != "MooncakeConnector":
|
||||
raise ValueError("Native SP KV transfer currently requires MooncakeConnector")
|
||||
parallel_state._TP = get_sp_group()
|
||||
try:
|
||||
ensure_kv_transfer_initialized(vllm_config, kv_cache_config)
|
||||
finally:
|
||||
parallel_state._TP = tp_group
|
||||
logger.info(
|
||||
"Initialized KV connector stub (WORKER role): connector=%s engine_id=%s",
|
||||
"Initialized KV connector (WORKER role): connector=%s engine_id=%s",
|
||||
vllm_config.kv_transfer_config.kv_connector,
|
||||
vllm_config.kv_transfer_config.engine_id,
|
||||
)
|
||||
@@ -156,26 +182,165 @@ def shutdown_kv_connector(*, scheduler_connector: KVConnectorBase_V1 | None = No
|
||||
ensure_kv_transfer_shutdown()
|
||||
|
||||
|
||||
def _empty_kv_cache_config() -> KVCacheConfig:
|
||||
"""Empty cache config used only for PR0 connector assembly."""
|
||||
|
||||
return KVCacheConfig(num_blocks=0, kv_cache_tensors=[], kv_cache_groups=[])
|
||||
def prepare_kv_requests(requests: tuple[DiffusionKVRequest, ...], params: Mapping[str, Any]) -> None:
|
||||
for request in requests:
|
||||
request.kv_transfer_params = dict(params)
|
||||
num_tokens = min(len(request.prompt_token_ids), params["num_transfer_tokens"])
|
||||
request.prompt_token_ids = request.prompt_token_ids[:num_tokens]
|
||||
request.num_prompt_tokens = num_tokens
|
||||
|
||||
|
||||
def _build_native_vllm_config(od_config: OmniDiffusionConfig) -> VllmConfig:
|
||||
from vllm_omni.diffusion.vllm_config import create_diffusion_vllm_config
|
||||
def install_mooncake_cfg_fanout(connector: KVConnectorBase_V1) -> None:
|
||||
"""Account for multiple CFG destinations on one native Mooncake ticket.
|
||||
|
||||
kv_transfer_config = getattr(od_config, "kv_transfer_config", None)
|
||||
assert isinstance(kv_transfer_config, KVTransferConfig)
|
||||
vllm_config = create_diffusion_vllm_config(torch.device("cpu"), od_config)
|
||||
vllm_config.kv_transfer_config = kv_transfer_config
|
||||
return vllm_config
|
||||
vLLM 0.29 initializes ``need_send`` to the paired consumer rank count,
|
||||
but increments ``sent`` per consumer request. Adapt only this producer
|
||||
instance, after KV initialization and before serving requests. Keep the
|
||||
upstream address planning, transport, timeout and completion code intact.
|
||||
"""
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector import MooncakeConnector
|
||||
|
||||
if not isinstance(connector, MooncakeConnector):
|
||||
return
|
||||
worker = connector.connector_worker
|
||||
if worker is None or worker.is_kv_consumer or getattr(worker, "_omni_cfg_fanout_installed", False):
|
||||
return
|
||||
build_transfer_params = worker._build_transfer_params
|
||||
|
||||
async def build_with_fanout(ready_reqs, agent_meta, local_regions, remote_regions):
|
||||
# commit_kv_load registers all rows before build_connector_meta. Thus
|
||||
# each rank's message contains the complete row set for each ticket,
|
||||
# including empty-block notifications. Count the full message, not
|
||||
# just the subset whose producer-ready events fired in this iteration.
|
||||
rows_per_ticket = Counter(transfer_id for transfer_id, _ in agent_meta.req_blocks.values())
|
||||
tickets = {send_meta.transfer_id: send_meta for _, send_meta in ready_reqs}
|
||||
for transfer_id, send_meta in tickets.items():
|
||||
rows = rows_per_ticket[transfer_id]
|
||||
previous = getattr(send_meta, "_omni_cfg_rows", None)
|
||||
if rows < 1 or (previous is not None and previous != rows):
|
||||
# Use the upstream transfer-error path: it decrements sending
|
||||
# without incrementing sent, so a malformed rank cannot free
|
||||
# source pages still needed by another rank.
|
||||
return [], [], [], [req_id for req_id, _ in ready_reqs], "Inconsistent Mooncake CFG receive count"
|
||||
# No await between checking and updating: all handlers run on the
|
||||
# sender loop. The marker belongs to the ticket, not to a request-global
|
||||
# variable; it disappears with the upstream ticket on completion/expiry.
|
||||
for transfer_id, send_meta in tickets.items():
|
||||
if getattr(send_meta, "_omni_cfg_rows", None) is None:
|
||||
send_meta.need_send *= rows_per_ticket[transfer_id]
|
||||
send_meta._omni_cfg_rows = rows_per_ticket[transfer_id]
|
||||
return await build_transfer_params(ready_reqs, agent_meta, local_regions, remote_regions)
|
||||
|
||||
worker._build_transfer_params = build_with_fanout # type: ignore[method-assign]
|
||||
setattr(worker, "_omni_cfg_fanout_installed", True)
|
||||
|
||||
|
||||
def commit_kv_load(
|
||||
connector: KVConnectorBase_V1,
|
||||
manager: KVCacheManager,
|
||||
requests: tuple[DiffusionKVRequest, ...],
|
||||
matched_tokens: list[int],
|
||||
) -> set[str]:
|
||||
expected = set()
|
||||
allocations = []
|
||||
# All rows sharing a transfer_id must reach the same connector metadata:
|
||||
# the producer uses this complete per-rank row set for fan-out accounting.
|
||||
# Validate every CFG row before mutating any connector state.
|
||||
for request, num_tokens in zip(requests, matched_tokens, strict=True):
|
||||
blocks = manager.get_blocks(request.request_id)
|
||||
# Mooncake's producer advertises complete physical blocks. Keep every
|
||||
# CFG receiver on that same block boundary so the native connector can
|
||||
# use its normal one-to-one page mapping even when the branches have
|
||||
# different logical prefix lengths. Bytes beyond ``num_tokens`` are
|
||||
# not marked computed and are overwritten by the DiT prefill.
|
||||
transfer_tokens = num_tokens
|
||||
if num_tokens > 0 and request.kv_transfer_params is not None:
|
||||
transfer_tokens = request.kv_transfer_params["num_transfer_tokens"]
|
||||
if type(transfer_tokens) is not int or not num_tokens <= transfer_tokens <= request.num_tokens:
|
||||
raise KVTransferRegistrationError(
|
||||
"Diffusion KV transfer boundary must cover the reusable prefix "
|
||||
f"without exceeding the allocated sequence: reusable={num_tokens}, "
|
||||
f"transfer={transfer_tokens!r}, allocated={request.num_tokens}"
|
||||
)
|
||||
prefix_blocks = KVCacheBlocks(
|
||||
tuple(
|
||||
group[: (transfer_tokens + spec.kv_cache_spec.block_size - 1) // spec.kv_cache_spec.block_size]
|
||||
for group, spec in zip(blocks.blocks, manager.kv_cache_config.kv_cache_groups, strict=True)
|
||||
)
|
||||
)
|
||||
allocations.append((request, prefix_blocks, num_tokens))
|
||||
try:
|
||||
for request, prefix_blocks, num_tokens in allocations:
|
||||
connector.update_state_after_alloc(request, prefix_blocks, num_tokens)
|
||||
request.num_computed_tokens = num_tokens
|
||||
if request.kv_transfer_params is not None:
|
||||
expected.add(request.request_id)
|
||||
except Exception as exc:
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector import MooncakeConnector
|
||||
from vllm.v1.request import RequestStatus
|
||||
|
||||
if not isinstance(connector, MooncakeConnector):
|
||||
# Unknown connectors may have started I/O during registration.
|
||||
raise
|
||||
# Mooncake only queues metadata here; no Worker has seen addresses yet.
|
||||
# Re-arm its pre-scheduling abort hook to replace even partially
|
||||
# registered CFG receives with empty-block notifications to the producer.
|
||||
for request in requests:
|
||||
if request.kv_transfer_params is not None:
|
||||
request.kv_transfer_params["do_remote_prefill"] = True
|
||||
request.status = RequestStatus.FINISHED_ABORTED
|
||||
delay_free, _ = connector.request_finished(request, [])
|
||||
if delay_free:
|
||||
raise RuntimeError("Connector could not cancel an undispatched KV load") from exc
|
||||
raise KVTransferRegistrationError(f"Could not register diffusion KV receive: {exc}") from exc
|
||||
return expected
|
||||
|
||||
|
||||
def wait_for_kv_load(
|
||||
active_connector: ActiveKVConnector, scheduler_output: DiffusionSchedulerOutput, timeout: float
|
||||
) -> KVConnectorOutput:
|
||||
connector = active_connector.kv_connector
|
||||
active_connector.pre_forward(scheduler_output)
|
||||
pending = set(scheduler_output.kv_transfer_request_ids)
|
||||
finished_ids = scheduler_output.kv_finished_request_ids
|
||||
if not pending:
|
||||
output = active_connector.post_forward(finished_ids)
|
||||
if output.invalid_block_ids:
|
||||
raise RuntimeError("Diffusion KV connector reported invalid remote pages")
|
||||
return output
|
||||
|
||||
received, sent = set(), set()
|
||||
deadline = time.monotonic() + timeout
|
||||
while True:
|
||||
finished_sending, finished_recving = connector.get_finished(finished_ids)
|
||||
sent.update(finished_sending or ())
|
||||
received.update(finished_recving or ())
|
||||
pending.difference_update(received)
|
||||
if connector.get_block_ids_with_load_errors():
|
||||
raise RuntimeError("Diffusion KV connector reported invalid remote pages")
|
||||
if not pending:
|
||||
break
|
||||
if time.monotonic() >= deadline:
|
||||
# A timeout is not a cancellation acknowledgement. Return partial
|
||||
# completion; Scheduler quarantines the remaining destination pages.
|
||||
break
|
||||
time.sleep(0.001)
|
||||
output = active_connector.post_forward(finished_ids)
|
||||
output.finished_sending = sent | (output.finished_sending or set())
|
||||
output.finished_recving = received | (output.finished_recving or set())
|
||||
if output.invalid_block_ids:
|
||||
raise RuntimeError("Diffusion KV connector reported invalid remote pages")
|
||||
return output
|
||||
|
||||
|
||||
def _validate_kv_transfer_config(config: KVTransferConfig) -> None:
|
||||
engine_id = config.engine_id
|
||||
if not isinstance(engine_id, str) or not engine_id.strip():
|
||||
raise ValueError("Diffusion native kv_transfer_config requires a non-empty engine_id")
|
||||
extra_config = config.kv_connector_extra_config or {}
|
||||
timeout = extra_config.get("transfer_timeout", 60.0)
|
||||
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or timeout <= 0:
|
||||
raise ValueError("Diffusion transfer_timeout must be a positive finite number of seconds")
|
||||
if config.kv_connector is not None and config.kv_role is None:
|
||||
raise ValueError("Diffusion native kv_transfer_config requires kv_role when kv_connector is set")
|
||||
if config.kv_role not in (None, "kv_consumer", "kv_producer", "kv_both"):
|
||||
|
||||
@@ -8,6 +8,7 @@ from collections.abc import Sequence
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheManager
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
|
||||
from vllm_omni.core.sched.utils import free_kv_blocks_in_physical_order
|
||||
from vllm_omni.diffusion.diffusion_kv.metadata import (
|
||||
DiffusionKVMetadata,
|
||||
DiffusionKVSequenceMetadata,
|
||||
@@ -179,7 +180,7 @@ class DiffusionKVCacheManager:
|
||||
requests = self._requests.pop(public_request_id, ())
|
||||
self._metadata.pop(public_request_id, None)
|
||||
for request in reversed(requests):
|
||||
self.native_manager.free(request)
|
||||
free_kv_blocks_in_physical_order(self.native_manager, request)
|
||||
self._internal_request_ids.discard(request.request_id)
|
||||
|
||||
def close(self) -> None:
|
||||
@@ -188,4 +189,4 @@ class DiffusionKVCacheManager:
|
||||
|
||||
def _rollback(self, requests: Sequence[DiffusionKVRequest]) -> None:
|
||||
for request in reversed(requests):
|
||||
self.native_manager.free(request)
|
||||
free_kv_blocks_in_physical_order(self.native_manager, request)
|
||||
@@ -41,6 +41,7 @@ class DiffusionKVSequenceMetadata:
|
||||
seq_len: int
|
||||
block_ids: tuple[list[int], ...]
|
||||
context_ids: tuple[str, ...] = ()
|
||||
num_computed_tokens: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -190,6 +190,10 @@ class DiffusionKVModelRunnerBackend:
|
||||
)
|
||||
return ulysses_degree
|
||||
|
||||
@property
|
||||
def kv_caches_by_layer(self) -> dict[str, torch.Tensor]:
|
||||
return {name: layer.kv_cache for name, layer in self._kv_cache_layer_adapters.items()}
|
||||
|
||||
def _get_max_rows_per_request(self) -> int:
|
||||
max_rows = getattr(self.od_config, "diffusion_kv_max_rows_per_request", None)
|
||||
if type(max_rows) is not int or max_rows <= 0:
|
||||
@@ -245,7 +249,9 @@ class DiffusionKVModelRunnerBackend:
|
||||
for capacity, block_size in zip(unaligned_max_num_blocks_per_group, block_sizes, strict=True)
|
||||
]
|
||||
|
||||
max_num_reqs = max_num_seqs * max_rows_per_request
|
||||
max_num_reqs = max_num_seqs
|
||||
if max_num_reqs % max_rows_per_request:
|
||||
raise ValueError("scheduler_config.max_num_seqs must be a multiple of diffusion_kv_max_rows_per_request")
|
||||
max_num_batched_tokens = getattr(scheduler_config, "max_num_batched_tokens", None)
|
||||
if type(max_num_batched_tokens) is not int or max_num_batched_tokens <= 0:
|
||||
raise ValueError("scheduler_config.max_num_batched_tokens must be a positive integer")
|
||||
@@ -254,9 +260,6 @@ class DiffusionKVModelRunnerBackend:
|
||||
cp_rank = get_dcp_group().rank_in_group if cp_size > 1 else 0
|
||||
cp_interleave = parallel_config.cp_kv_cache_interleave_size
|
||||
|
||||
# Native metadata builders size per-row buffers from max_num_seqs.
|
||||
# A diffusion public request can occupy several sequence/context rows.
|
||||
scheduler_config.max_num_seqs = max_num_reqs
|
||||
kv_caches: list[torch.Tensor | list[torch.Tensor]] = []
|
||||
previous_adapter_caches = {
|
||||
layer_name: adapter.kv_cache for layer_name, adapter in self._kv_cache_layer_adapters.items()
|
||||
@@ -303,7 +306,6 @@ class DiffusionKVModelRunnerBackend:
|
||||
self.vllm_config,
|
||||
)
|
||||
except Exception:
|
||||
scheduler_config.max_num_seqs = max_num_seqs
|
||||
for layer_name, previous_cache in previous_adapter_caches.items():
|
||||
self._kv_cache_layer_adapters[layer_name].kv_cache = previous_cache
|
||||
raise
|
||||
@@ -591,7 +593,7 @@ class DiffusionKVModelRunnerBackend:
|
||||
)
|
||||
raise RuntimeError(f"Diffusion KV request state is missing logical length for {identity!r}")
|
||||
|
||||
def remove_diffusion_kv_requests(self, request_ids: list[str]) -> int:
|
||||
def remove_diffusion_kv_requests(self, request_ids: list[str | tuple[str, int]]) -> int:
|
||||
"""Retire Worker rows without logically freeing Scheduler-owned blocks."""
|
||||
if (
|
||||
not is_scheduler_paged_kv_mode(
|
||||
@@ -600,11 +602,25 @@ class DiffusionKVModelRunnerBackend:
|
||||
or self.block_tables is None
|
||||
):
|
||||
return 0
|
||||
request_id_set = set(request_ids)
|
||||
request_generations = {
|
||||
request_id: generation
|
||||
for item in request_ids
|
||||
if isinstance(item, tuple)
|
||||
for request_id, generation in (item,)
|
||||
if isinstance(request_id, str) and type(generation) is int
|
||||
}
|
||||
request_id_set = {item if isinstance(item, str) else item[0] for item in request_ids}
|
||||
identities_and_rows = [
|
||||
(identity, row)
|
||||
for identity, row in self._diffusion_kv_identity_to_row.items()
|
||||
if identity[0] in request_id_set
|
||||
and (
|
||||
identity[0] not in request_generations
|
||||
or (
|
||||
(request_state := self._diffusion_kv_request_states.get(identity[0])) is not None
|
||||
and request_state.generation == request_generations[identity[0]]
|
||||
)
|
||||
)
|
||||
]
|
||||
rows = [row for _, row in identities_and_rows]
|
||||
if rows:
|
||||
@@ -620,9 +636,10 @@ class DiffusionKVModelRunnerBackend:
|
||||
]
|
||||
self._apply_rows(rows, installs)
|
||||
|
||||
removed_request_ids = {identity[0] for identity, _ in identities_and_rows}
|
||||
for identity, _ in identities_and_rows:
|
||||
del self._diffusion_kv_identity_to_row[identity]
|
||||
for request_id in request_id_set:
|
||||
for request_id in removed_request_ids:
|
||||
self._diffusion_kv_request_states.pop(request_id, None)
|
||||
self._diffusion_kv_free_rows.extend(rows)
|
||||
self._diffusion_kv_free_rows.sort(reverse=True)
|
||||
|
||||
@@ -84,12 +84,14 @@ class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
|
||||
attention_config = vllm_config.attention_config
|
||||
previous_backend = attention_config.backend
|
||||
previous_backend_per_kind = attention_config.backend_per_kind
|
||||
previous_pcp_size = vllm_config.parallel_config.prefill_context_parallel_size
|
||||
try:
|
||||
# This is a portable vLLM backend request. The active platform
|
||||
# resolves it to its native implementation, such as FlashAttention
|
||||
# on CUDA or AscendAttentionBackend on NPU.
|
||||
attention_config.backend = AttentionBackendEnum.FLASH_ATTN
|
||||
attention_config.backend_per_kind = {}
|
||||
vllm_config.parallel_config.prefill_context_parallel_size = 1
|
||||
with set_current_vllm_config(vllm_config):
|
||||
attn_backend = get_attn_backend(
|
||||
head_size=spec.head_size,
|
||||
@@ -101,6 +103,7 @@ class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
|
||||
finally:
|
||||
attention_config.backend = previous_backend
|
||||
attention_config.backend_per_kind = previous_backend_per_kind
|
||||
vllm_config.parallel_config.prefill_context_parallel_size = previous_pcp_size
|
||||
attn_backend = current_omni_platform.get_diffusion_paged_kv_attn_backend(
|
||||
attn_backend,
|
||||
ulysses_degree=ulysses_degree,
|
||||
@@ -758,6 +761,19 @@ class DiffusionPagedAttentionAdapter:
|
||||
slot_mappings=slot_mappings,
|
||||
causal=(row_segments[0].mode == "causal"),
|
||||
)
|
||||
# FA2's ``num_splits=0`` auto policy selects SplitKV for these
|
||||
# already-small piecewise calls. Reuse vLLM's supported metadata
|
||||
# control to select the single-pass kernel and avoid its extra
|
||||
# partial-output/reduction work. Leave newer native backends and
|
||||
# metadata without this control untouched.
|
||||
for layer_name, native_metadata in segment_metadata.items():
|
||||
layer = self.layers.get(layer_name)
|
||||
if (
|
||||
layer is not None
|
||||
and getattr(layer.impl, "vllm_flash_attn_version", None) == 2
|
||||
and hasattr(native_metadata, "max_num_splits")
|
||||
):
|
||||
native_metadata.max_num_splits = 1
|
||||
# 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
|
||||
@@ -901,9 +917,17 @@ class DiffusionPagedAttentionAdapter:
|
||||
raise KeyError(
|
||||
f"No piecewise native attention metadata was built for diffusion layer {layer_name!r}"
|
||||
) from exc
|
||||
# Identical rows are sliced and packed once per segment by the
|
||||
# homogeneous piecewise runner. Keeping the projection view here
|
||||
# avoids first copying the complete Q tensor only to copy each segment
|
||||
# again immediately afterwards. Other native paths still require one
|
||||
# packed query buffer.
|
||||
query_input = query_flat
|
||||
if piecewise_plan is None or piecewise_plan.homogeneous_batch_shape is None:
|
||||
query_input = query_flat.contiguous()
|
||||
return DiffusionPagedAttentionContext(
|
||||
layer=layer,
|
||||
query=query_flat.contiguous(),
|
||||
query=query_input,
|
||||
key_write=key_flat,
|
||||
value_write=value_flat,
|
||||
slot_mapping=slot_mapping,
|
||||
|
||||
@@ -113,8 +113,6 @@ class DiffusionKVRequest:
|
||||
self.shared_prefix_boundary = 0
|
||||
# Opaque request-scoped parameters consumed by an upstream KVConnector.
|
||||
self.kv_transfer_params = kv_transfer_params
|
||||
# Mooncake derives the remote-load length from this list. PR0 carries
|
||||
# the field without creating a placeholder or starting a load.
|
||||
self.prompt_token_ids = prompt_token_ids
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from dataclasses import replace
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm.utils.import_utils import resolve_obj_by_qualname
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheSpec
|
||||
from vllm.v1.outputs import KVConnectorOutput
|
||||
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.sched.interface import CachedRequestData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
@@ -159,7 +165,7 @@ class DiffusionExecutor(ABC):
|
||||
# ranks are not silently dropped.
|
||||
self.collective_rpc("set_kv_cache_configs", args=(kv_cache_configs, resolved_max_model_len))
|
||||
|
||||
def remove_diffusion_kv_requests(self, request_ids: list[str]) -> None:
|
||||
def remove_diffusion_kv_requests(self, request_ids: list[str | tuple[str, int]]) -> None:
|
||||
"""Clear request rows on every Worker after Scheduler retirement."""
|
||||
|
||||
unique_request_ids = list(dict.fromkeys(request_ids))
|
||||
@@ -170,6 +176,35 @@ class DiffusionExecutor(ABC):
|
||||
args=(unique_request_ids,),
|
||||
)
|
||||
|
||||
def prepare_kv_for_forward(self, scheduler_output: DiffusionSchedulerOutput) -> KVConnectorOutput | None:
|
||||
if scheduler_output.kv_connector_metadata is None:
|
||||
return
|
||||
transfer_output = replace(
|
||||
scheduler_output,
|
||||
scheduled_new_reqs=[],
|
||||
scheduled_cached_reqs=CachedRequestData.make_empty(),
|
||||
kv_prefetch_job=None,
|
||||
)
|
||||
outputs: list[KVConnectorOutput] = self.collective_rpc(
|
||||
"prepare_kv_for_forward",
|
||||
args=(transfer_output,),
|
||||
unique_reply_rank=0,
|
||||
exec_all_ranks=True,
|
||||
)
|
||||
if len(outputs) != self.od_config.num_gpus or any(output.invalid_block_ids for output in outputs):
|
||||
# Missing ranks / invalid pages cannot establish safe ownership.
|
||||
raise RuntimeError("Diffusion KV receive failed on one or more ranks")
|
||||
completed = getattr(self, "_kv_receive_completed_ranks", {})
|
||||
for rank, output in enumerate(outputs):
|
||||
for request_id in output.finished_recving or ():
|
||||
completed.setdefault(request_id, set()).add(rank)
|
||||
finished = {request_id for request_id, ranks in completed.items() if len(ranks) == len(outputs)}
|
||||
for request_id in finished:
|
||||
del completed[request_id]
|
||||
self._kv_receive_completed_ranks = completed
|
||||
outputs[0].finished_recving = finished
|
||||
return outputs[0]
|
||||
|
||||
@abstractmethod
|
||||
def shutdown(self) -> None:
|
||||
"""Shutdown the executor and release resources."""
|
||||
|
||||
@@ -282,6 +282,47 @@ def set_forward_context_denoise_step_idx(step_idx: int | None) -> None:
|
||||
ensure_active(step_idx)
|
||||
|
||||
|
||||
def get_paged_kv_computed_tokens() -> tuple[int, ...]:
|
||||
runtime = _forward_context.paged_kv_runtime if _forward_context is not None else None
|
||||
if runtime is None:
|
||||
return ()
|
||||
return tuple(row.kv_start_pos for row in runtime.metadata.prefill_rows)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def paged_kv_prefill(sequence_id: int, num_tokens: int):
|
||||
from dataclasses import replace
|
||||
|
||||
runtime = get_forward_context().paged_kv_runtime
|
||||
if runtime is None:
|
||||
raise RuntimeError("Paged KV prefill requires an active runtime")
|
||||
rows = runtime.metadata.prefill_rows
|
||||
matching_rows = [row for row in rows if row.sequence_id == sequence_id]
|
||||
if len(matching_rows) != 1:
|
||||
raise ValueError(
|
||||
f"Paged KV prefill requires exactly one active row for sequence {sequence_id}; found {len(matching_rows)}"
|
||||
)
|
||||
row = matching_rows[0]
|
||||
if type(num_tokens) is not int or not row.kv_start_pos < num_tokens <= row.seq_len:
|
||||
raise ValueError(
|
||||
"Paged KV prefill target must extend the active prefix without exceeding its allocation: "
|
||||
f"start={row.kv_start_pos}, target={num_tokens!r}, allocated={row.seq_len}"
|
||||
)
|
||||
prefill = replace(row, query_len=num_tokens - row.kv_start_pos, seq_len=num_tokens)
|
||||
batch = runtime.adapter.prepare_batch((prefill,))
|
||||
with runtime.adapter.activate(batch):
|
||||
yield
|
||||
runtime.metadata = replace(
|
||||
runtime.metadata,
|
||||
prefill_rows=tuple(
|
||||
replace(item, kv_start_pos=num_tokens, query_len=item.seq_len - num_tokens)
|
||||
if item.sequence_id == sequence_id
|
||||
else item
|
||||
for item in rows
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def set_forward_context_denoise_timestep(timestep: float | None) -> None:
|
||||
"""Set the normalized (descending, 1 -> 0) denoise timestep.
|
||||
|
||||
|
||||
@@ -75,7 +75,11 @@ from vllm_omni.diffusion.distributed.sp_plan import (
|
||||
SequenceParallelOutput,
|
||||
)
|
||||
from vllm_omni.diffusion.distributed.utils import get_local_device
|
||||
from vllm_omni.diffusion.forward_context import set_forward_context_denoise_step_idx
|
||||
from vllm_omni.diffusion.forward_context import (
|
||||
get_paged_kv_computed_tokens,
|
||||
paged_kv_prefill,
|
||||
set_forward_context_denoise_step_idx,
|
||||
)
|
||||
from vllm_omni.diffusion.layers.fused_moe import FusedMoE
|
||||
from vllm_omni.diffusion.layers.norm import RMSNorm
|
||||
from vllm_omni.diffusion.layers.rope import RotaryEmbedding
|
||||
@@ -1097,8 +1101,6 @@ class ImageKVCacheManager(nn.Module):
|
||||
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):
|
||||
@@ -1109,7 +1111,6 @@ class ImageKVCacheManager(nn.Module):
|
||||
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]
|
||||
@@ -1121,22 +1122,19 @@ class ImageKVCacheManager(nn.Module):
|
||||
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 self.sp_size > 1 and uncond_cfg_prefill:
|
||||
joint_text_query, joint_text_key, joint_text_value = query, key, value
|
||||
query, key, value = query[:, :0], key[:, :0], value[:, :0]
|
||||
elif 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]
|
||||
joint_text_key = key[:, :joint_query_len]
|
||||
joint_text_value = value[:, :joint_query_len]
|
||||
query = query[:, joint_query_len:]
|
||||
key = key[:, local_prompt_len:]
|
||||
value = value[:, local_prompt_len:]
|
||||
key = key[:, joint_query_len:]
|
||||
value = value[:, joint_query_len:]
|
||||
|
||||
if joint_text_query is None:
|
||||
attn_metadata = AttentionMetadata(full_attn_spans=full_attn_spans)
|
||||
@@ -1254,6 +1252,8 @@ class ImageKVCacheManager(nn.Module):
|
||||
) -> torch.Tensor:
|
||||
self.image_token_len = kwargs.get("num_image_tokens")
|
||||
if self.attn.is_paged_kv_active():
|
||||
# A native connector fills the same Scheduler-owned pages as
|
||||
# local prefill. Imported prefixes need no dense reconstruction.
|
||||
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")
|
||||
@@ -1837,7 +1837,9 @@ class HunYuanAttention(nn.Module):
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
scaling=self.scaling,
|
||||
image_token_len=4097,
|
||||
prefix=f"{prefix}.image_attn",
|
||||
# Native Mooncake pairs this canonical KV layer name with the AR
|
||||
# HunYuanAttention: model.layers.N.self_attn.attn. Keep both in sync.
|
||||
prefix=f"model.{prefix}",
|
||||
)
|
||||
self.image_rope2d_emb = HunYuanRotary2DEmbedder(
|
||||
num_heads=self.num_heads,
|
||||
@@ -2964,7 +2966,10 @@ class HunyuanImage3Text2ImagePipeline(DiffusionPipeline):
|
||||
seq_lens=[prefill_seq_len],
|
||||
num_image_tokens=0,
|
||||
ar_kv_reuse_len=negative_reuse_len,
|
||||
full_attn_spans=model_kwargs["full_attn_spans"][batch_slice]
|
||||
full_attn_spans=[
|
||||
[(start, min(end, prefill_seq_len)) for start, end in spans if start < prefill_seq_len]
|
||||
for spans in model_kwargs["full_attn_spans"][batch_slice]
|
||||
]
|
||||
if model_kwargs.get("full_attn_spans")
|
||||
else None,
|
||||
)
|
||||
@@ -3019,6 +3024,28 @@ class HunyuanImage3Text2ImagePipeline(DiffusionPipeline):
|
||||
device,
|
||||
):
|
||||
ar_kv_data = model_kwargs.pop("ar_kv_data", None)
|
||||
computed_tokens = get_paged_kv_computed_tokens()
|
||||
if computed_tokens and computed_tokens[0] > 0:
|
||||
positive_reuse_len = computed_tokens[0]
|
||||
if (
|
||||
len(computed_tokens) > 1
|
||||
and computed_tokens[1] < positive_reuse_len
|
||||
and (not cfg_parallel_ready or cfg_rank == 1)
|
||||
):
|
||||
prefill_inputs = self._build_negative_cfg_prefill_inputs(
|
||||
input_ids,
|
||||
model_kwargs,
|
||||
batch_size,
|
||||
computed_tokens[1],
|
||||
positive_reuse_len,
|
||||
cfg_parallel_ready,
|
||||
)
|
||||
with (
|
||||
paged_kv_prefill(1, positive_reuse_len),
|
||||
torch.autocast(device_type=device.type, dtype=torch.bfloat16),
|
||||
):
|
||||
self.model.forward_call(**prefill_inputs)
|
||||
return self._truncate_reused_prefix(input_ids, model_kwargs, positive_reuse_len), positive_reuse_len
|
||||
if ar_kv_data is None:
|
||||
logger.debug(
|
||||
"[AR KV Reuse] cfg_rank=%s: no AR KV received, fallback to full recompute (reuse_len=0)",
|
||||
|
||||
@@ -367,6 +367,8 @@ class HunyuanImage3Pipeline(
|
||||
|
||||
def __init__(self, od_config: OmniDiffusionConfig) -> None:
|
||||
self.hf_config = get_config(od_config.model, trust_remote_code=True)
|
||||
self.hf_config.cfg_distilled = _config_flag(self.hf_config, "cfg_distilled")
|
||||
self.hf_config.use_meanflow = _config_flag(self.hf_config, "use_meanflow")
|
||||
super().__init__(self.hf_config)
|
||||
# update diffusion config
|
||||
self.generation_config = GenerationConfig.from_pretrained(od_config.model)
|
||||
@@ -1282,7 +1284,9 @@ class HunyuanImage3Pipeline(
|
||||
|
||||
# 4. Encode conditional images
|
||||
# Skip encoding if AR KV reuse is enabled
|
||||
has_ar_kv = kwargs.get("ar_kv_data")
|
||||
has_ar_kv = kwargs.get("ar_kv_data") or request_layout_utils.native_kv_covers_cond_images(
|
||||
output, kwargs.get("kv_computed_tokens", ())
|
||||
)
|
||||
if batch_cond_image_info is not None and len(batch_cond_image_info[0]) > 0 and not has_ar_kv:
|
||||
cond_vae_images, cond_timestep, cond_vit_images = self._encode_cond_image(
|
||||
batch_cond_image_info, cfg_factor[mode], generator=generator
|
||||
@@ -1894,6 +1898,7 @@ class HunyuanImage3Pipeline(
|
||||
pipe._guidance_scale = guidance_scale
|
||||
pipe._guidance_rescale = getattr(sampling, "guidance_rescale", 0.0)
|
||||
|
||||
ar_kv_kwargs = self._extract_ar_kv_from_sampling(sampling)
|
||||
model_kwargs = self.prepare_model_inputs(
|
||||
prompt=prompt,
|
||||
cot_text=cot_text,
|
||||
@@ -1906,8 +1911,10 @@ class HunyuanImage3Pipeline(
|
||||
batch_cond_image_info=batch_cond_image_info,
|
||||
bot_task=tokenizer_bot_task,
|
||||
prepared_layout=request_layout_utils.get_hunyuan_prepared_layout(state),
|
||||
kv_computed_tokens=state.extra.get("kv_computed_tokens", ()),
|
||||
**ar_kv_kwargs,
|
||||
)
|
||||
model_kwargs.update(self._extract_ar_kv_from_sampling(sampling))
|
||||
model_kwargs.update(ar_kv_kwargs)
|
||||
model_kwargs["use_cache"] = False
|
||||
|
||||
input_ids = model_kwargs.pop("input_ids")
|
||||
@@ -2337,6 +2344,7 @@ class HunyuanImage3Pipeline(
|
||||
batch_cond_image_info=batch_cond_image_info,
|
||||
bot_task=tokenizer_bot_task,
|
||||
prepared_layout=request_layout_utils.get_hunyuan_prepared_layout(req.requests[0]),
|
||||
kv_computed_tokens=getattr(req.requests[0], "kv_computed_tokens", ()),
|
||||
**ar_kv_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
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -373,7 +373,32 @@ def build_hunyuan_diffusion_kv_requests(
|
||||
real_pos = tokenizer_output.real_pos
|
||||
assert prefix_positions is not None and real_pos is not None
|
||||
|
||||
def boundary_rows(field_name: str) -> list[int | None]:
|
||||
positions = getattr(tokenizer_output, field_name)
|
||||
if positions is None:
|
||||
return [None] * cfg_factor
|
||||
if not isinstance(positions, list) or not positions:
|
||||
raise ValueError(f"Hunyuan {field_name} must be a non-empty boundary list")
|
||||
# Batched tokenizer output contains a separate boundary for each CFG
|
||||
# row. Never broadcast a conditional boundary into a negative row.
|
||||
if cfg_factor == 1 and len(positions) == 1 and (positions[0] is None or type(positions[0]) is int):
|
||||
return positions
|
||||
if len(positions) == cfg_factor and all(
|
||||
isinstance(row, list) and len(row) == 1 and (row[0] is None or type(row[0]) is int) for row in positions
|
||||
):
|
||||
return [row[0] for row in positions]
|
||||
raise ValueError(f"Hunyuan {field_name} must contain one boundary per CFG row")
|
||||
|
||||
think_boundaries = boundary_rows("think_recaption_end_pos")
|
||||
uncond_boundaries = boundary_rows("uncond_cfg_start_pos")
|
||||
|
||||
target_len = hunyuan_num_image_tokens(prepared_layout.generated_image_info)
|
||||
reusable_lens = [0] * cfg_factor
|
||||
if think_boundaries[0] is not None:
|
||||
reusable_lens[0] = think_boundaries[0] or 0
|
||||
if any(boundary is not None for boundary in uncond_boundaries):
|
||||
for sequence_id in range(1, cfg_factor):
|
||||
reusable_lens[sequence_id] = min(reusable_lens[0], uncond_boundaries[sequence_id] or 0)
|
||||
return tuple(
|
||||
DiffusionKVRequest(
|
||||
f"{request.request_id}/diffusion-kv/{sequence_id}",
|
||||
@@ -383,6 +408,7 @@ def build_hunyuan_diffusion_kv_requests(
|
||||
prefix_len=int(prefix_row[-1].item()),
|
||||
target_len=target_len,
|
||||
seq_len=int(valid_row[-1].item()),
|
||||
prompt_token_ids=tokenizer_output.tokens[sequence_id, : reusable_lens[sequence_id]].tolist(),
|
||||
# Prompt and reference-image tokens are already embedded in this
|
||||
# row's primary self-attention sequence. Hunyuan therefore has no
|
||||
# independently projected cross/joint-attention KV context.
|
||||
@@ -401,3 +427,14 @@ def get_hunyuan_prepared_layout(source: Any) -> HunyuanPreparedLayout | None:
|
||||
f"HunyuanImage3 expected prepared_layout to be HunyuanPreparedLayout, got {type(prepared_layout).__name__}"
|
||||
)
|
||||
return prepared_layout
|
||||
|
||||
|
||||
def native_kv_covers_cond_images(output: TokenizerEncodeOutput, computed_tokens: tuple[int, ...]) -> bool:
|
||||
"""Skip image encoding only when every CFG row already contains its image KV."""
|
||||
image_slices = output.joint_image_slices
|
||||
if not image_slices or len(image_slices) != len(computed_tokens):
|
||||
return False
|
||||
return all(
|
||||
slices and all(image_slice.stop <= computed for image_slice in slices)
|
||||
for slices, computed in zip(image_slices, computed_tokens, strict=True)
|
||||
)
|
||||
@@ -70,6 +70,8 @@ class OmniDiffusionRequest:
|
||||
# Opaque native vLLM connector parameters. The Orchestrator and Scheduler
|
||||
# transport this bag without interpreting local pages or block IDs.
|
||||
kv_transfer_params: dict[str, Any] | None = None
|
||||
# Worker-populated scheduler reuse boundaries, available before encoding.
|
||||
kv_computed_tokens: tuple[int, ...] = ()
|
||||
# A model preprocessor may keep selected requests on the legacy full-forward
|
||||
# path even when the engine is globally configured for step execution.
|
||||
use_step_execution: bool = True
|
||||
|
||||
@@ -11,10 +11,17 @@ from dataclasses import fields
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
from vllm.v1.outputs import KVConnectorOutput
|
||||
from vllm.v1.request import RequestStatus
|
||||
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
|
||||
from vllm_omni.diffusion.diffusion_kv.manager import DiffusionKVAdmissionError, DiffusionKVCacheManager
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import (
|
||||
KVTransferRegistrationError,
|
||||
commit_kv_load,
|
||||
prepare_kv_requests,
|
||||
)
|
||||
from vllm_omni.diffusion.diffusion_kv.manager import DiffusionKVCacheManager
|
||||
from vllm_omni.diffusion.diffusion_kv.metadata import DiffusionKVMetadata
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.diffusion.sched.interface import (
|
||||
@@ -58,6 +65,12 @@ class BaseScheduler(ABC):
|
||||
self._prefetch_enabled: bool = False
|
||||
self._diffusion_kv_manager: DiffusionKVCacheManager | None = None
|
||||
self._kv_connector = None
|
||||
self._kv_transfer_request_ids: set[str] = set()
|
||||
self._kv_finished_request_ids: set[str] = set()
|
||||
self._kv_loading_request_ids: set[str] = set()
|
||||
self._kv_request_generations: dict[str, int] = {}
|
||||
self._kv_draining_requests: dict[str, SchedulerRequestState] = {}
|
||||
self._kv_received_request_ids: set[str] = set()
|
||||
|
||||
def initialize(
|
||||
self,
|
||||
@@ -75,6 +88,7 @@ class BaseScheduler(ABC):
|
||||
self._running.clear()
|
||||
self._running_sampling_params_key = None
|
||||
self._finished_req_ids.clear()
|
||||
self._reset_kv_transfer_state()
|
||||
max_num_seqs = getattr(od_config, "max_num_seqs", 1)
|
||||
try:
|
||||
self.max_num_running_reqs = max(1, int(max_num_seqs))
|
||||
@@ -114,7 +128,7 @@ class BaseScheduler(ABC):
|
||||
self._diffusion_kv_manager = None
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import create_scheduler_kv_connector
|
||||
|
||||
self._kv_connector = create_scheduler_kv_connector(od_config)
|
||||
self._kv_connector = create_scheduler_kv_connector(od_config, kv_cache_config, kv_vllm_config)
|
||||
self._reset_scheduler_state()
|
||||
|
||||
@property
|
||||
@@ -127,7 +141,7 @@ class BaseScheduler(ABC):
|
||||
return self._add_request_with_request_id(request.request_id, request)
|
||||
|
||||
def _add_request_with_request_id(self, request_id: str, request: OmniDiffusionRequest) -> str:
|
||||
if request_id in self._request_states:
|
||||
if request_id in self._request_states or request_id in self._kv_draining_requests:
|
||||
raise ValueError(f"request_id {request_id!r} is already active.")
|
||||
state = self._make_request_state(request_id, request)
|
||||
state.queued_at = time.perf_counter()
|
||||
@@ -147,7 +161,10 @@ class BaseScheduler(ABC):
|
||||
scheduled_cached_request_ids.append(request_id)
|
||||
|
||||
# Second, schedule WAITING requests while capacity remains.
|
||||
while self._waiting and len(self._running) < self.max_num_running_reqs:
|
||||
deferred: list[str] = []
|
||||
waiting_budget = len(self._waiting)
|
||||
while self._waiting and waiting_budget and len(self._running) < self.max_num_running_reqs:
|
||||
waiting_budget -= 1
|
||||
request_id = self._waiting[0]
|
||||
state = self._request_states.get(request_id)
|
||||
if state is None:
|
||||
@@ -161,21 +178,24 @@ class BaseScheduler(ABC):
|
||||
if self._diffusion_kv_manager.has_request(request_id):
|
||||
diffusion_kv_metadata = self._diffusion_kv_manager.get_metadata(request_id)
|
||||
else:
|
||||
matched_tokens: list[int] = []
|
||||
if self._kv_connector is not None:
|
||||
for request in state.diffusion_kv_requests:
|
||||
num_tokens, _ = self._kv_connector.get_num_new_matched_tokens(request, 0)
|
||||
if num_tokens is None:
|
||||
break
|
||||
matched_tokens.append(num_tokens)
|
||||
if len(matched_tokens) != len(state.diffusion_kv_requests):
|
||||
deferred.append(self._waiting.popleft())
|
||||
continue
|
||||
try:
|
||||
allocation = self._diffusion_kv_manager.reserve_request(
|
||||
request_id,
|
||||
state.diffusion_kv_requests,
|
||||
)
|
||||
except Exception as exc:
|
||||
# schedule() runs outside the Engine execution-error
|
||||
# wrapper. Convert both expected admission failures and
|
||||
# unexpected native allocator failures into a terminal
|
||||
# request error so the busy loop can wake its stream.
|
||||
if not isinstance(exc, DiffusionKVAdmissionError):
|
||||
logger.exception(
|
||||
"Unexpected Diffusion KV allocation failure for request %s",
|
||||
request_id,
|
||||
)
|
||||
# Reservation rolls back on failure and has not yet
|
||||
# handed any destination pages to the connector.
|
||||
self._finish_requests(
|
||||
{request_id: DiffusionRequestStatus.FINISHED_ERROR},
|
||||
{request_id: str(exc)},
|
||||
@@ -184,6 +204,27 @@ class BaseScheduler(ABC):
|
||||
if allocation is None:
|
||||
break
|
||||
diffusion_kv_metadata = allocation
|
||||
self._kv_request_generations[request_id] = allocation.allocation_generation
|
||||
if self._kv_connector is not None:
|
||||
try:
|
||||
transfer_ids = commit_kv_load(
|
||||
self._kv_connector,
|
||||
self._diffusion_kv_manager.native_manager,
|
||||
state.diffusion_kv_requests,
|
||||
matched_tokens,
|
||||
)
|
||||
except KVTransferRegistrationError as exc:
|
||||
self._finish_requests(
|
||||
{request_id: DiffusionRequestStatus.FINISHED_ERROR}, {request_id: str(exc)}
|
||||
)
|
||||
continue
|
||||
# Other exceptions have no proven rollback. Keep pages
|
||||
# allocated until the Engine shuts down the Workers.
|
||||
self._kv_transfer_request_ids.update(transfer_ids)
|
||||
if transfer_ids:
|
||||
self._kv_loading_request_ids.add(request_id)
|
||||
for sequence, request in zip(allocation.sequences, state.diffusion_kv_requests, strict=True):
|
||||
sequence.num_computed_tokens = request.num_computed_tokens
|
||||
|
||||
self._waiting.popleft()
|
||||
was_new_request = state.status == DiffusionRequestStatus.WAITING
|
||||
@@ -202,6 +243,8 @@ class BaseScheduler(ABC):
|
||||
else:
|
||||
scheduled_cached_request_ids.append(request_id)
|
||||
|
||||
self._waiting.extendleft(reversed(deferred))
|
||||
|
||||
# Expose the next waiting request (serial mode) so the runner can
|
||||
# prefetch its KV during this forward. Skip a request without
|
||||
# kv_sender_info (would target the wrong sender under multi-replica) or
|
||||
@@ -226,11 +269,19 @@ class BaseScheduler(ABC):
|
||||
num_running_reqs=len(self._running),
|
||||
num_waiting_reqs=len(self._waiting),
|
||||
kv_prefetch_job=kv_prefetch_job,
|
||||
kv_finished_request_ids=set(self._kv_finished_request_ids),
|
||||
)
|
||||
if self._kv_connector is not None and (
|
||||
self._kv_transfer_request_ids or self._kv_finished_request_ids or self._kv_draining_requests
|
||||
):
|
||||
scheduler_output.kv_connector_metadata = self._kv_connector.build_connector_meta(scheduler_output)
|
||||
scheduler_output.kv_transfer_request_ids = self._kv_transfer_request_ids
|
||||
self._kv_transfer_request_ids = set()
|
||||
|
||||
# update after schedule
|
||||
self._step_id += 1
|
||||
self._finished_req_ids.clear()
|
||||
self._kv_finished_request_ids.clear()
|
||||
return scheduler_output
|
||||
|
||||
@abstractmethod
|
||||
@@ -238,7 +289,53 @@ class BaseScheduler(ABC):
|
||||
pass
|
||||
|
||||
def has_requests(self) -> bool:
|
||||
return bool(self._waiting or self._running)
|
||||
return bool(
|
||||
self._waiting
|
||||
or self._running
|
||||
or self._kv_transfer_request_ids
|
||||
or self._kv_finished_request_ids
|
||||
or self._kv_draining_requests
|
||||
)
|
||||
|
||||
def update_kv_connector_output(self, output: KVConnectorOutput | None) -> None:
|
||||
if output is None:
|
||||
return
|
||||
assert self._kv_connector is not None
|
||||
self._kv_connector.update_connector_output(output)
|
||||
finished_ids = output.finished_recving or set()
|
||||
if not finished_ids:
|
||||
return
|
||||
for request_id in tuple(self._kv_loading_request_ids):
|
||||
state = self._request_states.get(request_id) or self._kv_draining_requests.get(request_id)
|
||||
if state is None:
|
||||
continue
|
||||
internal_ids = {request.request_id for request in state.diffusion_kv_requests}
|
||||
self._kv_received_request_ids.update(internal_ids.intersection(finished_ids))
|
||||
if internal_ids and internal_ids.issubset(self._kv_received_request_ids):
|
||||
self._kv_loading_request_ids.discard(request_id)
|
||||
self._kv_received_request_ids.difference_update(internal_ids)
|
||||
|
||||
def fail_incomplete_kv_loads(self, transfer_ids: set[str]) -> set[str]:
|
||||
statuses = {}
|
||||
for request_id in self._kv_loading_request_ids:
|
||||
state = self._request_states.get(request_id)
|
||||
if state is not None and any(req.request_id in transfer_ids for req in state.diffusion_kv_requests):
|
||||
statuses[request_id] = DiffusionRequestStatus.FINISHED_ERROR
|
||||
return self._finish_requests(statuses, {rid: "Timed out receiving diffusion KV" for rid in statuses})
|
||||
|
||||
def completed_kv_drains(self) -> set[str]:
|
||||
return self._kv_draining_requests.keys() - self._kv_loading_request_ids
|
||||
|
||||
def release_kv_drains(self, request_ids: set[str]) -> None:
|
||||
"""Called only after all ranks completed and Worker row cleanup succeeded."""
|
||||
for request_id in request_ids:
|
||||
if request_id in self._kv_loading_request_ids:
|
||||
raise RuntimeError("Cannot release diffusion pages before KV receive completes")
|
||||
state = self._kv_draining_requests[request_id]
|
||||
self._release_request_kv(state, state.status)
|
||||
del self._kv_draining_requests[request_id]
|
||||
if request_id not in self._request_states:
|
||||
self._kv_request_generations.pop(request_id, None)
|
||||
|
||||
def num_waiting_requests(self) -> int:
|
||||
return len(self._waiting)
|
||||
@@ -270,13 +367,27 @@ class BaseScheduler(ABC):
|
||||
def get_request_state(self, request_id: str) -> SchedulerRequestState | None:
|
||||
return self._request_states.get(request_id)
|
||||
|
||||
def get_diffusion_kv_cleanup_targets(self, request_ids: list[str]) -> list[str | tuple[str, int]]:
|
||||
targets: list[str | tuple[str, int]] = []
|
||||
for request_id in request_ids:
|
||||
if request_id in self._kv_loading_request_ids:
|
||||
continue
|
||||
generation = self._kv_request_generations.get(request_id)
|
||||
if generation is not None:
|
||||
targets.append((request_id, generation))
|
||||
else:
|
||||
targets.append(request_id)
|
||||
return targets
|
||||
|
||||
def pop_request_state(self, request_id: str) -> SchedulerRequestState | None:
|
||||
if self._diffusion_kv_manager is not None:
|
||||
self._diffusion_kv_manager.free_request(request_id)
|
||||
self._pop_extra_request_state(request_id)
|
||||
if request_id not in self._kv_draining_requests:
|
||||
self._kv_request_generations.pop(request_id, None)
|
||||
return self._request_states.pop(request_id, None)
|
||||
|
||||
def preempt_request(self, request_id: str) -> bool:
|
||||
if request_id in self._kv_loading_request_ids:
|
||||
return False
|
||||
if request_id not in self._request_states:
|
||||
return False
|
||||
if request_id in self._running:
|
||||
@@ -295,13 +406,7 @@ class BaseScheduler(ABC):
|
||||
self._finish_requests({request_id: status for request_id in request_ids})
|
||||
|
||||
def close(self) -> None:
|
||||
if self._diffusion_kv_manager is not None:
|
||||
self._diffusion_kv_manager.close()
|
||||
self._diffusion_kv_manager = None
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import shutdown_kv_connector
|
||||
|
||||
shutdown_kv_connector(scheduler_connector=self._kv_connector)
|
||||
self._kv_connector = None
|
||||
self._shutdown_diffusion_kv()
|
||||
self._request_states.clear()
|
||||
self._waiting.clear()
|
||||
self._running.clear()
|
||||
@@ -309,6 +414,26 @@ class BaseScheduler(ABC):
|
||||
self._finished_req_ids.clear()
|
||||
self._reset_scheduler_state()
|
||||
|
||||
def _reset_kv_transfer_state(self) -> None:
|
||||
"""Clear request bookkeeping for native KV transfers."""
|
||||
self._kv_transfer_request_ids.clear()
|
||||
self._kv_finished_request_ids.clear()
|
||||
self._kv_loading_request_ids.clear()
|
||||
self._kv_draining_requests.clear()
|
||||
self._kv_received_request_ids.clear()
|
||||
self._kv_request_generations.clear()
|
||||
|
||||
def _shutdown_diffusion_kv(self) -> None:
|
||||
"""Stop transfers before releasing their cache and request state."""
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import shutdown_kv_connector
|
||||
|
||||
shutdown_kv_connector(scheduler_connector=self._kv_connector)
|
||||
self._kv_connector = None
|
||||
if self._diffusion_kv_manager is not None:
|
||||
self._diffusion_kv_manager.close()
|
||||
self._diffusion_kv_manager = None
|
||||
self._reset_kv_transfer_state()
|
||||
|
||||
def _finish_requests(
|
||||
self,
|
||||
statuses: dict[str, DiffusionRequestStatus],
|
||||
@@ -326,7 +451,6 @@ class BaseScheduler(ABC):
|
||||
state = self._request_states.get(request_id)
|
||||
if state is None or state.is_finished():
|
||||
continue
|
||||
|
||||
finished_req_ids.add(request_id)
|
||||
if request_id in self._running:
|
||||
running_to_remove.add(request_id)
|
||||
@@ -341,10 +465,12 @@ class BaseScheduler(ABC):
|
||||
self._waiting = deque(request_id for request_id in self._waiting if request_id not in waiting_to_remove)
|
||||
|
||||
for request_id in finished_req_ids:
|
||||
if self._diffusion_kv_manager is not None:
|
||||
self._diffusion_kv_manager.free_request(request_id)
|
||||
state = self._request_states[request_id]
|
||||
status = statuses[request_id]
|
||||
if request_id in self._kv_loading_request_ids:
|
||||
self._kv_draining_requests[request_id] = state
|
||||
else:
|
||||
self._release_request_kv(state, status)
|
||||
state.status = status
|
||||
if status == DiffusionRequestStatus.FINISHED_ERROR:
|
||||
state.error = None if errors is None else errors.get(request_id)
|
||||
@@ -354,6 +480,27 @@ class BaseScheduler(ABC):
|
||||
self._finished_req_ids |= finished_req_ids
|
||||
return finished_req_ids
|
||||
|
||||
def _release_request_kv(self, state: SchedulerRequestState, status: DiffusionRequestStatus) -> None:
|
||||
request_id = state.request_id
|
||||
if self._kv_connector is not None:
|
||||
for request in state.diffusion_kv_requests:
|
||||
request.status = (
|
||||
RequestStatus.FINISHED_ABORTED
|
||||
if status == DiffusionRequestStatus.FINISHED_ABORTED
|
||||
else RequestStatus.FINISHED_STOPPED
|
||||
)
|
||||
if request.kv_transfer_params:
|
||||
self._kv_finished_request_ids.add(request.request_id)
|
||||
block_ids = []
|
||||
if self._diffusion_kv_manager.has_request(request_id):
|
||||
blocks = self._diffusion_kv_manager.native_manager.get_blocks(request.request_id)
|
||||
block_ids = blocks.get_block_ids()[0]
|
||||
delay_free, _ = self._kv_connector.request_finished(request, block_ids)
|
||||
if delay_free:
|
||||
raise RuntimeError("Diffusion consumer cannot release pages while KV transfer is active")
|
||||
if self._diffusion_kv_manager is not None:
|
||||
self._diffusion_kv_manager.free_request(request_id)
|
||||
|
||||
def _finalize_update_from_output(
|
||||
self,
|
||||
sched_output: DiffusionSchedulerOutput,
|
||||
@@ -391,13 +538,9 @@ class BaseScheduler(ABC):
|
||||
elif kv_requests:
|
||||
raise ValueError("dense_legacy request unexpectedly contains Scheduler Diffusion KV requests")
|
||||
|
||||
# Keep the request-scoped native connector bag on each Scheduler-owned
|
||||
# sequence without interpreting its contents or creating page state.
|
||||
kv_transfer_params = getattr(request, "kv_transfer_params", None)
|
||||
if kv_transfer_params is not None:
|
||||
for kv_request in kv_requests:
|
||||
if kv_request.kv_transfer_params is None:
|
||||
kv_request.kv_transfer_params = dict(kv_transfer_params)
|
||||
prepare_kv_requests(kv_requests, kv_transfer_params)
|
||||
|
||||
# DiffusionKVRequest objects are mutable Scheduler/native-KVCacheManager
|
||||
# state and must never ride the normal request payload to a Worker.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
@@ -254,9 +254,10 @@ class DiffusionSchedulerOutput:
|
||||
num_waiting_reqs: int
|
||||
# next request to background-prefetch KV
|
||||
kv_prefetch_job: KVPrefetchJob | None = None
|
||||
# Opaque metadata emitted by a future Scheduler-role connector. PR0 keeps
|
||||
# the input port but does not build or consume it.
|
||||
kv_connector_metadata: KVConnectorMetadata | None = None
|
||||
kv_transfer_request_ids: set[str] = field(default_factory=set)
|
||||
# Connector lifecycle uses per-sequence IDs, not public request IDs.
|
||||
kv_finished_request_ids: set[str] = field(default_factory=set)
|
||||
|
||||
@cached_property
|
||||
def scheduled_request_ids(self) -> list[str]:
|
||||
@@ -274,4 +275,4 @@ class DiffusionSchedulerOutput:
|
||||
|
||||
@property
|
||||
def is_empty(self) -> bool:
|
||||
return self.num_scheduled_reqs == 0
|
||||
return self.num_scheduled_reqs == 0 and self.kv_connector_metadata is 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
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -100,7 +100,7 @@ class RequestScheduler(BaseScheduler):
|
||||
|
||||
def update_from_output(self, sched_output: DiffusionSchedulerOutput, output: RunnerOutput) -> set[str]:
|
||||
scheduled_request_ids = sched_output.scheduled_request_ids
|
||||
if not scheduled_request_ids:
|
||||
if not scheduled_request_ids and not sched_output.finished_req_ids:
|
||||
return set()
|
||||
|
||||
terminal_statuses: dict[str, DiffusionRequestStatus] = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,7 +64,7 @@ class StepScheduler(BaseScheduler):
|
||||
|
||||
def update_from_output(self, sched_output: DiffusionSchedulerOutput, output: RunnerOutput) -> set[str]:
|
||||
scheduled_request_ids = sched_output.scheduled_request_ids
|
||||
if not scheduled_request_ids:
|
||||
if not scheduled_request_ids and not sched_output.finished_req_ids:
|
||||
return set()
|
||||
|
||||
terminal_statuses: dict[str, DiffusionRequestStatus] = {}
|
||||
|
||||
@@ -151,6 +151,9 @@ class _DiffusionVllmModelConfig:
|
||||
def get_head_size(self) -> int:
|
||||
return self._require_attention_geometry()[2]
|
||||
|
||||
def get_total_num_kv_heads(self) -> int:
|
||||
return self.hf_text_config.num_key_value_heads
|
||||
|
||||
@property
|
||||
def is_diffusion(self) -> bool:
|
||||
# This config is also installed as the current VllmConfig for the
|
||||
@@ -224,7 +227,10 @@ def configure_diffusion_vllm_config(vllm_config: VllmConfig, od_config: OmniDiff
|
||||
vllm_config.cache_config.kv_cache_memory_bytes = getattr(od_config, "kv_cache_memory_bytes", None)
|
||||
# Prefix hashes/publication are intentionally deferred to the next PR.
|
||||
vllm_config.cache_config.enable_prefix_caching = False
|
||||
vllm_config.scheduler_config.max_num_seqs = int(od_config.max_num_seqs)
|
||||
max_rows_per_request = getattr(od_config, "diffusion_kv_max_rows_per_request", None)
|
||||
if type(max_rows_per_request) is not int or max_rows_per_request <= 0:
|
||||
raise ValueError("paged_scheduler requires a positive diffusion_kv_max_rows_per_request")
|
||||
vllm_config.scheduler_config.max_num_seqs = int(od_config.max_num_seqs) * max_rows_per_request
|
||||
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)
|
||||
|
||||
@@ -193,14 +193,20 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
||||
self.state_cache: dict[str, StepRequestState] = {}
|
||||
|
||||
# Initialize KV cache manager for connector management.
|
||||
self.kv_transfer_manager = OmniKVTransferManager.from_od_config(od_config)
|
||||
self.kv_transfer_manager = (
|
||||
OmniKVTransferManager.from_od_config(od_config)
|
||||
if getattr(od_config, "kv_transfer_config", None) is None
|
||||
else None
|
||||
)
|
||||
self._kv_connector = None
|
||||
|
||||
# Prefetch covers TP / SP / CFG-Parallel / HSDP. Disabled when a CFG
|
||||
# companion KV collector is set (that KV is not backgrounded).
|
||||
has_cfg_companion_kv = getattr(od_config, "cfg_kv_collect_func", None) is not None
|
||||
|
||||
self._kv_prefetch_enabled = (
|
||||
bool(self.kv_transfer_manager.config.enable_kv_async_prefetch)
|
||||
self.kv_transfer_manager is not None
|
||||
and bool(self.kv_transfer_manager.config.enable_kv_async_prefetch)
|
||||
and not has_cfg_companion_kv
|
||||
and self.kv_transfer_manager.config.need_recv_cache
|
||||
)
|
||||
@@ -471,6 +477,16 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
||||
|
||||
self.diffusion_kv_backend.initialize_kv_cache(kv_cache_config)
|
||||
self.kv_cache_config = self.diffusion_kv_backend.kv_cache_config
|
||||
if getattr(self.od_config, "kv_transfer_config", None) is not None:
|
||||
from vllm.v1.worker.gpu.kv_connector import ActiveKVConnector
|
||||
|
||||
self._kv_connector = ActiveKVConnector(self.vllm_config, self.diffusion_kv_backend.kv_caches_by_layer)
|
||||
|
||||
def prepare_kv_for_forward(self, scheduler_output: DiffusionSchedulerOutput):
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import wait_for_kv_load
|
||||
|
||||
timeout = self.od_config.kv_transfer_config.kv_connector_extra_config.get("transfer_timeout", 60.0)
|
||||
return wait_for_kv_load(self._kv_connector, scheduler_output, timeout)
|
||||
|
||||
def install_diffusion_kv_metadata(self, metadata: DiffusionKVMetadata) -> bool:
|
||||
return self.diffusion_kv_backend.install_diffusion_kv_metadata(metadata)
|
||||
@@ -483,7 +499,7 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
||||
) -> int:
|
||||
return self.diffusion_kv_backend.get_diffusion_kv_row(request_id, sequence_id, context_id)
|
||||
|
||||
def remove_diffusion_kv_requests(self, request_ids: list[str]) -> int:
|
||||
def remove_diffusion_kv_requests(self, request_ids: list[str | tuple[str, int]]) -> int:
|
||||
return self.diffusion_kv_backend.remove_diffusion_kv_requests(request_ids)
|
||||
|
||||
def refresh_diffusion_kv_block_table_layout(self) -> None:
|
||||
@@ -520,8 +536,9 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
||||
DiffusionPagedAttentionRow(
|
||||
request_id=request_metadata.request_id,
|
||||
sequence_id=sequence.sequence_id,
|
||||
query_len=sequence.seq_len,
|
||||
query_len=sequence.seq_len - sequence.num_computed_tokens,
|
||||
seq_len=sequence.seq_len,
|
||||
kv_start_pos=sequence.num_computed_tokens,
|
||||
)
|
||||
)
|
||||
denoise_rows.append(
|
||||
@@ -606,6 +623,9 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
||||
kv_prefetch_job: KVPrefetchJob | None = None,
|
||||
use_prefetch: bool = False,
|
||||
) -> None:
|
||||
if self.kv_transfer_manager is None:
|
||||
self._initialize_generator(req.sampling_params)
|
||||
return
|
||||
# Receive AR KV. Single-request execution can use the prefetch path:
|
||||
# consume prior-forward payload, sync-fallback on miss; request-batch
|
||||
# execution keeps the synchronous per-request receive path.
|
||||
@@ -770,6 +790,8 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
||||
"Diffusion KV metadata count must match the request batch: "
|
||||
f"metadata={len(diffusion_kv_metadata)}, requests={len(reqs)}"
|
||||
)
|
||||
for req, metadata in zip(reqs, diffusion_kv_metadata, strict=True):
|
||||
req.kv_computed_tokens = tuple(seq.num_computed_tokens for seq in metadata.sequences)
|
||||
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
|
||||
@@ -1018,13 +1040,18 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
||||
kv_sender_info=sched_new_req.req.kv_sender_info,
|
||||
prepared_layout=getattr(sched_new_req.req, "prepared_layout", None),
|
||||
)
|
||||
if sched_new_req.diffusion_kv_metadata is not None:
|
||||
new_state.extra["kv_computed_tokens"] = tuple(
|
||||
seq.num_computed_tokens for seq in sched_new_req.diffusion_kv_metadata.sequences
|
||||
)
|
||||
state_req = copy.copy(sched_new_req.req)
|
||||
state_req.sampling_params = new_state.sampling
|
||||
self.kv_transfer_manager.receive_multi_kv_cache_distributed(
|
||||
state_req,
|
||||
cfg_kv_collect_func=getattr(self.od_config, "cfg_kv_collect_func", None),
|
||||
target_device=self._target_device,
|
||||
)
|
||||
if self.kv_transfer_manager is not None:
|
||||
self.kv_transfer_manager.receive_multi_kv_cache_distributed(
|
||||
state_req,
|
||||
cfg_kv_collect_func=getattr(self.od_config, "cfg_kv_collect_func", None),
|
||||
target_device=self._target_device,
|
||||
)
|
||||
self.state_cache[request_id] = new_state
|
||||
resolved.append(new_state)
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ from vllm_omni.diffusion.distributed.parallel_state import (
|
||||
get_hsdp_replicate_group,
|
||||
get_pp_group,
|
||||
get_sp_group,
|
||||
get_world_group,
|
||||
init_distributed_environment,
|
||||
initialize_model_parallel,
|
||||
model_parallel_is_initialized,
|
||||
@@ -116,7 +117,11 @@ def _all_gather_rank_values(value: Any) -> list[Any]:
|
||||
if not dist.is_available() or not dist.is_initialized():
|
||||
return [value]
|
||||
values: list[Any] = [None] * dist.get_world_size()
|
||||
dist.all_gather_object(values, value)
|
||||
# Object collectives are control-plane traffic. Using the default NCCL
|
||||
# group serializes them through temporary CUDA tensors, adding pointless
|
||||
# H2D/DtoH copies to every rank-wide status check. Reuse the world
|
||||
# coordinator's Gloo group, as vLLM does for CPU metadata collectives.
|
||||
dist.all_gather_object(values, value, group=get_world_group().cpu_group)
|
||||
return values
|
||||
|
||||
|
||||
@@ -446,15 +451,13 @@ class DiffusionWorker:
|
||||
raise RuntimeError("Diffusion KV memory snapshot was not captured before model loading")
|
||||
override = self.vllm_config.cache_config.kv_cache_memory_bytes
|
||||
if override:
|
||||
# Match native vLLM: an explicit cache budget skips automatic
|
||||
# capacity derivation, but still runs the maximum-shape model
|
||||
# request so lazy kernels and communication buffers initialize.
|
||||
self.model_runner.profile_run(profile_requests)
|
||||
# Post-allocation warmup avoids retaining a second activation arena.
|
||||
logger.info(
|
||||
"Worker %d: Initial free memory %s GiB, reserved %s GiB memory for "
|
||||
"Diffusion KV Cache as specified by kv_cache_memory_bytes config and "
|
||||
"skipped automatic memory profiling. This does not respect the "
|
||||
"gpu_memory_utilization config. A profile warmup was still executed.",
|
||||
"gpu_memory_utilization config. Model kernels will be initialized by "
|
||||
"the post-allocation startup warmup.",
|
||||
self.rank,
|
||||
format_gib(self.init_snapshot.free_memory),
|
||||
format_gib(int(override)),
|
||||
@@ -467,6 +470,8 @@ class DiffusionWorker:
|
||||
) as profile_result:
|
||||
self.model_runner.profile_run(profile_requests)
|
||||
|
||||
current_omni_platform.empty_cache()
|
||||
|
||||
available_memory = self.requested_memory - profile_result.non_kv_cache_memory
|
||||
if available_memory <= 0:
|
||||
raise RuntimeError(
|
||||
@@ -534,12 +539,18 @@ class DiffusionWorker:
|
||||
with self._maybe_get_memory_pool_context("kv_cache"):
|
||||
self.model_runner.set_kv_cache_config(kv_cache_config)
|
||||
|
||||
def remove_diffusion_kv_requests(self, request_ids: list[str]) -> int:
|
||||
def remove_diffusion_kv_requests(self, request_ids: list[str | tuple[str, int]]) -> int:
|
||||
"""Clear Worker-local rows without freeing Scheduler-owned blocks."""
|
||||
|
||||
assert self.model_runner is not None, "Model runner not initialized"
|
||||
return self.model_runner.remove_diffusion_kv_requests(request_ids)
|
||||
|
||||
def prepare_kv_for_forward(self, scheduler_output: DiffusionSchedulerOutput):
|
||||
return _run_and_gather_rank_values(
|
||||
"Diffusion KV receive",
|
||||
lambda: self.model_runner.prepare_kv_for_forward(scheduler_output),
|
||||
)
|
||||
|
||||
def init_lora_manager(self) -> None:
|
||||
"""Initialize the LoRA manager for this worker."""
|
||||
pipeline = self.model_runner.pipeline
|
||||
@@ -790,6 +801,15 @@ class DiffusionWorker:
|
||||
Args:
|
||||
level: Sleep level. Level 1 offloads weights, level 2 also saves buffers.
|
||||
"""
|
||||
# The config validator rejects sleep for the native paged path. Keep
|
||||
# this worker-side guard precise as well: test doubles and legacy
|
||||
# configs may expose arbitrary attributes through Mock/getattr.
|
||||
if (
|
||||
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
|
||||
is DiffusionKVCacheMode.PAGED_SCHEDULER
|
||||
and getattr(self.od_config, "kv_transfer_config", None) is not None
|
||||
):
|
||||
raise ValueError("Cannot sleep while native KV connector memory is registered")
|
||||
CuMemAllocator = _get_cumem_allocator_class()
|
||||
allocator = CuMemAllocator.get_instance()
|
||||
|
||||
@@ -1254,7 +1274,11 @@ class WorkerProc:
|
||||
|
||||
world_size = torch.distributed.get_world_size()
|
||||
statuses: list[dict[str, Any] | None] = [None] * world_size
|
||||
torch.distributed.all_gather_object(statuses, status)
|
||||
torch.distributed.all_gather_object(
|
||||
statuses,
|
||||
status,
|
||||
group=get_world_group().cpu_group,
|
||||
)
|
||||
missing_ranks = [rank for rank, rank_status in enumerate(statuses) if rank_status is None]
|
||||
if missing_ranks:
|
||||
logger.warning("RPC rank status gather returned missing entries for ranks: %s", missing_ranks)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Engine runtime error types."""
|
||||
|
||||
|
||||
class NativeKVHandoffError(RuntimeError):
|
||||
"""One request lost its producer binding or transfer metadata."""
|
||||
@@ -38,6 +38,7 @@ from vllm_omni.diffusion.data import is_diffusion_request_started_output
|
||||
from vllm_omni.distributed.omni_connectors.utils.config import stage_receives_chunks
|
||||
from vllm_omni.engine import OmniEngineCoreRequest
|
||||
from vllm_omni.engine.cfg_companion_tracker import CfgCompanionTracker
|
||||
from vllm_omni.engine.errors import NativeKVHandoffError
|
||||
from vllm_omni.engine.membership_controller import MembershipController
|
||||
from vllm_omni.engine.messages import (
|
||||
AbortRequestMessage,
|
||||
@@ -211,6 +212,7 @@ class OrchestratorRequestState:
|
||||
session_owned: bool = False
|
||||
running_counter_registered: bool = False
|
||||
request_artifact_dirs: set[str] = field(default_factory=set)
|
||||
native_kv_transfer_id: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -1363,6 +1365,16 @@ class OrchestratorBase:
|
||||
try:
|
||||
await dispatch()
|
||||
return True
|
||||
except NativeKVHandoffError as e:
|
||||
await self._fail_request_client_error(
|
||||
req_id,
|
||||
stage_id,
|
||||
str(e),
|
||||
status_code=HTTPStatus.BAD_GATEWAY.value,
|
||||
error_type="NativeKVHandoffError",
|
||||
release_owners=True,
|
||||
)
|
||||
return False
|
||||
except StageUnavailableError as e:
|
||||
# No specific replica to evict: the stage already has no live
|
||||
# replica or the chosen slot was evicted. Fail just this request.
|
||||
@@ -2193,12 +2205,13 @@ class OrchestratorBase:
|
||||
req_id,
|
||||
req_state,
|
||||
diffusion_prompt,
|
||||
submit_kwargs={
|
||||
"kv_sender_info": self._build_kv_sender_info(
|
||||
list(getattr(next_client, "engine_input_source", None) or [src_stage_id]),
|
||||
request_id=req_id,
|
||||
)
|
||||
},
|
||||
submit_kwargs=self._diffusion_submit_kwargs(
|
||||
req_id,
|
||||
src_stage_id,
|
||||
next_client,
|
||||
req_state,
|
||||
output,
|
||||
),
|
||||
params_override=self._maybe_clone_diffusion_params_for_cfg(req_id, params),
|
||||
)
|
||||
self._on_stage_submitted(
|
||||
@@ -2452,12 +2465,12 @@ class OrchestratorBase:
|
||||
_t_submit_start = _time.perf_counter()
|
||||
|
||||
if next_pool.stage_type == "diffusion":
|
||||
submit_kwargs = {
|
||||
"kv_sender_info": self._build_kv_sender_info(
|
||||
list(getattr(next_pool.stage_client, "engine_input_source", None) or [next_stage_id - 1]),
|
||||
request_id=request_id,
|
||||
)
|
||||
}
|
||||
submit_kwargs = self._diffusion_submit_kwargs(
|
||||
request_id,
|
||||
next_stage_id - 1,
|
||||
next_pool.stage_client,
|
||||
req_state,
|
||||
)
|
||||
submitted = await self._dispatch_or_fail_request(
|
||||
lambda: next_pool.submit_initial(
|
||||
request_id,
|
||||
@@ -2554,6 +2567,67 @@ class OrchestratorBase:
|
||||
|
||||
return True
|
||||
|
||||
def _maybe_attach_native_kv_transfer_params(
|
||||
self,
|
||||
req_state: OrchestratorRequestState,
|
||||
prompt: Any,
|
||||
) -> None:
|
||||
source_stage_id = 0
|
||||
if source_stage_id + 1 > req_state.final_stage_id:
|
||||
return
|
||||
config = getattr(self.stage_pools[source_stage_id].stage_vllm_config, "kv_transfer_config", None)
|
||||
if getattr(config, "kv_role", None) != "kv_producer":
|
||||
return
|
||||
if self.stage_pools[source_stage_id + 1].stage_type != "diffusion":
|
||||
return
|
||||
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import build_source_kv_transfer_params, mint_transfer_id
|
||||
|
||||
transfer_id = mint_transfer_id(req_state.request_id)
|
||||
req_state.native_kv_transfer_id = transfer_id
|
||||
params = build_source_kv_transfer_params(
|
||||
transfer_id=transfer_id,
|
||||
remote_engine_id=None,
|
||||
remote_bootstrap_addr=None,
|
||||
)
|
||||
for sampling in (req_state.sampling_params_list[source_stage_id], prompt.sampling_params):
|
||||
sampling.extra_args = {**(sampling.extra_args or {}), "kv_transfer_params": params}
|
||||
|
||||
def _diffusion_submit_kwargs(
|
||||
self,
|
||||
request_id: str,
|
||||
source_stage_id: int,
|
||||
next_client: Any,
|
||||
req_state: OrchestratorRequestState,
|
||||
output: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
source_stage_ids = list(getattr(next_client, "engine_input_source", None) or [source_stage_id])
|
||||
if req_state.native_kv_transfer_id is None:
|
||||
return {"kv_sender_info": self._build_kv_sender_info(source_stage_ids, request_id=request_id)}
|
||||
if output is None:
|
||||
raise NativeKVHandoffError("Native KV handoff requires one completed AR source")
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import (
|
||||
bootstrap_addr_from_kv_transfer_config,
|
||||
build_target_kv_transfer_params,
|
||||
)
|
||||
|
||||
source = self.stage_pools[source_stage_id].get_bound_client(request_id)
|
||||
config = getattr(getattr(source, "vllm_config", None), "kv_transfer_config", None)
|
||||
if source is None or config is None:
|
||||
raise NativeKVHandoffError(
|
||||
f"Native KV handoff for {request_id}: bound AR replica or its KV configuration is unavailable"
|
||||
)
|
||||
params = getattr(output, "kv_transfer_params", None)
|
||||
if not params or "num_transfer_tokens" not in params:
|
||||
raise NativeKVHandoffError("AR source completed without native KV transfer metadata")
|
||||
return {
|
||||
"kv_transfer_params": build_target_kv_transfer_params(
|
||||
source_params=params,
|
||||
remote_engine_id=config.engine_id,
|
||||
remote_bootstrap_addr=bootstrap_addr_from_kv_transfer_config(config),
|
||||
)
|
||||
}
|
||||
|
||||
def _build_kv_sender_info(
|
||||
self,
|
||||
sender_stage_ids: list[int],
|
||||
@@ -2677,6 +2751,7 @@ class Orchestrator(OrchestratorBase):
|
||||
request_artifact_dirs=set(msg.request_artifact_dirs or ()),
|
||||
)
|
||||
self.request_states[request_id] = req_state
|
||||
self._maybe_attach_native_kv_transfer_params(req_state, prompt)
|
||||
self._register_running_request(req_state)
|
||||
req_state.streaming.enabled = bool(getattr(prompt, "resumable", False))
|
||||
req_state.stage_submit_ts[stage_id] = _time.time()
|
||||
|
||||
@@ -1189,6 +1189,8 @@ def _project_omni_stage_engine_args(
|
||||
engine_args["async_chunk"] = connector_config.async_chunk
|
||||
if connector_config.omni_kv_config is not None:
|
||||
engine_args["omni_kv_config"] = copy.deepcopy(connector_config.omni_kv_config)
|
||||
if connector_config.kv_transfer_config is not None:
|
||||
engine_args["kv_transfer_config"] = copy.deepcopy(connector_config.kv_transfer_config)
|
||||
|
||||
if is_diffusion:
|
||||
engine_args["parallel_config"] = _project_omni_config_fields(
|
||||
@@ -1980,6 +1982,11 @@ def build_diffusion_config(
|
||||
)
|
||||
od_config = OmniDiffusionConfig(**{name: value for name, value in diffusion_kwargs.items() if value is not None})
|
||||
|
||||
for dimension in ("height", "width"):
|
||||
value = getattr(metadata.default_sampling_params, dimension, None)
|
||||
if isinstance(value, int) and value > 0:
|
||||
od_config.additional_config.setdefault(f"diffusion_kv_profile_{dimension}", value)
|
||||
|
||||
num_devices_per_stage = od_config.parallel_config.world_size
|
||||
device_control_env = current_omni_platform.device_control_env_var
|
||||
visible_devices_str = os.environ.get(device_control_env) if device_control_env else None
|
||||
|
||||
@@ -19,7 +19,7 @@ from typing import Any, TypedDict, cast
|
||||
import janus
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.config.omni_config import BaseVllmOmniStageConfig
|
||||
from vllm_omni.config.omni_config import BaseVllmOmniStageConfig, VllmOmniDiffusionStageConfig
|
||||
from vllm_omni.distributed.omni_connectors.utils.initialization import (
|
||||
resolve_omni_kv_config_for_stage,
|
||||
)
|
||||
@@ -47,6 +47,7 @@ from vllm_omni.engine.stage_engine_startup import (
|
||||
from vllm_omni.engine.stage_init_utils import (
|
||||
LogicalStageInitPlan,
|
||||
ReplicaInitPlan,
|
||||
StageMetadata,
|
||||
_inject_inferred_kv_tp_topology,
|
||||
acquire_device_locks,
|
||||
build_engine_args_dict,
|
||||
@@ -537,6 +538,7 @@ class StageRuntime:
|
||||
replicas_per_stage,
|
||||
replica_devices_map,
|
||||
)
|
||||
self._validate_native_kv_topology(stage_plans)
|
||||
return stage_plans
|
||||
|
||||
def _finalize_initialized_stages(
|
||||
@@ -585,6 +587,113 @@ class StageRuntime:
|
||||
|
||||
# ---- Internal methods ----
|
||||
|
||||
def _validate_native_kv_topology(self, plans: list[LogicalStageInitPlan]) -> None:
|
||||
"""Reject unsupported native AR->DiT graphs before launching any Worker."""
|
||||
if not any(plan.replicas[0].metadata.stage_type == "diffusion" for plan in plans):
|
||||
return # Native LLM prefill/decode is outside this path.
|
||||
roles = {}
|
||||
for plan in plans:
|
||||
replica = plan.replicas[0]
|
||||
if replica.stage_vllm_config is not None:
|
||||
config = getattr(replica.stage_vllm_config, "kv_transfer_config", None)
|
||||
elif isinstance(replica.stage_cfg, VllmOmniDiffusionStageConfig):
|
||||
config = (
|
||||
replica.stage_cfg.connector_config.kv_transfer_config
|
||||
or replica.stage_cfg.diffusion_config.kv_transfer_config
|
||||
)
|
||||
else:
|
||||
args = getattr(replica.stage_cfg, "engine_args", {})
|
||||
config = (
|
||||
args.get("kv_transfer_config")
|
||||
if isinstance(args, Mapping)
|
||||
else getattr(args, "kv_transfer_config", None)
|
||||
)
|
||||
if config is not None:
|
||||
roles[plan.stage_id] = config.get("kv_role") if isinstance(config, Mapping) else config.kv_role
|
||||
if not roles:
|
||||
return
|
||||
if (
|
||||
self._async_chunk
|
||||
or len(plans) != 2
|
||||
or roles != {0: "kv_producer", 1: "kv_consumer"}
|
||||
or plans[0].replicas[0].metadata.stage_type != "llm"
|
||||
or plans[1].replicas[0].metadata.stage_type != "diffusion"
|
||||
or list(plans[1].replicas[0].metadata.engine_input_source or []) != [0]
|
||||
):
|
||||
raise ValueError(
|
||||
"Native AR-to-DiT KV transfer currently supports only a two-stage "
|
||||
"0 (kv_producer) -> 1 (diffusion kv_consumer, engine_input_source=[0]) "
|
||||
"pipeline with async_chunk=False"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _prepare_replica_stage_config(
|
||||
stage_cfg: Any,
|
||||
*,
|
||||
stage_id: int,
|
||||
stage_type: str,
|
||||
replica_id: int,
|
||||
num_replicas: int,
|
||||
) -> tuple[Any, bool]:
|
||||
"""Isolate replica config and assign the native DiT KV identity."""
|
||||
if isinstance(stage_cfg, BaseVllmOmniStageConfig):
|
||||
native_kv = stage_cfg.connector_config.kv_transfer_config
|
||||
if native_kv is None and isinstance(stage_cfg, VllmOmniDiffusionStageConfig):
|
||||
native_kv = stage_cfg.diffusion_config.kv_transfer_config
|
||||
else:
|
||||
native_kv = (
|
||||
stage_cfg.engine_args.get("kv_transfer_config")
|
||||
if isinstance(stage_cfg.engine_args, Mapping)
|
||||
else getattr(stage_cfg.engine_args, "kv_transfer_config", None)
|
||||
)
|
||||
# Keep the logical stage's device pool and native KV identity
|
||||
# intact; each replica owns its config throughout startup.
|
||||
replica_cfg = copy.deepcopy(stage_cfg) if num_replicas > 1 or native_kv else stage_cfg
|
||||
if native_kv and stage_type == "diffusion":
|
||||
if isinstance(replica_cfg, VllmOmniDiffusionStageConfig):
|
||||
kv_config = (
|
||||
replica_cfg.connector_config.kv_transfer_config or replica_cfg.diffusion_config.kv_transfer_config
|
||||
)
|
||||
assert kv_config is not None
|
||||
kv_config.engine_id = f"{kv_config.engine_id}-s{stage_id}-r{replica_id}"
|
||||
else:
|
||||
kv_config = replica_cfg.engine_args["kv_transfer_config"]
|
||||
kv_config["engine_id"] = f"{kv_config['engine_id']}-s{stage_id}-r{replica_id}"
|
||||
return replica_cfg, bool(native_kv)
|
||||
|
||||
@staticmethod
|
||||
def _prepare_replica_vllm_config(
|
||||
stage_vllm_config: Any,
|
||||
replica_cfg: Any,
|
||||
replica_metadata: StageMetadata,
|
||||
*,
|
||||
native_kv: bool,
|
||||
) -> Any:
|
||||
"""Assign native AR KV identity and the producer bootstrap endpoint."""
|
||||
if not native_kv or stage_vllm_config is None:
|
||||
return stage_vllm_config
|
||||
replica_vllm_config = copy.deepcopy(stage_vllm_config)
|
||||
kv_config = replica_vllm_config.kv_transfer_config
|
||||
kv_config.engine_id = f"{kv_config.engine_id}-s{replica_metadata.stage_id}-r{replica_metadata.replica_id}"
|
||||
if kv_config.kv_connector == "MooncakeConnector" and kv_config.kv_role == "kv_producer":
|
||||
extra = kv_config.kv_connector_extra_config
|
||||
port = int(extra.get("bootstrap_port", 8998)) + replica_metadata.replica_id
|
||||
extra["bootstrap_addr"] = f"http://{kv_config.kv_ip}:{port}"
|
||||
if isinstance(replica_cfg, BaseVllmOmniStageConfig):
|
||||
runtime_cfg = replica_cfg.runtime_config
|
||||
runtime_cfg.env = {
|
||||
**(runtime_cfg.env or {}),
|
||||
"VLLM_MOONCAKE_BOOTSTRAP_PORT": str(port),
|
||||
}
|
||||
else:
|
||||
runtime_cfg = copy.deepcopy(replica_metadata.runtime_cfg or {})
|
||||
runtime_cfg["env"] = {
|
||||
**(runtime_cfg.get("env") or {}),
|
||||
"VLLM_MOONCAKE_BOOTSTRAP_PORT": str(port),
|
||||
}
|
||||
replica_metadata.runtime_cfg = runtime_cfg
|
||||
return replica_vllm_config
|
||||
|
||||
def _build_logical_stage_init_plans(
|
||||
self,
|
||||
omni_transfer_config: Any,
|
||||
@@ -658,9 +767,13 @@ class StageRuntime:
|
||||
)
|
||||
|
||||
for replica_id in range(num_replicas):
|
||||
# Keep the logical stage's device pool intact; each replica owns
|
||||
# the same config throughout planning, metadata and launch.
|
||||
replica_cfg = copy.deepcopy(stage_cfg) if num_replicas > 1 else stage_cfg
|
||||
replica_cfg, native_kv = self._prepare_replica_stage_config(
|
||||
stage_cfg,
|
||||
stage_id=stage_id,
|
||||
stage_type=base_metadata.stage_type,
|
||||
replica_id=replica_id,
|
||||
num_replicas=num_replicas,
|
||||
)
|
||||
if stage_idx in replica_devices_map:
|
||||
devices = replica_devices_map[stage_idx][replica_id]
|
||||
runtime_cfg = getattr(replica_cfg, "runtime_config", getattr(replica_cfg, "runtime", None))
|
||||
@@ -673,6 +786,12 @@ class StageRuntime:
|
||||
else extract_legacy_stage_metadata(replica_cfg)
|
||||
)
|
||||
replica_metadata.replica_id = replica_id
|
||||
replica_vllm_config = self._prepare_replica_vllm_config(
|
||||
stage_vllm_config,
|
||||
replica_cfg,
|
||||
replica_metadata,
|
||||
native_kv=native_kv,
|
||||
)
|
||||
if launch_mode == "remote" and replica_metadata.stage_type != "diffusion":
|
||||
replica_metadata.runtime_cfg = None
|
||||
replicas.append(
|
||||
@@ -684,7 +803,7 @@ class StageRuntime:
|
||||
metadata=replica_metadata,
|
||||
stage_connector_spec=stage_connector_spec,
|
||||
omni_kv_connector=omni_kv_connector,
|
||||
stage_vllm_config=stage_vllm_config,
|
||||
stage_vllm_config=replica_vllm_config,
|
||||
executor_class=executor_class,
|
||||
engine_args_dict=copy.deepcopy(engine_args_dict) if engine_args_dict is not None else None,
|
||||
)
|
||||
|
||||
@@ -1536,7 +1536,10 @@ class HunyuanImage3ForConditionalGeneration(nn.Module, SupportsMultiModal, Suppo
|
||||
# Used to embed timestep information into the input sequence.
|
||||
self.timestep_emb = TimestepEmbedder(hidden_size=config.hidden_size)
|
||||
|
||||
tokenizer = get_tokenizer(vllm_config.model_config.tokenizer)
|
||||
tokenizer = get_tokenizer(
|
||||
vllm_config.model_config.tokenizer,
|
||||
trust_remote_code=vllm_config.model_config.trust_remote_code,
|
||||
)
|
||||
self._mrope_img_token_id = tokenizer.convert_tokens_to_ids("<img>")
|
||||
self._mrope_boi_token_id = tokenizer.convert_tokens_to_ids("<boi>")
|
||||
self._mrope_eoi_token_id = tokenizer.convert_tokens_to_ids("<eoi>")
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
|
||||
|
||||
class OmniWorkerMixin:
|
||||
"""Mixin to ensure Omni plugins are loaded in worker processes."""
|
||||
"""Shared Omni plugin and native KV connector setup for workers."""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -15,6 +15,16 @@ class OmniWorkerMixin:
|
||||
|
||||
load_omni_general_plugins()
|
||||
|
||||
def initialize_from_config(self, kv_cache_config) -> None:
|
||||
# The concrete GPU/NPU/XPU worker supplies this method through the MRO.
|
||||
super().initialize_from_config(kv_cache_config) # type: ignore[misc]
|
||||
from vllm.distributed.kv_transfer import get_kv_transfer_group, has_kv_transfer_group
|
||||
|
||||
if has_kv_transfer_group():
|
||||
from vllm_omni.diffusion.diffusion_kv.kv_connector import install_mooncake_cfg_fanout
|
||||
|
||||
install_mooncake_cfg_fanout(get_kv_transfer_group())
|
||||
|
||||
|
||||
def maybe_unpad_input_ids(model, input_ids, num_tokens_unpadded):
|
||||
"""Trim cudagraph-bucket padding for models that split a flat ``input_ids``
|
||||
|
||||
Reference in new issue
Block a user