Files
vllm-omni/tests/engine/test_cfg_companion_lifecycle.py
T
tzhouamandClaude Fable 5 e6ad8b67bf fix(orchestrator): never dispatch diffusion with an incomplete CFG bundle
Nightly Bagel failures (builds 2892/2918) traced to a companion-output
race: _handle_kv_ready_raw_outputs marked a companion done on kv_ready
alone, before its processed output was stashed, so the parent could pass
the all_companions_done gate and _forward_to_next_stage dispatched with
0/N companion outputs - silently degraded CFG conditioning, and after an
abort a TP rank desync (mismatched all_reduce sizes, NCCL watchdog kill)
in the Bagel text-embedding path.

Lifecycle now enforces one invariant - diffusion dispatch never happens
with an incomplete companion bundle:
- a companion counts as done only after set_companion_output stashes its
  processed output (_route_output is the sole done-marking site);
- the forward gate checks completeness non-destructively and re-defers
  the parent instead of popping partial state; a bundle that is done but
  gone without ever being consumed fails the request instead of
  dispatching degraded CFG (was_bundled distinguishes streaming
  re-submissions);
- a companion aborted directly (parent not in the batch) fails its
  deferred parent immediately via the existing cleanup plumbing.

Deferred to a follow-up (needs a liveness owner in the orchestrator
loop): a deadline on deferred parents and detection of a companion
replica dying silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: tzhouam <tzhouam@gmail.com>
2026-07-30 15:22:38 +08:00

