Files
llama-watch/docs/superpowers/specs/2026-08-14-usage-stats-gpu-combos-design.md
T

132 lines
6.3 KiB
Markdown

# Usage Stats + GPU Combinations
**Date:** 2026-08-14
**Status:** Approved (design)
## Summary
Two extensions to the existing llama-watch actions:
1. 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/stats` endpoint.
2. 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`.
- `p95` is 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/events` SSE `activity` event (payload `{"id": N}`) fires per
completed request and is used as a "re-poll stats" trigger. It carries no
data itself.
- The Prometheus `/metrics` endpoint 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; returns
`null` on malformed input; missing fields default to `0`.
- `fetchStats(cfg: LlamaSwapConfig, modelId: string): Promise<UsageStats | null>`
fetches `/api/metrics/stats` (with `?model=` when `modelId` is not
`"all"`), returns parsed stats or `null` on HTTP error/parse failure.
- **`src/lib/stats-cache.ts`** (new):
- `StatsCache` holds `Set<string>` of registered model keys (`"all"` or a
model id) and a `Map<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`**: `decodeEvent` learns the `activity` event —
`{ type: "activity", id: number }`. `FeedEvent` (in `inflight-tracker.ts`)
gains the `activity` variant.
- **`src/lib/runtime.ts`**: runtime owns a `StatsCache` (created alongside
feed/poller, reset on config change). Feed `activity` events trigger
`statsCache.refresh()` throttled to at most once per 2s. Runtime exposes:
- `getStats(modelId: string): UsageStats | undefined`
- `watchStats(modelId: string): () => void` (register; returns
unsubscribe).
- Stats changes flow through the existing `subscribe`/`emit` mechanism so
actions re-render on the normal tick.
## 2. In-Flight action modes
- New settings (per key, in `inflight-monitor.ts`):
- `display: "count" | "usage"` (default `count`).
- `primaryStat: "requests" | "input_tokens" | "output_tokens" | "gen_p95"`
(default `requests`).
- Model setting semantics:
- Model select (PI datasource) gains an **"All models"** option, value
`all`.
- Count mode: `all` → total in-flight across all models; label
`ALL MODELS`.
- Usage mode: `all` → global stats; otherwise per-model stats.
- **Count mode**: unchanged rendering (count number, spark, `IDLE`/
`LOADING`/`OFF`/`OFFLINE`). The 1s spark sampler runs only in count mode.
- **Usage mode** (`renderUsage` in `render.ts`, dark `#10131a` bg):
- top: model short name or `ALL MODELS` (7px).
- center (24px bold): the `primaryStat` value, compact-formatted —
`< 1000` integer, else `x.xk`, else `x.xM` (`genP95` shows integer t/s).
- three small rows (7px): the remaining three stats, labeled `REQ`,
`IN`, `OUT`, `P95`.
- offline → `OFFLINE` overlay; stats unknown → `--`.
- Press opens the UI (unchanged).
## 3. GPU combinations
- New setting `gpuCombo?: string` — comma-separated GPU ids, e.g. `"0,2"`.
When non-empty it overrides `gpuId`.
- 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 trailing `undefined` value
from an empty ring leaves that element undefined.
- `gpu-graph.ts` render: for a combo, build per-GPU histories via
`poller.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; `primaryStat` has no effect in count
mode.
- `src/lib/datasources.ts`: the `models` datasource prepends
`{ label: "All models", value: "all" }` to its items (so the model select
offers both the aggregate and individual models).
- `ui/gpu.html`: add "GPU combination (comma-separated ids)" textfield,
optional, placeholder `e.g. 0,2`.
## Error Handling / Edge Cases
- Stats fetch error → keep last-known values; no `OFFLINE` flip (offline is
driven by feed status as today).
- Server offline → existing `OFFLINE` overlay 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`: `activity` event decodes to `{ type: "activity", id }`.
- `renderUsage`: big primary stat, small labeled rows, compact formatting
(`52.1k`), `--` when no data, `ALL MODELS` label, offline overlay.
- `combineSeries`: avg/max/sum per metric; empty/mismatched histories.
- In-Flight action: `all` count sums models.