From 71ccf55927a283b6d44956d5f8a7a7d291cefc6c Mon Sep 17 00:00:00 2001 From: patrykorwat Date: Thu, 20 Aug 2026 19:49:05 +0000 Subject: [PATCH] fix(axelera): serialize run() to prevent cross-camera result misattribution Voyager SDK does not tag results with a per-frame id and can reorder or buffer stream results under sustained load (observed on live with many thousands of frames: camera A started receiving boxes/paths from camera B). With several frames in flight, the FIFO pairing can store a result under a different frame's id. run() now holds a lock so at most one frame is in flight; the next stream result can only belong to the frame just pushed, making pairing exact regardless of SDK reordering. Pipeline throughput stays far below limits (~9ms per inference vs 28ms available per frame at 7x5fps). --- frigate/detectors/plugins/axelera.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frigate/detectors/plugins/axelera.py b/frigate/detectors/plugins/axelera.py index 1d1153f9ed..b25a4eb50b 100644 --- a/frigate/detectors/plugins/axelera.py +++ b/frigate/detectors/plugins/axelera.py @@ -146,6 +146,12 @@ class _AxeleraInference: self._pending_ids: collections.deque = collections.deque() self._results: dict[int, np.ndarray] = {} self._next_id = 0 + # Serializes run() calls: the SDK stream does not tag results with a + # per-frame id and can reorder/buffer results under sustained load + # (observed after many thousands of frames), so with several frames in + # flight the FIFO pop can misattribute a result to the wrong camera. + # With at most one frame in flight the result can only belong to it. + self._run_lock = threading.Lock() if stream_factory is None: # lazy import: never import the SDK unless the detector is used @@ -262,6 +268,14 @@ class _AxeleraInference: def run(self, tensor_input, timeout: float = INFERENCE_TIMEOUT) -> np.ndarray: """Run one inference and return this frame's detections (see detect_raw).""" + # At most ONE frame may be in flight at a time: with several frames in + # the stream the SDK can reorder/buffer their results (misattribution + # across cameras observed after many thousands of frames). Serialized, + # the next returned result can only belong to the one frame we pushed. + with self._run_lock: + return self._run_locked(tensor_input, timeout) + + def _run_locked(self, tensor_input, timeout: float) -> np.ndarray: if tensor_input is None or getattr(tensor_input, "ndim", 0) < 3: raise ValueError( f"Unexpected frame shape: {getattr(tensor_input, 'shape', tensor_input)}" @@ -316,7 +330,7 @@ class _AxeleraInference: def stop(self): # cleanup must not raise try: - self._frame_q.put_nowait(None) + self._frame_q.put_nowait((None, None)) except Exception: # noqa: S110, BLE001 pass try: