Files
vllm-omni/tests/model_executor/test_cfg_pairing.py
T
2026-08-18 04:33:13 +00:00

390 lines
14 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for the model-neutral CFG request-pairing scheduler patches.
The patches keep a cond/uncond request pair step-locked inside one engine:
admitted together, kept at equal ``num_computed_tokens``, finished together.
They are installed onto a lightweight fake scheduler here; vLLM's real
``Scheduler`` needs a full engine (and a GPU) to construct.
"""
from __future__ import annotations
from collections import deque
from dataclasses import dataclass, field
from types import SimpleNamespace
from typing import Any
import pytest
from vllm_omni.model_executor.models.common import cfg_pairing
from vllm_omni.model_executor.models.common.cfg_pairing import (
_equalize_cfg_pair_progress,
_patch_scheduler,
cfg_uncond_suffixes,
register_cfg_uncond_suffix,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
# ───────── fakes ─────────
class _FakeRequest:
"""Minimal stand-in for ``vllm.v1.request.Request`` (identity equality)."""
def __init__(self, request_id: str, extra_args: dict[str, Any] | None = None, step_tokens: int = 1) -> None:
self.request_id = request_id
self.sampling_params = SimpleNamespace(extra_args=extra_args)
self.num_computed_tokens = 0
self.step_tokens = step_tokens
self.skip_reading_prefix_cache = False
class _FakeWaitingQueue(deque):
"""FCFS waiting queue: vLLM's is also a ``deque`` subclass."""
def remove_requests(self, requests: list[_FakeRequest]) -> None:
keep = [req for req in self if not any(req is dropped for dropped in requests)]
self.clear()
self.extend(keep)
def prepend_request(self, request: _FakeRequest) -> None:
self.appendleft(request)
@dataclass
class _FakeSchedulerOutput:
num_scheduled_tokens: dict[str, int] = field(default_factory=dict)
total_num_scheduled_tokens: int = 0
def _make_scheduler_cls() -> type:
"""Fresh class per test so the in-place patches never stack."""
class FakeScheduler:
def __init__(self) -> None:
self.waiting = _FakeWaitingQueue()
self.running: list[_FakeRequest] = []
self.requests: dict[str, _FakeRequest] = {}
self.finished: list[set[str]] = []
self.observed_waiting: list[list[str]] = []
self.max_num_scheduled_tokens = 512
self.scheduler_config = SimpleNamespace(long_prefill_token_threshold=4096)
def add_request(self, request: _FakeRequest) -> None:
self.requests[request.request_id] = request
self.waiting.append(request)
def finish_requests(self, request_ids: Any, finished_status: Any) -> None:
if request_ids is None:
ids = set(self.requests)
elif isinstance(request_ids, str):
ids = {request_ids}
else:
ids = set(request_ids)
self.finished.append(ids)
self.running = [req for req in self.running if req.request_id not in ids]
for req_id in ids:
self.requests.pop(req_id, None)
def schedule(self) -> _FakeSchedulerOutput:
self.observed_waiting.append([req.request_id for req in self.waiting])
while self.waiting:
self.running.append(self.waiting.popleft())
output = _FakeSchedulerOutput()
for req in self.running:
req.num_computed_tokens += req.step_tokens
output.num_scheduled_tokens[req.request_id] = req.step_tokens
output.total_num_scheduled_tokens += req.step_tokens
return output
return FakeScheduler
@pytest.fixture
def scheduler() -> Any:
cls = _make_scheduler_cls()
_patch_scheduler(cls)
return cls()
@pytest.fixture(autouse=True)
def _restore_suffix_registry():
original = set(cfg_pairing._UNCOND_REQUEST_ID_SUFFIXES)
yield
cfg_pairing._UNCOND_REQUEST_ID_SUFFIXES.clear()
cfg_pairing._UNCOND_REQUEST_ID_SUFFIXES.update(original)
def _add_pair(
scheduler: Any,
pair_id: str | None = None,
base_id: str = "req-1",
suffix: str = "__cfg_uncond",
) -> tuple[_FakeRequest, _FakeRequest]:
cond_extra: dict[str, Any] = {"cfg_role": "cond"}
uncond_extra: dict[str, Any] = {"cfg_role": "uncond"}
if pair_id is not None:
cond_extra["cfg_pair_id"] = pair_id
uncond_extra["cfg_pair_id"] = pair_id
cond = _FakeRequest(base_id, cond_extra)
uncond = _FakeRequest(f"{base_id}{suffix}", uncond_extra)
scheduler.add_request(cond)
scheduler.add_request(uncond)
return cond, uncond
# ───────── pair-id derivation ─────────
def test_pair_id_is_derived_from_request_id_when_absent(scheduler: Any) -> None:
cond, uncond = _add_pair(scheduler)
assert scheduler._cfg_pairs == {"req-1": {"cond": cond.request_id, "uncond": uncond.request_id}}
assert scheduler._cfg_req_to_pair == {cond.request_id: "req-1", uncond.request_id: "req-1"}
def test_registered_companion_suffix_is_stripped(scheduler: Any) -> None:
register_cfg_uncond_suffix("::negative")
assert "::negative" in cfg_uncond_suffixes()
cond, uncond = _add_pair(scheduler, base_id="music-7", suffix="::negative")
assert scheduler._cfg_pairs == {"music-7": {"cond": cond.request_id, "uncond": uncond.request_id}}
def test_register_rejects_empty_suffix() -> None:
with pytest.raises(ValueError):
register_cfg_uncond_suffix("")
def test_uncond_without_known_suffix_keeps_its_own_request_id(scheduler: Any) -> None:
# Degenerate input: the companion never joins the cond pair, so the pair
# stays incomplete and the hold budget eventually releases it.
scheduler.add_request(_FakeRequest("solo-uncond", {"cfg_role": "uncond"}))
assert scheduler._cfg_pairs == {"solo-uncond": {"uncond": "solo-uncond"}}
def test_explicit_pair_id_takes_precedence(scheduler: Any) -> None:
cond, uncond = _add_pair(scheduler, pair_id="pair-A")
assert scheduler._cfg_pairs == {"pair-A": {"cond": cond.request_id, "uncond": uncond.request_id}}
assert cond.request_id not in scheduler._cfg_pairs
assert scheduler._cfg_req_to_pair[uncond.request_id] == "pair-A"
def test_cfg_requests_skip_prefix_cache_reads(scheduler: Any) -> None:
cond, uncond = _add_pair(scheduler)
assert cond.skip_reading_prefix_cache is True
assert uncond.skip_reading_prefix_cache is True
# ───────── admission holding ─────────
def test_lone_member_is_held_then_released(scheduler: Any) -> None:
scheduler.add_request(_FakeRequest("req-1", {"cfg_role": "cond"}))
scheduler.schedule()
assert scheduler.observed_waiting == [[]] # the base scheduler saw nothing
assert [req.request_id for req in scheduler.waiting] == ["req-1"]
assert scheduler.running == []
def test_complete_pair_is_admitted_together(scheduler: Any) -> None:
scheduler.add_request(_FakeRequest("req-1", {"cfg_role": "cond"}))
scheduler.schedule()
scheduler.add_request(_FakeRequest("req-1__cfg_uncond", {"cfg_role": "uncond"}))
scheduler.schedule()
assert scheduler.observed_waiting[-1] == ["req-1", "req-1__cfg_uncond"]
assert [req.request_id for req in scheduler.running] == ["req-1", "req-1__cfg_uncond"]
assert list(scheduler.waiting) == []
def test_hold_budget_expiry_drops_the_stale_pair(scheduler: Any, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(cfg_pairing, "_MAX_PAIR_HOLD_STEPS", 2)
scheduler.add_request(_FakeRequest("req-1", {"cfg_role": "cond"}))
scheduler.schedule()
scheduler.schedule()
assert scheduler.running == []
assert scheduler._cfg_pairs # still hoping for the partner
scheduler.schedule()
assert scheduler._cfg_pairs == {}
assert scheduler._cfg_req_to_pair == {}
assert [req.request_id for req in scheduler.running] == ["req-1"]
def test_waiting_queue_reorder_puts_partners_adjacent(scheduler: Any) -> None:
scheduler.add_request(_FakeRequest("req-1", {"cfg_role": "cond"}))
scheduler.add_request(_FakeRequest("other", None))
scheduler.add_request(_FakeRequest("req-1__cfg_uncond", {"cfg_role": "uncond"}))
scheduler.schedule()
assert scheduler.observed_waiting[-1] == ["req-1", "req-1__cfg_uncond", "other"]
# ───────── progress equalization ─────────
def _equalize_fixture(
cond_computed: int,
uncond_computed: int,
cond_sched: int,
uncond_sched: int,
) -> tuple[Any, _FakeSchedulerOutput, _FakeRequest, _FakeRequest]:
cond = _FakeRequest("req-1", {"cfg_role": "cond"})
uncond = _FakeRequest("req-1__cfg_uncond", {"cfg_role": "uncond"})
cond.num_computed_tokens = cond_computed
uncond.num_computed_tokens = uncond_computed
fake = SimpleNamespace(
_cfg_pairs={"req-1": {"cond": cond.request_id, "uncond": uncond.request_id}},
requests={cond.request_id: cond, uncond.request_id: uncond},
)
output = _FakeSchedulerOutput(
num_scheduled_tokens={cond.request_id: cond_sched, uncond.request_id: uncond_sched},
total_num_scheduled_tokens=cond_sched + uncond_sched,
)
return fake, output, cond, uncond
def test_equalize_pulls_the_ahead_member_back() -> None:
fake, output, cond, uncond = _equalize_fixture(10, 6, 5, 4)
_equalize_cfg_pair_progress(fake, output)
assert cond.num_computed_tokens == 6
assert uncond.num_computed_tokens == 6
assert output.num_scheduled_tokens == {cond.request_id: 1, uncond.request_id: 4}
assert output.total_num_scheduled_tokens == 5
def test_equalize_is_a_noop_when_the_pair_is_already_aligned() -> None:
fake, output, cond, uncond = _equalize_fixture(8, 8, 4, 4)
_equalize_cfg_pair_progress(fake, output)
assert (cond.num_computed_tokens, uncond.num_computed_tokens) == (8, 8)
assert output.total_num_scheduled_tokens == 8
def test_equalize_skips_an_infeasible_rollback() -> None:
# Clawing back 4 tokens would leave the cond row with 0 scheduled tokens,
# which the scheduler output cannot express; leave the pair to _drop_split_pairs.
fake, output, cond, uncond = _equalize_fixture(10, 6, 4, 4)
_equalize_cfg_pair_progress(fake, output)
assert cond.num_computed_tokens == 10
assert output.total_num_scheduled_tokens == 8
# ───────── finish ─────────
def test_finish_requests_finishes_the_partner(scheduler: Any) -> None:
cond, uncond = _add_pair(scheduler)
scheduler.schedule()
scheduler.finish_requests(cond.request_id, "FINISHED_STOPPED")
assert scheduler.finished == [{cond.request_id, uncond.request_id}]
assert scheduler._cfg_pairs == {}
assert scheduler._cfg_req_to_pair == {}
assert scheduler.running == []
def test_finish_all_requests_clears_the_registry(scheduler: Any) -> None:
_add_pair(scheduler)
scheduler.schedule()
scheduler.finish_requests(None, "FINISHED_ABORTED")
assert scheduler._cfg_pairs == {}
assert scheduler._cfg_req_to_pair == {}
# ───────── non-CFG traffic ─────────
def test_plain_requests_are_untouched(scheduler: Any) -> None:
scheduler.add_request(_FakeRequest("plain-1", None))
scheduler.add_request(_FakeRequest("plain-2", {"seed": 3}))
output = scheduler.schedule()
assert scheduler._cfg_pairs == {}
assert scheduler.observed_waiting == [["plain-1", "plain-2"]]
assert output.num_scheduled_tokens == {"plain-1": 1, "plain-2": 1}
assert output.total_num_scheduled_tokens == 2
# No pair ever existed, so the hold bookkeeping is never allocated.
assert not hasattr(scheduler, "_cfg_hold_counts")
def test_default_suffix_matches_the_audex_stage_processor() -> None:
from vllm_omni.model_executor.stage_input_processors.audex import CFG_UNCOND_SUFFIX
assert CFG_UNCOND_SUFFIX in cfg_uncond_suffixes()
def test_audex_cfg_module_still_exports_the_patch_entry_points() -> None:
from vllm_omni.model_executor.models.audex.cfg import apply_cfg_patches, cfg_patches_applied
assert apply_cfg_patches is cfg_pairing.apply_cfg_patches
assert cfg_patches_applied is cfg_pairing.cfg_patches_applied
# ───────── install gate ─────────
def test_gate_triggers_on_a_cfg_logits_processor() -> None:
from vllm_omni.engine.stage_init_utils import _stage_declares_cfg_pairs
class AudexCFGLogitsProcessor:
pass
assert _stage_declares_cfg_pairs(SimpleNamespace(logits_processors=[AudexCFGLogitsProcessor]))
# vLLM keeps unresolved entries as dotted strings.
assert _stage_declares_cfg_pairs(
SimpleNamespace(logits_processors=["vllm_omni.model_executor.models.audex.cfg:AudexCFGLogitsProcessor"])
)
def test_gate_triggers_on_a_declared_cfg_role_default() -> None:
from vllm_omni.engine.stage_init_utils import _stage_declares_cfg_pairs
assert _stage_declares_cfg_pairs(
SimpleNamespace(logits_processors=None, sampling_extra_args_keys=("cfg_role", "cfg_scale"))
)
def test_gate_stays_off_for_non_cfg_stages() -> None:
from vllm_omni.engine.stage_init_utils import _stage_declares_cfg_pairs
assert not _stage_declares_cfg_pairs(SimpleNamespace(logits_processors=[], sampling_extra_args_keys=()))
assert not _stage_declares_cfg_pairs(
SimpleNamespace(logits_processors=["some.module:MinPLogitsProcessor"], sampling_extra_args_keys=("tta_rvq",))
)
assert not _stage_declares_cfg_pairs(SimpleNamespace())
def test_sampling_extra_args_keys_are_derived_from_stage_defaults() -> None:
from vllm_omni.engine.stage_init_utils import _sampling_extra_args_keys
assert _sampling_extra_args_keys({"extra_args": {"cfg_scale": 1.5, "cfg_role": "cond"}}) == (
"cfg_role",
"cfg_scale",
)
assert _sampling_extra_args_keys({"temperature": 0.1}) == ()
assert _sampling_extra_args_keys(None) == ()