Skip to content

feat(llm): per-stream decode + effective prefill tok/s for TensorFold - #252

Closed
EagleClaw89 wants to merge 2 commits into
MiaAI-Lab:mainfrom
EagleClaw89:feat/tensorfold-per-stream-rates-v4
Closed

EagleClaw89 wants to merge 2 commits into
MiaAI-Lab:mainfrom
EagleClaw89:feat/tensorfold-per-stream-rates-v4

Conversation

@EagleClaw89

Copy link
Copy Markdown
Contributor

Why

Operators can see how hard a Spark is working (aggregate tok/s), but not how fast a conversation feels. Aggregate is throughput; per-stream is what a human experiences. This adds the missing half for TensorFold, measured from the engine's own counters.

What

  • perStreamTps — 1 / mean seconds per output token over the last window, from tensorfold:request_time_per_output_token_seconds. The histogram moves only on request completion, so the value is held between completions (never flashes 0), falls back to aggregate ÷ active streams, and stays null when nothing has been measured. Same design vocabulary as the existing Strata "active-slot mean", for TensorFold.
  • perStreamPrefillTps — effective prefill rate per request. Verified live: during a 35.4 s / 98,854-token prefill nothing in /metrics moves until the completion poll (prompt_tokens_total, the prefill histograms — all frozen; only requests_running changes). The rate is therefore derived as Δprompt ÷ Δprefill-seconds at completion and held, with a live "prefilling" indicator from requests_running > 0 && generation_tokens_running == 0 (verified: generation_tokens_running stays 0 across the whole prefill, > 0 during decode). prompt_tokens_total includes prefix-cache hits and /metrics publishes no cached counter, so the UI labels it "effective (incl. cached tokens)" — a cache-served prompt correctly reads ~140k tok/s while cold prefill on the same box measured 2,803 tok/s (ground truth 98,854 ÷ 35.14 s).
  • Running-count alias rule — TensorFold exports both requests_running and num_requests_running with identical values (verified across whole requests). Read the canonical name; use the alias only when the canonical series is absent; never sum. A test pins a single stream reporting 1, not 2.

Sanity gate + hold expiry on the prefill rate. Small prompts (the 1–200-token showcase/bench kind) and prefix-cache servings complete with sub-millisecond prefill times, so their Δprompt/Δprefill-seconds ratio is timer noise: observed live at 109k–313k tok/s against a verified cold-prefill ground truth of 2,803 tok/s (98,854 tokens ÷ 35.14 s, reproduced twice). Windows are therefore accepted only when Δprefill-seconds ≥ 50 ms, ≥ 50 prompt tokens, and an implied rate ≤ 15,000 tok/s (measured cold prefill on a DGX Spark is ~2.8–3.0k tok/s, so 15k is a 5x margin that keeps cache-credited agent windows — observed at 44k–313k — off the tile); rejected windows hold the last valid rate, and the hold expires after 20 s — the same staleness window the existing /health path already uses for its prefill estimate.

Exposed on the Prometheus endpoint as sparkdash_llm_per_stream_tokens_per_second and sparkdash_llm_per_stream_prefill_tokens_per_second. Both UI lines omit themselves when the engine reports nothing, so non-TensorFold cards are unchanged.

Tests

  • 9 new probe cases (TPOT window, hold-when-frozen, null when absent, queueOnly isolation, counter-reset re-seed, alias never-summed, prefill completion-window, prefill hold + prefillActive heuristic) + 2 UI cases.
  • Green: node --test server/collectors/__tests__/LlmProbe.*.test.js (197 pass), vitest for touched pages, npm run typecheck, npm run build.
  • Live on DGX Spark GB10 / TensorFold / Qwen3.8-Flash-Next: single 98,904-token request → requestsRunning: 1, prefillActive: true for the full 35 s (tile says "prefilling"), then prefillTps: 2803.6 + perStreamPrefillTps: 2803.6 held after completion; 2-stream bench → requestsRunning: 2, generationTps: 132.5, perStreamTps: 76.48.

… /metrics builds

Decode: perStreamTps = 1 / mean seconds per output token over the last
window, from tensorfold:request_time_per_output_token_seconds. The
histogram moves only when a request finishes, so the value is held (not
zeroed) between completions and falls back to aggregate / active streams
(null when idle and nothing measured).

