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.

Files changed (37) hide show
  1. matrice_inference/__init__.py +72 -0
  2. matrice_inference/py.typed +0 -0
  3. matrice_inference/server/__init__.py +23 -0
  4. matrice_inference/server/inference_interface.py +176 -0
  5. matrice_inference/server/model/__init__.py +1 -0
  6. matrice_inference/server/model/model_manager.py +274 -0
  7. matrice_inference/server/model/model_manager_wrapper.py +550 -0
  8. matrice_inference/server/model/triton_model_manager.py +290 -0
  9. matrice_inference/server/model/triton_server.py +1248 -0
  10. matrice_inference/server/proxy_interface.py +371 -0
  11. matrice_inference/server/server.py +1004 -0
  12. matrice_inference/server/stream/__init__.py +0 -0
  13. matrice_inference/server/stream/app_deployment.py +228 -0
  14. matrice_inference/server/stream/consumer_worker.py +201 -0
  15. matrice_inference/server/stream/frame_cache.py +127 -0
  16. matrice_inference/server/stream/inference_worker.py +163 -0
  17. matrice_inference/server/stream/post_processing_worker.py +230 -0
  18. matrice_inference/server/stream/producer_worker.py +147 -0
  19. matrice_inference/server/stream/stream_pipeline.py +451 -0
  20. matrice_inference/server/stream/utils.py +23 -0
  21. matrice_inference/tmp/abstract_model_manager.py +58 -0
  22. matrice_inference/tmp/aggregator/__init__.py +18 -0
  23. matrice_inference/tmp/aggregator/aggregator.py +330 -0
  24. matrice_inference/tmp/aggregator/analytics.py +906 -0
  25. matrice_inference/tmp/aggregator/ingestor.py +438 -0
  26. matrice_inference/tmp/aggregator/latency.py +597 -0
  27. matrice_inference/tmp/aggregator/pipeline.py +968 -0
  28. matrice_inference/tmp/aggregator/publisher.py +431 -0
  29. matrice_inference/tmp/aggregator/synchronizer.py +594 -0
  30. matrice_inference/tmp/batch_manager.py +239 -0
  31. matrice_inference/tmp/overall_inference_testing.py +338 -0
  32. matrice_inference/tmp/triton_utils.py +638 -0
  33. matrice_inference-0.1.2.dist-info/METADATA +28 -0
  34. matrice_inference-0.1.2.dist-info/RECORD +37 -0
  35. matrice_inference-0.1.2.dist-info/WHEEL +5 -0
  36. matrice_inference-0.1.2.dist-info/licenses/LICENSE.txt +21 -0
  37. matrice_inference-0.1.2.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,228 @@
