mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-01 20:36:51 +03:00
* 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
395 lines
12 KiB
Python
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()
|