main
py 96 lines 3.19 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5
6 from tests.models.registry import HF_EXAMPLE_MODELS
7 from vllm.assets.audio import AudioAsset
8
9 MODELS = {
10 "Gemma3nForConditionalGeneration": "google/gemma-3n-E2B-it",
11 "Gemma4ForConditionalGeneration": "google/gemma-4-E2B-it",
12 }
13
14 # Seconds. Spread wide enough that no two clips can produce the same number of
15 # mel frames: equal-length clips stack normally and never reach the ragged path.
16 CLIP_DURATIONS = (1.0, 3.5, 7.0, 12.0)
17
18
19 @pytest.fixture(scope="module")
20 def ragged_clips():
21 """One asset sliced to several durations, so only length varies."""
22 audio, sample_rate = AudioAsset("mary_had_lamb").audio_and_sample_rate
23
24 clips = []
25 for duration in CLIP_DURATIONS:
26 num_samples = int(duration * sample_rate)
27 if num_samples > len(audio):
28 pytest.skip(f"asset is shorter than {duration}s")
29 clips.append((audio[:num_samples], sample_rate))
30
31 assert len({len(clip) for clip, _ in clips}) == len(clips)
32 return clips
33
34
35 @pytest.mark.core_model
36 @pytest.mark.parametrize("arch", sorted(MODELS))
37 @pytest.mark.parametrize("max_tokens", [8])
38 def test_variable_length_audio_batching(
39 vllm_runner,
40 ragged_clips,
41 arch: str,
42 max_tokens: int,
43 ) -> None:
44 """Test batching of requests with different audio durations.
45
46 Audio features are unpadded per item so a multimodal cache entry does not
47 depend on the batch it was first processed in, and
48 `MultiModalFieldConfig.batched` stacks items only when their shapes agree.
49 A batch of differently sized clips therefore reaches the model as a list,
50 which used to take EngineCore down with
51
52 AttributeError: 'list' object has no attribute 'squeeze'
53
54 failing every in-flight request with EngineDeadError.
55 """
56 from transformers import AutoProcessor
57
58 model_name = MODELS[arch]
59 model_info = HF_EXAMPLE_MODELS.get_hf_info(arch)
60 model_info.check_available_online(on_fail="skip")
61 model_info.check_transformers_version(on_fail="skip")
62
63 # The two architectures spell the audio placeholder differently; ask the
64 # processor rather than hardcoding either.
65 processor = AutoProcessor.from_pretrained(model_name)
66 prompt = processor.apply_chat_template(
67 [
68 {
69 "role": "user",
70 "content": [
71 {"type": "audio", "audio": None},
72 {"type": "text", "text": "Transcribe this audio."},
73 ],
74 }
75 ],
76 tokenize=False,
77 add_generation_prompt=True,
78 )
79
80 with vllm_runner(
81 model_name,
82 dtype="bfloat16",
83 enforce_eager=True,
84 limit_mm_per_prompt={"audio": 1},
85 ) as vllm_model:
86 # One call so the clips are scheduled into the same forward pass;
87 # issued separately they never reach the ragged path.
88 outputs = vllm_model.generate_greedy(
89 [prompt] * len(ragged_clips),
90 max_tokens,
91 audios=[[clip] for clip in ragged_clips],
92 )
93
94 assert len(outputs) == len(ragged_clips)
95 for output in outputs:
96 assert len(output[1]) > 0, "Expected non-empty output"