matrice-inference 0.1.2__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 matrice-inference might be problematic. Click here for more details.
- matrice_inference/__init__.py +72 -0
- matrice_inference/py.typed +0 -0
- matrice_inference/server/__init__.py +23 -0
- matrice_inference/server/inference_interface.py +176 -0
- matrice_inference/server/model/__init__.py +1 -0
- matrice_inference/server/model/model_manager.py +274 -0
- matrice_inference/server/model/model_manager_wrapper.py +550 -0
- matrice_inference/server/model/triton_model_manager.py +290 -0
- matrice_inference/server/model/triton_server.py +1248 -0
- matrice_inference/server/proxy_interface.py +371 -0
- matrice_inference/server/server.py +1004 -0
- matrice_inference/server/stream/__init__.py +0 -0
- matrice_inference/server/stream/app_deployment.py +228 -0
- matrice_inference/server/stream/consumer_worker.py +201 -0
- matrice_inference/server/stream/frame_cache.py +127 -0
- matrice_inference/server/stream/inference_worker.py +163 -0
- matrice_inference/server/stream/post_processing_worker.py +230 -0
- matrice_inference/server/stream/producer_worker.py +147 -0
- matrice_inference/server/stream/stream_pipeline.py +451 -0
- matrice_inference/server/stream/utils.py +23 -0
- matrice_inference/tmp/abstract_model_manager.py +58 -0
- matrice_inference/tmp/aggregator/__init__.py +18 -0
- matrice_inference/tmp/aggregator/aggregator.py +330 -0
- matrice_inference/tmp/aggregator/analytics.py +906 -0
- matrice_inference/tmp/aggregator/ingestor.py +438 -0
- matrice_inference/tmp/aggregator/latency.py +597 -0
- matrice_inference/tmp/aggregator/pipeline.py +968 -0
- matrice_inference/tmp/aggregator/publisher.py +431 -0
- matrice_inference/tmp/aggregator/synchronizer.py +594 -0
- matrice_inference/tmp/batch_manager.py +239 -0
- matrice_inference/tmp/overall_inference_testing.py +338 -0
- matrice_inference/tmp/triton_utils.py +638 -0
- matrice_inference-0.1.2.dist-info/METADATA +28 -0
- matrice_inference-0.1.2.dist-info/RECORD +37 -0
- matrice_inference-0.1.2.dist-info/WHEEL +5 -0
- matrice_inference-0.1.2.dist-info/licenses/LICENSE.txt +21 -0
- matrice_inference-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import copy
|
|
5
|
+
from typing import Dict, Optional, Any, Set, Tuple
|
|
6
|
+
from queue import Queue, Empty
|
|
7
|
+
from collections import defaultdict, deque
|
|
8
|
+
import copy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ResultsAggregator:
|
|
12
|
+
"""
|
|
13
|
+
Optimized aggregation and combination of synchronized results from multiple deployments.
|
|
14
|
+
This component takes synchronized results and combines them into meaningful aggregated outputs
|
|
15
|
+
while maintaining consistent structure with individual deployment results.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
synchronized_results_queue: Queue,
|
|
21
|
+
aggregate_by_location: bool = False,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize the results aggregator.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
synchronized_results_queue: Queue containing synchronized results from synchronizer
|
|
28
|
+
aggregate_by_location: Whether to aggregate by location
|
|
29
|
+
"""
|
|
30
|
+
self.synchronized_results_queue = synchronized_results_queue
|
|
31
|
+
self.aggregated_results_queue = Queue()
|
|
32
|
+
self.aggregate_by_location = aggregate_by_location
|
|
33
|
+
|
|
34
|
+
# Threading and state management
|
|
35
|
+
self._stop_aggregation = threading.Event()
|
|
36
|
+
self._aggregation_thread: Optional[threading.Thread] = None
|
|
37
|
+
self._is_running = False
|
|
38
|
+
self._stats_lock = threading.Lock()
|
|
39
|
+
|
|
40
|
+
# Use more efficient data structure for tracking sent keys
|
|
41
|
+
# Use a deque with fixed maxlen for automatic cleanup
|
|
42
|
+
self._sent_keys: deque = deque(maxlen=50000) # More memory efficient than manual cleanup
|
|
43
|
+
self._sent_keys_set: Set[Tuple[str, str, int]] = set() # For O(1) lookups
|
|
44
|
+
|
|
45
|
+
# Statistics
|
|
46
|
+
self.stats = {
|
|
47
|
+
"start_time": None,
|
|
48
|
+
"results_processed": 0,
|
|
49
|
+
"aggregations_created": 0,
|
|
50
|
+
"errors": 0,
|
|
51
|
+
"last_error": None,
|
|
52
|
+
"last_error_time": None,
|
|
53
|
+
"duplicates_skipped": 0,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def start_aggregation(self) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Start the results aggregation process.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
bool: True if aggregation started successfully, False otherwise
|
|
62
|
+
"""
|
|
63
|
+
if self._is_running:
|
|
64
|
+
logging.warning("Results aggregation is already running")
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
self._is_running = True
|
|
69
|
+
self.stats["start_time"] = time.time()
|
|
70
|
+
self._stop_aggregation.clear()
|
|
71
|
+
|
|
72
|
+
# Start aggregation thread
|
|
73
|
+
self._aggregation_thread = threading.Thread(
|
|
74
|
+
target=self._aggregation_worker,
|
|
75
|
+
name="ResultsAggregator",
|
|
76
|
+
daemon=True,
|
|
77
|
+
)
|
|
78
|
+
self._aggregation_thread.start()
|
|
79
|
+
|
|
80
|
+
logging.info("Results aggregation started successfully")
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
self._record_error(f"Failed to start results aggregation: {str(exc)}")
|
|
85
|
+
self.stop_aggregation()
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
def stop_aggregation(self):
|
|
89
|
+
"""Stop the results aggregation process."""
|
|
90
|
+
if not self._is_running:
|
|
91
|
+
logging.info("Results aggregation is not running")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
self._is_running = False
|
|
95
|
+
self._stop_aggregation.set()
|
|
96
|
+
|
|
97
|
+
logging.info("Stopping results aggregation...")
|
|
98
|
+
|
|
99
|
+
# Wait for aggregation thread to complete
|
|
100
|
+
if self._aggregation_thread and self._aggregation_thread.is_alive():
|
|
101
|
+
try:
|
|
102
|
+
self._aggregation_thread.join(timeout=5.0)
|
|
103
|
+
if self._aggregation_thread.is_alive():
|
|
104
|
+
logging.warning("Results aggregation thread did not stop gracefully")
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
logging.error(f"Error joining aggregation thread: {exc}")
|
|
107
|
+
|
|
108
|
+
self._aggregation_thread = None
|
|
109
|
+
logging.info("Results aggregation stopped")
|
|
110
|
+
|
|
111
|
+
def _aggregation_worker(self):
|
|
112
|
+
"""Optimized main aggregation worker thread for immediate processing."""
|
|
113
|
+
logging.info("Results aggregation worker started")
|
|
114
|
+
last_log_time = time.time()
|
|
115
|
+
log_interval = 30.0 # Log every 30 seconds
|
|
116
|
+
|
|
117
|
+
while not self._stop_aggregation.is_set():
|
|
118
|
+
try:
|
|
119
|
+
# Get synchronized result from queue
|
|
120
|
+
try:
|
|
121
|
+
synced_result = self.synchronized_results_queue.get(timeout=1.0)
|
|
122
|
+
except Empty:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Process the single synchronized result immediately
|
|
126
|
+
aggregated_result = self._aggregate_single_result(synced_result)
|
|
127
|
+
|
|
128
|
+
if aggregated_result:
|
|
129
|
+
# Add to output queue immediately
|
|
130
|
+
self.aggregated_results_queue.put(aggregated_result)
|
|
131
|
+
|
|
132
|
+
# Update statistics
|
|
133
|
+
with self._stats_lock:
|
|
134
|
+
self.stats["results_processed"] += 1
|
|
135
|
+
self.stats["aggregations_created"] += 1
|
|
136
|
+
else:
|
|
137
|
+
# Track duplicates
|
|
138
|
+
with self._stats_lock:
|
|
139
|
+
self.stats["duplicates_skipped"] += 1
|
|
140
|
+
|
|
141
|
+
# Mark task as done
|
|
142
|
+
self.synchronized_results_queue.task_done()
|
|
143
|
+
|
|
144
|
+
# Reduced frequency logging
|
|
145
|
+
current_time = time.time()
|
|
146
|
+
if (current_time - last_log_time) > log_interval:
|
|
147
|
+
with self._stats_lock:
|
|
148
|
+
processed = self.stats["results_processed"]
|
|
149
|
+
duplicates = self.stats["duplicates_skipped"]
|
|
150
|
+
queue_size = self.aggregated_results_queue.qsize()
|
|
151
|
+
if processed > 0 or duplicates > 0:
|
|
152
|
+
logging.debug(f"Aggregator: processed={processed}, duplicates={duplicates}, queue_size={queue_size}")
|
|
153
|
+
last_log_time = current_time
|
|
154
|
+
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
if not self._stop_aggregation.is_set():
|
|
157
|
+
self._record_error(f"Error in aggregation worker: {str(exc)}")
|
|
158
|
+
time.sleep(0.1) # Prevent tight error loops
|
|
159
|
+
|
|
160
|
+
logging.info("Results aggregation worker stopped")
|
|
161
|
+
|
|
162
|
+
def _aggregate_single_result(self, sync_result: Dict) -> Optional[Dict]:
|
|
163
|
+
"""Optimized aggregation of a single synchronized result."""
|
|
164
|
+
try:
|
|
165
|
+
# Extract deployment results
|
|
166
|
+
deployment_results = sync_result.get("deployment_results", {})
|
|
167
|
+
if not deployment_results:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
# Get stream info from synchronized result
|
|
171
|
+
stream_key = sync_result.get("stream_key")
|
|
172
|
+
input_order = sync_result.get("input_order")
|
|
173
|
+
stream_group_key = sync_result.get("stream_group_key")
|
|
174
|
+
|
|
175
|
+
if not stream_key or input_order is None:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# Efficient duplicate checking using O(1) set lookup
|
|
179
|
+
key = (stream_group_key, stream_key, input_order)
|
|
180
|
+
if key in self._sent_keys_set:
|
|
181
|
+
return None # Duplicate, skip silently for performance
|
|
182
|
+
|
|
183
|
+
# Add to both deque (for automatic cleanup) and set (for fast lookup)
|
|
184
|
+
self._sent_keys.append(key)
|
|
185
|
+
self._sent_keys_set.add(key)
|
|
186
|
+
|
|
187
|
+
# Clean up set when deque automatically removes old items
|
|
188
|
+
if len(self._sent_keys_set) > self._sent_keys.maxlen:
|
|
189
|
+
# Only keep recent items in set - rebuild from deque
|
|
190
|
+
self._sent_keys_set = set(self._sent_keys)
|
|
191
|
+
|
|
192
|
+
# Extract input_stream and camera_info efficiently (avoid deep copy)
|
|
193
|
+
first_deployment_result = next(iter(deployment_results.values()))
|
|
194
|
+
first_app_result = first_deployment_result.get("result", {})
|
|
195
|
+
input_streams = first_app_result.get("input_streams", [])
|
|
196
|
+
|
|
197
|
+
# Get input stream data efficiently
|
|
198
|
+
if input_streams:
|
|
199
|
+
input_data = input_streams[0]
|
|
200
|
+
input_stream = copy.deepcopy(input_data.get("input_stream", input_data))
|
|
201
|
+
else:
|
|
202
|
+
input_stream = {}
|
|
203
|
+
|
|
204
|
+
# Get camera_info without deep copy
|
|
205
|
+
camera_info = first_app_result.get("camera_info", {})
|
|
206
|
+
|
|
207
|
+
# Collect all app results efficiently
|
|
208
|
+
agg_apps = []
|
|
209
|
+
current_timestamp = time.time()
|
|
210
|
+
|
|
211
|
+
for deployment_id, deployment_result in deployment_results.items():
|
|
212
|
+
app_result = deployment_result.get("result", {})
|
|
213
|
+
if not app_result:
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Create optimized app result
|
|
217
|
+
optimized_app_result = {
|
|
218
|
+
**app_result, # Shallow copy for performance
|
|
219
|
+
"deployment_id": deployment_id,
|
|
220
|
+
"deployment_timestamp": deployment_result.get("timestamp", current_timestamp)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Remove large content fields to save memory
|
|
224
|
+
if "input_streams" in optimized_app_result:
|
|
225
|
+
input_streams_clean = []
|
|
226
|
+
for item in optimized_app_result["input_streams"]:
|
|
227
|
+
clean_item = {k: v for k, v in item.items() if k != "content"}
|
|
228
|
+
if "input_stream" in clean_item and isinstance(clean_item["input_stream"], dict):
|
|
229
|
+
clean_item["input_stream"] = {k: v for k, v in clean_item["input_stream"].items() if k != "content"}
|
|
230
|
+
input_streams_clean.append(clean_item)
|
|
231
|
+
optimized_app_result["input_streams"] = input_streams_clean
|
|
232
|
+
|
|
233
|
+
agg_apps.append(optimized_app_result)
|
|
234
|
+
|
|
235
|
+
# Create optimized camera_results structure
|
|
236
|
+
camera_results = {
|
|
237
|
+
"input_stream": input_stream,
|
|
238
|
+
"camera_info": camera_info,
|
|
239
|
+
"agg_apps": agg_apps,
|
|
240
|
+
"aggregation_metadata": {
|
|
241
|
+
"stream_key": stream_key,
|
|
242
|
+
"input_order": input_order,
|
|
243
|
+
"stream_group_key": stream_group_key,
|
|
244
|
+
"deployment_count": len(deployment_results),
|
|
245
|
+
"aggregation_timestamp": current_timestamp,
|
|
246
|
+
"aggregation_type": "camera_results",
|
|
247
|
+
"synchronization_metadata": sync_result.get("synchronization_metadata", {})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return camera_results
|
|
252
|
+
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
self._record_error(f"Error aggregating single result: {str(exc)}")
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
def _record_error(self, error_message: str):
|
|
258
|
+
"""Record an error in statistics."""
|
|
259
|
+
with self._stats_lock:
|
|
260
|
+
self.stats["errors"] += 1
|
|
261
|
+
self.stats["last_error"] = error_message
|
|
262
|
+
self.stats["last_error_time"] = time.time()
|
|
263
|
+
# Reduce logging frequency for performance
|
|
264
|
+
if self.stats["errors"] % 10 == 1: # Log every 10th error
|
|
265
|
+
logging.error(f"Aggregator error (#{self.stats['errors']}): {error_message}")
|
|
266
|
+
|
|
267
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
268
|
+
"""Get current aggregation statistics."""
|
|
269
|
+
with self._stats_lock:
|
|
270
|
+
stats = self.stats.copy()
|
|
271
|
+
|
|
272
|
+
# Add runtime statistics
|
|
273
|
+
if stats["start_time"]:
|
|
274
|
+
stats["runtime_seconds"] = time.time() - stats["start_time"]
|
|
275
|
+
|
|
276
|
+
stats["output_queue_size"] = self.aggregated_results_queue.qsize()
|
|
277
|
+
stats["sent_keys_count"] = len(self._sent_keys)
|
|
278
|
+
stats["sent_keys_set_count"] = len(self._sent_keys_set)
|
|
279
|
+
|
|
280
|
+
return stats
|
|
281
|
+
|
|
282
|
+
def get_health_status(self) -> Dict[str, Any]:
|
|
283
|
+
"""Get health status of the aggregator."""
|
|
284
|
+
health = {
|
|
285
|
+
"status": "healthy",
|
|
286
|
+
"is_running": self._is_running,
|
|
287
|
+
"output_queue_size": self.aggregated_results_queue.qsize(),
|
|
288
|
+
"errors": self.stats["errors"],
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Check for recent errors (within last 60 seconds)
|
|
292
|
+
if (
|
|
293
|
+
self.stats["last_error_time"]
|
|
294
|
+
and (time.time() - self.stats["last_error_time"]) < 60
|
|
295
|
+
):
|
|
296
|
+
health["status"] = "degraded"
|
|
297
|
+
health["reason"] = f"Recent error: {self.stats['last_error']}"
|
|
298
|
+
logging.warning(f"Aggregator degraded due to recent error: {self.stats['last_error']}")
|
|
299
|
+
|
|
300
|
+
# Check if output queue is getting full
|
|
301
|
+
queue_size = self.aggregated_results_queue.qsize()
|
|
302
|
+
if queue_size > 1000:
|
|
303
|
+
health["status"] = "degraded"
|
|
304
|
+
health["reason"] = f"Output queue too large ({queue_size} items)"
|
|
305
|
+
logging.warning(f"Aggregator degraded: output queue has {queue_size} items (threshold: 100)")
|
|
306
|
+
|
|
307
|
+
# Check if not running when it should be
|
|
308
|
+
if not self._is_running:
|
|
309
|
+
health["status"] = "unhealthy"
|
|
310
|
+
health["reason"] = "Aggregator is not running"
|
|
311
|
+
logging.error("Aggregator is not running")
|
|
312
|
+
|
|
313
|
+
return health
|
|
314
|
+
|
|
315
|
+
def cleanup(self):
|
|
316
|
+
"""Clean up resources."""
|
|
317
|
+
self.stop_aggregation()
|
|
318
|
+
|
|
319
|
+
# Clear queues
|
|
320
|
+
try:
|
|
321
|
+
while not self.aggregated_results_queue.empty():
|
|
322
|
+
self.aggregated_results_queue.get_nowait()
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
# Clear tracking data
|
|
327
|
+
self._sent_keys.clear()
|
|
328
|
+
self._sent_keys_set.clear()
|
|
329
|
+
|
|
330
|
+
logging.info("Results aggregator cleanup completed")
|