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,384 @@
|
|
|
1
|
+
"""Speech screening: client-timed PCM, structural validation, never an audio quality proof.
|
|
2
|
+
|
|
3
|
+
Fixture JSONL rows: {id, text, reference_audio?: relative file, reference_text?: str,
|
|
4
|
+
unseen?: bool}. The unseen flag is an attestation, not independently verified.
|
|
5
|
+
One process serves all rounds; final comparative claims need a separate fresh-
|
|
6
|
+
process interleaved scheduler. Warmup requests are excluded from all metrics.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import re
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from urllib.parse import urlsplit
|
|
19
|
+
|
|
20
|
+
from inferencekit import evidence as ev, stats
|
|
21
|
+
from inferencekit.canonical import canonical_json
|
|
22
|
+
from inferencekit.ops.bench_serve import _record_call
|
|
23
|
+
from inferencekit.registry import Arg, OpError, operation
|
|
24
|
+
from inferencekit.serve.audio_client import AudioHTTP, AudioRequest, ENCODING, playback_metrics
|
|
25
|
+
from inferencekit.serve.client import Endpoint, api_key_from_env
|
|
26
|
+
from inferencekit.serve.metrics import pct
|
|
27
|
+
|
|
28
|
+
KIND = "audio_benchmark"
|
|
29
|
+
_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.+-]{0,63}$")
|
|
30
|
+
ARGS = [
|
|
31
|
+
Arg("base_url", "string", "Speech server root; stored only as a keyed endpoint tag.", required=True, sensitive=True),
|
|
32
|
+
Arg("model", "string", "Pinned model identifier sent to the server.", required=True),
|
|
33
|
+
Arg("model_revision", "string", "Checkpoint revision, sent and required in final effective settings."),
|
|
34
|
+
Arg("model_dtype", "string", "Checkpoint dtype, sent as dtype and checked in effective settings."),
|
|
35
|
+
Arg("model_quant", "string", "Complete quantization recipe, sent as quant and checked in effective settings."),
|
|
36
|
+
Arg("fixtures", "string", "JSONL rows: id, text, reference_audio?, reference_text?, unseen?.", required=True),
|
|
37
|
+
Arg("label", "string", "Configuration label for artifacts.", default="audio"),
|
|
38
|
+
Arg("rounds", "integer", "Timed passes; screening in one server process.", default=3),
|
|
39
|
+
Arg("first_round", "integer", "First round index; default zero or previous maximum plus one."),
|
|
40
|
+
Arg("warmup", "integer", "Untimed distinct-text requests before measurement.", default=1),
|
|
41
|
+
Arg("seed_base", "integer", "Shared deterministic round-seed base.", default=0),
|
|
42
|
+
Arg("sample_rate", "integer", "Required decoded PCM sample rate.", default=44100),
|
|
43
|
+
Arg("channels", "integer", "Required PCM channels.", default=1),
|
|
44
|
+
Arg("settings", "string", "JSON object of sampling/streaming settings; final must report them.", default="{}"),
|
|
45
|
+
Arg("api_key_env", "string", "Environment variable containing the key; never a key value."),
|
|
46
|
+
Arg("engine_version", "string", "Pinned engine version, sent and checked in effective settings."),
|
|
47
|
+
Arg("hardware_name", "string", "Measured hardware label; never inferred from a remote client."),
|
|
48
|
+
Arg("run_dir", "string", "Also copy request/output artifacts and metrics here."),
|
|
49
|
+
Arg("timeout_s", "number", "Whole-response deadline including DNS, connect, headers and streamed body.", default=300.0),
|
|
50
|
+
Arg("resamples", "integer", "Round-cluster bootstrap resamples.", default=stats.DEFAULT_RESAMPLES),
|
|
51
|
+
Arg("save_outputs", "string", "Keep decoded PCM or hashes only.", default="all", choices=("all", "hash_only")),
|
|
52
|
+
Arg("slo_first_audio_p95_ms", "number", "p95 simulated nonzero-signal onset limit.", default=2000.0),
|
|
53
|
+
Arg("slo_rtf", "number", "Maximum complete-generation wall seconds per audio second.", default=1.0),
|
|
54
|
+
Arg("extend", "string", "Append rounds to the same configuration's earlier evidence record."),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class AudioFixture:
|
|
60
|
+
id: str
|
|
61
|
+
text: str
|
|
62
|
+
text_sha256: str
|
|
63
|
+
reference_audio: bytes | None
|
|
64
|
+
reference_sha256: str | None
|
|
65
|
+
reference_text: str | None
|
|
66
|
+
reference_text_sha256: str | None
|
|
67
|
+
declared_unseen: bool
|
|
68
|
+
|
|
69
|
+
def identity(self):
|
|
70
|
+
return {"id": self.id, "text_sha256": self.text_sha256,
|
|
71
|
+
"reference_sha256": self.reference_sha256,
|
|
72
|
+
"reference_text_sha256": self.reference_text_sha256, "declared_unseen": self.declared_unseen}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _sha(data: bytes) -> str:
|
|
76
|
+
return hashlib.sha256(data).hexdigest()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_fixtures(path) -> tuple[list[AudioFixture], str]:
|
|
80
|
+
p = Path(path)
|
|
81
|
+
try:
|
|
82
|
+
raw = p.read_bytes()
|
|
83
|
+
docs = [json.loads(line) for line in raw.decode("utf-8").splitlines() if line.strip()]
|
|
84
|
+
except (OSError, ValueError):
|
|
85
|
+
raise OpError("cannot read speech fixture JSONL") from None
|
|
86
|
+
fixtures, ids = [], set()
|
|
87
|
+
for n, doc in enumerate(docs, 1):
|
|
88
|
+
if not isinstance(doc, dict) or not isinstance(doc.get("id"), str) or not _ID.fullmatch(doc["id"]):
|
|
89
|
+
raise OpError(f"speech fixture row {n}: invalid id")
|
|
90
|
+
if doc["id"] in ids:
|
|
91
|
+
raise OpError(f"speech fixture row {n}: duplicate id")
|
|
92
|
+
ids.add(doc["id"])
|
|
93
|
+
if not isinstance(doc.get("text"), str) or not doc["text"].strip():
|
|
94
|
+
raise OpError(f"speech fixture row {n}: nonempty text required")
|
|
95
|
+
if set(doc) - {"id", "text", "reference_audio", "reference_text", "unseen"}:
|
|
96
|
+
raise OpError(f"speech fixture row {n}: unknown field")
|
|
97
|
+
if "unseen" in doc and type(doc["unseen"]) is not bool:
|
|
98
|
+
raise OpError(f"speech fixture row {n}: unseen must be boolean")
|
|
99
|
+
reference, reference_text = None, doc.get("reference_text")
|
|
100
|
+
if "reference_audio" in doc:
|
|
101
|
+
if not isinstance(doc["reference_audio"], str):
|
|
102
|
+
raise OpError(f"speech fixture row {n}: reference_audio must be a file path")
|
|
103
|
+
if not isinstance(reference_text, str) or not reference_text.strip():
|
|
104
|
+
raise OpError(f"speech fixture row {n}: reference audio needs its transcript")
|
|
105
|
+
try:
|
|
106
|
+
reference = (p.parent / doc["reference_audio"]).read_bytes()
|
|
107
|
+
except OSError:
|
|
108
|
+
raise OpError(f"speech fixture row {n}: reference audio unreadable") from None
|
|
109
|
+
if not reference:
|
|
110
|
+
raise OpError(f"speech fixture row {n}: reference audio empty")
|
|
111
|
+
elif reference_text is not None:
|
|
112
|
+
raise OpError(f"speech fixture row {n}: transcript requires reference audio")
|
|
113
|
+
fixtures.append(AudioFixture(doc["id"], doc["text"], _sha(doc["text"].encode()), reference,
|
|
114
|
+
_sha(reference) if reference is not None else None, reference_text,
|
|
115
|
+
_sha(reference_text.encode()) if reference_text is not None else None,
|
|
116
|
+
doc.get("unseen", False)))
|
|
117
|
+
if not fixtures:
|
|
118
|
+
raise OpError("speech fixture set is empty")
|
|
119
|
+
# Content identity includes the reference bytes, independent of local filenames.
|
|
120
|
+
return fixtures, _sha(canonical_json([f.identity() for f in fixtures]).encode())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def round_seed(seed_base: int, round_index: int) -> int:
|
|
124
|
+
return int.from_bytes(hashlib.sha256(f"inferencekit-audio:{seed_base}:{round_index}".encode()).digest()[:4],
|
|
125
|
+
"big") & 0x7fffffff
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def audio_row(result, fixture, round_index, seed):
|
|
129
|
+
duration = result.audio_duration_s
|
|
130
|
+
complete = result.complete_audio_ms
|
|
131
|
+
return {**fixture.identity(), "round": round_index, "seed": seed, "ok": result.ok,
|
|
132
|
+
"error": result.error, "status": result.status, "t_send_ns": result.t_send_ns,
|
|
133
|
+
"t_done_ns": result.t_done_ns, "t_end_ns": result.t_end_ns,
|
|
134
|
+
"first_pcm_ms": result.first_audio_ms, "first_signal_ms": result.first_signal_ms,
|
|
135
|
+
"first_audio_ms": result.first_audible_ms, "complete_audio_ms": complete,
|
|
136
|
+
"audio_duration_s": duration, "rtf": complete / 1000 / duration if duration and complete is not None else None,
|
|
137
|
+
"leading_silence_ms": result.leading_silent_frames / result.sample_rate * 1000
|
|
138
|
+
if result.ok else None, "sample_rate": result.sample_rate, "channels": result.channels,
|
|
139
|
+
"sample_count": result.sample_count if result.ok else None,
|
|
140
|
+
"received_pcm_frames": sum(count for _, count in result.chunks),
|
|
141
|
+
"pcm_bytes": len(result.pcm) if result.ok else None,
|
|
142
|
+
"pcm_sha256": _sha(result.pcm) if result.ok else None, "effective": result.effective,
|
|
143
|
+
"chunks": [{"arrival_ms": (t - result.t_send_ns) / 1e6, "sample_count": count}
|
|
144
|
+
for t, count in result.chunks],
|
|
145
|
+
**(playback_metrics(result.chunks, result.sample_rate) if result.ok else
|
|
146
|
+
{"underruns": None, "underrun_ms": None, "max_stream_gap_ms": None})}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def audio_metrics(rows: list[dict], resamples: int) -> tuple[list[dict], dict]:
|
|
150
|
+
metrics, table = [], {}
|
|
151
|
+
for key, unit in (("first_audio_ms", "ms"), ("first_pcm_ms", "ms"), ("first_signal_ms", "ms"),
|
|
152
|
+
("complete_audio_ms", "ms"), ("rtf", "ratio"), ("max_stream_gap_ms", "ms")):
|
|
153
|
+
valid = [row for row in rows if row["ok"] and row.get(key) is not None]
|
|
154
|
+
values, units = [row[key] for row in valid], [row["round"] for row in valid]
|
|
155
|
+
cis = stats.bootstrap_ci(values, units=units, ps=(50, 95), resamples=resamples)
|
|
156
|
+
table[key] = {"n": len(values), "median": pct(values, 50), "p95": pct(values, 95),
|
|
157
|
+
"max": max(values, default=None), "ci95_median": list(cis[50]), "ci95_p95": list(cis[95])}
|
|
158
|
+
for p, stat in ((50, "median"), (95, "p95")):
|
|
159
|
+
metrics.append(ev.metric(key, pct(values, p), unit, stat, n=len(values), ci95=cis[p]))
|
|
160
|
+
return metrics, table
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _write(roots, relative: str, data: bytes):
|
|
164
|
+
for root in roots:
|
|
165
|
+
target = root / relative
|
|
166
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
target.write_bytes(data)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _previous(store, reference, config_sha, call):
|
|
171
|
+
if not reference:
|
|
172
|
+
return None, [], []
|
|
173
|
+
record = store.load(reference)
|
|
174
|
+
if record.get("kind") != KIND or record.get("data", {}).get("source") != "bench audio":
|
|
175
|
+
raise OpError("--extend requires a bench audio record")
|
|
176
|
+
if ev.integrity_problem(record):
|
|
177
|
+
raise OpError("--extend evidence integrity check failed")
|
|
178
|
+
if record["data"].get("configuration_sha256") != config_sha:
|
|
179
|
+
raise OpError("--extend requires the same workload, model, version, hardware and seed policy")
|
|
180
|
+
old_args = record["data"].get("request", {}).get("args", {})
|
|
181
|
+
if old_args.get("base_url") != call.request()["args"].get("base_url"):
|
|
182
|
+
raise OpError("--extend requires the same endpoint")
|
|
183
|
+
artifact = next((a for a in record["artifacts"] if a.get("role") == "requests"), None)
|
|
184
|
+
if artifact is None:
|
|
185
|
+
raise OpError("--extend per-request artifact missing")
|
|
186
|
+
previous_root = store.run_dir(record["evidence_id"])
|
|
187
|
+
path = previous_root / artifact["path"]
|
|
188
|
+
if not path.resolve().is_relative_to(previous_root.resolve()):
|
|
189
|
+
raise OpError("--extend per-request artifact escapes its record")
|
|
190
|
+
try:
|
|
191
|
+
raw = path.read_bytes()
|
|
192
|
+
if _sha(raw) != artifact["sha256"]:
|
|
193
|
+
raise ValueError
|
|
194
|
+
rows = [json.loads(line) for line in raw.decode().splitlines() if line.strip()]
|
|
195
|
+
except (OSError, ValueError):
|
|
196
|
+
raise OpError("--extend per-request artifact missing or changed") from None
|
|
197
|
+
return record, rows, record["data"]["rounds"]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@operation("bench_audio", ("bench", "audio"), "Client-timed streamed PCM speech screening; structural gates and "
|
|
201
|
+
"first-audio p95, complete audio, RTF and buffer underruns. Audio quality remains UNPROVEN.", ARGS)
|
|
202
|
+
def bench_audio(call, base_url, model, model_revision, model_dtype, model_quant, fixtures, label, rounds, first_round, warmup, seed_base, sample_rate,
|
|
203
|
+
channels, settings, api_key_env, engine_version, hardware_name, run_dir, timeout_s,
|
|
204
|
+
resamples, save_outputs, slo_first_audio_p95_ms, slo_rtf, extend):
|
|
205
|
+
if rounds < 1 or (first_round is not None and first_round < 0) or warmup < 0 or resamples < 1:
|
|
206
|
+
raise OpError("rounds/resamples must be positive; first_round/warmup nonnegative")
|
|
207
|
+
if not _ID.fullmatch(label):
|
|
208
|
+
raise OpError("invalid audio label")
|
|
209
|
+
if not model.strip():
|
|
210
|
+
raise OpError("audio model identifier must be nonempty")
|
|
211
|
+
if not 8000 <= sample_rate <= 192000 or not 1 <= channels <= 8:
|
|
212
|
+
raise OpError("PCM rate must be 8000..192000 Hz and channels 1..8")
|
|
213
|
+
if any(not math.isfinite(v) or v <= 0 for v in (timeout_s, slo_first_audio_p95_ms, slo_rtf)):
|
|
214
|
+
raise OpError("timeout and SLO limits must be finite and positive")
|
|
215
|
+
try:
|
|
216
|
+
setting_doc = json.loads(settings)
|
|
217
|
+
if not isinstance(setting_doc, dict):
|
|
218
|
+
raise ValueError
|
|
219
|
+
json.dumps(setting_doc, allow_nan=False)
|
|
220
|
+
if set(setting_doc) & {"model", "seed", "input", "sample_rate", "channels", "encoding", "stream"}:
|
|
221
|
+
raise ValueError
|
|
222
|
+
if ev.redact(setting_doc) != setting_doc:
|
|
223
|
+
raise ValueError
|
|
224
|
+
except ValueError:
|
|
225
|
+
raise OpError("settings must be a finite JSON object without reserved request fields") from None
|
|
226
|
+
pins = {key: value for key, value in (("model_revision", model_revision), ("dtype", model_dtype),
|
|
227
|
+
("quant", model_quant), ("engine_version", engine_version)) if value is not None}
|
|
228
|
+
for key, value in pins.items():
|
|
229
|
+
if key in setting_doc and setting_doc[key] != value:
|
|
230
|
+
raise OpError(f"settings conflict with declared {key}")
|
|
231
|
+
setting_doc[key] = value
|
|
232
|
+
fixture_rows, fixture_sha = load_fixtures(fixtures)
|
|
233
|
+
try:
|
|
234
|
+
parsed_url = urlsplit(base_url if "://" in base_url else "http://" + base_url)
|
|
235
|
+
if parsed_url.username is not None or parsed_url.password is not None or parsed_url.query or parsed_url.fragment:
|
|
236
|
+
raise ValueError
|
|
237
|
+
endpoint = Endpoint.parse(base_url, api_key_from_env(api_key_env))
|
|
238
|
+
except (ValueError, OpError):
|
|
239
|
+
raise OpError("invalid speech endpoint or API-key environment configuration") from None
|
|
240
|
+
record_call, persistent = _record_call(call, endpoint)
|
|
241
|
+
backend, store = AudioHTTP(endpoint, timeout_s=timeout_s), ev.Store()
|
|
242
|
+
run = Path(run_dir) if run_dir else None
|
|
243
|
+
workload_config = {"fixtures": fixture_sha, "model": model, "revision": model_revision,
|
|
244
|
+
"dtype": model_dtype, "quant": model_quant, "sample_rate": sample_rate,
|
|
245
|
+
"channels": channels, "encoding": ENCODING,
|
|
246
|
+
"settings": {key: value for key, value in setting_doc.items() if key != "engine_version"},
|
|
247
|
+
"seed_policy": {"name": "per_round", "base": seed_base}}
|
|
248
|
+
request_sha = _sha(canonical_json(workload_config).encode())
|
|
249
|
+
config_sha = _sha(canonical_json({**workload_config, "label": label, "hardware": hardware_name,
|
|
250
|
+
"engine_version": engine_version, "effective_settings": setting_doc,
|
|
251
|
+
"save_outputs": save_outputs,
|
|
252
|
+
"slo_first_audio_p95_ms": slo_first_audio_p95_ms, "slo_rtf": slo_rtf}).encode())
|
|
253
|
+
previous, previous_rows, previous_rounds = _previous(store, extend, config_sha, record_call)
|
|
254
|
+
done = max((row["index"] for row in previous_rounds), default=-1)
|
|
255
|
+
first_round = done + 1 if first_round is None else first_round
|
|
256
|
+
if first_round <= done:
|
|
257
|
+
raise OpError("--first-round conflicts with rounds already present in --extend")
|
|
258
|
+
if previous is not None and first_round != done + 1:
|
|
259
|
+
raise OpError("--extend --first-round must be the previous maximum plus one; skipped rounds are refused")
|
|
260
|
+
|
|
261
|
+
def request(fixture, seed, text=None):
|
|
262
|
+
return AudioRequest(fixture.id, fixture.text if text is None else text, model, seed, sample_rate,
|
|
263
|
+
channels, setting_doc, fixture.reference_audio, fixture.reference_text)
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
with store.staging_dir() as staging:
|
|
267
|
+
roots = [staging] + ([run] if run else [])
|
|
268
|
+
warm_rows, rows, round_rows, outputs = [], list(previous_rows), list(previous_rounds), []
|
|
269
|
+
if previous:
|
|
270
|
+
previous_root = store.run_dir(previous["evidence_id"])
|
|
271
|
+
for artifact in previous["artifacts"]:
|
|
272
|
+
if artifact.get("role") not in ("audio_pcm", "outputs_index"):
|
|
273
|
+
continue
|
|
274
|
+
src = previous_root / artifact["path"]
|
|
275
|
+
if not src.resolve().is_relative_to(previous_root.resolve()):
|
|
276
|
+
raise OpError("--extend artifact path escapes its record")
|
|
277
|
+
try:
|
|
278
|
+
blob = src.read_bytes()
|
|
279
|
+
except OSError:
|
|
280
|
+
raise OpError("--extend output artifact missing") from None
|
|
281
|
+
if _sha(blob) != artifact["sha256"]:
|
|
282
|
+
raise OpError("--extend output artifact changed")
|
|
283
|
+
if artifact["role"] == "outputs_index":
|
|
284
|
+
outputs = json.loads(blob)
|
|
285
|
+
else:
|
|
286
|
+
_write(roots, artifact["path"], blob)
|
|
287
|
+
for i in range(warmup):
|
|
288
|
+
fixture = fixture_rows[i % len(fixture_rows)]
|
|
289
|
+
text = f"Inferencekit untimed speech warmup {i}. " + fixture.text
|
|
290
|
+
while text in {f.text for f in fixture_rows}:
|
|
291
|
+
text = "Warmup. " + text
|
|
292
|
+
result = backend.generate(request(fixture, round_seed(seed_base, -i - 1), text))
|
|
293
|
+
warm_rows.append({"ok": result.ok, "error": result.error})
|
|
294
|
+
for round_index in range(first_round, first_round + rounds):
|
|
295
|
+
start = time.time()
|
|
296
|
+
current_rows = []
|
|
297
|
+
seed = round_seed(seed_base, round_index)
|
|
298
|
+
for fixture in fixture_rows:
|
|
299
|
+
result = backend.generate(request(fixture, seed))
|
|
300
|
+
row = audio_row(result, fixture, round_index, seed)
|
|
301
|
+
rows.append(row)
|
|
302
|
+
current_rows.append(row)
|
|
303
|
+
if result.ok and save_outputs == "all":
|
|
304
|
+
rel = f"outputs/{label}/r{round_index}/{fixture.id}.pcm"
|
|
305
|
+
_write(roots, rel, result.pcm)
|
|
306
|
+
outputs.append({"path": rel, "sha256": row["pcm_sha256"], "bytes": row["pcm_bytes"],
|
|
307
|
+
"sample_rate": sample_rate, "channels": channels, "encoding": ENCODING})
|
|
308
|
+
valid = [row for row in current_rows if row["ok"]]
|
|
309
|
+
round_rows.append({"index": round_index, "t_start": start, "t_end": time.time(), "seed": seed,
|
|
310
|
+
"n_failed": len(current_rows) - len(valid),
|
|
311
|
+
"audio_ms": sum(r["complete_audio_ms"] for r in valid) / len(valid) if valid else None,
|
|
312
|
+
"first_audio_p95_ms": pct([r["first_audio_ms"] for r in valid], 95),
|
|
313
|
+
"rtf": sum(r["rtf"] for r in valid) / len(valid) if valid else None})
|
|
314
|
+
metrics, table = audio_metrics(rows, resamples)
|
|
315
|
+
failed = sum(not row["ok"] for row in rows)
|
|
316
|
+
structure = "FAIL" if failed else "PASS"
|
|
317
|
+
timing = "FAIL" if failed or table["first_audio_ms"]["p95"] > slo_first_audio_p95_ms or \
|
|
318
|
+
table["rtf"]["max"] > slo_rtf or any(row["underruns"] for row in rows if row["ok"]) else "PASS"
|
|
319
|
+
verdict = stats.combine_claims([structure, timing, "UNPROVEN"])
|
|
320
|
+
data = {"source": "bench audio", "timer": "client perf_counter_ns", "label": label,
|
|
321
|
+
"endpoint_kind": "audio_http", "base_url_kind": "local" if endpoint.is_local() else "remote",
|
|
322
|
+
"fixture_set": {"sha256": fixture_sha, "fixtures": [f.identity() for f in fixture_rows]},
|
|
323
|
+
"configuration_sha256": config_sha, "extends": previous["evidence_id"] if previous else None,
|
|
324
|
+
"rounds": round_rows, "warmup": {"excluded": True, "requests": warm_rows},
|
|
325
|
+
"per_request": table, "failed_requests": failed, "total_requests": len(rows),
|
|
326
|
+
"structural_verdict": structure, "timing_verdict": timing, "quality_verdict": "UNPROVEN",
|
|
327
|
+
"verdict": verdict, "claim_validity": {"valid": not failed, "checked": [
|
|
328
|
+
"PCM metadata", "aligned nonempty chunks", "terminal final event", "sample count",
|
|
329
|
+
"nonzero signal", "effective settings (self-reported)"],
|
|
330
|
+
"violations": [r["error"] for r in rows if not r["ok"]]},
|
|
331
|
+
"slo": {"first_audio_p95_ms": slo_first_audio_p95_ms, "rtf_max": slo_rtf, "max_underruns": 0},
|
|
332
|
+
"memory": {"verdict": "NOT RUN", "source": "external process/Metal instrumentation required"},
|
|
333
|
+
"metric_claim_classes": {"first_pcm_ms": "observed (client timer)",
|
|
334
|
+
"first_signal_ms": "observed (client timer, nonzero-sample chunk arrival)",
|
|
335
|
+
"complete_audio_ms": "observed (client timer)", "max_stream_gap_ms": "observed (client timer)",
|
|
336
|
+
"first_audio_ms": "derived (PCM playback simulation, not an audio device measurement)",
|
|
337
|
+
"rtf": "derived (complete time / decoded PCM duration)",
|
|
338
|
+
"underruns": "derived (PCM playback simulation)", "audio_duration_s": "derived (frames / declared rate)"},
|
|
339
|
+
"definitions": {"first_pcm_ms": "First decoded nonempty PCM chunk, including silence.",
|
|
340
|
+
"sample_count": "Validated complete PCM frame count; null for failed responses.",
|
|
341
|
+
"received_pcm_frames": "Decoded frames received, including partial failed responses; diagnostic only.",
|
|
342
|
+
"first_signal_ms": "Arrival of the first chunk containing a nonzero int16 sample.",
|
|
343
|
+
"first_audio_ms": "Simulated nonzero-signal playback onset, including initial zero PCM and stalls.",
|
|
344
|
+
"complete_audio_ms": "Full HTTP body consumption minus request start, on the client clock.",
|
|
345
|
+
"rtf": "complete_audio_ms / (1000 * decoded PCM duration seconds); lower is faster.",
|
|
346
|
+
"underruns": "PCM playback simulation starting at first chunk, zero extra prebuffer."}}
|
|
347
|
+
limitations = ["Structural PCM checks do not establish intelligibility, completion, speaker similarity or perceptual quality; external reference proof required.",
|
|
348
|
+
"unseen is a fixture attestation; text/reference hashes bind inputs but do not prove the server has never seen them.",
|
|
349
|
+
"Rounds share one server process: screening only; comparative claims require fresh-process interleaved rounds.",
|
|
350
|
+
"NDJSON events must be flushed by the server. Arrival includes its buffering, HTTP line framing and network transfer.",
|
|
351
|
+
"Signal means a nonzero PCM sample, not validated speech; PCM silence is measured at exact zero.",
|
|
352
|
+
"Effective settings are self-reported, not independently proven.",
|
|
353
|
+
"Memory, swap and sustained repeated-run memory growth are NOT RUN; collect external telemetry."]
|
|
354
|
+
if not persistent:
|
|
355
|
+
limitations.append("Endpoint tag is process-local because no persistent HMAC key could be written.")
|
|
356
|
+
if warmup == 0 or any(not row["ok"] for row in warm_rows):
|
|
357
|
+
limitations.append("Warmup absent or failed: timed requests may include initialization.")
|
|
358
|
+
if len(round_rows) < 3:
|
|
359
|
+
limitations.append("Fewer than three rounds: insufficient independent-round evidence for a final claim.")
|
|
360
|
+
if save_outputs == "hash_only":
|
|
361
|
+
limitations.append("PCM outputs not retained: hashes alone cannot support later audio quality evaluation.")
|
|
362
|
+
_write(roots, "requests.jsonl", ("".join(json.dumps(ev.redact(row)) + "\n" for row in rows)).encode())
|
|
363
|
+
_write(roots, f"outputs/{label}/index.json", (json.dumps(outputs, indent=2) + "\n").encode())
|
|
364
|
+
artifacts = [ev.artifact(staging / "requests.jsonl", staging, "requests"),
|
|
365
|
+
ev.artifact(staging / f"outputs/{label}/index.json", staging, "outputs_index")]
|
|
366
|
+
for output in outputs:
|
|
367
|
+
artifacts.append(ev.artifact(staging / output["path"], staging, "audio_pcm"))
|
|
368
|
+
identity = ev.identity(gpu={"name": hardware_name}, engine={"name": label, "version": engine_version},
|
|
369
|
+
model={"id": model, "revision": model_revision, "dtype": model_dtype, "quant": model_quant},
|
|
370
|
+
workload={"mode": "single", "batch": 1, "endpoint_kind": "audio_http",
|
|
371
|
+
"request_set": {"sha256": request_sha, "n": len(fixture_rows)},
|
|
372
|
+
"seed_policy": {"name": "per_round", "base": seed_base}})
|
|
373
|
+
data["audio_config"] = {"sample_rate": sample_rate, "channels": channels,
|
|
374
|
+
"settings": setting_doc, "seed_base": seed_base, "save_outputs": save_outputs}
|
|
375
|
+
record = ev.build(record_call, kind=KIND, claim_class="observed",
|
|
376
|
+
summary=f"{label}: {len(rows)} speech requests, {failed} structural failures; quality UNPROVEN; {verdict}",
|
|
377
|
+
identity_=identity, metrics=metrics, artifacts=artifacts,
|
|
378
|
+
missing=["audio quality reference proof", "external memory telemetry"],
|
|
379
|
+
limitations=limitations, data=data)
|
|
380
|
+
if run:
|
|
381
|
+
_write([run], f"metrics/{label}.audio.json", (json.dumps(ev.redact(data), indent=2) + "\n").encode())
|
|
382
|
+
return store.save(record, staging)
|
|
383
|
+
finally:
|
|
384
|
+
backend.close()
|