main
py 486 lines 14.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import random
4 from typing import Any, NamedTuple, cast
5
6 import numpy as np
7 import pytest
8 from transformers import AutoTokenizer, PreTrainedTokenizerBase
9
10 from vllm.benchmarks.datasets import (
11 RandomDataset,
12 RandomMultiModalDataset,
13 SampleRequest,
14 )
15
16
17 @pytest.fixture(scope="session")
18 def hf_tokenizer() -> PreTrainedTokenizerBase:
19 # Use a small, commonly available tokenizer
20 return AutoTokenizer.from_pretrained("openai-community/gpt2")
21
22
23 class Params(NamedTuple):
24 num_requests: int
25 prefix_len: int
26 range_ratio: float
27 input_len: int
28 output_len: int
29
30
31 @pytest.fixture(scope="session")
32 def random_dataset_params() -> Params:
33 return Params(
34 num_requests=16, prefix_len=7, range_ratio=0.3, input_len=50, output_len=20
35 )
36
37
38 def _fingerprint_sample(req: SampleRequest) -> tuple[str, int, int]:
39 """Project a SampleRequest into a comparable tuple."""
40 assert isinstance(req.prompt, str)
41 return (req.prompt, req.prompt_len, req.expected_output_len)
42
43
44 def _collect_samples(
45 dataset: RandomDataset,
46 tokenizer: PreTrainedTokenizerBase,
47 num_requests: int = 16,
48 prefix_len: int = 7,
49 range_ratio: float = 0.3,
50 input_len: int = 50,
51 output_len: int = 20,
52 ) -> list[tuple[str, int, int]]:
53 samples = dataset.sample(
54 tokenizer=tokenizer,
55 num_requests=num_requests,
56 prefix_len=prefix_len,
57 range_ratio=range_ratio,
58 input_len=input_len,
59 output_len=output_len,
60 )
61 return [_fingerprint_sample(s) for s in samples]
62
63
64 @pytest.mark.benchmark
65 def test_random_dataset_same_seed(
66 hf_tokenizer: PreTrainedTokenizerBase, random_dataset_params: Params
67 ) -> None:
68 """Same seed should yield identical outputs, even if global RNGs change.
69
70 This guards against accidental reliance on Python's random or np.random
71 in RandomDataset after moving to numpy.default_rng.
72 """
73 p = random_dataset_params
74 common_seed = 123
75 dataset_a = RandomDataset(random_seed=common_seed)
76 dataset_b = RandomDataset(random_seed=common_seed)
77 a = _collect_samples(
78 dataset_a,
79 hf_tokenizer,
80 num_requests=p.num_requests,
81 prefix_len=p.prefix_len,
82 range_ratio=p.range_ratio,
83 input_len=p.input_len,
84 output_len=p.output_len,
85 )
86
87 # Perturb global RNG state to ensure isolation
88 random.seed(999)
89 _ = [random.random() for _ in range(100)]
90 np.random.seed(888)
91 _ = [np.random.random() for _ in range(100)]
92
93 b = _collect_samples(
94 dataset_b,
95 hf_tokenizer,
96 num_requests=p.num_requests,
97 prefix_len=p.prefix_len,
98 range_ratio=p.range_ratio,
99 input_len=p.input_len,
100 output_len=p.output_len,
101 )
102 assert a == b
103
104
105 @pytest.mark.benchmark
106 def test_random_dataset_different_seeds(
107 hf_tokenizer: PreTrainedTokenizerBase, random_dataset_params: Params
108 ) -> None:
109 """Different seeds should change outputs with overwhelming likelihood."""
110 p = random_dataset_params
111 seed_a = 0
112 dataset_a = RandomDataset(random_seed=seed_a)
113 a = _collect_samples(
114 dataset_a,
115 hf_tokenizer,
116 num_requests=p.num_requests,
117 prefix_len=p.prefix_len,
118 range_ratio=p.range_ratio,
119 input_len=p.input_len,
120 output_len=p.output_len,
121 )
122
123 seed_b = 999
124 dataset_b = RandomDataset(random_seed=seed_b)
125 # Perturb global RNG with same seed as dataset_a to ensure isolation
126 random.seed(seed_a)
127 np.random.seed(seed_a)
128 b = _collect_samples(
129 dataset_b,
130 hf_tokenizer,
131 num_requests=p.num_requests,
132 prefix_len=p.prefix_len,
133 range_ratio=p.range_ratio,
134 input_len=p.input_len,
135 output_len=p.output_len,
136 )
137 assert a != b
138
139
140 # -----------------------------
141 # RandomMultiModalDataset tests
142 # -----------------------------
143
144
145 def _mm_fingerprint_sample(
146 req: SampleRequest,
147 ) -> tuple[str, int, int, int, list[str]]:
148 """Create a compact fingerprint for multimodal samples.
149
150 Includes:
151 - prompt string
152 - prompt_len
153 - expected_output_len
154 - count of multimodal items
155 - per-item type and URL prefix (e.g., 'data:image/jpeg;base64,')
156 """
157 assert isinstance(req.prompt, str)
158 items = req.multi_modal_data or []
159 item_prefixes: list[str] = []
160 for it in items:
161 if isinstance(it, dict) and it.get("type") == "image_url":
162 url = it.get("image_url", {}).get("url", "")
163 # Only keep a short identifying prefix to avoid huge strings
164 item_prefixes.append(f"image:{url[:22]}")
165 elif isinstance(it, dict) and it.get("type") == "video_url":
166 url = it.get("video_url", {}).get("url", "")
167 item_prefixes.append(f"video:{url[:22]}")
168 else:
169 item_prefixes.append("unknown:")
170 return (
171 req.prompt,
172 req.prompt_len,
173 req.expected_output_len,
174 len(items),
175 item_prefixes,
176 )
177
178
179 def _collect_mm_samples(
180 dataset: RandomMultiModalDataset,
181 tokenizer: PreTrainedTokenizerBase,
182 *,
183 num_requests: int = 8,
184 prefix_len: int = 3,
185 range_ratio: float = 0.0,
186 input_len: int = 20,
187 output_len: int = 5,
188 base_items_per_request: int = 2,
189 num_mm_items_range_ratio: float = 0.0,
190 limit_mm_per_prompt: dict[str, int] | None = None,
191 bucket_config: dict[tuple[int, int, int], float] | None = None,
192 enable_multimodal_chat: bool = False,
193 ) -> list[SampleRequest]:
194 if limit_mm_per_prompt is None:
195 limit_mm_per_prompt = {"image": 5, "video": 0}
196 if bucket_config is None:
197 bucket_config = {(32, 32, 1): 0.5, (52, 64, 1): 0.5}
198 return dataset.sample(
199 tokenizer=tokenizer,
200 num_requests=num_requests,
201 prefix_len=prefix_len,
202 range_ratio=range_ratio,
203 input_len=input_len,
204 output_len=output_len,
205 base_items_per_request=base_items_per_request,
206 num_mm_items_range_ratio=num_mm_items_range_ratio,
207 limit_mm_per_prompt=limit_mm_per_prompt,
208 bucket_config=bucket_config,
209 enable_multimodal_chat=enable_multimodal_chat,
210 )
211
212
213 @pytest.mark.benchmark
214 def test_random_mm_same_seed(hf_tokenizer: PreTrainedTokenizerBase) -> None:
215 seed = 42
216 ds_a = RandomMultiModalDataset(random_seed=seed)
217 ds_b = RandomMultiModalDataset(random_seed=seed)
218 a = _collect_mm_samples(ds_a, hf_tokenizer)
219 b = _collect_mm_samples(ds_b, hf_tokenizer)
220 fa = [_mm_fingerprint_sample(s) for s in a]
221 fb = [_mm_fingerprint_sample(s) for s in b]
222 assert fa == fb
223
224
225 @pytest.mark.benchmark
226 def test_random_mm_different_seeds(
227 hf_tokenizer: PreTrainedTokenizerBase,
228 ) -> None:
229 ds_a = RandomMultiModalDataset(random_seed=0)
230 ds_b = RandomMultiModalDataset(random_seed=999)
231 a = _collect_mm_samples(ds_a, hf_tokenizer)
232 b = _collect_mm_samples(ds_b, hf_tokenizer)
233 fa = [_mm_fingerprint_sample(s) for s in a]
234 fb = [_mm_fingerprint_sample(s) for s in b]
235 assert fa != fb
236
237
238 @pytest.mark.benchmark
239 def test_random_mm_respects_limits(
240 hf_tokenizer: PreTrainedTokenizerBase,
241 ) -> None:
242 ds = RandomMultiModalDataset(random_seed=0)
243 # Requesting 3 items with a per-prompt limit of 1 should error per current
244 # design (dataset refuses to silently clamp below the requested baseline).
245 with pytest.raises(ValueError):
246 _collect_mm_samples(
247 ds,
248 hf_tokenizer,
249 num_requests=12,
250 base_items_per_request=3,
251 num_mm_items_range_ratio=0.0,
252 limit_mm_per_prompt={"image": 1, "video": 0},
253 bucket_config={(32, 32, 1): 1.0},
254 )
255
256
257 @pytest.mark.benchmark
258 def test_random_mm_zero_prob_entries_are_removed(
259 hf_tokenizer: PreTrainedTokenizerBase,
260 ) -> None:
261 ds = RandomMultiModalDataset(random_seed=0)
262 # Second bucket has zero probability and should be ignored after
263 # normalization
264 samples = _collect_mm_samples(
265 ds,
266 hf_tokenizer,
267 num_requests=6,
268 base_items_per_request=2,
269 num_mm_items_range_ratio=0.0,
270 limit_mm_per_prompt={"image": 10, "video": 0},
271 bucket_config={(32, 32, 1): 1.0, (52, 64, 1): 0.0},
272 )
273 for s in samples:
274 assert isinstance(s.multi_modal_data, list)
275 typed_mm = cast(list[dict[str, Any]], s.multi_modal_data)
276 for it in typed_mm:
277 assert it.get("type") == "image_url"
278
279
280 @pytest.mark.benchmark
281 def test_random_mm_zero_items(hf_tokenizer: PreTrainedTokenizerBase) -> None:
282 ds = RandomMultiModalDataset(random_seed=0)
283 samples = _collect_mm_samples(
284 ds,
285 hf_tokenizer,
286 num_requests=5,
287 base_items_per_request=0,
288 num_mm_items_range_ratio=0.0,
289 limit_mm_per_prompt={"image": 5, "video": 0},
290 bucket_config={(32, 32, 1): 1.0},
291 )
292 for s in samples:
293 assert s.multi_modal_data == []
294
295
296 @pytest.mark.benchmark
297 def test_random_mm_num_items_per_prompt(hf_tokenizer: PreTrainedTokenizerBase) -> None:
298 ds = RandomMultiModalDataset(random_seed=0)
299 # Fixed number of images per prompt
300 # set num_mm_items_range_ratio to 0.0
301 # TODO: modify video values when video sampling is implemented
302 samples_fixed_items = _collect_mm_samples(
303 ds,
304 hf_tokenizer,
305 num_requests=5,
306 base_items_per_request=3,
307 num_mm_items_range_ratio=0.0,
308 limit_mm_per_prompt={"image": 3, "video": 0},
309 bucket_config={(32, 32, 1): 1.0},
310 )
311 # Must have 5 requests each with 3 mm items per prompt
312 assert len(samples_fixed_items) == 5
313 for s in samples_fixed_items:
314 mm_data = cast(list[dict[str, Any]], s.multi_modal_data)
315 assert len(mm_data) == 3
316 for it in mm_data:
317 assert it.get("type") == "image_url"
318
319
320 @pytest.mark.benchmark
321 def test_random_mm_bucket_config_not_mutated(
322 hf_tokenizer: PreTrainedTokenizerBase,
323 ) -> None:
324 ds = RandomMultiModalDataset(random_seed=0)
325 # This bucket config is not normalized to sum to 1
326 # and has more buckets than requested images
327 original = {(32, 32, 1): 0.2, (52, 64, 1): 6, (25, 64, 1): 3}
328 # Keep a snapshot to compare after sampling
329 snapshot = dict(original)
330
331 _ = _collect_mm_samples(
332 ds,
333 hf_tokenizer,
334 num_requests=4,
335 base_items_per_request=1,
336 num_mm_items_range_ratio=0.0,
337 limit_mm_per_prompt={"image": 1, "video": 0},
338 bucket_config=original,
339 )
340
341 # Ensure the original dict content is unchanged
342 assert original == snapshot
343
344 # Vary number of mm items per prompt
345 # set num_mm_items_range_ratio to 0.5
346 samples_varying_items = _collect_mm_samples(
347 ds,
348 hf_tokenizer,
349 num_requests=5,
350 base_items_per_request=2,
351 num_mm_items_range_ratio=0.5,
352 limit_mm_per_prompt={"image": 4, "video": 0},
353 bucket_config={(32, 32, 1): 1.0},
354 )
355 # Must have 5 requests each with less than 4 mm items per prompt
356 # but at least 1 mm item per prompt
357 assert len(samples_varying_items) == 5
358 for s in samples_varying_items:
359 mm_data = cast(list[dict[str, Any]], s.multi_modal_data)
360 assert len(mm_data) <= 4
361 assert len(mm_data) >= 1
362 for it in mm_data:
363 assert it.get("type") == "image_url"
364
365
366 @pytest.mark.benchmark
367 def test_random_mm_video_sampling(hf_tokenizer: PreTrainedTokenizerBase) -> None:
368 """Test video sampling functionality in RandomMultiModalDataset."""
369 ds = RandomMultiModalDataset(random_seed=42)
370
371 # Test with video bucket configuration
372 bucket_config = {
373 (64, 64, 1): 0.3, # Images
374 (64, 64, 8): 0.7, # Videos
375 }
376
377 limit_mm_per_prompt = {"image": 2, "video": 2}
378
379 samples = _collect_mm_samples(
380 ds,
381 hf_tokenizer,
382 num_requests=5,
383 base_items_per_request=1,
384 num_mm_items_range_ratio=0.0,
385 limit_mm_per_prompt=limit_mm_per_prompt,
386 bucket_config=bucket_config,
387 )
388
389 assert len(samples) == 5
390
391 # Check that we have both images and videos
392 video_count = 0
393 image_count = 0
394
395 for s in samples:
396 mm_data = cast(list[dict[str, Any]], s.multi_modal_data)
397 assert len(mm_data) == 1
398
399 item = mm_data[0]
400 if item.get("type") == "video_url":
401 video_count += 1
402 # Verify video URL format
403 url = item.get("video_url", {}).get("url", "")
404 assert url.startswith("data:video/mp4;base64,")
405 elif item.get("type") == "image_url":
406 image_count += 1
407 # Verify image URL format
408 url = item.get("image_url", {}).get("url", "")
409 assert url.startswith("data:image/jpeg;base64,")
410
411 # Should have some videos due to 0.7 probability
412 assert video_count > 0
413 assert image_count > 0
414
415
416 @pytest.mark.benchmark
417 def test_random_mm_video_only_sampling(hf_tokenizer: PreTrainedTokenizerBase) -> None:
418 """Test sampling with only video buckets."""
419 ds = RandomMultiModalDataset(random_seed=42)
420
421 bucket_config = {
422 (64, 64, 8): 1.0, # Only videos
423 }
424
425 limit_mm_per_prompt = {"image": 0, "video": 1}
426
427 samples = _collect_mm_samples(
428 ds,
429 hf_tokenizer,
430 num_requests=3,
431 base_items_per_request=1,
432 num_mm_items_range_ratio=0.0,
433 limit_mm_per_prompt=limit_mm_per_prompt,
434 bucket_config=bucket_config,
435 )
436
437 assert len(samples) == 3
438
439 for s in samples:
440 mm_data = cast(list[dict[str, Any]], s.multi_modal_data)
441 assert len(mm_data) == 1
442
443 item = mm_data[0]
444 assert item.get("type") == "video_url"
445 url = item.get("video_url", {}).get("url", "")
446 assert url.startswith("data:video/mp4;base64,")
447
448
449 @pytest.mark.benchmark
450 def test_random_mm_video_deterministic_sampling(
451 hf_tokenizer: PreTrainedTokenizerBase,
452 ) -> None:
453 """Test that video sampling is deterministic with same seed."""
454 seed = 123
455 ds_a = RandomMultiModalDataset(random_seed=seed)
456 ds_b = RandomMultiModalDataset(random_seed=seed)
457
458 bucket_config = {
459 (64, 64, 8): 1.0, # Only videos
460 }
461
462 limit_mm_per_prompt = {"image": 0, "video": 1}
463
464 a = _collect_mm_samples(
465 ds_a,
466 hf_tokenizer,
467 num_requests=3,
468 base_items_per_request=1,
469 num_mm_items_range_ratio=0.0,
470 limit_mm_per_prompt=limit_mm_per_prompt,
471 bucket_config=bucket_config,
472 )
473
474 b = _collect_mm_samples(
475 ds_b,
476 hf_tokenizer,
477 num_requests=3,
478 base_items_per_request=1,
479 num_mm_items_range_ratio=0.0,
480 limit_mm_per_prompt=limit_mm_per_prompt,
481 bucket_config=bucket_config,
482 )
483
484 fa = [_mm_fingerprint_sample(s) for s in a]
485 fb = [_mm_fingerprint_sample(s) for s in b]
486 assert fa == fb