Apple Silicon Macs via lighter: ONNX detector on the Neural Engine and media engine decode (#24453)

* Run ONNX models on a Mac's Neural Engine through lighter's plugin provider

lighter's lighter.sh/ane device places an ONNX Runtime plugin execution
provider in the container. When it is present, the ONNX session setup
registers it once and opens sessions on its Neural Engine device, the same
place CUDA, ROCm and OpenVINO are chosen, so the onnx detector (and any
model that is not pinned to the CPU) runs there with no configuration. The
hardware probe reports it as an onnx unit.

* docs: hardware decode on an Apple Silicon Mac under lighter

A community section on the video decoding page: lighter's lighter.sh/video
device, hwaccel_args -c:v h264_v4l2m2m, and why the Raspberry Pi presets
decode a single-stream camera in software. The detector docs link to it.

* docs: set the lighter decoder per camera when codecs are mixed

* docs: the ONNX detector on a Mac's Neural Engine under lighter

* Format the Neural Engine provider setup

* Fall back to the default providers when the Neural Engine cannot load a model

* Apple Silicon ffmpeg presets for lighter's media engine, recommended when it is present
This commit is contained in:
Nick Rogers
2026-09-24 15:37:32 -06:00
committed by GitHub
parent c5889ef35f
commit c959df32c9
16 changed files with 413 additions and 16 deletions
+13 -11
View File
@@ -17,17 +17,19 @@ Hardware acceleration arguments tell FFmpeg to decode your camera's video stream
See [the hardware acceleration docs](/configuration/hardware_acceleration_video.md) for details on setting up hardware acceleration for your GPU / iGPU, then select the preset that matches your hardware.
| Preset (YAML config) | UI Label | Usage | Notes |
| --------------------- | ----------------------- | --------------------------------- | --------------------------------------------------------------- |
| preset-rpi-64-h264 | Raspberry Pi (H.264) | 64-bit Raspberry Pi, H.264 stream | |
| preset-rpi-64-h265 | Raspberry Pi (H.265) | 64-bit Raspberry Pi, H.265 stream | |
| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected |
| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
| preset-nvidia | NVIDIA GPU | NVIDIA GPU | |
| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | |
| preset-jetson-h265 | NVIDIA Jetson (H.265) | NVIDIA Jetson, H.265 stream | |
| preset-rkmpp | Rockchip RKMPP | Rockchip MPP | Use an image with the `-rk` suffix and run in privileged mode |
| Preset (YAML config) | UI Label | Usage | Notes |
| ------------------------- | ----------------------- | --------------------------------------------- | --------------------------------------------------------------- |
| preset-rpi-64-h264 | Raspberry Pi (H.264) | 64-bit Raspberry Pi, H.264 stream | |
| preset-rpi-64-h265 | Raspberry Pi (H.265) | 64-bit Raspberry Pi, H.265 stream | |
| preset-apple-silicon-h264 | Apple Silicon (H.264) | Apple Silicon Mac under lighter, H.264 stream | Needs the `lighter.sh/video` device |
| preset-apple-silicon-h265 | Apple Silicon (H.265) | Apple Silicon Mac under lighter, H.265 stream | Needs the `lighter.sh/video` device |
| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected |
| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
| preset-nvidia | NVIDIA GPU | NVIDIA GPU | |
| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | |
| preset-jetson-h265 | NVIDIA Jetson (H.265) | NVIDIA Jetson, H.265 stream | |
| preset-rkmpp | Rockchip RKMPP | Rockchip MPP | Use an image with the `-rk` suffix and run in privileged mode |
<ConfigTabs>
<TabItem value="ui">
@@ -43,6 +43,10 @@ Frigate supports presets for optimal hardware accelerated video decoding:
- [RKNN](#rockchip-platform): Frigate can utilize the media engine in RockChip SOCs to accelerate video decoding.
**Apple Silicon Mac** <CommunityBadge />
- [lighter](#apple-silicon-mac-lighter): Frigate can utilize the media engine in Apple Silicon Macs to accelerate video decoding, when running under the lighter container runtime.
**Other Hardware**
Depending on your system, these presets may not be compatible, and you may need to use manual hwaccel args to take advantage of your hardware. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro
@@ -533,3 +537,35 @@ output_args:
Make sure that your SoC supports hardware acceleration for your input stream and your input stream is h264 encoding. For example, if your camera streams with h264 encoding, your SoC must be able to de- and encode with it. If you are unsure whether your SoC meets the requirements, take a look at the datasheet.
:::
## Apple Silicon Mac (lighter)
[lighter](https://github.com/fieldwork-ai/lighter) is an open-source container runtime for macOS. It gives a container the Mac's media engine as a standard V4L2 decoder, backed by VideoToolbox, so Frigate decodes H.264 and H.265 streams in hardware with the ffmpeg it already ships. It works on M1 and newer Macs with lighter 0.9.2 or newer.
Give the container the video device. With Docker Compose:
```yaml {4-5}
services:
frigate:
...
devices:
- lighter.sh/video=all
```
Or with `docker run`, add `--device lighter.sh/video=all`.
Then set the preset for the codec your cameras stream. The decoder is specific to the codec, so if your cameras mix H.264 and H.265, set the preset for the most common codec globally and override it on the other cameras:
```yaml
ffmpeg:
hwaccel_args: preset-apple-silicon-h264
cameras:
garage: # an H.265 camera
ffmpeg:
hwaccel_args: preset-apple-silicon-h265
```
The presets decode on the media engine and encode the Birdseye restream and timelapses there too. Scaling to the detect resolution runs on the CPU, as ffmpeg's V4L2 decoders cannot scale.
lighter can also run object detection on the Mac's Neural Engine; see [Apple Neural Engine (lighter)](object_detectors.md#apple-neural-engine-lighter).
+21 -1
View File
@@ -34,6 +34,7 @@ Frigate supports multiple different detectors that work on different types of ha
**Apple Silicon**
- [Apple Silicon](#apple-silicon-detector): Apple Silicon can run on M1 and newer Apple Silicon devices.
- <CommunityBadge /> [ONNX](#apple-neural-engine-lighter): the ONNX detector runs on the Neural Engine of M1 and newer Macs when Frigate runs under the lighter container runtime.
**Intel**
@@ -484,7 +485,7 @@ See [ONNX supported models](#onnx) for supported models, there are some caveats:
## ONNX
ONNX is an open format for building machine learning models, Frigate supports running ONNX models on CPU, OpenVINO, ROCm, and TensorRT. On startup Frigate will automatically try to use a GPU if one is available.
ONNX is an open format for building machine learning models, Frigate supports running ONNX models on CPU, OpenVINO, ROCm, TensorRT, and a Mac's Neural Engine. On startup Frigate will automatically try to use a GPU if one is available.
:::info
@@ -500,6 +501,9 @@ If the correct build is used for your GPU then the GPU will be detected and used
- Nvidia GPUs will automatically be detected and used with the ONNX detector in the `-tensorrt` Frigate image.
- Jetson devices will automatically be detected and used with the ONNX detector in the `-tensorrt-jp6` Frigate image.
- **Apple Silicon Mac** <CommunityBadge />
- The Neural Engine will automatically be detected and used with the ONNX detector when Frigate runs under lighter with its Neural Engine device. See [Apple Neural Engine (lighter)](#apple-neural-engine-lighter).
:::
:::tip
@@ -515,6 +519,22 @@ models:
:::
### Apple Neural Engine (lighter) {#apple-neural-engine-lighter}
[lighter](https://github.com/fieldwork-ai/lighter) is an open-source container runtime for macOS. A container started with its `lighter.sh/ane` device gets an ONNX Runtime execution provider that runs models on the Mac's Neural Engine, and the ONNX detector uses it automatically, with the same models and configuration as on any other hardware. It works on M1 and newer Macs with lighter 0.9.2 or newer.
Give the Frigate container the Neural Engine device. With Docker Compose:
```yaml
services:
frigate:
image: ghcr.io/blakeblackshear/frigate:stable-standard-arm64
devices:
- lighter.sh/ane=all
```
Or with `docker run`, add `--device lighter.sh/ane=all`. Frigate then reports the Neural Engine under **Settings > System > Detection models**, and the ONNX detector's model loads on it. lighter can also decode camera streams on the Mac's media engine; see [Video Decoding](hardware_acceleration_video.md#apple-silicon-mac-lighter).
### Configuration {#configuration-onnx}
<ModelConfigDropdown detectorTitle="ONNX" models={objectDetectorsModels.onnx.models} />
+11 -1
View File
@@ -78,6 +78,10 @@ Frigate supports multiple different detectors that work on different types of ha
**Apple Silicon**
- [ONNX via lighter](#apple-silicon): The ONNX detector runs on the Neural Engine of M1 and newer Macs when Frigate runs in the lighter container runtime
- [Supports the same model architectures as the ONNX detector](../../configuration/object_detectors#apple-neural-engine-lighter)
- Runs inside the Frigate container, with no separate detector process to set up
- The recommended way to run Frigate on a Mac
- [Apple Silicon](#apple-silicon): Apple Silicon is usable on all M1 and newer Apple Silicon devices to provide efficient and fast object detection
- [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-detector)
- Runs well with any size models including large
@@ -211,7 +215,13 @@ Inference is done with the `onnx` detector type. Speeds will vary greatly depend
### Apple Silicon
With the [Apple Silicon](../configuration/object_detectors.md#apple-silicon-detector) detector Frigate can take advantage of the NPU in M1 and newer Apple Silicon.
Frigate on a Mac is best run in the [lighter](https://github.com/fieldwork-ai/lighter) container runtime, where the [ONNX detector](../configuration/object_detectors.md#apple-neural-engine-lighter) runs on the Neural Engine of M1 and newer Macs from inside the Frigate container. There is no separate detector process to install or keep running, and the same container can decode video on the Mac's media engine.
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time |
| ---- | -------------------------------------- | ----------------------- | ---------------------- |
| M1 | t-320: 3.3 ms s-320: 7 ms s-640: 13 ms | 320: 6.6 ms | Nano-320: 38 ms |
Alternatively, with the [Apple Silicon](../configuration/object_detectors.md#apple-silicon-detector) detector Frigate can take advantage of the NPU in M1 and newer Apple Silicon.
:::warning
+2
View File
@@ -429,6 +429,8 @@ def ffmpeg_presets():
hwaccel_presets = [
"preset-rpi-64-h264",
"preset-rpi-64-h265",
"preset-apple-silicon-h264",
"preset-apple-silicon-h265",
"preset-jetson-h264",
"preset-jetson-h265",
"preset-rkmpp",
+51
View File
@@ -46,8 +46,42 @@ _PROVIDER_LABELS = {
"MIGraphXExecutionProvider": "MIGraphX",
"OpenVINOExecutionProvider": "OpenVINO",
"CPUExecutionProvider": "CPU",
"LighterANE": "Neural Engine",
}
# lighter (https://github.com/fieldwork-ai/lighter) places an ONNX Runtime plugin
# execution provider in a container started with --device lighter.sh/ane=all,
# which runs models on a Mac's Neural Engine; LIGHTER_ANE_EP names where it is
LIGHTER_ANE_EP_NAME = "LighterANE"
LIGHTER_ANE_LIBRARY = "/usr/lib/lighter/liblighter_ane_ep.so"
def get_lighter_ane_devices() -> list[Any]:
"""Get the Neural Engine devices lighter's provider offers, registering it once.
Returns:
The provider's ONNX Runtime devices, or an empty list without lighter's device
"""
library = os.environ.get("LIGHTER_ANE_EP", LIGHTER_ANE_LIBRARY)
if not os.path.exists(library):
return []
devices = [d for d in ort.get_ep_devices() if d.ep_name == LIGHTER_ANE_EP_NAME]
if not devices:
try:
ort.register_execution_provider_library(LIGHTER_ANE_EP_NAME, library)
except Exception as e:
logger.warning(
f"Failed to load the Neural Engine provider from {library}: {e}"
)
return []
devices = [d for d in ort.get_ep_devices() if d.ep_name == LIGHTER_ANE_EP_NAME]
return devices
def is_arm64_platform() -> bool:
"""Check if we're running on an ARM platform."""
@@ -689,6 +723,23 @@ def get_optimized_runner(
if rknn_path:
return _record_runner(model_path, model_type, RKNNModelRunner(rknn_path))
if device != "CPU" and (ane_devices := get_lighter_ane_devices()):
sess_options = get_ort_session_options(model_type) or ort.SessionOptions()
sess_options.add_provider_for_devices(ane_devices, {})
try:
session = ort.InferenceSession(model_path, sess_options=sess_options)
except Exception as e:
logger.warning(
f"Failed to load {model_path} on the Neural Engine, using the default providers: {e}"
)
else:
return _record_runner(
model_path,
model_type,
ONNXModelRunner(session, model_type=model_type),
)
providers, options = get_ort_providers(device == "CPU", device, **kwargs)
if providers[0] == "CPUExecutionProvider":
+15
View File
@@ -25,6 +25,7 @@ SYS_ROOT = "/sys"
DEV_ROOT = "/dev"
PROC_ROOT = "/proc"
ETC_ROOT = "/etc"
LIB_ROOT = "/usr/lib"
# a Coral reports as Global Unichip until its firmware is loaded, then as Google
CORAL_USB_IDS = {("1a6e", "089a"), ("18d1", "9302")}
@@ -317,6 +318,19 @@ def detect_synaptics() -> DetectionHardware | None:
return _hardware("synaptics", "synaptics", "Synaptics NPU", units)
def detect_lighter_ane() -> DetectionHardware | None:
"""Find a Mac's Neural Engine by the provider library lighter's device places."""
library = os.environ.get(
"LIGHTER_ANE_EP", f"{LIB_ROOT}/lighter/liblighter_ane_ep.so"
)
if not os.path.exists(library):
return None
# runs through onnx, whose session picks lighter's provider when it is present
units = [HardwareUnit(device="onnx", label="Neural Engine")]
return _hardware("onnx:lighter", "onnx", "Apple Neural Engine", units)
def detect_cpu() -> DetectionHardware:
"""The CPU, which is always available."""
units = [HardwareUnit(device="cpu", label="CPU")]
@@ -338,6 +352,7 @@ PROBES = (
detect_rockchip,
detect_axengine,
detect_synaptics,
detect_lighter_ane,
detect_cpu,
)
+9
View File
@@ -84,6 +84,8 @@ _user_agent_args = [
PRESETS_HW_ACCEL_DECODE = {
"preset-rpi-64-h264": "-c:v:1 h264_v4l2m2m",
"preset-rpi-64-h265": "-c:v:1 hevc_v4l2m2m",
"preset-apple-silicon-h264": "-c:v h264_v4l2m2m",
"preset-apple-silicon-h265": "-c:v hevc_v4l2m2m",
FFMPEG_HWACCEL_VAAPI: "-hwaccel_flags allow_profile_mismatch -hwaccel vaapi -hwaccel_device {3} -hwaccel_output_format vaapi",
"preset-intel-qsv-h264": f"-hwaccel qsv -qsv_device {{3}} -hwaccel_output_format qsv -c:v h264_qsv{' -bsf:v dump_extra' if LIBAVFORMAT_VERSION_MAJOR >= 61 else ''}", # https://trac.ffmpeg.org/ticket/9766#comment:17
"preset-intel-qsv-h265": f"-load_plugin hevc_hw -hwaccel qsv -qsv_device {{3}} -hwaccel_output_format qsv{' -bsf:v dump_extra' if LIBAVFORMAT_VERSION_MAJOR >= 61 else ''}", # https://trac.ffmpeg.org/ticket/9766#comment:17
@@ -120,6 +122,9 @@ PRESETS_HW_ACCEL_DECODE["preset-rk-h265"] = PRESETS_HW_ACCEL_DECODE[
PRESETS_HW_ACCEL_SCALE = {
"preset-rpi-64-h264": "-r {0} -vf fps={0},scale={1}:{2}",
"preset-rpi-64-h265": "-r {0} -vf fps={0},scale={1}:{2}",
# ffmpeg's v4l2m2m decoders cannot scale, so frames are scaled on the CPU
"preset-apple-silicon-h264": "-r {0} -vf fps={0},scale={1}:{2}",
"preset-apple-silicon-h265": "-r {0} -vf fps={0},scale={1}:{2}",
FFMPEG_HWACCEL_VAAPI: "-r {0} -vf fps={0},scale_vaapi=w={1}:h={2},hwdownload,format=nv12",
"preset-intel-qsv-h264": "-r {0} -vf vpp_qsv=w={1}:h={2}:format=nv12,hwdownload,format=nv12,fps={0},format=yuv420p",
"preset-intel-qsv-h265": "-r {0} -vf vpp_qsv=w={1}:h={2}:format=nv12,hwdownload,format=nv12,fps={0},format=yuv420p",
@@ -150,6 +155,8 @@ PRESETS_HW_ACCEL_SCALE["preset-rk-h265"] = PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
"preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}",
"preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m {2}",
"preset-apple-silicon-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}",
"preset-apple-silicon-h265": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}",
# -vaapi_device is required in addition to -hwaccel_device: this is the only
# birdseye preset that uses hwupload, and ffmpeg 8 initializes filters before
# the decoder creates a device, so hwupload cannot see an -hwaccel_device one.
@@ -184,6 +191,8 @@ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-rk-h264"] = PRESETS_HW_ACCEL_ENCODE_BIR
PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = {
"preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m -pix_fmt yuv420p {2}",
"preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m -pix_fmt yuv420p {2}",
"preset-apple-silicon-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m -pix_fmt yuv420p {2}",
"preset-apple-silicon-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m -pix_fmt yuv420p {2}",
FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi {2}",
"preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {2}",
"preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v hevc_qsv -profile:v main -level:v 4.1 -async_depth:v 1 {2}",
+24 -1
View File
@@ -25,7 +25,7 @@ class HardwareProbeTestCase(unittest.TestCase):
self.root = tempfile.TemporaryDirectory()
self.addCleanup(self.root.cleanup)
for name in ("SYS_ROOT", "DEV_ROOT", "PROC_ROOT", "ETC_ROOT"):
for name in ("SYS_ROOT", "DEV_ROOT", "PROC_ROOT", "ETC_ROOT", "LIB_ROOT"):
sub = os.path.join(self.root.name, name.split("_")[0].lower())
os.makedirs(sub, exist_ok=True)
patcher = patch.object(hardware, name, sub)
@@ -167,6 +167,29 @@ class TestNvidia(HardwareProbeTestCase):
class TestAccelerators(HardwareProbeTestCase):
def test_the_neural_engine_is_found_by_lighters_provider_library(self):
write(os.path.join(self.lib_root, "lighter", "liblighter_ane_ep.so"))
with patch.dict(os.environ, clear=False) as env:
env.pop("LIGHTER_ANE_EP", None)
ane = self.probe()["onnx:lighter"]
self.assertEqual(ane.detector, "onnx")
self.assertEqual(ane.units[0].device, "onnx")
self.assertEqual(ane.units[0].label, "Neural Engine")
def test_the_neural_engine_is_found_where_lighter_ane_ep_points(self):
library = os.path.join(self.root.name, "elsewhere", "liblighter_ane_ep.so")
write(library)
with patch.dict(os.environ, {"LIGHTER_ANE_EP": library}):
self.assertIn("onnx:lighter", self.probe())
def test_no_neural_engine_is_reported_without_the_library(self):
with patch.dict(os.environ, clear=False) as env:
env.pop("LIGHTER_ANE_EP", None)
self.assertNotIn("onnx:lighter", self.probe())
def test_hailo_is_found_by_its_device_node(self):
write(os.path.join(self.dev_root, "hailo0"))
+10
View File
@@ -51,6 +51,16 @@ class TestFfmpegPresets(unittest.TestCase):
" ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"])
)
def test_ffmpeg_hwaccel_apple_preset_decodes_every_stream(self):
self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = (
"preset-apple-silicon-h265"
)
frigate_config = FrigateConfig(**self.default_ffmpeg)
cmd = " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"])
assert "-c:v hevc_v4l2m2m" in cmd
assert "-c:v:1" not in cmd
assert "scale=1920:1080" in cmd
def test_ffmpeg_hwaccel_not_preset(self):
self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = (
"-other-hwaccel args"
@@ -34,6 +34,12 @@ class HwaccelRecommendationTestCase(unittest.TestCase):
patcher.start()
self.addCleanup(patcher.stop)
self.sys_root = os.path.join(self.root.name, "sys")
os.makedirs(self.sys_root)
patcher = patch.object(hwaccel, "SYS_ROOT", self.sys_root)
patcher.start()
self.addCleanup(patcher.stop)
drm = patch.object(hwaccel, "enumerate_drm_devices", return_value={})
self.drm = drm.start()
self.addCleanup(drm.stop)
@@ -66,6 +72,12 @@ class HwaccelRecommendationTestCase(unittest.TestCase):
with open(os.path.join(self.proc_root, "cpuinfo"), "w") as f:
f.write(f"processor\t: 0\nmodel name\t: {model_name}\n")
def write_video_device(self, vendor: str) -> None:
device = os.path.join(self.sys_root, "class", "video4linux", "video0", "device")
os.makedirs(device)
with open(os.path.join(device, "vendor"), "w") as f:
f.write(f"{vendor}\n")
def write_device_tree(self) -> None:
os.makedirs(os.path.join(self.proc_root, "device-tree"), exist_ok=True)
with open(os.path.join(self.proc_root, "device-tree", "compatible"), "w") as f:
@@ -191,6 +203,22 @@ class TestAvailableFamilies(HwaccelRecommendationTestCase):
self.assertIn(recommended, [family.key for family in families])
class TestLighter(HwaccelRecommendationTestCase):
def test_lighters_media_engine_is_recommended(self):
self.write_video_device(hwaccel.LIGHTER_VIRTIO_VENDOR)
self.assertEqual(self.recommend(codecs={"h264"}), "apple-silicon")
self.assertEqual(
self.presets()["apple-silicon"],
{"h264": "preset-apple-silicon-h264", "h265": "preset-apple-silicon-h265"},
)
def test_another_virtio_media_device_is_not_lighters(self):
self.write_video_device("0x554d4551")
self.assertEqual(self.available(), [])
class TestCodecCoverage(HwaccelRecommendationTestCase):
def test_a_family_carries_a_preset_per_codec(self):
self.assertEqual(
+147
View File
@@ -1,5 +1,7 @@
"""Tests for the device each model runner reports after loading."""
import os
import tempfile
import threading
import unittest
from unittest.mock import MagicMock, patch
@@ -38,6 +40,7 @@ class TestRunnerDeviceName(unittest.TestCase):
self._onnx(["OpenVINOExecutionProvider"]).device_name, "OpenVINO"
)
self.assertEqual(self._onnx(["CPUExecutionProvider"]).device_name, "CPU")
self.assertEqual(self._onnx(["LighterANE"]).device_name, "Neural Engine")
self.assertEqual(self._onnx(["ROCMExecutionProvider"]).device_name, "ROCM")
self.assertEqual(self._onnx([]).device_name, "CPU")
@@ -129,3 +132,147 @@ class TestLoadedDeviceSnapshot(unittest.TestCase):
finally:
stop.set()
thread.join(timeout=5)
class TestLighterANE(unittest.TestCase):
"""lighter's plugin provider, picked up by the ONNX session setup."""
def setUp(self):
loaded_devices.clear()
self.root = tempfile.TemporaryDirectory()
self.addCleanup(self.root.cleanup)
self.library = os.path.join(self.root.name, "liblighter_ane_ep.so")
env = patch.dict(os.environ, {"LIGHTER_ANE_EP": self.library})
env.start()
self.addCleanup(env.stop)
def _device(self) -> MagicMock:
device = MagicMock()
device.ep_name = "LighterANE"
return device
def test_no_devices_without_the_library(self):
with patch.object(detection_runners.ort, "get_ep_devices") as get_ep_devices:
self.assertEqual(detection_runners.get_lighter_ane_devices(), [])
get_ep_devices.assert_not_called()
def test_the_provider_is_registered_once(self):
open(self.library, "w").close()
device = self._device()
other = MagicMock()
other.ep_name = "CPUExecutionProvider"
with (
patch.object(
detection_runners.ort,
"get_ep_devices",
side_effect=[[other], [other, device], [other, device]],
),
patch.object(
detection_runners.ort, "register_execution_provider_library"
) as register,
):
self.assertEqual(detection_runners.get_lighter_ane_devices(), [device])
self.assertEqual(detection_runners.get_lighter_ane_devices(), [device])
register.assert_called_once_with("LighterANE", self.library)
def test_a_provider_that_will_not_load_is_skipped(self):
open(self.library, "w").close()
with (
patch.object(detection_runners.ort, "get_ep_devices", return_value=[]),
patch.object(
detection_runners.ort,
"register_execution_provider_library",
side_effect=RuntimeError("not a provider"),
),
):
self.assertEqual(detection_runners.get_lighter_ane_devices(), [])
def test_the_session_runs_on_the_neural_engine(self):
device = self._device()
session = MagicMock()
session.get_providers.return_value = ["LighterANE", "CPUExecutionProvider"]
options = MagicMock()
with (
patch.object(detection_runners, "is_rknn_compatible", return_value=False),
patch.object(
detection_runners, "get_lighter_ane_devices", return_value=[device]
),
patch.object(
detection_runners, "get_ort_session_options", return_value=options
),
patch.object(
detection_runners.ort, "InferenceSession", return_value=session
) as inference_session,
):
runner = get_optimized_runner("/models/yolo.onnx", "AUTO", "yolo-generic")
options.add_provider_for_devices.assert_called_once_with([device], {})
inference_session.assert_called_once_with(
"/models/yolo.onnx", sess_options=options
)
self.assertIsInstance(runner, ONNXModelRunner)
self.assertEqual(
loaded_devices["/models/yolo.onnx"], ("yolo-generic", "Neural Engine")
)
def test_a_model_the_neural_engine_cannot_load_uses_the_default_providers(self):
session = MagicMock()
session.get_providers.return_value = ["CPUExecutionProvider"]
with (
patch.object(detection_runners, "is_rknn_compatible", return_value=False),
patch.object(
detection_runners, "get_lighter_ane_devices", return_value=[MagicMock()]
),
patch.object(
detection_runners,
"get_ort_providers",
return_value=(["CPUExecutionProvider"], [{}]),
),
patch.object(
detection_runners, "is_openvino_gpu_npu_available", return_value=False
),
patch.object(
detection_runners.ort,
"InferenceSession",
side_effect=[RuntimeError("unsupported"), session],
) as inference_session,
patch.object(
detection_runners, "get_ort_session_options", return_value=MagicMock()
),
self.assertLogs(detection_runners.logger, level="WARNING"),
):
runner = get_optimized_runner("/models/jina.onnx", "AUTO", "jina-v2")
self.assertEqual(inference_session.call_count, 2)
self.assertIsInstance(runner, ONNXModelRunner)
self.assertEqual(loaded_devices["/models/jina.onnx"], ("jina-v2", "CPU"))
def test_a_cpu_model_stays_on_the_cpu(self):
session = MagicMock()
session.get_providers.return_value = ["CPUExecutionProvider"]
with (
patch.object(detection_runners, "is_rknn_compatible", return_value=False),
patch.object(
detection_runners, "get_lighter_ane_devices", return_value=[MagicMock()]
) as ane,
patch.object(
detection_runners,
"get_ort_providers",
return_value=(["CPUExecutionProvider"], [{}]),
),
patch.object(
detection_runners.ort, "InferenceSession", return_value=session
),
patch.object(
detection_runners, "get_ort_session_options", return_value=None
),
):
get_optimized_runner("/models/arcface.onnx", "CPU", "arcface")
ane.assert_not_called()
+33 -1
View File
@@ -9,6 +9,7 @@ and resolve it per camera against that camera's detect stream.
"""
import logging
import os
import re
from pydantic import BaseModel, Field
@@ -23,14 +24,20 @@ from frigate.util.services import enumerate_drm_devices
logger = logging.getLogger(__name__)
# root the /proc reads use, so tests can point them at a fixture tree
# roots the /proc and /sys reads use, so tests can point them at a fixture tree
PROC_ROOT = "/proc"
SYS_ROOT = "/sys"
ANY_CODEC = "any"
# a Raspberry Pi has no detection hardware of its own, so it gets a key here
RASPBERRY_PI = "raspberrypi"
# lighter (https://github.com/fieldwork-ai/lighter) decodes on a Mac's media
# engine through virtio-media devices, which carry its virtio vendor id "LGHT"
LIGHTER_MEDIA = "lighter"
LIGHTER_VIRTIO_VENDOR = "0x4c474854"
# ffprobe names h265 streams hevc
CODEC_ALIASES = {"hevc": "h265"}
@@ -52,6 +59,7 @@ DECODE_HARDWARE = (
"rknn",
"openvino:GPU",
"onnx:amd",
LIGHTER_MEDIA,
RASPBERRY_PI,
)
@@ -98,6 +106,10 @@ FAMILY_RPI = HwaccelFamily(
key="rpi",
presets={"h264": "preset-rpi-64-h264", "h265": "preset-rpi-64-h265"},
)
FAMILY_APPLE = HwaccelFamily(
key="apple-silicon",
presets={"h264": "preset-apple-silicon-h264", "h265": "preset-apple-silicon-h265"},
)
def _read(path: str) -> str | None:
@@ -139,6 +151,20 @@ def _is_raspberry_pi() -> bool:
return "raspberrypi" in compatible
def _has_lighter_media() -> bool:
video = f"{SYS_ROOT}/class/video4linux"
try:
devices = os.listdir(video)
except OSError:
return False
return any(
_read(f"{video}/{device}/device/vendor") == LIGHTER_VIRTIO_VENDOR
for device in devices
)
def _intel_families(generation: int | None) -> list[HwaccelFamily]:
"""vaapi drives every Intel GPU, qsv only those from gen8 on."""
if generation is not None and generation < INTEL_QSV_SUPPORTED_GEN:
@@ -164,6 +190,9 @@ def _families(key: str, generation: int | None) -> list[HwaccelFamily]:
if key == "onnx:amd":
return [FAMILY_VAAPI]
if key == LIGHTER_MEDIA:
return [FAMILY_APPLE]
if key == RASPBERRY_PI:
return [FAMILY_RPI]
@@ -193,6 +222,9 @@ def _decode_hardware(detector_key: str | None) -> list[str]:
"""
present = {found.key for found in hardware_prober.probe()}
if _has_lighter_media():
present.add(LIGHTER_MEDIA)
if _is_raspberry_pi():
present.add(RASPBERRY_PI)
@@ -1567,6 +1567,8 @@
"presetLabels": {
"preset-rpi-64-h264": "Raspberry Pi (H.264)",
"preset-rpi-64-h265": "Raspberry Pi (H.265)",
"preset-apple-silicon-h264": "Apple Silicon (H.264)",
"preset-apple-silicon-h265": "Apple Silicon (H.265)",
"preset-vaapi": "VAAPI (Intel/AMD GPU)",
"preset-intel-qsv-h264": "Intel QuickSync (H.264)",
"preset-intel-qsv-h265": "Intel QuickSync (H.265)",
+1
View File
@@ -48,6 +48,7 @@
"rkmpp": "RKMPP (Rockchip)",
"jetson": "NVIDIA Jetson",
"rpi": "V4L2 (Raspberry Pi)",
"apple-silicon": "Media engine (Apple Silicon)",
"none": "None (software decoding)"
}
},
+10 -1
View File
@@ -256,7 +256,13 @@ export function detectionRows({
// ------------------------------------------------------------------ hwaccel
export type HwaccelFamilyKey =
"nvidia" | "vaapi" | "intel-qsv" | "rkmpp" | "jetson" | "rpi";
| "nvidia"
| "vaapi"
| "intel-qsv"
| "rkmpp"
| "jetson"
| "rpi"
| "apple-silicon";
export type HwaccelClass =
| { kind: "none" }
@@ -270,6 +276,7 @@ const PRESET_FAMILIES: [string, HwaccelFamilyKey][] = [
["preset-rk", "rkmpp"],
["preset-jetson", "jetson"],
["preset-rpi", "rpi"],
["preset-apple-silicon", "apple-silicon"],
];
export function hwaccelFamily(value: string | string[]): HwaccelClass {
@@ -294,6 +301,8 @@ const FAMILY_VENDORS: Record<HwaccelFamilyKey, GpuVendor[]> = {
vaapi: ["intel", "amd"],
rkmpp: ["rockchip"],
rpi: ["rpi"],
// lighter reports no decoder usage
"apple-silicon": [],
};
function decoderUsage(