matrice-streaming 0.1.63__tar.gz → 0.1.65__tar.gz

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 (71) hide show
  1. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/PKG-INFO +1 -1
  2. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/matrice_streaming.egg-info/PKG-INFO +1 -1
  3. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/nvdec.py +101 -12
  4. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py +34 -2
  5. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/LICENSE.txt +0 -0
  6. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/README.md +0 -0
  7. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/matrice_streaming.egg-info/SOURCES.txt +0 -0
  8. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/matrice_streaming.egg-info/dependency_links.txt +0 -0
  9. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/matrice_streaming.egg-info/not-zip-safe +0 -0
  10. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/matrice_streaming.egg-info/top_level.txt +0 -0
  11. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/pyproject.toml +0 -0
  12. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/setup.cfg +0 -0
  13. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/setup.py +0 -0
  14. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/__init__.py +0 -0
  15. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/client/__init__.py +0 -0
  16. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/client/client.py +0 -0
  17. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/client/client_utils.py +0 -0
  18. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/deployment/__init__.py +0 -0
  19. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/deployment/camera_manager.py +0 -0
  20. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/deployment/deployment.py +0 -0
  21. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/deployment/inference_pipeline.py +0 -0
  22. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/deployment/streaming_gateway_manager.py +0 -0
  23. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/deployment/todo.txt +0 -0
  24. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/py.typed +0 -0
  25. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/__init__.py +0 -0
  26. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/ARCHITECTURE.md +0 -0
  27. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/__init__.py +0 -0
  28. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +0 -0
  29. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +0 -0
  30. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +0 -0
  31. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +0 -0
  32. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/encoder_manager.py +0 -0
  33. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +0 -0
  34. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +0 -0
  35. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +0 -0
  36. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +0 -0
  37. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/frame_processor.py +0 -0
  38. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +0 -0
  39. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +0 -0
  40. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +0 -0
  41. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +0 -0
  42. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +0 -0
  43. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/retry_manager.py +0 -0
  44. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +0 -0
  45. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +0 -0
  46. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +0 -0
  47. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/debug/README.md +0 -0
  48. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/debug/__init__.py +0 -0
  49. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/debug/benchmark.py +0 -0
  50. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +0 -0
  51. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/debug/debug_stream_backend.py +0 -0
  52. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +0 -0
  53. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/debug/debug_utils.py +0 -0
  54. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/debug/example_debug_streaming.py +0 -0
  55. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/debug/test_videoplayback.py +0 -0
  56. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/dynamic_camera_manager.py +0 -0
  57. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/event_listener.py +0 -0
  58. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/metrics_reporter.py +0 -0
  59. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/streaming_action.py +0 -0
  60. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/streaming_gateway.py +0 -0
  61. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/streaming_gateway_utils.py +0 -0
  62. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/src/matrice_streaming/streaming_gateway/streaming_status_listener.py +0 -0
  63. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/tests/test_async_infrastructure.py +0 -0
  64. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/tests/test_batch_auto_calculation.py +0 -0
  65. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/tests/test_batching_verification.py +0 -0
  66. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/tests/test_e2e_production.py +0 -0
  67. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/tests/test_flatten_binary.py +0 -0
  68. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/tests/test_gstreamer_integration.py +0 -0
  69. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/tests/test_msgpack_fix.py +0 -0
  70. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/tests/test_phase1_unit.py +0 -0
  71. {matrice_streaming-0.1.63 → matrice_streaming-0.1.65}/tests/test_phase2_scaling.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice_streaming
3
- Version: 0.1.63
3
+ Version: 0.1.65
4
4
  Summary: Common server utilities for Matrice.ai services
5
5
  Author-email: "Matrice.ai" <dipendra@matrice.ai>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice_streaming
3
- Version: 0.1.63
3
+ Version: 0.1.65
4
4
  Summary: Common server utilities for Matrice.ai services
5
5
  Author-email: "Matrice.ai" <dipendra@matrice.ai>
6
6
  License-Expression: MIT
@@ -547,7 +547,12 @@ def surface_to_nv12(frame, target_h: int = 640, target_w: int = 640) -> Optional
547
547
  return nv12_frame[:, :, cp.newaxis] if nv12_frame is not None else None
548
548
 
549
549
  except Exception as e:
