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,330 @@
1
+ """Client-timed PCM speech over HTTP NDJSON, using only the standard library.
2
+
3
+ POST /v1/audio/speech accepts AudioRequest.body(). The response is UTF-8 NDJSON:
4
+ metadata {type, sample_rate, channels, encoding: "pcm_s16le"}, then one or more
5
+ audio {type, pcm_b64}, then final {type, sample_count, effective}. sample_count
6
+ counts PCM frames PER CHANNEL, not interleaved int16 values. Metadata must match
7
+ the request. effective must report model, seed and every requested setting.
8
+
9
+ First audio means decoded PCM bytes, including silence; signal onset is reported
10
+ separately. Neither signal detection nor these structural checks proves speech
11
+ intelligibility. The complete timer ends only after consuming the entire HTTP
12
+ body, including final and EOF. Engine-provided durations are never trusted.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import binascii
19
+ import http.client
20
+ import ipaddress
21
+ import json
22
+ import math
23
+ import queue
24
+ import socket
25
+ import struct
26
+ import threading
27
+ import time
28
+ from dataclasses import dataclass, field
29
+
30
+ from inferencekit import evidence as ev
31
+ from inferencekit.canonical import canonical_json
32
+ from inferencekit.serve.client import Endpoint, scrub
33
+
34
+ AUDIO_PATH = "/v1/audio/speech"
35
+ MAX_LINE_BYTES = 8 * 1024 * 1024
36
+ MAX_PCM_BYTES = 256 * 1024 * 1024
37
+ ENCODING = "pcm_s16le"
38
+
39
+
40
+ def _connect_bounded(address, timeout, source_address=None):
41
+ """Bound DNS plus TCP connect; an abandoned resolver cannot open a socket."""
42
+ host, port = address
43
+ deadline = time.monotonic() + timeout
44
+ try:
45
+ literal = ipaddress.ip_address(host)
46
+ except ValueError:
47
+ literal = None
48
+ if literal is not None:
49
+ family = socket.AF_INET6 if literal.version == 6 else socket.AF_INET
50
+ addresses = [(family, socket.SOCK_STREAM, 0, "", (host, port))]
51
+ else:
52
+ resolved = queue.Queue(maxsize=1)
53
+
54
+ def resolve():
55
+ try:
56
+ resolved.put((socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM), None))
57
+ except OSError as exc:
58
+ resolved.put((None, exc))
59
+
60
+ threading.Thread(target=resolve, daemon=True).start()
61
+ try:
62
+ addresses, error = resolved.get(timeout=max(0.001, deadline - time.monotonic()))
63
+ except queue.Empty:
64
+ raise TimeoutError("audio DNS deadline exceeded") from None
65
+ if error:
66
+ raise error
67
+ error = OSError("audio endpoint resolved no addresses")
68
+ for family, kind, protocol, _, destination in addresses:
69
+ remaining = deadline - time.monotonic()
70
+ if remaining <= 0:
71
+ raise TimeoutError("audio connection deadline exceeded")
72
+ sock = socket.socket(family, kind, protocol)
73
+ try:
74
+ sock.settimeout(remaining)
75
+ if source_address:
76
+ sock.bind(source_address)
77
+ sock.connect(destination)
78
+ return sock
79
+ except OSError as exc:
80
+ sock.close()
81
+ error = exc
82
+ raise error
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class AudioRequest:
87
+ id: str
88
+ text: str
89
+ model: str
90
+ seed: int = 0
91
+ sample_rate: int = 44100
92
+ channels: int = 1
93
+ settings: dict = field(default_factory=dict)
94
+ reference_audio: bytes | None = field(default=None, repr=False)
95
+ reference_text: str | None = None
96
+
97
+ def body(self) -> dict:
98
+ body = {"input": self.text, "model": self.model, "seed": self.seed,
99
+ "sample_rate": self.sample_rate, "channels": self.channels,
100
+ "encoding": ENCODING, "stream": True, "settings": self.settings}
101
+ if self.reference_audio is not None:
102
+ body["reference_audio_b64"] = base64.b64encode(self.reference_audio).decode("ascii")
103
+ body["reference_text"] = self.reference_text
104
+ return body
105
+
106
+
107
+ @dataclass
108
+ class AudioResult:
109
+ id: str
110
+ ok: bool = False
111
+ error: str | None = None
112
+ status: int | None = None
113
+ t_send_ns: int = 0
114
+ t_headers_ns: int | None = None
115
+ t_first_ns: int | None = None
116
+ t_signal_ns: int | None = None
117
+ t_audible_ns: int | None = None
118
+ t_done_ns: int | None = None
119
+ t_end_ns: int = 0
120
+ pcm: bytes = field(default=b"", repr=False)
121
+ sample_rate: int | None = None
122
+ channels: int | None = None
123
+ sample_count: int = 0
124
+ leading_silent_frames: int = 0
125
+ effective: dict | None = None
126
+ chunks: list[tuple[int, int]] = field(default_factory=list) # arrival ns, PCM frames
127
+
128
+ def milliseconds(self, timestamp: int | None) -> float | None:
129
+ return (timestamp - self.t_send_ns) / 1e6 if self.ok and timestamp is not None else None
130
+
131
+ @property
132
+ def first_audio_ms(self):
133
+ return self.milliseconds(self.t_first_ns)
134
+
135
+ @property
136
+ def first_signal_ms(self):
137
+ return self.milliseconds(self.t_signal_ns)
138
+
139
+ @property
140
+ def first_audible_ms(self):
141
+ return self.milliseconds(self.t_audible_ns)
142
+
143
+ @property
144
+ def complete_audio_ms(self):
145
+ return self.milliseconds(self.t_done_ns)
146
+
147
+ @property
148
+ def audio_duration_s(self):
149
+ return self.sample_count / self.sample_rate if self.ok and self.sample_rate else None
150
+
151
+
152
+ def playback_metrics(chunks: list[tuple[int, int]], sample_rate: int) -> dict:
153
+ """Simulate playback starting immediately at the first chunk; no hidden prebuffer.
154
+
155
+ Stall intervals count once per late arrival. Stream closure after the last
156
+ sample cannot cause an underrun. This is a derived PCM buffer simulation,
157
+ not a claim about an operating system's real audio device.
158
+ """
159
+ if not chunks:
160
+ return {"underruns": None, "underrun_ms": None, "max_stream_gap_ms": None}
161
+ play_end = chunks[0][0] / 1e9
162
+ count, stalled = 0, 0.0
163
+ for t_ns, frames in chunks:
164
+ arrival = t_ns / 1e9
165
+ gap = arrival - play_end
166
+ if gap > 1e-9:
167
+ count += 1
168
+ stalled += gap
169
+ play_end = max(play_end, arrival) + frames / sample_rate
170
+ gaps = [(b[0] - a[0]) / 1e6 for a, b in zip(chunks, chunks[1:])]
171
+ return {"underruns": count, "underrun_ms": stalled * 1000,
172
+ "max_stream_gap_ms": max(gaps, default=0.0)}
173
+
174
+
175
+ class AudioHTTP:
176
+ endpoint_kind = "audio_http"
177
+
178
+ def __init__(self, ep: Endpoint, timeout_s: float = 300.0):
179
+ if not math.isfinite(timeout_s) or timeout_s <= 0:
180
+ raise ValueError("timeout_s must be finite and positive")
181
+ self.ep, self.timeout_s = ep, timeout_s
182
+
183
+ def generate(self, request: AudioRequest) -> AudioResult:
184
+ res = AudioResult(request.id)
185
+ if (type(request.sample_rate) is not int or not 8000 <= request.sample_rate <= 192000
186
+ or type(request.channels) is not int or not 1 <= request.channels <= 8):
187
+ res.error = "request PCM rate must be 8000..192000 Hz and channels 1..8"
188
+ return res
189
+ cls = http.client.HTTPSConnection if self.ep.scheme == "https" else http.client.HTTPConnection
190
+ conn = cls(self.ep.host, self.ep.port, timeout=self.timeout_s)
191
+ headers = {"Content-Type": "application/json", "Accept": "application/x-ndjson", "Connection": "close"}
192
+ if self.ep.api_key:
193
+ headers["Authorization"] = "Bearer " + self.ep.api_key
194
+ payload = json.dumps(request.body(), allow_nan=False).encode()
195
+ pcm = bytearray()
196
+ metadata, ended = False, False
197
+ playback_end_ns = 0
198
+ res.t_send_ns = time.perf_counter_ns()
199
+ deadline = time.monotonic() + self.timeout_s
200
+ response = None
201
+ expired = threading.Event()
202
+ conn._create_connection = lambda address, timeout, source_address=None: _connect_bounded(
203
+ address, max(0.001, deadline - time.monotonic()), source_address)
204
+
205
+ def expire():
206
+ expired.set()
207
+ active = conn.sock or getattr(getattr(getattr(response, "fp", None), "raw", None), "_sock", None)
208
+ if active is not None:
209
+ try:
210
+ active.shutdown(socket.SHUT_RDWR)
211
+ except OSError:
212
+ pass
213
+ active.close()
214
+
215
+ # A receive timeout resets on each packet. This watchdog also interrupts
216
+ # a peer dripping header bytes or one never-terminated NDJSON line.
217
+ watchdog = threading.Timer(self.timeout_s, expire)
218
+ watchdog.daemon = True
219
+ watchdog.start()
220
+ try:
221
+ conn.request("POST", self.ep.prefix + AUDIO_PATH, body=payload, headers=headers)
222
+ if conn.sock is not None:
223
+ conn.sock.settimeout(max(0.001, deadline - time.monotonic()))
224
+ response = conn.getresponse()
225
+ res.status = response.status
226
+ res.t_headers_ns = time.perf_counter_ns()
227
+ if response.status != 200:
228
+ # Do not echo bodies: proxies and engines can echo credentials or input.
229
+ raise ValueError(f"HTTP {response.status}")
230
+ ctype = (response.getheader("Content-Type") or "").split(";")[0].strip().lower()
231
+ if ctype not in ("application/x-ndjson", "application/ndjson"):
232
+ raise ValueError("response must be application/x-ndjson")
233
+ while True:
234
+ if expired.is_set() or time.monotonic() >= deadline:
235
+ raise ValueError("response exceeded request deadline")
236
+ active = conn.sock or getattr(getattr(response.fp, "raw", None), "_sock", None)
237
+ if active is not None:
238
+ active.settimeout(max(0.001, deadline - time.monotonic()))
239
+ line = response.readline(MAX_LINE_BYTES + 1)
240
+ if expired.is_set() or time.monotonic() >= deadline:
241
+ raise ValueError("response exceeded request deadline")
242
+ if not line:
243
+ break
244
+ if len(line) > MAX_LINE_BYTES:
245
+ raise ValueError("NDJSON event exceeds size limit")
246
+ if not line.endswith(b"\n"):
247
+ raise ValueError("truncated NDJSON event (missing newline)")
248
+ if not line.strip():
249
+ continue
250
+ if ended:
251
+ raise ValueError("event after final")
252
+ doc = json.loads(line)
253
+ if not isinstance(doc, dict):
254
+ raise ValueError("event must be a JSON object")
255
+ kind = doc.get("type")
256
+ if kind == "metadata":
257
+ if metadata:
258
+ raise ValueError("duplicate metadata")
259
+ for key, expected in (("sample_rate", request.sample_rate), ("channels", request.channels)):
260
+ if type(doc.get(key)) is not int or doc[key] != expected:
261
+ raise ValueError(f"metadata {key} mismatch")
262
+ if doc.get("encoding") != ENCODING:
263
+ raise ValueError("metadata encoding must be pcm_s16le")
264
+ res.sample_rate, res.channels, metadata = request.sample_rate, request.channels, True
265
+ elif kind == "audio":
266
+ if not metadata:
267
+ raise ValueError("audio before metadata")
268
+ value = doc.get("pcm_b64")
269
+ if not isinstance(value, str):
270
+ raise ValueError("audio pcm_b64 must be a string")
271
+ data = base64.b64decode(value, validate=True)
272
+ t_pcm = time.perf_counter_ns() # PCM is now playable; headers never count
273
+ if not data or len(data) % (2 * res.channels):
274
+ raise ValueError("empty or truncated PCM frame")
275
+ if len(pcm) + len(data) > MAX_PCM_BYTES:
276
+ raise ValueError("PCM response exceeds size limit")
277
+ frames = len(data) // (2 * res.channels)
278
+ res.t_first_ns = res.t_first_ns if res.t_first_ns is not None else t_pcm
279
+ if res.t_signal_ns is None:
280
+ first = next((i // res.channels for i, (v,) in enumerate(struct.iter_unpack("<h", data))
281
+ if v != 0), None)
282
+ res.leading_silent_frames += frames if first is None else first
283
+ if first is not None:
284
+ res.t_signal_ns = t_pcm
285
+ res.t_audible_ns = max(t_pcm, playback_end_ns) + round(first / res.sample_rate * 1e9)
286
+ playback_end_ns = max(t_pcm, playback_end_ns) + round(frames / res.sample_rate * 1e9)
287
+ res.chunks.append((t_pcm, frames))
288
+ pcm.extend(data)
289
+ elif kind == "final":
290
+ if not metadata:
291
+ raise ValueError("final before metadata")
292
+ if type(doc.get("sample_count")) is not int or doc["sample_count"] <= 0:
293
+ raise ValueError("final sample_count must be a positive integer")
294
+ res.sample_count = doc["sample_count"]
295
+ if res.sample_count * 2 * res.channels != len(pcm):
296
+ raise ValueError("final sample_count does not match PCM")
297
+ expected = {"model": request.model, "seed": request.seed, **request.settings}
298
+ reported = doc.get("effective")
299
+ # Retain only requested fields. Unsolicited server metadata may
300
+ # contain internal endpoints, credentials or launch commands.
301
+ res.effective = ev.redact({key: reported[key] for key in expected if key in reported}) \
302
+ if isinstance(reported, dict) else None
303
+ if not isinstance(res.effective, dict) or canonical_json(res.effective) != canonical_json(expected):
304
+ raise ValueError("final effective settings missing or mismatched")
305
+ ended = True
306
+ else:
307
+ raise ValueError("unknown NDJSON event type")
308
+ if response.length not in (None, 0):
309
+ raise ValueError("truncated HTTP body")
310
+ res.t_done_ns = time.perf_counter_ns() # complete HTTP body, not merely final announcement
311
+ if not ended:
312
+ raise ValueError("missing final event")
313
+ if res.t_signal_ns is None:
314
+ raise ValueError("silent PCM output (all samples zero)")
315
+ res.pcm, res.ok = bytes(pcm), True
316
+ except (OSError, http.client.HTTPException, ValueError, binascii.Error) as exc:
317
+ # Parser errors are deliberately static; exception text may contain wire data or keys.
318
+ safe = "response exceeded request deadline" if expired.is_set() else \
319
+ str(exc) if type(exc) is ValueError else type(exc).__name__
320
+ if self.ep.api_key:
321
+ safe = safe.replace(self.ep.api_key, "<redacted>")
322
+ res.error = scrub(safe, self.ep)
323
+ finally:
324
+ watchdog.cancel()
325
+ conn.close()
326
+ res.t_end_ns = time.perf_counter_ns()
327
+ return res
328
+
329
+ def close(self):
330
+ return None