sgtop 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sgtop-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HarrytheOrange
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sgtop-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: sgtop
3
+ Version: 0.1.0
4
+ Summary: btop-style terminal monitor for a live SGLang deployment
5
+ Author: HarrytheOrange
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/HarrytheOrange/sgtop
8
+ Project-URL: Issues, https://github.com/HarrytheOrange/sgtop/issues
9
+ Keywords: sglang,llm,monitoring,tui,gpu,prometheus
10
+ Classifier: Environment :: Console :: Curses
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: System :: Monitoring
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # sgtop
22
+
23
+ A [btop](https://github.com/aristocratos/btop)-style terminal dashboard for a live [SGLang](https://github.com/sgl-project/sglang) deployment — concurrency, KV-cache headroom, GPU utilization, and (optionally) per-request latency, all in one screen.
24
+
25
+ ![sgtop screenshot](docs/screenshot.png)
26
+
27
+ Not affiliated with the SGLang project. It's a small, independent, read-only sidecar: it never touches your inference traffic, it only reads sglang's own log file, `nvidia-smi`, and (if enabled) sglang's Prometheus `/metrics` endpoint.
28
+
29
+ ## Why
30
+
31
+ `nvtop` tells you a GPU is at 98% utilization. It doesn't tell you whether your sglang server can actually take more concurrent requests, whether one data-parallel replica is starving while another is idle, or what your real TTFT looks like. `sgtop` answers those instead:
32
+
33
+ - **Concurrency vs. capacity** — running requests / the actual `max_running_requests` sglang computed at startup, not a guess.
34
+ - **KV-cache headroom** — tokens in use / total KV budget, per replica.
35
+ - **Per-DP-replica breakdown** — if you run `--dp-size > 1`, each replica gets its own panel, so an imbalanced load-balancing setup is obvious at a glance instead of hidden in an aggregate number.
36
+ - **Latency percentiles** — TTFT / end-to-end / queue-time p50/p90/p99 over a rolling 5-minute window, once you turn on `--enable-metrics` on the sglang side.
37
+ - **GPU panel** — utilization, memory, temperature, power, right below the request-level view.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pipx install sgtop # recommended — isolated, puts `sgtop` on your PATH
43
+ # or
44
+ pip install --user sgtop
45
+ ```
46
+
47
+ Requires Python ≥ 3.9 on Linux or macOS. No dependencies — it's pure standard library (`curses` + `urllib`), so there's nothing else to install.
48
+
49
+ From source:
50
+
51
+ ```bash
52
+ git clone https://github.com/HarrytheOrange/sgtop
53
+ cd sgtop
54
+ pip install -e .
55
+ ```
56
+
57
+ ## Quick start
58
+
59
+ `sgtop` is a client. It needs something to talk to: `sgtop-server`, a small sidecar that tails sglang's log file and exposes a JSON API. Run it on the same machine as sglang (or anywhere with network access to it):
60
+
61
+ ```bash
62
+ # 1. Launch sglang, redirecting its output to a file sgtop-server can tail:
63
+ python -m sglang.launch_server --model-path ... --dp-size 2 --enable-metrics > server.log 2>&1 &
64
+
65
+ # 2. Start the sidecar, pointing it at that log file:
66
+ sgtop-server --log server.log --service-port 30000 --port 30001 &
67
+
68
+ # 3. Watch it:
69
+ sgtop --port 30001
70
+ ```
71
+
72
+ `sgtop-server` also serves a plain HTML version of the same data at `http://<host>:30001/` — handy if you want to glance at it from a browser instead of a terminal, or point it at a machine other than the one you're `ssh`'d into:
73
+
74
+ ```bash
75
+ sgtop --host 10.0.0.5 --port 30001
76
+ ```
77
+
78
+ ## `--enable-metrics` (optional, recommended)
79
+
80
+ Without it, `sgtop` still shows concurrency, KV-cache usage, throughput and GPU stats — all parsed from sglang's own log lines, no extra flags needed.
81
+
82
+ Add `--enable-metrics` to your `launch_server` command and the LATENCY panel comes alive with TTFT, end-to-end latency, per-replica queue time, and prefix-cache hit rate, scraped from sglang's Prometheus endpoint. `sgtop-server` diffs consecutive scrapes itself to give you a real rolling-window p50/p90/p99 instead of an all-time average — Prometheus histograms are cumulative counters, so a naive read would just show a number that keeps drifting toward whatever the very first few requests looked like.
83
+
84
+ ## Keybindings
85
+
86
+ `q` to quit. That's it — it's a dashboard, not an editor.
87
+
88
+ ## How it works
89
+
90
+ - `sgtop-server` tails the log file sglang writes when you redirect its stdout/stderr, extracting the periodic `Decode batch` / `Prefill batch` lines each DP replica prints, plus the one-time `max_total_num_tokens=... max_running_requests=...` line printed at startup. It samples `nvidia-smi` on an interval, and if `--enable-metrics` is on, scrapes `/metrics` and keeps a short rolling window of histogram deltas for latency percentiles. All of it is exposed as JSON at `/api/status`.
91
+ - `sgtop` polls that JSON and renders it with `curses` — gradient meters, sparkline history, boxed panels, degrading gracefully to a 3-color/16-color palette on terminals without 256-color support.
92
+
93
+ Both are plain Python, no compiled extensions, so `pipx install` is instant.
94
+
95
+ ## License
96
+
97
+ MIT — see [LICENSE](LICENSE).
sgtop-0.1.0/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # sgtop
2
+
3
+ A [btop](https://github.com/aristocratos/btop)-style terminal dashboard for a live [SGLang](https://github.com/sgl-project/sglang) deployment — concurrency, KV-cache headroom, GPU utilization, and (optionally) per-request latency, all in one screen.
4
+
5
+ ![sgtop screenshot](docs/screenshot.png)
6
+
7
+ Not affiliated with the SGLang project. It's a small, independent, read-only sidecar: it never touches your inference traffic, it only reads sglang's own log file, `nvidia-smi`, and (if enabled) sglang's Prometheus `/metrics` endpoint.
8
+
9
+ ## Why
10
+
11
+ `nvtop` tells you a GPU is at 98% utilization. It doesn't tell you whether your sglang server can actually take more concurrent requests, whether one data-parallel replica is starving while another is idle, or what your real TTFT looks like. `sgtop` answers those instead:
12
+
13
+ - **Concurrency vs. capacity** — running requests / the actual `max_running_requests` sglang computed at startup, not a guess.
14
+ - **KV-cache headroom** — tokens in use / total KV budget, per replica.
15
+ - **Per-DP-replica breakdown** — if you run `--dp-size > 1`, each replica gets its own panel, so an imbalanced load-balancing setup is obvious at a glance instead of hidden in an aggregate number.
16
+ - **Latency percentiles** — TTFT / end-to-end / queue-time p50/p90/p99 over a rolling 5-minute window, once you turn on `--enable-metrics` on the sglang side.
17
+ - **GPU panel** — utilization, memory, temperature, power, right below the request-level view.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pipx install sgtop # recommended — isolated, puts `sgtop` on your PATH
23
+ # or
24
+ pip install --user sgtop
25
+ ```
26
+
27
+ Requires Python ≥ 3.9 on Linux or macOS. No dependencies — it's pure standard library (`curses` + `urllib`), so there's nothing else to install.
28
+
29
+ From source:
30
+
31
+ ```bash
32
+ git clone https://github.com/HarrytheOrange/sgtop
33
+ cd sgtop
34
+ pip install -e .
35
+ ```
36
+
37
+ ## Quick start
38
+
39
+ `sgtop` is a client. It needs something to talk to: `sgtop-server`, a small sidecar that tails sglang's log file and exposes a JSON API. Run it on the same machine as sglang (or anywhere with network access to it):
40
+
41
+ ```bash
42
+ # 1. Launch sglang, redirecting its output to a file sgtop-server can tail:
43
+ python -m sglang.launch_server --model-path ... --dp-size 2 --enable-metrics > server.log 2>&1 &
44
+
45
+ # 2. Start the sidecar, pointing it at that log file:
46
+ sgtop-server --log server.log --service-port 30000 --port 30001 &
47
+
48
+ # 3. Watch it:
49
+ sgtop --port 30001
50
+ ```
51
+
52
+ `sgtop-server` also serves a plain HTML version of the same data at `http://<host>:30001/` — handy if you want to glance at it from a browser instead of a terminal, or point it at a machine other than the one you're `ssh`'d into:
53
+
54
+ ```bash
55
+ sgtop --host 10.0.0.5 --port 30001
56
+ ```
57
+
58
+ ## `--enable-metrics` (optional, recommended)
59
+
60
+ Without it, `sgtop` still shows concurrency, KV-cache usage, throughput and GPU stats — all parsed from sglang's own log lines, no extra flags needed.
61
+
62
+ Add `--enable-metrics` to your `launch_server` command and the LATENCY panel comes alive with TTFT, end-to-end latency, per-replica queue time, and prefix-cache hit rate, scraped from sglang's Prometheus endpoint. `sgtop-server` diffs consecutive scrapes itself to give you a real rolling-window p50/p90/p99 instead of an all-time average — Prometheus histograms are cumulative counters, so a naive read would just show a number that keeps drifting toward whatever the very first few requests looked like.
63
+
64
+ ## Keybindings
65
+
66
+ `q` to quit. That's it — it's a dashboard, not an editor.
67
+
68
+ ## How it works
69
+
70
+ - `sgtop-server` tails the log file sglang writes when you redirect its stdout/stderr, extracting the periodic `Decode batch` / `Prefill batch` lines each DP replica prints, plus the one-time `max_total_num_tokens=... max_running_requests=...` line printed at startup. It samples `nvidia-smi` on an interval, and if `--enable-metrics` is on, scrapes `/metrics` and keeps a short rolling window of histogram deltas for latency percentiles. All of it is exposed as JSON at `/api/status`.
71
+ - `sgtop` polls that JSON and renders it with `curses` — gradient meters, sparkline history, boxed panels, degrading gracefully to a 3-color/16-color palette on terminals without 256-color support.
72
+
73
+ Both are plain Python, no compiled extensions, so `pipx install` is instant.
74
+
75
+ ## License
76
+
77
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sgtop"
7
+ version = "0.1.0"
8
+ description = "btop-style terminal monitor for a live SGLang deployment"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ dependencies = []
13
+ authors = [{ name = "HarrytheOrange" }]
14
+ keywords = ["sglang", "llm", "monitoring", "tui", "gpu", "prometheus"]
15
+ classifiers = [
16
+ "Environment :: Console :: Curses",
17
+ "Intended Audience :: Science/Research",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Operating System :: MacOS",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: System :: Monitoring",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/HarrytheOrange/sgtop"
26
+ Issues = "https://github.com/HarrytheOrange/sgtop/issues"
27
+
28
+ [project.scripts]
29
+ sgtop = "sgtop.cli:main"
30
+ sgtop-server = "sgtop.server:main"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
sgtop-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ import curses
4
+ import time
5
+ from datetime import datetime
6
+
7
+ from . import theme as th
8
+ from .data import bucket_series, fetch_status
9
+
10
+ SPARK_WIDTH = 40
11
+ WINDOW_S = 300 # matches the dashboard's own history retention
12
+
13
+
14
+ def human_uptime(seconds: float | None) -> str:
15
+ if not seconds:
16
+ return "—"
17
+ seconds = int(seconds)
18
+ h, rem = divmod(seconds, 3600)
19
+ m, _ = divmod(rem, 60)
20
+ d, h = divmod(h, 24)
21
+ return f"{d}d {h}h {m}m" if d else f"{h}h {m}m"
22
+
23
+
24
+ def fmt_secs(value: float | None) -> str:
25
+ if value is None:
26
+ return " n/a"
27
+ return f"{value*1000:4.0f}ms" if value < 1 else f"{value:5.2f}s"
28
+
29
+
30
+ class App:
31
+ def __init__(self, stdscr, host: str, port: int, interval: float):
32
+ self.stdscr = stdscr
33
+ self.host = host
34
+ self.port = port
35
+ self.interval = interval
36
+ self.theme = th.Theme()
37
+ self.last_status: dict | None = None
38
+
39
+ def run(self) -> None:
40
+ curses.curs_set(0)
41
+ self.theme.setup()
42
+ self.stdscr.nodelay(True)
43
+ self.stdscr.timeout(int(self.interval * 1000))
44
+ while True:
45
+ key = self.stdscr.getch()
46
+ if key in (ord("q"), 27):
47
+ return
48
+ status = fetch_status(self.host, self.port)
49
+ if status is not None:
50
+ self.last_status = status
51
+ self.render(self.last_status)
52
+
53
+ # -- rendering -----------------------------------------------------
54
+ def render(self, status: dict | None) -> None:
55
+ stdscr = self.stdscr
56
+ stdscr.erase()
57
+ h, w = stdscr.getmaxyx()
58
+ boxw = max(50, min(w - 2, 110))
59
+
60
+ self._header(0, boxw, status)
61
+
62
+ if status is None or not status.get("service_up"):
63
+ th.safe_addstr(stdscr, 3, 2, "waiting for sglang-dashboard at "
64
+ f"{self.host}:{self.port} ...", curses.color_pair(th.PAIR_BAD))
65
+ self._footer(h - 1, boxw)
66
+ stdscr.refresh()
67
+ return
68
+
69
+ now = status["now"]
70
+ caps = status.get("caps", {})
71
+ active = status.get("active", {})
72
+ hist_decode = status.get("history", {}).get("decode", [])
73
+ hist_gpu = status.get("history", {}).get("gpu", [])
74
+
75
+ y = 2
76
+ total_running = total_cap = total_tok = total_tok_cap = 0
77
+ for i, dp in enumerate(sorted(set(active) | set(caps))):
78
+ pair = th.PAIR_TITLE_CYAN if i % 2 == 0 else th.PAIR_TITLE_MAGENTA
79
+ y = self._dp_panel(y, boxw, dp, pair, active.get(dp, {}), caps.get(dp, {}), hist_decode, now)
80
+ cap = caps.get(dp, {})
81
+ a = active.get(dp, {})
82
+ total_running += a.get("running", 0)
83
+ total_cap += cap.get("max_running", 0)
84
+ total_tok += a.get("tokens", 0)
85
+ total_tok_cap += cap.get("max_tokens", 0)
86
+
87
+ y = self._total_panel(y, boxw, total_running, total_cap, status["summary"])
88
+ y = self._latency_panel(y, boxw, status.get("prom", {}))
89
+ y = self._gpu_panel(y, boxw, status.get("gpus", []), hist_gpu, now)
90
+
91
+ errors = status.get("errors", [])
92
+ if errors:
93
+ th.safe_addstr(stdscr, y, 0, f"⚠ {len(errors)} error(s) in the last 5min — see logs/deploy.log",
94
+ curses.color_pair(th.PAIR_BAD))
95
+ y += 1
96
+
97
+ self._footer(h - 1, boxw)
98
+ stdscr.refresh()
99
+
100
+ def _header(self, y: int, boxw: int, status: dict | None) -> None:
101
+ stdscr = self.stdscr
102
+ up = bool(status and status.get("service_up"))
103
+ dot = "●" if up else "○"
104
+ dot_pair = th.PAIR_TITLE_CYAN if up else th.PAIR_BAD
105
+ state = f"online · pid {status.get('pid')}" if up and status else "offline"
106
+ uptime = human_uptime(status.get("uptime")) if status else "—"
107
+ clock = datetime.now().strftime("%H:%M:%S")
108
+
109
+ th.safe_addstr(stdscr, y, 0, "sgtop", curses.A_BOLD | curses.color_pair(th.PAIR_TITLE_CYAN))
110
+ th.safe_addstr(stdscr, y, 6, " SGLang concurrency monitor", curses.A_DIM)
111
+ right = f"{dot} {state} up {uptime} {clock}"
112
+ th.safe_addstr(stdscr, y, max(0, boxw - len(right)), dot, curses.color_pair(dot_pair))
113
+ th.safe_addstr(stdscr, y, max(0, boxw - len(right)) + 2, right[2:])
114
+
115
+ def _footer(self, y: int, boxw: int) -> None:
116
+ th.safe_addstr(self.stdscr, y, 0, "q quit", curses.A_DIM)
117
+
118
+ def _dp_panel(self, y: int, boxw: int, dp: str, title_pair: int, a: dict, cap: dict,
119
+ hist_decode: list, now: float) -> int:
120
+ stdscr = self.stdscr
121
+ h = 6
122
+ th.draw_box(stdscr, self.theme, y, 0, h, boxw, dp, title_pair)
123
+ inner_w = boxw - 4
124
+ running, queue, tokens = a.get("running", 0), a.get("queue", 0), a.get("tokens", 0)
125
+ max_running, max_tokens = cap.get("max_running", 0), cap.get("max_tokens", 0)
126
+
127
+ if max_running:
128
+ label = f"concurrency {running}/{max_running} ({running/max_running*100:4.1f}%) q={queue}"
129
+ bar_w = th.fit_meter_width(inner_w, label)
130
+ th.draw_meter(stdscr, self.theme, y + 1, 2, bar_w, running / max_running, label)
131
+ else:
132
+ th.safe_addstr(stdscr, y + 1, 2, "concurrency: cap unknown (waiting for startup log line)",
133
+ curses.color_pair(th.PAIR_DIM))
134
+
135
+ if max_tokens:
136
+ label = f"kv-cache {tokens}/{max_tokens} ({tokens/max_tokens*100:4.1f}%)"
137
+ bar_w = th.fit_meter_width(inner_w, label)
138
+ th.draw_meter(stdscr, self.theme, y + 2, 2, bar_w, tokens / max_tokens, label)
139
+
140
+ thr_series = bucket_series(hist_decode, "ts", lambda it: it["throughput"], now, WINDOW_S,
141
+ SPARK_WIDTH, filter_fn=lambda it: it.get("dp") == dp)
142
+ th.draw_sparkline(stdscr, y + 3, 2, thr_series, title_pair)
143
+ th.safe_addstr(stdscr, y + 3, 2 + SPARK_WIDTH + 2,
144
+ f"decode {a.get('throughput', 0):7.1f} tok/s (last 5min)")
145
+
146
+ th.safe_addstr(stdscr, y + 4, 2,
147
+ f"accept_len={a.get('accept_len', 0):.2f} accept_rate={a.get('accept_rate', 0)*100:5.1f}%")
148
+ return y + h + 1
149
+
150
+ def _total_panel(self, y: int, boxw: int, running: int, cap: int, summary: dict) -> int:
151
+ stdscr = self.stdscr
152
+ h = 4
153
+ th.draw_box(stdscr, self.theme, y, 0, h, boxw, "TOTAL", th.PAIR_TITLE_ORANGE)
154
+ inner_w = boxw - 4
155
+ if cap:
156
+ label = f"concurrency {running}/{cap} ({running/cap*100:4.1f}%)"
157
+ bar_w = th.fit_meter_width(inner_w, label)
158
+ th.draw_meter(stdscr, self.theme, y + 1, 2, bar_w, running / cap, label)
159
+ th.safe_addstr(stdscr, y + 2, 2,
160
+ f"req/min={summary.get('requests_1m', 0):<4d} req/5min={summary.get('requests_5m', 0):<5d} "
161
+ f"accept_rate(60s)={summary.get('accept_rate_60s', 0)*100:5.1f}%")
162
+ return y + h + 1
163
+
164
+ def _latency_panel(self, y: int, boxw: int, prom: dict) -> int:
165
+ """TTFT / E2E / queue-time / cache-hit — everything that needs sglang
166
+ started with --enable-metrics. TTFT and E2E have no per-DP label
167
+ (sglang aggregates them server-wide), queue-time and cache-hit do."""
168
+ stdscr = self.stdscr
169
+ h = 5
170
+ th.draw_box(stdscr, self.theme, y, 0, h, boxw, "LATENCY (--enable-metrics, 5min)", th.PAIR_TITLE_MAGENTA)
171
+
172
+ if not prom.get("enabled"):
173
+ th.safe_addstr(stdscr, y + 2, 2,
174
+ "not enabled — add --enable-metrics to serve517.sh and restart to see this",
175
+ curses.color_pair(th.PAIR_DIM))
176
+ return y + h + 1
177
+
178
+ latency = prom.get("latency", {})
179
+ queue_by_dp = prom.get("queue_by_dp", {})
180
+ cache_by_dp = prom.get("cache_hit_by_dp", {})
181
+
182
+ def stat_row(row: int, label: str, s: dict | None) -> None:
183
+ if not s:
184
+ th.safe_addstr(stdscr, row, 2, f"{label:<5} no samples yet", curses.color_pair(th.PAIR_DIM))
185
+ return
186
+ th.safe_addstr(stdscr, row, 2,
187
+ f"{label:<5} mean={fmt_secs(s['mean'])} p50={fmt_secs(s['p50'])} "
188
+ f"p90={fmt_secs(s['p90'])} p99={fmt_secs(s['p99'])} n={s['count']:.0f}")
189
+
190
+ stat_row(y + 1, "TTFT", latency.get("ttft"))
191
+ stat_row(y + 2, "E2E", latency.get("e2e"))
192
+
193
+ parts = []
194
+ for dp in sorted(set(queue_by_dp) | set(cache_by_dp)):
195
+ q = queue_by_dp.get(dp)
196
+ qtxt = f"p50={fmt_secs(q['p50'])} p90={fmt_secs(q['p90'])}" if q else "no samples"
197
+ ch = cache_by_dp.get(dp)
198
+ chtxt = f" cache-hit={ch*100:4.1f}%" if ch is not None else ""
199
+ parts.append(f"DP{dp} queue {qtxt}{chtxt}")
200
+ th.safe_addstr(stdscr, y + 3, 2, " ".join(parts))
201
+ return y + h + 1
202
+
203
+ def _gpu_panel(self, y: int, boxw: int, gpus: list, hist_gpu: list, now: float) -> int:
204
+ stdscr = self.stdscr
205
+ n = max(1, len(gpus))
206
+ h = n + 2
207
+ th.draw_box(stdscr, self.theme, y, 0, h, boxw, "GPUs", th.PAIR_TITLE_CYAN)
208
+ inner_w = boxw - 4
209
+ bar_w = min(24, max(6, inner_w - 55))
210
+ for i, g in enumerate(gpus):
211
+ row = y + 1 + i
212
+ label = f"GPU{g['index']}"
213
+ th.safe_addstr(stdscr, row, 2, label)
214
+ end_x = th.draw_meter(stdscr, self.theme, row, 2 + len(label) + 1, bar_w, g["util"] / 100,
215
+ f"{g['util']:5.1f}%")
216
+ th.safe_addstr(stdscr, row, end_x + 2,
217
+ f"mem={g['mem_used']:6.0f}/{g['mem_total']:6.0f}MiB "
218
+ f"{g['temp']:3.0f}C {g['power']:5.1f}W")
219
+ return y + h + 1
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import curses
5
+
6
+ from .app import App
7
+ from .data import DEFAULT_HOST, DEFAULT_PORT
8
+
9
+
10
+ def main() -> None:
11
+ ap = argparse.ArgumentParser(
12
+ description="btop-style terminal monitor for a live SGLang deployment. "
13
+ "Reads a running monitor_dashboard.py's /api/status endpoint — no local "
14
+ "file access, so it works against any host on the LAN serving one."
15
+ )
16
+ ap.add_argument("--host", default=DEFAULT_HOST, help=f"dashboard host (default: {DEFAULT_HOST})")
17
+ ap.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"dashboard port (default: {DEFAULT_PORT})")
18
+ ap.add_argument("--interval", type=float, default=1.0, help="refresh interval in seconds (default: 1.0)")
19
+ args = ap.parse_args()
20
+
21
+ def _run(stdscr):
22
+ App(stdscr, args.host, args.port, args.interval).run()
23
+
24
+ curses.wrapper(_run)
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
@@ -0,0 +1,50 @@
1
+ """Networking + time-series helpers. No filesystem access: everything comes
2
+ from a running monitor_dashboard.py's /api/status endpoint, so sgtop can
3
+ point at any host on the LAN that's serving one."""
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import urllib.request
8
+ from typing import Callable, Optional
9
+
10
+ DEFAULT_HOST = "127.0.0.1"
11
+ DEFAULT_PORT = 30001
12
+
13
+
14
+ def fetch_status(host: str, port: int, timeout: float = 2.0) -> Optional[dict]:
15
+ try:
16
+ with urllib.request.urlopen(f"http://{host}:{port}/api/status", timeout=timeout) as r:
17
+ return json.loads(r.read())
18
+ except Exception:
19
+ return None
20
+
21
+
22
+ def bucket_series(
23
+ items: list[dict],
24
+ ts_key: str,
25
+ value_fn: Callable[[dict], Optional[float]],
26
+ now: float,
27
+ window_s: float,
28
+ width: int,
29
+ filter_fn: Optional[Callable[[dict], bool]] = None,
30
+ ) -> list[float]:
31
+ """Bucket `items` into `width` evenly-spaced buckets covering the last
32
+ `window_s` seconds, averaging value_fn(item) within each bucket. Empty
33
+ buckets come back as 0.0, so the sparkline reads as "idle", not "no data".
34
+ """
35
+ if width <= 0:
36
+ return []
37
+ buckets: list[list[float]] = [[] for _ in range(width)]
38
+ start = now - window_s
39
+ step = window_s / width
40
+ for item in items:
41
+ if filter_fn is not None and not filter_fn(item):
42
+ continue
43
+ ts = item.get(ts_key)
44
+ if ts is None or ts < start:
45
+ continue
46
+ idx = min(width - 1, max(0, int((ts - start) / step)))
47
+ v = value_fn(item)
48
+ if v is not None:
49
+ buckets[idx].append(v)
50
+ return [sum(b) / len(b) if b else 0.0 for b in buckets]