mirror of https://github.com/razor-ai/soup.git
127 lines
3.9 KiB
Python
127 lines
3.9 KiB
Python
"""v0.44.0 Part A — `soup monitor` GPU live-monitor command.
|
|
|
|
Renders a Rich panel with one row per detected GPU: Util / Temp / VRAM /
|
|
Power. Polls `nvidia-smi` (Linux/Windows/CUDA) at the configured refresh
|
|
rate. Apple Silicon variant is a stub note in v0.44.0.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
import typer
|
|
from rich.console import Console
|
|
from rich.live import Live
|
|
from rich.markup import escape
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
|
|
from soup_cli.utils.gpu_monitor import (
|
|
GpuSample,
|
|
detect_apple_silicon,
|
|
query_nvidia_smi,
|
|
)
|
|
|
|
console = Console()
|
|
|
|
|
|
def _format_pct(value: float | None) -> str:
|
|
return "—" if value is None else f"{value:5.1f}%"
|
|
|
|
|
|
def _format_mb(value: float | None) -> str:
|
|
return "—" if value is None else f"{value:7.0f} MB"
|
|
|
|
|
|
def _format_temp(value: float | None) -> str:
|
|
return "—" if value is None else f"{value:4.0f}°C"
|
|
|
|
|
|
def _format_power(value: float | None) -> str:
|
|
return "—" if value is None else f"{value:5.1f} W"
|
|
|
|
|
|
def _build_table(samples: list[GpuSample]) -> Table:
|
|
table = Table(show_header=True, header_style="bold cyan")
|
|
table.add_column("GPU", justify="right")
|
|
table.add_column("Name", overflow="fold")
|
|
table.add_column("Util", justify="right")
|
|
table.add_column("Mem Util", justify="right")
|
|
table.add_column("VRAM Used", justify="right")
|
|
table.add_column("VRAM Total", justify="right")
|
|
table.add_column("Temp", justify="right")
|
|
table.add_column("Power", justify="right")
|
|
for sample in samples:
|
|
table.add_row(
|
|
str(sample.index),
|
|
escape(sample.name),
|
|
_format_pct(sample.util_gpu_pct),
|
|
_format_pct(sample.util_mem_pct),
|
|
_format_mb(sample.mem_used_mb),
|
|
_format_mb(sample.mem_total_mb),
|
|
_format_temp(sample.temp_c),
|
|
_format_power(sample.power_w),
|
|
)
|
|
return table
|
|
|
|
|
|
def monitor(
|
|
refresh: float = typer.Option(
|
|
2.0,
|
|
"--refresh",
|
|
"-r",
|
|
help="Refresh interval in seconds (0.25 to 30).",
|
|
),
|
|
once: bool = typer.Option(
|
|
False,
|
|
"--once",
|
|
help="Print one snapshot and exit (skip the live panel).",
|
|
),
|
|
) -> None:
|
|
"""Live GPU monitor: Util / Temp / VRAM / Power per GPU.
|
|
|
|
Requires nvidia-smi on PATH. On Apple Silicon use Activity Monitor or
|
|
powermetrics — full Apple Silicon support lands in v0.44.1.
|
|
"""
|
|
if not (0.25 <= refresh <= 30.0):
|
|
console.print("[red]--refresh must be in [0.25, 30][/]")
|
|
raise typer.Exit(code=2)
|
|
if detect_apple_silicon():
|
|
console.print(
|
|
"[yellow]Apple Silicon detected — `soup monitor` is "
|
|
"Apple-Silicon-aware in v0.44.1.[/]\n"
|
|
"Use Activity Monitor → Window → GPU History for now."
|
|
)
|
|
ok, samples = query_nvidia_smi()
|
|
if not ok:
|
|
console.print(
|
|
"[yellow]nvidia-smi not found or returned non-zero. "
|
|
"Install NVIDIA drivers + CUDA toolkit, or run on a GPU host.[/]"
|
|
)
|
|
raise typer.Exit(code=1)
|
|
if once or not samples:
|
|
console.print(Panel(_build_table(samples), title="Soup GPU Monitor"))
|
|
return
|
|
with Live(
|
|
Panel(_build_table(samples), title="Soup GPU Monitor"),
|
|
refresh_per_second=max(1.0, 1.0 / refresh),
|
|
screen=False,
|
|
) as live:
|
|
try:
|
|
while True:
|
|
time.sleep(refresh)
|
|
ok, fresh = query_nvidia_smi()
|
|
if not ok:
|
|
live.update(
|
|
Panel(
|
|
"[yellow]nvidia-smi unavailable[/]",
|
|
title="Soup GPU Monitor",
|
|
)
|
|
)
|
|
continue
|
|
live.update(
|
|
Panel(_build_table(fresh), title="Soup GPU Monitor")
|
|
)
|
|
except KeyboardInterrupt:
|
|
console.print("[dim]exit[/]")
|