335 lines
14 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Deterministic tests for the CFG-companion output lifecycle.
Regression suite for the companion-output race behind the nightly Bagel
failures: ``kv_ready`` used to mark a companion done before its processed
output was stashed, letting the parent dispatch to the diffusion stage with
0/N companion outputs (degraded CFG conditioning, and after an abort a TP
rank desync). The invariant under test: **dispatch to the diffusion stage
never happens with an incomplete companion bundle** — the parent is
re-deferred while outputs are pending and failed when the bundle can no
longer complete.
"""
from __future__ import annotations
import asyncio
from types import SimpleNamespace
import pytest
from vllm_omni.engine.cfg_companion_tracker import CfgCompanionTracker
from vllm_omni.engine.messages import AbortRequestMessage, ErrorMessage
from vllm_omni.engine.orchestrator import Orchestrator
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
# ───────── tracker-level ordering semantics ─────────
def test_companion_first_then_parent_is_not_deferred():
tracker = CfgCompanionTracker()
tracker.register_companion("p", "neg", "p__neg")
tracker.set_companion_output("p__neg", {"o": 1})
assert tracker.on_companion_completed("p__neg") is None # parent not deferred yet
# Parent arrives afterwards: bundle already complete, no defer needed.
assert tracker.all_companions_done("p")
assert tracker.get_companion_outputs("p") == [{"o": 1}]
def test_parent_first_deferred_then_companion_flushes():
tracker = CfgCompanionTracker()
tracker.register_companion("p", "neg", "p__neg")
tracker.defer_parent("p", {"parent": True}, stage_id=0)
tracker.set_companion_output("p__neg", {"o": 1})
assert tracker.on_companion_completed("p__neg") == "p"
deferred = tracker.pop_pending_parent("p")
assert deferred is not None and deferred["engine_outputs"] == {"parent": True}
def test_duplicate_companion_completion_is_idempotent():
tracker = CfgCompanionTracker()
tracker.register_companion("p", "neg", "p__neg")
tracker.defer_parent("p", {}, stage_id=0)
tracker.set_companion_output("p__neg", {"o": 1})
assert tracker.on_companion_completed("p__neg") == "p"
assert tracker.on_companion_completed("p__neg") is None # duplicate: no re-flush
assert tracker.get_companion_outputs("p") == [{"o": 1}]
def test_bundle_survives_resubmission_until_cleanup():
"""get_companion_outputs is non-destructive: a streaming re-submission of
the parent bundles the complete set again (never a partial/empty one).
Only cleanup_parent releases the outputs."""
tracker = CfgCompanionTracker()
tracker.register_companion("p", "neg", "p__neg")
tracker.set_companion_output("p__neg", {"o": 1})
assert tracker.get_companion_outputs("p") == [{"o": 1}]
assert tracker.get_companion_outputs("p") == [{"o": 1}] # re-submit: full bundle again
tracker.cleanup_parent("p")
assert tracker.get_companion_outputs("p") == []
# ───────── orchestrator-method-level lifecycle ─────────
class _FakePool:
"""Minimal stage pool satisfying _forward_to_next_stage's preamble and cleanup."""
def __init__(self, stage_type: str = "diffusion") -> None:
self.stage_type = stage_type
self.stage_client = SimpleNamespace(requires_multimodal_data=False, custom_process_input_func=None)
self.aborted: list[list[str]] = []
async def abort_requests(self, request_ids):
self.aborted.append(list(request_ids))
def release_bindings(self, request_ids):
pass
def _make_orchestrator(num_stages: int = 2) -> Orchestrator:
orch = Orchestrator.__new__(Orchestrator)
orch._cfg_tracker = CfgCompanionTracker()
orch.request_states = {}
orch.stage_pools = [_FakePool("llm"), _FakePool("diffusion")][:num_stages]
orch.output_async_queue = asyncio.Queue()
orch.async_chunk = False
orch.duplex_control_plane = None
orch._pd_kv_params = {}
orch._pd_pair = None
orch._running_counter = None
orch._transfer_emitter = None
return orch
def _req_state(final_stage_id: int = 1) -> SimpleNamespace:
return SimpleNamespace(
final_stage_id=final_stage_id,
stage_submit_ts={},
sampling_params_list=[SimpleNamespace(output_kind=None)] * (final_stage_id + 1),
streaming=SimpleNamespace(enabled=False, segment_finished=False),
running_counter_registered=False,
prompt={"prompt": "x"},
pipeline_timings={},
)
@pytest.mark.asyncio
async def test_kv_ready_does_not_mark_companion_done():
"""kv_ready alone must not count a companion as done — only the processed
output (set_companion_output) may. Guards the original race."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch.request_states["p__neg"] = _req_state()
raw = SimpleNamespace(request_id="p__neg", kv_transfer_params={"kv_ready": True})
await orch._handle_kv_ready_raw_outputs(0, SimpleNamespace(outputs=[raw]))
assert not orch._cfg_tracker.all_companions_done("p")
assert orch._cfg_tracker.pop_pending_parent("p") is None
@pytest.mark.asyncio
async def test_parent_kv_ready_defers_until_companion_outputs_stashed():
"""Parent kv_ready with an incomplete bundle defers; the companion's
processed output later flushes it (companion-first vs parent-first order
both funnel through the same defer/flush path)."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch.request_states["p"] = _req_state()
raw = SimpleNamespace(request_id="p", kv_transfer_params={"kv_ready": True})
await orch._handle_kv_ready_raw_outputs(0, SimpleNamespace(outputs=[raw]))
deferred = orch._cfg_tracker.pop_pending_parent("p")
assert deferred is not None and deferred["stage_id"] == 0
@pytest.mark.asyncio
async def test_forward_re_defers_when_bundle_incomplete():
"""The forward gate re-defers (non-destructively) instead of dispatching
with 0/N companion outputs."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch._cfg_tracker.set_companion_output("p__neg", {"o": 1}) # stashed but NOT done-marked
state = _req_state()
orch.request_states["p"] = state
await orch._forward_to_next_stage("p", 0, {"parent": True}, state)
# Not dispatched: re-deferred with the stashed output left intact.
deferred = orch._cfg_tracker.pop_pending_parent("p")
assert deferred is not None
assert orch._cfg_tracker.get_companion_outputs("p") == [{"o": 1}]
assert orch.output_async_queue.empty()
@pytest.mark.asyncio
async def test_forward_fails_request_when_bundle_lost():
"""Companions done but outputs gone without a prior bundle: the request is
failed (terminal ErrorMessage + abort) instead of dispatching degraded CFG."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch._cfg_tracker.set_companion_output("p__neg", {"o": 1})
orch._cfg_tracker.on_companion_completed("p__neg")
orch._cfg_tracker._companion_outputs.pop("p__neg") # simulate lost output (unreachable normally)
state = _req_state()
orch.request_states["p"] = state
await orch._forward_to_next_stage("p", 0, {"parent": True}, state)
msg = orch.output_async_queue.get_nowait()
assert isinstance(msg, ErrorMessage)
assert msg.request_id == "p"
assert "incomplete" in msg.error
assert "p" not in orch.request_states
assert any("p" in batch for pool in orch.stage_pools for batch in pool.aborted)
@pytest.mark.asyncio
async def test_companion_abort_fails_deferred_parent():
"""A companion aborted directly (parent not in the batch) can never
complete the bundle — the deferred parent is failed immediately."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch._cfg_tracker.defer_parent("p", {"parent": True}, stage_id=0)
orch.request_states["p"] = _req_state()
await orch._handle_abort(AbortRequestMessage(request_ids=["p__neg"]))
msg = orch.output_async_queue.get_nowait()
assert isinstance(msg, ErrorMessage)
assert msg.request_id == "p"
assert "p__neg" in msg.error
assert "p" not in orch.request_states
assert not orch._cfg_tracker.is_companion("p__neg")
# Idempotent: a repeated abort of the same companion is a no-op.
await orch._handle_abort(AbortRequestMessage(request_ids=["p__neg"]))
assert orch.output_async_queue.empty()
@pytest.mark.asyncio
async def test_companion_abort_after_completion_does_not_fail_parent():
"""A late/duplicate abort of a companion whose output is already stashed
must not cancel the (possibly already dispatched) parent."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch._cfg_tracker.set_companion_output("p__neg", {"o": 1})
orch._cfg_tracker.on_companion_completed("p__neg")
orch.request_states["p"] = _req_state()
await orch._handle_abort(AbortRequestMessage(request_ids=["p__neg"]))
assert orch.output_async_queue.empty() # no parent failure emitted
assert "p" in orch.request_states # parent untouched
assert orch._cfg_tracker.get_companion_outputs("p") == [{"o": 1}] # bundle intact
class _CapturedDispatchError(Exception):
"""Sentinel raised by the capture hook to stop the forward after bundling."""
@pytest.mark.asyncio
async def test_repeated_forward_bundles_full_set_again():
"""A second forward of the same parent (streaming terminal update) must
dispatch the complete [parent, *companions] bundle again — never [parent]
alone. Captured at the diffusion input hook, i.e. actual dispatch content."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch._cfg_tracker.set_companion_output("p__neg", {"o": 1})
orch._cfg_tracker.on_companion_completed("p__neg")
state = _req_state()
orch.request_states["p"] = state
captured: list[list] = []
def _capture(source_outputs, prompt, requires_multimodal_data):
captured.append(list(source_outputs))
raise _CapturedDispatchError
orch.stage_pools[1].stage_client.custom_process_input_func = _capture
for _ in range(2):
with pytest.raises(_CapturedDispatchError):
await orch._forward_to_next_stage("p", 0, {"parent": True}, state)
assert captured == [[{"parent": True}, {"o": 1}], [{"parent": True}, {"o": 1}]]
@pytest.mark.asyncio
async def test_replica_loss_cleanup_releases_parent_tracker_state():
"""Replica-loss/membership teardown calls _cleanup_request_ids directly
(no cleanup_parent at the call site): the parent's tracker state must be
released and its companions folded into the same cleanup batch."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch._cfg_tracker.set_companion_output("p__neg", {"o": 1})
orch._cfg_tracker.defer_parent("p", {"parent": True}, stage_id=0)
orch.request_states["p"] = _req_state()
await orch._cleanup_request_ids(["p"], abort=True)
assert not orch._cfg_tracker.has_companions("p")
assert not orch._cfg_tracker.is_companion("p__neg")
assert orch._cfg_tracker.get_companion_outputs("p") == []
assert orch._cfg_tracker.pop_pending_parent("p") is None
aborted = {rid for pool in orch.stage_pools for b in pool.aborted for rid in b}
assert {"p", "p__neg"} <= aborted
@pytest.mark.asyncio
async def test_replica_loss_of_companion_fails_deferred_parent():
"""A companion torn down by replica loss before its output arrived means
the parent can never complete its bundle — the cleanup fails the parent."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch._cfg_tracker.defer_parent("p", {"parent": True}, stage_id=0)
orch.request_states["p"] = _req_state()
await orch._cleanup_request_ids(["p__neg"])
msg = orch.output_async_queue.get_nowait()
assert isinstance(msg, ErrorMessage)
assert msg.request_id == "p"
assert "p__neg" in msg.error
assert "p" not in orch.request_states
assert not orch._cfg_tracker.has_companions("p")
@pytest.mark.asyncio
async def test_finished_companion_cleanup_preserves_parent_bundle():
"""The normal companion-finished cleanup (_route_output cleans just the
companion id) must not disturb the parent's stashed bundle."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch._cfg_tracker.set_companion_output("p__neg", {"o": 1})
orch._cfg_tracker.on_companion_completed("p__neg")
orch.request_states["p"] = _req_state()
await orch._cleanup_request_ids(["p__neg"])
assert orch.output_async_queue.empty()
assert "p" in orch.request_states
assert orch._cfg_tracker.get_companion_outputs("p") == [{"o": 1}]
@pytest.mark.asyncio
async def test_parent_abort_still_expands_to_companions():
"""Regression-guard the existing behavior: aborting the parent aborts its
companions and clears tracker state, without emitting an error."""
orch = _make_orchestrator()
orch._cfg_tracker.register_companion("p", "neg", "p__neg")
orch._cfg_tracker.defer_parent("p", {"parent": True}, stage_id=0)
orch.request_states["p"] = _req_state()
await orch._handle_abort(AbortRequestMessage(request_ids=["p"]))
assert orch.output_async_queue.empty()
assert "p" not in orch.request_states
assert not orch._cfg_tracker.is_companion("p__neg")
aborted = {rid for pool in orch.stage_pools for batch in pool.aborted for rid in batch}
assert {"p", "p__neg"} <= aborted