Files
vllm-omni/tests/worker/test_capture_replay_contract.py
T

94 lines
3.6 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Static coupling check for the ``has_preprocess`` capture/replay buffers.
For models with ``has_preprocess``, CUDA-graph *capture* (``_dummy_run``) and
*replay* (``_preprocess``) MUST read from the same pre-allocated buffers
(``self.input_ids.gpu`` + ``self.inputs_embeds.gpu``); otherwise replay reads a
buffer capture never wrote and produces garbage. This is a source-level check
(no GPU) that catches a one-sided edit to either branch.
The check is AST-scoped: it inspects ONLY the body of the ``has_preprocess``
guarded branch (not a line window), so a buffer read removed from that branch
cannot be masked by an adjacent ``else``/``elif`` that happens to read it. It
matches by attribute chain (``self.input_ids.gpu``), so local variable renames
do not break it.
Scope: the BASE runner (``OmniGPUModelRunner``) and the generation runner's
``_dummy_run`` override (which gained the branch in RFC #5450 C6; its
``_preprocess`` is inherited from the base).
"""
import ast
import inspect
import textwrap
import pytest
from vllm_omni.worker.gpu_generation_model_runner import GPUGenerationModelRunner
from vllm_omni.worker.gpu_model_runner import OmniGPUModelRunner
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_REQUIRED_BUFFERS = {"self.input_ids.gpu", "self.inputs_embeds.gpu"}
def _attr_chain(node: ast.AST) -> str | None:
"""Reconstruct a dotted ``self.x.y`` chain from an ast.Attribute node."""
parts: list[str] = []
cur = node
while isinstance(cur, ast.Attribute):
parts.append(cur.attr)
cur = cur.value
if isinstance(cur, ast.Name):
parts.append(cur.id)
return ".".join(reversed(parts))
return None
def _attr_chains_in(nodes: list[ast.stmt]) -> set[str]:
chains: set[str] = set()
for stmt in nodes:
for inner in ast.walk(stmt):
if isinstance(inner, ast.Attribute):
chain = _attr_chain(inner)
if chain is not None:
chains.add(chain)
return chains
def _has_preprocess_branch_reads_both_buffers(method) -> bool:
"""True iff SOME ``if has_preprocess:`` branch body reads BOTH buffers.
Only the guarded branch's own ``body`` is inspected (elif/else excluded),
which is what makes a one-sided buffer removal fail.
"""
src = textwrap.dedent(inspect.getsource(method))
tree = ast.parse(src)
guarded = [node for node in ast.walk(tree) if isinstance(node, ast.If) and "has_preprocess" in ast.dump(node.test)]
assert guarded, f"{method.__qualname__}: has_preprocess branch removed entirely"
return any(_REQUIRED_BUFFERS <= _attr_chains_in(node.body) for node in guarded)
@pytest.mark.parametrize(
("runner_cls", "method_name"),
[
(OmniGPUModelRunner, "_dummy_run"),
(OmniGPUModelRunner, "_preprocess"),
(GPUGenerationModelRunner, "_dummy_run"),
],
)
def test_has_preprocess_branch_reads_shared_buffers(runner_cls, method_name):
method = getattr(runner_cls, method_name)
assert _has_preprocess_branch_reads_both_buffers(method), (
f"{runner_cls.__name__}.{method_name}: the has_preprocess branch no "
"longer reads BOTH self.input_ids.gpu and self.inputs_embeds.gpu — capture "
"and replay would diverge."
)
def test_generation_dummy_run_overrides_with_branch():
# Guard against the override being deleted (which would silently fall back
# to the base implementation and vacuously pass the parametrized check).
assert "_dummy_run" in GPUGenerationModelRunner.__dict__