inferencekit-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. inferencekit/__init__.py +9 -0
  2. inferencekit/__main__.py +5 -0
  3. inferencekit/analysis/__init__.py +1 -0
  4. inferencekit/analysis/ncu_csv.py +284 -0
  5. inferencekit/analysis/nsys_sqlite.py +518 -0
  6. inferencekit/canonical.py +104 -0
  7. inferencekit/checkpoint.py +396 -0
  8. inferencekit/cli.py +274 -0
  9. inferencekit/cuda/bw_peak.cu +252 -0
  10. inferencekit/cuda/torch_bw.py +60 -0
  11. inferencekit/engines/__init__.py +7 -0
  12. inferencekit/engines/adapters.py +190 -0
  13. inferencekit/engines/clip/__init__.py +13 -0
  14. inferencekit/engines/clip/comfyui.py +749 -0
  15. inferencekit/engines/clip/diffusers_ltx.py +601 -0
  16. inferencekit/engines/clip/noise.py +46 -0
  17. inferencekit/engines/clip/official_ltx.py +316 -0
  18. inferencekit/engines/clip/server.py +542 -0
  19. inferencekit/engines/clip/stepdump.py +182 -0
  20. inferencekit/engines/gates.py +94 -0
  21. inferencekit/engines/sglang_worker.py +86 -0
  22. inferencekit/engines/vllm_worker.py +129 -0
  23. inferencekit/evidence.py +427 -0
  24. inferencekit/gpuinfo.py +93 -0
  25. inferencekit/hardware.py +135 -0
  26. inferencekit/mcp_server.py +178 -0
  27. inferencekit/ops/__init__.py +30 -0
  28. inferencekit/ops/bench.py +184 -0
  29. inferencekit/ops/bench_audio.py +384 -0
  30. inferencekit/ops/bench_clip.py +819 -0
  31. inferencekit/ops/bench_image.py +393 -0
  32. inferencekit/ops/bench_rounds.py +297 -0
  33. inferencekit/ops/bench_serve.py +866 -0
  34. inferencekit/ops/ceiling.py +135 -0
  35. inferencekit/ops/compare.py +378 -0
  36. inferencekit/ops/correctness.py +153 -0
  37. inferencekit/ops/doctor.py +486 -0
  38. inferencekit/ops/gates.py +1772 -0
  39. inferencekit/ops/gates_clip.py +930 -0
  40. inferencekit/ops/intake.py +1321 -0
  41. inferencekit/ops/ledger.py +749 -0
  42. inferencekit/ops/manifest.py +309 -0
  43. inferencekit/ops/ncu.py +183 -0
  44. inferencekit/ops/nsys.py +224 -0
  45. inferencekit/ops/prompts.py +316 -0
  46. inferencekit/ops/quality.py +2161 -0
  47. inferencekit/ops/quality_clip.py +690 -0
  48. inferencekit/ops/quality_clip_steps.py +341 -0
  49. inferencekit/ops/report.py +317 -0
  50. inferencekit/ops/review.py +119 -0
  51. inferencekit/ops/rocprof.py +89 -0
  52. inferencekit/ops/roofline.py +392 -0
  53. inferencekit/ops/store.py +61 -0
  54. inferencekit/proc.py +172 -0
  55. inferencekit/prompts/ATTRIBUTION.md +56 -0
  56. inferencekit/prompts/__init__.py +167 -0
  57. inferencekit/prompts/build.py +518 -0
  58. inferencekit/prompts/dev-load.jsonl.gz +0 -0
  59. inferencekit/prompts/dev-load.manifest.json +212 -0
  60. inferencekit/prompts/dev.jsonl.gz +0 -0
  61. inferencekit/prompts/dev.manifest.json +214 -0
  62. inferencekit/prompts/exclusions.json +54 -0
  63. inferencekit/quality/__init__.py +23 -0
  64. inferencekit/quality/client.py +66 -0
  65. inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
  66. inferencekit/quality/fake.py +329 -0
  67. inferencekit/quality/gsm8k.py +224 -0
  68. inferencekit/quality/logprobs.py +106 -0
  69. inferencekit/quality/metrics.py +235 -0
  70. inferencekit/quality/niah.py +63 -0
  71. inferencekit/quality/parquet.py +296 -0
  72. inferencekit/quality/sampling.py +262 -0
  73. inferencekit/quality/step_replay.py +217 -0
  74. inferencekit/quality/video_metrics.py +304 -0
  75. inferencekit/registry.py +206 -0
  76. inferencekit/review/__init__.py +6 -0
  77. inferencekit/review/diff.py +163 -0
  78. inferencekit/review/media.py +181 -0
  79. inferencekit/review/page.py +920 -0
  80. inferencekit/review/rundir.py +844 -0
  81. inferencekit/roofline_headers.py +504 -0
  82. inferencekit/schemas/evidence.v1.json +130 -0
  83. inferencekit/schemas/evidence.v2.json +499 -0
  84. inferencekit/schemas/intake.v1.json +276 -0
  85. inferencekit/schemas/run-manifest.v1.json +118 -0
  86. inferencekit/serve/__init__.py +8 -0
  87. inferencekit/serve/audio_client.py +330 -0
  88. inferencekit/serve/client.py +446 -0
  89. inferencekit/serve/clip_client.py +351 -0
  90. inferencekit/serve/clip_rounds.py +429 -0
  91. inferencekit/serve/image_client.py +373 -0
  92. inferencekit/serve/loadgen.py +136 -0
  93. inferencekit/serve/metrics.py +203 -0
  94. inferencekit/serve/profiles.py +105 -0
  95. inferencekit/serve/prompts_io.py +125 -0
  96. inferencekit/serve/rounds.py +1111 -0
  97. inferencekit/serve/servers.py +897 -0
  98. inferencekit/stats.py +249 -0
  99. inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
  100. inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
  101. inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
  102. inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
  103. inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
  104. inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
