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.
- guidellm/__init__.py +5 -2
- guidellm/__main__.py +524 -255
- guidellm/backends/__init__.py +33 -0
- guidellm/backends/backend.py +109 -0
- guidellm/backends/openai.py +340 -0
- guidellm/backends/response_handlers.py +428 -0
- guidellm/benchmark/__init__.py +69 -39
- guidellm/benchmark/benchmarker.py +160 -316
- guidellm/benchmark/entrypoints.py +560 -127
- guidellm/benchmark/outputs/__init__.py +24 -0
- guidellm/benchmark/outputs/console.py +633 -0
- guidellm/benchmark/outputs/csv.py +721 -0
- guidellm/benchmark/outputs/html.py +473 -0
- guidellm/benchmark/outputs/output.py +169 -0
- guidellm/benchmark/outputs/serialized.py +69 -0
- guidellm/benchmark/profiles.py +718 -0
- guidellm/benchmark/progress.py +553 -556
- guidellm/benchmark/scenarios/__init__.py +40 -0
- guidellm/benchmark/scenarios/chat.json +6 -0
- guidellm/benchmark/scenarios/rag.json +6 -0
- guidellm/benchmark/schemas/__init__.py +66 -0
- guidellm/benchmark/schemas/base.py +402 -0
- guidellm/benchmark/schemas/generative/__init__.py +55 -0
- guidellm/benchmark/schemas/generative/accumulator.py +841 -0
- guidellm/benchmark/schemas/generative/benchmark.py +163 -0
- guidellm/benchmark/schemas/generative/entrypoints.py +381 -0
- guidellm/benchmark/schemas/generative/metrics.py +927 -0
- guidellm/benchmark/schemas/generative/report.py +158 -0
- guidellm/data/__init__.py +34 -4
- guidellm/data/builders.py +541 -0
- guidellm/data/collators.py +16 -0
- guidellm/data/config.py +120 -0
- guidellm/data/deserializers/__init__.py +49 -0
- guidellm/data/deserializers/deserializer.py +141 -0
- guidellm/data/deserializers/file.py +223 -0
- guidellm/data/deserializers/huggingface.py +94 -0
- guidellm/data/deserializers/memory.py +194 -0
- guidellm/data/deserializers/synthetic.py +246 -0
- guidellm/data/entrypoints.py +52 -0
- guidellm/data/loaders.py +190 -0
- guidellm/data/preprocessors/__init__.py +27 -0
- guidellm/data/preprocessors/formatters.py +410 -0
- guidellm/data/preprocessors/mappers.py +196 -0
- guidellm/data/preprocessors/preprocessor.py +30 -0
- guidellm/data/processor.py +29 -0
- guidellm/data/schemas.py +175 -0
- guidellm/data/utils/__init__.py +6 -0
- guidellm/data/utils/dataset.py +94 -0
- guidellm/extras/__init__.py +4 -0
- guidellm/extras/audio.py +220 -0
- guidellm/extras/vision.py +242 -0
- guidellm/logger.py +2 -2
- guidellm/mock_server/__init__.py +8 -0
- guidellm/mock_server/config.py +84 -0
- guidellm/mock_server/handlers/__init__.py +17 -0
- guidellm/mock_server/handlers/chat_completions.py +280 -0
- guidellm/mock_server/handlers/completions.py +280 -0
- guidellm/mock_server/handlers/tokenizer.py +142 -0
- guidellm/mock_server/models.py +510 -0
- guidellm/mock_server/server.py +238 -0
- guidellm/mock_server/utils.py +302 -0
- guidellm/scheduler/__init__.py +69 -26
- guidellm/scheduler/constraints/__init__.py +49 -0
- guidellm/scheduler/constraints/constraint.py +325 -0
- guidellm/scheduler/constraints/error.py +411 -0
- guidellm/scheduler/constraints/factory.py +182 -0
- guidellm/scheduler/constraints/request.py +312 -0
- guidellm/scheduler/constraints/saturation.py +722 -0
- guidellm/scheduler/environments.py +252 -0
- guidellm/scheduler/scheduler.py +137 -368
- guidellm/scheduler/schemas.py +358 -0
- guidellm/scheduler/strategies.py +617 -0
- guidellm/scheduler/worker.py +413 -419
- guidellm/scheduler/worker_group.py +712 -0
- guidellm/schemas/__init__.py +65 -0
- guidellm/schemas/base.py +417 -0
- guidellm/schemas/info.py +188 -0
- guidellm/schemas/request.py +235 -0
- guidellm/schemas/request_stats.py +349 -0
- guidellm/schemas/response.py +124 -0
- guidellm/schemas/statistics.py +1018 -0
- guidellm/{config.py → settings.py} +31 -24
- guidellm/utils/__init__.py +71 -8
- guidellm/utils/auto_importer.py +98 -0
- guidellm/utils/cli.py +132 -5
- guidellm/utils/console.py +566 -0
- guidellm/utils/encoding.py +778 -0
- guidellm/utils/functions.py +159 -0
- guidellm/utils/hf_datasets.py +1 -2
- guidellm/utils/hf_transformers.py +4 -4
- guidellm/utils/imports.py +9 -0
- guidellm/utils/messaging.py +1118 -0
- guidellm/utils/mixins.py +115 -0
- guidellm/utils/random.py +3 -4
- guidellm/utils/registry.py +220 -0
- guidellm/utils/singleton.py +133 -0
- guidellm/utils/synchronous.py +159 -0
- guidellm/utils/text.py +163 -50
- guidellm/utils/typing.py +41 -0
- guidellm/version.py +2 -2
- guidellm-0.6.0a5.dist-info/METADATA +364 -0
- guidellm-0.6.0a5.dist-info/RECORD +109 -0
- guidellm/backend/__init__.py +0 -23
- guidellm/backend/backend.py +0 -259
- guidellm/backend/openai.py +0 -708
- guidellm/backend/response.py +0 -136
- guidellm/benchmark/aggregator.py +0 -760
- guidellm/benchmark/benchmark.py +0 -837
- guidellm/benchmark/output.py +0 -997
- guidellm/benchmark/profile.py +0 -409
- guidellm/benchmark/scenario.py +0 -104
- guidellm/data/prideandprejudice.txt.gz +0 -0
- guidellm/dataset/__init__.py +0 -22
- guidellm/dataset/creator.py +0 -213
- guidellm/dataset/entrypoints.py +0 -42
- guidellm/dataset/file.py +0 -92
- guidellm/dataset/hf_datasets.py +0 -62
- guidellm/dataset/in_memory.py +0 -132
- guidellm/dataset/synthetic.py +0 -287
- guidellm/objects/__init__.py +0 -18
- guidellm/objects/pydantic.py +0 -89
- guidellm/objects/statistics.py +0 -953
- guidellm/preprocess/__init__.py +0 -3
- guidellm/preprocess/dataset.py +0 -374
- guidellm/presentation/__init__.py +0 -28
- guidellm/presentation/builder.py +0 -27
- guidellm/presentation/data_models.py +0 -232
- guidellm/presentation/injector.py +0 -66
- guidellm/request/__init__.py +0 -18
- guidellm/request/loader.py +0 -284
- guidellm/request/request.py +0 -79
- guidellm/request/types.py +0 -10
- guidellm/scheduler/queues.py +0 -25
- guidellm/scheduler/result.py +0 -155
- guidellm/scheduler/strategy.py +0 -495
- guidellm-0.3.1.dist-info/METADATA +0 -329
- guidellm-0.3.1.dist-info/RECORD +0 -62
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/WHEEL +0 -0
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/entry_points.txt +0 -0
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/licenses/LICENSE +0 -0
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Orchestrate multi-strategy benchmark execution through configurable profiles.
|
|
3
|
+
|
|
4
|
+
Provides abstractions for coordinating sequential execution of scheduling strategies
|
|
5
|
+
during benchmarking workflows. Profiles automatically generate strategies based on
|
|
6
|
+
configuration parameters, manage runtime constraints, and track completion state
|
|
7
|
+
across execution sequences. Each profile type implements a specific execution pattern
|
|
8
|
+
(synchronous, concurrent, throughput-focused, rate-based async, or adaptive sweep)
|
|
9
|
+
that determines how benchmark requests are scheduled and executed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from collections.abc import Generator
|
|
16
|
+
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from pydantic import (
|
|
20
|
+
Field,
|
|
21
|
+
NonNegativeFloat,
|
|
22
|
+
PositiveFloat,
|
|
23
|
+
PositiveInt,
|
|
24
|
+
computed_field,
|
|
25
|
+
field_serializer,
|
|
26
|
+
field_validator,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from guidellm import settings
|
|
30
|
+
from guidellm.scheduler import (
|
|
31
|
+
AsyncConstantStrategy,
|
|
32
|
+
AsyncPoissonStrategy,
|
|
33
|
+
ConcurrentStrategy,
|
|
34
|
+
Constraint,
|
|
35
|
+
ConstraintInitializer,
|
|
36
|
+
ConstraintsInitializerFactory,
|
|
37
|
+
SchedulingStrategy,
|
|
38
|
+
SynchronousStrategy,
|
|
39
|
+
ThroughputStrategy,
|
|
40
|
+
)
|
|
41
|
+
from guidellm.schemas import PydanticClassRegistryMixin
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from guidellm.benchmark.schemas import Benchmark
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"AsyncProfile",
|
|
48
|
+
"ConcurrentProfile",
|
|
49
|
+
"Profile",
|
|
50
|
+
"ProfileType",
|
|
51
|
+
"SweepProfile",
|
|
52
|
+
"SynchronousProfile",
|
|
53
|
+
"ThroughputProfile",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
ProfileType = Annotated[
|
|
57
|
+
Literal["synchronous", "concurrent", "throughput", "async", "sweep"],
|
|
58
|
+
"Profile type identifiers for polymorphic deserialization",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Profile(
|
|
63
|
+
PydanticClassRegistryMixin["Profile"],
|
|
64
|
+
ABC,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Coordinate multi-strategy benchmark execution with automatic strategy generation.
|
|
68
|
+
|
|
69
|
+
Manages sequential execution of scheduling strategies with automatic strategy
|
|
70
|
+
generation, constraint management, and completion tracking. Subclasses define
|
|
71
|
+
specific execution patterns like synchronous, concurrent, throughput-focused,
|
|
72
|
+
rate-based async, or adaptive sweep profiles.
|
|
73
|
+
|
|
74
|
+
:cvar schema_discriminator: Field name for polymorphic deserialization
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
schema_discriminator: ClassVar[str] = "type_"
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def __pydantic_schema_base_type__(cls) -> type[Profile]:
|
|
81
|
+
"""
|
|
82
|
+
Return base type for polymorphic validation hierarchy.
|
|
83
|
+
|
|
84
|
+
:return: Base Profile class for schema validation
|
|
85
|
+
"""
|
|
86
|
+
if cls.__name__ == "Profile":
|
|
87
|
+
return cls
|
|
88
|
+
|
|
89
|
+
return Profile
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def create(
|
|
93
|
+
cls,
|
|
94
|
+
rate_type: str,
|
|
95
|
+
rate: list[float] | None,
|
|
96
|
+
random_seed: int = 42,
|
|
97
|
+
**kwargs: Any,
|
|
98
|
+
) -> Profile:
|
|
99
|
+
"""
|
|
100
|
+
Create profile instances based on type identifier.
|
|
101
|
+
|
|
102
|
+
:param rate_type: Profile type identifier to instantiate
|
|
103
|
+
:param rate: Rate configuration for the profile strategy
|
|
104
|
+
:param random_seed: Seed for stochastic strategy reproducibility
|
|
105
|
+
:param kwargs: Additional profile-specific configuration parameters
|
|
106
|
+
:return: Configured profile instance for the specified type
|
|
107
|
+
:raises ValueError: If rate_type is not registered
|
|
108
|
+
"""
|
|
109
|
+
profile_class = cls.get_registered_object(rate_type)
|
|
110
|
+
if profile_class is None:
|
|
111
|
+
raise ValueError(f"Profile type '{rate_type}' is not registered")
|
|
112
|
+
|
|
113
|
+
resolved_kwargs = profile_class.resolve_args(
|
|
114
|
+
rate_type=rate_type, rate=rate, random_seed=random_seed, **kwargs
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return profile_class(**resolved_kwargs)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def resolve_args(
|
|
122
|
+
cls,
|
|
123
|
+
rate_type: str,
|
|
124
|
+
rate: list[float] | None,
|
|
125
|
+
random_seed: int,
|
|
126
|
+
**kwargs: Any,
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""
|
|
129
|
+
Resolve and validate arguments for profile construction.
|
|
130
|
+
|
|
131
|
+
:param rate_type: Profile type identifier
|
|
132
|
+
:param rate: Rate configuration parameter
|
|
133
|
+
:param random_seed: Seed for stochastic strategies
|
|
134
|
+
:param kwargs: Additional arguments to resolve and validate
|
|
135
|
+
:return: Resolved arguments dictionary for profile initialization
|
|
136
|
+
"""
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
type_: Literal["profile"] = Field(
|
|
140
|
+
description="Profile type discriminator for polymorphic serialization",
|
|
141
|
+
)
|
|
142
|
+
completed_strategies: list[SchedulingStrategy] = Field(
|
|
143
|
+
default_factory=list,
|
|
144
|
+
description="Strategies that completed execution in this profile",
|
|
145
|
+
)
|
|
146
|
+
constraints: dict[str, Any | dict[str, Any] | ConstraintInitializer] | None = Field(
|
|
147
|
+
default=None,
|
|
148
|
+
description="Runtime constraints applied to strategy execution",
|
|
149
|
+
)
|
|
150
|
+
rampup_duration: NonNegativeFloat = Field(
|
|
151
|
+
default=0.0,
|
|
152
|
+
description=(
|
|
153
|
+
"Duration in seconds to ramp up the targeted scheduling rate, if applicable"
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@computed_field # type: ignore[misc]
|
|
158
|
+
@property
|
|
159
|
+
def strategy_types(self) -> list[str]:
|
|
160
|
+
"""
|
|
161
|
+
:return: Strategy types executed or to be executed in this profile
|
|
162
|
+
"""
|
|
163
|
+
return [strat.type_ for strat in self.completed_strategies]
|
|
164
|
+
|
|
165
|
+
def strategies_generator(
|
|
166
|
+
self,
|
|
167
|
+
) -> Generator[
|
|
168
|
+
tuple[SchedulingStrategy, dict[str, Constraint] | None],
|
|
169
|
+
Benchmark | None,
|
|
170
|
+
None,
|
|
171
|
+
]:
|
|
172
|
+
"""
|
|
173
|
+
Generate strategies and constraints for sequential execution.
|
|
174
|
+
|
|
175
|
+
:return: Generator yielding (strategy, constraints) tuples and receiving
|
|
176
|
+
benchmark results after each execution
|
|
177
|
+
"""
|
|
178
|
+
prev_strategy: SchedulingStrategy | None = None
|
|
179
|
+
prev_benchmark: Benchmark | None = None
|
|
180
|
+
|
|
181
|
+
while (
|
|
182
|
+
strategy := self.next_strategy(prev_strategy, prev_benchmark)
|
|
183
|
+
) is not None:
|
|
184
|
+
constraints = self.next_strategy_constraints(
|
|
185
|
+
strategy, prev_strategy, prev_benchmark
|
|
186
|
+
)
|
|
187
|
+
prev_benchmark = yield (
|
|
188
|
+
strategy,
|
|
189
|
+
constraints,
|
|
190
|
+
)
|
|
191
|
+
prev_strategy = strategy
|
|
192
|
+
self.completed_strategies.append(prev_strategy)
|
|
193
|
+
|
|
194
|
+
@abstractmethod
|
|
195
|
+
def next_strategy(
|
|
196
|
+
self,
|
|
197
|
+
prev_strategy: SchedulingStrategy | None,
|
|
198
|
+
prev_benchmark: Benchmark | None,
|
|
199
|
+
) -> SchedulingStrategy | None:
|
|
200
|
+
"""
|
|
201
|
+
Generate next strategy in the profile execution sequence.
|
|
202
|
+
|
|
203
|
+
:param prev_strategy: Previously completed strategy instance
|
|
204
|
+
:param prev_benchmark: Benchmark results from previous strategy execution
|
|
205
|
+
:return: Next strategy to execute, or None if profile complete
|
|
206
|
+
"""
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
def next_strategy_constraints(
|
|
210
|
+
self,
|
|
211
|
+
next_strategy: SchedulingStrategy | None,
|
|
212
|
+
prev_strategy: SchedulingStrategy | None,
|
|
213
|
+
prev_benchmark: Benchmark | None,
|
|
214
|
+
) -> dict[str, Constraint] | None:
|
|
215
|
+
"""
|
|
216
|
+
Generate constraints for next strategy execution.
|
|
217
|
+
|
|
218
|
+
:param next_strategy: Strategy to be executed next
|
|
219
|
+
:param prev_strategy: Previously completed strategy instance
|
|
220
|
+
:param prev_benchmark: Benchmark results from previous strategy execution
|
|
221
|
+
:return: Constraints dictionary for next strategy, or None
|
|
222
|
+
"""
|
|
223
|
+
_ = (prev_strategy, prev_benchmark) # unused
|
|
224
|
+
return (
|
|
225
|
+
ConstraintsInitializerFactory.resolve(self.constraints)
|
|
226
|
+
if next_strategy and self.constraints
|
|
227
|
+
else None
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
@field_validator("constraints", mode="before")
|
|
231
|
+
@classmethod
|
|
232
|
+
def _constraints_validator(
|
|
233
|
+
cls, value: Any
|
|
234
|
+
) -> dict[str, Any | dict[str, Any] | ConstraintInitializer] | None:
|
|
235
|
+
if value is None:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
if not isinstance(value, dict):
|
|
239
|
+
raise ValueError("Constraints must be a dictionary")
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
key: (
|
|
243
|
+
ConstraintsInitializerFactory.deserialize(initializer_dict=val)
|
|
244
|
+
if isinstance(val, dict)
|
|
245
|
+
and "type_" in val
|
|
246
|
+
and not isinstance(val, ConstraintInitializer)
|
|
247
|
+
else val
|
|
248
|
+
)
|
|
249
|
+
for key, val in value.items()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@field_serializer("constraints")
|
|
253
|
+
def _constraints_serializer(
|
|
254
|
+
self,
|
|
255
|
+
constraints: dict[str, Any | dict[str, Any] | ConstraintInitializer] | None,
|
|
256
|
+
) -> dict[str, Any | dict[str, Any]] | None:
|
|
257
|
+
if constraints is None:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
key: (
|
|
262
|
+
val
|
|
263
|
+
if not isinstance(val, ConstraintInitializer)
|
|
264
|
+
else ConstraintsInitializerFactory.serialize(initializer=val)
|
|
265
|
+
)
|
|
266
|
+
for key, val in constraints.items()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@Profile.register("synchronous")
|
|
271
|
+
class SynchronousProfile(Profile):
|
|
272
|
+
"""
|
|
273
|
+
Execute single synchronous strategy for baseline performance metrics.
|
|
274
|
+
|
|
275
|
+
Executes requests sequentially with one request at a time, establishing
|
|
276
|
+
baseline performance metrics without concurrent execution overhead.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
type_: Literal["synchronous"] = "synchronous" # type: ignore[assignment]
|
|
280
|
+
|
|
281
|
+
@classmethod
|
|
282
|
+
def resolve_args(
|
|
283
|
+
cls,
|
|
284
|
+
rate_type: str,
|
|
285
|
+
rate: list[float] | None,
|
|
286
|
+
random_seed: int,
|
|
287
|
+
**kwargs: Any,
|
|
288
|
+
) -> dict[str, Any]:
|
|
289
|
+
"""
|
|
290
|
+
Resolve arguments for synchronous profile construction.
|
|
291
|
+
|
|
292
|
+
:param rate_type: Profile type identifier (ignored)
|
|
293
|
+
:param rate: Rate parameter (must be None)
|
|
294
|
+
:param random_seed: Random seed (ignored)
|
|
295
|
+
:param kwargs: Additional arguments passed through unchanged
|
|
296
|
+
:return: Resolved arguments dictionary
|
|
297
|
+
:raises ValueError: If rate is not None
|
|
298
|
+
"""
|
|
299
|
+
_ = (rate_type, random_seed) # unused
|
|
300
|
+
if rate is not None:
|
|
301
|
+
raise ValueError("SynchronousProfile does not accept a rate parameter")
|
|
302
|
+
|
|
303
|
+
return kwargs
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def strategy_types(self) -> list[str]:
|
|
307
|
+
"""
|
|
308
|
+
:return: Single synchronous strategy type
|
|
309
|
+
"""
|
|
310
|
+
return [self.type_]
|
|
311
|
+
|
|
312
|
+
def next_strategy(
|
|
313
|
+
self,
|
|
314
|
+
prev_strategy: SchedulingStrategy | None,
|
|
315
|
+
prev_benchmark: Benchmark | None,
|
|
316
|
+
) -> SynchronousStrategy | None:
|
|
317
|
+
"""
|
|
318
|
+
Generate synchronous strategy for first execution only.
|
|
319
|
+
|
|
320
|
+
:param prev_strategy: Previously completed strategy (unused)
|
|
321
|
+
:param prev_benchmark: Benchmark results from previous execution (unused)
|
|
322
|
+
:return: SynchronousStrategy for first execution, None afterward
|
|
323
|
+
"""
|
|
324
|
+
_ = (prev_strategy, prev_benchmark) # unused
|
|
325
|
+
if len(self.completed_strategies) >= 1:
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
return SynchronousStrategy()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@Profile.register("concurrent")
|
|
332
|
+
class ConcurrentProfile(Profile):
|
|
333
|
+
"""
|
|
334
|
+
Execute strategies with fixed concurrency levels for performance testing.
|
|
335
|
+
|
|
336
|
+
Executes requests with a fixed number of concurrent streams, useful for
|
|
337
|
+
testing system performance under specific concurrency levels.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
type_: Literal["concurrent"] = "concurrent" # type: ignore[assignment]
|
|
341
|
+
streams: list[PositiveInt] = Field(
|
|
342
|
+
description="Concurrent stream counts for request scheduling",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def resolve_args(
|
|
347
|
+
cls,
|
|
348
|
+
rate_type: str,
|
|
349
|
+
rate: list[float] | None,
|
|
350
|
+
random_seed: int,
|
|
351
|
+
**kwargs: Any,
|
|
352
|
+
) -> dict[str, Any]:
|
|
353
|
+
"""
|
|
354
|
+
Resolve arguments for concurrent profile construction.
|
|
355
|
+
|
|
356
|
+
:param rate_type: Profile type identifier (ignored)
|
|
357
|
+
:param rate: Rate parameter remapped to streams
|
|
358
|
+
:param random_seed: Random seed (ignored)
|
|
359
|
+
:param kwargs: Additional arguments passed through unchanged
|
|
360
|
+
:return: Resolved arguments dictionary
|
|
361
|
+
:raises ValueError: If rate is None
|
|
362
|
+
"""
|
|
363
|
+
_ = (rate_type, random_seed) # unused
|
|
364
|
+
rate = rate if isinstance(rate, list) or rate is None else [rate]
|
|
365
|
+
kwargs["streams"] = [int(stream) for stream in rate] if rate else None
|
|
366
|
+
return kwargs
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def strategy_types(self) -> list[str]:
|
|
370
|
+
"""
|
|
371
|
+
:return: Concurrent strategy types for each configured stream count
|
|
372
|
+
"""
|
|
373
|
+
return [self.type_] * len(self.streams)
|
|
374
|
+
|
|
375
|
+
def next_strategy(
|
|
376
|
+
self,
|
|
377
|
+
prev_strategy: SchedulingStrategy | None,
|
|
378
|
+
prev_benchmark: Benchmark | None,
|
|
379
|
+
) -> ConcurrentStrategy | None:
|
|
380
|
+
"""
|
|
381
|
+
Generate concurrent strategy for next stream count.
|
|
382
|
+
|
|
383
|
+
:param prev_strategy: Previously completed strategy (unused)
|
|
384
|
+
:param prev_benchmark: Benchmark results from previous execution (unused)
|
|
385
|
+
:return: ConcurrentStrategy with next stream count, or None if complete
|
|
386
|
+
"""
|
|
387
|
+
_ = (prev_strategy, prev_benchmark) # unused
|
|
388
|
+
|
|
389
|
+
if len(self.completed_strategies) >= len(self.streams):
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
return ConcurrentStrategy(
|
|
393
|
+
streams=self.streams[len(self.completed_strategies)],
|
|
394
|
+
rampup_duration=self.rampup_duration,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@Profile.register("throughput")
|
|
399
|
+
class ThroughputProfile(Profile):
|
|
400
|
+
"""
|
|
401
|
+
Maximize system throughput with optional concurrency constraints.
|
|
402
|
+
|
|
403
|
+
Maximizes system throughput by maintaining maximum concurrent requests,
|
|
404
|
+
optionally constrained by a concurrency limit.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
type_: Literal["throughput"] = "throughput" # type: ignore[assignment]
|
|
408
|
+
max_concurrency: PositiveInt | None = Field(
|
|
409
|
+
default=None,
|
|
410
|
+
description="Maximum concurrent requests to schedule",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
@classmethod
|
|
414
|
+
def resolve_args(
|
|
415
|
+
cls,
|
|
416
|
+
rate_type: str,
|
|
417
|
+
rate: list[float] | None,
|
|
418
|
+
random_seed: int,
|
|
419
|
+
**kwargs: Any,
|
|
420
|
+
) -> dict[str, Any]:
|
|
421
|
+
"""
|
|
422
|
+
Resolve arguments for throughput profile construction.
|
|
423
|
+
|
|
424
|
+
:param rate_type: Profile type identifier (ignored)
|
|
425
|
+
:param rate: Rate parameter remapped to max_concurrency
|
|
426
|
+
:param random_seed: Random seed (ignored)
|
|
427
|
+
:param kwargs: Additional arguments passed through unchanged
|
|
428
|
+
:return: Resolved arguments dictionary
|
|
429
|
+
"""
|
|
430
|
+
_ = (rate_type, random_seed) # unused
|
|
431
|
+
# Remap rate to max_concurrency, strip out random_seed
|
|
432
|
+
kwargs.pop("random_seed", None)
|
|
433
|
+
if rate is not None and len(rate) > 0:
|
|
434
|
+
kwargs["max_concurrency"] = rate[0]
|
|
435
|
+
else:
|
|
436
|
+
# Require explicit max_concurrency; in the future max_concurrency
|
|
437
|
+
# should be dynamic and rate can specify some tunable
|
|
438
|
+
raise ValueError("ThroughputProfile requires a rate parameter")
|
|
439
|
+
return kwargs
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def strategy_types(self) -> list[str]:
|
|
443
|
+
"""
|
|
444
|
+
:return: Single throughput strategy type
|
|
445
|
+
"""
|
|
446
|
+
return [self.type_]
|
|
447
|
+
|
|
448
|
+
def next_strategy(
|
|
449
|
+
self,
|
|
450
|
+
prev_strategy: SchedulingStrategy | None,
|
|
451
|
+
prev_benchmark: Benchmark | None,
|
|
452
|
+
) -> ThroughputStrategy | None:
|
|
453
|
+
"""
|
|
454
|
+
Generate throughput strategy for first execution only.
|
|
455
|
+
|
|
456
|
+
:param prev_strategy: Previously completed strategy (unused)
|
|
457
|
+
:param prev_benchmark: Benchmark results from previous execution (unused)
|
|
458
|
+
:return: ThroughputStrategy for first execution, None afterward
|
|
459
|
+
"""
|
|
460
|
+
_ = (prev_strategy, prev_benchmark) # unused
|
|
461
|
+
if len(self.completed_strategies) >= 1:
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
return ThroughputStrategy(
|
|
465
|
+
max_concurrency=self.max_concurrency, rampup_duration=self.rampup_duration
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@Profile.register(["async", "constant", "poisson"])
|
|
470
|
+
class AsyncProfile(Profile):
|
|
471
|
+
"""
|
|
472
|
+
Schedule requests at specified rates using constant or Poisson patterns.
|
|
473
|
+
|
|
474
|
+
Schedules requests at specified rates using either constant interval or
|
|
475
|
+
Poisson distribution patterns for realistic load simulation.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
type_: Literal["async", "constant", "poisson"] = "async" # type: ignore[assignment]
|
|
479
|
+
strategy_type: Literal["constant", "poisson"] = Field(
|
|
480
|
+
description="Asynchronous strategy pattern type",
|
|
481
|
+
)
|
|
482
|
+
rate: list[PositiveFloat] = Field(
|
|
483
|
+
description="Request scheduling rates in requests per second",
|
|
484
|
+
)
|
|
485
|
+
max_concurrency: PositiveInt | None = Field(
|
|
486
|
+
default=None,
|
|
487
|
+
description="Maximum concurrent requests to schedule",
|
|
488
|
+
)
|
|
489
|
+
random_seed: int = Field(
|
|
490
|
+
default=42,
|
|
491
|
+
description="Random seed for Poisson distribution strategy",
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
@classmethod
|
|
495
|
+
def resolve_args(
|
|
496
|
+
cls,
|
|
497
|
+
rate_type: str,
|
|
498
|
+
rate: list[float] | None,
|
|
499
|
+
random_seed: int,
|
|
500
|
+
**kwargs: Any,
|
|
501
|
+
) -> dict[str, Any]:
|
|
502
|
+
"""
|
|
503
|
+
Resolve arguments for async profile construction.
|
|
504
|
+
|
|
505
|
+
:param rate_type: Profile type identifier
|
|
506
|
+
:param rate: Rate configuration for the profile
|
|
507
|
+
:param random_seed: Seed for stochastic strategies
|
|
508
|
+
:param kwargs: Additional arguments passed through unchanged
|
|
509
|
+
:return: Resolved arguments dictionary
|
|
510
|
+
:raises ValueError: If rate is None
|
|
511
|
+
"""
|
|
512
|
+
if rate is None:
|
|
513
|
+
raise ValueError("AsyncProfile requires a rate parameter")
|
|
514
|
+
|
|
515
|
+
kwargs["type_"] = (
|
|
516
|
+
rate_type
|
|
517
|
+
if rate_type in ["async", "constant", "poisson"]
|
|
518
|
+
else kwargs.get("type_", "async")
|
|
519
|
+
)
|
|
520
|
+
kwargs["strategy_type"] = (
|
|
521
|
+
rate_type
|
|
522
|
+
if rate_type in ["constant", "poisson"]
|
|
523
|
+
else kwargs.get("strategy_type", "constant")
|
|
524
|
+
)
|
|
525
|
+
kwargs["rate"] = rate if isinstance(rate, list) else [rate]
|
|
526
|
+
kwargs["random_seed"] = random_seed
|
|
527
|
+
return kwargs
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def strategy_types(self) -> list[str]:
|
|
531
|
+
"""
|
|
532
|
+
:return: Async strategy types for each configured rate
|
|
533
|
+
"""
|
|
534
|
+
num_strategies = len(self.rate)
|
|
535
|
+
return [self.strategy_type] * num_strategies
|
|
536
|
+
|
|
537
|
+
def next_strategy(
|
|
538
|
+
self,
|
|
539
|
+
prev_strategy: SchedulingStrategy | None,
|
|
540
|
+
prev_benchmark: Benchmark | None,
|
|
541
|
+
) -> AsyncConstantStrategy | AsyncPoissonStrategy | None:
|
|
542
|
+
"""
|
|
543
|
+
Generate async strategy for next configured rate.
|
|
544
|
+
|
|
545
|
+
:param prev_strategy: Previously completed strategy (unused)
|
|
546
|
+
:param prev_benchmark: Benchmark results from previous execution (unused)
|
|
547
|
+
:return: AsyncConstantStrategy or AsyncPoissonStrategy for next rate,
|
|
548
|
+
or None if all rates completed
|
|
549
|
+
:raises ValueError: If strategy_type is neither 'constant' nor 'poisson'
|
|
550
|
+
"""
|
|
551
|
+
_ = (prev_strategy, prev_benchmark) # unused
|
|
552
|
+
|
|
553
|
+
if len(self.completed_strategies) >= len(self.rate):
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
current_rate = self.rate[len(self.completed_strategies)]
|
|
557
|
+
|
|
558
|
+
if self.strategy_type == "constant":
|
|
559
|
+
return AsyncConstantStrategy(
|
|
560
|
+
rate=current_rate, max_concurrency=self.max_concurrency
|
|
561
|
+
)
|
|
562
|
+
elif self.strategy_type == "poisson":
|
|
563
|
+
return AsyncPoissonStrategy(
|
|
564
|
+
rate=current_rate,
|
|
565
|
+
max_concurrency=self.max_concurrency,
|
|
566
|
+
random_seed=self.random_seed,
|
|
567
|
+
)
|
|
568
|
+
else:
|
|
569
|
+
raise ValueError(f"Invalid strategy type: {self.strategy_type}")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@Profile.register("sweep")
|
|
573
|
+
class SweepProfile(Profile):
|
|
574
|
+
"""
|
|
575
|
+
Discover optimal rate range through adaptive multi-strategy execution.
|
|
576
|
+
|
|
577
|
+
Automatically discovers optimal rate range by executing synchronous and
|
|
578
|
+
throughput strategies first, then interpolating rates for async strategies
|
|
579
|
+
to comprehensively sweep the performance space.
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
type_: Literal["sweep"] = "sweep" # type: ignore[assignment]
|
|
583
|
+
sweep_size: int = Field(
|
|
584
|
+
description="Number of strategies to generate for the sweep",
|
|
585
|
+
ge=2,
|
|
586
|
+
)
|
|
587
|
+
strategy_type: Literal["constant", "poisson"] = "constant"
|
|
588
|
+
max_concurrency: PositiveInt | None = Field(
|
|
589
|
+
default=None,
|
|
590
|
+
description="Maximum concurrent requests to schedule",
|
|
591
|
+
)
|
|
592
|
+
random_seed: int = Field(
|
|
593
|
+
default=42,
|
|
594
|
+
description="Random seed for Poisson distribution strategy",
|
|
595
|
+
)
|
|
596
|
+
synchronous_rate: float = Field(
|
|
597
|
+
default=-1.0,
|
|
598
|
+
description="Measured rate from synchronous strategy execution",
|
|
599
|
+
)
|
|
600
|
+
throughput_rate: float = Field(
|
|
601
|
+
default=-1.0,
|
|
602
|
+
description="Measured rate from throughput strategy execution",
|
|
603
|
+
)
|
|
604
|
+
async_rates: list[float] = Field(
|
|
605
|
+
default_factory=list,
|
|
606
|
+
description="Generated rates for async strategy sweep",
|
|
607
|
+
)
|
|
608
|
+
measured_rates: list[float] = Field(
|
|
609
|
+
default_factory=list,
|
|
610
|
+
description="Interpolated rates between synchronous and throughput",
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
@classmethod
|
|
614
|
+
def resolve_args(
|
|
615
|
+
cls,
|
|
616
|
+
rate_type: str,
|
|
617
|
+
rate: list[float] | None,
|
|
618
|
+
random_seed: int,
|
|
619
|
+
**kwargs: Any,
|
|
620
|
+
) -> dict[str, Any]:
|
|
621
|
+
"""
|
|
622
|
+
Resolve arguments for sweep profile construction.
|
|
623
|
+
|
|
624
|
+
:param rate_type: Async strategy type for sweep execution
|
|
625
|
+
:param rate: Rate parameter specifying sweep size (if provided)
|
|
626
|
+
:param random_seed: Seed for stochastic strategies
|
|
627
|
+
:param kwargs: Additional arguments passed through unchanged
|
|
628
|
+
:return: Resolved arguments dictionary
|
|
629
|
+
"""
|
|
630
|
+
sweep_size_from_rate = int(rate[0]) if rate else settings.default_sweep_number
|
|
631
|
+
kwargs["sweep_size"] = kwargs.get("sweep_size", sweep_size_from_rate)
|
|
632
|
+
kwargs["random_seed"] = random_seed
|
|
633
|
+
if rate_type in ["constant", "poisson"]:
|
|
634
|
+
kwargs["strategy_type"] = rate_type
|
|
635
|
+
return kwargs
|
|
636
|
+
|
|
637
|
+
@property
|
|
638
|
+
def strategy_types(self) -> list[str]:
|
|
639
|
+
"""
|
|
640
|
+
:return: Strategy types for the complete sweep sequence
|
|
641
|
+
"""
|
|
642
|
+
types = ["synchronous", "throughput"]
|
|
643
|
+
types += [self.strategy_type] * (self.sweep_size - len(types))
|
|
644
|
+
return types
|
|
645
|
+
|
|
646
|
+
def next_strategy(
|
|
647
|
+
self,
|
|
648
|
+
prev_strategy: SchedulingStrategy | None,
|
|
649
|
+
prev_benchmark: Benchmark | None,
|
|
650
|
+
) -> (
|
|
651
|
+
AsyncConstantStrategy
|
|
652
|
+
| AsyncPoissonStrategy
|
|
653
|
+
| SynchronousStrategy
|
|
654
|
+
| ThroughputStrategy
|
|
655
|
+
| None
|
|
656
|
+
):
|
|
657
|
+
"""
|
|
658
|
+
Generate next strategy in adaptive sweep sequence.
|
|
659
|
+
|
|
660
|
+
Executes synchronous and throughput strategies first to measure baseline
|
|
661
|
+
rates, then generates interpolated rates for async strategies.
|
|
662
|
+
|
|
663
|
+
:param prev_strategy: Previously completed strategy instance
|
|
664
|
+
:param prev_benchmark: Benchmark results from previous strategy execution
|
|
665
|
+
:return: Next strategy in sweep sequence, or None if complete
|
|
666
|
+
:raises ValueError: If strategy_type is neither 'constant' nor 'poisson'
|
|
667
|
+
"""
|
|
668
|
+
if prev_strategy is None:
|
|
669
|
+
return SynchronousStrategy()
|
|
670
|
+
|
|
671
|
+
if prev_strategy.type_ == "synchronous":
|
|
672
|
+
self.synchronous_rate = prev_benchmark.request_throughput.successful.mean
|
|
673
|
+
|
|
674
|
+
return ThroughputStrategy(
|
|
675
|
+
max_concurrency=self.max_concurrency,
|
|
676
|
+
rampup_duration=self.rampup_duration,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
if prev_strategy.type_ == "throughput":
|
|
680
|
+
self.throughput_rate = prev_benchmark.request_throughput.successful.mean
|
|
681
|
+
if self.synchronous_rate <= 0 and self.throughput_rate <= 0:
|
|
682
|
+
raise RuntimeError(
|
|
683
|
+
"Invalid rates in sweep; aborting. "
|
|
684
|
+
"Were there any successful requests?"
|
|
685
|
+
)
|
|
686
|
+
self.measured_rates = list(
|
|
687
|
+
np.linspace(
|
|
688
|
+
self.synchronous_rate,
|
|
689
|
+
self.throughput_rate,
|
|
690
|
+
self.sweep_size - 1,
|
|
691
|
+
)
|
|
692
|
+
)[1:] # don't rerun synchronous
|
|
693
|
+
|
|
694
|
+
next_index = (
|
|
695
|
+
len(self.completed_strategies) - 1 - 1
|
|
696
|
+
) # subtract synchronous and throughput
|
|
697
|
+
next_rate = (
|
|
698
|
+
self.measured_rates[next_index]
|
|
699
|
+
if next_index < len(self.measured_rates)
|
|
700
|
+
else None
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
if next_rate is None or next_rate <= 0:
|
|
704
|
+
# Stop if we don't have another valid rate to run
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
if self.strategy_type == "constant":
|
|
708
|
+
return AsyncConstantStrategy(
|
|
709
|
+
rate=next_rate, max_concurrency=self.max_concurrency
|
|
710
|
+
)
|
|
711
|
+
elif self.strategy_type == "poisson":
|
|
712
|
+
return AsyncPoissonStrategy(
|
|
713
|
+
rate=next_rate,
|
|
714
|
+
max_concurrency=self.max_concurrency,
|
|
715
|
+
random_seed=self.random_seed,
|
|
716
|
+
)
|
|
717
|
+
else:
|
|
718
|
+
raise ValueError(f"Invalid strategy type: {self.strategy_type}")
|