|
53 | 53 | # ── Deterministic calibration — env half (OPT-IN via ML8_DETERMINISTIC=1; MUST precede `import torch`) ─── |
54 | 54 | # ml8 GPTQ calibration was nondeterministic: the GPU Hessian forward used unforced reduction order |
55 | 55 | # and GPTQ's sequential error-feedback amplified the low-bit noise into DIFFERENT weight assignments |
56 | | -# run-to-run (~0.6 PPL spread — swamps the ±0.05 levers). These flags pin it bit-identically on the |
57 | | -# linear-attn/MLP path (verified). BUT torch.use_deterministic_algorithms(True) currently breaks |
58 | | -# full-model calibration: from the FIRST full-self-attention layer onward, the SDPA forward under |
59 | | -# the hammer yields a degenerate (NaN-suspected) Hessian → GPTQ Cholesky fails → tensors silently |
60 | | -# skipped → partial-coverage model. Root-cause + fix is deferred (see KG / handoff), so determinism |
61 | | -# is OPT-IN until then; the default path is the known-good nondeterministic calibrator. |
| 56 | +# run-to-run (~0.6 PPL spread — swamps the ±0.05 levers). These flags pin it bit-identically. |
| 57 | +# VERIFIED 2026-06-02: full-model deterministic calibration is bit-reproducible (187/188 tensors |
| 58 | +# byte-identical across two independent runs; the rest are FP8). The earlier "breaks at the first |
| 59 | +# self-attn layer" failure was NOT a determinism bug — use_deterministic_algorithms(True) enables |
| 60 | +# fill_uninitialized_memory, which surfaced a latent bug: the rotary inv_freq buffer (no GGUF |
| 61 | +# counterpart) was left uninitialized by meta+to_empty → NaN-filled → garbage RoPE. Fixed by |
| 62 | +# reinit_rotary_buffers() (runs every calibration; also repairs the silent non-deterministic case). |
62 | 63 | _DETERMINISTIC = os.environ.get("ML8_DETERMINISTIC") == "1" |
63 | 64 | if _DETERMINISTIC: |
64 | 65 | os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8") # required for deterministic cuBLAS/hipBLAS GEMM |
|
86 | 87 | try: setattr(torch.backends.cuda.matmul, _attr, _val) |
87 | 88 | except AttributeError: pass |
88 | 89 | print("[determinism] OPT-IN ENABLED: use_deterministic_algorithms(True) + seeded + pinned GEMM " |
89 | | - "— WARNING: breaks full-model coverage at the first self-attn layer (Cholesky/SDPA bug, WIP)") |
| 90 | + "— full-model calibration is bit-reproducible (rotary inv_freq reinit applied post-load)") |
90 | 91 |
|
91 | 92 | # Re-use existing calibration helpers (compute_hessian / gptq_quantize_linear / eval_ppl_wikitext / etc.) |
92 | 93 | sys.path.insert(0, str(Path(__file__).parent)) |
@@ -237,7 +238,19 @@ def find_dense_full_targets(model, coverage: str = "ffn"): |
237 | 238 | yield name, mod, Tier.ML8 |
238 | 239 |
|
239 | 240 | # Global ML8 roles (lm_head / eh_proj), if present as nn.Linear. |
| 241 | + # Skip lm_head when the model TIES embeddings: a tied lm_head IS token_embd |
| 242 | + # (already covered by the FP8 embedding tier), has no independent GGUF tensor |
| 243 | + # (the converter correctly emits no output.weight), and meta+to_empty leaves |
| 244 | + # its weight UNINITIALIZED — so quantizing it wastes time on silent garbage |
| 245 | + # (NaN under determinism) and produces a blob the converter discards anyway. |
| 246 | + _cfg = getattr(model, "config", None) |
| 247 | + _tied = bool(getattr(getattr(_cfg, "text_config", _cfg), "tie_word_embeddings", False) |
| 248 | + or getattr(_cfg, "tie_word_embeddings", False)) |
240 | 249 | for role in _FULL_ML8_GLOBAL_ORDER: |
| 250 | + if role == "lm_head" and _tied: |
| 251 | + print("[targets] skipping lm_head ML8 target: tie_word_embeddings=True " |
| 252 | + "(served by the FP8-tier tied token_embd at inference)") |
| 253 | + continue |
241 | 254 | for name, mod in by_name.items(): |
242 | 255 | if name.rsplit(".", 1)[-1] != role: |
243 | 256 | continue |
@@ -649,6 +662,46 @@ def load_resident_to_model(model: nn.Module, gguf_path: str, |
649 | 662 | return n_loaded |
650 | 663 |
|
651 | 664 |
|
| 665 | +def reinit_rotary_buffers(model: nn.Module, device: str) -> int: |
| 666 | + """Recompute every text rotary-embedding ``inv_freq`` buffer from config. |
| 667 | +
|
| 668 | + inv_freq is a NON-PERSISTENT buffer derived from rope_theta — it has no GGUF |
| 669 | + counterpart, so the resident loader (which walks ``named_parameters``) never |
| 670 | + fills it, and ``model.to_empty()`` leaves it UNINITIALIZED. The damage is |
| 671 | + silent without determinism: uninitialized memory is finite garbage, and since |
| 672 | + cos()/sin() clamp to [-1, 1] the forward never NaNs — it just applies WRONG |
| 673 | + positional encoding to every full-attention layer (linear-attn layers don't |
| 674 | + use RoPE). With ``use_deterministic_algorithms(True)`` that same uninitialized |
| 675 | + memory is NaN-filled, so the first RoPE turns q/k into NaN and the whole |
| 676 | + forward (and every downstream Hessian) blows up. Both are the SAME bug; the |
| 677 | + fix is to rebuild inv_freq the way the module's __init__ does and keep it in |
| 678 | + fp32 (HF never downcasts it; our blanket .to(dtype) did). Idempotent — safe to |
| 679 | + run on every calibration. Returns the number of rotary modules repaired. |
| 680 | + """ |
| 681 | + n = 0 |
| 682 | + for mod in model.modules(): |
| 683 | + cls = mod.__class__.__name__ |
| 684 | + if "RotaryEmbedding" not in cls or "Vision" in cls: |
| 685 | + continue # text rotary only; vision rotary has a different __init__ |
| 686 | + if not hasattr(mod, "inv_freq") or getattr(mod, "config", None) is None: |
| 687 | + continue |
| 688 | + try: |
| 689 | + fresh = type(mod)(mod.config, device=torch.device(device)) |
| 690 | + except Exception as e: # noqa: BLE001 — never let a probe-fix abort calibration |
| 691 | + print(f"[rotary-fix] WARN: could not rebuild {cls} inv_freq: {e}") |
| 692 | + continue |
| 693 | + with torch.no_grad(): |
| 694 | + # assign (not copy_) so the buffer keeps fresh's fp32 dtype, not the |
| 695 | + # bf16 the model-wide .to(dtype) left it as. |
| 696 | + mod.inv_freq = fresh.inv_freq.to(device=torch.device(device)) |
| 697 | + if hasattr(mod, "original_inv_freq") and hasattr(fresh, "original_inv_freq"): |
| 698 | + mod.original_inv_freq = fresh.original_inv_freq.to(device=torch.device(device)) |
| 699 | + if hasattr(fresh, "attention_scaling"): |
| 700 | + mod.attention_scaling = fresh.attention_scaling |
| 701 | + n += 1 |
| 702 | + return n |
| 703 | + |
| 704 | + |
652 | 705 | def _detect_moe_n_experts(config) -> int | None: |
653 | 706 | """Return n_experts if `config` describes an MoE model, else None. |
654 | 707 |
|
@@ -1163,6 +1216,73 @@ def main(): |
1163 | 1216 | # installed. MUST run before the first forward. ─── |
1164 | 1217 | apply_fla_arch_shim(model, args.device) |
1165 | 1218 |
|
| 1219 | + # ─── Repair rotary inv_freq: meta-build + to_empty leaves this non-persistent, |
| 1220 | + # non-GGUF buffer uninitialized → silently-wrong RoPE (and a hard NaN under |
| 1221 | + # determinism). MUST run before the first forward, on every calibration. ─── |
| 1222 | + _n_rotary = reinit_rotary_buffers(model, args.device) |
| 1223 | + print(f"[rotary-fix] reinitialized inv_freq on {_n_rotary} text rotary module(s)") |
| 1224 | + |
| 1225 | + # ─── NaN probe (opt-in ML8_NAN_PROBE=1): hook every decoder layer + the submodules |
| 1226 | + # of the first full_attention layer; on the FIRST forward, print per-module |
| 1227 | + # in-nan/out-nan/out-inf/finite-absmax in completion order, then self-remove. Used to |
| 1228 | + # pinpoint the determinism-induced NaN in the first full-attn layer (deferred bug). ─── |
| 1229 | + if os.environ.get("ML8_NAN_PROBE") == "1": |
| 1230 | + _dec = [m for _n, m in model.named_modules() |
| 1231 | + if m.__class__.__name__.endswith("DecoderLayer")] |
| 1232 | + _lt = list(getattr(getattr(model.config, "text_config", model.config), |
| 1233 | + "layer_types", ["?"] * len(_dec))) |
| 1234 | + _first_full = next((i for i, t in enumerate(_lt) if "full" in str(t)), 3) |
| 1235 | + _pb = {"ev": [], "done": False, "h": []} |
| 1236 | + |
| 1237 | + def _pstat(t): |
| 1238 | + if isinstance(t, (tuple, list)) and t and torch.is_tensor(t[0]): |
| 1239 | + t = t[0] |
| 1240 | + if not torch.is_tensor(t): |
| 1241 | + return (None, None, float("nan")) |
| 1242 | + tf = t.float() |
| 1243 | + fin = tf[torch.isfinite(tf)] |
| 1244 | + return (int(torch.isnan(tf).sum()), int(torch.isinf(tf).sum()), |
| 1245 | + float(fin.abs().max()) if fin.numel() else float("nan")) |
| 1246 | + |
| 1247 | + def _mk(label): |
| 1248 | + def _h(mod, inp, out): |
| 1249 | + if _pb["done"]: |
| 1250 | + return |
| 1251 | + it = inp[0] if isinstance(inp, (tuple, list)) and inp else inp |
| 1252 | + inn = int(torch.isnan(it.float()).sum()) if torch.is_tensor(it) else -1 |
| 1253 | + nan, inf, amax = _pstat(out) |
| 1254 | + _pb["ev"].append((label, inn, nan, inf, amax)) |
| 1255 | + return _h |
| 1256 | + |
| 1257 | + for i, m in enumerate(_dec): |
| 1258 | + _pb["h"].append(m.register_forward_hook( |
| 1259 | + _mk(f"LAYER[{i:02d}] ({_lt[i] if i < len(_lt) else '?'})"))) |
| 1260 | + if i == _first_full: |
| 1261 | + for sn, sm in m.named_modules(): |
| 1262 | + if sn: |
| 1263 | + _pb["h"].append(sm.register_forward_hook( |
| 1264 | + _mk(f" L{i}.{sn} <{sm.__class__.__name__}>"))) |
| 1265 | + |
| 1266 | + def _dump(mod, inp, out): |
| 1267 | + if _pb["done"]: |
| 1268 | + return |
| 1269 | + _pb["done"] = True |
| 1270 | + print("\n=== [NAN-PROBE] first forward, completion order ===") |
| 1271 | + fb = None |
| 1272 | + for label, inn, nan, inf, amax in _pb["ev"]: |
| 1273 | + bad = (nan and nan > 0) or (inf and inf > 0) |
| 1274 | + mark = " <<< FIRST NaN/Inf" if bad and fb is None else "" |
| 1275 | + if bad and fb is None: |
| 1276 | + fb = label |
| 1277 | + print(f"{label:<50} in_nan={inn} out_nan={nan} out_inf={inf} " |
| 1278 | + f"absmax={amax:.4g}{mark}") |
| 1279 | + print(f"[NAN-PROBE] first NaN/Inf at: {fb}\n", flush=True) |
| 1280 | + for h in _pb["h"]: |
| 1281 | + h.remove() |
| 1282 | + _dec[-1].register_forward_hook(_dump) |
| 1283 | + print(f"[NAN-PROBE] installed: {len(_dec)} decoder layers + submodules of " |
| 1284 | + f"layer {_first_full} (first full_attention)") |
| 1285 | + |
1166 | 1286 | # ─── Calibration corpus + baseline PPL (paged forward proves the swap works) ─── |
1167 | 1287 | print(f"[calib] loading {'budget ' + str(args.token_budget) + ' tok' if args.token_budget else str(args.n_samples) + ' samples'} " |
1168 | 1288 | f"seq_len={args.seq_len} corpus={args.corpus}") |
|
0 commit comments