6.2 KiB
6.2 KiB
Usage Stats + GPU Combinations
Date: 2026-08-14 Status: Approved (design)
Summary
Two extensions to the existing llama-watch actions:
- The In-Flight Monitor action gains a Usage stats display mode showing
token/request totals and the generation-speed P95 from llama-swap's
/api/metrics/statsendpoint. - The GPU Graph action gains GPU combinations: a key can aggregate an arbitrary subset of GPUs (e.g. only the two Blackwells).
Data Source (verified against llama-swap source)
GET /api/metrics/stats(no query param) → global aggregates.GET /api/metrics/stats?model=<id>→ per-model aggregates.- Response fields used:
total_requests,total_input_tokens,total_output_tokens,gen_histogram.p95. p95is a tokens/sec generation-speed percentile, not latency.- Totals cover llama-swap's in-memory activity retention (default ~1000 requests), not lifetime counters.
- The
/api/eventsSSEactivityevent (payload{"id": N}) fires per completed request and is used as a "re-poll stats" trigger. It carries no data itself. - The Prometheus
/metricsendpoint exposes no token/request/latency data and is not used for this feature.
1. Usage stats data layer
src/lib/stats.ts(new):interface UsageStats { totalRequests: number; totalInputTokens: number; totalOutputTokens: number; genP95: number }.parseStats(json: unknown): UsageStats | null— defensive; returnsnullon malformed input; missing fields default to0.fetchStats(cfg: LlamaSwapConfig, modelId: string): Promise<UsageStats | null>— fetches/api/metrics/stats(with?model=whenmodelIdis not"all"), returns parsed stats ornullon HTTP error/parse failure.
src/lib/stats-cache.ts(new):StatsCacheholdsSet<string>of registered model keys ("all"or a model id) and aMap<key, UsageStats | null>of last-known values.- One shared 5s interval polls every registered key via
fetchStats. register(key)/unregister(key)start/stop the interval as needed (no ref counting — each key registers/unregisters once).get(key): UsageStats | undefined.refresh()re-polls all registered keys.- Change listener invoked after each poll completes (success or failure).
- Fetch failure keeps the last-known value.
src/lib/event-feed.ts:decodeEventlearns theactivityevent —{ type: "activity", id: number }.FeedEvent(ininflight-tracker.ts) gains theactivityvariant.src/lib/runtime.ts: runtime owns aStatsCache(created alongside feed/poller, reset on config change). Feedactivityevents triggerstatsCache.refresh()throttled to at most once per 2s. Runtime exposes:getStats(modelId: string): UsageStats | undefinedwatchStats(modelId: string): () => void(register; returns unsubscribe).- Stats changes flow through the existing
subscribe/emitmechanism so actions re-render on the normal tick.
2. In-Flight action modes
- New settings (per key, in
inflight-monitor.ts):display: "count" | "usage"(defaultcount).primaryStat: "requests" | "input_tokens" | "output_tokens" | "gen_p95"(defaultrequests).
- Model setting semantics:
- Model select (PI datasource) gains an "All models" option, value
all. - Count mode:
all→ total in-flight across all models; labelALL MODELS. - Usage mode:
all→ global stats; otherwise per-model stats.
- Model select (PI datasource) gains an "All models" option, value
- Count mode: unchanged rendering (count number, spark,
IDLE/LOADING/OFF/OFFLINE). The 1s spark sampler runs only in count mode. - Usage mode (
renderUsageinrender.ts, dark#10131abg):- top: model short name or
ALL MODELS(7px). - center (24px bold): the
primaryStatvalue, compact-formatted —< 1000integer, elsex.xk, elsex.xM(genP95shows integer t/s). - three small rows (7px): the remaining three stats, labeled
REQ,IN,OUT,P95. - offline →
OFFLINEoverlay; stats unknown →--.
- top: model short name or
- Press opens the UI (unchanged).
3. GPU combinations
- New setting
gpuCombo?: string— comma-separated GPU ids, e.g."0,2". When non-empty it overridesgpuId. - Pure helper in
metrics-poller.ts(exported, tested):combineSeries(histories: number[][], kind): { value?: number; history: number[] }— element-wise across the per-GPU rings, aligned by index, applying the existing aggregate rules:util_percent/memory_util_percent/fan→ average,temperature→ max,power→ sum. A trailingundefinedvalue from an empty ring leaves that element undefined. gpu-graph.tsrender: for a combo, build per-GPU histories viapoller.getHistory(id, metric), combine, use the combined value/history. Unknown ids are filtered out; if none remain →--.- Label:
GPU 0+2(ids joined with+).
4. Property inspectors
ui/inflight.html: add "Display" select (count/usage) and "Primary stat" select. Both always visible;primaryStathas no effect in count mode. Model datasource includes thealloption (handled in the PI datasource provider if one exists, else the select defaults).ui/gpu.html: add "GPU combination (comma-separated ids)" textfield, optional, placeholdere.g. 0,2.
Error Handling / Edge Cases
- Stats fetch error → keep last-known values; no
OFFLINEflip (offline is driven by feed status as today). - Server offline → existing
OFFLINEoverlay in both modes. - Stats not yet loaded →
--placeholders. - Activity-triggered refresh throttled to 1 per 2s.
- Combo with no valid ids →
--. - No manifest change (both features extend existing actions).
Testing
parseStats: happy path; malformed input; missing fields →0.StatsCache: register/unregister lifecycle (interval start/stop), single poll per model, values cached, refresh on demand, failure keeps last value. Uses an injected fetch function.decodeEvent:activityevent decodes to{ type: "activity", id }.renderUsage: big primary stat, small labeled rows, compact formatting (52.1k),--when no data,ALL MODELSlabel, offline overlay.combineSeries: avg/max/sum per metric; empty/mismatched histories.- In-Flight action:
allcount sums models.