Add Axelera Metis NPU detector plugin v.0.19 (#24270)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / AMD64 Smoke Test (push) Blocked by required conditions
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

* Add Axelera Metis NPU object detector plugin

Runs Frigate inference on the Axelera Metis NPU through the vendor's
low-level axelera.runtime API with pip-installed, sha256-pinned wheels
from Axelera's public artifactory (self-contained: auditwheel-repaired
libs + bundled firmware).

- async send_input/receive_output contract (FIFO pairing, overload emits
  zero rows instead of raising, frames are copied out of camera shm)
- generic YOLO-family decoder driven by the compiled model's manifest.json
  (padded input geometry, dequant params, baked postprocess graph via onnx)
- labels come from the compiled model's model_info.json; empty labelmaps
  fail loudly at init

* object_detection: async runner path for the axelera detector

* detectors: detect_axelera probe (/dev/metis*) + device node init

* stats: Axelera Metis board temperature poller

The card exposes no utilization counter usable next to a live detector, so
the poller reports the board-controller temperature read via axcmd (cached
5s; the read must not race detector init).

* docs: Axelera Metis detector (install, config, model catalog, downloads)

* axelera: expose Metis colon-name device node and image deps for runtime enumeration

The Axelera runtime enumerates Metis cards via the colon-separated node
name created by the vendor udev rule on the host (e.g. /dev/metis-0:1:0)
and probes kernel state with lsmod/lspci. Containers only receive the
raw /dev/metis-0-1-0 node, so the detector never found the card.

- init-devices: create /dev/metis-BB:DD:F aliases for each Metis node
- install_deps: add kmod and pciutils so the runtime's driver check works

* axelera: review round 2 (downloader, stop release, pixel format, docs size, per-class NMS)

- download model zips through ModelDownloader (atomic .part rename; a
  truncated download no longer poisons the cache)
- stop(): release the Context (releases connection/model/instance); the
  previous instance.free() call never existed in the runtime API and was
  silently swallowed
- honor input_pixel_format: flip to RGB planes only when Frigate delivers
  BGR (input_pixel_format: bgr); with rgb the frame passes through as-is
- docs/catalog: width/height = compiled 640x640 input (square regions no
  longer get squashed twice), document input_pixel_format: bgr
- NMS via xyxy_to_xywh_for_nms + per-class cv2.dnn.NMSBoxes, matching the
  other detector plugins instead of a hand-rolled class-agnostic pass

* axelera: report Metis temperature only on the NPU card

The same board readout was surfaced twice on the System page: once as
the detector temperature (get_hardware_temperatures) and once on the
NPU card (npu_usages). Keep the NPU card, where the value belongs.

* axelera: report AIPU utilization as the detector run duty cycle

The runtime exposes no device-side usage counter (mvm_utilisation is a
configuration limit, not a readout), so the NPU card rendered empty.
ModelInstance.run blocks until the AIPU completes: the detector process
now times each run and publishes busy/wall over a 2s window through a
small file under the tmp cache dir; the stats poller reads it and sets
the npu key. A stale file (detector idle/restarting) reports nothing
rather than a stale number.

* axelera: cut hot-path allocations in preprocess and dequantise

Preprocess reused three fresh arrays per frame (resize output, quantised
copy, strided channel flip at write time) and refilled the whole padded
input buffer. Now resize writes into a scratch with cv2 dst=, the BGR->RGB
swap is a cvtColor into scratch (no strided copy), the int8 quantise runs
with out=, and only the border strips the content rect does not cover get
memset. Measured 39% faster on the 704x576 -> 640x640 path.

Dequantise allocated two fp32 temporaries per head (astype then the
dequant expression) before the transpose copy. copyto now casts int8->fp32
while transposing straight into a reused fp32 feed buffer, and the zp/scale
math runs in place. Measured 78% faster for the YOLOX-S head set, outputs
bit-identical (int8->fp32 cast is lossless).

Argmax+gather replaces max+argmax (one class-axis reduction instead of
two), and the postprocess session pins intra_op_num_threads=1: one frame
is in flight through the decoder, extra ORT threads only add context
switches.

* axelera: publish the run duty cycle through a shared Value

Replace the tmp-file duty-cycle handoff (and the FRIGATE_TMP env var
nothing else reads) with a third multiprocessing.Value alongside
avg_inference_speed and detection_start: ObjectDetectProcess creates it,
AsyncDetectorRunner passes it to the detector, and the plugin publishes
busy/wall of the blocking ModelInstance.run over a 2s window, mirroring
how memryx receives its shared state. No filesystem path, no staleness
window, no atomic rename, and stats/hardware.py no longer imports the
plugin module to find the file.

The producer also ticks the window while the queue is empty, so the duty
cycle falls to 0 when the card idles instead of sticking at its last
value.

Name it what it is: host-side occupancy of the run call, including DMA
and driver time, not an AIPU core counter (the runtime exposes no
device-side one). The NPU entry now reads temperature from the board
controller plus the detector-reported run duty cycle folded in where
the detector processes are in reach.

* axelera: enumerate devices explicitly and fail with an actionable error

Context.list_devices() keys off the lspci BDF and needs the colon-form
node to be present, so surface an empty enumeration at init with the
fix-it hint instead of a bare device_connect(None) ConnectionError
several frames deeper. Uses the DeviceInfo the runtime enumerated
rather than relying on the pick-first implicit path.

* axelera: deliver every accepted result under backpressure, reset duty cycle on restart

Greptile P1: _push_result dropped a completion when the result queue was
full, stranding the runner's send_times entry and the input SHM mapping
(1 submission : 1 result contract). Replace the drop with a stop-aware
blocking put so an accepted submission always produces exactly one
delivered completion.

Greptile P2: start_or_restart did not clear run_duty_cycle when starting
a replacement process, so stats could show the previous process's duty
cycle while the new detector was still initializing. Reset it to the
-1.0 sentinel alongside detection_start, inside the async branch.

Greptile P2 (offline docs): list axelera among the detector types whose
pre-seeded runtime files live under /config/model_cache/runtimes/<type>.

* axelera: reviewer round 3 (plugin-owned enumeration tools, generic DEVICE_ALIASES, generic duty-cycle fold)

install_deps.sh: drop kmod/pciutils from the base image. The runtime's
list_devices() only greps popen'd lspci/lsmod output over /sys and /proc,
so the plugin now installs shell stand-ins in its own
/config/model_cache/axelera/bin and prepends it to PATH when the real
tools are missing (hardware-verified: the stand-in lspci prints the
Device 1f9d:<id> line the runtime matches, the lsmod shape matches
/proc/modules).

init-devices: replace the Metis-keyed alias block with a generic
DEVICE_ALIASES env var (comma separated alias=target pairs, next to
DEVICE_ACL_PATHS). Docker rejects colon-named nodes in --device AND in
bind mounts (probed: 'too many colons' on both the long volume form and
the mount form), so the in-container symlink stays necessary; docs carry
the pair users must set.

stats/util.py: fold any detector's published run_duty_cycle into the
matching npu_usages entry generically; _axelera_run_duty_cycle and the
per-vendor branch are gone.

plugin: the no-device RuntimeError now names the raw dash-form node
(--device rejects colons, users can only pass the raw node) and points
at DEVICE_ALIASES for the colon alias.

* axelera: reviewer round 4 (aggregate shared-type duty cycles, american spellings)

stats/util.py: several detector processes can share one hardware type (e.g.
two axelera cards on one host). Iterating and assigning made the displayed
NPU usage depend on dict order, last writer wins. Collect the published
windows per type and write the busiest one, matching what the removed
per-vendor helper did with max().

plugin: the remaining British spellings (dequantise family) corrected to
the American forms the repo requires.

plugin: model path resolution follows hailo8l's single-string pattern (URL
or local path or preset name in one value), the download uses urllib
directly like memryx, and _find_model_json now raises on an ambiguous
multi-model directory instead of returning whichever sorts first.

plugin: AxeleraDetector exposes shutdown() (the async contract hook base.py
calls) instead of close(); __del__ guards a partially initialised instance.

* axelera: download URL models through the atomic ModelDownloader

* axelera: bring the detector unit tests in tree

* init-devices: create DEVICE_ALIASES before the ACL-mode exits

An alias is not an ACL grant: users running with FRIGATE_RUN_AS_ROOT or
FRIGATE_DEVICE_ACLS=false still need the colon-form node the runtime
enumerates, so create the aliases right after the euid check. Document
DEVICE_ALIASES next to DEVICE_ACL_PATHS and note it cannot work under
docker's user: mode (the container cannot write to /dev).

