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,393 @@
1
+ """Client-timed image benchmark with round-level evidence and strict request identity.
2
+
3
+ Fixtures are JSONL objects {id,prompt,width,height,steps,guidance_scale,settings?}.
4
+ settings carries all other generation choices (negative_prompt, scheduler,
5
+ flow_shift, uncond_skip_layers, precision, etc.). Unknown top-level keys fail
6
+ closed. Timed images use fresh round seeds. Warm-ups cover every shape/settings
7
+ combination using separate prompts and seeds. No semantic quality pass is inferred
8
+ from a valid PNG or from the server's effective-settings self-report.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import math
15
+ import re
16
+ import statistics
17
+ import time
18
+ from pathlib import Path
19
+
20
+ from inferencekit import evidence as ev, stats
21
+ from inferencekit.canonical import canonical_json, file_sha256
22
+ from inferencekit.ops.bench_serve import _record_call
23
+ from inferencekit.registry import Arg, OpError, operation, utc_now
24
+ from inferencekit.serve import image_client as ic
25
+ from inferencekit.serve.client import Endpoint, api_key_from_env
26
+
27
+ KIND = "image_benchmark"
28
+ SEED_POLICIES = ("per_round", "fixed", "list")
29
+ _ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.+-]{0,127}$")
30
+ _LABEL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.+-]{0,63}$")
31
+ _ROW_KEYS = frozenset({"id", "task", "prompt", "width", "height", "steps", "guidance_scale", "settings"})
32
+ _RESERVED = frozenset({"id", "task", "model", "prompt", "size", "width", "height", "steps", "seed", "guidance_scale"})
33
+ _PIN_KEYS = ("model_revision", "dtype", "quant")
34
+ MAX_FIXTURE_BYTES = 4 * 1024 * 1024
35
+ MAX_FIXTURES = 1000
36
+ MAX_REQUESTS = 10000
37
+
38
+ ARGS = [
39
+ Arg("base_url", "string", "Image HTTP server root (never stored as an address).", required=True, sensitive=True),
40
+ Arg("prompts", "string", "Image fixtures JSONL: id,prompt,width,height,steps,guidance_scale,settings?.", required=True),
41
+ Arg("model", "string", "Pinned model identifier sent to the engine.", required=True),
42
+ Arg("model_revision", "string", "Checkpoint revision, sent and required in effective settings."),
43
+ Arg("dtype", "string", "Weight/compute precision label, sent and required in effective settings."),
44
+ Arg("quant", "string", "Quantization label, sent and required in effective settings."),
45
+ Arg("label", "string", "Engine configuration label.", default="image_http"),
46
+ Arg("engine_version", "string", "Pinned engine version or commit."),
47
+ Arg("gpu_name", "string", "Exact GPU model (required; image service may be remote).", required=True),
48
+ Arg("gpu_count", "integer", "Number of GPUs serving images.", default=1),
49
+ Arg("rounds", "integer", "Timed passes over the image fixtures.", default=3),
50
+ Arg("first_round", "integer", "First round index; defaults to zero, or previous last + 1 with extend."),
51
+ Arg("warmup", "integer", "Untimed images per distinct request shape/settings combination.", default=1),
52
+ Arg("allow_cache", "string", "Permit only reuse of the fixed empty-negative embedding; every fixture must "
53
+ "pin negative_prompt to an empty string. Positive-prompt and generated-output reuse remain forbidden.",
54
+ default="none", choices=("none", "fixed_negative_embedding")),
55
+ Arg("seed_policy", "string", "per_round=fresh paired round seeds; fixed=quality checks; list=explicit seeds.",
56
+ default="per_round", choices=SEED_POLICIES),
57
+ Arg("seed_base", "integer", "Base seed for the deterministic round-seed policy.", default=0),
58
+ Arg("seeds", "array", "Explicit list of seeds indexed by round, for seed_policy=list.", items="integer"),
59
+ Arg("timeout_s", "number", "Finite whole-response deadline per image, seconds.", default=ic.DEFAULT_TIMEOUT_S),
60
+ Arg("resamples", "integer", "Bootstrap resamples over complete rounds.", default=stats.DEFAULT_RESAMPLES),
61
+ Arg("save_outputs", "string", "all saves PNGs; hash_only saves output hashes.", default="all",
62
+ choices=("all", "hash_only")),
63
+ Arg("api_key_env", "string", "Name of the environment variable holding the API key (never its value)."),
64
+ Arg("run_dir", "string", "Optional run directory receiving inputs, outputs and metrics."),
65
+ Arg("extend", "string", "Previous evidence id for the same exact configuration; append new rounds."),
66
+ ]
67
+
68
+
69
+ def _finite_json(value, depth=0) -> bool:
70
+ if depth > 32:
71
+ return False
72
+ if isinstance(value, float):
73
+ return math.isfinite(value)
74
+ if isinstance(value, dict):
75
+ return all(isinstance(k, str) and _finite_json(v, depth + 1) for k, v in value.items())
76
+ if isinstance(value, list):
77
+ return all(_finite_json(v, depth + 1) for v in value)
78
+ return value is None or isinstance(value, (str, int, bool))
79
+
80
+
81
+ def load_image_set(path) -> list[dict]:
82
+ try:
83
+ with Path(path).open("rb") as source:
84
+ raw = source.read(MAX_FIXTURE_BYTES + 1)
85
+ except OSError:
86
+ raise OpError("image fixture file is not readable") from None
87
+ if len(raw) > MAX_FIXTURE_BYTES:
88
+ raise OpError("image fixture file exceeds byte bound")
89
+ rows, seen = [], set()
90
+ try:
91
+ lines = raw.decode("utf-8").splitlines()
92
+ except UnicodeDecodeError:
93
+ raise OpError("image fixture file must be UTF-8 JSONL") from None
94
+ for line_no, line in enumerate(lines, 1):
95
+ if not line.strip():
96
+ continue
97
+ try:
98
+ row = json.loads(line)
99
+ except (ValueError, RecursionError):
100
+ raise OpError(f"image fixture line {line_no} is not JSON") from None
101
+ prefix = f"image fixture line {line_no}: "
102
+ if not isinstance(row, dict) or not _finite_json(row):
103
+ raise OpError(prefix + "must be an object with finite JSON values")
104
+ if set(row) - _ROW_KEYS:
105
+ raise OpError(prefix + "unknown top-level settings; put generation settings in settings")
106
+ if not isinstance(row.get("id"), str) or not _ID.fullmatch(row["id"]) or row["id"] in seen:
107
+ raise OpError(prefix + "id must be unique and safe as a filename")
108
+ seen.add(row["id"])
109
+ if row.get("task", "t2i") != "t2i":
110
+ raise OpError(prefix + "task must be t2i")
111
+ if not isinstance(row.get("prompt"), str) or not row["prompt"].strip():
112
+ raise OpError(prefix + "prompt must be non-empty text")
113
+ for key in ("width", "height", "steps"):
114
+ if type(row.get(key)) is not int or not 1 <= row[key] <= 16384:
115
+ raise OpError(prefix + key + " must be an integer in [1,16384]")
116
+ if row["width"] * row["height"] > ic.MAX_PIXELS:
117
+ raise OpError(prefix + "image exceeds pixel bound")
118
+ scale = row.get("guidance_scale")
119
+ if isinstance(scale, bool) or not isinstance(scale, (int, float)) or not 0 <= scale <= 1000:
120
+ raise OpError(prefix + "guidance_scale must be finite and in [0,1000]")
121
+ settings = row.get("settings", {})
122
+ if not isinstance(settings, dict) or set(settings) & _RESERVED:
123
+ raise OpError(prefix + "settings must be an object without required request field overrides")
124
+ if ev.redact(settings) != settings:
125
+ raise OpError(prefix + "settings must not contain credentials or secret-looking fields")
126
+ rows.append({**row, "task": "t2i", "settings": settings})
127
+ if len(rows) > MAX_FIXTURES:
128
+ raise OpError("too many image fixtures")
129
+ if not rows:
130
+ raise OpError("image fixture file contains no requests")
131
+ return rows
132
+
133
+
134
+ def round_seed(policy, base, seeds, index):
135
+ if policy == "fixed":
136
+ return base
137
+ if policy == "list":
138
+ return seeds[index]
139
+ return int.from_bytes(hashlib.sha256(f"inferencekit-image-seed:{base}:{index}".encode()).digest()[:4], "big") & 0x7FFFFFFF
140
+
141
+
142
+ def request_set(rows, model, pins, policy, base, seeds, allow_cache="none"):
143
+ doc = {"items": rows, "model": model, "settings": pins,
144
+ "seed_policy": {"name": policy, "base": None if policy == "list" else base,
145
+ "seeds": seeds if policy == "list" else None}}
146
+ if allow_cache != "none":
147
+ doc["cache_policy"] = allow_cache
148
+ return {"sha256": hashlib.sha256(canonical_json(doc).encode()).hexdigest(), "n": len(rows)}
149
+
150
+
151
+ def image_task_set(rows, model, revision, policy, base, seeds, allow_cache="none"):
152
+ """Bind an image task while allowing steps and numerical format to vary.
153
+
154
+ All other fixture fields (including prompts, dimensions and settings) remain
155
+ part of the identity. This is only for explicitly labelled tradeoff timing.
156
+ """
157
+ items = []
158
+ for row in rows:
159
+ item = {k: v for k, v in row.items() if k != "steps"}
160
+ item["settings"] = {k: v for k, v in row.get("settings", {}).items()
161
+ if k not in ("dtype", "quant")}
162
+ items.append(item)
163
+ doc = {"items": items, "model": model, "revision": revision,
164
+ "seed_policy": {"name": policy, "base": None if policy == "list" else base,
165
+ "seeds": seeds if policy == "list" else None},
166
+ "cache_policy": allow_cache}
167
+ return {"sha256": hashlib.sha256(canonical_json(doc).encode()).hexdigest(), "n": len(items)}
168
+
169
+
170
+ def _request(row, model, seed, pins, prompt=None):
171
+ settings = {**row["settings"]}
172
+ for key, value in pins.items():
173
+ if key in settings and settings[key] != value:
174
+ raise OpError(f"fixture setting {key} conflicts with the pinned operation argument")
175
+ settings[key] = value
176
+ return ic.ImageRequest(row["id"], model, prompt if prompt is not None else row["prompt"],
177
+ row["width"], row["height"], row["steps"], row["guidance_scale"], seed, settings)
178
+
179
+
180
+ def _put(staging, run, relative, data):
181
+ for root in [staging] + ([run] if run else []):
182
+ path = root / relative
183
+ path.parent.mkdir(parents=True, exist_ok=True)
184
+ path.write_bytes(data)
185
+
186
+
187
+ def _previous(store, ref, rcall, ident):
188
+ previous = store.load(ref)
189
+ if previous.get("kind") != KIND or ev.integrity_problem(previous):
190
+ raise OpError("extend needs an intact image benchmark record")
191
+ if previous["identity"] != ident:
192
+ raise OpError("extend needs identical workload, model, engine version and hardware")
193
+ old_args = previous.get("data", {}).get("request", {}).get("args", {})
194
+ for key in ("base_url", "save_outputs", "seed_policy", "seed_base", "seeds", "timeout_s"):
195
+ if old_args.get(key) != ev.redact(rcall.request()["args"]).get(key):
196
+ raise OpError(f"extend configuration differs: {key}")
197
+ artifact = next((a for a in previous.get("artifacts", []) if a.get("role") == "requests"), None)
198
+ if not artifact:
199
+ raise OpError("extend needs the previous request artifact")
200
+ path = store.run_dir(previous["evidence_id"]) / artifact["path"]
201
+ if not path.is_file() or file_sha256(path)[0] != artifact["sha256"]:
202
+ raise OpError("extend request artifact is missing or changed")
203
+ rows = [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
204
+ return previous, rows
205
+
206
+
207
+ @operation("bench_image", ("bench", "image"), "Client-timed image generation with PNG validation, paired round "
208
+ "seeds, effective-setting/cache gates and image artifacts; semantic quality remains UNPROVEN.", ARGS)
209
+ def bench_image(call, base_url, prompts, model, model_revision, dtype, quant, label, engine_version, gpu_name,
210
+ gpu_count, rounds, first_round, warmup, seed_policy, seed_base, seeds, timeout_s, resamples,
211
+ save_outputs, api_key_env, run_dir, extend, allow_cache="none"):
212
+ if not _LABEL.fullmatch(label or ""):
213
+ raise OpError("image label must be a safe 1-64 character filename")
214
+ if not model.strip() or not gpu_name.strip():
215
+ raise OpError("model and gpu_name must be non-empty")
216
+ if not 1 <= rounds <= 10000 or not 0 <= warmup <= 100 or not 1 <= gpu_count <= 1024:
217
+ raise OpError("rounds, warmup or gpu_count is outside its finite bound")
218
+ if not 1 <= resamples <= 100000 or first_round is not None and not 0 <= first_round <= 1000000:
219
+ raise OpError("resamples or first_round is outside its finite bound")
220
+ if not 0 <= seed_base <= 0x7FFFFFFF:
221
+ raise OpError("seed_base must be a 31-bit nonnegative integer")
222
+ if seed_policy == "list":
223
+ if not seeds or any(type(s) is not int or not 0 <= s <= 0x7FFFFFFF for s in seeds):
224
+ raise OpError("list seed policy requires 31-bit nonnegative seeds")
225
+ elif seeds is not None:
226
+ raise OpError("seeds is only valid with seed_policy=list")
227
+ fixtures = load_image_set(prompts)
228
+ if allow_cache != "none" and any(row["settings"].get("negative_prompt") != "" for row in fixtures):
229
+ raise OpError("fixed_negative_embedding reuse requires negative_prompt='' in every fixture's settings")
230
+ if len(fixtures) * (rounds + warmup) > MAX_REQUESTS:
231
+ raise OpError("image run exceeds request count bound")
232
+ pins = {key: value for key, value in zip(_PIN_KEYS, (model_revision, dtype, quant)) if value is not None}
233
+ # Validate conflicts before any network operation, including warm-ups.
234
+ for row in fixtures:
235
+ _request(row, model, seed_base, pins)
236
+ policy = {"name": seed_policy, "base": None if seed_policy == "list" else seed_base}
237
+ rset = request_set(fixtures, model, pins, seed_policy, seed_base, seeds, allow_cache)
238
+ uniform = {key: fixtures[0][key] if all(row[key] == fixtures[0][key] for row in fixtures) else None
239
+ for key in ("width", "height", "steps", "guidance_scale")}
240
+ ident = ev.identity(gpu={"name": gpu_name, "count": gpu_count},
241
+ engine={"name": label, "version": engine_version},
242
+ model={"id": model, "revision": model_revision, "dtype": dtype, "quant": quant},
243
+ workload={"mode": "single", "batch": 1, "task": "t2i", "endpoint_kind": "image_http",
244
+ "prompt_set": {"name": "image fixtures", "sha256": hashlib.sha256(
245
+ canonical_json(fixtures).encode()).hexdigest()},
246
+ "image_task_set": image_task_set(fixtures, model, model_revision, seed_policy,
247
+ seed_base, seeds, allow_cache),
248
+ "request_set": rset, "seed_policy": policy, **uniform})
249
+ try:
250
+ ep = Endpoint.parse(base_url, api_key_from_env(api_key_env))
251
+ backend = ic.ImageHTTP(ep, timeout_s=timeout_s)
252
+ except ValueError:
253
+ raise OpError("invalid image endpoint or finite timeout") from None
254
+ rcall, key_persistent = _record_call(call, ep)
255
+ store, run = ev.Store(), Path(run_dir) if run_dir else None
256
+ previous, old_rows = _previous(store, extend, rcall, ident) if extend else (None, [])
257
+ old_rounds = (previous or {}).get("data", {}).get("rounds", [])
258
+ done = max((r["index"] for r in old_rounds), default=-1)
259
+ start = done + 1 if first_round is None else first_round
260
+ if extend and start != done + 1:
261
+ raise OpError("extend first_round must be the previous last round + 1, without overlaps or gaps")
262
+ last = start + rounds - 1
263
+ if seed_policy == "list" and len(seeds) <= last:
264
+ raise OpError("seed list does not cover the requested round indices")
265
+ round_seeds = [round_seed(seed_policy, seed_base, seeds, r) for r in range(start, last + 1)]
266
+ if seed_policy != "fixed" and len(set(round_seeds + [r["seed"] for r in old_rounds])) != len(round_seeds) + len(old_rounds):
267
+ raise OpError("timed round seeds must be unique (use fixed only for labelled quality checks)")
268
+ with store.staging_dir() as staging:
269
+ warm_rows, timed, rounds_table, output_index = [], list(old_rows), list(old_rounds), {}
270
+ shapes, taken = set(), {row["prompt"] for row in fixtures}
271
+ for row in fixtures:
272
+ shape = canonical_json({k: v for k, v in row.items() if k not in ("id", "prompt")})
273
+ if shape in shapes:
274
+ continue
275
+ shapes.add(shape)
276
+ for number in range(warmup):
277
+ warm_seed = int.from_bytes(hashlib.sha256(f"image-warm:{seed_base}:{len(shapes)}:{number}".encode()).digest()[:4], "big") & 0x7FFFFFFF
278
+ # Avoid both seed and prompt collisions with the timed workload.
279
+ while warm_seed in round_seeds:
280
+ warm_seed = (warm_seed + 1) & 0x7FFFFFFF
281
+ prompt = row["prompt"] + " (inferencekit warm-up)"
282
+ while prompt in taken:
283
+ prompt += " warm-up"
284
+ res = backend.generate(_request(row, model, warm_seed, pins, prompt))
285
+ warm_rows.append({"id": row["id"], "ok": res.ok, "error": res.error,
286
+ "e2e_ms": res.e2e_ms, "seed": warm_seed})
287
+ for index, seed in zip(range(start, last + 1), round_seeds):
288
+ t_start, started_at, current = time.time(), utc_now(), []
289
+ for row in fixtures:
290
+ req = _request(row, model, seed, pins)
291
+ res = backend.generate(req)
292
+ key = f"r{index}/{row['id']}"
293
+ body = req.body()
294
+ _put(staging, run, f"inputs/{label}/{key}.json", (json.dumps(ev.redact(body), ensure_ascii=False) + "\n").encode())
295
+ sha = hashlib.sha256(res.image).hexdigest() if res.image is not None else None
296
+ pixel_sha = res.pixel_sha256
297
+ if res.image is not None and save_outputs == "all":
298
+ path = f"outputs/{label}/{key}.png"
299
+ _put(staging, run, path, res.image)
300
+ output_index[path] = sha
301
+ entry = {"id": row["id"], "round": index, "seed": seed, "ok": res.ok, "error": res.error,
302
+ "e2e_ms": res.e2e_ms, "t_send_ns": res.t_send_ns, "t_done_ns": res.t_done_ns,
303
+ "t_body_ns": res.t_body_ns,
304
+ "request_sha256": hashlib.sha256(canonical_json(body).encode()).hexdigest(),
305
+ "out_sha256": sha, "pixel_sha256": pixel_sha,
306
+ "image_bytes": len(res.image) if res.image is not None else None,
307
+ "effective": res.effective, "effective_mismatch": res.effective_mismatch,
308
+ "cache": res.cache, "width": res.width, "height": res.height}
309
+ current.append(entry)
310
+ timed.append(entry)
311
+ failures = sum(not row["ok"] for row in current)
312
+ mean = statistics.mean(row["e2e_ms"] for row in current) if not failures else None
313
+ rounds_table.append({"index": index, "round": index, "started_at": started_at, "seed": seed,
314
+ "t_start": t_start, "t_end": time.time(), "n_failed": failures,
315
+ "n_images": len(current), "image_ms": mean,
316
+ "images_per_hour": 3_600_000 / mean if mean and mean > 0 else None})
317
+ violations = []
318
+ for row in timed:
319
+ if not row["ok"]:
320
+ violations.append(f"{row['id']} r{row['round']}: image request failed")
321
+ if row["effective_mismatch"]:
322
+ violations.append(f"{row['id']} r{row['round']}: effective settings differ or are missing")
323
+ forbidden_cache = [name for name in row["cache"] or []
324
+ if allow_cache == "none" or name != "fixed_negative_embedding"]
325
+ if forbidden_cache:
326
+ violations.append(f"{row['id']} r{row['round']}: engine reports forbidden cached stages")
327
+ hashes = {}
328
+ for row in timed:
329
+ if row["ok"] and row["pixel_sha256"]:
330
+ hashes.setdefault(row["pixel_sha256"], set()).add(row["request_sha256"])
331
+ if any(len(requests) > 1 for requests in hashes.values()):
332
+ violations.append("identical decoded pixel data for different requests: cached output or ignored settings")
333
+ if any(not row["ok"] for row in warm_rows):
334
+ violations.append("one or more warm-up requests failed")
335
+ request_path = staging / "requests.jsonl"
336
+ request_path.write_text("".join(json.dumps(ev.redact(row), ensure_ascii=False) + "\n" for row in timed))
337
+ artifacts = [ev.artifact(request_path, staging, "requests")]
338
+ if output_index:
339
+ output_path = staging / "outputs" / label / "index.json"
340
+ output_path.write_text(json.dumps({"files": output_index}, indent=2) + "\n")
341
+ artifacts.append(ev.artifact(output_path, staging, "outputs_index"))
342
+ metrics = []
343
+ for name, unit in (("image_ms", "ms"), ("images_per_hour", "images/h")):
344
+ values = [row[name] for row in rounds_table if row[name] is not None]
345
+ ci = stats.bootstrap_ci(values, resamples=resamples)[50]
346
+ metrics.append(ev.metric(name, statistics.median(values) if values else None,
347
+ unit, "median", n=len(values), ci95=ci))
348
+ limitations = ["semantic image quality UNPROVEN: PNG structure and server self-reports do not assess quality",
349
+ "effective settings and cache reports are engine self-reports; independent execution proof is required"]
350
+ if allow_cache != "none":
351
+ limitations.append("Fixed empty-negative embedding reuse is permitted for every compared engine; "
352
+ "verify its fixed model/settings and actual reused stage independently. "
353
+ "Positive conditioning, denoising, VAE and output reuse remain forbidden.")
354
+ if not warmup:
355
+ limitations.append("warmup=0: timed requests may include compilation or graph capture")
356
+ if seed_policy == "fixed":
357
+ limitations.append("fixed seed policy repeats requests: output-cache absence is UNPROVEN")
358
+ if not ep.is_local():
359
+ limitations.append("client timer includes network transfer from a remote image engine")
360
+ if not key_persistent:
361
+ limitations.append("endpoint tag persists only for this process; later extend may be unavailable")
362
+ if save_outputs == "hash_only":
363
+ limitations.append("only output hashes saved; independent image quality checks need saved image bytes")
364
+ if not model_revision or not dtype or not engine_version:
365
+ limitations.append("model revision, precision or engine version is not fully pinned")
366
+ if previous:
367
+ violations = list(dict.fromkeys(previous["data"].get("claim_validity", {}).get("violations", []) + violations))
368
+ validity = {"valid": not violations and seed_policy != "fixed", "violations": violations,
369
+ "cache_policy": allow_cache,
370
+ "fixed_negative_reused_requests": sum("fixed_negative_embedding" in (row["cache"] or [])
371
+ for row in timed),
372
+ "warnings": [], "checked": ["decoded PNG dimensions/structure", "effective settings (self-reported)",
373
+ "cache (self-reported)", "unique decoded pixel data"]}
374
+ data = {"source": "bench image", "label": label, "rounds": rounds_table,
375
+ "cache_policy": allow_cache,
376
+ "warmup": {"per_shape": warmup, "requests": warm_rows}, "claim_validity": validity,
377
+ "quality": {"verdict": "UNPROVEN", "reason": "no semantic image quality evaluation supplied"},
378
+ "timer": "client perf_counter_ns: before connection through HTTP response, PNG decoding and validation",
379
+ "metric_claim_classes": {"image_ms": "observed (client timer)", "images_per_hour": "derived"},
380
+ "definitions": {"image_ms": "mean per-image latency within each complete round; median across rounds",
381
+ "images_per_hour": "3,600,000 / round mean image_ms (one request at a time)"},
382
+ "extends": previous["evidence_id"] if previous else None,
383
+ "outputs_from": ([previous["evidence_id"]] + previous["data"].get("outputs_from", [])) if previous else [],
384
+ "save_outputs": save_outputs}
385
+ if run:
386
+ _put(staging, run, f"metrics/{label}.image.json", (json.dumps(data, indent=2) + "\n").encode())
387
+ record = ev.build(rcall, kind=KIND, claim_class="observed",
388
+ summary=f"{label}: {len(rounds_table)} image rounds x {len(fixtures)} fixtures, client-timed; semantic quality UNPROVEN",
389
+ identity_=ident, metrics=metrics, artifacts=artifacts,
390
+ missing=["semantic image quality"] + (["complete successful image rounds"] if not any(
391
+ row["image_ms"] is not None for row in rounds_table) else []),
392
+ limitations=limitations, data=data)
393
+ return store.save(record, staging)