Skip to content

Performance improvement: Avoid unnecessary GPU detail queries in _monitor_power()#1264

Merged
benoit-cty merged 5 commits into
mlco2:masterfrom
vishali-mp:perf/issue-gpu-heavyweight-calls-every-second
Jul 19, 2026
Merged

Performance improvement: Avoid unnecessary GPU detail queries in _monitor_power()#1264
benoit-cty merged 5 commits into
mlco2:masterfrom
vishali-mp:perf/issue-gpu-heavyweight-calls-every-second

Conversation

@vishali-mp

@vishali-mp vishali-mp commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description

The _monitor_power() hot path invokes get_gpu_details() every second, but only gpu_index and gpu_utilization are consumed by the caller. get_gpu_details() retrieves memory info, temperature, compute processes, graphics processes, compute mode, and other metadata — all discarded in the monitoring loop. On multi-GPU systems this triggers dozens of unnecessary NVML calls per second, with process enumeration APIs being the most expensive.

Changes:

  • codecarbon/core/gpu_device.py: New get_gpu_utilization_lightweight() that returns only gpu_index and gpu_utilization, skipping memory, temperature, process lists, compute mode, and other unused attributes.
  • codecarbon/core/gpu.py: New get_gpu_utilization_list() that iterates devices using the lightweight method.
  • codecarbon/emissions_tracker.py: Updated _monitor_power() to call get_gpu_utilization_list() instead of get_gpu_details().
  • tests/test_gpu_nvidia.py: Added tests for get_gpu_utilization_lightweight(), get_gpu_utilization_list(), and exception handling (returns [] on NVML error).
  • **tests/test_emissions_tracker.py: Added 4 edge case tests covering all guard conditions in _monitor_power(): gpu_index is None, GPU not in monitored IDs, missing gpu_utilization key, and empty result list.
  • scripts/benchmark_gpu_monitoring.py: Benchmark harness to measure performance delta between old and new paths.

Related Issue

Closes #1237

Motivation and Context

_monitor_power() runs every second and only needs GPU utilization to compute a rolling average. The full get_gpu_details() call retrieves memory (nvmlDeviceGetMemoryInfo), temperature (nvmlDeviceGetTemperature), compute processes (nvmlDeviceGetComputeRunningProcesses), graphics processes (nvmlDeviceGetGraphicsRunningProcesses), and compute mode — none of which are used in this hot path. The process enumeration APIs are especially expensive as they iterate active GPU processes and collect PID-level information.

The lightweight path reduces NVML calls per GPU per second from ~7 to just 1 (only nvmlDeviceGetUtilizationRates), preserving the existing get_gpu_details() for code paths that need full metadata (e.g. __repr__, static info queries).

How Has This Been Tested?

  • Unit tests in tests/test_gpu_nvidia.py: test_gpu_utilization_list, test_gpu_utilization_lightweight, test_gpu_utilization_list_empty_on_exception.
  • Integration tests in tests/test_emissions_tracker.py: test_monitor_power_collects_gpu_utilization_lightweight (happy path), test_monitor_power_skips_gpu_when_index_is_none, test_monitor_power_skips_gpu_not_in_monitored_ids, test_monitor_power_skips_gpu_when_utilization_key_missing, test_monitor_power_handles_empty_gpu_utilization_list.
  • Benchmark script in scripts/benchmark_gpu_monitoring.py: compares calls/sec of lightweight vs full path.
  • All existing tests pass, pre-commit checks (autoflake, isort, black, flake8) pass.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

AI Usage Disclosure

  • 🟥 AI-vibecoded: You cannot explain the logic. Car analogy : the car drive by itself, you are outside it and just tell it where to go.
  • 🟠 AI-generated: Car analogy : the car drive by itself, you are inside and give instructions.
  • ⭐ AI-assisted. Car analogy : you drive the car, AI help you find your way.
  • ♻️ No AI used. Car analogy : you drive the car.

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the docs/how-to/contributing.md document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

_get_gpu_details makes 8 NVML calls per GPU (memory, temperature, power,
energy, utilization, compute mode, compute processes, graphics processes)
but _monitor_power only consumes gpu_index and gpu_utilization.

Add get_gpu_utilization_lightweight() which makes 1 NVML call per GPU
(nvmlDeviceGetUtilizationRates), and switch _monitor_power to use it.

Reduces NVML calls per monitoring cycle from 8 to 1 per GPU — on an
8-GPU system that's 56 fewer NVML calls per second (64 → 8).

get_gpu_details() is preserved unchanged for measure_power_and_energy
where power_usage is consumed.
Covers all guard conditions in the monitoring loop:
- gpu_index is None → entry skipped
- gpu_index not in monitored set → entry skipped
- gpu_utilization key missing → entry skipped
- empty list from get_gpu_utilization_list() → nothing collected
@vishali-mp
vishali-mp requested a review from a team as a code owner July 7, 2026 19:54
@vishali-mp vishali-mp changed the title test(gpu): add edge case tests for lightweight GPU monitoring path perf(gpu): add lightweight GPU monitoring path Jul 7, 2026
@vishali-mp vishali-mp changed the title perf(gpu): add lightweight GPU monitoring path Performance improvement: Avoid unnecessary GPU detail queries in _monitor_power() Jul 7, 2026
@benoit-cty

Copy link
Copy Markdown
Contributor

Thanks a lot, sorry for the delay.

Here is the content of your script scripts/benchmark_gpu_monitoring.py as I will delete it because we think it not worth to keep it in main branch:

#!/usr/bin/env python3
"""
Benchmark: GPU monitoring overhead — heavyweight get_gpu_details vs lightweight get_gpu_utilization_list.

Measures how many unnecessary NVML calls the per-second _monitor_power() hot path
makes on multi-GPU systems, and the latency difference between the old full-detail
path and the new lightweight utilization-only path.

Usage:
    # Quick run (default)
    uv run python scripts/benchmark_gpu_monitoring.py

    # Full benchmark with subprocess cold-start samples
    uv run python scripts/benchmark_gpu_monitoring.py all

    # Simulated multi-GPU scale (no real GPU needed)
    uv run python scripts/benchmark_gpu_monitoring.py all --simulate-gpus 8

Methodology:
    - Cold metrics: spawn fresh Python subprocesses, each performing full GPU init
    - Warm metrics: repeat calls in the same process after warm-up
    - p50 (median) reported across multiple samples
    - NVML call counts derived from source code audit (gpu_nvidia.py + gpu_device.py)
    - On real NVIDIA hardware: wall-clock timing of actual NVML calls
    - On non-NVIDIA hardware: mock NVML with realistic simulated call latencies
"""

from __future__ import annotations

import argparse
import json
import os
import statistics
import subprocess
import sys
import time
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
RESULTS_DIR = REPO_ROOT / ".context"
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
DEFAULT_RESULTS = RESULTS_DIR / "gpu-benchmark-results.jsonl"

# NVML call categories based on source audit (gpu_nvidia.py + gpu_device.py)
# _monitor_power() calls get_gpu_details() every 1s but only uses gpu_utilization
NVML_CALLS_HEAVY = [
    "nvmlDeviceGetMemoryInfo",  # → free_memory, total_memory, used_memory  — DISCARDED
    "nvmlDeviceGetTemperature",  # → temperature                              — DISCARDED
    "nvmlDeviceGetPowerUsage",  # → power_usage                              — DISCARDED
    "nvmlDeviceGetTotalEnergyConsumption",  # → total_energy_consumption            — DISCARDED
    "nvmlDeviceGetUtilizationRates",  # → gpu_utilization                          — USED
    "nvmlDeviceGetComputeMode",  # → compute_mode                             — DISCARDED
    "nvmlDeviceGetComputeRunningProcesses",  # → compute_processes                  — DISCARDED (most expensive)
    "nvmlDeviceGetGraphicsRunningProcesses",  # → graphics_processes                — DISCARDED (most expensive)
]

NVML_CALLS_LIGHTWEIGHT = [
    "nvmlDeviceGetUtilizationRates",  # ← the only call we need for utilization
]

# Simulated per-call latencies (microseconds) for non-GPU systems.
# Based on typical NVML overheads reported in NVIDIA docs & community benchmarks.
# Process enumeration (GetComputeRunningProcesses) is the most expensive because
# it iterates active GPU processes and collects PID-level info.
SIMULATED_LATENCY_US: dict[str, float] = {
    "nvmlDeviceGetMemoryInfo": 50,
    "nvmlDeviceGetTemperature": 40,
    "nvmlDeviceGetPowerUsage": 45,
    "nvmlDeviceGetTotalEnergyConsumption": 40,
    "nvmlDeviceGetUtilizationRates": 50,
    "nvmlDeviceGetComputeMode": 35,
    "nvmlDeviceGetComputeRunningProcesses": 500,  # ← expensive: process enumeration
    "nvmlDeviceGetGraphicsRunningProcesses": 500,  # ← expensive: process enumeration
    "nvmlDeviceGetName": 40,
    "nvmlDeviceGetUUID": 35,
    "nvmlDeviceGetEnforcedPowerLimit": 40,
}


@dataclass
class LatencyStats:
    count: int = 0
    min_ms: float = 0.0
    max_ms: float = 0.0
    mean_ms: float = 0.0
    p50_ms: float = 0.0
    p95_ms: float = 0.0


@dataclass
class NvmlCallBreakdown:
    call_name: str
    latency_us: float
    used_by_monitor: bool


@dataclass
class GpuDetailMethodBenchmark:
    method: str  # "get_gpu_details" or "get_gpu_utilization_list"
    gpu_count: int
    nvml_calls_per_second: int
    nvml_calls_unused_per_second: int
    latency_per_call_ms: LatencyStats
    latency_per_second_ms: float  # projected = per_gpu * gpu_count


@dataclass
class MonitoringOverheadProjection:
    metric: str
    heavy_path: float
    lightweight_path: float
    savings: float
    unit: str


@dataclass
class BenchmarkReport:
    timestamp: str
    mode: str
    hostname: str
    gpu_backend: str
    gpu_count_real: int
    simulated: bool
    call_breakdown: list[dict]
    method_benchmarks: list[dict]
    projections: list[dict]
    result: str = ""


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _percentile(sorted_values: list[float], pct: float) -> float:
    if not sorted_values:
        return 0.0
    if len(sorted_values) == 1:
        return sorted_values[0]
    k = (len(sorted_values) - 1) * (pct / 100.0)
    f = int(k)
    c = min(f + 1, len(sorted_values) - 1)
    if f == c:
        return sorted_values[f]
    return sorted_values[f] + (sorted_values[c] - sorted_values[f]) * (k - f)


def compute_stats(values_ms: list[float]) -> LatencyStats:
    if not values_ms:
        return LatencyStats(count=0)
    s = sorted(values_ms)
    return LatencyStats(
        count=len(s),
        min_ms=s[0],
        max_ms=s[-1],
        mean_ms=statistics.mean(s),
        p50_ms=_percentile(s, 50),
        p95_ms=_percentile(s, 95),
    )


def _detect_gpu_backend() -> tuple[str, int]:
    """Detect real GPU backend and count. Returns (backend_name, count)."""
    try:
        from codecarbon.core.gpu import AMDSMI_AVAILABLE, PYNVML_AVAILABLE

        if PYNVML_AVAILABLE:
            from codecarbon.core import gpu_nvidia

            count = gpu_nvidia.pynvml.nvmlDeviceGetCount()
            return ("nvidia", count)
        if AMDSMI_AVAILABLE:
            return ("amd", 0)  # count not trivial
    except Exception:
        pass
    return ("none", 0)


def _collect_call_breakdown() -> list[dict]:
    """Return the per-NVML-call breakdown showing what's used vs discarded."""
    results = []
    for call in NVML_CALLS_HEAVY:
        results.append(
            {
                "call_name": call,
                "used_by_monitor": call == "nvmlDeviceGetUtilizationRates",
                "simulated_latency_us": SIMULATED_LATENCY_US.get(call, 50),
            }
        )
    return results


def _mock_time_for_call(call_name: str) -> None:
    """Sleep to simulate NVML call latency when no real GPU is available."""
    time.sleep(SIMULATED_LATENCY_US.get(call_name, 50) / 1_000_000)


class MockNvidiaGPUDevice:
    """A lightweight mock that simulates NVML call latencies.

    Used on non-NVIDIA systems so the benchmark can still measure
    relative overhead and project multi-GPU scaling.
    """

    def __init__(self, gpu_index: int):
        self.gpu_index = gpu_index

    def get_gpu_details(self) -> dict:
        _mock_time_for_call("nvmlDeviceGetMemoryInfo")
        _mock_time_for_call("nvmlDeviceGetTemperature")
        _mock_time_for_call("nvmlDeviceGetPowerUsage")
        _mock_time_for_call("nvmlDeviceGetTotalEnergyConsumption")
        _mock_time_for_call("nvmlDeviceGetUtilizationRates")
        _mock_time_for_call("nvmlDeviceGetComputeMode")
        _mock_time_for_call("nvmlDeviceGetComputeRunningProcesses")
        _mock_time_for_call("nvmlDeviceGetGraphicsRunningProcesses")
        return {"gpu_index": self.gpu_index, "gpu_utilization": 50}

    def get_gpu_utilization_lightweight(self) -> dict:
        _mock_time_for_call("nvmlDeviceGetUtilizationRates")
        return {"gpu_index": self.gpu_index, "gpu_utilization": 50}


def _benchmark_method(
    devices: list,
    method_name: str,
    samples: int = 200,
    warmup: int = 20,
) -> LatencyStats:
    """Benchmark a GPU method. Returns latency stats in milliseconds."""
    for _ in range(warmup):
        if method_name == "get_gpu_details":
            [d.get_gpu_details() for d in devices]
        else:
            [d.get_gpu_utilization_lightweight() for d in devices]

    timings = []
    for _ in range(samples):
        t0 = time.perf_counter()
        if method_name == "get_gpu_details":
            [d.get_gpu_details() for d in devices]
        else:
            [d.get_gpu_utilization_lightweight() for d in devices]
        elapsed_ms = (time.perf_counter() - t0) * 1000
        timings.append(elapsed_ms)

    return compute_stats(timings)


def _benchmark_real_gpu(gpu_count: int) -> tuple[list[dict], list[dict]]:
    """Benchmark using real GPU hardware via AllGPUDevices."""
    sys.path.insert(0, str(REPO_ROOT))
    from codecarbon.core.gpu import AllGPUDevices

    devices = AllGPUDevices()
    actual_count = devices.device_count

    heavy_stats = _benchmark_method(devices.devices, "get_gpu_details")
    light_stats = _benchmark_method(devices.devices, "get_gpu_utilization_lightweight")

    method_benchmarks = [
        {
            "method": "get_gpu_details",
            "gpu_count": actual_count,
            "nvml_calls_per_second": len(NVML_CALLS_HEAVY) * actual_count,
            "nvml_calls_unused_per_second": (len(NVML_CALLS_HEAVY) - 1) * actual_count,
            "latency_per_call_ms": asdict(heavy_stats),
            "latency_per_second_ms": heavy_stats.p50_ms,
        },
        {
            "method": "get_gpu_utilization_list",
            "gpu_count": actual_count,
            "nvml_calls_per_second": len(NVML_CALLS_LIGHTWEIGHT) * actual_count,
            "nvml_calls_unused_per_second": 0,
            "latency_per_call_ms": asdict(light_stats),
            "latency_per_second_ms": light_stats.p50_ms,
        },
    ]

    # Scale projections for multi-GPU
    for simulated_count in [1, 4, 8]:
        scale = simulated_count / actual_count if actual_count else 1
        method_benchmarks.append(
            {
                "method": f"get_gpu_details (projected {simulated_count} GPU)",
                "gpu_count": simulated_count,
                "nvml_calls_per_second": len(NVML_CALLS_HEAVY) * simulated_count,
                "nvml_calls_unused_per_second": (len(NVML_CALLS_HEAVY) - 1)
                * simulated_count,
                "latency_per_call_ms": asdict(heavy_stats),
                "latency_per_second_ms": heavy_stats.p50_ms * scale,
            }
        )
        method_benchmarks.append(
            {
                "method": f"get_gpu_utilization_list (projected {simulated_count} GPU)",
                "gpu_count": simulated_count,
                "nvml_calls_per_second": len(NVML_CALLS_LIGHTWEIGHT) * simulated_count,
                "nvml_calls_unused_per_second": 0,
                "latency_per_call_ms": asdict(light_stats),
                "latency_per_second_ms": light_stats.p50_ms * scale,
            }
        )

    return method_benchmarks, []


def _benchmark_simulated_gpu(simulate_gpus: int) -> tuple[list[dict], list[dict]]:
    """Benchmark using mock devices with simulated NVML latencies."""
    devices = [MockNvidiaGPUDevice(i) for i in range(simulate_gpus)]

    heavy_stats = _benchmark_method(devices, "get_gpu_details")
    light_stats = _benchmark_method(devices, "get_gpu_utilization_lightweight")

    method_benchmarks = [
        {
            "method": "get_gpu_details",
            "gpu_count": simulate_gpus,
            "nvml_calls_per_second": len(NVML_CALLS_HEAVY) * simulate_gpus,
            "nvml_calls_unused_per_second": (len(NVML_CALLS_HEAVY) - 1) * simulate_gpus,
            "latency_per_call_ms": asdict(heavy_stats),
            "latency_per_second_ms": heavy_stats.p50_ms,
        },
        {
            "method": "get_gpu_utilization_list",
            "gpu_count": simulate_gpus,
            "nvml_calls_per_second": len(NVML_CALLS_LIGHTWEIGHT) * simulate_gpus,
            "nvml_calls_unused_per_second": 0,
            "latency_per_call_ms": asdict(light_stats),
            "latency_per_second_ms": light_stats.p50_ms,
        },
    ]

    return method_benchmarks, []


def _compute_projections(method_benchmarks: list[dict]) -> list[dict]:
    """Compute time-savings projections from benchmark results."""
    heavy = next(
        (m for m in method_benchmarks if m["method"] == "get_gpu_details"), None
    )
    light = next(
        (m for m in method_benchmarks if m["method"] == "get_gpu_utilization_list"),
        None,
    )
    if not heavy or not light:
        return []

    heavy_per_sec = heavy["latency_per_second_ms"]
    light_per_sec = light["latency_per_second_ms"]
    savings_per_sec = heavy_per_sec - light_per_sec

    gpu_count = heavy["gpu_count"]

    return [
        {
            "metric": "Per-second monitoring overhead",
            "heavy_path_ms": heavy_per_sec,
            "lightweight_path_ms": light_per_sec,
            "savings_ms": savings_per_sec,
            "savings_pct": (
                round((savings_per_sec / heavy_per_sec) * 100, 1)
                if heavy_per_sec
                else 0
            ),
            "unit": "ms/s",
        },
        {
            "metric": "Per-minute monitoring overhead",
            "heavy_path_ms": heavy_per_sec * 60,
            "lightweight_path_ms": light_per_sec * 60,
            "savings_ms": savings_per_sec * 60,
            "savings_pct": (
                round((savings_per_sec / heavy_per_sec) * 100, 1)
                if heavy_per_sec
                else 0
            ),
            "unit": "ms/min",
        },
        {
            "metric": "Per-hour monitoring overhead",
            "heavy_path_ms": heavy_per_sec * 3600,
            "lightweight_path_ms": light_per_sec * 3600,
            "savings_ms": savings_per_sec * 3600,
            "savings_pct": (
                round((savings_per_sec / heavy_per_sec) * 100, 1)
                if heavy_per_sec
                else 0
            ),
            "unit": "ms/hr",
        },
        {
            "metric": "Per-day monitoring overhead (24h)",
            "heavy_path_ms": heavy_per_sec * 86400,
            "lightweight_path_ms": light_per_sec * 86400,
            "savings_ms": savings_per_sec * 86400,
            "savings_pct": (
                round((savings_per_sec / heavy_per_sec) * 100, 1)
                if heavy_per_sec
                else 0
            ),
            "unit": "ms/day",
        },
        {
            "metric": "Unnecessary NVML calls per second",
            "heavy_path_value": heavy["nvml_calls_unused_per_second"],
            "lightweight_path_value": 0,
            "savings_value": heavy["nvml_calls_unused_per_second"],
            "unit": "calls/s",
        },
        {
            "metric": f"Unnecessary NVML calls per hour (on {gpu_count} GPU{'s' if gpu_count != 1 else ''})",
            "heavy_path_value": heavy["nvml_calls_unused_per_second"] * 3600,
            "lightweight_path_value": 0,
            "savings_value": heavy["nvml_calls_unused_per_second"] * 3600,
            "unit": "calls/hr",
        },
    ]


def run_all(simulate_gpus: int | None = None) -> BenchmarkReport:
    backend, real_count = _detect_gpu_backend()
    simulated = backend == "none" and simulate_gpus is not None

    if backend != "none" and real_count > 0:
        gpu_backend = f"nvidia ({real_count} GPU{'s' if real_count != 1 else ''})"
        method_bms, _ = _benchmark_real_gpu(real_count)
    elif simulate_gpus:
        gpu_backend = (
            f"simulated ({simulate_gpus} GPU{'s' if simulate_gpus != 1 else ''})"
        )
        method_bms, _ = _benchmark_simulated_gpu(simulate_gpus)
    else:
        gpu_backend = "none (no GPU available, use --simulate-gpus N)"
        method_bms = []

    projections = _compute_projections(method_bms) if method_bms else []

    call_breakdown = _collect_call_breakdown()

    return BenchmarkReport(
        timestamp=_now_iso(),
        mode="all",
        hostname=os.uname().nodename,
        gpu_backend=gpu_backend,
        gpu_count_real=real_count,
        simulated=simulated,
        call_breakdown=call_breakdown,
        method_benchmarks=method_bms,
        projections=projections,
    )


def print_report(report: BenchmarkReport) -> None:
    sep = "─" * 72

    print(f"\n{' GPU Monitoring Overhead Benchmark ':=^72}")
    print(f"  Host:        {report.hostname}")
    print(f"  GPU backend: {report.gpu_backend}")
    print(f"  Simulated:   {report.simulated}")
    print(f"  Timestamp:   {report.timestamp}")

    if report.simulated:
        print(f"\n{' ⚠ SIMULATED — No real GPU detected ':=^72}")
        print("  Call latencies are estimated (see SIMULATED_LATENCY_US in script).")
        print("  Run this on an NVIDIA GPU machine for real hardware measurements.")

    # NVML call breakdown
    print(f"\n{sep}")
    print(f"{' NVML Call Breakdown (per GPU, per call to get_gpu_details) ':=^72}")
    print(f"{'NVML Call':40s} {'Latency (µs)':15s} {'Used by monitor':20s}")
    print("-" * 72)
    for cb in report.call_breakdown:
        used = "YES" if cb["used_by_monitor"] else ""
        print(
            f"{cb['call_name']:40s} {cb['simulated_latency_us']:>10.0f} µs  {used:20s}"
        )

    unused = sum(1 for cb in report.call_breakdown if not cb["used_by_monitor"])
    total = len(report.call_breakdown)
    print(f"\n{unused}/{total} NVML calls DISCARDED by _monitor_power()")
    print(f"  → Only 1/{total} calls actually used (gpu_utilization)")

    # Method benchmarks
    if report.method_benchmarks:
        print(f"\n{sep}")
        print(f"{' Method Latency Benchmarks ':=^72}")
        print(
            f"{'Method':50s} {'p50':>8s} {'mean':>8s} {'p95':>8s}  {'NVML calls/s':>14s}"
        )
        print("-" * 72)
        for mb in report.method_benchmarks:
            lat = mb["latency_per_call_ms"]
            print(
                f"{mb['method']:50s} "
                f"{lat['p50_ms']:>7.2f}ms {lat['mean_ms']:>7.2f}ms {lat['p95_ms']:>7.2f}ms  "
                f"{mb['nvml_calls_per_second']:>8d}/s"
            )

    # Projections
    if report.projections:
        print(f"\n{sep}")
        print(f"{' Projected Savings (heavyweight → lightweight) ':=^72}")
        print(f"{'Metric':50s} {'Heavy':>12s} {'Light':>12s} {'Savings':>12s}")
        print("-" * 72)
        for p in report.projections:
            if "savings_pct" in p:
                print(
                    f"{p['metric']:50s} "
                    f"{p['heavy_path_ms']:>8.1f}ms {p['lightweight_path_ms']:>8.1f}ms "
                    f"{p['savings_ms']:>8.1f}ms ({p['savings_pct']}%)"
                )
            else:
                print(
                    f"{p['metric']:50s} "
                    f"{p['heavy_path_value']:>12,d} {p['lightweight_path_value']:>12,d} "
                    f"{p['savings_value']:>12,d}"
                )

    print(f"\n{sep}")
    print(f"{' Summary ':=^72}")
    if report.projections:
        hourly = next(
            (
                p
                for p in report.projections
                if p["metric"] == "Per-hour monitoring overhead"
            ),
            None,
        )
        daily = next(
            (
                p
                for p in report.projections
                if p["metric"] == "Per-day monitoring overhead (24h)"
            ),
            None,
        )
        nvml_daily = next(
            (p for p in report.projections if "NVML calls per hour" in p["metric"]),
            None,
        )
        if hourly:
            print(
                f"  Each second of monitoring saves   {hourly['savings_ms'] / 3600:.3f} ms"
            )
            print(
                f"  Per hour of continuous monitoring saves  {hourly['savings_ms'] / 1000:.1f} s"
            )
        if daily:
            print(
                f"  Per 24h day of monitoring saves   {daily['savings_ms'] / 1000:.0f} s ({daily['savings_ms'] / 60000:.1f} min)"
            )
        if nvml_daily:
            print(
                f"  Unnecessary NVML calls per 24h:    {nvml_daily['savings_value'] * 24:,d}"
            )
    print(f"{'=' * 72}\n")


