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.
Files changed (104) hide show
  1. inferencekit/__init__.py +9 -0
  2. inferencekit/__main__.py +5 -0
  3. inferencekit/analysis/__init__.py +1 -0
  4. inferencekit/analysis/ncu_csv.py +284 -0
  5. inferencekit/analysis/nsys_sqlite.py +518 -0
  6. inferencekit/canonical.py +104 -0
  7. inferencekit/checkpoint.py +396 -0
  8. inferencekit/cli.py +274 -0
  9. inferencekit/cuda/bw_peak.cu +252 -0
  10. inferencekit/cuda/torch_bw.py +60 -0
  11. inferencekit/engines/__init__.py +7 -0
  12. inferencekit/engines/adapters.py +190 -0
  13. inferencekit/engines/clip/__init__.py +13 -0
  14. inferencekit/engines/clip/comfyui.py +749 -0
  15. inferencekit/engines/clip/diffusers_ltx.py +601 -0
  16. inferencekit/engines/clip/noise.py +46 -0
  17. inferencekit/engines/clip/official_ltx.py +316 -0
  18. inferencekit/engines/clip/server.py +542 -0
  19. inferencekit/engines/clip/stepdump.py +182 -0
  20. inferencekit/engines/gates.py +94 -0
  21. inferencekit/engines/sglang_worker.py +86 -0
  22. inferencekit/engines/vllm_worker.py +129 -0
  23. inferencekit/evidence.py +427 -0
  24. inferencekit/gpuinfo.py +93 -0
  25. inferencekit/hardware.py +135 -0
  26. inferencekit/mcp_server.py +178 -0
  27. inferencekit/ops/__init__.py +30 -0
  28. inferencekit/ops/bench.py +184 -0
  29. inferencekit/ops/bench_audio.py +384 -0
  30. inferencekit/ops/bench_clip.py +819 -0
  31. inferencekit/ops/bench_image.py +393 -0
  32. inferencekit/ops/bench_rounds.py +297 -0
  33. inferencekit/ops/bench_serve.py +866 -0
  34. inferencekit/ops/ceiling.py +135 -0
  35. inferencekit/ops/compare.py +378 -0
  36. inferencekit/ops/correctness.py +153 -0
  37. inferencekit/ops/doctor.py +486 -0
  38. inferencekit/ops/gates.py +1772 -0
  39. inferencekit/ops/gates_clip.py +930 -0
  40. inferencekit/ops/intake.py +1321 -0
  41. inferencekit/ops/ledger.py +749 -0
  42. inferencekit/ops/manifest.py +309 -0
  43. inferencekit/ops/ncu.py +183 -0
  44. inferencekit/ops/nsys.py +224 -0
  45. inferencekit/ops/prompts.py +316 -0
  46. inferencekit/ops/quality.py +2161 -0
  47. inferencekit/ops/quality_clip.py +690 -0
  48. inferencekit/ops/quality_clip_steps.py +341 -0
  49. inferencekit/ops/report.py +317 -0
  50. inferencekit/ops/review.py +119 -0
  51. inferencekit/ops/rocprof.py +89 -0
  52. inferencekit/ops/roofline.py +392 -0
  53. inferencekit/ops/store.py +61 -0
  54. inferencekit/proc.py +172 -0
  55. inferencekit/prompts/ATTRIBUTION.md +56 -0
  56. inferencekit/prompts/__init__.py +167 -0
  57. inferencekit/prompts/build.py +518 -0
  58. inferencekit/prompts/dev-load.jsonl.gz +0 -0
  59. inferencekit/prompts/dev-load.manifest.json +212 -0
  60. inferencekit/prompts/dev.jsonl.gz +0 -0
  61. inferencekit/prompts/dev.manifest.json +214 -0
  62. inferencekit/prompts/exclusions.json +54 -0
  63. inferencekit/quality/__init__.py +23 -0
  64. inferencekit/quality/client.py +66 -0
  65. inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
  66. inferencekit/quality/fake.py +329 -0
  67. inferencekit/quality/gsm8k.py +224 -0
  68. inferencekit/quality/logprobs.py +106 -0
  69. inferencekit/quality/metrics.py +235 -0
  70. inferencekit/quality/niah.py +63 -0
  71. inferencekit/quality/parquet.py +296 -0
  72. inferencekit/quality/sampling.py +262 -0
  73. inferencekit/quality/step_replay.py +217 -0
  74. inferencekit/quality/video_metrics.py +304 -0
  75. inferencekit/registry.py +206 -0
  76. inferencekit/review/__init__.py +6 -0
  77. inferencekit/review/diff.py +163 -0
  78. inferencekit/review/media.py +181 -0
  79. inferencekit/review/page.py +920 -0
  80. inferencekit/review/rundir.py +844 -0
  81. inferencekit/roofline_headers.py +504 -0
  82. inferencekit/schemas/evidence.v1.json +130 -0
  83. inferencekit/schemas/evidence.v2.json +499 -0
  84. inferencekit/schemas/intake.v1.json +276 -0
  85. inferencekit/schemas/run-manifest.v1.json +118 -0
  86. inferencekit/serve/__init__.py +8 -0
  87. inferencekit/serve/audio_client.py +330 -0
  88. inferencekit/serve/client.py +446 -0
  89. inferencekit/serve/clip_client.py +351 -0
  90. inferencekit/serve/clip_rounds.py +429 -0
  91. inferencekit/serve/image_client.py +373 -0
  92. inferencekit/serve/loadgen.py +136 -0
  93. inferencekit/serve/metrics.py +203 -0
  94. inferencekit/serve/profiles.py +105 -0
  95. inferencekit/serve/prompts_io.py +125 -0
  96. inferencekit/serve/rounds.py +1111 -0
  97. inferencekit/serve/servers.py +897 -0
  98. inferencekit/stats.py +249 -0
  99. inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
  100. inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
  101. inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
  102. inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
  103. inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
  104. 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