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