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,930 @@
|
|
|
1
|
+
"""gates clip: anti-cheat gates for a video clip speed claim.
|
|
2
|
+
|
|
3
|
+
A clip time is only worth publishing if the engine made the clip it was asked for. These gates probe a
|
|
4
|
+
live clip engine (the clip HTTP contract, or ComfyUI through its adapter: serve/clip_client.py) and,
|
|
5
|
+
optionally, check a ``bench clip`` record's timed clips. Each gate ends PASS, FAIL, UNPROVEN or NOT RUN
|
|
6
|
+
with the probes and numbers that decided it:
|
|
7
|
+
|
|
8
|
+
steps_honoured the engine's own step counter (effective.steps_run) equals the steps asked on every
|
|
9
|
+
probe; a clip at N steps differs from the same request at N/2 steps; and N steps take
|
|
10
|
+
measurably longer than N/2 (median e2e ratio >= 1.10 in >= 2/3 of the pairs; FAIL below
|
|
11
|
+
1.03: the step count changes nothing). An engine that runs a fixed fraction of whatever
|
|
12
|
+
it is asked scales the same way; quality clip (outputs vs the reference at the same
|
|
13
|
+
steps) catches that, not this gate.
|
|
14
|
+
cfg_honoured the same request with another guidance scale (1.0, or +3 when the clip set's is <= 1.5)
|
|
15
|
+
gives a different clip. The time ratio is recorded, not judged (an engine may batch the
|
|
16
|
+
two guidance branches).
|
|
17
|
+
resolution_frames every clip is decoded (PyAV in the video venv with --python, else ffprobe): the decoded
|
|
18
|
+
frames' width and height, the decoded frame count, the fps from the decoded timestamps
|
|
19
|
+
and, when audio was asked for, the decoded audio duration (within 0.1 s + one frame of
|
|
20
|
+
num_frames / fps) must match the request. What the container header claims is recorded
|
|
21
|
+
beside it but never trusted.
|
|
22
|
+
seed_honoured a different seed (same prompt) gives a different clip, and the same seed repeated stays
|
|
23
|
+
within the A/A floor: FAIL when the different seed is no farther from the clip than the
|
|
24
|
+
repeat is, or when the repeat is more than half as far (the seed is not reproduced).
|
|
25
|
+
prompt_sensitivity a one-word change in the prompt (same seed) gives a different clip.
|
|
26
|
+
no_cache replaying a request is not > 10% faster than its first time (median over pairs and in
|
|
27
|
+
>= 2/3 of the pairs, as gates run's caching gate); different requests never get
|
|
28
|
+
byte-identical clips; no probe reports a stage served from a cache. With
|
|
29
|
+
--bench-evidence, the same holds for every timed clip of that bench clip run.
|
|
30
|
+
|
|
31
|
+
--bench-evidence: the record must verify (its id matches its content, its requests.jsonl matches the
|
|
32
|
+
sha256 it lists). Its timed clips are found in its own store directory, in those of the records it
|
|
33
|
+
extends (``data.outputs_from``: bench clip --extend and bench rounds' clip mode keep each round's clips
|
|
34
|
+
with the record that timed it) and in the run directory (``outputs/<label>/r<round>/<id>.<fmt>``), and
|
|
35
|
+
each file must have the sha256 its row records: one that does not is not the clip that was timed (FAIL);
|
|
36
|
+
a timed clip with no file anywhere leaves resolution_frames UNPROVEN. ``--max-decode N`` decodes a random
|
|
37
|
+
N of them, drawn from the probe seed.
|
|
38
|
+
|
|
39
|
+
"Different clip" is judged on decoded pixels when --python names a venv with PyAV and numpy: the mean
|
|
40
|
+
absolute difference (MAD, 0-255 levels) between the clips must exceed max(3 x the same-seed repeat's MAD,
|
|
41
|
+
0.5). Byte-identical clips are always a FAIL; without --python, clips whose bytes differ leave the gate
|
|
42
|
+
UNPROVEN (bytes can differ while the pictures do not).
|
|
43
|
+
|
|
44
|
+
The probes are one randomly interleaved sequence after an untimed warm-up: each repeat follows its first
|
|
45
|
+
request with other probes between them, seeds are drawn fresh for every run (and recorded), and the
|
|
46
|
+
one-word change is drawn from a pool, so no probe is predictable from this code. Probes use one clip of
|
|
47
|
+
the clip set (``--clip-id``, default the first): its task, shape, steps and guidance. A refused or failed
|
|
48
|
+
probe is evidence: it leaves its gates UNPROVEN, never PASS. Only when the engine answers no probe at all
|
|
49
|
+
are the live gates NOT RUN.
|
|
50
|
+
|
|
51
|
+
No probe follows a request with the same prompt or image file, so an engine never has a legitimate
|
|
52
|
+
encoding to reuse (ComfyUI's node cache keeps the last run's outputs: a text encoder whose prompt did not
|
|
53
|
+
change is skipped, and no_cache fails on any costly cached stage but the fixed negative prompt's). Each
|
|
54
|
+
request/replay pair is a group with its own prompt (the clip's for the first, then the clip's plus
|
|
55
|
+
" (probe N)") and, for i2v, its own image file (the same picture with a comment chunk: image_variant); the
|
|
56
|
+
probes compared with each other are in one group (N/2 steps with its pair, the other seed with the first,
|
|
57
|
+
the other guidance with the last), the one-word change has an image file of its own, and the warm-up has
|
|
58
|
+
a prompt and image file no probe has. With --pairs 1 the first group is too big to keep its probes apart:
|
|
59
|
+
those neighbours are recorded and named in no_cache.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
import dataclasses
|
|
65
|
+
import hashlib
|
|
66
|
+
import json
|
|
67
|
+
import random
|
|
68
|
+
import secrets
|
|
69
|
+
import time
|
|
70
|
+
import zlib
|
|
71
|
+
from pathlib import Path
|
|
72
|
+
|
|
73
|
+
from inferencekit import evidence as ev
|
|
74
|
+
from inferencekit.canonical import file_sha256
|
|
75
|
+
from inferencekit.ops import bench_clip as bc
|
|
76
|
+
from inferencekit.ops.bench_serve import _record_call
|
|
77
|
+
from inferencekit.ops.gates import _gate, _median, overall
|
|
78
|
+
from inferencekit.ops.quality_clip import probe_clips, run_worker
|
|
79
|
+
from inferencekit.proc import which
|
|
80
|
+
from inferencekit.registry import Arg, OpError, operation
|
|
81
|
+
from inferencekit.serve import clip_client as cc
|
|
82
|
+
from inferencekit.serve.client import Endpoint, api_key_from_env
|
|
83
|
+
|
|
84
|
+
GATES = ("steps_honoured", "cfg_honoured", "resolution_frames", "seed_honoured", "prompt_sensitivity", "no_cache")
|
|
85
|
+
LIVE = {"steps_honoured", "cfg_honoured", "seed_honoured", "prompt_sensitivity"}
|
|
86
|
+
# "clip_gates" is a newer evidence kind; on a store without it the record falls back to "gates".
|
|
87
|
+
KIND = "clip_gates" if "clip_gates" in ev.KINDS else "gates"
|
|
88
|
+
FORMAT = "inferencekit.gates.clip.v1"
|
|
89
|
+
STEP_RATIO_PASS = 1.10 # N steps must take >= 1.10x the time of N/2 ...
|
|
90
|
+
STEP_RATIO_FAIL = 1.03 # ... and below 1.03x the step count plainly changes nothing
|
|
91
|
+
SENSITIVITY = 3.0 # a changed input must move the clip > 3x as far as the same request repeated
|
|
92
|
+
MIN_MAD = 0.5 # ... and by more than half a grey level on average
|
|
93
|
+
REPRO_SHARE = 0.5 # the repeat may be at most half as far as a different seed
|
|
94
|
+
FPS_TOL = 0.01 # relative
|
|
95
|
+
AUDIO_TOL_S = 0.1 # plus one frame
|
|
96
|
+
SWAP_WORDS = ("crimson", "turquoise", "golden", "silver", "violet", "emerald", "amber", "cobalt", "scarlet",
|
|
97
|
+
"ivory", "copper", "indigo", "lavender", "charcoal", "saffron")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --------------------------------------------------------------------------- probes
|
|
101
|
+
|
|
102
|
+
@dataclasses.dataclass
|
|
103
|
+
class Probe:
|
|
104
|
+
key: str
|
|
105
|
+
kind: str # base | replay | half_steps | other_seed | prompt | cfg
|
|
106
|
+
req: cc.ClipRequest
|
|
107
|
+
pair: int | None = None
|
|
108
|
+
meta: dict = dataclasses.field(default_factory=dict)
|
|
109
|
+
vs: str = "base0" # other_seed, prompt, cfg: the probe this one is compared with
|
|
110
|
+
res: cc.ClipResult | None = None
|
|
111
|
+
sha: str | None = None
|
|
112
|
+
path: Path | None = None
|
|
113
|
+
t_start: float | None = None
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def ok(self) -> bool:
|
|
117
|
+
return self.res is not None and self.res.ok and self.sha is not None
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def e2e_ms(self) -> float | None:
|
|
121
|
+
return self.res.e2e_ms if self.ok else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def one_word_change(prompt: str, rng: random.Random) -> tuple[str, dict]:
|
|
125
|
+
"""The prompt with one word replaced by a word from the pool (or one word put in front)."""
|
|
126
|
+
words = prompt.split(" ")
|
|
127
|
+
idx = [i for i, w in enumerate(words) if w.isalpha() and len(w) >= 4]
|
|
128
|
+
if idx:
|
|
129
|
+
i = rng.choice(idx)
|
|
130
|
+
old = words[i]
|
|
131
|
+
new = rng.choice([w for w in SWAP_WORDS if w != old.lower()])
|
|
132
|
+
words[i] = new.capitalize() if old[:1].isupper() else new
|
|
133
|
+
return " ".join(words), {"replaced": old, "with": words[i], "word_index": i}
|
|
134
|
+
new = rng.choice(SWAP_WORDS)
|
|
135
|
+
return f"{new} {prompt}", {"inserted": new}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def alt_guidance(g: float) -> float:
|
|
139
|
+
return 1.0 if g > 1.5 else round(g + 3.0, 6)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
PROBE_SUFFIX = " (probe {n})" # the prompts of probe groups after the first
|
|
143
|
+
PNG_SIG = b"\x89PNG\r\n\x1a\n"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def image_variant(data: bytes, tag: str) -> bytes | None:
|
|
147
|
+
"""The same picture as another file: ``tag`` in a comment that decoders skip (a PNG tEXt chunk after IHDR,
|
|
148
|
+
a JPEG COM segment after SOI and APP0). None for any other format."""
|
|
149
|
+
text = tag.encode("latin-1")
|
|
150
|
+
if data.startswith(PNG_SIG) and data[12:16] == b"IHDR":
|
|
151
|
+
end = 8 + 12 + int.from_bytes(data[8:12], "big") # the signature, then IHDR: length, type, data, crc
|
|
152
|
+
body = b"Comment\x00" + text
|
|
153
|
+
chunk = len(body).to_bytes(4, "big") + b"tEXt" + body + zlib.crc32(b"tEXt" + body).to_bytes(4, "big")
|
|
154
|
+
return data[:end] + chunk + data[end:]
|
|
155
|
+
if data.startswith(b"\xff\xd8\xff"):
|
|
156
|
+
at = 4 + int.from_bytes(data[4:6], "big") if data[2:4] == b"\xff\xe0" else 2 # JFIF's APP0 stays first
|
|
157
|
+
return data[:at] + b"\xff\xfe" + (len(text) + 2).to_bytes(2, "big") + text + data[at:]
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def clash(a: cc.ClipRequest, b: cc.ClipRequest) -> bool:
|
|
162
|
+
"""Requests in a row that share a prompt or an image file: the second could reuse an encoding of the first
|
|
163
|
+
(ComfyUI's node cache keeps the last run's outputs)."""
|
|
164
|
+
return a.prompt == b.prompt or (a.image is not None and a.image == b.image)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def plan_probes(clip: dict, clipset, rng: random.Random, pairs: int) -> list[Probe]:
|
|
168
|
+
"""Probes in their first-seen roles; ``order`` interleaves them.
|
|
169
|
+
|
|
170
|
+
Each request/replay pair is a group with an input of its own: the clip's prompt for group 0, the prompt
|
|
171
|
+
plus " (probe N)" for the others, and for i2v the clip's image file for group 0 and the same picture as
|
|
172
|
+
another file (image_variant) for the others. The probes compared with each other share their group's
|
|
173
|
+
inputs: N/2 steps with its pair, the other seed with group 0, the other guidance with the last group. The
|
|
174
|
+
one-word change gets a variant image of its own. So ``order`` can keep every request from following one
|
|
175
|
+
with the same prompt or image file, and no engine can serve a probe's encoding from the probe before it."""
|
|
176
|
+
seeds: list[int] = []
|
|
177
|
+
while len(seeds) < pairs + 1:
|
|
178
|
+
s = rng.getrandbits(31)
|
|
179
|
+
if s not in seeds:
|
|
180
|
+
seeds.append(s)
|
|
181
|
+
probes = []
|
|
182
|
+
half = clip["steps"] // 2
|
|
183
|
+
first = bc.make_request(clip, seeds[0], clipset)
|
|
184
|
+
prompts = [first.prompt] + [first.prompt + PROBE_SUFFIX.format(n=i + 1) for i in range(1, pairs)]
|
|
185
|
+
for i in range(pairs):
|
|
186
|
+
image = first.image
|
|
187
|
+
if image is not None and i:
|
|
188
|
+
image = image_variant(first.image, f"inferencekit probe group {i}") or first.image
|
|
189
|
+
base = dataclasses.replace(first, id=f"base{i}", seed=seeds[i], prompt=prompts[i], image=image)
|
|
190
|
+
probes.append(Probe(f"base{i}", "base", base, pair=i))
|
|
191
|
+
probes.append(Probe(f"replay{i}", "replay", dataclasses.replace(base, id=f"replay{i}"), pair=i))
|
|
192
|
+
if half >= 1:
|
|
193
|
+
probes.append(Probe(f"half{i}", "half_steps", dataclasses.replace(base, id=f"half{i}", steps=half),
|
|
194
|
+
pair=i, meta={"steps": half}))
|
|
195
|
+
b0, last = probes[0].req, next(p.req for p in probes if p.key == f"base{pairs - 1}")
|
|
196
|
+
probes.append(Probe("seed", "other_seed", dataclasses.replace(b0, id="seed", seed=seeds[pairs])))
|
|
197
|
+
prompt, how = one_word_change(b0.prompt, rng)
|
|
198
|
+
image = (image_variant(b0.image, "inferencekit probe prompt") or b0.image) if b0.image is not None else None
|
|
199
|
+
probes.append(Probe("prompt", "prompt", dataclasses.replace(b0, id="prompt", prompt=prompt, image=image),
|
|
200
|
+
meta=how))
|
|
201
|
+
g = alt_guidance(last.guidance_scale)
|
|
202
|
+
probes.append(Probe("cfg", "cfg", dataclasses.replace(last, id="cfg", guidance_scale=g),
|
|
203
|
+
meta={"guidance_scale": g}, vs=f"base{pairs - 1}"))
|
|
204
|
+
return probes
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def warmup_request(b0: cc.ClipRequest, i: int, seed: int, probes: list[Probe]) -> cc.ClipRequest:
|
|
208
|
+
"""An untimed warm-up at the probe shape with a prompt (and an image file) no probe has."""
|
|
209
|
+
image = (image_variant(b0.image, "inferencekit warm-up") or b0.image) if b0.image is not None else None
|
|
210
|
+
return dataclasses.replace(b0, id=f"warmup{i}", seed=seed, image=image,
|
|
211
|
+
prompt=bc.warmup_prompt(b0.prompt, {p.req.prompt for p in probes}))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def neighbour_clashes(seq: list[Probe]) -> list[list[str]]:
|
|
215
|
+
"""[key, next key] of probes in a row that share a prompt or an image file."""
|
|
216
|
+
return [[a.key, b.key] for a, b in zip(seq, seq[1:]) if clash(a.req, b.req)]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def order(probes: list[Probe], rng: random.Random, tries: int = 200) -> list[Probe]:
|
|
220
|
+
"""A random interleave that keeps each repeat after its first request with others between them, and no
|
|
221
|
+
probe right after one with the same prompt or image file; when no such order is found (one pair has
|
|
222
|
+
too few other probes to go between its own), the one with the fewest such neighbours."""
|
|
223
|
+
by = {p.key: p for p in probes}
|
|
224
|
+
best = None
|
|
225
|
+
for _ in range(tries):
|
|
226
|
+
chains = [[by[f"base{p.pair}"], p] for p in probes if p.kind == "replay"]
|
|
227
|
+
chains += [[p] for p in probes if p.kind not in ("base", "replay")]
|
|
228
|
+
seq: list[Probe] = []
|
|
229
|
+
while chains: # a random chain head each step, one that does not clash with the last probe when possible
|
|
230
|
+
free = [c for c in chains if not (seq and clash(seq[-1].req, c[0].req))]
|
|
231
|
+
c = rng.choice(free or chains)
|
|
232
|
+
seq.append(c.pop(0))
|
|
233
|
+
chains = [x for x in chains if x]
|
|
234
|
+
adjacent = sum(1 for a, b in zip(seq, seq[1:]) if b.kind == "replay" and a.key == f"base{b.pair}")
|
|
235
|
+
score = (adjacent, len(neighbour_clashes(seq)))
|
|
236
|
+
if best is None or score < best[0]:
|
|
237
|
+
best = (score, seq)
|
|
238
|
+
if score == (0, 0):
|
|
239
|
+
break
|
|
240
|
+
return best[1]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def run_probes(backend, seq: list[Probe], staging: Path, label: str) -> None:
|
|
244
|
+
for p in seq:
|
|
245
|
+
p.t_start = time.time()
|
|
246
|
+
p.res = backend.generate(p.req)
|
|
247
|
+
if p.res.ok and p.res.video is not None:
|
|
248
|
+
p.sha = hashlib.sha256(p.res.video).hexdigest()
|
|
249
|
+
p.path = staging / "outputs" / label / "probes" / f"{p.key}.{p.res.format}"
|
|
250
|
+
p.path.parent.mkdir(parents=True, exist_ok=True)
|
|
251
|
+
p.path.write_bytes(p.res.video)
|
|
252
|
+
p.res.video = None # on disk now
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def probe_row(p: Probe, decoded: dict | None) -> dict:
|
|
256
|
+
body = p.req.body()
|
|
257
|
+
if "image" in body:
|
|
258
|
+
body["image"] = {"sha256": hashlib.sha256(p.req.image).hexdigest(), "bytes": len(p.req.image)}
|
|
259
|
+
r = p.res
|
|
260
|
+
return {"key": p.key, "kind": p.kind, "pair": p.pair, "meta": p.meta, "request": body, "ok": p.ok,
|
|
261
|
+
"status": r.status if r else None, "error": (r.error if r else "not sent"),
|
|
262
|
+
"e2e_ms": p.e2e_ms, "out_sha256": p.sha, "format": r.format if r and r.ok else None,
|
|
263
|
+
"effective": r.effective if r else None, "stage_ms": r.stage_ms if r else None,
|
|
264
|
+
"cache": r.cache if r else None, "decoded": decoded, "t_start": p.t_start}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# --------------------------------------------------------------------------- distances
|
|
268
|
+
|
|
269
|
+
DISTANCE_PAIRS = (("same", "base", "replay"), ("steps", "base", "half"))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def distance_plan(probes: dict[str, Probe], pairs: int) -> list[tuple[str, str, str]]:
|
|
273
|
+
"""(name, a, b) clip pairs whose pixel distance the gates need."""
|
|
274
|
+
out = []
|
|
275
|
+
for i in range(pairs):
|
|
276
|
+
out.append((f"same{i}", f"base{i}", f"replay{i}"))
|
|
277
|
+
if f"half{i}" in probes:
|
|
278
|
+
out.append((f"steps{i}", f"base{i}", f"half{i}"))
|
|
279
|
+
out += [(k, probes[k].vs, k) for k in ("seed", "prompt", "cfg")]
|
|
280
|
+
return [x for x in out if probes[x[1]].ok and probes[x[2]].ok and probes[x[1]].sha != probes[x[2]].sha]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def distances(probes: dict[str, Probe], plan: list[tuple[str, str, str]], python: str | None, work: Path,
|
|
284
|
+
timeout_s: float) -> tuple[dict | None, str | None]:
|
|
285
|
+
"""{name: {"mad", "psnr", "frames_compared", "ok", "error"}} on decoded pixels, or (None, why) without the
|
|
286
|
+
worker."""
|
|
287
|
+
if not python:
|
|
288
|
+
return None, "no --python: clips were compared by bytes only"
|
|
289
|
+
if not plan:
|
|
290
|
+
return {}, None
|
|
291
|
+
doc = run_worker(python, {"task": "compare", "metrics": ["mad", "psnr"], "frame_stride": 1,
|
|
292
|
+
"pairs": [{"key": n, "a": str(probes[a].path), "b": str(probes[b].path)}
|
|
293
|
+
for n, a, b in plan]}, work, "distances", timeout_s)
|
|
294
|
+
out = {}
|
|
295
|
+
for it in doc["items"]:
|
|
296
|
+
mean = it.get("mean") or {}
|
|
297
|
+
out[it["key"]] = {"ok": bool(it.get("ok")) and mean.get("mad") is not None, "mad": mean.get("mad"),
|
|
298
|
+
"psnr": mean.get("psnr"), "frames_compared": it.get("frames_compared"),
|
|
299
|
+
"frames": [it.get("frames_a"), it.get("frames_b")], "error": it.get("error")}
|
|
300
|
+
return out, None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class Judge:
|
|
304
|
+
"""Shared facts for the gates: probes by key, pixel distances, the same-seed A/A floor, and the probes run
|
|
305
|
+
right after one with the same prompt or image file (``clashes``)."""
|
|
306
|
+
|
|
307
|
+
def __init__(self, probes: dict[str, Probe], dist: dict | None, pairs: int, clashes: list | None = None):
|
|
308
|
+
self.p, self.dist, self.pairs, self.clashes = probes, dist, pairs, list(clashes or [])
|
|
309
|
+
self.floor_rows = []
|
|
310
|
+
for i in range(pairs):
|
|
311
|
+
a, b = probes[f"base{i}"], probes[f"replay{i}"]
|
|
312
|
+
if not (a.ok and b.ok):
|
|
313
|
+
continue
|
|
314
|
+
if a.sha == b.sha:
|
|
315
|
+
self.floor_rows.append({"pair": i, "identical_bytes": True, "mad": 0.0})
|
|
316
|
+
elif dist is not None and (dist.get(f"same{i}") or {}).get("ok"):
|
|
317
|
+
self.floor_rows.append({"pair": i, "identical_bytes": False, "mad": dist[f"same{i}"]["mad"],
|
|
318
|
+
"psnr": dist[f"same{i}"]["psnr"]})
|
|
319
|
+
else:
|
|
320
|
+
self.floor_rows.append({"pair": i, "identical_bytes": False, "mad": None})
|
|
321
|
+
known = [r["mad"] for r in self.floor_rows]
|
|
322
|
+
# The largest repeat distance; None when no repeat was judged or one could not be compared.
|
|
323
|
+
self.floor = max(known) if known and None not in known else None
|
|
324
|
+
|
|
325
|
+
def differs(self, name: str, a: str, b: str) -> dict:
|
|
326
|
+
"""How clip b differs from clip a: same_bytes | within_floor | differs | small | bytes_only | missing."""
|
|
327
|
+
pa, pb = self.p[a], self.p[b]
|
|
328
|
+
row = {"name": name, "a": a, "b": b}
|
|
329
|
+
if not (pa.ok and pb.ok):
|
|
330
|
+
return {**row, "state": "missing"}
|
|
331
|
+
if pa.sha == pb.sha:
|
|
332
|
+
return {**row, "state": "same_bytes", "mad": 0.0}
|
|
333
|
+
if self.dist is None:
|
|
334
|
+
return {**row, "state": "bytes_only"}
|
|
335
|
+
d = self.dist.get(name) or {}
|
|
336
|
+
if not d.get("ok"):
|
|
337
|
+
return {**row, "state": "missing", "error": d.get("error") or "not compared"}
|
|
338
|
+
row.update({"mad": d["mad"], "psnr": d["psnr"]})
|
|
339
|
+
if self.floor is None:
|
|
340
|
+
return {**row, "state": "small" if d["mad"] <= MIN_MAD else "no_floor"}
|
|
341
|
+
if d["mad"] <= self.floor:
|
|
342
|
+
return {**row, "state": "within_floor"}
|
|
343
|
+
if d["mad"] > max(SENSITIVITY * self.floor, MIN_MAD):
|
|
344
|
+
return {**row, "state": "differs"}
|
|
345
|
+
return {**row, "state": "small"}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _verdict_of(rows: list[dict]) -> tuple[str, list[dict]]:
|
|
349
|
+
"""FAIL when any changed input left the clip the same; PASS when every one moved it clearly."""
|
|
350
|
+
bad = [r for r in rows if r["state"] in ("same_bytes", "within_floor")]
|
|
351
|
+
if bad:
|
|
352
|
+
return "FAIL", bad
|
|
353
|
+
if rows and all(r["state"] == "differs" for r in rows):
|
|
354
|
+
return "PASS", []
|
|
355
|
+
return "UNPROVEN", [r for r in rows if r["state"] != "differs"]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
STATE_TEXT = {"same_bytes": "byte-identical clips", "within_floor": "no farther apart than the same request repeated",
|
|
359
|
+
"small": "the clips barely differ (not above 3x the repeat floor and 0.5 grey levels)",
|
|
360
|
+
"bytes_only": "bytes differ but pixels were not compared (pass --python)",
|
|
361
|
+
"no_floor": "no same-seed repeat to compare against", "missing": "a probe failed or was not compared"}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _explain(rows: list[dict]) -> str:
|
|
365
|
+
return "; ".join(f"{r['b']} vs {r['a']}: {STATE_TEXT.get(r['state'], r['state'])}"
|
|
366
|
+
+ (f" (MAD {r['mad']:.3g})" if r.get("mad") is not None else "") for r in rows[:4])
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _failed_note(probes: list[Probe]) -> str | None:
|
|
370
|
+
bad = [p for p in probes if not p.ok]
|
|
371
|
+
if not bad:
|
|
372
|
+
return None
|
|
373
|
+
first = next((p.res.error for p in bad if p.res is not None and p.res.error), "not sent")
|
|
374
|
+
return f"{len(bad)} probe(s) failed (first: {str(first)[:160]})"
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# --------------------------------------------------------------------------- the gates
|
|
378
|
+
|
|
379
|
+
def gate_steps(j: Judge, steps: int) -> dict:
|
|
380
|
+
if steps < 2:
|
|
381
|
+
return _gate("steps_honoured", "NOT RUN", "the probe clip asks for fewer than 2 steps: no N/2 to compare")
|
|
382
|
+
probes = list(j.p.values())
|
|
383
|
+
counter = [{"key": p.key, "asked": p.req.steps, "steps_run": (p.res.effective or {}).get("steps_run")}
|
|
384
|
+
for p in probes if p.ok]
|
|
385
|
+
reported = [c for c in counter if c["steps_run"] is not None]
|
|
386
|
+
wrong = [c for c in reported if c["steps_run"] != c["asked"]]
|
|
387
|
+
rows = [j.differs(f"steps{i}", f"base{i}", f"half{i}") for i in range(j.pairs)]
|
|
388
|
+
out_v, out_bad = _verdict_of(rows)
|
|
389
|
+
ratios = []
|
|
390
|
+
for i in range(j.pairs):
|
|
391
|
+
a, h = j.p[f"base{i}"], j.p[f"half{i}"]
|
|
392
|
+
if a.e2e_ms and h.e2e_ms:
|
|
393
|
+
ratios.append({"pair": i, "e2e_ms": a.e2e_ms, "half_e2e_ms": h.e2e_ms, "ratio": a.e2e_ms / h.e2e_ms,
|
|
394
|
+
"denoise_ratio": _stage_ratio(a, h, "denoise")})
|
|
395
|
+
rs = [r["ratio"] for r in ratios]
|
|
396
|
+
med = _median(rs)
|
|
397
|
+
typical = bool(rs) and 3 * sum(1 for x in rs if x >= STEP_RATIO_PASS) >= 2 * len(rs)
|
|
398
|
+
metrics = {"step_time_ratio_median": med, "pairs": len(rs), "steps_reported": len(reported),
|
|
399
|
+
"steps_mismatch": len(wrong), "half_steps": steps // 2}
|
|
400
|
+
details = {"counter": counter, "outputs": rows, "timing": ratios}
|
|
401
|
+
lim = ["an engine that runs a fixed fraction of the steps it is asked for scales like an honest one here; "
|
|
402
|
+
"quality clip against the reference at the same steps catches it"]
|
|
403
|
+
note = _failed_note([j.p[k] for k in j.p if k.startswith(("base", "half"))])
|
|
404
|
+
if note:
|
|
405
|
+
lim.append(note)
|
|
406
|
+
if wrong:
|
|
407
|
+
return _gate("steps_honoured", "FAIL", f"the engine reports steps_run != asked on {len(wrong)} probe(s) "
|
|
408
|
+
f"(e.g. {wrong[0]['key']}: asked {wrong[0]['asked']}, ran {wrong[0]['steps_run']})",
|
|
409
|
+
metrics, details, lim)
|
|
410
|
+
if out_v == "FAIL":
|
|
411
|
+
return _gate("steps_honoured", "FAIL", f"N/2 steps gave the same clip as N steps: {_explain(out_bad)}",
|
|
412
|
+
metrics, details, lim)
|
|
413
|
+
if med is not None and med < STEP_RATIO_FAIL:
|
|
414
|
+
return _gate("steps_honoured", "FAIL", f"{steps} steps take {med:.3f}x the time of {steps // 2} (median "
|
|
415
|
+
f"of {len(rs)} pair(s)): the step count does not change the work done", metrics, details, lim)
|
|
416
|
+
problems = []
|
|
417
|
+
if not reported:
|
|
418
|
+
problems.append("the engine reports no steps_run (effective) to check")
|
|
419
|
+
if out_v != "PASS":
|
|
420
|
+
problems.append(f"outputs: {_explain(out_bad)}")
|
|
421
|
+
if med is None:
|
|
422
|
+
problems.append("no N vs N/2 pair was timed")
|
|
423
|
+
elif not (med >= STEP_RATIO_PASS and typical):
|
|
424
|
+
problems.append(f"time ratio N vs N/2 {med:.3f}x (median of {len(rs)}) is below {STEP_RATIO_PASS}x or not "
|
|
425
|
+
"in 2/3 of the pairs")
|
|
426
|
+
if problems:
|
|
427
|
+
return _gate("steps_honoured", "UNPROVEN", "; ".join(problems), metrics, details, lim)
|
|
428
|
+
return _gate("steps_honoured", "PASS", f"steps_run == asked on {len(reported)} probe(s); N/2 steps changes the "
|
|
429
|
+
f"clip; {steps} steps take {med:.2f}x the time of {steps // 2}", metrics, details, lim)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _stage_ratio(a: Probe, b: Probe, stage: str) -> float | None:
|
|
433
|
+
sa, sb = (a.res.stage_ms or {}).get(stage), (b.res.stage_ms or {}).get(stage)
|
|
434
|
+
ok = all(isinstance(x, (int, float)) and not isinstance(x, bool) and x > 0 for x in (sa, sb))
|
|
435
|
+
return sa / sb if ok else None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def gate_changed(j: Judge, name: str, probe: str, what: str) -> dict:
|
|
439
|
+
"""cfg_honoured / prompt_sensitivity: one changed input, same seed, must change the clip."""
|
|
440
|
+
a, b = j.p[j.p[probe].vs], j.p[probe]
|
|
441
|
+
rows = [j.differs(probe, a.key, probe)]
|
|
442
|
+
v, bad = _verdict_of(rows)
|
|
443
|
+
metrics = {"mad": rows[0].get("mad"), "psnr": rows[0].get("psnr"), "repeat_floor_mad": j.floor}
|
|
444
|
+
details = {"change": b.meta, "outputs": rows, "floor": j.floor_rows}
|
|
445
|
+
if name == "cfg_honoured" and a.e2e_ms and b.e2e_ms:
|
|
446
|
+
metrics["time_ratio"] = a.e2e_ms / b.e2e_ms
|
|
447
|
+
lim = [x for x in [_failed_note([a, b])] if x]
|
|
448
|
+
if v == "PASS":
|
|
449
|
+
return _gate(name, "PASS", f"{what} changed the clip (MAD {rows[0]['mad']:.3g} vs the repeat's "
|
|
450
|
+
f"{j.floor:.3g})", metrics, details, lim)
|
|
451
|
+
return _gate(name, v, f"{what}: {_explain(bad)}", metrics, details, lim)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def gate_seed(j: Judge) -> dict:
|
|
455
|
+
rows = [j.differs("seed", j.p["seed"].vs, "seed")]
|
|
456
|
+
v, bad = _verdict_of(rows)
|
|
457
|
+
mads = [r["mad"] for r in rows if r.get("mad") is not None and r["state"] != "same_bytes"]
|
|
458
|
+
metrics = {"repeat_floor_mad": j.floor, "different_seed_mad_min": min(mads) if mads else None,
|
|
459
|
+
"repeats": len(j.floor_rows), "different_seed_pairs": len(rows)}
|
|
460
|
+
details = {"floor": j.floor_rows, "different_seeds": rows}
|
|
461
|
+
lim = [x for x in [_failed_note([p for k, p in j.p.items() if k.startswith(("base", "replay")) or k == "seed"])]
|
|
462
|
+
if x]
|
|
463
|
+
same = [r for r in rows if r["state"] == "same_bytes"]
|
|
464
|
+
if same: # checked first: definitive whatever the floor is
|
|
465
|
+
return _gate("seed_honoured", "FAIL", f"a different seed did not change the clip: {_explain(same)}",
|
|
466
|
+
metrics, details, lim)
|
|
467
|
+
if j.floor is not None and mads and j.floor > REPRO_SHARE * min(mads):
|
|
468
|
+
return _gate("seed_honoured", "FAIL", f"the same seed does not reproduce the clip: the repeat is MAD "
|
|
469
|
+
f"{j.floor:.3g} from the first, a different seed {min(mads):.3g}", metrics, details, lim)
|
|
470
|
+
if v == "FAIL":
|
|
471
|
+
return _gate("seed_honoured", "FAIL", f"a different seed did not change the clip: {_explain(bad)}",
|
|
472
|
+
metrics, details, lim)
|
|
473
|
+
if v == "PASS" and j.floor is not None:
|
|
474
|
+
return _gate("seed_honoured", "PASS", f"different seeds change the clip (MAD >= {min(mads):.3g}); the "
|
|
475
|
+
f"same seed repeats within MAD {j.floor:.3g}", metrics, details, lim)
|
|
476
|
+
why = _explain(bad) if bad else "no same-seed repeat could be compared"
|
|
477
|
+
return _gate("seed_honoured", "UNPROVEN", why, metrics, details, lim)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def check_decoded(d: dict | None, asked: dict, audio: bool | None) -> list[str]:
|
|
481
|
+
"""What the decoded clip gets wrong against the request (empty = all good)."""
|
|
482
|
+
if not d or not d.get("ok"):
|
|
483
|
+
return []
|
|
484
|
+
v, problems = d.get("video") or {}, []
|
|
485
|
+
if not v.get("frames"):
|
|
486
|
+
return ["no video frames decoded"]
|
|
487
|
+
if (v.get("width"), v.get("height")) != (asked["width"], asked["height"]):
|
|
488
|
+
problems.append(f"decoded {v.get('width')}x{v.get('height')}, asked {asked['width']}x{asked['height']}")
|
|
489
|
+
if len(v.get("sizes") or []) > 1:
|
|
490
|
+
problems.append(f"frame size changes inside the clip: {v['sizes'][:4]}")
|
|
491
|
+
if v["frames"] != asked["num_frames"]:
|
|
492
|
+
problems.append(f"decoded {v['frames']} frames, asked {asked['num_frames']}")
|
|
493
|
+
if v.get("fps") is not None and abs(v["fps"] - asked["fps"]) > FPS_TOL * asked["fps"]:
|
|
494
|
+
problems.append(f"decoded timestamps give {v['fps']:.4g} fps, asked {asked['fps']:g}")
|
|
495
|
+
if audio:
|
|
496
|
+
a = d.get("audio") or {}
|
|
497
|
+
want = asked["num_frames"] / asked["fps"]
|
|
498
|
+
if not a.get("duration_s"):
|
|
499
|
+
problems.append("audio was asked for but none was decoded")
|
|
500
|
+
elif abs(a["duration_s"] - want) > AUDIO_TOL_S + 1 / asked["fps"]:
|
|
501
|
+
problems.append(f"decoded audio is {a['duration_s']:.3f} s, the clip is {want:.3f} s")
|
|
502
|
+
return problems
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def header_claims(d: dict | None) -> list[str]:
|
|
506
|
+
"""Where the container header disagrees with what decoding gave (recorded, never trusted)."""
|
|
507
|
+
if not d or not d.get("ok"):
|
|
508
|
+
return []
|
|
509
|
+
hv, v, out = (d.get("header") or {}).get("video") or {}, d.get("video") or {}, []
|
|
510
|
+
for k in ("width", "height", "frames"):
|
|
511
|
+
if hv.get(k) is not None and v.get(k) is not None and hv[k] != v[k]:
|
|
512
|
+
out.append(f"header {k} {hv[k]} vs decoded {v[k]}")
|
|
513
|
+
return out
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def gate_resolution(items: list[dict], decoder: str | None, why_not: str | None, left_out: int = 0) -> dict:
|
|
517
|
+
"""items: {key, asked, audio, decoded, source, problems?, error?}: ``problems`` found before decoding (a
|
|
518
|
+
file that is not the timed clip) fail the clip; ``error`` says why a clip has no decode (no file)."""
|
|
519
|
+
if not items:
|
|
520
|
+
return _gate("resolution_frames", "NOT RUN", why_not or "no clip to decode")
|
|
521
|
+
rows, bad, undecoded = [], [], []
|
|
522
|
+
for it in items:
|
|
523
|
+
d = it["decoded"]
|
|
524
|
+
probs = list(it.get("problems") or []) + check_decoded(d, it["asked"], it["audio"])
|
|
525
|
+
row = {"key": it["key"], "source": it["source"], "problems": probs, "header_mismatch": header_claims(d),
|
|
526
|
+
"decoded": {k: (d.get("video") or {}).get(k) for k in ("width", "height", "frames", "fps")}
|
|
527
|
+
if d and d.get("ok") else None,
|
|
528
|
+
"audio_s": ((d or {}).get("audio") or {}).get("duration_s") if d and d.get("ok") else None}
|
|
529
|
+
if probs:
|
|
530
|
+
bad.append(row)
|
|
531
|
+
elif not d or not d.get("ok"):
|
|
532
|
+
why = (d or {}).get("error") or it.get("error") or (f"not decoded: {why_not}" if why_not else "not decoded")
|
|
533
|
+
undecoded.append({"key": it["key"], "error": why})
|
|
534
|
+
rows.append(row)
|
|
535
|
+
decoded = sum(1 for r in rows if r["decoded"] is not None)
|
|
536
|
+
metrics = {"clips_decoded": decoded, "clips_wrong": len(bad), "clips_undecoded": len(undecoded),
|
|
537
|
+
"clips_left_out": left_out}
|
|
538
|
+
lim = [f"decoded with {decoder}"] if decoder else []
|
|
539
|
+
if left_out:
|
|
540
|
+
lim.append(f"--max-decode: {left_out} timed clip(s) of the bench clip run were not decoded (a random subset "
|
|
541
|
+
"was, drawn from the probe seed)")
|
|
542
|
+
if bad:
|
|
543
|
+
return _gate("resolution_frames", "FAIL", f"{len(bad)} clip(s) are not what was asked (e.g. {bad[0]['key']}: "
|
|
544
|
+
f"{'; '.join(bad[0]['problems'])})", metrics, rows, lim)
|
|
545
|
+
if undecoded:
|
|
546
|
+
return _gate("resolution_frames", "UNPROVEN", f"{len(undecoded)} clip(s) could not be decoded (e.g. "
|
|
547
|
+
f"{undecoded[0]['key']}: {undecoded[0]['error'][:160]})", metrics, rows, lim)
|
|
548
|
+
return _gate("resolution_frames", "PASS", f"{decoded} decoded clip(s) match the asked size, frame count, fps"
|
|
549
|
+
+ (" and audio length" if any(it["audio"] for it in items) else ""), metrics, rows, lim)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def gate_no_cache(j: Judge | None, threshold: float, static: dict | None) -> dict:
|
|
553
|
+
problems, unproven, rows, metrics, lim = [], [], [], {}, []
|
|
554
|
+
if j is not None:
|
|
555
|
+
live = list(j.p.values())
|
|
556
|
+
cached = [{"key": p.key, "cache": p.res.cache} for p in live if p.res is not None and p.res.cache]
|
|
557
|
+
if cached:
|
|
558
|
+
after = {b for _, b in j.clashes} & {c["key"] for c in cached}
|
|
559
|
+
problems.append(f"{len(cached)} probe(s) report stages served from a cache (e.g. {cached[0]['key']}: "
|
|
560
|
+
f"{cached[0]['cache'][:4]})" + (
|
|
561
|
+
f"; {len(after)} of them ran right after a probe with the same prompt or image file "
|
|
562
|
+
"(no order without such neighbours was found: use --pairs 2 or more)" if after else ""))
|
|
563
|
+
reused = [p.key for p in live if p.res is not None and p.res.cache_allowed]
|
|
564
|
+
if reused:
|
|
565
|
+
lim.append(f"{len(reused)} probe(s): the engine reused the encoding of its fixed negative prompt "
|
|
566
|
+
f"({cc.NEGATIVE_PROMPT_CACHE}: computed once, not per clip; allowed for every engine)")
|
|
567
|
+
sp = []
|
|
568
|
+
for i in range(j.pairs):
|
|
569
|
+
a, b = j.p[f"base{i}"], j.p[f"replay{i}"]
|
|
570
|
+
if a.e2e_ms and b.e2e_ms:
|
|
571
|
+
sp.append(a.e2e_ms / b.e2e_ms)
|
|
572
|
+
rows.append({"pair": i, "first_e2e_ms": a.e2e_ms, "replay_e2e_ms": b.e2e_ms,
|
|
573
|
+
"speedup": a.e2e_ms / b.e2e_ms, "same_bytes": a.sha == b.sha})
|
|
574
|
+
med = _median(sp)
|
|
575
|
+
most = bool(sp) and 3 * sum(1 for x in sp if x > 1 + threshold) >= 2 * len(sp)
|
|
576
|
+
metrics.update({"replay_speedup_median": med, "replay_pairs": len(sp), "reported_cached": len(cached),
|
|
577
|
+
"neighbour_clashes": len(j.clashes)})
|
|
578
|
+
if j.clashes:
|
|
579
|
+
lim.append(f"{len(j.clashes)} probe(s) ran right after one with the same prompt or image file (e.g. "
|
|
580
|
+
f"{j.clashes[0][0]} -> {j.clashes[0][1]}): an engine that keeps the last run's encodings "
|
|
581
|
+
"(ComfyUI) reuses them there; use --pairs 2 or more")
|
|
582
|
+
if med is not None and med > 1 + threshold and most:
|
|
583
|
+
problems.append(f"replays are {med:.2f}x faster than their first request (median of {len(sp)}; limit "
|
|
584
|
+
f"{1 + threshold:.2f}x)")
|
|
585
|
+
elif not sp:
|
|
586
|
+
unproven.append("no request and its replay both succeeded")
|
|
587
|
+
distinct = [p for p in live if p.kind != "replay" and p.ok]
|
|
588
|
+
dup = _duplicates({p.key: p.sha for p in distinct})
|
|
589
|
+
metrics["distinct_probe_clips"] = len(distinct)
|
|
590
|
+
if dup:
|
|
591
|
+
problems.append(f"different requests got byte-identical clips: {dup[:3]} (an output cache, or an input "
|
|
592
|
+
"the engine ignores)")
|
|
593
|
+
note = _failed_note(live)
|
|
594
|
+
if note:
|
|
595
|
+
unproven.append(note)
|
|
596
|
+
if static is not None:
|
|
597
|
+
metrics.update({"bench_clips": static["n"], "bench_duplicates": len(static["duplicates"]),
|
|
598
|
+
"bench_cached": len(static["cached"])})
|
|
599
|
+
if static["duplicates"]:
|
|
600
|
+
problems.append(f"bench clip: different requests got byte-identical clips: {static['duplicates'][:3]}")
|
|
601
|
+
if static["cached"]:
|
|
602
|
+
problems.append(f"bench clip: {len(static['cached'])} timed clip(s) report cached stages (e.g. "
|
|
603
|
+
f"{static['cached'][0]})")
|
|
604
|
+
details = {"replays": rows, "static": static, "neighbour_clashes": j.clashes if j is not None else None}
|
|
605
|
+
if j is None and static is None:
|
|
606
|
+
return _gate("no_cache", "NOT RUN", "no live probes and no --bench-evidence")
|
|
607
|
+
if problems:
|
|
608
|
+
return _gate("no_cache", "FAIL", "; ".join(problems), metrics, details, lim)
|
|
609
|
+
if unproven:
|
|
610
|
+
return _gate("no_cache", "UNPROVEN", "; ".join(unproven), metrics, details, lim)
|
|
611
|
+
parts = []
|
|
612
|
+
if j is not None:
|
|
613
|
+
parts.append(f"replays {metrics['replay_speedup_median']:.2f}x (limit {1 + threshold:.2f}x); "
|
|
614
|
+
f"{metrics['distinct_probe_clips']} distinct requests, distinct clips")
|
|
615
|
+
if static is not None:
|
|
616
|
+
parts.append(f"bench clip: {static['n']} timed clip(s), no duplicate or cached clip")
|
|
617
|
+
return _gate("no_cache", "PASS", "; ".join(parts), metrics, details, lim)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _duplicates(shas: dict[str, str]) -> list[list[str]]:
|
|
621
|
+
groups: dict[str, list[str]] = {}
|
|
622
|
+
for k, s in shas.items():
|
|
623
|
+
groups.setdefault(s, []).append(k)
|
|
624
|
+
return [sorted(v) for v in groups.values() if len(v) > 1]
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# --------------------------------------------------------------------------- a bench clip record
|
|
628
|
+
|
|
629
|
+
def load_bench(store: ev.Store, ref: str, run_dir: str | None = None) -> dict:
|
|
630
|
+
"""Rows, the directories its clips may be in, and the asked audio of a bench clip record.
|
|
631
|
+
|
|
632
|
+
Refused when the record or its requests.jsonl was edited after capture: a gates record built from an
|
|
633
|
+
edited input would carry a fresh id that verifies."""
|
|
634
|
+
rec = store.load(ref)
|
|
635
|
+
data = rec.get("data") or {}
|
|
636
|
+
if data.get("source") != "bench clip":
|
|
637
|
+
raise OpError(f"--bench-evidence {ref}: not a bench clip record")
|
|
638
|
+
problem = ev.integrity_problem(rec)
|
|
639
|
+
if problem:
|
|
640
|
+
raise OpError(f"--bench-evidence: {problem}; refusing to check it")
|
|
641
|
+
root = store.run_dir(rec["evidence_id"])
|
|
642
|
+
art = next((a for a in rec.get("artifacts", []) if a.get("role") == "requests"), None)
|
|
643
|
+
path = root / art["path"] if art and isinstance(art.get("path"), str) else None
|
|
644
|
+
if path is None or not path.is_file():
|
|
645
|
+
raise OpError(f"--bench-evidence {ref}: its requests.jsonl is not in the store ({root})")
|
|
646
|
+
if file_sha256(path)[0] != art.get("sha256"):
|
|
647
|
+
raise OpError(f"--bench-evidence {ref}: its requests.jsonl does not match the sha256 the record lists "
|
|
648
|
+
"(edited after capture?); refusing to check it")
|
|
649
|
+
rows = [json.loads(x) for x in path.read_text().splitlines() if x.strip()]
|
|
650
|
+
# Where the timed clips may be: this record's directory, those of the records it extends (each keeps the
|
|
651
|
+
# rounds it timed), and the run directories (bench clip's and this call's --run-dir).
|
|
652
|
+
roots = [root] + [store.run_dir(e) for e in data.get("outputs_from") or [] if isinstance(e, str)
|
|
653
|
+
and e.startswith("ev_") and "/" not in e and ".." not in e]
|
|
654
|
+
bench_run = ((data.get("request") or {}).get("args") or {}).get("run_dir")
|
|
655
|
+
for rd in (run_dir, bench_run): # records keep paths under the home directory as ~/...
|
|
656
|
+
d = Path(rd).expanduser() if isinstance(rd, str) and rd else None
|
|
657
|
+
if d is not None and d.is_dir() and d not in roots:
|
|
658
|
+
roots.append(d)
|
|
659
|
+
audio = (rec.get("identity") or {}).get("workload", {}).get("audio")
|
|
660
|
+
return {"record": rec, "root": root, "roots": roots, "rows": rows, "label": data.get("label"),
|
|
661
|
+
"hash_only": data.get("save_outputs") == "hash_only", "audio": audio if isinstance(audio, bool) else None}
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def static_cache(bench: dict) -> dict:
|
|
665
|
+
"""Different requests (clip id, seed) must never share a clip; no timed clip may report a cached stage."""
|
|
666
|
+
shas: dict[str, set] = {}
|
|
667
|
+
for r in bench["rows"]:
|
|
668
|
+
if r.get("ok") and r.get("out_sha256"):
|
|
669
|
+
shas.setdefault(r["out_sha256"], set()).add(f"{r['id']}@seed{r['seed']}")
|
|
670
|
+
dup = [sorted(v) for v in shas.values() if len(v) > 1]
|
|
671
|
+
cached = [f"{r['id']} r{r['round']}" for r in bench["rows"] if r.get("ok") and r.get("cache")]
|
|
672
|
+
return {"n": sum(1 for r in bench["rows"] if r.get("ok")), "duplicates": dup, "cached": cached}
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def bench_clips(bench: dict, limit: int, rng: random.Random) -> tuple[list[dict], int]:
|
|
676
|
+
"""The timed clips of a bench clip record to decode: [{key, path, asked, state, error}] with state ok (the
|
|
677
|
+
file has the row's sha256), changed (files found, none with it) or lost (no file), and how many timed
|
|
678
|
+
clips ``limit`` left out (0 = decode all; else a random ``limit`` of them)."""
|
|
679
|
+
timed = [r for r in bench["rows"] if r.get("ok") and r.get("out_sha256")]
|
|
680
|
+
chosen = timed
|
|
681
|
+
if limit and len(timed) > limit:
|
|
682
|
+
chosen = [timed[i] for i in sorted(rng.sample(range(len(timed)), limit))]
|
|
683
|
+
out = []
|
|
684
|
+
for r in chosen:
|
|
685
|
+
key = f"bench:r{r.get('round')}/{r.get('id')}"
|
|
686
|
+
rel = Path("outputs", str(bench["label"]), f"r{r.get('round')}", f"{r.get('id')}.{r.get('format')}")
|
|
687
|
+
it = {"key": key, "path": None, "asked": r.get("asked"), "state": "lost", "error": None}
|
|
688
|
+
if not (r.get("format") and isinstance(r.get("asked"), dict)) or rel.is_absolute() or ".." in rel.parts:
|
|
689
|
+
out.append({**it, "error": "the row names no usable clip file"})
|
|
690
|
+
continue
|
|
691
|
+
found = [root / rel for root in bench["roots"] if (root / rel).is_file()]
|
|
692
|
+
hit = next((f for f in found if file_sha256(f)[0] == r["out_sha256"]), None)
|
|
693
|
+
if hit is not None:
|
|
694
|
+
out.append({**it, "path": hit, "state": "ok"})
|
|
695
|
+
elif found:
|
|
696
|
+
out.append({**it, "state": "changed", "error": "clip file is not the clip that was timed (its sha256 "
|
|
697
|
+
"differs from the row's out_sha256)"})
|
|
698
|
+
else:
|
|
699
|
+
out.append({**it, "error": "no clip file kept (--save-outputs hash_only)" if bench["hash_only"] else
|
|
700
|
+
"no clip file in the record's store directory, the records it extends or the run directory"})
|
|
701
|
+
return out, len(timed) - len(chosen)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# --------------------------------------------------------------------------- the operation
|
|
705
|
+
|
|
706
|
+
@operation("gates_clip", ("gates", "clip"), "Anti-cheat gates for a video clip engine: steps and guidance honoured, "
|
|
707
|
+
"decoded resolution / frame count / fps / audio length, seeds and prompts change the clip, no cached "
|
|
708
|
+
"outputs (replay timing, unique clips); optionally the timed clips of a bench clip record.",
|
|
709
|
+
[Arg("base_url", "string", "Engine root, e.g. http://127.0.0.1:8000 (the live gates need it). Never "
|
|
710
|
+
"stored: records keep an HMAC of it (as bench clip does).", sensitive=True),
|
|
711
|
+
Arg("clips", "string", "Clip set JSONL (as bench clip): the probes use one of its clips."),
|
|
712
|
+
Arg("clip_id", "string", "Clip of the set the probes are built from (default: the first)."),
|
|
713
|
+
Arg("endpoint_kind", "string", "clip_http (POST /v1/clips) or comfyui (needs --workflow).",
|
|
714
|
+
default="clip_http", choices=cc.ENDPOINT_KINDS),
|
|
715
|
+
Arg("workflow", "string", "comfyui: the API-format workflow JSON."),
|
|
716
|
+
Arg("label", "string", "Engine label for the record (default: the endpoint kind)."),
|
|
717
|
+
Arg("model", "string", "Model id for the record's identity."),
|
|
718
|
+
Arg("engine_version", "string", "Engine version, recorded in the identity."),
|
|
719
|
+
Arg("bench_evidence", "string", "Evidence id of a bench clip run (with --extend, its last record): "
|
|
720
|
+
"its timed clips, every round, are hash-checked, decoded and checked for duplicates and cached "
|
|
721
|
+
"stages."),
|
|
722
|
+
Arg("max_decode", "integer", "Decode at most this many of the bench clip run's clips, a random subset "
|
|
723
|
+
"drawn from the probe seed (0 = all).", default=0),
|
|
724
|
+
Arg("python", "string", "Python of the video pipeline's venv (PyAV + numpy): decodes clips and "
|
|
725
|
+
"compares their pixels. Without it, ffprobe decodes and clips are compared by bytes only."),
|
|
726
|
+
Arg("pairs", "integer", "Request/replay pairs (each also gets an N/2-steps probe).", default=2),
|
|
727
|
+
Arg("warmup", "integer", "Untimed warm-up clips at the probe shape before the probes.", default=1),
|
|
728
|
+
Arg("threshold", "number", "no_cache: largest allowed replay speedup - 1.", default=0.10),
|
|
729
|
+
Arg("seed", "integer", "Probe seed (default: drawn at random for every run and recorded; pass it only "
|
|
730
|
+
"to reproduce a run)."),
|
|
731
|
+
Arg("api_key_env", "string", "Name of the environment variable holding the API key (never stored)."),
|
|
732
|
+
Arg("timeout_s", "number", "Per-clip timeout, seconds.", default=cc.DEFAULT_TIMEOUT_S),
|
|
733
|
+
Arg("run_dir", "string", "Run directory: the record is also copied to <run-dir>/evidence/.")])
|
|
734
|
+
def gates_clip(call, base_url, clips, clip_id, endpoint_kind, label, model, engine_version, bench_evidence,
|
|
735
|
+
max_decode, python, pairs, warmup, threshold, seed, api_key_env, timeout_s, workflow, run_dir):
|
|
736
|
+
if not base_url and not bench_evidence:
|
|
737
|
+
raise OpError("give --base-url (live probes, with --clips) and/or --bench-evidence (a bench clip run)")
|
|
738
|
+
if base_url and not clips:
|
|
739
|
+
raise OpError("--base-url needs --clips (the clip set the probes are built from)")
|
|
740
|
+
if pairs < 1:
|
|
741
|
+
raise OpError("pairs must be >= 1")
|
|
742
|
+
if warmup < 0 or max_decode < 0:
|
|
743
|
+
raise OpError("warmup and max_decode must be >= 0")
|
|
744
|
+
if endpoint_kind == "comfyui" and base_url and not workflow:
|
|
745
|
+
raise OpError("--endpoint-kind comfyui needs --workflow (the API-format workflow JSON)")
|
|
746
|
+
label = label or endpoint_kind
|
|
747
|
+
store = ev.Store()
|
|
748
|
+
bench = load_bench(store, bench_evidence, run_dir) if bench_evidence else None
|
|
749
|
+
rcall, ep, clipset, clip = call, None, None, None
|
|
750
|
+
if base_url:
|
|
751
|
+
clipset = bc.load_clip_set(clips)
|
|
752
|
+
clip = next((c for c in clipset.clips if c["id"] == clip_id), None) if clip_id else clipset.clips[0]
|
|
753
|
+
if clip is None:
|
|
754
|
+
raise OpError(f"--clip-id {clip_id!r} is not in the clip set")
|
|
755
|
+
try:
|
|
756
|
+
ep = Endpoint.parse(base_url, api_key_from_env(api_key_env))
|
|
757
|
+
except ValueError as e:
|
|
758
|
+
raise OpError(str(e)) from None
|
|
759
|
+
rcall, _ = _record_call(call, ep)
|
|
760
|
+
probe_seed = seed if seed is not None else secrets.randbits(31)
|
|
761
|
+
rng = random.Random(probe_seed)
|
|
762
|
+
|
|
763
|
+
with store.staging_dir() as staging:
|
|
764
|
+
probes: dict[str, Probe] = {}
|
|
765
|
+
warm, judge, dist, why_bytes, seq = [], None, None, None, []
|
|
766
|
+
if ep is not None:
|
|
767
|
+
try:
|
|
768
|
+
backend = cc.make_backend(endpoint_kind, ep, timeout_s=timeout_s, workflow=workflow)
|
|
769
|
+
except (ValueError, LookupError) as e:
|
|
770
|
+
raise OpError(str(e)) from None
|
|
771
|
+
try:
|
|
772
|
+
status, detail = backend.health()
|
|
773
|
+
if status != 200:
|
|
774
|
+
raise OpError(f"engine not ready: GET {cc.HEALTH_PATH} answered {status or 'nothing'} ({detail})")
|
|
775
|
+
planned = plan_probes(clip, clipset, rng, pairs)
|
|
776
|
+
probes = {p.key: p for p in planned}
|
|
777
|
+
for i in range(warmup):
|
|
778
|
+
wseed = rng.getrandbits(31)
|
|
779
|
+
wreq = warmup_request(planned[0].req, i, wseed, planned)
|
|
780
|
+
res = backend.generate(wreq)
|
|
781
|
+
warm.append({"seed": wseed, "prompt": wreq.prompt, "ok": res.ok, "e2e_ms": res.e2e_ms,
|
|
782
|
+
"error": res.error})
|
|
783
|
+
seq = order(planned, rng)
|
|
784
|
+
run_probes(backend, seq, staging, label)
|
|
785
|
+
finally:
|
|
786
|
+
backend.close()
|
|
787
|
+
live_ok = any(p.ok for p in probes.values())
|
|
788
|
+
files, asked = [], {}
|
|
789
|
+
for p in probes.values():
|
|
790
|
+
if p.ok:
|
|
791
|
+
files.append((p.key, p.path))
|
|
792
|
+
asked[p.key] = ({"width": p.req.width, "height": p.req.height, "num_frames": p.req.num_frames,
|
|
793
|
+
"fps": p.req.fps}, p.req.audio, "probe")
|
|
794
|
+
# Its own generator, so the live probes do not depend on whether a bench record was given.
|
|
795
|
+
bench_items, left_out = bench_clips(bench, max_decode, random.Random(f"bench:{probe_seed}")) if bench \
|
|
796
|
+
else ([], 0)
|
|
797
|
+
for it in bench_items:
|
|
798
|
+
if it["state"] == "ok":
|
|
799
|
+
files.append((it["key"], it["path"]))
|
|
800
|
+
asked[it["key"]] = (it["asked"], bench["audio"], "bench clip")
|
|
801
|
+
decoded, decoder, why_not = {}, None, None
|
|
802
|
+
if files: # the probes are done: a decoder that fails must not throw them away
|
|
803
|
+
try:
|
|
804
|
+
decoded, decoder = probe_clips(files, python, staging / "worker", timeout_s)
|
|
805
|
+
except OpError as e:
|
|
806
|
+
why_not = str(e)
|
|
807
|
+
if python and which("ffprobe"):
|
|
808
|
+
decoded, decoder = probe_clips(files, None, staging / "worker", timeout_s)
|
|
809
|
+
decoder += f" (the --python worker failed: {why_not[:200]})"
|
|
810
|
+
if live_ok:
|
|
811
|
+
try:
|
|
812
|
+
dist, why_bytes = distances(probes, distance_plan(probes, pairs), python, staging / "worker",
|
|
813
|
+
timeout_s)
|
|
814
|
+
except OpError as e:
|
|
815
|
+
dist, why_bytes = None, f"pixel comparison failed, clips compared by bytes only: {str(e)[:300]}"
|
|
816
|
+
judge = Judge(probes, dist, pairs, neighbour_clashes(seq))
|
|
817
|
+
|
|
818
|
+
gates = []
|
|
819
|
+
|
|
820
|
+
def live(g: dict) -> dict:
|
|
821
|
+
g["applicable"] = ep is not None
|
|
822
|
+
return g
|
|
823
|
+
|
|
824
|
+
not_run = _failed_note(list(probes.values())) or "no --base-url: live probes not run"
|
|
825
|
+
if judge is None:
|
|
826
|
+
for name in ("steps_honoured", "cfg_honoured"):
|
|
827
|
+
gates.append(live(_gate(name, "NOT RUN", not_run)))
|
|
828
|
+
else:
|
|
829
|
+
gates.append(live(gate_steps(judge, clip["steps"])))
|
|
830
|
+
gates.append(live(gate_changed(judge, "cfg_honoured", "cfg", f"guidance {clip['guidance_scale']:g} -> "
|
|
831
|
+
f"{probes['cfg'].meta['guidance_scale']:g}")))
|
|
832
|
+
res_items = [{"key": k, "asked": asked[k][0], "audio": asked[k][1], "source": asked[k][2],
|
|
833
|
+
"decoded": decoded.get(k)} for k, _ in files]
|
|
834
|
+
res_items += [{"key": it["key"], "asked": it["asked"] or {}, "audio": bench["audio"], "source": "bench clip",
|
|
835
|
+
"decoded": None, "error": it["error"],
|
|
836
|
+
"problems": [it["error"]] if it["state"] == "changed" else []}
|
|
837
|
+
for it in bench_items if it["state"] != "ok"]
|
|
838
|
+
if not res_items and bench is not None and why_not is None:
|
|
839
|
+
why_not = "the bench clip record has no timed clip (every clip failed?), so none was decoded"
|
|
840
|
+
g = gate_resolution(res_items, decoder, why_not, left_out)
|
|
841
|
+
g["applicable"] = bool(files) or bench is not None # a bench record's clips are expected to be decoded
|
|
842
|
+
gates.append(g)
|
|
843
|
+
if judge is None:
|
|
844
|
+
for name in ("seed_honoured", "prompt_sensitivity"):
|
|
845
|
+
gates.append(live(_gate(name, "NOT RUN", not_run)))
|
|
846
|
+
else:
|
|
847
|
+
gates.append(live(gate_seed(judge)))
|
|
848
|
+
gates.append(live(gate_changed(judge, "prompt_sensitivity", "prompt", "a one-word change")))
|
|
849
|
+
g = gate_no_cache(judge, threshold, static_cache(bench) if bench else None)
|
|
850
|
+
g["applicable"] = ep is not None or bench is not None
|
|
851
|
+
gates.append(g)
|
|
852
|
+
if why_bytes and judge is not None:
|
|
853
|
+
for g in gates:
|
|
854
|
+
if g["gate"] in LIVE:
|
|
855
|
+
g["limitations"].append(why_bytes)
|
|
856
|
+
|
|
857
|
+
log = [probe_row(p, decoded.get(p.key)) for p in probes.values()]
|
|
858
|
+
return _record(rcall, store, staging, gates, log=log, warm=warm, label=label, model=model,
|
|
859
|
+
engine_version=engine_version, endpoint_kind=endpoint_kind if ep else None, clipset=clipset,
|
|
860
|
+
clip=clip, probe_seed=probe_seed, decoder=decoder, dist=dist, judge=judge, bench=bench,
|
|
861
|
+
threshold=threshold, python_given=bool(python), seq=[p.key for p in seq])
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _metrics_of(gates: list[dict]) -> list[dict]:
|
|
865
|
+
out = []
|
|
866
|
+
for g in gates:
|
|
867
|
+
for k, v in g["metrics"].items():
|
|
868
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)):
|
|
869
|
+
continue
|
|
870
|
+
unit = ("x" if "ratio" in k or "speedup" in k else "levels" if "mad" in k else "dB" if "psnr" in k
|
|
871
|
+
else "count")
|
|
872
|
+
out.append(ev.metric(f"{g['gate']}.{k}", v, unit, "median" if "median" in k else
|
|
873
|
+
"min" if k.endswith("_min") else "value" if unit != "count" else "count"))
|
|
874
|
+
return out
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _record(rcall, store, staging, gates, *, log, warm, label, model, engine_version, endpoint_kind, clipset, clip,
|
|
878
|
+
probe_seed, decoder, dist, judge, bench, threshold, python_given, seq=()) -> dict:
|
|
879
|
+
verdict = overall(gates)
|
|
880
|
+
artifacts = []
|
|
881
|
+
if log:
|
|
882
|
+
p = staging / "probes.jsonl"
|
|
883
|
+
p.write_text("".join(json.dumps(ev.redact(x), ensure_ascii=False) + "\n" for x in log), encoding="utf-8")
|
|
884
|
+
artifacts.append(ev.artifact(p, staging, "probe requests and replies"))
|
|
885
|
+
files = {f"probes/{x['key']}.{x['format']}": x["out_sha256"] for x in log if x["ok"]}
|
|
886
|
+
idx = staging / "outputs" / label / "index.json"
|
|
887
|
+
idx.parent.mkdir(parents=True, exist_ok=True)
|
|
888
|
+
idx.write_text(json.dumps({"label": label, "files": dict(sorted(files.items()))}, indent=1) + "\n")
|
|
889
|
+
artifacts.append(ev.artifact(idx, staging, "outputs_index"))
|
|
890
|
+
for f in (staging / "worker").glob("*.results.json") if (staging / "worker").is_dir() else []:
|
|
891
|
+
f.unlink()
|
|
892
|
+
lim = [f"{g['gate']}: {x}" for g in gates for x in g["limitations"]]
|
|
893
|
+
if clip is not None:
|
|
894
|
+
lim.append(f"probes use one clip of the set ({clip['id']}, {clip['task']}); run again with --clip-id for "
|
|
895
|
+
"another task or shape")
|
|
896
|
+
if judge is not None and judge.pairs > 1:
|
|
897
|
+
lim.append(f"probe groups after the first use the clip's prompt plus '{PROBE_SUFFIX.format(n='N').strip()}'"
|
|
898
|
+
+ (" and the clip's picture as another file" if clip and clip.get("task") == "i2v" else "")
|
|
899
|
+
+ ", so no probe follows one with the same prompt or image file")
|
|
900
|
+
if clip is not None and clip.get("task") == "i2v" and log and \
|
|
901
|
+
image_variant(clipset.images[clip["id"]][0], "check") is None:
|
|
902
|
+
lim.append("the clip's image is not a PNG or JPEG file, so every probe sends the same file: an engine that "
|
|
903
|
+
"keeps the last run's image encoding (ComfyUI) reuses it and fails no_cache")
|
|
904
|
+
bad_warm = [w for w in warm if not w["ok"]]
|
|
905
|
+
if bad_warm:
|
|
906
|
+
lim.append(f"{len(bad_warm)} warm-up clip(s) failed ({bad_warm[0]['error']})")
|
|
907
|
+
missing = [g["gate"] for g in gates if g["verdict"] == "NOT RUN"]
|
|
908
|
+
summary = f"gates clip {verdict}: " + ", ".join(f"{g['gate']} {g['verdict']}" for g in gates)
|
|
909
|
+
workload = {"mode": "single", "batch": 1, "endpoint_kind": endpoint_kind}
|
|
910
|
+
if clip is not None:
|
|
911
|
+
workload.update({k: clip.get(k) for k in bc.SHAPE_KEYS})
|
|
912
|
+
workload["clip_set"] = clipset.identity()
|
|
913
|
+
workload["audio"] = clip.get("audio")
|
|
914
|
+
ident = ev.identity(engine={"name": label, "version": engine_version}, model={"id": model}, workload=workload)
|
|
915
|
+
data = {"verdict": verdict, "gates": {g["gate"]: g for g in gates}, "gates_format": FORMAT, "label": label,
|
|
916
|
+
"not_run_but_expected": [g["gate"] for g in gates if g["verdict"] == "NOT RUN" and g.get("applicable")],
|
|
917
|
+
"probe_seed": probe_seed, "probe_clip": clip["id"] if clip else None, "warmup": warm,
|
|
918
|
+
"probe_order": list(seq), "neighbour_clashes": judge.clashes if judge else None,
|
|
919
|
+
"decoder": decoder, "pixels_compared": dist is not None, "python_given": python_given,
|
|
920
|
+
"repeat_floor_mad": judge.floor if judge else None,
|
|
921
|
+
"bench_evidence": bench["record"]["evidence_id"] if bench else None,
|
|
922
|
+
"thresholds": {"replay_speedup_max": 1 + threshold, "step_ratio_pass": STEP_RATIO_PASS,
|
|
923
|
+
"step_ratio_fail": STEP_RATIO_FAIL, "sensitivity_x_floor": SENSITIVITY,
|
|
924
|
+
"min_mad": MIN_MAD, "repro_share": REPRO_SHARE, "fps_tol": FPS_TOL,
|
|
925
|
+
"audio_tol_s": AUDIO_TOL_S},
|
|
926
|
+
"definitions": {"mad": "mean absolute difference of decoded RGB24 pixels, 0-255, over the frames of both "
|
|
927
|
+
"clips", "repeat_floor_mad": "largest MAD between a request and its replay"}}
|
|
928
|
+
rec = ev.build(rcall, kind=KIND, claim_class="observed", summary=summary, identity_=ident,
|
|
929
|
+
metrics=_metrics_of(gates), artifacts=artifacts, missing=missing, limitations=lim, data=data)
|
|
930
|
+
return store.save(rec, staging)
|