[Orchestrator] Implement cache reset RPC forwarding to AR stage engine cores (#6766)

Signed-off-by: Asthenia <asthenia0412@gmail.com>
Signed-off-by: Gao Han <hgaoaf@connect.ust.hk>
Co-authored-by: Asthenia <asthenia0412@gmail.com>
Co-authored-by: Gao Han <hgaoaf@connect.ust.hk>
This commit is contained in:
authored and GitHub committed 2026-09-24 15:32:07 +00:00
1 parent 535bd35b42
commit 0295eeb6f3
7 files changed
+497 -65

No files matched your search

@@ -1206,3 +1206,110 @@ async def test_rpc_failure_capture_is_limited_to_pause_and_resume() -> None:
finally:
for q in queues:
q.close()
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["reset_mm_cache", "reset_encoder_cache", "reset_prefix_cache"])
async def test_cache_reset_rpc_failure_is_nonfatal_and_visits_all_replicas(method):
clients = [FakeStageClient(stage_type="llm") for _ in range(2)]
setattr(clients[0], f"{method}_async", AsyncMock(side_effect=RuntimeError("reset failed")))
ok = AsyncMock(return_value=True)
setattr(clients[1], f"{method}_async", ok)
orchestrator, queues = _build_bare_orchestrator(_build_stage_pools([clients]))
try:
await orchestrator._handle_collective_rpc(
CollectiveRPCRequestMessage(rpc_id="reset", method=method, args=(), kwargs={}, stage_ids=[0], timeout=1.0)
)
result = queues[2].async_q.get_nowait()
assert result.results[0]["supported"] is False
assert "reset failed" in result.results[0]["error"]
assert result.results[1] is True
ok.assert_awaited_once_with()
# Errors before StagePool dispatch (e.g. a missing route) must also
# be serialized at the orchestrator boundary, then allow another RPC.
orchestrator.stage_pools[0].collective_rpc = AsyncMock(side_effect=RuntimeError("route failed"))
await orchestrator._handle_collective_rpc(
CollectiveRPCRequestMessage(rpc_id="route", method=method, args=(), kwargs={}, stage_ids=[0])
)
assert all("route failed" in r["error"] for r in queues[2].async_q.get_nowait().results)
orchestrator.stage_pools[0].collective_rpc = AsyncMock(return_value=True)
await orchestrator._handle_collective_rpc(
CollectiveRPCRequestMessage(rpc_id="next", method=method, args=(), kwargs={}, stage_ids=[0])
)
assert queues[2].async_q.get_nowait().results == [True, True]
finally:
for q in queues:
q.close()
@pytest.mark.asyncio
@pytest.mark.parametrize(
"method, kwargs, clears",
[
("reset_mm_cache", {}, True),
("sleep", {}, True),
("pause_scheduler", {"clear_cache": True}, True),
("pause_scheduler", {"clear_cache": False}, False),
("reset_prefix_cache", {}, False),
("reset_encoder_cache", {}, False),
],
)
async def test_control_rpc_clears_only_selected_downstream_sender(method, kwargs, clears):
pools = _build_stage_pools([[FakeStageClient(stage_type="llm") for _ in range(2)] for _ in range(2)])
orchestrator, queues = _build_bare_orchestrator(pools)
events = []
renderers = [SimpleNamespace(clear_mm_cache_async=AsyncMock()) for _ in range(2)]
renderers[1].clear_mm_cache_async.side_effect = lambda: events.append("sender")
orchestrator._stage_input_processors = {i: SimpleNamespace(renderer=r) for i, r in enumerate(renderers)}
async def rpc(**kwargs):
events.append("receiver")
return True
pools[0].collective_rpc = AsyncMock()
pools[1].collective_rpc = AsyncMock(side_effect=rpc)
try:
await orchestrator._handle_collective_rpc(
CollectiveRPCRequestMessage(
rpc_id="reset", method=method, args=(), kwargs=kwargs, stage_ids=[1], timeout=1.0
)
)
renderers[0].clear_mm_cache_async.assert_not_awaited()
pools[0].collective_rpc.assert_not_awaited()
assert renderers[1].clear_mm_cache_async.await_count == int(clears)
assert events == (["sender"] if clears else []) + ["receiver", "receiver"]
assert queues[2].async_q.get_nowait().results == [True, True]
finally:
for q in queues:
q.close()
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", ["error", "timeout"])
async def test_downstream_sender_clear_failure_is_nonfatal(failure):
pools = _build_stage_pools([[FakeStageClient(stage_type="llm")]])
orchestrator, queues = _build_bare_orchestrator(pools)
async def clear():
if failure == "timeout":
await asyncio.Event().wait()
raise RuntimeError("sender cache failed")
renderer = SimpleNamespace(clear_mm_cache_async=AsyncMock(side_effect=clear))
orchestrator._stage_input_processors[0] = SimpleNamespace(renderer=renderer)
pools[0].collective_rpc = AsyncMock(return_value=None)
message = CollectiveRPCRequestMessage(
rpc_id="reset", method="reset_mm_cache", args=(), kwargs={}, stage_ids=[0], timeout=0.01
)
try:
await orchestrator._handle_collective_rpc(message)
result = queues[2].async_q.get_nowait().results[0]
assert result["supported"] is False
assert ("TimeoutError" if failure == "timeout" else "sender cache failed") in result["error"]
pools[0].collective_rpc.assert_not_awaited()
renderer.clear_mm_cache_async = AsyncMock()
await orchestrator._handle_collective_rpc(message)
assert queues[2].async_q.get_nowait().results == [None]
finally:
for q in queues:
q.close()
+60 -5
View File
@@ -1,3 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Unit tests for StagePool.collective_rpc EngineCore control dispatch."""
from __future__ import annotations
@@ -40,14 +43,14 @@ def test_collective_rpc_normalizes_none_args_on_control_helper():
def test_collective_rpc_unrelated_async_helper_uses_collective_path():
async def run() -> None:
other = AsyncMock(return_value="should-not-run")
pool, client = _make_pool(reset_prefix_cache_async=other)
pool, client = _make_pool(is_sleeping_async=other)
result = await pool.collective_rpc(0, "reset_prefix_cache", timeout=1.5, args=("x",), kwargs={"k": 1})
result = await pool.collective_rpc(0, "is_sleeping", timeout=1.5, args=("x",), kwargs={"k": 1})
assert result == {"via": "collective"}
other.assert_not_awaited()
client.collective_rpc_async.assert_awaited_once_with(
method="reset_prefix_cache",
method="is_sleeping",
timeout=1.5,
args=("x",),
kwargs={"k": 1},
@@ -63,7 +66,7 @@ def test_collective_rpc_control_helper_honors_timeout():
await asyncio.sleep(1.0)
return "slept"
pool, _client = _make_pool(sleep_async=slow_sleep)
pool, _client = _make_pool(sleep_async=AsyncMock(side_effect=slow_sleep))
with pytest.raises(asyncio.TimeoutError):
await pool.collective_rpc(0, "sleep", timeout=0.01, args=(1,))
@@ -80,11 +83,38 @@ def test_collective_rpc_control_method_reraises_worker_error():
asyncio.run(run())
@pytest.mark.cpu
def test_collective_rpc_reset_caches_use_control_helpers():
async def run() -> None:
reset_prefix = AsyncMock(return_value=True)
reset_encoder = AsyncMock(return_value=None)
reset_mm = AsyncMock(return_value=None)
pool, client = _make_pool(
reset_prefix_cache_async=reset_prefix,
reset_encoder_cache_async=reset_encoder,
reset_mm_cache_async=reset_mm,
)
result = await pool.collective_rpc(0, "reset_prefix_cache", args=(True, True), timeout=2.0)
assert result is True
reset_prefix.assert_awaited_once_with(True, True)
await pool.collective_rpc(0, "reset_encoder_cache", timeout=2.0)
reset_encoder.assert_awaited_once()
await pool.collective_rpc(0, "reset_mm_cache", timeout=2.0)
reset_mm.assert_awaited_once()
client.collective_rpc_async.assert_not_awaited()
asyncio.run(run())
@pytest.mark.cpu
def test_collective_rpc_non_control_method_still_returns_error_dict():
async def run() -> None:
pool, _client = _make_pool(collective_rpc_async=AsyncMock(side_effect=RuntimeError("probe failed")))
result = await pool.collective_rpc(0, "reset_prefix_cache")
result = await pool.collective_rpc(0, "is_sleeping")
assert result["supported"] is False
assert "probe failed" in result["error"]
@@ -122,3 +152,28 @@ def test_abort_requests_does_not_commit_op_state_when_engine_abort_fails():
abort.assert_awaited_once()
asyncio.run(run())
@pytest.mark.cpu
@pytest.mark.parametrize("method", ["reset_prefix_cache", "reset_encoder_cache", "reset_mm_cache"])
@pytest.mark.parametrize("failure", ["error", "timeout", "missing"])
def test_cache_reset_failure_is_serialized_and_pool_remains_usable(method, failure):
async def run():
async def reset():
if failure == "timeout":
await asyncio.Event().wait()
raise RuntimeError("cache reset failed")
pool, client = _make_pool()
if failure != "missing":
setattr(client, f"{method}_async", AsyncMock(side_effect=reset))
result = await pool.collective_rpc(0, method, timeout=0.01)
assert result["supported"] is False
assert result["error"]
if failure == "timeout":
assert "timed out" in result["error"]
client.collective_rpc_async.assert_not_awaited()
# A subsequent control request still completes on the same pool.
assert await pool.collective_rpc(0, "is_sleeping") == {"via": "collective"}
asyncio.run(run())
@@ -0,0 +1,77 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Real EngineCore cache invalidation through the AsyncOmni control plane."""
import pytest
from PIL import Image
from vllm import SamplingParams
from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
from vllm_omni.entrypoints.async_omni import AsyncOmni
@pytest.mark.advanced_model
@pytest.mark.omni
@pytest.mark.cuda
@pytest.mark.asyncio
async def test_cache_resets_invalidate_real_engine_and_allow_repeated_multimodal_inputs():
deploy_config = modify_stage_config(
get_deploy_config_path("ci/qwen2_5_omni_thinker_only.yaml"),
updates={
"stages": {
0: {
"enable_prefix_caching": True,
"mm_processor_cache_gb": 0.1,
"max_model_len": 2048,
"max_num_batched_tokens": 2048,
"num_gpu_blocks_override": 256,
"enforce_eager": True,
}
}
},
)
engine = AsyncOmni(
model="Qwen/Qwen2.5-Omni-7B",
deploy_config=deploy_config,
stage_init_timeout=600,
)
async def generate(prompt, request_id):
final = None
async for output in engine.generate(
prompt,
request_id=request_id,
sampling_params_list=[SamplingParams(max_tokens=4, temperature=0)],
):
final = output
assert final is not None and final.finished
assert final.outputs and final.outputs[0].token_ids
return final
try:
prompt = "Explain how a cache works. " * 40
cold = await generate(prompt, "cold")
warm = await generate(prompt, "warm")
assert cold.num_cached_tokens == 0
assert warm.num_cached_tokens > 0
assert await engine.reset_prefix_cache(reset_running_requests=True, reset_connector=True)
await engine.reset_encoder_cache()
await engine.reset_mm_cache()
reset = await generate(prompt, "reset")
assert reset.num_cached_tokens == 0
assert reset.outputs[0].token_ids == cold.outputs[0].token_ids
image_prompt = {
"prompt": "<|im_start|>user\n<|vision_start|><|IMAGE|><|vision_end|>Describe this image.<|im_end|>\n<|im_start|>assistant\n",
"multi_modal_data": {"image": Image.new("RGB", (56, 56), "red")},
}
before = await generate(image_prompt, "image-before")
await engine.reset_mm_cache()
await engine.reset_encoder_cache()
assert await engine.reset_prefix_cache()
after = await generate(image_prompt, "image-after")
assert after.outputs[0].token_ids == before.outputs[0].token_ids
finally:
engine.shutdown()
@@ -14,7 +14,7 @@ from vllm.renderers import BaseRenderer
from vllm.v1.engine.input_processor import InputProcessor
from vllm_omni.diffusion.data import CuMemTag, OmniACK
from vllm_omni.entrypoints.async_omni import AsyncOmni
from vllm_omni.entrypoints.async_omni import CACHE_RESET_TIMEOUT_S, AsyncOmni
pytestmark = [pytest.mark.core_model]
@@ -31,7 +31,7 @@ def _make_omni(*, stage_types: list[str]) -> AsyncOmni:
omni._level2_sleeping = False
omni.event_resolver = SimpleNamespace(watch_task=lambda *a, **k: None, resolve=AsyncMock())
omni._final_output_handler = lambda: None
omni.reset_mm_cache = AsyncMock()
omni._clear_frontend_mm_cache = AsyncMock()
stage_configs = [SimpleNamespace(stage_type=stage_type) for stage_type in stage_types]
omni.engine = SimpleNamespace(
@@ -46,7 +46,8 @@ def _make_omni(*, stage_types: list[str]) -> AsyncOmni:
@pytest.mark.cpu
def test_reset_mm_cache_routes_through_renderer(mocker):
async def run() -> None:
omni = object.__new__(AsyncOmni)
omni = _make_omni(stage_types=["llm"])
del omni._clear_frontend_mm_cache
renderer = mocker.create_autospec(BaseRenderer, instance=True)
input_processor = mocker.create_autospec(InputProcessor, instance=True)
input_processor.renderer = renderer
@@ -115,9 +116,10 @@ def test_pause_generation_still_rpcs_when_already_paused():
kwargs={"mode": "abort", "clear_cache": True},
stage_ids=[0],
)
omni.reset_prefix_cache.assert_awaited_once()
omni.reset_mm_cache.assert_awaited_once()
omni.reset_encoder_cache.assert_awaited_once()
omni.reset_prefix_cache.assert_not_awaited()
omni.reset_mm_cache.assert_not_awaited()
omni.reset_encoder_cache.assert_not_awaited()
omni._clear_frontend_mm_cache.assert_awaited_once_with()
asyncio.run(run())
@@ -142,6 +144,84 @@ def test_resume_generation_resumes_ar_then_clears_frontend_pause():
asyncio.run(run())
@pytest.mark.cpu
def test_reset_prefix_cache_forwards_to_ar_stages():
async def run() -> None:
omni = _make_omni(stage_types=["llm", "diffusion"])
result = await omni.reset_prefix_cache(reset_running_requests=True, reset_connector=True)
assert result is True
omni.collective_rpc.assert_awaited_once_with(
method="reset_prefix_cache",
args=(True, True),
kwargs=None,
stage_ids=[0],
timeout=CACHE_RESET_TIMEOUT_S,
)
asyncio.run(run())
@pytest.mark.cpu
def test_reset_prefix_cache_diffusion_only_skips_rpc():
async def run() -> None:
omni = _make_omni(stage_types=["diffusion"])
result = await omni.reset_prefix_cache()
assert result is True
omni.collective_rpc.assert_not_awaited()
asyncio.run(run())
@pytest.mark.cpu
def test_reset_encoder_cache_forwards_to_ar_stages():
async def run() -> None:
omni = _make_omni(stage_types=["llm", "diffusion"])
await omni.reset_encoder_cache()
omni.collective_rpc.assert_awaited_once_with(
method="reset_encoder_cache",
args=(),
kwargs=None,
stage_ids=[0],
timeout=CACHE_RESET_TIMEOUT_S,
)
asyncio.run(run())
@pytest.mark.cpu
def test_reset_mm_cache_clears_frontend_then_forwards_to_ar_stages():
async def run() -> None:
omni = _make_omni(stage_types=["llm", "diffusion"])
calls = []
renderer = SimpleNamespace(clear_mm_cache_async=AsyncMock(side_effect=lambda: calls.append("frontend")))
omni.input_processor = SimpleNamespace(renderer=renderer)
del omni._clear_frontend_mm_cache
async def rpc(**kwargs):
calls.append("engine")
return [None]
omni.collective_rpc.side_effect = rpc
await omni.reset_mm_cache()
assert calls == ["frontend", "engine"]
omni.collective_rpc.assert_awaited_once_with(
method="reset_mm_cache",
args=(),
kwargs=None,
stage_ids=[0],
timeout=CACHE_RESET_TIMEOUT_S,
)
asyncio.run(run())
@pytest.mark.cpu
def test_sleep_routes_ar_via_collective_rpc_and_diffusion_to_worker_rpc():
async def run() -> None:
@@ -539,8 +619,9 @@ def test_mixed_engine_keep_sends_engine_core_and_diffusion_pauses():
"kwargs": {"mode": "keep"},
"stage_ids": [1],
}
# The frontend cache reset still runs after the backend pauses.
omni.reset_prefix_cache.assert_awaited_once()
# Only P0 needs clearing here: pause_scheduler already resets P1.
omni.reset_prefix_cache.assert_not_awaited()
omni._clear_frontend_mm_cache.assert_awaited_once_with()
await omni.resume_generation()
assert _rpc_methods(omni)[2:] == [("resume_scheduler", [0]), ("resume_scheduler", [1])]
@@ -579,9 +660,11 @@ def test_keep_then_sleep_wake_resume_keeps_admission_paused_until_resume():
omni.reset_prefix_cache = AsyncMock(return_value=True)
omni.reset_encoder_cache = AsyncMock()
omni.collective_rpc = AsyncMock(
side_effect=lambda **kw: [OmniACK(task_id="t", status="SUCCESS", stage_id=0, rank=0)]
if kw["method"] in {"handle_sleep_task", "handle_wake_task"}
else [None]
side_effect=lambda **kw: (
[OmniACK(task_id="t", status="SUCCESS", stage_id=0, rank=0)]
if kw["method"] in {"handle_sleep_task", "handle_wake_task"}
else [None]
)
)
await omni.pause_generation(mode="keep", clear_cache=False)
@@ -683,3 +766,63 @@ def test_two_ar_stages_partial_resume_keeps_admission_paused():
assert await omni.is_paused() is False
asyncio.run(run())
@pytest.mark.cpu
@pytest.mark.parametrize("method", ["reset_prefix_cache", "reset_encoder_cache", "reset_mm_cache"])
@pytest.mark.parametrize("stage_ids, targets", [(None, [0, 2]), ([2], [2]), ([1], []), ([], [])])
def test_cache_resets_respect_stage_selection(method, stage_ids, targets):
async def run():
omni = _make_omni(stage_types=["llm", "diffusion", "llm"])
await getattr(omni, method)(stage_ids=stage_ids, timeout=2.0)
if targets:
assert omni.collective_rpc.await_args.kwargs["stage_ids"] == targets
assert omni.collective_rpc.await_args.kwargs["timeout"] == 2.0
else:
omni.collective_rpc.assert_not_awaited()
assert omni._clear_frontend_mm_cache.await_count == int(method == "reset_mm_cache" and 0 in targets)
asyncio.run(run())
@pytest.mark.cpu
@pytest.mark.parametrize("method", ["reset_prefix_cache", "reset_encoder_cache", "reset_mm_cache"])
@pytest.mark.parametrize("failure", [{"todo": "unsupported"}, {"supported": False}, {"error": "reset failed"}])
def test_cache_resets_reject_failure_from_any_replica(method, failure):
async def run():
omni = _make_omni(stage_types=["llm"])
# Exercise the actual collective_rpc too: a single stage can have
# multiple replicas, so result indexes are not stage IDs.
del omni.collective_rpc
omni.engine.collective_rpc_async.return_value = [True, failure]
with pytest.raises(RuntimeError, match=method):
await getattr(omni, method)()
asyncio.run(run())
@pytest.mark.cpu
def test_prefix_reset_returns_false_if_any_replica_refuses():
async def run():
omni = _make_omni(stage_types=["llm"])
omni.collective_rpc.return_value = [True, [True, False]]
assert await omni.reset_prefix_cache() is False
asyncio.run(run())
@pytest.mark.cpu
@pytest.mark.parametrize("operation", ["pause_generation", "sleep"])
@pytest.mark.parametrize("stage_ids", [[0], [2]])
def test_pause_and_sleep_do_not_reset_unselected_stages_or_reset_twice(operation, stage_ids):
async def run():
omni = _make_omni(stage_types=["llm", "diffusion", "llm"])
kwargs = {"clear_cache": True} if operation == "pause_generation" else {}
await getattr(omni, operation)(stage_ids=stage_ids, **kwargs)
assert omni.collective_rpc.await_count == 1
rpc = omni.collective_rpc.await_args.kwargs
assert rpc["method"] == ("pause_scheduler" if operation == "pause_generation" else "sleep")
assert rpc["stage_ids"] == stage_ids
assert omni._clear_frontend_mm_cache.await_count == int(0 in stage_ids)
asyncio.run(run())
+20 -6
View File
@@ -704,8 +704,21 @@ class OrchestratorBase:
results: list[Any] = []
stage_ids: list[int] = []
for pool in target_pools:
clear_sender = pool.stage_type != "diffusion" and (
method in ("reset_mm_cache", "sleep")
or (method == "pause_scheduler" and kwargs.get("clear_cache", args[1] if len(args) > 1 else True))
)
for replica_id in pool.live_replica_ids():
try:
if clear_sender:
processor = self._stage_input_processors.get(pool.stage_id)
if processor is not None:
clear = processor.renderer.clear_mm_cache_async()
if timeout is None:
await clear
else:
await asyncio.wait_for(clear, timeout=timeout)
clear_sender = False
stage_result = await pool.collective_rpc(
replica_id=replica_id,
method=method,
@@ -714,13 +727,14 @@ class OrchestratorBase:
kwargs=kwargs,
)
except Exception as exc:
if method not in ("pause_scheduler", "resume_scheduler"):
if (
method not in ("pause_scheduler", "resume_scheduler")
and method not in StagePool._CACHE_RESET_METHODS
):
raise
# A pause or resume that fails on one replica leaves the
# engine usable, so report it as this replica's result
# rather than out of the request handler; the caller
# reaches both through _engine_core_rpc, which raises on
# the error result and can then retry or resume.
# Administrative failures must reach the caller without
# terminating unrelated stages. _engine_core_rpc raises
# on this result; remaining replicas still receive the RPC.
stage_result = {"supported": False, "error": f"{type(exc).__name__}: {exc}"}
stage_ids.append(pool.stage_id)
results.append(stage_result)
+7 -1
View File
@@ -101,12 +101,16 @@ class StagePool:
DISPATCH_RETRY_INTERVAL_S: float = 0.1
# Only these EngineCore helpers may skip collective_rpc_async. A generic
# ``{method}_async`` on AsyncMPClient must not silently drop timeout.
_CACHE_RESET_METHODS = frozenset({"reset_prefix_cache", "reset_encoder_cache", "reset_mm_cache"})
_ENGINE_CORE_CONTROL_ASYNC_METHODS = frozenset(
{
"pause_scheduler",
"resume_scheduler",
"sleep",
"wake_up",
"reset_prefix_cache",
"reset_encoder_cache",
"reset_mm_cache",
}
)
@@ -1330,6 +1334,8 @@ class StagePool:
if timeout is not None:
return await asyncio.wait_for(result, timeout=timeout)
return await result
if method in self._CACHE_RESET_METHODS:
return {"supported": False, "error": f"EngineCore helper {method}_async is unavailable"}
return await client.collective_rpc_async(
method=method,
@@ -1344,7 +1350,7 @@ class StagePool:
replica_id,
method,
)
if method in self._ENGINE_CORE_CONTROL_ASYNC_METHODS:
if method in self._ENGINE_CORE_CONTROL_ASYNC_METHODS and method not in self._CACHE_RESET_METHODS:
raise
if isinstance(exc, TimeoutError):
error = f"{type(exc).__name__}: {method} timed out after {timeout}s"
+72 -42
View File
@@ -46,6 +46,8 @@ if TYPE_CHECKING:
logger = init_logger(__name__)
CACHE_RESET_TIMEOUT_S = 60.0
class AsyncOmni(AsyncOmniBase, EngineClient):
"""Asynchronous unified entry point for multi-stage pipelines using AsyncOmniEngine.
@@ -535,17 +537,15 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
stage_ids=stage_ids,
)
unsupported_stage_ids: list[int] = []
effective_stage_ids = stage_ids or list(range(len(results)))
for index, result in enumerate(results):
if isinstance(result, dict) and result.get("todo"):
unsupported_stage_ids.append(effective_stage_ids[index])
if unsupported_stage_ids:
unsupported_results = [
index for index, result in enumerate(results) if isinstance(result, dict) and result.get("todo")
]
if unsupported_results:
logger.warning(
"[AsyncOmni] collective_rpc(%s) has TODO support on stage(s): %s",
"[AsyncOmni] collective_rpc(%s) has TODO support in replica result(s) %s (requested stages: %s)",
method,
unsupported_stage_ids,
unsupported_results,
stage_ids,
)
return results
@@ -557,23 +557,28 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
stage_ids: list[int],
args: tuple[Any, ...] = (),
kwargs: dict[str, Any] | None = None,
timeout: float | None = None,
) -> list[Any]:
"""Call an engine control helper via collective_rpc (orchestrator loop).
StagePool resolves ``{method}_async`` on the AR client when present
(vLLM AsyncMPClient convention); diffusion stages answer the same
method names inside DiffusionEngine. Raises if any replica reports
failure.
"""
"""Call EngineCore helpers and reject failures from any replica."""
timeout_kwargs = {"timeout": timeout} if timeout is not None else {}
results = await self.collective_rpc(
method=method,
args=args,
kwargs=kwargs,
stage_ids=stage_ids,
**timeout_kwargs,
)
def check_result(result: Any) -> None:
if isinstance(result, (list, tuple)):
for item in result:
check_result(item)
elif isinstance(result, dict):
if result.get("error") or result.get("todo") or result.get("supported") is False:
raise RuntimeError(f"{method} failed: {result}")
for result in results:
if isinstance(result, dict) and result.get("error"):
raise RuntimeError(f"{method} failed: {result['error']}")
check_result(result)
return results
@staticmethod
@@ -768,13 +773,8 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
# Frontend / sender-side cache clear (P0). EngineCore.pause_scheduler
# already clears AR-side caches when clear_cache=True.
if clear_cache:
await self.reset_prefix_cache(
reset_running_requests=not wait_for_inflight_requests,
reset_connector=True,
)
await self.reset_mm_cache()
await self.reset_encoder_cache()
if clear_cache and 0 in ar_stage_ids:
await self._clear_frontend_mm_cache()
async def resume_generation(self, stage_ids: list[int] | None = None) -> None:
"""Resume generation after :meth:`pause_generation`."""
@@ -835,34 +835,63 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
"""
return await self.collective_rpc(method="profile", args=(False, None), stage_ids=stages)
async def reset_mm_cache(self) -> None:
"""Reset the frontend (P0) multimodal processor cache.
``EngineCore.sleep(level>=1)`` already clears the P1 receiver cache.
Clearing P0 avoids hash-only follow-up requests after that reset.
"""
async def _clear_frontend_mm_cache(self) -> None:
"""Clear P0 through the renderer's serialized multimodal executor."""
renderer = self.renderer
if renderer is not None:
await renderer.clear_mm_cache_async()
async def reset_encoder_cache(self) -> None:
"""Reset the encoder cache for all stages.
async def reset_mm_cache(
self, *, stage_ids: list[int] | None = None, timeout: float = CACHE_RESET_TIMEOUT_S
) -> None:
"""Reset sender and receiver MM caches on selected AR stages.
TODO: Forward to Orchestrator process via message.
By default all AR stages are selected; diffusion stages are skipped.
Stage 0's sender is the frontend renderer. Downstream senders are
cleared by the orchestrator before resetting their engine cores.
Call while generation is paused to avoid racing new inputs.
"""
logger.warning("[AsyncOmni] reset_encoder_cache not yet supported with Orchestrator process")
ar_stage_ids, _ = self._split_stage_ids_by_type(stage_ids)
if 0 in ar_stage_ids:
await asyncio.wait_for(self._clear_frontend_mm_cache(), timeout=timeout)
if ar_stage_ids:
await self._engine_core_rpc("reset_mm_cache", stage_ids=ar_stage_ids, timeout=timeout)
async def reset_encoder_cache(
self, *, stage_ids: list[int] | None = None, timeout: float = CACHE_RESET_TIMEOUT_S
) -> None:
"""Reset encoder caches on selected AR stages (all by default).
Diffusion stages are skipped. RPC failures are raised to the caller.
"""
ar_stage_ids, _ = self._split_stage_ids_by_type(stage_ids)
if ar_stage_ids:
await self._engine_core_rpc("reset_encoder_cache", stage_ids=ar_stage_ids, timeout=timeout)
async def reset_prefix_cache(
self,
reset_running_requests: bool = False,
reset_connector: bool = False,
*,
stage_ids: list[int] | None = None,
timeout: float = CACHE_RESET_TIMEOUT_S,
) -> bool:
"""Reset the prefix cache for all stages.
"""Reset prefix caches on selected AR stages (all by default).
TODO: Forward to Orchestrator process via message.
Diffusion stages are skipped. Return False if a stage cannot reset
its cache; unsupported operations and RPC failures raise instead of
silently retaining KV computed under previous model weights.
"""
logger.warning("[AsyncOmni] reset_prefix_cache not yet supported with Orchestrator process")
return True
ar_stage_ids, _ = self._split_stage_ids_by_type(stage_ids)
if not ar_stage_ids:
return True
results = await self._engine_core_rpc(
"reset_prefix_cache",
stage_ids=ar_stage_ids,
args=(reset_running_requests, reset_connector),
timeout=timeout,
)
return all(self._coerce_stage_bool(result) for result in results)
async def sleep(
self, stage_ids: list[int] | None = None, level: int = 2, mode: PauseMode = "abort"
@@ -895,11 +924,12 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
self._paused = True
await self._pause_cond.wait_for(lambda: getattr(self, "_admitting", 0) == 0)
# P0 sender cache must drop hashes before EngineCore.sleep clears P1.
await self.reset_mm_cache()
ar_stage_ids, diffusion_stage_ids = self._split_stage_ids_by_type(stage_ids)
# EngineCore.sleep resets receiver caches itself; only clear P0 here.
if 0 in ar_stage_ids:
await self._clear_frontend_mm_cache()
self._final_output_handler()
ar_stage_ids, diffusion_stage_ids = self._split_stage_ids_by_type(stage_ids)
final_acks: list[OmniACK] = []
if ar_stage_ids:
self._hold_admission_until_resume = True