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,429 @@
|
|
|
1
|
+
"""Clip rounds: every clip engine of a run manifest, R rounds in a Williams order, one server at a time.
|
|
2
|
+
|
|
3
|
+
The text runner's reasoning (serve/rounds.py) holds for clips too: a GPU's speed drifts, so every round
|
|
4
|
+
runs every engine once, in a balanced order. This module reuses, by import, its order design
|
|
5
|
+
(round_orders, default_rounds), manifest checks (load_manifest, engine_specs), output parsing
|
|
6
|
+
(parse_serve_output, request_rows, cell_status) and summary (summarize), plus the server lifecycle of
|
|
7
|
+
servers.py: clean environment, launch in its own process group, readiness, group stop, GPU memory back
|
|
8
|
+
to its pre-launch level, and the GPU lock held for the whole run. What differs from text rounds:
|
|
9
|
+
|
|
10
|
+
* readiness is ``GET /health`` (the clip HTTP contract) unless the manifest's engine names another
|
|
11
|
+
``ready_check``; there is no /v1/models lookup and no chat warm-up: bench clip's own warm-up (untimed
|
|
12
|
+
clips at every distinct request shape of the set, before its first timed clip) runs in every cell,
|
|
13
|
+
after each fresh launch;
|
|
14
|
+
* the measurement is ``inferencekit bench clip`` as a subprocess, with ``--first-round k-1`` so round k
|
|
15
|
+
gets the same pre-registered seed on every engine, and ``--extend`` from the engine's previous record
|
|
16
|
+
so its last record holds all its rounds (``compare --metric clip_ms`` takes two of those);
|
|
17
|
+
* per-round values are clip_ms (mean over the round's clips, bench_clip.round_stats) and clips_per_hour,
|
|
18
|
+
per task and 'all';
|
|
19
|
+
* the server log is kept but not run through the text engines' resolved-config parser, whose hard gates
|
|
20
|
+
(CUDA graphs, attention fallback) are rules for LLM servers; what a clip engine says it ran is in
|
|
21
|
+
bench clip's rows (``effective``, self-reported).
|
|
22
|
+
|
|
23
|
+
Manifest engine fields beyond the text runner's: ``endpoint_kind`` (clip_http by default, or comfyui) and
|
|
24
|
+
``workflow`` (comfyui; a path relative to the manifest is resolved there).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import shlex
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from inferencekit import evidence as ev
|
|
37
|
+
from inferencekit import proc, stats
|
|
38
|
+
from inferencekit.canonical import file_sha256
|
|
39
|
+
from inferencekit.gpuinfo import gpu_identity
|
|
40
|
+
from inferencekit.ops import bench_clip as bc
|
|
41
|
+
from inferencekit.ops.doctor import gpu_processes
|
|
42
|
+
from inferencekit.registry import OpError
|
|
43
|
+
from inferencekit.serve import clip_client as cc
|
|
44
|
+
from inferencekit.serve import rounds as rr
|
|
45
|
+
from inferencekit.serve import servers
|
|
46
|
+
|
|
47
|
+
CLIP_CMD_ENV = "INFERENCEKIT_BENCH_CLIP_CMD"
|
|
48
|
+
DIRECTIONS = {"clip_ms": False, "clips_per_hour": True} # ranked here even where rr.higher_is_better cannot
|
|
49
|
+
OUT_DIR = "clip_rounds"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --------------------------------------------------------------------------- manifest
|
|
53
|
+
|
|
54
|
+
def clip_specs(manifest: dict, manifest_path: str | os.PathLike, only: str | list[str] | None) -> list[dict]:
|
|
55
|
+
"""rr.engine_specs plus the clip fields: endpoint_kind, workflow, and /health as the default ready check."""
|
|
56
|
+
raw = {str(e.get("label") or e.get("name") or ""): e for e in manifest["engines"] if isinstance(e, dict)}
|
|
57
|
+
specs = rr.engine_specs(manifest, only)
|
|
58
|
+
base = Path(manifest_path).resolve().parent
|
|
59
|
+
problems = []
|
|
60
|
+
for s in specs:
|
|
61
|
+
e = raw.get(s["label"]) or {}
|
|
62
|
+
if not e.get("ready_check"):
|
|
63
|
+
s["ready_check"] = cc.HEALTH_PATH
|
|
64
|
+
s["endpoint_kind"] = e.get("endpoint_kind") or "clip_http"
|
|
65
|
+
if s["endpoint_kind"] not in cc.ENDPOINT_KINDS:
|
|
66
|
+
problems.append(f"engine '{s['label']}': endpoint_kind must be one of {list(cc.ENDPOINT_KINDS)}")
|
|
67
|
+
wf = e.get("workflow")
|
|
68
|
+
if wf is not None and not isinstance(wf, str):
|
|
69
|
+
problems.append(f"engine '{s['label']}': 'workflow' must be a path")
|
|
70
|
+
elif wf:
|
|
71
|
+
s["workflow"] = str(Path(wf) if Path(wf).is_absolute() or not (base / wf).is_file() else base / wf)
|
|
72
|
+
if problems:
|
|
73
|
+
raise OpError("manifest engines: " + "; ".join(problems))
|
|
74
|
+
return specs
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --------------------------------------------------------------------------- bench clip
|
|
78
|
+
|
|
79
|
+
def clip_argv(template: str | None, ctx: dict, extra: str | None) -> list[str]:
|
|
80
|
+
"""The ``bench clip`` command for one cell.
|
|
81
|
+
|
|
82
|
+
Default: ``<python> -m inferencekit bench clip --base-url ... --clips ... --endpoint-kind ... --label ...
|
|
83
|
+
--run-dir ... --rounds 1 --first-round <k-1> --warmup N``, plus ``--extend`` (the engine's previous
|
|
84
|
+
record), ``--workflow``, ``--engine-version`` and ``--model`` when known, then ``extra`` (the same for
|
|
85
|
+
every engine: seed policy, timeouts, --gpu-name...). A template (``clip_cmd`` or
|
|
86
|
+
$INFERENCEKIT_BENCH_CLIP_CMD) with ``{placeholders}`` is used as written ({base_url} {clips} {label}
|
|
87
|
+
{round} {first_round} {run_dir} {endpoint_kind} {workflow} {extend} {warmup} {model}); a template
|
|
88
|
+
without placeholders replaces only the program part.
|
|
89
|
+
"""
|
|
90
|
+
template = template or os.environ.get(CLIP_CMD_ENV) or ""
|
|
91
|
+
words = shlex.split(template) if template else [sys.executable, "-m", "inferencekit", "bench", "clip"]
|
|
92
|
+
if any("{" in w for w in words):
|
|
93
|
+
try:
|
|
94
|
+
argv = [w.format(**ctx) for w in words]
|
|
95
|
+
except (KeyError, IndexError, ValueError) as e:
|
|
96
|
+
raise OpError(f"clip command template: unknown placeholder {e}") from None
|
|
97
|
+
else:
|
|
98
|
+
argv = words + ["--base-url", ctx["base_url"], "--clips", ctx["clips"], "--endpoint-kind",
|
|
99
|
+
ctx["endpoint_kind"], "--label", ctx["label"], "--run-dir", ctx["run_dir"], "--rounds", "1",
|
|
100
|
+
"--first-round", str(ctx["first_round"]), "--warmup", str(ctx["warmup"])]
|
|
101
|
+
for flag, key in (("--extend", "extend"), ("--workflow", "workflow"), ("--engine-version", "engine_version"),
|
|
102
|
+
("--model", "model")):
|
|
103
|
+
if ctx.get(key):
|
|
104
|
+
argv += [flag, str(ctx[key])]
|
|
105
|
+
return argv + (shlex.split(extra) if extra else [])
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def round_metrics(rows: list[dict], k: int) -> dict:
|
|
109
|
+
"""``{task | 'all': {metric: {value, unit, stat, n}}}`` for bench clip round ``k`` (from its rows)."""
|
|
110
|
+
mine = [r for r in rows if r.get("round") == k]
|
|
111
|
+
if not mine:
|
|
112
|
+
return {} # no rows for this round: the cell has no metrics (cell_status says serve_no_metrics)
|
|
113
|
+
out: dict[str, dict] = {}
|
|
114
|
+
for subject, v in bc.round_stats(mine).items(): # the same per-round values as bench clip's data.rounds
|
|
115
|
+
mets = {}
|
|
116
|
+
if v["clip_ms"] is not None:
|
|
117
|
+
mets["clip_ms"] = {"value": v["clip_ms"], "unit": "ms", "stat": "mean", "n": v["n"]}
|
|
118
|
+
mets["clips_per_hour"] = {"value": v["clips_per_hour"], "unit": "clips/h", "stat": "rate", "n": v["n"]}
|
|
119
|
+
mets["failed_requests"] = {"value": float(v["n_failed"]), "unit": "clips", "stat": "count",
|
|
120
|
+
"n": v["n"] + v["n_failed"]}
|
|
121
|
+
out[subject] = mets
|
|
122
|
+
return out
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# --------------------------------------------------------------------------- one cell
|
|
126
|
+
|
|
127
|
+
def run_clip_cell(spec: dict, k: int, position: int, cfg: dict) -> dict:
|
|
128
|
+
"""Launch, check, measure (bench clip, one round) and stop one engine for round ``k``. Never raises for engine
|
|
129
|
+
failures."""
|
|
130
|
+
run_dir: Path = cfg["run_dir"]
|
|
131
|
+
label = spec["label"]
|
|
132
|
+
base = f"http://{spec['host']}:{spec['port']}"
|
|
133
|
+
log_rel = f"logs/{label}.r{k}.log"
|
|
134
|
+
log_path = run_dir / log_rel
|
|
135
|
+
cell: dict = {"round": k, "position": position, "label": label, "kind": spec["kind"],
|
|
136
|
+
"endpoint_kind": spec["endpoint_kind"], "status": None, "log": log_rel, "t_start": time.time()}
|
|
137
|
+
if log_path.exists():
|
|
138
|
+
log_path.unlink()
|
|
139
|
+
env, env_info = servers.server_env(spec["env"])
|
|
140
|
+
cell["env"] = env_info
|
|
141
|
+
if servers.port_in_use(spec["host"], spec["port"]):
|
|
142
|
+
cell.update(status="port_busy", detail=f"something already listens on {spec['host']}:{spec['port']}; "
|
|
143
|
+
"refusing to benchmark it in place of the engine")
|
|
144
|
+
cell["t_end"] = time.time()
|
|
145
|
+
return cell
|
|
146
|
+
gpu_before = cfg["gpu_reader"]()
|
|
147
|
+
server = None
|
|
148
|
+
try:
|
|
149
|
+
try:
|
|
150
|
+
server = servers.launch(spec["launch"], env, log_path, cwd=spec.get("cwd"))
|
|
151
|
+
except OSError as e:
|
|
152
|
+
cell.update(status="launch_failed", detail=str(e)[:300])
|
|
153
|
+
return cell
|
|
154
|
+
ready = servers.wait_ready(server, base + spec["ready_check"], cfg["startup_timeout_s"], cfg["poll_s"])
|
|
155
|
+
cell["ready"] = ready
|
|
156
|
+
if ready["status"] != "ready":
|
|
157
|
+
cell["status"] = "startup_timeout" if ready["status"] == "timeout" else "exited_early"
|
|
158
|
+
return cell
|
|
159
|
+
ctx = {"base_url": base, "clips": str(cfg["clips_path"]), "label": label, "round": k, "first_round": k - 1,
|
|
160
|
+
"run_dir": str(run_dir), "endpoint_kind": spec["endpoint_kind"], "workflow": spec.get("workflow") or "",
|
|
161
|
+
"engine_version": spec.get("version") or "", "extend": cfg["last_ev"].get(label) or "",
|
|
162
|
+
"warmup": cfg["warmup"], "model": cfg.get("model_id") or ""}
|
|
163
|
+
argv = clip_argv(cfg["clip_cmd"], ctx, cfg["clip_args"])
|
|
164
|
+
child_env = dict(os.environ)
|
|
165
|
+
# Same evidence store as this run, and the GPU lock is already held (by this process).
|
|
166
|
+
root = str(ev.default_root().resolve())
|
|
167
|
+
# The child's record is read from its stdout (parse_serve_output): full JSON, never the terse view.
|
|
168
|
+
child_env.update({"INFERKIT_RUNS_DIR": root, "INFERENCEKIT_RUNS_DIR": root,
|
|
169
|
+
"INFERENCEKIT_GPU_LOCK_HELD": str(cfg.get("gpu_lock_path") or ""),
|
|
170
|
+
"INFERENCEKIT_OUTPUT": "json"})
|
|
171
|
+
t0 = time.monotonic()
|
|
172
|
+
res = proc.run(argv, timeout=cfg["clip_timeout_s"], env=child_env) # a timeout kills its process group
|
|
173
|
+
record = rr.parse_serve_output(res.stdout)
|
|
174
|
+
cell["clip"] = {"returncode": res.returncode, "seconds": round(time.monotonic() - t0, 3),
|
|
175
|
+
"evidence_id": record.get("evidence_id") if record else None,
|
|
176
|
+
"stderr_tail": ev.redact_text(res.stderr[-1500:]) if res.returncode != 0 else None}
|
|
177
|
+
rows = rr.request_rows(record, ev.default_root())
|
|
178
|
+
cell["metrics"] = round_metrics(rows or [], k - 1)
|
|
179
|
+
cell["metrics_source"] = f"bench clip requests artifact, round {k - 1} (mean over its clips)"
|
|
180
|
+
if record is not None and res.returncode == 0 and record.get("evidence_id"):
|
|
181
|
+
cfg["last_ev"][label] = record["evidence_id"]
|
|
182
|
+
validity = (record.get("data") or {}).get("claim_validity") or {}
|
|
183
|
+
cell["claim_valid"] = validity.get("valid")
|
|
184
|
+
cell["violations"] = (validity.get("violations") or [])[:5]
|
|
185
|
+
# clips (all rounds so far: the record extends the last one) that reused the fixed negative prompt's
|
|
186
|
+
# encoding; allowed for every engine, stated with the ranking below
|
|
187
|
+
cell["cache_allowed_clips"] = validity.get("cache_allowed_clips")
|
|
188
|
+
cell["status"], detail = rr.cell_status(res.returncode, record, cell["metrics"] if rows else {})
|
|
189
|
+
if detail:
|
|
190
|
+
cell["detail"] = detail
|
|
191
|
+
return cell
|
|
192
|
+
finally:
|
|
193
|
+
if server is not None:
|
|
194
|
+
cell["stop"] = servers.stop(server, cfg["stop_timeout_s"])
|
|
195
|
+
if not cell["stop"]["group_gone"] and cell["status"] == "ok":
|
|
196
|
+
cell["status"] = "stop_failed"
|
|
197
|
+
cell["gpu_memory"] = servers.wait_gpu_memory(gpu_before, cfg["gpu_mem_tolerance_mib"],
|
|
198
|
+
cfg["gpu_mem_wait_s"], reader=cfg["gpu_reader"])
|
|
199
|
+
cell["t_end"] = time.time()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# --------------------------------------------------------------------------- summary
|
|
203
|
+
|
|
204
|
+
def summarize(cells: list[dict], labels: list[str], resamples: int = stats.DEFAULT_RESAMPLES, seed: int = 0) -> dict:
|
|
205
|
+
"""rr.summarize, with clips_per_hour ranked as a rate (rr.higher_is_better does not know '_per_hour')."""
|
|
206
|
+
out = rr.summarize(cells, labels, resamples=resamples, seed=seed, mode="single")
|
|
207
|
+
by_round: dict[tuple[str, str, str], dict[int, float]] = {}
|
|
208
|
+
for c in cells:
|
|
209
|
+
if c.get("status") in rr.STATUSES_OK:
|
|
210
|
+
for bucket, mets in (c.get("metrics") or {}).items():
|
|
211
|
+
for name, m in mets.items():
|
|
212
|
+
by_round.setdefault((c["label"], bucket, name), {})[c["round"]] = m["value"]
|
|
213
|
+
for name, higher in DIRECTIONS.items():
|
|
214
|
+
if rr.higher_is_better(name) == higher:
|
|
215
|
+
continue # the text runner ranks it already
|
|
216
|
+
for bucket in sorted({r["bucket"] for r in out["rows"] if r["metric"] == name}):
|
|
217
|
+
rows = [r for r in out["rows"] if r["metric"] == name and r["bucket"] == bucket]
|
|
218
|
+
eligible = [r for r in rows if not r["failed_requests"]]
|
|
219
|
+
best = (max if higher else min)(eligible, key=lambda r: r["median"])["engine"] if eligible else None
|
|
220
|
+
for r in rows:
|
|
221
|
+
r.update(higher_is_better=higher, best=best)
|
|
222
|
+
if r["failed_requests"]:
|
|
223
|
+
r["excluded_from_best"] = (f"{r['failed_requests']:g} failed clip(s) over {r['n']} round(s): "
|
|
224
|
+
"its rate covers only the clips it returned")
|
|
225
|
+
continue
|
|
226
|
+
if best is None:
|
|
227
|
+
continue
|
|
228
|
+
b, c = by_round[(best, bucket, name)], by_round[(r["engine"], bucket, name)]
|
|
229
|
+
paired = sorted(set(b) & set(c))
|
|
230
|
+
if r["engine"] == best:
|
|
231
|
+
r.update(ratio_to_best=1.0, n_pairs=len(paired))
|
|
232
|
+
elif paired:
|
|
233
|
+
res = stats.paired_ratio_ci([b[i] for i in paired], [c[i] for i in paired], resamples=resamples,
|
|
234
|
+
seed=seed)
|
|
235
|
+
r.update(ratio_to_best=res["ratio"], ratio_ci95=list(res["ci95"]), n_pairs=res["n_pairs"])
|
|
236
|
+
out["definitions"] = {
|
|
237
|
+
"median": "median over rounds of the per-round value (clip_ms: mean over that round's clips of the task, "
|
|
238
|
+
"or of every clip for 'all', from bench clip's requests artifact; client timer) (observed)",
|
|
239
|
+
"clips_per_hour": "per round: 3,600,000 x clips / sum of their clip_ms, one clip at a time (derived)",
|
|
240
|
+
"ci95": "bootstrap 95% CI of that median, rounds resampled",
|
|
241
|
+
"ratio_to_best": "median(engine) / median(best engine) over rounds both completed, with a paired bootstrap "
|
|
242
|
+
"95% CI resampling rounds (derived); best = lowest clip_ms, highest clips_per_hour, among "
|
|
243
|
+
"engines with no failed clip in the task",
|
|
244
|
+
"failed_requests": "clips that failed in a round; an engine with any failure in a task is never best there "
|
|
245
|
+
"and gets no ratio"}
|
|
246
|
+
return out
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def markdown(summary: dict, plan: dict, cells: list[dict]) -> str:
|
|
250
|
+
fmt = rr._fmt
|
|
251
|
+
lines = [f"# bench rounds: clip mode, {plan['rounds']} rounds, {len(plan['engines'])} engines", "",
|
|
252
|
+
f"Order: {plan['design']['design']} design, period {plan['design']['period']}, balanced: "
|
|
253
|
+
f"{'yes' if plan['design']['balanced'] else 'NO'}. Clip set {plan['clips']['name']} (sha256 "
|
|
254
|
+
f"{plan['clips']['sha256'][:12]}..., n={plan['clips']['n']}).", "",
|
|
255
|
+
"clip_ms medians and CIs are [observed] (client timer in `bench clip`, request sent to clip in hand); "
|
|
256
|
+
"clips_per_hour and ratios are [derived]. Stage times are self-reported by the engines and are only in "
|
|
257
|
+
"each engine's bench clip record.", "",
|
|
258
|
+
"| engine | task | metric | median | 95% CI | rounds | ratio to best [derived] | ratio 95% CI |",
|
|
259
|
+
"|---|---|---|---|---|---|---|---|"]
|
|
260
|
+
invalid = {c["label"] for c in cells if c.get("claim_valid") is False}
|
|
261
|
+
for r in summary["rows"]:
|
|
262
|
+
unit = f" {r['unit']}" if r.get("unit") else ""
|
|
263
|
+
mark = " (best)" if r["engine"] == r["best"] else ""
|
|
264
|
+
if r.get("failed_requests"):
|
|
265
|
+
mark += " [FAILURES]"
|
|
266
|
+
if r["engine"] in invalid:
|
|
267
|
+
mark += " [CLAIM INVALID]"
|
|
268
|
+
ratio_ci = "-" if r["engine"] == r["best"] or r["ratio_to_best"] is None else \
|
|
269
|
+
f"{fmt(r['ratio_ci95'][0])} - {fmt(r['ratio_ci95'][1])}"
|
|
270
|
+
lines.append(f"| {r['engine']}{mark} | {r['bucket']} | {r['metric']} | {fmt(r['median'])}{unit} | "
|
|
271
|
+
f"{fmt(r['ci95'][0])} - {fmt(r['ci95'][1])} | {r['n']} | {fmt(r['ratio_to_best'])} | "
|
|
272
|
+
f"{ratio_ci} |")
|
|
273
|
+
if not summary["rows"]:
|
|
274
|
+
lines.append("| (no successful rounds) | | | | | | | |")
|
|
275
|
+
failed = [c for c in cells if c.get("status") != "ok"]
|
|
276
|
+
if failed:
|
|
277
|
+
lines += ["", "## Failed cells", "", "| round | engine | status | detail |", "|---|---|---|---|"]
|
|
278
|
+
for c in failed:
|
|
279
|
+
detail = c.get("detail") or (c.get("ready") or {}).get("last") or (c.get("clip") or {}).get("returncode")
|
|
280
|
+
lines.append(f"| {c['round']} | {c['label']} | {c['status']} | {str(detail)[:120]} |")
|
|
281
|
+
return "\n".join(lines) + "\n"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# --------------------------------------------------------------------------- the run
|
|
285
|
+
|
|
286
|
+
def run_clip_rounds(manifest_path: str, clips_path: str, rounds: int | None = None, engines: str | None = None,
|
|
287
|
+
run_dir: str | None = None, gpu_lock_path: str | None = None, startup_timeout_s: float = 1800,
|
|
288
|
+
stop_timeout_s: float = 30, clip_timeout_s: float = 14400, warmup: int = 1,
|
|
289
|
+
clip_cmd: str | None = None, clip_args: str | None = None, gpu_mem_tolerance_mib: int = 512,
|
|
290
|
+
gpu_mem_wait_s: float = 60, poll_s: float = 0.5, gpu_reader=None) -> dict:
|
|
291
|
+
"""Run every (round, engine) clip cell under the GPU lock; return plan, cells, summary and paths."""
|
|
292
|
+
manifest = rr.load_manifest(manifest_path)
|
|
293
|
+
specs = clip_specs(manifest, manifest_path, engines)
|
|
294
|
+
if not specs:
|
|
295
|
+
raise OpError("no engines selected")
|
|
296
|
+
rounds = rr.default_rounds(len(specs)) if rounds is None else rounds
|
|
297
|
+
if rounds < 1:
|
|
298
|
+
raise OpError("rounds must be >= 1")
|
|
299
|
+
if warmup < 0:
|
|
300
|
+
raise OpError("warmup must be >= 0")
|
|
301
|
+
clipset = bc.load_clip_set(clips_path) # fail before any launch when the set is bad
|
|
302
|
+
rdir = Path(run_dir) if run_dir else Path(manifest_path).resolve().parent
|
|
303
|
+
rdir.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
labels = [s["label"] for s in specs]
|
|
305
|
+
design = rr.round_orders(labels, rounds)
|
|
306
|
+
plan = {"mode": "clip", "rounds": rounds,
|
|
307
|
+
"engines": [{k: s.get(k) for k in ("label", "name", "kind", "version", "port", "ready_check", "role",
|
|
308
|
+
"endpoint_kind")} for s in specs],
|
|
309
|
+
"design": design, "clips": {"name": clipset.name, "sha256": clipset.sha256, "n": len(clipset.clips)},
|
|
310
|
+
"settings": {"startup_timeout_s": startup_timeout_s, "stop_timeout_s": stop_timeout_s,
|
|
311
|
+
"clip_timeout_s": clip_timeout_s, "warmup": warmup, "clip_args": clip_args,
|
|
312
|
+
"clip_cmd_custom": bool(clip_cmd or os.environ.get(CLIP_CMD_ENV)),
|
|
313
|
+
"gpu_mem_tolerance_mib": gpu_mem_tolerance_mib, "gpu_mem_wait_s": gpu_mem_wait_s,
|
|
314
|
+
"warmup_kind": "bench clip's own warm-up after every launch: `warmup` untimed clip(s) for "
|
|
315
|
+
"each distinct request shape of the set (task, size, frames, fps, steps, "
|
|
316
|
+
"guidance, audio, image), all before the first timed clip, seeds outside "
|
|
317
|
+
"the timed seed policy and prompts no timed clip has"},
|
|
318
|
+
"manifest_sha256": file_sha256(Path(manifest_path))[0]}
|
|
319
|
+
cfg = {"run_dir": rdir, "clips_path": Path(clips_path).resolve(), "startup_timeout_s": startup_timeout_s,
|
|
320
|
+
"stop_timeout_s": stop_timeout_s, "clip_timeout_s": clip_timeout_s, "warmup": warmup, "clip_cmd": clip_cmd,
|
|
321
|
+
"clip_args": clip_args, "gpu_mem_tolerance_mib": gpu_mem_tolerance_mib, "gpu_mem_wait_s": gpu_mem_wait_s,
|
|
322
|
+
"poll_s": poll_s, "gpu_reader": gpu_reader or servers.gpu_memory_used_mib,
|
|
323
|
+
"model_id": (manifest.get("model") or {}).get("id"), "last_ev": {}}
|
|
324
|
+
out_dir = rdir / OUT_DIR
|
|
325
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
cells: list[dict] = []
|
|
327
|
+
with servers.gpu_lock(gpu_lock_path, owner=f"inferencekit bench rounds (clip) {rdir.name}") as lock:
|
|
328
|
+
plan["gpu_lock"] = lock["path"]
|
|
329
|
+
cfg["gpu_lock_path"] = lock["path"]
|
|
330
|
+
plan["started"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
331
|
+
(out_dir / "plan.json").write_text(json.dumps(ev.redact(plan), indent=2) + "\n")
|
|
332
|
+
by_label = {s["label"]: s for s in specs}
|
|
333
|
+
for k, order in enumerate(design["orders"], 1):
|
|
334
|
+
for pos, label in enumerate(order):
|
|
335
|
+
cell = run_clip_cell(by_label[label], k, pos, cfg)
|
|
336
|
+
cells.append(cell)
|
|
337
|
+
(out_dir / f"{label}.r{k}.json").write_text(json.dumps(ev.redact(cell), indent=2) + "\n")
|
|
338
|
+
plan["ended"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
339
|
+
summary = summarize(cells, labels)
|
|
340
|
+
plan["clip_evidence_final"] = dict(cfg["last_ev"]) # each engine's bench clip record with all its rounds
|
|
341
|
+
(out_dir / "plan.json").write_text(json.dumps(ev.redact(plan), indent=2) + "\n")
|
|
342
|
+
(out_dir / "summary.json").write_text(json.dumps(ev.redact({"plan": plan, "summary": summary}), indent=2) + "\n")
|
|
343
|
+
md = markdown(summary, plan, cells)
|
|
344
|
+
(out_dir / "summary.md").write_text(md)
|
|
345
|
+
return {"plan": plan, "cells": cells, "summary": summary, "markdown": md, "run_dir": rdir, "manifest": manifest,
|
|
346
|
+
"clipset": clipset}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def bench_rounds_clip(call, *, manifest: str, clips: str, rounds: int | None = None, engines: str | None = None,
|
|
350
|
+
run_dir: str | None = None, gpu_lock: str | None = None, startup_timeout_s: float = 1800,
|
|
351
|
+
stop_timeout_s: float = 30, clip_timeout_s: float = 14400, warmup: int = 1,
|
|
352
|
+
clip_args: str | None = None, clip_cmd: str | None = None, gpu_mem_tolerance_mib: int = 512,
|
|
353
|
+
gpu_reader=None) -> dict:
|
|
354
|
+
"""bench rounds in clip mode: run the rounds and store one ``clip_benchmark`` record (the summary)."""
|
|
355
|
+
from inferencekit.ops.bench_rounds import _limitations # the text runner's cell, order and failure notes
|
|
356
|
+
|
|
357
|
+
others = gpu_processes()
|
|
358
|
+
result = run_clip_rounds(manifest, clips, rounds, engines=engines, run_dir=run_dir, gpu_lock_path=gpu_lock,
|
|
359
|
+
startup_timeout_s=startup_timeout_s, stop_timeout_s=stop_timeout_s,
|
|
360
|
+
clip_timeout_s=clip_timeout_s, warmup=warmup, clip_cmd=clip_cmd, clip_args=clip_args,
|
|
361
|
+
gpu_mem_tolerance_mib=gpu_mem_tolerance_mib, gpu_reader=gpu_reader)
|
|
362
|
+
plan, summary, rdir, clipset = result["plan"], result["summary"], Path(result["run_dir"]), result["clipset"]
|
|
363
|
+
labels = [e["label"] for e in plan["engines"]]
|
|
364
|
+
limitations, missing = _limitations({**result, "resolved_config": {}})
|
|
365
|
+
if others:
|
|
366
|
+
limitations.append(f"{len(others)} other compute process(es) were on the GPU when the run started: "
|
|
367
|
+
"timings may be contended")
|
|
368
|
+
for label in labels:
|
|
369
|
+
bad = sorted({v for c in result["cells"] if c["label"] == label for v in c.get("violations") or []})
|
|
370
|
+
if bad:
|
|
371
|
+
limitations.append(f"CLAIM INVALID: {label}: {'; '.join(bad[:3])}")
|
|
372
|
+
reused = [x for x in labels if any((c.get("cache_allowed_clips") or 0) > 0 for c in result["cells"]
|
|
373
|
+
if c["label"] == x)]
|
|
374
|
+
if reused:
|
|
375
|
+
rest = [x for x in labels if x not in reused]
|
|
376
|
+
limitations.append(f"{', '.join(reused)} reused the encoding of a fixed negative prompt "
|
|
377
|
+
f"({cc.NEGATIVE_PROMPT_CACHE}: computed once, not per clip; allowed for every engine)"
|
|
378
|
+
+ (f"; {', '.join(rest)} encoded it on every clip, so the ranking compares their full text "
|
|
379
|
+
"encoding with a reused one" if rest else ""))
|
|
380
|
+
limitations.append("server logs kept under logs/ but not parsed for a resolved configuration (the text "
|
|
381
|
+
"engines' parser and hard gates do not apply to clip engines); each engine's self-reported "
|
|
382
|
+
"effective settings are in its bench clip record")
|
|
383
|
+
metrics = []
|
|
384
|
+
for r in summary["rows"]:
|
|
385
|
+
subject = f"{r['engine']}/{r['bucket']}"
|
|
386
|
+
metrics.append(ev.metric(r["metric"], r["median"], r["unit"] or "unknown", "median_over_rounds", n=r["n"],
|
|
387
|
+
ci95=r["ci95"], subject=subject))
|
|
388
|
+
if r["ratio_to_best"] is not None:
|
|
389
|
+
metrics.append(ev.metric(f"{r['metric']}_ratio_to_best", r["ratio_to_best"], "x", "ratio_of_medians",
|
|
390
|
+
n=r["n_pairs"], ci95=r["ratio_ci95"], subject=subject))
|
|
391
|
+
m = result["manifest"].get("model") or {}
|
|
392
|
+
kinds = sorted({e["endpoint_kind"] for e in plan["engines"]})
|
|
393
|
+
ident = ev.identity(gpu=gpu_identity(), engine={"name": "+".join(labels), "flags": {"engines": labels}},
|
|
394
|
+
model={"id": m.get("id"), "revision": m.get("revision"), "dtype": m.get("precision")},
|
|
395
|
+
workload={"mode": "single", "batch": 1, "endpoint_kind": "+".join(kinds),
|
|
396
|
+
"clip_set": clipset.identity(),
|
|
397
|
+
**{k: clipset.uniform(k) for k in bc.SHAPE_KEYS}})
|
|
398
|
+
n_ok = sum(1 for c in result["cells"] if c.get("status") == "ok")
|
|
399
|
+
line = (f"bench rounds (clip): {len(labels)} engines x {plan['rounds']} rounds, {n_ok}/{len(result['cells'])} "
|
|
400
|
+
f"cells ok, Williams order {'balanced' if plan['design']['balanced'] else 'NOT balanced'}")
|
|
401
|
+
heads = [r for r in summary["rows"] if r["engine"] == r["best"] and r["metric"] == "clip_ms" and
|
|
402
|
+
r["bucket"] == "all"]
|
|
403
|
+
if heads:
|
|
404
|
+
line += f"; best clip_ms [all]: {heads[0]['engine']} {heads[0]['median'] / 1000:.3g} s [observed]"
|
|
405
|
+
artifacts = [ev.artifact(rdir / OUT_DIR / name, rdir, role)
|
|
406
|
+
for name, role in (("summary.md", "summary"), ("summary.json", "summary"), ("plan.json", "plan"))
|
|
407
|
+
if (rdir / OUT_DIR / name).is_file()]
|
|
408
|
+
artifacts += [ev.artifact(rdir / c["log"], rdir, "server_log") for c in result["cells"]
|
|
409
|
+
if (rdir / c["log"]).is_file()]
|
|
410
|
+
cells = [{k: c.get(k) for k in ("round", "position", "label", "kind", "endpoint_kind", "status", "ready", "clip",
|
|
411
|
+
"stop", "gpu_memory", "env", "log", "metrics_source", "claim_valid",
|
|
412
|
+
"violations", "cache_allowed_clips", "t_start", "t_end", "detail")}
|
|
413
|
+
for c in result["cells"]]
|
|
414
|
+
record = ev.build(call, kind=bc.KIND, claim_class="observed", summary=line, identity_=ident, metrics=metrics,
|
|
415
|
+
artifacts=artifacts, missing=missing, limitations=limitations,
|
|
416
|
+
data={"source": "bench rounds clip", "run_dir": str(rdir), "plan": plan, "cells": cells,
|
|
417
|
+
"rows": summary["rows"], "definitions": summary["definitions"],
|
|
418
|
+
"metric_claim_classes": {"clip_ms": "observed (median over rounds of bench clip's "
|
|
419
|
+
"client timer)",
|
|
420
|
+
"clips_per_hour": "derived", "<metric>_ratio_to_best": "derived"},
|
|
421
|
+
"failures": summary.get("failures") or [],
|
|
422
|
+
"clip_evidence": [c["clip"]["evidence_id"] for c in result["cells"]
|
|
423
|
+
if (c.get("clip") or {}).get("evidence_id")],
|
|
424
|
+
"gpu_processes_at_start": len(others), "markdown": result["markdown"]})
|
|
425
|
+
rec = ev.Store().save(record)
|
|
426
|
+
copy = rdir / "evidence" / f"{rec['evidence_id']}.json"
|
|
427
|
+
copy.parent.mkdir(parents=True, exist_ok=True)
|
|
428
|
+
copy.write_text(json.dumps(rec, indent=2, ensure_ascii=False) + "\n")
|
|
429
|
+
return rec
|