main
py 4,776 lines 178 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """This module defines a framework for sampling benchmark requests from various
4 datasets. Each dataset subclass of BenchmarkDataset must implement sample
5 generation. Supported dataset types include:
6 - ShareGPT
7 - Random (synthetic)
8 - Sonnet
9 - BurstGPT
10 - HuggingFace
11 - VisionArena
12 """
13
14 import argparse
15 import ast
16 import io
17 import json
18 import logging
19 import math
20 import random
21 from abc import ABC, abstractmethod
22 from collections.abc import Callable, Iterator, Mapping
23 from contextlib import suppress
24 from dataclasses import dataclass, replace
25 from functools import cache
26 from io import BytesIO
27 from pathlib import Path
28 from tempfile import TemporaryDirectory
29 from typing import Any, cast
30
31 import numpy as np
32 import pybase64 as base64
33 from PIL import Image
34 from typing_extensions import deprecated
35
36 from vllm.benchmarks.datasets.utils import (
37 RangeRatio,
38 _resolve_range_ratios,
39 get_sampling_params,
40 )
41 from vllm.inputs import MultiModalDataDict
42 from vllm.lora.request import LoRARequest
43 from vllm.lora.utils import get_adapter_absolute_path
44 from vllm.multimodal.audio import get_audio_duration
45 from vllm.multimodal.image import convert_image_mode
46 from vllm.multimodal.utils import encode_image_url, fetch_image
47 from vllm.tokenizers import TokenizerLike
48 from vllm.transformers_utils.repo_utils import hf_api
49 from vllm.utils.argparse_utils import FlexibleArgumentParser
50 from vllm.utils.import_utils import PlaceholderModule
51 from vllm.utils.mistral import is_mistral_tokenizer
52
53 try:
54 from datasets import load_dataset
55 except ImportError:
56 datasets = PlaceholderModule("datasets")
57 load_dataset = datasets.placeholder_attr("load_dataset")
58
59 try:
60 import pandas as pd
61 except ImportError:
62 pd = PlaceholderModule("pandas")
63
64 try:
65 import soundfile as sf
66 except ImportError:
67 sf = PlaceholderModule("soundfile")
68
69
70 logger = logging.getLogger(__name__)
71
72 DEFAULT_NUM_PROMPTS = 1000
73
74
75 @dataclass
76 class SampleRequest:
77 """Represents a single inference request for benchmarking."""
78
79 prompt: str | list[str] | list[int] | list[list[int]] | list[dict]
80 prompt_len: int
81 expected_output_len: int = 0
82 multi_modal_data: MultiModalDataDict | dict | list[dict] | None = None
83 lora_request: LoRARequest | None = None
84 request_id: str | None = None
85 timestamp: float | None = None
86 # Pre-built chat messages. When set, the chat backend uses this list
87 # directly and skips constructing messages from `prompt` + multimodal
88 # content. Mutually exclusive with the `prompt`-based path.
89 chat_messages: list[dict[str, Any]] | None = None
90 # Per-request fields merged into the request body (e.g. tools,
91 # tool_choice, response_format). Shallow-merged with --extra-body at
92 # dispatch time; per-request keys win.
93 request_overrides: dict | None = None
94
95
96 # -----------------------------------------------------------------------------
97 # Benchmark Dataset Base Class
98 # -----------------------------------------------------------------------------
99
100
101 class BenchmarkDataset(ABC):
102 DEFAULT_SEED = 0
103 IS_MULTIMODAL = False
104
105 def __init__(
106 self,
107 dataset_path: str | None = None,
108 random_seed: int = DEFAULT_SEED,
109 disable_shuffle: bool = False,
110 **kwargs,
111 ) -> None:
112 """Initialize the BenchmarkDataset with an optional dataset path and random
113 seed.
114
115 Args:
116 dataset_path (Optional[str]): Path to the dataset. If None, it
117 indicates that a default or random dataset might be used.
118 random_seed (int): Seed value for reproducible shuffling or
119 sampling. Defaults to DEFAULT_SEED.
120 disable_shuffle (bool): If True, keep the dataset in its original
121 order instead of shuffling it.
122
123 """
124 self.dataset_path = dataset_path
125 # Set the random seed, ensuring that a None value is replaced with the
126 # default seed.
127 self.random_seed = random_seed if random_seed is not None else self.DEFAULT_SEED
128 self.disable_shuffle = disable_shuffle
129 self.data: Any | None = None
130
131 def apply_multimodal_chat_transformation(
132 self,
133 prompt: str,
134 mm_content: MultiModalDataDict | dict | list[dict] | None = None,
135 ) -> list[dict]:
136 """Transform a prompt and optional multimodal content into a chat format.
137 This method is used for chat models that expect a specific conversation
138 format.
139 """
140 content = [{"text": prompt, "type": "text"}]
141 if mm_content is not None:
142 if isinstance(mm_content, list):
143 content.extend(cast(list[dict[str, Any]], mm_content))
144 elif isinstance(mm_content, dict):
145 content.append(mm_content)
146 else:
147 raise TypeError(
148 f"Could not process multimodal content of type: {type(mm_content)}"
149 )
150 return [{"role": "user", "content": content}]
151
152 def load_data(self) -> None:
153 """Load data from the dataset path into self.data.
154
155 This method must be overridden by subclasses since the method to load
156 data will vary depending on the dataset format and source.
157
158 Raises:
159 NotImplementedError: If a subclass does not implement this method.
160
161 """
162 # TODO (jenniferzhao): add support for downloading data
163 raise NotImplementedError("load_data must be implemented in subclasses.")
164
165 @staticmethod
166 def get_random_lora_request(
167 max_loras: int | None = None,
168 lora_path: str | None = None,
169 ) -> LoRARequest | None:
170 """Optionally select a random LoRA request.
171
172 This method is used when LoRA parameters are provided. It randomly
173 selects a LoRA based on max_loras.
174
175 Args:
176 max_loras (Optional[int]): The maximum number of LoRAs available.
177 If `None`, LoRA is not used.
178 lora_path (Optional[str]): Path to the LoRA parameters on disk.
179 If `None`, LoRA is not used.
180
181 Returns:
182 A new [`LoRARequest`][vllm.lora.request.LoRARequest]
183 (or `None` if not applicable).
184
185 """
186 if max_loras is None or lora_path is None:
187 return None
188
189 # Generate a random LoRA ID in the range [1, max_loras].
190 lora_id = random.randint(1, max_loras)
191 lora_request = LoRARequest(
192 lora_name=str(lora_id),
193 lora_int_id=lora_id,
194 lora_path=lora_path_on_disk(lora_path),
195 )
196 return lora_request
197
198 @staticmethod
199 def get_round_robin_lora_request(
200 index: int,
201 max_loras: int | None = None,
202 lora_path: str | None = None,
203 ) -> LoRARequest | None:
204 """Optionally select a LoRA request using deterministic round-robin.
205
206 This method cycles through LoRA IDs in order based on the request
207 index, providing reproducible LoRA assignment.
208
209 Args:
210 index (int): The request index used for round-robin selection.
211 max_loras (Optional[int]): The maximum number of LoRAs available.
212 If `None`, LoRA is not used.
213 lora_path (Optional[str]): Path to the LoRA parameters on disk.
214 If `None`, LoRA is not used.
215
216 Returns:
217 A new [`LoRARequest`][vllm.lora.request.LoRARequest]
218 (or `None` if not applicable).
219
220 """
221 if max_loras is None or lora_path is None:
222 return None
223
224 # Deterministic round-robin: cycle through [1, max_loras]
225 lora_id = index % max_loras + 1
226 lora_request = LoRARequest(
227 lora_name=str(lora_id),
228 lora_int_id=lora_id,
229 lora_path=lora_path_on_disk(lora_path),
230 )
231 return lora_request
232
233 @staticmethod
234 def get_lora_request(
235 index: int,
236 max_loras: int | None = None,
237 lora_path: str | None = None,
238 lora_assignment: str = "random",
239 ) -> LoRARequest | None:
240 """Select a LoRA request using the specified assignment strategy.
241
242 Args:
243 index (int): The request index (used for round-robin).
244 max_loras (Optional[int]): The maximum number of LoRAs available.
245 lora_path (Optional[str]): Path to the LoRA parameters on disk.
246 lora_assignment (str): Strategy for LoRA selection.
247 'random' (default) or 'round-robin'.
248
249 Returns:
250 A new [`LoRARequest`][vllm.lora.request.LoRARequest]
251 (or `None` if not applicable).
252
253 """
254 if lora_assignment == "round-robin":
255 return BenchmarkDataset.get_round_robin_lora_request(
256 index=index, max_loras=max_loras, lora_path=lora_path
257 )
258 return BenchmarkDataset.get_random_lora_request(
259 max_loras=max_loras, lora_path=lora_path
260 )
261
262 @abstractmethod
263 def sample(
264 self,
265 tokenizer: TokenizerLike,
266 num_requests: int,
267 request_id_prefix: str = "",
268 no_oversample: bool = False,
269 **kwargs,
270 ) -> list[SampleRequest]:
271 """Abstract method to generate sample requests from the dataset.
272
273 Subclasses must override this method to implement dataset-specific logic
274 for generating a list of SampleRequest objects.
275
276 Args:
277 tokenizer (TokenizerLike): The tokenizer to be used
278 for processing the dataset's text.
279 num_requests (int): The number of sample requests to generate.
280 request_id_prefix (str): The prefix of request_id.
281 no_oversample (bool): If True, do not oversample the dataset to
282 reach num_requests.
283
284 Returns:
285 list[SampleRequest]: A list of sample requests generated from the
286 dataset.
287
288 """
289 raise NotImplementedError("sample must be implemented in subclasses.")
290
291 def maybe_oversample_requests(
292 self,
293 requests: list[SampleRequest],
294 num_requests: int,
295 request_id_prefix: str = "",
296 no_oversample: bool = False,
297 ) -> None:
298 """Oversamples the list of requests if its size is less than the desired
299 number.
300
301 Args:
302 requests (List[SampleRequest]): The current list of sampled
303 requests.
304 num_requests (int): The target number of requests.
305 request_id_prefix (str): The prefix applied to generated request
306 identifiers.
307 no_oversample (bool): If True, return the requests unchanged
308 instead of oversampling up to num_requests.
309
310 """
311 if no_oversample:
312 logger.info("Skipping oversampling. Total samples: %d.", len(requests))
313 return
314
315 if len(requests) < num_requests:
316 random.seed(self.random_seed)
317 needed = num_requests - len(requests)
318 additional = []
319 for i in range(needed):
320 req = replace(
321 random.choice(requests),
322 request_id=request_id_prefix + str(len(requests) + i),
323 )
324 additional.append(req)
325 requests.extend(additional)
326 logger.info("Oversampled requests to reach %d total samples.", num_requests)
327
328 ids = [req.request_id for req in requests]
329 if len(ids) != len(set(ids)):
330 raise ValueError(
331 "Duplicate request_id found in the sampled "
332 "requests. Please ensure that each request_id "
333 "is unique."
334 )
335
336
337 # -----------------------------------------------------------------------------
338 # Utility Functions and Global Caches
339 # -----------------------------------------------------------------------------
340
341
342 def is_valid_sequence(
343 prompt_len: int,
344 output_len: int,
345 min_len: int = 4,
346 max_prompt_len: int = 1024,
347 max_total_len: int = 2048,
348 skip_min_output_len_check: bool = False,
349 ) -> bool:
350 """Validate a sequence based on prompt and output lengths.
351
352 Default pruning criteria are copied from the original `sample_hf_requests`
353 and `sample_sharegpt_requests` functions in benchmark_serving.py, as well as
354 from `sample_requests` in benchmark_throughput.py.
355 """
356 # Check for invalid conditions
357 prompt_too_short = prompt_len < min_len
358 output_too_short = (not skip_min_output_len_check) and (output_len < min_len)
359 prompt_too_long = prompt_len > max_prompt_len
360 combined_too_long = (prompt_len + output_len) > max_total_len
361
362 # Return True if none of the invalid conditions are met
363 return not (
364 prompt_too_short or output_too_short or prompt_too_long or combined_too_long
365 )
366
367
368 @cache
369 def lora_path_on_disk(lora_path: str) -> str:
370 return get_adapter_absolute_path(lora_path)
371
372
373 # Global cache for LoRA tokenizers.
374 lora_tokenizer_cache: dict[int, TokenizerLike] = {}
375
376
377 def process_image(
378 image: Any,
379 *,
380 ensure_client_side_data: bool = False,
381 ) -> Mapping[str, Any]:
382 """Process a single image input and return a multimedia content dictionary.
383
384 Supports the following input types:
385
386 1. Dictionary with raw image bytes: - Expects a dict with a 'bytes' key
387 containing raw image data. - Loads the bytes as a PIL.Image.Image.
388
389 2. PIL.Image.Image input: - Converts the image to RGB. - Saves the image as
390 a JPEG in memory. - Encodes the JPEG data as a base64 string. - Returns
391 a dictionary with the image as a base64 data URL.
392
393 3. String input: - Treats the string as a URL, local file path, or base64
394 encoded data. - If string starts with "data:image/", treats as base64.
395 - If string starts with "http://", "https://", or "file://", treats as URL.
396 - Otherwise treats as local file path and prepends "file://".
397 - If ensure_client_side_data is True, local and HTTP(S) image references
398 are loaded and encoded as base64 image data URLs. Existing data:image
399 URLs are kept unchanged.
400 - Returns a dictionary with the image URL or base64 data.
401
402 Raises:
403 ValueError: If the input is not a supported type.
404
405 """
406 if isinstance(image, dict) and "bytes" in image:
407 image = Image.open(BytesIO(image["bytes"]))
408 if isinstance(image, Image.Image):
409 image = convert_image_mode(image, "RGB")
410 with io.BytesIO() as image_data:
411 image.save(image_data, format="JPEG")
412 image_base64 = base64.b64encode(image_data.getvalue()).decode("utf-8")
413 return {
414 "type": "image_url",
415 "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"},
416 }
417
418 if isinstance(image, str):
419 image_url = (
420 image
421 if image.startswith(("http://", "https://", "file://", "data:image/"))
422 else f"file://{image}"
423 )
424
425 if ensure_client_side_data and not image_url.startswith("data:image/"):
426 try:
427 fetched_image = fetch_image(image_url)
428 image_url = encode_image_url(fetched_image)
429 except Exception as e:
430 raise ValueError(f"Invalid image URL: {image_url}") from e
431 return {"type": "image_url", "image_url": {"url": image_url}}
432
433 raise ValueError(
434 f"Invalid image input {image}. Must be a PIL.Image.Image, "
435 "str (URL, file path, or base64 data URL), or dictionary with raw image bytes."
436 )
437
438
439 def process_video(video: Any) -> Mapping[str, Any]:
440 """Process a single video input and return a multimedia content dictionary.
441
442 Supports the following input types:
443
444 1. Dictionary with raw video bytes: - Expects a dict with a 'bytes' key
445 containing raw video data.
446
447 2. String input: - Treats the string as a URL or local file path. -
448 Prepends "file://" if the string doesn't start with "http://" or
449 "file://". - Returns a dictionary with the image URL.
450
451 Raises:
452 ValueError: If the input is not a supported type.
453
454 """
455 if isinstance(video, dict) and "bytes" in video:
456 video_bytes = video["bytes"]
457 video_base64 = base64.b64encode(video_bytes).decode("utf-8")
458 return {
459 "type": "video_url",
460 "video_url": {"url": f"data:video/mp4;base64,{video_base64}"},
461 }
462
463 if isinstance(video, str):
464 video_url = (
465 video
466 if video.startswith(("http://", "https://", "file://"))
467 else f"file://{video}"
468 )
469 return {"type": "video_url", "video_url": {"url": video_url}}
470
471 raise ValueError(
472 f"Invalid video input {video}. Must be a string of local path/remote url, or a dictionary with raw video bytes in the form of `{{'bytes': raw_video_bytes}}`." # noqa: E501
473 )
474
475
476 def process_audio(audio: Any) -> tuple:
477 """Process a single audio input and return a (array, sample_rate) tuple.
478
479 Supports:
480 1. String: treated as a file path, loaded with soundfile.
481 2. Dict with 'array' and 'sampling_rate' keys: HuggingFace audio format.
482 3. Tuple (array, sr): passed through directly.
483 """
484 if isinstance(audio, str):
485 return sf.read(audio)
486 if isinstance(audio, dict) and "array" in audio and "sampling_rate" in audio:
487 return audio["array"], audio["sampling_rate"]
488 if isinstance(audio, tuple) and len(audio) == 2:
489 return audio
490 raise ValueError(
491 f"Invalid audio input {audio}. Must be a file path string, "
492 "a dict with 'array' and 'sampling_rate', or a (array, sr) tuple."
493 )
494
495
496 def gen_prompt_decode_to_target_len(
497 tokenizer: TokenizerLike,
498 token_sequence: list[int],
499 target_token_len: int,
500 max_retry: int = 10,
501 add_special_tokens: bool = False,
502 rng: np.random.Generator | None = None,
503 ) -> tuple[str, list[int], int]:
504 """Ensure decoded-then-encoded prompt length matches the target token length.
505
506 This function decodes an initial token sequence to text and re-encodes it
507 , iteratively adjusting the token sequence length to match a target.
508 This is necessary because some tokenizers do not guarantee a 1:1 mapping
509 between consecutive tokens and the decoded-then-encoded sequence length.
510 For example, for GPT2Tokenizer:
511 [6880, 6881] -> ['Ġcalls', 'here'] ->
512 [1650, 939, 486] -> ['Ġcall', 'sh', 'ere']
513
514 Returns a tuple of the final prompt string, the adjusted token sequence,
515 and the token mismatch (final_len - target_token_len) if the retry budget
516 is exhausted.
517 """
518 remain_num_try = max_retry
519 token_mismatch = 0
520 while True:
521 prompt = tokenizer.decode(token_sequence)
522 token_sequence = tokenizer.encode(prompt, add_special_tokens=add_special_tokens)
523 if remain_num_try <= 0:
524 if len(token_sequence) != target_token_len:
525 token_mismatch = len(token_sequence) - target_token_len
526 break
527
528 if len(token_sequence) == target_token_len:
529 break
530 elif len(token_sequence) < target_token_len:
531 if rng is not None:
532 extra_tokens = rng.integers(
533 0,
534 tokenizer.vocab_size,
535 size=target_token_len - len(token_sequence),
536 ).tolist()
537 else:
538 extra_tokens = np.random.randint(
539 0,
540 tokenizer.vocab_size,
541 size=target_token_len - len(token_sequence),
542 ).tolist()
543 token_sequence.extend(extra_tokens)
544 elif len(token_sequence) > target_token_len:
545 token_sequence = token_sequence[:target_token_len]
546
547 remain_num_try -= 1
548
549 return prompt, token_sequence, token_mismatch
550
551
552 # -----------------------------------------------------------------------------
553 # Random Dataset Implementation (Synthetic Data)
554 # -----------------------------------------------------------------------------
555
556
557 class RandomDataset(BenchmarkDataset):
558 """Synthetic text-only dataset for serving/throughput benchmarks.
559
560 Strategy:
561 - Sample input/output token lengths per request from integer-uniform ranges
562 around configured means (controlled by range_ratio).
563 - Prepend a fixed random prefix of length prefix_len.
564 - Generate the remaining tokens as a reproducible sequence:
565 (offset + index + arange(input_len)) % vocab_size.
566 - Decode then re-encode/truncate to ensure prompt token counts match.
567 - Uses numpy.default_rng seeded with random_seed for reproducible sampling.
568 """
569
570 # Default values copied from benchmark_serving.py for the random dataset.
571 DEFAULT_PREFIX_LEN = 0
572 DEFAULT_RANGE_RATIO = 0.0
573 DEFAULT_INPUT_LEN = 1024
574 DEFAULT_OUTPUT_LEN = 128
575
576 def __init__(self, **kwargs) -> None:
577 super().__init__(**kwargs)
578 # Use numpy's default_rng for deterministic sampling
579 # Do not use random.seed() or np.random.seed() elsewhere in this class.
580 # This ensures that the RNG is isolated from global RNG state.
581 self._rng = np.random.default_rng(self.random_seed)
582
583 def sample(
584 self,
585 tokenizer: TokenizerLike,
586 num_requests: int,
587 request_id_prefix: str = "",
588 no_oversample: bool = False,
589 prefix_len: int = DEFAULT_PREFIX_LEN,
590 range_ratio: RangeRatio = DEFAULT_RANGE_RATIO,
591 input_len: int = DEFAULT_INPUT_LEN,
592 output_len: int = DEFAULT_OUTPUT_LEN,
593 batchsize: int = 1,
594 max_loras: int | None = None,
595 lora_path: str | None = None,
596 lora_assignment: str = "random",
597 **kwargs,
598 ) -> list[SampleRequest]:
599 resolved_input_rr, _ = _resolve_range_ratios(range_ratio)
600
601 num_special = int(tokenizer.num_special_tokens_to_add())
602 real_input_len = max(0, int(input_len) - num_special)
603 min_sampled_input = math.floor(
604 real_input_len * (1.0 - float(resolved_input_rr))
605 )
606 min_total_input = int(prefix_len) + min_sampled_input
607 if min_total_input < 1:
608 raise ValueError(
609 "--random-input-len is too small: with tokenizer special "
610 f"tokens {num_special} and "
611 f"input range ratio {resolved_input_rr}, "
612 "the minimum possible total input tokens (prefix + sampled) is "
613 f"{min_total_input}. Increase --random-input-len and/or "
614 "--random-prefix-len, or decrease the input range ratio "
615 "so that prefix_len + floor(max(0, random_input_len - "
616 "num_special)) * (1 - input_range_ratio) >= 1."
617 )
618
619 input_lens, output_lens, offsets = get_sampling_params(
620 self._rng,
621 num_requests,
622 range_ratio,
623 input_len,
624 output_len,
625 tokenizer,
626 )
627
628 vocab_size = tokenizer.vocab_size
629 prohibited_tokens = tokenizer.all_special_ids
630 all_tokens = np.arange(vocab_size)
631 allowed_tokens = np.array(list(set(all_tokens) - set(prohibited_tokens)))
632
633 # Generate prefix once
634 prefix_token_ids = self.get_prefix(tokenizer, allowed_tokens, prefix_len)
635
636 requests: list[SampleRequest] = []
637 token_mismatch_total = 0
638 for i in range(num_requests):
639 prompt, total_input_len, token_mismatch = self.generate_token_sequence( # noqa: E501
640 tokenizer=tokenizer,
641 prefix_token_ids=prefix_token_ids,
642 prefix_len=prefix_len,
643 vocab_size=vocab_size,
644 input_len=int(input_lens[i]),
645 offset=int(offsets[i]),
646 index=i,
647 allowed_tokens=allowed_tokens,
648 )
649 token_mismatch_total += token_mismatch
650 lora_req = self.get_lora_request(
651 index=i,
652 max_loras=max_loras,
653 lora_path=lora_path,
654 lora_assignment=lora_assignment,
655 )
656 requests.append(
657 SampleRequest(
658 prompt=prompt,
659 prompt_len=total_input_len,
660 expected_output_len=int(output_lens[i]),
661 lora_request=lora_req,
662 request_id=request_id_prefix + str(i),
663 )
664 )
665 # only used for embeddings benchmark.
666 if batchsize > 1:
667 batch_requests: list[SampleRequest] = []
668 # Create batched requests
669 for i in range(0, num_requests, batchsize):
670 batch = requests[i : i + batchsize]
671 batch_requests.append(
672 SampleRequest(
673 prompt=[req.prompt for req in batch], # type: ignore[arg-type]
674 prompt_len=sum(req.prompt_len for req in batch),
675 request_id=request_id_prefix + str(i // batchsize),
676 )
677 )
678 requests = batch_requests
679
680 if token_mismatch_total != 0:
681 sign = "more" if token_mismatch_total > 0 else "fewer"
682 logger.warning(
683 "Across all generated prompts, there were %d %s tokens "
684 "than expected after decoding and re-encoding. This is "
685 "expected due to the imperfect nature of the sampling "
686 "procedure.",
687 abs(token_mismatch_total),
688 sign,
689 )
690
691 return requests
692
693 def get_prefix(
694 self,
695 tokenizer: TokenizerLike,
696 allowed_tokens: np.ndarray,
697 prefix_len: int,
698 ) -> list[int]:
699 """Get the prefix for the dataset."""
700 if prefix_len <= 0:
701 return []
702
703 prefix_tokens = allowed_tokens[
704 self._rng.integers(0, len(allowed_tokens), size=prefix_len)
705 ].tolist()
706 _, adjusted_tokens, token_mismatch = gen_prompt_decode_to_target_len(
707 tokenizer=tokenizer,
708 token_sequence=prefix_tokens,
709 target_token_len=prefix_len,
710 add_special_tokens=False,
711 rng=self._rng,
712 )
713 if token_mismatch != 0:
714 sign = "more" if token_mismatch > 0 else "fewer"
715 logger.warning(
716 "Prefix tokenization produced %d %s tokens than expected "
717 "after decoding and re-encoding. This is expected due to "
718 "the imperfect nature of the sampling procedure",
719 abs(token_mismatch),
720 sign,
721 )
722 return adjusted_tokens
723
724 def generate_token_sequence(
725 self,
726 *,
727 tokenizer: TokenizerLike,
728 prefix_token_ids: list[int],
729 prefix_len: int,
730 vocab_size: int,
731 input_len: int,
732 offset: int,
733 index: int,
734 allowed_tokens: np.ndarray,
735 ) -> tuple[str, int, int]:
736 """Returns (prompt, total_input_len).
737
738 NOTE: After decoding the prompt we have to encode and decode it again.
739 This is done because in some cases N consecutive tokens
740 give a string tokenized into != N number of tokens.
741 For example for GPT2Tokenizer:
742 [6880, 6881] -> ['Ġcalls', 'here'] ->
743 [1650, 939, 486] -> ['Ġcall', 'sh', 'ere']
744 To avoid uncontrolled change of the prompt length,
745 the encoded sequence is truncated before being decoded again.
746 """
747 # Build the inner sequence by sampling
748 # sequentially from the allowed tokens
749 inner_seq = allowed_tokens[
750 (offset + index + np.arange(input_len)) % len(allowed_tokens)
751 ].tolist()
752 token_sequence = prefix_token_ids + inner_seq
753
754 # Decode, then re-encode and truncate to preserve token count invariants
755 total_input_len = prefix_len + int(input_len)
756 prompt, adjusted_token_sequence, token_mismatch = (
757 gen_prompt_decode_to_target_len(
758 tokenizer=tokenizer,
759 token_sequence=token_sequence,
760 target_token_len=total_input_len,
761 add_special_tokens=False,
762 rng=self._rng,
763 )
764 )
765 total_input_len = len(adjusted_token_sequence)
766 return prompt, total_input_len, token_mismatch
767
768
769 # -----------------------------------------------------------------------------
770 # Random Dataset Implementation (Synthetic Data)
771 # -----------------------------------------------------------------------------
772
773
774 class RandomDatasetForReranking(RandomDataset):
775 """Random dataset specialized for the needs of scoring:
776 - Batches of inputs
777 - Inputs composed of pairs
778 """
779
780 def __init__(self, **kwargs) -> None:
781 super().__init__(**kwargs)
782
783 def sample(
784 self,
785 tokenizer: TokenizerLike,
786 num_requests: int,
787 request_id_prefix: str = "",
788 no_oversample: bool = False,
789 prefix_len: int = RandomDataset.DEFAULT_PREFIX_LEN,
790 range_ratio: RangeRatio = RandomDataset.DEFAULT_RANGE_RATIO,
791 input_len: int = RandomDataset.DEFAULT_INPUT_LEN,
792 output_len: int = RandomDataset.DEFAULT_OUTPUT_LEN,
793 batchsize: int = 1,
794 max_loras: int | None = None,
795 lora_path: str | None = None,
796 lora_assignment: str = "random",
797 is_reranker: bool = True,
798 **kwargs,
799 ) -> list[SampleRequest]:
800 n_sep_tokens = int(is_reranker)
801
802 query_len_param = (input_len // 2) - n_sep_tokens if is_reranker else input_len
803
804 query_lens, _, query_offsets = get_sampling_params(
805 self._rng,
806 1,
807 range_ratio,
808 query_len_param,
809 0,
810 tokenizer,
811 )
812
813 query_len = int(query_lens[0])
814
815 if not is_reranker:
816 assert num_requests > 1 and batchsize > 1
817 num_requests -= 1
818 batchsize -= 1
819 doc_len_param = input_len
820 else:
821 doc_len_param = input_len - query_len - n_sep_tokens
822
823 doc_lens, _, doc_offsets = get_sampling_params(
824 self._rng,
825 num_requests,
826 range_ratio,
827 doc_len_param,
828 0,
829 tokenizer,
830 )
831
832 vocab_size = tokenizer.vocab_size
833 prohibited_tokens = tokenizer.all_special_ids
834 all_tokens = np.arange(vocab_size)
835 allowed_tokens = np.array(list(set(all_tokens) - set(prohibited_tokens)))
836
837 query_prompt, query_input_len, token_mismatch_total = (
838 self.generate_token_sequence(
839 tokenizer=tokenizer,
840 prefix_token_ids=[],
841 prefix_len=0,
842 vocab_size=vocab_size,
843 input_len=query_len,
844 offset=int(query_offsets[0]),
845 index=0,
846 allowed_tokens=allowed_tokens,
847 )
848 )
849
850 requests = []
851 for i in range(num_requests):
852 prompt, total_input_len, token_mismatch = self.generate_token_sequence( # noqa: E501
853 tokenizer=tokenizer,
854 prefix_token_ids=[],
855 prefix_len=0,
856 vocab_size=vocab_size,
857 input_len=int(doc_lens[i]),
858 offset=int(doc_offsets[i]),
859 index=i + 1,
860 allowed_tokens=allowed_tokens,
861 )
862 token_mismatch_total += token_mismatch
863 requests.append((prompt, total_input_len))
864
865 batch_requests = []
866 # Create batched requests
867 for i in range(0, num_requests, batchsize):
868 batch = requests[i : i + batchsize]
869 query_contrib = (
870 (query_input_len + n_sep_tokens) * len(batch)
871 if is_reranker
872 else query_input_len
873 )
874 batch_requests.append(
875 SampleRequest(
876 prompt=[query_prompt] + [req[0] for req in batch],
877 prompt_len=query_contrib + sum(req[1] for req in batch),
878 expected_output_len=0,
879 request_id=request_id_prefix + str(i // batchsize),
880 )
881 )
882
883 if token_mismatch_total != 0:
884 logger.warning(
885 "Across all generated prompts, there were %d %s tokens "
886 "than expected after decoding and re-encoding. This is "
887 "expected due to the imperfect nature of the sampling "
888 "procedure.",
889 abs(token_mismatch_total),
890 "more" if token_mismatch_total > 0 else "fewer",
891 )
892
893 return batch_requests
894
895
896 # -----------------------------------------------------------------------------
897 # MultiModalDataset Implementation
898 # -----------------------------------------------------------------------------
899
900
901 class RandomMultiModalDataset(RandomDataset):
902 """Synthetic multimodal dataset (text + images) that extends RandomDataset.
903
904 Status:
905 - Images: supported via synthetic RGB data.
906 - Video: supported via synthetic RGB data.
907 - Audio: not yet supported.
908
909 Sampling overview:
910 1) Number of items per request is sampled uniformly from the integer range
911 [floor(n·(1−r)), ceil(n·(1+r))], where n is the base count and r is
912 `num_mm_items_range_ratio` in [0, 1]. r=0 keeps it fixed; r=1 allows 0.
913 The maximum is further clamped to the sum of per-modality limits.
914 2) Each item’s modality and shape is sampled from `bucket_config`, a dict
915 mapping (height, width, num_frames) → probability. We treat
916 `num_frames`=1 as image and `num_frames` > 1 as video.
917 Entries with zero probability are removed and the rest are renormalized
918 to sum to 1.
919 3) Per-modality hard caps are enforced via `limit_mm_per_prompt`.
920 When a modality reaches its cap, all of its buckets are excluded and the
921 remaining probabilities are renormalized.
922
923 Example bucket configuration:
924 {(256, 256, 1): 0.5, (720, 1280, 1): 0.4, (720, 1280, 16): 0.1}
925 - Two image buckets (`num_frames`=1) and one video bucket
926 (`num_frames`=16).
927 OBS.: Only image sampling is supported for now.
928 """
929
930 IS_MULTIMODAL = True
931 DEFAULT_LIMIT_MM_PER_PROMPT = {"image": 255, "video": 1}
932
933 DEFAULT_BASE_ITEMS_PER_REQUEST = 1
934 DEFAULT_NUM_MM_ITEMS_RANGE_RATIO = 0.0
935 DEFAULT_MM_ITEM_BUCKET_CONFIG = {
936 (256, 256, 1): 0.5,
937 (720, 1280, 1): 0.5,
938 (720, 1280, 16): 0.0,
939 }
940 DEFAULT_ENABLE_MULTIMODAL_CHAT = False
941
942 def __init__(self, **kwargs) -> None:
943 super().__init__(**kwargs)
944
945 def generate_synthetic_image(self, width: int, height: int) -> Image.Image:
946 """Generate synthetic PIL image with random RGB values.
947
948 NOTE: iid pixel sampling results in worst-case compression
949 (good for stressing I/O), but very unlike real photos.
950 We could consider a “low-freq” mode (e.g., noise blur)
951 to emulate network realism instead of max stress.
952 """
953 random_pixels = self._rng.integers(
954 0,
955 256,
956 (height, width, 3),
957 dtype=np.uint8,
958 )
959 return Image.fromarray(random_pixels)
960
961 def generate_synthetic_video(
962 self, width: int, height: int, num_frames: int
963 ) -> dict:
964 """Generate synthetic video with random values.
965
966 Creates a video with random pixel values, encodes it to MP4 format,
967 and returns the content as bytes.
968 """
969 import cv2
970
971 random_pixels = self._rng.integers(
972 0,
973 256,
974 (num_frames, height, width, 3),
975 dtype=np.uint8,
976 )
977
978 fourcc = cv2.VideoWriter_fourcc(*"mp4v")
979 fps = 30 # frames per second
980
981 with TemporaryDirectory() as temp_dir:
982 temp_path = Path(temp_dir) / "video.mp4"
983
984 video_writer = cv2.VideoWriter(
985 str(temp_path), fourcc=fourcc, fps=fps, frameSize=(width, height)
986 )
987
988 try:
989 if not video_writer.isOpened():
990 raise RuntimeError("Failed to create video writer")
991
992 for frame in random_pixels:
993 video_writer.write(frame)
994 finally:
995 video_writer.release()
996
997 return {"bytes": temp_path.read_bytes()}
998
999 def map_config_to_modality(self, config: tuple[int, int, int]) -> str:
1000 """Map the configuration to the modality."""
1001 if config[-1] == 1:
1002 return "image"
1003 elif config[-1] > 1:
1004 return "video"
1005 else:
1006 raise ValueError(f"Invalid multimodal item configuration: {config}")
1007
1008 def normalize_bucket_config(
1009 self, bucket_config: dict[tuple[int, int, int], float]
1010 ) -> dict[tuple[int, int, int], float]:
1011 """Remove zero probability entries
1012 and normalize the bucket config to sum to 1.
1013 """
1014 # Raise error if value is negative
1015 if any(v < 0 for v in bucket_config.values()):
1016 raise ValueError("Bucket config values must be non-negative.")
1017 # Remove zero probability entries
1018 bucket_config = {k: v for k, v in bucket_config.items() if v > 0}
1019 # if bucket config is empty, raise error
1020 if not bucket_config:
1021 raise ValueError(
1022 "Got invalid bucket config. Bucket config values must be non-zero."
1023 )
1024 # Normalize the remaining bucket config to sum to 1
1025 total = sum(bucket_config.values())
1026 return {k: v / total for k, v in bucket_config.items()}
1027
1028 def generate_mm_item(
1029 self,
1030 mm_item_config: tuple[int, int, int],
1031 ) -> Mapping[str, Any]:
1032 """Create synthetic images and videos and
1033 apply process_image/process_video respectively.
1034 This follows the OpenAI API chat completions
1035 https://github.com/openai/openai-python
1036 """
1037 if self.map_config_to_modality(mm_item_config) == "image":
1038 return process_image(
1039 self.generate_synthetic_image(mm_item_config[1], mm_item_config[0])
1040 )
1041 elif self.map_config_to_modality(mm_item_config) == "video":
1042 return process_video(
1043 self.generate_synthetic_video(
1044 mm_item_config[1], mm_item_config[0], mm_item_config[2]
1045 )
1046 )
1047 else:
1048 raise ValueError(f"Invalid multimodal item configuration: {mm_item_config}")
1049
1050 def get_mm_item_sampling_params(
1051 self,
1052 base_items_per_request: int,
1053 num_mm_items_range_ratio: float,
1054 limit_mm_per_prompt: dict[str, int],
1055 bucket_config: dict[tuple[int, int, int], float],
1056 ) -> tuple[int, int, dict[str, int], dict[tuple[int, int, int], float]]:
1057 """Get the sampling parameters for the multimodal items."""
1058 # Enforce num_mm_items_range_ratio <= 1
1059 if not (0.0 <= num_mm_items_range_ratio <= 1.0):
1060 raise ValueError("num_mm_items_range_ratio must be in [0, 1].")
1061
1062 # Ensure modalities to sample are in limit_mm_per_prompt
1063 for k, v in bucket_config.items():
1064 # get modality from bucket config
1065 modality = self.map_config_to_modality(k)
1066 if modality not in limit_mm_per_prompt:
1067 raise ValueError(
1068 f"Modality {modality} is not in "
1069 f"limit_mm_per_prompt: "
1070 f"{limit_mm_per_prompt.keys()}"
1071 )
1072
1073 # Remove zero probability entries
1074 # and normalize bucket config to sum to 1
1075 bucket_config = self.normalize_bucket_config(bucket_config)
1076 logger.info(
1077 "Normalized bucket config: %s",
1078 bucket_config,
1079 )
1080 # Only consider limit per prompt for modalities in bucket config
1081 allowed_modalities = {self.map_config_to_modality(cfg) for cfg in bucket_config}
1082 limit_mm_per_prompt = {
1083 k: v for k, v in limit_mm_per_prompt.items() if k in allowed_modalities
1084 }
1085 if not limit_mm_per_prompt:
1086 raise ValueError("No valid limits for modalities present in bucket_config.")
1087
1088 logger.info(
1089 "Updated mm-limit-per-prompt: %s",
1090 limit_mm_per_prompt,
1091 )
1092
1093 # Get max and min num mm items and ensure
1094 # it is at most the sum of limit_mm_per_prompt for all modalities
1095 max_num_mm_items = min(
1096 sum(limit_mm_per_prompt.values()),
1097 math.ceil(base_items_per_request * (1 + num_mm_items_range_ratio)),
1098 )
1099 # Ensure min num mm items is at least 0
1100 min_num_mm_items = max(
1101 0, math.floor(base_items_per_request * (1 - num_mm_items_range_ratio))
1102 )
1103 # Raise error if min num mm items is greater than max num mm items
1104 if min_num_mm_items > max_num_mm_items:
1105 raise ValueError(
1106 f"Min num mm items is greater than max mm items: "
1107 f"{min_num_mm_items} > {max_num_mm_items}"
1108 )
1109
1110 logger.info(
1111 "Sampling number of multimodal items from [%s, %s]",
1112 min_num_mm_items,
1113 max_num_mm_items,
1114 )
1115
1116 return (
1117 min_num_mm_items,
1118 max_num_mm_items,
1119 limit_mm_per_prompt,
1120 bucket_config,
1121 )
1122
1123 def get_mm_item_iterator(
1124 self,
1125 min_num_mm_items: int,
1126 max_num_mm_items: int,
1127 bucket_config: dict[tuple[int, int, int], float],
1128 limit_mm_per_prompt: dict[str, int],
1129 ) -> Iterator[tuple[int, int, int]]:
1130 """Iterator over the multimodal items for each request
1131 whose size is between min_num_mm_items and max_num_mm_items.
1132
1133 Loop over the bucket config and sample a multimodal item.
1134 Loop until the number of multimodal items sampled is equal to
1135 request_num_mm_items or limit of multimodal items per prompt
1136 for all modalities is reached.
1137
1138 Note:
1139 - This function operates on a per-request shallow copy of
1140 `bucket_config` (tuple->float). The original dict passed to
1141 `sample` is not mutated. If this ever changes, a test
1142 is implemented and will fail.
1143
1144 """
1145 # Get the number of multimodal items to sample
1146 request_num_mm_items = int(
1147 self._rng.integers(min_num_mm_items, max_num_mm_items + 1)
1148 )
1149 # If request_num_mm_items is 0, yield an empty iterator
1150 if request_num_mm_items == 0:
1151 return
1152 # Initialize modality counters
1153 modality_counter = {self.map_config_to_modality(k): 0 for k in bucket_config}
1154 # Copy the bucket config to avoid modifying the original
1155 bucket_config_copy = bucket_config.copy()
1156 # Loop over the number of multimodal items to sample
1157 while sum(modality_counter.values()) < request_num_mm_items:
1158 # Sample a multimodal item config
1159 mm_item_config = self._rng.choice(
1160 list(bucket_config_copy.keys()), p=list(bucket_config_copy.values())
1161 )
1162 modality = self.map_config_to_modality(mm_item_config)
1163 # Check that modality count is less than limit per prompt
1164 if modality_counter[modality] < limit_mm_per_prompt[modality]:
1165 modality_counter[modality] += 1
1166 yield (mm_item_config)
1167 else:
1168 # If the counter is greater than the limit per prompt
1169 # set all multimodal items of this modality to 0
1170 for k, v in bucket_config_copy.items():
1171 if self.map_config_to_modality(k) == modality:
1172 bucket_config_copy[k] = 0
1173 # If all configs are 0, break the loop
1174 # This should not happen as request_num_mm_items is at most
1175 # the sum of limit_mm_per_prompt for all modalities
1176 if all(v == 0 for v in bucket_config_copy.values()):
1177 logger.warning(
1178 "Exhausted all multimodal items of modality %s", modality
1179 )
1180 break
1181 # Renormalize the bucket config
1182 bucket_config_copy = self.normalize_bucket_config(bucket_config_copy)
1183
1184 def sample(
1185 self,
1186 tokenizer: TokenizerLike,
1187 num_requests: int,
1188 request_id_prefix: str = "",
1189 no_oversample: bool = False,
1190 prefix_len: int = RandomDataset.DEFAULT_PREFIX_LEN,
1191 range_ratio: RangeRatio = RandomDataset.DEFAULT_RANGE_RATIO,
1192 input_len: int = RandomDataset.DEFAULT_INPUT_LEN,
1193 output_len: int = RandomDataset.DEFAULT_OUTPUT_LEN,
1194 batchsize: int = 1,
1195 max_loras: int | None = None,
1196 lora_path: str | None = None,
1197 lora_assignment: str = "random",
1198 limit_mm_per_prompt: dict[str, int] = DEFAULT_LIMIT_MM_PER_PROMPT,
1199 base_items_per_request: int = DEFAULT_BASE_ITEMS_PER_REQUEST,
1200 num_mm_items_range_ratio: float = DEFAULT_NUM_MM_ITEMS_RANGE_RATIO,
1201 bucket_config: dict[
1202 tuple[int, int, int], float
1203 ] = DEFAULT_MM_ITEM_BUCKET_CONFIG,
1204 enable_multimodal_chat: bool = DEFAULT_ENABLE_MULTIMODAL_CHAT,
1205 **kwargs,
1206 ) -> list[SampleRequest]:
1207 if batchsize != 1:
1208 raise NotImplementedError(
1209 "batchsize > 1 is not supported for RandomMultiModalDataset."
1210 )
1211
1212 input_lens, output_lens, offsets = get_sampling_params(
1213 self._rng,
1214 num_requests,
1215 range_ratio,
1216 input_len,
1217 output_len,
1218 tokenizer,
1219 )
1220
1221 (
1222 min_num_mm_items,
1223 max_num_mm_items,
1224 limit_mm_per_prompt,
1225 bucket_config,
1226 ) = self.get_mm_item_sampling_params(
1227 base_items_per_request,
1228 num_mm_items_range_ratio,
1229 limit_mm_per_prompt,
1230 bucket_config,
1231 )
1232
1233 vocab_size = tokenizer.vocab_size
1234 if is_mistral_tokenizer(tokenizer):
1235 prohibited_tokens = tokenizer.all_special_ids
1236 else:
1237 # Can't use tokenizer.all_special_ids since
1238 # it returns ONLY ids from special_tokens_map.json
1239 # We want to exclude placeholder tokens and all
1240 # tokens that indicate start/end of image as it
1241 # may break prompt replacement logic.
1242 assert hasattr(tokenizer, "added_tokens_decoder")
1243 prohibited_tokens = list(
1244 tok_id
1245 for tok_id, token in tokenizer.added_tokens_decoder.items()
1246 if token.special
1247 )
1248 all_tokens = np.arange(vocab_size)
1249 allowed_tokens = np.array(list(set(all_tokens) - set(prohibited_tokens)))
1250 logger.debug(
1251 "Sampling from %d out of %d (vocab size)", len(allowed_tokens), vocab_size
1252 )
1253 # Generate prefix once
1254 prefix_token_ids = self.get_prefix(tokenizer, allowed_tokens, prefix_len)
1255 # Add synthetic multimodal items to each request
1256 mm_requests = []
1257 token_mismatch_total = 0
1258 for i in range(num_requests):
1259 prompt, total_input_len, token_mismatch = self.generate_token_sequence( # noqa: E501
1260 tokenizer=tokenizer,
1261 prefix_token_ids=prefix_token_ids,
1262 prefix_len=prefix_len,
1263 vocab_size=vocab_size,
1264 input_len=int(input_lens[i]),
1265 offset=int(offsets[i]),
1266 index=i,
1267 allowed_tokens=allowed_tokens,
1268 )
1269 token_mismatch_total += token_mismatch
1270 # Get multimodal item iterator for a given request
1271 mm_item_iterator = self.get_mm_item_iterator(
1272 min_num_mm_items,
1273 max_num_mm_items,
1274 bucket_config,
1275 limit_mm_per_prompt,
1276 )
1277
1278 mm_content = cast(
1279 list[dict[str, Any]],
1280 [
1281 self.generate_mm_item(mm_item_config)
1282 for mm_item_config in mm_item_iterator
1283 ],
1284 )
1285
1286 if enable_multimodal_chat:
1287 # NOTE: For now this option is only provided for completeness
1288 # given that the serve.py benchmark currently does not use it.
1289 mm_chat_prompt: Any = prompt
1290 mm_chat_prompt = self.apply_multimodal_chat_transformation(
1291 prompt, mm_content
1292 )
1293 sample_request = SampleRequest(
1294 prompt=mm_chat_prompt,
1295 prompt_len=total_input_len,
1296 expected_output_len=int(output_lens[i]),
1297 multi_modal_data=None,
1298 request_id=request_id_prefix + str(i),
1299 )
1300 else:
1301 sample_request = SampleRequest(
1302 prompt=prompt,
1303 prompt_len=total_input_len,
1304 expected_output_len=int(output_lens[i]),
1305 multi_modal_data=mm_content,
1306 request_id=request_id_prefix + str(i),
1307 )
1308 mm_requests.append(sample_request)
1309
1310 if token_mismatch_total != 0:
1311 sign = "more" if token_mismatch_total > 0 else "fewer"
1312 logger.warning(
1313 "Across all generated prompts, there were %d %s tokens "
1314 "than expected after decoding and re-encoding. This is "
1315 "expected due to the imperfect nature of the sampling "
1316 "procedure.",
1317 abs(token_mismatch_total),
1318 sign,
1319 )
1320
1321 return mm_requests
1322
1323
1324 # -----------------------------------------------------------------------------
1325 # ShareGPT Dataset Implementation
1326 # -----------------------------------------------------------------------------
1327
1328
1329 class ShareGPTDataset(BenchmarkDataset):
1330 """Implements the ShareGPT dataset. Loads data from a JSON file and generates
1331 sample requests based on conversation turns.
1332 """
1333
1334 def __init__(self, **kwargs) -> None:
1335 super().__init__(**kwargs)
1336 self.load_data()
1337
1338 def load_data(self) -> None:
1339 if self.dataset_path is None:
1340 raise ValueError("dataset_path must be provided for loading data.")
1341
1342 with open(self.dataset_path, encoding="utf-8") as f:
1343 self.data = json.load(f)
1344 # Filter entries with at least two conversation turns.
1345 self.data = [
1346 entry
1347 for entry in self.data
1348 if "conversations" in entry and len(entry["conversations"]) >= 2
1349 ]
1350 random.seed(self.random_seed)
1351 if not getattr(self, "disable_shuffle", False):
1352 random.shuffle(self.data)
1353
1354 def sample(
1355 self,
1356 tokenizer: TokenizerLike,
1357 num_requests: int,
1358 request_id_prefix: str = "",
1359 no_oversample: bool = False,
1360 lora_path: str | None = None,
1361 max_loras: int | None = None,
1362 output_len: int | None = None,
1363 enable_multimodal_chat: bool = False,
1364 lora_assignment: str = "random",
1365 **kwargs,
1366 ) -> list[SampleRequest]:
1367 assert self.data is not None, "Dataset must be loaded before sampling"
1368 samples: list[SampleRequest] = []
1369 ind = 0
1370 for entry in self.data:
1371 if len(samples) >= num_requests:
1372 break
1373 prompt, completion = (
1374 entry["conversations"][0]["value"],
1375 entry["conversations"][1]["value"],
1376 )
1377
1378 lora_request = self.get_lora_request(
1379 index=ind,
1380 max_loras=max_loras,
1381 lora_path=lora_path,
1382 lora_assignment=lora_assignment,
1383 )
1384 prompt_ids = tokenizer(prompt).input_ids
1385 completion_ids = tokenizer(completion).input_ids
1386 prompt_len = len(prompt_ids)
1387 new_output_len = len(completion_ids) if output_len is None else output_len
1388 if not is_valid_sequence(
1389 prompt_len,
1390 new_output_len,
1391 skip_min_output_len_check=output_len is not None,
1392 ):
1393 continue
1394 if image_path := entry.get("image"):
1395 mm_content = process_image(image_path)
1396 elif video_path := entry.get("video"):
1397 mm_content = process_video(video_path)
1398 else:
1399 mm_content = None
1400 if enable_multimodal_chat:
1401 prompt = self.apply_multimodal_chat_transformation(prompt, mm_content)
1402 samples.append(
1403 SampleRequest(
1404 prompt=prompt,
1405 prompt_len=prompt_len,
1406 expected_output_len=new_output_len,
1407 lora_request=lora_request,
1408 multi_modal_data=mm_content,
1409 request_id=request_id_prefix + str(ind),
1410 )
1411 )
1412 ind += 1
1413 self.maybe_oversample_requests(
1414 samples, num_requests, request_id_prefix, no_oversample
1415 )
1416 return samples
1417
1418
1419 class TimedTrace(BenchmarkDataset):
1420 """Implements a base class to replay various timed traces.
1421 Loads data from a JSON file and generates sample requests
1422 based on the timing information in the traces.
1423 """
1424
1425 def __init__(self, **kwargs) -> None:
1426 super().__init__(**kwargs)
1427 random.seed(self.random_seed)
1428 np.random.seed(self.random_seed)
1429
1430 # Set parameters with defaults from timed_trace_group arguments
1431 self.chunk_size = int(kwargs.get("timed_trace_chunk_hash_size", 16))
1432 self.sec_multiplier = float(kwargs.get("timed_trace_sec_multiplier", 1))
1433 self.label_ts = str(kwargs.get("timed_trace_label_timestamp"))
1434 self.label_input_length = str(kwargs.get("timed_trace_label_input_length"))
1435 self.label_output_length = str(kwargs.get("timed_trace_label_output_length"))
1436 self.label_hash_ids = str(kwargs.get("timed_trace_label_hash_ids"))
1437 print(
1438 f"timed-trace: chunk_size: {self.chunk_size}, "
1439 f"sec_multiplier: {self.sec_multiplier}, "
1440 f'label_ts: "{self.label_ts}", '
1441 f'label_input_length: "{self.label_input_length}", '
1442 f'label_output_length: "{self.label_output_length}", '
1443 f'label_hash_ids: "{self.label_hash_ids}"'
1444 )
1445 self._expanded_generated_prompts: dict[str, Any] = {}
1446 self.load_data()
1447
1448 def load_data(self) -> None:
1449 # check if the file is there
1450 if self.dataset_path is None:
1451 raise ValueError("dataset_path must be provided for loading data.")
1452
1453 # load and we will do transformation once we have the Tokenizer available
1454 # this is jsonl data format
1455 with open(self.dataset_path) as f:
1456 self.data = f.readlines()
1457
1458 def _sample_token(
1459 self, num_tokens: int, tokenizer: TokenizerLike, seed: int | None = None
1460 ) -> list[int]:
1461 # Initialize vocab only if it doesn't exist yet
1462 if not hasattr(self, "vocab"):
1463 self.vocab = tokenizer.get_vocab()
1464 # Remove the special tokens.
1465 self.vocab = {
1466 k: v
1467 for k, v in self.vocab.items()
1468 if v not in tokenizer.all_special_ids
1469 }
1470 # Create a sorted list of vocab values for deterministic sampling
1471 self.vocab_values_sorted = sorted(self.vocab.values())
1472
1473 # Use the provided seed if given, otherwise use global random state
1474 if seed is not None:
1475 rng = random.Random(seed)
1476 sampled_token_ids = rng.choices(self.vocab_values_sorted, k=num_tokens)
1477 else:
1478 sampled_token_ids = random.choices(self.vocab_values_sorted, k=num_tokens)
1479
1480 return sampled_token_ids
1481
1482 def _expand_prompt(
1483 self,
1484 chunked_hashes: list[int],
1485 target_input_size: int,
1486 tokenizer: TokenizerLike,
1487 ) -> list[int]:
1488 raw_tokenized_prompt = []
1489 for h in chunked_hashes:
1490 # Calculate how many tokens to expand for this chunk
1491 expanded_size = (
1492 self.chunk_size
1493 if target_input_size >= self.chunk_size
1494 else target_input_size
1495 )
1496
1497 # Cache key includes size for partial chunks at the end
1498 key = f"{h}:{expanded_size}"
1499
1500 if key not in self._expanded_generated_prompts:
1501 # Convert key to a deterministic seed
1502 key_seed = hash(key) & 0xFFFFFFFF # Convert to 32-bit int
1503 self._expanded_generated_prompts[key] = self._sample_token(
1504 expanded_size, tokenizer, seed=key_seed
1505 )
1506 # once inserted get the tokenized prompt and append to the list
1507 raw_tokenized_prompt.extend(self._expanded_generated_prompts[key])
1508 target_input_size -= expanded_size
1509
1510 if target_input_size <= 0:
1511 break
1512
1513 return raw_tokenized_prompt
1514
1515 def sample(
1516 self,
1517 tokenizer: TokenizerLike,
1518 num_requests: int,
1519 request_id_prefix: str = "",
1520 no_oversample: bool = False,
1521 **kwargs,
1522 ) -> list[SampleRequest]:
1523 samples: list[SampleRequest] = []
1524 assert tokenizer is not None, "Tokenizer must be provided, now is Null"
1525 assert self.data is not None, "Data must be loaded before sampling"
1526
1527 for ind, entry in enumerate(self.data):
1528 if len(samples) >= num_requests:
1529 break
1530
1531 # now we create the SampleRequest with timing info
1532 entry = json.loads(entry.strip())
1533 input_length = entry.get(self.label_input_length)
1534 if input_length is None:
1535 raise ValueError(
1536 f"Input length field '{self.label_input_length}' "
1537 f"not found in trace entry. "
1538 f"Available fields: {list(entry.keys())}. "
1539 f"Use --label-input-length to specify the correct "
1540 f"field name."
1541 )
1542 new_output_len = entry.get(self.label_output_length)
1543 if new_output_len is None:
1544 raise ValueError(
1545 f"Output length field '{self.label_output_length}' "
1546 f"not found in trace entry. "
1547 f"Available fields: {list(entry.keys())}. "
1548 f"Use --label-output-length to specify the correct "
1549 f"field name."
1550 )
1551 prompt_ids = self._expand_prompt(
1552 entry.get(self.label_hash_ids, []), input_length, tokenizer
1553 )
1554
1555 # Get timestamp with proper error handling
1556 ts_value = entry.get(self.label_ts)
1557 if ts_value is None:
1558 raise ValueError(
1559 f"Timestamp field '{self.label_ts}' not found in trace entry. "
1560 f"Available fields: {list(entry.keys())}. "
1561 f"Use --label-timestamp to specify the correct field name."
1562 )
1563 timestamp = float(ts_value) * self.sec_multiplier
1564
1565 prompt_len = len(prompt_ids)
1566
1567 samples.append(
1568 SampleRequest(
1569 prompt=prompt_ids,
1570 prompt_len=prompt_len,
1571 expected_output_len=new_output_len,
1572 lora_request=None,
1573 multi_modal_data=None,
1574 request_id=request_id_prefix + str(ind),
1575 timestamp=timestamp,
1576 )
1577 )
1578 return samples
1579
1580
1581 def add_dataset_parser(parser: FlexibleArgumentParser):
1582 parser.add_argument(
1583 "--trust-remote-code",
1584 action="store_true",
1585 help="Trust remote code from huggingface",
1586 )
1587 parser.add_argument("--seed", type=int, default=0)
1588 parser.add_argument(
1589 "--num-prompts",
1590 type=int,
1591 default=DEFAULT_NUM_PROMPTS,
1592 help="Number of prompts to process.",
1593 )
1594 parser.add_argument(
1595 "--dataset-name",
1596 type=str,
1597 default="random",
1598 choices=[
1599 "sharegpt",
1600 "burstgpt",
1601 "sonnet",
1602 "random",
1603 "random-mm",
1604 "random-rerank",
1605 "hf",
1606 "custom",
1607 "custom_audio",
1608 "custom_image",
1609 "prefix_repetition",
1610 "spec_bench",
1611 "speed_bench",
1612 "timed_trace",
1613 ],
1614 help="Name of the dataset to benchmark on.",
1615 )
1616 parser.add_argument(
1617 "--no-stream",
1618 action="store_true",
1619 help="Do not load the dataset in streaming mode.",
1620 )
1621 parser.add_argument(
1622 "--dataset-path",
1623 type=str,
1624 default=None,
1625 help="Path to the sharegpt/sonnet dataset or the HF dataset ID if "
1626 "using HF dataset.",
1627 )
1628 parser.add_argument(
1629 "--no-oversample",
1630 action="store_true",
1631 help="Do not oversample if the dataset has fewer samples than num-prompts.",
1632 )
1633 parser.add_argument(
1634 "--skip-chat-template",
1635 action="store_true",
1636 help="Skip applying chat template to prompt for datasets that support it.",
1637 )
1638 parser.add_argument(
1639 "--enable-multimodal-chat",
1640 action="store_true",
1641 help="Enable multimodal chat transformation for datasets that support it.",
1642 )
1643 parser.add_argument(
1644 "--disable-shuffle",
1645 action="store_true",
1646 help="Disable shuffling of dataset samples for deterministic ordering.",
1647 )
1648
1649 # group for dataset specific arguments
1650 custom_group = parser.add_argument_group("custom dataset options")
1651 custom_group.add_argument(
1652 "--custom-output-len",
1653 type=int,
1654 default=256,
1655 help="Number of output tokens per request. Unless it is set to -1, the "
1656 "value overrides potential output length loaded from the dataset. It is "
1657 "used only for custom dataset.",
1658 )
1659 custom_group.add_argument(
1660 "--custom-ensure-client-side-data",
1661 action="store_true",
1662 help=(
1663 "Ensure custom dataset media is sent as client-side data instead "
1664 "of references. For custom_image datasets, this loads local and "
1665 "HTTP(S) images on the benchmark client and encodes them as "
1666 "base64 data URLs. Existing data:image URLs are kept unchanged."
1667 ),
1668 )
1669
1670 spec_bench_group = parser.add_argument_group("spec bench dataset options")
1671 spec_bench_group.add_argument(
1672 "--spec-bench-output-len",
1673 type=int,
1674 default=256,
1675 help="Num of output tokens per request, used only for spec bench dataset.",
1676 )
1677 spec_bench_group.add_argument(
1678 "--spec-bench-category",
1679 type=str,
1680 default=None,
1681 help="Category for spec bench dataset. If None, use all categories.",
1682 )
1683
1684 sonnet_group = parser.add_argument_group("sonnet dataset options")
1685 sonnet_group.add_argument(
1686 "--sonnet-input-len",
1687 type=int,
1688 default=550,
1689 help="Number of input tokens per request, used only for sonnet dataset.",
1690 )
1691 sonnet_group.add_argument(
1692 "--sonnet-output-len",
1693 type=int,
1694 default=150,
1695 help="Number of output tokens per request, used only for sonnet dataset.",
1696 )
1697 sonnet_group.add_argument(
1698 "--sonnet-prefix-len",
1699 type=int,
1700 default=200,
1701 help="Number of prefix tokens per request, used only for sonnet dataset.",
1702 )
1703
1704 sharegpt_group = parser.add_argument_group("sharegpt dataset options")
1705 sharegpt_group.add_argument(
1706 "--sharegpt-output-len",
1707 type=int,
1708 default=None,
1709 help="Output length for each request. Overrides the output length "
1710 "from the ShareGPT dataset.",
1711 )
1712
1713 timed_trace_group = parser.add_argument_group("timed-trace dataset options")
1714 timed_trace_group.add_argument(
1715 "--timed-trace-chunk-hash-size",
1716 type=int,
1717 default=16,
1718 help=(
1719 "Each hash tokens, if present, represent how many token "
1720 "hashes. For example in the Moonshot traces it is 512, while "
1721 "the Qwen/Alibaba has 16."
1722 ),
1723 )
1724 timed_trace_group.add_argument(
1725 "--timed-trace-sec-multiplier",
1726 type=float,
1727 default=1,
1728 help=(
1729 "What multiplier to use when converting timestamps to "
1730 "seconds. We will multiply timestamps by this. For example"
1731 "if the timestamps are in milliseconds, then pass 0.001."
1732 "If they are already in seconds, then the default 1 is sufficient."
1733 ),
1734 )
1735 timed_trace_group.add_argument(
1736 "--timed-trace-label-timestamp",
1737 type=str,
1738 default="timestamp",
1739 help="What json label to use to index the timestamp in the trace.",
1740 )
1741 timed_trace_group.add_argument(
1742 "--timed-trace-label-input-length",
1743 type=str,
1744 default="input_length",
1745 help=("What json label to use to index the input length field in the trace."),
1746 )
1747 timed_trace_group.add_argument(
1748 "--timed-trace-label-output-length",
1749 type=str,
1750 default="output_length",
1751 help=("What json label to use to index the output length field in the trace."),
1752 )
1753 timed_trace_group.add_argument(
1754 "--timed-trace-label-hash-ids",
1755 type=str,
1756 default="hash_ids",
1757 help=("What json label to use to index the hash ids for the input prompts."),
1758 )
1759
1760 blazedit_group = parser.add_argument_group("blazedit dataset options")
1761 blazedit_group.add_argument(
1762 "--blazedit-min-distance",
1763 type=float,
1764 default=0.0,
1765 help="Minimum distance for blazedit dataset. Min: 0, Max: 1.0",
1766 )
1767 blazedit_group.add_argument(
1768 "--blazedit-max-distance",
1769 type=float,
1770 default=1.0,
1771 help="Maximum distance for blazedit dataset. Min: 0, Max: 1.0",
1772 )
1773
1774 asr_group = parser.add_argument_group("asr dataset options")
1775 asr_group.add_argument(
1776 "--asr-max-audio-len-sec",
1777 type=float,
1778 default=float("inf"),
1779 help="Maximum audio length in seconds for ASR dataset.",
1780 )
1781 asr_group.add_argument(
1782 "--asr-min-audio-len-sec",
1783 type=float,
1784 default=0.0,
1785 help="Minimum audio length in seconds for ASR dataset.",
1786 )
1787
1788 random_group = parser.add_argument_group("random dataset options")
1789 add_random_dataset_base_args(random_group)
1790
1791 random_mm_group = parser.add_argument_group(
1792 "random multimodal dataset options extended from random dataset"
1793 )
1794 add_random_multimodal_dataset_args(random_mm_group)
1795
1796 hf_group = parser.add_argument_group("hf dataset options")
1797 hf_group.add_argument(
1798 "--hf-subset", type=str, default=None, help="Subset of the HF dataset."
1799 )
1800 hf_group.add_argument(
1801 "--hf-split", type=str, default=None, help="Split of the HF dataset."
1802 )
1803 hf_group.add_argument(
1804 "--hf-name",
1805 type=str,
1806 default=None,
1807 help=(
1808 "Name of the dataset on HuggingFace "
1809 "(e.g., 'lmarena-ai/VisionArena-Chat'). "
1810 "Specify this if your dataset-path is a local path."
1811 ),
1812 )
1813 hf_group.add_argument(
1814 "--hf-output-len",
1815 type=int,
1816 default=None,
1817 help="Output length for each request. Overrides the output lengths "
1818 "from the sampled HF dataset.",
1819 )
1820
1821 bfcl_group = parser.add_argument_group(
1822 "BFCL dataset options", description=BFCLDataset.__doc__
1823 )
1824 bfcl_group.add_argument(
1825 "--bfcl-categories",
1826 type=lambda s: [c.strip() for c in s.split(",") if c.strip()],
1827 default=None,
1828 help="Comma-separated list of BFCL v3 category names (without the "
1829 "'BFCL_v3_' prefix or '.json' suffix) to sample from, e.g. "
1830 "'simple,live_simple,multiple'. Defaults to "
1831 f"'{','.join(BFCLDataset.DEFAULT_CATEGORIES)}'.",
1832 )
1833
1834 prefix_repetition_group = parser.add_argument_group(
1835 "prefix repetition dataset options"
1836 )
1837 prefix_repetition_group.add_argument(
1838 "--prefix-repetition-prefix-len",
1839 type=int,
1840 default=256,
1841 help="Number of prefix tokens per request, used only for prefix "
1842 "repetition dataset.",
1843 )
1844 prefix_repetition_group.add_argument(
1845 "--prefix-repetition-suffix-len",
1846 type=int,
1847 default=256,
1848 help="Number of suffix tokens per request, used only for prefix "
1849 "repetition dataset. Total input length is prefix_len + suffix_len.",
1850 )
1851 prefix_repetition_group.add_argument(
1852 "--prefix-repetition-num-prefixes",
1853 type=int,
1854 default=10,
1855 help="Number of prefixes to generate, used only for prefix repetition "
1856 "dataset. Prompts per prefix is num_requests // num_prefixes.",
1857 )
1858 prefix_repetition_group.add_argument(
1859 "--prefix-repetition-output-len",
1860 type=int,
1861 default=128,
1862 help="Number of output tokens per request, used only for prefix "
1863 "repetition dataset.",
1864 )
1865
1866 speed_bench_group = parser.add_argument_group(
1867 "speed bench dataset options", description=SpeedBench.__doc__
1868 )
1869 speed_bench_group.add_argument(
1870 "--speed-bench-dataset-subset",
1871 type=str,
1872 default="qualitative",
1873 choices={
1874 "qualitative",
1875 "throughput_1k",
1876 "throughput_2k",
1877 "throughput_8k",
1878 "throughput_16k",
1879 "throughput_32k",
1880 },
1881 help="Subset of the SPEED-Bench dataset.",
1882 )
1883 speed_bench_group.add_argument(
1884 "--speed-bench-output-len",
1885 type=int,
1886 default=4096,
1887 help="Num of output tokens per request, used only for speed bench dataset.",
1888 )
1889 speed_bench_group.add_argument(
1890 "--speed-bench-category",
1891 type=str,
1892 default=None,
1893 help="Category for speed bench dataset. If None, use all categories.",
1894 )
1895
1896
1897 def add_random_dataset_base_args(
1898 parser_or_group: FlexibleArgumentParser | argparse._ArgumentGroup,
1899 ) -> None:
1900 """Add CLI arguments for base random dataset options.
1901
1902 This function adds arguments needed for:
1903 - random (random dataset)
1904 - random-mm (random multimodal dataset)
1905 - random-rerank (random dataset for reranking)
1906
1907 Args:
1908 parser_or_group: Either a parser or an argument group to add arguments to.
1909
1910 """
1911 parser_or_group.add_argument(
1912 "--random-input-len",
1913 type=int,
1914 default=1024,
1915 help="Number of input tokens per request, used only for random sampling.",
1916 )
1917 parser_or_group.add_argument(
1918 "--random-output-len",
1919 type=int,
1920 default=128,
1921 help="Number of output tokens per request, used only for random sampling.",
1922 )
1923 parser_or_group.add_argument(
1924 "--random-range-ratio",
1925 type=str,
1926 default="0.0",
1927 help="Range ratio for sampling input/output length, "
1928 "used only for random sampling. A single float applies to both "
1929 'ISL and OSL. A JSON dict like \'{"input": 0.3, "output": 0.5}\' '
1930 "sets them independently. Values must be in [0, 1).",
1931 )
1932 parser_or_group.add_argument(
1933 "--random-prefix-len",
1934 type=int,
1935 default=0,
1936 help=(
1937 "Number of fixed prefix tokens before the random context "
1938 "in a request. "
1939 "The total input length is the sum of `random-prefix-len` and "
1940 "a random "
1941 "context length sampled from [input_len * (1 - range_ratio), "
1942 "input_len * (1 + range_ratio)]."
1943 ),
1944 )
1945 parser_or_group.add_argument(
1946 "--random-batch-size",
1947 type=int,
1948 default=1,
1949 help=("Batch size for random sampling. Only used for embeddings benchmark."),
1950 )
1951 parser_or_group.add_argument(
1952 "--no-reranker",
1953 action="store_true",
1954 help=(
1955 "Whether the model supports reranking natively."
1956 " Only used for reranker benchmark."
1957 ),
1958 )
1959
1960
1961 def add_random_multimodal_dataset_args(
1962 parser_or_group: FlexibleArgumentParser | argparse._ArgumentGroup,
1963 ) -> None:
1964 """Add CLI arguments for random multimodal dataset options.
1965
1966 This function adds arguments needed for:
1967 - random-mm (random multimodal dataset)
1968
1969 Args:
1970 parser_or_group: Either a parser or an argument group to add arguments to.
1971
1972 """
1973 parser_or_group.add_argument(
1974 "--random-mm-base-items-per-request",
1975 type=int,
1976 default=RandomMultiModalDataset.DEFAULT_BASE_ITEMS_PER_REQUEST,
1977 help=(
1978 "Base number of multimodal items per request for random-mm. "
1979 "Actual per-request count is sampled around this base using "
1980 "--random-mm-num-mm-items-range-ratio."
1981 ),
1982 )
1983 parser_or_group.add_argument(
1984 "--random-mm-num-mm-items-range-ratio",
1985 type=float,
1986 default=RandomMultiModalDataset.DEFAULT_NUM_MM_ITEMS_RANGE_RATIO,
1987 help=(
1988 "Range ratio r in [0, 1] for sampling items per request. "
1989 "We sample uniformly from the closed integer range "
1990 "[floor(n*(1-r)), ceil(n*(1+r))] "
1991 "where n is the base items per request. "
1992 "r=0 keeps it fixed; r=1 allows 0 items. The maximum is clamped "
1993 "to the sum of per-modality limits from "
1994 "--random-mm-limit-mm-per-prompt. "
1995 "An error is raised if the computed min exceeds the max."
1996 ),
1997 )
1998 parser_or_group.add_argument(
1999 "--random-mm-limit-mm-per-prompt",
2000 type=json.loads,
2001 default=RandomMultiModalDataset.DEFAULT_LIMIT_MM_PER_PROMPT,
2002 help=(
2003 "Per-modality hard caps for items attached per request, e.g. "
2004 '\'{"image": 3, "video": 0}\'. The sampled per-request item '
2005 "count is clamped to the sum of these limits. When a modality "
2006 "reaches its cap, its buckets are excluded and probabilities are "
2007 "renormalized."
2008 "OBS.: Only image sampling is supported for now."
2009 ),
2010 )
2011
2012 def _parse_mm_bucket_config(v: object) -> dict[tuple[int, int, int], float]:
2013 # If already a dict (e.g., programmatic call), normalize keys
2014 def normalize(d: dict) -> dict[tuple[int, int, int], float]:
2015 out: dict[tuple[int, int, int], float] = {}
2016 for k, val in d.items():
2017 key = k
2018 if isinstance(key, str):
2019 with suppress(Exception):
2020 key = ast.literal_eval(key)
2021 if not (
2022 isinstance(key, tuple)
2023 and len(key) == 3
2024 and all(isinstance(x, int) for x in key)
2025 ):
2026 raise ValueError(
2027 f"Invalid bucket key {k!r}. Expected tuple (H, W, T)."
2028 )
2029 out[(int(key[0]), int(key[1]), int(key[2]))] = float(val)
2030 return out
2031
2032 if isinstance(v, dict):
2033 return normalize(v)
2034 if isinstance(v, str):
2035 # Python literal (supports tuple keys)
2036 parsed = ast.literal_eval(v)
2037 if not isinstance(parsed, dict):
2038 raise ValueError("Bucket config must parse to a dict.")
2039 return normalize(parsed)
2040 raise ValueError("Unsupported value for --random-mm-bucket-config.")
2041
2042 parser_or_group.add_argument(
2043 "--random-mm-bucket-config",
2044 type=_parse_mm_bucket_config,
2045 default=RandomMultiModalDataset.DEFAULT_MM_ITEM_BUCKET_CONFIG,
2046 help=(
2047 "The bucket config is a dictionary mapping a multimodal item"
2048 "sampling configuration to a probability."
2049 "Currently allows for 2 modalities: images and videos. "
2050 "An bucket key is a tuple of (height, width, num_frames)"
2051 "The value is the probability of sampling that specific item. "
2052 "Example: "
2053 "--random-mm-bucket-config "
2054 "{(256, 256, 1): 0.5, (720, 1280, 1): 0.4, (720, 1280, 16): 0.10} "
2055 "First item: images with resolution 256x256 w.p. 0.5"
2056 "Second item: images with resolution 720x1280 w.p. 0.4 "
2057 "Third item: videos with resolution 720x1280 and 16 frames w.p. 0.1"
2058 "OBS.: If the probabilities do not sum to 1, they are normalized."
2059 "OBS bis.: Only image sampling is supported for now."
2060 ),
2061 )
2062
2063
2064 def _parse_range_ratio(value: str) -> RangeRatio:
2065 """Parse a ``--random-range-ratio`` CLI string.
2066
2067 Accepts either a plain float (``"0.3"``) or a JSON dict
2068 (``'{"input": 0.3, "output": 0.5}'``).
2069 """
2070 try:
2071 return float(value)
2072 except ValueError:
2073 return json.loads(value)
2074
2075
2076 def get_samples(
2077 args,
2078 tokenizer: TokenizerLike | None,
2079 *,
2080 multimodal_backends: tuple[str, ...] = ("openai-chat", "openai-audio"),
2081 ) -> list[SampleRequest]:
2082 if not hasattr(args, "request_id_prefix"):
2083 args.request_id_prefix = ""
2084
2085 if hasattr(args, "random_range_ratio") and isinstance(args.random_range_ratio, str):
2086 args.random_range_ratio = _parse_range_ratio(args.random_range_ratio)
2087
2088 if args.dataset_name == "custom":
2089 dataset = CustomDataset(
2090 dataset_path=args.dataset_path,
2091 disable_shuffle=args.disable_shuffle,
2092 random_seed=args.seed,
2093 )
2094 input_requests = dataset.sample(
2095 num_requests=args.num_prompts,
2096 tokenizer=tokenizer,
2097 output_len=args.custom_output_len,
2098 skip_chat_template=args.skip_chat_template,
2099 chat_template_kwargs=getattr(args, "chat_template_kwargs", None),
2100 request_id_prefix=args.request_id_prefix,
2101 no_oversample=args.no_oversample,
2102 )
2103
2104 elif args.dataset_name == "custom_image":
2105 dataset = CustomImageDataset(
2106 dataset_path=args.dataset_path,
2107 disable_shuffle=args.disable_shuffle,
2108 random_seed=args.seed,
2109 )
2110 input_requests = dataset.sample(
2111 num_requests=args.num_prompts,
2112 tokenizer=tokenizer,
2113 output_len=args.custom_output_len,
2114 enable_multimodal_chat=args.enable_multimodal_chat,
2115 ensure_client_side_data=getattr(
2116 args, "custom_ensure_client_side_data", False
2117 ),
2118 request_id_prefix=args.request_id_prefix,
2119 no_oversample=args.no_oversample,
2120 )
2121
2122 elif args.dataset_name == "custom_audio":
2123 dataset = CustomAudioDataset(
2124 dataset_path=args.dataset_path,
2125 disable_shuffle=args.disable_shuffle,
2126 random_seed=args.seed,
2127 )
2128 input_requests = dataset.sample(
2129 num_requests=args.num_prompts,
2130 tokenizer=tokenizer,
2131 output_len=args.custom_output_len,
2132 enable_multimodal_chat=args.enable_multimodal_chat,
2133 request_id_prefix=args.request_id_prefix,
2134 no_oversample=args.no_oversample,
2135 )
2136
2137 elif args.dataset_name == "sonnet":
2138 assert tokenizer is not None, (
2139 "Tokenizer must be initialized for the 'sonnet' dataset."
2140 )
2141 sonnet_dataset = SonnetDataset(
2142 dataset_path=args.dataset_path, disable_shuffle=args.disable_shuffle
2143 )
2144 # For the "sonnet" dataset, formatting depends on the backend.
2145 # Chat-style backends leave templating to the server; completions
2146 # backends need the prompt rendered client side.
2147 if args.backend in ("openai-chat", "openai-responses"):
2148 input_requests = sonnet_dataset.sample(
2149 num_requests=args.num_prompts,
2150 input_len=args.sonnet_input_len,
2151 output_len=args.sonnet_output_len,
2152 prefix_len=args.sonnet_prefix_len,
2153 tokenizer=tokenizer,
2154 return_prompt_formatted=False,
2155 request_id_prefix=args.request_id_prefix,
2156 no_oversample=args.no_oversample,
2157 )
2158 else:
2159 assert (
2160 hasattr(tokenizer, "chat_template") and tokenizer.chat_template
2161 ) or (
2162 hasattr(tokenizer, "default_chat_template")
2163 and tokenizer.default_chat_template
2164 ), "Tokenizer/model must have chat template for sonnet dataset."
2165 input_requests = sonnet_dataset.sample(
2166 num_requests=args.num_prompts,
2167 input_len=args.sonnet_input_len,
2168 output_len=args.sonnet_output_len,
2169 prefix_len=args.sonnet_prefix_len,
2170 tokenizer=tokenizer,
2171 return_prompt_formatted=True,
2172 request_id_prefix=args.request_id_prefix,
2173 no_oversample=args.no_oversample,
2174 )
2175
2176 elif args.dataset_name == "hf":
2177 # all following datasets are implemented from the
2178 # HuggingFaceDataset base class
2179 hf_kwargs = {}
2180 dataset_class: type[BenchmarkDataset]
2181 if (
2182 args.dataset_path in VisionArenaDataset.SUPPORTED_DATASET_PATHS
2183 or args.hf_name in VisionArenaDataset.SUPPORTED_DATASET_PATHS
2184 ):
2185 dataset_class = VisionArenaDataset
2186 args.hf_split = args.hf_split if args.hf_split else "train"
2187 args.hf_subset = None
2188 elif (
2189 args.dataset_path in MMVUDataset.SUPPORTED_DATASET_PATHS
2190 or args.hf_name in MMVUDataset.SUPPORTED_DATASET_PATHS
2191 ):
2192 dataset_class = MMVUDataset
2193 args.hf_split = args.hf_split if args.hf_split else "validation"
2194 args.hf_subset = None
2195 elif (
2196 args.dataset_path in InstructCoderDataset.SUPPORTED_DATASET_PATHS
2197 or args.hf_name in InstructCoderDataset.SUPPORTED_DATASET_PATHS
2198 ):
2199 dataset_class = InstructCoderDataset
2200 args.hf_split = args.hf_split if args.hf_split else "train"
2201 elif (
2202 args.dataset_path in MTBenchDataset.SUPPORTED_DATASET_PATHS
2203 or args.hf_name in MTBenchDataset.SUPPORTED_DATASET_PATHS
2204 ):
2205 dataset_class = MTBenchDataset
2206 args.hf_split = args.hf_split if args.hf_split else "train"
2207 elif (
2208 args.dataset_path in HumanEvalDataset.SUPPORTED_DATASET_PATHS
2209 or args.hf_name in HumanEvalDataset.SUPPORTED_DATASET_PATHS
2210 ):
2211 dataset_class = HumanEvalDataset
2212 args.hf_split = args.hf_split if args.hf_split else "test"
2213 elif (
2214 args.dataset_path in GSM8KDataset.SUPPORTED_DATASET_PATHS
2215 or args.hf_name in GSM8KDataset.SUPPORTED_DATASET_PATHS
2216 ):
2217 dataset_class = GSM8KDataset
2218 args.hf_subset = args.hf_subset if args.hf_subset else "main"
2219 args.hf_split = args.hf_split if args.hf_split else "test"
2220 elif (
2221 args.dataset_path in MultiModalConversationDataset.SUPPORTED_DATASET_PATHS
2222 or args.hf_name in MultiModalConversationDataset.SUPPORTED_DATASET_PATHS
2223 ):
2224 dataset_class = MultiModalConversationDataset
2225 elif (
2226 args.dataset_path in ConversationDataset.SUPPORTED_DATASET_PATHS
2227 or args.hf_name in ConversationDataset.SUPPORTED_DATASET_PATHS
2228 ):
2229 dataset_class = ConversationDataset
2230 elif (
2231 args.dataset_path in AIMODataset.SUPPORTED_DATASET_PATHS
2232 or args.hf_name in AIMODataset.SUPPORTED_DATASET_PATHS
2233 ):
2234 dataset_class = AIMODataset
2235 args.hf_split = args.hf_split if args.hf_split else "train"
2236 elif (
2237 args.dataset_path in NextEditPredictionDataset.SUPPORTED_DATASET_PATHS # noqa: E501
2238 or args.hf_name in NextEditPredictionDataset.SUPPORTED_DATASET_PATHS
2239 ):
2240 dataset_class = NextEditPredictionDataset
2241 args.hf_split = args.hf_split if args.hf_split else "train"
2242 elif (
2243 args.dataset_path in ASRDataset.SUPPORTED_DATASET_PATHS
2244 or args.hf_name in ASRDataset.SUPPORTED_DATASET_PATHS
2245 ):
2246 dataset_class = ASRDataset
2247 args.hf_split = args.hf_split if args.hf_split else "train"
2248 hf_kwargs = {
2249 "asr_min_audio_len_sec": args.asr_min_audio_len_sec,
2250 "asr_max_audio_len_sec": args.asr_max_audio_len_sec,
2251 }
2252 elif args.dataset_path in BlazeditDataset.SUPPORTED_DATASET_PATHS:
2253 dataset_class = BlazeditDataset
2254 args.hf_split = args.hf_split if args.hf_split else "train"
2255 hf_kwargs = {
2256 "min_distance": args.blazedit_min_distance,
2257 "max_distance": args.blazedit_max_distance,
2258 }
2259 elif (
2260 args.dataset_path in MLPerfDataset.SUPPORTED_DATASET_PATHS
2261 or args.hf_name in MLPerfDataset.SUPPORTED_DATASET_PATHS
2262 ):
2263 dataset_class = MLPerfDataset
2264 args.hf_split = args.hf_split if args.hf_split else "train"
2265 elif (
2266 args.dataset_path in MMStarDataset.SUPPORTED_DATASET_PATHS
2267 or args.hf_name in MMStarDataset.SUPPORTED_DATASET_PATHS
2268 ):
2269 dataset_class = MMStarDataset
2270 args.hf_split = args.hf_split if args.hf_split else "val"
2271 args.hf_subset = None
2272 elif (
2273 args.dataset_path in BFCLDataset.SUPPORTED_DATASET_PATHS
2274 or args.hf_name in BFCLDataset.SUPPORTED_DATASET_PATHS
2275 ):
2276 if args.backend != "openai-chat":
2277 raise ValueError(
2278 "BFCL dataset requires the 'openai-chat' backend because "
2279 "it sends per-request tool schemas via chat completions."
2280 )
2281 dataset_class = BFCLDataset
2282 # BFCL does not use HF splits/subsets; stub values for base init.
2283 args.hf_split = args.hf_split if args.hf_split else "train"
2284 args.hf_subset = None
2285 hf_kwargs = {"categories": args.bfcl_categories}
2286 else:
2287 supported_datasets = set(
2288 [
2289 dataset_name
2290 for cls in HuggingFaceDataset.__subclasses__()
2291 for dataset_name in cls.SUPPORTED_DATASET_PATHS
2292 ]
2293 )
2294 raise ValueError(
2295 f"Unsupported dataset path: {args.dataset_path}. "
2296 "Huggingface dataset only supports dataset_path"
2297 f" from one of following: {supported_datasets}. "
2298 "Please consider contributing if you would "
2299 "like to add support for additional dataset formats."
2300 )
2301
2302 if (
2303 dataset_class.IS_MULTIMODAL
2304 and args.backend not in multimodal_backends
2305 and "embeddings-" not in args.backend
2306 ):
2307 # multi-modal benchmark is only available on chat-style backends;
2308 # which ones are allowed is caller-controlled (serve uses the
2309 # OpenAI endpoints, throughput uses vllm-chat).
2310 raise ValueError(
2311 f"Multi-modal content is not supported on backend "
2312 f"{args.backend!r}; use one of {sorted(multimodal_backends)}."
2313 )
2314 assert tokenizer is not None, (
2315 "Tokenizer must be initialized for the 'hf' dataset."
2316 )
2317 input_requests = dataset_class(
2318 dataset_path=args.dataset_path,
2319 dataset_subset=args.hf_subset,
2320 dataset_split=args.hf_split,
2321 random_seed=args.seed,
2322 no_stream=args.no_stream,
2323 hf_name=args.hf_name,
2324 disable_shuffle=args.disable_shuffle,
2325 trust_remote_code=args.trust_remote_code,
2326 ).sample(
2327 num_requests=args.num_prompts,
2328 tokenizer=tokenizer,
2329 output_len=args.hf_output_len,
2330 enable_multimodal_chat=args.enable_multimodal_chat,
2331 request_id_prefix=args.request_id_prefix,
2332 no_oversample=args.no_oversample,
2333 skip_chat_template=args.skip_chat_template,
2334 **hf_kwargs,
2335 )
2336
2337 elif args.dataset_name == "timed_trace":
2338 assert tokenizer is not None, (
2339 "Tokenizer must be initialized for the 'timed_trace' dataset."
2340 )
2341 dataloader = TimedTrace(**vars(args))
2342 input_requests = dataloader.sample(
2343 num_requests=args.num_prompts,
2344 tokenizer=tokenizer,
2345 request_id_prefix=args.request_id_prefix,
2346 )
2347
2348 else:
2349 # For datasets that follow a similar structure, use a mapping.
2350 assert tokenizer is not None, (
2351 f"Tokenizer must be initialized for the '{args.dataset_name}' dataset."
2352 )
2353 dataset_mapping = {
2354 "spec_bench": lambda: SpecBench(
2355 dataset_path=args.dataset_path,
2356 category=args.spec_bench_category,
2357 disable_shuffle=args.disable_shuffle,
2358 ).sample(
2359 num_requests=args.num_prompts,
2360 tokenizer=tokenizer,
2361 output_len=args.spec_bench_output_len,
2362 enable_multimodal_chat=args.enable_multimodal_chat,
2363 request_id_prefix=args.request_id_prefix,
2364 no_oversample=args.no_oversample,
2365 ),
2366 "sharegpt": lambda: ShareGPTDataset(
2367 random_seed=args.seed,
2368 dataset_path=args.dataset_path,
2369 disable_shuffle=args.disable_shuffle,
2370 ).sample(
2371 tokenizer=tokenizer,
2372 num_requests=args.num_prompts,
2373 output_len=args.sharegpt_output_len,
2374 enable_multimodal_chat=args.enable_multimodal_chat,
2375 request_id_prefix=args.request_id_prefix,
2376 no_oversample=args.no_oversample,
2377 ),
2378 "burstgpt": lambda: BurstGPTDataset(
2379 random_seed=args.seed,
2380 dataset_path=args.dataset_path,
2381 disable_shuffle=args.disable_shuffle,
2382 ).sample(
2383 tokenizer=tokenizer,
2384 num_requests=args.num_prompts,
2385 request_id_prefix=args.request_id_prefix,
2386 no_oversample=args.no_oversample,
2387 ),
2388 "random": lambda: RandomDataset(
2389 random_seed=args.seed,
2390 dataset_path=args.dataset_path,
2391 disable_shuffle=args.disable_shuffle,
2392 ).sample(
2393 tokenizer=tokenizer,
2394 num_requests=args.num_prompts,
2395 prefix_len=args.random_prefix_len,
2396 input_len=args.random_input_len,
2397 output_len=args.random_output_len,
2398 range_ratio=args.random_range_ratio,
2399 request_id_prefix=args.request_id_prefix,
2400 batchsize=args.random_batch_size,
2401 no_oversample=args.no_oversample,
2402 ),
2403 "random-mm": lambda: RandomMultiModalDataset(
2404 random_seed=args.seed,
2405 dataset_path=args.dataset_path,
2406 disable_shuffle=args.disable_shuffle,
2407 ).sample(
2408 tokenizer=tokenizer,
2409 num_requests=args.num_prompts,
2410 prefix_len=args.random_prefix_len,
2411 range_ratio=args.random_range_ratio,
2412 input_len=args.random_input_len,
2413 output_len=args.random_output_len,
2414 base_items_per_request=args.random_mm_base_items_per_request,
2415 limit_mm_per_prompt=args.random_mm_limit_mm_per_prompt,
2416 num_mm_items_range_ratio=args.random_mm_num_mm_items_range_ratio,
2417 bucket_config=args.random_mm_bucket_config,
2418 enable_multimodal_chat=args.enable_multimodal_chat,
2419 request_id_prefix=args.request_id_prefix,
2420 no_oversample=args.no_oversample,
2421 ),
2422 "random-rerank": lambda: RandomDatasetForReranking(
2423 random_seed=args.seed,
2424 dataset_path=args.dataset_path,
2425 disable_shuffle=args.disable_shuffle,
2426 ).sample(
2427 tokenizer=tokenizer,
2428 num_requests=args.num_prompts,
2429 input_len=args.random_input_len,
2430 range_ratio=args.random_range_ratio,
2431 request_id_prefix=args.request_id_prefix,
2432 batchsize=args.random_batch_size,
2433 is_reranker=not args.no_reranker,
2434 ),
2435 "prefix_repetition": lambda: PrefixRepetitionRandomDataset(
2436 random_seed=args.seed,
2437 dataset_path=args.dataset_path,
2438 disable_shuffle=args.disable_shuffle,
2439 ).sample(
2440 tokenizer=tokenizer,
2441 num_requests=args.num_prompts,
2442 prefix_len=args.prefix_repetition_prefix_len,
2443 suffix_len=args.prefix_repetition_suffix_len,
2444 num_prefixes=args.prefix_repetition_num_prefixes,
2445 output_len=args.prefix_repetition_output_len,
2446 request_id_prefix=args.request_id_prefix,
2447 no_oversample=args.no_oversample,
2448 ),
2449 "speed_bench": lambda: SpeedBench(
2450 dataset_path=args.dataset_path,
2451 dataset_subset=args.speed_bench_dataset_subset,
2452 category=args.speed_bench_category,
2453 disable_shuffle=args.disable_shuffle,
2454 ).sample(
2455 num_requests=args.num_prompts,
2456 tokenizer=tokenizer,
2457 output_len=args.speed_bench_output_len,
2458 skip_chat_template=args.skip_chat_template,
2459 chat_template_kwargs=getattr(args, "chat_template_kwargs", None),
2460 enable_multimodal_chat=args.enable_multimodal_chat,
2461 request_id_prefix=args.request_id_prefix,
2462 no_oversample=args.no_oversample,
2463 ),
2464 }
2465
2466 try:
2467 # Enforce endpoint compatibility for multimodal datasets.
2468 if (
2469 args.dataset_name == "random-mm"
2470 and args.backend not in multimodal_backends
2471 ):
2472 raise ValueError(
2473 f"Multi-modal content (images) is not supported on backend "
2474 f"{args.backend!r}; use one of {sorted(multimodal_backends)}."
2475 )
2476 input_requests = dataset_mapping[args.dataset_name]()
2477 except KeyError as err:
2478 raise ValueError(f"Unknown dataset: {args.dataset_name}") from err
2479
2480 return input_requests
2481
2482
2483 # -----------------------------------------------------------------------------
2484 # Custom Dataset Implementation
2485 # -----------------------------------------------------------------------------
2486
2487
2488 class CustomDataset(BenchmarkDataset):
2489 """Implements the Custom dataset. Loads data from a JSONL file and generates
2490 sample requests based on conversation turns. E.g.,
2491 ```
2492 {"prompt": "What is the capital of India?", "output_tokens": 10}
2493 {"prompt": "What is the capital of Iran?", "output_tokens": 1520}
2494 {"prompt": "What is the capital of China?", "output_tokens": 819}
2495 ```
2496 Note that 'output_tokens' column is optional and has to be provided only if
2497 'custom-output-len' argument is None or -1.
2498 """
2499
2500 def __init__(self, **kwargs) -> None:
2501 super().__init__(**kwargs)
2502 self.load_data()
2503
2504 def load_data(self) -> None:
2505 if self.dataset_path is None:
2506 raise ValueError("dataset_path must be provided for loading data.")
2507
2508 # self.data will be a list of dictionaries
2509 # e.g., [{"prompt": "What is the capital of India?"}, ...]
2510 # This will be the standardized format which load_data()
2511 # has to convert into depending on the filetype of dataset_path.
2512 # sample() will assume this standardized format of self.data
2513 self.data: list[dict] = []
2514
2515 # Load the JSONL file
2516 if self.dataset_path.endswith(".jsonl"):
2517 jsonl_data = pd.read_json(path_or_buf=self.dataset_path, lines=True)
2518
2519 # check if the JSONL file has a 'prompt' column
2520 if "prompt" not in jsonl_data.columns:
2521 raise ValueError("JSONL file must contain a 'prompt' column.")
2522
2523 # Convert each row to a dictionary and append to self.data
2524 # This will convert the DataFrame to a list of dictionaries
2525 # where each dictionary corresponds to a row in the DataFrame.
2526 # This is the standardized format we want for self.data
2527 for _, row in jsonl_data.iterrows():
2528 self.data.append(row.to_dict())
2529 else:
2530 raise NotImplementedError(
2531 "Only JSONL format is supported for CustomDataset."
2532 )
2533
2534 random.seed(self.random_seed)
2535 if not getattr(self, "disable_shuffle", False):
2536 random.shuffle(self.data)
2537
2538 def sample(
2539 self,
2540 tokenizer: TokenizerLike | None,
2541 num_requests: int,
2542 request_id_prefix: str = "",
2543 no_oversample: bool = False,
2544 lora_path: str | None = None,
2545 max_loras: int | None = None,
2546 output_len: int | None = None,
2547 enable_multimodal_chat: bool = False,
2548 skip_chat_template: bool = False,
2549 chat_template_kwargs: dict | None = None,
2550 **kwargs,
2551 ) -> list[SampleRequest]:
2552 # load all data if needed
2553 self.num_available_samples = len(self.data)
2554 if num_requests <= 0:
2555 num_requests = self.num_available_samples
2556 logger.info(
2557 "num_requests is set to 0 or negative, "
2558 "so using all available samples: %d",
2559 num_requests,
2560 )
2561
2562 sampled_requests: list[SampleRequest] = []
2563 for i, item in enumerate(self.data):
2564 if len(sampled_requests) >= num_requests:
2565 break
2566 prompt = item["prompt"]
2567
2568 if tokenizer is None:
2569 new_output_len = 1
2570 elif output_len is None or output_len == -1:
2571 # check that the request has an 'output_tokens' field
2572 if "output_tokens" not in item:
2573 raise ValueError(
2574 "If no output length is provided the "
2575 "custom dataset must contain an 'output_tokens' field."
2576 )
2577 # Use number of output tokens from the request data
2578 try:
2579 new_output_len = int(item["output_tokens"])
2580 except (ValueError, TypeError) as e:
2581 raise ValueError(
2582 f"Invalid value for 'output_tokens' in custom dataset: "
2583 f"'{item['output_tokens']}'. Must be an integer."
2584 ) from e
2585 else:
2586 new_output_len = output_len
2587
2588 if tokenizer is None:
2589 prompt_len = 1
2590 else:
2591 # apply template
2592 if not skip_chat_template:
2593 prompt = tokenizer.apply_chat_template(
2594 [{"role": "user", "content": prompt}],
2595 add_generation_prompt=True,
2596 tokenize=False,
2597 **(chat_template_kwargs or {}),
2598 )
2599
2600 prompt_len = len(tokenizer(prompt).input_ids)
2601 sampled_requests.append(
2602 SampleRequest(
2603 prompt=prompt,
2604 prompt_len=prompt_len,
2605 expected_output_len=new_output_len,
2606 request_id=request_id_prefix + str(i),
2607 )
2608 )
2609 self.maybe_oversample_requests(
2610 sampled_requests, num_requests, request_id_prefix, no_oversample
2611 )
2612
2613 return sampled_requests
2614
2615
2616 class CustomImageDataset(CustomDataset):
2617 """Implements the Custom image dataset. Loads data from a JSONL file and generates
2618 sample requests based on conversation turns. E.g.,
2619 ```
2620 {
2621 "prompt": "How many red blocks in the given images?",
2622 "image_files": ["path/to/image1.png", "path/to/image2.png"],
2623 }
2624 {
2625 "prompt": "Which country has the most pokemons based on the given graphs?",
2626 "image_files": ["path/to/image.png"],
2627 }
2628 {
2629 "content": [
2630 {"type": "text", "text": "Compare these images: "},
2631 {"type": "image", "image": "path/to/image1.png"},
2632 {"type": "text", "text": " and "},
2633 {"type": "image_url", "image_url": {"url": "path/to/image2.png"}},
2634 ],
2635 }
2636 ```
2637
2638 This is used to benchmark multimodal LLMs on arbitrary datasets.
2639 """
2640
2641 IS_MULTIMODAL = True
2642
2643 def load_data(self) -> None:
2644 if self.dataset_path is None:
2645 raise ValueError("dataset_path must be provided for loading data.")
2646
2647 self.data: list[dict] = []
2648
2649 if not self.dataset_path.endswith(".jsonl"):
2650 raise NotImplementedError(
2651 "Only JSONL format is supported for CustomImageDataset."
2652 )
2653
2654 with open(self.dataset_path, encoding="utf-8") as f:
2655 for line_number, line in enumerate(f, start=1):
2656 line = line.strip()
2657 if not line:
2658 continue
2659
2660 try:
2661 item = json.loads(line)
2662 except json.JSONDecodeError as e:
2663 raise ValueError(
2664 f"Invalid JSON in custom image dataset line {line_number}: {e}"
2665 ) from e
2666
2667 if not isinstance(item, dict):
2668 raise ValueError(
2669 "Each custom image dataset line must contain a JSON object. "
2670 f"Found {type(item)} on line {line_number}."
2671 )
2672
2673 has_legacy_fields = "prompt" in item and "image_files" in item
2674 has_interleaved_content = "content" in item
2675 if not has_legacy_fields and not has_interleaved_content:
2676 raise ValueError(
2677 "Each custom image dataset line must contain either "
2678 "'prompt' and 'image_files' fields, or a 'content' field. "
2679 f"Invalid line: {line_number}."
2680 )
2681
2682 self.data.append(item)
2683
2684 random.seed(self.random_seed)
2685 if not getattr(self, "disable_shuffle", False):
2686 random.shuffle(self.data)
2687
2688 @staticmethod
2689 def _validate_content_parts(content: Any) -> list[dict[str, Any]]:
2690 if not isinstance(content, list):
2691 raise ValueError(
2692 "'content' must be a list of text and image content dictionaries."
2693 )
2694
2695 if not content:
2696 raise ValueError("'content' must contain at least one item.")
2697
2698 parts: list[dict[str, Any]] = []
2699 for part in content:
2700 if not isinstance(part, dict):
2701 raise ValueError(
2702 f"Each item in 'content' must be a dictionary. Found {type(part)}."
2703 )
2704 parts.append(part)
2705
2706 return parts
2707
2708 @classmethod
2709 def _process_content_part(
2710 cls,
2711 part: dict[str, Any],
2712 *,
2713 ensure_client_side_data: bool = False,
2714 ) -> dict[str, Any]:
2715 content_type = part.get("type")
2716 if content_type == "text":
2717 text = part.get("text")
2718 if not isinstance(text, str):
2719 raise ValueError("Text content parts must contain a string 'text'.")
2720 return {"type": "text", "text": text}
2721
2722 if content_type == "image":
2723 if "image" not in part:
2724 raise ValueError("Image content parts must contain an 'image' field.")
2725 return dict(
2726 process_image(
2727 part["image"],
2728 ensure_client_side_data=ensure_client_side_data,
2729 )
2730 )
2731
2732 if content_type == "image_url":
2733 image_url = part.get("image_url")
2734 if isinstance(image_url, str):
2735 return dict(
2736 process_image(
2737 image_url,
2738 ensure_client_side_data=ensure_client_side_data,
2739 )
2740 )
2741
2742 if isinstance(image_url, dict):
2743 url = image_url.get("url")
2744 if not isinstance(url, str):
2745 raise ValueError(
2746 "Image URL content parts must contain a string 'image_url.url'."
2747 )
2748
2749 processed_part = dict(
2750 process_image(
2751 url,
2752 ensure_client_side_data=ensure_client_side_data,
2753 )
2754 )
2755 processed_image_url = dict(processed_part["image_url"])
2756 processed_image_url.update(
2757 {key: value for key, value in image_url.items() if key != "url"}
2758 )
2759 processed_part["image_url"] = processed_image_url
2760 return processed_part
2761
2762 raise ValueError(
2763 "Image URL content parts must contain an 'image_url' string "
2764 "or dictionary."
2765 )
2766
2767 raise ValueError(
2768 "Content parts must have type 'text', 'image', or 'image_url'. "
2769 f"Found: {content_type!r}."
2770 )
2771
2772 @classmethod
2773 def _process_interleaved_content(
2774 cls,
2775 content: Any,
2776 *,
2777 ensure_client_side_data: bool = False,
2778 ) -> list[dict[str, Any]]:
2779 return [
2780 cls._process_content_part(
2781 part,
2782 ensure_client_side_data=ensure_client_side_data,
2783 )
2784 for part in cls._validate_content_parts(content)
2785 ]
2786
2787 @staticmethod
2788 def _get_text_from_content(content: list[dict[str, Any]]) -> str:
2789 return "".join(part["text"] for part in content if part.get("type") == "text")
2790
2791 @staticmethod
2792 def _process_image_files(
2793 images: Any,
2794 *,
2795 ensure_client_side_data: bool = False,
2796 ) -> dict[str, Any] | list[dict[str, Any]]:
2797 if not isinstance(images, list) or not images:
2798 raise ValueError("'image_files' must be a non-empty list.")
2799
2800 mm_content = [
2801 dict(
2802 process_image(
2803 image,
2804 ensure_client_side_data=ensure_client_side_data,
2805 )
2806 )
2807 for image in images
2808 ]
2809 if len(mm_content) == 1:
2810 return mm_content[0]
2811
2812 return mm_content
2813
2814 def sample(
2815 self,
2816 tokenizer: TokenizerLike | None,
2817 num_requests: int,
2818 request_id_prefix: str = "",
2819 no_oversample: bool = False,
2820 lora_path: str | None = None,
2821 max_loras: int | None = None,
2822 output_len: int | None = None,
2823 enable_multimodal_chat: bool = False,
2824 skip_chat_template: bool = False,
2825 chat_template_kwargs: dict | None = None,
2826 ensure_client_side_data: bool = False,
2827 **kwargs,
2828 ) -> list[SampleRequest]:
2829 # load all data if needed
2830 self.num_available_samples = len(self.data)
2831 if num_requests <= 0:
2832 num_requests = self.num_available_samples
2833 logger.info(
2834 "num_requests is set to 0 or negative, "
2835 "so using all available samples: %d",
2836 num_requests,
2837 )
2838
2839 sampled_requests: list[SampleRequest] = []
2840 for i, item in enumerate(self.data):
2841 if len(sampled_requests) >= num_requests:
2842 break
2843
2844 if "content" in item:
2845 content = self._process_interleaved_content(
2846 item["content"],
2847 ensure_client_side_data=ensure_client_side_data,
2848 )
2849 text_prompt = self._get_text_from_content(content)
2850 prompt_len = (
2851 1 if tokenizer is None else len(tokenizer(text_prompt).input_ids)
2852 )
2853 prompt = (
2854 [{"role": "user", "content": content}]
2855 if enable_multimodal_chat
2856 else content
2857 )
2858 sampled_requests.append(
2859 SampleRequest(
2860 prompt=prompt,
2861 prompt_len=prompt_len,
2862 expected_output_len=output_len or 0,
2863 multi_modal_data=None,
2864 request_id=request_id_prefix + str(i),
2865 )
2866 )
2867 continue
2868
2869 prompt = item["prompt"]
2870 if not isinstance(prompt, str):
2871 raise ValueError("'prompt' must be a string.")
2872
2873 prompt_len = 1 if tokenizer is None else len(tokenizer(prompt).input_ids)
2874 mm_content = self._process_image_files(
2875 item["image_files"],
2876 ensure_client_side_data=ensure_client_side_data,
2877 )
2878 if enable_multimodal_chat:
2879 # Note: when chat is enabled the request prompt_len is no longer
2880 # accurate and we will be using request output to count the
2881 # actual prompt len
2882 prompt = self.apply_multimodal_chat_transformation(prompt, mm_content)
2883
2884 sampled_requests.append(
2885 SampleRequest(
2886 prompt=prompt,
2887 prompt_len=prompt_len,
2888 expected_output_len=output_len or 0,
2889 multi_modal_data=mm_content,
2890 request_id=request_id_prefix + str(i),
2891 )
2892 )
2893 self.maybe_oversample_requests(
2894 sampled_requests, num_requests, request_id_prefix, no_oversample
2895 )
2896
2897 return sampled_requests
2898
2899
2900 class CustomAudioDataset(CustomDataset):
2901 """Custom dataset for audio benchmarking. Loads data from a JSONL file. E.g.,
2902 {"prompt": "Transcribe the audio.", "audio": "/path/to/audio.wav"}
2903
2904 Supports both:
2905 - Dedicated ASR models (e.g. Whisper) via openai-audio & /v1/audio/transcriptions
2906 - Chat-based audio models (e.g. Qwen2-Audio) via openai-chat & /v1/chat/completions
2907 """
2908
2909 IS_MULTIMODAL = True
2910
2911 def sample(
2912 self,
2913 tokenizer: TokenizerLike | None,
2914 num_requests: int,
2915 request_id_prefix: str = "",
2916 no_oversample: bool = False,
2917 lora_path: str | None = None,
2918 max_loras: int | None = None,
2919 output_len: int | None = None,
2920 enable_multimodal_chat: bool = False,
2921 skip_chat_template: bool = False,
2922 chat_template_kwargs: dict | None = None,
2923 **kwargs,
2924 ) -> list[SampleRequest]:
2925 self.num_available_samples = len(self.data)
2926 if num_requests <= 0:
2927 num_requests = self.num_available_samples
2928 sampled_requests: list[SampleRequest] = []
2929 for i, item in enumerate(self.data):
2930 if len(sampled_requests) >= num_requests:
2931 break
2932 prompt = item.get("prompt", "")
2933 if tokenizer is None:
2934 prompt_len = 1
2935 new_output_len = (
2936 output_len if (output_len is not None and output_len != -1) else 256
2937 )
2938 mm_content = None
2939 else:
2940 use_chat_template = (
2941 not skip_chat_template
2942 and hasattr(tokenizer, "chat_template")
2943 and tokenizer.chat_template is not None
2944 )
2945 if enable_multimodal_chat:
2946 # Chat-based audio models (e.g., Qwen2-Audio):
2947 # encode audio as base64; serve.py assembles the chat message
2948 # as: {"role": "user", "content": [
2949 # {"type": "text", "text": prompt},
2950 # {"type": "input_audio", "input_audio": {...}}
2951 # ]}
2952 y, sr = process_audio(item["audio"])
2953 buf = io.BytesIO()
2954 sf.write(buf, y, sr, format="WAV")
2955 audio_base64 = base64.b64encode(buf.getvalue()).decode("utf-8")
2956 mm_content = {
2957 "type": "input_audio",
2958 "input_audio": {
2959 "data": audio_base64,
2960 "format": "wav",
2961 },
2962 }
2963 # prompt stays as plain string; serve.py handles wrapping
2964 else:
2965 # Whisper-style models: load audio array locally
2966 y, sr = process_audio(item["audio"])
2967 mm_content = {"audio": (y, sr)}
2968 if use_chat_template:
2969 # ASR models with a chat template but not multimodal chat
2970 prompt = tokenizer.apply_chat_template(
2971 [{"role": "user", "content": prompt}],
2972 add_generation_prompt=True,
2973 tokenize=False,
2974 )
2975 # else: plain prompt for Whisper-style models
2976 prompt_len = (
2977 len(tokenizer(prompt).input_ids) if isinstance(prompt, str) else 1
2978 )
2979 if output_len is None or output_len == -1:
2980 if "output_tokens" not in item:
2981 raise ValueError(
2982 "If no output length is provided the "
2983 "custom dataset must contain an 'output_tokens' field."
2984 )
2985 new_output_len = int(item["output_tokens"])
2986 else:
2987 assert output_len is not None
2988 new_output_len = output_len
2989 sampled_requests.append(
2990 SampleRequest(
2991 prompt=prompt,
2992 prompt_len=prompt_len,
2993 expected_output_len=new_output_len,
2994 multi_modal_data=mm_content,
2995 request_id=request_id_prefix + str(i),
2996 )
2997 )
2998 self.maybe_oversample_requests(
2999 sampled_requests, num_requests, request_id_prefix, no_oversample
3000 )
3001 return sampled_requests
3002
3003
3004 # -----------------------------------------------------------------------------
3005 # Spec Bench Dataset Implementation
3006 # -----------------------------------------------------------------------------
3007
3008
3009 class SpecBench(CustomDataset):
3010 """Implements the SpecBench dataset: https://github.com/hemingkx/Spec-Bench
3011 Download the dataset using:
3012 wget https://raw.githubusercontent.com/hemingkx/Spec-Bench/refs/heads/main/data/spec_bench/question.jsonl
3013 """ # noqa: E501
3014
3015 def __init__(self, **kwargs) -> None:
3016 self.category = kwargs.pop("category", None)
3017 super().__init__(**kwargs)
3018 self.load_data()
3019
3020 def load_data(self) -> None:
3021 if self.dataset_path is None:
3022 raise ValueError("dataset_path must be provided for loading data.")
3023
3024 self.data = []
3025
3026 # Load the JSONL file
3027 jsonl_data = pd.read_json(path_or_buf=self.dataset_path, lines=True)
3028
3029 # check if the JSONL file has a 'turns' column
3030 if "turns" not in jsonl_data.columns:
3031 raise ValueError("JSONL file must contain a 'turns' column.")
3032
3033 for _, row in jsonl_data.iterrows():
3034 # sample only from a specific category if specified
3035 if (not self.category) or (self.category == row["category"]):
3036 prompt = row["turns"][0]
3037 self.data.append({"prompt": prompt})
3038
3039 random.seed(self.random_seed)
3040 if not getattr(self, "disable_shuffle", False):
3041 random.shuffle(self.data)
3042
3043 def sample(
3044 self,
3045 tokenizer: TokenizerLike | None,
3046 num_requests: int,
3047 request_id_prefix: str = "",
3048 no_oversample: bool = False,
3049 lora_path: str | None = None,
3050 max_loras: int | None = None,
3051 output_len: int | None = None,
3052 enable_multimodal_chat: bool = False,
3053 skip_chat_template: bool = False,
3054 chat_template_kwargs: dict | None = None,
3055 **kwargs,
3056 ) -> list[SampleRequest]:
3057 # leverage CustomDataset sample
3058 return super().sample(
3059 tokenizer=tokenizer,
3060 num_requests=num_requests,
3061 request_id_prefix=request_id_prefix,
3062 no_oversample=no_oversample,
3063 lora_path=lora_path,
3064 max_loras=max_loras,
3065 output_len=output_len,
3066 enable_multimodal_chat=enable_multimodal_chat,
3067 skip_chat_template=skip_chat_template,
3068 chat_template_kwargs=chat_template_kwargs,
3069 **kwargs,
3070 )
3071
3072
3073 # -----------------------------------------------------------------------------
3074 # Sonnet Dataset Implementation
3075 # -----------------------------------------------------------------------------
3076
3077
3078 @deprecated(
3079 "SonnetDataset is deprecated and will be removed in a future version.",
3080 )
3081 class SonnetDataset(BenchmarkDataset):
3082 """Simplified implementation of the Sonnet dataset. Loads poem lines from a
3083 text file and generates sample requests. Default values here copied from
3084 `benchmark_serving.py` for the sonnet dataset.
3085 """
3086
3087 DEFAULT_PREFIX_LEN = 200
3088 DEFAULT_INPUT_LEN = 550
3089 DEFAULT_OUTPUT_LEN = 150
3090
3091 def __init__(
3092 self,
3093 **kwargs,
3094 ) -> None:
3095 super().__init__(**kwargs)
3096 self.load_data()
3097
3098 def load_data(self) -> None:
3099 if not self.dataset_path:
3100 raise ValueError("dataset_path must be provided.")
3101 with open(self.dataset_path, encoding="utf-8") as f:
3102 self.data = f.readlines()
3103
3104 def sample(
3105 self,
3106 tokenizer: TokenizerLike,
3107 num_requests: int,
3108 request_id_prefix: str = "",
3109 no_oversample: bool = False,
3110 prefix_len: int = DEFAULT_PREFIX_LEN,
3111 input_len: int = DEFAULT_INPUT_LEN,
3112 output_len: int = DEFAULT_OUTPUT_LEN,
3113 return_prompt_formatted: bool = False,
3114 **kwargs,
3115 ) -> list[SampleRequest]:
3116 poem_lines = self.data
3117 assert poem_lines is not None
3118 # Calculate average token length for a poem line.
3119 tokenized_lines = [tokenizer(line).input_ids for line in poem_lines]
3120 avg_len = sum(len(tokens) for tokens in tokenized_lines) / len(tokenized_lines)
3121
3122 # Build the base prompt.
3123 base_prompt = "Pick as many lines as you can from these poem lines:\n"
3124 base_msg = [{"role": "user", "content": base_prompt}]
3125 base_fmt = tokenizer.apply_chat_template(
3126 base_msg, # type: ignore[arg-type]
3127 add_generation_prompt=True,
3128 tokenize=False,
3129 )
3130 assert isinstance(base_fmt, str)
3131 base_offset = len(tokenizer(base_fmt).input_ids)
3132 if input_len <= base_offset:
3133 raise ValueError(
3134 f"'input_len' must be higher than the base prompt length "
3135 f"({base_offset})."
3136 )
3137
3138 # Determine how many poem lines to use.
3139 num_input_lines = max(round((input_len - base_offset) / avg_len), 1)
3140 num_prefix_lines = max(round((prefix_len - base_offset) / avg_len), 0)
3141 prefix_lines = poem_lines[:num_prefix_lines]
3142
3143 samples: list[SampleRequest] = []
3144 ind = 0
3145 while len(samples) < num_requests:
3146 extra_lines = random.choices(
3147 poem_lines, k=num_input_lines - num_prefix_lines
3148 )
3149 prompt = f"{base_prompt}{''.join(prefix_lines + extra_lines)}"
3150 msg = [{"role": "user", "content": prompt}]
3151 prompt_formatted = tokenizer.apply_chat_template(
3152 msg, # type: ignore[arg-type]
3153 add_generation_prompt=True,
3154 tokenize=False,
3155 )
3156 assert isinstance(prompt_formatted, str)
3157 prompt_len = len(tokenizer(prompt_formatted).input_ids)
3158 if prompt_len <= input_len:
3159 samples.append(
3160 SampleRequest(
3161 prompt=(
3162 prompt_formatted if return_prompt_formatted else prompt
3163 ),
3164 prompt_len=prompt_len,
3165 expected_output_len=output_len,
3166 request_id=request_id_prefix + str(ind),
3167 )
3168 )
3169 ind += 1
3170 return samples
3171
3172
3173 # -----------------------------------------------------------------------------
3174 # BurstGPT Dataset Implementation
3175 # -----------------------------------------------------------------------------
3176
3177
3178 class BurstGPTDataset(BenchmarkDataset):
3179 """Implements the BurstGPT dataset. Loads data from a CSV file and generates
3180 sample requests based on synthetic prompt generation. Only rows with Model
3181 "GPT-4" and positive response tokens are used.
3182 """
3183
3184 def __init__(self, **kwargs) -> None:
3185 super().__init__(**kwargs)
3186 self.load_data()
3187
3188 def load_data(
3189 self,
3190 ):
3191 if self.dataset_path is None:
3192 raise ValueError("dataset_path must be provided for loading data.")
3193
3194 df = pd.read_csv(self.dataset_path)
3195 # Filter to keep only GPT-4 rows.
3196 gpt4_df = df[df["Model"] == "GPT-4"]
3197 # Remove failed requests (where Response tokens is 0 or less).
3198 gpt4_df = gpt4_df[gpt4_df["Response tokens"] > 0]
3199 # Sample the desired number of rows.
3200 self.data = gpt4_df
3201
3202 def _sample_loaded_data(self, num_requests: int) -> list:
3203 assert self.data is not None, "Dataset must be loaded before sampling"
3204 if num_requests <= len(self.data):
3205 data = self.data.sample(n=num_requests, random_state=self.random_seed)
3206 else:
3207 data = self.data.sample(
3208 n=num_requests,
3209 random_state=self.random_seed,
3210 replace=True,
3211 )
3212 # Convert the dataframe to a list of lists.
3213 return data.values.tolist()
3214
3215 def sample(
3216 self,
3217 tokenizer: TokenizerLike,
3218 num_requests: int,
3219 request_id_prefix: str = "",
3220 no_oversample: bool = False,
3221 lora_assignment: str = "random",
3222 max_loras: int | None = None,
3223 lora_path: str | None = None,
3224 **kwargs,
3225 ) -> list[SampleRequest]:
3226 samples = []
3227 data = self._sample_loaded_data(num_requests=num_requests)
3228 for i in range(num_requests):
3229 input_len = int(data[i][2])
3230 output_len = int(data[i][3])
3231 lora_req = self.get_lora_request(
3232 index=i,
3233 max_loras=max_loras,
3234 lora_path=lora_path,
3235 lora_assignment=lora_assignment,
3236 )
3237 vocab_size = tokenizer.vocab_size
3238 # Generate a synthetic prompt: a list of token IDs computed as (i +
3239 # j) modulo vocab_size.
3240 token_ids = [(i + j) % vocab_size for j in range(input_len)]
3241 prompt = tokenizer.decode(token_ids)
3242 samples.append(
3243 SampleRequest(
3244 prompt=prompt,
3245 prompt_len=input_len,
3246 expected_output_len=output_len,
3247 lora_request=lora_req,
3248 request_id=request_id_prefix + str(i),
3249 )
3250 )
3251 return samples
3252
3253
3254 # -----------------------------------------------------------------------------
3255 # HuggingFace Dataset Base Implementation
3256 # -----------------------------------------------------------------------------
3257 class HuggingFaceDataset(BenchmarkDataset):
3258 """Base class for datasets hosted on HuggingFace."""
3259
3260 SUPPORTED_DATASET_PATHS: set[str] | dict[str, Callable] = set()
3261
3262 def __init__(
3263 self,
3264 dataset_path: str,
3265 dataset_split: str,
3266 no_stream: bool = False,
3267 dataset_subset: str | None = None,
3268 hf_name: str | None = None,
3269 trust_remote_code: bool = False,
3270 **kwargs,
3271 ) -> None:
3272 super().__init__(dataset_path=dataset_path, **kwargs)
3273
3274 self.dataset_split = dataset_split
3275 self.dataset_subset = dataset_subset
3276 self.load_stream = not no_stream
3277 self.hf_name = hf_name or dataset_path
3278 self.trust_remote_code = trust_remote_code
3279 self.load_data()
3280
3281 def load_data(self) -> None:
3282 """Load data from HuggingFace datasets."""
3283 self.data = load_dataset(
3284 self.dataset_path,
3285 name=self.dataset_subset,
3286 split=self.dataset_split,
3287 streaming=self.load_stream,
3288 trust_remote_code=self.trust_remote_code,
3289 )
3290 if not getattr(self, "disable_shuffle", False):
3291 self.data = self.data.shuffle(seed=self.random_seed)
3292
3293
3294 # -----------------------------------------------------------------------------
3295 # Conversation Dataset Implementation
3296 # -----------------------------------------------------------------------------
3297
3298
3299 class ConversationDataset(HuggingFaceDataset):
3300 """Dataset for text-only conversation data."""
3301
3302 SUPPORTED_DATASET_PATHS = {
3303 "Aeala/ShareGPT_Vicuna_unfiltered",
3304 }
3305 IS_MULTIMODAL = False
3306
3307 def sample(
3308 self,
3309 tokenizer: TokenizerLike,
3310 num_requests: int,
3311 request_id_prefix: str = "",
3312 no_oversample: bool = False,
3313 output_len: int | None = None,
3314 enable_multimodal_chat: bool = False,
3315 **kwargs,
3316 ) -> list[SampleRequest]:
3317 assert self.data is not None, "Dataset must be loaded before sampling"
3318 # Filter examples with at least 2 conversations
3319 filtered_data = self.data.filter(lambda x: len(x["conversations"]) >= 2)
3320 sampled_requests: list[SampleRequest] = []
3321 ind = 0
3322 dynamic_output = output_len is None
3323
3324 for item in filtered_data:
3325 if len(sampled_requests) >= num_requests:
3326 break
3327 conv = item["conversations"]
3328 prompt, completion = conv[0]["value"], conv[1]["value"]
3329
3330 prompt_ids = tokenizer(prompt).input_ids
3331 completion_ids = tokenizer(completion).input_ids
3332 prompt_len = len(prompt_ids)
3333 completion_len = len(completion_ids)
3334 output_len = completion_len if dynamic_output else output_len
3335 assert isinstance(output_len, int) and output_len > 0
3336 if dynamic_output and not is_valid_sequence(prompt_len, completion_len):
3337 continue
3338 mm_content = process_image(item["image"]) if "image" in item else None
3339 if enable_multimodal_chat:
3340 # Note: when chat is enabled the request prompt_len is no longer
3341 # accurate and we will be using request output to count the
3342 # actual prompt len and output len
3343 prompt = self.apply_multimodal_chat_transformation(prompt, mm_content)
3344 sampled_requests.append(
3345 SampleRequest(
3346 prompt=prompt,
3347 prompt_len=prompt_len,
3348 expected_output_len=output_len,
3349 multi_modal_data=mm_content,
3350 request_id=request_id_prefix + str(ind),
3351 )
3352 )
3353 ind += 1
3354 self.maybe_oversample_requests(
3355 sampled_requests, num_requests, request_id_prefix, no_oversample
3356 )
3357 return sampled_requests
3358
3359
3360 class MultiModalConversationDataset(HuggingFaceDataset):
3361 """Dataset for multimodal conversation data."""
3362
3363 SUPPORTED_DATASET_PATHS = {
3364 "lmms-lab/LLaVA-OneVision-Data",
3365 }
3366 IS_MULTIMODAL = True
3367
3368 def sample(
3369 self,
3370 tokenizer: TokenizerLike,
3371 num_requests: int,
3372 request_id_prefix: str = "",
3373 no_oversample: bool = False,
3374 output_len: int | None = None,
3375 enable_multimodal_chat: bool = False,
3376 **kwargs,
3377 ) -> list[SampleRequest]:
3378 assert self.data is not None, "Dataset must be loaded before sampling"
3379 # Filter examples with at least 2 conversations
3380 filtered_data = self.data.filter(lambda x: len(x["conversations"]) >= 2)
3381 sampled_requests: list[SampleRequest] = []
3382 ind = 0
3383 dynamic_output = output_len is None
3384
3385 for item in filtered_data:
3386 if len(sampled_requests) >= num_requests:
3387 break
3388 conv = item["conversations"]
3389 prompt, completion = conv[0]["value"], conv[1]["value"]
3390
3391 prompt_ids = tokenizer(prompt).input_ids
3392 completion_ids = tokenizer(completion).input_ids
3393 prompt_len = len(prompt_ids)
3394 completion_len = len(completion_ids)
3395 output_len = completion_len if dynamic_output else output_len
3396 assert isinstance(output_len, int) and output_len > 0
3397 if dynamic_output and not is_valid_sequence(prompt_len, completion_len):
3398 continue
3399 mm_content = process_image(item["image"]) if "image" in item else None
3400 if enable_multimodal_chat:
3401 # Note: when chat is enabled the request prompt_len is no longer
3402 # accurate and we will be using request output to count the
3403 # actual prompt len and output len
3404 prompt = self.apply_multimodal_chat_transformation(prompt, mm_content)
3405 sampled_requests.append(
3406 SampleRequest(
3407 prompt=prompt,
3408 prompt_len=prompt_len,
3409 expected_output_len=output_len,
3410 multi_modal_data=mm_content,
3411 request_id=request_id_prefix + str(ind),
3412 )
3413 )
3414 ind += 1
3415 self.maybe_oversample_requests(
3416 sampled_requests, num_requests, request_id_prefix, no_oversample
3417 )
3418 return sampled_requests
3419
3420
3421 # -----------------------------------------------------------------------------
3422 # Vision Arena Dataset Implementation
3423 # -----------------------------------------------------------------------------
3424
3425
3426 class VisionArenaDataset(HuggingFaceDataset):
3427 """Vision Arena Dataset."""
3428
3429 DEFAULT_OUTPUT_LEN = 128
3430 SUPPORTED_DATASET_PATHS = {
3431 "lmarena-ai/VisionArena-Chat": lambda x: x["conversation"][0][0]["content"],
3432 "lmarena-ai/vision-arena-bench-v0.1": lambda x: x["turns"][0][0]["content"],
3433 }
3434 IS_MULTIMODAL = True
3435
3436 def sample(
3437 self,
3438 tokenizer: TokenizerLike,
3439 num_requests: int,
3440 request_id_prefix: str = "",
3441 no_oversample: bool = False,
3442 output_len: int | None = None,
3443 enable_multimodal_chat: bool = False,
3444 **kwargs,
3445 ) -> list[SampleRequest]:
3446 parser_fn = self.SUPPORTED_DATASET_PATHS.get(self.hf_name)
3447 if parser_fn is None:
3448 raise ValueError(f"Unsupported dataset path: {self.hf_name}")
3449
3450 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
3451
3452 assert self.data is not None, "Dataset must be loaded before sampling"
3453 sampled_requests: list[SampleRequest] = []
3454 for i, item in enumerate(self.data):
3455 if len(sampled_requests) >= num_requests:
3456 break
3457
3458 prompt = parser_fn(item)
3459 mm_content = process_image(item["images"][0])
3460 prompt_len = len(tokenizer.encode(prompt))
3461 if enable_multimodal_chat:
3462 # Note: when chat is enabled the request prompt_len is no longer
3463 # accurate and we will be using request output to count the
3464 # actual prompt len
3465 prompt = self.apply_multimodal_chat_transformation(prompt, mm_content)
3466
3467 sampled_requests.append(
3468 SampleRequest(
3469 prompt=prompt,
3470 prompt_len=prompt_len,
3471 expected_output_len=output_len,
3472 multi_modal_data=mm_content,
3473 request_id=request_id_prefix + str(i),
3474 )
3475 )
3476
3477 self.maybe_oversample_requests(
3478 sampled_requests, num_requests, request_id_prefix, no_oversample
3479 )
3480 return sampled_requests
3481
3482
3483 class MMVUDataset(HuggingFaceDataset):
3484 """MMVU Dataset.
3485 https://huggingface.co/datasets/yale-nlp/MMVU
3486 """
3487
3488 DEFAULT_OUTPUT_LEN = 128
3489 SUPPORTED_DATASET_PATHS = {
3490 "yale-nlp/MMVU": lambda x: (
3491 x["question"]
3492 + " "
3493 + (" ".join(f"{k}.{v}" for k, v in x["choices"].items()))
3494 ),
3495 }
3496
3497 def __init__(self, **kwargs) -> None:
3498 super().__init__(**kwargs)
3499
3500 self._remote_path_root = (
3501 f"https://huggingface.co/datasets/{self.hf_name}/resolve/main"
3502 )
3503 self._local_path_root = hf_api().snapshot_download(
3504 self.hf_name,
3505 repo_type="dataset",
3506 )
3507
3508 def sample(
3509 self,
3510 tokenizer: TokenizerLike,
3511 num_requests: int,
3512 request_id_prefix: str = "",
3513 no_oversample: bool = False,
3514 output_len: int | None = None,
3515 enable_multimodal_chat: bool = False,
3516 **kwargs,
3517 ) -> list[SampleRequest]:
3518 parser_fn = self.SUPPORTED_DATASET_PATHS.get(self.hf_name)
3519 if parser_fn is None:
3520 raise ValueError(f"Unsupported dataset path: {self.hf_name}")
3521
3522 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
3523
3524 assert self.data is not None, "Dataset must be loaded before sampling"
3525 sampled_requests: list[SampleRequest] = []
3526 for i, item in enumerate(self.data):
3527 if len(sampled_requests) >= num_requests:
3528 break
3529
3530 prompt = parser_fn(item)
3531 mm_content = process_video(
3532 item["video"].replace(self._remote_path_root, self._local_path_root)
3533 )
3534 prompt_len = len(tokenizer.encode(prompt))
3535 if enable_multimodal_chat:
3536 # Note: when chat is enabled the request prompt_len is no longer
3537 # accurate and we will be using request output to count the
3538 # actual prompt len
3539 prompt = self.apply_multimodal_chat_transformation(prompt, mm_content)
3540
3541 sampled_requests.append(
3542 SampleRequest(
3543 prompt=prompt,
3544 prompt_len=prompt_len,
3545 expected_output_len=output_len,
3546 multi_modal_data=mm_content,
3547 request_id=request_id_prefix + str(i),
3548 )
3549 )
3550
3551 self.maybe_oversample_requests(
3552 sampled_requests, num_requests, request_id_prefix, no_oversample
3553 )
3554 return sampled_requests
3555
3556
3557 # -----------------------------------------------------------------------------
3558 # Instruct Coder Dataset Implementation
3559 # -----------------------------------------------------------------------------
3560
3561
3562 class InstructCoderDataset(HuggingFaceDataset):
3563 """InstructCoder Dataset.
3564 https://huggingface.co/datasets/likaixin/InstructCoder
3565
3566 InstructCoder is the dataset designed for general code editing. It consists
3567 of 114,239 instruction-input-output triplets, and covers multiple distinct
3568 code editing scenario.
3569 """
3570
3571 DEFAULT_OUTPUT_LEN = 200 # this is the average default output length
3572 SUPPORTED_DATASET_PATHS = {
3573 "likaixin/InstructCoder",
3574 }
3575
3576 def sample(
3577 self,
3578 tokenizer: TokenizerLike,
3579 num_requests: int,
3580 request_id_prefix: str = "",
3581 no_oversample: bool = False,
3582 output_len: int | None = None,
3583 enable_multimodal_chat: bool = False,
3584 skip_chat_template: bool = False,
3585 **kwargs,
3586 ) -> list[SampleRequest]:
3587 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
3588 sampled_requests: list[SampleRequest] = []
3589 for i, prompt in enumerate(self.sample_prompts(n=num_requests)):
3590 prompt_text = prompt
3591 # apply template
3592 if not skip_chat_template:
3593 prompt_text_result = tokenizer.apply_chat_template(
3594 [{"role": "user", "content": prompt}],
3595 add_generation_prompt=True,
3596 tokenize=False,
3597 )
3598 assert isinstance(prompt_text_result, str)
3599 prompt_text = prompt_text_result
3600
3601 prompt_len = len(tokenizer(prompt_text).input_ids)
3602 sampled_requests.append(
3603 SampleRequest(
3604 prompt=prompt_text,
3605 prompt_len=prompt_len,
3606 expected_output_len=output_len,
3607 request_id=request_id_prefix + str(i),
3608 )
3609 )
3610 self.maybe_oversample_requests(
3611 sampled_requests, num_requests, request_id_prefix, no_oversample
3612 )
3613 return sampled_requests
3614
3615 def sample_prompts(self, n: int) -> Iterator[str]:
3616 assert self.data is not None, "Dataset must be loaded before sampling"
3617 for item in self.data.take(n):
3618 prompt = (
3619 f"{item['input']}\n\n{item['instruction']} Just output "
3620 "the code, do not include any explanation."
3621 )
3622 yield prompt
3623
3624
3625 # -----------------------------------------------------------------------------
3626 # MT-Bench Dataset Implementation
3627 # -----------------------------------------------------------------------------
3628
3629
3630 class MTBenchDataset(HuggingFaceDataset):
3631 """MT-Bench Dataset.
3632 https://huggingface.co/datasets/philschmid/mt-bench
3633
3634 We create a single turn dataset for MT-Bench.
3635 This is similar to Spec decoding benchmark setup in vLLM
3636 https://github.com/vllm-project/vllm/blob/9d98ab5ec/examples/offline_inference/eagle.py#L14-L18
3637 """ # noqa: E501
3638
3639 DEFAULT_OUTPUT_LEN = 256 # avg len used in SD bench in vLLM
3640 SUPPORTED_DATASET_PATHS = {
3641 "philschmid/mt-bench",
3642 }
3643
3644 def sample(
3645 self,
3646 tokenizer: TokenizerLike,
3647 num_requests: int,
3648 request_id_prefix: str = "",
3649 no_oversample: bool = False,
3650 output_len: int | None = None,
3651 enable_multimodal_chat: bool = False,
3652 skip_chat_template: bool = False,
3653 **kwargs,
3654 ) -> list[SampleRequest]:
3655 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
3656 assert self.data is not None, "Dataset must be loaded before sampling"
3657 sampled_requests: list[SampleRequest] = []
3658
3659 for i, item in enumerate(self.data):
3660 if len(sampled_requests) >= num_requests:
3661 break
3662 prompt = item["turns"][0]
3663
3664 # apply template
3665 if not skip_chat_template:
3666 prompt = tokenizer.apply_chat_template(
3667 [{"role": "user", "content": prompt}],
3668 add_generation_prompt=True,
3669 tokenize=False,
3670 )
3671
3672 prompt_len = len(tokenizer(prompt).input_ids)
3673 sampled_requests.append(
3674 SampleRequest(
3675 prompt=prompt,
3676 prompt_len=prompt_len,
3677 expected_output_len=output_len,
3678 request_id=request_id_prefix + str(i),
3679 )
3680 )
3681 self.maybe_oversample_requests(
3682 sampled_requests, num_requests, request_id_prefix, no_oversample
3683 )
3684 return sampled_requests
3685
3686
3687 # -----------------------------------------------------------------------------
3688 # HumanEval Dataset Implementation
3689 # -----------------------------------------------------------------------------
3690
3691
3692 class HumanEvalDataset(HuggingFaceDataset):
3693 """HumanEvalDataset Dataset.
3694 https://huggingface.co/datasets/openai/openai_humaneval
3695
3696 We create a single turn dataset for HumanEval.
3697 """
3698
3699 DEFAULT_OUTPUT_LEN = 256
3700 SUPPORTED_DATASET_PATHS = {
3701 "openai/openai_humaneval",
3702 }
3703
3704 def sample(
3705 self,
3706 tokenizer: TokenizerLike,
3707 num_requests: int,
3708 request_id_prefix: str = "",
3709 no_oversample: bool = False,
3710 output_len: int | None = None,
3711 enable_multimodal_chat: bool = False,
3712 skip_chat_template: bool = False,
3713 **kwargs,
3714 ) -> list[SampleRequest]:
3715 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
3716 sampled_requests: list[SampleRequest] = []
3717 assert self.data is not None, "Data must be loaded before sampling"
3718
3719 for i, item in enumerate(self.data):
3720 if len(sampled_requests) >= num_requests:
3721 break
3722 prompt = item["prompt"]
3723
3724 # apply template
3725 if not skip_chat_template:
3726 prompt = tokenizer.apply_chat_template(
3727 [{"role": "user", "content": prompt}],
3728 add_generation_prompt=True,
3729 tokenize=False,
3730 )
3731
3732 prompt_len = len(tokenizer(prompt).input_ids)
3733 sampled_requests.append(
3734 SampleRequest(
3735 prompt=prompt,
3736 prompt_len=prompt_len,
3737 expected_output_len=output_len,
3738 request_id=request_id_prefix + str(i),
3739 )
3740 )
3741 self.maybe_oversample_requests(
3742 sampled_requests, num_requests, request_id_prefix, no_oversample
3743 )
3744 return sampled_requests
3745
3746
3747 # -----------------------------------------------------------------------------
3748 # GSM8K Dataset Implementation
3749 # -----------------------------------------------------------------------------
3750
3751
3752 class GSM8KDataset(HuggingFaceDataset):
3753 """GSM8K Dataset.
3754 https://huggingface.co/datasets/openai/gsm8k
3755
3756 We create a single turn dataset for GSM8K.
3757 """
3758
3759 DEFAULT_OUTPUT_LEN = 256
3760 SUPPORTED_DATASET_PATHS = {
3761 "openai/gsm8k",
3762 }
3763
3764 def sample(
3765 self,
3766 tokenizer: TokenizerLike,
3767 num_requests: int,
3768 request_id_prefix: str = "",
3769 no_oversample: bool = False,
3770 output_len: int | None = None,
3771 enable_multimodal_chat: bool = False,
3772 skip_chat_template: bool = False,
3773 **kwargs,
3774 ) -> list[SampleRequest]:
3775 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
3776 sampled_requests: list[SampleRequest] = []
3777 assert self.data is not None, "Data must be loaded before sampling"
3778
3779 for i, item in enumerate(self.data):
3780 if len(sampled_requests) >= num_requests:
3781 break
3782 prompt = item["question"]
3783
3784 # apply template
3785 if not skip_chat_template:
3786 prompt = tokenizer.apply_chat_template(
3787 [{"role": "user", "content": prompt}],
3788 add_generation_prompt=True,
3789 tokenize=False,
3790 )
3791
3792 prompt_len = len(tokenizer(prompt).input_ids)
3793 sampled_requests.append(
3794 SampleRequest(
3795 prompt=prompt,
3796 prompt_len=prompt_len,
3797 expected_output_len=output_len,
3798 request_id=request_id_prefix + str(i),
3799 )
3800 )
3801 self.maybe_oversample_requests(
3802 sampled_requests, num_requests, request_id_prefix, no_oversample
3803 )
3804 return sampled_requests
3805
3806
3807 # -----------------------------------------------------------------------------
3808 # Blazedit Dataset Implementation
3809 # -----------------------------------------------------------------------------
3810
3811
3812 class BlazeditDataset(HuggingFaceDataset):
3813 """Blazedit Dataset.
3814 https://github.com/ise-uiuc/blazedit
3815
3816 5k char version: vdaita/edit_5k_char
3817 10k char version: vdaita/edit_10k_char
3818 """ # noqa: E501
3819
3820 # 5k char version will have output as ~5k chars
3821 # 10k char version will have output as ~10k chars
3822 # Assuming 3 char per token, 10k chars will be 3333 tokens
3823 # We set default to 4000 to be safe
3824 DEFAULT_OUTPUT_LEN = 4000
3825 SUPPORTED_DATASET_PATHS = {
3826 "vdaita/edit_5k_char",
3827 "vdaita/edit_10k_char",
3828 }
3829
3830 def sample(
3831 self,
3832 tokenizer: TokenizerLike,
3833 num_requests: int,
3834 request_id_prefix: str = "",
3835 no_oversample: bool = False,
3836 output_len: int | None = None,
3837 skip_chat_template: bool = False,
3838 min_distance: float = 0.0,
3839 max_distance: float = 1.0,
3840 **kwargs,
3841 ) -> list[SampleRequest]:
3842 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
3843 sampled_requests: list[SampleRequest] = []
3844
3845 assert self.data is not None, "Dataset must be loaded before sampling"
3846 for i, item in enumerate(self.data):
3847 if len(sampled_requests) >= num_requests:
3848 break
3849 code = item["code"]
3850 change_request = item["change_request"]
3851 norm_distance = item["norm_distance"]
3852
3853 # compare the levenshtein distance normalized by code length
3854 if norm_distance < min_distance or norm_distance > max_distance:
3855 continue
3856
3857 # template copied from
3858 # https://github.com/ise-uiuc/blazedit/blob/7765137e656fd62de877422d2e4cf8de51228054/dataset/create_refined_dataset.py#L94-L105 # noqa: E501
3859 prompt = f"""Given a code file, please apply the change requests and generate the new file.
3860
3861 Original file:
3862 ```python
3863 {code}
3864 ```
3865
3866 Change request:
3867 {change_request}
3868
3869 Please generate the new code file in the "New file" section below.""" # noqa: E501
3870
3871 # apply template
3872 if not skip_chat_template:
3873 prompt_result = tokenizer.apply_chat_template(
3874 [{"role": "user", "content": prompt}],
3875 add_generation_prompt=True,
3876 tokenize=False,
3877 )
3878 assert isinstance(prompt_result, str)
3879 prompt = prompt_result
3880
3881 prompt_len = len(tokenizer(prompt).input_ids)
3882
3883 sampled_requests.append(
3884 SampleRequest(
3885 prompt=prompt,
3886 prompt_len=prompt_len,
3887 expected_output_len=output_len,
3888 request_id=request_id_prefix + str(i),
3889 )
3890 )
3891 self.maybe_oversample_requests(
3892 sampled_requests, num_requests, request_id_prefix, no_oversample
3893 )
3894
3895 return sampled_requests
3896
3897
3898 # -----------------------------------------------------------------------------
3899 # AIMO Dataset Implementation
3900 # -----------------------------------------------------------------------------
3901
3902
3903 class AIMODataset(HuggingFaceDataset):
3904 """Dataset class for processing a AIMO dataset with reasoning questions."""
3905
3906 SUPPORTED_DATASET_PATHS = {
3907 "AI-MO/aimo-validation-aime",
3908 "AI-MO/NuminaMath-1.5",
3909 "AI-MO/NuminaMath-CoT",
3910 }
3911
3912 def sample(
3913 self,
3914 tokenizer: TokenizerLike,
3915 num_requests: int,
3916 request_id_prefix: str = "",
3917 no_oversample: bool = False,
3918 output_len: int | None = None,
3919 **kwargs,
3920 ) -> list[SampleRequest]:
3921 assert self.data is not None, "Dataset must be loaded before sampling"
3922 sampled_requests: list[SampleRequest] = []
3923 ind = 0
3924 dynamic_output = output_len is None
3925
3926 for item in self.data:
3927 if len(sampled_requests) >= num_requests:
3928 break
3929 prompt, completion = item["problem"], item["solution"]
3930
3931 prompt_ids = tokenizer(prompt).input_ids
3932 completion_ids = tokenizer(completion).input_ids
3933 prompt_len = len(prompt_ids)
3934 completion_len = len(completion_ids)
3935 output_len = completion_len if dynamic_output else output_len
3936 assert isinstance(output_len, int) and output_len > 0
3937 if dynamic_output and not is_valid_sequence(
3938 prompt_len, completion_len, max_prompt_len=2048, max_total_len=32000
3939 ):
3940 continue
3941 sampled_requests.append(
3942 SampleRequest(
3943 prompt=prompt,
3944 prompt_len=prompt_len,
3945 expected_output_len=output_len,
3946 multi_modal_data=None,
3947 request_id=request_id_prefix + str(ind),
3948 )
3949 )
3950 ind += 1
3951 self.maybe_oversample_requests(
3952 sampled_requests, num_requests, request_id_prefix, no_oversample
3953 )
3954 return sampled_requests
3955
3956
3957 # -----------------------------------------------------------------------------
3958 # Next Edit Prediction Dataset Implementation
3959 # -----------------------------------------------------------------------------
3960
3961
3962 zeta_prompt = """### Instruction:
3963 You are a code completion assistant and your task is to analyze user edits and then rewrite an excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking into account the cursor location.
3964
3965 ### User Edits:
3966
3967 {}
3968
3969 ### User Excerpt:
3970
3971 {}
3972
3973 ### Response:
3974
3975 """ # noqa: E501
3976
3977
3978 def _format_zeta_prompt(
3979 sample: dict, original_start_marker: str = "<|editable_region_start|>"
3980 ) -> dict:
3981 """Format the zeta prompt for the Next Edit Prediction (NEP) dataset.
3982
3983 This function formats examples from the NEP dataset
3984 into prompts and expected outputs. It could be
3985 further extended to support more NEP datasets.
3986
3987 Args:
3988 sample: The dataset sample containing events,
3989 inputs, and outputs.
3990 original_start_marker: The marker indicating the
3991 start of the editable region. Defaults to
3992 "<|editable_region_start|>".
3993
3994 Returns:
3995 A dictionary with the formatted prompts and expected outputs.
3996
3997 """
3998 events = sample["events"]
3999 input = sample["input"]
4000 output = sample["output"]
4001 prompt = zeta_prompt.format(events, input)
4002
4003 # following the original implementation, extract the focused region
4004 # from the raw output
4005 output_start_index = output.find(original_start_marker)
4006 output_focused_region = output[output_start_index:]
4007 expected_output = output_focused_region
4008
4009 return {"prompt": prompt, "expected_output": expected_output}
4010
4011
4012 class NextEditPredictionDataset(HuggingFaceDataset):
4013 """Dataset class for processing a Next Edit Prediction dataset."""
4014
4015 SUPPORTED_DATASET_PATHS = {
4016 "zed-industries/zeta",
4017 }
4018 MAPPING_PROMPT_FUNCS = {
4019 "zed-industries/zeta": _format_zeta_prompt,
4020 }
4021
4022 def sample(
4023 self,
4024 tokenizer: TokenizerLike,
4025 num_requests: int,
4026 request_id_prefix: str = "",
4027 no_oversample: bool = False,
4028 **kwargs,
4029 ):
4030 formatting_prompt_func = self.MAPPING_PROMPT_FUNCS.get(self.hf_name)
4031 if formatting_prompt_func is None:
4032 raise ValueError(f"Unsupported dataset path: {self.hf_name}")
4033 assert self.data is not None, "Dataset must be loaded before sampling"
4034 samples = []
4035 for i, sample in enumerate(self.data):
4036 sample = formatting_prompt_func(sample)
4037 samples.append(
4038 SampleRequest(
4039 prompt=sample["prompt"],
4040 prompt_len=len(tokenizer(sample["prompt"]).input_ids),
4041 expected_output_len=len(
4042 tokenizer(sample["expected_output"]).input_ids
4043 ),
4044 request_id=request_id_prefix + str(i),
4045 )
4046 )
4047 if len(samples) >= num_requests:
4048 break
4049 self.maybe_oversample_requests(
4050 samples, num_requests, request_id_prefix, no_oversample
4051 )
4052 return samples
4053
4054
4055 # -----------------------------------------------------------------------------
4056 # ASR Dataset Implementation
4057 # -----------------------------------------------------------------------------
4058
4059
4060 class ASRDataset(HuggingFaceDataset):
4061 """Dataset class for processing a ASR dataset for transcription.
4062 Tested on the following set:
4063
4064 +---------------------------+----------------------------------------+--------------------------+-----------------------------+
4065 | Dataset | Domain | Speaking Style | hf-subset |
4066 +---------------------------+----------------------------------------+--------------------------+-----------------------------+
4067 | TED-LIUM | TED talks | Oratory | release1, release2, release3|
4068 | | | | release3-speaker-adaptation |
4069 | VoxPopuli | European Parliament | Oratory | en, de, it, fr, ... |
4070 | LibriSpeech | Audiobook | Narrated | "LIUM/tedlium" |
4071 | GigaSpeech | Audiobook, podcast, YouTube | Narrated, spontaneous | xs, s, m, l, xl, dev, test |
4072 | SPGISpeech | Financial meetings | Oratory, spontaneous | S, M, L, dev, test |
4073 | Earnings22-Cleaned-AA | Long form earnings calls | Prepared remarks, Q&A | test |
4074 | Earnings22-Tiny-Filtered | Earnings calls | Prepared remarks, Q&A | validation |
4075 | AMI | Meetings | Spontaneous | ihm, sdm |
4076 +---------------------------+----------------------------------------+--------------------------+-----------------------------+
4077
4078 """ # noqa: E501
4079
4080 EARNINGS22_CLEANED_DATASET = "ArtificialAnalysis/Earnings22-Cleaned-AA"
4081 EARNINGS22_TINY_FILTERED_DATASET = (
4082 "D4nt3/esb-datasets-earnings22-validation-tiny-filtered"
4083 )
4084 LIBRISPEECH_DATASET = "openslr/librispeech_asr"
4085
4086 SUPPORTED_DATASET_PATHS = {
4087 LIBRISPEECH_DATASET,
4088 "facebook/voxpopuli",
4089 "LIUM/tedlium",
4090 "edinburghcstr/ami",
4091 "speechcolab/gigaspeech",
4092 "kensho/spgispeech",
4093 EARNINGS22_CLEANED_DATASET,
4094 EARNINGS22_TINY_FILTERED_DATASET,
4095 }
4096
4097 DEFAULT_OUTPUT_LEN = 1024
4098 IS_MULTIMODAL = True
4099
4100 def load_data(self) -> None:
4101 if self.hf_name == self.EARNINGS22_CLEANED_DATASET:
4102 # This subset stores repo-local MP3 paths instead of a HF `Audio`
4103 # column, so eagerly materialize it back into the common schema.
4104 self.data = load_dataset(
4105 self.dataset_path,
4106 name=self.dataset_subset,
4107 split=self.dataset_split,
4108 streaming=False,
4109 trust_remote_code=self.trust_remote_code,
4110 )
4111 if not getattr(self, "disable_shuffle", False):
4112 self.data = self.data.shuffle(seed=self.random_seed)
4113 self._materialize_local_audio_column()
4114 return
4115 if self.hf_name in (
4116 self.EARNINGS22_TINY_FILTERED_DATASET,
4117 self.LIBRISPEECH_DATASET,
4118 ):
4119 super().load_data()
4120 self._disable_audio_decode()
4121 return
4122
4123 super().load_data()
4124
4125 def _disable_audio_decode(self) -> None:
4126 from datasets import Audio
4127
4128 assert self.data is not None, "Dataset must be loaded before sampling"
4129 self.data = self.data.cast_column("audio", Audio(decode=False))
4130
4131 def _materialize_local_audio_column(self) -> None:
4132 local_path_root = Path(
4133 hf_api().snapshot_download(self.hf_name, repo_type="dataset")
4134 )
4135 assert self.data is not None, "Dataset must be loaded before sampling"
4136 self.data = self.data.map(
4137 lambda item: {
4138 "audio": str(local_path_root / item["url"]),
4139 "text": item["transcript"],
4140 }
4141 )
4142 self._disable_audio_decode()
4143
4144 def sample(
4145 self,
4146 tokenizer: TokenizerLike,
4147 num_requests: int,
4148 request_id_prefix: str = "",
4149 no_oversample: bool = False,
4150 output_len: int | None = None,
4151 **kwargs,
4152 ) -> list[SampleRequest]:
4153 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
4154 name_or_path = getattr(tokenizer, "name_or_path", "")
4155 tok_class = type(tokenizer).__name__
4156 if "openai" in name_or_path:
4157 prompt = "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>"
4158 elif tok_class == "CohereAsrTokenizer" or "cohere" in name_or_path.lower():
4159 # CohereAsrTokenizer does not inject a decoder start token, so the
4160 # decoder prompt must supply the full control-token sequence.
4161 # Token order: context boundary, transcript start, emotion (default
4162 # undefined), language (en), transcription directive (en), punctuation
4163 # enabled, no ITN, no timestamp, no diarization.
4164 prompt = (
4165 "<|startofcontext|><|startoftranscript|>"
4166 "<|emo:undefined|><|en|><|en|><|pnc|><|noitn|>"
4167 "<|notimestamp|><|nodiarize|>"
4168 )
4169 else:
4170 prompt = ""
4171 prompt_len = len(tokenizer(prompt).input_ids)
4172 assert self.data is not None, "Dataset must be loaded before sampling"
4173 sampled_requests: list[SampleRequest] = []
4174 ind = 0
4175 skipped = 0
4176 asr_min_audio_len_sec: float = float(kwargs.get("asr_min_audio_len_sec") or 0.0)
4177 asr_max_audio_len_sec: float = float(
4178 kwargs.get("asr_max_audio_len_sec") or float("inf")
4179 )
4180 durations = []
4181 for item in self.data:
4182 if len(sampled_requests) >= num_requests:
4183 break
4184 mm_content: dict[str, Any]
4185 audio = item["audio"]
4186 if (
4187 isinstance(audio, dict)
4188 and "array" in audio
4189 and "sampling_rate" in audio
4190 ):
4191 y, sr = audio["array"], audio["sampling_rate"]
4192 duration_s = get_audio_duration(y=y, sr=sr)
4193 mm_content = {"audio": (y, sr)}
4194 elif isinstance(audio, str):
4195 duration_s = sf.info(audio).duration
4196 mm_content = {"audio_path": audio}
4197 elif isinstance(audio, dict) and audio.get("bytes") is not None:
4198 with BytesIO(audio["bytes"]) as audio_buffer:
4199 y, sr = sf.read(audio_buffer, dtype="float32")
4200 duration_s = get_audio_duration(y=y, sr=sr)
4201 mm_content = {"audio": (y, sr)}
4202 elif isinstance(audio, dict) and audio.get("path"):
4203 duration_s = sf.info(audio["path"]).duration
4204 mm_content = {"audio_path": audio["path"]}
4205 else:
4206 raise ValueError(
4207 "ASR samples must provide decoded audio arrays, "
4208 "embedded audio bytes, or a local audio path."
4209 )
4210 if duration_s < asr_min_audio_len_sec or duration_s > asr_max_audio_len_sec:
4211 skipped += 1
4212 continue
4213
4214 durations.append(duration_s)
4215 sampled_requests.append(
4216 SampleRequest(
4217 prompt=prompt,
4218 prompt_len=prompt_len,
4219 expected_output_len=output_len,
4220 multi_modal_data=mm_content,
4221 request_id=request_id_prefix + str(ind),
4222 )
4223 )
4224 ind += 1
4225 if skipped:
4226 logger.warning(
4227 "%d samples discarded from dataset due to"
4228 " their length being greater than"
4229 " what Whisper supports.",
4230 skipped,
4231 )
4232
4233 logger.info("Number of audio samples: %d", len(durations))
4234 avg_duration = sum(durations) / len(durations) if durations else 0
4235 min_duration = min(durations) if durations else 0
4236 max_duration = max(durations) if durations else 0
4237 median_duration = np.median(durations) if durations else 0
4238 logger.info(
4239 "Audio duration statistics (s): avg=%.2f, min=%.2f, max=%.2f, median=%.2f",
4240 avg_duration,
4241 min_duration,
4242 max_duration,
4243 median_duration,
4244 )
4245
4246 self.maybe_oversample_requests(
4247 sampled_requests, num_requests, request_id_prefix, no_oversample
4248 )
4249 return sampled_requests
4250
4251
4252 # -----------------------------------------------------------------------------
4253 # MLPerf Dataset Implementation
4254 # -----------------------------------------------------------------------------
4255
4256
4257 class MLPerfDataset(HuggingFaceDataset):
4258 """MLPerf Inference Dataset.
4259
4260 Dataset on HF:
4261 https://huggingface.co/datasets/mgoin/mlperf-inference-llama2-data
4262 https://huggingface.co/datasets/mgoin/mlperf-inference-llama3.1-data
4263
4264 Each record contains:
4265 - "system_prompt": system role instruction.
4266 - "question": user question.
4267 - "output": reference answer.
4268
4269 We combine the system prompt and question into a chat-formatted prompt
4270 (using the tokenizer's chat template) and set the expected output length to
4271 the tokenized length of the provided reference answer.
4272 """
4273
4274 SUPPORTED_DATASET_PATHS = {
4275 "mgoin/mlperf-inference-llama2-data",
4276 "mgoin/mlperf-inference-llama3.1-data",
4277 }
4278
4279 def sample(
4280 self,
4281 tokenizer: TokenizerLike,
4282 num_requests: int,
4283 request_id_prefix: str = "",
4284 no_oversample: bool = False,
4285 output_len: int | None = None,
4286 **kwargs,
4287 ) -> list[SampleRequest]:
4288 assert self.data is not None, "Dataset must be loaded before sampling"
4289 sampled_requests: list[SampleRequest] = []
4290 ind = 0
4291
4292 for item in self.data:
4293 if len(sampled_requests) >= num_requests:
4294 break
4295
4296 system_prompt = item["system_prompt"]
4297 question = item["question"]
4298 reference_answer = item["output"]
4299
4300 # Build chat-style prompt using tokenizer template, if available.
4301 messages = [
4302 {"role": "system", "content": system_prompt},
4303 {"role": "user", "content": question},
4304 ]
4305 prompt_formatted = tokenizer.apply_chat_template(
4306 messages, # type: ignore[arg-type]
4307 add_generation_prompt=True,
4308 tokenize=False,
4309 )
4310 assert isinstance(prompt_formatted, str)
4311 prompt_len = len(tokenizer(prompt_formatted).input_ids)
4312
4313 # Determine output length from reference answer tokens.
4314 ref_out_len = len(
4315 tokenizer(reference_answer, add_special_tokens=False).input_ids
4316 )
4317 expected_output_len = ref_out_len if output_len is None else output_len
4318
4319 # Validate sequence lengths.
4320 if not is_valid_sequence(prompt_len, expected_output_len):
4321 continue
4322
4323 sampled_requests.append(
4324 SampleRequest(
4325 prompt=prompt_formatted,
4326 prompt_len=prompt_len,
4327 expected_output_len=expected_output_len,
4328 request_id=request_id_prefix + str(ind),
4329 )
4330 )
4331 ind += 1
4332
4333 self.maybe_oversample_requests(
4334 sampled_requests, num_requests, request_id_prefix, no_oversample
4335 )
4336 return sampled_requests
4337
4338
4339 # -----------------------------------------------------------------------------
4340 # Prefix Repetition Dataset Implementation
4341 # -----------------------------------------------------------------------------
4342
4343
4344 class PrefixRepetitionRandomDataset(BenchmarkDataset):
4345 # Default values copied from benchmark_serving.py for the repeated prefix
4346 # dataset.
4347 DEFAULT_PREFIX_LEN = 256
4348 DEFAULT_SUFFIX_LEN = 256
4349 DEFAULT_NUM_PREFIXES = 10
4350 DEFAULT_OUTPUT_LEN = 128
4351
4352 def __init__(
4353 self,
4354 **kwargs,
4355 ) -> None:
4356 super().__init__(**kwargs)
4357 random.seed(self.random_seed)
4358 np.random.seed(self.random_seed)
4359
4360 def sample(
4361 self,
4362 tokenizer: TokenizerLike,
4363 num_requests: int,
4364 request_id_prefix: str = "",
4365 no_oversample: bool = False,
4366 prefix_len: int = DEFAULT_PREFIX_LEN,
4367 suffix_len: int = DEFAULT_SUFFIX_LEN,
4368 num_prefixes: int = DEFAULT_NUM_PREFIXES,
4369 output_len: int = DEFAULT_OUTPUT_LEN,
4370 **kwargs,
4371 ) -> list[SampleRequest]:
4372 vocab_size = tokenizer.vocab_size
4373 prompts_per_prefix = num_requests // num_prefixes
4374 if prompts_per_prefix == 0:
4375 raise ValueError(
4376 f"num_requests ({num_requests}) must be greater than or equal "
4377 f"to num_prefixes ({num_prefixes})"
4378 )
4379
4380 def _generate_exact_length_tokens(target_length: int) -> tuple[list[int], int]:
4381 """Generate tokens that decode and re-encode to exactly
4382 target_length."""
4383 # Generate random tokens
4384 tokens = np.random.randint(0, vocab_size, size=target_length).tolist()
4385
4386 _, adjusted_tokens, token_mismatch = gen_prompt_decode_to_target_len( # noqa: E501
4387 tokenizer=tokenizer,
4388 token_sequence=tokens,
4389 target_token_len=target_length,
4390 add_special_tokens=False,
4391 )
4392 return adjusted_tokens, token_mismatch
4393
4394 requests = []
4395 token_mismatch_total = 0
4396 for _ in range(num_prefixes):
4397 prefix_tokens, prefix_mismatch = _generate_exact_length_tokens(prefix_len)
4398 token_mismatch_total += prefix_mismatch
4399
4400 for _ in range(prompts_per_prefix):
4401 suffix_tokens, suffix_mismatch = _generate_exact_length_tokens(
4402 suffix_len
4403 )
4404 token_mismatch_total += suffix_mismatch
4405 combined_tokens = prefix_tokens + suffix_tokens
4406 prompt = tokenizer.decode(combined_tokens)
4407 prompt_len = len(combined_tokens)
4408 requests.append(
4409 SampleRequest(
4410 prompt=prompt,
4411 prompt_len=prompt_len,
4412 expected_output_len=output_len,
4413 )
4414 )
4415
4416 if token_mismatch_total != 0:
4417 sign = "more" if token_mismatch_total > 0 else "fewer"
4418 logger.warning(
4419 "Across all generated prompts, there were %d %s tokens "
4420 "than expected after decoding and re-encoding. This is "
4421 "expected due to the imperfect nature of the sampling "
4422 "procedure.",
4423 abs(token_mismatch_total),
4424 sign,
4425 )
4426 if not getattr(self, "disable_shuffle", False):
4427 random.shuffle(requests)
4428 return requests
4429
4430
4431 # -----------------------------------------------------------------------------
4432 # MMStar Dataset Implementation
4433 # -----------------------------------------------------------------------------
4434
4435
4436 class MMStarDataset(HuggingFaceDataset):
4437 """Lin-Chen/MMStar: https://huggingface.co/datasets/Lin-Chen/MMStar
4438 refer to: https://github.com/sgl-project/SpecForge/pull/106
4439 """
4440
4441 DEFAULT_OUTPUT_LEN = 128
4442 SUPPORTED_DATASET_PATHS = {"Lin-Chen/MMStar"}
4443 IS_MULTIMODAL = True
4444
4445 def sample(
4446 self,
4447 tokenizer: TokenizerLike,
4448 num_requests: int,
4449 request_id_prefix: str = "",
4450 no_oversample: bool = False,
4451 output_len: int | None = None,
4452 enable_multimodal_chat: bool = False,
4453 **kwargs,
4454 ) -> list[SampleRequest]:
4455 # If --hf-output-len is not set, use the default output length.
4456 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
4457 assert self.data is not None, "Dataset must be loaded before sampling"
4458 sampled_requests: list[SampleRequest] = []
4459
4460 for ind, item in enumerate(self.data):
4461 if len(sampled_requests) >= num_requests:
4462 break
4463 # Split the question text from options
4464 # (keep only the part before "Options:").
4465 full_q: str = item.get("question", "")
4466 question_text = full_q.split("Options:", 1)[0].strip()
4467
4468 # Multimodal image content.
4469 mm_content = process_image(item["image"])
4470
4471 # Compute prompt token length (note: this is plain text length
4472 # if enable_multimodal_chat is False).
4473 prompt_len = len(tokenizer(question_text).input_ids)
4474
4475 prompt: str | list[dict]
4476 if enable_multimodal_chat:
4477 # If multimodal content should be embedded in the chat message,
4478 # convert to [{"role":"user","content":[...]}]
4479 prompt = self.apply_multimodal_chat_transformation(
4480 question_text, mm_content
4481 )
4482 mm_for_request = None # Already embedded in chat content.
4483 else:
4484 # Default: prompt is plain text,
4485 # image is in mm_content for the bench to assemble.
4486 prompt = question_text
4487 mm_for_request = mm_content
4488
4489 sampled_requests.append(
4490 SampleRequest(
4491 prompt=prompt,
4492 prompt_len=prompt_len,
4493 expected_output_len=output_len,
4494 multi_modal_data=mm_for_request,
4495 request_id=request_id_prefix + str(ind),
4496 )
4497 )
4498
4499 self.maybe_oversample_requests(
4500 sampled_requests, num_requests, request_id_prefix, no_oversample
4501 )
4502 return sampled_requests
4503
4504
4505 # -----------------------------------------------------------------------------
4506 # BFCL (Berkeley Function Calling Leaderboard) Dataset Implementation
4507 # -----------------------------------------------------------------------------
4508
4509
4510 class BFCLDataset(HuggingFaceDataset):
4511 """Berkeley Function Calling Leaderboard dataset.
4512
4513 https://huggingface.co/datasets/gorilla-llm/Berkeley-Function-Calling-Leaderboard
4514
4515 BFCL ships one JSON-lines file per category at the repo root (e.g.
4516 ``BFCL_v3_simple.json``, ``BFCL_v3_live_simple.json``) rather than a
4517 single HuggingFace split. Each record has ``{id, question, function}``
4518 where ``function`` uses a non-OpenAI schema dialect (``"type": "dict"``).
4519
4520 This dataset loader:
4521 - downloads the selected per-category files via ``hf_hub_download``
4522 and interleaves rows round-robin so sampling is balanced
4523 - translates BFCL function schemas to OpenAI tool format
4524 - sets :attr:`SampleRequest.chat_messages` directly and attaches
4525 ``tools`` / ``tool_choice`` via :attr:`SampleRequest.request_overrides`,
4526 producing production-alike tool calling traffic when used with an
4527 ``openai-chat`` backend
4528 """
4529
4530 DEFAULT_OUTPUT_LEN = 512
4531 DEFAULT_CATEGORIES = ("simple", "live_simple", "multiple")
4532 SUPPORTED_DATASET_PATHS = {
4533 "gorilla-llm/Berkeley-Function-Calling-Leaderboard",
4534 }
4535 IS_MULTIMODAL = False
4536
4537 # BFCL primitive type names that are not valid JSON Schema types.
4538 # Map them to the closest JSON Schema equivalent so that grammar
4539 # backends (xgrammar, outlines) accept the translated tool schema.
4540 _TYPE_REMAP = {
4541 "dict": "object",
4542 "float": "number",
4543 "tuple": "array",
4544 "any": "string",
4545 }
4546
4547 def load_data(self) -> None:
4548 """Defer loading to :meth:`sample` where categories are known."""
4549 self.data = None
4550
4551 def _resolve_categories(self, categories: list[str] | None) -> list[str]:
4552 if not categories:
4553 return list(self.DEFAULT_CATEGORIES)
4554 resolved: list[str] = []
4555 for c in categories:
4556 c = c.strip()
4557 if not c:
4558 continue
4559 resolved.append(c)
4560 return resolved or list(self.DEFAULT_CATEGORIES)
4561
4562 def _load_category(self, category: str) -> list[dict]:
4563 # Local import: huggingface_hub.errors is a small module and
4564 # importing at call site keeps module import cheap for users who
4565 # never touch BFCL.
4566 from huggingface_hub.errors import EntryNotFoundError
4567
4568 filename = f"BFCL_v3_{category}.json"
4569 try:
4570 path = hf_api().hf_hub_download(
4571 self.dataset_path, filename, repo_type="dataset"
4572 )
4573 except EntryNotFoundError as e:
4574 defaults = ", ".join(self.DEFAULT_CATEGORIES)
4575 raise ValueError(
4576 f"BFCL category '{category}' not found: file '{filename}' "
4577 f"does not exist in {self.dataset_path}. Check --bfcl-categories "
4578 f"(defaults: {defaults})."
4579 ) from e
4580 rows: list[dict] = []
4581 with open(path) as f:
4582 for line in f:
4583 line = line.strip()
4584 if not line:
4585 continue
4586 rows.append(json.loads(line))
4587 return rows
4588
4589 @classmethod
4590 def _translate_schema(cls, node: Any) -> Any:
4591 """Recursively translate BFCL-flavored JSON schema to strict JSON Schema."""
4592 if isinstance(node, dict):
4593 translated = {k: cls._translate_schema(v) for k, v in node.items()}
4594 t = translated.get("type")
4595 if isinstance(t, str) and t in cls._TYPE_REMAP:
4596 translated["type"] = cls._TYPE_REMAP[t]
4597 return translated
4598 if isinstance(node, list):
4599 return [cls._translate_schema(v) for v in node]
4600 return node
4601
4602 @classmethod
4603 def _to_openai_tools(cls, functions: list[dict]) -> list[dict]:
4604 tools: list[dict] = []
4605 for fn in functions:
4606 translated = cls._translate_schema(fn)
4607 tools.append({"type": "function", "function": translated})
4608 return tools
4609
4610 def sample(
4611 self,
4612 tokenizer: TokenizerLike,
4613 num_requests: int,
4614 request_id_prefix: str = "",
4615 no_oversample: bool = False,
4616 output_len: int | None = None,
4617 categories: list[str] | None = None,
4618 **kwargs,
4619 ) -> list[SampleRequest]:
4620 output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
4621 categories = self._resolve_categories(categories)
4622
4623 per_category_rows: list[list[dict]] = [
4624 self._load_category(c) for c in categories
4625 ]
4626 # Round-robin interleave so that when --disable-shuffle is set,
4627 # taking the first num_requests rows still yields balanced category
4628 # coverage. When shuffle is on (the default) this ordering is
4629 # randomized away, which is fine — the subsequent random sample is
4630 # already balanced in expectation.
4631 interleaved: list[dict] = []
4632 max_len = max((len(rows) for rows in per_category_rows), default=0)
4633 for i in range(max_len):
4634 for rows in per_category_rows:
4635 if i < len(rows):
4636 interleaved.append(rows[i])
4637
4638 if not self.disable_shuffle:
4639 rng = random.Random(self.random_seed)
4640 rng.shuffle(interleaved)
4641
4642 sampled_requests: list[SampleRequest] = []
4643 for row in interleaved:
4644 if len(sampled_requests) >= num_requests:
4645 break
4646 question = row.get("question")
4647 functions = row.get("function")
4648 if not question or not functions:
4649 continue
4650 # BFCL question is list[list[dict]] — outer is turns. Use the
4651 # first turn only; skip multi-turn categories in this loader.
4652 if not isinstance(question, list) or not question:
4653 continue
4654 first_turn = question[0]
4655 if not isinstance(first_turn, list) or not first_turn:
4656 continue
4657 messages = first_turn
4658 if not isinstance(functions, list):
4659 functions = [functions]
4660
4661 tools = self._to_openai_tools(functions)
4662
4663 # Best-effort prompt length for percentile bucketing. Pass tools=
4664 # so modern chat templates (Llama 3.1+, Qwen, gpt-oss harmony,
4665 # Hermes) render the tool schemas — without this, the estimate
4666 # misses a significant chunk of the true input for BFCL traffic.
4667 # Older tokenizers reject the kwarg; fall back to tools-free.
4668 try:
4669 rendered = tokenizer.apply_chat_template(
4670 messages,
4671 tools=tools,
4672 tokenize=False,
4673 add_generation_prompt=True,
4674 )
4675 except TypeError:
4676 rendered = tokenizer.apply_chat_template(
4677 messages, tokenize=False, add_generation_prompt=True
4678 )
4679 except Exception as e:
4680 # Unexpected template failure — prompt_len will fall back to a
4681 # plain-text concatenation. Log so the degraded estimate is
4682 # visible instead of silently skewing latency buckets.
4683 logger.warning(
4684 "BFCL: apply_chat_template failed for a sample, falling "
4685 "back to plain-text prompt length: %s",
4686 e,
4687 exc_info=True,
4688 )
4689 rendered = None
4690 if rendered is not None and isinstance(rendered, str):
4691 prompt_len = len(tokenizer(rendered).input_ids)
4692 else:
4693 text = "\n".join(m.get("content", "") for m in messages)
4694 prompt_len = len(tokenizer(text).input_ids)
4695
4696 # The chat backend uses `messages` directly; `prompt` is only
4697 # kept as a fallback string for display/debug.
4698 prompt_text = messages[-1].get("content", "") if messages else ""
4699
4700 sampled_requests.append(
4701 SampleRequest(
4702 prompt=prompt_text,
4703 prompt_len=prompt_len,
4704 expected_output_len=output_len,
4705 request_id=request_id_prefix + str(len(sampled_requests)),
4706 chat_messages=messages,
4707 request_overrides={
4708 "tools": tools,
4709 "tool_choice": "auto",
4710 },
4711 )
4712 )
4713
4714 self.maybe_oversample_requests(
4715 sampled_requests, num_requests, request_id_prefix, no_oversample
4716 )
4717 return sampled_requests
4718
4719
4720 # -----------------------------------------------------------------------------
4721 # Speed Bench Dataset Implementation
4722 # -----------------------------------------------------------------------------
4723
4724
4725 class SpeedBench(CustomDataset):
4726 """SPEED-Bench dataset: https://huggingface.co/datasets/nvidia/SPEED-Bench.
4727
4728 Download the dataset using:
4729
4730 `curl -LsSf https://raw.githubusercontent.com/NVIDIA-NeMo/Skills/refs/heads/main/nemo_skills/dataset/speed-bench/prepare.py | python3 -`
4731 """ # noqa: E501
4732
4733 DOWNLOAD_SCRIPT_URL = "https://raw.githubusercontent.com/NVIDIA-NeMo/Skills/refs/heads/main/nemo_skills/dataset/speed-bench/prepare.py"
4734
4735 def __init__(self, **kwargs) -> None:
4736 self.dataset_subset = kwargs.pop("dataset_subset", "qualitative")
4737 self.category = kwargs.pop("category", None)
4738 super().__init__(**kwargs)
4739 self.load_data()
4740
4741 def load_data(self) -> None:
4742 if self.dataset_path is None:
4743 raise ValueError("dataset_path must be provided for loading data.")
4744
4745 if not Path(self.dataset_path).is_dir():
4746 raise ValueError(
4747 f"dataset_path {self.dataset_path} is not a directory. "
4748 f"Please make sure to download the dataset from HuggingFace using "
4749 f"`curl -LsSf {self.DOWNLOAD_SCRIPT_URL} | python3 -`"
4750 )
4751
4752 self.data = []
4753
4754 # Load the JSONL file
4755 jsonl_data = pd.read_json(
4756 path_or_buf=Path(self.dataset_path) / f"{self.dataset_subset}.jsonl",
4757 lines=True,
4758 )
4759
4760 # check if the JSONL file has a 'turns' column
4761 if "messages" not in jsonl_data.columns:
4762 raise ValueError(
4763 "JSONL file must contain a 'messages' column. "
4764 "Please make sure to download the dataset from HuggingFace using "
4765 f"`curl -LsSf {self.DOWNLOAD_SCRIPT_URL} | python3 -`"
4766 )
4767
4768 for _, row in jsonl_data.iterrows():
4769 # sample only from a specific category if specified
4770 if (not self.category) or (self.category == row["category"]):
4771 prompt = row["messages"][0]["content"]
4772 self.data.append({"prompt": prompt})
4773
4774 random.seed(self.random_seed)
4775 if not getattr(self, "disable_shuffle", False):
4776 random.shuffle(self.data)