From 9d8a1a10a40804ba281823861f8f5253234b6fc7 Mon Sep 17 00:00:00 2001 From: patrykorwat Date: Thu, 20 Aug 2026 21:19:04 +0000 Subject: [PATCH] fix(axelera): revert primer-index-drain; it wedged production startup The primer 'drain by run index' was too fragile under Frigate's real workload: with 7 cameras starting simultaneously the first few real frames arrive before the SDK finishes emitting primer results, their ids were drained without a result, run() hit the timeout and Frigate restarted the detect process every ~15s (zero detections). Revert to the stable approach: keep-alive frames sustain the SDK during quiet/init, _pump never consumes a pending id while the queue is empty, and run() is serialized (lock) so at most one real frame is in flight. The remaining theoretical primer/keep-alive misattribution window only exists when the queue is empty mid-run, which does not happen under Frigate's continuous feed. --- frigate/detectors/plugins/axelera.py | 34 ++++++++-------------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/frigate/detectors/plugins/axelera.py b/frigate/detectors/plugins/axelera.py index a0aa1c296a..e3ea5436e7 100644 --- a/frigate/detectors/plugins/axelera.py +++ b/frigate/detectors/plugins/axelera.py @@ -153,9 +153,6 @@ 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 @@ -181,19 +178,21 @@ class _AxeleraInference: def _feed(self): # primer frames: the SDK init pre-buffers several frames before the - # source yields (validated on hardware). Their results are drained by - # _pump (by index) and NEVER attributed to a request id. + # source yields (validated on hardware). Their results are dropped by + # the pump while the pending queue is empty. for _ in range(PRIMER_FRAMES): yield np.zeros((self._height, self._width, 3), np.uint8) while True: + frame = frame_id = None try: frame_id, frame = self._frame_q.get(timeout=1.0) except queue.Empty: - # 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 + # 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 + ) if frame is None: # shutdown sentinel from stop() return if frame_id is not None: @@ -255,14 +254,7 @@ class _AxeleraInference: def _pump(self): logger.info("axelera: inference stream started") try: - 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 + for frame_result in self._stream: out = _pad_rows(self._detection_rows(frame_result)) if self._pending_ids: # FIFO: the stream returns results in submission order, so @@ -309,12 +301,6 @@ 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