Skip to content

Commit ecf2984

Browse files
parfeniukinkDmytro Parfeniuk
andauthored
[Dataset]: Iterate through benchmark dataset once (#48)
issue link: #44 ### Summary: * `--max-requests` CLI parameter now supports the `dataset` value. * `MaxRequestsType(ParamType)` is used as a custom `click` param type to validate input data * `RequestGenerator(ABC)` now includes the abstract `__len__` that corresponds to the length of the dataset if it is supported. * Unit tests are added * Smoke tests are added * `guidellm/utils/text.py` is fixed. There is no reason to check if the file start with the `'http'` string. Basically this is a Bug since you never read a text file properly because of old `if/else` condition. --------- Co-authored-by: Dmytro Parfeniuk <parfeniukinik@gmail.com>
1 parent 474ad29 commit ecf2984

File tree

18 files changed

+410
-35
lines changed

18 files changed

+410
-35
lines changed

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ exclude = ["venv", ".tox"]
104104
# Check: https://mypy.readthedocs.io/en/latest/config_file.html#import-discovery
105105
follow_imports = 'silent'
106106

107+
[[tool.mypy.overrides]]
108+
module = ["datasets.*"]
109+
ignore_missing_imports=true
110+
107111

108112
[tool.ruff]
109113
line-length = 88
@@ -122,6 +126,8 @@ ignore = [
122126
"ISC001",
123127
"TCH002",
124128
"PLW1514", # allow Path.open without encoding
129+
"RET505", # allow `else` blocks
130+
"RET506" # allow `else` blocks
125131

126132
]
127133
select = [

src/guidellm/backend/openai.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ async def make_request(
111111
stream=True,
112112
**request_args,
113113
)
114+
114115
token_count = 0
115116
async for chunk in stream:
116117
choice = chunk.choices[0]

src/guidellm/executor/profile_generator.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from typing import Any, Dict, Literal, Optional, Sequence, Union, get_args
1+
from typing import Any, Dict, List, Literal, Optional, Sequence, Union, get_args
22

33
import numpy as np
44
from loguru import logger
5+
from numpy._typing import NDArray
56
from pydantic import Field
67

78
from guidellm.config import settings
@@ -190,12 +191,14 @@ def next(self, current_report: TextGenerationBenchmarkReport) -> Optional[Profil
190191
elif self.mode == "sweep":
191192
profile = self.create_sweep_profile(
192193
self.generated_count,
193-
sync_benchmark=current_report.benchmarks[0]
194-
if current_report.benchmarks
195-
else None,
196-
throughput_benchmark=current_report.benchmarks[1]
197-
if len(current_report.benchmarks) > 1
198-
else None,
194+
sync_benchmark=(
195+
current_report.benchmarks[0] if current_report.benchmarks else None
196+
),
197+
throughput_benchmark=(
198+
current_report.benchmarks[1]
199+
if len(current_report.benchmarks) > 1
200+
else None
201+
),
199202
)
200203
else:
201204
err = ValueError(f"Invalid mode: {self.mode}")
@@ -333,11 +336,15 @@ def create_sweep_profile(
333336

334337
min_rate = sync_benchmark.completed_request_rate
335338
max_rate = throughput_benchmark.completed_request_rate
336-
intermediate_rates = list(
339+
intermediate_rates: List[NDArray] = list(
337340
np.linspace(min_rate, max_rate, settings.num_sweep_profiles + 1)
338341
)[1:]
339342

340343
return Profile(
341344
load_gen_mode="constant",
342-
load_gen_rate=intermediate_rates[index - 2],
345+
load_gen_rate=(
346+
float(load_gen_rate)
347+
if (load_gen_rate := intermediate_rates[index - 2])
348+
else 1.0 # the fallback value
349+
),
343350
)

src/guidellm/main.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import asyncio
2-
from typing import Literal, Optional, get_args
2+
from typing import Literal, Optional, Union, get_args
33

44
import click
55
from loguru import logger
@@ -13,7 +13,7 @@
1313
TransformersDatasetRequestGenerator,
1414
)
1515
from guidellm.request.base import RequestGenerator
16-
from guidellm.utils import BenchmarkReportProgress
16+
from guidellm.utils import BenchmarkReportProgress, cli_params
1717

1818
__all__ = ["generate_benchmark_report"]
1919

@@ -120,7 +120,7 @@
120120
)
121121
@click.option(
122122
"--max-requests",
123-
type=int,
123+
type=cli_params.MAX_REQUESTS,
124124
default=None,
125125
help=(
126126
"The maximum number of requests for each benchmark run. "
@@ -161,7 +161,7 @@ def generate_benchmark_report_cli(
161161
rate_type: ProfileGenerationMode,
162162
rate: Optional[float],
163163
max_seconds: Optional[int],
164-
max_requests: Optional[int],
164+
max_requests: Union[Literal["dataset"], int, None],
165165
output_path: str,
166166
enable_continuous_refresh: bool,
167167
):
@@ -194,7 +194,7 @@ def generate_benchmark_report(
194194
rate_type: ProfileGenerationMode,
195195
rate: Optional[float],
196196
max_seconds: Optional[int],
197-
max_requests: Optional[int],
197+
max_requests: Union[Literal["dataset"], int, None],
198198
output_path: str,
199199
cont_refresh_table: bool,
200200
) -> GuidanceReport:
@@ -256,13 +256,18 @@ def generate_benchmark_report(
256256
else:
257257
raise ValueError(f"Unknown data type: {data_type}")
258258

259+
if data_type == "emulated" and max_requests == "dataset":
260+
raise ValueError("Cannot use 'dataset' for emulated data")
261+
259262
# Create executor
260263
executor = Executor(
261264
backend=backend_inst,
262265
request_generator=request_generator,
263266
mode=rate_type,
264267
rate=rate if rate_type in ("constant", "poisson") else None,
265-
max_number=max_requests,
268+
max_number=(
269+
len(request_generator) if max_requests == "dataset" else max_requests
270+
),
266271
max_duration=max_seconds,
267272
)
268273

src/guidellm/request/base.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,21 @@ def __iter__(self) -> Iterator[TextGenerationRequest]:
105105
while not self._stop_event.is_set():
106106
yield self.create_item()
107107

108+
@abstractmethod
109+
def __len__(self) -> int:
110+
"""
111+
Abstract method to get the length of the collection to be generated.
112+
"""
113+
114+
@abstractmethod
115+
def create_item(self) -> TextGenerationRequest:
116+
"""
117+
Abstract method to create a new result request item.
118+
119+
:return: A new result request.
120+
:rtype: TextGenerationRequest
121+
"""
122+
108123
@property
109124
def type_(self) -> str:
110125
"""
@@ -155,15 +170,6 @@ def async_queue_size(self) -> int:
155170
"""
156171
return self._async_queue_size
157172

158-
@abstractmethod
159-
def create_item(self) -> TextGenerationRequest:
160-
"""
161-
Abstract method to create a new result request item.
162-
163-
:return: A new result request.
164-
:rtype: TextGenerationRequest
165-
"""
166-
167173
def stop(self):
168174
"""
169175
Stop the background task that populates the queue.

src/guidellm/request/emulated.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,12 @@ def __init__(
339339
async_queue_size=async_queue_size,
340340
)
341341

342+
def __len__(self) -> int:
343+
raise NotImplementedError(
344+
"Can't get the length of the emulated dataset. "
345+
"Check the `--data-type` CLI parameter."
346+
)
347+
342348
def create_item(self) -> TextGenerationRequest:
343349
"""
344350
Create a new text generation request item from the data.

src/guidellm/request/file.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ def __init__(
5454
async_queue_size=async_queue_size,
5555
)
5656

57+
def __len__(self) -> int:
58+
"""
59+
Return the number of text lines.
60+
"""
61+
62+
return len(self._data)
63+
5764
def create_item(self) -> TextGenerationRequest:
5865
"""
5966
Create a new result request item from the data.

src/guidellm/request/transformers.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
from pathlib import Path
22
from typing import Optional, Union
33

4-
from datasets import ( # type: ignore # noqa: PGH003
5-
Dataset,
6-
DatasetDict,
7-
IterableDataset,
8-
IterableDatasetDict,
9-
)
4+
from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict
105
from loguru import logger
116
from transformers import PreTrainedTokenizer # type: ignore # noqa: PGH003
127

@@ -57,7 +52,9 @@ def __init__(
5752
self._column = column
5853
self._kwargs = kwargs
5954

60-
self._hf_dataset = load_transformers_dataset(dataset, split=split, **kwargs)
55+
self._hf_dataset: Union[Dataset, IterableDataset] = load_transformers_dataset(
56+
dataset, split=split, **kwargs
57+
)
6158
self._hf_column = resolve_transformers_dataset_column(
6259
self._hf_dataset, column=column
6360
)
@@ -73,6 +70,12 @@ def __init__(
7370
async_queue_size=async_queue_size,
7471
)
7572

73+
def __len__(self) -> int:
74+
if not isinstance(self._hf_dataset, Dataset):
75+
raise ValueError("Can't get dataset size for IterableDataset object")
76+
else:
77+
return len(self._hf_dataset)
78+
7679
def create_item(self) -> TextGenerationRequest:
7780
"""
7881
Create a new result request item from the dataset.

src/guidellm/utils/cli_params.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
This module includes custom CLI parameters for the `click` package.
3+
"""
4+
5+
from typing import Any, Optional
6+
7+
from click import Context, Parameter, ParamType
8+
9+
__all__ = ["MAX_REQUESTS"]
10+
11+
12+
class MaxRequestsType(ParamType):
13+
"""
14+
Catch the `dataset` string parameter to determine the behavior of the Scheduler.
15+
"""
16+
17+
name = "max_requests"
18+
19+
def convert(
20+
self, value: Any, param: Optional[Parameter], ctx: Optional[Context]
21+
) -> Any:
22+
if isinstance(value, int):
23+
return value
24+
25+
try:
26+
return int(value)
27+
except ValueError:
28+
if value == "dataset":
29+
return value
30+
else:
31+
self.fail(f"{value} is not a valid integer or 'dataset'", param, ctx)
32+
33+
34+
MAX_REQUESTS = MaxRequestsType()

src/guidellm/utils/progress.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def update_benchmark(
139139
:type req_per_sec: float
140140
:raises ValueError: If trying to update a completed benchmark.
141141
"""
142+
142143
if self.benchmark_tasks_completed[index]:
143144
err = ValueError(f"Benchmark {index} already completed")
144145
logger.error("Error updating benchmark: {}", err)
@@ -162,9 +163,11 @@ def update_benchmark(
162163
total=completed_total,
163164
completed=completed_count if not completed else completed_total,
164165
req_per_sec=(f"{req_per_sec:.2f}" if req_per_sec else "#.##"),
165-
start_time_str=datetime.fromtimestamp(start_time).strftime("%H:%M:%S")
166-
if start_time
167-
else "--:--:--",
166+
start_time_str=(
167+
datetime.fromtimestamp(start_time).strftime("%H:%M:%S")
168+
if start_time
169+
else "--:--:--"
170+
),
168171
)
169172
logger.debug(
170173
"Updated benchmark task at index {}: {}% complete",

0 commit comments

Comments
 (0)