Add passive Strata snapshot tooling and IQ3_S baseline

snapshot.sh reads /metrics and /v1/status only (no prompts), so it is
safe while the server is in use; summarize.py turns a snapshot into
Markdown and can compare two. First snapshot is the IQ3_S baseline
from 11.9 h of pi use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Witty
2026-10-26 13:15:19 -07:00
co-authored by Claude Opus 5.5
parent e59662b873
commit 7545ad6c2b
7 changed files with 210 additions and 0 deletions
+1
View File
@@ -53,6 +53,7 @@ proxmox/ files from lovey, at their real paths (etc/..., usr/local/bin/...,
strata/ strata-iq3_s.json, the engine config (API key removed)
pi/ ~/.pi/agent/models.json and settings.json for the client (API key removed)
docs/ per-layer notes, rebuild steps and the change log
benchmarks/ live metric snapshots (passive) and later benchmark runs
```
**Secrets are not in this repo** (it is public). The Strata API key lives only in
+29
View File
@@ -0,0 +1,29 @@
# Benchmarks
## Live snapshots (passive, safe while working)
`./benchmarks/snapshot.sh <label>` reads Strata's `/metrics` and `/v1/status` and writes
`live/<UTC time>-<label>/` with the raw JSON and a `summary.md`. It sends no prompts, so it doesn't
load the GPUs or disturb a session. To compare two snapshots:
```sh
python3 -I benchmarks/summarize.py benchmarks/live/<new> benchmarks/live/<old>
```
Caveats: the counters cover everything since the engine started, so the mix of work changes the numbers.
The detailed per-request list only holds the last 12 requests (`/api/requests` is off in this config).
For a before/after on a tuning or quant change, snapshot just before the change, then again after a
similar stretch of use. "Prompt tok/s (all reads)" mixes cold prompts with small cached tails; the
"cold, >=4K" row only fills in when a recent request read at least 4K uncached tokens.
## Standard benchmark (needs an idle server)
`strata.py bench run --url http://127.0.0.1:8080` on the guest runs Strata's fixed protocol
(`docs/BENCHMARK_PROTOCOL.md` in the Strata repo, 21 requests, ~2–4 min). This is the comparable
number for tuning and quant changes. **Only run it when nothing else is using the server.** Not run yet.
## Baseline
`live/2026-10-26T2015Z-baseline-iq3_s/`: IQ3_S, engine 0.1.51 at `4b9e05a2`, 250 W cap, gpu-unlock,
settings as in `strata/strata-iq3_s.json`. 758 requests over 11.9 h of pi use: decode 136.8 tok/s,
draft acceptance 75.8%, 98% of prompt tokens served from cache, expert cache hit ~99%.
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"service": "strata", "model": "qwen3.8-flash-next-iq3_s", "loaded": true, "auto_load": true, "structured_output": {"formats": ["json_object", "json_schema"], "method": "prompt_and_validate", "constrained_decoding": false, "stream_buffered": true}, "engine": "0.1.51", "started": 1793002959, "uptime_s": 42742, "cache_max_tokens": 131072, "context": {"native": 131072, "max_positions": 131072}, "concurrency": {"serving": 1, "requested": 1}, "dialects": ["/v1/chat/completions", "/v1/messages", "/v1/responses"], "vision": {"enabled": true, "available": true, "error": null}, "image_generation": {"enabled": false}, "activity": {"requests": 758, "in_flight": 0, "last_request_at": 1793045696}, "last_timings": {"cache_n": 57189, "prompt_n": 953, "prompt_ms": 1445.0, "prompt_per_token_ms": 1.516, "prompt_per_second": 659.5, "predicted_n": 333, "predicted_ms": 2499.9, "predicted_per_token_ms": 7.507, "predicted_per_second": 133.2, "draft_n": 305, "draft_n_accepted": 209, "at": 1793045696}, "vram": {"elastic": false}, "machine": {"at": 1793045701, "gpu": {"name": "NVIDIA GeForce RTX 3090 + NVIDIA GeForce RTX 3090", "used_mib": 48586.0, "total_mib": 49152.0, "util_pct": 0.0, "temp_c": 59, "power_w": 228.298}, "ram": {"used_gib": 77.3, "total_gib": 110.0}}}
@@ -0,0 +1,64 @@
# Strata live snapshot, 2026-10-26 20:15 UTC
Model `qwen3.8-flash-next-iq3_s`, engine 0.1.51. Passive counters from normal use, not a benchmark: mixed workloads, so compare like with like.
| Metric | Value |
|---|---:|
| Uptime (h) | 11.9 |
| Requests | 758 |
| Decode tok/s (all output) | 136.8 |
| Prompt tok/s (all reads) | 1,349.8 |
| Prompt tok/s (cold, >=4K, recent) | - |
| Draft acceptance % | 75.8 |
| Prompt tokens from cache % | 98.0 |
| Expert cache hit % (recent) | 99.0 |
| Experts over PCIe % (recent) | 0.1 |
| Output tokens | 1,125,514 |
| Prompt tokens read | 1,016,042 |
## Recent requests
| Prompt | Reused | Read | Prompt tok/s | Output | Decode tok/s | Draft acc % | Hit % | Finish |
|---:|---:|---:|---:|---:|---:|---:|---:|---|
| 58,142 | 57,189 | 953 | 659.5 | 333 | 133.2 | 68.5 | 99.6 | stop |
| 56,910 | 56,644 | 266 | 382.6 | 279 | 112.2 | 56.2 | 99.5 | stop |
| 56,145 | 55,930 | 215 | 374.4 | 498 | 110.9 | 57.1 | 99.5 | stop |
| 55,813 | 55,789 | 24 | 193.1 | 115 | 137.3 | 77.8 | 99.2 | stop |
| 55,202 | 54,807 | 395 | 490.1 | 585 | 137.9 | 75.4 | 98.8 | stop |
| 54,511 | 54,478 | 33 | 190.4 | 294 | 117.7 | 62.9 | 99.1 | stop |
| 54,361 | 54,326 | 35 | 204.8 | 116 | 125.2 | 70.3 | 99.4 | stop |
| 53,592 | 53,146 | 446 | 631.4 | 733 | 134.1 | 70.0 | 99.0 | stop |
| 53,041 | 52,819 | 222 | 451.9 | 106 | 146.7 | 75.7 | 97.2 | stop |
| 52,684 | 52,479 | 205 | 344.4 | 135 | 132.2 | 75.7 | 98.9 | stop |
| 50,103 | 50,086 | 17 | 184.4 | 2,376 | 128.7 | 68.3 | 99.2 | stop |
| 48,862 | 48,273 | 589 | 667.1 | 1,225 | 130.1 | 70.9 | 98.9 | stop |
## Engine settings
| Setting | Value |
|---|---|
| `version` | 0.1.51 |
| `max_context` | 131072 |
| `kv` | int8 |
| `kv_resident` | 32768 |
| `expert_slots` | 17889 |
| `expert_slots_primary` | 9508 |
| `expert_cache_mib` | 34724 |
| `expert_cache_primary_mib` | 17542 |
| `arena_mib` | 47962 |
| `spec` | 6 |
| `mtp_max` | 4 |
| `spec_min_p` | 0.50 |
| `drafting` | mtp |
| `prefill_cap` | 8192 |
| `prefill_stages` | 2 |
| `prefill_pipe` | 0 |
| `pcie_frac` | 0.34 |
| `conversation_cache_mib` | 8192 |
| `vram_free_mib` | 455 |
## Hardware at snapshot
- GPU0: 54°C, 115 W, 23.7/24.0 GiB, util 0%
- GPU1: 59°C, 113 W, 23.8/24.0 GiB, util 0%
- Guest RAM: 77.3/110.0 GiB
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Passive snapshot of Strata's live counters: GETs /metrics and /v1/status only, sends no prompts,
# so it is safe to run while the server is in use. Writes benchmarks/live/<UTC time>-<label>/.
# Usage: benchmarks/snapshot.sh [label] (run from the workstation; needs ssh to llm-nixos)
set -eu
host=${STRATA_HOST:-jason@192.168.1.67}
label=${1:-snapshot}
dir="$(dirname "$0")/live/$(date -u +%Y-%m-%dT%H%MZ)-$label"
mkdir -p "$dir"
for ep in metrics v1/status; do
ssh "$host" 'curl -sf -H "Authorization: Bearer $(sudo cat /var/lib/strata/api-key)" http://127.0.0.1:8080/'"$ep" \
> "$dir/$(basename "$ep").json"
done
python3 -I "$(dirname "$0")/summarize.py" "$dir" > "$dir/summary.md"
cat "$dir/summary.md"
+99
View File
@@ -0,0 +1,99 @@
"""Summarize a Strata live snapshot (metrics.json + status.json) as Markdown.
Usage: python3 summarize.py <snapshot dir> [<older snapshot dir>] (second dir adds a comparison column)
"""
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
ENGINE_KEYS = ["version", "max_context", "kv", "kv_resident", "expert_slots", "expert_slots_primary",
"expert_cache_mib", "expert_cache_primary_mib", "arena_mib", "spec", "mtp_max", "spec_min_p",
"drafting", "prefill_cap", "prefill_stages", "prefill_pipe", "pcie_frac", "conversation_cache_mib",
"vram_free_mib"]
def load(d):
d = Path(d)
return json.loads((d / "metrics.json").read_text()), json.loads((d / "status.json").read_text())
def stats(m):
t = m["totals"]
read = t["prompt_tokens"] - t["reused"]
out = {
"uptime_h": (m["time"] - t["since"]) / 3600,
"requests": t["requests"],
"decode_tok_s": t["output_tokens"] / (t["decode_ms"] / 1000) if t["decode_ms"] else None,
"prompt_read_tok_s": read / (t["prompt_ms"] / 1000) if t["prompt_ms"] else None,
"draft_accept_pct": 100 * t["drafts_accepted"] / t["drafts_offered"] if t["drafts_offered"] else None,
"prompt_reuse_pct": 100 * t["reused"] / t["prompt_tokens"] if t["prompt_tokens"] else None,
"output_tokens": t["output_tokens"],
"prompt_tokens_read": read,
}
# Cold prompts (little reuse, >= 4K read) show real prefill speed; the totals mix in tiny cached tails.
cold = [r for r in m["requests"] if r["prompt_read"] >= 4096 and r["prompt_ms"]]
if cold:
out["cold_prefill_tok_s"] = sum(r["prompt_read"] for r in cold) / (sum(r["prompt_ms"] for r in cold) / 1000)
recent = [r for r in m["requests"] if r["output_tokens"] >= 100]
if recent:
out["recent_hit_rate_pct"] = 100 * sum(r["hit_rate"] for r in recent) / len(recent)
out["recent_pcie_share_pct"] = 100 * sum(r["pcie_share"] for r in recent) / len(recent)
return out
def fmt(v):
if v is None:
return "-"
if isinstance(v, float):
return f"{v:,.1f}"
return f"{v:,}"
def main():
m, s = load(sys.argv[1])
old = load(sys.argv[2]) if len(sys.argv) > 2 else None
cur = stats(m)
prev = stats(old[0]) if old else {}
when = datetime.fromtimestamp(m["time"], timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
print(f"# Strata live snapshot, {when}\n")
print(f"Model `{s['model']}`, engine {s['engine']}. Passive counters from normal use, not a benchmark:"
" mixed workloads, so compare like with like.\n")
head = "| Metric | Value |" + (" Before |" if old else "")
print(head + "\n|---|---:|" + ("---:|" if old else ""))
labels = {
"uptime_h": "Uptime (h)", "requests": "Requests", "decode_tok_s": "Decode tok/s (all output)",
"prompt_read_tok_s": "Prompt tok/s (all reads)", "cold_prefill_tok_s": "Prompt tok/s (cold, >=4K, recent)",
"draft_accept_pct": "Draft acceptance %", "prompt_reuse_pct": "Prompt tokens from cache %",
"recent_hit_rate_pct": "Expert cache hit % (recent)", "recent_pcie_share_pct": "Experts over PCIe % (recent)",
"output_tokens": "Output tokens", "prompt_tokens_read": "Prompt tokens read",
}
for k, label in labels.items():
print(f"| {label} | {fmt(cur.get(k))} |" + (f" {fmt(prev.get(k))} |" if old else ""))
print("\n## Recent requests\n")
print("| Prompt | Reused | Read | Prompt tok/s | Output | Decode tok/s | Draft acc % | Hit % | Finish |")
print("|---:|---:|---:|---:|---:|---:|---:|---:|---|")
for r in m["requests"]:
ptps = r["prompt_read"] / (r["prompt_ms"] / 1000) if r["prompt_ms"] else None
acc = 100 * r["drafts_accepted"] / r["drafts_offered"] if r["drafts_offered"] else None
print(f"| {fmt(r['prompt_tokens'])} | {fmt(r['reused'])} | {fmt(r['prompt_read'])} | {fmt(ptps)} "
f"| {fmt(r['output_tokens'])} | {fmt(r['decode_tok_s'])} | {fmt(acc)} | {fmt(100 * r['hit_rate'])} "
f"| {r['finish']} |")
e = m["engine"]
print("\n## Engine settings\n")
print("| Setting | Value |\n|---|---|")
for k in ENGINE_KEYS:
if k in e:
print(f"| `{k}` | {e[k]} |")
g = m["hardware"]["gpus"]
print("\n## Hardware at snapshot\n")
for x in g:
print(f"- GPU{x['index']}: {x['temp']}°C, {x['power']:.0f} W, "
f"{x['mem_used'] / 2**30:.1f}/{x['mem_total'] / 2**30:.1f} GiB, util {x['util']}%")
print(f"- Guest RAM: {s['machine']['ram']['used_gib']}/{s['machine']['ram']['total_gib']} GiB")
if __name__ == "__main__":
main()