# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Unit tests for vllm_omni/core/prefix_cache (manager + controller + pool). CPU-only: the controller runs in eager mode. Uses a fake group view, so no vLLM runtime is required (runnable without a vllm install via ``pytest --confcutdir=tests/core``). """ import ast import logging import sys import threading import time from pathlib import Path from types import SimpleNamespace import pytest import torch try: # pragma: no cover - shim only matters on vllm-less dev machines import vllm # noqa: F401 except ModuleNotFoundError: # Bypass vllm_omni/__init__ (which imports vllm): register namespace # parents so the pure-torch prefix_cache subpackage imports directly. _root = Path(__file__).resolve().parents[2] for _pkg in ("vllm_omni", "vllm_omni.core", "vllm_omni.utils"): if _pkg not in sys.modules: _m = __import__("types").ModuleType(_pkg) _m.__path__ = [str(_root / _pkg.replace(".", "/"))] sys.modules[_pkg] = _m _vllm = __import__("types").ModuleType("vllm") _vllm_logger = __import__("types").ModuleType("vllm.logger") _vllm_logger.init_logger = logging.getLogger _vllm.logger = _vllm_logger sys.modules["vllm"] = _vllm sys.modules["vllm.logger"] = _vllm_logger from vllm_omni.core.prefix_cache.controller import StagingBufferHolder from vllm_omni.core.prefix_cache.group_view import ( FullAttentionGroupView, check_prefix_cache_kv_groups, stage_prefix_cache_config, ) from vllm_omni.core.prefix_cache.interface import ( HIDDEN_KEY, ModelCachePolicy, OmniPrefixCacheStagingTimeoutError, OmniPrefixCacheUnmatchError, PrefixCacheConfig, WriteSchedule, ) from vllm_omni.core.prefix_cache.manager import ( OmniPrefixCacheManager, _is_step_token_tensor, _snapshot_leftover_mm_cpu, ) pytestmark = [pytest.mark.core_model, pytest.mark.cpu] NUM_BLOCKS = 16 BLOCK_SIZE = 4 HIDDEN = 8 DTYPE = torch.float32 class FakeView: """Duck-typed group view backed by plain dicts.""" def __init__(self): self.block_size = BLOCK_SIZE self.req_blocks: dict[str, list[int]] = {} self.order: list[str] = [] self.computed: dict[str, int] = {} self.step_slot_mapping: torch.Tensor | None = None def slots_for(self, req_id, token_start, token_end): blocks = self.req_blocks[req_id] slots = [] for pos in range(token_start, token_end): slots.append(blocks[pos // BLOCK_SIZE] * BLOCK_SIZE + pos % BLOCK_SIZE) return torch.tensor(slots, dtype=torch.long) def batch_req_ids(self) -> list[str]: return list(self.order) def step_slots_cpu(self, req_ids, num_scheduled) -> torch.Tensor: parts = [] for r in req_ids: n = int(num_scheduled.get(r, 0)) if n <= 0: continue start = self.computed.get(r, 0) parts.append(self.slots_for(r, start, start + n)) return torch.cat(parts) if parts else torch.empty((0,), dtype=torch.long) class FakeNewReq: def __init__(self, req_id, num_computed_tokens=0, block_ids=None): self.req_id = req_id self.num_computed_tokens = num_computed_tokens self.block_ids = block_ids class FakeSchedOut: def __init__(self, new_reqs=(), finished=(), num_scheduled=None): self.scheduled_new_reqs = list(new_reqs) self.finished_req_ids = set(finished) self.num_scheduled_tokens = dict(num_scheduled or {}) def make_manager(view=None, policy=None, **cfg_kwargs) -> tuple[OmniPrefixCacheManager, FakeView]: view = view or FakeView() config = PrefixCacheConfig(num_blocks=NUM_BLOCKS, block_size=BLOCK_SIZE, **cfg_kwargs) mgr = OmniPrefixCacheManager(config, view, eager=True) if policy is not None: mgr.register_policy(policy) return mgr, view def run_step( mgr, view, reqs: dict[str, tuple[list[int], int, int]], new_hits=None, finished=(), mm=None, num_tokens_padded=None, ) -> int: """One step: reqs = req_id -> (blocks, sched_start_pos, sched_tokens).""" view.order = list(reqs.keys()) new_reqs = [] num_sched = {} slot_parts = [] hidden_parts = [] for req_id, (blocks, start_pos, sched) in reqs.items(): view.req_blocks[req_id] = blocks view.computed[req_id] = start_pos num_sched[req_id] = sched slots = view.slots_for(req_id, start_pos, start_pos + sched) slot_parts.append(slots) hidden_parts.append(slots.to(DTYPE).unsqueeze(1).expand(sched, HIDDEN).clone()) hit = (new_hits or {}).get(req_id, 0) new_reqs.append(FakeNewReq(req_id, num_computed_tokens=hit, block_ids=[list(blocks)])) view.step_slot_mapping = torch.cat(slot_parts) hidden = torch.cat(hidden_parts) sched_out = FakeSchedOut(new_reqs=new_reqs, finished=finished, num_scheduled=num_sched) mgr.new_step_starts(sched_out) n = int(view.step_slot_mapping.numel()) padded = n if num_tokens_padded is None else int(num_tokens_padded) return mgr.save_outputs(hidden, mm or {}, num_tokens_unpadded=n, num_tokens_padded=padded) def expected_rows(slots: torch.Tensor) -> torch.Tensor: return slots.to(DTYPE).unsqueeze(1).expand(slots.numel(), HIDDEN) def plan_fetch(mgr, slots, key, *, req_id): with mgr._state_lock: src = mgr._slot_ref(slots, key, req_id) with torch.inference_mode(): return mgr._fetch_source(src) def _assert_leftover_shapes(inp, got, n: int) -> None: """Every leftover tensor keeps its shape unless dim0 is this step's token count.""" if isinstance(inp, torch.Tensor): if inp.ndim >= 1 and n > 0 and inp.shape[0] == n: assert got.shape == (n, *inp.shape[1:]) else: assert got.shape == inp.shape return if isinstance(inp, dict): assert set(got) == set(inp) for k in inp: _assert_leftover_shapes(inp[k], got[k], n) return if isinstance(inp, (list, tuple)): assert len(got) == len(inp) for a, b in zip(inp, got): _assert_leftover_shapes(a, b, n) def _module_scope_imported_modules(path: Path) -> list[str]: """Top-level import module names, skipping ``if TYPE_CHECKING`` blocks.""" tree = ast.parse(path.read_text()) names: list[str] = [] def _from_stmt(node: ast.AST) -> None: if isinstance(node, ast.Import): names.extend(alias.name for alias in node.names) elif isinstance(node, ast.ImportFrom) and node.module: names.append(node.module) for node in tree.body: if isinstance(node, ast.If) and isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING": continue _from_stmt(node) return names def test_prefix_cache_package_has_no_module_scope_vllm_import(): """This package must import without vllm. See the package docstring.""" pkg = Path(__file__).resolve().parents[2] / "vllm_omni" / "core" / "prefix_cache" leaked: list[str] = [] for path in sorted(pkg.glob("*.py")): for mod in _module_scope_imported_modules(path): if mod == "vllm" or mod.startswith("vllm."): leaked.append(f"{path.name}: {mod}") assert not leaked, leaked def test_no_hit_passthrough(): mgr, view = make_manager() sid = run_step(mgr, view, {"a": ([0, 1], 0, 8)}) outs = mgr.materialize(sid, ["a"]) assert torch.equal(outs.hidden_states["a"], expected_rows(view.slots_for("a", 0, 8))) def test_hit_merge_from_mirror(): mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}) mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}, finished=["a"]) outs = mgr.materialize(s2, ["b"]) merged = outs.hidden_states["b"] assert merged.shape == (12, HIDDEN) assert torch.equal(merged[:8], expected_rows(view.slots_for("b", 0, 8))) assert torch.equal(merged[8:], expected_rows(view.slots_for("b", 8, 12))) @pytest.mark.parametrize("a_finished_before_b", [False, True]) def test_partial_match_hits_shared_prefix_and_writes_divergent_block(a_finished_before_b): """`a` occupies [0, 1, 2]; `b` shares the first two blocks and diverges on the third ([0, 1, 3]). `b` must merge 8 hit rows from the shared blocks plus its own 4 rows, land block 3 in the pool, and leave `a`'s block 2 untouched — whether `a` already finished or is still running.""" mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0, 1, 2], 0, 12)}) mgr.materialize(s1, ["a"]) a_tail = view.slots_for("a", 8, 12) assert torch.equal(plan_fetch(mgr, a_tail, HIDDEN_KEY, req_id="a"), expected_rows(a_tail)) finished = ["a"] if a_finished_before_b else () s2 = run_step(mgr, view, {"b": ([0, 1, 3], 8, 4)}, new_hits={"b": 8}, finished=finished) outs = mgr.materialize(s2, ["b"]) merged = outs.hidden_states["b"] assert merged.shape == (12, HIDDEN) assert torch.equal(merged[:8], expected_rows(view.slots_for("b", 0, 8))) assert torch.equal(merged[8:], expected_rows(view.slots_for("b", 8, 12))) # Divergent third block landed; the shared prefix and a's own tail did not move. b_tail = view.slots_for("b", 8, 12) assert torch.equal(plan_fetch(mgr, b_tail, HIDDEN_KEY, req_id="b"), expected_rows(b_tail)) assert torch.equal(plan_fetch(mgr, a_tail, HIDDEN_KEY, req_id="a"), expected_rows(a_tail)) shared = view.slots_for("a", 0, 8) assert torch.equal(plan_fetch(mgr, shared, HIDDEN_KEY, req_id="a"), expected_rows(shared)) if not a_finished_before_b: # `a` finishes later; releasing its task must not disturb b's rows. s3 = run_step(mgr, view, {"b": ([0, 1, 3, 4], 12, 1)}, finished=["a"]) mgr.materialize(s3, ["b"]) assert torch.equal(plan_fetch(mgr, b_tail, HIDDEN_KEY, req_id="b"), expected_rows(b_tail)) def test_live_req_reentering_new_reqs_is_not_a_hit(): """async_chunk continuation: the same id re-enters scheduled_new_reqs with num_computed_tokens equal to what it computed itself. No hit span is opened and materialize returns only this step's rows (parity with the pre-refactor cache; a delivered_upto span is Phase 2). Once the id has finished, the same id is a new request again and its hit is merged.""" mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}) mgr.materialize(s1, ["a"]) # Next chunk arrives: `a` is scheduled again as a "new" request at 8. s2 = run_step(mgr, view, {"a": ([0, 1, 2], 8, 4)}, new_hits={"a": 8}) assert not mgr._step_ctxs[s2].hits outs = mgr.materialize(s2, ["a"]) assert torch.equal(outs.hidden_states["a"], expected_rows(view.slots_for("a", 8, 12))) # `a` finishes; a later request reusing the id is new and does hit. s3 = run_step(mgr, view, {"b": ([3], 0, 2)}, finished=["a"]) mgr.materialize(s3, ["b"]) s4 = run_step(mgr, view, {"a": ([0, 1, 4], 8, 4)}, new_hits={"a": 8}, finished=["b"]) assert mgr._step_ctxs[s4].hits == {"a": (8, [0, 1])} outs = mgr.materialize(s4, ["a"]) assert outs.hidden_states["a"].shape == (12, HIDDEN) assert torch.equal(outs.hidden_states["a"][:8], expected_rows(view.slots_for("a", 0, 8))) def test_join_next_step_hit_waits_done_then_reads_pool(): """CPU stand-in for the non-eager path: dispatch holds the task instead of writing the pool. A same-step hit must join(done), drain, then read the pool.""" mgr, view = make_manager() real_run = mgr._controller._run_eager real_join = mgr._controller.join def join_then_scatter(tids): # Stand-in committer: the write lands only when someone joins it. for tid in tids: task = mgr._controller.get_task(tid) if task is not None and not task.done.is_set(): real_run(task) real_join(tids) mgr._controller.dispatch = lambda tasks: None mgr._controller.join = join_then_scatter view.req_blocks["a"] = [0, 1] sid = run_step( mgr, view, {"a": ([0, 1], 0, 8), "b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}, ) outs = mgr.materialize(sid, ["a", "b"]) assert torch.equal(outs.hidden_states["b"][:8], expected_rows(view.slots_for("b", 0, 8))) slots = view.slots_for("a", 0, 8) assert torch.equal(mgr._pool.rows(HIDDEN_KEY, slots), expected_rows(slots)) def test_same_step_hit_reads_in_transit(): mgr, view = make_manager() view.req_blocks["a"] = [0, 1] sid = run_step( mgr, view, {"a": ([0, 1], 0, 8), "b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}, ) outs = mgr.materialize(sid, ["a", "b"]) assert torch.equal(outs.hidden_states["b"][:8], expected_rows(view.slots_for("b", 0, 8))) def test_absent_hit_fails_fast(): mgr, view = make_manager() view.req_blocks["c"] = [5, 6] sid = run_step(mgr, view, {"c": ([5, 6, 7], 8, 4)}, new_hits={"c": 8}) d2h = mgr._step_ctxs[sid].d2h with pytest.raises(OmniPrefixCacheUnmatchError): mgr.materialize(sid, ["c"]) if d2h is not None: assert not mgr._controller._staging_pool._busy[d2h.staging_slot] def test_hit_not_block_aligned_fails_at_register(): mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}) mgr.materialize(s1, ["a"]) with pytest.raises(OmniPrefixCacheUnmatchError, match="prefix hit not block aligned"): run_step(mgr, view, {"b": ([0, 1, 2], 8, 4)}, new_hits={"b": 6}) def test_mm_cached_key_merge(): mgr, view = make_manager() feat = 3 mm1 = {"talker.h": torch.arange(8 * feat, dtype=DTYPE).reshape(8, feat)} s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}, mm=mm1) mgr.materialize(s1, ["a"]) mm2 = {"talker.h": torch.full((4, feat), 7.0)} s2 = run_step(mgr, view, {"b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}, finished=["a"], mm=mm2) outs = mgr.materialize(s2, ["b"]) merged = outs.mm_outputs["talker.h"]["b"] assert merged.shape == (12, feat) assert torch.equal(merged[:8], mm1["talker.h"]) assert torch.equal(merged[8:], mm2["talker.h"]) def test_codes_audio_matches_ordinary_mm_key(): """Without a deferred policy, codes.audio is an ordinary mm key (dim0 = tokens).""" mgr, view = make_manager() step1 = torch.arange(8 * 2, dtype=DTYPE).reshape(8, 2) s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}, mm={"codes.audio": step1}) assert torch.equal(mgr.materialize(s1, ["a"]).mm_outputs["codes.audio"]["a"], step1) step2 = torch.full((4, 2), 7.0) s2 = run_step(mgr, view, {"b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}, finished=["a"], mm={"codes.audio": step2}) merged = mgr.materialize(s2, ["b"]).mm_outputs["codes.audio"]["b"] assert torch.equal(merged, torch.cat([step1, step2], dim=0)) def test_unpadded_mm_registers_on_padded_step(): """Qwen3-TTS codes.audio is scheduled-length; cudagraph makes padded > n. Immediate-only `shape[0] == padded` would skip ensure_key, drop the rows into leftover, and silently serve hits without a cached prefix. """ mgr, view = make_manager() audio = torch.arange(8 * 2, dtype=DTYPE).reshape(8, 2) s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}, mm={"codes.audio": audio}, num_tokens_padded=16) assert mgr._pool.has_key("codes.audio") assert "codes.audio" not in mgr._step_ctxs[s1].mm_cpu_snapshot assert torch.equal(mgr.materialize(s1, ["a"]).mm_outputs["codes.audio"]["a"], audio) s2 = run_step( mgr, view, {"b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}, finished=["a"], mm={"codes.audio": torch.full((4, 2), 3.0)}, num_tokens_padded=16, ) merged = mgr.materialize(s2, ["b"]).mm_outputs["codes.audio"]["b"] assert torch.equal(merged[:8], audio) assert torch.equal(merged[8:], torch.full((4, 2), 3.0)) def test_split_step_outputs_routes_immediate_deferred_leftover(): """One split: immediate device→host, deferred write + leftover read, leftover only.""" policy = ModelCachePolicy(needs_full_hidden_states=True, deferred_keys=frozenset({"codes.audio"})) mgr, view = make_manager(policy=policy) view.order = ["a"] view.req_blocks["a"] = [0] view.computed["a"] = 0 n = 4 mgr.new_step_starts(FakeSchedOut(new_reqs=[FakeNewReq("a")], num_scheduled={"a": n})) hidden = torch.ones(n, HIDDEN) mm = { "codes.audio": torch.full((n, 2), 5.0), "codes.ref": torch.zeros(15, 2), "tok": torch.full((n, 2), 7.0), } step_outputs = mgr._split_step_outputs( hidden, mm, n, n, slots_cpu=view.step_slots_cpu(["a"], {"a": n}), req_order=["a"], num_sched={"a": n}, query_start={"a": 0}, ) assert HIDDEN_KEY in step_outputs.immediate and "tok" in step_outputs.immediate assert "codes.audio" not in step_outputs.immediate and "codes.ref" not in step_outputs.immediate assert "codes.audio" in step_outputs.leftover and "codes.ref" in step_outputs.leftover assert "tok" not in step_outputs.leftover assert len(step_outputs.deferred_chunks) == 1 assert step_outputs.deferred_chunks[0][0] == "a" assert "codes.audio" in step_outputs.deferred_chunks[0][1].tensors def test_leftover_snapshot_preserves_non_token_major_shapes(): """P1: leftover copy must not use `shape[0] >= n` as a slice predicate. Tensors whose first dim is this step's token count (`shape[0] == n`) may be sliced; every other tensor — including `codes.ref` with ref_len >> n, and list-held tensors — must keep the input shape. """ n = 4 mm = { "codes.ref": torch.arange(30, dtype=DTYPE).reshape(15, 2), "tags": [torch.ones(3, 5), torch.zeros(7)], "tokenish": torch.arange(n * 2, dtype=DTYPE).reshape(n, 2), "scalar": torch.tensor(3.0), } out, event = _snapshot_leftover_mm_cpu(mm, set(), n) assert event is None _assert_leftover_shapes(mm, out, n) assert torch.equal(out["codes.ref"], mm["codes.ref"]) assert out["codes.ref"].shape == (15, 2) @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA leftover copy") def test_leftover_snapshot_cuda_is_pinned_and_event_ordered(): """CUDA leftover lands pinned via a non-blocking copy; values are exact once the event is waited.""" n = 4 mm = { "codes.audio": torch.arange(n * 2, dtype=DTYPE, device="cuda").reshape(n, 2), "codes.ref": torch.arange(30, dtype=DTYPE, device="cuda").reshape(15, 2), "tags": [torch.ones(3, 5, device="cuda")], } out, event = _snapshot_leftover_mm_cpu(mm, set(), n) assert event is not None event.synchronize() _assert_leftover_shapes(mm, out, n) for t in (out["codes.audio"], out["codes.ref"], out["tags"][0]): assert t.device.type == "cpu" and t.is_pinned() assert torch.equal(out["codes.audio"], mm["codes.audio"].cpu()) assert torch.equal(out["codes.ref"], mm["codes.ref"].cpu()) def test_codes_ref_matches_old_build_mm_cpu_path(): """Leftover `codes.ref` equals the old runner path: build_mm_cpu + to_payload_element.""" pytest.importorskip("vllm") from vllm_omni.utils.mm_outputs import build_mm_cpu, to_payload_element refs = [torch.arange(8, dtype=torch.long), torch.arange(12, dtype=torch.long)] mgr, view = make_manager() sid = run_step(mgr, view, {"a": ([0], 0, 2), "b": ([1], 0, 2)}, mm={"codes.ref": refs}) ctx = mgr._step_ctxs[sid] assert "codes.ref" in ctx.mm_cpu_snapshot assert [t.shape for t in ctx.mm_cpu_snapshot["codes.ref"]] == [t.shape for t in refs] outs = mgr.materialize(sid, ["a", "b"]) old = build_mm_cpu({"codes.ref": refs}) total = 4 for req_id, idx, start, end in (("a", 0, 0, 2), ("b", 1, 2, 4)): want = to_payload_element(old["codes.ref"], idx, start=start, end=end, pass_lists_through=True, seq_len=total) got = outs.mm_outputs["codes.ref"][req_id] assert len(got) == len(want) for g, w in zip(got, want): assert torch.equal(g, w) def test_is_step_token_tensor(): n, padded = 4, 8 assert _is_step_token_tensor(torch.zeros(n, 2), n, padded) assert _is_step_token_tensor(torch.zeros(padded, 2), n, padded) assert not _is_step_token_tensor([torch.zeros(1, 8)], n, padded) assert not _is_step_token_tensor(torch.zeros(15, 2), n, padded) assert not _is_step_token_tensor(torch.tensor([1, 2, 3]), n, padded) def test_policy_from_model_shim(): class M: requires_full_prefix_cached_hidden_states = False deferred_prefix_cache_mm_keys = {"codes.audio"} p = ModelCachePolicy.from_model(M()) assert p.needs_full_hidden_states is False assert p.hidden_key is None assert p.deferred_keys == frozenset({"codes.audio"}) assert p.get_hit_keys([HIDDEN_KEY, "codes.audio"]) == ["codes.audio"] assert p.skip_immediate_mm("codes.audio") d = ModelCachePolicy.from_model(object()) assert d.needs_full_hidden_states is True and not d.deferred_keys assert d.hidden_key == HIDDEN_KEY assert d.get_hit_keys(["talker.h", HIDDEN_KEY]) == [HIDDEN_KEY, "talker.h"] def test_deferred_key_accumulates_and_flushes_on_finish(): policy = ModelCachePolicy(needs_full_hidden_states=False, deferred_keys=frozenset({"codes.audio"})) mgr, view = make_manager(policy=policy) feat = 2 for pos in range(2): sid = run_step( mgr, view, {"a": ([3], pos, 1)}, mm={"codes.audio": torch.full((1, feat), float(pos + 1))}, ) mgr.materialize(sid, ["a"]) slots = view.slots_for("a", 0, 2) rows = plan_fetch(mgr, slots, "codes.audio", req_id="a") assert torch.equal(rows[:, 0], torch.tensor([1.0, 2.0])) sid = run_step(mgr, view, {"z": ([9], 0, 1)}, finished=["a"]) mgr.materialize(sid, ["z"]) mirror = mgr._pool.rows("codes.audio", slots) assert torch.equal(mirror[:, 0], torch.tensor([1.0, 2.0])) def test_deferred_gpu_bytes_held_until_last_view_drops(): """Shared deferred clone is one allocation; finishing the first co-scheduled request must not drop ``_staged_bytes`` while the other still pins it.""" policy = ModelCachePolicy(needs_full_hidden_states=False, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) feat = 2 n = 4 s1 = run_step( mgr, view, {"a": ([0], 0, 2), "b": ([1], 0, 2)}, mm={"k": torch.zeros(n, feat)}, ) mgr.materialize(s1, ["a", "b"]) clone_bytes = n * feat * 4 # float32 assert mgr._controller._staged_bytes == clone_bytes s2 = run_step(mgr, view, {"z": ([9], 0, 1)}, finished=["a"]) mgr.materialize(s2, ["z"]) assert mgr._controller._staged_bytes == clone_bytes s3 = run_step(mgr, view, {"z2": ([9], 1, 1)}, finished=["b"]) mgr.materialize(s3, ["z2"]) assert mgr._controller._staged_bytes == 0 def test_cap_forces_flush_of_deferred(): """One long request's deferred clone must not pin the whole budget: the flush closes its open task and the next step opens a new one.""" policy = ModelCachePolicy(needs_full_hidden_states=False, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy, gpu_staging_bytes=48) # three (1, 4) fp32 chunks for pos in range(4): sid = run_step(mgr, view, {"a": ([2, 3], pos, 1)}, mm={"k": torch.full((1, 4), float(pos + 1))}) mgr.materialize(sid, ["a"]) # Fourth chunk flushed the first three; only it is still charged. assert mgr._controller._staged_bytes == 16 rows = mgr._pool.rows("k", view.slots_for("a", 0, 4))[:, 0].tolist() assert rows == [1.0, 2.0, 3.0, 0.0] assert mgr._request_tasks.deferred["a"].write_n == 2 assert len(mgr._request_tasks.tasks["a"]) == 1 # the flushed write was drained def test_deferred_tenant_succession_no_stale_wins(): policy = ModelCachePolicy(needs_full_hidden_states=False, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) s1 = run_step(mgr, view, {"a": ([2], 0, 2)}, mm={"k": torch.full((2, 2), 1.0)}) mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"b": ([2], 0, 2)}, mm={"k": torch.full((2, 2), 2.0)}) mgr.materialize(s2, ["b"]) slots = view.slots_for("b", 0, 2) s3 = run_step(mgr, view, {"z1": ([9], 0, 1)}, finished=["b"]) mgr.materialize(s3, ["z1"]) s4 = run_step(mgr, view, {"z2": ([9], 1, 1)}, finished=["a"]) mgr.materialize(s4, ["z2"]) assert torch.equal(mgr._pool.rows("k", slots), torch.full((2, 2), 2.0)) def test_reacquired_slot_deferred_rewrite_wins_over_interim_tenant(): """Preempt + resume onto the same block: the rewrite must open a new write, or the old task's reassigned tombstone drops the fresh rows and the interim tenant's stale value is served as a's.""" policy = ModelCachePolicy(needs_full_hidden_states=False, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) s1 = run_step(mgr, view, {"a": ([2], 0, 1)}, mm={"k": torch.full((1, 2), 11.0)}) mgr.materialize(s1, ["a"]) slots = view.slots_for("a", 0, 1) s2 = run_step(mgr, view, {"b": ([2], 0, 1)}, mm={"k": torch.full((1, 2), 22.0)}) mgr.materialize(s2, ["b"]) s3 = run_step(mgr, view, {"z": ([9], 0, 1)}, finished=["b"]) # 22 committed mgr.materialize(s3, ["z"]) s4 = run_step(mgr, view, {"a": ([2], 0, 1)}, mm={"k": torch.full((1, 2), 33.0)}) # a re-claims mgr.materialize(s4, ["a"]) s5 = run_step(mgr, view, {"z2": ([9], 1, 1)}, finished=["a"]) # flush a's write mgr.materialize(s5, ["z2"]) assert torch.equal(mgr._pool.rows("k", slots), torch.full((1, 2), 33.0)) def test_deferred_key_hit_reads_staged_rows_not_mirror(): policy = ModelCachePolicy(needs_full_hidden_states=True, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) s1 = run_step(mgr, view, {"a": ([0], 0, 4)}, mm={"k": torch.full((4, 2), 1.0)}) mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"b": ([0, 1], 4, 2)}, new_hits={"b": 4}, mm={"k": torch.full((2, 2), 9.0)}) rows = mgr.materialize(s2, ["b"]).mm_outputs["k"]["b"] assert torch.equal(rows[:4], torch.full((4, 2), 1.0)), rows assert torch.equal(rows[4:], torch.full((2, 2), 9.0)) def test_append_to_closed_deferred_entry_opens_new_one(): """A deferred task closed by the budget flush (or an early escalate) cannot take more chunks; the next save opens a new write and a hit reads both.""" policy = ModelCachePolicy(needs_full_hidden_states=False, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) s1 = run_step(mgr, view, {"a": ([2], 0, 1)}, mm={"k": torch.full((1, 2), 1.0)}) mgr.materialize(s1, ["a"]) first = mgr._request_tasks.deferred["a"] mgr._controller.escalate([first.tid]) s2 = run_step(mgr, view, {"a": ([2], 1, 1)}, mm={"k": torch.full((1, 2), 2.0)}) mgr.materialize(s2, ["a"]) assert mgr._request_tasks.deferred["a"].tid != first.tid slots = view.slots_for("a", 0, 2) rows = plan_fetch(mgr, slots, "k", req_id="a") assert torch.equal(rows[:, 0], torch.tensor([1.0, 2.0])) def test_deferred_unpadded_registers_on_padded_step(): """Qwen3-TTS: deferred codes.audio is unpadded; cudagraph pads hidden.""" policy = ModelCachePolicy(needs_full_hidden_states=False, deferred_keys=frozenset({"codes.audio"})) mgr, view = make_manager(policy=policy) audio = torch.arange(8 * 2, dtype=DTYPE).reshape(8, 2) s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}, mm={"codes.audio": audio}, num_tokens_padded=16) assert mgr._pool.has_key("codes.audio") assert "codes.audio" in mgr._step_ctxs[s1].mm_cpu_snapshot rows = plan_fetch(mgr, view.slots_for("a", 0, 8), "codes.audio", req_id="a") assert torch.equal(rows, audio) outs = mgr.materialize(s1, ["a"]) assert torch.equal(outs.mm_outputs["codes.audio"]["a"], audio) def test_check_kv_groups_rejects_empty_or_multi(): with pytest.raises(OmniPrefixCacheUnmatchError, match="single full-attention"): check_prefix_cache_kv_groups([]) with pytest.raises(OmniPrefixCacheUnmatchError, match="single full-attention"): check_prefix_cache_kv_groups([object(), object()]) def _stage_cfg(*, enable=True, pooling=False, kv_transfer=None, groups=(object(),), spec=None, match_unit=None): return stage_prefix_cache_config( kv_cache_config=SimpleNamespace(num_blocks=NUM_BLOCKS, kv_cache_groups=list(groups)), cache_config=SimpleNamespace(enable_prefix_caching=enable, block_size=BLOCK_SIZE, prefix_match_unit=match_unit), kv_transfer_config=kv_transfer, scheduler_config=SimpleNamespace(max_num_batched_tokens=64, max_model_len=128), model_config=None, is_pooling_model=pooling, speculative_config=spec, ) def test_stage_prefix_cache_config_gate(): """The runner-side gate the GPU and NPU runners share: off / pooling stages get no cache; kv_consumer, spec decode, sub-block matching and hybrid groups refuse loudly.""" assert _stage_cfg(enable=False) is None assert _stage_cfg(pooling=True) is None # kv_consumer / kv_both refuse before the group check, on both platforms. with pytest.raises(OmniPrefixCacheUnmatchError, match="KV connector"): _stage_cfg(kv_transfer=SimpleNamespace(is_kv_consumer=True), groups=()) assert _stage_cfg(kv_transfer=SimpleNamespace(is_kv_consumer=True), enable=False) is None with pytest.raises(OmniPrefixCacheUnmatchError, match="speculative decoding"): _stage_cfg(spec=SimpleNamespace(method="ngram"), groups=()) with pytest.raises(OmniPrefixCacheUnmatchError, match="block-aligned"): _stage_cfg(match_unit=BLOCK_SIZE // 2, groups=()) # A unit equal to the block is a whole-block match; only the group check is left. with pytest.raises(OmniPrefixCacheUnmatchError, match="single full-attention"): _stage_cfg(match_unit=BLOCK_SIZE, groups=()) with pytest.raises(OmniPrefixCacheUnmatchError, match="single full-attention"): _stage_cfg(groups=(object(), object())) def test_npu_runner_uses_shared_prefix_cache_gate(): """npu_model_runner cannot be imported without vllm_ascend; pin at the source level that it stages through the shared gate instead of a hand-rolled subset of the GPU checks.""" path = Path(__file__).resolve().parents[2] / "vllm_omni/platforms/npu/worker/npu_model_runner.py" tree = ast.parse(path.read_text()) calls = {n.func.id for n in ast.walk(tree) if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)} assert "stage_prefix_cache_config" in calls assert "check_prefix_cache_kv_groups" not in calls def _table_slots(table, req_idx, token_start, token_end, block_size=BLOCK_SIZE): if token_end <= token_start: return torch.empty((0,), dtype=torch.long) token_positions = torch.arange(token_start, token_end, dtype=torch.long) block_offsets = token_positions // block_size max_blocks = int(table.shape[1]) valid = block_offsets < max_blocks if not bool(valid.all()): token_positions = token_positions[valid] block_offsets = block_offsets[valid] if token_positions.numel() == 0: return torch.empty((0,), dtype=torch.long) block_ids = table[req_idx, block_offsets].to(torch.long) return block_ids * block_size + (token_positions % block_size) def test_group_view_step_slots_cpu(): class TensorWrap: def __init__(self, t): self.cpu = t class Group: def __init__(self, t): self.block_table = TensorWrap(t) class BT: def __init__(self, t): self._g = Group(t) self.block_tables = [self._g.block_table] def __getitem__(self, idx): assert idx == 0 return self._g class IB: def __init__(self, table): self.req_ids = ["r1", "r2"] self.req_id_to_index = {"r1": 0, "r2": 1} self.num_computed_tokens_cpu = torch.tensor([8, 0]) self.block_table = BT(table) table = torch.tensor([[2, 5, 7], [1, 0, 0]]) view = FullAttentionGroupView(IB(table), block_size=BLOCK_SIZE) assert view.step_slots_cpu(["r1"], {"r1": 2}).tolist() == [7 * 4 + 0, 7 * 4 + 1] assert view.step_slots_cpu(["r1"], {"r1": 8}).tolist() == [7 * 4 + i for i in range(4)] assert view.step_slots_cpu(["r1"], {"r1": 0}).numel() == 0 def test_join_next_step_previous_save(): mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0], 0, 2)}) assert len(mgr._join_next_step_tids) == 1 task_id = mgr._join_next_step_tids[0] mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"a": ([0], 2, 1)}) assert mgr._controller.get_task(task_id) is None assert int(mgr._slot_status.state[HIDDEN_KEY][view.slots_for("a", 0, 2)].min()) == 2 mgr.materialize(s2, ["a"]) def test_tenant_succession_hit_reads_newest(): mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0], 0, 4)}) mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"b": ([0], 0, 4)}, finished=["a"]) mgr.materialize(s2, ["b"]) b_hidden = expected_rows(view.slots_for("b", 0, 4)) s3 = run_step(mgr, view, {"c": ([0, 1], 4, 2)}, new_hits={"c": 4}, finished=["b"]) outs = mgr.materialize(s3, ["c"]) assert torch.equal(outs.hidden_states["c"][:4], b_hidden) def test_tenant_succession_mm_key(): mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([2], 0, 4)}, mm={"k": torch.full((4, 2), 1.0)}) mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"b": ([2], 0, 4)}, finished=["a"], mm={"k": torch.full((4, 2), 2.0)}) mgr.materialize(s2, ["b"]) s3 = run_step( mgr, view, {"c": ([2, 3], 4, 2)}, new_hits={"c": 4}, finished=["b"], mm={"k": torch.full((2, 2), 3.0)} ) rows = mgr.materialize(s3, ["c"]).mm_outputs["k"]["c"] assert torch.equal(rows[:4], torch.full((4, 2), 2.0)) assert torch.equal(rows[4:], torch.full((2, 2), 3.0)) def test_scatter_rows_coalesces_per_key_last_chunk_wins_and_skips_reassigned(): from vllm_omni.core.prefix_cache.controller import WriteSchedule, WriteTask, _BudgetTicket, _WriteChunk t1, t2 = _BudgetTicket(nbytes=8), _BudgetTicket(nbytes=8) def chunk(slots, base, ticket): s = torch.tensor(slots, dtype=torch.int64) host = {"k": torch.full((len(slots), 2), float(base)) + s[:, None]} return _WriteChunk(slots_cpu=s, tensors={}, host=host, budget=ticket) task = WriteTask( tid=1, req_id="a", write_n=1, schedule=WriteSchedule.JOIN_ON_FINISH, chunks=[chunk([0, 1], 10, t1), chunk([2, 3], 20, t1), chunk([1, 4], 30, t2)], ) assert task.budget_tickets() == [t1, t2] task.add_reassigned("k", torch.tensor([3], dtype=torch.int64)) rows = task.scatter_rows() assert len(rows) == 1 key, slots, host = rows[0] assert key == "k" and slots.tolist() == [0, 2, 1, 4] # slot 1: the third chunk's row (base 30) replaces the first chunk's; slot 3 is reassigned. assert host[:, 0].tolist() == [10.0, 22.0, 31.0, 34.0] def test_slot_reuse_pushes_skip_to_old_task(): """Reassignment = task swap. Keep the old write in progress so the new write records those rows as no longer owned by it.""" mgr, view = make_manager() def hold(tasks): for task in tasks: task.host_ready.set() task.done.set() # satisfy the next save's done barrier; state stays QUEUED mgr._controller.dispatch = hold policy = ModelCachePolicy(needs_full_hidden_states=False) mgr.register_policy(policy) s1 = run_step(mgr, view, {"a": ([2], 0, 1)}, mm={"k": torch.ones(1, 2)}) mgr.materialize(s1, ["a"]) old_tid = next(iter(mgr._request_tasks.tasks["a"])) old_task = mgr._controller.get_task(old_tid) s2 = run_step(mgr, view, {"b": ([2], 0, 2)}, mm={"k": torch.full((2, 2), 2.0)}) assert old_task is not None and "k" in old_task.reassigned reused = view.slots_for("b", 0, 1) assert bool(torch.isin(reused, old_task.reassigned["k"]).all()) mgr.materialize(s2, ["b"]) def test_step_slots_cpu_matches_block_table_math(): class TensorWrap: def __init__(self, t): self.cpu = t class Group: def __init__(self, t): self.block_table = TensorWrap(t) class BT: def __init__(self, t): self._g = Group(t) self.block_tables = [self._g.block_table] def __getitem__(self, idx): return self._g class IB: def __init__(self, table, computed): self.req_ids = ["r1", "r2"] self.req_id_to_index = {"r1": 0, "r2": 1} self.num_computed_tokens_cpu = torch.tensor(computed) self.block_table = BT(table) table = torch.tensor([[2, 5, 7], [1, 3, 4]]) view = FullAttentionGroupView(IB(table, [4, 0]), block_size=BLOCK_SIZE) num_sched = {"r1": 3, "r2": 5} got = view.step_slots_cpu(["r1", "r2"], num_sched) want = torch.cat([_table_slots(table, 0, 4, 7), _table_slots(table, 1, 0, 5)]) assert torch.equal(got, want), (got, want) assert torch.equal(view.step_slots_cpu(["r1", "r2"], {"r1": 3, "r2": 0}), _table_slots(table, 0, 4, 7)) def _ib_with_layout(**layout): """InputBatch fake whose group-0 block table carries vLLM's layout attrs.""" attrs = dict(kv_cache_block_size=BLOCK_SIZE, blocks_per_kv_block=1, use_hybrid_blocks=False, dcp_world_size=1) attrs.update(layout) group = SimpleNamespace(block_table=SimpleNamespace(cpu=torch.zeros((1, 2), dtype=torch.int32)), **attrs) class BT: block_tables = [group.block_table] def __getitem__(self, idx): return group return SimpleNamespace(req_ids=[], req_id_to_index={}, num_computed_tokens_cpu=torch.zeros(1), block_table=BT()) def test_group_view_refuses_hybrid_kernel_blocks_and_dcp(): """step_slots_cpu assumes one allocator block per table column and no token striping; both are silent wrong-slot writes if not refused.""" FullAttentionGroupView(_ib_with_layout(), block_size=BLOCK_SIZE) with pytest.raises(OmniPrefixCacheUnmatchError, match="kernel_block_size"): FullAttentionGroupView(_ib_with_layout(use_hybrid_blocks=True, blocks_per_kv_block=2), block_size=BLOCK_SIZE) with pytest.raises(OmniPrefixCacheUnmatchError, match="decode context parallel"): FullAttentionGroupView(_ib_with_layout(dcp_world_size=2), block_size=BLOCK_SIZE) with pytest.raises(OmniPrefixCacheUnmatchError, match="does not match"): FullAttentionGroupView(_ib_with_layout(kv_cache_block_size=BLOCK_SIZE * 2), block_size=BLOCK_SIZE) def test_step_context_consumed_by_id_not_order(): mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0], 0, 4)}) s2 = run_step(mgr, view, {"b": ([1], 0, 4)}) mgr.discard_step(s2) outs = mgr.materialize(s1, ["a"]) assert torch.equal(outs.hidden_states["a"], expected_rows(view.slots_for("a", 0, 4))) assert len(mgr._step_ctxs) == 0 def test_step_context_exactly_once(): mgr, view = make_manager() sid = run_step(mgr, view, {"a": ([0], 0, 4)}) mgr.materialize(sid, ["a"]) with pytest.raises(OmniPrefixCacheUnmatchError): mgr.materialize(sid, ["a"]) with pytest.raises(OmniPrefixCacheUnmatchError): mgr.discard_step(sid) def test_unconsumed_contexts_timeout(): mgr, view = make_manager(staging_claim_timeout_s=0.0) with pytest.raises(OmniPrefixCacheStagingTimeoutError, match="unconsumed step contexts"): for pos in range(8): run_step(mgr, view, {"a": ([0, 1, 2, 3], pos, 1)}) def test_save_waits_until_a_slot_is_consumed(): mgr, view = make_manager(staging_depth=2, staging_claim_timeout_s=2.0) s0 = run_step(mgr, view, {"a": ([0], 0, 1)}) s1 = run_step(mgr, view, {"a": ([0], 1, 1)}) err: list[BaseException] = [] sid_holder: list[int] = [] def _blocked_save() -> None: try: sid_holder.append(run_step(mgr, view, {"a": ([0], 2, 1)})) except BaseException as e: err.append(e) t = threading.Thread(target=_blocked_save, daemon=True) t.start() time.sleep(0.1) assert t.is_alive() mgr.discard_step(s0) t.join(timeout=1.0) assert not t.is_alive() assert not err mgr.discard_step(s1) mgr.discard_step(sid_holder[0]) def test_leftover_only_save_claims_staging_slot(): policy = ModelCachePolicy(needs_full_hidden_states=False) mgr, view = make_manager(policy=policy, staging_depth=2, staging_claim_timeout_s=0.0) leftover = {"codes.ref": torch.zeros(3, 2)} s0 = run_step(mgr, view, {"a": ([0], 0, 1)}, mm=leftover) s1 = run_step(mgr, view, {"a": ([0], 1, 1)}, mm=leftover) assert mgr._step_ctxs[s0].d2h is not None and mgr._step_ctxs[s0].d2h.views == {} assert mgr._step_ctxs[s1].d2h is not None and mgr._step_ctxs[s1].d2h.views == {} with pytest.raises(OmniPrefixCacheStagingTimeoutError, match="unconsumed step contexts"): run_step(mgr, view, {"a": ([0], 2, 1)}, mm=leftover) def test_save_slot_mismatch_fails_fast(): mgr, view = make_manager() view.order = ["a"] view.req_blocks["a"] = [0] view.computed["a"] = 0 mgr.new_step_starts(FakeSchedOut(new_reqs=[FakeNewReq("a")], num_scheduled={"a": 2})) hidden = torch.zeros(4, HIDDEN, dtype=DTYPE) with pytest.raises(OmniPrefixCacheUnmatchError): mgr.save_outputs(hidden, {}, num_tokens_unpadded=4, num_tokens_padded=4) def test_materialize_rejects_out_of_snapshot_ids(): mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}) mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}, finished=["a"]) with pytest.raises(OmniPrefixCacheUnmatchError, match="outside the save snapshot"): mgr.materialize(s2, ["b", "late_joiner"]) def test_materialize_unknown_step_id_fails_fast(): mgr, view = make_manager() sid = run_step(mgr, view, {"a": ([0], 0, 4)}) with pytest.raises(OmniPrefixCacheUnmatchError): mgr.materialize(sid + 999, ["ghost"]) mgr.discard_step(sid) def test_mm_hit_span_never_registered_serves_mirror_baseline(): """A hit span slot on which a sparse mm key was never registered is legitimate absence — served from the mirror baseline, not a crash.""" policy = ModelCachePolicy(needs_full_hidden_states=True, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) s1 = run_step(mgr, view, {"a": ([0], 0, 4)}, mm={"k": torch.full((4, 2), 5.0)}) mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"c": ([1], 0, 4)}) mgr.materialize(s2, ["c"]) s3 = run_step(mgr, view, {"b": ([0, 1, 2], 8, 2)}, new_hits={"b": 8}, mm={"k": torch.full((2, 2), 9.0)}) rows = mgr.materialize(s3, ["b"]).mm_outputs["k"]["b"] assert torch.equal(rows[:4], torch.full((4, 2), 5.0)) assert torch.equal(rows[4:8], torch.zeros(4, 2)) assert torch.equal(rows[8:], torch.full((2, 2), 9.0)) def test_mm_in_transit_unresolvable_fails_fast(): """Rows registered in-transit whose entry cannot serve them must raise.""" policy = ModelCachePolicy(needs_full_hidden_states=True, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) s1 = run_step(mgr, view, {"a": ([0], 0, 4)}, mm={"k": torch.full((4, 2), 1.0)}) mgr.materialize(s1, ["a"]) tid = mgr._request_tasks.deferred["a"].tid mgr._controller._tasks.pop(tid) s2 = run_step(mgr, view, {"b": ([0, 1], 4, 2)}, new_hits={"b": 4}, mm={"k": torch.full((2, 2), 9.0)}) with pytest.raises(OmniPrefixCacheUnmatchError): mgr.materialize(s2, ["b"]) def test_lock_never_covers_fetch_or_join(): """State lock must not be held across join / join_host_ready / fetch_host.""" policy = ModelCachePolicy(needs_full_hidden_states=True, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) calls = [] def probe(kind): on_facade = not threading.current_thread().name.startswith("omni-prefix-cache-prefetch") calls.append((kind, on_facade and mgr._state_lock.locked())) real_fetch = mgr._controller.fetch_host real_join_hr = mgr._controller.join_host_ready real_join = mgr._controller.join mgr._controller.fetch_host = lambda *a, **kw: (probe("fetch"), real_fetch(*a, **kw))[1] mgr._controller.join_host_ready = lambda ids: (probe("join"), real_join_hr(ids))[1] mgr._controller.join = lambda ids: (probe("join"), real_join(ids))[1] s1 = run_step(mgr, view, {"a": ([0], 0, 4)}, mm={"k": torch.full((4, 2), 1.0)}) mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"b": ([0, 1], 4, 2)}, new_hits={"b": 4}, mm={"k": torch.full((2, 2), 9.0)}) mgr.materialize(s2, ["b"]) assert any(kind == "fetch" for kind, _ in calls) assert any(kind == "join" for kind, _ in calls) assert all(not locked for _, locked in calls), calls def test_failed_write_fails_fast_at_next_facade_entry(): mgr, view = make_manager() sid = run_step(mgr, view, {"a": ([0], 0, 4)}) mgr.materialize(sid, ["a"]) from vllm_omni.core.prefix_cache.controller import WriteTask, _WriteChunk task = WriteTask( tid=999, req_id="x", write_n=1, schedule=WriteSchedule.JOIN_NEXT_STEP, chunks=[_WriteChunk(slots_cpu=torch.tensor([0]), tensors={})], ) mgr._controller._tasks[999] = task mgr._controller._fail_task(999) with pytest.raises(OmniPrefixCacheUnmatchError, match="write failed"): run_step(mgr, view, {"a": ([0, 1], 4, 1)}) # Sticky: the record is one-shot and the first raise may land in a # swallowed path (prefetch Future dropped by discard_step); a later # entry must keep raising, never serve zeros for the lost rows. with pytest.raises(OmniPrefixCacheUnmatchError, match="write failed"): run_step(mgr, view, {"c": ([0, 1], 0, 4)}) def test_join_raises_on_failed_task(): """A joiner woken by FAILED must not treat the wake as a completed write; the one-shot failure record may already be drained elsewhere.""" mgr, view = make_manager() _hold_writes(mgr) run_step(mgr, view, {"a": ([0], 0, 4)}) tid = next(iter(mgr._request_tasks.tasks["a"])) mgr._controller._fail_task(tid) with pytest.raises(OmniPrefixCacheUnmatchError, match="failed before done"): mgr._controller.join([tid]) @pytest.mark.parametrize("fail_at", ["copy", "scatter"]) def test_eager_dispatch_failure_releases_step_and_all_task_owners(monkeypatch, fail_at): from vllm_omni.core.prefix_cache.controller import TaskState, WriteTask mgr, view = make_manager(staging_claim_timeout_s=0) ctrl = mgr._controller preserved_sid = run_step(mgr, view, {"previous": ([3], 0, 4)}) if fail_at == "copy": original = WriteTask.mark_host_ready def fail_copy(task): if task.req_id == "b": raise RuntimeError("injected copy failure") return original(task) monkeypatch.setattr(WriteTask, "mark_host_ready", fail_copy) else: original = ctrl._scatter def fail_scatter(task): if task.req_id == "b": raise RuntimeError("injected scatter failure") return original(task) monkeypatch.setattr(ctrl, "_scatter", fail_scatter) try: with pytest.raises(RuntimeError, match=f"injected {fail_at} failure"): run_step(mgr, view, {"a": ([0], 0, 4), "b": ([1], 0, 4), "c": ([2], 0, 4)}) assert set(mgr._step_ctxs) == {preserved_sid} assert ctrl._staged_bytes == 0 tasks = [task for task in ctrl._tasks.values() if task.req_id in {"a", "b", "c"}] assert len(tasks) == 3 for task in tasks: expected = TaskState.WRITTEN if task.req_id == "a" else TaskState.FAILED assert task.state is expected assert task.done.is_set() and task.host_ready.is_set() assert all( holder == StagingBufferHolder.for_step(preserved_sid) for holders in ctrl._staging_pool._busy for holder in holders ) mgr.discard_step(preserved_sid) assert all(not holders for holders in ctrl._staging_pool._busy) with pytest.raises(OmniPrefixCacheUnmatchError, match="write failed"): mgr.new_step_starts(FakeSchedOut()) finally: mgr.shutdown() def test_eager_escalation_failure_releases_deferred_tasks(monkeypatch): from vllm_omni.core.prefix_cache.controller import TaskState mgr, view = make_manager(policy=ModelCachePolicy(needs_full_hidden_states=False, deferred_keys=frozenset({"k"}))) ctrl = mgr._controller sid = run_step(mgr, view, {"a": ([0], 0, 4), "b": ([1], 0, 4)}, mm={"k": torch.ones(8, 2)}) mgr.discard_step(sid) assert ctrl._staged_bytes > 0 tasks = list(ctrl._tasks.values()) def fail_scatter(task): raise RuntimeError("injected deferred scatter failure") monkeypatch.setattr(ctrl, "_scatter", fail_scatter) try: with pytest.raises(RuntimeError, match="injected deferred scatter failure"): ctrl.escalate([task.tid for task in tasks]) assert ctrl._staged_bytes == 0 assert all(task.state is TaskState.FAILED for task in tasks) assert all(task.done.is_set() and task.host_ready.is_set() for task in tasks) assert all(not holders for holders in ctrl._staging_pool._busy) with pytest.raises(OmniPrefixCacheUnmatchError, match="write failed"): mgr.new_step_starts(FakeSchedOut()) finally: mgr.shutdown() def test_per_request_staging_writes(): mgr, view = make_manager() sid = run_step(mgr, view, {"p": ([0, 1], 0, 8), "d": ([2], 0, 1)}) tp = mgr._controller.get_task(next(iter(mgr._request_tasks.tasks["p"]))) td = mgr._controller.get_task(next(iter(mgr._request_tasks.tasks["d"]))) assert tp is not None and td is not None assert (tp.req_id, td.req_id) == ("p", "d") assert mgr._join_next_step_tids == [tp.tid, td.tid] outs = mgr.materialize(sid, ["p", "d"]) assert torch.equal(outs.hidden_states["p"], expected_rows(view.slots_for("p", 0, 8))) assert torch.equal(outs.hidden_states["d"], expected_rows(view.slots_for("d", 0, 1))) def test_staging_step_prefills_task_host_and_recycles(): mgr, view = make_manager() for i in range(6): sid = run_step(mgr, view, {"a": ([i % 8, (i % 8) + 8], i, 1)}) ctx = mgr._step_ctxs[sid] assert ctx.d2h is not None expect = expected_rows(view.slots_for("a", i, i + 1)) outs = mgr.materialize(sid, ["a"]) assert torch.equal(outs.hidden_states["a"], expect), i def test_oversized_step_fails_fast(): mgr, view = make_manager(staging_capacity_tokens=4) with pytest.raises(OmniPrefixCacheUnmatchError, match="staging capacity is 4"): run_step(mgr, view, {"a": ([0, 1], 0, 8)}) def test_staging_slot_eager_save_leaves_only_step_holder(): """Eager submit writes the pool inline, so the task holder is bound and released inside save; materialize then empties the slot.""" mgr, view = make_manager() sid = run_step(mgr, view, {"p": ([0, 1], 0, 8)}) ctx = mgr._step_ctxs[sid] assert ctx.d2h is not None busy = mgr._controller._staging_pool._busy[ctx.d2h.staging_slot] assert busy == {StagingBufferHolder.for_step(sid)} mgr.materialize(sid, ["p"]) assert not busy def _hold_writes(mgr) -> None: """Non-eager stand-in: dispatch takes the task to device→host done, but leaves the pool write to the caller (`mgr._controller._scatter`).""" def dispatch(tasks): for task in tasks: assert task.claim_copy() task.mark_host_ready() mgr._controller.dispatch = dispatch def test_eager_copy_runs_outside_state_lock_and_key_install_inside(): """Eager mode (NPU/CPU): the copy + pool write must not run under _state_lock, or materialize on the builder thread waits a whole D2H; publishing a new pool key must happen under it.""" policy = ModelCachePolicy(needs_full_hidden_states=True, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) seen: dict[str, bool] = {} def probe(name): free = mgr._state_lock.acquire(blocking=False) if free: mgr._state_lock.release() seen[name] = free real_run, real_install = mgr._controller._run_eager, mgr._pool.install_key def run_eager(task): probe("run_eager") real_run(task) def install_key(key, storage): probe("install_key") real_install(key, storage) mgr._controller._run_eager = run_eager mgr._pool.install_key = install_key sid = run_step(mgr, view, {"a": ([0], 0, 2)}, mm={"k": torch.ones(2, 2)}) assert seen == {"run_eager": True, "install_key": False} mgr.materialize(sid, ["a"]) assert mgr._pool.has_key("k") and "k" in mgr._slot_status.state def test_eager_finish_escalate_runs_outside_state_lock(): """Finish/abort escalate must not _run_eager under _state_lock.""" policy = ModelCachePolicy(needs_full_hidden_states=False, deferred_keys=frozenset({"k"})) mgr, view = make_manager(policy=policy) s1 = run_step(mgr, view, {"a": ([2], 0, 1)}, mm={"k": torch.ones(1, 2)}) mgr.materialize(s1, ["a"]) seen: list[bool] = [] real_run = mgr._controller._run_eager def run_eager(task): free = mgr._state_lock.acquire(blocking=False) if free: mgr._state_lock.release() seen.append(free) real_run(task) mgr._controller._run_eager = run_eager s2 = run_step(mgr, view, {"z": ([9], 0, 1)}, finished=["a"]) assert seen == [True] mgr.materialize(s2, ["z"]) def test_staging_task_slot_released_at_pool_write_not_drain(): mgr, view = make_manager() _hold_writes(mgr) sid = run_step(mgr, view, {"p": ([0, 1], 0, 8)}) ctx = mgr._step_ctxs[sid] tid = next(iter(mgr._request_tasks.tasks["p"])) busy = mgr._controller._staging_pool._busy[ctx.d2h.staging_slot] assert StagingBufferHolder.for_task(tid) in busy and StagingBufferHolder.for_step(sid) in busy task = mgr._controller.get_task(tid) mgr._controller._scatter(task) # Released by the committer's pool write; the manager has not drained yet. assert busy == {StagingBufferHolder.for_step(sid)} assert mgr._controller.get_task(tid) is not None mgr.materialize(sid, ["p"]) assert not busy assert mgr._controller.get_task(tid) is None def test_blocked_save_is_released_by_pool_write_alone(): """All step contexts consumed, the only slot held by a pending write: the committer finishing that write must unblock the waiting save.""" mgr, view = make_manager(staging_depth=1, staging_claim_timeout_s=2.0) _hold_writes(mgr) s0 = run_step(mgr, view, {"a": ([0], 0, 1)}) tid = next(iter(mgr._request_tasks.tasks["a"])) mgr.materialize(s0, ["a"]) slot = mgr._step_ctxs.get(s0) # consumed assert slot is None assert mgr._controller._staging_pool._busy[0] == {StagingBufferHolder.for_task(tid)} err: list[BaseException] = [] sid_holder: list[int] = [] def _blocked_save() -> None: try: sid_holder.append(run_step(mgr, view, {"a": ([0], 1, 1)})) except BaseException as e: err.append(e) t = threading.Thread(target=_blocked_save, daemon=True) t.start() time.sleep(0.1) assert t.is_alive() mgr._controller._scatter(mgr._controller.get_task(tid)) t.join(timeout=1.0) assert not t.is_alive() assert not err mgr.discard_step(sid_holder[0]) def test_hit_prefetch_prebuilds_merged_buffer(): mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}) mgr.materialize(s1, ["a"]) s2 = run_step(mgr, view, {"b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}, finished=["a"]) ctx = mgr._step_ctxs[s2] fut = ctx.hit_prefetch["b"][HIDDEN_KEY] buf = fut.result() assert buf.shape == (12, HIDDEN) assert torch.equal(buf[:8], expected_rows(view.slots_for("b", 0, 8))) merged = mgr.materialize(s2, ["b"]).hidden_states["b"] assert merged.data_ptr() == buf.data_ptr() assert torch.equal(merged[8:], expected_rows(view.slots_for("b", 8, 12))) def test_same_step_hit_prefetch_starts_at_save(caplog): """b hits blocks a computes in the same step: nothing to plan at new_step_starts (rows ABSENT), planned at publish once a's write is registered, so the gather runs before the next step can reuse them.""" mgr, view = make_manager() view.req_blocks["a"] = [0, 1] with caplog.at_level(logging.CRITICAL, logger="vllm_omni.core.prefix_cache.manager"): sid = run_step(mgr, view, {"a": ([0, 1], 0, 8), "b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}) assert not any("omni prefix cache unmatch" in r.message for r in caplog.records) ctx = mgr._step_ctxs[sid] fut = ctx.hit_prefetch["b"][HIDDEN_KEY] assert torch.equal(fut.result()[:8], expected_rows(view.slots_for("b", 0, 8))) outs = mgr.materialize(sid, ["a", "b"]) assert torch.equal(outs.hidden_states["b"][:8], expected_rows(view.slots_for("b", 0, 8))) def test_delayed_read_of_reassigned_hit_raises(): """A version check with no COW copy still raises for live and finished. The production path registers the ref first so remap preserves the rows (see test_delayed_read_after_committed_reuse_serves_preserved).""" mgr, view = make_manager() s1 = run_step(mgr, view, {"a": ([0, 1], 0, 8)}) mgr.materialize(s1, ["a"]) slots = view.slots_for("a", 0, 8) with mgr._state_lock: planned = mgr._slot_status.get_slot_status(HIDDEN_KEY).slot_version[slots].clone() mgr._controller.dispatch = lambda tasks: None # new tenant's write stays IN_TRANSIT s2 = run_step(mgr, view, {"c": ([0, 1], 0, 8)}, finished=["a"]) for req in ("c", "a"): # live then finished — both fatal without a preserved copy with pytest.raises(OmniPrefixCacheUnmatchError, match="reassigned"): mgr._ensure_not_reassigned(slots, HIDDEN_KEY, req_id=req, planned_version=planned) mgr.discard_step(s2) def test_delayed_read_after_committed_reuse_serves_preserved(): """Amy's block-0 reuse: plan A's read, then B commits over the same slots. The reusing write copy-on-writes A's committed rows into the planned ref; the delayed fetch must return those, not B's.""" mgr, view = make_manager() sa = run_step(mgr, view, {"a": ([0], 0, 4)}) mgr.materialize(sa, ["a"]) # A committed into block 0 slots = view.slots_for("a", 0, 4) with mgr._state_lock: src = mgr._slot_ref(slots, HIDDEN_KEY, "a") # plan A's read assert src.already_staged and not src.staged_list and not src.join_tids sb = run_step(mgr, view, {"b": ([0], 0, 4)}, finished=["a"]) mgr.materialize(sb, ["b"]) # A freed, block 0 reused by B and COMMITTED # run_step derives rows from slot ids, so A's and B's values coincide. # Scribble the pool so only the preserved copy can produce A's rows. assert set(src.preserved) == {int(s) for s in slots.tolist()} with torch.inference_mode(): mgr._pool.write(HIDDEN_KEY, slots, torch.full((4, HIDDEN), 99.0, dtype=DTYPE)) got = mgr._fetch_source(src) assert torch.equal(got, expected_rows(slots)) assert all(ref is not src for ref in mgr._pending_reads) def test_done_fires_only_after_completion_record_published(): """A join(done) waiter (the save barrier) must find the completion record on its next drain — done-before-append leaves occupancy IN_TRANSIT and the COW preserve skips the reused rows.""" mgr, view = make_manager() checks = [] orig_scatter = mgr._controller._scatter def scatter(task): orig_set = task.done.set def checked_set(): checks.append(task.tid in mgr._controller._completed) orig_set() task.done.set = checked_set orig_scatter(task) mgr._controller._scatter = scatter sid = run_step(mgr, view, {"a": ([0], 0, 4)}) mgr.materialize(sid, ["a"]) assert checks and all(checks) def test_failed_wakes_only_after_failure_record_published(): """Same window as the done/_completed one, failure side: a joiner woken by FAILED must find the record on drain, not read never-written rows.""" mgr, view = make_manager() _hold_writes(mgr) # keep the write non-terminal run_step(mgr, view, {"a": ([0], 0, 4)}) tid = next(iter(mgr._request_tasks.tasks["a"])) task = mgr._controller.get_task(tid) checks = [] orig_set = task.done.set def checked_set(): checks.append(tid in mgr._controller._failed) orig_set() task.done.set = checked_set mgr._controller._fail_task(tid) assert checks and all(checks) def test_save_waits_pool_write_of_previous_step(): """A save must not reuse slots whose JOIN_NEXT_STEP write has not reached the pool — no delayed read could recover such a row.""" mgr, view = make_manager(staging_claim_timeout_s=0.2) _hold_writes(mgr) # host_ready only; the pool write never happens run_step(mgr, view, {"a": ([0], 0, 4)}) with pytest.raises(OmniPrefixCacheUnmatchError, match="did not reach done"): run_step(mgr, view, {"b": ([1], 0, 4)}) def test_join_next_step_hit_survives_task_already_drained(): mgr, view = make_manager() mgr._controller.dispatch = lambda tasks: None # registered, never run view.req_blocks["a"] = [0, 1] sid = run_step(mgr, view, {"a": ([0, 1], 0, 8), "b": ([0, 1, 2], 8, 4)}, new_hits={"b": 8}) with mgr._state_lock: src = mgr._slot_ref(view.slots_for("b", 0, 8), HIDDEN_KEY, "b") for tid in src.join_tids: mgr._controller._run_eager(mgr._controller.get_task(tid)) with torch.inference_mode(): with mgr._state_lock: mgr._commit_drained_writes() assert mgr._controller.get_task(src.join_tids[0]) is None assert torch.equal(mgr._fetch_source(src), expected_rows(view.slots_for("b", 0, 8))) mgr.materialize(sid, ["a", "b"]) def test_leftover_mm_snapshot_survives_live_overwrite(): """Leftover mm is copied at save; mutating the live buffer afterwards must not change the snapshot.""" mgr, view = make_manager() live = [torch.arange(10, dtype=DTYPE)] sid = run_step(mgr, view, {"a": ([0], 0, 2)}, mm={"codes.ref": live}) assert "codes.ref" in mgr._step_ctxs[sid].mm_cpu_snapshot snap = mgr._step_ctxs[sid].mm_cpu_snapshot["codes.ref"] live[0].fill_(99.0) assert torch.equal(snap[0], torch.arange(10, dtype=DTYPE)) mgr.discard_step(sid) @pytest.mark.parametrize("needs_full_hidden_states", [True, False]) def test_deferred_leftover_snapshot_survives_live_overwrite(needs_full_hidden_states): """Deferred tails are leftover-copied at save for materialize; the GPU freeze is a different product (pool write on finish). ``False`` is the hidden opt-out + miss path (Qwen3-TTS / Higgs talker).""" policy = ModelCachePolicy( needs_full_hidden_states=needs_full_hidden_states, deferred_keys=frozenset({"codes.audio"}) ) mgr, view = make_manager(policy=policy) live = torch.full((2, 2), 1.0) ref = [torch.arange(4, dtype=DTYPE)] sid = run_step(mgr, view, {"a": ([0], 0, 2)}, mm={"codes.audio": live, "codes.ref": ref}) assert "codes.audio" in mgr._step_ctxs[sid].mm_cpu_snapshot live.fill_(99.0) ref[0].fill_(99.0) outs = mgr.materialize(sid, ["a"]) assert torch.equal(outs.mm_outputs["codes.audio"]["a"], torch.full((2, 2), 1.0)) assert torch.equal(outs.mm_outputs["codes.ref"]["a"][0], torch.arange(4, dtype=DTYPE)) assert (outs.hidden_states is None) == (not needs_full_hidden_states) def test_frozen_mm_clone_survives_live_overwrite(): mgr, view = make_manager() live = torch.full((2, 2), 1.0) sid = run_step(mgr, view, {"a": ([0], 0, 2)}, mm={"codes.audio": live}) assert "codes.audio" not in mgr._step_ctxs[sid].mm_cpu_snapshot live.fill_(99.0) outs = mgr.materialize(sid, ["a"]) assert torch.equal(outs.mm_outputs["codes.audio"]["a"], torch.full((2, 2), 1.0)) def test_hidden_opt_out_miss_returns_mm_and_releases_slot(): policy = ModelCachePolicy(needs_full_hidden_states=False) mgr, view = make_manager(policy=policy) for i in range(6): sid = run_step(mgr, view, {"a": ([i % 8, (i % 8) + 8], i, 1)}, mm={"k": torch.full((1, 2), float(i))}) ctx = mgr._step_ctxs[sid] assert ctx.d2h is not None, i outs = mgr.materialize(sid, ["a"]) assert outs.hidden_states is None assert torch.equal(outs.mm_outputs["k"]["a"], torch.full((1, 2), float(i))) assert not mgr._controller._staging_pool._busy[ctx.d2h.staging_slot], i def test_save_releases_staging_if_commit_drained_writes_fails(): mgr, view = make_manager(staging_depth=2) calls = {"n": 0} real = mgr._commit_drained_writes def wrapped(): calls["n"] += 1 if calls["n"] == 2: raise OmniPrefixCacheUnmatchError("injected fail") return real() mgr._commit_drained_writes = wrapped with pytest.raises(OmniPrefixCacheUnmatchError, match="injected fail"): run_step(mgr, view, {"a": ([0], 0, 4)}) assert all(not busy for busy in mgr._controller._staging_pool._busy) mgr._commit_drained_writes = real sid = run_step(mgr, view, {"a": ([0], 0, 4)}) assert mgr._step_ctxs[sid].d2h is not None mgr.materialize(sid, ["a"]) def test_fetch_host_maps_slots_across_layouts(): """Identity, prefix, gapped, and a two-`_WriteChunk` gather.""" from vllm_omni.core.prefix_cache.block_pool import PrefixBlockPool from vllm_omni.core.prefix_cache.controller import OmniPrefixCacheController, WriteTask, _WriteChunk cfg = PrefixCacheConfig(num_blocks=NUM_BLOCKS, block_size=BLOCK_SIZE) pool = PrefixBlockPool(cfg) pool.ensure_key(HIDDEN_KEY, DTYPE, HIDDEN) ctrl = OmniPrefixCacheController(pool, cfg, eager=True) slots = torch.tensor([40, 41, 42, 43, 44], dtype=torch.int64) rows = torch.arange(slots.numel() * HIDDEN, dtype=DTYPE).reshape(slots.numel(), HIDDEN) def _expect(want): return torch.stack([rows[(slots == s).nonzero()[0, 0]] for s in want.tolist()]) one = WriteTask( tid=1, req_id="r", write_n=1, schedule=WriteSchedule.JOIN_ON_FINISH, chunks=[_WriteChunk(slots_cpu=slots, tensors={HIDDEN_KEY: rows})], ) for want in (slots, slots[:3], slots[[0, 1, 3, 4]]): assert torch.equal(ctrl.fetch_host(one, want, HIDDEN_KEY), _expect(want)) two = WriteTask( tid=2, req_id="r", write_n=2, schedule=WriteSchedule.JOIN_ON_FINISH, chunks=[ _WriteChunk(slots_cpu=slots[:3], tensors={HIDDEN_KEY: rows[:3]}), _WriteChunk(slots_cpu=slots[3:], tensors={HIDDEN_KEY: rows[3:]}), ], ) want = slots[[0, 3, 1, 4]] assert torch.equal(ctrl.fetch_host(two, want, HIDDEN_KEY), _expect(want)) def test_fetch_host_waits_staging_step_d2h_event(): """JOIN_NEXT_STEP stores chunk.host as a staging view; fetch_host waits step_d2h_event before slicing. Production JOIN_NEXT_STEP hits wait for the pool write instead; this is the unit-level wait contract for that branch.""" from vllm_omni.core.prefix_cache.block_pool import PrefixBlockPool from vllm_omni.core.prefix_cache.controller import OmniPrefixCacheController, WriteTask, _WriteChunk cfg = PrefixCacheConfig(num_blocks=NUM_BLOCKS, block_size=BLOCK_SIZE) pool = PrefixBlockPool(cfg) ctrl = OmniPrefixCacheController(pool, cfg, eager=True) slots = torch.tensor([0, 1, 2], dtype=torch.int64) src = torch.arange(3 * HIDDEN, dtype=DTYPE).reshape(3, HIDDEN) landing = torch.zeros_like(src) class _HostEvent: n = 0 def synchronize(self): self.n += 1 landing.copy_(src) event = _HostEvent() chunk = _WriteChunk(slots_cpu=slots, tensors={HIDDEN_KEY: src}) chunk.host = {HIDDEN_KEY: landing} task = WriteTask( tid=1, req_id="r", write_n=1, schedule=WriteSchedule.JOIN_NEXT_STEP, chunks=[chunk], staging_slot=0, step_d2h_event=event, ) rows = ctrl.fetch_host(task, slots, HIDDEN_KEY) assert event.n == 1 assert torch.equal(rows, src) def _bare_controller(eager=True): from vllm_omni.core.prefix_cache.block_pool import PrefixBlockPool from vllm_omni.core.prefix_cache.controller import OmniPrefixCacheController cfg = PrefixCacheConfig(num_blocks=NUM_BLOCKS, block_size=BLOCK_SIZE) pool = PrefixBlockPool(cfg) pool.ensure_key(HIDDEN_KEY, DTYPE, HIDDEN) ctrl = OmniPrefixCacheController(pool, cfg, eager=eager) return ctrl, pool def _bare_task(tid=7, schedule=WriteSchedule.JOIN_NEXT_STEP, host=None, tensors=None, budget=None, queued=False): from vllm_omni.core.prefix_cache.controller import TaskState, WriteTask, _WriteChunk slots = torch.tensor([0, 1], dtype=torch.int64) chunk = _WriteChunk(slots_cpu=slots, tensors=tensors or {}, budget=budget) if host is not None: chunk.host = {HIDDEN_KEY: host} task = WriteTask(tid=tid, req_id="r", write_n=1, schedule=schedule, chunks=[chunk]) if queued: task.transition(TaskState.QUEUED) return task, slots def test_task_state_is_a_strict_chain(): """PENDING -> QUEUED -> COPYING -> HOST_READY -> WRITTEN, one step at a time.""" from vllm_omni.core.prefix_cache.controller import TaskState task, _ = _bare_task() assert task.state is TaskState.PENDING assert not task.claim_copy() # PENDING may not skip QUEUED with pytest.raises(OmniPrefixCacheUnmatchError, match="illegal transition PENDING -> HOST_READY"): task.transition(TaskState.HOST_READY) task.transition(TaskState.QUEUED) assert task.claim_copy() assert task.state is TaskState.COPYING and not task.host_ready.is_set() assert not task.claim_copy() # second claimant loses task.mark_host_ready() assert task.state is TaskState.HOST_READY and task.host_ready.is_set() and not task.done.is_set() with pytest.raises(OmniPrefixCacheUnmatchError, match="illegal transition HOST_READY -> COPYING"): task.transition(TaskState.COPYING) # no going back task.mark_done() assert task.state is TaskState.WRITTEN and task.done.is_set() assert not task.mark_failed() # terminal stays terminal assert task.state is TaskState.WRITTEN def test_task_failed_sets_both_events_and_closes_append(): from vllm_omni.core.prefix_cache.controller import TaskState, _WriteChunk task, slots = _bare_task() assert task.mark_failed() assert task.state is TaskState.FAILED and task.host_ready.is_set() and task.done.is_set() assert task.append_chunk(_WriteChunk(slots_cpu=slots, tensors={})) is TaskState.FAILED def test_escalate_leaves_claimed_task_alone(): """Once the worker owns the copy stage (COPYING), escalate must not re-queue the tid — that re-queue was the double-write race.""" from vllm_omni.core.prefix_cache.controller import TaskState ctrl, _ = _bare_controller(eager=True) ctrl._eager = False # exercise the queue path without a worker thread later, _ = _bare_task(tid=1, schedule=WriteSchedule.JOIN_ON_FINISH) ctrl.submit(later, queued=False) assert later.state is TaskState.PENDING ctrl.escalate([1]) assert later.state is TaskState.QUEUED and list(ctrl._queue_hi) == [1] ctrl.escalate([1]) # already high-priority: no duplicate assert list(ctrl._queue_hi) == [1] ctrl._queue_hi.clear() later.transition(TaskState.COPYING) ctrl.escalate([1]) assert list(ctrl._queue_hi) == [] and list(ctrl._queue_lo) == [] def test_join_on_stuck_committer_raises_instead_of_hanging(): from vllm_omni.core.prefix_cache.block_pool import PrefixBlockPool from vllm_omni.core.prefix_cache.controller import OmniPrefixCacheController, TaskState cfg = PrefixCacheConfig(num_blocks=NUM_BLOCKS, block_size=BLOCK_SIZE, staging_claim_timeout_s=0.05) ctrl = OmniPrefixCacheController(PrefixBlockPool(cfg), cfg, eager=True) ctrl._eager = False # queue path without a worker thread: nothing will finish the task task, _ = _bare_task(tid=1, schedule=WriteSchedule.JOIN_ON_FINISH) ctrl.submit(task, queued=True) task.transition(TaskState.COPYING) with pytest.raises( OmniPrefixCacheUnmatchError, match=r"task 1 \(join_on_finish\) did not reach host_ready .*state=COPYING" ): ctrl.join_host_ready([1]) with pytest.raises(OmniPrefixCacheUnmatchError, match="did not reach done"): ctrl.join([1]) def test_scatter_host_ready_writes_only_staged_tasks(): from vllm_omni.core.prefix_cache.controller import TaskState ctrl, pool = _bare_controller() task, slots = _bare_task(host=torch.ones(2, HIDDEN), queued=True) ctrl._tasks[task.tid] = task assert task.claim_copy() ctrl._blocked.append(task.tid) ctrl._scatter_host_ready() # COPYING: not ready, stays blocked assert ctrl._blocked == [task.tid] and list(ctrl._completed) == [] task.mark_host_ready() ctrl._scatter_host_ready() assert ctrl._blocked == [] and list(ctrl._completed) == [task.tid] assert task.state is TaskState.WRITTEN assert torch.equal(pool.rows(HIDDEN_KEY, slots), torch.ones(2, HIDDEN)) def test_budget_ticket_uncharges_once_per_clone(): from vllm_omni.core.prefix_cache.controller import _BudgetTicket ctrl, _ = _bare_controller() ticket = _BudgetTicket(nbytes=100) ctrl.reserve(100) a, _ = _bare_task(tid=1, budget=ticket) b, _ = _bare_task(tid=2, budget=ticket) ctrl.pin_budget(ticket, 1) ctrl.pin_budget(ticket, 2) ctrl._release_staged_bytes(a) assert ctrl._staged_bytes == 100 # b still holds the clone ctrl._release_staged_bytes(a) # idempotent per tid assert ctrl._staged_bytes == 100 ctrl._release_staged_bytes(b) assert ctrl._staged_bytes == 0 ctrl._release_staged_bytes(b) # freed once, never twice assert ctrl._staged_bytes == 0 def test_fail_task_after_copy_does_not_uncharge_twice(): from vllm_omni.core.prefix_cache.controller import TaskState, _BudgetTicket ctrl, _ = _bare_controller() ticket = _BudgetTicket(nbytes=64) ctrl.reserve(64) task, _ = _bare_task(tid=3, budget=ticket, queued=True) ctrl._tasks[task.tid] = task ctrl.pin_budget(ticket, 3) assert task.claim_copy() task.mark_host_ready() ctrl._release_staged_bytes(task) # what _copy_task does on HOST_READY assert ctrl._staged_bytes == 0 ctrl._fail_task(3) assert task.state is TaskState.FAILED and ctrl._staged_bytes == 0 assert list(ctrl._failed) == [3] ctrl._fail_task(3) # terminal: no second publish assert list(ctrl._failed) == [3] def test_from_vllm_config_uses_batched_tokens(): cfg = PrefixCacheConfig.from_vllm_config( num_blocks=NUM_BLOCKS, block_size=BLOCK_SIZE, scheduler_config=SimpleNamespace(max_num_batched_tokens=8192, max_model_len=32768, max_num_seqs=64), ) assert cfg.staging_capacity_tokens == 8192 assert cfg.staging_depth == 4 cfg_fallback = PrefixCacheConfig.from_vllm_config( num_blocks=NUM_BLOCKS, block_size=BLOCK_SIZE, scheduler_config=SimpleNamespace(max_num_batched_tokens=None, max_model_len=4096), ) assert cfg_fallback.staging_capacity_tokens == 4096