guidellm 0.4.0a21__py3-none-any.whl → 0.4.0a169__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.

Potentially problematic release.


This version of guidellm might be problematic. Click here for more details.

Files changed (115) hide show
  1. guidellm/__init__.py +5 -2
  2. guidellm/__main__.py +452 -252
  3. guidellm/backends/__init__.py +33 -0
  4. guidellm/backends/backend.py +110 -0
  5. guidellm/backends/openai.py +355 -0
  6. guidellm/backends/response_handlers.py +455 -0
  7. guidellm/benchmark/__init__.py +53 -39
  8. guidellm/benchmark/benchmarker.py +150 -317
  9. guidellm/benchmark/entrypoints.py +467 -128
  10. guidellm/benchmark/output.py +519 -771
  11. guidellm/benchmark/profile.py +580 -280
  12. guidellm/benchmark/progress.py +568 -549
  13. guidellm/benchmark/scenarios/__init__.py +40 -0
  14. guidellm/benchmark/scenarios/chat.json +6 -0
  15. guidellm/benchmark/scenarios/rag.json +6 -0
  16. guidellm/benchmark/schemas.py +2086 -0
  17. guidellm/data/__init__.py +28 -4
  18. guidellm/data/collators.py +16 -0
  19. guidellm/data/deserializers/__init__.py +53 -0
  20. guidellm/data/deserializers/deserializer.py +144 -0
  21. guidellm/data/deserializers/file.py +222 -0
  22. guidellm/data/deserializers/huggingface.py +94 -0
  23. guidellm/data/deserializers/memory.py +194 -0
  24. guidellm/data/deserializers/synthetic.py +348 -0
  25. guidellm/data/loaders.py +149 -0
  26. guidellm/data/preprocessors/__init__.py +25 -0
  27. guidellm/data/preprocessors/formatters.py +404 -0
  28. guidellm/data/preprocessors/mappers.py +198 -0
  29. guidellm/data/preprocessors/preprocessor.py +31 -0
  30. guidellm/data/processor.py +31 -0
  31. guidellm/data/schemas.py +13 -0
  32. guidellm/data/utils/__init__.py +6 -0
  33. guidellm/data/utils/dataset.py +94 -0
  34. guidellm/extras/__init__.py +4 -0
  35. guidellm/extras/audio.py +215 -0
  36. guidellm/extras/vision.py +242 -0
  37. guidellm/logger.py +2 -2
  38. guidellm/mock_server/__init__.py +8 -0
  39. guidellm/mock_server/config.py +84 -0
  40. guidellm/mock_server/handlers/__init__.py +17 -0
  41. guidellm/mock_server/handlers/chat_completions.py +280 -0
  42. guidellm/mock_server/handlers/completions.py +280 -0
  43. guidellm/mock_server/handlers/tokenizer.py +142 -0
  44. guidellm/mock_server/models.py +510 -0
  45. guidellm/mock_server/server.py +168 -0
  46. guidellm/mock_server/utils.py +302 -0
  47. guidellm/preprocess/dataset.py +23 -26
  48. guidellm/presentation/builder.py +2 -2
  49. guidellm/presentation/data_models.py +25 -21
  50. guidellm/presentation/injector.py +2 -3
  51. guidellm/scheduler/__init__.py +65 -26
  52. guidellm/scheduler/constraints.py +1035 -0
  53. guidellm/scheduler/environments.py +252 -0
  54. guidellm/scheduler/scheduler.py +140 -368
  55. guidellm/scheduler/schemas.py +272 -0
  56. guidellm/scheduler/strategies.py +519 -0
  57. guidellm/scheduler/worker.py +391 -420
  58. guidellm/scheduler/worker_group.py +707 -0
  59. guidellm/schemas/__init__.py +31 -0
  60. guidellm/schemas/info.py +159 -0
  61. guidellm/schemas/request.py +226 -0
  62. guidellm/schemas/response.py +119 -0
  63. guidellm/schemas/stats.py +228 -0
  64. guidellm/{config.py → settings.py} +32 -21
  65. guidellm/utils/__init__.py +95 -8
  66. guidellm/utils/auto_importer.py +98 -0
  67. guidellm/utils/cli.py +71 -2
  68. guidellm/utils/console.py +183 -0
  69. guidellm/utils/encoding.py +778 -0
  70. guidellm/utils/functions.py +134 -0
  71. guidellm/utils/hf_datasets.py +1 -2
  72. guidellm/utils/hf_transformers.py +4 -4
  73. guidellm/utils/imports.py +9 -0
  74. guidellm/utils/messaging.py +1118 -0
  75. guidellm/utils/mixins.py +115 -0
  76. guidellm/utils/pydantic_utils.py +411 -0
  77. guidellm/utils/random.py +3 -4
  78. guidellm/utils/registry.py +220 -0
  79. guidellm/utils/singleton.py +133 -0
  80. guidellm/{objects → utils}/statistics.py +341 -247
  81. guidellm/utils/synchronous.py +159 -0
  82. guidellm/utils/text.py +163 -50
  83. guidellm/utils/typing.py +41 -0
  84. guidellm/version.py +1 -1
  85. {guidellm-0.4.0a21.dist-info → guidellm-0.4.0a169.dist-info}/METADATA +33 -10
  86. guidellm-0.4.0a169.dist-info/RECORD +95 -0
  87. guidellm/backend/__init__.py +0 -23
  88. guidellm/backend/backend.py +0 -259
  89. guidellm/backend/openai.py +0 -705
  90. guidellm/backend/response.py +0 -136
  91. guidellm/benchmark/aggregator.py +0 -760
  92. guidellm/benchmark/benchmark.py +0 -837
  93. guidellm/benchmark/scenario.py +0 -104
  94. guidellm/data/prideandprejudice.txt.gz +0 -0
  95. guidellm/dataset/__init__.py +0 -22
  96. guidellm/dataset/creator.py +0 -213
  97. guidellm/dataset/entrypoints.py +0 -42
  98. guidellm/dataset/file.py +0 -92
  99. guidellm/dataset/hf_datasets.py +0 -62
  100. guidellm/dataset/in_memory.py +0 -132
  101. guidellm/dataset/synthetic.py +0 -287
  102. guidellm/objects/__init__.py +0 -18
  103. guidellm/objects/pydantic.py +0 -89
  104. guidellm/request/__init__.py +0 -18
  105. guidellm/request/loader.py +0 -284
  106. guidellm/request/request.py +0 -79
  107. guidellm/request/types.py +0 -10
  108. guidellm/scheduler/queues.py +0 -25
  109. guidellm/scheduler/result.py +0 -155
  110. guidellm/scheduler/strategy.py +0 -495
  111. guidellm-0.4.0a21.dist-info/RECORD +0 -62
  112. {guidellm-0.4.0a21.dist-info → guidellm-0.4.0a169.dist-info}/WHEEL +0 -0
  113. {guidellm-0.4.0a21.dist-info → guidellm-0.4.0a169.dist-info}/entry_points.txt +0 -0
  114. {guidellm-0.4.0a21.dist-info → guidellm-0.4.0a169.dist-info}/licenses/LICENSE +0 -0
  115. {guidellm-0.4.0a21.dist-info → guidellm-0.4.0a169.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,228 @@
1
+ """
2
+ Request statistics and metrics for generative AI benchmark analysis.
3
+
4
+ Provides data structures for capturing and analyzing performance metrics from
5
+ generative AI workloads. Contains request-level statistics including token counts,
6
+ latency measurements, and throughput calculations for text generation benchmarks.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Literal
12
+
13
+ from pydantic import Field, computed_field
14
+
15
+ from guidellm.schemas.info import RequestInfo
16
+ from guidellm.schemas.request import GenerativeRequestType, UsageMetrics
17
+ from guidellm.utils import StandardBaseDict
18
+
19
+ __all__ = ["GenerativeRequestStats"]
20
+
21
+
22
+ class GenerativeRequestStats(StandardBaseDict):
23
+ """
24
+ Request statistics for generative AI text generation workloads.
25
+
26
+ Captures comprehensive performance metrics for individual generative requests,
27
+ including token counts, timing measurements, and derived performance statistics.
28
+ Provides computed properties for latency analysis, throughput calculations,
29
+ and token generation metrics essential for benchmark evaluation.
30
+
31
+ Example:
32
+ ::
33
+ stats = GenerativeRequestStats(
34
+ request_id="req_123",
35
+ request_type="text_completion",
36
+ info=request_info,
37
+ input_metrics=input_usage,
38
+ output_metrics=output_usage
39
+ )
40
+ throughput = stats.output_tokens_per_second
41
+ """
42
+
43
+ type_: Literal["generative_request_stats"] = "generative_request_stats"
44
+ request_id: str = Field(description="Unique identifier for the request")
45
+ request_type: GenerativeRequestType | str = Field(
46
+ description="Type of generative request: text or chat completion"
47
+ )
48
+ request_args: str | None = Field(
49
+ default=None, description="Arguments passed to the backend for this request"
50
+ )
51
+ output: str | None = Field(
52
+ description="Generated text output, if request completed successfully"
53
+ )
54
+ info: RequestInfo = Field(
55
+ description="Metadata and timing information for the request"
56
+ )
57
+ input_metrics: UsageMetrics = Field(
58
+ description="Usage statistics for the input prompt"
59
+ )
60
+ output_metrics: UsageMetrics = Field(
61
+ description="Usage statistics for the generated output"
62
+ )
63
+
64
+ # Request stats
65
+ @computed_field # type: ignore[misc]
66
+ @property
67
+ def request_latency(self) -> float | None:
68
+ """
69
+ End-to-end request processing latency in seconds.
70
+
71
+ :return: Duration from request start to completion, or None if unavailable.
72
+ """
73
+ if not self.info.timings.request_end or not self.info.timings.request_start:
74
+ return None
75
+
76
+ return self.info.timings.request_end - self.info.timings.request_start
77
+
78
+ # General token stats
79
+ @computed_field # type: ignore[misc]
80
+ @property
81
+ def prompt_tokens(self) -> int | None:
82
+ """
83
+ Number of tokens in the input prompt.
84
+
85
+ :return: Input prompt token count, or None if unavailable.
86
+ """
87
+ return self.input_metrics.text_tokens
88
+
89
+ @computed_field # type: ignore[misc]
90
+ @property
91
+ def input_tokens(self) -> int | None:
92
+ """
93
+ Number of tokens in the input prompt.
94
+
95
+ :return: Input prompt token count, or None if unavailable.
96
+ """
97
+ return self.input_metrics.total_tokens
98
+
99
+ @computed_field # type: ignore[misc]
100
+ @property
101
+ def output_tokens(self) -> int | None:
102
+ """
103
+ Number of tokens in the generated output.
104
+
105
+ :return: Generated output token count, or None if unavailable.
106
+ """
107
+ return self.output_metrics.total_tokens
108
+
109
+ @computed_field # type: ignore[misc]
110
+ @property
111
+ def total_tokens(self) -> int | None:
112
+ """
113
+ Total token count including prompt and output tokens.
114
+
115
+ :return: Sum of prompt and output tokens, or None if either is unavailable.
116
+ """
117
+ input_tokens = self.input_metrics.total_tokens
118
+ output_tokens = self.output_metrics.total_tokens
119
+
120
+ if input_tokens is None and output_tokens is None:
121
+ return None
122
+
123
+ return (input_tokens or 0) + (output_tokens or 0)
124
+
125
+ @computed_field # type: ignore[misc]
126
+ @property
127
+ def time_to_first_token_ms(self) -> float | None:
128
+ """
129
+ Time to first token generation in milliseconds.
130
+
131
+ :return: Latency from request start to first token, or None if unavailable.
132
+ """
133
+ if (
134
+ not self.info.timings.first_iteration
135
+ or not self.info.timings.request_start
136
+ or self.info.timings.first_iteration == self.info.timings.last_iteration
137
+ ):
138
+ return None
139
+
140
+ return 1000 * (
141
+ self.info.timings.first_iteration - self.info.timings.request_start
142
+ )
143
+
144
+ @computed_field # type: ignore[misc]
145
+ @property
146
+ def time_per_output_token_ms(self) -> float | None:
147
+ """
148
+ Average time per output token in milliseconds.
149
+
150
+ Includes time for first token and all subsequent tokens.
151
+
152
+ :return: Average milliseconds per output token, or None if unavailable.
153
+ """
154
+ if (
155
+ not self.info.timings.request_start
156
+ or not self.info.timings.last_iteration
157
+ or not self.output_metrics.total_tokens
158
+ ):
159
+ return None
160
+
161
+ return (
162
+ 1000
163
+ * (self.info.timings.last_iteration - self.info.timings.request_start)
164
+ / self.output_metrics.total_tokens
165
+ )
166
+
167
+ @computed_field # type: ignore[misc]
168
+ @property
169
+ def inter_token_latency_ms(self) -> float | None:
170
+ """
171
+ Average inter-token latency in milliseconds.
172
+
173
+ Measures time between token generations, excluding first token.
174
+
175
+ :return: Average milliseconds between tokens, or None if unavailable.
176
+ """
177
+ if (
178
+ not self.info.timings.first_iteration
179
+ or not self.info.timings.last_iteration
180
+ or not self.output_metrics.total_tokens
181
+ or self.output_metrics.total_tokens <= 1
182
+ ):
183
+ return None
184
+
185
+ return (
186
+ 1000
187
+ * (self.info.timings.last_iteration - self.info.timings.first_iteration)
188
+ / (self.output_metrics.total_tokens - 1)
189
+ )
190
+
191
+ @computed_field # type: ignore[misc]
192
+ @property
193
+ def tokens_per_second(self) -> float | None:
194
+ """
195
+ Overall token throughput including prompt and output tokens.
196
+
197
+ :return: Total tokens per second, or None if unavailable.
198
+ """
199
+ if not (latency := self.request_latency) or self.total_tokens is None:
200
+ return None
201
+
202
+ return self.total_tokens / latency
203
+
204
+ @computed_field # type: ignore[misc]
205
+ @property
206
+ def output_tokens_per_second(self) -> float | None:
207
+ """
208
+ Output token generation throughput.
209
+
210
+ :return: Output tokens per second, or None if unavailable.
211
+ """
212
+ if not (latency := self.request_latency) or self.output_tokens is None:
213
+ return None
214
+
215
+ return self.output_tokens / latency
216
+
217
+ @computed_field # type: ignore[misc]
218
+ @property
219
+ def output_tokens_per_iteration(self) -> float | None:
220
+ """
221
+ Average output tokens generated per iteration.
222
+
223
+ :return: Output tokens per iteration, or None if unavailable.
224
+ """
225
+ if self.output_tokens is None or not self.info.timings.iterations:
226
+ return None
227
+
228
+ return self.output_tokens / self.info.timings.iterations
@@ -1,8 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
- import os
3
4
  from collections.abc import Sequence
4
5
  from enum import Enum
5
- from typing import Literal, Optional
6
+ from typing import Literal
6
7
 
7
8
  from pydantic import BaseModel, Field, model_validator
8
9
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -46,8 +47,8 @@ class LoggingSettings(BaseModel):
46
47
  disabled: bool = False
47
48
  clear_loggers: bool = True
48
49
  console_log_level: str = "WARNING"
49
- log_file: Optional[str] = None
50
- log_file_level: Optional[str] = None
50
+ log_file: str | None = None
51
+ log_file_level: str | None = None
51
52
 
52
53
 
53
54
  class DatasetSettings(BaseModel):
@@ -80,14 +81,18 @@ class OpenAISettings(BaseModel):
80
81
  for OpenAI server based pathways
81
82
  """
82
83
 
83
- api_key: Optional[str] = None
84
- bearer_token: Optional[str] = None
85
- headers: Optional[dict[str, str]] = None
86
- organization: Optional[str] = None
87
- project: Optional[str] = None
84
+ api_key: str | None = None
85
+ bearer_token: str | None = None
86
+ headers: dict[str, str] | None = None
87
+ organization: str | None = None
88
+ project: str | None = None
88
89
  base_url: str = "http://localhost:8000"
89
90
  max_output_tokens: int = 16384
90
91
  verify: bool = True
92
+ max_output_key: dict[Literal["text_completions", "chat_completions"], str] = {
93
+ "text_completions": "max_tokens",
94
+ "chat_completions": "max_completion_tokens",
95
+ }
91
96
 
92
97
 
93
98
  class ReportGenerationSettings(BaseModel):
@@ -131,24 +136,30 @@ class Settings(BaseSettings):
131
136
  request_http2: bool = True
132
137
 
133
138
  # Scheduler settings
139
+ mp_context_type: Literal["spawn", "fork", "forkserver"] | None = "fork"
140
+ mp_serialization: Literal["dict", "sequence"] | None = "dict"
141
+ mp_encoding: (
142
+ Literal["msgpack", "msgspec"]
143
+ | None
144
+ | list[Literal["msgpack", "msgspec"] | None]
145
+ ) = ["msgspec", "msgpack", None]
146
+ mp_messaging_object: Literal["queue", "manager_queue", "pipe"] = "queue"
147
+ mp_requests_send_buffer_size: int = 1
148
+ mp_poll_interval: float = 0.1
149
+ mp_max_pending_buffer_percent: float = 0.5
150
+ mp_max_worker_buffer_percent: float = 0.2
134
151
  max_concurrency: int = 512
135
- max_worker_processes: int = Field(
136
- # use number of CPUs - 1, but at least 10
137
- default_factory=lambda: max((os.cpu_count() or 1) - 1, 10)
138
- )
139
- min_queued_requests: int = 20
140
- scheduler_start_delay: float = 5
152
+ max_worker_processes: int = 10
153
+ scheduler_start_delay_non_distributed: float = 1.0
154
+ constraint_error_window_size: float = 30
155
+ constraint_error_min_processed: float = 30
141
156
 
142
157
  # Data settings
143
158
  dataset: DatasetSettings = DatasetSettings()
144
159
 
145
160
  # Request/stats settings
146
- preferred_prompt_tokens_source: Optional[
147
- Literal["request", "response", "local"]
148
- ] = "response"
149
- preferred_output_tokens_source: Optional[
150
- Literal["request", "response", "local"]
151
- ] = "response"
161
+ preferred_prompt_tokens_source: Literal["request", "response"] = "response"
162
+ preferred_output_tokens_source: Literal["request", "response"] = "response"
152
163
  preferred_backend: Literal["openai"] = "openai"
153
164
  preferred_route: Literal["text_completions", "chat_completions"] = (
154
165
  "text_completions"
@@ -1,39 +1,126 @@
1
- from .colors import Colors
1
+ from .auto_importer import AutoImporterMixin
2
+ from .console import Colors, Console, ConsoleUpdateStep, StatusIcons, StatusStyles
2
3
  from .default_group import DefaultGroupHandler
3
4
  from .dict import recursive_key_update
4
- from .hf_datasets import (
5
- SUPPORTED_TYPES,
6
- save_dataset_to_file,
5
+ from .encoding import (
6
+ Encoder,
7
+ EncodingTypesAlias,
8
+ MessageEncoding,
9
+ SerializationTypesAlias,
10
+ Serializer,
7
11
  )
8
- from .hf_transformers import (
9
- check_load_processor,
12
+ from .functions import (
13
+ all_defined,
14
+ safe_add,
15
+ safe_divide,
16
+ safe_format_timestamp,
17
+ safe_getattr,
18
+ safe_multiply,
19
+ )
20
+ from .hf_datasets import SUPPORTED_TYPES, save_dataset_to_file
21
+ from .hf_transformers import check_load_processor
22
+ from .imports import json
23
+ from .messaging import (
24
+ InterProcessMessaging,
25
+ InterProcessMessagingManagerQueue,
26
+ InterProcessMessagingPipe,
27
+ InterProcessMessagingQueue,
28
+ SendMessageT,
29
+ )
30
+ from .mixins import InfoMixin
31
+ from .pydantic_utils import (
32
+ PydanticClassRegistryMixin,
33
+ ReloadableBaseModel,
34
+ StandardBaseDict,
35
+ StandardBaseModel,
36
+ StatusBreakdown,
10
37
  )
11
38
  from .random import IntegerRangeSampler
39
+ from .registry import RegistryMixin, RegistryObjT
40
+ from .singleton import SingletonMixin, ThreadSafeSingletonMixin
41
+ from .statistics import (
42
+ DistributionSummary,
43
+ Percentiles,
44
+ RunningStats,
45
+ StatusDistributionSummary,
46
+ TimeRunningStats,
47
+ )
48
+ from .synchronous import (
49
+ wait_for_sync_barrier,
50
+ wait_for_sync_event,
51
+ wait_for_sync_objects,
52
+ )
12
53
  from .text import (
13
54
  EndlessTextCreator,
14
55
  camelize_str,
15
56
  clean_text,
16
57
  filter_text,
17
- is_puncutation,
58
+ format_value_display,
59
+ is_punctuation,
18
60
  load_text,
19
61
  split_text,
20
62
  split_text_list_by_length,
21
63
  )
64
+ from .typing import get_literal_vals
22
65
 
23
66
  __all__ = [
24
67
  "SUPPORTED_TYPES",
68
+ "AutoImporterMixin",
69
+ "Colors",
25
70
  "Colors",
71
+ "Console",
72
+ "ConsoleUpdateStep",
26
73
  "DefaultGroupHandler",
74
+ "DistributionSummary",
75
+ "Encoder",
76
+ "EncodingTypesAlias",
27
77
  "EndlessTextCreator",
78
+ "InfoMixin",
28
79
  "IntegerRangeSampler",
80
+ "InterProcessMessaging",
81
+ "InterProcessMessagingManagerQueue",
82
+ "InterProcessMessagingPipe",
83
+ "InterProcessMessagingQueue",
84
+ "MessageEncoding",
85
+ "MessageEncoding",
86
+ "Percentiles",
87
+ "PydanticClassRegistryMixin",
88
+ "RegistryMixin",
89
+ "RegistryObjT",
90
+ "ReloadableBaseModel",
91
+ "RunningStats",
92
+ "SendMessageT",
93
+ "SerializationTypesAlias",
94
+ "Serializer",
95
+ "SingletonMixin",
96
+ "StandardBaseDict",
97
+ "StandardBaseModel",
98
+ "StatusBreakdown",
99
+ "StatusDistributionSummary",
100
+ "StatusIcons",
101
+ "StatusStyles",
102
+ "ThreadSafeSingletonMixin",
103
+ "TimeRunningStats",
104
+ "all_defined",
29
105
  "camelize_str",
30
106
  "check_load_processor",
31
107
  "clean_text",
32
108
  "filter_text",
33
- "is_puncutation",
109
+ "format_value_display",
110
+ "get_literal_vals",
111
+ "is_punctuation",
112
+ "json",
34
113
  "load_text",
35
114
  "recursive_key_update",
115
+ "safe_add",
116
+ "safe_divide",
117
+ "safe_format_timestamp",
118
+ "safe_getattr",
119
+ "safe_multiply",
36
120
  "save_dataset_to_file",
37
121
  "split_text",
38
122
  "split_text_list_by_length",
123
+ "wait_for_sync_barrier",
124
+ "wait_for_sync_event",
125
+ "wait_for_sync_objects",
39
126
  ]
@@ -0,0 +1,98 @@
1
+ """
2
+ Automatic module importing utilities for dynamic class discovery.
3
+
4
+ This module provides a mixin class for automatic module importing within a package,
5
+ enabling dynamic discovery of classes and implementations without explicit imports.
6
+ It is particularly useful for auto-registering classes in a registry pattern where
7
+ subclasses need to be discoverable at runtime.
8
+
9
+ The AutoImporterMixin can be combined with registration mechanisms to create
10
+ extensible systems where new implementations are automatically discovered and
11
+ registered when they are placed in the correct package structure.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import importlib
17
+ import pkgutil
18
+ import sys
19
+ from typing import ClassVar
20
+
21
+ __all__ = ["AutoImporterMixin"]
22
+
23
+
24
+ class AutoImporterMixin:
25
+ """
26
+ Mixin class for automatic module importing within packages.
27
+
28
+ This mixin enables dynamic discovery of classes and implementations without
29
+ explicit imports by automatically importing all modules within specified
30
+ packages. It is designed for use with class registration mechanisms to enable
31
+ automatic discovery and registration of classes when they are placed in the
32
+ correct package structure.
33
+
34
+ Example:
35
+ ::
36
+ from guidellm.utils import AutoImporterMixin
37
+
38
+ class MyRegistry(AutoImporterMixin):
39
+ auto_package = "my_package.implementations"
40
+
41
+ MyRegistry.auto_import_package_modules()
42
+
43
+ :cvar auto_package: Package name or tuple of package names to import modules from
44
+ :cvar auto_ignore_modules: Module names to ignore during import
45
+ :cvar auto_imported_modules: List tracking which modules have been imported
46
+ """
47
+
48
+ auto_package: ClassVar[str | tuple[str, ...] | None] = None
49
+ auto_ignore_modules: ClassVar[tuple[str, ...] | None] = None
50
+ auto_imported_modules: ClassVar[list[str] | None] = None
51
+
52
+ @classmethod
53
+ def auto_import_package_modules(cls) -> None:
54
+ """
55
+ Automatically import all modules within the specified package(s).
56
+
57
+ Scans the package(s) defined in the `auto_package` class variable and imports
58
+ all modules found, tracking them in `auto_imported_modules`. Skips packages
59
+ (directories) and any modules listed in `auto_ignore_modules`.
60
+
61
+ :raises ValueError: If the `auto_package` class variable is not set
62
+ """
63
+ if cls.auto_package is None:
64
+ raise ValueError(
65
+ "The class variable 'auto_package' must be set to the package name to "
66
+ "import modules from."
67
+ )
68
+
69
+ cls.auto_imported_modules = []
70
+ packages = (
71
+ cls.auto_package
72
+ if isinstance(cls.auto_package, tuple)
73
+ else (cls.auto_package,)
74
+ )
75
+
76
+ for package_name in packages:
77
+ package = importlib.import_module(package_name)
78
+
79
+ for _, module_name, is_pkg in pkgutil.walk_packages(
80
+ package.__path__, package.__name__ + "."
81
+ ):
82
+ if (
83
+ is_pkg
84
+ or (
85
+ cls.auto_ignore_modules is not None
86
+ and module_name in cls.auto_ignore_modules
87
+ )
88
+ or module_name in cls.auto_imported_modules
89
+ ):
90
+ # Skip packages and ignored modules
91
+ continue
92
+
93
+ if module_name in sys.modules:
94
+ # Avoid circular imports
95
+ cls.auto_imported_modules.append(module_name)
96
+ else:
97
+ importlib.import_module(module_name)
98
+ cls.auto_imported_modules.append(module_name)
guidellm/utils/cli.py CHANGED
@@ -3,10 +3,56 @@ from typing import Any
3
3
 
4
4
  import click
5
5
 
6
+ __all__ = [
7
+ "Union",
8
+ "format_list_arg",
9
+ "parse_json",
10
+ "parse_list_floats",
11
+ "set_if_not_default",
12
+ ]
6
13
 
7
- def parse_json(ctx, param, value): # noqa: ARG001
14
+
15
+ def parse_list_floats(ctx, param, value): # noqa: ARG001
16
+ """
17
+ Callback to parse a comma-separated string into a list of floats.
18
+ """
19
+ # This callback only runs if the --rate option is provided by the user.
20
+ # If it's not, 'value' will be None, and Click will use the 'default'.
8
21
  if value is None:
22
+ return None # Keep the default
23
+
24
+ try:
25
+ # Split by comma, strip any whitespace, and convert to float
26
+ return [float(item.strip()) for item in value.split(",")]
27
+ except ValueError as e:
28
+ # Raise a Click error if any part isn't a valid float
29
+ raise click.BadParameter(
30
+ f"Value '{value}' is not a valid comma-separated list "
31
+ f"of floats/ints. Error: {e}"
32
+ ) from e
33
+
34
+ def parse_json(ctx, param, value): # noqa: ARG001
35
+ if value is None or value == [None]:
9
36
  return None
37
+ if isinstance(value, list | tuple):
38
+ return [parse_json(ctx, param, val) for val in value]
39
+
40
+ if "{" not in value and "}" not in value and "=" in value:
41
+ # Treat it as a key=value pair if it doesn't look like JSON.
42
+ result = {}
43
+ for pair in value.split(","):
44
+ if "=" not in pair:
45
+ raise click.BadParameter(
46
+ f"{param.name} must be a valid JSON string or key=value pairs."
47
+ )
48
+ key, val = pair.split("=", 1)
49
+ result[key.strip()] = val.strip()
50
+ return result
51
+
52
+ if "{" not in value and "}" not in value:
53
+ # Treat it as a plain string if it doesn't look like JSON.
54
+ return value
55
+
10
56
  try:
11
57
  return json.loads(value)
12
58
  except json.JSONDecodeError as err:
@@ -26,6 +72,29 @@ def set_if_not_default(ctx: click.Context, **kwargs) -> dict[str, Any]:
26
72
  return values
27
73
 
28
74
 
75
+ def format_list_arg(
76
+ value: Any, default: Any = None, simplify_single: bool = False
77
+ ) -> list[Any] | Any:
78
+ """
79
+ Format a multi-argument value for display.
80
+
81
+ :param value: The value to format, which can be a single value or a list/tuple.
82
+ :param default: The default value to set if the value is non truthy.
83
+ :param simplify_single: If True and the value is a single-item list/tuple,
84
+ return the single item instead of a list.
85
+ :return: Formatted list of values, or single value if simplify_single and applicable
86
+ """
87
+ if not value:
88
+ return default
89
+
90
+ if isinstance(value, tuple):
91
+ value = list(value)
92
+ elif not isinstance(value, list):
93
+ value = [value]
94
+
95
+ return value if not simplify_single or len(value) != 1 else value[0]
96
+
97
+
29
98
  class Union(click.ParamType):
30
99
  """
31
100
  A custom click parameter type that allows for multiple types to be accepted.
@@ -35,7 +104,7 @@ class Union(click.ParamType):
35
104
  self.types = types
36
105
  self.name = "".join(t.name for t in types)
37
106
 
38
- def convert(self, value, param, ctx): # noqa: RET503
107
+ def convert(self, value, param, ctx):
39
108
  fails = []
40
109
  for t in self.types:
41
110
  try: