Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ OpenFilter Library release notes

## [Unreleased]

## v1.1.2 - 2026-06-28

### Fixed

- **GPU usage percent emitted as float for Cloud Monitoring ([FILTER-585](https://plainsight-ai.atlassian.net/browse/FILTER-585))**: `Metrics.gpu_thread_func` now casts `gpu_usage_percent` to `float` before export. NVML reports `gpu_util` as an integer, but the Cloud Monitoring descriptor `openfilter_gpu_usage_percent` is type-locked to DOUBLE, so each export batch had one point rejected with `value type for metric must be DOUBLE, but is INT64`. The cast is targeted to this single summary field; the per-device `gpuN` series and the legitimately INT64 descriptors (`gpu_accessible`, `camera_connected`) are unchanged.

## v1.1.1 - 2026-06-01

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.1.1
v1.1.2
2 changes: 1 addition & 1 deletion openfilter/filter_runtime/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def gpu_thread_func(self, stop_evt: Event):

self.gpu = gpu
self.gpu_accessible = 1
self.gpu_usage_percent = gpu.get('gpu0', 0.0)
self.gpu_usage_percent = float(gpu.get('gpu0', 0.0)) # NVML gpu_util is int; Cloud Monitoring's DOUBLE descriptor rejects it otherwise

except Exception as exc:
logger.error(exc)
Expand Down
35 changes: 35 additions & 0 deletions tests/test_gpu_usage_percent_float.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Tests for ensuring that openfilter_gpu_usage_percent is always emitted as a float."""

from unittest.mock import MagicMock, patch

from openfilter.filter_runtime.gpu import GPUMetrics
from openfilter.filter_runtime.metrics import Metrics


def _run_one_iteration(mock_metrics):
"""Run gpu_thread_func for exactly one loop iteration with mocked NVML metrics."""
metrics = object.__new__(Metrics) # bare object, skip __init__'s background threads

stop_evt = MagicMock()
stop_evt.wait.side_effect = [False, True] # run the loop body exactly once

with patch('openfilter.filter_runtime.metrics.get_gpu_metrics', return_value=mock_metrics):
metrics.gpu_thread_func(stop_evt)

return metrics


def test_gpu_usage_percent_is_float():
"""gpu_usage_percent is cast to float even when NVML returns an int gpu_util."""
metrics = _run_one_iteration([GPUMetrics(index=0, gpu_util=55, mem_used_mb=1024)])

assert type(metrics.gpu_usage_percent) is float
assert metrics.gpu_usage_percent == 55.0


def test_gpu_usage_percent_is_float_when_gpu_idle():
"""An idle GPU reports integer gpu_util==0; without the float() cast this path would emit an int 0 and be rejected by the DOUBLE-locked Cloud Monitoring descriptor, so this test fails on the un-cast code."""
metrics = _run_one_iteration([GPUMetrics(index=0, gpu_util=0, mem_used_mb=1024)])

assert type(metrics.gpu_usage_percent) is float
assert metrics.gpu_usage_percent == 0.0
Loading