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,844 @@
|
|
|
1
|
+
"""Read a run directory (``runs/<run-id>/``) into the rows and columns of the review page.
|
|
2
|
+
|
|
3
|
+
Layout read:
|
|
4
|
+
|
|
5
|
+
inputs/<bucket>/<id>.json | .txt | .wav exact request bodies, texts or audio
|
|
6
|
+
outputs/<engine>/<bucket>/<id>.json | .txt | .wav
|
|
7
|
+
metrics/<engine>.json | .jsonl speed + quality per input and per bucket
|
|
8
|
+
manifest.json, intake.yaml optional: engine order, roles, modality
|
|
9
|
+
|
|
10
|
+
Video clips (bench clip's layout) use the round as the bucket: ``outputs/<engine>/r<k>/<id>.mp4``,
|
|
11
|
+
the exact request body each engine was sent at ``inputs/<engine>/r<k>/<id>.json`` (rows come from
|
|
12
|
+
those when there is no ``inputs/r<k>/<id>.json``), ``metrics/<engine>.clip.json`` for the speed
|
|
13
|
+
summary, and per-clip times and self-reported stages from the bench clip record's ``requests`` rows.
|
|
14
|
+
|
|
15
|
+
Everything is read defensively, by field presence: a missing file is a NOT RUN cell, an
|
|
16
|
+
unreadable one is a FAILED cell with the reason, and unknown fields are ignored. The
|
|
17
|
+
``metrics/<engine>.json`` fields this module understands are listed in ``load_metrics``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
BUCKET_ORDER = ("very_short", "short", "medium", "long")
|
|
28
|
+
AUDIO_EXT = (".wav", ".flac", ".mp3", ".ogg", ".opus")
|
|
29
|
+
VIDEO_EXT = (".mp4", ".webm", ".mov", ".mkv", ".m4v")
|
|
30
|
+
IMAGE_EXT = (".png",)
|
|
31
|
+
MODALITIES = ("llm", "tts", "stt", "video", "image")
|
|
32
|
+
ROUND_DIR = re.compile(r"^r\d+$") # bench clip: outputs/<label>/r<round>/<id>.<fmt>
|
|
33
|
+
# What a clip request body says about the clip (bench clip's inputs/<label>/r<k>/<id>.json), shown with the input.
|
|
34
|
+
CLIP_FIELDS = ("task", "width", "height", "num_frames", "fps", "steps", "guidance_scale", "seed", "noise_seed",
|
|
35
|
+
"audio", "negative_prompt", "sampler", "shift")
|
|
36
|
+
GROUND_TRUTH_SUFFIXES = ("gt.txt", "ref.txt", "truth.txt", "transcript.txt")
|
|
37
|
+
REFERENCE_DEFAULT = "reference"
|
|
38
|
+
_NATURAL = re.compile(r"(\d+)")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def natural_key(s: str):
|
|
42
|
+
return [int(p) if p.isdigit() else p.lower() for p in _NATURAL.split(s)]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def bucket_key(b: str):
|
|
46
|
+
# Known buckets in their order, then the rest in natural order (bench clip's rounds: r2 before r10).
|
|
47
|
+
return (BUCKET_ORDER.index(b), []) if b in BUCKET_ORDER else (len(BUCKET_ORDER), natural_key(b))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Item:
|
|
52
|
+
"""One input (a row): what was sent, and the ground truth when there is one."""
|
|
53
|
+
|
|
54
|
+
id: str
|
|
55
|
+
bucket: str
|
|
56
|
+
text: str | None = None # the prompt / the text to speak, flattened for display
|
|
57
|
+
messages: list | None = None # chat messages, when the input is a chat request body
|
|
58
|
+
audio: str | None = None # run-dir-relative path of an input audio file
|
|
59
|
+
ground_truth: str | None = None # expected transcript (STT) or the text itself (TTS)
|
|
60
|
+
clip: dict | None = None # video: what the clip request asked for (task, shape, steps, seed, image)
|
|
61
|
+
files: list[str] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Cell:
|
|
66
|
+
"""One engine's output for one input."""
|
|
67
|
+
|
|
68
|
+
engine: str
|
|
69
|
+
status: str = "not_run" # ok | not_run | failed
|
|
70
|
+
reason: str = ""
|
|
71
|
+
text: str | None = None
|
|
72
|
+
token_ids: list | None = None
|
|
73
|
+
pieces: list[str] | None = None # per-token text, when the output carries it (logprobs)
|
|
74
|
+
audio: str | None = None
|
|
75
|
+
video: str | None = None # run-dir-relative path of the generated clip
|
|
76
|
+
image: str | None = None
|
|
77
|
+
pcm: str | None = None # raw PCM needs verified index metadata before playback
|
|
78
|
+
clip_row: dict | None = None # video: bench clip's row for this clip (client time, stages, effective)
|
|
79
|
+
transcript: str | None = None # ASR of generated audio (TTS) or the transcript itself (STT)
|
|
80
|
+
finish_reason: str | None = None
|
|
81
|
+
reasoning: str | None = None # a reasoning model's separate thinking text, when the output has it
|
|
82
|
+
metrics: dict = field(default_factory=dict)
|
|
83
|
+
files: list[str] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class EngineInfo:
|
|
88
|
+
label: str
|
|
89
|
+
role: str | None = None
|
|
90
|
+
metrics: dict = field(default_factory=dict) # top-level fields of metrics/<engine>.json
|
|
91
|
+
per_input: dict = field(default_factory=dict) # input id -> {metric: value}
|
|
92
|
+
per_bucket: dict = field(default_factory=dict) # bucket -> {metric: value}
|
|
93
|
+
metrics_file: str | None = None
|
|
94
|
+
# From evidence records about this label (run-dir evidence/ or --evidence):
|
|
95
|
+
ev_quality: list = field(default_factory=list) # [{name, verdict, value, unit, evidence}]
|
|
96
|
+
ev_gates: dict | None = None # {verdict, violations, evidence}
|
|
97
|
+
capacity: dict | None = None # {n_star, reason, level_kind, slo, usd_per_1m, price, ...}
|
|
98
|
+
evidence: list = field(default_factory=list) # evidence ids that mention this label
|
|
99
|
+
clip_rows: dict = field(default_factory=dict) # video: "r<k>/<id>" -> bench clip request row
|
|
100
|
+
stages: dict = field(default_factory=dict) # video: stage -> median ms over clips (self-reported)
|
|
101
|
+
media_rows: dict = field(default_factory=dict)
|
|
102
|
+
media_kind: str | None = None
|
|
103
|
+
metric_classes: dict = field(default_factory=dict)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class Run:
|
|
108
|
+
root: Path
|
|
109
|
+
modality: str
|
|
110
|
+
engines: list[EngineInfo]
|
|
111
|
+
reference: str | None
|
|
112
|
+
items: list[Item]
|
|
113
|
+
cells: dict[tuple[str, str], Cell]
|
|
114
|
+
warnings: list[str] = field(default_factory=list)
|
|
115
|
+
sources: list[str] = field(default_factory=list) # run-relative paths read to build the page
|
|
116
|
+
evidence_attached: int = 0 # evidence records read (evidence/, --evidence, the store)
|
|
117
|
+
|
|
118
|
+
def buckets(self) -> list[str]:
|
|
119
|
+
return sorted({i.bucket for i in self.items}, key=bucket_key)
|
|
120
|
+
|
|
121
|
+
def engine(self, label: str) -> EngineInfo | None:
|
|
122
|
+
return next((e for e in self.engines if e.label == label), None)
|
|
123
|
+
|
|
124
|
+
def key(self, item: Item) -> str:
|
|
125
|
+
return f"{item.bucket}/{item.id}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# --------------------------------------------------------------------------- helpers
|
|
129
|
+
|
|
130
|
+
def _read_json(path: Path):
|
|
131
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _split_name(name: str) -> tuple[str, str]:
|
|
135
|
+
"""'short-0007.gt.txt' -> ('short-0007', 'gt.txt'). A clip ('scene.01.mp4' -> ('scene.01', 'mp4')) keeps
|
|
136
|
+
every dot but the last: bench clip ids may contain dots, and a clip file is always <id>.<video ext>."""
|
|
137
|
+
if Path(name).suffix.lower() in VIDEO_EXT + IMAGE_EXT + (".pcm",):
|
|
138
|
+
return name[:-len(Path(name).suffix)], Path(name).suffix.lower()[1:]
|
|
139
|
+
stem, _, suffix = name.partition(".")
|
|
140
|
+
return stem, suffix.lower()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def flatten_messages(messages) -> str:
|
|
144
|
+
parts = []
|
|
145
|
+
for m in messages or []:
|
|
146
|
+
if not isinstance(m, dict):
|
|
147
|
+
continue
|
|
148
|
+
content = m.get("content")
|
|
149
|
+
if isinstance(content, list): # OpenAI content parts
|
|
150
|
+
content = " ".join(str(p.get("text", "")) for p in content if isinstance(p, dict))
|
|
151
|
+
parts.append(f"{m.get('role', '?')}: {content if content is not None else ''}")
|
|
152
|
+
return "\n\n".join(parts)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def token_pieces(obj: dict) -> list[str] | None:
|
|
156
|
+
"""Per-token text of an output, from explicit fields or OpenAI-style logprobs."""
|
|
157
|
+
for key in ("token_texts", "tokens"):
|
|
158
|
+
v = obj.get(key)
|
|
159
|
+
if isinstance(v, list) and v and all(isinstance(x, str) for x in v):
|
|
160
|
+
return v
|
|
161
|
+
lp = obj.get("logprobs")
|
|
162
|
+
if isinstance(lp, dict):
|
|
163
|
+
content = lp.get("content")
|
|
164
|
+
if isinstance(content, list) and content and all(isinstance(c, dict) and "token" in c for c in content):
|
|
165
|
+
return [str(c["token"]) for c in content]
|
|
166
|
+
toks = lp.get("tokens")
|
|
167
|
+
if isinstance(toks, list) and toks and all(isinstance(x, str) for x in toks):
|
|
168
|
+
return toks
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _output_text(obj: dict) -> str | None:
|
|
173
|
+
if isinstance(obj.get("text"), str):
|
|
174
|
+
return obj["text"]
|
|
175
|
+
choices = obj.get("choices") # a raw OpenAI response saved as-is
|
|
176
|
+
if isinstance(choices, list) and choices and isinstance(choices[0], dict):
|
|
177
|
+
c = choices[0]
|
|
178
|
+
if isinstance(c.get("message"), dict) and isinstance(c["message"].get("content"), str):
|
|
179
|
+
return c["message"]["content"]
|
|
180
|
+
if isinstance(c.get("text"), str):
|
|
181
|
+
return c["text"]
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _ids(value) -> list | None:
|
|
186
|
+
return value if isinstance(value, list) and all(isinstance(x, int) for x in value) else None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def detect_modality(root: Path, items: list[Item], cells: dict) -> str:
|
|
190
|
+
for name in ("manifest.json", "run.json"):
|
|
191
|
+
p = root / name
|
|
192
|
+
if p.is_file():
|
|
193
|
+
try:
|
|
194
|
+
m = _read_json(p)
|
|
195
|
+
mod = m.get("modality") or (m.get("model") or {}).get("modality")
|
|
196
|
+
if mod in MODALITIES:
|
|
197
|
+
return mod
|
|
198
|
+
except (OSError, ValueError, AttributeError):
|
|
199
|
+
pass
|
|
200
|
+
intake = root / "intake.yaml"
|
|
201
|
+
if intake.is_file(): # one top-level key; no YAML parser needed for it
|
|
202
|
+
found = re.search(r"(?m)^modality:\s*['\"]?(llm|tts|stt|video|image)\b", intake.read_text(errors="replace"))
|
|
203
|
+
if found:
|
|
204
|
+
return found.group(1)
|
|
205
|
+
if any(c.video for c in cells.values()):
|
|
206
|
+
return "video"
|
|
207
|
+
if any(c.image for c in cells.values()) or any((root / "metrics").glob("*.image.json")):
|
|
208
|
+
return "image"
|
|
209
|
+
if any(c.audio or c.pcm for c in cells.values()) or any((root / "metrics").glob("*.audio.json")):
|
|
210
|
+
return "tts"
|
|
211
|
+
if any(i.audio for i in items):
|
|
212
|
+
return "stt"
|
|
213
|
+
return "llm"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# --------------------------------------------------------------------------- loading
|
|
217
|
+
|
|
218
|
+
def load_prompt_set(path: Path, items: dict[str, Item], run: Run) -> None:
|
|
219
|
+
"""Fill rows from a prompt-set JSONL ({"id", "bucket", "messages" | "prompt" | "text"} per line)."""
|
|
220
|
+
from inferencekit import prompts as sets
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
lines = sets.read_text(sets.existing(path)).splitlines() # .jsonl or .jsonl.gz
|
|
224
|
+
except (OSError, ValueError) as e: # missing, corrupt .jsonl.gz, or not UTF-8
|
|
225
|
+
run.warnings.append(f"prompt set {path.name}: unreadable ({e})")
|
|
226
|
+
return
|
|
227
|
+
for n, line in enumerate(lines, 1):
|
|
228
|
+
if not line.strip():
|
|
229
|
+
continue
|
|
230
|
+
try:
|
|
231
|
+
obj = json.loads(line)
|
|
232
|
+
except ValueError:
|
|
233
|
+
run.warnings.append(f"prompt set {path.name}:{n}: not JSON")
|
|
234
|
+
continue
|
|
235
|
+
if not isinstance(obj, dict) or not obj.get("id"):
|
|
236
|
+
continue
|
|
237
|
+
# Unbucketed media fixtures apply to every observed round of that id.
|
|
238
|
+
targets = [i for i in items.values() if i.id == str(obj["id"]) and ROUND_DIR.match(i.bucket)] \
|
|
239
|
+
if not obj.get("bucket") else []
|
|
240
|
+
if not targets:
|
|
241
|
+
targets = [items.setdefault(f"{obj.get('bucket') or 'unbucketed'}/{obj['id']}",
|
|
242
|
+
Item(id=str(obj["id"]), bucket=str(obj.get("bucket") or "unbucketed")))]
|
|
243
|
+
for item in targets:
|
|
244
|
+
if item.text is None:
|
|
245
|
+
if isinstance(obj.get("messages"), list):
|
|
246
|
+
item.messages = obj["messages"]
|
|
247
|
+
item.text = flatten_messages(obj["messages"])
|
|
248
|
+
elif isinstance(obj.get("prompt") or obj.get("text"), str):
|
|
249
|
+
item.text = obj.get("prompt") or obj.get("text")
|
|
250
|
+
if item.ground_truth is None and isinstance(obj.get("ground_truth"), str):
|
|
251
|
+
item.ground_truth = obj["ground_truth"]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def load_inputs(root: Path, run: Run) -> dict[str, Item]:
|
|
255
|
+
items: dict[str, Item] = {}
|
|
256
|
+
base = root / "inputs"
|
|
257
|
+
if not base.is_dir():
|
|
258
|
+
return items
|
|
259
|
+
# bench serve also keeps each engine's exact request bodies at inputs/<label>/<bucket>/<id>.json (and
|
|
260
|
+
# inputs/<label>/load-<level>/...); those are per-engine copies, not inputs: only inputs/<bucket>/<id>
|
|
261
|
+
# (the shared path) and inputs/<id> are rows.
|
|
262
|
+
out_dir = root / "outputs"
|
|
263
|
+
labels = {d.name for d in out_dir.iterdir() if d.is_dir()} if out_dir.is_dir() else set()
|
|
264
|
+
for ending in (".clip.json", ".image.json", ".audio.json"):
|
|
265
|
+
labels.update(p.name[:-len(ending)] for p in (root / "metrics").glob(f"*{ending}"))
|
|
266
|
+
clip_copies: dict[str, list[tuple[str, dict]]] = {} # "r<k>/<id>" -> [(path, clip request body)] per engine
|
|
267
|
+
for path in sorted(p for p in base.rglob("*") if p.is_file()):
|
|
268
|
+
rel = path.relative_to(base)
|
|
269
|
+
if (len(rel.parts) == 3 and rel.parts[0] in labels and ROUND_DIR.match(rel.parts[1])
|
|
270
|
+
and path.suffix == ".json"):
|
|
271
|
+
body = _clip_body(path) # bench clip: the exact body this engine was sent for this clip and round
|
|
272
|
+
if body is not None: # <id>.json, and the id may contain dots
|
|
273
|
+
clip_copies.setdefault(f"{rel.parts[1]}/{path.name[:-len('.json')]}", []).append(
|
|
274
|
+
(path.relative_to(root).as_posix(), body))
|
|
275
|
+
continue
|
|
276
|
+
if len(rel.parts) > 2 or (len(rel.parts) == 2 and rel.parts[0] in labels):
|
|
277
|
+
continue
|
|
278
|
+
bucket = rel.parts[0] if len(rel.parts) > 1 else "unbucketed"
|
|
279
|
+
stem, suffix = _split_name(path.name)
|
|
280
|
+
if ROUND_DIR.match(bucket) and path.suffix == ".json": # a clip request r<k>/<id>.json: the id may hold dots
|
|
281
|
+
stem, suffix = path.name[:-len(".json")], "json"
|
|
282
|
+
item = items.setdefault(f"{bucket}/{stem}", Item(id=stem, bucket=bucket))
|
|
283
|
+
rel_root = path.relative_to(root).as_posix()
|
|
284
|
+
item.files.append(rel_root)
|
|
285
|
+
run.sources.append(rel_root)
|
|
286
|
+
try:
|
|
287
|
+
if suffix in GROUND_TRUTH_SUFFIXES:
|
|
288
|
+
item.ground_truth = path.read_text(encoding="utf-8", errors="replace").strip()
|
|
289
|
+
elif suffix == "json":
|
|
290
|
+
body = _read_json(path)
|
|
291
|
+
if isinstance(body, dict):
|
|
292
|
+
if isinstance(body.get("messages"), list):
|
|
293
|
+
item.messages = body["messages"]
|
|
294
|
+
item.text = flatten_messages(body["messages"])
|
|
295
|
+
elif isinstance(body.get("prompt"), str):
|
|
296
|
+
item.text = body["prompt"]
|
|
297
|
+
elif isinstance(body.get("text"), str) or isinstance(body.get("input"), str):
|
|
298
|
+
item.text = body.get("text") if isinstance(body.get("text"), str) else body["input"]
|
|
299
|
+
if "num_frames" in body or ("width" in body and "height" in body):
|
|
300
|
+
item.clip = clip_spec(body)
|
|
301
|
+
for k in ("ground_truth", "reference_text", "transcript"):
|
|
302
|
+
if isinstance(body.get(k), str):
|
|
303
|
+
item.ground_truth = body[k]
|
|
304
|
+
break
|
|
305
|
+
elif suffix == "txt":
|
|
306
|
+
item.text = path.read_text(encoding="utf-8", errors="replace")
|
|
307
|
+
elif path.suffix.lower() in AUDIO_EXT and not item.audio:
|
|
308
|
+
item.audio = rel_root
|
|
309
|
+
except (OSError, ValueError) as e:
|
|
310
|
+
run.warnings.append(f"{rel_root}: unreadable ({e})")
|
|
311
|
+
for key, copies in sorted(clip_copies.items()):
|
|
312
|
+
if key in items: # the shared path inputs/r<k>/<id>.json wins over the per-engine copies
|
|
313
|
+
continue
|
|
314
|
+
bucket, _, cid = key.partition("/")
|
|
315
|
+
first_path, body = copies[0]
|
|
316
|
+
item = items[key] = Item(id=cid, bucket=bucket, text=body["prompt"] if isinstance(body.get("prompt"), str)
|
|
317
|
+
else None, clip=clip_spec(body), files=[p for p, _ in copies])
|
|
318
|
+
run.sources += item.files
|
|
319
|
+
differ = [p for p, b in copies[1:] if clip_spec(b) != item.clip or b.get("prompt") != body.get("prompt")]
|
|
320
|
+
if differ:
|
|
321
|
+
run.warnings.append(f"{key}: the engines were not sent the same clip request ({first_path} differs from "
|
|
322
|
+
f"{', '.join(differ[:3])}): their clips are not comparable")
|
|
323
|
+
for item in items.values():
|
|
324
|
+
if item.audio and item.ground_truth is None and item.text is not None and item.messages is None:
|
|
325
|
+
item.ground_truth = item.text.strip() # STT: the .txt next to the audio is its transcript
|
|
326
|
+
return items
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _clip_body(path: Path) -> dict | None:
|
|
330
|
+
"""A saved video/image request body, or None for e.g. a chat request copy."""
|
|
331
|
+
try:
|
|
332
|
+
body = _read_json(path)
|
|
333
|
+
except (OSError, ValueError):
|
|
334
|
+
return None
|
|
335
|
+
return body if isinstance(body, dict) and ("num_frames" in body or
|
|
336
|
+
("width" in body and "height" in body)) else None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def clip_spec(body: dict) -> dict:
|
|
340
|
+
"""The fields of a clip request shown with the input (the image as its sha256 and size, never its bytes)."""
|
|
341
|
+
spec = {k: body[k] for k in CLIP_FIELDS if k in body and isinstance(body[k], (str, int, float, bool))}
|
|
342
|
+
image = body.get("image")
|
|
343
|
+
if isinstance(image, dict) and isinstance(image.get("sha256"), str):
|
|
344
|
+
spec["image_sha256"] = image["sha256"]
|
|
345
|
+
if isinstance(image.get("bytes"), int):
|
|
346
|
+
spec["image_bytes"] = image["bytes"]
|
|
347
|
+
return spec
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def load_metrics(root: Path, label: str, run: Run) -> EngineInfo:
|
|
351
|
+
"""metrics/<label>.json or .jsonl.
|
|
352
|
+
|
|
353
|
+
Understood fields (all optional):
|
|
354
|
+
per_input | inputs {id: {metric: value}} or [{"id": ..., metric: value}]
|
|
355
|
+
per_bucket | buckets {bucket: {metric: value}}
|
|
356
|
+
quality {name: verdict | {"verdict", "value", "unit"}} or a list of those
|
|
357
|
+
gates "PASS" or {"verdict", "violations"}
|
|
358
|
+
cost {"usd_per_1m_output_tokens" | "usd_per_1m": x, "gpu_price_usd_h": y} or a number
|
|
359
|
+
ceiling_tok_s, pct_of_ceiling, role, evidence (list of ids)
|
|
360
|
+
A value is a number or {"value"|"median": x, "ci95": [lo, hi]}. A .jsonl file holds one
|
|
361
|
+
{"id": ..., metric: value} line per input. Video: ``metrics/<label>.clip.json`` (bench clip:
|
|
362
|
+
{"speed": {"all": {clip_ms, clips_per_hour}, "stages_self_reported": ...}, "rounds": [...]}) is
|
|
363
|
+
read as well, for the summary row and the per-round values.
|
|
364
|
+
"""
|
|
365
|
+
info = EngineInfo(label=label)
|
|
366
|
+
load_clip_metrics(root, label, info, run)
|
|
367
|
+
jpath, lpath = root / "metrics" / f"{label}.json", root / "metrics" / f"{label}.jsonl"
|
|
368
|
+
try:
|
|
369
|
+
if jpath.is_file():
|
|
370
|
+
data = _read_json(jpath)
|
|
371
|
+
info.metrics_file = jpath.relative_to(root).as_posix()
|
|
372
|
+
if not isinstance(data, dict):
|
|
373
|
+
raise ValueError("not a JSON object")
|
|
374
|
+
per_input = data.get("per_input", data.get("inputs"))
|
|
375
|
+
per_bucket = data.get("per_bucket", data.get("buckets"))
|
|
376
|
+
speed = data.get("speed") # bench serve: {"speed": {"per_bucket": ..., "per_input_median": ...}}
|
|
377
|
+
if isinstance(speed, dict):
|
|
378
|
+
per_input = per_input if per_input is not None else speed.get("per_input_median",
|
|
379
|
+
speed.get("per_input"))
|
|
380
|
+
per_bucket = per_bucket if per_bucket is not None else speed.get("per_bucket")
|
|
381
|
+
info.metrics = {k: v for k, v in data.items()
|
|
382
|
+
if k not in ("per_input", "inputs", "per_bucket", "buckets", "speed")}
|
|
383
|
+
elif lpath.is_file():
|
|
384
|
+
info.metrics_file = lpath.relative_to(root).as_posix()
|
|
385
|
+
per_input = [json.loads(line) for line in lpath.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
386
|
+
per_bucket = None
|
|
387
|
+
else:
|
|
388
|
+
return info
|
|
389
|
+
except (OSError, ValueError) as e:
|
|
390
|
+
run.warnings.append(f"metrics/{label}: unreadable ({e})")
|
|
391
|
+
return info
|
|
392
|
+
run.sources.append(info.metrics_file)
|
|
393
|
+
clip_bucket = dict(info.per_bucket) # what the clip summary filled; the metrics file adds to it
|
|
394
|
+
if isinstance(per_input, dict):
|
|
395
|
+
info.per_input = {str(k): v for k, v in per_input.items() if isinstance(v, dict)}
|
|
396
|
+
elif isinstance(per_input, list):
|
|
397
|
+
for row in per_input:
|
|
398
|
+
if isinstance(row, dict) and (row.get("id") or row.get("input_id")):
|
|
399
|
+
rid = str(row.get("id") or row.get("input_id"))
|
|
400
|
+
info.per_input[rid] = {k: v for k, v in row.items() if k not in ("id", "input_id")}
|
|
401
|
+
if isinstance(per_bucket, dict):
|
|
402
|
+
info.per_bucket = {**clip_bucket, **{str(k): v for k, v in per_bucket.items() if isinstance(v, dict)}}
|
|
403
|
+
if isinstance(info.metrics.get("role"), str):
|
|
404
|
+
info.role = info.metrics["role"]
|
|
405
|
+
return info
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def load_clip_metrics(root: Path, label: str, info: EngineInfo, run: Run) -> None:
|
|
409
|
+
"""metrics/<label>.clip.json (bench clip --run-dir): the 'all' speed (clip time in s, clips per hour), each
|
|
410
|
+
round's values as that round's bucket, and the self-reported stage medians. All observed or derived by bench
|
|
411
|
+
clip; the page converts ms to s only."""
|
|
412
|
+
path = root / "metrics" / f"{label}.clip.json"
|
|
413
|
+
if not path.is_file():
|
|
414
|
+
return
|
|
415
|
+
try:
|
|
416
|
+
data = _read_json(path)
|
|
417
|
+
if not isinstance(data, dict):
|
|
418
|
+
raise ValueError("not a JSON object")
|
|
419
|
+
except (OSError, ValueError) as e:
|
|
420
|
+
run.warnings.append(f"metrics/{label}.clip.json: unreadable ({e})")
|
|
421
|
+
return
|
|
422
|
+
rel = path.relative_to(root).as_posix()
|
|
423
|
+
run.sources.append(rel)
|
|
424
|
+
info.metrics_file = info.metrics_file or rel
|
|
425
|
+
speed = data.get("speed") if isinstance(data.get("speed"), dict) else {}
|
|
426
|
+
overall = speed.get("all") if isinstance(speed.get("all"), dict) else {}
|
|
427
|
+
row = {}
|
|
428
|
+
cm = overall.get("clip_ms")
|
|
429
|
+
if isinstance(cm, dict) and isinstance(cm.get("median"), (int, float)):
|
|
430
|
+
ci = cm.get("ci95_median")
|
|
431
|
+
ok = isinstance(ci, list) and len(ci) == 2 and all(isinstance(c, (int, float)) for c in ci)
|
|
432
|
+
row["clip_s"] = {"value": cm["median"] / 1000, "ci95": [ci[0] / 1000, ci[1] / 1000] if ok else None}
|
|
433
|
+
cph = overall.get("clips_per_hour")
|
|
434
|
+
if isinstance(cph, dict) and isinstance(cph.get("median"), (int, float)):
|
|
435
|
+
ci = cph.get("ci95_median")
|
|
436
|
+
row["clips_per_hour"] = {"value": cph["median"], "ci95": ci if isinstance(ci, list) and len(ci) == 2
|
|
437
|
+
and all(isinstance(c, (int, float)) for c in ci) else None}
|
|
438
|
+
if row:
|
|
439
|
+
info.per_bucket["all"] = row
|
|
440
|
+
for r in data.get("rounds") or []:
|
|
441
|
+
if isinstance(r, dict) and isinstance(r.get("index"), int):
|
|
442
|
+
vals = {}
|
|
443
|
+
if isinstance(r.get("clip_ms"), (int, float)):
|
|
444
|
+
vals["clip_s"] = r["clip_ms"] / 1000
|
|
445
|
+
if isinstance(r.get("clips_per_hour"), (int, float)):
|
|
446
|
+
vals["clips_per_hour"] = r["clips_per_hour"]
|
|
447
|
+
if vals:
|
|
448
|
+
info.per_bucket[f"r{r['index']}"] = vals
|
|
449
|
+
stages = speed.get("stages_self_reported")
|
|
450
|
+
if isinstance(stages, dict):
|
|
451
|
+
info.stages = {str(k): float(v["median"]) for k, v in stages.items()
|
|
452
|
+
if isinstance(v, dict) and isinstance(v.get("median"), (int, float))}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def load_cell(root: Path, engine: str, item: Item, run: Run) -> Cell:
|
|
456
|
+
cell = Cell(engine=engine)
|
|
457
|
+
folder = root / "outputs" / engine / item.bucket
|
|
458
|
+
if item.bucket == "unbucketed":
|
|
459
|
+
folder = root / "outputs" / engine
|
|
460
|
+
found = sorted(p for p in folder.glob(f"{item.id}.*") if p.is_file()) if folder.is_dir() else []
|
|
461
|
+
found = [p for p in found if _split_name(p.name)[0] == item.id or p.name == f"{item.id}.json"]
|
|
462
|
+
for path in found:
|
|
463
|
+
rel = path.relative_to(root).as_posix()
|
|
464
|
+
cell.files.append(rel)
|
|
465
|
+
run.sources.append(rel)
|
|
466
|
+
_, suffix = _split_name(path.name)
|
|
467
|
+
if path.name == f"{item.id}.json":
|
|
468
|
+
suffix = "json"
|
|
469
|
+
try:
|
|
470
|
+
if suffix == "json":
|
|
471
|
+
obj = _read_json(path)
|
|
472
|
+
if not isinstance(obj, dict):
|
|
473
|
+
raise ValueError("not a JSON object")
|
|
474
|
+
if obj.get("error"):
|
|
475
|
+
cell.status, cell.reason = "failed", str(obj["error"])
|
|
476
|
+
cell.text = _output_text(obj) if cell.text is None else cell.text
|
|
477
|
+
cell.token_ids = _ids(obj.get("token_ids")) or cell.token_ids
|
|
478
|
+
cell.pieces = token_pieces(obj) or cell.pieces
|
|
479
|
+
cell.finish_reason = obj.get("finish_reason") if isinstance(obj.get("finish_reason"), str) else None
|
|
480
|
+
for k in ("reasoning", "reasoning_content"):
|
|
481
|
+
if isinstance(obj.get(k), str) and obj[k]:
|
|
482
|
+
cell.reasoning = obj[k]
|
|
483
|
+
for k in ("transcript", "asr_text"):
|
|
484
|
+
if isinstance(obj.get(k), str):
|
|
485
|
+
cell.transcript = obj[k]
|
|
486
|
+
elif suffix in ("asr.txt", "transcript.txt"):
|
|
487
|
+
cell.transcript = path.read_text(encoding="utf-8", errors="replace")
|
|
488
|
+
elif suffix in GROUND_TRUTH_SUFFIXES:
|
|
489
|
+
continue
|
|
490
|
+
elif suffix == "txt":
|
|
491
|
+
if cell.text is None:
|
|
492
|
+
cell.text = path.read_text(encoding="utf-8", errors="replace")
|
|
493
|
+
elif path.suffix.lower() in AUDIO_EXT and not cell.audio:
|
|
494
|
+
cell.audio = rel
|
|
495
|
+
elif path.suffix.lower() in VIDEO_EXT and not cell.video:
|
|
496
|
+
cell.video = rel
|
|
497
|
+
elif path.suffix.lower() in IMAGE_EXT and not cell.image:
|
|
498
|
+
cell.image = rel
|
|
499
|
+
elif path.suffix.lower() == ".pcm" and not cell.pcm:
|
|
500
|
+
cell.pcm = rel
|
|
501
|
+
except (OSError, ValueError) as e:
|
|
502
|
+
cell.status, cell.reason = "failed", f"{rel}: unreadable ({e})"
|
|
503
|
+
info = run.engine(engine)
|
|
504
|
+
if info is not None:
|
|
505
|
+
cell.metrics = dict(info.per_input.get(item.id) or info.per_input.get(f"{item.bucket}/{item.id}") or {})
|
|
506
|
+
for k in ("transcript", "asr_text"):
|
|
507
|
+
if cell.transcript is None and isinstance(cell.metrics.get(k), str):
|
|
508
|
+
cell.transcript = cell.metrics[k]
|
|
509
|
+
if cell.metrics.get("error") and cell.status != "failed":
|
|
510
|
+
cell.status, cell.reason = "failed", str(cell.metrics["error"])
|
|
511
|
+
if cell.status == "failed":
|
|
512
|
+
return cell
|
|
513
|
+
if cell.text is not None or cell.audio or cell.video or cell.image or cell.pcm or cell.token_ids is not None:
|
|
514
|
+
cell.status = "ok"
|
|
515
|
+
elif found:
|
|
516
|
+
cell.status, cell.reason = "failed", "output file has no text, token ids, audio, video or image"
|
|
517
|
+
else:
|
|
518
|
+
cell.status, cell.reason = "not_run", "no output file for this input"
|
|
519
|
+
return cell
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _manifest_engines(root: Path) -> list[dict]:
|
|
523
|
+
p = root / "manifest.json"
|
|
524
|
+
if not p.is_file():
|
|
525
|
+
return []
|
|
526
|
+
try:
|
|
527
|
+
engines = _read_json(p).get("engines")
|
|
528
|
+
except (OSError, ValueError, AttributeError):
|
|
529
|
+
return []
|
|
530
|
+
return [e for e in engines if isinstance(e, dict)] if isinstance(engines, list) else []
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _label_of(rec: dict) -> str | None:
|
|
534
|
+
data = rec.get("data") or {}
|
|
535
|
+
label = data.get("label") or ((rec.get("identity") or {}).get("engine") or {}).get("name")
|
|
536
|
+
return str(label) if label else None
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _metric(rec: dict, names, subject=None) -> float | None:
|
|
540
|
+
for name in names:
|
|
541
|
+
for m in rec.get("metrics") or []:
|
|
542
|
+
if (isinstance(m, dict) and m.get("name") == name and isinstance(m.get("value"), (int, float))
|
|
543
|
+
and (subject is None or m.get("subject") in (subject, None))):
|
|
544
|
+
return float(m["value"])
|
|
545
|
+
return None
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def attach_evidence(run: Run, records: list[tuple[str, dict]]) -> None:
|
|
549
|
+
"""Quality, gate and capacity verdicts per engine label, from evidence records (v1 or v2, by field presence)."""
|
|
550
|
+
from inferencekit.evidence import integrity_problem
|
|
551
|
+
|
|
552
|
+
seen = set()
|
|
553
|
+
for source, rec in records:
|
|
554
|
+
if not isinstance(rec, dict) or not str(rec.get("evidence_id", "")).startswith("ev_"):
|
|
555
|
+
continue
|
|
556
|
+
if rec["evidence_id"] in seen: # the same record from evidence/ and --evidence counts once
|
|
557
|
+
continue
|
|
558
|
+
seen.add(rec["evidence_id"])
|
|
559
|
+
run.evidence_attached += 1
|
|
560
|
+
data = rec.get("data") or {}
|
|
561
|
+
kind = data.get("v2_kind") or ("gates" if data.get("gates_format") else rec.get("kind"))
|
|
562
|
+
eid = rec["evidence_id"]
|
|
563
|
+
problem = integrity_problem(rec)
|
|
564
|
+
if problem:
|
|
565
|
+
run.warnings.append(f"{source}: {problem}: its verdicts are shown as UNPROVEN")
|
|
566
|
+
touched = []
|
|
567
|
+
if kind == "quality_trajectory":
|
|
568
|
+
for label, verdict in (data.get("verdicts") or {}).items():
|
|
569
|
+
info = run.engine(str(label))
|
|
570
|
+
if info is not None:
|
|
571
|
+
info.ev_quality.append({"name": "top-1 agreement", "unit": "%", "evidence": eid,
|
|
572
|
+
"verdict": "UNPROVEN" if problem else str(verdict),
|
|
573
|
+
"value": _metric(rec, ("top1_agreement_pct",), str(label))})
|
|
574
|
+
touched.append(info)
|
|
575
|
+
elif kind == "clip_quality":
|
|
576
|
+
touched += clip_quality(run, rec, source, problem)
|
|
577
|
+
else:
|
|
578
|
+
info = run.engine(_label_of(rec) or "")
|
|
579
|
+
if info is None:
|
|
580
|
+
continue
|
|
581
|
+
touched.append(info)
|
|
582
|
+
if kind == "clip_benchmark" and data.get("source") == "bench clip":
|
|
583
|
+
for key, row in clip_rows(run, rec, source).items(): # merged: quality clip may have come first
|
|
584
|
+
info.clip_rows.setdefault(key, {}).update(row)
|
|
585
|
+
elif kind in ("image_benchmark", "audio_benchmark"):
|
|
586
|
+
from inferencekit.review.media import attach_rows, attach_verdicts
|
|
587
|
+
|
|
588
|
+
media_kind = "image" if kind == "image_benchmark" else "audio"
|
|
589
|
+
if data.get("source") == f"bench {media_kind}":
|
|
590
|
+
attach_verdicts(info, media_kind, data, eid, problem)
|
|
591
|
+
if not problem:
|
|
592
|
+
attach_rows(run, info, rec, source)
|
|
593
|
+
elif kind == "quality_task":
|
|
594
|
+
name = str(data.get("task") or "task")
|
|
595
|
+
if name == "sampling":
|
|
596
|
+
value, unit = _metric(rec, ("first_token_tv_excess",), info.label), "probability"
|
|
597
|
+
else:
|
|
598
|
+
value, unit = _metric(rec, (f"{name}_accuracy_pct", "accuracy_pct", "niah_accuracy_pct"),
|
|
599
|
+
info.label), "%"
|
|
600
|
+
info.ev_quality.append({"name": name, "unit": unit, "evidence": eid, "value": value,
|
|
601
|
+
"verdict": "UNPROVEN" if problem else data.get("verdict")})
|
|
602
|
+
elif kind in ("gates", "clip_gates"):
|
|
603
|
+
gates = data.get("gates") or {}
|
|
604
|
+
info.ev_gates = {"verdict": "UNPROVEN" if problem else str(data.get("verdict") or "UNPROVEN"),
|
|
605
|
+
"violations": [f"{g}: {v.get('verdict')}" for g, v in gates.items()
|
|
606
|
+
if isinstance(v, dict) and v.get("verdict") != "PASS"],
|
|
607
|
+
"evidence": eid}
|
|
608
|
+
elif kind == "capacity":
|
|
609
|
+
# bench serve writes n_star=None for several reasons (SLO missed at the first level, no SLO,
|
|
610
|
+
# closed loop, no levels); data.reason says which, data.level_kind whether it is streams or req/s.
|
|
611
|
+
star = data["n_star"] if "n_star" in data else _metric(rec, ("n_star", "rate_star_rps"))
|
|
612
|
+
level_kind = data.get("level_kind") or ("rate" if _metric(rec, ("rate_star_rps",)) is not None
|
|
613
|
+
else "concurrency")
|
|
614
|
+
info.capacity = {"n_star": star, "reason": str(data["reason"]) if data.get("reason") else None,
|
|
615
|
+
"level_kind": level_kind, "slo": data.get("slo"), "evidence": eid,
|
|
616
|
+
"usd_per_1m": _metric(rec, ("usd_per_1m_output_tokens",)),
|
|
617
|
+
"price": data.get("gpu_price_usd_h"), "edited": bool(problem)}
|
|
618
|
+
for info in touched:
|
|
619
|
+
if eid not in info.evidence:
|
|
620
|
+
info.evidence.append(eid)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# Frame-quality metrics of a clip_quality record (quality clip): (name, unit, names it may carry), mean over a
|
|
624
|
+
# clip's frames, then over clips; psnr_db is the alias other writers use.
|
|
625
|
+
CLIP_QUALITY = (("psnr", "dB", ("psnr", "psnr_db")), ("ssim", "", ("ssim",)), ("lpips", "", ("lpips",)))
|
|
626
|
+
CLIP_ROW_KEYS = ("ok", "error", "seed", "e2e_ms", "stage_ms", "stage_sum_ms", "stage_sum_ok", "steps_run", "w", "h",
|
|
627
|
+
"frames", "fps", "dtype", "attention", "effective_mismatch", "cache", "out_sha256")
|
|
628
|
+
_QUALITY_KEY = re.compile(r"^engine:(.+)\|(r\d+/[^/]+)$") # quality clip's clip_metrics rows: engine:<label>|r<k>/<id>
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def artifact_lines(run: Run, rec: dict, source: str, role: str, what: str) -> list[dict] | None:
|
|
632
|
+
"""The JSON lines of a record's artifact with ``role``, found in the evidence store or, for a run directory
|
|
633
|
+
inside the store root (``runs/<run-id>``), next to it. The file must have the sha256 the record lists;
|
|
634
|
+
otherwise nothing is read and the page says why (``what`` names what is then not shown)."""
|
|
635
|
+
from inferencekit.canonical import file_sha256
|
|
636
|
+
from inferencekit.evidence import Store
|
|
637
|
+
|
|
638
|
+
art = next((a for a in rec.get("artifacts") or [] if isinstance(a, dict) and a.get("role") == role), None)
|
|
639
|
+
eid = rec["evidence_id"]
|
|
640
|
+
if not art or not isinstance(art.get("path"), str) or ".." in Path(art["path"]).parts:
|
|
641
|
+
return None
|
|
642
|
+
for base in (Store().run_dir(eid), run.root.parent / eid):
|
|
643
|
+
path = base / art["path"]
|
|
644
|
+
if not path.is_file():
|
|
645
|
+
continue
|
|
646
|
+
if file_sha256(path)[0] != art.get("sha256"):
|
|
647
|
+
run.warnings.append(f"{source}: its {role} file at {path.name} does not match the record's sha256: "
|
|
648
|
+
f"{what} not shown")
|
|
649
|
+
return None
|
|
650
|
+
out = []
|
|
651
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
652
|
+
try:
|
|
653
|
+
row = json.loads(line) if line.strip() else None
|
|
654
|
+
except ValueError:
|
|
655
|
+
continue
|
|
656
|
+
if isinstance(row, dict):
|
|
657
|
+
out.append(row)
|
|
658
|
+
return out
|
|
659
|
+
run.warnings.append(f"{source}: record {eid[:15]}: its {role} file is not in the evidence store (pass "
|
|
660
|
+
f"--runs-dir for that store): {what} not shown")
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def clip_rows(run: Run, rec: dict, source: str) -> dict:
|
|
665
|
+
"""``"r<k>/<id>" -> row`` from a bench clip record's ``requests`` artifact (requests.jsonl)."""
|
|
666
|
+
out = {}
|
|
667
|
+
for row in artifact_lines(run, rec, source, "requests", "per-clip times") or []:
|
|
668
|
+
if isinstance(row.get("id"), str) and isinstance(row.get("round"), int):
|
|
669
|
+
out[f"r{row['round']}/{row['id']}"] = {k: row.get(k) for k in CLIP_ROW_KEYS if k in row}
|
|
670
|
+
return out
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def clip_quality(run: Run, rec: dict, source: str, problem: str | None) -> list:
|
|
674
|
+
"""A quality clip record covers every engine it was given: ``data.engines[label]`` holds each one's verdict,
|
|
675
|
+
its metrics are ``psnr`` / ``ssim`` / ``lpips`` with ``subject`` = the label, and the per-clip scores are the
|
|
676
|
+
``clip_metrics`` artifact's rows (key ``engine:<label>|r<k>/<id>``, ``mean`` per metric). Returns the engines
|
|
677
|
+
it touched."""
|
|
678
|
+
data, eid = rec.get("data") or {}, rec["evidence_id"]
|
|
679
|
+
engines = data.get("engines") if isinstance(data.get("engines"), dict) else {}
|
|
680
|
+
touched = []
|
|
681
|
+
for label, block in engines.items():
|
|
682
|
+
info = run.engine(str(label))
|
|
683
|
+
if info is None or not isinstance(block, dict):
|
|
684
|
+
continue
|
|
685
|
+
touched.append(info)
|
|
686
|
+
verdict = "UNPROVEN" if problem else str(block.get("verdict") or "UNPROVEN")
|
|
687
|
+
for name, unit, names in CLIP_QUALITY:
|
|
688
|
+
value = _metric(rec, names, info.label)
|
|
689
|
+
if value is not None:
|
|
690
|
+
info.ev_quality.append({"name": name, "unit": unit, "evidence": eid, "value": value,
|
|
691
|
+
"verdict": verdict})
|
|
692
|
+
if not any(q["evidence"] == eid for q in info.ev_quality):
|
|
693
|
+
info.ev_quality.append({"name": "clip quality", "unit": None, "evidence": eid, "value": None,
|
|
694
|
+
"verdict": verdict})
|
|
695
|
+
if not touched:
|
|
696
|
+
return touched
|
|
697
|
+
for row in artifact_lines(run, rec, source, "clip_metrics", "per-clip quality") or []:
|
|
698
|
+
found = _QUALITY_KEY.match(str(row.get("key") or ""))
|
|
699
|
+
info = run.engine(found.group(1)) if found else None
|
|
700
|
+
if info is None or info not in touched or row.get("ok") is False:
|
|
701
|
+
continue
|
|
702
|
+
mean = row.get("mean") if isinstance(row.get("mean"), dict) else {}
|
|
703
|
+
got = {name: float(mean[name]) for name, _, _ in CLIP_QUALITY
|
|
704
|
+
if isinstance(mean.get(name), (int, float)) and not isinstance(mean.get(name), bool)}
|
|
705
|
+
if got:
|
|
706
|
+
info.clip_rows.setdefault(found.group(2), {}).update(got)
|
|
707
|
+
return touched
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def merge_clip_rows(run: Run) -> None:
|
|
711
|
+
"""Video: each cell gets its bench clip row (client-timed clip_s, self-reported stages, effective shape) and
|
|
712
|
+
quality clip's per-clip numbers. A clip the engine failed is FAILED with bench clip's error, not NOT RUN."""
|
|
713
|
+
for info in run.engines:
|
|
714
|
+
for item in run.items:
|
|
715
|
+
row = info.clip_rows.get(run.key(item))
|
|
716
|
+
cell = run.cells.get((info.label, run.key(item)))
|
|
717
|
+
if not row or cell is None:
|
|
718
|
+
continue
|
|
719
|
+
cell.clip_row = row
|
|
720
|
+
if isinstance(row.get("e2e_ms"), (int, float)):
|
|
721
|
+
cell.metrics.setdefault("clip_s", row["e2e_ms"] / 1000)
|
|
722
|
+
for name, _, _ in CLIP_QUALITY:
|
|
723
|
+
if isinstance(row.get(name), (int, float)):
|
|
724
|
+
cell.metrics.setdefault(name, row[name])
|
|
725
|
+
if row.get("ok") is False and cell.status != "failed":
|
|
726
|
+
cell.status, cell.reason = "failed", f"bench clip: {row.get('error') or 'the clip failed'}"
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def run_evidence(root: Path, run: Run) -> list[tuple[str, dict]]:
|
|
730
|
+
out = []
|
|
731
|
+
d = root / "evidence"
|
|
732
|
+
if d.is_dir():
|
|
733
|
+
for p in sorted(d.glob("*.json")):
|
|
734
|
+
try:
|
|
735
|
+
out.append((p.relative_to(root).as_posix(), _read_json(p)))
|
|
736
|
+
run.sources.append(p.relative_to(root).as_posix())
|
|
737
|
+
except (OSError, ValueError) as e:
|
|
738
|
+
run.warnings.append(f"evidence/{p.name}: unreadable ({e})")
|
|
739
|
+
return out
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def load_run(root: str | Path, reference: str | None = None, prompts: str | Path | None = None,
|
|
743
|
+
extra_evidence: list[dict] | None = None) -> Run:
|
|
744
|
+
"""Read a run directory. ``reference`` names the outputs/<label> column the others are diffed against;
|
|
745
|
+
``prompts`` (a prompt-set JSONL) fills the input column when inputs/ lacks them; ``extra_evidence``
|
|
746
|
+
adds records (e.g. gates) beyond the run's own evidence/ directory."""
|
|
747
|
+
root = Path(root)
|
|
748
|
+
run = Run(root=root, modality="llm", engines=[], reference=None, items=[], cells={})
|
|
749
|
+
items = load_inputs(root, run)
|
|
750
|
+
if not items and not (root / "inputs").is_dir() and not prompts:
|
|
751
|
+
run.warnings.append("no inputs/ directory (and no --prompts): rows come from the outputs, without prompts")
|
|
752
|
+
|
|
753
|
+
out_dir = root / "outputs"
|
|
754
|
+
labels = sorted((p.name for p in out_dir.iterdir() if p.is_dir()), key=natural_key) if out_dir.is_dir() else []
|
|
755
|
+
met_dir = root / "metrics"
|
|
756
|
+
if met_dir.is_dir():
|
|
757
|
+
for p in sorted(met_dir.iterdir(), key=lambda q: natural_key(q.name)):
|
|
758
|
+
if p.name.endswith(".load.json") or p.suffix not in (".json", ".jsonl"):
|
|
759
|
+
continue # load-mode levels belong to the engine of the same label, not a column of their own
|
|
760
|
+
ending = next((s for s in (".clip.json", ".image.json", ".audio.json") if p.name.endswith(s)), None)
|
|
761
|
+
label = p.name[:-len(ending)] if ending else p.name.rsplit(".", 1)[0]
|
|
762
|
+
if label not in labels:
|
|
763
|
+
labels.append(label)
|
|
764
|
+
# Manifest order first (baseline recipe, tuned, custom), then anything else found.
|
|
765
|
+
manifest = _manifest_engines(root)
|
|
766
|
+
roles = {}
|
|
767
|
+
order = []
|
|
768
|
+
for e in manifest:
|
|
769
|
+
label = str(e.get("label") or e.get("name") or "")
|
|
770
|
+
if label:
|
|
771
|
+
roles[label] = e.get("role")
|
|
772
|
+
if label in labels:
|
|
773
|
+
order.append(label)
|
|
774
|
+
order += [lb for lb in labels if lb not in order]
|
|
775
|
+
|
|
776
|
+
if reference is not None:
|
|
777
|
+
if reference not in order:
|
|
778
|
+
from inferencekit.registry import OpError
|
|
779
|
+
|
|
780
|
+
raise OpError(f"reference '{reference}' has no outputs/{reference}/ or metrics/{reference}.json in "
|
|
781
|
+
f"{root}; found: {order or 'nothing'}")
|
|
782
|
+
run.reference = reference
|
|
783
|
+
elif REFERENCE_DEFAULT in order:
|
|
784
|
+
run.reference = REFERENCE_DEFAULT
|
|
785
|
+
if run.reference:
|
|
786
|
+
order = [run.reference] + [lb for lb in order if lb != run.reference]
|
|
787
|
+
|
|
788
|
+
for label in order:
|
|
789
|
+
info = load_metrics(root, label, run)
|
|
790
|
+
from inferencekit.review.media import load_media_metrics
|
|
791
|
+
|
|
792
|
+
load_media_metrics(root, label, info, run, items)
|
|
793
|
+
info.role = roles.get(label) or info.role or ("reference" if label == run.reference else None)
|
|
794
|
+
run.engines.append(info)
|
|
795
|
+
|
|
796
|
+
# Rows: every input, plus anything an engine produced for an input that is not in inputs/.
|
|
797
|
+
for label in order:
|
|
798
|
+
base = out_dir / label
|
|
799
|
+
if not base.is_dir():
|
|
800
|
+
continue
|
|
801
|
+
deeper = 0
|
|
802
|
+
for path in base.rglob("*"):
|
|
803
|
+
if not path.is_file():
|
|
804
|
+
continue
|
|
805
|
+
rel = path.relative_to(base)
|
|
806
|
+
if len(rel.parts) != 2: # index.json at the root; load-level dirs (<level>/<bucket>/<id>) below
|
|
807
|
+
deeper += len(rel.parts) > 2
|
|
808
|
+
continue
|
|
809
|
+
bucket = rel.parts[0]
|
|
810
|
+
stem, _ = _split_name(path.name)
|
|
811
|
+
if f"{bucket}/{stem}" not in items:
|
|
812
|
+
items[f"{bucket}/{stem}"] = Item(id=stem, bucket=bucket)
|
|
813
|
+
if (root / "inputs").is_dir():
|
|
814
|
+
run.warnings.append(f"outputs/{label}/{rel.as_posix()}: no matching input under "
|
|
815
|
+
f"inputs/{bucket}/")
|
|
816
|
+
if deeper:
|
|
817
|
+
run.warnings.append(f"outputs/{label}/: {deeper} file(s) in load-level directories are not shown "
|
|
818
|
+
"(the page shows the single-user outputs, outputs/<label>/<bucket>/<id>)")
|
|
819
|
+
if prompts:
|
|
820
|
+
load_prompt_set(Path(prompts), items, run)
|
|
821
|
+
run.items = sorted(items.values(), key=lambda i: (bucket_key(i.bucket), natural_key(i.id)))
|
|
822
|
+
for item in run.items:
|
|
823
|
+
for info in run.engines:
|
|
824
|
+
run.cells[(info.label, run.key(item))] = load_cell(root, info.label, item, run)
|
|
825
|
+
attach_evidence(run, run_evidence(root, run) + [(f"evidence {str(r.get('evidence_id', '?'))[:15]}", r)
|
|
826
|
+
for r in extra_evidence or [] if isinstance(r, dict)])
|
|
827
|
+
from inferencekit.review.media import merge_rows
|
|
828
|
+
|
|
829
|
+
merge_rows(run)
|
|
830
|
+
run.modality = detect_modality(root, run.items, run.cells)
|
|
831
|
+
if run.modality == "llm" and not run.reference:
|
|
832
|
+
run.warnings.insert(0, "no reference outputs (pass --reference LABEL or add outputs/reference/): "
|
|
833
|
+
"divergence is not marked")
|
|
834
|
+
if run.modality == "video":
|
|
835
|
+
merge_clip_rows(run)
|
|
836
|
+
if not run.reference:
|
|
837
|
+
run.warnings.insert(0, "no reference clips (pass --reference LABEL or add outputs/reference/): there is "
|
|
838
|
+
"no column to compare the clips with by eye")
|
|
839
|
+
if run.modality == "tts":
|
|
840
|
+
for item in run.items:
|
|
841
|
+
if item.ground_truth is None and item.text:
|
|
842
|
+
item.ground_truth = item.text.strip()
|
|
843
|
+
run.sources = sorted(set(run.sources))
|
|
844
|
+
return run
|