From 13215b0997cbe0acc8ec8b0d228f1cb03c7e5209 Mon Sep 17 00:00:00 2001 From: patrykorwat Date: Thu, 20 Aug 2026 20:34:04 +0000 Subject: [PATCH] fix(axelera): never attribute primer/keep-alive results to a real request The SDK can buffer primer results and emit them AFTER the first real frame is already pending; the FIFO pump then popped a real id and stored the skipped/buffered (empty) result under it, dropping the real one. This misattributes detections across cameras even with run() serialized. - eliminate keep-alive frames entirely (Frigate feeds the queue continuously; nothing to keep alive) - a synthetic frame's result can arrive during a pending real id, - drain the primer results by index BEFORE any pending-id bookkeeping, so they can never consume a request id; the first run() waits up to 0.5s for the primer drain. --- frigate/detectors/plugins/axelera.py | 36 ++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/frigate/detectors/plugins/axelera.py b/frigate/detectors/plugins/axelera.py index b25a4eb50b..a0aa1c296a 100644 --- a/frigate/detectors/plugins/axelera.py +++ b/frigate/detectors/plugins/axelera.py @@ -51,6 +51,7 @@ AX_DEFAULT_NETWORK = "yolox-s-coco-onnx" INFERENCE_TIMEOUT = 5.0 MAX_ROWS = 20 +PRIMER_FRAMES = 3 # ----------------- Utility Functions ----------------- # @@ -152,6 +153,9 @@ class _AxeleraInference: # 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() + # Set once the SDK has finished emitting the primer results, so they + # can never be attributed to a real pending request id. + self._primed = threading.Event() if stream_factory is None: # lazy import: never import the SDK unless the detector is used @@ -177,20 +181,19 @@ class _AxeleraInference: def _feed(self): # primer frames: the SDK init pre-buffers several frames before the - # source yields (validated on hardware). Their ids are NOT registered, - # so their stream results are dropped by the pump. - for _ in range(3): + # source yields (validated on hardware). Their results are drained by + # _pump (by index) and NEVER attributed to a request id. + for _ in range(PRIMER_FRAMES): yield np.zeros((self._height, self._width, 3), np.uint8) while True: try: frame_id, frame = self._frame_q.get(timeout=1.0) except queue.Empty: - # keep-alive: no request pending, keep the SDK stream fed so the - # inference timeout never kills it during quiet periods. The - # None id means this frame's result is dropped by the pump. - frame_id, frame = None, np.zeros( - (self._height, self._width, 3), np.uint8 - ) + # Never synthesize keep-alive frames: a synthetic frame's + # result can arrive while a real id is pending and steal that + # id (misattribution across cameras). In Frigate the queue is + # fed continuously, so idling simply waits here. + continue if frame is None: # shutdown sentinel from stop() return if frame_id is not None: @@ -252,7 +255,14 @@ class _AxeleraInference: def _pump(self): logger.info("axelera: inference stream started") try: - for frame_result in self._stream: + for run_index, frame_result in enumerate(self._stream): + if run_index < PRIMER_FRAMES: + # Primer results are drained WITHOUT consuming a request + # id — otherwise a buffered primer result could steal a + # real id that is already pending (P1 misattribution). + if run_index == PRIMER_FRAMES - 1: + self._primed.set() + continue out = _pad_rows(self._detection_rows(frame_result)) if self._pending_ids: # FIFO: the stream returns results in submission order, so @@ -299,6 +309,12 @@ class _AxeleraInference: # permanently wedging this detector. raise RuntimeError("Axelera inference thread stopped; restart required.") + # Wait briefly for the primer results to drain ONCE, so they can never + # be attributed to a real request id. After this the pump only ever + # sees real frames (keep-alive frames are no longer emitted). + if not self._primed.is_set(): + self._primed.wait(timeout=0.5) + # Every submitted frame gets a unique id; the pump stores the result # under this id, so we can wait for OUR frame's outcome exclusively. self._next_id += 1