diff --git a/frigate/test/test_gpu_stats.py b/frigate/test/test_gpu_stats.py index f6986912f7..e94625f645 100644 --- a/frigate/test/test_gpu_stats.py +++ b/frigate/test/test_gpu_stats.py @@ -40,18 +40,18 @@ class TestGpuStats(unittest.TestCase): "driver": "i915", "pid": "100", "engines": { - "render": (1_000_000_000, 0), - "video": (5_000_000_000, 0), - "video-enhance": (200_000_000, 0), - "compute": (0, 0), + "render": (1_000_000_000, 0, 1), + "video": (5_000_000_000, 0, 1), + "video-enhance": (200_000_000, 0, 1), + "compute": (0, 0, 1), }, }, ("0000:00:02.0", "2", "200"): { "driver": "i915", "pid": "200", "engines": { - "render": (0, 0), - "compute": (2_000_000_000, 0), + "render": (0, 0, 1), + "compute": (2_000_000_000, 0, 1), }, }, } @@ -60,18 +60,18 @@ class TestGpuStats(unittest.TestCase): "driver": "i915", "pid": "100", "engines": { - "render": (1_200_000_000, 0), - "video": (5_500_000_000, 0), - "video-enhance": (300_000_000, 0), - "compute": (0, 0), + "render": (1_200_000_000, 0, 1), + "video": (5_500_000_000, 0, 1), + "video-enhance": (300_000_000, 0, 1), + "compute": (0, 0, 1), }, }, ("0000:00:02.0", "2", "200"): { "driver": "i915", "pid": "200", "engines": { - "render": (0, 0), - "compute": (2_100_000_000, 0), + "render": (0, 0, 1), + "compute": (2_100_000_000, 0, 1), }, }, } @@ -92,6 +92,64 @@ class TestGpuStats(unittest.TestCase): }, } + @patch("frigate.stats.intel_gpu_info.intel_gpu_name_resolver.get_names") + @patch("frigate.util.services.time.sleep") + @patch("frigate.util.services.time.monotonic") + @patch("frigate.util.services._read_intel_drm_fdinfo") + def test_intel_gpu_stats_xe_capacity( + self, read_fdinfo, monotonic, sleep, get_names + ): + # Xe engines report cumulative cycles paired with total cycles, plus a + # per-class capacity. drm-cycles-* is summed across every instance of a + # class, so on Battlemage (capacity 2 for vcs/vecs) busy/total must be + # divided by capacity to land in 0-100%. + monotonic.side_effect = [0.0, 1.0] + get_names.return_value = {"0000:03:00.0": "Intel Arc"} + + # Deltas over the window (busy, total): render 200/1000 cap 1 = 20%, + # video 800/1000 cap 2 = 40%, video-enhance 400/1000 cap 2 = 20%, + # compute 100/1000 cap 1 = 10%. Without the capacity divisor video/ + # video-enhance would read 80%/40% and dec would clamp at 100%. + snapshot_a = { + ("0000:03:00.0", "1", "300"): { + "driver": "xe", + "pid": "300", + "engines": { + "render": (0, 0, 1), + "video": (0, 0, 2), + "video-enhance": (0, 0, 2), + "compute": (0, 0, 1), + }, + }, + } + snapshot_b = { + ("0000:03:00.0", "1", "300"): { + "driver": "xe", + "pid": "300", + "engines": { + "render": (200, 1000, 1), + "video": (800, 1000, 2), + "video-enhance": (400, 1000, 2), + "compute": (100, 1000, 1), + }, + }, + } + read_fdinfo.side_effect = [snapshot_a, snapshot_b] + + intel_stats = get_intel_gpu_stats(None) + + assert intel_stats == { + "0000:03:00.0": { + "name": "Intel Arc", + "vendor": "intel", + "gpu": "90.0%", + "mem": "-%", + "compute": "30.0%", + "dec": "60.0%", + "clients": {"300": "90.0%"}, + }, + } + @patch("frigate.util.services._read_intel_drm_fdinfo") def test_intel_gpu_stats_no_clients(self, read_fdinfo): read_fdinfo.return_value = {} diff --git a/frigate/util/services.py b/frigate/util/services.py index 956e7893c5..62929d32df 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -360,7 +360,7 @@ def _read_intel_drm_fdinfo(target_pdev: str | None) -> dict: if key in snapshot: continue - engines: dict[str, tuple[int, int]] = {} + engines: dict[str, tuple[int, int, int]] = {} if driver == "i915": for fkey, engine in _I915_ENGINE_KEYS.items(): @@ -368,19 +368,34 @@ def _read_intel_drm_fdinfo(target_pdev: str | None) -> dict: if not raw: continue try: - engines[engine] = (int(raw.split()[0]), 0) + engines[engine] = (int(raw.split()[0]), 0, 1) except (ValueError, IndexError): continue else: for suffix, engine in _XE_ENGINE_KEYS.items(): busy_raw = fields.get(f"drm-cycles-{suffix}") total_raw = fields.get(f"drm-total-cycles-{suffix}") + if not (busy_raw and total_raw): continue + + # drm-cycles-* is summed across every instance of the engine + # class while drm-total-cycles-* tracks a single instance, so + # busy/total scales up to the capacity (e.g. Battlemage + # reports 2 for vcs/vecs). Capture it to divide back out; + # absent means a single engine, so default to 1. + capacity_raw = fields.get(f"drm-engine-capacity-{suffix}") + + try: + capacity = int(capacity_raw.split()[0]) if capacity_raw else 1 + except (ValueError, IndexError): + capacity = 1 + try: engines[engine] = ( int(busy_raw.split()[0]), int(total_raw.split()[0]), + max(1, capacity), ) except (ValueError, IndexError): continue @@ -450,11 +465,13 @@ def get_intel_gpu_stats( pid_pct = per_pdev_pid_pct.setdefault(pdev, {}) client_total = 0.0 - for engine, (busy_b, total_b) in data_b["engines"].items(): + for engine, (busy_b, total_b, capacity) in data_b["engines"].items(): if engine not in engine_pct: continue - busy_a, total_a = data_a["engines"].get(engine, (busy_b, total_b)) + busy_a, total_a, _ = data_a["engines"].get( + engine, (busy_b, total_b, capacity) + ) if data_b["driver"] == "i915": delta = max(0, busy_b - busy_a) @@ -464,7 +481,9 @@ def get_intel_gpu_stats( delta_total = total_b - total_a if delta_total <= 0: continue - pct = min(100.0, delta_busy / delta_total * 100.0) + # Normalize by capacity so a class with N engine instances + # (busy summed across all N) reports 0-100%, not 0-N*100%. + pct = min(100.0, delta_busy / (delta_total * capacity) * 100.0) engine_pct[engine] += pct client_total += pct