550
- logger.warning(f"surface_to_nv12 failed: {e}")
550
+ # Safely encode error message (some CUDA errors contain non-ASCII chars like '×')
551
+ try:
552
+ err_msg = str(e).encode('ascii', errors='replace').decode('ascii')
553
+ except Exception:
554
+ err_msg = "unknown error"
555
+ logger.warning(f"surface_to_nv12 failed: {err_msg}")
551
556
  return None
552
557
 
553
558
 
@@ -709,12 +714,17 @@ def nvdec_pool_worker(
709
714
  target_w: int = 640,
710
715
  target_fps: int = 0,
711
716
  shared_frame_count: Optional[mp.Value] = None,
717
+ gpu_frame_count: Optional[mp.Value] = None,
712
718
  ):
713
719
  """NVDEC worker thread.
714
720
 
715
721
  Decodes frames and writes NV12 tensors to ring buffers.
716
722
  Uses dedicated CUDA stream per worker for kernel overlap.
717
723
  Supports FPS limiting when target_fps > 0.
724
+
725
+ Args:
726
+ shared_frame_count: Global counter (all GPUs)
727
+ gpu_frame_count: Per-GPU counter (this GPU only)
718
728
  """
719
729
  if CUPY_AVAILABLE:
720
730
  cp.cuda.Device(pool.gpu_id).use()