Prefill: prompt_tokens_total and the prefill histograms advance ONLY in
the poll where a request finishes (verified on a live server: a 35 s
prefill moves nothing until it completes), so no mid-prefill rate
exists. Derive the per-request effective prefill rate from
delta prompt tokens / delta prefill seconds at completion and hold it;
show 'prefilling' while a request is open with generation_tokens_running
at 0 — the only live prefill signal this surface has. prompt_tokens_total
includes prefix-cache tokens and /metrics has no cached counter, so the
number is an effective rate, labelled as such.

Running count: TensorFold exports requests_running and the
num_requests_running alias with identical values; read the canonical
name, fall back to the alias only when absent, never sum (test pins the
single-stream case against double counting).

Exposed as sparkdash_llm_per_stream_tokens_per_second and
sparkdash_llm_per_stream_prefill_tokens_per_second on the Prometheus
endpoint.
… cards and the LLM panel

Decode tile gains 'N tok/s per stream'; Prefill tile gains
'N tok/s per request — effective (incl. cached tokens)' so prefix-cache
hits are never presented as raw GPU speed. Both lines omit themselves
when the engine reports nothing.
@MiaAI-Lab

Copy link
Copy Markdown
Owner

now on main as fa16c54, with the UI in d37dcc4. TensorFold completion-window counters feed per-stream decode and effective prefill fields, retaining noise guards and prefill expiry (server/collectors/LlmProbe.js:1856, :1917). The GB10-specific upper ceiling was removed because effective throughput includes cache credit; a 30,000 tok/s regression failed with the ceiling and now passes. Both commits passed typecheck, build and the full suite, with authorship and changelog credit preserved. (Review notes prepared with Claude Code on behalf of the maintainer.)

@MiaAI-Lab MiaAI-Lab closed this Oct 22, 2026
pavelkubicek added a commit to pavelkubicek/sparkDash that referenced this pull request Oct 27, 2026
78 upstream commits: MQTT publishing (MiaAI-Lab#248), harness occupancy helpers (MiaAI-Lab#208),
GPU clock cap from Settings (MiaAI-Lab#218), settings i18n foundation (MiaAI-Lab#226),
configurable GPU temperature limits (MiaAI-Lab#273), Locals/Splash engines,
TensorFold cache-counter fixes (MiaAI-Lab#252/MiaAI-Lab#260/MiaAI-Lab#272) and the zsh-safe remote probes (MiaAI-Lab#255/MiaAI-Lab#257).

Conflict resolutions:
- server/collectors/LlmProbe.js + its TensorFold test: took main's implementation,
  which supersedes this fork's /health `live` + /metrics work with the cache-counter
  precedence, running/waiting aliases, histogram validation and prefill holds. This
  drops the fork's parallel /metrics fetch (micro-optimisation only).
- SystemCollector: main's zsh-safe find probes merged into the fork's build/parse
  refactor, keeping the fork-only RAPL package-power reads; _getRemoteNetwork now uses
  the injected executor (the merge had left the fork body calling sshExec, which made
  main's zsh-glob corpus test run real SSH).
- OverviewPage: FleetKpis keeps the fork's placement but takes all sparks, so hidden
  cluster workers count in the KPI tiles (main's fix).
- SparkPage: main's column layout (History in the hardware column, storage/network in
  the right column) over the fork's full-width History panel.
- SettingsDialog: fork VulnHunter/REA opt-in toggles kept, metrics-history toggle
  switched to main's i18n keys.
- index.js: fork vulnHunt/rea spark cleanup kept alongside MQTT removeUnit/applySettings.
- CHANGELOG/README/package.json: both sides' entries kept; test globs unioned so the
  fork-only models/autopower/vulnhunt suites still run.
MiaAI-Lab added a commit that referenced this pull request Nov 6, 2026
The TensorFold tiles from #252 and #302 grew long captions ("Decode ·
aggregate", "40.0 tok/s · last completions", "Prefill · last completed",
"Effective rate · includes cached tokens", "Not measured live") that made
the Overview card taller than its numbers warrant. Keep the distinction
they carry but say it in fewer words: "Decode · total" (tooltip: sum
across running streams), "tok/s · completed", "Prefill · completed",
"incl. cached tokens", "no live sample"; the worker-of hint is trimmed
the same way. The full explanations stay in the hover tooltips. Both
locales updated; two unused caption keys removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants