win-llmtop 0.1.0__py3-none-any.whl

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.
llmtop.py ADDED
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+ """Live terminal panel for llama.cpp's llama-server on Windows: what's loaded, what it's doing, how fast, and where the memory goes.
4
+
5
+ win-llmtop live view, [ ] change the history window, q quits
6
+ win-llmtop --once one snapshot of the live page and exit
7
+ win-llmtop --once --page all every page
8
+ win-llmtop --plan project each preset's VRAM against the OS budget (loads nothing)
9
+ win-llmtop --plan <preset> the full memory map of one preset
10
+
11
+ Pages:
12
+ 1 Live GPU and RAM with trend charts, the running request, recent requests and events
13
+ 2 Memory VRAM by component against the OS budget, llama-server's own RAM, layer overview
14
+ 3 System system RAM by consumer, page file, RAM trends
15
+ 4 Requests per-preset volume and speed, prefill by prompt size, decode by context depth, every request
16
+ 5 Events alerts, model loads, router evictions, slot picks, errors
17
+ 6 Tensors every layer or tensor with its device (t layers/tensors, d device filter)
18
+ Keys: 1-6 or left/right switch pages, [ ] the time window, g graph height, s graph style, p pause, arrows and PgUp/PgDn scroll, q quits.
19
+
20
+ Sources:
21
+ /models, /slots?model=<preset> router API: loaded preset and its flags, context in use, prompt progress, tokens decoded (live)
22
+ <preset>.log per-request timings from the model process; the router log has loads, queueing and evictions
23
+ Windows performance counters dedicated and shared (system RAM) VRAM per process, GPU engine load, paging; DXGI gives the OS VRAM budget
24
+ GGUF header + llama-fit-params the model's tensors and llama.cpp's own memory projection
25
+
26
+ History is kept for 24 h in memory and mirrored to disk so it survives restarts (--no-persist to disable).
27
+ """
28
+
29
+ import argparse
30
+ import os
31
+ import platform
32
+ import shutil
33
+ import sys
34
+ import time
35
+ import traceback
36
+
37
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
38
+ from llmtop_lib import model, pages, state, win # noqa: E402
39
+ from llmtop_lib import ui # noqa: E402
40
+ from llmtop_lib.ui import C # noqa: E402
41
+
42
+ WINDOWS = {label: i for i, (_, label) in enumerate(pages.View.WINDOWS)}
43
+ PAGE_NAMES = [n.lower() for n in pages.View.PAGES]
44
+
45
+
46
+ def read_key():
47
+ """One key press as a name, or None. Windows console only."""
48
+ import msvcrt
49
+
50
+ if not msvcrt.kbhit():
51
+ return None
52
+ ch = msvcrt.getwch()
53
+ if ch in ("\x00", "\xe0"):
54
+ return {"H": "up", "P": "down", "I": "pgup", "Q": "pgdn", "G": "home", "O": "end", "K": "left", "M": "right"}.get(msvcrt.getwch())
55
+ key = {"\x03": "q", "\x1b": "q", "\t": "tab"}.get(ch, ch)
56
+ return key.lower() if len(key) == 1 else key
57
+
58
+
59
+ def handle(key, c, view, snap, H):
60
+ page = pages.View.PAGES[view.page]
61
+ n = len(pages.View.PAGES)
62
+ if key in [str(i + 1) for i in range(n)]:
63
+ view.page, view.scroll = int(key) - 1, 0
64
+ elif key in ("tab", "right"):
65
+ view.page, view.scroll = (view.page + 1) % n, 0
66
+ elif key == "left":
67
+ view.page, view.scroll = (view.page - 1) % n, 0
68
+ elif key in ("[", "-", "_"):
69
+ view.win = max(0, view.win - 1)
70
+ elif key in ("]", "=", "+"):
71
+ view.win = min(len(pages.View.WINDOWS) - 1, view.win + 1)
72
+ elif key in ("p", " "):
73
+ view.paused = not view.paused
74
+ elif key == "s":
75
+ ui.STYLE["mode"] = "blocks" if ui.STYLE["mode"] == "braille" else "braille"
76
+ view.msg = "graph style: %s" % ui.STYLE["mode"]
77
+ elif key == "g":
78
+ view.gh = (view.gh + 1) % 5
79
+ view.msg = "graph height: %s" % ("auto" if view.gh == 0 else "%d row%s" % (view.gh, "s" if view.gh > 1 else ""))
80
+ elif key == "m" and page in ("Memory", "Tensors"):
81
+ view.msel, view.scroll = view.msel + 1, 0
82
+ elif key == "m" and page == "Requests":
83
+ view.rsel, view.scroll = (view.rsel + 1) % (len({r["preset"] for r in c.requests}) + 1), 0
84
+ elif key == "f" and page == "Events":
85
+ view.level, view.scroll = (view.level + 1) % 3, 0
86
+ elif key == "t" and page == "Tensors":
87
+ view.tmode, view.scroll = (view.tmode + 1) % 2, 0
88
+ elif key == "d" and page == "Tensors":
89
+ view.dev, view.scroll = (view.dev + 1) % 3, 0
90
+ elif key == "up":
91
+ view.scroll = max(0, view.scroll - 1)
92
+ elif key == "down":
93
+ view.scroll += 1
94
+ elif key == "pgup":
95
+ view.scroll = max(0, view.scroll - max(1, H - 6))
96
+ elif key == "pgdn":
97
+ view.scroll += max(1, H - 6)
98
+ elif key == "home":
99
+ view.scroll = 0
100
+ elif key == "end":
101
+ view.scroll = 10**9
102
+
103
+
104
+ def frame_text(c, snap, view, W, H):
105
+ if snap is None:
106
+ return ["llmtop: collector error (press q to quit)", view.msg or "collector error"]
107
+ try:
108
+ return pages.render(c, snap, view, W, H)
109
+ except Exception:
110
+ tb = traceback.format_exc().splitlines()
111
+ return ["llmtop: render error (press q to quit)"] + tb[-(H - 2):]
112
+
113
+
114
+ def live(c, view):
115
+ """Full-screen loop. Redraws in place (cursor home) so there is no flicker."""
116
+ out = sys.stdout
117
+ out.write("\x1b[?1049h\x1b[?25l")
118
+ snap, frozen, prev, last_size = None, None, None, None
119
+ try:
120
+ while True:
121
+ t0 = time.time()
122
+ err = None
123
+ try:
124
+ snap = c.tick(t0)
125
+ except Exception as e:
126
+ err = traceback.format_exc()
127
+ view.msg = "collector error: %s" % e
128
+ size = shutil.get_terminal_size((120, 40))
129
+ if (size.columns, size.lines) != last_size: # a resize reflows what is on screen: start from a clean one
130
+ out.write("\x1b[2J")
131
+ prev, last_size = None, (size.columns, size.lines)
132
+ if snap is None:
133
+ err_lines = err.splitlines() if err else ([view.msg] if view.msg else ["unknown error"])
134
+ lines = ["llmtop: collector error (press q to quit)"] + err_lines[-(size.lines - 2):]
135
+ else:
136
+ if view.paused and frozen is None:
137
+ frozen = snap
138
+ elif not view.paused:
139
+ frozen = None
140
+ lines = frame_text(c, frozen or snap, view, size.columns, size.lines)
141
+ text = "\x1b[H" + "\x1b[K\n".join(lines) + "\x1b[K\x1b[J"
142
+ if text != prev:
143
+ out.write(text)
144
+ out.flush()
145
+ prev = text
146
+ deadline = t0 + 1.0 / c.cfg["hz"]
147
+ while time.time() < deadline:
148
+ key = read_key()
149
+ if key == "q":
150
+ return
151
+ if key:
152
+ handle(key, c, view, snap, size.lines)
153
+ prev = None
154
+ break
155
+ time.sleep(0.01)
156
+ finally:
157
+ out.write("\x1b[?25h\x1b[?1049l")
158
+ out.flush()
159
+
160
+
161
+ def snapshot(c, view, which, width):
162
+ """Warm the collector up (rates need two samples) and print pages once."""
163
+ snap = None
164
+ for _ in range(4):
165
+ snap = c.tick()
166
+ time.sleep(0.3)
167
+ deadline = time.time() + 5
168
+ while any(e.get("state") == "pending" for e in c.mem.values()) and time.time() < deadline:
169
+ time.sleep(0.1)
170
+ snap = c.tick()
171
+ for i, name in enumerate(PAGE_NAMES):
172
+ if which in (name, "all"):
173
+ view.page = i
174
+ if which == "all":
175
+ print("\n" + "=" * width)
176
+ print("\n".join(frame_text(c, snap, view, width, 200 if which == "all" or name != "live" else 60)).rstrip())
177
+
178
+
179
+ def plan(c, view, which, width):
180
+ """Memory projection of presets against the OS VRAM budget, without loading anything."""
181
+ snap = c.tick()
182
+ g = snap["gpu"]
183
+ names = [which] if which in c.flags else list(c.flags)
184
+ for name in names:
185
+ c.ensure_map(name)
186
+ deadline = time.time() + 120
187
+ while any(c.mem.get(n, {}).get("state") == "pending" for n in names) and time.time() < deadline:
188
+ time.sleep(0.1)
189
+ if which in c.flags:
190
+ view.page = 1
191
+ loaded = [n for n, p in snap.get("presets", {}).items() if p.get("status") == "loaded"]
192
+ order = loaded + [n for n in c.flags if n not in loaded]
193
+ view.msel = order.index(which) if which in order else 0
194
+ print("\n".join(frame_text(c, snap, view, width, 200)).rstrip())
195
+ return
196
+ print("%s: %s MiB, OS budget %s MiB" % (g["name"], "{:,.0f}".format(g["vram"]), "{:,.0f}".format(g["budget"])) if g else "no GPU telemetry")
197
+ print("\n%-26s %9s %9s %9s %9s %9s %10s %s" % ("preset", "weights", "KV+state", "compute", "draft", "overhead", "projected", "vs OS budget"))
198
+ for name in names:
199
+ e = c.mem.get(name, {})
200
+ if e.get("state") != "ready":
201
+ print("%-26s %s" % (name, e.get("error", "no projection")))
202
+ continue
203
+ cal = c.calib.get(c.calib_key(e["key"]))
204
+ rows = model.memory_rows(e["map"], None, cal["overhead"] if cal else None)
205
+ by = lambda pred, rows=rows: sum(r[1] for r in rows if pred(r[0]))
206
+ total = sum(r[1] for r in rows)
207
+ head = g["budget"] - total if g else float("nan")
208
+ print("%-26s %9.0f %9.0f %9.0f %9.0f %9s %10.0f %+.0f MiB%s%s" % (name, by(lambda n: n.startswith("weights")), by(lambda n: n.startswith(("KV", "recurrent"))), by(lambda n: n == "compute buffers"),
209
+ by(lambda n: n.startswith(("MTP", "draft"))), "%.0f" % cal["overhead"] if cal else "?", total, head, " over the budget" if head < 0 else "", "" if cal else " (not calibrated)"))
210
+ print("\nFigures are MiB. 'overhead' is resident VRAM minus the projected rows, measured on an earlier load of the same model file, context size and cache types (shared by every preset with that configuration).")
211
+ print("'?' means no such load has been measured yet, so the projection has no overhead term. 'vs OS budget' compares the projected resident VRAM with the DXGI budget; shared GPU memory is not part of it.")
212
+
213
+
214
+ def check(c, args):
215
+ """One-shot diagnostic report on prerequisites, reachable sources, and exit code."""
216
+ snap, tick_error = None, None
217
+ try:
218
+ c.load_history() # opens and reads the logs, as a normal run does; writes nothing with persistence off
219
+ for _ in range(3): # like --once: log tails and rates need more than one sample
220
+ snap = c.tick()
221
+ time.sleep(0.3)
222
+ except Exception as e:
223
+ tick_error = "%s: %s" % (type(e).__name__, e)
224
+ if snap is None:
225
+ snap = {"presets": {}, "notices": [], "gpu": None, "llama": None}
226
+
227
+ # 1. Windows build, Python version and bitness
228
+ if win.WIN:
229
+ wv = sys.getwindowsversion()
230
+ win_ver = "Windows %d.%d.%d" % (wv.major, wv.minor, wv.build)
231
+ else:
232
+ win_ver = sys.platform
233
+ py_ver = "Python %s %s" % (platform.python_version(), "64-bit" if win.IS_64 else "32-bit")
234
+ win_ok = win.WIN and win.IS_64
235
+ print("%-18s %-5s %s" % ("windows", "ok" if win_ok else "FAIL", "%s, %s" % (win_ver, py_ver)))
236
+ if tick_error:
237
+ print("%-18s %-5s %s" % ("collector", "FAIL", tick_error))
238
+
239
+ # 2. GPU: DXGI adapters, watched adapter, PDH GPU counters
240
+ dx = c.dx
241
+ adapters = dx.adapters if dx else []
242
+ pd = c.counters.sample() if c.counters else {}
243
+ pdh_procs = pd.get("procs") or {}
244
+ pdh_adapters = pd.get("adapters") or {}
245
+ pdh_ok = bool(pdh_procs or pdh_adapters)
246
+ watched = c.gpu
247
+ gpu_telemetry_ok = bool(win.WIN and adapters and watched and pdh_ok)
248
+ if not win.WIN:
249
+ gpu_status, gpu_fact = "FAIL", "Windows only"
250
+ elif not adapters:
251
+ gpu_status, gpu_fact = "--", "no DXGI adapters found"
252
+ else:
253
+ ad_strs = ["%s (%.0f MiB, budget %.0f MiB)" % (a["name"], a["vram"], a["budget"]) for a in adapters]
254
+ watch_str = "watching %s" % watched["name"] if watched else "no adapter selected"
255
+ pdh_str = "PDH counters present" if pdh_ok else "PDH counters absent"
256
+ gpu_fact = "%s, %s; %s" % ("; ".join(ad_strs), watch_str, pdh_str)
257
+ gpu_status = "ok" if gpu_telemetry_ok else "FAIL"
258
+ print("%-18s %-5s %s" % ("gpu", gpu_status, gpu_fact))
259
+
260
+ # 3. llama-server processes (pid, exe from proc_info, or the reason it couldn't be read)
261
+ if not win.WIN:
262
+ proc_status, proc_fact = "--", "Windows only"
263
+ elif not c.pids:
264
+ proc_status, proc_fact = "--", "none running"
265
+ else:
266
+ proc_status = "ok"
267
+ items = []
268
+ for pid in c.pids:
269
+ info = win.proc_info(pid)
270
+ exe = info.get("exe")
271
+ if exe:
272
+ items.append("pid %d: %s" % (pid, exe))
273
+ else:
274
+ err = win.errors.get(("proc_info", pid))
275
+ reason = err[1] if err else "cannot read process info"
276
+ items.append("pid %d: %s" % (pid, reason))
277
+ proc_fact = "; ".join(items)
278
+ print("%-18s %-5s %s" % ("processes", proc_status, proc_fact))
279
+
280
+ # 4. the server: URL used and how it was found, mode, models reported (count), /slots reachable for a loaded model (or its slot_error)
281
+ server_ok = bool(c.router_up)
282
+ if server_ok:
283
+ server_status = "ok"
284
+ url = c.cfg.get("router") or ""
285
+ how = "explicit" if c.router_explicit else (c.router_discovery or "discovered")
286
+ mode_str = "%s mode" % (c.mode or "unknown")
287
+ m_count = len(c.models)
288
+ count_str = "%d model%s reported" % (m_count, "" if m_count == 1 else "s")
289
+ loaded = [n for n, p in snap["presets"].items() if p.get("status") == "loaded"]
290
+ if loaded:
291
+ slot_errs = [p.get("slot_error") for n, p in snap["presets"].items() if p.get("status") == "loaded" and p.get("slot_error")]
292
+ if slot_errs:
293
+ slot_str = "/slots error: %s" % ", ".join(slot_errs)
294
+ else:
295
+ slot_str = "/slots reachable"
296
+ else:
297
+ slot_str = "/slots not queried (no loaded models)"
298
+ server_fact = "%s (%s), %s, %s, %s" % (url, how, mode_str, count_str, slot_str)
299
+ else:
300
+ server_status = "FAIL"
301
+ url = c.cfg.get("router") or ""
302
+ tried = ", ".join(c.router_tried) if c.router_tried else url
303
+ err = (": " + c.router_error) if c.router_error else ""
304
+ server_fact = "server unreachable (tried %s)%s" % (tried, err)
305
+ print("%-18s %-5s %s" % ("server", server_status, server_fact))
306
+
307
+ # 5. log files: per model, the path and whether it exists and has timestamps; the router log
308
+ log_items = []
309
+ for name in c.models:
310
+ path = c.preset_log_path(name)
311
+ if path:
312
+ exists = os.path.isfile(path)
313
+ has_ts = c.follow[name][1].timestamps if name in c.follow else False
314
+ ts_str = ", timestamps" if has_ts else ", no timestamps"
315
+ log_items.append("%s: %s (%s%s)" % (name, path, "exists" if exists else "absent", ts_str if exists else ""))
316
+ else:
317
+ log_items.append("%s: no log path" % name)
318
+ rl_path = c.cfg.get("router_log") or (c.router_tail.path if c.router_tail else None)
319
+ if rl_path:
320
+ log_items.append("router log: %s (%s)" % (rl_path, "exists" if os.path.isfile(rl_path) else "absent"))
321
+ elif c.mode == "router":
322
+ log_items.append("router log: none found (model loads and evictions need it: pass --router-log or --log-dir)")
323
+ if not log_items:
324
+ log_status, log_fact = "--", "no log files configured or found"
325
+ else:
326
+ log_fact = "; ".join(log_items)
327
+ log_status = "ok" if any("exists" in item for item in log_items) else "--"
328
+ print("%-18s %-5s %s" % ("logs", log_status, log_fact))
329
+
330
+ # 6. llama-fit-params: found next to the server, on PATH, or not found
331
+ fit_found = None
332
+ fit_where = None
333
+ candidates = []
334
+ for pid in c.pids:
335
+ info = win.proc_info(pid) if win.WIN else {}
336
+ exe = info.get("exe")
337
+ if exe:
338
+ d = os.path.dirname(exe)
339
+ candidates.extend([os.path.join(d, "llama-fit-params.exe"), os.path.join(d, "llama-fit-params")])
340
+ for _, exe in c.flags.values():
341
+ if exe:
342
+ d = os.path.dirname(exe)
343
+ candidates.extend([os.path.join(d, "llama-fit-params.exe"), os.path.join(d, "llama-fit-params")])
344
+ for cand in candidates:
345
+ if os.path.isfile(cand):
346
+ fit_found = cand
347
+ fit_where = "found next to server"
348
+ break
349
+ if not fit_found:
350
+ w = shutil.which("llama-fit-params.exe") or shutil.which("llama-fit-params")
351
+ if w:
352
+ fit_found = w
353
+ fit_where = "found on PATH"
354
+ if fit_found:
355
+ fit_status = "ok"
356
+ fit_fact = "%s (%s)" % (fit_where, fit_found)
357
+ else:
358
+ fit_status = "--"
359
+ fit_fact = "not found"
360
+ print("%-18s %-5s %s" % ("llama-fit-params", fit_status, fit_fact))
361
+
362
+ # 7. the state folder: path and whether it is writable (check without creating it when --no-persist)
363
+ state_dir = c.state_dir
364
+ if args.no_persist:
365
+ if os.path.isdir(state_dir):
366
+ try:
367
+ tf = os.path.join(state_dir, ".test_write_%d" % os.getpid())
368
+ with open(tf, "w") as f:
369
+ f.write("")
370
+ os.remove(tf)
371
+ writable = True
372
+ except Exception:
373
+ writable = False
374
+ else:
375
+ p = os.path.abspath(state_dir)
376
+ while p and not os.path.exists(p):
377
+ prev = p
378
+ p = os.path.dirname(p)
379
+ if p == prev:
380
+ break
381
+ try:
382
+ tf = os.path.join(p, ".test_write_%d" % os.getpid())
383
+ with open(tf, "w") as f:
384
+ f.write("")
385
+ os.remove(tf)
386
+ writable = True
387
+ except Exception:
388
+ writable = False
389
+ if writable:
390
+ state_status, state_fact = "ok", "%s (writable)" % state_dir
391
+ else:
392
+ state_status, state_fact = "FAIL", "%s (not writable)" % state_dir
393
+ else:
394
+ try:
395
+ os.makedirs(state_dir, exist_ok=True)
396
+ tf = os.path.join(state_dir, ".test_write_%d" % os.getpid())
397
+ with open(tf, "w") as f:
398
+ f.write("")
399
+ os.remove(tf)
400
+ state_status, state_fact = "ok", "%s (writable)" % state_dir
401
+ except Exception as e:
402
+ state_status, state_fact = "FAIL", "%s (%s)" % (state_dir, e)
403
+ print("%-18s %-5s %s" % ("state", state_status, state_fact))
404
+
405
+ # 8. then every snap["notices"] line
406
+ for n in snap.get("notices", []):
407
+ print("%-18s %-5s %s" % ("notice", "--", n))
408
+
409
+ return 0 if (server_ok and gpu_telemetry_ok) else 1
410
+
411
+
412
+ def main():
413
+ if sys.version_info < (3, 11):
414
+ sys.exit("win-llmtop needs Python 3.11 or later (this is %d.%d)" % sys.version_info[:2])
415
+ ap = argparse.ArgumentParser(prog="win-llmtop", description="Live terminal panel for llama.cpp's llama-server on Windows: GPU and shared memory, RAM, requests.")
416
+ ap.add_argument("--check", action="store_true", help="report on prerequisites and reachable sources, then exit")
417
+ ap.add_argument("--once", action="store_true", help="print one snapshot and exit")
418
+ ap.add_argument("--page", choices=PAGE_NAMES + ["all"], default="live", help="page for --once")
419
+ ap.add_argument("--plan", nargs="?", const="all", metavar="PRESET", help="project VRAM per preset against the OS budget")
420
+ ap.add_argument("--window", choices=list(WINDOWS), default="5m", help="initial history window")
421
+ ap.add_argument("--router", default=os.environ.get("LLMTOP_ROUTER") or None, metavar="URL", help="llama-server URL (default: find it; env LLMTOP_ROUTER)")
422
+ ap.add_argument("--log-dir", default=os.environ.get("LLMTOP_LOG_DIR") or None, metavar="DIR", help="directory of llama-server logs (env LLMTOP_LOG_DIR)")
423
+ ap.add_argument("--router-log", default=None, metavar="PATH", help="router log file path")
424
+ ap.add_argument("--state-dir", default=os.environ.get("LLMTOP_STATE_DIR") or state.CFG["state_dir"], metavar="DIR", help="directory for persisted history and calibration (env LLMTOP_STATE_DIR)")
425
+ ap.add_argument("--api-key", default=os.environ.get("LLMTOP_API_KEY") or os.environ.get("LLAMA_API_KEY") or None, metavar="KEY", help="Bearer token for llama-server (env LLMTOP_API_KEY or LLAMA_API_KEY)")
426
+ ap.add_argument("--gpu", default=None, metavar="INDEX|NAME", help="adapter index or name substring")
427
+ ap.add_argument("--hz", type=float, default=state.CFG["hz"], help="sampling and redraw rate")
428
+ ap.add_argument("--history-hours", type=int, default=state.CFG["history_hours"])
429
+ ap.add_argument("--spill-mib", type=int, default=state.CFG["spill_mib"], help="excess shared GPU memory (MiB) that raises an alert")
430
+ ap.add_argument("--threshold", action="append", default=[], metavar="NAME=VALUE", help="override an alert threshold (see CFG in llmtop_lib/state.py); repeatable")
431
+ ap.add_argument("--no-persist", action="store_true", help="do not mirror history to disk")
432
+ ap.add_argument("--graph-style", choices=("braille", "blocks"), default=None, help="braille line plots, or block-character charts if the font lacks braille")
433
+ ap.add_argument("--no-color", action="store_true")
434
+ ap.add_argument("--width", type=int, default=0, help="line width for --once and --plan")
435
+ args = ap.parse_args()
436
+ if not win.WIN:
437
+ sys.exit("llmtop reads Windows performance counters and runs on Windows only.")
438
+ if not win.IS_64:
439
+ sys.exit("win-llmtop needs 64-bit Python (32-bit Python cannot read a 64-bit llama-server's memory)")
440
+ if args.graph_style is None:
441
+ args.graph_style = "braille" if "WT_SESSION" in os.environ else "blocks"
442
+ ui.STYLE["mode"] = args.graph_style
443
+ vt_ok = win.enable_vt()
444
+ for stream in (sys.stdout, sys.stderr):
445
+ try:
446
+ stream.reconfigure(encoding="utf-8", errors="replace")
447
+ except (AttributeError, ValueError):
448
+ pass
449
+ interactive = sys.stdout.isatty() and not args.once and not args.check and args.plan is None
450
+ if not vt_ok and sys.stdout.isatty():
451
+ C.enable(False)
452
+ else:
453
+ C.enable(not args.no_color and sys.stdout.isatty())
454
+ user_thresholds = set()
455
+ cfg = dict(state.CFG, router=args.router, log_dir=args.log_dir, router_log=args.router_log, state_dir=args.state_dir,
456
+ api_key=args.api_key, gpu=args.gpu, hz=args.hz, history_hours=args.history_hours, spill_mib=args.spill_mib,
457
+ persist=not (args.no_persist or args.once or args.plan is not None or args.check),
458
+ calibrate=not (args.no_persist or args.plan is not None or args.check),
459
+ user_thresholds=user_thresholds)
460
+ num_thresholds = {k: v for k, v in state.CFG.items() if isinstance(v, (int, float)) and not isinstance(v, bool)}
461
+ for item in args.threshold:
462
+ name, _, value = item.partition("=")
463
+ if name not in num_thresholds or not value:
464
+ sys.exit("unknown or non-numeric threshold %r; thresholds: %s" % (item, ", ".join(num_thresholds)))
465
+ cfg[name] = type(num_thresholds[name])(float(value))
466
+ user_thresholds.add(name)
467
+ c = state.Collector(cfg)
468
+ if not args.check:
469
+ c.load_history()
470
+ view = pages.View(WINDOWS[args.window])
471
+ width = args.width or shutil.get_terminal_size((140, 40)).columns
472
+ try:
473
+ if args.check:
474
+ rc = check(c, args)
475
+ sys.exit(rc)
476
+ elif args.plan is not None:
477
+ plan(c, view, args.plan, width)
478
+ elif interactive:
479
+ live(c, view)
480
+ else:
481
+ snapshot(c, view, args.page, width)
482
+ except KeyboardInterrupt:
483
+ pass
484
+ finally:
485
+ c.close()
486
+
487
+
488
+ if __name__ == "__main__":
489
+ main()
llmtop_lib/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """Building blocks for llmtop.py."""