inferencekit-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inferencekit/__init__.py +9 -0
- inferencekit/__main__.py +5 -0
- inferencekit/analysis/__init__.py +1 -0
- inferencekit/analysis/ncu_csv.py +284 -0
- inferencekit/analysis/nsys_sqlite.py +518 -0
- inferencekit/canonical.py +104 -0
- inferencekit/checkpoint.py +396 -0
- inferencekit/cli.py +274 -0
- inferencekit/cuda/bw_peak.cu +252 -0
- inferencekit/cuda/torch_bw.py +60 -0
- inferencekit/engines/__init__.py +7 -0
- inferencekit/engines/adapters.py +190 -0
- inferencekit/engines/clip/__init__.py +13 -0
- inferencekit/engines/clip/comfyui.py +749 -0
- inferencekit/engines/clip/diffusers_ltx.py +601 -0
- inferencekit/engines/clip/noise.py +46 -0
- inferencekit/engines/clip/official_ltx.py +316 -0
- inferencekit/engines/clip/server.py +542 -0
- inferencekit/engines/clip/stepdump.py +182 -0
- inferencekit/engines/gates.py +94 -0
- inferencekit/engines/sglang_worker.py +86 -0
- inferencekit/engines/vllm_worker.py +129 -0
- inferencekit/evidence.py +427 -0
- inferencekit/gpuinfo.py +93 -0
- inferencekit/hardware.py +135 -0
- inferencekit/mcp_server.py +178 -0
- inferencekit/ops/__init__.py +30 -0
- inferencekit/ops/bench.py +184 -0
- inferencekit/ops/bench_audio.py +384 -0
- inferencekit/ops/bench_clip.py +819 -0
- inferencekit/ops/bench_image.py +393 -0
- inferencekit/ops/bench_rounds.py +297 -0
- inferencekit/ops/bench_serve.py +866 -0
- inferencekit/ops/ceiling.py +135 -0
- inferencekit/ops/compare.py +378 -0
- inferencekit/ops/correctness.py +153 -0
- inferencekit/ops/doctor.py +486 -0
- inferencekit/ops/gates.py +1772 -0
- inferencekit/ops/gates_clip.py +930 -0
- inferencekit/ops/intake.py +1321 -0
- inferencekit/ops/ledger.py +749 -0
- inferencekit/ops/manifest.py +309 -0
- inferencekit/ops/ncu.py +183 -0
- inferencekit/ops/nsys.py +224 -0
- inferencekit/ops/prompts.py +316 -0
- inferencekit/ops/quality.py +2161 -0
- inferencekit/ops/quality_clip.py +690 -0
- inferencekit/ops/quality_clip_steps.py +341 -0
- inferencekit/ops/report.py +317 -0
- inferencekit/ops/review.py +119 -0
- inferencekit/ops/rocprof.py +89 -0
- inferencekit/ops/roofline.py +392 -0
- inferencekit/ops/store.py +61 -0
- inferencekit/proc.py +172 -0
- inferencekit/prompts/ATTRIBUTION.md +56 -0
- inferencekit/prompts/__init__.py +167 -0
- inferencekit/prompts/build.py +518 -0
- inferencekit/prompts/dev-load.jsonl.gz +0 -0
- inferencekit/prompts/dev-load.manifest.json +212 -0
- inferencekit/prompts/dev.jsonl.gz +0 -0
- inferencekit/prompts/dev.manifest.json +214 -0
- inferencekit/prompts/exclusions.json +54 -0
- inferencekit/quality/__init__.py +23 -0
- inferencekit/quality/client.py +66 -0
- inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
- inferencekit/quality/fake.py +329 -0
- inferencekit/quality/gsm8k.py +224 -0
- inferencekit/quality/logprobs.py +106 -0
- inferencekit/quality/metrics.py +235 -0
- inferencekit/quality/niah.py +63 -0
- inferencekit/quality/parquet.py +296 -0
- inferencekit/quality/sampling.py +262 -0
- inferencekit/quality/step_replay.py +217 -0
- inferencekit/quality/video_metrics.py +304 -0
- inferencekit/registry.py +206 -0
- inferencekit/review/__init__.py +6 -0
- inferencekit/review/diff.py +163 -0
- inferencekit/review/media.py +181 -0
- inferencekit/review/page.py +920 -0
- inferencekit/review/rundir.py +844 -0
- inferencekit/roofline_headers.py +504 -0
- inferencekit/schemas/evidence.v1.json +130 -0
- inferencekit/schemas/evidence.v2.json +499 -0
- inferencekit/schemas/intake.v1.json +276 -0
- inferencekit/schemas/run-manifest.v1.json +118 -0
- inferencekit/serve/__init__.py +8 -0
- inferencekit/serve/audio_client.py +330 -0
- inferencekit/serve/client.py +446 -0
- inferencekit/serve/clip_client.py +351 -0
- inferencekit/serve/clip_rounds.py +429 -0
- inferencekit/serve/image_client.py +373 -0
- inferencekit/serve/loadgen.py +136 -0
- inferencekit/serve/metrics.py +203 -0
- inferencekit/serve/profiles.py +105 -0
- inferencekit/serve/prompts_io.py +125 -0
- inferencekit/serve/rounds.py +1111 -0
- inferencekit/serve/servers.py +897 -0
- inferencekit/stats.py +249 -0
- inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
- inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
- inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
- inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
|
@@ -0,0 +1,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))
|