mozo 0.1.0__py3-none-any.whl → 0.2.0__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.
mozo/manager.py ADDED
@@ -0,0 +1,294 @@
1
+ """
2
+ Model Manager for Mozo
3
+
4
+ Manages the lifecycle of model instances including:
5
+ - Lazy loading (models loaded on-demand, not at startup)
6
+ - Thread-safe access
7
+ - Usage tracking
8
+ - Automatic cleanup of inactive models
9
+ - Memory management
10
+ """
11
+
12
+ import time
13
+ import gc
14
+ from threading import Lock
15
+ from typing import Dict, List, Optional
16
+ from .factory import ModelFactory
17
+
18
+
19
+ class ModelManager:
20
+ """
21
+ Manages the lifecycle of model instances.
22
+
23
+ Key features:
24
+ - Lazy loading: Models are only loaded when first requested
25
+ - Thread-safe: Multiple concurrent requests handled correctly
26
+ - Usage tracking: Timestamp recorded for each model access
27
+ - Memory management: Can unload models to free memory
28
+
29
+ Similar to the vision engine's dynamic model deployment pattern.
30
+ """
31
+
32
+ def __init__(self):
33
+ """Initialize the model manager."""
34
+ self._models: Dict[str, object] = {} # model_id → model instance
35
+ self._last_used: Dict[str, float] = {} # model_id → timestamp
36
+ self._locks: Dict[str, Lock] = {} # model_id → Lock (for thread-safe loading)
37
+ self._global_lock = Lock() # Lock for managing _locks dict
38
+ self._factory = ModelFactory()
39
+
40
+ def _get_model_id(self, family: str, variant: str) -> str:
41
+ """
42
+ Generate a unique model ID from family and variant.
43
+
44
+ Args:
45
+ family: Model family name
46
+ variant: Model variant name
47
+
48
+ Returns:
49
+ str: Unique model identifier in format 'family/variant'
50
+ """
51
+ return f"{family}/{variant}"
52
+
53
+ def _parse_model_id(self, model_id: str) -> tuple:
54
+ """
55
+ Parse a model ID into family and variant components.
56
+
57
+ Args:
58
+ model_id: Model identifier in format 'family/variant'
59
+
60
+ Returns:
61
+ tuple: (family, variant)
62
+
63
+ Raises:
64
+ ValueError: If model_id format is invalid
65
+ """
66
+ if '/' not in model_id:
67
+ raise ValueError(
68
+ f"Invalid model_id format: '{model_id}'. "
69
+ f"Expected format: 'family/variant' (e.g., 'detectron2/mask_rcnn_R_50_FPN_3x')"
70
+ )
71
+
72
+ parts = model_id.split('/', 1)
73
+ return parts[0], parts[1]
74
+
75
+ def get_model(self, family: str, variant: str):
76
+ """
77
+ Get a model instance, loading it if necessary (lazy loading).
78
+
79
+ This method is thread-safe: if multiple threads request the same model
80
+ simultaneously, only one will load it while others wait.
81
+
82
+ Args:
83
+ family: Model family name (e.g., 'detectron2', 'depth_anything')
84
+ variant: Model variant name (e.g., 'mask_rcnn_R_50_FPN_3x', 'small')
85
+
86
+ Returns:
87
+ Model predictor instance
88
+
89
+ Raises:
90
+ ValueError: If family or variant is invalid
91
+ RuntimeError: If model fails to load
92
+
93
+ Example:
94
+ >>> manager = ModelManager()
95
+ >>> model = manager.get_model('detectron2', 'mask_rcnn_R_50_FPN_3x')
96
+ >>> predictions = model.predict(image)
97
+ """
98
+ model_id = self._get_model_id(family, variant)
99
+
100
+ # Ensure a lock exists for this model (thread-safe lock creation)
101
+ with self._global_lock:
102
+ if model_id not in self._locks:
103
+ self._locks[model_id] = Lock()
104
+
105
+ # Only one thread can load a given model at a time
106
+ with self._locks[model_id]:
107
+ # Check if model is already loaded
108
+ if model_id not in self._models:
109
+ print(f"[ModelManager] Loading model: {model_id} (family={family}, variant={variant})...")
110
+ try:
111
+ self._models[model_id] = self._factory.create_model(family, variant)
112
+ print(f"[ModelManager] Model {model_id} loaded successfully.")
113
+ except Exception as e:
114
+ print(f"[ModelManager] ERROR: Failed to load model {model_id}: {e}")
115
+ raise RuntimeError(f"Failed to load model {model_id}") from e
116
+ else:
117
+ print(f"[ModelManager] Model {model_id} already loaded, reusing existing instance.")
118
+
119
+ # Update last used timestamp
120
+ self._last_used[model_id] = time.time()
121
+
122
+ return self._models[model_id]
123
+
124
+ def get_model_by_id(self, model_id: str):
125
+ """
126
+ Get a model instance by its full ID (family/variant format).
127
+
128
+ Args:
129
+ model_id: Full model identifier (e.g., 'detectron2/mask_rcnn_R_50_FPN_3x')
130
+
131
+ Returns:
132
+ Model predictor instance
133
+
134
+ Raises:
135
+ ValueError: If model_id format is invalid
136
+ """
137
+ family, variant = self._parse_model_id(model_id)
138
+ return self.get_model(family, variant)
139
+
140
+ def unload_model(self, family: str, variant: str) -> bool:
141
+ """
142
+ Explicitly unload a model to free memory.
143
+
144
+ Args:
145
+ family: Model family name
146
+ variant: Model variant name
147
+
148
+ Returns:
149
+ bool: True if model was unloaded, False if it wasn't loaded
150
+
151
+ Example:
152
+ >>> manager.unload_model('detectron2', 'mask_rcnn_R_50_FPN_3x')
153
+ """
154
+ model_id = self._get_model_id(family, variant)
155
+ return self.unload_model_by_id(model_id)
156
+
157
+ def unload_model_by_id(self, model_id: str) -> bool:
158
+ """
159
+ Explicitly unload a model by its ID to free memory.
160
+
161
+ Args:
162
+ model_id: Full model identifier
163
+
164
+ Returns:
165
+ bool: True if model was unloaded, False if it wasn't loaded
166
+ """
167
+ if model_id in self._models:
168
+ print(f"[ModelManager] Unloading model: {model_id}...")
169
+ del self._models[model_id]
170
+ if model_id in self._last_used:
171
+ del self._last_used[model_id]
172
+
173
+ # Explicitly trigger garbage collection to free memory
174
+ gc.collect()
175
+
176
+ print(f"[ModelManager] Model {model_id} unloaded successfully.")
177
+ return True
178
+ else:
179
+ print(f"[ModelManager] Model {model_id} not loaded, nothing to unload.")
180
+ return False
181
+
182
+ def list_loaded_models(self) -> List[str]:
183
+ """
184
+ Get list of currently loaded model IDs.
185
+
186
+ Returns:
187
+ list: Model IDs of all loaded models
188
+ """
189
+ return list(self._models.keys())
190
+
191
+ def get_inactive_models(self, inactive_seconds: int = 600) -> List[str]:
192
+ """
193
+ Find models that haven't been used in the specified time period.
194
+
195
+ Args:
196
+ inactive_seconds: Time threshold in seconds (default: 600 = 10 minutes)
197
+
198
+ Returns:
199
+ list: Model IDs of inactive models
200
+ """
201
+ current_time = time.time()
202
+ inactive = []
203
+
204
+ for model_id, last_used in self._last_used.items():
205
+ if current_time - last_used > inactive_seconds:
206
+ inactive.append(model_id)
207
+
208
+ return inactive
209
+
210
+ def cleanup_inactive_models(self, inactive_seconds: int = 600) -> int:
211
+ """
212
+ Automatically unload models that haven't been used recently.
213
+
214
+ This is similar to the vision engine's automatic model cleanup.
215
+
216
+ Args:
217
+ inactive_seconds: Time threshold in seconds (default: 600 = 10 minutes)
218
+
219
+ Returns:
220
+ int: Number of models unloaded
221
+
222
+ Example:
223
+ >>> # Unload models inactive for more than 10 minutes
224
+ >>> count = manager.cleanup_inactive_models(600)
225
+ >>> print(f"Unloaded {count} inactive models")
226
+ """
227
+ inactive_models = self.get_inactive_models(inactive_seconds)
228
+ count = 0
229
+
230
+ for model_id in inactive_models:
231
+ if self.unload_model_by_id(model_id):
232
+ count += 1
233
+
234
+ if count > 0:
235
+ print(f"[ModelManager] Cleanup: Unloaded {count} inactive model(s).")
236
+
237
+ return count
238
+
239
+ def get_model_info(self, model_id: Optional[str] = None) -> dict:
240
+ """
241
+ Get information about loaded models.
242
+
243
+ Args:
244
+ model_id: Optional model ID. If None, returns info about all loaded models
245
+
246
+ Returns:
247
+ dict: Model information including load status and last used time
248
+ """
249
+ if model_id is None:
250
+ # Return info for all loaded models
251
+ result = {}
252
+ current_time = time.time()
253
+ for mid in self._models.keys():
254
+ last_used = self._last_used.get(mid, 0)
255
+ result[mid] = {
256
+ 'loaded': True,
257
+ 'last_used': last_used,
258
+ 'inactive_seconds': int(current_time - last_used) if last_used > 0 else 0
259
+ }
260
+ return result
261
+ else:
262
+ # Return info for specific model
263
+ if model_id in self._models:
264
+ last_used = self._last_used.get(model_id, 0)
265
+ current_time = time.time()
266
+ return {
267
+ 'model_id': model_id,
268
+ 'loaded': True,
269
+ 'last_used': last_used,
270
+ 'inactive_seconds': int(current_time - last_used) if last_used > 0 else 0
271
+ }
272
+ else:
273
+ return {
274
+ 'model_id': model_id,
275
+ 'loaded': False
276
+ }
277
+
278
+ def unload_all_models(self) -> int:
279
+ """
280
+ Unload all currently loaded models.
281
+
282
+ Useful for cleanup or testing.
283
+
284
+ Returns:
285
+ int: Number of models unloaded
286
+ """
287
+ model_ids = list(self._models.keys())
288
+ count = 0
289
+
290
+ for model_id in model_ids:
291
+ if self.unload_model_by_id(model_id):
292
+ count += 1
293
+
294
+ return count
mozo/registry.py ADDED
@@ -0,0 +1,235 @@
1
+ """
2
+ Model Registry for Mozo
3
+
4
+ Centralized registry of all available model families and their variants.
5
+ This registry maps model families to their adapter classes and supported variants.
6
+
7
+ Usage:
8
+ # To add a new model family, add an entry to MODEL_REGISTRY
9
+ # To add a new variant to an existing family, add it to the 'variants' dict
10
+
11
+ Example:
12
+ 'detectron2': {
13
+ 'adapter_class': 'Detectron2Predictor',
14
+ 'task_type': 'object_detection',
15
+ 'variants': {
16
+ 'mask_rcnn_R_50_FPN_3x': {
17
+ 'variant': 'mask_rcnn_R_50_FPN_3x',
18
+ 'confidence_threshold': 0.5,
19
+ 'device': 'cpu'
20
+ }
21
+ }
22
+ }
23
+ """
24
+
25
+ # Main model registry - maps family names to adapter configurations
26
+ MODEL_REGISTRY = {
27
+ 'detectron2': {
28
+ 'adapter_class': 'Detectron2Predictor',
29
+ 'module': 'mozo.adapters.detectron2',
30
+ 'task_type': 'object_detection',
31
+ 'description': 'Detectron2 models for object detection, instance segmentation, and keypoint detection',
32
+ 'variants': {
33
+ # Mask R-CNN variants (Instance Segmentation)
34
+ 'mask_rcnn_R_50_FPN_3x': {
35
+ 'variant': 'mask_rcnn_R_50_FPN_3x',
36
+ 'confidence_threshold': 0.5,
37
+ 'device': 'cpu'
38
+ },
39
+ 'mask_rcnn_R_50_C4_1x': {
40
+ 'variant': 'mask_rcnn_R_50_C4_1x',
41
+ 'confidence_threshold': 0.5,
42
+ 'device': 'cpu'
43
+ },
44
+ 'mask_rcnn_R_50_C4_3x': {
45
+ 'variant': 'mask_rcnn_R_50_C4_3x',
46
+ 'confidence_threshold': 0.5,
47
+ 'device': 'cpu'
48
+ },
49
+ 'mask_rcnn_R_101_FPN_3x': {
50
+ 'variant': 'mask_rcnn_R_101_FPN_3x',
51
+ 'confidence_threshold': 0.5,
52
+ 'device': 'cpu'
53
+ },
54
+ 'mask_rcnn_X_101_32x8d_FPN_3x': {
55
+ 'variant': 'mask_rcnn_X_101_32x8d_FPN_3x',
56
+ 'confidence_threshold': 0.5,
57
+ 'device': 'cpu'
58
+ },
59
+
60
+ # Faster R-CNN variants (Object Detection)
61
+ 'faster_rcnn_R_50_FPN_3x': {
62
+ 'variant': 'faster_rcnn_R_50_FPN_3x',
63
+ 'confidence_threshold': 0.7,
64
+ 'device': 'cpu'
65
+ },
66
+ 'faster_rcnn_R_50_C4_1x': {
67
+ 'variant': 'faster_rcnn_R_50_C4_1x',
68
+ 'confidence_threshold': 0.7,
69
+ 'device': 'cpu'
70
+ },
71
+ 'faster_rcnn_R_101_FPN_3x': {
72
+ 'variant': 'faster_rcnn_R_101_FPN_3x',
73
+ 'confidence_threshold': 0.7,
74
+ 'device': 'cpu'
75
+ },
76
+ 'faster_rcnn_X_101_32x8d_FPN_3x': {
77
+ 'variant': 'faster_rcnn_X_101_32x8d_FPN_3x',
78
+ 'confidence_threshold': 0.7,
79
+ 'device': 'cpu'
80
+ },
81
+
82
+ # RetinaNet variants (Object Detection)
83
+ 'retinanet_R_50_FPN_1x': {
84
+ 'variant': 'retinanet_R_50_FPN_1x',
85
+ 'confidence_threshold': 0.5,
86
+ 'device': 'cpu'
87
+ },
88
+ 'retinanet_R_50_FPN_3x': {
89
+ 'variant': 'retinanet_R_50_FPN_3x',
90
+ 'confidence_threshold': 0.5,
91
+ 'device': 'cpu'
92
+ },
93
+ 'retinanet_R_101_FPN_3x': {
94
+ 'variant': 'retinanet_R_101_FPN_3x',
95
+ 'confidence_threshold': 0.5,
96
+ 'device': 'cpu'
97
+ },
98
+
99
+ # Keypoint R-CNN variants (Keypoint Detection)
100
+ 'keypoint_rcnn_R_50_FPN_3x': {
101
+ 'variant': 'keypoint_rcnn_R_50_FPN_3x',
102
+ 'confidence_threshold': 0.5,
103
+ 'device': 'cpu'
104
+ },
105
+ 'keypoint_rcnn_R_101_FPN_3x': {
106
+ 'variant': 'keypoint_rcnn_R_101_FPN_3x',
107
+ 'confidence_threshold': 0.5,
108
+ 'device': 'cpu'
109
+ },
110
+ 'keypoint_rcnn_X_101_32x8d_FPN_3x': {
111
+ 'variant': 'keypoint_rcnn_X_101_32x8d_FPN_3x',
112
+ 'confidence_threshold': 0.5,
113
+ 'device': 'cpu'
114
+ },
115
+
116
+ # RPN variants (Region Proposal Network)
117
+ 'rpn_R_50_FPN_1x': {
118
+ 'variant': 'rpn_R_50_FPN_1x',
119
+ 'confidence_threshold': 0.5,
120
+ 'device': 'cpu'
121
+ },
122
+
123
+ # Fast R-CNN variants
124
+ 'fast_rcnn_R_50_FPN_1x': {
125
+ 'variant': 'fast_rcnn_R_50_FPN_1x',
126
+ 'confidence_threshold': 0.5,
127
+ 'device': 'cpu'
128
+ },
129
+ }
130
+ },
131
+
132
+ 'depth_anything': {
133
+ 'adapter_class': 'DepthAnythingPredictor',
134
+ 'module': 'mozo.adapters.depth_anything',
135
+ 'task_type': 'depth_estimation',
136
+ 'description': 'Depth Anything V2 models for monocular depth estimation',
137
+ 'variants': {
138
+ 'small': {
139
+ 'variant': 'small'
140
+ },
141
+ 'base': {
142
+ 'variant': 'base'
143
+ },
144
+ 'large': {
145
+ 'variant': 'large'
146
+ },
147
+ }
148
+ },
149
+
150
+ 'qwen2.5_vl': {
151
+ 'adapter_class': 'Qwen2_5VLPredictor',
152
+ 'module': 'mozo.adapters.qwen2_5_vl',
153
+ 'task_type': 'visual_question_answering',
154
+ 'description': 'Qwen2.5-VL models for vision-language understanding, VQA, and image analysis',
155
+ 'variants': {
156
+ '7b-instruct': {
157
+ 'variant': '7b-instruct',
158
+ 'device': 'cpu', # MPS has compatibility issues with Qwen2.5-VL
159
+ 'torch_dtype': 'auto'
160
+ },
161
+ }
162
+ },
163
+ }
164
+
165
+
166
+ def get_available_families():
167
+ """
168
+ Get list of all available model families.
169
+
170
+ Returns:
171
+ list: List of model family names
172
+ """
173
+ return list(MODEL_REGISTRY.keys())
174
+
175
+
176
+ def get_available_variants(family):
177
+ """
178
+ Get list of all available variants for a model family.
179
+
180
+ Args:
181
+ family: Model family name
182
+
183
+ Returns:
184
+ list: List of variant names for the family
185
+
186
+ Raises:
187
+ ValueError: If family is not in registry
188
+ """
189
+ if family not in MODEL_REGISTRY:
190
+ raise ValueError(f"Unknown model family: '{family}'. Available families: {get_available_families()}")
191
+ return list(MODEL_REGISTRY[family]['variants'].keys())
192
+
193
+
194
+ def get_model_info(family, variant=None):
195
+ """
196
+ Get detailed information about a model family or specific variant.
197
+
198
+ Args:
199
+ family: Model family name
200
+ variant: Optional variant name. If None, returns family-level info
201
+
202
+ Returns:
203
+ dict: Model information
204
+
205
+ Raises:
206
+ ValueError: If family or variant is not found
207
+ """
208
+ if family not in MODEL_REGISTRY:
209
+ raise ValueError(f"Unknown model family: '{family}'. Available families: {get_available_families()}")
210
+
211
+ family_config = MODEL_REGISTRY[family]
212
+
213
+ if variant is None:
214
+ # Return family-level info
215
+ return {
216
+ 'family': family,
217
+ 'adapter_class': family_config['adapter_class'],
218
+ 'task_type': family_config['task_type'],
219
+ 'description': family_config.get('description', ''),
220
+ 'num_variants': len(family_config['variants']),
221
+ 'variants': list(family_config['variants'].keys())
222
+ }
223
+ else:
224
+ # Return variant-specific info
225
+ if variant not in family_config['variants']:
226
+ raise ValueError(
227
+ f"Unknown variant '{variant}' for family '{family}'. "
228
+ f"Available variants: {list(family_config['variants'].keys())}"
229
+ )
230
+ return {
231
+ 'family': family,
232
+ 'variant': variant,
233
+ 'task_type': family_config['task_type'],
234
+ 'parameters': family_config['variants'][variant]
235
+ }