@@ -727,7 +737,6 @@ def nvdec_pool_worker(
727
737
  frames_since_counter_update = 0
728
738
  counter_batch_size = 100
729
739
  start_time = time.perf_counter()
730
- last_log_time = start_time
731
740
  camera_ids = pool.get_camera_ids_for_decoder(decoder_idx)
732
741
  num_streams = len(camera_ids)
733
742
 
@@ -745,19 +754,12 @@ def nvdec_pool_worker(
745
754
  next_frame_time = 0
746
755
  fps_mode = ", unlimited FPS"
747
756
 
748
- logger.info(f"Worker {worker_id}: decoder={decoder_idx}, cams={num_streams}{fps_mode}")
757
+ logger.debug(f"Worker {worker_id}: decoder={decoder_idx}, cams={num_streams}{fps_mode}")
749
758
 
750
759
  while not stop_event.is_set():
751
760
  if time.perf_counter() - start_time >= duration_sec:
752
761
  break
753
762
 
754
- now = time.perf_counter()
755
- if now - last_log_time >= 5.0:
756
- elapsed = now - start_time
757
- fps = local_frames / elapsed if elapsed > 0 else 0
758
- logger.info(f"Worker {worker_id}: {local_frames} frames, {fps:.0f} FPS")
759
- last_log_time = now
760
-
761
763
  # FPS limiting: wait until next scheduled frame time
762
764
  if fps_limit_enabled:
763
765
  current_time = time.perf_counter()
@@ -782,11 +784,16 @@ def nvdec_pool_worker(
782
784
  local_frames += 1
783
785
  frames_since_counter_update += 1
784
786
 
785
- # Update shared counter for real-time progress
787
+ # Update global counter (all GPUs)
786
788
  if shared_frame_count is not None:
787
789
  with shared_frame_count.get_lock():
788
790
  shared_frame_count.value += 1
789
791
 
792
+ # Update per-GPU counter (this GPU only)
793
+ if gpu_frame_count is not None:
794
+ with gpu_frame_count.get_lock():
795
+ gpu_frame_count.value += 1
796
+
790
797
  # Update next frame time for FPS limiting
791
798
  if fps_limit_enabled:
792
799
  next_frame_time += frame_interval
@@ -842,10 +849,19 @@ def nvdec_pool_process(
842
849
  num_slots: int = 32,
843
850
  target_fps: int = 0,
844
851
  shared_frame_count: Optional[mp.Value] = None,
852
+ gpu_frame_counts: Optional[Dict[int, mp.Value]] = None,
853
+ total_num_streams: int = 0,
854
+ total_num_gpus: int = 1,
845
855
  ):
846
856
  """NVDEC process for one GPU.
847
857
 
848
858
  Creates NV12 ring buffers: (H*1.5, W) = 0.6 MB/frame.
859
+
860
+ Args:
861
+ gpu_frame_counts: Dict mapping gpu_id -> per-GPU frame counter (for per-GPU stats)
862
+ shared_frame_count: Global frame counter (for overall stats)
863
+ total_num_streams: Total streams across ALL GPUs (for global per-stream calc)
864
+ total_num_gpus: Total number of GPUs (for context in logging)
849
865
  """
850
866
  if not camera_configs:
851
867
  return
@@ -854,6 +870,9 @@ def nvdec_pool_process(
854
870
  target_h = camera_configs[0].height
855
871
  target_w = camera_configs[0].width
856
872
 
873
+ # Get per-GPU counter (or fall back to shared if not provided)
874
+ gpu_frame_count = gpu_frame_counts.get(gpu_id) if gpu_frame_counts else None
875
+
857
876
  if CUPY_AVAILABLE:
858
877
  cp.cuda.Device(gpu_id).use()
859
878
 
@@ -947,15 +966,85 @@ def nvdec_pool_process(
947
966
  target_w,
948
967
  target_fps,
949
968
  shared_frame_count,
969
+ gpu_frame_count, # Per-GPU counter
950
970
  )
951
971
  )
952
972
  t.start()
953
973
  threads.append(t)
954
974
 
975
+ # Progress monitoring loop with current/avg FPS tracking
955
976
  start_time = time.perf_counter()
977
+ last_report_time = start_time
978
+ last_gpu_frame_count = 0
979
+ last_global_frame_count = 0
980
+ report_interval = 5.0
981
+ processing_start_time = None
982
+ gpu_frames_at_start = 0
983
+ global_frames_at_start = 0
984
+ num_gpu_streams = len(camera_configs)
985
+
956
986
  while not stop_event.is_set():
957
- if time.perf_counter() - start_time >= duration_sec:
987
+ current_time = time.perf_counter()
988
+ if current_time - start_time >= duration_sec:
958
989
  break
990
+
991
+ # Periodic progress report with current and average FPS
992
+ if current_time - last_report_time >= report_interval:
993
+ elapsed = current_time - start_time
994
+ remaining = max(0, duration_sec - elapsed)
995
+
996
+ # Get per-GPU frame count (this GPU only)
997
+ gpu_frames = gpu_frame_count.value if gpu_frame_count else 0
998
+ gpu_interval_frames = gpu_frames - last_gpu_frame_count
999
+ gpu_interval_fps = gpu_interval_frames / report_interval
1000
+ gpu_per_stream_fps = gpu_interval_fps / num_gpu_streams if num_gpu_streams > 0 else 0
1001
+
1002
+ # Get global frame count (all GPUs)
1003
+ global_frames = shared_frame_count.value if shared_frame_count else 0
1004
+ global_interval_frames = global_frames - last_global_frame_count
1005
+ global_interval_fps = global_interval_frames / report_interval
1006
+ global_per_stream_fps = global_interval_fps / total_num_streams if total_num_streams > 0 else 0
1007
+
1008
+ # Track when processing actually starts (exclude warmup)
1009
+ if processing_start_time is None and gpu_frames > 0:
1010
+ processing_start_time = last_report_time
1011
+ gpu_frames_at_start = last_gpu_frame_count
1012
+ global_frames_at_start = last_global_frame_count
1013
+
1014
+ # Calculate average FPS excluding warmup
1015
+ if processing_start_time is not None:
1016
+ processing_elapsed = current_time - processing_start_time
1017
+
1018
+ # Per-GPU averages
1019
+ gpu_processing_frames = gpu_frames - gpu_frames_at_start
1020
+ gpu_avg_fps = gpu_processing_frames / processing_elapsed if processing_elapsed > 0 else 0
1021
+ gpu_avg_per_stream = gpu_avg_fps / num_gpu_streams if num_gpu_streams > 0 else 0
1022
+
1023
+ # Global averages
1024
+ global_processing_frames = global_frames - global_frames_at_start
1025
+ global_avg_fps = global_processing_frames / processing_elapsed if processing_elapsed > 0 else 0
1026
+ global_avg_per_stream = global_avg_fps / total_num_streams if total_num_streams > 0 else 0
1027
+
1028
+ # Log per-GPU stats
1029
+ logger.info(
1030
+ f"GPU{gpu_id} [{elapsed:5.1f}s] {gpu_frames:,} frames ({num_gpu_streams} cams) | "
1031
+ f"cur: {gpu_interval_fps:,.0f} FPS ({gpu_per_stream_fps:.1f}/cam) | "
1032
+ f"avg: {gpu_avg_fps:,.0f} FPS ({gpu_avg_per_stream:.1f}/cam)"
1033
+ )
1034
+
1035
+ # Log global stats (only from GPU0 to avoid spam)
1036
+ if gpu_id == 0:
1037
+ logger.info(
1038
+ f"GLOBAL [{elapsed:5.1f}s] {global_frames:,} frames ({total_num_streams} cams, {total_num_gpus} GPUs) | "
1039
+ f"cur: {global_interval_fps:,.0f} FPS ({global_per_stream_fps:.1f}/cam) | "
1040
+ f"avg: {global_avg_fps:,.0f} FPS ({global_avg_per_stream:.1f}/cam) | "
1041
+ f"{remaining:.0f}s left"
1042
+ )
1043
+
1044
+ last_gpu_frame_count = gpu_frames
1045
+ last_global_frame_count = global_frames
1046
+ last_report_time = current_time
1047
+
959
1048
  time.sleep(0.1)
960
1049
 
961
1050
  thread_stop_event.set()
@@ -123,6 +123,7 @@ class NVDECWorkerManager:
123
123
  self._stop_event: Optional[mp.Event] = None
124
124
  self._result_queue: Optional[mp.Queue] = None
125
125
  self._shared_frame_count: Optional[mp.Value] = None
126
+ self._gpu_frame_counts: Dict[int, mp.Value] = {} # Per-GPU counters
126
127
  self._start_time: Optional[float] = None
127
128
  self._is_running = False
128
129
 
@@ -200,9 +201,20 @@ class NVDECWorkerManager:
200
201
  ctx = mp.get_context("spawn")
201
202
  self._stop_event = ctx.Event()
202
203
  self._result_queue = ctx.Queue()
203
- self._shared_frame_count = ctx.Value('i', 0)
204
+ self._shared_frame_count = ctx.Value('L', 0) # Global counter (all GPUs)
204
205
  self._start_time = time.perf_counter()
205
206
 
207
+ # Create per-GPU frame counters
208
+ self._gpu_frame_counts = {}
209
+ for gpu_id in range(self.num_gpus):
210
+ if self._gpu_camera_assignments[gpu_id]: # Only if GPU has cameras
211
+ self._gpu_frame_counts[gpu_id] = ctx.Value('L', 0)
212
+
213
+ total_num_streams = len(self._stream_configs)
214
+ total_num_gpus = len([g for g in range(self.num_gpus) if self._gpu_camera_assignments[g]])
215
+
216
+ logger.info(f"Starting NVDEC: {total_num_streams} cameras across {total_num_gpus} GPUs")
217
+
206
218
  # Start one process per GPU that has cameras
207
219
  for gpu_id in range(self.num_gpus):
208
220
  gpu_cameras = self._gpu_camera_assignments[gpu_id]
@@ -221,7 +233,10 @@ class NVDECWorkerManager:
221
233
  self.nvdec_burst_size, # burst_size
222
234
  self.num_slots, # num_slots
223
235
  self.target_fps, # target_fps
224
- self._shared_frame_count, # shared_frame_count
236
+ self._shared_frame_count, # shared_frame_count (global)
237
+ self._gpu_frame_counts, # gpu_frame_counts (per-GPU dict)
238
+ total_num_streams, # total_num_streams
239
+ total_num_gpus, # total_num_gpus
225
240
  ),
226
241
  name=f"NVDECWorker-GPU{gpu_id}",
227
242
  daemon=False,
@@ -311,6 +326,23 @@ class NVDECWorkerManager:
311
326
  if self._stream_configs else 0
312
327
  )
313
328
 
329
+ # Add per-GPU frame counts and FPS
330
+ if self._gpu_frame_counts and self._start_time:
331
+ elapsed = time.perf_counter() - self._start_time
332
+ gpu_stats = {}
333
+ for gpu_id, counter in self._gpu_frame_counts.items():
334
+ gpu_frames = counter.value
335
+ num_cams = len(self._gpu_camera_assignments.get(gpu_id, []))
336
+ gpu_fps = gpu_frames / elapsed if elapsed > 0 else 0
337
+ gpu_per_cam = gpu_fps / num_cams if num_cams > 0 else 0
338
+ gpu_stats[f'GPU{gpu_id}'] = {
339
+ 'frames': gpu_frames,
340
+ 'cameras': num_cams,
341
+ 'fps': gpu_fps,
342
+ 'fps_per_cam': gpu_per_cam,
343
+ }
344
+ stats['per_gpu_stats'] = gpu_stats
345
+
314
346
  # Collect any available results from queue (non-blocking)
315
347
  gpu_results = []
316
348
  if self._result_queue: