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).
This commit is contained in:
patrykorwat 2026-08-20 19:49:05 +00:00
parent 8a3483c7a2
commit 71ccf55927

View File

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