[Bugfix][MiniCPMO45] Return bare tensor in Thinker forward to fix repetition loop (#7497) (#7517)

Signed-off-by: BeatSeat <wendavid552@gmail.com>
This commit is contained in:
BeatSeat authored and GitHub committed 2026-09-14 21:11:15 +08:00
1 parent 446c2b5474
commit 3d793481a0
3 files changed
+127 -17

No files matched your search

@@ -0,0 +1,123 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from unittest.mock import MagicMock
import pytest
import torch
from vllm_omni.model_executor.models.minicpmo_4_5.minicpmo_4_5_omni import (
MiniCPMO45OmniForConditionalGeneration,
)
from vllm_omni.model_executor.models.minicpmo_4_5.minicpmo_4_5_omni_llm import (
MiniCPMO45OmniLLMForConditionalGeneration,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_thinker_forward_returns_bare_hidden_states():
"""Verify that MiniCPMO45OmniLLMForConditionalGeneration.forward returns
a bare hidden_states Tensor (not a tuple), which stock vLLM GPUModelRunner
expects.
"""
model = MiniCPMO45OmniLLMForConditionalGeneration.__new__(MiniCPMO45OmniLLMForConditionalGeneration)
torch.nn.Module.__init__(model)
model.llm = MagicMock()
mock_hidden_states = torch.randn(4, 4096)
model.llm.model = MagicMock(return_value=mock_hidden_states)
out = model.forward(
input_ids=torch.tensor([1, 2, 3, 4]),
positions=torch.tensor([0, 1, 2, 3]),
inputs_embeds=torch.randn(4, 4096),
)
assert isinstance(out, torch.Tensor), f"Expected bare torch.Tensor, got {type(out)}"
assert out.shape == (4, 4096)
assert torch.equal(out, mock_hidden_states)
def test_thinker_forward_builds_embeddings_once_when_inputs_embeds_none():
"""Verify that when inputs_embeds is None, forward builds input embeddings
exactly once and forwards them to self.llm.model with input_ids=None."""
model = MiniCPMO45OmniLLMForConditionalGeneration.__new__(MiniCPMO45OmniLLMForConditionalGeneration)
torch.nn.Module.__init__(model)
model.llm = MagicMock()
mock_hidden_states = torch.randn(4, 4096)
model.llm.model = MagicMock(return_value=mock_hidden_states)
mock_mm_embeds = [torch.randn(2, 4096)]
mock_input_embeds = torch.randn(4, 4096)
model.get_multimodal_embeddings = MagicMock(return_value=mock_mm_embeds)
model.get_input_embeddings = MagicMock(return_value=mock_input_embeds)
input_ids = torch.tensor([1, 2, 3, 4])
positions = torch.tensor([0, 1, 2, 3])
out = model.forward(
input_ids=input_ids,
positions=positions,
inputs_embeds=None,
)
# Assert input embeddings are built exactly once
model.get_multimodal_embeddings.assert_called_once()
model.get_input_embeddings.assert_called_once_with(input_ids, mock_mm_embeds)
# Verify forwarded to self.llm.model with input_ids=None and inputs_embeds
model.llm.model.assert_called_once_with(
None,
positions,
None,
inputs_embeds=mock_input_embeds,
)
assert isinstance(out, torch.Tensor), f"Expected bare torch.Tensor, got {type(out)}"
assert torch.equal(out, mock_hidden_states)
@pytest.mark.parametrize(
("thinker_shape", "expected_shape"),
[
((1, 4096), (1, 4096)),
((5, 4096), (5, 4096)),
((1, 1, 4096), (1, 4096)),
((1, 5, 4096), (5, 4096)),
],
)
def test_wrapper_shape_normalization(thinker_shape, expected_shape):
"""Verify that MiniCPMO45OmniForConditionalGeneration normalizes 3D
hidden states of shape (1, seq_len, H) to (seq_len, H), while leaving
2D hidden states intact (especially (1, H) for single-token decode)."""
wrapper = MiniCPMO45OmniForConditionalGeneration.__new__(MiniCPMO45OmniForConditionalGeneration)
torch.nn.Module.__init__(wrapper)
wrapper.model_stage = "llm"
wrapper.thinker = torch.nn.Module()
mock_hidden = torch.randn(*thinker_shape)
wrapper.thinker.forward = MagicMock(return_value=mock_hidden)
out = wrapper.forward(
input_ids=torch.tensor([1]),
positions=torch.tensor([0]),
)
assert out.text_hidden_states.shape == expected_shape, (
f"Expected text_hidden_states shape {expected_shape}, got {out.text_hidden_states.shape}"
)
assert out.multimodal_outputs["latent"].shape == expected_shape, (
f"Expected latent shape {expected_shape}, got {out.multimodal_outputs['latent'].shape}"
)
if __name__ == "__main__":
test_thinker_forward_returns_bare_hidden_states()
test_thinker_forward_builds_embeddings_once_when_inputs_embeds_none()
for shape, exp in [
((1, 4096), (1, 4096)),
((5, 4096), (5, 4096)),
((1, 1, 4096), (1, 4096)),
((1, 5, 4096), (5, 4096)),
]:
test_wrapper_shape_normalization(shape, exp)
print("ALL TESTS PASSED SUCCESSFULLY!")
@@ -547,13 +547,13 @@ class MiniCPMO45OmniForConditionalGeneration(nn.Module, SupportsMultiModal, Supp
)
if isinstance(thinker_output, tuple):
embeds, text_hidden_states = thinker_output
_, text_hidden_states = thinker_output
else:
text_hidden_states = thinker_output
# Prepare hidden states for downstream stages
# Ensure correct shape: (batch_size, seq_len, hidden_dim)
if added_batch_dim:
# Ensure correct shape: (seq_len, hidden_dim)
if text_hidden_states.ndim == 3 and text_hidden_states.shape[0] == 1:
text_hidden_states = text_hidden_states.squeeze(0)
# Return hidden states with latent in multimodal_outputs for stage_input_processors
@@ -4611,9 +4611,6 @@ class MiniCPMO45OmniLLMForConditionalGeneration(nn.Module, SupportsMultiModal, S
inputs_embeds: torch.Tensor | None = None,
**kwargs: object,
) -> torch.Tensor | IntermediateTensors:
"""Forward pass through thinker model."""
text_inputs_embeds = None
if intermediate_tensors is not None:
inputs_embeds = None
@@ -4621,22 +4618,12 @@ class MiniCPMO45OmniLLMForConditionalGeneration(nn.Module, SupportsMultiModal, S
elif inputs_embeds is None:
multimodal_embeddings = self.get_multimodal_embeddings(**kwargs)
inputs_embeds = self.get_input_embeddings(input_ids, multimodal_embeddings)
text_inputs_embeds = self.get_input_embeddings(
input_ids,
(
[(torch.zeros_like(embeddings), "image") for embeddings in multimodal_embeddings]
if multimodal_embeddings is not None
else None
),
)
input_ids = None
else:
text_inputs_embeds = inputs_embeds
# Forward through language model
hidden_states = self.llm.model(input_ids, positions, intermediate_tensors, inputs_embeds=inputs_embeds)
return text_inputs_embeds, hidden_states.unsqueeze(0) if hidden_states.ndim == 2 else hidden_states
return hidden_states
def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
"""Compute logits from hidden states."""