Files
frigate/frigate/detectors/hardware.py
T
Nick RogersandGitHub c959df32c9 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
2026-09-24 15:37:32 -06:00

395 lines
12 KiB
Python

"""Discovery of object detection hardware attached to the system.
Every probe here is a filesystem read. Nothing shells out, initializes a
runtime, or opens a device, so this is cheap enough to run from the API process
while detector children hold the hardware.
Hardware is reported whether or not this image ships a detector that can drive
it. Matching hardware to an image is a separate concern.
"""
import logging
import os
from glob import glob
from pydantic import BaseModel, Field
from frigate.const import SUPPORTED_RK_SOCS
from frigate.detectors.detector_types import config_types
from frigate.util.services import enumerate_drm_devices
logger = logging.getLogger(__name__)
# roots the probes read from, so tests can point them at a fixture tree
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")}
INTEL_DRM_DRIVERS = ("i915", "xe")
AMD_DRM_DRIVERS = ("amdgpu",)
class HardwareUnit(BaseModel):
"""One physical piece of hardware."""
device: str = Field(
title="Device string",
description="The value to put in a model's devices list, for example 'edgetpu:pci:1'.",
)
label: str = Field(
title="Unit label",
description="How to identify this unit among others of the same kind, for example 'PCIe 1'.",
)
class DetectionHardware(BaseModel):
"""A kind of detection hardware, and every unit of it that was found."""
key: str = Field(
title="Hardware key",
description="Stable identifier for this kind of hardware.",
)
detector: str = Field(
title="Detector type",
description="The detector that drives this hardware.",
)
name: str = Field(
title="Hardware name",
description="Human readable name for this kind of hardware.",
)
units: list[HardwareUnit] = Field(
title="Units",
description="Each physical piece of this hardware that was found.",
)
count: int = Field(
title="Unit count",
description="How many units were found.",
)
unlimited: bool = Field(
title="Unlimited detectors",
description="Whether this hardware can run more inference processes than there are units.",
)
def _read(path: str) -> str | None:
"""Read a small file, returning None if it cannot be read."""
try:
with open(path) as f:
return f.read().strip()
except OSError:
return None
def _is_shareable(detector: str) -> bool:
"""Whether a detector lets the same device run more than one process."""
config_class = config_types.get(detector)
# a detector missing from this image is assumed to behave like most of them
return config_class.shareable if config_class else True
def _hardware(
key: str, detector: str, name: str, units: list[HardwareUnit]
) -> DetectionHardware:
return DetectionHardware(
key=key,
detector=detector,
name=name,
units=units,
count=len(units),
unlimited=_is_shareable(detector),
)
def detect_coral_pci() -> DetectionHardware | None:
"""Find PCIe and M.2 Coral accelerators, which register as apex devices."""
names = sorted(
os.path.basename(path) for path in glob(f"{SYS_ROOT}/class/apex/apex_*")
)
if not names:
return None
units = [
HardwareUnit(device=f"edgetpu:pci:{index}", label=f"PCIe {index}")
for index in range(len(names))
]
return _hardware("edgetpu:pci", "edgetpu", "Coral EdgeTPU (PCIe)", units)
def detect_coral_usb() -> DetectionHardware | None:
"""Find USB Coral accelerators by their USB vendor and product ids."""
found = 0
for device_dir in sorted(glob(f"{SYS_ROOT}/bus/usb/devices/*")):
vendor = _read(os.path.join(device_dir, "idVendor"))
product = _read(os.path.join(device_dir, "idProduct"))
if vendor and product and (vendor.lower(), product.lower()) in CORAL_USB_IDS:
found += 1
if not found:
return None
units = [
HardwareUnit(device=f"edgetpu:usb:{index}", label=f"USB {index}")
for index in range(found)
]
return _hardware("edgetpu:usb", "edgetpu", "Coral EdgeTPU (USB)", units)
def _drm_devices(drivers: tuple[str, ...]) -> list[str]:
"""PCI addresses of DRM devices bound to one of the given drivers."""
return sorted(
pdev for pdev, driver in enumerate_drm_devices().items() if driver in drivers
)
def detect_intel_gpu() -> DetectionHardware | None:
"""Find Intel GPUs through their DRM driver."""
pdevs = _drm_devices(INTEL_DRM_DRIVERS)
if not pdevs:
return None
# OpenVINO reports a lone GPU as "GPU" and enumerates them as GPU.0, GPU.1
# only when there is more than one
if len(pdevs) == 1:
units = [HardwareUnit(device="openvino:GPU", label=pdevs[0])]
else:
units = [
HardwareUnit(device=f"openvino:GPU.{index}", label=pdev)
for index, pdev in enumerate(pdevs)
]
return _hardware("openvino:GPU", "openvino", "Intel GPU", units)
def detect_intel_npu() -> DetectionHardware | None:
"""Find Intel NPUs, which register as accel devices bound to intel_vpu."""
units = []
for accel_path in sorted(glob(f"{SYS_ROOT}/class/accel/accel*")):
try:
driver = os.path.basename(os.readlink(f"{accel_path}/device/driver"))
except OSError:
continue
if driver != "intel_vpu":
continue
units.append(
HardwareUnit(device="openvino:NPU", label=os.path.basename(accel_path))
)
if not units:
return None
# OpenVINO has no way to address a specific NPU, so only the first is usable
return _hardware("openvino:NPU", "openvino", "Intel NPU", units[:1])
def detect_amd_gpu() -> DetectionHardware | None:
"""Find AMD GPUs through their DRM driver."""
pdevs = _drm_devices(AMD_DRM_DRIVERS)
if not pdevs:
return None
# ROCm runs through onnx, whose MIGraphX provider takes no device index, so
# only one is addressable
units = [HardwareUnit(device="onnx", label=pdevs[0])]
return _hardware("onnx:amd", "onnx", "AMD GPU", units)
def detect_nvidia_gpu() -> DetectionHardware | None:
"""Find discrete Nvidia GPUs through the nvidia driver's proc entries."""
units = []
for index, gpu_dir in enumerate(sorted(glob(f"{PROC_ROOT}/driver/nvidia/gpus/*"))):
information = _read(os.path.join(gpu_dir, "information")) or ""
name = f"GPU {index}"
for line in information.splitlines():
if line.startswith("Model:"):
name = line.split(":", 1)[1].strip()
break
units.append(HardwareUnit(device=f"onnx:{index}", label=name))
if not units:
return None
# the model name is more useful as the hardware name when there is only one
name = units[0].label if len(units) == 1 else "NVIDIA GPU"
return _hardware("onnx:nvidia", "onnx", name, units)
def detect_jetson() -> DetectionHardware | None:
"""Find an Nvidia Jetson, whose integrated GPU runs through tensorrt."""
is_jetson = os.path.isfile(f"{ETC_ROOT}/nv_tegra_release") or os.path.exists(
f"{SYS_ROOT}/devices/gpu.0/load"
)
if not is_jetson:
return None
units = [HardwareUnit(device="tensorrt:0", label="Integrated GPU")]
return _hardware("tensorrt", "tensorrt", "NVIDIA Jetson", units)
def _dev_units(pattern: str, device: str, label: str) -> list[HardwareUnit]:
"""Build units from device nodes matching a glob."""
return [
HardwareUnit(device=device.format(index=index), label=f"{label} {index}")
for index in range(len(glob(f"{DEV_ROOT}/{pattern}")))
]
def detect_hailo() -> DetectionHardware | None:
"""Find Hailo accelerators by their device nodes."""
nodes = sorted(glob(f"{DEV_ROOT}/hailo*"))
if not nodes:
return None
# the hailo runtime schedules across every attached device itself, so there
# is nothing to address individually
units = [HardwareUnit(device="hailo:PCIe", label=os.path.basename(nodes[0]))]
return _hardware("hailo", "hailo", "Hailo", units)
def detect_memryx() -> DetectionHardware | None:
"""Find MemryX accelerators by their device nodes."""
units = _dev_units("memx*", "memryx:PCIe:{index}", "PCIe")
if not units:
return None
return _hardware("memryx", "memryx", "MemryX MX3", units)
def detect_deepx() -> DetectionHardware | None:
"""Find DEEPX NPUs by their device nodes."""
units = _dev_units("dxrt*", "deepx:PCIe:{index}", "PCIe")
if not units:
return None
return _hardware("deepx", "deepx", "DEEPX NPU", units)
def detect_rockchip() -> DetectionHardware | None:
"""Find a Rockchip NPU by reading the SoC from the device tree."""
compatible = _read(f"{PROC_ROOT}/device-tree/compatible")
if not compatible:
return None
soc = compatible.split(",")[-1].strip("\x00")
if soc not in SUPPORTED_RK_SOCS:
return None
units = [HardwareUnit(device="rknn", label=soc.upper())]
return _hardware("rknn", "rknn", f"Rockchip NPU ({soc.upper()})", units)
def detect_axengine() -> DetectionHardware | None:
"""Find an AXERA accelerator by its control device node."""
if not os.path.exists(f"{DEV_ROOT}/axcl_host"):
return None
units = [HardwareUnit(device="axengine", label="AXERA")]
return _hardware("axengine", "axengine", "AXERA NPU", units)
def detect_synaptics() -> DetectionHardware | None:
"""Find a Synaptics NPU by its device node."""
if not os.path.exists(f"{DEV_ROOT}/synap"):
return None
units = [HardwareUnit(device="synaptics", label="Synaptics")]
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")]
return _hardware("cpu", "cpu", "CPU", units)
# ordered so accelerators are offered ahead of the CPU fallback
PROBES = (
detect_coral_pci,
detect_coral_usb,
detect_hailo,
detect_memryx,
detect_deepx,
detect_intel_npu,
detect_intel_gpu,
detect_nvidia_gpu,
detect_jetson,
detect_amd_gpu,
detect_rockchip,
detect_axengine,
detect_synaptics,
detect_lighter_ane,
detect_cpu,
)
class HardwareProber:
"""Probes for detection hardware, caching the result for the process."""
_hardware: list[DetectionHardware] | None = None
def probe(self, refresh: bool = False) -> list[DetectionHardware]:
"""Get the detection hardware attached to this system.
Args:
refresh: Probe again instead of using the cached result
Returns:
Every kind of detection hardware that was found
"""
if self._hardware is not None and not refresh:
return self._hardware
found = []
for probe in PROBES:
try:
hardware = probe()
except Exception:
logger.warning("Failed to probe for %s", probe.__name__, exc_info=True)
continue
if hardware is not None:
found.append(hardware)
logger.debug("Detected hardware: %s", [h.key for h in found])
self._hardware = found
return found
hardware_prober = HardwareProber()