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,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()