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,262 @@
1
+ """The first-token distribution a server's own log-probabilities imply under the request's sampling settings.
2
+
3
+ ``quality sampling`` asks a server for its top-20 log-probabilities of the first generated token (raw
4
+ log-softmax, before temperature, top-k and top-p, as vLLM returns them and the server contract requires),
5
+ builds the distribution its sampler should draw from, and tests the server's seeded draws against it.
6
+
7
+ Order of the cuts (Hugging Face and vLLM): temperature, then top-k, then top-p on the top-k set
8
+ renormalised, with min-p checked both before and after top-p (vLLM and Hugging Face differ on where
9
+ min-p goes; a prompt counts only when both orders keep the same tokens). SGLang ("joint") takes top-p
10
+ over the whole distribution and the first top_k tokens by position, then min-p; its kept set can only
11
+ be larger. A prompt counts only when that order keeps the same tokens too, so a correct sampler in
12
+ either order passes.
13
+
14
+ Only 20 tokens are listed, so the distribution is known exactly only when the kept set provably lies
15
+ inside them. ``implied`` proves that or says why not:
16
+
17
+ * top_k < K (the number listed): the top-k set is the first top_k listed tokens; a near-tie across
18
+ that boundary is ambiguous.
19
+ * top_k >= K, or off: unlisted tokens may sit in the top-k set. Each has a log-probability <= the
20
+ K-th's and together they hold at most R of the raw mass, R = 1 - the listed mass with every
21
+ listed log-probability lowered by ``tol``. Hugging Face and vLLM keep every token tied with the
22
+ k-th value, so there are top_k - K of them plus any number of ties: at most top_k - K + R / p_K
23
+ (p_K the K-th's probability), and no count bound above temperature 1 with top_k > K (a tie at a
24
+ low k-th value can add any number of small tokens) or with no top-k. ``unseen_bound`` turns that
25
+ into the most tempered weight they can add; the top-p cut must be the same for every unlisted
26
+ weight from 0 to that bound. A bound below ``UNSEEN_EPS`` of the kept weight is ignored.
27
+ * Every top-p and min-p comparison must hold with logits moved by ``tol`` (logprobs are rounded in
28
+ JSON, and two identical requests can differ slightly); a cut between two tokens whose
29
+ log-probabilities are within ``tol`` is ambiguous (the engine may keep either).
30
+
31
+ Exclusion reasons (``implied(...)["status"]``): ``beyond_top`` (the kept set may reach past the
32
+ listed tokens), ``boundary`` (a cut within the tolerance), ``min_p_order`` (the two min-p orders
33
+ keep different tokens), ``top_p_order`` (top-p over the top-k set and over the whole distribution
34
+ keep different tokens), ``bad_logprobs`` (none, or not log-probabilities).
35
+
36
+ ``expected_tv`` is the mean of TV(empirical, p) when n draws come from p exactly, the parametric
37
+ bootstrap's mean in closed form: TV is half the sum over tokens of |count/n - p|, each count is
38
+ Binomial(n, p_t), and E|X - np| has De Moivre's closed form, so no Monte Carlo noise enters.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import math
44
+ from typing import Hashable, Mapping, Sequence
45
+
46
+ LOGPROB_TOL = 1e-3 # nats: smallest logit tolerance for the cut comparisons
47
+ UNSEEN_EPS = 1e-4 # unlisted weight below this share of the kept weight is treated as absent
48
+ GREEDY_T = 1e-5 # vLLM samples greedily below this temperature
49
+ _R_EPS = 1e-9 # unlisted raw mass below this: the listed tokens are the whole vocabulary
50
+
51
+
52
+ def sampling_settings(extra: Mapping) -> tuple[dict, list[str]]:
53
+ """{temperature, top_k, top_p, min_p} from a request body (None = off), and the fields not sent (those
54
+ vLLM fills from generation_config.json when absent).
55
+
56
+ temperature is required (a server's default is not knowable from outside); top_k <= 0 or -1, top_p >= 1
57
+ and min_p <= 0 are off. Raises ValueError on a missing temperature or a value of the wrong type.
58
+ """
59
+ def num(key, cast):
60
+ v = extra.get(key)
61
+ if v is None:
62
+ return None
63
+ if isinstance(v, bool) or not isinstance(v, (int, float)):
64
+ raise ValueError(f"{key} must be a number, got {v!r}")
65
+ return cast(v)
66
+
67
+ t = num("temperature", float)
68
+ if t is None:
69
+ raise ValueError("temperature is not set: the check needs the sampling settings the draws use")
70
+ if t < 0 or not math.isfinite(t):
71
+ raise ValueError(f"temperature must be >= 0, got {t}")
72
+ k = num("top_k", float)
73
+ if k is not None and k != int(k):
74
+ raise ValueError(f"top_k must be an integer, got {k}")
75
+ p, mp = num("top_p", float), num("min_p", float)
76
+ if p is not None and not 0 < p <= 1:
77
+ raise ValueError(f"top_p must be in (0, 1], got {p}")
78
+ if mp is not None and not 0 <= mp <= 1:
79
+ raise ValueError(f"min_p must be in [0, 1], got {mp}")
80
+ out = {"temperature": t, "top_k": int(k) if k is not None and k > 0 else None,
81
+ "top_p": p if p is not None and p < 1 else None, "min_p": mp if mp else None}
82
+ return out, [f for f in ("top_k", "top_p", "min_p", "repetition_penalty") if f not in extra]
83
+
84
+
85
+ def unseen_bound(rest: float, p_last: float, p_top: float, temperature: float, m: float) -> float:
86
+ """Largest total tempered weight (relative to the top token's) of at most ``m`` unlisted tokens, each with
87
+ raw probability <= ``p_last`` and together <= ``rest``. inf when it has no bound (T > 1 with no top-k)."""
88
+ if rest <= 0 or m <= 0 or p_last <= 0:
89
+ return 0.0
90
+ a = 1.0 / temperature
91
+ if a >= 1: # convex: as many at the cap as the mass allows
92
+ return (p_last / p_top) ** a * min(m, rest / p_last)
93
+ if math.isinf(m): # concave: spreading the mass over more tokens raises the sum without limit
94
+ return math.inf
95
+ return m * (min(rest / m, p_last) / p_top) ** a
96
+
97
+
98
+ def _cut_range(w: Sequence[float], n_set: int, u_max: float, top_p: float | None, t: float) -> tuple:
99
+ """Top-p over the first ``n_set`` weights plus 0..``u_max`` of unlisted weight sorted after them.
100
+
101
+ Returns (lo, hi): the fewest and the most listed tokens the nucleus can keep when every logit moves by up
102
+ to ``t`` (in tempered units) and the unlisted weight is anywhere in its range; hi is None when the
103
+ nucleus may not close inside the listed tokens. A token j is kept when the mass before it is < top_p.
104
+ """
105
+ if top_p is None:
106
+ return (n_set, n_set) if u_max == 0 else (n_set, None)
107
+ total = sum(w[:n_set])
108
+ e = math.exp(2 * t)
109
+ lo = hi = None
110
+ acc = 0.0
111
+ for j in range(n_set):
112
+ acc += w[j]
113
+ rest = total - acc
114
+ # the nucleus closes at token j+1 (1-based) at the latest / earliest moves of the inputs
115
+ if lo is None and acc / (acc + rest / e) >= top_p:
116
+ lo = j + 1
117
+ if hi is None and acc / (acc + (rest + u_max) * e) >= top_p:
118
+ hi = j + 1
119
+ break
120
+ if lo is None: # rounding: the listed set always closes when nothing unlisted is in it
121
+ lo = n_set
122
+ return lo, hi
123
+
124
+
125
+ def implied(top: Sequence[tuple[Hashable, float]], temperature: float, top_k: int | None = None,
126
+ top_p: float | None = None, min_p: float | None = None, tol: float = LOGPROB_TOL) -> dict:
127
+ """The kept tokens and their probabilities, from the listed (key, raw logprob) pairs, or why not provable.
128
+
129
+ ``top`` is the server's top-K list for one position (any order). Returns {"status": "ok", "probs":
130
+ {key: p}, "kept": n, "listed": K, "max_p", "unseen_bound"} or {"status": <reason>, "detail": text}.
131
+ """
132
+ items = [(k, float(v)) for k, v in top if isinstance(v, (int, float)) and not isinstance(v, bool)]
133
+ if not items or len(items) != len(top) or any(math.isnan(v) or v > 1e-6 for _, v in items):
134
+ return {"status": "bad_logprobs", "detail": "missing, non-numeric or positive log-probabilities"}
135
+ items.sort(key=lambda kv: -kv[1]) # stable: exact ties keep the server's order
136
+ keys, lp = [k for k, _ in items], [v for _, v in items]
137
+ big_k = len(lp)
138
+ tol = max(tol, 0.0)
139
+ listed = sum(math.exp(v) for v in lp)
140
+ # the unlisted raw mass, at most: the listed log-probabilities may each be up to tol too high
141
+ rest = max(0.0, 1.0 - listed * math.exp(-tol)) if 1.0 - listed > _R_EPS else 0.0
142
+
143
+ def done(n: int, u: float, u_raw: float) -> dict:
144
+ # a cut between two near-tied tokens is ambiguous (the engine may keep either); past the last listed
145
+ # token the next one is unlisted, and a tie with it cannot be ruled out while unlisted weight remains
146
+ if n < big_k and lp[n - 1] - lp[n] < tol:
147
+ return {"status": "boundary", "detail": f"the cut after token {n} is a near-tie with token {n + 1}"}
148
+ if n >= big_k and u > 0:
149
+ return {"status": "beyond_top", "detail": "every listed token is kept and unlisted ones may tie"}
150
+ z = sum(w[:n])
151
+ probs: dict = {}
152
+ for key, x in zip(keys[:n], w[:n]):
153
+ probs[key] = probs.get(key, 0.0) + x / z
154
+ return {"status": "ok", "probs": probs, "kept": n, "listed": big_k, "max_p": max(probs.values()),
155
+ "unseen_bound": u_raw / (z + u_raw) if u_raw else 0.0}
156
+
157
+ if temperature < GREEDY_T: # greedy: the argmax only
158
+ w = [1.0]
159
+ clear = lp[0] - lp[1] >= tol if big_k > 1 else rest < math.exp(lp[0] - tol)
160
+ if not clear:
161
+ return {"status": "boundary", "detail": "greedy: the top two tokens are within the tolerance"}
162
+ return done(1, 0.0, 0.0)
163
+ t = tol / temperature
164
+ w = [math.exp((v - lp[0]) / temperature) for v in lp]
165
+ p_last, p_top = math.exp(lp[-1]), math.exp(lp[0])
166
+ if top_k is not None and top_k < big_k:
167
+ if lp[top_k - 1] - lp[top_k] < tol:
168
+ return {"status": "boundary", "detail": f"top_k {top_k}: tokens {top_k} and {top_k + 1} are a near-tie"}
169
+ n_k, m = top_k, 0.0
170
+ elif top_k is None or (top_k > big_k and temperature > 1):
171
+ n_k, m = big_k, math.inf
172
+ else: # top_k - K unlisted tokens, plus any number tied with the k-th value (each <= p_K, together <= rest)
173
+ n_k, m = big_k, (top_k - big_k) + rest / p_last
174
+ u_raw = unseen_bound(rest, p_last, p_top, temperature, m) if rest > 0 else 0.0
175
+ u_max = 0.0 if u_raw <= UNSEEN_EPS * sum(w[:n_k]) else u_raw
176
+ m_c, unseen_pass = n_k, u_max > 0
177
+ if min_p is not None:
178
+ thr = math.log(min_p)
179
+ lw = [(v - lp[0]) / temperature for v in lp[:n_k]]
180
+ if any(abs(x - thr) < 2 * t for x in lw):
181
+ return {"status": "boundary", "detail": f"min_p {min_p}: a token within the tolerance of the threshold"}
182
+ m_c = sum(1 for x in lw if x >= thr)
183
+ unseen_pass = u_max > 0 and m_c == n_k # an unlisted token sorts after every listed one
184
+
185
+ def orders(tt: float) -> list[tuple[int, int | None]]:
186
+ """(fewest, most) listed tokens kept, most None when unlisted ones may be kept too; one entry per
187
+ min-p order: top-p then min-p (Hugging Face), and min-p then top-p (vLLM)."""
188
+ lo, hi = _cut_range(w, n_k, u_max, top_p, tt)
189
+ if hi is None:
190
+ a = (min(lo, m_c), None if unseen_pass else m_c)
191
+ else:
192
+ a = (min(lo, m_c), min(hi, m_c))
193
+ if min_p is None:
194
+ return [a]
195
+ lo, hi = _cut_range(w, min(n_k, m_c), u_max if unseen_pass else 0.0, top_p, tt)
196
+ return [a, (lo, hi)]
197
+
198
+ got = orders(t)
199
+ if any(hi is None for _, hi in got):
200
+ return {"status": "beyond_top", "detail": "the kept set may include unlisted tokens"}
201
+ if any(lo != hi for lo, hi in got):
202
+ moved = any(hi is None or lo != hi for lo, hi in orders(0.0)) # the unlisted weight alone moves it
203
+ return {"status": "beyond_top" if moved else "boundary",
204
+ "detail": "the nucleus depends on the unlisted weight" if moved
205
+ else f"top_p {top_p}: a cumulative probability within the tolerance"}
206
+ if len({lo for lo, _ in got}) > 1:
207
+ return {"status": "min_p_order", "detail": f"min_p {min_p} before and after top_p keep "
208
+ f"{got[1][0]} and {got[0][0]} tokens"}
209
+ n = got[0][0]
210
+ if top_k is not None and top_p is not None: # with no top-k the two orders are the same
211
+ joint = _joint_most(w, lp, rest, top_k, top_p, min_p, temperature, t)
212
+ if joint != n:
213
+ return {"status": "top_p_order", "detail": f"top_p {top_p} over the top-k set keeps {n} tokens, over "
214
+ f"the whole distribution (SGLang) up to {'unlisted ones too' if joint is None else joint}"}
215
+ return done(n, u_max, u_raw)
216
+
217
+
218
+ def _joint_most(w: Sequence[float], lp: Sequence[float], rest: float, top_k: int, top_p: float,
219
+ min_p: float | None, temperature: float, t: float) -> int | None:
220
+ """The most listed tokens SGLang's joint order can keep (None: unlisted ones too): top-p over the whole tempered
221
+ distribution (every unlisted token's weight counts, at its largest), the first top_k by position, then min-p."""
222
+ big_k = len(w)
223
+ u_all = unseen_bound(rest, math.exp(lp[-1]), math.exp(lp[0]), temperature, math.inf) if rest > 0 else 0.0
224
+ _, hi = _cut_range(w, big_k, u_all, top_p, t)
225
+ n = (top_k if top_k <= big_k else None) if hi is None else min(hi, top_k)
226
+ if min_p is not None:
227
+ thr = math.log(min_p)
228
+ n_min = sum(1 for v in lp if (v - lp[0]) / temperature >= thr)
229
+ if n_min < big_k: # every unlisted token is below the K-th, so below the threshold too
230
+ n = n_min if n is None else min(n, n_min)
231
+ return n
232
+
233
+
234
+ def binomial_mad(n: int, p: float) -> float:
235
+ """E|X - n p| for X ~ Binomial(n, p): 2 m C(n, m) p^m (1-p)^(n-m+1) with m = floor(n p) + 1 (De Moivre)."""
236
+ if n <= 0 or p <= 0.0 or p >= 1.0:
237
+ return 0.0
238
+ m = math.floor(n * p) + 1
239
+ if m > n:
240
+ return 0.0
241
+ log = (math.log(2 * m) + math.lgamma(n + 1) - math.lgamma(m + 1) - math.lgamma(n - m + 1)
242
+ + m * math.log(p) + (n - m + 1) * math.log1p(-p))
243
+ return math.exp(log)
244
+
245
+
246
+ def expected_tv(probs: Mapping[Hashable, float], n: int) -> float:
247
+ """Mean TV between the histogram of n draws from ``probs`` and ``probs`` itself (exact, see the module doc)."""
248
+ return 0.5 * sum(binomial_mad(n, p) for p in probs.values()) / n if n > 0 else 0.0
249
+
250
+
251
+ def tv_to(hist: Mapping[Hashable, int], probs: Mapping[Hashable, float]) -> float | None:
252
+ """TV between a histogram of draws and a distribution; draws outside ``probs`` count in full."""
253
+ n = sum(hist.values())
254
+ if n <= 0:
255
+ return None
256
+ keys = set(hist) | set(probs)
257
+ return 0.5 * sum(abs(hist.get(k, 0) / n - probs.get(k, 0.0)) for k in keys)
258
+
259
+
260
+ def tv_between(a: Mapping[Hashable, float], b: Mapping[Hashable, float]) -> float:
261
+ """TV between two distributions over the same kind of keys."""
262
+ return 0.5 * sum(abs(a.get(k, 0.0) - b.get(k, 0.0)) for k in set(a) | set(b))
@@ -0,0 +1,217 @@
1
+ """The replay worker of ``quality clip-steps``: runs inside the reference pipeline's own Python, on its GPU.
2
+
3
+ Usage: python step_replay.py CONFIG.json (inferencekit importable: the harness puts a path holding only the
4
+ inferencekit package first on PYTHONPATH). Prints one line ``INFERKIT_RESULT {...}`` on stdout; results go to the
5
+ config's ``out``; logs to stderr. Needs torch, safetensors, numpy and PyAV (the reference's venv has them).
6
+
7
+ Config: {"pipeline": {"name", "model", "task", "device", "dtype", "offload", "attention", "extra"},
8
+ "alt_attention": backend name, "out": path,
9
+ "requests": [{"key", "body": the request, "clip": the delivered clip's path, "dump": its dump's path}]}
10
+
11
+ The reference pipeline is loaded once (the clip server's factory: ``--pipeline`` names), then for each request:
12
+ 1. the dump's layout against the request (stepdump.layout_problems) and its recorded request;
13
+ 2. continuity inside the dump: each step's x_next equals the next step's x, the final latent the last x_next;
14
+ 3. replay with the reference's own backend (pipeline.replay: every step starts from the dump's x_t): per step
15
+ and stream, e = |out_eng - out_ref| / |out_ref| (L2), cosine, the chain error |x_next_eng - update(x_eng,
16
+ out_eng)| / |x_next_eng| with the reference's scheduler, the reference's sigmas and dtypes; the reference's
17
+ own initial latent against the dump's first x; the frames the reference decodes from the dump's final
18
+ latent. A dump with another step count than the reference's schedule stops at step 0;
19
+ 4. the same replay with ``alt_attention`` (an exact but different kernel): the floor f = |out_alt - out_ref| /
20
+ |out_ref| on the same inputs;
21
+ 5. the decode check: the delivered clip's decoded frames (PyAV) against the reference's decode, PSNR per frame.
22
+ Judging is the harness's job (ops/quality_clip_steps.py); this file only measures.
23
+ """
24
+
25
+ import json
26
+ import math
27
+ import os
28
+ import sys
29
+ import time
30
+ import traceback
31
+
32
+ _HERE = os.path.dirname(os.path.abspath(__file__))
33
+ sys.path[:] = [p for p in sys.path if os.path.abspath(p or os.curdir) != _HERE]
34
+
35
+ MARK = "INFERKIT_RESULT "
36
+ PSNR_CAP_DB = 100.0
37
+
38
+
39
+ class _Stop(Exception):
40
+ pass
41
+
42
+
43
+ def log(msg):
44
+ print(f"step_replay: {msg}", file=sys.stderr, flush=True)
45
+
46
+
47
+ def _rel(a, b):
48
+ """|a - b| / |b| (L2, float64); 0 when both are zero."""
49
+ a, b = a.double(), b.double()
50
+ nb = float(b.norm())
51
+ d = float((a - b).norm())
52
+ return d / nb if nb else (0.0 if d == 0 else math.inf)
53
+
54
+
55
+ def _cos(a, b):
56
+ a, b = a.double().flatten(), b.double().flatten()
57
+ n = float(a.norm() * b.norm())
58
+ return float((a * b).sum()) / n if n else None
59
+
60
+
61
+ def continuity(dump):
62
+ """[(what, rel diff)] for x_next[i] vs x[i+1] and final vs the last x_next, per stream (0.0 = equal)."""
63
+ out = []
64
+ n = dump.n_steps
65
+ for s in dump.streams:
66
+ for i in range(n):
67
+ a = dump.get(f"step.{i}.{s}.x_next")
68
+ nxt = f"step.{i + 1}.{s}.x" if i + 1 < n else f"final.{s}"
69
+ if not dump.has(nxt) or not dump.has(f"step.{i}.{s}.x_next"):
70
+ continue
71
+ b = dump.get(nxt)
72
+ out.append((f"{s} step {i} x_next vs {nxt}", 0.0 if a.shape == b.shape and bool((a == b).all())
73
+ else (_rel(a, b) if a.shape == b.shape else math.inf)))
74
+ return out
75
+
76
+
77
+ def decode_clip(path):
78
+ import av
79
+
80
+ c = av.open(path)
81
+ try:
82
+ s = c.streams.video[0]
83
+ s.thread_type = "AUTO"
84
+ return [f.to_ndarray(format="rgb24") for f in c.decode(s)]
85
+ finally:
86
+ c.close()
87
+
88
+
89
+ def decode_check(frames_ref, clip_path):
90
+ import numpy as np
91
+
92
+ ref = np.clip(np.round(np.asarray(frames_ref, dtype=np.float64) * 255.0), 0, 255).astype(np.uint8)
93
+ got = decode_clip(clip_path)
94
+ out = {"frames_ref": int(ref.shape[0]), "frames_clip": len(got), "size_ref": [int(ref.shape[2]), int(ref.shape[1])],
95
+ "size_clip": [int(got[0].shape[1]), int(got[0].shape[0])] if got else None}
96
+ if not got or tuple(got[0].shape) != tuple(ref.shape[1:]):
97
+ out["psnr_db"] = None
98
+ return out
99
+ vals = []
100
+ for a, b in zip(ref, got):
101
+ mse = float(np.mean((a.astype(np.float64) - b.astype(np.float64)) ** 2))
102
+ vals.append(PSNR_CAP_DB if mse == 0 else min(PSNR_CAP_DB, 10 * math.log10(255.0 ** 2 / mse)))
103
+ out["psnr_db"] = sum(vals) / len(vals)
104
+ out["psnr_db_min_frame"] = min(vals)
105
+ return out
106
+
107
+
108
+ def one_request(pipe, req, alt):
109
+ from inferencekit.engines.clip import stepdump
110
+ from inferencekit.engines.clip.server import parse_job
111
+
112
+ body = dict(req["body"])
113
+ if body.get("task") == "i2v" or body.get("image") is not None:
114
+ return {"key": req["key"], "error": "i2v replay is not supported (the request's image bytes are not kept)"}
115
+ job = parse_job({k: v for k, v in body.items() if k in stepdump.REQUEST_FIELDS})
116
+ res = {"key": req["key"], "error": None}
117
+ if not os.path.isfile(req["dump"]):
118
+ res["error"] = "no dump for this clip (the engine wrote none under its sha256)"
119
+ return res
120
+ dump = stepdump.DumpReader(req["dump"])
121
+ shapes = {k: list(v) for k, v in pipe.noise_shapes(job).items()}
122
+ res["layout"] = stepdump.layout_problems(dump, shapes, job.steps)
123
+ want = stepdump.request_of(body)
124
+ got = stepdump.request_of(dump.request) if isinstance(dump.request, dict) else {}
125
+ res["request_diff"] = [k for k in stepdump.COMPARED_FIELDS if got.get(k) != want.get(k)]
126
+ res["n_steps"], res["streams"] = dump.n_steps, dump.streams
127
+ res["dump_dtypes"] = {s: {f: dump.dtype(f"step.0.{s}.{f}").lower() for f in stepdump.FIELDS
128
+ if dump.has(f"step.0.{s}.{f}")} for s in dump.streams}
129
+ res["dump_bytes"] = os.path.getsize(req["dump"])
130
+ if res["layout"] and any(("missing" in p or "shape" in p or "streams" in p) for p in res["layout"]):
131
+ return res # cannot be replayed step for step
132
+ res["continuity"] = [[w, v] for w, v in continuity(dump)]
133
+ sig = dump.get("sigma").tolist() if dump.has("sigma") else []
134
+ sig_n = dump.get("sigma_next").tolist() if dump.has("sigma_next") else []
135
+ sched = {}
136
+
137
+ def on_schedule(sigmas):
138
+ sched["ref"] = sigmas
139
+ if len(sigmas) - 1 != dump.n_steps:
140
+ raise _Stop()
141
+
142
+ ref_out, rows = {}, {}
143
+
144
+ def on_step(i, stream, sigma, sigma_next, out, chain):
145
+ eng = dump.get(f"step.{i}.{stream}.out").to(out.device)
146
+ x_next = dump.get(f"step.{i}.{stream}.x_next").to(out.device)
147
+ rows[(i, stream)] = {"i": i, "stream": stream, "sigma_ref": sigma, "sigma_next_ref": sigma_next,
148
+ "e": _rel(eng, out), "cos": _cos(eng, out), "chain": _rel(chain, x_next),
149
+ "norm_ref": float(out.double().norm())}
150
+ ref_out[(i, stream)] = out.detach().float().cpu()
151
+
152
+ t0 = time.perf_counter()
153
+ try:
154
+ rep = pipe.replay(job, dump, on_step, on_schedule=on_schedule)
155
+ except _Stop:
156
+ res["schedule"] = {"ref_sigmas": sched.get("ref"), "sigma": sig, "sigma_next": sig_n, "stopped": True}
157
+ res["seconds"] = {"replay": round(time.perf_counter() - t0, 3)}
158
+ return res
159
+ t1 = time.perf_counter()
160
+ res["schedule"] = {"ref_sigmas": sched.get("ref"), "sigma": sig, "sigma_next": sig_n, "stopped": False}
161
+ res["ref_dtypes"] = rep["dtypes"]
162
+ res["initial"] = {s: _rel(dump.get(f"step.0.{s}.x").float(), t) for s, t in rep["initial"].items()
163
+ if dump.has(f"step.0.{s}.x")}
164
+ res["decode"] = decode_check(rep["frames"], req["clip"])
165
+ t2 = time.perf_counter()
166
+ floors = {}
167
+
168
+ def on_alt(i, stream, sigma, sigma_next, out, chain):
169
+ ref = ref_out.get((i, stream))
170
+ if ref is not None:
171
+ floors[(i, stream)] = _rel(out.float(), ref.to(out.device))
172
+
173
+ pipe.replay(job, dump, on_alt, attention=alt, decode=False)
174
+ t3 = time.perf_counter()
175
+ for k, row in rows.items():
176
+ row["floor"] = floors.get(k)
177
+ res["steps"] = [rows[k] for k in sorted(rows)]
178
+ res["seconds"] = {"replay": round(t1 - t0, 3), "decode_check": round(t2 - t1, 3), "alt_replay": round(t3 - t2, 3)}
179
+ return res
180
+
181
+
182
+ def main(argv):
183
+ cfg = json.loads(open(argv[1]).read())
184
+ t0 = time.time()
185
+ try:
186
+ from inferencekit.engines.clip.server import PipelineOptions, load_factory
187
+
188
+ pc = cfg["pipeline"]
189
+ opts = PipelineOptions(model=pc["model"], task=pc.get("task") or "t2v", device=pc.get("device") or "cuda",
190
+ dtype=pc.get("dtype") or "bf16", offload=pc.get("offload") or "none",
191
+ attention=pc.get("attention"), extra=dict(pc.get("extra") or {}))
192
+ pipe = load_factory(pc["name"])(opts)
193
+ load_s = time.time() - t0
194
+ log(f"pipeline loaded in {load_s:.1f} s")
195
+ items = []
196
+ for req in cfg["requests"]:
197
+ t = time.time()
198
+ try:
199
+ items.append(one_request(pipe, req, cfg["alt_attention"]))
200
+ except Exception as e: # noqa: BLE001 - one request's failure is its result
201
+ traceback.print_exc(file=sys.stderr)
202
+ items.append({"key": req["key"], "error": f"{type(e).__name__}: {e}"[:500]})
203
+ log(f"{req['key']}: {time.time() - t:.1f} s")
204
+ doc = {"items": items, "describe": pipe.describe(), "load_s": round(load_s, 3),
205
+ "total_s": round(time.time() - t0, 3)}
206
+ with open(cfg["out"], "w") as f:
207
+ json.dump(doc, f)
208
+ except Exception as e: # noqa: BLE001
209
+ traceback.print_exc(file=sys.stderr)
210
+ print(MARK + json.dumps({"ok": False, "error": f"{type(e).__name__}: {e}"[:500]}), flush=True)
211
+ return 1
212
+ print(MARK + json.dumps({"ok": True, "n": len(items), "seconds": round(time.time() - t0, 3)}), flush=True)
213
+ return 0
214
+
215
+
216
+ if __name__ == "__main__":
217
+ raise SystemExit(main(sys.argv))