safffrron commited on
Commit
f157cf0
·
verified ·
1 Parent(s): f8ea377

Upload folder using huggingface_hub

Browse files
src/eaimath/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """CS6013 Efficient AI — compression recipe for Qwen3.5-4B on the math domain."""
2
+
3
+ from .model import DEFAULT_MODEL
4
+
5
+ __all__ = ["DEFAULT_MODEL"]
6
+ __version__ = "0.1.0"
src/eaimath/adaptive_artifact.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Physical packing for the Round-14 block-adaptive Bucket-C representation.
2
+
3
+ The research builder selects one bit width per output-row block, but evaluates
4
+ the resulting values as an ordinary BF16 Hugging Face checkpoint. This module
5
+ turns that selection into the actual submission object: densely packed integer
6
+ codes, FP16 group scales, one uint8 width selector per adaptive block, and BF16
7
+ passthrough tensors for protected parameters.
8
+
9
+ Unlike the older nested Bucket-B artifact, ``state_dict`` here is deliberately a
10
+ flat mapping of names to tensors. The course starter's size audit counts only
11
+ immediate tensor values, so this layout makes its tensor-payload report equal to
12
+ the exact representation accounting instead of accidentally reporting zero.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import math
20
+ import re
21
+ import time
22
+ from pathlib import Path
23
+ from typing import Any, Mapping
24
+
25
+ import torch
26
+
27
+ from .model import load_model, save_checkpoint
28
+ from .pack import pack_tensor, unpack_tensor
29
+ from .quantize import EXCLUDED, QuantSpec, quantize_dequantize
30
+
31
+ FORMAT = "eaimath-block-adaptive-c-v1"
32
+ EXCLUDED_RE = re.compile(EXCLUDED)
33
+ PROTECTED_RE = re.compile(r"A_log|dt_bias|conv1d|in_proj_a|in_proj_b|norm|bias")
34
+ ADAPTIVE_RE = re.compile(r"linear_attn|self_attn|\.mlp\.")
35
+
36
+
37
+ def tensor_payload_bytes(tensors: Mapping[str, torch.Tensor]) -> int:
38
+ """Bytes counted by the starter evaluator's immediate state-dict audit."""
39
+ return sum(int(value.numel()) * int(value.element_size()) for value in tensors.values())
40
+
41
+
42
+ def sha256_file(path: str | Path, chunk_size: int = 8 * 1024 * 1024) -> str:
43
+ digest = hashlib.sha256()
44
+ with Path(path).open("rb") as handle:
45
+ while chunk := handle.read(chunk_size):
46
+ digest.update(chunk)
47
+ return digest.hexdigest()
48
+
49
+
50
+ def _is_adaptive(name: str, tensor: torch.Tensor) -> bool:
51
+ return (
52
+ tensor.ndim == 2
53
+ and name.endswith(".weight")
54
+ and not PROTECTED_RE.search(name)
55
+ and bool(ADAPTIVE_RE.search(name))
56
+ )
57
+
58
+
59
+ def _store_entry(
60
+ entry: dict[str, Any],
61
+ tensors: dict[str, torch.Tensor],
62
+ prefix: str,
63
+ ) -> dict[str, Any]:
64
+ """Move entry tensors into the flat payload and retain JSON-safe metadata."""
65
+ metadata: dict[str, Any] = {}
66
+ tensor_keys: dict[str, str] = {}
67
+ for field, value in entry.items():
68
+ if isinstance(value, torch.Tensor):
69
+ key = f"{prefix}.{field}"
70
+ if key in tensors:
71
+ raise KeyError(f"duplicate artifact tensor key: {key}")
72
+ tensors[key] = value.detach().cpu().contiguous()
73
+ tensor_keys[field] = key
74
+ elif isinstance(value, tuple):
75
+ metadata[field] = list(value)
76
+ else:
77
+ metadata[field] = value
78
+ metadata["tensor_keys"] = tensor_keys
79
+ return metadata
80
+
81
+
82
+ def _load_entry(metadata: Mapping[str, Any], tensors: Mapping[str, torch.Tensor]) -> dict[str, Any]:
83
+ entry = {key: value for key, value in metadata.items() if key != "tensor_keys"}
84
+ for field, key in metadata.get("tensor_keys", {}).items():
85
+ if key not in tensors:
86
+ raise KeyError(f"artifact tensor is missing: {key}")
87
+ entry[field] = tensors[key]
88
+ if "shape" in entry:
89
+ entry["shape"] = tuple(entry["shape"])
90
+ return entry
91
+
92
+
93
+ def _row_ids(widths: torch.Tensor, bit_width: int, rows: int, block: int) -> torch.Tensor:
94
+ selected: list[int] = []
95
+ for block_index, width in enumerate(widths.tolist()):
96
+ if int(width) != bit_width:
97
+ continue
98
+ start = block_index * block
99
+ selected.extend(range(start, min(rows, start + block)))
100
+ return torch.tensor(selected, dtype=torch.int64)
101
+
102
+
103
+ def pack_block_adaptive_state(
104
+ state_dict: Mapping[str, torch.Tensor],
105
+ allocation_report: Mapping[str, Any],
106
+ *,
107
+ reference_state_dict: Mapping[str, torch.Tensor] | None = None,
108
+ scale_normalization: str = "none",
109
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
110
+ """Pack a block-adaptive BF16 state dict and verify exact byte accounting.
111
+
112
+ ``state_dict`` should be the unquantized learned source. When supplied,
113
+ ``reference_state_dict`` is the expanded fake-quant checkpoint used during
114
+ research evaluation; the reported repack error then proves that physical
115
+ codes reproduce that evaluated model without applying a second quantizer.
116
+ """
117
+ if allocation_report.get("format") != FORMAT:
118
+ raise ValueError(
119
+ f"expected allocation format {FORMAT!r}, got {allocation_report.get('format')!r}"
120
+ )
121
+ selected_bits = allocation_report.get("selected_bits")
122
+ if not isinstance(selected_bits, dict) or not selected_bits:
123
+ raise ValueError("allocation report has no selected_bits mapping")
124
+ block = int(allocation_report.get("row_block", 0))
125
+ group_size = int(allocation_report.get("group_size", 0))
126
+ if block <= 0 or group_size <= 0:
127
+ raise ValueError(f"invalid block/group sizes: {block}/{group_size}")
128
+
129
+ tensors: dict[str, torch.Tensor] = {}
130
+ entries: dict[str, dict[str, Any]] = {}
131
+ aliases: dict[str, str] = {}
132
+ seen: dict[int, str] = {}
133
+ reported_names: set[str] = set()
134
+ max_abs_error = 0.0
135
+ max_rmse = 0.0
136
+ max_abs_scale_error = 0.0
137
+ max_scale_rmse = 0.0
138
+ worst_tensor = ""
139
+ canonical_index = 0
140
+
141
+ for name, tensor in state_dict.items():
142
+ if not isinstance(tensor, torch.Tensor) or EXCLUDED_RE.search(name):
143
+ continue
144
+ pointer = tensor.data_ptr()
145
+ if pointer and pointer in seen:
146
+ aliases[name] = seen[pointer]
147
+ continue
148
+ if pointer:
149
+ seen[pointer] = name
150
+ widths_raw = selected_bits.get(name)
151
+ if widths_raw is None:
152
+ raise KeyError(
153
+ f"text tensor {name!r} has no allocation; refusing uncharged passthrough"
154
+ )
155
+ widths = torch.tensor([int(value) for value in widths_raw], dtype=torch.uint8)
156
+ if widths.numel() == 0:
157
+ raise ValueError(f"tensor {name!r} has an empty width selection")
158
+ reported_names.add(name)
159
+ prefix = f"t{canonical_index:04d}"
160
+ canonical_index += 1
161
+
162
+ adaptive = _is_adaptive(name, tensor)
163
+ expected_blocks = math.ceil(tensor.shape[0] / block) if adaptive else 1
164
+ if widths.numel() != expected_blocks:
165
+ raise ValueError(
166
+ f"width count for {name} is {widths.numel()}, expected {expected_blocks}"
167
+ )
168
+
169
+ if adaptive:
170
+ selector_key = f"{prefix}.selector"
171
+ tensors[selector_key] = widths.contiguous()
172
+ groups: dict[str, dict[str, Any]] = {}
173
+ reconstructed = torch.empty_like(tensor, device="cpu")
174
+ for bit_width in sorted(set(int(value) for value in widths.tolist())):
175
+ ids = _row_ids(widths, bit_width, tensor.shape[0], block)
176
+ selected = tensor.detach().cpu().index_select(0, ids)
177
+ packed = pack_tensor(
178
+ selected,
179
+ bit_width,
180
+ group_size=group_size,
181
+ scale_normalization=scale_normalization,
182
+ )
183
+ groups[str(bit_width)] = {
184
+ "rows": int(ids.numel()),
185
+ "entry": _store_entry(packed, tensors, f"{prefix}.b{bit_width}"),
186
+ }
187
+ unpacked = unpack_tensor(packed).to(reconstructed.dtype)
188
+ reconstructed.index_copy_(0, ids, unpacked)
189
+ if bit_width < 16:
190
+ ideal = quantize_dequantize(
191
+ selected,
192
+ QuantSpec(".*", bit_width, group_size),
193
+ )
194
+ scale_delta = ideal.float() - unpacked.float()
195
+ if scale_delta.numel():
196
+ max_abs_scale_error = max(
197
+ max_abs_scale_error, float(scale_delta.abs().max())
198
+ )
199
+ max_scale_rmse = max(
200
+ max_scale_rmse,
201
+ float(scale_delta.square().mean().sqrt()),
202
+ )
203
+ entries[name] = {
204
+ "kind": "adaptive_rows",
205
+ "shape": list(tensor.shape),
206
+ "block": block,
207
+ "selector_key": selector_key,
208
+ "groups": groups,
209
+ }
210
+ else:
211
+ bit_width = int(widths[0])
212
+ packed = pack_tensor(
213
+ tensor.detach().cpu(),
214
+ bit_width,
215
+ group_size=group_size,
216
+ scale_normalization=scale_normalization,
217
+ )
218
+ entries[name] = {
219
+ "kind": "tensor",
220
+ "bits": bit_width,
221
+ "entry": _store_entry(packed, tensors, prefix),
222
+ }
223
+ reconstructed = unpack_tensor(packed)
224
+ if bit_width < 16:
225
+ ideal = quantize_dequantize(
226
+ tensor.detach().cpu(),
227
+ QuantSpec(".*", bit_width, group_size),
228
+ )
229
+ scale_delta = ideal.float() - reconstructed.float()
230
+ if scale_delta.numel():
231
+ max_abs_scale_error = max(
232
+ max_abs_scale_error, float(scale_delta.abs().max())
233
+ )
234
+ max_scale_rmse = max(
235
+ max_scale_rmse,
236
+ float(scale_delta.square().mean().sqrt()),
237
+ )
238
+
239
+ comparison = tensor
240
+ if reference_state_dict is not None:
241
+ if name not in reference_state_dict:
242
+ raise KeyError(f"reference checkpoint is missing text tensor {name!r}")
243
+ comparison = reference_state_dict[name]
244
+ if tuple(comparison.shape) != tuple(tensor.shape):
245
+ raise ValueError(
246
+ f"reference shape for {name} is {tuple(comparison.shape)}, "
247
+ f"expected {tuple(tensor.shape)}"
248
+ )
249
+ delta = comparison.detach().float().cpu() - reconstructed.float().cpu()
250
+ abs_error = float(delta.abs().max()) if delta.numel() else 0.0
251
+ rmse = float(delta.square().mean().sqrt()) if delta.numel() else 0.0
252
+ if abs_error > max_abs_error:
253
+ max_abs_error = abs_error
254
+ worst_tensor = name
255
+ max_rmse = max(max_rmse, rmse)
256
+
257
+ unused = set(selected_bits) - reported_names
258
+ if unused:
259
+ preview = ", ".join(sorted(unused)[:8])
260
+ raise KeyError(f"allocation contains {len(unused)} absent tensors: {preview}")
261
+
262
+ actual_bytes = tensor_payload_bytes(tensors)
263
+ charged_bytes = int(allocation_report.get("total_bytes", -1))
264
+ if actual_bytes != charged_bytes:
265
+ raise ValueError(
266
+ "physical tensor bytes do not match allocation: "
267
+ f"packed={actual_bytes:,}, charged={charged_bytes:,}"
268
+ )
269
+ selector_bytes = sum(
270
+ tensor.numel() * tensor.element_size()
271
+ for key, tensor in tensors.items()
272
+ if key.endswith(".selector")
273
+ )
274
+ expected_selectors = int(allocation_report.get("selector_bytes", -1))
275
+ if selector_bytes != expected_selectors:
276
+ raise ValueError(
277
+ f"selector bytes {selector_bytes:,} do not match report {expected_selectors:,}"
278
+ )
279
+
280
+ report = {
281
+ "format": FORMAT,
282
+ "canonical_tensors": len(entries),
283
+ "aliases": len(aliases),
284
+ "flat_payload_tensors": len(tensors),
285
+ "tensor_bytes": actual_bytes,
286
+ "selector_bytes": selector_bytes,
287
+ "max_abs_repack_error": max_abs_error,
288
+ "max_repack_rmse": max_rmse,
289
+ "max_abs_scale_storage_error": max_abs_scale_error,
290
+ "max_scale_storage_rmse": max_scale_rmse,
291
+ "worst_tensor": worst_tensor,
292
+ "error_reference": (
293
+ "expanded_fake_quant" if reference_state_dict is not None else "packing_source"
294
+ ),
295
+ "row_block": block,
296
+ "group_size": group_size,
297
+ "scale_normalization": scale_normalization,
298
+ "source_budget": int(allocation_report.get("budget", charged_bytes)),
299
+ "source_margin": int(allocation_report.get("margin_bytes", 0)),
300
+ }
301
+ payload = {
302
+ "format": FORMAT,
303
+ "model": allocation_report.get("model"),
304
+ "state_dict": tensors,
305
+ "entries": entries,
306
+ "aliases": aliases,
307
+ "report": report,
308
+ }
309
+ return payload, report
310
+
311
+
312
+ def unpack_block_adaptive_state(payload: Mapping[str, Any]) -> dict[str, torch.Tensor]:
313
+ """Restore every packed text tensor without loading the base architecture."""
314
+ if payload.get("format") != FORMAT:
315
+ raise ValueError(f"unsupported block-adaptive artifact: {payload.get('format')!r}")
316
+ tensors = payload.get("state_dict")
317
+ entries = payload.get("entries")
318
+ if not isinstance(tensors, dict) or not isinstance(entries, dict):
319
+ raise ValueError("artifact is missing flat state_dict or entries metadata")
320
+ restored: dict[str, torch.Tensor] = {}
321
+ for name, metadata in entries.items():
322
+ kind = metadata.get("kind")
323
+ if kind == "tensor":
324
+ value = unpack_tensor(_load_entry(metadata["entry"], tensors))
325
+ elif kind == "adaptive_rows":
326
+ shape = tuple(int(value) for value in metadata["shape"])
327
+ widths = tensors[metadata["selector_key"]].to(torch.uint8)
328
+ value = torch.empty(shape, dtype=torch.bfloat16)
329
+ for bit_text, group_metadata in metadata["groups"].items():
330
+ bit_width = int(bit_text)
331
+ ids = _row_ids(widths, bit_width, shape[0], int(metadata["block"]))
332
+ if ids.numel() != int(group_metadata["rows"]):
333
+ raise ValueError(f"row count mismatch while restoring {name} at {bit_width} bits")
334
+ rows = unpack_tensor(_load_entry(group_metadata["entry"], tensors))
335
+ value.index_copy_(0, ids, rows.to(value.dtype))
336
+ else:
337
+ raise ValueError(f"unknown entry kind for {name}: {kind!r}")
338
+ restored[name] = value
339
+ for alias, canonical in payload.get("aliases", {}).items():
340
+ if canonical not in restored:
341
+ raise KeyError(f"alias target is absent: {alias} -> {canonical}")
342
+ restored[alias] = restored[canonical]
343
+ return restored
344
+
345
+
346
+ def save_block_adaptive_artifact(payload: Mapping[str, Any], path: str | Path) -> dict[str, Any]:
347
+ output = Path(path)
348
+ if output.exists():
349
+ raise FileExistsError(f"refusing to overwrite artifact: {output}")
350
+ output.parent.mkdir(parents=True, exist_ok=True)
351
+ started = time.time()
352
+ torch.save(dict(payload), output)
353
+ report = dict(payload.get("report", {}))
354
+ report.update(
355
+ {
356
+ "disk_bytes": output.stat().st_size,
357
+ "sha256": sha256_file(output),
358
+ "save_seconds": round(time.time() - started, 3),
359
+ }
360
+ )
361
+ output.with_suffix(output.suffix + ".json").write_text(json.dumps(report, indent=2))
362
+ return report
363
+
364
+
365
+ def load_block_adaptive_artifact(path: str | Path) -> dict[str, Any]:
366
+ payload = torch.load(Path(path), map_location="cpu", weights_only=False)
367
+ if not isinstance(payload, dict) or payload.get("format") != FORMAT:
368
+ raise ValueError(f"unsupported block-adaptive artifact in {path}")
369
+ actual = tensor_payload_bytes(payload.get("state_dict", {}))
370
+ expected = int(payload.get("report", {}).get("tensor_bytes", -1))
371
+ if actual != expected:
372
+ raise ValueError(f"artifact tensor bytes changed: {actual:,} != {expected:,}")
373
+ return payload
374
+
375
+
376
+ def restore_block_adaptive_artifact(
377
+ model_name: str,
378
+ artifact_path: str | Path,
379
+ output_path: str | Path,
380
+ *,
381
+ cache_dir: str | None = None,
382
+ ) -> dict[str, Any]:
383
+ """Expand a physical artifact into the BF16 HF checkpoint used by evaluation."""
384
+ out = Path(output_path)
385
+ if out.exists() and (not out.is_dir() or any(out.iterdir())):
386
+ raise FileExistsError(f"refusing to overwrite restore target: {out}")
387
+ started = time.time()
388
+ payload = load_block_adaptive_artifact(artifact_path)
389
+ restored = unpack_block_adaptive_state(payload)
390
+ model = load_model(
391
+ model_name,
392
+ dtype="bfloat16",
393
+ device_map=None,
394
+ cache_dir=cache_dir,
395
+ multimodal=True,
396
+ )
397
+ targets = model.state_dict()
398
+ with torch.no_grad():
399
+ for name, target in targets.items():
400
+ if EXCLUDED_RE.search(name):
401
+ target.zero_()
402
+ continue
403
+ if name not in restored:
404
+ raise KeyError(f"artifact did not restore text tensor: {name}")
405
+ value = restored[name]
406
+ if tuple(value.shape) != tuple(target.shape):
407
+ raise ValueError(
408
+ f"shape mismatch for {name}: {tuple(value.shape)} != {tuple(target.shape)}"
409
+ )
410
+ target.copy_(value.to(dtype=target.dtype))
411
+ copied = save_checkpoint(model, out, source_model=model_name, cache_dir=cache_dir)
412
+ report = {
413
+ "format": FORMAT,
414
+ "artifact": str(artifact_path),
415
+ "artifact_sha256": sha256_file(artifact_path),
416
+ "tensor_bytes": int(payload["report"]["tensor_bytes"]),
417
+ "restored_text_tensors": len(restored),
418
+ "zeroed_excluded_tensors": sum(1 for name in targets if EXCLUDED_RE.search(name)),
419
+ "auxiliary_files": copied,
420
+ "restore_seconds": round(time.time() - started, 1),
421
+ }
422
+ (out / "block_adaptive_restore.json").write_text(json.dumps(report, indent=2))
423
+ return report
src/eaimath/answers.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Extraction and equivalence checking for free-form math answers.
2
+
3
+ The hidden leaderboard eval is math, so answers are LaTeX expressions inside
4
+ ``\\boxed{...}`` rather than multiple-choice letters. Extraction therefore needs
5
+ real brace matching (``\\boxed{\\frac{1}{2}}`` breaks any naive regex), and
6
+ comparison needs symbolic equivalence (``0.5`` == ``\\frac{1}{2}`` == ``1/2``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from fractions import Fraction
13
+
14
+ BOXED_TOKENS = ("\\boxed", "\\fbox")
15
+
16
+ # Applied in order when falling back to string comparison.
17
+ _STRIP_WRAPPERS = (
18
+ (r"\\left", ""),
19
+ (r"\\right", ""),
20
+ (r"\\!", ""),
21
+ (r"\\,", ""),
22
+ (r"\\;", ""),
23
+ (r"\\ ", " "),
24
+ (r"\\dfrac", r"\\frac"),
25
+ (r"\\tfrac", r"\\frac"),
26
+ (r"\\cdot", "*"),
27
+ (r"\\times", "*"),
28
+ (r"\^\{\\circ\}", ""),
29
+ (r"\^\\circ", ""),
30
+ (r"\\%", ""),
31
+ (r"\\\$", ""),
32
+ )
33
+
34
+
35
+ def extract_boxed(text: str) -> str | None:
36
+ """Return the contents of the LAST ``\\boxed{...}`` in ``text``.
37
+
38
+ Uses brace-depth matching so nested LaTeX survives. Returns ``None`` when no
39
+ boxed expression is present or the braces never close (a truncated
40
+ generation), which the caller should treat as an unparsed answer.
41
+ """
42
+ if not text:
43
+ return None
44
+
45
+ start_idx = -1
46
+ token_len = 0
47
+ for token in BOXED_TOKENS:
48
+ idx = text.rfind(token)
49
+ if idx > start_idx:
50
+ start_idx, token_len = idx, len(token)
51
+ if start_idx == -1:
52
+ return None
53
+
54
+ i = start_idx + token_len
55
+ while i < len(text) and text[i].isspace():
56
+ i += 1
57
+ if i >= len(text):
58
+ return None
59
+
60
+ # `\boxed 42` (no braces) — take the next non-space run.
61
+ if text[i] != "{":
62
+ match = re.match(r"[^\s$\\]+", text[i:])
63
+ return match.group(0) if match else None
64
+
65
+ depth = 0
66
+ content_start = i + 1
67
+ while i < len(text):
68
+ if text[i] == "{":
69
+ depth += 1
70
+ elif text[i] == "}":
71
+ depth -= 1
72
+ if depth == 0:
73
+ return text[content_start:i]
74
+ i += 1
75
+ return None # unclosed brace -> truncated mid-answer
76
+
77
+
78
+ def extract_answer(text: str) -> str | None:
79
+ """Extract a final answer, preferring ``\\boxed{}`` with light fallbacks."""
80
+ boxed = extract_boxed(text)
81
+ if boxed is not None:
82
+ return boxed.strip()
83
+
84
+ # Fallback: an explicit "answer is X" on one of the last few lines.
85
+ tail = [ln.strip() for ln in text.strip().splitlines() if ln.strip()][-5:]
86
+ pattern = re.compile(
87
+ r"(?:final\s+answer|answer)\s*(?:is|:)\s*\$?([^\s$.,]+)", re.IGNORECASE
88
+ )
89
+ for line in reversed(tail):
90
+ match = pattern.search(line)
91
+ if match:
92
+ return match.group(1).strip()
93
+ return None
94
+
95
+
96
+ def normalize(expr: str) -> str:
97
+ """Aggressively normalize a LaTeX answer for string comparison."""
98
+ if expr is None:
99
+ return ""
100
+ out = expr.strip()
101
+ out = re.sub(r"^\$+|\$+$", "", out).strip()
102
+ out = re.sub(r"^\\\[|\\\]$", "", out).strip()
103
+ for pattern, repl in _STRIP_WRAPPERS:
104
+ out = re.sub(pattern, repl, out)
105
+ out = re.sub(r"\\text\{([^}]*)\}", r"\1", out)
106
+ out = re.sub(r"\\mathrm\{([^}]*)\}", r"\1", out)
107
+ # 1,234,567 -> 1234567 (thousands separators only, not tuples)
108
+ if re.fullmatch(r"-?\d{1,3}(,\d{3})+(\.\d+)?", out):
109
+ out = out.replace(",", "")
110
+ out = out.replace(" ", "").rstrip(".")
111
+ # Trailing ".0" and redundant leading "+"
112
+ out = re.sub(r"^\+", "", out)
113
+ if re.fullmatch(r"-?\d+\.0+", out):
114
+ out = out.split(".")[0]
115
+ return out
116
+
117
+
118
+ def _as_number(expr: str) -> float | None:
119
+ """Best-effort numeric value for simple ints, decimals and \\frac{a}{b}."""
120
+ text = normalize(expr)
121
+ try:
122
+ return float(Fraction(text))
123
+ except (ValueError, ZeroDivisionError):
124
+ pass
125
+ frac = re.fullmatch(r"\\frac\{(-?[\d.]+)\}\{(-?[\d.]+)\}", text)
126
+ if frac:
127
+ try:
128
+ denom = float(frac.group(2))
129
+ return float(frac.group(1)) / denom if denom else None
130
+ except (ValueError, ZeroDivisionError):
131
+ return None
132
+ return None
133
+
134
+
135
+ _MATH_VERIFY_STATE: dict[str, object] = {}
136
+
137
+
138
+ def _math_verify():
139
+ """Lazily import math_verify; cache the failure so we only warn once."""
140
+ if "fn" not in _MATH_VERIFY_STATE:
141
+ try:
142
+ from math_verify import parse, verify
143
+
144
+ _MATH_VERIFY_STATE["fn"] = (parse, verify)
145
+ except Exception: # pragma: no cover - depends on optional install
146
+ _MATH_VERIFY_STATE["fn"] = None
147
+ return _MATH_VERIFY_STATE["fn"]
148
+
149
+
150
+ def answers_match(predicted: str | None, gold: str) -> bool:
151
+ """True when ``predicted`` is mathematically equivalent to ``gold``.
152
+
153
+ Tries symbolic equivalence via ``math_verify`` first, then numeric
154
+ comparison, then normalized string equality. Each stage is a superset of
155
+ what the next can catch, so a False here means all three disagreed.
156
+ """
157
+ if predicted is None:
158
+ return False
159
+
160
+ checker = _math_verify()
161
+ if checker is not None:
162
+ parse, verify = checker
163
+ try:
164
+ if verify(parse(f"${gold}$"), parse(f"${predicted}$")):
165
+ return True
166
+ except Exception:
167
+ pass # malformed LaTeX -> fall through to the cheaper checks
168
+
169
+ pred_num, gold_num = _as_number(predicted), _as_number(gold)
170
+ if pred_num is not None and gold_num is not None:
171
+ return abs(pred_num - gold_num) < 1e-6
172
+
173
+ return normalize(predicted) == normalize(gold)
src/eaimath/artifact.py ADDED
@@ -0,0 +1,686 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Submission-shaped packing and restoration for the Round 9 Bucket-B artifact.
2
+
3
+ The calibrated GPTQ checkpoint is an inference diagnostic, not the object the
4
+ course grades: modules skipped by GPTQ are still BF16 in that directory. This
5
+ module dequantizes the diagnostic values once, repacks every text parameter at
6
+ the target mixed precision, stores only selected embedding rows, and restores a
7
+ normal BF16 Hugging Face checkpoint.
8
+
9
+ The artifact deliberately preserves original tokenizer ids. The supplied
10
+ evaluator copies the original tokenizer into the restored directory, so a dense
11
+ id remap would be overwritten and would make prompts invalid.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import lzma
18
+ import re
19
+ import tempfile
20
+ import time
21
+ import zlib
22
+ from pathlib import Path
23
+ from typing import Any, Iterable
24
+
25
+ import numpy as np
26
+ import torch
27
+
28
+ from .embedding_predictor import predict_token_rows
29
+ from .model import DEFAULT_MODEL, load_model, load_tokenizer, save_checkpoint
30
+ from .pack import (
31
+ pack_bits,
32
+ pack_rows,
33
+ pack_tensor,
34
+ state_dict_bytes,
35
+ unpack_bits,
36
+ unpack_adaptive_codebook,
37
+ unpack_rows,
38
+ unpack_tensor,
39
+ )
40
+
41
+ FORMAT = "eaimath-r09-v1"
42
+ ZLIB_MAGIC = b"EAIMATH_ZLIB_V1\n"
43
+ LZMA_MAGIC = b"EAIMATH_LZMA_V1\n"
44
+ EXCLUDED = re.compile(r"visual|vision_tower|(^|\.)mtp\.")
45
+ PROTECTED = re.compile(r"A_log|dt_bias|conv1d|in_proj_a|in_proj_b|norm|bias")
46
+ EMBED = re.compile(r"embed_tokens|lm_head")
47
+ LINEAR_ATTN = re.compile(r"linear_attn")
48
+ FULL_ATTN = re.compile(r"self_attn")
49
+ MLP = re.compile(r"\.mlp\.")
50
+
51
+
52
+ def component_and_bits(
53
+ name: str,
54
+ recipe_bits: dict[str, int] | None = None,
55
+ ) -> tuple[str, int]:
56
+ """Return component and stored width; protected parameters always stay BF16."""
57
+ bits = recipe_bits or {
58
+ "mlp": 3,
59
+ "linear_attn": 3,
60
+ "full_attn": 8,
61
+ "embed": 8,
62
+ }
63
+ if PROTECTED.search(name):
64
+ return "protected", 16
65
+ if EMBED.search(name):
66
+ return "embed", bits["embed"]
67
+ if LINEAR_ATTN.search(name):
68
+ return "linear_attn", bits["linear_attn"]
69
+ if FULL_ATTN.search(name):
70
+ return "full_attn", bits["full_attn"]
71
+ if MLP.search(name):
72
+ return "mlp", bits["mlp"]
73
+ return "other", 16
74
+
75
+
76
+ def _tensor_error(original: torch.Tensor, restored: torch.Tensor) -> tuple[float, float]:
77
+ if original.numel() == 0:
78
+ return 0.0, 0.0
79
+ delta = original.detach().float().cpu() - restored.detach().float().cpu()
80
+ return float(delta.abs().max()), float(delta.square().mean().sqrt())
81
+
82
+
83
+ def read_trace_corpus(
84
+ paths: Iterable[Path],
85
+ ) -> tuple[list[str], list[str], dict[str, int]]:
86
+ """Read verified train-only problems and completions without mixing their roles."""
87
+ problems: list[str] = []
88
+ completions: list[str] = []
89
+ stats = {"rows": 0, "usable": 0, "bad_json": 0, "characters": 0}
90
+ for path in paths:
91
+ if not path.is_file():
92
+ raise FileNotFoundError(f"vocabulary corpus does not exist: {path}")
93
+ with path.open(encoding="utf-8") as handle:
94
+ for line in handle:
95
+ stats["rows"] += 1
96
+ try:
97
+ row = json.loads(line)
98
+ except json.JSONDecodeError:
99
+ stats["bad_json"] += 1
100
+ continue
101
+ problem = str(row.get("problem") or "").strip()
102
+ completion = str(row.get("completion") or row.get("response") or "").strip()
103
+ if not problem or not completion:
104
+ continue
105
+ if row.get("correct") is False or row.get("finished") is False:
106
+ continue
107
+ problems.append(problem)
108
+ completions.append(completion)
109
+ stats["usable"] += 1
110
+ stats["characters"] += len(problem) + len(completion)
111
+ if stats["usable"] < 128 or stats["characters"] < 1_000_000:
112
+ raise ValueError(
113
+ "vocabulary ranking needs at least 128 verified traces and 1M characters; "
114
+ f"found {stats['usable']} traces / {stats['characters']:,} characters"
115
+ )
116
+ return problems, completions, stats
117
+
118
+
119
+ def read_trace_texts(paths: Iterable[Path]) -> tuple[list[str], dict[str, int]]:
120
+ """Read disjoint training problems and verified completions for vocab ranking."""
121
+ problems, completions, stats = read_trace_corpus(paths)
122
+ return problems + completions, stats
123
+
124
+
125
+ def _decode_gptq_qzeros(module) -> torch.Tensor:
126
+ """Decode GPTQ v2 zero points without relying on a kernel-private helper.
127
+
128
+ Three-bit GPTQ does not have an integer ``pack_factor``: 32 values span
129
+ three 32-bit words, with values 10 and 21 crossing word boundaries.
130
+ GPTQModel's TorchLinear streaming helper uses the ordinary pack-factor path
131
+ and therefore cannot decode this layout. Its reference dequantizer has the
132
+ special case reproduced here.
133
+ """
134
+ qzeros = module.qzeros.detach().cpu()
135
+ bits = int(module.bits)
136
+ word_bits = int(module.pack_dtype_bits)
137
+ maxq = int(module.maxq)
138
+
139
+ if bits in {2, 4, 8}:
140
+ shifts = torch.arange(0, word_bits, bits, dtype=torch.int32).reshape(1, 1, -1)
141
+ zeros = torch.bitwise_right_shift(qzeros.unsqueeze(2), shifts)
142
+ zeros = torch.bitwise_and(zeros, maxq)
143
+ elif bits == 3:
144
+ if word_bits != 32 or qzeros.shape[1] % 3:
145
+ raise RuntimeError(
146
+ "unsupported 3-bit GPTQ zero layout: "
147
+ f"word_bits={word_bits}, qzeros={tuple(qzeros.shape)}"
148
+ )
149
+ shifts = torch.tensor(
150
+ [
151
+ [0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 0],
152
+ [0, 1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31],
153
+ [0, 2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 0],
154
+ ],
155
+ dtype=torch.int32,
156
+ ).reshape(1, 1, 3, 12)
157
+ zeros = qzeros.reshape(qzeros.shape[0], qzeros.shape[1] // 3, 3, 1)
158
+ zeros = torch.bitwise_right_shift(zeros.expand(-1, -1, -1, 12), shifts)
159
+ zeros[:, :, 0, 10] = (zeros[:, :, 0, 10] & 0x3) | (
160
+ (zeros[:, :, 1, 0] << 2) & 0x4
161
+ )
162
+ zeros[:, :, 1, 11] = (zeros[:, :, 1, 11] & 0x1) | (
163
+ (zeros[:, :, 2, 0] << 1) & 0x6
164
+ )
165
+ zeros = torch.bitwise_and(zeros, maxq)
166
+ zeros = torch.cat(
167
+ [zeros[:, :, 0, :11], zeros[:, :, 1, 1:12], zeros[:, :, 2, 1:11]],
168
+ dim=2,
169
+ )
170
+ else:
171
+ raise RuntimeError(f"unsupported GPTQ zero-point width: {bits}")
172
+
173
+ expected = int(module.scales.numel())
174
+ if zeros.numel() != expected:
175
+ raise RuntimeError(
176
+ "decoded GPTQ zero-point count does not match scales: "
177
+ f"zeros={zeros.numel()}, scales={expected}"
178
+ )
179
+ return zeros.reshape(module.scales.shape).to(torch.int64)
180
+
181
+
182
+ def _capture_gptq_entry(module) -> tuple[dict[str, Any], float, torch.Tensor]:
183
+ """Store GPTQ's exact codes/scales instead of re-quantizing its values."""
184
+ dequantized = module.dequantize_weight().detach().cpu()
185
+ scales = module.scales.detach().cpu().contiguous()
186
+ g_idx = module.g_idx.detach().cpu().long()
187
+ expected_shape = (module.in_features, module.out_features)
188
+ if tuple(dequantized.shape) != expected_shape or g_idx.numel() != module.in_features:
189
+ raise RuntimeError(
190
+ "GPTQ auto-padding was not removed by dequantization: "
191
+ f"weight={tuple(dequantized.shape)}, g_idx={g_idx.numel()}, "
192
+ f"expected={expected_shape}"
193
+ )
194
+ zeros = _decode_gptq_qzeros(module)
195
+ scale_rows = scales.float().index_select(0, g_idx)
196
+ zero_rows = zeros.index_select(0, g_idx)
197
+ codes = torch.round(dequantized.float() / scale_rows + zero_rows.float()).to(torch.int64)
198
+ codes.clamp_(0, 2**module.bits - 1)
199
+ reconstructed = scales.index_select(0, g_idx) * (
200
+ codes - zero_rows
201
+ ).to(scales.dtype)
202
+ target = dequantized.to(torch.bfloat16)
203
+ max_error = float((reconstructed.to(torch.bfloat16) - target).float().abs().max())
204
+
205
+ sequential = torch.div(
206
+ torch.arange(module.in_features, dtype=torch.int64),
207
+ module.group_size,
208
+ rounding_mode="floor",
209
+ ).clamp_max(scales.shape[0] - 1)
210
+ zero_flat = zeros.reshape(-1)
211
+ uniform_zero = bool(torch.all(zero_flat == zero_flat[0]))
212
+ entry: dict[str, Any] = {
213
+ "kind": "gptq_codes",
214
+ "packed": torch.from_numpy(pack_bits(codes.numpy().astype(np.uint32), module.bits)),
215
+ "scale": scales,
216
+ "shape": (module.out_features, module.in_features),
217
+ "code_shape": tuple(codes.shape),
218
+ "bits": int(module.bits),
219
+ "group": int(module.group_size),
220
+ "sym": bool(module.sym),
221
+ "dtype": "torch.bfloat16",
222
+ "zero_value": int(zero_flat[0]) if uniform_zero else None,
223
+ }
224
+ if not uniform_zero:
225
+ entry["zero_packed"] = torch.from_numpy(
226
+ pack_bits(zeros.numpy().astype(np.uint32), module.bits)
227
+ )
228
+ entry["zero_shape"] = tuple(zeros.shape)
229
+ if not torch.equal(g_idx, sequential):
230
+ entry["g_idx"] = g_idx.to(torch.int32)
231
+ return entry, max_error, target.T.contiguous()
232
+
233
+
234
+ def unpack_gptq_entry(entry: dict[str, Any]) -> torch.Tensor:
235
+ """Expand an exact code/scale GPTQ entry to its BF16 linear weight."""
236
+ in_features, out_features = tuple(entry["code_shape"])
237
+ count = in_features * out_features
238
+ codes = unpack_bits(entry["packed"].numpy(), entry["bits"], count)
239
+ codes_tensor = torch.from_numpy(codes.astype(np.int16)).reshape(in_features, out_features)
240
+ scales = entry["scale"]
241
+ if entry.get("g_idx") is None:
242
+ g_idx = torch.div(
243
+ torch.arange(in_features, dtype=torch.int64),
244
+ entry["group"],
245
+ rounding_mode="floor",
246
+ ).clamp_max(scales.shape[0] - 1)
247
+ else:
248
+ g_idx = entry["g_idx"].long()
249
+ if entry.get("zero_value") is not None:
250
+ zero_rows = int(entry["zero_value"])
251
+ else:
252
+ zero_shape = tuple(entry["zero_shape"])
253
+ zero_count = int(np.prod(zero_shape))
254
+ zeros = unpack_bits(entry["zero_packed"].numpy(), entry["bits"], zero_count)
255
+ zero_tensor = torch.from_numpy(zeros.astype(np.int16)).reshape(zero_shape)
256
+ zero_rows = zero_tensor.index_select(0, g_idx)
257
+ values = scales.index_select(0, g_idx) * (
258
+ codes_tensor - zero_rows
259
+ ).to(scales.dtype)
260
+ return values.T.contiguous().to(torch.bfloat16)
261
+
262
+
263
+ def dequantize_gptq_model(checkpoint: str | Path):
264
+ """Capture exact GPTQ entries, then replace linears for a normal state dict."""
265
+ import torch.nn as nn
266
+ from gptqmodel import GPTQModel
267
+ from gptqmodel.nn_modules.qlinear import BaseQuantLinear
268
+ from gptqmodel.nn_modules.qlinear.torch import TorchLinear
269
+ from gptqmodel.utils.backend import BACKEND
270
+
271
+ loaded = GPTQModel.load(
272
+ str(checkpoint),
273
+ backend=BACKEND.GPTQ_TORCH,
274
+ device="cpu",
275
+ trust_remote_code=True,
276
+ )
277
+ model = loaded.model
278
+ named = dict(model.named_modules())
279
+ replaced = 0
280
+ exact_weights: dict[str, dict[str, Any]] = {}
281
+ capture_error = 0.0
282
+ for name, module in list(model.named_modules()):
283
+ if isinstance(module, BaseQuantLinear) and not isinstance(module, TorchLinear):
284
+ raise TypeError(
285
+ f"{name} loaded as {type(module).__name__}; the submission packer "
286
+ "requires BACKEND.GPTQ_TORCH"
287
+ )
288
+ if not isinstance(module, TorchLinear):
289
+ continue
290
+ exact_entry, error, weight = _capture_gptq_entry(module)
291
+ exact_weights[f"{name}.weight"] = exact_entry
292
+ capture_error = max(capture_error, error)
293
+ replacement = nn.Linear(
294
+ module.in_features,
295
+ module.out_features,
296
+ bias=module.bias is not None,
297
+ device="cpu",
298
+ dtype=torch.bfloat16,
299
+ )
300
+ replacement.weight = nn.Parameter(weight)
301
+ if module.bias is not None:
302
+ replacement.bias = nn.Parameter(module.bias.detach().cpu().to(torch.bfloat16))
303
+ parent_name, child_name = name.rsplit(".", 1)
304
+ setattr(named[parent_name], child_name, replacement)
305
+ named[name] = replacement
306
+ replaced += 1
307
+ if replaced == 0:
308
+ raise RuntimeError("no GPTQ TorchLinear modules were found in the source checkpoint")
309
+ return loaded, model, replaced, exact_weights, capture_error
310
+
311
+
312
+ def pack_model_state(
313
+ state_dict: dict[str, torch.Tensor],
314
+ keep_ids: list[int],
315
+ *,
316
+ group_size: int = 128,
317
+ exact_entries: dict[str, dict[str, Any]] | None = None,
318
+ recipe_bits: dict[str, int] | None = None,
319
+ recipe_name: str = "m3l3a8e8",
320
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
321
+ """Pack unique text tensors and validate every local round trip."""
322
+ packed: dict[str, dict[str, Any]] = {}
323
+ aliases: dict[str, str] = {}
324
+ seen: dict[int, str] = {}
325
+ groups: dict[str, dict[str, float | int]] = {}
326
+ max_abs_error = 0.0
327
+ max_rmse = 0.0
328
+ worst_tensor = ""
329
+ keep_tensor = torch.tensor(keep_ids, dtype=torch.int64)
330
+ exact_entries = exact_entries or {}
331
+
332
+ for index, (name, tensor) in enumerate(state_dict.items(), 1):
333
+ if EXCLUDED.search(name) or not isinstance(tensor, torch.Tensor):
334
+ continue
335
+ pointer = tensor.data_ptr()
336
+ if pointer and pointer in seen:
337
+ aliases[name] = seen[pointer]
338
+ continue
339
+ if pointer:
340
+ seen[pointer] = name
341
+
342
+ component, bits = component_and_bits(name, recipe_bits)
343
+ if name in exact_entries:
344
+ entry = exact_entries[name]
345
+ restored = unpack_gptq_entry(entry)
346
+ original = tensor.detach().cpu()
347
+ elif component == "embed" and tensor.ndim == 2 and tensor.shape[0] > len(keep_ids):
348
+ entry = pack_rows(tensor, keep_tensor, bits, group_size=group_size)
349
+ restored = unpack_rows(entry, fill="zero").index_select(0, keep_tensor)
350
+ original = tensor.detach().cpu().index_select(0, keep_tensor)
351
+ else:
352
+ entry = pack_tensor(tensor, bits, group_size=group_size)
353
+ restored = unpack_tensor(entry)
354
+ original = tensor.detach().cpu()
355
+ abs_error, rmse = _tensor_error(original, restored)
356
+ if abs_error > max_abs_error:
357
+ max_abs_error = abs_error
358
+ worst_tensor = name
359
+ max_rmse = max(max_rmse, rmse)
360
+ packed[name] = entry
361
+
362
+ group = groups.setdefault(
363
+ component,
364
+ {"tensors": 0, "source_parameters": 0, "stored_bytes": 0, "bits": bits},
365
+ )
366
+ group["tensors"] = int(group["tensors"]) + 1
367
+ group["source_parameters"] = int(group["source_parameters"]) + tensor.numel()
368
+ group["stored_bytes"] = int(group["stored_bytes"]) + state_dict_bytes({name: entry})
369
+ if index % 100 == 0:
370
+ print(f" packed through state tensor {index}/{len(state_dict)}: {name}")
371
+
372
+ if not packed:
373
+ raise RuntimeError("source state dict yielded no text tensors")
374
+ if not any(entry["kind"].endswith("_rows") for entry in packed.values()):
375
+ raise RuntimeError("embedding row packing was not applied")
376
+ missing_exact = sorted(set(exact_entries) - set(packed))
377
+ if missing_exact:
378
+ raise RuntimeError(f"{len(missing_exact)} captured GPTQ weights were not packed: {missing_exact[:3]}")
379
+
380
+ report = {
381
+ "format": FORMAT,
382
+ "recipe": recipe_name,
383
+ "recipe_bits": recipe_bits,
384
+ "group_size": group_size,
385
+ "vocab_size": len(keep_ids),
386
+ "packed_tensors": len(packed),
387
+ "aliases": len(aliases),
388
+ "tensor_bytes": state_dict_bytes(packed),
389
+ "groups": groups,
390
+ "max_abs_repack_error": max_abs_error,
391
+ "max_repack_rmse": max_rmse,
392
+ "worst_tensor": worst_tensor,
393
+ }
394
+ payload = {
395
+ "format": FORMAT,
396
+ "recipe": recipe_name,
397
+ "group_size": group_size,
398
+ "keep_ids": keep_tensor.to(torch.int32),
399
+ "state_dict": packed,
400
+ "aliases": aliases,
401
+ "report": report,
402
+ }
403
+ return payload, report
404
+
405
+
406
+ def add_input_embedding_extension(
407
+ payload: dict[str, Any],
408
+ source_weight: torch.Tensor,
409
+ input_ids: Iterable[int],
410
+ *,
411
+ bits: int,
412
+ group_size: int = 128,
413
+ ) -> dict[str, Any]:
414
+ """Store extra input-only rows while retaining the smaller output allow-set."""
415
+ output_ids = set(int(value) for value in payload["keep_ids"].tolist())
416
+ input_set = set(int(value) for value in input_ids)
417
+ if not output_ids.issubset(input_set):
418
+ raise ValueError("input vocabulary must contain every output token id")
419
+ extra = sorted(input_set - output_ids)
420
+ if not extra:
421
+ raise ValueError("input extension has no rows beyond the output vocabulary")
422
+ row_names = [
423
+ name
424
+ for name, entry in payload["state_dict"].items()
425
+ if entry["kind"] in {"quant_rows", "raw_rows"}
426
+ ]
427
+ if len(row_names) != 1:
428
+ raise ValueError(f"expected one canonical row-packed embedding, found {row_names}")
429
+ entry = pack_rows(
430
+ source_weight,
431
+ torch.tensor(extra, dtype=torch.int64),
432
+ bits,
433
+ group_size=group_size,
434
+ )
435
+ extension = {
436
+ "target": row_names[0],
437
+ "entry": entry,
438
+ "bits": bits,
439
+ "output_vocab_size": len(output_ids),
440
+ "input_vocab_size": len(input_set),
441
+ "extra_rows": len(extra),
442
+ }
443
+ payload["input_embedding_extension"] = extension
444
+ extension_bytes = state_dict_bytes({"input_embedding_extension": entry})
445
+ payload["report"]["tensor_bytes"] += extension_bytes
446
+ payload["report"]["output_vocab_size"] = len(output_ids)
447
+ payload["report"]["input_vocab_size"] = len(input_set)
448
+ payload["report"]["input_extension_bits"] = bits
449
+ payload["report"]["input_extension_rows"] = len(extra)
450
+ payload["report"]["input_extension_bytes"] = extension_bytes
451
+ return extension
452
+
453
+
454
+ def save_artifact(
455
+ payload: dict[str, Any],
456
+ output_path: Path,
457
+ *,
458
+ lossless_codec: str = "none",
459
+ zlib_level: int = 6,
460
+ lzma_preset: int = 6,
461
+ ) -> dict[str, Any]:
462
+ output_path.parent.mkdir(parents=True, exist_ok=True)
463
+ if output_path.exists():
464
+ raise FileExistsError(f"refusing to overwrite existing artifact: {output_path}")
465
+ if lossless_codec not in {"none", "zlib", "lzma"}:
466
+ raise ValueError(f"unsupported lossless artifact codec: {lossless_codec}")
467
+ raw_disk_bytes = None
468
+ if lossless_codec == "none":
469
+ torch.save(payload, output_path)
470
+ else:
471
+ temporary_path: Path | None = None
472
+ try:
473
+ with tempfile.NamedTemporaryFile(
474
+ dir=output_path.parent,
475
+ prefix=f".{output_path.name}.",
476
+ suffix=".raw",
477
+ delete=False,
478
+ ) as handle:
479
+ temporary_path = Path(handle.name)
480
+ torch.save(payload, temporary_path)
481
+ raw_disk_bytes = temporary_path.stat().st_size
482
+ compressor = (
483
+ zlib.compressobj(level=zlib_level)
484
+ if lossless_codec == "zlib"
485
+ else lzma.LZMACompressor(preset=lzma_preset)
486
+ )
487
+ magic = ZLIB_MAGIC if lossless_codec == "zlib" else LZMA_MAGIC
488
+ with temporary_path.open("rb") as source, output_path.open("xb") as target:
489
+ target.write(magic)
490
+ while block := source.read(8 * 1024 * 1024):
491
+ target.write(compressor.compress(block))
492
+ target.write(compressor.flush())
493
+ except Exception:
494
+ if output_path.exists():
495
+ output_path.unlink()
496
+ raise
497
+ finally:
498
+ if temporary_path is not None and temporary_path.exists():
499
+ temporary_path.unlink()
500
+ disk_bytes = output_path.stat().st_size
501
+ report = {
502
+ **payload["report"],
503
+ "artifact_path": str(output_path),
504
+ "disk_bytes": disk_bytes,
505
+ "lossless_codec": lossless_codec,
506
+ }
507
+ if raw_disk_bytes is not None:
508
+ report.update(
509
+ {
510
+ "raw_artifact_bytes": raw_disk_bytes,
511
+ "lossless_savings_bytes": raw_disk_bytes - disk_bytes,
512
+ "codec_level": zlib_level if lossless_codec == "zlib" else lzma_preset,
513
+ }
514
+ )
515
+ if lossless_codec == "zlib":
516
+ report["zlib_level"] = zlib_level
517
+ else:
518
+ report["lzma_preset"] = lzma_preset
519
+ output_path.with_suffix(output_path.suffix + ".json").write_text(json.dumps(report, indent=2))
520
+ return report
521
+
522
+
523
+ def load_artifact(path: str | Path) -> dict[str, Any]:
524
+ artifact = Path(path)
525
+ with artifact.open("rb") as source:
526
+ magic = source.read(len(ZLIB_MAGIC))
527
+ if magic in {ZLIB_MAGIC, LZMA_MAGIC}:
528
+ decompressor = zlib.decompressobj() if magic == ZLIB_MAGIC else lzma.LZMADecompressor()
529
+ # Keep the restored stream beside the artifact so a multi-GB load
530
+ # cannot unexpectedly exhaust a small system /tmp partition.
531
+ with tempfile.TemporaryFile(dir=artifact.parent) as restored:
532
+ while block := source.read(8 * 1024 * 1024):
533
+ restored.write(decompressor.decompress(block))
534
+ if magic == ZLIB_MAGIC:
535
+ restored.write(decompressor.flush())
536
+ if not decompressor.eof:
537
+ codec = "zlib" if magic == ZLIB_MAGIC else "lzma"
538
+ raise ValueError(f"truncated {codec} artifact: {path}")
539
+ restored.seek(0)
540
+ payload = torch.load(restored, map_location="cpu", weights_only=False)
541
+ else:
542
+ source.seek(0)
543
+ payload = torch.load(source, map_location="cpu", weights_only=False)
544
+ if not isinstance(payload, dict) or payload.get("format") != FORMAT:
545
+ raise ValueError(f"unsupported artifact format in {path}")
546
+ return payload
547
+
548
+
549
+ def restore_artifact(
550
+ model_name: str,
551
+ artifact_path: str | Path,
552
+ output_path: str | Path,
553
+ *,
554
+ embedding_fill: str = "zero",
555
+ cache_dir: str | None = None,
556
+ ) -> dict[str, Any]:
557
+ """Expand the artifact into the evaluator's required full BF16 checkpoint."""
558
+ if embedding_fill not in {"zero", "mean", "token", "base"}:
559
+ raise ValueError("embedding_fill must be zero, mean, token, or base")
560
+ out = Path(output_path)
561
+ if out.exists():
562
+ if not out.is_dir() or any(out.iterdir()):
563
+ raise FileExistsError(f"refusing to overwrite restore target: {out}")
564
+
565
+ payload = load_artifact(artifact_path)
566
+ started = time.time()
567
+ model = load_model(
568
+ model_name,
569
+ dtype="bfloat16",
570
+ device_map=None,
571
+ cache_dir=cache_dir,
572
+ multimodal=True,
573
+ )
574
+ targets = model.state_dict()
575
+ restored_names: set[str] = set()
576
+ token_prediction = None
577
+ if embedding_fill == "token":
578
+ predictor = payload.get("embedding_predictor")
579
+ if predictor is None:
580
+ raise ValueError("artifact has no token-string embedding predictor")
581
+ tokenizer = load_tokenizer(model_name, cache_dir=cache_dir)
582
+ token_prediction = predict_token_rows(predictor, tokenizer)
583
+
584
+ with torch.no_grad():
585
+ for name, entry in payload["state_dict"].items():
586
+ if name not in targets:
587
+ raise KeyError(f"artifact tensor is absent from base architecture: {name}")
588
+ if entry["kind"] == "gptq_codes":
589
+ value = unpack_gptq_entry(entry)
590
+ elif entry["kind"] == "adaptive_codebook":
591
+ value = unpack_adaptive_codebook(entry)
592
+ elif entry["kind"] in {"quant_rows", "raw_rows"}:
593
+ if embedding_fill == "base":
594
+ selected_entry = {
595
+ **entry,
596
+ "kind": "quant" if entry["kind"] == "quant_rows" else "raw",
597
+ }
598
+ selected = unpack_tensor(selected_entry)
599
+ value = targets[name].detach().cpu().clone()
600
+ value.index_copy_(0, entry["row_ids"].long(), selected)
601
+ elif embedding_fill == "token":
602
+ if token_prediction is None or tuple(token_prediction.shape) != tuple(
603
+ entry["full_shape"]
604
+ ):
605
+ raise RuntimeError(
606
+ "token prediction shape does not match row-packed embedding: "
607
+ f"prediction={None if token_prediction is None else tuple(token_prediction.shape)}, "
608
+ f"expected={tuple(entry['full_shape'])}"
609
+ )
610
+ selected_entry = {
611
+ **entry,
612
+ "kind": "quant" if entry["kind"] == "quant_rows" else "raw",
613
+ }
614
+ selected = unpack_tensor(selected_entry)
615
+ value = token_prediction.clone()
616
+ value.index_copy_(0, entry["row_ids"].long(), selected)
617
+ else:
618
+ value = unpack_rows(entry, fill=embedding_fill)
619
+ else:
620
+ value = unpack_tensor(entry)
621
+ extension = payload.get("input_embedding_extension")
622
+ if extension is not None and extension.get("target") == name:
623
+ extension_entry = extension["entry"]
624
+ dense_entry = {
625
+ **extension_entry,
626
+ "kind": "quant"
627
+ if extension_entry["kind"] == "quant_rows"
628
+ else "raw",
629
+ }
630
+ extra_values = unpack_tensor(dense_entry)
631
+ value.index_copy_(0, extension_entry["row_ids"].long(), extra_values)
632
+ targets[name].copy_(value.to(dtype=targets[name].dtype))
633
+ restored_names.add(name)
634
+
635
+ for alias, canonical in payload["aliases"].items():
636
+ if alias not in targets or canonical not in targets:
637
+ raise KeyError(f"artifact alias cannot be resolved: {alias} -> {canonical}")
638
+ if targets[alias].data_ptr() != targets[canonical].data_ptr():
639
+ targets[alias].copy_(targets[canonical])
640
+ restored_names.add(alias)
641
+
642
+ missing_text = [
643
+ name
644
+ for name in targets
645
+ if not EXCLUDED.search(name) and name not in restored_names
646
+ ]
647
+ if missing_text:
648
+ preview = ", ".join(missing_text[:8])
649
+ raise RuntimeError(f"artifact did not restore {len(missing_text)} text tensors: {preview}")
650
+
651
+ copied = save_checkpoint(model, out, source_model=model_name, cache_dir=cache_dir)
652
+ keep = set(int(token_id) for token_id in payload["keep_ids"].tolist())
653
+ original_vocab = max(
654
+ int(entry["full_shape"][0])
655
+ for entry in payload["state_dict"].values()
656
+ if entry["kind"] in {"quant_rows", "raw_rows"}
657
+ )
658
+ suppressed = [token_id for token_id in range(original_vocab) if token_id not in keep]
659
+ try:
660
+ generation_config = model.generation_config
661
+ generation_config.suppress_tokens = suppressed
662
+ generation_config.save_pretrained(out)
663
+ except Exception as exc: # noqa: BLE001 - explicit manifest still records the failure
664
+ raise RuntimeError(f"could not save suppress_tokens generation config: {exc}") from exc
665
+
666
+ report = {
667
+ "format": FORMAT,
668
+ "artifact": str(artifact_path),
669
+ "model_name": model_name,
670
+ "embedding_fill": embedding_fill,
671
+ "submission_valid": embedding_fill in {"zero", "mean", "token"},
672
+ "restored_text_tensors": len(restored_names),
673
+ "suppressed_tokens": len(suppressed),
674
+ "kept_tokens": len(keep),
675
+ "input_vocab_size": payload.get("report", {}).get("input_vocab_size", len(keep)),
676
+ "auxiliary_files": copied,
677
+ "restore_seconds": round(time.time() - started, 1),
678
+ }
679
+ (out / "artifact_restore.json").write_text(json.dumps(report, indent=2))
680
+ (out / "keep_ids.json").write_text(json.dumps(sorted(keep)))
681
+ return report
682
+
683
+
684
+ def default_model() -> str:
685
+ """Small indirection retained for the standalone submission wrapper."""
686
+ return DEFAULT_MODEL
src/eaimath/artifact_refine.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Physical code-grid refinement helpers for the Bucket-B artifact."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import math
7
+ import re
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ import torch
12
+
13
+ from .artifact import unpack_gptq_entry
14
+ from .pack import pack_bits, unpack_bits, unpack_tensor
15
+
16
+ MLP = re.compile(r"\.mlp\.")
17
+ GDN = re.compile(r"linear_attn")
18
+ FULL_ATTN = re.compile(r"self_attn")
19
+
20
+
21
+ def resolved_recipe_from_report(report: dict[str, Any]) -> str:
22
+ """Recover the exact payload recipe from a physical artifact sidecar.
23
+
24
+ Early Round-21 sidecars inherited the base report's recipe even though the
25
+ payload recipe correctly included its refinement suffix. The refinement
26
+ fields are complete, so existing artifacts can be validated without being
27
+ rebuilt or decompressed an extra time.
28
+ """
29
+ recipe = str(report.get("recipe") or "")
30
+ refinement = report.get("refinement")
31
+ if refinement == "frozen_grid_adapter_projection_v1":
32
+ if "+grid-" in recipe:
33
+ return recipe
34
+ profile = report.get("refinement_profile")
35
+ alpha = report.get("refinement_alpha")
36
+ if not recipe or profile not in {"all", "attention", "gdn"} or alpha is None:
37
+ raise ValueError("incomplete frozen-grid recipe metadata")
38
+ return f"{recipe}+grid-{profile}-a{float(alpha):g}"
39
+ if refinement == "dual_adaptive_codebook_v1":
40
+ if "+aaac-" in recipe:
41
+ return recipe
42
+ mode = report.get("refinement_mode")
43
+ if not recipe or mode not in {"activation", "weight"}:
44
+ raise ValueError("incomplete adaptive-codebook recipe metadata")
45
+ return f"{recipe}+aaac-gdn-{mode}"
46
+ if refinement == "stochastic_grid_adapter_projection_v1":
47
+ if "+stochastic-" in recipe:
48
+ return recipe
49
+ profile = report.get("refinement_profile")
50
+ gain = report.get("refinement_gain")
51
+ seed = report.get("refinement_seed")
52
+ if not recipe or profile not in {"all", "attention", "gdn"} or gain is None or seed is None:
53
+ raise ValueError("incomplete stochastic-grid recipe metadata")
54
+ return f"{recipe}+stochastic-{profile}-g{float(gain):g}-s{int(seed)}"
55
+ if not recipe:
56
+ raise ValueError("artifact sidecar has no recipe")
57
+ return recipe
58
+
59
+
60
+ def component(name: str) -> str | None:
61
+ if MLP.search(name):
62
+ return "mlp"
63
+ if GDN.search(name):
64
+ return "gdn"
65
+ if FULL_ATTN.search(name):
66
+ return "full_attn"
67
+ return None
68
+
69
+
70
+ def profile_matches(name: str, profile: str) -> bool:
71
+ value = component(name)
72
+ if profile == "all":
73
+ return value is not None
74
+ if profile == "attention":
75
+ return value in {"gdn", "full_attn"}
76
+ if profile == "gdn":
77
+ return value == "gdn"
78
+ raise ValueError(f"unknown projection profile: {profile}")
79
+
80
+
81
+ def _gptq_codes_and_zeros(entry: dict[str, Any]) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
82
+ in_features, out_features = tuple(entry["code_shape"])
83
+ count = in_features * out_features
84
+ codes = unpack_bits(entry["packed"].numpy(), entry["bits"], count)
85
+ codes_tensor = torch.from_numpy(codes.astype(np.int64)).reshape(in_features, out_features)
86
+ scales = entry["scale"].float()
87
+ if entry.get("g_idx") is None:
88
+ g_idx = torch.div(
89
+ torch.arange(in_features, dtype=torch.int64),
90
+ int(entry["group"]),
91
+ rounding_mode="floor",
92
+ ).clamp_max(scales.shape[0] - 1)
93
+ else:
94
+ g_idx = entry["g_idx"].long()
95
+ if entry.get("zero_value") is not None:
96
+ zeros = torch.full_like(scales, int(entry["zero_value"]), dtype=torch.int64)
97
+ else:
98
+ zero_shape = tuple(entry["zero_shape"])
99
+ zero_count = int(np.prod(zero_shape))
100
+ values = unpack_bits(entry["zero_packed"].numpy(), entry["bits"], zero_count)
101
+ zeros = torch.from_numpy(values.astype(np.int64)).reshape(zero_shape)
102
+ return codes_tensor, scales.index_select(0, g_idx), zeros.index_select(0, g_idx)
103
+
104
+
105
+ def project_gptq_entry(
106
+ entry: dict[str, Any], adapted: torch.Tensor, alpha: float
107
+ ) -> tuple[dict[str, Any], dict[str, float | int]]:
108
+ base = unpack_gptq_entry(entry).float()
109
+ target = base + alpha * (adapted.detach().float().cpu() - base)
110
+ old_codes, scale_rows, zero_rows = _gptq_codes_and_zeros(entry)
111
+ maxq = 2 ** int(entry["bits"]) - 1
112
+ new_codes = torch.round(target.T / scale_rows.clamp_min(1e-12) + zero_rows).clamp(0, maxq)
113
+ new_codes = new_codes.to(torch.int64)
114
+ projected = (scale_rows * (new_codes - zero_rows).float()).T
115
+ changed = new_codes.ne(old_codes)
116
+ updated = copy.copy(entry)
117
+ updated["packed"] = torch.from_numpy(
118
+ pack_bits(new_codes.numpy().astype(np.uint32), int(entry["bits"]))
119
+ )
120
+ return updated, {
121
+ "weights": int(new_codes.numel()),
122
+ "changed_codes": int(changed.sum()),
123
+ "target_delta_rmse": float((target - base).square().mean().sqrt()),
124
+ "projection_rmse": float((target - projected).square().mean().sqrt()),
125
+ }
126
+
127
+
128
+ def project_quant_entry(
129
+ entry: dict[str, Any], adapted: torch.Tensor, alpha: float
130
+ ) -> tuple[dict[str, Any], dict[str, float | int]]:
131
+ base = unpack_tensor(entry).float()
132
+ target = base + alpha * (adapted.detach().float().cpu() - base)
133
+ bits, group = int(entry["bits"]), int(entry["group"])
134
+ count = int(np.prod(entry["shape"]))
135
+ old = unpack_bits(entry["packed"].numpy(), bits, count)
136
+ old_codes = torch.from_numpy(old.astype(np.int64)).reshape(-1, group)
137
+ scale = entry["scale"].float().reshape(-1, 1)
138
+ scale = scale * math.ldexp(1.0, -int(entry.get("scale_exponent", 0)))
139
+ flat = target.reshape(-1, group)
140
+ if entry["symmetric"]:
141
+ qmax = 2 ** (bits - 1) - 1
142
+ new_codes = torch.round(flat / scale.clamp_min(1e-12)) + qmax + 1
143
+ new_codes = new_codes.clamp(0, 2**bits - 1).to(torch.int64)
144
+ projected = (new_codes - qmax - 1).float() * scale
145
+ else:
146
+ zero = entry["zero"].float().reshape(-1, 1)
147
+ new_codes = (torch.round(flat / scale.clamp_min(1e-12)) + zero).clamp(0, 2**bits - 1)
148
+ new_codes = new_codes.to(torch.int64)
149
+ projected = (new_codes.float() - zero) * scale
150
+ updated = copy.copy(entry)
151
+ updated["packed"] = torch.from_numpy(pack_bits(new_codes.numpy().astype(np.uint32), bits))
152
+ return updated, {
153
+ "weights": int(new_codes.numel()),
154
+ "changed_codes": int(new_codes.ne(old_codes).sum()),
155
+ "target_delta_rmse": float((target - base).square().mean().sqrt()),
156
+ "projection_rmse": float((target - projected.reshape_as(target)).square().mean().sqrt()),
157
+ }
158
+
159
+
160
+ def project_entry(
161
+ entry: dict[str, Any], adapted: torch.Tensor, alpha: float
162
+ ) -> tuple[dict[str, Any], dict[str, float | int]]:
163
+ if entry.get("kind") == "gptq_codes":
164
+ return project_gptq_entry(entry, adapted, alpha)
165
+ if entry.get("kind") == "quant":
166
+ return project_quant_entry(entry, adapted, alpha)
167
+ raise ValueError(f"entry kind cannot be grid-projected: {entry.get('kind')!r}")
168
+
169
+
170
+ def _stochastic_integer_round(desired: torch.Tensor, *, generator: torch.Generator) -> torch.Tensor:
171
+ """Unbiased stochastic rounding to integers with a deterministic generator."""
172
+ lower = torch.floor(desired)
173
+ probability = desired - lower
174
+ draw = torch.rand(desired.shape, generator=generator, dtype=torch.float32)
175
+ return lower + draw.lt(probability)
176
+
177
+
178
+ def stochastic_project_gptq_entry(
179
+ entry: dict[str, Any], adapted: torch.Tensor, alpha: float, seed: int
180
+ ) -> tuple[dict[str, Any], dict[str, float | int]]:
181
+ base = unpack_gptq_entry(entry).float()
182
+ delta = adapted.detach().float().cpu() - base
183
+ target = base + alpha * delta
184
+ old_codes, scale_rows, zero_rows = _gptq_codes_and_zeros(entry)
185
+ desired = old_codes.float() + alpha * delta.T / scale_rows.clamp_min(1e-12)
186
+ generator = torch.Generator().manual_seed(seed)
187
+ maxq = 2 ** int(entry["bits"]) - 1
188
+ new_codes = _stochastic_integer_round(desired, generator=generator)
189
+ new_codes = new_codes.clamp(0, maxq).to(torch.int64)
190
+ projected = (scale_rows * (new_codes - zero_rows).float()).T
191
+ changed = new_codes.ne(old_codes)
192
+ updated = copy.copy(entry)
193
+ updated["packed"] = torch.from_numpy(
194
+ pack_bits(new_codes.numpy().astype(np.uint32), int(entry["bits"]))
195
+ )
196
+ return updated, {
197
+ "weights": int(new_codes.numel()),
198
+ "changed_codes": int(changed.sum()),
199
+ "absolute_code_steps": int((new_codes - old_codes).abs().sum()),
200
+ "target_delta_rmse": float((target - base).square().mean().sqrt()),
201
+ "projection_rmse": float((target - projected).square().mean().sqrt()),
202
+ }
203
+
204
+
205
+ def stochastic_project_quant_entry(
206
+ entry: dict[str, Any], adapted: torch.Tensor, alpha: float, seed: int
207
+ ) -> tuple[dict[str, Any], dict[str, float | int]]:
208
+ base = unpack_tensor(entry).float()
209
+ delta = adapted.detach().float().cpu() - base
210
+ target = base + alpha * delta
211
+ bits, group = int(entry["bits"]), int(entry["group"])
212
+ count = int(np.prod(entry["shape"]))
213
+ old = unpack_bits(entry["packed"].numpy(), bits, count)
214
+ old_codes = torch.from_numpy(old.astype(np.int64)).reshape(-1, group)
215
+ scale = entry["scale"].float().reshape(-1, 1)
216
+ scale = scale * math.ldexp(1.0, -int(entry.get("scale_exponent", 0)))
217
+ flat_delta = delta.reshape(-1, group)
218
+ if entry["symmetric"]:
219
+ qmax = 2 ** (bits - 1) - 1
220
+ zero = qmax + 1
221
+ else:
222
+ zero = entry["zero"].float().reshape(-1, 1)
223
+ desired = old_codes.float() + alpha * flat_delta / scale.clamp_min(1e-12)
224
+ generator = torch.Generator().manual_seed(seed)
225
+ new_codes = _stochastic_integer_round(desired, generator=generator)
226
+ new_codes = new_codes.clamp(0, 2**bits - 1).to(torch.int64)
227
+ projected = (new_codes.float() - zero) * scale
228
+ changed = new_codes.ne(old_codes)
229
+ updated = copy.copy(entry)
230
+ updated["packed"] = torch.from_numpy(pack_bits(new_codes.numpy().astype(np.uint32), bits))
231
+ return updated, {
232
+ "weights": int(new_codes.numel()),
233
+ "changed_codes": int(changed.sum()),
234
+ "absolute_code_steps": int((new_codes - old_codes).abs().sum()),
235
+ "target_delta_rmse": float((target - base).square().mean().sqrt()),
236
+ "projection_rmse": float((target - projected.reshape_as(target)).square().mean().sqrt()),
237
+ }
238
+
239
+
240
+ def stochastic_project_entry(
241
+ entry: dict[str, Any], adapted: torch.Tensor, alpha: float, seed: int
242
+ ) -> tuple[dict[str, Any], dict[str, float | int]]:
243
+ """Stochastically preserve a sub-grid learned update in physical codes."""
244
+ if entry.get("kind") == "gptq_codes":
245
+ return stochastic_project_gptq_entry(entry, adapted, alpha, seed)
246
+ if entry.get("kind") == "quant":
247
+ return stochastic_project_quant_entry(entry, adapted, alpha, seed)
248
+ raise ValueError(f"entry kind cannot be grid-projected: {entry.get('kind')!r}")
src/eaimath/buckets.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Submission buckets — three per week, scored on accuracy *within* a band.
2
+
3
+ The leaderboard ranks by accuracy inside each compression band, not on the
4
+ size/accuracy frontier. That inverts the optimization: there is **no reward for
5
+ being smaller than your bucket allows**, so every submission should sit as close
6
+ to its upper size limit as it can. A 2.0 GB model and a 3.3 GB model compete on
7
+ equal terms in bucket C; the 3.3 GB one simply gets more bits to spend.
8
+
9
+ Three bands × two tracks = six submissions selected at the end of each week.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+
16
+ # Conservative denominator: the text-only weight budget we actually compress.
17
+ # If the graders measure against the full HF repo (9.31 GB, vision tower
18
+ # included) every limit below is ~11% more generous -- worth confirming, but
19
+ # aiming at the tighter number can only be safe.
20
+ ORIGINAL_GB = 8.412
21
+ ORIGINAL_GB_FULL_REPO = 9.313
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class Bucket:
26
+ name: str
27
+ label: str
28
+ low_pct: float
29
+ high_pct: float
30
+
31
+ @property
32
+ def low_gb(self) -> float:
33
+ return self.low_pct / 100 * ORIGINAL_GB
34
+
35
+ @property
36
+ def high_gb(self) -> float:
37
+ return self.high_pct / 100 * ORIGINAL_GB
38
+
39
+ @property
40
+ def target_gb(self) -> float:
41
+ """Where a submission should aim: just under the ceiling."""
42
+ return self.high_gb * 0.98
43
+
44
+ @property
45
+ def bits_per_weight(self) -> tuple[float, float]:
46
+ return (16 * self.low_pct / 100, 16 * self.high_pct / 100)
47
+
48
+
49
+ BUCKETS: tuple[Bucket, ...] = (
50
+ Bucket("A", "≤10%", 0.0, 10.0),
51
+ Bucket("B", "10–20%", 10.0, 20.0),
52
+ Bucket("C", "20–40%", 20.0, 40.0),
53
+ )
54
+
55
+ BY_NAME = {b.name: b for b in BUCKETS}
56
+
57
+
58
+ def classify(size_gb: float | None) -> Bucket | None:
59
+ """Which bucket a checkpoint qualifies for, or None if it is too large."""
60
+ if size_gb is None:
61
+ return None
62
+ pct = 100 * size_gb / ORIGINAL_GB
63
+ for bucket in BUCKETS:
64
+ if bucket.low_pct <= pct < bucket.high_pct:
65
+ return bucket
66
+ return None
67
+
68
+
69
+ def pct_of_original(size_gb: float) -> float:
70
+ return 100 * size_gb / ORIGINAL_GB
71
+
72
+
73
+ def describe() -> str:
74
+ lines = [
75
+ f"{'bucket':<8}{'band':<10}{'size range':>18}{'bits/weight':>14}{'aim for':>10}",
76
+ "-" * 60,
77
+ ]
78
+ for b in BUCKETS:
79
+ lo, hi = b.bits_per_weight
80
+ lines.append(
81
+ f"{b.name:<8}{b.label:<10}{f'{b.low_gb:.2f}–{b.high_gb:.2f} GB':>18}"
82
+ f"{f'{lo:.2f}–{hi:.2f}':>14}{b.target_gb:>9.2f} GB"
83
+ )
84
+ lines.append("")
85
+ lines.append(
86
+ f"Scored on accuracy within a band, so aim at the ceiling — being "
87
+ f"smaller earns nothing.\nDenominator: {ORIGINAL_GB} GB (text-only). "
88
+ f"If graders use the full repo ({ORIGINAL_GB_FULL_REPO} GB) every limit "
89
+ f"is ~11% looser."
90
+ )
91
+ return "\n".join(lines)
src/eaimath/data.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Math evaluation sets and calibration data.
2
+
3
+ The leaderboard set is hidden and probably postdates the model, so we keep two
4
+ tiers deliberately separate:
5
+
6
+ * **gate** — small, fast, run on every recipe. Cheap signal for iteration.
7
+ * **holdout** — recent competitions we never tune against. The honest estimate.
8
+
9
+ AIME 2024 is deliberately excluded from the holdout: it is measurably
10
+ contaminated (inflating scores 10-20 points over clean contests), so it flatters
11
+ every recipe equally and discriminates between none of them.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, Iterable, Sequence
18
+
19
+
20
+ @dataclass
21
+ class MathExample:
22
+ example_id: str
23
+ problem: str
24
+ answer: str
25
+ source: str
26
+ metadata: dict[str, Any] = field(default_factory=dict)
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class DatasetSpec:
31
+ """How to pull one benchmark off the Hub.
32
+
33
+ Field names differ between mirrors of the same benchmark, so each role lists
34
+ candidate column names tried in order.
35
+ """
36
+
37
+ name: str
38
+ hf_id: str
39
+ split: str = "test"
40
+ config: str | None = None
41
+ problem_fields: Sequence[str] = ("problem", "Problem", "question", "Question")
42
+ answer_fields: Sequence[str] = ("answer", "Answer", "solution", "expected_answer")
43
+ tier: str = "gate"
44
+ filters: tuple[tuple[str, str, Any], ...] = ()
45
+ max_examples: int | None = None
46
+ note: str = ""
47
+
48
+
49
+ # Three tiers, by how often we run them and how much tuning pressure they can
50
+ # absorb before their numbers stop meaning anything.
51
+ #
52
+ # gate every experiment. Cheap, tuned against freely.
53
+ # checkpoint before a weekly leaderboard submission. Moderate tuning risk.
54
+ # holdout the two graded checkpoints only. NEVER tuned against — these are
55
+ # post-release contests and the closest proxy we have for a hidden
56
+ # eval that "is not available in public domain today".
57
+ REGISTRY: dict[str, DatasetSpec] = {
58
+ "math500_hard": DatasetSpec(
59
+ name="math500_hard",
60
+ hf_id="HuggingFaceH4/MATH-500",
61
+ split="test",
62
+ tier="gate",
63
+ filters=(("level", "gte", 4),),
64
+ max_examples=100,
65
+ note="MATH-500 levels 4-5, deterministic 100-problem subsample. The "
66
+ "fast regression signal: sensitive enough to catch damage, cheap "
67
+ "enough to run on every recipe.",
68
+ ),
69
+ "math500": DatasetSpec(
70
+ name="math500",
71
+ hf_id="HuggingFaceH4/MATH-500",
72
+ split="test",
73
+ tier="checkpoint",
74
+ note="Full 500. Largely saturated for this model (~84.5 bf16), so it "
75
+ "detects collapse but not subtle reasoning damage.",
76
+ ),
77
+ "aime25": DatasetSpec(
78
+ name="aime25",
79
+ hf_id="MathArena/aime_2025",
80
+ split="train",
81
+ tier="checkpoint",
82
+ note="30 problems. Hard tail — where quantization damage actually shows.",
83
+ ),
84
+ "hmmt_feb25": DatasetSpec(
85
+ name="hmmt_feb25",
86
+ hf_id="MathArena/hmmt_feb_2025",
87
+ split="train",
88
+ tier="checkpoint",
89
+ note="30 problems. Reported on the model card (74.0), so we have a "
90
+ "published bf16 reference to validate our harness against.",
91
+ ),
92
+ "aime26": DatasetSpec(
93
+ name="aime26",
94
+ hf_id="MathArena/aime_2026",
95
+ split="train",
96
+ tier="holdout",
97
+ note="30 problems, Feb 2026 contest. Post-dates most training data.",
98
+ ),
99
+ "hmmt_feb26": DatasetSpec(
100
+ name="hmmt_feb26",
101
+ hf_id="MathArena/hmmt_feb_2026",
102
+ split="train",
103
+ tier="holdout",
104
+ note="33 problems, Feb 2026 contest. Cleanest proxy for the hidden eval.",
105
+ ),
106
+ "aime24": DatasetSpec(
107
+ name="aime24",
108
+ hf_id="Maxwell-Jia/AIME_2024",
109
+ split="train",
110
+ tier="diagnostic",
111
+ note="Measurably contaminated — inflates scores 10-20 points over clean "
112
+ "contests. Diagnostic only, never for recipe selection.",
113
+ ),
114
+ }
115
+
116
+ # Training pools — problems with known answers, used to generate our own
117
+ # reasoning traces. Disjoint from every eval set above: MATH-500 is drawn from
118
+ # the MATH *test* split, so the MATH train split cannot leak into it.
119
+ TRAIN_REGISTRY: dict[str, DatasetSpec] = {
120
+ "math_train": DatasetSpec(
121
+ name="math_train",
122
+ hf_id="EleutherAI/hendrycks_math",
123
+ split="train",
124
+ config="algebra",
125
+ answer_fields=("solution",), # gold answer is the \boxed{} in the solution
126
+ tier="train",
127
+ note="MATH train split. Pass --config to pick a subject.",
128
+ ),
129
+ "openr1": DatasetSpec(
130
+ name="openr1",
131
+ hf_id="open-r1/OpenR1-Math-220k",
132
+ split="train",
133
+ answer_fields=("answer", "solution"),
134
+ tier="train",
135
+ note="220k competition problems with verified answers.",
136
+ ),
137
+ }
138
+
139
+ MATH_SUBJECTS = (
140
+ "algebra", "counting_and_probability", "geometry", "intermediate_algebra",
141
+ "number_theory", "prealgebra", "precalculus",
142
+ )
143
+
144
+ SUITES: dict[str, list[str]] = {
145
+ "gate": ["math500_hard"],
146
+ "checkpoint": ["math500", "aime25", "hmmt_feb25"],
147
+ "holdout": ["aime26", "hmmt_feb26"],
148
+ }
149
+
150
+
151
+ def _passes_filters(row: dict[str, Any], filters: Sequence[tuple[str, str, Any]]) -> bool:
152
+ for field_name, op, value in filters:
153
+ actual = row.get(field_name)
154
+ if actual is None:
155
+ return False
156
+ if op == "gte" and not actual >= value:
157
+ return False
158
+ if op == "lte" and not actual <= value:
159
+ return False
160
+ if op == "eq" and actual != value:
161
+ return False
162
+ if op == "in" and actual not in value:
163
+ return False
164
+ return True
165
+
166
+
167
+ def _subsample(examples: list[MathExample], n: int, seed: int = 0) -> list[MathExample]:
168
+ """Deterministic subsample, stable across runs and machines.
169
+
170
+ Shuffles with a fixed seed rather than taking a prefix, because these sets
171
+ are ordered by subject/difficulty and a prefix would be badly skewed. The
172
+ gate set must be identical across every recipe or the comparison is
173
+ meaningless.
174
+ """
175
+ import random
176
+
177
+ if len(examples) <= n:
178
+ return examples
179
+ indices = sorted(range(len(examples)))
180
+ random.Random(seed).shuffle(indices)
181
+ return [examples[i] for i in sorted(indices[:n])]
182
+
183
+
184
+ def _resolve_field(row: dict[str, Any], candidates: Iterable[str]) -> str | None:
185
+ lowered = {k.lower(): k for k in row}
186
+ for candidate in candidates:
187
+ key = lowered.get(candidate.lower())
188
+ if key is not None and row[key] is not None:
189
+ return str(row[key])
190
+ return None
191
+
192
+
193
+ def load_dataset_examples(
194
+ spec: DatasetSpec | str,
195
+ limit: int | None = None,
196
+ cache_dir: str | None = None,
197
+ ) -> list[MathExample]:
198
+ """Load one benchmark into ``MathExample`` records.
199
+
200
+ Raises with the observed column names when a field cannot be resolved, so a
201
+ schema change on the Hub produces an actionable error instead of silently
202
+ empty problems.
203
+ """
204
+ from datasets import load_dataset
205
+
206
+ if isinstance(spec, str):
207
+ table = {**REGISTRY, **TRAIN_REGISTRY}
208
+ if spec not in table:
209
+ raise KeyError(f"Unknown dataset {spec!r}. Known: {sorted(table)}")
210
+ spec = table[spec]
211
+
212
+ kwargs: dict[str, Any] = {"split": spec.split}
213
+ if spec.config:
214
+ kwargs["name"] = spec.config
215
+ if cache_dir:
216
+ kwargs["cache_dir"] = cache_dir
217
+
218
+ dataset = load_dataset(spec.hf_id, **kwargs)
219
+
220
+ examples: list[MathExample] = []
221
+ for i, row in enumerate(dataset):
222
+ if not _passes_filters(row, spec.filters):
223
+ continue
224
+ problem = _resolve_field(row, spec.problem_fields)
225
+ answer = _resolve_field(row, spec.answer_fields)
226
+ if problem is None or answer is None:
227
+ raise ValueError(
228
+ f"{spec.name}: could not resolve problem/answer fields. "
229
+ f"Available columns: {sorted(row)}. "
230
+ f"Tried problem={list(spec.problem_fields)}, answer={list(spec.answer_fields)}."
231
+ )
232
+ if "\\boxed" in answer:
233
+ from .answers import extract_boxed
234
+
235
+ boxed = extract_boxed(answer)
236
+ if boxed is None:
237
+ continue # unparseable gold: drop rather than train on it
238
+ answer = boxed
239
+
240
+ examples.append(
241
+ MathExample(
242
+ example_id=f"{spec.name}:{i}",
243
+ problem=problem,
244
+ answer=answer,
245
+ source=spec.name,
246
+ metadata={
247
+ k: row[k]
248
+ for k in ("level", "subject", "type", "url", "id", "problem_idx")
249
+ if k in row
250
+ },
251
+ )
252
+ )
253
+
254
+ # Spec cap first (defines the canonical set), then the ad-hoc --limit.
255
+ if spec.max_examples is not None:
256
+ examples = _subsample(examples, spec.max_examples)
257
+ if limit is not None:
258
+ examples = examples[:limit]
259
+ return examples
260
+
261
+
262
+ def load_suite(
263
+ names: Sequence[str],
264
+ limit: int | None = None,
265
+ cache_dir: str | None = None,
266
+ ) -> list[MathExample]:
267
+ """Load and concatenate several benchmarks. ``limit`` applies per dataset.
268
+
269
+ Accepts tier names (``gate``/``checkpoint``/``holdout``) as shorthand for
270
+ the datasets in that tier.
271
+ """
272
+ resolved: list[str] = []
273
+ for name in names:
274
+ resolved.extend(SUITES[name] if name in SUITES else [name])
275
+
276
+ out: list[MathExample] = []
277
+ for name in resolved:
278
+ out.extend(load_dataset_examples(name, limit=limit, cache_dir=cache_dir))
279
+ return out
280
+
281
+
282
+ def describe_registry() -> str:
283
+ lines = []
284
+ for tier in ("gate", "checkpoint", "holdout", "diagnostic"):
285
+ members = [s for s in REGISTRY.values() if s.tier == tier]
286
+ if not members:
287
+ continue
288
+ lines.append(f"[{tier}]")
289
+ for spec in members:
290
+ cap = f" (capped at {spec.max_examples})" if spec.max_examples else ""
291
+ lines.append(f" {spec.name:<14} {spec.hf_id}{cap}")
292
+ lines.append(f" {'':<14} {spec.note}")
293
+ return "\n".join(lines)
294
+
295
+
296
+ MATH_PROMPT = (
297
+ "Solve the following math problem. Put your final answer inside "
298
+ "\\boxed{{}} on the last line.\n\n"
299
+ "Problem:\n{problem}"
300
+ )
301
+
302
+
303
+ def build_prompt(example: MathExample) -> str:
304
+ return MATH_PROMPT.format(problem=example.problem.strip())
src/eaimath/delta_sharing.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cross-layer MLP sharing with low-rank, layer-specific weight deltas.
2
+
3
+ Hard tying erased every layer's identity and scored zero in Round 8. This
4
+ module keeps one full MLP per contiguous Middle-Cycle group and approximates
5
+ each follower as ``shared_base + U @ V.T``. It is the directly testable
6
+ initialization used by DeltaLLM-style recovery and the same broad idea as Basis
7
+ Sharing: shared storage plus small layer-specific coefficients.
8
+
9
+ The evaluation checkpoint expands the deltas back to ordinary dense weights so
10
+ vLLM needs no custom kernel. The report separately counts the compressed
11
+ representation; a submission decompressor would perform the same expansion.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ import torch
19
+
20
+ from .sharing import MLP_PROJECTIONS, find_decoder_layers, sharing_groups
21
+
22
+
23
+ def low_rank_delta(
24
+ anchor: torch.Tensor,
25
+ target: torch.Tensor,
26
+ rank: int,
27
+ *,
28
+ device: str,
29
+ niter: int = 1,
30
+ seed: int = 0,
31
+ ) -> tuple[torch.Tensor, dict[str, float]]:
32
+ """Return ``anchor + rank-r(target-anchor)`` and reconstruction metrics."""
33
+ if rank < 1 or rank > min(target.shape):
34
+ raise ValueError(f"rank {rank} is invalid for matrix {tuple(target.shape)}")
35
+ torch.manual_seed(seed)
36
+ if torch.cuda.is_available():
37
+ torch.cuda.manual_seed_all(seed)
38
+
39
+ base = anchor.detach().to(device=device, dtype=torch.float32)
40
+ wanted = target.detach().to(device=device, dtype=torch.float32)
41
+ delta = wanted - base
42
+ delta_norm = torch.linalg.vector_norm(delta)
43
+ target_norm = torch.linalg.vector_norm(wanted)
44
+
45
+ # Oversampling materially improves randomized SVD at almost no storage cost;
46
+ # only the requested rank is retained in the representation.
47
+ q = min(rank + 8, min(delta.shape))
48
+ u, s, v = torch.svd_lowrank(delta, q=q, niter=niter)
49
+ approximation = (u[:, :rank] * s[:rank]) @ v[:, :rank].T
50
+ residual = delta - approximation
51
+ residual_norm = torch.linalg.vector_norm(residual)
52
+ restored = (base + approximation).to(dtype=target.dtype, device="cpu")
53
+
54
+ metrics = {
55
+ "relative_delta_error": float(residual_norm / delta_norm.clamp_min(1e-12)),
56
+ "relative_weight_error": float(residual_norm / target_norm.clamp_min(1e-12)),
57
+ "delta_norm": float(delta_norm),
58
+ "residual_norm": float(residual_norm),
59
+ }
60
+ del base, wanted, delta, approximation, residual, u, s, v
61
+ if str(device).startswith("cuda"):
62
+ torch.cuda.empty_cache()
63
+ return restored, metrics
64
+
65
+
66
+ def apply_delta_sharing(
67
+ model: torch.nn.Module,
68
+ factor: int,
69
+ rank: int,
70
+ *,
71
+ device: str,
72
+ niter: int = 1,
73
+ seed: int = 0,
74
+ ) -> dict[str, Any]:
75
+ """Approximate follower MLPs in place and count the compressed form."""
76
+ layers = find_decoder_layers(model)
77
+ groups = sharing_groups(len(layers), factor)
78
+ dense_mlp_params = sum(
79
+ getattr(layer.mlp, projection).weight.numel()
80
+ for layer in layers
81
+ for projection in MLP_PROJECTIONS
82
+ )
83
+ basis_layers: set[int] = {0, 1, len(layers) - 1}
84
+ follower_layers: set[int] = set()
85
+ rows: list[dict[str, Any]] = []
86
+ delta_params = 0
87
+
88
+ with torch.no_grad():
89
+ for group_index, group in enumerate(groups):
90
+ if len(group) < 2:
91
+ basis_layers.update(group)
92
+ continue
93
+ anchor_index = group[0]
94
+ basis_layers.add(anchor_index)
95
+ for follower_index in group[1:]:
96
+ follower_layers.add(follower_index)
97
+ for projection_index, projection in enumerate(MLP_PROJECTIONS):
98
+ anchor = getattr(layers[anchor_index].mlp, projection).weight
99
+ follower = getattr(layers[follower_index].mlp, projection).weight
100
+ restored, metrics = low_rank_delta(
101
+ anchor,
102
+ follower,
103
+ rank,
104
+ device=device,
105
+ niter=niter,
106
+ seed=seed + group_index * 10_000 + follower_index * 10 + projection_index,
107
+ )
108
+ follower.copy_(restored)
109
+ delta_params += rank * (follower.shape[0] + follower.shape[1])
110
+ rows.append(
111
+ {
112
+ "group": group_index,
113
+ "anchor_layer": anchor_index,
114
+ "follower_layer": follower_index,
115
+ "projection": projection,
116
+ "shape": list(follower.shape),
117
+ **metrics,
118
+ }
119
+ )
120
+
121
+ basis_params = sum(
122
+ getattr(layers[index].mlp, projection).weight.numel()
123
+ for index in sorted(basis_layers)
124
+ for projection in MLP_PROJECTIONS
125
+ )
126
+ represented = basis_params + delta_params
127
+ return {
128
+ "factor": factor,
129
+ "rank": rank,
130
+ "niter": niter,
131
+ "groups": [group for group in groups if len(group) > 1],
132
+ "basis_layers": sorted(basis_layers),
133
+ "follower_layers": sorted(follower_layers),
134
+ "dense_mlp_params": dense_mlp_params,
135
+ "basis_params": basis_params,
136
+ "delta_params": delta_params,
137
+ "represented_mlp_params": represented,
138
+ "mlp_parameter_fraction": represented / dense_mlp_params,
139
+ "bf16_representation_bytes": represented * 2,
140
+ "max_relative_weight_error": max(row["relative_weight_error"] for row in rows),
141
+ "mean_relative_weight_error": sum(row["relative_weight_error"] for row in rows) / len(rows),
142
+ "max_relative_delta_error": max(row["relative_delta_error"] for row in rows),
143
+ "rows": rows,
144
+ }
src/eaimath/embedding_predictor.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tiny deterministic predictor for vocabulary rows omitted from an artifact.
2
+
3
+ The predictor borrows the idea of intra-frame prediction from codecs: a token's
4
+ string is side information already present in the tokenizer, so only a small
5
+ linear dictionary has to be stored. Exact retained rows are scattered over the
6
+ prediction during restoration.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ from typing import Any
13
+
14
+ import torch
15
+
16
+ from .pack import pack_tensor, state_dict_bytes, unpack_tensor
17
+
18
+ FEATURE_DIM = 272
19
+
20
+
21
+ def _token_strings(tokenizer: Any, ids: list[int]) -> list[str]:
22
+ tokens = tokenizer.convert_ids_to_tokens(ids)
23
+ if isinstance(tokens, str):
24
+ tokens = [tokens]
25
+ return ["" if token is None else str(token) for token in tokens]
26
+
27
+
28
+ def token_string_features(tokenizer: Any, ids: torch.Tensor) -> torch.Tensor:
29
+ """Return tokenizer-derived features without storing anything per token.
30
+
31
+ Layout: 256 normalized UTF-8 byte counts, eight log-length buckets, four
32
+ first-byte classes and four last-byte classes. The last two blocks retain a
33
+ little order information while keeping the dictionary under one megabyte.
34
+ """
35
+ flat_ids = ids.detach().cpu().long().reshape(-1)
36
+ strings = _token_strings(tokenizer, [int(value) for value in flat_ids.tolist()])
37
+ features = torch.zeros((len(strings), FEATURE_DIM), dtype=torch.float32)
38
+ for row, token in enumerate(strings):
39
+ encoded = token.encode("utf-8", errors="replace") or b"\x00"
40
+ byte_ids = torch.tensor(list(encoded), dtype=torch.int64)
41
+ counts = torch.bincount(byte_ids, minlength=256).float()
42
+ features[row, :256] = counts / counts.square().sum().sqrt().clamp_min(1.0)
43
+ length_bucket = min(7, int(math.log2(max(1, len(encoded)))))
44
+ features[row, 256 + length_bucket] = 1.0
45
+ features[row, 264 + encoded[0] // 64] = 1.0
46
+ features[row, 268 + encoded[-1] // 64] = 1.0
47
+ return features
48
+
49
+
50
+ def _sample_ids(vocab_size: int, sample_size: int, offset: float = 0.0) -> torch.Tensor:
51
+ count = min(vocab_size, sample_size)
52
+ if count == vocab_size:
53
+ return torch.arange(vocab_size, dtype=torch.int64)
54
+ positions = (torch.arange(count, dtype=torch.float64) + offset) * vocab_size / count
55
+ return positions.floor().clamp_max(vocab_size - 1).long().unique()
56
+
57
+
58
+ def fit_token_predictor(
59
+ weight: torch.Tensor,
60
+ tokenizer: Any,
61
+ *,
62
+ sample_size: int = 65_536,
63
+ heldout_size: int = 4_096,
64
+ ridge: float = 1e-2,
65
+ device: str = "cpu",
66
+ ) -> tuple[dict[str, Any], dict[str, float | int | str]]:
67
+ """Fit and INT8-pack a ridge dictionary for a full embedding matrix."""
68
+ if weight.ndim != 2 or weight.shape[0] < 2:
69
+ raise ValueError(f"expected a vocabulary matrix, found {tuple(weight.shape)}")
70
+ if ridge <= 0:
71
+ raise ValueError("ridge must be positive")
72
+ train_ids = _sample_ids(weight.shape[0], sample_size)
73
+ x = token_string_features(tokenizer, train_ids).to(device)
74
+ y = weight.detach().cpu().index_select(0, train_ids).float().to(device)
75
+ gram = x.T @ x
76
+ gram.diagonal().add_(ridge)
77
+ basis = torch.linalg.solve(gram, x.T @ y).cpu()
78
+
79
+ heldout_ids = _sample_ids(weight.shape[0], heldout_size, offset=0.5)
80
+ # Exclude any collision with the evenly spaced training grid.
81
+ train_set = set(int(value) for value in train_ids.tolist())
82
+ heldout_list = [int(value) for value in heldout_ids.tolist() if int(value) not in train_set]
83
+ if not heldout_list:
84
+ heldout_list = [int(train_ids[-1])]
85
+ heldout_ids = torch.tensor(heldout_list, dtype=torch.int64)
86
+ hx = token_string_features(tokenizer, heldout_ids)
87
+ target = weight.detach().cpu().index_select(0, heldout_ids).float()
88
+ prediction = hx @ basis
89
+ cosine = torch.nn.functional.cosine_similarity(prediction, target, dim=1).mean()
90
+ denominator = target.square().mean().sqrt().clamp_min(1e-12)
91
+ relative_rmse = (prediction - target).square().mean().sqrt() / denominator
92
+
93
+ packed_basis = pack_tensor(basis, bits=8, group_size=256)
94
+ entry: dict[str, Any] = {
95
+ "kind": "token_string_ridge_v1",
96
+ "feature_dim": FEATURE_DIM,
97
+ "basis": packed_basis,
98
+ "vocab_size": int(weight.shape[0]),
99
+ "hidden_size": int(weight.shape[1]),
100
+ "ridge": float(ridge),
101
+ "sample_size": int(train_ids.numel()),
102
+ }
103
+ report: dict[str, float | int | str] = {
104
+ "kind": entry["kind"],
105
+ "stored_bytes": state_dict_bytes({"basis": packed_basis}),
106
+ "sample_size": int(train_ids.numel()),
107
+ "heldout_size": int(heldout_ids.numel()),
108
+ "heldout_mean_cosine": float(cosine),
109
+ "heldout_relative_rmse": float(relative_rmse),
110
+ }
111
+ return entry, report
112
+
113
+
114
+ def predict_token_rows(
115
+ entry: dict[str, Any],
116
+ tokenizer: Any,
117
+ *,
118
+ batch_size: int = 8_192,
119
+ ) -> torch.Tensor:
120
+ """Decode a predictor into the original dense vocabulary matrix."""
121
+ if entry.get("kind") != "token_string_ridge_v1":
122
+ raise ValueError(f"unsupported token predictor: {entry.get('kind')!r}")
123
+ basis = unpack_tensor(entry["basis"]).float()
124
+ rows = []
125
+ for start in range(0, int(entry["vocab_size"]), batch_size):
126
+ stop = min(int(entry["vocab_size"]), start + batch_size)
127
+ ids = torch.arange(start, stop, dtype=torch.int64)
128
+ rows.append((token_string_features(tokenizer, ids) @ basis).to(torch.bfloat16))
129
+ return torch.cat(rows, dim=0)
src/eaimath/evaluate.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The math eval loop, and the metrics that actually discriminate recipes.
2
+
3
+ Accuracy alone is not enough. Published measurements on this exact model family
4
+ show W4A16 retaining ~99% of MATH-500 while losing ~11-20 points of AIME, and
5
+ show quantization roughly doubling the truncation rate on AIME under a fixed
6
+ token cap. The mechanism is that low-bit weights perturb high-entropy
7
+ "branching" tokens, the model rambles, and it never emits its closing tag.
8
+
9
+ So every run reports three things:
10
+
11
+ * ``accuracy`` — did it get the answer right
12
+ * ``truncation_rate`` — did it run out of budget instead of stopping
13
+ * ``mean_generated_tokens`` / ``think_close_rate`` — is CoT inflating
14
+
15
+ A recipe that holds accuracy while inflating tokens is not safe; it is a recipe
16
+ that will collapse the moment the grader's token cap is tighter than ours.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import statistics
23
+ from dataclasses import asdict, dataclass
24
+ from pathlib import Path
25
+ from typing import Any, Sequence
26
+
27
+ from .answers import answers_match, extract_answer
28
+ from .data import MathExample, build_prompt
29
+ from .generate import GenerationOutput, generate
30
+
31
+ THINK_CLOSE_TAG = "</think>"
32
+
33
+
34
+ @dataclass
35
+ class MathPrediction:
36
+ example_id: str
37
+ source: str
38
+ gold: str
39
+ predicted: str | None
40
+ correct: bool
41
+ finished: bool
42
+ think_closed: bool
43
+ num_generated_tokens: int
44
+ num_prompt_tokens: int
45
+ response: str
46
+
47
+
48
+ def score_generation(
49
+ example: MathExample, output: GenerationOutput
50
+ ) -> MathPrediction:
51
+ predicted = extract_answer(output.text)
52
+ return MathPrediction(
53
+ example_id=example.example_id,
54
+ source=example.source,
55
+ gold=example.answer,
56
+ predicted=predicted,
57
+ correct=answers_match(predicted, example.answer),
58
+ finished=output.finished,
59
+ # A thinking model that never closes its tag has looped, even if it
60
+ # somehow stopped afterwards.
61
+ think_closed=THINK_CLOSE_TAG in output.text,
62
+ num_generated_tokens=output.num_generated_tokens,
63
+ num_prompt_tokens=output.num_prompt_tokens,
64
+ response=output.text,
65
+ )
66
+
67
+
68
+ def summarize(predictions: Sequence[MathPrediction]) -> dict[str, Any]:
69
+ n = len(predictions)
70
+ if n == 0:
71
+ return {"num_examples": 0}
72
+
73
+ lengths = [p.num_generated_tokens for p in predictions]
74
+ finished = [p for p in predictions if p.finished]
75
+ truncated = [p for p in predictions if not p.finished]
76
+
77
+ return {
78
+ "num_examples": n,
79
+ "accuracy": sum(p.correct for p in predictions) / n,
80
+ "parse_rate": sum(p.predicted is not None for p in predictions) / n,
81
+ # The headline risk metric: budget exhaustion, not wrong answers.
82
+ "truncation_rate": len(truncated) / n,
83
+ "think_close_rate": sum(p.think_closed for p in predictions) / n,
84
+ "mean_generated_tokens": statistics.mean(lengths),
85
+ "median_generated_tokens": statistics.median(lengths),
86
+ "max_generated_tokens": max(lengths),
87
+ # Splitting accuracy by termination separates "reasoned badly" from
88
+ # "never got to answer" — they need different fixes.
89
+ "accuracy_when_finished": (
90
+ sum(p.correct for p in finished) / len(finished) if finished else None
91
+ ),
92
+ "accuracy_when_truncated": (
93
+ sum(p.correct for p in truncated) / len(truncated) if truncated else None
94
+ ),
95
+ }
96
+
97
+
98
+ def summarize_by_source(predictions: Sequence[MathPrediction]) -> dict[str, Any]:
99
+ sources = sorted({p.source for p in predictions})
100
+ return {s: summarize([p for p in predictions if p.source == s]) for s in sources}
101
+
102
+
103
+ def run_eval(
104
+ model,
105
+ tokenizer,
106
+ examples: Sequence[MathExample],
107
+ *,
108
+ max_new_tokens: int = 65536,
109
+ temperature: float = 0.0,
110
+ top_p: float = 0.95,
111
+ top_k: int = 20,
112
+ presence_penalty: float = 0.0,
113
+ repetition_penalty: float = 1.0,
114
+ batch_size: int = 8,
115
+ enable_thinking: bool | None = None,
116
+ ) -> list[MathPrediction]:
117
+ outputs = generate(
118
+ model,
119
+ tokenizer,
120
+ [build_prompt(ex) for ex in examples],
121
+ max_new_tokens=max_new_tokens,
122
+ temperature=temperature,
123
+ top_p=top_p,
124
+ top_k=top_k,
125
+ repetition_penalty=repetition_penalty,
126
+ batch_size=batch_size,
127
+ enable_thinking=enable_thinking,
128
+ desc="math eval",
129
+ )
130
+ return [score_generation(ex, out) for ex, out in zip(examples, outputs)]
131
+
132
+
133
+ def run_eval_vllm(
134
+ model_path: str,
135
+ tokenizer,
136
+ examples: Sequence[MathExample],
137
+ *,
138
+ max_new_tokens: int = 65536,
139
+ temperature: float = 0.0,
140
+ top_p: float = 0.95,
141
+ top_k: int = 20,
142
+ presence_penalty: float = 0.0,
143
+ repetition_penalty: float = 1.0,
144
+ enable_thinking: bool | None = None,
145
+ gpu_memory_utilization: float = 0.90,
146
+ allowed_token_ids: Sequence[int] | None = None,
147
+ llm=None,
148
+ ) -> tuple[list[MathPrediction], object]:
149
+ """Same scoring, vLLM engine. Returns the engine so it can be reused."""
150
+ from .vllm_backend import generate_vllm
151
+
152
+ outputs, llm = generate_vllm(
153
+ model_path,
154
+ tokenizer,
155
+ [build_prompt(ex) for ex in examples],
156
+ max_new_tokens=max_new_tokens,
157
+ temperature=temperature,
158
+ top_p=top_p,
159
+ top_k=top_k,
160
+ presence_penalty=presence_penalty,
161
+ repetition_penalty=repetition_penalty,
162
+ enable_thinking=enable_thinking,
163
+ gpu_memory_utilization=gpu_memory_utilization,
164
+ allowed_token_ids=allowed_token_ids,
165
+ llm=llm,
166
+ )
167
+ return [score_generation(ex, out) for ex, out in zip(examples, outputs)], llm
168
+
169
+
170
+ def save_results(
171
+ output_dir: str | Path,
172
+ *,
173
+ run_name: str,
174
+ config: dict[str, Any],
175
+ predictions: Sequence[MathPrediction],
176
+ ) -> Path:
177
+ """Write ``summary.json`` (tracked) and ``generations.jsonl`` (gitignored)."""
178
+ out = Path(output_dir) / run_name
179
+ out.mkdir(parents=True, exist_ok=True)
180
+
181
+ summary = {
182
+ "run_name": run_name,
183
+ "config": config,
184
+ "overall": summarize(predictions),
185
+ "by_source": summarize_by_source(predictions),
186
+ }
187
+ (out / "summary.json").write_text(json.dumps(summary, indent=2))
188
+
189
+ with (out / "generations.jsonl").open("w") as fh:
190
+ for prediction in predictions:
191
+ fh.write(json.dumps(asdict(prediction)) + "\n")
192
+
193
+ return out
194
+
195
+
196
+ def format_summary(summary: dict[str, Any]) -> str:
197
+ overall = summary["overall"]
198
+ if not overall.get("num_examples"):
199
+ return "no examples evaluated"
200
+
201
+ lines = [
202
+ f" examples {overall['num_examples']}",
203
+ f" accuracy {overall['accuracy']:.3f}",
204
+ f" parse rate {overall['parse_rate']:.3f}",
205
+ f" TRUNCATION RATE {overall['truncation_rate']:.3f} <- budget exhaustion",
206
+ f" think-close rate {overall['think_close_rate']:.3f}",
207
+ f" mean gen tokens {overall['mean_generated_tokens']:.0f}",
208
+ f" median gen tokens {overall['median_generated_tokens']:.0f}",
209
+ f" max gen tokens {overall['max_generated_tokens']}",
210
+ ]
211
+ if overall.get("accuracy_when_finished") is not None:
212
+ lines.append(f" acc | finished {overall['accuracy_when_finished']:.3f}")
213
+ if overall.get("accuracy_when_truncated") is not None:
214
+ lines.append(f" acc | truncated {overall['accuracy_when_truncated']:.3f}")
215
+
216
+ lines.append("")
217
+ for source, stats in summary["by_source"].items():
218
+ lines.append(
219
+ f" [{source}] n={stats['num_examples']} acc={stats['accuracy']:.3f} "
220
+ f"trunc={stats['truncation_rate']:.3f} tok={stats['mean_generated_tokens']:.0f}"
221
+ )
222
+ return "\n".join(lines)
src/eaimath/extreme_quant.py ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Training-free extreme weight compression primitives for Bucket A.
2
+
3
+ The functions in this module produce dense BF16 values for ordinary Hugging
4
+ Face/vLLM evaluation while accounting for the compact representation that
5
+ would be stored in a submission artifact. They deliberately avoid custom
6
+ inference kernels: Round 11 asks which representation still reasons, not which
7
+ kernel is fastest.
8
+
9
+ Three representation families are implemented:
10
+
11
+ * binary and ternary scalar grids with one BF16 scale per group;
12
+ * binary grids with a fixed number of exact sparse residuals per group;
13
+ * residual vector codebooks (an intentionally small AQLM-style screen).
14
+
15
+ The vector-codebook implementation is not claimed to reproduce AQLM. AQLM
16
+ jointly optimizes block outputs and codebooks; this is a deterministic,
17
+ weight-only cold-start test that tells us whether paying for that optimization
18
+ is justified on Qwen3.5's hybrid architecture.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import hashlib
24
+ import math
25
+ import re
26
+ from dataclasses import asdict, dataclass, replace
27
+ from typing import Any, Sequence
28
+
29
+ import torch
30
+
31
+ from .model import BUDGET_GROUPS
32
+ from .quantize import EXCLUDED, PROTECTED, QuantSpec, quantize_dequantize
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class ExtremeSpec:
37
+ kind: str
38
+ group_size: int = 256
39
+ outliers: int = 0
40
+ vector_dim: int = 8
41
+ codebooks: int = 0
42
+ codebook_size: int = 16
43
+ kmeans_iterations: int = 4
44
+ kmeans_samples: int = 32_768
45
+ assign_chunk: int = 65_536
46
+ rank: int = 0
47
+
48
+
49
+ def _component(name: str) -> str:
50
+ for group, pattern in BUDGET_GROUPS:
51
+ if re.search(pattern, name):
52
+ return group
53
+ return "other"
54
+
55
+
56
+ def _seed_for(name: str, seed: int) -> int:
57
+ digest = hashlib.sha256(f"{seed}:{name}".encode()).digest()
58
+ return int.from_bytes(digest[:8], "little") % (2**31)
59
+
60
+
61
+ def _grouped(weight: torch.Tensor, group_size: int) -> tuple[torch.Tensor, tuple[int, ...]]:
62
+ shape = tuple(weight.shape)
63
+ width = shape[-1]
64
+ if group_size <= 0 or width % group_size:
65
+ group_size = width
66
+ return weight.reshape(-1, group_size).float(), shape
67
+
68
+
69
+ def _normalized_fwht(values: torch.Tensor) -> torch.Tensor:
70
+ """Orthonormal Walsh-Hadamard transform over the last dimension.
71
+
72
+ The transform is its own inverse and fixed by shape, so a submission stores
73
+ no transform matrix. This is the transform-coding analogue of using a DCT
74
+ before scalar quantization, but every operation is an add/subtract and all
75
+ power-of-two Qwen group widths are supported.
76
+ """
77
+ width = values.shape[-1]
78
+ if width < 1 or width & (width - 1):
79
+ raise ValueError(f"Hadamard width must be a power of two, found {width}")
80
+ # ``float().contiguous()`` can alias an already-contiguous float input.
81
+ # The butterfly is in-place, so clone to keep this a pure transform.
82
+ output = values.float().contiguous().clone()
83
+ stride = 1
84
+ while stride < width:
85
+ blocks = output.reshape(-1, width // (2 * stride), 2, stride)
86
+ left = blocks[:, :, 0, :].clone()
87
+ right = blocks[:, :, 1, :].clone()
88
+ blocks[:, :, 0, :] = left + right
89
+ blocks[:, :, 1, :] = left - right
90
+ output = blocks.reshape(values.shape)
91
+ stride *= 2
92
+ return output / math.sqrt(width)
93
+
94
+
95
+ def _hadamard_quantize(weight: torch.Tensor, spec: ExtremeSpec) -> tuple[torch.Tensor, int]:
96
+ """Transform-code independent groups and invert to dense evaluation values."""
97
+ base_kind = spec.kind.removeprefix("hadamard_")
98
+ if base_kind == spec.kind:
99
+ raise ValueError(f"not a Hadamard spec: {spec.kind}")
100
+ groups, shape = _grouped(weight, spec.group_size)
101
+ if groups.shape[-1] & (groups.shape[-1] - 1):
102
+ raise ValueError("Hadamard transform requires a power-of-two effective group")
103
+ coefficients = _normalized_fwht(groups)
104
+ base_spec = replace(spec, kind=base_kind, group_size=groups.shape[-1])
105
+ if base_kind == "binary":
106
+ coded = _binary(coefficients, base_spec)
107
+ codebook_values = 0
108
+ elif base_kind == "ternary":
109
+ coded = _ternary(coefficients, base_spec)
110
+ codebook_values = 0
111
+ elif base_kind == "sparse_binary":
112
+ coded = _sparse_binary(coefficients, base_spec)
113
+ codebook_values = 0
114
+ elif base_kind.startswith("int"):
115
+ bits = int(base_kind.removeprefix("int"))
116
+ coded = quantize_dequantize(
117
+ coefficients,
118
+ QuantSpec(pattern=".*", bits=bits, group_size=groups.shape[-1]),
119
+ )
120
+ codebook_values = 0
121
+ else:
122
+ raise ValueError(f"unsupported Hadamard coefficient codec: {base_kind}")
123
+ restored = _normalized_fwht(coded).reshape(shape).to(weight.dtype)
124
+ return restored, codebook_values
125
+
126
+
127
+ def _binary(weight: torch.Tensor, spec: ExtremeSpec) -> torch.Tensor:
128
+ groups, shape = _grouped(weight, spec.group_size)
129
+ scale = groups.abs().mean(dim=1, keepdim=True).clamp_min(1e-8)
130
+ signs = torch.where(groups >= 0, 1.0, -1.0)
131
+ return (signs * scale).reshape(shape).to(weight.dtype)
132
+
133
+
134
+ def _ternary(weight: torch.Tensor, spec: ExtremeSpec) -> torch.Tensor:
135
+ """MSE-select a symmetric ternary threshold independently per group."""
136
+ groups, shape = _grouped(weight, spec.group_size)
137
+ magnitude = groups.abs()
138
+ mean_abs = magnitude.mean(dim=1, keepdim=True).clamp_min(1e-8)
139
+ best = torch.zeros_like(groups)
140
+ best_error = torch.full(
141
+ (groups.shape[0], 1), float("inf"), device=groups.device, dtype=groups.dtype
142
+ )
143
+ # A compact grid is enough for a diagnostic and avoids training on eval data.
144
+ for multiplier in (0.35, 0.45, 0.55, 0.65, 0.75, 0.90):
145
+ mask = magnitude >= mean_abs * multiplier
146
+ count = mask.sum(dim=1, keepdim=True).clamp_min(1)
147
+ scale = (magnitude * mask).sum(dim=1, keepdim=True) / count
148
+ candidate = torch.sign(groups) * mask * scale
149
+ error = (groups - candidate).square().mean(dim=1, keepdim=True)
150
+ improve = error < best_error
151
+ best = torch.where(improve, candidate, best)
152
+ best_error = torch.where(improve, error, best_error)
153
+ return best.reshape(shape).to(weight.dtype)
154
+
155
+
156
+ def _activation_weighted_ternary(
157
+ weight: torch.Tensor,
158
+ spec: ExtremeSpec,
159
+ importance: torch.Tensor | None,
160
+ ) -> torch.Tensor:
161
+ """Ternarize against diagonal activation covariance instead of weight MSE."""
162
+ if importance is None or weight.ndim != 2 or importance.numel() != weight.shape[-1]:
163
+ return _ternary(weight, replace(spec, kind="ternary"))
164
+ groups, shape = _grouped(weight, spec.group_size)
165
+ group = groups.shape[-1]
166
+ coordinate_weight = importance.detach().float().clamp_min(0)
167
+ coordinate_weight = coordinate_weight / coordinate_weight.mean().clamp_min(1e-12)
168
+ weights = coordinate_weight.reshape(1, -1).expand(shape[0], -1).reshape(-1, group)
169
+ magnitude = groups.abs()
170
+ mean_abs = (magnitude * weights).sum(dim=1, keepdim=True) / weights.sum(
171
+ dim=1, keepdim=True
172
+ ).clamp_min(1e-12)
173
+ best = torch.zeros_like(groups)
174
+ best_error = torch.full(
175
+ (groups.shape[0], 1), float("inf"), device=groups.device, dtype=groups.dtype
176
+ )
177
+ for multiplier in (0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.90, 1.10):
178
+ mask = magnitude >= mean_abs * multiplier
179
+ selected_weight = weights * mask
180
+ scale = (magnitude * selected_weight).sum(dim=1, keepdim=True) / selected_weight.sum(
181
+ dim=1, keepdim=True
182
+ ).clamp_min(1e-12)
183
+ candidate = torch.sign(groups) * mask * scale
184
+ error = ((groups - candidate).square() * weights).sum(dim=1, keepdim=True)
185
+ improve = error < best_error
186
+ best = torch.where(improve, candidate, best)
187
+ best_error = torch.where(improve, error, best_error)
188
+ return best.reshape(shape).to(weight.dtype)
189
+
190
+
191
+ def _sigma_delta_ternary(
192
+ weight: torch.Tensor,
193
+ spec: ExtremeSpec,
194
+ importance: torch.Tensor | None,
195
+ ) -> torch.Tensor:
196
+ """First-order error-feedback ternary in activation-energy order.
197
+
198
+ Coordinates with lower observed energy are visited first, allowing their
199
+ quantization residue to influence later, more important coordinates. The
200
+ traversal is reconstructed from the same train-only sensitivity vector, so
201
+ it requires no stored permutation or side information.
202
+ """
203
+ if importance is None or weight.ndim != 2 or importance.numel() != weight.shape[-1]:
204
+ return _ternary(weight, replace(spec, kind="ternary"))
205
+ groups, shape = _grouped(weight, spec.group_size)
206
+ group = groups.shape[-1]
207
+ column_groups = shape[-1] // group
208
+ energy = importance.detach().float().clamp_min(0).reshape(column_groups, group)
209
+ order = energy.argsort(dim=1).repeat(shape[0], 1).to(groups.device)
210
+ ordinary = _ternary(weight, replace(spec, kind="ternary")).reshape_as(groups).float()
211
+ scale = ordinary.abs().amax(dim=1).clamp_min(1e-8)
212
+ output = torch.zeros_like(groups)
213
+ feedback = torch.zeros(groups.shape[0], device=groups.device, dtype=groups.dtype)
214
+ rows = torch.arange(groups.shape[0], device=groups.device)
215
+ for step in range(group):
216
+ index = order[:, step]
217
+ value = groups[rows, index] + feedback
218
+ quantized = torch.where(
219
+ value > scale * 0.5,
220
+ scale,
221
+ torch.where(value < -scale * 0.5, -scale, torch.zeros_like(value)),
222
+ )
223
+ output[rows, index] = quantized
224
+ feedback = value - quantized
225
+ return output.reshape(shape).to(weight.dtype)
226
+
227
+
228
+ def _structured_residual_ternary(
229
+ weight: torch.Tensor,
230
+ spec: ExtremeSpec,
231
+ *,
232
+ seed: int,
233
+ ) -> tuple[torch.Tensor, int]:
234
+ """Preserve a low-rank subspace and ternarize only the residual."""
235
+ if weight.ndim != 2 or spec.rank < 1:
236
+ raise ValueError("srr_ternary requires a matrix and rank >= 1")
237
+ rank = min(spec.rank, min(weight.shape))
238
+ target = weight.float()
239
+ devices = [target.device.index or 0] if target.is_cuda else []
240
+ with torch.random.fork_rng(devices=devices):
241
+ torch.manual_seed(seed)
242
+ if target.is_cuda:
243
+ torch.cuda.manual_seed_all(seed)
244
+ u, singular, v = torch.svd_lowrank(
245
+ target,
246
+ q=min(rank + 4, min(target.shape)),
247
+ niter=2,
248
+ )
249
+ left = u[:, :rank] * singular[:rank]
250
+ right = v[:, :rank].T
251
+ residual = target - left @ right
252
+ coded = _ternary(residual, replace(spec, kind="ternary"))
253
+ # BF16 left/right factors are the complete side information.
254
+ factor_values = rank * (weight.shape[0] + weight.shape[1])
255
+ return (coded.float() + left @ right).to(weight.dtype), factor_values
256
+
257
+
258
+ def _sparse_binary(weight: torch.Tensor, spec: ExtremeSpec) -> torch.Tensor:
259
+ """Binary base plus exact, fixed-rate residual exceptions in each group."""
260
+ groups, shape = _grouped(weight, spec.group_size)
261
+ if not 0 < spec.outliers < groups.shape[1]:
262
+ raise ValueError("sparse_binary requires 0 < outliers < group_size")
263
+
264
+ first_scale = groups.abs().mean(dim=1, keepdim=True).clamp_min(1e-8)
265
+ first = torch.where(groups >= 0, first_scale, -first_scale)
266
+ indices = (groups - first).abs().topk(spec.outliers, dim=1).indices
267
+ residual_mask = torch.zeros_like(groups, dtype=torch.bool)
268
+ residual_mask.scatter_(1, indices, True)
269
+
270
+ base_mask = ~residual_mask
271
+ scale = (
272
+ (groups.abs() * base_mask).sum(dim=1, keepdim=True)
273
+ / base_mask.sum(dim=1, keepdim=True).clamp_min(1)
274
+ ).clamp_min(1e-8)
275
+ output = torch.where(groups >= 0, scale, -scale)
276
+ output[residual_mask] = groups[residual_mask]
277
+ return output.reshape(shape).to(weight.dtype)
278
+
279
+
280
+ def _assign(points: torch.Tensor, centers: torch.Tensor, chunk: int) -> torch.Tensor:
281
+ assignments = []
282
+ center_norm = centers.square().sum(dim=1).unsqueeze(0)
283
+ for start in range(0, points.shape[0], chunk):
284
+ block = points[start : start + chunk]
285
+ distances = block.square().sum(dim=1, keepdim=True) + center_norm
286
+ distances.addmm_(block, centers.T, beta=1.0, alpha=-2.0)
287
+ assignments.append(distances.argmin(dim=1))
288
+ return torch.cat(assignments)
289
+
290
+
291
+ def _fit_codebook(
292
+ points: torch.Tensor,
293
+ *,
294
+ size: int,
295
+ iterations: int,
296
+ max_samples: int,
297
+ chunk: int,
298
+ seed: int,
299
+ ) -> torch.Tensor:
300
+ if points.shape[0] < size:
301
+ raise ValueError(f"need at least {size} vectors, found {points.shape[0]}")
302
+ generator = torch.Generator(device="cpu")
303
+ generator.manual_seed(seed)
304
+ if points.shape[0] > max_samples:
305
+ # randperm(points.shape[0]) would allocate gigabytes for the largest
306
+ # MLP matrices. Sampling with replacement keeps this bounded at O(sample).
307
+ sample_idx = torch.randint(
308
+ points.shape[0], (max_samples,), generator=generator
309
+ )
310
+ sample = points.index_select(0, sample_idx.to(points.device))
311
+ else:
312
+ sample = points
313
+ init_idx = torch.randperm(sample.shape[0], generator=generator)[:size]
314
+ centers = sample.index_select(0, init_idx.to(sample.device)).clone()
315
+
316
+ for _ in range(iterations):
317
+ assignment = _assign(sample, centers, chunk)
318
+ sums = torch.zeros_like(centers)
319
+ sums.index_add_(0, assignment, sample)
320
+ counts = torch.bincount(assignment, minlength=size).to(sample.dtype).unsqueeze(1)
321
+ nonempty = counts.squeeze(1) > 0
322
+ centers[nonempty] = sums[nonempty] / counts[nonempty]
323
+ return centers
324
+
325
+
326
+ def _vector_additive(
327
+ weight: torch.Tensor, spec: ExtremeSpec, *, seed: int
328
+ ) -> tuple[torch.Tensor, int]:
329
+ if spec.codebooks < 1:
330
+ raise ValueError("vector quantization requires at least one codebook")
331
+ if spec.codebook_size < 2 or spec.codebook_size & (spec.codebook_size - 1):
332
+ raise ValueError("codebook_size must be a power of two")
333
+
334
+ original_shape = tuple(weight.shape)
335
+ flat = weight.float().reshape(-1)
336
+ padding = (-flat.numel()) % spec.vector_dim
337
+ if padding:
338
+ flat = torch.nn.functional.pad(flat, (0, padding))
339
+ points = flat.reshape(-1, spec.vector_dim)
340
+ residual = points.clone()
341
+ reconstructed = torch.zeros_like(points)
342
+ codebook_values = 0
343
+
344
+ for book in range(spec.codebooks):
345
+ centers = _fit_codebook(
346
+ residual,
347
+ size=spec.codebook_size,
348
+ iterations=spec.kmeans_iterations,
349
+ max_samples=spec.kmeans_samples,
350
+ chunk=spec.assign_chunk,
351
+ seed=seed + book * 104_729,
352
+ )
353
+ assignment = _assign(residual, centers, spec.assign_chunk)
354
+ contribution = centers.index_select(0, assignment)
355
+ reconstructed.add_(contribution)
356
+ residual.sub_(contribution)
357
+ codebook_values += centers.numel()
358
+
359
+ restored = reconstructed.reshape(-1)[: weight.numel()].reshape(original_shape)
360
+ return restored.to(weight.dtype), codebook_values
361
+
362
+
363
+ def _representation_bits(
364
+ numel: int, spec: ExtremeSpec, *, codebook_values: int = 0
365
+ ) -> int:
366
+ if spec.kind.startswith("hadamard_"):
367
+ # The fixed normalized transform is reconstructed from group width and
368
+ # costs no payload bytes. Only its coefficient codec is charged.
369
+ return _representation_bits(
370
+ numel,
371
+ replace(spec, kind=spec.kind.removeprefix("hadamard_")),
372
+ codebook_values=codebook_values,
373
+ )
374
+ if spec.kind == "aw_ternary":
375
+ return _representation_bits(
376
+ numel, replace(spec, kind="ternary"), codebook_values=codebook_values
377
+ )
378
+ if spec.kind == "sigma_ternary":
379
+ return _representation_bits(
380
+ numel, replace(spec, kind="ternary"), codebook_values=codebook_values
381
+ )
382
+ if spec.kind == "binary":
383
+ groups = math.ceil(numel / spec.group_size)
384
+ return numel + groups * 16
385
+ if spec.kind == "ternary":
386
+ # Five base-3 digits fit in one byte. Account in independently packed groups.
387
+ groups = math.ceil(numel / spec.group_size)
388
+ trit_bytes = groups * math.ceil(spec.group_size / 5)
389
+ return trit_bytes * 8 + groups * 16
390
+ if spec.kind == "sparse_binary":
391
+ groups = math.ceil(numel / spec.group_size)
392
+ position_bits = math.ceil(math.log2(spec.group_size))
393
+ return numel + groups * (16 + spec.outliers * (16 + position_bits))
394
+ if spec.kind == "vector":
395
+ vectors = math.ceil(numel / spec.vector_dim)
396
+ index_bits = int(math.log2(spec.codebook_size))
397
+ return vectors * index_bits * spec.codebooks + codebook_values * 16
398
+ if spec.kind.startswith("int"):
399
+ bits = int(spec.kind.removeprefix("int"))
400
+ groups = math.ceil(numel / spec.group_size)
401
+ return numel * bits + groups * 16
402
+ if spec.kind == "bf16":
403
+ return numel * 16
404
+ raise ValueError(f"unknown extreme quantizer kind: {spec.kind}")
405
+
406
+
407
+ def quantize_tensor(
408
+ weight: torch.Tensor,
409
+ spec: ExtremeSpec,
410
+ *,
411
+ seed: int,
412
+ importance: torch.Tensor | None = None,
413
+ ) -> tuple[torch.Tensor, int, dict[str, Any]]:
414
+ factor_values = 0
415
+ if spec.kind.startswith("hadamard_"):
416
+ restored, codebook_values = _hadamard_quantize(weight, spec)
417
+ elif spec.kind == "binary":
418
+ restored = _binary(weight, spec)
419
+ codebook_values = 0
420
+ elif spec.kind == "ternary":
421
+ restored = _ternary(weight, spec)
422
+ codebook_values = 0
423
+ elif spec.kind == "aw_ternary":
424
+ restored = _activation_weighted_ternary(weight, spec, importance)
425
+ codebook_values = 0
426
+ elif spec.kind == "sigma_ternary":
427
+ restored = _sigma_delta_ternary(weight, spec, importance)
428
+ codebook_values = 0
429
+ elif spec.kind == "srr_ternary":
430
+ restored, factor_values = _structured_residual_ternary(weight, spec, seed=seed)
431
+ codebook_values = 0
432
+ elif spec.kind == "sparse_binary":
433
+ restored = _sparse_binary(weight, spec)
434
+ codebook_values = 0
435
+ elif spec.kind == "vector":
436
+ restored, codebook_values = _vector_additive(weight, spec, seed=seed)
437
+ elif spec.kind.startswith("int"):
438
+ bits = int(spec.kind.removeprefix("int"))
439
+ restored = quantize_dequantize(
440
+ weight, QuantSpec(pattern=".*", bits=bits, group_size=spec.group_size)
441
+ )
442
+ codebook_values = 0
443
+ elif spec.kind == "bf16":
444
+ restored = weight
445
+ codebook_values = 0
446
+ else:
447
+ raise ValueError(f"unknown extreme quantizer kind: {spec.kind}")
448
+
449
+ delta = weight.float() - restored.float()
450
+ denominator = weight.float().square().sum().sqrt().clamp_min(1e-12)
451
+ info = {
452
+ "relative_weight_error": float(delta.square().sum().sqrt() / denominator),
453
+ "max_abs_error": float(delta.abs().max()),
454
+ "codebook_values": codebook_values,
455
+ "factor_values": factor_values,
456
+ "activation_weighted": spec.kind == "aw_ternary" and importance is not None,
457
+ "error_feedback": spec.kind == "sigma_ternary" and importance is not None,
458
+ }
459
+ if spec.kind == "srr_ternary":
460
+ bits = _representation_bits(
461
+ weight.numel(), replace(spec, kind="ternary"), codebook_values=0
462
+ ) + factor_values * 16
463
+ else:
464
+ bits = _representation_bits(weight.numel(), spec, codebook_values=codebook_values)
465
+ return restored, bits, info
466
+
467
+
468
+ def apply_extreme_recipe(
469
+ model: torch.nn.Module,
470
+ specs: dict[str, ExtremeSpec],
471
+ *,
472
+ device: str,
473
+ stored_vocab_rows: int,
474
+ stored_vocab_ids: Sequence[int] | None = None,
475
+ input_second_moments: dict[str, torch.Tensor] | None = None,
476
+ seed: int = 6013,
477
+ ) -> dict[str, Any]:
478
+ """Quantize the text model in place and return physical representation accounting."""
479
+ excluded = re.compile(EXCLUDED)
480
+ protected = re.compile(PROTECTED.pattern)
481
+ seen: set[int] = set()
482
+ tensors: list[dict[str, Any]] = []
483
+ component_bits: dict[str, int] = {}
484
+ component_params: dict[str, int] = {}
485
+ total_bits = 0
486
+ total_counted_params = 0
487
+ weighted_squared_error = 0.0
488
+ weighted_squared_norm = 0.0
489
+ vocab_ids = None
490
+ sensitivity_hits = 0
491
+ if stored_vocab_ids is not None:
492
+ vocab_ids = torch.tensor(sorted(set(int(value) for value in stored_vocab_ids)))
493
+ if vocab_ids.numel() != stored_vocab_rows:
494
+ raise ValueError(
495
+ "stored_vocab_ids must contain exactly stored_vocab_rows unique ids: "
496
+ f"ids={vocab_ids.numel()}, rows={stored_vocab_rows}"
497
+ )
498
+
499
+ with torch.no_grad():
500
+ for name, param in model.named_parameters():
501
+ if excluded.search(name):
502
+ continue
503
+ pointer = param.data_ptr()
504
+ if pointer in seen:
505
+ continue
506
+ seen.add(pointer)
507
+
508
+ component = _component(name)
509
+ spec = ExtremeSpec("bf16") if protected.search(name) else specs.get(
510
+ component, ExtremeSpec("bf16")
511
+ )
512
+ target = param.data.to(device) if spec.kind != "bf16" else param.data
513
+ original = target.float()
514
+ module_name = name.removesuffix(".weight")
515
+ importance = None
516
+ if input_second_moments is not None and module_name in input_second_moments:
517
+ importance = input_second_moments[module_name].to(target.device)
518
+ sensitivity_hits += 1
519
+ restored, bits, info = quantize_tensor(
520
+ target,
521
+ spec,
522
+ seed=_seed_for(name, seed),
523
+ importance=importance,
524
+ )
525
+ if component == "embed" and param.ndim == 2 and stored_vocab_rows < param.shape[0]:
526
+ if vocab_ids is None:
527
+ raise ValueError(
528
+ "a compact embedding claim requires exact stored_vocab_ids; "
529
+ "output masking alone does not reconstruct omitted input rows"
530
+ )
531
+ if int(vocab_ids[0]) < 0 or int(vocab_ids[-1]) >= param.shape[0]:
532
+ raise ValueError("stored vocabulary ids are outside the embedding table")
533
+ keep_mask = torch.zeros(param.shape[0], dtype=torch.bool, device=restored.device)
534
+ keep_mask[vocab_ids.to(restored.device)] = True
535
+ restored[~keep_mask] = 0
536
+ if spec.kind != "bf16" or (
537
+ component == "embed" and param.ndim == 2 and stored_vocab_rows < param.shape[0]
538
+ ):
539
+ param.data.copy_(restored.to(param.device))
540
+
541
+ counted_params = param.numel()
542
+ # The physical A artifact will store selected original-id rows only.
543
+ # Quantize the full table for an input-compatible diagnostic, but do
544
+ # not charge rows that the compact representation omits.
545
+ if component == "embed" and param.ndim == 2:
546
+ counted_params = min(stored_vocab_rows, param.shape[0]) * param.shape[1]
547
+ ratio = counted_params / param.numel()
548
+ bits = math.ceil(bits * ratio)
549
+
550
+ component_bits[component] = component_bits.get(component, 0) + bits
551
+ component_params[component] = component_params.get(component, 0) + counted_params
552
+ total_bits += bits
553
+ total_counted_params += counted_params
554
+ weighted_squared_error += float((original - restored.float()).square().sum())
555
+ weighted_squared_norm += float(original.square().sum())
556
+ tensors.append(
557
+ {
558
+ "name": name,
559
+ "component": component,
560
+ "shape": list(param.shape),
561
+ "spec": asdict(spec),
562
+ "counted_params": counted_params,
563
+ "representation_bytes": math.ceil(bits / 8),
564
+ **info,
565
+ }
566
+ )
567
+
568
+ return {
569
+ "format": "eaimath-extreme-screen-v1",
570
+ "stored_vocab_rows": stored_vocab_rows,
571
+ "omitted_embedding_rows_zeroed": vocab_ids is not None,
572
+ "total_params_in_representation": total_counted_params,
573
+ "total_bytes": math.ceil(total_bits / 8),
574
+ "total_gb": total_bits / 8e9,
575
+ "effective_bits_per_weight": total_bits / total_counted_params,
576
+ "relative_weight_error": math.sqrt(weighted_squared_error / weighted_squared_norm),
577
+ "component_bytes": {name: math.ceil(bits / 8) for name, bits in component_bits.items()},
578
+ "component_params": component_params,
579
+ "specs": {name: asdict(spec) for name, spec in specs.items()},
580
+ "sensitivity_tensors_used": sensitivity_hits,
581
+ "tensors": tensors,
582
+ }
src/eaimath/gauge.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact MLP gauge transformations used as quantization preconditioners.
2
+
3
+ For a gated MLP ``down(silu(gate(x)) * up(x))``, every intermediate channel has
4
+ an exact positive scaling symmetry: multiply one row of ``up`` by ``c`` and the
5
+ matching column of ``down`` by ``1/c``. Gauge fixing chooses ``c`` so the two
6
+ channel norms are balanced before low-bit compression. The factors are fused
7
+ into the weights, so neither the compact artifact nor inference stores them.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from typing import Any
14
+
15
+ import torch
16
+ import torch.nn.functional as F
17
+
18
+
19
+ def balance_up_down(
20
+ up: torch.Tensor,
21
+ down: torch.Tensor,
22
+ *,
23
+ min_scale: float = 1 / 16,
24
+ max_scale: float = 16,
25
+ ) -> tuple[torch.Tensor, torch.Tensor, dict[str, Any]]:
26
+ """Return a function-equivalent, norm-balanced pair of MLP weights."""
27
+ if up.ndim != 2 or down.ndim != 2 or up.shape[0] != down.shape[1]:
28
+ raise ValueError(f"incompatible up/down shapes: {tuple(up.shape)}, {tuple(down.shape)}")
29
+ up32 = up.float()
30
+ down32 = down.float()
31
+ up_rms = up32.square().mean(dim=1).sqrt().clamp_min(1e-12)
32
+ down_rms = down32.square().mean(dim=0).sqrt().clamp_min(1e-12)
33
+ scale = torch.sqrt(down_rms / up_rms).clamp(min_scale, max_scale)
34
+ balanced_up = up32 * scale[:, None]
35
+ balanced_down = down32 / scale[None, :]
36
+ before_ratio = torch.maximum(up_rms / down_rms, down_rms / up_rms)
37
+ after_up = balanced_up.square().mean(dim=1).sqrt().clamp_min(1e-12)
38
+ after_down = balanced_down.square().mean(dim=0).sqrt().clamp_min(1e-12)
39
+ after_ratio = torch.maximum(after_up / after_down, after_down / after_up)
40
+ quantiles = torch.tensor([0.01, 0.5, 0.99], device=scale.device)
41
+ scale_quantiles = torch.quantile(scale, quantiles).cpu().tolist()
42
+ report = {
43
+ "channels": scale.numel(),
44
+ "scale_min": float(scale.min()),
45
+ "scale_p01": scale_quantiles[0],
46
+ "scale_median": scale_quantiles[1],
47
+ "scale_p99": scale_quantiles[2],
48
+ "scale_max": float(scale.max()),
49
+ "mean_norm_ratio_before": float(before_ratio.mean()),
50
+ "mean_norm_ratio_after": float(after_ratio.mean()),
51
+ "clamped_low": int((scale == min_scale).sum()),
52
+ "clamped_high": int((scale == max_scale).sum()),
53
+ "payload_bytes": 0,
54
+ }
55
+ return balanced_up.to(up.dtype), balanced_down.to(down.dtype), report
56
+
57
+
58
+ def gated_mlp_output(
59
+ inputs: torch.Tensor,
60
+ gate: torch.Tensor,
61
+ up: torch.Tensor,
62
+ down: torch.Tensor,
63
+ ) -> torch.Tensor:
64
+ return (F.silu(inputs @ gate.T) * (inputs @ up.T)) @ down.T
65
+
66
+
67
+ def apply_mlp_gauge(
68
+ model: torch.nn.Module,
69
+ *,
70
+ device: str,
71
+ verification_vectors: int = 2,
72
+ seed: int = 6013,
73
+ ) -> dict[str, Any]:
74
+ """Fuse the exact up/down gauge into every dense text MLP in ``model``."""
75
+ rows = []
76
+ generator = torch.Generator(device="cpu")
77
+ generator.manual_seed(seed)
78
+ with torch.no_grad():
79
+ for name, module in model.named_modules():
80
+ if "visual" in name or "mtp" in name:
81
+ continue
82
+ if not all(hasattr(module, attr) for attr in ("gate_proj", "up_proj", "down_proj")):
83
+ continue
84
+ gate_param = module.gate_proj.weight
85
+ up_param = module.up_proj.weight
86
+ down_param = module.down_proj.weight
87
+ gate = gate_param.detach().to(device)
88
+ up = up_param.detach().to(device)
89
+ down = down_param.detach().to(device)
90
+ inputs = torch.randn(
91
+ verification_vectors,
92
+ up.shape[1],
93
+ generator=generator,
94
+ dtype=torch.float32,
95
+ ).to(device=device, dtype=up.dtype)
96
+ before = gated_mlp_output(inputs, gate, up, down).float()
97
+ balanced_up, balanced_down, report = balance_up_down(up, down)
98
+ after = gated_mlp_output(inputs, gate, balanced_up, balanced_down).float()
99
+ denominator = before.square().sum().sqrt().clamp_min(1e-12)
100
+ report.update(
101
+ {
102
+ "module": name,
103
+ "up_shape": list(up.shape),
104
+ "down_shape": list(down.shape),
105
+ "verification_relative_l2": float(
106
+ (before - after).square().sum().sqrt() / denominator
107
+ ),
108
+ "verification_max_abs": float((before - after).abs().max()),
109
+ }
110
+ )
111
+ up_param.copy_(balanced_up.to(up_param.device, dtype=up_param.dtype))
112
+ down_param.copy_(balanced_down.to(down_param.device, dtype=down_param.dtype))
113
+ if getattr(module.up_proj, "bias", None) is not None:
114
+ bias = module.up_proj.bias
115
+ # Qwen uses bias-free MLPs, but preserve the symmetry for any
116
+ # compatible checkpoint that does carry an up-projection bias.
117
+ up_rms = up.float().square().mean(dim=1).sqrt().clamp_min(1e-12)
118
+ down_rms = down.float().square().mean(dim=0).sqrt().clamp_min(1e-12)
119
+ scale = torch.sqrt(down_rms / up_rms).clamp(1 / 16, 16)
120
+ bias.mul_(scale.to(bias.device, dtype=bias.dtype))
121
+ rows.append(report)
122
+
123
+ if not rows:
124
+ raise RuntimeError("no dense gate/up/down MLP modules were found")
125
+ squared = sum(row["verification_relative_l2"] ** 2 for row in rows)
126
+ return {
127
+ "format": "eaimath-mlp-gauge-v1",
128
+ "method": "exact positive up/down channel gauge balancing",
129
+ "modules": len(rows),
130
+ "payload_bytes": 0,
131
+ "rms_verification_relative_l2": math.sqrt(squared / len(rows)),
132
+ "max_verification_relative_l2": max(
133
+ row["verification_relative_l2"] for row in rows
134
+ ),
135
+ "rows": rows,
136
+ }
src/eaimath/generate.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Batched generation with the bookkeeping our metrics need.
2
+
3
+ ``transformers.generate`` gives back padded token ids and nothing about *why*
4
+ each sequence stopped. For a thinking model that distinction is the whole game:
5
+ a sequence that ran out of budget is a different failure from one that finished
6
+ and got the answer wrong, and only the first is fixable by shortening CoT.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Sequence
13
+
14
+ import torch
15
+ from tqdm.auto import tqdm
16
+
17
+
18
+ @dataclass
19
+ class GenerationOutput:
20
+ text: str
21
+ num_prompt_tokens: int
22
+ num_generated_tokens: int
23
+ finished: bool # True = emitted EOS; False = hit max_new_tokens
24
+
25
+
26
+ def build_chat_prompts(tokenizer, prompts: Sequence[str], enable_thinking: bool | None = None):
27
+ """Apply the chat template, passing ``enable_thinking`` only if supported."""
28
+ kwargs = {"tokenize": False, "add_generation_prompt": True}
29
+ if enable_thinking is not None:
30
+ kwargs["enable_thinking"] = enable_thinking
31
+
32
+ messages = [[{"role": "user", "content": p}] for p in prompts]
33
+ try:
34
+ return [tokenizer.apply_chat_template(m, **kwargs) for m in messages]
35
+ except TypeError:
36
+ kwargs.pop("enable_thinking", None)
37
+ return [tokenizer.apply_chat_template(m, **kwargs) for m in messages]
38
+
39
+
40
+ @torch.inference_mode()
41
+ def generate(
42
+ model,
43
+ tokenizer,
44
+ prompts: Sequence[str],
45
+ *,
46
+ max_new_tokens: int = 65536,
47
+ temperature: float = 0.0,
48
+ top_p: float = 0.95,
49
+ top_k: int = 20,
50
+ repetition_penalty: float = 1.0,
51
+ batch_size: int = 8,
52
+ enable_thinking: bool | None = None,
53
+ desc: str = "generating",
54
+ ) -> list[GenerationOutput]:
55
+ chat_texts = build_chat_prompts(tokenizer, prompts, enable_thinking)
56
+ do_sample = temperature > 0
57
+ eos_ids = _eos_token_ids(model, tokenizer)
58
+
59
+ gen_kwargs = {
60
+ "max_new_tokens": max_new_tokens,
61
+ "pad_token_id": tokenizer.pad_token_id,
62
+ "do_sample": do_sample,
63
+ }
64
+ # HF has no presence_penalty; repetition_penalty is the available analogue.
65
+ if repetition_penalty != 1.0:
66
+ gen_kwargs["repetition_penalty"] = repetition_penalty
67
+ if do_sample:
68
+ gen_kwargs.update(temperature=temperature, top_p=top_p, top_k=top_k)
69
+
70
+ results: list[GenerationOutput] = []
71
+ progress = tqdm(total=len(chat_texts), desc=desc, unit="ex")
72
+ index = 0
73
+ current_batch = batch_size
74
+
75
+ try:
76
+ while index < len(chat_texts):
77
+ batch = chat_texts[index : index + current_batch]
78
+ try:
79
+ results.extend(
80
+ _run_batch(model, tokenizer, batch, gen_kwargs, eos_ids)
81
+ )
82
+ except Exception as exc:
83
+ # A shared box means free VRAM changes under us, and long-CoT KV
84
+ # cache grows with max_new_tokens. Back off rather than losing
85
+ # an hour of completed work -- but only for genuine OOM; anything
86
+ # else is a real bug and must surface.
87
+ if not _is_oom(exc) or current_batch == 1:
88
+ raise
89
+ torch.cuda.empty_cache()
90
+ current_batch = max(1, current_batch // 2)
91
+ progress.write(f" OOM -> retrying at batch size {current_batch}")
92
+ continue
93
+ index += len(batch)
94
+ progress.update(len(batch))
95
+ finally:
96
+ progress.close()
97
+
98
+ return results
99
+
100
+
101
+ _OOM_TYPES = tuple(
102
+ err
103
+ for err in (
104
+ getattr(torch, "OutOfMemoryError", None),
105
+ getattr(torch.cuda, "OutOfMemoryError", None),
106
+ )
107
+ if err is not None
108
+ )
109
+
110
+
111
+ def _is_oom(exc: BaseException) -> bool:
112
+ """True for out-of-memory failures only, across torch versions."""
113
+ if _OOM_TYPES and isinstance(exc, _OOM_TYPES):
114
+ return True
115
+ return isinstance(exc, RuntimeError) and "out of memory" in str(exc).lower()
116
+
117
+
118
+ @torch.inference_mode()
119
+ def _run_batch(model, tokenizer, batch, gen_kwargs, eos_ids) -> list[GenerationOutput]:
120
+ inputs = tokenizer(batch, return_tensors="pt", padding=True, truncation=False)
121
+ inputs = {k: v.to(model.device) for k, v in inputs.items()}
122
+ prompt_len = inputs["input_ids"].shape[1]
123
+
124
+ outputs = model.generate(**inputs, **gen_kwargs)
125
+
126
+ out: list[GenerationOutput] = []
127
+ for row, prompt_tokens in zip(outputs, inputs["attention_mask"].sum(dim=1).tolist()):
128
+ generated = row[prompt_len:]
129
+ num_generated, finished = _measure_completion(generated, eos_ids)
130
+ out.append(
131
+ GenerationOutput(
132
+ text=tokenizer.decode(generated[:num_generated], skip_special_tokens=True),
133
+ num_prompt_tokens=int(prompt_tokens),
134
+ num_generated_tokens=num_generated,
135
+ finished=finished,
136
+ )
137
+ )
138
+ return out
139
+
140
+
141
+ def _eos_token_ids(model, tokenizer) -> set[int]:
142
+ ids: set[int] = set()
143
+ config_eos = getattr(getattr(model, "generation_config", None), "eos_token_id", None)
144
+ for source in (config_eos, tokenizer.eos_token_id):
145
+ if isinstance(source, int):
146
+ ids.add(source)
147
+ elif isinstance(source, (list, tuple)):
148
+ ids.update(int(i) for i in source)
149
+ return ids
150
+
151
+
152
+ def _measure_completion(generated: torch.Tensor, eos_ids: set[int]) -> tuple[int, bool]:
153
+ """Length up to and including the first EOS, and whether one was found.
154
+
155
+ Sequences that finish early are right-padded by ``generate``, so scanning for
156
+ the first EOS is what separates a real stop from budget exhaustion.
157
+ """
158
+ if eos_ids:
159
+ for position, token in enumerate(generated.tolist()):
160
+ if token in eos_ids:
161
+ return position + 1, True
162
+ return int(generated.shape[0]), False
src/eaimath/gptq_backend.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GPTQModel loading adapter for the existing Hugging Face evaluation loop.
2
+
3
+ GPTQModel's public object deliberately wraps the underlying Transformers model.
4
+ It exposes ``generate`` but does not promise every attribute that
5
+ ``eaimath.generate`` uses (notably ``device`` and ``generation_config``). This
6
+ small adapter keeps that compatibility boundary in one place.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ import torch
14
+
15
+
16
+ class GPTQGenerationAdapter:
17
+ """Expose GPTQModel through the subset of ``PreTrainedModel`` we evaluate."""
18
+
19
+ def __init__(self, wrapped: Any, device: str):
20
+ self.wrapped = wrapped
21
+ self.device = torch.device(device)
22
+ inner = getattr(wrapped, "model", None)
23
+ self.generation_config = getattr(inner, "generation_config", None)
24
+
25
+ def generate(self, *args, **kwargs):
26
+ return self.wrapped.generate(*args, **kwargs)
27
+
28
+
29
+ def load_gptq_for_generation(model_path: str, device: str) -> GPTQGenerationAdapter:
30
+ """Load a packed GPTQModel checkpoint on one device for deterministic eval."""
31
+ try:
32
+ from gptqmodel import GPTQModel
33
+ except ImportError as exc:
34
+ raise RuntimeError(
35
+ "The gptqmodel backend is not installed. Run "
36
+ "`./scripts/setup_round8.sh` in the project environment."
37
+ ) from exc
38
+
39
+ wrapped = GPTQModel.load(model_path, device=device)
40
+ return GPTQGenerationAdapter(wrapped, device)
src/eaimath/ledger.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The results ledger: one row per checkpoint, not per experiment.
2
+
3
+ Storage is **one small file per checkpoint**, under `experiments/records/`.
4
+
5
+ This is the third design. A single JSON array conflicted whenever two machines
6
+ recorded anything. Append-only JSONL was better but still conflicted, because
7
+ both sides append at the same end-of-file position — verified with
8
+ `git merge-file`, which reported a conflict even though the two added rows were
9
+ different. One file per checkpoint is the version that actually works: two
10
+ machines recording different checkpoints create different files, which git
11
+ merges without even noticing. Recording the *same* checkpoint twice still
12
+ conflicts, and should — that is a real collision.
13
+
14
+ `RESULTS.md` is a **build artifact and is gitignored**, fully derived from these
15
+ records. Rebuild or print it with `python scripts/results.py`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ RECORDS = Path("experiments/records") # one JSON file per checkpoint
25
+ RESULTS_MD = Path("RESULTS.md") # derived, gitignored
26
+
27
+ # Commentary lives in code so nobody has to hand-edit a generated file.
28
+ # The reference every result is quoted against. It must be a row that a run
29
+ # directory can produce: the previous reference claimed 0.950 at rp=1.10, matched
30
+ # no run on any machine, and disagreed with both measurements that did exist.
31
+ BASELINE = "bf16_rp120"
32
+
33
+ CURATED_NOTES: dict[str, str] = {
34
+ "bf16_rp120": "**REFERENCE** bf16 at its actual optimum, rp=1.20. acc|finished 1.000",
35
+ "bf16_rp110": "bf16 at rp=1.10 — identical to w16 on all three metrics, so the "
36
+ "quantize/save/load path is exactly lossless",
37
+ "bf16-greedy-nopenalty": "same weights, no penalty — decoding is worth ~+30 pts",
38
+ "w16": "identity check: quantize/save/eval path is lossless",
39
+ "mlp4": "Red Hat's recipe (GDN untouched). No loss vs w16.",
40
+ "mlp4_attn4": "full_attn@4 costs 21 pts for 0.44 GB — worst trade found",
41
+ "mlp4_lin8": "linear_attn@8 free on top of attn@4 — GDN survives INT8",
42
+ "all4_lin8": "embed@4 looked free here (−3 pts); round 4 revised this",
43
+ "mlp4_lin4": "linear_attn@4 cliff — later shown to be a full_attn@4 artifact",
44
+ "all4": "acc|finished 1.000 — the loss is pure truncation",
45
+ "mlp3_lin8": "mlp@3 cliff (RTN; untested with a calibrated quantizer)",
46
+ "mlp3_lin4": "98% truncated, yet acc|finished still 1.000",
47
+ "all3": "collapsed — this is the RTN row of the published recovery ladder",
48
+ "mlp2_lin8": "distinct failure: gibberish, stops at 1k tokens, 0% parse",
49
+ "m4l16a16e4": "embed@4 free here; GDN and attention untouched",
50
+ "m4l8a16e4": "linear_attn@8 free AGAIN (+0.010) for −1.00 GB",
51
+ "m4l8a8e8": "3.14 GB / 0.890 at rp=1.10",
52
+ "m4l8a8e4": "acc|finished 0.988 — the 8 pt gap was all truncation",
53
+ "m4l4a8e4": "GDN@4 at rp=1.10",
54
+ "m3l8a8e4": "MLP cliff between 4 and 3 bits (RTN)",
55
+ "m4l8a8e4_rp12": "2.83 GB at w16 accuracy — rp=1.20 not 1.10, zero bytes",
56
+ "m4l8a8e4_g64": "g=64 does not help (−0.030) and costs +0.06 GB",
57
+ "m4l8a16e4_g64": "g=64 does not help (−0.080)",
58
+ "m4l8a8e8_g64": "g=64 flat (+0.010) for +0.07 GB — finer groups rejected",
59
+ "m4l6a8e4_g64": "linear_attn@6 at g=64",
60
+ "m5l6a8e4_rp12": "**BEST OVERALL** 2.86 GB / 0.950 = bf16 exactly, 2.94x, fewer tokens than bf16",
61
+ "m5l6a8e8_rp12": "3.18 GB / 0.950 — same score as embed@4 above: embed is free once mlp is 5",
62
+ "m4l8a8e8_rp12": "0.890 → 0.920 from rp alone; mlp@4 caps out here regardless of the other bits",
63
+ "m4l6a8e8_rp12": "2.89 GB / 0.920 — superseded by m5l6a8e4: fewer bytes AND 3 pts more",
64
+ "m4l5a8e4_rp12": "2.45 GB / 0.860 — GDN survives 5 bits",
65
+ "m4l6a8e4_rp12": "2.57 GB / 0.840 — embed@4 costs ~8 pts vs embed@8 here",
66
+ "m4l4a8e4_rp12": "0.670 → 0.750 purely from retuning the penalty",
67
+ # Round 7 — the overnight sweep on buckets A and B.
68
+ "all3_rp13": "rp=1.30 made truncation WORSE (1.000) — past the useful range",
69
+ "all3_rp14": "trunc 0.900, still 0.000 — finishing more does not mean answering",
70
+ "all3_rp15": "trunc 0.660 and tokens −23%, accuracy STILL 0.000. Law 1 breaks here",
71
+ "m3l6a8e8_rp12": "mlp@3, everything else healthy: **0.300** — damaged, NOT dead",
72
+ "m3l6a8e8_rp135": "same weights at rp=1.35: 0.300 → 0.030. The penalty optimum has a ceiling",
73
+ "m3l3a8e8_rp12": "0.000 — adding GDN@3 on top of mlp@3 is what actually kills it",
74
+ "m3l3a8e8_g64_rp12": "g=64 does not rescue 3-bit either — 0.000",
75
+ "m4l6a8e4_rp12_sh2": "2x MLP sharing, no recovery: 0.000. Even the gentle factor collapses",
76
+ "m4l6a8e4_rp12_sh4": "4x MLP sharing, no recovery: 0.000 at trunc 0.46 — finishes, wrong",
77
+ "m5l6a8e4_rp12_v64k": "INVALID — probe read the wrong JSON key and kept only ~4k tokens",
78
+ "m5l6a8e4_rp12_v32k": "INVALID — same bug as the 64k row",
79
+ # Round 8 — calibrated GPTQ and clean BF16 sharing controls.
80
+ "gptq_mlp3_gar_rp12": "GPTQ MLP@3 with GAR: 0.870; 3-bit weights are healthy under calibration",
81
+ "gptq_mlp3_descact_rp12": "GPTQ MLP@3 with desc_act: ties GAR at 0.870",
82
+ "gptq_m3l4a8e8_gar_rp12": "**BEST GPTQ PROBE** 2.36 GB / 0.910; only 3 pts below re-measured bf16",
83
+ "gptq_m3l4a6e4_gar_rp12": "1.97 GB / 0.860; +30k vocab projects to 1.678 GB but leaves only 4 MB slack",
84
+ "gptq_m3l3a8e8_gar_rp12": "**B PRECURSOR** exact RTN recipe rose 0.000→0.860; +30k vocab projects to 1.664 GB",
85
+ "w16_sh2_mean_rp12": "BF16 2x MLP sharing, mean init: 0.000. Quantization was not the cause",
86
+ "w16_sh2_first_rp12": "BF16 2x MLP sharing, first init: finishes 77%, still 0.000",
87
+ "w16_sh4_mean_rp12": "BF16 4x MLP sharing, mean init: 0.000; untrained sharing closed",
88
+ # Round 9 — physical Bucket-B artifact and embedding-row controls.
89
+ "b0_zero_rp12": "**PHYSICAL B CANDIDATE** 1.664 GB / 0.780; exact GPTQ codes + 30k stored rows",
90
+ "b0_zero_checkpoint_rp12": "same physical B0 on n=560: 0.786, acc|finished 0.971",
91
+ "b0_zero_nomask_n30_rp12": "n=30 deployment control without an explicit output keep-set: ties zero fill",
92
+ "b0_mean_n30_rp12": "submission-valid mean omitted-row fill: no gain over zero on n=30",
93
+ "b0_base_n30_rp12": "INVALID diagnostic: original omitted rows improve 0.767→0.833 on n=30",
94
+ # Round 10 — B vocabulary/decoding plateau and A delta-sharing capacity.
95
+ "r10_b1_pf30_rp115": "30k problem-first physical B, rp=1.15: pure termination loss",
96
+ "r10_b1_pf30_rp120": "30k problem-first physical B, rp=1.20; no material gain over Round 9",
97
+ "r10_b1_pf30_rp125": "30k physical B over-penalized: 0.800→0.670",
98
+ "r10_b2_pf36_rp115": "36k problem-first physical B, rp=1.15",
99
+ "r10_b2_pf36_rp120": "**ROUND 10 GATE WINNER** 1.680 GB / 0.810; must defer to n=560",
100
+ "r10_b2_pf36_rp125": "lower truncation, but acc|finished collapses 0.988→0.860",
101
+ "r10_b2_pf36_checkpoint_rp120": "**PHYSICAL B FRONTIER** n=560 0.787: one problem above Round 9, effectively tied",
102
+ "r10_a_delta_f2_r64_n30": "factor 2/rank 64 delta sharing: finishes 43%, still 0/30",
103
+ "r10_a_delta_f8_r64_n30": "factor 8/rank 64 delta sharing: 100% truncation, 0/30",
104
+ "r10_a_delta_f8_r32_n30": "factor 8/rank 32 delta sharing: 100% truncation, 0/30",
105
+ "r10_a_delta_f8_r16_n30": "factor 8/rank 16 size endpoint: 100% truncation, 0/30",
106
+ # Round 12 — learned cross-band transfer and creative controls.
107
+ "r12_short2500_c_checkpoint": "**NEW C FRONTIER** short2500 + m5l6a8e4: n=560 0.916",
108
+ "r12_balanced6000_c_checkpoint": "balanced learned source: healthy 0.914, below short2500",
109
+ "r12_balanced6000_gauge_c_checkpoint": "gauge cuts truncation but trades conditional correctness; accuracy stays 0.914",
110
+ "r12_short2500_gauge_c_checkpoint": "exact SwiGLU gauge control: 0.909 vs ordinary short C 0.916",
111
+ "r12_hadamard_c_checkpoint": "fixed-transform codec: 0.887; rejected",
112
+ "r12_paro_int4_checkpoint": "public Paro method control: 0.886 and repository outside C",
113
+ "r12_short2500_b_zero_checkpoint": "short-source physical B zero fill: n=560 0.814",
114
+ "r12_short2500_b_token_checkpoint": "**NEW B FRONTIER** token predictor: n=560 0.834 vs zero 0.814",
115
+ }
116
+
117
+ HEADER = """# Results ledger
118
+
119
+ One row per **checkpoint**. Sweeps and ablations live in `experiments/`.
120
+
121
+ **Generated file — do not edit.** Rebuild with `python scripts/results.py`.
122
+ Source of truth is one file per checkpoint under `experiments/records/`, so two
123
+ machines recording different results never touch the same file.
124
+
125
+ Eval: `gate` (MATH-500 levels 4-5, n=100), 32,768-token cap, greedy. Size is the
126
+ compressed size from the quantization recipe; `pack.py` verifies those are real
127
+ bytes to 0.0%.
128
+
129
+ Noise floor ~±5 points unpaired — use `scripts/05_compare_runs.py` for paired
130
+ McNemar before believing a small delta.
131
+
132
+ """
133
+
134
+ COLUMNS = [
135
+ ("checkpoint", "checkpoint", "s"),
136
+ ("size GB", "size_gb", ".2f"),
137
+ ("bpw", "bits_per_weight", ".2f"),
138
+ ("acc", "accuracy", ".3f"),
139
+ ("trunc", "truncation_rate", ".3f"),
140
+ ("mean tok", "mean_generated_tokens", ".0f"),
141
+ ("notes", "notes", "s"),
142
+ ]
143
+
144
+
145
+ def load() -> list[dict[str, Any]]:
146
+ """Every recorded checkpoint, largest first."""
147
+ if not RECORDS.exists():
148
+ return []
149
+ rows = [json.loads(p.read_text()) for p in sorted(RECORDS.glob("*.json"))]
150
+ return sorted(rows, key=lambda r: -(r.get("size_gb") or 0))
151
+
152
+
153
+ def _safe_name(checkpoint: str) -> str:
154
+ return "".join(c if c.isalnum() or c in "-_" else "_" for c in checkpoint)
155
+
156
+
157
+ def record(
158
+ checkpoint: str,
159
+ *,
160
+ size_gb: float,
161
+ bits_per_weight: float,
162
+ summary: dict[str, Any],
163
+ notes: str = "",
164
+ run_name: str = "",
165
+ suite: str = "gate",
166
+ ) -> None:
167
+ """Write one checkpoint's result to its own file.
168
+
169
+ ``suite`` is part of the identity, not decoration. The same recipe evaluated
170
+ on `gate` (n=100) and on `checkpoint` (n=560) produces two different numbers,
171
+ and without this they collide on one filename -- the second silently
172
+ replacing the first in a table whose header promises every row is `gate`.
173
+ Callers must fold the suite into ``checkpoint`` for anything but `gate`.
174
+ """
175
+ RECORDS.mkdir(parents=True, exist_ok=True)
176
+ row = {
177
+ "checkpoint": checkpoint,
178
+ "suite": suite,
179
+ "size_gb": size_gb,
180
+ "bits_per_weight": bits_per_weight,
181
+ "accuracy": summary.get("accuracy"),
182
+ "truncation_rate": summary.get("truncation_rate"),
183
+ "mean_generated_tokens": summary.get("mean_generated_tokens"),
184
+ "notes": notes,
185
+ "run_name": run_name,
186
+ }
187
+ (RECORDS / f"{_safe_name(checkpoint)}.json").write_text(json.dumps(row, indent=2))
188
+
189
+
190
+ def render(rows: list[dict[str, Any]]) -> str:
191
+ head = "| " + " | ".join(name for name, *_ in COLUMNS) + " |"
192
+ align = "|" + "|".join(":--" if f == "s" else "--:" for _, _, f in COLUMNS) + "|"
193
+ lines = [HEADER, head, align]
194
+
195
+ for row in rows:
196
+ cells = []
197
+ for _, key, fmt in COLUMNS:
198
+ value = row.get(key)
199
+ if key == "notes":
200
+ cells.append(CURATED_NOTES.get(row["checkpoint"], str(value or "")) or "—")
201
+ elif value is None:
202
+ cells.append("—")
203
+ elif fmt == "s":
204
+ cells.append(str(value))
205
+ else:
206
+ cells.append(f"{value:{fmt}}")
207
+ lines.append("| " + " | ".join(cells) + " |")
208
+
209
+ baseline = next((r for r in rows if r["checkpoint"] == BASELINE), None)
210
+ if baseline and baseline.get("accuracy") is not None:
211
+ lines += [
212
+ "",
213
+ f"Baseline: **bf16 = {baseline['accuracy']:.3f} acc, "
214
+ f"{baseline['truncation_rate']:.3f} trunc, "
215
+ f"{baseline['mean_generated_tokens']:.0f} tokens, "
216
+ f"{baseline['size_gb']:.2f} GB** "
217
+ f"(`{BASELINE}`, greedy + repetition_penalty=1.20 — bf16's actual optimum).",
218
+ ]
219
+ return "\n".join(lines) + "\n"
220
+
221
+
222
+ def write_results_md() -> Path:
223
+ RESULTS_MD.write_text(render(load()))
224
+ return RESULTS_MD
src/eaimath/model.py ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Loading, inspecting and slimming the Qwen3.5-4B checkpoint.
2
+
3
+ Qwen3.5-4B is a hybrid multimodal model — 24 Gated DeltaNet (linear attention)
4
+ layers interleaved with 8 full-attention layers, plus a 24-block vision tower
5
+ and a multi-token-prediction head. For a text-only math benchmark the vision
6
+ tower and MTP head are never executed, so they are ~455M parameters (9.7% of the
7
+ checkpoint) of pure dead weight.
8
+
9
+ Exact class names and module paths for this architecture vary across
10
+ transformers releases, so everything here introspects the loaded module tree
11
+ rather than hardcoding paths. ``describe_model`` exists to dump ground truth
12
+ before we commit to any of it.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import re
19
+ import shutil
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ import torch
25
+
26
+ DEFAULT_MODEL = "Qwen/Qwen3.5-4B"
27
+
28
+ # First matching pattern wins, so order matters: `visual`/`mtp` must precede the
29
+ # generic `mlp`/`norm` patterns since those submodules contain MLPs too.
30
+ BUDGET_GROUPS: tuple[tuple[str, str], ...] = (
31
+ ("vision", r"visual|vision"),
32
+ ("mtp", r"(^|\.)mtp\."),
33
+ ("embed", r"embed_tokens|lm_head"),
34
+ ("linear_attn", r"linear_attn"),
35
+ ("full_attn", r"self_attn"),
36
+ ("mlp", r"\.mlp\."),
37
+ ("norm", r"norm"),
38
+ )
39
+
40
+ # Tiny but precision-critical: the SSM recurrence dynamics. Keeping every one of
41
+ # these in fp32 costs ~3 MB, and quantizing them is what makes low-bit SSM
42
+ # quantization collapse.
43
+ SSM_SENSITIVE = r"A_log|dt_bias|conv1d|in_proj_a|in_proj_b"
44
+
45
+
46
+ @dataclass
47
+ class GroupStats:
48
+ name: str
49
+ num_tensors: int = 0
50
+ num_params: int = 0 # unique storage only
51
+ num_bytes: int = 0 # unique storage only
52
+ raw_params: int = 0 # counting tied aliases twice
53
+ num_aliases: int = 0
54
+ examples: list[str] = field(default_factory=list)
55
+
56
+ def add(self, name: str, tensor: torch.Tensor, *, counted: bool) -> None:
57
+ self.num_tensors += 1
58
+ self.raw_params += tensor.numel()
59
+ if counted:
60
+ self.num_params += tensor.numel()
61
+ self.num_bytes += tensor.numel() * tensor.element_size()
62
+ else:
63
+ self.num_aliases += 1
64
+ if len(self.examples) < 3:
65
+ self.examples.append(name + (" (tied alias)" if not counted else ""))
66
+
67
+
68
+ def _group_for(name: str) -> str:
69
+ for group, pattern in BUDGET_GROUPS:
70
+ if re.search(pattern, name):
71
+ return group
72
+ return "other"
73
+
74
+
75
+ def parameter_budget(state_dict: dict[str, torch.Tensor]) -> dict[str, Any]:
76
+ """Group every tensor by component and report params / bytes per group.
77
+
78
+ Tied weights (Qwen3.5 ties ``lm_head`` to ``embed_tokens``) appear twice in a
79
+ ``state_dict`` while sharing one allocation. Counting both would overstate
80
+ the checkpoint by 636M params / 1.27 GB here — and checkpoint bytes is the
81
+ metric we are graded on — so aliases are detected by storage pointer and
82
+ counted once.
83
+ """
84
+ groups: dict[str, GroupStats] = {}
85
+ seen_storage: dict[int, str] = {}
86
+ aliases: list[dict[str, str]] = []
87
+
88
+ for name, tensor in state_dict.items():
89
+ if not isinstance(tensor, torch.Tensor):
90
+ continue
91
+ pointer = tensor.data_ptr()
92
+ is_alias = pointer != 0 and pointer in seen_storage
93
+ if is_alias:
94
+ aliases.append({"name": name, "aliases": seen_storage[pointer]})
95
+ else:
96
+ seen_storage[pointer] = name
97
+ groups.setdefault(_group_for(name), GroupStats(_group_for(name))).add(
98
+ name, tensor, counted=not is_alias
99
+ )
100
+
101
+ total_params = sum(g.num_params for g in groups.values())
102
+ total_bytes = sum(g.num_bytes for g in groups.values())
103
+ return {
104
+ "total_params": total_params,
105
+ "total_bytes": total_bytes,
106
+ "total_gb": total_bytes / 1e9,
107
+ "raw_params_with_aliases": sum(g.raw_params for g in groups.values()),
108
+ "tied_aliases": aliases,
109
+ "groups": {
110
+ name: {
111
+ "num_tensors": g.num_tensors,
112
+ "num_params": g.num_params,
113
+ "num_bytes": g.num_bytes,
114
+ "gb": g.num_bytes / 1e9,
115
+ "num_aliases": g.num_aliases,
116
+ "pct_params": 100 * g.num_params / total_params if total_params else 0.0,
117
+ "examples": g.examples,
118
+ }
119
+ for name, g in sorted(groups.items(), key=lambda kv: -kv[1].num_params)
120
+ },
121
+ }
122
+
123
+
124
+ def checkpoint_size_bytes(state_dict: dict[str, torch.Tensor]) -> int:
125
+ """Unique bytes in a state_dict — the number the leaderboard scores."""
126
+ return parameter_budget(state_dict)["total_bytes"]
127
+
128
+
129
+ def format_budget(budget: dict[str, Any]) -> str:
130
+ lines = [
131
+ f"{'component':<16}{'params':>16}{'% params':>11}{'GB':>9}{'tensors':>9}",
132
+ "-" * 61,
133
+ ]
134
+ for name, stats in budget["groups"].items():
135
+ suffix = f" ({stats['num_aliases']} tied)" if stats["num_aliases"] else ""
136
+ lines.append(
137
+ f"{name:<16}{stats['num_params']:>16,}{stats['pct_params']:>10.1f}%"
138
+ f"{stats['gb']:>9.3f}{stats['num_tensors']:>9}{suffix}"
139
+ )
140
+ lines.append("-" * 61)
141
+ lines.append(
142
+ f"{'TOTAL':<16}{budget['total_params']:>16,}{100.0:>10.1f}%"
143
+ f"{budget['total_gb']:>9.3f}"
144
+ )
145
+ if budget.get("tied_aliases"):
146
+ excess = budget["raw_params_with_aliases"] - budget["total_params"]
147
+ lines.append(
148
+ f" ({len(budget['tied_aliases'])} tied alias tensor(s) counted once; "
149
+ f"naive state_dict sum would overstate by {excess:,} params)"
150
+ )
151
+ return "\n".join(lines)
152
+
153
+
154
+ def load_model(
155
+ model_id: str = DEFAULT_MODEL,
156
+ dtype: str = "bfloat16",
157
+ device_map: str | None = "auto",
158
+ cache_dir: str | None = None,
159
+ device: str | None = None,
160
+ multimodal: bool = False,
161
+ ):
162
+ """Load the model, trying each plausible auto-class for this architecture.
163
+
164
+ On transformers >= 5, ``AutoModelForCausalLM`` resolves Qwen3.5 to
165
+ ``Qwen3_5ForCausalLM`` and materializes only the language model — the vision
166
+ tower and MTP head are never allocated, so the 455M-param text-only saving
167
+ happens for free at load time.
168
+
169
+ Pass ``device`` to pin the whole model to one GPU. At 8.4 GB it fits on any
170
+ of ours, and ``device_map="auto"`` across several GPUs would pipeline-shard
171
+ it instead, adding cross-device hops on every layer for no benefit.
172
+ """
173
+ import transformers
174
+
175
+ torch_dtype = {
176
+ "bfloat16": torch.bfloat16,
177
+ "float16": torch.float16,
178
+ "float32": torch.float32,
179
+ }[dtype]
180
+
181
+ kwargs: dict[str, Any] = {
182
+ "dtype": torch_dtype,
183
+ "trust_remote_code": True,
184
+ }
185
+ if device is not None:
186
+ kwargs["device_map"] = {"": device}
187
+ elif device_map is not None:
188
+ kwargs["device_map"] = device_map
189
+ if cache_dir:
190
+ kwargs["cache_dir"] = cache_dir
191
+
192
+ candidates = [
193
+ "AutoModelForCausalLM",
194
+ "AutoModelForImageTextToText",
195
+ "AutoModelForVision2Seq",
196
+ "AutoModel",
197
+ ]
198
+ if multimodal:
199
+ # vLLM only registers Qwen3_5ForConditionalGeneration and rejects a
200
+ # text-only Qwen3_5TextConfig (vllm#39231). Any checkpoint we intend to
201
+ # evaluate with vLLM must therefore keep the multimodal wrapper, even
202
+ # though the vision tower is never executed for a math prompt.
203
+ candidates = [
204
+ "Qwen3_5ForConditionalGeneration",
205
+ "AutoModelForImageTextToText",
206
+ "AutoModelForVision2Seq",
207
+ "AutoModelForCausalLM",
208
+ ]
209
+ errors: list[str] = []
210
+ for class_name in candidates:
211
+ auto_class = getattr(transformers, class_name, None)
212
+ if auto_class is None:
213
+ continue
214
+ try:
215
+ model = auto_class.from_pretrained(model_id, **kwargs)
216
+ model.eval()
217
+ print(f"[model] loaded {model_id} via {class_name}")
218
+ return model
219
+ except Exception as exc: # noqa: BLE001 - we want the full error list
220
+ errors.append(f" {class_name}: {type(exc).__name__}: {exc}")
221
+
222
+ raise RuntimeError(
223
+ f"Could not load {model_id} with any auto-class.\n" + "\n".join(errors)
224
+ )
225
+
226
+
227
+ def load_tokenizer(model_id: str = DEFAULT_MODEL, cache_dir: str | None = None):
228
+ from transformers import AutoTokenizer
229
+
230
+ kwargs: dict[str, Any] = {"trust_remote_code": True}
231
+ if cache_dir:
232
+ kwargs["cache_dir"] = cache_dir
233
+ tokenizer = AutoTokenizer.from_pretrained(model_id, **kwargs)
234
+ if tokenizer.pad_token is None:
235
+ tokenizer.pad_token = tokenizer.eos_token
236
+ tokenizer.padding_side = "left" # required for correct batched generation
237
+ return tokenizer
238
+
239
+
240
+ def estimate_memory(
241
+ model: torch.nn.Module, batch_size: int, seq_len: int
242
+ ) -> dict[str, float]:
243
+ """Predict peak VRAM for a generation run, so OOM is caught before it costs an hour.
244
+
245
+ Only the 8 full-attention layers hold a growing KV cache; the 24 Gated
246
+ DeltaNet layers keep a fixed-size recurrent state regardless of length,
247
+ which is why this model's cache is far cheaper than a dense transformer's.
248
+ """
249
+ config = getattr(model, "config", None)
250
+ config = getattr(config, "text_config", config)
251
+
252
+ layer_types = getattr(config, "layer_types", None)
253
+ num_layers = getattr(config, "num_hidden_layers", 32)
254
+ if layer_types:
255
+ num_full = sum(1 for t in layer_types if "full" in str(t))
256
+ else:
257
+ interval = getattr(config, "full_attention_interval", 4)
258
+ num_full = num_layers // interval
259
+
260
+ kv_heads = getattr(config, "num_key_value_heads", 4)
261
+ head_dim = getattr(config, "head_dim", 256)
262
+ dtype_size = 2
263
+
264
+ kv_bytes_per_token = num_full * kv_heads * head_dim * 2 * dtype_size
265
+ kv_bytes = kv_bytes_per_token * seq_len * batch_size
266
+
267
+ # DynamicCache grows by torch.cat on every decode step: allocate (n+1),
268
+ # copy, free n. Peak is therefore ~2x the steady-state cache, which is
269
+ # exactly the "ladder" you see climbing in nvtop until it OOMs.
270
+ # expandable_segments lets the allocator grow a segment in place, cutting
271
+ # the transient sharply -- but we still budget for it.
272
+ growth_factor = 1.4 if "expandable_segments" in os.environ.get(
273
+ "PYTORCH_CUDA_ALLOC_CONF", ""
274
+ ) else 2.0
275
+ kv_peak_bytes = kv_bytes * growth_factor
276
+
277
+ # Gated DeltaNet recurrent state: [v_heads, k_dim, v_dim] per layer, length-independent.
278
+ v_heads = getattr(config, "linear_num_value_heads", 32)
279
+ k_dim = getattr(config, "linear_key_head_dim", 128)
280
+ v_dim = getattr(config, "linear_value_head_dim", 128)
281
+ state_bytes = (num_layers - num_full) * v_heads * k_dim * v_dim * 4 * batch_size
282
+
283
+ weight_bytes = sum(p.numel() * p.element_size() for p in model.parameters())
284
+
285
+ return {
286
+ "weights_gb": weight_bytes / 1e9,
287
+ "kv_cache_gb": kv_bytes / 1e9,
288
+ "kv_peak_gb": kv_peak_bytes / 1e9,
289
+ "ssm_state_gb": state_bytes / 1e9,
290
+ "steady_gb": (weight_bytes + kv_bytes + state_bytes) / 1e9,
291
+ # What the preflight must check against: peak, not steady state.
292
+ "total_gb": (weight_bytes + kv_peak_bytes + state_bytes) / 1e9,
293
+ "kv_bytes_per_token": kv_bytes_per_token,
294
+ "growth_factor": growth_factor,
295
+ }
296
+
297
+
298
+ def free_vram_gb(device: str | torch.device) -> tuple[float, float]:
299
+ """(free, total) GB on ``device`` — reflects other users' jobs on a shared box."""
300
+ free, total = torch.cuda.mem_get_info(torch.device(device))
301
+ return free / 1e9, total / 1e9
302
+
303
+
304
+ def describe_model(model: torch.nn.Module, max_depth: int = 3) -> dict[str, Any]:
305
+ """Summarize the module tree — the ground truth we build stripping on."""
306
+ tree: list[dict[str, Any]] = []
307
+ for name, module in model.named_modules():
308
+ depth = name.count(".")
309
+ if name and depth <= max_depth:
310
+ n_params = sum(p.numel() for p in module.parameters(recurse=True))
311
+ tree.append(
312
+ {
313
+ "path": name,
314
+ "class": type(module).__name__,
315
+ "depth": depth,
316
+ "num_params": n_params,
317
+ }
318
+ )
319
+
320
+ state_dict = model.state_dict()
321
+ sensitive = [n for n in state_dict if re.search(SSM_SENSITIVE, n)]
322
+ return {
323
+ "model_class": type(model).__name__,
324
+ "num_parameters": sum(p.numel() for p in model.parameters()),
325
+ "module_tree": tree,
326
+ "budget": parameter_budget(state_dict),
327
+ "ssm_sensitive_tensors": {
328
+ "count": len(sensitive),
329
+ "num_params": sum(state_dict[n].numel() for n in sensitive),
330
+ "names": sensitive[:20],
331
+ },
332
+ "state_dict_prefixes": sorted(
333
+ {".".join(n.split(".")[:2]) for n in state_dict}
334
+ ),
335
+ }
336
+
337
+
338
+ def find_submodule(model: torch.nn.Module, pattern: str) -> list[str]:
339
+ """Paths of modules whose name matches ``pattern`` and whose parent does not."""
340
+ regex = re.compile(pattern)
341
+ hits = [name for name, _ in model.named_modules() if name and regex.search(name)]
342
+ # Keep only the shallowest match on each branch.
343
+ return [h for h in hits if not any(h.startswith(o + ".") for o in hits)]
344
+
345
+
346
+ def strip_unused_modules(
347
+ model: torch.nn.Module,
348
+ drop_vision: bool = True,
349
+ drop_mtp: bool = True,
350
+ ) -> dict[str, Any]:
351
+ """Delete text-irrelevant submodules in place; return what was removed.
352
+
353
+ The vision tower and MTP head together are ~455M params. A text-only math
354
+ eval never executes either: the ViT has no image inputs, and the MTP head is
355
+ a speculative-decoding draft head that HF ``generate()`` does not call.
356
+ """
357
+ removed: list[dict[str, Any]] = []
358
+ targets: list[str] = []
359
+ if drop_vision:
360
+ targets.append(r"(^|\.)(visual|vision_tower)$")
361
+ if drop_mtp:
362
+ targets.append(r"(^|\.)mtp$")
363
+
364
+ for pattern in targets:
365
+ for path in find_submodule(model, pattern):
366
+ parent = model
367
+ parts = path.split(".")
368
+ for part in parts[:-1]:
369
+ parent = getattr(parent, part)
370
+ child = getattr(parent, parts[-1], None)
371
+ if child is None:
372
+ continue
373
+ n_params = sum(p.numel() for p in child.parameters(recurse=True))
374
+ setattr(parent, parts[-1], None)
375
+ removed.append({"path": path, "num_params": n_params})
376
+
377
+ # transformers >= 5 loads Qwen3.5 through Qwen3_5ForCausalLM, which never
378
+ # allocates the vision tower or MTP head. Finding nothing to remove is the
379
+ # expected outcome there, not a failure.
380
+ already_text_only = not removed and not find_submodule(model, r"visual|vision_tower|mtp")
381
+
382
+ return {
383
+ "removed": removed,
384
+ "num_params_removed": sum(r["num_params"] for r in removed),
385
+ "bytes_removed_bf16": 2 * sum(r["num_params"] for r in removed),
386
+ "already_text_only": already_text_only,
387
+ }
388
+
389
+
390
+ # Files the model repo carries that `save_pretrained` does not reproduce.
391
+ # vLLM's multimodal path builds an image processor even for a text-only prompt,
392
+ # so a checkpoint missing `preprocessor_config.json` fails to load outright.
393
+ AUXILIARY_FILES = (
394
+ "preprocessor_config.json",
395
+ "video_preprocessor_config.json",
396
+ "chat_template.jinja",
397
+ )
398
+
399
+
400
+ def save_checkpoint(
401
+ model: torch.nn.Module,
402
+ out_dir: str | Path,
403
+ source_model: str = DEFAULT_MODEL,
404
+ cache_dir: str | None = None,
405
+ ) -> list[str]:
406
+ """Write a checkpoint that vLLM and transformers can both load.
407
+
408
+ ``model.save_pretrained`` emits weights + config, and the tokenizer covers
409
+ vocab/merges, but the processor and chat-template files come from the source
410
+ repo and must be copied across explicitly.
411
+ """
412
+ out = Path(out_dir)
413
+ out.mkdir(parents=True, exist_ok=True)
414
+ model.save_pretrained(out)
415
+ load_tokenizer(source_model, cache_dir=cache_dir).save_pretrained(out)
416
+
417
+ copied: list[str] = []
418
+ source_dir = Path(source_model)
419
+ for filename in AUXILIARY_FILES:
420
+ target = out / filename
421
+ if target.exists():
422
+ copied.append(filename)
423
+ continue
424
+ try:
425
+ if source_dir.is_dir():
426
+ candidate = source_dir / filename
427
+ if not candidate.is_file():
428
+ continue
429
+ shutil.copy(candidate, target)
430
+ else:
431
+ from huggingface_hub import hf_hub_download
432
+
433
+ downloaded = hf_hub_download(
434
+ source_model, filename, cache_dir=cache_dir
435
+ )
436
+ shutil.copy(downloaded, target)
437
+ copied.append(filename)
438
+ except Exception as exc: # noqa: BLE001 - optional files; report and continue
439
+ print(f"[save_checkpoint] could not copy {filename}: {exc}")
440
+ return copied
src/eaimath/pack.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Real bit-packing — turning our analytic sizes into actual bytes on disk.
2
+
3
+ Everything so far has used *simulated* quantization: weights rounded onto a
4
+ coarse grid but stored back as bf16, with the compressed size computed
5
+ analytically from the recipe. That is the right tool for research — it isolates
6
+ accuracy effects without committing to a format — but a submission is scored on
7
+ bytes that actually exist.
8
+
9
+ This module closes that gap. `pack_tensor` stores an n-bit quantized tensor as
10
+ a flat `uint8` buffer plus fp16 scales, so `numel * element_size` over the saved
11
+ state dict equals the number we have been reporting. `unpack_tensor` inverts it.
12
+
13
+ Sub-byte widths are packed densely (two 4-bit values per byte, eight 3-bit
14
+ values per three bytes, and so on) rather than padded to a byte, because padding
15
+ would silently inflate 3-bit to 4-bit and make our size claims wrong.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ from typing import Any
22
+
23
+ import numpy as np
24
+ import torch
25
+
26
+ # Packing 2.26B values at once would materialise a ~9 GB bit array, so the
27
+ # general path works in chunks.
28
+ CHUNK = 1 << 24
29
+
30
+
31
+ def _pack_uint8_chunked(values: np.ndarray, bits: int) -> np.ndarray:
32
+ out = []
33
+ for start in range(0, values.size, CHUNK):
34
+ chunk = values[start : start + CHUNK]
35
+ bit_planes = ((chunk[:, None] >> np.arange(bits - 1, -1, -1)) & 1).astype(np.uint8)
36
+ out.append(np.packbits(bit_planes.reshape(-1)))
37
+ return np.concatenate(out) if out else np.zeros(0, dtype=np.uint8)
38
+
39
+
40
+ def _unpack_uint8_chunked(packed: np.ndarray, bits: int, count: int) -> np.ndarray:
41
+ bit_stream = np.unpackbits(packed)[: count * bits].reshape(count, bits)
42
+ weights = (1 << np.arange(bits - 1, -1, -1)).astype(np.uint32)
43
+ return (bit_stream * weights).sum(axis=1).astype(np.uint32)
44
+
45
+
46
+ def pack_bits(values: np.ndarray, bits: int) -> np.ndarray:
47
+ """Pack unsigned integers of width ``bits`` into a dense uint8 buffer."""
48
+ flat = np.ascontiguousarray(values.reshape(-1)).astype(np.uint32)
49
+ if bits == 8:
50
+ return flat.astype(np.uint8)
51
+ if bits == 4:
52
+ # Fast path: two nibbles per byte. Pad to even length.
53
+ padded = np.pad(flat, (0, flat.size % 2)).astype(np.uint8)
54
+ return (padded[0::2] << 4 | padded[1::2]).astype(np.uint8)
55
+ if bits == 16:
56
+ return flat.astype(np.uint16).view(np.uint8)
57
+ # Preserve widths above eight bits. The general bit-plane packer accepts
58
+ # uint32 values; narrowing here silently wrapped 9--15 bit codes.
59
+ return _pack_uint8_chunked(flat, bits)
60
+
61
+
62
+ def unpack_bits(packed: np.ndarray, bits: int, count: int) -> np.ndarray:
63
+ if bits == 8:
64
+ return packed[:count].astype(np.uint32)
65
+ if bits == 4:
66
+ high, low = packed >> 4, packed & 0x0F
67
+ return np.stack([high, low], axis=1).reshape(-1)[:count].astype(np.uint32)
68
+ if bits == 16:
69
+ return packed.view(np.uint16)[:count].astype(np.uint32)
70
+ return _unpack_uint8_chunked(packed, bits, count)
71
+
72
+
73
+ def _store_scale(
74
+ scale: torch.Tensor,
75
+ normalization: str,
76
+ ) -> tuple[torch.Tensor, int]:
77
+ """Encode a positive scale field in two bytes per group.
78
+
79
+ ``pow2`` is a block-floating representation: one exact power-of-two
80
+ exponent is shared by the tensor entry, while every group retains an FP16
81
+ mantissa. It costs the same tensor bytes as raw FP16 scales but prevents
82
+ the tiny scales used by 9--12 bit weights from falling into FP16's
83
+ subnormal range.
84
+ """
85
+ if normalization == "none":
86
+ return scale.reshape(-1).to(torch.float16), 0
87
+ if normalization != "pow2":
88
+ raise ValueError(
89
+ f"unsupported scale normalization {normalization!r}; expected none or pow2"
90
+ )
91
+ maximum = float(scale.detach().abs().max()) if scale.numel() else 0.0
92
+ exponent = 0 if maximum <= 0.0 else -math.floor(math.log2(maximum))
93
+ # A normalized maximum in [1, 2) is comfortably inside FP16's normal
94
+ # range. The exponent is metadata (one small integer per entry), while
95
+ # the charged scale stream remains one FP16 value per group.
96
+ normalized = scale * math.ldexp(1.0, exponent)
97
+ return normalized.reshape(-1).to(torch.float16), int(exponent)
98
+
99
+
100
+ def pack_tensor(
101
+ weight: torch.Tensor,
102
+ bits: int,
103
+ group_size: int = 128,
104
+ symmetric: bool = True,
105
+ scale_normalization: str = "none",
106
+ ) -> dict[str, Any]:
107
+ """Quantize and pack one tensor into buffers whose bytes are the real cost."""
108
+ if bits >= 16:
109
+ return {"kind": "raw", "data": weight.to(torch.bfloat16).cpu()}
110
+
111
+ shape = tuple(weight.shape)
112
+ group = group_size if group_size > 0 and shape[-1] % group_size == 0 else shape[-1]
113
+ flat = weight.detach().float().cpu().reshape(-1, group)
114
+
115
+ if symmetric:
116
+ qmax = 2 ** (bits - 1) - 1
117
+ scale = (flat.abs().amax(dim=1, keepdim=True) / qmax).clamp(min=1e-8)
118
+ q = torch.round(flat / scale).clamp(-qmax - 1, qmax)
119
+ codes = (q + (qmax + 1)).to(torch.int64) # shift to unsigned for packing
120
+ zero = None
121
+ else:
122
+ qmax = 2**bits - 1
123
+ wmin, wmax = flat.amin(dim=1, keepdim=True), flat.amax(dim=1, keepdim=True)
124
+ scale = ((wmax - wmin) / qmax).clamp(min=1e-8)
125
+ zero = torch.round(-wmin / scale)
126
+ codes = torch.clamp(torch.round(flat / scale) + zero, 0, qmax).to(torch.int64)
127
+
128
+ packed = pack_bits(codes.numpy().astype(np.uint32), bits)
129
+ stored_scale, scale_exponent = _store_scale(scale, scale_normalization)
130
+ entry = {
131
+ "kind": "quant",
132
+ "packed": torch.from_numpy(packed), # uint8 -> 1 byte each, honestly counted
133
+ "scale": stored_scale,
134
+ "shape": shape,
135
+ "bits": bits,
136
+ "group": group,
137
+ "symmetric": symmetric,
138
+ "dtype": str(weight.dtype),
139
+ "scale_normalization": scale_normalization,
140
+ }
141
+ entry["scale_exponent"] = scale_exponent
142
+ if zero is not None:
143
+ entry["zero"] = zero.reshape(-1).to(torch.float16)
144
+ return entry
145
+
146
+
147
+ def unpack_tensor(entry: dict[str, Any]) -> torch.Tensor:
148
+ if entry["kind"] == "raw":
149
+ return entry["data"]
150
+
151
+ bits, group, shape = entry["bits"], entry["group"], tuple(entry["shape"])
152
+ count = int(np.prod(shape))
153
+ codes = unpack_bits(entry["packed"].numpy(), bits, count)
154
+ codes = torch.from_numpy(codes.astype(np.int64)).reshape(-1, group)
155
+ scale = entry["scale"].float().reshape(-1, 1)
156
+ scale = scale * math.ldexp(1.0, -int(entry.get("scale_exponent", 0)))
157
+
158
+ if entry["symmetric"]:
159
+ qmax = 2 ** (bits - 1) - 1
160
+ values = (codes - (qmax + 1)).float() * scale
161
+ else:
162
+ values = (codes.float() - entry["zero"].float().reshape(-1, 1)) * scale
163
+
164
+ dtype = getattr(torch, entry["dtype"].replace("torch.", ""))
165
+ return values.reshape(shape).to(dtype)
166
+
167
+
168
+ def _nearest_code_indices(values: torch.Tensor, codebook: torch.Tensor) -> torch.Tensor:
169
+ """Nearest scalar-codebook entry without materialising a huge 3-D tensor."""
170
+ best_distance = torch.full_like(values, float("inf"), dtype=torch.float32)
171
+ best_index = torch.zeros_like(values, dtype=torch.int64)
172
+ for index, center in enumerate(codebook):
173
+ distance = (values - center).square()
174
+ replace = distance < best_distance
175
+ best_distance = torch.where(replace, distance, best_distance)
176
+ best_index = torch.where(replace, index, best_index)
177
+ return best_index
178
+
179
+
180
+ def pack_adaptive_codebook(
181
+ weight: torch.Tensor,
182
+ *,
183
+ bits: int = 4,
184
+ group_size: int = 128,
185
+ importance: torch.Tensor | None = None,
186
+ codebook_count: int = 2,
187
+ iterations: int = 4,
188
+ ) -> tuple[dict[str, Any], dict[str, float | int]]:
189
+ """Pack groups with tiny learned scalar codebooks and sign-bit selectors.
190
+
191
+ The sign of each otherwise-positive FP16 group scale encodes which of two
192
+ codebooks is used. The absolute scale and packed indices are ordinary
193
+ physical tensors, so byte accounting includes every codebook value while
194
+ the selector itself costs no additional storage.
195
+ """
196
+ if bits != 4:
197
+ raise ValueError("adaptive codebooks currently require four-bit indices")
198
+ if codebook_count != 2:
199
+ raise ValueError("scale-sign encoding requires exactly two codebooks")
200
+ if iterations < 1:
201
+ raise ValueError("iterations must be positive")
202
+ shape = tuple(weight.shape)
203
+ if weight.ndim != 2 or shape[-1] % group_size:
204
+ raise ValueError(
205
+ f"adaptive codebooks need a 2-D matrix divisible by group size; got {shape}"
206
+ )
207
+ device = weight.device
208
+ flat = weight.detach().float().reshape(-1, group_size)
209
+ groups = flat.shape[0]
210
+ if importance is None:
211
+ feature_weight = torch.ones(group_size, device=device, dtype=torch.float32)
212
+ else:
213
+ feature_weight = importance.detach().to(device=device, dtype=torch.float32)
214
+ if feature_weight.numel() != shape[-1]:
215
+ raise ValueError(
216
+ f"importance has {feature_weight.numel()} values, expected {shape[-1]}"
217
+ )
218
+ feature_weight = feature_weight.reshape(-1, group_size).repeat(shape[0], 1)
219
+ feature_weight = feature_weight / feature_weight.mean().clamp_min(1e-12)
220
+ if feature_weight.ndim == 1:
221
+ feature_weight = feature_weight.expand(groups, -1)
222
+
223
+ scale = flat.abs().amax(dim=1).clamp_min(1e-8)
224
+ normalized = flat / scale[:, None]
225
+ shape_score = normalized.abs().mean(dim=1)
226
+ split = shape_score.median()
227
+ partitions = (shape_score > split).long()
228
+ levels = 2**bits
229
+ quantiles = torch.linspace(0, 1, levels + 2, device=device)[1:-1]
230
+ books = []
231
+ for book in range(codebook_count):
232
+ values = normalized[partitions == book].reshape(-1)
233
+ if values.numel() < levels:
234
+ values = normalized.reshape(-1)
235
+ # Bound initialization cost for unusually large matrices while keeping
236
+ # deterministic coverage across their complete flattened range.
237
+ if values.numel() > 2_000_000:
238
+ stride = max(1, values.numel() // 2_000_000)
239
+ values = values[::stride][:2_000_000]
240
+ books.append(torch.quantile(values, quantiles).sort().values)
241
+ codebooks = torch.stack(books)
242
+
243
+ selector = partitions
244
+ indices = torch.zeros_like(flat, dtype=torch.int64)
245
+ for _ in range(iterations):
246
+ errors = []
247
+ candidate_indices = []
248
+ for book in range(codebook_count):
249
+ codes = _nearest_code_indices(flat / scale[:, None], codebooks[book])
250
+ reconstructed = codebooks[book][codes] * scale[:, None]
251
+ errors.append(((flat - reconstructed).square() * feature_weight).sum(dim=1))
252
+ candidate_indices.append(codes)
253
+ selector = torch.stack(errors, dim=1).argmin(dim=1)
254
+ indices = torch.where(
255
+ selector[:, None].eq(0), candidate_indices[0], candidate_indices[1]
256
+ )
257
+ chosen = codebooks[selector[:, None], indices]
258
+ numerator = (feature_weight * flat * chosen).sum(dim=1)
259
+ denominator = (feature_weight * chosen.square()).sum(dim=1).clamp_min(1e-12)
260
+ scale = (numerator / denominator).abs().clamp_min(1e-8)
261
+ normalized = flat / scale[:, None]
262
+ for book in range(codebook_count):
263
+ selected_groups = selector.eq(book)
264
+ if not bool(selected_groups.any()):
265
+ continue
266
+ selected_codes = indices[selected_groups].reshape(-1)
267
+ selected_values = normalized[selected_groups].reshape(-1)
268
+ selected_weights = feature_weight[selected_groups].reshape(-1)
269
+ sums = torch.zeros(levels, device=device).scatter_add_(
270
+ 0, selected_codes, selected_values * selected_weights
271
+ )
272
+ counts = torch.zeros(levels, device=device).scatter_add_(
273
+ 0, selected_codes, selected_weights
274
+ )
275
+ updated = torch.where(
276
+ counts > 0, sums / counts.clamp_min(1e-12), codebooks[book]
277
+ )
278
+ codebooks[book] = updated.sort().values.clamp(-1.5, 1.5)
279
+
280
+ # Recompute once after the final codebook update and then charge the exact
281
+ # FP16 scale/codebook representation used by the decoder.
282
+ errors = []
283
+ candidate_indices = []
284
+ for book in range(codebook_count):
285
+ codes = _nearest_code_indices(flat / scale[:, None], codebooks[book])
286
+ reconstructed = codebooks[book][codes] * scale[:, None]
287
+ errors.append(((flat - reconstructed).square() * feature_weight).sum(dim=1))
288
+ candidate_indices.append(codes)
289
+ selector = torch.stack(errors, dim=1).argmin(dim=1)
290
+ indices = torch.where(selector[:, None].eq(0), candidate_indices[0], candidate_indices[1])
291
+ signed_scale = scale.to(torch.float16)
292
+ signed_scale = torch.where(selector.bool(), -signed_scale.abs(), signed_scale.abs())
293
+ stored_books = codebooks.to(torch.float16)
294
+ packed = pack_bits(indices.detach().cpu().numpy().astype(np.uint32), bits)
295
+ entry: dict[str, Any] = {
296
+ "kind": "adaptive_codebook",
297
+ "packed": torch.from_numpy(packed),
298
+ "scale": signed_scale.detach().cpu(),
299
+ "codebooks": stored_books.detach().cpu(),
300
+ "shape": shape,
301
+ "bits": bits,
302
+ "group": group_size,
303
+ "dtype": str(weight.dtype),
304
+ "selector_encoding": "scale_sign",
305
+ }
306
+ restored = unpack_adaptive_codebook(entry).to(device=device, dtype=torch.float32)
307
+ delta = weight.detach().float() - restored
308
+ weighted = feature_weight * delta.reshape(-1, group_size).square()
309
+ report = {
310
+ "groups": int(groups),
311
+ "selector_one_groups": int(selector.sum()),
312
+ "codebook_bytes": int(stored_books.numel() * stored_books.element_size()),
313
+ "weighted_sse": float(weighted.sum()),
314
+ "weighted_mse": float(weighted.mean()),
315
+ "rmse": float(delta.square().mean().sqrt()),
316
+ "max_abs_error": float(delta.abs().max()),
317
+ }
318
+ return entry, report
319
+
320
+
321
+ def unpack_adaptive_codebook(entry: dict[str, Any]) -> torch.Tensor:
322
+ if entry.get("kind") != "adaptive_codebook":
323
+ raise ValueError(f"not an adaptive-codebook entry: {entry.get('kind')!r}")
324
+ bits, group, shape = int(entry["bits"]), int(entry["group"]), tuple(entry["shape"])
325
+ count = int(np.prod(shape))
326
+ codes = unpack_bits(entry["packed"].numpy(), bits, count)
327
+ codes_tensor = torch.from_numpy(codes.astype(np.int64)).reshape(-1, group)
328
+ signed_scale = entry["scale"].float().reshape(-1)
329
+ selector = torch.signbit(signed_scale).long()
330
+ scale = signed_scale.abs().reshape(-1, 1)
331
+ books = entry["codebooks"].float()
332
+ chosen = books[selector[:, None], codes_tensor]
333
+ values = chosen * scale
334
+ dtype = getattr(torch, entry["dtype"].replace("torch.", ""))
335
+ return values.reshape(shape).to(dtype)
336
+
337
+
338
+ def pack_rows(
339
+ weight: torch.Tensor,
340
+ row_ids: torch.Tensor,
341
+ bits: int,
342
+ group_size: int = 128,
343
+ symmetric: bool = True,
344
+ ) -> dict[str, Any]:
345
+ """Pack selected rows while retaining the full tensor shape in metadata.
346
+
347
+ The original token ids are preserved. This matters for the course evaluator,
348
+ which restores the original tokenizer after decompression. At restore time the
349
+ omitted rows can be filled deterministically and the selected rows scattered
350
+ back to their original ids.
351
+ """
352
+ if weight.ndim != 2:
353
+ raise ValueError(f"row packing requires a matrix, got shape {tuple(weight.shape)}")
354
+ ids = row_ids.detach().to(device="cpu", dtype=torch.int64).sort().values
355
+ if ids.numel() == 0:
356
+ raise ValueError("row_ids must not be empty")
357
+ if int(ids[0]) < 0 or int(ids[-1]) >= weight.shape[0]:
358
+ raise ValueError(
359
+ f"row ids [{int(ids[0])}, {int(ids[-1])}] outside 0..{weight.shape[0] - 1}"
360
+ )
361
+ if torch.unique_consecutive(ids).numel() != ids.numel():
362
+ raise ValueError("row_ids contains duplicates")
363
+
364
+ selected = weight.detach().cpu().index_select(0, ids)
365
+ entry = pack_tensor(selected, bits, group_size=group_size, symmetric=symmetric)
366
+ entry["kind"] = "quant_rows" if entry["kind"] == "quant" else "raw_rows"
367
+ entry["full_shape"] = tuple(weight.shape)
368
+ entry["row_ids"] = ids.to(torch.int32)
369
+ return entry
370
+
371
+
372
+ def unpack_rows(entry: dict[str, Any], fill: str = "zero") -> torch.Tensor:
373
+ """Expand a row-packed matrix with a deterministic omitted-row fill."""
374
+ if entry["kind"] not in {"quant_rows", "raw_rows"}:
375
+ raise ValueError(f"not a row-packed entry: {entry.get('kind')!r}")
376
+ dense_entry = {**entry, "kind": "quant" if entry["kind"] == "quant_rows" else "raw"}
377
+ selected = unpack_tensor(dense_entry)
378
+ full_shape = tuple(entry["full_shape"])
379
+ if fill == "zero":
380
+ restored = torch.zeros(full_shape, dtype=selected.dtype)
381
+ elif fill == "mean":
382
+ mean_row = selected.float().mean(dim=0).to(selected.dtype)
383
+ restored = mean_row.expand(full_shape[0], -1).clone()
384
+ else:
385
+ raise ValueError(f"unsupported row fill {fill!r}; expected zero or mean")
386
+ restored.index_copy_(0, entry["row_ids"].long(), selected)
387
+ return restored
388
+
389
+
390
+ def state_dict_bytes(packed: dict[str, dict[str, Any]]) -> int:
391
+ """Real bytes of a packed state dict — the number a submission is scored on."""
392
+ total = 0
393
+ for entry in packed.values():
394
+ for value in entry.values():
395
+ if isinstance(value, torch.Tensor):
396
+ total += value.numel() * value.element_size()
397
+ return total
src/eaimath/peft_compat.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Narrow PEFT compatibility helpers for a shared quantization environment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def is_runtime_quantized(model: Any) -> bool:
9
+ """Return whether PEFT should treat ``model`` as a quantized runtime model."""
10
+ if bool(getattr(model, "is_quantized", False)):
11
+ return True
12
+ if getattr(model, "quantization_method", None):
13
+ return True
14
+ config = getattr(model, "config", None)
15
+ return bool(getattr(config, "quantization_config", None))
16
+
17
+
18
+ QUANT_DISPATCHERS = (
19
+ "dispatch_eetq",
20
+ "dispatch_aqlm",
21
+ "dispatch_awq",
22
+ "dispatch_gptq",
23
+ "dispatch_hqq",
24
+ "dispatch_inc",
25
+ "dispatch_torchao",
26
+ )
27
+
28
+
29
+ def prepare_dense_lora_dispatch(model: Any, *, dispatch_module: Any | None = None) -> bool:
30
+ """Bypass PEFT's irrelevant quantized dispatchers for a dense BF16 model.
31
+
32
+ PEFT 0.18 calls every available quantization backend before its ordinary
33
+ ``torch.nn.Linear`` fallback. Merely having GPTQModel or TorchAO installed
34
+ can therefore raise an optional-backend compatibility error even though the
35
+ target model is not quantized. Round 12 needs those packages elsewhere, so
36
+ disabling their dispatch functions in this one dense-training process is
37
+ safer than mutating the shared environment.
38
+ """
39
+ if is_runtime_quantized(model):
40
+ return False
41
+ if dispatch_module is None:
42
+ from peft.tuners.lora import model as dispatch_module
43
+
44
+ def not_applicable(*_args: Any, **_kwargs: Any) -> None:
45
+ return None
46
+
47
+ for name in QUANT_DISPATCHERS:
48
+ if not hasattr(dispatch_module, name):
49
+ raise RuntimeError(f"PEFT dense dispatch compatibility is missing {name}")
50
+ setattr(dispatch_module, name, not_applicable)
51
+ # Avoid eager BitsAndBytes imports at the start of PEFT's dispatcher list.
52
+ dispatch_module.is_bnb_available = lambda: False
53
+ dispatch_module.is_bnb_4bit_available = lambda: False
54
+ return True
src/eaimath/quantize.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Group-wise weight quantization with exact size accounting.
2
+
3
+ Simulated ("fake") quantization: weights are quantized then dequantized back to
4
+ bf16, so the saved checkpoint is still a normal HF model that vLLM and
5
+ `transformers` load unchanged. That is not a hack — it mirrors the submission
6
+ pipeline exactly, where ``convert_to_hf_checkpoint`` must emit a full bf16 model.
7
+ The *scored* size is computed analytically from the recipe, not from the bytes on
8
+ disk.
9
+
10
+ This lets us answer the question that decides our compression ceiling — how far
11
+ can each layer group be pushed before long chain-of-thought breaks — without
12
+ first committing to a packed format or a quantization toolchain.
13
+
14
+ Round-to-nearest is deliberate as a starting point: it is the floor, needs no
15
+ calibration data, and is enough to *rank* layer-group sensitivity.
16
+
17
+ But it is a worse floor than we assumed. arXiv 2606.25519 measures CoT *token
18
+ inflation* by quantizer on Qwen3-4B at group size 128, and RTN is the worst of
19
+ the lot: +42.5% at INT4 against GPTQ's +12.0% and rotation-based ParoQuant's
20
+ +4.7%. At INT3 the spread across methods reaches 10x. Since our accuracy is
21
+ gated by truncation, token inflation is the quantity that actually costs us
22
+ points -- so moving off RTN is worth considerably more here than the "1-2
23
+ points" that reconstruction-error framing would suggest.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from dataclasses import dataclass
30
+ from typing import Any, Iterable
31
+
32
+ import torch
33
+
34
+ KEEP_BITS = 16 # bf16 passthrough
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class QuantSpec:
39
+ """Quantization for every parameter whose name matches ``pattern``.
40
+
41
+ ``group_size`` groups along the input dimension (the last axis); 0 means one
42
+ group per output channel. Smaller groups cost more scale bytes but track the
43
+ weight distribution better.
44
+ """
45
+
46
+ pattern: str
47
+ bits: int
48
+ group_size: int = 128
49
+ symmetric: bool = True
50
+
51
+ def bits_per_weight(self) -> float:
52
+ """Effective stored bits per weight, including scale/zero-point overhead."""
53
+ if self.bits >= KEEP_BITS:
54
+ return float(KEEP_BITS)
55
+ if self.group_size <= 0:
56
+ return float(self.bits)
57
+ # fp16 scale per group, plus an int zero-point per group when asymmetric.
58
+ overhead = 16 + (0 if self.symmetric else self.bits)
59
+ return self.bits + overhead / self.group_size
60
+
61
+
62
+ def quantize_dequantize(
63
+ weight: torch.Tensor, spec: QuantSpec
64
+ ) -> torch.Tensor:
65
+ """Round-trip ``weight`` through ``spec``'s grid, returning the same dtype."""
66
+ if spec.bits >= KEEP_BITS:
67
+ return weight
68
+
69
+ original_dtype, original_shape = weight.dtype, weight.shape
70
+ group_size = spec.group_size if spec.group_size > 0 else original_shape[-1]
71
+ if original_shape[-1] % group_size != 0:
72
+ # Fall back to per-channel rather than silently mis-grouping.
73
+ group_size = original_shape[-1]
74
+
75
+ flat = weight.reshape(-1, group_size).float()
76
+
77
+ if spec.symmetric:
78
+ qmax = 2 ** (spec.bits - 1) - 1
79
+ scale = (flat.abs().amax(dim=1, keepdim=True) / qmax).clamp(min=1e-8)
80
+ q = torch.round(flat / scale).clamp(-qmax - 1, qmax)
81
+ out = q * scale
82
+ else:
83
+ qmax = 2**spec.bits - 1
84
+ wmin = flat.amin(dim=1, keepdim=True)
85
+ wmax = flat.amax(dim=1, keepdim=True)
86
+ scale = ((wmax - wmin) / qmax).clamp(min=1e-8)
87
+ zero = torch.round(-wmin / scale)
88
+ q = torch.clamp(torch.round(flat / scale) + zero, 0, qmax)
89
+ out = (q - zero) * scale
90
+
91
+ return out.reshape(original_shape).to(original_dtype)
92
+
93
+
94
+ class Recipe:
95
+ """Ordered specs; first match wins, anything unmatched stays bf16."""
96
+
97
+ def __init__(self, specs: Iterable[QuantSpec]):
98
+ self.specs = list(specs)
99
+ self._compiled = [(re.compile(s.pattern), s) for s in self.specs]
100
+
101
+ def spec_for(self, name: str) -> QuantSpec | None:
102
+ for regex, spec in self._compiled:
103
+ if regex.search(name):
104
+ return spec
105
+ return None
106
+
107
+
108
+ # The vision tower and MTP head are never executed by a text-only math eval, and
109
+ # a real submission simply drops them (llama.cpp's text-only conversion of this
110
+ # model does exactly that). They are excluded from both quantization and the size
111
+ # accounting. We nonetheless keep them *present* in the saved checkpoint, because
112
+ # vLLM refuses to load a text-only Qwen3_5 config (vllm#39231).
113
+ EXCLUDED = r"visual|vision_tower|(^|\.)mtp\."
114
+
115
+
116
+ def apply_recipe(
117
+ model: torch.nn.Module, recipe: Recipe, device: str | None = None
118
+ ) -> dict[str, Any]:
119
+ """Fake-quantize ``model`` in place; return per-group size accounting.
120
+
121
+ Tied tensors are counted once — Qwen3.5 ties ``lm_head`` to ``embed_tokens``,
122
+ and double-counting would overstate the checkpoint by 1.27 GB.
123
+ """
124
+ excluded_regex = re.compile(EXCLUDED)
125
+ seen_storage: set[int] = set()
126
+ groups: dict[str, dict[str, float]] = {}
127
+ total_bits = 0.0
128
+ total_params = 0
129
+ excluded_params = 0
130
+
131
+ with torch.no_grad():
132
+ for name, param in model.named_parameters():
133
+ if excluded_regex.search(name):
134
+ excluded_params += param.numel()
135
+ continue
136
+ spec = recipe.spec_for(name)
137
+ bits = spec.bits_per_weight() if spec else float(KEEP_BITS)
138
+
139
+ if spec is not None and spec.bits < KEEP_BITS:
140
+ target = param.data.to(device) if device else param.data
141
+ quantized = quantize_dequantize(target, spec)
142
+ param.data.copy_(quantized.to(param.data.device))
143
+
144
+ pointer = param.data_ptr()
145
+ if pointer in seen_storage:
146
+ continue # tied alias: already counted
147
+ seen_storage.add(pointer)
148
+
149
+ label = _group_label(name, spec)
150
+ entry = groups.setdefault(
151
+ label, {"params": 0, "bits_per_weight": bits, "bytes": 0.0}
152
+ )
153
+ entry["params"] += param.numel()
154
+ entry["bytes"] += param.numel() * bits / 8
155
+ total_params += param.numel()
156
+ total_bits += param.numel() * bits
157
+
158
+ total_bytes = total_bits / 8
159
+ return {
160
+ "total_params": total_params,
161
+ "total_bytes": int(total_bytes),
162
+ "total_gb": total_bytes / 1e9,
163
+ "effective_bits_per_weight": total_bits / total_params if total_params else 0.0,
164
+ "compression_vs_bf16": (total_params * 2) / total_bytes if total_bytes else 0.0,
165
+ "groups": groups,
166
+ # Present in the saved checkpoint for vLLM compatibility, excluded from
167
+ # the score because a real submission drops them.
168
+ "excluded_params": excluded_params,
169
+ "excluded_gb_bf16": excluded_params * 2 / 1e9,
170
+ }
171
+
172
+
173
+ def _group_label(name: str, spec: QuantSpec | None) -> str:
174
+ from .model import BUDGET_GROUPS
175
+
176
+ for group, pattern in BUDGET_GROUPS:
177
+ if re.search(pattern, name):
178
+ return f"{group}@{spec.bits if spec else KEEP_BITS}b"
179
+ return f"other@{spec.bits if spec else KEEP_BITS}b"
180
+
181
+
182
+ def format_size_report(report: dict[str, Any]) -> str:
183
+ lines = [
184
+ f"{'group':<22}{'params':>15}{'bpw':>7}{'GB':>9}",
185
+ "-" * 53,
186
+ ]
187
+ for label, stats in sorted(report["groups"].items(), key=lambda kv: -kv[1]["params"]):
188
+ lines.append(
189
+ f"{label:<22}{stats['params']:>15,}"
190
+ f"{stats['bits_per_weight']:>7.2f}{stats['bytes'] / 1e9:>9.3f}"
191
+ )
192
+ lines.append("-" * 53)
193
+ lines.append(
194
+ f"{'TOTAL':<22}{report['total_params']:>15,}"
195
+ f"{report['effective_bits_per_weight']:>7.2f}{report['total_gb']:>9.3f}"
196
+ )
197
+ lines.append(f" compression vs bf16: {report['compression_vs_bf16']:.2f}x")
198
+ if report.get("excluded_params"):
199
+ lines.append(
200
+ f" excluded (vision+MTP, present on disk but not scored): "
201
+ f"{report['excluded_params']:,} params / {report['excluded_gb_bf16']:.2f} GB"
202
+ )
203
+ return "\n".join(lines)
204
+
205
+
206
+ # The SSM recurrence dynamics must stay high precision: error compounds through
207
+ # the linear recurrence instead of being renormalized each step, and naive
208
+ # low-bit PTQ on these collapses SSM models entirely. They are ~0.02% of
209
+ # parameters, so protecting all of them is nearly free.
210
+ PROTECTED = QuantSpec(
211
+ pattern=r"A_log|dt_bias|conv1d|in_proj_a|in_proj_b|norm|bias",
212
+ bits=KEEP_BITS,
213
+ )
214
+
215
+
216
+ def build_recipe(
217
+ mlp_bits: int = 16,
218
+ linear_attn_bits: int = 16,
219
+ full_attn_bits: int = 16,
220
+ embed_bits: int = 16,
221
+ group_size: int = 128,
222
+ symmetric: bool = True,
223
+ ) -> Recipe:
224
+ """Per-component recipe. PROTECTED comes first so it always wins."""
225
+ return Recipe(
226
+ [
227
+ PROTECTED,
228
+ QuantSpec(r"embed_tokens|lm_head", embed_bits, group_size, symmetric),
229
+ QuantSpec(r"linear_attn", linear_attn_bits, group_size, symmetric),
230
+ QuantSpec(r"self_attn", full_attn_bits, group_size, symmetric),
231
+ QuantSpec(r"\.mlp\.", mlp_bits, group_size, symmetric),
232
+ ]
233
+ )
src/eaimath/rate_distortion.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact-byte utilities for monotone rate--distortion allocation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import heapq
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Choice:
12
+ bits: int
13
+ bytes: int
14
+ distortion: float
15
+
16
+
17
+ def quantized_bytes(shape: tuple[int, ...], bits: int, group_size: int = 128) -> int:
18
+ numel = math.prod(shape)
19
+ if bits >= 16 or len(shape) < 2:
20
+ return numel * 2
21
+ group = group_size if group_size > 0 and shape[-1] % group_size == 0 else shape[-1]
22
+ groups = math.ceil(numel / group)
23
+ return math.ceil(numel * bits / 8) + groups * 2
24
+
25
+
26
+ def allocate(
27
+ options: dict[str, list[Choice]],
28
+ budget: int,
29
+ ) -> tuple[dict[str, Choice], int]:
30
+ """Greedily buy monotone next upgrades by distortion reduction per byte."""
31
+ if not options:
32
+ raise ValueError("no rate--distortion options")
33
+ ordered = {
34
+ name: sorted(rows, key=lambda row: (row.bytes, row.distortion))
35
+ for name, rows in options.items()
36
+ }
37
+ if any(not rows for rows in ordered.values()):
38
+ raise ValueError("every tensor requires at least one choice")
39
+ index = {name: 0 for name in ordered}
40
+ total = sum(rows[0].bytes for rows in ordered.values())
41
+ if total > budget:
42
+ raise ValueError(f"minimum representation is {total:,} bytes, over budget {budget:,}")
43
+
44
+ # Only one next upgrade per item is eligible at a time. The original
45
+ # implementation rebuilt and scanned that full set after buying every
46
+ # individual upgrade: O(items * upgrades), which becomes effectively
47
+ # quadratic for Round 14's 280k row blocks. A heap preserves the exact
48
+ # greedy ordering in O((items + upgrades) log items).
49
+ name_rank = {name: rank for rank, name in enumerate(sorted(ordered))}
50
+ heap: list[tuple[float, float, int, int, str, int]] = []
51
+
52
+ def push_next(name: str) -> None:
53
+ current_index = index[name]
54
+ rows = ordered[name]
55
+ if current_index + 1 >= len(rows):
56
+ return
57
+ current = rows[current_index]
58
+ target = rows[current_index + 1]
59
+ added = target.bytes - current.bytes
60
+ gain = current.distortion - target.distortion
61
+ if added <= 0 or gain <= 0:
62
+ return
63
+ # This is the min-heap inverse of the old max tuple:
64
+ # (gain / added, gain, -added, name).
65
+ heapq.heappush(
66
+ heap,
67
+ (-gain / added, -gain, added, -name_rank[name], name, current_index + 1),
68
+ )
69
+
70
+ for name in ordered:
71
+ push_next(name)
72
+
73
+ while heap:
74
+ _ratio, _gain, added, _rank, winner, target_index = heapq.heappop(heap)
75
+ if total + added > budget:
76
+ # Total never decreases, so this upgrade (and its dependent chain)
77
+ # can never fit later. Smaller independent upgrades remain eligible.
78
+ continue
79
+ total += added
80
+ index[winner] = target_index
81
+ push_next(winner)
82
+
83
+ selected = {name: ordered[name][position] for name, position in index.items()}
84
+ return selected, total
src/eaimath/sharing.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MLP layer sharing — buying size structurally instead of with bits.
2
+
3
+ Bucket A (<=0.84 GB) is out of reach by quantization alone. ParetoQ measures 3-
4
+ and 4-bit QAT saturating at 10 B tokens while sub-2-bit needs 30 B, because
5
+ sub-2-bit "breaks the grid" and has to form new representations rather than nudge
6
+ within one. Our token budget is 1-3 B. So the bits have to stay at 3-4 and the
7
+ size has to come from somewhere else.
8
+
9
+ The MLPs are 53.9% of the model (2.26 B of 4.21 B). Tying one MLP block across
10
+ every k-th layer divides that by k while keeping every forward pass intact --
11
+ unlike depth pruning, which deletes the pass as well and collapses this workload
12
+ (GSM8K 79.3 -> 0.9 at 25% removal; AIME24 gone after a single layer).
13
+
14
+ **Why this is measurable tonight with no new inference code.** We tie the
15
+ *Parameters*, so `apply_recipe`'s existing data_ptr dedup counts the block once
16
+ and the size report is the real shared size. Then `untie` clones them back into
17
+ separate storage before saving, so what lands on disk is an ordinary checkpoint
18
+ that vLLM loads unchanged. Accuracy measured on that checkpoint is exactly the
19
+ shared model's accuracy -- the tying is mathematically identical, only the
20
+ storage differs. A submission would ship the deduped form plus an expansion
21
+ step, which is precisely the "compression + decompression" pair the course asks
22
+ for.
23
+
24
+ **What this does NOT do.** There is no recovery training here. Averaging k
25
+ learned blocks into one and running it cold measures the *raw damage*, which is
26
+ the number we need to size the LoRA/distillation budget against -- not a result
27
+ to submit. Expect it to look bad.
28
+
29
+ Middle-Cycle only: layers 0, 1 and the last are left alone. They are
30
+ independently found to be functionally distinct in Qwen3-4B, and tying them is a
31
+ known way to destroy a model for no size win worth having.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import Any
37
+
38
+ import torch
39
+
40
+ # The projections inside one MLP block. Named rather than discovered so that a
41
+ # renamed submodule fails loudly instead of silently sharing nothing.
42
+ MLP_PROJECTIONS = ("gate_proj", "up_proj", "down_proj")
43
+
44
+ PROTECTED_HEAD = 2 # never tie layers 0-1
45
+ PROTECTED_TAIL = 1 # never tie the final layer
46
+
47
+
48
+ def find_decoder_layers(model: torch.nn.Module) -> torch.nn.ModuleList:
49
+ """The decoder layer list, wherever this transformers release put it.
50
+
51
+ Qwen3.5 is multimodal, so the text stack sits under a `language_model` in
52
+ some releases and directly under `model` in others. Both appear in the
53
+ versions we have run against, so this introspects instead of guessing.
54
+ """
55
+ candidates: list[torch.nn.ModuleList] = []
56
+ for name, module in model.named_modules():
57
+ if not isinstance(module, torch.nn.ModuleList):
58
+ continue
59
+ if "visual" in name or "vision" in name or ".mtp" in name:
60
+ continue
61
+ if not name.endswith("layers"):
62
+ continue
63
+ first = module[0] if len(module) else None
64
+ if first is not None and hasattr(first, "mlp"):
65
+ candidates.append(module)
66
+ if not candidates:
67
+ raise RuntimeError(
68
+ "Could not locate the decoder layer list. Run "
69
+ "`python scripts/01_inspect_model.py` and check the module tree."
70
+ )
71
+ # The text stack is the longest one that is not the vision tower.
72
+ return max(candidates, key=len)
73
+
74
+
75
+ def sharing_groups(num_layers: int, factor: int) -> list[list[int]]:
76
+ """Contiguous groups of layer indices to tie, honouring Middle-Cycle."""
77
+ if factor < 2:
78
+ return []
79
+ body = list(range(PROTECTED_HEAD, num_layers - PROTECTED_TAIL))
80
+ return [body[i : i + factor] for i in range(0, len(body), factor)]
81
+
82
+
83
+ def tie_mlps(
84
+ model: torch.nn.Module, factor: int, *, init: str = "mean"
85
+ ) -> dict[str, Any]:
86
+ """Tie each group of ``factor`` MLP blocks to one shared block, in place.
87
+
88
+ ``init='mean'`` averages the group's weights into the shared block;
89
+ ``init='first'`` keeps the first block and discards the rest. Mean is the
90
+ default because it is the standard cheap initializer and discarding 3 of 4
91
+ learned blocks is strictly worse. Neither is permutation-aligned merging,
92
+ which is what a real attempt would use -- see PLAN.md W6.
93
+
94
+ Returns accounting: how many MLP params exist after tying, and how many the
95
+ dense model had.
96
+ """
97
+ if init not in ("mean", "first"):
98
+ raise ValueError(f"init must be 'mean' or 'first', got {init!r}")
99
+
100
+ layers = find_decoder_layers(model)
101
+ groups = sharing_groups(len(layers), factor)
102
+ if not groups:
103
+ return {"factor": factor, "groups": 0, "tied_layers": 0}
104
+
105
+ dense_params = sum(
106
+ p.numel()
107
+ for layer in layers
108
+ for name in MLP_PROJECTIONS
109
+ for p in getattr(layer.mlp, name).parameters()
110
+ )
111
+
112
+ tied_layers = 0
113
+ with torch.no_grad():
114
+ for group in groups:
115
+ if len(group) < 2:
116
+ continue # a trailing singleton shares with nobody
117
+ anchor = layers[group[0]]
118
+ for proj_name in MLP_PROJECTIONS:
119
+ anchor_proj = getattr(anchor.mlp, proj_name)
120
+ if init == "mean":
121
+ stacked = torch.stack([
122
+ getattr(layers[i].mlp, proj_name).weight.data.float()
123
+ for i in group
124
+ ])
125
+ anchor_proj.weight.data.copy_(
126
+ stacked.mean(dim=0).to(anchor_proj.weight.dtype)
127
+ )
128
+ # Point every follower at the anchor's Parameter object. Same
129
+ # storage => apply_recipe's data_ptr dedup counts it once.
130
+ for i in group[1:]:
131
+ getattr(layers[i].mlp, proj_name).weight = anchor_proj.weight
132
+ tied_layers += len(group) - 1
133
+
134
+ shared_params = _unique_mlp_params(layers)
135
+ return {
136
+ "factor": factor,
137
+ "init": init,
138
+ "groups": len([g for g in groups if len(g) > 1]),
139
+ "tied_layers": tied_layers,
140
+ "mlp_params_dense": dense_params,
141
+ "mlp_params_shared": shared_params,
142
+ "mlp_params_saved": dense_params - shared_params,
143
+ }
144
+
145
+
146
+ def _unique_mlp_params(layers: torch.nn.ModuleList) -> int:
147
+ seen: set[int] = set()
148
+ total = 0
149
+ for layer in layers:
150
+ for proj_name in MLP_PROJECTIONS:
151
+ weight = getattr(layer.mlp, proj_name).weight
152
+ if weight.data_ptr() in seen:
153
+ continue
154
+ seen.add(weight.data_ptr())
155
+ total += weight.numel()
156
+ return total
157
+
158
+
159
+ def untie(model: torch.nn.Module) -> int:
160
+ """Give every tied MLP weight its own storage again.
161
+
162
+ Call this after quantizing and before saving. `safetensors` refuses to write
163
+ tensors that share memory, and transformers' workaround silently drops the
164
+ duplicates -- which would produce a checkpoint with missing keys. Cloning
165
+ costs disk, not accuracy: the values are identical, so the model evaluated
166
+ is exactly the shared one.
167
+ """
168
+ layers = find_decoder_layers(model)
169
+ seen: set[int] = set()
170
+ cloned = 0
171
+ with torch.no_grad():
172
+ for layer in layers:
173
+ for proj_name in MLP_PROJECTIONS:
174
+ proj = getattr(layer.mlp, proj_name)
175
+ pointer = proj.weight.data_ptr()
176
+ if pointer not in seen:
177
+ seen.add(pointer)
178
+ continue
179
+ proj.weight = torch.nn.Parameter(
180
+ proj.weight.data.clone(), requires_grad=proj.weight.requires_grad
181
+ )
182
+ cloned += 1
183
+ return cloned
184
+
185
+
186
+ def format_sharing_report(report: dict[str, Any]) -> str:
187
+ if not report.get("groups"):
188
+ return "[sharing] none"
189
+ saved_gb = report["mlp_params_saved"] * 2 / 1e9
190
+ return (
191
+ f"[sharing] {report['factor']}x MLP, init={report['init']}: "
192
+ f"{report['groups']} groups, {report['tied_layers']} layers tied\n"
193
+ f" MLP params {report['mlp_params_dense']:,} -> "
194
+ f"{report['mlp_params_shared']:,} "
195
+ f"(-{report['mlp_params_saved']:,}, -{saved_gb:.2f} GB at bf16)"
196
+ )
src/eaimath/vllm_backend.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """vLLM generation backend — the fix for our eval throughput problem.
2
+
3
+ The HF path runs a fixed batch until *every* sequence in it stops. With a 43%
4
+ truncation rate and a 32k cap, one long sequence pins the whole batch for 32,768
5
+ steps while the other 15 sit finished and padded. That is why a 50-problem shard
6
+ took ~4 hours.
7
+
8
+ vLLM's continuous batching retires finished sequences immediately and admits new
9
+ ones, and paged attention removes the `torch.cat` cache-regrowth spike entirely.
10
+ For this workload — high variance in output length, long tails — that is worth
11
+ roughly an order of magnitude.
12
+
13
+ Optional dependency: ``pip install -e '.[vllm]'``. The HF backend in
14
+ ``generate.py`` stays the reference implementation and always works.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from pathlib import Path
21
+ from typing import Sequence
22
+
23
+ from .generate import GenerationOutput, build_chat_prompts
24
+
25
+
26
+ def _checkpoint_dtype(model_path: str) -> str:
27
+ """Use FP16 activations for GPTQ; retain BF16 for the established RTN path."""
28
+ # Official ParoQuant checkpoints use an FP16 activation kernel and can be
29
+ # addressed by a remote Hugging Face id, before a local config exists.
30
+ if "paro" in model_path.lower():
31
+ return "float16"
32
+ root = Path(model_path)
33
+ quantize_config = root / "quantize_config.json"
34
+ if quantize_config.is_file():
35
+ try:
36
+ config = json.loads(quantize_config.read_text())
37
+ if config.get("bits") in (2, 3, 4, 8):
38
+ return "float16"
39
+ except (OSError, json.JSONDecodeError):
40
+ pass
41
+
42
+ model_config = root / "config.json"
43
+ if model_config.is_file():
44
+ try:
45
+ config = json.loads(model_config.read_text())
46
+ quantization = config.get("quantization_config") or {}
47
+ if str(quantization.get("quant_method", "")).lower() in {"gptq", "paroquant"}:
48
+ return "float16"
49
+ except (OSError, json.JSONDecodeError):
50
+ pass
51
+ return "bfloat16"
52
+
53
+
54
+ def is_available() -> bool:
55
+ import importlib.util
56
+
57
+ return importlib.util.find_spec("vllm") is not None
58
+
59
+
60
+ def generate_vllm(
61
+ model_path: str,
62
+ tokenizer,
63
+ prompts: Sequence[str],
64
+ *,
65
+ max_new_tokens: int = 65536,
66
+ temperature: float = 0.0,
67
+ top_p: float = 0.95,
68
+ top_k: int = 20,
69
+ presence_penalty: float = 0.0,
70
+ repetition_penalty: float = 1.0,
71
+ enable_thinking: bool | None = None,
72
+ gpu_memory_utilization: float = 0.90,
73
+ max_model_len: int | None = None,
74
+ seed: int = 0,
75
+ allowed_token_ids: Sequence[int] | None = None,
76
+ llm=None,
77
+ ) -> tuple[list[GenerationOutput], object]:
78
+ """Generate with vLLM, returning the same records as the HF backend.
79
+
80
+ Returns ``(outputs, llm)`` so the engine can be reused across calls — engine
81
+ startup costs ~1-2 min, which matters when sweeping recipes.
82
+ """
83
+ from vllm import LLM, SamplingParams
84
+
85
+ chat_texts = build_chat_prompts(tokenizer, prompts, enable_thinking)
86
+
87
+ if llm is None:
88
+ longest_prompt = max(len(tokenizer(t)["input_ids"]) for t in chat_texts)
89
+ if max_model_len is None:
90
+ # Headroom over the longest prompt so no request is rejected outright.
91
+ max_model_len = max_new_tokens + longest_prompt + 256
92
+ llm = LLM(
93
+ model=model_path,
94
+ # vLLM 0.19 GPTQ kernels support FP16 activations. The dense base
95
+ # model and our RTN-value checkpoints remain on their proven BF16 path.
96
+ dtype=_checkpoint_dtype(model_path),
97
+ gpu_memory_utilization=gpu_memory_utilization,
98
+ max_model_len=max_model_len,
99
+ trust_remote_code=True,
100
+ seed=seed,
101
+ )
102
+
103
+ sampling_kwargs = dict(
104
+ max_tokens=max_new_tokens,
105
+ temperature=temperature,
106
+ # vLLM ignores top_p/top_k at temperature 0, but passing them when
107
+ # sampling keeps parity with Qwen's recommended thinking-mode settings.
108
+ top_p=top_p if temperature > 0 else 1.0,
109
+ top_k=top_k if temperature > 0 else -1,
110
+ presence_penalty=presence_penalty,
111
+ repetition_penalty=repetition_penalty,
112
+ )
113
+ if allowed_token_ids:
114
+ # Restricting generation to a token subset is how we measure the cost of
115
+ # a trimmed vocabulary without rebuilding the tokenizer. Not every vLLM
116
+ # release accepts it, and an overnight sweep must not die at 3 a.m. over
117
+ # a kwarg -- so fail loudly here, before the engine is built.
118
+ try:
119
+ SamplingParams(**sampling_kwargs, allowed_token_ids=[0])
120
+ except TypeError as exc:
121
+ raise RuntimeError(
122
+ "This vLLM build does not accept SamplingParams(allowed_token_ids=...), "
123
+ "so the vocabulary-restriction probe cannot run here. Drop "
124
+ "--vocab-size to run everything else.\n"
125
+ f" underlying error: {exc}"
126
+ ) from exc
127
+ sampling_kwargs["allowed_token_ids"] = list(allowed_token_ids)
128
+
129
+ sampling = SamplingParams(**sampling_kwargs)
130
+
131
+ results: list[GenerationOutput] = []
132
+ for request, chat_text in zip(llm.generate(list(chat_texts), sampling), chat_texts):
133
+ completion = request.outputs[0]
134
+ results.append(
135
+ GenerationOutput(
136
+ text=completion.text,
137
+ num_prompt_tokens=len(request.prompt_token_ids),
138
+ num_generated_tokens=len(completion.token_ids),
139
+ # vLLM reports this directly, so unlike the HF path we do not
140
+ # have to infer termination by scanning for EOS.
141
+ finished=completion.finish_reason != "length",
142
+ )
143
+ )
144
+ return results, llm
src/eaimath/vocab.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Vocabulary trimming — 285 MB, the single largest untouched lever.
2
+
3
+ The tied embedding is 635.7 M params, 15.1% of the model. A math-only model does
4
+ not need 248,320 tokens: a 13 k vocabulary retains 98.6% coverage on MATH-500 and
5
+ AIME, and math is the easiest domain to trim (far easier than code or
6
+ multilingual).
7
+
8
+ vocab params @4.125 bpw
9
+ 248,320 635.7 M 327.8 MB
10
+ 32,768 83.9 M 43.3 MB
11
+
12
+ That 285 MB is 34% of the entire bucket-A ceiling. It is also what makes bucket B
13
+ reachable *with a healthy attention stack* -- see `scripts/plan_configs.py`:
14
+ without trimming, fitting under 1.68 GB forces full_attn down to ~3 bits, and
15
+ full_attn is the most fragile component we have measured (-21 points at 4 bits).
16
+
17
+ **Two separate jobs, deliberately separated.**
18
+
19
+ 1. `allowed_token_ids` (here) measures the *accuracy cost* of a smaller
20
+ vocabulary with no tokenizer surgery at all: generation is restricted to the
21
+ kept set, so the model produces exactly what a trimmed model could produce.
22
+ This validates the idea before anyone builds the real thing.
23
+ 2. Actually rebuilding the tokenizer -- filtering merges so the vocabulary stays
24
+ closed, remapping ids, slicing the embedding -- is the shipping step, and it
25
+ only makes sense once (1) says the cost is acceptable.
26
+
27
+ Doing (1) first also dissolves the zero-logit trap. Zeroing a dropped embedding
28
+ row does not remove the token: its logit becomes exactly 0, which wins the argmax
29
+ whenever every real logit is negative. Masking at sampling time cannot have that
30
+ failure mode.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from collections import Counter
36
+ from typing import Any, Iterable, Sequence
37
+
38
+ # Every byte-level fallback token, so any string stays encodable, plus the
39
+ # specials the chat template depends on. Termination is our failure mode and
40
+ # </think> / EOS are the mechanism, so they are never droppable.
41
+ ALWAYS_KEEP_TOKENS = (
42
+ "<|endoftext|>", "<|im_start|>", "<|im_end|>",
43
+ "<think>", "</think>",
44
+ )
45
+
46
+
47
+ def token_frequencies(texts: Iterable[str], tokenizer) -> Counter:
48
+ """Token id -> count over a corpus, using the model's own tokenizer."""
49
+ counts: Counter = Counter()
50
+ for text in texts:
51
+ counts.update(tokenizer(text, add_special_tokens=False)["input_ids"])
52
+ return counts
53
+
54
+
55
+ def build_keep_set(
56
+ counts: Counter,
57
+ tokenizer,
58
+ target_size: int,
59
+ *,
60
+ priority_counts: Counter | None = None,
61
+ coverage_report: bool = True,
62
+ ) -> dict[str, Any]:
63
+ """The ``target_size`` most useful token ids, plus everything unconditional.
64
+
65
+ Byte-fallback tokens are kept whole rather than by frequency: dropping one
66
+ makes some strings unencodable, and the storage is 256 rows.
67
+ """
68
+ keep: set[int] = set()
69
+
70
+ for token in ALWAYS_KEEP_TOKENS:
71
+ token_id = tokenizer.convert_tokens_to_ids(token)
72
+ if token_id is not None and token_id >= 0:
73
+ keep.add(token_id)
74
+ for token_id in getattr(tokenizer, "all_special_ids", []) or []:
75
+ keep.add(token_id)
76
+
77
+ # Byte-level fallbacks: single-byte pieces the BPE falls back to.
78
+ vocab = tokenizer.get_vocab()
79
+ for token, token_id in vocab.items():
80
+ if len(token) == 1 or (token.startswith("<0x") and token.endswith(">")):
81
+ keep.add(token_id)
82
+
83
+ total = sum(counts.values())
84
+ covered = sum(counts[i] for i in keep if i in counts)
85
+ priority_added = 0
86
+ if priority_counts:
87
+ # Round 9 showed that omitted *input* rows are the clearest recoverable
88
+ # loss. Preserve every token seen in disjoint training problems before
89
+ # the much longer completions consume the frequency budget.
90
+ for token_id, _ in priority_counts.most_common():
91
+ if len(keep) >= target_size:
92
+ break
93
+ if token_id not in keep:
94
+ keep.add(token_id)
95
+ covered += counts[token_id]
96
+ priority_added += 1
97
+ for token_id, count in counts.most_common():
98
+ if len(keep) >= target_size:
99
+ break
100
+ if token_id not in keep:
101
+ keep.add(token_id)
102
+ covered += count
103
+
104
+ if len(keep) < target_size:
105
+ # The corpus did not contain enough distinct tokens to fill the budget.
106
+ # Silently shipping the short set is what turned the first vocabulary
107
+ # probe into a measurement of "restrict the model to 4k tokens drawn
108
+ # from problem statements" -- which unsurprisingly scored 0.010, and
109
+ # said nothing at all about vocabulary trimming.
110
+ #
111
+ # BPE vocabularies are ordered by merge priority, so low ids are the
112
+ # most generally useful pieces. Filling from the bottom is a defensible
113
+ # completion, and the caller is told it happened.
114
+ filled = 0
115
+ for token_id in range(len(vocab)):
116
+ if len(keep) >= target_size:
117
+ break
118
+ if token_id not in keep:
119
+ keep.add(token_id)
120
+ filled += 1
121
+ else:
122
+ filled = 0
123
+
124
+ result: dict[str, Any] = {
125
+ "keep_ids": sorted(keep),
126
+ "kept": len(keep),
127
+ "original_vocab": len(vocab),
128
+ "target_size": target_size,
129
+ "observed_distinct": len(counts),
130
+ "priority_distinct": len(priority_counts or {}),
131
+ "priority_added": priority_added,
132
+ "filled_by_id_order": filled,
133
+ }
134
+ if coverage_report and total:
135
+ result["token_coverage"] = covered / total
136
+ result["dropped_token_mass"] = 1.0 - covered / total
137
+ return result
138
+
139
+
140
+ def embedding_savings(
141
+ original_vocab: int, kept: int, hidden_size: int, bits_per_weight: float
142
+ ) -> dict[str, float]:
143
+ """Bytes reclaimed by a trim, for the tied embedding + lm_head (counted once)."""
144
+ dropped_params = (original_vocab - kept) * hidden_size
145
+ return {
146
+ "dropped_rows": original_vocab - kept,
147
+ "dropped_params": dropped_params,
148
+ "saved_bytes": dropped_params * bits_per_weight / 8,
149
+ "saved_mb": dropped_params * bits_per_weight / 8 / 1e6,
150
+ }
151
+
152
+
153
+ def sampling_kwargs(keep_ids: Sequence[int] | None) -> dict[str, Any]:
154
+ """SamplingParams kwargs restricting generation to ``keep_ids``.
155
+
156
+ Returns empty when there is nothing to restrict, so callers can splat this
157
+ unconditionally.
158
+ """
159
+ if not keep_ids:
160
+ return {}
161
+ return {"allowed_token_ids": list(keep_ids)}
src/eaimath/workspace.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Workspace layout helpers shared by runners, collectors, and hygiene tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from pathlib import Path
7
+
8
+
9
+ def round_log_files(
10
+ round_name: str,
11
+ patterns: Iterable[str] = (),
12
+ experiments: Path = Path("experiments"),
13
+ ) -> list[Path]:
14
+ """Return round-local logs plus legacy flat/archive locations.
15
+
16
+ Round 15 onward writes logs beside its round. Rounds 8--14 historically
17
+ wrote into ``experiments/logs``; the hygiene tool archives those files
18
+ without making old evidence collectors blind to them.
19
+ """
20
+ local = experiments / "rounds" / round_name / "logs"
21
+ legacy = experiments / "logs"
22
+ candidates = [local / "runner.log", legacy / f"{round_name}_runner.log"]
23
+ for pattern in patterns:
24
+ candidates.extend(sorted(local.glob(pattern)))
25
+ candidates.extend(sorted(legacy.glob(pattern)))
26
+
27
+ archive = legacy / "archive"
28
+ if archive.is_dir():
29
+ candidates.extend(sorted(archive.rglob(f"{round_name}_runner.log")))
30
+ for pattern in patterns:
31
+ candidates.extend(sorted(archive.rglob(pattern)))
32
+
33
+ unique: list[Path] = []
34
+ seen: set[Path] = set()
35
+ for path in candidates:
36
+ resolved = path.resolve()
37
+ if path.is_file() and resolved not in seen:
38
+ seen.add(resolved)
39
+ unique.append(path)
40
+ return unique