@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 8a87cd27d9
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 14, 05:30 UTC

[CI] Speed up Whisper tests by reusing server (#22859)

Signed-off-by: mgoin <mgoin64@gmail.com>

Michael Goin committed Aug 15, 2025 at 16:56 UTC 8a87cd27d94f03068b9cbc85058636fc16222e24
2 files changed +263 -291
tests/entrypoints/openai/test_transcription_validation.py
+141 -179
@@ -4,19 +4,20 @@
4 # imports for guided decoding tests
5 import io
6 import json
7 -from unittest.mock import patch
7
8 import librosa
9 import numpy as np
10 import openai
11 import pytest
12 +import pytest_asyncio
13 import soundfile as sf
14 -from openai._base_client import AsyncAPIClient
14
15 from vllm.assets.audio import AudioAsset
16
17 from ...utils import RemoteOpenAIServer
18
19 +MODEL_NAME = "openai/whisper-large-v3-turbo"
20 +SERVER_ARGS = ["--enforce-eager"]
21 MISTRAL_FORMAT_ARGS = [
22 "--tokenizer_mode", "mistral", "--config_format", "mistral",
23 "--load_format", "mistral"
@@ -37,6 +38,18 @@ def winning_call():
38 yield f
39
40
41 +@pytest.fixture(scope="module")
42 +def server():
43 + with RemoteOpenAIServer(MODEL_NAME, SERVER_ARGS) as remote_server:
44 + yield remote_server
45 +
46 +
47 +@pytest_asyncio.fixture
48 +async def client(server):
49 + async with server.get_async_client() as async_client:
50 + yield async_client
51 +
52 +
53 @pytest.mark.asyncio
54 @pytest.mark.parametrize(
55 "model_name",
@@ -61,25 +74,33 @@ async def test_basic_audio(mary_had_lamb, model_name):
74
75
76 @pytest.mark.asyncio
64 -async def test_bad_requests(mary_had_lamb):
65 - model_name = "openai/whisper-small"
66 - server_args = ["--enforce-eager"]
67 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
77 +async def test_non_asr_model(winning_call):
78 + # text to text model
79 + model_name = "JackFram/llama-68m"
80 + with RemoteOpenAIServer(model_name, SERVER_ARGS) as remote_server:
81 client = remote_server.get_async_client()
69 -
70 - # invalid language
71 - with pytest.raises(openai.BadRequestError):
72 - await client.audio.transcriptions.create(model=model_name,
73 - file=mary_had_lamb,
74 - language="hh",
75 - temperature=0.0)
82 + res = await client.audio.transcriptions.create(model=model_name,
83 + file=winning_call,
84 + language="en",
85 + temperature=0.0)
86 + err = res.error
87 + assert err["code"] == 400 and not res.text
88 + assert err[
89 + "message"] == "The model does not support Transcriptions API"
90
91
92 @pytest.mark.asyncio
79 -@pytest.mark.parametrize("model_name", ["openai/whisper-large-v3-turbo"])
80 -async def test_long_audio_request(mary_had_lamb, model_name):
81 - server_args = ["--enforce-eager"]
93 +async def test_bad_requests(mary_had_lamb, client):
94 + # invalid language
95 + with pytest.raises(openai.BadRequestError):
96 + await client.audio.transcriptions.create(model=MODEL_NAME,
97 + file=mary_had_lamb,
98 + language="hh",
99 + temperature=0.0)
100 +
101
102 +@pytest.mark.asyncio
103 +async def test_long_audio_request(mary_had_lamb, client):
104 mary_had_lamb.seek(0)
105 audio, sr = librosa.load(mary_had_lamb)
106 # Add small silence after each audio for repeatability in the split process
@@ -89,188 +110,129 @@ async def test_long_audio_request(mary_had_lamb, model_name):
110 buffer = io.BytesIO()
111 sf.write(buffer, repeated_audio, sr, format='WAV')
112 buffer.seek(0)
92 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
93 - client = remote_server.get_async_client()
94 - transcription = await client.audio.transcriptions.create(
95 - model=model_name,
96 - file=buffer,
97 - language="en",
98 - response_format="text",
99 - temperature=0.0)
100 - out = json.loads(transcription)['text']
101 - counts = out.count("Mary had a little lamb")
102 - assert counts == 10, counts
103 -
104 -
105 -@pytest.mark.asyncio
106 -async def test_non_asr_model(winning_call):
107 - # text to text model
108 - model_name = "JackFram/llama-68m"
109 - server_args = ["--enforce-eager"]
110 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
111 - client = remote_server.get_async_client()
112 - res = await client.audio.transcriptions.create(model=model_name,
113 - file=winning_call,
114 - language="en",
115 - temperature=0.0)
116 - err = res.error
117 - assert err["code"] == 400 and not res.text
118 - assert err[
119 - "message"] == "The model does not support Transcriptions API"
113 + transcription = await client.audio.transcriptions.create(
114 + model=MODEL_NAME,
115 + file=buffer,
116 + language="en",
117 + response_format="text",
118 + temperature=0.0)
119 + out = json.loads(transcription)['text']
120 + counts = out.count("Mary had a little lamb")
121 + assert counts == 10, counts
122
123
124 @pytest.mark.asyncio
123 -async def test_completion_endpoints():
125 +async def test_completion_endpoints(client):
126 # text to text model
125 - model_name = "openai/whisper-small"
126 - server_args = ["--enforce-eager"]
127 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
128 - client = remote_server.get_async_client()
129 - res = await client.chat.completions.create(
130 - model=model_name,
131 - messages=[{
132 - "role": "system",
133 - "content": "You are a helpful assistant."
134 - }])
135 - err = res.error
136 - assert err["code"] == 400
137 - assert err[
138 - "message"] == "The model does not support Chat Completions API"
139 -
140 - res = await client.completions.create(model=model_name, prompt="Hello")
141 - err = res.error
142 - assert err["code"] == 400
143 - assert err["message"] == "The model does not support Completions API"
127 + res = await client.chat.completions.create(
128 + model=MODEL_NAME,
129 + messages=[{
130 + "role": "system",
131 + "content": "You are a helpful assistant."
132 + }])
133 + err = res.error
134 + assert err["code"] == 400
135 + assert err["message"] == "The model does not support Chat Completions API"
136 +
137 + res = await client.completions.create(model=MODEL_NAME, prompt="Hello")
138 + err = res.error
139 + assert err["code"] == 400
140 + assert err["message"] == "The model does not support Completions API"
141
142
143 @pytest.mark.asyncio
147 -async def test_streaming_response(winning_call):
148 - model_name = "openai/whisper-small"
149 - server_args = ["--enforce-eager"]
144 +async def test_streaming_response(winning_call, client):
145 transcription = ""
151 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
152 - client = remote_server.get_async_client()
153 - res_no_stream = await client.audio.transcriptions.create(
154 - model=model_name,
155 - file=winning_call,
156 - response_format="json",
157 - language="en",
158 - temperature=0.0)
159 - # Unfortunately this only works when the openai client is patched
160 - # to use streaming mode, not exposed in the transcription api.
161 - original_post = AsyncAPIClient.post
162 -
163 - async def post_with_stream(*args, **kwargs):
164 - kwargs['stream'] = True
165 - return await original_post(*args, **kwargs)
166 -
167 - with patch.object(AsyncAPIClient, "post", new=post_with_stream):
168 - client = remote_server.get_async_client()
169 - res = await client.audio.transcriptions.create(
170 - model=model_name,
171 - file=winning_call,
172 - language="en",
173 - temperature=0.0,
174 - extra_body=dict(stream=True),
175 - timeout=30)
176 - # Reconstruct from chunks and validate
177 - async for chunk in res:
178 - # just a chunk
179 - text = chunk.choices[0]['delta']['content']
180 - transcription += text
181 -
182 - assert transcription == res_no_stream.text
146 + res_no_stream = await client.audio.transcriptions.create(
147 + model=MODEL_NAME,
148 + file=winning_call,
149 + response_format="json",
150 + language="en",
151 + temperature=0.0)
152 + res = await client.audio.transcriptions.create(model=MODEL_NAME,
153 + file=winning_call,
154 + language="en",
155 + temperature=0.0,
156 + stream=True,
157 + timeout=30)
158 + # Reconstruct from chunks and validate
159 + async for chunk in res:
160 + text = chunk.choices[0]['delta']['content']
161 + transcription += text
162 +
163 + assert transcription == res_no_stream.text
164
165
166 @pytest.mark.asyncio
186 -async def test_stream_options(winning_call):
187 - model_name = "openai/whisper-small"
188 - server_args = ["--enforce-eager"]
189 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
190 - original_post = AsyncAPIClient.post
191 -
192 - async def post_with_stream(*args, **kwargs):
193 - kwargs['stream'] = True
194 - return await original_post(*args, **kwargs)
195 -
196 - with patch.object(AsyncAPIClient, "post", new=post_with_stream):
197 - client = remote_server.get_async_client()
198 - res = await client.audio.transcriptions.create(
199 - model=model_name,
200 - file=winning_call,
201 - language="en",
202 - temperature=0.0,
203 - extra_body=dict(stream=True,
204 - stream_include_usage=True,
205 - stream_continuous_usage_stats=True),
206 - timeout=30)
207 - final = False
208 - continuous = True
209 - async for chunk in res:
210 - if not len(chunk.choices):
211 - # final usage sent
212 - final = True
213 - else:
214 - continuous = continuous and hasattr(chunk, 'usage')
215 - assert final and continuous
167 +async def test_stream_options(winning_call, client):
168 + res = await client.audio.transcriptions.create(
169 + model=MODEL_NAME,
170 + file=winning_call,
171 + language="en",
172 + temperature=0.0,
173 + stream=True,
174 + extra_body=dict(stream_include_usage=True,
175 + stream_continuous_usage_stats=True),
176 + timeout=30)
177 + final = False
178 + continuous = True
179 + async for chunk in res:
180 + if not len(chunk.choices):
181 + # final usage sent
182 + final = True
183 + else:
184 + continuous = continuous and hasattr(chunk, 'usage')
185 + assert final and continuous
186
187
188 @pytest.mark.asyncio
219 -async def test_sampling_params(mary_had_lamb):
189 +async def test_sampling_params(mary_had_lamb, client):
190 """
191 Compare sampling with params and greedy sampling to assert results
192 are different when extreme sampling parameters values are picked.
193 """
224 - model_name = "openai/whisper-small"
225 - server_args = ["--enforce-eager"]
226 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
227 - client = remote_server.get_async_client()
228 - transcription = await client.audio.transcriptions.create(
229 - model=model_name,
230 - file=mary_had_lamb,
231 - language="en",
232 - temperature=0.8,
233 - extra_body=dict(seed=42,
234 - repetition_penalty=1.9,
235 - top_k=12,
236 - top_p=0.4,
237 - min_p=0.5,
238 - frequency_penalty=1.8,
239 - presence_penalty=2.0))
240 -
241 - greedy_transcription = await client.audio.transcriptions.create(
242 - model=model_name,
243 - file=mary_had_lamb,
244 - language="en",
245 - temperature=0.0,
246 - extra_body=dict(seed=42))
247 -
248 - assert greedy_transcription.text != transcription.text
194 + transcription = await client.audio.transcriptions.create(
195 + model=MODEL_NAME,
196 + file=mary_had_lamb,
197 + language="en",
198 + temperature=0.8,
199 + extra_body=dict(seed=42,
200 + repetition_penalty=1.9,
201 + top_k=12,
202 + top_p=0.4,
203 + min_p=0.5,
204 + frequency_penalty=1.8,
205 + presence_penalty=2.0))
206 +
207 + greedy_transcription = await client.audio.transcriptions.create(
208 + model=MODEL_NAME,
209 + file=mary_had_lamb,
210 + language="en",
211 + temperature=0.0,
212 + extra_body=dict(seed=42))
213 +
214 + assert greedy_transcription.text != transcription.text
215
216
217 @pytest.mark.asyncio
252 -async def test_audio_prompt(mary_had_lamb):
253 - model_name = "openai/whisper-large-v3-turbo"
254 - server_args = ["--enforce-eager"]
218 +async def test_audio_prompt(mary_had_lamb, client):
219 prompt = "This is a speech, recorded in a phonograph."
256 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
257 - #Prompts should not omit the part of original prompt while transcribing.
258 - prefix = "The first words I spoke in the original phonograph"
259 - client = remote_server.get_async_client()
260 - transcription = await client.audio.transcriptions.create(
261 - model=model_name,
262 - file=mary_had_lamb,
263 - language="en",
264 - response_format="text",
265 - temperature=0.0)
266 - out = json.loads(transcription)['text']
267 - assert prefix in out
268 - transcription_wprompt = await client.audio.transcriptions.create(
269 - model=model_name,
270 - file=mary_had_lamb,
271 - language="en",
272 - response_format="text",
273 - prompt=prompt,
274 - temperature=0.0)
275 - out_prompt = json.loads(transcription_wprompt)['text']
276 - assert prefix in out_prompt
220 + #Prompts should not omit the part of original prompt while transcribing.
221 + prefix = "The first words I spoke in the original phonograph"
222 + transcription = await client.audio.transcriptions.create(
223 + model=MODEL_NAME,
224 + file=mary_had_lamb,
225 + language="en",
226 + response_format="text",
227 + temperature=0.0)
228 + out = json.loads(transcription)['text']
229 + assert prefix in out
230 + transcription_wprompt = await client.audio.transcriptions.create(
231 + model=MODEL_NAME,
232 + file=mary_had_lamb,
233 + language="en",
234 + response_format="text",
235 + prompt=prompt,
236 + temperature=0.0)
237 + out_prompt = json.loads(transcription_wprompt)['text']
238 + assert prefix in out_prompt
tests/entrypoints/openai/test_translation_validation.py
+122 -112
@@ -4,18 +4,21 @@
4 import io
5 # imports for guided decoding tests
6 import json
7 -from unittest.mock import patch
7
8 +import httpx
9 import librosa
10 import numpy as np
11 import pytest
12 +import pytest_asyncio
13 import soundfile as sf
13 -from openai._base_client import AsyncAPIClient
14
15 from vllm.assets.audio import AudioAsset
16
17 from ...utils import RemoteOpenAIServer
18
19 +MODEL_NAME = "openai/whisper-small"
20 +SERVER_ARGS = ["--enforce-eager"]
21 +
22
23 @pytest.fixture
24 def foscolo():
@@ -25,50 +28,23 @@ def foscolo():
28 yield f
29
30
28 -# NOTE: (NickLucche) the large-v3-turbo model was not trained on translation!
29 -@pytest.mark.asyncio
30 -async def test_basic_audio(foscolo):
31 - model_name = "openai/whisper-small"
32 - server_args = ["--enforce-eager"]
33 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
34 - client = remote_server.get_async_client()
35 - translation = await client.audio.translations.create(
36 - model=model_name,
37 - file=foscolo,
38 - response_format="text",
39 - # TODO remove once language detection is implemented
40 - extra_body=dict(language="it"),
41 - temperature=0.0)
42 - out = json.loads(translation)['text'].strip().lower()
43 - assert "greek sea" in out
31 +@pytest.fixture(scope="module")
32 +def server():
33 + with RemoteOpenAIServer(MODEL_NAME, SERVER_ARGS) as remote_server:
34 + yield remote_server
35
36
46 -@pytest.mark.asyncio
47 -async def test_audio_prompt(foscolo):
48 - model_name = "openai/whisper-small"
49 - server_args = ["--enforce-eager"]
50 - # Condition whisper on starting text
51 - prompt = "Nor have I ever"
52 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
53 - client = remote_server.get_async_client()
54 - transcription = await client.audio.translations.create(
55 - model=model_name,
56 - file=foscolo,
57 - prompt=prompt,
58 - extra_body=dict(language="it"),
59 - response_format="text",
60 - temperature=0.0)
61 - out = json.loads(transcription)['text']
62 - assert "Nor will I ever touch the sacred" not in out
63 - assert prompt not in out
37 +@pytest_asyncio.fixture
38 +async def client(server):
39 + async with server.get_async_client() as async_client:
40 + yield async_client
41
42
43 @pytest.mark.asyncio
44 async def test_non_asr_model(foscolo):
45 # text to text model
46 model_name = "JackFram/llama-68m"
70 - server_args = ["--enforce-eager"]
71 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
47 + with RemoteOpenAIServer(model_name, SERVER_ARGS) as remote_server:
48 client = remote_server.get_async_client()
49 res = await client.audio.translations.create(model=model_name,
50 file=foscolo,
@@ -78,81 +54,117 @@ async def test_non_asr_model(foscolo):
54 assert err["message"] == "The model does not support Translations API"
55
56
57 +# NOTE: (NickLucche) the large-v3-turbo model was not trained on translation!
58 +@pytest.mark.asyncio
59 +async def test_basic_audio(foscolo, client):
60 + translation = await client.audio.translations.create(
61 + model=MODEL_NAME,
62 + file=foscolo,
63 + response_format="text",
64 + # TODO remove once language detection is implemented
65 + extra_body=dict(language="it"),
66 + temperature=0.0)
67 + out = json.loads(translation)['text'].strip().lower()
68 + assert "greek sea" in out
69 +
70 +
71 +@pytest.mark.asyncio
72 +async def test_audio_prompt(foscolo, client):
73 + # Condition whisper on starting text
74 + prompt = "Nor have I ever"
75 + transcription = await client.audio.translations.create(
76 + model=MODEL_NAME,
77 + file=foscolo,
78 + prompt=prompt,
79 + extra_body=dict(language="it"),
80 + response_format="text",
81 + temperature=0.0)
82 + out = json.loads(transcription)['text']
83 + assert "Nor will I ever touch the sacred" not in out
84 + assert prompt not in out
85 +
86 +
87 @pytest.mark.asyncio
82 -async def test_streaming_response(foscolo):
83 - model_name = "openai/whisper-small"
84 - server_args = ["--enforce-eager"]
88 +async def test_streaming_response(foscolo, client, server):
89 translation = ""
86 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
87 - client = remote_server.get_async_client()
88 - res_no_stream = await client.audio.translations.create(
89 - model=model_name,
90 - file=foscolo,
91 - response_format="json",
92 - extra_body=dict(language="it"),
93 - temperature=0.0)
94 - # Unfortunately this only works when the openai client is patched
95 - # to use streaming mode, not exposed in the translation api.
96 - original_post = AsyncAPIClient.post
97 -
98 - async def post_with_stream(*args, **kwargs):
99 - kwargs['stream'] = True
100 - return await original_post(*args, **kwargs)
101 -
102 - with patch.object(AsyncAPIClient, "post", new=post_with_stream):
103 - client = remote_server.get_async_client()
104 - res = await client.audio.translations.create(model=model_name,
105 - file=foscolo,
106 - temperature=0.0,
107 - extra_body=dict(
108 - stream=True,
109 - language="it"))
110 - # Reconstruct from chunks and validate
111 - async for chunk in res:
112 - # just a chunk
113 - text = chunk.choices[0]['delta']['content']
114 - translation += text
115 -
116 - assert translation == res_no_stream.text
90 + res_no_stream = await client.audio.translations.create(
91 + model=MODEL_NAME,
92 + file=foscolo,
93 + response_format="json",
94 + extra_body=dict(language="it"),
95 + temperature=0.0)
96 + # Stream via HTTPX since OpenAI translation client doesn't expose streaming
97 + url = server.url_for("v1/audio/translations")
98 + headers = {"Authorization": f"Bearer {server.DUMMY_API_KEY}"}
99 + data = {
100 + "model": MODEL_NAME,
101 + "language": "it",
102 + "stream": True,
103 + "temperature": 0.0,
104 + }
105 + foscolo.seek(0)
106 + async with httpx.AsyncClient() as http_client:
107 + files = {"file": foscolo}
108 + async with http_client.stream("POST",
109 + url,
110 + headers=headers,
111 + data=data,
112 + files=files) as response:
113 + async for line in response.aiter_lines():
114 + if not line:
115 + continue
116 + if line.startswith("data: "):
117 + line = line[len("data: "):]
118 + if line.strip() == "[DONE]":
119 + break
120 + chunk = json.loads(line)
121 + text = chunk["choices"][0].get("delta", {}).get("content")
122 + translation += text or ""
123 +
124 + assert translation == res_no_stream.text
125
126
127 @pytest.mark.asyncio
120 -async def test_stream_options(foscolo):
121 - model_name = "openai/whisper-small"
122 - server_args = ["--enforce-eager"]
123 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
124 - original_post = AsyncAPIClient.post
125 -
126 - async def post_with_stream(*args, **kwargs):
127 - kwargs['stream'] = True
128 - return await original_post(*args, **kwargs)
129 -
130 - with patch.object(AsyncAPIClient, "post", new=post_with_stream):
131 - client = remote_server.get_async_client()
132 - res = await client.audio.translations.create(
133 - model=model_name,
134 - file=foscolo,
135 - temperature=0.0,
136 - extra_body=dict(language="it",
137 - stream=True,
138 - stream_include_usage=True,
139 - stream_continuous_usage_stats=True))
140 - final = False
141 - continuous = True
142 - async for chunk in res:
143 - if not len(chunk.choices):
128 +async def test_stream_options(foscolo, client, server):
129 + url = server.url_for("v1/audio/translations")
130 + headers = {"Authorization": f"Bearer {server.DUMMY_API_KEY}"}
131 + data = {
132 + "model": MODEL_NAME,
133 + "language": "it",
134 + "stream": True,
135 + "stream_include_usage": True,
136 + "stream_continuous_usage_stats": True,
137 + "temperature": 0.0,
138 + }
139 + foscolo.seek(0)
140 + final = False
141 + continuous = True
142 + async with httpx.AsyncClient() as http_client:
143 + files = {"file": foscolo}
144 + async with http_client.stream("POST",
145 + url,
146 + headers=headers,
147 + data=data,
148 + files=files) as response:
149 + async for line in response.aiter_lines():
150 + if not line:
151 + continue
152 + if line.startswith("data: "):
153 + line = line[len("data: "):]
154 + if line.strip() == "[DONE]":
155 + break
156 + chunk = json.loads(line)
157 + choices = chunk.get("choices", [])
158 + if not choices:
159 # final usage sent
160 final = True
161 else:
147 - continuous = continuous and hasattr(chunk, 'usage')
148 - assert final and continuous
162 + continuous = continuous and ("usage" in chunk)
163 + assert final and continuous
164
165
166 @pytest.mark.asyncio
152 -async def test_long_audio_request(foscolo):
153 - model_name = "openai/whisper-small"
154 - server_args = ["--enforce-eager"]
155 -
167 +async def test_long_audio_request(foscolo, client):
168 foscolo.seek(0)
169 audio, sr = librosa.load(foscolo)
170 repeated_audio = np.tile(audio, 2)
@@ -160,13 +172,11 @@ async def test_long_audio_request(foscolo):
172 buffer = io.BytesIO()
173 sf.write(buffer, repeated_audio, sr, format='WAV')
174 buffer.seek(0)
163 - with RemoteOpenAIServer(model_name, server_args) as remote_server:
164 - client = remote_server.get_async_client()
165 - translation = await client.audio.translations.create(
166 - model=model_name,
167 - file=buffer,
168 - extra_body=dict(language="it"),
169 - response_format="text",
170 - temperature=0.0)
171 - out = json.loads(translation)['text'].strip().lower()
172 - assert out.count("greek sea") == 2
175 + translation = await client.audio.translations.create(
176 + model=MODEL_NAME,
177 + file=buffer,
178 + extra_body=dict(language="it"),
179 + response_format="text",
180 + temperature=0.0)
181 + out = json.loads(translation)['text'].strip().lower()
182 + assert out.count("greek sea") == 2