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,115 @@
1
+ """
2
+ Mixin classes for common metadata extraction and object introspection.
3
+
4
+ Provides reusable mixins for extracting structured metadata from objects,
5
+ enabling consistent information exposure across different class hierarchies.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ __all__ = ["InfoMixin"]
13
+
14
+
15
+ PYTHON_PRIMITIVES = (str, int, float, bool, list, tuple, dict)
16
+ """Type alias for serialized object representations"""
17
+
18
+
19
+ class InfoMixin:
20
+ """
21
+ Mixin class providing standardized metadata extraction for introspection.
22
+
23
+ Enables consistent object metadata extraction patterns across different
24
+ class hierarchies for debugging, serialization, and runtime analysis.
25
+ Provides both instance and class-level methods for extracting structured
26
+ information from arbitrary objects with fallback handling for objects
27
+ without built-in info capabilities.
28
+
29
+ Example:
30
+ ::
31
+ from guidellm.utils.mixins import InfoMixin
32
+
33
+ class ConfiguredClass(InfoMixin):
34
+ def __init__(self, setting: str):
35
+ self.setting = setting
36
+
37
+ obj = ConfiguredClass("value")
38
+ # Returns {'str': 'ConfiguredClass(...)', 'type': 'ConfiguredClass', ...}
39
+ print(obj.info)
40
+ """
41
+
42
+ @classmethod
43
+ def extract_from_obj(cls, obj: Any) -> dict[str, Any]:
44
+ """
45
+ Extract structured metadata from any object.
46
+
47
+ Attempts to use the object's own `info` method or property if available,
48
+ otherwise constructs metadata from object attributes and type information.
49
+ Provides consistent metadata format across different object types.
50
+
51
+ :param obj: Object to extract metadata from
52
+ :return: Dictionary containing object metadata including type, class,
53
+ module, and public attributes
54
+ """
55
+ if hasattr(obj, "info"):
56
+ return obj.info() if callable(obj.info) else obj.info
57
+
58
+ return {
59
+ "str": str(obj),
60
+ "type": type(obj).__name__,
61
+ "class": obj.__class__.__name__ if hasattr(obj, "__class__") else None,
62
+ "module": obj.__class__.__module__ if hasattr(obj, "__class__") else None,
63
+ "attributes": (
64
+ {
65
+ key: val if isinstance(val, PYTHON_PRIMITIVES) else repr(val)
66
+ for key, val in obj.__dict__.items()
67
+ if not key.startswith("_")
68
+ }
69
+ if hasattr(obj, "__dict__")
70
+ else {}
71
+ ),
72
+ }
73
+
74
+ @classmethod
75
+ def create_info_dict(cls, obj: Any) -> dict[str, Any]:
76
+ """
77
+ Create a structured info dictionary for the given object.
78
+
79
+ Builds standardized metadata dictionary containing object identification,
80
+ type information, and accessible attributes. Used internally by other
81
+ info extraction methods and available for direct metadata construction.
82
+
83
+ :param obj: Object to extract info from
84
+ :return: Dictionary containing structured metadata about the object
85
+ """
86
+ return {
87
+ "str": str(obj),
88
+ "type": type(obj).__name__,
89
+ "class": obj.__class__.__name__ if hasattr(obj, "__class__") else None,
90
+ "module": obj.__class__.__module__ if hasattr(obj, "__class__") else None,
91
+ "attributes": (
92
+ {
93
+ key: val
94
+ if isinstance(val, str | int | float | bool | list | dict)
95
+ else repr(val)
96
+ for key, val in obj.__dict__.items()
97
+ if not key.startswith("_")
98
+ }
99
+ if hasattr(obj, "__dict__")
100
+ else {}
101
+ ),
102
+ }
103
+
104
+ @property
105
+ def info(self) -> dict[str, Any]:
106
+ """
107
+ Return structured metadata about this instance.
108
+
109
+ Provides consistent access to object metadata for debugging, serialization,
110
+ and introspection. Uses the create_info_dict method to generate standardized
111
+ metadata format including class information and public attributes.
112
+
113
+ :return: Dictionary containing class name, module, and public attributes
114
+ """
115
+ return self.create_info_dict(self)
guidellm/utils/random.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import random
2
2
  from collections.abc import Iterator
3
- from typing import Optional
4
3
 
5
4
  __all__ = ["IntegerRangeSampler"]
6
5
 
@@ -9,9 +8,9 @@ class IntegerRangeSampler:
9
8
  def __init__(
10
9
  self,
11
10
  average: int,
12
- variance: Optional[int],
13
- min_value: Optional[int],
14
- max_value: Optional[int],
11
+ variance: int | None,
12
+ min_value: int | None,
13
+ max_value: int | None,
15
14
  random_seed: int,
16
15
  ):
17
16
  self.average = average
@@ -0,0 +1,220 @@
1
+ """
2
+ Registry system for dynamic object registration and discovery.
3
+
4
+ Provides a flexible object registration system with optional auto-discovery
5
+ capabilities through decorators and module imports. Enables dynamic discovery
6
+ and instantiation of implementations based on configuration parameters, supporting
7
+ both manual registration and automatic package-based discovery for extensible
8
+ plugin architectures.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable
14
+ from typing import ClassVar, Generic, TypeVar, cast
15
+
16
+ from guidellm.utils.auto_importer import AutoImporterMixin
17
+
18
+ __all__ = ["RegisterT", "RegistryMixin", "RegistryObjT"]
19
+
20
+
21
+ RegistryObjT = TypeVar("RegistryObjT")
22
+ """Generic type variable for objects managed by the registry system."""
23
+ RegisterT = TypeVar(
24
+ "RegisterT", bound=type
25
+ ) # Must be bound to type to ensure __name__ is available.
26
+ """Generic type variable for the args and return values within the registry."""
27
+
28
+
29
+ class RegistryMixin(Generic[RegistryObjT], AutoImporterMixin):
30
+ """
31
+ Generic mixin for creating object registries with optional auto-discovery.
32
+
33
+ Enables classes to maintain separate registries of objects that can be dynamically
34
+ discovered and instantiated through decorators and module imports. Supports both
35
+ manual registration via decorators and automatic discovery through package scanning
36
+ for extensible plugin architectures.
37
+
38
+ Example:
39
+ ::
40
+ class BaseAlgorithm(RegistryMixin):
41
+ pass
42
+
43
+ @BaseAlgorithm.register()
44
+ class ConcreteAlgorithm(BaseAlgorithm):
45
+ pass
46
+
47
+ @BaseAlgorithm.register("custom_name")
48
+ class AnotherAlgorithm(BaseAlgorithm):
49
+ pass
50
+
51
+ # Get all registered implementations
52
+ algorithms = BaseAlgorithm.registered_objects()
53
+
54
+ Example with auto-discovery:
55
+ ::
56
+ class TokenProposal(RegistryMixin):
57
+ registry_auto_discovery = True
58
+ auto_package = "mypackage.proposals"
59
+
60
+ # Automatically imports and registers decorated objects
61
+ proposals = TokenProposal.registered_objects()
62
+
63
+ :cvar registry: Dictionary mapping names to registered objects
64
+ :cvar registry_auto_discovery: Enable automatic package-based discovery
65
+ :cvar registry_populated: Track whether auto-discovery has completed
66
+ """
67
+
68
+ registry: ClassVar[dict[str, RegistryObjT] | None] = None # type: ignore[misc]
69
+ registry_auto_discovery: ClassVar[bool] = False
70
+ registry_populated: ClassVar[bool] = False
71
+
72
+ @classmethod
73
+ def register(
74
+ cls, name: str | list[str] | None = None
75
+ ) -> Callable[[RegisterT], RegisterT]:
76
+ """
77
+ Decorator for registering objects with the registry.
78
+
79
+ :param name: Optional name(s) to register the object under.
80
+ If None, uses the object's __name__ attribute
81
+ :return: Decorator function that registers the decorated object
82
+ :raises ValueError: If name is not a string, list of strings, or None
83
+ """
84
+
85
+ def _decorator(obj: RegisterT) -> RegisterT:
86
+ cls.register_decorator(obj, name=name)
87
+ return obj
88
+
89
+ return _decorator
90
+
91
+ @classmethod
92
+ def register_decorator(
93
+ cls, obj: RegisterT, name: str | list[str] | None = None
94
+ ) -> RegisterT:
95
+ """
96
+ Register an object directly with the registry.
97
+
98
+ :param obj: The object to register
99
+ :param name: Optional name(s) to register the object under.
100
+ If None, uses the object's __name__ attribute
101
+ :return: The registered object
102
+ :raises ValueError: If the object is already registered or name is invalid
103
+ """
104
+
105
+ if name is None:
106
+ name = obj.__name__
107
+ elif not isinstance(name, str | list):
108
+ raise ValueError(
109
+ "RegistryMixin.register_decorator name must be a string or "
110
+ f"an iterable of strings. Got {name}."
111
+ )
112
+
113
+ if cls.registry is None:
114
+ cls.registry = {}
115
+
116
+ names = [name] if isinstance(name, str) else list(name)
117
+
118
+ for register_name in names:
119
+ if not isinstance(register_name, str):
120
+ raise ValueError(
121
+ "RegistryMixin.register_decorator name must be a string or "
122
+ f"a list of strings. Got {register_name}."
123
+ )
124
+
125
+ if register_name in cls.registry:
126
+ raise ValueError(
127
+ f"RegistryMixin.register_decorator cannot register an object "
128
+ f"{obj} with the name {register_name} because it is already "
129
+ "registered."
130
+ )
131
+
132
+ cls.registry[register_name] = cast("RegistryObjT", obj)
133
+
134
+ return obj
135
+
136
+ @classmethod
137
+ def auto_populate_registry(cls) -> bool:
138
+ """
139
+ Import and register all modules from the auto_package.
140
+
141
+ Automatically called by registered_objects when registry_auto_discovery is True
142
+ to ensure all available implementations are discovered.
143
+
144
+ :return: True if registry was populated, False if already populated
145
+ :raises ValueError: If called when registry_auto_discovery is False
146
+ """
147
+ if not cls.registry_auto_discovery:
148
+ raise ValueError(
149
+ "RegistryMixin.auto_populate_registry() cannot be called "
150
+ "because registry_auto_discovery is set to False. "
151
+ "Set registry_auto_discovery to True to enable auto-discovery."
152
+ )
153
+
154
+ if cls.registry_populated:
155
+ return False
156
+
157
+ cls.auto_import_package_modules()
158
+ cls.registry_populated = True
159
+
160
+ return True
161
+
162
+ @classmethod
163
+ def registered_objects(cls) -> tuple[RegistryObjT, ...]:
164
+ """
165
+ Get all registered objects from the registry.
166
+
167
+ Automatically triggers auto-discovery if registry_auto_discovery is enabled
168
+ to ensure all available implementations are included.
169
+
170
+ :return: Tuple of all registered objects including auto-discovered ones
171
+ :raises ValueError: If called before any objects have been registered
172
+ """
173
+ if cls.registry_auto_discovery:
174
+ cls.auto_populate_registry()
175
+
176
+ if cls.registry is None:
177
+ raise ValueError(
178
+ "RegistryMixin.registered_objects() must be called after "
179
+ "registering objects with RegistryMixin.register()."
180
+ )
181
+
182
+ return tuple(cls.registry.values())
183
+
184
+ @classmethod
185
+ def is_registered(cls, name: str) -> bool:
186
+ """
187
+ Check if an object is registered under the given name.
188
+ It matches first by exact name, then by str.lower().
189
+
190
+ :param name: The name to check for registration.
191
+ :return: True if the object is registered, False otherwise.
192
+ """
193
+ if cls.registry is None:
194
+ return False
195
+
196
+ return name in cls.registry or name.lower() in [
197
+ key.lower() for key in cls.registry
198
+ ]
199
+
200
+ @classmethod
201
+ def get_registered_object(cls, name: str) -> RegistryObjT | None:
202
+ """
203
+ Get a registered object by its name. It matches first by exact name,
204
+ then by str.lower().
205
+
206
+ :param name: The name of the registered object.
207
+ :return: The registered object if found, None otherwise.
208
+ """
209
+ if cls.registry is None:
210
+ return None
211
+
212
+ if name in cls.registry:
213
+ return cls.registry[name]
214
+
215
+ name_casefold = name.lower()
216
+ for k, v in cls.registry.items():
217
+ if name_casefold == k.lower():
218
+ return v
219
+
220
+ return None # Not found
@@ -0,0 +1,133 @@
1
+ """
2
+ Singleton pattern implementations for ensuring single instance classes.
3
+
4
+ Provides singleton mixins for creating classes that maintain a single instance
5
+ throughout the application lifecycle, with support for both basic and thread-safe
6
+ implementations. These mixins integrate with the scheduler and other system components
7
+ to ensure consistent state management and prevent duplicate resource allocation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+
14
+ __all__ = ["SingletonMixin", "ThreadSafeSingletonMixin"]
15
+
16
+
17
+ class SingletonMixin:
18
+ """
19
+ Basic singleton mixin ensuring single instance per class.
20
+
21
+ Implements the singleton pattern using class variables to control instance
22
+ creation. Subclasses must call super().__init__() for proper initialization
23
+ state management. Suitable for single-threaded environments or when external
24
+ synchronization is provided.
25
+
26
+ Example:
27
+ ::
28
+ class ConfigManager(SingletonMixin):
29
+ def __init__(self, config_path: str):
30
+ super().__init__()
31
+ if not self.initialized:
32
+ self.config = load_config(config_path)
33
+
34
+ manager1 = ConfigManager("config.json")
35
+ manager2 = ConfigManager("config.json")
36
+ assert manager1 is manager2
37
+ """
38
+
39
+ _singleton_initialized: bool
40
+ _init_lock: threading.Lock
41
+
42
+ def __new__(cls, *args, **kwargs): # noqa: ARG004
43
+ """
44
+ Create or return the singleton instance.
45
+
46
+ :param args: Positional arguments passed to the constructor
47
+ :param kwargs: Keyword arguments passed to the constructor
48
+ :return: The singleton instance of the class
49
+ """
50
+ # Use class-specific attribute name to avoid inheritance issues
51
+ attr_name = f"_singleton_instance_{cls.__name__}"
52
+
53
+ if not hasattr(cls, attr_name) or getattr(cls, attr_name) is None:
54
+ instance = super().__new__(cls)
55
+ setattr(cls, attr_name, instance)
56
+ instance._singleton_initialized = False
57
+ return getattr(cls, attr_name)
58
+
59
+ def __init__(self):
60
+ """Initialize the singleton instance exactly once."""
61
+ if hasattr(self, "_singleton_initialized") and self._singleton_initialized:
62
+ return
63
+ self._singleton_initialized = True
64
+
65
+ @property
66
+ def initialized(self):
67
+ """Return True if the singleton has been initialized."""
68
+ return getattr(self, "_singleton_initialized", False)
69
+
70
+
71
+ class ThreadSafeSingletonMixin(SingletonMixin):
72
+ """
73
+ Thread-safe singleton mixin with locking mechanisms.
74
+
75
+ Extends SingletonMixin with thread safety using locks to prevent race
76
+ conditions during instance creation in multi-threaded environments. Essential
77
+ for scheduler components and other shared resources accessed concurrently.
78
+
79
+ Example:
80
+ ::
81
+ class SchedulerResource(ThreadSafeSingletonMixin):
82
+ def __init__(self):
83
+ super().__init__()
84
+ if not self.initialized:
85
+ self.resource_pool = initialize_resources()
86
+ """
87
+
88
+ def __new__(cls, *args, **kwargs): # noqa: ARG004
89
+ """
90
+ Create or return the singleton instance with thread safety.
91
+
92
+ :param args: Positional arguments passed to the constructor
93
+ :param kwargs: Keyword arguments passed to the constructor
94
+ :return: The singleton instance of the class
95
+ """
96
+ # Use class-specific lock and instance names to avoid inheritance issues
97
+ lock_attr_name = f"_singleton_lock_{cls.__name__}"
98
+ instance_attr_name = f"_singleton_instance_{cls.__name__}"
99
+
100
+ with getattr(cls, lock_attr_name):
101
+ instance_exists = (
102
+ hasattr(cls, instance_attr_name)
103
+ and getattr(cls, instance_attr_name) is not None
104
+ )
105
+ if not instance_exists:
106
+ instance = super(SingletonMixin, cls).__new__(cls)
107
+ setattr(cls, instance_attr_name, instance)
108
+ instance._singleton_initialized = False
109
+ instance._init_lock = threading.Lock()
110
+ return getattr(cls, instance_attr_name)
111
+
112
+ def __init_subclass__(cls, *args, **kwargs):
113
+ super().__init_subclass__(*args, **kwargs)
114
+ lock_attr_name = f"_singleton_lock_{cls.__name__}"
115
+ setattr(cls, lock_attr_name, threading.Lock())
116
+
117
+ def __init__(self):
118
+ """Initialize the singleton instance with thread-safe initialization."""
119
+ with self._init_lock:
120
+ if hasattr(self, "_singleton_initialized") and self._singleton_initialized:
121
+ return
122
+ self._singleton_initialized = True
123
+
124
+ @property
125
+ def thread_lock(self):
126
+ """Return the thread lock for this singleton instance."""
127
+ return getattr(self, "_init_lock", None)
128
+
129
+ @classmethod
130
+ def get_singleton_lock(cls):
131
+ """Get the class-specific singleton creation lock."""
132
+ lock_attr_name = f"_singleton_lock_{cls.__name__}"
133
+ return getattr(cls, lock_attr_name, None)
@@ -0,0 +1,159 @@
1
+ """
2
+ Async utilities for waiting on synchronization objects.
3
+
4
+ This module provides async-compatible wrappers for threading and multiprocessing
5
+ synchronization primitives (Events and Barriers). These utilities enable async code
6
+ to wait for synchronization objects without blocking the event loop, essential for
7
+ coordinating between async and sync code or between processes in the guidellm system.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import contextlib
14
+ import time
15
+ from multiprocessing.synchronize import Barrier as ProcessingBarrier
16
+ from multiprocessing.synchronize import Event as ProcessingEvent
17
+ from threading import Barrier as ThreadingBarrier
18
+ from threading import Event as ThreadingEvent
19
+
20
+ __all__ = [
21
+ "SyncObjectTypesAlias",
22
+ "wait_for_sync_barrier",
23
+ "wait_for_sync_event",
24
+ "wait_for_sync_objects",
25
+ ]
26
+
27
+
28
+ # Type alias for threading and multiprocessing synchronization object types
29
+ SyncObjectTypesAlias = (
30
+ ThreadingEvent | ProcessingEvent | ThreadingBarrier | ProcessingBarrier
31
+ )
32
+
33
+
34
+ async def wait_for_sync_event(
35
+ event: ThreadingEvent | ProcessingEvent,
36
+ poll_interval: float,
37
+ ) -> None:
38
+ """
39
+ Asynchronously wait for a threading or multiprocessing Event to be set.
40
+
41
+ This function polls the event at regular intervals without blocking the async
42
+ event loop, allowing other async tasks to continue executing while waiting.
43
+
44
+ :param event: The Event object to wait for (threading or multiprocessing)
45
+ :param poll_interval: Time in seconds between polling checks
46
+ :raises asyncio.CancelledError: If the async task is cancelled
47
+ """
48
+ stop = ThreadingEvent()
49
+
50
+ def _watch():
51
+ try:
52
+ while not stop.is_set():
53
+ if event.wait(timeout=poll_interval):
54
+ return
55
+ except Exception as err: # noqa: BLE001
56
+ if stop.is_set():
57
+ return # Ignore error if we should have stopped
58
+ raise err
59
+
60
+ try:
61
+ await asyncio.to_thread(_watch)
62
+ except asyncio.CancelledError:
63
+ stop.set()
64
+ raise
65
+
66
+
67
+ async def wait_for_sync_barrier(
68
+ barrier: ThreadingBarrier | ProcessingBarrier,
69
+ poll_interval: float,
70
+ ) -> None:
71
+ """
72
+ Asynchronously wait for a threading or multiprocessing Barrier to be reached.
73
+
74
+ This function polls the barrier at regular intervals without blocking the async
75
+ event loop, allowing other async tasks to continue executing while waiting.
76
+
77
+ :param barrier: The Barrier object to wait for (threading or multiprocessing)
78
+ :param poll_interval: Time in seconds between polling checks
79
+ :raises asyncio.CancelledError: If the async task is cancelled
80
+ """
81
+ stop = ThreadingEvent()
82
+ barrier_broken = ThreadingEvent()
83
+
84
+ def _wait_indefinite():
85
+ try:
86
+ # wait forever, count on barrier broken event to exit
87
+ barrier.wait()
88
+ barrier_broken.set()
89
+ except Exception as err:
90
+ if stop.is_set():
91
+ return # Ignore error if we should have stopped
92
+ raise err
93
+
94
+ def _watch():
95
+ while not barrier_broken.is_set():
96
+ if stop.is_set():
97
+ with contextlib.suppress(Exception):
98
+ if not barrier.broken:
99
+ barrier.abort()
100
+ break
101
+ time.sleep(poll_interval)
102
+
103
+ try:
104
+ await asyncio.gather(
105
+ asyncio.to_thread(_wait_indefinite),
106
+ asyncio.to_thread(_watch),
107
+ )
108
+ except asyncio.CancelledError:
109
+ stop.set()
110
+ raise
111
+
112
+
113
+ async def wait_for_sync_objects(
114
+ objects: SyncObjectTypesAlias
115
+ | list[SyncObjectTypesAlias]
116
+ | dict[str, SyncObjectTypesAlias],
117
+ poll_interval: float = 0.1,
118
+ ) -> int | str:
119
+ """
120
+ Asynchronously wait for the first synchronization object to complete.
121
+
122
+ This function waits for the first Event to be set or Barrier to be reached
123
+ from a collection of synchronization objects. It returns immediately when
124
+ any object completes and cancels waiting on the remaining objects.
125
+
126
+ :param objects: Single sync object, list of objects, or dict mapping names
127
+ to objects
128
+ :param poll_interval: Time in seconds between polling checks for each object
129
+ :return: Index (for list/single) or key name (for dict) of the first
130
+ completed object
131
+ :raises asyncio.CancelledError: If the async task is canceled
132
+ """
133
+ keys: list[int | str]
134
+ if isinstance(objects, dict):
135
+ keys = list(objects.keys())
136
+ objects = list(objects.values())
137
+ elif isinstance(objects, list):
138
+ keys = list(range(len(objects)))
139
+ else:
140
+ keys = [0]
141
+ objects = [objects]
142
+
143
+ tasks = [
144
+ asyncio.create_task(
145
+ wait_for_sync_barrier(obj, poll_interval)
146
+ if isinstance(obj, ThreadingBarrier | ProcessingBarrier)
147
+ else wait_for_sync_event(obj, poll_interval)
148
+ )
149
+ for obj in objects
150
+ ]
151
+
152
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
153
+
154
+ # Cancel the remaining pending tasks
155
+ for pend in pending:
156
+ pend.cancel()
157
+ await asyncio.gather(*pending, return_exceptions=True)
158
+
159
+ return keys[tasks.index(list(done)[0])]