[Bugfix][MammothModa2] Stop allocating AR KV cache for the replaced language model (#8095)

Signed-off-by: MrlixiangWE <mrdanaer@gmail.com>
Co-authored-by: zijianc2 <157244773+zijianc2@users.noreply.github.com>
Co-authored-by: hsliuustc0106 <222337142+hsliuustc0106@users.noreply.github.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
authored and GitHub committed 2026-09-25 21:28:59 +08:00
1 parent 29dacbd3ef
commit b41e6707b1
2 files changed
+193 -1

No files matched your search

@@ -0,0 +1,145 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""The AR stage must register attention layers for its own language model only.
The Qwen-VL parent ``__init__`` builds a stock language model before MammothModa2
replaces it. Its attention layers stay in ``static_forward_context`` unless they are
removed, and the KV cache is then sized and allocated for both models. The parent
``__init__`` and the replacement are stubbed here so only the registry is exercised.
"""
import gc
import weakref
from types import SimpleNamespace
import pytest
import torch
from torch import nn
from vllm.model_executor.models.utils import StageMissingLayer
import vllm_omni.model_executor.models.mammoth_moda2.mammoth_moda2 as mm2
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
NUM_LAYERS = 3
VISION_LAYER = "ar.visual.blocks.0.attn"
AR_CLASSES = [
(mm2.MammothModa2ARForConditionalGeneration, mm2.Qwen2_5_VLForConditionalGeneration),
(mm2.MammothModa2Qwen3ARForConditionalGeneration, mm2.Qwen3VLForConditionalGeneration),
]
AR_IDS = ["preview_qwen2_5_vl", "dev_qwen3_vl"]
class _FakeAttention(nn.Module):
def __init__(self, registry: dict, name: str):
super().__init__()
registry[name] = self
class _FakeLM(nn.Module):
def __init__(self, registry: dict, prefix: str):
super().__init__()
self.layers = nn.ModuleList(
_FakeAttention(registry, f"{prefix}.layers.{i}.self_attn.attn") for i in range(NUM_LAYERS)
)
self.make_empty_intermediate_tensors = lambda *args, **kwargs: None
class _FakeCompiledLM(_FakeLM):
cleanups = 0
def cleanup(self):
type(self).cleanups += 1
def _build(
monkeypatch,
ar_class,
parent,
*,
encoder_only=False,
parent_lm=_FakeLM,
parent_registers=True,
pp_rank=(1, True),
stray_parent_layer=False,
):
registry: dict = {}
built = {}
def parent_init(self, *, vllm_config, prefix=""):
nn.Module.__init__(self)
self.visual = _FakeAttention(registry, VISION_LAYER)
stock = nn.Module()
stock.model = parent_lm(registry if parent_registers else {}, f"{prefix}.language_model.model")
built["parent_lm"] = weakref.ref(stock)
if stray_parent_layer:
# A parent-prefixed layer that is not part of the module being dropped.
registry[f"{prefix}.language_model.model.layers.9.self_attn.attn"] = nn.Module()
self.language_model = StageMissingLayer("language_model", stock) if encoder_only else stock
# The model module only imports get_current_vllm_config with this fix; keep the
# reverted source testable so the registry assertion is what fails there.
monkeypatch.setattr(
mm2,
"get_current_vllm_config",
lambda: SimpleNamespace(compilation_config=SimpleNamespace(static_forward_context=registry)),
raising=False,
)
monkeypatch.setattr(parent, "__init__", parent_init)
monkeypatch.setattr(mm2, "init_vllm_registered_model", lambda *, prefix, **kwargs: _FakeLM(registry, prefix))
monkeypatch.setattr(mm2, "MammothModa2Qwen3ForCausalLM", lambda *, vllm_config, prefix: _FakeLM(registry, prefix))
monkeypatch.setattr(mm2, "TorchCompileWithNoGuardsWrapper", _FakeCompiledLM, raising=False)
monkeypatch.setattr(torch.accelerator, "empty_cache", lambda: None)
world_size, is_first_rank = pp_rank
monkeypatch.setattr(
mm2, "get_pp_group", lambda: SimpleNamespace(world_size=world_size, is_first_rank=is_first_rank)
)
hf_config = SimpleNamespace(llm_config=SimpleNamespace(text_config=SimpleNamespace()))
vllm_config = SimpleNamespace(
model_config=SimpleNamespace(hf_config=hf_config, architectures=["MammothModa2Model"])
)
vllm_config.with_hf_config = lambda hf, architectures=None: vllm_config
model = ar_class(vllm_config=vllm_config, prefix="ar")
return model, registry, built
@pytest.mark.parametrize("encoder_only", [False, True], ids=["default", "mm_encoder_only"])
@pytest.mark.parametrize(("ar_class", "parent"), AR_CLASSES, ids=AR_IDS)
def test_ar_stage_registers_only_the_replacement_language_model(monkeypatch, ar_class, parent, encoder_only):
model, registry, built = _build(monkeypatch, ar_class, parent, encoder_only=encoder_only)
assert set(registry) == {f"ar.language_model.layers.{i}.self_attn.attn" for i in range(NUM_LAYERS)} | {VISION_LAYER}
gc.collect()
assert built["parent_lm"]() is None
@pytest.mark.parametrize(("ar_class", "parent"), AR_CLASSES, ids=AR_IDS)
def test_compiled_parent_language_model_releases_its_hook(monkeypatch, ar_class, parent):
_FakeCompiledLM.cleanups = 0
_build(monkeypatch, ar_class, parent, parent_lm=_FakeCompiledLM)
assert _FakeCompiledLM.cleanups == 1
@pytest.mark.parametrize(("ar_class", "parent"), AR_CLASSES, ids=AR_IDS)
def test_unexpected_parent_registry_fails_loudly(monkeypatch, ar_class, parent):
with pytest.raises(RuntimeError, match="Unexpected attention registry"):
_build(monkeypatch, ar_class, parent, parent_registers=False)
@pytest.mark.parametrize(("ar_class", "parent"), AR_CLASSES, ids=AR_IDS)
def test_later_pipeline_rank_without_parent_layers_starts(monkeypatch, ar_class, parent):
_, registry, _ = _build(monkeypatch, ar_class, parent, parent_registers=False, pp_rank=(2, False))
assert set(registry) == {f"ar.language_model.layers.{i}.self_attn.attn" for i in range(NUM_LAYERS)} | {VISION_LAYER}
@pytest.mark.parametrize(("ar_class", "parent"), AR_CLASSES, ids=AR_IDS)
def test_first_pipeline_rank_without_parent_layers_fails(monkeypatch, ar_class, parent):
with pytest.raises(RuntimeError, match="Unexpected attention registry"):
_build(monkeypatch, ar_class, parent, parent_registers=False, pp_rank=(2, True))
@pytest.mark.parametrize(("ar_class", "parent"), AR_CLASSES, ids=AR_IDS)
def test_later_pipeline_rank_still_rejects_a_leftover_parent_layer(monkeypatch, ar_class, parent):
with pytest.raises(RuntimeError, match="Unexpected attention registry"):
_build(monkeypatch, ar_class, parent, parent_registers=False, pp_rank=(2, False), stray_parent_layer=True)
@@ -1,5 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import gc
from collections.abc import Callable, Iterable, Mapping
from itertools import islice
from typing import Any
@@ -8,8 +12,10 @@ import torch
from torch import nn
from transformers import Qwen2Config, Qwen3VLProcessor
from transformers.models.qwen2_5_vl.processing_qwen2_5_vl import Qwen2_5_VLProcessor
from vllm.config import CacheConfig, VllmConfig
from vllm.compilation.wrapper import TorchCompileWithNoGuardsWrapper
from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
from vllm.distributed import get_pp_group
from vllm.logger import init_logger
from vllm.model_executor.layers.layernorm import RMSNorm
from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.layers.quantization import QuantizationConfig
@@ -40,6 +46,7 @@ from vllm.model_executor.models.qwen3_vl import (
from vllm.model_executor.models.utils import (
AutoWeightsLoader,
PPMissingLayer,
StageMissingLayer,
WeightsMapper,
init_vllm_registered_model,
is_pp_missing_parameter,
@@ -57,6 +64,8 @@ from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights, is_interleaved
from vllm_omni.transformers_utils.configs.mammoth_moda2 import Mammothmoda2Config
logger = init_logger(__name__)
def _runtime_meta(runtime_info: dict[str, Any]) -> dict[str, Any]:
"""Accept both legacy flat metadata and the canonical nested payload."""
@@ -623,6 +632,42 @@ class MammothModa2Qwen3ForCausalLM(MammothModa2Qwen2ForCausalLM):
)
def _drop_parent_language_model(model: nn.Module, prefix: str) -> None:
"""Unregister and release the language model built by the Qwen-VL parent ``__init__``.
The parent builds a stock Qwen LM whose attention layers register in the static
forward context as ``{prefix}.language_model.model.layers.*``. MammothModa2 replaces
it with its own LM (``{prefix}.language_model.layers.*``); without this, both sets of
layers are counted when the KV cache is sized and each gets its own KV blocks.
"""
old = model.language_model
if isinstance(old, StageMissingLayer): # mm_encoder_only
old = old.__dict__["module"]
ctx = get_current_vllm_config().compilation_config.static_forward_context
owned = set(old.modules())
dropped = [name for name, layer in ctx.items() if layer in owned]
for name in dropped:
del ctx[name]
stale = [name for name in ctx if name.startswith(maybe_prefix(prefix, "language_model."))]
# A later pipeline rank may hold none of the parent's layers, so only the first
# rank has to find some; no rank may leave any behind.
pp_group = get_pp_group()
must_drop = pp_group.world_size == 1 or pp_group.is_first_rank
if (must_drop and not dropped) or stale:
raise RuntimeError(
f"Unexpected attention registry for the parent language model (dropped {len(dropped)}, left {stale}); "
"check the Qwen-VL __init__."
)
# With torch.compile, the compile wrapper's bytecode hook would keep the module alive.
for module in owned:
if isinstance(module, TorchCompileWithNoGuardsWrapper):
module.cleanup()
logger.debug("Dropped %d attention layers of the parent language model.", len(dropped))
del model.language_model, old, owned
gc.collect()
torch.accelerator.empty_cache()
@MULTIMODAL_REGISTRY.register_processor(
MammothModa2ARMultiModalProcessor,
info=MammothModa2ARProcessingInfo,
@@ -656,6 +701,7 @@ class MammothModa2ARForConditionalGeneration(Qwen2_5_VLForConditionalGeneration)
ar_vllm_config = vllm_config.with_hf_config(ar_hf_config, architectures=vllm_config.model_config.architectures)
# Initialize multi-modal components like the vision tower first.
super().__init__(vllm_config=ar_vllm_config, prefix=prefix)
_drop_parent_language_model(self, prefix)
# Replace with the custom MoE language model.
lm_hf_config = getattr(
ar_vllm_config.model_config.hf_config, "text_config", ar_vllm_config.model_config.hf_config
@@ -824,6 +870,7 @@ class MammothModa2Qwen3ARForConditionalGeneration(Qwen3VLForConditionalGeneratio
architectures=vllm_config.model_config.architectures,
)
super().__init__(vllm_config=ar_vllm_config, prefix=prefix)
_drop_parent_language_model(self, prefix)
text_config = ar_hf_config.text_config
self.language_model = MammothModa2Qwen3ForCausalLM(
vllm_config=ar_vllm_config.with_hf_config(text_config),