From 4c57431c2da92a524f1dc680b0b7360050211b2d Mon Sep 17 00:00:00 2001 From: Filious Louis <1417132+fjlouis@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:53:59 -0500 Subject: [PATCH] Resolve Intel NPU device dynamically --- frigate/test/test_gpu_stats.py | 74 ++++++++++++++++++++++++++++------ frigate/util/services.py | 25 ++++++------ 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/frigate/test/test_gpu_stats.py b/frigate/test/test_gpu_stats.py index 160756860e..f6cf196880 100644 --- a/frigate/test/test_gpu_stats.py +++ b/frigate/test/test_gpu_stats.py @@ -24,35 +24,85 @@ class TestGpuStats(unittest.TestCase): @patch("frigate.util.services.time.sleep") @patch("frigate.util.services.time.time", side_effect=[0.0, 1.0]) + @patch( + "frigate.util.services.os.readlink", + return_value="/sys/bus/pci/drivers/intel_vpu", + ) + @patch( + "frigate.util.services.glob.glob", + return_value=["/sys/class/accel/accel0"], + ) @patch( "builtins.open", side_effect=[StringIO("1000"), StringIO("1250")], ) - def test_openvino_npu_stats_runtime_active_time(self, open_file, time, sleep): + def test_openvino_npu_stats_discovers_accel0( + self, open_file, glob, readlink, time, sleep + ): assert get_openvino_npu_stats() == {"npu": "25.0", "mem": "-%"} - assert open_file.call_args_list[0].args[0].endswith("runtime_active_time") + open_file.assert_any_call( + "/sys/class/accel/accel0/device/power/runtime_active_time" + ) @patch("frigate.util.services.time.sleep") @patch("frigate.util.services.time.time", side_effect=[0.0, 1.0]) @patch( - "builtins.open", + "frigate.util.services.os.readlink", side_effect=[ - FileNotFoundError, - StringIO("1000000"), - StringIO("1250000"), + "/sys/bus/pci/drivers/other", + "/sys/bus/pci/drivers/intel_vpu", ], ) - def test_openvino_npu_stats_busy_time_fallback(self, open_file, time, sleep): + @patch( + "frigate.util.services.glob.glob", + return_value=[ + "/sys/class/accel/accel0", + "/sys/class/accel/accel1", + ], + ) + @patch( + "builtins.open", + side_effect=[StringIO("1000"), StringIO("1250")], + ) + def test_openvino_npu_stats_skips_non_intel_accelerator( + self, open_file, glob, readlink, time, sleep + ): assert get_openvino_npu_stats() == {"npu": "25.0", "mem": "-%"} - assert open_file.call_args_list[0].args[0].endswith("runtime_active_time") - assert open_file.call_args_list[1].args[0].endswith("npu_busy_time_us") + open_file.assert_any_call( + "/sys/class/accel/accel1/device/power/runtime_active_time" + ) - @patch("builtins.open", side_effect=FileNotFoundError) - def test_openvino_npu_stats_unavailable(self, open_file): + @patch( + "frigate.util.services.os.readlink", + return_value="/sys/bus/pci/drivers/other", + ) + @patch( + "frigate.util.services.glob.glob", + return_value=["/sys/class/accel/accel0"], + ) + @patch("builtins.open") + def test_openvino_npu_stats_no_intel_accelerator(self, open_file, glob, readlink): assert get_openvino_npu_stats() is None - assert open_file.call_count == 2 + open_file.assert_not_called() + + @patch( + "frigate.util.services.os.readlink", + return_value="/sys/bus/pci/drivers/intel_vpu", + ) + @patch( + "frigate.util.services.glob.glob", + return_value=["/sys/class/accel/accel0"], + ) + @patch("builtins.open", side_effect=FileNotFoundError) + def test_openvino_npu_stats_runtime_counter_unavailable( + self, open_file, glob, readlink + ): + assert get_openvino_npu_stats() is None + open_file.assert_called_once_with( + "/sys/class/accel/accel0/device/power/runtime_active_time" + ) @patch("frigate.stats.intel_gpu_info.intel_gpu_name_resolver.get_names") @patch("frigate.util.services.time.sleep") diff --git a/frigate/util/services.py b/frigate/util/services.py index f367235b28..395d30c801 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -1,6 +1,7 @@ """Utilities for services.""" import asyncio +import glob import json import logging import os @@ -670,19 +671,17 @@ def get_intel_gpu_stats( def get_openvino_npu_stats() -> dict[str, str] | None: """Get NPU stats using openvino.""" - NPU_RUNTIME_PATHS = ( - ( - "/sys/devices/pci0000:00/0000:00:0b.0/power/runtime_active_time", - 1_000, # Milliseconds per second - ), - ( - "/sys/class/accel/accel0/device/npu_busy_time_us", - 1_000_000, # Microseconds per second - ), - ) - - for runtime_path, units_per_second in NPU_RUNTIME_PATHS: + for accel_path in sorted(glob.glob("/sys/class/accel/accel*")): try: + driver = os.path.basename(os.readlink(f"{accel_path}/device/driver")) + except OSError: + continue + + if driver != "intel_vpu": + continue + + try: + runtime_path = f"{accel_path}/device/power/runtime_active_time" with open(runtime_path) as f: initial_runtime = float(f.read().strip()) break @@ -705,7 +704,7 @@ def get_openvino_npu_stats() -> dict[str, str] | None: # Calculate usage percentage runtime_diff = current_runtime - initial_runtime - time_diff = (current_time - initial_time) * units_per_second + time_diff = (current_time - initial_time) * 1000.0 # Convert to milliseconds if time_diff > 0: usage = min(100.0, max(0.0, (runtime_diff / time_diff * 100.0)))