guidellm 0.3.1__py3-none-any.whl → 0.6.0a5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. guidellm/__init__.py +5 -2
  2. guidellm/__main__.py +524 -255
  3. guidellm/backends/__init__.py +33 -0
  4. guidellm/backends/backend.py +109 -0
  5. guidellm/backends/openai.py +340 -0
  6. guidellm/backends/response_handlers.py +428 -0
  7. guidellm/benchmark/__init__.py +69 -39
  8. guidellm/benchmark/benchmarker.py +160 -316
  9. guidellm/benchmark/entrypoints.py +560 -127
  10. guidellm/benchmark/outputs/__init__.py +24 -0
  11. guidellm/benchmark/outputs/console.py +633 -0
  12. guidellm/benchmark/outputs/csv.py +721 -0
  13. guidellm/benchmark/outputs/html.py +473 -0
  14. guidellm/benchmark/outputs/output.py +169 -0
  15. guidellm/benchmark/outputs/serialized.py +69 -0
  16. guidellm/benchmark/profiles.py +718 -0
  17. guidellm/benchmark/progress.py +553 -556
  18. guidellm/benchmark/scenarios/__init__.py +40 -0
  19. guidellm/benchmark/scenarios/chat.json +6 -0
  20. guidellm/benchmark/scenarios/rag.json +6 -0
  21. guidellm/benchmark/schemas/__init__.py +66 -0
  22. guidellm/benchmark/schemas/base.py +402 -0
  23. guidellm/benchmark/schemas/generative/__init__.py +55 -0
  24. guidellm/benchmark/schemas/generative/accumulator.py +841 -0
  25. guidellm/benchmark/schemas/generative/benchmark.py +163 -0
  26. guidellm/benchmark/schemas/generative/entrypoints.py +381 -0
  27. guidellm/benchmark/schemas/generative/metrics.py +927 -0
  28. guidellm/benchmark/schemas/generative/report.py +158 -0
  29. guidellm/data/__init__.py +34 -4
  30. guidellm/data/builders.py +541 -0
  31. guidellm/data/collators.py +16 -0
  32. guidellm/data/config.py +120 -0
  33. guidellm/data/deserializers/__init__.py +49 -0
  34. guidellm/data/deserializers/deserializer.py +141 -0
  35. guidellm/data/deserializers/file.py +223 -0
  36. guidellm/data/deserializers/huggingface.py +94 -0
  37. guidellm/data/deserializers/memory.py +194 -0
  38. guidellm/data/deserializers/synthetic.py +246 -0
  39. guidellm/data/entrypoints.py +52 -0
  40. guidellm/data/loaders.py +190 -0
  41. guidellm/data/preprocessors/__init__.py +27 -0
  42. guidellm/data/preprocessors/formatters.py +410 -0
  43. guidellm/data/preprocessors/mappers.py +196 -0
  44. guidellm/data/preprocessors/preprocessor.py +30 -0
  45. guidellm/data/processor.py +29 -0
  46. guidellm/data/schemas.py +175 -0
  47. guidellm/data/utils/__init__.py +6 -0
  48. guidellm/data/utils/dataset.py +94 -0
  49. guidellm/extras/__init__.py +4 -0
  50. guidellm/extras/audio.py +220 -0
  51. guidellm/extras/vision.py +242 -0
  52. guidellm/logger.py +2 -2
  53. guidellm/mock_server/__init__.py +8 -0
  54. guidellm/mock_server/config.py +84 -0
  55. guidellm/mock_server/handlers/__init__.py +17 -0
  56. guidellm/mock_server/handlers/chat_completions.py +280 -0
  57. guidellm/mock_server/handlers/completions.py +280 -0
  58. guidellm/mock_server/handlers/tokenizer.py +142 -0
  59. guidellm/mock_server/models.py +510 -0
  60. guidellm/mock_server/server.py +238 -0
  61. guidellm/mock_server/utils.py +302 -0
  62. guidellm/scheduler/__init__.py +69 -26
  63. guidellm/scheduler/constraints/__init__.py +49 -0
  64. guidellm/scheduler/constraints/constraint.py +325 -0
  65. guidellm/scheduler/constraints/error.py +411 -0
  66. guidellm/scheduler/constraints/factory.py +182 -0
  67. guidellm/scheduler/constraints/request.py +312 -0
  68. guidellm/scheduler/constraints/saturation.py +722 -0
  69. guidellm/scheduler/environments.py +252 -0
  70. guidellm/scheduler/scheduler.py +137 -368
  71. guidellm/scheduler/schemas.py +358 -0
  72. guidellm/scheduler/strategies.py +617 -0
  73. guidellm/scheduler/worker.py +413 -419
  74. guidellm/scheduler/worker_group.py +712 -0
  75. guidellm/schemas/__init__.py +65 -0
  76. guidellm/schemas/base.py +417 -0
  77. guidellm/schemas/info.py +188 -0
  78. guidellm/schemas/request.py +235 -0
  79. guidellm/schemas/request_stats.py +349 -0
  80. guidellm/schemas/response.py +124 -0
  81. guidellm/schemas/statistics.py +1018 -0
  82. guidellm/{config.py → settings.py} +31 -24
  83. guidellm/utils/__init__.py +71 -8
  84. guidellm/utils/auto_importer.py +98 -0
  85. guidellm/utils/cli.py +132 -5
  86. guidellm/utils/console.py +566 -0
  87. guidellm/utils/encoding.py +778 -0
  88. guidellm/utils/functions.py +159 -0
  89. guidellm/utils/hf_datasets.py +1 -2
  90. guidellm/utils/hf_transformers.py +4 -4
  91. guidellm/utils/imports.py +9 -0
  92. guidellm/utils/messaging.py +1118 -0
  93. guidellm/utils/mixins.py +115 -0
  94. guidellm/utils/random.py +3 -4
  95. guidellm/utils/registry.py +220 -0
  96. guidellm/utils/singleton.py +133 -0
  97. guidellm/utils/synchronous.py +159 -0
  98. guidellm/utils/text.py +163 -50
  99. guidellm/utils/typing.py +41 -0
  100. guidellm/version.py +2 -2
  101. guidellm-0.6.0a5.dist-info/METADATA +364 -0
  102. guidellm-0.6.0a5.dist-info/RECORD +109 -0
  103. guidellm/backend/__init__.py +0 -23
  104. guidellm/backend/backend.py +0 -259
  105. guidellm/backend/openai.py +0 -708
  106. guidellm/backend/response.py +0 -136
  107. guidellm/benchmark/aggregator.py +0 -760
  108. guidellm/benchmark/benchmark.py +0 -837
  109. guidellm/benchmark/output.py +0 -997
  110. guidellm/benchmark/profile.py +0 -409
  111. guidellm/benchmark/scenario.py +0 -104
  112. guidellm/data/prideandprejudice.txt.gz +0 -0
  113. guidellm/dataset/__init__.py +0 -22
  114. guidellm/dataset/creator.py +0 -213
  115. guidellm/dataset/entrypoints.py +0 -42
  116. guidellm/dataset/file.py +0 -92
  117. guidellm/dataset/hf_datasets.py +0 -62
  118. guidellm/dataset/in_memory.py +0 -132
  119. guidellm/dataset/synthetic.py +0 -287
  120. guidellm/objects/__init__.py +0 -18
  121. guidellm/objects/pydantic.py +0 -89
  122. guidellm/objects/statistics.py +0 -953
  123. guidellm/preprocess/__init__.py +0 -3
  124. guidellm/preprocess/dataset.py +0 -374
  125. guidellm/presentation/__init__.py +0 -28
  126. guidellm/presentation/builder.py +0 -27
  127. guidellm/presentation/data_models.py +0 -232
  128. guidellm/presentation/injector.py +0 -66
  129. guidellm/request/__init__.py +0 -18
  130. guidellm/request/loader.py +0 -284
  131. guidellm/request/request.py +0 -79
  132. guidellm/request/types.py +0 -10
  133. guidellm/scheduler/queues.py +0 -25
  134. guidellm/scheduler/result.py +0 -155
  135. guidellm/scheduler/strategy.py +0 -495
  136. guidellm-0.3.1.dist-info/METADATA +0 -329
  137. guidellm-0.3.1.dist-info/RECORD +0 -62
  138. {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/WHEEL +0 -0
  139. {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/entry_points.txt +0 -0
  140. {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/licenses/LICENSE +0 -0
  141. {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/top_level.txt +0 -0
@@ -1,997 +0,0 @@
1
- import csv
2
- import json
3
- import math
4
- from collections import OrderedDict
5
- from copy import deepcopy
6
- from datetime import datetime
7
- from pathlib import Path
8
- from typing import Any, Literal, Optional, Union
9
-
10
- import yaml
11
- from pydantic import Field
12
- from rich.console import Console
13
- from rich.padding import Padding
14
- from rich.text import Text
15
-
16
- from guidellm.benchmark.benchmark import GenerativeBenchmark, GenerativeMetrics
17
- from guidellm.benchmark.profile import (
18
- AsyncProfile,
19
- ConcurrentProfile,
20
- SweepProfile,
21
- ThroughputProfile,
22
- )
23
- from guidellm.config import settings
24
- from guidellm.objects import (
25
- DistributionSummary,
26
- StandardBaseModel,
27
- StatusDistributionSummary,
28
- )
29
- from guidellm.presentation import UIDataBuilder
30
- from guidellm.presentation.injector import create_report
31
- from guidellm.scheduler import strategy_display_str
32
- from guidellm.utils import Colors, split_text_list_by_length
33
- from guidellm.utils.dict import recursive_key_update
34
- from guidellm.utils.text import camelize_str
35
-
36
- __all__ = [
37
- "GenerativeBenchmarksConsole",
38
- "GenerativeBenchmarksReport",
39
- ]
40
-
41
-
42
- class GenerativeBenchmarksReport(StandardBaseModel):
43
- """
44
- A pydantic model representing a completed benchmark report.
45
- Contains a list of benchmarks along with convenience methods for finalizing
46
- and saving the report.
47
- """
48
-
49
- @staticmethod
50
- def load_file(path: Union[str, Path]) -> "GenerativeBenchmarksReport":
51
- """
52
- Load a report from a file. The file type is determined by the file extension.
53
- If the file is a directory, it expects a file named benchmarks.json under the
54
- directory.
55
-
56
- :param path: The path to load the report from.
57
- :return: The loaded report.
58
- """
59
- path, type_ = GenerativeBenchmarksReport._file_setup(path)
60
-
61
- if type_ == "json":
62
- with path.open("r") as file:
63
- model_dict = json.load(file)
64
-
65
- return GenerativeBenchmarksReport.model_validate(model_dict)
66
-
67
- if type_ == "yaml":
68
- with path.open("r") as file:
69
- model_dict = yaml.safe_load(file)
70
-
71
- return GenerativeBenchmarksReport.model_validate(model_dict)
72
-
73
- if type_ == "csv":
74
- raise ValueError(f"CSV file type is not supported for loading: {path}.")
75
-
76
- if type_ == "html":
77
- raise ValueError(f"HTML file type is not supported for loading: {path}.")
78
-
79
- raise ValueError(f"Unsupported file type: {type_} for {path}.")
80
-
81
- benchmarks: list[GenerativeBenchmark] = Field(
82
- description="The list of completed benchmarks contained within the report.",
83
- default_factory=list,
84
- )
85
-
86
- def set_sample_size(
87
- self, sample_size: Optional[int]
88
- ) -> "GenerativeBenchmarksReport":
89
- """
90
- Set the sample size for each benchmark in the report. In doing this, it will
91
- reduce the contained requests of each benchmark to the sample size.
92
- If sample size is None, it will return the report as is.
93
-
94
- :param sample_size: The sample size to set for each benchmark.
95
- If None, the report will be returned as is.
96
- :return: The report with the sample size set for each benchmark.
97
- """
98
-
99
- if sample_size is not None:
100
- for benchmark in self.benchmarks:
101
- benchmark.set_sample_size(sample_size)
102
-
103
- return self
104
-
105
- def save_file(self, path: Union[str, Path]) -> Path:
106
- """
107
- Save the report to a file. The file type is determined by the file extension.
108
- If the file is a directory, it will save the report to a file named
109
- benchmarks.json under the directory.
110
-
111
- :param path: The path to save the report to.
112
- :return: The path to the saved report.
113
- """
114
- path, type_ = GenerativeBenchmarksReport._file_setup(path)
115
-
116
- if type_ == "json":
117
- return self.save_json(path)
118
-
119
- if type_ == "yaml":
120
- return self.save_yaml(path)
121
-
122
- if type_ == "csv":
123
- return self.save_csv(path)
124
-
125
- if type_ == "html":
126
- return self.save_html(path)
127
-
128
- raise ValueError(f"Unsupported file type: {type_} for {path}.")
129
-
130
- def save_json(self, path: Union[str, Path]) -> Path:
131
- """
132
- Save the report to a JSON file containing all of the report data which is
133
- reloadable using the pydantic model. If the file is a directory, it will save
134
- the report to a file named benchmarks.json under the directory.
135
-
136
- :param path: The path to save the report to.
137
- :return: The path to the saved report.
138
- """
139
- path, type_ = GenerativeBenchmarksReport._file_setup(path, "json")
140
-
141
- if type_ != "json":
142
- raise ValueError(
143
- f"Unsupported file type for saving a JSON: {type_} for {path}."
144
- )
145
-
146
- model_dict = self.model_dump()
147
-
148
- with path.open("w", encoding="utf-8") as file:
149
- json.dump(model_dict, file, ensure_ascii=False, indent=4)
150
-
151
- return path
152
-
153
- def save_yaml(self, path: Union[str, Path]) -> Path:
154
- """
155
- Save the report to a YAML file containing all of the report data which is
156
- reloadable using the pydantic model. If the file is a directory, it will save
157
- the report to a file named benchmarks.yaml under the directory.
158
-
159
- :param path: The path to save the report to.
160
- :return: The path to the saved report.
161
- """
162
-
163
- path, type_ = GenerativeBenchmarksReport._file_setup(path, "yaml")
164
-
165
- if type_ != "yaml":
166
- raise ValueError(
167
- f"Unsupported file type for saving a YAML: {type_} for {path}."
168
- )
169
-
170
- model_dict = self.model_dump()
171
- model_yaml = yaml.dump(model_dict)
172
-
173
- with path.open("w") as file:
174
- file.write(model_yaml)
175
-
176
- return path
177
-
178
- def save_csv(self, path: Union[str, Path]) -> Path:
179
- """
180
- Save the report to a CSV file containing the summarized statistics and values
181
- for each report. Note, this data is not reloadable using the pydantic model.
182
- If the file is a directory, it will save the report to a file named
183
- benchmarks.csv under the directory.
184
-
185
- :param path: The path to save the report to.
186
- :return: The path to the saved report.
187
- """
188
- path, type_ = GenerativeBenchmarksReport._file_setup(path, "csv")
189
-
190
- if type_ != "csv":
191
- raise ValueError(
192
- f"Unsupported file type for saving a CSV: {type_} for {path}."
193
- )
194
-
195
- with path.open("w", newline="") as file:
196
- writer = csv.writer(file)
197
- headers: list[str] = []
198
- rows: list[list[Union[str, float, list[float]]]] = []
199
-
200
- for benchmark in self.benchmarks:
201
- benchmark_headers: list[str] = []
202
- benchmark_values: list[Union[str, float, list[float]]] = []
203
-
204
- desc_headers, desc_values = self._benchmark_desc_headers_and_values(
205
- benchmark
206
- )
207
- benchmark_headers += desc_headers
208
- benchmark_values += desc_values
209
-
210
- for status in StatusDistributionSummary.model_fields:
211
- status_headers, status_values = (
212
- self._benchmark_status_headers_and_values(benchmark, status)
213
- )
214
- benchmark_headers += status_headers
215
- benchmark_values += status_values
216
-
217
- benchmark_extra_headers, benchmark_extra_values = (
218
- self._benchmark_extras_headers_and_values(benchmark)
219
- )
220
- benchmark_headers += benchmark_extra_headers
221
- benchmark_values += benchmark_extra_values
222
-
223
- if not headers:
224
- headers = benchmark_headers
225
- rows.append(benchmark_values)
226
-
227
- writer.writerow(headers)
228
- for row in rows:
229
- writer.writerow(row)
230
-
231
- return path
232
-
233
- def save_html(self, path: Union[str, Path]) -> Path:
234
- """
235
- Download html, inject report data and save to a file.
236
-
237
- :param path: The path to create the report at.
238
- :return: The path to the report.
239
- """
240
-
241
- data_builder = UIDataBuilder(self.benchmarks)
242
- data = data_builder.to_dict()
243
- camel_data = recursive_key_update(deepcopy(data), camelize_str)
244
- ui_api_data = {}
245
- for k, v in camel_data.items():
246
- key = f"window.{k} = {{}};"
247
- value = f"window.{k} = {json.dumps(v, indent=2)};\n"
248
- ui_api_data[key] = value
249
- return create_report(ui_api_data, path)
250
-
251
- @staticmethod
252
- def _file_setup(
253
- path: Union[str, Path],
254
- default_file_type: Literal["json", "yaml", "csv", "html"] = "json",
255
- ) -> tuple[Path, Literal["json", "yaml", "csv", "html"]]:
256
- path = Path(path) if not isinstance(path, Path) else path
257
-
258
- if path.is_dir():
259
- path = path / f"benchmarks.{default_file_type}"
260
-
261
- path.parent.mkdir(parents=True, exist_ok=True)
262
- path_suffix = path.suffix.lower()
263
-
264
- if path_suffix == ".json":
265
- return path, "json"
266
-
267
- if path_suffix in [".yaml", ".yml"]:
268
- return path, "yaml"
269
-
270
- if path_suffix in [".csv"]:
271
- return path, "csv"
272
-
273
- if path_suffix in [".html"]:
274
- return path, "html"
275
-
276
- raise ValueError(
277
- f"Unsupported file extension: {path_suffix} for {path}; "
278
- "expected json, yaml, csv, or html."
279
- )
280
-
281
- @staticmethod
282
- def _benchmark_desc_headers_and_values(
283
- benchmark: GenerativeBenchmark,
284
- ) -> tuple[list[str], list[Union[str, float]]]:
285
- headers = [
286
- "Type",
287
- "Run Id",
288
- "Id",
289
- "Name",
290
- "Start Time",
291
- "End Time",
292
- "Duration",
293
- ]
294
- values: list[Union[str, float]] = [
295
- benchmark.type_,
296
- benchmark.run_id,
297
- benchmark.id_,
298
- strategy_display_str(benchmark.args.strategy),
299
- datetime.fromtimestamp(benchmark.start_time).strftime("%Y-%m-%d %H:%M:%S"),
300
- datetime.fromtimestamp(benchmark.end_time).strftime("%Y-%m-%d %H:%M:%S"),
301
- benchmark.duration,
302
- ]
303
-
304
- if len(headers) != len(values):
305
- raise ValueError("Headers and values length mismatch.")
306
-
307
- return headers, values
308
-
309
- @staticmethod
310
- def _benchmark_extras_headers_and_values(
311
- benchmark: GenerativeBenchmark,
312
- ) -> tuple[list[str], list[str]]:
313
- headers = ["Args", "Worker", "Request Loader", "Extras"]
314
- values: list[str] = [
315
- json.dumps(benchmark.args.model_dump()),
316
- json.dumps(benchmark.worker.model_dump()),
317
- json.dumps(benchmark.request_loader.model_dump()),
318
- json.dumps(benchmark.extras),
319
- ]
320
-
321
- if len(headers) != len(values):
322
- raise ValueError("Headers and values length mismatch.")
323
-
324
- return headers, values
325
-
326
- @staticmethod
327
- def _benchmark_status_headers_and_values(
328
- benchmark: GenerativeBenchmark, status: str
329
- ) -> tuple[list[str], list[Union[float, list[float]]]]:
330
- headers = [
331
- f"{status.capitalize()} Requests",
332
- ]
333
- values = [
334
- getattr(benchmark.request_totals, status),
335
- ]
336
-
337
- for metric in GenerativeMetrics.model_fields:
338
- metric_headers, metric_values = (
339
- GenerativeBenchmarksReport._benchmark_status_metrics_stats(
340
- benchmark, status, metric
341
- )
342
- )
343
- headers += metric_headers
344
- values += metric_values
345
-
346
- if len(headers) != len(values):
347
- raise ValueError("Headers and values length mismatch.")
348
-
349
- return headers, values
350
-
351
- @staticmethod
352
- def _benchmark_status_metrics_stats(
353
- benchmark: GenerativeBenchmark,
354
- status: str,
355
- metric: str,
356
- ) -> tuple[list[str], list[Union[float, list[float]]]]:
357
- status_display = status.capitalize()
358
- metric_display = metric.replace("_", " ").capitalize()
359
- status_dist_summary: StatusDistributionSummary = getattr(
360
- benchmark.metrics, metric
361
- )
362
- dist_summary: DistributionSummary = getattr(status_dist_summary, status)
363
- headers = [
364
- f"{status_display} {metric_display} mean",
365
- f"{status_display} {metric_display} median",
366
- f"{status_display} {metric_display} std dev",
367
- (
368
- f"{status_display} {metric_display} "
369
- "[min, 0.1, 1, 5, 10, 25, 75, 90, 95, 99, max]"
370
- ),
371
- ]
372
- values: list[Union[float, list[float]]] = [
373
- dist_summary.mean,
374
- dist_summary.median,
375
- dist_summary.std_dev,
376
- [
377
- dist_summary.min,
378
- dist_summary.percentiles.p001,
379
- dist_summary.percentiles.p01,
380
- dist_summary.percentiles.p05,
381
- dist_summary.percentiles.p10,
382
- dist_summary.percentiles.p25,
383
- dist_summary.percentiles.p75,
384
- dist_summary.percentiles.p90,
385
- dist_summary.percentiles.p95,
386
- dist_summary.percentiles.p99,
387
- dist_summary.max,
388
- ],
389
- ]
390
-
391
- if len(headers) != len(values):
392
- raise ValueError("Headers and values length mismatch.")
393
-
394
- return headers, values
395
-
396
-
397
- class GenerativeBenchmarksConsole:
398
- """
399
- A class for outputting progress and benchmark results to the console.
400
- Utilizes the rich library for formatting, enabling colored and styled output.
401
- """
402
-
403
- def __init__(self, enabled: bool = True):
404
- """
405
- :param enabled: Whether to enable console output. Defaults to True.
406
- If False, all console output will be suppressed.
407
- """
408
- self.enabled = enabled
409
- self.benchmarks: Optional[list[GenerativeBenchmark]] = None
410
- self.console = Console()
411
-
412
- @property
413
- def benchmarks_profile_str(self) -> str:
414
- """
415
- :return: A string representation of the profile used for the benchmarks.
416
- """
417
- profile = self.benchmarks[0].args.profile if self.benchmarks else None
418
-
419
- if profile is None:
420
- return "None"
421
-
422
- profile_args = OrderedDict(
423
- {
424
- "type": profile.type_,
425
- "strategies": profile.strategy_types,
426
- }
427
- )
428
-
429
- if isinstance(profile, ConcurrentProfile):
430
- profile_args["streams"] = str(profile.streams)
431
- elif isinstance(profile, ThroughputProfile):
432
- profile_args["max_concurrency"] = str(profile.max_concurrency)
433
- elif isinstance(profile, AsyncProfile):
434
- profile_args["max_concurrency"] = str(profile.max_concurrency)
435
- profile_args["rate"] = str(profile.rate)
436
- profile_args["initial_burst"] = str(profile.initial_burst)
437
- elif isinstance(profile, SweepProfile):
438
- profile_args["sweep_size"] = str(profile.sweep_size)
439
-
440
- return ", ".join(f"{key}={value}" for key, value in profile_args.items())
441
-
442
- @property
443
- def benchmarks_args_str(self) -> str:
444
- """
445
- :return: A string representation of the arguments used for the benchmarks.
446
- """
447
- args = self.benchmarks[0].args if self.benchmarks else None
448
-
449
- if args is None:
450
- return "None"
451
-
452
- args_dict = OrderedDict(
453
- {
454
- "max_number": args.max_number,
455
- "max_duration": args.max_duration,
456
- "warmup_number": args.warmup_number,
457
- "warmup_duration": args.warmup_duration,
458
- "cooldown_number": args.cooldown_number,
459
- "cooldown_duration": args.cooldown_duration,
460
- }
461
- )
462
-
463
- return ", ".join(f"{key}={value}" for key, value in args_dict.items())
464
-
465
- @property
466
- def benchmarks_worker_desc_str(self) -> str:
467
- """
468
- :return: A string representation of the worker used for the benchmarks.
469
- """
470
- return str(self.benchmarks[0].worker) if self.benchmarks else "None"
471
-
472
- @property
473
- def benchmarks_request_loader_desc_str(self) -> str:
474
- """
475
- :return: A string representation of the request loader used for the benchmarks.
476
- """
477
- return str(self.benchmarks[0].request_loader) if self.benchmarks else "None"
478
-
479
- @property
480
- def benchmarks_extras_str(self) -> str:
481
- """
482
- :return: A string representation of the extras used for the benchmarks.
483
- """
484
- extras = self.benchmarks[0].extras if self.benchmarks else None
485
-
486
- if not extras:
487
- return "None"
488
-
489
- return ", ".join(f"{key}={value}" for key, value in extras.items())
490
-
491
- def print_section_header(self, title: str, indent: int = 0, new_lines: int = 2):
492
- """
493
- Print out a styled section header to the console.
494
- The title is underlined, bolded, and colored with the INFO color.
495
-
496
- :param title: The title of the section.
497
- :param indent: The number of spaces to indent the title.
498
- Defaults to 0.
499
- :param new_lines: The number of new lines to print before the title.
500
- Defaults to 2.
501
- """
502
- self.print_line(
503
- value=f"{title}:",
504
- style=f"bold underline {Colors.INFO}",
505
- indent=indent,
506
- new_lines=new_lines,
507
- )
508
-
509
- def print_labeled_line(
510
- self, label: str, value: str, indent: int = 4, new_lines: int = 0
511
- ):
512
- """
513
- Print out a styled, labeled line (label: value) to the console.
514
- The label is bolded and colored with the INFO color,
515
- and the value is italicized.
516
-
517
- :param label: The label of the line.
518
- :param value: The value of the line.
519
- :param indent: The number of spaces to indent the line.
520
- Defaults to 4.
521
- :param new_lines: The number of new lines to print before the line.
522
- Defaults to 0.
523
- """
524
- self.print_line(
525
- value=[label + ":", value],
526
- style=["bold " + Colors.INFO, "italic"],
527
- new_lines=new_lines,
528
- indent=indent,
529
- )
530
-
531
- def print_line(
532
- self,
533
- value: Union[str, list[str]],
534
- style: Union[str, list[str]] = "",
535
- indent: int = 0,
536
- new_lines: int = 0,
537
- ):
538
- """
539
- Print out a a value to the console as a line with optional indentation.
540
-
541
- :param value: The value to print.
542
- :param style: The style to apply to the value.
543
- Defaults to none.
544
- :param indent: The number of spaces to indent the line.
545
- Defaults to 0.
546
- :param new_lines: The number of new lines to print before the value.
547
- Defaults to 0.
548
- """
549
- if not self.enabled:
550
- return
551
-
552
- text = Text()
553
-
554
- for _ in range(new_lines):
555
- text.append("\n")
556
-
557
- if not isinstance(value, list):
558
- value = [value]
559
-
560
- if not isinstance(style, list):
561
- style = [style for _ in range(len(value))]
562
-
563
- if len(value) != len(style):
564
- raise ValueError(
565
- f"Value and style length mismatch. Value length: {len(value)}, "
566
- f"Style length: {len(style)}."
567
- )
568
-
569
- for val, sty in zip(value, style):
570
- text.append(val, style=sty)
571
-
572
- self.console.print(Padding.indent(text, indent))
573
-
574
- def print_table(
575
- self,
576
- headers: list[str],
577
- rows: list[list[Any]],
578
- title: str,
579
- sections: Optional[dict[str, tuple[int, int]]] = None,
580
- max_char_per_col: int = 2**10,
581
- indent: int = 0,
582
- new_lines: int = 2,
583
- ):
584
- """
585
- Print a table to the console with the given headers and rows.
586
-
587
- :param headers: The headers of the table.
588
- :param rows: The rows of the table.
589
- :param title: The title of the table.
590
- :param sections: The sections of the table grouping columns together.
591
- This is a mapping of the section display name to a tuple of the start and
592
- end column indices. If None, no sections are added (default).
593
- :param max_char_per_col: The maximum number of characters per column.
594
- :param indent: The number of spaces to indent the table.
595
- Defaults to 0.
596
- :param new_lines: The number of new lines to print before the table.
597
- Defaults to 0.
598
- """
599
-
600
- if rows and any(len(row) != len(headers) for row in rows):
601
- raise ValueError(
602
- f"Headers and rows length mismatch. Headers length: {len(headers)}, "
603
- f"Row length: {len(rows[0]) if rows else 'N/A'}."
604
- )
605
-
606
- max_characters_per_column = self.calculate_max_chars_per_column(
607
- headers, rows, sections, max_char_per_col
608
- )
609
-
610
- self.print_section_header(title, indent=indent, new_lines=new_lines)
611
- self.print_table_divider(
612
- max_characters_per_column, include_separators=False, indent=indent
613
- )
614
- if sections:
615
- self.print_table_sections(
616
- sections, max_characters_per_column, indent=indent
617
- )
618
- self.print_table_row(
619
- split_text_list_by_length(headers, max_characters_per_column),
620
- style=f"bold {Colors.INFO}",
621
- indent=indent,
622
- )
623
- self.print_table_divider(
624
- max_characters_per_column, include_separators=True, indent=indent
625
- )
626
- for row in rows:
627
- self.print_table_row(
628
- split_text_list_by_length(row, max_characters_per_column),
629
- style="italic",
630
- indent=indent,
631
- )
632
- self.print_table_divider(
633
- max_characters_per_column, include_separators=False, indent=indent
634
- )
635
-
636
- def calculate_max_chars_per_column(
637
- self,
638
- headers: list[str],
639
- rows: list[list[Any]],
640
- sections: Optional[dict[str, tuple[int, int]]],
641
- max_char_per_col: int,
642
- ) -> list[int]:
643
- """
644
- Calculate the maximum number of characters per column in the table.
645
- This is done by checking the length of the headers, rows, and optional sections
646
- to ensure all columns are accounted for and spaced correctly.
647
-
648
- :param headers: The headers of the table.
649
- :param rows: The rows of the table.
650
- :param sections: The sections of the table grouping columns together.
651
- This is a mapping of the section display name to a tuple of the start and
652
- end column indices. If None, no sections are added (default).
653
- :param max_char_per_col: The maximum number of characters per column.
654
- :return: A list of the maximum number of characters per column.
655
- """
656
- max_characters_per_column = []
657
- for ind in range(len(headers)):
658
- max_characters_per_column.append(min(len(headers[ind]), max_char_per_col))
659
-
660
- for row in rows:
661
- max_characters_per_column[ind] = max(
662
- max_characters_per_column[ind], len(str(row[ind]))
663
- )
664
-
665
- if not sections:
666
- return max_characters_per_column
667
-
668
- for section in sections:
669
- start_col, end_col = sections[section]
670
- min_section_len = len(section) + (
671
- end_col - start_col
672
- ) # ensure we have enough space for separators
673
- chars_in_columns = sum(
674
- max_characters_per_column[start_col : end_col + 1]
675
- ) + 2 * (end_col - start_col)
676
- if min_section_len > chars_in_columns:
677
- add_chars_per_col = math.ceil(
678
- (min_section_len - chars_in_columns) / (end_col - start_col + 1)
679
- )
680
- for col in range(start_col, end_col + 1):
681
- max_characters_per_column[col] += add_chars_per_col
682
-
683
- return max_characters_per_column
684
-
685
- def print_table_divider(
686
- self, max_chars_per_column: list[int], include_separators: bool, indent: int = 0
687
- ):
688
- """
689
- Print a divider line for the table (top and bottom of table with '=' characters)
690
-
691
- :param max_chars_per_column: The maximum number of characters per column.
692
- :param include_separators: Whether to include separators between columns.
693
- :param indent: The number of spaces to indent the line.
694
- Defaults to 0.
695
- """
696
- if include_separators:
697
- columns = [
698
- settings.table_headers_border_char * max_chars
699
- + settings.table_column_separator_char
700
- + settings.table_headers_border_char
701
- for max_chars in max_chars_per_column
702
- ]
703
- else:
704
- columns = [
705
- settings.table_border_char * (max_chars + 2)
706
- for max_chars in max_chars_per_column
707
- ]
708
-
709
- columns[-1] = columns[-1][:-2]
710
- self.print_line(value=columns, style=Colors.INFO, indent=indent)
711
-
712
- def print_table_sections(
713
- self,
714
- sections: dict[str, tuple[int, int]],
715
- max_chars_per_column: list[int],
716
- indent: int = 0,
717
- ):
718
- """
719
- Print the sections of the table with corresponding separators to the columns
720
- the sections are mapped to to ensure it is compliant with a CSV format.
721
- For example, a section named "Metadata" with columns 0-3 will print this:
722
- Metadata ,,,,
723
- Where the spaces plus the separators at the end will span the columns 0-3.
724
- All columns must be accounted for in the sections.
725
-
726
- :param sections: The sections of the table.
727
- :param max_chars_per_column: The maximum number of characters per column.
728
- :param indent: The number of spaces to indent the line.
729
- Defaults to 0.
730
- """
731
- section_tuples = [(start, end, name) for name, (start, end) in sections.items()]
732
- section_tuples.sort(key=lambda x: x[0])
733
-
734
- if any(start > end for start, end, _ in section_tuples):
735
- raise ValueError(f"Invalid section ranges: {section_tuples}")
736
-
737
- if (
738
- any(
739
- section_tuples[ind][1] + 1 != section_tuples[ind + 1][0]
740
- for ind in range(len(section_tuples) - 1)
741
- )
742
- or section_tuples[0][0] != 0
743
- or section_tuples[-1][1] != len(max_chars_per_column) - 1
744
- ):
745
- raise ValueError(f"Invalid section ranges: {section_tuples}")
746
-
747
- line_values = []
748
- line_styles = []
749
- for section, (start_col, end_col) in sections.items():
750
- section_length = sum(max_chars_per_column[start_col : end_col + 1]) + 2 * (
751
- end_col - start_col + 1
752
- )
753
- num_separators = end_col - start_col
754
- line_values.append(section)
755
- line_styles.append("bold " + Colors.INFO)
756
- line_values.append(
757
- " " * (section_length - len(section) - num_separators - 2)
758
- )
759
- line_styles.append("")
760
- line_values.append(settings.table_column_separator_char * num_separators)
761
- line_styles.append("")
762
- line_values.append(settings.table_column_separator_char + " ")
763
- line_styles.append(Colors.INFO)
764
- line_values = line_values[:-1]
765
- line_styles = line_styles[:-1]
766
- self.print_line(value=line_values, style=line_styles, indent=indent)
767
-
768
- def print_table_row(
769
- self, column_lines: list[list[str]], style: str, indent: int = 0
770
- ):
771
- """
772
- Print a single row of a table to the console.
773
-
774
- :param column_lines: The lines of text to print for each column.
775
- :param indent: The number of spaces to indent the line.
776
- Defaults to 0.
777
- """
778
- for row in range(len(column_lines[0])):
779
- print_line = []
780
- print_styles = []
781
- for column in range(len(column_lines)):
782
- print_line.extend(
783
- [
784
- column_lines[column][row],
785
- settings.table_column_separator_char,
786
- " ",
787
- ]
788
- )
789
- print_styles.extend([style, Colors.INFO, ""])
790
- print_line = print_line[:-2]
791
- print_styles = print_styles[:-2]
792
- self.print_line(value=print_line, style=print_styles, indent=indent)
793
-
794
- def print_benchmarks_metadata(self):
795
- """
796
- Print out the metadata of the benchmarks to the console including the run id,
797
- duration, profile, args, worker, request loader, and extras.
798
- """
799
-
800
- if not self.benchmarks:
801
- raise ValueError(
802
- "No benchmarks to print metadata for. Please set benchmarks first."
803
- )
804
-
805
- start_time = self.benchmarks[0].run_stats.start_time
806
- end_time = self.benchmarks[-1].run_stats.end_time
807
- duration = end_time - start_time
808
-
809
- self.print_section_header(title="Benchmarks Metadata")
810
- self.print_labeled_line(
811
- label="Run id",
812
- value=str(self.benchmarks[0].run_id),
813
- )
814
- self.print_labeled_line(
815
- label="Duration",
816
- value=f"{duration:.1f} seconds",
817
- )
818
- self.print_labeled_line(
819
- label="Profile",
820
- value=self.benchmarks_profile_str,
821
- )
822
- self.print_labeled_line(
823
- label="Args",
824
- value=self.benchmarks_args_str,
825
- )
826
- self.print_labeled_line(
827
- label="Worker",
828
- value=self.benchmarks_worker_desc_str,
829
- )
830
- self.print_labeled_line(
831
- label="Request Loader",
832
- value=self.benchmarks_request_loader_desc_str,
833
- )
834
- self.print_labeled_line(
835
- label="Extras",
836
- value=self.benchmarks_extras_str,
837
- )
838
-
839
- def print_benchmarks_info(self):
840
- """
841
- Print out the benchmark information to the console including the start time,
842
- end time, duration, request totals, and token totals for each benchmark.
843
- """
844
- if not self.benchmarks:
845
- raise ValueError(
846
- "No benchmarks to print info for. Please set benchmarks first."
847
- )
848
-
849
- sections = {
850
- "Metadata": (0, 3),
851
- "Requests Made": (4, 6),
852
- "Prompt Tok/Req": (7, 9),
853
- "Output Tok/Req": (10, 12),
854
- "Prompt Tok Total": (13, 15),
855
- "Output Tok Total": (16, 18),
856
- }
857
- headers = [
858
- "Benchmark",
859
- "Start Time",
860
- "End Time",
861
- "Duration (s)",
862
- "Comp",
863
- "Inc",
864
- "Err",
865
- "Comp",
866
- "Inc",
867
- "Err",
868
- "Comp",
869
- "Inc",
870
- "Err",
871
- "Comp",
872
- "Inc",
873
- "Err",
874
- "Comp",
875
- "Inc",
876
- "Err",
877
- ]
878
- rows = []
879
-
880
- for benchmark in self.benchmarks:
881
- rows.append(
882
- [
883
- strategy_display_str(benchmark.args.strategy),
884
- f"{datetime.fromtimestamp(benchmark.start_time).strftime('%H:%M:%S')}",
885
- f"{datetime.fromtimestamp(benchmark.end_time).strftime('%H:%M:%S')}",
886
- f"{(benchmark.end_time - benchmark.start_time):.1f}",
887
- f"{benchmark.request_totals.successful:.0f}",
888
- f"{benchmark.request_totals.incomplete:.0f}",
889
- f"{benchmark.request_totals.errored:.0f}",
890
- f"{benchmark.metrics.prompt_token_count.successful.mean:.1f}",
891
- f"{benchmark.metrics.prompt_token_count.incomplete.mean:.1f}",
892
- f"{benchmark.metrics.prompt_token_count.errored.mean:.1f}",
893
- f"{benchmark.metrics.output_token_count.successful.mean:.1f}",
894
- f"{benchmark.metrics.output_token_count.incomplete.mean:.1f}",
895
- f"{benchmark.metrics.output_token_count.errored.mean:.1f}",
896
- f"{benchmark.metrics.prompt_token_count.successful.total_sum:.0f}",
897
- f"{benchmark.metrics.prompt_token_count.incomplete.total_sum:.0f}",
898
- f"{benchmark.metrics.prompt_token_count.errored.total_sum:.0f}",
899
- f"{benchmark.metrics.output_token_count.successful.total_sum:.0f}",
900
- f"{benchmark.metrics.output_token_count.incomplete.total_sum:.0f}",
901
- f"{benchmark.metrics.output_token_count.errored.total_sum:.0f}",
902
- ]
903
- )
904
-
905
- self.print_table(
906
- headers=headers, rows=rows, title="Benchmarks Info", sections=sections
907
- )
908
-
909
- def print_benchmarks_stats(self):
910
- """
911
- Print out the benchmark statistics to the console including the requests per
912
- second, request concurrency, output tokens per second, total tokens per second,
913
- request latency, time to first token, inter token latency, and time per output
914
- token for each benchmark.
915
- """
916
- if not self.benchmarks:
917
- raise ValueError(
918
- "No benchmarks to print stats for. Please set benchmarks first."
919
- )
920
-
921
- sections = {
922
- "Metadata": (0, 0),
923
- "Request Stats": (1, 2),
924
- "Out Tok/sec": (3, 3),
925
- "Tot Tok/sec": (4, 4),
926
- "Req Latency (sec)": (5, 7),
927
- "TTFT (ms)": (8, 10),
928
- "ITL (ms)": (11, 13),
929
- "TPOT (ms)": (14, 16),
930
- }
931
- headers = [
932
- "Benchmark",
933
- "Per Second",
934
- "Concurrency",
935
- "mean",
936
- "mean",
937
- "mean",
938
- "median",
939
- "p99",
940
- "mean",
941
- "median",
942
- "p99",
943
- "mean",
944
- "median",
945
- "p99",
946
- "mean",
947
- "median",
948
- "p99",
949
- ]
950
- rows = []
951
-
952
- for benchmark in self.benchmarks:
953
- rows.append(
954
- [
955
- strategy_display_str(benchmark.args.strategy),
956
- f"{benchmark.metrics.requests_per_second.successful.mean:.2f}",
957
- f"{benchmark.metrics.request_concurrency.successful.mean:.2f}",
958
- f"{benchmark.metrics.output_tokens_per_second.successful.mean:.1f}",
959
- f"{benchmark.metrics.tokens_per_second.successful.mean:.1f}",
960
- f"{benchmark.metrics.request_latency.successful.mean:.2f}",
961
- f"{benchmark.metrics.request_latency.successful.median:.2f}",
962
- f"{benchmark.metrics.request_latency.successful.percentiles.p99:.2f}",
963
- f"{benchmark.metrics.time_to_first_token_ms.successful.mean:.1f}",
964
- f"{benchmark.metrics.time_to_first_token_ms.successful.median:.1f}",
965
- f"{benchmark.metrics.time_to_first_token_ms.successful.percentiles.p99:.1f}",
966
- f"{benchmark.metrics.inter_token_latency_ms.successful.mean:.1f}",
967
- f"{benchmark.metrics.inter_token_latency_ms.successful.median:.1f}",
968
- f"{benchmark.metrics.inter_token_latency_ms.successful.percentiles.p99:.1f}",
969
- f"{benchmark.metrics.time_per_output_token_ms.successful.mean:.1f}",
970
- f"{benchmark.metrics.time_per_output_token_ms.successful.median:.1f}",
971
- f"{benchmark.metrics.time_per_output_token_ms.successful.percentiles.p99:.1f}",
972
- ]
973
- )
974
-
975
- self.print_table(
976
- headers=headers,
977
- rows=rows,
978
- title="Benchmarks Stats",
979
- sections=sections,
980
- )
981
-
982
- def print_full_report(self):
983
- """
984
- Print out the benchmark statistics to the console.
985
- Temporarily enables the console if it's disabled.
986
-
987
- Format:
988
- - Metadata
989
- - Info
990
- - Stats
991
- """
992
- orig_enabled = self.enabled
993
- self.enabled = True
994
- self.print_benchmarks_metadata()
995
- self.print_benchmarks_info()
996
- self.print_benchmarks_stats()
997
- self.enabled = orig_enabled