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,504 @@
1
+ """Bytes read per batch-1 decode token, from the real tensors of a checkpoint.
2
+
3
+ ``roofline --method headers`` (the default whenever safetensors are available). The weight
4
+ term is the sum of stored tensor bytes (dtype x shape from the safetensors headers, so packed
5
+ FP4 values, FP8 values and every scale tensor count exactly as stored), grouped by the role of
6
+ the module each tensor belongs to:
7
+
8
+ attention full / sliding attention projections and their scales
9
+ linear_attention Gated DeltaNet (or other linear-attention / SSM) projections, conv, A_log...
10
+ mlp_dense dense MLPs (including the always-on MLP next to an MoE block)
11
+ moe_experts routed experts, counted x top_k / num_experts (batch 1 reads top_k of them)
12
+ moe_shared shared experts (always on)
13
+ moe_router router / gate weights
14
+ norms norm weights and per-layer scalars
15
+ lm_head the logits matrix, read in full every token (a tied one is the embedding,
16
+ counted once here)
17
+ embedding_row one row of each token-embedding table (the current token's)
18
+ other any text-decoder tensor not matched above (counted; listed by name pattern)
19
+
20
+ Excluded from decode (reported, not counted): vision / audio towers and projectors, the MTP /
21
+ next-n head (unless include_mtp), and the unread rows of the embedding table.
22
+
23
+ The per-token state term adds:
24
+ * full-attention KV: context x 2 (K, V) x kv width x kv bytes per layer;
25
+ * sliding-window layers: min(context, window) tokens instead of context;
26
+ * linear-attention (Gated DeltaNet) recurrent state, read and written every token:
27
+ num_value_heads x key_head_dim x value_head_dim per layer, plus the conv state
28
+ (conv channels x (kernel - 1)), also read and written. Architectures whose state layout is
29
+ not known here are marked UNPROVEN rather than guessed.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import re
35
+ from collections import defaultdict
36
+
37
+ from inferencekit.checkpoint import Checkpoint, Tensor
38
+ from inferencekit.ops.roofline import text_config
39
+ from inferencekit.registry import OpError
40
+
41
+ GROUPS = ("attention", "linear_attention", "mlp_dense", "moe_experts", "moe_shared", "moe_router", "norms",
42
+ "lm_head", "embedding_row", "other")
43
+ KV_BYTES = {"bf16": 2.0, "fp16": 2.0, "fp8": 1.0, "q8_0": 34 / 32}
44
+ STATE_BYTES = {"fp32": 4, "bf16": 2, "fp16": 2}
45
+ CONFIG_DTYPE = {"float32": "fp32", "float": "fp32", "bfloat16": "bf16", "float16": "fp16", "half": "fp16"}
46
+
47
+ N_EXPERTS_KEYS = ("num_experts", "num_local_experts", "n_routed_experts", "moe_num_experts", "num_routed_experts")
48
+ TOP_K_KEYS = ("num_experts_per_tok", "top_k_experts", "moe_top_k", "num_experts_per_token")
49
+ MTP_KEYS = ("num_nextn_predict_layers", "mtp_num_hidden_layers", "num_mtp_layers")
50
+
51
+ _TOWER = {"visual", "vision_tower", "vision_model", "vision_encoder", "embed_vision", "vision_embed",
52
+ "multi_modal_projector", "mm_projector", "audio_tower", "audio_model", "audio_encoder", "embed_audio",
53
+ "image_newline", "speech_encoder"}
54
+ _EMBED = {"embed_tokens", "wte", "word_embeddings", "tok_embeddings", "embed_tokens_per_layer"}
55
+ _LINEAR_ATTN = {"linear_attn", "linear_attention", "mamba", "ssm", "mixer", "time_mix"}
56
+ _ATTN = {"self_attn", "attn", "attention", "self_attention"}
57
+ _MLP = {"mlp", "feed_forward", "ffn", "block_sparse_moe", "moe", "channel_mix"}
58
+ _LAYER = re.compile(r"(?:^|\.)(?:layers|h|blocks)\.(\d+)(?:\.|$)")
59
+ _EXPERT_INDEX = re.compile(r"\.experts\.(\d+)\.")
60
+
61
+ MOE_CAVEAT = ("MoE: counted top_k / num_experts of each MoE layer's routed-expert bytes. Batch-1 decode reads "
62
+ "exactly top_k experts per MoE layer per token, so this is exact at batch 1; it assumes no expert "
63
+ "reuse across consecutive tokens from cache (experts are MBs, L2 does not keep them). At batch > 1 "
64
+ "tokens in the same step share any expert they both pick, so bytes per token fall. Shared experts, "
65
+ "the dense MLP and the router are read in full every token.")
66
+
67
+
68
+ def _first(c: dict, keys) -> int | None:
69
+ for k in keys:
70
+ if c.get(k) not in (None, 0):
71
+ return int(c[k])
72
+ return None
73
+
74
+
75
+ def _module(comps: list[str]) -> str:
76
+ """The module a parameter belongs to: 'q_norm' for 'layers.0.self_attn.q_norm.weight'."""
77
+ return comps[-2] if len(comps) > 1 and comps[-1] in ("weight", "bias") else comps[-1]
78
+
79
+
80
+ def _is_norm(comps: list[str]) -> bool:
81
+ m = _module(comps)
82
+ return "norm" in m or m.startswith("ln_") or m in ("ln", "layer_scalar", "layer_scale")
83
+
84
+
85
+ def classify(name: str, c: dict, config: dict) -> tuple[str, int | None]:
86
+ """(group, layer index) of one tensor. Groups outside GROUPS: 'tower', 'mtp', 'extra_layer'."""
87
+ comps = name.split(".")
88
+ if any(x in _TOWER or x.startswith(("vision", "audio")) for x in comps):
89
+ return "tower", None
90
+ if comps[0] in ("mtp", "nextn") or any(x.startswith("mtp") or x == "nextn" or x.startswith("nextn_")
91
+ for x in comps):
92
+ return "mtp", None
93
+ m = _LAYER.search(name)
94
+ if m is None:
95
+ # 'lm_head.weight', or nested as in 'language_model.lm_head.weight' (older multimodal saves)
96
+ if "lm_head" in comps or "embed_out" in comps or name == "output.weight":
97
+ return "lm_head", None
98
+ if any(x in _EMBED for x in comps):
99
+ return "embedding", None
100
+ if _is_norm(comps):
101
+ return "norms", None
102
+ return "other", None
103
+ layer = int(m.group(1))
104
+ n_layers = int(c.get("num_hidden_layers") or 0)
105
+ if n_layers and layer >= n_layers:
106
+ has_mtp = _first(c, MTP_KEYS) or _first(config, MTP_KEYS)
107
+ return ("mtp" if has_mtp else "extra_layer"), layer
108
+ if "shared_expert" in name or "shared_experts" in name:
109
+ return "moe_shared", layer
110
+ if "experts" in comps:
111
+ return "moe_experts", layer
112
+ if "router" in comps or "e_score_correction_bias" in comps or ("gate" in comps and _MLP & set(comps)):
113
+ return "moe_router", layer
114
+ if _is_norm(comps):
115
+ return "norms", layer
116
+ if _LINEAR_ATTN & set(comps):
117
+ return "linear_attention", layer
118
+ if _ATTN & set(comps):
119
+ return "attention", layer
120
+ if _MLP & set(comps):
121
+ return "mlp_dense", layer
122
+ return "other", layer
123
+
124
+
125
+ def _pattern(name: str) -> str:
126
+ return re.sub(r"\.\d+\.", ".N.", name)
127
+
128
+
129
+ # --------------------------------------------------------------------------- weight formats
130
+
131
+ _FLOAT = {"BF16": "bf16", "F16": "fp16", "F32": "fp32", "F64": "fp64"}
132
+ # leaves of a module's quantization tensors ('<module>.weight_scale' belongs to '<module>.weight')
133
+ _SCALE_LEAVES = {"weight_scale", "weight_scale_2", "weight_scale_inv", "input_scale", "weight_global_scale",
134
+ "input_global_scale", "weight_zero_point", "scales", "qzeros", "g_idx"}
135
+ _SCALE_SUFFIXES = ("_scale_2", "_scale_inv", "_scale", "_scales")
136
+
137
+
138
+ def _format(w: Tensor, scale: Tensor | None) -> str:
139
+ """The format of one weight from its stored dtype and its scale tensor's dtype and grouping."""
140
+ if w.dtype == "U8" and scale is not None and w.shape and scale.shape:
141
+ if w.name.endswith("_blocks"): # gpt-oss style MXFP4: [..., blocks, 16 bytes] + [..., blocks] scales
142
+ return "mxfp4"
143
+ group = (w.shape[-1] * 2) / scale.shape[-1] # two FP4 values per byte
144
+ if scale.dtype == "F8_E4M3" and group == 16:
145
+ return "nvfp4"
146
+ if scale.dtype in ("U8", "F8_E8M0") and group == 32:
147
+ return "mxfp4"
148
+ if w.dtype.startswith("F8_"):
149
+ return "fp8"
150
+ return _FLOAT.get(w.dtype, w.dtype.lower())
151
+
152
+
153
+ def tensor_formats(tensors: dict[str, Tensor]) -> dict[str, str]:
154
+ """Format label per tensor. Scale tensors take their weight's format (NVFP4 values are stored as
155
+ U8 pairs and their per-16 scales as F8_E4M3: both are 'nvfp4'); other tensors their own dtype."""
156
+
157
+ def scale_of(name: str) -> Tensor | None:
158
+ stem = name.removesuffix("_blocks")
159
+ return next((tensors[n] for n in (name + "_scale", name + "_scale_inv", stem + "_scales")
160
+ if n in tensors), None)
161
+
162
+ def owner(name: str) -> str | None:
163
+ """The weight a scale tensor belongs to, or None when ``name`` is not a scale tensor."""
164
+ module, _, leaf = name.rpartition(".")
165
+ if leaf in _SCALE_LEAVES and f"{module}.weight" in tensors:
166
+ return f"{module}.weight"
167
+ for suf in _SCALE_SUFFIXES:
168
+ if name.endswith(suf):
169
+ base = name.removesuffix(suf)
170
+ for cand in (base, base + "_blocks"):
171
+ if cand in tensors and cand != name:
172
+ return cand
173
+ return None
174
+
175
+ out = {n: _format(t, scale_of(n)) for n, t in tensors.items() if owner(n) is None}
176
+ for n, t in tensors.items():
177
+ if n not in out:
178
+ out[n] = out.get(owner(n)) or _format(t, None)
179
+ return out
180
+
181
+
182
+ # --------------------------------------------------------------------------- per-layer kinds
183
+
184
+ _KIND = {"full_attention": "full", "attention": "full", "global": "full", "global_attention": "full",
185
+ "sliding_attention": "sliding", "sliding": "sliding", "local": "sliding", "local_attention": "sliding",
186
+ "linear_attention": "linear", "gated_deltanet": "linear", "mamba": "linear", "chunked_attention": "chunked"}
187
+
188
+
189
+ def layer_kinds(c: dict, by_layer: dict[int, set[str]], notes: list[str]) -> list[str]:
190
+ """'full' | 'sliding' | 'linear' | 'chunked' | 'unknown' per decoder layer."""
191
+ n = int(c.get("num_hidden_layers") or (max(by_layer) + 1 if by_layer else 0))
192
+ types = c.get("layer_types")
193
+ if isinstance(types, list) and len(types) == n:
194
+ return [_KIND.get(str(t), "unknown") for t in types]
195
+ kinds = []
196
+ for i in range(n):
197
+ groups = by_layer.get(i, set())
198
+ if "linear_attention" in groups:
199
+ kinds.append("linear")
200
+ elif "attention" in groups:
201
+ kinds.append("full")
202
+ else:
203
+ kinds.append("unknown")
204
+ window = c.get("sliding_window")
205
+ if window and c.get("use_sliding_window") is not False and "full" in kinds:
206
+ notes.append(f"config sets sliding_window={window} without layer_types: every attention layer is counted "
207
+ f"as full attention (an upper bound on KV reads; exact when context <= {window})")
208
+ return kinds
209
+
210
+
211
+ # --------------------------------------------------------------------------- KV cache
212
+
213
+ def _rows(t: Tensor | None) -> int | None:
214
+ return t.shape[0] if t is not None and t.shape else None
215
+
216
+
217
+ def kv_width(i: int, kind: str, c: dict, layer_tensors: dict[str, Tensor], notes: list[str]) -> tuple[int, str]:
218
+ """Elements cached per token in layer i (K and V together), and where the number came from."""
219
+ if c.get("kv_lora_rank"): # MLA: one latent (+ the rope part of K) per token, no separate V
220
+ return int(c["kv_lora_rank"]) + int(c.get("qk_rope_head_dim") or 0), "config (MLA latent)"
221
+ heads = int(c.get("num_attention_heads") or 1)
222
+ if kind == "full" and c.get("num_global_key_value_heads"):
223
+ kvh = int(c["num_global_key_value_heads"])
224
+ else:
225
+ kvh = int(c.get("num_key_value_heads") or heads)
226
+ if kind == "full" and c.get("global_head_dim"):
227
+ hd = int(c["global_head_dim"])
228
+ else:
229
+ hd = int(c.get("head_dim") or int(c["hidden_size"]) // heads)
230
+ cfg_k = kvh * hd
231
+
232
+ def find(suffixes):
233
+ return next((t for n, t in layer_tensors.items() if n.endswith(suffixes)), None)
234
+
235
+ k_rows = _rows(find(("k_proj.weight", ".wk.weight", ".key.weight")))
236
+ v_rows = _rows(find(("v_proj.weight", ".wv.weight", ".value.weight")))
237
+ if k_rows is None:
238
+ return 2 * cfg_k, "config"
239
+ if v_rows is None:
240
+ if c.get("attention_k_eq_v"):
241
+ notes.append("attention_k_eq_v: full-attention layers have no v_proj (V comes from the K projection); "
242
+ "K and V are still counted as two cached tensors, as engines store both")
243
+ v_rows = k_rows
244
+ if k_rows != cfg_k:
245
+ notes.append(f"layer {i}: k_proj has {k_rows} output rows, config gives {kvh} kv heads x {hd} = {cfg_k}; "
246
+ f"used the checkpoint's {k_rows}")
247
+ return k_rows + v_rows, "checkpoint k_proj/v_proj rows"
248
+
249
+
250
+ # --------------------------------------------------------------------------- recurrent state
251
+
252
+ GDN_KEYS = ("linear_num_value_heads", "linear_key_head_dim", "linear_value_head_dim", "linear_num_key_heads")
253
+
254
+
255
+ def gdn_state(c: dict, config: dict, layer_tensors: dict[str, Tensor], state_dtype: str | None,
256
+ conv_dtype: str) -> dict:
257
+ """Gated DeltaNet state of one layer (Qwen3-Next / Qwen3.5-style config names)."""
258
+ if not all(c.get(k) for k in GDN_KEYS):
259
+ keys = sorted(k for k in c if any(s in k for s in ("linear", "ssm", "mamba", "state", "conv")))
260
+ return {"status": "UNPROVEN", "reason": "linear-attention layers, but the config does not use the Gated "
261
+ "DeltaNet names " + ", ".join(GDN_KEYS) + f"; state layout unknown (config keys seen: {keys})"}
262
+ nv, nk = int(c["linear_num_value_heads"]), int(c["linear_num_key_heads"])
263
+ dk, dv = int(c["linear_key_head_dim"]), int(c["linear_value_head_dim"])
264
+ kernel = int(c.get("linear_conv_kernel_dim") or 4)
265
+ conv_dim = 2 * nk * dk + nv * dv # q, k and v channels go through the causal conv
266
+ # Cross-check against the tensors: conv1d [conv_dim, 1, kernel], in_proj_z [nv*dv, hidden], A_log [nv].
267
+ conv = next((t for n, t in layer_tensors.items() if n.endswith("conv1d.weight")), None)
268
+ z = next((t for n, t in layer_tensors.items() if n.endswith("in_proj_z.weight")), None)
269
+ a_log = next((t for n, t in layer_tensors.items() if n.endswith("A_log")), None)
270
+ problems = []
271
+ if conv is not None and (conv.shape[0] != conv_dim or conv.shape[-1] != kernel):
272
+ problems.append(f"conv1d is {list(conv.shape)}, config implies [{conv_dim}, 1, {kernel}]")
273
+ if z is not None and z.shape[0] != nv * dv:
274
+ problems.append(f"in_proj_z has {z.shape[0]} rows, config implies {nv * dv}")
275
+ if a_log is not None and a_log.shape and a_log.shape[0] != nv:
276
+ problems.append(f"A_log has {a_log.shape[0]} heads, config implies {nv}")
277
+ if problems:
278
+ return {"status": "UNPROVEN", "reason": "Gated DeltaNet config does not match the checkpoint: "
279
+ + "; ".join(problems)}
280
+ source = "flag"
281
+ if state_dtype is None:
282
+ cfg = CONFIG_DTYPE.get(str(c.get("mamba_ssm_dtype") or config.get("mamba_ssm_dtype") or ""))
283
+ state_dtype, source = (cfg, "config mamba_ssm_dtype") if cfg else ("bf16", "default")
284
+ recurrent = nv * dk * dv * STATE_BYTES[state_dtype]
285
+ conv_state = conv_dim * (kernel - 1) * STATE_BYTES[conv_dtype]
286
+ return {"status": "ok", "kind": "gated_deltanet", "num_value_heads": nv, "num_key_heads": nk,
287
+ "key_head_dim": dk, "value_head_dim": dv, "conv_dim": conv_dim, "conv_kernel": kernel,
288
+ "state_dtype": state_dtype, "state_dtype_source": source, "conv_dtype": conv_dtype,
289
+ "recurrent_bytes": recurrent, "conv_bytes": conv_state,
290
+ "read_write_bytes_per_token": 2 * (recurrent + conv_state)}
291
+
292
+
293
+ # --------------------------------------------------------------------------- the count
294
+
295
+ def header_decode_bytes(ckpt: Checkpoint, config: dict, context: int, kv_dtype: str = "bf16",
296
+ state_dtype: str | None = None, include_mtp: bool = False) -> dict:
297
+ """Bytes read from DRAM to decode one token at batch 1, from real tensor sizes. No I/O."""
298
+ if kv_dtype not in KV_BYTES:
299
+ raise OpError(f"unsupported kv dtype {kv_dtype}; use one of {sorted(KV_BYTES)}")
300
+ if state_dtype is not None and state_dtype not in STATE_BYTES:
301
+ raise OpError(f"unsupported state dtype {state_dtype}; use one of {sorted(STATE_BYTES)}")
302
+ c = text_config(config)
303
+ notes: list[str] = []
304
+ unproven: list[str] = []
305
+ stored = defaultdict(int)
306
+ read = defaultdict(float)
307
+ count = defaultdict(int)
308
+ by_dtype = defaultdict(float)
309
+ by_format = defaultdict(float)
310
+ formats = tensor_formats(ckpt.tensors)
311
+ other_patterns: set[str] = set()
312
+ by_layer: dict[int, set[str]] = defaultdict(set)
313
+ layer_tensors: dict[int, dict[str, Tensor]] = defaultdict(dict)
314
+ embeddings: list[Tensor] = []
315
+ experts: list[tuple[Tensor, int]] = []
316
+
317
+ def add(group: str, t: Tensor, nbytes: float):
318
+ stored[group] += t.nbytes
319
+ read[group] += nbytes
320
+ count[group] += 1
321
+ by_dtype[t.dtype] += nbytes
322
+ by_format[formats[t.name]] += nbytes
323
+
324
+ for t in ckpt.tensors.values():
325
+ group, layer = classify(t.name, c, config)
326
+ if layer is not None and group not in ("mtp", "extra_layer"):
327
+ by_layer[layer].add(group)
328
+ layer_tensors[layer][t.name] = t
329
+ if group == "tower":
330
+ stored["excluded_towers"] += t.nbytes
331
+ count["excluded_towers"] += 1
332
+ elif group == "mtp":
333
+ if include_mtp:
334
+ add("mtp", t, t.nbytes)
335
+ else:
336
+ stored["excluded_mtp"] += t.nbytes
337
+ count["excluded_mtp"] += 1
338
+ elif group == "embedding":
339
+ embeddings.append(t)
340
+ elif group == "moe_experts":
341
+ experts.append((t, layer))
342
+ else:
343
+ if group in ("other", "extra_layer"):
344
+ other_patterns.add(_pattern(t.name))
345
+ group = "other"
346
+ add(group, t, t.nbytes)
347
+
348
+ # lm_head: a tied one is the embedding matrix, read in full for the logits and counted once.
349
+ vocab = int(c.get("vocab_size") or 0)
350
+ tied = bool(c.get("tie_word_embeddings", config.get("tie_word_embeddings", False)))
351
+ main_embed = next((t for t in embeddings if t.name.endswith(("embed_tokens.weight", "wte.weight",
352
+ "word_embeddings.weight", "tok_embeddings.weight"))),
353
+ None)
354
+ tied_used = False
355
+ if count["lm_head"] == 0:
356
+ if tied and main_embed is not None:
357
+ add("lm_head", main_embed, main_embed.nbytes)
358
+ tied_used = True
359
+ notes.append(f"tied lm_head: {main_embed.name} is read in full for the logits; counted once, as lm_head")
360
+ else:
361
+ unproven.append("lm_head: no lm_head tensor and tie_word_embeddings is false; the logits matrix "
362
+ "is missing from the count")
363
+ for t in embeddings:
364
+ if tied_used and t is main_embed:
365
+ continue # already read in full as the lm_head
366
+ rows = t.shape[0] if t.shape else 1
367
+ add("embedding_row", t, t.nbytes / rows) # stored = the whole table, read = one row
368
+ if vocab and main_embed is not None and main_embed.shape and main_embed.shape[0] != vocab:
369
+ notes.append(f"{main_embed.name} has {main_embed.shape[0]} rows, config vocab_size is {vocab}")
370
+
371
+ # MoE: top_k of num_experts routed experts per MoE layer.
372
+ moe = None
373
+ if experts:
374
+ n_exp = _first(c, N_EXPERTS_KEYS)
375
+ top_k = _first(c, TOP_K_KEYS)
376
+ if not n_exp or not top_k:
377
+ raise OpError("the checkpoint has routed-expert tensors but the config gives no expert count / top-k "
378
+ f"(looked for {N_EXPERTS_KEYS} and {TOP_K_KEYS})")
379
+ frac = top_k / n_exp
380
+ seen: dict[int, set[int]] = defaultdict(set)
381
+ fused_lead: set[int] = set()
382
+ for t, layer in experts:
383
+ add("moe_experts", t, t.nbytes * frac)
384
+ m = _EXPERT_INDEX.search(t.name)
385
+ if m:
386
+ seen[layer].add(int(m.group(1)))
387
+ elif t.shape and t.name.endswith(("proj", "weight", "_blocks", "_scales", "gate_up_proj", "down_proj")):
388
+ fused_lead.add(t.shape[0])
389
+ per_layer_counts = sorted({len(v) for v in seen.values()})
390
+ if per_layer_counts and per_layer_counts != [n_exp]:
391
+ notes.append(f"expert tensors per MoE layer: {per_layer_counts}, config num_experts = {n_exp}")
392
+ if fused_lead and fused_lead != {n_exp}:
393
+ notes.append(f"fused expert tensors lead with {sorted(fused_lead)}, config num_experts = {n_exp}")
394
+ moe_layers = sorted({layer for _, layer in experts})
395
+ moe = {"num_experts": n_exp, "top_k": top_k, "fraction": frac, "moe_layers": len(moe_layers),
396
+ "experts_stored_bytes": stored["moe_experts"], "experts_read_bytes": read["moe_experts"],
397
+ "shared_expert_bytes": read["moe_shared"], "caveat": MOE_CAVEAT}
398
+
399
+ weights = sum(read[g] for g in (*GROUPS, "mtp"))
400
+
401
+ # KV cache and recurrent state.
402
+ kinds = layer_kinds(c, by_layer, notes)
403
+ window = int(c.get("sliding_window") or 0)
404
+ kv_elem = KV_BYTES[kv_dtype]
405
+ kv_layers = []
406
+ kv_total = 0.0
407
+ slope = 0.0 # d(kv bytes) / d(context) at this context, for the report's context check
408
+ state_layers = []
409
+ state_total: float | None = 0.0
410
+ model_dtype = CONFIG_DTYPE.get(str(c.get("dtype") or c.get("torch_dtype") or config.get("torch_dtype")
411
+ or config.get("dtype") or "bfloat16"), "bf16")
412
+ for i, kind in enumerate(kinds):
413
+ lt = layer_tensors.get(i, {})
414
+ if kind in ("full", "sliding"):
415
+ width, source = kv_width(i, kind, c, lt, notes)
416
+ if kind == "sliding":
417
+ if not window:
418
+ unproven.append(f"layer {i}: sliding attention but the config has no sliding_window")
419
+ continue
420
+ tokens = min(context, window)
421
+ else:
422
+ tokens = context
423
+ per_ctx = width * kv_elem
424
+ kv_layers.append({"layer": i, "kind": kind, "elements_per_token": width, "source": source,
425
+ "tokens_read": tokens, "bytes": per_ctx * tokens})
426
+ kv_total += per_ctx * tokens
427
+ if kind == "full" or context < window:
428
+ slope += per_ctx
429
+ elif kind == "linear":
430
+ st = gdn_state(c, config, lt, state_dtype, model_dtype)
431
+ st["layer"] = i
432
+ state_layers.append(st)
433
+ if st["status"] != "ok":
434
+ state_total = None
435
+ unproven.append(f"layer {i}: {st['reason']}")
436
+ elif state_total is not None:
437
+ state_total += st["read_write_bytes_per_token"]
438
+ else:
439
+ unproven.append(f"layer {i}: layer type '{kind}' has no KV/state model here")
440
+ unproven = _dedupe_layers(unproven)
441
+ notes = list(dict.fromkeys(notes))
442
+ kind_counts = {k: kinds.count(k) for k in dict.fromkeys(kinds)}
443
+ state_summary = None
444
+ if state_layers:
445
+ ok = [s for s in state_layers if s["status"] == "ok"]
446
+ state_summary = {"status": "ok" if state_total is not None else "UNPROVEN", "layers": len(state_layers),
447
+ "per_layer": {k: v for k, v in (ok[0] if ok else state_layers[0]).items() if k != "layer"},
448
+ "bytes": state_total}
449
+ total = weights + kv_total + (state_total or 0.0)
450
+ groups = {g: {"read_bytes": read[g], "stored_bytes": stored[g], "tensors": count[g]}
451
+ for g in (*GROUPS, "mtp") if count[g]}
452
+ excluded = {k.removeprefix("excluded_"): {"stored_bytes": stored[k], "tensors": count[k]}
453
+ for k in ("excluded_towers", "excluded_mtp") if count[k]}
454
+ return {
455
+ "method": "headers",
456
+ "checkpoint": {"source": ckpt.source, "commit": ckpt.commit, "files": ckpt.files,
457
+ "headers_digest": ckpt.headers_digest, "tensors": len(ckpt.tensors),
458
+ "stored_bytes": ckpt.stored_bytes},
459
+ "groups": groups,
460
+ "excluded": excluded,
461
+ "other_patterns": sorted(other_patterns),
462
+ "read_bytes_by_dtype": dict(sorted(by_dtype.items())),
463
+ "read_bytes_by_format": dict(sorted(by_format.items())),
464
+ "moe": moe,
465
+ "layer_kinds": kind_counts,
466
+ "sliding_window": window or None,
467
+ "kv": {"dtype": kv_dtype, "bytes_per_element": kv_elem, "layers": _compress(kv_layers), "bytes": kv_total},
468
+ "state": state_summary,
469
+ "include_mtp": include_mtp,
470
+ "lm_head_bytes": read["lm_head"],
471
+ "weight_bytes": weights,
472
+ "kv_bytes_per_context_token": slope,
473
+ "context": context,
474
+ "kv_bytes": kv_total,
475
+ "state_bytes": state_total,
476
+ "bytes_per_token": total,
477
+ "notes": notes,
478
+ "unproven": unproven,
479
+ }
480
+
481
+
482
+ def _compress(kv_layers: list[dict]) -> list[dict]:
483
+ """Group identical per-layer KV rows: [{kind, elements_per_token, tokens_read, layers: [...]}]."""
484
+ out: dict[tuple, dict] = {}
485
+ for row in kv_layers:
486
+ key = (row["kind"], row["elements_per_token"], row["source"], row["tokens_read"])
487
+ g = out.setdefault(key, {"kind": row["kind"], "elements_per_token": row["elements_per_token"],
488
+ "source": row["source"], "tokens_read": row["tokens_read"], "layers": [],
489
+ "bytes": 0.0})
490
+ g["layers"].append(row["layer"])
491
+ g["bytes"] += row["bytes"]
492
+ return list(out.values())
493
+
494
+
495
+ def _dedupe_layers(lines: list[str]) -> list[str]:
496
+ """'layer 3: X', 'layer 7: X' -> 'layers 3, 7: X'."""
497
+ by_reason: dict[str, list[str]] = {}
498
+ for line in lines:
499
+ m = re.match(r"layer (\d+): (.*)", line)
500
+ if m:
501
+ by_reason.setdefault(m.group(2), []).append(m.group(1))
502
+ else:
503
+ by_reason.setdefault(line, [])
504
+ return [(f"layers {', '.join(ls)}: {r}" if ls else r) for r, ls in by_reason.items()]
@@ -0,0 +1,130 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:inferkit:schema:evidence:v1",
4
+ "title": "inferkit evidence record, version 1",
5
+ "description": "One measurement or derivation, content-addressed: evidence_id = 'ev_' + sha256 of the canonical JSON (sorted keys, no whitespace) of every other field.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema", "evidence_id", "kind", "claim_class", "summary", "identity", "provenance",
9
+ "metrics", "artifacts", "coverage", "limitations", "data"],
10
+ "properties": {
11
+ "schema": {"const": "inferkit.evidence.v1"},
12
+ "evidence_id": {"type": "string", "pattern": "^ev_[0-9a-f]{64}$"},
13
+ "kind": {
14
+ "enum": ["environment", "roofline", "ceiling", "benchmark", "nsys_capture", "nsys_timeline",
15
+ "ncu_capture", "ncu_kernels", "correctness", "comparison"]
16
+ },
17
+ "claim_class": {
18
+ "description": "observed = measured on hardware; derived = computed from other evidence or a model config; inferred = a judgement that goes beyond the data.",
19
+ "enum": ["observed", "derived", "inferred"]
20
+ },
21
+ "summary": {"type": "string", "maxLength": 600},
22
+ "identity": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "required": ["gpu", "engine", "model", "workload"],
26
+ "properties": {
27
+ "gpu": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "properties": {
31
+ "name": {"$ref": "#/$defs/optString"},
32
+ "count": {"$ref": "#/$defs/optInt"},
33
+ "driver": {"$ref": "#/$defs/optString"},
34
+ "cuda": {"$ref": "#/$defs/optString"},
35
+ "clocks": {"enum": ["default", "locked", null]}
36
+ }
37
+ },
38
+ "engine": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "properties": {
42
+ "name": {"$ref": "#/$defs/optString"},
43
+ "version": {"$ref": "#/$defs/optString"},
44
+ "commit": {"$ref": "#/$defs/optString"},
45
+ "flags": {"type": "object"}
46
+ }
47
+ },
48
+ "model": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "properties": {
52
+ "id": {"$ref": "#/$defs/optString"},
53
+ "revision": {"$ref": "#/$defs/optString"},
54
+ "dtype": {"$ref": "#/$defs/optString"},
55
+ "quant": {"$ref": "#/$defs/optString"}
56
+ }
57
+ },
58
+ "workload": {
59
+ "type": "object",
60
+ "additionalProperties": false,
61
+ "properties": {
62
+ "prompt_len": {"$ref": "#/$defs/optInt"},
63
+ "gen_len": {"$ref": "#/$defs/optInt"},
64
+ "batch": {"$ref": "#/$defs/optInt"},
65
+ "seed": {"$ref": "#/$defs/optInt"}
66
+ }
67
+ }
68
+ }
69
+ },
70
+ "provenance": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["tool", "tool_version", "argv_digest", "started", "ended"],
74
+ "properties": {
75
+ "tool": {"type": "string"},
76
+ "tool_version": {"type": "string"},
77
+ "argv_digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"},
78
+ "started": {"type": "string"},
79
+ "ended": {"type": "string"}
80
+ }
81
+ },
82
+ "metrics": {"type": "array", "items": {"$ref": "#/$defs/metric"}},
83
+ "artifacts": {"type": "array", "items": {"$ref": "#/$defs/artifact"}},
84
+ "coverage": {
85
+ "type": "object",
86
+ "additionalProperties": false,
87
+ "required": ["complete", "missing"],
88
+ "properties": {
89
+ "complete": {"type": "boolean"},
90
+ "missing": {"type": "array", "items": {"type": "string"}}
91
+ }
92
+ },
93
+ "limitations": {"type": "array", "items": {"type": "string"}},
94
+ "data": {"type": "object"}
95
+ },
96
+ "$defs": {
97
+ "optString": {"type": ["string", "null"]},
98
+ "optInt": {"type": ["integer", "null"]},
99
+ "metric": {
100
+ "type": "object",
101
+ "additionalProperties": false,
102
+ "required": ["name", "value", "unit", "stat"],
103
+ "properties": {
104
+ "name": {"type": "string"},
105
+ "value": {"type": ["number", "null"]},
106
+ "unit": {"type": "string"},
107
+ "stat": {"type": "string"},
108
+ "n": {"type": ["integer", "null"]},
109
+ "ci95": {
110
+ "oneOf": [
111
+ {"type": "null"},
112
+ {"type": "array", "minItems": 2, "maxItems": 2, "items": {"type": ["number", "null"]}}
113
+ ]
114
+ },
115
+ "subject": {"type": "string", "description": "What the metric is about when a record holds several subjects (a kernel name, a buffer size)."}
116
+ }
117
+ },
118
+ "artifact": {
119
+ "type": "object",
120
+ "additionalProperties": false,
121
+ "required": ["path", "sha256", "bytes"],
122
+ "properties": {
123
+ "path": {"type": "string", "description": "Relative to the record's run directory, unless role is 'input'."},
124
+ "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
125
+ "bytes": {"type": "integer", "minimum": 0},
126
+ "role": {"type": "string"}
127
+ }
128
+ }
129
+ }
130
+ }