SGVR GPU / Simple GPU monitor for the lab's MLXP H200 nodes. Check GPU ownership, utilization, storage, and usage history before launching another Kubernetes pod.
- Every GPU process is attributed to its pod and owner, not just a PID.
- One client can survey both H200 nodes with
sgpu --all once, or pick a node withsgpu -n 1/sgpu -n 2. - In-pod TUI via
kubectl exec -it: smooth refresh, scrolling, sorting, owner filtering, and a stats screen. - Usage stats 24/7: a cluster pulse (KST utilization/VRAM sparklines, utilization zones, observed compute windows), owner momentum, GPU-hours, awards, heatmaps, and idle-allocation warnings.
- Shared storage (pv-01/pv-02) usage at a glance.
- Monitor pod is read-only, always-on, and requests no GPU.
Version history: CHANGELOG.md.
uv tool install sgpu # persistent install via uv (recommended)
pipx install sgpu # or pipx
pip install sgpu # or plain pip (needs pip; WSL/Ubuntu often lacks it)
uvx sgpu # or run once without installingUpgrade with the tool you installed with — uv tool upgrade sgpu,
pipx upgrade sgpu, or pip install -U sgpu. sgpu tells you the right one
when it detects your client is behind the server.
Needs kubectl with an MLXP kubeconfig (setup).
One kubeconfig covers both H200 servers - the two downloads
(sgvr-node-01/-02) share the same token and contexts, so either file
works for every node.
sgpu interactive TUI sgpu stats [days] usage report + awards
sgpu once one-shot dashboard sgpu apps processes + owners
sgpu watch [sec] dumb-terminal loop sgpu nvitop raw nvitop
sgpu pods|smi|gpustat|json|health|version|--help
MLXP has two H200 servers (p-sgvr-node-01, p-sgvr-node-02):
sgpu -n 1 once node-01 (shorthand for p-sgvr-node-01)
sgpu -n 2 once node-02
sgpu --all once survey both nodes at once (any text command)
sgpu --all stats ONE lab-wide merged report: combined leaderboard,
awards and heatmaps with a NODE column per owner
sgpu once uses your current kubectl context's namespace
In the dashboard TUI, n switches node 1/2 without relaunching. In the stats
screen, n cycles the current node, the other node, and LAB.
TUI keys:
j/k scroll
Tab switch pane
s sort
o owner filter
p pause
t stats screen
h/d/w/m stats axis: hour/day/week/month
a cycle stats axis
r refresh
q quit
Options: -n namespace, --pod, -r refresh, --no-color, --json
for stable agent-readable JSON on text commands.
Env: SGPU_NAMESPACE, SGPU_POD, SGPU_NO_UPDATE_CHECK=1 (silence the
upgrade nudge).
Agent-friendly examples:
sgpu -n 2 json --json
sgpu -n 2 pods --json
sgpu -n 2 apps --json
sgpu -n 2 stats 14 --json --scope lab
sgpu --all json --jsonThe monitor server is upgraded centrally (one image redeploy updates the
dashboard/stats UI for everyone — no client action needed). The client
(this pip package) only changes for client-side features (-n, --all,
reconnect). When your client falls behind the server, sgpu shows a yellow
↑ update available banner in the TUI and a one-line hint after text
commands — just run pip install -U sgpu (or uv tool upgrade sgpu).
Anyone with kubectl access can use the monitor pod without installing
sgpu.
kubectl exec -it -n p-sgvr-node-01 sangmin-gpu-monitor -- python3 /opt/gpu-monitor/tui.py
kubectl exec -n p-sgvr-node-01 sangmin-gpu-monitor -- curl -fsS http://127.0.0.1:8080/tableEndpoints on :8080:
/table /apps /json /stats /pods /smi /topo /gpustat /health /version
/stats/files /stats/raw?date=YYYYMMDD
Text endpoints support ?color=1&cols=N&ascii=1.
SGPU samples every 60 seconds around the clock into raw JSONL, gzips and rolls
up daily summaries, and stores the results on the shared volume at
pv-01/sangmin/sgpu (~0.1 MB/day/node gzipped). The interval is set by
SGPU_SAMPLE_INTERVAL; the aggregator preserves a compatible daily median
while recognizing confirmed sustained cadence changes within a day, so even a
large 15-second to 900-second switch neither under-credits normal samples nor
fragments Flow. Missing telemetry remains bounded and breaks a Flow window.
Retention defaults to 365 days and is capped at 2 GB. sgpu stats 30 starts
with a Cluster pulse: compact KST utilization and VRAM sparklines, a
quiet/light/work/hot utilization-zone distribution, and observed compute
windows. It then shows the leaderboard, awards, owner
momentum, daily activity, and KST hour heatmaps. The curses stats screen keeps
the pulse above the leaderboard/grid; on a short terminal it drops awards
before it drops the pulse or grid.
Device telemetry is derived from the util, mem, and mem_total fields
already recorded in raw sample v1. Daily rollup v2 stores only weighted sums,
24 KST buckets, ten utilization buckets, and window counters, so rendering
does not re-scan raw history. On first access, an older rollup is rebuilt once
from its preserved raw/gz file and atomically replaced. If raw history has
already been removed, its owner totals remain valid but the pulse marks
partial coverage (by node-day in LAB scope, so a same-date peer cannot
mask a missing node) or stays hidden when no device telemetry is available,
rather than treating unknown time as 0% utilization. Contiguous Flow windows
are stitched across UTC rollup boundaries. A multi-node LAB pulse labels its
exact aggregate as node compute windows and longest per-node window;
it does not present that as a global cross-node time-window union.
The pulse is cluster-wide telemetry, not a per-owner estimate. Therefore an
owner-filtered /stats?owner=... result intentionally omits it; use that view
for the owner's GPU-hours, efficiency, streak, and allocations.
The monitor pod must stay running for stats to accumulate. It is designed to do that with tini init,
restartPolicy: Always, and no GPU allocation.
sgpu stats [days] (or sgpu --all stats for the whole lab) ranks everyone
by GPU-hours and hands out playful badges. Example:
SGPU usage report — last 7 days — all nodes (node-01+node-02)
data: 7 days, coverage 168.0h
Cluster pulse
KST UTIL ▁▁▂▃▄▆▇█▇▆▄▃▂▂▃▄▆▇▆▄▃▂▁▁ avg 54% hot 31%
VRAM ▁▂▂▃▄▅▆▇▇▆▅▄▃▃▄▅▆▇▇▆▄▃▂▁ avg 47%
UTIL mix quiet 23% light 18% work 28% hot 31%
Flow 12 node compute windows · longest 9h18m per-node (any GPU >= 50%)
Awards
🏆 Best researcher: jiwon — 92.4 effective GPU-h (81% avg over 114.1 GPU-h)
⚡ Power user: jiwon — 114.1 GPU-h
🎯 Sharpshooter: minseo — 97% avg SM over 40.2 GPU-h
🧠 Memory heavyweight: haeun — 139.7 GiB peak
🦉 Night owl: doyun — 71% of activity in KST 0-5h
💤 Most headroom: sangho — 22% avg util over 48 GPU-h (free speedup waiting)
🪑 Seat warmer: taemin — 9.8 idle GPU-h allocated
Leaderboard
# OWNER NODE GPU-H EFF-H AVG-SM% AVG-UTIL% PEAK-MEM ALLOC-H IDLE-H IDLE%
1. jiwon node-01 114.1 92.4 81 81 81.1 114.5 0.4 0
2. sangho node-02 48.0 10.6 22 30 129.2 50.1 2.1 4
3. minseo node-02 40.2 39.0 97 97 62.9 40.2 0.0 0
4. haeun node-01 37.9 27.2 72 72 139.7 39.4 1.6 4
5. taemin node-01 11.6 0.1 7 25 139.0 21.4 9.8 46
Momentum
jiwon 7d streak · 7/7d · 81% eff | minseo 5d streak · 6/7d · 97% eff
Ranking is by GPU-H (GPU-hours). Each owner holds at most 3 badges.
| Badge | Awarded to | Threshold |
|---|---|---|
| 🏆 Best researcher | Most effective GPU-hours (GPU-H × avg util) — busiest and actually computing |
≥40% avg util, ≥1 GPU-H |
| ⚡ Power user | Most GPU-hours overall | ≥1 GPU-H |
| 🎯 Sharpshooter | Highest average SM% — squeezes the most out of each GPU |
≥2 GPU-H |
| 🧠 Memory heavyweight | Highest peak GPU memory used | ≥32 GiB |
| 🦉 Night owl | Biggest share of own activity in KST 00–05h | ≥1 GPU-H in window |
| 💤 Most headroom | Lowest avg util among heavy users — a free speedup is waiting | ≥4 GPU-H and util <40% |
| 🪑 Seat warmer | Most idle allocated GPU-hours (holds GPUs without using them) | ≥2 idle GPU-H (needs the pod-allocation view) |
Column meanings (GPU-H, EFF-H, SM%, PEAK-MEM, ALLOC-H, IDLE-H, …)
are in What the numbers mean. The NODE column
(lab-wide view) shows each person's home node, or both if they split their
work across nodes.
Names above are illustrative. Press
?in the TUI for the same reference in-app.
Press ? in the TUI for this same reference in-app.
| Column | Meaning |
|---|---|
[N/M free +K idle] |
Summary line under the GPU table: N = GPUs a new pod could request right now (total minus pods' GPU requests; green >0, red 0). +K idle (yellow) = GPUs reserved by Running pods that aren't using them — physically idle and reclaimable if the holder releases them. ~N/M = process-based estimate (pod API unavailable). |
UTIL |
Whole-GPU utilization %: share of time the GPU did any work (NVML/nvidia-smi). |
SM% |
Per-process SM (streaming-multiprocessor) activity — how hard that process drove the GPU cores. |
MEM / PEAK-MEM |
GPU memory in use / highest seen (each H200 ≈ 140 GiB). |
GPU-H |
GPU-hours: time integrated over how many GPUs an owner had processes on. |
EFF-H |
Effective GPU-hours = GPU-H × avg util (compute actually done, not just held). |
ALLOC-H |
Allocated GPU-hours from pods' nvidia.com/gpu requests. |
IDLE-H / IDLE% |
Allocated but no process running — a wasted reservation. |
Cluster pulse |
Device-level (not owner-attributed) KST utilization and VRAM rhythm. UTIL mix is weighted GPU time in quiet 0–9%, light 10–39%, work 40–69%, and hot 70–100% zones. |
Flow |
Contiguous observed windows in one node's telemetry where any sampled GPU was at least 50% utilized; it is a cadence signal, not job-start accounting. Missing telemetry breaks a window. In LAB scope, node compute windows is the exact sum of per-node windows and longest per-node is their maximum — neither claims a global cross-node time union. |
Momentum |
Each owner's active-day count, current streak, and effective-utilization profile over observed days only, so a monitor outage is not counted as personal inactivity. |
REQ / ACT |
(pods table) GPUs a pod requested vs. actively using right now. |
POWER / TEMP |
Power draw / cap, and temperature. |
STORAGE |
Shared pv-01/pv-02 volume usage (used / total / free). |
UTIL vs SM%:
UTILis the whole card being busy at all;SM%is how saturated the compute cores are for a specific process. High UTIL with low SM% usually means the GPU is waiting on data (I/O, small batches), not computing hard — that's whereEFF-Hand the "Most headroom" award come in.
The monitor runs from a public image (docker.io/alex6095/sgpu-monitor),
so no registry login or pull secret is needed. Deploy one pod per node -
always pass -n (a bare kubectl apply would hit your current context's
namespace):
# For each node namespace (p-sgvr-node-01 and/or p-sgvr-node-02):
kubectl apply -n p-sgvr-node-01 -f k8s/gpu-monitor.yaml
kubectl wait --for=condition=Ready pod/sangmin-gpu-monitor -n p-sgvr-node-01 --timeout=180sPods are immutable, so to roll out a new image: kubectl delete pod sangmin-gpu-monitor -n <ns> then apply again.
Optional, for the pod-allocation view and idle stats (kubelet syncs it in
within a minute, no restart; use the same -n):
kubectl -n p-sgvr-node-01 create secret generic sgpu-kubeconfig --from-file=config=$HOME/.kube/configAnyone with exec access to the monitor pod can read that token. This is fine inside a trusting lab namespace; use a least-privileged kubeconfig.
Maintainer: build & publish the image
docker build -f docker/Dockerfile.gpu-monitor -t docker.io/alex6095/sgpu-monitor:X.Y.Z .
docker push docker.io/alex6095/sgpu-monitor:X.Y.Z # keep the repo publicBump the tag on every change - never repush a tag (imagePullPolicy: IfNotPresent would keep a node's cached layer). The NVIDIA driver
(580.126.16) is pinned in the image; if a node runs a different driver the
server degrades to source=nvidia-smi or /health 503 instead of crashing.
mkdir -p ~/.local/bin ~/.kube
V=$(curl -fsSL https://dl.k8s.io/release/stable.txt)
curl -fsSL -o ~/.local/bin/kubectl "https://dl.k8s.io/release/${V}/bin/linux/amd64/kubectl" && chmod +x ~/.local/bin/kubectl
cp /path/to/sgvr-node-01-kubeconfig.yaml ~/.kube/config && chmod 600 ~/.kube/config
# Either node's kubeconfig works for both - pick the node with `sgpu -n 1|2`.
kubectl get pods -n p-sgvr-node-02 # connectivity testSGPU_MOCK=1 python3 tools/gpu-monitor/server.py # full pipeline, no GPU needed
SGPU_MOCK=1 python3 tools/gpu-monitor/tui.py
python3 -m unittest discover -s tests
python3 tools/render_readme_images.py # synthetic public screenshots
SGPU_README_LIVE=1 python3 tools/render_readme_images.py # optional live captureHow it works: sgpu is a thin Python client. It uses kubectl exec to reach
the monitor pod, where server.py renders the dashboard. Process-to-pod
attribution reads /proc/<pid>/environ (HOSTNAME = pod name), and owner is
inferred from the pod-name prefix.
Known limits: pods overriding spec.hostname and MPS may show as ?.
Troubleshooting:
TUI reconnects after an EOF -> sgpu checks the actual pod UID/Ready state
and server health; a transient stream loss
uses bounded backoff instead of assuming a
rollout
disconnected TUI left in the pod -> current monitor images stop it after 45s
without terminal-output progress, avoiding
orphan polling processes
broken terminal after dropped TUI -> reset (older clients)
frozen TUI -> rerun sgpu
garbled bars -> Windows Terminal or --no-color