1
+
2
+ from typing import Dict, List, Optional
3
+ import time
4
+ import logging
5
+ from matrice_common.session import Session
6
+ from matrice_inference.server.stream.utils import CameraConfig
7
+
8
+
9
+ class AppDeployment:
10
+ """Handles app deployment configuration and camera setup for streaming pipeline."""
11
+
12
+ def __init__(self, session: Session, app_deployment_id: str, connection_timeout: int = 1200): # Increased from 300 to 1200
13
+ self.app_deployment_id = app_deployment_id
14
+ self.rpc = session.rpc
15
+ self.session = session
16
+ self.connection_timeout = connection_timeout
17
+ self.logger = logging.getLogger(__name__)
18
+
19
+ def get_input_topics(self) -> List[Dict]:
20
+ """Get input topics for the app deployment."""
21
+ try:
22
+ response = self.rpc.get(f"/v1/inference/get_input_topics_by_app_deployment_id/{self.app_deployment_id}")
23
+ if response.get("success", False):
24
+ return response.get("data", [])
25
+ else:
26
+ self.logger.error(f"Failed to get input topics: {response.get('message', 'Unknown error')}")
27
+ return []
28
+ except Exception as e:
29
+ self.logger.error(f"Exception getting input topics: {str(e)}")
30
+ return []
31
+
32
+ def get_output_topics(self) -> List[Dict]:
33
+ """Get output topics for the app deployment."""
34
+ try:
35
+ response = self.rpc.get(f"/v1/inference/get_output_topics_by_app_deployment_id/{self.app_deployment_id}")
36
+ if response.get("success", False):
37
+ return response.get("data", [])
38
+ else:
39
+ self.logger.error(f"Failed to get output topics: {response.get('message', 'Unknown error')}")
40
+ return []
41
+ except Exception as e:
42
+ self.logger.error(f"Exception getting output topics: {str(e)}")
43
+ return []
44
+
45
+ def get_camera_configs(self) -> Dict[str, CameraConfig]:
46
+ """
47
+ Get camera configurations for the streaming pipeline.
48
+
49
+ Returns:
50
+ Dict[str, CameraConfig]: Dictionary mapping camera_id to CameraConfig
51
+ """
52
+ camera_configs = {}
53
+
54
+ try:
55
+ # Get input and output topics
56
+ input_topics = self.get_input_topics()
57
+ output_topics = self.get_output_topics()
58
+
59
+ if not input_topics:
60
+ self.logger.warning("No input topics found for app deployment")
61
+ return camera_configs
62
+
63
+ # Create mapping of camera_id to output topic
64
+ output_topic_map = {}
65
+ for output_topic in output_topics:
66
+ camera_id = output_topic.get("cameraId")
67
+ if camera_id:
68
+ output_topic_map[camera_id] = output_topic
69
+
70
+ # Process each input topic to create camera config
71
+ for input_topic in input_topics:
72
+ try:
73
+ camera_id = input_topic.get("cameraId")
74
+ if not camera_id:
75
+ self.logger.warning("Input topic missing camera ID, skipping")
76
+ continue
77
+
78
+ # Get corresponding output topic
79
+ output_topic = output_topic_map.get(camera_id)
80
+ if not output_topic:
81
+ self.logger.warning(f"No output topic found for camera {camera_id}, skipping")
82
+ continue
83
+
84
+ # Get connection info for this server
85
+ server_id = input_topic.get("serverId")
86
+ server_type = input_topic.get("serverType", "redis").lower()
87
+
88
+ if not server_id:
89
+ self.logger.warning(f"No server ID found for camera {camera_id}, skipping")
90
+ continue
91
+
92
+ connection_info = self.get_and_wait_for_connection_info(server_type, server_id)
93
+ if not connection_info:
94
+ self.logger.error(f"Could not get connection info for camera {camera_id}, skipping")
95
+ continue
96
+
97
+ # Create stream config
98
+ stream_config = connection_info.copy()
99
+ stream_config["stream_type"] = server_type
100
+
101
+ # Create camera config
102
+ camera_config = CameraConfig(
103
+ camera_id=camera_id,
104
+ input_topic=input_topic.get("topicName"),
105
+ output_topic=output_topic.get("topicName"),
106
+ stream_config=stream_config,
107
+ enabled=True
108
+ )
109
+
110
+ camera_configs[camera_id] = camera_config
111
+ self.logger.info(f"Created camera config for {camera_id} using {server_type}")
112
+
113
+ except Exception as e:
114
+ self.logger.error(f"Error creating config for camera {camera_id}: {str(e)}")
115
+ continue
116
+
117
+ self.logger.info(f"Successfully created {len(camera_configs)} camera configurations")
118
+ return camera_configs
119
+
120
+ except Exception as e:
121
+ self.logger.error(f"Error getting camera configs: {str(e)}")
122
+ return camera_configs
123
+
124
+ def get_and_wait_for_connection_info(self, server_type: str, server_id: str) -> Optional[Dict]:
125
+ """Get the connection information for the streaming gateway."""
126
+ def _get_kafka_connection_info():
127
+ try:
128
+ response = self.rpc.get(f"/v1/actions/get_kafka_server/{server_id}")
129
+ if response.get("success", False):
130
+ data = response.get("data")
131
+ if (
132
+ data
133
+ and data.get("ipAddress")
134
+ and data.get("port")
135
+ and data.get("status") == "running"
136
+ ):
137
+ return {
138
+ 'bootstrap_servers': f'{data["ipAddress"]}:{data["port"]}',
139
+ 'sasl_mechanism': 'SCRAM-SHA-256',
140
+ 'sasl_username': 'matrice-sdk-user',
141
+ 'sasl_password': 'matrice-sdk-password',
142
+ 'security_protocol': 'SASL_PLAINTEXT'
143
+ }
144
+ else:
145
+ self.logger.debug("Kafka connection information is not complete, waiting...")
146
+ return None
147
+ else:
148
+ self.logger.debug("Failed to get Kafka connection information: %s", response.get("message", "Unknown error"))
149
+ return None
150
+ except Exception as exc:
151
+ self.logger.debug("Exception getting Kafka connection info: %s", str(exc))
152
+ return None
153
+
154
+ def _get_redis_connection_info():
155
+ try:
156
+ response = self.rpc.get(f"/v1/actions/redis_servers/{server_id}")
157
+ if response.get("success", False):
158
+ data = response.get("data")
159
+ if (
160
+ data
161
+ and data.get("host")
162
+ and data.get("port")
163
+ and data.get("status") == "running"
164
+ ):
165
+ return {
166
+ 'host': data["host"],
167
+ 'port': int(data["port"]),
168
+ 'password': data.get("password", ""),
169
+ 'username': data.get("username"),
170
+ 'db': data.get("db", 0),
171
+ 'connection_timeout': 120 # Increased from 30 to 120
172
+ }
173
+ else:
174
+ self.logger.debug("Redis connection information is not complete, waiting...")
175
+ return None
176
+ else:
177
+ self.logger.debug("Failed to get Redis connection information: %s", response.get("message", "Unknown error"))
178
+ return None
179
+ except Exception as exc:
180
+ self.logger.debug("Exception getting Redis connection info: %s", str(exc))
181
+ return None
182
+
183
+ start_time = time.time()
184
+ last_log_time = 0
185
+
186
+ while True:
187
+ current_time = time.time()
188
+
189
+ # Get connection info based on server type
190
+ connection_info = None
191
+ if server_type == "kafka":
192
+ connection_info = _get_kafka_connection_info()
193
+ elif server_type == "redis":
194
+ connection_info = _get_redis_connection_info()
195
+ else:
196
+ raise ValueError(f"Unsupported server type: {server_type}")
197
+
198
+ # If we got valid connection info, return it
199
+ if connection_info:
200
+ self.logger.info("Successfully retrieved %s connection information", server_type)
201
+ return connection_info
202
+
203
+ # Check timeout
204
+ if current_time - start_time > self.connection_timeout:
205
+ error_msg = f"Timeout waiting for {server_type} connection information after {self.connection_timeout} seconds"
206
+ self.logger.error(error_msg)
207
+
208
+ # Log the last response for debugging
209
+ try:
210
+ if server_type == "kafka":
211
+ response = self.rpc.get(f"/v1/actions/get_kafka_server/{server_id}")
212
+ else:
213
+ response = self.rpc.get(f"/v1/actions/redis_servers/{server_id}")
214
+ self.logger.error("Last response received: %s", response)
215
+ except Exception as exc:
216
+ self.logger.error("Failed to get last response for debugging: %s", str(exc))
217
+
218
+ return None # Return None instead of raising exception to allow graceful handling
219
+
220
+ # Log waiting message every 10 seconds to avoid spam
221
+ if current_time - last_log_time >= 10:
222
+ elapsed = current_time - start_time
223
+ remaining = self.connection_timeout - elapsed
224
+ self.logger.info("Waiting for %s connection information... (%.1fs elapsed, %.1fs remaining)",
225
+ server_type, elapsed, remaining)
226
+ last_log_time = current_time
227
+
228
+ time.sleep(1)
@@ -0,0 +1,201 @@
1
+ # Import moved to method where it's needed to avoid circular imports
2
+ from matrice_inference.server.stream.utils import CameraConfig, StreamMessage
3
+ import asyncio
4
+ import json
5
+ import time
6
+ import logging
7
+ import threading
8
+ import queue
9
+ from datetime import datetime, timezone
10
+ import logging
11
+
12
+ class ConsumerWorker:
13
+ """Handles message consumption from streams."""
14
+
15
+ def __init__(self, camera_id: str, worker_id: int, stream_config: dict, input_topic: str,
16
+ inference_queue: queue.PriorityQueue, message_timeout: float,
17
+ camera_config: CameraConfig):
18
+ self.camera_id = camera_id
19
+ self.worker_id = worker_id
20
+ self.stream_config = stream_config
21
+ self.input_topic = input_topic
22
+ self.inference_queue = inference_queue
23
+ self.message_timeout = message_timeout
24
+ self.camera_config = camera_config
25
+ self.running = False
26
+ self.stream = None # Will be created in worker thread's event loop
27
+ self.logger = logging.getLogger(f"{__name__}.consumer.{camera_id}.{worker_id}")
28
+
29
+ def start(self):
30
+ """Start the consumer worker in a separate thread."""
31
+ self.running = True
32
+ thread = threading.Thread(target=self._run, name=f"Consumer-{self.camera_id}-{self.worker_id}", daemon=False)
33
+ thread.start()
34
+ return thread
35
+
36
+ def stop(self):
37
+ """Stop the consumer worker."""
38
+ self.running = False
39
+
40
+ def _run(self):
41
+ """Main consumer loop."""
42
+ # Create a new event loop for this worker thread
43
+ loop = asyncio.new_event_loop()
44
+ asyncio.set_event_loop(loop)
45
+
46
+ self.logger.info(f"Started consumer worker for camera {self.camera_id}")
47
+
48
+ try:
49
+ # Initialize stream in this event loop
50
+ loop.run_until_complete(self._initialize_stream())
51
+
52
+ while self.running and self.camera_config.enabled:
53
+ try:
54
+ # Get message from stream
55
+ message_data = loop.run_until_complete(
56
+ self._get_message_safely()
57
+ )
58
+
59
+ if not message_data:
60
+ continue
61
+
62
+ # Parse and create task
63
+ self._process_message(message_data)
64
+
65
+ except Exception as e:
66
+ self.logger.error(f"Consumer error: {e}")
67
+ time.sleep(1.0)
68
+
69
+ finally:
70
+ # Clean up stream
71
+ if self.stream:
72
+ try:
73
+ loop.run_until_complete(self.stream.async_close())
74
+ except Exception as e:
75
+ self.logger.error(f"Error closing stream: {e}")
76
+ loop.close()
77
+ self.logger.info(f"Consumer worker stopped for camera {self.camera_id}")
78
+
79
+ async def _initialize_stream(self):
80
+ """Initialize MatriceStream in the current event loop."""
81
+ try:
82
+ from matrice_common.stream.matrice_stream import MatriceStream, StreamType
83
+
84
+ # Determine stream type
85
+ stream_type = StreamType.KAFKA if self.stream_config.get("stream_type", "kafka").lower() == "kafka" else StreamType.REDIS
86
+
87
+ # Create stream configuration
88
+ if stream_type == StreamType.KAFKA:
89
+ stream_params = {
90
+ "bootstrap_servers": self.stream_config.get("bootstrap_servers", "localhost:9092"),
91
+ "sasl_username": self.stream_config.get("sasl_username", "matrice-sdk-user"),
92
+ "sasl_password": self.stream_config.get("sasl_password", "matrice-sdk-password"),
93
+ "sasl_mechanism": self.stream_config.get("sasl_mechanism", "SCRAM-SHA-256"),
94
+ "security_protocol": self.stream_config.get("security_protocol", "SASL_PLAINTEXT"),
95
+ }
96
+ else: # Redis
97
+ stream_params = {
98
+ "host": self.stream_config.get("host", "localhost"),
99
+ "port": self.stream_config.get("port", 6379),
100
+ "password": self.stream_config.get("password"),
101
+ "username": self.stream_config.get("username"),
102
+ "db": self.stream_config.get("db", 0),
103
+ "connection_timeout": self.stream_config.get("connection_timeout", 120),
104
+ }
105
+
106
+ # Create and setup stream
107
+ self.stream = MatriceStream(stream_type, **stream_params)
108
+ await self.stream.async_setup(self.input_topic, f"inference_consumer_{self.camera_id}_{self.worker_id}")
109
+ # TODO: Add app name to the consumer group id to make sure it processing once only
110
+
111
+ self.logger.info(f"Initialized {stream_type.value} stream for consumer worker {self.worker_id}")
112
+
113
+ except Exception as e:
114
+ self.logger.error(f"Failed to initialize stream for consumer worker: {e}")
115
+ raise
116
+
117
+ async def _get_message_safely(self):
118
+ """Safely get message from stream in the current event loop."""
119
+ try:
120
+ if not self.stream:
121
+ self.logger.error("Stream not initialized")
122
+ return None
123
+ return await self.stream.async_get_message(self.message_timeout)
124
+ except Exception as e:
125
+ # Handle stream issues gracefully
126
+ self.logger.debug(f"Error getting message from stream: {e}")
127
+ return None
128
+
129
+ def _process_message(self, message_data):
130
+ """Process incoming message and add to inference queue."""
131
+ try:
132
+ # Parse message data - handle camera streamer format
133
+ if isinstance(message_data.get("data"), bytes):
134
+ data = json.loads(message_data["data"].decode("utf-8"))
135
+ else:
136
+ data = message_data.get("data", {})
137
+
138
+ # Handle camera streamer input format
139
+ input_stream = data.get("input_stream", {})
140
+ if not input_stream:
141
+ # Fallback to direct format
142
+ input_stream = data
143
+
144
+ # Create stream message
145
+ stream_msg = StreamMessage(
146
+ camera_id=self.camera_id,
147
+ message_key=message_data.get("key", data.get("input_name", f"{self.camera_id}_{int(time.time())}")),
148
+ data=data,
149
+ timestamp=datetime.now(timezone.utc),
150
+ priority=1
151
+ )
152
+
153
+ # Ensure extra_params is a dictionary
154
+ extra_params = data.get("extra_params", {})
155
+ if not isinstance(extra_params, dict):
156
+ self.logger.warning(f"extra_params is not a dict, converting from {type(extra_params)}: {extra_params}")
157
+ if isinstance(extra_params, list):
158
+ # Convert list to dict if possible
159
+ if len(extra_params) == 0:
160
+ extra_params = {}
161
+ elif all(isinstance(item, dict) for item in extra_params):
162
+ # Merge all dictionaries in the list
163
+ merged_params = {}
164
+ for item in extra_params:
165
+ merged_params.update(item)
166
+ extra_params = merged_params
167
+ else:
168
+ extra_params = {}
169
+ else:
170
+ extra_params = {}
171
+
172
+ # Determine frame_id (prefer value from upstream gateway; otherwise fallback to message key)
173
+ frame_id = data.get("frame_id")
174
+ if not frame_id:
175
+ frame_id = message_data.get("key", data.get("input_name", f"{self.camera_id}_{int(time.time() * 1000)}"))
176
+
177
+ # Attach frame_id into input_stream for propagation if not present
178
+ try:
179
+ if isinstance(input_stream, dict) and "frame_id" not in input_stream:
180
+ input_stream["frame_id"] = frame_id
181
+ except Exception:
182
+ pass
183
+
184
+ # Create inference task with camera streamer format
185
+ task_data = {
186
+ "message": stream_msg,
187
+ "input_stream": input_stream, # Pass the full input_stream
188
+ "stream_key": f"{self.camera_id}_{stream_msg.message_key}",
189
+ "extra_params": extra_params,
190
+ "camera_config": self.camera_config.__dict__,
191
+ "frame_id": frame_id
192
+ }
193
+
194
+ # Add to inference queue with timestamp as tie-breaker for priority queue comparison
195
+ self.inference_queue.put((stream_msg.priority, time.time(), task_data))
196
+
197
+ except json.JSONDecodeError as e:
198
+ self.logger.error(f"Failed to parse message JSON: {e}")
199
+ except Exception as e:
200
+ self.logger.error(f"Error processing message: {e}")
201
+
@@ -0,0 +1,127 @@
1
+ import logging
2
+ import threading
3
+ import queue
4
+
5
+
6
+ try:
7
+ import redis # type: ignore
8
+ except Exception: # pragma: no cover
9
+ redis = None # type: ignore
10
+
11
+
12
+ class RedisFrameCache:
13
+ """Non-blocking Redis cache for frames keyed by frame_id.
14
+
15
+ Stores base64 string content under key 'stream:frames:{frame_id}' with field 'frame'.
16
+ Each insert sets or refreshes the TTL.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ host: str = "localhost",
22
+ port: int = 6379,
23
+ db: int = 0,
24
+ password: str = None,
25
+ username: str = None,
26
+ ttl_seconds: int = 300,
27
+ prefix: str = "stream:frames:",
28
+ max_queue: int = 10000,
29
+ worker_threads: int = 2,
30
+ connect_timeout: float = 2.0,
31
+ socket_timeout: float = 0.5,
32
+ ) -> None:
33
+ self.logger = logging.getLogger(__name__ + ".frame_cache")
34
+ self.ttl_seconds = int(ttl_seconds)
35
+ self.prefix = prefix
36
+ self.queue: "queue.Queue" = queue.Queue(maxsize=max_queue)
37
+ self.threads = []
38
+ self.running = False
39
+ self._client = None
40
+ self._worker_threads = max(1, int(worker_threads))
41
+
42
+ if redis is None:
43
+ self.logger.warning("redis package not installed; frame caching disabled")
44
+ return
45
+
46
+ try:
47
+ self._client = redis.Redis(
48
+ host=host,
49
+ port=port,
50
+ db=db,
51
+ password=password,
52
+ username=username,
53
+ socket_connect_timeout=connect_timeout,
54
+ socket_timeout=socket_timeout,
55
+ health_check_interval=30,
56
+ retry_on_timeout=True,
57
+ decode_responses=True, # store strings directly
58
+ )
59
+ except Exception as e:
60
+ self.logger.warning("Failed to init Redis client: %s", e)
61
+ self._client = None
62
+
63
+ def start(self) -> None:
64
+ if not self._client or self.running:
65
+ return
66
+ self.running = True
67
+ for i in range(self._worker_threads):
68
+ t = threading.Thread(target=self._worker, name=f"FrameCache-{i}", daemon=True)
69
+ t.start()
70
+ self.threads.append(t)
71
+
72
+ def stop(self) -> None:
73
+ if not self.running:
74
+ return
75
+ self.running = False
76
+ for _ in self.threads:
77
+ try:
78
+ self.queue.put_nowait(None)
79
+ except Exception:
80
+ pass
81
+ for t in self.threads:
82
+ try:
83
+ t.join(timeout=2.0)
84
+ except Exception:
85
+ pass
86
+ self.threads.clear()
87
+
88
+ def put(self, frame_id: str, base64_content: str) -> None:
89
+ """Enqueue a cache write for the given frame.
90
+
91
+ - frame_id: unique identifier
92
+ - base64_content: base64-encoded image string
93
+ """
94
+ if not self._client or not self.running:
95
+ return
96
+ if not frame_id or not base64_content:
97
+ return
98
+ try:
99
+ key = f"{self.prefix}{frame_id}"
100
+ self.queue.put_nowait((key, base64_content))
101
+ except queue.Full:
102
+ # Drop silently; never block pipeline
103
+ self.logger.debug("Frame cache queue full; dropping frame_id=%s", frame_id)
104
+
105
+ def _worker(self) -> None:
106
+ while self.running:
107
+ try:
108
+ item = self.queue.get(timeout=0.5)
109
+ except queue.Empty:
110
+ continue
111
+ if item is None:
112
+ break
113
+ key, base64_content = item
114
+ try:
115
+ # Store base64 string in a Redis hash field 'frame', then set TTL
116
+ # Mimics the Go backend behavior
117
+ self._client.hset(key, "frame", base64_content)
118
+ self._client.expire(key, self.ttl_seconds)
119
+ except Exception as e:
120
+ self.logger.debug("Failed to cache frame %s: %s", key, e)
121
+ finally:
122
+ try:
123
+ self.queue.task_done()
124
+ except Exception:
125
+ pass
126
+
127
+