GUI-FB3 item 4: the speed cards show the run average when idle

The Monitor tab (serve/web/app.js), the API monitor page (monitor.js, monitor.html) and the Terminal Overview (strata_cli/overview.py) show the token-weighted
average of the run instead of the last request's speed when the server is idle; live values are unchanged. Ported by hand onto rc1 from the community patch
by keinebenutzer: https://github.com/keinebenutzer/STRATA-average-patch (MIT, Copyright (c) 2026 keinebenutzer), made against v0.1.55 c62d5e3a. Read in full
(no network, telemetry, eval or markup in what it adds); none of its files was run.
Tests: serve/test_speed_average.py (the page code run in node, display-only guard), tests/terminal/test_overview.py (edge cases, fallback).
This commit is contained in:
Niko1221 committed 2026-11-02 11:00:20 +01:00
1 parent 4a93c5428c
commit 6acd50efe4
6 files changed
+182 -11

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
"""The run average of the speed cards (community patch: keinebenutzer/STRATA-average-patch, MIT, made against v0.1.55 c62d5e3a; ported by hand onto rc1).
serve/web/app.js (the Monitor tab's Decode and Prefill cards), serve/web/monitor.js and monitor.html (the API monitor page) and strata_cli/overview.py (the Terminal
Overview) show, when the server is idle, the token-weighted average of the run instead of the last request's speed. Live values stay live. This file runs the
browser code the way a page would (a few lines of node, no browser, no network) and checks that what was added is display only."""
import json
import re
import shutil
import subprocess
import unittest
from pathlib import Path
WEB = Path(__file__).resolve().parent / "web"
NODE = shutil.which("node")
def run_node(source: str) -> object:
done = subprocess.run([NODE, "-e", source], capture_output=True, text=True, timeout=60, check=True)
return json.loads(done.stdout)
def function_source(text: str, name: str) -> str:
"""One top-level function of a browser script, by its opening line to its closing brace in column 0."""
start = text.index(f"function {name}(")
end = text.index("\n}\n", start) + 3
return text[start:end]
def cards_block(text: str) -> str:
"""The lines of renderMonitor that make the Decode and Prefill cards."""
start = text.index(" // the eight cards")
return text[start:text.index(' spark("sp-speed"', start)]
@unittest.skipUnless(NODE, "node is needed to run the page's own code")
class MonitorTab(unittest.TestCase):
"""serve/web/app.js: the two cards, run with stand-ins for the page's helpers."""
def cards(self, live, last, totals):
block = cards_block((WEB / "app.js").read_text(encoding="utf-8"))
script = ("const calls = []; const setMetric = (...a) => calls.push(a); const fmt = (n, d = 0) => Number(n).toFixed(d);\n"
f"function run(live, last, totals) {{\n{block}\n}}\nrun({json.dumps(live)}, {json.dumps(last)}, {json.dumps(totals)});\nconsole.log(JSON.stringify(calls));")
script = script.replace("spark(", "(() => {})(")
calls = run_node(script)
return {c[0]: (c[1], c[3]) for c in calls}
TOTALS = {"requests": 12, "prompt_tokens": 90000, "reused": 5000, "output_tokens": 40000, "prompt_ms": 120000.0, "decode_ms": 1000000.0}
LAST = {"decode_tok_s": 50.5, "prompt_ms": 2000.0, "prompt_tokens": 1000, "reused": 0}
def test_idle_shows_the_run_average_for_both_cards(self):
cards = self.cards({"state": "idle"}, self.LAST, self.TOTALS)
self.assertEqual(cards["speed"], ("40.0", "Decode · run average"))
self.assertEqual(cards["prefill"], ("708", "Prefill · run average"), "(90000 - 5000 reused) tokens in 120 s")
def test_live_values_stay_live(self):
self.assertEqual(self.cards({"state": "generating", "tok_s": 31.5, "prefill_tok_s_mean": 600}, self.LAST, self.TOTALS)["speed"], ("31.5", "Decode now"))
self.assertEqual(self.cards({"state": "generating", "tok_s": 31.5, "prefill_tok_s_mean": 600}, self.LAST, self.TOTALS)["prefill"], ("600", "Prefill this request"))
self.assertEqual(self.cards({"state": "reading", "prefill_tok_s_mean": 820}, self.LAST, self.TOTALS)["prefill"], ("820", "Prefill now"))
def test_without_totals_it_is_the_last_request_and_never_an_invented_zero(self):
for totals in (None, {}, {"output_tokens": 0, "decode_ms": 0, "prompt_tokens": 0, "prompt_ms": 0}):
cards = self.cards({"state": "idle"}, self.LAST, totals)
self.assertEqual(cards["speed"], ("50.5", "Decode last request"), totals)
self.assertEqual(cards["prefill"], ("500", "Prefill last request"), totals)
cards = self.cards({"state": "idle"}, None, None)
self.assertEqual(cards["speed"], (None, "Decode"))
self.assertEqual(cards["prefill"], (None, "Prefill"))
@unittest.skipUnless(NODE, "node is needed to run the page's own code")
class MonitorPage(unittest.TestCase):
def average(self, rows):
source = function_source((WEB / "monitor.js").read_text(encoding="utf-8"), "averageDecodeSpeed")
return run_node(f"{source}\nconsole.log(JSON.stringify(averageDecodeSpeed({json.dumps(rows)})));")
def test_it_is_token_weighted_over_the_finished_requests(self):
long_one = {"timings": {"predicted_n": 19511, "predicted_ms": 386000}}
short_one = {"timings": {"predicted_n": 11, "predicted_ms": 410}}
self.assertAlmostEqual(self.average([long_one, short_one]), (19511 + 11) / (386410 / 1000), places=6)
self.assertGreater(self.average([long_one, short_one]), 50, "a plain mean of the two (about 38) would let the short one count as much as the long one")
def test_requests_without_timings_or_with_one_token_are_left_out(self):
self.assertIsNone(self.average([]))
self.assertIsNone(self.average([{}, {"timings": None}, {"timings": {"predicted_n": 1, "predicted_ms": 10}}, {"timings": {"predicted_n": 5, "predicted_ms": 0}},
{"timings": {"predicted_n": "9", "predicted_ms": 5}}]))
self.assertEqual(self.average([{"timings": {"predicted_n": 100, "predicted_ms": 2000}}, {}]), 50)
def test_the_card_says_what_it_shows(self):
html = (WEB / "monitor.html").read_text(encoding="utf-8")
self.assertIn("Average decode speed", html)
self.assertIn("Run average", html)
self.assertNotIn("Last decode speed", html)
js = (WEB / "monitor.js").read_text(encoding="utf-8")
self.assertIn('text("speed", speed(averageDecodeSpeed(records.filter(r => !active(r)))))', js)
class AddedCodeIsDisplayOnly(unittest.TestCase):
"""What the patch added reads numbers the page already has and writes text with textContent: no request, no eval, no markup, no address."""
def test_nothing_but_arithmetic_and_text(self):
pieces = [cards_block((WEB / "app.js").read_text(encoding="utf-8")), function_source((WEB / "monitor.js").read_text(encoding="utf-8"), "averageDecodeSpeed"),
(Path(__file__).resolve().parents[1] / "strata_cli" / "overview.py").read_text(encoding="utf-8")]
pieces[2] = pieces[2][pieces[2].index("def run_average_speed"):pieces[2].index("def _cards")]
forbidden = re.compile(r"\b(fetch|XMLHttpRequest|WebSocket|sendBeacon|eval|Function|import|require|innerHTML|insertAdjacentHTML|document\.write|"
r"subprocess|socket|urllib|requests|os\.system|open)\s*\(|https?://|\.src\s*=|new Image")
for piece in pieces:
self.assertIsNone(forbidden.search(piece), forbidden.search(piece) and forbidden.search(piece).group(0))
self.assertIn("textContent", (WEB / "monitor.js").read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()
+9 -4
View File
@@ -464,13 +464,18 @@ function renderMonitor(live, hw, st, eng, h, last, requests, totals, kept) {
$("state-bar").style.width = `${pct}%`;
// the eight cards
const speed = live.state === "generating" ? live.tok_s : last ? last.decode_tok_s : null;
// Idle: not the last request's speed (one request is noisy) but the run's average from the server's totals
// (token-weighted, the same figure the engine's DONE lines and Prometheus report). Live stays live.
const avgDecode = totals && totals.decode_ms > 0 && totals.output_tokens > 0 ? totals.output_tokens / (totals.decode_ms / 1000) : null;
const readTokens = totals ? totals.prompt_tokens - (totals.reused || 0) : 0;
const avgPrefill = totals && totals.prompt_ms > 0 && readTokens > 0 ? readTokens / (totals.prompt_ms / 1000) : null;
const speed = live.state === "generating" ? live.tok_s : avgDecode != null ? avgDecode : last ? last.decode_tok_s : null;
setMetric("speed", speed == null ? null : fmt(speed, 1), "t/s",
live.state === "generating" ? "Decode now" : last ? "Decode last request" : "Decode");
live.state === "generating" ? "Decode now" : avgDecode != null ? "Decode · run average" : last ? "Decode last request" : "Decode");
const prefill = live.state !== "idle" ? live.prefill_tok_s_mean
: last && last.prompt_ms > 0 ? Math.max(0, last.prompt_tokens - (last.reused || 0)) / (last.prompt_ms / 1000) : null;
: avgPrefill != null ? avgPrefill : last && last.prompt_ms > 0 ? Math.max(0, last.prompt_tokens - (last.reused || 0)) / (last.prompt_ms / 1000) : null;
setMetric("prefill", prefill == null ? null : fmt(prefill), "t/s",
live.state === "reading" ? "Prefill now" : live.state === "generating" ? "Prefill this request" : last ? "Prefill last request" : "Prefill");
live.state === "reading" ? "Prefill now" : live.state === "generating" ? "Prefill this request" : avgPrefill != null ? "Prefill · run average" : last ? "Prefill last request" : "Prefill");
spark("sp-speed", h.tok_s);
spark("sp-prefill", h.prefill_tok_s_mean);
// a model split across several cards (issue #112): the cards show their total / mean / hottest, and each card's own
+1 -1
View File
@@ -23,7 +23,7 @@ header{height:70px;padding:0 32px;display:flex;align-items:center;justify-conten
<div class="card"><div class="label">Model</div><div id="model-state" class="value"><span class="dot"></span><span id="loaded">Connecting…</span></div><div id="model" class="model">127.0.0.1:5001/v1</div></div>
<div class="card"><div class="label">Requests retained</div><div id="count" class="value">—</div><div id="active" class="small">Waiting for server</div></div>
<div class="card"><div class="label">Last wall-clock</div><div id="wall" class="value">—</div><div class="small">Includes queue and model loading</div></div>
<div class="card"><div class="label">Last decode speed</div><div id="speed" class="value">—</div><div class="small">Engine timing · tokens / second</div></div>
<div class="card"><div class="label">Average decode speed</div><div id="speed" class="value">—</div><div class="small">Run average · tokens / second</div></div>
</div>
<section class="panel"><div class="panel-bar"><h2>Requests</h2><div class="filters"><input id="search" aria-label="Filter requests" placeholder="Search ID or endpoint"><select id="filter" aria-label="Request status"><option value="all">All statuses</option><option value="active">Active</option><option value="completed">Completed</option><option value="error">Errors</option></select></div></div>
<div class="workspace"><div id="requests" class="requests" aria-label="Request list"></div><div class="detail">
+14 -1
View File
@@ -6,6 +6,19 @@ document.documentElement.dataset.theme = localStorage.getItem("strata.theme") ||
const seconds = n => typeof n === "number" ? `${n.toFixed(2)} s` : "—";
const speed = n => typeof n === "number" ? n.toFixed(1) : "—";
const active = r => !["completed", "error", "disconnected"].includes(r.state);
// The run's average decode speed: every token produced over the run divided by the run's decode time, from each
// finished request's engine timings (predicted_n over predicted_ms). Token-weighted, so a short request does not
// outweigh a long one - the same figure the engine's DONE lines and the server's totals report. Not the last request.
function averageDecodeSpeed(rows) {
let tokens = 0, ms = 0;
for (const r of rows) {
const t = r.timings;
if (!t) continue;
const n = t.predicted_n, d = t.predicted_ms;
if (typeof n === "number" && typeof d === "number" && n >= 2 && d > 0) { tokens += n; ms += d; }
}
return ms > 0 ? tokens / (ms / 1000) : null;
}
function text(id, value) { const el = $(id); if (el.textContent !== String(value)) el.textContent = value; }
async function api(path, body) {
const response = await fetch(path, {cache:"no-store", headers:{...(apiKey ? {Authorization:`Bearer ${apiKey}`} : {}), ...(body !== undefined ? {"Content-Type":"application/json"} : {})}, ...(body !== undefined ? {method:"POST", body:JSON.stringify(body)} : {})});
@@ -85,7 +98,7 @@ async function refresh() {
text("model", status.model); text("count", records.length);
text("active", inflight ? `${inflight} active / queued` : status.auto_load ? "Loads automatically on the next request" : "No active requests");
const last = records.find(r => !active(r));
text("wall", last ? seconds(last.wallclock_s) : "—"); text("speed", speed(last?.timings?.predicted_per_second));
text("wall", last ? seconds(last.wallclock_s) : "—"); text("speed", speed(averageDecodeSpeed(records.filter(r => !active(r)))));
$("load").disabled = operating || Boolean(inflight) || status.loaded; $("unload").disabled = operating || Boolean(inflight) || !status.loaded;
list(); if (selected) await showDetail();
text("updated", `Live · updated ${new Date().toLocaleTimeString()}`);
+14 -1
View File
@@ -61,6 +61,16 @@ def request_speed(r: dict) -> float | None:
return round(out / (ms / 1000.0), 1) if out and out >= 2 and ms and ms > 0 else None # one token has no decode span (a probe showed 1,250 tok/s)
def run_average_speed(metrics: dict) -> float | None:
"""The run's average decode speed: every token the server produced over the whole run divided by the run's
decode time (the server's `totals`, the same token-weighted figure the engine's DONE lines and Prometheus use).
Not the last request's speed - a single request is noisy; this is the average over everything since the start.
None when the server reported no totals (never an invented zero)."""
totals = metrics.get("totals") or {}
out, ms = _num(totals.get("output_tokens")), _num(totals.get("decode_ms"))
return round(out / (ms / 1000.0), 1) if out and out >= 2 and ms and ms > 0 else None
def _cards(hw: dict, static: dict) -> list[dict]:
"""Each graphics card's own readings when the server lists two or more (hardware.gpus); [] for one card or none: the Overview then
draws exactly what it always drew."""
@@ -125,7 +135,10 @@ def build(metrics: dict | None) -> dict | None:
elif state == "Reading" and _num(live.get("prefill_tok_s_mean")) is not None:
speed, speed_note = _num(live.get("prefill_tok_s_mean")), "reading the prompt"
else:
speed, speed_note = (request_speed(last) if last else None), "last request"
# Idle: not the last request's speed (one request is noisy) but the run's average, from the server's totals.
speed, speed_note = run_average_speed(metrics), "average of the run"
if speed is None:
speed, speed_note = (request_speed(last) if last else None), "last request"
slots, total_slots = _num(engine.get("expert_slots")), _num(engine.get("experts"))
cache_mib = _num(engine.get("expert_cache_mib"))
gen, genmax, width = _num(hw.get("gpu_pcie_gen")), _num(hw.get("gpu_pcie_gen_max")), _num(hw.get("gpu_pcie_width"))
+32 -4
View File
@@ -32,7 +32,9 @@ def metrics(state="reading", **over):
"requests": [{"id": "a", "time": now - 5, "duration_s": 429.8, "finish_reason": "stop", "prompt_tokens": 17183, "reused": 0,
"output_tokens": 19511, "decode_ms": 386000},
{"id": "b", "time": now - 9, "duration_s": 4.0, "finish_reason": "stop", "prompt_tokens": 626, "reused": 0,
"output_tokens": 11, "decode_ms": 410}]}
"output_tokens": 11, "decode_ms": 410}],
"totals": {"requests": 12, "prompt_tokens": 90000, "reused": 5000, "output_tokens": 40000,
"prompt_ms": 120000.0, "decode_ms": 1000000.0}}
m.update(over)
return m
@@ -58,14 +60,40 @@ class Build(unittest.TestCase):
self.assertEqual(m["context"][0], 36742 + 120)
self.assertEqual(m["progress"], (120.0, 4096.0))
def test_idle_shows_the_last_request(self):
def test_idle_shows_the_run_average(self):
m = overview.build(metrics("idle"))
self.assertEqual(m["state"], "Idle")
self.assertEqual(m["speed"][1], "last request")
self.assertAlmostEqual(m["speed"][0], 50.5, places=1) # 19511 tokens in 386 s
self.assertEqual(m["speed"][1], "average of the run")
self.assertAlmostEqual(m["speed"][0], 40.0, places=1) # totals: 40000 tokens in 1000 s, not the last request's 50.5
self.assertEqual(m["context"][0], 17183 + 19511)
self.assertEqual(m["progress"], (None, None))
def test_the_run_average_is_token_weighted_and_never_invented(self):
"""Community patch (keinebenutzer/STRATA-average-patch, MIT): the idle tile is every token over the run's decode time, from the server's totals."""
self.assertEqual(overview.run_average_speed({"totals": {"output_tokens": 40000, "decode_ms": 1000000.0}}), 40.0)
# a long request and a short one: the long one counts for what it is (a plain mean of the two would be 30.3)
self.assertEqual(overview.run_average_speed({"totals": {"output_tokens": 19522, "decode_ms": 386410.0}}), 50.5)
for totals in (None, {}, {"output_tokens": 0, "decode_ms": 100.0}, {"output_tokens": 1, "decode_ms": 100.0}, {"output_tokens": 500, "decode_ms": 0},
{"output_tokens": 500}, {"decode_ms": 500.0}, {"output_tokens": True, "decode_ms": 5.0}, {"output_tokens": "9", "decode_ms": 3.0},
{"output_tokens": float("nan"), "decode_ms": 3.0}):
self.assertIsNone(overview.run_average_speed({"totals": totals}), totals)
self.assertIsNone(overview.run_average_speed({}))
def test_without_totals_idle_falls_back_to_the_last_request(self):
m = metrics("idle")
del m["totals"]
built = overview.build(m)
self.assertEqual(built["speed"][1], "last request")
self.assertAlmostEqual(built["speed"][0], 50.5, places=1)
m = metrics("idle", totals={"requests": 0, "output_tokens": 0, "decode_ms": 0.0})
self.assertEqual(overview.build(m)["speed"][1], "last request", "an empty run is not an average of zero")
def test_live_values_stay_live(self):
"""Only the idle value is an average: generating and reading are what they were."""
self.assertEqual(overview.build(metrics("generating"))["speed"][:2], (50.4, "now"))
self.assertEqual(overview.build(metrics("reading"))["speed"][:2], (820.0, "reading the prompt"))
self.assertIn("average of the run", draw(overview.build(metrics("idle"))))
def test_queued_and_error(self):
self.assertEqual(overview.build(metrics("idle", live={"state": "idle", "queued": 3}))["state"], "Queued")
self.assertEqual(overview.build(metrics("idle", engine={"model": "x", "error": "boom"}))["state"], "Error")