crit-eval 0.1.1__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 (58) hide show
  1. crit_eval/__init__.py +104 -0
  2. crit_eval/__main__.py +10 -0
  3. crit_eval/ab.py +94 -0
  4. crit_eval/ceiling.py +259 -0
  5. crit_eval/chat.py +462 -0
  6. crit_eval/cli.py +347 -0
  7. crit_eval/context.py +300 -0
  8. crit_eval/deadline.py +43 -0
  9. crit_eval/estimate.py +258 -0
  10. crit_eval/examples/mock_adapter.py +154 -0
  11. crit_eval/fixed.py +143 -0
  12. crit_eval/gguf_deepseek4.py +122 -0
  13. crit_eval/gguf_layout.py +308 -0
  14. crit_eval/gguf_qwen4exp.py +118 -0
  15. crit_eval/headroom.py +869 -0
  16. crit_eval/host.py +213 -0
  17. crit_eval/hostload.py +53 -0
  18. crit_eval/indexers.py +155 -0
  19. crit_eval/kernels.py +422 -0
  20. crit_eval/logprob.py +29 -0
  21. crit_eval/metrics.py +428 -0
  22. crit_eval/outcomes.py +291 -0
  23. crit_eval/packages.py +150 -0
  24. crit_eval/pin.py +82 -0
  25. crit_eval/prepare.py +505 -0
  26. crit_eval/probe/device_probe.cu +550 -0
  27. crit_eval/probe/kernel_trace.cpp +92 -0
  28. crit_eval/probe/logprob_compare.cu +61 -0
  29. crit_eval/probe/logprob_compare.h +23 -0
  30. crit_eval/probe.py +186 -0
  31. crit_eval/protocol.json +199 -0
  32. crit_eval/reference.py +1898 -0
  33. crit_eval/report.py +175 -0
  34. crit_eval/runner.py +1488 -0
  35. crit_eval/schemes.py +507 -0
  36. crit_eval/status.py +1019 -0
  37. crit_eval/suite/agent/README.md.txt +158 -0
  38. crit_eval/suite/agent/cli.py.txt +86 -0
  39. crit_eval/suite/agent/headroom.py.txt +117 -0
  40. crit_eval/suite/agent/metrics.py.txt +106 -0
  41. crit_eval/suite/agent/prepare.py.txt +135 -0
  42. crit_eval/suite/agent/reference.py.txt +171 -0
  43. crit_eval/suite/agent/report.py.txt +87 -0
  44. crit_eval/suite/agent/runner.py.txt +180 -0
  45. crit_eval/suite/agent/tools.json +6 -0
  46. crit_eval/suite/followup.json +12 -0
  47. crit_eval/suite/heldout.json +26 -0
  48. crit_eval/suite/long.json +40 -0
  49. crit_eval/suite/long_document.txt +503 -0
  50. crit_eval/suite/quality.json +48 -0
  51. crit_eval/suite/speed.json +69 -0
  52. crit_eval/tensors.py +52 -0
  53. crit_eval-0.1.1.dist-info/METADATA +186 -0
  54. crit_eval-0.1.1.dist-info/RECORD +58 -0
  55. crit_eval-0.1.1.dist-info/WHEEL +5 -0
  56. crit_eval-0.1.1.dist-info/entry_points.txt +2 -0
  57. crit_eval-0.1.1.dist-info/licenses/LICENSE +208 -0
  58. crit_eval-0.1.1.dist-info/top_level.txt +1 -0
crit_eval/__init__.py ADDED
@@ -0,0 +1,104 @@
1
+ """crit eval: one benchmark and quality suite that every engine runs the same way."""
2
+ import json
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+
7
+ # The package holds its suite, protocol, probes and examples; a checkout of crit-eval is its parent.
8
+ EVAL_DIR = Path(__file__).resolve().parent
9
+ SUITE_DIR = EVAL_DIR / "suite"
10
+ __version__ = "0.1.1"
11
+ CONTRACT = "eval-v1"
12
+ PROTOCOL_ID = re.compile(r"crit-eval-v(\d+)\.(\d+)\.(\d+)")
13
+ # The ids before crit-eval had its own repository (crit-eval-v4, crit-eval-v4.1, ... crit-eval-v8): major and minor.
14
+ EARLIER_ID = re.compile(r"crit-eval-v([1-9]\d*)(?:\.(\d+))?")
15
+
16
+
17
+ def protocol_version(identifier):
18
+ """crit-eval-v0.1.0 -> (0, 1, 0). An earlier id sorts below every such version: crit-eval-v4.1 -> (-1, 4, 1)."""
19
+ text = str(identifier)
20
+ match = PROTOCOL_ID.fullmatch(text)
21
+ if match:
22
+ return tuple(int(part) for part in match.groups())
23
+ match = EARLIER_ID.fullmatch(text)
24
+ if match:
25
+ return -1, int(match[1]), int(match[2] or 0)
26
+ raise ValueError(f"not a crit-eval protocol id: {identifier}")
27
+
28
+
29
+ def protocol_series(identifier):
30
+ """What two protocols share when their scores compare and data prepared under one runs under the other: the minor
31
+ version before 1.0 (crit-eval-v0.1.x), the major from 1.0 on; an earlier id's major (docs/README.md)."""
32
+ major, minor, _ = protocol_version(identifier)
33
+ return (major, minor) if major <= 0 else (major,)
34
+
35
+
36
+ # The context a user wants engines built for, unless settings.toml's context_tokens says otherwise; the model's
37
+ # positions and the device's memory may allow less.
38
+ DEFAULT_CONTEXT_TOKENS = 262_144
39
+
40
+
41
+ def checkouts():
42
+ """The checkouts crit-eval runs from: crit-eval's (when the package is not installed into site-packages) and the
43
+ crit
44
+ checkout of the crit_core it imports, which holds the machine's settings.toml."""
45
+ import crit_core
46
+ found = [EVAL_DIR.parent, Path(crit_core.__file__).resolve().parents[3]]
47
+ return [root for i, root in enumerate(found) if root not in found[:i] and (root / ".git").exists()]
48
+
49
+
50
+ def machine_settings():
51
+ """Memory thresholds from settings.toml (vram_headroom_gib, context_reserve_gib), the wanted context
52
+ (context_tokens) and the reference package source (eval_packages, crit_eval.packages): crit-eval's checkout's,
53
+ else the crit checkout's (checkouts); without one crit-eval uses the protocol's defaults and
54
+ DEFAULT_CONTEXT_TOKENS."""
55
+ path = next((root / "settings.toml" for root in checkouts() if (root / "settings.toml").is_file()), None)
56
+ if path is None:
57
+ return {}
58
+ import tomllib
59
+ value = tomllib.loads(path.read_text(encoding="utf-8"))
60
+ found = {key: float(value[key]) for key in ("vram_headroom_gib", "context_reserve_gib")
61
+ if isinstance(value.get(key), (int, float)) and not isinstance(value.get(key), bool)}
62
+ if type(value.get("context_tokens")) is int and value["context_tokens"] > 0:
63
+ found["context_tokens"] = value["context_tokens"]
64
+ if isinstance(value.get("eval_packages"), str) and value["eval_packages"]:
65
+ found["eval_packages"] = value["eval_packages"]
66
+ return found
67
+
68
+
69
+ def requested_context():
70
+ """The context the user wants: settings.toml's context_tokens, else DEFAULT_CONTEXT_TOKENS."""
71
+ return machine_settings().get("context_tokens", DEFAULT_CONTEXT_TOKENS)
72
+
73
+
74
+ def portable_path(path):
75
+ """A path as results store it, naming no user: relative to a checkout crit-eval runs from (posix) inside it, `~/...` inside
76
+ the home directory, the bare file name where another user directory (/home/NAME, C:/Users/NAME, WSL's
77
+ /mnt/c/Users/NAME) holds it, else the absolute path (toolkits under /usr/local or Program Files). None stays
78
+ None."""
79
+ if path is None:
80
+ return None
81
+ path = Path(os.path.abspath(path))
82
+ for root, prefix in [(root, "") for root in checkouts()] + [(Path.home(), "~/")]:
83
+ try:
84
+ return prefix + path.relative_to(root).as_posix()
85
+ except ValueError:
86
+ pass
87
+ if any(part.lower() in ("users", "home") for part in path.parts[:-2]):
88
+ return path.name
89
+ return path.as_posix()
90
+
91
+
92
+ def load_json(path):
93
+ with open(path, encoding="utf-8") as handle:
94
+ return json.load(handle)
95
+
96
+
97
+ def write_json(path, value):
98
+ path = Path(path)
99
+ path.parent.mkdir(parents=True, exist_ok=True)
100
+ temporary = path.with_name(path.name + ".tmp")
101
+ with open(temporary, "w", encoding="utf-8") as handle:
102
+ json.dump(value, handle, indent=1)
103
+ handle.write("\n")
104
+ os.replace(temporary, path)
crit_eval/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ if __package__ in (None, ""):
5
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
6
+ from crit_eval.cli import main
7
+ else:
8
+ from .cli import main
9
+
10
+ raise SystemExit(main())
crit_eval/ab.py ADDED
@@ -0,0 +1,94 @@
1
+ """Paired speed runs: `crit eval ab --adapter A --config CA --config-b CB --model M --data D --out FILE`.
2
+
3
+ A shared device drifts between sessions (other processes, clocks, temperature) by more than the
4
+ run-to-run spread inside one session, so two results measured apart cannot confirm a small change.
5
+ `ab` measures both engines in one session: each round runs the speed suite for both, in alternating
6
+ order (A B, then B A), loading and closing each engine in turn. It reports every round's scores and the
7
+ paired ratio B / A per round; the median and spread of those ratios are facts about the pair, measured
8
+ under the same conditions.
9
+ """
10
+ import datetime
11
+ import time
12
+ from pathlib import Path
13
+
14
+ from . import EVAL_DIR, load_json, metrics, write_json
15
+ from .pin import load_continuations
16
+ from .prepare import PREPARED, check_prepared
17
+ from .runner import load_adapter, open_engine, run_speed
18
+
19
+ KPIS = ("decode_score", "prefill_score", "ttft_score_ms")
20
+
21
+
22
+ def run_ab(adapter_a, config_a, adapter_b, config_b, model, data, out, rounds=2, warmups=None, repetitions=None,
23
+ log=print, context_tokens=None):
24
+ protocol = load_json(EVAL_DIR / "protocol.json")
25
+ prepared = load_json(Path(data) / PREPARED)
26
+ check_prepared(prepared, protocol, data)
27
+ if rounds < 1:
28
+ raise SystemExit("--rounds must be at least 1")
29
+ continuations = load_continuations(data, prepared)
30
+ warmups = protocol["speed"]["warmup_runs"] if warmups is None else warmups
31
+ repetitions = protocol["speed"]["repetitions"] if repetitions is None else repetitions
32
+ engines = {"a": (load_adapter(adapter_a), dict(config_a or {})), "b": (load_adapter(adapter_b), dict(config_b or {}))}
33
+ result = {"started_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
34
+ "model_sha256": prepared["model"]["sha256"], "suite_sha256": prepared["suite_sha256"],
35
+ "warmup_runs": warmups, "repetitions": repetitions, "context_tokens": context_tokens, "rounds": [],
36
+ "engines": {}}
37
+ for r in range(rounds):
38
+ order = ("a", "b") if r % 2 == 0 else ("b", "a")
39
+ row = {"order": list(order)}
40
+ for key in order:
41
+ module, config = engines[key]
42
+ started = time.perf_counter()
43
+ engine, info = open_engine(module, model, config, speculative=True, context_tokens=context_tokens)
44
+ try:
45
+ speed = run_speed(engine, info, prepared, protocol, warmups, repetitions,
46
+ lambda message, k=key: log(f"round {r + 1} {k}: {message}"), continuations)
47
+ finally:
48
+ engine.close()
49
+ result["engines"].setdefault(key, {"name": info["name"], "configuration": info["configuration"]})
50
+ row[key] = {kpi: speed.get(kpi) for kpi in KPIS} | {"complete": speed["complete"],
51
+ "decode_text": speed["decode_text"],
52
+ "seconds": time.perf_counter() - started}
53
+ result["rounds"].append(row)
54
+ result["paired"] = paired(result["rounds"])
55
+ result["ended_at"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
56
+ write_json(out, result)
57
+ return result
58
+
59
+
60
+ def paired(rounds):
61
+ """Per KPI: the B / A ratio in each round, their median and spread (max - min). For time to first
62
+ token the ratio is A / B, so a ratio above 1 means B is faster on every KPI."""
63
+ out = {}
64
+ for kpi in KPIS:
65
+ ratios = []
66
+ for row in rounds:
67
+ a, b = row["a"].get(kpi), row["b"].get(kpi)
68
+ if a and b:
69
+ ratios.append(a / b if kpi == "ttft_score_ms" else b / a)
70
+ out[kpi] = {"ratios": ratios, "median": metrics.median(ratios),
71
+ "spread": (max(ratios) - min(ratios)) if len(ratios) > 1 else None}
72
+ return out
73
+
74
+
75
+ def render(result):
76
+ names = {k: v["name"] for k, v in result["engines"].items()}
77
+ lines = [f"A = {names.get('a')}, B = {names.get('b')}; {len(result['rounds'])} rounds, order alternating"]
78
+ for i, row in enumerate(result["rounds"], 1):
79
+ cells = []
80
+ for key in ("a", "b"):
81
+ r = row[key]
82
+ cells.append(f"{key.upper()}: decode {_fmt(r['decode_score'], '.1f')}, prefill {_fmt(r['prefill_score'], ',.0f')}, "
83
+ f"ttft {_fmt(r['ttft_score_ms'], ',.0f')} ms")
84
+ lines.append(f"round {i} ({' '.join(k.upper() for k in row['order'])}): " + "; ".join(cells))
85
+ for kpi, label in (("decode_score", "decode"), ("prefill_score", "prefill"), ("ttft_score_ms", "ttft (speed)")):
86
+ p = result["paired"][kpi]
87
+ if p["median"] is not None:
88
+ lines.append(f"B vs A {label}: {p['median'] - 1:+.1%} median over rounds"
89
+ + (f", spread {p['spread']:.1%}" if p["spread"] is not None else ""))
90
+ return "\n".join(lines)
91
+
92
+
93
+ def _fmt(value, spec):
94
+ return "–" if value is None else format(value, spec)
crit_eval/ceiling.py ADDED
@@ -0,0 +1,259 @@
1
+ """The model's arithmetic per token, for the prefill floor, its weight bytes, for the memory
2
+ floor of a short prompt, and its KV bytes per token, for the decode floor at long context. Counted once from the weight headers and config when the
3
+ suite is prepared, so every engine evaluated against one data directory gets the same ceiling.
4
+
5
+ Prefill is compute bound. Its floor for a prompt of N tokens is
6
+
7
+ matmul N x sum over weight formats of FLOPs per token / that format's GEMM rate
8
+ attention N(N+1)/2 query-key pairs x FLOPs per pair / the BF16 rate
9
+ recurrent N x linear-attention FLOPs per token / the BF16 rate (chunked form, approximate)
10
+
11
+ A projection weight of R x K elements costs 2RK FLOPs per token at the rate of the format it is stored in
12
+ (crit_eval.schemes): BF16/F16/F32 (bf16), FP8 (fp8), NVFP4 (nvfp4), weight-only integer codes (AWQ, GPTQ,
13
+ MLX, bitsandbytes NF4, compressed-tensors pack-quantized: bf16, as engines widen them for the GEMM) and 8-bit
14
+ integer weights with per-row scales (bitsandbytes LLM.int8, compressed-tensors int-quantized: fp8, the int8 rate).
15
+ A GGUF's ggml block types
16
+ count at the fp8 rate (engines multiply their integer codes on 8-bit tensor cores, which run int8 and FP8 at
17
+ the same rate), its MXFP4 and NVFP4 at the nvfp4 rate and its F32/F16/BF16 at bf16. Embedding tables
18
+ (gathered), vision towers, norms, convolutions and the output head (one row of logits per
19
+ prompt) are left out. Engines may compute in other formats; the floor uses the stored ones, and
20
+ prefill_floor_ms prices the quantized matrices with other prompt activations on request (ACTIVATIONS:
21
+ BF16, FP16 accumulation, FP8), as crit_eval.headroom.activation_ceilings reports them.
22
+
23
+ Time to first token cannot beat either floor: the prompt's arithmetic, or reading the weights
24
+ once (language-model tensors and the output head, not embeddings, vision or draft layers)
25
+ plus writing the prompt's KV, at the device bandwidth.
26
+ """
27
+ import math
28
+ import os
29
+
30
+ from crit_core.model import open_model, parse_model_config
31
+ from crit_core.model.gguf import TYPE_BY_NAME, TensorInfo as GgufTensor
32
+
33
+ from .schemes import PRIMARY, quantization_of, refuse_unread, stored
34
+ from .tensors import tensor_role
35
+
36
+
37
+ def _source(model):
38
+ # A model path that does not exist reads as a model without files, as a folder without them does.
39
+ return open_model(model) if os.path.lexists(model) else None
40
+
41
+
42
+ def model_config(model):
43
+ """The model's config.json read with Core's rules (crit_core.model.ModelSource.config): the folder's own, or
44
+ the one a GGUF's metadata derives; an empty config when the model has none. Rules read the text section
45
+ (`text_config` over the root)."""
46
+ source = _source(model)
47
+ return source.config() if source else parse_model_config({})
48
+
49
+
50
+ def weight_files(model, drafts=False):
51
+ """The weight files of the model (a folder or a weight file, crit_core.model.open_model) with the draft blocks
52
+ their tensors' roles need: (files, draft_blocks). Without `drafts`, draft-layer files are left out; with it,
53
+ those beside the checkpoint join it. ([], no blocks) for a folder without weights."""
54
+ source = _source(model)
55
+ return (list(source.weight_files(drafts)), source.draft_blocks) if source else ([], frozenset())
56
+
57
+
58
+ def weight_shards(model, drafts=False):
59
+ """The safetensors files of the model, validated by crit_core.model (weight_files of a safetensors model);
60
+ [] for any other model."""
61
+ source = _source(model)
62
+ return list(source.weight_files(drafts)) if source and source.format == "safetensors" else []
63
+
64
+
65
+ def _read(role):
66
+ # A pass of the language model reads its layers, experts and head; embedding rows are gathered.
67
+ return role.kind in ("layer", "expert", "head") and not role.draft
68
+
69
+
70
+ def weight_bytes(tensors, file_name="", draft_blocks=()):
71
+ """Bytes of the tensors one forward pass reads (crit_eval.tensors: layers, experts and the output head, not
72
+ embeddings, other modalities or draft layers). tensors: {name: crit_core.model.TensorInfo} of one file."""
73
+ return sum(t.nbytes for name, t in tensors.items() if _read(tensor_role(name, file_name, draft_blocks)))
74
+
75
+
76
+ def _ggml_format(dtype):
77
+ """A GGUF tensor's GEMM rate key (see the module docstring); None for the integer types."""
78
+ if dtype in ("F32", "F16", "BF16"):
79
+ return "bf16"
80
+ if dtype in ("MXFP4", "NVFP4"):
81
+ return "nvfp4"
82
+ return "fp8" if TYPE_BY_NAME[dtype].quantized else None
83
+
84
+
85
+ def matmul_flops(tensors, file_name="", draft_blocks=(), quantization=None):
86
+ """FLOPs per token by stored weight format, and how many matrices each format has, over one file's
87
+ {name: TensorInfo}. The output head is left out (one row of logits per prompt). A 3-D expert tensor counts as
88
+ one matrix per expert, as the per-expert matrices of a checkpoint that stores them apart do. quantization: the
89
+ config's quantization_config (or MLX's quantization), which names the scheme of packed integer weights
90
+ (crit_eval.schemes)."""
91
+ flops, matrices, seen = {}, {}, set()
92
+ for name, tensor in tensors.items():
93
+ role = tensor_role(name, file_name, draft_blocks)
94
+ stacked = len(tensor.shape) == 3 and role.kind == "expert"
95
+ if not _read(role) or role.kind == "head":
96
+ continue
97
+ count = tensor.shape[0] if stacked else 1
98
+ if isinstance(tensor, GgufTensor):
99
+ if "conv" in name or (len(tensor.shape) != 2 and not stacked): # GGUF squeezes conv kernels to 2-D
100
+ continue
101
+ fmt = _ggml_format(tensor.dtype) if name.endswith(".weight") else None
102
+ elements = math.prod(tensor.shape)
103
+ else:
104
+ base = next((name[:-len(suffix)] for suffix in PRIMARY if name.endswith(suffix)), None)
105
+ found = None if base is None or base in seen else stored(tensors, base, quantization)
106
+ # Plain and FP8 weights of other ranks are norms, biases and convolution kernels, not matrices.
107
+ if found is None or (found[0] in ("plain", "fp8") and len(tensor.shape) != 2 and not stacked):
108
+ continue
109
+ seen.add(base)
110
+ _, fmt, elements = found
111
+ if fmt:
112
+ flops[fmt] = flops.get(fmt, 0) + 2 * elements
113
+ matrices[fmt] = matrices.get(fmt, 0) + count
114
+ return flops, matrices
115
+
116
+
117
+ def model_ceiling(model):
118
+ """Per-token arithmetic and KV bytes of the model, or None for a model without weight files."""
119
+ shards, drafts = weight_files(model)
120
+ if not shards:
121
+ return None
122
+ config = model_config(model)
123
+ quantization = quantization_of(config)
124
+ flops, matrices, read = {}, {}, 0
125
+ for shard in shards:
126
+ tensors = {tensor.name: tensor for tensor in shard.tensors}
127
+ refuse_unread(quantization, tensors)
128
+ read += weight_bytes(tensors, shard.path.name, drafts)
129
+ f, m = matmul_flops(tensors, shard.path.name, drafts, quantization)
130
+ for key in f:
131
+ flops[key] = flops.get(key, 0) + f[key]
132
+ matrices[key] = matrices.get(key, 0) + m[key]
133
+ shape = config.attention_shape()
134
+ out = {"matmul_flops_per_token": flops, "matrices": matrices, "weight_bytes": read,
135
+ "attention_layers": None, "attention_flops_per_pair": None, "kv_bytes_per_token": None,
136
+ "recurrent_flops_per_token": 0}
137
+ if shape:
138
+ full, heads, head_dim, kv_heads = shape
139
+ # Scores (QK^T) and weighted values (PV): 2 x 2 x heads x head_dim FLOPs per query-key pair.
140
+ out.update(attention_layers=full, attention_flops_per_pair=4 * heads * head_dim * full,
141
+ kv_bytes_per_token=2 * kv_heads * head_dim * 2 * full) # K and V, BF16
142
+ linear = config.layer_types.count("linear_attention")
143
+ attention = config.linear
144
+ hv, dk, dv = ((attention.num_value_heads, attention.key_head_dim, attention.value_head_dim) if attention
145
+ else (None, None, None))
146
+ if linear and hv and dk and dv:
147
+ chunk = 64 # chunked gated delta rule: state read/update plus intra-chunk scores and values
148
+ out["recurrent_flops_per_token"] = linear * hv * (4 * dk * dv + 2 * chunk * (dk + dv))
149
+ return out
150
+
151
+
152
+ RATE_SOURCES = (("tflops", "cuBLASLt GEMM"), ("mma_tflops", "raw MMA"))
153
+
154
+
155
+ def best_rates(device):
156
+ """The best achievable TFLOPS per format of a `crit eval probe` result, and what achieved each: {key: tflops} and
157
+ {key: "cuBLASLt GEMM" or "raw MMA"}. cuBLASLt's GEMMs (bf16, fp8, nvfp4) are not every device's best kernels
158
+ (an RTX 5090's BF16 GEMM reaches 185 TFLOPS where its BF16 tensor cores run 252), so each format takes the larger
159
+ of its GEMM rate and its raw tensor-core rate (bf16, fp16, fp16_acc, fp8, nvfp4, fp4_fp8). A probe from before
160
+ the raw rates has GEMM rates alone."""
161
+ rates, sources = {}, {}
162
+ for key, source in RATE_SOURCES:
163
+ for name, value in ((device or {}).get(key) or {}).items():
164
+ if value and value > rates.get(name, 0):
165
+ rates[name], sources[name] = value, source
166
+ return rates, sources
167
+
168
+
169
+ # The activation formats a quantized matrix's prompt GEMM can take, and the rates (crit_eval.probe keys) each runs at
170
+ # for each stored weight format, the best available one counting: BF16 activations on BF16 tensor cores with FP32
171
+ # accumulation, FP16 activations accumulating in FP16, FP8 activations (FP4 weights times FP8 through kind::f8f6f4, or
172
+ # widened to FP8), and the stored format itself (W8A8, W4A4). Weights stored in BF16 stay at the BF16 rate.
173
+ ACTIVATIONS = {"bf16": {"fp8": ("bf16",), "nvfp4": ("bf16",)},
174
+ "fp16_acc": {"fp8": ("fp16_acc",), "nvfp4": ("fp16_acc",)},
175
+ "fp8": {"fp8": ("fp8",), "nvfp4": ("fp4_fp8", "fp8")},
176
+ "stored": {"fp8": ("fp8",), "nvfp4": ("nvfp4",)}}
177
+ QUANTIZED = ("fp8", "nvfp4")
178
+
179
+
180
+ def quantized_formats(ceiling):
181
+ """The quantized stored formats whose matrices the model's prompt GEMMs multiply (fp8, nvfp4)."""
182
+ return [f for f in QUANTIZED if ((ceiling or {}).get("matmul_flops_per_token") or {}).get(f)]
183
+
184
+
185
+ def activation_shares(activations, formats):
186
+ """{stored format: {activation: share}} from an engine's prompt_activations: one activation for every quantized
187
+ format ("fp16_acc"), one per format ({"nvfp4": "fp16_acc", "fp8": "fp8"}) or shares of a format's FLOPs
188
+ ({"nvfp4": {"fp16_acc": 0.93, "bf16": 0.07}}); None when it names no format of `formats` or an activation that is
189
+ not one of ACTIVATIONS. A format it leaves out keeps its stored activations."""
190
+ if isinstance(activations, str):
191
+ activations = {f: activations for f in formats}
192
+ if not isinstance(activations, dict):
193
+ return None
194
+ shares = {}
195
+ for f in formats:
196
+ value = activations.get(f, "stored")
197
+ value = {value: 1.0} if isinstance(value, str) else value
198
+ if (not isinstance(value, dict) or not value or any(k not in ACTIVATIONS for k in value)
199
+ or any(not isinstance(v, (int, float)) or v < 0 for v in value.values())):
200
+ return None
201
+ total = sum(value.values())
202
+ if total <= 0:
203
+ return None
204
+ shares[f] = {k: v / total for k, v in value.items()}
205
+ return shares if any(f in activations for f in formats) else None
206
+
207
+
208
+ def matmul_rate(tflops, stored, activation=None):
209
+ """(TFLOPS, rate key) a stored format's matrices multiply at with the given activation (ACTIVATIONS; None: the
210
+ stored format's own), the best of the activation's rates; (None, the keys tried) when none was probed."""
211
+ keys = ACTIVATIONS[activation].get(stored, (stored,)) if activation else (stored,)
212
+ found = [(tflops[key], key) for key in keys if (tflops or {}).get(key)]
213
+ return max(found) if found else (None, keys)
214
+
215
+
216
+ def prefill_floor_ms(ceiling, tflops, prompt_tokens, activations=None):
217
+ """Floor of one prefill in ms with its parts (matmul_by_format: each stored format's matmul ms), or (None,
218
+ missing rates). activations: {stored format: {activation: share}} (activation_shares) for the quantized
219
+ formats' prompt GEMMs; without it each format multiplies at its own rate."""
220
+ if not ceiling or not tflops:
221
+ return None, ["device GEMM rates (--device)"] if not tflops else ["model arithmetic (re-run prepare)"]
222
+ missing, by_format, n = [], {}, prompt_tokens
223
+ for f, flops in ceiling["matmul_flops_per_token"].items():
224
+ ms = 0.0
225
+ for activation, share in ((activations or {}).get(f) or {None: 1.0}).items():
226
+ rate, key = matmul_rate(tflops, f, activation)
227
+ if rate is None:
228
+ missing += [k for k in key if k not in missing]
229
+ continue
230
+ ms += share * n * flops / (rate * 1e12) * 1000
231
+ by_format[f] = ms
232
+ if not tflops.get("bf16"):
233
+ missing.append("bf16")
234
+ if missing:
235
+ return None, [f"{f} GEMM rate" for f in sorted(set(missing))]
236
+ matmul = sum(by_format.values())
237
+ pairs = n * (n + 1) / 2
238
+ # Attention at the faster of the BF16 and FP8 rates: engines run Q K^T and P V on FP8 tensor cores within
239
+ # the quality budget, so a BF16-only floor is one they beat.
240
+ attention_rate = max(tflops["bf16"], tflops.get("fp8") or 0)
241
+ attention = pairs * (ceiling.get("attention_flops_per_pair") or 0) / (attention_rate * 1e12) * 1000
242
+ recurrent = n * (ceiling.get("recurrent_flops_per_token") or 0) / (tflops["bf16"] * 1e12) * 1000
243
+ return {"matmul_ms": matmul, "attention_ms": attention, "recurrent_ms": recurrent,
244
+ "total_ms": matmul + attention + recurrent, "matmul_by_format": by_format}, []
245
+
246
+
247
+ def ttft_floor_ms(ceiling, tflops, bandwidth_gbs, prompt_tokens, bytes_per_pass=None, activations=None):
248
+ """Floor of the time to first token: the larger of the prompt's compute floor (with the quantized formats'
249
+ activations, prefill_floor_ms) and one read of the weights (bytes_per_pass when the adapter declares it) plus the
250
+ prompt's KV writes."""
251
+ compute, missing = prefill_floor_ms(ceiling, tflops, prompt_tokens, activations)
252
+ weights = bytes_per_pass or (ceiling or {}).get("weight_bytes")
253
+ if not weights or not bandwidth_gbs:
254
+ missing = missing + ["device bandwidth (--device)" if not bandwidth_gbs else "model weight bytes (re-run prepare)"]
255
+ if missing:
256
+ return None, missing
257
+ memory = (weights + (ceiling.get("kv_bytes_per_token") or 0) * prompt_tokens) / (bandwidth_gbs * 1e9) * 1000
258
+ return {"compute_ms": compute["total_ms"], "memory_ms": memory, "total_ms": max(compute["total_ms"], memory),
259
+ "bound": "compute" if compute["total_ms"] >= memory else "memory"}, []