| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Integration tests for Whisper models with LoRA adapters. |
| 4 | |
| 5 | These tests verify that Whisper models can correctly load and use LoRA adapters |
| 6 | for speech-to-text transcription tasks. |
| 7 | """ |
| 8 | |
| 9 | import pytest |
| 10 | |
| 11 | import vllm |
| 12 | from vllm.assets.audio import AudioAsset |
| 13 | from vllm.lora.request import LoRARequest |
| 14 | from vllm.platforms import current_platform |
| 15 | |
| 16 | from ..utils import create_new_process_for_each_test |
| 17 | |
| 18 | # Model configuration |
| 19 | WHISPER_MODEL = "openai/whisper-small" |
| 20 | |
| 21 | # Test prompts for Whisper transcription |
| 22 | WHISPER_PROMPT = "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>" |
| 23 | |
| 24 | # Note: whisper_lora_files fixture is defined in conftest.py |
| 25 | |
| 26 | |
| 27 | @pytest.fixture(autouse=True) |
| 28 | def use_spawn_for_whisper(monkeypatch): |
| 29 | """Whisper has issues with forked workers, use spawn instead.""" |
| 30 | monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn") |
| 31 | |
| 32 | |
| 33 | def create_whisper_llm( |
| 34 | enable_lora: bool = True, max_loras: int = 2, attn_backend: str | None = None |
| 35 | ): |
| 36 | """Create a Whisper LLM instance with optional LoRA support.""" |
| 37 | return vllm.LLM( |
| 38 | model=WHISPER_MODEL, |
| 39 | enable_lora=enable_lora, |
| 40 | max_loras=max_loras if enable_lora else 1, |
| 41 | max_lora_rank=64, |
| 42 | max_model_len=448, |
| 43 | dtype="half", |
| 44 | enforce_eager=True, # For stability in tests |
| 45 | attention_config={"backend": attn_backend}, |
| 46 | ) |
| 47 | |
| 48 | |
| 49 | def run_whisper_inference( |
| 50 | llm: vllm.LLM, |
| 51 | lora_path: str | None = None, |
| 52 | lora_id: int = 1, |
| 53 | ) -> list[str]: |
| 54 | """Run Whisper inference with optional LoRA adapter.""" |
| 55 | # Load test audio |
| 56 | audio_asset = AudioAsset("mary_had_lamb") |
| 57 | audio_data = audio_asset.audio_and_sample_rate |
| 58 | |
| 59 | inputs = [ |
| 60 | { |
| 61 | "prompt": WHISPER_PROMPT, |
| 62 | "multi_modal_data": {"audio": audio_data}, |
| 63 | } |
| 64 | ] |
| 65 | |
| 66 | sampling_params = vllm.SamplingParams( |
| 67 | temperature=0, |
| 68 | max_tokens=200, |
| 69 | ) |
| 70 | |
| 71 | # Prepare LoRA request if adapter path is provided |
| 72 | lora_request = None |
| 73 | if lora_path: |
| 74 | lora_request = LoRARequest( |
| 75 | lora_name=f"whisper_lora_{lora_id}", |
| 76 | lora_int_id=lora_id, |
| 77 | lora_path=lora_path, |
| 78 | ) |
| 79 | |
| 80 | outputs = llm.generate(inputs, sampling_params, lora_request=lora_request) |
| 81 | |
| 82 | return [output.outputs[0].text for output in outputs] |
| 83 | |
| 84 | |
| 85 | @create_new_process_for_each_test() |
| 86 | def test_whisper_lora_inference(whisper_lora_files): |
| 87 | """Test basic Whisper inference with a LoRA adapter. |
| 88 | |
| 89 | This test verifies that: |
| 90 | 1. Whisper model can be loaded with LoRA support enabled |
| 91 | 2. A LoRA adapter can be applied during inference |
| 92 | 3. The model produces valid transcription output |
| 93 | """ |
| 94 | llm = create_whisper_llm(enable_lora=True) |
| 95 | |
| 96 | # Run inference with LoRA |
| 97 | outputs = run_whisper_inference(llm, lora_path=whisper_lora_files, lora_id=1) |
| 98 | |
| 99 | # Verify we got a non-empty transcription |
| 100 | assert len(outputs) == 1 |
| 101 | assert len(outputs[0]) > 0, "Expected non-empty transcription output" |
| 102 | |
| 103 | # The output should contain some recognizable words from the audio |
| 104 | # (Mary had a little lamb) |
| 105 | print(f"Transcription output: {outputs[0]}") |
| 106 | |
| 107 | |
| 108 | @create_new_process_for_each_test() |
| 109 | def test_whisper_multi_lora(whisper_lora_files): |
| 110 | """Test Whisper with multiple LoRA adapter IDs. |
| 111 | |
| 112 | This test verifies that the same LoRA adapter can be loaded with |
| 113 | different IDs and produce consistent results. |
| 114 | """ |
| 115 | llm = create_whisper_llm( |
| 116 | enable_lora=True, |
| 117 | max_loras=4, |
| 118 | attn_backend="TRITON_ATTN" if current_platform.is_rocm() else None, |
| 119 | ) |
| 120 | |
| 121 | # Test with different LoRA IDs using the same adapter |
| 122 | outputs_lora1 = run_whisper_inference(llm, lora_path=whisper_lora_files, lora_id=1) |
| 123 | outputs_lora2 = run_whisper_inference(llm, lora_path=whisper_lora_files, lora_id=2) |
| 124 | |
| 125 | # Both should produce valid outputs |
| 126 | assert len(outputs_lora1[0]) > 0 |
| 127 | assert len(outputs_lora2[0]) > 0 |
| 128 | |
| 129 | # Same adapter with different IDs should produce same output |
| 130 | assert outputs_lora1 == outputs_lora2, ( |
| 131 | f"Expected same outputs for same adapter with different IDs. " |
| 132 | f"Got: {outputs_lora1} vs {outputs_lora2}" |
| 133 | ) |