* detectors: cover the Axelera Metis probe in test_detector_hardware

* Update docs/docs/configuration/non_root.md

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* axelera: address the 2026-10-12 review round

- hash directory models deterministically in compute_model_hash instead of
  raising IsADirectoryError at startup
- keep canonical model labels addressable after user renames in the
  labelmap lookup (two classes can share one renamed name)
- key the URL model cache by source and archive content, extract replaced
  archives into fresh directories, prune superseded variants conservatively
- keep publishing the run duty cycle when the hardware poll has no entry
  (duty-only npu_usages entry instead of a hidden detector)
- document the user: mode device reach instead of host-only alias creation

* axelera: prune only generated cache entries

A fresh download matched user-provided layouts (the docs-recommended
<name>/ directory, <name>.zip archives) and look-alike model names
(yolox.zip vs yolox-s-*) by prefix. Pruning is now gated on the exact
generated identity pattern <name>-<source>-<32 hex digits>, so downloads
never touch user-provided directories, other models, or other sources.

* axelera: narrow the model path design to memryx-style zips

- path is always a .zip of the compiled output (local or http(s) URL),
  extracted fresh on every start under a lock, like the memryx
  custom-model flow
- _decode returns the raw class index and Frigate's labelmap names it;
  _build_label_lookup and _read_model_labels are gone, and the YOLOX
  objectness check keys off model_type
- scheme-less URL support and the quantized/manifest.json fallback are
  dropped
- width/height must equal the compiled input size; init raises otherwise
  instead of letterboxing
- compute_model_hash is back to the unchanged file-only hashing; the
  .extracted marker, source/content cache-name hashes and
  _prune_model_cache are removed

* Update frigate/test/test_axelera_detector.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* axelera: drop the run duty cycle, match the hailo temperature flow

- no other accelerator reports NPU usage and the published value was time
  blocked in run(), not device utilization from the vendor API; removing
  it leaves the runner selection as the only core change
- the board temperature moves to a get_hardware_temperatures branch like
  hailo, so it renders on the detector card; _poll_axelera, its POLLERS
  entry and the _scan_detectors line are gone
- axelera_runtime2 is not installed: axelera.runtime imports it only
  inside a try for its separate new-runtime API, and the low-level API
  the plugin uses (Context, load_model, device_connect,
  load_model_instance) lives in axelera.runtime.objects

* axelera: follow-ups from the 2026-10-13 review round

- the offline-setup table no longer lists the axelera_runtime2 wheels,
  which are not installed
- a failed board temperature read starts a 60s cooldown so an
  unresponsive axcmd does not delay every stats update

* axelera: drop the model-hash test, fix the model.json error messages

* axelera: drop DEVICE_ALIASES, document the CDI device spec

* axelera: use ln -sf in the CDI hook example

---------

