From 04f20fc8135dde1431072e7eaecfd3466580640f Mon Sep 17 00:00:00 2001 From: TrainToGPB Date: Tue, 4 Aug 2026 12:29:13 +0900 Subject: [PATCH 1/2] [Bugfix][Model] Gemma3n/Gemma4: pad variable-length audio batches Audio features are unpadded per item so a multimodal cache entry does not depend on the batch it was first processed in, but nothing re-pads them: MultiModalFieldConfig.batched stacks only items that share a shape and otherwise hands the model a plain list. Gemma4AudioInputs lets that list through via dynamic_dims={"s"}, so _process_audio_input calls .squeeze(1) on it and kills EngineCore. Four concurrent requests whose clips differ in duration are enough to trigger this. Pad the ragged batch to its longest clip in one shared helper and call it from gemma4_mm, gemma4_unified and gemma3n_mm. Padded frames are zeroed and masked out, so they never reach the language model. Gemma3n additionally needs dynamic_dims to get past TensorSchema validation, which is where it fails today. Co-authored-by: Claude Signed-off-by: TrainToGPB --- .../multimodal/processing/test_audio_utils.py | 66 +++++++++++++++++++ vllm/model_executor/models/audio_utils.py | 52 +++++++++++++++ vllm/model_executor/models/gemma3n_mm.py | 15 +++-- vllm/model_executor/models/gemma4_mm.py | 13 ++-- vllm/model_executor/models/gemma4_unified.py | 7 +- 5 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 tests/models/multimodal/processing/test_audio_utils.py create mode 100644 vllm/model_executor/models/audio_utils.py diff --git a/tests/models/multimodal/processing/test_audio_utils.py b/tests/models/multimodal/processing/test_audio_utils.py new file mode 100644 index 000000000000..8cfe744f7b56 --- /dev/null +++ b/tests/models/multimodal/processing/test_audio_utils.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""CPU tests for the audio batching helper shared by Gemma3n and Gemma4. + +Clips of differing duration reach `_process_audio_input` as a list rather than +a stacked tensor. Treating that list as a tensor used to abort the forward pass +and kill EngineCore, so these tests pin the list handling down. +""" + +import torch + +from vllm.model_executor.models.audio_utils import batch_audio_features + + +def test_ragged_clips_are_padded_to_batch_max(): + short = torch.randn(3, 4) + long = torch.randn(5, 4) + masks = [torch.ones(3, dtype=torch.bool), torch.ones(5, dtype=torch.bool)] + + features, mask = batch_audio_features([short, long], masks) + + assert features.shape == (2, 5, 4) + assert mask.shape == (2, 5) + torch.testing.assert_close(features[0, :3], short) + torch.testing.assert_close(features[1], long) + # The frames that only exist to square off the batch carry no signal and + # are masked out, so the audio tower never attends to them. + assert torch.all(features[0, 3:] == 0) + assert mask[0].tolist() == [True, True, True, False, False] + assert mask[1].all() + + +def test_padding_preserves_masked_out_frames_inside_a_clip(): + features = [torch.randn(4, 2), torch.randn(2, 2)] + masks = [ + torch.tensor([True, True, False, False]), + torch.tensor([True, False]), + ] + + _, mask = batch_audio_features(features, masks) + + assert mask[0].tolist() == [True, True, False, False] + assert mask[1].tolist() == [True, False, False, False] + + +def test_equal_length_clips_are_left_alone(): + features = torch.randn(2, 3, 4) + mask = torch.ones(2, 3, dtype=torch.bool) + + batched_features, batched_mask = batch_audio_features(features, mask) + + torch.testing.assert_close(batched_features, features) + torch.testing.assert_close(batched_mask, mask) + + +def test_padding_keeps_feature_dtype(): + features = [ + torch.randn(3, 4, dtype=torch.bfloat16), + torch.randn(5, 4, dtype=torch.bfloat16), + ] + masks = [torch.ones(3, dtype=torch.bool), torch.ones(5, dtype=torch.bool)] + + batched_features, batched_mask = batch_audio_features(features, masks) + + assert batched_features.dtype == torch.bfloat16 + assert batched_mask.dtype == torch.bool diff --git a/vllm/model_executor/models/audio_utils.py b/vllm/model_executor/models/audio_utils.py new file mode 100644 index 000000000000..a50e4b65d28e --- /dev/null +++ b/vllm/model_executor/models/audio_utils.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Audio helpers shared by multimodal model implementations. + +Kept free of heavy CUDA dependencies so it stays testable without a full +vLLM build. +""" + +import torch + + +def batch_audio_features( + input_features: torch.Tensor | list[torch.Tensor], + input_features_mask: torch.Tensor | list[torch.Tensor], +) -> tuple[torch.Tensor, torch.Tensor]: + """Return mel features and their validity mask as batched tensors. + + Audio features are unpadded per item so that a multimodal cache entry does + not depend on the batch it was first processed in. + [`MultiModalFieldConfig.batched`][vllm.multimodal.inputs.MultiModalFieldConfig.batched] + stacks items only when their shapes agree, so a batch of clips with + differing durations reaches the model as a list and must be re-padded here. + + Padded frames are zero-filled and marked invalid. Audio towers consume the + mask, and callers keep only masked-in positions, so padding never reaches + the language model. + + Args: + input_features: `(bn, s, f)` tensor, or a list of `(s_i, f)` tensors + when clip durations differ. + input_features_mask: Matching `(bn, s)` tensor, or list of `(s_i,)` + tensors. `True` marks a valid frame. + + Returns: + The `(bn, s_max, f)` features and their `(bn, s_max)` mask. + """ + if isinstance(input_features, torch.Tensor): + return input_features.squeeze(1), input_features_mask.squeeze(1) + + max_len = max(features.shape[0] for features in input_features) + batched_features = input_features[0].new_zeros( + (len(input_features), max_len, input_features[0].shape[-1]) + ) + batched_mask = input_features_mask[0].new_zeros( + (len(input_features_mask), max_len), dtype=torch.bool + ) + for i, (features, mask) in enumerate( + zip(input_features, input_features_mask, strict=True) + ): + batched_features[i, : features.shape[0]] = features + batched_mask[i, : mask.shape[0]] = mask + return batched_features, batched_mask diff --git a/vllm/model_executor/models/gemma3n_mm.py b/vllm/model_executor/models/gemma3n_mm.py index 1dd44313c1e5..6f817c792ed9 100644 --- a/vllm/model_executor/models/gemma3n_mm.py +++ b/vllm/model_executor/models/gemma3n_mm.py @@ -24,6 +24,7 @@ from vllm.model_executor.layers.layernorm import RMSNorm from vllm.model_executor.layers.linear import RowParallelLinear from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding +from vllm.model_executor.models.audio_utils import batch_audio_features from vllm.model_executor.models.gemma3n import Gemma3nForCausalLM from vllm.model_executor.models.gemma3n_audio_utils import ( adjust_audio_features_to_expected_length, @@ -92,8 +93,12 @@ class Gemma3nAudioInputs(TensorSchema): """ type: Literal["audio"] = "audio" - input_features_padded: Annotated[torch.Tensor, TensorShape("bn", "s", "f")] - input_features_mask: Annotated[torch.Tensor, TensorShape("bn", "s")] + input_features_padded: Annotated[ + torch.Tensor, TensorShape("bn", "s", "f", dynamic_dims={"s"}) + ] + input_features_mask: Annotated[ + torch.Tensor, TensorShape("bn", "s", dynamic_dims={"s"}) + ] Gemma3nImageInputs = Gemma3nImagePixelInputs @@ -615,8 +620,10 @@ def _process_audio_input( audio_input: Gemma3nAudioInputs, ) -> list[torch.Tensor]: # Run on padded features to enable batching - input_features = audio_input["input_features_padded"].squeeze(1) - input_features_mask = audio_input["input_features_mask"].squeeze(1) + input_features, input_features_mask = batch_audio_features( + audio_input["input_features_padded"], + audio_input["input_features_mask"], + ) audio_outputs = self.audio_tower(input_features, ~input_features_mask) audio_encodings = audio_outputs.last_hidden_state audio_mask = audio_outputs.audio_mel_mask diff --git a/vllm/model_executor/models/gemma4_mm.py b/vllm/model_executor/models/gemma4_mm.py index e158a7850164..c31e7e13159c 100644 --- a/vllm/model_executor/models/gemma4_mm.py +++ b/vllm/model_executor/models/gemma4_mm.py @@ -40,6 +40,7 @@ from vllm.logger import init_logger from vllm.model_executor.layers.layernorm import RMSNorm from vllm.model_executor.layers.linear import ReplicatedLinear +from vllm.model_executor.models.audio_utils import batch_audio_features from vllm.model_executor.models.gemma4 import ( _GEMMA4_EXPERT_PARENT_MAPPER, Gemma4ForCausalLM, @@ -750,9 +751,9 @@ def _call_hf_processor( if "input_features" in processed_outputs: # Unpad per-item so each item's cache entry is # self-contained. The batched() field config in - # _get_mm_fields_config will re-pad all fields to the - # batch's max length at batch time, ensuring consistent - # padding regardless of cache history. + # _get_mm_fields_config stacks equal-length items and leaves + # the rest as a list; _process_audio_input re-pads that list + # to the batch's max length. masks = processed_outputs["input_features_mask"] unpadded_features = [ f[mask] @@ -1487,8 +1488,10 @@ def _process_audio_input( self, audio_input: Gemma4AudioInputs, ) -> list[torch.Tensor]: - input_features = audio_input["input_features_padded"].squeeze(1) - input_features_mask = audio_input["input_features_mask"].squeeze(1) + input_features, input_features_mask = batch_audio_features( + audio_input["input_features_padded"], + audio_input["input_features_mask"], + ) # Run audio tower — mask convention: True=valid, False=padding. audio_outputs = self.audio_tower(input_features, input_features_mask) diff --git a/vllm/model_executor/models/gemma4_unified.py b/vllm/model_executor/models/gemma4_unified.py index 64c084b6161b..7c1ea040ba3f 100644 --- a/vllm/model_executor/models/gemma4_unified.py +++ b/vllm/model_executor/models/gemma4_unified.py @@ -30,6 +30,7 @@ from vllm.config import VllmConfig from vllm.config.multimodal import VideoDummyOptions from vllm.model_executor.layers.linear import ColumnParallelLinear +from vllm.model_executor.models.audio_utils import batch_audio_features from vllm.model_executor.models.gemma4 import Gemma4ForCausalLM from vllm.model_executor.models.gemma4_mm import ( _SUPPORTED_SOFT_TOKENS, @@ -423,8 +424,10 @@ def _process_audio_input( No audio tower: the per-frame raw features are passed straight through the multimodal embedder, then padding is stripped. """ - input_features = audio_input["input_features_padded"].squeeze(1) - input_features_mask = audio_input["input_features_mask"].squeeze(1) + input_features, input_features_mask = batch_audio_features( + audio_input["input_features_padded"], + audio_input["input_features_mask"], + ) target_dtype = self.embed_audio.embedding_projection.weight.dtype audio_features = self.embed_audio(input_features.to(target_dtype)) From a1e11c47be51b27a90fb523f542a5c9d0ff89dc4 Mon Sep 17 00:00:00 2001 From: TrainToGPB Date: Tue, 4 Aug 2026 18:34:42 +0900 Subject: [PATCH 2/2] Address review: fold helper into gemma3n_mm, swap unit tests for e2e - Move `batch_audio_features` from a new `audio_utils.py` into `gemma3n_mm.py`; `gemma4_mm.py` and `gemma4_unified.py` import it from there. No new shared module, and no import cycle: `gemma3n_mm` does not reach back into either gemma4 module. - Replace `tests/models/multimodal/processing/test_audio_utils.py` with an end-to-end test at `tests/models/multimodal/generation/test_gemma_ragged_audio.py`, covering Gemma3n and Gemma4. It follows the existing `test_ultravox.py::test_variable_length_audio_batching`: same name, same `vllm_runner` + skip-guard shape, same "outputs are non-empty" assertion. One difference from the ultravox test: rather than relying on two assets happening to differ in duration, it slices a single asset to 1.0/3.5/7.0/ 12.0s and asserts the four clips really do differ in length. Equal-length clips stack normally and never reach the ragged path, so leaving that to chance would let the test pass without exercising the bug. The audio placeholder is read from the processor rather than hardcoded, since the two architectures spell it differently. Signed-off-by: TrainToGPB --- .../generation/test_gemma_ragged_audio.py | 96 +++++++++++++++++++ .../multimodal/processing/test_audio_utils.py | 66 ------------- vllm/model_executor/models/audio_utils.py | 52 ---------- vllm/model_executor/models/gemma3n_mm.py | 47 ++++++++- vllm/model_executor/models/gemma4_mm.py | 2 +- vllm/model_executor/models/gemma4_unified.py | 2 +- 6 files changed, 144 insertions(+), 121 deletions(-) create mode 100644 tests/models/multimodal/generation/test_gemma_ragged_audio.py delete mode 100644 tests/models/multimodal/processing/test_audio_utils.py delete mode 100644 vllm/model_executor/models/audio_utils.py diff --git a/tests/models/multimodal/generation/test_gemma_ragged_audio.py b/tests/models/multimodal/generation/test_gemma_ragged_audio.py new file mode 100644 index 000000000000..e51eb002effd --- /dev/null +++ b/tests/models/multimodal/generation/test_gemma_ragged_audio.py @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +import pytest + +from tests.models.registry import HF_EXAMPLE_MODELS +from vllm.assets.audio import AudioAsset + +MODELS = { + "Gemma3nForConditionalGeneration": "google/gemma-3n-E2B-it", + "Gemma4ForConditionalGeneration": "google/gemma-4-E2B-it", +} + +# Seconds. Spread wide enough that no two clips can produce the same number of +# mel frames: equal-length clips stack normally and never reach the ragged path. +CLIP_DURATIONS = (1.0, 3.5, 7.0, 12.0) + + +@pytest.fixture(scope="module") +def ragged_clips(): + """One asset sliced to several durations, so only length varies.""" + audio, sample_rate = AudioAsset("mary_had_lamb").audio_and_sample_rate + + clips = [] + for duration in CLIP_DURATIONS: + num_samples = int(duration * sample_rate) + if num_samples > len(audio): + pytest.skip(f"asset is shorter than {duration}s") + clips.append((audio[:num_samples], sample_rate)) + + assert len({len(clip) for clip, _ in clips}) == len(clips) + return clips + + +@pytest.mark.core_model +@pytest.mark.parametrize("arch", sorted(MODELS)) +@pytest.mark.parametrize("max_tokens", [8]) +def test_variable_length_audio_batching( + vllm_runner, + ragged_clips, + arch: str, + max_tokens: int, +) -> None: + """Test batching of requests with different audio durations. + + Audio features are unpadded per item so a multimodal cache entry does not + depend on the batch it was first processed in, and + `MultiModalFieldConfig.batched` stacks items only when their shapes agree. + A batch of differently sized clips therefore reaches the model as a list, + which used to take EngineCore down with + + AttributeError: 'list' object has no attribute 'squeeze' + + failing every in-flight request with EngineDeadError. + """ + from transformers import AutoProcessor + + model_name = MODELS[arch] + model_info = HF_EXAMPLE_MODELS.get_hf_info(arch) + model_info.check_available_online(on_fail="skip") + model_info.check_transformers_version(on_fail="skip") + + # The two architectures spell the audio placeholder differently; ask the + # processor rather than hardcoding either. + processor = AutoProcessor.from_pretrained(model_name) + prompt = processor.apply_chat_template( + [ + { + "role": "user", + "content": [ + {"type": "audio", "audio": None}, + {"type": "text", "text": "Transcribe this audio."}, + ], + } + ], + tokenize=False, + add_generation_prompt=True, + ) + + with vllm_runner( + model_name, + dtype="bfloat16", + enforce_eager=True, + limit_mm_per_prompt={"audio": 1}, + ) as vllm_model: + # One call so the clips are scheduled into the same forward pass; + # issued separately they never reach the ragged path. + outputs = vllm_model.generate_greedy( + [prompt] * len(ragged_clips), + max_tokens, + audios=[[clip] for clip in ragged_clips], + ) + + assert len(outputs) == len(ragged_clips) + for output in outputs: + assert len(output[1]) > 0, "Expected non-empty output" diff --git a/tests/models/multimodal/processing/test_audio_utils.py b/tests/models/multimodal/processing/test_audio_utils.py deleted file mode 100644 index 8cfe744f7b56..000000000000 --- a/tests/models/multimodal/processing/test_audio_utils.py +++ /dev/null @@ -1,66 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project -"""CPU tests for the audio batching helper shared by Gemma3n and Gemma4. - -Clips of differing duration reach `_process_audio_input` as a list rather than -a stacked tensor. Treating that list as a tensor used to abort the forward pass -and kill EngineCore, so these tests pin the list handling down. -""" - -import torch - -from vllm.model_executor.models.audio_utils import batch_audio_features - - -def test_ragged_clips_are_padded_to_batch_max(): - short = torch.randn(3, 4) - long = torch.randn(5, 4) - masks = [torch.ones(3, dtype=torch.bool), torch.ones(5, dtype=torch.bool)] - - features, mask = batch_audio_features([short, long], masks) - - assert features.shape == (2, 5, 4) - assert mask.shape == (2, 5) - torch.testing.assert_close(features[0, :3], short) - torch.testing.assert_close(features[1], long) - # The frames that only exist to square off the batch carry no signal and - # are masked out, so the audio tower never attends to them. - assert torch.all(features[0, 3:] == 0) - assert mask[0].tolist() == [True, True, True, False, False] - assert mask[1].all() - - -def test_padding_preserves_masked_out_frames_inside_a_clip(): - features = [torch.randn(4, 2), torch.randn(2, 2)] - masks = [ - torch.tensor([True, True, False, False]), - torch.tensor([True, False]), - ] - - _, mask = batch_audio_features(features, masks) - - assert mask[0].tolist() == [True, True, False, False] - assert mask[1].tolist() == [True, False, False, False] - - -def test_equal_length_clips_are_left_alone(): - features = torch.randn(2, 3, 4) - mask = torch.ones(2, 3, dtype=torch.bool) - - batched_features, batched_mask = batch_audio_features(features, mask) - - torch.testing.assert_close(batched_features, features) - torch.testing.assert_close(batched_mask, mask) - - -def test_padding_keeps_feature_dtype(): - features = [ - torch.randn(3, 4, dtype=torch.bfloat16), - torch.randn(5, 4, dtype=torch.bfloat16), - ] - masks = [torch.ones(3, dtype=torch.bool), torch.ones(5, dtype=torch.bool)] - - batched_features, batched_mask = batch_audio_features(features, masks) - - assert batched_features.dtype == torch.bfloat16 - assert batched_mask.dtype == torch.bool diff --git a/vllm/model_executor/models/audio_utils.py b/vllm/model_executor/models/audio_utils.py deleted file mode 100644 index a50e4b65d28e..000000000000 --- a/vllm/model_executor/models/audio_utils.py +++ /dev/null @@ -1,52 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project -"""Audio helpers shared by multimodal model implementations. - -Kept free of heavy CUDA dependencies so it stays testable without a full -vLLM build. -""" - -import torch - - -def batch_audio_features( - input_features: torch.Tensor | list[torch.Tensor], - input_features_mask: torch.Tensor | list[torch.Tensor], -) -> tuple[torch.Tensor, torch.Tensor]: - """Return mel features and their validity mask as batched tensors. - - Audio features are unpadded per item so that a multimodal cache entry does - not depend on the batch it was first processed in. - [`MultiModalFieldConfig.batched`][vllm.multimodal.inputs.MultiModalFieldConfig.batched] - stacks items only when their shapes agree, so a batch of clips with - differing durations reaches the model as a list and must be re-padded here. - - Padded frames are zero-filled and marked invalid. Audio towers consume the - mask, and callers keep only masked-in positions, so padding never reaches - the language model. - - Args: - input_features: `(bn, s, f)` tensor, or a list of `(s_i, f)` tensors - when clip durations differ. - input_features_mask: Matching `(bn, s)` tensor, or list of `(s_i,)` - tensors. `True` marks a valid frame. - - Returns: - The `(bn, s_max, f)` features and their `(bn, s_max)` mask. - """ - if isinstance(input_features, torch.Tensor): - return input_features.squeeze(1), input_features_mask.squeeze(1) - - max_len = max(features.shape[0] for features in input_features) - batched_features = input_features[0].new_zeros( - (len(input_features), max_len, input_features[0].shape[-1]) - ) - batched_mask = input_features_mask[0].new_zeros( - (len(input_features_mask), max_len), dtype=torch.bool - ) - for i, (features, mask) in enumerate( - zip(input_features, input_features_mask, strict=True) - ): - batched_features[i, : features.shape[0]] = features - batched_mask[i, : mask.shape[0]] = mask - return batched_features, batched_mask diff --git a/vllm/model_executor/models/gemma3n_mm.py b/vllm/model_executor/models/gemma3n_mm.py index 6f817c792ed9..7762ddd3a3ee 100644 --- a/vllm/model_executor/models/gemma3n_mm.py +++ b/vllm/model_executor/models/gemma3n_mm.py @@ -24,7 +24,6 @@ from vllm.model_executor.layers.layernorm import RMSNorm from vllm.model_executor.layers.linear import RowParallelLinear from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding -from vllm.model_executor.models.audio_utils import batch_audio_features from vllm.model_executor.models.gemma3n import Gemma3nForCausalLM from vllm.model_executor.models.gemma3n_audio_utils import ( adjust_audio_features_to_expected_length, @@ -84,6 +83,52 @@ class Gemma3nImagePixelInputs(TensorSchema): pixel_values: Annotated[torch.Tensor, TensorShape("bn", 3, "h", "w")] +def batch_audio_features( + input_features: torch.Tensor | list[torch.Tensor], + input_features_mask: torch.Tensor | list[torch.Tensor], +) -> tuple[torch.Tensor, torch.Tensor]: + """Return mel features and their validity mask as batched tensors. + + Audio features are unpadded per item so that a multimodal cache entry does + not depend on the batch it was first processed in. + [`MultiModalFieldConfig.batched`][vllm.multimodal.inputs.MultiModalFieldConfig.batched] + stacks items only when their shapes agree, so a batch of clips with + differing durations reaches the model as a list and must be re-padded here. + + Padded frames are zero-filled and marked invalid. Audio towers consume the + mask, and callers keep only masked-in positions, so padding never reaches + the language model. + + Gemma4 shares this helper: its audio path has the same unpad/re-pad + contract and the same fields. + + Args: + input_features: `(bn, s, f)` tensor, or a list of `(s_i, f)` tensors + when clip durations differ. + input_features_mask: Matching `(bn, s)` tensor, or list of `(s_i,)` + tensors. `True` marks a valid frame. + + Returns: + The `(bn, s_max, f)` features and their `(bn, s_max)` mask. + """ + if isinstance(input_features, torch.Tensor): + return input_features.squeeze(1), input_features_mask.squeeze(1) + + max_len = max(features.shape[0] for features in input_features) + batched_features = input_features[0].new_zeros( + (len(input_features), max_len, input_features[0].shape[-1]) + ) + batched_mask = input_features_mask[0].new_zeros( + (len(input_features_mask), max_len), dtype=torch.bool + ) + for i, (features, mask) in enumerate( + zip(input_features, input_features_mask, strict=True) + ): + batched_features[i, : features.shape[0]] = features + batched_mask[i, : mask.shape[0]] = mask + return batched_features, batched_mask + + class Gemma3nAudioInputs(TensorSchema): """ Dimensions: diff --git a/vllm/model_executor/models/gemma4_mm.py b/vllm/model_executor/models/gemma4_mm.py index c31e7e13159c..48136e7c2f45 100644 --- a/vllm/model_executor/models/gemma4_mm.py +++ b/vllm/model_executor/models/gemma4_mm.py @@ -40,7 +40,7 @@ from vllm.logger import init_logger from vllm.model_executor.layers.layernorm import RMSNorm from vllm.model_executor.layers.linear import ReplicatedLinear -from vllm.model_executor.models.audio_utils import batch_audio_features +from vllm.model_executor.models.gemma3n_mm import batch_audio_features from vllm.model_executor.models.gemma4 import ( _GEMMA4_EXPERT_PARENT_MAPPER, Gemma4ForCausalLM, diff --git a/vllm/model_executor/models/gemma4_unified.py b/vllm/model_executor/models/gemma4_unified.py index 7c1ea040ba3f..f41a36adfdb6 100644 --- a/vllm/model_executor/models/gemma4_unified.py +++ b/vllm/model_executor/models/gemma4_unified.py @@ -30,7 +30,7 @@ from vllm.config import VllmConfig from vllm.config.multimodal import VideoDummyOptions from vllm.model_executor.layers.linear import ColumnParallelLinear -from vllm.model_executor.models.audio_utils import batch_audio_features +from vllm.model_executor.models.gemma3n_mm import batch_audio_features from vllm.model_executor.models.gemma4 import Gemma4ForCausalLM from vllm.model_executor.models.gemma4_mm import ( _SUPPORTED_SOFT_TOKENS,