inferencekit-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inferencekit/__init__.py +9 -0
- inferencekit/__main__.py +5 -0
- inferencekit/analysis/__init__.py +1 -0
- inferencekit/analysis/ncu_csv.py +284 -0
- inferencekit/analysis/nsys_sqlite.py +518 -0
- inferencekit/canonical.py +104 -0
- inferencekit/checkpoint.py +396 -0
- inferencekit/cli.py +274 -0
- inferencekit/cuda/bw_peak.cu +252 -0
- inferencekit/cuda/torch_bw.py +60 -0
- inferencekit/engines/__init__.py +7 -0
- inferencekit/engines/adapters.py +190 -0
- inferencekit/engines/clip/__init__.py +13 -0
- inferencekit/engines/clip/comfyui.py +749 -0
- inferencekit/engines/clip/diffusers_ltx.py +601 -0
- inferencekit/engines/clip/noise.py +46 -0
- inferencekit/engines/clip/official_ltx.py +316 -0
- inferencekit/engines/clip/server.py +542 -0
- inferencekit/engines/clip/stepdump.py +182 -0
- inferencekit/engines/gates.py +94 -0
- inferencekit/engines/sglang_worker.py +86 -0
- inferencekit/engines/vllm_worker.py +129 -0
- inferencekit/evidence.py +427 -0
- inferencekit/gpuinfo.py +93 -0
- inferencekit/hardware.py +135 -0
- inferencekit/mcp_server.py +178 -0
- inferencekit/ops/__init__.py +30 -0
- inferencekit/ops/bench.py +184 -0
- inferencekit/ops/bench_audio.py +384 -0
- inferencekit/ops/bench_clip.py +819 -0
- inferencekit/ops/bench_image.py +393 -0
- inferencekit/ops/bench_rounds.py +297 -0
- inferencekit/ops/bench_serve.py +866 -0
- inferencekit/ops/ceiling.py +135 -0
- inferencekit/ops/compare.py +378 -0
- inferencekit/ops/correctness.py +153 -0
- inferencekit/ops/doctor.py +486 -0
- inferencekit/ops/gates.py +1772 -0
- inferencekit/ops/gates_clip.py +930 -0
- inferencekit/ops/intake.py +1321 -0
- inferencekit/ops/ledger.py +749 -0
- inferencekit/ops/manifest.py +309 -0
- inferencekit/ops/ncu.py +183 -0
- inferencekit/ops/nsys.py +224 -0
- inferencekit/ops/prompts.py +316 -0
- inferencekit/ops/quality.py +2161 -0
- inferencekit/ops/quality_clip.py +690 -0
- inferencekit/ops/quality_clip_steps.py +341 -0
- inferencekit/ops/report.py +317 -0
- inferencekit/ops/review.py +119 -0
- inferencekit/ops/rocprof.py +89 -0
- inferencekit/ops/roofline.py +392 -0
- inferencekit/ops/store.py +61 -0
- inferencekit/proc.py +172 -0
- inferencekit/prompts/ATTRIBUTION.md +56 -0
- inferencekit/prompts/__init__.py +167 -0
- inferencekit/prompts/build.py +518 -0
- inferencekit/prompts/dev-load.jsonl.gz +0 -0
- inferencekit/prompts/dev-load.manifest.json +212 -0
- inferencekit/prompts/dev.jsonl.gz +0 -0
- inferencekit/prompts/dev.manifest.json +214 -0
- inferencekit/prompts/exclusions.json +54 -0
- inferencekit/quality/__init__.py +23 -0
- inferencekit/quality/client.py +66 -0
- inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
- inferencekit/quality/fake.py +329 -0
- inferencekit/quality/gsm8k.py +224 -0
- inferencekit/quality/logprobs.py +106 -0
- inferencekit/quality/metrics.py +235 -0
- inferencekit/quality/niah.py +63 -0
- inferencekit/quality/parquet.py +296 -0
- inferencekit/quality/sampling.py +262 -0
- inferencekit/quality/step_replay.py +217 -0
- inferencekit/quality/video_metrics.py +304 -0
- inferencekit/registry.py +206 -0
- inferencekit/review/__init__.py +6 -0
- inferencekit/review/diff.py +163 -0
- inferencekit/review/media.py +181 -0
- inferencekit/review/page.py +920 -0
- inferencekit/review/rundir.py +844 -0
- inferencekit/roofline_headers.py +504 -0
- inferencekit/schemas/evidence.v1.json +130 -0
- inferencekit/schemas/evidence.v2.json +499 -0
- inferencekit/schemas/intake.v1.json +276 -0
- inferencekit/schemas/run-manifest.v1.json +118 -0
- inferencekit/serve/__init__.py +8 -0
- inferencekit/serve/audio_client.py +330 -0
- inferencekit/serve/client.py +446 -0
- inferencekit/serve/clip_client.py +351 -0
- inferencekit/serve/clip_rounds.py +429 -0
- inferencekit/serve/image_client.py +373 -0
- inferencekit/serve/loadgen.py +136 -0
- inferencekit/serve/metrics.py +203 -0
- inferencekit/serve/profiles.py +105 -0
- inferencekit/serve/prompts_io.py +125 -0
- inferencekit/serve/rounds.py +1111 -0
- inferencekit/serve/servers.py +897 -0
- inferencekit/stats.py +249 -0
- inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
- inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
- inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
- inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"""ledger trailers / history: every kept commit carries its numbers as git trailers.
|
|
2
|
+
|
|
3
|
+
``ledger trailers <ev...>`` turns evidence records into a trailer block for a commit message:
|
|
4
|
+
|
|
5
|
+
Perf-Workload: Qwen/Qwen3-8B FP8, RTX 5090, vllm 0.11.0, batch 1, prompt 1024, gen 256
|
|
6
|
+
Perf-Decode-Tok-S: 185.3 (95% CI 185.2-185.3) [+2.1% vs 1a2b3c4]
|
|
7
|
+
Perf-TTFT-Ms: 60.7 (95% CI 60.1-61.0)
|
|
8
|
+
Perf-P95-Latency-Ms: 1452
|
|
9
|
+
Perf-Quality: greedy prefix agreement 100.00% (5/5 identical) PASS
|
|
10
|
+
Perf-Cost: $0.93 per 1M output tokens at $0.62/GPU-h (1 stream)
|
|
11
|
+
Perf-Capacity-N: 24 streams (SLO p95 TTFT <= 300 ms, p95 TPOT <= 50 ms, failures <= 0)
|
|
12
|
+
Perf-Goodput: 18.2 req/s at N*=24
|
|
13
|
+
Perf-Evidence: ev_3f2a..., ev_9c1d...
|
|
14
|
+
Perf-Baseline: 1a2b3c4
|
|
15
|
+
|
|
16
|
+
Rules: a metric the evidence does not contain is omitted (listed under ``omitted`` with the
|
|
17
|
+
reason), never written as zero. Values are read by field presence, so v1 ``benchmark`` and
|
|
18
|
+
v2 ``serve_benchmark`` / ``capacity`` / ``quality_*`` records both work. The delta against a
|
|
19
|
+
baseline comes from a ``comparison`` record when one is given (ratio of medians with its
|
|
20
|
+
CI and verdict); otherwise from ``--baseline-ev`` as a plain ratio of medians, labelled
|
|
21
|
+
"ratio of medians, no CI" inside the trailer itself. Values never contain a newline, so
|
|
22
|
+
evidence text cannot forge extra trailers.
|
|
23
|
+
|
|
24
|
+
One commit, one engine: speed and capacity records of more than one engine label (or more
|
|
25
|
+
than one single-user speed record) are refused unless ``--label`` picks one; the baseline's
|
|
26
|
+
record goes in ``--baseline-ev``. Prices are per GPU-hour, as ``bench serve`` takes them: a
|
|
27
|
+
capacity record's own cost is printed at its own price, and a different ``--gpu-price-usd-h``
|
|
28
|
+
reprices it from the record's per-GPU throughput at N* (derived). In text mode the trailer
|
|
29
|
+
block goes to stdout (for ``git commit -m "$(...)"``) and what was omitted or limited goes to
|
|
30
|
+
stderr, so it is seen but never lands in the commit message.
|
|
31
|
+
|
|
32
|
+
``history`` reads the trailers back with ``git log``, prints a table and a sparkline per
|
|
33
|
+
metric (oldest to newest), and with ``--tag-best`` points local tags ``perf/best-<metric>``
|
|
34
|
+
at the best commit. Tags are never pushed.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import re
|
|
41
|
+
import statistics
|
|
42
|
+
import sys
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
from inferencekit import evidence as ev
|
|
46
|
+
from inferencekit.canonical import sha256_hex
|
|
47
|
+
from inferencekit.proc import run as run_cmd
|
|
48
|
+
from inferencekit.registry import Arg, OpError, operation
|
|
49
|
+
|
|
50
|
+
ORDER = ("Perf-Workload", "Perf-Decode-Tok-S", "Perf-TTFT-Ms", "Perf-P95-Latency-Ms", "Perf-Quality", "Perf-Cost",
|
|
51
|
+
"Perf-Capacity-N", "Perf-Goodput", "Perf-Evidence", "Perf-Baseline")
|
|
52
|
+
SPEED_KINDS = ("benchmark", "serve_benchmark")
|
|
53
|
+
# Metric names accepted for each headline (v1 names first; v2 aliases after).
|
|
54
|
+
DECODE = ("decode_tok_s", "decode_tok_s_median")
|
|
55
|
+
TTFT = ("ttft_ms", "ttft_p50_ms", "ttft_median_ms")
|
|
56
|
+
P95_DIRECT = ("e2e_p95_ms", "p95_e2e_ms", "latency_p95_ms", "e2e_latency_p95_ms", "total_p95_ms")
|
|
57
|
+
P95_BY_STAT = ("e2e_ms", "e2e_latency_ms", "latency_ms", "total_ms")
|
|
58
|
+
N_STAR = ("n_star", "capacity_n", "n_star_streams")
|
|
59
|
+
GOODPUT = ("goodput_rps", "goodput_req_s", "goodput")
|
|
60
|
+
THROUGHPUT_PER_GPU = ("output_tok_s_per_gpu", "output_tok_s", "throughput_tok_s")
|
|
61
|
+
COST = ("usd_per_1m_output_tokens", "cost_usd_per_1m_output_tokens", "usd_per_1m_tokens", "cost_per_1m_tokens_usd")
|
|
62
|
+
CENTRAL = (None, "median", "p50", "mean")
|
|
63
|
+
SPARK = "▁▂▃▄▅▆▇█"
|
|
64
|
+
MAX_SUBJECTS = 6
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def short(eid: str | None) -> str:
|
|
68
|
+
return (eid or "?")[:15]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def one_line(text) -> str:
|
|
72
|
+
"""Trailer values are one line: newlines (and other control characters) become spaces."""
|
|
73
|
+
return re.sub(r"[\x00-\x1f\x7f]+", " ", str(text)).strip()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def num(x: float | None, digits: int = 4) -> str:
|
|
77
|
+
if x is None:
|
|
78
|
+
return "n/a"
|
|
79
|
+
if float(x).is_integer() and abs(x) < 1e15:
|
|
80
|
+
return str(int(x))
|
|
81
|
+
if abs(x) >= 1000:
|
|
82
|
+
return f"{x:.0f}"
|
|
83
|
+
if abs(x) >= 100:
|
|
84
|
+
return f"{x:.1f}"
|
|
85
|
+
return f"{x:.{digits}g}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------------------- reading records
|
|
89
|
+
|
|
90
|
+
def metrics_named(rec: dict, names, stats=CENTRAL) -> list[dict]:
|
|
91
|
+
out = []
|
|
92
|
+
for name in names:
|
|
93
|
+
for m in rec.get("metrics") or []:
|
|
94
|
+
if not isinstance(m, dict) or m.get("name") != name or m.get("value") is None:
|
|
95
|
+
continue
|
|
96
|
+
if stats is not None and m.get("stat") not in stats:
|
|
97
|
+
continue
|
|
98
|
+
out.append(m)
|
|
99
|
+
if out:
|
|
100
|
+
return out
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def pick(recs: list[dict], names, stats=CENTRAL, kinds=None) -> tuple[list[dict], dict] | None:
|
|
105
|
+
"""The first record (in the order given) holding metric ``names``: (metrics, record).
|
|
106
|
+
|
|
107
|
+
A metric without a subject/bucket is the overall value and wins; otherwise every
|
|
108
|
+
per-subject value is returned (e.g. one per input bucket).
|
|
109
|
+
"""
|
|
110
|
+
for rec in recs:
|
|
111
|
+
if kinds and rec.get("kind") not in kinds:
|
|
112
|
+
continue
|
|
113
|
+
found = metrics_named(rec, names, stats)
|
|
114
|
+
if not found:
|
|
115
|
+
continue
|
|
116
|
+
overall = [m for m in found if m.get("subject") in (None, "", "all") and not m.get("bucket")]
|
|
117
|
+
return (overall[:1] if overall else found[:MAX_SUBJECTS]), rec
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def kind_of(rec: dict) -> str | None:
|
|
122
|
+
"""The record's kind; v2 kinds written as a v1 kind plus data.v2_kind (before the v2 schema merged) count."""
|
|
123
|
+
data = rec.get("data") or {}
|
|
124
|
+
return data.get("v2_kind") or ("gates" if data.get("gates_format") else rec.get("kind"))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def gpu_count(rec: dict) -> int:
|
|
128
|
+
g = (rec.get("identity") or {}).get("gpu") or {}
|
|
129
|
+
n = g.get("count") or (rec.get("data") or {}).get("gpu_count") or 1
|
|
130
|
+
return int(n) if isinstance(n, (int, float)) and n >= 1 else 1
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def is_load(rec: dict) -> bool:
|
|
134
|
+
return ((rec.get("identity") or {}).get("workload") or {}).get("mode") == "load"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def label_of(rec: dict) -> str | None:
|
|
138
|
+
label = (rec.get("data") or {}).get("label") or ((rec.get("identity") or {}).get("engine") or {}).get("name")
|
|
139
|
+
return str(label) if label else None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def with_ci(m: dict) -> str:
|
|
143
|
+
text = num(m["value"])
|
|
144
|
+
ci = m.get("ci95")
|
|
145
|
+
if isinstance(ci, list) and len(ci) == 2 and None not in ci:
|
|
146
|
+
text += f" (95% CI {num(ci[0])}-{num(ci[1])})"
|
|
147
|
+
return text
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def values_text(ms: list[dict]) -> str:
|
|
151
|
+
if len(ms) == 1:
|
|
152
|
+
return with_ci(ms[0])
|
|
153
|
+
# Several subjects (e.g. buckets) and no overall value: the first number is the first subject's.
|
|
154
|
+
return "; ".join(f"{with_ci(m)} {one_line(m.get('subject') or m.get('bucket'))}" for m in ms)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def slo_text(slo: dict | None) -> str:
|
|
158
|
+
if not isinstance(slo, dict):
|
|
159
|
+
return ""
|
|
160
|
+
parts = []
|
|
161
|
+
if slo.get("ttft_p95_ms") is not None:
|
|
162
|
+
parts.append(f"p95 TTFT <= {num(slo['ttft_p95_ms'])} ms")
|
|
163
|
+
if slo.get("tpot_p95_ms") is not None:
|
|
164
|
+
parts.append(f"p95 TPOT <= {num(slo['tpot_p95_ms'])} ms")
|
|
165
|
+
if slo.get("max_fail") is not None:
|
|
166
|
+
parts.append(f"failures <= {slo['max_fail']}")
|
|
167
|
+
return ", ".join(parts)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def workload_text(rec: dict) -> str:
|
|
171
|
+
ident = rec.get("identity") or {}
|
|
172
|
+
m, g, e, w = (ident.get(k) or {} for k in ("model", "gpu", "engine", "workload"))
|
|
173
|
+
parts = []
|
|
174
|
+
model = " ".join(str(x) for x in (m.get("id"), (m.get("quant") or m.get("dtype") or "").upper() or None) if x)
|
|
175
|
+
if model:
|
|
176
|
+
parts.append(model)
|
|
177
|
+
if g.get("name"):
|
|
178
|
+
parts.append(f"{g['count']}x {g['name']}" if (g.get("count") or 1) > 1 else str(g["name"]))
|
|
179
|
+
if e.get("name"):
|
|
180
|
+
parts.append(" ".join(str(x) for x in (e.get("name"), e.get("version")) if x))
|
|
181
|
+
if w.get("mode"):
|
|
182
|
+
parts.append({"single": "single-user", "load": "open-loop load"}.get(w["mode"], str(w["mode"])))
|
|
183
|
+
for key, label in (("batch", "batch"), ("concurrency", "concurrency"), ("rate_rps", "rate"),
|
|
184
|
+
("prompt_len", "prompt"), ("gen_len", "gen"), ("bucket", "bucket")):
|
|
185
|
+
v = w.get(key)
|
|
186
|
+
if v is not None:
|
|
187
|
+
shown = ",".join(num(x) if isinstance(x, (int, float)) else str(x) for x in v) if isinstance(v, list) \
|
|
188
|
+
else (num(v) if isinstance(v, float) else v)
|
|
189
|
+
parts.append(f"{label} {shown}" + (" req/s" if key == "rate_rps" else ""))
|
|
190
|
+
ps = w.get("prompt_set")
|
|
191
|
+
if isinstance(ps, dict) and ps.get("name"):
|
|
192
|
+
parts.append(f"prompts {ps['name']}" + (f" ({str(ps['sha256'])[:12]})" if ps.get("sha256") else ""))
|
|
193
|
+
return one_line(", ".join(parts))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# --------------------------------------------------------------------------- building trailers
|
|
197
|
+
|
|
198
|
+
class Builder:
|
|
199
|
+
def __init__(self):
|
|
200
|
+
self.trailers: list[dict] = []
|
|
201
|
+
self.omitted: list[dict] = []
|
|
202
|
+
self.limitations: list[str] = []
|
|
203
|
+
|
|
204
|
+
def add(self, key: str, value: str, claim_class: str, sources: list[str]):
|
|
205
|
+
self.trailers.append({"key": key, "value": one_line(value), "claim_class": claim_class,
|
|
206
|
+
"sources": [s for s in sources if s]})
|
|
207
|
+
|
|
208
|
+
def omit(self, key: str, reason: str):
|
|
209
|
+
self.omitted.append({"key": key, "reason": reason})
|
|
210
|
+
|
|
211
|
+
def text(self) -> str:
|
|
212
|
+
rank = {k: i for i, k in enumerate(ORDER)}
|
|
213
|
+
rows = sorted(self.trailers, key=lambda t: rank.get(t["key"], len(ORDER)))
|
|
214
|
+
return "\n".join(f"{t['key']}: {t['value']}" for t in rows)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def delta(metric: str, rec: dict, value: float, comparisons: list[dict], base: dict | None, label: str | None,
|
|
218
|
+
higher_is_better: bool, b: Builder) -> str:
|
|
219
|
+
"""'[+2.1% (95% CI ...) improved vs 1a2b3c4]' from a comparison record, else a plain ratio of medians."""
|
|
220
|
+
for c in comparisons:
|
|
221
|
+
d = c.get("data") or {}
|
|
222
|
+
if d.get("metric") != metric or (c is not rec and d.get("candidate") not in (None, rec.get("evidence_id"))):
|
|
223
|
+
continue
|
|
224
|
+
ratio = next((m for m in c.get("metrics") or [] if m.get("name") == f"{metric}_ratio"), None)
|
|
225
|
+
if not ratio or ratio.get("value") is None:
|
|
226
|
+
continue
|
|
227
|
+
text = f"{(ratio['value'] - 1) * 100:+.1f}%"
|
|
228
|
+
ci = ratio.get("ci95")
|
|
229
|
+
if isinstance(ci, list) and len(ci) == 2 and None not in ci:
|
|
230
|
+
text += f" (95% CI {(ci[0] - 1) * 100:+.1f}% to {(ci[1] - 1) * 100:+.1f}%)"
|
|
231
|
+
if d.get("verdict"):
|
|
232
|
+
text += f" {d['verdict']}"
|
|
233
|
+
return f" [{text} vs {label or short(d.get('baseline'))}]"
|
|
234
|
+
if base is None:
|
|
235
|
+
return ""
|
|
236
|
+
got = pick([base], DECODE if metric == "decode_tok_s" else TTFT if metric == "ttft_ms" else (metric,))
|
|
237
|
+
if not got or len(got[0]) != 1 or not got[0][0].get("value"):
|
|
238
|
+
b.limitations.append(f"baseline {short(base.get('evidence_id'))} has no overall {metric}: no delta")
|
|
239
|
+
return ""
|
|
240
|
+
ratio = value / got[0][0]["value"]
|
|
241
|
+
b.limitations.append(f"{metric} delta is a ratio of medians without a CI or verdict; run `inferencekit compare` "
|
|
242
|
+
"on interleaved rounds for that")
|
|
243
|
+
return f" [{(ratio - 1) * 100:+.1f}% vs {label or short(base.get('evidence_id'))}, ratio of medians, no CI]"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def build_trailers(recs: list[dict], base: dict | None, baseline_commit: str | None,
|
|
247
|
+
gpu_price_usd_h: float | None) -> Builder:
|
|
248
|
+
b = Builder()
|
|
249
|
+
comparisons = [r for r in recs if kind_of(r) == "comparison"]
|
|
250
|
+
label = baseline_commit[:7] if baseline_commit else (short(base["evidence_id"]) if base else None)
|
|
251
|
+
# Single-user headlines come from single-user records; load levels feed capacity, not TTFT.
|
|
252
|
+
speed_all = [r for r in recs if kind_of(r) in SPEED_KINDS]
|
|
253
|
+
speed_recs = [r for r in speed_all if not is_load(r)]
|
|
254
|
+
engine = next((label_of(r) for r in speed_all if label_of(r)), None)
|
|
255
|
+
capacity = [r for r in recs if kind_of(r) == "capacity"]
|
|
256
|
+
capacity.sort(key=lambda r: label_of(r) != engine) # the measured engine's capacity record first
|
|
257
|
+
|
|
258
|
+
first = (speed_recs or speed_all or capacity or recs or [None])[0]
|
|
259
|
+
wl = workload_text(first) if first else ""
|
|
260
|
+
if wl:
|
|
261
|
+
b.add("Perf-Workload", wl, "observed", [first.get("evidence_id")])
|
|
262
|
+
else:
|
|
263
|
+
b.omit("Perf-Workload", "no identity in the evidence")
|
|
264
|
+
|
|
265
|
+
decode_value = None
|
|
266
|
+
for key, names, metric, higher in (("Perf-Decode-Tok-S", DECODE, "decode_tok_s", True),
|
|
267
|
+
("Perf-TTFT-Ms", TTFT, "ttft_ms", False)):
|
|
268
|
+
got = pick(speed_recs, names)
|
|
269
|
+
if not got: # a comparison record alone still holds the candidate's median
|
|
270
|
+
got = next(((ms, c) for c in comparisons for ms in [[m for m in c.get("metrics") or []
|
|
271
|
+
if m.get("name") == metric and m.get("subject") == "candidate" and m.get("value") is not None]]
|
|
272
|
+
if ms), None)
|
|
273
|
+
if not got:
|
|
274
|
+
b.omit(key, f"no {metric} median in single-user benchmark evidence"
|
|
275
|
+
+ (" (load-mode records feed Perf-Capacity-N, not this)" if speed_all else ""))
|
|
276
|
+
continue
|
|
277
|
+
ms, rec = got
|
|
278
|
+
text, change = values_text(ms), ""
|
|
279
|
+
if len(ms) == 1:
|
|
280
|
+
change = delta(metric, rec, ms[0]["value"], comparisons, base, label, higher, b)
|
|
281
|
+
if metric == "decode_tok_s":
|
|
282
|
+
decode_value = (ms[0]["value"], rec)
|
|
283
|
+
cls = "derived" if change or rec.get("kind") == "comparison" else rec.get("claim_class", "observed")
|
|
284
|
+
b.add(key, text + change, cls, [rec.get("evidence_id")])
|
|
285
|
+
|
|
286
|
+
got = pick(speed_recs, P95_DIRECT, stats=None) or pick(speed_recs, P95_BY_STAT, stats=("p95",))
|
|
287
|
+
if got:
|
|
288
|
+
b.add("Perf-P95-Latency-Ms", values_text(got[0]), got[1].get("claim_class", "observed"),
|
|
289
|
+
[got[1].get("evidence_id")])
|
|
290
|
+
else:
|
|
291
|
+
b.omit("Perf-P95-Latency-Ms", "no single-user p95 end-to-end latency in the evidence (bench decode does "
|
|
292
|
+
"not measure it; bench serve does)")
|
|
293
|
+
|
|
294
|
+
quality, qsrc = quality_parts(recs, engine)
|
|
295
|
+
if quality:
|
|
296
|
+
b.add("Perf-Quality", "; ".join(quality), "derived", qsrc)
|
|
297
|
+
else:
|
|
298
|
+
b.omit("Perf-Quality", "no correctness / quality evidence")
|
|
299
|
+
|
|
300
|
+
cost_trailer(b, capacity, decode_value, gpu_price_usd_h)
|
|
301
|
+
|
|
302
|
+
n_star = None
|
|
303
|
+
got = pick(capacity, N_STAR, stats=None) or pick(capacity, ("rate_star_rps",), stats=None)
|
|
304
|
+
if got:
|
|
305
|
+
m, rec = got[0][0], got[1]
|
|
306
|
+
data = rec.get("data") or {}
|
|
307
|
+
slo = data.get("slo") or ((rec.get("identity") or {}).get("workload") or {}).get("slo")
|
|
308
|
+
s = slo_text(slo)
|
|
309
|
+
# N* is server-wide (concurrent streams or arrival rate at the server), not per GPU.
|
|
310
|
+
# The SLO held at the top of the grid: N* is a lower bound.
|
|
311
|
+
lower = "at least " if str(data.get("reason") or "").startswith("SLO holds at every level") else ""
|
|
312
|
+
if m["name"] == "rate_star_rps" or data.get("level_kind") == "rate":
|
|
313
|
+
value = f"max rate {lower}{num(m['value'])} req/s"
|
|
314
|
+
else:
|
|
315
|
+
n_star = m["value"]
|
|
316
|
+
value = f"{lower}{num(n_star)} streams"
|
|
317
|
+
if gpu_count(rec) > 1:
|
|
318
|
+
value += f" across {gpu_count(rec)} GPUs"
|
|
319
|
+
b.add("Perf-Capacity-N", value + (f" (SLO {s})" if s else " (SLO not recorded)"),
|
|
320
|
+
rec.get("claim_class", "observed"), [rec.get("evidence_id")])
|
|
321
|
+
if not s:
|
|
322
|
+
b.limitations.append("capacity evidence carries no SLO: N* cannot be interpreted without it")
|
|
323
|
+
elif capacity:
|
|
324
|
+
why = (capacity[0].get("data") or {}).get("reason") or "N* undefined"
|
|
325
|
+
b.omit("Perf-Capacity-N", f"capacity evidence has no N*: {why}")
|
|
326
|
+
else:
|
|
327
|
+
b.omit("Perf-Capacity-N", "no capacity evidence with N*")
|
|
328
|
+
got = pick(capacity, GOODPUT, stats=None)
|
|
329
|
+
if got:
|
|
330
|
+
m, rec = got[0][0], got[1]
|
|
331
|
+
unit = m.get("unit") or "req/s"
|
|
332
|
+
b.add("Perf-Goodput", f"{num(m['value'])} {unit}" + (f" at N*={num(n_star)}" if n_star is not None else ""),
|
|
333
|
+
rec.get("claim_class", "observed"), [rec.get("evidence_id")])
|
|
334
|
+
else:
|
|
335
|
+
b.omit("Perf-Goodput", "no goodput in capacity evidence")
|
|
336
|
+
|
|
337
|
+
ids = [r.get("evidence_id") for r in recs] + ([base.get("evidence_id")] if base else [])
|
|
338
|
+
b.add("Perf-Evidence", ", ".join(short(i) for i in dict.fromkeys(ids) if i), "observed", [])
|
|
339
|
+
baseline = baseline_commit or (short(base["evidence_id"]) if base else None)
|
|
340
|
+
if baseline is None:
|
|
341
|
+
baseline = next((short((c.get("data") or {}).get("baseline")) for c in comparisons
|
|
342
|
+
if (c.get("data") or {}).get("baseline")), None)
|
|
343
|
+
if baseline:
|
|
344
|
+
b.add("Perf-Baseline", baseline, "observed", [])
|
|
345
|
+
else:
|
|
346
|
+
b.omit("Perf-Baseline", "no --baseline-commit, --baseline-ev or comparison record")
|
|
347
|
+
return b
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _subject_metric(rec: dict, names, subject: str | None, others=()) -> dict | None:
|
|
351
|
+
"""The metric named in ``names`` for ``subject`` (the engine label), else the first engine-level one.
|
|
352
|
+
Metrics whose subject is in ``others`` (another server's, e.g. the sampler check's baseline) never stand in."""
|
|
353
|
+
for name in names:
|
|
354
|
+
ms = [m for m in rec.get("metrics") or [] if isinstance(m, dict) and m.get("name") == name
|
|
355
|
+
and m.get("value") is not None and m.get("subject") not in others]
|
|
356
|
+
if not ms:
|
|
357
|
+
continue
|
|
358
|
+
exact = [m for m in ms if subject is not None and m.get("subject") == subject]
|
|
359
|
+
floor = (rec.get("data") or {}).get("noise_floor")
|
|
360
|
+
plain = [m for m in ms if "@" not in str(m.get("subject") or "") and m.get("subject") != floor]
|
|
361
|
+
found = (exact or plain or ms)[0]
|
|
362
|
+
return found
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def quality_parts(recs: list[dict], engine: str | None) -> tuple[list[str], list[str]]:
|
|
367
|
+
"""One phrase per quality record. The engine label goes last, so the leading number stays the value."""
|
|
368
|
+
parts, sources = [], []
|
|
369
|
+
for r in recs:
|
|
370
|
+
kind, data = kind_of(r), r.get("data") or {}
|
|
371
|
+
if kind == "correctness":
|
|
372
|
+
pa = metrics_named(r, ("prefix_agreement_pct",), stats=None)
|
|
373
|
+
em = metrics_named(r, ("exact_match_pct",), stats=None)
|
|
374
|
+
if pa:
|
|
375
|
+
text = f"greedy prefix agreement {pa[0]['value']:.2f}%"
|
|
376
|
+
if em and em[0].get("n"):
|
|
377
|
+
text += f" ({round(em[0]['value'] * em[0]['n'] / 100)}/{em[0]['n']} identical)"
|
|
378
|
+
if isinstance(data.get("verdict"), str):
|
|
379
|
+
text += f" {data['verdict']}"
|
|
380
|
+
parts.append(text)
|
|
381
|
+
sources.append(r.get("evidence_id"))
|
|
382
|
+
elif kind == "quality_trajectory":
|
|
383
|
+
m = _subject_metric(r, ("top1_agreement_pct", "tf_agreement_pct", "token_agreement_pct"), engine)
|
|
384
|
+
if not m:
|
|
385
|
+
continue
|
|
386
|
+
subject = m.get("subject")
|
|
387
|
+
text = f"{m['name'].replace('_pct', '').replace('_', '-')} {m['value']:.2f}%"
|
|
388
|
+
floor_label = data.get("noise_floor")
|
|
389
|
+
floor = next((x for x in r.get("metrics") or [] if x.get("name") == m["name"] and floor_label
|
|
390
|
+
and x.get("subject") == floor_label and x.get("value") is not None), None)
|
|
391
|
+
if floor:
|
|
392
|
+
text += f" (noise floor {floor['value']:.2f}%)"
|
|
393
|
+
verdict = (data.get("verdicts") or {}).get(subject) or data.get("verdict")
|
|
394
|
+
if isinstance(verdict, str):
|
|
395
|
+
text += f" {verdict}"
|
|
396
|
+
parts.append(text + (f" [{one_line(subject)}]" if subject else ""))
|
|
397
|
+
sources.append(r.get("evidence_id"))
|
|
398
|
+
elif kind in ("quality_task", "quality"):
|
|
399
|
+
task = str(data.get("task") or "task")
|
|
400
|
+
if task == "sampling":
|
|
401
|
+
# only this server's own number: with --baseline-url the record also carries the baseline's
|
|
402
|
+
m = _subject_metric(r, ("first_token_tv_excess",), engine, others=("baseline",))
|
|
403
|
+
if not m:
|
|
404
|
+
who = data.get("label") or engine
|
|
405
|
+
parts.append("first token tv excess n/a" + (f" {data['verdict']}" if isinstance(
|
|
406
|
+
data.get("verdict"), str) else "") + (f" [{one_line(who)}]" if who else ""))
|
|
407
|
+
sources.append(r.get("evidence_id"))
|
|
408
|
+
continue
|
|
409
|
+
else:
|
|
410
|
+
m = _subject_metric(r, (f"{task}_accuracy_pct", "accuracy_pct", "score"), engine)
|
|
411
|
+
if not m:
|
|
412
|
+
m = next((x for x in r.get("metrics") or [] if x.get("value") is not None
|
|
413
|
+
and any(w in x.get("name", "") for w in ("accuracy", "wer", "score", "agreement"))), None)
|
|
414
|
+
if not m:
|
|
415
|
+
continue
|
|
416
|
+
name = m["name"].replace("_pct", "").replace("_", " ")
|
|
417
|
+
unit = "%" if m.get("unit") == "%" or m["name"].endswith("_pct") else ""
|
|
418
|
+
text = f"{name} {num(m['value'])}{unit}"
|
|
419
|
+
ci = m.get("ci95")
|
|
420
|
+
if isinstance(ci, list) and len(ci) == 2 and None not in ci:
|
|
421
|
+
sep = " to " if min(ci) < 0 else "-" # a signed number (TV excess): "-0.0085-0.0116" misreads
|
|
422
|
+
text += f" (95% CI {num(ci[0])}{sep}{num(ci[1])}{unit})"
|
|
423
|
+
if m.get("n"):
|
|
424
|
+
text += f" (n={m['n']})"
|
|
425
|
+
if isinstance(data.get("verdict"), str):
|
|
426
|
+
text += f" {data['verdict']}"
|
|
427
|
+
parts.append(text + (f" [{one_line(m['subject'])}]" if m.get("subject") else ""))
|
|
428
|
+
sources.append(r.get("evidence_id"))
|
|
429
|
+
return parts, sources
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def cost_trailer(b: Builder, capacity: list[dict], decode_value, price: float | None) -> None:
|
|
433
|
+
"""Perf-Cost. Prices are $ per GPU-hour (as bench serve takes them).
|
|
434
|
+
|
|
435
|
+
A capacity record's own cost is printed at the price it was computed with. A different
|
|
436
|
+
``--gpu-price-usd-h`` reprices it from the record's per-GPU throughput at N* (derived);
|
|
437
|
+
the record's number is never shown next to a price it was not computed at.
|
|
438
|
+
"""
|
|
439
|
+
got = pick(capacity, COST, stats=None)
|
|
440
|
+
if got:
|
|
441
|
+
m, rec = got[0][0], got[1]
|
|
442
|
+
own = (rec.get("data") or {}).get("gpu_price_usd_h")
|
|
443
|
+
own = float(own) if isinstance(own, (int, float)) and not isinstance(own, bool) and own > 0 else None
|
|
444
|
+
if price is None or (own is not None and abs(price - own) < 1e-9):
|
|
445
|
+
b.add("Perf-Cost", f"${m['value']:.2f} per 1M output tokens" + (f" at ${own:.2f}/GPU-h" if own else "")
|
|
446
|
+
+ " (at N*)", rec.get("claim_class", "observed"), [rec.get("evidence_id")])
|
|
447
|
+
if own is None:
|
|
448
|
+
b.limitations.append("Perf-Cost: the capacity record does not say which GPU price it used")
|
|
449
|
+
return
|
|
450
|
+
tput = pick([rec], ("output_tok_s_per_gpu",), stats=None)
|
|
451
|
+
if tput and len(tput[0]) == 1 and tput[0][0]["value"]:
|
|
452
|
+
usd = price / (tput[0][0]["value"] * 3600) * 1e6
|
|
453
|
+
b.add("Perf-Cost", f"${usd:.2f} per 1M output tokens at ${price:.2f}/GPU-h (at N*, repriced)", "derived",
|
|
454
|
+
[rec.get("evidence_id")])
|
|
455
|
+
b.limitations.append(f"Perf-Cost repriced at ${price:.2f}/GPU-h from output_tok_s_per_gpu at N*; capacity "
|
|
456
|
+
f"record {short(rec.get('evidence_id'))} priced it at "
|
|
457
|
+
+ (f"${own:.2f}/GPU-h (${m['value']:.2f})" if own else "an unrecorded price"))
|
|
458
|
+
return
|
|
459
|
+
b.add("Perf-Cost", f"${m['value']:.2f} per 1M output tokens" + (f" at ${own:.2f}/GPU-h" if own else "")
|
|
460
|
+
+ " (at N*)", rec.get("claim_class", "observed"), [rec.get("evidence_id")])
|
|
461
|
+
b.limitations.append(f"--gpu-price-usd-h {price:g} ignored: capacity record {short(rec.get('evidence_id'))} "
|
|
462
|
+
"has no per-GPU throughput at N* to reprice from, so its own cost and price are shown")
|
|
463
|
+
return
|
|
464
|
+
if price is None:
|
|
465
|
+
b.omit("Perf-Cost", "no --gpu-price-usd-h and no cost in capacity evidence")
|
|
466
|
+
return
|
|
467
|
+
got = pick(capacity, THROUGHPUT_PER_GPU, stats=None)
|
|
468
|
+
if got and got[0][0]["value"]:
|
|
469
|
+
tps, rec, note = got[0][0]["value"], got[1], "at N*"
|
|
470
|
+
# Server-wide throughput (not per GPU) is paid for by every GPU of the server.
|
|
471
|
+
gpus = 1 if got[0][0]["name"] == "output_tok_s_per_gpu" else gpu_count(rec)
|
|
472
|
+
elif decode_value and decode_value[0]:
|
|
473
|
+
(tps, rec), note = decode_value, "1 stream"
|
|
474
|
+
gpus = gpu_count(rec) # one decode stream occupies every GPU of a tensor-parallel engine
|
|
475
|
+
b.limitations.append("Perf-Cost uses single-stream decode speed: serving many users per GPU costs less; "
|
|
476
|
+
"add capacity evidence (bench serve load mode) for the cost at N*")
|
|
477
|
+
else:
|
|
478
|
+
b.omit("Perf-Cost", "no throughput to price")
|
|
479
|
+
return
|
|
480
|
+
usd = price * gpus / (tps * 3600) * 1e6
|
|
481
|
+
at = f"${price:.2f}/GPU-h" + (f" x {gpus} GPUs" if gpus > 1 else "")
|
|
482
|
+
b.add("Perf-Cost", f"${usd:.2f} per 1M output tokens at {at} ({note})", "derived", [rec.get("evidence_id")])
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def select_engine(recs: list[dict], label: str | None) -> list[dict]:
|
|
486
|
+
"""The records of the one engine a commit measures; refuses anything ambiguous.
|
|
487
|
+
|
|
488
|
+
Two engines' speed records (or a baseline's and a candidate's passed together) would
|
|
489
|
+
otherwise put the first one's number in the commit as its headline.
|
|
490
|
+
"""
|
|
491
|
+
measured = SPEED_KINDS + ("capacity",)
|
|
492
|
+
labels = sorted({label_of(r) for r in recs if kind_of(r) in measured and label_of(r)})
|
|
493
|
+
if label is not None:
|
|
494
|
+
if label not in labels:
|
|
495
|
+
raise OpError(f"--label {label!r}: no benchmark or capacity record has that engine label; found: "
|
|
496
|
+
f"{', '.join(labels) or 'none'}")
|
|
497
|
+
recs = [r for r in recs if kind_of(r) not in measured or label_of(r) == label]
|
|
498
|
+
elif len(labels) > 1:
|
|
499
|
+
raise OpError(f"benchmark/capacity records of {len(labels)} engine labels ({', '.join(labels)}): pass --label "
|
|
500
|
+
"for the engine this commit measures, and the baseline's record as --baseline-ev (or a "
|
|
501
|
+
"`compare` record for a delta with a CI)")
|
|
502
|
+
for what, group in (("single-user benchmark", [r for r in recs if kind_of(r) in SPEED_KINDS and not is_load(r)]),
|
|
503
|
+
("capacity", [r for r in recs if kind_of(r) == "capacity"])):
|
|
504
|
+
if len(group) > 1:
|
|
505
|
+
raise OpError(f"{len(group)} {what} records for one engine "
|
|
506
|
+
f"({', '.join(short(r.get('evidence_id')) for r in group)}): pass only the one this commit "
|
|
507
|
+
"measures; a baseline goes in --baseline-ev")
|
|
508
|
+
return recs
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _load(store: ev.Store, ref: str, what: str) -> dict:
|
|
512
|
+
rec = store.load(ref)
|
|
513
|
+
problem = ev.integrity_problem(rec)
|
|
514
|
+
if problem: # a ledger entry is permanent: never write numbers from an edited record into it
|
|
515
|
+
raise OpError(f"{what} {problem}; refusing to write trailers from it")
|
|
516
|
+
return rec
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@operation("ledger_trailers", ("ledger", "trailers"), "Git trailers (Perf-Decode-Tok-S, Perf-TTFT-Ms, Perf-Quality, "
|
|
520
|
+
"Perf-Cost, Perf-Capacity-N...) for a commit message, from evidence ids; missing metrics are omitted, "
|
|
521
|
+
"never zero.",
|
|
522
|
+
[Arg("ids", "array", "Evidence ids (or unique prefixes, or paths): benchmark, comparison, correctness, "
|
|
523
|
+
"quality and capacity records.", required=True, positional=True),
|
|
524
|
+
Arg("baseline_commit", "string", "Commit the change is measured against (shown in the delta and "
|
|
525
|
+
"Perf-Baseline)."),
|
|
526
|
+
Arg("baseline_ev", "string", "Baseline benchmark evidence id, for the delta when no comparison record "
|
|
527
|
+
"is given."),
|
|
528
|
+
Arg("label", "string", "Engine label the trailers describe, when the evidence holds several engines."),
|
|
529
|
+
Arg("gpu_price_usd_h", "number", "GPU price in $ per GPU-hour (as bench serve takes it), for Perf-Cost; "
|
|
530
|
+
"multiplied by the GPU count where the throughput is the whole server's."),
|
|
531
|
+
Arg("format", "string", "text: the trailer block on stdout, what was omitted or limited on stderr; "
|
|
532
|
+
"json: trailers with claim classes, sources and what was omitted.", default="text",
|
|
533
|
+
choices=("text", "json"))])
|
|
534
|
+
def ledger_trailers(call, ids, baseline_commit, label, baseline_ev, gpu_price_usd_h, format): # noqa: A002
|
|
535
|
+
store = ev.Store()
|
|
536
|
+
recs, seen = [], set()
|
|
537
|
+
for ref in ids:
|
|
538
|
+
rec = _load(store, ref, "evidence")
|
|
539
|
+
if rec.get("evidence_id") not in seen:
|
|
540
|
+
seen.add(rec.get("evidence_id"))
|
|
541
|
+
recs.append(rec)
|
|
542
|
+
base = _load(store, baseline_ev, "baseline") if baseline_ev else None
|
|
543
|
+
if baseline_commit is not None and not re.fullmatch(r"[0-9A-Za-z._/-]{4,64}", baseline_commit):
|
|
544
|
+
raise OpError(f"--baseline-commit {baseline_commit!r} does not look like a commit or ref")
|
|
545
|
+
if gpu_price_usd_h is not None and gpu_price_usd_h <= 0:
|
|
546
|
+
raise OpError("--gpu-price-usd-h must be > 0")
|
|
547
|
+
recs = select_engine(recs, label)
|
|
548
|
+
b = build_trailers(recs, base, baseline_commit, gpu_price_usd_h)
|
|
549
|
+
text = b.text()
|
|
550
|
+
out = {"trailers": b.trailers, "omitted": b.omitted, "limitations": b.limitations, "text": text,
|
|
551
|
+
"evidence": [r.get("evidence_id") for r in recs] + ([base.get("evidence_id")] if base else [])}
|
|
552
|
+
if format == "json":
|
|
553
|
+
return out
|
|
554
|
+
out_dir = store.root / "ledger"
|
|
555
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
556
|
+
path = out_dir / f"trailers-{sha256_hex(text)[:12]}.txt"
|
|
557
|
+
path.write_text(text + "\n")
|
|
558
|
+
# Provenance for the derived numbers (deltas, repriced costs): claim class and source record per trailer.
|
|
559
|
+
path.with_suffix(".json").write_text(json.dumps({"request": call.request(), "generated": call.started, **out},
|
|
560
|
+
indent=2, ensure_ascii=False) + "\n")
|
|
561
|
+
# stdout is the commit message ($(...) captures it); the caveats go to stderr so they are seen, not committed.
|
|
562
|
+
for o in b.omitted:
|
|
563
|
+
print(f"inferencekit ledger: omitted {o['key']}: {o['reason']}", file=sys.stderr)
|
|
564
|
+
for lim in b.limitations:
|
|
565
|
+
print(f"inferencekit ledger: limitation: {lim}", file=sys.stderr)
|
|
566
|
+
return {"format": "md", "markdown": text, "path": str(path), **out}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# --------------------------------------------------------------------------- history
|
|
570
|
+
|
|
571
|
+
HIGHER_IS_BETTER = {"Perf-Decode-Tok-S": True, "Perf-Quality": True, "Perf-Capacity-N": True,
|
|
572
|
+
"Perf-Goodput": True, "Perf-TTFT-Ms": False, "Perf-P95-Latency-Ms": False, "Perf-Cost": False}
|
|
573
|
+
NOT_METRICS = ("Perf-Workload", "Perf-Evidence", "Perf-Baseline")
|
|
574
|
+
_NUMBER = re.compile(r"(?<![A-Za-z0-9_.])[-+]?\d+(?:\.\d+)?")
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def leading_number(value: str) -> float | None:
|
|
578
|
+
"""The first free-standing number of a trailer value ('$0.91 per 1M' -> 0.91; 'gsm8k 85%' -> 85)."""
|
|
579
|
+
m = _NUMBER.search(value or "")
|
|
580
|
+
return float(m.group(0)) if m else None
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def direction(key: str, sample: str = "") -> bool | None:
|
|
584
|
+
"""True if higher is better, False if lower is, None if unknown. ``sample``: a value of that trailer."""
|
|
585
|
+
if key == "Perf-Quality" and re.search(r"\b(c|w)er\b|\bkl\b|error", sample, re.IGNORECASE):
|
|
586
|
+
return False # an error rate or a divergence, e.g. "asr-wer 2.1%"
|
|
587
|
+
if key in HIGHER_IS_BETTER:
|
|
588
|
+
return HIGHER_IS_BETTER[key]
|
|
589
|
+
low = key.lower()
|
|
590
|
+
if low.endswith(("-ms", "-s-latency")) or "cost" in low or "latency" in low:
|
|
591
|
+
return False
|
|
592
|
+
if low.endswith(("-tok-s", "-rps")) or "goodput" in low or "throughput" in low:
|
|
593
|
+
return True
|
|
594
|
+
return None
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def metric_key(name: str) -> str:
|
|
598
|
+
"""'decode_tok_s', 'decode-tok-s' or 'Perf-Decode-Tok-S' -> 'Perf-Decode-Tok-S' (case-insensitive match later)."""
|
|
599
|
+
n = name.strip().replace("_", "-")
|
|
600
|
+
return n if n.lower().startswith("perf-") else "Perf-" + n
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def sparkline(values: list[float | None]) -> str:
|
|
604
|
+
"""One character per commit; '·' where a commit has no value (never drawn as zero)."""
|
|
605
|
+
present = [v for v in values if v is not None]
|
|
606
|
+
if not present:
|
|
607
|
+
return ""
|
|
608
|
+
lo, hi = min(present), max(present)
|
|
609
|
+
out = []
|
|
610
|
+
for v in values:
|
|
611
|
+
if v is None:
|
|
612
|
+
out.append("·")
|
|
613
|
+
elif hi == lo:
|
|
614
|
+
out.append(SPARK[len(SPARK) // 2])
|
|
615
|
+
else:
|
|
616
|
+
out.append(SPARK[min(len(SPARK) - 1, int((v - lo) / (hi - lo) * (len(SPARK) - 1) + 0.5))])
|
|
617
|
+
return "".join(out)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def git_log(repo: str, rev: str, limit: int) -> list[dict]:
|
|
621
|
+
fmt = "%H%x1f%h%x1f%cI%x1f%s%x1f%(trailers:only,unfold)%x1e"
|
|
622
|
+
res = run_cmd(["git", "-C", repo, "log", f"--max-count={limit}", f"--format={fmt}", rev, "--"], timeout=60)
|
|
623
|
+
if res.returncode == 127:
|
|
624
|
+
raise OpError("git not found on PATH")
|
|
625
|
+
if not res.ok:
|
|
626
|
+
raise OpError(f"git log failed in {repo}: {res.stderr.strip()[:300]}")
|
|
627
|
+
commits = []
|
|
628
|
+
for chunk in res.stdout.split("\x1e"):
|
|
629
|
+
chunk = chunk.strip("\n")
|
|
630
|
+
if not chunk:
|
|
631
|
+
continue
|
|
632
|
+
fields = chunk.split("\x1f")
|
|
633
|
+
if len(fields) < 5:
|
|
634
|
+
continue
|
|
635
|
+
sha, short_sha, date, subject, trailer_text = fields[:5]
|
|
636
|
+
trailers: dict[str, str] = {}
|
|
637
|
+
for line in trailer_text.splitlines():
|
|
638
|
+
key, sep, value = line.partition(":")
|
|
639
|
+
if sep and key.strip().lower().startswith("perf-"):
|
|
640
|
+
trailers.setdefault(key.strip(), value.strip()) # first occurrence wins
|
|
641
|
+
commits.append({"commit": sha, "short": short_sha, "date": date, "subject": subject, "trailers": trailers})
|
|
642
|
+
return commits
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _cell(text: str) -> str:
|
|
646
|
+
return str(text).replace("|", "\\|")
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@operation("history", ("history",), "Commits with Perf-* trailers from git log: a table, a sparkline per metric, "
|
|
650
|
+
"the best commit per metric, and optional local perf/best-<metric> tags.",
|
|
651
|
+
[Arg("repo", "string", "Git repository.", default="."),
|
|
652
|
+
Arg("metric", "string", "Only this metric (e.g. decode-tok-s or Perf-TTFT-Ms)."),
|
|
653
|
+
Arg("rev", "string", "Revision or range to read.", default="HEAD"),
|
|
654
|
+
Arg("limit", "integer", "Most recent commits to read.", default=200),
|
|
655
|
+
Arg("tag_best", "boolean", "Point local tags perf/best-<metric> at the best commit per metric (never "
|
|
656
|
+
"pushed).", default=False),
|
|
657
|
+
Arg("format", "string", "md: table and sparklines; json: parsed commits.", default="md",
|
|
658
|
+
choices=("md", "json"))])
|
|
659
|
+
def history(call, repo, metric, rev, limit, tag_best, format): # noqa: A002
|
|
660
|
+
if not Path(repo).is_dir():
|
|
661
|
+
raise OpError(f"repository not found: {repo}")
|
|
662
|
+
if rev.startswith("-"):
|
|
663
|
+
raise OpError(f"rev {rev!r} must not start with '-'")
|
|
664
|
+
commits = [c for c in git_log(repo, rev, limit) if c["trailers"]]
|
|
665
|
+
commits.reverse() # oldest first: sparklines read left to right in time
|
|
666
|
+
keys: list[str] = []
|
|
667
|
+
for c in commits:
|
|
668
|
+
for k in c["trailers"]:
|
|
669
|
+
if k not in NOT_METRICS and k not in keys:
|
|
670
|
+
keys.append(k)
|
|
671
|
+
keys.sort(key=lambda k: (ORDER.index(k) if k in ORDER else len(ORDER), k))
|
|
672
|
+
if metric:
|
|
673
|
+
want = metric_key(metric).lower()
|
|
674
|
+
keys = [k for k in keys if k.lower() == want]
|
|
675
|
+
if not keys:
|
|
676
|
+
raise OpError(f"no commits carry trailer {metric_key(metric)} in {repo} ({rev})")
|
|
677
|
+
for c in commits:
|
|
678
|
+
c["values"] = {k: leading_number(c["trailers"][k]) for k in keys if k in c["trailers"]}
|
|
679
|
+
|
|
680
|
+
# Only numbers measured on the same workload are comparable: the newest commit's workload decides.
|
|
681
|
+
latest_wl = next((c["trailers"].get("Perf-Workload") for c in reversed(commits)
|
|
682
|
+
if c["trailers"].get("Perf-Workload")), None)
|
|
683
|
+
comparable = [c for c in commits if latest_wl is None or c["trailers"].get("Perf-Workload") in (None, latest_wl)]
|
|
684
|
+
notes = []
|
|
685
|
+
if len(comparable) < len(commits):
|
|
686
|
+
notes.append(f"{len(commits) - len(comparable)} commit(s) measured another Perf-Workload: listed, but left out "
|
|
687
|
+
f"of sparklines and best-of (workload: {latest_wl})")
|
|
688
|
+
|
|
689
|
+
summary = {}
|
|
690
|
+
for k in keys:
|
|
691
|
+
series = [c["values"].get(k) for c in comparable]
|
|
692
|
+
present = [(c, c["values"][k]) for c in comparable if c["values"].get(k) is not None]
|
|
693
|
+
d = direction(k, next((c["trailers"][k] for c in reversed(comparable) if k in c["trailers"]), ""))
|
|
694
|
+
best = None
|
|
695
|
+
if present and d is not None:
|
|
696
|
+
target = max(v for _, v in present) if d else min(v for _, v in present)
|
|
697
|
+
best_c = [c for c, v in present if v == target][-1] # newest commit on a tie
|
|
698
|
+
best = {"commit": best_c["commit"], "short": best_c["short"], "value": target}
|
|
699
|
+
summary[k] = {"sparkline": sparkline(series), "n": len(present), "higher_is_better": d, "best": best,
|
|
700
|
+
"first": present[0][1] if present else None, "last": present[-1][1] if present else None,
|
|
701
|
+
"median": statistics.median([v for _, v in present]) if present else None}
|
|
702
|
+
if d is None and present:
|
|
703
|
+
notes.append(f"{k}: direction unknown (not a standard trailer), no best commit chosen")
|
|
704
|
+
|
|
705
|
+
tags = []
|
|
706
|
+
if tag_best:
|
|
707
|
+
for k, s in summary.items():
|
|
708
|
+
if not s["best"]:
|
|
709
|
+
continue
|
|
710
|
+
tag = "perf/best-" + re.sub(r"[^a-z0-9-]+", "-", k[len("Perf-"):].lower()).strip("-")
|
|
711
|
+
# A lightweight local bookmark, moved when a better commit appears; never signed or pushed.
|
|
712
|
+
res = run_cmd(["git", "-C", repo, "-c", "tag.gpgSign=false", "tag", "-f", tag, s["best"]["commit"]],
|
|
713
|
+
timeout=30)
|
|
714
|
+
if not res.ok:
|
|
715
|
+
raise OpError(f"git tag {tag} failed: {res.stderr.strip()[:300]}")
|
|
716
|
+
tags.append({"tag": tag, "commit": s["best"]["short"], "value": s["best"]["value"]})
|
|
717
|
+
|
|
718
|
+
out = {"repo": str(repo), "rev": rev, "commits": commits, "metrics": summary, "tags": tags, "notes": notes,
|
|
719
|
+
"claim_class": "as recorded in each commit's trailers (see its Perf-Evidence)"}
|
|
720
|
+
if format == "json":
|
|
721
|
+
return out
|
|
722
|
+
lines = [f"# Perf history: {Path(repo).resolve().name} ({rev}, {len(commits)} commits with Perf-* trailers)", ""]
|
|
723
|
+
if not commits:
|
|
724
|
+
lines.append("No commits with Perf-* trailers. Add them with `inferencekit ledger trailers <ev...>`.")
|
|
725
|
+
else:
|
|
726
|
+
short_keys = [k[len("Perf-"):] for k in keys]
|
|
727
|
+
lines += ["| commit | date | subject | " + " | ".join(short_keys) + " |",
|
|
728
|
+
"|---|---|---|" + "---|" * len(keys)]
|
|
729
|
+
for c in reversed(commits): # newest first in the table, like git log
|
|
730
|
+
vals = [num(c["values"][k]) if c["values"].get(k) is not None else "·" for k in keys]
|
|
731
|
+
lines.append(f"| {c['short']} | {c['date'][:10]} | {_cell(c['subject'][:60])} | " + " | ".join(vals) + " |")
|
|
732
|
+
lines += ["", "Sparklines, oldest to newest (· = no value on that commit):", ""]
|
|
733
|
+
width = max((len(k) for k in short_keys), default=0)
|
|
734
|
+
for k, sk in zip(keys, short_keys):
|
|
735
|
+
s = summary[k]
|
|
736
|
+
best = (f", best {num(s['best']['value'])} at {s['best']['short']}"
|
|
737
|
+
f" ({'higher' if s['higher_is_better'] else 'lower'} is better)") if s["best"] else ""
|
|
738
|
+
lines.append(f" {sk.ljust(width)} {s['sparkline']} first {num(s['first'])}, last {num(s['last'])}"
|
|
739
|
+
f"{best}")
|
|
740
|
+
if tags:
|
|
741
|
+
lines += ["", "Local tags (not pushed): " + ", ".join(f"{t['tag']} -> {t['commit']}" for t in tags)]
|
|
742
|
+
if notes:
|
|
743
|
+
lines += [""] + [f"Note: {n}" for n in notes]
|
|
744
|
+
text = "\n".join(lines) + "\n"
|
|
745
|
+
out_dir = ev.Store().root / "ledger"
|
|
746
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
747
|
+
path = out_dir / f"history-{sha256_hex(json.dumps([repo, rev, metric, text]))[:12]}.md"
|
|
748
|
+
path.write_text(text)
|
|
749
|
+
return {"format": "md", "markdown": text, "path": str(path), **out}
|