def run_cold_subprocess(simulate_gpus: int | None = None) -> BenchmarkReport:
    """Spawn a fresh subprocess to measure cold-start GPU detection overhead."""
    cmd = [
        sys.executable,
        __file__,
        "cold",
        "--json",
    ]
    if simulate_gpus:
        cmd.extend(["--simulate-gpus", str(simulate_gpus)])
    env = os.environ.copy()
    t0 = time.perf_counter()
    proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60, env=env)
    elapsed_ms = (time.perf_counter() - t0) * 1000
    if proc.returncode != 0:
        print(f"Subprocess failed: {proc.stderr[:500]}")
        return BenchmarkReport(
            timestamp=_now_iso(),
            mode="cold_subprocess",
            hostname=os.uname().nodename,
            gpu_backend="error",
            gpu_count_real=0,
            simulated=False,
            call_breakdown=[],
            method_benchmarks=[],
            projections=[],
            result="error",
        )
    report = json.loads(proc.stdout)
    report["mode"] = "cold_subprocess"
    report["result"] = f"cold_subprocess_overhead_ms={elapsed_ms:.1f}"
    return BenchmarkReport(**report)


def main() -> None:
    p = argparse.ArgumentParser(description="GPU monitoring overhead benchmark")
    p.add_argument("mode", nargs="?", default="quick", choices=["quick", "all", "cold"])
    p.add_argument(
        "--simulate-gpus",
        type=int,
        default=None,
        help="Simulate N GPUs (default: auto-detect)",
    )
    p.add_argument(
        "--json", action="store_true", help="Output JSON (for subprocess consumption)"
    )
    p.add_argument("--results-file", type=Path, default=DEFAULT_RESULTS)
    args = p.parse_args()

    if args.mode == "quick":
        report = run_all(args.simulate_gpus)
        print_report(report)

    elif args.mode == "all":
        report = run_all(args.simulate_gpus)
        if args.json:
            print(json.dumps(asdict(report), default=str))
        else:
            print_report(report)

        # Also run cold subprocess if not already in one
        if not args.json and not os.environ.get("_BENCHMARK_CHILD"):
            print("\n--- Cold subprocess benchmark ---")
            cold_report = run_cold_subprocess(args.simulate_gpus)
            print(f"Cold subprocess overhead: {cold_report.result}")

    elif args.mode == "cold":
        os.environ["_BENCHMARK_CHILD"] = "1"
        report = run_all(args.simulate_gpus)
        if args.json:
            print(json.dumps(asdict(report), default=str))
        else:
            print_report(report)

    # Append to results file
    if not args.json and args.mode != "cold":
        with open(args.results_file, "a") as f:
            f.write(json.dumps(asdict(report), default=str) + "\n")
        print(f"→ Results appended to {args.results_file}")


if __name__ == "__main__":
    main()

Same with .context/gpu-benchmark-results.jsonl:

{"timestamp": "2026-06-22T13:04:21.864329+00:00", "mode": "all", "hostname": "Mac.lan", "gpu_backend": "simulated (8 GPUs)", "gpu_count_real": 0, "simulated": true, "call_breakdown": [{"call_name": "nvmlDeviceGetMemoryInfo", "used_by_monitor": false, "simulated_latency_us": 50}, {"call_name": "nvmlDeviceGetTemperature", "used_by_monitor": false, "simulated_latency_us": 40}, {"call_name": "nvmlDeviceGetPowerUsage", "used_by_monitor": false, "simulated_latency_us": 45}, {"call_name": "nvmlDeviceGetTotalEnergyConsumption", "used_by_monitor": false, "simulated_latency_us": 40}, {"call_name": "nvmlDeviceGetUtilizationRates", "used_by_monitor": true, "simulated_latency_us": 50}, {"call_name": "nvmlDeviceGetComputeMode", "used_by_monitor": false, "simulated_latency_us": 35}, {"call_name": "nvmlDeviceGetComputeRunningProcesses", "used_by_monitor": false, "simulated_latency_us": 500}, {"call_name": "nvmlDeviceGetGraphicsRunningProcesses", "used_by_monitor": false, "simulated_latency_us": 500}], "method_benchmarks": [{"method": "get_gpu_details", "gpu_count": 8, "nvml_calls_per_second": 64, "nvml_calls_unused_per_second": 56, "latency_per_call_ms": {"count": 200, "min_ms": 12.55650000530295, "max_ms": 67.93629100138787, "mean_ms": 13.547916884053848, "p50_ms": 12.920312496135011, "p95_ms": 15.044183352438257}, "latency_per_second_ms": 12.920312496135011}, {"method": "get_gpu_utilization_list", "gpu_count": 8, "nvml_calls_per_second": 8, "nvml_calls_unused_per_second": 0, "latency_per_call_ms": {"count": 200, "min_ms": 0.5256250005913898, "max_ms": 0.6670419970760122, "mean_ms": 0.5427240101562347, "p50_ms": 0.5367710036807694, "p95_ms": 0.5937666595855262}, "latency_per_second_ms": 0.5367710036807694}], "projections": [{"metric": "Per-second monitoring overhead", "heavy_path_ms": 12.920312496135011, "lightweight_path_ms": 0.5367710036807694, "savings_ms": 12.383541492454242, "savings_pct": 95.8, "unit": "ms/s"}, {"metric": "Per-minute monitoring overhead", "heavy_path_ms": 775.2187497681007, "lightweight_path_ms": 32.20626022084616, "savings_ms": 743.0124895472545, "savings_pct": 95.8, "unit": "ms/min"}, {"metric": "Per-hour monitoring overhead", "heavy_path_ms": 46513.12498608604, "lightweight_path_ms": 1932.3756132507697, "savings_ms": 44580.74937283527, "savings_pct": 95.8, "unit": "ms/hr"}, {"metric": "Per-day monitoring overhead (24h)", "heavy_path_ms": 1116314.999666065, "lightweight_path_ms": 46377.01471801847, "savings_ms": 1069937.9849480465, "savings_pct": 95.8, "unit": "ms/day"}, {"metric": "Unnecessary NVML calls per second", "heavy_path_value": 56, "lightweight_path_value": 0, "savings_value": 56, "unit": "calls/s"}, {"metric": "Unnecessary NVML calls per hour (on 8 GPUs)", "heavy_path_value": 201600, "lightweight_path_value": 0, "savings_value": 201600, "unit": "calls/hr"}], "result": ""}
{"timestamp": "2026-06-22T13:04:31.008513+00:00", "mode": "all", "hostname": "Mac.lan", "gpu_backend": "simulated (1 GPU)", "gpu_count_real": 0, "simulated": true, "call_breakdown": [{"call_name": "nvmlDeviceGetMemoryInfo", "used_by_monitor": false, "simulated_latency_us": 50}, {"call_name": "nvmlDeviceGetTemperature", "used_by_monitor": false, "simulated_latency_us": 40}, {"call_name": "nvmlDeviceGetPowerUsage", "used_by_monitor": false, "simulated_latency_us": 45}, {"call_name": "nvmlDeviceGetTotalEnergyConsumption", "used_by_monitor": false, "simulated_latency_us": 40}, {"call_name": "nvmlDeviceGetUtilizationRates", "used_by_monitor": true, "simulated_latency_us": 50}, {"call_name": "nvmlDeviceGetComputeMode", "used_by_monitor": false, "simulated_latency_us": 35}, {"call_name": "nvmlDeviceGetComputeRunningProcesses", "used_by_monitor": false, "simulated_latency_us": 500}, {"call_name": "nvmlDeviceGetGraphicsRunningProcesses", "used_by_monitor": false, "simulated_latency_us": 500}], "method_benchmarks": [{"method": "get_gpu_details", "gpu_count": 1, "nvml_calls_per_second": 8, "nvml_calls_unused_per_second": 7, "latency_per_call_ms": {"count": 200, "min_ms": 1.5633330040145665, "max_ms": 1.9456660083960742, "mean_ms": 1.6176056357653579, "p50_ms": 1.605146004294511, "p95_ms": 1.665493459586287}, "latency_per_second_ms": 1.605146004294511}, {"method": "get_gpu_utilization_list", "gpu_count": 1, "nvml_calls_per_second": 1, "nvml_calls_unused_per_second": 0, "latency_per_call_ms": {"count": 200, "min_ms": 0.06458298594225198, "max_ms": 0.14504100545309484, "mean_ms": 0.06767040984414052, "p50_ms": 0.06535449210787192, "p95_ms": 0.0750916529796086}, "latency_per_second_ms": 0.06535449210787192}], "projections": [{"metric": "Per-second monitoring overhead", "heavy_path_ms": 1.605146004294511, "lightweight_path_ms": 0.06535449210787192, "savings_ms": 1.539791512186639, "savings_pct": 95.9, "unit": "ms/s"}, {"metric": "Per-minute monitoring overhead", "heavy_path_ms": 96.30876025767066, "lightweight_path_ms": 3.921269526472315, "savings_ms": 92.38749073119834, "savings_pct": 95.9, "unit": "ms/min"}, {"metric": "Per-hour monitoring overhead", "heavy_path_ms": 5778.525615460239, "lightweight_path_ms": 235.2761715883389, "savings_ms": 5543.2494438719, "savings_pct": 95.9, "unit": "ms/hr"}, {"metric": "Per-day monitoring overhead (24h)", "heavy_path_ms": 138684.61477104574, "lightweight_path_ms": 5646.628118120134, "savings_ms": 133037.9866529256, "savings_pct": 95.9, "unit": "ms/day"}, {"metric": "Unnecessary NVML calls per second", "heavy_path_value": 7, "lightweight_path_value": 0, "savings_value": 7, "unit": "calls/s"}, {"metric": "Unnecessary NVML calls per hour (on 1 GPU)", "heavy_path_value": 25200, "lightweight_path_value": 0, "savings_value": 25200, "unit": "calls/hr"}], "result": ""}
{"timestamp": "2026-07-02T03:54:32.856813+00:00", "mode": "all", "hostname": "Mac.lan", "gpu_backend": "simulated (1 GPU)", "gpu_count_real": 0, "simulated": true, "call_breakdown": [{"call_name": "nvmlDeviceGetMemoryInfo", "used_by_monitor": false, "simulated_latency_us": 50}, {"call_name": "nvmlDeviceGetTemperature", "used_by_monitor": false, "simulated_latency_us": 40}, {"call_name": "nvmlDeviceGetPowerUsage", "used_by_monitor": false, "simulated_latency_us": 45}, {"call_name": "nvmlDeviceGetTotalEnergyConsumption", "used_by_monitor": false, "simulated_latency_us": 40}, {"call_name": "nvmlDeviceGetUtilizationRates", "used_by_monitor": true, "simulated_latency_us": 50}, {"call_name": "nvmlDeviceGetComputeMode", "used_by_monitor": false, "simulated_latency_us": 35}, {"call_name": "nvmlDeviceGetComputeRunningProcesses", "used_by_monitor": false, "simulated_latency_us": 500}, {"call_name": "nvmlDeviceGetGraphicsRunningProcesses", "used_by_monitor": false, "simulated_latency_us": 500}], "method_benchmarks": [{"method": "get_gpu_details", "gpu_count": 1, "nvml_calls_per_second": 8, "nvml_calls_unused_per_second": 7, "latency_per_call_ms": {"count": 200, "min_ms": 1.4970839984016493, "max_ms": 1.756750003551133, "mean_ms": 1.6207081100947107, "p50_ms": 1.6159379993041512, "p95_ms": 1.666629193641711}, "latency_per_second_ms": 1.6159379993041512}, {"method": "get_gpu_utilization_list", "gpu_count": 1, "nvml_calls_per_second": 1, "nvml_calls_unused_per_second": 0, "latency_per_call_ms": {"count": 200, "min_ms": 0.062167004216462374, "max_ms": 0.13416699948720634, "mean_ms": 0.06766192989744013, "p50_ms": 0.0667294989398215, "p95_ms": 0.07051039865473285}, "latency_per_second_ms": 0.0667294989398215}], "projections": [{"metric": "Per-second monitoring overhead", "heavy_path_ms": 1.6159379993041512, "lightweight_path_ms": 0.0667294989398215, "savings_ms": 1.5492085003643297, "savings_pct": 95.9, "unit": "ms/s"}, {"metric": "Per-minute monitoring overhead", "heavy_path_ms": 96.95627995824907, "lightweight_path_ms": 4.00376993638929, "savings_ms": 92.95251002185978, "savings_pct": 95.9, "unit": "ms/min"}, {"metric": "Per-hour monitoring overhead", "heavy_path_ms": 5817.376797494944, "lightweight_path_ms": 240.2261961833574, "savings_ms": 5577.150601311587, "savings_pct": 95.9, "unit": "ms/hr"}, {"metric": "Per-day monitoring overhead (24h)", "heavy_path_ms": 139617.04313987866, "lightweight_path_ms": 5765.428708400577, "savings_ms": 133851.61443147808, "savings_pct": 95.9, "unit": "ms/day"}, {"metric": "Unnecessary NVML calls per second", "heavy_path_value": 7, "lightweight_path_value": 0, "savings_value": 7, "unit": "calls/s"}, {"metric": "Unnecessary NVML calls per hour (on 1 GPU)", "heavy_path_value": 25200, "lightweight_path_value": 0, "savings_value": 25200, "unit": "calls/hr"}], "result": ""}

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.70%. Comparing base (3161c53) to head (c6866c5).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1264      +/-   ##
==========================================
+ Coverage   89.64%   89.70%   +0.05%     
==========================================
  Files          48       48              
  Lines        4771     4778       +7     
==========================================
+ Hits         4277     4286       +9     
+ Misses        494      492       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@benoit-cty benoit-cty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, I've added a small commit to fix the tests.

@benoit-cty
benoit-cty merged commit 68e0f71 into mlco2:master Jul 19, 2026
13 checks passed
@vishali-mp

Copy link
Copy Markdown
Contributor Author

Thank you @benoit-cty! really appreciate you following up with the PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance improvement: Avoid unnecessary GPU detail queries in _monitor_power()

2 participants