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.
This commit is contained in:
patrykorwat 2026-08-20 20:34:04 +00:00
parent 71ccf55927
commit 13215b0997

View File

@ -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