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,297 @@
|
|
|
1
|
+
"""bench rounds: every engine of a run manifest, R balanced rounds, one server at a time, via bench serve.
|
|
2
|
+
|
|
3
|
+
For each round (order from a Williams design, see ``serve/rounds.py``) and each engine:
|
|
4
|
+
launch the server from the manifest (argv, env, port) under a clean environment and in
|
|
5
|
+
its own process group, wait for ``ready_check``, warm up, run ``inferencekit bench serve``
|
|
6
|
+
against it as a subprocess, scrape ``/metrics`` before and after, stop the whole process
|
|
7
|
+
group, and check GPU memory is back to its pre-launch level. The run holds an exclusive
|
|
8
|
+
GPU lock file throughout.
|
|
9
|
+
|
|
10
|
+
The server log of every cell is kept (``logs/<label>.r<k>.log``) and parsed for the
|
|
11
|
+
configuration the engine reports it actually used (``resolved_config/<label>.r<k>.json``):
|
|
12
|
+
status RESOLVED / PARTIAL / SELF_REPORTED / UNPROVEN, hard-gate checks (CUDA graphs,
|
|
13
|
+
attention fallback, GPU offload), warning lines and unknown settings.
|
|
14
|
+
|
|
15
|
+
Output: one evidence record (kind ``serve_benchmark``): per engine x bucket x metric, the
|
|
16
|
+
median over rounds with a bootstrap 95% CI, and the ratio to the best engine with a paired
|
|
17
|
+
bootstrap 95% CI [derived]; plus ``rounds/summary.md`` in the run directory. Single mode's
|
|
18
|
+
medians are [observed] (client-side timings of the round's requests); load mode's come
|
|
19
|
+
from bench serve's capacity records (N* and the values at N*) and are [derived].
|
|
20
|
+
|
|
21
|
+
``--extra-body`` (or the manifest's ``workload.extra_body``) times every engine with the same
|
|
22
|
+
extra request fields, e.g. sampling settings. ``--ci-target`` turns ``--rounds`` into a maximum
|
|
23
|
+
and stops at the first whole Williams period, from ``--min-rounds`` on, where every engine's
|
|
24
|
+
decode_tok_s ratio CI is narrow enough; the rule is recorded and named in the limitations.
|
|
25
|
+
|
|
26
|
+
``--mode clip`` (video) hands the run to ``serve/clip_rounds.py``: the same rounds, launches and lock,
|
|
27
|
+
with ``bench clip`` per cell instead of bench serve and ``--prompts`` as the clip set; --warmup counts
|
|
28
|
+
untimed clips per distinct request shape (use 1: each is a whole clip). Adaptive stop and the text
|
|
29
|
+
request settings (--ref-e2e-ms, --chat-template-kwargs, --extra-body) are refused there.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from inferencekit import evidence as ev
|
|
38
|
+
from inferencekit.gpuinfo import gpu_identity
|
|
39
|
+
from inferencekit.ops.doctor import gpu_processes
|
|
40
|
+
from inferencekit.registry import Arg, OpError, operation
|
|
41
|
+
from inferencekit.serve import rounds as rr
|
|
42
|
+
|
|
43
|
+
KIND = "serve_benchmark"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _limitations(result: dict) -> tuple[list[str], list[str]]:
|
|
47
|
+
plan, cells, resolved = result["plan"], result["cells"], result["resolved_config"]
|
|
48
|
+
labels = [e["label"] for e in plan["engines"]]
|
|
49
|
+
lim, missing = [], []
|
|
50
|
+
d = plan["design"]
|
|
51
|
+
if not d["balanced"]:
|
|
52
|
+
lim.append(f"order not balanced: {plan['rounds']} rounds is not a multiple of the Williams period "
|
|
53
|
+
f"{d['period']} for {len(labels)} engines (position and carry-over effects not fully cancelled)")
|
|
54
|
+
if plan["rounds"] < 2:
|
|
55
|
+
lim.append("fewer than 2 rounds: no confidence intervals")
|
|
56
|
+
failures = result["summary"].get("failures") or []
|
|
57
|
+
per_engine_buckets = {}
|
|
58
|
+
for f in failures:
|
|
59
|
+
per_engine_buckets.setdefault(f["engine"], []).append(f["bucket"])
|
|
60
|
+
for f in failures:
|
|
61
|
+
if f["bucket"] == "all" and len(per_engine_buckets[f["engine"]]) > 1:
|
|
62
|
+
continue # the per-bucket lines already say it
|
|
63
|
+
of = f" of {f['requests']:g}" if isinstance(f.get("requests"), (int, float)) else ""
|
|
64
|
+
lim.append(f"{f['engine']}/{f['bucket']}: {f['failed_requests']:g}{of} requests failed over {f['rounds']} "
|
|
65
|
+
"round(s); this engine is not ranked there (latencies cover only answered requests)")
|
|
66
|
+
sched = plan.get("load_schedule")
|
|
67
|
+
if sched is not None and not sched.get("shared"):
|
|
68
|
+
lim.append("load schedules differ per engine: concurrency levels were turned into arrival rates with "
|
|
69
|
+
"each engine's own warm-up e2e, so the same level N offered different loads; pass "
|
|
70
|
+
"--ref-e2e-ms, or --rates in --serve-args")
|
|
71
|
+
by_status: dict[str, list[str]] = {}
|
|
72
|
+
for c in cells:
|
|
73
|
+
if c.get("status") != "ok":
|
|
74
|
+
by_status.setdefault(c.get("status") or "unknown", []).append(f"{c['label']} r{c['round']}")
|
|
75
|
+
for status, where in sorted(by_status.items()):
|
|
76
|
+
lim.append(f"{len(where)} cell(s) {status}: {', '.join(where[:8])}")
|
|
77
|
+
for label in labels:
|
|
78
|
+
if not any(c["label"] == label and c.get("status") == "ok" for c in cells):
|
|
79
|
+
missing.append(f"{label}: no successful round")
|
|
80
|
+
not_back = [f"{c['label']} r{c['round']}" for c in cells
|
|
81
|
+
if (c.get("gpu_memory") or {}).get("checked") and not c["gpu_memory"].get("returned")]
|
|
82
|
+
if not_back:
|
|
83
|
+
lim.append(f"GPU memory did not return to its pre-launch level after: {', '.join(not_back[:8])} "
|
|
84
|
+
"(a leftover process may have contended with the next engine)")
|
|
85
|
+
if any(c.get("gpu_memory") and not c["gpu_memory"].get("checked") for c in cells):
|
|
86
|
+
lim.append("GPU memory release not checked (nvidia-smi not available)")
|
|
87
|
+
killed = [f"{c['label']} r{c['round']}" for c in cells if (c.get("stop") or {}).get("method") == "sigkill"]
|
|
88
|
+
if killed:
|
|
89
|
+
lim.append(f"server ignored SIGTERM and was killed: {', '.join(killed[:8])}")
|
|
90
|
+
alive = [f"{c['label']} r{c['round']}" for c in cells if c.get("stop") and not c["stop"].get("group_gone")]
|
|
91
|
+
if alive:
|
|
92
|
+
lim.append(f"processes of the server survived SIGKILL: {', '.join(alive[:8])}")
|
|
93
|
+
for label, info in resolved.items():
|
|
94
|
+
if info["status"] != "RESOLVED":
|
|
95
|
+
lim.append(f"{label}: resolved configuration {info['status']} (raw log kept under logs/)")
|
|
96
|
+
if info.get("self_reported"):
|
|
97
|
+
used = "it is the only source" if info["status"] == "SELF_REPORTED" else \
|
|
98
|
+
"ignored for status and hard gates (the engine's own log lines decide)"
|
|
99
|
+
lim.append(f"{label}: settings self-reported by the engine process, not verified "
|
|
100
|
+
f"({', '.join(info['self_reported'][:8])}); {used}")
|
|
101
|
+
for v in info["violations"]:
|
|
102
|
+
lim.append(f"HARD GATE: {label}: {v}")
|
|
103
|
+
if len(info.get("attention_backends") or []) > 1:
|
|
104
|
+
lim.append(f"{label}: attention backend differed between rounds: {info['attention_backends']}")
|
|
105
|
+
if info["unknown_settings"]:
|
|
106
|
+
lim.append(f"{label}: engine reported unknown settings: {', '.join(info['unknown_settings'][:6])}")
|
|
107
|
+
if info["warnings"]:
|
|
108
|
+
lim.append(f"{label}: {info['warnings']} distinct warning line(s) in the server log "
|
|
109
|
+
"(resolved_config/*.json)")
|
|
110
|
+
secret = sorted({n for c in cells for n in (c.get("env") or {}).get("secret_values_in_manifest", [])})
|
|
111
|
+
if secret:
|
|
112
|
+
lim.append(f"manifest env writes secret-looking variable(s) {secret} as values; use null to inherit them "
|
|
113
|
+
"from the caller instead")
|
|
114
|
+
absent = sorted({n for c in cells for n in (c.get("env") or {}).get("missing", [])})
|
|
115
|
+
if absent:
|
|
116
|
+
lim.append(f"manifest env asks to inherit {absent}, but they were not set")
|
|
117
|
+
for w in (plan.get("extra_body") or {}).get("warnings") or []:
|
|
118
|
+
lim.append(f"extra body sent to every engine: {w}") # the warning names where the body came from
|
|
119
|
+
adaptive = plan.get("adaptive")
|
|
120
|
+
if adaptive:
|
|
121
|
+
last = (adaptive.get("checks") or [{}])[-1].get("widths") or {}
|
|
122
|
+
widths = ", ".join(f"{lb} {w['width']:.3g}" for lb, w in last.items() if w.get("width") is not None)
|
|
123
|
+
if plan.get("stopped_early"):
|
|
124
|
+
lim.append(f"adaptive stop: stopped after round {plan['rounds']} of at most {adaptive['max_rounds']} "
|
|
125
|
+
f"because every engine's CI half-width was <= {adaptive['ci_target']:g} ({widths}); stopping "
|
|
126
|
+
"once a CI looks narrow is optional stopping, so these 95% CIs are slightly too narrow: fix "
|
|
127
|
+
"the rule before the run (plan.adaptive.rule)")
|
|
128
|
+
else:
|
|
129
|
+
lim.append(f"adaptive stop: the CI target {adaptive['ci_target']:g} was "
|
|
130
|
+
f"{'met' if adaptive.get('met') else 'NOT met'} by round {plan['rounds']}, the maximum "
|
|
131
|
+
f"({widths or 'no CI'})")
|
|
132
|
+
return lim, missing
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@operation("bench_rounds", ("bench", "rounds"), "Run every engine of a run manifest for R balanced (Williams) "
|
|
136
|
+
"rounds, one server at a time: clean launch, readiness, warm-up, bench serve, /metrics, group stop, "
|
|
137
|
+
"GPU memory check, resolved-config capture; medians with CIs and ratio to best.",
|
|
138
|
+
[Arg("manifest", "string", "Run manifest (runs/<run-id>/manifest.json); its 'engines' are launched.",
|
|
139
|
+
required=True),
|
|
140
|
+
Arg("prompts", "string", "Prompt set (JSONL) passed to every bench serve call (--mode clip: the clip "
|
|
141
|
+
"set, passed to every bench clip call).", required=True),
|
|
142
|
+
Arg("rounds", "integer", "Rounds; each runs every engine once. Default: the smallest multiple of the "
|
|
143
|
+
"Williams period that is at least 3, so the order is balanced (2 engines: 4, 3: 6, 4: 4, 5: 10)."),
|
|
144
|
+
Arg("mode", "string", "bench serve mode (single, load), or clip: video clip engines measured with bench "
|
|
145
|
+
"clip, --prompts is the clip set (JSONL).", default="single", choices=("single", "load", "clip")),
|
|
146
|
+
Arg("engines", "string", "Comma-separated engine labels to run (default: every engine in the manifest)."),
|
|
147
|
+
Arg("run_dir", "string", "Run directory for logs and results (default: the manifest's directory)."),
|
|
148
|
+
Arg("gpu_lock", "string", "Lock file held for the whole run (default: $INFERENCEKIT_GPU_LOCK or "
|
|
149
|
+
"~/.cache/inferencekit/gpu.lock)."),
|
|
150
|
+
Arg("startup_timeout_s", "number", "Seconds to wait for an engine's ready_check to answer 200.",
|
|
151
|
+
default=1200),
|
|
152
|
+
Arg("stop_timeout_s", "number", "Seconds between SIGTERM and SIGKILL of a server's process group.",
|
|
153
|
+
default=30),
|
|
154
|
+
Arg("serve_timeout_s", "number", "Seconds before one bench serve call (--mode clip: one bench clip call) "
|
|
155
|
+
"is abandoned.", default=7200),
|
|
156
|
+
Arg("warmup", "integer", "Untimed warm-up requests per server before bench serve (the set's first "
|
|
157
|
+
"prompts, each salted with a unique prefix tag so they never seed the prefix cache). --mode clip: "
|
|
158
|
+
"untimed clips per distinct request shape (bench clip --warmup); use 1, each is a whole clip.",
|
|
159
|
+
default=2),
|
|
160
|
+
Arg("ref_e2e_ms", "number", "Load mode: unloaded e2e latency (ms) passed as --ref-e2e-ms to every bench "
|
|
161
|
+
"serve call, so every engine gets the same arrival schedule. Default: measured by the first load "
|
|
162
|
+
"cell's warm-up and passed to every later cell (unless --serve-args has --rates, --ref-e2e-ms or "
|
|
163
|
+
"--closed-loop)."),
|
|
164
|
+
Arg("serve_args", "string", "Extra arguments appended to every bench serve call (e.g. the load grid; "
|
|
165
|
+
"--mode clip: to every bench clip call, e.g. '--seed-base 7')."),
|
|
166
|
+
Arg("serve_cmd", "string", "Command that runs bench serve, default '<python> -m inferencekit bench "
|
|
167
|
+
"serve' (or $INFERENCEKIT_BENCH_SERVE_CMD); {base_url} {prompts} {mode} {label} {round} {run_dir} "
|
|
168
|
+
"{model} placeholders are filled in (--mode clip: the bench clip command, or "
|
|
169
|
+
"$INFERENCEKIT_BENCH_CLIP_CMD; placeholders in serve/clip_rounds.py). Stored only as a digest.",
|
|
170
|
+
sensitive=True),
|
|
171
|
+
Arg("gpu_mem_tolerance_mib", "integer", "GPU memory above the pre-launch level still counted as "
|
|
172
|
+
"released.", default=512),
|
|
173
|
+
Arg("chat_template_kwargs", "string", "JSON object passed to every bench serve call (and the warm-up) "
|
|
174
|
+
"as --chat-template-kwargs, e.g. '{\"enable_thinking\": false}'. Default: the manifest's "
|
|
175
|
+
"workload.chat_template_kwargs."),
|
|
176
|
+
Arg("extra_body", "string", "JSON object passed to every bench serve call as --extra-body (merged last "
|
|
177
|
+
"into every timed request), e.g. sampling settings '{\"temperature\": 1.0, \"top_k\": 64, "
|
|
178
|
+
"\"top_p\": 0.95, \"seed\": 1234}'. Default: the manifest's workload.extra_body."),
|
|
179
|
+
Arg("ci_target", "number", "Adaptive stop (single mode): --rounds becomes the maximum (default 12, "
|
|
180
|
+
"rounded up to whole Williams periods), and the run stops after the first whole period, from "
|
|
181
|
+
"--min-rounds on, where every engine's decode_tok_s [all] ratio to the best engine has a 95% CI "
|
|
182
|
+
"half-width at or below this fraction of the ratio (e.g. 0.01 = +/-1%). Default: off, every round "
|
|
183
|
+
"runs."),
|
|
184
|
+
Arg("min_rounds", "integer", "Adaptive stop: never stop before this many rounds (default: the default "
|
|
185
|
+
"--rounds, the smallest whole number of Williams periods that is at least 3). Needs --ci-target.")])
|
|
186
|
+
def bench_rounds(call, manifest, prompts, rounds, mode, engines, run_dir, gpu_lock, startup_timeout_s,
|
|
187
|
+
stop_timeout_s, serve_timeout_s, warmup, ref_e2e_ms, serve_args, serve_cmd, gpu_mem_tolerance_mib,
|
|
188
|
+
chat_template_kwargs=None, extra_body=None, ci_target=None, min_rounds=None):
|
|
189
|
+
if mode == "clip": # video clips: bench clip per cell (serve/clip_rounds.py); --prompts is the clip set
|
|
190
|
+
unsupported = [flag for flag, value in (("--ci-target", ci_target), ("--min-rounds", min_rounds),
|
|
191
|
+
("--ref-e2e-ms", ref_e2e_ms),
|
|
192
|
+
("--chat-template-kwargs", chat_template_kwargs),
|
|
193
|
+
("--extra-body", extra_body)) if value is not None]
|
|
194
|
+
if unsupported:
|
|
195
|
+
raise OpError(f"{', '.join(unsupported)} not supported with --mode clip (bench clip times whole clips; "
|
|
196
|
+
"adaptive stop and text request settings do not apply)")
|
|
197
|
+
from inferencekit.serve.clip_rounds import bench_rounds_clip
|
|
198
|
+
return bench_rounds_clip(call, manifest=manifest, clips=prompts, rounds=rounds, engines=engines,
|
|
199
|
+
run_dir=run_dir, gpu_lock=gpu_lock, startup_timeout_s=startup_timeout_s,
|
|
200
|
+
stop_timeout_s=stop_timeout_s, clip_timeout_s=serve_timeout_s, warmup=warmup,
|
|
201
|
+
clip_args=serve_args, clip_cmd=serve_cmd, gpu_mem_tolerance_mib=gpu_mem_tolerance_mib)
|
|
202
|
+
others = gpu_processes()
|
|
203
|
+
result = rr.run_rounds(manifest, prompts, rounds, mode, engines=engines, run_dir=run_dir,
|
|
204
|
+
gpu_lock_path=gpu_lock, startup_timeout_s=startup_timeout_s,
|
|
205
|
+
stop_timeout_s=stop_timeout_s, serve_timeout_s=serve_timeout_s, warmup=warmup,
|
|
206
|
+
serve_cmd=serve_cmd, serve_args=serve_args, gpu_mem_tolerance_mib=gpu_mem_tolerance_mib,
|
|
207
|
+
ref_e2e_ms=ref_e2e_ms, chat_template_kwargs=chat_template_kwargs, extra_body=extra_body,
|
|
208
|
+
ci_target=ci_target, min_rounds=min_rounds)
|
|
209
|
+
plan, summary, rdir = result["plan"], result["summary"], Path(result["run_dir"])
|
|
210
|
+
labels = [e["label"] for e in plan["engines"]]
|
|
211
|
+
limitations, missing = _limitations(result)
|
|
212
|
+
if others:
|
|
213
|
+
limitations.append(f"{len(others)} other compute process(es) were on the GPU when the run started: "
|
|
214
|
+
"timings may be contended")
|
|
215
|
+
metrics = []
|
|
216
|
+
for r in summary["rows"]:
|
|
217
|
+
subject = f"{r['engine']}/{r['bucket']}"
|
|
218
|
+
metrics.append(ev.metric(r["metric"], r["median"], r["unit"] or "unknown", "median_over_rounds", n=r["n"],
|
|
219
|
+
ci95=r["ci95"], subject=subject))
|
|
220
|
+
if r["ratio_to_best"] is not None:
|
|
221
|
+
metrics.append(ev.metric(f"{r['metric']}_ratio_to_best", r["ratio_to_best"], "x", "ratio_of_medians",
|
|
222
|
+
n=r["n_pairs"], ci95=r["ratio_ci95"], subject=subject))
|
|
223
|
+
m = result["manifest"].get("model") or {}
|
|
224
|
+
flags = {"engines": labels}
|
|
225
|
+
if plan.get("extra_body"):
|
|
226
|
+
flags["extra_body"] = plan["extra_body"]["value"] # same body for every engine: part of what was measured
|
|
227
|
+
ident = ev.identity(gpu=gpu_identity(),
|
|
228
|
+
engine={"name": "+".join(labels), "flags": flags},
|
|
229
|
+
model={"id": m.get("id"), "revision": m.get("revision"), "dtype": m.get("precision")},
|
|
230
|
+
workload={"mode": mode, "endpoint_kind": "openai_chat",
|
|
231
|
+
"prompt_set": {"name": plan["prompts"]["name"],
|
|
232
|
+
"sha256": plan["prompts"]["sha256"]}})
|
|
233
|
+
n_ok = sum(1 for c in result["cells"] if c.get("status") == "ok")
|
|
234
|
+
load = mode == "load"
|
|
235
|
+
cls = "derived" if load else "observed"
|
|
236
|
+
summary_line = (f"bench rounds ({mode}): {len(labels)} engines x {plan['rounds']} rounds, "
|
|
237
|
+
f"{n_ok}/{len(result['cells'])} "
|
|
238
|
+
f"cells ok, Williams order {'balanced' if plan['design']['balanced'] else 'NOT balanced'}")
|
|
239
|
+
adaptive = plan.get("adaptive")
|
|
240
|
+
if adaptive:
|
|
241
|
+
target = f"CI target {adaptive['ci_target']:g}"
|
|
242
|
+
summary_line += (f" (adaptive: stopped early, {target} met)" if plan.get("stopped_early") else
|
|
243
|
+
f" (adaptive: {target} {'met' if adaptive.get('met') else 'NOT met'} at the maximum)")
|
|
244
|
+
if plan.get("extra_body"):
|
|
245
|
+
summary_line += "; extra body " + json.dumps(plan["extra_body"]["value"], sort_keys=True)[:120]
|
|
246
|
+
heads = [r for r in summary["rows"] if r["engine"] == r["best"]]
|
|
247
|
+
if heads:
|
|
248
|
+
h = heads[0]
|
|
249
|
+
summary_line += f"; best {h['metric']} [{h['bucket']}]: {h['engine']} {h['median']:.4g} [{cls}]"
|
|
250
|
+
artifacts = [ev.artifact(rdir / "rounds" / name, rdir, role)
|
|
251
|
+
for name, role in (("summary.md", "summary"), ("summary.json", "summary"), ("plan.json", "plan"))
|
|
252
|
+
if (rdir / "rounds" / name).is_file()]
|
|
253
|
+
for c in result["cells"]:
|
|
254
|
+
log = rdir / c["log"]
|
|
255
|
+
if log.is_file():
|
|
256
|
+
artifacts.append(ev.artifact(log, rdir, "server_log"))
|
|
257
|
+
rc = (c.get("resolved_config") or {}).get("file")
|
|
258
|
+
if rc and (rdir / rc).is_file():
|
|
259
|
+
artifacts.append(ev.artifact(rdir / rc, rdir, "resolved_config"))
|
|
260
|
+
v2 = KIND in ev.KINDS
|
|
261
|
+
if not v2:
|
|
262
|
+
limitations.append("evidence v2 (kind serve_benchmark) not available in this build: record written "
|
|
263
|
+
"without schema validation")
|
|
264
|
+
cells = [{k: c.get(k) for k in ("round", "position", "label", "kind", "status", "served_model", "ready",
|
|
265
|
+
"warmup", "serve", "stop", "gpu_memory", "resolved_config", "env", "log",
|
|
266
|
+
"metrics_source", "load_schedule", "t_start", "t_end", "detail")}
|
|
267
|
+
for c in result["cells"]]
|
|
268
|
+
if load:
|
|
269
|
+
claim_classes = {"<metric>": "derived (median over rounds of bench serve's capacity record: N* and the "
|
|
270
|
+
"values at N*, derived from its observed load levels)",
|
|
271
|
+
"<metric>_ratio_to_best": "derived"}
|
|
272
|
+
else:
|
|
273
|
+
claim_classes = {"<metric>": "observed (median over rounds of bench serve's client-side timings)",
|
|
274
|
+
"<metric>_ratio_to_best": "derived"}
|
|
275
|
+
record = ev.build(call, kind=KIND if v2 else "benchmark", claim_class=cls, summary=summary_line,
|
|
276
|
+
identity_=ident, metrics=metrics, artifacts=artifacts, missing=missing,
|
|
277
|
+
limitations=limitations,
|
|
278
|
+
data={"run_dir": str(rdir), "plan": plan, "cells": cells, "rows": summary["rows"],
|
|
279
|
+
"definitions": summary["definitions"],
|
|
280
|
+
"metric_claim_classes": claim_classes, "failures": summary.get("failures") or [],
|
|
281
|
+
"resolved_config": result["resolved_config"],
|
|
282
|
+
"bench_serve_evidence": [c["serve"]["evidence_id"] for c in result["cells"]
|
|
283
|
+
if (c.get("serve") or {}).get("evidence_id")],
|
|
284
|
+
"gpu_processes_at_start": len(others), "markdown": result["markdown"]})
|
|
285
|
+
store = ev.Store()
|
|
286
|
+
if v2:
|
|
287
|
+
rec = store.save(record)
|
|
288
|
+
else:
|
|
289
|
+
record["kind"] = KIND
|
|
290
|
+
rec = ev.finalize(record)
|
|
291
|
+
target = store.run_dir(rec["evidence_id"])
|
|
292
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
293
|
+
(target / "evidence.json").write_text(json.dumps(rec, indent=2, ensure_ascii=False) + "\n")
|
|
294
|
+
copy = rdir / "evidence" / f"{rec['evidence_id']}.json"
|
|
295
|
+
copy.parent.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
copy.write_text(json.dumps(rec, indent=2, ensure_ascii=False) + "\n")
|
|
297
|
+
return rec
|