Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9d9cd78
wip: rework Hermes update UX; drop toast flow and CpuPanel
Aug 9, 2026
8ac5ca6
fix(bench): survive server restarts without "Benchmark not found"
Aug 11, 2026
cc935fd
chore: gitignore runtime bench-active.json checkpoint
Aug 11, 2026
0f88aee
Release 1.8.0: non-Spark GPU host support + prefill tok/s
Aug 15, 2026
ee374ef
docs: add Hermes Agent monitoring section to README; fix stale toast …
Aug 15, 2026
7d63a87
ui: rename dialog/button to 'Add Spark/GPU Host'
Aug 15, 2026
8306e1a
docs: explain Docker SSH key mount for remote Sparks
Aug 16, 2026
a747e2b
feat: daily decode/prefill history and cached vs uncached tok/s
Aug 16, 2026
7c6db24
Harden bind default, bench port allowlist, and README box.
Aug 16, 2026
37e4325
Set BIND_HOST in docker-compose.dev.yml so LAN docker:dev matches prod.
Aug 16, 2026
56761fe
Show CPU temperature on dedicated GPU hosts only.
Aug 16, 2026
f3deca3
Add opt-in Tailnet monitoring (default off).
Aug 16, 2026
0395a3d
fix(llm): show SGLang engine as Active and keep live tok/s under load
0xWhiteMage Aug 19, 2026
bf2709a
Merge PR #53: fix(llm) SGLang Engine tile and live tok/s under load
Aug 19, 2026
88aefcc
Release 1.8.2: EXL3 live tok/s and Docker Hub IPv6 workaround.
Aug 23, 2026
ac886dc
Release 1.8.3: structured decode bench (count 1→200) and thinking-off…
Aug 28, 2026
30bd286
Release 1.8.4: add Decode bench types (structured/prose/code/JSON) se…
Aug 28, 2026
e93fc87
Release 1.8.5: default Decode type to Structured and make Code a high…
Aug 28, 2026
0cedb04
feat: collect and show CPU temperature on DGX Sparks
Aug 29, 2026
cf8dcc0
fix: copy decode-bench tok/s with the same one-decimal precision as t…
Aug 29, 2026
eff85ee
fix: probe current SGLang /server_info and /model_info first
Aug 29, 2026
deabf77
Merge PR #71: fix: copy decode-bench tok/s with one decimal like the …
Aug 29, 2026
7b47cd1
Merge PR #72: fix: probe current SGLang /server_info and /model_info …
Aug 29, 2026
cc44d35
Release 1.8.6: add prefill benchmark (context sweep to 300k with TTFT).
Aug 31, 2026
a561c22
fix: reuse SSH transports for remote collectors
yhwho Sep 4, 2026
1aa3c65
test: retarget DecodeBench assertions at the lab structured protocol
andrei-dotdna Aug 29, 2026
447a072
fix: avoid recursive proc net fallback
0xdfi Aug 24, 2026
78d67de
fix: include showcase fixture in production image
0xdfi Aug 24, 2026
d319899
test: make LLM rate timing deterministic
0xdfi Aug 24, 2026
e157493
Show NVRM NV_ERR_NO_MEMORY on the GPU panel, polled once a minute.
Aug 16, 2026
e03b9d6
feat: hide worker nodes and add remote decode/prefill benches.
MiaAI-Lab Sep 6, 2026
b654c22
fix: invalidate failed SSH multiplex transports
yhwho Sep 7, 2026
9a9416e
Merge remote-tracking branch 'origin/main' into fix/reuse-ssh-transports
yhwho Sep 7, 2026
3cd2f87
feat: LLM panel tok/s & TTFT trend chart with busy-sample averages
unsaltedbutter-ai Aug 31, 2026
89114cf
fix: drop TTFT overlay from tok/s trend chart per review
unsaltedbutter-ai Sep 6, 2026
62907ed
feat: fixed 30-min scrolling chart window; configurable history reten…
unsaltedbutter-ai Sep 6, 2026
06bd776
fix: reconcile LLM API keys after out-of-band llmPorts edits (PATCH r…
sethforprivacy Sep 2, 2026
701145b
fix: arm PATCH llmPorts key sync on the property itself, against post…
sethforprivacy Sep 6, 2026
8e6dc0e
merge: #77 reuse SSH transports for remote collectors
cursoragent Sep 7, 2026
8ca8ac7
merge: #75 LLM tok/s trend chart
cursoragent Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
PORT=5555
# Listen address. Loopback by default (the dashboard exposes SSH + power controls);
# set to the host LAN IP or 0.0.0.0 to allow access from another machine.
BIND_HOST=127.0.0.1
LLM_PORT=8888
POLL_INTERVAL_GPU=2000
POLL_INTERVAL_CPU=2000
POLL_INTERVAL_NETWORK=2000
POLL_INTERVAL_STORAGE=5000
POLL_INTERVAL_LLM=2000
POLL_INTERVAL_BANDWIDTH=1000
POLL_INTERVAL_BANDWIDTH=1000
# Optional: ssh -i path inside the container when the key is not a default OpenSSH name
# SSH_IDENTITY_FILE=/root/.ssh/id_ed25519
# Reuse authenticated SSH transports for remote polling. Set to 0 to disable.
SSH_CONTROL_PERSIST_SECONDS=60
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ sparks.json
config/sparks-secrets.json
config/.secrets-key
config/bench-history.json
config/bench-active.json
config/prefill-bench-history.json
config/prefill-bench-active.json
config/showcase-history.json
config/llm-daily.json
config/gpu-memory.json
config/gpu-memory.json.*
config/bench-exports/
Expand All @@ -19,5 +23,6 @@ PLAN.md
tetris.html
.zcode/
HANDOFF.md
HANDOFF-HERMES.md
handoff.md
test/
132 changes: 132 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,138 @@ Format: version sections are listed newest first.

---

## [Unreleased]

### Added
- **Hide worker nodes** — Settings toggle. Worker-role Sparks drop off Overview cards and the tab bar (the open worker tab stays). Direct URLs and batch Wake / Shutdown / Hermes still include them.
- **On-demand Remote bench** — a **Remote** button next to decode/prefill opens a host + port (HTTPS) field. Paste a Tailscale URL such as `https://name.ts.net/v1/models`; nothing is probed until you run Decode or Prefill against it.
- **Decode / prefill benches on remote Sparks** — if the remote LLM is not reachable on its LAN IP (loopback-only bind), sparkDash opens an SSH local-forward to `127.0.0.1:<port>` for the job. Bench buttons stay on the LLM card even when the live probe shows no model.

### Fixed
- **Remote SSH session churn** — collectors reuse an authenticated SSH transport instead of creating a full SSH/PAM login for every metric poll. Failed transports invalidate readiness so subsequent callers share a recovery probe. `SSH_CONTROL_PERSIST_SECONDS=0` restores one connection per command if needed.
- **Prefill bench still dying at ~5 min** — Node undici aborts streams with no headers/body after 300s. Long prefills now use an Agent with those idle timeouts disabled; the per-size AbortSignal remains the bound.

---

## [1.8.6] — 2026-09-01

### Added
- **Prefill benchmark** — sequential context-size sweep (1k–300k) measuring prefill tok/s (`prompt_tokens` ÷ TTFT) and TTFT. Unique prefix per size so prefix-cache does not inflate later runs. Button on the LLM card; persisted last run. Per-size timeout scales with context (90s floor, ~8 ms/token, 45 min cap).
- **DGX Spark CPU temperature** — remote Sparks collect CPU temp over SSH with the same hwmon allowlist as hosts (`acpitz` / `coretemp` / `k10temp` / `zenpower`; NVMe / CX7 filtered out). Overview shows a CPU bar and Spark pages show a CPU row on the GPU panel when the reading is above 0°C.

### Fixed
- **Prefill bench 256k timeout** — per-size cap was 12 minutes (`~3 ms/token`); slow prefills aborted before first token. Now ~8 ms/token with a 45 minute cap, and the error names the limit.
- **Copy results tok/s** — clipboard text now uses one decimal like the decode-bench table (`31.5` not `31`), and TTFT uses the same mean as the table. ([#57](https://github.com/MiaAI-Lab/sparkDash/issues/57))
- **SGLang log spam** — probe current `/server_info` and `/model_info` first; keep the deprecated `/get_*` aliases as fallback so old servers still work. ([#52](https://github.com/MiaAI-Lab/sparkDash/issues/52))

---

## [1.8.5] — 2026-08-28

### Fixed
- **Decode type picker defaults to Structured** — opening the sheet (or loading a previous run) no longer leaves Prose/Code/JSON selected. A still-running job still shows its type.
- **Code workload was prose-speed** — the LRU + "thorough comments" prompt is English with `def` sprinkled in, so DFlash2 accept matched Prose. Code is now `clamp_00`…`clamp_49` identical-shape Python helpers, no comments.

---

## [1.8.4] — 2026-08-28

### Added
- **Decode benchmark type picker** — choose **Structured** (default, count 1→200), **Prose** (lab hash-map explanation), **Code** (fixed LRU-cache Python prompt), or **JSON** (GPU-metrics catalog) before Run. Labels are output types only — no `response_format`, grammars, or guided JSON. Same lab protocol for every type: temp 0, `top_p` 1, thinking off, 32-token warmup, default 400 tokens. The selected type is shown on results and in copied summaries.

---

## [1.8.3] — 2026-08-28

### Changed
- **Decode benchmark uses the lab structured protocol** — count 1→200 (numbers only) instead of the Showcase JSON/YAML catalog + fill-to-max. Temperature **0**, `top_p` **1**, thinking **off**, 32-token warmup, default max tokens **400**. Concurrency 1 is the same prompt as glm-5.3-flash-sm120 `tests/bench_decode.py --structured`; concurrent streams get a unique suffix so they do not share a prefix-cache block.
- **Thinking flags default off** — GLM / Qwen / MiniMax think unless the request disables it. `applyThinkingFlags` now defaults to off and always sends `enable_thinking`, `thinking`, and `thinking_mode`. HTTP 400 retries keep an explicit off payload instead of stripping flags (stripping lets hybrid models think by default). Showcase treats a missing thinking flag as off.

---

## [1.8.2] — 2026-08-23

### Added
- **EXL3 live tok/s** — detect ExLlamaV3 `tools/serve_openai.py` (`owned_by: exl3` or `/health` `{ok, busy}`) instead of mislabeling it as vLLM. Generation and prefill tok/s come from `/health` cumulative token counters (no Prometheus `/metrics`).
- **Tailnet monitoring** — opt-in per unit (`tailscaleMonitoring`, default **off**); `tailscale status --json` on the host and a Tailnet card under Resources. Flags a unit that is healthy on the LAN but off its tailnet. ([#43](https://github.com/MiaAI-Lab/sparkDash/pull/43))
- **NV_ERR_NO_MEMORY on the GPU panel** — count of NVRM `NV_ERR_NO_MEMORY` kernel log lines since boot (shown when > 0). Journal is scanned at most once a minute, not on the 2s poll. Replaces the approach in [#40](https://github.com/MiaAI-Lab/sparkDash/pull/40).

### Security
- **`BIND_HOST` now defaults to `127.0.0.1` (loopback) instead of `0.0.0.0`** — the dashboard is unauthenticated and can SSH into and power off Sparks, so it is no longer reachable on the LAN by default. Set `BIND_HOST` to the host's LAN IP (or `0.0.0.0`) to opt in to remote access. **Migration:** if you access sparkDash from another machine via bare-metal `npm start`, set `BIND_HOST` explicitly. Production and dev Compose both set `BIND_HOST=0.0.0.0` (`network_mode: host`). Startup now also warns when bound to a non-loopback address. ([#35](https://github.com/MiaAI-Lab/sparkDash/pull/35))

### Fixed
- Decode bench `POST /api/sparks/:id/llm/bench` rejects LLM ports that are not in the Spark's configured list (same allowlist as showcase). ([#45](https://github.com/MiaAI-Lab/sparkDash/pull/45))
- **Host CPU temperature** — dedicated GPU hosts (`kind: host`) show CPU temp on the RAM panel and Overview (hidden at 0°C / no sensor). Remote hosts now read hwmon/thermal over SSH. DGX Sparks still do not display CPU temp (remote Sparks still skip the extra sensor SSH). ([#34](https://github.com/MiaAI-Lab/sparkDash/pull/34))

### Changed
- Docker Node base image pulls from `public.ecr.aws/docker/library/node` so Spark builds do not fail on Docker Hub IPv6 `auth.docker.io` / “network is unreachable”. `deploy.sh` prints that workaround if a build still fails.
- README architecture diagram top border aligned with the box. ([#39](https://github.com/MiaAI-Lab/sparkDash/pull/39))

---

## [1.8.1] — 2026-08-16

### Added
- **Daily LLM tok/s history** — busy-sample rollups (peak + mean) for decode and prefill, persisted in `config/llm-daily.json` (30 UTC days). 14-day peak chart on the LLM card; `GET /api/sparks/:id/llm/daily`.
- **Cached vs uncached prefill tok/s** — live rows when the backend splits kinds: ds4 labeled prefill counters, llama.cpp `/slots` `n_prompt_tokens_cache`, SGLang `sglang:cached_tokens_total` (L1 `cache_source="device"`). Combined Prefill stays computed/uncached. vLLM is unchanged (combined prefill + prefix-cache hit rate).

### Changed
- **Docker SSH key auth** — compose comments + README: key auth runs inside the container (`/root/.ssh`), not the host user’s `~/.ssh`. Custom-named keys must be mounted as `id_ed25519` (or set `SSH_IDENTITY_FILE`). LAN IPs are from the sparkDash host. Add/Edit Spark hint when auth is Key.

### Fixed
- SGLang `/metrics` no longer overwrites `/get_server_info` tok/s when both are present.
- llama.cpp `n_prompt_tokens_processed: 0` is not treated as missing (fully cached prompts).

---

## [1.8.0] — 2026-08-15

### Added
- **Non-Spark unit support** (`kind: "host"`) — dedicated GPU hosts (any Linux box with an NVIDIA GPU, e.g. a workstation with an RTX card) are first-class units: added from the **+** button (choose **Dedicated GPU host**), monitored via SSH + `nvidia-smi` exactly like a Spark, but never labeled as a DGX Spark.
- **Detected host hardware** — for `kind: "host"`, the header shows real hardware detected once when online (GPU model, CUDA driver, CPU model/cores, system RAM) instead of fixed GB10 specs.
- **Separate system RAM vs discrete VRAM** — for host units, VRAM comes straight from `nvidia-smi` (`memory.used` / `memory.total`, free = total − used) while system RAM is read from `/proc/meminfo`. Spark behavior is unchanged (GB10 unified HBM pool).
- **RAM panel + Overview RAM bar** — host unit pages get a dedicated RAM panel, and Overview cards show a RAM bar under VRAM for hosts.
- **Host Resources layout** — host unit pages stack **RAM → Network → Storage** in the right column with **GPU** filling the left column (Sparks keep the original layout). CX7 IP is hidden for hosts (Spark-specific NIC).
- **Prefill tok/s** (moved from Unreleased) — live LLM panel sparkline, Overview cards as two columns (**tok/s** | **prefill**), decode-bench **Prefill** column (`prompt_tokens` ÷ TTFT).
- **Live prefill measurement** — vLLM uses engine-step `iteration_tokens_total` surplus over generation (so a short/cached prefill that lands in the same poll as the first decode tokens still counts); prompt/TTFT counters are the fallback because they often only move at first token. ds4 uses computed (not cached) prefill token diffs. Idle returns to 0. Opening a saved chat in the UI does not hit the GPU; prefill is the prompt/KV pass when you send or regenerate.

### Changed
- **Decode benchmark matches Showcase structural** — same prompt catalog and fill-to-max shaping (`min_tokens` / `ignore_eos` / fill suffix); no 4k unique prefill prefix. Temperature **0**, thinking **off** (Showcase defaults are temp 0.7 and thinking off). Default max tokens 512.
- **Update Hermes button is now a permanent, neutral control** — no more toast notifications for Hermes updates. It turns warning-yellow and shows a commit-count badge **only when an update is actually available**; clicking it opens the update dialog (status / pending commits / release notes) as before.
- **Overview "Update Hermes" button** (formerly "Update All") follows the same rule — neutral by default, warning-yellow with a pending-count badge only when ≥1 monitored Spark has an update available. Pressing it now shows a **live progress bar** (x/y Sparks settled, driven by WS per-Spark update status) until every started update finishes.

### Fixed
- **Decode benchmark "Benchmark not found" mid-run** — running jobs lived only in memory, so a `node --watch` / SIGTERM reload dropped them and the dialog poll hit 404. Active benches are now checkpointed to `config/bench-active.json`, finalized on shutdown, and recovered as interrupted on boot; the dialog also recovers via the list endpoint instead of showing a bare 404.

### Removed
- **Toast system** (`useToasts.ts`, `useHermesAlerts.ts`, `components/ui/Toaster.tsx`) — Hermes notifications now live entirely on the header button instead of pop-up toasts.

---

## [1.7.0] — 2026-08-08

### Added
- **Hermes Agent service per Spark** — opt-in `hermesMonitoring` toggle in Edit Spark; when on, sparkDash treats the Hermes Agent CLI (nousresearch/hermes-agent) as installed on that machine
- **Update notifications** — background `hermes update --check` poll (10 min) per monitored Spark; a toast alerts when an update is available
- **One-click update** — `Update Hermes` button in the Spark header and in the update alert toast; runs `hermes update` over SSH (non-interactive), with running/success/error state streamed over WS
- **Hermes status in snapshot** — installed / version / updateAvailable / behindCommits / checkedAt / job status per Spark (`snapshot.hermes`)
- **Toast system** — minimal built-in toast store + Toaster component (no new dependency), reused for Hermes alerts
- **Update confirmation dialog with real content** — clicking Update Hermes (header button or alert toast) opens a modal with **Update now** / **Cancel**. When the update is only commits on `main` (no newer tagged release than what is installed), it shows the **actual pending commits** from git (`HEAD..origin/main`) instead of the latest-release changelog — the full release changelog is shown only when a real version bump exists
- **`GET /api/sparks/:id/hermes/updates`** — update preview: latest release (cached) + installed version + **real pending commits** from git on the Spark + a resolved view; the old `/api/hermes/releases/latest` is superseded
- **Update All** — `POST /api/sparks/hermes/update-all` + Overview button runs `hermes update` on every Spark with Hermes Agent enabled (per-spark start/skip/fail summary; per-spark progress still streamed over WS)
- **`POST /api/sparks/:id/hermes/check`** (force check now) and **`POST /api/sparks/:id/hermes/update`** (background job, 202)

### Changed
- `hermesMonitoring` normalized in Spark config; server boots HermesProbe only when enabled (all roles, local + remote)
- Toast stack renders above modals at `z-index: 10000`

### Fixed
- **Local Spark Hermes runs as the wrong user (root), corrupting the install** — hermes + its git repo belong to the host user, but the local path executed hermes as the container root. That produced git "dubious ownership" failures and, once worked around, wrote root-owned files into the user's tree (tools/*.py, uv.lock, …) and ran `uv pip install` as root — which failed and left `venv/bin/hermes` missing, breaking the `hermes` CLI entirely. The local path now resolves the host user from the host passwd bind mount and drops to that user via `setpriv` (`nsenter` + host mount ns so host git is visible), with a self-healing root-owned-file repair step. Remote SSH already ran as the real user.
- **Broken launcher detection + auto-repair** — when the `hermes` launcher exists but cannot execute (e.g. missing venv entry point), sparkDash now reports "broken install" instead of a false "no update" and the one-click update automatically rebuilds the venv entry point (`uv pip install -e .`), then retries.
- **Stale git lock bricks later updates** — an interrupted `hermes update` can leave `.git/shallow.lock` (or any `*.lock`) behind, making every later fetch fail; leftover `*.lock` files are cleared before each check/update.

---

## [1.6.0] — 2026-08-07

### Added
Expand Down
16 changes: 13 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
# Dockerfile for arm64 (DGX Spark GB10 platform)
# ============================================================

FROM node:22-bookworm-slim AS builder
# library/node via public.ecr.aws — Docker Hub (docker.io) often resolves
# to IPv6; Sparks with no IPv6 route fail auth.docker.io with
# "network is unreachable". ECR public is the same official image, IPv4-first.
ARG NODE_IMAGE=public.ecr.aws/docker/library/node:22-bookworm-slim
FROM ${NODE_IMAGE} AS builder

WORKDIR /app

Expand All @@ -18,7 +22,11 @@ COPY package.json package-lock.json* ./
RUN npm ci --no-audit --no-fund \
|| (echo "npm ci failed once — retrying…" && npm cache clean --force && npm ci --no-audit --no-fund)

# Copy source and build
# Copy source and build. VITE_HISTORY_HOURS sets the frontend metrics-history
# retention window (see src/hooks/metricsStore.ts); override via
# `docker compose build --build-arg VITE_HISTORY_HOURS=4` or the env in compose.
ARG VITE_HISTORY_HOURS=8
ENV VITE_HISTORY_HOURS=${VITE_HISTORY_HOURS}
COPY . .
RUN npm run build

Expand All @@ -31,7 +39,7 @@ RUN npm prune --omit=dev --no-audit --no-fund \
# ============================================================
# Production image — lean runtime
# ============================================================
FROM node:22-bookworm-slim
FROM ${NODE_IMAGE}

# SSH client + sshpass for remote Sparks; util-linux provides nsenter for host GPU/net
RUN apt-get update && apt-get install -y --no-install-recommends \
Expand All @@ -46,6 +54,8 @@ COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json
COPY --from=builder /app/server ./server
COPY --from=builder /app/src/shared ./src/shared
COPY --from=builder /app/src/components/ShowcasePage/showcasePrompts.ts ./src/components/ShowcasePage/showcasePrompts.ts
COPY --from=builder /app/config ./config

# Volume for persistent sparks.json
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# sparkDash — Development Dockerfile with live reload
# ============================================================

FROM node:22-bookworm-slim
# See Dockerfile: avoid docker.io IPv6 auth failures on Sparks.
ARG NODE_IMAGE=public.ecr.aws/docker/library/node:22-bookworm-slim
FROM ${NODE_IMAGE}

WORKDIR /app

Expand Down
Loading