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,182 @@
|
|
|
1
|
+
"""Per-step dumps: what a clip engine leaves of every request for ``inferencekit quality clip-steps``.
|
|
2
|
+
|
|
3
|
+
THE CONTRACT (the same for every diffusion clip engine, whatever it is built on):
|
|
4
|
+
|
|
5
|
+
The engine process finds a directory in its environment variable ``CLIP_STEP_DUMP_DIR``. For every clip
|
|
6
|
+
request it serves (timed or not: there is no audit mode, and the requests to check are picked after the
|
|
7
|
+
clips are made), it writes one file before it answers:
|
|
8
|
+
|
|
9
|
+
$CLIP_STEP_DUMP_DIR/<video_sha256>.safetensors
|
|
10
|
+
|
|
11
|
+
<video_sha256> is the lowercase hex sha256 of the mp4 bytes it answers with. The file is in the
|
|
12
|
+
safetensors format and holds, for each latent stream <s> the model denoises ("video", and "audio" for an
|
|
13
|
+
audio-and-video model) and each denoising step i = 0 .. N-1 in the order the steps ran:
|
|
14
|
+
|
|
15
|
+
step.<i>.<s>.x the latent the step started from (x_t)
|
|
16
|
+
step.<i>.<s>.out the guided model output the sampler's update took: every guidance pass of the
|
|
17
|
+
step combined (and rescaled, if the pipeline rescales), in the form the sampler
|
|
18
|
+
takes it. Flow-matching models: the velocity, so that the Euler update is
|
|
19
|
+
x_next = x + (sigma_next - sigma) * out; a sampler that holds the denoised
|
|
20
|
+
estimate x0 writes (x - x0) / sigma
|
|
21
|
+
step.<i>.<s>.x_next the latent the update produced (x_{t-1})
|
|
22
|
+
final.<s> the latent the clip was decoded from
|
|
23
|
+
sigma [N] float64: each step's noise level (flow matching: x = (1 - sigma) x0 + sigma noise)
|
|
24
|
+
sigma_next [N] float64: the noise level each step's update went to
|
|
25
|
+
|
|
26
|
+
Latents have the layout and shape of the initial noise (video [1, C, F, H, W], audio [1, C, L, M]; not a
|
|
27
|
+
packed token sequence) and the dtype the engine holds them in. The file's metadata (safetensors' string
|
|
28
|
+
map) holds "format": "clip-steps.v1" and "request": the request body as JSON (the checker compares its prompt,
|
|
29
|
+
shape, steps, guidance scale, seed and noise seed with the request it sent). Write under a temporary name and
|
|
30
|
+
rename, so no reader sees a partial file; never delete one.
|
|
31
|
+
|
|
32
|
+
Cost on LTX-2.5 (768x512, 121 frames, 30 steps, float32 latents): 292,150,200 bytes per request, written in
|
|
33
|
+
0.26 s (median of 2 clips, the clip server's step_dump stage, RTX PRO 6000) [measured].
|
|
34
|
+
|
|
35
|
+
``StepRecorder`` writes this format from torch tensors in a few lines (any engine can copy the idea);
|
|
36
|
+
``DumpReader`` and ``layout_problems`` are the checker's side. Both need torch and safetensors, imported when
|
|
37
|
+
used: the clip server and the harness import this module without them.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
import os
|
|
44
|
+
import re
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
ENV = "CLIP_STEP_DUMP_DIR"
|
|
48
|
+
FORMAT = "clip-steps.v1"
|
|
49
|
+
SUFFIX = ".safetensors"
|
|
50
|
+
FIELDS = ("x", "out", "x_next")
|
|
51
|
+
REQUEST_FIELDS = ("task", "prompt", "width", "height", "num_frames", "fps", "steps", "guidance_scale", "seed",
|
|
52
|
+
"noise_seed", "audio")
|
|
53
|
+
# What the checker compares between a dump's request and the request it sent (task and audio are not replayed).
|
|
54
|
+
COMPARED_FIELDS = ("prompt", "width", "height", "num_frames", "fps", "steps", "guidance_scale", "seed", "noise_seed")
|
|
55
|
+
STEP_KEY = re.compile(r"^step\.(\d+)\.([a-z0-9_]+)\.(x|out|x_next)$")
|
|
56
|
+
FINAL_KEY = re.compile(r"^final\.([a-z0-9_]+)$")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def request_of(job) -> dict:
|
|
60
|
+
"""The request fields a dump records (a ClipJob or a request body; noise_seed defaults to seed)."""
|
|
61
|
+
get = job.get if isinstance(job, dict) else lambda k, d=None: getattr(job, k, d)
|
|
62
|
+
out = {k: get(k) for k in REQUEST_FIELDS}
|
|
63
|
+
if out.get("noise_seed") is None:
|
|
64
|
+
out["noise_seed"] = out.get("seed")
|
|
65
|
+
if out.get("fps") is not None:
|
|
66
|
+
out["fps"] = float(out["fps"])
|
|
67
|
+
if out.get("guidance_scale") is not None:
|
|
68
|
+
out["guidance_scale"] = float(out["guidance_scale"])
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def dump_path(directory, video_sha256: str) -> Path:
|
|
73
|
+
return Path(directory) / f"{video_sha256.lower()}{SUFFIX}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class StepRecorder:
|
|
77
|
+
"""Collects one request's steps (CPU copies) and writes them in the contract's format."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, request: dict):
|
|
80
|
+
self.request = dict(request)
|
|
81
|
+
self.tensors: dict = {}
|
|
82
|
+
self.sigma: list[float] = []
|
|
83
|
+
self.sigma_next: list[float] = []
|
|
84
|
+
self.count: dict[str, int] = {}
|
|
85
|
+
self.finals: dict = {}
|
|
86
|
+
|
|
87
|
+
def step(self, stream: str, sigma, sigma_next, x, out, x_next) -> None:
|
|
88
|
+
i = self.count.get(stream, 0)
|
|
89
|
+
self.count[stream] = i + 1
|
|
90
|
+
if i == len(self.sigma): # the first stream to reach step i records its noise levels
|
|
91
|
+
self.sigma.append(float(sigma))
|
|
92
|
+
self.sigma_next.append(float(sigma_next))
|
|
93
|
+
for name, t in zip(FIELDS, (x, out, x_next)):
|
|
94
|
+
self.tensors[f"step.{i}.{stream}.{name}"] = t.detach().to("cpu", copy=True).contiguous()
|
|
95
|
+
|
|
96
|
+
def final(self, stream: str, latent) -> None:
|
|
97
|
+
self.finals[stream] = latent.detach().to("cpu", copy=True).contiguous()
|
|
98
|
+
|
|
99
|
+
def save(self, path) -> int:
|
|
100
|
+
"""Write the file (temporary name, then rename); its size in bytes."""
|
|
101
|
+
import torch
|
|
102
|
+
from safetensors.torch import save_file
|
|
103
|
+
|
|
104
|
+
tensors = dict(self.tensors)
|
|
105
|
+
for s, n in self.count.items():
|
|
106
|
+
final = self.finals.get(s)
|
|
107
|
+
tensors[f"final.{s}"] = final if final is not None else tensors[f"step.{n - 1}.{s}.x_next"].clone()
|
|
108
|
+
tensors["sigma"] = torch.tensor(self.sigma, dtype=torch.float64)
|
|
109
|
+
tensors["sigma_next"] = torch.tensor(self.sigma_next, dtype=torch.float64)
|
|
110
|
+
path = Path(path)
|
|
111
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
tmp = path.with_name(path.name + ".partial")
|
|
113
|
+
save_file(tensors, str(tmp), metadata={"format": FORMAT, "request": json.dumps(self.request, sort_keys=True)})
|
|
114
|
+
os.replace(tmp, path)
|
|
115
|
+
return path.stat().st_size
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DumpReader:
|
|
119
|
+
"""One dump file, read lazily (tensors load on ``get``)."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, path):
|
|
122
|
+
from safetensors import safe_open
|
|
123
|
+
|
|
124
|
+
self.path = Path(path)
|
|
125
|
+
self._f = safe_open(str(path), framework="pt", device="cpu")
|
|
126
|
+
self.meta = self._f.metadata() or {}
|
|
127
|
+
self.keys = set(self._f.keys())
|
|
128
|
+
try:
|
|
129
|
+
self.request = json.loads(self.meta.get("request") or "null")
|
|
130
|
+
except ValueError:
|
|
131
|
+
self.request = None
|
|
132
|
+
steps: dict[str, set[int]] = {}
|
|
133
|
+
for k in self.keys:
|
|
134
|
+
m = STEP_KEY.match(k)
|
|
135
|
+
if m:
|
|
136
|
+
steps.setdefault(m.group(2), set()).add(int(m.group(1)))
|
|
137
|
+
self.streams = sorted(steps)
|
|
138
|
+
self.n_steps = max((max(v) + 1 for v in steps.values()), default=0)
|
|
139
|
+
self._steps = steps
|
|
140
|
+
|
|
141
|
+
def get(self, key: str):
|
|
142
|
+
return self._f.get_tensor(key)
|
|
143
|
+
|
|
144
|
+
def has(self, key: str) -> bool:
|
|
145
|
+
return key in self.keys
|
|
146
|
+
|
|
147
|
+
def shape(self, key: str) -> tuple:
|
|
148
|
+
return tuple(self._f.get_slice(key).get_shape())
|
|
149
|
+
|
|
150
|
+
def dtype(self, key: str) -> str:
|
|
151
|
+
"""safetensors' dtype name: F32, BF16, F16, F64..."""
|
|
152
|
+
return str(self._f.get_slice(key).get_dtype())
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def layout_problems(d: DumpReader, shapes: dict, steps: int) -> list[str]:
|
|
156
|
+
"""What is wrong with a dump's layout against the request (streams, step count, keys, shapes, dtypes)."""
|
|
157
|
+
out = []
|
|
158
|
+
if d.meta.get("format") != FORMAT:
|
|
159
|
+
out.append(f"metadata format is {d.meta.get('format')!r}, not {FORMAT!r}")
|
|
160
|
+
if set(d.streams) != set(shapes):
|
|
161
|
+
out.append(f"streams {d.streams} differ from the model's latent streams {sorted(shapes)}")
|
|
162
|
+
if d.n_steps != steps:
|
|
163
|
+
out.append(f"{d.n_steps} steps dumped, {steps} asked")
|
|
164
|
+
allowed = {"sigma", "sigma_next"}
|
|
165
|
+
for s in shapes:
|
|
166
|
+
allowed.add(f"final.{s}")
|
|
167
|
+
allowed.update(f"step.{i}.{s}.{f}" for i in range(steps) for f in FIELDS)
|
|
168
|
+
missing, extra = sorted(allowed - d.keys), sorted(d.keys - allowed)
|
|
169
|
+
if missing:
|
|
170
|
+
out.append(f"{len(missing)} tensor(s) missing (e.g. {missing[0]})")
|
|
171
|
+
if extra:
|
|
172
|
+
out.append(f"{len(extra)} unexpected tensor(s) (e.g. {extra[0]})")
|
|
173
|
+
for name in ("sigma", "sigma_next"):
|
|
174
|
+
if d.has(name) and d.shape(name) != (d.n_steps,):
|
|
175
|
+
out.append(f"{name} has shape {d.shape(name)}, not ({d.n_steps},)")
|
|
176
|
+
for s, shape in shapes.items():
|
|
177
|
+
bad = [k for k in sorted(d.keys) if (k == f"final.{s}" or (k.startswith("step.") and f".{s}." in k))
|
|
178
|
+
and d.shape(k) != tuple(shape)]
|
|
179
|
+
if bad:
|
|
180
|
+
out.append(f"{len(bad)} {s} tensor(s) have another shape than the request's latent {list(shape)} "
|
|
181
|
+
f"(e.g. {bad[0]}: {list(d.shape(bad[0]))})")
|
|
182
|
+
return out
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Hard gates: configurations whose decode numbers must not be presented as an engine's speed.
|
|
2
|
+
|
|
3
|
+
A benchmark that silently ran without CUDA graphs, or on a fallback attention
|
|
4
|
+
kernel, measures a misconfiguration rather than the engine. Batch-1 decode is
|
|
5
|
+
dominated by per-kernel launch overhead without CUDA graphs (each token launches
|
|
6
|
+
hundreds of small kernels; a graph replays them with one launch), and fallback
|
|
7
|
+
attention kernels can be several times slower. Such runs stay recorded, but their
|
|
8
|
+
violations go into ``limitations`` and the claim is marked invalid.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
# Attention implementations that engines use only when the fast path is unavailable.
|
|
16
|
+
FALLBACK_ATTENTION = {"TORCH_SDPA", "XFORMERS", "FLEX_ATTENTION", "TORCH_NATIVE"}
|
|
17
|
+
# Legitimate on some hardware, a fallback on others: recorded as a warning.
|
|
18
|
+
SUSPECT_ATTENTION = {"TRITON_ATTN", "TRITON"}
|
|
19
|
+
|
|
20
|
+
# Keyed by the name with separators removed, so "Flash Attention", "FLASH_ATTN" and
|
|
21
|
+
# "flash-attn" all map to one canonical spelling.
|
|
22
|
+
_ALIASES = {"FLASHATTENTION": "FLASH_ATTN", "FLASHATTN": "FLASH_ATTN", "FLASHINFER": "FLASHINFER",
|
|
23
|
+
"TRITONATTENTION": "TRITON_ATTN", "TRITONATTN": "TRITON_ATTN", "TORCHSDPA": "TORCH_SDPA",
|
|
24
|
+
"FLEXATTENTION": "FLEX_ATTENTION", "XFORMERS": "XFORMERS", "TORCHNATIVE": "TORCH_NATIVE",
|
|
25
|
+
"FA3": "FA3", "FLASHMLA": "FLASHMLA", "CUTLASSMLA": "CUTLASS_MLA"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def normalize_backend(name: str | None) -> str | None:
|
|
29
|
+
if not name:
|
|
30
|
+
return None
|
|
31
|
+
n = name.strip().upper().replace("ATTENTIONBACKENDENUM.", "").replace(" ", "_").replace("-", "_")
|
|
32
|
+
return _ALIASES.get(n.replace("_", ""), n)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_attention(name: str) -> bool:
|
|
36
|
+
# "Using X backend" also appears for MoE, sampling and communication; keep attention only.
|
|
37
|
+
return "ATTN" in name or "ATTENTION" in name or "MLA" in name or name in _ALIASES.values()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def attention_backend_from_log(log_text: str) -> str | None:
|
|
41
|
+
"""vLLM logs e.g. 'Using Flash Attention backend' or 'Using FLASHINFER backend'. Last mention wins."""
|
|
42
|
+
hits = re.findall(r"Using (?:AttentionBackendEnum\.)?([A-Za-z0-9_][A-Za-z0-9_ ]*?) (?:attention )?backend",
|
|
43
|
+
log_text)
|
|
44
|
+
names = [normalize_backend(h) for h in hits]
|
|
45
|
+
names = [n for n in names if n and _is_attention(n)]
|
|
46
|
+
return names[-1] if names else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check(engine: str, flags: dict, attention_backend: str | None) -> dict:
|
|
50
|
+
"""Return {'valid': bool | None, 'violations': [...], 'warnings': [...], 'checked': [...]}.
|
|
51
|
+
|
|
52
|
+
valid is None when the adapter could not see enough to check (e.g. a ``cmd``
|
|
53
|
+
benchmark that reported no flags): unknown is not the same as fine.
|
|
54
|
+
"""
|
|
55
|
+
violations, warnings, checked = [], [], []
|
|
56
|
+
if engine == "vllm":
|
|
57
|
+
checked.append("cuda_graphs")
|
|
58
|
+
if flags.get("enforce_eager") is True:
|
|
59
|
+
violations.append("CUDA graphs off (enforce_eager=True)")
|
|
60
|
+
mode = str(flags.get("cudagraph_mode") or "")
|
|
61
|
+
if mode.upper().endswith("NONE"):
|
|
62
|
+
violations.append(f"CUDA graphs off (cudagraph_mode={mode})")
|
|
63
|
+
sizes = flags.get("cudagraph_capture_sizes")
|
|
64
|
+
if isinstance(sizes, dict) and sizes.get("count") == 0 and flags.get("enforce_eager") is not True:
|
|
65
|
+
violations.append("CUDA graphs off (no capture sizes)")
|
|
66
|
+
elif engine == "sglang":
|
|
67
|
+
checked.append("cuda_graphs")
|
|
68
|
+
if flags.get("disable_cuda_graph") is True:
|
|
69
|
+
violations.append("CUDA graphs off (disable_cuda_graph=True)")
|
|
70
|
+
elif engine == "llamacpp":
|
|
71
|
+
checked.append("gpu_offload")
|
|
72
|
+
ngl = flags.get("n_gpu_layers")
|
|
73
|
+
if ngl == 0:
|
|
74
|
+
violations.append("no layers on the GPU (n_gpu_layers=0)")
|
|
75
|
+
elif isinstance(ngl, int) and ngl < 99:
|
|
76
|
+
warnings.append(f"n_gpu_layers={ngl}: check that every layer is offloaded")
|
|
77
|
+
backends = str(flags.get("backends") or "")
|
|
78
|
+
if backends and not re.search(r"CUDA|Vulkan|Metal|HIP|SYCL|MUSA", backends, re.IGNORECASE):
|
|
79
|
+
violations.append(f"no GPU backend in this llama.cpp build (backends={backends})")
|
|
80
|
+
elif engine == "cmd":
|
|
81
|
+
if "cuda_graphs" in flags:
|
|
82
|
+
checked.append("cuda_graphs")
|
|
83
|
+
if flags["cuda_graphs"] is False:
|
|
84
|
+
violations.append("CUDA graphs off (reported by the command)")
|
|
85
|
+
backend = normalize_backend(attention_backend or flags.get("attention_backend"))
|
|
86
|
+
if backend:
|
|
87
|
+
checked.append("attention_backend")
|
|
88
|
+
if backend in FALLBACK_ATTENTION:
|
|
89
|
+
violations.append(f"fallback attention backend ({backend})")
|
|
90
|
+
elif backend in SUSPECT_ATTENTION:
|
|
91
|
+
warnings.append(f"attention backend {backend} is a fallback on many NVIDIA GPUs: confirm it is intended")
|
|
92
|
+
valid = (not violations) if checked else None
|
|
93
|
+
return {"valid": valid, "violations": violations, "warnings": warnings, "checked": checked,
|
|
94
|
+
"attention_backend": backend}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Batch-1 decode timing inside SGLang's own Python (offline Engine). Standard library + sglang only.
|
|
2
|
+
|
|
3
|
+
Usage: python sglang_worker.py CONFIG.json
|
|
4
|
+
Same protocol and timing definitions as vllm_worker.py. SGLang's radix cache reuses
|
|
5
|
+
prompt prefixes, so each call again gets a fresh synthetic prompt.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import random
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
MARK = "INFERKIT_RESULT "
|
|
15
|
+
# Only these server arguments are recorded. Never dump the whole object: it can hold an API key.
|
|
16
|
+
FLAG_FIELDS = ("disable_cuda_graph", "cuda_graph_max_bs", "cuda_graph_bs", "attention_backend",
|
|
17
|
+
"prefill_attention_backend", "decode_attention_backend", "sampling_backend", "quantization", "dtype",
|
|
18
|
+
"kv_cache_dtype", "enable_torch_compile", "tp_size", "disable_radix_cache", "mem_fraction_static",
|
|
19
|
+
"context_length", "speculative_algorithm")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def prompt_ids(seed, label, n, vocab):
|
|
23
|
+
lo = 1000 if vocab > 2000 else 10
|
|
24
|
+
hi = min(vocab, 30000)
|
|
25
|
+
rng = random.Random(f"{seed}:{label}")
|
|
26
|
+
return [lo + int(rng.random() * (hi - lo)) for _ in range(n)]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def plain(x):
|
|
30
|
+
if x is None or isinstance(x, (bool, int, float, str)):
|
|
31
|
+
return x
|
|
32
|
+
if isinstance(x, (list, tuple)):
|
|
33
|
+
return [plain(v) for v in list(x)[:256]]
|
|
34
|
+
return str(x)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
cfg = json.load(open(sys.argv[1]))
|
|
39
|
+
import sglang
|
|
40
|
+
|
|
41
|
+
engine_args = dict(cfg.get("engine_args", {}))
|
|
42
|
+
engine_args.setdefault("random_seed", cfg["seed"])
|
|
43
|
+
engine = sglang.Engine(model_path=cfg["model"], **engine_args)
|
|
44
|
+
try:
|
|
45
|
+
vocab = len(engine.tokenizer_manager.tokenizer)
|
|
46
|
+
except Exception: # noqa: BLE001
|
|
47
|
+
vocab = 32000
|
|
48
|
+
|
|
49
|
+
def generate(ids, max_new):
|
|
50
|
+
params = {"temperature": 0.0, "max_new_tokens": max_new, "ignore_eos": True}
|
|
51
|
+
t0 = time.perf_counter()
|
|
52
|
+
out = engine.generate(input_ids=ids, sampling_params=params)
|
|
53
|
+
dt = time.perf_counter() - t0
|
|
54
|
+
toks = out.get("output_ids") or []
|
|
55
|
+
n_out = (out.get("meta_info") or {}).get("completion_tokens", len(toks))
|
|
56
|
+
return dt, [int(t) for t in toks], int(n_out)
|
|
57
|
+
|
|
58
|
+
n, g = cfg["prompt_len"], cfg["gen_len"]
|
|
59
|
+
_, ref_tokens, _ = generate(prompt_ids(cfg["seed"], "reference", n, vocab), g)
|
|
60
|
+
for w in range(cfg.get("warmup", 1) - 1):
|
|
61
|
+
generate(prompt_ids(cfg["seed"], f"warmup{w}", n, vocab), g)
|
|
62
|
+
rounds = []
|
|
63
|
+
for k in range(cfg["rounds"]):
|
|
64
|
+
i = cfg.get("round_offset", 0) + k
|
|
65
|
+
t_start = time.time()
|
|
66
|
+
ttft, _, _ = generate(prompt_ids(cfg["seed"], f"round{i}:first", n, vocab), 1)
|
|
67
|
+
total, _, n_out = generate(prompt_ids(cfg["seed"], f"round{i}:full", n, vocab), g)
|
|
68
|
+
t_end = time.time()
|
|
69
|
+
decode_s = total - ttft
|
|
70
|
+
ok = n_out > 1 and decode_s > 0
|
|
71
|
+
rounds.append({"index": i, "t_start": t_start, "t_end": t_end, "ttft_ms": ttft * 1e3,
|
|
72
|
+
"total_ms": total * 1e3, "gen_tokens": n_out,
|
|
73
|
+
"decode_tok_s": (n_out - 1) / decode_s if ok else None,
|
|
74
|
+
"tpot_ms": decode_s * 1e3 / (n_out - 1) if ok else None})
|
|
75
|
+
args = getattr(engine, "server_args", None)
|
|
76
|
+
flags = {f: plain(getattr(args, f, None)) for f in FLAG_FIELDS if hasattr(args, f)}
|
|
77
|
+
result = {"engine": {"name": "sglang", "version": getattr(sglang, "__version__", None)}, "flags": flags,
|
|
78
|
+
"rounds": rounds, "tokens": ref_tokens,
|
|
79
|
+
"prompt": {"vocab_bound": min(vocab, 30000), "rule": "ids from random.Random('<seed>:<label>')"}}
|
|
80
|
+
sys.stdout.write(MARK + json.dumps(result) + "\n")
|
|
81
|
+
sys.stdout.flush()
|
|
82
|
+
os._exit(0)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
main()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Batch-1 decode timing inside vLLM's own Python. Standard library + vllm only.
|
|
2
|
+
|
|
3
|
+
Usage: python vllm_worker.py CONFIG.json
|
|
4
|
+
Prints exactly one line ``INFERKIT_RESULT {...}`` on stdout; logs go to stderr.
|
|
5
|
+
|
|
6
|
+
Timing per round (offline engine, no HTTP, no network):
|
|
7
|
+
ttft = wall time of a generation with max_tokens=1 (prefill + first sample)
|
|
8
|
+
total = wall time of a generation with max_tokens=gen_len
|
|
9
|
+
decode tok/s = (tokens - 1) / (total - ttft)
|
|
10
|
+
Every call gets a fresh synthetic prompt: vLLM caches prompt prefixes, and a cached
|
|
11
|
+
prompt would make TTFT look far better than a real request's.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import random
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
MARK = "INFERKIT_RESULT "
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def prompt_ids(seed, label, n, vocab):
|
|
24
|
+
"""Deterministic synthetic token ids, avoiding the low ids where special tokens live."""
|
|
25
|
+
lo = 1000 if vocab > 2000 else 10
|
|
26
|
+
hi = min(vocab, 30000)
|
|
27
|
+
rng = random.Random(f"{seed}:{label}") # str seeds hash with SHA-512: stable everywhere
|
|
28
|
+
return [lo + int(rng.random() * (hi - lo)) for _ in range(n)]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get(obj, path):
|
|
32
|
+
for part in path.split("."):
|
|
33
|
+
if obj is None:
|
|
34
|
+
return None
|
|
35
|
+
obj = getattr(obj, part, None)
|
|
36
|
+
return obj
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def plain(x):
|
|
40
|
+
"""Enums and config objects become strings; containers are kept small."""
|
|
41
|
+
if x is None or isinstance(x, (bool, int, float, str)):
|
|
42
|
+
return x
|
|
43
|
+
if isinstance(x, (list, tuple, set)):
|
|
44
|
+
return [plain(v) for v in list(x)[:256]]
|
|
45
|
+
return str(x)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def effective_flags(llm):
|
|
49
|
+
"""What the engine actually runs with (after its own defaults), from its config objects."""
|
|
50
|
+
eng = llm.llm_engine
|
|
51
|
+
cfg = get(eng, "vllm_config")
|
|
52
|
+
if cfg is None and hasattr(eng, "get_vllm_config"):
|
|
53
|
+
try:
|
|
54
|
+
cfg = eng.get_vllm_config()
|
|
55
|
+
except Exception: # noqa: BLE001
|
|
56
|
+
cfg = None
|
|
57
|
+
sizes = plain(get(cfg, "compilation_config.cudagraph_capture_sizes"))
|
|
58
|
+
flags = {
|
|
59
|
+
"enforce_eager": plain(get(cfg, "model_config.enforce_eager")),
|
|
60
|
+
"dtype": plain(get(cfg, "model_config.dtype")),
|
|
61
|
+
"quantization": plain(get(cfg, "model_config.quantization")),
|
|
62
|
+
"max_model_len": plain(get(cfg, "model_config.max_model_len")),
|
|
63
|
+
"kv_cache_dtype": plain(get(cfg, "cache_config.cache_dtype")),
|
|
64
|
+
"block_size": plain(get(cfg, "cache_config.block_size")),
|
|
65
|
+
"enable_prefix_caching": plain(get(cfg, "cache_config.enable_prefix_caching")),
|
|
66
|
+
"gpu_memory_utilization": plain(get(cfg, "cache_config.gpu_memory_utilization")),
|
|
67
|
+
"tensor_parallel_size": plain(get(cfg, "parallel_config.tensor_parallel_size")),
|
|
68
|
+
"compilation_level": plain(get(cfg, "compilation_config.level")),
|
|
69
|
+
"compilation_mode": plain(get(cfg, "compilation_config.mode")),
|
|
70
|
+
"cudagraph_mode": plain(get(cfg, "compilation_config.cudagraph_mode")),
|
|
71
|
+
"speculative_decoding": get(cfg, "speculative_config") is not None,
|
|
72
|
+
}
|
|
73
|
+
if isinstance(sizes, list):
|
|
74
|
+
flags["cudagraph_capture_sizes"] = {"count": len(sizes), "min": min(sizes) if sizes else None,
|
|
75
|
+
"max": max(sizes) if sizes else None, "includes_1": 1 in sizes}
|
|
76
|
+
return flags
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main():
|
|
80
|
+
cfg = json.load(open(sys.argv[1]))
|
|
81
|
+
import vllm
|
|
82
|
+
from vllm import LLM, SamplingParams
|
|
83
|
+
|
|
84
|
+
engine_args = dict(cfg.get("engine_args", {}))
|
|
85
|
+
engine_args.setdefault("seed", cfg["seed"])
|
|
86
|
+
llm = LLM(model=cfg["model"], **engine_args)
|
|
87
|
+
try:
|
|
88
|
+
vocab = len(llm.get_tokenizer())
|
|
89
|
+
except Exception: # noqa: BLE001 - e.g. skip_tokenizer_init
|
|
90
|
+
vocab = 32000
|
|
91
|
+
|
|
92
|
+
def generate(ids, max_tokens):
|
|
93
|
+
sp = SamplingParams(temperature=0.0, max_tokens=max_tokens, ignore_eos=True)
|
|
94
|
+
t0 = time.perf_counter()
|
|
95
|
+
try:
|
|
96
|
+
outs = llm.generate([{"prompt_token_ids": ids}], sp, use_tqdm=False)
|
|
97
|
+
except TypeError: # older vLLM: token ids as a keyword argument
|
|
98
|
+
outs = llm.generate(prompt_token_ids=[ids], sampling_params=sp, use_tqdm=False)
|
|
99
|
+
dt = time.perf_counter() - t0
|
|
100
|
+
return dt, [int(t) for t in outs[0].outputs[0].token_ids]
|
|
101
|
+
|
|
102
|
+
n, g = cfg["prompt_len"], cfg["gen_len"]
|
|
103
|
+
# Warm-up on the fixed reference prompt; its tokens feed the correctness check.
|
|
104
|
+
_, ref_tokens = generate(prompt_ids(cfg["seed"], "reference", n, vocab), g)
|
|
105
|
+
for w in range(cfg.get("warmup", 1) - 1):
|
|
106
|
+
generate(prompt_ids(cfg["seed"], f"warmup{w}", n, vocab), g)
|
|
107
|
+
rounds = []
|
|
108
|
+
for k in range(cfg["rounds"]):
|
|
109
|
+
i = cfg.get("round_offset", 0) + k
|
|
110
|
+
t_start = time.time()
|
|
111
|
+
ttft, _ = generate(prompt_ids(cfg["seed"], f"round{i}:first", n, vocab), 1)
|
|
112
|
+
total, toks = generate(prompt_ids(cfg["seed"], f"round{i}:full", n, vocab), g)
|
|
113
|
+
t_end = time.time()
|
|
114
|
+
decode_s = total - ttft
|
|
115
|
+
ok = len(toks) > 1 and decode_s > 0
|
|
116
|
+
rounds.append({"index": i, "t_start": t_start, "t_end": t_end, "ttft_ms": ttft * 1e3,
|
|
117
|
+
"total_ms": total * 1e3, "gen_tokens": len(toks),
|
|
118
|
+
"decode_tok_s": (len(toks) - 1) / decode_s if ok else None,
|
|
119
|
+
"tpot_ms": decode_s * 1e3 / (len(toks) - 1) if ok else None})
|
|
120
|
+
result = {"engine": {"name": "vllm", "version": getattr(vllm, "__version__", None)},
|
|
121
|
+
"flags": effective_flags(llm), "rounds": rounds, "tokens": ref_tokens,
|
|
122
|
+
"prompt": {"vocab_bound": min(vocab, 30000), "rule": "ids from random.Random('<seed>:<label>')"}}
|
|
123
|
+
sys.stdout.write(MARK + json.dumps(result) + "\n")
|
|
124
|
+
sys.stdout.flush()
|
|
125
|
+
os._exit(0) # skip engine teardown, which can hang on some versions; the OS frees the GPU
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|