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.

Files changed (116) hide show
  1. guidellm/__init__.py +5 -2
  2. guidellm/__main__.py +451 -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 +148 -317
  9. guidellm/benchmark/entrypoints.py +466 -128
  10. guidellm/benchmark/output.py +517 -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 +2085 -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 +109 -0
  21. guidellm/data/deserializers/file.py +222 -0
  22. guidellm/data/deserializers/huggingface.py +94 -0
  23. guidellm/data/deserializers/memory.py +192 -0
  24. guidellm/data/deserializers/synthetic.py +346 -0
  25. guidellm/data/loaders.py +145 -0
  26. guidellm/data/preprocessors/__init__.py +25 -0
  27. guidellm/data/preprocessors/formatters.py +412 -0
  28. guidellm/data/preprocessors/mappers.py +198 -0
  29. guidellm/data/preprocessors/preprocessor.py +29 -0
  30. guidellm/data/processor.py +30 -0
  31. guidellm/data/schemas.py +13 -0
  32. guidellm/data/utils/__init__.py +10 -0
  33. guidellm/data/utils/dataset.py +94 -0
  34. guidellm/data/utils/functions.py +18 -0
  35. guidellm/extras/__init__.py +4 -0
  36. guidellm/extras/audio.py +215 -0
  37. guidellm/extras/vision.py +242 -0
  38. guidellm/logger.py +2 -2
  39. guidellm/mock_server/__init__.py +8 -0
  40. guidellm/mock_server/config.py +84 -0
  41. guidellm/mock_server/handlers/__init__.py +17 -0
  42. guidellm/mock_server/handlers/chat_completions.py +280 -0
  43. guidellm/mock_server/handlers/completions.py +280 -0
  44. guidellm/mock_server/handlers/tokenizer.py +142 -0
  45. guidellm/mock_server/models.py +510 -0
  46. guidellm/mock_server/server.py +168 -0
  47. guidellm/mock_server/utils.py +302 -0
  48. guidellm/preprocess/dataset.py +23 -26
  49. guidellm/presentation/builder.py +2 -2
  50. guidellm/presentation/data_models.py +25 -21
  51. guidellm/presentation/injector.py +2 -3
  52. guidellm/scheduler/__init__.py +65 -26
  53. guidellm/scheduler/constraints.py +1035 -0
  54. guidellm/scheduler/environments.py +252 -0
  55. guidellm/scheduler/scheduler.py +140 -368
  56. guidellm/scheduler/schemas.py +272 -0
  57. guidellm/scheduler/strategies.py +519 -0
  58. guidellm/scheduler/worker.py +391 -420
  59. guidellm/scheduler/worker_group.py +707 -0
  60. guidellm/schemas/__init__.py +31 -0
  61. guidellm/schemas/info.py +159 -0
  62. guidellm/schemas/request.py +216 -0
  63. guidellm/schemas/response.py +119 -0
  64. guidellm/schemas/stats.py +228 -0
  65. guidellm/{config.py → settings.py} +32 -21
  66. guidellm/utils/__init__.py +95 -8
  67. guidellm/utils/auto_importer.py +98 -0
  68. guidellm/utils/cli.py +46 -2
  69. guidellm/utils/console.py +183 -0
  70. guidellm/utils/encoding.py +778 -0
  71. guidellm/utils/functions.py +134 -0
  72. guidellm/utils/hf_datasets.py +1 -2
  73. guidellm/utils/hf_transformers.py +4 -4
  74. guidellm/utils/imports.py +9 -0
  75. guidellm/utils/messaging.py +1118 -0
  76. guidellm/utils/mixins.py +115 -0
  77. guidellm/utils/pydantic_utils.py +411 -0
  78. guidellm/utils/random.py +3 -4
  79. guidellm/utils/registry.py +220 -0
  80. guidellm/utils/singleton.py +133 -0
  81. guidellm/{objects → utils}/statistics.py +341 -247
  82. guidellm/utils/synchronous.py +159 -0
  83. guidellm/utils/text.py +163 -50
  84. guidellm/utils/typing.py +41 -0
  85. guidellm/version.py +1 -1
  86. {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/METADATA +33 -10
  87. guidellm-0.4.0a155.dist-info/RECORD +96 -0
  88. guidellm/backend/__init__.py +0 -23
  89. guidellm/backend/backend.py +0 -259
  90. guidellm/backend/openai.py +0 -705
  91. guidellm/backend/response.py +0 -136
  92. guidellm/benchmark/aggregator.py +0 -760
  93. guidellm/benchmark/benchmark.py +0 -837
  94. guidellm/benchmark/scenario.py +0 -104
  95. guidellm/data/prideandprejudice.txt.gz +0 -0
  96. guidellm/dataset/__init__.py +0 -22
  97. guidellm/dataset/creator.py +0 -213
  98. guidellm/dataset/entrypoints.py +0 -42
  99. guidellm/dataset/file.py +0 -92
  100. guidellm/dataset/hf_datasets.py +0 -62
  101. guidellm/dataset/in_memory.py +0 -132
  102. guidellm/dataset/synthetic.py +0 -287
  103. guidellm/objects/__init__.py +0 -18
  104. guidellm/objects/pydantic.py +0 -89
  105. guidellm/request/__init__.py +0 -18
  106. guidellm/request/loader.py +0 -284
  107. guidellm/request/request.py +0 -79
  108. guidellm/request/types.py +0 -10
  109. guidellm/scheduler/queues.py +0 -25
  110. guidellm/scheduler/result.py +0 -155
  111. guidellm/scheduler/strategy.py +0 -495
  112. guidellm-0.4.0a18.dist-info/RECORD +0 -62
  113. {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/WHEEL +0 -0
  114. {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/entry_points.txt +0 -0
  115. {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/licenses/LICENSE +0 -0
  116. {guidellm-0.4.0a18.dist-info → guidellm-0.4.0a155.dist-info}/top_level.txt +0 -0
@@ -1,20 +1,46 @@
1
- from collections.abc import Sequence
2
- from typing import Literal, Optional, Union
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 Field, computed_field
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.config import settings
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(StandardBaseModel):
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="The type of benchmarking profile to use.",
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
- measured_rates: list[float] = Field(
130
+ completed_strategies: list[SchedulingStrategy] = Field(
42
131
  default_factory=list,
43
- description=("The average rates measured for the strategies that have run."),
132
+ description="Strategies that have completed execution in this profile",
44
133
  )
45
- measured_concurrencies: list[float] = Field(
46
- default_factory=list,
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
- return []
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
- def next_strategy(self) -> Optional[SchedulingStrategy]:
63
- return None
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(self) -> Optional[SchedulingStrategy]:
74
- if self.completed_strategies >= 1:
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: Union[int, Sequence[int]] = Field(
104
- description="The number of concurrent streams to use.",
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
- num_strategies = len(self.streams) if isinstance(self.streams, Sequence) else 1
110
-
111
- return [self.type_] * num_strategies
112
-
113
- def next_strategy(self) -> Optional[SchedulingStrategy]:
114
- streams = self.streams if isinstance(self.streams, Sequence) else [self.streams]
115
-
116
- if self.completed_strategies >= len(streams):
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: Optional[int] = Field(
385
+ max_concurrency: PositiveInt | None = Field(
154
386
  default=None,
155
- description="The maximum number of concurrent requests that can be scheduled.",
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(self) -> Optional[SchedulingStrategy]:
163
- if self.completed_strategies >= 1:
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
- class AsyncProfile(ThroughputProfile):
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="The type of asynchronous strategy to use.",
456
+ description="Asynchronous strategy pattern type to use",
191
457
  )
192
- rate: Union[float, Sequence[float]] = Field(
193
- description="The rate of requests per second to use.",
458
+ rate: list[PositiveFloat] = Field(
459
+ description="Request scheduling rate in requests per second",
194
460
  )
195
- initial_burst: bool = Field(
196
- default=True,
461
+ startup_duration: NonNegativeFloat = Field(
462
+ default=0.0,
197
463
  description=(
198
- "True to send an initial burst of requests (math.floor(self.rate)) "
199
- "to reach target rate. False to not send an initial burst."
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
- num_strategies = len(self.rate) if isinstance(self.rate, Sequence) else 1
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(self) -> Optional[SchedulingStrategy]:
217
- rate = self.rate if isinstance(self.rate, Sequence) else [self.rate]
218
-
219
- if self.completed_strategies >= len(rate):
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=rate[self.completed_strategies],
225
- initial_burst=self.initial_burst,
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=rate[self.completed_strategies],
231
- initial_burst=self.initial_burst,
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
- if not all(isinstance(r, (float, int)) and r > 0 for r in rate):
258
- raise ValueError(
259
- f"All rate values must be positive numbers, received {rate}"
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="The number of strategies to generate for the sweep.",
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
- rate: float = -1
279
- rate_type: Literal["constant", "poisson"] = "constant"
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
- return (
284
- ["synchronous"] + ["throughput"] + [self.rate_type] * (self.sweep_size - 2) # type: ignore[return-value]
285
- )
286
-
287
- def next_strategy(self) -> Optional[SchedulingStrategy]:
288
- if self.completed_strategies >= self.sweep_size:
289
- return None
290
-
291
- if self.completed_strategies == 0:
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 self.completed_strategies == 1:
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
- min_rate = self.measured_rates[0]
300
- max_rate = self.measured_rates[1]
301
- rates = np.linspace(min_rate, max_rate, self.sweep_size - 1)[1:]
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.rate_type == "constant":
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=rates[self.completed_strategies - 2],
306
- initial_burst=self.initial_burst,
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.rate_type == "poisson":
701
+ elif self.strategy_type == "poisson":
310
702
  return AsyncPoissonStrategy(
311
- rate=rates[self.completed_strategies - 2],
312
- initial_burst=self.initial_burst,
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.rate_type}")
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}")