@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 226ba9fc9e
Fork 0

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

[ASR] Add Long Audio benchmark and correctness test (#44587)

Signed-off-by: Ekagra Ranjan <3116519+ekagra-ranjan@users.noreply.github.com>

Ekagra Ranjan committed Jun 12, 2026 at 00:11 UTC 226ba9fc9e285556e7269e4efe102530a4d9fedb
5 files changed +530 -46
docs/benchmarking/cli.md
+2 -2
@@ -37,7 +37,7 @@ th {
37 | HuggingFace-HumanEval | ✅ | ✅ | `openai/openai_humaneval` |
38 | HuggingFace-GSM8K | ✅ | ✅ | `openai/gsm8k` |
39 | HuggingFace-Blazedit | ✅ | ✅ | `vdaita/edit_5k_char`, `vdaita/edit_10k_char` |
40 -| HuggingFace-ASR | ✅ | ✅ | `openslr/librispeech_asr`, `facebook/voxpopuli`, `LIUM/tedlium`, `edinburghcstr/ami`, `speechcolab/gigaspeech`, `kensho/spgispeech` |
40 +| HuggingFace-ASR | ✅ | ✅ | `openslr/librispeech_asr`, `facebook/voxpopuli`, `LIUM/tedlium`, `edinburghcstr/ami`, `speechcolab/gigaspeech`, `kensho/spgispeech`, `ArtificialAnalysis/Earnings22-Cleaned-AA`, `D4nt3/esb-datasets-earnings22-validation-tiny-filtered` |
41 | Spec Bench | ✅ | ✅ | `wget https://raw.githubusercontent.com/hemingkx/Spec-Bench/refs/heads/main/data/spec_bench/question.jsonl` |
42 | SPEED-Bench | ✅ | ✅ | `curl -LsSf https://raw.githubusercontent.com/NVIDIA-NeMo/Skills/refs/heads/main/nemo_skills/dataset/speed-bench/prepare.py \| python3 -` |
43 | Custom | ✅ | ✅ | Local file: `data.jsonl` |
@@ -532,7 +532,7 @@ vllm bench serve \
532 --blazedit-max-distance 0.99
533 ```
534
535 -`openslr/librispeech_asr`, `facebook/voxpopuli`, `LIUM/tedlium`, `edinburghcstr/ami`, `speechcolab/gigaspeech`, `kensho/spgispeech`
535 +`openslr/librispeech_asr`, `facebook/voxpopuli`, `LIUM/tedlium`, `edinburghcstr/ami`, `speechcolab/gigaspeech`, `kensho/spgispeech`, `ArtificialAnalysis/Earnings22-Cleaned-AA`, `D4nt3/esb-datasets-earnings22-validation-tiny-filtered`
536
537 ```bash
538 vllm bench serve \
tests/benchmarks/test_audio_dataset.py new
+200
@@ -0,0 +1,200 @@
1 +#!/usr/bin/env python3
2 +# SPDX-License-Identifier: Apache-2.0
3 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4 +
5 +import asyncio
6 +from pathlib import Path
7 +from typing import Protocol, cast
8 +
9 +import numpy as np
10 +import pytest
11 +import soundfile as sf
12 +
13 +import vllm.benchmarks.datasets.datasets as datasets_module
14 +import vllm.benchmarks.lib.endpoint_request_func as request_func_module
15 +from vllm.benchmarks.lib.endpoint_request_func import RequestFuncInput
16 +
17 +pytestmark = pytest.mark.skip_global_cleanup
18 +
19 +
20 +class _ReadableBinary(Protocol):
21 + def read(self, size: int = -1) -> bytes: ...
22 +
23 +
24 +class _TokenizedPrompt:
25 + def __init__(self, prompt: str) -> None:
26 + self.input_ids = prompt.split()
27 +
28 +
29 +class _Tokenizer:
30 + def __init__(self, name_or_path: str = "openai/whisper-large-v3") -> None:
31 + self.name_or_path = name_or_path
32 +
33 + def __call__(self, prompt: str) -> _TokenizedPrompt:
34 + return _TokenizedPrompt(prompt)
35 +
36 +
37 +def _write_wav(path: Path, duration_s: float = 0.1, sample_rate: int = 16_000) -> None:
38 + num_samples = int(duration_s * sample_rate)
39 + sf.write(path, np.zeros(num_samples, dtype=np.float32), sample_rate)
40 +
41 +
42 +class _FakeFormData:
43 + def __init__(self) -> None:
44 + self.fields: list[tuple[str, object, dict[str, str]]] = []
45 +
46 + def add_field(self, name: str, value: object, **kwargs: str) -> None:
47 + self.fields.append((name, value, kwargs))
48 +
49 +
50 +class _FakeContent:
51 + async def iter_any(self):
52 + yield b'data: {"choices":[{"delta":{"content":"hello"}}]}\n\n'
53 + yield b'data: {"usage":{"completion_tokens":1}}\n\n'
54 + yield b"data: [DONE]\n\n"
55 +
56 +
57 +class _FakeResponse:
58 + def __init__(self) -> None:
59 + self.status = 200
60 + self.reason = "OK"
61 + self.content = _FakeContent()
62 +
63 + async def __aenter__(self):
64 + return self
65 +
66 + async def __aexit__(self, exc_type, exc, tb):
67 + return False
68 +
69 +
70 +class _FakeSession:
71 + def __init__(self) -> None:
72 + self.uploaded_bytes: bytes | None = None
73 + self.upload_filename: str | None = None
74 + self.fields: list[tuple[str, object, dict[str, str]]] | None = None
75 +
76 + def post(self, *, url: str, data: _FakeFormData, headers: dict[str, str]):
77 + del url, headers
78 + self.fields = list(data.fields)
79 + _, file_obj, file_kwargs = self.fields[0]
80 + file_obj = cast(_ReadableBinary, file_obj)
81 + self.uploaded_bytes = file_obj.read()
82 + self.upload_filename = file_kwargs.get("filename")
83 + return _FakeResponse()
84 +
85 +
86 +def test_asr_dataset_sample_handles_local_audio_paths(tmp_path: Path) -> None:
87 + audio_path = tmp_path / "earnings.wav"
88 + _write_wav(audio_path, duration_s=0.1)
89 +
90 + dataset = object.__new__(datasets_module.ASRDataset)
91 + dataset.data = [
92 + {
93 + "audio": {
94 + "path": str(audio_path),
95 + "bytes": None,
96 + },
97 + "text": "quarterly earnings call",
98 + }
99 + ]
100 +
101 + samples = dataset.sample(
102 + tokenizer=_Tokenizer(),
103 + num_requests=1,
104 + output_len=32,
105 + asr_min_audio_len_sec=0.0,
106 + asr_max_audio_len_sec=1.0,
107 + )
108 +
109 + assert len(samples) == 1
110 + assert samples[0].multi_modal_data == {"audio_path": str(audio_path)}
111 + assert (
112 + samples[0].prompt == "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>"
113 + )
114 +
115 +
116 +def test_asr_dataset_sample_handles_embedded_audio_bytes(tmp_path: Path) -> None:
117 + audio_path = tmp_path / "earnings.wav"
118 + _write_wav(audio_path, duration_s=0.1)
119 +
120 + dataset = object.__new__(datasets_module.ASRDataset)
121 + dataset.data = [
122 + {
123 + "audio": {
124 + "path": None,
125 + "bytes": audio_path.read_bytes(),
126 + },
127 + "text": "quarterly earnings call",
128 + }
129 + ]
130 +
131 + samples = dataset.sample(
132 + tokenizer=_Tokenizer(),
133 + num_requests=1,
134 + output_len=32,
135 + asr_min_audio_len_sec=0.0,
136 + asr_max_audio_len_sec=1.0,
137 + )
138 +
139 + assert len(samples) == 1
140 + assert isinstance(samples[0].multi_modal_data, dict)
141 + audio, sample_rate = samples[0].multi_modal_data["audio"]
142 + assert sample_rate == 16_000
143 + assert isinstance(audio, np.ndarray)
144 + assert audio.size > 0
145 +
146 +
147 +def test_async_request_openai_audio_handles_local_audio_paths(
148 + monkeypatch: pytest.MonkeyPatch,
149 + tmp_path: Path,
150 +) -> None:
151 + audio_path = tmp_path / "earnings.wav"
152 + _write_wav(audio_path, duration_s=0.25)
153 +
154 + monkeypatch.setattr(request_func_module.aiohttp, "FormData", _FakeFormData)
155 + session = _FakeSession()
156 + request_input = RequestFuncInput(
157 + prompt="",
158 + api_url="http://localhost:8000/v1/audio/transcriptions",
159 + prompt_len=1,
160 + output_len=32,
161 + model="openai/whisper-large-v3",
162 + multi_modal_content={"audio_path": str(audio_path)},
163 + )
164 +
165 + output = asyncio.run(
166 + request_func_module.async_request_openai_audio(request_input, session)
167 + )
168 +
169 + assert session.upload_filename == audio_path.name
170 + assert session.uploaded_bytes == audio_path.read_bytes()
171 + assert output.success is True
172 + assert output.generated_text == "hello"
173 + assert output.output_tokens == 1
174 + assert output.input_audio_duration == pytest.approx(0.25, abs=1e-2)
175 +
176 +
177 +def test_async_request_openai_audio_handles_decoded_audio_arrays(
178 + monkeypatch: pytest.MonkeyPatch,
179 +) -> None:
180 + monkeypatch.setattr(request_func_module.aiohttp, "FormData", _FakeFormData)
181 + session = _FakeSession()
182 + request_input = RequestFuncInput(
183 + prompt="",
184 + api_url="http://localhost:8000/v1/audio/transcriptions",
185 + prompt_len=1,
186 + output_len=32,
187 + model="openai/whisper-large-v3",
188 + multi_modal_content={
189 + "audio": (np.zeros(1_600, dtype=np.float32), 16_000),
190 + },
191 + )
192 +
193 + output = asyncio.run(
194 + request_func_module.async_request_openai_audio(request_input, session)
195 + )
196 +
197 + assert session.upload_filename == "audio.wav"
198 + assert session.uploaded_bytes is not None
199 + assert output.success is True
200 + assert output.generated_text == "hello"
tests/entrypoints/speech_to_text/correctness/test_transcription_api_correctness.py
+201 -19
@@ -16,10 +16,11 @@ from statistics import mean, median
16 import pytest
17 import soundfile
18 import torch
19 -from datasets import load_dataset
19 +from datasets import Audio, load_dataset
20 from evaluate import load
21 from transformers.models.whisper.english_normalizer import EnglishTextNormalizer
22
23 +from vllm.benchmarks.datasets.datasets import ASRDataset
24 from vllm.multimodal.audio import get_audio_duration
25 from vllm.tokenizers import get_tokenizer
26
@@ -38,6 +39,20 @@ def to_bytes(y, sr):
39 return buffer
40
41
42 +def load_audio_sample(audio):
43 + # Avoid torchcodec in CI by decoding dataset audio with soundfile.
44 + if "array" in audio and "sampling_rate" in audio:
45 + return audio["array"], audio["sampling_rate"]
46 +
47 + if audio.get("path"):
48 + return soundfile.read(audio["path"], dtype="float32")
49 +
50 + if audio.get("bytes") is not None:
51 + return soundfile.read(io.BytesIO(audio["bytes"]), dtype="float32")
52 +
53 + raise ValueError("Audio sample did not contain array, path, or bytes data")
54 +
55 +
56 # not all models have a normalizer so use the one from whisper as a standard option
57 normalizer_model_info = HF_EXAMPLE_MODELS.find_hf_info("openai/whisper-large-v3")
58 normalizer_tokenizer = get_tokenizer(
@@ -48,7 +63,7 @@ normalizer_tokenizer = get_tokenizer(
63 normalizer = EnglishTextNormalizer(normalizer_tokenizer.english_spelling_normalizer)
64
65
51 -async def transcribe_audio(client, tokenizer, y, sr):
66 +async def transcribe_audio(client, tokenizer, y, sr, extra_body=None):
67 # Send loaded audio directly instead of loading from disk,
68 # don't account for that time though
69 with to_bytes(y, sr) as f:
@@ -58,6 +73,7 @@ async def transcribe_audio(client, tokenizer, y, sr):
73 model=tokenizer.name_or_path,
74 language="en",
75 temperature=0.0,
76 + extra_body=extra_body,
77 )
78 end_time = time.perf_counter()
79 # NOTE there's no streaming in transcriptions, can't measure ttft
@@ -68,17 +84,21 @@ async def transcribe_audio(client, tokenizer, y, sr):
84 return latency, num_output_tokens, transcription.text
85
86
71 -async def bound_transcribe(sem, client, tokenizer, audio, reference):
87 +async def bound_transcribe(
88 + sem, client, tokenizer, audio, sr, reference, extra_body=None
89 +):
90 # Use semaphore to limit concurrent requests.
91 async with sem:
74 - result = await transcribe_audio(client, tokenizer, *audio)
92 + result = await transcribe_audio(
93 + client, tokenizer, audio, sr, extra_body=extra_body
94 + )
95 # Normalize *english* output/reference for evaluation.
96 out = normalizer(result[2])
97 ref = normalizer(reference)
98 return result[:2] + (out, ref)
99
100
81 -async def process_dataset(model, client, data, concurrent_request):
101 +async def process_dataset(model, client, data, concurrent_request, extra_body=None):
102 sem = asyncio.Semaphore(concurrent_request)
103
104 model_info = HF_EXAMPLE_MODELS.find_hf_info(model)
@@ -89,14 +109,16 @@ async def process_dataset(model, client, data, concurrent_request):
109 )
110
111 # Warmup call as the first `load_audio` server-side is quite slow.
92 - audio, sr = data[0]["audio"]["array"], data[0]["audio"]["sampling_rate"]
93 - _ = await bound_transcribe(sem, client, tokenizer, (audio, sr), "")
112 + audio, sr = load_audio_sample(data[0]["audio"])
113 + _ = await bound_transcribe(sem, client, tokenizer, audio, sr, "", extra_body)
114
115 tasks: list[asyncio.Task] = []
116 for sample in data:
97 - audio, sr = sample["audio"]["array"], sample["audio"]["sampling_rate"]
117 + audio, sr = load_audio_sample(sample["audio"])
118 task = asyncio.create_task(
99 - bound_transcribe(sem, client, tokenizer, (audio, sr), sample["text"])
119 + bound_transcribe(
120 + sem, client, tokenizer, audio, sr, sample["text"], extra_body
121 + )
122 )
123 tasks.append(task)
124 return await asyncio.gather(*tasks)
@@ -121,19 +143,36 @@ def print_performance_metrics(results, total_time):
143
144
145 def add_duration(sample):
124 - y, sr = sample["audio"]["array"], sample["audio"]["sampling_rate"]
146 + y, sr = load_audio_sample(sample["audio"])
147 sample["duration_ms"] = get_audio_duration(y=y, sr=sr) * 1000
148 return sample
149
150
129 -def load_hf_dataset(dataset_repo: str, split="validation", **hf_kwargs):
130 - ## Load and filter the dataset
131 - dataset = load_dataset(dataset_repo, split=split, **hf_kwargs)
132 - if "duration_ms" not in dataset[0]:
133 - # compute duration to filter
151 +def load_asr_dataset_rows(dataset_repo: str, split="validation", **hf_kwargs):
152 + if dataset_repo in ASRDataset.SUPPORTED_DATASET_PATHS:
153 + asr_dataset_kwargs = {
154 + "dataset_path": dataset_repo,
155 + "dataset_split": split,
156 + "disable_shuffle": True,
157 + "no_stream": True,
158 + }
159 + for key in ("dataset_subset", "hf_name", "trust_remote_code"):
160 + if key in hf_kwargs:
161 + asr_dataset_kwargs[key] = hf_kwargs[key]
162 + return ASRDataset(**asr_dataset_kwargs).data
163 +
164 + return load_dataset(dataset_repo, split=split, **hf_kwargs)
165 +
166 +
167 +def load_shortform_eval_dataset(dataset_repo: str, split="validation", **hf_kwargs):
168 + ## Load and filter the dataset.
169 + dataset = load_asr_dataset_rows(dataset_repo, split=split, **hf_kwargs)
170 + dataset = dataset.cast_column("audio", Audio(decode=False))
171 + if "duration_ms" not in dataset.column_names:
172 + # Compute duration to filter.
173 dataset = dataset.map(add_duration)
174
136 - # Whisper max supported duration
175 + # Whisper max supported duration.
176 dataset = dataset.filter(lambda example: example["duration_ms"] < 30000)
177 return dataset
178
@@ -145,11 +184,16 @@ def run_evaluation(
184 max_concurrent_reqs: int,
185 n_examples: int = -1,
186 print_metrics: bool = True,
187 + extra_body=None,
188 ):
189 if n_examples > 0:
190 dataset = dataset.select(range(n_examples))
191 start = time.perf_counter()
152 - results = asyncio.run(process_dataset(model, client, dataset, max_concurrent_reqs))
192 + results = asyncio.run(
193 + process_dataset(
194 + model, client, dataset, max_concurrent_reqs, extra_body=extra_body
195 + )
196 + )
197 end = time.perf_counter()
198 total_time = end - start
199 print(f"Total Test Time: {total_time:.4f} seconds")
@@ -164,6 +208,106 @@ def run_evaluation(
208 return wer_score
209
210
211 +LONGFORM_DATASET_REPO = ASRDataset.EARNINGS22_CLEANED_DATASET
212 +LONGFORM_DATASET_SPLIT = "test"
213 +LONGFORM_NUM_SAMPLES = 6
214 +
215 +
216 +def load_longform_dataset():
217 + dataset = load_asr_dataset_rows(
218 + LONGFORM_DATASET_REPO,
219 + split=LONGFORM_DATASET_SPLIT,
220 + )
221 + assert len(dataset) >= LONGFORM_NUM_SAMPLES
222 + return dataset.select(range(LONGFORM_NUM_SAMPLES))
223 +
224 +
225 +async def transcribe_audio_path(client, tokenizer, audio_path: str, extra_body=None):
226 + with open(audio_path, "rb") as f:
227 + start_time = time.perf_counter()
228 + transcription = await client.audio.transcriptions.create(
229 + file=f,
230 + model=tokenizer.name_or_path,
231 + language="en",
232 + temperature=0.0,
233 + extra_body=extra_body,
234 + )
235 + end_time = time.perf_counter()
236 +
237 + latency = end_time - start_time
238 + num_output_tokens = len(
239 + tokenizer(transcription.text, add_special_tokens=False).input_ids
240 + )
241 + return latency, num_output_tokens, transcription.text
242 +
243 +
244 +async def bound_transcribe_path(
245 + sem, client, tokenizer, audio_path, reference, extra_body=None
246 +):
247 + async with sem:
248 + result = await transcribe_audio_path(
249 + client, tokenizer, audio_path, extra_body=extra_body
250 + )
251 + out = normalizer(result[2])
252 + ref = normalizer(reference)
253 + return result[:2] + (out, ref)
254 +
255 +
256 +async def process_longform_dataset(
257 + model, client, data, concurrent_request, extra_body=None
258 +):
259 + sem = asyncio.Semaphore(concurrent_request)
260 +
261 + model_info = HF_EXAMPLE_MODELS.find_hf_info(model)
262 + tokenizer = get_tokenizer(
263 + model,
264 + tokenizer_mode=model_info.tokenizer_mode,
265 + trust_remote_code=model_info.trust_remote_code,
266 + )
267 +
268 + warmup_path = data[0]["audio"]["path"]
269 + _ = await bound_transcribe_path(sem, client, tokenizer, warmup_path, "", extra_body)
270 +
271 + tasks: list[asyncio.Task] = []
272 + for sample in data:
273 + audio_path = sample["audio"]["path"]
274 + task = asyncio.create_task(
275 + bound_transcribe_path(
276 + sem, client, tokenizer, audio_path, sample["text"], extra_body
277 + )
278 + )
279 + tasks.append(task)
280 + return await asyncio.gather(*tasks)
281 +
282 +
283 +def run_longform_evaluation(
284 + model: str,
285 + client,
286 + dataset,
287 + max_concurrent_reqs: int,
288 + print_metrics: bool = True,
289 + extra_body=None,
290 +):
291 + start = time.perf_counter()
292 + results = asyncio.run(
293 + process_longform_dataset(
294 + model, client, dataset, max_concurrent_reqs, extra_body=extra_body
295 + )
296 + )
297 + end = time.perf_counter()
298 + total_time = end - start
299 + print(f"Total Test Time: {total_time:.4f} seconds")
300 + if print_metrics:
301 + print_performance_metrics(results, total_time)
302 +
303 + predictions = [res[2] for res in results]
304 + references = [res[3] for res in results]
305 + wer = load("wer")
306 + wer_score = 100 * wer.compute(references=references, predictions=predictions)
307 + print("WER:", wer_score)
308 + return wer_score
309 +
310 +
311 # alternatives "openai/whisper-large-v2", "openai/whisper-large-v3-turbo"..
312 # NOTE: Expected WER measured with equivalent hf.transformers args:
313 # whisper-large-v3 + esb-datasets-earnings22-validation-tiny-filtered.
@@ -184,7 +328,6 @@ def test_wer_correctness(
328 ):
329 model_name, expected_wer = model_config
330 model_info = HF_EXAMPLE_MODELS.find_hf_info(model_name)
187 - # TODO refactor to use `ASRDataset`
331 server_args = [
332 "--enforce-eager",
333 f"--tokenizer_mode={model_info.tokenizer_mode}",
@@ -197,7 +340,7 @@ def test_wer_correctness(
340 model_name,
341 server_args,
342 ) as remote_server:
200 - dataset = load_hf_dataset(dataset_repo)
343 + dataset = load_shortform_eval_dataset(dataset_repo)
344
345 if not max_concurrent_request:
346 # No max concurrency
@@ -216,3 +359,42 @@ def test_wer_correctness(
359
360 if expected_wer:
361 torch.testing.assert_close(wer, expected_wer, atol=1e-1, rtol=1e-2)
362 +
363 +
364 +# 14-22mins of 6 audio samples of total ~115 mins and just 37MB.
365 +# checks for long audio transcription correctness and RMS split.
366 +@pytest.mark.parametrize(
367 + "model_config",
368 + [("openai/whisper-large-v3", 9.5)],
369 +)
370 +def test_long_audio_wer_correctness(model_config):
371 + model_name, expected_wer = model_config
372 + model_info = HF_EXAMPLE_MODELS.find_hf_info(model_name)
373 + server_args = [
374 + f"--tokenizer_mode={model_info.tokenizer_mode}",
375 + ]
376 +
377 + if model_info.trust_remote_code:
378 + server_args.append("--trust-remote-code")
379 +
380 + # 1800 seconds is 30 minutes
381 + env_dict = {
382 + "VLLM_MAX_AUDIO_DECODE_DURATION_S": "1800",
383 + }
384 +
385 + with RemoteOpenAIServer(
386 + model_name,
387 + server_args,
388 + env_dict=env_dict,
389 + ) as remote_server:
390 + dataset = load_longform_dataset()
391 + client = remote_server.get_async_client()
392 + wer = run_longform_evaluation(
393 + model=model_name,
394 + client=client,
395 + dataset=dataset,
396 + max_concurrent_reqs=LONGFORM_NUM_SAMPLES,
397 + )
398 +
399 + print(f"Expected WER: {expected_wer}, Actual WER: {wer}")
400 + torch.testing.assert_close(wer, expected_wer, atol=1e-1, rtol=1e-2)
vllm/benchmarks/datasets/datasets.py
+83 -14
@@ -4001,20 +4001,27 @@ class ASRDataset(HuggingFaceDataset):
4001 Dataset class for processing a ASR dataset for transcription.
4002 Tested on the following set:
4003
4004 - +----------------+----------------------------------------+--------------------------+-----------------------------+
4005 - | Dataset | Domain | Speaking Style | hf-subset |
4006 - +----------------+----------------------------------------+--------------------------+-----------------------------+
4007 - | TED-LIUM | TED talks | Oratory | release1, release2, release3|
4008 - | | | | release3-speaker-adaptation |
4009 - | VoxPopuli | European Parliament | Oratory | en, de, it, fr, ... |
4010 - | LibriSpeech | Audiobook | Narrated | "LIUM/tedlium" |
4011 - | GigaSpeech | Audiobook, podcast, YouTube | Narrated, spontaneous | xs, s, m, l, xl, dev, test |
4012 - | SPGISpeech | Financial meetings | Oratory, spontaneous | S, M, L, dev, test |
4013 - | AMI | Meetings | Spontaneous | ihm, sdm |
4014 - +----------------+----------------------------------------+--------------------------+-----------------------------+
4004 + +---------------------------+----------------------------------------+--------------------------+-----------------------------+
4005 + | Dataset | Domain | Speaking Style | hf-subset |
4006 + +---------------------------+----------------------------------------+--------------------------+-----------------------------+
4007 + | TED-LIUM | TED talks | Oratory | release1, release2, release3|
4008 + | | | | release3-speaker-adaptation |
4009 + | VoxPopuli | European Parliament | Oratory | en, de, it, fr, ... |
4010 + | LibriSpeech | Audiobook | Narrated | "LIUM/tedlium" |
4011 + | GigaSpeech | Audiobook, podcast, YouTube | Narrated, spontaneous | xs, s, m, l, xl, dev, test |
4012 + | SPGISpeech | Financial meetings | Oratory, spontaneous | S, M, L, dev, test |
4013 + | Earnings22-Cleaned-AA | Long form earnings calls | Prepared remarks, Q&A | test |
4014 + | Earnings22-Tiny-Filtered | Earnings calls | Prepared remarks, Q&A | validation |
4015 + | AMI | Meetings | Spontaneous | ihm, sdm |
4016 + +---------------------------+----------------------------------------+--------------------------+-----------------------------+
4017
4018 """ # noqa: E501
4019
4020 + EARNINGS22_CLEANED_DATASET = "ArtificialAnalysis/Earnings22-Cleaned-AA"
4021 + EARNINGS22_TINY_FILTERED_DATASET = (
4022 + "D4nt3/esb-datasets-earnings22-validation-tiny-filtered"
4023 + )
4024 +
4025 SUPPORTED_DATASET_PATHS = {
4026 "openslr/librispeech_asr",
4027 "facebook/voxpopuli",
@@ -4022,11 +4029,52 @@ class ASRDataset(HuggingFaceDataset):
4029 "edinburghcstr/ami",
4030 "speechcolab/gigaspeech",
4031 "kensho/spgispeech",
4032 + EARNINGS22_CLEANED_DATASET,
4033 + EARNINGS22_TINY_FILTERED_DATASET,
4034 }
4035
4036 DEFAULT_OUTPUT_LEN = 1024
4037 IS_MULTIMODAL = True
4038
4039 + def load_data(self) -> None:
4040 + if self.hf_name == self.EARNINGS22_CLEANED_DATASET:
4041 + # This subset stores repo-local MP3 paths instead of a HF `Audio`
4042 + # column, so eagerly materialize it back into the common schema.
4043 + self.data = load_dataset(
4044 + self.dataset_path,
4045 + name=self.dataset_subset,
4046 + split=self.dataset_split,
4047 + streaming=False,
4048 + trust_remote_code=self.trust_remote_code,
4049 + )
4050 + if not getattr(self, "disable_shuffle", False):
4051 + self.data = self.data.shuffle(seed=self.random_seed)
4052 + self._materialize_local_audio_column()
4053 + return
4054 + if self.hf_name == self.EARNINGS22_TINY_FILTERED_DATASET:
4055 + super().load_data()
4056 + self._disable_audio_decode()
4057 + return
4058 +
4059 + super().load_data()
4060 +
4061 + def _disable_audio_decode(self) -> None:
4062 + from datasets import Audio
4063 +
4064 + self.data = self.data.cast_column("audio", Audio(decode=False))
4065 +
4066 + def _materialize_local_audio_column(self) -> None:
4067 + local_path_root = Path(
4068 + hf_api().snapshot_download(self.hf_name, repo_type="dataset")
4069 + )
4070 + self.data = self.data.map(
4071 + lambda item: {
4072 + "audio": str(local_path_root / item["url"]),
4073 + "text": item["transcript"],
4074 + }
4075 + )
4076 + self._disable_audio_decode()
4077 +
4078 def sample(
4079 self,
4080 tokenizer: TokenizerLike,
@@ -4052,14 +4100,35 @@ class ASRDataset(HuggingFaceDataset):
4100 if len(sampled_requests) >= num_requests:
4101 break
4102 audio = item["audio"]
4055 - y, sr = audio["array"], audio["sampling_rate"]
4056 - duration_s = get_audio_duration(y=y, sr=sr)
4103 + if (
4104 + isinstance(audio, dict)
4105 + and "array" in audio
4106 + and "sampling_rate" in audio
4107 + ):
4108 + y, sr = audio["array"], audio["sampling_rate"]
4109 + duration_s = get_audio_duration(y=y, sr=sr)
4110 + mm_content = {"audio": (y, sr)}
4111 + elif isinstance(audio, str):
4112 + duration_s = sf.info(audio).duration
4113 + mm_content = {"audio_path": audio}
4114 + elif isinstance(audio, dict) and audio.get("path"):
4115 + duration_s = sf.info(audio["path"]).duration
4116 + mm_content = {"audio_path": audio["path"]}
4117 + elif isinstance(audio, dict) and audio.get("bytes") is not None:
4118 + with BytesIO(audio["bytes"]) as audio_buffer:
4119 + y, sr = sf.read(audio_buffer, dtype="float32")
4120 + duration_s = get_audio_duration(y=y, sr=sr)
4121 + mm_content = {"audio": (y, sr)}
4122 + else:
4123 + raise ValueError(
4124 + "ASR samples must provide decoded audio arrays, "
4125 + "embedded audio bytes, or a local audio path."
4126 + )
4127 if duration_s < asr_min_audio_len_sec or duration_s > asr_max_audio_len_sec:
4128 skipped += 1
4129 continue
4130
4131 durations.append(duration_s)
4062 - mm_content = {"audio": (y, sr)}
4132 sampled_requests.append(
4133 SampleRequest(
4134 prompt=prompt,
vllm/benchmarks/lib/endpoint_request_func.py
+44 -11
@@ -445,7 +445,6 @@ async def async_request_openai_audio(
445 api_url = request_func_input.api_url
446 _validate_api_url(api_url, "OpenAI Audio API", {"transcriptions", "translations"})
447
448 - content = [{"type": "text", "text": request_func_input.prompt}]
448 payload = {
449 "model": request_func_input.model_name
450 if request_func_input.model_name
@@ -469,19 +468,26 @@ async def async_request_openai_audio(
468 buffer.seek(0)
469 return buffer
470
472 - mm_audio = request_func_input.multi_modal_content
473 - if not isinstance(mm_audio, dict) or "audio" not in mm_audio:
474 - raise TypeError("multi_modal_content must be a dict containing 'audio'")
475 - with to_bytes(*mm_audio["audio"]) as f:
471 + async def send_audio_file(
472 + audio_file: io.BytesIO | Any,
473 + *,
474 + input_audio_duration: float,
475 + filename: str | None = None,
476 + content_type: str | None = None,
477 + ) -> RequestFuncOutput:
478 form = aiohttp.FormData()
477 - form.add_field("file", f, content_type="audio/wav")
479 + add_field_kwargs: dict[str, str] = {}
480 + if filename is not None:
481 + add_field_kwargs["filename"] = filename
482 + if content_type is not None:
483 + add_field_kwargs["content_type"] = content_type
484 + form.add_field("file", audio_file, **add_field_kwargs)
485 for key, value in payload.items():
486 form.add_field(key, str(value))
487
488 output = RequestFuncOutput()
489 output.prompt_len = request_func_input.prompt_len
483 - output.input_audio_duration = soundfile.info(f).duration
484 - f.seek(0)
490 + output.input_audio_duration = input_audio_duration
491
492 generated_text = ""
493 ttft = 0.0
@@ -541,9 +547,36 @@ async def async_request_openai_audio(
547 exc_info = sys.exc_info()
548 output.error = "".join(traceback.format_exception(*exc_info))
549
544 - if pbar:
545 - pbar.update(1)
546 - return output
550 + if pbar:
551 + pbar.update(1)
552 + return output
553 +
554 + mm_audio = request_func_input.multi_modal_content
555 + if not isinstance(mm_audio, dict):
556 + raise TypeError(
557 + "multi_modal_content must be a dict containing 'audio' or 'audio_path'"
558 + )
559 + if "audio" in mm_audio:
560 + with to_bytes(*mm_audio["audio"]) as f:
561 + input_audio_duration = soundfile.info(f).duration
562 + f.seek(0)
563 + return await send_audio_file(
564 + f,
565 + input_audio_duration=input_audio_duration,
566 + filename="audio.wav",
567 + content_type="audio/wav",
568 + )
569 + if "audio_path" in mm_audio:
570 + audio_path = mm_audio["audio_path"]
571 + with open(audio_path, "rb") as f:
572 + return await send_audio_file(
573 + f,
574 + input_audio_duration=soundfile.info(audio_path).duration,
575 + filename=os.path.basename(audio_path),
576 + )
577 + raise TypeError(
578 + "multi_modal_content must be a dict containing 'audio' or 'audio_path'"
579 + )
580
581
582 async def _run_pooling_request(