From 8916c0d8ab02fa5f8d2c91db4245aa1db375df26 Mon Sep 17 00:00:00 2001 From: rk9595 Date: Mon, 31 Aug 2026 16:19:44 +0530 Subject: [PATCH 1/2] [Bugfix] Stop vocoder stages from declaring SupportsPP without the PP attribute MiMoAudioToken2WavForConditionalGenerationVLLM and CovoAudioCode2WavForConditionalGeneration declare SupportsPP but never provide make_empty_intermediate_tensors. Since vLLM 0.28 turned that member from a method on the Protocol class into a bare annotation, nothing supplies it, so vllm.model_executor.models.interfaces.supports_pp() reports True for both while any read of the attribute raises AttributeError -- the failure fixed for the talker in #6803. Neither stage implements pipeline parallelism: both are vocoders with no inner LM to delegate to, and their forward methods ignore intermediate_tensors (mypy already flagged both signatures as incompatible with the supertype). Drop the declaration rather than inventing a PP implementation they do not have. Add a static contract test over every vllm_omni class declaring SupportsPP, asserting it defines the attribute or assigns it in __init__. The check is AST based because models are too heavy to construct in a unit test and the repo convention is to assign on the instance, which no class-level hasattr sees. Reverting the #6803 fix makes this test fail on that class. Closes #6859 Signed-off-by: rk9595 --- .../models/test_supports_pp_contract.py | 92 +++++++++++++++++++ .../models/covo_audio/covo_audio_code2wav.py | 3 +- .../models/mimo_audio/mimo_audio_code2wav.py | 6 +- 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 tests/model_executor/models/test_supports_pp_contract.py diff --git a/tests/model_executor/models/test_supports_pp_contract.py b/tests/model_executor/models/test_supports_pp_contract.py new file mode 100644 index 00000000000..afbc75e7dc1 --- /dev/null +++ b/tests/model_executor/models/test_supports_pp_contract.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +"""Every model declaring ``SupportsPP`` must supply ``make_empty_intermediate_tensors``. + +vLLM 0.28 turned ``SupportsPP.make_empty_intermediate_tensors`` from a method on +the Protocol class into a bare annotation, so declaring ``SupportsPP`` no longer +supplies one. A class that declares the interface without providing the +attribute is advertised as pipeline-parallel capable by +``vllm.model_executor.models.interfaces.supports_pp`` and then raises +``AttributeError`` wherever the attribute is read (#6790, #6859). + +The check is static: models import torch and vLLM layers, so building them is far +too heavy for a unit test, and the repo-wide convention is to assign the +attribute on the instance inside ``__init__`` (mirroring upstream +``Qwen2ForCausalLM``), which no class-level ``hasattr`` would see. +""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest + +pytestmark = [pytest.mark.core_model] + +MODELS_DIR = Path(__file__).resolve().parents[3] / "vllm_omni" / "model_executor" / "models" +ATTR = "make_empty_intermediate_tensors" + + +def _declares_supports_pp(node: ast.ClassDef) -> bool: + for base in node.bases: + if isinstance(base, ast.Name) and base.id == "SupportsPP": + return True + if isinstance(base, ast.Attribute) and base.attr == "SupportsPP": + return True + return False + + +def _provides_attr(node: ast.ClassDef) -> bool: + """True if the class defines the attribute or assigns it to ``self``.""" + for child in ast.walk(node): + if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)) and child.name == ATTR: + return True + if isinstance(child, ast.Assign): + for target in child.targets: + if isinstance(target, ast.Attribute) and target.attr == ATTR: + return True + if isinstance(child, ast.AnnAssign): + target = child.target + if isinstance(target, ast.Attribute) and target.attr == ATTR: + return True + if isinstance(target, ast.Name) and target.id == ATTR: + return True + return False + + +def _supports_pp_classes() -> list[tuple[str, str, bool]]: + """Return ``(relative_path, class_name, provides_attr)`` for every SupportsPP class.""" + found: list[tuple[str, str, bool]] = [] + for path in sorted(MODELS_DIR.rglob("*.py")): + try: + tree = ast.parse(path.read_text(encoding="utf-8")) + except SyntaxError: # pragma: no cover - vendored files are still valid python + continue + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and _declares_supports_pp(node): + found.append( + ( + str(path.relative_to(MODELS_DIR.parents[2])), + node.name, + _provides_attr(node), + ) + ) + return found + + +@pytest.mark.cpu +def test_supports_pp_classes_provide_make_empty_intermediate_tensors(): + classes = _supports_pp_classes() + assert classes, f"no SupportsPP classes found under {MODELS_DIR}; the scan is broken" + + missing = [f"{path}::{name}" for path, name, provides in classes if not provides] + assert not missing, ( + "These classes declare SupportsPP but never provide " + f"`{ATTR}`, so vLLM advertises them as pipeline-parallel capable and any " + "read of the attribute raises AttributeError. Either assign it (usually " + "`self." + ATTR + " = self.model." + ATTR + "` after building the inner " + "model) or drop SupportsPP if the model does not implement pipeline " + "parallelism:\n " + "\n ".join(missing) + ) diff --git a/vllm_omni/model_executor/models/covo_audio/covo_audio_code2wav.py b/vllm_omni/model_executor/models/covo_audio/covo_audio_code2wav.py index 15cd0875e64..cca19c48cce 100644 --- a/vllm_omni/model_executor/models/covo_audio/covo_audio_code2wav.py +++ b/vllm_omni/model_executor/models/covo_audio/covo_audio_code2wav.py @@ -9,7 +9,6 @@ import torch from torch import nn from vllm.config import VllmConfig -from vllm.model_executor.models import SupportsPP from vllm.v1.outputs import SamplerOutput from vllm.v1.sample.metadata import SamplingMetadata from vllm.v1.sample.sampler import Sampler @@ -18,7 +17,7 @@ from .token2wav import JsonHParams, Token2WavDecoder -class CovoAudioCode2WavForConditionalGeneration(nn.Module, SupportsPP): +class CovoAudioCode2WavForConditionalGeneration(nn.Module): def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): super().__init__() diff --git a/vllm_omni/model_executor/models/mimo_audio/mimo_audio_code2wav.py b/vllm_omni/model_executor/models/mimo_audio/mimo_audio_code2wav.py index 5423788a0af..284290e6692 100644 --- a/vllm_omni/model_executor/models/mimo_audio/mimo_audio_code2wav.py +++ b/vllm_omni/model_executor/models/mimo_audio/mimo_audio_code2wav.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + # Copyright 2025 Xiaomi Corporation. import logging import os @@ -13,7 +16,6 @@ from transformers import AutoTokenizer, Qwen2Config from vllm.config import VllmConfig from vllm.model_executor.layers.logits_processor import LogitsProcessor -from vllm.model_executor.models import SupportsPP from vllm.sequence import IntermediateTensors from vllm.v1.outputs import SamplerOutput from vllm.v1.sample.metadata import SamplingMetadata @@ -432,7 +434,7 @@ def get_tokenizer_worker( return _TOKENIZER_WORKER_CACHE[key] -class MiMoAudioToken2WavForConditionalGenerationVLLM(nn.Module, SupportsPP): +class MiMoAudioToken2WavForConditionalGenerationVLLM(nn.Module): """Decode MiMo audio codes to waveform for the code2wav stage.""" have_multimodal_outputs = True From f1142e33d6ba2651e804a16e703db6867d8ccec2 Mon Sep 17 00:00:00 2001 From: Rakesh Kariya Date: Wed, 16 Sep 2026 10:42:30 +0530 Subject: [PATCH 2/2] [Bugfix] Reject bare SupportsPP annotations in the contract test A bare annotation binds nothing at runtime, which is exactly how make_empty_intermediate_tensors went missing in vLLM 0.28, so only count an AnnAssign that carries a value. Also count class-level assignment, which the Assign branch previously missed. Adds bidirectional tests over the checker itself. Signed-off-by: Rakesh Kariya --- .../models/test_supports_pp_contract.py | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/tests/model_executor/models/test_supports_pp_contract.py b/tests/model_executor/models/test_supports_pp_contract.py index afbc75e7dc1..c78effc3fa4 100644 --- a/tests/model_executor/models/test_supports_pp_contract.py +++ b/tests/model_executor/models/test_supports_pp_contract.py @@ -39,7 +39,12 @@ def _declares_supports_pp(node: ast.ClassDef) -> bool: def _provides_attr(node: ast.ClassDef) -> bool: - """True if the class defines the attribute or assigns it to ``self``.""" + """True if the class defines the attribute or binds it on the class or ``self``. + + An annotation without a value (``make_empty_intermediate_tensors: Callable``) + binds nothing at runtime -- that is exactly how the attribute went missing in + vLLM 0.28 -- so only an ``AnnAssign`` carrying a value counts. + """ for child in ast.walk(node): if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)) and child.name == ATTR: return True @@ -47,7 +52,9 @@ def _provides_attr(node: ast.ClassDef) -> bool: for target in child.targets: if isinstance(target, ast.Attribute) and target.attr == ATTR: return True - if isinstance(child, ast.AnnAssign): + if isinstance(target, ast.Name) and target.id == ATTR: + return True + if isinstance(child, ast.AnnAssign) and child.value is not None: target = child.target if isinstance(target, ast.Attribute) and target.attr == ATTR: return True @@ -90,3 +97,39 @@ def test_supports_pp_classes_provide_make_empty_intermediate_tensors(): "model) or drop SupportsPP if the model does not implement pipeline " "parallelism:\n " + "\n ".join(missing) ) + + +BINDS_ATTR = [ + pytest.param("def " + ATTR + "(self, *a, **kw): ...", id="method"), + pytest.param("def __init__(self):\n self." + ATTR + " = self.model." + ATTR, id="self-assign"), + pytest.param( + "def __init__(self):\n self." + ATTR + ": Callable = self.model." + ATTR, + id="self-annotated-assign", + ), + pytest.param(ATTR + " = staticmethod(_make_empty)", id="class-assign"), + pytest.param(ATTR + ": Callable = staticmethod(_make_empty)", id="class-annotated-assign"), +] + +BINDS_NOTHING = [ + pytest.param(ATTR + ": Callable", id="bare-class-annotation"), + pytest.param("def __init__(self):\n self." + ATTR + ": Callable", id="bare-self-annotation"), + pytest.param("def forward(self, intermediate_tensors=None): ...", id="unrelated-method"), +] + + +def _parse_class(body: str) -> ast.ClassDef: + node = ast.parse(f"class M(nn.Module, SupportsPP):\n {body}\n").body[0] + assert isinstance(node, ast.ClassDef) and _declares_supports_pp(node) + return node + + +@pytest.mark.cpu +@pytest.mark.parametrize("body", BINDS_ATTR) +def test_provides_attr_accepts_real_bindings(body): + assert _provides_attr(_parse_class(body)) + + +@pytest.mark.cpu +@pytest.mark.parametrize("body", BINDS_NOTHING) +def test_provides_attr_rejects_bare_annotations(body): + assert not _provides_attr(_parse_class(body))