Co-authored-by: Patryk Watkowski <patrykorwat@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Patryk Orwat
2026-10-20 09:53:28 -05:00
committed by GitHub
co-authored by Patryk Watkowski greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
parent 5bb2cde8b3
commit 91e91c68ef
13 changed files with 1802 additions and 5 deletions
+31
View File
@@ -1278,3 +1278,34 @@ axengine:
input_dtype: int
input_pixel_format: bgr
labelmap_path: /labelmap/coco-80.txt
axelera:
title: Axelera Metis
models:
- key: yolox
label: YOLOX
recommended: true
download: |-
Axelera models must be compiled for the Metis NPU with the Voyager SDK before Frigate can run them (compilation is not supported inside the container). Compile a model with the SDK, zip the compiled output, and point the model's `path` at the zip. The default recommendation is the Apache-2.0 licensed YOLOX (Megvii YOLOX, e.g. the `yolox-s-coco-onnx` model-zoo network).
ui: |-
Navigate to **Settings > System > Detection models** and select **Axelera Metis** from the **Hardware** dropdown. Then, on the same model, open the **Custom Model** tab and configure:
| Field | Value |
| ---------------------------------------- | ------------------------------------------------------------ |
| **Custom object detector model path** | `/config/model_cache/axelera/yolox-s-coco-onnx.zip` |
| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
| **Object detection model input width** | `640` (must equal the compiled input size) |
| **Object detection model input height** | `640` |
| **Model Input Pixel Color Format** | `bgr` (Frigate delivers BGR; the plugin hands RGB planes to the compiled input) |
| **Model Input Tensor Shape** | `nhwc` (Frigate's default value) |
| **Model Input D Type** | `int` (Frigate's default value) |
| **Object Detection Model Type** | `yolox` |
yaml: |-
models:
- devices:
- axelera:PCIe
path: /config/model_cache/axelera/yolox-s-coco-onnx.zip
model_type: yolox
width: 640
height: 640
input_pixel_format: bgr
labelmap_path: /labelmap/coco-80.txt
+43 -1
View File
@@ -63,6 +63,10 @@ Frigate supports multiple different detectors that work on different types of ha
- [AXEngine](#axera): axmodels can run on AXERA AI acceleration.
**Axelera** <CommunityBadge />
- [Axelera Metis](#axelera-metis): The Metis AI Accelerator is available in M.2 and PCIe formats, running compiled YOLO models through the vendor runtime.
**For Testing**
- [CPU Detector (not recommended for actual use](#cpu-detector-not-recommended): Use a CPU to run tflite model, this is not recommended and in most cases OpenVINO can be used in CPU mode with better results.
@@ -100,7 +104,7 @@ models:
- openvino:GPU
```
Coral EdgeTPU and MemryX accelerators can only be opened by one process, so those devices can not be repeated.
Coral EdgeTPU, MemryX, and Axelera Metis accelerators can only be opened by one process, so those devices can not be repeated.
### Running more than one model
@@ -940,3 +944,41 @@ The AXEngine python package is not part of the Frigate image. It is downloaded a
When configuring the AXEngine detector, you have to specify the model name.
<ModelConfigDropdown detectorTitle="AXEngine" models={objectDetectorsModels.axengine.models} />
---
## Axelera Metis
<CommunityBadge />
Hardware accelerated object detection is supported on the Axelera Metis AI Accelerator, available in M.2 and PCIe card formats. Inference runs through Axelera's low-level runtime (`axelera.runtime`), which executes compiled YOLO-family models with a flat per-frame latency.
See the [installation docs](../frigate/installation.md#axelera-metis) for information on configuring the Metis hardware.
:::info
The Axelera runtime is not part of the Frigate image. It is downloaded and installed into `/config/.local` the first time an Axelera detector is configured, verified against pinned checksums, and updated automatically when a Frigate release pins a new version. If the container has no internet access, see [Detector runtimes](/frigate/network_requirements#detector-runtimes) for how to provide the files yourself.
:::
### Configuration {#configuration-axelera}
<ModelConfigDropdown detectorTitle="Axelera Metis" models={objectDetectorsModels.axelera.models} />
#### Using a Custom Model
Models must be compiled for the Metis NPU with the Axelera Voyager SDK before Frigate can run them. The compiled output is a directory containing `1/model.json`, `1/manifest.json`, and the baked `1/postprocess_graph.onnx`.
Compile the model on a machine with the Voyager SDK installed (compilation is not supported inside the Frigate container), for example with the SDK's `deploy.py` against a model-zoo yaml. Then zip the compiled output and point the model's `path` at the zip (bind-mounted, or at an http(s) URL); the archive extracts fresh on every start.
```yaml
models:
- devices:
- axelera:PCIe
path: /config/model_cache/axelera/yolox-s-coco-onnx.zip
width: 640 # must equal the compiled input size
height: 640
input_pixel_format: bgr # Frigate delivers BGR; the plugin feeds RGB planes
model_type: yolox
labelmap_path: /labelmap/coco-80.txt
```
+54
View File
@@ -596,6 +596,60 @@ If you are using `docker run`, add this option to your command `--device /dev/ax
Finally, configure [hardware object detection](/configuration/object_detectors#axera) to complete the setup.
### Axelera Metis
The Axelera Metis AI Accelerator is available in the M.2 and PCIe card form factors and is supported on x86 (Intel/AMD) PCs and Rock 5B class arm64 boards.
The Axelera runtime used inside the container is not part of the Frigate image; Frigate downloads and installs it at first start once an Axelera detector is configured. Containers without internet access can provide the files themselves, see [Detector runtimes](/frigate/network_requirements#detector-runtimes). The host side kernel driver still has to be installed as described below.
#### Installation
Install the Metis PCIe kernel driver and firmware on the **host** following [Axelera's driver installation guide](https://developer.axelera.ai/axelera-documentation/Getting-Started/GettingStartedWithMetis/InstallingAxeleraMetisDriver). The driver creates the `/dev/metis*` device nodes. Reboot after installing it.
#### Setup
To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable`
The runtime enumerates the card by the colon-separated name the host udev rules create as a symlink (for example `/dev/metis-0:1:0`). Docker cannot pass that name as a `--device` argument, and a bind mount resolves the symlink to the raw node, which the runtime does not enumerate. A CDI device spec defines the container path and host path as separate fields, and a `startContainer` hook recreates the symlink inside the container.
Create `/etc/cdi/axelera.yaml` on the host, matching the node `ls /dev/metis*` prints:
```yaml
cdiVersion: "0.6.0"
kind: axelera.ai/metis
devices:
- name: "0"
containerEdits:
deviceNodes:
- path: /dev/metis-0-1-0
hostPath: /dev/metis-0-1-0
- path: /dev/dma_heap/system
hooks:
- hookName: startContainer
path: /bin/ln
args: ["/bin/ln", "-sf", "metis-0-1-0", "/dev/metis-0:1:0"]
timeout: 5
```
Then grant the device in your `docker-compose.yml` file:
```yaml
devices:
- axelera.ai/metis=0
```
If you are using `docker run`, add `--device axelera.ai/metis=0` to your command. CDI is enabled by default in Docker 28 and later; older versions need `"features": {"cdi": true}` in `daemon.json`. The hook runs as the container user, so this setup needs the container to start as root and does not work under Docker's `user:` mode.
:::warning
DMA-BUF must be available on the host kernel. On Debian/Ubuntu this is provided by the default kernel; on other systems it may need enabling (`CONFIG_DMABUF_HEAPS_SYSTEM`). If Frigate's log reports that no DMA heap can be allocated, check that `/dev/dma_heap/system` exists on the host and is passed into the container.
:::
#### Configuration
Finally, configure [hardware object detection](/configuration/object_detectors#axelera-metis) to complete the setup.
## Docker
Running through Docker with Docker Compose is the recommended install method.
+2 -1
View File
@@ -65,11 +65,12 @@ The SDKs for a few hardware detectors are not shipped in the Frigate image. They
| [Hailo 8 / 8L / 8R](/configuration/object_detectors#hailo) | 4.21.0 | `hailort-debian12-amd64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_x86_64.whl` on x86, `hailort-debian12-arm64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_aarch64.whl` on arm64 | [GitHub release](https://github.com/frigate-nvr/hailort/releases/tag/v4.21.0) |
| [MemryX MX3](/configuration/object_detectors#memryx-mx3) | 2.1.0 | `mx_accl_frigate-2.1.0.zip` (the release source archive, renamed) | [GitHub archive](https://github.com/memryx/mx_accl_frigate/archive/refs/tags/v2.1.0.zip) |
| [AXERA AXEngine](/configuration/object_detectors#axera) | 0.1.3 | `axengine-0.1.3-py3-none-any.whl` | [GitHub release](https://github.com/AXERA-TECH/pyaxengine/releases/tag/0.1.3-frigate) |
| [Axelera Metis](/configuration/object_detectors#axelera-metis) | 1.8.0 | `axelera_runtime-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl` on x86, `axelera_runtime-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl` on arm64, plus `axelera_firmware-1.8.0-py3-none-any.whl` on both | [Axelera Artifactory PyPI](https://software.axelera.ai/artifactory/#web/ui/redirect/pypi/axelera-pypi/simple) |
If the container cannot reach GitHub, provide the files yourself:
1. Download the files for your architecture on a machine with internet access.
2. Place them, with exactly the file names listed above, in `/config/model_cache/runtimes/<detector>/`, where `<detector>` is the detector named in your config's `devices` (`hailo`, `memryx`, or `axengine`).
2. Place them, with exactly the file names listed above, in `/config/model_cache/runtimes/<detector>/`, where `<detector>` is the detector named in your config's `devices` (`hailo`, `memryx`, `axengine`, or `axelera`).
3. Start Frigate. Files whose checksum matches are installed without any download; a file with the wrong checksum is discarded and downloaded again, so a failed startup log names the file to replace.
The `GITHUB_ENDPOINT` mirror variable below applies to these downloads as well.