@@ -0,0 +1,1772 @@
1
+ """gates run / gates spec-exact: anti-cheat gates for a speed claim.
2
+
3
+ A speed number is only worth publishing if the engine did the work it claims. These
4
+ gates probe a live OpenAI-compatible endpoint (``/v1/chat/completions``, streamed) and
5
+ check a ``bench serve`` run directory. Each gate ends PASS, FAIL, UNPROVEN or NOT RUN
6
+ with the requests and numbers that decided it:
7
+
8
+ caching replaying prompts must not be >10% faster (TTFT or decode) than the
9
+ first time (median over pairs, and in >= 2/3 of the pairs), and a
10
+ prompt changed by one character must not get the old answer back
11
+ (output / "semantic" caching).
12
+ token_count usage.completion_tokens == streamed token ids (or logprob entries)
13
+ == re-tokenized text (when a tokenizer is given). A reply cut at
14
+ max_tokens whose logprob stream is shorter than usage is UNPROVEN, not
15
+ FAIL: special tokens (end of turn, padding) can be counted but not streamed;
16
+ re-tokenized text that covers usage settles it. Copied strings (rare,
17
+ echo) and text after EOS are not held to the re-tokenized count.
18
+ output_length with ignore_eos the output is exactly max_tokens long; in a run dir,
19
+ per-prompt lengths are within +/-10% of the reference engine's.
20
+ unique_prompts (run dir) the load schedule never repeats a prompt inside a level.
21
+ spec_exact greedy outputs with speculation (A) match the engine without it (B);
22
+ early divergences may not exceed the A/A floor of B run twice.
23
+ hidden_probes T=1 requests (top_p 1, top_k off) must not equal the greedy output on
24
+ every prompt (temperature honoured; seeds reported); top-k logprobs must
25
+ be finite, normalised, sorted, and the greedy token must be the argmax.
26
+ rare_tokens CJK, emoji, code identifiers, long numbers and uncommon Unicode must
27
+ be reproduced verbatim (catches pruned vocabularies / logit masks); a
28
+ slip in the random ASCII nonces around a string is recorded, not a miss.
29
+
30
+ The live probes run as one randomly interleaved stream: each repeat (and each perturbed
31
+ prompt) follows its first-seen request a few requests later, mixed with every other probe,
32
+ so an engine cannot tell a probe by its position and slow drift over the run affects
33
+ both members of a pair alike. Nothing about a probe is predictable from this code: the
34
+ seed is drawn fresh for every run (and recorded), every probe prompt carries a random
35
+ tag, the echo instruction, its delimiters and padding are drawn from pools, and the rare
36
+ strings are sampled from a pool and generated at random around a random nonce. For an
37
+ evaluation, run the probes on the sealed prompt set (``--prompts sealed.jsonl``) so not
38
+ even the underlying prompts were public.
39
+
40
+ Refusing a probe never helps: a probe that gets an HTTP error or a broken stream is
41
+ evidence. A refused rare string counts as a miss (unless the reference engine refuses it
42
+ too); a refused cache, count, ignore_eos, sampling or logprob probe makes its gate at
43
+ least UNPROVEN, with the status and error listed. Only when the endpoint answers no
44
+ probe at all are the live gates NOT RUN. A gate that the inputs asked for (or allowed)
45
+ and that ends NOT RUN makes the record's overall verdict UNPROVEN, never PASS. The
46
+ optional extensions the probes add (``Client.OPTIONAL``: token ids, special tokens in the
47
+ stream, top_p / top_k on the samples) are the exception: a server that rejects one with a
48
+ 400 gets the request again without it, and the reply records the dropped field.
49
+
50
+ Limitation: the hidden probes run in their own sequential stream (concurrency 1), not
51
+ inside the ``bench serve`` load, so an engine that behaves only when idle is not caught
52
+ by them; ``probe_bodies()`` returns the probe requests for a load runner to mix in.
53
+
54
+ The SSE client here is deliberately small and private to the gates (the ``bench serve``
55
+ client is the timing reference). Times are ``time.perf_counter_ns`` on the client.
56
+
57
+ Run directory layout read by the static checks (as bench serve writes it):
58
+ outputs/<engine>/<bucket>/<id>.json {"text", "token_ids", "finish_reason", "usage", "error"?, ...}
59
+ inputs/<engine>/<bucket>/<id>.json the request body (max_tokens, ignore_eos / min_tokens);
60
+ inputs/<bucket>/<id>.json else the engine-independent copy
61
+ load mode (bench serve): inputs/<engine>/load-<level>/<bucket>/<id>[~k].json request bodies and
62
+ outputs/<engine>/load-<level>/<bucket>/<id>[~k].json ({"id", "salt", "level", ...});
63
+ or any ``*schedule*.jsonl`` / ``*requests*.jsonl`` with one load request per line:
64
+ {"level": <concurrency or rate>, "prompt_id"|"id": "...", "salt"?: "...", "prompt_sha256"?: "...",
65
+ "engine"?: "..."}
66
+ """
67
+
68
+ from __future__ import annotations
69
+
70
+ import hashlib
71
+ import json
72
+ import math
73
+ import os
74
+ import random
75
+ import secrets
76
+ import statistics
77
+ import time
78
+ import unicodedata
79
+ import urllib.error
80
+ import urllib.request
81
+ from dataclasses import dataclass, field
82
+ from pathlib import Path
83
+
84
+ from inferencekit import evidence as ev
85
+ from inferencekit import prompts as ps
86
+ from inferencekit.ops.correctness import first_divergence
87
+ from inferencekit.ops.intake import apply_pin
88
+ from inferencekit.registry import Arg, OpError, operation
89
+ from inferencekit.stats import combine_claims
90
+
91
+ GATES = ("caching", "token_count", "output_length", "unique_prompts", "spec_exact", "hidden_probes", "rare_tokens")
92
+ LIVE = {"caching", "token_count", "output_length", "spec_exact", "hidden_probes", "rare_tokens"}
93
+ STATIC = {"token_count", "output_length", "unique_prompts"}
94
+ # "gates" is a v2 evidence kind; on a v1-only store the record falls back to "correctness".
95
+ KIND = "gates" if "gates" in ev.KINDS else "correctness"
96
+
97
+ # Strings a model must copy verbatim, by the part of the vocabulary a pruned vocabulary or a
98
+ # logit mask would drop first. Each run samples one string per class and wraps it in random
99
+ # ASCII nonces; "code" and "number" are generated fresh every run.
100
+ RARE_POOL = {
101
+ "cjk": ("東京特許許可局許可局長", "鬱蒼とした森の奥で", "魑魅魍魎が跋扈する", "齟齬を来す麒麟と鸚鵡", "饕餮纹的青铜鼎",
102
+ "薔薇の棘と檸檬の皮"),
103
+ "hangul": ("한국어 맞춤법 검사기", "훈민정음 해례본", "닭볶음탕과 떡볶이"),
104
+ "emoji": ("🦀🧬🪐🫧", "🦩🪼🫎🪿", "🧿🪬🫀🫁", "🛸🌌🔭🧪", "🦔🪵🍄🌰"),
105
+ "emoji_zwj": ("👩🏽‍🔬 👨‍👩‍👧‍👦", "🧑🏿‍🚀 👩‍🌾", "🧑‍🤝‍🧑 👨🏻‍💻"),
106
+ "unicode": ("ꙮ ᚠᚢᚦᚨᚱᚲ Ⱎ ꓘ", "ᚠᛇᚻ ᛒᛦᚦ ᚹᛁᚾ", "Ꭰ ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ", "ꝏ ȸ ȹ ʬ ʭ"),
107
+ "devanagari": ("नमस्ते दुनिया", "संस्कृतम् भाषा", "क्षत्रिय ज्ञान"),
108
+ "arabic": ("مرحبا بالعالم", "اللغة العربية الفصحى", "خوارزمية الترتيب"),
109
+ "math": ("Ελληνικά ∮ E⋅da = Q, ∀x∈ℝ", "∑ᵢ xᵢ² ≤ ∏ⱼ ‖vⱼ‖", "ℵ₀ < 𝔠 ⊆ ℘(ℕ)"),
110
+ }
111
+ RARE_STRINGS = tuple((cls, s) for cls, xs in RARE_POOL.items() for s in xs) # the fixed part of the pool
112
+ # Echo prompts: instruction phrasings x delimiter pairs x padding, all drawn per probe pair.
113
+ ECHO_TEMPLATES = (
114
+ "Repeat the text between {o} and {c} exactly, character for character. Output only that text.\n{o}{s}{c}",
115
+ "Copy the string inside {o} {c} verbatim and reply with nothing else.\n{o}{s}{c}",
116
+ "Please echo back exactly what appears between {o} and {c}, keeping every character unchanged.\n\n{o}{s}{c}",
117
+ "Echo task: return the characters enclosed by {o} and {c}, unchanged, with no commentary. {o}{s}{c}",
118
+ "Here is a string: {o}{s}{c}\nWrite it out again exactly as given and output only that string.",
119
+ )
120
+ ECHO_DELIMS = (("<<<", ">>>"), ("[[", "]]"), ("{{", "}}"), ("«", "»"), ("⟦", "⟧"), ("BEGIN:", ":END"))
121
+ PADDING = ("", "Reference {n}.", "(ticket {n})", "Request id {n}. Thanks.", "Batch {n}: one item.", "Note {n}.")
122
+ INSTANT = 1e6 # "speedup" recorded when a repeat streams all its tokens in one instant
123
+ NONCE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
124
+
125
+
126
+ # --------------------------------------------------------------------------- SSE client
127
+
128
+ @dataclass
129
+ class Reply:
130
+ """One streamed completion, as the client saw it."""
131
+
132
+ status: int = 0
133
+ error: str | None = None
134
+ text: str = ""
135
+ reasoning: str = "" # delta.reasoning_content of reasoning models (counted as output tokens)
136
+ token_ids: list | None = None
137
+ lp: list | None = None # logprobs.content entries, one per token
138
+ finish_reason: str | None = None
139
+ usage: dict | None = None
140
+ ttft_ms: float | None = None
141
+ total_ms: float | None = None
142
+ t_first_ns: int | None = None
143
+ t_last_ns: int | None = None
144
+ chunks: int = 0
145
+ dropped: list = field(default_factory=list) # request fields the server rejected
146
+
147
+ @property
148
+ def ok(self) -> bool:
149
+ return self.status == 200 and self.error is None
150
+
151
+ @property
152
+ def full_text(self) -> str:
153
+ """Everything generated: reasoning (if streamed separately) then the answer."""
154
+ return self.reasoning + self.text
155
+
156
+ def streamed_count(self) -> tuple[int | None, str | None]:
157
+ """Tokens counted from the stream itself: token ids, else logprob entries."""
158
+ if self.token_ids:
159
+ return len(self.token_ids), "token_ids"
160
+ if self.lp:
161
+ return len(self.lp), "logprobs"
162
+ return None, None
163
+
164
+ def usage_count(self) -> int | None:
165
+ u = self.usage or {}
166
+ v = u.get("completion_tokens")
167
+ return v if isinstance(v, int) else None
168
+
169
+ def decode_tok_s(self) -> float | None:
170
+ n, _ = self.streamed_count()
171
+ n = n if n is not None else self.usage_count()
172
+ if not n or n < 2 or self.t_first_ns is None or self.t_last_ns is None or self.t_last_ns <= self.t_first_ns:
173
+ return None
174
+ return (n - 1) / ((self.t_last_ns - self.t_first_ns) / 1e9)
175
+
176
+ def summary(self) -> dict:
177
+ n, how = self.streamed_count()
178
+ return {"status": self.status, "error": self.error, "text": self.text[:2000],
179
+ "reasoning": self.reasoning[:2000] or None, "finish_reason": self.finish_reason,
180
+ "usage_completion_tokens": self.usage_count(), "streamed_tokens": n, "streamed_method": how,
181
+ "ttft_ms": self.ttft_ms, "total_ms": self.total_ms, "decode_tok_s": self.decode_tok_s(),
182
+ "chunks": self.chunks, "dropped_fields": self.dropped}
183
+
184
+
185
+ def chat_url(base_url: str) -> str:
186
+ base = base_url.rstrip("/")
187
+ if base.endswith("/chat/completions"):
188
+ return base
189
+ return base + ("/chat/completions" if base.endswith("/v1") else "/v1/chat/completions")
190
+
191
+
192
+ def models_url(base_url: str) -> str:
193
+ base = base_url.rstrip("/")
194
+ for suffix in ("/chat/completions",):
195
+ if base.endswith(suffix):
196
+ base = base[: -len(suffix)]
197
+ return base + ("/models" if base.endswith("/v1") else "/v1/models")
198
+
199
+
200
+ class Client:
201
+ """Minimal streaming client for an OpenAI-compatible chat endpoint."""
202
+
203
+ # Extensions the probes add. A 400 on a request carrying ones the server has not yet accepted is
204
+ # retried without them (top_k alone first, see SUSPECT_FIRST); a field refused that way is left out
205
+ # of later requests too.
206
+ OPTIONAL = ("return_token_ids", "skip_special_tokens", "top_p", "top_k")
207
+
208
+ def __init__(self, base_url: str, model: str | None, api_key: str | None = None, timeout: float = 120.0,
209
+ token_ids: bool = True, extra: dict | None = None):
210
+ self.base_url, self.api_key, self.timeout = base_url, api_key, timeout
211
+ self.ask_ids = token_ids
212
+ self.extra = dict(extra or {})
213
+ self.accepted: set[str] = set() # optional fields seen in a request that got a 200
214
+ self.rejected: set[str] = set() # optional fields the server refused with a 400
215
+ self.model = model or self._first_model()
216
+
217
+ def _headers(self) -> dict:
218
+ h = {"Content-Type": "application/json", "Accept": "text/event-stream"}
219
+ if self.api_key:
220
+ h["Authorization"] = f"Bearer {self.api_key}"
221
+ return h
222
+
223
+ def _first_model(self) -> str:
224
+ req = urllib.request.Request(models_url(self.base_url), headers=self._headers())
225
+ try:
226
+ with urllib.request.urlopen(req, timeout=self.timeout) as r: # noqa: S310 - user-given endpoint
227
+ data = json.loads(r.read().decode("utf-8"))
228
+ return data["data"][0]["id"]
229
+ except (urllib.error.URLError, OSError, ValueError, KeyError, IndexError) as e:
230
+ raise OpError(f"cannot list models at {models_url(self.base_url)} ({e}); pass --model") from None
231
+
232
+ def body(self, messages, max_tokens, temperature=0.0, seed=None, ignore_eos=False, logprobs=False,
233
+ top_logprobs=0, full_vocab=False, keep_special=False) -> dict:
234
+ """One request. ``full_vocab``: sample from the whole distribution (top_p 1, top_k off), not
235
+ the model's default truncation. ``keep_special``: stream special tokens too, so every counted
236
+ token shows up in the stream (vLLM and SGLang ``skip_special_tokens``)."""
237
+ b = {"model": self.model, "messages": messages, "max_tokens": max_tokens, "temperature": temperature,
238
+ "stream": True, "stream_options": {"include_usage": True}}
239
+ if seed is not None:
240
+ b["seed"] = seed
241
+ if ignore_eos:
242
+ b["ignore_eos"] = True
243
+ if logprobs:
244
+ b["logprobs"] = True
245
+ if top_logprobs:
246
+ b["top_logprobs"] = top_logprobs
247
+ if full_vocab:
248
+ b["top_p"], b["top_k"] = 1.0, -1
249
+ if keep_special:
250
+ b["skip_special_tokens"] = False
251
+ if self.ask_ids:
252
+ b["return_token_ids"] = True
253
+ for k, v in self.extra.items():
254
+ b.setdefault(k, v)
255
+ return b
256
+
257
+ def _ours(self, body: dict) -> list[str]:
258
+ """Optional fields in ``body`` that the probes set (a value from --extra-body is the user's)."""
259
+ return [k for k in self.OPTIONAL if k in body and (k not in self.extra or self.extra[k] != body[k])]
260
+
261
+ def _without(self, body: dict, keys: list[str]) -> dict:
262
+ out = {k: v for k, v in body.items() if k not in keys}
263
+ out.update({k: self.extra[k] for k in keys if k in self.extra}) # the user's own value, if any
264
+ return out
265
+
266
+ # Dropped alone first on a 400, so that refusing it does not take the other fields with it: top_k is
267
+ # the one servers refuse most (the OpenAI API has no top_k; some servers want top_k > 0).
268
+ SUSPECT_FIRST = ("top_k",)
269
+
270
+ def send(self, body: dict) -> Reply:
271
+ known = [k for k in self._ours(body) if k in self.rejected] # refused before: do not ask again
272
+ if known:
273
+ body = self._without(body, known)
274
+ reply = self._send(body)
275
+ # Only fields this server has not already taken can be the reason for a 400.
276
+ unsure = [k for k in self._ours(body) if k not in self.accepted] if reply.status == 400 else []
277
+ tries = [[k] for k in self.SUSPECT_FIRST if k in unsure and len(unsure) > 1] + ([unsure] if unsure else [])
278
+ drop: list[str] = []
279
+ for drop in tries:
280
+ reply = self._send(self._without(body, drop))
281
+ if reply.status != 400:
282
+ break
283
+ if drop and reply.ok:
284
+ body = self._without(body, drop)
285
+ self.rejected.update(drop)
286
+ if "return_token_ids" in drop:
287
+ self.ask_ids = False
288
+ if reply.ok:
289
+ self.accepted.update(self._ours(body))
290
+ reply.dropped = known + drop
291
+ return reply
292
+
293
+ def _send(self, body: dict) -> Reply:
294
+ rep = Reply()
295
+ req = urllib.request.Request(chat_url(self.base_url), data=json.dumps(body).encode("utf-8"),
296
+ headers=self._headers(), method="POST")
297
+ t0 = time.perf_counter_ns()
298
+ try:
299
+ resp = urllib.request.urlopen(req, timeout=self.timeout) # noqa: S310 - user-given endpoint
300
+ except urllib.error.HTTPError as e:
301
+ rep.status = e.code
302
+ rep.error = e.read()[:500].decode("utf-8", "replace")
303
+ return rep
304
+ except (urllib.error.URLError, OSError) as e:
305
+ rep.error = f"connection failed: {e}"
306
+ return rep
307
+ rep.status = resp.status
308
+ ids, lps = [], []
309
+ try:
310
+ with resp:
311
+ for raw in resp:
312
+ line = raw.decode("utf-8", "replace").strip()
313
+ if not line.startswith("data:"):
314
+ continue
315
+ payload = line[5:].strip()
316
+ if payload == "[DONE]":
317
+ break
318
+ try:
319
+ chunk = json.loads(payload)
320
+ except ValueError:
321
+ continue
322
+ now = time.perf_counter_ns()
323
+ rep.chunks += 1
324
+ if chunk.get("usage"):
325
+ rep.usage = chunk["usage"]
326
+ for ch in chunk.get("choices") or []:
327
+ delta = ch.get("delta") or {}
328
+ piece = delta.get("content") or ""
329
+ think = delta.get("reasoning_content") or delta.get("reasoning") or ""
330
+ tids = ch.get("token_ids") or delta.get("token_ids") or []
331
+ lpc = ((ch.get("logprobs") or {}).get("content")) or []
332
+ if piece or think or tids or lpc:
333
+ if rep.t_first_ns is None:
334
+ rep.t_first_ns = now
335
+ rep.ttft_ms = (now - t0) / 1e6
336
+ rep.t_last_ns = now
337
+ rep.text += piece
338
+ rep.reasoning += think
339
+ ids.extend(tids)
340
+ lps.extend(lpc)
341
+ if ch.get("finish_reason"):
342
+ rep.finish_reason = ch["finish_reason"]
343
+ except (OSError, ValueError) as e:
344
+ rep.error = f"stream broke: {e}"
345
+ rep.total_ms = (time.perf_counter_ns() - t0) / 1e6
346
+ rep.token_ids = ids or None
347
+ rep.lp = lps or None
348
+ return rep
349
+
350
+
351
+ # --------------------------------------------------------------------------- helpers
352
+
353
+ def load_tokenizer(name: str | None):
354
+ """A ``text -> token count`` function from a Hugging Face tokenizer, or None."""
355
+ if not name:
356
+ return None
357
+ try:
358
+ from transformers import AutoTokenizer # optional; not a dependency
359
+ except ImportError:
360
+ return None
361
+ tok = AutoTokenizer.from_pretrained(name)
362
+ return lambda text: len(tok(text, add_special_tokens=False)["input_ids"])
363
+
364
+
365
+ def _msgs(text: str) -> list[dict]:
366
+ return [{"role": "user", "content": text}]
367
+
368
+
369
+ def nonce(rng: random.Random, n: int = 16) -> str:
370
+ return "".join(rng.choice(NONCE_ALPHABET) for _ in range(n))
371
+
372
+
373
+ def perturb(s: str, rng: random.Random) -> str:
374
+ """The same string with exactly one character changed."""
375
+ i = rng.randrange(len(s))
376
+ choices = [c for c in NONCE_ALPHABET if c != s[i]]
377
+ return s[:i] + rng.choice(choices) + s[i + 1:]
378
+
379
+
380
+ def echo_style(rng: random.Random) -> tuple[str, tuple[str, str], str, bool]:
381
+ """(template, delimiters, padding, padding first?) for one echo probe or pair."""
382
+ pad = rng.choice(PADDING).format(n=nonce(rng, 6))
383
+ return rng.choice(ECHO_TEMPLATES), rng.choice(ECHO_DELIMS), pad, rng.random() < 0.5
384
+
385
+
386
+ def echo_prompt(s: str, style) -> str:
387
+ tmpl, (o, c), pad, first = style
388
+ body = tmpl.format(o=o, c=c, s=s)
389
+ if not pad:
390
+ return body
391
+ return f"{pad}\n{body}" if first else f"{body}\n{pad}"
392
+
393
+
394
+ def rare_targets(rng: random.Random) -> list[tuple[str, str]]:
395
+ """One string per class from the pool plus fresh code / number strings, each between random nonces."""
396
+ out = [(cls, rng.choice(xs)) for cls, xs in RARE_POOL.items()]
397
+ ident = "".join(rng.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(5))
398
+ out.append(("code", f"__{ident}_{nonce(rng, 4)}__ fn_{nonce(rng, 7)} {ident.capitalize()}Handler"
399
+ f"V{rng.randrange(2, 99)}"))
400
+ out.append(("number", "".join(rng.choice("0123456789") for _ in range(rng.randrange(30, 46)))))
401
+ out.append(("number", f"0x{rng.getrandbits(64):016x} {rng.randrange(1, 10)}.{rng.randrange(10**8):08d}e-"
402
+ f"{rng.randrange(10, 40)}"))
403
+ return [(cls, f"{nonce(rng, 4)} {t} {nonce(rng, 4)}") for cls, t in out]
404
+
405
+
406
+ def rare_core(target: str) -> str:
407
+ """The rare string itself, without the random ASCII nonce on each side ("<nonce> <string> <nonce>").
408
+
409
+ The nonces only make a probe unpredictable. A pruned vocabulary or a logit mask loses the rare
410
+ characters, never plain ASCII letters, so a slip in a nonce says nothing about the vocabulary."""
411
+ head, sep, rest = target.partition(" ")
412
+ core, sep2, _ = rest.rpartition(" ")
413
+ return core if sep and sep2 and core else target
414
+
415
+
416
+ def tag_prompt(text: str, rng: random.Random) -> str:
417
+ """The prompt with a random tag, so a probe is never a prompt seen before (dev set or not)."""
418
+ t = nonce(rng, 8)
419
+ forms = (f"[{t}] {text}", f"{text}\n\n(ref {t})", f"Request {t}:\n{text}", f"{text} #{t}")
420
+ return rng.choice(forms)
421
+
422
+
423
+ def reproduced(target: str, text: str) -> bool:
424
+ """Verbatim (or Unicode-NFC-equivalent) occurrence of target in text."""
425
+ return target in text or unicodedata.normalize("NFC", target) in unicodedata.normalize("NFC", text)
426
+
427
+
428
+ def _median(xs):
429
+ xs = [x for x in xs if x is not None]
430
+ return statistics.median(xs) if xs else None
431
+
432
+
433
+ def _gate(name, verdict, reason, metrics=None, details=None, limitations=None) -> dict:
434
+ return {"gate": name, "verdict": verdict, "reason": reason, "metrics": metrics or {},
435
+ "details": details or [], "limitations": limitations or []}
436
+
437
+
438
+ def _failed(jobs: list) -> list[dict]:
439
+ """Probes that got no usable reply (HTTP error, broken stream, never sent): evidence, not noise."""
440
+ out = []
441
+ for j in jobs:
442
+ r = j.reply
443
+ if r is None or not r.ok:
444
+ out.append({"kind": j.kind, "key": j.key, "status": r.status if r else None,
445
+ "error": ((r.error if r else "not sent") or "")[:300]})
446
+ return out
447
+
448
+
449
+ def _fail_note(failed: list[dict], what: str = "probe") -> str:
450
+ by: dict[str, int] = {}
451
+ for f in failed:
452
+ k = f"HTTP {f['status']}" if f["status"] else "no response"
453
+ by[k] = by.get(k, 0) + 1
454
+ first = next((f["error"] for f in failed if f["error"]), "")
455
+ return (f"{len(failed)} {what}(s) failed ({', '.join(f'{k} x{v}' for k, v in sorted(by.items()))}"
456
+ + (f"; first error: {first[:160]}" if first else "") + ")")
457
+
458
+
459
+ def alive(jobs: dict) -> tuple[bool, str]:
460
+ """(the endpoint answered at least one probe, first error otherwise)."""
461
+ allj = [j for js in jobs.values() for j in js if not j.kind.endswith("reference")]
462
+ if any(j.reply is not None and j.reply.ok for j in allj):
463
+ return True, ""
464
+ failed = _failed(allj)
465
+ return False, _fail_note(failed) if failed else "no probe was sent"
466
+
467
+
468
+ def _sequence(rep: Reply) -> tuple[list, str]:
469
+ """What to compare token by token: ids, else logprob token strings, else characters."""
470
+ if rep.token_ids:
471
+ return list(rep.token_ids), "token_ids"
472
+ if rep.lp:
473
+ return [e.get("token") for e in rep.lp], "logprob_tokens"
474
+ return list(rep.full_text), "characters"
475
+
476
+
477
+ # --------------------------------------------------------------------------- live probe plan
478
+
479
+ @dataclass
480
+ class Job:
481
+ kind: str
482
+ key: str
483
+ body: dict
484
+ meta: dict = field(default_factory=dict)
485
+ reply: Reply | None = None
486
+
487
+
488
+ class Prober:
489
+ def __init__(self, client: Client, max_tokens: int, seed: int, log: list):
490
+ self.c, self.max_tokens, self.rng, self.log = client, max_tokens, random.Random(seed), log
491
+
492
+ def job(self, kind, key, text, **kw) -> Job:
493
+ max_tokens = kw.pop("max_tokens", self.max_tokens)
494
+ meta = kw.pop("meta", {})
495
+ return Job(kind, key, self.c.body(_msgs(text), max_tokens, **kw), meta)
496
+
497
+ def run(self, jobs: list[Job], shuffle: bool = True) -> None:
498
+ order = list(jobs)
499
+ if shuffle:
500
+ self.rng.shuffle(order)
501
+ for j in order:
502
+ j.reply = self.c.send(j.body)
503
+ self.log.append({"kind": j.kind, "key": j.key, "meta": j.meta,
504
+ "request": {k: v for k, v in j.body.items() if k != "messages"},
505
+ "prompt_sha256": hashlib.sha256(json.dumps(j.body["messages"], ensure_ascii=False)
506
+ .encode("utf-8")).hexdigest(),
507
+ "reply": j.reply.summary()})
508
+
509
+
510
+ def stagger(first: list, then: list) -> list:
511
+ """[f0, f1, t0, f2, t1, ...]: each follow-up shortly after its first request, so slow drift
512
+ (clocks, other tenants) hits a first-seen request and its repeat alike."""
513
+ out = []
514
+ for i, f in enumerate(first):
515
+ out.append(f)
516
+ if i >= 1:
517
+ out.append(then[i - 1])
518
+ out += then[len(first) - 1:] if first else []
519
+ return out
520
+
521
+
522
+ def interleave(chains: list[list], rng: random.Random) -> list:
523
+ """A uniformly random merge of several sequences that keeps each sequence's own order."""
524
+ chains = [list(c) for c in chains if c]
525
+ out = []
526
+ while chains:
527
+ r = rng.randrange(sum(len(c) for c in chains))
528
+ for c in chains:
529
+ if r < len(c):
530
+ out.append(c.pop(0))
531
+ break
532
+ r -= len(c)
533
+ chains = [c for c in chains if c]
534
+ return out
535
+
536
+
537
+ N_COUNT, N_IGNORE_EOS, N_LOGPROBS = 4, 3, 3 # count, ignore_eos and logprob probes per run
538
+ SAMPLING_DEFAULT = 8 # hidden_probes sampling prompts when --sampling-prompts is not given
539
+
540
+
541
+ def probe_pool(items: list[dict]) -> list[dict]:
542
+ return [it for it in items if it["bucket"] in ("short", "medium")] or list(items)
543
+
544
+
545
+ def probes_needed(gates: set, n_cache: int, n_sampling: int) -> int:
546
+ """Distinct short/medium prompts the live probes draw: 2 warm-ups plus each wanted gate's share."""
547
+ return (2 + (n_cache if "caching" in gates else 0) + (N_COUNT if "token_count" in gates else 0)
548
+ + (N_IGNORE_EOS if "output_length" in gates else 0)
549
+ + (n_sampling + N_LOGPROBS if "hidden_probes" in gates else 0))
550
+
551
+
552
+ def sampling_count(pool_size: int, gates: set, n_cache: int, asked: int | None) -> tuple[int, str | None]:
553
+ """(sampling prompts, limitation or None). ``asked`` is used as given. Unset, it is SAMPLING_DEFAULT, or
554
+ what a smaller prompt set leaves room for (never below SAMPLING_MIN_FAIL), and the record says so."""
555
+ if asked is not None or "hidden_probes" not in gates:
556
+ return (SAMPLING_DEFAULT if asked is None else asked), None
557
+ n = min(SAMPLING_DEFAULT, max(SAMPLING_MIN_FAIL, pool_size - probes_needed(gates, n_cache, 0)))
558
+ if n == SAMPLING_DEFAULT:
559
+ return n, None
560
+ return n, (f"the prompt set has {pool_size} short/medium prompts, room for {n} sampling prompts, not "
561
+ f"{SAMPLING_DEFAULT}: an honest engine whose samples match greedy on a prompt with chance 1/3 "
562
+ f"FAILs with chance 1 in {3 ** n}, not 1 in {3 ** SAMPLING_DEFAULT} (derived; use a larger set, or "
563
+ "fewer --cache-prompts)")
564
+
565
+
566
+ def plan_and_run(client: Client, items: list[dict], *, gates: set, max_tokens: int, seed: int, n_cache: int,
567
+ log: list, reference: Client | None, echo_tokens: int = 64,
568
+ n_sampling: int = SAMPLING_DEFAULT) -> dict[str, list[Job]]:
569
+ """Build the probe jobs, run them as one randomly interleaved stream, return them grouped by kind.
570
+
571
+ Ordered pairs (first-seen -> repeat, original -> perturbed) stay in order but are
572
+ staggered and mixed with every other probe, so nothing marks a request as a probe and
573
+ drift over the run affects both members of a pair alike.
574
+ """
575
+ rng = random.Random(seed)
576
+ pr = Prober(client, max_tokens, seed, log)
577
+ pool = probe_pool(items)
578
+ need = probes_needed(gates, n_cache, n_sampling)
579
+ if len(pool) < need:
580
+ raise OpError(f"the prompt set has {len(pool)} short/medium prompts; the probes need {need} distinct ones "
581
+ "(use a larger set, or lower --sampling-prompts / --cache-prompts)")
582
+ rng.shuffle(pool)
583
+ take = iter(pool)
584
+
585
+ def nxt():
586
+ return tag_prompt(ps.prompt_text(next(take)), rng)
587
+
588
+ echo_max = max(max_tokens, echo_tokens)
589
+ warm = [pr.job("warmup", f"w{i}", nxt()) for i in range(2)]
590
+ pr.run(warm, shuffle=False)
591
+ chains, free, out = [], [], {"warmup": warm}
592
+ if "caching" in gates:
593
+ texts = [nxt() for _ in range(n_cache)]
594
+ first = [pr.job("cache_first", f"c{i}", t) for i, t in enumerate(texts)]
595
+ again = [pr.job("cache_repeat", f"c{i}", t) for i, t in enumerate(texts)]
596
+ nonces = [nonce(rng) for _ in range(4)]
597
+ styles = [echo_style(rng) for _ in nonces] # one style per pair: the pair differs in one character only
598
+ echo1 = [pr.job("echo_orig", f"e{i}", echo_prompt(s, st), meta={"nonce": s}, max_tokens=echo_max)
599
+ for i, (s, st) in enumerate(zip(nonces, styles))]
600
+ perturbed = [perturb(s, rng) for s in nonces]
601
+ echo2 = [pr.job("echo_perturbed", f"e{i}", echo_prompt(p, st), meta={"nonce": p, "orig": s},
602
+ max_tokens=echo_max) for i, (s, p, st) in enumerate(zip(nonces, perturbed, styles))]
603
+ chains += [stagger(first, again), stagger(echo1, echo2)]
604
+ out.update(cache_first=first, cache_repeat=again, echo_orig=echo1, echo_perturbed=echo2)
605
+ # Count and exact-length probes ask for special tokens in the stream: an engine that counts an
606
+ # end-of-turn token in usage but streams nothing for it would otherwise look like it lies.
607
+ if "token_count" in gates:
608
+ out["count"] = [pr.job("count", f"n{i}", nxt(), logprobs=True, keep_special=True) for i in range(N_COUNT)]
609
+ free += out["count"]
610
+ if "output_length" in gates:
611
+ out["ignore_eos"] = [pr.job("ignore_eos", f"l{i}", nxt(), ignore_eos=True, logprobs=True, keep_special=True)
612
+ for i in range(N_IGNORE_EOS)]
613
+ free += out["ignore_eos"]
614
+ if "hidden_probes" in gates:
615
+ samp = []
616
+ for i in range(n_sampling):
617
+ t = nxt()
618
+ sa, sb = rng.randrange(1, 2**31), rng.randrange(1, 2**31)
619
+ samp += [pr.job("greedy", f"s{i}", t),
620
+ pr.job("sample_a", f"s{i}", t, temperature=1.0, seed=sa, full_vocab=True),
621
+ pr.job("sample_a2", f"s{i}", t, temperature=1.0, seed=sa, full_vocab=True),
622
+ pr.job("sample_b", f"s{i}", t, temperature=1.0, seed=sb, full_vocab=True)]
623
+ lps = [pr.job("logprobs", f"p{i}", nxt(), logprobs=True, top_logprobs=5) for i in range(N_LOGPROBS)]
624
+ free += samp + lps
625
+ out.update(sampling=samp, logprobs=lps)
626
+ if "rare_tokens" in gates:
627
+ rare = [pr.job("rare", f"r{i}", echo_prompt(t, echo_style(rng)),
628
+ meta={"class": cls, "target": t, "core": rare_core(t)}, max_tokens=echo_max)
629
+ for i, (cls, t) in enumerate(rare_targets(rng))]
630
+ free += rare
631
+ out["rare"] = rare
632
+ if reference is not None:
633
+ ref_jobs = [Job("rare_reference", j.key, {**reference.body(j.body["messages"], j.body["max_tokens"])},
634
+ j.meta) for j in rare]
635
+ Prober(reference, max_tokens, seed, log).run(ref_jobs, shuffle=False)
636
+ out["rare_reference"] = ref_jobs
637
+ pr.run(interleave(chains + [[j] for j in free], rng), shuffle=False)
638
+ return out
639
+
640
+
641
+ def probe_bodies(seed: int | None = None, model: str = "", max_tokens: int = 64, n_sampling: int = SAMPLING_DEFAULT,
642
+ n_logprobs: int = N_LOGPROBS, prompts: str = "dev") -> list[dict]:
643
+ """Hidden-probe requests for a load runner to mix into its schedule (``{"id", "kind", "body"}``).
644
+
645
+ The sampling probes come in groups that share a prompt (greedy, two samples with one seed, one
646
+ with another); the logprob probes ask for top-5 logprobs. Run them inside the load, write their
647
+ replies as ``outputs/<engine>/probes/<id>.json`` and ``gates run --run-dir`` checks them with the
648
+ same rules as the live hidden_probes gate. ``optional`` lists body fields a runner may drop and
649
+ resend if the server answers 400 (as the live client does).
650
+ """
651
+ rng = random.Random(secrets.randbits(63) if seed is None else seed)
652
+ pool = [it for it in ps.load(prompts) if it["bucket"] in ("short", "medium")]
653
+ if len(pool) < n_sampling + n_logprobs:
654
+ raise OpError(f"the prompt set has {len(pool)} short/medium prompts; the probes need "
655
+ f"{n_sampling + n_logprobs} distinct ones")
656
+ rng.shuffle(pool)
657
+ take = iter(pool)
658
+ c = Client.__new__(Client) # body() only: no endpoint is contacted
659
+ c.model, c.ask_ids, c.extra = model, False, {}
660
+ out = []
661
+ sample = {"temperature": 1.0, "full_vocab": True}
662
+ for i in range(n_sampling):
663
+ t = tag_prompt(ps.prompt_text(next(take)), rng)
664
+ sa, sb = rng.randrange(1, 2**31), rng.randrange(1, 2**31)
665
+ for kind, kw in (("greedy", {}), ("sample_a", {**sample, "seed": sa}), ("sample_a2", {**sample, "seed": sa}),
666
+ ("sample_b", {**sample, "seed": sb})):
667
+ body = c.body(_msgs(t), max_tokens, **kw)
668
+ out.append({"id": f"probe-s{i}-{kind}", "kind": kind, "group": f"s{i}", "body": body,
669
+ "optional": [k for k in Client.OPTIONAL if k in body]})
670
+ for i in range(n_logprobs):
671
+ out.append({"id": f"probe-p{i}", "kind": "logprobs", "group": f"p{i}",
672
+ "body": c.body(_msgs(tag_prompt(ps.prompt_text(next(take)), rng)), max_tokens, logprobs=True,
673
+ top_logprobs=5), "optional": []})
674
+ return out
675
+
676
+
677
+ # --------------------------------------------------------------------------- live gates
678
+
679
+ def gate_caching(jobs: dict, threshold: float) -> dict:
680
+ first = {j.key: j.reply for j in jobs.get("cache_first", [])}
681
+ again = {j.key: j.reply for j in jobs.get("cache_repeat", [])}
682
+ failed = _failed([j for k in ("cache_first", "cache_repeat", "echo_orig", "echo_perturbed")
683
+ for j in jobs.get(k, [])])
684
+ rows, ttft_sp, dec_sp = [], [], []
685
+ for k, a in first.items():
686
+ b = again.get(k)
687
+ if not (a and b and a.ok and b.ok):
688
+ continue
689
+ t = a.ttft_ms / b.ttft_ms if a.ttft_ms and b.ttft_ms else None
690
+ d = (b.decode_tok_s() / a.decode_tok_s()) if a.decode_tok_s() and b.decode_tok_s() else None
691
+ if d is None and a.decode_tok_s() and b.t_first_ns is not None and b.t_first_ns == b.t_last_ns \
692
+ and (b.streamed_count()[0] or b.usage_count() or 0) >= 2:
693
+ d = INSTANT # every token arrived at once: nothing was decoded
694
+ ttft_sp.append(t)
695
+ dec_sp.append(d)
696
+ rows.append({"key": k, "first_ttft_ms": a.ttft_ms, "repeat_ttft_ms": b.ttft_ms, "ttft_speedup": t,
697
+ "first_decode_tok_s": a.decode_tok_s(), "repeat_decode_tok_s": b.decode_tok_s(),
698
+ "decode_speedup": d, "same_text": a.full_text == b.full_text})
699
+ mt, md = _median(ttft_sp), _median(dec_sp)
700
+ cached, unclear = [], 0
701
+ origs = {j.key: j for j in jobs.get("echo_orig", [])}
702
+ for j in jobs.get("echo_perturbed", []):
703
+ o = origs.get(j.key)
704
+ if not (o and o.reply and o.reply.ok and j.reply and j.reply.ok):
705
+ continue # counted in `failed`
706
+ new, old = j.meta["nonce"], j.meta["orig"]
707
+ # The old string coming back for the new prompt is a cache hit; a model that echoes
708
+ # neither string tells us nothing.
709
+ hit = old in j.reply.full_text and new not in j.reply.full_text
710
+ if hit:
711
+ cached.append({"key": j.key, "sent": new, "got": j.reply.full_text[:200], "first_reply_to": old})
712
+ elif new not in j.reply.full_text and old not in j.reply.full_text:
713
+ unclear += 1
714
+ judged = sum(1 for j in jobs.get("echo_perturbed", []) if j.reply and j.reply.ok
715
+ and origs.get(j.key) and origs[j.key].reply and origs[j.key].reply.ok) - unclear
716
+ metrics = {"ttft_speedup_median": mt, "decode_speedup_median": md, "pairs": len(rows),
717
+ "perturbed_cache_hits": len(cached), "perturbed_pairs": judged, "failed_probes": len(failed)}
718
+ lim = []
719
+ problems = []
720
+
721
+ def most(xs): # a speedup must be typical, not one lucky pair: >= 2/3 of pairs beyond the limit
722
+ xs = [x for x in xs if x is not None]
723
+ return bool(xs) and 3 * sum(1 for x in xs if x > 1 + threshold) >= 2 * len(xs)
724
+
725
+ if md is not None and md > 1 + threshold and most(dec_sp):
726
+ problems.append(f"repeated prompts decode {md:.2f}x faster than first-seen (output caching?)")
727
+ if mt is not None and mt > 1 + threshold and most(ttft_sp):
728
+ problems.append(f"repeated prompts reach the first token {mt:.2f}x faster than first-seen: a prefix or output "
729
+ "cache is on; benchmark rounds that replay prompts would be inflated (disable prefix "
730
+ "caching for timing, or use unique prompts per round)")
731
+ if cached:
732
+ problems.append(f"{len(cached)} prompt(s) changed by one character got the previous answer back "
733
+ "(similarity/semantic output cache)")
734
+ if unclear:
735
+ lim.append(f"{unclear} perturbed echo probe(s) were inconclusive (the model did not echo either string)")
736
+ if mt is None or md is None:
737
+ lim.append("timing pairs incomplete; the timing half of the gate is weaker")
738
+ if problems:
739
+ return _gate("caching", "FAIL", "; ".join(problems), metrics, rows + cached + failed, lim)
740
+ if failed:
741
+ return _gate("caching", "UNPROVEN", f"{_fail_note(failed)}: a refused probe cannot be judged, so the cache "
742
+ "check is incomplete", metrics, rows + failed, lim)
743
+ if not rows and not judged:
744
+ return _gate("caching", "UNPROVEN", "no first/repeat pair and no perturbed pair could be judged", metrics,
745
+ rows, lim)
746
+ if mt is None or md is None:
747
+ return _gate("caching", "UNPROVEN", "no cache hits on perturbed prompts, but the repeat timing could not be "
748
+ "measured", metrics, rows, lim)
749
+ return _gate("caching", "PASS", f"repeat speedup median TTFT {mt:.2f}x, decode {md:.2f}x (limit "
750
+ f"{1 + threshold:.2f}x); 0 perturbed prompts answered from cache", metrics, rows, lim)
751
+
752
+
753
+ def count_rows(replies: list[tuple], count_tokens) -> list[dict]:
754
+ """One row per usable reply; ``replies`` holds (key, reply) or (key, reply, max_tokens)."""
755
+ rows = []
756
+ for key, r, *mt in replies:
757
+ if not r or not r.ok:
758
+ continue
759
+ n, how = r.streamed_count()
760
+ retok = count_tokens(r.full_text) if count_tokens else None
761
+ rows.append({"key": key, "usage": r.usage_count(), "streamed": n, "streamed_method": how,
762
+ "retokenized": retok, "finish_reason": r.finish_reason, "max_tokens": mt[0] if mt else None})
763
+ return rows
764
+
765
+
766
+ # Replies whose text is not expected to re-tokenize to the generated count: after EOS (ignore_eos) an
767
+ # engine may generate special tokens it strips from the text, and a copied rare or echo string can be
768
+ # generated as a non-canonical split (17 generated tokens re-tokenize to 22, say).
769
+ RETOK_EXEMPT = ("ignore_eos:", "rare:", "echo_orig:", "echo_perturbed:")
770
+ # The copy probes (rare, echo) ask for no logprobs, so on an engine without token ids usage and the text
771
+ # are their only counts, and the gap is set aside the same way: the count probes check usage. (13 Oct:
772
+ # rare 'ArrF क्षत्रिय ज्ञान 6bzC' copied verbatim, usage 17, re-tokenized 14, nothing streamed; observed.)
773
+ # An ignore_eos reply asks for logprobs; without a stream its text stays the check on usage.
774
+ COPY_PROBES = ("rare:", "echo_orig:", "echo_perturbed:")
775
+ UNSTREAMED_EOS = "tokens not streamed: special tokens after EOS?"
776
+ UNSTREAMED_CUT = "tokens counted but not streamed (a held-back partial character at the cut?)"
777
+
778
+
779
+ def unstreamed(u, s, how, finish, max_tokens) -> bool:
780
+ """A logprob stream shorter than usage on a reply cut at max_tokens: the gap may be special tokens
781
+ (end of turn, padding) that were generated and counted but never streamed. The stream cannot tell
782
+ that from an engine that stops early and over-reports, so such a row is unproven, not failed."""
783
+ return (u is not None and s is not None and s < u and how == "logprobs" and finish == "length"
784
+ and max_tokens is not None and u == max_tokens)
785
+
786
+
787
+ def text_covers(t, u, retok_tol: float) -> bool:
788
+ """The re-tokenized text holds at least as many tokens as usage claims (and not wildly more): it
789
+ refutes over-reporting, the only cheat a short stream could hide."""
790
+ return t is not None and u is not None and u <= t <= u + max(2, retok_tol * u)
791
+
792
+
793
+ def judge_counts(rows: list[dict], retok_tol: float) -> dict:
794
+ """Sort count rows into {"bad": mismatches (FAIL), "unsettled": logprob streams short of usage at the
795
+ max_tokens limit (UNPROVEN), "by_text": such streams whose re-tokenized text covers usage (a
796
+ limitation), "retok_only": re-tokenize-only gaps on RETOK_EXEMPT rows (a limitation), "checked": rows
797
+ with >= 2 independent counts}."""
798
+ bad, unsettled, by_text, retok_only, checked = [], [], [], [], 0
799
+ for row in rows:
800
+ u, s, t = row["usage"], row["streamed"], row["retokenized"]
801
+ key = str(row.get("key", ""))
802
+ have = [x for x in (u, s, t) if x is not None]
803
+ if len(have) < 2:
804
+ continue
805
+ why = []
806
+ # An engine may count the end-of-sequence token in usage without streaming it.
807
+ slack = 1 if row.get("finish_reason") == "stop" else 0
808
+ stream_agrees = u is not None and s is not None and abs(u - s) <= slack
809
+ settled = False
810
+ if u is not None and s is not None and not stream_agrees:
811
+ if unstreamed(u, s, row.get("streamed_method"), row.get("finish_reason"), row.get("max_tokens")):
812
+ what = UNSTREAMED_EOS if key.startswith("ignore_eos:") else UNSTREAMED_CUT
813
+ if text_covers(t, u, retok_tol): # the text proves the count the stream could not
814
+ settled = True
815
+ by_text.append({**row, "problem": f"{u - s} {what}; the re-tokenized text ({t}) covers usage"})
816
+ else:
817
+ unsettled.append({**row, "problem": f"{u - s} {what}"})
818
+ else:
819
+ why.append(f"usage {u} != streamed {s}")
820
+ ref = s if s is not None else u
821
+ set_aside = False
822
+ if t is not None and ref is not None and abs(t - ref) > max(2, retok_tol * ref) and not settled:
823
+ if u is not None and key.startswith(RETOK_EXEMPT) and (s is not None or key.startswith(COPY_PROBES)):
824
+ # Stream and usage already judged this reply (agreeing, or caught above), or it is a copy
825
+ # probe with usage alone; the text is not a fair count here.
826
+ set_aside = s is None
827
+ retok_only.append({**row, "problem": f"re-tokenized {t} vs {ref}"})
828
+ else:
829
+ why.append(f"re-tokenized {t} vs {ref}")
830
+ checked += not set_aside # usage against the text alone, set aside: nothing was checked
831
+ if why:
832
+ bad.append({**row, "problem": "; ".join(why)})
833
+ return {"bad": bad, "unsettled": unsettled, "by_text": by_text, "retok_only": retok_only, "checked": checked}
834
+
835
+
836
+ def unstreamed_note(rows: list[dict], after_eos: bool | None = None) -> str:
837
+ """'a to b tokens not streamed: ...' for each kind of row: ignore_eos replies (``after_eos``, or by
838
+ key) and replies cut at max_tokens without it."""
839
+ out = []
840
+ for eos, what in ((True, UNSTREAMED_EOS), (False, UNSTREAMED_CUT)):
841
+ gaps = sorted(r["usage"] - r["streamed"] for r in rows
842
+ if (str(r.get("key", "")).startswith("ignore_eos:") if after_eos is None else after_eos) == eos)
843
+ if gaps:
844
+ out.append(f"{gaps[0]} to {gaps[-1]} {what}" if gaps[0] != gaps[-1] else f"{gaps[0]} {what}")
845
+ return "; ".join(out)
846
+
847
+
848
+ def gate_token_count(jobs: dict, count_tokens, retok_tol: float, static_rows: list[dict] | None = None) -> dict:
849
+ replies = [(f"{k}:{j.key}", j.reply, j.body.get("max_tokens")) for k, js in jobs.items()
850
+ if not k.endswith("reference") for j in js]
851
+ rows = count_rows(replies, count_tokens) + list(static_rows or [])
852
+ judged = judge_counts(rows, retok_tol)
853
+ bad, unsettled, checked = judged["bad"], judged["unsettled"], judged["checked"]
854
+ failed = _failed(jobs.get("count", []))
855
+ lim = []
856
+ if count_tokens is None:
857
+ lim.append("no tokenizer given: re-tokenized counts not checked (pass --tokenizer)")
858
+ if judged["retok_only"]:
859
+ lim.append(f"{len(judged['retok_only'])} ignore_eos / echo / rare repl(ies) re-tokenize to a different count "
860
+ "than they streamed or usage says (special tokens stripped from the text, or a copied string "
861
+ "generated as a non-canonical split); the re-tokenized gap is not counted as a mismatch (their "
862
+ "stream, where there is one, was judged against usage, and the count probes check usage)")
863
+ if judged["by_text"]:
864
+ lim.append(f"{len(judged['by_text'])} repl(ies) cut at max_tokens streamed fewer logprob entries than usage "
865
+ f"counts ({unstreamed_note(judged['by_text'])}), but their re-tokenized text covers usage: the "
866
+ "count is proven by the text, not the stream")
867
+ methods = sorted({r["streamed_method"] for r in rows if r.get("streamed_method")})
868
+ metrics = {"replies_checked": checked, "mismatches": len(bad), "unstreamed_at_limit": len(unsettled),
869
+ "unstreamed_settled_by_text": len(judged["by_text"]),
870
+ "retokenize_only_gaps": len(judged["retok_only"]), "streamed_methods": methods,
871
+ "failed_probes": len(failed)}
872
+ if bad:
873
+ return _gate("token_count", "FAIL", f"{len(bad)}/{checked} replies report a token count that the stream "
874
+ "does not support", metrics, bad[:50] + unsettled[:20] + failed, lim)
875
+ notes = []
876
+ if failed:
877
+ notes.append(_fail_note(failed, "count probe"))
878
+ if unsettled:
879
+ notes.append(f"{len(unsettled)}/{checked} replies cut at max_tokens stream fewer logprob entries than usage "
880
+ f"counts ({unstreamed_note(unsettled)}): the stream cannot prove the count (the engine returned "
881
+ "no token ids, and skip_special_tokens: false on the count and ignore_eos probes did not bring "
882
+ "the missing tokens into the stream" + ("; pass --tokenizer so the text can settle it)"
883
+ if count_tokens is None else ")"))
884
+ if notes:
885
+ return _gate("token_count", "UNPROVEN", "; ".join(notes) + f"; {checked - len(unsettled)} other replies agree",
886
+ metrics, unsettled[:50] + failed, lim)
887
+ if checked == 0:
888
+ verdict = "UNPROVEN" if rows else "NOT RUN"
889
+ return _gate("token_count", verdict, "no reply carried two independent token counts (no usage, token ids, "
890
+ "logprobs or tokenizer)", metrics, judged["retok_only"][:20], lim)
891
+ return _gate("token_count", "PASS", f"{checked} replies: usage, streamed and re-tokenized counts agree",
892
+ metrics, judged["by_text"][:20] + judged["retok_only"][:20], lim)
893
+
894
+
895
+ def gate_output_length_live(jobs: dict, count_tokens) -> dict:
896
+ rows, bad, stop_short, short_stream, by_text, usage_only = [], [], [], [], [], 0
897
+ failed = _failed(jobs.get("ignore_eos", []))
898
+ for j in jobs.get("ignore_eos", []):
899
+ r = j.reply
900
+ want = j.body["max_tokens"]
901
+ if not r or not r.ok:
902
+ continue
903
+ n, how = r.streamed_count()
904
+ if n is None and count_tokens is not None:
905
+ n, how = count_tokens(r.full_text), "retokenized"
906
+ if n is None:
907
+ n, how = r.usage_count(), "usage"
908
+ usage_only += 1
909
+ u, st = r.usage_count(), r.streamed_count()[0]
910
+ slack = 1 if r.finish_reason == "stop" else 0 # the EOS token may be counted on one side only
911
+ disagree = u is not None and st is not None and abs(u - st) > slack
912
+ row = {"key": j.key, "max_tokens": want, "length": n, "method": how, "finish_reason": r.finish_reason,
913
+ "usage": u, "streamed": st}
914
+ rows.append(row)
915
+ if how == "retokenized": # detokenize -> retokenize is not exact; allow a small slack
916
+ wrong = n is None or abs(n - want) > max(2, 0.02 * want)
917
+ else:
918
+ wrong = n != want
919
+ problems = []
920
+ if u is not None and u > want:
921
+ problems.append(f"usage {u} > max_tokens {want}")
922
+ if u is not None and st is not None and st > u + slack:
923
+ problems.append(f"streamed {st} > usage {u}")
924
+ if r.finish_reason == "length" and u is not None and u < want:
925
+ problems.append(f"finish_reason 'length' with usage {u} < max_tokens {want}")
926
+ if not wrong and not problems:
927
+ continue
928
+ if not problems and unstreamed(u, st, how, r.finish_reason, want):
929
+ # usage says max_tokens and the reply was cut there, but some counted tokens never reached the
930
+ # logprob stream (an engine that skips special tokens after EOS); the stream cannot settle it,
931
+ # re-tokenized text that covers max_tokens can.
932
+ retok = count_tokens(r.full_text) if count_tokens is not None else None
933
+ (by_text if text_covers(retok, u, 0.02) else short_stream).append(
934
+ {**row, "retokenized": retok, "problem": f"{u - st} {UNSTREAMED_EOS}"})
935
+ elif not problems and r.finish_reason == "stop" and not disagree and n is not None and n < want:
936
+ stop_short.append(row) # ended at EOS with consistent counts: ignore_eos not supported
937
+ else:
938
+ bad.append({**row, "problem": "; ".join(problems) or f"length {n} != max_tokens {want}"})
939
+ metrics = {"ignore_eos_requests": len(rows), "exact": len(rows) - len(bad) - len(stop_short) - len(short_stream),
940
+ "unstreamed_at_limit": len(short_stream), "unstreamed_settled_by_text": len(by_text),
941
+ "failed_probes": len(failed)}
942
+ lim = [f"{usage_only} length(s) known only from usage (no token ids, logprobs or tokenizer)"] if usage_only else []
943
+ if by_text:
944
+ lim.append(f"{len(by_text)} ignore_eos repl(ies) streamed fewer logprob entries than max_tokens "
945
+ f"({unstreamed_note(by_text, after_eos=True)}), but their re-tokenized text covers max_tokens: the "
946
+ "length is proven by the text, not the stream")
947
+ if bad:
948
+ return _gate("output_length", "FAIL", f"{len(bad)}/{len(rows)} ignore_eos replies are not max_tokens long "
949
+ "(finish_reason 'length' below max_tokens, usage above it, or usage disagrees with the stream)",
950
+ metrics, bad + short_stream + failed, lim)
951
+ if failed:
952
+ return _gate("output_length", "UNPROVEN", f"{_fail_note(failed, 'ignore_eos request')}: the engine refused "
953
+ "exact-length requests, so fixed-length runs cannot be checked live (compare lengths against a "
954
+ "reference engine: gates run --run-dir --reference-engine)", metrics, failed + rows, lim)
955
+ unsure = []
956
+ if stop_short:
957
+ unsure.append(f"{len(stop_short)}/{len(rows)} ignore_eos replies stopped at EOS (finish_reason 'stop', counts "
958
+ "consistent): the engine does not honour ignore_eos, so fixed-length runs are not fixed length; "
959
+ "compare lengths against a reference engine instead (gates run --run-dir --reference-engine)")
960
+ if short_stream:
961
+ unsure.append(f"{len(short_stream)}/{len(rows)} ignore_eos replies report usage = max_tokens and finish "
962
+ f"'length' but stream fewer logprob entries ({unstreamed_note(short_stream, after_eos=True)}): "
963
+ "the length is not proven by the stream (the engine returned no token ids and did not stream "
964
+ "the missing tokens with skip_special_tokens: false; compare lengths against a reference "
965
+ "engine: gates run --run-dir --reference-engine)")
966
+ if unsure:
967
+ return _gate("output_length", "UNPROVEN", "; ".join(unsure), metrics, stop_short + short_stream, lim)
968
+ if not rows:
969
+ return _gate("output_length", "NOT RUN", "no ignore_eos probe was sent", metrics)
970
+ verdict = "UNPROVEN" if usage_only == len(rows) else "PASS"
971
+ return _gate("output_length", verdict, f"{len(rows)} ignore_eos replies are exactly max_tokens long"
972
+ + (" (by usage only)" if verdict == "UNPROVEN" else ""), metrics, rows, lim)
973
+
974
+
975
+ def check_logprob_entry(e: dict, greedy: bool) -> list[str]:
976
+ out = []
977
+ lp = e.get("logprob")
978
+ top = e.get("top_logprobs") or []
979
+ vals = [t.get("logprob") for t in top]
980
+ if not isinstance(lp, (int, float)) or not math.isfinite(lp) or lp > 1e-6:
981
+ out.append(f"chosen logprob {lp!r} not finite and <= 0")
982
+ if any(not isinstance(v, (int, float)) or not math.isfinite(v) or v > 1e-6 for v in vals):
983
+ out.append("a top logprob is not finite and <= 0")
984
+ return out
985
+ if vals:
986
+ if any(vals[i] < vals[i + 1] - 1e-6 for i in range(len(vals) - 1)):
987
+ out.append("top_logprobs not sorted")
988
+ mass = sum(math.exp(v) for v in vals)
989
+ if mass > 1 + 1e-3:
990
+ out.append(f"top-k probability mass {mass:.4f} > 1 (not normalised)")
991
+ if greedy and isinstance(lp, (int, float)) and lp < max(vals) - 1e-3:
992
+ out.append(f"greedy token logprob {lp:.4f} < top logprob {max(vals):.4f} (greedy token is not the argmax)")
993
+ if greedy and top and top[0].get("token") != e.get("token") and isinstance(lp, (int, float)) \
994
+ and abs(lp - vals[0]) > 1e-3:
995
+ out.append(f"greedy token {e.get('token')!r} != top-1 {top[0].get('token')!r}")
996
+ return out
997
+
998
+
999
+ HIDDEN_LIMIT = ("probes ran in their own sequential stream (concurrency 1), not inside the benchmark load: an engine "
1000
+ "that behaves only when idle is not caught here (mix probe_bodies() into the load and pass --run-dir "
1001
+ "to check their replies)")
1002
+
1003
+
1004
+ # hidden_probes sampling rule. A confident model can return its greedy answer at T=1 by chance: on
1005
+ # 14 Oct, short extraction answers came back byte for byte equal to greedy from stock engines, on 2/3,
1006
+ # 0/3 and 1/3 prompts on three hosts (observed), so take p ~ 1/3 as the chance that both samples of a
1007
+ # prompt equal greedy (inferred). The old rule (FAIL when >= half of 3 prompts matched) then failed an
1008
+ # honest engine with P = 3p^2(1-p) + p^3 = 7/27, about 26% of runs (derived). FAIL now needs every one
1009
+ # of at least SAMPLING_MIN_FAIL prompts to match: with n = 8, P = p^8 = 1/6561, about 1.5e-4 (derived).
1010
+ # >= 3/4 matching is UNPROVEN (6-7 of 8: about 2% at p = 1/3, derived). An engine that ignores
1011
+ # temperature matches on every prompt, so it still FAILs.
1012
+ SAMPLING_MIN_FAIL = 4
1013
+
1014
+
1015
+ def sampling_verdict(equal: int, n: int) -> tuple[str, str]:
1016
+ """(verdict, reason) for ``equal`` of ``n`` prompts whose T=1 samples both equal the greedy output."""
1017
+ if n >= SAMPLING_MIN_FAIL and equal == n:
1018
+ return "FAIL", f"T=1 samples equal the greedy output on all {n} prompts (temperature ignored?)"
1019
+ if 4 * equal >= 3 * n:
1020
+ why = (f"with fewer than {SAMPLING_MIN_FAIL} prompts that can be chance" if n < SAMPLING_MIN_FAIL
1021
+ else "more than chance usually gives on a confident model, but not all, so temperature is not shown "
1022
+ "to be ignored")
1023
+ return "UNPROVEN", (f"T=1 samples equal the greedy output on {equal}/{n} prompts: {why} (rerun with more "
1024
+ "--sampling-prompts)")
1025
+ return "PASS", f"T=1 samples differ from greedy on {n - equal}/{n} prompts"
1026
+
1027
+
1028
+ def gate_hidden_probes(jobs: dict, where: str = "live") -> dict:
1029
+ by_key: dict[str, dict] = {}
1030
+ for j in jobs.get("sampling", []):
1031
+ by_key.setdefault(j.key, {})[j.kind] = j.reply
1032
+ samp_failed = _failed(jobs.get("sampling", []))
1033
+ rows, ignored, seed_ok, seed_checked = [], 0, 0, 0
1034
+ for k, d in by_key.items():
1035
+ g, a, a2, b = (d.get(x) for x in ("greedy", "sample_a", "sample_a2", "sample_b"))
1036
+ if not all(r and r.ok for r in (g, a, b)):
1037
+ continue
1038
+ same_as_greedy = a.full_text == g.full_text and b.full_text == g.full_text
1039
+ ignored += same_as_greedy
1040
+ seeded = None
1041
+ if a2 and a2.ok:
1042
+ seeded = a.full_text == a2.full_text
1043
+ seed_checked += 1
1044
+ seed_ok += bool(seeded)
1045
+ rows.append({"key": k, "samples_equal_greedy": same_as_greedy, "seed_reproduced": seeded,
1046
+ "seeds_differ": a.full_text != b.full_text})
1047
+ sub, details = [], list(rows)
1048
+ lim = [HIDDEN_LIMIT] if where == "live" else []
1049
+ metrics = {"sampling_prompts": len(rows), "samples_equal_greedy": ignored,
1050
+ "seed_reproduced": f"{seed_ok}/{seed_checked}", "failed_probes": len(samp_failed)}
1051
+ if rows:
1052
+ sub.append(sampling_verdict(ignored, len(rows)))
1053
+ if len(rows) < SAMPLING_MIN_FAIL:
1054
+ lim.append(f"only {len(rows)} sampling prompt(s) came back complete: too few to FAIL an engine that "
1055
+ f"ignores temperature (at least {SAMPLING_MIN_FAIL} are needed; the default is "
1056
+ f"{SAMPLING_DEFAULT})")
1057
+ if seed_checked and seed_ok < seed_checked:
1058
+ lim.append(f"the same seed reproduced the sample on {seed_ok}/{seed_checked} prompts: the engine ignores "
1059
+ "`seed` or samples nondeterministically (seeded comparisons are not possible)")
1060
+ refused = sorted({k for j in jobs.get("sampling", []) if j.reply
1061
+ for k in j.reply.dropped if k in ("top_p", "top_k")})
1062
+ if refused:
1063
+ lim.append(f"the server refused {' and '.join(refused)} on the T=1 samples (top_p 1 and top_k -1 ask for the "
1064
+ f"whole distribution), so they used its default {' and '.join(refused)}: a default that cuts the "
1065
+ "distribution makes samples equal to greedy more likely")
1066
+ if samp_failed:
1067
+ sub.append(("UNPROVEN", _fail_note(samp_failed, "sampling probe")))
1068
+ details += samp_failed
1069
+ elif not rows:
1070
+ sub.append(("UNPROVEN", "no sampling probe group came back complete"))
1071
+ lp_jobs = jobs.get("logprobs", [])
1072
+ lp_failed = _failed(lp_jobs)
1073
+ lp_missing = [j.key for j in lp_jobs if j.reply and j.reply.ok and not j.reply.lp]
1074
+ lp_bad, lp_entries, lp_replies = [], 0, 0
1075
+ for j in lp_jobs:
1076
+ r = j.reply
1077
+ if not r or not r.ok or not r.lp:
1078
+ continue
1079
+ lp_replies += 1
1080
+ for pos, e in enumerate(r.lp):
1081
+ lp_entries += 1
1082
+ for prob in check_logprob_entry(e, greedy=True):
1083
+ lp_bad.append({"key": j.key, "position": pos, "problem": prob})
1084
+ metrics.update(logprob_replies=lp_replies, logprob_positions=lp_entries, logprob_violations=len(lp_bad),
1085
+ logprob_failed=len(lp_failed), logprob_missing=len(lp_missing))
1086
+ if lp_bad:
1087
+ sub.append(("FAIL", f"{len(lp_bad)} logprob violations in {lp_entries} positions"))
1088
+ details += lp_bad[:50]
1089
+ elif lp_replies:
1090
+ sub.append(("PASS", f"{lp_entries} positions: logprobs finite, sorted, normalised, greedy == argmax"))
1091
+ if lp_failed:
1092
+ sub.append(("UNPROVEN", _fail_note(lp_failed, "logprob probe")))
1093
+ details += lp_failed
1094
+ if lp_missing:
1095
+ sub.append(("UNPROVEN", f"{len(lp_missing)}/{len(lp_jobs)} logprob probes came back without logprobs "
1096
+ "(logprobs were requested): the logprob check could not run"))
1097
+ if not lp_jobs:
1098
+ sub.append(("UNPROVEN", "no logprob probe"))
1099
+ verdict = combine_claims(v for v, _ in sub)
1100
+ return _gate("hidden_probes", verdict, "; ".join(f"{v}: {why}" for v, why in sub), metrics, details, lim)
1101
+
1102
+
1103
+ def gate_rare(jobs: dict) -> dict:
1104
+ # A probe is judged on the rare string itself (meta "core"); the random ASCII nonces around it only
1105
+ # make it unpredictable. In the 13-14 Oct records all 3 rare_tokens FAILs (two stock vLLM configs and
1106
+ # one custom engine, none with a reference engine; observed) were one miss each: the model cut the
1107
+ # trailing nonce ("... 72Ry" -> "... 72R", or dropped it) and ended at EOS well under max_tokens, with
1108
+ # the CJK / emoji / Hangul string intact. A pruned vocabulary or logit mask cannot cause that slip.
1109
+ ref = {j.key: j.reply for j in jobs.get("rare_reference", [])}
1110
+ rows, misses, ok_n, refused, slips = [], [], 0, 0, []
1111
+ for j in jobs.get("rare", []):
1112
+ r = j.reply
1113
+ rr = ref.get(j.key)
1114
+ core = j.meta.get("core") or j.meta["target"]
1115
+ ref_errored = rr is not None and not rr.ok
1116
+ ref_hit = reproduced(core, rr.full_text) if rr is not None and rr.ok else None
1117
+ row = {"key": j.key, "class": j.meta["class"], "target": j.meta["target"], "reference_reproduced": ref_hit}
1118
+ if not r or not r.ok:
1119
+ # A refused rare string is a miss (it is exactly what a pruned vocabulary would want to
1120
+ # hide), unless the reference engine refuses it too.
1121
+ refused += 1
1122
+ row.update(reproduced=False, refused=True, status=r.status if r else None,
1123
+ error=((r.error if r else "not sent") or "")[:200], reference_refused=ref_errored)
1124
+ rows.append(row)
1125
+ if not ref_errored:
1126
+ misses.append(row)
1127
+ continue
1128
+ ok_n += 1
1129
+ verbatim = reproduced(j.meta["target"], r.full_text)
1130
+ hit = verbatim or reproduced(core, r.full_text)
1131
+ row.update(reproduced=hit, nonces_intact=verbatim, got=r.full_text[:200])
1132
+ rows.append(row)
1133
+ if hit and not verbatim:
1134
+ slips.append(row)
1135
+ if not hit and ref_hit is not False:
1136
+ misses.append(row)
1137
+ if not rows:
1138
+ return _gate("rare_tokens", "NOT RUN", "no rare-string probe was sent")
1139
+ lim = [] if ref else ["no reference engine: a miss may be the model's own weakness (pass --reference-url)"]
1140
+ if slips:
1141
+ lim.append(f"{len(slips)} rare string(s) came back intact inside a slipped random ASCII nonce (a model copy "
1142
+ "error; an engine that drops rare tokens keeps plain ASCII), not counted as misses")
1143
+ metrics = {"probes": len(rows), "answered": ok_n, "refused": refused,
1144
+ "reproduced": sum(1 for r in rows if r["reproduced"]), "nonce_slips": len(slips),
1145
+ "counted_misses": len(misses)}
1146
+ if misses:
1147
+ classes = sorted({m["class"] for m in misses})
1148
+ n_ref = sum(1 for m in misses if m.get("refused"))
1149
+ return _gate("rare_tokens", "FAIL", f"{len(misses)}/{len(rows)} rare strings not reproduced verbatim "
1150
+ f"({', '.join(classes)})" + (f"; {n_ref} of them refused with an error" if n_ref else ""),
1151
+ metrics, misses, lim)
1152
+ n_rep = metrics["reproduced"]
1153
+ said = f"{n_rep} rare strings reproduced verbatim" + (f" ({len(slips)} inside a slipped ASCII nonce)" if slips
1154
+ else "")
1155
+ shared = ok_n - n_rep
1156
+ said += f"; {shared} missed by the reference engine too (not counted)" if shared else ""
1157
+ if refused:
1158
+ return _gate("rare_tokens", "UNPROVEN", f"{said}; {refused} refused by both engines (not judged)", metrics,
1159
+ [r for r in rows if r.get("refused")] + slips, lim)
1160
+ return _gate("rare_tokens", "PASS", said, metrics, slips, lim)
1161
+
1162
+
1163
+ # --------------------------------------------------------------------------- spec exactness
1164
+
1165
+ def spec_exact(a: Client, b: Client, items: list[dict], max_tokens: int, early_tokens: int, tolerance: int,
1166
+ log: list) -> dict:
1167
+ """A (speculative) vs B (plain), and B vs B again for the A/A floor, greedy, same prompts."""
1168
+ rows = []
1169
+ for it in items:
1170
+ msgs = it["messages"]
1171
+ rb = b.send(b.body(msgs, max_tokens))
1172
+ ra = a.send(a.body(msgs, max_tokens))
1173
+ rb2 = b.send(b.body(msgs, max_tokens))
1174
+ for tag, r in (("B", rb), ("A", ra), ("B2", rb2)):
1175
+ log.append({"kind": f"spec_{tag}", "key": it["id"], "reply": r.summary()})
1176
+ if not (ra.ok and rb.ok and rb2.ok):
1177
+ rows.append({"id": it["id"], "error": ra.error or rb.error or rb2.error})
1178
+ continue
1179
+ sa, ma = _sequence(ra)
1180
+ sb, mb = _sequence(rb)
1181
+ sb2, _ = _sequence(rb2)
1182
+ if ma != mb: # compare like with like
1183
+ sa, sb, sb2, ma = list(ra.full_text), list(rb.full_text), list(rb2.full_text), "characters"
1184
+ rows.append({"id": it["id"], "bucket": it.get("bucket"), "method": ma,
1185
+ "ab_first_divergence": first_divergence(sa, sb),
1186
+ "aa_first_divergence": first_divergence(sb, sb2), "len_a": len(sa), "len_b": len(sb)})
1187
+ ok = [r for r in rows if "error" not in r]
1188
+ errs = [r for r in rows if "error" in r]
1189
+ if not ok:
1190
+ return _gate("spec_exact", "UNPROVEN" if rows else "NOT RUN", "no prompt got three successful replies"
1191
+ + (f" ({len(errs)} failed; first error: {str(errs[0]['error'])[:160]})" if errs else ""), {}, rows)
1192
+
1193
+ def early(key):
1194
+ return sum(1 for r in ok if r[key] is not None and r[key] < early_tokens)
1195
+
1196
+ ab_div = [r["ab_first_divergence"] for r in ok if r["ab_first_divergence"] is not None]
1197
+ aa_div = [r["aa_first_divergence"] for r in ok if r["aa_first_divergence"] is not None]
1198
+ e_ab, e_aa = early("ab_first_divergence"), early("aa_first_divergence")
1199
+ metrics = {"prompts": len(ok), "ab_identical": len(ok) - len(ab_div), "aa_identical": len(ok) - len(aa_div),
1200
+ "ab_early_divergences": e_ab, "aa_early_divergences": e_aa, "early_tokens": early_tokens,
1201
+ "ab_first_divergence_median": _median(ab_div), "aa_first_divergence_median": _median(aa_div),
1202
+ "methods": sorted({r["method"] for r in ok})}
1203
+ lim = []
1204
+ if "characters" in metrics["methods"]:
1205
+ lim.append("no token ids or logprobs: compared characters (divergence positions are in characters)")
1206
+ diverged = [r for r in ok if r["ab_first_divergence"] is not None][:50]
1207
+ if e_ab > e_aa + tolerance:
1208
+ return _gate("spec_exact", "FAIL", f"{e_ab}/{len(ok)} prompts diverge from the non-speculative engine before "
1209
+ f"token {early_tokens}; the A/A floor is {e_aa} (+{tolerance} allowed)", metrics,
1210
+ diverged + errs, lim)
1211
+ # Late divergences: one prompt in eight above the A/A floor is allowed for reordered float
1212
+ # sums in the verify pass; more is not proven exact (the quality gates must decide).
1213
+ late_allow = tolerance + max(1, len(ok) // 8)
1214
+ metrics["late_allowed"] = late_allow
1215
+ if len(ab_div) > len(aa_div) + late_allow:
1216
+ return _gate("spec_exact", "UNPROVEN", f"{len(ab_div)}/{len(ok)} prompts diverge from the non-speculative "
1217
+ f"engine (all at or after token {early_tokens}) vs an A/A floor of {len(aa_div)} (+{late_allow} "
1218
+ "allowed): speculation is not shown to be exact; compare quality before publishing", metrics,
1219
+ diverged + errs, lim)
1220
+ if errs:
1221
+ return _gate("spec_exact", "UNPROVEN", f"{len(errs)} prompt(s) failed (first error: "
1222
+ f"{str(errs[0]['error'])[:160]}); on the other {len(ok)}: {len(ok) - len(ab_div)} identical",
1223
+ metrics, diverged + errs, lim)
1224
+ return _gate("spec_exact", "PASS", f"{len(ok) - len(ab_div)}/{len(ok)} identical; early divergences {e_ab} <= "
1225
+ f"A/A floor {e_aa} + {tolerance}; any divergence {len(ab_div)} <= A/A {len(aa_div)} + {late_allow}",
1226
+ metrics, diverged, lim)
1227
+
1228
+
1229
+ # --------------------------------------------------------------------------- static run-dir checks
1230
+
1231
+ def _read_json(p: Path):
1232
+ try:
1233
+ return json.loads(p.read_text(encoding="utf-8"))
1234
+ except (OSError, ValueError):
1235
+ return None
1236
+
1237
+
1238
+ def _out_len(o: dict) -> tuple[int | None, str | None]:
1239
+ """Generated length of a saved output, or (None, None) when it is unreadable or the request failed."""
1240
+ if o.get("error"):
1241
+ return None, None
1242
+ if isinstance(o.get("token_ids"), list):
1243
+ return len(o["token_ids"]), "token_ids"
1244
+ u = (o.get("usage") or {}).get("completion_tokens")
1245
+ return (u, "usage") if isinstance(u, int) else (None, None)
1246
+
1247
+
1248
+ def outputs_of(root: Path) -> dict[str, dict]:
1249
+ """{id: output} for outputs/<engine>/<bucket>/<id>.json (root = outputs/<engine>; load-* and probes skipped)."""
1250
+ out = {}
1251
+ if root.is_dir():
1252
+ for p in sorted(root.glob("*/*.json")):
1253
+ if p.parent.name.startswith("load-") or p.parent.name == "probes":
1254
+ continue
1255
+ o = _read_json(p)
1256
+ out[p.stem] = {**o, "_bucket": p.parent.name} if isinstance(o, dict) else {"_bucket": p.parent.name,
1257
+ "_unreadable": True}
1258
+ return out
1259
+
1260
+
1261
+ def static_counts(run_dir: Path, engine: str) -> list[dict]:
1262
+ rows = []
1263
+ for k, o in outputs_of(run_dir / "outputs" / engine).items():
1264
+ if o.get("_unreadable") or o.get("error"):
1265
+ continue
1266
+ ids = o.get("token_ids")
1267
+ u = (o.get("usage") or {}).get("completion_tokens")
1268
+ rows.append({"key": f"run:{k}", "usage": u if isinstance(u, int) else None,
1269
+ "streamed": len(ids) if isinstance(ids, list) else None,
1270
+ "streamed_method": "token_ids" if isinstance(ids, list) else None, "retokenized": None,
1271
+ "finish_reason": o.get("finish_reason")})
1272
+ return rows
1273
+
1274
+
1275
+ def _inputs_of(run_dir: Path, engine: str) -> dict[str, dict]:
1276
+ """{id: request body}: inputs/<engine>/<bucket>/ (bench serve) else the shared inputs/<bucket>/."""
1277
+ out: dict[str, dict] = {}
1278
+ for root in (run_dir / "inputs", run_dir / "inputs" / engine): # the engine's own copy wins
1279
+ if not root.is_dir():
1280
+ continue
1281
+ for p in sorted(root.glob("*/*.json")):
1282
+ if p.parent.name.startswith("load-"):
1283
+ continue
1284
+ body = _read_json(p)
1285
+ if isinstance(body, dict) and ("max_tokens" in body or "messages" in body or "prompt" in body):
1286
+ out[p.stem] = body
1287
+ return out
1288
+
1289
+
1290
+ def _exact_length(req: dict) -> int | None:
1291
+ """max_tokens when the request forces an exact length (ignore_eos, or min_tokens == max_tokens)."""
1292
+ mt = req.get("max_tokens") if isinstance(req.get("max_tokens"), int) else req.get("max_completion_tokens")
1293
+ if not isinstance(mt, int):
1294
+ return None
1295
+ if req.get("ignore_eos") or req.get("min_tokens") == mt:
1296
+ return mt
1297
+ return None
1298
+
1299
+
1300
+ def gate_output_length_static(run_dir: Path, engine: str, reference: Path | None, tol: float,
1301
+ max_outliers: float) -> dict:
1302
+ cand = outputs_of(run_dir / "outputs" / engine)
1303
+ if not cand:
1304
+ return _gate("output_length", "NOT RUN", f"no outputs under outputs/{engine}/")
1305
+ sub, details, metrics = [], [], {}
1306
+ # Exact-length requests must come back exactly max_tokens long; a missing or failed output is unproven.
1307
+ eos_bad, eos_missing, eos_n = [], [], 0
1308
+ for k, req in _inputs_of(run_dir, engine).items():
1309
+ want = _exact_length(req)
1310
+ if want is None:
1311
+ continue
1312
+ eos_n += 1
1313
+ o = cand.get(k)
1314
+ n, how = _out_len(o) if o and not o.get("_unreadable") else (None, None)
1315
+ if n is None:
1316
+ eos_missing.append({"id": k, "max_tokens": want, "problem": "no readable output" if not o or
1317
+ o.get("_unreadable") else f"request failed: {str(o.get('error'))[:120]}"})
1318
+ elif n != want:
1319
+ eos_bad.append({"id": k, "max_tokens": want, "length": n, "method": how})
1320
+ if eos_n:
1321
+ v = "FAIL" if eos_bad else ("UNPROVEN" if eos_missing else "PASS")
1322
+ sub.append((v, f"exact length: {eos_n - len(eos_bad) - len(eos_missing)}/{eos_n} outputs exactly max_tokens "
1323
+ f"long, {len(eos_bad)} wrong, {len(eos_missing)} missing or failed"))
1324
+ details += eos_bad[:50] + eos_missing[:50]
1325
+ metrics.update(exact_length_outputs=eos_n, exact_length_wrong=len(eos_bad), exact_length_missing=len(eos_missing))
1326
+ if reference is not None:
1327
+ ref = outputs_of(reference)
1328
+ ratios, outl, missing, ref_n = [], [], [], 0
1329
+ for k, r_o in ref.items():
1330
+ m, _ = _out_len(r_o) if not r_o.get("_unreadable") else (None, None)
1331
+ if m is None:
1332
+ continue # the reference itself has no length here: nothing to compare against
1333
+ ref_n += 1
1334
+ o = cand.get(k)
1335
+ n, _ = _out_len(o) if o and not o.get("_unreadable") else (None, None)
1336
+ if n is None:
1337
+ # A prompt the candidate did not answer is a failure, not a skipped row.
1338
+ missing.append({"id": k, "reference_length": m, "problem": "no readable candidate output" if not o or
1339
+ o.get("_unreadable") else f"candidate request failed: {str(o.get('error'))[:120]}"})
1340
+ continue
1341
+ ratio = n / m if m else (1.0 if n == 0 else None)
1342
+ ratios.append(ratio if ratio is not None else 0.0)
1343
+ if ratio is None or abs(ratio - 1) > tol:
1344
+ outl.append({"id": k, "length": n, "reference_length": m,
1345
+ "ratio": round(ratio, 4) if ratio is not None else None})
1346
+ coverage = (ref_n - len(missing)) / ref_n if ref_n else None
1347
+ metrics.update(reference_outputs=ref_n, reference_pairs=len(ratios), outside_tolerance=len(outl),
1348
+ missing_outputs=len(missing), coverage=coverage, length_ratio_median=_median(ratios))
1349
+ if ref_n:
1350
+ off = len(outl) + len(missing) # missing outputs count as outliers
1351
+ frac = off / ref_n
1352
+ total_ratio = sum(ratios) / ref_n # missing outputs count as length 0
1353
+ bad = frac > max_outliers or abs(total_ratio - 1) > tol
1354
+ v = "FAIL" if bad else ("UNPROVEN" if missing else "PASS")
1355
+ sub.append((v, f"{len(outl)}/{ref_n} prompts outside +/-{tol:.0%} of the reference length and "
1356
+ f"{len(missing)} without a candidate output (together {frac:.0%}, allowed "
1357
+ f"{max_outliers:.0%}); coverage {coverage:.0%}; mean ratio {total_ratio:.3f}"))
1358
+ details += outl[:50] + missing[:50]
1359
+ else:
1360
+ sub.append(("NOT RUN", "the reference outputs carry no lengths"))
1361
+ if not sub:
1362
+ return _gate("output_length", "NOT RUN", "no exact-length requests and no reference outputs given", metrics)
1363
+ return _gate("output_length", combine_claims(v for v, _ in sub), "; ".join(f"{v}: {w}" for v, w in sub),
1364
+ metrics, details)
1365
+
1366
+
1367
+ def _load_requests(run_dir: Path) -> list[dict]:
1368
+ """One row per load request: {"source", "engine", "level", "key", "salt", "prompt"}.
1369
+
1370
+ bench serve layout first (inputs/<engine>/load-<level>/<bucket>/<id>[~k].json request bodies,
1371
+ else outputs/<engine>/load-<level>/...), then any *schedule*.jsonl / *requests*.jsonl."""
1372
+ rows = []
1373
+ for kind in ("inputs", "outputs"):
1374
+ base = run_dir / kind
1375
+ found = False
1376
+ for d in sorted(base.glob("*/load-*")) if base.is_dir() else []:
1377
+ if not d.is_dir():
1378
+ continue
1379
+ found = True
1380
+ engine, level = d.parent.name, d.name[len("load-"):]
1381
+ for p in sorted(d.glob("*/*.json")):
1382
+ o = _read_json(p)
1383
+ if not isinstance(o, dict):
1384
+ continue
1385
+ pid = o.get("id") or p.stem.split("~")[0]
1386
+ if kind == "inputs" and (o.get("messages") is not None or o.get("prompt") is not None):
1387
+ key = hashlib.sha256(json.dumps(o.get("messages", o.get("prompt")), ensure_ascii=False,
1388
+ sort_keys=True).encode("utf-8")).hexdigest()
1389
+ else:
1390
+ key = f"{pid}|{o.get('salt') or ''}"
1391
+ salt = o.get("salt") if kind == "outputs" else _salt_of(o)
1392
+ rows.append({"source": str(p.relative_to(run_dir)), "engine": engine, "level": level, "key": key,
1393
+ "salt": salt, "prompt": pid})
1394
+ if found:
1395
+ return rows
1396
+ for f in sorted(p for p in run_dir.rglob("*.jsonl") if "schedule" in p.name or "requests" in p.name):
1397
+ for line in f.read_text(encoding="utf-8").splitlines():
1398
+ try:
1399
+ r = json.loads(line) if line.strip() else None
1400
+ except ValueError:
1401
+ continue
1402
+ if not isinstance(r, dict):
1403
+ continue
1404
+ level = r.get("level", r.get("concurrency", r.get("rate_rps")))
1405
+ if level is None:
1406
+ continue
1407
+ if r.get("prompt_sha256"):
1408
+ key = r["prompt_sha256"]
1409
+ elif r.get("messages") is not None or r.get("prompt") is not None:
1410
+ key = hashlib.sha256(json.dumps(r.get("messages", r.get("prompt")), ensure_ascii=False,
1411
+ sort_keys=True).encode("utf-8")).hexdigest()
1412
+ elif r.get("prompt_id", r.get("id")) is not None:
1413
+ key = f"{r.get('prompt_id', r.get('id'))}|{r.get('salt') or ''}"
1414
+ else:
1415
+ continue
1416
+ rows.append({"source": str(f.relative_to(run_dir)), "engine": r.get("engine") or str(
1417
+ f.relative_to(run_dir).parent), "level": level, "key": key, "salt": r.get("salt"),
1418
+ "prompt": r.get("prompt_id", r.get("id", key))})
1419
+ return rows
1420
+
1421
+
1422
+ def _salt_of(body: dict) -> str | None:
1423
+ """The bench serve reuse tag ("[ik-...] ") at the start of the first user message, if any."""
1424
+ for m in body.get("messages") or []:
1425
+ c = m.get("content") if isinstance(m, dict) else None
1426
+ if isinstance(c, str):
1427
+ if c.startswith("[ik-") and "] " in c[:40]:
1428
+ return c[1:c.index("] ")]
1429
+ return None
1430
+ return None
1431
+
1432
+
1433
+ def has_load_data(run_dir: Path) -> bool:
1434
+ return bool(_load_requests(run_dir))
1435
+
1436
+
1437
+ def gate_unique_prompts(run_dir: Path) -> dict:
1438
+ reqs = _load_requests(run_dir)
1439
+ if not reqs:
1440
+ return _gate("unique_prompts", "NOT RUN", "no load requests in the run dir (bench serve load-<level>/ "
1441
+ "inputs or outputs, or a *schedule*.jsonl / *requests*.jsonl)")
1442
+ seen: dict[tuple, dict] = {}
1443
+ repeats, salted = [], 0
1444
+ for r in reqs:
1445
+ scope = (r["engine"], r["level"])
1446
+ counts = seen.setdefault(scope, {})
1447
+ counts[r["key"]] = counts.get(r["key"], 0) + 1
1448
+ salted += bool(r["salt"])
1449
+ if counts[r["key"]] == 2:
1450
+ repeats.append({"source": r["source"], "engine": r["engine"], "level": r["level"], "prompt": r["prompt"]})
1451
+ metrics = {"requests": len(reqs), "levels": len(seen), "repeated_prompts": len(repeats), "salted_reuses": salted}
1452
+ lim = []
1453
+ if salted:
1454
+ lim.append(f"{salted} load request(s) reuse a prompt behind a unique tag (the prompt pool ran out): exact and "
1455
+ "prefix caches cannot serve them, a near-duplicate cache could")
1456
+ if repeats:
1457
+ return _gate("unique_prompts", "FAIL", f"{len(repeats)} prompt(s) repeat inside a load level (a cache would "
1458
+ "serve them)", metrics, repeats[:50], lim)
1459
+ return _gate("unique_prompts", "PASS", f"{len(reqs)} load requests over {len(seen)} level(s): no prompt repeats "
1460
+ "inside a level", metrics, [], lim)
1461
+
1462
+
1463
+ def static_probe_jobs(run_dir: Path, engine: str) -> dict:
1464
+ """Hidden-probe replies a load runner saved under outputs/<engine>/probes/<id>.json (see probe_bodies)."""
1465
+ d = run_dir / "outputs" / engine / "probes"
1466
+ jobs: dict[str, list] = {}
1467
+ for p in sorted(d.glob("*.json")) if d.is_dir() else []:
1468
+ o = _read_json(p)
1469
+ if not isinstance(o, dict) or not o.get("kind"):
1470
+ continue
1471
+ lp = o.get("logprobs")
1472
+ lp = lp.get("content") if isinstance(lp, dict) else lp
1473
+ rep = Reply(status=200 if not o.get("error") else int(o.get("status") or 500), error=o.get("error"),
1474
+ text=o.get("text") or "", reasoning=o.get("reasoning") or "", token_ids=o.get("token_ids"),
1475
+ lp=lp if isinstance(lp, list) and lp else None, finish_reason=o.get("finish_reason"),
1476
+ usage=o.get("usage"))
1477
+ kind = o["kind"]
1478
+ job = Job(kind, o.get("group") or p.stem, {}, {}, rep)
1479
+ jobs.setdefault("logprobs" if kind == "logprobs" else "sampling", []).append(job)
1480
+ return jobs
1481
+
1482
+
1483
+ # --------------------------------------------------------------------------- record
1484
+
1485
+ def _metrics_of(gates: list[dict]) -> list[dict]:
1486
+ out = []
1487
+ for g in gates:
1488
+ for k, v in g["metrics"].items():
1489
+ if isinstance(v, bool) or not isinstance(v, (int, float)):
1490
+ continue
1491
+ unit = "x" if "speedup" in k or "ratio" in k else ("tokens" if "divergence" in k or "early" in k
1492
+ else "fraction" if "coverage" in k else "count")
1493
+ stat = "median" if "median" in k else "count"
1494
+ out.append(ev.metric(f"{g['gate']}.{k}", v, unit, stat))
1495
+ return out
1496
+
1497
+
1498
+ def overall(gates: list[dict]) -> str:
1499
+ """The record's verdict. A gate the inputs asked for (or allowed) that ended NOT RUN counts as
1500
+ UNPROVEN: missing evidence is never a pass."""
1501
+ return combine_claims("UNPROVEN" if g["verdict"] == "NOT RUN" and g.get("applicable") else g["verdict"]
1502
+ for g in gates)
1503
+
1504
+
1505
+ def _record(call, gates: list[dict], *, live: bool, engine: str | None, model: str | None, prompt_set: dict | None,
1506
+ log: list, extra: dict) -> dict:
1507
+ verdict = overall(gates)
1508
+ store = ev.Store()
1509
+ with store.staging_dir() as staging:
1510
+ artifacts = []
1511
+ if log:
1512
+ p = staging / "probes.jsonl"
1513
+ p.write_text("".join(json.dumps(x, ensure_ascii=False) + "\n" for x in log), encoding="utf-8")
1514
+ artifacts.append(ev.artifact(p, staging, "probe requests and replies"))
1515
+ lim = [f"{g['gate']}: {x}" for g in gates for x in g["limitations"]]
1516
+ missing = [g["gate"] for g in gates if g["verdict"] == "NOT RUN"]
1517
+ summary = f"gates {verdict}: " + ", ".join(f"{g['gate']} {g['verdict']}" for g in gates)
1518
+ rec = ev.build(call, kind=KIND, claim_class="observed" if live else "derived", summary=summary,
1519
+ identity_=ev.identity(engine={"name": engine}, model={"id": model}),
1520
+ metrics=_metrics_of(gates), artifacts=artifacts, missing=missing, limitations=lim,
1521
+ data={"verdict": verdict, "gates": {g["gate"]: g for g in gates}, "prompt_set": prompt_set,
1522
+ "not_run_but_expected": [g["gate"] for g in gates
1523
+ if g["verdict"] == "NOT RUN" and g.get("applicable")],
1524
+ "gates_format": "inferencekit.gates.v2", **extra})
1525
+ return store.save(rec, staging)
1526
+
1527
+
1528
+ def _load_prompts(name_or_path: str) -> list[dict]:
1529
+ try:
1530
+ items = ps.load(name_or_path)
1531
+ except (OSError, ValueError) as e:
1532
+ raise OpError(f"cannot read prompt set {name_or_path!r}: {e}") from None
1533
+ if not items:
1534
+ raise OpError(f"prompt set {name_or_path!r} is empty")
1535
+ return items
1536
+
1537
+
1538
+ def _extra(text: str | None) -> dict:
1539
+ if not text:
1540
+ return {}
1541
+ try:
1542
+ val = json.loads(text)
1543
+ except ValueError:
1544
+ raise OpError("extra_body must be a JSON object") from None
1545
+ if not isinstance(val, dict):
1546
+ raise OpError("extra_body must be a JSON object")
1547
+ return val
1548
+
1549
+
1550
+ def _template_kwargs(extra: dict, flag: str | None, items: list[dict]) -> dict:
1551
+ """chat_template_kwargs for every probe, merged per key: --extra-body's, --chat-template-kwargs, then the
1552
+ prompt records' own field when every record carries the same one (probes are built from the set's text,
1553
+ so a per-record value that differs between records cannot follow a probe)."""
1554
+ try:
1555
+ own = json.loads(flag) if flag else {}
1556
+ except ValueError:
1557
+ raise OpError("chat_template_kwargs must be a JSON object") from None
1558
+ if not isinstance(own, dict):
1559
+ raise OpError("chat_template_kwargs must be a JSON object")
1560
+ recs = [it.get("chat_template_kwargs") for it in items if isinstance(it, dict)]
1561
+ shared = recs[0] if recs and isinstance(recs[0], dict) and all(r == recs[0] for r in recs) else None
1562
+ if shared is None and any(isinstance(r, dict) for r in recs):
1563
+ raise OpError("prompt records carry different chat_template_kwargs; gates probes need one setting for "
1564
+ "the whole set (pass --chat-template-kwargs)")
1565
+ out: dict = {}
1566
+ for layer in (extra.get("chat_template_kwargs"), own, shared):
1567
+ if isinstance(layer, dict):
1568
+ out.update(layer)
1569
+ return out
1570
+
1571
+
1572
+ def _api_key(env_name: str | None) -> str | None:
1573
+ if not env_name:
1574
+ return None
1575
+ v = os.environ.get(env_name)
1576
+ if not v:
1577
+ raise OpError(f"environment variable {env_name} is empty")
1578
+ return v
1579
+
1580
+
1581
+ @operation("run_gates", ("gates", "run"), "Anti-cheat gates: probe a live OpenAI-compatible endpoint (caching, "
1582
+ "token counts, exact lengths, sampling/logprob probes, rare-token pack) and check a bench serve run dir "
1583
+ "(unique prompts per load level, lengths vs the reference). Each gate: PASS/FAIL/UNPROVEN/NOT RUN; a "
1584
+ "refused probe is evidence, and an expected gate that did not run makes the verdict UNPROVEN.",
1585
+ [Arg("base_url", "string", "Endpoint base URL, e.g. http://127.0.0.1:8000 (live gates need it)."),
1586
+ Arg("model", "string", "Model name to send (default: the first id from /v1/models)."),
1587
+ Arg("engine", "string", "Engine label: names outputs/<engine>/ in the run dir and the record."),
1588
+ Arg("run_dir", "string", "bench serve run directory for the static checks."),
1589
+ Arg("reference_engine", "string", "Reference engine label under <run_dir>/outputs/ for length checks."),
1590
+ Arg("reference_outputs", "string", "Or: a reference outputs directory (<bucket>/<id>.json)."),
1591
+ Arg("reference_url", "string", "Reference endpoint for the rare-token pack (misses it shares are "
1592
+ "not counted)."),
1593
+ Arg("reference_api_key_env", "string", "Environment variable with the reference endpoint's API key "
1594
+ "(default: none)."),
1595
+ Arg("spec_off_url", "string", "Same model without speculative decoding: runs the spec_exact gate with "
1596
+ "base_url as the speculative engine."),
1597
+ Arg("prompts", "string", "Prompt set for the probes: 'dev' or a .jsonl path (use the sealed set for an "
1598
+ "evaluation).", default="dev"),
1599
+ Arg("gates", "array", "Gates to run (default: every gate the inputs allow).", choices=GATES),
1600
+ Arg("tokenizer", "string", "Hugging Face tokenizer id for re-tokenized counts (needs transformers)."),
1601
+ Arg("max_tokens", "integer", "Generation length of the probes.", default=64),
1602
+ Arg("cache_prompts", "integer", "Prompts replayed by the caching gate.", default=6),
1603
+ Arg("sampling_prompts", "integer", "hidden_probes: prompts sampled at T=1 (each sent greedy, twice with "
1604
+ "one seed, once with another). FAIL needs the samples to equal greedy on every prompt and at "
1605
+ "least 4 prompts; >= 3/4 equal is UNPROVEN. Default 8, or fewer (not below 4) when the prompt "
1606
+ "set is too small, recorded as a limitation."),
1607
+ Arg("threshold", "number", "Caching gate: largest allowed repeat speedup - 1.", default=0.10),
1608
+ Arg("length_tolerance", "number", "Per-prompt length tolerance vs the reference.", default=0.10),
1609
+ Arg("max_length_outliers", "number", "Share of prompts allowed outside the length tolerance (missing "
1610
+ "outputs count).", default=0.10),
1611
+ Arg("spec_limit", "integer", "spec_exact: prompts used (0 = all).", default=16),
1612
+ Arg("early_tokens", "integer", "spec_exact: a divergence before this token is 'early'.", default=32),
1613
+ Arg("spec_tolerance", "integer", "spec_exact: early divergences allowed above the A/A floor.", default=0),
1614
+ Arg("seed", "integer", "Probe seed (default: drawn at random for every run and recorded; pass it only to "
1615
+ "replay a run)."),
1616
+ Arg("echo_tokens", "integer", "max_tokens of the echo / rare-string probes (room for a reasoning "
1617
+ "model's preamble).", default=64),
1618
+ Arg("extra_body", "string", "JSON object merged into every request, e.g. "
1619
+ "'{\"chat_template_kwargs\": {\"enable_thinking\": false}}'."),
1620
+ Arg("api_key_env", "string", "Name of the environment variable holding the API key (value never stored)."),
1621
+ Arg("timeout", "number", "Per-request timeout in seconds.", default=120.0),
1622
+ Arg("chat_template_kwargs", "string", "JSON object sent as chat_template_kwargs on every request, e.g. "
1623
+ "'{\"enable_thinking\": false}' (merged over --extra-body's; a chat_template_kwargs field shared by "
1624
+ "every record of --prompts is merged over this, as bench serve does per request).")])
1625
+ def run_gates(call, base_url, model, engine, run_dir, reference_engine, reference_outputs, reference_url,
1626
+ reference_api_key_env, spec_off_url, prompts, gates, tokenizer, max_tokens, cache_prompts, threshold,
1627
+ length_tolerance, max_length_outliers, spec_limit, early_tokens, spec_tolerance, seed, echo_tokens,
1628
+ extra_body, api_key_env, timeout, chat_template_kwargs=None, sampling_prompts=None):
1629
+ wanted = set(gates or GATES)
1630
+ if not base_url and not run_dir:
1631
+ raise OpError("give base_url (live gates) and/or run_dir (static checks)")
1632
+ if sampling_prompts is not None and sampling_prompts < 1:
1633
+ raise OpError(f"sampling_prompts must be at least 1 (FAIL needs at least {SAMPLING_MIN_FAIL}; the default "
1634
+ f"is {SAMPLING_DEFAULT})")
1635
+ if "spec_exact" in wanted and not spec_off_url and gates:
1636
+ raise OpError("spec_exact needs spec_off_url (the same model without speculation)")
1637
+ key = _api_key(api_key_env)
1638
+ ref_key = _api_key(reference_api_key_env)
1639
+ extra = _extra(extra_body)
1640
+ count_tokens = load_tokenizer(tokenizer)
1641
+ seed = secrets.randbits(63) if seed is None else int(seed)
1642
+ results, log = [], []
1643
+ items = _load_prompts(prompts)
1644
+ ctk = apply_pin(_template_kwargs(extra, chat_template_kwargs, items), run_dir, "gates run")
1645
+ if ctk:
1646
+ extra["chat_template_kwargs"] = ctk
1647
+ rd = Path(run_dir) if run_dir else None
1648
+ if rd is not None and not rd.is_dir():
1649
+ raise OpError(f"run dir {rd} not found")
1650
+ # Gates the inputs ask for: named in --gates, or (by default) every gate the inputs make possible.
1651
+ if gates:
1652
+ expected = set(gates)
1653
+ else:
1654
+ expected = set()
1655
+ if base_url:
1656
+ expected |= LIVE - {"spec_exact"}
1657
+ if spec_off_url:
1658
+ expected.add("spec_exact")
1659
+ if rd is not None and engine:
1660
+ expected |= {"token_count", "output_length"}
1661
+ if rd is not None and has_load_data(rd):
1662
+ expected.add("unique_prompts")
1663
+ jobs: dict = {}
1664
+ client = None
1665
+ up, why_down = True, ""
1666
+ sampling_note = None
1667
+ if base_url:
1668
+ client = Client(base_url, model, key, timeout, extra=extra)
1669
+ ref_client = (Client(reference_url, None, ref_key, timeout, extra=extra)
1670
+ if reference_url and "rare_tokens" in wanted else None)
1671
+ n_sampling, sampling_note = sampling_count(len(probe_pool(items)), wanted & LIVE, cache_prompts,
1672
+ sampling_prompts)
1673
+ jobs = plan_and_run(client, items, gates=wanted & LIVE, max_tokens=max_tokens, seed=seed,
1674
+ n_cache=cache_prompts, log=log, reference=ref_client, echo_tokens=echo_tokens,
1675
+ n_sampling=n_sampling)
1676
+ up, why_down = alive(jobs)
1677
+ down = _gate("", "NOT RUN", f"the endpoint answered no probe: {why_down}")
1678
+ static_rows = static_counts(rd, engine) if rd is not None and engine else []
1679
+ static_probes = static_probe_jobs(rd, engine) if rd is not None and engine else {}
1680
+
1681
+ def live(name, fn):
1682
+ if not client:
1683
+ return _gate(name, "NOT RUN", "no base_url")
1684
+ return fn() if up else {**down, "gate": name}
1685
+
1686
+ for g in GATES:
1687
+ if g not in wanted:
1688
+ continue
1689
+ if g == "caching":
1690
+ res = live(g, lambda: gate_caching(jobs, threshold))
1691
+ elif g == "token_count":
1692
+ if (client and up) or static_rows:
1693
+ res = gate_token_count(jobs if up else {}, count_tokens, 0.02, static_rows)
1694
+ elif client:
1695
+ res = {**down, "gate": g}
1696
+ else:
1697
+ res = _gate(g, "NOT RUN", "no base_url and no run-dir outputs (pass --engine)")
1698
+ elif g == "output_length":
1699
+ parts = []
1700
+ if client:
1701
+ parts.append(live(g, lambda: gate_output_length_live(jobs, count_tokens)))
1702
+ if rd is not None and engine:
1703
+ ref = Path(reference_outputs) if reference_outputs else (
1704
+ rd / "outputs" / reference_engine if reference_engine else None)
1705
+ parts.append(gate_output_length_static(rd, engine, ref, length_tolerance, max_length_outliers))
1706
+ res = _merge(g, parts) if parts else _gate(g, "NOT RUN", "no base_url and no run dir + engine")
1707
+ elif g == "unique_prompts":
1708
+ res = gate_unique_prompts(rd) if rd is not None else _gate(g, "NOT RUN", "no run_dir")
1709
+ elif g == "spec_exact":
1710
+ if client and spec_off_url:
1711
+ off = Client(spec_off_url, None, key, timeout, extra=extra)
1712
+ sel = items[:spec_limit] if spec_limit else items
1713
+ res = spec_exact(client, off, sel, max_tokens, early_tokens, spec_tolerance, log)
1714
+ else:
1715
+ res = _gate(g, "NOT RUN", "needs base_url and spec_off_url (or use gates spec-exact)")
1716
+ elif g == "hidden_probes":
1717
+ parts = []
1718
+ if client:
1719
+ part = live(g, lambda: gate_hidden_probes(jobs))
1720
+ if sampling_note:
1721
+ part["limitations"] = [*part.get("limitations", []), sampling_note]
1722
+ parts.append(part)
1723
+ if static_probes:
1724
+ parts.append(gate_hidden_probes(static_probes, where="run dir"))
1725
+ res = _merge(g, parts) if parts else _gate(g, "NOT RUN", "no base_url and no probe replies in the run dir")
1726
+ else: # rare_tokens
1727
+ res = live(g, lambda: gate_rare(jobs))
1728
+ res["applicable"] = g in expected
1729
+ results.append(res)
1730
+ return _record(call, results, live=client is not None, engine=engine, model=client.model if client else model,
1731
+ prompt_set=ps.identity(prompts), log=log,
1732
+ extra={"endpoint_kind": "openai-chat" if client else None, "max_tokens": max_tokens,
1733
+ "seed": seed, "expected_gates": sorted(expected)})
1734
+
1735
+
1736
+ def _merge(name: str, parts: list[dict]) -> dict:
1737
+ """One gate from its live and run-dir halves."""
1738
+ if len(parts) == 1:
1739
+ return parts[0]
1740
+ tags = ("live", "run dir")
1741
+ return _gate(name, combine_claims(p["verdict"] for p in parts),
1742
+ " | ".join(f"{t}: {p['verdict']}: {p['reason']}" for t, p in zip(tags, parts)),
1743
+ {f"{t.replace(' ', '_')}_{k}": v for t, p in zip(tags, parts) for k, v in p["metrics"].items()},
1744
+ [d for p in parts for d in p["details"]], [x for p in parts for x in p["limitations"]])
1745
+
1746
+
1747
+ @operation("gates_spec_exact", ("gates", "spec-exact"), "Speculative-decoding exactness: greedy outputs of engine A "
1748
+ "(speculation on) vs B (off) on the prompt set; FAIL if early divergences exceed B's own A/A floor.",
1749
+ [Arg("a", "string", "Base URL of the engine with speculation / MTP.", required=True),
1750
+ Arg("b", "string", "Base URL of the same model without speculation.", required=True),
1751
+ Arg("model", "string", "Model name for A (default: first id from A's /v1/models)."),
1752
+ Arg("model_b", "string", "Model name for B (default: first id from B's /v1/models)."),
1753
+ Arg("prompts", "string", "Prompt set: 'dev' or a .jsonl path.", default="dev"),
1754
+ Arg("limit", "integer", "Use the first N prompts (0 = all).", default=0),
1755
+ Arg("max_tokens", "integer", "Greedy generation length.", default=128),
1756
+ Arg("early_tokens", "integer", "A divergence before this token counts as early.", default=32),
1757
+ Arg("tolerance", "integer", "Early divergences allowed above the A/A floor.", default=0),
1758
+ Arg("extra_body", "string", "JSON object merged into every request."),
1759
+ Arg("api_key_env", "string", "Name of the environment variable holding the API key."),
1760
+ Arg("timeout", "number", "Per-request timeout in seconds.", default=120.0)])
1761
+ def gates_spec_exact(call, a, b, model, model_b, prompts, limit, max_tokens, early_tokens, tolerance, extra_body,
1762
+ api_key_env, timeout):
1763
+ key = _api_key(api_key_env)
1764
+ extra = _extra(extra_body)
1765
+ ca, cb = Client(a, model, key, timeout, extra=extra), Client(b, model_b, key, timeout, extra=extra)
1766
+ items = _load_prompts(prompts)
1767
+ items = items[:limit] if limit else items
1768
+ log: list = []
1769
+ res = spec_exact(ca, cb, items, max_tokens, early_tokens, tolerance, log)
1770
+ res["applicable"] = True
1771
+ return _record(call, [res], live=True, engine=None, model=ca.model, prompt_set=ps.identity(prompts), log=log,
1772
+ extra={"endpoint_kind": "openai-chat", "max_tokens": max_tokens})