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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. guidellm/__init__.py +5 -2
  2. guidellm/__main__.py +524 -255
  3. guidellm/backends/__init__.py +33 -0
  4. guidellm/backends/backend.py +109 -0
  5. guidellm/backends/openai.py +340 -0
  6. guidellm/backends/response_handlers.py +428 -0
  7. guidellm/benchmark/__init__.py +69 -39
  8. guidellm/benchmark/benchmarker.py +160 -316
  9. guidellm/benchmark/entrypoints.py +560 -127
  10. guidellm/benchmark/outputs/__init__.py +24 -0
  11. guidellm/benchmark/outputs/console.py +633 -0
  12. guidellm/benchmark/outputs/csv.py +721 -0
  13. guidellm/benchmark/outputs/html.py +473 -0
  14. guidellm/benchmark/outputs/output.py +169 -0
  15. guidellm/benchmark/outputs/serialized.py +69 -0
  16. guidellm/benchmark/profiles.py +718 -0
  17. guidellm/benchmark/progress.py +553 -556
  18. guidellm/benchmark/scenarios/__init__.py +40 -0
  19. guidellm/benchmark/scenarios/chat.json +6 -0
  20. guidellm/benchmark/scenarios/rag.json +6 -0
  21. guidellm/benchmark/schemas/__init__.py +66 -0
  22. guidellm/benchmark/schemas/base.py +402 -0
  23. guidellm/benchmark/schemas/generative/__init__.py +55 -0
  24. guidellm/benchmark/schemas/generative/accumulator.py +841 -0
  25. guidellm/benchmark/schemas/generative/benchmark.py +163 -0
  26. guidellm/benchmark/schemas/generative/entrypoints.py +381 -0
  27. guidellm/benchmark/schemas/generative/metrics.py +927 -0
  28. guidellm/benchmark/schemas/generative/report.py +158 -0
  29. guidellm/data/__init__.py +34 -4
  30. guidellm/data/builders.py +541 -0
  31. guidellm/data/collators.py +16 -0
  32. guidellm/data/config.py +120 -0
  33. guidellm/data/deserializers/__init__.py +49 -0
  34. guidellm/data/deserializers/deserializer.py +141 -0
  35. guidellm/data/deserializers/file.py +223 -0
  36. guidellm/data/deserializers/huggingface.py +94 -0
  37. guidellm/data/deserializers/memory.py +194 -0
  38. guidellm/data/deserializers/synthetic.py +246 -0
  39. guidellm/data/entrypoints.py +52 -0
  40. guidellm/data/loaders.py +190 -0
  41. guidellm/data/preprocessors/__init__.py +27 -0
  42. guidellm/data/preprocessors/formatters.py +410 -0
  43. guidellm/data/preprocessors/mappers.py +196 -0
  44. guidellm/data/preprocessors/preprocessor.py +30 -0
  45. guidellm/data/processor.py +29 -0
  46. guidellm/data/schemas.py +175 -0
  47. guidellm/data/utils/__init__.py +6 -0
  48. guidellm/data/utils/dataset.py +94 -0
  49. guidellm/extras/__init__.py +4 -0
  50. guidellm/extras/audio.py +220 -0
  51. guidellm/extras/vision.py +242 -0
  52. guidellm/logger.py +2 -2
  53. guidellm/mock_server/__init__.py +8 -0
  54. guidellm/mock_server/config.py +84 -0
  55. guidellm/mock_server/handlers/__init__.py +17 -0
  56. guidellm/mock_server/handlers/chat_completions.py +280 -0
  57. guidellm/mock_server/handlers/completions.py +280 -0
  58. guidellm/mock_server/handlers/tokenizer.py +142 -0
  59. guidellm/mock_server/models.py +510 -0
  60. guidellm/mock_server/server.py +238 -0
  61. guidellm/mock_server/utils.py +302 -0
  62. guidellm/scheduler/__init__.py +69 -26
  63. guidellm/scheduler/constraints/__init__.py +49 -0
  64. guidellm/scheduler/constraints/constraint.py +325 -0
  65. guidellm/scheduler/constraints/error.py +411 -0
  66. guidellm/scheduler/constraints/factory.py +182 -0
  67. guidellm/scheduler/constraints/request.py +312 -0
  68. guidellm/scheduler/constraints/saturation.py +722 -0
  69. guidellm/scheduler/environments.py +252 -0
  70. guidellm/scheduler/scheduler.py +137 -368
  71. guidellm/scheduler/schemas.py +358 -0
  72. guidellm/scheduler/strategies.py +617 -0
  73. guidellm/scheduler/worker.py +413 -419
  74. guidellm/scheduler/worker_group.py +712 -0
  75. guidellm/schemas/__init__.py +65 -0
  76. guidellm/schemas/base.py +417 -0
  77. guidellm/schemas/info.py +188 -0
  78. guidellm/schemas/request.py +235 -0
  79. guidellm/schemas/request_stats.py +349 -0
  80. guidellm/schemas/response.py +124 -0
  81. guidellm/schemas/statistics.py +1018 -0
  82. guidellm/{config.py → settings.py} +31 -24
  83. guidellm/utils/__init__.py +71 -8
  84. guidellm/utils/auto_importer.py +98 -0
  85. guidellm/utils/cli.py +132 -5
  86. guidellm/utils/console.py +566 -0
  87. guidellm/utils/encoding.py +778 -0
  88. guidellm/utils/functions.py +159 -0
  89. guidellm/utils/hf_datasets.py +1 -2
  90. guidellm/utils/hf_transformers.py +4 -4
  91. guidellm/utils/imports.py +9 -0
  92. guidellm/utils/messaging.py +1118 -0
  93. guidellm/utils/mixins.py +115 -0
  94. guidellm/utils/random.py +3 -4
  95. guidellm/utils/registry.py +220 -0
  96. guidellm/utils/singleton.py +133 -0
  97. guidellm/utils/synchronous.py +159 -0
  98. guidellm/utils/text.py +163 -50
  99. guidellm/utils/typing.py +41 -0
  100. guidellm/version.py +2 -2
  101. guidellm-0.6.0a5.dist-info/METADATA +364 -0
  102. guidellm-0.6.0a5.dist-info/RECORD +109 -0
  103. guidellm/backend/__init__.py +0 -23
  104. guidellm/backend/backend.py +0 -259
  105. guidellm/backend/openai.py +0 -708
  106. guidellm/backend/response.py +0 -136
  107. guidellm/benchmark/aggregator.py +0 -760
  108. guidellm/benchmark/benchmark.py +0 -837
  109. guidellm/benchmark/output.py +0 -997
  110. guidellm/benchmark/profile.py +0 -409
  111. guidellm/benchmark/scenario.py +0 -104
  112. guidellm/data/prideandprejudice.txt.gz +0 -0
  113. guidellm/dataset/__init__.py +0 -22
  114. guidellm/dataset/creator.py +0 -213
  115. guidellm/dataset/entrypoints.py +0 -42
  116. guidellm/dataset/file.py +0 -92
  117. guidellm/dataset/hf_datasets.py +0 -62
  118. guidellm/dataset/in_memory.py +0 -132
  119. guidellm/dataset/synthetic.py +0 -287
  120. guidellm/objects/__init__.py +0 -18
  121. guidellm/objects/pydantic.py +0 -89
  122. guidellm/objects/statistics.py +0 -953
  123. guidellm/preprocess/__init__.py +0 -3
  124. guidellm/preprocess/dataset.py +0 -374
  125. guidellm/presentation/__init__.py +0 -28
  126. guidellm/presentation/builder.py +0 -27
  127. guidellm/presentation/data_models.py +0 -232
  128. guidellm/presentation/injector.py +0 -66
  129. guidellm/request/__init__.py +0 -18
  130. guidellm/request/loader.py +0 -284
  131. guidellm/request/request.py +0 -79
  132. guidellm/request/types.py +0 -10
  133. guidellm/scheduler/queues.py +0 -25
  134. guidellm/scheduler/result.py +0 -155
  135. guidellm/scheduler/strategy.py +0 -495
  136. guidellm-0.3.1.dist-info/METADATA +0 -329
  137. guidellm-0.3.1.dist-info/RECORD +0 -62
  138. {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/WHEEL +0 -0
  139. {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/entry_points.txt +0 -0
  140. {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/licenses/LICENSE +0 -0
  141. {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/top_level.txt +0 -0
@@ -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}")