diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 4cb68a5241..d40428a301 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -277,7 +277,7 @@ jobs:
coverage-test:
name: Full Code Coverage Test
runs-on: ubuntu-latest
- # Suite runs under pytest-xdist on a 2-vCPU runner (~45 min with swap);
+ # Suite runs under pytest-xdist on a 2-vCPU runner (~35 min with swap);
# 75 catches a real hang well below the old serial 90.
timeout-minutes: 75
steps:
@@ -285,14 +285,20 @@ jobs:
- name: Add swap space
# The 2-vCPU runner has ~7GB RAM; two xdist workers each holding
# torch + resident models overflow it. Swap absorbs the model-load
- # spikes (mirrors the notebook-checks job).
+ # spikes. 8G left the suite's heavy tail one xdist scheduling roll
+ # from the runner killing the step at ~95% (identical SHAs pass and
+ # fail), so 16G — on the ~65G /mnt temp disk, keeping the OS disk
+ # free for the uv env and model caches.
run: |
sudo swapoff /swapfile 2>/dev/null || true
sudo rm -f /swapfile
- sudo fallocate -l 8G /swapfile
- sudo chmod 600 /swapfile
- sudo mkswap /swapfile
- sudo swapon /swapfile
+ sudo swapoff /mnt/swapfile 2>/dev/null || true
+ sudo rm -f /mnt/swapfile
+ sudo fallocate -l 16G /mnt/swapfile
+ sudo chmod 600 /mnt/swapfile
+ sudo mkswap /mnt/swapfile
+ sudo swapon /mnt/swapfile
+ free -h && df -h / /mnt
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
@@ -358,6 +364,15 @@ jobs:
# Worker count knob (this runner is 2-vCPU, so -n auto = 2). If swap
# still can't hold peak memory, drop to "-n 1" or grow the swapfile.
XDIST_ARGS: "-n auto --dist loadscope"
+ - name: Memory post-mortem
+ # The tail-of-suite kills leave no cause in the step log; dmesg names
+ # the killer (kernel OOM, systemd-oomd, or neither) for the next one.
+ if: failure()
+ run: |
+ sudo dmesg | tail -n 80 || true
+ free -h || true
+ swapon --show || true
+ df -h / /mnt || true
- name: Build check
run: uv build
- name: Upload Coverage Report Artifact
diff --git a/.gitignore b/.gitignore
index 570bd085ca..090cbb72da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ docs/source/generated
!.claude/commands/
.adapter-progress.json
transformer_lens/tools/model_registry/data/verification_checkpoint.json
+venv/
diff --git a/demos/BERT.ipynb b/demos/BERT.ipynb
index 63404a85af..e8670d454b 100644
--- a/demos/BERT.ipynb
+++ b/demos/BERT.ipynb
@@ -307,7 +307,7 @@
"output_type": "stream",
"text": [
"Prompt: ['The [MASK] is bright today.', 'She [MASK] to the store.', 'The dog [MASK] the ball.']\n",
- "Prediction: \"['Prediction 0: sun', 'Prediction 1: returned', 'Prediction 2: has']\"\n"
+ "Prediction: \"['Prediction 0: sun', 'Prediction 1: went', 'Prediction 2: caught']\"\n"
]
}
],
@@ -499,7 +499,13 @@
"\n",
"inputs = tokenizer(sentence_a, sentence_b, return_tensors=\"pt\")\n",
"device = next(nsp.parameters()).device\n",
- "predictions = nsp(inputs[\"input_ids\"].to(device), return_type=\"predictions\")\n",
+ "# token_type_ids mark where sentence A ends and B begins — without them the NSP\n",
+ "# head sees one undifferentiated span and can return the wrong verdict.\n",
+ "predictions = nsp(\n",
+ " inputs[\"input_ids\"].to(device),\n",
+ " token_type_ids=inputs[\"token_type_ids\"].to(device),\n",
+ " return_type=\"predictions\",\n",
+ ")\n",
"\n",
"print(f\"Sentence A: {sentence_a}\")\n",
"print(f\"Sentence B: {sentence_b}\")\n",
diff --git a/demos/Grokking_Demo.ipynb b/demos/Grokking_Demo.ipynb
index 0eb0f88fe3..b2046db581 100644
--- a/demos/Grokking_Demo.ipynb
+++ b/demos/Grokking_Demo.ipynb
@@ -784,7 +784,9 @@
"model_checkpoints = []\n",
"checkpoint_epochs = []\n",
"if TRAIN_MODEL:\n",
- " for epoch in tqdm.tqdm(range(num_epochs)):\n",
+ "# mininterval throttles tqdm below Jupyter's IOPub rate limit while keeping a\n",
+ "# visible progress bar.\n",
+ " for epoch in tqdm.tqdm(range(num_epochs), mininterval=2):\n",
" train_logits = model(train_data)\n",
" train_loss = loss_fn(train_logits, train_labels)\n",
" train_loss.backward()\n",
@@ -1867,53 +1869,18 @@
},
{
"cell_type": "code",
- "execution_count": 45,
+ "execution_count": null,
"metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "key_fourier_embed torch.Size([8, 128])\n"
- ]
- },
- {
- "data": {
- "text/html": [
- "
"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
- "key_freqs = [17, 25, 32, 47]\n",
- "key_freq_indices = [33, 34, 49, 50, 63, 64, 93, 94]\n",
+ "# Derive the key frequencies from this run's embedding instead of hardcoding\n",
+ "# values from a past run — they vary with seed and training length.\n",
+ "fourier_norms = (fourier_basis @ W_E).norm(dim=-1)\n",
+ "key_freq_indices = [\n",
+ " i for i, norm in enumerate(fourier_norms) if i > 0 and norm > fourier_norms.max() / 4\n",
+ "]\n",
+ "key_freqs = sorted({(i + 1) // 2 for i in key_freq_indices})\n",
+ "print(\"key_freqs\", key_freqs)\n",
"fourier_embed = fourier_basis @ W_E\n",
"key_fourier_embed = fourier_embed[key_freq_indices]\n",
"print(\"key_fourier_embed\", key_fourier_embed.shape)\n",
diff --git a/demos/doc_sanitize.cfg b/demos/doc_sanitize.cfg
index fd4388894d..4926ba521f 100644
--- a/demos/doc_sanitize.cfg
+++ b/demos/doc_sanitize.cfg
@@ -37,3 +37,7 @@ replace: \1
[regex7]
regex: [^\n]*DeprecationWarning:(?=\n\nHookedTransformer is deprecated and will be removed in 4\.0\. Use TransformerBridge\.boot_transformers\(\.\.\.\) instead, then call enable_compatibility_mode\(\) for HookedTransformer-equivalent numerics\.)
replace: DeprecationWarning:
+
+[regex8]
+regex: /(?:var|tmp|private)/\S*
+replace: TMP-PATH
diff --git a/docs/source/content/contributing.md b/docs/source/content/contributing.md
index 20e10f5ce1..02cadb3990 100644
--- a/docs/source/content/contributing.md
+++ b/docs/source/content/contributing.md
@@ -328,7 +328,7 @@ set -a; source .env; set +a
uv run python -m transformer_lens.tools.model_registry.verify_models --model
```
-`verify_models` runs phases 1–4 (forward correctness vs HF, hook firing + gradients, weight processing, generation quality) and updates `data/supported_models.json` with the resulting status and per-phase scores. We recommend running `--dry-run` first to project memory and parameter count without loading the model, and verifying one model at a time — concurrent loads tend to OOM a single device.
+`verify_models` runs phases 1–4 (forward correctness vs HF, hook firing + gradients, weight processing, text-generation quality) and updates `data/supported_models.json` with the resulting status and per-phase scores. We recommend running `--dry-run` first to project memory and parameter count without loading the model, and verifying one model at a time — concurrent loads tend to OOM a single device.
Running with `--no-hf-reference` skips the HuggingFace numerical comparison (Phase 1 becomes structural-only). A passing run is then recorded as **provisional** (status 4), which does *not* count as verified — re-run without the flag for a real HF-compared verification.
@@ -341,11 +341,11 @@ It's worth reading the per-phase scores in addition to the final status — the
| 1 | 100% | — | Verification fails |
| 2 | 75% | `logits_equivalence`, `loss_equivalence` | Verification fails |
| 3 | 75% | `logits_equivalence`, `loss_equivalence` | Verification fails |
-| 4 | 50% | — | **Non-gating** — below 50% adds `"low text quality"` to the registry `note`; never fails verification. |
+| 4 | 54.5% (measured pass line, `p4_pass_threshold()`) | — | **Non-gating** — below the line adds a `"text quality poor (P4=…)"` note; never fails verification. |
| 7 | 75% | `multimodal_forward` | Verification fails. A NULL score also fails. |
| 8 | 75% | `audio_forward` | Verification fails. A NULL score also fails. |
-Phase 4 is intentionally lenient — it's a coherence metric, not a correctness check. A sub-100% Phase-4 score on a small parity-test model can still indicate a real adapter bug that the gates don't catch (missing `preprocess_weights` fold, wrong `default_prepend_bos`, and so on); the model can pass verification overall and still be worth a manual look.
+Phase 4 prompts each model with its resolved prompt profile (chat template, translation, code, own-language continuation, ...) and scores the generation against a known-good reference with one pinned multilingual judge, via the perplexity ratio `PPL(generated)/PPL(reference)`. It's intentionally lenient — a coherence metric, not a correctness check. A sub-100% Phase-4 score on a small parity-test model can still indicate a real adapter bug that the gates don't catch (missing `preprocess_weights` fold, wrong `default_prepend_bos`, and so on); the model can pass verification overall and still be worth a manual look.
If verification fails by `~1e-3` or more against the HF reference, the bisection workflow lives at [Debugging Numerical Divergence](debugging_numerical_divergence.md).
diff --git a/docs/source/content/jacobian_lens_fitting.md b/docs/source/content/jacobian_lens_fitting.md
index 8c19095f82..6323c7bae3 100644
--- a/docs/source/content/jacobian_lens_fitting.md
+++ b/docs/source/content/jacobian_lens_fitting.md
@@ -318,6 +318,49 @@ exact values will not necessarily transfer.
time." Here `k` is an **upper bound**: `support` returns *at most* `k` active vectors (often
fewer), never `k` padded with zero-coefficient slots.
+### Occupancy and fraction of variance
+
+Two statistics turn the honesty bullets above into numbers you can measure. Both build on J-lens
+vector dictionaries and sparse supports, but occupancy uses its own projection-residual recurrence.
+
+`occupancy` estimates **how many J-lens vectors are meaningfully active** in a single activation —
+the quantity behind the paper's `k <= 25`. At each step it admits the unused atom with the greatest
+signed, norm-normalized correlation with the current residual, projects the activation onto the full
+selected span, and sets the next residual to `x - Pi_S x`. This is the same per-step correlation
+*rule* as `decompose`, but `decompose` recurses on a nonnegative coefficient-fit residual and may
+stop early, whereas occupancy selects exactly `max_atoms` atoms, so their supports need not match.
+Occupancy records the per-step captured variance `||Pi_S x||^2 / ||x||^2` and compares that curve
+against the same occupancy recurrence on `num_control_dictionaries` random unit-norm dictionaries.
+The occupancy is the step of **maximum separation** between the real and averaged-control
+*cumulative* captured variance — the point past which further vectors add no more than random
+directions would. It is deterministic given `seed` and needs no threshold.
+
+```python
+occ = lens.occupancy(model, "The Eiffel Tower is in the city of", layer=6, position=-1)
+occ.occupancy # int: meaningfully-active vector count (a small positive integer)
+occ.marginal_captured_variance # [max_atoms] real per-step captured-variance gains (for plotting)
+occ.control_captured_variance # [max_atoms] averaged random-control gains
+```
+
+`fraction_of_variance` profiles the **J-space share of activation variance over a prompt corpus**.
+For each `(layer, position)` at or past `skip_first` (mirroring the fit's early-position skip) it
+records `||j_space_component||^2 / ||activation||^2` — the `selected_support` span projection,
+matching the paper's appendix operationalization, **not** the nonnegative `reconstruction`. Per
+layer it reports the `median` of those fractions and the `pooled` ratio
+`sum(||j_space||^2) / sum(||activation||^2)`.
+Each token tensor must represent one prompt and have shape `[1, seq]`; `skip_first` must be
+non-negative even when explicit `positions` override its sampling behavior.
+
+```python
+profile = lens.fraction_of_variance(model, prompts, layers=[3, 6], k=8)
+profile.median # {layer: median fraction} -- the paper's "median 6-7%" quantity
+profile.pooled # {layer: pooled ratio in [0, 1]}
+```
+
+Both are **shape** claims on open weights: expect a small occupancy and a small variance fraction,
+but do not expect the paper's closed-model figures (see *Interpreting the numbers honestly* above)
+to transfer numerically.
+
The full-vocabulary dictionary is cached on the model's device and is vocabulary-sized
(gigabytes for large models); release it with `lens.clear_device_cache()`.
diff --git a/docs/source/content/migrating_to_v3.md b/docs/source/content/migrating_to_v3.md
index f5047bf2fe..65881ac18c 100644
--- a/docs/source/content/migrating_to_v3.md
+++ b/docs/source/content/migrating_to_v3.md
@@ -122,7 +122,7 @@ For the full mapping of legacy → canonical names and the expected tensor shape
Two semantic differences inside `enable_compatibility_mode()` worth knowing if you are porting activation-patching, DLA, or attribution-patching code:
-- **`blocks.{i}.hook_mlp_in` fires pre-ln2** (matching legacy `HookedTransformer`). Use `bridge.set_use_hook_mlp_in(True)` to enable it — setting `cfg.use_hook_mlp_in = True` directly is honored when blocks share the bridge's `cfg`, but the setter is the supported entry point. The pre-ln2 placement means cached values from one run can be patched into another and re-flow through `ln2 → mlp` consistently across the bridge and `HookedTransformer`.
+- **`blocks.{i}.hook_mlp_in` fires pre-ln2** (matching legacy `HookedTransformer`). Enable it with `bridge.set_use_hook_mlp_in(True)` or `bridge.cfg.use_hook_mlp_in = True`; direct config assignment routes through the same validation and propagation path as the setter. The pre-ln2 placement means cached values from one run can be patched into another and re-flow through `ln2 → mlp` consistently across the bridge and `HookedTransformer`.
- **`hook_q_input` / `hook_k_input` / `hook_v_input` / `hook_attn_in`** also fire pre-ln1 in compat mode. On the per-head LN application that follows, the bridge routes through the raw HF norm rather than the `NormalizationBridge` wrapper, so `ln1`'s sub-hooks (`hook_in`, `hook_normalized`, `hook_scale`) do **not** fire once per head the way legacy `LayerNormPre` would. Q/K/V projections downstream still match legacy numerically; only the intermediate LN sub-hook firing is suppressed.
Post-norm architectures (OLMo 2, BERT-style encoders) and MLA blocks (DeepSeek V2/V3/R1) do not participate in the pre-ln1 capture — `MLABlockBridge` does not expose those aliases, and post-norm models would read the post-attention residual instead of the block input.
@@ -144,7 +144,35 @@ If your code only touches these APIs, the migration is genuinely just the loadin
### BERT Next Sentence Prediction
-`BertNextSentencePrediction` is not ported to `TransformerBridge`. Keep using `HookedEncoder` + `BertNextSentencePrediction` for NSP workflows. The bridge's BERT adapter does load NSP HuggingFace checkpoints (it rewires the unembed to `cls.seq_relationship`), but the high-level NSP API – sentence-pair tokenization, `[CLS]` pooling, "sequential"/"not sequential" decoding — is not exposed. If this is feature is something you'd like added to TransformerBridge, please file an issue.
+NSP runs on the bridge today — load the NSP head via `model_class` and pass the
+sentence-pair tokenization through:
+
+```python
+from transformers import AutoTokenizer, BertForNextSentencePrediction
+from transformer_lens.model_bridge import TransformerBridge
+
+tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-cased")
+nsp = TransformerBridge.boot_transformers(
+ "google-bert/bert-base-cased",
+ model_class=BertForNextSentencePrediction,
+)
+nsp.enable_compatibility_mode()
+
+inputs = tokenizer("A man walked into a grocery store.", "He bought an apple.", return_tensors="pt")
+nsp(inputs["input_ids"], token_type_ids=inputs["token_type_ids"], return_type="predictions")
+# 'The sentences are sequential'
+```
+
+**Pass `token_type_ids`.** They are what tells BERT where the first sentence ends
+and the second begins; without them the NSP head scores a single undifferentiated
+span and can return the wrong verdict (on the pair above, dropping them collapses
+the logits from ±4.37 to ±0.58, and a genuinely non-sequential pair flips to
+"sequential"). With them, the bridge reproduces the raw HuggingFace NSP logits
+exactly.
+
+The legacy `BertNextSentencePrediction` wrapper is deprecated and cannot wrap a
+`TransformerBridge` — it reaches for `HookedEncoder`-only internals
+(`encoder_output`, `pooler`, `nsp_head`). Use the recipe above instead.
### New in 3.x: streaming generation
diff --git a/scripts/phase4_review.py b/scripts/phase4_review.py
new file mode 100644
index 0000000000..3b6e98fbc9
--- /dev/null
+++ b/scripts/phase4_review.py
@@ -0,0 +1,72 @@
+"""Registry-wide Phase-4 review: which verified models' stored scores predate
+the profile rework and deserve a re-run.
+
+phase4_score is a mixed-scale column: entries stamped p4_scoring_version=2
+were measured with the pinned-judge reference-ratio scoring (pass line 56);
+unstamped entries carry the old GPT-2 absolute-perplexity scale (pass line 85)
+and are never compared against the new line — they are re-run candidates.
+Read-only.
+"""
+
+import argparse
+
+from transformer_lens.benchmarks.text_quality_profiles import (
+ P4_SCORING_VERSION,
+ resolve_profile,
+)
+from transformer_lens.tools.model_registry.registry_io import load_supported_models_raw
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--below", type=float, default=None, help="Only scores below this")
+ parser.add_argument("--limit", type=int, default=None, help="Max rows per section")
+ args = parser.parse_args()
+
+ current: list = []
+ stale: list = []
+ for entry in load_supported_models_raw().get("models", []):
+ if entry.get("status") != 1 or entry.get("phase4_score") is None:
+ continue
+ score = entry["phase4_score"]
+ if args.below is not None and score >= args.below:
+ continue
+ profile = str(
+ resolve_profile(
+ entry["model_id"], entry.get("architecture_id"), entry.get("prompt_profile")
+ )
+ )
+ row = (profile != "continuation", score, entry["model_id"], profile)
+ if entry.get("p4_scoring_version") == P4_SCORING_VERSION:
+ current.append(row)
+ else:
+ stale.append(row)
+
+ # Profile-changed first, then ascending score: measurement changed most.
+ for rows in (current, stale):
+ rows.sort(key=lambda r: (not r[0], r[1]))
+ if args.limit:
+ current = current[: args.limit]
+ stale = stale[: args.limit]
+
+ print(
+ f"{len(current)} scored on the current scale (v{P4_SCORING_VERSION}); "
+ f"{len(stale)} on the old GPT-2 scale (re-run candidates)\n"
+ )
+ for title, rows in (
+ (f"v{P4_SCORING_VERSION} (reference-ratio scale, pass 56)", current),
+ ("v1 (GPT-2 scale — scores NOT comparable to the new pass line)", stale),
+ ):
+ if not rows:
+ continue
+ changed = sum(1 for r in rows if r[0])
+ print(f"== {title}: {len(rows)} models, {changed} non-default profiles")
+ print(f"{'score':>6} {'profile':<28} model")
+ for is_changed, score, model_id, profile in rows:
+ marker = "*" if is_changed else " "
+ print(f"{score:6.1f}{marker} {profile:<28} {model_id}")
+ print()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/text_quality_judge_bakeoff.py b/scripts/text_quality_judge_bakeoff.py
new file mode 100644
index 0000000000..95aed50529
--- /dev/null
+++ b/scripts/text_quality_judge_bakeoff.py
@@ -0,0 +1,464 @@
+"""Bake off perplexity judges for the reworked Phase-4 text-quality benchmark.
+
+Scores two small causal LMs as candidate PPL judges: for each of 9 languages
+(8 natural + "code"), build a fluent corpus from ``text_quality_profiles`` and
+six deterministic corruptions per fluent string (shuffle/repeat/charnoise x2/
+crosslang x2). The judge that best separates fluent from corrupted text by
+ROC AUC, worst-language-first, wins; its R_FAIL/R_GOOD thresholds and
+per-reference PPLs are then emitted for the real benchmark to consume.
+
+Run: uv run python scripts/text_quality_judge_bakeoff.py
+ uv run python scripts/text_quality_judge_bakeoff.py --languages en,fr --models gpt2
+"""
+from __future__ import annotations
+
+import argparse
+import gc
+import json
+import math
+import random
+import statistics
+import sys
+import time
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Optional
+
+import numpy as np
+import torch
+from huggingface_hub import HfApi
+from transformers import AutoModelForCausalLM, AutoTokenizer
+
+from transformer_lens.benchmarks.text_quality_profiles import (
+ CHAT_PROMPTS,
+ CONTINUATION_PROMPTS,
+ PIVOT_SENTENCES,
+)
+
+# All PIVOT_SENTENCES languages plus code, so no scored language is left
+# uncalibrated (it/nl/pt/hi were absent from the original judge selection run).
+DEFAULT_LANGUAGES = [
+ "en",
+ "fr",
+ "es",
+ "de",
+ "it",
+ "nl",
+ "pt",
+ "zh",
+ "ar",
+ "ru",
+ "ja",
+ "hi",
+ "code",
+]
+DEFAULT_MODELS = ["bigscience/bloom-560m", "Qwen/Qwen2.5-0.5B"]
+
+NO_SPACE_LANGS = {"zh", "ja"}
+CHARNOISE_RATES = (0.15, 0.4)
+CROSSLANG_RATES = (0.3, 0.7)
+
+OUT_JSON = Path("judge_reference_ppls.json") # cwd; override with --out
+
+# ---------------------------------------------------------------------------
+# Fluent corpus
+# ---------------------------------------------------------------------------
+
+
+def build_fluent_corpus(languages: list[str]) -> dict[str, list[str]]:
+ """Fluent strings per language: pivot sentences + continuation/chat references."""
+ corpus: dict[str, list[str]] = {}
+ for lang in languages:
+ if lang == "code":
+ corpus[lang] = [p.reference for p in CONTINUATION_PROMPTS["code"]]
+ continue
+ texts = list(PIVOT_SENTENCES.get(lang, ()))
+ texts += [p.reference for p in CONTINUATION_PROMPTS.get(lang, ())]
+ texts += [p.reference for p in CHAT_PROMPTS.get(lang, ())]
+ corpus[lang] = texts
+ return corpus
+
+
+# ---------------------------------------------------------------------------
+# Corruptions (deterministic given a shared random.Random)
+# ---------------------------------------------------------------------------
+
+
+def tokenize_units(text: str, lang: str) -> list[str]:
+ """Words for space-delimited languages, characters for zh/ja."""
+ return list(text) if lang in NO_SPACE_LANGS else text.split()
+
+
+def join_units(units: list[str], lang: str) -> str:
+ return "".join(units) if lang in NO_SPACE_LANGS else " ".join(units)
+
+
+def corrupt_shuffle(text: str, lang: str, rng: random.Random) -> str:
+ """Permute word (or char) order."""
+ units = tokenize_units(text, lang)
+ rng.shuffle(units)
+ return join_units(units, lang)
+
+
+def corrupt_repeat(text: str, lang: str) -> str:
+ """Repeat the first 3 words (5 chars for zh/ja) until the original length."""
+ units = tokenize_units(text, lang)
+ if not units:
+ return text
+ n = 5 if lang in NO_SPACE_LANGS else 3
+ seed = units[:n] or units
+ out = [seed[i % len(seed)] for i in range(len(units))]
+ return join_units(out, lang)
+
+
+def corrupt_charnoise(text: str, rate: float, rng: random.Random) -> str:
+ """Swap-adjacent-or-delete at `rate` of character positions."""
+ chars = list(text)
+ out: list[str] = []
+ i = 0
+ while i < len(chars):
+ if rng.random() < rate:
+ if i + 1 < len(chars) and rng.random() < 0.5:
+ out.append(chars[i + 1])
+ out.append(chars[i])
+ i += 2
+ continue
+ i += 1 # delete
+ continue
+ out.append(chars[i])
+ i += 1
+ return "".join(out)
+
+
+def corrupt_crosslang(
+ text: str, lang: str, rate: float, other_units: list[str], rng: random.Random
+) -> str:
+ """Replace `rate` of tokens with tokens drawn from another language's fluent text."""
+ units = tokenize_units(text, lang)
+ if not units or not other_units:
+ return text
+ n_replace = min(len(units), max(1, round(rate * len(units))))
+ idxs = rng.sample(range(len(units)), n_replace)
+ out = units[:]
+ for idx in idxs:
+ out[idx] = rng.choice(other_units)
+ return join_units(out, lang)
+
+
+@dataclass
+class CorruptedSample:
+ """One corrupted variant of a fluent source string."""
+
+ lang: str
+ source_text: str
+ kind: str
+ severity: Optional[float]
+ text: str
+
+
+def build_corruptions(
+ languages: list[str], fluent_corpus: dict[str, list[str]], seed: int = 42
+) -> dict[str, list[CorruptedSample]]:
+ """6 corrupted variants per fluent string, generated once and shared by both candidates."""
+ rng = random.Random(seed)
+ other_pool: dict[str, list[str]] = {}
+ for i, lang in enumerate(languages):
+ next_lang = languages[(i + 1) % len(languages)]
+ other_text = " ".join(fluent_corpus.get(next_lang, []))
+ other_pool[lang] = tokenize_units(other_text, lang)
+
+ by_lang: dict[str, list[CorruptedSample]] = {lang: [] for lang in languages}
+ for lang in languages:
+ for text in fluent_corpus[lang]:
+ samples = by_lang[lang]
+ samples.append(
+ CorruptedSample(lang, text, "shuffle", None, corrupt_shuffle(text, lang, rng))
+ )
+ samples.append(CorruptedSample(lang, text, "repeat", None, corrupt_repeat(text, lang)))
+ for rate in CHARNOISE_RATES:
+ samples.append(
+ CorruptedSample(
+ lang, text, "charnoise", rate, corrupt_charnoise(text, rate, rng)
+ )
+ )
+ for rate in CROSSLANG_RATES:
+ samples.append(
+ CorruptedSample(
+ lang,
+ text,
+ "crosslang",
+ rate,
+ corrupt_crosslang(text, lang, rate, other_pool[lang], rng),
+ )
+ )
+ return by_lang
+
+
+# ---------------------------------------------------------------------------
+# PPL scoring
+# ---------------------------------------------------------------------------
+
+
+def _with_retry(fn, *args, **kwargs): # type: ignore[no-untyped-def]
+ """One retry after 60s on a 429/rate-limit error."""
+ try:
+ return fn(*args, **kwargs)
+ except Exception as exc: # noqa: BLE001
+ msg = str(exc)
+ if "429" in msg or "rate limit" in msg.lower():
+ print(f"429 hit, retrying in 60s: {msg}", file=sys.stderr)
+ time.sleep(60)
+ return fn(*args, **kwargs)
+ raise
+
+
+def score_text(text: str, tokenizer, model) -> Optional[dict]: # type: ignore[no-untyped-def]
+ """NLL-based PPL for one string; None if tokenization has <2 tokens."""
+ enc = tokenizer(text, return_tensors="pt")
+ ids = enc["input_ids"]
+ n_tokens = int(ids.shape[1])
+ if n_tokens < 2:
+ return None
+ t0 = time.perf_counter()
+ with torch.no_grad():
+ out = model(input_ids=ids, labels=ids)
+ dt = time.perf_counter() - t0
+ ppl = math.exp(out.loss.item())
+ unk_id = tokenizer.unk_token_id
+ unk_count = int((ids == unk_id).sum().item()) if unk_id is not None else None
+ return {"ppl": ppl, "n_tokens": n_tokens, "unk_count": unk_count, "dt": dt}
+
+
+# ---------------------------------------------------------------------------
+# AUC (hand-rolled, rank-based Mann-Whitney)
+# ---------------------------------------------------------------------------
+
+
+def auc_score(neg_scores: list[float], pos_scores: list[float]) -> float:
+ """P(pos > neg) via rank-sum; positive = corrupted, negative = fluent."""
+ n_pos, n_neg = len(pos_scores), len(neg_scores)
+ if n_pos == 0 or n_neg == 0:
+ return float("nan")
+ combined = [(s, 0) for s in neg_scores] + [(s, 1) for s in pos_scores]
+ combined.sort(key=lambda x: x[0])
+ n = len(combined)
+ ranks = [0.0] * n
+ i = 0
+ while i < n:
+ j = i
+ while j < n and combined[j][0] == combined[i][0]:
+ j += 1
+ avg_rank = (i + 1 + j) / 2.0 # 1-indexed, averaged over the tie block
+ for k in range(i, j):
+ ranks[k] = avg_rank
+ i = j
+ rank_sum_pos = sum(r for r, (_, lbl) in zip(ranks, combined) if lbl == 1)
+ return (rank_sum_pos - n_pos * (n_pos + 1) / 2) / (n_pos * n_neg)
+
+
+# ---------------------------------------------------------------------------
+# Per-candidate run
+# ---------------------------------------------------------------------------
+
+
+def run_candidate(
+ model_id: str,
+ languages: list[str],
+ fluent_corpus: dict[str, list[str]],
+ corrupted_by_lang: dict[str, list[CorruptedSample]],
+) -> dict:
+ print(f"Loading {model_id} ...")
+ tokenizer = _with_retry(AutoTokenizer.from_pretrained, model_id)
+ model = _with_retry(AutoModelForCausalLM.from_pretrained, model_id, dtype=torch.float32)
+ model.eval()
+
+ fluent_ppl: dict[str, dict[str, float]] = {}
+ lang_stats: dict[str, dict] = {}
+ total_dt = 0.0
+ total_forwards = 0
+
+ for lang in languages:
+ fluent_ppl[lang] = {}
+ ln_fluent: list[float] = []
+ has_unk = tokenizer.unk_token_id is not None
+ unk_count = 0
+ unk_total = 0
+
+ for text in fluent_corpus[lang]:
+ res = score_text(text, tokenizer, model)
+ if res is None:
+ continue
+ total_forwards += 1
+ total_dt += res["dt"]
+ fluent_ppl[lang][text] = res["ppl"]
+ ln_fluent.append(math.log(res["ppl"]))
+ if has_unk:
+ unk_count += res["unk_count"]
+ unk_total += res["n_tokens"]
+
+ ln_corrupt: list[float] = []
+ ln_ratios: list[float] = []
+ ratio_raw: list[float] = []
+ for sample in corrupted_by_lang[lang]:
+ if sample.source_text not in fluent_ppl[lang]:
+ continue # source string itself was skipped (too short)
+ res = score_text(sample.text, tokenizer, model)
+ if res is None:
+ continue
+ total_forwards += 1
+ total_dt += res["dt"]
+ ppl_src = fluent_ppl[lang][sample.source_text]
+ ln_corrupt.append(math.log(res["ppl"]))
+ ln_ratios.append(math.log(res["ppl"]) - math.log(ppl_src))
+ ratio_raw.append(res["ppl"] / ppl_src)
+ if has_unk:
+ unk_count += res["unk_count"]
+ unk_total += res["n_tokens"]
+
+ lang_stats[lang] = {
+ "auc": auc_score(ln_fluent, ln_corrupt),
+ "median_ln_ratio": float(np.median(ln_ratios)) if ln_ratios else float("nan"),
+ "unk_rate": (unk_count / unk_total) if has_unk and unk_total else None,
+ "mean_fluent_ppl": (
+ float(np.mean(list(fluent_ppl[lang].values())))
+ if fluent_ppl[lang]
+ else float("nan")
+ ),
+ "ratio_raw": ratio_raw,
+ }
+
+ avg_forward_time = total_dt / total_forwards if total_forwards else float("nan")
+ del model
+ gc.collect()
+ return {
+ "fluent_ppl": fluent_ppl,
+ "lang_stats": lang_stats,
+ "avg_forward_time": avg_forward_time,
+ }
+
+
+# ---------------------------------------------------------------------------
+# Winner selection + constants
+# ---------------------------------------------------------------------------
+
+
+def pick_winner(results: dict[str, dict], languages: list[str]) -> tuple[str, str]:
+ def min_auc(name: str) -> float:
+ return min(results[name]["lang_stats"][l]["auc"] for l in languages)
+
+ def spread(name: str) -> float:
+ vals = [results[name]["lang_stats"][l]["median_ln_ratio"] for l in languages]
+ return max(vals) - min(vals)
+
+ def speed(name: str) -> float:
+ return results[name]["avg_forward_time"]
+
+ names = sorted(results, key=min_auc, reverse=True)
+ best = min_auc(names[0])
+ tied = [n for n in names if best - min_auc(n) <= 0.01]
+ if len(tied) == 1:
+ return tied[0], "highest minimum per-language AUC"
+
+ best_spread = min(spread(n) for n in tied)
+ tied2 = [n for n in tied if spread(n) == best_spread]
+ if len(tied2) == 1:
+ return tied2[0], "min-AUC tie -> smaller cross-language ln-ratio spread"
+
+ winner = min(tied2, key=speed)
+ return winner, "min-AUC tie -> spread tie -> faster wall-clock per forward"
+
+
+def compute_r_fail(winner_stats: dict, languages: list[str]) -> float:
+ """Geo-mean over languages of the MEDIAN corrupted/fluent ratio.
+
+ A low percentile degenerates below 1 in weak-separation languages (some
+ corruptions do not raise perplexity there), which would invert the log
+ mapping; the median is the robust "typical broken output" anchor. This is
+ the exact derivation of the shipped JUDGE_R_FAIL."""
+ per_lang = []
+ for lang in languages:
+ raw = winner_stats["lang_stats"][lang]["ratio_raw"]
+ if raw:
+ per_lang.append(float(np.median(raw)))
+ return statistics.geometric_mean(per_lang)
+
+
+def compute_r_good(winner_stats: dict, languages: list[str]) -> float:
+ per_lang = []
+ for lang in languages:
+ vals = list(winner_stats["fluent_ppl"][lang].values())
+ ratios = [vals[i] / vals[j] for i in range(len(vals)) for j in range(len(vals)) if i != j]
+ if ratios:
+ per_lang.append(float(np.percentile(ratios, 90)))
+ return statistics.geometric_mean(per_lang)
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+
+def parse_args() -> argparse.Namespace:
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument("--languages", default=",".join(DEFAULT_LANGUAGES))
+ p.add_argument("--models", default=",".join(DEFAULT_MODELS))
+ p.add_argument("--out", type=Path, default=OUT_JSON, help="Where to write reference PPLs")
+ return p.parse_args()
+
+
+def main() -> None:
+ args = parse_args()
+ languages = [l.strip() for l in args.languages.split(",") if l.strip()]
+ models = [m.strip() for m in args.models.split(",") if m.strip()]
+
+ api = HfApi()
+ for model_id in models:
+ info = _with_retry(api.model_info, model_id)
+ print(f"{model_id} revision sha: {info.sha}")
+
+ fluent_corpus = build_fluent_corpus(languages)
+ corrupted_by_lang = build_corruptions(languages, fluent_corpus)
+
+ results: dict[str, dict] = {}
+ for model_id in models:
+ results[model_id] = run_candidate(model_id, languages, fluent_corpus, corrupted_by_lang)
+
+ header = f"{'candidate':28s} {'lang':6s} {'auc':>7s} {'med_ln_ratio':>13s} {'unk_rate':>9s} {'mean_ppl':>10s}"
+ print("\n" + header)
+ print("-" * len(header))
+ for model_id in models:
+ for lang in languages:
+ st = results[model_id]["lang_stats"][lang]
+ unk_str = f"{st['unk_rate']:.4f}" if st["unk_rate"] is not None else "n/a"
+ print(
+ f"{model_id:28s} {lang:6s} {st['auc']:7.3f} {st['median_ln_ratio']:13.3f} "
+ f"{unk_str:>9s} {st['mean_fluent_ppl']:10.2f}"
+ )
+
+ winner, reason = pick_winner(results, languages)
+ print(f"\nwinner: {winner} ({reason})")
+ for model_id in models:
+ print(
+ f" {model_id}: avg forward wall-clock {results[model_id]['avg_forward_time']*1000:.2f} ms"
+ )
+
+ r_fail = compute_r_fail(results[winner], languages)
+ r_good = compute_r_good(results[winner], languages)
+ print(f"JUDGE_R_FAIL = {r_fail:.1f}")
+ print(f"JUDGE_R_GOOD = {r_good:.2f}")
+ print(f"pass line score(R_GOOD) = {100 - 100 * math.log(r_good) / math.log(r_fail):.1f}")
+
+ flagged = [
+ lang
+ for lang in languages
+ if all(results[m]["lang_stats"][lang]["auc"] < 0.8 for m in models)
+ ]
+ print(f"languages with AUC < 0.8 for BOTH candidates: {flagged or 'none'}")
+
+ args.out.parent.mkdir(parents=True, exist_ok=True)
+ args.out.write_text(json.dumps(results[winner]["fluent_ppl"], ensure_ascii=False, indent=2))
+ print(f"\nreference PPLs written to {args.out}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/QUARANTINES.md b/tests/QUARANTINES.md
index 049c050701..89d835d4e0 100644
--- a/tests/QUARANTINES.md
+++ b/tests/QUARANTINES.md
@@ -79,14 +79,11 @@ Big-model adapter tests use `@pytest.mark.slow`, CI tier filters `-m "not slow"`
| Path | Reason | Issue |
|---|---|---|
-| [`unit/model_bridge/test_bridge_generate_no_tokenizer.py`:30,128](unit/model_bridge/test_bridge_generate_no_tokenizer.py) | `skipif(_MACOS_ARM64)` — KV-cache NaN | Upstream PyTorch/HF on M-series Macs |
-| [`integration/model_bridge/test_bridge_generate_stopping_criteria.py`](integration/model_bridge/test_bridge_generate_stopping_criteria.py) | `skipif(_MACOS_ARM64)`, KV-cache NaN (one `use_past_kv_cache=True` test) | Upstream PyTorch/HF on M-series Macs |
| [`acceptance/test_hooked_transformer.py`](acceptance/test_hooked_transformer.py) | `redwood_attn_2l` (2 tests) — `ArthurConmy/redwood_tokenizer`'s merges name a token missing from its vocab (`Ġpati`), rejected by tokenizers >= 0.20 on both the fast and slow paths | Third-party repo; the weights load fine, only the tokenizer is unusable |
-**Un-skip:** when upstream resolves. Don't bypass — produces NaN logits. The redwood skip is
-evaluated at collection by actually attempting the load, so it disappears on its own if the repo
-is fixed or the tokenizers constraint relaxes; substituting a different tokenizer is not a fix
-(it would change token ids and invalidate the pinned expected loss).
+**Un-skip:** evaluated at collection by actually attempting the load, so it disappears on its own
+if the repo is fixed or the tokenizers constraint relaxes; substituting a different tokenizer is
+not a fix (it would change token ids and invalidate the pinned expected loss).
---
@@ -94,22 +91,6 @@ is fixed or the tokenizers constraint relaxes; substituting a different tokenize
No modules are currently quarantined this way.
-**Resolved 2026-08-17.** `acceptance/test_hooked_transformer.py`, `test_hooked_encoder.py` and
-`test_hooked_encoder_decoder.py` had carried module-level `pytest.mark.skip(reason="CI test
-pollution")` since #1129. Re-running them found no pollution: each passes alone, and the full
-acceptance tier with all three enabled is green (231 passed, 39 skipped). What the skips were
-hiding was four genuine failures, all now fixed at the source:
-
-| Was failing | Actual cause |
-|---|---|
-| `test_bert_block` | transformers 5.x returns a tensor from `BertLayer.forward`, so the test's `[0]` took batch element 0 instead of tuple element 0 |
-| `test_bloom_similarity_*` (×2) | the HF fixture loaded bloom at its checkpoint dtype (fp16) while TL loads fp32 — the comparison measured HF's own fp16 error (0.259 log-softmax against *itself*), not TL |
-| `test_model[redwood_attn_2l]`, `test_from_pretrained_no_processing[redwood_attn_2l]` | `ArthurConmy/redwood_tokenizer` has merges referencing a token absent from its vocab, which tokenizers >= 0.20 rejects; see the per-test skip below |
-
-Two silent TransformerLens bugs also lived in this blind spot the whole time: T5's decoder
-self-attention was never causally masked, and its relative-position bias used the encoder's
-bucketing. Both are fixed. Keep these modules enabled.
-
---
## Technical debt — individual
diff --git a/tests/acceptance/model_bridge/test_run_with_cache_batch.py b/tests/acceptance/model_bridge/test_run_with_cache_batch.py
index 3600c42ce7..8462a498de 100644
--- a/tests/acceptance/model_bridge/test_run_with_cache_batch.py
+++ b/tests/acceptance/model_bridge/test_run_with_cache_batch.py
@@ -7,6 +7,19 @@
import torch
+from transformer_lens.utilities import get_attention_mask
+
+
+def _last_real_token_positions(model, prompts: list[str]) -> torch.Tensor:
+ tokens = model.to_tokens(prompts)
+ attention_mask = get_attention_mask(
+ model.tokenizer,
+ tokens,
+ prepend_bos=getattr(model.cfg, "default_prepend_bos", True),
+ )
+ positions = torch.arange(tokens.shape[1], device=tokens.device).expand_as(tokens)
+ return positions.masked_fill(attention_mask == 0, -1).max(dim=1).values
+
def test_run_with_cache_batch_matches_individual(gpt2_bridge):
"""Batched run_with_cache logits at the last real token should match per-prompt runs."""
@@ -23,9 +36,9 @@ def test_run_with_cache_batch_matches_individual(gpt2_bridge):
# Batched run
batched_logits, _ = gpt2_bridge.run_with_cache(prompts)
- # With left-padding forced internally, position -1 is the last real token
- for i in range(len(prompts)):
- batched_last = batched_logits[i, -1, :]
+ last_real_positions = _last_real_token_positions(gpt2_bridge, prompts)
+ for i, position in enumerate(last_real_positions):
+ batched_last = batched_logits[i, position, :]
assert torch.allclose(
individual_logits[i], batched_last, atol=1e-4
), f"Prompt {i} logit mismatch between individual and batched run_with_cache"
@@ -54,11 +67,11 @@ def capture_individual(tensor, hook):
# Batched run
captured_batched = []
+ last_real_positions = _last_real_token_positions(gpt2_bridge, prompts)
def capture_batched(tensor, hook):
- # For left-padded batch, last real token is at position -1 for all
- for i in range(tensor.shape[0]):
- captured_batched.append(tensor[i, -1, :].detach().clone())
+ for i, position in enumerate(last_real_positions):
+ captured_batched.append(tensor[i, position, :].detach().clone())
gpt2_bridge.run_with_hooks(
prompts,
diff --git a/tests/integration/benchmarks/__init__.py b/tests/integration/benchmarks/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/integration/benchmarks/test_text_quality_profiles.py b/tests/integration/benchmarks/test_text_quality_profiles.py
new file mode 100644
index 0000000000..1e1f96fa64
--- /dev/null
+++ b/tests/integration/benchmarks/test_text_quality_profiles.py
@@ -0,0 +1,171 @@
+"""End-to-end Phase-4 profile scoring against real models and the real judge.
+
+Each test guards a profile path that unit stubs cannot: the Marian test keeps
+seq2seq whole-output scoring (pre-profile P4 masked a prompt "continuation"
+that seq2seq output does not have and scored 0); the Florence-2 test keeps the
+caption path (text-only prompts yield a bare EOS on image-conditioned models);
+the judge tests pin the revision and the fluent-vs-corrupted separation the
+bake-off measured.
+"""
+
+import pytest
+
+pytest.importorskip("transformers")
+
+from transformer_lens.benchmarks.text_quality import benchmark_text_quality
+
+
+def _boot(model_id, **kwargs):
+ from transformer_lens.model_bridge import TransformerBridge
+
+ try:
+ return TransformerBridge.boot_transformers(model_id, device="cpu", **kwargs)
+ except (OSError, ConnectionError, TimeoutError) as exc:
+ pytest.skip(f"{model_id} unavailable offline: {exc}")
+
+
+@pytest.fixture(scope="module")
+def judge():
+ from transformer_lens.benchmarks.text_quality import load_judge
+
+ try:
+ return load_judge()
+ except (OSError, ConnectionError, TimeoutError) as exc:
+ pytest.skip(f"judge unavailable offline: {exc}")
+
+
+def test_translation_profile_marian(judge):
+ """Seq2seq output is standalone (a translation), not a continuation of the
+ prompt; the translation profile must score it whole, against the pivot
+ reference, in the direction parsed from the model id."""
+ bridge = _boot("Helsinki-NLP/opus-mt-nl-en")
+ assert bridge.original_model.config.is_encoder_decoder # precondition
+
+ judge_model, judge_tokenizer = judge
+ result = benchmark_text_quality(
+ bridge,
+ "task:translation@nl-en",
+ judge_model=judge_model,
+ judge_tokenizer=judge_tokenizer,
+ )
+ assert result.details is not None, result.message
+ assert result.details["prompt_profile"] == "task:translation@nl-en"
+ # A working translator of 3 short pivot sentences must land well above the
+ # broken floor (score 0 = judge's typical-corruption perplexity ratio).
+ assert result.details["score"] > 50.0, result.details
+
+
+def test_caption_profile_florence2(judge):
+ """Florence-2 emits a bare EOS for text-only prompts; P4 must drive real
+ image-conditioned captions and score them under the caption profile."""
+ pytest.importorskip("PIL")
+ bridge = _boot("florence-community/Florence-2-base", trust_remote_code=True)
+
+ judge_model, judge_tokenizer = judge
+ result = benchmark_text_quality(
+ bridge,
+ "continuation", # deliberately wrong: the caption adjustment must win
+ judge_model=judge_model,
+ judge_tokenizer=judge_tokenizer,
+ )
+ assert result.details is not None, result.message
+ assert result.details["prompt_profile"] == "caption"
+ assert result.details["score"] > 0.0
+
+
+def test_chat_profile_templates_and_scores(judge):
+ """Chat models are scored through their own template (prepend_bos=False —
+ the template supplies BOS); output must not be the template markers."""
+ bridge = _boot("Qwen/Qwen2.5-0.5B-Instruct")
+
+ judge_model, judge_tokenizer = judge
+ result = benchmark_text_quality(
+ bridge,
+ "chat",
+ judge_model=judge_model,
+ judge_tokenizer=judge_tokenizer,
+ )
+ assert result.details is not None, result.message
+ assert result.details["prompt_profile"] == "chat"
+ assert "<|im_start|>" not in result.details["generated_text"]
+ assert result.details["score"] > 50.0, result.details
+
+
+def test_fluent_vs_shuffled_separation_end_to_end(judge):
+ """The full scoring chain must separate real model output from word salad:
+ any break (mask slip, ratio inversion, penalty loss) collapses the gap."""
+ import random
+
+ from transformer_lens.benchmarks.text_quality import (
+ _compute_repetition_penalty,
+ _judge_perplexity,
+ _ratio_to_score,
+ )
+ from transformer_lens.benchmarks.text_quality_profiles import CONTINUATION_PROMPTS
+
+ judge_model, judge_tokenizer = judge
+ entry = CONTINUATION_PROMPTS["en"][0]
+ # A DISTINCT on-topic fluent paraphrase, not the reference itself: ref/ref
+ # is identically 1 -> 100 and would pass with the judge deleted.
+ # Measured: ppl 8.3 vs ref 5.1 -> score 83.2 (judge conditioned on the
+ # relativity prompt correctly rejects off-topic fluent text).
+ fluent = (
+ " measurements of time and distance depend on the observer's motion,"
+ " so no single frame of reference is absolute."
+ )
+ words = entry.reference.split()
+ random.Random(42).shuffle(words)
+ shuffled = " ".join(words)
+
+ ref_ppl, err = _judge_perplexity(entry.reference, entry.prompt, judge_tokenizer, judge_model)
+ assert err is None
+ fluent_ppl, err = _judge_perplexity(fluent, entry.prompt, judge_tokenizer, judge_model)
+ assert err is None
+ fluent_score = _ratio_to_score(fluent_ppl / ref_ppl) * _compute_repetition_penalty(fluent)
+ shuf_ppl, err = _judge_perplexity(shuffled, entry.prompt, judge_tokenizer, judge_model)
+ assert err is None
+ shuffled_score = _ratio_to_score(shuf_ppl / ref_ppl) * _compute_repetition_penalty(shuffled)
+
+ assert fluent_score >= 60.0, fluent_score
+ assert shuffled_score < 50.0, (shuf_ppl, ref_ppl)
+ assert fluent_score - shuffled_score >= 30.0
+
+
+def test_reference_perplexities_match_pinned_values(judge):
+ """Judge-revision/reference drift guard: the judge's perplexity on a few
+ fixed reference strings must match values measured at bake-off time
+ (2026-08-20, Qwen2.5-0.5B@060db649, fp32 CPU). A judge unpin or a silent
+ reference edit moves these."""
+ judge_model, judge_tokenizer = judge
+ from transformer_lens.benchmarks.text_quality import _judge_perplexity
+ from transformer_lens.benchmarks.text_quality_profiles import PIVOT_SENTENCES
+
+ pinned = {
+ ("en", 0): 30.79,
+ ("fr", 0): 81.85,
+ ("zh", 0): 53.15,
+ }
+ for (lang, idx), expected in pinned.items():
+ ppl, err = _judge_perplexity(PIVOT_SENTENCES[lang][idx], "", judge_tokenizer, judge_model)
+ assert err is None
+ assert ppl == pytest.approx(expected, rel=0.15), (lang, idx, ppl)
+
+
+def test_continuation_references_share_a_scale(judge):
+ """Per-language reference PPLs must sit within 3.5x of the language
+ median: an outlier reference makes its prompt's bar proportionally looser
+ (the old en[3] measured 31.3 vs median 8.9 and handed gpt2 a clamp-100 on
+ output worse than its 62-scoring sibling prompt)."""
+ judge_model, judge_tokenizer = judge
+ from transformer_lens.benchmarks.text_quality import _judge_perplexity
+ from transformer_lens.benchmarks.text_quality_profiles import CONTINUATION_PROMPTS
+
+ for lang, prompts in CONTINUATION_PROMPTS.items():
+ ppls = []
+ for pp in prompts:
+ ppl, err = _judge_perplexity(pp.reference, pp.prompt, judge_tokenizer, judge_model)
+ assert err is None, (lang, err)
+ ppls.append(ppl)
+ median = sorted(ppls)[len(ppls) // 2]
+ for i, ppl in enumerate(ppls):
+ assert ppl <= 3.5 * median, (lang, i, round(ppl, 1), round(median, 1))
diff --git a/tests/integration/model_bridge/compatibility/test_bridge_cache_behavior.py b/tests/integration/model_bridge/compatibility/test_bridge_cache_behavior.py
index 0a347cac49..506f84b6af 100644
--- a/tests/integration/model_bridge/compatibility/test_bridge_cache_behavior.py
+++ b/tests/integration/model_bridge/compatibility/test_bridge_cache_behavior.py
@@ -128,9 +128,8 @@ class TestCacheEqualityWithHookedTransformer:
def test_cache_values_match(self, bridge_compat, reference_ht):
"""Cache activations should match between bridge and HookedTransformer.
- Note: Raw attention scores use different masking sentinels:
- HookedTransformer uses -inf, Bridge uses torch.finfo(dtype).min.
- Unmasked scores and resulting patterns should still match.
+ Compatibility mode normalizes masked attention scores to -inf, matching
+ HookedTransformer. Unmasked scores and resulting patterns should match.
"""
prompt = "Hello World!"
_, bridge_cache = bridge_compat.run_with_cache(prompt)
@@ -148,7 +147,7 @@ def test_cache_values_match(self, bridge_compat, reference_ht):
), f"Shape mismatch for {hook}: {ht_act.shape} vs {bridge_act.shape}"
if hook == "blocks.0.attn.hook_attn_scores":
- # Different masking sentinels — compare only unmasked positions
+ # Compare the informative, unmasked scores separately.
masked = torch.isinf(ht_act)
unmasked = ~masked
assert torch.allclose(
diff --git a/tests/integration/model_bridge/helpers.py b/tests/integration/model_bridge/helpers.py
index 19e271323b..ebf5cfbf9c 100644
--- a/tests/integration/model_bridge/helpers.py
+++ b/tests/integration/model_bridge/helpers.py
@@ -1,8 +1,36 @@
"""Shared helpers for bridge integration tests."""
+import copy
+
import torch
+def make_tiny_pair(hf_config, arch_name, *, loader=None):
+ """Seeded tiny (bridge, ref) pair sharing identical weights.
+
+ ``loader(config) -> model`` builds each side (default
+ ``AutoModelForCausalLM.from_config``); ref keeps the seeded init, hf gets a
+ state-dict copy, and the bridge wraps hf with eager attention on cpu.
+ """
+ from transformers import AutoModelForCausalLM
+
+ from transformer_lens.model_bridge.sources._bridge_builder import (
+ build_bridge_from_module,
+ )
+
+ if loader is None:
+ loader = AutoModelForCausalLM.from_config
+ hf_config._attn_implementation = "eager"
+ torch.manual_seed(42)
+ ref = loader(hf_config).eval()
+ hf = loader(copy.deepcopy(hf_config)).eval()
+ hf.load_state_dict(ref.state_dict())
+ bridge = build_bridge_from_module(
+ hf, arch_name, hf_config=copy.deepcopy(hf_config), tokenizer=None, device="cpu"
+ ).eval()
+ return bridge, ref
+
+
def assert_bridge_matches_hf(bridge, *args, atol: float = 1e-5, **kwargs) -> None:
"""Assert the bridge's logits match its wrapped HF model on the same inputs.
diff --git a/tests/integration/model_bridge/test_attention_score_sentinel.py b/tests/integration/model_bridge/test_attention_score_sentinel.py
new file mode 100644
index 0000000000..424198df58
--- /dev/null
+++ b/tests/integration/model_bridge/test_attention_score_sentinel.py
@@ -0,0 +1,67 @@
+"""Compatibility-mode attention-score sentinel regression coverage."""
+
+import torch
+
+SCORES = "blocks.0.attn.hook_attn_scores"
+PATTERN = "blocks.0.attn.hook_pattern"
+
+
+def test_gpt2_compatibility_scores_use_negative_infinity(
+ gpt2_bridge_compat, gpt2_hooked_processed
+) -> None:
+ """GPT-2's direct HF mask is normalized before the compatibility hook."""
+ tokens = gpt2_hooked_processed.to_tokens("The capital of France is")
+ _, bridge_cache = gpt2_bridge_compat.run_with_cache(tokens, names_filter=[SCORES])
+ _, hooked_cache = gpt2_hooked_processed.run_with_cache(tokens, names_filter=[SCORES])
+
+ bridge_scores, hooked_scores = bridge_cache[SCORES], hooked_cache[SCORES]
+ causal_mask = torch.isneginf(hooked_scores)
+ assert causal_mask.any()
+ assert torch.isneginf(bridge_scores[causal_mask]).all()
+ torch.testing.assert_close(
+ bridge_scores[~causal_mask], hooked_scores[~causal_mask], rtol=0, atol=0
+ )
+
+
+def test_gpt2_left_padding_uses_negative_infinity_and_finite_patterns(
+ gpt2_bridge_compat,
+) -> None:
+ """Fully masked pad queries are zeroed after softmax in compatibility mode."""
+ long = gpt2_bridge_compat.to_tokens("The capital of France is")
+ short = gpt2_bridge_compat.to_tokens("Paris")
+ n_pad = long.shape[1] - short.shape[1]
+ padded_short = torch.cat([torch.zeros_like(long[:, :n_pad]), short], dim=1)
+ tokens = torch.cat([long, padded_short], dim=0)
+ attention_mask = torch.cat(
+ [
+ torch.ones_like(long),
+ torch.cat([torch.zeros_like(long[:, :n_pad]), torch.ones_like(short)], dim=1),
+ ],
+ dim=0,
+ )
+
+ _, cache = gpt2_bridge_compat.run_with_cache(
+ tokens, attention_mask=attention_mask, names_filter=[SCORES, PATTERN]
+ )
+ scores, pattern = cache[SCORES], cache[PATTERN]
+ key_padding = ~attention_mask.bool()[:, None, None, :]
+ causal = torch.triu(torch.ones(long.shape[1], long.shape[1], dtype=torch.bool), diagonal=1)[
+ None, None
+ ]
+ masked = (key_padding | causal).expand_as(scores)
+
+ assert torch.isneginf(scores[masked]).all()
+ assert torch.isfinite(pattern).all()
+
+
+def test_gpt2_mixed_dtype_mask_is_normalized_before_addition(gpt2_bridge_compat) -> None:
+ """A lower-precision HF mask sentinel must survive score upcasting."""
+ scores = torch.zeros(1, 1, 2, 2, dtype=torch.float32)
+ attention_mask = torch.zeros_like(scores, dtype=torch.float16)
+ attention_mask[..., 0, 1] = torch.finfo(torch.float16).min
+
+ actual = gpt2_bridge_compat.blocks[0].attn._apply_reconstruct_attention_mask(
+ scores, attention_mask, seq_len=2
+ )
+
+ assert torch.isneginf(actual[..., 0, 1]).all()
diff --git a/tests/integration/model_bridge/test_attention_weight_accessors.py b/tests/integration/model_bridge/test_attention_weight_accessors.py
index 5390471f86..8afc91dbd5 100644
--- a/tests/integration/model_bridge/test_attention_weight_accessors.py
+++ b/tests/integration/model_bridge/test_attention_weight_accessors.py
@@ -51,7 +51,7 @@ def llama_bridge():
@pytest.fixture(scope="module")
-def gpt2_bridge():
+def tiny_gpt2_bridge():
"""Tiny GPT-2: square Conv1D c_proj (control — no-transpose path must stay correct)."""
from transformers import GPT2Config, GPT2LMHeadModel
@@ -116,8 +116,8 @@ def test_w_v_uses_kv_heads(self, llama_bridge):
class TestConv1DAccessorParity:
- def test_w_o_reproduces_c_proj(self, gpt2_bridge):
- bridge, hf_model = gpt2_bridge
+ def test_w_o_reproduces_c_proj(self, tiny_gpt2_bridge):
+ bridge, hf_model = tiny_gpt2_bridge
attn = bridge.blocks[0].attn
w_o = attn.W_O
assert w_o.shape == (4, 16, 64)
@@ -126,3 +126,59 @@ def test_w_o_reproduces_c_proj(self, gpt2_bridge):
expected = c_proj(z.reshape(2, 64))
actual = torch.einsum("bhd,hdm->bm", z, w_o) + attn.b_O
assert torch.allclose(actual, expected, atol=1e-5)
+
+
+class TestWeightCircuitsGQA:
+ """QK/OV/composition circuits expand grouped K/V to n_heads (issue #1553).
+
+ Pre-fix, every property below raised at FactoredMatrix construction on GQA
+ models because the grouped [n_kv_heads] axis cannot broadcast against the
+ per-query-head [n_heads] axis.
+ """
+
+ def test_qk_factors_align_to_query_heads(self, llama_bridge):
+ bridge, _ = llama_bridge
+ QK = bridge.QK
+ assert QK.A.shape == (2, 4, 64, 16)
+ assert QK.B.shape == (2, 4, 16, 64)
+ # Query head h reads kv head h // (n_heads // n_kv_heads).
+ for h in range(4):
+ assert torch.equal(QK.B[0, h], bridge.blocks[0].attn.W_K[h // 2].T)
+
+ def test_ov_factors_align_to_query_heads(self, llama_bridge):
+ bridge, _ = llama_bridge
+ OV = bridge.OV
+ assert OV.A.shape == (2, 4, 64, 16)
+ assert OV.B.shape == (2, 4, 16, 64)
+ for h in range(4):
+ assert torch.equal(OV.A[1, h], bridge.blocks[1].attn.W_V[h // 2])
+
+ def test_for_attn_layers_variants_align(self, llama_bridge):
+ bridge, _ = llama_bridge
+ indices, QK = bridge.QK_for_attn_layers()
+ assert indices == [0, 1]
+ assert QK.A.shape == (2, 4, 64, 16)
+ assert QK.B.shape == (2, 4, 16, 64)
+ _, OV = bridge.OV_for_attn_layers()
+ assert OV.A.shape == (2, 4, 64, 16)
+ assert OV.B.shape == (2, 4, 16, 64)
+
+ @pytest.mark.parametrize("mode", ["Q", "K", "V"])
+ def test_composition_scores_cover_all_query_heads(self, llama_bridge, mode):
+ bridge, _ = llama_bridge
+ result = bridge.all_composition_scores(mode)
+ assert result.scores.shape == (2, 4, 2, 4)
+ assert len(result.head_labels) == 8
+
+ def test_raw_kv_stacks_stay_grouped(self, llama_bridge):
+ bridge, _ = llama_bridge
+ assert bridge.W_K.shape == (2, 2, 64, 16)
+ assert bridge.W_V.shape == (2, 2, 64, 16)
+
+ def test_mha_circuits_untouched(self, tiny_gpt2_bridge):
+ bridge, _ = tiny_gpt2_bridge
+ QK, OV = bridge.QK, bridge.OV
+ assert torch.equal(QK.A, bridge.W_Q)
+ assert torch.equal(QK.B, bridge.W_K.transpose(-2, -1))
+ assert torch.equal(OV.A, bridge.W_V)
+ assert torch.equal(OV.B, bridge.W_O)
diff --git a/tests/integration/model_bridge/test_batched_generate_position_ids.py b/tests/integration/model_bridge/test_batched_generate_position_ids.py
new file mode 100644
index 0000000000..a4e6224242
--- /dev/null
+++ b/tests/integration/model_bridge/test_batched_generate_position_ids.py
@@ -0,0 +1,133 @@
+"""position_ids handling during batched-list generation.
+
+Batched list input is left-padded internally, so each row's real tokens start at
+a different offset. The bridge derives position_ids for that, but only models
+that neither reject the kwarg nor derive positions themselves may receive them
+(#1626).
+
+The cached decoding path needs the same gate as the prompt path. Every branch
+there supplies position_ids, including a ``total_len - 1`` fallback that counts
+pad slots, so gating only the prompt derivation diverts a refused model into the
+fallback instead of leaving it alone.
+
+OPT is the vehicle for the refused case: ``OPTLearnedPositionalEmbedding``
+consumes the attention mask and derives its own positions, so the gate declines
+it, while its forward would happily accept the kwarg and use it.
+"""
+
+from __future__ import annotations
+
+import functools
+
+import pytest
+import torch
+
+GREEDY = dict(max_new_tokens=4, do_sample=False, verbose=False)
+PROMPTS = ["The capital of France is the city of", "Hi"]
+
+
+@pytest.fixture(scope="module")
+def opt_bridge():
+ """A model the gate refuses. Its positional embedding reads the mask."""
+ from transformer_lens.model_bridge import TransformerBridge
+
+ bridge = TransformerBridge.boot_transformers(
+ "hf-internal-testing/tiny-random-OPTForCausalLM", device="cpu", dtype=torch.float32
+ )
+ bridge.eval()
+ return bridge
+
+
+def _stack_logits(output) -> torch.Tensor:
+ logits = output.logits
+ return torch.stack(list(logits)) if isinstance(logits, (list, tuple)) else logits
+
+
+def _position_ids_per_step(bridge, use_past_kv_cache: bool) -> list:
+ """The position_ids each forward actually received, one entry per step."""
+ seen: list = []
+ original = bridge.original_model.forward
+
+ # functools.wraps so the gate still sees the real signature; a bare
+ # (*args, **kwargs) spy would look like it accepts position_ids.
+ @functools.wraps(original)
+ def _spy(*args, **kwargs):
+ supplied = kwargs.get("position_ids")
+ seen.append(None if supplied is None else supplied.tolist())
+ return original(*args, **kwargs)
+
+ bridge.original_model.forward = _spy
+ try:
+ bridge.generate(list(PROMPTS), use_past_kv_cache=use_past_kv_cache, **GREEDY)
+ finally:
+ bridge.original_model.forward = original
+ return seen
+
+
+def test_gate_refuses_opt(opt_bridge) -> None:
+ """Guards the premise of the tests below: OPT must be the refused case."""
+ assert opt_bridge._accepts_derived_position_ids() is False
+
+
+def test_refused_model_generates_identically_with_and_without_cache(opt_bridge) -> None:
+ """Cached decoding must not change the answer.
+
+ Compared on logits rather than decoded text on purpose: greedy argmax
+ absorbs the drift and the strings match even when the positions are wrong.
+ """
+ cached = opt_bridge.generate(
+ list(PROMPTS), use_past_kv_cache=True, output_logits=True, **GREEDY
+ )
+ uncached = opt_bridge.generate(
+ list(PROMPTS), use_past_kv_cache=False, output_logits=True, **GREEDY
+ )
+
+ torch.testing.assert_close(_stack_logits(cached), _stack_logits(uncached), rtol=0, atol=1e-5)
+
+
+def test_refused_model_receives_no_position_ids_on_cached_steps(opt_bridge) -> None:
+ """The mechanism, not just the symptom.
+
+ The fallback supplies a per-batch constant, so a coarser check can miss it;
+ assert the kwarg never reaches a model that derives positions itself.
+ """
+ assert _position_ids_per_step(opt_bridge, use_past_kv_cache=True) == [None] * (
+ GREEDY["max_new_tokens"]
+ )
+
+
+def test_accepted_model_still_receives_per_row_position_ids(distilgpt2_bridge) -> None:
+ """The gate must not disarm the models it was never meant to exclude."""
+ if distilgpt2_bridge.tokenizer.pad_token_id is None:
+ distilgpt2_bridge.tokenizer.pad_token = distilgpt2_bridge.tokenizer.eos_token
+
+ seen = _position_ids_per_step(distilgpt2_bridge, use_past_kv_cache=True)
+
+ assert seen[0] is not None, "prompt step must still receive derived positions"
+ cached_steps = [step for step in seen[1:] if step is not None]
+ assert len(cached_steps) == len(seen) - 1, "cached steps must still be supplied"
+ # Row 1 ("Hi") is left-padded, so its position must be strictly lower than
+ # row 0's. A pad-counting fallback would give both rows the same value.
+ first_cached = cached_steps[0]
+ assert first_cached[1][0] < first_cached[0][0], first_cached
+
+
+def test_accepted_model_generates_identically_with_and_without_cache(distilgpt2_bridge) -> None:
+ """Control for the parity property on a model the gate allows.
+
+ Looser than the OPT case at 1e-3. Cached decoding and full recompute sum in
+ different orders, which on distilgpt2's logit scale of ~132 shows as 1.4e-04,
+ or 1e-06 relative. The regression this guards moves logits by ~0.3, so the
+ margin is still more than two orders of magnitude.
+ """
+ if distilgpt2_bridge.tokenizer.pad_token_id is None:
+ distilgpt2_bridge.tokenizer.pad_token = distilgpt2_bridge.tokenizer.eos_token
+
+ cached = distilgpt2_bridge.generate(
+ list(PROMPTS), use_past_kv_cache=True, output_logits=True, **GREEDY
+ )
+ uncached = distilgpt2_bridge.generate(
+ list(PROMPTS), use_past_kv_cache=False, output_logits=True, **GREEDY
+ )
+
+ torch.testing.assert_close(_stack_logits(cached), _stack_logits(uncached), rtol=0, atol=1e-3)
diff --git a/tests/integration/model_bridge/test_bert_verification_fixes.py b/tests/integration/model_bridge/test_bert_verification_fixes.py
new file mode 100644
index 0000000000..b637432e2c
--- /dev/null
+++ b/tests/integration/model_bridge/test_bert_verification_fixes.py
@@ -0,0 +1,93 @@
+"""The four defects that kept google-bert/bert-base-cased at FAILED since June.
+
+1. The tokenizer fallback installs bos_token='<|endoftext|>' on BERT (which has
+ none) and to_tokens string-prepended it, WordPiece-shredding it into 8 subword
+ tokens at the front of every input.
+2. The tokenizer_prepends_bos probe compared position 0 against that fake BOS id
+ instead of recognizing [CLS], desyncing the bridge from HookedTransformer.
+3. The component harness fed float tensors to token_type_embed (an nn.Embedding).
+4. Phase 2 loaded the masked LM into HookedTransformer — a causal decoder — and
+ graded the bridge against a bidirectional model run under a causal mask.
+"""
+
+import pytest
+import torch
+
+from transformer_lens.model_bridge import TransformerBridge
+
+MODEL = "google-bert/bert-base-cased"
+TEXT = (
+ "Natural language processing tasks, such as question answering, "
+ "machine translation, reading comprehension, and summarization, "
+ "are typically approached with supervised learning."
+)
+
+
+@pytest.fixture(scope="module")
+def bert():
+ return TransformerBridge.boot_transformers(MODEL, device="cpu")
+
+
+def test_probe_recognizes_cls_as_prepended_bos(bert) -> None:
+ assert bert.cfg.tokenizer_prepends_bos is True
+
+
+def test_to_tokens_is_clean_wordpiece(bert) -> None:
+ tokens = bert.to_tokens(TEXT)
+ assert tokens.shape[1] == 32, tokens.shape
+ decoded = bert.tokenizer.convert_ids_to_tokens(tokens[0])
+ assert decoded[:2] == ["[CLS]", "Natural"], decoded[:6]
+
+
+def test_prepend_false_strips_cls_matching_hooked_transformer(bert) -> None:
+ assert bert.to_tokens(TEXT, prepend_bos=False).shape[1] == 31
+
+
+def test_non_atomic_bos_is_never_string_prepended(bert) -> None:
+ """Defense in depth: even with the flag forced to the manual-prepend path,
+ a BOS the tokenizer would shred must not be prepended."""
+ original = bert.cfg.tokenizer_prepends_bos
+ bert.cfg.tokenizer_prepends_bos = False
+ try:
+ tokens = bert.to_tokens(TEXT, prepend_bos=True)
+ finally:
+ bert.cfg.tokenizer_prepends_bos = original
+ assert tokens.shape[1] == 32, tokens.shape # 40 when the shred happens
+ decoded = bert.tokenizer.convert_ids_to_tokens(tokens[0])
+ assert decoded[1] == "Natural", decoded[:6]
+
+
+def test_component_harness_feeds_ints_to_embedding_tables(bert) -> None:
+ from transformers import AutoModelForMaskedLM
+
+ from transformer_lens.benchmarks.component_benchmark import benchmark_all_components
+
+ hf = AutoModelForMaskedLM.from_pretrained(MODEL, dtype=torch.float32).eval()
+ result = benchmark_all_components(bert, hf)
+ assert result.passed, result.message
+ assert "token_type_embed" not in str(result.details), result.details
+
+
+def test_phase2_never_grades_against_a_causal_reference() -> None:
+ """A masked LM loaded into HookedTransformer runs bidirectional weights under
+ a causal mask — every comparison against it is noise. Phase 2 must skip that
+ reference; numerical checks fall back to the Phase 1 HF logits."""
+ from transformer_lens.benchmarks.main_benchmark import run_benchmark_suite
+ from transformer_lens.benchmarks.utils import BenchmarkSeverity
+
+ results = run_benchmark_suite(
+ model_name=MODEL,
+ device="cpu",
+ phases=[1, 2],
+ use_hf_reference=True,
+ use_ht_reference=True,
+ enable_compatibility_mode=False,
+ verbose=False,
+ track_memory=False,
+ )
+ hard_failures = [
+ r
+ for r in results
+ if not r.passed and r.severity not in (BenchmarkSeverity.SKIPPED, BenchmarkSeverity.WARNING)
+ ]
+ assert not hard_failures, [f"{r.name}: {r.message[:90]}" for r in hard_failures]
diff --git a/tests/integration/model_bridge/test_bloom_gated_hooks.py b/tests/integration/model_bridge/test_bloom_gated_hooks.py
new file mode 100644
index 0000000000..21ce1a609d
--- /dev/null
+++ b/tests/integration/model_bridge/test_bloom_gated_hooks.py
@@ -0,0 +1,44 @@
+"""Bloom's gated attention hooks must fire like every other joint-QKV bridge.
+
+`BloomAttentionBridge` overrides both `forward` and `_reconstruct_attention`, and
+the overrides projected Q/K/V directly and always took the plain output
+projection. `hook_result`, `hook_q_input`, `hook_k_input`, `hook_v_input` and
+`hook_attn_in` therefore never fired — enabling `use_attn_result` on a Bloom
+model silently produced nothing.
+"""
+
+import pytest
+import torch
+
+from transformer_lens.benchmarks import benchmark_gated_hooks_fire
+from transformer_lens.model_bridge import TransformerBridge
+
+MODEL = "bigscience/bloom-560m"
+PROMPT = "The theory of relativity explains that the speed of light"
+
+
+@pytest.fixture(scope="module")
+def bridge():
+ return TransformerBridge.boot_transformers(MODEL, device="cpu")
+
+
+def test_every_gated_hook_fires(bridge) -> None:
+ result = benchmark_gated_hooks_fire(bridge, PROMPT)
+ assert result.passed, result.message
+ fired = (result.details or {}).get("fired_counts", {})
+ assert fired and all(count > 0 for count in fired.values()), fired
+
+
+@pytest.mark.parametrize("flag", ["use_attn_result", "use_split_qkv_input", "use_attn_in"])
+def test_gated_paths_preserve_the_output(bridge, flag: str) -> None:
+ """The fork re-parameterizes the same math, so logits must not move beyond
+ the op-order noise a correct implementation shows (gpt2 1e-4, pythia 4e-3)."""
+ tokens = bridge.to_tokens(PROMPT)
+ with torch.no_grad():
+ baseline = bridge(tokens).float()
+ setattr(bridge.cfg, flag, True)
+ try:
+ gated = bridge(tokens).float()
+ finally:
+ setattr(bridge.cfg, flag, False)
+ torch.testing.assert_close(gated, baseline, atol=5e-3, rtol=1e-3)
diff --git a/tests/integration/model_bridge/test_bridge_generate_stopping_criteria.py b/tests/integration/model_bridge/test_bridge_generate_stopping_criteria.py
index 92256c4d90..7dacaf8002 100644
--- a/tests/integration/model_bridge/test_bridge_generate_stopping_criteria.py
+++ b/tests/integration/model_bridge/test_bridge_generate_stopping_criteria.py
@@ -18,14 +18,11 @@
cached-eager-attention path can NaN (issue #1322).
"""
-import platform
import pytest
import torch
from transformers import StoppingCriteria, StoppingCriteriaList
-_MACOS_ARM64 = platform.system() == "Darwin" and platform.machine() == "arm64"
-
# Common kwargs for the greedy, macOS-safe, token-returning generate calls below.
_GEN = dict(do_sample=False, use_past_kv_cache=False, return_type="tokens", verbose=False)
@@ -225,7 +222,6 @@ def test_batched_generation_stops(bridge_with_pad):
assert torch.equal(out1, out2), "batched greedy generation must be deterministic"
-@pytest.mark.skipif(_MACOS_ARM64, reason="Upstream macOS-arm64 KV-cache NaN, see issue #1322.")
def test_stop_string_with_kv_cache(bridge):
"""stop_strings also works on the default KV-cache path (not only the no-cache path)."""
tokens = bridge.to_tokens("The quick brown")
diff --git a/tests/integration/model_bridge/test_bridge_integration.py b/tests/integration/model_bridge/test_bridge_integration.py
index 3e2a8a79ec..9ae02e4068 100644
--- a/tests/integration/model_bridge/test_bridge_integration.py
+++ b/tests/integration/model_bridge/test_bridge_integration.py
@@ -573,73 +573,6 @@ def test_get_params_configuration_mismatch():
bridge.cfg.n_layers = original_n_layers
-def test_get_params_multi_query_attention_reshaping():
- """Test Multi-Query Attention weight reshaping logic without requiring a large model.
-
- This test verifies that the get_params function can correctly handle different
- weight shapes that occur in Multi-Query Attention architectures, where K and V
- weights have different shapes than Q weights.
- """
- model_name = "gpt2"
- bridge = TransformerBridge.boot_transformers(model_name)
-
- # Get the original attention layer to modify
- original_attn = bridge.blocks[0].attn
- original_k_weight = original_attn.k.weight.clone()
- original_v_weight = original_attn.v.weight.clone()
-
- try:
- # Test case 1: Simulate MQA where K and V have shape [d_head, d_model]
- # instead of [d_model, d_model]
- d_head = bridge.cfg.d_head
- d_model = bridge.cfg.d_model
-
- # Create MQA-style K and V weights with shape [d_head, d_model]
- mqa_k_weight = torch.randn(
- d_head, d_model, dtype=original_k_weight.dtype, device=original_k_weight.device
- )
- mqa_v_weight = torch.randn(
- d_head, d_model, dtype=original_v_weight.dtype, device=original_v_weight.device
- )
-
- # Temporarily replace the weights
- original_attn.k.weight.data = mqa_k_weight
- original_attn.v.weight.data = mqa_v_weight
-
- # This should work without raising exceptions
- params_dict = bridge.get_params()
-
- # Verify the weights were reshaped correctly
- # For MQA: K and V should be expanded from [d_head, d_model] to [n_heads, d_model, d_head] (same as Q)
- k_param = params_dict["blocks.0.attn.W_K"]
- v_param = params_dict["blocks.0.attn.W_V"]
-
- expected_shape = (bridge.cfg.n_heads, bridge.cfg.d_model, bridge.cfg.d_head)
- assert (
- k_param.shape == expected_shape
- ), f"K weight should be reshaped to {expected_shape}, got {k_param.shape}"
- assert (
- v_param.shape == expected_shape
- ), f"V weight should be reshaped to {expected_shape}, got {v_param.shape}"
-
- # Verify that all heads contain the transposed MQA weight (due to transpose + expand operation)
- expected_k_per_head = mqa_k_weight.transpose(0, 1) # [d_head, d_model] -> [d_model, d_head]
- expected_v_per_head = mqa_v_weight.transpose(0, 1) # [d_head, d_model] -> [d_model, d_head]
-
- for head_idx in range(bridge.cfg.n_heads):
- assert torch.allclose(
- k_param[head_idx], expected_k_per_head
- ), f"K head {head_idx} should match transposed MQA weight"
- assert torch.allclose(
- v_param[head_idx], expected_v_per_head
- ), f"V head {head_idx} should match transposed MQA weight"
-
- finally:
- # Always restore original weights
- original_attn.k.weight.data = original_k_weight
- original_attn.v.weight.data = original_v_weight
-
-
def test_TransformerBridge_hooks_backward_hooks():
"""Test that TransformerBridge.hooks() correctly registers backward hooks.
diff --git a/tests/integration/model_bridge/test_bridge_layer_past_cache.py b/tests/integration/model_bridge/test_bridge_layer_past_cache.py
new file mode 100644
index 0000000000..31261dc7e9
--- /dev/null
+++ b/tests/integration/model_bridge/test_bridge_layer_past_cache.py
@@ -0,0 +1,72 @@
+"""Architectures that name the KV cache `layer_past` must still populate it.
+
+GPT-NeoX, GPT-J, Bloom, Falcon, MPT, CodeGen and GPT-BigCode take the cache as
+`layer_past`; everything modern takes `past_key_values`. Reading only the latter
+left the cache empty, so each decode step attended to itself alone and generation
+silently ignored the prompt — pythia-1.4b answered every prompt with
+" the first time.\\n\\n\\n...".
+"""
+
+import pytest
+import torch
+from transformers import DynamicCache
+
+from transformer_lens.model_bridge import TransformerBridge
+
+MODEL = "EleutherAI/pythia-70m" # GPTNeoX: takes `layer_past`
+PROMPT = "The theory of relativity explains that"
+
+
+@pytest.fixture(scope="module")
+def bridge():
+ return TransformerBridge.boot_transformers(MODEL, device="cpu")
+
+
+def test_forward_populates_a_layer_past_cache(bridge) -> None:
+ cache = DynamicCache()
+ tokens = bridge.to_tokens(PROMPT)
+ with torch.no_grad():
+ bridge(tokens, past_key_values=cache, use_cache=True)
+ assert (
+ cache.get_seq_length() == tokens.shape[1]
+ ), f"cache holds {cache.get_seq_length()} of {tokens.shape[1]} tokens"
+
+
+def test_cached_generation_matches_uncached(bridge) -> None:
+ """The cache is an optimization: it must not change what is generated."""
+ outputs = {}
+ for use_cache in (True, False):
+ torch.manual_seed(42)
+ outputs[use_cache] = bridge.generate(
+ PROMPT,
+ max_new_tokens=15,
+ do_sample=False,
+ verbose=False,
+ use_past_kv_cache=use_cache,
+ )
+ assert outputs[True] == outputs[False], f"cached={outputs[True]!r}\nuncached={outputs[False]!r}"
+
+
+def test_generation_depends_on_the_prompt(bridge) -> None:
+ """Engagement check: an empty cache made every prompt yield the same text,
+ which prompt-independent output is the loudest symptom of."""
+ torch.manual_seed(42)
+ a = bridge.generate(
+ "The theory of relativity explains that",
+ max_new_tokens=12,
+ do_sample=False,
+ verbose=False,
+ use_past_kv_cache=True,
+ )
+ torch.manual_seed(42)
+ b = bridge.generate(
+ "Modern computing relies heavily on",
+ max_new_tokens=12,
+ do_sample=False,
+ verbose=False,
+ use_past_kv_cache=True,
+ )
+ assert (
+ a[len("The theory of relativity explains that") :]
+ != b[len("Modern computing relies heavily on") :]
+ )
diff --git a/tests/integration/model_bridge/test_bridge_qk_ov_vs_hooked_gqa.py b/tests/integration/model_bridge/test_bridge_qk_ov_vs_hooked_gqa.py
new file mode 100644
index 0000000000..998003419f
--- /dev/null
+++ b/tests/integration/model_bridge/test_bridge_qk_ov_vs_hooked_gqa.py
@@ -0,0 +1,54 @@
+"""GQA weight-circuit parity between TransformerBridge and HookedTransformer.
+
+Acceptance for https://github.com/TransformerLensOrg/TransformerLens/issues/1553:
+bridge.QK/OV on a GQA model must match HookedTransformer.QK/OV (whose
+GroupedQueryAttention repeat_interleaves grouped K/V) within fp tolerance.
+Qwen2-0.5B (14 query heads, 2 kv heads) is a small ungated GQA model both
+systems support; it is not CI-cached, hence @pytest.mark.slow.
+"""
+
+import pytest
+import torch
+
+from transformer_lens import HookedTransformer
+from transformer_lens.model_bridge.bridge import TransformerBridge
+
+MODEL = "Qwen/Qwen2-0.5B"
+
+
+@pytest.mark.slow
+class TestGQAWeightCircuitParity:
+ @pytest.fixture(scope="class")
+ def bridge_and_hooked(self):
+ bridge = TransformerBridge.boot_transformers(MODEL, device="cpu")
+ hooked = HookedTransformer.from_pretrained_no_processing(MODEL, device="cpu")
+ return bridge, hooked
+
+ def test_model_is_gqa(self, bridge_and_hooked):
+ bridge, _ = bridge_and_hooked
+ assert bridge.cfg.n_key_value_heads is not None
+ assert bridge.cfg.n_key_value_heads < bridge.cfg.n_heads
+
+ def test_qk_factors_match_hooked(self, bridge_and_hooked):
+ bridge, hooked = bridge_and_hooked
+ torch.testing.assert_close(bridge.QK.A, hooked.QK.A)
+ torch.testing.assert_close(bridge.QK.B, hooked.QK.B)
+
+ def test_ov_factors_match_hooked(self, bridge_and_hooked):
+ bridge, hooked = bridge_and_hooked
+ torch.testing.assert_close(bridge.OV.A, hooked.OV.A)
+ torch.testing.assert_close(bridge.OV.B, hooked.OV.B)
+
+ def test_qk_product_matches_hooked(self, bridge_and_hooked):
+ bridge, hooked = bridge_and_hooked
+ torch.testing.assert_close(bridge.QK.A[0] @ bridge.QK.B[0], hooked.QK.A[0] @ hooked.QK.B[0])
+
+ def test_for_attn_layers_match_hooked(self, bridge_and_hooked):
+ bridge, hooked = bridge_and_hooked
+ indices, QK = bridge.QK_for_attn_layers()
+ assert indices == list(range(bridge.cfg.n_layers))
+ torch.testing.assert_close(QK.A, hooked.QK.A)
+ torch.testing.assert_close(QK.B, hooked.QK.B)
+ _, OV = bridge.OV_for_attn_layers()
+ torch.testing.assert_close(OV.A, hooked.OV.A)
+ torch.testing.assert_close(OV.B, hooked.OV.B)
diff --git a/tests/integration/model_bridge/test_cohere2_nope_no_warning.py b/tests/integration/model_bridge/test_cohere2_nope_no_warning.py
new file mode 100644
index 0000000000..71214936a7
--- /dev/null
+++ b/tests/integration/model_bridge/test_cohere2_nope_no_warning.py
@@ -0,0 +1,50 @@
+"""Cohere2 NoPE layers must not warn about missing position_embeddings.
+
+Full-attention (global) layers deliberately null position_embeddings — RoPE is
+sliding-window-only on Cohere2 — so the base bridge's missing-RoPE RuntimeWarning
+is spurious there.
+"""
+
+from __future__ import annotations
+
+import warnings
+
+import pytest
+import torch
+from transformers import AutoModelForCausalLM, Cohere2Config
+
+from transformer_lens.model_bridge.sources import build_bridge_from_module
+
+
+@pytest.fixture(scope="module")
+def cohere2_bridge():
+ cfg = Cohere2Config(
+ vocab_size=64,
+ hidden_size=32,
+ num_hidden_layers=2,
+ num_attention_heads=4,
+ num_key_value_heads=2,
+ intermediate_size=64,
+ max_position_embeddings=32,
+ sliding_window=4,
+ sliding_window_pattern=2,
+ layer_types=["sliding_attention", "full_attention"],
+ )
+ cfg._attn_implementation = "eager"
+ torch.manual_seed(0)
+ hf = AutoModelForCausalLM.from_config(cfg).eval()
+ return build_bridge_from_module(
+ hf, "Cohere2ForCausalLM", hf_config=cfg, tokenizer=None, device="cpu"
+ ).eval()
+
+
+def test_nope_layer_forward_emits_no_rope_warning(cohere2_bridge) -> None:
+ tokens = torch.tensor([[1, 2, 3, 4, 5]])
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter("always")
+ with torch.no_grad():
+ cohere2_bridge.run_with_cache(tokens, names_filter=["blocks.1.attn.hook_pattern"])
+ rope_warnings = [
+ w for w in caught if issubclass(w.category, RuntimeWarning) and "RoPE" in str(w.message)
+ ]
+ assert rope_warnings == [], [str(w.message) for w in rope_warnings]
diff --git a/tests/integration/model_bridge/test_encdec_string_generation.py b/tests/integration/model_bridge/test_encdec_string_generation.py
new file mode 100644
index 0000000000..6fa8df4bc5
--- /dev/null
+++ b/tests/integration/model_bridge/test_encdec_string_generation.py
@@ -0,0 +1,202 @@
+"""bridge.generate(str) on encoder-decoder models must tokenize with the
+tokenizer's native recipe. to_tokens' decoder-style BOS policy injected a
+stray and dropped the trailing , corrupting encoder input — m2m100
+degenerated into token loops; Marian/T5 degraded silently.
+
+A tiny-random M2M100 is used because its lang-code recipe genuinely differs
+from to_tokens output (Marian's happens to coincide, so it cannot
+discriminate); random weights are fine — greedy decoding is deterministic, so
+outputs match iff the encoder input matches.
+"""
+
+import pytest
+import torch
+
+pytest.importorskip("transformers")
+
+
+def test_m2m100_string_generation_matches_native_recipe():
+ from transformer_lens.model_bridge import TransformerBridge
+
+ try:
+ bridge = TransformerBridge.boot_transformers(
+ "hf-internal-testing/tiny-random-M2M100ForConditionalGeneration", device="cpu"
+ )
+ except (OSError, ConnectionError, TimeoutError) as exc:
+ pytest.skip(f"tiny-random-m2m100 unavailable offline: {exc}")
+
+ text = "Ik moet nu echt gaan slapen."
+ native_ids = bridge.tokenizer(text, return_tensors="pt")["input_ids"]
+ to_tokens_ids = bridge.to_tokens(text)
+ assert (
+ native_ids[0].tolist() != to_tokens_ids[0].tolist()
+ ), "precondition: recipes must differ or this test cannot discriminate"
+ # Assert on the tokens generate() actually consumed (random tiny weights
+ # emit input-independent output, so generated text cannot discriminate).
+ _, fed = bridge.generate(
+ text, max_new_tokens=4, temperature=0.0, return_type="tokens", return_input_tokens=True
+ )
+ assert isinstance(fed, torch.Tensor)
+ assert fed[0].tolist() == native_ids[0].tolist(), (fed[0].tolist(), native_ids[0].tolist())
+
+
+def test_m2m100_batched_list_generation_matches_native_recipe():
+ """The list-input branch had the same corruption (unpatched in the first
+ fix): batched generate on M2M100/MBart fed to_tokens-mangled encoder
+ input. Both rows must match the tokenizer's own padded batch encoding."""
+ from transformer_lens.model_bridge import TransformerBridge
+
+ try:
+ bridge = TransformerBridge.boot_transformers(
+ "hf-internal-testing/tiny-random-M2M100ForConditionalGeneration", device="cpu"
+ )
+ except (OSError, ConnectionError, TimeoutError) as exc:
+ pytest.skip(f"tiny-random-m2m100 unavailable offline: {exc}")
+
+ texts = ["Ik moet nu echt gaan slapen.", "Ik kan niet zo leven."]
+ native = bridge.tokenizer(texts, return_tensors="pt", padding=True)["input_ids"]
+ _, fed = bridge.generate(
+ texts, max_new_tokens=4, temperature=0.0, return_type="tokens", return_input_tokens=True
+ )
+ assert isinstance(fed, torch.Tensor)
+ assert fed.tolist() == native.tolist(), (fed.tolist(), native.tolist())
+
+
+def test_generation_config_forced_bos_applied_by_default():
+ """HF's generate() applies generation_config defaults; bart-large-cnn pins
+ forced_bos_token_id=0 there and its summaries degrade without it. The
+ bridge must honor the config value when the caller passes none."""
+ from transformer_lens.model_bridge import TransformerBridge
+
+ try:
+ bridge = TransformerBridge.boot_transformers(
+ "hf-internal-testing/tiny-random-M2M100ForConditionalGeneration", device="cpu"
+ )
+ except (OSError, ConnectionError, TimeoutError) as exc:
+ pytest.skip(f"tiny-random-m2m100 unavailable offline: {exc}")
+
+ forced = 7
+ bridge.original_model.generation_config.forced_bos_token_id = forced
+ out = bridge.generate(
+ "Ik moet nu echt gaan slapen.", max_new_tokens=4, temperature=0.0, return_type="tokens"
+ )
+ assert out[0, 1].item() == forced
+
+
+def test_generation_config_min_length_suppresses_early_eos():
+ """bart-large-cnn pins min_length=56 in its generation config; HF's
+ generate() suppresses EOS until then. Without it the bridge loop can EOS
+ on step one and emit an empty summary (observed live, scored 0)."""
+ from transformer_lens.model_bridge import TransformerBridge
+
+ try:
+ bridge = TransformerBridge.boot_transformers(
+ "hf-internal-testing/tiny-random-M2M100ForConditionalGeneration", device="cpu"
+ )
+ except (OSError, ConnectionError, TimeoutError) as exc:
+ pytest.skip(f"tiny-random-m2m100 unavailable offline: {exc}")
+
+ from unittest import mock
+
+ from transformer_lens import utilities as tl_utils
+
+ eos = bridge.original_model.config.eos_token_id
+ # Sample EOS whenever its logit is finite: the loop's -inf suppression is
+ # then the ONLY thing that can delay it, so this discriminates exactly
+ # that mechanism (tiny-random weights never prefer EOS on their own).
+ real_sample = tl_utils.sample_logits
+
+ def eos_greedy(logits, **kwargs):
+ out = real_sample(logits, **kwargs)
+ finite = torch.isfinite(logits[:, eos])
+ out[finite] = eos
+ return out
+
+ bridge.original_model.generation_config.min_length = 10
+ with mock.patch.object(tl_utils, "sample_logits", eos_greedy):
+ out = bridge.generate(
+ "Ik moet nu echt gaan slapen.",
+ max_new_tokens=16,
+ temperature=0.0,
+ return_type="tokens",
+ stop_at_eos=True,
+ )
+ decoder_part = out[0, 1:].tolist()
+ # Without suppression EOS lands at decoder position 1; with it, no EOS
+ # before the floor and EOS immediately after it lifts.
+ assert not any(t == eos for t in decoder_part[:8]), decoder_part
+ assert eos in decoder_part, decoder_part
+
+
+def test_generation_config_no_repeat_ngram_applied():
+ """bart-large-cnn pins no_repeat_ngram_size=3; HF applies it by default.
+ Without it greedy decoding falls into a BOS attractor (observed live:
+ empty summary, scored 0). Force an attractor token and assert the
+ processor breaks the loop."""
+ from unittest import mock
+
+ from transformer_lens import utilities as tl_utils
+ from transformer_lens.model_bridge import TransformerBridge
+
+ try:
+ bridge = TransformerBridge.boot_transformers(
+ "hf-internal-testing/tiny-random-M2M100ForConditionalGeneration", device="cpu"
+ )
+ except (OSError, ConnectionError, TimeoutError) as exc:
+ pytest.skip(f"tiny-random-m2m100 unavailable offline: {exc}")
+
+ attractor = 5
+ real_sample = tl_utils.sample_logits
+
+ def prefer_attractor(logits, **kwargs):
+ out = real_sample(logits, **kwargs)
+ allowed = torch.isfinite(logits[:, attractor])
+ out[allowed] = attractor
+ return out
+
+ bridge.original_model.generation_config.no_repeat_ngram_size = 2
+ with mock.patch.object(tl_utils, "sample_logits", prefer_attractor):
+ out = bridge.generate(
+ "Ik moet nu echt gaan slapen.",
+ max_new_tokens=8,
+ temperature=0.0,
+ return_type="tokens",
+ stop_at_eos=False,
+ )
+ seq = out[0].tolist()
+ runs = [seq[i] == seq[i + 1] == attractor for i in range(len(seq) - 1)]
+ # A (5,5) bigram may occur once, but 5,5,5 requires repeating it — banned.
+ assert not any(
+ seq[i] == seq[i + 1] == seq[i + 2] == attractor for i in range(len(seq) - 2)
+ ), seq
+
+
+def test_batched_unequal_rows_match_solo_generation():
+ """Id equality can't see mask handling: the batched enc-dec path fed
+ native ids but no attention mask, so the short row of an unequal batch
+ attended over pads. Greedy decoding of the short prompt must be identical
+ batched and solo."""
+ from transformer_lens.model_bridge import TransformerBridge
+
+ try:
+ bridge = TransformerBridge.boot_transformers(
+ "hf-internal-testing/tiny-random-M2M100ForConditionalGeneration", device="cpu"
+ )
+ except (OSError, ConnectionError, TimeoutError) as exc:
+ pytest.skip(f"tiny-random-m2m100 unavailable offline: {exc}")
+
+ short = "Ik slaap."
+ long = "Ik moet nu echt heel snel gaan slapen want het is al veel te laat geworden."
+ # Logits-level: argmax can survive unmasked pads on a tiny model, the
+ # step-0 distribution cannot.
+ solo = bridge.generate(
+ short, max_new_tokens=2, temperature=0.0, return_type="tokens", output_logits=True
+ )
+ batched = bridge.generate(
+ [short, long], max_new_tokens=2, temperature=0.0, return_type="tokens", output_logits=True
+ )
+ solo_step0 = solo.logits[0][0]
+ batched_step0_row0 = batched.logits[0][0]
+ assert torch.allclose(solo_step0, batched_step0_row0, atol=1e-4), float(
+ (solo_step0 - batched_step0_row0).abs().max()
+ )
diff --git a/tests/integration/model_bridge/test_generate_attention_mask.py b/tests/integration/model_bridge/test_generate_attention_mask.py
new file mode 100644
index 0000000000..7fbe82b71c
--- /dev/null
+++ b/tests/integration/model_bridge/test_generate_attention_mask.py
@@ -0,0 +1,274 @@
+"""Generation from an already-padded prompt.
+
+``generate()`` had no way to be told which prompt tokens are padding, so a
+pre-padded tensor generated as though its pads were real context: every real
+token's position was shifted and the continuation diverged from the same prompt
+unpadded. See #1612.
+
+Two routes now work. ``attention_mask`` states the padding explicitly, and
+``padding_side`` — which the bridge accepted but never applied to token input —
+reads it off the pad token. Only the explicit mask can express an interior gap
+or a pad id that also occurs as a real token.
+"""
+
+from __future__ import annotations
+
+import copy
+
+import pytest
+import torch
+
+GREEDY = dict(max_new_tokens=5, do_sample=False, verbose=False)
+
+
+@pytest.fixture(scope="module")
+def prompt(distilgpt2_bridge) -> torch.Tensor:
+ return distilgpt2_bridge.to_tokens("The capital of France is")
+
+
+@pytest.fixture(scope="module")
+def unpadded_continuation(distilgpt2_bridge, prompt) -> list[int]:
+ return distilgpt2_bridge.generate(prompt, **GREEDY)[0, prompt.shape[1] :].tolist()
+
+
+def _left_pad(bridge, tokens: torch.Tensor, n_pad: int) -> tuple[torch.Tensor, torch.Tensor]:
+ pad_id = bridge.tokenizer.pad_token_id
+ if pad_id is None:
+ pad_id = bridge.tokenizer.eos_token_id
+ padded = torch.cat([torch.full((1, n_pad), pad_id, dtype=tokens.dtype), tokens], dim=1)
+ mask = torch.cat(
+ [torch.zeros(1, n_pad, dtype=torch.long), torch.ones(1, tokens.shape[1], dtype=torch.long)],
+ dim=1,
+ )
+ return padded, mask
+
+
+@pytest.mark.parametrize("use_past_kv_cache", [True, False])
+@pytest.mark.parametrize("n_pad", [1, 3, 7])
+def test_attention_mask_recovers_the_unpadded_continuation(
+ distilgpt2_bridge, prompt, unpadded_continuation, n_pad, use_past_kv_cache
+) -> None:
+ """The whole point: padding a prompt must not change what it generates."""
+ padded, mask = _left_pad(distilgpt2_bridge, prompt, n_pad)
+
+ out = distilgpt2_bridge.generate(
+ padded, attention_mask=mask, use_past_kv_cache=use_past_kv_cache, **GREEDY
+ )
+
+ assert out[0, n_pad + prompt.shape[1] :].tolist() == unpadded_continuation
+
+
+def test_without_a_mask_the_pads_are_treated_as_context(
+ distilgpt2_bridge, prompt, unpadded_continuation
+) -> None:
+ """The unfixed behaviour, pinned so a regression is visible rather than silent.
+
+ padding_side defaults to "right", so leading pads are not recognised and the
+ continuation drifts. This is the case #1612 reported.
+ """
+ padded, _ = _left_pad(distilgpt2_bridge, prompt, 4)
+
+ out = distilgpt2_bridge.generate(padded, **GREEDY)
+
+ assert out[0, 4 + prompt.shape[1] :].tolist() != unpadded_continuation
+
+
+def test_padding_side_left_is_applied_to_token_input(
+ distilgpt2_bridge, prompt, unpadded_continuation
+) -> None:
+ """generate() has always documented a padding_side argument, but applied it
+ only when tokenizing string or list input. For a token tensor it was inert."""
+ padded, _ = _left_pad(distilgpt2_bridge, prompt, 4)
+
+ out = distilgpt2_bridge.generate(padded, padding_side="left", **GREEDY)
+
+ assert out[0, 4 + prompt.shape[1] :].tolist() == unpadded_continuation
+
+
+def test_padding_side_is_restored_afterwards(distilgpt2_bridge, prompt) -> None:
+ """The tokenizer is shared across a session, so the override must not leak."""
+ before = distilgpt2_bridge.tokenizer.padding_side
+ padded, _ = _left_pad(distilgpt2_bridge, prompt, 3)
+
+ distilgpt2_bridge.generate(padded, padding_side="left", **GREEDY)
+
+ assert distilgpt2_bridge.tokenizer.padding_side == before
+
+
+def test_explicit_mask_wins_over_the_padding_side_heuristic(
+ distilgpt2_bridge, prompt, unpadded_continuation
+) -> None:
+ """A caller who states the padding must not be second-guessed by the pad-token
+ scan, which here would mask nothing because padding_side is "right"."""
+ padded, mask = _left_pad(distilgpt2_bridge, prompt, 4)
+
+ out = distilgpt2_bridge.generate(padded, attention_mask=mask, padding_side="right", **GREEDY)
+
+ assert out[0, 4 + prompt.shape[1] :].tolist() == unpadded_continuation
+
+
+def test_interior_gap_needs_the_explicit_mask(distilgpt2_bridge, prompt) -> None:
+ """padding_side can only describe padding at one edge. A masked-out token in
+ the middle shifts later positions just the same, and only a mask says so."""
+ pad_id = distilgpt2_bridge.tokenizer.eos_token_id
+ gapped = prompt.clone()
+ gapped[0, 2] = pad_id
+ mask = torch.ones_like(prompt)
+ mask[0, 2] = 0
+ compact = torch.cat([prompt[:, :2], prompt[:, 3:]], dim=1)
+
+ reference = distilgpt2_bridge.generate(compact, **GREEDY)[0, compact.shape[1] :].tolist()
+ via_mask = distilgpt2_bridge.generate(gapped, attention_mask=mask, **GREEDY)[
+ 0, prompt.shape[1] :
+ ].tolist()
+
+ assert via_mask == reference
+
+
+def test_rows_padded_to_different_lengths(distilgpt2_bridge) -> None:
+ """Each row must generate what it would alone, whatever its own pad count."""
+ long_prompt = distilgpt2_bridge.to_tokens("The capital of France is")
+ short_prompt = distilgpt2_bridge.to_tokens("Hello")
+ width = max(long_prompt.shape[1], short_prompt.shape[1])
+
+ rows, masks = [], []
+ for tokens in (long_prompt, short_prompt):
+ padded, mask = _left_pad(distilgpt2_bridge, tokens, width - tokens.shape[1])
+ rows.append(padded)
+ masks.append(mask)
+
+ out = distilgpt2_bridge.generate(
+ torch.cat(rows, dim=0), attention_mask=torch.cat(masks, dim=0), **GREEDY
+ )
+
+ for index, tokens in enumerate((long_prompt, short_prompt)):
+ solo = distilgpt2_bridge.generate(tokens, **GREEDY)[0, tokens.shape[1] :].tolist()
+ assert out[index, width:].tolist() == solo
+
+
+def test_the_mask_reaches_every_step_not_just_the_first(distilgpt2_bridge, prompt) -> None:
+ """Before #1612 an attention_mask kwarg was absorbed into **multimodal_kwargs,
+ which are merged into the forward kwargs on step 0 only. That made the first
+ token come out right and every later one wrong, which is worse to debug than a
+ uniform failure. Each step must see a mask covering the prompt plus the tokens
+ generated so far.
+ """
+ n_pad = 3
+ padded, mask = _left_pad(distilgpt2_bridge, prompt, n_pad)
+ prompt_width = padded.shape[1]
+ seen: list[torch.Tensor | None] = []
+
+ original = distilgpt2_bridge.original_model.forward
+
+ def _spy(*args, **kwargs):
+ seen.append(kwargs.get("attention_mask"))
+ return original(*args, **kwargs)
+
+ distilgpt2_bridge.original_model.forward = _spy
+ try:
+ distilgpt2_bridge.generate(padded, attention_mask=mask, **GREEDY)
+ finally:
+ distilgpt2_bridge.original_model.forward = original
+
+ assert len(seen) == GREEDY["max_new_tokens"]
+ for step, observed in enumerate(seen):
+ assert observed is not None, f"step {step} received no attention_mask"
+ assert observed.shape[1] == prompt_width + step
+ # The prompt's padding stays masked however far generation has run.
+ assert observed[0, :n_pad].sum() == 0
+ assert observed[0, n_pad:].all()
+
+
+def test_unpadded_generation_is_unchanged(distilgpt2_bridge, prompt, unpadded_continuation) -> None:
+ """An all-ones mask is what the model assumes anyway, so supplying one must
+ be a no-op rather than a second code path."""
+ out = distilgpt2_bridge.generate(prompt, attention_mask=torch.ones_like(prompt), **GREEDY)
+
+ assert out[0, prompt.shape[1] :].tolist() == unpadded_continuation
+
+
+def test_string_and_list_input_still_work(distilgpt2_bridge) -> None:
+ """The list path builds its own mask; neither route may regress."""
+ if distilgpt2_bridge.tokenizer.pad_token_id is None:
+ distilgpt2_bridge.tokenizer.pad_token = distilgpt2_bridge.tokenizer.eos_token
+
+ solo = distilgpt2_bridge.generate("The capital of France is", **GREEDY)
+ batched = distilgpt2_bridge.generate(["The capital of France is", "Hi"], **GREEDY)
+
+ assert isinstance(solo, str) and solo.startswith("The capital of France is")
+ assert batched[0] == solo
+
+
+def test_mask_shape_must_match_the_prompt(distilgpt2_bridge, prompt) -> None:
+ """generate() extends the mask itself, so a pre-extended one is a mistake
+ worth naming rather than broadcasting into something unintended."""
+ with pytest.raises(ValueError, match="does not match the prompt shape"):
+ distilgpt2_bridge.generate(
+ prompt, attention_mask=torch.ones(1, prompt.shape[1] + 5, dtype=torch.long), **GREEDY
+ )
+
+
+def test_padding_side_without_a_tokenizer_is_an_error(distilgpt2_bridge, prompt) -> None:
+ """A bridge booted without a tokenizer has nothing to read the padding from, so
+ padding_side would be inert — leaving exactly the bug this module is about, but
+ silently. attention_mask still works there and the message must say so."""
+ bridge = copy.copy(distilgpt2_bridge)
+ bridge.tokenizer = None
+ assert bridge.tokenizer is None and distilgpt2_bridge.tokenizer is not None
+ padded, _ = _left_pad(distilgpt2_bridge, prompt, 3)
+
+ with pytest.raises(ValueError, match="this bridge has none"):
+ bridge.generate(padded, padding_side="left", **GREEDY)
+
+
+def test_padding_side_without_a_pad_token_is_an_error(distilgpt2_bridge, prompt) -> None:
+ """Same reasoning for a tokenizer that has no pad id to scan for."""
+ padded, _ = _left_pad(distilgpt2_bridge, prompt, 3)
+ tokenizer = distilgpt2_bridge.tokenizer
+ original_pad = tokenizer.pad_token_id
+ tokenizer.pad_token_id = None
+ try:
+ with pytest.raises(ValueError, match="pad_token_id"):
+ distilgpt2_bridge.generate(padded, padding_side="left", **GREEDY)
+ finally:
+ tokenizer.pad_token_id = original_pad
+
+
+def test_a_tokenizerless_bridge_still_accepts_an_explicit_mask(
+ distilgpt2_bridge, prompt, unpadded_continuation
+) -> None:
+ """The alternative the error points at has to actually work."""
+ bridge = copy.copy(distilgpt2_bridge)
+ bridge.tokenizer = None
+ n_pad = 3
+ padded, mask = _left_pad(distilgpt2_bridge, prompt, n_pad)
+
+ out = bridge.generate(padded, attention_mask=mask, **GREEDY)
+
+ assert out[0, n_pad + prompt.shape[1] :].tolist() == unpadded_continuation
+
+
+def test_inputs_embeds_forwards_the_mask_untouched(distilgpt2_bridge, prompt) -> None:
+ """There are no token positions to correct on the embeds path, but processors
+ emit an attention_mask alongside their other outputs and callers pass the lot
+ straight through. Before this parameter existed that mask reached the model via
+ **multimodal_kwargs, so it must still arrive rather than raise.
+ """
+ embeds = distilgpt2_bridge.original_model.get_input_embeddings()(prompt)
+ mask = torch.ones_like(prompt)
+ seen: list[torch.Tensor | None] = []
+
+ original = distilgpt2_bridge.original_model.forward
+
+ def _spy(*args, **kwargs):
+ seen.append(kwargs.get("attention_mask"))
+ return original(*args, **kwargs)
+
+ distilgpt2_bridge.original_model.forward = _spy
+ try:
+ distilgpt2_bridge.generate(embeds, attention_mask=mask, **GREEDY)
+ finally:
+ distilgpt2_bridge.original_model.forward = original
+
+ assert seen and seen[0] is not None
+ torch.testing.assert_close(seen[0], mask)
diff --git a/tests/integration/model_bridge/test_glm4v_adapter.py b/tests/integration/model_bridge/test_glm4v_adapter.py
index 176ebc9804..86b393da3f 100644
--- a/tests/integration/model_bridge/test_glm4v_adapter.py
+++ b/tests/integration/model_bridge/test_glm4v_adapter.py
@@ -114,6 +114,29 @@ def grab(tensor, hook):
class TestGlm4vGeneration:
+ def test_generate_after_multimodal_forward(self, glm4v_bridge, snapshot_path):
+ """A multimodal forward caches mRoPE rope_deltas on the HF module; text-only
+ generate must not add that stale delta to its cached-step positions."""
+ from PIL import Image
+ from transformers import AutoProcessor
+
+ proc = AutoProcessor.from_pretrained(snapshot_path)
+ img = Image.new("RGB", (112, 112), "red")
+ messages = [
+ {
+ "role": "user",
+ "content": [{"type": "image"}, {"type": "text", "text": "Describe"}],
+ }
+ ]
+ text = proc.apply_chat_template(messages, add_generation_prompt=True)
+ inputs = dict(proc(text=[text], images=[img], return_tensors="pt"))
+ bridge_inputs = {k: v for k, v in inputs.items() if k != "input_ids"}
+ with torch.no_grad():
+ glm4v_bridge(inputs["input_ids"], **bridge_inputs)
+
+ text_out = glm4v_bridge.generate("Hello", max_new_tokens=5, do_sample=False, verbose=False)
+ assert isinstance(text_out, str)
+
def test_generate(self, glm4v_bridge):
text = glm4v_bridge.generate("Hello", max_new_tokens=5, do_sample=False, verbose=False)
assert isinstance(text, str)
diff --git a/tests/integration/model_bridge/test_left_padding_positions.py b/tests/integration/model_bridge/test_left_padding_positions.py
new file mode 100644
index 0000000000..46d9ab8ff5
--- /dev/null
+++ b/tests/integration/model_bridge/test_left_padding_positions.py
@@ -0,0 +1,367 @@
+"""Regression tests for left-padding position handling in TransformerBridge.
+
+A causal LM's logits at a sequence's real token positions must not depend on how
+that sequence is padded, provided the caller supplies the matching attention_mask.
+Left padding shifts every real token's absolute position, so position_ids have to
+be derived from the mask; without that the bridge silently returns wrong logits
+and a wrong loss. See #1609.
+
+Right padding is included as a control: causality already protects it, so it was
+never affected and must stay that way.
+"""
+
+from __future__ import annotations
+
+import pytest
+import torch
+
+from transformer_lens import utilities as utils
+
+PAD_ID = 0
+
+
+def _pad(tokens: torch.Tensor, n_pad: int, side: str) -> tuple[torch.Tensor, torch.Tensor]:
+ """Pad `tokens` on `side`, returning (padded_tokens, attention_mask)."""
+ pads = torch.full((tokens.shape[0], n_pad), PAD_ID, dtype=tokens.dtype)
+ ones = torch.ones(tokens.shape, dtype=torch.long)
+ zeros = torch.zeros((tokens.shape[0], n_pad), dtype=torch.long)
+ if side == "left":
+ return torch.cat([pads, tokens], dim=1), torch.cat([zeros, ones], dim=1)
+ return torch.cat([tokens, pads], dim=1), torch.cat([ones, zeros], dim=1)
+
+
+def _mixed_batch(
+ tokens: torch.Tensor, n_pad: int
+) -> tuple[torch.Tensor, torch.Tensor, tuple, tuple]:
+ """A batch of one right-padded, one left-padded and one unpadded row."""
+ width = tokens.shape[1] + n_pad
+ right, m_right = _pad(tokens, n_pad, "right")
+ left, m_left = _pad(tokens, n_pad, "left")
+ plain = torch.arange(20, 20 + width, dtype=tokens.dtype).unsqueeze(0)
+ m_plain = torch.ones(1, width, dtype=torch.long)
+ batch = torch.cat([right, left, plain], dim=0)
+ mask = torch.cat([m_right, m_left, m_plain], dim=0)
+ return batch, mask, (right, m_right), (plain, m_plain)
+
+
+def _spy_on_position_ids(bridge, tokens_in: torch.Tensor, mask: torch.Tensor):
+ """Run a forward, returning the position_ids the wrapped model actually saw."""
+ seen: dict[str, object] = {}
+ original = bridge.original_model.forward
+
+ def _spy(*args, **kwargs):
+ seen["position_ids"] = kwargs.get("position_ids")
+ return original(*args, **kwargs)
+
+ bridge.original_model.forward = _spy
+ try:
+ with torch.no_grad():
+ bridge(tokens_in, attention_mask=mask, return_type="logits")
+ finally:
+ bridge.original_model.forward = original
+ return seen["position_ids"]
+
+
+@pytest.fixture(scope="module")
+def tokens(distilgpt2_bridge) -> torch.Tensor:
+ return distilgpt2_bridge.to_tokens("The capital of France is")
+
+
+@pytest.mark.parametrize("side", ["left", "right"])
+@pytest.mark.parametrize("n_pad", [1, 3, 5])
+def test_logits_are_invariant_to_padding(distilgpt2_bridge, tokens, side, n_pad) -> None:
+ """Padding must not change the logits at a sequence's real positions."""
+ padded, mask = _pad(tokens, n_pad, side)
+ real = slice(n_pad, None) if side == "left" else slice(None, tokens.shape[1])
+
+ with torch.no_grad():
+ baseline = distilgpt2_bridge(tokens, return_type="logits")
+ actual = distilgpt2_bridge(padded, attention_mask=mask, return_type="logits")[:, real]
+
+ assert torch.isfinite(actual).all()
+ torch.testing.assert_close(actual, baseline, rtol=1e-3, atol=1e-3)
+
+
+@pytest.mark.parametrize("side", ["left", "right"])
+def test_compat_mode_logits_are_invariant_to_padding(distilgpt2_bridge_compat, side: str) -> None:
+ """enable_compatibility_mode() promises HookedTransformer-equivalent numerics,
+ which this property is part of."""
+ tokens = distilgpt2_bridge_compat.to_tokens("The capital of France is")
+ n_pad = 3
+ padded, mask = _pad(tokens, n_pad, side)
+ real = slice(n_pad, None) if side == "left" else slice(None, tokens.shape[1])
+
+ with torch.no_grad():
+ baseline = distilgpt2_bridge_compat(tokens, return_type="logits")
+ actual = distilgpt2_bridge_compat(padded, attention_mask=mask, return_type="logits")[
+ :, real
+ ]
+
+ torch.testing.assert_close(actual, baseline, rtol=1e-3, atol=1e-3)
+
+
+def test_derived_position_ids_match_hooked_transformer(distilgpt2_bridge, tokens) -> None:
+ """The derived positions must be the ones HookedTransformer would use, i.e. the
+ shared get_offset_position_ids helper rather than a parallel derivation."""
+ n_pad = 3
+ padded, mask = _pad(tokens, n_pad, "left")
+ expected = utils.get_offset_position_ids(0, mask)
+
+ with torch.no_grad():
+ derived = distilgpt2_bridge(padded, attention_mask=mask, return_type="logits")
+ supplied = distilgpt2_bridge(
+ padded, attention_mask=mask, position_ids=expected, return_type="logits"
+ )
+
+ torch.testing.assert_close(derived, supplied, rtol=1e-5, atol=1e-5)
+
+
+def test_explicit_position_ids_take_precedence(distilgpt2_bridge, tokens) -> None:
+ """A caller-supplied position_ids must not be overwritten by the derivation."""
+ n_pad = 3
+ padded, mask = _pad(tokens, n_pad, "left")
+ derived_positions = utils.get_offset_position_ids(0, mask)
+ shifted = derived_positions + 1 # deliberately different, but still in range
+
+ with torch.no_grad():
+ default = distilgpt2_bridge(padded, attention_mask=mask, return_type="logits")
+ overridden = distilgpt2_bridge(
+ padded, attention_mask=mask, position_ids=shifted, return_type="logits"
+ )
+
+ assert not torch.allclose(default, overridden, rtol=1e-3, atol=1e-3)
+
+
+@pytest.mark.parametrize("gap", [slice(3, 5), slice(1, 2)])
+def test_interior_mask_gap_uses_derived_positions(distilgpt2_bridge, tokens, gap) -> None:
+ """A mask gap that is not leading padding still shifts later positions, so the
+ derivation must fire for any mask, not only ones starting with a pad."""
+ mask = torch.ones(tokens.shape, dtype=torch.long)
+ mask[0, gap] = 0
+ gapped = tokens.clone()
+ gapped[0, gap] = PAD_ID
+ expected = utils.get_offset_position_ids(0, mask)
+
+ with torch.no_grad():
+ derived = distilgpt2_bridge(gapped, attention_mask=mask, return_type="logits")
+ supplied = distilgpt2_bridge(
+ gapped, attention_mask=mask, position_ids=expected, return_type="logits"
+ )
+
+ torch.testing.assert_close(derived, supplied, rtol=1e-5, atol=1e-5)
+
+
+@pytest.mark.parametrize("mask_kind", ["all_ones", "right_padded"])
+def test_no_position_ids_injected_when_unnecessary(distilgpt2_bridge, tokens, mask_kind) -> None:
+ """Masks whose attended tokens already sit at their default positions must not
+ get position_ids injected: it is a no-op at best, and models whose forward does
+ not accept position_ids would raise.
+ """
+ if mask_kind == "all_ones":
+ passed, mask = tokens, torch.ones(tokens.shape, dtype=torch.long)
+ else:
+ passed, mask = _pad(tokens, 3, "right")
+
+ assert _spy_on_position_ids(distilgpt2_bridge, passed, mask) is None
+
+
+def test_unshifted_rows_keep_default_positions(distilgpt2_bridge, tokens) -> None:
+ """The decision is per row, not per batch: only rows whose mask moves an
+ attended token get derived positions, so the others stay on plain arange."""
+ n_pad = 3
+ batch, mask, _, _ = _mixed_batch(tokens, n_pad)
+ seen = _spy_on_position_ids(distilgpt2_bridge, batch, mask)
+
+ assert seen is not None
+ arange = torch.arange(batch.shape[1])
+ torch.testing.assert_close(seen[0], arange) # right-padded
+ torch.testing.assert_close(seen[2], arange) # unpadded
+ torch.testing.assert_close(seen[1], utils.get_offset_position_ids(0, mask)[1]) # left-padded
+
+
+def test_one_left_padded_row_does_not_perturb_its_neighbours(distilgpt2_bridge, tokens) -> None:
+ """Derived positions for one row must not change its unshifted neighbours."""
+ n_pad = 3
+ batch, mask, (right, m_right), (plain, m_plain) = _mixed_batch(tokens, n_pad)
+ control_batch = torch.cat([right, right, plain], dim=0)
+ control_mask = torch.cat([m_right, m_right, m_plain], dim=0)
+
+ with torch.no_grad():
+ mixed = distilgpt2_bridge(batch, attention_mask=mask, return_type="logits")
+ control = distilgpt2_bridge(
+ control_batch, attention_mask=control_mask, return_type="logits"
+ )
+ unpadded = distilgpt2_bridge(tokens, return_type="logits")
+
+ # Matching batch shapes isolate derived-position handling from BLAS kernel
+ # changes caused by comparing batched and single-row matrix multiplications.
+ torch.testing.assert_close(mixed[0:1], control[0:1], rtol=0, atol=1e-6)
+ torch.testing.assert_close(mixed[2:3], control[2:3], rtol=0, atol=1e-6)
+ # ...while the row that did need correcting still gets it.
+ torch.testing.assert_close(mixed[1:2, n_pad:], unpadded, rtol=1e-3, atol=1e-3)
+
+
+def test_float_attention_mask_is_accepted(distilgpt2_bridge, tokens) -> None:
+ """Derived positions index an embedding table, so a float 0/1 mask must not
+ produce float position_ids."""
+ n_pad = 3
+ padded, mask = _pad(tokens, n_pad, "left")
+
+ with torch.no_grad():
+ baseline = distilgpt2_bridge(tokens, return_type="logits")
+ actual = distilgpt2_bridge(padded, attention_mask=mask.float(), return_type="logits")
+
+ torch.testing.assert_close(actual[:, n_pad:], baseline, rtol=1e-3, atol=1e-3)
+
+
+@pytest.mark.parametrize(
+ "mask_row",
+ [
+ pytest.param([0, 0, 0, 0, 0, 0], id="all_masked"),
+ pytest.param([0, 0, 0, 0, 0, 1], id="single_real_token"),
+ pytest.param([1, 0, 1, 0, 1, 1], id="two_interior_gaps"),
+ pytest.param([0, 1, 1, 1, 1, 0], id="padded_both_ends"),
+ ],
+)
+def test_degenerate_masks_do_not_crash(distilgpt2_bridge, tokens, mask_row) -> None:
+ """Shapes the happy path never reaches. An all-masked row in particular must
+ not inject anything: every position is a pad, so nothing is displaced."""
+ mask = torch.tensor([mask_row[: tokens.shape[1]]], dtype=torch.long)
+ positions = _spy_on_position_ids(distilgpt2_bridge, tokens, mask)
+
+ if mask.sum() == 0:
+ assert positions is None
+ else:
+ expected = utils.get_offset_position_ids(0, mask)
+ torch.testing.assert_close(positions, expected)
+
+
+def test_four_dimensional_mask_is_left_alone(distilgpt2_bridge, tokens) -> None:
+ """A 4-D mask is an additive attention bias, not a 0/1 padding mask, so the
+ cumsum derivation is meaningless on it."""
+ seq = tokens.shape[1]
+ mask = torch.ones(1, 1, seq, seq)
+ assert _spy_on_position_ids(distilgpt2_bridge, tokens, mask) is None
+
+
+def test_inputs_embeds_are_left_alone(distilgpt2_bridge, tokens) -> None:
+ """Float input is pre-computed embeddings; there are no token positions to
+ derive and the batch/seq layout is not guaranteed to match the mask."""
+ embeds = distilgpt2_bridge.original_model.get_input_embeddings()(tokens)
+ _, mask = _pad(tokens[:, :-2], 2, "left")
+ assert _spy_on_position_ids(distilgpt2_bridge, embeds, mask) is None
+
+
+def test_gradients_flow_through_a_left_padded_forward(distilgpt2_bridge, tokens) -> None:
+ """The derivation must not detach the graph or poison the loss with pads."""
+ n_pad = 3
+ padded, mask = _pad(tokens, n_pad, "left")
+
+ loss = distilgpt2_bridge(padded, attention_mask=mask, return_type="loss")
+ loss.backward()
+ grad = distilgpt2_bridge.original_model.get_input_embeddings().weight.grad
+ try:
+ assert torch.isfinite(loss)
+ assert grad is not None and torch.isfinite(grad).all() and (grad != 0).any()
+ finally:
+ distilgpt2_bridge.zero_grad(set_to_none=True)
+
+
+def test_run_with_cache_matches_forward_under_left_padding(distilgpt2_bridge, tokens) -> None:
+ """run_with_cache routes through a different kwarg-filtering path, so the
+ injection has to survive it identically."""
+ n_pad = 3
+ padded, mask = _pad(tokens, n_pad, "left")
+
+ with torch.no_grad():
+ direct = distilgpt2_bridge(padded, attention_mask=mask, return_type="logits")
+ cached, activations = distilgpt2_bridge.run_with_cache(padded, attention_mask=mask)
+
+ torch.testing.assert_close(direct, cached, rtol=0, atol=1e-6)
+ assert len(activations) > 0
+
+
+@pytest.fixture(scope="module")
+def opt_bridge():
+ """OPT is the one supported architecture whose positional embedding consumes
+ the attention mask, so it must be left to derive positions for itself."""
+ from transformer_lens.model_bridge import TransformerBridge
+
+ bridge = TransformerBridge.boot_transformers(
+ "hf-internal-testing/tiny-random-OPTForCausalLM", device="cpu", dtype=torch.float32
+ )
+ bridge.eval()
+ return bridge
+
+
+def test_self_deriving_model_is_left_alone(opt_bridge) -> None:
+ """OPTLearnedPositionalEmbedding derives from the mask already, and uses its
+ own convention (-1) for padded slots. Overriding it buys no correctness and
+ silently changes the padded slots, so the bridge must stay out of the way.
+ """
+ ids = torch.arange(20, 26).unsqueeze(0)
+ n_pad = 3
+ padded, mask = _pad(ids, n_pad, "left")
+
+ assert opt_bridge._accepts_derived_position_ids() is False
+ assert _spy_on_position_ids(opt_bridge, padded, mask) is None
+
+ with torch.no_grad():
+ bridge_out = opt_bridge(padded, attention_mask=mask, return_type="logits")
+ hf_out = opt_bridge.original_model(input_ids=padded, attention_mask=mask).logits
+ unpadded = opt_bridge(ids, attention_mask=torch.ones_like(ids), return_type="logits")
+
+ # Deferring to OPT keeps the bridge exactly on HF, and OPT's own derivation
+ # already delivers the padding-invariance this module is about.
+ torch.testing.assert_close(bridge_out, hf_out, rtol=0, atol=1e-6)
+ torch.testing.assert_close(bridge_out[:, n_pad:], unpadded, rtol=1e-4, atol=1e-4)
+
+
+def test_cached_step_with_left_padding(distilgpt2_bridge, tokens) -> None:
+ """With a KV cache the mask spans past+new while input_ids is only the new
+ token, so the derivation must be offset back to the tokens being passed.
+
+ Prefill goes through the bridge (return_type="logits_and_cache") so the
+ cached keys and values are built under the same position convention the step
+ uses. Stepping off a cache prefilled by raw HF mixes two conventions and is
+ not equivalent to anything.
+ """
+ n_pad = 3
+ padded, mask = _pad(tokens, n_pad, "left")
+ new_token = torch.tensor([[318]])
+ extended = torch.cat([mask, torch.ones(1, 1, dtype=torch.long)], dim=1)
+
+ with torch.no_grad():
+ _, cache = distilgpt2_bridge(padded, attention_mask=mask, return_type="logits_and_cache")
+ step = distilgpt2_bridge(
+ new_token, attention_mask=extended, past_key_values=cache, return_type="logits"
+ )
+ # Ground truth: the same prompt and token with no padding at all.
+ unpadded = distilgpt2_bridge(torch.cat([tokens, new_token], dim=1), return_type="logits")
+
+ assert step.shape[:2] == (1, 1)
+ torch.testing.assert_close(step, unpadded[:, -1:], rtol=1e-3, atol=1e-3)
+
+
+def test_cached_decoding_with_left_padding_matches_full_recompute(
+ distilgpt2_bridge, tokens
+) -> None:
+ """Several cached steps in a row: each must land on what recomputing the
+ whole left-padded sequence would give, or the offset drifts with the cache."""
+ n_pad = 3
+ padded, mask = _pad(tokens, n_pad, "left")
+
+ with torch.no_grad():
+ _, cache = distilgpt2_bridge(padded, attention_mask=mask, return_type="logits_and_cache")
+ sequence, grown = padded, mask
+ for _ in range(4):
+ logits = distilgpt2_bridge(sequence, attention_mask=grown, return_type="logits")
+ next_token = logits[:, -1].argmax(dim=-1, keepdim=True)
+ sequence = torch.cat([sequence, next_token], dim=1)
+ grown = torch.cat([grown, torch.ones(1, 1, dtype=torch.long)], dim=1)
+
+ stepped = distilgpt2_bridge(
+ next_token, attention_mask=grown, past_key_values=cache, return_type="logits"
+ )
+ full = distilgpt2_bridge(sequence, attention_mask=grown, return_type="logits")
+ torch.testing.assert_close(stepped, full[:, -1:], rtol=1e-3, atol=1e-3)
diff --git a/tests/integration/model_bridge/test_llada_adapter.py b/tests/integration/model_bridge/test_llada_adapter.py
index 4368615b8b..2078560d96 100644
--- a/tests/integration/model_bridge/test_llada_adapter.py
+++ b/tests/integration/model_bridge/test_llada_adapter.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import copy
+import functools
import gc
import math
import weakref
@@ -649,6 +650,68 @@ def test_padding_mask_blocks_keys_without_becoming_causal(models: TinyModels) ->
)
+def test_left_padding_does_not_inject_unsupported_position_ids(models: TinyModels) -> None:
+ """The bridge derives position_ids from attention_mask for left-padded input
+ (#1609), but this forward takes neither position_ids nor **kwargs — as the
+ released LLaDA remote code does not — so the kwarg would raise TypeError
+ where the model used to return logits.
+ """
+ tokens = torch.tensor([[63, 63, 5, 7, 9]])
+ attention_mask = torch.tensor([[0, 0, 1, 1, 1]])
+ with torch.inference_mode():
+ reference_logits = models.reference(tokens, attention_mask=attention_mask).logits
+ bridge_logits = models.bridge(tokens, attention_mask=attention_mask)
+ torch.testing.assert_close(bridge_logits, reference_logits, rtol=1e-5, atol=1e-6)
+
+
+def test_batched_list_input_does_not_inject_unsupported_position_ids() -> None:
+ """Batched list input builds its own attention_mask and position_ids so pad
+ tokens don't contaminate the forward (#1626). The mask is safe for any model;
+ the position_ids are not, and this forward takes neither them nor **kwargs.
+
+ A local bridge rather than the module fixture: this needs a tokenizer, and
+ attaching one to the shared instance would leak into the other tests. The
+ tokenizer is given a BOS so the path under test is reached independently of
+ BOS handling elsewhere.
+ """
+ local = _build_models()
+ tokenizer = _offline_tokenizer()
+ tokenizer.bos_token = ""
+ local.bridge.tokenizer = tokenizer
+
+ seen: dict = {}
+ original = local.bridge.original_model.forward
+
+ # functools.wraps so inspect.signature() still resolves to the real forward:
+ # the gate reads that signature, and a bare (*args, **kwargs) spy would look
+ # like it accepts position_ids and defeat the check under test.
+ @functools.wraps(original)
+ def _spy(*args, **kwargs):
+ seen.clear()
+ seen.update(kwargs)
+ return original(*args, **kwargs)
+
+ local.bridge.original_model.forward = _spy
+ try:
+ with torch.inference_mode():
+ logits = local.bridge(["token_5 token_7 token_9", "token_5"], return_type="logits")
+ batched = dict(seen)
+ with torch.inference_mode():
+ local.bridge("token_5 token_7 token_9", return_type="logits")
+ unbatched = dict(seen)
+ finally:
+ local.bridge.original_model.forward = original
+
+ assert logits.shape[0] == 2
+ assert "position_ids" not in batched
+ # The mask is still supplied — withholding it would reintroduce the padding
+ # contamination this branch exists to prevent.
+ assert "attention_mask" in batched
+ # Control: a single unbatched string never reached this branch, so the gate
+ # must not have changed anything for it either.
+ assert "position_ids" not in unbatched
+
+
def test_run_with_cache_exposes_hooks_without_hf_output_attentions(
models: TinyModels,
) -> None:
diff --git a/tests/integration/model_bridge/test_olmo2_attention_input_state.py b/tests/integration/model_bridge/test_olmo2_attention_input_state.py
new file mode 100644
index 0000000000..5a89a2f78c
--- /dev/null
+++ b/tests/integration/model_bridge/test_olmo2_attention_input_state.py
@@ -0,0 +1,73 @@
+"""OLMo 2 attention-input fork regression tests using a tiny local HF model."""
+
+import pytest
+import torch
+from transformers import AutoModelForCausalLM
+from transformers.models.olmo2 import Olmo2Config
+
+from transformer_lens.model_bridge import TransformerBridge
+from transformer_lens.model_bridge.sources._bridge_builder import (
+ build_bridge_config_from_hf,
+)
+from transformer_lens.model_bridge.supported_architectures.olmo2 import (
+ Olmo2ArchitectureAdapter,
+)
+
+
+class _Tokenizer:
+ pass
+
+
+def _tiny_olmo2_bridge() -> TransformerBridge:
+ torch.manual_seed(0)
+ config = Olmo2Config(
+ vocab_size=64,
+ hidden_size=32,
+ intermediate_size=64,
+ num_hidden_layers=2,
+ num_attention_heads=4,
+ num_key_value_heads=2,
+ max_position_embeddings=32,
+ )
+ config.architectures = ["Olmo2ForCausalLM"]
+ hf_model = AutoModelForCausalLM.from_config(config).to(torch.float32).eval()
+ bridge_config = build_bridge_config_from_hf(
+ hf_model.config, "Olmo2ForCausalLM", "olmo2-tiny", torch.float32
+ )
+ return TransformerBridge(
+ hf_model, Olmo2ArchitectureAdapter(bridge_config), tokenizer=_Tokenizer()
+ )
+
+
+@pytest.mark.parametrize(
+ ("setter_name", "input_hook_names"),
+ [
+ (
+ "set_use_split_qkv_input",
+ (
+ "blocks.1.attn.hook_q_input",
+ "blocks.1.attn.hook_k_input",
+ "blocks.1.attn.hook_v_input",
+ ),
+ ),
+ ("set_use_attn_in", ("blocks.1.attn.hook_attn_in",)),
+ ],
+)
+def test_attention_input_fork_does_not_leak_state_between_forwards(
+ setter_name: str, input_hook_names: tuple[str, ...]
+) -> None:
+ bridge = _tiny_olmo2_bridge()
+ bridge.enable_compatibility_mode()
+ getattr(bridge, setter_name)(True)
+ tokens = torch.tensor([[1, 2, 3, 4, 5]])
+
+ with torch.no_grad():
+ first_logits, first_cache = bridge.run_with_cache(tokens)
+ second_logits, second_cache = bridge.run_with_cache(tokens)
+
+ torch.testing.assert_close(second_logits, first_logits, rtol=0, atol=0)
+ for hook_name in input_hook_names:
+ torch.testing.assert_close(second_cache[hook_name], first_cache[hook_name], rtol=0, atol=0)
+ for cache in (first_cache, second_cache):
+ expected = cache["blocks.1.hook_resid_pre"].unsqueeze(2).expand_as(cache[hook_name])
+ torch.testing.assert_close(cache[hook_name], expected, rtol=0, atol=0)
diff --git a/tests/integration/model_bridge/test_parent_module_traversal.py b/tests/integration/model_bridge/test_parent_module_traversal.py
new file mode 100644
index 0000000000..5f60c79305
--- /dev/null
+++ b/tests/integration/model_bridge/test_parent_module_traversal.py
@@ -0,0 +1,397 @@
+"""Download-free parent traversal coverage across Bridge architecture shapes."""
+
+from dataclasses import dataclass
+from typing import Any, Callable
+
+import pytest
+import torch
+from torch import nn
+from transformers import (
+ ASTConfig,
+ ASTForAudioClassification,
+ BartConfig,
+ BartForConditionalGeneration,
+ BertConfig,
+ BertForMaskedLM,
+ BertForNextSentencePrediction,
+ BertForPreTraining,
+ BloomConfig,
+ BloomForCausalLM,
+ GPT2Config,
+ GPT2LMHeadModel,
+ GPTNeoXConfig,
+ GPTNeoXForCausalLM,
+ HubertConfig,
+ HubertForCTC,
+ LlamaConfig,
+ LlamaForCausalLM,
+ MistralConfig,
+ MistralForCausalLM,
+ MixtralConfig,
+ MixtralForCausalLM,
+ T5Config,
+ T5ForConditionalGeneration,
+ ViTConfig,
+ ViTForImageClassification,
+ ViTModel,
+)
+
+from transformer_lens.model_bridge.sources import build_bridge_from_module
+
+
+@dataclass(frozen=True)
+class ArchitectureCase:
+ name: str
+ model_type: type[nn.Module]
+ config_factory: Callable[[], Any]
+ architecture: str
+
+
+def _bert_config() -> BertConfig:
+ return BertConfig(
+ vocab_size=32,
+ hidden_size=16,
+ num_hidden_layers=1,
+ num_attention_heads=4,
+ intermediate_size=32,
+ max_position_embeddings=16,
+ )
+
+
+def _vit_config() -> ViTConfig:
+ return ViTConfig(
+ image_size=16,
+ patch_size=4,
+ num_channels=3,
+ hidden_size=16,
+ num_hidden_layers=1,
+ num_attention_heads=4,
+ intermediate_size=32,
+ num_labels=3,
+ )
+
+
+def _bart_config() -> BartConfig:
+ return BartConfig(
+ vocab_size=32,
+ d_model=16,
+ encoder_layers=1,
+ decoder_layers=1,
+ encoder_attention_heads=4,
+ decoder_attention_heads=4,
+ encoder_ffn_dim=32,
+ decoder_ffn_dim=32,
+ max_position_embeddings=16,
+ )
+
+
+def _hubert_config() -> HubertConfig:
+ return HubertConfig(
+ vocab_size=32,
+ hidden_size=16,
+ num_hidden_layers=1,
+ num_attention_heads=4,
+ intermediate_size=32,
+ conv_dim=(8,),
+ conv_stride=(2,),
+ conv_kernel=(3,),
+ num_conv_pos_embeddings=4,
+ num_conv_pos_embedding_groups=2,
+ )
+
+
+ARCHITECTURE_CASES = (
+ ArchitectureCase(
+ "gpt2-joint-qkv",
+ GPT2LMHeadModel,
+ lambda: GPT2Config(
+ vocab_size=32,
+ n_positions=16,
+ n_ctx=16,
+ n_embd=16,
+ n_layer=1,
+ n_head=4,
+ n_inner=32,
+ ),
+ "GPT2LMHeadModel",
+ ),
+ ArchitectureCase(
+ "bloom-joint-qkv",
+ BloomForCausalLM,
+ lambda: BloomConfig(vocab_size=32, hidden_size=16, n_layer=1, n_head=4),
+ "BloomForCausalLM",
+ ),
+ ArchitectureCase(
+ "gpt-neox-rotary",
+ GPTNeoXForCausalLM,
+ lambda: GPTNeoXConfig(
+ vocab_size=32,
+ hidden_size=16,
+ intermediate_size=32,
+ num_hidden_layers=1,
+ num_attention_heads=4,
+ max_position_embeddings=16,
+ ),
+ "GPTNeoXForCausalLM",
+ ),
+ ArchitectureCase(
+ "llama-split-qkv-rope",
+ LlamaForCausalLM,
+ lambda: LlamaConfig(
+ vocab_size=32,
+ hidden_size=16,
+ intermediate_size=32,
+ num_hidden_layers=1,
+ num_attention_heads=4,
+ num_key_value_heads=4,
+ max_position_embeddings=16,
+ ),
+ "LlamaForCausalLM",
+ ),
+ ArchitectureCase(
+ "mistral-gqa",
+ MistralForCausalLM,
+ lambda: MistralConfig(
+ vocab_size=32,
+ hidden_size=16,
+ intermediate_size=32,
+ num_hidden_layers=1,
+ num_attention_heads=4,
+ num_key_value_heads=2,
+ max_position_embeddings=16,
+ ),
+ "MistralForCausalLM",
+ ),
+ ArchitectureCase("bert-mlm", BertForMaskedLM, _bert_config, "BertForMaskedLM"),
+ ArchitectureCase(
+ "bert-nsp",
+ BertForNextSentencePrediction,
+ _bert_config,
+ "BertForMaskedLM",
+ ),
+ ArchitectureCase(
+ "bert-mlm-nsp",
+ BertForPreTraining,
+ _bert_config,
+ "BertForMaskedLM",
+ ),
+ ArchitectureCase(
+ "t5-encoder-decoder",
+ T5ForConditionalGeneration,
+ lambda: T5Config(
+ vocab_size=32,
+ d_model=16,
+ d_kv=4,
+ d_ff=32,
+ num_layers=1,
+ num_decoder_layers=1,
+ num_heads=4,
+ ),
+ "T5ForConditionalGeneration",
+ ),
+ ArchitectureCase(
+ "bart-encoder-decoder",
+ BartForConditionalGeneration,
+ _bart_config,
+ "BartForConditionalGeneration",
+ ),
+ ArchitectureCase(
+ "mixtral-moe",
+ MixtralForCausalLM,
+ lambda: MixtralConfig(
+ vocab_size=32,
+ hidden_size=16,
+ intermediate_size=32,
+ num_hidden_layers=1,
+ num_attention_heads=4,
+ num_key_value_heads=2,
+ num_local_experts=2,
+ num_experts_per_tok=1,
+ max_position_embeddings=16,
+ ),
+ "MixtralForCausalLM",
+ ),
+ ArchitectureCase(
+ "vit-vision",
+ ViTForImageClassification,
+ _vit_config,
+ "ViTForImageClassification",
+ ),
+ ArchitectureCase("vit-bare-pooler", ViTModel, _vit_config, "ViTModel"),
+ ArchitectureCase(
+ "hubert-audio",
+ HubertForCTC,
+ _hubert_config,
+ "HubertForCTC",
+ ),
+ ArchitectureCase(
+ "ast-audio-classifier",
+ ASTForAudioClassification,
+ lambda: ASTConfig(
+ hidden_size=16,
+ num_hidden_layers=1,
+ num_attention_heads=4,
+ intermediate_size=32,
+ patch_size=4,
+ frequency_stride=4,
+ time_stride=4,
+ max_length=16,
+ num_mel_bins=16,
+ ),
+ "ASTForAudioClassification",
+ ),
+)
+
+ARCHITECTURE_CASE_BY_NAME = {case.name: case for case in ARCHITECTURE_CASES}
+
+
+def _named_identities(named_values: Any) -> dict[int, str]:
+ return {id(value): name for name, value in named_values}
+
+
+def _assert_same_identities(expected: dict[int, str], actual: dict[int, str]) -> None:
+ missing = [expected[identity] for identity in expected.keys() - actual.keys()]
+ unexpected = [actual[identity] for identity in actual.keys() - expected.keys()]
+ assert actual.keys() == expected.keys(), f"missing={missing}, unexpected={unexpected}"
+
+
+@pytest.mark.parametrize("case", ARCHITECTURE_CASES, ids=lambda case: case.name)
+def test_parent_and_direct_traversal_have_identical_state(case: ArchitectureCase) -> None:
+ config = case.config_factory()
+ model = case.model_type(config).eval()
+ bridge = build_bridge_from_module(
+ model,
+ case.architecture,
+ hf_config=config,
+ dtype=torch.float32,
+ device="cpu",
+ model_name=f"tiny-{case.name}",
+ )
+ parent = nn.Module()
+ parent.add_module("bridge", bridge)
+
+ source_parameters = _named_identities(bridge.original_model.named_parameters())
+ direct_parameters = _named_identities(bridge.named_parameters())
+ parent_parameters = _named_identities(parent.named_parameters())
+ source_buffers = _named_identities(bridge.original_model.named_buffers())
+ direct_buffers = _named_identities(bridge.named_buffers())
+ parent_buffers = _named_identities(parent.named_buffers())
+
+ _assert_same_identities(source_parameters, direct_parameters)
+ _assert_same_identities(direct_parameters, parent_parameters)
+ _assert_same_identities(source_buffers, direct_buffers)
+ _assert_same_identities(direct_buffers, parent_buffers)
+
+
+def test_parent_dtype_conversion_updates_container_owned_state() -> None:
+ bart_config = _bart_config()
+ bridge = build_bridge_from_module(
+ BartForConditionalGeneration(bart_config),
+ "BartForConditionalGeneration",
+ hf_config=bart_config,
+ dtype=torch.float32,
+ device="cpu",
+ model_name="tiny-bart-container-buffer",
+ )
+ parent = nn.Module()
+ parent.add_module("bridge", bridge)
+
+ parent.to(torch.float64)
+
+ assert bridge.original_model.final_logits_bias.dtype == torch.float64
+ assert id(bridge.original_model.final_logits_bias) in {
+ id(buffer) for buffer in parent.buffers()
+ }
+
+
+def test_parent_assign_load_updates_container_owned_state() -> None:
+ bart_config = _bart_config()
+ bridge = build_bridge_from_module(
+ BartForConditionalGeneration(bart_config),
+ "BartForConditionalGeneration",
+ hf_config=bart_config,
+ dtype=torch.float32,
+ device="cpu",
+ model_name="tiny-bart-container-buffer-load",
+ )
+ parent = nn.Module()
+ parent.add_module("bridge", bridge)
+ state = parent.state_dict()
+ buffer_key = "bridge._container_state_owners.final_logits_bias"
+ state[buffer_key] = torch.ones_like(state[buffer_key])
+
+ parent.load_state_dict(state, strict=True, assign=True)
+
+ assert torch.equal(bridge.original_model.final_logits_bias, torch.ones_like(state[buffer_key]))
+ assert id(bridge.original_model.final_logits_bias) in {
+ id(buffer) for buffer in parent.buffers()
+ }
+
+
+@pytest.mark.parametrize(
+ ("case_name", "container_path", "state_name", "state_key"),
+ (
+ ("bart-encoder-decoder", "", "final_logits_bias", "final_logits_bias"),
+ ("hubert-audio", "hubert", "masked_spec_embed", "hubert.masked_spec_embed"),
+ ),
+)
+def test_direct_assign_load_stays_current_after_apply(
+ case_name: str, container_path: str, state_name: str, state_key: str
+) -> None:
+ case = ARCHITECTURE_CASE_BY_NAME[case_name]
+ config = case.config_factory()
+ bridge = build_bridge_from_module(
+ case.model_type(config),
+ case.architecture,
+ hf_config=config,
+ dtype=torch.float32,
+ device="cpu",
+ model_name=f"tiny-{case.name}-direct-assign",
+ )
+ original_container = (
+ bridge.original_model.get_submodule(container_path)
+ if container_path
+ else bridge.original_model
+ )
+ owner_container = (
+ bridge._container_state_owners.get_submodule(container_path)
+ if container_path
+ else bridge._container_state_owners
+ )
+ replacement = torch.full_like(getattr(original_container, state_name), 7)
+
+ bridge.load_state_dict({state_key: replacement}, strict=False, assign=True)
+
+ assert getattr(owner_container, state_name) is getattr(original_container, state_name)
+ bridge.cpu()
+ assert torch.equal(getattr(original_container, state_name), replacement)
+
+
+@pytest.mark.parametrize(
+ ("case_name", "key_fragment", "expected_keys"),
+ (
+ ("bert-nsp", "pooler", {"pooler.weight", "pooler.bias"}),
+ ("vit-bare-pooler", "pooler", {"pooler.weight", "pooler.bias"}),
+ (
+ "ast-audio-classifier",
+ "classifier",
+ {"classifier_ln.weight", "classifier_ln.bias"},
+ ),
+ ),
+)
+def test_task_head_state_dict_keys_are_not_reexpanded(
+ case_name: str, key_fragment: str, expected_keys: set[str]
+) -> None:
+ case = ARCHITECTURE_CASE_BY_NAME[case_name]
+ config = case.config_factory()
+ bridge = build_bridge_from_module(
+ case.model_type(config),
+ case.architecture,
+ hf_config=config,
+ dtype=torch.float32,
+ device="cpu",
+ model_name=f"tiny-{case.name}-state-dict-keys",
+ )
+
+ actual_keys = {key for key in bridge.state_dict() if key_fragment in key}
+ assert actual_keys == expected_keys
diff --git a/tests/integration/model_bridge/test_seq2seq_benchmark_loss.py b/tests/integration/model_bridge/test_seq2seq_benchmark_loss.py
new file mode 100644
index 0000000000..46dd763cd2
--- /dev/null
+++ b/tests/integration/model_bridge/test_seq2seq_benchmark_loss.py
@@ -0,0 +1,35 @@
+"""Benchmark loss calls must supply labels and a resolvable ablation hook on seq2seq.
+
+The bridge refuses label-less return_type="loss" for encoder-decoder models
+(encoder input_ids are not decoder targets). forward_pass.py was updated when
+that guard landed; hook_registration and weight_processing kept the bare call,
+so P2 hook_functionality errored on all seven seq2seq architectures. The
+ablation hook also targeted blocks.0.* which does not exist on encoder-decoder
+bridges, silently no-opping the whole check.
+"""
+
+import pytest
+
+from transformer_lens.benchmarks.hook_registration import benchmark_hook_functionality
+from transformer_lens.benchmarks.utils import BenchmarkSeverity, bridge_self_target_loss
+from transformer_lens.model_bridge import TransformerBridge
+
+TEXT = "translate English to German: Hello world"
+
+
+@pytest.fixture(scope="module")
+def t5():
+ return TransformerBridge.boot_transformers("google-t5/t5-small", device="cpu")
+
+
+def test_self_target_loss_is_finite_on_seq2seq(t5) -> None:
+ loss = bridge_self_target_loss(t5, TEXT)
+ assert loss.ndim == 0 and loss.isfinite()
+
+
+def test_hook_functionality_runs_and_the_ablation_bites(t5) -> None:
+ result = benchmark_hook_functionality(t5, TEXT)
+ assert result.passed, result.message
+ assert result.severity != BenchmarkSeverity.ERROR, result.message
+ # A vacuous run (unresolvable hook) reports "minimal effect: 0.000000".
+ assert "minimal effect" not in result.message, result.message
diff --git a/tests/integration/model_bridge/test_seq2seq_loss.py b/tests/integration/model_bridge/test_seq2seq_loss.py
new file mode 100644
index 0000000000..51d109d9cd
--- /dev/null
+++ b/tests/integration/model_bridge/test_seq2seq_loss.py
@@ -0,0 +1,281 @@
+"""Regression tests for TransformerBridge explicit-label loss semantics."""
+
+from __future__ import annotations
+
+import pytest
+import torch
+import torch.nn.functional as F
+from transformers import (
+ BartConfig,
+ BartForConditionalGeneration,
+ GPT2Config,
+ GPT2LMHeadModel,
+ T5Config,
+ T5ForConditionalGeneration,
+)
+
+from tests.integration.model_bridge.helpers import make_tiny_pair
+from transformer_lens.model_bridge import TransformerBridge
+
+
+@pytest.fixture(scope="module")
+def tiny_gpt2_pair() -> tuple[TransformerBridge, torch.nn.Module]:
+ config = GPT2Config(
+ vocab_size=32,
+ n_embd=16,
+ n_layer=1,
+ n_head=2,
+ n_positions=16,
+ n_ctx=16,
+ bos_token_id=1,
+ eos_token_id=2,
+ pad_token_id=0,
+ )
+ return make_tiny_pair(config, "GPT2LMHeadModel", loader=GPT2LMHeadModel)
+
+
+@pytest.fixture(scope="module", params=("bart", "t5"))
+def tiny_seq2seq_pair(request) -> tuple[TransformerBridge, torch.nn.Module]:
+ if request.param == "bart":
+ config = BartConfig(
+ vocab_size=32,
+ d_model=16,
+ encoder_layers=1,
+ decoder_layers=1,
+ encoder_attention_heads=2,
+ decoder_attention_heads=2,
+ encoder_ffn_dim=32,
+ decoder_ffn_dim=32,
+ max_position_embeddings=32,
+ pad_token_id=0,
+ bos_token_id=1,
+ eos_token_id=2,
+ decoder_start_token_id=2,
+ )
+ return make_tiny_pair(
+ config,
+ "BartForConditionalGeneration",
+ loader=BartForConditionalGeneration,
+ )
+
+ config = T5Config(
+ vocab_size=32,
+ d_model=16,
+ d_kv=8,
+ d_ff=32,
+ num_layers=1,
+ num_decoder_layers=1,
+ num_heads=2,
+ pad_token_id=0,
+ eos_token_id=1,
+ decoder_start_token_id=0,
+ )
+ return make_tiny_pair(
+ config,
+ "T5ForConditionalGeneration",
+ loader=T5ForConditionalGeneration,
+ )
+
+
+def test_seq2seq_loss_and_logits_follow_labels(
+ tiny_seq2seq_pair: tuple[TransformerBridge, torch.nn.Module],
+) -> None:
+ bridge, reference = tiny_seq2seq_pair
+ source = torch.tensor([[4, 5, 6, 7, 2]])
+ label_batches = (
+ torch.tensor([[8, 9, 10, 2, -100]]),
+ torch.tensor([[11, 12, 13, 2, -100]]),
+ )
+
+ bridge_losses = []
+ bridge_logits = []
+ for labels in label_batches:
+ with torch.no_grad():
+ loss = bridge(source, labels=labels, return_type="loss")
+ logits = bridge(source, labels=labels, return_type="logits")
+ reference_output = reference(input_ids=source, labels=labels)
+
+ torch.testing.assert_close(loss, reference_output.loss)
+ torch.testing.assert_close(logits, reference_output.logits)
+ bridge_losses.append(loss)
+ bridge_logits.append(logits)
+
+ assert not torch.allclose(bridge_losses[0], bridge_losses[1])
+ assert not torch.allclose(bridge_logits[0], bridge_logits[1])
+
+
+def test_seq2seq_both_allows_shorter_target(
+ tiny_seq2seq_pair: tuple[TransformerBridge, torch.nn.Module],
+) -> None:
+ bridge, reference = tiny_seq2seq_pair
+ source = torch.tensor([[4, 5, 6, 7, 2]])
+ labels = torch.tensor([[14, 15, 2]])
+
+ with torch.no_grad():
+ logits, loss = bridge(source, labels=labels, return_type="both")
+ reference_output = reference(input_ids=source, labels=labels)
+
+ assert logits.shape[:2] == labels.shape
+ torch.testing.assert_close(logits, reference_output.logits)
+ torch.testing.assert_close(loss, reference_output.loss)
+
+
+def test_seq2seq_loss_supports_hf_tuple_output(
+ tiny_seq2seq_pair: tuple[TransformerBridge, torch.nn.Module],
+) -> None:
+ bridge, reference = tiny_seq2seq_pair
+ source = torch.tensor([[4, 5, 6, 7, 2]])
+ labels = torch.tensor([[14, 15, 2]])
+
+ with torch.no_grad():
+ logits, loss = bridge(
+ source,
+ labels=labels,
+ return_type="both",
+ return_dict=False,
+ )
+ reference_loss, reference_logits, *_ = reference(
+ input_ids=source,
+ labels=labels,
+ return_dict=False,
+ )
+
+ torch.testing.assert_close(logits, reference_logits)
+ torch.testing.assert_close(loss, reference_loss)
+
+
+@pytest.mark.parametrize("return_type", ("loss", "both"))
+def test_seq2seq_per_token_loss_matches_unshifted_labels(
+ tiny_seq2seq_pair: tuple[TransformerBridge, torch.nn.Module],
+ return_type: str,
+) -> None:
+ bridge, reference = tiny_seq2seq_pair
+ source = torch.tensor([[4, 5, 6, 7, 2]])
+ labels = torch.tensor([[14, 15, 2, -100]])
+
+ with torch.no_grad():
+ output = bridge(
+ source,
+ labels=labels,
+ return_type=return_type,
+ loss_per_token=True,
+ )
+ reference_logits = reference(input_ids=source, labels=labels).logits
+
+ loss = output[1] if isinstance(output, tuple) else output
+ expected = F.cross_entropy(
+ reference_logits.flatten(0, 1),
+ labels.flatten(),
+ reduction="none",
+ ignore_index=-100,
+ ).view_as(labels)
+
+ assert loss.shape == labels.shape
+ assert loss[0, -1] == 0
+ torch.testing.assert_close(loss, expected)
+
+
+@pytest.mark.parametrize("return_type", ("loss", "both"))
+def test_seq2seq_loss_requires_labels(
+ tiny_seq2seq_pair: tuple[TransformerBridge, torch.nn.Module],
+ return_type: str,
+) -> None:
+ bridge, _ = tiny_seq2seq_pair
+ source = torch.tensor([[4, 5, 6, 7, 2]])
+
+ with pytest.raises(ValueError, match="labels are required"):
+ bridge(source, return_type=return_type)
+
+
+def test_causal_loss_uses_explicit_labels(
+ tiny_gpt2_pair: tuple[TransformerBridge, torch.nn.Module],
+) -> None:
+ bridge, reference = tiny_gpt2_pair
+ input_ids = torch.tensor([[4, 5, 6, 7, 2]])
+ labels = torch.full_like(input_ids, 9)
+
+ with torch.no_grad():
+ default_loss = bridge(input_ids, return_type="loss")
+ logits, labeled_loss = bridge(input_ids, labels=labels, return_type="both")
+ reference_output = reference(input_ids=input_ids, labels=labels)
+
+ assert not torch.allclose(labeled_loss, default_loss)
+ torch.testing.assert_close(logits, reference_output.logits)
+ torch.testing.assert_close(labeled_loss, reference_output.loss)
+
+
+def test_causal_labels_support_hf_tuple_output(
+ tiny_gpt2_pair: tuple[TransformerBridge, torch.nn.Module],
+) -> None:
+ bridge, reference = tiny_gpt2_pair
+ input_ids = torch.tensor([[4, 5, 6, 7, 2]])
+ labels = torch.full_like(input_ids, 9)
+
+ with torch.no_grad():
+ logits, loss = bridge(
+ input_ids,
+ labels=labels,
+ return_type="both",
+ return_dict=False,
+ )
+ reference_loss, reference_logits, *_ = reference(
+ input_ids=input_ids,
+ labels=labels,
+ return_dict=False,
+ )
+
+ torch.testing.assert_close(logits, reference_logits)
+ torch.testing.assert_close(loss, reference_loss)
+
+
+def test_causal_per_token_loss_uses_labels_and_ignore_index(
+ tiny_gpt2_pair: tuple[TransformerBridge, torch.nn.Module],
+) -> None:
+ bridge, reference = tiny_gpt2_pair
+ input_ids = torch.tensor([[4, 5, 6, 7, 2]])
+ labels = torch.tensor([[9, 8, 7, -100, -100]])
+
+ with torch.no_grad():
+ loss = bridge(
+ input_ids,
+ labels=labels,
+ return_type="loss",
+ loss_per_token=True,
+ )
+ reference_logits = reference(input_ids=input_ids).logits
+
+ expected = F.cross_entropy(
+ reference_logits[:, :-1].flatten(0, 1),
+ labels[:, 1:].flatten(),
+ reduction="none",
+ ignore_index=-100,
+ ).view_as(labels[:, 1:])
+
+ assert loss.shape == labels[:, 1:].shape
+ assert torch.count_nonzero(loss[:, 2:]) == 0
+ torch.testing.assert_close(loss, expected)
+
+
+def test_causal_explicit_labels_preserve_attention_mask_contract(
+ tiny_gpt2_pair: tuple[TransformerBridge, torch.nn.Module],
+) -> None:
+ bridge, _ = tiny_gpt2_pair
+ input_ids = torch.tensor([[4, 5, 6, 0, 0]])
+ labels = torch.tensor([[9, 8, 7, 6, 5]])
+ attention_mask = torch.tensor([[1, 1, 1, 0, 0]])
+
+ with torch.no_grad():
+ logits = bridge(input_ids, attention_mask=attention_mask, return_type="logits")
+ loss = bridge(
+ input_ids,
+ labels=labels,
+ attention_mask=attention_mask,
+ return_type="loss",
+ )
+
+ transition_mask = attention_mask[:, :-1].bool() & attention_mask[:, 1:].bool()
+ expected = F.cross_entropy(
+ logits[:, :-1][transition_mask],
+ labels[:, 1:][transition_mask],
+ )
+ torch.testing.assert_close(loss, expected)
diff --git a/tests/integration/test_jacobian_lens.py b/tests/integration/test_jacobian_lens.py
index ba263c85c4..661942dee6 100644
--- a/tests/integration/test_jacobian_lens.py
+++ b/tests/integration/test_jacobian_lens.py
@@ -464,6 +464,74 @@ def test_decompose_gpt2_activation_reconstructs_and_is_orthogonal(published_gpt2
assert cosine.abs().item() < 1e-3
+def test_occupancy_gpt2_activation_is_a_small_positive_integer(published_gpt2_lens, gpt2_bridge):
+ """occupancy on a real GPT-2 activation returns a positive integer within ``[1, max_atoms]``,
+ with per-step real and control captured-variance curves of the right shape. We assert shape and
+ bounds -- not the paper's closed-model count (an open-weight observation, recorded on failure).
+ A reduced control count keeps CI fast."""
+ layer, max_atoms = 6, 12
+ result = published_gpt2_lens.occupancy(
+ gpt2_bridge,
+ PROMPT,
+ layer=layer,
+ position=-1,
+ max_atoms=max_atoms,
+ num_control_dictionaries=8,
+ )
+
+ assert isinstance(result.occupancy, int)
+ assert 1 <= result.occupancy <= max_atoms, f"occupancy={result.occupancy} max_atoms={max_atoms}"
+ assert result.marginal_captured_variance.shape == (max_atoms,)
+ assert result.control_captured_variance.shape == (max_atoms,)
+ assert result.support.shape == (max_atoms,)
+ assert (result.support >= 0).all() and (result.support < gpt2_bridge.cfg.d_vocab).all()
+
+ # Cumulative captured variance is a projection ratio: non-decreasing and bounded to [0, 1].
+ real_cumulative = result.marginal_captured_variance.cumsum(0)
+ assert (result.marginal_captured_variance >= -1e-4).all()
+ assert (real_cumulative >= -1e-4).all() and (real_cumulative <= 1.0 + 1e-4).all()
+
+ # Deterministic given the seed: an identical call reproduces the count and both curves exactly.
+ repeat = published_gpt2_lens.occupancy(
+ gpt2_bridge,
+ PROMPT,
+ layer=layer,
+ position=-1,
+ max_atoms=max_atoms,
+ num_control_dictionaries=8,
+ )
+ assert repeat.occupancy == result.occupancy
+ assert torch.equal(repeat.marginal_captured_variance, result.marginal_captured_variance)
+ assert torch.equal(repeat.control_captured_variance, result.control_captured_variance)
+
+
+def test_fraction_of_variance_gpt2_is_a_small_ratio(published_gpt2_lens, gpt2_bridge):
+ """fraction_of_variance over a small corpus: each layer's median and pooled ratio land in
+ ``[0, 1]`` with samples recorded, consistent with the paper's "J-space is a small fraction of
+ total variance" (an open-weight observation, not a numeric match to the closed-model figures).
+ FIT_PROMPTS are long enough to clear the default 16-position skip."""
+ layers = [3, 6]
+ profile = published_gpt2_lens.fraction_of_variance(gpt2_bridge, FIT_PROMPTS, layers=layers, k=8)
+
+ assert profile.layers == layers
+ for layer in layers:
+ per_position = profile.per_position[layer]
+ assert per_position.numel() > 0
+ # Each fraction is ||projection||^2 / ||activation||^2, a variance ratio in [0, 1].
+ assert (per_position >= 0.0).all() and (per_position <= 1.0).all()
+ assert 0.0 <= profile.median[layer] <= 1.0
+ assert 0.0 <= profile.pooled[layer] <= 1.0
+
+ # positions= overrides the skip_first sweep: one explicit position per prompt over the
+ # two-prompt corpus exercises the override path and cross-prompt pooling (one sample each).
+ pinned = published_gpt2_lens.fraction_of_variance(
+ gpt2_bridge, FIT_PROMPTS, layers=layers, k=8, positions=[-1]
+ )
+ for layer in layers:
+ assert pinned.per_position[layer].numel() == len(FIT_PROMPTS)
+ assert 0.0 <= pinned.pooled[layer] <= 1.0
+
+
@pytest.mark.slow
def test_decompose_gemma_activation_is_valid():
"""Decompose a real gemma-2-2b-it activation via its published lens (slow: real download)."""
diff --git a/tests/unit/benchmarks/test_forward_pass_loss.py b/tests/unit/benchmarks/test_forward_pass_loss.py
new file mode 100644
index 0000000000..6b011321a9
--- /dev/null
+++ b/tests/unit/benchmarks/test_forward_pass_loss.py
@@ -0,0 +1,36 @@
+"""Loss benchmarks must follow the explicit-label Bridge contract."""
+
+from typing import Any
+
+import pytest
+import torch
+
+from transformer_lens.benchmarks.forward_pass import benchmark_loss_equivalence
+from transformer_lens.model_bridge import TransformerBridge
+
+
+def test_benchmark_loss_equivalence_supplies_tokenized_self_labels(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ bridge = object.__new__(TransformerBridge)
+ torch.nn.Module.__init__(bridge)
+ labels = torch.tensor([[1, 2, 3]])
+ forward_kwargs: dict[str, Any] = {}
+
+ def to_tokens(text: str, **kwargs: Any) -> torch.Tensor:
+ assert text == "benchmark text"
+ return labels
+
+ def forward(input: str, **kwargs: Any) -> torch.Tensor:
+ assert input == "benchmark text"
+ forward_kwargs.update(kwargs)
+ return torch.tensor(1.25)
+
+ monkeypatch.setattr(bridge, "to_tokens", to_tokens)
+ monkeypatch.setattr(bridge, "forward", forward)
+
+ result = benchmark_loss_equivalence(bridge, "benchmark text", reference_loss=1.25)
+
+ assert result.passed
+ assert forward_kwargs["labels"] is labels
+ assert forward_kwargs["return_type"] == "loss"
diff --git a/tests/unit/benchmarks/test_text_quality_image_conditioned.py b/tests/unit/benchmarks/test_text_quality_image_conditioned.py
index b6b0be4522..25938ed8b6 100644
--- a/tests/unit/benchmarks/test_text_quality_image_conditioned.py
+++ b/tests/unit/benchmarks/test_text_quality_image_conditioned.py
@@ -1,11 +1,6 @@
-"""Image-conditioned seq2seq (Florence-2) must score P4 from real captions.
-
-Florence-2 needs pixel_values to generate: given a text-only prompt its decoder
-emits a 1-token EOS, so every continuation is "too short" and P4 scored 0 (a
-misleading failure for a working model). The fix drives real image-conditioned
-caption generation ( on synthetic test images) and scores that
-grammatical output instead — a genuine quality signal, not a skip.
-"""
+"""Caption test images must be real, distinct RGB inputs — averaging caption
+scores over identical images would be a fake sample size. (The end-to-end
+Florence-2 caption test lives in tests/integration/benchmarks/.)"""
import pytest
@@ -21,29 +16,3 @@ def test_build_caption_test_images_are_distinct_rgb():
assert all(im.mode == "RGB" and im.size == (224, 224) for im in images)
# Distinct backgrounds -> distinct pixel data (averaging over samples is real).
assert len({im.tobytes() for im in images}) == 3
-
-
-def test_florence2_text_quality_scores_image_captions():
- from transformer_lens.benchmarks.text_quality import benchmark_text_quality
- from transformer_lens.model_bridge import TransformerBridge
-
- try:
- bridge = TransformerBridge.boot_transformers(
- "florence-community/Florence-2-base-ft", device="cpu"
- )
- except (OSError, ConnectionError, TimeoutError) as exc:
- pytest.skip(f"florence-2 unavailable offline: {exc}")
-
- # Preconditions: this is the image-conditioned seq2seq path.
- assert bridge.original_model.config.is_encoder_decoder
- assert getattr(bridge.cfg, "is_multimodal", False)
-
- result = benchmark_text_quality(
- bridge, "The theory of relativity explains that", max_new_tokens=50, device="cpu"
- )
- # Pre-fix: "Scoring failed for all prompts" (score absent -> registry P4=0).
- assert result.details is not None, result.message
- assert "score" in result.details, result.message
- assert result.details["score"] > 0
- # Scored the model's actual captions, not the 4 text-only prompts.
- assert result.details["num_prompts"] >= 1
diff --git a/tests/unit/benchmarks/test_text_quality_scoring.py b/tests/unit/benchmarks/test_text_quality_scoring.py
new file mode 100644
index 0000000000..21c368a6fa
--- /dev/null
+++ b/tests/unit/benchmarks/test_text_quality_scoring.py
@@ -0,0 +1,712 @@
+"""Reference-ratio Phase-4 scoring: the score must be a judge-handicap-free
+comparison against a reference completion, with penalties for loops and
+truncation, generated via token-level slicing (string-prefix slicing breaks
+under chat templates because generate() strips special tokens on decode)."""
+
+import math
+from types import SimpleNamespace
+
+import pytest
+import torch
+
+pytest.importorskip("transformers")
+
+from transformer_lens.benchmarks.text_quality import (
+ JUDGE_R_FAIL,
+ _length_penalty,
+ _ratio_to_score,
+ benchmark_text_quality,
+)
+from transformer_lens.benchmarks.utils import BenchmarkResult, BenchmarkSeverity
+
+
+class FakeVocabTokenizer:
+ """Whitespace tokenizer with a growable vocab and template-marker specials."""
+
+ chat_template = None
+
+ def __init__(self):
+ self._vocab: list[str] = []
+ self._special: set[int] = set()
+ self.mask_token = None
+
+ def _id(self, word: str, special: bool = False) -> int:
+ if word not in self._vocab:
+ self._vocab.append(word)
+ idx = self._vocab.index(word)
+ if special:
+ self._special.add(idx)
+ return idx
+
+ def encode_words(self, text: str, special: bool = False) -> list[int]:
+ return [self._id(w, special) for w in text.split()]
+
+ def __call__(self, text, return_tensors=None):
+ # Native recipe used for encoder-decoder inputs.
+ ids = self.encode_words(text)
+ if return_tensors == "pt":
+ return {"input_ids": torch.tensor([ids])}
+ return {"input_ids": ids}
+
+ def decode(self, ids, skip_special_tokens=True):
+ ids = ids.tolist() if hasattr(ids, "tolist") else list(ids)
+ words = [self._vocab[i] for i in ids if not (skip_special_tokens and i in self._special)]
+ return " ".join(words)
+
+ def apply_chat_template(self, messages, add_generation_prompt=True, tokenize=False):
+ return f"<|im_start|> {messages[0]['content']} <|im_end|>"
+
+
+class FakeBridge:
+ """Decoder-only bridge stub: generate() echoes prompt ids + canned continuation."""
+
+ def __init__(
+ self, continuation="the quick brown fox jumps over the lazy dog today", chat_template=None
+ ):
+ self.tokenizer = FakeVocabTokenizer()
+ self.tokenizer.chat_template = chat_template
+ self.adapter = SimpleNamespace(supports_generation=True, native_sampler=None)
+ self.original_model = SimpleNamespace(
+ config=SimpleNamespace(is_encoder_decoder=False, architectures=["FakeLM"])
+ )
+ self.cfg = SimpleNamespace(device="cpu", is_multimodal=False, model_name="fake")
+ self._continuation = continuation
+ self.generate_calls: list[dict] = []
+ self.to_tokens_calls: list = []
+
+ def to_tokens(self, text, prepend_bos=None, **kwargs):
+ self.to_tokens_calls.append(prepend_bos)
+ special = text.startswith("<|im_start|>")
+ if special:
+ # Template markers become special ids that decode drops.
+ ids = []
+ for word in text.split():
+ is_marker = word.startswith("<|")
+ ids.append(self.tokenizer._id(word, special=is_marker))
+ return torch.tensor([ids])
+ return torch.tensor([self.tokenizer.encode_words(text)])
+
+ def generate(self, input, **kwargs):
+ self.generate_calls.append(kwargs)
+ cont_ids = self.tokenizer.encode_words(self._continuation)
+ return torch.cat([input, torch.tensor([cont_ids])], dim=1)
+
+
+class FakeJudgeTokenizer:
+ """Word-level judge tokenizer sharing nothing with the bridge's."""
+
+ def __init__(self):
+ self._vocab: list[str] = []
+
+ def __call__(self, text, return_tensors=None):
+ ids = []
+ for w in text.split():
+ if w not in self._vocab:
+ self._vocab.append(w)
+ ids.append(self._vocab.index(w))
+ if return_tensors == "pt":
+ return {"input_ids": torch.tensor([ids])}
+ return {"input_ids": ids}
+
+
+class FakeJudge:
+ """Judge whose loss is a configurable function of the scored token ids.
+
+ Records (masked_context_words, scored_words) per call so tests can assert
+ what the judge was conditioned on."""
+
+ def __init__(self, tokenizer: FakeJudgeTokenizer, loss_fn):
+ self._tokenizer = tokenizer
+ self._loss_fn = loss_fn
+ self.calls: list = []
+
+ def __call__(self, input_ids, labels=None):
+ pairs = list(zip(input_ids[0].tolist(), labels[0].tolist()))
+ scored = [int(t) for t, l in pairs if l != -100]
+ masked = [int(t) for t, l in pairs if l == -100]
+ words = " ".join(self._tokenizer._vocab[i] for i in scored)
+ self.calls.append((" ".join(self._tokenizer._vocab[i] for i in masked), words))
+ return SimpleNamespace(loss=torch.tensor(self._loss_fn(words)))
+
+
+def _run(bridge, profile="continuation", loss_fn=lambda text: 1.0, **kwargs):
+ judge_tokenizer = FakeJudgeTokenizer()
+ judge = FakeJudge(judge_tokenizer, loss_fn)
+ result = benchmark_text_quality(
+ bridge, profile, judge_model=judge, judge_tokenizer=judge_tokenizer, **kwargs
+ )
+ bridge.judge_calls = judge.calls
+ return result
+
+
+class TestRatioMath:
+ def test_ratio_one_scores_100(self):
+ assert _ratio_to_score(1.0) == 100.0
+
+ def test_ratio_r_fail_scores_zero(self):
+ assert _ratio_to_score(JUDGE_R_FAIL) == pytest.approx(0.0, abs=1e-9)
+
+ def test_ratio_sqrt_r_fail_scores_50(self):
+ """Registry's phase-4 floor of 50 = geometric midpoint of good and broken."""
+ assert _ratio_to_score(math.sqrt(JUDGE_R_FAIL)) == pytest.approx(50.0, abs=1e-9)
+
+ def test_ratio_below_one_clamps_to_100(self):
+ """Beating the reference is not extra credit (loops get there trivially)."""
+ assert _ratio_to_score(0.2) == 100.0
+
+ def test_ratio_is_handicap_invariant(self):
+ """A judge that is k-times worse at some language multiplies BOTH sides'
+ perplexity, so the score must not move (the old absolute-perplexity
+ mapping drops by 10 ln k)."""
+ gen_ppl, ref_ppl, k = 40.0, 25.0, 10.0
+ assert _ratio_to_score(gen_ppl / ref_ppl) == pytest.approx(
+ _ratio_to_score((k * gen_ppl) / (k * ref_ppl))
+ )
+
+
+class TestLengthPenalty:
+ def test_neutral_at_reference_length(self):
+ assert _length_penalty(40, 40) == 1.0
+
+ def test_neutral_band_half_to_triple(self):
+ """Neutral in [0.5x, 3x] of reference: terse-but-complete answers are
+ not punished, and the old 25% floor (which never fired in four
+ sweeps — a contentless chat stub scored 93.6) is gone."""
+ assert _length_penalty(20, 40) == 1.0
+ assert _length_penalty(120, 40) == 1.0
+
+ def test_penalizes_below_half(self):
+ assert _length_penalty(10, 40) == pytest.approx(0.5)
+ assert _length_penalty(13, 41) == pytest.approx(13 / 20.5)
+
+ def test_penalizes_overlength(self):
+ """Rambling output the repetition penalty misses: 6x the reference
+ pays half."""
+ assert _length_penalty(240, 40) == pytest.approx(0.5)
+
+ def test_zero_reference_is_neutral(self):
+ assert _length_penalty(3, 0) == 1.0
+
+
+class TestBenchmarkPipeline:
+ def test_fluent_output_scores_high(self):
+ # Continuation long enough to sit in the length-penalty neutral band
+ # for every reference; ratio 1 then clamps every prompt to 100.
+ fluent = "the quick brown fox jumps over the lazy dog today while the sun sets slowly behind the old hills"
+ result = _run(FakeBridge(continuation=fluent), loss_fn=lambda t: 1.0)
+ assert result.details is not None
+ assert result.details["score"] == 100.0
+
+ def test_looping_output_scores_low_despite_low_ratio(self):
+ """A degenerate loop has LOW judge perplexity; only the repetition
+ penalty catches it under ratio scoring."""
+ loop = "the cat sat the cat sat the cat sat the cat sat"
+ result = _run(FakeBridge(continuation=loop), loss_fn=lambda t: 0.1)
+ assert result.details is not None
+ assert result.details["score"] < 50.0
+
+ def test_one_token_output_scores_zero(self):
+ """Florence-style bare-EOS output: one token is not scoreable text."""
+ result = _run(FakeBridge(continuation="x"), loss_fn=lambda t: 1.0)
+ assert result.details is not None
+ assert result.details["score"] == 0.0
+
+ def test_generated_segment_sliced_by_token_count(self):
+ """Chat-template prompts are not string prefixes of decoded output
+ (specials are stripped); prompt words must still be excluded from the
+ judged text."""
+ seen: list[str] = []
+
+ def record(text):
+ seen.append(text)
+ return 1.0
+
+ bridge = FakeBridge(chat_template="{{messages}}")
+ result = _run(bridge, profile="chat", loss_fn=record)
+ assert result.details is not None
+ gen_texts = seen[0::2] # generated, reference alternate
+ assert all("<|im_start|>" not in t for t in seen)
+ for text in gen_texts:
+ assert text == bridge._continuation
+
+ def test_context_mask_does_not_swallow_first_generated_token(self):
+ """Tokenizing prompt+text as one string lets the tokenizer merge
+ across the seam, so the context mask swallows the first generated
+ token; the pieces must be tokenized separately."""
+ seen: list[str] = []
+
+ def record(text):
+ seen.append(text)
+ return 1.0
+
+ bridge = FakeBridge(continuation="zebra jumps over seven quiet green hills today")
+ _run(bridge, loss_fn=record)
+ gen_texts = [t for t in seen if "zebra" in t or "jumps" in t]
+ assert gen_texts, seen
+ assert all(t.startswith("zebra") for t in gen_texts), gen_texts
+
+ def test_empty_generation_is_danger(self):
+ result = _run(FakeBridge(continuation=""), loss_fn=lambda t: 1.0)
+ assert result.severity == BenchmarkSeverity.DANGER
+ assert result.passed is False
+
+ def test_uncovered_profile_skips_with_coverage_instruction(self):
+ """A coverage gap must tell the operator to file an issue, never score."""
+ result = _run(FakeBridge(), profile="task:translation@en-sw")
+ assert result.severity == BenchmarkSeverity.SKIPPED
+ assert "task:translation@en-sw" in result.message
+ assert "file a TransformerLens issue" in result.message
+
+ def test_chat_without_template_downgrades_to_continuation(self):
+ bridge = FakeBridge(chat_template=None)
+ result = _run(bridge, profile="chat")
+ assert result.details is not None
+ assert result.details["prompt_profile"] == "continuation"
+ assert "no chat template" in result.details["profile_adjustment"]
+
+ def test_per_prompt_seed_independent_of_order(self):
+ """Each prompt's sample stream restarts at the benchmark seed, so a
+ prompt's output cannot depend on how much RNG earlier prompts consumed."""
+
+ class RngBridge(FakeBridge):
+ def generate(self, input, **kwargs):
+ # Fixed RNG consumption, then a draw: identical across prompts
+ # only if every prompt's stream restarts at the benchmark seed.
+ torch.rand(5)
+ draw = int(torch.randint(0, 10_000, (1,)).item())
+ cont = self.tokenizer.encode_words(f"gen{draw} token one two")
+ return torch.cat([input, torch.tensor([cont])], dim=1)
+
+ seen: list[str] = []
+
+ def record(text):
+ seen.append(text)
+ return 1.0
+
+ _run(RngBridge(), loss_fn=record)
+ gen_texts = [t for t in seen if t.startswith("gen")]
+ assert len(gen_texts) >= 2
+ assert len(set(gen_texts)) == 1, gen_texts
+
+
+class TestProfileDataIntegrity:
+ def test_every_table_entry_is_scoreable(self):
+ from transformer_lens.benchmarks import text_quality_profiles as p
+ from transformer_lens.benchmarks.text_quality import _wrong_language
+
+ tables = [
+ p.CONTINUATION_PROMPTS,
+ p.CHAT_PROMPTS,
+ p.SUMMARIZATION_PROMPTS,
+ p.INSTRUCTION_PROMPTS,
+ p.DENOISE_PROMPTS,
+ ]
+ for table in tables:
+ for lang, entries in table.items():
+ for entry in entries:
+ assert entry.prompt.strip()
+ assert entry.reference.strip()
+ # Content, not just shape: a reference that self-flags as
+ # wrong-language hard-zeros its own sample (a fr reference
+ # did; the shape checks missed it).
+ if table in (p.CONTINUATION_PROMPTS, p.CHAT_PROMPTS):
+ assert not _wrong_language(entry.reference, lang), (
+ lang,
+ entry.reference[:50],
+ )
+
+ def test_pivot_sentences_index_aligned(self):
+ from transformer_lens.benchmarks.text_quality_profiles import PIVOT_SENTENCES
+
+ lengths = {lang: len(rows) for lang, rows in PIVOT_SENTENCES.items()}
+ assert set(lengths.values()) == {3}, lengths
+
+ def test_all_kinds_have_knobs(self):
+ from transformer_lens.benchmarks import text_quality_profiles as p
+
+ assert set(p.MAX_NEW_TOKENS_BY_KIND) == set(p.PROFILE_KINDS)
+
+
+class TestTranslationWiring:
+ def test_forced_bos_threaded_for_multilingual_translators(self):
+ """M2M100/MBart select target language via the first decoder token;
+ dropping the forced_bos_token_id kwarg silently translates into an
+ arbitrary language (and the judge would score that fluent text well)."""
+
+ class M2M100Bridge(FakeBridge):
+ def __init__(self):
+ super().__init__(continuation="ich muss jetzt wirklich schlafen gehen heute abend")
+ self.original_model.config.is_encoder_decoder = True
+ self.tokenizer.get_lang_id = lambda lang: {"de": 777, "en": 700}.get(lang, 0)
+ self.tokenizer.src_lang = "en"
+
+ def generate(self, input, **kwargs):
+ # Real enc-dec output shape: [decoder_start] + generated, never
+ # the echoed source prompt.
+ self.generate_calls.append(kwargs)
+ start = torch.tensor([[0]])
+ cont_ids = self.tokenizer.encode_words(self._continuation)
+ return torch.cat([start, torch.tensor([cont_ids])], dim=1)
+
+ bridge = M2M100Bridge()
+ result = _run(bridge, profile="task:translation@en-de")
+ assert result.details is not None, result.message
+ assert all(call.get("forced_bos_token_id") == 777 for call in bridge.generate_calls)
+ assert bridge.tokenizer.src_lang == "en"
+
+
+class TestReviewGuards:
+ """Guards for defects found in adversarial review of the rework."""
+
+ def test_cjk_repetition_penalty_uses_characters(self):
+ """Whitespace-split n-grams see zh/ja text as one word and never fire —
+ exactly where the judge rewards loops with low perplexity."""
+ from transformer_lens.benchmarks.text_quality import _compute_repetition_penalty
+
+ assert _compute_repetition_penalty("的" * 20) < 0.2
+ assert _compute_repetition_penalty("のの" * 10) < 0.3
+ fluent_zh = "长城是中国古代伟大的防御工程,每年吸引大量游客。"
+ assert _compute_repetition_penalty(fluent_zh) > 0.7
+
+ def test_wrong_language_output_scores_zero(self):
+ """Ratio scoring measures fluency, not language: fluent English beats a
+ short German reference and clamps to 100 unless language is checked."""
+ bridge = FakeBridge(continuation="the quick brown fox jumps over the lazy dog and the cat")
+ seen = []
+ judge_tokenizer = FakeJudgeTokenizer()
+ judge = FakeJudge(judge_tokenizer, lambda t: (seen.append(t) or 1.0))
+ from transformer_lens.benchmarks.text_quality import benchmark_text_quality
+
+ result = benchmark_text_quality(
+ bridge, "continuation@de", judge_model=judge, judge_tokenizer=judge_tokenizer
+ )
+ assert result.details is not None
+ assert result.details["score"] == 0.0
+ assert "not in 'de'" in result.details["per_prompt"]
+
+ def test_wrong_language_check_passes_correct_language(self):
+ bridge = FakeBridge(
+ continuation="der alte Zug ist nicht mit einem neuen Wagen gefahren und die Leute"
+ )
+ judge_tokenizer = FakeJudgeTokenizer()
+ judge = FakeJudge(judge_tokenizer, lambda t: 1.0)
+ from transformer_lens.benchmarks.text_quality import benchmark_text_quality
+
+ result = benchmark_text_quality(
+ bridge, "continuation@de", judge_model=judge, judge_tokenizer=judge_tokenizer
+ )
+ assert result.details is not None
+ assert result.details["score"] > 0.0
+
+ def test_empty_output_scored_zero_not_dropped(self):
+ """An empty generation must drag the average down, not vanish from it."""
+
+ class HalfEmptyBridge(FakeBridge):
+ def __init__(self):
+ super().__init__()
+ self._call = 0
+
+ def generate(self, input, **kwargs):
+ self._call += 1
+ if self._call % 2 == 0:
+ return input # no new tokens -> empty continuation
+ cont = self.tokenizer.encode_words(self._continuation)
+ return torch.cat([input, torch.tensor([cont])], dim=1)
+
+ result = _run(HalfEmptyBridge(), loss_fn=lambda t: 1.0)
+ assert result.details is not None
+ assert result.details["num_prompts"] == 4
+ assert 40.0 <= result.details["score"] <= 60.0, result.details
+
+ def test_denoise_t5_fill_spliced_into_sentence(self):
+ """Bare span fragments have judge PPL in the thousands, making the
+ ratio vacuous; the fill must be judged inside the restored sentence."""
+ seen: list[str] = []
+
+ class DenoiseBridge(FakeBridge):
+ def __init__(self):
+ super().__init__(continuation="played happily")
+ self.original_model.config.is_encoder_decoder = True
+
+ def generate(self, input, **kwargs):
+ self.generate_calls.append(kwargs)
+ start = torch.tensor([[0]])
+ cont = self.tokenizer.encode_words(self._continuation)
+ return torch.cat([start, torch.tensor([cont])], dim=1)
+
+ bridge = DenoiseBridge()
+ judge_tokenizer = FakeJudgeTokenizer()
+ judge = FakeJudge(judge_tokenizer, lambda t: (seen.append(t) or 1.0))
+ from transformer_lens.benchmarks.text_quality import benchmark_text_quality
+
+ result = benchmark_text_quality(
+ bridge, "task:denoise", judge_model=judge, judge_tokenizer=judge_tokenizer
+ )
+ assert result.details is not None
+ # The bare fill must never reach the judge; every judged text is a
+ # full restored sentence.
+ assert seen and all(len(t.split()) >= 8 for t in seen), seen
+ assert "The children played happily in the park until the sun went down." in seen
+
+ def test_chat_prepend_bos_false_threaded_to_tokenizer(self):
+ """The chat template supplies its own BOS; to_tokens must receive
+ prepend_bos=False or the prompt gets a double BOS."""
+ bridge = FakeBridge(chat_template="{{messages}}")
+ _run(bridge, profile="chat")
+ assert bridge.to_tokens_calls and all(v is False for v in bridge.to_tokens_calls)
+
+ def test_translation_scored_jointly(self):
+ """Short pivot sentences have unstable judge PPL; the three samples
+ must be concatenated into one judged pair."""
+
+ class MarianBridge(FakeBridge):
+ def __init__(self):
+ super().__init__(continuation="ik moet nu echt gaan slapen vandaag")
+ self.original_model.config.is_encoder_decoder = True
+
+ def generate(self, input, **kwargs):
+ self.generate_calls.append(kwargs)
+ start = torch.tensor([[0]])
+ cont = self.tokenizer.encode_words(self._continuation)
+ return torch.cat([start, torch.tensor([cont])], dim=1)
+
+ bridge = MarianBridge()
+ judge_tokenizer = FakeJudgeTokenizer()
+ judge = FakeJudge(judge_tokenizer, lambda t: 1.0)
+ from transformer_lens.benchmarks.text_quality import benchmark_text_quality
+
+ result = benchmark_text_quality(
+ bridge, "task:translation@en-nl", judge_model=judge, judge_tokenizer=judge_tokenizer
+ )
+ assert result.details is not None
+ assert result.details["num_prompts"] == 1
+ assert len(bridge.generate_calls) == 3 # generation stays per-sentence
+
+ def test_task_kinds_generate_greedily(self):
+ """Users run translators deterministically; sampling variance also
+ makes a single-sample score unstable. Task kinds must pass
+ temperature 0.0 while open-ended kinds keep sampling."""
+ cont_bridge = FakeBridge()
+ _run(cont_bridge, profile="continuation")
+ assert all(c["temperature"] == 0.7 for c in cont_bridge.generate_calls)
+
+ class MarianBridge(FakeBridge):
+ def __init__(self):
+ super().__init__(continuation="ik moet nu echt gaan slapen vandaag")
+ self.original_model.config.is_encoder_decoder = True
+
+ def generate(self, input, **kwargs):
+ self.generate_calls.append(kwargs)
+ start = torch.tensor([[0]])
+ cont = self.tokenizer.encode_words(self._continuation)
+ return torch.cat([start, torch.tensor([cont])], dim=1)
+
+ task_bridge = MarianBridge()
+ _run(task_bridge, profile="task:translation@en-nl")
+ assert all(c["temperature"] == 0.0 for c in task_bridge.generate_calls)
+
+ def test_encdec_prompt_uses_native_tokenizer_recipe(self):
+ """Encoder input must follow the tokenizer's own recipe (lang token +
+ trailing ); to_tokens' BOS policy injects and drops ,
+ which sent m2m100 into a quote-mark loop."""
+ BOS, EOS = 901, 902
+
+ class RecipeTokenizer(FakeVocabTokenizer):
+ def __call__(self, text, return_tensors=None):
+ ids = self.encode_words(text) + [EOS]
+ if return_tensors == "pt":
+ return {"input_ids": torch.tensor([ids])}
+ return {"input_ids": ids}
+
+ class RecipeBridge(FakeBridge):
+ def __init__(self):
+ super().__init__(continuation="ik moet gaan slapen vandaag echt nu")
+ self.tokenizer.__class__ = RecipeTokenizer
+ self.original_model.config.is_encoder_decoder = True
+ self.seen_inputs: list = []
+
+ def to_tokens(self, text, prepend_bos=None, **kwargs):
+ self.to_tokens_calls.append(prepend_bos)
+ return torch.tensor([[BOS] + self.tokenizer.encode_words(text)])
+
+ def generate(self, input, **kwargs):
+ self.generate_calls.append(kwargs)
+ self.seen_inputs.append(input[0].tolist())
+ start = torch.tensor([[0]])
+ cont = self.tokenizer.encode_words(self._continuation)
+ return torch.cat([start, torch.tensor([cont])], dim=1)
+
+ bridge = RecipeBridge()
+ _run(bridge, profile="task:translation@en-nl")
+ assert bridge.seen_inputs, "no generation happened"
+ for ids in bridge.seen_inputs:
+ assert ids[-1] == EOS, ids
+ assert BOS not in ids, ids
+
+ def test_dead_encdec_denoise_scores_zero(self):
+ """An empty span fill must not be spliced into the prompt sentence:
+ the splice hands a dead enc-dec model the near-reference sentence and
+ a free 100 (decoder-only dead models already scored 0)."""
+
+ class DeadT5Bridge(FakeBridge):
+ def __init__(self):
+ super().__init__()
+ self.original_model.config.is_encoder_decoder = True
+ self.tokenizer.mask_token = None
+
+ def generate(self, input, **kwargs):
+ self.generate_calls.append(kwargs)
+ return torch.tensor([[0]])
+
+ result = _run(DeadT5Bridge(), profile="task:denoise")
+ assert result.details["score"] == 0.0
+ assert result.severity == BenchmarkSeverity.DANGER
+
+
+class TestRound2ReviewGuards:
+ """Guards for the second review round's confirmed findings."""
+
+ def test_curated_strings_never_self_flag(self):
+ """The wrong-language detector must accept every curated string in its
+ own language — a reference that self-flags hard-zeros its sample (a
+ French reference did, via 'de/et' hitting other languages' sets)."""
+ from transformer_lens.benchmarks.text_quality import _wrong_language
+ from transformer_lens.benchmarks.text_quality_profiles import (
+ CHAT_PROMPTS,
+ CONTINUATION_PROMPTS,
+ PIVOT_SENTENCES,
+ )
+
+ offenders = []
+ for table in (CONTINUATION_PROMPTS, CHAT_PROMPTS):
+ for lang, prompts in table.items():
+ for pp in prompts:
+ for text in (pp.prompt, pp.reference):
+ if _wrong_language(text, lang):
+ offenders.append((lang, text[:50]))
+ for lang, sents in PIVOT_SENTENCES.items():
+ for s in sents:
+ if _wrong_language(s, lang):
+ offenders.append((lang, s[:50]))
+ assert offenders == []
+
+ def test_cjk_loop_penalized_despite_space(self):
+ """A single space in a degenerate CJK loop restored the inert word
+ path (penalty 1.0 vs 0.053); char mode must key off CJK content."""
+ from transformer_lens.benchmarks.text_quality import _compute_repetition_penalty
+
+ assert _compute_repetition_penalty("的的的的的的的的的 的的的的的的的的的的") <= 0.3
+ assert _compute_repetition_penalty("我该去睡觉了,因为明天有一个很重要的会议要参加。") > 0.5
+
+ def test_registry_floor_equals_pass_line(self):
+ """[floor, pass) previously got passed=False with a clean note; both
+ numbers must come from the same constant."""
+ from transformer_lens.benchmarks.text_quality_profiles import p4_pass_threshold
+ from transformer_lens.tools.model_registry.verify_models import (
+ _MIN_PHASE_SCORES,
+ )
+
+ assert _MIN_PHASE_SCORES[4] == p4_pass_threshold()
+
+ def test_judge_cannot_self_score(self):
+ """Ratio scoring against the judge's own perplexity is self-grading."""
+ from transformer_lens.benchmarks.text_quality import JUDGE_MODEL_ID
+
+ result = _run(FakeBridge(), profile="continuation", model_name=JUDGE_MODEL_ID)
+ assert result.severity == BenchmarkSeverity.SKIPPED
+ assert result.message.startswith("P4 skipped:")
+
+ def test_chat_judged_with_prompt_context(self):
+ """A fluent off-topic stub scores ~98 when chat output is judged
+ standalone; conditioning on the user prompt is the relevance signal."""
+ from transformer_lens.benchmarks.text_quality_profiles import (
+ JUDGE_CONTEXT_KINDS,
+ )
+
+ assert "chat" in JUDGE_CONTEXT_KINDS
+ assert "task:instruction" in JUDGE_CONTEXT_KINDS
+ # Unconditioned summarization scored hallucinated summaries 100 (the
+ # judge never saw the article); unconditioned denoise rated broken
+ # restorations more fluent than the reference.
+ assert "task:summarization" in JUDGE_CONTEXT_KINDS
+ assert "task:denoise" in JUDGE_CONTEXT_KINDS
+ bridge = FakeBridge()
+ bridge.tokenizer.chat_template = "{{messages}}"
+ _run(bridge, profile="chat")
+ contexts = [c for c, _t in bridge.judge_calls]
+ assert any(c for c in contexts), "judge never saw the user prompt as context"
+
+ def test_p1_only_note_labels_skip_as_coverage_gap(self):
+ """A skipped P4 is a coverage gap; the stale score must not be
+ relabeled 'text quality poor'."""
+ from transformer_lens.tools.model_registry.verify_models import (
+ _p1_only_core_note,
+ )
+
+ skipped = BenchmarkResult(
+ name="text_quality",
+ severity=BenchmarkSeverity.SKIPPED,
+ message="P4 skipped: no prompts for profile 'continuation@xx' — file an issue",
+ )
+ skipped.phase = 4
+ note = _p1_only_core_note(None, [skipped])
+ assert "P4 skipped" in note and "poor" not in note
+ assert "poor (P4=40.0)" in _p1_only_core_note(40.0, [])
+ assert "errored" in _p1_only_core_note(None, [])
+
+ def test_arch_rule_keeps_stored_language(self):
+ """The arch rule fixes the kind; a scraped @fr of the same kind must
+ survive resolve->writeback or curation can never stick."""
+ from transformer_lens.benchmarks.text_quality_profiles import resolve_profile
+
+ spec = resolve_profile(
+ "some/pegasus-clone",
+ "PegasusForConditionalGeneration",
+ registry_profile="task:summarization@fr",
+ )
+ assert str(spec) == "task:summarization@fr"
+ spec = resolve_profile(
+ "some/pegasus-clone",
+ "PegasusForConditionalGeneration",
+ registry_profile="continuation@fr",
+ )
+ assert str(spec) == "task:summarization"
+
+
+class TestForcedBosVocabCollision:
+ """Bare ISO codes collide with ordinary subwords (T5's 'de' id 221,
+ Marian's 'en' id 39) and were injected as forced decoder tokens,
+ corrupting every translator without a real lang-code system."""
+
+ def test_plain_vocab_word_is_not_a_lang_code(self):
+ from transformer_lens.benchmarks.text_quality import _forced_bos_for_target
+
+ class PlainSeq2SeqTokenizer:
+ unk_token_id = 3
+
+ def convert_tokens_to_ids(self, tok):
+ return {"de": 221, "en": 39}.get(tok, 3)
+
+ assert _forced_bos_for_target(PlainSeq2SeqTokenizer(), "de") is None
+
+ def test_nllb_style_code_still_resolves(self):
+ from transformer_lens.benchmarks.text_quality import _forced_bos_for_target
+
+ class NllbLikeTokenizer:
+ unk_token_id = 3
+
+ def convert_tokens_to_ids(self, tok):
+ return {"deu_Latn": 256042}.get(tok, 3)
+
+ assert _forced_bos_for_target(NllbLikeTokenizer(), "de") == 256042
+
+
+class TestAllGenerationsCaptured:
+ def test_details_carry_every_prompts_generation(self):
+ """Only the first prompt's output was stored; the registry-wide
+ review needs every generation inspectable."""
+ bridge = FakeBridge()
+ result = _run(bridge, profile="continuation")
+ texts = result.details["generated_texts"]
+ assert len(texts) == result.details["num_prompts"]
+ assert all(isinstance(t, str) and t for t in texts)
diff --git a/tests/unit/benchmarks/test_text_quality_seq2seq.py b/tests/unit/benchmarks/test_text_quality_seq2seq.py
deleted file mode 100644
index 161fa0de46..0000000000
--- a/tests/unit/benchmarks/test_text_quality_seq2seq.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""Encoder-decoder text-quality scoring must score the full decoder output.
-
-Seq2seq models (Marian/T5/BART) emit a standalone output, not a continuation
-of the prompt. Scoring it as a continuation subtracts the prompt length and
-trips the "continuation too short (< 2 tokens)" guard for every prompt when the
-output is ~ the prompt length (Marian nl->en on an English prompt), scoring 0.
-The fix scores the whole generated sequence for encoder-decoder models.
-"""
-
-import pytest
-
-pytest.importorskip("transformers")
-
-
-def test_marian_text_quality_scores_full_output():
- from transformer_lens.benchmarks.text_quality import benchmark_text_quality
- from transformer_lens.model_bridge import TransformerBridge
-
- try:
- bridge = TransformerBridge.boot_transformers("Helsinki-NLP/opus-mt-nl-en", device="cpu")
- except (OSError, ConnectionError, TimeoutError) as exc:
- pytest.skip(f"marian unavailable offline: {exc}")
-
- assert bridge.original_model.config.is_encoder_decoder # precondition
-
- result = benchmark_text_quality(
- bridge, "Natural language processing is", max_new_tokens=20, device="cpu"
- )
- # Pre-fix this returned "Scoring failed for all prompts" (score absent).
- assert result.details is not None, result.message
- assert "score" in result.details, result.message
- assert result.details["score"] > 0
diff --git a/tests/unit/model_bridge/compatibility/test_use_attn_result.py b/tests/unit/model_bridge/compatibility/test_use_attn_result.py
index 521a5a7bfc..86de8e0168 100644
--- a/tests/unit/model_bridge/compatibility/test_use_attn_result.py
+++ b/tests/unit/model_bridge/compatibility/test_use_attn_result.py
@@ -70,21 +70,19 @@ def _hook(tensor, hook):
def test_hook_result_does_not_fire_when_flag_off(gpt2_bridge):
- """When `use_attn_result=False` the per-head einsum path is skipped, so
- `hook_result` must NOT fire (no activation captured)."""
+ """When `use_attn_result=False`, explicitly adding hook_result should fail
+ with a clear error explaining how to enable the gated hook."""
x = torch.arange(1, 9).unsqueeze(0)
assert gpt2_bridge.cfg.use_attn_result is False
- fired = {"result": False}
def _hook(tensor, hook):
- fired["result"] = True
return tensor
- gpt2_bridge.run_with_hooks(x, fwd_hooks=[("blocks.0.attn.hook_result", _hook)])
- assert fired["result"] is False, (
- "hook_result fired when use_attn_result was False; the flag is "
- "supposed to skip the per-head computation."
- )
+ with pytest.raises(ValueError, match="set_use_attn_result"):
+ gpt2_bridge.run_with_hooks(
+ x,
+ fwd_hooks=[("blocks.0.attn.hook_result", _hook)],
+ )
def test_use_attn_result_applicability_raises_on_unsupported(monkeypatch, gpt2_bridge):
diff --git a/tests/unit/model_bridge/generalized_components/test_base.py b/tests/unit/model_bridge/generalized_components/test_base.py
index df3c9a1529..23754c5dd8 100644
--- a/tests/unit/model_bridge/generalized_components/test_base.py
+++ b/tests/unit/model_bridge/generalized_components/test_base.py
@@ -5,9 +5,15 @@
import torch.nn as nn
from transformer_lens.hook_points import HookPoint
+from transformer_lens.model_bridge.generalized_components.attention import (
+ AttentionBridge,
+)
from transformer_lens.model_bridge.generalized_components.base import (
GeneralizedComponent,
)
+from transformer_lens.model_bridge.generalized_components.unembedding import (
+ UnembeddingBridge,
+)
class MockOriginalComponent(nn.Module):
@@ -377,5 +383,80 @@ def test_complex_object_attributes(self):
assert component.complex_attr["nested"]["deep"] == [1, 2, 3]
+@pytest.mark.parametrize("use_keyword", [False, True])
+def test_forward_preserves_input_dtype(use_keyword: bool):
+ """Parameter storage dtype must not determine the component compute dtype."""
+
+ class MixedPrecisionComponent(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.auxiliary = nn.Parameter(torch.ones((), dtype=torch.float32))
+ self.received_dtype = None
+
+ def forward(self, hidden_states):
+ self.received_dtype = hidden_states.dtype
+ return hidden_states.clone()
+
+ original = MixedPrecisionComponent()
+ component = MockGeneralizedComponent("mixed_precision")
+ component.set_original_component(original)
+ inputs = torch.ones(2, 3, dtype=torch.bfloat16)
+
+ output = component(hidden_states=inputs) if use_keyword else component(inputs)
+
+ assert original.received_dtype == torch.bfloat16
+ assert output.dtype == torch.bfloat16
+ torch.testing.assert_close(output, inputs)
+
+
+@pytest.mark.parametrize("input_name", ["positional", "hidden_states", "query_input"])
+def test_attention_forward_preserves_input_dtype(input_name: str):
+ class MixedPrecisionAttention(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.auxiliary = nn.Parameter(torch.ones((), dtype=torch.float32))
+ self.received_dtype = None
+
+ def forward(self, hidden_states=None, query_input=None):
+ value = query_input if query_input is not None else hidden_states
+ self.received_dtype = value.dtype
+ return value
+
+ original = MixedPrecisionAttention()
+ bridge = AttentionBridge(name="attention", config=None)
+ bridge.set_original_component(original)
+ inputs = torch.ones(2, 3, dtype=torch.bfloat16)
+
+ if input_name == "positional":
+ output = bridge(inputs)
+ else:
+ output = bridge(**{input_name: inputs})
+
+ assert original.received_dtype == torch.bfloat16
+ assert output.dtype == torch.bfloat16
+
+
+def test_unembedding_forward_preserves_input_dtype():
+ class MixedPrecisionUnembedding(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.auxiliary = nn.Parameter(torch.ones((), dtype=torch.float32))
+ self.received_dtype = None
+
+ def forward(self, hidden_states):
+ self.received_dtype = hidden_states.dtype
+ return hidden_states
+
+ original = MixedPrecisionUnembedding()
+ bridge = UnembeddingBridge(name="unembed")
+ bridge.set_original_component(original)
+ inputs = torch.ones(2, 3, dtype=torch.bfloat16)
+
+ output = bridge(inputs)
+
+ assert original.received_dtype == torch.bfloat16
+ assert output.dtype == torch.bfloat16
+
+
if __name__ == "__main__":
pytest.main([__file__])
diff --git a/tests/unit/model_bridge/generalized_components/test_moe_bridge_tuple_output.py b/tests/unit/model_bridge/generalized_components/test_moe_bridge_tuple_output.py
index 3b644497bc..c321c9ec4a 100644
--- a/tests/unit/model_bridge/generalized_components/test_moe_bridge_tuple_output.py
+++ b/tests/unit/model_bridge/generalized_components/test_moe_bridge_tuple_output.py
@@ -39,6 +39,23 @@ def _bridge_with_stub(fake_forward) -> MoEBridge:
class TestMoEBridgeTupleOutput:
+ @pytest.mark.parametrize("use_keyword", [False, True])
+ def test_preserves_input_dtype(self, use_keyword: bool) -> None:
+ received_dtype = None
+
+ def fake_forward(hidden_states):
+ nonlocal received_dtype
+ received_dtype = hidden_states.dtype
+ return hidden_states
+
+ bridge = _bridge_with_stub(fake_forward)
+ hidden_states = torch.ones(1, 3, 4, dtype=torch.bfloat16)
+
+ output = bridge(hidden_states=hidden_states) if use_keyword else bridge(hidden_states)
+
+ assert received_dtype == torch.bfloat16
+ assert output.dtype == torch.bfloat16
+
def test_empty_tuple_raises_clear_type_error(self) -> None:
bridge = _bridge_with_stub(lambda *a, **kw: ())
with pytest.raises(TypeError, match="torch.Tensor"):
diff --git a/tests/unit/model_bridge/sources/__init__.py b/tests/unit/model_bridge/sources/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/unit/model_bridge/sources/test_native_init_semantics.py b/tests/unit/model_bridge/sources/test_native_init_semantics.py
new file mode 100644
index 0000000000..69ca1581bf
--- /dev/null
+++ b/tests/unit/model_bridge/sources/test_native_init_semantics.py
@@ -0,0 +1,76 @@
+"""Native init: seeded reproducibility across device/dtype, and initializer_range gain."""
+
+import pytest
+import torch
+
+from transformer_lens.config import TransformerBridgeConfig
+from transformer_lens.model_bridge import TransformerBridge
+from transformer_lens.model_bridge.sources.native.init import initialize_native_model
+
+
+def _cfg(**overrides):
+ base = dict(
+ n_layers=2,
+ d_model=64,
+ d_head=16,
+ n_heads=4,
+ d_mlp=128,
+ d_vocab=100,
+ n_ctx=16,
+ act_fn="gelu",
+ seed=0,
+ )
+ base.update(overrides)
+ return TransformerBridgeConfig(**base)
+
+
+def test_seeded_reinit_after_dtype_cast_reproduces_boot_weights():
+ """Same seed must give the same weights whether init runs before or after
+ the .to(dtype) cast (boot inits first; init_weights() runs after)."""
+ cfg = _cfg()
+ boot_then_cast = TransformerBridge.boot_native(cfg).to(torch.float16)
+
+ recast = TransformerBridge.boot_native(cfg).to(torch.float16)
+ native = recast.original_model
+ initialize_native_model(native, cfg) # re-init AFTER the cast
+
+ for (name, a), (_, b) in zip(
+ boot_then_cast.original_model.named_parameters(), native.named_parameters()
+ ):
+ assert torch.equal(a, b), f"{name} diverged between init-before and init-after cast"
+
+
+def test_seeded_init_is_deterministic():
+ cfg = _cfg()
+ a = TransformerBridge.boot_native(cfg)
+ b = TransformerBridge.boot_native(cfg)
+ for (name, pa), (_, pb) in zip(
+ a.original_model.named_parameters(), b.original_model.named_parameters()
+ ):
+ assert torch.equal(pa, pb), name
+
+
+@pytest.mark.parametrize("mode", ["xavier_normal", "kaiming_normal"])
+def test_initializer_range_scales_non_gpt2_modes(mode):
+ """An explicit initializer_range acts as the gain (legacy semantics)."""
+ plain = TransformerBridge.boot_native(_cfg(init_mode=mode))
+ scaled = TransformerBridge.boot_native(_cfg(init_mode=mode, initializer_range=0.5))
+
+ w_plain = plain.original_model.layers[0].attn.q.weight
+ w_scaled = scaled.original_model.layers[0].attn.q.weight
+ ratio = (w_scaled.std() / w_plain.std()).item()
+ assert ratio == pytest.approx(0.5, rel=0.05), f"gain not applied: ratio {ratio:.4f}"
+
+
+def test_gpt2_mode_default_std_matches_legacy_formula():
+ """Unset initializer_range must give N(0, 0.64/d_model) — std 0.8/sqrt(d_model).
+
+ The legacy scheme, not GPT-2's paper 0.02: toy-model training dynamics
+ (the grokking demo memorizes vs. stalls) depend on this scale.
+ """
+ import math
+
+ cfg = _cfg(d_model=128, d_head=32, d_mlp=512, d_vocab=114)
+ bridge = TransformerBridge.boot_native(cfg)
+ std = bridge.original_model.layers[0].attn.q.weight.std().item()
+ assert std == pytest.approx(0.8 / math.sqrt(128), rel=0.05)
diff --git a/tests/unit/model_bridge/supported_architectures/test_adapter_capability_guards.py b/tests/unit/model_bridge/supported_architectures/test_adapter_capability_guards.py
new file mode 100644
index 0000000000..8c44ae39b1
--- /dev/null
+++ b/tests/unit/model_bridge/supported_architectures/test_adapter_capability_guards.py
@@ -0,0 +1,76 @@
+"""Capability flags must reach their consuming bridge code paths.
+
+Exercises the real forward/loss guard and generation-caching resolver with the
+real adapter classes on a bare bridge (full construction needs remote-code
+models these unit tests cannot load).
+"""
+
+from __future__ import annotations
+
+from types import SimpleNamespace
+
+import pytest
+import torch.nn as nn
+
+from tests.unit.model_bridge.supported_architectures.helpers import make_bridge_cfg
+from transformer_lens.model_bridge.bridge import TransformerBridge
+from transformer_lens.model_bridge.supported_architectures.dream import (
+ DreamArchitectureAdapter,
+)
+from transformer_lens.model_bridge.supported_architectures.gidd import (
+ GiddArchitectureAdapter,
+)
+
+
+class _StubModel(nn.Module):
+ """original_model is beartype-hinted nn.Module; carry only .config."""
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.config = SimpleNamespace(is_encoder_decoder=False)
+
+
+def _bare_bridge(adapter) -> TransformerBridge:
+ bridge = object.__new__(TransformerBridge)
+ nn.Module.__init__(bridge)
+ bridge.adapter = adapter
+ bridge.cfg = adapter.cfg
+ bridge.__dict__["original_model"] = _StubModel()
+ return bridge
+
+
+class TestDiffusionLossGuard:
+ """Diffusion LMs must refuse the shifted causal loss instead of silently
+ computing it (bridge.forward's supports_causal_loss guard)."""
+
+ def test_dream_loss_raises(self) -> None:
+ cfg = make_bridge_cfg("DreamModel", n_key_value_heads=4)
+ bridge = _bare_bridge(DreamArchitectureAdapter(cfg))
+ with pytest.raises(NotImplementedError, match="shifted causal"):
+ bridge.forward("hi", return_type="loss")
+
+ def test_gidd_both_raises(self) -> None:
+ cfg = make_bridge_cfg("GiddForDiffusionLM")
+ bridge = _bare_bridge(GiddArchitectureAdapter(cfg))
+ with pytest.raises(NotImplementedError, match="shifted causal"):
+ bridge.forward("hi", return_type="both")
+
+
+class TestRavenGenerationCaching:
+ """Huginn's depth recurrence cannot use HF past_key_values stepping; the
+ resolver must refuse the cache and reject batched generation."""
+
+ def _bridge(self) -> TransformerBridge:
+ from transformer_lens.model_bridge.supported_architectures.raven import (
+ RavenArchitectureAdapter,
+ )
+
+ cfg = make_bridge_cfg("RavenForCausalLM")
+ return _bare_bridge(RavenArchitectureAdapter(cfg))
+
+ def test_kv_cache_refused(self) -> None:
+ assert self._bridge()._resolve_generation_caching(True, batched=False) is False
+
+ def test_batched_generation_rejected(self) -> None:
+ with pytest.raises(NotImplementedError):
+ self._bridge()._resolve_generation_caching(True, batched=True)
diff --git a/tests/unit/model_bridge/supported_architectures/test_ast_adapter.py b/tests/unit/model_bridge/supported_architectures/test_ast_adapter.py
index 2bb395ea95..a2fe7f910e 100644
--- a/tests/unit/model_bridge/supported_architectures/test_ast_adapter.py
+++ b/tests/unit/model_bridge/supported_architectures/test_ast_adapter.py
@@ -68,6 +68,7 @@ def test_classification_prefix_rebinding(self, hf_config, tl_config):
adapter.prepare_model(model)
assert adapter.component_mapping["blocks"].name == "audio_spectrogram_transformer.layers"
+ assert adapter.component_mapping["classifier_ln"].name == "classifier.layernorm"
assert adapter.component_mapping["unembed"].name == "classifier.dense"
assert adapter.cfg.d_vocab_out == 2
diff --git a/tests/unit/model_bridge/supported_architectures/test_bert_adapter.py b/tests/unit/model_bridge/supported_architectures/test_bert_adapter.py
index a5d8e5ae31..60a966aec2 100644
--- a/tests/unit/model_bridge/supported_architectures/test_bert_adapter.py
+++ b/tests/unit/model_bridge/supported_architectures/test_bert_adapter.py
@@ -8,9 +8,10 @@
- Anti-drift config flags
"""
+from types import SimpleNamespace
+
import pytest
-from transformer_lens.config import TransformerBridgeConfig
from transformer_lens.config.transformer_bridge_config import TransformerBridgeConfig
from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion
from transformer_lens.conversion_utils.param_processing_conversion import (
@@ -74,8 +75,11 @@ class TestBertComponentMapping:
def test_top_level_keys(self, adapter: BertArchitectureAdapter) -> None:
assert set(adapter.component_mapping.keys()) == {
"embed",
+ "token_type_embed",
"pos_embed",
+ "embed_ln",
"blocks",
+ "mlm_head",
"ln_final",
"unembed",
}
@@ -83,19 +87,55 @@ def test_top_level_keys(self, adapter: BertArchitectureAdapter) -> None:
def test_bridge_types(self, adapter: BertArchitectureAdapter) -> None:
mapping = adapter.component_mapping
assert isinstance(mapping["embed"], EmbeddingBridge)
+ assert isinstance(mapping["token_type_embed"], EmbeddingBridge)
assert isinstance(mapping["pos_embed"], PosEmbedBridge)
+ assert isinstance(mapping["embed_ln"], NormalizationBridge)
assert isinstance(mapping["blocks"], BlockBridge)
+ assert isinstance(mapping["mlm_head"], LinearBridge)
assert isinstance(mapping["ln_final"], NormalizationBridge)
assert isinstance(mapping["unembed"], UnembeddingBridge)
def test_top_level_hf_paths(self, adapter: BertArchitectureAdapter) -> None:
mapping = adapter.component_mapping
assert mapping["embed"].name == "bert.embeddings.word_embeddings"
+ assert mapping["token_type_embed"].name == "bert.embeddings.token_type_embeddings"
assert mapping["pos_embed"].name == "bert.embeddings.position_embeddings"
+ assert mapping["embed_ln"].name == "bert.embeddings.LayerNorm"
assert mapping["blocks"].name == "bert.encoder.layer"
+ assert mapping["mlm_head"].name == "cls.predictions.transform.dense"
assert mapping["ln_final"].name == "cls.predictions.transform.LayerNorm"
assert mapping["unembed"].name == "cls.predictions.decoder"
+ def test_token_type_embedding_is_cached_with_hf_output(self) -> None:
+ import torch
+ from transformers import BertForMaskedLM
+
+ from transformer_lens.model_bridge.sources import build_bridge_from_module
+
+ hf_model = BertForMaskedLM.from_pretrained("bert-base-cased").eval()
+ input_ids = torch.tensor([[101, 7592, 102, 2088, 102]])
+ token_type_ids = torch.tensor([[0, 0, 0, 1, 1]])
+ with torch.no_grad():
+ expected = hf_model.bert.embeddings.token_type_embeddings(token_type_ids).clone()
+
+ bridge = build_bridge_from_module(
+ hf_model,
+ "BertForMaskedLM",
+ hf_config=hf_model.config,
+ dtype=torch.float32,
+ device="cpu",
+ model_name="bert-base-cased",
+ )
+ with torch.no_grad():
+ _, cache = bridge.run_with_cache(
+ input_ids,
+ token_type_ids=token_type_ids,
+ names_filter=["token_type_embed.hook_out"],
+ )
+
+ assert "token_type_embed.hook_out" in cache
+ torch.testing.assert_close(cache["token_type_embed.hook_out"], expected)
+
def test_block_submodule_keys(self, adapter: BertArchitectureAdapter) -> None:
assert set(adapter.component_mapping["blocks"].submodules.keys()) == {
"ln1",
@@ -140,6 +180,36 @@ def test_mlp_submodule_hf_paths(self, adapter: BertArchitectureAdapter) -> None:
assert mlp.submodules["out"].name == "output.dense"
+class TestBertTaskHeadMappings:
+ def test_nsp_only_model_uses_hooked_encoder_names(self) -> None:
+ adapter = BertArchitectureAdapter(_make_cfg())
+ hf_model = SimpleNamespace(
+ bert=SimpleNamespace(pooler=object()),
+ cls=SimpleNamespace(seq_relationship=object()),
+ )
+
+ adapter.prepare_model(hf_model)
+
+ assert adapter.components["pooler"].name == "bert.pooler.dense"
+ assert adapter.components["unembed"].name == "cls.seq_relationship"
+ assert "mlm_head" not in adapter.components
+ assert "ln_final" not in adapter.components
+
+ def test_combined_mlm_nsp_model_registers_both_heads(self) -> None:
+ adapter = BertArchitectureAdapter(_make_cfg())
+ hf_model = SimpleNamespace(
+ bert=SimpleNamespace(pooler=object()),
+ cls=SimpleNamespace(predictions=object(), seq_relationship=object()),
+ )
+
+ adapter.prepare_model(hf_model)
+
+ assert adapter.components["pooler"].name == "bert.pooler.dense"
+ assert adapter.components["mlm_head"].name == "cls.predictions.transform.dense"
+ assert adapter.components["nsp_head"].name == "cls.seq_relationship"
+ assert adapter.components["unembed"].name == "cls.predictions.decoder"
+
+
# ---------------------------------------------------------------------------
# Anti-drift config flags
# ---------------------------------------------------------------------------
diff --git a/tests/unit/model_bridge/supported_architectures/test_olmo_adapter.py b/tests/unit/model_bridge/supported_architectures/test_olmo_adapter.py
index ab0f5d04d7..201d2381d7 100644
--- a/tests/unit/model_bridge/supported_architectures/test_olmo_adapter.py
+++ b/tests/unit/model_bridge/supported_architectures/test_olmo_adapter.py
@@ -100,6 +100,14 @@ def __init__(self, d_model: int, n_heads: int, n_kv_heads: int) -> None:
self.o_proj = nn.Linear(n_heads * head_dim, d_model, bias=False)
+class _RecordingLinear(nn.Linear):
+ """Record the bridge input dtype while handling conversion internally."""
+
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
+ self.received_dtype = input.dtype
+ return super().forward(input.to(self.weight.dtype))
+
+
def _wire_attention_bridge(
adapter: OlmoArchitectureAdapter,
cfg: TransformerBridgeConfig,
@@ -297,6 +305,32 @@ def _hook(tensor: torch.Tensor, hook: Any) -> None:
assert seen["v"] == torch.Size([batch, seq_len, cfg.n_key_value_heads, cfg.d_head])
assert seen["z"] == torch.Size([batch, seq_len, cfg.n_heads, cfg.d_head])
+ def test_forward_preserves_input_dtype_at_projection_boundary(
+ self, adapter: OlmoArchitectureAdapter, cfg: TransformerBridgeConfig
+ ) -> None:
+ attn_bridge = _wire_attention_bridge(adapter, cfg)
+ recordings = []
+ for name in ("q", "k", "v"):
+ projection = getattr(attn_bridge, name)
+ original = projection.original_component
+ assert isinstance(original, nn.Linear)
+ recording = _RecordingLinear(original.in_features, original.out_features, bias=False)
+ recording.load_state_dict(original.state_dict())
+ projection.set_original_component(recording)
+ recordings.append(recording)
+
+ hidden_states = torch.randn(1, 3, cfg.d_model, dtype=torch.bfloat16)
+ position_embeddings = identity_rope(3, cfg.d_head)
+
+ attn_bridge(
+ hidden_states=hidden_states,
+ position_embeddings=position_embeddings,
+ attention_mask=None,
+ )
+
+ for projection in recordings:
+ assert projection.received_dtype == torch.bfloat16
+
class TestOlmoClipQkv:
"""The reconstructed forward must clamp Q/K/V when config.clip_qkv is set."""
diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py
index cdf99ae1a3..fdf7541279 100644
--- a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py
+++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py
@@ -279,6 +279,9 @@ def _bare_model(self) -> object:
"""No 'vit'/'deit'/'classifier' attribute — mimics bare ViTModel/DeiTModel."""
return SimpleNamespace()
+ def _bare_model_with_pooler(self) -> object:
+ return SimpleNamespace(pooler=SimpleNamespace(dense=SimpleNamespace()))
+
def _vit_for_classification(self) -> object:
return SimpleNamespace(vit=SimpleNamespace(), classifier=SimpleNamespace())
@@ -305,6 +308,12 @@ def test_bare_model_has_no_unembed(self, adapter: ViTArchitectureAdapter) -> Non
adapter.prepare_model(self._bare_model())
assert "unembed" not in adapter.component_mapping
+ def test_bare_model_maps_pooler_without_root_name_collision(
+ self, adapter: ViTArchitectureAdapter
+ ) -> None:
+ adapter.prepare_model(self._bare_model_with_pooler())
+ assert adapter.component_mapping["pooler"].name == "pooler.dense"
+
def test_bare_model_does_not_require_encoder_attribute(
self, adapter: ViTArchitectureAdapter
) -> None:
diff --git a/tests/unit/model_bridge/test_batched_string_padding.py b/tests/unit/model_bridge/test_batched_string_padding.py
new file mode 100644
index 0000000000..4345d18717
--- /dev/null
+++ b/tests/unit/model_bridge/test_batched_string_padding.py
@@ -0,0 +1,131 @@
+"""Padding behavior for ragged string lists passed to TransformerBridge."""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from typing import Any
+
+import pytest
+import torch
+
+from transformer_lens.utilities import get_attention_mask
+
+PROMPTS = ["The quick brown fox", "Hello there, world! This is a longer sentence."]
+RESID_PRE = "blocks.0.hook_resid_pre"
+
+
+@pytest.fixture(scope="module")
+def compatibility_bridge():
+ """A cached model used for token-layout and real-token numerical checks."""
+ from transformer_lens.model_bridge import TransformerBridge
+
+ bridge = TransformerBridge.boot_transformers("distilgpt2", device="cpu", dtype=torch.float32)
+ bridge.enable_compatibility_mode()
+ bridge.eval()
+ return bridge
+
+
+def test_to_tokens_honors_explicit_padding_side(compatibility_bridge) -> None:
+ """A per-call padding override must win without mutating the shared tokenizer."""
+ tokenizer = compatibility_bridge.tokenizer
+ original_side = tokenizer.padding_side
+ tokenizer.padding_side = "right"
+ try:
+ left = compatibility_bridge.to_tokens(PROMPTS, padding_side="left")
+ right = compatibility_bridge.to_tokens(PROMPTS, padding_side="right")
+
+ assert not torch.equal(left, right)
+ assert left[0, 0].item() == tokenizer.pad_token_id
+ assert left[0, -1].item() != tokenizer.pad_token_id
+ assert right[0, -1].item() == tokenizer.pad_token_id
+ assert tokenizer.padding_side == "right"
+ finally:
+ tokenizer.padding_side = original_side
+
+
+def test_default_right_padded_text_matches_pretokenized_logits(compatibility_bridge) -> None:
+ """Default text forwarding must match the public right-padded token layout."""
+ tokenizer = compatibility_bridge.tokenizer
+ original_side = tokenizer.padding_side
+ tokenizer.padding_side = "right"
+ try:
+ tokens = compatibility_bridge.to_tokens(PROMPTS)
+ with torch.no_grad():
+ text_logits = compatibility_bridge(PROMPTS)
+ token_logits = compatibility_bridge(tokens)
+
+ torch.testing.assert_close(text_logits, token_logits, rtol=0, atol=0)
+ finally:
+ tokenizer.padding_side = original_side
+
+
+@pytest.mark.parametrize("padding_side", ["left", "right"])
+def test_ragged_forward_matches_public_layout_and_single_sequences(
+ compatibility_bridge, monkeypatch, padding_side: str
+) -> None:
+ """String-list forward uses public token layout and preserves real-token values."""
+ tokenizer = compatibility_bridge.tokenizer
+ original_side = tokenizer.padding_side
+ tokenizer.padding_side = padding_side
+ try:
+ expected_tokens = compatibility_bridge.to_tokens(PROMPTS)
+ attention_mask = get_attention_mask(tokenizer, expected_tokens, prepend_bos=True).bool()
+ tokenization_calls: list[torch.Tensor] = []
+ original_to_tokens: Callable[..., torch.Tensor] = compatibility_bridge.to_tokens
+
+ def recording_to_tokens(input: Any, *args: Any, **kwargs: Any) -> torch.Tensor:
+ tokens = original_to_tokens(input, *args, **kwargs)
+ if isinstance(input, list):
+ tokenization_calls.append(tokens.detach().clone())
+ return tokens
+
+ monkeypatch.setattr(compatibility_bridge, "to_tokens", recording_to_tokens)
+
+ with torch.no_grad():
+ batch_logits, batch_cache = compatibility_bridge.run_with_cache(
+ PROMPTS, names_filter=[RESID_PRE]
+ )
+ single_results = [
+ compatibility_bridge.run_with_cache(prompt, names_filter=[RESID_PRE])
+ for prompt in PROMPTS
+ ]
+
+ assert len(tokenization_calls) == 1
+ assert torch.equal(tokenization_calls[0], expected_tokens)
+
+ for index, (single_logits, single_cache) in enumerate(single_results):
+ real_tokens = attention_mask[index]
+ torch.testing.assert_close(
+ batch_logits[index, real_tokens], single_logits[0], rtol=1e-5, atol=1e-4
+ )
+ torch.testing.assert_close(
+ batch_cache[RESID_PRE][index, real_tokens],
+ single_cache[RESID_PRE][0],
+ rtol=1e-5,
+ atol=1e-4,
+ )
+ finally:
+ tokenizer.padding_side = original_side
+
+
+def test_generate_keeps_left_padding_for_ragged_strings(compatibility_bridge) -> None:
+ """Generation keeps real tokens flush-right even when the tokenizer defaults right."""
+ tokenizer = compatibility_bridge.tokenizer
+ original_side = tokenizer.padding_side
+ try:
+ tokenizer.padding_side = "left"
+ expected_tokens = compatibility_bridge.to_tokens(PROMPTS)
+ tokenizer.padding_side = "right"
+
+ _, input_tokens = compatibility_bridge.generate(
+ PROMPTS,
+ max_new_tokens=1,
+ do_sample=False,
+ verbose=False,
+ return_input_tokens=True,
+ )
+
+ assert torch.equal(input_tokens, expected_tokens)
+ assert tokenizer.padding_side == "right"
+ finally:
+ tokenizer.padding_side = original_side
diff --git a/tests/unit/model_bridge/test_boot_native.py b/tests/unit/model_bridge/test_boot_native.py
index 1e11891f64..2f0069069f 100644
--- a/tests/unit/model_bridge/test_boot_native.py
+++ b/tests/unit/model_bridge/test_boot_native.py
@@ -1,12 +1,17 @@
"""Tests for ``TransformerBridge.boot_native`` classmethod."""
+
from __future__ import annotations
import sys
+import pytest
import torch
+import torch.nn as nn
from transformer_lens.config import TransformerBridgeConfig
from transformer_lens.model_bridge import TransformerBridge
+from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter
+from transformer_lens.model_bridge.generalized_components import LinearBridge
from transformer_lens.model_bridge.sources.native import NativeModel
@@ -71,6 +76,51 @@ def test_boot_native_returns_bridge_over_native_model():
assert isinstance(bridge.original_model, NativeModel)
+@pytest.mark.parametrize("stop_at_layer", [0, 2, -1])
+def test_boot_native_direct_stop_matches_cached_stop(stop_at_layer: int):
+ bridge = TransformerBridge.boot_native(_cfg(n_layers=3))
+ bridge.eval()
+ tokens = torch.tensor([[1, 2, 3]])
+
+ with torch.no_grad():
+ expected, _ = bridge.run_with_cache(tokens, stop_at_layer=stop_at_layer)
+ actual = bridge(tokens, stop_at_layer=stop_at_layer)
+
+ assert actual.shape == (1, 3, bridge.cfg.d_model)
+ torch.testing.assert_close(actual, expected)
+
+
+def test_native_state_dict_round_trip_restores_parameters():
+ bridge = TransformerBridge.boot_native(_cfg())
+
+ saved_state_dict = {key: value.detach().clone() for key, value in bridge.state_dict().items()}
+ original_parameters = {
+ name: parameter.detach().clone() for name, parameter in bridge.named_parameters()
+ }
+
+ with torch.no_grad():
+ for parameter in bridge.parameters():
+ parameter.zero_()
+
+ result = bridge.load_state_dict(saved_state_dict, strict=True)
+
+ assert result.missing_keys == []
+ assert result.unexpected_keys == []
+
+ for name, parameter in bridge.named_parameters():
+ torch.testing.assert_close(parameter, original_parameters[name])
+
+
+def test_native_state_dict_strict_rejects_unexpected_keys():
+ bridge = TransformerBridge.boot_native(_cfg())
+
+ with pytest.raises(RuntimeError, match="Unexpected key"):
+ bridge.load_state_dict(
+ {"not.a.real.weight": torch.zeros(1)},
+ strict=True,
+ )
+
+
def test_boot_native_accepts_dict_config():
cfg_dict = dict(
d_model=32,
@@ -88,6 +138,30 @@ def test_boot_native_accepts_dict_config():
assert bridge.cfg.architecture == "TransformerLensNative"
+def test_boot_native_rejects_legacy_config_with_actionable_error():
+ import pytest
+
+ from transformer_lens import HookedTransformerConfig
+
+ legacy_config = HookedTransformerConfig(
+ n_layers=1,
+ d_model=32,
+ n_ctx=8,
+ d_head=16,
+ n_heads=2,
+ d_vocab=16,
+ act_fn="gelu",
+ )
+
+ with pytest.raises(
+ TypeError,
+ match=(
+ "boot_native expected a TransformerBridgeConfig or dict, " "got HookedTransformerConfig"
+ ),
+ ):
+ TransformerBridge.boot_native(legacy_config)
+
+
def test_boot_native_does_not_perturb_global_rng():
"""``boot_native(seed=...)`` must use a scoped torch.Generator instead of
``torch.manual_seed``. Otherwise a user calling boot_native then
@@ -127,6 +201,70 @@ def test_boot_native_distinct_seeds_diverge():
assert any(diffs), "Two different seeds produced identical params"
+def test_boot_native_skips_custom_init_when_disabled(monkeypatch):
+ def fail_if_called(*_args, **_kwargs):
+ pytest.fail("initialize_native_model was called with init_weights=False")
+
+ def fail_if_forked(*_args, **_kwargs):
+ pytest.fail("fork_rng was called with init_weights=False")
+
+ monkeypatch.setattr(
+ "transformer_lens.model_bridge.sources.native.initialize_native_model",
+ fail_if_called,
+ )
+ monkeypatch.setattr(torch.random, "fork_rng", fail_if_forked)
+ bridge = TransformerBridge.boot_native(_cfg(init_weights=False))
+
+ assert isinstance(bridge.original_model, NativeModel)
+ assert torch.count_nonzero(bridge.original_model.layers[0].attn.q.bias) > 0
+
+
+def test_native_bridge_init_weights_reinitializes_in_place_and_honors_seed():
+ bridge = TransformerBridge.boot_native(_cfg(seed=123))
+ model = bridge.original_model
+ expected = {name: param.detach().clone() for name, param in model.named_parameters()}
+
+ with torch.no_grad():
+ for param in model.parameters():
+ param.fill_(42)
+
+ bridge.init_weights()
+
+ assert bridge.original_model is model
+ for name, param in model.named_parameters():
+ assert torch.equal(param, expected[name]), f"Seed mismatch on {name}"
+
+
+def test_native_bridge_init_weights_does_not_perturb_global_rng():
+ bridge = TransformerBridge.boot_native(_cfg(seed=42))
+ torch.manual_seed(0)
+ expected_after = torch.randn(5)
+
+ torch.manual_seed(0)
+ bridge.init_weights()
+ actual_after = torch.randn(5)
+
+ assert torch.equal(actual_after, expected_after)
+
+
+def test_init_weights_rejects_non_native_bridge():
+ class StubModel(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.proj = nn.Linear(4, 4)
+
+ class StubAdapter(ArchitectureAdapter):
+ def __init__(self, cfg):
+ super().__init__(cfg)
+ self.component_mapping = {"stub_proj": LinearBridge(name="proj")}
+
+ cfg = _cfg(architecture="StubForTest")
+ bridge = TransformerBridge(StubModel(), StubAdapter(cfg), tokenizer=None)
+
+ with pytest.raises(RuntimeError, match=r"boot_native.*StubModel"):
+ bridge.init_weights()
+
+
def test_boot_native_forward_and_cache():
cfg = _cfg()
bridge = TransformerBridge.boot_native(cfg)
@@ -137,6 +275,35 @@ def test_boot_native_forward_and_cache():
assert "blocks.0.attn.hook_pattern" in cache
+@pytest.mark.parametrize("prepend_bos", [True, False])
+def test_run_with_cache_forwards_prepend_bos_for_string_input(monkeypatch, prepend_bos):
+ cfg = _cfg()
+ bridge = TransformerBridge.boot_native(cfg)
+ bridge._tokenizer = object()
+ tokenization_calls = []
+
+ def to_tokens(input, prepend_bos=None, padding_side=None):
+ tokenization_calls.append((input, prepend_bos, padding_side))
+ if prepend_bos is None:
+ prepend_bos = bridge.cfg.default_prepend_bos
+ tokens = [0, 7] if prepend_bos else [7]
+ return torch.tensor([tokens])
+
+ monkeypatch.setattr(bridge, "to_tokens", to_tokens)
+ bridge.eval()
+
+ with torch.no_grad():
+ direct_logits = bridge("hello", prepend_bos=prepend_bos)
+ cached_logits, cache = bridge.run_with_cache("hello", prepend_bos=prepend_bos)
+
+ assert tokenization_calls == [
+ ("hello", prepend_bos, None),
+ ("hello", prepend_bos, None),
+ ]
+ torch.testing.assert_close(cached_logits, direct_logits)
+ assert cache["hook_embed"].shape[1] == direct_logits.shape[1]
+
+
def test_boot_native_does_not_load_transformers_runtime():
# Sanity that the native path doesn't depend on HuggingFace's `transformers`
# for the runtime work — we check that calling boot_native doesn't trigger
@@ -323,3 +490,40 @@ def test_boot_native_supports_training_step():
), "No non-zero gradients after backward"
optimizer.step()
optimizer.zero_grad()
+
+
+def test_boot_native_resolves_initializer_range_sentinel():
+ """Regression for #1568 — the resolved initializer range must be used by boot_native."""
+ import math
+
+ cfg = _cfg(init_mode="gpt2")
+ expected = 0.8 / math.sqrt(cfg.d_model)
+
+ assert cfg.initializer_range == pytest.approx(expected)
+
+ bridge = TransformerBridge.boot_native(cfg)
+ assert bridge.W_E.std().item() == pytest.approx(expected, rel=0.15)
+
+
+def test_boot_native_resolves_non_gpt2_initializer_range_sentinel():
+ cfg = _cfg(init_mode="kaiming_normal")
+
+ assert cfg.initializer_range == pytest.approx(1.0)
+
+
+def test_boot_native_kaiming_gain_scales_weights():
+ """Regression for #1568 — xavier/kaiming init must use initializer_range
+ as a multiplicative gain. Without this, the config value is silently
+ ignored and every kaiming/xavier model gets the same fixed scale
+ regardless of what the caller asked for."""
+ cfg_gain_1 = _cfg(init_mode="kaiming_normal", initializer_range=1.0, seed=0)
+ cfg_gain_2 = _cfg(init_mode="kaiming_normal", initializer_range=2.0, seed=0)
+
+ bridge_1 = TransformerBridge.boot_native(cfg_gain_1)
+ bridge_2 = TransformerBridge.boot_native(cfg_gain_2)
+
+ std_1 = bridge_1.W_E.std().item()
+ std_2 = bridge_2.W_E.std().item()
+
+ # Same seed, only gain differs -> std should scale ~proportionally.
+ assert std_2 / std_1 == pytest.approx(2.0)
diff --git a/tests/unit/model_bridge/test_bridge_generate_no_tokenizer.py b/tests/unit/model_bridge/test_bridge_generate_no_tokenizer.py
index 9819fec1bc..cdeeedcfed 100644
--- a/tests/unit/model_bridge/test_bridge_generate_no_tokenizer.py
+++ b/tests/unit/model_bridge/test_bridge_generate_no_tokenizer.py
@@ -8,7 +8,6 @@
generation path (algorithmic/custom-tokenized use cases).
"""
-import platform
import pytest
import torch
@@ -17,8 +16,6 @@
_PROMPT_TOKENS = torch.tensor([[15496, 11, 314, 1101, 257]], dtype=torch.long)
-_MACOS_ARM64 = platform.system() == "Darwin" and platform.machine() == "arm64"
-
@pytest.fixture(scope="module")
def tokenizer_free_bridge():
@@ -27,7 +24,6 @@ def tokenizer_free_bridge():
return bridge
-@pytest.mark.skipif(_MACOS_ARM64, reason="Upstream macOS-arm64 KV-cache NaN; see linked issue.")
def test_generate_without_tokenizer_stop_at_eos_false_kv_cache(tokenizer_free_bridge):
"""generate() with no tokenizer, stop_at_eos=False, use_past_kv_cache=True."""
bridge = tokenizer_free_bridge
@@ -164,7 +160,6 @@ def test_generate_string_input_without_tokenizer_errors(tokenizer_free_bridge):
bridge.generate("hello", max_new_tokens=3, verbose=False)
-@pytest.mark.skipif(_MACOS_ARM64, reason="Upstream macOS-arm64 KV-cache NaN; see linked issue.")
def test_generate_return_type_str_without_tokenizer_errors(tokenizer_free_bridge):
"""generate(return_type='str') must error when no tokenizer is set.
diff --git a/tests/unit/model_bridge/test_bridge_vs_hooked_transformer_mlp_in_patching.py b/tests/unit/model_bridge/test_bridge_vs_hooked_transformer_mlp_in_patching.py
index e643905a13..917ac098ab 100644
--- a/tests/unit/model_bridge/test_bridge_vs_hooked_transformer_mlp_in_patching.py
+++ b/tests/unit/model_bridge/test_bridge_vs_hooked_transformer_mlp_in_patching.py
@@ -3,6 +3,7 @@
Parameterized over Pythia (native autograd LN) and GPT-2 (manual LN), and over
``no_processing`` so both folded and unfolded compat-mode setups are covered.
"""
+
from __future__ import annotations
import pytest
@@ -105,20 +106,19 @@ def _inner(tensor: torch.Tensor, hook: object) -> torch.Tensor:
@pytest.mark.slow
def test_mlp_in_gated_off_does_not_fire() -> None:
- """When ``use_hook_mlp_in`` is False, the bridge pre-ln2 closure must skip firing."""
+ """When ``use_hook_mlp_in`` is False, explicitly adding hook_mlp_in
+ should fail with a clear error explaining how to enable the gated hook."""
bridge = TransformerBridge.boot_transformers("gpt2", device="cpu")
bridge.enable_compatibility_mode(no_processing=True)
bridge.set_use_hook_mlp_in(False)
- fire_count = {"n": 0}
-
def _counter(tensor: torch.Tensor, hook: object) -> torch.Tensor:
- fire_count["n"] += 1
return tensor
prompt = torch.arange(1, 9).unsqueeze(0)
- bridge.run_with_hooks(prompt, fwd_hooks=[("blocks.0.hook_mlp_in", _counter)])
- assert fire_count["n"] == 0, (
- f"hook_mlp_in fired {fire_count['n']} times with use_hook_mlp_in=False; "
- "should not fire when the flag is off"
- )
+
+ with pytest.raises(ValueError, match="set_use_hook_mlp_in"):
+ bridge.run_with_hooks(
+ prompt,
+ fwd_hooks=[("blocks.0.hook_mlp_in", _counter)],
+ )
diff --git a/tests/unit/model_bridge/test_config_flag_assignment.py b/tests/unit/model_bridge/test_config_flag_assignment.py
new file mode 100644
index 0000000000..d613312a05
--- /dev/null
+++ b/tests/unit/model_bridge/test_config_flag_assignment.py
@@ -0,0 +1,223 @@
+"""Tests for direct assignment of Bridge-managed hook flags (#1689)."""
+
+from __future__ import annotations
+
+import copy
+import gc
+
+import pytest
+import torch
+from torch import nn
+from transformers import GPT2Config, GPT2LMHeadModel, LlamaConfig, LlamaForCausalLM
+
+from transformer_lens.config import TransformerBridgeConfig
+from transformer_lens.factories.architecture_adapter_factory import (
+ ArchitectureAdapterFactory,
+)
+from transformer_lens.model_bridge import TransformerBridge
+from transformer_lens.model_bridge.sources._bridge_builder import (
+ build_bridge_from_module,
+)
+from transformer_lens.model_bridge.sources.native import NativeModel
+
+
+def _cfg() -> TransformerBridgeConfig:
+ return TransformerBridgeConfig(
+ d_model=32,
+ d_head=16,
+ n_heads=2,
+ n_layers=1,
+ n_ctx=8,
+ d_vocab=16,
+ d_mlp=64,
+ act_fn="gelu",
+ normalization_type="LN",
+ seed=0,
+ )
+
+
+def _tiny_gpt2_bridge() -> TransformerBridge:
+ hf_config = GPT2Config(
+ n_layer=1,
+ n_head=2,
+ n_embd=32,
+ n_positions=8,
+ n_ctx=8,
+ vocab_size=16,
+ )
+ hf_model = GPT2LMHeadModel(hf_config).eval()
+ return build_bridge_from_module(
+ hf_model,
+ "GPT2LMHeadModel",
+ hf_config=hf_config,
+ tokenizer=None,
+ device="cpu",
+ )
+
+
+def _tiny_llama_bridge() -> TransformerBridge:
+ hf_config = LlamaConfig(
+ hidden_size=32,
+ intermediate_size=64,
+ num_hidden_layers=1,
+ num_attention_heads=2,
+ num_key_value_heads=2,
+ vocab_size=16,
+ max_position_embeddings=8,
+ )
+ hf_model = LlamaForCausalLM(hf_config).eval()
+ return build_bridge_from_module(
+ hf_model,
+ "LlamaForCausalLM",
+ hf_config=hf_config,
+ tokenizer=None,
+ device="cpu",
+ )
+
+
+@pytest.fixture(params=["gpt2", "llama"], ids=["shared-config", "cloned-config"])
+def bridge_with_config_mode(request: pytest.FixtureRequest) -> TransformerBridge:
+ bridge = _tiny_gpt2_bridge() if request.param == "gpt2" else _tiny_llama_bridge()
+ attn_config_is_shared = bridge.blocks[0].attn.config is bridge.cfg
+ assert attn_config_is_shared is (request.param == "gpt2")
+ return bridge
+
+
+@pytest.mark.parametrize(
+ ("flag_name", "hook_name"),
+ [
+ ("use_attn_result", "blocks.0.attn.hook_result"),
+ ("use_attn_in", "blocks.0.attn.hook_attn_in"),
+ ("use_hook_mlp_in", "blocks.0.hook_mlp_in"),
+ ("use_split_qkv_input", "blocks.0.attn.hook_q_input"),
+ ],
+)
+def test_direct_assignment_matches_setter_hook_behavior(
+ bridge_with_config_mode: TransformerBridge, flag_name: str, hook_name: str
+) -> None:
+ bridge = bridge_with_config_mode
+ tokens = torch.randint(0, bridge.cfg.d_vocab, (1, 8))
+
+ setattr(bridge.cfg, flag_name, True)
+ _, direct_cache = bridge.run_with_cache(tokens, names_filter=[hook_name])
+
+ setattr(bridge.cfg, flag_name, False)
+ getattr(bridge, f"set_{flag_name}")(True)
+ _, setter_cache = bridge.run_with_cache(tokens, names_filter=[hook_name])
+
+ assert list(direct_cache) == [hook_name]
+ assert list(setter_cache) == [hook_name]
+ assert direct_cache[hook_name].shape == setter_cache[hook_name].shape
+
+
+def test_direct_assignment_preserves_mutual_exclusivity() -> None:
+ bridge = _tiny_gpt2_bridge()
+
+ bridge.cfg.use_split_qkv_input = True
+ with pytest.raises(ValueError, match="mutually exclusive"):
+ bridge.cfg.use_attn_in = True
+ assert bridge.cfg.use_attn_in is False
+
+ bridge.cfg.use_split_qkv_input = False
+ bridge.cfg.use_attn_in = True
+ with pytest.raises(ValueError, match="mutually exclusive"):
+ bridge.cfg.use_split_qkv_input = True
+ assert bridge.cfg.use_split_qkv_input is False
+
+
+@pytest.mark.parametrize("flag_name", ["use_attn_result", "use_attn_in", "use_split_qkv_input"])
+def test_direct_assignment_preserves_unsupported_architecture_errors(
+ monkeypatch: pytest.MonkeyPatch, flag_name: str
+) -> None:
+ bridge = TransformerBridge.boot_native(_cfg())
+
+ class _FakeBlock(nn.Module):
+ def __init__(self) -> None:
+ super().__init__()
+ self.attn = nn.Identity()
+
+ monkeypatch.setattr(bridge, "blocks", nn.ModuleList([_FakeBlock()]), raising=True)
+
+ with pytest.raises(NotImplementedError, match=flag_name):
+ setattr(bridge.cfg, flag_name, True)
+ assert getattr(bridge.cfg, flag_name) is False
+
+
+def test_deepcopied_live_config_is_not_bound_to_original_bridge() -> None:
+ bridge = TransformerBridge.boot_native(_cfg())
+ copied_cfg = copy.deepcopy(bridge.cfg)
+
+ copied_cfg.use_hook_mlp_in = True
+
+ assert copied_cfg.use_hook_mlp_in is True
+ assert bridge.cfg.use_hook_mlp_in is False
+
+
+def test_deepcopied_bridge_rebinds_its_config() -> None:
+ bridge = TransformerBridge.boot_native(_cfg())
+ copied_bridge = copy.deepcopy(bridge)
+
+ copied_bridge.cfg.use_hook_mlp_in = True
+
+ assert copied_bridge.cfg.use_hook_mlp_in is True
+ assert copied_bridge.blocks[0].config.use_hook_mlp_in is True
+ assert bridge.cfg.use_hook_mlp_in is False
+
+
+def test_shallow_copied_bridge_does_not_replace_live_config_binding() -> None:
+ bridge = TransformerBridge.boot_native(_cfg())
+ with pytest.warns(UserWarning, match="already bound to another live"):
+ copied_bridge = copy.copy(bridge)
+
+ assert copied_bridge.cfg is bridge.cfg
+ assert bridge.cfg._bridge_ref() is bridge
+
+ del copied_bridge
+ gc.collect()
+ bridge.cfg.use_hook_mlp_in = True
+
+ assert bridge.blocks[0].config.use_hook_mlp_in is True
+
+
+def test_constructor_warns_when_live_bridge_already_owns_config() -> None:
+ cfg = _cfg()
+ cfg.architecture = "TransformerLensNative"
+ first_model = NativeModel(cfg)
+ second_model = NativeModel(cfg)
+ first_adapter = ArchitectureAdapterFactory.select_architecture_adapter(cfg)
+ second_adapter = ArchitectureAdapterFactory.select_architecture_adapter(cfg)
+ first_adapter.prepare_model(first_model)
+ second_adapter.prepare_model(second_model)
+ first_bridge = TransformerBridge(first_model, first_adapter, tokenizer=None)
+
+ with pytest.warns(UserWarning, match="already bound to another live"):
+ second_bridge = TransformerBridge(second_model, second_adapter, tokenizer=None)
+
+ assert second_bridge.cfg is first_bridge.cfg
+ assert cfg._bridge_ref() is first_bridge
+
+
+def test_attention_flag_propagation_does_not_dispatch_bound_cloned_config() -> None:
+ bridge = _tiny_llama_bridge()
+ cloned_cfg = bridge.blocks[0].attn.config
+ other_bridge = TransformerBridge.boot_native(_cfg())
+ assert cloned_cfg is not bridge.cfg
+ cloned_cfg._bind_bridge(other_bridge)
+
+ bridge.set_use_attn_in(True)
+
+ assert cloned_cfg.use_attn_in is True
+ assert other_bridge.cfg.use_attn_in is False
+
+
+def test_mlp_flag_propagation_does_not_dispatch_bound_cloned_config() -> None:
+ bridge = _tiny_gpt2_bridge()
+ cloned_cfg = bridge.blocks[0].config
+ other_bridge = TransformerBridge.boot_native(_cfg())
+ assert cloned_cfg is not bridge.cfg
+ cloned_cfg._bind_bridge(other_bridge)
+
+ bridge.set_use_hook_mlp_in(True)
+
+ assert cloned_cfg.use_hook_mlp_in is True
+ assert other_bridge.cfg.use_hook_mlp_in is False
diff --git a/tests/unit/model_bridge/test_expand_kv_heads.py b/tests/unit/model_bridge/test_expand_kv_heads.py
new file mode 100644
index 0000000000..35b60b78db
--- /dev/null
+++ b/tests/unit/model_bridge/test_expand_kv_heads.py
@@ -0,0 +1,56 @@
+"""Unit tests for TransformerBridge._expand_kv_heads.
+
+Regression for https://github.com/TransformerLensOrg/TransformerLens/issues/1553:
+weight circuits on GQA models must expand the grouped K/V head axis to n_heads
+(repeat_interleave, matching HookedTransformer's GroupedQueryAttention layout)
+before factoring, while MHA weights pass through untouched.
+"""
+
+import pytest
+import torch
+
+from transformer_lens.config.transformer_bridge_config import TransformerBridgeConfig
+from transformer_lens.model_bridge.bridge import TransformerBridge
+
+
+def _bridge_stub(n_heads: int) -> TransformerBridge:
+ """Uninitialized bridge carrying only the cfg that _expand_kv_heads reads."""
+ bridge = TransformerBridge.__new__(TransformerBridge)
+ bridge.cfg = TransformerBridgeConfig(
+ d_model=8,
+ d_head=2,
+ n_heads=n_heads,
+ n_layers=2,
+ n_ctx=8,
+ d_vocab=16,
+ )
+ return bridge
+
+
+class TestExpandKvHeads:
+ def test_grouped_kv_expands_by_repeat_interleave(self):
+ bridge = _bridge_stub(n_heads=4)
+ grouped = torch.arange(2 * 2 * 3 * 2, dtype=torch.float32).reshape(2, 2, 3, 2)
+
+ expanded = bridge._expand_kv_heads(grouped)
+
+ assert expanded.shape == (2, 4, 3, 2)
+ # Query head h must read kv head h // (n_heads // n_kv_heads).
+ for h in range(4):
+ assert torch.equal(expanded[:, h], grouped[:, h // 2])
+
+ def test_mha_weights_pass_through_untouched(self):
+ bridge = _bridge_stub(n_heads=4)
+ mha = torch.randn(2, 4, 3, 2)
+ assert bridge._expand_kv_heads(mha) is mha
+
+ def test_non_4d_input_passes_through_untouched(self):
+ bridge = _bridge_stub(n_heads=4)
+ bias_stack = torch.randn(2, 2, 2)
+ assert bridge._expand_kv_heads(bias_stack) is bias_stack
+
+ def test_indivisible_head_counts_raise(self):
+ bridge = _bridge_stub(n_heads=4)
+ grouped = torch.randn(2, 3, 3, 2)
+ with pytest.raises(ValueError, match="multiple of n_kv_heads"):
+ bridge._expand_kv_heads(grouped)
diff --git a/tests/unit/model_bridge/test_gated_hooks.py b/tests/unit/model_bridge/test_gated_hooks.py
new file mode 100644
index 0000000000..4e212eb0af
--- /dev/null
+++ b/tests/unit/model_bridge/test_gated_hooks.py
@@ -0,0 +1,90 @@
+"""Tests for gated hook validation (issue #1688).
+
+Adding a hook to a gated-off hook point (hook_result, hook_mlp_in, hook_attn_in,
+hook_{q,k,v}_input) should fail loudly, not silently accept the hook and never fire it.
+"""
+
+from __future__ import annotations
+
+import warnings
+
+import pytest
+import torch
+
+from transformer_lens.config import TransformerBridgeConfig
+from transformer_lens.model_bridge import TransformerBridge
+
+
+def _cfg(**overrides) -> TransformerBridgeConfig:
+ base = dict(
+ d_model=32,
+ d_head=16,
+ n_heads=2,
+ n_layers=1,
+ n_ctx=8,
+ d_vocab=16,
+ d_mlp=64,
+ act_fn="gelu",
+ normalization_type="LN",
+ seed=0,
+ )
+ base.update(overrides)
+ return TransformerBridgeConfig(**base)
+
+
+def test_add_hook_rejects_gated_attn_result():
+ """add_hook on hook_result with use_attn_result=False raises a clear ValueError."""
+ bridge = TransformerBridge.boot_native(_cfg())
+ with pytest.raises(ValueError, match="use_attn_result"):
+ bridge.add_hook("blocks.0.attn.hook_result", lambda t, hook=None: t)
+
+
+def test_add_hook_rejects_gated_split_qkv_input():
+ """add_hook on hook_q_input with use_split_qkv_input=False raises a clear ValueError."""
+ bridge = TransformerBridge.boot_native(_cfg())
+ with pytest.raises(ValueError, match="use_split_qkv_input"):
+ bridge.add_hook("blocks.0.attn.hook_q_input", lambda t, hook=None: t)
+
+
+def test_add_hook_rejects_gated_mlp_in():
+ """add_hook on hook_mlp_in with use_hook_mlp_in=False raises a clear ValueError."""
+ bridge = TransformerBridge.boot_native(_cfg())
+ with pytest.raises(ValueError, match="use_hook_mlp_in"):
+ bridge.add_hook("blocks.0.hook_mlp_in", lambda t, hook=None: t)
+
+
+def test_add_hook_rejects_gated_attn_in():
+ """add_hook on hook_attn_in with use_attn_in=False raises a clear ValueError."""
+ bridge = TransformerBridge.boot_native(_cfg())
+ with pytest.raises(ValueError, match="use_attn_in"):
+ bridge.add_hook("blocks.0.hook_attn_in", lambda t, hook=None: t)
+
+
+def test_add_hook_succeeds_after_enabling_setter():
+ """Regression guard: enabling the flag via the setter still lets the hook fire."""
+ bridge = TransformerBridge.boot_native(_cfg())
+ bridge.set_use_hook_mlp_in(True)
+
+ fired = []
+ bridge.add_hook("blocks.0.hook_mlp_in", lambda t, hook=None: fired.append(1) or t)
+
+ tokens = torch.randint(0, 16, (1, 8))
+ bridge(tokens, return_type="logits")
+
+ assert len(fired) > 0, "Hook did not fire after enabling use_hook_mlp_in via the setter"
+
+
+def test_run_with_cache_warns_on_fully_gated_names_filter():
+ """run_with_cache with a filter matching only gated-off names warns instead of
+ silently returning an empty cache."""
+ bridge = TransformerBridge.boot_native(_cfg())
+ tokens = torch.randint(0, 16, (1, 8))
+
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter("always")
+ _, cache = bridge.run_with_cache(tokens, names_filter=["blocks.0.hook_mlp_in"])
+
+ assert len(cache) == 0
+ assert any("gated-off" in str(w.message) for w in caught), (
+ "Expected a warning naming the gated-off hook, got: " f"{[str(w.message) for w in caught]}"
+ )
diff --git a/tests/unit/model_bridge/test_get_params_util.py b/tests/unit/model_bridge/test_get_params_util.py
index e6ff03b7ba..4284988d8c 100644
--- a/tests/unit/model_bridge/test_get_params_util.py
+++ b/tests/unit/model_bridge/test_get_params_util.py
@@ -73,15 +73,15 @@ def test_get_bridge_params_attention_reshaping(self):
w_v = params[f"blocks.{layer_idx}.attn.W_V"]
w_o = params[f"blocks.{layer_idx}.attn.W_O"]
- # Shape alone cannot catch a reshape that scrambles the elements,
- # nor Q/K/V being read from the wrong projection: pin the VALUES
- # against the source weights the mock block exposes.
+ # Shape alone cannot catch Q/K/V being read from the wrong
+ # projection: pin the VALUES against the TL-layout properties the
+ # mock block exposes.
block = mock_bridge.blocks[layer_idx]
n_heads, d_model, d_head = 12, 768, 64
assert w_q.shape == (n_heads, d_model, d_head)
assert w_o.shape == (n_heads, d_head, d_model)
- torch.testing.assert_close(w_q, block.attn.q.weight.reshape(n_heads, d_model, d_head))
- torch.testing.assert_close(w_o, block.attn.o.weight.reshape(n_heads, d_head, d_model))
+ torch.testing.assert_close(w_q, block.attn.W_Q)
+ torch.testing.assert_close(w_o, block.attn.W_O)
# Negative control: Q and K must be distinguishable in this fixture,
# or reading either one would satisfy the assertions above.
assert not torch.equal(w_q, w_k)
@@ -197,13 +197,12 @@ def _create_mock_bridge_with_none_biases(self):
# Set all biases to None
for block in mock_bridge.blocks:
- block.attn.q.bias = None
- block.attn.k.bias = None
- block.attn.v.bias = None
- block.attn.o.bias = None
- setattr(block.mlp, "in", Mock())
- getattr(block.mlp, "in").bias = None
- block.mlp.out.bias = None
+ block.attn.b_Q = None
+ block.attn.b_K = None
+ block.attn.b_V = None
+ block.attn.b_O = None
+ block.mlp.b_in = None
+ block.mlp.b_out = None
return mock_bridge
@@ -231,45 +230,99 @@ def _create_mock_bridge_with_gate_weights(self):
# Add gate weights to MLP
for block in mock_bridge.blocks:
- block.mlp.gate = Mock()
- block.mlp.gate.weight = torch.randn(3072, 768)
- block.mlp.gate.bias = torch.randn(3072)
- block.mlp.W_gate = torch.randn(3072, 768)
+ block.mlp.W_gate = torch.randn(768, 3072)
+ block.mlp.b_gate = torch.randn(3072)
return mock_bridge
def _create_mock_block(self):
- """Create a mock transformer block."""
+ """Create a mock transformer block exposing TL-layout weight properties."""
block = Mock()
- # Mock attention
+ # Mock attention (TL-layout properties, as the component bridges expose)
block.attn = Mock()
- block.attn.q = Mock()
- block.attn.q.weight = torch.randn(768, 768)
- block.attn.q.bias = torch.randn(768)
+ block.attn.W_Q = torch.randn(12, 768, 64)
+ block.attn.W_K = torch.randn(12, 768, 64)
+ block.attn.W_V = torch.randn(12, 768, 64)
+ block.attn.W_O = torch.randn(12, 64, 768)
+ block.attn.b_Q = torch.randn(12, 64)
+ block.attn.b_K = torch.randn(12, 64)
+ block.attn.b_V = torch.randn(12, 64)
+ block.attn.b_O = torch.randn(768)
+
+ # Mock MLP
+ block.mlp = Mock()
+ block.mlp.W_in = torch.randn(768, 3072)
+ block.mlp.W_out = torch.randn(3072, 768)
+ block.mlp.b_in = torch.randn(3072)
+ block.mlp.b_out = torch.randn(768)
- block.attn.k = Mock()
- block.attn.k.weight = torch.randn(768, 768)
- block.attn.k.bias = torch.randn(768)
+ return block
- block.attn.v = Mock()
- block.attn.v.weight = torch.randn(768, 768)
- block.attn.v.bias = torch.randn(768)
- block.attn.o = Mock()
- block.attn.o.weight = torch.randn(768, 768)
- block.attn.o.bias = torch.randn(768)
+class TestGQAExpansion:
+ """Grouped K/V must be expanded to n_heads (legacy HT convention)."""
- # Mock MLP (mirrors MLPBridge's normalized accessor API)
- block.mlp = Mock()
- block.mlp.W_in = torch.randn(768, 3072)
- block.mlp.W_out = torch.randn(3072, 768)
- setattr(block.mlp, "in", Mock())
- getattr(block.mlp, "in").weight = torch.randn(768, 3072)
- getattr(block.mlp, "in").bias = torch.randn(3072)
+ def _make_gqa_bridge(self):
+ mock_bridge = Mock()
+ mock_bridge.cfg = Mock()
+ mock_bridge.cfg.n_layers = 1
+ mock_bridge.cfg.d_model = 64
+ mock_bridge.cfg.n_heads = 4
+ mock_bridge.cfg.d_head = 16
+ mock_bridge.cfg.d_vocab = 100
+ mock_bridge.cfg.n_ctx = 32
+ mock_bridge.cfg.d_mlp = 128
+ mock_bridge.cfg.device = torch.device("cpu")
- block.mlp.out = Mock()
- block.mlp.out.weight = torch.randn(3072, 768)
- block.mlp.out.bias = torch.randn(768)
+ mock_bridge.embed = Mock()
+ mock_bridge.embed.weight = torch.randn(100, 64)
+ mock_bridge.pos_embed = Mock()
+ mock_bridge.pos_embed.weight = torch.randn(32, 64)
+ mock_bridge.unembed = Mock()
+ mock_bridge.unembed.weight = torch.randn(100, 64)
- return block
+ block = Mock()
+ block.attn = Mock()
+ block.attn.W_Q = torch.randn(4, 64, 16)
+ block.attn.W_K = torch.randn(2, 64, 16) # grouped: n_kv_heads=2
+ block.attn.W_V = torch.randn(2, 64, 16)
+ block.attn.W_O = torch.randn(4, 16, 64)
+ block.attn.b_Q = torch.randn(4, 16)
+ block.attn.b_K = torch.randn(2, 16)
+ block.attn.b_V = torch.randn(2, 16)
+ block.attn.b_O = torch.randn(64)
+ block.mlp = Mock()
+ block.mlp.W_in = torch.randn(64, 128)
+ block.mlp.W_out = torch.randn(128, 64)
+ block.mlp.b_in = torch.randn(128)
+ block.mlp.b_out = torch.randn(64)
+ mock_bridge.blocks = [block]
+ return mock_bridge
+
+ def test_grouped_kv_expanded_to_n_heads(self):
+ bridge = self._make_gqa_bridge()
+ params = get_bridge_params(bridge)
+
+ w_k = params["blocks.0.attn.W_K"]
+ w_v = params["blocks.0.attn.W_V"]
+ assert w_k.shape == (4, 64, 16)
+ assert w_v.shape == (4, 64, 16)
+ # repeat_interleave semantics: heads 0,1 share kv head 0; heads 2,3 share kv head 1
+ assert torch.equal(w_k[0], w_k[1])
+ assert torch.equal(w_k[0], bridge.blocks[0].attn.W_K[0])
+ assert torch.equal(w_k[2], bridge.blocks[0].attn.W_K[1])
+
+ def test_grouped_biases_expanded(self):
+ bridge = self._make_gqa_bridge()
+ params = get_bridge_params(bridge)
+ b_k = params["blocks.0.attn.b_K"]
+ assert b_k.shape == (4, 16)
+ assert params["blocks.0.attn.b_V"].shape == (4, 16)
+ # Pairing must be repeat_interleave (blocked), not tiling: heads 0,1
+ # share kv head 0 and heads 2,3 share kv head 1.
+ grouped = bridge.blocks[0].attn.b_K
+ assert torch.equal(b_k[0], b_k[1])
+ assert torch.equal(b_k[0], grouped[0])
+ assert torch.equal(b_k[2], grouped[1])
+ assert torch.equal(params["blocks.0.attn.b_Q"], bridge.blocks[0].attn.b_Q)
diff --git a/tests/unit/model_bridge/test_get_params_util_helpers.py b/tests/unit/model_bridge/test_get_params_util_helpers.py
index de764400e1..daa1ba4177 100644
--- a/tests/unit/model_bridge/test_get_params_util_helpers.py
+++ b/tests/unit/model_bridge/test_get_params_util_helpers.py
@@ -1,91 +1,40 @@
"""Tests for get_params_util helper functions."""
-import torch
-
-from transformer_lens.model_bridge.get_params_util import (
- _get_n_kv_heads,
- _get_or_create_bias,
- _reshape_kv_weight,
-)
-
-
-class _FakeCfg:
- """Minimal config stub for testing."""
+from unittest.mock import Mock
- def __init__(self, **kwargs):
- for k, v in kwargs.items():
- setattr(self, k, v)
-
-
-class TestGetNKVHeads:
- def test_prefers_kv_heads_over_n_heads(self):
- cfg = _FakeCfg(n_heads=12, n_key_value_heads=4)
- assert _get_n_kv_heads(cfg) == 4
- assert _get_n_kv_heads(cfg) != cfg.n_heads
-
- def test_fallback_to_n_heads_when_missing(self):
- cfg = _FakeCfg(n_heads=12)
- assert _get_n_kv_heads(cfg) == 12
- assert not hasattr(cfg, "n_key_value_heads")
+import torch
- def test_none_kv_heads_falls_back(self):
- # n_key_value_heads exists but is None — should fall back
- cfg = _FakeCfg(n_heads=12, n_key_value_heads=None)
- assert _get_n_kv_heads(cfg) == 12
+from transformer_lens.model_bridge.get_params_util import _tensor_attr
-class TestReshapeKVWeight:
- def test_full_size_preserves_data(self):
- cfg = _FakeCfg(d_model=64, n_heads=4, d_head=16)
- weight = torch.randn(64, 64)
- result = _reshape_kv_weight(weight, cfg, "cpu", torch.float32)
- assert result.shape == (4, 64, 16)
- # Total elements must be preserved
- assert result.numel() == weight.numel()
- # Data must be the same (just reshaped)
- assert torch.equal(result.reshape(-1), weight.reshape(-1))
+class TestTensorAttr:
+ def test_returns_first_tensor_among_names(self):
+ obj = Mock()
+ obj.w = torch.ones(3)
+ obj.weight = torch.zeros(3)
+ assert torch.equal(_tensor_attr(obj, "w", "weight"), torch.ones(3))
- def test_mqa_weight_expands_heads(self):
- cfg = _FakeCfg(d_model=64, n_heads=4, d_head=16)
- # MQA: single head (d_head, d_model)
- weight = torch.randn(16, 64)
- result = _reshape_kv_weight(weight, cfg, "cpu", torch.float32)
- assert result.shape == (4, 64, 16)
- # All 4 heads should be identical copies of the single head
- for i in range(1, 4):
- assert torch.equal(result[i], result[0])
+ def test_falls_through_non_tensor_values(self):
+ # Mock auto-attributes return Mocks, which must not be mistaken for weights.
+ obj = Mock()
+ obj.weight = torch.full((2,), 5.0)
+ result = _tensor_attr(obj, "w", "weight")
+ assert torch.equal(result, torch.full((2,), 5.0))
- def test_numel_match_uses_view(self):
- cfg = _FakeCfg(d_model=64, n_heads=4, d_head=16)
- # Non-standard shape but total elements match
- weight = torch.randn(4 * 64 * 16).reshape(32, 128)
- result = _reshape_kv_weight(weight, cfg, "cpu", torch.float32)
- assert result.shape == (4, 64, 16)
- assert result.numel() == weight.numel()
+ def test_none_object_returns_none(self):
+ assert _tensor_attr(None, "weight") is None
- def test_incompatible_shape_returns_zeros(self):
- cfg = _FakeCfg(d_model=64, n_heads=4, d_head=16)
- weight = torch.randn(7, 13) # impossible to reshape
- result = _reshape_kv_weight(weight, cfg, "cpu", torch.float32)
- assert result.shape == (4, 64, 16)
- assert torch.all(result == 0)
- # Verify it's actually zeros, not just small values
- assert result.sum().item() == 0.0
+ def test_missing_and_none_attrs_return_none(self):
+ class Holder:
+ bias = None
+ assert _tensor_attr(Holder(), "nonexistent", "bias") is None
-class TestGetOrCreateBias:
- def test_reshapes_existing_bias(self):
- bias = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
- result = _get_or_create_bias(bias, n_heads=2, d_head=4, device="cpu", dtype=torch.float32)
- assert result.shape == (2, 4)
- # Verify the reshape is correct — first head gets [1,2,3,4]
- assert torch.equal(result[0], torch.tensor([1.0, 2.0, 3.0, 4.0]))
- assert torch.equal(result[1], torch.tensor([5.0, 6.0, 7.0, 8.0]))
+ def test_property_raising_is_skipped(self):
+ class Flaky:
+ @property
+ def w(self):
+ raise AttributeError("not materialized")
- def test_none_creates_zeros(self):
- result = _get_or_create_bias(None, n_heads=4, d_head=16, device="cpu", dtype=torch.float32)
- assert result.shape == (4, 16)
- assert result.sum().item() == 0.0
+ weight = torch.ones(2)
- def test_none_respects_dtype(self):
- result = _get_or_create_bias(None, n_heads=2, d_head=8, device="cpu", dtype=torch.float16)
- assert result.dtype == torch.float16
+ assert torch.equal(_tensor_attr(Flaky(), "w", "weight"), torch.ones(2))
diff --git a/tests/unit/model_bridge/test_loss_attention_mask.py b/tests/unit/model_bridge/test_loss_attention_mask.py
new file mode 100644
index 0000000000..de9151f629
--- /dev/null
+++ b/tests/unit/model_bridge/test_loss_attention_mask.py
@@ -0,0 +1,200 @@
+"""Regression tests for padding-aware TransformerBridge causal loss."""
+
+from __future__ import annotations
+
+import pytest
+import torch
+import torch.nn.functional as F
+
+from transformer_lens.config import TransformerBridgeConfig
+from transformer_lens.model_bridge import TransformerBridge
+
+
+def _bridge() -> TransformerBridge:
+ cfg = TransformerBridgeConfig(
+ d_model=32,
+ d_head=8,
+ n_heads=4,
+ n_layers=2,
+ n_ctx=6,
+ d_vocab=32,
+ d_mlp=64,
+ act_fn="gelu",
+ normalization_type="LN",
+ seed=7,
+ initializer_range=0.2,
+ )
+ return TransformerBridge.boot_native(cfg)
+
+
+def _extract_loss(output: torch.Tensor | tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor:
+ return output[1] if isinstance(output, tuple) else output
+
+
+def _manual_masked_loss(
+ logits: torch.Tensor, tokens: torch.Tensor, attention_mask: torch.Tensor
+) -> torch.Tensor:
+ transition_mask = attention_mask[:, :-1].bool() & attention_mask[:, 1:].bool()
+ return F.cross_entropy(
+ logits[:, :-1][transition_mask],
+ tokens[:, 1:][transition_mask],
+ )
+
+
+@pytest.mark.parametrize("return_type", ["loss", "both"])
+def test_forward_loss_ignores_masked_padding_tokens(return_type: str) -> None:
+ bridge = _bridge()
+ attention_mask = torch.tensor(
+ [
+ [1, 1, 1, 0, 0, 0],
+ [1, 1, 1, 1, 1, 1],
+ ]
+ )
+ token_batches = (
+ torch.tensor(
+ [
+ [1, 2, 3, 0, 0, 0],
+ [4, 5, 6, 7, 8, 9],
+ ]
+ ),
+ torch.tensor(
+ [
+ [1, 2, 3, 31, 30, 29],
+ [4, 5, 6, 7, 8, 9],
+ ]
+ ),
+ )
+
+ losses = []
+ for tokens in token_batches:
+ output = bridge(tokens, attention_mask=attention_mask, return_type=return_type)
+ loss = _extract_loss(output)
+ logits = bridge(tokens, attention_mask=attention_mask, return_type="logits")
+ expected = _manual_masked_loss(logits, tokens, attention_mask)
+
+ torch.testing.assert_close(loss, expected)
+ losses.append(loss)
+
+ torch.testing.assert_close(losses[0], losses[1])
+
+
+def test_forward_loss_per_token_zeros_masked_transitions() -> None:
+ bridge = _bridge()
+ tokens = torch.tensor(
+ [
+ [1, 2, 3, 0, 0, 0],
+ [4, 5, 6, 7, 8, 9],
+ ]
+ )
+ attention_mask = torch.tensor(
+ [
+ [1, 1, 1, 0, 0, 0],
+ [1, 1, 1, 1, 1, 1],
+ ]
+ )
+
+ loss = bridge(
+ tokens,
+ attention_mask=attention_mask,
+ return_type="loss",
+ loss_per_token=True,
+ )
+ next_token_mask = torch.logical_and(attention_mask[:, :-1], attention_mask[:, 1:])
+
+ assert torch.count_nonzero(loss[~next_token_mask]) == 0
+
+
+def test_forward_loss_is_finite_with_left_padding() -> None:
+ bridge = _bridge()
+ tokens = torch.tensor(
+ [
+ [0, 0, 0, 1, 2, 3],
+ [4, 5, 6, 7, 8, 9],
+ ]
+ )
+ attention_mask = torch.tensor(
+ [
+ [0, 0, 0, 1, 1, 1],
+ [1, 1, 1, 1, 1, 1],
+ ]
+ )
+ position_ids = attention_mask.long().cumsum(-1) - 1
+ position_ids.masked_fill_(attention_mask == 0, 1)
+
+ logits = bridge(
+ tokens,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ return_type="logits",
+ )
+ loss = bridge(
+ tokens,
+ attention_mask=attention_mask,
+ position_ids=position_ids,
+ return_type="loss",
+ )
+
+ assert torch.isfinite(logits).all()
+ assert torch.isfinite(loss)
+
+
+@pytest.mark.parametrize("mask_kind", ["bool", "additive"])
+@pytest.mark.parametrize("mask_layout", ["key_only", "causal"])
+def test_forward_loss_accepts_equivalent_4d_attention_mask(
+ mask_kind: str, mask_layout: str
+) -> None:
+ bridge = _bridge()
+ tokens = torch.tensor(
+ [
+ [1, 2, 3, 0, 0, 0],
+ [4, 5, 6, 7, 8, 9],
+ ]
+ )
+ attention_mask = torch.tensor(
+ [
+ [1, 1, 1, 0, 0, 0],
+ [1, 1, 1, 1, 1, 1],
+ ]
+ )
+ blocked = ~attention_mask.bool()[:, None, None, :]
+ if mask_layout == "causal":
+ blocked = blocked | torch.ones(6, 6, dtype=torch.bool).triu(1)[None, None]
+ attention_mask_4d = blocked if mask_kind == "bool" else blocked.float() * -10_000.0
+
+ logits_2d, loss_2d = bridge(
+ tokens,
+ attention_mask=attention_mask,
+ return_type="both",
+ )
+ logits_4d, loss_4d = bridge(
+ tokens,
+ attention_mask=attention_mask_4d,
+ return_type="both",
+ )
+
+ torch.testing.assert_close(logits_4d, logits_2d, rtol=0, atol=0)
+ torch.testing.assert_close(loss_4d, loss_2d)
+ torch.testing.assert_close(loss_4d, _manual_masked_loss(logits_4d, tokens, attention_mask))
+
+
+def test_loss_fn_reduces_rectangular_cached_4d_attention_mask() -> None:
+ bridge = _bridge()
+ tokens = torch.tensor([[4, 5]])
+ logits = torch.zeros(1, 2, 32)
+ logits[0, 0, 5] = 2.0
+ cache_and_new_mask = torch.tensor([[0, 1, 1, 1, 1, 1]])
+ key_blocked = ~cache_and_new_mask.bool()[:, None, None, :]
+ query_positions = torch.tensor([4, 5])
+ causal = torch.arange(6)[None, None, None, :] > query_positions[None, None, :, None]
+ attention_mask_4d = key_blocked | causal
+
+ loss = bridge.loss_fn(
+ logits,
+ tokens,
+ attention_mask=attention_mask_4d,
+ per_token=True,
+ )
+
+ expected = F.cross_entropy(logits[:, 0], tokens[:, 1])
+ torch.testing.assert_close(loss, expected.reshape(1, 1))
+ assert loss.shape == (1, 1)
diff --git a/tests/unit/model_bridge/test_n_params_total.py b/tests/unit/model_bridge/test_n_params_total.py
new file mode 100644
index 0000000000..d34142efda
--- /dev/null
+++ b/tests/unit/model_bridge/test_n_params_total.py
@@ -0,0 +1,27 @@
+"""Tests for ``TransformerBridge.n_params_total`` on real model layouts."""
+
+import pytest
+import torch
+from transformers import AutoModelForCausalLM
+
+from transformer_lens.model_bridge import TransformerBridge
+
+
+@pytest.mark.parametrize("model_name", ["gpt2", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5"])
+def test_n_params_total_matches_uninstrumented_model(model_name: str) -> None:
+ hf_model = AutoModelForCausalLM.from_pretrained(
+ model_name, dtype=torch.float32, attn_implementation="eager"
+ )
+ expected = sum(parameter.numel() for parameter in hf_model.parameters())
+ bridge = TransformerBridge.boot_transformers(model_name, hf_model=hf_model)
+ bridge.enable_compatibility_mode()
+ assert bridge.n_params_total == expected
+
+ if "Qwen2" in model_name:
+ tl_parameters = bridge.tl_parameters()
+ for name in ("W_K", "W_V"):
+ weight = tl_parameters[f"blocks.0.attn.{name}"]
+ assert weight.shape[0] == bridge.cfg.n_heads
+ assert torch.count_nonzero(weight)
+ assert not torch.count_nonzero(tl_parameters["pos_embed.W_pos"])
+ assert bridge.n_params_total < sum(p.numel() for p in tl_parameters.values())
diff --git a/tests/unit/model_bridge/test_position_ids_injection_gate.py b/tests/unit/model_bridge/test_position_ids_injection_gate.py
new file mode 100644
index 0000000000..68d5a8f8b0
--- /dev/null
+++ b/tests/unit/model_bridge/test_position_ids_injection_gate.py
@@ -0,0 +1,160 @@
+"""Unit tests for the target gate on mask-derived ``position_ids`` injection.
+
+``TransformerBridge.forward`` derives ``position_ids`` from ``attention_mask``
+so left-padded input gets the right absolute positions (see #1609). That kwarg
+is only safe for models that both accept it and do not derive positions
+themselves, so the injection is gated the same way ``output_attentions`` is in
+``run_with_cache``. The gate is exercised directly here with stand-in modules:
+the models it exists to protect (fixed-signature remote code, mRoPE) are either
+never loaded in CI or belong to the integration tier.
+"""
+
+from __future__ import annotations
+
+from types import SimpleNamespace
+from typing import Any, Optional
+
+import torch
+import torch.nn as nn
+
+from transformer_lens.model_bridge import TransformerBridge
+
+# Unbound so it can run against a stand-in that owns only ``original_model``;
+# the gate reads nothing else off the bridge.
+gate = TransformerBridge._accepts_derived_position_ids
+
+
+def _bridge_over(model: Optional[nn.Module]) -> Any:
+ return SimpleNamespace(original_model=model)
+
+
+class _FixedSignature(nn.Module):
+ """Mirrors ``LLaDAModelLM.forward``: no ``position_ids``, no ``**kwargs``."""
+
+ def forward(
+ self,
+ input_ids: torch.Tensor,
+ attention_mask: Optional[torch.Tensor] = None,
+ use_cache: bool = False,
+ ) -> torch.Tensor:
+ return input_ids
+
+
+class _AcceptsPositionIds(nn.Module):
+ def forward(
+ self,
+ input_ids: torch.Tensor,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ return input_ids
+
+
+class _AcceptsKwargs(nn.Module):
+ def forward(self, input_ids: torch.Tensor, **kwargs: Any) -> torch.Tensor:
+ return input_ids
+
+
+class _MaskConsumingEmbedding(nn.Embedding):
+ """Mirrors ``OPTLearnedPositionalEmbedding``: positions come from the mask."""
+
+ def forward( # type: ignore[override]
+ self,
+ attention_mask: torch.Tensor,
+ past_key_values_length: int = 0,
+ position_ids: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ if position_ids is None:
+ position_ids = (attention_mask.cumsum(1) * attention_mask - 1).long()
+ return super().forward(position_ids)
+
+
+class _OwnsPositions(_AcceptsPositionIds):
+ """mRoPE models compute a 3-D index here, but only while position_ids is None."""
+
+ def get_rope_index(self, *args: Any, **kwargs: Any) -> None:
+ return None
+
+
+class TestSignatureGate:
+ def test_refuses_model_that_cannot_take_position_ids(self) -> None:
+ """Injecting into a fixed-signature remote-code forward raises TypeError
+ where the model previously returned logits."""
+ assert gate(_bridge_over(_FixedSignature())) is False
+
+ def test_allows_explicit_position_ids_parameter(self) -> None:
+ assert gate(_bridge_over(_AcceptsPositionIds())) is True
+
+ def test_allows_var_keyword_forward(self) -> None:
+ """**kwargs forwards pass the kwarg through to the inner model."""
+ assert gate(_bridge_over(_AcceptsKwargs())) is True
+
+
+class TestOwnsPositionsGate:
+ def test_refuses_model_defining_get_rope_index(self) -> None:
+ assert gate(_bridge_over(_OwnsPositions())) is False
+
+ def test_refuses_wrapper_whose_inner_model_owns_positions(self) -> None:
+ """get_rope_index lives on the inner text model, while original_model is
+ usually the ForConditionalGeneration wrapper around it."""
+ wrapper = _AcceptsPositionIds()
+ wrapper.model = _OwnsPositions()
+ assert gate(_bridge_over(wrapper)) is False
+
+ def test_refuses_wrapper_whose_language_model_owns_positions(self) -> None:
+ wrapper = _AcceptsPositionIds()
+ wrapper.language_model = _OwnsPositions()
+ assert gate(_bridge_over(wrapper)) is False
+
+ def test_refuses_mrope_section_in_config(self) -> None:
+ """Config-level backstop: the section list is what makes positions 3-D."""
+ model = _AcceptsPositionIds()
+ model.config = SimpleNamespace( # type: ignore[assignment]
+ rope_scaling={"mrope_section": [1, 1, 2], "rope_type": "default"}
+ )
+ assert gate(_bridge_over(model)) is False
+
+ def test_refuses_mrope_section_in_text_config(self) -> None:
+ """Multimodal configs nest the text model's rope_scaling one level down."""
+ model = _AcceptsPositionIds()
+ model.config = SimpleNamespace( # type: ignore[assignment]
+ rope_scaling=None,
+ text_config=SimpleNamespace(rope_scaling={"mrope_section": [1, 1, 2]}),
+ )
+ assert gate(_bridge_over(model)) is False
+
+ def test_refuses_mask_consuming_positional_embedding(self) -> None:
+ """OPT's OPTLearnedPositionalEmbedding takes the mask and derives its own
+ positions, including its own convention for the padded slots."""
+ model = _AcceptsPositionIds()
+ model.embed_positions = _MaskConsumingEmbedding(8, 4)
+ assert gate(_bridge_over(model)) is False
+
+ def test_ordinary_embeddings_do_not_trip_the_scan(self) -> None:
+ """nn.Embedding takes only indices, so the common case stays injectable."""
+ model = _AcceptsPositionIds()
+ model.wte = nn.Embedding(8, 4)
+ model.wpe = nn.Embedding(8, 4)
+ assert gate(_bridge_over(model)) is True
+
+ def test_plain_rope_scaling_is_not_treated_as_mrope(self) -> None:
+ """Only mrope_section means a multi-stream index; yarn/linear do not."""
+ model = _AcceptsPositionIds()
+ model.config = SimpleNamespace( # type: ignore[assignment]
+ rope_scaling={"rope_type": "yarn", "factor": 8.0}
+ )
+ assert gate(_bridge_over(model)) is True
+
+
+class TestDriverAndCaching:
+ def test_refuses_bridge_without_a_local_module(self) -> None:
+ """A bridge with no wrapped module exposes nothing to introspect."""
+ assert gate(_bridge_over(None)) is False
+
+ def test_recomputes_when_the_underlying_model_is_swapped(self) -> None:
+ """Weight processing replaces original_model, so a cached verdict keyed on
+ the old module must not survive."""
+ bridge = _bridge_over(_AcceptsPositionIds())
+ assert gate(bridge) is True
+ bridge.original_model = _FixedSignature()
+ assert gate(bridge) is False
diff --git a/tests/unit/model_bridge/test_reset_hooks_registry.py b/tests/unit/model_bridge/test_reset_hooks_registry.py
new file mode 100644
index 0000000000..ff15660c3f
--- /dev/null
+++ b/tests/unit/model_bridge/test_reset_hooks_registry.py
@@ -0,0 +1,63 @@
+"""reset_hooks must clear every registered hook point, not just component-owned ones."""
+
+from __future__ import annotations
+
+import torch
+
+from transformer_lens.config import TransformerBridgeConfig
+from transformer_lens.model_bridge import TransformerBridge
+
+
+def _bridge() -> TransformerBridge:
+ cfg = TransformerBridgeConfig(
+ d_model=32,
+ d_head=16,
+ n_heads=2,
+ n_layers=1,
+ n_ctx=8,
+ d_vocab=16,
+ d_mlp=64,
+ act_fn="gelu",
+ normalization_type="LN",
+ seed=0,
+ )
+ return TransformerBridge.boot_native(cfg)
+
+
+def _points_with_hooks(bridge: TransformerBridge) -> list[str]:
+ return [name for name, hp in bridge._hook_registry.items() if hp.fwd_hooks or hp.bwd_hooks]
+
+
+def test_reset_hooks_clears_every_registry_point() -> None:
+ bridge = _bridge()
+ for hp in bridge._hook_registry.values():
+ hp.add_hook(lambda tensor, hook: tensor)
+ assert _points_with_hooks(bridge), "sanity: hooks were added"
+
+ bridge.reset_hooks()
+
+ leaked = _points_with_hooks(bridge)
+ assert leaked == [], f"reset_hooks leaked hooks on registry points: {leaked[:5]}"
+
+
+def test_reset_hooks_permanent_semantics() -> None:
+ bridge = _bridge()
+ name, hp = next(iter(bridge._hook_registry.items()))
+ hp.add_hook(lambda tensor, hook: tensor, is_permanent=True)
+
+ bridge.reset_hooks()
+ assert hp.fwd_hooks, "permanent hook must survive a default reset"
+
+ bridge.reset_hooks(including_permanent=True)
+ assert not hp.fwd_hooks, "including_permanent=True must clear permanent hooks"
+
+
+def test_reset_hooks_still_functional_after_forward() -> None:
+ bridge = _bridge()
+ seen: list[str] = []
+ for name, hp in bridge._hook_registry.items():
+ hp.add_hook(lambda tensor, hook: seen.append(hook.name))
+ bridge.reset_hooks()
+ with torch.no_grad():
+ bridge(torch.randint(0, bridge.cfg.d_vocab, (1, 4)))
+ assert seen == [], "cleared hooks must not fire on forward"
diff --git a/tests/unit/model_bridge/test_state_dict_composition.py b/tests/unit/model_bridge/test_state_dict_composition.py
new file mode 100644
index 0000000000..a8f5a7401b
--- /dev/null
+++ b/tests/unit/model_bridge/test_state_dict_composition.py
@@ -0,0 +1,171 @@
+"""Regression tests for recursive TransformerBridge checkpoint composition (#1655)."""
+
+from __future__ import annotations
+
+from collections import OrderedDict
+from types import SimpleNamespace
+
+import pytest
+import torch
+from transformers import GPT2Config, GPT2LMHeadModel
+
+from transformer_lens.config import TransformerBridgeConfig
+from transformer_lens.model_bridge import TransformerBridge
+from transformer_lens.model_bridge.generalized_components import (
+ JointGateUpMLPBridge,
+ JointQKVAttentionBridge,
+ LinearBridge,
+)
+from transformer_lens.model_bridge.sources import build_bridge_from_module
+
+
+def _native_bridge() -> TransformerBridge:
+ cfg = TransformerBridgeConfig(
+ d_model=32,
+ d_head=16,
+ n_heads=2,
+ n_layers=2,
+ n_ctx=8,
+ d_vocab=16,
+ d_mlp=64,
+ act_fn="gelu",
+ normalization_type="LN",
+ seed=0,
+ )
+ return TransformerBridge.boot_native(cfg)
+
+
+def _parent_with_bridge(bridge: TransformerBridge) -> torch.nn.Module:
+ parent = torch.nn.Module()
+ parent.add_module("bridge", bridge)
+ return parent
+
+
+def test_state_dict_with_destination_and_prefix_uses_recursive_semantics() -> None:
+ bridge = _native_bridge()
+ sentinel = torch.tensor(1)
+ destination: OrderedDict[str, torch.Tensor] = OrderedDict({"sentinel": sentinel})
+
+ returned = bridge.state_dict(destination=destination, prefix="nested.bridge.")
+
+ assert returned is destination
+ assert destination["sentinel"] is sentinel
+ recursive_keys = set(destination) - {"sentinel"}
+ assert recursive_keys
+ assert all(key.startswith("nested.bridge.") for key in recursive_keys)
+
+
+def test_parent_state_dict_strict_round_trip() -> None:
+ parent = _parent_with_bridge(_native_bridge())
+ checkpoint = {key: value.clone() for key, value in parent.state_dict().items()}
+
+ with torch.no_grad():
+ for parameter in parent.parameters():
+ parameter.zero_()
+
+ result = parent.load_state_dict(checkpoint, strict=True)
+
+ assert result.missing_keys == []
+ assert result.unexpected_keys == []
+ reloaded = parent.state_dict()
+ for key, value in checkpoint.items():
+ assert torch.equal(reloaded[key], value), f"{key} did not round-trip"
+
+
+def test_parent_registration_is_stable_across_first_forward() -> None:
+ bridge = _native_bridge()
+ parent = _parent_with_bridge(bridge)
+
+ for block in bridge.blocks:
+ assert block.attn._ln1_module is block.ln1.original_component
+
+ keys_before = tuple(parent.state_dict())
+ assert not any("._ln1_module." in key for key in keys_before)
+ with torch.no_grad():
+ bridge(torch.randint(0, bridge.cfg.d_vocab, (1, 4)))
+ keys_after = tuple(parent.state_dict())
+
+ assert keys_after == keys_before
+ assert not any("._ln1_module." in key for key in keys_after)
+
+
+def test_nested_joint_qkv_bridge_strict_round_trip() -> None:
+ cfg = GPT2Config(
+ vocab_size=32,
+ n_positions=16,
+ n_embd=16,
+ n_layer=1,
+ n_head=2,
+ n_inner=32,
+ pad_token_id=0,
+ bos_token_id=1,
+ eos_token_id=2,
+ )
+ bridge = build_bridge_from_module(
+ GPT2LMHeadModel(cfg),
+ architecture="GPT2LMHeadModel",
+ hf_config=cfg,
+ )
+ parent = _parent_with_bridge(bridge)
+
+ checkpoint = {key: value.clone() for key, value in parent.state_dict().items()}
+ assert not any(".qkv." in key for key in checkpoint)
+ with torch.no_grad():
+ for parameter in parent.parameters():
+ parameter.zero_()
+
+ result = parent.load_state_dict(checkpoint, strict=True)
+
+ assert result.missing_keys == []
+ assert result.unexpected_keys == []
+ reloaded = parent.state_dict()
+ for key, value in checkpoint.items():
+ assert torch.equal(reloaded[key], value), f"{key} did not round-trip"
+
+
+def _filtered_joint_component(kind: str) -> torch.nn.Module:
+ filtered_child = LinearBridge(name=kind)
+ filtered_child.set_original_component(torch.nn.Linear(4, 8))
+ cfg = SimpleNamespace(n_heads=2, d_head=4)
+
+ if kind == "qkv":
+ qkv_component = JointQKVAttentionBridge(
+ name="attn",
+ config=cfg,
+ submodules={"qkv": filtered_child},
+ )
+ for child_name in ("q", "k", "v"):
+ getattr(qkv_component, child_name).set_original_component(torch.nn.Linear(4, 4))
+ return qkv_component
+ gate_up_component = JointGateUpMLPBridge(
+ name="mlp",
+ config=cfg,
+ submodules={"gate_up": filtered_child},
+ )
+ gate_up_component.add_module("gate_up", filtered_child)
+ gate_up_component.gate.set_original_component(torch.nn.Linear(4, 4))
+ getattr(gate_up_component, "in").set_original_component(torch.nn.Linear(4, 4))
+ return gate_up_component
+
+
+@pytest.mark.parametrize("filtered_child_name", ["qkv", "gate_up"])
+def test_filtered_joint_component_strict_round_trip(filtered_child_name: str) -> None:
+ component = _filtered_joint_component(filtered_child_name)
+ filtered_child = component.get_submodule(filtered_child_name)
+ checkpoint = {key: value.clone() for key, value in component.state_dict().items()}
+
+ assert checkpoint
+ assert not any(key.startswith(f"{filtered_child_name}.") for key in checkpoint)
+ with torch.no_grad():
+ for parameter in component.parameters():
+ parameter.zero_()
+
+ result = component.load_state_dict(checkpoint, strict=True)
+
+ assert result.missing_keys == []
+ assert result.unexpected_keys == []
+ reloaded = component.state_dict()
+ for key, value in checkpoint.items():
+ assert torch.equal(reloaded[key], value), f"{key} did not round-trip"
+ for parameter in filtered_child.parameters():
+ assert torch.count_nonzero(parameter) == 0
diff --git a/tests/unit/model_bridge/test_state_dict_round_trip.py b/tests/unit/model_bridge/test_state_dict_round_trip.py
new file mode 100644
index 0000000000..b0d707d89f
--- /dev/null
+++ b/tests/unit/model_bridge/test_state_dict_round_trip.py
@@ -0,0 +1,196 @@
+"""Regression tests for TransformerBridge.state_dict()/load_state_dict() round-tripping (#1587).
+
+state_dict() emits TL-renamed keys (e.g. "blocks.0.attn.q.weight"), but
+load_state_dict() only matched raw native parameter names, so a
+state_dict() -> load_state_dict() round trip silently loaded nothing and
+strict=True was silently downgraded to strict=False.
+"""
+from __future__ import annotations
+
+import pytest
+import torch
+
+from transformer_lens.config import TransformerBridgeConfig
+from transformer_lens.model_bridge import TransformerBridge
+
+
+def _native_cfg(**overrides) -> TransformerBridgeConfig:
+ base = dict(
+ d_model=32,
+ d_head=16,
+ n_heads=2,
+ n_layers=2,
+ n_ctx=8,
+ d_vocab=16,
+ d_mlp=64,
+ act_fn="gelu",
+ normalization_type="LN",
+ seed=0,
+ )
+ base.update(overrides)
+ return TransformerBridgeConfig(**base)
+
+
+def test_native_round_trip_overwrites_params_not_a_noop():
+ bridge = TransformerBridge.boot_native(_native_cfg())
+
+ sd = {k: v.clone() for k, v in bridge.state_dict().items()}
+ assert sd, "state_dict() returned no TL-format keys"
+
+ with torch.no_grad():
+ for p in bridge.parameters():
+ p.zero_()
+ assert all((p == 0).all() for p in bridge.parameters())
+
+ bridge.load_state_dict(sd, strict=True)
+
+ # Compare against the snapshot directly rather than asserting "not all
+ # zero" - LayerNorm bias legitimately initializes to all-zero, so that
+ # check would pass even for a param that never got reloaded.
+ reloaded = bridge.state_dict()
+ for key, value in sd.items():
+ assert torch.equal(reloaded[key], value), f"{key} did not round-trip"
+
+
+def test_native_strict_true_raises_on_missing_key():
+ bridge = TransformerBridge.boot_native(_native_cfg())
+ sd = bridge.state_dict()
+ incomplete = dict(sd)
+ incomplete.pop(next(iter(incomplete)))
+
+ with pytest.raises(RuntimeError, match="Missing key"):
+ bridge.load_state_dict(incomplete, strict=True)
+
+
+def test_native_strict_true_raises_on_unexpected_key():
+ bridge = TransformerBridge.boot_native(_native_cfg())
+ sd = dict(bridge.state_dict())
+ sd["totally.bogus.key"] = torch.zeros(1)
+
+ with pytest.raises(RuntimeError, match="Unexpected key"):
+ bridge.load_state_dict(sd, strict=True)
+
+
+def test_native_strict_false_does_not_raise_on_partial_dict():
+ bridge = TransformerBridge.boot_native(_native_cfg())
+ sd = bridge.state_dict()
+ first_key = next(iter(sd))
+ partial = {first_key: sd[first_key]}
+
+ result = bridge.load_state_dict(partial, strict=False)
+ assert result.unexpected_keys == []
+ assert len(result.missing_keys) > 0
+
+
+def test_native_raw_keys_still_load_tracr_style():
+ """boot_native's own raw parameter names must keep loading directly,
+ mirroring tracr's make_tracr_transformer_bridge_state_dict compatibility
+ contract (utilities/tracr.py)."""
+ bridge = TransformerBridge.boot_native(_native_cfg())
+ raw_sd = {k: v.clone() for k, v in bridge.original_model.state_dict().items()}
+
+ with torch.no_grad():
+ for p in bridge.parameters():
+ p.zero_()
+
+ bridge.load_state_dict(raw_sd, strict=True)
+
+ reloaded_raw = bridge.original_model.state_dict()
+ for key, value in raw_sd.items():
+ assert torch.equal(reloaded_raw[key], value), f"{key} did not round-trip"
+
+
+def test_native_clean_key_dict_with_partial_aliases_does_not_raise_strict():
+ """A complete raw-HF-format-style state dict (clean keys, _original_component
+ stripped) writes only one alias per shared-storage TL key -- boot_native's own
+ wrapping produces this aliasing internally (e.g. "layers.0.ln1.weight" is
+ reachable via two different _original_component paths onto the same
+ Parameter), not just gpt2's c_attn split. Since aliases of the same tensor
+ share storage, writing any one of them is sufficient; strict=True must not
+ report the other, unwritten aliases as missing."""
+ bridge = TransformerBridge.boot_native(_native_cfg())
+
+ raw_sd = bridge.original_model.state_dict()
+ clean_to_actuals: dict[str, list[str]] = {}
+ for actual_key in raw_sd:
+ if actual_key != "_original_component":
+ clean_to_actuals.setdefault(actual_key.replace("._original_component", ""), []).append(
+ actual_key
+ )
+ assert any(len(keys) > 1 for keys in clean_to_actuals.values()), (
+ "fixture assumption broken: expected boot_native to have some "
+ "clean key reachable through more than one actual path"
+ )
+
+ # One representative actual key's value per clean key, same shape as a
+ # real raw-HF-format checkpoint (no duplicate paths for the same param).
+ clean_sd = {clean_key: raw_sd[keys[0]].clone() for clean_key, keys in clean_to_actuals.items()}
+
+ with torch.no_grad():
+ for p in bridge.parameters():
+ p.zero_()
+
+ result = bridge.load_state_dict(clean_sd, strict=True)
+ assert result.missing_keys == []
+ assert result.unexpected_keys == []
+
+ reloaded_raw = bridge.original_model.state_dict()
+ for clean_key, actual_keys in clean_to_actuals.items():
+ value = clean_sd[clean_key]
+ for actual_key in actual_keys:
+ assert torch.equal(
+ reloaded_raw[actual_key], value
+ ), f"{actual_key} (alias of {clean_key}) did not round-trip"
+
+
+@pytest.mark.slow
+def test_boot_transformers_round_trip_matches_forward_pass():
+ """GPT-2's Conv1D-combined attention makes the bridge's q/k/v components
+ storage-sharing VIEWS into c_attn, not independent parameters - so this is
+ the case that actually exercises convert_hf_key_to_tl_key's HF-name
+ renaming, not just identity passthrough like boot_native does."""
+ bridge = TransformerBridge.boot_transformers("gpt2", device="cpu")
+ bridge.eval()
+
+ torch.manual_seed(0)
+ tokens = torch.randint(0, 1000, (1, 8))
+ with torch.no_grad():
+ logits_before = bridge(tokens).clone()
+
+ sd = {k: v.clone() for k, v in bridge.state_dict().items()}
+
+ with torch.no_grad():
+ for p in bridge.parameters():
+ p.zero_()
+
+ bridge.load_state_dict(sd, strict=True)
+
+ with torch.no_grad():
+ logits_after = bridge(tokens).clone()
+
+ max_diff = (logits_before - logits_after).abs().max().item()
+ assert torch.allclose(
+ logits_before, logits_after, atol=1e-5
+ ), f"round trip did not restore forward-pass output: max diff={max_diff:.3e}"
+
+
+@pytest.mark.slow
+def test_boot_transformers_clean_key_dict_does_not_raise_strict():
+ """Reported review case on real gpt2: a complete raw-HF-format-style state
+ dict (clean keys) writes only one alias per shared-storage TL key, since
+ gpt2's split q/k/v are views into c_attn reachable via multiple actual
+ paths. strict=True previously raised ~337 false "missing key" errors even
+ though the load fully restores the forward pass."""
+ bridge = TransformerBridge.boot_transformers("gpt2", device="cpu")
+
+ raw_sd = bridge.original_model.state_dict()
+ clean_sd = {
+ actual_key.replace("._original_component", ""): value.clone()
+ for actual_key, value in raw_sd.items()
+ if actual_key != "_original_component"
+ }
+ assert len(clean_sd) < len(raw_sd), "fixture assumption broken: expected some aliasing on gpt2"
+
+ result = bridge.load_state_dict(clean_sd, strict=True)
+ assert result.missing_keys == []
+ assert result.unexpected_keys == []
diff --git a/tests/unit/model_bridge/test_tokenizer_reassignment.py b/tests/unit/model_bridge/test_tokenizer_reassignment.py
new file mode 100644
index 0000000000..b32d9f6140
--- /dev/null
+++ b/tests/unit/model_bridge/test_tokenizer_reassignment.py
@@ -0,0 +1,128 @@
+"""Tests for tokenizer reassignment wiring."""
+
+import pytest
+from transformers import AutoTokenizer
+
+from transformer_lens.config import TransformerBridgeConfig
+from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter
+from transformer_lens.model_bridge.bridge import TransformerBridge
+
+
+class MockAdapter(ArchitectureAdapter):
+ """Minimal adapter for testing."""
+
+ def __init__(self, cfg: TransformerBridgeConfig):
+ super().__init__(cfg)
+ self.component_mapping = {"embed": None}
+
+
+def _bare_bridge(adapter, tokenizer):
+ """Bridge stand-in replaying __init__'s tokenizer sequence without a model.
+
+ Full construction needs a wrapped model; the wiring under test only reads
+ adapter/cfg, so build a bare instance the way
+ test_generation_benchmark_mechanics does.
+ """
+ import torch.nn as nn
+
+ bridge = object.__new__(TransformerBridge)
+ nn.Module.__init__(bridge)
+ bridge.adapter = adapter
+ bridge.cfg = adapter.cfg
+ bridge._tokenizer = None
+ if tokenizer is not None:
+ bridge.tokenizer = tokenizer
+ if bridge.cfg.d_vocab_out == -1:
+ bridge.cfg.d_vocab_out = bridge.cfg.d_vocab
+ return bridge
+
+
+class TestTokenizerReassignment:
+ """Test that tokenizer reassignment re-runs wiring logic."""
+
+ @pytest.fixture
+ def base_cfg(self) -> TransformerBridgeConfig:
+ return TransformerBridgeConfig(
+ d_model=768,
+ d_head=64,
+ n_layers=12,
+ n_ctx=1024,
+ d_vocab=-1, # Will be inferred from tokenizer
+ d_mlp=3072,
+ n_heads=12,
+ )
+
+ @pytest.fixture
+ def gpt2_tokenizer(self):
+ """GPT-2 tokenizer (does not prepend BOS by default)."""
+ return AutoTokenizer.from_pretrained("gpt2")
+
+ @pytest.fixture
+ def llama_style_tokenizer(self):
+ """A tokenizer that prepends BOS (using gpt-neox as example)."""
+ tok = AutoTokenizer.from_pretrained("EleutherAI/gpt-neox-20b")
+ return tok
+
+ def test_initial_tokenizer_sets_d_vocab(self, base_cfg, gpt2_tokenizer):
+ """Test that initial tokenizer assignment sets d_vocab."""
+ adapter = MockAdapter(base_cfg)
+ bridge = _bare_bridge(adapter, gpt2_tokenizer)
+
+ # GPT-2 vocab size is 50257
+ assert bridge.cfg.d_vocab == 50257
+ assert bridge.cfg.d_vocab_out == 50257
+
+ def test_reassignment_updates_d_vocab(self, base_cfg, gpt2_tokenizer, llama_style_tokenizer):
+ """Test that reassigning tokenizer updates d_vocab."""
+ adapter = MockAdapter(base_cfg)
+ bridge = _bare_bridge(adapter, gpt2_tokenizer)
+
+ old_d_vocab = bridge.cfg.d_vocab
+
+ bridge.tokenizer = llama_style_tokenizer
+
+ # GPT-NeoX has a different vocab size than GPT-2
+ assert bridge.cfg.d_vocab != old_d_vocab
+ assert bridge.cfg.d_vocab_out == bridge.cfg.d_vocab
+
+ def test_reassignment_updates_bos_flag(self, base_cfg, gpt2_tokenizer, llama_style_tokenizer):
+ """Test that reassigning tokenizer updates tokenizer_prepends_bos."""
+ adapter = MockAdapter(base_cfg)
+ bridge = _bare_bridge(adapter, gpt2_tokenizer)
+
+ gpt2_bos = bridge.cfg.tokenizer_prepends_bos
+
+ bridge.tokenizer = llama_style_tokenizer
+
+ neox_bos = bridge.cfg.tokenizer_prepends_bos
+ # The flags should be properly detected (actual values depend on tokenizer behavior)
+ assert isinstance(neox_bos, bool)
+
+ def test_reassignment_to_none_preserves_config(self, base_cfg, gpt2_tokenizer):
+ """Test that setting tokenizer to None doesn't crash."""
+ adapter = MockAdapter(base_cfg)
+ bridge = _bare_bridge(adapter, gpt2_tokenizer)
+
+ old_d_vocab = bridge.cfg.d_vocab
+
+ bridge.tokenizer = None
+
+ assert bridge.tokenizer is None
+ assert bridge.cfg.d_vocab == old_d_vocab # Preserved from previous tokenizer
+
+ def test_tokenizer_property_returns_tokenizer(self, base_cfg, gpt2_tokenizer):
+ """Test that the tokenizer property returns the stored tokenizer."""
+ adapter = MockAdapter(base_cfg)
+ bridge = _bare_bridge(adapter, gpt2_tokenizer)
+
+ assert bridge.tokenizer is not None
+ assert hasattr(bridge.tokenizer, "encode")
+
+ def test_no_tokenizer_at_init(self, base_cfg):
+ """Test that bridge can be created without tokenizer."""
+ base_cfg.d_vocab = 50257 # Set explicitly since no tokenizer
+ adapter = MockAdapter(base_cfg)
+ bridge = _bare_bridge(adapter, None)
+
+ assert bridge.tokenizer is None
+ assert bridge.cfg.d_vocab == 50257
diff --git a/tests/unit/test_activation_cache.py b/tests/unit/test_activation_cache.py
new file mode 100644
index 0000000000..9debaf4a30
--- /dev/null
+++ b/tests/unit/test_activation_cache.py
@@ -0,0 +1,63 @@
+import pytest
+import torch
+
+from transformer_lens import ActivationCache
+from transformer_lens.config import TransformerBridgeConfig
+from transformer_lens.model_bridge import TransformerBridge
+
+
+@pytest.fixture(scope="module", params=["LN", "RMS"])
+def activation_cache(request: pytest.FixtureRequest) -> ActivationCache:
+ cfg = TransformerBridgeConfig(
+ n_layers=2,
+ d_model=16,
+ n_ctx=8,
+ d_head=4,
+ n_heads=4,
+ d_vocab=32,
+ act_fn="gelu",
+ normalization_type=request.param,
+ )
+ with torch.random.fork_rng(devices=[]):
+ torch.manual_seed(0)
+ model = TransformerBridge.boot_native(cfg)
+ tokens = torch.tensor(
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ]
+ )
+ _, cache = model.run_with_cache(tokens)
+ return cache
+
+
+@pytest.mark.parametrize("layer", [1, -1], ids=["cached-scale", "recomputed-final-ln"])
+@pytest.mark.parametrize(
+ "pos_slice", [None, (1, 3), -1], ids=["all-positions", "position-slice", "scalar-position"]
+)
+@pytest.mark.parametrize("apply_ln", [False, True], ids=["raw", "normalized"])
+def test_batchless_accumulated_resid_matches_batched_row(
+ activation_cache: ActivationCache,
+ layer: int,
+ pos_slice: tuple[int, int] | int | None,
+ apply_ln: bool,
+) -> None:
+ batch_index = 1
+ batched = activation_cache.accumulated_resid(
+ layer=layer,
+ pos_slice=pos_slice,
+ apply_ln=apply_ln,
+ )
+
+ batchless_cache = activation_cache.apply_slice_to_batch_dim(batch_index)
+ assert not batchless_cache.has_batch_dim
+ batchless = batchless_cache.accumulated_resid(
+ layer=layer,
+ pos_slice=pos_slice,
+ apply_ln=apply_ln,
+ )
+
+ expected = batched[:, batch_index]
+ assert batchless.shape == expected.shape
+ torch.testing.assert_close(batchless, expected)
diff --git a/tests/unit/test_activation_cache_batch_dim.py b/tests/unit/test_activation_cache_batch_dim.py
new file mode 100644
index 0000000000..d5521761f9
--- /dev/null
+++ b/tests/unit/test_activation_cache_batch_dim.py
@@ -0,0 +1,69 @@
+"""Batch-dim handling for caches holding broadcast / position-indexed entries.
+
+T5-style caches mix genuinely batched activations with entries whose leading dim
+is not the batch: broadcast entries (leading 1) and position-indexed entries
+(leading dim = seq len, e.g. relative position bias). remove_batch_dim and
+apply_slice_to_batch_dim must not corrupt those.
+"""
+
+from __future__ import annotations
+
+import pytest
+import torch
+
+from transformer_lens.ActivationCache import ActivationCache
+
+
+def _mixed_batch1_cache() -> ActivationCache:
+ return ActivationCache(
+ {
+ "blocks.0.hook_resid_pre": torch.randn(1, 5, 4),
+ "blocks.0.hook_pattern": torch.randn(1, 2, 5, 5),
+ # Position-indexed entry: leading dim is seq len, not batch.
+ "blocks.0.attn.hook_rel_pos_bias": torch.randn(5, 5),
+ },
+ model=None,
+ has_batch_dim=True,
+ )
+
+
+def test_remove_batch_dim_leaves_position_indexed_entries_alone() -> None:
+ cache = _mixed_batch1_cache()
+ bias_before = cache["blocks.0.attn.hook_rel_pos_bias"].clone()
+
+ cache.remove_batch_dim()
+
+ assert cache["blocks.0.hook_resid_pre"].shape == (5, 4)
+ assert cache["blocks.0.hook_pattern"].shape == (2, 5, 5)
+ assert torch.equal(cache["blocks.0.attn.hook_rel_pos_bias"], bias_before)
+
+
+def test_remove_batch_dim_refuses_true_batch_gt_1_despite_broadcast_entry() -> None:
+ cache = ActivationCache(
+ {
+ "blocks.0.hook_resid_pre": torch.randn(2, 5, 4),
+ "blocks.0.hook_mlp_out": torch.randn(2, 5, 4),
+ "hook_pos_indices": torch.randn(1, 5),
+ },
+ model=None,
+ has_batch_dim=True,
+ )
+ with pytest.raises(AssertionError, match="batch size 2"):
+ cache.remove_batch_dim()
+
+
+def test_apply_slice_to_batch_dim_skips_broadcast_entries() -> None:
+ cache = ActivationCache(
+ {
+ "blocks.0.hook_resid_pre": torch.randn(3, 5, 4),
+ "blocks.0.hook_mlp_out": torch.randn(3, 5, 4),
+ "hook_pos_indices": torch.randn(1, 5),
+ },
+ model=None,
+ has_batch_dim=True,
+ )
+ sliced = cache.apply_slice_to_batch_dim((1, 3))
+
+ assert sliced["blocks.0.hook_resid_pre"].shape == (2, 5, 4)
+ assert sliced["blocks.0.hook_mlp_out"].shape == (2, 5, 4)
+ assert sliced["hook_pos_indices"].shape == (1, 5)
diff --git a/tests/unit/test_gradient_mismatch_grading.py b/tests/unit/test_gradient_mismatch_grading.py
new file mode 100644
index 0000000000..c7cabae231
--- /dev/null
+++ b/tests/unit/test_gradient_mismatch_grading.py
@@ -0,0 +1,130 @@
+"""Gradient mismatches are graded on scale-aware statistics, not worst-case elements.
+
+Registering backward hooks forces normalization off HF's native autograd onto the
+python-norm path, which shifts results at float-rounding scale. Elementwise
+`allclose` graded that shift as failure: one element of 55,296 crossing the
+tolerance scored the same as a real divergence. The band accepts a mismatch only
+when it is both diffuse (rel_l2) and localized to a handful of elements (COUNT,
+not fraction — detection guarantees count >= 1, so a fractional guard was
+unsatisfiable below 10,000 elements and re-created the false failure on
+gemma-3-270m's 6,912-element MQA hook_rot_k).
+"""
+
+import pytest
+
+from transformer_lens.benchmarks.backward_gradients import (
+ OVER_TOLERANCE_MAX_ELEMENTS,
+ REL_L2_TOLERANCE,
+ gradient_mismatch_is_numerical_noise,
+)
+
+# Measured bridge-vs-HT fallback noise. Only mismatches the detection gate records
+# can reach the classifier, so every fixture has count >= 1 (frac-zero rows from
+# the original study never reach it and prove nothing).
+NOISE = [
+ ("Qwen3-0.6B rot_q [1,27,16,128]", 1.714e-05, 1),
+ ("gemma-3-270m rot_k [1,27,1,256] (MQA, 6912 elements)", 5.0e-05, 1),
+]
+# Injected bugs of known severity on the Qwen3 rot_q tensor (55,296 elements).
+BUGS = [
+ ("one head scaled 1%", 2.97e-03, 758),
+ ("uniform scale 0.1%", 1.00e-03, 432),
+ ("60 elements +50", 1.09e-02, 60),
+]
+
+
+@pytest.mark.parametrize("label,rel_l2,count", NOISE)
+def test_fallback_noise_is_accepted(label: str, rel_l2: float, count: int) -> None:
+ assert gradient_mismatch_is_numerical_noise(rel_l2, count), label
+
+
+@pytest.mark.parametrize("label,rel_l2,count", BUGS)
+def test_real_divergence_is_rejected(label: str, rel_l2: float, count: int) -> None:
+ assert not gradient_mismatch_is_numerical_noise(rel_l2, count), label
+
+
+def test_thresholds_keep_a_margin_on_both_dimensions() -> None:
+ """Both guards must sit clear of both populations, not graze either."""
+ worst_noise_rel = max(rel for _, rel, _ in NOISE)
+ best_bug_rel = min(rel for _, rel, _ in BUGS)
+ assert worst_noise_rel * 2 <= REL_L2_TOLERANCE, worst_noise_rel
+ assert best_bug_rel >= REL_L2_TOLERANCE * 5, best_bug_rel
+ worst_noise_count = max(count for _, _, count in NOISE)
+ best_bug_count = min(count for _, _, count in BUGS)
+ assert worst_noise_count * 3 <= OVER_TOLERANCE_MAX_ELEMENTS + 1, worst_noise_count
+ assert best_bug_count >= OVER_TOLERANCE_MAX_ELEMENTS * 10, best_bug_count
+
+
+def test_localized_divergence_is_rejected_even_when_diffuse_error_is_small() -> None:
+ """The count is the guard: a concentrated error rel_l2 would dilute still fails."""
+ assert not gradient_mismatch_is_numerical_noise(REL_L2_TOLERANCE / 10, 60)
+ assert not gradient_mismatch_is_numerical_noise(1e-2, 1)
+
+
+def test_zero_reference_divergence_is_rejected() -> None:
+ """A zero reference gradient with a nonzero bridge gradient records
+ rel_l2=inf in the benchmark body — maximal divergence, never noise."""
+ assert not gradient_mismatch_is_numerical_noise(float("inf"), 1)
+
+
+def test_boundaries_are_inclusive() -> None:
+ """Pin the <= contract on both dimensions."""
+ assert gradient_mismatch_is_numerical_noise(REL_L2_TOLERANCE, OVER_TOLERANCE_MAX_ELEMENTS)
+ assert not gradient_mismatch_is_numerical_noise(
+ REL_L2_TOLERANCE * 1.01, OVER_TOLERANCE_MAX_ELEMENTS
+ )
+ assert not gradient_mismatch_is_numerical_noise(
+ REL_L2_TOLERANCE, OVER_TOLERANCE_MAX_ELEMENTS + 1
+ )
+
+
+class TestMismatchStatsHelper:
+ """The stats the classifier consumes, driven with synthetic tensors."""
+
+ def test_zero_reference_nonzero_bridge_is_infinite(self) -> None:
+ import torch
+
+ from transformer_lens.benchmarks.backward_gradients import (
+ gradient_mismatch_stats,
+ )
+
+ stats = gradient_mismatch_stats(torch.ones(100), torch.zeros(100), 0.2, 3e-4)
+ assert stats["rel_l2"] == float("inf")
+ assert not gradient_mismatch_is_numerical_noise(stats["rel_l2"], stats["over_count"])
+
+ def test_matching_zeros_agree(self) -> None:
+ import torch
+
+ from transformer_lens.benchmarks.backward_gradients import (
+ gradient_mismatch_stats,
+ )
+
+ stats = gradient_mismatch_stats(torch.zeros(100), torch.zeros(100), 0.2, 3e-4)
+ assert stats["rel_l2"] == 0.0 and stats["over_count"] == 0
+
+ def test_over_count_matches_detection_predicate(self) -> None:
+ import torch
+
+ from transformer_lens.benchmarks.backward_gradients import (
+ gradient_mismatch_stats,
+ )
+
+ ref = torch.full((6912,), 258.0) # gemma-3-270m rot_k scale
+ bridge = ref.clone()
+ bridge[0] += 0.5 # one element past atol + rtol*|ref|
+ stats = gradient_mismatch_stats(bridge, ref, 0.2, 3e-4)
+ assert stats["over_count"] == 1
+ assert gradient_mismatch_is_numerical_noise(stats["rel_l2"], stats["over_count"]), stats
+
+
+class TestFp32GradientPredicate:
+ def test_reduced_precision_needs_upcast(self) -> None:
+ import torch
+
+ from transformer_lens.benchmarks.backward_gradients import needs_fp32_gradients
+
+ assert needs_fp32_gradients(torch.bfloat16)
+ assert needs_fp32_gradients(torch.float16)
+ assert not needs_fp32_gradients(torch.float32)
+ assert not needs_fp32_gradients(torch.float64)
+ assert not needs_fp32_gradients(None)
diff --git a/tests/unit/test_lm_utils.py b/tests/unit/test_lm_utils.py
new file mode 100644
index 0000000000..ca3e6a8975
--- /dev/null
+++ b/tests/unit/test_lm_utils.py
@@ -0,0 +1,54 @@
+"""Unit tests for language-model loss and accuracy helpers."""
+
+from __future__ import annotations
+
+import pytest
+import torch
+from beartype.roar import BeartypeCallHintParamViolation
+
+from transformer_lens.utilities.lm_utils import lm_accuracy, lm_cross_entropy_loss
+
+
+def test_lm_cross_entropy_loss_rejects_mismatched_attention_mask() -> None:
+ logits = torch.zeros(1, 2, 3)
+ tokens = torch.tensor([[0, 1]])
+ attention_mask = torch.ones(1, 5, dtype=torch.long)
+
+ with pytest.raises(
+ (AssertionError, BeartypeCallHintParamViolation),
+ match="attention_mask|axis 'pos'",
+ ):
+ lm_cross_entropy_loss(logits, tokens, attention_mask)
+
+
+def test_lm_cross_entropy_loss_masks_nan_transition() -> None:
+ logits = torch.tensor(
+ [
+ [
+ [torch.nan, torch.nan],
+ [0.0, 0.0],
+ [0.0, 0.0],
+ ]
+ ]
+ )
+ tokens = torch.tensor([[0, 1, 0]])
+ attention_mask = torch.tensor([[0, 1, 1]])
+
+ per_token = lm_cross_entropy_loss(logits, tokens, attention_mask, per_token=True)
+ scalar = lm_cross_entropy_loss(logits, tokens, attention_mask)
+ expected = torch.log(torch.tensor(2.0))
+
+ torch.testing.assert_close(per_token, torch.stack((expected.new_zeros(()), expected))[None])
+ torch.testing.assert_close(scalar, expected)
+ assert torch.isfinite(per_token).all()
+ assert torch.isfinite(scalar)
+
+
+def test_lm_accuracy_per_token_returns_bool_pos_minus_one() -> None:
+ logits = torch.zeros(2, 4, 3)
+ tokens = torch.tensor([[0, 1, 2, 0], [2, 1, 0, 2]])
+
+ accuracy = lm_accuracy(logits, tokens, per_token=True)
+
+ assert accuracy.dtype is torch.bool
+ assert accuracy.shape == (2, 3)
diff --git a/tests/unit/test_moe_fold_guard.py b/tests/unit/test_moe_fold_guard.py
new file mode 100644
index 0000000000..f6afddf5c5
--- /dev/null
+++ b/tests/unit/test_moe_fold_guard.py
@@ -0,0 +1,104 @@
+"""MoE models fold their norms like dense models do.
+
+History: HT once switched MoE models to the gain-less *Pre norm while its
+process step refused to fold the experts, silently dropping the gains entirely
+(OLMoE sat 20.5 off HF in log-softmax, 0% argmax). A guard then refused folding
+outright, which diverged from the bridge (which folds) at unembed.hook_in. The
+shared ProcessWeights fold handles the router and every expert's W_in/W_gate,
+and HT-with-MoE-fold measures bit-exact against HF (0.0000 log-softmax,
+100% argmax on OLMoE-1B-7B), so folding is simply enabled.
+"""
+
+from types import SimpleNamespace
+from unittest import mock
+
+import pytest
+
+from transformer_lens.loading_from_pretrained import get_pretrained_model_config
+
+
+def _moe_config(architecture: str, num_experts: int) -> SimpleNamespace:
+ return SimpleNamespace(
+ architectures=[architecture],
+ hidden_size=64,
+ num_attention_heads=4,
+ num_key_value_heads=4,
+ intermediate_size=128,
+ num_hidden_layers=2,
+ max_position_embeddings=512,
+ rms_norm_eps=1e-6,
+ vocab_size=100,
+ hidden_act="silu",
+ rope_theta=500000.0,
+ sliding_window=None,
+ num_experts=num_experts,
+ num_local_experts=num_experts,
+ num_experts_per_tok=2,
+ norm_topk_prob=False,
+ tie_word_embeddings=False,
+ initializer_range=0.02,
+ )
+
+
+@pytest.mark.parametrize(
+ "model_name,architecture,n_experts",
+ [
+ ("allenai/OLMoE-1B-7B-0924", "OlmoeForCausalLM", 64),
+ ("mistralai/Mixtral-8x7B-v0.1", "MixtralForCausalLM", 8),
+ ],
+)
+@mock.patch("transformer_lens.loading_from_pretrained.AutoConfig")
+def test_moe_folds_like_dense(mock_auto_config, caplog, model_name, architecture, n_experts):
+ """fold_ln=True switches MoE to the folded *Pre norm, same as dense, without
+ a refusal warning — the gains are folded into router and experts, not dropped."""
+ mock_auto_config.from_pretrained.return_value = _moe_config(architecture, n_experts)
+ with caplog.at_level("WARNING"):
+ cfg = get_pretrained_model_config(model_name, fold_ln=True)
+ assert cfg.num_experts == n_experts
+ assert cfg.normalization_type == "RMSPre", cfg.normalization_type
+ assert not any("MoE" in r.getMessage() for r in caplog.records), [
+ r.getMessage() for r in caplog.records
+ ]
+
+
+def test_process_weights_folds_moe_and_preserves_outputs() -> None:
+ """The fold must engage (norms swap to *Pre) and be equivalence-preserving on
+ a MoE model — a skipped fold leaves RMS modules; a broken fold moves logits."""
+ import torch
+
+ from transformer_lens import HookedTransformer, HookedTransformerConfig
+ from transformer_lens.components import RMSNormPre
+
+ torch.manual_seed(0)
+ cfg = HookedTransformerConfig(
+ n_layers=2,
+ d_model=32,
+ d_head=8,
+ n_heads=4,
+ d_mlp=64,
+ d_vocab=50,
+ n_ctx=16,
+ act_fn="silu",
+ normalization_type="RMS",
+ gated_mlp=True,
+ num_experts=4,
+ experts_per_token=2,
+ )
+ model = HookedTransformer(cfg)
+ with torch.no_grad():
+ for name, param in model.named_parameters():
+ torch.nn.init.normal_(param, std=0.2)
+ # Non-trivial gains, or folding is a vacuous multiply-by-one.
+ for block in model.blocks:
+ block.ln1.w.copy_(torch.rand_like(block.ln1.w) + 0.5)
+ block.ln2.w.copy_(torch.rand_like(block.ln2.w) + 0.5)
+ model.ln_final.w.copy_(torch.rand_like(model.ln_final.w) + 0.5)
+ model.eval()
+ tokens = torch.randint(0, 50, (1, 8))
+ with torch.no_grad():
+ before = model(tokens)
+ model.process_weights_(fold_ln=True, center_writing_weights=False, center_unembed=False)
+ assert isinstance(model.blocks[0].ln2, RMSNormPre), type(model.blocks[0].ln2).__name__
+ with torch.no_grad():
+ after = model(tokens)
+ torch.testing.assert_close(after, before, atol=1e-4, rtol=1e-4)
diff --git a/tests/unit/test_post_norm_processing_guards.py b/tests/unit/test_post_norm_processing_guards.py
new file mode 100644
index 0000000000..deb7684a04
--- /dev/null
+++ b/tests/unit/test_post_norm_processing_guards.py
@@ -0,0 +1,119 @@
+"""LN folding and writing-weight centering must stay off for post-norm decoders.
+
+Both transforms assume the norm gain sits on a sublayer's INPUT. OLMo 2/3 apply
+ln1/ln2 to the sublayer OUTPUT, so folding is the wrong algebra: on the real
+allenai/Olmo-3-1025-7B it moved log-softmax by 19.73 and dropped argmax agreement
+with HF to 0%. The guard existed but named only OLMo 2, so OLMo 3 folded silently.
+"""
+
+from types import SimpleNamespace
+from unittest import mock
+
+import pytest
+import torch
+
+from transformer_lens.loading_from_pretrained import get_pretrained_model_config
+from transformer_lens.utilities.architectures import POST_NORM_ARCHITECTURES
+from transformer_lens.weight_processing import ProcessWeights
+
+
+def _tl_config(architecture: str):
+ from transformer_lens import HookedTransformerConfig
+
+ return HookedTransformerConfig(
+ n_layers=0,
+ d_model=8,
+ n_ctx=16,
+ d_head=4,
+ n_heads=2,
+ d_vocab=10,
+ act_fn="silu",
+ normalization_type="RMS",
+ original_architecture=architecture,
+ positional_embedding_type="rotary",
+ )
+
+
+POST_NORM_MODELS = [
+ ("allenai/Olmo-3-1025-7B", "Olmo3ForCausalLM"),
+ ("allenai/OLMo-2-0425-1B", "Olmo2ForCausalLM"),
+]
+
+
+def _olmo_hf_config(architecture: str) -> SimpleNamespace:
+ return SimpleNamespace(
+ architectures=[architecture],
+ hidden_size=64,
+ num_attention_heads=4,
+ num_key_value_heads=4,
+ intermediate_size=128,
+ num_hidden_layers=4,
+ max_position_embeddings=512,
+ rms_norm_eps=1e-6,
+ vocab_size=100,
+ hidden_act="silu",
+ rope_theta=500000.0,
+ layer_types=["sliding_attention"] * 3 + ["full_attention"],
+ sliding_window=4096,
+ initializer_range=0.02,
+ tie_word_embeddings=False,
+ rope_parameters={
+ "sliding_attention": {"rope_type": "default", "rope_theta": 500000.0},
+ "full_attention": {"rope_type": "default", "rope_theta": 500000.0},
+ },
+ )
+
+
+@pytest.mark.parametrize("model_name,architecture", POST_NORM_MODELS)
+@mock.patch("transformer_lens.loading_from_pretrained.AutoConfig")
+def test_fold_ln_is_refused(mock_auto_config, caplog, model_name, architecture) -> None:
+ mock_auto_config.from_pretrained.return_value = _olmo_hf_config(architecture)
+ with caplog.at_level("WARNING"):
+ get_pretrained_model_config(model_name, fold_ln=True)
+ assert any(
+ "fold_ln=True is incompatible" in record.getMessage() for record in caplog.records
+ ), [r.getMessage() for r in caplog.records]
+
+
+@mock.patch("transformer_lens.loading_from_pretrained.AutoConfig")
+def test_pre_norm_architecture_still_folds(mock_auto_config, caplog) -> None:
+ """Negative control: the guard must not disable folding for everyone."""
+ mock_auto_config.from_pretrained.return_value = SimpleNamespace(
+ architectures=["LlamaForCausalLM"],
+ hidden_size=64,
+ num_attention_heads=4,
+ num_key_value_heads=4,
+ intermediate_size=128,
+ num_hidden_layers=2,
+ max_position_embeddings=512,
+ rms_norm_eps=1e-6,
+ vocab_size=100,
+ hidden_act="silu",
+ rope_theta=10000.0,
+ )
+ with caplog.at_level("WARNING"):
+ get_pretrained_model_config("01-ai/Yi-6B", fold_ln=True)
+ assert not any("fold_ln=True is incompatible" in r.getMessage() for r in caplog.records)
+
+
+@pytest.mark.parametrize("architecture", sorted(POST_NORM_ARCHITECTURES))
+def test_embeddings_are_not_centered(architecture) -> None:
+ """The first attention's input is un-normed, so centering W_E shifts a residual
+ stream nothing re-normalizes."""
+ torch.manual_seed(0)
+ embedding = torch.randn(10, 8)
+ state = {"embed.W_E": embedding.clone()}
+ cfg = _tl_config(architecture)
+ out = ProcessWeights.center_writing_weights(state, cfg)
+ torch.testing.assert_close(out["embed.W_E"], embedding)
+
+
+def test_embeddings_are_centered_for_pre_norm() -> None:
+ """Engagement check: centering is a no-op above only because of the guard."""
+ torch.manual_seed(0)
+ embedding = torch.randn(10, 8)
+ state = {"embed.W_E": embedding.clone()}
+ cfg = _tl_config("LlamaForCausalLM")
+ out = ProcessWeights.center_writing_weights(state, cfg)
+ assert not torch.allclose(out["embed.W_E"], embedding)
+ torch.testing.assert_close(out["embed.W_E"].mean(-1), torch.zeros(10), atol=1e-6, rtol=0)
diff --git a/tests/unit/test_train_config_isolation.py b/tests/unit/test_train_config_isolation.py
new file mode 100644
index 0000000000..0107d35694
--- /dev/null
+++ b/tests/unit/test_train_config_isolation.py
@@ -0,0 +1,37 @@
+"""train() must not mutate the caller's config (device/wandb defaults)."""
+
+from __future__ import annotations
+
+import torch
+from torch.utils.data import Dataset
+
+from transformer_lens import HookedTransformer, HookedTransformerConfig
+from transformer_lens.train import HookedTransformerTrainConfig, train
+
+
+def test_train_leaves_caller_config_untouched() -> None:
+ model = HookedTransformer(
+ HookedTransformerConfig(
+ n_layers=1, d_model=16, d_head=8, n_heads=2, n_ctx=8, d_vocab=16, act_fn="gelu"
+ )
+ )
+
+ class _TokensDataset(Dataset):
+ def __len__(self) -> int:
+ return 1
+
+ def __getitem__(self, idx: int) -> dict:
+ return {"tokens": torch.tensor([1, 2, 3, 4])}
+
+ dataset = _TokensDataset()
+ config = HookedTransformerTrainConfig(
+ num_epochs=1,
+ batch_size=1,
+ lr=1e-3,
+ seed=0,
+ device=None,
+ )
+
+ train(model, config, dataset)
+
+ assert config.device is None, "train() wrote its resolved device onto the caller's config"
diff --git a/tests/unit/tools/model_registry/test_clear_hf_cache.py b/tests/unit/tools/model_registry/test_clear_hf_cache.py
new file mode 100644
index 0000000000..b9047a736d
--- /dev/null
+++ b/tests/unit/tools/model_registry/test_clear_hf_cache.py
@@ -0,0 +1,30 @@
+"""_clear_hf_cache must never delete the pinned Phase-4 judge: the sweep clears
+the HF cache after every model family, and re-downloading the judge each time
+defeats the batch preload."""
+
+import pytest
+
+pytest.importorskip("transformers")
+
+
+def test_clear_hf_cache_preserves_judge_snapshot(tmp_path, monkeypatch):
+ from pathlib import Path
+
+ from transformer_lens.benchmarks.text_quality import JUDGE_MODEL_ID
+ from transformer_lens.tools.model_registry import verify_models
+
+ hub = tmp_path / ".cache" / "huggingface" / "hub"
+ judge_dir = hub / ("models--" + JUDGE_MODEL_ID.replace("/", "--")) / "blobs"
+ other_dir = hub / "models--someone--other-model" / "blobs"
+ judge_dir.mkdir(parents=True)
+ other_dir.mkdir(parents=True)
+ judge_blob = judge_dir / "aaaa"
+ other_blob = other_dir / "bbbb"
+ judge_blob.write_bytes(b"judge-weights")
+ other_blob.write_bytes(b"other-weights")
+
+ monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path))
+ verify_models._clear_hf_cache(quiet=True)
+
+ assert judge_blob.exists(), "judge blob must survive the per-family cache clear"
+ assert not other_blob.exists(), "non-judge blobs must still be cleared"
diff --git a/tests/unit/tools/model_registry/test_prompt_profiles.py b/tests/unit/tools/model_registry/test_prompt_profiles.py
new file mode 100644
index 0000000000..00c4a4dcd7
--- /dev/null
+++ b/tests/unit/tools/model_registry/test_prompt_profiles.py
@@ -0,0 +1,231 @@
+"""Profile resolution: curation must beat unreliable Hub metadata (observed
+mis-tags: mt0-base as text-generation, conversational on base models,
+unordered Helsinki-NLP language tags), and gaps must fall through safely."""
+
+import pytest
+
+pytest.importorskip("transformers")
+
+from transformer_lens.benchmarks.text_quality_profiles import (
+ DEFAULT_PROFILE,
+ HFSignals,
+ ProfileSpec,
+ extract_languages,
+ profile_from_hf_signals,
+ resolve_profile,
+)
+
+
+class TestPrecedence:
+ def test_override_beats_architecture_rule(self, monkeypatch):
+ """A real override-vs-arch clash: Pegasus's arch rule says
+ summarization; a per-model override must still win."""
+ from transformer_lens.benchmarks import text_quality_profiles as tp
+
+ monkeypatch.setitem(tp.MODEL_PROFILE_OVERRIDES, "google/pegasus-xsum", "continuation")
+ spec = resolve_profile("google/pegasus-xsum", "PegasusForConditionalGeneration")
+ assert spec == ProfileSpec("continuation")
+
+ def test_override_beats_signals(self):
+ """long-t5 override must win even when signals disagree."""
+ spec = resolve_profile(
+ "google/long-t5-tglobal-base",
+ "LongT5ForConditionalGeneration",
+ signals=HFSignals(pipeline_tag="summarization"),
+ )
+ assert spec == ProfileSpec("task:denoise")
+
+ def test_architecture_rule_beats_fetched_tag(self):
+ """Pegasus is summarization by architecture even if the Hub tag lies."""
+ spec = resolve_profile(
+ "google/pegasus-xsum",
+ "PegasusForConditionalGeneration",
+ signals=HFSignals(pipeline_tag="text-generation"),
+ )
+ assert spec.kind == "task:summarization"
+
+ def test_mt0_mistag_resolves_to_instruction(self):
+ """Hub tags mt0-base text-generation; the override must correct it."""
+ spec = resolve_profile(
+ "bigscience/mt0-base",
+ "MT5ForConditionalGeneration",
+ signals=HFSignals(pipeline_tag="text-generation"),
+ )
+ assert spec.kind == "task:instruction"
+
+ def test_fetched_tag_fills_gap(self):
+ """BART has no arch rule (checkpoint-dependent); the Hub tag decides."""
+ spec = resolve_profile(
+ "facebook/bart-large-cnn",
+ "BartForConditionalGeneration",
+ signals=HFSignals(pipeline_tag="summarization", languages=("en",)),
+ )
+ assert spec.kind == "task:summarization"
+
+ def test_null_pipeline_tag_falls_through_to_arch_rule(self):
+ """m2m100 has pipeline_tag=None on the Hub; the arch rule must hold."""
+ spec = resolve_profile(
+ "facebook/m2m100_418M",
+ "M2M100ForConditionalGeneration",
+ signals=HFSignals(pipeline_tag=None),
+ )
+ assert spec.kind == "task:translation"
+
+ def test_stored_registry_value_used_when_no_signals(self):
+ spec = resolve_profile("some/model", "GPT2LMHeadModel", "continuation@fr")
+ assert spec == ProfileSpec("continuation", "fr")
+
+ def test_unknown_seq2seq_defaults_to_denoise_not_continuation(self):
+ """An unlabelled seq2seq cannot continue text; denoising is its only prompt."""
+ spec = resolve_profile("someone/random-t5", "T5GemmaForConditionalGeneration")
+ assert spec.kind == "task:denoise"
+
+ def test_unknown_causal_lm_defaults_to_continuation(self):
+ assert resolve_profile("someone/random-lm", "LlamaForCausalLM") == DEFAULT_PROFILE
+
+
+class TestHubSignals:
+ def test_conversational_tag_alone_is_not_chat(self):
+ """HF adds `conversational` to ANY repo shipping a chat template, base
+ models included (observed on Qwen/Qwen2.5-0.5B)."""
+ spec = profile_from_hf_signals(
+ "Qwen/Qwen2.5-0.5B",
+ "Qwen2ForCausalLM",
+ HFSignals(pipeline_tag="text-generation", tags=("conversational",)),
+ )
+ assert spec is not None and spec.kind == "continuation"
+
+ def test_code_tag_maps_to_code_continuation(self):
+ spec = profile_from_hf_signals(
+ "bigcode/some-model", "GPTBigCodeForCausalLM", HFSignals(tags=("code",))
+ )
+ assert spec == ProfileSpec("continuation", "code")
+
+ def test_marian_direction_from_model_id_not_tag_order(self):
+ """Helsinki-NLP language tags are unordered; only opus-mt-{src}-{tgt}
+ carries the direction."""
+ spec = resolve_profile(
+ "Helsinki-NLP/opus-mt-nl-en",
+ "MarianMTModel",
+ signals=HFSignals(languages=("en", "nl")), # tag order is wrong on purpose
+ )
+ assert (spec.src, spec.lang) == ("nl", "en")
+
+ def test_translation_tag_without_direction_returns_none(self):
+ """Tag lists are unordered: guessing a pair risks a reversed or
+ identity direction, so signals alone must abstain (resolution then
+ falls through to overrides/arch rules — t5-small still lands on
+ en-de via its override)."""
+ spec = profile_from_hf_signals(
+ "google-t5/t5-small",
+ "T5ForConditionalGeneration",
+ HFSignals(pipeline_tag="translation"),
+ )
+ assert spec is None
+ resolved = resolve_profile(
+ "google-t5/t5-small",
+ "T5ForConditionalGeneration",
+ signals=HFSignals(pipeline_tag="translation"),
+ )
+ assert (resolved.src, resolved.lang) == ("en", "de")
+
+ def test_translation_tag_with_en_and_target_infers_pair(self):
+ spec = profile_from_hf_signals(
+ "someone/en-fr-translator",
+ "BartForConditionalGeneration",
+ HFSignals(pipeline_tag="translation", languages=("en", "fr")),
+ )
+ assert spec is not None and (spec.src, spec.lang) == ("en", "fr")
+
+
+class TestLanguageExtraction:
+ def test_handles_str_and_list(self):
+ assert extract_languages("fr", []) == ("fr",)
+ assert extract_languages(["de", "en"], []) == ("de", "en")
+
+ def test_merges_iso_tags_and_drops_noise(self):
+ langs = extract_languages(
+ None, ["pytorch", "transformers", "nl", "marian", "safetensors", "en"]
+ )
+ assert langs == ("nl", "en")
+
+ def test_caps_at_eight(self):
+ many = ["fr", "es", "de", "it", "nl", "pt", "ru", "ja", "ar", "hi"]
+ assert len(extract_languages(many, [])) == 8
+
+
+class TestProfileSpecGrammar:
+ def test_round_trip(self):
+ for text in ("continuation", "continuation@code", "chat@fr", "task:translation@en-de"):
+ assert str(ProfileSpec.parse(text)) == text
+
+ def test_rejects_unknown_kind(self):
+ with pytest.raises(ValueError):
+ ProfileSpec.parse("poetry@en")
+
+ def test_rejects_translation_without_pair(self):
+ with pytest.raises(ValueError):
+ ProfileSpec.parse("task:translation@de")
+
+
+class TestChatIdHeuristic:
+ """Instruct/chat/-it ids resolve to the chat profile — nothing else can
+ (the conversational tag covers base models; no arch distinguishes tuned
+ from base). Runtime downgrades template-less models back to continuation."""
+
+ def test_instruct_id_resolves_chat(self):
+ spec = resolve_profile("Qwen/Qwen2.5-0.5B-Instruct", "Qwen2ForCausalLM")
+ assert spec.kind == "chat"
+
+ def test_it_suffix_resolves_chat(self):
+ spec = resolve_profile("google/gemma-2-2b-it", "Gemma2ForCausalLM")
+ assert spec.kind == "chat"
+
+ def test_base_id_stays_continuation(self):
+ assert resolve_profile("Qwen/Qwen2.5-0.5B", "Qwen2ForCausalLM").kind == "continuation"
+ assert resolve_profile("google/gemma-2-2b", "Gemma2ForCausalLM").kind == "continuation"
+
+ def test_override_still_beats_chat_heuristic(self, monkeypatch):
+ from transformer_lens.benchmarks import text_quality_profiles as tp
+
+ monkeypatch.setitem(tp.MODEL_PROFILE_OVERRIDES, "someone/model-instruct", "continuation@fr")
+ spec = resolve_profile("someone/model-instruct", "LlamaForCausalLM")
+ assert spec == ProfileSpec("continuation", "fr")
+
+ def test_arch_rule_beats_chat_heuristic(self):
+ """A Blenderbot-style arch keeps its rule even with a chatty id."""
+ spec = resolve_profile("someone/blenderbot-chat", "BlenderbotForConditionalGeneration")
+ assert spec.kind == "chat" # via arch rule, not id — same outcome
+ spec2 = resolve_profile("someone/opus-mt-nl-en-chat", "MarianMTModel")
+ assert spec2.kind == "task:translation"
+
+
+def test_chat_heuristic_keeps_stored_language():
+ """The id heuristic fixes the kind; a stored chat@fr must survive
+ resolve->writeback (it was flattened to chat@en and clobbered)."""
+ from transformer_lens.benchmarks.text_quality_profiles import resolve_profile
+
+ spec = resolve_profile("org/model-7b-instruct", "LlamaForCausalLM", registry_profile="chat@fr")
+ assert str(spec) == "chat@fr"
+ # A stored non-chat profile does not hijack the heuristic.
+ spec = resolve_profile(
+ "org/model-7b-instruct", "LlamaForCausalLM", registry_profile="continuation@fr"
+ )
+ assert str(spec) == "chat"
+
+
+def test_non_english_denoise_is_a_coverage_gap():
+ """IndicBART's stale score was measured under a broken MBart profile;
+ until Indic denoise prompts exist, a non-en denoise profile must SKIP
+ (coverage gap), never score against English sentences."""
+ from transformer_lens.benchmarks.text_quality_profiles import (
+ ProfileSpec,
+ prompts_for,
+ resolve_profile,
+ )
+
+ assert str(resolve_profile("ai4bharat/IndicBART", "MBartForConditionalGeneration")) == (
+ "task:denoise@hi"
+ )
+ assert prompts_for(ProfileSpec("task:denoise", lang="hi")) is None
+ assert prompts_for(ProfileSpec("task:denoise", lang="en")) is not None
diff --git a/tests/unit/tools/model_registry/test_update_model_registry.py b/tests/unit/tools/model_registry/test_update_model_registry.py
new file mode 100644
index 0000000000..663457c2fe
--- /dev/null
+++ b/tests/unit/tools/model_registry/test_update_model_registry.py
@@ -0,0 +1,331 @@
+"""Regression tests for main_benchmark.update_model_registry.
+
+This path was a drifted mirror of verify_models' registry-writing logic: its
+phase dict stopped at phase 3 and it wrote STATUS_VERIFIED unconditionally,
+bypassing the provisional gate for --no-hf-reference runs. It now shares
+registry_io's extract_phase_scores / pass_status, so these tests pin the
+registry outcomes, not the internals.
+"""
+import json
+from types import SimpleNamespace
+
+import pytest
+
+from transformer_lens.benchmarks.main_benchmark import update_model_registry
+from transformer_lens.benchmarks.utils import BenchmarkResult, BenchmarkSeverity
+from transformer_lens.tools.model_registry.registry_io import (
+ STATUS_FAILED,
+ STATUS_PROVISIONAL,
+ STATUS_VERIFIED,
+)
+
+ARCH = "GPT2LMHeadModel"
+
+
+def _result(phase, passed, name="forward_pass", severity=None, details=None):
+ if severity is None:
+ severity = BenchmarkSeverity.INFO if passed else BenchmarkSeverity.DANGER
+ return BenchmarkResult(
+ name=name,
+ severity=severity,
+ message="ok" if passed else "mismatch",
+ details=details,
+ passed=passed,
+ phase=phase,
+ )
+
+
+@pytest.fixture
+def registry_paths(tmp_path, monkeypatch):
+ """Point registry_io at temp files and stub the AutoConfig network call."""
+ from transformer_lens.tools.model_registry import registry_io
+
+ supported = {
+ "total_architectures": 1,
+ "total_models": 1,
+ "total_verified": 0,
+ "models": [
+ {
+ "architecture_id": ARCH,
+ "model_id": "seeded/model",
+ "status": 0,
+ "verified_date": None,
+ "metadata": None,
+ "note": None,
+ },
+ ],
+ }
+ supported_path = tmp_path / "supported_models.json"
+ supported_path.write_text(json.dumps(supported, indent=2))
+ history_path = tmp_path / "verification_history.json"
+
+ monkeypatch.setattr(registry_io, "_SUPPORTED_MODELS_PATH", supported_path)
+ monkeypatch.setattr(registry_io, "_VERIFICATION_HISTORY_PATH", history_path)
+ monkeypatch.setattr(
+ "transformers.AutoConfig.from_pretrained",
+ lambda *args, **kwargs: SimpleNamespace(architectures=[ARCH]),
+ )
+ return supported_path, history_path
+
+
+def _entry(supported_path, model_id):
+ data = json.loads(supported_path.read_text())
+ return next(m for m in data["models"] if m["model_id"] == model_id), data
+
+
+class TestProvisionalGate:
+ def test_no_hf_reference_writes_provisional(self, registry_paths):
+ supported_path, history_path = registry_paths
+ results = [_result(1, True)]
+
+ assert update_model_registry("new/model", results, use_hf_reference=False)
+
+ entry, data = _entry(supported_path, "new/model")
+ assert entry["status"] == STATUS_PROVISIONAL
+ assert entry["note"].startswith("Structural only (no HF reference)")
+ assert data["total_verified"] == 0
+ assert data["total_provisional"] == 1
+ # No history record: VerificationHistory.is_verified() treats any
+ # record as verified — the second "counts as verified" path.
+ assert not history_path.exists()
+
+ def test_hf_reference_writes_verified(self, registry_paths):
+ supported_path, history_path = registry_paths
+ results = [_result(1, True)]
+
+ assert update_model_registry("new/model", results, use_hf_reference=True)
+
+ entry, data = _entry(supported_path, "new/model")
+ assert entry["status"] == STATUS_VERIFIED
+ assert data["total_verified"] == 1
+ history = json.loads(history_path.read_text())
+ assert history["records"][-1]["model_id"] == "new/model"
+ assert history["records"][-1]["verified_by"] == "main_benchmark"
+
+ def test_default_is_conservative_provisional(self, registry_paths):
+ supported_path, _ = registry_paths
+ update_model_registry("new/model", [_result(1, True)])
+ entry, _ = _entry(supported_path, "new/model")
+ assert entry["status"] == STATUS_PROVISIONAL
+
+
+class TestPhaseCoverage:
+ def test_phase9_contributes_phase9_score(self, registry_paths):
+ # The drifted mirror's {1: [], 2: [], 3: []} dict silently dropped P9.
+ supported_path, _ = registry_paths
+ results = [_result(1, True), _result(9, True, name="vision_forward")]
+
+ update_model_registry("new/model", results, use_hf_reference=True)
+
+ entry, _ = _entry(supported_path, "new/model")
+ assert entry["phase9_score"] == 100.0
+ assert entry["status"] == STATUS_VERIFIED
+
+ def test_unrun_phases_preserve_existing_scores(self, registry_paths):
+ # The old path wrote None for unrun phases, clobbering prior scores.
+ supported_path, _ = registry_paths
+ update_model_registry("seeded/model", [_result(2, True)], use_hf_reference=True)
+ entry, _ = _entry(supported_path, "seeded/model")
+ assert entry["phase2_score"] == 100.0
+
+ update_model_registry("seeded/model", [_result(1, True)], use_hf_reference=True)
+ entry, _ = _entry(supported_path, "seeded/model")
+ assert entry["phase1_score"] == 100.0
+ assert entry["phase2_score"] == 100.0
+
+
+class TestThresholdGate:
+ def test_failing_scores_write_failed_not_verified(self, registry_paths):
+ # The drifted mirror wrote VERIFIED even for all-fail runs.
+ supported_path, _ = registry_paths
+ results = [_result(1, False, name="logits_equivalence")]
+
+ update_model_registry("seeded/model", results, use_hf_reference=True)
+
+ entry, data = _entry(supported_path, "seeded/model")
+ assert entry["status"] == STATUS_FAILED
+ assert "Below threshold" in entry["note"]
+ assert data["total_verified"] == 0
+
+
+class TestPromptProfileWriteback:
+ """The Phase-4 profile actually used must land in the registry sparsely:
+ non-default profiles are recorded, the default writes no key at all (the
+ registry JSON is served to the docs site; 15k default keys are dead weight)."""
+
+ def _p4(self, profile):
+ return _result(
+ 4,
+ True,
+ name="text_quality",
+ details={"score": 91.0, "prompt_profile": profile},
+ )
+
+ def test_prompt_profile_written_from_p4_details(self, registry_paths):
+ supported_path, _ = registry_paths
+ update_model_registry(
+ "seeded/model",
+ [_result(1, True), self._p4("task:translation@en-de")],
+ use_hf_reference=True,
+ )
+ entry, _ = _entry(supported_path, "seeded/model")
+ assert entry["prompt_profile"] == "task:translation@en-de"
+ # Key order: sparse key sits right after note, before phase scores.
+ keys = list(entry)
+ assert keys.index("prompt_profile") == keys.index("note") + 1
+
+ def test_default_profile_not_written(self, registry_paths):
+ supported_path, _ = registry_paths
+ update_model_registry(
+ "seeded/model",
+ [_result(1, True), self._p4("continuation")],
+ use_hf_reference=True,
+ )
+ entry, _ = _entry(supported_path, "seeded/model")
+ assert "prompt_profile" not in entry
+
+ def test_existing_profile_survives_profileless_rerun(self, registry_paths):
+ """A later run without a P4 result must not clobber the stored profile."""
+ supported_path, _ = registry_paths
+ update_model_registry(
+ "seeded/model",
+ [_result(1, True), self._p4("chat@fr")],
+ use_hf_reference=True,
+ )
+ update_model_registry("seeded/model", [_result(1, True)], use_hf_reference=True)
+ entry, _ = _entry(supported_path, "seeded/model")
+ assert entry["prompt_profile"] == "chat@fr"
+
+ def test_default_profile_clears_stale_nondefault(self, registry_paths):
+ """A model re-resolved to the default must lose its old sparse key —
+ otherwise a stale 'chat@fr' misdescribes how the score was produced."""
+ supported_path, _ = registry_paths
+ update_model_registry(
+ "seeded/model",
+ [_result(1, True), self._p4("chat@fr")],
+ use_hf_reference=True,
+ )
+ update_model_registry(
+ "seeded/model",
+ [_result(1, True), self._p4("continuation")],
+ use_hf_reference=True,
+ )
+ entry, _ = _entry(supported_path, "seeded/model")
+ assert "prompt_profile" not in entry
+
+ def test_new_entry_append_carries_profile(self, registry_paths):
+ """The append (model-not-in-registry) branch must also write the sparse
+ key, positioned after note."""
+ supported_path, _ = registry_paths
+ update_model_registry(
+ "unseeded/model",
+ [_result(1, True), self._p4("task:summarization")],
+ use_hf_reference=True,
+ )
+ entry, _ = _entry(supported_path, "unseeded/model")
+ assert entry["prompt_profile"] == "task:summarization"
+ keys = list(entry)
+ assert keys.index("prompt_profile") == keys.index("note") + 1
+
+
+class TestP4ScoringVersionStamp:
+ """phase4_score is a mixed-scale column (old GPT-2 scale vs pinned-judge
+ ratio scale); every P4-bearing write must stamp the scale it measured on,
+ and writes without a P4 result must not touch an existing stamp."""
+
+ def test_p4_write_stamps_current_version(self, registry_paths):
+ from transformer_lens.benchmarks.text_quality_profiles import P4_SCORING_VERSION
+ from transformer_lens.tools.model_registry import registry_io
+
+ supported_path, _ = registry_paths
+ registry_io.update_model_status(
+ "seeded/model",
+ "GPT2LMHeadModel",
+ registry_io.STATUS_VERIFIED,
+ phase_scores={1: 100.0, 4: 91.0},
+ )
+ entry, _ = _entry(supported_path, "seeded/model")
+ assert entry["p4_scoring_version"] == P4_SCORING_VERSION
+
+ def test_no_p4_write_preserves_existing_stamp(self, registry_paths):
+ from transformer_lens.tools.model_registry import registry_io
+
+ supported_path, _ = registry_paths
+ registry_io.update_model_status(
+ "seeded/model",
+ "GPT2LMHeadModel",
+ registry_io.STATUS_VERIFIED,
+ phase_scores={1: 100.0, 4: 91.0},
+ )
+ registry_io.update_model_status(
+ "seeded/model",
+ "GPT2LMHeadModel",
+ registry_io.STATUS_VERIFIED,
+ phase_scores={1: 100.0},
+ )
+ entry, _ = _entry(supported_path, "seeded/model")
+ assert entry["p4_scoring_version"] == 2
+ assert entry["phase4_score"] == 91.0
+
+ def test_old_scale_entry_has_no_stamp(self, registry_paths):
+ supported_path, _ = registry_paths
+ entry, _ = _entry(supported_path, "seeded/model")
+ assert "p4_scoring_version" not in entry
+
+ def test_new_entry_with_p4_is_stamped(self, registry_paths):
+ from transformer_lens.tools.model_registry import registry_io
+
+ supported_path, _ = registry_paths
+ registry_io.update_model_status(
+ "brand/new-model",
+ "GPT2LMHeadModel",
+ registry_io.STATUS_VERIFIED,
+ phase_scores={1: 100.0, 4: 77.0},
+ )
+ entry, _ = _entry(supported_path, "brand/new-model")
+ assert entry["p4_scoring_version"] == 2
+
+
+class TestPreservedIssueSuffix:
+ """A phases-1-4 pass must not overwrite tracked residue from phases it
+ did not re-run (gemma-2-2b-it's P3=95.5 unembed_centering note was
+ clobbered by a bare 'Core verification completed')."""
+
+ def test_sub100_score_from_unrun_phase_is_retained(self, registry_paths):
+ from transformer_lens.tools.model_registry import registry_io
+ from transformer_lens.tools.model_registry.verify_models import (
+ _preserved_issue_suffix,
+ )
+
+ registry_io.update_model_status(
+ "seeded/model",
+ "GPT2LMHeadModel",
+ registry_io.STATUS_VERIFIED,
+ phase_scores={1: 100.0, 3: 95.5},
+ )
+ assert _preserved_issue_suffix("seeded/model", [1, 4]) == (
+ " (prior issues retained: P3=95.5%)"
+ )
+ # Re-running the phase drops it from the suffix (the fresh score speaks).
+ assert _preserved_issue_suffix("seeded/model", [1, 3, 4]) == ""
+
+ def test_clean_entry_has_no_suffix(self, registry_paths):
+ from transformer_lens.tools.model_registry.verify_models import (
+ _preserved_issue_suffix,
+ )
+
+ assert _preserved_issue_suffix("seeded/model", [1, 4]) == ""
+
+
+def test_judge_overhead_not_charged_to_accelerator():
+ """The judge is CPU-pinned; charging its 2.5 GB to a cuda budget caused
+ spurious VRAM skips."""
+ from transformer_lens.tools.model_registry.verify_models import (
+ estimate_benchmark_memory_gb,
+ )
+
+ # Small model so the phase-4 peak (model + judge) is the max across phases.
+ cpu = estimate_benchmark_memory_gb(int(1e6), phases=[1, 4], device="cpu")
+ cuda = estimate_benchmark_memory_gb(int(1e6), phases=[1, 4], device="cuda")
+ assert cpu > 2.5
+ assert cuda < 0.1
diff --git a/tests/unit/tools/test_jacobian_lens.py b/tests/unit/tools/test_jacobian_lens.py
index f01ebe302b..9b83793725 100644
--- a/tests/unit/tools/test_jacobian_lens.py
+++ b/tests/unit/tools/test_jacobian_lens.py
@@ -4,7 +4,7 @@
from enum import IntEnum
from inspect import Parameter, signature
from types import SimpleNamespace
-from typing import Any
+from typing import Any, Optional, Sequence
import numpy as np
import pytest
@@ -21,6 +21,8 @@
from transformer_lens.tools.analysis import (
JacobianLens,
JSpaceDecomposition,
+ JSpaceOccupancy,
+ JSpaceVarianceProfile,
get_sparse_decomposition,
)
from transformer_lens.utilities.activation_functions import apply_softcap
@@ -1391,3 +1393,107 @@ def test_decompose_rejects_unfitted_layer(toy_model: _ToyBridge, fitted_lens: Ja
fitted_lens.decompose(
toy_model, torch.randn(toy_model.cfg.d_model), layer=N_LAYERS - 1, k=3
)
+
+
+def test_occupancy_on_toy_model_raw_activation(
+ toy_model: _ToyBridge, fitted_lens: JacobianLens
+) -> None:
+ """occupancy on a raw activation returns a JSpaceOccupancy with a count in [1, max_atoms]."""
+ layer = fitted_lens.source_layers[0]
+ activation = torch.randn(toy_model.cfg.d_model)
+ result = fitted_lens.occupancy(toy_model, activation, layer, max_atoms=5, seed=0)
+ assert isinstance(result, JSpaceOccupancy)
+ assert 1 <= result.occupancy <= 5
+ assert result.support.numel() == 5
+ assert result.marginal_captured_variance.shape == result.control_captured_variance.shape
+
+
+def test_occupancy_prompt_path_runs(toy_model: _ToyBridge, fitted_lens: JacobianLens) -> None:
+ """occupancy accepts a prompt plus position, running the model to fetch the activation."""
+ layer = fitted_lens.source_layers[0]
+ result = fitted_lens.occupancy(
+ toy_model, "a toy prompt", layer, position=-1, max_atoms=5, seed=0
+ )
+ assert isinstance(result, JSpaceOccupancy)
+ assert 1 <= result.occupancy <= 5
+
+
+def test_occupancy_rejects_unfitted_layer(toy_model: _ToyBridge, fitted_lens: JacobianLens) -> None:
+ """occupancy shares decompose's validation: an unfitted layer raises."""
+ with pytest.raises(ValueError):
+ fitted_lens.occupancy(toy_model, torch.randn(toy_model.cfg.d_model), layer=N_LAYERS - 1)
+
+
+def test_fraction_of_variance_on_toy_model(
+ toy_model: _ToyBridge, fitted_lens: JacobianLens
+) -> None:
+ """fraction_of_variance returns per-layer median and pooled ratios in [0, 1] over a corpus."""
+ profile = fitted_lens.fraction_of_variance(
+ toy_model, ["a toy prompt here", "another toy prompt goes here"], k=3, skip_first=0
+ )
+ assert isinstance(profile, JSpaceVarianceProfile)
+ assert profile.layers == list(fitted_lens.source_layers)
+ for layer in profile.layers:
+ assert 0.0 <= profile.median[layer] <= 1.0
+ assert 0.0 <= profile.pooled[layer] <= 1.0
+ assert profile.per_position[layer].numel() > 0
+
+
+def test_fraction_of_variance_rejects_unfitted_layer(
+ toy_model: _ToyBridge, fitted_lens: JacobianLens
+) -> None:
+ with pytest.raises(ValueError):
+ fitted_lens.fraction_of_variance(toy_model, "a toy prompt", layers=[N_LAYERS - 1])
+
+
+def test_fraction_of_variance_rejects_empty_corpus(
+ toy_model: _ToyBridge, fitted_lens: JacobianLens
+) -> None:
+ with pytest.raises(ValueError):
+ fitted_lens.fraction_of_variance(toy_model, [])
+
+
+@pytest.mark.parametrize(
+ "tokens",
+ [
+ torch.zeros(4, dtype=torch.long),
+ torch.zeros((2, 4), dtype=torch.long),
+ torch.zeros((1, 1, 4), dtype=torch.long),
+ ],
+ ids=["missing-batch-dimension", "multiple-prompts", "extra-dimension"],
+)
+def test_fraction_of_variance_rejects_invalid_token_shape(
+ toy_model: _ToyBridge, fitted_lens: JacobianLens, tokens: torch.Tensor
+) -> None:
+ with pytest.raises(
+ ValueError,
+ match=r"fraction_of_variance expects each tokenized prompt to have shape \[1, seq\]",
+ ):
+ fitted_lens.fraction_of_variance(toy_model, tokens, k=3, skip_first=0)
+
+
+@pytest.mark.parametrize("positions", [None, [0]], ids=["default-sampling", "explicit-positions"])
+def test_fraction_of_variance_rejects_negative_skip_first(
+ toy_model: _ToyBridge,
+ fitted_lens: JacobianLens,
+ positions: Optional[Sequence[int]],
+) -> None:
+ with pytest.raises(ValueError, match="skip_first must be non-negative"):
+ fitted_lens.fraction_of_variance(
+ toy_model, "a toy prompt", k=3, skip_first=-1, positions=positions
+ )
+
+
+def test_fraction_of_variance_yields_nan_when_no_positions_are_sampled(
+ toy_model: _ToyBridge, fitted_lens: JacobianLens
+) -> None:
+ """When ``skip_first`` exceeds every prompt's length no position is sampled, so each layer's
+ ``median`` and ``pooled`` are NaN and ``per_position`` is empty (the documented contract)."""
+ import math
+
+ profile = fitted_lens.fraction_of_variance(toy_model, "a toy prompt", k=3, skip_first=999)
+ assert profile.layers == list(fitted_lens.source_layers)
+ for layer in profile.layers:
+ assert math.isnan(profile.median[layer])
+ assert math.isnan(profile.pooled[layer])
+ assert profile.per_position[layer].numel() == 0
diff --git a/tests/unit/tools/test_jacobian_lens_decomposition.py b/tests/unit/tools/test_jacobian_lens_decomposition.py
index c448e5d80b..3b822166e7 100644
--- a/tests/unit/tools/test_jacobian_lens_decomposition.py
+++ b/tests/unit/tools/test_jacobian_lens_decomposition.py
@@ -12,10 +12,12 @@
from transformer_lens.tools.analysis.jacobian_lens_decomposition import (
JSpaceDecomposition,
+ JSpaceOccupancy,
_gradient_pursuit_step,
_nnls_tolerances,
_nonnegative_least_squares,
_validate_nnls_kkt,
+ estimate_occupancy,
get_sparse_decomposition,
)
@@ -753,3 +755,121 @@ def test_rejects_non_2d_dictionary():
def test_rejects_non_1d_target():
with pytest.raises(ValueError):
get_sparse_decomposition(torch.ones(2, 4), torch.eye(4), k=1)
+
+
+# --------------------------------------------------------------------------- #
+# Occupancy estimator (estimate_occupancy)
+# --------------------------------------------------------------------------- #
+@pytest.mark.parametrize("planted_atom_count", [1, 4, 7])
+def test_occupancy_recovers_planted_sparsity_on_orthonormal_dictionary(planted_atom_count):
+ """On an orthonormal dictionary a k-sparse planted target has occupancy exactly k: the real
+ greedy captures the planted atoms then saturates, so the point of maximum separation from the
+ random control lands at k."""
+ torch.manual_seed(0)
+ d_model = 24
+ dictionary = torch.linalg.qr(torch.randn(d_model, d_model)).Q # orthonormal atoms (rows)
+ target = torch.zeros(d_model)
+ for index in range(planted_atom_count):
+ target = target + (3.0 - 0.3 * index) * dictionary[2 * index + 1]
+
+ max_atoms = min(2 * planted_atom_count + 4, d_model)
+ result = estimate_occupancy(target, dictionary, max_atoms=max_atoms)
+
+ assert isinstance(result, JSpaceOccupancy)
+ assert result.occupancy == planted_atom_count
+ assert result.support.numel() == max_atoms
+
+
+def test_occupancy_increases_with_planted_sparsity():
+ """Occupancy tracks the planted sparsity: a 2-sparse target occupies fewer atoms than a
+ 9-sparse one on the same orthonormal dictionary (both recovered exactly)."""
+ torch.manual_seed(1)
+ d_model = 24
+ dictionary = torch.linalg.qr(torch.randn(d_model, d_model)).Q
+
+ def planted(count):
+ target = torch.zeros(d_model)
+ for index in range(count):
+ target = target + (3.0 - 0.2 * index) * dictionary[2 * index + 1]
+ return target
+
+ assert (
+ estimate_occupancy(planted(2), dictionary, max_atoms=20).occupancy
+ < estimate_occupancy(planted(9), dictionary, max_atoms=20).occupancy
+ )
+
+
+def test_occupancy_is_deterministic():
+ """Identical inputs and seed give identical results (the random control is seeded)."""
+ torch.manual_seed(2)
+ dictionary = torch.randn(40, 8)
+ target = torch.randn(8)
+ first = estimate_occupancy(target, dictionary, seed=0)
+ second = estimate_occupancy(target, dictionary, seed=0)
+ assert first.occupancy == second.occupancy
+ assert torch.equal(first.support, second.support)
+ assert torch.allclose(first.control_captured_variance, second.control_captured_variance)
+
+
+def test_occupancy_rejects_invalid_inputs():
+ dictionary = torch.eye(6)
+ with pytest.raises(ValueError, match="max_atoms must be between"):
+ estimate_occupancy(torch.ones(6), dictionary, max_atoms=0)
+ with pytest.raises(ValueError, match="max_atoms must be between"):
+ estimate_occupancy(torch.ones(6), dictionary, max_atoms=99)
+ with pytest.raises(ValueError, match="num_control_dictionaries must be at least 1"):
+ estimate_occupancy(torch.ones(6), dictionary, max_atoms=6, num_control_dictionaries=0)
+ with pytest.raises(ValueError, match="x must be 1-D of length"):
+ estimate_occupancy(torch.ones(5), dictionary) # target length != d_model
+ with pytest.raises(ValueError, match="x must be 1-D of length"):
+ estimate_occupancy(torch.ones(2, 6), dictionary) # non-1-D target
+ zero_norm_dictionary = torch.eye(6)
+ zero_norm_dictionary[2] = 0.0
+ with pytest.raises(ValueError, match="dictionary contains a non-finite or zero-norm atom"):
+ estimate_occupancy(torch.ones(6), zero_norm_dictionary, max_atoms=6)
+
+
+@pytest.mark.parametrize("invalid_value", [float("nan"), float("inf"), float("-inf")])
+def test_occupancy_rejects_non_finite_target(invalid_value):
+ target = torch.ones(6)
+ target[0] = invalid_value
+ with pytest.raises(ValueError, match="x contains non-finite entries"):
+ estimate_occupancy(target, torch.eye(6), max_atoms=6)
+
+
+def test_occupancy_rejects_zero_norm_target():
+ with pytest.raises(ValueError, match="x must have non-zero norm"):
+ estimate_occupancy(torch.zeros(6), torch.eye(6), max_atoms=6)
+
+
+@pytest.mark.parametrize("invalid_value", [float("nan"), float("inf"), float("-inf")])
+def test_occupancy_rejects_non_finite_dictionary(invalid_value):
+ dictionary = torch.eye(6)
+ dictionary[0, 0] = invalid_value
+ with pytest.raises(ValueError, match="dictionary contains non-finite entries"):
+ estimate_occupancy(torch.ones(6), dictionary, max_atoms=6)
+
+
+def test_occupancy_rejects_non_finite_dictionary_norm():
+ dictionary = torch.eye(6)
+ dictionary[0] = torch.finfo(torch.float32).max
+ with pytest.raises(ValueError, match="dictionary contains a non-finite or zero-norm atom"):
+ estimate_occupancy(torch.ones(6), dictionary, max_atoms=6)
+
+
+@pytest.mark.parametrize("complex_input", ["x", "dictionary"])
+def test_occupancy_rejects_complex_inputs(complex_input):
+ target = torch.ones(6)
+ dictionary = torch.eye(6)
+ if complex_input == "x":
+ target = target.to(torch.complex64)
+ else:
+ dictionary = dictionary.to(torch.complex64)
+ with pytest.raises(ValueError, match="x and dictionary must be real-valued"):
+ estimate_occupancy(target, dictionary, max_atoms=6)
+
+
+def test_occupancy_rejects_non_finite_target_norm():
+ target = torch.full((6,), torch.finfo(torch.float32).max)
+ with pytest.raises(ValueError, match="x must have finite norm"):
+ estimate_occupancy(target, torch.eye(6), max_atoms=6)
diff --git a/tests/unit/utilities/test_get_input_with_manually_prepended_bos.py b/tests/unit/utilities/test_get_input_with_manually_prepended_bos.py
new file mode 100644
index 0000000000..f11a38bac1
--- /dev/null
+++ b/tests/unit/utilities/test_get_input_with_manually_prepended_bos.py
@@ -0,0 +1,70 @@
+"""Tests for get_input_with_manually_prepended_bos when the tokenizer has no BOS token.
+
+``to_tokens`` reaches this helper whenever the caller wants a BOS that the tokenizer
+will not add on its own — ``prepend_bos and not cfg.tokenizer_prepends_bos``. For a
+tokenizer with no BOS token that condition is *correctly* true rather than stale:
+``detect_tokenizer_bos_eos`` requires a ``bos_token_id``, so it reports False for
+BERT and T5, and ``prepend_bos`` defaults to True. The helper then evaluated
+``None + input`` and raised ``TypeError: unsupported operand type(s) for +:
+'NoneType' and 'str'``, naming neither the tokenizer nor the flag.
+
+This is the prepend-side counterpart of #1628, which covers the removal side.
+"""
+
+from __future__ import annotations
+
+import pytest
+from transformers import AutoTokenizer
+
+from transformer_lens.utilities.tokenize_utils import (
+ get_input_with_manually_prepended_bos,
+)
+
+
+@pytest.fixture(
+ scope="module",
+ params=["google-bert/bert-base-cased", "google-t5/t5-small"],
+)
+def no_bos_tokenizer(request):
+ """BERT opens with [CLS] and T5 with nothing, so bos_token is None for both."""
+ tokenizer = AutoTokenizer.from_pretrained(request.param)
+ assert tokenizer.bos_token is None
+ return tokenizer
+
+
+@pytest.fixture(scope="module")
+def bos_tokenizer():
+ tokenizer = AutoTokenizer.from_pretrained("distilgpt2")
+ assert tokenizer.bos_token is not None
+ return tokenizer
+
+
+def test_no_bos_token_returns_string_unchanged(no_bos_tokenizer) -> None:
+ """There is no BOS to prepend, so the string must come back untouched."""
+ assert get_input_with_manually_prepended_bos(no_bos_tokenizer.bos_token, "hello world") == (
+ "hello world"
+ )
+
+
+def test_no_bos_token_returns_list_unchanged(no_bos_tokenizer) -> None:
+ """Same for the batched form — and no partially-prepended list."""
+ inputs = ["hello world", "second string"]
+
+ result = get_input_with_manually_prepended_bos(no_bos_tokenizer.bos_token, inputs)
+
+ assert result == ["hello world", "second string"]
+
+
+def test_a_real_bos_is_still_prepended_to_a_string(bos_tokenizer) -> None:
+ """The guard must not disturb the case the helper exists for."""
+ result = get_input_with_manually_prepended_bos(bos_tokenizer.bos_token, "hello world")
+
+ assert result == bos_tokenizer.bos_token + "hello world"
+
+
+def test_a_real_bos_is_still_prepended_to_a_list(bos_tokenizer) -> None:
+ bos = bos_tokenizer.bos_token
+
+ result = get_input_with_manually_prepended_bos(bos, ["hello world", "second string"])
+
+ assert result == [bos + "hello world", bos + "second string"]
diff --git a/tests/unit/utilities/test_get_tokens_with_bos_removed.py b/tests/unit/utilities/test_get_tokens_with_bos_removed.py
new file mode 100644
index 0000000000..75e51ad741
--- /dev/null
+++ b/tests/unit/utilities/test_get_tokens_with_bos_removed.py
@@ -0,0 +1,82 @@
+"""Tests for get_tokens_with_bos_removed when the tokenizer has no BOS token.
+
+Callers gate this helper on ``cfg.tokenizer_prepends_bos``. That flag is set by
+``detect_tokenizer_bos_eos``, which requires a ``bos_token_id`` — so a tokenizer
+with none should never reach here. It does when the flag is stale: a bridge built
+via ``build_bridge_from_module(tokenizer=None)`` keeps the config default of True,
+and the tokenizer setter only re-runs detection on *re*-assignment.
+
+Trusting a stale flag is not harmless. Under right padding the helper drops the
+first token unconditionally, which silently removes ``[CLS]`` from a BERT
+tokenizer's output; under left padding it compares tokens against ``None`` and
+raises an ``AttributeError`` naming neither the tokenizer nor the flag.
+"""
+
+from __future__ import annotations
+
+import pytest
+import torch
+from transformers import AutoTokenizer
+
+from transformer_lens.utilities.tokenize_utils import get_tokens_with_bos_removed
+
+
+@pytest.fixture(scope="module")
+def no_bos_tokenizer():
+ """BERT uses [CLS] rather than a BOS token, so bos_token_id is None."""
+ tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-cased")
+ assert tokenizer.bos_token_id is None
+ return tokenizer
+
+
+@pytest.fixture(scope="module")
+def bos_tokenizer():
+ tokenizer = AutoTokenizer.from_pretrained("distilgpt2")
+ assert tokenizer.bos_token_id is not None
+ return tokenizer
+
+
+@pytest.mark.parametrize("padding_side", ["left", "right"])
+def test_no_bos_token_returns_tokens_unchanged(no_bos_tokenizer, padding_side) -> None:
+ """There is no BOS to remove, so the tokens must come back untouched."""
+ no_bos_tokenizer.padding_side = padding_side
+ tokens = torch.tensor([[101, 19082, 1362, 102]])
+
+ result = get_tokens_with_bos_removed(no_bos_tokenizer, tokens)
+
+ torch.testing.assert_close(result, tokens)
+
+
+def test_no_bos_token_does_not_drop_cls_under_right_padding(no_bos_tokenizer) -> None:
+ """The damaging case: [CLS] is not a BOS token, and dropping it changes what
+ the model is asked to encode."""
+ no_bos_tokenizer.padding_side = "right"
+ tokens = no_bos_tokenizer("hello world", return_tensors="pt")["input_ids"]
+
+ result = get_tokens_with_bos_removed(no_bos_tokenizer, tokens)
+
+ assert result.shape == tokens.shape
+ assert result[0, 0].item() == no_bos_tokenizer.cls_token_id
+
+
+def test_no_bos_token_does_not_raise_under_left_padding(no_bos_tokenizer) -> None:
+ """Previously `(tokens == None).int()` — a Python bool, not a tensor."""
+ no_bos_tokenizer.padding_side = "left"
+ tokens = torch.tensor([[101, 19082, 1362, 102]])
+
+ result = get_tokens_with_bos_removed(no_bos_tokenizer, tokens)
+
+ assert result.shape == tokens.shape
+
+
+@pytest.mark.parametrize("padding_side", ["left", "right"])
+def test_a_real_bos_is_still_removed(bos_tokenizer, padding_side) -> None:
+ """The guard must not disturb the case the helper exists for."""
+ bos_tokenizer.padding_side = padding_side
+ bos = bos_tokenizer.bos_token_id
+ tokens = torch.tensor([[bos, 15496, 995]])
+
+ result = get_tokens_with_bos_removed(bos_tokenizer, tokens)
+
+ assert result.shape[-1] == tokens.shape[-1] - 1
+ assert bos not in result[0].tolist()
diff --git a/tests/unit/utilities/test_multi_gpu_unit.py b/tests/unit/utilities/test_multi_gpu_unit.py
index 5924c74762..f6b180b879 100644
--- a/tests/unit/utilities/test_multi_gpu_unit.py
+++ b/tests/unit/utilities/test_multi_gpu_unit.py
@@ -5,13 +5,17 @@
import pytest
import torch
+import torch.nn as nn
from transformer_lens.utilities import (
calculate_available_device_cuda_memory,
determine_available_memory_for_available_devices,
sort_devices_based_on_available_memory,
)
-from transformer_lens.utilities.multi_gpu import get_device_for_block_index
+from transformer_lens.utilities.multi_gpu import (
+ cast_floating_params_to_dtype,
+ get_device_for_block_index,
+)
def mock_available_devices(memory_stats: list[tuple[int, int]]):
@@ -129,3 +133,127 @@ def test_cpu_device_is_returned_unchanged(self):
cfg = _cuda_cfg(n_layers=62, n_devices=8)
result = get_device_for_block_index(30, cfg, device="cpu")
assert result.type == "cpu"
+
+
+class TestCastFloatingParamsToDtype:
+ """Regression tests for cast_floating_params_to_dtype.
+
+ See: https://github.com/TransformerLensOrg/TransformerLens/issues/1713
+ The function was casting quantizer-owned FP8 scale tensors (float8_e8m0fnu)
+ to bfloat16, which corrupts the weight/scale pair relationship and breaks
+ MXFP4 checkpoints.
+ """
+
+ def test_casts_standard_floats_to_target_dtype(self):
+ """Positive control: standard float dtypes should be cast."""
+ model = nn.Linear(4, 4)
+ model.weight = nn.Parameter(torch.zeros(4, 4, dtype=torch.float32))
+ cast_floating_params_to_dtype(model, torch.bfloat16)
+ assert model.weight.dtype == torch.bfloat16
+
+ def test_skips_params_already_at_target_dtype(self):
+ """Params already at target dtype are left untouched."""
+ model = nn.Linear(4, 4)
+ original = torch.zeros(4, 4, dtype=torch.bfloat16)
+ model.weight = nn.Parameter(original)
+ cast_floating_params_to_dtype(model, torch.bfloat16)
+ assert model.weight.dtype == torch.bfloat16
+ assert model.weight.data_ptr() == original.data_ptr()
+
+ def test_skips_non_floating_point_params(self):
+ """Integer params (packed quantized weights) are left untouched."""
+ model = nn.Linear(4, 4, bias=False)
+ model.weight = nn.Parameter(torch.zeros(4, 4, dtype=torch.int8), requires_grad=False)
+ cast_floating_params_to_dtype(model, torch.bfloat16)
+ assert model.weight.dtype == torch.int8
+
+ @pytest.mark.parametrize(
+ "fp8_dtype",
+ [
+ torch.float8_e4m3fn,
+ torch.float8_e5m2,
+ pytest.param(
+ getattr(torch, "float8_e8m0fnu", None),
+ marks=pytest.mark.skipif(
+ not hasattr(torch, "float8_e8m0fnu"), reason="torch < 2.7"
+ ),
+ ),
+ ],
+ )
+ def test_skips_one_byte_floats_fp8_scales(self, fp8_dtype):
+ """FP8 scale tensors must NOT be cast — they are quantizer-owned.
+
+ This is the key regression test for the MXFP4 bug: casting float8_e8m0fnu
+ scales to bfloat16 breaks the weight/scale pair relationship.
+ """
+ model = nn.Linear(4, 4, bias=False)
+ model.weight = nn.Parameter(torch.zeros(4, 4, dtype=fp8_dtype), requires_grad=False)
+ cast_floating_params_to_dtype(model, torch.bfloat16)
+ assert model.weight.dtype == fp8_dtype
+
+ def test_mixed_module_casts_selectively(self):
+ """A module with both standard and FP8 params: only standard params cast."""
+
+ class MixedModule(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.standard_weight = nn.Parameter(torch.zeros(4, 4, dtype=torch.float32))
+ self.fp8_scale = nn.Parameter(
+ torch.zeros(4, 4, dtype=torch.float8_e4m3fn), requires_grad=False
+ )
+ self.packed_weight = nn.Parameter(
+ torch.zeros(4, 4, dtype=torch.int8), requires_grad=False
+ )
+
+ model = MixedModule()
+ cast_floating_params_to_dtype(model, torch.bfloat16)
+
+ assert model.standard_weight.dtype == torch.bfloat16
+ assert model.fp8_scale.dtype == torch.float8_e4m3fn
+ assert model.packed_weight.dtype == torch.int8
+
+
+class TestMaybeCastFloatingParams:
+ """Tests for maybe_cast_floating_params helper.
+
+ See: https://github.com/TransformerLensOrg/TransformerLens/issues/1713
+ The helper wraps cast_floating_params_to_dtype with a quantization check,
+ skipping the cast entirely when the model has an active quantization_config.
+ """
+
+ def test_casts_unquantized_model(self):
+ """Unquantized models should have their params cast."""
+ from types import SimpleNamespace
+
+ from transformer_lens.utilities.multi_gpu import maybe_cast_floating_params
+
+ model = nn.Linear(4, 4)
+ model.weight = nn.Parameter(torch.zeros(4, 4, dtype=torch.float32))
+ model.config = SimpleNamespace(quantization_config=None)
+
+ maybe_cast_floating_params(model, torch.bfloat16)
+ assert model.weight.dtype == torch.bfloat16
+
+ def test_skips_quantized_model(self):
+ """Quantized models should NOT have their params cast."""
+ from types import SimpleNamespace
+
+ from transformer_lens.utilities.multi_gpu import maybe_cast_floating_params
+
+ model = nn.Linear(4, 4)
+ model.weight = nn.Parameter(torch.zeros(4, 4, dtype=torch.float32))
+ model.config = SimpleNamespace(quantization_config=SimpleNamespace(quant_method="mxfp4"))
+
+ maybe_cast_floating_params(model, torch.bfloat16)
+ assert model.weight.dtype == torch.float32 # NOT cast
+
+ def test_skips_model_without_config(self):
+ """Models without a config attribute should be cast (no quantization)."""
+ from transformer_lens.utilities.multi_gpu import maybe_cast_floating_params
+
+ model = nn.Linear(4, 4)
+ model.weight = nn.Parameter(torch.zeros(4, 4, dtype=torch.float32))
+ # No model.config attribute
+
+ maybe_cast_floating_params(model, torch.bfloat16)
+ assert model.weight.dtype == torch.bfloat16
diff --git a/tests/unit/utilities/test_tokenize_utils.py b/tests/unit/utilities/test_tokenize_utils.py
new file mode 100644
index 0000000000..0a5447f8e6
--- /dev/null
+++ b/tests/unit/utilities/test_tokenize_utils.py
@@ -0,0 +1,33 @@
+"""Tests for per-call padding-side overrides in tokenization utilities."""
+
+from copy import deepcopy
+
+import torch
+
+from transformer_lens import utils
+
+
+def test_attention_mask_uses_explicit_padding_side(gpt2_tokenizer) -> None:
+ tokenizer = deepcopy(gpt2_tokenizer)
+ tokenizer.pad_token = tokenizer.eos_token
+ tokenizer.padding_side = "right"
+ pad = tokenizer.pad_token_id
+ tokens = torch.tensor([[pad, pad, 10, 11], [pad, 20, 21, 22]])
+
+ mask = utils.get_attention_mask(tokenizer, tokens, prepend_bos=True, padding_side="left")
+
+ assert torch.equal(mask, torch.tensor([[0, 1, 1, 1], [1, 1, 1, 1]]))
+
+
+def test_bos_removal_uses_explicit_padding_side(gpt2_tokenizer) -> None:
+ tokenizer = deepcopy(gpt2_tokenizer)
+ tokenizer.pad_token = tokenizer.eos_token
+ tokenizer.bos_token = tokenizer.convert_ids_to_tokens(0)
+ tokenizer.padding_side = "right"
+ pad = tokenizer.pad_token_id
+ bos = tokenizer.bos_token_id
+ tokens = torch.tensor([[pad, bos, 10, 11], [bos, 20, 21, 22]])
+
+ result = utils.get_tokens_with_bos_removed(tokenizer, tokens, padding_side="left")
+
+ assert torch.equal(result, torch.tensor([[pad, 10, 11], [20, 21, 22]]))
diff --git a/transformer_lens/ActivationCache.py b/transformer_lens/ActivationCache.py
index 76992cf779..6c2a61122f 100644
--- a/transformer_lens/ActivationCache.py
+++ b/transformer_lens/ActivationCache.py
@@ -14,6 +14,7 @@ class first, including the examples, and then skimming the available methods. Yo
from __future__ import annotations
import logging
+from collections import Counter
from typing import (
TYPE_CHECKING,
Any,
@@ -152,6 +153,19 @@ def __init__(
# Note: model reference prevents garbage collection. Set cache.model = None if unneeded.
+ def _batch_size(self) -> int:
+ """The cache's batch size: the most common leading dim across entries.
+
+ Caches may hold non-batch entries alongside genuinely batched
+ activations — broadcast entries with a leading dim of 1 (e.g. the
+ bridge's position-index inputs) or position-indexed entries whose
+ leading dim is the sequence length (e.g. T5's relative position bias).
+ The batched activations vastly outnumber both, so the mode is the
+ reliable signal where max/min are not.
+ """
+ counts = Counter(v.size(0) for v in self.cache_dict.values() if v.ndim > 0)
+ return counts.most_common(1)[0][0] if counts else 1
+
def remove_batch_dim(self) -> ActivationCache:
"""Remove the Batch Dimension (if a single batch item).
@@ -159,16 +173,13 @@ def remove_batch_dim(self) -> ActivationCache:
The ActivationCache with the batch dimension removed.
"""
if self.has_batch_dim:
- # Skip tensors without a batch dimension
- has_batch_1 = any(v.size(0) == 1 for v in self.cache_dict.values())
+ batch_size = self._batch_size()
+ assert (
+ batch_size == 1
+ ), f"Cannot remove batch dimension from cache with batch size {batch_size}"
for key in self.cache_dict:
- if self.cache_dict[key].size(0) == 1:
+ if self.cache_dict[key].ndim > 0 and self.cache_dict[key].size(0) == 1:
self.cache_dict[key] = self.cache_dict[key][0]
- else:
- assert has_batch_1, (
- f"Cannot remove batch dimension from cache with batch size > 1, "
- f"for key {key} with shape {self.cache_dict[key].shape}"
- )
self.has_batch_dim = False
else:
logging.warning("Tried removing batch dimension after already having removed it.")
@@ -338,8 +349,16 @@ def apply_slice_to_batch_dim(self, batch_slice: Union[Slice, SliceInput]) -> Act
self.has_batch_dim or batch_slice.mode == "empty"
), "Cannot index into a cache without a batch dim"
still_has_batch_dim = (batch_slice.mode != "int") and self.has_batch_dim
+ batch_size = self._batch_size()
+ # Broadcast entries (leading dim 1 when the true batch is larger) are not
+ # batched — leave them untouched so slicing can't index out of bounds.
new_cache_dict = {
- name: batch_slice.apply(param, dim=0) for name, param in self.cache_dict.items()
+ name: (
+ batch_slice.apply(param, dim=0)
+ if param.ndim > 0 and param.size(0) == batch_size
+ else param
+ )
+ for name, param in self.cache_dict.items()
}
return ActivationCache(new_cache_dict, self.model, has_batch_dim=still_has_batch_dim)
@@ -495,6 +514,7 @@ def accumulated_resid(
layer,
pos_slice=pos_slice,
mlp_input=mlp_input,
+ has_batch_dim=self.has_batch_dim,
recompute_ln=recompute_ln,
)
if return_labels:
@@ -1376,17 +1396,17 @@ def apply_ln_to_stack(
# Logit lens: apply final layer norm to each component with recomputed statistics
if recompute_ln and layer == self.model.cfg.n_layers and hasattr(self.model, "ln_final"):
ln_final = self.model.ln_final
- had_pos_dim = residual_stack.ndim == 4
results = []
for i in range(residual_stack.shape[0]):
x = residual_stack[i]
- # ln_final expects (batch, pos, d_model); ensure pos dim present
+ original_shape = x.shape
+ # ln_final expects (batch, pos, d_model); restore missing structural dimensions
+ if not has_batch_dim:
+ x = x.unsqueeze(0)
if x.ndim == 2:
x = x.unsqueeze(1)
out = ln_final(x)
- if not had_pos_dim:
- out = out.squeeze(1)
- results.append(out)
+ results.append(out.reshape(original_shape))
return torch.stack(results, dim=0)
# Center the stack onlny if the model uses LayerNorm
diff --git a/transformer_lens/HookedTransformer.py b/transformer_lens/HookedTransformer.py
index 7184426b55..ee717982c1 100644
--- a/transformer_lens/HookedTransformer.py
+++ b/transformer_lens/HookedTransformer.py
@@ -75,6 +75,7 @@
init_xavier_uniform_,
softcap_enabled,
)
+from transformer_lens.utilities.architectures import POST_NORM_ARCHITECTURES
from transformer_lens.utilities.devices import move_to_and_update_config
from transformer_lens.weight_processing import ProcessWeights
@@ -1435,18 +1436,20 @@ def from_pretrained(
"Setting center_writing_weights=False instead."
)
center_writing_weights = False
- # OLMo 2 post-norm is incompatible with fold_ln/center_writing_weights (pre-norm only)
- if cfg.original_architecture == "Olmo2ForCausalLM":
+ # Post-norm architectures are incompatible with fold_ln/center_writing_weights,
+ # both of which assume the norm gain sits on a sublayer's input.
+ if cfg.original_architecture in POST_NORM_ARCHITECTURES:
if fold_ln:
logging.warning(
- "fold_ln=True is incompatible with OLMo 2's post-norm architecture. "
- "Setting fold_ln=False."
+ f"fold_ln=True is incompatible with {cfg.original_architecture}'s "
+ "post-norm architecture. Setting fold_ln=False."
)
fold_ln = False
if center_writing_weights:
logging.warning(
- "center_writing_weights=True is incompatible with OLMo 2's post-norm "
- "architecture. Setting center_writing_weights=False."
+ f"center_writing_weights=True is incompatible with "
+ f"{cfg.original_architecture}'s post-norm architecture. "
+ "Setting center_writing_weights=False."
)
center_writing_weights = False
if center_unembed and softcap_enabled(cfg.output_logits_soft_cap):
@@ -1686,12 +1689,7 @@ def load_and_process_state_dict(
state_dict = self.fill_missing_keys(state_dict)
if fold_ln:
- if self.cfg.num_experts and self.cfg.num_experts > 1:
- logging.warning(
- "You are using MoE, so the layer norm weights can't be folded! Skipping"
- )
- fold_ln = False
- elif self.cfg.normalization_type not in ["LN", "LNPre", "RMS", "RMSPre"]:
+ if self.cfg.normalization_type not in ["LN", "LNPre", "RMS", "RMSPre"]:
logging.warning(
"You are not using LayerNorm or RMSNorm, so the layer norm weights can't be folded! Skipping"
)
diff --git a/transformer_lens/benchmarks/AGENTS.md b/transformer_lens/benchmarks/AGENTS.md
index be6ad26d0b..a039bfd1e4 100644
--- a/transformer_lens/benchmarks/AGENTS.md
+++ b/transformer_lens/benchmarks/AGENTS.md
@@ -10,7 +10,7 @@ If an agent is here because the user asked to "update the registry" or "verify a
## What this directory IS for
-- The phase-by-phase benchmark implementations (`forward_pass.py`, `generation.py`, `hook_registration.py`, `weight_processing.py`, `multimodal.py`, `audio.py`, `vision.py`, `encoder_common.py`, `text_quality.py`, `granular_weight_processing.py`, `component_outputs.py`, `backward_gradients.py`, `activation_cache.py`, `component_benchmark.py`, `hook_structure.py`).
+- The phase-by-phase benchmark implementations (`forward_pass.py`, `generation.py`, `hook_registration.py`, `weight_processing.py`, `multimodal.py`, `audio.py`, `vision.py`, `encoder_common.py`, `text_quality.py`, `text_quality_profiles.py` (Phase-4 prompt-profile data + resolver), `granular_weight_processing.py`, `component_outputs.py`, `backward_gradients.py`, `activation_cache.py`, `component_benchmark.py`, `hook_structure.py`).
- `main_benchmark.py` — exploratory benchmark runner for ad-hoc comparison. Useful for debugging a single model's phase scores without touching the registry.
- `utils.py` — shared helpers including `BenchmarkSeverity`.
diff --git a/transformer_lens/benchmarks/backward_gradients.py b/transformer_lens/benchmarks/backward_gradients.py
index 4e75009d74..24d527960b 100644
--- a/transformer_lens/benchmarks/backward_gradients.py
+++ b/transformer_lens/benchmarks/backward_gradients.py
@@ -14,6 +14,59 @@
from transformer_lens.hook_points import HookPoint
from transformer_lens.model_bridge import TransformerBridge
+# Grading band for numerical (non-convention) gradient mismatches. Registering
+# backward hooks forces normalization off HF's native autograd onto the python
+# norm, which shifts results at float-rounding scale; measured noise is ~1e-5
+# rel_l2 with a single over-tolerance element, while injected bugs start at
+# ~1e-3 rel_l2 with 60+ elements over. Valid for fp32 gradients only — the
+# gradient section upcasts reduced-precision models before comparing.
+REL_L2_TOLERANCE = 1e-4
+OVER_TOLERANCE_MAX_ELEMENTS = 3
+
+
+def needs_fp32_gradients(dtype: Optional[torch.dtype]) -> bool:
+ """Reduced-precision gradients cannot be graded against the fp32-calibrated
+ band — bf16's rounding floor alone is ~2e-3 rel_l2, inside the bug band."""
+ return dtype is not None and dtype not in (torch.float32, torch.float64)
+
+
+def gradient_mismatch_stats(
+ bridge_finite: torch.Tensor,
+ reference_finite: torch.Tensor,
+ abs_tolerance: float,
+ rel_tolerance: float,
+) -> dict:
+ """Scale-aware statistics for grading one recorded gradient mismatch.
+
+ A zero reference with a nonzero bridge gradient is the maximally divergent
+ case, not perfect agreement, so rel_l2 is inf there rather than 0.
+ """
+ bf, rf = bridge_finite.float(), reference_finite.float()
+ ref_norm = torch.norm(rf)
+ diff_norm = torch.norm(bf - rf)
+ if ref_norm > 0:
+ rel_l2 = (diff_norm / ref_norm).item()
+ else:
+ rel_l2 = 0.0 if diff_norm == 0 else float("inf")
+ over_count = int(
+ (torch.abs(bf - rf) > abs_tolerance + rel_tolerance * torch.abs(rf)).sum().item()
+ )
+ return {"rel_l2": rel_l2, "over_count": over_count}
+
+
+def gradient_mismatch_is_numerical_noise(rel_l2: float, over_count: int) -> bool:
+ """True when a gradient mismatch is diffuse and tiny rather than a divergence.
+
+ Elementwise worst-case cannot separate the two: one element of 55k crossing
+ the tolerance scores the same as a head scaled by 1%. rel_l2 separates them
+ by 58x or more, and the element COUNT guards the localized case rel_l2 would
+ dilute. A count (not a fraction) keeps the band reachable on small tensors:
+ detection guarantees count >= 1, so a fractional guard of 1e-4 was
+ arithmetically unsatisfiable below 10,000 elements (gemma-3-270m's MQA
+ hook_rot_k is 6,912).
+ """
+ return rel_l2 <= REL_L2_TOLERANCE and over_count <= OVER_TOLERANCE_MAX_ELEMENTS
+
def benchmark_backward_hooks(
bridge: TransformerBridge,
@@ -119,6 +172,7 @@ def benchmark_backward_hooks(
]
mismatches = []
+ mismatch_stats: dict = {}
for hook_name in sorted(common_hooks):
if hook_name in excluded_hooks:
continue
@@ -148,8 +202,16 @@ def benchmark_backward_hooks(
mean_diff = torch.mean(torch.abs(bf - rf)).item()
rel_diff = torch.abs(bf - rf) / (torch.abs(bf) + 1e-8)
mean_rel = rel_diff.mean().item()
+ # Scale-aware stats for grading. Elementwise worst-case alone
+ # cannot separate a real divergence from the float-rounding
+ # shift the python-norm fallback introduces when backward
+ # hooks force normalization off HF's native autograd path.
+ stats = gradient_mismatch_stats(bf, rf, abs_tolerance, rel_tolerance)
+ mismatch_stats[hook_name] = stats
mismatches.append(
- f"{hook_name}: Value mismatch - max_diff={max_diff:.6f}, mean_diff={mean_diff:.6f}, mean_rel={mean_rel:.6f}"
+ f"{hook_name}: Value mismatch - max_diff={max_diff:.6f}, mean_diff={mean_diff:.6f}, "
+ f"mean_rel={mean_rel:.6f}, rel_l2={stats['rel_l2']:.3e}, "
+ f"over_count={stats['over_count']}"
)
tested_hooks = len(common_hooks) - len(excluded_hooks)
@@ -169,6 +231,10 @@ def benchmark_backward_hooks(
"k_norm", # QK norm: Bridge uses 4D, HT uses 2D (shape convention)
"ln1.hook_",
"ln2.hook_",
+ # Sandwich norms (gemma-2/3): same class as ln1/ln2 above, which
+ # predate them.
+ "ln1_post.hook_",
+ "ln2_post.hook_",
"ln_final.hook_",
"hook_resid_mid",
"hook_resid_pre",
@@ -180,8 +246,25 @@ def benchmark_backward_hooks(
"mlp.hook_pre",
"hook_mlp_out",
]
+
+ def within_noise_band(entry: str) -> bool:
+ """Diffuse, tiny deviation — the fallback's rounding, not a divergence.
+
+ Measured noise across architectures is rel_l2 ~1e-5 with a single
+ over-tolerance element on the rotary hooks (the only ones outside
+ the pattern list); injected bugs of a 1% head scale or a 0.1%
+ uniform scale land at rel_l2 1e-3+ with 60+ elements over.
+ """
+ name = entry.split(":")[0]
+ stats = mismatch_stats.get(name)
+ if stats is None:
+ return False
+ return gradient_mismatch_is_numerical_noise(stats["rel_l2"], stats["over_count"])
+
acceptable_mismatches = [
- m for m in mismatches if any(pattern in m for pattern in acceptable_patterns)
+ m
+ for m in mismatches
+ if any(pattern in m for pattern in acceptable_patterns) or within_noise_band(m)
]
if len(acceptable_mismatches) == len(mismatches):
@@ -402,6 +485,9 @@ def benchmark_critical_backward_hooks(
"k_norm", # QK norm: Bridge uses 4D, HT uses 2D (shape convention)
"ln1.hook_",
"ln2.hook_",
+ # Sandwich norms (gemma-2/3): same class as ln1/ln2 above.
+ "ln1_post.hook_",
+ "ln2_post.hook_",
"hook_resid_pre",
"hook_resid_mid",
"hook_resid_post",
diff --git a/transformer_lens/benchmarks/component_outputs.py b/transformer_lens/benchmarks/component_outputs.py
index 989f88760f..af0c0bef72 100644
--- a/transformer_lens/benchmarks/component_outputs.py
+++ b/transformer_lens/benchmarks/component_outputs.py
@@ -894,6 +894,16 @@ def _run_component(
except AttributeError:
# Skip this component
raise ValueError("Cannot test pos_embed - unclear interface")
+ elif isinstance(getattr(component, "original_component", component), torch.nn.Embedding):
+ # Any other embedding table (BERT's token_type_embed) rejects the
+ # float default: embeddings index with integer ids. The HF side is
+ # the bare nn.Embedding, the bridge side wraps one. Ids are derived
+ # from test_input so both sides index identically.
+ embedding_table = getattr(component, "original_component", component)
+ assert isinstance(embedding_table, torch.nn.Embedding) # narrowed by the elif
+ num_ids = int(embedding_table.num_embeddings)
+ id_input = (test_input.abs().sum(dim=-1) * 1e3).long() % num_ids
+ return component(id_input)
elif component_path == "project_in":
# project_in expects word_embed_proj_dim, not d_model.
word_embed_proj_dim = getattr(self.cfg, "word_embed_proj_dim", None)
diff --git a/transformer_lens/benchmarks/forward_pass.py b/transformer_lens/benchmarks/forward_pass.py
index 0f872e2bb0..c405f4e90b 100644
--- a/transformer_lens/benchmarks/forward_pass.py
+++ b/transformer_lens/benchmarks/forward_pass.py
@@ -8,12 +8,18 @@
from transformer_lens.benchmarks.utils import (
BenchmarkResult,
BenchmarkSeverity,
+ bridge_self_target_loss,
compare_scalars,
compare_tensors,
)
from transformer_lens.model_bridge import TransformerBridge
+def _compute_self_target_loss(bridge: TransformerBridge, test_text: str) -> torch.Tensor:
+ """Compute loss with the tokenized input supplied as explicit labels."""
+ return bridge_self_target_loss(bridge, test_text)
+
+
def _is_encoder_decoder(model: torch.nn.Module) -> bool:
"""Check if a model is an encoder-decoder architecture."""
config = getattr(model, "config", None)
@@ -193,7 +199,7 @@ def benchmark_loss_equivalence(
BenchmarkResult with comparison details
"""
try:
- bridge_loss = bridge(test_text, return_type="loss")
+ bridge_loss = _compute_self_target_loss(bridge, test_text)
if reference_model is None and reference_loss is None:
# No reference - just verify loss is valid
diff --git a/transformer_lens/benchmarks/hook_registration.py b/transformer_lens/benchmarks/hook_registration.py
index 77b819d25b..3318b65030 100644
--- a/transformer_lens/benchmarks/hook_registration.py
+++ b/transformer_lens/benchmarks/hook_registration.py
@@ -8,6 +8,7 @@
from transformer_lens.benchmarks.utils import (
BenchmarkResult,
BenchmarkSeverity,
+ bridge_self_target_loss,
compare_activation_dicts,
compare_scalars,
filter_expected_missing_hooks,
@@ -696,9 +697,26 @@ def ablation_hook(activation, hook):
return activation
# Test bridge
- bridge_original = bridge(test_text, return_type="loss")
+ # Encoder-decoder bridges name their stacks; a bare blocks.* hook would
+ # silently no-op and make the ablation vacuous.
+ ablation_target = next(
+ (
+ name
+ for name in (
+ "blocks.0.attn.hook_v",
+ "encoder_blocks.0.attn.hook_v",
+ "decoder_blocks.0.attn.hook_v",
+ )
+ if name in bridge.hook_dict
+ ),
+ "blocks.0.attn.hook_v",
+ )
+ bridge_original = bridge_self_target_loss(bridge, test_text)
bridge_ablated = bridge.run_with_hooks(
- test_text, return_type="loss", fwd_hooks=[("blocks.0.attn.hook_v", ablation_hook)]
+ test_text,
+ return_type="loss",
+ labels=bridge.to_tokens(test_text),
+ fwd_hooks=[(ablation_target, ablation_hook)],
)
bridge_effect = bridge_ablated - bridge_original
diff --git a/transformer_lens/benchmarks/main_benchmark.py b/transformer_lens/benchmarks/main_benchmark.py
index 6e531abfd9..cc364e3ecd 100644
--- a/transformer_lens/benchmarks/main_benchmark.py
+++ b/transformer_lens/benchmarks/main_benchmark.py
@@ -5,10 +5,12 @@
Phase 1: HF + Bridge (unprocessed) - Compare against raw HuggingFace model
Phase 2: Bridge (unprocessed) + HT (unprocessed) - Compare unprocessed models
Phase 3: Bridge (processed) + HT (processed) - Full compatibility mode testing
-Phase 4: Text Quality - Perplexity-based legibility scoring via GPT-2 Medium
+Phase 4: Text Quality - profile prompts scored by a pinned judge's perplexity ratio
Phase 5: Granular Weight Processing Tests (optional, individual flags)
Phase 6: Granular Weight Processing Tests (optional, combined flags)
Phase 7: Multimodal Tests (only for multimodal models with pixel_values support)
+Phase 8: Audio Tests (only for audio encoder models / audio-conditioned decoders)
+Phase 9: Vision Tests (only for vision-only encoder models, e.g. ViT/DeiT)
"""
import gc
@@ -31,9 +33,11 @@
benchmark_backward_hooks,
benchmark_critical_backward_hooks,
benchmark_gradient_computation,
+ needs_fp32_gradients,
)
from transformer_lens.benchmarks.component_benchmark import benchmark_all_components
from transformer_lens.benchmarks.forward_pass import (
+ _compute_self_target_loss,
benchmark_forward_pass,
benchmark_logits_equivalence,
benchmark_loss_equivalence,
@@ -512,17 +516,20 @@ def add_result(result: BenchmarkResult) -> None:
if verbose:
print("6. Backward Gradient Benchmarks")
- # MPS does not support bfloat16 autograd. Upcast to float32 for gradient tests if needed.
+ # Gradient comparisons are graded against fp32-calibrated thresholds
+ # (REL_L2_TOLERANCE): bf16's rounding floor alone is ~2e-3 rel_l2, inside the
+ # measured bug band, so reduced-precision gradients cannot be graded at all.
+ # Upcast for the gradient section on every device (MPS additionally lacks
+ # bf16 autograd), then restore below.
bridge_grad_dtype = bridge_model.cfg.dtype if hasattr(bridge_model, "cfg") else None
- bridge_device = next(bridge_model.parameters()).device
- mps_bf16_upcast = str(bridge_device).startswith("mps") and bridge_grad_dtype == torch.bfloat16
- if mps_bf16_upcast:
+ grad_fp32_upcast = needs_fp32_gradients(bridge_grad_dtype)
+ if grad_fp32_upcast:
try:
bridge_model.to(torch.float32)
if reference_model is not None:
reference_model.to(torch.float32)
except Exception:
- mps_bf16_upcast = False # Upcast failed; proceed as-is
+ grad_fp32_upcast = False # Upcast failed; proceed as-is
if ht_available:
try:
@@ -561,7 +568,7 @@ def add_result(result: BenchmarkResult) -> None:
if verbose:
print(f"✗ Gradient benchmark failed: {e}\n")
- if mps_bf16_upcast and bridge_grad_dtype is not None:
+ if grad_fp32_upcast and bridge_grad_dtype is not None:
try:
bridge_model.to(bridge_grad_dtype)
if reference_model is not None:
@@ -585,8 +592,9 @@ def run_benchmark_suite(
test_weight_processing_individually: bool = False,
phases: list[int] | None = None,
trust_remote_code: bool = False,
- scoring_model: PreTrainedModel | None = None,
- scoring_tokenizer: PreTrainedTokenizerBase | None = None,
+ judge_model: PreTrainedModel | None = None,
+ judge_tokenizer: PreTrainedTokenizerBase | None = None,
+ prompt_profile: str | None = None,
) -> List[BenchmarkResult]:
"""Run comprehensive benchmark suite for TransformerBridge.
@@ -594,7 +602,7 @@ def run_benchmark_suite(
Phase 1: HF + Bridge (unprocessed) - Compare against raw HuggingFace model
Phase 2: Bridge (unprocessed) + HT (unprocessed) - Compare unprocessed models
Phase 3: Bridge (processed) + HT (processed) - Full compatibility mode testing
- Phase 4: Text Quality - Perplexity-based legibility scoring via GPT-2
+ Phase 4: Text Quality - profile prompts scored by a pinned judge's perplexity ratio
Phase 5: Individual Weight Processing Flags (optional)
Phase 6: Combined Weight Processing Flags (optional)
@@ -617,9 +625,12 @@ def run_benchmark_suite(
tests that check each processing flag individually (default: False)
phases: Optional list of phase numbers to run (e.g., [1, 2, 3]). If None, runs all phases.
trust_remote_code: Whether to trust remote code for custom architectures.
- scoring_model: Optional pre-loaded GPT-2 scoring model for Phase 4. When
- provided with scoring_tokenizer, avoids reloading for each model in batch.
- scoring_tokenizer: Optional pre-loaded tokenizer for Phase 4 scoring model.
+ judge_model: Optional pre-loaded Phase-4 judge. When provided with
+ judge_tokenizer, avoids reloading for each model in batch.
+ judge_tokenizer: Optional pre-loaded tokenizer for the Phase-4 judge.
+ prompt_profile: Optional Phase-4 prompt profile (e.g. "chat",
+ "task:translation@en-de"). Resolved from curation + the registry
+ when None.
Returns:
List of BenchmarkResult objects
@@ -1221,7 +1232,7 @@ def cleanup_model(model, model_name_str: str):
with torch.no_grad():
bridge_logits = bridge_unprocessed(test_text, return_type="logits")
phase1_reference.hf_logits = bridge_logits.detach().cpu().clone()
- bridge_loss = bridge_unprocessed(test_text, return_type="loss")
+ bridge_loss = _compute_self_target_loss(bridge_unprocessed, test_text)
phase1_reference.hf_loss = bridge_loss.item()
phase1_reference.test_text = test_text
if needs_upcast:
@@ -1356,6 +1367,14 @@ def cleanup_model(model, model_name_str: str):
if bridge_bos is not None:
ht_prepend_bos = bridge_bos
+ # HookedTransformer is a causal decoder: loading a masked LM into it runs a
+ # bidirectional model under a causal mask, so it can never be a valid
+ # reference — numerical comparisons fall back to the Phase 1 HF logits.
+ if use_ht_reference and is_masked_lm_model(model_name, trust_remote_code=trust_remote_code):
+ if verbose:
+ print("Skipping HookedTransformer reference: masked-LM is not representable causally.")
+ use_ht_reference = False
+
# Load HookedTransformer for comparison (after generation benchmarks)
ht_model_unprocessed = None
if should_run_phase(2) and use_ht_reference:
@@ -1426,7 +1445,7 @@ def cleanup_model(model, model_name_str: str):
# (e.g., OpenELM).
# ========================================================================
- # PHASE 4: Text Quality (GPT-2 perplexity scoring)
+ # PHASE 4: Text Quality (profile prompts, judge perplexity-ratio scoring)
# Runs before Phase 3 so it can reuse bridge_unprocessed (Phase 3
# destructively processes the weights, consuming the bridge).
# ========================================================================
@@ -1445,21 +1464,34 @@ def cleanup_model(model, model_name_str: str):
and not is_masked_lm_model(model_name, trust_remote_code=trust_remote_code)
and not is_audio_model(model_name, trust_remote_code=trust_remote_code)
):
+ if prompt_profile is None:
+ from transformer_lens.benchmarks.text_quality_profiles import (
+ resolve_profile,
+ )
+ from transformer_lens.tools.model_registry.registry_io import (
+ registry_prompt_profile,
+ )
+
+ config = getattr(bridge_unprocessed, "original_model", None)
+ archs = getattr(getattr(config, "config", None), "architectures", None) or []
+ prompt_profile = str(
+ resolve_profile(
+ model_name, archs[0] if archs else None, registry_prompt_profile(model_name)
+ )
+ )
+
if verbose:
print(f"\n{'='*80}")
- print("PHASE 2.5: Text Quality (GPT-2 perplexity scoring)")
+ print(f"PHASE 2.5: Text Quality (profile {prompt_profile}, judge ratio scoring)")
print(f"{'='*80}\n")
try:
text_quality_result = benchmark_text_quality(
bridge_unprocessed,
- test_text,
- max_new_tokens=50,
- scoring_model_name="gpt2",
- pass_threshold=85.0,
- device=device,
- scoring_model=scoring_model,
- scoring_tokenizer=scoring_tokenizer,
+ prompt_profile,
+ judge_model=judge_model,
+ judge_tokenizer=judge_tokenizer,
+ model_name=model_name,
)
text_quality_result.phase = 4
add_result(text_quality_result)
@@ -1667,7 +1699,7 @@ def _cleanup_bridge_unprocessed():
_cleanup_bridge_unprocessed()
_skip_phase3 = True
if verbose:
- print("\n⚠ Phase 3 skipped (not in phases list)\n")
+ print("\n⚠ Phase 3 skipped (excluded by phases filter or adapter applicable_phases)\n")
elif is_encoder_decoder_model(model_name):
_cleanup_bridge_unprocessed()
_skip_phase3 = True
@@ -1980,34 +2012,49 @@ def _cleanup_bridge_unprocessed():
return results
-def update_model_registry(model_name: str, results: List[BenchmarkResult]) -> bool:
+def update_model_registry(
+ model_name: str, results: List[BenchmarkResult], use_hf_reference: bool = False
+) -> bool:
"""Update the model registry with benchmark results.
Args:
model_name: The model that was benchmarked
results: List of benchmark results
+ use_hf_reference: Whether the run numerically compared against an HF
+ reference. Defaults to False so an unstated reference state records
+ a passing run as PROVISIONAL, never VERIFIED.
Returns:
True if registry was updated successfully
"""
from transformer_lens.tools.model_registry.registry_io import (
- STATUS_VERIFIED,
+ STATUS_FAILED,
+ STATUS_PROVISIONAL,
add_verification_record,
update_model_status,
)
- # Calculate phase scores (percentage of passed tests per phase)
- phase_results: Dict[int, List[bool]] = {1: [], 2: [], 3: []}
- for result in results:
- if result.phase in phase_results and result.severity != BenchmarkSeverity.SKIPPED:
- phase_results[result.phase].append(result.passed)
+ # Threshold/note logic shared with verify_models so the two paths can't drift.
+ from transformer_lens.tools.model_registry.verify_models import (
+ _build_verified_note,
+ _check_phase_scores,
+ _extract_phase_scores,
+ _extract_prompt_profile,
+ _pass_status,
+ _sanitize_note,
+ )
- phase_scores: Dict[int, Optional[float]] = {}
- for phase, passed_list in phase_results.items():
- if passed_list:
- phase_scores[phase] = round(sum(passed_list) / len(passed_list) * 100, 1)
- else:
- phase_scores[phase] = None
+ phase_scores = _extract_phase_scores(results)
+
+ score_error = _check_phase_scores(phase_scores, results)
+ if score_error:
+ status = STATUS_FAILED
+ note = score_error
+ else:
+ status = _pass_status(use_hf_reference)
+ note = _build_verified_note(phase_scores, results)
+ if status == STATUS_PROVISIONAL:
+ note = f"Structural only (no HF reference): {note}"
# Try to determine architecture
architecture_id = "Unknown"
@@ -2024,21 +2071,27 @@ def update_model_registry(model_name: str, results: List[BenchmarkResult]) -> bo
updated = update_model_status(
model_id=model_name,
arch_id=architecture_id,
- status=STATUS_VERIFIED,
+ status=status,
phase_scores=phase_scores,
+ note=note,
+ sanitize_fn=_sanitize_note,
+ prompt_profile=_extract_prompt_profile(results),
)
- add_verification_record(
- model_id=model_name,
- arch_id=architecture_id,
- notes="Benchmark passed",
- verified_by="main_benchmark",
- )
+ # No history record for provisional runs — VerificationHistory.is_verified()
+ # treats any record as verified, which would bypass the provisional gate.
+ if status != STATUS_PROVISIONAL:
+ add_verification_record(
+ model_id=model_name,
+ arch_id=architecture_id,
+ notes=note,
+ verified_by="main_benchmark",
+ sanitize_fn=_sanitize_note,
+ )
- print(
- f"Updated registry for {model_name}: "
- f"P1={phase_scores.get(1)}%, P2={phase_scores.get(2)}%, P3={phase_scores.get(3)}%"
- )
+ label = {STATUS_FAILED: "FAILED", STATUS_PROVISIONAL: "PROVISIONAL"}.get(status, "VERIFIED")
+ score_parts = ", ".join(f"P{p}={s}%" for p, s in sorted(phase_scores.items()))
+ print(f"Updated registry for {model_name} ({label}): {score_parts or 'no phase results'}")
return updated
@@ -2102,7 +2155,9 @@ def main():
)
if args.update_registry:
- update_model_registry(args.model, results)
+ # Same requested-reference state verify_models feeds pass_status(): a
+ # --no-hf-reference run can only mint PROVISIONAL, never VERIFIED.
+ update_model_registry(args.model, results, use_hf_reference=not args.no_hf_reference)
if __name__ == "__main__":
diff --git a/transformer_lens/benchmarks/text_quality.py b/transformer_lens/benchmarks/text_quality.py
index aa4c9b4bd9..b281c7dfb6 100644
--- a/transformer_lens/benchmarks/text_quality.py
+++ b/transformer_lens/benchmarks/text_quality.py
@@ -1,17 +1,22 @@
"""Text quality benchmark for TransformerBridge.
-Generates text with the bridge model from multiple diverse prompts and scores
-each continuation's legibility using GPT-2 as a perplexity-based judge.
-Only the generated continuation tokens are scored (prompt tokens are masked),
-and a repetition penalty is applied to catch degenerate looping output.
-
-Generation is seeded for reproducibility, and the scoring model is loaded once
-and reused across all prompts.
+Generates text the way a real user of the model would (its prompt profile:
+chat template, translation source, code, own-language continuation — see
+``text_quality_profiles``) and scores each output against a known-good
+reference completion with one pinned multilingual judge. The score derives
+from the perplexity ratio PPL_judge(generated)/PPL_judge(reference), which
+cancels the judge's per-language handicap; a repetition penalty catches
+degenerate loops (which the ratio alone rewards) and a length penalty
+catches truncated output.
+
+Generation is seeded per prompt for reproducibility, and the judge is loaded
+once (CPU/fp32 always, so scores do not depend on the verifying machine) and
+reused across all prompts.
"""
import gc
import math
-from typing import List, Optional, Tuple
+from typing import Any, List, Optional, Tuple, Union
import torch
from transformers import (
@@ -21,6 +26,22 @@
PreTrainedTokenizerBase,
)
+from transformer_lens.benchmarks.text_quality_profiles import (
+ CAPTION_REFERENCES,
+ JUDGE_CONTEXT_KINDS,
+ JUDGE_R_FAIL,
+ LANG_ISO3,
+ LANG_NAMES,
+ MAX_NEW_TOKENS_BY_KIND,
+ NLLB_CODES,
+ PREPEND_BOS_BY_KIND,
+ T5_PREFIX_ARCHITECTURES,
+ TEMPERATURE_BY_KIND,
+ ProfilePrompt,
+ ProfileSpec,
+ p4_pass_threshold,
+ prompts_for,
+)
from transformer_lens.benchmarks.utils import (
BenchmarkResult,
BenchmarkSeverity,
@@ -28,96 +49,74 @@
)
from transformer_lens.model_bridge import TransformerBridge
-# Diverse prompts used alongside the caller-provided test_text to get a robust
-# quality signal across different domains and styles.
-_DEFAULT_PROMPTS = [
- "The theory of relativity explains that",
- "In the dense forests of the Amazon,",
- "Modern computing relies heavily on",
-]
-
-
-def _load_scoring_model(
- scoring_model_name: str,
- device: str,
-) -> Tuple[PreTrainedModel, PreTrainedTokenizerBase]:
- """Load the scoring model and tokenizer.
-
- Separated from perplexity computation so the caller can load once and
- reuse across multiple prompts.
- """
- tokenizer = AutoTokenizer.from_pretrained(scoring_model_name)
- model = AutoModelForCausalLM.from_pretrained(scoring_model_name)
- torch.nn.Module.to(model, device)
+# The one judge every model is scored with, pinned by revision so a Hub update
+# can never silently move every score. Selection + measurements live in
+# scripts/text_quality_judge_bakeoff.py; separation is weakest in de/ru, so
+# scores there carry wider error bars.
+JUDGE_MODEL_ID = "Qwen/Qwen2.5-0.5B"
+JUDGE_REVISION = "060db6499f32faf8b98477b0a26969ef7d8b9987"
+
+
+def load_judge() -> Tuple[PreTrainedModel, PreTrainedTokenizerBase]:
+ """Load the pinned judge on CPU in fp32 (machine-independent scores)."""
+ tokenizer = AutoTokenizer.from_pretrained(JUDGE_MODEL_ID, revision=JUDGE_REVISION)
+ model = AutoModelForCausalLM.from_pretrained(
+ JUDGE_MODEL_ID, revision=JUDGE_REVISION, dtype=torch.float32
+ )
+ torch.nn.Module.to(model, "cpu")
model.eval()
return model, tokenizer
-def _compute_continuation_perplexity(
- prompt: str,
- full_text: str,
- tokenizer: PreTrainedTokenizerBase,
- scoring_model: PreTrainedModel,
- device: str,
+def _judge_perplexity(
+ text: str,
+ context: str,
+ tokenizer: Any,
+ judge: Any,
) -> Tuple[float, Optional[str]]:
- """Compute perplexity of only the continuation tokens (excluding prompt).
-
- Prompt tokens are masked with -100 in labels so CrossEntropyLoss ignores
- them. This prevents well-formed prompt text from artificially lowering
- the perplexity of generated content.
-
- Args:
- prompt: The original input prompt.
- full_text: The complete text (prompt + generated continuation).
- tokenizer: Pre-loaded tokenizer.
- scoring_model: Pre-loaded scoring model.
- device: Device string.
-
- Returns:
- Tuple of (perplexity, error_message). error_message is None on success.
- """
+ """Judge perplexity of ``text``; ``context`` tokens are label-masked so only
+ ``text`` is scored. Returns (ppl, error)."""
try:
- encodings = tokenizer(full_text, return_tensors="pt")
- input_ids = encodings["input_ids"].to(device)
+ # Tokenize the pieces separately: tokenizing the concatenated string
+ # lets BPE merge across the boundary and shifts the label mask into
+ # the scored text.
+ text_ids = tokenizer(text, return_tensors="pt")["input_ids"]
+ context_len = 0
+ input_ids = text_ids
+ if context:
+ context_ids = tokenizer(context, return_tensors="pt")["input_ids"]
+ context_len = context_ids.shape[1]
+ input_ids = torch.cat([context_ids, text_ids], dim=1)
+
+ if text_ids.shape[1] < 2:
+ return float("inf"), "Scored text too short (< 2 judge tokens)"
- # Tokenize just the prompt to find where continuation starts
- prompt_encodings = tokenizer(prompt, return_tensors="pt")
- prompt_len = prompt_encodings["input_ids"].shape[1]
-
- # Build labels: -100 for prompt positions, actual ids for continuation
labels = input_ids.clone()
- labels[0, :prompt_len] = -100
-
- continuation_len = input_ids.shape[1] - prompt_len
- if continuation_len < 2:
- return float("inf"), "Generated continuation too short (< 2 tokens)"
+ if context_len:
+ labels[0, :context_len] = -100
with torch.no_grad():
- outputs = scoring_model(input_ids, labels=labels)
- loss = outputs.loss.item()
-
- perplexity = math.exp(loss)
- return perplexity, None
-
+ loss = judge(input_ids, labels=labels).loss.item()
+ return math.exp(loss), None
except Exception as e:
return float("inf"), f"Perplexity computation failed: {str(e)}"
def _compute_repetition_penalty(text: str, ns: Tuple[int, ...] = (2, 3, 4)) -> float:
- """Compute a repetition penalty based on n-gram uniqueness ratio.
-
- Returns a multiplier in [0.0, 1.0] where 1.0 means no repetition and
- lower values penalize repetitive text. The penalty is the minimum
- unique-n-gram ratio across all checked n-gram sizes.
+ """Minimum unique-n-gram ratio in [0, 1]; low values mean looping output.
- Args:
- text: The generated continuation text (prompt excluded).
- ns: Tuple of n-gram sizes to check.
-
- Returns:
- Penalty multiplier in [0.0, 1.0].
+ Load-bearing under ratio scoring: a degenerate loop has LOW judge
+ perplexity, so without this multiplier it would score 100.
"""
words = text.lower().split()
+ # Scriptio continua (zh/ja): word n-grams are inert exactly where the
+ # judge rewards loops most, and a single stray space would restore the
+ # word path — so char n-grams whenever the text is CJK-dominated.
+ compact = "".join(text.split())
+ if compact:
+ cjk = sum(1 for c in compact if 0x3040 <= ord(c) <= 0x30FF or 0x4E00 <= ord(c) <= 0x9FFF)
+ if cjk / len(compact) >= 0.3 and len(compact) >= 8:
+ words = list(compact)
if len(words) < 2:
return 1.0
@@ -134,23 +133,84 @@ def _compute_repetition_penalty(text: str, ns: Tuple[int, ...] = (2, 3, 4)) -> f
return min_ratio
-def _perplexity_to_score(perplexity: float) -> float:
- """Map continuation perplexity to a 0-100 legibility score.
-
- Uses: score = 135 - 10 * ln(perplexity), capped to [0, 100].
- Calibrated for continuation-only perplexity (higher than full-text).
- A well-functioning model typically gets ppl 40-60 -> score 94-98.
- Default pass threshold of 85 corresponds to approximately ppl 150.
+def _ratio_to_score(ratio: float) -> float:
+ """Map generated/reference perplexity ratio to 0-100.
- Args:
- perplexity: The perplexity value from the scoring model.
-
- Returns:
- Score from 0.0 to 100.0.
+ score = 100 - 100*ln(ratio)/ln(R_FAIL), clamped: ratio<=1 (as good as the
+ reference) scores 100, ratio=R_FAIL scores 0, and score 50 falls at
+ sqrt(R_FAIL) — the geometric midpoint between reference quality and
+ unambiguously broken output, which keeps the registry's phase-4 floor of
+ 50 principled.
"""
- if perplexity <= 0 or math.isinf(perplexity):
+ if ratio <= 0 or math.isinf(ratio) or math.isnan(ratio):
return 0.0
- return max(0.0, min(100.0, 135.0 - 10.0 * math.log(perplexity)))
+ if ratio <= 1.0:
+ return 100.0
+ return max(0.0, min(100.0, 100.0 - 100.0 * math.log(ratio) / math.log(JUDGE_R_FAIL)))
+
+
+_SCRIPT_RANGES: dict[str, Tuple[Tuple[int, int], ...]] = {
+ "zh": ((0x4E00, 0x9FFF),),
+ "ja": ((0x3040, 0x30FF), (0x4E00, 0x9FFF)),
+ "ar": ((0x0600, 0x06FF),),
+ "ru": ((0x0400, 0x04FF),),
+ "hi": ((0x0900, 0x097F),),
+}
+
+_LATIN_STOPWORDS: dict[str, frozenset] = {
+ "en": frozenset("the and of to is that with for was are it in on".split()),
+ "fr": frozenset(
+ "le la les des une est que je pas dans de et il elle un en du au pour sur ne ce se".split()
+ ),
+ "es": frozenset("el los una es que no por con para como de en la y se del las".split()),
+ "de": frozenset(
+ "der die das und ist nicht ich ein eine mit den von zu im auf f\u00fcr sich".split()
+ ),
+ "it": frozenset("il la di che non per una sono del gli e in un le si con".split()),
+ "nl": frozenset("de het een en is niet ik van dat met op voor aan zijn".split()),
+ "pt": frozenset("o os uma de e que n\u00e3o para com por em um as dos da".split()),
+}
+
+
+def _wrong_language(text: str, lang: str) -> bool:
+ """Conservatively true only when the text is clearly NOT in ``lang``.
+
+ Ratio scoring alone measures fluency, not language: fluent English output
+ beats a short German reference and clamps to 100, so an untranslated echo
+ would otherwise score perfectly. Non-Latin targets check script presence;
+ Latin targets require zero expected-language stopwords while another
+ covered language has several.
+ """
+ if lang in ("code", ""):
+ return False
+ ranges = _SCRIPT_RANGES.get(lang)
+ if ranges is not None:
+ letters = [c for c in text if c.isalpha()]
+ if not letters:
+ return False
+ in_script = sum(1 for c in letters if any(lo <= ord(c) <= hi for lo, hi in ranges))
+ return in_script / len(letters) < 0.3
+ expected = _LATIN_STOPWORDS.get(lang)
+ if expected is None:
+ return False
+ tokens = [w.strip(".,;:!?\"'()") for w in text.lower().split()]
+ hits = {code: sum(1 for w in tokens if w in stops) for code, stops in _LATIN_STOPWORDS.items()}
+ return hits[lang] == 0 and max(hits.values(), default=0) >= 3
+
+
+def _length_penalty(gen_tokens: int, ref_tokens: int) -> float:
+ """Penalize output far shorter OR far longer than its reference.
+
+ Neutral band [0.5x, 3x] of reference length. The old 25% floor never
+ fired once in four validation sweeps — a contentless 13-token chat stub
+ against a 41-token reference scored 93.6; at a 0.5x floor it drops below
+ the pass line. The 3x cap is the second net for rambling output the
+ repetition penalty misses."""
+ if ref_tokens <= 0:
+ return 1.0
+ under = gen_tokens / (0.5 * ref_tokens)
+ over = (3.0 * ref_tokens) / max(gen_tokens, 1)
+ return max(0.0, min(1.0, under, over))
def _build_caption_test_images(n: int = 3) -> list:
@@ -184,7 +244,7 @@ def _build_caption_test_images(n: int = 3) -> list:
def _generate_image_conditioned_captions(
bridge: TransformerBridge, max_new_tokens: int
-) -> List[Tuple[str, str]]:
+) -> List[Tuple[int, str]]:
"""Caption synthetic images for image-conditioned seq2seq (Florence-2 emits
nothing text-only, so text-only P4 is uninformative); [] if no processor/PIL."""
processor = getattr(bridge, "processor", None)
@@ -202,7 +262,7 @@ def _generate_image_conditioned_captions(
is_task_captioner = hasattr(processor, "post_process_generation")
task = "" if is_task_captioner else "Describe this image in detail."
- samples: List[Tuple[str, str]] = []
+ samples: List[Tuple[int, str]] = []
for i, image in enumerate(images):
try:
inputs = processor(text=task, images=image, return_tensors="pt")
@@ -216,48 +276,135 @@ def _generate_image_conditioned_captions(
input_ids, max_new_tokens=max_new_tokens, return_type="tokens", **extra
)
if isinstance(out, torch.Tensor):
- text = bridge.tokenizer.decode(out[0], skip_special_tokens=True).strip()
+ is_encoder_decoder = bool(
+ getattr(getattr(bridge, "original_model", None), "config", None)
+ and getattr(bridge.original_model.config, "is_encoder_decoder", False)
+ )
+ # Decoder-only VLM output is prompt + continuation; scoring the
+ # fluent prompt as caption text would inflate every sample.
+ caption_ids = out[0] if is_encoder_decoder else out[0, input_ids.shape[-1] :]
+ text = bridge.tokenizer.decode(caption_ids, skip_special_tokens=True).strip()
if text:
- samples.append((f"image_{i}", text))
+ samples.append((i, text))
except Exception:
continue
return samples
+def _architecture_id(bridge: Any) -> str:
+ """First HF architecture name of the wrapped model, or ''."""
+ config = getattr(getattr(bridge, "original_model", None), "config", None)
+ architectures = getattr(config, "architectures", None) or []
+ return architectures[0] if architectures else ""
+
+
+def _resolve_lang_code(tokenizer, lang: str) -> Optional[str]:
+ """The tokenizer's own code string for ``lang`` ("de" / "de_DE" /
+ "deu_Latn"), or None. transformers 5.x NllbTokenizer exposes neither
+ get_lang_id nor lang_code_to_id, so candidates are probed through the
+ vocab as well."""
+ lang = lang.lower()
+ lang_code_to_id = getattr(tokenizer, "lang_code_to_id", None)
+ if isinstance(lang_code_to_id, dict):
+ if lang in lang_code_to_id:
+ return lang
+ iso3 = LANG_ISO3.get(lang, "")
+ for code in lang_code_to_id:
+ code_lower = code.lower()
+ if code_lower.startswith(lang + "_") or (iso3 and code_lower.startswith(iso3 + "_")):
+ return code
+ # Vocab probing is only safe for DISTINCTIVE code forms ("deu_Latn"): a
+ # bare ISO code collides with ordinary subwords (T5's "de", Marian's "en")
+ # and would be injected as a forced decoder token.
+ nllb = NLLB_CODES.get(lang)
+ unk_id = getattr(tokenizer, "unk_token_id", None)
+ convert = getattr(tokenizer, "convert_tokens_to_ids", None)
+ if nllb and callable(convert):
+ try:
+ token_id = convert(nllb)
+ except Exception:
+ return None
+ if isinstance(token_id, int) and token_id >= 0 and token_id != unk_id:
+ return nllb
+ return None
+
+
+def _forced_bos_for_target(tokenizer, tgt_lang: str) -> Optional[int]:
+ """Target-language decoder token for multilingual translators, or None."""
+ get_lang_id = getattr(tokenizer, "get_lang_id", None)
+ if callable(get_lang_id):
+ try:
+ return int(get_lang_id(tgt_lang))
+ except Exception:
+ return None
+ code = _resolve_lang_code(tokenizer, tgt_lang)
+ if code is None:
+ return None
+ lang_code_to_id = getattr(tokenizer, "lang_code_to_id", None)
+ if isinstance(lang_code_to_id, dict) and code in lang_code_to_id:
+ return int(lang_code_to_id[code])
+ try:
+ token_id = tokenizer.convert_tokens_to_ids(code)
+ except Exception:
+ return None
+ if (
+ isinstance(token_id, int)
+ and token_id >= 0
+ and token_id != getattr(tokenizer, "unk_token_id", None)
+ ):
+ return int(token_id)
+ return None
+
+
+def _build_model_input(
+ bridge: Any,
+ spec: ProfileSpec,
+ prompt: ProfilePrompt,
+ architecture_id: str,
+) -> str:
+ """Render one profile prompt into the text this model expects."""
+ if spec.kind == "chat":
+ return bridge.tokenizer.apply_chat_template(
+ [{"role": "user", "content": prompt.prompt}],
+ add_generation_prompt=True,
+ tokenize=False,
+ )
+ if spec.kind == "task:translation" and architecture_id in T5_PREFIX_ARCHITECTURES:
+ src_name = LANG_NAMES.get(spec.src or "en", "English")
+ tgt_name = LANG_NAMES.get(spec.lang, "German")
+ return f"translate {src_name} to {tgt_name}: {prompt.prompt}"
+ if spec.kind == "task:summarization" and architecture_id in T5_PREFIX_ARCHITECTURES:
+ return f"summarize: {prompt.prompt}"
+ return prompt.prompt
+
+
def benchmark_text_quality(
- bridge: TransformerBridge,
- test_text: str,
- max_new_tokens: int = 50,
- scoring_model_name: str = "gpt2",
- pass_threshold: float = 85.0,
- device: str = "cpu",
- scoring_model: Optional[PreTrainedModel] = None,
- scoring_tokenizer: Optional[PreTrainedTokenizerBase] = None,
+ bridge: Any,
+ profile: Union[str, ProfileSpec] = "continuation",
+ *,
+ max_new_tokens: Optional[int] = None,
+ judge_model: Optional[Any] = None,
+ judge_tokenizer: Optional[Any] = None,
+ model_name: Optional[str] = None,
) -> BenchmarkResult:
- """Benchmark text generation quality using continuation-only perplexity scoring.
-
- Generates text from multiple diverse prompts, scores each continuation using
- GPT-2 perplexity (prompt tokens masked), applies a repetition penalty,
- and returns the averaged score.
-
- Args:
- bridge: TransformerBridge model to test.
- test_text: Primary input prompt (additional diverse prompts are also used).
- max_new_tokens: Number of tokens to generate per prompt.
- scoring_model_name: HuggingFace model to use as scorer.
- pass_threshold: Minimum average score to pass (default 95.0).
- device: Device for the scoring model.
- scoring_model: Optional pre-loaded scoring model. When provided alongside
- scoring_tokenizer, skips loading and avoids cleanup (caller owns lifecycle).
- scoring_tokenizer: Optional pre-loaded tokenizer for the scoring model.
-
- Returns:
- BenchmarkResult with quality score details.
+ """Benchmark text generation quality with profile prompts and reference-ratio scoring.
+
+ Generates from the model's prompt-profile prompts through the real user
+ path (``bridge.generate``), then scores each output against the prompt's
+ reference completion via the pinned judge's perplexity ratio, with
+ repetition and length penalties.
"""
+ if model_name is not None and model_name.lower() == JUDGE_MODEL_ID.lower():
+ # Ratio scoring against the judge's own perplexity is self-grading.
+ return BenchmarkResult(
+ name="text_quality",
+ severity=BenchmarkSeverity.SKIPPED,
+ message=f"P4 skipped: {model_name} is the pinned judge — cannot self-score",
+ )
_loaded_locally = False
- tokenizer = scoring_tokenizer
+ tokenizer = judge_tokenizer
try:
- prompts = [test_text] + _DEFAULT_PROMPTS
+ spec = ProfileSpec.parse(profile) if isinstance(profile, str) else profile
# Diffusion LMs produce text through their native sampler; scoring that
# text is as meaningful as scoring autoregressive output.
@@ -271,95 +418,211 @@ def benchmark_text_quality(
message="Skipped: architecture supports no text generation",
)
- # Encoder-decoder models (T5/Marian/BART) emit a standalone decoder
- # output (translation, summary), not a continuation of the prompt, so
- # there is no prompt prefix to mask out — the whole generated text is the
- # content to score. (An en-in→en translation whose output ~= the prompt
- # length otherwise trips the "continuation too short" guard for every
- # prompt and scores 0.)
is_encoder_decoder = bool(
getattr(getattr(bridge, "original_model", None), "config", None)
and getattr(bridge.original_model.config, "is_encoder_decoder", False)
)
- # Image-conditioned seq2seq (e.g. Florence-2) emits a 1-token EOS for a
- # text-only prompt — it needs pixel_values to produce anything. For those
- # we drive real caption generation from test images and score that.
is_multimodal = bool(getattr(getattr(bridge, "cfg", None), "is_multimodal", False))
- image_conditioned = is_encoder_decoder and is_multimodal
- # Generate text to score (prompt, full_text)
- generations: List[Tuple[str, str]] = []
+ # Effective-profile adjustments. Image-conditioned seq2seq (Florence-2)
+ # emits a bare EOS for text-only prompts — caption real images instead.
+ # A chat profile without a chat template downgrades to continuation;
+ # never the other direction (base models may ship templates).
+ adjustment = ""
+ if is_encoder_decoder and is_multimodal:
+ spec = ProfileSpec("caption")
+ elif spec.kind == "chat":
+ if getattr(bridge.tokenizer, "chat_template", None) is None:
+ spec = ProfileSpec("continuation", spec.lang)
+ adjustment = "chat profile downgraded: tokenizer has no chat template"
+ else:
+ try:
+ bridge.tokenizer.apply_chat_template(
+ [{"role": "user", "content": "probe"}],
+ add_generation_prompt=True,
+ tokenize=False,
+ )
+ except Exception as template_error:
+ spec = ProfileSpec("continuation", spec.lang)
+ adjustment = f"chat profile downgraded: template raised {template_error!r}"
+
+ denoise_style = "mask" if getattr(bridge.tokenizer, "mask_token", None) else "t5"
+ profile_prompts = prompts_for(spec, denoise_style=denoise_style)
+ if profile_prompts is None:
+ return BenchmarkResult(
+ name="text_quality",
+ severity=BenchmarkSeverity.SKIPPED,
+ message=(
+ f"P4 skipped: no prompts for profile '{spec}' — file a "
+ "TransformerLens issue to add coverage in "
+ "benchmarks/text_quality_profiles.py"
+ ),
+ )
+
+ if max_new_tokens is None:
+ max_new_tokens = MAX_NEW_TOKENS_BY_KIND.get(spec.kind, 50)
+
+ architecture_id = _architecture_id(bridge)
+ forced_bos: Optional[int] = None
+ if spec.kind == "task:translation":
+ src_lang_attr = getattr(bridge.tokenizer, "src_lang", None)
+ if src_lang_attr is not None and spec.src:
+ src_code = _resolve_lang_code(bridge.tokenizer, spec.src)
+ if src_code is not None:
+ try:
+ bridge.tokenizer.src_lang = src_code
+ except Exception:
+ pass
+ forced_bos = _forced_bos_for_target(bridge.tokenizer, spec.lang)
+
+ # Generate: (profile_prompt, generated_text) pairs. Token-level slicing —
+ # generate() decodes with skip_special_tokens, so the prompt string is
+ # not reliably a prefix of the output string (chat templates).
+ generations: List[Tuple[ProfilePrompt, str]] = []
primary_generated = ""
- if image_conditioned:
+ if spec.kind == "caption":
with deterministic_rng():
captions = _generate_image_conditioned_captions(bridge, max_new_tokens)
if not captions:
- # Cannot reach this model's real (image-conditioned) generation —
- # skip rather than score its degenerate text-only output.
return BenchmarkResult(
name="text_quality",
severity=BenchmarkSeverity.SKIPPED,
message="Skipped: image-conditioned model; image processor/PIL unavailable",
)
- # No prompt prefix to mask — the whole caption is the content (handled
- # by the is_encoder_decoder path in the scoring loop below).
- generations = [("", text) for _, text in captions]
+ generations = [
+ (ProfilePrompt(prompt="", reference=CAPTION_REFERENCES[i]), text)
+ for i, text in captions
+ if i < len(CAPTION_REFERENCES)
+ ]
primary_generated = captions[0][1]
else:
- with deterministic_rng():
- for i, prompt in enumerate(prompts):
- generated = generator(
- prompt,
- max_new_tokens=max_new_tokens,
- temperature=0.7,
+ prepend_bos = PREPEND_BOS_BY_KIND.get(spec.kind)
+ # Native diffusion samplers take neither return_type nor forced_bos;
+ # bound-method identity can't detect them (new object per access).
+ is_autoregressive = getattr(bridge.adapter, "supports_generation", True)
+ for prompt in profile_prompts:
+ model_input = _build_model_input(bridge, spec, prompt, architecture_id)
+ if is_encoder_decoder:
+ # Encoder input follows the tokenizer's own recipe (lang
+ # token + trailing ); to_tokens' BOS policy corrupts it
+ # (m2m100 loops on a stray ).
+ prompt_ids = bridge.tokenizer(model_input, return_tensors="pt")["input_ids"].to(
+ bridge.cfg.device
)
- if not isinstance(generated, str) or len(generated.strip()) == 0:
- continue
- generations.append((prompt, generated))
- if i == 0:
- primary_generated = generated
+ else:
+ prompt_ids = bridge.to_tokens(model_input, prepend_bos=prepend_bos)
+ gen_kwargs: dict = {
+ "max_new_tokens": max_new_tokens,
+ "temperature": TEMPERATURE_BY_KIND.get(spec.kind, 0.7),
+ }
+ if is_autoregressive:
+ gen_kwargs["return_type"] = "tokens"
+ if forced_bos is not None:
+ gen_kwargs["forced_bos_token_id"] = forced_bos
+ # Seeded per prompt so each sample stream is independent of the
+ # previous prompt's length.
+ with deterministic_rng():
+ out = generator(prompt_ids, **gen_kwargs)
+ if not isinstance(out, torch.Tensor):
+ continue
+ generated_ids = out[0, 1:] if is_encoder_decoder else out[0, prompt_ids.shape[-1] :]
+ generated = bridge.tokenizer.decode(generated_ids, skip_special_tokens=True)
+ if spec.kind == "task:denoise" and denoise_style == "t5":
+ # Splice the fill back so both ratio sides are full
+ # sentences (bare fragments judge in the thousands). An
+ # EMPTY fill must stay empty or a dead model inherits the
+ # near-reference sentence and a free 100.
+ if generated.strip():
+ generated = prompt.prompt.replace("", generated.strip())
+ # Empty output is a scored failure (0), not a dropped sample —
+ # dropping it would average only over the prompts that worked.
+ generations.append((prompt, generated))
+ if not primary_generated:
+ primary_generated = generated
if len(generations) == 0:
return BenchmarkResult(
name="text_quality",
severity=BenchmarkSeverity.DANGER,
- message="Generation produced empty output for all prompts",
+ message="Generation produced no scoreable output for any prompt",
passed=False,
)
- # Load scoring model if not pre-loaded by caller
- if scoring_model is None or tokenizer is None:
- scoring_model, tokenizer = _load_scoring_model(scoring_model_name, device)
+ if judge_model is None or tokenizer is None:
+ judge_model, tokenizer = load_judge()
_loaded_locally = True
- # Score each continuation
+ # Judge context per kind is JUDGE_CONTEXT_KINDS' call. Translation is
+ # scored jointly: per-sentence judge perplexity on the short pivots is
+ # unstable (measured spread 4.8-3497), so the samples concatenate into
+ # one gen/ref pair.
+ # Captured pre-merge so translation keeps its per-sentence texts.
+ all_generated_texts = [text for _, text in generations]
+
+ if spec.kind == "task:translation" and len(generations) > 1:
+ joiner = "" if spec.lang in ("zh", "ja") else " "
+ joint = ProfilePrompt(
+ prompt="",
+ reference=joiner.join(g[0].reference for g in generations),
+ lang=spec.lang,
+ )
+ generations = [(joint, joiner.join(g[1] for g in generations))]
+
+ sample_lang = spec.lang if spec.kind != "caption" else "en"
per_prompt_scores = []
- per_prompt_perplexities = []
+ per_prompt_ratios = []
per_prompt_penalties = []
prompt_details_parts = []
- for prompt, full_text in generations:
- # For encoder-decoder output there is no prompt-in-continuation to
- # mask; score the entire generated sequence.
- score_prompt = "" if is_encoder_decoder else prompt
- perplexity, error = _compute_continuation_perplexity(
- score_prompt, full_text, tokenizer, scoring_model, device
- )
- if error is not None:
+ for prompt, generated in generations:
+ context = prompt.prompt if spec.kind in JUDGE_CONTEXT_KINDS else ""
+
+ gen_token_count = len(tokenizer(generated)["input_ids"]) if generated.strip() else 0
+ if gen_token_count < 2:
+ # Empty or one-token output is a scored failure, not a dropped
+ # sample (Florence-style bare EOS, dead generation).
+ per_prompt_scores.append(0.0)
+ per_prompt_ratios.append(float("inf"))
+ per_prompt_penalties.append(0.0)
+ prompt_details_parts.append("score=0.0 (output < 2 tokens)")
+ continue
+ check_lang = prompt.lang if spec.kind != "task:translation" else sample_lang
+ if _wrong_language(generated, check_lang):
+ # Fluency-only ratio scoring would rate untranslated or
+ # wrong-language output above the reference; hard zero.
+ per_prompt_scores.append(0.0)
+ per_prompt_ratios.append(float("inf"))
+ per_prompt_penalties.append(0.0)
+ prompt_details_parts.append(f"score=0.0 (output not in '{check_lang}')")
continue
- raw_score = _perplexity_to_score(perplexity)
+ gen_ppl, gen_err = _judge_perplexity(generated, context, tokenizer, judge_model)
+ ref_ppl, ref_err = _judge_perplexity(prompt.reference, context, tokenizer, judge_model)
+ if gen_err is not None:
+ # The model's own output was unjudgeable — scored failure.
+ per_prompt_scores.append(0.0)
+ per_prompt_ratios.append(float("inf"))
+ per_prompt_penalties.append(0.0)
+ prompt_details_parts.append(f"score=0.0 ({gen_err})")
+ continue
+ if ref_err is not None:
+ # Our reference failed to judge — a data problem, not the
+ # model's; exclude the sample and say so.
+ prompt_details_parts.append(f"excluded (reference: {ref_err})")
+ continue
- # Repetition penalty on continuation only
- continuation = full_text[len(score_prompt) :]
- rep_penalty = _compute_repetition_penalty(continuation)
- adjusted_score = raw_score * rep_penalty
+ ratio = gen_ppl / ref_ppl if ref_ppl > 0 else float("inf")
+ rep_penalty = _compute_repetition_penalty(generated)
+ ref_token_count = len(tokenizer(prompt.reference)["input_ids"])
+ len_penalty = _length_penalty(gen_token_count, ref_token_count)
+ adjusted_score = _ratio_to_score(ratio) * rep_penalty * len_penalty
per_prompt_scores.append(adjusted_score)
- per_prompt_perplexities.append(perplexity)
+ per_prompt_ratios.append(ratio)
per_prompt_penalties.append(rep_penalty)
prompt_details_parts.append(
- f"ppl={perplexity:.1f} score={adjusted_score:.1f} rep={rep_penalty:.2f}"
+ f"ratio={ratio:.2f} ppl={gen_ppl:.1f} ref_ppl={ref_ppl:.1f} "
+ f"rep={rep_penalty:.2f} len={len_penalty:.2f} score={adjusted_score:.1f}"
)
if len(per_prompt_scores) == 0:
@@ -372,19 +635,26 @@ def benchmark_text_quality(
)
avg_score = sum(per_prompt_scores) / len(per_prompt_scores)
- avg_perplexity = sum(per_prompt_perplexities) / len(per_prompt_perplexities)
+ finite_ratios = [r for r in per_prompt_ratios if math.isfinite(r)]
+ avg_ratio = sum(finite_ratios) / len(finite_ratios) if finite_ratios else float("inf")
avg_rep_penalty = sum(per_prompt_penalties) / len(per_prompt_penalties)
+ pass_threshold = p4_pass_threshold()
details = {
"score": round(avg_score, 1),
- "avg_perplexity": round(avg_perplexity, 2),
+ "prompt_profile": str(spec),
+ "judge_model": JUDGE_MODEL_ID,
+ "judge_revision": JUDGE_REVISION,
+ "avg_ratio": round(avg_ratio, 3) if math.isfinite(avg_ratio) else "inf",
"avg_repetition_penalty": round(avg_rep_penalty, 2),
"num_prompts": len(per_prompt_scores),
"per_prompt": " | ".join(prompt_details_parts),
- "scoring_model": scoring_model_name,
"max_new_tokens": max_new_tokens,
"generated_text": primary_generated,
+ "generated_texts": all_generated_texts,
}
+ if adjustment:
+ details["profile_adjustment"] = adjustment
if avg_score >= pass_threshold:
return BenchmarkResult(
@@ -392,18 +662,17 @@ def benchmark_text_quality(
severity=BenchmarkSeverity.INFO,
message=(
f"Text quality score: {avg_score:.1f}/100 "
- f"(avg perplexity: {avg_perplexity:.1f}, "
- f"{len(per_prompt_scores)} prompts)"
+ f"(profile {spec}, {len(per_prompt_scores)} prompts)"
),
details=details,
)
- elif avg_score >= 80.0:
+ elif avg_score >= pass_threshold / 2:
return BenchmarkResult(
name="text_quality",
severity=BenchmarkSeverity.WARNING,
message=(
f"Text quality score: {avg_score:.1f}/100 "
- f"(below {pass_threshold}, avg perplexity: {avg_perplexity:.1f})"
+ f"(below {pass_threshold:.0f}, profile {spec})"
),
details=details,
passed=False,
@@ -414,8 +683,7 @@ def benchmark_text_quality(
severity=BenchmarkSeverity.DANGER,
message=(
f"Text quality score: {avg_score:.1f}/100 "
- f"(avg perplexity: {avg_perplexity:.1f}) "
- f"— generated text may be incoherent"
+ f"(profile {spec}) — generated text may be incoherent"
),
details=details,
passed=False,
@@ -431,13 +699,8 @@ def benchmark_text_quality(
finally:
if _loaded_locally:
- if scoring_model is not None:
- del scoring_model
+ if judge_model is not None:
+ del judge_model
if tokenizer is not None:
del tokenizer
gc.collect()
- if device != "cpu" and torch.cuda.is_available():
- torch.cuda.empty_cache()
- if device == "mps" and hasattr(torch, "mps") and hasattr(torch.mps, "empty_cache"):
- torch.mps.synchronize()
- torch.mps.empty_cache()
diff --git a/transformer_lens/benchmarks/text_quality_profiles.py b/transformer_lens/benchmarks/text_quality_profiles.py
new file mode 100644
index 0000000000..2b09083bcb
--- /dev/null
+++ b/transformer_lens/benchmarks/text_quality_profiles.py
@@ -0,0 +1,1141 @@
+"""Prompt profiles and reference data for the Phase-4 text-quality benchmark.
+
+Each verified model is scored on prompts a real user would feed it (its
+``prompt_profile``): chat models get their chat template, translation models get
+source sentences, code models get code, multilingual models get their own
+language. Every prompt carries a known-good reference completion; scoring is the
+ratio of judge perplexities PPL(generated)/PPL(reference), which cancels the
+judge's per-language handicap.
+
+Profile resolution is curation-first because Hub metadata is unreliable
+(observed live 2026-08-20): ``bigscience/mt0-base`` is mis-tagged
+``text-generation``; ``facebook/m2m100_418M`` and ``google/long-t5-tglobal-base``
+have no ``pipeline_tag`` at all; the ``conversational`` tag is added by HF for
+*any* repo shipping a chat template, including base models like
+``Qwen/Qwen2.5-0.5B``; Helsinki-NLP language tags are unordered, so Marian
+direction must come from the model id. Precedence: per-model override >
+architecture rule > fetched HF signals > stored registry value > default.
+
+Pivot sentences are from Tatoeba (https://tatoeba.org, CC BY 2.0 FR); source
+sentence ids are noted inline. Everything else is hand-authored.
+
+This module stays stdlib-only: the registry scraper imports it at scan time.
+
+Language x kind coverage (prompts exist where marked; uncovered combinations
+SKIP with a file-an-issue message, they never score against wrong-language
+data):
+
+ kind en fr es de zh ja ru ar hi it nl pt ro code
+ continuation x x x x x x x x - - - - - x
+ chat x x x x x x x x - - - - - -
+ task:instruction x x - - x - - - - - - - - -
+ task:summarization x x - - x - - - - - - - - -
+ task:denoise x - - - - - - - - - - - - -
+ PIVOT (translation) x x x x x x x x x x x x - -
+
+hi/it/nl/pt have pivot coverage only (translation targets); ro exists only in
+NLLB_CODES. Filling continuation/chat for those plus it/nl/pt/hi bake-off
+calibration is tracked as a follow-up.
+"""
+
+from __future__ import annotations
+
+import math
+from dataclasses import dataclass
+from typing import Optional
+
+PROFILE_KINDS = (
+ "continuation",
+ "chat",
+ "task:instruction",
+ "task:translation",
+ "task:summarization",
+ "task:denoise",
+ "caption",
+)
+
+
+@dataclass(frozen=True)
+class ProfileSpec:
+ """A parsed prompt profile: what to feed the model and in which language."""
+
+ kind: str
+ lang: str = "en"
+ src: Optional[str] = None # translation source language
+
+ @classmethod
+ def parse(cls, spec: str) -> "ProfileSpec":
+ """Parse ``kind[@lang]`` (translation: ``@src-tgt``); '@' because task kinds contain ':'."""
+ kind, _, lang = spec.partition("@")
+ if kind not in PROFILE_KINDS:
+ raise ValueError(f"Unknown profile kind {kind!r} in {spec!r}")
+ if not lang:
+ return cls(kind=kind)
+ if kind == "task:translation":
+ src, sep, tgt = lang.partition("-")
+ if not sep or not src or not tgt:
+ raise ValueError(f"Translation profile needs '@src-tgt', got {spec!r}")
+ return cls(kind=kind, lang=tgt, src=src)
+ return cls(kind=kind, lang=lang)
+
+ def __str__(self) -> str:
+ if self.kind == "task:translation" and self.src:
+ return f"{self.kind}@{self.src}-{self.lang}"
+ if self.lang != "en":
+ return f"{self.kind}@{self.lang}"
+ return self.kind
+
+
+@dataclass(frozen=True)
+class ProfilePrompt:
+ """One scored sample: model input and a known-good reference completion."""
+
+ prompt: str
+ reference: str
+ lang: str = "en"
+
+
+DEFAULT_PROFILE = ProfileSpec("continuation", "en")
+
+
+def is_default_profile(profile) -> bool:
+ """One sparse-encoding rule for every registry writer: the bare default
+ continuation@en profile is never stored (a lang-tagged continuation is)."""
+ if isinstance(profile, ProfileSpec):
+ return profile == DEFAULT_PROFILE
+ try:
+ return ProfileSpec.parse(str(profile)) == DEFAULT_PROFILE
+ except ValueError:
+ return False
+
+
+# ---------------------------------------------------------------------------
+# Pivot sentences (Tatoeba, CC BY 2.0 FR) — index-aligned across languages.
+# English #1277 / #1284 / #1315; per-language ids in row comments.
+# Feed translation pairs and the judge bake-off's fluent corpus.
+# ---------------------------------------------------------------------------
+
+PIVOT_SENTENCES: dict[str, tuple[str, str, str]] = {
+ "en": ( # 1277, 1284, 1315
+ "I have to go to sleep.",
+ "I will be back soon.",
+ "I can't live that kind of life.",
+ ),
+ "fr": ( # 373908, 3099, 3131
+ "Je dois aller dormir.",
+ "Je serai bientôt de retour.",
+ "Je ne peux pas vivre comme ça.",
+ ),
+ "es": ( # 2482, 2489, 2521
+ "Tengo que irme a dormir.",
+ "Volveré pronto.",
+ "No puedo vivir así.",
+ ),
+ "de": ( # 1195088, 85, 117
+ "Ich muss schlafen.",
+ "Ich werde bald zurück sein.",
+ "Ich kann so ein Leben nicht leben.",
+ ),
+ "it": ( # 4369, 375118, 2733911
+ "Devo andare a dormire.",
+ "Torno subito.",
+ "Non posso vivere quel tipo di vita.",
+ ),
+ "nl": ( # 5966, 5984, 378741
+ "Ik moet gaan slapen.",
+ "Ik ben zo terug.",
+ "Ik kan zo niet leven.",
+ ),
+ "pt": ( # 182184, 331974, 405254
+ "Preciso ir dormir.",
+ "Voltarei em breve.",
+ "Eu não posso viver esse tipo de vida.",
+ ),
+ "ru": ( # 5410, 374353, 5449
+ "Мне пора идти спать.",
+ "Я скоро вернусь.",
+ "Я так жить не могу.",
+ ),
+ "zh": ( # 2, 9 (Hans transcription), 35 — one script; mixing traditional
+ # into a simplified-dominant judge destroys that row's zero point.
+ "我该去睡觉了。",
+ "我很快就会回来。",
+ "我不能这样活着。",
+ ),
+ "ja": ( # 4703, 4709, 4742
+ "私は眠らなければなりません。",
+ "すぐに戻ります。",
+ "私はそんな風には生きられない。",
+ ),
+ "ar": ( # 372962, 400781, 549626
+ "عليّ أن أنام.",
+ "سأعود قريباً.",
+ "لا أستطيع أن أعيش حياة كتلك.",
+ ),
+ "hi": ( # 3792910, 3793971, 11371181
+ "मुझे सोना है।",
+ "मैं जल्द लौटूंगी।",
+ "मैं ऐसी जिंदगी नहीं जी सकता।",
+ ),
+}
+
+# ---------------------------------------------------------------------------
+# Continuation prompts. English is seeded from the pre-rework default prompts so
+# control-model scores stay comparable. "code" is a language here: code models
+# continue code the way prose models continue prose.
+# ---------------------------------------------------------------------------
+
+CONTINUATION_PROMPTS: dict[str, tuple[ProfilePrompt, ...]] = {
+ "en": (
+ ProfilePrompt(
+ "The theory of relativity explains that",
+ " time and space are not absolute but depend on the observer's "
+ "motion, so clocks moving at high speed tick more slowly than "
+ "clocks at rest.",
+ ),
+ ProfilePrompt(
+ "In the dense forests of the Amazon,",
+ " thousands of plant and animal species live in a delicate "
+ "balance, and scientists continue to discover new ones every year.",
+ ),
+ ProfilePrompt(
+ "Modern computing relies heavily on",
+ " fast processors and large amounts of memory, which allow "
+ "software to handle enormous quantities of data in real time.",
+ ),
+ ProfilePrompt(
+ "The city library opens early on weekdays, and",
+ # Judge PPL 8.7 (en median 8.9). References must stay within
+ # ~3.5x of the language median or this prompt's bar loosens
+ # proportionally; the integration test pins the band.
+ " many people stop by in the morning to read or borrow books before work.",
+ ),
+ ),
+ "fr": (
+ ProfilePrompt(
+ "La tour Eiffel est l'un des monuments",
+ " les plus célèbres du monde, et des millions de visiteurs "
+ "montent chaque année à son sommet pour admirer Paris.",
+ lang="fr",
+ ),
+ ProfilePrompt(
+ "Chaque matin, le boulanger du village",
+ " prépare du pain frais et des croissants que les habitants "
+ "viennent acheter dès l'ouverture de la boutique.",
+ lang="fr",
+ ),
+ ProfilePrompt(
+ "La science moderne repose sur",
+ " l'observation, l'expérience et le raisonnement, qui permettent "
+ "de comprendre les lois de la nature.",
+ lang="fr",
+ ),
+ ProfilePrompt(
+ "Pendant l'hiver, les montagnes",
+ " se couvrent de neige et attirent de nombreux skieurs venus de " "toute l'Europe.",
+ lang="fr",
+ ),
+ ),
+ "es": (
+ ProfilePrompt(
+ "El clima de la región mediterránea es",
+ " templado, con veranos secos y calurosos e inviernos suaves y "
+ "lluviosos, ideal para el cultivo de olivos.",
+ lang="es",
+ ),
+ ProfilePrompt(
+ "Cada domingo por la mañana, el mercado",
+ " se llena de gente que compra fruta fresca, verduras y flores a "
+ "los vendedores locales.",
+ lang="es",
+ ),
+ ProfilePrompt(
+ "La historia de América Latina está marcada por",
+ " una gran diversidad cultural, fruto del encuentro entre pueblos "
+ "indígenas, europeos y africanos.",
+ lang="es",
+ ),
+ ProfilePrompt(
+ "Los avances de la medicina moderna permiten",
+ " tratar enfermedades que hace pocas décadas se consideraban "
+ "incurables, y prolongar la vida de millones de personas.",
+ lang="es",
+ ),
+ ),
+ "de": (
+ ProfilePrompt(
+ "Der Schwarzwald ist bekannt für",
+ " seine dichten Wälder, tiefen Täler und traditionellen "
+ "Bauernhäuser, die jedes Jahr viele Wanderer anziehen.",
+ lang="de",
+ ),
+ ProfilePrompt(
+ "Jeden Morgen fährt der Zug",
+ " pünktlich um sieben Uhr vom Hauptbahnhof ab und bringt die "
+ "Pendler in die umliegenden Städte zur Arbeit.",
+ lang="de",
+ ),
+ ProfilePrompt(
+ "Die deutsche Sprache hat",
+ " viele lange zusammengesetzte Wörter, die Lernende oft "
+ "überraschen, aber einer klaren Logik folgen.",
+ lang="de",
+ ),
+ ProfilePrompt(
+ "In der modernen Industrie spielen Roboter",
+ " eine immer größere Rolle, weil sie schwere und gefährliche "
+ "Arbeiten schneller und sicherer erledigen können.",
+ lang="de",
+ ),
+ ),
+ "zh": (
+ ProfilePrompt(
+ "长城是中国古代",
+ "伟大的防御工程,绵延数千公里,每年吸引大量游客前来参观。",
+ lang="zh",
+ ),
+ ProfilePrompt(
+ "每天早晨,公园里",
+ "有许多老人打太极拳、散步和下棋,气氛十分热闹。",
+ lang="zh",
+ ),
+ ProfilePrompt(
+ "现代科技的发展使得",
+ "人们的生活越来越方便,购物、学习和工作都可以在网上完成。",
+ lang="zh",
+ ),
+ ProfilePrompt(
+ "春天到了,山上的",
+ "花都开了,许多家庭趁着周末去郊外踏青赏花。",
+ lang="zh",
+ ),
+ ),
+ "ja": (
+ ProfilePrompt(
+ "日本の四季は",
+ "それぞれ美しく、春には桜、秋には紅葉を楽しむために多くの人が旅行に出かけます。",
+ lang="ja",
+ ),
+ ProfilePrompt(
+ "毎朝、駅の周りには",
+ "通勤や通学の人々が行き交い、店が次々と開き始めます。",
+ lang="ja",
+ ),
+ ProfilePrompt(
+ "現代の技術の進歩により、",
+ "私たちの生活はますます便利になり、買い物も勉強も家にいながらできるようになりました。",
+ lang="ja",
+ ),
+ ProfilePrompt(
+ "図書館は静かな場所で、",
+ "学生たちが本を読んだり、勉強したりするのに最適です。",
+ lang="ja",
+ ),
+ ),
+ "ru": (
+ ProfilePrompt(
+ "Зимой в Сибири",
+ " очень холодно, температура часто опускается ниже сорока "
+ "градусов, но местные жители привыкли к таким морозам.",
+ lang="ru",
+ ),
+ ProfilePrompt(
+ "Каждое утро студенты",
+ " спешат на занятия в университет, а вечером собираются в "
+ "библиотеке, чтобы готовиться к экзаменам.",
+ lang="ru",
+ ),
+ ProfilePrompt(
+ "Современная наука позволяет",
+ " лечить болезни, которые раньше считались неизлечимыми, и "
+ "продлевать жизнь миллионам людей.",
+ lang="ru",
+ ),
+ ProfilePrompt(
+ "Русская литература известна",
+ " во всём мире благодаря произведениям Толстого, Достоевского и "
+ "Чехова, которые переведены на десятки языков.",
+ lang="ru",
+ ),
+ ),
+ "ar": (
+ ProfilePrompt(
+ "تشتهر مدينة القاهرة",
+ " بتاريخها العريق ومساجدها القديمة وأسواقها الشعبية التي يزورها "
+ "السياح من جميع أنحاء العالم.",
+ lang="ar",
+ ),
+ ProfilePrompt(
+ "في كل صباح يذهب الطلاب",
+ " إلى المدرسة مبكرين، ويقضون اليوم في تعلم القراءة والكتابة " "والعلوم.",
+ lang="ar",
+ ),
+ ProfilePrompt(
+ "يساعد التقدم العلمي الحديث",
+ " الأطباء على علاج أمراض كانت تعتبر مستعصية قبل عقود قليلة.",
+ lang="ar",
+ ),
+ ProfilePrompt(
+ "تعتبر اللغة العربية",
+ " من أقدم اللغات الحية في العالم، ويتحدث بها ملايين الناس في " "الوطن العربي وخارجه.",
+ lang="ar",
+ ),
+ ),
+ "code": (
+ ProfilePrompt(
+ 'def is_prime(n):\n """Return True if n is a prime number."""\n',
+ " if n < 2:\n return False\n"
+ " for i in range(2, int(n ** 0.5) + 1):\n"
+ " if n % i == 0:\n return False\n"
+ " return True\n",
+ lang="code",
+ ),
+ ProfilePrompt(
+ 'def count_words(text):\n """Count occurrences of each word in text."""\n',
+ " counts = {}\n for word in text.split():\n"
+ " counts[word] = counts.get(word, 0) + 1\n"
+ " return counts\n",
+ lang="code",
+ ),
+ ProfilePrompt(
+ "def fibonacci(n):\n" ' """Return the first n Fibonacci numbers as a list."""\n',
+ " result = []\n a, b = 0, 1\n"
+ " for _ in range(n):\n result.append(a)\n"
+ " a, b = b, a + b\n return result\n",
+ lang="code",
+ ),
+ ProfilePrompt(
+ "// Return the largest number in the array.\n" "function findMax(numbers) {\n",
+ " let max = numbers[0];\n"
+ " for (const n of numbers) {\n"
+ " if (n > max) max = n;\n }\n"
+ " return max;\n}\n",
+ lang="code",
+ ),
+ ),
+}
+
+# ---------------------------------------------------------------------------
+# Chat prompts: realistic user turns (rendered through the tokenizer's chat
+# template at run time) with a good assistant reply as reference.
+# ---------------------------------------------------------------------------
+
+CHAT_PROMPTS: dict[str, tuple[ProfilePrompt, ...]] = {
+ "en": (
+ ProfilePrompt(
+ "How do I keep basil alive indoors?",
+ "Keep basil in a warm spot with at least six hours of sunlight a "
+ "day, water it when the top of the soil feels dry, and pinch off "
+ "flower buds so the plant keeps producing leaves.",
+ ),
+ ProfilePrompt(
+ "What's a good way to remember people's names?",
+ "Repeat the name right after you hear it, use it once or twice in "
+ "conversation, and link it to something memorable about the "
+ "person, like their job or where you met.",
+ ),
+ ProfilePrompt(
+ "Explain why the sky is blue in simple terms.",
+ "Sunlight is made of many colors, and the air scatters blue light "
+ "more than the other colors because blue travels in shorter "
+ "waves. When you look up, that scattered blue light is what you "
+ "see.",
+ ),
+ ),
+ "fr": (
+ ProfilePrompt(
+ "Comment préparer un bon café à la maison ?",
+ "Utilisez du café fraîchement moulu, une eau à environ 90 degrés "
+ "et un dosage d'une cuillère à soupe par tasse. Laissez infuser "
+ "quelques minutes avant de servir.",
+ lang="fr",
+ ),
+ ProfilePrompt(
+ "Quels sont les avantages de la lecture quotidienne ?",
+ "Lire chaque jour enrichit le vocabulaire, améliore la "
+ "concentration et réduit le stress. C'est aussi un excellent "
+ "moyen de découvrir de nouvelles idées.",
+ lang="fr",
+ ),
+ ProfilePrompt(
+ "Explique-moi simplement pourquoi les feuilles tombent en automne.",
+ "En automne, les jours raccourcissent et les arbres reçoivent "
+ "moins de lumière. Ils cessent de nourrir leurs feuilles, qui "
+ "changent de couleur puis tombent pour économiser l'énergie "
+ "pendant l'hiver.",
+ lang="fr",
+ ),
+ ),
+ "es": (
+ ProfilePrompt(
+ "¿Cómo puedo mejorar mi memoria para estudiar?",
+ "Estudia en sesiones cortas y regulares, repasa lo aprendido al "
+ "día siguiente y explica el tema en voz alta con tus propias "
+ "palabras. Dormir bien también ayuda mucho a fijar los "
+ "recuerdos.",
+ lang="es",
+ ),
+ ProfilePrompt(
+ "¿Qué debo tener en cuenta al adoptar un gato?",
+ "Prepara un espacio tranquilo con comida, agua y un arenero "
+ "limpio. Dale tiempo para adaptarse, llévalo al veterinario para "
+ "sus vacunas y juega con él todos los días.",
+ lang="es",
+ ),
+ ProfilePrompt(
+ "Explícame de forma sencilla cómo funciona un molino de viento.",
+ "El viento empuja las aspas del molino y las hace girar. Ese giro "
+ "mueve un eje conectado a una máquina o a un generador, que "
+ "convierte el movimiento en trabajo útil o en electricidad.",
+ lang="es",
+ ),
+ ),
+ "de": (
+ ProfilePrompt(
+ "Wie kann ich beim Einkaufen Geld sparen?",
+ "Schreiben Sie vorher eine Einkaufsliste und halten Sie sich "
+ "daran, vergleichen Sie Preise und kaufen Sie saisonale "
+ "Produkte. Große Packungen lohnen sich nur, wenn Sie alles "
+ "verbrauchen.",
+ lang="de",
+ ),
+ ProfilePrompt(
+ "Was ist ein guter Weg, eine neue Sprache zu lernen?",
+ "Üben Sie jeden Tag ein wenig, hören Sie Podcasts oder Musik in "
+ "der Sprache und sprechen Sie so früh wie möglich mit "
+ "Muttersprachlern. Regelmäßigkeit ist wichtiger als lange "
+ "Lerneinheiten.",
+ lang="de",
+ ),
+ ProfilePrompt(
+ "Erkläre mir einfach, warum es Ebbe und Flut gibt.",
+ "Der Mond zieht mit seiner Schwerkraft am Wasser der Ozeane. Auf "
+ "der dem Mond zugewandten Seite der Erde hebt sich das Wasser, "
+ "und während sich die Erde dreht, wandert dieser Wasserberg — so "
+ "entstehen Ebbe und Flut.",
+ lang="de",
+ ),
+ ),
+ "zh": (
+ ProfilePrompt(
+ "怎样才能养成早起的习惯?",
+ "每天固定同一时间睡觉和起床,睡前少看手机,把闹钟放在离床远一点的地方。坚持两三个星期,身体就会慢慢适应新的作息。",
+ lang="zh",
+ ),
+ ProfilePrompt(
+ "第一次做饭应该注意什么?",
+ "先从简单的菜开始,提前准备好所有材料,注意用火安全,切菜时小心手指。做完后记得关闭燃气,慢慢积累经验就会越来越熟练。",
+ lang="zh",
+ ),
+ ProfilePrompt(
+ "请用简单的话解释为什么会下雨。",
+ "太阳把地面上的水晒热,水变成水蒸气升到天上,遇冷凝结成小水滴,聚在一起形成云。当水滴越来越重,云托不住它们时,就落下来变成雨。",
+ lang="zh",
+ ),
+ ),
+ "ja": (
+ ProfilePrompt(
+ "朝型の生活に変えるにはどうすればいいですか?",
+ "毎日同じ時間に寝起きし、寝る前はスマートフォンを見ないようにしましょう。朝に日光を浴びると体内時計が整い、二、三週間続ければ自然に朝型になります。",
+ lang="ja",
+ ),
+ ProfilePrompt(
+ "初めての一人暮らしで気をつけることは何ですか?",
+ "毎月の家賃や食費など生活費の計画を立て、無理のない範囲で貯金をしましょう。防犯のために戸締まりを忘れず、近所のスーパーや病院の場所も早めに確認しておくと安心です。",
+ lang="ja",
+ ),
+ ProfilePrompt(
+ "虹がどうしてできるのか、簡単に説明してください。",
+ "雨上がりの空気中には小さな水滴がたくさん残っています。太陽の光がその水滴の中で曲がって反射すると、光が七つの色に分かれて見えます。これが虹です。",
+ lang="ja",
+ ),
+ ),
+ "ru": (
+ ProfilePrompt(
+ "Как научиться рано вставать?",
+ "Ложитесь и вставайте в одно и то же время каждый день, не "
+ "смотрите в телефон перед сном и ставьте будильник подальше от "
+ "кровати. Через пару недель организм привыкнет к новому режиму.",
+ lang="ru",
+ ),
+ ProfilePrompt(
+ "Что почитать, чтобы полюбить чтение?",
+ "Начните с коротких книг на темы, которые вам действительно "
+ "интересны, — детективы, приключения или научно-популярные "
+ "рассказы. Главное — читать понемногу каждый день и не "
+ "заставлять себя дочитывать скучное.",
+ lang="ru",
+ ),
+ ProfilePrompt(
+ "Объясни простыми словами, почему летом жарко, а зимой холодно.",
+ "Земля вращается вокруг Солнца с наклонённой осью. Летом наше "
+ "полушарие наклонено к Солнцу, лучи падают прямее и сильнее "
+ "нагревают землю. Зимой оно отклонено от Солнца, лучи идут под "
+ "углом и греют слабее.",
+ lang="ru",
+ ),
+ ),
+ "ar": (
+ ProfilePrompt(
+ "كيف أنظم وقتي أثناء الدراسة؟",
+ "قسّم يومك إلى فترات قصيرة للدراسة مع فترات راحة منتظمة، وابدأ "
+ "بأصعب المواد عندما يكون ذهنك صافياً. اكتب قائمة بالمهام كل صباح "
+ "والتزم بها قدر الإمكان.",
+ lang="ar",
+ ),
+ ProfilePrompt(
+ "ما هي فوائد المشي اليومي؟",
+ "المشي كل يوم يقوي القلب والعضلات ويساعد على تخفيف التوتر "
+ "وتحسين المزاج. كما أنه يساعد على النوم بشكل أفضل ولا يحتاج إلى "
+ "أي معدات خاصة.",
+ lang="ar",
+ ),
+ ProfilePrompt(
+ "اشرح لي ببساطة كيف تصنع النحلة العسل.",
+ "تجمع النحلة رحيق الأزهار وتخزنه في معدة خاصة، ثم تعود إلى "
+ "الخلية وتسلمه لنحلات أخرى تضيف إليه مواد تحوله إلى عسل. بعد ذلك "
+ "يوضع العسل في الأقراص الشمعية ويجفف بتحريك الأجنحة حتى ينضج.",
+ lang="ar",
+ ),
+ ),
+}
+
+# ---------------------------------------------------------------------------
+# Task prompts.
+# ---------------------------------------------------------------------------
+
+SUMMARIZATION_PROMPTS: dict[str, tuple[ProfilePrompt, ...]] = {
+ "en": (
+ ProfilePrompt(
+ "The city council voted on Tuesday to approve funding for a new "
+ "public library in the downtown district. The project, which has "
+ "been debated for over two years, will cost an estimated twelve "
+ "million dollars and is expected to open in the spring of 2028. "
+ "Supporters argued that the current library, built in 1962, is "
+ "too small and lacks modern facilities. Opponents raised "
+ "concerns about the cost and the loss of a parking lot at the "
+ "proposed site. The mayor said the new building would include "
+ "community meeting rooms, a children's wing, and free computer "
+ "access for residents.",
+ "The city council approved a twelve million dollar downtown "
+ "library, expected to open in spring 2028, replacing the "
+ "outdated 1962 building despite concerns over cost and parking.",
+ ),
+ ProfilePrompt(
+ "Researchers at a European university have published a study "
+ "showing that regular walking can significantly improve sleep "
+ "quality in adults over sixty. The study followed four hundred "
+ "participants for one year, half of whom walked for thirty "
+ "minutes a day while the other half kept their usual habits. "
+ "Those in the walking group fell asleep faster, woke less often "
+ "during the night, and reported feeling more rested in the "
+ "morning. The researchers noted that the benefits appeared "
+ "within the first two months and lasted for the rest of the "
+ "study.",
+ "A year-long study of four hundred older adults found that "
+ "walking thirty minutes daily improved sleep quality within two "
+ "months, helping participants fall asleep faster and wake less "
+ "often.",
+ ),
+ ProfilePrompt(
+ "A severe storm swept through the coastal region on Friday "
+ "night, leaving thousands of homes without electricity and "
+ "forcing the closure of the main highway. Emergency crews worked "
+ "through the weekend to clear fallen trees and restore power "
+ "lines. Officials said no serious injuries were reported, though "
+ "several boats were damaged in the harbor. Schools in the area "
+ "remained closed on Monday while cleanup continued, and "
+ "residents were advised to avoid the beachfront until inspectors "
+ "declared it safe.",
+ "A Friday night storm cut power to thousands of coastal homes "
+ "and closed the main highway; crews restored services over the "
+ "weekend with no serious injuries reported.",
+ ),
+ ),
+}
+
+INSTRUCTION_PROMPTS: dict[str, tuple[ProfilePrompt, ...]] = {
+ "en": (
+ ProfilePrompt(
+ "List three things to pack for a day hike.",
+ "Water, snacks, and a map of the trail.",
+ ),
+ ProfilePrompt(
+ "Write one sentence describing what a lighthouse does.",
+ "A lighthouse shines a bright light to guide ships safely along " "the coast at night.",
+ ),
+ ProfilePrompt(
+ "Name the four seasons of the year.",
+ "Spring, summer, autumn, and winter.",
+ ),
+ ),
+}
+
+# Pretrained-only seq2seq models (T5, BART) were trained to fill masked spans,
+# not to follow instructions; feed them their native denoising format.
+DENOISE_PROMPTS: dict[str, tuple[ProfilePrompt, ...]] = {
+ # ONE sentinel per "t5" prompt: the whole special-stripped output is the
+ # fill, spliced back by the runner so both ratio sides are full sentences
+ # (bare span fragments judge in the thousands).
+ "t5": (
+ ProfilePrompt(
+ "The children in the park until the sun went down.",
+ "The children played happily in the park until the sun went down.",
+ ),
+ ProfilePrompt(
+ "Every morning she drinks a cup of and reads the newspaper.",
+ "Every morning she drinks a cup of coffee and reads the newspaper.",
+ ),
+ ProfilePrompt(
+ "The old bridge across the was built many years ago.",
+ "The old bridge across the river was built many years ago.",
+ ),
+ ),
+ "mask": (
+ ProfilePrompt(
+ "The children played in the park until the sun went down.",
+ "The children played happily in the park until the sun went down.",
+ ),
+ ProfilePrompt(
+ "Every morning she drinks a cup of and reads the newspaper.",
+ "Every morning she drinks a cup of coffee and reads the newspaper.",
+ ),
+ ProfilePrompt(
+ "The old bridge across the was built many years ago.",
+ "The old bridge across the river was built many years ago.",
+ ),
+ ),
+}
+
+# References for the synthetic caption images built by the text-quality
+# benchmark (index-aligned with _build_caption_test_images).
+CAPTION_REFERENCES: tuple[str, ...] = (
+ "The image shows a blue rectangle and a green oval on a white background.",
+ "The image shows a large yellow circle on a black background.",
+ "The image shows a dark green rectangle and an orange oval on a light " "blue background.",
+)
+
+# ---------------------------------------------------------------------------
+# Per-kind generation and judging knobs.
+# ---------------------------------------------------------------------------
+
+MAX_NEW_TOKENS_BY_KIND: dict[str, int] = {
+ "continuation": 50,
+ "chat": 64,
+ "task:instruction": 48,
+ "task:translation": 48,
+ "task:summarization": 48,
+ "task:denoise": 24,
+ "caption": 50,
+}
+
+# Chat prompts arrive pre-templated (the template supplies its own BOS);
+# everything else follows the adapter default.
+PREPEND_BOS_BY_KIND: dict[str, Optional[bool]] = {
+ "chat": False,
+}
+
+# Bake-off-measured scoring anchors; scripts/text_quality_judge_bakeoff.py
+# regenerates them (it prints these names verbatim; last run 2026-08-20, full
+# 13-domain corpus). R_FAIL = geo-mean of per-language MEDIAN corrupted/fluent
+# ratios — a low percentile degenerates below 1 in weak-separation languages.
+# R_GOOD = the paraphrase noise floor; score(R_GOOD) is the pass line.
+JUDGE_R_FAIL = 18.1
+JUDGE_R_GOOD = 3.74
+
+
+# Known scale properties (measured during the 2026-08 output audit):
+# - Saturation: any ratio <= 1 scores 100 — "at least reference-fluent" is the
+# top of the scale, with no resolution above it.
+# - Judge family-favoring: the pinned Qwen judge rates Qwen-family models a
+# few points friendlier than others; watch Qwen entries in campaign reruns.
+
+
+def p4_pass_threshold() -> float:
+ """The P4 pass line, derived from the bake-off noise floor. The registry
+ floor imports this so [floor, pass) can never silently diverge again."""
+ return round(100.0 - 100.0 * math.log(JUDGE_R_GOOD) / math.log(JUDGE_R_FAIL), 1)
+
+
+# Registry scale marker for phase4_score. Absent = v1 (unpinned GPT-2,
+# 135-10*ln(ppl), pass 85); 2 = pinned-judge reference-ratio scale (pass 56).
+# The column mixes populations until the backlog is re-run, so every P4 write
+# stamps the scale it was measured on.
+P4_SCORING_VERSION = 2
+
+# Task output is generated greedily — that is how users run translators and
+# summarizers, and it removes sampling variance from a single-sample score.
+# Open-ended kinds keep sampling (greedy makes base models loop).
+TEMPERATURE_BY_KIND: dict[str, float] = {
+ "continuation": 0.7,
+ "chat": 0.7,
+ "task:instruction": 0.0,
+ "task:translation": 0.0,
+ "task:summarization": 0.0,
+ "task:denoise": 0.0,
+ "caption": 0.0,
+}
+
+# Kinds whose judge PPL is conditioned on the prompt — the relevance signal:
+# unconditioned, fluent-but-off-topic or hallucinated output judges as well as
+# a real answer. Translation stays unconditioned (cross-lingual conditioning
+# is noisy; the language check covers it); caption's source is an image the
+# judge cannot read.
+JUDGE_CONTEXT_KINDS = frozenset(
+ {"continuation", "chat", "task:instruction", "task:summarization", "task:denoise"}
+)
+
+# T5-family checkpoints expect a natural-language task prefix on the source.
+T5_PREFIX_ARCHITECTURES = frozenset(
+ {
+ "T5ForConditionalGeneration",
+ "T5WithLMHeadModel",
+ "MT5ForConditionalGeneration",
+ "LongT5ForConditionalGeneration",
+ "SwitchTransformersForConditionalGeneration",
+ "UMT5ForConditionalGeneration",
+ }
+)
+
+# Full NLLB (flores-200) codes for covered languages; transformers 5.x
+# NllbTokenizer resolves them only via convert_tokens_to_ids.
+NLLB_CODES: dict[str, str] = {
+ "en": "eng_Latn",
+ "fr": "fra_Latn",
+ "es": "spa_Latn",
+ "de": "deu_Latn",
+ "it": "ita_Latn",
+ "nl": "nld_Latn",
+ "pt": "por_Latn",
+ "ru": "rus_Cyrl",
+ "zh": "zho_Hans",
+ "ja": "jpn_Jpan",
+ "ar": "arb_Arab",
+ "hi": "hin_Deva",
+ "ro": "ron_Latn",
+}
+
+# ISO 639-3 equivalents for NLLB-style language codes ("deu_Latn").
+LANG_ISO3: dict[str, str] = {
+ "en": "eng",
+ "fr": "fra",
+ "es": "spa",
+ "de": "deu",
+ "it": "ita",
+ "nl": "nld",
+ "pt": "por",
+ "ru": "rus",
+ "zh": "zho",
+ "ja": "jpn",
+ "ar": "ara",
+ "hi": "hin",
+ "ro": "ron",
+}
+
+LANG_NAMES: dict[str, str] = {
+ "en": "English",
+ "fr": "French",
+ "es": "Spanish",
+ "de": "German",
+ "it": "Italian",
+ "nl": "Dutch",
+ "pt": "Portuguese",
+ "ru": "Russian",
+ "zh": "Chinese",
+ "ja": "Japanese",
+ "ar": "Arabic",
+ "hi": "Hindi",
+ "ro": "Romanian",
+}
+
+# ---------------------------------------------------------------------------
+# Curation: architecture rules and per-model overrides.
+# ---------------------------------------------------------------------------
+
+# Architectures whose task is unambiguous. T5/BART/Switch and Falcon/MPT are
+# deliberately absent: their task depends on the checkpoint, so they resolve
+# through overrides or fetched Hub signals.
+ARCHITECTURE_PROFILE_KINDS: dict[str, str] = {
+ "MarianMTModel": "task:translation",
+ "M2M100ForConditionalGeneration": "task:translation",
+ "PegasusForConditionalGeneration": "task:summarization",
+ "LEDForConditionalGeneration": "task:summarization",
+ "BlenderbotForConditionalGeneration": "chat",
+ "BlenderbotSmallForConditionalGeneration": "chat",
+ "GPTBigCodeForCausalLM": "continuation@code",
+ "CodeGenForCausalLM": "continuation@code",
+}
+
+# An unlabelled seq2seq model cannot continue text; its pretraining task is the
+# only prompt it understands.
+SEQ2SEQ_FALLBACK_KIND = "task:denoise"
+
+MODEL_PROFILE_OVERRIDES: dict[str, str] = {
+ # T5 v1.0 checkpoints were multitask-trained with task prefixes; the WMT
+ # en-de pair is their canonical supervised task.
+ "google-t5/t5-small": "task:translation@en-de",
+ "google-t5/t5-base": "task:translation@en-de",
+ "google-t5/t5-large": "task:translation@en-de",
+ "t5-small": "task:translation@en-de",
+ "t5-base": "task:translation@en-de",
+ "t5-large": "task:translation@en-de",
+ # mt0 is instruction-tuned MT5 (Hub mis-tags it text-generation).
+ "bigscience/mt0-small": "task:instruction",
+ "bigscience/mt0-base": "task:instruction",
+ "bigscience/mt0-large": "task:instruction",
+ # Pretrained-only checkpoints: denoising is their only language.
+ "google/long-t5-tglobal-base": "task:denoise",
+ "google/long-t5-local-base": "task:denoise",
+ # Base model that ships a chat template (Hub tags it conversational).
+ "Qwen/Qwen2.5-0.5B": "continuation",
+ # Task depends on the checkpoint for BART (arch rule deliberately absent);
+ # without a scraped registry profile these canonical ones need curation.
+ "facebook/bart-large-cnn": "task:summarization",
+ "facebook/bart-large-xsum": "task:summarization",
+ # MBart has no arch rule (base checkpoints are denoising pretrains, task
+ # varies by fine-tune) — the canonical translators are curated instead.
+ "facebook/mbart-large-50-many-to-many-mmt": "task:translation@en-de",
+ "facebook/mbart-large-50-one-to-many-mmt": "task:translation@en-de",
+ "facebook/mbart-large-50-many-to-one-mmt": "task:translation@de-en",
+ # Indic-language denoiser; English denoise prompts measure the wrong
+ # thing, so this skips until Indic coverage exists.
+ "ai4bharat/IndicBART": "task:denoise@hi",
+ # Code checkpoints on general-purpose architectures.
+ "Salesforce/codegen-350M-mono": "continuation@code",
+ "bigcode/starcoderbase-1b": "continuation@code",
+ "replit/replit-code-v1-3b": "continuation@code",
+}
+
+# ---------------------------------------------------------------------------
+# Hub-signal distillation and profile resolution.
+# ---------------------------------------------------------------------------
+
+# Full ISO 639-1 code set, used to pick language codes out of unstructured Hub
+# tag lists. Complete on purpose: a dropped code silently reroutes a model to
+# English prompts.
+ISO_639_1 = frozenset(
+ "aa ab ae af ak am an ar as av ay az ba be bg bh bi bm bn bo br bs ca ce "
+ "ch co cr cs cu cv cy da de dv dz ee el en eo es et eu fa ff fi fj fo fr "
+ "fy ga gd gl gn gu gv ha he hi ho hr ht hu hy hz ia id ie ig ii ik io is "
+ "it iu ja jv ka kg ki kj kk kl km kn ko kr ks ku kv kw ky la lb lg li ln "
+ "lo lt lu lv mg mh mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv "
+ "ny oc oj om or os pa pi pl ps pt qu rm rn ro ru rw sa sc sd se sg si sk "
+ "sl sm sn so sq sr ss st su sv sw ta te tg th ti tk tl tn to tr ts tt tw "
+ "ty ug uk ur uz ve vi vo wa wo xh yi yo za zh zu".split()
+)
+
+_PIPELINE_TAG_KINDS: dict[str, str] = {
+ "translation": "task:translation",
+ "summarization": "task:summarization",
+ "text2text-generation": "task:denoise",
+ "text-generation": "continuation",
+ "image-text-to-text": "caption",
+ "image-to-text": "caption",
+}
+
+# Hub tags that mark code models (`conversational` is deliberately NOT mapped
+# to chat: HF adds it for any repo shipping a chat template, base models
+# included).
+_CODE_TAGS = frozenset({"code", "code-generation", "coding"})
+
+
+@dataclass(frozen=True)
+class HFSignals:
+ """Distilled Hub metadata for one model, as fetched by the scraper."""
+
+ pipeline_tag: Optional[str] = None
+ languages: tuple[str, ...] = ()
+ tags: tuple[str, ...] = ()
+
+
+def extract_languages(card_data_language: object, tags: object) -> tuple[str, ...]:
+ """Normalize cardData.language (str or list) plus tag-list ISO codes, noise dropped."""
+ langs: list[str] = []
+ if isinstance(card_data_language, str):
+ langs.append(card_data_language.lower())
+ elif isinstance(card_data_language, (list, tuple)):
+ langs.extend(str(item).lower() for item in card_data_language)
+ if isinstance(tags, (list, tuple)):
+ langs.extend(str(t).lower() for t in tags)
+ seen: list[str] = []
+ for lang in langs:
+ # The ISO gate alone filters framework/task tag noise ("pytorch",
+ # "marian", "multilingual" are not ISO 639-1 codes).
+ if lang in ISO_639_1 and lang not in seen:
+ seen.append(lang)
+ if len(seen) >= 8:
+ break
+ return tuple(seen)
+
+
+def _marian_pair_from_model_id(model_id: str) -> Optional[tuple[str, str]]:
+ """Parse opus-mt-{src}-{tgt} from the id; Helsinki-NLP language tags are unordered."""
+ name = model_id.rsplit("/", 1)[-1].lower()
+ if not name.startswith("opus-mt-"):
+ return None
+ parts = name[len("opus-mt-") :].split("-")
+ if len(parts) == 2 and all(len(p) in (2, 3) for p in parts):
+ return parts[0], parts[1]
+ return None
+
+
+_CHAT_ID_MARKERS = ("instruct", "-chat", "_chat")
+
+
+def _id_says_chat(model_id: str) -> bool:
+ """Instruction-tuned checkpoints are used through their chat template; the
+ id is the only reliable signal (HF's `conversational` tag also covers base
+ models, and no architecture distinguishes tuned from base)."""
+ name = model_id.rsplit("/", 1)[-1].lower()
+ if name.endswith("-it") or "-it-" in name:
+ return True
+ return any(marker in name for marker in _CHAT_ID_MARKERS)
+
+
+def _first_covered_language(languages: tuple[str, ...], table: dict) -> Optional[str]:
+ for lang in languages:
+ if lang in table:
+ return lang
+ return None
+
+
+def profile_from_hf_signals(
+ model_id: str,
+ architecture_id: str,
+ signals: HFSignals,
+) -> Optional[ProfileSpec]:
+ """Distill fetched Hub metadata into a profile, or None when it says nothing."""
+ tags_lower = {t.lower() for t in signals.tags}
+ if tags_lower & _CODE_TAGS:
+ return ProfileSpec("continuation", "code")
+ kind = _PIPELINE_TAG_KINDS.get((signals.pipeline_tag or "").lower())
+ if kind is None:
+ return None
+ if kind == "task:translation":
+ pair = _marian_pair_from_model_id(model_id)
+ if pair is not None:
+ return ProfileSpec(kind, lang=pair[1], src=pair[0])
+ non_en = [lang for lang in signals.languages if lang != "en"]
+ if "en" in signals.languages and non_en:
+ return ProfileSpec(kind, lang=non_en[0], src="en")
+ # Direction unknowable from tags alone (tag lists are unordered);
+ # fall through rather than guess a reversed or identity pair.
+ return None
+ lang = _first_covered_language(signals.languages, CONTINUATION_PROMPTS) or "en"
+ if kind == "continuation":
+ return ProfileSpec(kind, lang)
+ return ProfileSpec(kind)
+
+
+def resolve_profile(
+ model_id: str,
+ architecture_id: Optional[str],
+ registry_profile: Optional[str] = None,
+ signals: Optional[HFSignals] = None,
+) -> ProfileSpec:
+ """Resolve a model's profile: override > architecture rule > live signals >
+ stored registry value > default (seq2seq falls back to denoising)."""
+ override = MODEL_PROFILE_OVERRIDES.get(model_id)
+ if override is not None:
+ return ProfileSpec.parse(override)
+
+ # Instruction-tuned ids get the chat profile (the runtime downgrades to
+ # continuation when no chat template actually exists). Checked before the
+ # signals layer: the `conversational` tag is deliberately not mapped.
+ if _id_says_chat(model_id) and ARCHITECTURE_PROFILE_KINDS.get(architecture_id or "") is None:
+ # The heuristic fixes only the KIND; a stored chat profile keeps its
+ # language or writeback would flatten curation to @en.
+ if registry_profile:
+ try:
+ stored = ProfileSpec.parse(registry_profile)
+ if stored.kind == "chat":
+ return stored
+ except ValueError:
+ pass
+ lang = "en"
+ if signals is not None:
+ lang = _first_covered_language(signals.languages, CHAT_PROMPTS) or "en"
+ return ProfileSpec("chat", lang)
+
+ arch_kind = ARCHITECTURE_PROFILE_KINDS.get(architecture_id or "")
+ if arch_kind is not None:
+ arch_spec = ProfileSpec.parse(arch_kind)
+ # The arch rule fixes only the KIND; a stored same-kind profile keeps
+ # its language so curation survives the writeback round-trip.
+ if registry_profile:
+ try:
+ stored = ProfileSpec.parse(registry_profile)
+ if stored.kind == arch_spec.kind:
+ if arch_spec.kind != "task:translation":
+ return stored
+ except ValueError:
+ pass
+ if arch_spec.kind == "task:translation":
+ pair = _marian_pair_from_model_id(model_id)
+ if pair is not None:
+ return ProfileSpec(arch_spec.kind, lang=pair[1], src=pair[0])
+ if signals is not None:
+ from_signals = profile_from_hf_signals(model_id, architecture_id or "", signals)
+ if from_signals is not None and from_signals.kind == "task:translation":
+ return from_signals
+ if registry_profile:
+ try:
+ stored = ProfileSpec.parse(registry_profile)
+ if stored.kind == "task:translation":
+ return stored
+ except ValueError:
+ pass
+ return ProfileSpec(arch_spec.kind, lang="de", src="en")
+ return arch_spec
+
+ if signals is not None:
+ from_signals = profile_from_hf_signals(model_id, architecture_id or "", signals)
+ if from_signals is not None:
+ return from_signals
+
+ if registry_profile:
+ try:
+ return ProfileSpec.parse(registry_profile)
+ except ValueError:
+ pass
+
+ try:
+ from transformer_lens.utilities.architectures import classify_architecture
+
+ if architecture_id and classify_architecture(architecture_id) == "seq2seq":
+ return ProfileSpec.parse(SEQ2SEQ_FALLBACK_KIND)
+ except ImportError: # pragma: no cover - torch-free scraper environments
+ pass
+ return DEFAULT_PROFILE
+
+
+def prompts_for(
+ spec: ProfileSpec, denoise_style: str = "t5"
+) -> Optional[tuple[ProfilePrompt, ...]]:
+ """Prompt set for a profile, or None when coverage is missing (caller skips
+ with a file-an-issue message naming the gap)."""
+ if spec.kind == "continuation":
+ return CONTINUATION_PROMPTS.get(spec.lang)
+ if spec.kind == "chat":
+ return CHAT_PROMPTS.get(spec.lang)
+ if spec.kind == "task:instruction":
+ return INSTRUCTION_PROMPTS.get(spec.lang)
+ if spec.kind == "task:summarization":
+ return SUMMARIZATION_PROMPTS.get(spec.lang)
+ if spec.kind == "task:denoise":
+ # Denoise prompts are English-only; a non-en denoise profile
+ # (IndicBART) is a coverage gap, not a zero.
+ if spec.lang not in ("en", ""):
+ return None
+ return DENOISE_PROMPTS.get(denoise_style)
+ if spec.kind == "task:translation":
+ src = spec.src or "en"
+ if src not in PIVOT_SENTENCES or spec.lang not in PIVOT_SENTENCES:
+ return None
+ return tuple(
+ ProfilePrompt(prompt=s, reference=t, lang=spec.lang)
+ for s, t in zip(PIVOT_SENTENCES[src], PIVOT_SENTENCES[spec.lang])
+ )
+ if spec.kind == "caption":
+ return tuple(ProfilePrompt(prompt="", reference=ref) for ref in CAPTION_REFERENCES)
+ return None
diff --git a/transformer_lens/benchmarks/utils.py b/transformer_lens/benchmarks/utils.py
index 7fa1069ca0..19295e0e0b 100644
--- a/transformer_lens/benchmarks/utils.py
+++ b/transformer_lens/benchmarks/utils.py
@@ -459,3 +459,13 @@ def format_results(results: List[BenchmarkResult]) -> str:
output.append("=" * 80)
return "\n".join(output)
+
+
+def bridge_self_target_loss(bridge, test_text: str):
+ """Loss with the tokenized input as explicit labels.
+
+ Seq2seq bridges refuse label-less return_type="loss" (encoder input_ids are
+ not decoder targets), so every benchmark loss call routes through here.
+ """
+ labels = bridge.to_tokens(test_text)
+ return bridge(test_text, labels=labels, return_type="loss")
diff --git a/transformer_lens/benchmarks/weight_processing.py b/transformer_lens/benchmarks/weight_processing.py
index b755e832dc..71ee968c55 100644
--- a/transformer_lens/benchmarks/weight_processing.py
+++ b/transformer_lens/benchmarks/weight_processing.py
@@ -8,6 +8,7 @@
from transformer_lens.benchmarks.utils import (
BenchmarkResult,
BenchmarkSeverity,
+ bridge_self_target_loss,
is_tiny_test_model,
safe_allclose,
)
@@ -147,7 +148,7 @@ def benchmark_weight_sharing(
"""
try:
# Get baseline loss
- bridge_original = bridge(test_text, return_type="loss")
+ bridge_original = bridge_self_target_loss(bridge, test_text)
if reference_model is not None:
reference_original = reference_model(test_text, return_type="loss")
@@ -212,7 +213,7 @@ def benchmark_weight_sharing(
reference_model.blocks[bridge_attn_idx].attn.W_V[0, :, :] = 0
# Test modified losses
- bridge_modified = bridge(test_text, return_type="loss")
+ bridge_modified = bridge_self_target_loss(bridge, test_text)
reference_modified = reference_model(test_text, return_type="loss")
bridge_change = bridge_modified - bridge_original
@@ -254,7 +255,7 @@ def benchmark_weight_sharing(
with torch.no_grad():
ws_attn_block.attn.W_V[0, :, :] = 0
- bridge_modified = bridge(test_text, return_type="loss")
+ bridge_modified = bridge_self_target_loss(bridge, test_text)
change = abs(bridge_modified - bridge_original)
# Restore weights
@@ -302,7 +303,7 @@ def benchmark_weight_modification(
"""
try:
# Get original loss
- original_loss = bridge(test_text, return_type="loss")
+ original_loss = bridge_self_target_loss(bridge, test_text)
# Find first block with attention (hybrid models may not have attn on block 0)
wm_attn_blocks = bridge.blocks_with("attn")
@@ -334,7 +335,7 @@ def benchmark_weight_modification(
# Get modified loss (with error handling to restore weights)
try:
- modified_loss = bridge(test_text, return_type="loss")
+ modified_loss = bridge_self_target_loss(bridge, test_text)
except Exception as forward_error:
# Restore weights before reporting error
with torch.no_grad():
@@ -369,7 +370,7 @@ def benchmark_weight_modification(
with torch.no_grad():
original_mlp_w = mlp_block.mlp.out.weight.clone()
mlp_block.mlp.out.weight[0, :] = 0
- mlp_modified_loss = bridge(test_text, return_type="loss")
+ mlp_modified_loss = bridge_self_target_loss(bridge, test_text)
with torch.no_grad():
mlp_block.mlp.out.weight.copy_(original_mlp_w)
mlp_change = abs(mlp_modified_loss - original_loss)
diff --git a/transformer_lens/components/transformer_block.py b/transformer_lens/components/transformer_block.py
index 7017d0acba..e1ebda8428 100644
--- a/transformer_lens/components/transformer_block.py
+++ b/transformer_lens/components/transformer_block.py
@@ -25,6 +25,7 @@
from transformer_lens.factories.mlp_factory import MLPFactory
from transformer_lens.hook_points import HookPoint
from transformer_lens.utilities import repeat_along_head_dimension
+from transformer_lens.utilities.architectures import POST_NORM_ARCHITECTURES
class TransformerBlock(nn.Module):
@@ -154,7 +155,7 @@ def forward(
key_input = attn_in
value_input = attn_in
- if self.cfg.original_architecture in ("Olmo2ForCausalLM", "Olmo3ForCausalLM"):
+ if self.cfg.original_architecture in POST_NORM_ARCHITECTURES:
attn_out = self.attn(
query_input=query_input,
key_input=key_input,
@@ -182,7 +183,7 @@ def forward(
# and before the hook. We do it before the hook so hook_attn_out captures "that which
# is added to the residual stream"
attn_out = self.ln1_post(attn_out)
- if self.cfg.original_architecture in ("Olmo2ForCausalLM", "Olmo3ForCausalLM"):
+ if self.cfg.original_architecture in POST_NORM_ARCHITECTURES:
# OLMo 2/3 post-norm: ln1 applies before the residual add, so it must
# precede the hook for hook_attn_out to capture the additive contribution.
attn_out = self.ln1(attn_out)
@@ -196,7 +197,7 @@ def forward(
mlp_in = (
resid_mid if not self.cfg.use_hook_mlp_in else self.hook_mlp_in(resid_mid.clone())
)
- if self.cfg.original_architecture in ("Olmo2ForCausalLM", "Olmo3ForCausalLM"):
+ if self.cfg.original_architecture in POST_NORM_ARCHITECTURES:
# Post-norm: apply_mlp applies ln2 before hook_mlp_out internally.
mlp_out = self.apply_mlp(mlp_in)
else:
@@ -228,7 +229,7 @@ def apply_mlp(
mlp_out = self.mlp(normalized_resid) # [batch, pos, d_model]
if self.cfg.use_normalization_before_and_after:
mlp_out = self.ln2_post(mlp_out)
- if self.cfg.original_architecture in ("Olmo2ForCausalLM", "Olmo3ForCausalLM"):
+ if self.cfg.original_architecture in POST_NORM_ARCHITECTURES:
# OLMo 2/3 post-norm: ln2 applies before the residual add, so it must
# precede the hook for hook_mlp_out to capture the additive contribution.
mlp_out = self.ln2(mlp_out)
diff --git a/transformer_lens/config/transformer_bridge_config.py b/transformer_lens/config/transformer_bridge_config.py
index e09d55e75a..28a3d4c2f3 100644
--- a/transformer_lens/config/transformer_bridge_config.py
+++ b/transformer_lens/config/transformer_bridge_config.py
@@ -1,7 +1,10 @@
"""Configuration class for TransformerBridge."""
-from typing import Optional
+import warnings
+import weakref
+from typing import Any, Optional
+import numpy as np
import torch
from transformer_lens.utilities.activation_functions import SOFTCAP_DISABLED
@@ -18,6 +21,17 @@ class TransformerBridgeConfig(TransformerLensConfig):
Also includes all HookedTransformerConfig fields for compatibility.
"""
+ __slots__ = ("_bridge_ref",)
+
+ _BRIDGE_MANAGED_HOOK_FLAGS = frozenset(
+ {
+ "use_attn_result",
+ "use_attn_in",
+ "use_hook_mlp_in",
+ "use_split_qkv_input",
+ }
+ )
+
def __init__(
self,
d_model: int,
@@ -109,6 +123,7 @@ def __init__(
**kwargs,
):
"""Initialize TransformerBridgeConfig."""
+ object.__setattr__(self, "_bridge_ref", None)
super().__init__(
d_model=d_model,
d_head=d_head,
@@ -204,9 +219,48 @@ def __init__(
self.vision_num_layers = vision_num_layers
self.vision_num_heads = vision_num_heads
self.mm_tokens_per_image = mm_tokens_per_image
-
self.__post_init__()
+ def __setattr__(self, name: str, value: Any) -> None:
+ """Route live Bridge hook-flag assignments through their public setters."""
+ if name in self._BRIDGE_MANAGED_HOOK_FLAGS:
+ bridge_ref = getattr(self, "_bridge_ref", None)
+ bridge = bridge_ref() if bridge_ref is not None else None
+ if bridge is not None:
+ getattr(bridge, f"set_{name}")(value)
+ return
+ super().__setattr__(name, value)
+
+ def __getstate__(self) -> dict[str, Any]:
+ """Serialize config data without retaining its live Bridge binding."""
+ return self.__dict__.copy()
+
+ def __setstate__(self, state: dict[str, Any]) -> None:
+ """Restore an unbound config copy."""
+ self.__dict__.update(state)
+ object.__setattr__(self, "_bridge_ref", None)
+
+ def _bind_bridge(self, bridge: Any) -> None:
+ """Bind runtime hook-flag assignments to a constructed Bridge."""
+ bridge_ref = getattr(self, "_bridge_ref", None)
+ bound_bridge = bridge_ref() if bridge_ref is not None else None
+ if bound_bridge is None:
+ object.__setattr__(self, "_bridge_ref", weakref.ref(bridge))
+ elif bound_bridge is not bridge:
+ warnings.warn(
+ "TransformerBridgeConfig is already bound to another live "
+ "TransformerBridge; declining to bind it to this instance. "
+ "Direct assignments to Bridge-managed hook flags will continue "
+ "to configure the existing TransformerBridge.",
+ stacklevel=3,
+ )
+
+ def _set_bridge_managed_hook_flag(self, name: str, value: bool) -> None:
+ """Set a managed flag without re-entering the Bridge setter."""
+ if name not in self._BRIDGE_MANAGED_HOOK_FLAGS:
+ raise ValueError(f"Unknown Bridge-managed hook flag: {name}")
+ object.__setattr__(self, name, value)
+
def __post_init__(self):
"""Post-initialization processing."""
# dtype is guaranteed to be set at this point
@@ -219,6 +273,18 @@ def __post_init__(self):
):
raise ValueError(f"architecture must be a string, got {type(self.architecture)}")
+ # Resolve the initializer_range sentinel (-1.0 means "not set by the user").
+ # Mirrors HookedTransformerConfig.__post_init__ (hooked_transformer_config.py).
+ # Guarded with getattr: this method also runs once from the dataclass
+ # parent's __init__, before self.initializer_range is assigned below.
+ if getattr(self, "initializer_range", None) is not None:
+ if self.initializer_range < 0 and self.init_mode == "gpt2":
+ # Roughly copy the GPT-2 value, but proportional to sqrt(1/d_model)
+ self.initializer_range = 0.8 / np.sqrt(self.d_model)
+ if self.initializer_range < 0 and self.init_mode != "gpt2":
+ # This is the gain parameter for the weight initialisation
+ self.initializer_range = 1.0
+
# Call parent's __post_init__ after our validation
if hasattr(super(), "__post_init__"):
super().__post_init__()
diff --git a/transformer_lens/loading_from_pretrained.py b/transformer_lens/loading_from_pretrained.py
index 7e5bc58f1d..3b8cac7619 100644
--- a/transformer_lens/loading_from_pretrained.py
+++ b/transformer_lens/loading_from_pretrained.py
@@ -58,6 +58,7 @@
convert_t5_weights,
)
from transformer_lens.supported_models import MODEL_ALIASES, OFFICIAL_MODEL_NAMES
+from transformer_lens.utilities.architectures import POST_NORM_ARCHITECTURES
from transformer_lens.utilities.heterogeneous_config import het_safe_view
from transformer_lens.utilities.hf_utils import get_rotary_pct_from_config
from transformer_lens.utilities.quantization import (
@@ -1785,11 +1786,12 @@ def get_pretrained_model_config(
)
fold_ln = False
- # OLMo 2 uses post-norm (norm after attention/MLP, not before), so folding
- # the norm weights into adjacent linear layers is not mathematically valid.
- if cfg_dict.get("original_architecture") == "Olmo2ForCausalLM" and fold_ln:
+ # Post-norm blocks normalize the sublayer output, so folding the norm weights
+ # into adjacent linear layers is not mathematically valid.
+ architecture = cfg_dict.get("original_architecture")
+ if architecture in POST_NORM_ARCHITECTURES and fold_ln:
logging.warning(
- "fold_ln=True is incompatible with OLMo 2's post-norm architecture. "
+ f"fold_ln=True is incompatible with {architecture}'s post-norm architecture. "
"Setting fold_ln=False."
)
fold_ln = False
diff --git a/transformer_lens/model_bridge/bridge.py b/transformer_lens/model_bridge/bridge.py
index 94896edc45..670e7ef139 100644
--- a/transformer_lens/model_bridge/bridge.py
+++ b/transformer_lens/model_bridge/bridge.py
@@ -17,6 +17,7 @@
Callable,
Dict,
FrozenSet,
+ Iterable,
Iterator,
List,
Literal,
@@ -32,6 +33,8 @@
import torch
import tqdm
from torch import nn
+from torch.nn import functional as F
+from transformers.tokenization_utils_base import PreTrainedTokenizerBase
from transformer_lens import utilities as utils
from transformer_lens.ActivationCache import ActivationCache
@@ -39,7 +42,10 @@
from transformer_lens.FactoredMatrix import FactoredMatrix
from transformer_lens.hook_points import HookIntrospectionMixin, HookPoint
from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter
-from transformer_lens.model_bridge.component_setup import set_original_components
+from transformer_lens.model_bridge.component_setup import (
+ refresh_container_state_owners,
+ set_original_components,
+)
from transformer_lens.model_bridge.composition_scores import CompositionScores
from transformer_lens.model_bridge.exceptions import StopAtLayerException
from transformer_lens.model_bridge.generalized_components.base import (
@@ -192,18 +198,13 @@ def __init__(self, model: nn.Module, adapter: ArchitectureAdapter, tokenizer: An
tokenizer: The tokenizer to use (required)
"""
super().__init__()
+ self._n_params_total = sum(parameter.numel() for parameter in model.parameters())
self.__dict__["original_model"] = model
self.adapter = adapter
self.cfg = adapter.cfg
- self.tokenizer = tokenizer
- if self.cfg.d_vocab == -1 and self.tokenizer is not None:
- if hasattr(self.tokenizer, "get_vocab"):
- vocab = self.tokenizer.get_vocab()
- self.cfg.d_vocab = max(vocab.values()) + 1
- elif hasattr(self.tokenizer, "vocab"):
- self.cfg.d_vocab = max(self.tokenizer.vocab.values()) + 1
- else:
- self.cfg.d_vocab = getattr(self.tokenizer, "vocab_size", 50257)
+ self._tokenizer = None
+ if tokenizer is not None:
+ self.tokenizer = tokenizer # Use the property setter
if self.cfg.d_vocab_out == -1:
self.cfg.d_vocab_out = self.cfg.d_vocab
self.compatibility_mode = False
@@ -241,6 +242,63 @@ def __init__(self, model: nn.Module, adapter: ArchitectureAdapter, tokenizer: An
# train() recurses, so this stamps the wrappers with the model's mode.
original_model.train(original_model.training)
self.train(original_model.training)
+ self.cfg._bind_bridge(self)
+
+ def __setstate__(self, state: dict[str, Any]) -> None:
+ """Restore runtime config routing after deepcopy or deserialization."""
+ super().__setstate__(state)
+ self.cfg._bind_bridge(self)
+
+ @property
+ def tokenizer(self) -> Any:
+ """The tokenizer used for encoding/decoding text."""
+ return self._tokenizer
+
+ @tokenizer.setter
+ def tokenizer(self, value: Any) -> None:
+ """Set tokenizer and re-run wiring (d_vocab, BOS/EOS detection, padding).
+
+ On initial assignment (during __init__), the boot path has already run
+ setup_tokenizer, so we skip calling it again. However, we still infer
+ d_vocab if it wasn't set from the model config (d_vocab == -1).
+
+ On reassignment, we re-run the tokenizer wiring and update d_vocab to
+ keep cfg in sync with the new tokenizer.
+ """
+ is_reassignment = getattr(self, "_tokenizer", None) is not None
+ cfg = getattr(self, "cfg", None)
+ if value is not None and cfg is not None:
+ if is_reassignment:
+ from transformer_lens.model_bridge.sources._bridge_builder import (
+ detect_tokenizer_bos_eos,
+ )
+ from transformer_lens.model_bridge.sources.transformers import (
+ setup_tokenizer,
+ )
+
+ value = setup_tokenizer(
+ value, default_padding_side=getattr(cfg, "default_padding_side", None)
+ )
+ cfg.tokenizer_prepends_bos, cfg.tokenizer_appends_eos = detect_tokenizer_bos_eos(
+ value
+ )
+
+ # Infer d_vocab: on initial assignment only if not set (-1),
+ # on reassignment always update to match new tokenizer.
+ # Use getattr for cfg attributes since tests may use SimpleNamespace.
+ d_vocab = getattr(cfg, "d_vocab", None)
+ if d_vocab == -1 or is_reassignment:
+ if hasattr(value, "get_vocab"):
+ vocab = value.get_vocab()
+ cfg.d_vocab = max(vocab.values()) + 1
+ elif hasattr(value, "vocab"):
+ cfg.d_vocab = max(value.vocab.values()) + 1
+ else:
+ cfg.d_vocab = getattr(value, "vocab_size", 50257)
+ d_vocab_out = getattr(cfg, "d_vocab_out", None)
+ if d_vocab_out == -1 or is_reassignment:
+ cfg.d_vocab_out = getattr(cfg, "d_vocab", d_vocab_out)
+ self._tokenizer = value
@classmethod
def boot_transformers(
@@ -336,10 +394,34 @@ def boot_transformers(
checkpoint_value=checkpoint_value,
)
+ @overload
+ @classmethod
+ def boot_native(
+ cls,
+ config: TransformerBridgeConfig,
+ tokenizer: Optional[Any] = None,
+ device: Optional[Union[str, torch.device]] = None,
+ dtype: Optional[torch.dtype] = None,
+ model_name: str = "native",
+ ) -> "TransformerBridge":
+ ...
+
+ @overload
+ @classmethod
+ def boot_native(
+ cls,
+ config: Dict[str, Any],
+ tokenizer: Optional[Any] = None,
+ device: Optional[Union[str, torch.device]] = None,
+ dtype: Optional[torch.dtype] = None,
+ model_name: str = "native",
+ ) -> "TransformerBridge":
+ ...
+
@classmethod
def boot_native(
cls,
- config: Union[TransformerBridgeConfig, dict],
+ config: Any,
tokenizer: Optional[Any] = None,
device: Optional[Union[str, torch.device]] = None,
dtype: Optional[torch.dtype] = None,
@@ -350,6 +432,15 @@ def boot_native(
No HuggingFace Hub call, no ``transformers`` import. ``config.init_mode``
and ``config.seed`` control reproducibility.
"""
+ # Impl signature stays Any so this guard is reachable — a Union hint
+ # would have beartype reject foreign configs with its own error first.
+ if not isinstance(config, (TransformerBridgeConfig, dict)):
+ raise TypeError(
+ "boot_native expected a TransformerBridgeConfig or dict, "
+ f"got {type(config).__name__}. Construct a TransformerBridgeConfig "
+ "with the same fields."
+ )
+
import copy as _copy
from transformer_lens.config import TransformerBridgeConfig as _Cfg
@@ -383,14 +474,16 @@ def boot_native(
# Fork RNG around construction + init when seeded so neither nn.Linear's
# default reset_parameters nor our scoped init perturb the caller's RNG.
- # Unseeded calls let global RNG advance normally.
- if cfg.seed is not None:
+ # When custom init is disabled, construction keeps PyTorch's normal global
+ # RNG semantics and cfg.seed has no initialization work to control.
+ if cfg.init_weights and cfg.seed is not None:
with torch.random.fork_rng(devices=[]):
model = NativeModel(cfg)
initialize_native_model(model, cfg)
else:
model = NativeModel(cfg)
- initialize_native_model(model, cfg)
+ if cfg.init_weights:
+ initialize_native_model(model, cfg)
if device is not None:
model = model.to(device)
@@ -407,6 +500,22 @@ def boot_native(
model_name=model_name,
)
+ def init_weights(self) -> None:
+ """Reinitialize a TL-native model in place using the bridge config."""
+ from transformer_lens.model_bridge.sources.native.init import (
+ initialize_native_model,
+ )
+ from transformer_lens.model_bridge.sources.native.model import NativeModel
+
+ model = self.original_model
+ if not isinstance(model, NativeModel):
+ raise RuntimeError(
+ "TransformerBridge.init_weights() is only supported for TL-native "
+ "bridges created with TransformerBridge.boot_native(...); this bridge "
+ f"wraps {type(model).__name__}."
+ )
+ initialize_native_model(model, self.cfg)
+
@property
def original_model(self) -> nn.Module:
"""Return the wrapped underlying model; raises AttributeError if it was never set."""
@@ -792,18 +901,17 @@ def hook_dict(self) -> dict[str, HookPoint]:
@property
def n_params_total(self) -> int:
- """Total number of parameters in the model, including embeddings, biases,
- and layer norm weights.
+ """Number of parameters in the wrapped model before bridge instrumentation.
- Mirrors :attr:`HookedTransformer.n_params_total`. Use this when you want
- the actual parameter count for memory budgeting, comparison with
- HuggingFace's ``model.num_parameters()``, or alignment with reported
- model sizes in papers (e.g. the Pythia suite).
+ This follows PyTorch's parameter iteration semantics, counting tied
+ parameters once. Bridge-created split views and synthetic zero tensors
+ are excluded, so the result can differ from
+ :attr:`HookedTransformer.n_params_total` and :meth:`tl_parameters`.
Returns:
- int: ``sum(p.numel() for p in self.parameters())``
+ int: Parameter count of the uninstrumented wrapped model.
"""
- return sum(p.numel() for p in self.parameters())
+ return self._n_params_total
def clear_hook_registry(self) -> None:
"""Clear the hook registry and force re-initialization."""
@@ -882,6 +990,10 @@ def __getattr__(self, name: str) -> Any:
# Use __dict__ directly to avoid recursion
if "_modules" in self.__dict__ and name in self.__dict__["_modules"]: # type: ignore[arg-type]
return self.__dict__["_modules"][name]
+ adapter = self.__dict__.get("adapter")
+ component_mapping = getattr(adapter, "component_mapping", None)
+ if component_mapping is not None and name in component_mapping:
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
if "original_model" in self.__dict__ and self.__dict__["original_model"] is not None:
try:
name_split = name.split(".")
@@ -1220,13 +1332,23 @@ def to_tokens(
padding_side = getattr(self.tokenizer, "padding_side", "right")
tokenizer_prepends_bos = getattr(self.cfg, "tokenizer_prepends_bos", True)
if prepend_bos and (not tokenizer_prepends_bos):
- input = utils.get_input_with_manually_prepended_bos(self.tokenizer.bos_token, input)
+ bos = self.tokenizer.bos_token
+ encodes_atomically = (
+ bos is not None
+ and len(self.tokenizer(bos, add_special_tokens=False)["input_ids"]) == 1
+ )
+ if encodes_atomically:
+ input = utils.get_input_with_manually_prepended_bos(bos, input)
+ # else: the fallback BOS is not an atom in this vocab (e.g.
+ # '<|endoftext|>' installed on BERT); prepending the string would
+ # tokenize to subword garbage, so skip rather than pollute the input.
if isinstance(input, str):
input = [input]
tokens = self.tokenizer(
input,
return_tensors="pt",
padding=True,
+ padding_side=padding_side,
truncation=truncate,
max_length=self.cfg.n_ctx if truncate else None,
)["input_ids"]
@@ -1239,7 +1361,9 @@ def to_tokens(
while tokens.shape[-1] > 1 and (tokens[:, -1] == self.tokenizer.eos_token_id).all():
tokens = tokens[:, :-1]
if not prepend_bos and tokenizer_prepends_bos:
- tokens = utils.get_tokens_with_bos_removed(self.tokenizer, tokens)
+ tokens = utils.get_tokens_with_bos_removed(
+ self.tokenizer, tokens, padding_side=padding_side
+ )
if move_to_device:
tokens = tokens.to(self.cfg.device)
return tokens
@@ -1504,6 +1628,26 @@ def _reshape_o(self, w: torch.Tensor) -> torch.Tensor:
return w.reshape(self.cfg.n_heads, d_head, self.cfg.d_model)
return w
+ def _expand_kv_heads(self, w: torch.Tensor) -> torch.Tensor:
+ """Expand stacked grouped K/V weights along the head axis to n_heads.
+
+ GQA models store one K/V projection per key-value head while W_Q/W_O are
+ per-query-head, so weight circuits must repeat the grouped K/V up to
+ n_heads before factoring: query head h reads kv head
+ h // (n_heads // n_kv_heads), i.e. repeat_interleave — the same layout
+ GroupedQueryAttention.W_K/W_V expose on HookedTransformer. No-op for MHA,
+ where the head axes already match.
+ """
+ if w.ndim != 4 or w.shape[1] == self.cfg.n_heads:
+ return w
+ n_kv_heads = w.shape[1]
+ if self.cfg.n_heads % n_kv_heads != 0:
+ raise ValueError(
+ f"Cannot expand {n_kv_heads} key-value heads to {self.cfg.n_heads} "
+ f"query heads: n_heads must be a multiple of n_kv_heads."
+ )
+ return w.repeat_interleave(self.cfg.n_heads // n_kv_heads, dim=1)
+
@property
def W_K(self) -> torch.Tensor:
"""Stack the key weights across all layers."""
@@ -1589,24 +1733,24 @@ def W_E(self) -> torch.Tensor:
@property
def QK(self):
"""QK circuit. On hybrids, returns attn layers only (with warning). See QK_for_attn_layers()."""
- return FactoredMatrix(self.W_Q, self.W_K.transpose(-2, -1))
+ return FactoredMatrix(self.W_Q, self._expand_kv_heads(self.W_K).transpose(-2, -1))
@property
def OV(self):
"""OV circuit. On hybrids, returns attn layers only (with warning). See OV_for_attn_layers()."""
- return FactoredMatrix(self.W_V, self.W_O)
+ return FactoredMatrix(self._expand_kv_heads(self.W_V), self.W_O)
def QK_for_attn_layers(self) -> Tuple[List[int], FactoredMatrix]:
"""QK circuit for attention layers only. Returns (layer_indices, FactoredMatrix)."""
q_indices, W_Q = self.stack_params_for("attn", "attn.W_Q", self._reshape_qkv)
_, W_K = self.stack_params_for("attn", "attn.W_K", self._reshape_qkv)
- return q_indices, FactoredMatrix(W_Q, W_K.transpose(-2, -1))
+ return q_indices, FactoredMatrix(W_Q, self._expand_kv_heads(W_K).transpose(-2, -1))
def OV_for_attn_layers(self) -> Tuple[List[int], FactoredMatrix]:
"""OV circuit for attention layers only. Returns (layer_indices, FactoredMatrix)."""
v_indices, W_V = self.stack_params_for("attn", "attn.W_V", self._reshape_qkv)
_, W_O = self.stack_params_for("attn", "attn.W_O", self._reshape_o)
- return v_indices, FactoredMatrix(W_V, W_O)
+ return v_indices, FactoredMatrix(self._expand_kv_heads(W_V), W_O)
# ------------------------------------------------------------------
# Mechanistic interpretability analysis methods
@@ -1733,17 +1877,17 @@ def _stack(attr_path: str, reshape_fn: Optional[Callable] = None) -> torch.Tenso
weights = [w.to(target_device) for w in weights]
return torch.stack(weights, dim=0)
- W_V = _stack("attn.W_V", self._reshape_qkv)
+ W_V = self._expand_kv_heads(_stack("attn.W_V", self._reshape_qkv))
W_O = _stack("attn.W_O", self._reshape_o)
left = FactoredMatrix(W_V, W_O)
if mode == "Q":
W_Q = _stack("attn.W_Q", self._reshape_qkv)
- W_K = _stack("attn.W_K", self._reshape_qkv)
+ W_K = self._expand_kv_heads(_stack("attn.W_K", self._reshape_qkv))
right = FactoredMatrix(W_Q, W_K.transpose(-2, -1))
elif mode == "K":
W_Q = _stack("attn.W_Q", self._reshape_qkv)
- W_K = _stack("attn.W_K", self._reshape_qkv)
+ W_K = self._expand_kv_heads(_stack("attn.W_K", self._reshape_qkv))
right = FactoredMatrix(W_Q, W_K.transpose(-2, -1)).T
elif mode == "V":
right = left
@@ -1869,6 +2013,79 @@ def tl_named_parameters(self) -> Iterator[tuple[str, torch.Tensor]]:
"""
return iter(self.get_params().items())
+ def _accepts_derived_position_ids(self) -> bool:
+ """Whether it is safe to hand the wrapped model a mask-derived ``position_ids``.
+
+ Two families of model must be left alone, so the injection below is
+ gated on the target the same way ``output_attentions`` is in
+ :meth:`run_with_cache`:
+
+ * **Fixed-signature models.** Remote-code forwards such as
+ ``LLaDAModelLM.forward`` take neither ``position_ids`` nor
+ ``**kwargs``, so passing it raises ``TypeError`` where the model
+ previously returned logits.
+ * **Models that own their position derivation.** mRoPE architectures
+ (Qwen2-VL, Qwen2.5-VL, Qwen3-VL, GLM-4V) build a 3-D temporal /
+ height / width index in ``get_rope_index``, and only while
+ ``position_ids is None``; a supplied 2-D tensor is silently expanded
+ across all three streams instead. Their derivation already scatters
+ positions onto attended slots only, so it handles left padding
+ correctly on its own and needs no help from us.
+ * **Mask-consuming positional embeddings.** OPT's
+ ``OPTLearnedPositionalEmbedding.forward`` takes the mask and derives
+ the same positions we would, so injection buys nothing — but it does
+ replace the model's own padding-slot convention with ours, which
+ shows up as a whole-tensor diff.
+ """
+ underlying = getattr(self, "original_model", None)
+ if underlying is None:
+ return False
+
+ cached = self.__dict__.get("_derived_position_ids_ok")
+ if cached is not None and cached[0] is underlying:
+ return bool(cached[1])
+
+ def verdict() -> bool:
+ fwd_params = inspect.signature(underlying.forward).parameters
+ if "position_ids" not in fwd_params and not any(
+ p.kind is inspect.Parameter.VAR_KEYWORD for p in fwd_params.values()
+ ):
+ return False
+
+ # ``get_rope_index`` lives on the inner text model, not the
+ # ForConditionalGeneration wrapper that is usually original_model.
+ for module in (
+ underlying,
+ getattr(underlying, "model", None),
+ getattr(underlying, "language_model", None),
+ ):
+ if module is not None and hasattr(module, "get_rope_index"):
+ return False
+
+ # Config-level backstop for mRoPE models that spell the derivation
+ # differently; the section list is what makes positions 3-D.
+ config = getattr(underlying, "config", None)
+ for candidate in (config, getattr(config, "text_config", None)):
+ scaling = getattr(candidate, "rope_scaling", None)
+ if isinstance(scaling, dict) and "mrope_section" in scaling:
+ return False
+
+ # A positional embedding that takes the mask derives positions for
+ # itself. Only embeddings that override nn.Embedding.forward are
+ # worth inspecting, which keeps this to a handful per model.
+ for module in underlying.modules():
+ if not isinstance(module, nn.Embedding):
+ continue
+ if type(module).forward is nn.Embedding.forward:
+ continue
+ if "attention_mask" in inspect.signature(module.forward).parameters:
+ return False
+ return True
+
+ accepts = verdict()
+ self.__dict__["_derived_position_ids_ok"] = (underlying, accepts)
+ return accepts
+
def forward(
self,
input: Union[str, List[str], torch.Tensor],
@@ -1877,6 +2094,7 @@ def forward(
prepend_bos: Optional[bool] = None,
padding_side: Optional[str] = None,
attention_mask: Optional[torch.Tensor] = None,
+ labels: Optional[torch.Tensor] = None,
start_at_layer: Optional[int] = None,
stop_at_layer: Optional[int] = None,
pixel_values: Optional[torch.Tensor] = None,
@@ -1891,6 +2109,8 @@ def forward(
loss_per_token: Whether to return loss per token
prepend_bos: Whether to prepend BOS token
padding_side: Which side to pad on
+ labels: Explicit language-model targets. Encoder-decoder models require
+ labels for loss; decoder-only models fall back to input IDs when omitted.
start_at_layer: Not implemented in TransformerBridge. The bridge delegates
to HuggingFace's model.forward() which owns the layer iteration loop,
making start_at_layer infeasible without monkey-patching HF internals
@@ -1910,7 +2130,18 @@ def forward(
Model output based on return_type
"""
- if return_type in ("loss", "both") and not self.adapter.supports_causal_loss:
+ model_config = getattr(self.original_model, "config", None)
+ is_encoder_decoder = bool(getattr(model_config, "is_encoder_decoder", False))
+ if return_type in ("loss", "both") and is_encoder_decoder and labels is None:
+ raise ValueError(
+ "labels are required for seq2seq return_type='loss' or 'both'; "
+ "encoder input_ids are not decoder targets"
+ )
+ if (
+ return_type in ("loss", "both")
+ and not is_encoder_decoder
+ and not self.adapter.supports_causal_loss
+ ):
architecture = self.cfg.architecture or type(self.adapter).__name__
raise NotImplementedError(
f"{architecture} does not support TransformerBridge's shifted causal "
@@ -1918,6 +2149,9 @@ def forward(
"masked-token objective explicitly."
)
+ if labels is not None:
+ kwargs["labels"] = labels
+
if start_at_layer is not None:
raise NotImplementedError(
"start_at_layer is not supported in TransformerBridge. "
@@ -1940,8 +2174,11 @@ def forward(
"The bridge only supports stop_at_layer on 'blocks'."
)
if hasattr(self, "blocks"):
+ effective_stop_at_layer = (
+ len(self.blocks) + stop_at_layer if stop_at_layer < 0 else stop_at_layer
+ )
for block in self.blocks:
- block._stop_at_layer_idx = stop_at_layer
+ block._stop_at_layer_idx = effective_stop_at_layer
# Map HookedEncoderDecoder-style kwargs to HF-compatible names
if "decoder_input" in kwargs:
@@ -1952,16 +2189,17 @@ def forward(
else:
kwargs.pop("one_zero_attention_mask")
- # Detect batched list input that will need padding. For this case we force
- # left-padding internally and auto-compute attention_mask + position_ids
- # (unless the caller passed them explicitly) so pad tokens don't contaminate
- # attention or position embeddings.
+ # Detect batched list input that may need padding. Forward follows the
+ # requested/tokenizer side; generation separately forces left-padding.
_is_batched_list = (
isinstance(input, list)
and len(input) > 1
and not getattr(self.cfg, "is_audio_model", False)
and not getattr(self.cfg, "is_visual_model", False)
)
+ _resolved_padding_side = padding_side
+ if _resolved_padding_side is None and self.tokenizer is not None:
+ _resolved_padding_side = getattr(self.tokenizer, "padding_side", "right")
try:
if isinstance(input, (str, list)):
@@ -1975,20 +2213,9 @@ def forward(
"Visual models require tensor input (pixel values), not text. "
"Pass a torch.Tensor or use the pixel_values parameter."
)
- if _is_batched_list and padding_side is None:
- # Force left-padding so real tokens are flush-right.
- _orig_padding_side = self.tokenizer.padding_side
- self.tokenizer.padding_side = "left"
- try:
- input_ids = self.to_tokens(
- input, prepend_bos=prepend_bos, padding_side=padding_side
- )
- finally:
- self.tokenizer.padding_side = _orig_padding_side
- else:
- input_ids = self.to_tokens(
- input, prepend_bos=prepend_bos, padding_side=padding_side
- )
+ input_ids = self.to_tokens(
+ input, prepend_bos=prepend_bos, padding_side=padding_side
+ )
else:
input_ids = input
# Promote 1D integer token tensors to 2D [batch=1, seq] to match
@@ -2007,40 +2234,78 @@ def forward(
isinstance(input_ids, torch.Tensor) and input_ids.is_floating_point()
)
- # Auto-compute attention_mask + position_ids for batched list input
- # when the caller didn't supply them. Matches HF generation convention.
+ # Left padding needs a mask and corrected positions. Right padding is
+ # harmless for causal real-token positions and remains unmasked to
+ # match HookedTransformer; bidirectional/encoder inputs still need it.
if (
_is_batched_list
and attention_mask is None
and self.tokenizer is not None
and self.tokenizer.pad_token_id is not None
and not _is_inputs_embeds
+ and (
+ _resolved_padding_side == "left"
+ or is_encoder_decoder
+ or not self.adapter.supports_causal_loss
+ )
):
- _prev_side = self.tokenizer.padding_side
- self.tokenizer.padding_side = "left"
- try:
- attention_mask = utils.get_attention_mask(
- self.tokenizer,
- input_ids,
- prepend_bos=getattr(self.cfg, "default_prepend_bos", True),
- ).to(self.cfg.device)
- finally:
- self.tokenizer.padding_side = _prev_side
- if "position_ids" not in kwargs:
+ attention_mask = utils.get_attention_mask(
+ self.tokenizer,
+ input_ids,
+ prepend_bos=getattr(self.cfg, "default_prepend_bos", True),
+ padding_side=_resolved_padding_side,
+ ).to(self.cfg.device)
+ # Gated on the target for the same reason the derivation below is:
+ # a fixed-signature forward raises TypeError on the kwarg, and a
+ # model that owns its own position derivation is overridden by it
+ # (#1626).
+ if "position_ids" not in kwargs and self._accepts_derived_position_ids():
position_ids = attention_mask.long().cumsum(-1) - 1
position_ids.masked_fill_(attention_mask == 0, 1)
kwargs["position_ids"] = position_ids
+ # Any masked-out token shifts the absolute position of every real token
+ # after it, so positions must be derived from the mask rather than left
+ # to HF's default arange. This is the same derivation HookedTransformer
+ # applies in pos_embed; without it the bridge silently returns wrong
+ # logits. An all-ones mask reduces to arange, so this is a no-op there.
+ #
+ # The mask spans any cached prefix as well as the new tokens, so it is
+ # offset back to just the tokens actually being passed — matching how
+ # AbstractAttention/PosEmbed use past_kv_pos_offset.
+ if (
+ attention_mask is not None
+ and "position_ids" not in kwargs
+ and not _is_inputs_embeds
+ and attention_mask.ndim == 2
+ and isinstance(input_ids, torch.Tensor)
+ and input_ids.ndim == 2
+ and attention_mask.shape[1] >= input_ids.shape[1]
+ and self._accepts_derived_position_ids()
+ ):
+ # .long() because callers may hand in a float 0/1 mask, and
+ # positions index an embedding table.
+ _derived = utils.get_offset_position_ids(0, attention_mask.long())
+ _arange = torch.arange(attention_mask.shape[1], device=_derived.device)
+ # Decide per row, not per batch. A row only needs the derived
+ # positions when its mask actually moves one of its attended
+ # tokens off the default position — i.e. a masked token precedes
+ # a real one (left padding, or an interior gap). Rows that are
+ # unpadded or purely right-padded keep arange verbatim, so one
+ # left-padded row in a batch cannot perturb its neighbours.
+ _needs = ((_derived != _arange) & (attention_mask != 0)).any(dim=1, keepdim=True)
+ if bool(_needs.any()):
+ _positions = torch.where(_needs, _derived, _arange.expand_as(_derived))
+ kwargs["position_ids"] = _positions[
+ :, attention_mask.shape[1] - input_ids.shape[1] :
+ ]
+
if attention_mask is not None:
kwargs["attention_mask"] = attention_mask
if kwargs.pop("use_past_kv_cache", False) or kwargs.get("use_cache", False):
kwargs["use_cache"] = True
# Auto-generate decoder_input_ids for encoder-decoder models
- if (
- "decoder_input_ids" not in kwargs
- and hasattr(self.original_model, "config")
- and getattr(self.original_model.config, "is_encoder_decoder", False)
- ):
+ if "decoder_input_ids" not in kwargs and labels is None and is_encoder_decoder:
decoder_start_token_id = getattr(
self.original_model.config, "decoder_start_token_id", None
)
@@ -2116,7 +2381,11 @@ def forward(
if hasattr(output, "logits"):
logits = output.logits
elif isinstance(output, tuple) and len(output) > 0:
- logits = output[0]
+ # With labels forwarded, HF tuple outputs are (loss, logits, ...).
+ if labels is not None and len(output) > 1:
+ logits = output[1]
+ else:
+ logits = output[0]
elif hasattr(output, "last_hidden_state"):
# Bare encoder models (ViTModel, DeiTModel, BertModel, etc. without
# a task head) return e.g. BaseModelOutput/BaseModelOutputWithPooling,
@@ -2131,6 +2400,18 @@ def forward(
elif return_type == "logits_and_cache":
past_key_values = getattr(output, "past_key_values", None)
return (logits, past_key_values)
+ elif is_encoder_decoder and return_type in ("loss", "both"):
+ assert isinstance(
+ logits, torch.Tensor
+ ), f"Expected seq2seq logits tensor, got {type(logits)}"
+ assert isinstance(labels, torch.Tensor)
+ return self._finalize_seq2seq_return(
+ return_type,
+ logits,
+ labels,
+ output,
+ loss_per_token=loss_per_token,
+ )
elif return_type == "loss":
if getattr(self.cfg, "is_audio_model", False):
raise ValueError(
@@ -2145,7 +2426,7 @@ def forward(
"yourself from the returned logits, or use hf_generate()-style "
"direct access to self.original_model for HF's own loss."
)
- if _is_inputs_embeds:
+ if _is_inputs_embeds and labels is None:
raise ValueError(
"Cannot compute loss with inputs_embeds — token IDs required for labels."
)
@@ -2155,21 +2436,46 @@ def forward(
assert isinstance(
logits, torch.Tensor
), f"Expected logits tensor, got {type(logits)}"
- return self.loss_fn(logits, input_ids, per_token=loss_per_token)
+ if labels is not None:
+ return self._causal_labels_loss(
+ logits,
+ labels,
+ attention_mask=attention_mask,
+ per_token=loss_per_token,
+ )
+ return self.loss_fn(
+ logits,
+ input_ids,
+ attention_mask=attention_mask,
+ per_token=loss_per_token,
+ )
elif return_type == "both":
if getattr(self.cfg, "is_audio_model", False):
raise ValueError(
"Audio models do not support return_type='both'. "
"CTC loss requires aligned frame-level labels."
)
- if _is_inputs_embeds:
+ if _is_inputs_embeds and labels is None:
raise ValueError(
"Cannot compute loss with inputs_embeds — token IDs required for labels."
)
assert isinstance(
logits, torch.Tensor
), f"Expected logits tensor, got {type(logits)}"
- loss = self.loss_fn(logits, input_ids, per_token=loss_per_token)
+ if labels is not None:
+ loss = self._causal_labels_loss(
+ logits,
+ labels,
+ attention_mask=attention_mask,
+ per_token=loss_per_token,
+ )
+ else:
+ loss = self.loss_fn(
+ logits,
+ input_ids,
+ attention_mask=attention_mask,
+ per_token=loss_per_token,
+ )
return (logits, loss)
elif return_type == "predictions":
assert (
@@ -2256,8 +2562,147 @@ def loss_fn(
"""
if tokens.device != logits.device:
tokens = tokens.to(logits.device)
+ if attention_mask is not None:
+ if attention_mask.device != logits.device:
+ attention_mask = attention_mask.to(logits.device)
+ attention_mask = self._prepare_loss_attention_mask(attention_mask, tokens)
return lm_cross_entropy_loss(logits, tokens, attention_mask, per_token)
+ def _causal_labels_loss(
+ self,
+ logits: torch.Tensor,
+ labels: torch.Tensor,
+ attention_mask: Optional[torch.Tensor] = None,
+ per_token: bool = False,
+ ) -> torch.Tensor:
+ """Compute shifted causal loss against explicit labels, ignoring ``-100``."""
+ if labels.device != logits.device:
+ labels = labels.to(logits.device)
+ if labels.shape != logits.shape[:-1]:
+ raise ValueError(
+ "causal labels must match the logits batch and position dimensions, "
+ f"got labels {tuple(labels.shape)} and logits {tuple(logits.shape)}"
+ )
+
+ losses = F.cross_entropy(
+ logits[:, :-1].flatten(0, 1),
+ labels[:, 1:].flatten(),
+ reduction="none",
+ ignore_index=-100,
+ ).view_as(labels[:, 1:])
+ valid_targets = labels[:, 1:] != -100
+ if attention_mask is not None:
+ if attention_mask.device != logits.device:
+ attention_mask = attention_mask.to(logits.device)
+ token_mask = self._prepare_loss_attention_mask(attention_mask, labels)
+ valid_targets &= token_mask[:, :-1] & token_mask[:, 1:]
+ losses = losses.masked_fill(~valid_targets, 0.0)
+ return losses if per_token else losses.sum() / valid_targets.sum()
+
+ @staticmethod
+ def _seq2seq_loss(
+ logits: torch.Tensor,
+ labels: torch.Tensor,
+ native_loss: Any,
+ *,
+ per_token: bool,
+ ) -> torch.Tensor:
+ """Return encoder-decoder loss without the causal LM token shift."""
+ if labels.device != logits.device:
+ labels = labels.to(logits.device)
+ if labels.shape != logits.shape[:-1]:
+ raise ValueError(
+ "seq2seq labels must match the decoder logits batch and position "
+ f"dimensions, got labels {tuple(labels.shape)} and logits "
+ f"{tuple(logits.shape)}"
+ )
+ if not per_token and isinstance(native_loss, torch.Tensor):
+ return native_loss
+
+ losses = F.cross_entropy(
+ logits.flatten(0, 1),
+ labels.flatten(),
+ reduction="none" if per_token else "mean",
+ ignore_index=-100,
+ )
+ return losses.view_as(labels) if per_token else losses
+
+ def _finalize_seq2seq_return(
+ self,
+ return_type: str,
+ logits: torch.Tensor,
+ labels: torch.Tensor,
+ native_output: Any,
+ *,
+ loss_per_token: bool,
+ ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
+ loss = self._seq2seq_loss(
+ logits,
+ labels,
+ getattr(native_output, "loss", None),
+ per_token=loss_per_token,
+ )
+ return (logits, loss) if return_type == "both" else loss
+
+ @staticmethod
+ def _prepare_loss_attention_mask(
+ attention_mask: torch.Tensor, tokens: torch.Tensor
+ ) -> torch.Tensor:
+ """Reduce a forward attention mask to the token window scored by the loss."""
+ batch, pos = tokens.shape
+ if attention_mask.ndim not in (2, 4):
+ raise ValueError(
+ "attention_mask must be 2D [batch, key_pos] or 4D "
+ f"[batch, *, query_pos, key_pos], got shape {tuple(attention_mask.shape)}"
+ )
+ if attention_mask.shape[0] != batch:
+ raise ValueError(
+ "attention_mask batch dimension must match tokens, "
+ f"got {attention_mask.shape[0]} and {batch}"
+ )
+
+ if attention_mask.ndim == 2:
+ if attention_mask.shape[1] < pos:
+ raise ValueError(
+ "attention_mask must cover every scored token, "
+ f"got length {attention_mask.shape[1]} for {pos} tokens"
+ )
+ return attention_mask[:, -pos:].bool()
+
+ query_pos, key_pos = attention_mask.shape[-2:]
+ if key_pos < pos:
+ raise ValueError(
+ "attention_mask must cover every scored token, "
+ f"got key length {key_pos} for {pos} tokens"
+ )
+
+ blocked = attention_mask if attention_mask.dtype is torch.bool else attention_mask < -1.0
+ if query_pos == 1:
+ # Broadcast key-only masks use one query row for the full sequence.
+ keep = ~blocked[..., 0, -pos:]
+ else:
+ if query_pos < pos:
+ raise ValueError(
+ "attention_mask must contain a query row for every scored token, "
+ f"got {query_pos} rows for {pos} tokens"
+ )
+ # The aligned diagonal excludes causal masking while retaining padding.
+ diagonal = torch.diagonal(
+ blocked,
+ offset=key_pos - query_pos,
+ dim1=-2,
+ dim2=-1,
+ )
+ if diagonal.shape[-1] < pos:
+ raise ValueError(
+ "attention_mask diagonal must cover every scored token, "
+ f"got length {diagonal.shape[-1]} for {pos} tokens"
+ )
+ keep = ~diagonal[..., -pos:]
+
+ # A token is padding only when every broadcast/head mask blocks its key.
+ return keep.reshape(batch, -1, pos).any(dim=1)
+
@overload
def run_with_cache(
self,
@@ -2414,6 +2859,7 @@ def cache_hook(tensor: torch.Tensor, *, hook: Any) -> Optional[torch.Tensor]:
effective_stop_layer = len(self.blocks) + stop_at_layer
else:
effective_stop_layer = stop_at_layer
+ gated_names_skipped: List[str] = []
for hook_name, hook in hook_dict.items():
if names_filter_fn(hook_name):
if effective_stop_layer is not None:
@@ -2424,7 +2870,26 @@ def cache_hook(tensor: torch.Tensor, *, hook: Any) -> Optional[torch.Tensor]:
continue
except (IndexError, ValueError):
pass
+
+ # Only validate gated hooks when the caller explicitly supplied
+ # a names_filter. The default filter matches every hook and must
+ # not cause gated hooks to be treated as explicitly requested.
+ if names_filter is not None:
+ try:
+ self.check_hooks_to_add(hook_name)
+ except ValueError:
+ gated_names_skipped.append(hook_name)
+ continue
+
hooks.append((hook, hook_name))
+
+ if names_filter is not None and gated_names_skipped:
+ warnings.warn(
+ f"run_with_cache: skipped {len(gated_names_skipped)} gated-off hook name(s) "
+ f"that will never be cached: {gated_names_skipped}. Call the relevant "
+ "set_use_*(True) setter first to enable them.",
+ stacklevel=2,
+ )
self.context_level += 1
context_level = self.context_level
try:
@@ -2440,13 +2905,15 @@ def cache_hook(tensor: torch.Tensor, *, hook: Any) -> Optional[torch.Tensor]:
processed_args = [input]
if processed_args and isinstance(processed_args[0], str):
assert self.tokenizer is not None, "Tokenizer must be set to pass string input."
- input_ids = self.to_tokens(processed_args[0])
+ prepend_bos = kwargs.pop("prepend_bos", None)
+ input_ids = self.to_tokens(processed_args[0], prepend_bos=prepend_bos)
input_ids = input_ids.to(next(self.original_model.parameters()).device)
kwargs["input_ids"] = input_ids
processed_args = processed_args[1:]
elif "input" in kwargs and isinstance(kwargs["input"], str):
assert self.tokenizer is not None, "Tokenizer must be set to pass string input."
- input_ids = self.to_tokens(kwargs["input"])
+ prepend_bos = kwargs.pop("prepend_bos", None)
+ input_ids = self.to_tokens(kwargs["input"], prepend_bos=prepend_bos)
input_ids = input_ids.to(next(self.original_model.parameters()).device)
kwargs["input_ids"] = input_ids
del kwargs["input"]
@@ -2618,7 +3085,12 @@ def run_with_hooks(
effective_stop_layer = stop_at_layer
def add_hook_to_point(
- hook_point: HookPoint, hook_fn: Callable, name: str, dir: Literal["fwd", "bwd"] = "fwd"
+ hook_point: HookPoint,
+ hook_fn: Callable,
+ name: str,
+ dir: Literal["fwd", "bwd"] = "fwd",
+ *,
+ is_explicit: bool = True,
):
if effective_stop_layer is not None and name.startswith("blocks."):
try:
@@ -2627,6 +3099,15 @@ def add_hook_to_point(
return
except (IndexError, ValueError):
pass
+ if is_explicit:
+ self.check_hooks_to_add(name)
+ elif self._gated_hook_reason(name) is not None:
+ warnings.warn(
+ f"run_with_hooks(): filter matched gated-off hook name '{name}', skipped. "
+ "Call the relevant set_use_*(True) setter first to enable it.",
+ stacklevel=2,
+ )
+ return
if self.compatibility_mode and name != hook_point.name:
alias_names_list: list[str] = []
if hook_point.name is not None:
@@ -2665,7 +3146,11 @@ def wrapped_hook_fn(tensor, hook, _orig_fn=original_hook_fn):
actual_hook_name = aliases[hook_name_or_filter]
if actual_hook_name in hook_dict:
add_hook_to_point(
- hook_dict[actual_hook_name], hook_fn, actual_hook_name, direction
+ hook_dict[actual_hook_name],
+ hook_fn,
+ actual_hook_name,
+ direction,
+ is_explicit=True,
)
else:
hook_dict = self.hook_dict
@@ -2677,7 +3162,13 @@ def wrapped_hook_fn(tensor, hook, _orig_fn=original_hook_fn):
continue
seen_hooks.add(hook_id)
hook_name_to_use = hook_point.name if hook_point.name else name
- add_hook_to_point(hook_point, hook_fn, hook_name_to_use, direction)
+ add_hook_to_point(
+ hook_point,
+ hook_fn,
+ hook_name_to_use,
+ direction,
+ is_explicit=False,
+ )
try:
self.context_level = context_level
@@ -2777,6 +3268,21 @@ def _resolve_stopping_criteria(
return criteria if len(criteria) > 0 else None
+ def _encdec_ngram_processor(self) -> Optional[Any]:
+ """generation_config.no_repeat_ngram_size as transformers' own
+ processor, or None. HF applies it by default; parity for models whose
+ greedy decode needs it to escape token attractors."""
+ size = getattr(
+ getattr(self.original_model, "generation_config", None),
+ "no_repeat_ngram_size",
+ None,
+ )
+ if not size:
+ return None
+ from transformers.generation.logits_process import NoRepeatNGramLogitsProcessor
+
+ return NoRepeatNGramLogitsProcessor(size)
+
def _generate_tokens(
self,
current_tokens: torch.Tensor,
@@ -2808,6 +3314,10 @@ def _generate_tokens(
multimodal_kwargs: Dict[str, Any],
verbose: bool,
stopping_criteria_list: Optional[Any] = None,
+ initial_attention_mask: Optional[torch.Tensor] = None,
+ min_decoder_length: Optional[int] = None,
+ ngram_processor: Optional[Any] = None,
+ encoder_attention_mask: Optional[torch.Tensor] = None,
) -> Generator[Tuple[torch.Tensor, torch.Tensor, bool], None, None]:
"""Core generation loop. Yields (sampled_tokens, final_logits, all_finished) per step.
@@ -2823,6 +3333,23 @@ def _generate_tokens(
# A row may finish via EOS and/or any of the configured stopping criteria.
any_stop_active = stop_at_eos or stopping_criteria_list is not None
+ # Models that own their position derivation (the gate refuses them) cache
+ # mRoPE deltas on the module between calls; a text-only prefill never
+ # refreshes them, so a stale delta from an earlier multimodal forward gets
+ # added to every cached-step position. HF's generate recomputes them at
+ # prefill via prepare_inputs_for_generation, which this loop bypasses —
+ # so match it by clearing before the prompt pass. A multimodal prefill
+ # recomputes its own fresh deltas regardless.
+ if not self._accepts_derived_position_ids():
+ underlying = getattr(self, "original_model", None)
+ for module in (
+ underlying,
+ getattr(underlying, "model", None),
+ getattr(underlying, "language_model", None),
+ ):
+ if module is not None and hasattr(module, "rope_deltas"):
+ module.rope_deltas = None
+
# Pure-SSM models (Mamba-1/2) take the stateful cache as `cache_params`;
# modern hybrids (Bamba, NemotronH, FalconH1) take `past_key_values` and
# would receive a duplicate cache_params via **kwargs cascade otherwise.
@@ -2835,17 +3362,44 @@ def _generate_tokens(
for gen_step_idx in tqdm.tqdm(range(max_new_tokens), disable=not verbose):
with torch.no_grad():
if is_encoder_decoder:
+ assert encoder_input is not None
+ encdec_kwargs: Dict[str, Any] = {}
+ if encoder_attention_mask is not None:
+ encdec_kwargs["attention_mask"] = encoder_attention_mask.to(
+ encoder_input.device
+ )
logits = self(
encoder_input,
return_type="logits",
decoder_input=decoder_tokens,
+ **encdec_kwargs,
)
else:
forward_kwargs: Dict[str, Any] = {}
+ # A prompt mask covers only the prompt, so extend it by one
+ # attended column per token generated so far. position_ids are
+ # left to forward(), which derives them from the mask for the
+ # models that can take them.
+ running_attention_mask: Optional[torch.Tensor] = None
+ if initial_attention_mask is not None:
+ n_generated = current_tokens.shape[1] - initial_attention_mask.shape[1]
+ running_attention_mask = torch.cat(
+ [
+ initial_attention_mask.to(current_tokens.device),
+ torch.ones(
+ (current_tokens.shape[0], n_generated),
+ dtype=initial_attention_mask.dtype,
+ device=current_tokens.device,
+ ),
+ ],
+ dim=1,
+ )
+ forward_kwargs["attention_mask"] = running_attention_mask
# Compute attention mask and position_ids for batched
# inputs with padding.
if (
- _is_batched_list
+ initial_attention_mask is None
+ and _is_batched_list
and self.tokenizer is not None
and self.tokenizer.pad_token_id is not None
):
@@ -2858,9 +3412,12 @@ def _generate_tokens(
).to(self.cfg.device)
self.tokenizer.padding_side = _prev_side
forward_kwargs["attention_mask"] = attn_mask
- position_ids = attn_mask.long().cumsum(-1) - 1
- position_ids.masked_fill_(attn_mask == 0, 1)
- forward_kwargs["position_ids"] = position_ids
+ # Same target gate as the forward() path: the mask is safe
+ # for every model, the derived positions are not (#1626).
+ if self._accepts_derived_position_ids():
+ position_ids = attn_mask.long().cumsum(-1) - 1
+ position_ids.masked_fill_(attn_mask == 0, 1)
+ forward_kwargs["position_ids"] = position_ids
if gen_step_idx == 0:
if pixel_values is not None:
forward_kwargs["pixel_values"] = pixel_values
@@ -2914,17 +3471,32 @@ def _generate_tokens(
dtype=torch.long,
device=device,
)
- if "position_ids" in forward_kwargs:
- forward_kwargs["position_ids"] = forward_kwargs["position_ids"][
- :, -1:
- ]
- else:
- forward_kwargs["position_ids"] = torch.full(
- (batch_size, 1),
- total_len - 1,
- dtype=torch.long,
- device=device,
- )
+ # Gated as a whole (#1626): every branch below supplies
+ # position_ids, so gating only the prompt derivation
+ # above would divert a refused model into the
+ # total_len - 1 fallback, which counts pad slots and is
+ # wrong per row for a left-padded batch. A model that
+ # owns its position derivation gets the mask alone,
+ # matching the uncached path.
+ if self._accepts_derived_position_ids():
+ if "position_ids" in forward_kwargs:
+ forward_kwargs["position_ids"] = forward_kwargs["position_ids"][
+ :, -1:
+ ]
+ elif running_attention_mask is not None:
+ # total_len - 1 counts pad slots, so it is wrong
+ # for a left-padded prompt. Derive the new token's
+ # position from the mask instead.
+ forward_kwargs["position_ids"] = utils.get_offset_position_ids(
+ 0, running_attention_mask.long()
+ )[:, -1:]
+ else:
+ forward_kwargs["position_ids"] = torch.full(
+ (batch_size, 1),
+ total_len - 1,
+ dtype=torch.long,
+ device=device,
+ )
logits = self(
current_tokens[:, -1:],
return_type="logits",
@@ -2949,6 +3521,23 @@ def _generate_tokens(
if _generate_from_embeds and generated_token_ids
else None
)
+ # transformers' own NoRepeatNGramLogitsProcessor, honoring
+ # generation_config (bart-large-cnn pins 3; without it greedy
+ # decoding falls into a BOS attractor and emits nothing).
+ if ngram_processor is not None and decoder_tokens is not None:
+ final_logits = ngram_processor(decoder_tokens, final_logits)
+ # HF's generate() suppresses EOS below generation_config.min_length
+ # (bart-large-cnn pins 56); without this the loop can EOS on step
+ # one and emit an empty summary.
+ if (
+ min_decoder_length is not None
+ and is_encoder_decoder
+ and decoder_tokens is not None
+ and decoder_tokens.shape[1] < min_decoder_length
+ and stop_tokens
+ ):
+ final_logits = final_logits.clone()
+ final_logits[:, stop_tokens] = float("-inf")
if do_sample:
sampled_tokens = utils.sample_logits(
final_logits,
@@ -3083,6 +3672,8 @@ def generate(
pixel_values: Optional[torch.Tensor] = None,
stop_strings: Optional[Union[str, List[str]]] = None,
stopping_criteria: Optional[Any] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ forced_bos_token_id: Optional[int] = None,
**multimodal_kwargs,
) -> (
str
@@ -3159,6 +3750,24 @@ def generate(
Stateful/SSM models raise only when run with use_past_kv_cache=False (the
default keeps them on the hooked loop). Each error names the supported
alternative.
+ attention_mask: Optional ``[batch, pos]`` 0/1 mask over the prompt, marking
+ which prompt tokens are real. Required to generate correctly from an
+ already-padded token tensor: without it the pad tokens are treated as
+ real context and every real token's position is shifted, so the
+ continuation differs from the same prompt unpadded. The mask is extended
+ by one attended column per generated token. Takes precedence over the
+ ``padding_side`` heuristic, and unlike it can express an interior gap or
+ a pad id that also occurs as a real token. Passing ``padding_side``
+ instead reads the padding off the pad token, which is enough for the
+ common single-edge case, and raises if this bridge has no tokenizer
+ or pad id to read it from. On the encoder-decoder and inputs_embeds
+ paths the mask is forwarded to the model as-is rather than grown per
+ step, which is what processors emitting one alongside
+ ``pixel_values`` expect.
+ forced_bos_token_id: Optional token id seeded as the first decoder token
+ after ``decoder_start`` on encoder-decoder models. Multilingual
+ translators (M2M100/MBart/NLLB) select their target language this way.
+ Raises ValueError on decoder-only models.
Returns:
Generated sequence as string, list of strings, or tensor depending on input type and return_type.
@@ -3189,22 +3798,44 @@ def generate(
use_past_kv_cache = self._resolve_generation_caching(use_past_kv_cache, _is_batched_list)
_generate_from_embeds = False
+ _encdec_early = hasattr(self.original_model, "config") and getattr(
+ self.original_model.config, "is_encoder_decoder", False
+ )
if isinstance(input, str):
- input_tokens = self.to_tokens(
- input, prepend_bos=prepend_bos, move_to_device=True, truncate=False
- )
+ if _encdec_early:
+ # Deliberate divergence: prepend_bos is IGNORED for enc-dec
+ # string/list input. Encoder input follows the tokenizer's own
+ # recipe (lang token + trailing ); to_tokens' decoder-style
+ # BOS policy corrupts it — m2m100 degenerates to loops.
+ input_tokens = self.tokenizer(input, return_tensors="pt")["input_ids"].to(
+ self.cfg.device
+ )
+ else:
+ input_tokens = self.to_tokens(
+ input, prepend_bos=prepend_bos, move_to_device=True, truncate=False
+ )
input_type = "str"
elif isinstance(input, list):
- # Force left-padding for batched generation so real tokens are
- # flush-right and logits[:, -1, :] is always the last real token.
- if _is_batched_list:
- _orig_padding_side = self.tokenizer.padding_side
- self.tokenizer.padding_side = "left"
- input_tokens = self.to_tokens(
- input, prepend_bos=prepend_bos, move_to_device=True, truncate=False
- )
- if _is_batched_list:
- self.tokenizer.padding_side = _orig_padding_side
+ if _encdec_early:
+ # Same native-recipe rule as the str branch: to_tokens' BOS
+ # policy corrupts encoder inputs (stray , dropped ).
+ # Keep the tokenizer's mask too — unequal rows otherwise
+ # attend over pads in the encoder.
+ _enc_batch = self.tokenizer(input, return_tensors="pt", padding=True)
+ input_tokens = _enc_batch["input_ids"].to(self.cfg.device)
+ if attention_mask is None and "attention_mask" in _enc_batch:
+ attention_mask = _enc_batch["attention_mask"].to(self.cfg.device)
+ else:
+ # Force left-padding for batched generation so real tokens are
+ # flush-right and logits[:, -1, :] is always the last real token.
+ if _is_batched_list:
+ _orig_padding_side = self.tokenizer.padding_side
+ self.tokenizer.padding_side = "left"
+ input_tokens = self.to_tokens(
+ input, prepend_bos=prepend_bos, move_to_device=True, truncate=False
+ )
+ if _is_batched_list:
+ self.tokenizer.padding_side = _orig_padding_side
input_type = "list"
elif isinstance(input, torch.Tensor) and input.is_floating_point():
# inputs_embeds: pre-computed embeddings (e.g., from multimodal models)
@@ -3215,6 +3846,63 @@ def generate(
input_tokens = input.to(self.cfg.device)
input_type = "tokens"
+ # Without one of these a pre-padded tensor generates as though its pads were
+ # real context, shifting every real token's position (#1612). An explicit
+ # mask wins; otherwise the padding is read off the tokens, but only when the
+ # caller asked for that by passing padding_side. Deriving a mask on the
+ # default path would silently change behaviour for every existing caller,
+ # and would demand a real tokenizer where today none is required.
+ initial_attention_mask: Optional[torch.Tensor] = attention_mask
+ if initial_attention_mask is not None and (
+ _generate_from_embeds
+ or getattr(getattr(self.original_model, "config", None), "is_encoder_decoder", False)
+ ):
+ # Growing the mask per step only means something for decoder-only token
+ # generation. On these paths the mask used to arrive via
+ # **multimodal_kwargs and be forwarded to the model untouched — as
+ # processors emit it alongside pixel_values — so keep doing that rather
+ # than reject a call that worked before this parameter existed.
+ multimodal_kwargs = {**multimodal_kwargs, "attention_mask": initial_attention_mask}
+ initial_attention_mask = None
+ if initial_attention_mask is not None:
+ if initial_attention_mask.shape != input_tokens.shape:
+ raise ValueError(
+ f"attention_mask shape {tuple(initial_attention_mask.shape)} does not "
+ f"match the prompt shape {tuple(input_tokens.shape)}. Pass a 0/1 mask "
+ "covering exactly the prompt tokens; generate() extends it itself."
+ )
+ initial_attention_mask = initial_attention_mask.to(self.cfg.device)
+ elif padding_side is not None and input_type == "tokens":
+ # Reading the padding off the tokens needs a tokenizer with a pad id.
+ # Without one the argument would be inert, leaving exactly the bug this
+ # fixes — silently, on a bridge booted without a tokenizer. Say so
+ # rather than generate something quietly wrong.
+ if not isinstance(self.tokenizer, PreTrainedTokenizerBase):
+ raise ValueError(
+ "generate(padding_side=...) reads the padding off the pad token, "
+ "which needs a tokenizer; this bridge has none. Pass "
+ "attention_mask=... to state the padding directly instead."
+ )
+ if self.tokenizer.pad_token_id is None:
+ raise ValueError(
+ "generate(padding_side=...) reads the padding off the pad token, "
+ "but this tokenizer has no pad_token_id. Set one, or pass "
+ "attention_mask=... to state the padding directly instead."
+ )
+ _prepend = self.cfg.default_prepend_bos if prepend_bos is None else prepend_bos
+ _orig_side = self.tokenizer.padding_side
+ self.tokenizer.padding_side = padding_side
+ try:
+ initial_attention_mask = utils.get_attention_mask(
+ self.tokenizer, input_tokens, _prepend
+ ).to(self.cfg.device)
+ finally:
+ self.tokenizer.padding_side = _orig_side
+ # An all-ones mask is what the model assumes anyway; skipping it keeps
+ # the unpadded path byte-identical to before.
+ if initial_attention_mask is not None and bool(initial_attention_mask.all()):
+ initial_attention_mask = None
+
# Determine return type
if return_type == "input":
if input_type in ["str", "list"]:
@@ -3266,6 +3954,18 @@ def generate(
is_encoder_decoder = hasattr(self.original_model, "config") and getattr(
self.original_model.config, "is_encoder_decoder", False
)
+ if forced_bos_token_id is None and is_encoder_decoder:
+ # HF's generate() applies generation_config defaults; bart-large-cnn
+ # pins forced_bos_token_id=0 there and degrades without it.
+ forced_bos_token_id = getattr(
+ getattr(self.original_model, "generation_config", None),
+ "forced_bos_token_id",
+ None,
+ )
+ if forced_bos_token_id is not None and not is_encoder_decoder:
+ # Raise before any state mutation (_capture_hf_cache) and before
+ # the stateful hf_generate early-return would drop the kwarg.
+ raise ValueError("forced_bos_token_id is only meaningful for encoder-decoder models")
# return_cache recomputes run_with_cache on the generated output (see issue #697).
# That is well-defined only for single-sequence, decoder-only text generation, so
@@ -3434,6 +4134,16 @@ def generate(
dtype=input_tokens.dtype,
device=self.cfg.device,
)
+ if forced_bos_token_id is not None:
+ # Multilingual seq2seq (M2M100/MBart/NLLB) selects the target
+ # language via the first decoder token after decoder_start.
+ forced = torch.full(
+ (batch_size, 1),
+ forced_bos_token_id,
+ dtype=input_tokens.dtype,
+ device=self.cfg.device,
+ )
+ decoder_tokens = torch.cat([decoder_tokens, forced], dim=1)
try:
for sampled_tokens, final_logits, all_finished in self._generate_tokens(
@@ -3465,6 +4175,18 @@ def generate(
multimodal_kwargs=multimodal_kwargs if multimodal_kwargs else {},
verbose=verbose,
stopping_criteria_list=stopping_criteria_list,
+ initial_attention_mask=initial_attention_mask,
+ min_decoder_length=(
+ getattr(
+ getattr(self.original_model, "generation_config", None),
+ "min_length",
+ None,
+ )
+ if is_encoder_decoder
+ else None
+ ),
+ ngram_processor=(self._encdec_ngram_processor() if is_encoder_decoder else None),
+ encoder_attention_mask=(attention_mask if is_encoder_decoder else None),
):
sampled_tokens_list.append(sampled_tokens.unsqueeze(1))
if logits_seq_list is not None:
@@ -3480,7 +4202,8 @@ def generate(
sampled_tokens = torch.cat(sampled_tokens_list, dim=1)
if is_encoder_decoder:
# Reconstruct full decoder sequence: start token + generated tokens
- output_tokens = torch.cat([decoder_tokens[:, :1], sampled_tokens], dim=1)
+ decoder_seed_len = 2 if forced_bos_token_id is not None else 1
+ output_tokens = torch.cat([decoder_tokens[:, :decoder_seed_len], sampled_tokens], dim=1)
elif _generate_from_embeds:
# For inputs_embeds, we only have the generated token IDs (no input token IDs)
output_tokens = sampled_tokens
@@ -3677,22 +4400,38 @@ def generate_stream(
_is_batched_list = isinstance(input, list) and len(input) > 1
use_past_kv_cache = self._resolve_generation_caching(use_past_kv_cache, _is_batched_list)
+ _encdec_early = hasattr(self.original_model, "config") and getattr(
+ self.original_model.config, "is_encoder_decoder", False
+ )
if isinstance(input, str):
- input_tokens = self.to_tokens(
- input, prepend_bos=prepend_bos, move_to_device=True, truncate=False
- )
+ if _encdec_early:
+ # Native recipe: to_tokens' BOS policy corrupts encoder inputs.
+ input_tokens = self.tokenizer(input, return_tensors="pt")["input_ids"].to(
+ self.cfg.device
+ )
+ else:
+ input_tokens = self.to_tokens(
+ input, prepend_bos=prepend_bos, move_to_device=True, truncate=False
+ )
input_type = "str"
elif isinstance(input, list):
- if _is_batched_list:
+ if _encdec_early:
+ input_tokens = self.tokenizer(input, return_tensors="pt", padding=True)[
+ "input_ids"
+ ].to(self.cfg.device)
+ elif _is_batched_list:
_orig_ps = self.tokenizer.padding_side
self.tokenizer.padding_side = "left"
- try:
+ try:
+ input_tokens = self.to_tokens(
+ input, prepend_bos=prepend_bos, move_to_device=True, truncate=False
+ )
+ finally:
+ self.tokenizer.padding_side = _orig_ps
+ else:
input_tokens = self.to_tokens(
input, prepend_bos=prepend_bos, move_to_device=True, truncate=False
)
- finally:
- if _is_batched_list:
- self.tokenizer.padding_side = _orig_ps
input_type = "list"
else:
input_tokens = input.to(self.cfg.device)
@@ -4156,6 +4895,37 @@ def train(self, mode: bool = True) -> "TransformerBridge":
original.train(mode)
return self
+ def _gated_hook_reason(self, hook_point_name: str) -> Optional[str]:
+ """Return the disabled setter name if hook_point_name is gated off, else None."""
+ if hook_point_name.endswith("attn.hook_result") and not self.cfg.use_attn_result:
+ return "use_attn_result"
+ if (
+ hook_point_name.endswith(("hook_q_input", "hook_k_input", "hook_v_input"))
+ and not self.cfg.use_split_qkv_input
+ ):
+ return "use_split_qkv_input"
+ if hook_point_name.endswith("mlp_in") and not self.cfg.use_hook_mlp_in:
+ return "use_hook_mlp_in"
+ if hook_point_name.endswith("attn_in") and not self.cfg.use_attn_in:
+ return "use_attn_in"
+ return None
+
+ def check_hooks_to_add(self, hook_point_name: str) -> None:
+ """Raise a clear error if a hook is being explicitly added to a gated-off hook point.
+
+ Mirrors HookedTransformer.check_hooks_to_add, but raises a ValueError
+ naming the setter to call, instead of a bare assert. Only for explicit,
+ user-named hook points — a filter/callable matching a gated name uses
+ _gated_hook_reason directly and skips with a warning instead, since the
+ filter was not necessarily targeting that name on purpose.
+ """
+ reason = self._gated_hook_reason(hook_point_name)
+ if reason is not None:
+ raise ValueError(
+ f"Cannot add hook {hook_point_name} because {reason} is False. "
+ f"Call set_{reason}(True) first."
+ )
+
def add_hook(
self,
name: Union[str, Callable[[str], bool]],
@@ -4177,13 +4947,24 @@ def add_hook(
if callable(name) and not isinstance(name, str):
hook_dict = self.hook_dict
seen_hooks: set[int] = set()
+ gated_names_skipped: List[str] = []
for hook_name, hook_point in hook_dict.items():
if name(hook_name):
hook_id = id(hook_point)
if hook_id in seen_hooks:
continue
seen_hooks.add(hook_id)
+ if self._gated_hook_reason(hook_name) is not None:
+ gated_names_skipped.append(hook_name)
+ continue
hook_point.add_hook(hook_fn, dir=dir, is_permanent=is_permanent)
+ if gated_names_skipped:
+ warnings.warn(
+ f"add_hook: filter matched {len(gated_names_skipped)} gated-off hook "
+ f"name(s) that were skipped: {gated_names_skipped}. Call the relevant "
+ "set_use_*(True) setter first to enable them.",
+ stacklevel=2,
+ )
return
component = self
@@ -4197,6 +4978,7 @@ def add_hook(
if hasattr(component, hook_name):
hook_point = getattr(component, hook_name)
if isinstance(hook_point, HookPoint):
+ self.check_hooks_to_add(name)
hook_point.add_hook(hook_fn, dir=dir, is_permanent=is_permanent)
else:
raise AttributeError(
@@ -4219,16 +5001,52 @@ def add_perma_hook(
"""
self.add_hook(name, hook_fn, dir=dir, is_permanent=True)
- def reset_hooks(self, clear_contexts=True):
- """Remove all hooks from the model."""
+ def hook_points(self) -> Iterable[HookPoint]:
+ """All registered :class:`HookPoint` instances."""
+ return self._hook_registry.values()
- def remove_hooks_recursive(module):
- if isinstance(module, GeneralizedComponent):
- module.remove_hooks()
- for child in module.children():
- remove_hooks_recursive(child)
+ def clear_contexts(self) -> None:
+ """Clear the stored ``ctx`` on every registered hook point."""
+ for hp in self._hook_registry.values():
+ hp.clear_context()
- remove_hooks_recursive(self)
+ def remove_all_hook_fns(
+ self,
+ direction: Literal["fwd", "bwd", "both"] = "both",
+ including_permanent: bool = False,
+ level: Optional[int] = None,
+ ) -> None:
+ """Remove hook functions from every registered hook point."""
+ for hp in self._hook_registry.values():
+ hp.remove_hooks(dir=direction, including_permanent=including_permanent, level=level)
+
+ def reset_hooks(
+ self,
+ clear_contexts: bool = True,
+ direction: Literal["fwd", "bwd", "both"] = "both",
+ including_permanent: bool = False,
+ level: Optional[int] = None,
+ ) -> None:
+ """Remove hooks from the model; mirrors ``HookedRootModule.reset_hooks``.
+
+ Clears through the hook registry (which holds hook points the component
+ walk cannot reach, e.g. alias-registered points) and, on a full reset,
+ additionally walks the component tree — dev's registry is not asserted
+ canonical, so both passes run belt-and-suspenders.
+ """
+ if clear_contexts:
+ self.clear_contexts()
+ self.remove_all_hook_fns(direction, including_permanent=including_permanent, level=level)
+
+ if direction == "both" and level is None:
+
+ def remove_hooks_recursive(module):
+ if isinstance(module, GeneralizedComponent):
+ module.remove_hooks()
+ for child in module.children():
+ remove_hooks_recursive(child)
+
+ remove_hooks_recursive(self)
def hooks(self, fwd_hooks=[], bwd_hooks=[], reset_hooks_end=True, clear_contexts=False):
"""Context manager for temporarily adding hooks.
@@ -4258,7 +5076,18 @@ def add_hook_to_point(
hook_fn: Callable,
name: str,
dir: Literal["fwd", "bwd"] = "fwd",
+ *,
+ is_explicit: bool = True,
):
+ if is_explicit:
+ self.check_hooks_to_add(name)
+ elif self._gated_hook_reason(name) is not None:
+ warnings.warn(
+ f"hooks(): filter matched gated-off hook name '{name}', skipped. "
+ "Call the relevant set_use_*(True) setter first to enable it.",
+ stacklevel=2,
+ )
+ return
if self.compatibility_mode and name != hook_point.name:
alias_names_list: list[str] = []
if hook_point.name is not None:
@@ -4282,7 +5111,11 @@ def apply_hooks(hooks: List[Tuple[Union[str, Callable], Callable]], is_fwd: bool
actual_hook_name = aliases[hook_name_or_filter]
if actual_hook_name in hook_dict:
add_hook_to_point(
- hook_dict[actual_hook_name], hook_fn, actual_hook_name, direction
+ hook_dict[actual_hook_name],
+ hook_fn,
+ actual_hook_name,
+ direction,
+ is_explicit=True,
)
else:
hook_dict = self.hook_dict
@@ -4294,7 +5127,13 @@ def apply_hooks(hooks: List[Tuple[Union[str, Callable], Callable]], is_fwd: bool
continue
seen_hooks.add(hook_id)
hook_name_to_use = hook_point.name if hook_point.name else name
- add_hook_to_point(hook_point, hook_fn, hook_name_to_use, direction)
+ add_hook_to_point(
+ hook_point,
+ hook_fn,
+ hook_name_to_use,
+ direction,
+ is_explicit=False,
+ )
try:
self.context_level = context_level
@@ -4320,7 +5159,7 @@ def set_use_attn_result(self, use_attn_result: bool):
"""
if use_attn_result:
self._validate_attention_fork_supported("use_attn_result")
- self.cfg.use_attn_result = use_attn_result
+ self.cfg._set_bridge_managed_hook_flag("use_attn_result", use_attn_result)
self._propagate_attention_flag("use_attn_result", use_attn_result)
def set_use_split_qkv_input(self, use_split_qkv_input: bool):
@@ -4335,7 +5174,7 @@ def set_use_split_qkv_input(self, use_split_qkv_input: bool):
"Call set_use_attn_in(False) before enabling use_split_qkv_input."
)
self._validate_attention_fork_supported("use_split_qkv_input")
- self.cfg.use_split_qkv_input = use_split_qkv_input
+ self.cfg._set_bridge_managed_hook_flag("use_split_qkv_input", use_split_qkv_input)
self._propagate_attention_flag("use_split_qkv_input", use_split_qkv_input)
def set_use_attn_in(self, use_attn_in: bool):
@@ -4353,7 +5192,7 @@ def set_use_attn_in(self, use_attn_in: bool):
"Call set_use_split_qkv_input(False) before enabling use_attn_in."
)
self._validate_attention_fork_supported("use_attn_in")
- self.cfg.use_attn_in = use_attn_in
+ self.cfg._set_bridge_managed_hook_flag("use_attn_in", use_attn_in)
self._propagate_attention_flag("use_attn_in", use_attn_in)
def set_use_hook_mlp_in(self, use_hook_mlp_in: bool) -> None:
@@ -4362,18 +5201,26 @@ def set_use_hook_mlp_in(self, use_hook_mlp_in: bool) -> None:
See :py:meth:`HookedTransformer.set_use_hook_mlp_in`.
"""
- self.cfg.use_hook_mlp_in = use_hook_mlp_in
+ self.cfg._set_bridge_managed_hook_flag("use_hook_mlp_in", use_hook_mlp_in)
if not hasattr(self, "blocks"):
return
for block in self.blocks:
block_cfg = getattr(block, "config", None)
if block_cfg is not None and block_cfg is not self.cfg:
try:
- block_cfg.use_hook_mlp_in = use_hook_mlp_in
- except Exception:
+ self._write_propagated_hook_flag(block_cfg, "use_hook_mlp_in", use_hook_mlp_in)
+ except (AttributeError, TypeError):
pass
block._use_hook_mlp_in = use_hook_mlp_in
+ @staticmethod
+ def _write_propagated_hook_flag(config: Any, flag_name: str, value: bool) -> None:
+ """Write a cloned config flag without dispatching through its live Bridge."""
+ if isinstance(config, TransformerBridgeConfig):
+ config._set_bridge_managed_hook_flag(flag_name, value)
+ else:
+ object.__setattr__(config, flag_name, value)
+
def _propagate_attention_flag(self, flag_name: str, value: bool) -> None:
"""Mirror `bridge.cfg.` onto every block's attention config.
@@ -4394,11 +5241,10 @@ def _propagate_attention_flag(self, flag_name: str, value: bool) -> None:
attn_cfg = getattr(attn, "config", None)
if attn_cfg is not None and attn_cfg is not self.cfg:
try:
- setattr(attn_cfg, flag_name, value)
- except Exception:
- # Some cfg objects may be frozen/immutable. Skip silently —
- # the block simply won't honor the flag, which is the
- # same outcome as before this fix.
+ self._write_propagated_hook_flag(attn_cfg, flag_name, value)
+ except (AttributeError, TypeError):
+ # Some config-like objects reject attributes even when
+ # bypassing their custom __setattr__ implementation.
pass
def _validate_attention_fork_supported(self, flag_name: str) -> None:
@@ -4567,8 +5413,8 @@ def _normalize_bridge_key_to_hf(self, key: str) -> str:
block_list_names = {"blocks", "L_blocks", "H_blocks", "encoder_blocks", "decoder_blocks"}
for tl_name, component in component_mapping.items():
if component.name and tl_name not in block_list_names:
- # Skip if TL name is already a suffix of the HF path (avoids doubling).
- if tl_name != component.name and not component.name.endswith("." + tl_name):
+ # Skip if TL name is already a segment of its HF path (avoids doubling).
+ if tl_name != component.name and tl_name not in component.name.split("."):
attr_to_hf[tl_name] = component.name
# Map block-level components (ln1, ln2, attn, mlp) for all block lists
@@ -4600,9 +5446,10 @@ def state_dict(self, destination=None, prefix="", keep_vars=False):
Converts HuggingFace format keys to TransformerLens format and filters out
_original_component references and nested HuggingFace components.
- This returns a clean state dict with only bridge component paths converted to TL format,
- excluding nested HF components (like c_fc, c_proj, c_attn) that exist inside
- original_component modules.
+ A direct no-argument call returns a clean state dict with bridge component
+ paths converted to TL format. Calls that supply ``destination`` or
+ ``prefix`` use standard ``nn.Module`` recursive semantics so a Bridge can
+ compose inside a parent module.
Args:
destination: Optional dict to store state dict in
@@ -4610,14 +5457,17 @@ def state_dict(self, destination=None, prefix="", keep_vars=False):
keep_vars: Whether to keep variables as Variables instead of tensors
Returns:
- Dict containing the state dict with TransformerLens format keys
+ Direct calls return TransformerLens-format keys; recursive calls
+ return the supplied destination with standard module-tree keys.
"""
- if destination is not None:
- raw_state_dict = self.original_model.state_dict(
- destination=destination, prefix=prefix, keep_vars=keep_vars
+ if destination is not None or prefix:
+ return super().state_dict(
+ destination=destination,
+ prefix=prefix,
+ keep_vars=keep_vars,
)
- else:
- raw_state_dict = self.original_model.state_dict(prefix=prefix, keep_vars=keep_vars)
+
+ raw_state_dict = self.original_model.state_dict(keep_vars=keep_vars)
# Clean _original_component references and convert to TL format
# Also filter out nested HuggingFace components that are wrapped by bridge components
@@ -4648,9 +5498,39 @@ def state_dict(self, destination=None, prefix="", keep_vars=False):
return tl_state_dict
+ def _tl_key_to_actual_keys(self) -> dict[str, list[str]]:
+ """Inverse of the renaming state_dict() applies: map each TL-format key
+ back to every raw parameter/buffer path that represents it.
+
+ Mirrors the filtering and key-conversion in state_dict() exactly, except
+ it keeps every raw key for a given TL key instead of only the first-seen
+ one. Bridge components frequently expose the same underlying parameter
+ through more than one attribute path (e.g. GPT-2's split q/k/v weights
+ are views into the wrapped module's combined c_attn weight, reachable
+ both via a block-level shortcut and via the nested _original_component
+ chain) - all of those aliases must be written for the round trip to
+ actually change what forward() reads, not just what state_dict() shows.
+ """
+ mapping: dict[str, list[str]] = {}
+ for actual_key in self.original_model.state_dict():
+ if actual_key == "_original_component" or actual_key.startswith("_original_component."):
+ continue
+ clean_key = actual_key.replace("._original_component", "")
+ if not self._is_valid_bridge_path(clean_key):
+ continue
+ hf_key = self._normalize_bridge_key_to_hf(clean_key)
+ tl_key = self.adapter.convert_hf_key_to_tl_key(hf_key)
+ mapping.setdefault(tl_key, []).append(actual_key)
+ return mapping
+
def load_state_dict(self, state_dict, strict=True, assign=False):
"""Load state dict into the model, handling both clean keys and original keys with _original_component references.
+ Accepts three key formats: TL-format keys as emitted by state_dict()
+ (e.g. "blocks.0.attn.q.weight"), raw native parameter paths (e.g. for
+ ``boot_native`` / tracr-style loading), and raw paths with
+ "_original_component" segments stripped.
+
Args:
state_dict: Dictionary containing a whole state of the module
strict: Whether to strictly enforce that the keys in state_dict match the keys returned by this module's state_dict() function
@@ -4661,27 +5541,60 @@ def load_state_dict(self, state_dict, strict=True, assign=False):
"""
current_state_dict = self.original_model.state_dict()
clean_to_actual = {}
- actual_to_clean = {}
for actual_key in current_state_dict.keys():
if actual_key != "_original_component":
- clean_key = actual_key.replace("._original_component", "")
- clean_to_actual[clean_key] = actual_key
- actual_to_clean[actual_key] = clean_key
+ clean_to_actual[actual_key.replace("._original_component", "")] = actual_key
+
+ tl_to_actual = self._tl_key_to_actual_keys()
+
mapped_state_dict = {}
+ unexpected_keys = []
for input_key, value in state_dict.items():
if input_key in current_state_dict:
mapped_state_dict[input_key] = value
- else:
- if input_key in clean_to_actual:
- actual_key = clean_to_actual[input_key]
+ elif input_key in clean_to_actual:
+ mapped_state_dict[clean_to_actual[input_key]] = value
+ elif input_key in tl_to_actual:
+ for actual_key in tl_to_actual[input_key]:
mapped_state_dict[actual_key] = value
- else:
- mapped_state_dict[input_key] = value
- effective_strict = strict and len(mapped_state_dict) == len(current_state_dict)
- return self.original_model.load_state_dict(
- mapped_state_dict, strict=effective_strict, assign=assign
+ else:
+ unexpected_keys.append(input_key)
+
+ # A TL key's actual-key aliases share the same underlying storage (see
+ # _tl_key_to_actual_keys), so writing any one of them already updates
+ # what forward() reads for all of them. Treat the group as satisfied
+ # if any alias was written -- e.g. a caller supplying clean/raw keys
+ # (the branch above maps each clean key to exactly one actual key)
+ # shouldn't have the *other*, unwritten aliases reported as missing.
+ missing_keys = sorted(
+ actual_key
+ for actual_keys in tl_to_actual.values()
+ if not any(k in mapped_state_dict for k in actual_keys)
+ for actual_key in actual_keys
)
+ if strict and (missing_keys or unexpected_keys):
+ error_msgs = []
+ if unexpected_keys:
+ error_msgs.append(
+ "Unexpected key(s) in state_dict: "
+ + ", ".join(f'"{k}"' for k in sorted(unexpected_keys))
+ )
+ if missing_keys:
+ error_msgs.append(
+ "Missing key(s) in state_dict: " + ", ".join(f'"{k}"' for k in missing_keys)
+ )
+ raise RuntimeError(
+ "Error(s) in loading state_dict for {}:\n\t{}".format(
+ type(self.original_model).__name__, "\n\t".join(error_msgs)
+ )
+ )
+
+ result = self.original_model.load_state_dict(mapped_state_dict, strict=False, assign=assign)
+ if assign:
+ refresh_container_state_owners(self)
+ return type(result)(missing_keys=missing_keys, unexpected_keys=unexpected_keys)
+
def get_params(self):
"""Access to model parameters in the format expected by SVDInterpreter.
diff --git a/transformer_lens/model_bridge/component_setup.py b/transformer_lens/model_bridge/component_setup.py
index c628c643d5..b5ac1017cc 100644
--- a/transformer_lens/model_bridge/component_setup.py
+++ b/transformer_lens/model_bridge/component_setup.py
@@ -20,6 +20,63 @@
pass
+class _ContainerStateOwner(nn.Module):
+ """Registered view of state owned directly by an unwrapped container."""
+
+ def __init__(self, original_container: nn.Module) -> None:
+ super().__init__()
+ self.__dict__["_original_container"] = original_container
+
+ def _sync_original_container(self) -> None:
+ original_container = self.__dict__["_original_container"]
+ original_container._parameters.update(self._parameters)
+ original_container._buffers.update(self._buffers)
+
+ def _refresh_from_original_container(self) -> None:
+ original_container = self.__dict__["_original_container"]
+ for name in self._parameters:
+ self._parameters[name] = original_container._parameters[name]
+ for name in self._buffers:
+ self._buffers[name] = original_container._buffers[name]
+
+ def _apply(self, fn: Any, recurse: bool = True) -> "_ContainerStateOwner":
+ self._refresh_from_original_container()
+ super()._apply(fn, recurse=recurse)
+ self._sync_original_container()
+ return self
+
+ def _load_from_state_dict(
+ self,
+ state_dict: dict[str, Any],
+ prefix: str,
+ local_metadata: dict[str, Any],
+ strict: bool,
+ missing_keys: list[str],
+ unexpected_keys: list[str],
+ error_msgs: list[str],
+ ) -> None:
+ super()._load_from_state_dict(
+ state_dict,
+ prefix,
+ local_metadata,
+ strict,
+ missing_keys,
+ unexpected_keys,
+ error_msgs,
+ )
+ self._sync_original_container()
+
+
+def refresh_container_state_owners(bridge_module: nn.Module) -> None:
+ """Refresh registered container state from the original model tree."""
+ root_owner = bridge_module._modules.get("_container_state_owners")
+ if not isinstance(root_owner, _ContainerStateOwner):
+ return
+ for owner in root_owner.modules():
+ if isinstance(owner, _ContainerStateOwner):
+ owner._refresh_from_original_container()
+
+
def replace_remote_component(
replacement_component: nn.Module, remote_path: str, remote_model: RemoteModel
) -> None:
@@ -56,6 +113,69 @@ def set_original_components(
"""
component_mapping = architecture_adapter.get_component_mapping()
setup_components(component_mapping, bridge_module, architecture_adapter, original_model)
+ if isinstance(original_model, nn.Module):
+ _register_unowned_container_state(bridge_module, original_model)
+
+
+def _register_unowned_container_state(bridge_module: nn.Module, original_model: nn.Module) -> None:
+ """Make direct container parameters and buffers reachable through the Bridge tree."""
+ registered_parameter_ids = {
+ id(parameter)
+ for module in bridge_module.modules()
+ for parameter in module._parameters.values()
+ if parameter is not None
+ }
+ registered_buffer_ids = {
+ id(buffer)
+ for module in bridge_module.modules()
+ for buffer in module._buffers.values()
+ if buffer is not None
+ }
+ missing_parameters: list[tuple[str, nn.Module, str, nn.Parameter]] = []
+ missing_buffers: list[tuple[str, nn.Module, str, Any]] = []
+
+ for module_path, module in original_model.named_modules():
+ if any(child is not None for child in module._modules.values()):
+ for parameter_name, parameter in module._parameters.items():
+ if parameter is not None and id(parameter) not in registered_parameter_ids:
+ missing_parameters.append((module_path, module, parameter_name, parameter))
+ registered_parameter_ids.add(id(parameter))
+ for buffer_name, buffer in module._buffers.items():
+ if buffer is not None and id(buffer) not in registered_buffer_ids:
+ missing_buffers.append((module_path, module, buffer_name, buffer))
+ registered_buffer_ids.add(id(buffer))
+
+ if not missing_parameters and not missing_buffers:
+ return
+
+ owner_by_path: dict[str, _ContainerStateOwner] = {"": _ContainerStateOwner(original_model)}
+ root_owner = owner_by_path[""]
+ original_modules = dict(original_model.named_modules())
+
+ def get_owner(module_path: str) -> _ContainerStateOwner:
+ current_path = ""
+ current_owner = root_owner
+ for path_part in module_path.split(".") if module_path else ():
+ child_path = f"{current_path}.{path_part}" if current_path else path_part
+ if child_path not in owner_by_path:
+ child_owner = _ContainerStateOwner(original_modules[child_path])
+ current_owner.add_module(path_part, child_owner)
+ owner_by_path[child_path] = child_owner
+ current_owner = owner_by_path[child_path]
+ current_path = child_path
+ return current_owner
+
+ for module_path, _, parameter_name, parameter in missing_parameters:
+ get_owner(module_path).register_parameter(parameter_name, parameter)
+
+ for module_path, module, buffer_name, buffer in missing_buffers:
+ get_owner(module_path).register_buffer(
+ buffer_name,
+ buffer,
+ persistent=buffer_name not in module._non_persistent_buffers_set,
+ )
+
+ bridge_module.add_module("_container_state_owners", root_owner)
def setup_submodules(
@@ -286,6 +406,8 @@ def setup_blocks_bridge(
block_bridge.name = f"{blocks_template.name}.{i}"
block_bridge.set_original_component(original_block)
setup_submodules(block_bridge, architecture_adapter, original_block)
+ if hasattr(block_bridge, "_wire_ln1_module"):
+ block_bridge._wire_ln1_module()
bridged_blocks.append(block_bridge)
replace_remote_component(bridged_blocks, blocks_template.name, original_model)
return bridged_blocks
diff --git a/transformer_lens/model_bridge/generalized_components/alibi_joint_qkv_attention.py b/transformer_lens/model_bridge/generalized_components/alibi_joint_qkv_attention.py
index acb1c6a537..971d5fb0a1 100644
--- a/transformer_lens/model_bridge/generalized_components/alibi_joint_qkv_attention.py
+++ b/transformer_lens/model_bridge/generalized_components/alibi_joint_qkv_attention.py
@@ -118,6 +118,7 @@ def _reconstruct_attention(
# Add attention mask
attention_mask = kwargs.get("attention_mask", None)
if attention_mask is not None:
+ attention_mask = self._normalize_compatibility_mask_sentinel(attention_mask)
attn_scores = attn_scores + attention_mask[:, :, :, : attn_scores.shape[-1]]
attn_scores = self.hook_attn_scores(attn_scores)
diff --git a/transformer_lens/model_bridge/generalized_components/attention.py b/transformer_lens/model_bridge/generalized_components/attention.py
index 3fc9af1b69..2f072da52c 100644
--- a/transformer_lens/model_bridge/generalized_components/attention.py
+++ b/transformer_lens/model_bridge/generalized_components/attention.py
@@ -429,14 +429,19 @@ def _update_kv_cache(
present in kwargs, K and V are returned unchanged.
"""
past_key_values = kwargs.get("past_key_values", None)
+ if past_key_values is None:
+ # GPT-NeoX/GPT-J/Bloom/Falcon/MPT/CodeGen/GPTBigCode still name the
+ # cache `layer_past`; missing it leaves the cache empty, so every
+ # decode step attends to itself alone and generation ignores the prompt.
+ past_key_values = kwargs.get("layer_past", None)
if past_key_values is None:
return k, v
layer_idx = getattr(self, "_layer_idx", None)
if layer_idx is None:
logger.warning(
"%s: past_key_values provided but _layer_idx is None "
- "(HF component missing layer_idx attribute). "
- "KV cache update skipped — generation will be slow.",
+ "(HF component missing layer_idx attribute). KV cache update "
+ "skipped — cached generation will ignore earlier tokens.",
self.name,
)
return k, v
@@ -518,10 +523,25 @@ def _softmax_dropout_pattern(
attn_weights = torch.nn.functional.softmax(attn_scores, dim=-1)
if target_dtype is not None:
attn_weights = attn_weights.to(target_dtype)
+ attn_weights = self._scrub_compatibility_pattern_nans(attn_weights)
attn_weights = self._apply_attn_dropout(attn_weights)
attn_weights = self.hook_pattern(attn_weights)
return attn_weights
+ def _scrub_compatibility_pattern_nans(self, pattern: torch.Tensor) -> torch.Tensor:
+ """Match HookedTransformer for fully masked attention rows."""
+ if self.compatibility_mode:
+ pattern = torch.where(torch.isnan(pattern), torch.zeros_like(pattern), pattern)
+ return pattern
+
+ def _normalize_compatibility_mask_sentinel(self, attention_mask: torch.Tensor) -> torch.Tensor:
+ """Normalize additive mask sentinels before dtype conversion or addition."""
+ if self.compatibility_mode and attention_mask.is_floating_point():
+ attention_mask = attention_mask.masked_fill(
+ attention_mask <= torch.finfo(attention_mask.dtype).min, -torch.inf
+ )
+ return attention_mask
+
def _reshape_attn_output(
self,
attn_output: torch.Tensor,
@@ -558,6 +578,7 @@ def _apply_reconstruct_attention_mask(
if q_seq_len is None:
q_seq_len = seq_len
min_dtype = torch.finfo(attn_scores.dtype).min
+ mask_value = -torch.inf if self.compatibility_mode else min_dtype
use_direct_hf_mask = attention_mask is not None and attention_mask.ndim >= 4
# Bidirectional attention (encoders) and cross-attention have no causal
# structure, so only synthesize the triangular mask for causal self-attention.
@@ -569,29 +590,29 @@ def _apply_reconstruct_attention_mask(
q_seq_len, seq_len, device=attn_scores.device, dtype=torch.bool
)
causal_mask = torch.tril(causal_mask, diagonal=seq_len - q_seq_len)
- attn_scores = attn_scores.masked_fill(~causal_mask, min_dtype)
-
- if attention_mask is None:
- return attn_scores
-
- if attention_mask.shape[-1] != seq_len:
- attention_mask = attention_mask[..., :seq_len]
- if attention_mask.ndim >= 3 and attention_mask.shape[-2] != q_seq_len:
- # Extra query rows mean a full-sequence mask on a cached decode step,
- # where the live queries are the LAST rows; taking the first hands
- # every step position 0 (Baichuan-13B fuses ALiBi slopes in here).
- attention_mask = attention_mask[..., -q_seq_len:, :]
-
- if attention_mask.dtype == torch.bool:
- attention_mask = torch.where(
- attention_mask,
- torch.zeros((), dtype=attn_scores.dtype, device=attn_scores.device),
- torch.full((), min_dtype, dtype=attn_scores.dtype, device=attn_scores.device),
- )
- else:
- attention_mask = attention_mask.to(dtype=attn_scores.dtype)
+ attn_scores = attn_scores.masked_fill(~causal_mask, mask_value)
+
+ if attention_mask is not None:
+ if attention_mask.shape[-1] != seq_len:
+ attention_mask = attention_mask[..., :seq_len]
+ if attention_mask.ndim >= 3 and attention_mask.shape[-2] != q_seq_len:
+ # Extra query rows mean a full-sequence mask on a cached decode step,
+ # where the live queries are the LAST rows; taking the first hands
+ # every step position 0 (Baichuan-13B fuses ALiBi slopes in here).
+ attention_mask = attention_mask[..., -q_seq_len:, :]
+
+ if attention_mask.dtype == torch.bool:
+ attention_mask = torch.where(
+ attention_mask,
+ torch.zeros((), dtype=attn_scores.dtype, device=attn_scores.device),
+ torch.full((), mask_value, dtype=attn_scores.dtype, device=attn_scores.device),
+ )
+ else:
+ attention_mask = self._normalize_compatibility_mask_sentinel(attention_mask)
+ attention_mask = attention_mask.to(dtype=attn_scores.dtype)
+ attn_scores = attn_scores + attention_mask
- return attn_scores + attention_mask
+ return attn_scores
def _get_n_heads(self, use_kv: bool = False) -> int:
"""Resolve the number of attention heads from config.
@@ -792,42 +813,14 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
raise RuntimeError(
f"Original component not set for {self.name}. Call set_original_component() first."
)
- # Skip non-fp params: quantized weights (bnb uint8/int8, GPTQ/AWQ int32,
- # HQQ, torchao) are stored in integer dtypes and dequantized internally
- # during matmul. The compute dtype must come from a fp parameter; casting
- # fp inputs to an integer storage dtype destroys precision.
- target_dtype = None
- for p in self.original_component.parameters():
- if not p.dtype.is_floating_point:
- continue
- target_dtype = p.dtype
- break
if "query_input" in kwargs:
hooked = self.hook_in(kwargs["query_input"])
- if (
- target_dtype is not None
- and isinstance(hooked, torch.Tensor)
- and hooked.is_floating_point()
- ):
- hooked = hooked.to(dtype=target_dtype)
kwargs["query_input"] = hooked
elif "hidden_states" in kwargs:
hooked = self.hook_in(kwargs["hidden_states"])
- if (
- target_dtype is not None
- and isinstance(hooked, torch.Tensor)
- and hooked.is_floating_point()
- ):
- hooked = hooked.to(dtype=target_dtype)
kwargs["hidden_states"] = hooked
elif len(args) > 0 and isinstance(args[0], torch.Tensor):
hooked = self.hook_in(args[0])
- if (
- target_dtype is not None
- and isinstance(hooked, torch.Tensor)
- and hooked.is_floating_point()
- ):
- hooked = hooked.to(dtype=target_dtype)
args = (hooked,) + args[1:]
# try/finally so the captured tensor (and its autograd graph) is
# released even if original_component raises.
diff --git a/transformer_lens/model_bridge/generalized_components/base.py b/transformer_lens/model_bridge/generalized_components/base.py
index af8087282a..3d092489a6 100644
--- a/transformer_lens/model_bridge/generalized_components/base.py
+++ b/transformer_lens/model_bridge/generalized_components/base.py
@@ -320,16 +320,6 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
raise RuntimeError(
f"Original component not set for {self.name}. Call set_original_component() first."
)
- # Skip non-fp params: quantized weights (bnb uint8/int8, GPTQ/AWQ int32,
- # HQQ, torchao) are stored in integer dtypes and dequantized internally
- # during matmul. The compute dtype must come from a fp parameter; casting
- # fp inputs to an integer storage dtype destroys precision.
- target_dtype = None
- for p in original_component.parameters():
- if not p.dtype.is_floating_point:
- continue
- target_dtype = p.dtype
- break
input_arg_names = [
"input",
"hidden_states",
@@ -342,19 +332,11 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
for name in input_arg_names:
if name in kwargs:
hooked = self.hook_in(kwargs[name])
- if (
- target_dtype is not None
- and isinstance(hooked, torch.Tensor)
- and hooked.is_floating_point()
- ):
- hooked = hooked.to(dtype=target_dtype)
kwargs[name] = hooked
input_found = True
break
if not input_found and len(args) > 0 and isinstance(args[0], torch.Tensor):
hooked_input = self.hook_in(args[0])
- if target_dtype is not None and hooked_input.is_floating_point():
- hooked_input = hooked_input.to(dtype=target_dtype)
args = (hooked_input,) + args[1:]
input_found = True
output = original_component(*args, **kwargs)
diff --git a/transformer_lens/model_bridge/generalized_components/block.py b/transformer_lens/model_bridge/generalized_components/block.py
index 5806a146fa..fdb3970af6 100644
--- a/transformer_lens/model_bridge/generalized_components/block.py
+++ b/transformer_lens/model_bridge/generalized_components/block.py
@@ -119,12 +119,35 @@ def __init__(
# blocks) when use_hook_mlp_in is set. See #1317.
self.hook_mlp_in = HookPoint()
+ def _wire_ln1_module(self) -> None:
+ """Keep the raw ln1 execution reference outside the ownership tree."""
+ from transformer_lens.model_bridge.generalized_components.attention import (
+ AttentionBridge,
+ )
+
+ ln1 = self.submodules.get("ln1") if self.submodules else None
+ attn = self.submodules.get("attn") if self.submodules else None
+ if not isinstance(attn, AttentionBridge):
+ return
+
+ ln1_module = None
+ if (
+ ln1 is not None
+ and getattr(attn, "supports_split_qkv_fork", False)
+ and getattr(ln1, "original_component", None) is not None
+ ):
+ ln1_module = ln1.original_component
+
+ attn._modules.pop("_ln1_module", None)
+ object.__setattr__(attn, "_ln1_module", ln1_module)
+
def _maybe_wire_capture_hooks(self) -> None:
"""Install the block's capture hooks (split-qkv fork, hook_mlp_in).
Registered on the bridge submodule, not ``original_component`` — the
manual bridge forward never calls the raw module. Idempotent.
"""
+ self._wire_ln1_module()
if self._capture_hooks_wired:
return
from transformer_lens.model_bridge.generalized_components.attention import (
@@ -147,7 +170,6 @@ def _capture_pre_ln1(_module: torch.nn.Module, args: tuple) -> None:
handle = ln1.register_forward_pre_hook(_capture_pre_ln1)
self._capture_hook_handles.append(handle)
- attn._ln1_module = ln1.original_component
# hook_mlp_in must capture the MLP-branch entry point: ln2's input on
# pre-norm blocks, the MLP's own input on post-norm blocks (where ln2
@@ -190,6 +212,16 @@ def _read_use_hook_mlp_in(self) -> bool:
return bool(cfg.use_hook_mlp_in)
return self._use_hook_mlp_in
+ def _clear_attention_capture(self) -> None:
+ """Release the transient residual captured for attention input forks."""
+ from transformer_lens.model_bridge.generalized_components.attention import (
+ AttentionBridge,
+ )
+
+ attn = self.submodules.get("attn") if self.submodules else None
+ if isinstance(attn, AttentionBridge):
+ attn._captured_pre_ln_residual = None
+
def forward(self, *args: Any, **kwargs: Any) -> Any:
"""Forward pass through the block bridge.
@@ -209,6 +241,7 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
)
self._maybe_wire_capture_hooks()
+ self._clear_attention_capture()
self._check_stop_at_layer(*args, **kwargs)
args, kwargs = self._hook_input_hidden_states(args, kwargs)
@@ -216,7 +249,10 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
# This prevents errors when passing encoder-specific params to decoder-only models
filtered_kwargs = self._filter_kwargs_for_forward(kwargs, len(args))
- output = self.original_component(*args, **filtered_kwargs)
+ try:
+ output = self.original_component(*args, **filtered_kwargs)
+ finally:
+ self._clear_attention_capture()
force_tuple_for_bare_tensor = self._is_standalone_hidden_state_call(args, filtered_kwargs)
return self._apply_output_hook(
output, force_tuple_for_bare_tensor=force_tuple_for_bare_tensor
@@ -272,6 +308,13 @@ def _is_standalone_hidden_state_call(args: tuple, kwargs: dict) -> bool:
and isinstance(kwargs["hidden_states"], torch.Tensor)
)
+ def _extract_layer_idx(self) -> Optional[int]:
+ """Parse this block's layer index from its name (TL/GPT-2/LLaMA patterns)."""
+ if self.name is None:
+ return None
+ match = re.search(r"(?:^|\.)(?:blocks|h|layers)\.(\d+)", self.name)
+ return int(match.group(1)) if match else None
+
def _check_stop_at_layer(self, *args: Any, **kwargs: Any) -> None:
"""Check if execution should stop before this block. Raises StopAtLayerException.
@@ -281,11 +324,9 @@ def _check_stop_at_layer(self, *args: Any, **kwargs: Any) -> None:
if not (hasattr(self, "_stop_at_layer_idx") and self._stop_at_layer_idx is not None):
return
if self.name is not None:
- match = (
- re.search(r"blocks\.(\d+)", self.name)
- or re.search(r"\.h\.(\d+)", self.name)
- or re.search(r"\.layers\.(\d+)", self.name)
- )
+ # Anchored alternation: native models name blocks top-level
+ # ("layers.0"), which the old leading-dot pattern missed entirely.
+ match = re.search(r"(?:^|\.)(?:blocks|h|layers)\.(\d+)", self.name)
else:
match = None
if match:
diff --git a/transformer_lens/model_bridge/generalized_components/bloom_attention.py b/transformer_lens/model_bridge/generalized_components/bloom_attention.py
index cbbd290990..c8a148aed1 100644
--- a/transformer_lens/model_bridge/generalized_components/bloom_attention.py
+++ b/transformer_lens/model_bridge/generalized_components/bloom_attention.py
@@ -101,10 +101,15 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
# Apply input hook
hooked_input = self.hook_in(hidden_states)
- # Run through split Q/K/V projections (these fire hook_q, hook_k, hook_v)
- q_output = self.q(hooked_input)
- k_output = self.k(hooked_input)
- v_output = self.v(hooked_input)
+ # Run through split Q/K/V projections (these fire hook_q, hook_k, hook_v),
+ # via the per-head fork when use_split_qkv_input / use_attn_in is set so
+ # those gated hooks fire here as they do on every other joint-QKV bridge.
+ if self._is_split_qkv_fork_active():
+ q_output, k_output, v_output = self._split_forward_qkv(hooked_input)
+ else:
+ q_output = self.q(hooked_input)
+ k_output = self.k(hooked_input)
+ v_output = self.v(hooked_input)
# Reconstruct attention with ALiBi (fires hook_attn_scores, hook_pattern)
attn_output, attn_weights = self._reconstruct_attention(
@@ -199,7 +204,18 @@ def _reconstruct_attention(
attn_output = self._reshape_attn_output(
attn_output, batch_size, seq_len, num_heads, head_dim
)
- attn_output = self._apply_output_projection(attn_output)
+ if (
+ bool(getattr(self.config, "use_attn_result", False))
+ and hasattr(self, "o")
+ and self.o.original_component is not None
+ ):
+ # Fire hook_z on the flat pre-projection tensor first, so patches at
+ # hook_z reach the per-head computation (same order as the parent).
+ attn_output = self.o.hook_in(attn_output)
+ z_4d = attn_output.view(batch_size, seq_len, num_heads, head_dim)
+ attn_output = self._compute_per_head_result(z_4d, num_heads, head_dim)
+ else:
+ attn_output = self._apply_output_projection(attn_output)
return (attn_output, attn_weights)
diff --git a/transformer_lens/model_bridge/generalized_components/joint_gate_up_mlp.py b/transformer_lens/model_bridge/generalized_components/joint_gate_up_mlp.py
index 15539b38b4..7d40d57afd 100644
--- a/transformer_lens/model_bridge/generalized_components/joint_gate_up_mlp.py
+++ b/transformer_lens/model_bridge/generalized_components/joint_gate_up_mlp.py
@@ -65,6 +65,9 @@ def __init__(
self._activation_fn: Any = None
self._register_state_dict_hook(JointGateUpMLPBridge._filter_gate_up_state_dict)
+ self.register_load_state_dict_pre_hook(
+ JointGateUpMLPBridge._restore_filtered_gate_up_state_dict
+ )
@staticmethod
def _filter_gate_up_state_dict(
@@ -79,6 +82,29 @@ def _filter_gate_up_state_dict(
for k in keys_to_remove:
del state_dict[k]
+ @staticmethod
+ def _restore_filtered_gate_up_state_dict(
+ module: torch.nn.Module,
+ state_dict: Dict[str, Any],
+ prefix: str,
+ local_metadata: Dict[str, Any],
+ strict: bool,
+ missing_keys: list[str],
+ unexpected_keys: list[str],
+ error_msgs: list[str],
+ ) -> None:
+ """Insert current combined weights only to satisfy strict key matching.
+
+ Production checkpoints restore authoritative values through the unfiltered
+ Hugging Face ``_original_component`` path.
+ """
+ del local_metadata, strict, missing_keys, unexpected_keys, error_msgs
+ gate_up = module._modules.get("gate_up")
+ if gate_up is None:
+ return
+ for key, value in gate_up.state_dict(prefix=f"{prefix}gate_up.").items():
+ state_dict.setdefault(key, value)
+
def _default_split_gate_up(
self,
original_mlp_component: Any,
diff --git a/transformer_lens/model_bridge/generalized_components/joint_qkv_attention.py b/transformer_lens/model_bridge/generalized_components/joint_qkv_attention.py
index 438437474a..9e8b80d324 100644
--- a/transformer_lens/model_bridge/generalized_components/joint_qkv_attention.py
+++ b/transformer_lens/model_bridge/generalized_components/joint_qkv_attention.py
@@ -100,6 +100,9 @@ def __init__(
# Exclude stale qkv combined weights from state_dict after splitting.
self._register_state_dict_hook(JointQKVAttentionBridge._filter_qkv_state_dict)
+ self.register_load_state_dict_pre_hook(
+ JointQKVAttentionBridge._restore_filtered_qkv_state_dict
+ )
def __deepcopy__(self, memo):
"""Share split_qkv_matrix and config across clones instead of copying.
@@ -143,6 +146,29 @@ def _filter_qkv_state_dict(
for k in keys_to_remove:
del state_dict[k]
+ @staticmethod
+ def _restore_filtered_qkv_state_dict(
+ module: torch.nn.Module,
+ state_dict: Dict[str, Any],
+ prefix: str,
+ local_metadata: Dict[str, Any],
+ strict: bool,
+ missing_keys: list[str],
+ unexpected_keys: list[str],
+ error_msgs: list[str],
+ ) -> None:
+ """Insert current combined weights only to satisfy strict key matching.
+
+ Production checkpoints restore authoritative values through the unfiltered
+ Hugging Face ``_original_component`` path.
+ """
+ del local_metadata, strict, missing_keys, unexpected_keys, error_msgs
+ qkv = module._modules.get("qkv")
+ if qkv is None:
+ return
+ for key, value in qkv.state_dict(prefix=f"{prefix}qkv.").items():
+ state_dict.setdefault(key, value)
+
def _create_qkv_conversion_rule(self) -> BaseTensorConversion:
"""Create the appropriate conversion rule for the individual q, k, and v matrices.
diff --git a/transformer_lens/model_bridge/generalized_components/mla_attention.py b/transformer_lens/model_bridge/generalized_components/mla_attention.py
index 688312321d..c733b7731e 100644
--- a/transformer_lens/model_bridge/generalized_components/mla_attention.py
+++ b/transformer_lens/model_bridge/generalized_components/mla_attention.py
@@ -296,6 +296,7 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
attn_scores = torch.matmul(query_states, key_states.transpose(-2, -1)) * scaling
if attention_mask is not None:
+ attention_mask = self._normalize_compatibility_mask_sentinel(attention_mask)
attn_scores = attn_scores + attention_mask
attn_scores = self.hook_attn_scores(attn_scores)
diff --git a/transformer_lens/model_bridge/generalized_components/moe.py b/transformer_lens/model_bridge/generalized_components/moe.py
index 334b7dc513..19ba06ae68 100644
--- a/transformer_lens/model_bridge/generalized_components/moe.py
+++ b/transformer_lens/model_bridge/generalized_components/moe.py
@@ -281,28 +281,11 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
raise RuntimeError(
f"Original component not set for {self.name}. Call set_original_component() first."
)
- target_dtype = None
- try:
- target_dtype = next(self.original_component.parameters()).dtype
- except StopIteration:
- pass
if len(args) > 0:
hooked = self.hook_in(args[0])
- if (
- target_dtype is not None
- and isinstance(hooked, torch.Tensor)
- and hooked.is_floating_point()
- ):
- hooked = hooked.to(dtype=target_dtype)
args = (hooked,) + args[1:]
elif "hidden_states" in kwargs:
hooked = self.hook_in(kwargs["hidden_states"])
- if (
- target_dtype is not None
- and isinstance(hooked, torch.Tensor)
- and hooked.is_floating_point()
- ):
- hooked = hooked.to(dtype=target_dtype)
kwargs = {**kwargs, "hidden_states": hooked}
output = self.original_component(*args, **kwargs)
if isinstance(output, tuple):
diff --git a/transformer_lens/model_bridge/generalized_components/mpt_alibi_attention.py b/transformer_lens/model_bridge/generalized_components/mpt_alibi_attention.py
index 6163d21c9c..6ea12b4750 100644
--- a/transformer_lens/model_bridge/generalized_components/mpt_alibi_attention.py
+++ b/transformer_lens/model_bridge/generalized_components/mpt_alibi_attention.py
@@ -92,9 +92,10 @@ def _reconstruct_attention(
# MPT passes a bool 4D mask (True = masked), not an additive float mask.
attention_mask = kwargs.get("attention_mask", None)
if attention_mask is not None:
- attn_scores = attn_scores.masked_fill(
- attention_mask, torch.finfo(attn_scores.dtype).min
+ mask_value = (
+ -torch.inf if self.compatibility_mode else torch.finfo(attn_scores.dtype).min
)
+ attn_scores = attn_scores.masked_fill(attention_mask, mask_value)
attn_scores = self.hook_attn_scores(attn_scores)
diff --git a/transformer_lens/model_bridge/generalized_components/position_embeddings_attention.py b/transformer_lens/model_bridge/generalized_components/position_embeddings_attention.py
index f5dbbdaf2d..a9dffaf18d 100644
--- a/transformer_lens/model_bridge/generalized_components/position_embeddings_attention.py
+++ b/transformer_lens/model_bridge/generalized_components/position_embeddings_attention.py
@@ -348,20 +348,6 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
# Apply input hook
hidden_states = self.hook_in(hidden_states)
- # Match dtype of HF module. Skip non-fp params: quantized weights (bnb
- # uint8/int8, GPTQ/AWQ int32, HQQ, torchao) are stored in integer dtypes
- # and dequantized internally during matmul. The compute dtype must come
- # from a fp parameter; casting fp inputs to an integer storage dtype
- # destroys precision.
- target_dtype = None
- for p in hf_attn.parameters():
- if not p.dtype.is_floating_point:
- continue
- target_dtype = p.dtype
- break
- if target_dtype is not None and hidden_states.is_floating_point():
- hidden_states = hidden_states.to(dtype=target_dtype)
-
input_shape = hidden_states.shape[:-1]
head_dim = hf_attn.head_dim
hidden_shape = (*input_shape, -1, head_dim)
@@ -622,6 +608,7 @@ def forward(self, *args: Any, **kwargs: Any) -> Any:
attn_weights = torch.nn.functional.softmax(attn_scores, dim=-1, dtype=torch.float32).to(
query_states.dtype
)
+ attn_weights = self._scrub_compatibility_pattern_nans(attn_weights)
# --- Dropout ---
dropout_rate = getattr(hf_attn, "attention_dropout", 0.0)
diff --git a/transformer_lens/model_bridge/generalized_components/unembedding.py b/transformer_lens/model_bridge/generalized_components/unembedding.py
index ac7ab7e740..7c43e1bd9d 100644
--- a/transformer_lens/model_bridge/generalized_components/unembedding.py
+++ b/transformer_lens/model_bridge/generalized_components/unembedding.py
@@ -86,18 +86,7 @@ def forward(self, hidden_states: torch.Tensor, **kwargs: Any) -> torch.Tensor:
raise RuntimeError(
f"Original component not set for {self.name}. Call set_original_component() first."
)
- target_dtype = None
- try:
- target_dtype = next(self.original_component.parameters()).dtype
- except StopIteration:
- pass
hidden_states = self.hook_in(hidden_states)
- if (
- target_dtype is not None
- and isinstance(hidden_states, torch.Tensor)
- and hidden_states.is_floating_point()
- ):
- hidden_states = hidden_states.to(dtype=target_dtype)
output = self.original_component(hidden_states, **kwargs)
output = self.hook_out(output)
diff --git a/transformer_lens/model_bridge/get_params_util.py b/transformer_lens/model_bridge/get_params_util.py
index a1a242ce7c..05330cedd8 100644
--- a/transformer_lens/model_bridge/get_params_util.py
+++ b/transformer_lens/model_bridge/get_params_util.py
@@ -1,48 +1,50 @@
"""Utility function for getting model parameters in TransformerLens format."""
import logging
-from typing import Dict
+from typing import Dict, Optional
import torch
logger = logging.getLogger(__name__)
-def _get_n_kv_heads(cfg) -> int:
- """Resolve the number of key/value heads, falling back to n_heads."""
- if hasattr(cfg, "n_key_value_heads") and isinstance(cfg.n_key_value_heads, int):
- return cfg.n_key_value_heads
- return cfg.n_heads
+def _tensor_attr(obj, *names: str) -> Optional[torch.Tensor]:
+ """First attribute of ``obj`` among ``names`` that is an actual tensor, else None.
+ NotImplementedError counts as absent: MLA attention raises it from W_Q/W_K/W_V/W_O
+ (compressed projections have no standard per-head form).
+ """
+ for name in names:
+ try:
+ value = getattr(obj, name)
+ except (AttributeError, TypeError, NotImplementedError):
+ continue
+ if isinstance(value, torch.Tensor):
+ return value
+ return None
-def _reshape_kv_weight(weight: torch.Tensor, cfg, device, dtype) -> torch.Tensor:
- """Reshape a K or V weight matrix to (n_heads, d_model, d_head)."""
- d_head = cfg.d_model // cfg.n_heads
- if weight.shape == (cfg.d_model, cfg.d_model):
- return weight.reshape(cfg.n_heads, cfg.d_model, d_head)
- if weight.shape == (cfg.d_head, cfg.d_model) or weight.shape == (
- cfg.d_model // cfg.n_heads,
- cfg.d_model,
- ):
- return weight.transpose(0, 1).unsqueeze(0).expand(cfg.n_heads, -1, -1)
- if weight.numel() == cfg.n_heads * cfg.d_model * cfg.d_head:
- return weight.view(cfg.n_heads, cfg.d_model, cfg.d_head)
- return torch.zeros(cfg.n_heads, cfg.d_model, cfg.d_head, device=device, dtype=dtype)
+def get_bridge_params(bridge) -> Dict[str, torch.Tensor]:
+ """Model parameters in SVDInterpreter format.
-def _get_or_create_bias(bias, n_heads: int, d_head: int, device, dtype) -> torch.Tensor:
- """Reshape existing bias to (n_heads, d_head), or create zeros if None."""
- if bias is not None:
- return bias.reshape(n_heads, -1)
- return torch.zeros(n_heads, d_head, device=device, dtype=dtype)
+ Reads the bridge components' TL-layout weight properties (``W_Q``,
+ ``W_in``, ...), which already account for layout conversion and weight
+ processing. For missing weights, returns zero tensors of appropriate shape
+ instead of raising exceptions. Skips attn keys for non-attention layers.
+ LayerNorm params (``blocks.{i}.ln1.w`` etc.) are included when the modules
+ still carry them (i.e. before folding) so consumers can detect fold state.
+ Returns:
+ dict: Dictionary of parameter tensors with TransformerLens naming convention
-def get_bridge_params(bridge) -> Dict[str, torch.Tensor]:
- """Model parameters in SVDInterpreter format. Skips attn keys for non-attention layers."""
- params_dict = {}
+ Raises:
+ ValueError: If configuration is inconsistent (e.g., cfg.n_layers != len(blocks))
+ """
+ cfg = bridge.cfg
+ params_dict: Dict[str, torch.Tensor] = {}
def _get_device_dtype():
"""Infer device/dtype from the first available model parameter."""
- device = getattr(bridge.cfg, "device", None) or torch.device("cpu")
+ device = getattr(cfg, "device", None) or torch.device("cpu")
dtype = torch.float32
try:
first_param = next(bridge.parameters())
@@ -52,24 +54,21 @@ def _get_device_dtype():
pass
return (device, dtype)
- try:
- params_dict["embed.W_E"] = bridge.embed.weight
- except AttributeError:
- device, dtype = _get_device_dtype()
- params_dict["embed.W_E"] = torch.zeros(
- bridge.cfg.d_vocab, bridge.cfg.d_model, device=device, dtype=dtype
- )
- try:
- params_dict["pos_embed.W_pos"] = bridge.pos_embed.weight
- except AttributeError:
+ def _zeros(*shape) -> torch.Tensor:
device, dtype = _get_device_dtype()
- params_dict["pos_embed.W_pos"] = torch.zeros(
- bridge.cfg.n_ctx, bridge.cfg.d_model, device=device, dtype=dtype
- )
- for layer_idx in range(bridge.cfg.n_layers):
+ return torch.zeros(*shape, device=device, dtype=dtype)
+
+ embed = _tensor_attr(getattr(bridge, "embed", None), "W_E", "weight")
+ params_dict["embed.W_E"] = embed if embed is not None else _zeros(cfg.d_vocab, cfg.d_model)
+
+ pos = _tensor_attr(getattr(bridge, "pos_embed", None), "W_pos", "weight")
+ params_dict["pos_embed.W_pos"] = pos if pos is not None else _zeros(cfg.n_ctx, cfg.d_model)
+
+ for layer_idx in range(cfg.n_layers):
if layer_idx >= len(bridge.blocks):
raise ValueError(
- f"Configuration mismatch: cfg.n_layers={bridge.cfg.n_layers} but only {len(bridge.blocks)} blocks found. Layer {layer_idx} does not exist."
+ f"Configuration mismatch: cfg.n_layers={cfg.n_layers} but only "
+ f"{len(bridge.blocks)} blocks found. Layer {layer_idx} does not exist."
)
block = bridge.blocks[layer_idx]
@@ -79,128 +78,120 @@ def _get_device_dtype():
except (TypeError, AttributeError):
has_attn = hasattr(block, "attn") # Mock fallback
if has_attn:
- try:
- w_q = block.attn.q.weight
- w_k = block.attn.k.weight
- w_v = block.attn.v.weight
- w_o = block.attn.o.weight
- if w_q.shape == (bridge.cfg.d_model, bridge.cfg.d_model):
- d_head = bridge.cfg.d_model // bridge.cfg.n_heads
- w_q = w_q.reshape(bridge.cfg.n_heads, bridge.cfg.d_model, d_head)
- w_o = w_o.reshape(bridge.cfg.n_heads, d_head, bridge.cfg.d_model)
- device, dtype = _get_device_dtype()
- w_k = _reshape_kv_weight(w_k, bridge.cfg, device, dtype)
- w_v = _reshape_kv_weight(w_v, bridge.cfg, device, dtype)
+ attn = block.attn
+ w_q = _tensor_attr(attn, "W_Q")
+ w_k = _tensor_attr(attn, "W_K")
+ w_v = _tensor_attr(attn, "W_V")
+ w_o = _tensor_attr(attn, "W_O")
+ if w_q is None or w_k is None or w_v is None or w_o is None:
+ logger.debug(
+ "Block %d has 'attn' but no TL-layout W_Q/W_K/W_V/W_O properties — "
+ "skipping attention weights for this layer",
+ layer_idx,
+ )
+ else:
+ # GQA: expand grouped K/V (and their biases below) to n_heads so
+ # per-head pairings like SVDInterpreter's OV = W_V[h] @ W_O[h]
+ # line up — the legacy HT convention repeat_interleaved these.
+ n_kv_heads = w_k.shape[0]
+ if w_k.ndim == 3 and 0 < n_kv_heads < cfg.n_heads:
+ if cfg.n_heads % n_kv_heads != 0:
+ raise ValueError(
+ f"blocks.{layer_idx}.attn: n_heads ({cfg.n_heads}) is not "
+ f"divisible by n_kv_heads ({n_kv_heads}); cannot expand "
+ "grouped K/V to per-query heads."
+ )
+ repeats = cfg.n_heads // n_kv_heads
+ w_k = torch.repeat_interleave(w_k, repeats, dim=0)
+ w_v = torch.repeat_interleave(w_v, repeats, dim=0)
params_dict[f"blocks.{layer_idx}.attn.W_Q"] = w_q
params_dict[f"blocks.{layer_idx}.attn.W_K"] = w_k
params_dict[f"blocks.{layer_idx}.attn.W_V"] = w_v
params_dict[f"blocks.{layer_idx}.attn.W_O"] = w_o
- device, dtype = _get_device_dtype()
- n_kv_heads = _get_n_kv_heads(bridge.cfg)
- params_dict[f"blocks.{layer_idx}.attn.b_Q"] = _get_or_create_bias(
- block.attn.q.bias, bridge.cfg.n_heads, bridge.cfg.d_head, device, dtype
- )
- params_dict[f"blocks.{layer_idx}.attn.b_K"] = _get_or_create_bias(
- block.attn.k.bias, n_kv_heads, bridge.cfg.d_head, device, dtype
+ for bias_name in ("b_Q", "b_K", "b_V"):
+ bias = _tensor_attr(attn, bias_name)
+ if bias is None:
+ bias = _zeros(cfg.n_heads, cfg.d_head)
+ elif bias.ndim == 2 and 0 < bias.shape[0] < cfg.n_heads:
+ bias = torch.repeat_interleave(bias, cfg.n_heads // bias.shape[0], dim=0)
+ params_dict[f"blocks.{layer_idx}.attn.{bias_name}"] = bias
+ b_O = _tensor_attr(attn, "b_O")
+ params_dict[f"blocks.{layer_idx}.attn.b_O"] = (
+ b_O if b_O is not None else _zeros(cfg.d_model)
)
- params_dict[f"blocks.{layer_idx}.attn.b_V"] = _get_or_create_bias(
- block.attn.v.bias, n_kv_heads, bridge.cfg.d_head, device, dtype
- )
- if block.attn.o.bias is not None:
- params_dict[f"blocks.{layer_idx}.attn.b_O"] = block.attn.o.bias
- else:
- device, dtype = _get_device_dtype()
- params_dict[f"blocks.{layer_idx}.attn.b_O"] = torch.zeros(
- bridge.cfg.d_model, device=device, dtype=dtype
- )
- except AttributeError as e:
- logger.debug(
- "Block %d has 'attn' in _modules but attention params could not "
- "be extracted (missing q/k/v/o?): %s — skipping attention weights "
- "for this layer",
+
+ d_mlp = cfg.d_mlp if cfg.d_mlp is not None else 4 * cfg.d_model
+ mlp = getattr(block, "mlp", None)
+ w_in = _tensor_attr(mlp, "W_in")
+ if w_in is None:
+ if mlp is not None:
+ # Zero-filling a real MLP silently yields wrong numbers downstream
+ # (SVD/weight analyses decompose zeros). Say so — the fill stays for
+ # architectures that genuinely have no MLP under this name.
+ logger.warning(
+ "Block %d MLP weights could not be extracted — emitting ZEROS "
+ "for blocks.%d.mlp.W_in/W_out/b_in/b_out. Any weight-space "
+ "analysis of this layer will be meaningless.",
+ layer_idx,
layer_idx,
- e,
- )
- try:
- # Dense layers of an interleaved MoE stack keep their projections
- # under dense_* — `gate` there is the sparse layers' ROUTER, so the
- # standard names would either miss the weights (silently zero-filling
- # a real dense MLP) or read the router as a gate projection.
- # `is True`, not truthiness: auto-vivifying stand-ins (Mock blocks in
- # this module's own tests) return a truthy object for any attribute
- # and would take the dense branch with non-tensor projections.
- if getattr(block.mlp, "bound_dense", False) is True:
- mlp_in = getattr(block.mlp, "dense_in", None)
- mlp_out = getattr(block.mlp, "dense_out", None)
- mlp_gate = getattr(block.mlp, "dense_gate", None)
- else:
- mlp_in = getattr(block.mlp, "in", None) or getattr(block.mlp, "input", None)
- mlp_out = getattr(block.mlp, "out", None)
- mlp_gate = getattr(block.mlp, "gate", None)
- if mlp_in is None:
- raise AttributeError("MLP has no 'in' or 'input' attribute")
- # Use normalized accessors for consistent TL orientation
- params_dict[f"blocks.{layer_idx}.mlp.W_in"] = block.mlp.W_in
- params_dict[f"blocks.{layer_idx}.mlp.W_out"] = block.mlp.W_out
- mlp_in_bias = mlp_in.bias
- if mlp_in_bias is not None:
- params_dict[f"blocks.{layer_idx}.mlp.b_in"] = mlp_in_bias
- else:
- device, dtype = _get_device_dtype()
- d_mlp = bridge.cfg.d_mlp if bridge.cfg.d_mlp is not None else 4 * bridge.cfg.d_model
- params_dict[f"blocks.{layer_idx}.mlp.b_in"] = torch.zeros(
- d_mlp, device=device, dtype=dtype
- )
- mlp_out_bias = mlp_out.bias if mlp_out is not None else None
- if mlp_out_bias is not None:
- params_dict[f"blocks.{layer_idx}.mlp.b_out"] = mlp_out_bias
- else:
- device, dtype = _get_device_dtype()
- params_dict[f"blocks.{layer_idx}.mlp.b_out"] = torch.zeros(
- bridge.cfg.d_model, device=device, dtype=dtype
)
- if mlp_gate is not None and hasattr(mlp_gate, "weight"):
- w_gate = block.mlp.W_gate
- if w_gate is not None:
- params_dict[f"blocks.{layer_idx}.mlp.W_gate"] = w_gate
- if getattr(mlp_gate, "bias", None) is not None:
- params_dict[f"blocks.{layer_idx}.mlp.b_gate"] = mlp_gate.bias
- except AttributeError as e:
- # Zero-filling a real MLP silently yields wrong numbers downstream
- # (SVD/weight analyses decompose zeros). Say so — the fill stays for
- # architectures that genuinely have no MLP under this name.
- logger.warning(
- "Block %d MLP weights could not be extracted (%s) — emitting "
- "ZEROS for blocks.%d.mlp.W_in/W_out/b_in/b_out. Any weight-space "
- "analysis of this layer will be meaningless.",
- layer_idx,
- e,
- layer_idx,
- )
- device, dtype = _get_device_dtype()
- d_mlp = bridge.cfg.d_mlp if bridge.cfg.d_mlp is not None else 4 * bridge.cfg.d_model
- params_dict[f"blocks.{layer_idx}.mlp.W_in"] = torch.zeros(
- bridge.cfg.d_model, d_mlp, device=device, dtype=dtype
+ params_dict[f"blocks.{layer_idx}.mlp.W_in"] = _zeros(cfg.d_model, d_mlp)
+ params_dict[f"blocks.{layer_idx}.mlp.W_out"] = _zeros(d_mlp, cfg.d_model)
+ params_dict[f"blocks.{layer_idx}.mlp.b_in"] = _zeros(d_mlp)
+ params_dict[f"blocks.{layer_idx}.mlp.b_out"] = _zeros(cfg.d_model)
+ else:
+ params_dict[f"blocks.{layer_idx}.mlp.W_in"] = w_in
+ w_out = _tensor_attr(mlp, "W_out")
+ params_dict[f"blocks.{layer_idx}.mlp.W_out"] = (
+ w_out if w_out is not None else _zeros(d_mlp, cfg.d_model)
)
- params_dict[f"blocks.{layer_idx}.mlp.W_out"] = torch.zeros(
- d_mlp, bridge.cfg.d_model, device=device, dtype=dtype
+ b_in = _tensor_attr(mlp, "b_in")
+ params_dict[f"blocks.{layer_idx}.mlp.b_in"] = (
+ b_in if b_in is not None else _zeros(d_mlp)
)
- params_dict[f"blocks.{layer_idx}.mlp.b_in"] = torch.zeros(
- d_mlp, device=device, dtype=dtype
+ b_out = _tensor_attr(mlp, "b_out")
+ params_dict[f"blocks.{layer_idx}.mlp.b_out"] = (
+ b_out if b_out is not None else _zeros(cfg.d_model)
)
- params_dict[f"blocks.{layer_idx}.mlp.b_out"] = torch.zeros(
- bridge.cfg.d_model, device=device, dtype=dtype
- )
- try:
- params_dict["unembed.W_U"] = bridge.unembed.weight.T
- except AttributeError:
- device, dtype = _get_device_dtype()
- params_dict["unembed.W_U"] = torch.zeros(
- bridge.cfg.d_model, bridge.cfg.d_vocab, device=device, dtype=dtype
- )
- try:
- params_dict["unembed.b_U"] = bridge.unembed.b_U
- except AttributeError:
- device, dtype = _get_device_dtype()
- params_dict["unembed.b_U"] = torch.zeros(bridge.cfg.d_vocab, device=device, dtype=dtype)
+ w_gate = _tensor_attr(mlp, "W_gate")
+ # Raw-attribute fallback is for plain gated MLPs only: `gate` on an
+ # interleaved-MoE component (anything exposing bound_dense) is the
+ # sparse layers' ROUTER, never a gate projection.
+ is_moe = getattr(type(mlp), "bound_dense", None) is not None
+ if w_gate is None and not is_moe:
+ w_gate = _tensor_attr(getattr(mlp, "gate", None), "weight")
+ if w_gate is not None:
+ params_dict[f"blocks.{layer_idx}.mlp.W_gate"] = w_gate
+ b_gate = _tensor_attr(mlp, "b_gate")
+ if b_gate is None and not is_moe:
+ b_gate = _tensor_attr(getattr(mlp, "gate", None), "bias")
+ if b_gate is not None:
+ params_dict[f"blocks.{layer_idx}.mlp.b_gate"] = b_gate
+
+ # LN params (present pre-folding; folded models carry identities or none).
+ for ln_name in ("ln1", "ln2"):
+ ln = getattr(block, ln_name, None)
+ ln_w = _tensor_attr(ln, "w", "weight")
+ if ln_w is not None:
+ params_dict[f"blocks.{layer_idx}.{ln_name}.w"] = ln_w
+ ln_b = _tensor_attr(ln, "b", "bias")
+ if ln_b is not None:
+ params_dict[f"blocks.{layer_idx}.{ln_name}.b"] = ln_b
+
+ ln_final_w = _tensor_attr(getattr(bridge, "ln_final", None), "w", "weight")
+ if ln_final_w is not None:
+ params_dict["ln_final.w"] = ln_final_w
+ ln_final_b = _tensor_attr(getattr(bridge, "ln_final", None), "b", "bias")
+ if ln_final_b is not None:
+ params_dict["ln_final.b"] = ln_final_b
+
+ unembed = getattr(bridge, "unembed", None)
+ w_u = _tensor_attr(unembed, "W_U")
+ if w_u is None:
+ raw = _tensor_attr(unembed, "weight")
+ w_u = raw.T if raw is not None else _zeros(cfg.d_model, cfg.d_vocab)
+ params_dict["unembed.W_U"] = w_u
+ b_u = _tensor_attr(unembed, "b_U")
+ params_dict["unembed.b_U"] = b_u if b_u is not None else _zeros(cfg.d_vocab)
+
return params_dict
diff --git a/transformer_lens/model_bridge/sources/native/init.py b/transformer_lens/model_bridge/sources/native/init.py
index 229ac7e27f..bc8fdd31e1 100644
--- a/transformer_lens/model_bridge/sources/native/init.py
+++ b/transformer_lens/model_bridge/sources/native/init.py
@@ -29,37 +29,60 @@
)
# Residual-scaled output is gpt2-specific; other modes treat every weight the
-# same. Each entry takes ``(tensor, generator)`` to thread the scoped Generator.
-_NonResidualInit = Callable[[torch.Tensor, Optional[torch.Generator]], torch.Tensor]
+# same. Each entry takes ``(tensor, generator, gain)`` — gain honors
+# ``cfg.initializer_range`` like the legacy init did (which passed it as the
+# xavier/kaiming gain); kaiming has no gain kwarg, so scale after.
+_NonResidualInit = Callable[[torch.Tensor, Optional[torch.Generator], float], torch.Tensor]
_NON_RESIDUAL_MODES: dict[str, _NonResidualInit] = {
- "xavier_uniform": lambda t, g: nn.init.xavier_uniform_(t, generator=g),
- "xavier_normal": lambda t, g: nn.init.xavier_normal_(t, generator=g),
- "kaiming_uniform": lambda t, g: nn.init.kaiming_uniform_(t, nonlinearity="relu", generator=g),
- "kaiming_normal": lambda t, g: nn.init.kaiming_normal_(t, nonlinearity="relu", generator=g),
+ "xavier_uniform": lambda t, g, gain: nn.init.xavier_uniform_(t, gain=gain, generator=g),
+ "xavier_normal": lambda t, g, gain: nn.init.xavier_normal_(t, gain=gain, generator=g),
+ "kaiming_uniform": lambda t, g, gain: nn.init.kaiming_uniform_(
+ t, nonlinearity="relu", generator=g
+ ).mul_(gain),
+ "kaiming_normal": lambda t, g, gain: nn.init.kaiming_normal_(
+ t, nonlinearity="relu", generator=g
+ ).mul_(gain),
}
_SUPPORTED_MODES = frozenset({"gpt2", *_NON_RESIDUAL_MODES})
+def _unwrap_component(module: nn.Module) -> nn.Module:
+ """Return the native module stored behind a bridge wrapper, if present."""
+ original = getattr(module, "original_component", None)
+ return original if isinstance(original, nn.Module) else module
+
+
def initialize_native_model(
model: NativeModel, cfg: TransformerBridgeConfig, seed: int | None = None
) -> None:
"""Initialize ``model`` weights in-place. Honors ``cfg.init_mode`` and ``cfg.seed``."""
effective_seed = seed if seed is not None else cfg.seed
- # Scoped generator on the model's device — None falls back to the global RNG.
- try:
- gen_device = next(model.parameters()).device
- except StopIteration:
- gen_device = torch.device("cpu")
+ # Always generate on CPU/fp32 and copy into the parameter: boot initializes
+ # before .to(device)/.to(dtype) while init_weights() runs after, and a
+ # generator seeded on the live parameter device produces a different stream
+ # — the same seed must reproduce the same weights either way.
generator: Optional[torch.Generator]
if effective_seed is not None:
- g = torch.Generator(device=gen_device)
+ g = torch.Generator()
g.manual_seed(effective_seed)
generator = g
else:
generator = None
+ def _staged(
+ fn: Callable[[torch.Tensor], torch.Tensor],
+ ) -> Callable[[torch.Tensor], torch.Tensor]:
+ def apply(t: torch.Tensor) -> torch.Tensor:
+ staging = torch.empty(t.shape, dtype=torch.float32)
+ fn(staging)
+ with torch.no_grad():
+ t.copy_(staging)
+ return t
+
+ return apply
+
init_mode = (cfg.init_mode or "gpt2").lower()
if init_mode not in _SUPPORTED_MODES:
raise NotImplementedError(
@@ -70,7 +93,18 @@ def initialize_native_model(
weight_init: Callable[[torch.Tensor], torch.Tensor]
output_init: Callable[[torch.Tensor], torch.Tensor]
if init_mode == "gpt2":
- std = cfg.initializer_range if cfg.initializer_range > 0 else 0.02
+ # Default matches the legacy TL scheme: N(0, 0.64/d_model), i.e.
+ # std = 0.8/sqrt(d_model), not GPT-2's paper 0.02 — toy-model training
+ # dynamics (e.g. the grokking demo) depend on this scale.
+ std = cfg.initializer_range if cfg.initializer_range > 0 else 0.8 / math.sqrt(cfg.d_model)
+
+ # NOTE: this residual output scaling (1/sqrt(2*n_layers), applied only
+ # to output projections below) is NOT present in HookedTransformer's
+ # _init_weights_gpt2 (see transformer_lens/HookedTransformer.py).
+ # Intentional delta for NativeModel: kept because it follows the
+ # original GPT-2 paper's residual-scaling convention and improves
+ # training stability at init for deeper models. Flagged in issue #1568
+ # as a maintainer call; kept + documented rather than removed.
residual_scale = 1.0 / math.sqrt(2 * cfg.n_layers)
weight_init = lambda t: nn.init.normal_(
t, mean=0.0, std=std, generator=generator
@@ -80,22 +114,35 @@ def initialize_native_model(
)
else:
fn = _NON_RESIDUAL_MODES[init_mode]
- weight_init = lambda t: fn(t, generator) # noqa: E731
+ # Honor an explicitly-set initializer_range as the gain (legacy
+ # behavior); the sentinel/default keeps plain xavier/kaiming scaling.
+ gain = cfg.initializer_range if cfg.initializer_range > 0 else 1.0
+ weight_init = lambda t: fn(t, generator, gain) # noqa: E731
output_init = weight_init
- weight_init(model.tok_embed.weight)
+ weight_init = _staged(weight_init)
+ output_init = _staged(output_init)
+
+ tok_embed = cast(nn.Embedding, _unwrap_component(model.tok_embed))
+ weight_init(tok_embed.weight)
if model.pos is not None:
- weight_init(model.pos.weight)
+ pos = cast(nn.Embedding, _unwrap_component(model.pos))
+ weight_init(pos.weight)
# Rotary has only registered buffers (cos/sin), no parameters to init.
for block in model.layers:
- _init_block(block, weight_init=weight_init, output_init=output_init)
+ native_block = cast(NativeBlock, _unwrap_component(block))
+ _init_block(native_block, weight_init=weight_init, output_init=output_init)
_init_norm(model.ln_out)
- weight_init(model.head.weight)
+ head = cast(nn.Linear, _unwrap_component(model.head))
+ weight_init(head.weight)
+ if head.bias is not None:
+ nn.init.zeros_(head.bias)
def _init_norm(norm: nn.Module) -> None:
+ norm = _unwrap_component(norm)
if isinstance(norm, NativeRMSNorm):
nn.init.ones_(norm.weight)
elif isinstance(norm, nn.LayerNorm):
@@ -114,13 +161,19 @@ def _init_block(
output_init: Callable[[torch.Tensor], torch.Tensor],
) -> None:
_init_norm(block.ln1)
- _init_attention(block.attn, weight_init=weight_init, output_init=output_init)
+ attn = cast(NativeAttention, _unwrap_component(block.attn))
+ _init_attention(attn, weight_init=weight_init, output_init=output_init)
if not block.cfg.attn_only:
_init_norm(block.ln2)
- if isinstance(block.mlp, NativeGatedMLP):
- _init_gated_mlp(block.mlp, weight_init=weight_init, output_init=output_init)
+ mlp = _unwrap_component(block.mlp)
+ if isinstance(mlp, NativeGatedMLP):
+ _init_gated_mlp(mlp, weight_init=weight_init, output_init=output_init)
else:
- _init_mlp(block.mlp, weight_init=weight_init, output_init=output_init)
+ _init_mlp(
+ cast(NativeMLP, mlp),
+ weight_init=weight_init,
+ output_init=output_init,
+ )
def _init_attention(
@@ -129,13 +182,15 @@ def _init_attention(
weight_init: Callable[[torch.Tensor], torch.Tensor],
output_init: Callable[[torch.Tensor], torch.Tensor],
) -> None:
- for linear in (attn.q, attn.k, attn.v):
+ for component in (attn.q, attn.k, attn.v):
+ linear = cast(nn.Linear, _unwrap_component(component))
weight_init(linear.weight)
if linear.bias is not None:
nn.init.zeros_(linear.bias)
- output_init(attn.o.weight)
- if attn.o.bias is not None:
- nn.init.zeros_(attn.o.bias)
+ output = cast(nn.Linear, _unwrap_component(attn.o))
+ output_init(output.weight)
+ if output.bias is not None:
+ nn.init.zeros_(output.bias)
def _init_mlp(
@@ -144,10 +199,12 @@ def _init_mlp(
weight_init: Callable[[torch.Tensor], torch.Tensor],
output_init: Callable[[torch.Tensor], torch.Tensor],
) -> None:
- weight_init(mlp.fc_in.weight)
- nn.init.zeros_(mlp.fc_in.bias)
- output_init(mlp.fc_out.weight)
- nn.init.zeros_(mlp.fc_out.bias)
+ fc_in = cast(nn.Linear, _unwrap_component(mlp.fc_in))
+ fc_out = cast(nn.Linear, _unwrap_component(mlp.fc_out))
+ weight_init(fc_in.weight)
+ nn.init.zeros_(fc_in.bias)
+ output_init(fc_out.weight)
+ nn.init.zeros_(fc_out.bias)
def _init_gated_mlp(
@@ -156,8 +213,10 @@ def _init_gated_mlp(
weight_init: Callable[[torch.Tensor], torch.Tensor],
output_init: Callable[[torch.Tensor], torch.Tensor],
) -> None:
- weight_init(mlp.gate.weight)
+ gate = cast(nn.Linear, _unwrap_component(mlp.gate))
+ weight_init(gate.weight)
# ``in`` is registered via add_module; getattr resolves it from _modules.
- in_proj = cast(nn.Linear, getattr(mlp, "in"))
+ in_proj = cast(nn.Linear, _unwrap_component(getattr(mlp, "in")))
+ out_proj = cast(nn.Linear, _unwrap_component(mlp.out))
weight_init(in_proj.weight)
- output_init(mlp.out.weight)
+ output_init(out_proj.weight)
diff --git a/transformer_lens/model_bridge/sources/native/model.py b/transformer_lens/model_bridge/sources/native/model.py
index 5617459e79..995a8664bd 100644
--- a/transformer_lens/model_bridge/sources/native/model.py
+++ b/transformer_lens/model_bridge/sources/native/model.py
@@ -281,6 +281,9 @@ def forward(
scores = scores.masked_fill(block_mask, float("-inf"))
pattern = F.softmax(scores, dim=-1)
+ # Fully masked padding queries softmax to NaN; overwrite masked entries
+ # so those rows contribute a zero attention update instead of poisoning later layers.
+ pattern = pattern.masked_fill(block_mask, 0.0)
attn = torch.matmul(pattern, v).transpose(1, 2).contiguous().view(batch, seq, -1)
out = self.o(attn)
@@ -436,15 +439,27 @@ def __init__(self, cfg: TransformerBridgeConfig):
def forward(
self,
- input_ids: torch.Tensor,
+ input_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.Tensor] = None,
+ inputs_embeds: Optional[torch.Tensor] = None,
**kwargs,
) -> torch.Tensor:
"""Returns logits directly."""
+ if input_ids is not None and inputs_embeds is not None:
+ raise ValueError("Exactly one of input_ids or inputs_embeds must be provided.")
+ if input_ids is not None:
+ model_input = input_ids
+ hidden_states = self.tok_embed(input_ids)
+ elif inputs_embeds is not None:
+ model_input = inputs_embeds
+ hidden_states = inputs_embeds
+ else:
+ raise ValueError("Exactly one of input_ids or inputs_embeds must be provided.")
+
# Bounds check up front so both absolute and rotary paths produce a
# self-explanatory error rather than IndexError / shape mismatch.
- seq_len = input_ids.shape[-1]
+ seq_len = model_input.shape[1]
if seq_len > self.cfg.n_ctx:
raise ValueError(
f"input length {seq_len} exceeds n_ctx={self.cfg.n_ctx}; "
@@ -453,11 +468,12 @@ def forward(
# Resolve position_ids before the block loop so rotary sees the caller's
# positions, not the dense default.
- batch, seq = input_ids.shape
+ batch, seq = model_input.shape[:2]
if position_ids is None:
- position_ids = torch.arange(seq, device=input_ids.device).unsqueeze(0).expand(batch, -1)
+ position_ids = (
+ torch.arange(seq, device=model_input.device).unsqueeze(0).expand(batch, -1)
+ )
- hidden_states = self.tok_embed(input_ids)
if self.pos is not None:
hidden_states = hidden_states + self.pos(position_ids)
diff --git a/transformer_lens/model_bridge/sources/transformers.py b/transformer_lens/model_bridge/sources/transformers.py
index 40d40ca190..25fa5cb1e6 100644
--- a/transformer_lens/model_bridge/sources/transformers.py
+++ b/transformer_lens/model_bridge/sources/transformers.py
@@ -750,7 +750,6 @@ def boot(
# resolved values.
from transformer_lens.utilities.multi_gpu import (
MIXED_CPU_GPU_ERROR,
- cast_floating_params_to_dtype,
count_unique_devices,
find_embedding_device,
find_misplaced_modules,
@@ -847,7 +846,11 @@ def boot(
# Cast params to dtype; preserve float32 buffers (e.g., RotaryEmbedding.inv_freq).
# Use module-level alignment so Accelerate can temporarily materialize offloaded
# parameters before we touch them.
- cast_floating_params_to_dtype(hf_model, dtype)
+ # Skip dtype normalization entirely when model has an active quantizer: the
+ # quantizer owns specific dtypes (e.g., FP8 scales) that must not be overwritten.
+ from transformer_lens.utilities.multi_gpu import maybe_cast_floating_params
+
+ maybe_cast_floating_params(hf_model, dtype)
# Derive cfg.device / cfg.n_devices from hf_device_map when present. This covers:
# - fresh loads with a resolved device_map (set above)
# - pre-loaded hf_model that the caller dispatched themselves (e.g., device_map="auto")
@@ -959,10 +962,16 @@ def boot(
if tokenizer is not None:
# Detect BOS/EOS behavior (use non-empty string; empty is unreliable with token aliasing)
encoded_test = tokenizer.encode("a")
+ leading_special_ids = {
+ token_id
+ for token_id in (tokenizer.bos_token_id, getattr(tokenizer, "cls_token_id", None))
+ if token_id is not None
+ }
+ # CLS counts: BERT-style tokenizers prepend [CLS], which HookedTransformer
+ # treats as the BOS-like token; comparing only against bos_token_id (a
+ # fallback string on such tokenizers) concludes False and desyncs the stacks.
adapter.cfg.tokenizer_prepends_bos = (
- len(encoded_test) > 1
- and tokenizer.bos_token_id is not None
- and encoded_test[0] == tokenizer.bos_token_id
+ len(encoded_test) > 1 and encoded_test[0] in leading_special_ids
)
adapter.cfg.tokenizer_appends_eos = (
len(encoded_test) > 1
diff --git a/transformer_lens/model_bridge/supported_architectures/ast.py b/transformer_lens/model_bridge/supported_architectures/ast.py
index 78bde24b9d..5ecd7c7134 100644
--- a/transformer_lens/model_bridge/supported_architectures/ast.py
+++ b/transformer_lens/model_bridge/supported_architectures/ast.py
@@ -128,6 +128,11 @@ def prepare_model(self, hf_model: Any) -> None:
and hasattr(hf_model, "classifier")
and hasattr(hf_model.classifier, "dense")
):
+ self.component_mapping["classifier_ln"] = NormalizationBridge(
+ name="classifier.layernorm",
+ config=self.cfg,
+ use_native_layernorm_autograd=True,
+ )
self.component_mapping["unembed"] = UnembeddingBridge(name="classifier.dense")
self.cfg.d_vocab = num_labels
self.cfg.d_vocab_out = num_labels
diff --git a/transformer_lens/model_bridge/supported_architectures/bert.py b/transformer_lens/model_bridge/supported_architectures/bert.py
index f58c841711..f71a237d6a 100644
--- a/transformer_lens/model_bridge/supported_architectures/bert.py
+++ b/transformer_lens/model_bridge/supported_architectures/bert.py
@@ -87,7 +87,13 @@ def __init__(self, cfg: Any) -> None:
# MLM defaults; prepare_model() adjusts for other task heads (e.g., NSP).
self.component_mapping = {
"embed": EmbeddingBridge(name="bert.embeddings.word_embeddings"),
+ "token_type_embed": EmbeddingBridge(name="bert.embeddings.token_type_embeddings"),
"pos_embed": PosEmbedBridge(name="bert.embeddings.position_embeddings"),
+ "embed_ln": NormalizationBridge(
+ name="bert.embeddings.LayerNorm",
+ config=self.cfg,
+ use_native_layernorm_autograd=True,
+ ),
"blocks": BlockBridge(
name="bert.encoder.layer",
# BERT has no single MLP module (intermediate.dense and output.dense
@@ -129,6 +135,7 @@ def __init__(self, cfg: Any) -> None:
),
},
),
+ "mlm_head": LinearBridge(name="cls.predictions.transform.dense"),
"unembed": UnembeddingBridge(name="cls.predictions.decoder"),
"ln_final": NormalizationBridge(
name="cls.predictions.transform.LayerNorm",
@@ -144,7 +151,15 @@ def prepare_model(self, hf_model: Any) -> None:
BertForNextSentencePrediction has cls.seq_relationship (NSP head)
and no MLM-specific LayerNorm.
"""
- if hasattr(hf_model, "cls") and hasattr(hf_model.cls, "seq_relationship"):
- # NSP model — swap head components
+ if getattr(getattr(hf_model, "bert", None), "pooler", None) is not None:
+ self.components["pooler"] = LinearBridge(name="bert.pooler.dense")
+
+ has_predictions = hasattr(getattr(hf_model, "cls", None), "predictions")
+ has_nsp_head = hasattr(getattr(hf_model, "cls", None), "seq_relationship")
+ if has_nsp_head and has_predictions:
+ self.components["nsp_head"] = LinearBridge(name="cls.seq_relationship")
+ elif has_nsp_head:
+ # NSP-only model — swap head components.
self.components["unembed"] = UnembeddingBridge(name="cls.seq_relationship")
+ self.components.pop("mlm_head", None)
self.components.pop("ln_final", None)
diff --git a/transformer_lens/model_bridge/supported_architectures/cohere.py b/transformer_lens/model_bridge/supported_architectures/cohere.py
index 11d977cccc..bbc377e6bd 100644
--- a/transformer_lens/model_bridge/supported_architectures/cohere.py
+++ b/transformer_lens/model_bridge/supported_architectures/cohere.py
@@ -185,6 +185,9 @@ class _Cohere2AttentionBridge(PositionEmbeddingsAttentionBridge):
reconstruction path.
"""
+ # Nulls position_embeddings on NoPE layers by design.
+ rope_optional = True
+
def forward(self, *args: Any, **kwargs: Any) -> Any:
"""Drop position_embeddings on Cohere2 full-attention NoPE layers."""
if self._is_nope_layer():
diff --git a/transformer_lens/model_bridge/supported_architectures/dream.py b/transformer_lens/model_bridge/supported_architectures/dream.py
index e11fd35050..12f444867c 100644
--- a/transformer_lens/model_bridge/supported_architectures/dream.py
+++ b/transformer_lens/model_bridge/supported_architectures/dream.py
@@ -100,6 +100,8 @@ class DreamArchitectureAdapter(Qwen2ArchitectureAdapter):
# sampler's text (benchmarks route through diffusion_generate).
applicable_phases: list[int] = [1, 2, 3, 4]
supports_generation: bool = False
+ # Bidirectional masked-denoising objective; shifted causal CE is undefined.
+ supports_causal_loss: bool = False
# Sampling is iterative denoising, not left-to-right; Dream ships the
# schedule as a mixin method whose per-step forward goes through __call__,
# so bridge hooks fire during sampling.
diff --git a/transformer_lens/model_bridge/supported_architectures/gidd.py b/transformer_lens/model_bridge/supported_architectures/gidd.py
index 5a269507a7..6a65b7d8d7 100644
--- a/transformer_lens/model_bridge/supported_architectures/gidd.py
+++ b/transformer_lens/model_bridge/supported_architectures/gidd.py
@@ -57,6 +57,8 @@ class GiddArchitectureAdapter(ArchitectureAdapter):
applicable_phases: list[int] = [1, 2, 3, 4]
supports_generation: bool = False
+ # Bidirectional masked-denoising objective; shifted causal CE is undefined.
+ supports_causal_loss: bool = False
# Block-wise denoising with self-correction, shipped on the model class.
native_sampler: str = "generate"
# ScaledLinear applies a runtime weight scale; folding norms into those
diff --git a/transformer_lens/model_bridge/supported_architectures/llada.py b/transformer_lens/model_bridge/supported_architectures/llada.py
index 36eae41a65..5a4d867847 100644
--- a/transformer_lens/model_bridge/supported_architectures/llada.py
+++ b/transformer_lens/model_bridge/supported_architectures/llada.py
@@ -124,6 +124,7 @@ def forward(
attn_scores = self.hook_attn_scores(attn_scores)
pattern = torch.nn.functional.softmax(attn_scores, dim=-1, dtype=torch.float32).to(q.dtype)
+ pattern = self._scrub_compatibility_pattern_nans(pattern)
dropout = float(getattr(block.config, "attention_dropout", 0.0))
if block.training and dropout > 0.0:
pattern = torch.nn.functional.dropout(pattern, p=dropout, training=True)
diff --git a/transformer_lens/model_bridge/supported_architectures/nanogpt.py b/transformer_lens/model_bridge/supported_architectures/nanogpt.py
index 13d381d2cf..5ba52e4bf8 100644
--- a/transformer_lens/model_bridge/supported_architectures/nanogpt.py
+++ b/transformer_lens/model_bridge/supported_architectures/nanogpt.py
@@ -1,7 +1,5 @@
from typing import Any
-import torch
-
from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion
from transformer_lens.conversion_utils.param_processing_conversion import (
ParamProcessingConversion,
@@ -88,16 +86,3 @@ def __init__(self, cfg: Any) -> None:
), # Final layer norm
"unembed": UnembeddingBridge(name="lm_head"),
}
-
- def convert_weights(self, remote_module: Any) -> dict[str, torch.Tensor]:
- # Nanogpt models saved after torch.compile() have this unwanted prefix
- # This is a simple way to remove it
- unwanted_prefix = "_orig_mod."
- state_dict: dict[str, torch.Tensor] = (
- remote_module.state_dict() if hasattr(remote_module, "state_dict") else remote_module
- )
- for k, v in list(state_dict.items()):
- if k.startswith(unwanted_prefix):
- state_dict[k[len(unwanted_prefix) :]] = state_dict.pop(k)
-
- return super().convert_weights(remote_module) # type: ignore[misc]
diff --git a/transformer_lens/model_bridge/supported_architectures/pretrain.py b/transformer_lens/model_bridge/supported_architectures/pretrain.py
index 20091b65ff..5a7dfe269b 100644
--- a/transformer_lens/model_bridge/supported_architectures/pretrain.py
+++ b/transformer_lens/model_bridge/supported_architectures/pretrain.py
@@ -368,11 +368,10 @@ class PretrainArchitectureAdapter(ArchitectureAdapter):
def __init__(self, cfg: Any) -> None:
super().__init__(cfg)
- self.cfg.normalization_type = "RMS"
- self.cfg.positional_embedding_type = "rotary"
- self.cfg.final_rms = True
- self.cfg.gated_mlp = True
- self.cfg.attn_only = False
+ # Also sets uses_rms_norm=True: norm bridges fall back to it when the
+ # wrapped norm's class name doesn't identify itself as RMSNorm, and a
+ # False fallback would mean-center RMS hook intermediates.
+ self._set_rms_rotary_defaults()
self.component_mapping = {
# "inner." because this adapter expects the source model to
diff --git a/transformer_lens/model_bridge/supported_architectures/raven.py b/transformer_lens/model_bridge/supported_architectures/raven.py
index 85ada2a2b1..cafdcb46ba 100644
--- a/transformer_lens/model_bridge/supported_architectures/raven.py
+++ b/transformer_lens/model_bridge/supported_architectures/raven.py
@@ -135,6 +135,10 @@ class RavenArchitectureAdapter(ArchitectureAdapter):
# state make the phases non-meaningful. Correctness lives in the
# integration tests (seed pinned before bridge and HF calls).
applicable_phases: list[int] = []
+ # Depth-recurrent core: HF-style past_key_values stepping cannot represent
+ # the re-injected recurrence, and batched left-padded stepping compounds it.
+ supports_kv_cache = False
+ supports_batched_generation = False
def __init__(self, cfg: Any) -> None:
"""Initialize the Raven / Huginn architecture adapter."""
diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py
index 1aa5394b0a..eef75a916c 100644
--- a/transformer_lens/model_bridge/supported_architectures/vit.py
+++ b/transformer_lens/model_bridge/supported_architectures/vit.py
@@ -204,3 +204,5 @@ def prepare_model(self, hf_model: Any) -> None:
self.component_mapping = self._build_component_mapping(
prefix=prefix, with_classifier=with_classifier
)
+ if not with_classifier and getattr(hf_model, "pooler", None) is not None:
+ self.component_mapping["pooler"] = LinearBridge(name="pooler.dense")
diff --git a/transformer_lens/tools/analysis/__init__.py b/transformer_lens/tools/analysis/__init__.py
index e15800c5fc..a02d225a67 100644
--- a/transformer_lens/tools/analysis/__init__.py
+++ b/transformer_lens/tools/analysis/__init__.py
@@ -28,15 +28,21 @@
)
from transformer_lens.tools.analysis.jacobian_lens_decomposition import (
JSpaceDecomposition,
+ JSpaceOccupancy,
+ JSpaceVarianceProfile,
+ estimate_occupancy,
get_sparse_decomposition,
)
__all__ = [
"DirectLogitAttribution",
"JSpaceDecomposition",
+ "JSpaceOccupancy",
+ "JSpaceVarianceProfile",
"JacobianLens",
"JacobianLensReadout",
"direct_logit_attribution",
+ "estimate_occupancy",
"get_act_patch_direct_path",
"get_act_patch_direct_path_all_sources",
"get_sparse_decomposition",
diff --git a/transformer_lens/tools/analysis/jacobian_lens.py b/transformer_lens/tools/analysis/jacobian_lens.py
index d4e568cb60..810f457d5b 100644
--- a/transformer_lens/tools/analysis/jacobian_lens.py
+++ b/transformer_lens/tools/analysis/jacobian_lens.py
@@ -73,6 +73,9 @@
from transformer_lens.tools.analysis.jacobian_lens_decomposition import (
DEFAULT_K,
JSpaceDecomposition,
+ JSpaceOccupancy,
+ JSpaceVarianceProfile,
+ estimate_occupancy,
get_sparse_decomposition,
)
from transformer_lens.utilities.hf_utils import call_hf_with_retry
@@ -899,6 +902,27 @@ def decompose(
RuntimeError: If the default nonnegative least-squares solver cannot validate its
result against the KKT conditions.
"""
+ activation, resolved_layer = self._resolve_activation(
+ model, activation_or_prompt, layer, position
+ )
+ dictionary = self.lens_vector_dictionary(model, resolved_layer)
+ return get_sparse_decomposition(
+ activation.float().to(dictionary.device), dictionary, k, algorithm=algorithm
+ )
+
+ def _resolve_activation(
+ self,
+ model: Any,
+ activation_or_prompt: Union[torch.Tensor, str],
+ layer: int,
+ position: Optional[int],
+ ) -> Tuple[torch.Tensor, int]:
+ """Validate the model and layer, then resolve either a raw ``[d_model]`` activation or a
+ prompt plus ``position`` to the activation vector to analyse.
+
+ Returns ``(activation, resolved_layer)``. Shared by :meth:`decompose` and
+ :meth:`occupancy` so both accept the same input forms with identical validation.
+ """
self.validate_model(model)
resolved_layer = _normalize_layer(layer, model.cfg.n_layers)
if resolved_layer not in self.jacobians:
@@ -938,10 +962,169 @@ def decompose(
_, cache = model.run_with_cache(tokens, names_filter=lambda name: name == hook_name)
norm_position = _normalize_positions([position], tokens.shape[1])[0]
activation = cache[hook_name][0, norm_position, :]
+ return activation, resolved_layer
+ @torch.no_grad()
+ def occupancy(
+ self,
+ model: Any,
+ activation_or_prompt: Union[torch.Tensor, str],
+ layer: int,
+ *,
+ position: Optional[int] = None,
+ max_atoms: int = DEFAULT_K,
+ num_control_dictionaries: int = 32,
+ seed: int = 0,
+ ) -> JSpaceOccupancy:
+ """Estimate how many J-lens vectors are meaningfully active in an activation at ``layer``.
+
+ Resolves ``activation_or_prompt`` (a raw ``[d_model]`` vector, or a prompt plus
+ ``position``) exactly as :meth:`decompose`, builds the cached full-vocabulary dictionary
+ via :meth:`lens_vector_dictionary`, and calls :func:`estimate_occupancy`.
+
+ Args:
+ model: A raw ``TransformerBridge``.
+ activation_or_prompt: An activation vector, or a prompt (string / token tensor).
+ layer: Source layer (must be a fitted source layer).
+ position: Token position when a prompt is given; ``None`` for a raw activation.
+ max_atoms: Maximum number of J-lens vectors to consider.
+ num_control_dictionaries: Number of random control dictionaries to average over.
+ seed: Seed for the random control dictionaries (reproducibility).
+
+ Returns:
+ A :class:`JSpaceOccupancy`.
+ """
+ activation, resolved_layer = self._resolve_activation(
+ model, activation_or_prompt, layer, position
+ )
dictionary = self.lens_vector_dictionary(model, resolved_layer)
- return get_sparse_decomposition(
- activation.float().to(dictionary.device), dictionary, k, algorithm=algorithm
+ return estimate_occupancy(
+ activation.float().to(dictionary.device),
+ dictionary,
+ max_atoms=max_atoms,
+ num_control_dictionaries=num_control_dictionaries,
+ seed=seed,
+ )
+
+ @torch.no_grad()
+ def fraction_of_variance(
+ self,
+ model: Any,
+ prompts: Union[str, torch.Tensor, Sequence[Union[str, torch.Tensor]]],
+ layers: Optional[Sequence[int]] = None,
+ *,
+ k: int = DEFAULT_K,
+ skip_first: int = 16,
+ positions: Optional[Sequence[int]] = None,
+ show_progress: bool = False,
+ ) -> JSpaceVarianceProfile:
+ """Profile the J-space share of activation variance over a prompt corpus.
+
+ Each prompt is run once (caching ``blocks.{layer}.hook_out`` for every requested layer).
+ At each sampled position the activation is decomposed and its J-space variance fraction
+ ``||j_space_component||^2 / ||activation||^2`` is recorded. The numerator is the
+ ``j_space_component`` -- the orthogonal projection of the activation onto the span of the
+ selected support (the paper's appendix "J-space component"), *not* the nonnegative
+ ``reconstruction``; the two coincide only when every selected atom stays active. Per layer
+ the profile reports the median of those fractions and the pooled ratio
+ ``sum(||j_space_component||^2) / sum(||activation||^2)`` (the paper's "fraction of total
+ variance").
+
+ A layer that samples no positions -- every prompt shorter than ``skip_first``, or only
+ zero-norm activations -- contributes no fractions: its ``median`` and ``pooled`` are
+ ``float("nan")`` and its ``per_position`` tensor is empty.
+
+ Args:
+ model: A raw ``TransformerBridge``.
+ prompts: A prompt, or a sequence of prompts. Each token tensor must represent exactly
+ one prompt and have shape ``[1, seq]``.
+ layers: Source layers to profile; defaults to all fitted ``source_layers``.
+ k: Number of J-lens vectors per decomposition.
+ skip_first: Non-negative index before which positions are skipped (mirrors the fit's
+ early-position skip); not used for sampling when ``positions`` is given.
+ positions: Explicit positions to sample instead of ``skip_first`` onward.
+ show_progress: Show a tqdm progress bar over prompts.
+
+ Returns:
+ A :class:`JSpaceVarianceProfile`.
+
+ Raises:
+ ValueError: On an invalid model, an unfitted layer, an empty corpus, a negative
+ ``skip_first``, or a token tensor that does not have shape ``[1, seq]``.
+ """
+ self.validate_model(model)
+ if skip_first < 0:
+ raise ValueError(f"skip_first must be non-negative, got {skip_first}")
+ if layers is None:
+ resolved_layers = list(self.source_layers)
+ else:
+ resolved_layers = [_normalize_layer(layer, model.cfg.n_layers) for layer in layers]
+ for layer in resolved_layers:
+ if layer not in self.jacobians:
+ raise ValueError(
+ f"layer {layer} is not in this lens's source layers; "
+ f"available: {self.source_layers}"
+ )
+ prompt_list: List[Union[str, torch.Tensor]] = (
+ [prompts] if isinstance(prompts, (str, torch.Tensor)) else list(prompts)
+ )
+ if not prompt_list:
+ raise ValueError("prompts must be a non-empty prompt or sequence of prompts")
+
+ hook_names = {layer: _resid_post_hook_name(layer) for layer in resolved_layers}
+ wanted_hooks = set(hook_names.values())
+ dictionaries = {
+ layer: self.lens_vector_dictionary(model, layer) for layer in resolved_layers
+ }
+ fractions: Dict[int, List[float]] = {layer: [] for layer in resolved_layers}
+ pooled_j_space: Dict[int, float] = {layer: 0.0 for layer in resolved_layers}
+ pooled_total: Dict[int, float] = {layer: 0.0 for layer in resolved_layers}
+
+ for prompt in tqdm(prompt_list, desc="J-space variance", disable=not show_progress):
+ tokens = model.to_tokens(prompt) if isinstance(prompt, str) else prompt
+ if tokens.ndim != 2 or tokens.shape[0] != 1:
+ raise ValueError(
+ "fraction_of_variance expects each tokenized prompt to have shape "
+ f"[1, seq], got {tuple(tokens.shape)}"
+ )
+ _, cache = model.run_with_cache(tokens, names_filter=lambda name: name in wanted_hooks)
+ seq_len = tokens.shape[1]
+ sampled = (
+ list(range(skip_first, seq_len))
+ if positions is None
+ else _normalize_positions(positions, seq_len)
+ )
+ for layer in resolved_layers:
+ dictionary = dictionaries[layer]
+ activations = cache[hook_names[layer]][0] # [seq, d_model]
+ for position in sampled:
+ activation = activations[position].float().to(dictionary.device)
+ total = float(activation @ activation)
+ if total <= 0.0:
+ continue
+ decomposition = get_sparse_decomposition(activation, dictionary, k)
+ j_space = float(
+ decomposition.j_space_component @ decomposition.j_space_component
+ )
+ fractions[layer].append(j_space / total)
+ pooled_j_space[layer] += j_space
+ pooled_total[layer] += total
+
+ median = {
+ layer: float(torch.tensor(fractions[layer]).median())
+ if fractions[layer]
+ else float("nan")
+ for layer in resolved_layers
+ }
+ pooled = {
+ layer: pooled_j_space[layer] / pooled_total[layer]
+ if pooled_total[layer] > 0
+ else float("nan")
+ for layer in resolved_layers
+ }
+ per_position = {layer: torch.tensor(fractions[layer]) for layer in resolved_layers}
+ return JSpaceVarianceProfile(
+ layers=resolved_layers, median=median, pooled=pooled, per_position=per_position
)
# ------------------------------------------------------------------ #
diff --git a/transformer_lens/tools/analysis/jacobian_lens_decomposition.py b/transformer_lens/tools/analysis/jacobian_lens_decomposition.py
index 4ae46e832c..38040e037f 100644
--- a/transformer_lens/tools/analysis/jacobian_lens_decomposition.py
+++ b/transformer_lens/tools/analysis/jacobian_lens_decomposition.py
@@ -61,7 +61,7 @@
import math
from dataclasses import dataclass
-from typing import List
+from typing import Dict, List, Tuple
import torch
@@ -447,3 +447,174 @@ def get_sparse_decomposition(
j_space_component=j_space_component,
non_j_space_component=non_j_space_component,
)
+
+
+@dataclass
+class JSpaceOccupancy:
+ """Result of a J-space occupancy estimate.
+
+ Attributes:
+ occupancy: Estimated number of meaningfully-active atoms -- the step of maximum
+ separation between the real and random-control cumulative captured variance.
+ marginal_captured_variance: Per-step captured-variance gain of the real greedy selection,
+ shape ``[max_atoms]``.
+ control_captured_variance: Per-step captured-variance gain averaged over the random
+ control dictionaries, shape ``[max_atoms]``.
+ support: Greedily selected atom indices, shape ``[max_atoms]`` (token ids when the
+ dictionary is the vocabulary of J-lens vectors).
+ """
+
+ occupancy: int
+ marginal_captured_variance: torch.Tensor
+ control_captured_variance: torch.Tensor
+ support: torch.Tensor
+
+
+def _greedy_captured_variance_gains(
+ atoms: torch.Tensor, atom_norms: torch.Tensor, target: torch.Tensor, max_atoms: int
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ """Greedily select exactly ``max_atoms`` atoms and return captured-variance gains.
+
+ At each step, add the unused atom with the greatest signed, norm-normalized correlation with
+ the current residual. Project ``target`` orthogonally onto the full selected span using a
+ pseudoinverse, then set the next residual to ``target - projection``. The captured variance is
+ ``||Pi_S target||^2 / ||target||^2``; the returned values are its per-step increments.
+
+ This shares the per-step correlation rule with :func:`get_sparse_decomposition`, but not its
+ residual recurrence: sparse decomposition uses a nonnegative coefficient-fit residual and may
+ stop early, while this recurrence does not stop early, so the selected supports can differ.
+ """
+ total_variance = float(target @ target)
+ support: List[int] = []
+ residual = target.clone()
+ captured_variance_gains: List[float] = []
+ previous_captured_variance = 0.0
+ for _ in range(max_atoms):
+ correlation = (atoms @ residual) / atom_norms
+ for chosen in support:
+ correlation[chosen] = float("-inf")
+ support.append(int(torch.argmax(correlation).item()))
+ active_atoms = atoms[support].T
+ projection = active_atoms @ (torch.linalg.pinv(active_atoms) @ target)
+ captured_variance = float((projection @ projection) / total_variance)
+ captured_variance_gains.append(captured_variance - previous_captured_variance)
+ previous_captured_variance = captured_variance
+ residual = target - projection
+ return torch.tensor(captured_variance_gains), torch.tensor(support, dtype=torch.long)
+
+
+def estimate_occupancy(
+ x: torch.Tensor,
+ dictionary: torch.Tensor,
+ *,
+ max_atoms: int = DEFAULT_K,
+ num_control_dictionaries: int = 32,
+ seed: int = 0,
+) -> JSpaceOccupancy:
+ """Estimate how many dictionary atoms are meaningfully active in ``x``.
+
+ Runs the projection-residual recurrence described in
+ :func:`_greedy_captured_variance_gains` for exactly ``max_atoms`` steps and compares the real
+ per-step captured-variance curve against the same recurrence on ``num_control_dictionaries``
+ random unit-norm dictionaries of the same size. This shares sparse decomposition's per-step
+ correlation rule, but uses an unconstrained span-projection residual rather than a nonnegative
+ coefficient-fit residual, so their supports need not match. The occupancy is the step of
+ maximum separation between the real and (averaged) control *cumulative* captured variance --
+ the point past which further atoms add no more than random directions would. Deterministic
+ given ``seed`` and needs no threshold. (Captured variance is a projection, hence scale-free,
+ so the random control atoms are simply unit-norm.)
+
+ Args:
+ x: Target vector, shape ``[d_model]``.
+ dictionary: Atom matrix, shape ``[num_atoms, d_model]`` (rows are atoms).
+ max_atoms: Number of atoms to select in the real and control recurrences.
+ num_control_dictionaries: Number of random control dictionaries to average over.
+ seed: Seed for the random control dictionaries (reproducibility).
+
+ Returns:
+ An :class:`JSpaceOccupancy`.
+
+ Raises:
+ ValueError: On complex inputs, a non-2-D dictionary, a target whose length does not match
+ ``d_model``, ``max_atoms`` outside ``[1, num_atoms]``,
+ ``num_control_dictionaries < 1``, a target with non-finite entries or a non-finite or
+ zero norm, or a dictionary with non-finite or zero-norm atoms.
+ """
+ if dictionary.ndim != 2:
+ raise ValueError(
+ f"dictionary must be 2-D [num_atoms, d_model], got shape {tuple(dictionary.shape)}"
+ )
+ num_atoms, d_model = dictionary.shape
+ if x.ndim != 1 or x.shape[0] != d_model:
+ raise ValueError(f"x must be 1-D of length d_model={d_model}, got shape {tuple(x.shape)}")
+ if not 1 <= max_atoms <= num_atoms:
+ raise ValueError(f"max_atoms must be between 1 and num_atoms={num_atoms}, got {max_atoms}")
+ if num_control_dictionaries < 1:
+ raise ValueError(
+ f"num_control_dictionaries must be at least 1, got {num_control_dictionaries}"
+ )
+ if torch.is_complex(x) or torch.is_complex(dictionary):
+ raise ValueError("x and dictionary must be real-valued")
+
+ target = x.float()
+ atoms = dictionary.float()
+ if not bool(torch.isfinite(target).all()):
+ raise ValueError("x contains non-finite entries")
+ target_squared_norm = target @ target
+ if not bool(torch.isfinite(target_squared_norm)):
+ raise ValueError("x must have finite norm")
+ if float(target_squared_norm) <= 0.0:
+ raise ValueError("x must have non-zero norm")
+ if not bool(torch.isfinite(atoms).all()):
+ raise ValueError("dictionary contains non-finite entries")
+ atom_norms = (atoms * atoms).sum(dim=1).sqrt()
+ if not bool(torch.isfinite(atom_norms).all()) or bool((atom_norms == 0).any()):
+ raise ValueError("dictionary contains a non-finite or zero-norm atom")
+
+ real_captured_variance, support = _greedy_captured_variance_gains(
+ atoms, atom_norms, target, max_atoms
+ )
+
+ generator = torch.Generator(device=atoms.device).manual_seed(seed)
+ control_atom_norms = torch.ones(num_atoms, device=atoms.device)
+ control_variance_runs: List[torch.Tensor] = []
+ for _ in range(num_control_dictionaries):
+ random_atoms = torch.randn(
+ num_atoms, d_model, generator=generator, device=atoms.device, dtype=atoms.dtype
+ )
+ random_atoms = random_atoms / (random_atoms * random_atoms).sum(dim=1, keepdim=True).sqrt()
+ control_run_variance, _ = _greedy_captured_variance_gains(
+ random_atoms, control_atom_norms, target, max_atoms
+ )
+ control_variance_runs.append(control_run_variance)
+ control_captured_variance = torch.stack(control_variance_runs).mean(dim=0)
+
+ separation = real_captured_variance.cumsum(dim=0) - control_captured_variance.cumsum(dim=0)
+ occupancy = int(torch.argmax(separation).item()) + 1
+ return JSpaceOccupancy(
+ occupancy=occupancy,
+ marginal_captured_variance=real_captured_variance,
+ control_captured_variance=control_captured_variance,
+ support=support,
+ )
+
+
+@dataclass
+class JSpaceVarianceProfile:
+ """Per-layer J-space variance profile over a prompt corpus.
+
+ Produced by :meth:`JacobianLens.fraction_of_variance`.
+
+ Attributes:
+ layers: The source layers profiled, in order.
+ median: Per-layer median over positions of the J-space variance fraction
+ ``||j_space_component||^2 / ||activation||^2``.
+ pooled: Per-layer pooled ratio ``sum(||j_space_component||^2) / sum(||activation||^2)``
+ across the corpus (the paper's "fraction of total variance").
+ per_position: Per-layer 1-D tensor of the raw per-position variance fractions.
+ """
+
+ layers: List[int]
+ median: Dict[int, float]
+ pooled: Dict[int, float]
+ per_position: Dict[int, torch.Tensor]
diff --git a/transformer_lens/tools/model_registry/AGENTS.md b/transformer_lens/tools/model_registry/AGENTS.md
index 8198be7013..e3e2c1e4c1 100644
--- a/transformer_lens/tools/model_registry/AGENTS.md
+++ b/transformer_lens/tools/model_registry/AGENTS.md
@@ -114,7 +114,7 @@ Never edit manually.
| 1 | Core forward correctness vs HuggingFace logits |
| 2 | Hook firing + gradient flow |
| 3 | Weight processing (compatibility mode, fold/centre) |
-| 4 | Text-generation quality |
+| 4 | Text-generation quality (per-model prompt profile, scored by a pinned multilingual judge) |
| 7 | Multimodal (vision/text alignment) — only Llava / Gemma3-multimodal |
| 8 | Audio — Hubert (waveform) and AST (spectrogram) |
| 9 | Vision — ViT/DeiT pixel forward, hook/cache firing, representation stability, classification decode |
@@ -132,24 +132,26 @@ SSM / recurrent families and the hybrids (Mamba-1/2, gated-delta-net, NemotronH,
| 1 | **100%** | — | `STATUS_FAILED` |
| 2 | 75% | `logits_equivalence`, `loss_equivalence` | `STATUS_FAILED` |
| 3 | 75% | `logits_equivalence`, `loss_equivalence` | `STATUS_FAILED` |
-| 4 | 50% | — | **Non-gating.** Below 50% adds `"low text quality"` to the registry `note`; never causes `STATUS_FAILED`. |
+| 4 | 54.5% — the measured pass line `p4_pass_threshold()` (score of the bake-off noise floor `JUDGE_R_GOOD`) | — | **Non-gating.** Below the line adds `"text quality poor (P4=…)"` to the registry `note`; never causes `STATUS_FAILED`. |
| 7 | 75% | `multimodal_forward` | `STATUS_FAILED`. NULL score (processor unavailable) also fails. |
| 8 | 75% | `audio_forward` | `STATUS_FAILED`. NULL score also fails. |
| 9 | 75% | `vision_forward`, `vision_cache` | `STATUS_FAILED`. NULL score also fails. |
-Phase 4 is intentionally lenient — source ([`verify_models.py:554`](verify_models.py)) calls it *"a quality metric, not a correctness check."* The 50% bar asks "is the text coherent at all?" not "is this adapter clean?"
+P4 prompts each model with its resolved **prompt profile** — chat template, translation, code, own-language continuation, or another task kind — via `resolve_profile()` in [`benchmarks/text_quality_profiles.py`](../../benchmarks/text_quality_profiles.py) (precedence: per-model override > architecture rule > live HF Hub signals > stored registry value > default). The resolved profile is cached sparsely on the entry as `prompt_profile` (key omitted when it's just the default). Each generation is scored against a known-good reference by one pinned multilingual judge via the perplexity ratio `PPL(generated)/PPL(reference)`, which cancels the judge's per-language handicap; the pass/fail constants are measured, not hand-picked, in [`benchmarks/text_quality.py`](../../benchmarks/text_quality.py).
+
+Phase 4 is a quality metric, not a correctness check. Its floor is not hand-picked: it equals the benchmark pass line, derived from the judge bake-off's fluent-vs-fluent noise floor (`p4_pass_threshold()` in [`benchmarks/text_quality_profiles.py`](../../benchmarks/text_quality_profiles.py)), so the registry note and the benchmark verdict can never disagree.
**For adapter authors:** a `STATUS_VERIFIED` entry with P4 well below 100% on a small parity-test model can still indicate a real bug the system doesn't gate on (e.g. missing `preprocess_weights` fold). Investigate manually even when VERIFIED.
**Reading the result:**
- `status==1` + `note="Full verification completed"` → all gates passed, no quality flag. Good.
-- `status==1` + `note` mentions `"low text quality"` → P4 < 50%; investigate.
+- `status==1` + `note` mentions `"text quality poor"` → P4 below the pass line; investigate (`scripts/phase4_review.py` orders the candidates and separates old-scale scores).
- `status==1` + P4 < 100% on a small model, no quality flag → potential weight-fold/tokenizer bug; investigate.
- `status==3` (FAILED) → `note` carries the failure reason; debug from there.
- `status==4` (PROVISIONAL) → structural-only pass via `--no-hf-reference`; Phase 1 was never numerically compared to HF, so it does **not** count as verified (`note` is prefixed `Structural only (no HF reference)`). Re-run without the flag for a real verification.
-P1/P3 failures: [supported_architectures/AGENTS.md §When to override preprocess_weights](../../model_bridge/supported_architectures/AGENTS.md#when-to-override-preprocess_weights), [debugging_numerical_divergence.md](../../../docs/source/content/debugging_numerical_divergence.md). P4 drift: [§Tokenizer policy](../../model_bridge/supported_architectures/AGENTS.md#tokenizer-policy) (logit-scale / embedding-scale folds typically degrade P4 without crossing the 50% gate).
+P1/P3 failures: [supported_architectures/AGENTS.md §When to override preprocess_weights](../../model_bridge/supported_architectures/AGENTS.md#when-to-override-preprocess_weights), [debugging_numerical_divergence.md](../../../docs/source/content/debugging_numerical_divergence.md). P4 drift: [§Tokenizer policy](../../model_bridge/supported_architectures/AGENTS.md#tokenizer-policy) (logit-scale / embedding-scale folds typically degrade P4 without crossing the pass line).
---
diff --git a/transformer_lens/tools/model_registry/data/supported_models.json b/transformer_lens/tools/model_registry/data/supported_models.json
index 080c7d91d2..fabaf12e6d 100644
--- a/transformer_lens/tools/model_registry/data/supported_models.json
+++ b/transformer_lens/tools/model_registry/data/supported_models.json
@@ -9,7 +9,7 @@
"total_architectures": 143,
"total_models": 15670,
"total_provisional": 7,
- "total_verified": 1203,
+ "total_verified": 1205,
"models": [
{
"architecture_id": "FalconH1ForCausalLM",
@@ -101,7 +101,7 @@
"status": 3,
"verified_date": "2026-06-27",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.013494, mean_rel=0.006767",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.013494, mean_rel=0.006767",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -115,7 +115,7 @@
"status": 3,
"verified_date": "2026-06-27",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.020484, mean_rel=0.006617",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.020484, mean_rel=0.006617",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -295,15 +295,16 @@
"architecture_id": "LlamaForCausalLM",
"model_id": "01-ai/Yi-1.5-6B",
"status": 1,
- "verified_date": "2026-06-26",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 65.6,
+ "phase4_score": 75.0,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "LlamaForCausalLM",
@@ -435,15 +436,16 @@
"architecture_id": "LlamaForCausalLM",
"model_id": "01-ai/Yi-6B",
"status": 1,
- "verified_date": "2026-02-25",
+ "verified_date": "2026-08-20",
"metadata": null,
- "note": "Core verification completed",
+ "note": "Full verification completed",
"phase1_score": 100.0,
- "phase2_score": null,
- "phase3_score": null,
- "phase4_score": 67.4,
+ "phase2_score": 100.0,
+ "phase3_score": 100.0,
+ "phase4_score": 70.2,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "LlamaForCausalLM",
@@ -479,7 +481,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -934,7 +936,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -1238,7 +1240,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=59.411900, mean_rel=0.569168",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=59.411900, mean_rel=0.569168",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -1634,7 +1636,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'MixtralDecoderLayer' object has no attribute 'block_sparse_moe'",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'MixtralDecoderLayer' object has no attribute 'block_sparse_moe'",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -1828,7 +1830,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=59.402462, mean_rel=0.441563",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=59.402462, mean_rel=0.441563",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -2931,7 +2933,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'MixtralDecoderLayer' object has no attribute 'block_sparse_moe'",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'MixtralDecoderLayer' object has no attribute 'block_sparse_moe'",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -3754,7 +3756,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=16.391922, mean_rel=3.526243",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=16.391922, mean_rel=3.526243",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 95.0,
@@ -3768,7 +3770,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=18.059958, mean_rel=2.710044",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=18.059958, mean_rel=2.710044",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 95.0,
@@ -4219,7 +4221,7 @@
"status": 3,
"verified_date": "2026-02-23",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.002074, mean_rel=0.000409",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.002074, mean_rel=0.000409",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -5405,15 +5407,16 @@
"architecture_id": "MixtralForCausalLM",
"model_id": "BEE-spoke-data/Mixtral-GQA-400m-v2",
"status": 1,
- "verified_date": "2026-03-23",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 91.0,
+ "phase4_score": 86.1,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "LlamaForCausalLM",
@@ -5836,7 +5839,7 @@
"status": 3,
"verified_date": "2026-02-23",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -6031,7 +6034,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=33.204865, mean_rel=0.370595",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=33.204865, mean_rel=0.370595",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -6297,7 +6300,7 @@
"status": 3,
"verified_date": "2026-07-21",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.7% < 75.0% (failed: generation, gene \u2014 Forward pass failed: index out of range in self",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.7% < 75.0% (failed: generation, gene — Forward pass failed: index out of range in self",
"phase1_score": 50.0,
"phase2_score": 7.7,
"phase3_score": 44.4,
@@ -7656,7 +7659,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"phase1_score": 100.0,
"phase2_score": 92.3,
"phase3_score": 90.0,
@@ -7670,7 +7673,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=6.747103, mean_rel=0.054769",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=6.747103, mean_rel=0.054769",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -7684,7 +7687,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"phase1_score": 100.0,
"phase2_score": 92.3,
"phase3_score": 90.0,
@@ -7698,7 +7701,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"phase1_score": 100.0,
"phase2_score": 92.3,
"phase3_score": 90.0,
@@ -7712,7 +7715,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=5.479654, mean_rel=0.052641",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=5.479654, mean_rel=0.052641",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -7726,7 +7729,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"phase1_score": 100.0,
"phase2_score": 92.3,
"phase3_score": 90.0,
@@ -7740,7 +7743,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=6.650925, mean_rel=0.050073",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=6.650925, mean_rel=0.050073",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -7754,7 +7757,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"phase1_score": 100.0,
"phase2_score": 92.3,
"phase3_score": 90.0,
@@ -7768,7 +7771,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=5.641898, mean_rel=0.054789",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=5.641898, mean_rel=0.054789",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -7782,7 +7785,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=5.808517, mean_rel=0.051374",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=5.808517, mean_rel=0.051374",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -11822,7 +11825,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=32.255035, mean_rel=0.318908",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=32.255035, mean_rel=0.318908",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -12810,7 +12813,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"phase1_score": 100.0,
"phase2_score": 76.9,
"phase3_score": 90.0,
@@ -14363,7 +14366,7 @@
"status": 3,
"verified_date": "2026-07-01",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 24/51 components failed (24 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 24/51 components failed (24 critical)",
"phase1_score": 0.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -14377,7 +14380,7 @@
"status": 3,
"verified_date": "2026-07-01",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 50/99 components failed (50 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 50/99 components failed (50 critical)",
"phase1_score": 0.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -14391,7 +14394,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 32/68 components failed (32 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 32/68 components failed (32 critical)",
"phase1_score": 0.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -16343,7 +16346,7 @@
"architecture_id": "GPTJForCausalLM",
"model_id": "EleutherAI/gpt-j-6b",
"status": 1,
- "verified_date": "2026-03-10",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -16351,13 +16354,14 @@
"phase3_score": 100.0,
"phase4_score": 84.3,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "GPTNeoForCausalLM",
"model_id": "EleutherAI/gpt-neo-1.3B",
"status": 1,
- "verified_date": "2026-03-10",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -16365,13 +16369,14 @@
"phase3_score": 100.0,
"phase4_score": 97.6,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "GPTNeoForCausalLM",
"model_id": "EleutherAI/gpt-neo-125m",
"status": 1,
- "verified_date": "2026-03-10",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -16379,7 +16384,8 @@
"phase3_score": 100.0,
"phase4_score": 94.7,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "GPTNeoForCausalLM",
@@ -16609,15 +16615,16 @@
"architecture_id": "GPTNeoXForCausalLM",
"model_id": "EleutherAI/pythia-1.4b",
"status": 1,
- "verified_date": "2026-02-23",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 93.2,
+ "phase4_score": 97.3,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "GPTNeoXForCausalLM",
@@ -17227,7 +17234,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=3.625000, mean_rel=0.024780",
+ "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=3.625000, mean_rel=0.024780",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 89.5,
@@ -18179,7 +18186,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Can't load the model for 'EleutherAI/pythia-410m-seed1'. If you were trying to load it from 'https://hug",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Can't load the model for 'EleutherAI/pythia-410m-seed1'. If you were trying to load it from 'https://hug",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -18597,15 +18604,17 @@
"architecture_id": "GPTNeoXForCausalLM",
"model_id": "EleutherAI/pythia-70m",
"status": 1,
- "verified_date": "2026-07-23",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification completed",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 70.5,
+ "phase4_score": 56.6,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "GPTNeoXForCausalLM",
@@ -18655,7 +18664,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 77.0/100 (avg perplexity: 327.9) \u2014 generated text may be incoherent",
+ "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 77.0/100 (avg perplexity: 327.9) — generated text may be incoherent",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 89.5,
@@ -18669,7 +18678,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 68.8/100 (avg perplexity: 743.0) \u2014 generated text may be incoherent",
+ "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 68.8/100 (avg perplexity: 743.0) — generated text may be incoherent",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 89.5,
@@ -19530,7 +19539,7 @@
"status": 3,
"verified_date": "2026-03-11",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Ex0bit/Elbaz-Olmo-3-7B-Instruct-abliterated does not appear to have files named ('model-00001-of-00006.s",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Ex0bit/Elbaz-Olmo-3-7B-Instruct-abliterated does not appear to have files named ('model-00001-of-00006.s",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -20280,7 +20289,7 @@
"status": 3,
"verified_date": "2026-03-11",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -20330,7 +20339,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=31.355835, mean_rel=0.607207",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=31.355835, mean_rel=0.607207",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -22630,7 +22639,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.011660, mean_rel=0.002703",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.011660, mean_rel=0.002703",
"phase1_score": 50.0,
"phase2_score": 91.7,
"phase3_score": 100.0,
@@ -22811,7 +22820,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: \u2014 144/196 components failed (144 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: — 144/196 components failed (144 critical)",
"phase1_score": 0.0,
"phase2_score": 15.4,
"phase3_score": 21.1,
@@ -25324,7 +25333,7 @@
"architecture_id": "MixtralForCausalLM",
"model_id": "Isotonic/TinyMixtral-4x248M-MoE",
"status": 1,
- "verified_date": "2026-03-23",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -25332,7 +25341,8 @@
"phase3_score": 100.0,
"phase4_score": 92.5,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Qwen2ForCausalLM",
@@ -26790,7 +26800,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 73.9/100 (avg perplexity: 10.2) \u2014 generated text may be incoherent",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 73.9/100 (avg perplexity: 10.2) — generated text may be incoherent",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -29208,7 +29218,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 83.9/100 (below 85.0, avg perplexity: 125.9)",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 83.9/100 (below 85.0, avg perplexity: 125.9)",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -31868,7 +31878,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: MachadoDeCastro/krull-micro does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: MachadoDeCastro/krull-micro does not appear to have a file named pytorch_model.bin or model.safetensors.",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -33388,7 +33398,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: \u2014 12/87 components failed (12 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: — 12/87 components failed (12 critical)",
"phase1_score": 0.0,
"phase2_score": 15.4,
"phase3_score": 50.0,
@@ -33774,7 +33784,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 83.9/100 (below 85.0, avg perplexity: 125.9)",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 83.9/100 (below 85.0, avg perplexity: 125.9)",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -35084,7 +35094,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=39.516827, mean_rel=0.391392",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=39.516827, mean_rel=0.391392",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -36224,7 +36234,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=63.272919, mean_rel=0.497796",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=63.272919, mean_rel=0.497796",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -37144,7 +37154,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=34.928375, mean_rel=0.262157",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=34.928375, mean_rel=0.262157",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -37980,7 +37990,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=29.677444, mean_rel=0.238732",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=29.677444, mean_rel=0.238732",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -38495,7 +38505,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 45.4/100 (avg perplexity: 196.5) \u2014 generated text may be incoherent",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 45.4/100 (avg perplexity: 196.5) — generated text may be incoherent",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -39917,7 +39927,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=50.417328, mean_rel=0.326480",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=50.417328, mean_rel=0.326480",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -40398,7 +40408,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=34.771893, mean_rel=0.288610",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=34.771893, mean_rel=0.288610",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -41127,7 +41137,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=61.738430, mean_rel=0.447178",
+ "note": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=61.738430, mean_rel=0.447178",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -41721,7 +41731,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: extra_special_tokens must be a list/tuple of str or AddedToken, or a dict mapping names to tokens",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: extra_special_tokens must be a list/tuple of str or AddedToken, or a dict mapping names to tokens",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -43604,7 +43614,7 @@
"architecture_id": "Qwen2ForCausalLM",
"model_id": "Qwen/Qwen2.5-0.5B",
"status": 1,
- "verified_date": "2026-07-23",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -43612,21 +43622,25 @@
"phase3_score": 100.0,
"phase4_score": 96.3,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Qwen2ForCausalLM",
"model_id": "Qwen/Qwen2.5-0.5B-Instruct",
"status": 1,
- "verified_date": "2026-03-10",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification completed",
+ "prompt_profile": "chat",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 96.6,
+ "phase4_score": 97.9,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Qwen2ForCausalLM",
@@ -43672,15 +43686,16 @@
"architecture_id": "Qwen2ForCausalLM",
"model_id": "Qwen/Qwen2.5-1.5B",
"status": 1,
- "verified_date": "2026-02-24",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 97.2,
+ "phase4_score": 96.5,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Qwen2ForCausalLM",
@@ -44596,7 +44611,7 @@
"architecture_id": "Qwen3ForCausalLM",
"model_id": "Qwen/Qwen3-0.6B",
"status": 1,
- "verified_date": "2026-04-15",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -44604,7 +44619,8 @@
"phase3_score": 100.0,
"phase4_score": 91.9,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Qwen3ForCausalLM",
@@ -44637,15 +44653,16 @@
"architecture_id": "Qwen3ForCausalLM",
"model_id": "Qwen/Qwen3-1.7B",
"status": 1,
- "verified_date": "2026-02-22",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 95.3,
+ "phase4_score": 97.7,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Qwen3ForCausalLM",
@@ -47436,7 +47453,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: split_with_sizes expects split_sizes to sum exactly to 1152 (input tensor's size at dimension 0), but go",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: split_with_sizes expects split_sizes to sum exactly to 1152 (input tensor's size at dimension 0), but go",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -54292,7 +54309,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=nan, mean_rel=nan",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=nan, mean_rel=nan",
"phase1_score": 50.0,
"phase2_score": 75.0,
"phase3_score": 94.1,
@@ -55725,7 +55742,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: argument 'vocab': 'dict' object cannot be converted to 'Sequence'",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: argument 'vocab': 'dict' object cannot be converted to 'Sequence'",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -58388,7 +58405,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 130/132 components failed (125 high, 5 medium)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 130/132 components failed (125 high, 5 medium)",
"phase1_score": 0.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -59943,7 +59960,7 @@
"status": 3,
"verified_date": "2026-02-23",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Xenova/distilgpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Xenova/distilgpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -59985,7 +60002,7 @@
"status": 3,
"verified_date": "2026-02-23",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Xenova/gpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Xenova/gpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -64432,7 +64449,7 @@
"status": 3,
"verified_date": "2026-04-02",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.004379, mean_rel=0.022909",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.004379, mean_rel=0.022909",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -65087,7 +65104,7 @@
"architecture_id": "Olmo2ForCausalLM",
"model_id": "allenai/OLMo-2-0425-1B",
"status": 1,
- "verified_date": "2026-04-15",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -65095,7 +65112,8 @@
"phase3_score": 100.0,
"phase4_score": 94.8,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Olmo2ForCausalLM",
@@ -65616,7 +65634,7 @@
"architecture_id": "OlmoeForCausalLM",
"model_id": "allenai/OLMoE-1B-7B-0924",
"status": 1,
- "verified_date": "2026-03-10",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -65624,7 +65642,8 @@
"phase3_score": 100.0,
"phase4_score": 97.1,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "OlmoeForCausalLM",
@@ -65658,15 +65677,16 @@
"architecture_id": "Olmo3ForCausalLM",
"model_id": "allenai/Olmo-3-1025-7B",
"status": 1,
- "verified_date": "2026-02-26",
+ "verified_date": "2026-08-20",
"metadata": null,
- "note": "Core verification completed",
+ "note": "Full verification completed",
"phase1_score": 100.0,
- "phase2_score": null,
- "phase3_score": null,
- "phase4_score": 95.8,
+ "phase2_score": 100.0,
+ "phase3_score": 100.0,
+ "phase4_score": 98.8,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Olmo3ForCausalLM",
@@ -67937,7 +67957,7 @@
"architecture_id": "OpenELMForCausalLM",
"model_id": "apple/OpenELM-1_1B",
"status": 1,
- "verified_date": "2026-08-14",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -67968,7 +67988,7 @@
"architecture_id": "OpenELMForCausalLM",
"model_id": "apple/OpenELM-270M",
"status": 1,
- "verified_date": "2026-08-14",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -68267,7 +68287,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: \u2014 12/87 components failed (12 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: — 12/87 components failed (12 critical)",
"phase1_score": 0.0,
"phase2_score": 15.4,
"phase3_score": 50.0,
@@ -71390,7 +71410,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.005348, mean_rel=0.000007",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.005348, mean_rel=0.000007",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 95.0,
@@ -72866,7 +72886,7 @@
"downloads": 89081,
"total_params": null
},
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: This modeling file requires the following packages that were not found in your environment: bitsandbytes",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: This modeling file requires the following packages that were not found in your environment: bitsandbytes",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -72991,7 +73011,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -74348,7 +74368,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -74360,15 +74380,16 @@
"architecture_id": "BloomForCausalLM",
"model_id": "bigscience/bloom-1b7",
"status": 1,
- "verified_date": "2026-02-24",
+ "verified_date": "2026-08-20",
"metadata": null,
- "note": "Full verification completed with issues: P3=95.2% (failed: hook_functionality)",
+ "note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
- "phase3_score": 95.2,
- "phase4_score": 90.4,
+ "phase3_score": 100.0,
+ "phase4_score": 97.4,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "BloomForCausalLM",
@@ -74404,7 +74425,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -74416,15 +74437,16 @@
"architecture_id": "BloomForCausalLM",
"model_id": "bigscience/bloom-560m",
"status": 1,
- "verified_date": "2026-04-07",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 75.9,
+ "phase4_score": 89.2,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "BloomForCausalLM",
@@ -74432,7 +74454,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -74460,7 +74482,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -74474,7 +74496,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P2=69.2% < 75.0% (f \u2014 59/64 components failed (59 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P2=69.2% < 75.0% (f — 59/64 components failed (59 critical)",
"phase1_score": 0.0,
"phase2_score": 69.2,
"phase3_score": 75.0,
@@ -74656,7 +74678,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -74670,7 +74692,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -74682,15 +74704,18 @@
"architecture_id": "MT5ForConditionalGeneration",
"model_id": "bigscience/mt0-base",
"status": 1,
- "verified_date": "2026-06-26",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification passed, but text quality poor (P4=26.8). Needs review",
+ "prompt_profile": "task:instruction",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 97.8,
+ "phase4_score": 26.8,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "MT5ForConditionalGeneration",
@@ -74796,7 +74821,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=65.931717, mean_rel=2.066483",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=65.931717, mean_rel=2.066483",
"phase1_score": 50.0,
"phase2_score": 92.3,
"phase3_score": 95.0,
@@ -74810,7 +74835,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=22.915417, mean_rel=11.391559",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=22.915417, mean_rel=11.391559",
"phase1_score": 50.0,
"phase2_score": 92.3,
"phase3_score": 95.0,
@@ -76262,7 +76287,7 @@
"status": 3,
"verified_date": "2026-02-23",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: 'NoneType' object has no attribute 'from_pretrained'",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: 'NoneType' object has no attribute 'from_pretrained'",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -79416,7 +79441,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.003122, mean_rel=0.000469",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.003122, mean_rel=0.000469",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -80340,7 +80365,7 @@
"status": 3,
"verified_date": "2026-05-08",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 36/282 components failed (36 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 36/282 components failed (36 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -83983,7 +84008,7 @@
"architecture_id": "GPT2LMHeadModel",
"model_id": "distilbert/distilgpt2",
"status": 1,
- "verified_date": "2026-04-07",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -83991,7 +84016,8 @@
"phase3_score": 100.0,
"phase4_score": 81.0,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Qwen3ForCausalLM",
@@ -85218,7 +85244,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 56/243 components failed (56 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 56/243 components failed (56 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -87386,7 +87412,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -87667,7 +87693,7 @@
"architecture_id": "OPTForCausalLM",
"model_id": "facebook/opt-1.3b",
"status": 1,
- "verified_date": "2026-03-10",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -87675,13 +87701,14 @@
"phase3_score": 100.0,
"phase4_score": 96.4,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "OPTForCausalLM",
"model_id": "facebook/opt-125m",
"status": 1,
- "verified_date": "2026-03-10",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -87689,7 +87716,8 @@
"phase3_score": 100.0,
"phase4_score": 90.9,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "OPTForCausalLM",
@@ -90392,7 +90420,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence \u2014 Text quality score: 68.7/100 (avg perplexity: 776.1) \u2014 generated text may be incoherent",
+ "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence — Text quality score: 68.7/100 (avg perplexity: 776.1) — generated text may be incoherent",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 89.5,
@@ -92426,16 +92454,17 @@
{
"architecture_id": "BertForMaskedLM",
"model_id": "google-bert/bert-base-cased",
- "status": 3,
- "verified_date": "2026-06-25",
+ "status": 1,
+ "verified_date": "2026-08-20",
"metadata": null,
- "note": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Shape mismatch: torch.Size([1, 40, 28996]) vs torch.Size([1, 32, 28996])",
+ "note": "Full verification completed",
"phase1_score": 100.0,
- "phase2_score": 66.7,
+ "phase2_score": 100.0,
"phase3_score": 100.0,
"phase4_score": null,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "BertForMaskedLM",
@@ -92443,7 +92472,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=9.275972, mean_rel=13.166794",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=9.275972, mean_rel=13.166794",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -92541,7 +92570,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Shape mismatch: torch.Size([1, 38, 30522]) vs torch.Size([1, 30, 30522])",
+ "note": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Shape mismatch: torch.Size([1, 38, 30522]) vs torch.Size([1, 30, 30522])",
"phase1_score": 100.0,
"phase2_score": 66.7,
"phase3_score": 100.0,
@@ -92555,7 +92584,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Shape mismatch: torch.Size([1, 40, 28996]) vs torch.Size([1, 32, 28996])",
+ "note": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Shape mismatch: torch.Size([1, 40, 28996]) vs torch.Size([1, 32, 28996])",
"phase1_score": 100.0,
"phase2_score": 66.7,
"phase3_score": 100.0,
@@ -92583,7 +92612,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Shape mismatch: torch.Size([1, 38, 30522]) vs torch.Size([1, 30, 30522])",
+ "note": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Shape mismatch: torch.Size([1, 38, 30522]) vs torch.Size([1, 30, 30522])",
"phase1_score": 100.0,
"phase2_score": 66.7,
"phase3_score": 100.0,
@@ -92608,16 +92637,19 @@
{
"architecture_id": "T5ForConditionalGeneration",
"model_id": "google-t5/t5-base",
- "status": 0,
- "verified_date": null,
+ "status": 1,
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": null,
- "phase1_score": null,
- "phase2_score": null,
+ "note": "Core verification completed",
+ "prompt_profile": "task:translation@en-de",
+ "p4_scoring_version": 2,
+ "phase1_score": 100.0,
+ "phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": null,
+ "phase4_score": 100.0,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "T5ForConditionalGeneration",
@@ -92637,15 +92669,18 @@
"architecture_id": "T5ForConditionalGeneration",
"model_id": "google-t5/t5-small",
"status": 1,
- "verified_date": "2026-07-23",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification completed",
+ "prompt_profile": "task:translation@en-de",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 97.6,
+ "phase4_score": 90.4,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "T5ForConditionalGeneration",
@@ -92972,7 +93007,7 @@
"architecture_id": "Gemma2ForCausalLM",
"model_id": "google/gemma-2-2b",
"status": 1,
- "verified_date": "2026-07-23",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed with issues: P3=95.5% (failed: unembed_centering)",
"phase1_score": 100.0,
@@ -92980,21 +93015,25 @@
"phase3_score": 95.5,
"phase4_score": 98.8,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Gemma2ForCausalLM",
"model_id": "google/gemma-2-2b-it",
"status": 1,
- "verified_date": "2026-05-19",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed with issues: P3=95.5% (failed: unembed_centering)",
+ "note": "Core verification completed (prior issues retained: P3=95.5%)",
+ "prompt_profile": "chat",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 95.5,
- "phase4_score": 100.0,
+ "phase4_score": 97.9,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Gemma2ForCausalLM",
@@ -93042,7 +93081,7 @@
"architecture_id": "GemmaForCausalLM",
"model_id": "google/gemma-2b",
"status": 1,
- "verified_date": "2026-05-19",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -93050,7 +93089,8 @@
"phase3_score": 100.0,
"phase4_score": 91.7,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "GemmaForCausalLM",
@@ -93152,7 +93192,7 @@
"architecture_id": "Gemma3ForCausalLM",
"model_id": "google/gemma-3-1b-it",
"status": 1,
- "verified_date": "2026-03-10",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -93160,7 +93200,8 @@
"phase3_score": 100.0,
"phase4_score": 99.3,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Gemma3ForCausalLM",
@@ -93207,7 +93248,7 @@
"architecture_id": "Gemma3ForCausalLM",
"model_id": "google/gemma-3-270m",
"status": 1,
- "verified_date": "2026-07-23",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -93215,7 +93256,8 @@
"phase3_score": 100.0,
"phase4_score": 92.7,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Gemma3ForCausalLM",
@@ -98700,7 +98742,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.003898, mean_rel=0.027017",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.003898, mean_rel=0.027017",
"phase1_score": 50.0,
"phase2_score": 92.3,
"phase3_score": 95.0,
@@ -100800,7 +100842,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 4/24 components failed (4 high)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 4/24 components failed (4 high)",
"phase1_score": 0.0,
"phase2_score": 100.0,
"phase3_score": 85.0,
@@ -101745,7 +101787,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=4.489960, mean_rel=1.238444",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=4.489960, mean_rel=1.238444",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -101871,7 +101913,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=20.307718, mean_rel=6.347236",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=20.307718, mean_rel=6.347236",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -101883,15 +101925,16 @@
"architecture_id": "GraniteForCausalLM",
"model_id": "ibm-granite/granite-3.1-2b-instruct",
"status": 1,
- "verified_date": "2026-03-17",
+ "verified_date": "2026-08-20",
"metadata": null,
- "note": "Core verification completed",
+ "note": "Full verification completed",
"phase1_score": 100.0,
- "phase2_score": null,
- "phase3_score": null,
- "phase4_score": 96.5,
+ "phase2_score": 100.0,
+ "phase3_score": 100.0,
+ "phase4_score": 100.0,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "GraniteMoeForCausalLM",
@@ -101969,7 +102012,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=4.789991, mean_rel=1.159405",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=4.789991, mean_rel=1.159405",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -102179,7 +102222,7 @@
"status": 3,
"verified_date": "2026-04-14",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'NoneType' object has no attribute 'in_proj'",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'NoneType' object has no attribute 'in_proj'",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -102193,7 +102236,7 @@
"status": 3,
"verified_date": "2026-04-15",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/307 components failed (72 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/307 components failed (72 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -102207,7 +102250,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/307 components failed (72 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/307 components failed (72 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -102221,7 +102264,7 @@
"status": 3,
"verified_date": "2026-04-15",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 56/243 components failed (56 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 56/243 components failed (56 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -102235,7 +102278,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 56/243 components failed (56 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 56/243 components failed (56 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -102249,7 +102292,7 @@
"status": 3,
"verified_date": "2026-04-15",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/307 components failed (72 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/307 components failed (72 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -102263,7 +102306,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/307 components failed (72 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/307 components failed (72 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -102305,7 +102348,7 @@
"status": 3,
"verified_date": "2026-04-15",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/347 components failed (72 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/347 components failed (72 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -102319,7 +102362,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/347 components failed (72 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/347 components failed (72 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -102361,7 +102404,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/347 components failed (72 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/347 components failed (72 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -103668,7 +103711,7 @@
"downloads": 5738,
"total_params": null
},
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'type'",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'type'",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -103748,7 +103791,7 @@
"architecture_id": "InternLM2ForCausalLM",
"model_id": "internlm/internlm2-chat-1_8b",
"status": 1,
- "verified_date": "2026-07-23",
+ "verified_date": "2026-08-20",
"metadata": {
"downloads": 5069,
"total_params": 1889110016
@@ -103759,7 +103802,8 @@
"phase3_score": 100.0,
"phase4_score": 84.2,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "InternLM2ForCausalLM",
@@ -108810,7 +108854,7 @@
"status": 3,
"verified_date": "2026-04-08",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load katuni4ka/tiny-random-deepseek-v3:\n(Request ID",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load katuni4ka/tiny-random-deepseek-v3:\n(Request ID",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -108824,7 +108868,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: split_with_sizes expects split_sizes to sum exactly to 256 (input tensor's size at dimension 0), but got",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: split_with_sizes expects split_sizes to sum exactly to 256 (input tensor's size at dimension 0), but got",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -117839,7 +117883,7 @@
"status": 1,
"verified_date": "2026-02-25",
"metadata": null,
- "note": "Below threshold: P3=81.8% but required tests failed: logits_equivalence \u2014 Scalars differ: 0.000000 vs -0.015625",
+ "note": "Below threshold: P3=81.8% but required tests failed: logits_equivalence — Scalars differ: 0.000000 vs -0.015625",
"phase1_score": 100.0,
"phase2_score": 78.6,
"phase3_score": 81.8,
@@ -118813,7 +118857,7 @@
"architecture_id": "Phi3ForCausalLM",
"model_id": "microsoft/Phi-3-mini-4k-instruct",
"status": 1,
- "verified_date": "2026-04-07",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -118821,7 +118865,8 @@
"phase3_score": 100.0,
"phase4_score": 98.5,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Phi3ForCausalLM",
@@ -118855,7 +118900,7 @@
"architecture_id": "Phi3ForCausalLM",
"model_id": "microsoft/Phi-3.5-mini-instruct",
"status": 1,
- "verified_date": "2026-03-30",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -118863,7 +118908,8 @@
"phase3_score": 100.0,
"phase4_score": 97.4,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Phi3ForCausalLM",
@@ -118995,15 +119041,16 @@
"architecture_id": "PhiForCausalLM",
"model_id": "microsoft/phi-1",
"status": 1,
- "verified_date": "2026-02-22",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 92.3,
+ "phase4_score": 90.6,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "PhiForCausalLM",
@@ -119023,15 +119070,16 @@
"architecture_id": "PhiForCausalLM",
"model_id": "microsoft/phi-2",
"status": 1,
- "verified_date": "2026-03-27",
+ "verified_date": "2026-08-20",
"metadata": null,
- "note": "Full verification completed with issues: P2=92.9% (failed: backward_hooks)",
+ "note": "Full verification completed with issues: P2=93.3% (failed: backward_hooks)",
"phase1_score": 100.0,
- "phase2_score": 92.9,
+ "phase2_score": 93.3,
"phase3_score": 100.0,
"phase4_score": 95.8,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "PhiForCausalLM",
@@ -120219,15 +120267,16 @@
"architecture_id": "MistralForCausalLM",
"model_id": "mistralai/Mistral-7B-v0.1",
"status": 1,
- "verified_date": "2026-02-26",
+ "verified_date": "2026-08-20",
"metadata": null,
- "note": "Core verification completed",
+ "note": "Full verification completed",
"phase1_score": 100.0,
- "phase2_score": null,
- "phase3_score": null,
- "phase4_score": 96.6,
+ "phase2_score": 100.0,
+ "phase3_score": 100.0,
+ "phase4_score": 95.4,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "MistralForCausalLM",
@@ -124446,7 +124495,7 @@
"status": 3,
"verified_date": "2026-07-21",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -125422,7 +125471,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=75.0% but required tests failed \u2014 Tensors differ: max_diff=378.613281, mean_rel=0.057195",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=75.0% but required tests failed — Tensors differ: max_diff=378.613281, mean_rel=0.057195",
"phase1_score": 50.0,
"phase2_score": 76.9,
"phase3_score": 75.0,
@@ -126574,7 +126623,7 @@
"status": 3,
"verified_date": "2026-03-11",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: onnx-community/gemma-3-270m-it-ONNX does not appear to have a file named pytorch_model.bin or model.safe",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: onnx-community/gemma-3-270m-it-ONNX does not appear to have a file named pytorch_model.bin or model.safe",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -126602,7 +126651,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: onnx-community/granite-4.0-1b-ONNX-web does not appear to have a file named pytorch_model.bin or model.s",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: onnx-community/granite-4.0-1b-ONNX-web does not appear to have a file named pytorch_model.bin or model.s",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -126616,7 +126665,7 @@
"status": 3,
"verified_date": "2026-04-14",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: onnx-community/granite-4.0-350m-ONNX-web does not appear to have a file named pytorch_model.bin or model",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: onnx-community/granite-4.0-350m-ONNX-web does not appear to have a file named pytorch_model.bin or model",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -126644,7 +126693,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: onnx-community/tiny-random-olmo-hf does not appear to have a file named pytorch_model.bin or model.safet",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: onnx-community/tiny-random-olmo-hf does not appear to have a file named pytorch_model.bin or model.safet",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -127204,15 +127253,17 @@
"architecture_id": "GPT2LMHeadModel",
"model_id": "openai-community/gpt2",
"status": 1,
- "verified_date": "2026-07-23",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification completed",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 88.5,
+ "phase4_score": 68.2,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "GPT2LMHeadModel",
@@ -127971,7 +128022,7 @@
"status": 3,
"verified_date": "2026-04-08",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load optimum-intel-internal-testing/tiny-random-dee",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load optimum-intel-internal-testing/tiny-random-dee",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -128089,7 +128140,7 @@
"status": 3,
"verified_date": "2026-02-23",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: optimum/gpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: optimum/gpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -129621,7 +129672,7 @@
"status": 3,
"verified_date": "2026-05-08",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/190 components failed (24 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/190 components failed (24 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -134412,7 +134463,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P4=4.1% < 50.0% (failed: text_quality) \u2014 Text quality score: 4.1/100 (avg perplexity: 3.4) \u2014 generated text may be incoherent",
+ "note": "Below threshold: P4=4.1% < 50.0% (failed: text_quality) — Text quality score: 4.1/100 (avg perplexity: 3.4) — generated text may be incoherent",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -136082,7 +136133,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation \u2014 Generated text has no new tokens",
+ "note": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation — Generated text has no new tokens",
"phase1_score": 100.0,
"phase2_score": 69.2,
"phase3_score": 95.0,
@@ -137686,7 +137737,7 @@
"status": 3,
"verified_date": "2026-03-11",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 104/242 components failed (104 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 104/242 components failed (104 critical)",
"phase1_score": 0.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -138281,7 +138332,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence; P4=6.8% < 50.0% (failed: te \u2014 Text quality score: 6.8/100 (avg perplexity: 372419.9) \u2014 generated text may be incoherent",
+ "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence; P4=6.8% < 50.0% (failed: te — Text quality score: 6.8/100 (avg perplexity: 372419.9) — generated text may be incoherent",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 89.5,
@@ -138295,7 +138346,7 @@
"status": 3,
"verified_date": "2026-07-21",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'FalconDecoderLayer' object has no attribute 'ln_attn'",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'FalconDecoderLayer' object has no attribute 'ln_attn'",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -138615,15 +138666,16 @@
"architecture_id": "StableLmForCausalLM",
"model_id": "stabilityai/stablelm-2-1_6b",
"status": 1,
- "verified_date": "2026-02-22",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 98.0,
+ "phase4_score": 95.3,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "StableLmForCausalLM",
@@ -139088,14 +139140,15 @@
"architecture_id": "MambaForCausalLM",
"model_id": "state-spaces/mamba-130m-hf",
"status": 1,
- "verified_date": "2026-07-23",
+ "verified_date": "2026-08-20",
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
"phase4_score": 95.8,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "MambaForCausalLM",
@@ -141778,7 +141831,7 @@
"status": 3,
"verified_date": "2026-04-09",
"metadata": null,
- "note": "Below threshold: P3=50.0% < 75.0% (failed: process_bridge_weights, layer_norm_folding, weight_modifi \u2014 Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
+ "note": "Below threshold: P3=50.0% < 75.0% (failed: process_bridge_weights, layer_norm_folding, weight_modifi — Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
"phase1_score": 100.0,
"phase2_score": 83.3,
"phase3_score": 50.0,
@@ -143238,7 +143291,7 @@
"status": 3,
"verified_date": "2026-04-08",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 2/12 components failed (2 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 2/12 components failed (2 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 94.7,
@@ -143252,7 +143305,7 @@
"status": 3,
"verified_date": "2026-04-08",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 18/32 components failed (18 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 18/32 components failed (18 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 94.7,
@@ -143280,7 +143333,7 @@
"status": 3,
"verified_date": "2026-02-22",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/12 components failed (1 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/12 components failed (1 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 83.3,
@@ -143521,7 +143574,7 @@
"downloads": 207171,
"total_params": 2574656
},
- "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence \u2014 Text quality score: 72.2/100 (avg perplexity: 558.8) \u2014 generated text may be incoherent",
+ "note": "Below threshold: P3=89.5% but required tests failed: logits_equivalence — Text quality score: 72.2/100 (avg perplexity: 558.8) — generated text may be incoherent",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 89.5,
@@ -148033,7 +148086,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 56/243 components failed (56 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 56/243 components failed (56 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -148047,7 +148100,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/307 components failed (72 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/307 components failed (72 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -148061,7 +148114,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=7.7% < 75.0% (failed: g \u2014 144/307 components failed (144 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=7.7% < 75.0% (failed: g — 144/307 components failed (144 critical)",
"phase1_score": 0.0,
"phase2_score": 7.7,
"phase3_score": 22.2,
@@ -149856,7 +149909,7 @@
"status": 3,
"verified_date": "2026-02-24",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.004045, mean_rel=0.000066",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.004045, mean_rel=0.000066",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
@@ -154634,7 +154687,7 @@
"status": 3,
"verified_date": "2026-04-09",
"metadata": null,
- "note": "Below threshold: P3=55.6% < 75.0% (failed: process_bridge_weights, weight_modification, hook_functio \u2014 Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
+ "note": "Below threshold: P3=55.6% < 75.0% (failed: process_bridge_weights, weight_modification, hook_functio — Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
"phase1_score": 100.0,
"phase2_score": 83.3,
"phase3_score": 55.6,
@@ -156401,7 +156454,7 @@
"architecture_id": "SmolLM3ForCausalLM",
"model_id": "HuggingFaceTB/SmolLM3-3B",
"status": 1,
- "verified_date": "2026-06-04",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -156409,7 +156462,8 @@
"phase3_score": 100.0,
"phase4_score": 99.4,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "SmolLM3ForCausalLM",
@@ -160071,7 +160125,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=0.009886, mean_rel=0.980186",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=0.009886, mean_rel=0.980186",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -162241,7 +162295,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation \u2014 Generated text has no new tokens",
+ "note": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation — Generated text has no new tokens",
"phase1_score": 100.0,
"phase2_score": 69.2,
"phase3_score": 95.0,
@@ -168317,7 +168371,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: [Errno 2] No such file or directory: 'baichuan-inc/Baichuan2-7B-Chat-4bits/pytorch_model.bin'",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: [Errno 2] No such file or directory: 'baichuan-inc/Baichuan2-7B-Chat-4bits/pytorch_model.bin'",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -168359,7 +168413,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 70/609 components failed (70 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 70/609 components failed (70 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -168371,15 +168425,16 @@
"architecture_id": "Gemma4ForConditionalGeneration",
"model_id": "google/gemma-4-E2B",
"status": 1,
- "verified_date": "2026-06-30",
+ "verified_date": "2026-08-20",
"metadata": null,
- "note": "Core verification completed",
+ "note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
"phase4_score": 88.4,
"phase7_score": 100.0,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Gemma4ForConditionalGeneration",
@@ -172741,7 +172796,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 70/609 components failed (70 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 70/609 components failed (70 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -172755,7 +172810,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 60/534 components failed (60 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 60/534 components failed (60 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -172781,29 +172836,31 @@
"architecture_id": "ApertusForCausalLM",
"model_id": "swiss-ai/Apertus-v1.1-0.5B",
"status": 1,
- "verified_date": "2026-06-25",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 98.9,
+ "phase4_score": 94.5,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "ApertusForCausalLM",
"model_id": "swiss-ai/Apertus-v1.1-1.5B",
"status": 1,
- "verified_date": "2026-06-26",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 100.0,
- "phase4_score": 94.1,
+ "phase4_score": 92.3,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "ApertusForCausalLM",
@@ -173987,7 +174044,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -175065,7 +175122,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -175891,7 +175948,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -176563,7 +176620,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -177277,7 +177334,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -177669,7 +177726,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=38.5% < 75.0% (failed: \u2014 1/15 components failed (1 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=38.5% < 75.0% (failed: — 1/15 components failed (1 critical)",
"phase1_score": 0.0,
"phase2_score": 38.5,
"phase3_score": 38.9,
@@ -178481,7 +178538,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: LiquidAI/LFM2-8B-A1B-ONNX does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: LiquidAI/LFM2-8B-A1B-ONNX does not appear to have a file named pytorch_model.bin or model.safetensors.",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -178509,7 +178566,7 @@
"status": 3,
"verified_date": "2026-06-26",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: LiquidAI/LFM2.5-8B-A1B-ONNX does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: LiquidAI/LFM2.5-8B-A1B-ONNX does not appear to have a file named pytorch_model.bin or model.safetensors.",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -178703,15 +178760,18 @@
"architecture_id": "BartForConditionalGeneration",
"model_id": "facebook/bart-large-cnn",
"status": 1,
- "verified_date": "2026-07-08",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification completed",
+ "prompt_profile": "task:summarization",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 78.7,
+ "phase4_score": 99.2,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "BartForConditionalGeneration",
@@ -178943,7 +179003,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -178957,7 +179017,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179041,7 +179101,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 9/167 components failed (9 critical)",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 9/167 components failed (9 critical)",
"phase1_score": 0.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179055,7 +179115,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 8/167 components failed (8 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 8/167 components failed (8 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179069,7 +179129,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 8/167 components failed (8 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 8/167 components failed (8 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179083,7 +179143,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 8/167 components failed (8 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 8/167 components failed (8 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179097,7 +179157,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/247 components failed (12 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/247 components failed (12 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179111,7 +179171,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/247 components failed (12 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/247 components failed (12 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179125,7 +179185,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/247 components failed (12 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/247 components failed (12 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179139,7 +179199,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179153,7 +179213,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179167,7 +179227,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179181,7 +179241,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179195,7 +179255,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179209,7 +179269,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179223,7 +179283,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179237,7 +179297,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179251,7 +179311,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179265,7 +179325,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -179279,7 +179339,7 @@
"status": 3,
"verified_date": "2026-06-25",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -181281,7 +181341,7 @@
"status": 3,
"verified_date": "2026-07-21",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=11.357496, mean_rel=4.069944",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=11.357496, mean_rel=4.069944",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 90.0,
@@ -182665,15 +182725,18 @@
"architecture_id": "MarianMTModel",
"model_id": "Helsinki-NLP/opus-mt-nl-en",
"status": 1,
- "verified_date": "2026-07-24",
+ "verified_date": "2026-08-21",
"metadata": null,
"note": "Core verification completed",
+ "prompt_profile": "task:translation@nl-en",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 80.7,
+ "phase4_score": 94.2,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "MarianMTModel",
@@ -183071,15 +183134,18 @@
"architecture_id": "M2M100ForConditionalGeneration",
"model_id": "facebook/m2m100_418M",
"status": 1,
- "verified_date": "2026-07-24",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed with issues, low text quality",
+ "note": "Core verification completed",
+ "prompt_profile": "task:translation@en-de",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 38.6,
+ "phase4_score": 100.0,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "M2M100ForConditionalGeneration",
@@ -183239,43 +183305,52 @@
"architecture_id": "MBartForConditionalGeneration",
"model_id": "facebook/mbart-large-50-many-to-many-mmt",
"status": 1,
- "verified_date": "2026-07-24",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification completed",
+ "prompt_profile": "task:translation@en-de",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 89.4,
+ "phase4_score": 100.0,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "MBartForConditionalGeneration",
"model_id": "facebook/mbart-large-50",
"status": 1,
- "verified_date": "2026-07-07",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification completed",
+ "prompt_profile": "task:denoise",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 100.0,
+ "phase4_score": 87.1,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "MBartForConditionalGeneration",
"model_id": "facebook/mbart-large-cc25",
"status": 1,
- "verified_date": "2026-07-07",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification passed, but text quality poor (P4=25.0). Needs review",
+ "prompt_profile": "task:denoise",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 92.4,
+ "phase4_score": 25.0,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "MBartForConditionalGeneration",
@@ -183283,11 +183358,11 @@
"status": 1,
"verified_date": "2026-07-07",
"metadata": null,
- "note": "Full verification completed with issues, low text quality",
+ "note": "P4 nulled: prior score measured under a broken MBart profile (mis-profiled + unconstrained target language); awaiting Indic denoise prompt coverage",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 32.5,
+ "phase4_score": null,
"phase7_score": null,
"phase8_score": null
},
@@ -183309,15 +183384,18 @@
"architecture_id": "PegasusForConditionalGeneration",
"model_id": "google/pegasus-xsum",
"status": 1,
- "verified_date": "2026-07-24",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed",
+ "note": "Core verification completed",
+ "prompt_profile": "task:summarization",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 100.0,
+ "phase4_score": 90.0,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "PegasusForConditionalGeneration",
@@ -183465,7 +183543,7 @@
"status": 3,
"verified_date": "2026-07-07",
"metadata": null,
- "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=27.217707, mean_rel=1.057937",
+ "note": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=27.217707, mean_rel=1.057937",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": 94.7,
@@ -183645,7 +183723,7 @@
"architecture_id": "Starcoder2ForCausalLM",
"model_id": "bigcode/starcoder2-3b",
"status": 1,
- "verified_date": "2026-07-23",
+ "verified_date": "2026-08-20",
"metadata": null,
"note": "Full verification completed",
"phase1_score": 100.0,
@@ -183653,7 +183731,8 @@
"phase3_score": 100.0,
"phase4_score": 95.3,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "Starcoder2ForCausalLM",
@@ -183841,15 +183920,18 @@
"architecture_id": "LongT5ForConditionalGeneration",
"model_id": "google/long-t5-tglobal-base",
"status": 1,
- "verified_date": "2026-07-24",
+ "verified_date": "2026-08-21",
"metadata": null,
- "note": "Full verification completed with issues, low text quality",
+ "note": "Core verification passed, but text quality poor (P4=48.4). Needs review",
+ "prompt_profile": "task:denoise",
+ "p4_scoring_version": 2,
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
- "phase4_score": 49.2,
+ "phase4_score": 48.4,
"phase7_score": null,
- "phase8_score": null
+ "phase8_score": null,
+ "phase9_score": null
},
{
"architecture_id": "LongT5ForConditionalGeneration",
@@ -183857,7 +183939,7 @@
"status": 3,
"verified_date": "2026-07-07",
"metadata": null,
- "note": "Checkpoint ships no lm_head.weight with tie_word_embeddings=false, so HF randomly re-initializes the LM head on every load (Google released it as a pretraining artifact requiring fine-tuning); the nondeterministic unembed is the only P1 failure \u2014 all 233 other components including the local-attention encoder pass. Not an adapter bug.",
+ "note": "Checkpoint ships no lm_head.weight with tie_word_embeddings=false, so HF randomly re-initializes the LM head on every load (Google released it as a pretraining artifact requiring fine-tuning); the nondeterministic unembed is the only P1 failure — all 233 other components including the local-attention encoder pass. Not an adapter bug.",
"phase1_score": 50.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -184025,7 +184107,7 @@
"status": 1,
"verified_date": "2026-07-07",
"metadata": null,
- "note": "Verified on a local snapshot of yujiepan/llama-4-tiny-random with text_config.attn_temperature_tuning coerced to bool \u2014 the upstream config declares it as int 4, which transformers 5.x strict config validation rejects. P7 skipped: the tiny ships no processor files. P3=95 (attention_output_centering worst_mean=0.099 on random weights). Official Scout/Maverick checkpoints registered for big-hardware verification.",
+ "note": "Verified on a local snapshot of yujiepan/llama-4-tiny-random with text_config.attn_temperature_tuning coerced to bool — the upstream config declares it as int 4, which transformers 5.x strict config validation rejects. P7 skipped: the tiny ships no processor files. P3=95 (attention_output_centering worst_mean=0.099 on random weights). Official Scout/Maverick checkpoints registered for big-hardware verification.",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": 95.0,
@@ -186545,7 +186627,7 @@
"status": 3,
"verified_date": "2026-07-07",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -187343,7 +187425,7 @@
"status": 3,
"verified_date": "2026-07-21",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -187441,7 +187523,7 @@
"status": 3,
"verified_date": "2026-07-21",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -187497,7 +187579,7 @@
"status": 3,
"verified_date": "2026-07-21",
"metadata": null,
- "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "note": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"phase1_score": 0.0,
"phase2_score": null,
"phase3_score": null,
@@ -216267,7 +216349,7 @@
"status": 1,
"verified_date": "2026-07-23",
"metadata": null,
- "note": "Full verification completed (P1 in fp32, P2/P4 in bf16 for memory). bf16 P1 is precision-bound: max_diff=0.375, mean_rel=0.022 at bf16, but the same comparison in fp32 matches within tolerance \u2014 measured, not assumed. Required a reconstruction dtype fix: FlexOlmoRotaryEmbedding returns fp32 cos/sin without casting to the input dtype, promoting the attention output to fp32 against bf16 projection weights.",
+ "note": "Full verification completed (P1 in fp32, P2/P4 in bf16 for memory). bf16 P1 is precision-bound: max_diff=0.375, mean_rel=0.022 at bf16, but the same comparison in fp32 matches within tolerance — measured, not assumed. Required a reconstruction dtype fix: FlexOlmoRotaryEmbedding returns fp32 cos/sin without casting to the input dtype, promoting the attention output to fp32 against bf16 projection weights.",
"phase1_score": 100.0,
"phase2_score": 100.0,
"phase3_score": null,
@@ -216505,7 +216587,7 @@
"status": 1,
"verified_date": "2026-07-23",
"metadata": null,
- "note": "Forward parity PROVEN: bridge byte-identical to raw HF in fp32 on identical ids (max \u0394logit 0, max \u0394log_softmax 0, 100% argmax). P4=55.9 is genuine model behavior (one hard NL prompt, diffusion sampling, weak GPT-2 judge), not a bridge defect. HF end-to-end capture requires a 4D (b,1,s,s) block attention mask.",
+ "note": "Forward parity PROVEN: bridge byte-identical to raw HF in fp32 on identical ids (max Δlogit 0, max Δlog_softmax 0, 100% argmax). P4=55.9 is genuine model behavior (one hard NL prompt, diffusion sampling, weak GPT-2 judge), not a bridge defect. HF end-to-end capture requires a 4D (b,1,s,s) block attention mask.",
"phase1_score": 100.0,
"phase2_score": null,
"phase3_score": null,
diff --git a/transformer_lens/tools/model_registry/data/verification_history.json b/transformer_lens/tools/model_registry/data/verification_history.json
index 8bbd370ca0..24dd53444e 100644
--- a/transformer_lens/tools/model_registry/data/verification_history.json
+++ b/transformer_lens/tools/model_registry/data/verification_history.json
@@ -1,5 +1,5 @@
{
- "last_updated": "2026-08-14T15:51:08.675104",
+ "last_updated": "2026-08-21T12:51:05.468344",
"records": [
{
"model_id": "Macropodus/macbert4mdcspell_v1",
@@ -2287,7 +2287,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% \u2014 No tokenizer files on HuggingFace (ValueError: Couldn't instantiate the backend tokenizer)",
+ "notes": "Below threshold: P1=0.0% < 100.0% — No tokenizer files on HuggingFace (ValueError: Couldn't instantiate the backend tokenizer)",
"invalidated": false,
"invalidation_reason": null
},
@@ -2297,7 +2297,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% \u2014 Requires bitsandbytes 8-bit quantization (ImportError: pip install -U bitsandbytes>=0.46.1)",
+ "notes": "Below threshold: P1=0.0% < 100.0% — Requires bitsandbytes 8-bit quantization (ImportError: pip install -U bitsandbytes>=0.46.1)",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -2397,7 +2397,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 47/292 components failed (14 critical, 33 high)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 47/292 components failed (14 critical, 33 high)",
"invalidated": false,
"invalidation_reason": null
},
@@ -2407,7 +2407,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.125000, mean_rel=0.033691",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.125000, mean_rel=0.033691",
"invalidated": false,
"invalidation_reason": null
},
@@ -2417,7 +2417,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.281250, mean_rel=0.051025",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.281250, mean_rel=0.051025",
"invalidated": false,
"invalidation_reason": null
},
@@ -2447,7 +2447,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -2457,7 +2457,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: 'NoneType' object has no attribute 'from_pretrained'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: 'NoneType' object has no attribute 'from_pretrained'",
"invalidated": false,
"invalidation_reason": null
},
@@ -2467,7 +2467,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -2477,7 +2477,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -2487,7 +2487,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 31/196 components failed (31 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 31/196 components failed (31 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -2497,7 +2497,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 48/76 components failed (48 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 48/76 components failed (48 critical)",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -2507,7 +2507,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.250000, mean_rel=0.045166",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.250000, mean_rel=0.045166",
"invalidated": false,
"invalidation_reason": null
},
@@ -2677,7 +2677,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene \u2014 Forward pass failed: '<' not supported between instances of 'NoneType' and 'int'",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene — Forward pass failed: '<' not supported between instances of 'NoneType' and 'int'",
"invalidated": false,
"invalidation_reason": null
},
@@ -2697,7 +2697,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene \u2014 Forward pass failed: '<' not supported between instances of 'NoneType' and 'int'",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene — Forward pass failed: '<' not supported between instances of 'NoneType' and 'int'",
"invalidated": false,
"invalidation_reason": null
},
@@ -2777,7 +2777,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/12 components failed (1 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/12 components failed (1 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -2787,7 +2787,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, critical_forward_ho \u2014 Tensors differ: max_diff=0.083040, mean_rel=0.006218",
+ "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, critical_forward_ho — Tensors differ: max_diff=0.083040, mean_rel=0.006218",
"invalidated": false,
"invalidation_reason": null
},
@@ -2837,7 +2837,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -2847,7 +2847,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Tensors differ: max_diff=0.097573, mean_rel=0.008319",
+ "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Tensors differ: max_diff=0.097573, mean_rel=0.008319",
"invalidated": false,
"invalidation_reason": null
},
@@ -2857,7 +2857,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Tensors differ: max_diff=0.129729, mean_rel=0.023225",
+ "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Tensors differ: max_diff=0.129729, mean_rel=0.023225",
"invalidated": false,
"invalidation_reason": null
},
@@ -2867,7 +2867,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=71.4% < 75.0% (failed: logits_equivalence, hook_functionality, critical_forward_ \u2014 Tensors differ: max_diff=0.286175, mean_rel=0.028925",
+ "notes": "Below threshold: P2=71.4% < 75.0% (failed: logits_equivalence, hook_functionality, critical_forward_ — Tensors differ: max_diff=0.286175, mean_rel=0.028925",
"invalidated": false,
"invalidation_reason": null
},
@@ -2887,7 +2887,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: unsloth/gemma-3-1b-it-GGUF does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: unsloth/gemma-3-1b-it-GGUF does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -2897,7 +2897,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -2937,7 +2937,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=71.4% < 75.0% (failed: logits_equivalence, loss_equivalence, critical_forward_ho \u2014 Tensors differ: max_diff=0.705528, mean_rel=0.011718",
+ "notes": "Below threshold: P2=71.4% < 75.0% (failed: logits_equivalence, loss_equivalence, critical_forward_ho — Tensors differ: max_diff=0.705528, mean_rel=0.011718",
"invalidated": false,
"invalidation_reason": null
},
@@ -2967,7 +2967,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -3037,7 +3037,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -3107,7 +3107,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=8.3% < 75.0% (failed: g \u2014 2/148 components failed (2 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=8.3% < 75.0% (failed: g — 2/148 components failed (2 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -3147,7 +3147,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: ",
"invalidated": false,
"invalidation_reason": null
},
@@ -3227,7 +3227,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -3327,7 +3327,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=1.562500, mean_rel=0.753906",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=1.562500, mean_rel=0.753906",
"invalidated": false,
"invalidation_reason": null
},
@@ -3357,7 +3357,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.250000, mean_rel=0.014526",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.250000, mean_rel=0.014526",
"invalidated": false,
"invalidation_reason": null
},
@@ -3477,7 +3477,7 @@
"verified_date": "2026-02-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: No module named 'triton'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: No module named 'triton'",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -3677,7 +3677,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -3727,7 +3727,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -3907,7 +3907,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/147 components failed (1 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/147 components failed (1 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -3947,7 +3947,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: ",
"invalidated": false,
"invalidation_reason": null
},
@@ -4077,7 +4077,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Xenova/distilgpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Xenova/distilgpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": false,
"invalidation_reason": null
},
@@ -4087,7 +4087,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Xenova/gpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Xenova/gpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": false,
"invalidation_reason": null
},
@@ -4117,7 +4117,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: optimum/gpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: optimum/gpt2 does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": false,
"invalidation_reason": null
},
@@ -4157,7 +4157,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -4167,7 +4167,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: 'NoneType' object has no attribute 'from_pretrained'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: 'NoneType' object has no attribute 'from_pretrained'",
"invalidated": false,
"invalidation_reason": null
},
@@ -4177,7 +4177,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -4187,7 +4187,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -4207,7 +4207,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 48/76 components failed (48 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 48/76 components failed (48 critical)",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -4247,7 +4247,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.002074, mean_rel=0.000409",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.002074, mean_rel=0.000409",
"invalidated": false,
"invalidation_reason": null
},
@@ -4537,7 +4537,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 8-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": false,
"invalidation_reason": null
},
@@ -4637,7 +4637,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -4717,7 +4717,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -4807,7 +4807,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -4847,7 +4847,7 @@
"verified_date": "2026-02-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -4867,7 +4867,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Loading a GPTQ quantized model requires optimum (`pip install optimum`)",
"invalidated": false,
"invalidation_reason": null
},
@@ -5027,7 +5027,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5047,7 +5047,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: unsloth/Llama-3.2-1B-Instruct-GGUF does not appear to have a file named pytorch_model.bin or model.safet",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: unsloth/Llama-3.2-1B-Instruct-GGUF does not appear to have a file named pytorch_model.bin or model.safet",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5077,7 +5077,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5087,7 +5087,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5117,7 +5117,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5127,7 +5127,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5177,7 +5177,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5197,7 +5197,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5257,7 +5257,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5327,7 +5327,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5357,7 +5357,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5377,7 +5377,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: unsloth/DeepSeek-R1-Distill-Qwen-1.5B-GGUF does not appear to have a file named pytorch_model.bin or mod",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: unsloth/DeepSeek-R1-Distill-Qwen-1.5B-GGUF does not appear to have a file named pytorch_model.bin or mod",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5407,7 +5407,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5447,7 +5447,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 130/132 components failed (125 high, 5 medium)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 130/132 components failed (125 high, 5 medium)",
"invalidated": false,
"invalidation_reason": null
},
@@ -5457,7 +5457,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.002012, mean_rel=0.000401",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.002012, mean_rel=0.000401",
"invalidated": false,
"invalidation_reason": null
},
@@ -5597,7 +5597,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: extra_special_tokens must be a list/tuple of str or AddedToken, or a dict mapping names to tokens",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: extra_special_tokens must be a list/tuple of str or AddedToken, or a dict mapping names to tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -5607,7 +5607,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.004045, mean_rel=0.000066",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.004045, mean_rel=0.000066",
"invalidated": false,
"invalidation_reason": null
},
@@ -5617,7 +5617,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.003122, mean_rel=0.000469",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.003122, mean_rel=0.000469",
"invalidated": false,
"invalidation_reason": null
},
@@ -5687,7 +5687,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.011660, mean_rel=0.002703",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.011660, mean_rel=0.002703",
"invalidated": false,
"invalidation_reason": null
},
@@ -5797,7 +5797,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: No module named 'triton'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: No module named 'triton'",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5827,7 +5827,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5857,7 +5857,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: No module named 'triton'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: No module named 'triton'",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5867,7 +5867,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5877,7 +5877,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: No module named 'triton'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: No module named 'triton'",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5917,7 +5917,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: unsloth/Qwen3-0.6B-GGUF does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: unsloth/Qwen3-0.6B-GGUF does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5937,7 +5937,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5947,7 +5947,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5957,7 +5957,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: unsloth/Qwen3-4B-GGUF does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: unsloth/Qwen3-4B-GGUF does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5967,7 +5967,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5977,7 +5977,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5987,7 +5987,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -5997,7 +5997,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: unsloth/Qwen3-1.7B-GGUF does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: unsloth/Qwen3-1.7B-GGUF does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6007,7 +6007,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6017,7 +6017,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6027,7 +6027,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6037,7 +6037,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6047,7 +6047,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6057,7 +6057,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6067,7 +6067,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6077,7 +6077,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6087,7 +6087,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6097,7 +6097,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: No module named 'triton'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: No module named 'triton'",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6117,7 +6117,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6127,7 +6127,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6137,7 +6137,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6147,7 +6147,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6157,7 +6157,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6177,7 +6177,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6197,7 +6197,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6207,7 +6207,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6217,7 +6217,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br \u2014 Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed, load_bridge_unprocessed, load_br — Failed to load unprocessed TransformerBridge: compressed_tensors is not installed and is required for compressed-tensors quantization. Please install ",
"invalidated": true,
"invalidation_reason": "TransformerLens does not support quantized models at this time"
},
@@ -6227,7 +6227,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 1/130 components failed (1 high)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 1/130 components failed (1 high)",
"invalidated": false,
"invalidation_reason": null
},
@@ -6237,7 +6237,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 1/130 components failed (1 high)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 1/130 components failed (1 high)",
"invalidated": false,
"invalidation_reason": null
},
@@ -6247,7 +6247,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 1/130 components failed (1 high)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 1/130 components failed (1 high)",
"invalidated": false,
"invalidation_reason": null
},
@@ -6317,7 +6317,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=71.4% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_registry, hook \u2014 Bridge is missing 56 hooks from reference model",
+ "notes": "Below threshold: P3=71.4% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_registry, hook — Bridge is missing 56 hooks from reference model",
"invalidated": false,
"invalidation_reason": null
},
@@ -6347,7 +6347,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=71.4% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_registry, hook \u2014 Bridge is missing 56 hooks from reference model",
+ "notes": "Below threshold: P3=71.4% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_registry, hook — Bridge is missing 56 hooks from reference model",
"invalidated": false,
"invalidation_reason": null
},
@@ -6707,7 +6707,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=85.0% but required tests failed: logits_equivalence \u2014 Found 1 significant mismatches in critical hooks",
+ "notes": "Below threshold: P3=85.0% but required tests failed: logits_equivalence — Found 1 significant mismatches in critical hooks",
"invalidated": false,
"invalidation_reason": null
},
@@ -6767,7 +6767,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=nan, mean_rel=nan",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=nan, mean_rel=nan",
"invalidated": false,
"invalidation_reason": null
},
@@ -6807,7 +6807,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence \u2014 Tensors differ: max_diff=28.810717, mean_rel=73.159515",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence — Tensors differ: max_diff=28.810717, mean_rel=73.159515",
"invalidated": false,
"invalidation_reason": null
},
@@ -7077,7 +7077,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence \u2014 Tensors differ: max_diff=28.810717, mean_rel=73.159515",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence — Tensors differ: max_diff=28.810717, mean_rel=73.159515",
"invalidated": false,
"invalidation_reason": null
},
@@ -7217,7 +7217,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=81.8% but required tests failed: logits_equivalence \u2014 Scalars differ: 0.000000 vs -0.015625",
+ "notes": "Below threshold: P3=81.8% but required tests failed: logits_equivalence — Scalars differ: 0.000000 vs -0.015625",
"invalidated": false,
"invalidation_reason": null
},
@@ -7237,7 +7237,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=71.4% < 75.0% (failed: loss_equivalence, hook_functionality, critical_forward_ho \u2014 Scalars differ: 5.875000 vs 5.812500",
+ "notes": "Below threshold: P2=71.4% < 75.0% (failed: loss_equivalence, hook_functionality, critical_forward_ho — Scalars differ: 5.875000 vs 5.812500",
"invalidated": false,
"invalidation_reason": null
},
@@ -7247,7 +7247,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=71.4% < 75.0% (failed: loss_equivalence, hook_functionality, critical_forward_ho \u2014 Scalars differ: 5.875000 vs 5.812500",
+ "notes": "Below threshold: P2=71.4% < 75.0% (failed: loss_equivalence, hook_functionality, critical_forward_ho — Scalars differ: 5.875000 vs 5.812500",
"invalidated": false,
"invalidation_reason": null
},
@@ -7267,7 +7267,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=71.4% < 75.0% (failed: loss_equivalence, hook_functionality, critical_forward_ho \u2014 Scalars differ: 5.968750 vs 5.875000",
+ "notes": "Below threshold: P2=71.4% < 75.0% (failed: loss_equivalence, hook_functionality, critical_forward_ho — Scalars differ: 5.968750 vs 5.875000",
"invalidated": false,
"invalidation_reason": null
},
@@ -7287,7 +7287,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P4=0.0% < 50.0% (failed: text_quality) \u2014 Text quality score: 77.3/100 (avg perplexity: 30.4) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P4=0.0% < 50.0% (failed: text_quality) — Text quality score: 77.3/100 (avg perplexity: 30.4) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7297,7 +7297,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P4=0.0% < 50.0% (failed: text_quality) \u2014 Text quality score: 77.3/100 (avg perplexity: 30.4) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P4=0.0% < 50.0% (failed: text_quality) — Text quality score: 77.3/100 (avg perplexity: 30.4) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7317,7 +7317,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P4=4.1% < 50.0% (failed: text_quality) \u2014 Text quality score: 4.1/100 (avg perplexity: 3.4) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P4=4.1% < 50.0% (failed: text_quality) — Text quality score: 4.1/100 (avg perplexity: 3.4) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7337,7 +7337,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence \u2014 Text quality score: 68.7/100 (avg perplexity: 776.1) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence — Text quality score: 68.7/100 (avg perplexity: 776.1) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7347,7 +7347,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence \u2014 Text quality score: 68.7/100 (avg perplexity: 776.1) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence — Text quality score: 68.7/100 (avg perplexity: 776.1) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7357,7 +7357,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence \u2014 Text quality score: 68.7/100 (avg perplexity: 776.1) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence — Text quality score: 68.7/100 (avg perplexity: 776.1) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7367,7 +7367,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence; P4=6.8% < 50.0% (failed: te \u2014 Text quality score: 6.8/100 (avg perplexity: 372419.9) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence; P4=6.8% < 50.0% (failed: te — Text quality score: 6.8/100 (avg perplexity: 372419.9) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7377,7 +7377,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 77.0/100 (avg perplexity: 327.9) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 77.0/100 (avg perplexity: 327.9) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7387,7 +7387,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 68.8/100 (avg perplexity: 743.0) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 68.8/100 (avg perplexity: 743.0) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7397,7 +7397,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=3.625000, mean_rel=0.024780",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=3.625000, mean_rel=0.024780",
"invalidated": false,
"invalidation_reason": null
},
@@ -7407,7 +7407,7 @@
"verified_date": "2026-02-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Can't load the model for 'EleutherAI/pythia-410m-seed1'. If you were trying to load it from 'https://hug",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Can't load the model for 'EleutherAI/pythia-410m-seed1'. If you were trying to load it from 'https://hug",
"invalidated": false,
"invalidation_reason": null
},
@@ -7437,7 +7437,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.002956, mean_rel=0.000962",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.002956, mean_rel=0.000962",
"invalidated": false,
"invalidation_reason": null
},
@@ -7457,7 +7457,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.002956, mean_rel=0.000962",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.002956, mean_rel=0.000962",
"invalidated": false,
"invalidation_reason": null
},
@@ -7577,7 +7577,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=76.2% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=13.293901, mean_rel=32.253456",
+ "notes": "Below threshold: P3=76.2% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=13.293901, mean_rel=32.253456",
"invalidated": false,
"invalidation_reason": null
},
@@ -7627,7 +7627,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.5% but required tests failed: logits_equivalence \u2014 Found 1 significant mismatches in critical hooks",
+ "notes": "Below threshold: P3=90.5% but required tests failed: logits_equivalence — Found 1 significant mismatches in critical hooks",
"invalidated": false,
"invalidation_reason": null
},
@@ -7637,7 +7637,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=94.7% but required tests failed: logits_equivalence \u2014 Text quality score: 77.5/100 (avg perplexity: 372.2) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=94.7% but required tests failed: logits_equivalence — Text quality score: 77.5/100 (avg perplexity: 372.2) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -7647,7 +7647,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=94.7% but required tests failed: log \u2014 2/10 components failed (2 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=94.7% but required tests failed: log — 2/10 components failed (2 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -7697,7 +7697,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=12.128962, mean_rel=0.271985",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=12.128962, mean_rel=0.271985",
"invalidated": false,
"invalidation_reason": null
},
@@ -7737,7 +7737,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence \u2014 Tensors differ: max_diff=28.810719, mean_rel=60.400272",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence — Tensors differ: max_diff=28.810719, mean_rel=60.400272",
"invalidated": false,
"invalidation_reason": null
},
@@ -7767,7 +7767,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Tensors differ: max_diff=19.425457, mean_rel=11.940315",
+ "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Tensors differ: max_diff=19.425457, mean_rel=11.940315",
"invalidated": false,
"invalidation_reason": null
},
@@ -7787,7 +7787,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=58.8% < 75.0% (failed: weight_modification, hook_functionality, run_with_cache, \u2014 Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
+ "notes": "Below threshold: P3=58.8% < 75.0% (failed: weight_modification, hook_functionality, run_with_cache, — Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
"invalidated": false,
"invalidation_reason": null
},
@@ -7797,7 +7797,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=25.509125, mean_rel=0.521523",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=25.509125, mean_rel=0.521523",
"invalidated": false,
"invalidation_reason": null
},
@@ -7807,7 +7807,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=76.2% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=21.740696, mean_rel=13.788611",
+ "notes": "Below threshold: P3=76.2% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=21.740696, mean_rel=13.788611",
"invalidated": false,
"invalidation_reason": null
},
@@ -7847,7 +7847,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.002039, mean_rel=0.000401",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.002039, mean_rel=0.000401",
"invalidated": false,
"invalidation_reason": null
},
@@ -7947,7 +7947,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=12.128962, mean_rel=0.271985",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=12.128962, mean_rel=0.271985",
"invalidated": false,
"invalidation_reason": null
},
@@ -8007,7 +8007,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=94.7% but required tests failed: logits_equivalence \u2014 Text quality score: 77.5/100 (avg perplexity: 372.2) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=94.7% but required tests failed: logits_equivalence — Text quality score: 77.5/100 (avg perplexity: 372.2) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -8027,7 +8027,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.002039, mean_rel=0.000401",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.002039, mean_rel=0.000401",
"invalidated": false,
"invalidation_reason": null
},
@@ -8047,7 +8047,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=58.8% < 75.0% (failed: weight_modification, hook_functionality, run_with_cache, \u2014 Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
+ "notes": "Below threshold: P3=58.8% < 75.0% (failed: weight_modification, hook_functionality, run_with_cache, — Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
"invalidated": false,
"invalidation_reason": null
},
@@ -8067,7 +8067,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.002956, mean_rel=0.000962",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.002956, mean_rel=0.000962",
"invalidated": false,
"invalidation_reason": null
},
@@ -8077,7 +8077,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.002956, mean_rel=0.000962",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.002956, mean_rel=0.000962",
"invalidated": false,
"invalidation_reason": null
},
@@ -8287,7 +8287,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=76.2% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=13.293901, mean_rel=32.253456",
+ "notes": "Below threshold: P3=76.2% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=13.293901, mean_rel=32.253456",
"invalidated": false,
"invalidation_reason": null
},
@@ -8337,7 +8337,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.5% but required tests failed: logits_equivalence \u2014 Found 1 significant mismatches in critical hooks",
+ "notes": "Below threshold: P3=90.5% but required tests failed: logits_equivalence — Found 1 significant mismatches in critical hooks",
"invalidated": false,
"invalidation_reason": null
},
@@ -8357,7 +8357,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=94.7% but required tests failed: log \u2014 2/10 components failed (2 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=94.7% but required tests failed: log — 2/10 components failed (2 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -8407,7 +8407,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=50.0% < 75.0% (failed: hook_functionality, critical_forward_hooks, forward_hooks \u2014 Backward hooks check failed: 'tuple' object has no attribute 'clone'",
+ "notes": "Below threshold: P3=50.0% < 75.0% (failed: hook_functionality, critical_forward_hooks, forward_hooks — Backward hooks check failed: 'tuple' object has no attribute 'clone'",
"invalidated": false,
"invalidation_reason": null
},
@@ -8417,7 +8417,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=50.0% < 75.0% (failed: hook_functionality, critical_forward_hooks, forward_hooks \u2014 Backward hooks check failed: 'tuple' object has no attribute 'clone'",
+ "notes": "Below threshold: P3=50.0% < 75.0% (failed: hook_functionality, critical_forward_hooks, forward_hooks — Backward hooks check failed: 'tuple' object has no attribute 'clone'",
"invalidated": false,
"invalidation_reason": null
},
@@ -8447,7 +8447,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence \u2014 Tensors differ: max_diff=28.810719, mean_rel=60.400272",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence — Tensors differ: max_diff=28.810719, mean_rel=60.400272",
"invalidated": false,
"invalidation_reason": null
},
@@ -8477,7 +8477,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Tensors differ: max_diff=19.425457, mean_rel=11.940315",
+ "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Tensors differ: max_diff=19.425457, mean_rel=11.940315",
"invalidated": false,
"invalidation_reason": null
},
@@ -8507,7 +8507,7 @@
"verified_date": "2026-03-09",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=50.0% < 75.0% (failed: hook_functionality, critical_forward_hooks, forward_hooks \u2014 Backward hooks check failed: 'tuple' object has no attribute 'clone'",
+ "notes": "Below threshold: P3=50.0% < 75.0% (failed: hook_functionality, critical_forward_hooks, forward_hooks — Backward hooks check failed: 'tuple' object has no attribute 'clone'",
"invalidated": false,
"invalidation_reason": null
},
@@ -8687,7 +8687,7 @@
"verified_date": "2026-03-10",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=94.7% but required tests failed: log \u2014 2/10 components failed (2 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=94.7% but required tests failed: log — 2/10 components failed (2 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -8707,7 +8707,7 @@
"verified_date": "2026-03-10",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log \u2014 1/149 components failed (1 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log — 1/149 components failed (1 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -8727,7 +8727,7 @@
"verified_date": "2026-03-10",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=18.423609, mean_rel=0.259477",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=18.423609, mean_rel=0.259477",
"invalidated": false,
"invalidation_reason": null
},
@@ -8927,7 +8927,7 @@
"verified_date": "2026-03-10",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=76.2% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=13.293901, mean_rel=32.253456",
+ "notes": "Below threshold: P3=76.2% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=13.293901, mean_rel=32.253456",
"invalidated": false,
"invalidation_reason": null
},
@@ -8977,7 +8977,7 @@
"verified_date": "2026-03-10",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.5% but required tests failed: logits_equivalence \u2014 Found 1 significant mismatches in critical hooks",
+ "notes": "Below threshold: P3=90.5% but required tests failed: logits_equivalence — Found 1 significant mismatches in critical hooks",
"invalidated": false,
"invalidation_reason": null
},
@@ -9087,7 +9087,7 @@
"verified_date": "2026-03-10",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence \u2014 Tensors differ: max_diff=28.810719, mean_rel=60.400272",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence — Tensors differ: max_diff=28.810719, mean_rel=60.400272",
"invalidated": false,
"invalidation_reason": null
},
@@ -9117,7 +9117,7 @@
"verified_date": "2026-03-10",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Tensors differ: max_diff=19.425457, mean_rel=11.940315",
+ "notes": "Below threshold: P2=64.3% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Tensors differ: max_diff=19.425457, mean_rel=11.940315",
"invalidated": false,
"invalidation_reason": null
},
@@ -9137,7 +9137,7 @@
"verified_date": "2026-03-10",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=58.8% < 75.0% (failed: weight_modification, hook_functionality, run_with_cache, \u2014 Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
+ "notes": "Below threshold: P3=58.8% < 75.0% (failed: weight_modification, hook_functionality, run_with_cache, — Critical backward hooks check failed: Output 0 of BackwardHookFunctionBackward is a view and is being modified inplace. This view was created inside a",
"invalidated": false,
"invalidation_reason": null
},
@@ -9627,7 +9627,7 @@
"verified_date": "2026-03-11",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
"invalidated": false,
"invalidation_reason": null
},
@@ -9637,7 +9637,7 @@
"verified_date": "2026-03-11",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: onnx-community/gemma-3-270m-it-ONNX does not appear to have a file named pytorch_model.bin or model.safe",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: onnx-community/gemma-3-270m-it-ONNX does not appear to have a file named pytorch_model.bin or model.safe",
"invalidated": false,
"invalidation_reason": null
},
@@ -9787,7 +9787,7 @@
"verified_date": "2026-03-11",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 104/242 components failed (104 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 104/242 components failed (104 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -9997,7 +9997,7 @@
"verified_date": "2026-03-11",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Using `bitsandbytes` 4-bit quantization requires bitsandbytes: `pip install -U bitsandbytes>=0.46.1`",
"invalidated": false,
"invalidation_reason": null
},
@@ -10137,7 +10137,7 @@
"verified_date": "2026-03-11",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Ex0bit/Elbaz-Olmo-3-7B-Instruct-abliterated does not appear to have files named ('model-00001-of-00006.s",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Ex0bit/Elbaz-Olmo-3-7B-Instruct-abliterated does not appear to have files named ('model-00001-of-00006.s",
"invalidated": false,
"invalidation_reason": null
},
@@ -10367,7 +10367,7 @@
"verified_date": "2026-03-19",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 3/197 components failed (3 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 3/197 components failed (3 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -10567,7 +10567,7 @@
"verified_date": "2026-03-27",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.1% < 75.0% (failed: generation, gene \u2014 Forward pass failed: 'PhiAttention' object has no attribute 'o_proj'",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.1% < 75.0% (failed: generation, gene — Forward pass failed: 'PhiAttention' object has no attribute 'o_proj'",
"invalidated": false,
"invalidation_reason": null
},
@@ -10577,7 +10577,7 @@
"verified_date": "2026-03-27",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.1% < 75.0% (failed: generation, gene \u2014 Forward pass failed: 'PhiAttention' object has no attribute 'o_proj'",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.1% < 75.0% (failed: generation, gene — Forward pass failed: 'PhiAttention' object has no attribute 'o_proj'",
"invalidated": false,
"invalidation_reason": null
},
@@ -10607,7 +10607,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 64/163 components failed (64 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 64/163 components failed (64 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -10617,7 +10617,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 64/163 components failed (64 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 64/163 components failed (64 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -10627,7 +10627,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 64/163 components failed (64 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 64/163 components failed (64 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -10637,7 +10637,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 64/163 components failed (64 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 64/163 components failed (64 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -10647,7 +10647,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gener \u2014 Forward pass failed: shape '[1, 28, 24, 71]' is invalid for input of size 47796",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gener — Forward pass failed: shape '[1, 28, 24, 71]' is invalid for input of size 47796",
"invalidated": false,
"invalidation_reason": null
},
@@ -10697,7 +10697,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene \u2014 Forward pass failed: shape '[1, 28, 24, 71]' is invalid for input of size 47796",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene — Forward pass failed: shape '[1, 28, 24, 71]' is invalid for input of size 47796",
"invalidated": false,
"invalidation_reason": null
},
@@ -10717,7 +10717,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 64/163 components failed (64 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 64/163 components failed (64 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -10737,7 +10737,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene \u2014 Forward pass failed: shape '[1, 28, 24, 71]' is invalid for input of size 47796",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene — Forward pass failed: shape '[1, 28, 24, 71]' is invalid for input of size 47796",
"invalidated": false,
"invalidation_reason": null
},
@@ -10747,7 +10747,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene \u2014 Forward pass failed: shape '[1, 28, 24, 128]' is invalid for input of size 28672",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=8.3% < 75.0% (failed: generation, gene — Forward pass failed: shape '[1, 28, 24, 128]' is invalid for input of size 28672",
"invalidated": false,
"invalidation_reason": null
},
@@ -10767,7 +10767,7 @@
"verified_date": "2026-03-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: unsupported operand type(s) for *: 'NoneType' and 'int'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: unsupported operand type(s) for *: 'NoneType' and 'int'",
"invalidated": false,
"invalidation_reason": null
},
@@ -11067,7 +11067,7 @@
"verified_date": "2026-04-02",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.004379, mean_rel=0.022909",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.004379, mean_rel=0.022909",
"invalidated": false,
"invalidation_reason": null
},
@@ -11147,7 +11147,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 6/32 components failed (6 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 6/32 components failed (6 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11157,7 +11157,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 6/32 components failed (6 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 6/32 components failed (6 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11167,7 +11167,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 5/32 components failed (4 critical, 1 medium)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 5/32 components failed (4 critical, 1 medium)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11177,7 +11177,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 4/32 components failed (4 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 4/32 components failed (4 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11217,7 +11217,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 18/32 components failed (18 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 18/32 components failed (18 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11227,7 +11227,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load trl-internal-testing/tiny-DeepseekV3ForCausalL",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load trl-internal-testing/tiny-DeepseekV3ForCausalL",
"invalidated": false,
"invalidation_reason": null
},
@@ -11237,7 +11237,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load katuni4ka/tiny-random-deepseek-v3:\n(Request ID",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load katuni4ka/tiny-random-deepseek-v3:\n(Request ID",
"invalidated": false,
"invalidation_reason": null
},
@@ -11247,7 +11247,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load optimum-intel-internal-testing/tiny-random-dee",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: There was a specific connection error when trying to load optimum-intel-internal-testing/tiny-random-dee",
"invalidated": false,
"invalidation_reason": null
},
@@ -11257,7 +11257,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 18/32 components failed (18 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 18/32 components failed (18 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11267,7 +11267,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 18/32 components failed (18 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 18/32 components failed (18 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11277,7 +11277,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 18/32 components failed (18 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 18/32 components failed (18 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11287,7 +11287,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 18/32 components failed (18 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 18/32 components failed (18 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11297,7 +11297,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 18/32 components failed (18 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 18/32 components failed (18 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11317,7 +11317,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 10/28 components failed (10 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 10/28 components failed (10 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11337,7 +11337,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 4/22 components failed (4 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 4/22 components failed (4 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11347,7 +11347,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 4/22 components failed (4 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 4/22 components failed (4 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11357,7 +11357,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 10/24 components failed (10 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 10/24 components failed (10 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11367,7 +11367,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 10/24 components failed (10 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 10/24 components failed (10 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11377,7 +11377,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 10/24 components failed (10 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 10/24 components failed (10 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11397,7 +11397,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/16 components failed (1 medium)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/16 components failed (1 medium)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11437,7 +11437,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/16 components failed (1 medium)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/16 components failed (1 medium)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11457,7 +11457,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 2/12 components failed (2 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 2/12 components failed (2 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11467,7 +11467,7 @@
"verified_date": "2026-04-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 2/12 components failed (2 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 2/12 components failed (2 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11737,7 +11737,7 @@
"verified_date": "2026-04-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: This modeling file requires the following packages that were not found in your environment: bitsandbytes",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: This modeling file requires the following packages that were not found in your environment: bitsandbytes",
"invalidated": false,
"invalidation_reason": null
},
@@ -11747,7 +11747,7 @@
"verified_date": "2026-04-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: This modeling file requires the following packages that were not found in your environment: bitsandbytes",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: This modeling file requires the following packages that were not found in your environment: bitsandbytes",
"invalidated": false,
"invalidation_reason": null
},
@@ -11757,7 +11757,7 @@
"verified_date": "2026-04-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=74.608353, mean_rel=1.619285",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=74.608353, mean_rel=1.619285",
"invalidated": false,
"invalidation_reason": null
},
@@ -11767,7 +11767,7 @@
"verified_date": "2026-04-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=78.619270, mean_rel=1.866265",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=78.619270, mean_rel=1.866265",
"invalidated": false,
"invalidation_reason": null
},
@@ -11777,7 +11777,7 @@
"verified_date": "2026-04-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=nan, mean_rel=nan",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=nan, mean_rel=nan",
"invalidated": false,
"invalidation_reason": null
},
@@ -11787,7 +11787,7 @@
"verified_date": "2026-04-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=33.073044, mean_rel=0.316714",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=33.073044, mean_rel=0.316714",
"invalidated": false,
"invalidation_reason": null
},
@@ -11797,7 +11797,7 @@
"verified_date": "2026-04-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=33.073044, mean_rel=0.316714",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=33.073044, mean_rel=0.316714",
"invalidated": false,
"invalidation_reason": null
},
@@ -11807,7 +11807,7 @@
"verified_date": "2026-04-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation \u2014 Generation failed: 'NoneType' object is not subscriptable",
+ "notes": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation — Generation failed: 'NoneType' object is not subscriptable",
"invalidated": false,
"invalidation_reason": null
},
@@ -11827,7 +11827,7 @@
"verified_date": "2026-04-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.7% < 75.0% (failed: generation, gene \u2014 Forward pass failed: Cannot copy out of meta tensor; no data!",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.7% < 75.0% (failed: generation, gene — Forward pass failed: Cannot copy out of meta tensor; no data!",
"invalidated": false,
"invalidation_reason": null
},
@@ -11857,7 +11857,7 @@
"verified_date": "2026-05-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/190 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/190 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11867,7 +11867,7 @@
"verified_date": "2026-05-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/190 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/190 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -11877,7 +11877,7 @@
"verified_date": "2026-05-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/558 components failed (72 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/558 components failed (72 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12277,7 +12277,7 @@
"verified_date": "2026-06-04",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12307,7 +12307,7 @@
"verified_date": "2026-06-04",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12337,7 +12337,7 @@
"verified_date": "2026-06-05",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence \u2014 Weight magnitude issues: 1 too large",
+ "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence — Weight magnitude issues: 1 too large",
"invalidated": false,
"invalidation_reason": null
},
@@ -12347,7 +12347,7 @@
"verified_date": "2026-06-05",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence \u2014 Weight magnitude issues: 1 too large",
+ "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence — Weight magnitude issues: 1 too large",
"invalidated": false,
"invalidation_reason": null
},
@@ -12447,7 +12447,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/211 components failed (1 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/211 components failed (1 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12467,7 +12467,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 9/167 components failed (9 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 9/167 components failed (9 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12487,7 +12487,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12497,7 +12497,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12507,7 +12507,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12517,7 +12517,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12527,7 +12527,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12537,7 +12537,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12547,7 +12547,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12557,7 +12557,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12567,7 +12567,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12577,7 +12577,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12587,7 +12587,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12597,7 +12597,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: I/O error: IO Error: No space left on device (os error 28)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12657,7 +12657,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 8/167 components failed (8 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 8/167 components failed (8 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12667,7 +12667,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 8/167 components failed (8 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 8/167 components failed (8 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12677,7 +12677,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 8/167 components failed (8 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 8/167 components failed (8 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12687,7 +12687,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/247 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/247 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12697,7 +12697,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/247 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/247 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12707,7 +12707,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/247 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/247 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12717,7 +12717,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12727,7 +12727,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12737,7 +12737,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12747,7 +12747,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12757,7 +12757,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12767,7 +12767,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12777,7 +12777,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12787,7 +12787,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12797,7 +12797,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12807,7 +12807,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12817,7 +12817,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/487 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/487 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12827,7 +12827,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12837,7 +12837,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 26/527 components failed (26 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 26/527 components failed (26 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -12877,7 +12877,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: MachadoDeCastro/krull-micro does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: MachadoDeCastro/krull-micro does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": false,
"invalidation_reason": null
},
@@ -12907,7 +12907,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Shape mismatch: torch.Size([1, 40, 28996]) vs torch.Size([1, 32, 28996])",
+ "notes": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Shape mismatch: torch.Size([1, 40, 28996]) vs torch.Size([1, 32, 28996])",
"invalidated": false,
"invalidation_reason": null
},
@@ -12917,7 +12917,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=9.275972, mean_rel=13.166794",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=9.275972, mean_rel=13.166794",
"invalidated": false,
"invalidation_reason": null
},
@@ -13057,7 +13057,7 @@
"verified_date": "2026-06-25",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -13197,7 +13197,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: [Errno 2] No such file or directory: 'baichuan-inc/Baichuan2-7B-Chat-4bits/pytorch_model.bin'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: [Errno 2] No such file or directory: 'baichuan-inc/Baichuan2-7B-Chat-4bits/pytorch_model.bin'",
"invalidated": false,
"invalidation_reason": null
},
@@ -13217,7 +13217,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Shape mismatch: torch.Size([1, 38, 30522]) vs torch.Size([1, 30, 30522])",
+ "notes": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Shape mismatch: torch.Size([1, 38, 30522]) vs torch.Size([1, 30, 30522])",
"invalidated": false,
"invalidation_reason": null
},
@@ -13227,7 +13227,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Shape mismatch: torch.Size([1, 40, 28996]) vs torch.Size([1, 32, 28996])",
+ "notes": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Shape mismatch: torch.Size([1, 40, 28996]) vs torch.Size([1, 32, 28996])",
"invalidated": false,
"invalidation_reason": null
},
@@ -13247,7 +13247,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, \u2014 Shape mismatch: torch.Size([1, 38, 30522]) vs torch.Size([1, 30, 30522])",
+ "notes": "Below threshold: P2=66.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Shape mismatch: torch.Size([1, 38, 30522]) vs torch.Size([1, 30, 30522])",
"invalidated": false,
"invalidation_reason": null
},
@@ -13277,7 +13277,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -13287,7 +13287,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -13297,7 +13297,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -13307,7 +13307,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -13317,7 +13317,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P2=69.2% < 75.0% (f \u2014 59/64 components failed (59 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P2=69.2% < 75.0% (f — 59/64 components failed (59 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -13407,7 +13407,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=0.009886, mean_rel=0.980186",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=0.009886, mean_rel=0.980186",
"invalidated": false,
"invalidation_reason": null
},
@@ -13417,7 +13417,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -13427,7 +13427,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -13447,7 +13447,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=65.931717, mean_rel=2.066483",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=65.931717, mean_rel=2.066483",
"invalidated": false,
"invalidation_reason": null
},
@@ -13457,7 +13457,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=22.915417, mean_rel=11.391559",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=22.915417, mean_rel=11.391559",
"invalidated": false,
"invalidation_reason": null
},
@@ -13477,7 +13477,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.003898, mean_rel=0.027017",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.003898, mean_rel=0.027017",
"invalidated": false,
"invalidation_reason": null
},
@@ -13577,7 +13577,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 4/24 components failed (4 high)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 4/24 components failed (4 high)",
"invalidated": false,
"invalidation_reason": null
},
@@ -13667,7 +13667,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
"invalidated": false,
"invalidation_reason": null
},
@@ -13817,7 +13817,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation \u2014 Generated text has no new tokens",
+ "notes": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation — Generated text has no new tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -13837,7 +13837,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation \u2014 Generated text has no new tokens",
+ "notes": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation — Generated text has no new tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -13847,7 +13847,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=38.5% < 75.0% (failed: \u2014 1/15 components failed (1 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=38.5% < 75.0% (failed: — 1/15 components failed (1 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -13867,7 +13867,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: split_with_sizes expects split_sizes to sum exactly to 1152 (input tensor's size at dimension 0), but go",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: split_with_sizes expects split_sizes to sum exactly to 1152 (input tensor's size at dimension 0), but go",
"invalidated": false,
"invalidation_reason": null
},
@@ -14077,7 +14077,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: \u2014 12/87 components failed (12 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: — 12/87 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14107,7 +14107,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: \u2014 12/87 components failed (12 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: — 12/87 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14157,7 +14157,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: \u2014 144/196 components failed (144 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=15.4% < 75.0% (failed: — 144/196 components failed (144 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14287,7 +14287,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=59.411900, mean_rel=0.569168",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=59.411900, mean_rel=0.569168",
"invalidated": false,
"invalidation_reason": null
},
@@ -14297,7 +14297,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=59.402462, mean_rel=0.441563",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=59.402462, mean_rel=0.441563",
"invalidated": false,
"invalidation_reason": null
},
@@ -14307,7 +14307,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=33.204865, mean_rel=0.370595",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=33.204865, mean_rel=0.370595",
"invalidated": false,
"invalidation_reason": null
},
@@ -14317,7 +14317,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=32.255035, mean_rel=0.318908",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=32.255035, mean_rel=0.318908",
"invalidated": false,
"invalidation_reason": null
},
@@ -14327,7 +14327,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -14337,7 +14337,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=31.355835, mean_rel=0.607207",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=31.355835, mean_rel=0.607207",
"invalidated": false,
"invalidation_reason": null
},
@@ -14347,7 +14347,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 73.9/100 (avg perplexity: 10.2) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 73.9/100 (avg perplexity: 10.2) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -14357,7 +14357,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 83.9/100 (below 85.0, avg perplexity: 125.9)",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 83.9/100 (below 85.0, avg perplexity: 125.9)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14367,7 +14367,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 70/609 components failed (70 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 70/609 components failed (70 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14377,7 +14377,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 70/609 components failed (70 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 70/609 components failed (70 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14387,7 +14387,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 60/534 components failed (60 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 60/534 components failed (60 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14467,7 +14467,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
"invalidated": false,
"invalidation_reason": null
},
@@ -14477,7 +14477,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=16.391922, mean_rel=3.526243",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=16.391922, mean_rel=3.526243",
"invalidated": false,
"invalidation_reason": null
},
@@ -14487,7 +14487,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=18.059958, mean_rel=2.710044",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=18.059958, mean_rel=2.710044",
"invalidated": false,
"invalidation_reason": null
},
@@ -14517,7 +14517,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=4.489960, mean_rel=1.238444",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=4.489960, mean_rel=1.238444",
"invalidated": false,
"invalidation_reason": null
},
@@ -14527,7 +14527,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=20.307718, mean_rel=6.347236",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=20.307718, mean_rel=6.347236",
"invalidated": false,
"invalidation_reason": null
},
@@ -14567,7 +14567,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 56/243 components failed (56 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 56/243 components failed (56 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14597,7 +14597,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/307 components failed (72 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/307 components failed (72 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14607,7 +14607,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 56/243 components failed (56 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 56/243 components failed (56 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14617,7 +14617,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/307 components failed (72 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/307 components failed (72 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14627,7 +14627,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/347 components failed (72 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/347 components failed (72 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14637,7 +14637,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/347 components failed (72 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/347 components failed (72 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -14647,7 +14647,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'type'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'type'",
"invalidated": false,
"invalidation_reason": null
},
@@ -14697,7 +14697,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": false,
"invalidation_reason": null
},
@@ -14787,7 +14787,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: argument 'vocab': 'dict' object cannot be converted to 'Sequence'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: argument 'vocab': 'dict' object cannot be converted to 'Sequence'",
"invalidated": false,
"invalidation_reason": null
},
@@ -14857,7 +14857,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -14877,7 +14877,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: split_with_sizes expects split_sizes to sum exactly to 256 (input tensor's size at dimension 0), but got",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: split_with_sizes expects split_sizes to sum exactly to 256 (input tensor's size at dimension 0), but got",
"invalidated": false,
"invalidation_reason": null
},
@@ -14997,7 +14997,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.005348, mean_rel=0.000007",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.005348, mean_rel=0.000007",
"invalidated": false,
"invalidation_reason": null
},
@@ -15067,7 +15067,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=75.0% but required tests failed \u2014 Tensors differ: max_diff=378.613281, mean_rel=0.057195",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=75.0% but required tests failed — Tensors differ: max_diff=378.613281, mean_rel=0.057195",
"invalidated": false,
"invalidation_reason": null
},
@@ -15227,7 +15227,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 83.9/100 (below 85.0, avg perplexity: 125.9)",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 83.9/100 (below 85.0, avg perplexity: 125.9)",
"invalidated": false,
"invalidation_reason": null
},
@@ -15237,7 +15237,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=39.516827, mean_rel=0.391392",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=39.516827, mean_rel=0.391392",
"invalidated": false,
"invalidation_reason": null
},
@@ -15247,7 +15247,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=63.272919, mean_rel=0.497796",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=63.272919, mean_rel=0.497796",
"invalidated": false,
"invalidation_reason": null
},
@@ -15257,7 +15257,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=34.928375, mean_rel=0.262157",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=34.928375, mean_rel=0.262157",
"invalidated": false,
"invalidation_reason": null
},
@@ -15267,7 +15267,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=29.677444, mean_rel=0.238732",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=29.677444, mean_rel=0.238732",
"invalidated": false,
"invalidation_reason": null
},
@@ -15277,7 +15277,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 45.4/100 (avg perplexity: 196.5) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 45.4/100 (avg perplexity: 196.5) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -15287,7 +15287,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=50.417328, mean_rel=0.326480",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=50.417328, mean_rel=0.326480",
"invalidated": false,
"invalidation_reason": null
},
@@ -15297,7 +15297,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=34.771893, mean_rel=0.288610",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=34.771893, mean_rel=0.288610",
"invalidated": false,
"invalidation_reason": null
},
@@ -15307,7 +15307,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=61.738430, mean_rel=0.447178",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=61.738430, mean_rel=0.447178",
"invalidated": false,
"invalidation_reason": null
},
@@ -15407,7 +15407,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
"invalidated": false,
"invalidation_reason": null
},
@@ -15427,7 +15427,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=4.789991, mean_rel=1.159405",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=4.789991, mean_rel=1.159405",
"invalidated": false,
"invalidation_reason": null
},
@@ -15487,7 +15487,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: onnx-community/granite-4.0-1b-ONNX-web does not appear to have a file named pytorch_model.bin or model.s",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: onnx-community/granite-4.0-1b-ONNX-web does not appear to have a file named pytorch_model.bin or model.s",
"invalidated": false,
"invalidation_reason": null
},
@@ -15517,7 +15517,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 56/243 components failed (56 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 56/243 components failed (56 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -15527,7 +15527,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 72/307 components failed (72 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 72/307 components failed (72 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -15537,7 +15537,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=7.7% < 75.0% (failed: g \u2014 144/307 components failed (144 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=7.7% < 75.0% (failed: g — 144/307 components failed (144 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -15567,7 +15567,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": false,
"invalidation_reason": null
},
@@ -15577,7 +15577,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: You set `ignore_mismatched_sizes` to `False`, thus raising an error. For details look at the above repor",
"invalidated": false,
"invalidation_reason": null
},
@@ -15587,7 +15587,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: LiquidAI/LFM2-8B-A1B-ONNX does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: LiquidAI/LFM2-8B-A1B-ONNX does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": false,
"invalidation_reason": null
},
@@ -15597,7 +15597,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: LiquidAI/LFM2.5-8B-A1B-ONNX does not appear to have a file named pytorch_model.bin or model.safetensors.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: LiquidAI/LFM2.5-8B-A1B-ONNX does not appear to have a file named pytorch_model.bin or model.safetensors.",
"invalidated": false,
"invalidation_reason": null
},
@@ -15617,7 +15617,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Loading an AWQ quantized model requires gptqmodel. Please install it with `pip install gptqmodel`",
"invalidated": false,
"invalidation_reason": null
},
@@ -15957,7 +15957,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'MixtralDecoderLayer' object has no attribute 'block_sparse_moe'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'MixtralDecoderLayer' object has no attribute 'block_sparse_moe'",
"invalidated": false,
"invalidation_reason": null
},
@@ -15967,7 +15967,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'MixtralDecoderLayer' object has no attribute 'block_sparse_moe'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'MixtralDecoderLayer' object has no attribute 'block_sparse_moe'",
"invalidated": false,
"invalidation_reason": null
},
@@ -15977,7 +15977,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -15987,7 +15987,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=6.747103, mean_rel=0.054769",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=6.747103, mean_rel=0.054769",
"invalidated": false,
"invalidation_reason": null
},
@@ -15997,7 +15997,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -16007,7 +16007,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -16017,7 +16017,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=5.479654, mean_rel=0.052641",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=5.479654, mean_rel=0.052641",
"invalidated": false,
"invalidation_reason": null
},
@@ -16027,7 +16027,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -16037,7 +16037,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=6.650925, mean_rel=0.050073",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=6.650925, mean_rel=0.050073",
"invalidated": false,
"invalidation_reason": null
},
@@ -16047,7 +16047,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Generated text has no new tokens",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Generated text has no new tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -16057,7 +16057,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=5.641898, mean_rel=0.054789",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=5.641898, mean_rel=0.054789",
"invalidated": false,
"invalidation_reason": null
},
@@ -16067,7 +16067,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=5.808517, mean_rel=0.051374",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=5.808517, mean_rel=0.051374",
"invalidated": false,
"invalidation_reason": null
},
@@ -16307,7 +16307,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: onnx-community/tiny-random-olmo-hf does not appear to have a file named pytorch_model.bin or model.safet",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: onnx-community/tiny-random-olmo-hf does not appear to have a file named pytorch_model.bin or model.safet",
"invalidated": false,
"invalidation_reason": null
},
@@ -16327,7 +16327,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 32/68 components failed (32 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 32/68 components failed (32 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -16337,7 +16337,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
"invalidated": false,
"invalidation_reason": null
},
@@ -16347,7 +16347,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: self_attn: q_norm declared but HF module has none.",
"invalidated": false,
"invalidation_reason": null
},
@@ -16357,7 +16357,7 @@
"verified_date": "2026-06-26",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.013494, mean_rel=0.006767",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.013494, mean_rel=0.006767",
"invalidated": false,
"invalidation_reason": null
},
@@ -16367,7 +16367,7 @@
"verified_date": "2026-06-27",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=95.0% but required tests failed \u2014 Tensors differ: max_diff=0.437500, mean_rel=0.223633",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=95.0% but required tests failed — Tensors differ: max_diff=0.437500, mean_rel=0.223633",
"invalidated": false,
"invalidation_reason": null
},
@@ -16377,7 +16377,7 @@
"verified_date": "2026-06-27",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.013494, mean_rel=0.006767",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.013494, mean_rel=0.006767",
"invalidated": false,
"invalidation_reason": null
},
@@ -16387,7 +16387,7 @@
"verified_date": "2026-06-27",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=0.020484, mean_rel=0.006617",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=0.020484, mean_rel=0.006617",
"invalidated": false,
"invalidation_reason": null
},
@@ -16397,7 +16397,7 @@
"verified_date": "2026-07-01",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 96/171 components failed (96 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 96/171 components failed (96 critical)",
"invalidated": true,
"invalidation_reason": "Superseded by the clean re-run (P1=100) after the component-benchmark fix that skips SSM mixer-internal submodules; the 96/171 component failures were the isolated harness feeding d_model-shaped inputs to SSM-internal projections, not a real divergence."
},
@@ -16427,7 +16427,7 @@
"verified_date": "2026-07-01",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=94.7% but required tests failed: logits_equivalence \u2014 Tensors differ: max_diff=0.375000, mean_rel=0.002045",
+ "notes": "Below threshold: P3=94.7% but required tests failed: logits_equivalence — Tensors differ: max_diff=0.375000, mean_rel=0.002045",
"invalidated": true,
"invalidation_reason": "bf16 precision of compatibility-mode center_unembed (root-caused by toggle: off=0.000 both dtypes, fp32=4.2e-5); superseded by the clean fp32 run (P3=100). Not an algorithmic bug."
},
@@ -16547,7 +16547,7 @@
"verified_date": "2026-07-01",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 24/51 components failed (24 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 24/51 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -16577,7 +16577,7 @@
"verified_date": "2026-07-01",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) \u2014 50/99 components failed (50 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits) — 50/99 components failed (50 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -16897,7 +16897,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 2/16 components failed (2 high)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 2/16 components failed (2 high)",
"invalidated": false,
"invalidation_reason": null
},
@@ -16907,7 +16907,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 2/16 components failed (2 high)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 2/16 components failed (2 high)",
"invalidated": false,
"invalidation_reason": null
},
@@ -16977,7 +16977,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass) \u2014 Forward pass failed: Could not infer dtype of NoneType",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass) — Forward pass failed: Could not infer dtype of NoneType",
"invalidated": false,
"invalidation_reason": null
},
@@ -16987,7 +16987,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass) \u2014 Forward pass failed: Could not infer dtype of NoneType",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass) — Forward pass failed: Could not infer dtype of NoneType",
"invalidated": false,
"invalidation_reason": null
},
@@ -17007,7 +17007,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/75 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/75 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17017,7 +17017,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/75 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/75 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17027,7 +17027,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/75 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/75 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17037,7 +17037,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/75 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/75 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17067,7 +17067,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Cannot build Piece from string \":0\"",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Cannot build Piece from string \":0\"",
"invalidated": false,
"invalidation_reason": null
},
@@ -17077,7 +17077,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=95.0% but required tests failed \u2014 Tensors differ: max_diff=0.084499, mean_rel=1.582682",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=95.0% but required tests failed — Tensors differ: max_diff=0.084499, mean_rel=1.582682",
"invalidated": false,
"invalidation_reason": null
},
@@ -17087,7 +17087,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=95.0% but required tests failed \u2014 Tensors differ: max_diff=0.096940, mean_rel=2.925628",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=95.0% but required tests failed — Tensors differ: max_diff=0.096940, mean_rel=2.925628",
"invalidated": false,
"invalidation_reason": null
},
@@ -17097,7 +17097,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=0.079056, mean_rel=0.949212",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=0.079056, mean_rel=0.949212",
"invalidated": false,
"invalidation_reason": null
},
@@ -17117,7 +17117,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=30.8% < 75.0% (failed: \u2014 5/12 components failed (5 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=30.8% < 75.0% (failed: — 5/12 components failed (5 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17127,7 +17127,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=30.8% < 75.0% (failed: \u2014 5/12 components failed (5 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=30.8% < 75.0% (failed: — 5/12 components failed (5 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17147,7 +17147,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/16 components failed (1 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/16 components failed (1 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17157,7 +17157,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/16 components failed (1 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/16 components failed (1 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17177,7 +17177,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=13.567083, mean_rel=3.490963",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=13.567083, mean_rel=3.490963",
"invalidated": false,
"invalidation_reason": null
},
@@ -17187,7 +17187,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=13.567083, mean_rel=3.490963",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=13.567083, mean_rel=3.490963",
"invalidated": false,
"invalidation_reason": null
},
@@ -17227,7 +17227,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P3=78.9% but requir \u2014 30/184 components failed (30 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P3=78.9% but requir — 30/184 components failed (30 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17237,7 +17237,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P3=78.9% but requir \u2014 30/184 components failed (30 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P3=78.9% but requir — 30/184 components failed (30 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17247,7 +17247,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=78.9% but required tests failed \u2014 Tensors differ: max_diff=27.805214, mean_rel=17.542879",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=78.9% but required tests failed — Tensors differ: max_diff=27.805214, mean_rel=17.542879",
"invalidated": false,
"invalidation_reason": null
},
@@ -17257,7 +17257,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence \u2014 Attention output weights not well-centered (worst_mean=0.061788)",
+ "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence — Attention output weights not well-centered (worst_mean=0.061788)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17267,7 +17267,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence \u2014 Attention output weights not well-centered (worst_mean=0.061788)",
+ "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence — Attention output weights not well-centered (worst_mean=0.061788)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17277,7 +17277,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence \u2014 Attention output weights not well-centered (worst_mean=0.061788)",
+ "notes": "Below threshold: P3=78.9% but required tests failed: logits_equivalence, loss_equivalence — Attention output weights not well-centered (worst_mean=0.061788)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17307,7 +17307,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 Error running comprehensive component benchmark: index 2 is out of range",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — Error running comprehensive component benchmark: index 2 is out of range",
"invalidated": false,
"invalidation_reason": null
},
@@ -17367,7 +17367,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Checkpoint ships no lm_head.weight with tie_word_embeddings=false, so HF randomly re-initializes the LM head on every load (Google released it as a pretraining artifact requiring fine-tuning); the nondeterministic unembed is the only P1 failure \u2014 all 233 other components including the local-attention encoder pass. Not an adapter bug.",
+ "notes": "Checkpoint ships no lm_head.weight with tie_word_embeddings=false, so HF randomly re-initializes the LM head on every load (Google released it as a pretraining artifact requiring fine-tuning); the nondeterministic unembed is the only P1 failure — all 233 other components including the local-attention encoder pass. Not an adapter bug.",
"invalidated": false,
"invalidation_reason": null
},
@@ -17437,7 +17437,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Verified on a local snapshot of yujiepan/llama-4-tiny-random with text_config.attn_temperature_tuning coerced to bool \u2014 the upstream config declares it as int 4, which transformers 5.x strict config validation rejects. P7 skipped: the tiny ships no processor files. P3=95 (attention_output_centering worst_mean=0.099 on random weights). Official Scout/Maverick checkpoints registered for big-hardware verification.",
+ "notes": "Verified on a local snapshot of yujiepan/llama-4-tiny-random with text_config.attn_temperature_tuning coerced to bool — the upstream config declares it as int 4, which transformers 5.x strict config validation rejects. P7 skipped: the tiny ships no processor files. P3=95 (attention_output_centering worst_mean=0.099 on random weights). Official Scout/Maverick checkpoints registered for big-hardware verification.",
"invalidated": false,
"invalidation_reason": null
},
@@ -17517,7 +17517,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=30.078743, mean_rel=0.607381",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=30.078743, mean_rel=0.607381",
"invalidated": false,
"invalidation_reason": null
},
@@ -17537,7 +17537,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=30.078743, mean_rel=0.607381",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=30.078743, mean_rel=0.607381",
"invalidated": false,
"invalidation_reason": null
},
@@ -17567,7 +17567,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=27.217707, mean_rel=1.057937",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=27.217707, mean_rel=1.057937",
"invalidated": false,
"invalidation_reason": null
},
@@ -17597,7 +17597,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=7.7% < 75.0% (failed: g \u2014 90/154 components failed (90 critical)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass); P2=7.7% < 75.0% (failed: g — 90/154 components failed (90 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17777,7 +17777,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -17797,7 +17797,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/462 components failed (1 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/462 components failed (1 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -17907,7 +17907,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation \u2014 Generated text has no new tokens",
+ "notes": "Below threshold: P2=69.2% < 75.0% (failed: generation, generation_with_kv_cache, multiple_generation — Generated text has no new tokens",
"invalidated": false,
"invalidation_reason": null
},
@@ -17927,7 +17927,7 @@
"verified_date": "2026-07-07",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=30.8% < 75.0% (failed: hook_functional \u2014 Forward pass failed: index out of range in self",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=30.8% < 75.0% (failed: hook_functional — Forward pass failed: index out of range in self",
"invalidated": false,
"invalidation_reason": null
},
@@ -18027,7 +18027,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Input must be a List[Union[str, AddedToken]]",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Input must be a List[Union[str, AddedToken]]",
"invalidated": false,
"invalidation_reason": null
},
@@ -18097,7 +18097,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: DreamGenerationConfig.validate() got an unexpected keyword argument 'user_set_attributes'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: DreamGenerationConfig.validate() got an unexpected keyword argument 'user_set_attributes'",
"invalidated": false,
"invalidation_reason": null
},
@@ -18187,7 +18187,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Text quality score: 76.4/100 (avg perplexity: 16.6) \u2014 generated text may be incoherent",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Text quality score: 76.4/100 (avg perplexity: 16.6) — generated text may be incoherent",
"invalidated": false,
"invalidation_reason": null
},
@@ -18197,7 +18197,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=0.207234, mean_rel=0.000825",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=0.207234, mean_rel=0.000825",
"invalidated": false,
"invalidation_reason": null
},
@@ -18237,7 +18237,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=19.557707, mean_rel=0.281903",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=19.557707, mean_rel=0.281903",
"invalidated": false,
"invalidation_reason": null
},
@@ -18257,7 +18257,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=19.557709, mean_rel=0.281903",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=19.557709, mean_rel=0.281903",
"invalidated": false,
"invalidation_reason": null
},
@@ -18267,7 +18267,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=19.557707, mean_rel=0.281903",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=19.557707, mean_rel=0.281903",
"invalidated": false,
"invalidation_reason": null
},
@@ -18297,7 +18297,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'GiddForDiffusionLM' object has no attribute 'all_tied_weights_keys'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'GiddForDiffusionLM' object has no attribute 'all_tied_weights_keys'",
"invalidated": false,
"invalidation_reason": null
},
@@ -18307,7 +18307,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'GiddModel' object has no attribute 'weight'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'GiddModel' object has no attribute 'weight'",
"invalidated": false,
"invalidation_reason": null
},
@@ -18317,7 +18317,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=26.121746, mean_rel=4.879314",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=26.121746, mean_rel=4.879314",
"invalidated": false,
"invalidation_reason": null
},
@@ -18327,7 +18327,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/136 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/136 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18337,7 +18337,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/268 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/268 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18347,7 +18347,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/136 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/136 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18357,7 +18357,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/268 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/268 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18387,7 +18387,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Could not determine supported architecture from config. Available architectures: ['AfmoeForCausalLM', 'A",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Could not determine supported architecture from config. Available architectures: ['AfmoeForCausalLM', 'A",
"invalidated": false,
"invalidation_reason": null
},
@@ -18397,7 +18397,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 24/100 components failed (24 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 24/100 components failed (24 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18417,7 +18417,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=42.9% < 75.0% (failed: hook_functional \u2014 Forward pass failed: 'tuple' object has no attribute 'dtype'",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=42.9% < 75.0% (failed: hook_functional — Forward pass failed: 'tuple' object has no attribute 'dtype'",
"invalidated": false,
"invalidation_reason": null
},
@@ -18447,7 +18447,7 @@
"verified_date": "2026-07-08",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 36/475 components failed (36 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 36/475 components failed (36 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18467,7 +18467,7 @@
"verified_date": "2026-07-12",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=90.0% but required tests failed: log \u2014 8/98 components failed (8 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=90.0% but required tests failed: log — 8/98 components failed (8 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18477,7 +18477,7 @@
"verified_date": "2026-07-12",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=90.0% but required tests failed: log \u2014 10/114 components failed (10 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=90.0% but required tests failed: log — 10/114 components failed (10 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18487,7 +18487,7 @@
"verified_date": "2026-07-12",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=90.0% but required tests failed: log \u2014 10/114 components failed (10 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=90.0% but required tests failed: log — 10/114 components failed (10 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18497,7 +18497,7 @@
"verified_date": "2026-07-14",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log \u2014 10/114 components failed (10 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log — 10/114 components failed (10 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18507,7 +18507,7 @@
"verified_date": "2026-07-14",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log \u2014 10/114 components failed (10 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log — 10/114 components failed (10 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18517,7 +18517,7 @@
"verified_date": "2026-07-14",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log \u2014 10/114 components failed (10 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log — 10/114 components failed (10 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18527,7 +18527,7 @@
"verified_date": "2026-07-14",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log \u2014 20/114 components failed (20 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P3=89.5% but required tests failed: log — 20/114 components failed (20 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -18537,7 +18537,7 @@
"verified_date": "2026-07-14",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=23.349285, mean_rel=0.575189",
+ "notes": "Below threshold: P3=89.5% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=23.349285, mean_rel=0.575189",
"invalidated": false,
"invalidation_reason": null
},
@@ -18627,7 +18627,7 @@
"verified_date": "2026-07-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": false,
"invalidation_reason": null
},
@@ -18637,7 +18637,7 @@
"verified_date": "2026-07-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": false,
"invalidation_reason": null
},
@@ -18647,7 +18647,7 @@
"verified_date": "2026-07-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: The model's quantization config from the arguments has no `quant_method` attribute. Make sure that the m",
"invalidated": false,
"invalidation_reason": null
},
@@ -18787,7 +18787,7 @@
"verified_date": "2026-07-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed \u2014 Tensors differ: max_diff=11.357496, mean_rel=4.069944",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits); P3=90.0% but required tests failed — Tensors differ: max_diff=11.357496, mean_rel=4.069944",
"invalidated": false,
"invalidation_reason": null
},
@@ -18837,7 +18837,7 @@
"verified_date": "2026-07-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.7% < 75.0% (failed: generation, gene \u2014 Forward pass failed: create_causal_mask() got an unexpected keyword argument 'input_embeds'",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.7% < 75.0% (failed: generation, gene — Forward pass failed: create_causal_mask() got an unexpected keyword argument 'input_embeds'",
"invalidated": false,
"invalidation_reason": null
},
@@ -18847,7 +18847,7 @@
"verified_date": "2026-07-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: 'FalconDecoderLayer' object has no attribute 'ln_attn'",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: 'FalconDecoderLayer' object has no attribute 'ln_attn'",
"invalidated": false,
"invalidation_reason": null
},
@@ -18917,7 +18917,7 @@
"verified_date": "2026-07-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.7% < 75.0% (failed: generation, gene \u2014 Forward pass failed: index out of range in self",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=7.7% < 75.0% (failed: generation, gene — Forward pass failed: index out of range in self",
"invalidated": false,
"invalidation_reason": null
},
@@ -18947,7 +18947,7 @@
"verified_date": "2026-07-21",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Couldn't instantiate the backend tokenizer from one of: \n(1) a `tokenizers` library serialization file, ",
"invalidated": false,
"invalidation_reason": null
},
@@ -19257,7 +19257,7 @@
"verified_date": "2026-07-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 Error running comprehensive component benchmark: Component attn not found in blocks.0 components",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — Error running comprehensive component benchmark: Component attn not found in blocks.0 components",
"invalidated": false,
"invalidation_reason": null
},
@@ -19297,7 +19297,7 @@
"verified_date": "2026-07-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence \u2014 Tensors differ: max_diff=32.690491, mean_rel=0.409154",
+ "notes": "Below threshold: P3=90.0% but required tests failed: logits_equivalence, loss_equivalence — Tensors differ: max_diff=32.690491, mean_rel=0.409154",
"invalidated": false,
"invalidation_reason": null
},
@@ -19327,7 +19327,7 @@
"verified_date": "2026-07-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P2=31.2% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_registry, hook \u2014 Logits computation failed: Invalid positional_embedding_type passed in relative_positional_bias",
+ "notes": "Below threshold: P2=31.2% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_registry, hook — Logits computation failed: Invalid positional_embedding_type passed in relative_positional_bias",
"invalidated": false,
"invalidation_reason": null
},
@@ -19347,7 +19347,7 @@
"verified_date": "2026-07-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 12/126 components failed (12 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 12/126 components failed (12 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -19457,7 +19457,7 @@
"verified_date": "2026-07-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Could not determine supported architecture from config. Available architectures: ['AfmoeForCausalLM', 'A",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Could not determine supported architecture from config. Available architectures: ['AfmoeForCausalLM', 'A",
"invalidated": false,
"invalidation_reason": null
},
@@ -19467,7 +19467,7 @@
"verified_date": "2026-07-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=2.599856, mean_rel=0.102672",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=2.599856, mean_rel=0.102672",
"invalidated": false,
"invalidation_reason": null
},
@@ -19517,7 +19517,7 @@
"verified_date": "2026-07-22",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P3=83.3% but required tests failed: logits_equivalence, loss_equivalence \u2014 Unembed matrix not well-centered (mean=0.075038)",
+ "notes": "Below threshold: P3=83.3% but required tests failed: logits_equivalence, loss_equivalence — Unembed matrix not well-centered (mean=0.075038)",
"invalidated": false,
"invalidation_reason": null
},
@@ -19927,7 +19927,7 @@
"verified_date": "2026-07-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=17.494274, mean_rel=8.712387",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=17.494274, mean_rel=8.712387",
"invalidated": false,
"invalidation_reason": null
},
@@ -19937,7 +19937,7 @@
"verified_date": "2026-07-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=18.492882, mean_rel=11.615888",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=18.492882, mean_rel=11.615888",
"invalidated": false,
"invalidation_reason": null
},
@@ -19947,7 +19947,7 @@
"verified_date": "2026-07-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=17.494274, mean_rel=8.712387",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=17.494274, mean_rel=8.712387",
"invalidated": false,
"invalidation_reason": null
},
@@ -19957,7 +19957,7 @@
"verified_date": "2026-07-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) \u2014 Tensors differ: max_diff=18.492882, mean_rel=11.615888",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass_logits) — Tensors differ: max_diff=18.492882, mean_rel=11.615888",
"invalidated": false,
"invalidation_reason": null
},
@@ -20187,7 +20187,7 @@
"verified_date": "2026-07-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=30.8% < 75.0% (failed: hook_functional \u2014 Forward pass failed: index out of range in self",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: forward_pass); P2=30.8% < 75.0% (failed: hook_functional — Forward pass failed: index out of range in self",
"invalidated": false,
"invalidation_reason": null
},
@@ -20387,7 +20387,7 @@
"verified_date": "2026-07-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 2/23 components failed (2 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 2/23 components failed (2 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -20397,7 +20397,7 @@
"verified_date": "2026-07-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P3=90.0% but requir \u2014 1/15 components failed (1 low)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P3=90.0% but requir — 1/15 components failed (1 low)",
"invalidated": false,
"invalidation_reason": null
},
@@ -20417,7 +20417,7 @@
"verified_date": "2026-07-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 2/23 components failed (2 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 2/23 components failed (2 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -20427,7 +20427,7 @@
"verified_date": "2026-07-23",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P3=90.0% but requir \u2014 1/15 components failed (1 low)",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: all_components, forward_pass_logits); P3=90.0% but requir — 1/15 components failed (1 low)",
"invalidated": false,
"invalidation_reason": null
},
@@ -20607,7 +20607,7 @@
"verified_date": "2026-07-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) \u2014 1/107 components failed (1 critical)",
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components) — 1/107 components failed (1 critical)",
"invalidated": false,
"invalidation_reason": null
},
@@ -20777,7 +20777,7 @@
"verified_date": "2026-07-24",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P8=0.0% < 75.0% (failed: audio_text_forward) \u2014 Audio-conditioned forward failed: 'NoneType' object has no attribute '_attn_implementation'",
+ "notes": "Below threshold: P8=0.0% < 75.0% (failed: audio_text_forward) — Audio-conditioned forward failed: 'NoneType' object has no attribute '_attn_implementation'",
"invalidated": false,
"invalidation_reason": null
},
@@ -20907,7 +20907,7 @@
"verified_date": "2026-07-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P8=0.0% < 75.0% (failed: audio_forward, audio_cache, audio_representation_stability \u2014 Audio forward pass failed: Dimension out of range (expected to be in range of [-3, 2], but got 3)",
+ "notes": "Below threshold: P8=0.0% < 75.0% (failed: audio_forward, audio_cache, audio_representation_stability — Audio forward pass failed: Dimension out of range (expected to be in range of [-3, 2], but got 3)",
"invalidated": false,
"invalidation_reason": null
},
@@ -20957,7 +20957,7 @@
"verified_date": "2026-07-30",
"verified_by": "verify_models",
"transformerlens_version": null,
- "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) \u2014 Failed to load unprocessed TransformerBridge: Could not determine supported architecture from config. Available architectures: ['AfmoeForCausalLM', 'A",
+ "notes": "Below threshold: P1=0.0% < 100.0% (failed: load_bridge_unprocessed) — Failed to load unprocessed TransformerBridge: Could not determine supported architecture from config. Available architectures: ['AfmoeForCausalLM', 'A",
"invalidated": false,
"invalidation_reason": null
},
@@ -21280,6 +21280,1766 @@
"notes": "Full verification completed",
"invalidated": false,
"invalidation_reason": null
+ },
+ {
+ "model_id": "google-t5/t5-small",
+ "architecture_id": "T5ForConditionalGeneration",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-t5/t5-small",
+ "architecture_id": "T5ForConditionalGeneration",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-t5/t5-base",
+ "architecture_id": "T5ForConditionalGeneration",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "openai-community/gpt2",
+ "architecture_id": "GPT2LMHeadModel",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "distilbert/distilgpt2",
+ "architecture_id": "GPT2LMHeadModel",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "swiss-ai/Apertus-v1.1-0.5B",
+ "architecture_id": "ApertusForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "swiss-ai/Apertus-v1.1-1.5B",
+ "architecture_id": "ApertusForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "apple/OpenELM-270M",
+ "architecture_id": "OpenELMForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "apple/OpenELM-1_1B",
+ "architecture_id": "OpenELMForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Isotonic/TinyMixtral-4x248M-MoE",
+ "architecture_id": "MixtralForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "BEE-spoke-data/Mixtral-GQA-400m-v2",
+ "architecture_id": "MixtralForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-4-E2B",
+ "architecture_id": "Gemma4ForConditionalGeneration",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-2-2b",
+ "architecture_id": "Gemma2ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=93.3% (failed: backward_hooks); P3=90.9% (failed: unembed_centering, backward_hooks)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-2-2b-it",
+ "architecture_id": "Gemma2ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P3=95.5% (failed: unembed_centering)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/Phi-3-mini-4k-instruct",
+ "architecture_id": "Phi3ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/Phi-3.5-mini-instruct",
+ "architecture_id": "Phi3ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "01-ai/Yi-6B",
+ "architecture_id": "LlamaForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "01-ai/Yi-1.5-6B",
+ "architecture_id": "LlamaForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen2.5-0.5B",
+ "architecture_id": "Qwen2ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen2.5-1.5B",
+ "architecture_id": "Qwen2ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen3-0.6B",
+ "architecture_id": "Qwen3ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=93.3% (failed: backward_hooks)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen3-1.7B",
+ "architecture_id": "Qwen3ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/pythia-70m",
+ "architecture_id": "GPTNeoXForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/pythia-1.4b",
+ "architecture_id": "GPTNeoXForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/gpt-neo-125m",
+ "architecture_id": "GPTNeoForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/gpt-neo-1.3B",
+ "architecture_id": "GPTNeoForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/opt-125m",
+ "architecture_id": "OPTForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/opt-1.3b",
+ "architecture_id": "OPTForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigscience/bloom-560m",
+ "architecture_id": "BloomForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=93.3% (failed: gated_hooks_fire); P3=95.5% (failed: gated_hooks_fire)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigscience/bloom-1b7",
+ "architecture_id": "BloomForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=93.3% (failed: gated_hooks_fire); P3=95.5% (failed: gated_hooks_fire)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-2b",
+ "architecture_id": "GemmaForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-3-270m",
+ "architecture_id": "Gemma3ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-3-1b-it",
+ "architecture_id": "Gemma3ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/phi-1",
+ "architecture_id": "PhiForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/phi-2",
+ "architecture_id": "PhiForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=93.3% (failed: backward_hooks)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "stabilityai/stablelm-2-1_6b",
+ "architecture_id": "StableLmForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "state-spaces/mamba-130m-hf",
+ "architecture_id": "MambaForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "ibm-granite/granite-3.1-2b-instruct",
+ "architecture_id": "GraniteForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "HuggingFaceTB/SmolLM3-3B",
+ "architecture_id": "SmolLM3ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/OLMo-2-0425-1B",
+ "architecture_id": "Olmo2ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/Olmo-3-1025-7B",
+ "architecture_id": "Olmo3ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Below threshold: P3=71.4% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Tensors differ: max_diff=23.143579, mean_rel=9.020543",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/OLMoE-1B-7B-0924",
+ "architecture_id": "OlmoeForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Below threshold: P3=72.7% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Tensors differ: max_diff=26.260244, mean_rel=3.783418",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/gpt-j-6b",
+ "architecture_id": "GPTJForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "mistralai/Mistral-7B-v0.1",
+ "architecture_id": "MistralForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/bart-large-cnn",
+ "architecture_id": "BartForConditionalGeneration",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigscience/mt0-base",
+ "architecture_id": "MT5ForConditionalGeneration",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/long-t5-tglobal-base",
+ "architecture_id": "LongT5ForConditionalGeneration",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/pegasus-xsum",
+ "architecture_id": "PegasusForConditionalGeneration",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/m2m100_418M",
+ "architecture_id": "M2M100ForConditionalGeneration",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigcode/starcoder2-3b",
+ "architecture_id": "Starcoder2ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "internlm/internlm2-chat-1_8b",
+ "architecture_id": "InternLM2ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/Olmo-3-1025-7B",
+ "architecture_id": "Olmo3ForCausalLM",
+ "verified_date": "2026-08-19",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-t5/t5-small",
+ "architecture_id": "T5ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=92.9% (failed: hook_functionality)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-t5/t5-base",
+ "architecture_id": "T5ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality: P2=92.9% (failed: hook_functionality)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "openai-community/gpt2",
+ "architecture_id": "GPT2LMHeadModel",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "distilbert/distilgpt2",
+ "architecture_id": "GPT2LMHeadModel",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "swiss-ai/Apertus-v1.1-0.5B",
+ "architecture_id": "ApertusForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "swiss-ai/Apertus-v1.1-1.5B",
+ "architecture_id": "ApertusForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "apple/OpenELM-270M",
+ "architecture_id": "OpenELMForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "apple/OpenELM-1_1B",
+ "architecture_id": "OpenELMForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Isotonic/TinyMixtral-4x248M-MoE",
+ "architecture_id": "MixtralForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "BEE-spoke-data/Mixtral-GQA-400m-v2",
+ "architecture_id": "MixtralForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-4-E2B",
+ "architecture_id": "Gemma4ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-2-2b",
+ "architecture_id": "Gemma2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P3=95.5% (failed: unembed_centering)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-2-2b-it",
+ "architecture_id": "Gemma2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P3=95.5% (failed: unembed_centering)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/Phi-3-mini-4k-instruct",
+ "architecture_id": "Phi3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/Phi-3.5-mini-instruct",
+ "architecture_id": "Phi3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "01-ai/Yi-6B",
+ "architecture_id": "LlamaForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "01-ai/Yi-1.5-6B",
+ "architecture_id": "LlamaForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen2.5-0.5B",
+ "architecture_id": "Qwen2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen2.5-1.5B",
+ "architecture_id": "Qwen2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen3-0.6B",
+ "architecture_id": "Qwen3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen3-1.7B",
+ "architecture_id": "Qwen3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/pythia-70m",
+ "architecture_id": "GPTNeoXForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/pythia-1.4b",
+ "architecture_id": "GPTNeoXForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/gpt-neo-125m",
+ "architecture_id": "GPTNeoForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/gpt-neo-1.3B",
+ "architecture_id": "GPTNeoForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/opt-125m",
+ "architecture_id": "OPTForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/opt-1.3b",
+ "architecture_id": "OPTForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigscience/bloom-560m",
+ "architecture_id": "BloomForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigscience/bloom-1b7",
+ "architecture_id": "BloomForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-2b",
+ "architecture_id": "GemmaForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-3-270m",
+ "architecture_id": "Gemma3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-3-1b-it",
+ "architecture_id": "Gemma3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/phi-1",
+ "architecture_id": "PhiForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/phi-2",
+ "architecture_id": "PhiForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=93.3% (failed: backward_hooks)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "stabilityai/stablelm-2-1_6b",
+ "architecture_id": "StableLmForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "state-spaces/mamba-130m-hf",
+ "architecture_id": "MambaForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "ibm-granite/granite-3.1-2b-instruct",
+ "architecture_id": "GraniteForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "HuggingFaceTB/SmolLM3-3B",
+ "architecture_id": "SmolLM3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/OLMo-2-0425-1B",
+ "architecture_id": "Olmo2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/Olmo-3-1025-7B",
+ "architecture_id": "Olmo3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/OLMoE-1B-7B-0924",
+ "architecture_id": "OlmoeForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P3=95.5% (failed: forward_hooks)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/gpt-j-6b",
+ "architecture_id": "GPTJForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "mistralai/Mistral-7B-v0.1",
+ "architecture_id": "MistralForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/bart-large-cnn",
+ "architecture_id": "BartForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=92.9% (failed: hook_functionality)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigscience/mt0-base",
+ "architecture_id": "MT5ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality: P2=92.9% (failed: hook_functionality)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/long-t5-tglobal-base",
+ "architecture_id": "LongT5ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality: P2=92.9% (failed: hook_functionality)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/pegasus-xsum",
+ "architecture_id": "PegasusForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=92.9% (failed: hook_functionality)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/m2m100_418M",
+ "architecture_id": "M2M100ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality: P2=92.9% (failed: hook_functionality)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigcode/starcoder2-3b",
+ "architecture_id": "Starcoder2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "internlm/internlm2-chat-1_8b",
+ "architecture_id": "InternLM2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-t5/t5-small",
+ "architecture_id": "T5ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-t5/t5-base",
+ "architecture_id": "T5ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "openai-community/gpt2",
+ "architecture_id": "GPT2LMHeadModel",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "distilbert/distilgpt2",
+ "architecture_id": "GPT2LMHeadModel",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "swiss-ai/Apertus-v1.1-0.5B",
+ "architecture_id": "ApertusForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "swiss-ai/Apertus-v1.1-1.5B",
+ "architecture_id": "ApertusForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "apple/OpenELM-270M",
+ "architecture_id": "OpenELMForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "apple/OpenELM-1_1B",
+ "architecture_id": "OpenELMForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Isotonic/TinyMixtral-4x248M-MoE",
+ "architecture_id": "MixtralForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "BEE-spoke-data/Mixtral-GQA-400m-v2",
+ "architecture_id": "MixtralForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-4-E2B",
+ "architecture_id": "Gemma4ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-2-2b",
+ "architecture_id": "Gemma2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P3=95.5% (failed: unembed_centering)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-2-2b-it",
+ "architecture_id": "Gemma2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P3=95.5% (failed: unembed_centering)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/Phi-3-mini-4k-instruct",
+ "architecture_id": "Phi3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/Phi-3.5-mini-instruct",
+ "architecture_id": "Phi3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "01-ai/Yi-6B",
+ "architecture_id": "LlamaForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "01-ai/Yi-1.5-6B",
+ "architecture_id": "LlamaForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen2.5-0.5B",
+ "architecture_id": "Qwen2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen2.5-1.5B",
+ "architecture_id": "Qwen2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen3-0.6B",
+ "architecture_id": "Qwen3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "Qwen/Qwen3-1.7B",
+ "architecture_id": "Qwen3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/pythia-70m",
+ "architecture_id": "GPTNeoXForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/pythia-1.4b",
+ "architecture_id": "GPTNeoXForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/gpt-neo-125m",
+ "architecture_id": "GPTNeoForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/gpt-neo-1.3B",
+ "architecture_id": "GPTNeoForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/opt-125m",
+ "architecture_id": "OPTForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/opt-1.3b",
+ "architecture_id": "OPTForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigscience/bloom-560m",
+ "architecture_id": "BloomForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigscience/bloom-1b7",
+ "architecture_id": "BloomForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-2b",
+ "architecture_id": "GemmaForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-3-270m",
+ "architecture_id": "Gemma3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/gemma-3-1b-it",
+ "architecture_id": "Gemma3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/phi-1",
+ "architecture_id": "PhiForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "microsoft/phi-2",
+ "architecture_id": "PhiForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues: P2=93.3% (failed: backward_hooks)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "stabilityai/stablelm-2-1_6b",
+ "architecture_id": "StableLmForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "state-spaces/mamba-130m-hf",
+ "architecture_id": "MambaForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "ibm-granite/granite-3.1-2b-instruct",
+ "architecture_id": "GraniteForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "HuggingFaceTB/SmolLM3-3B",
+ "architecture_id": "SmolLM3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/OLMo-2-0425-1B",
+ "architecture_id": "Olmo2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/Olmo-3-1025-7B",
+ "architecture_id": "Olmo3ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "allenai/OLMoE-1B-7B-0924",
+ "architecture_id": "OlmoeForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "EleutherAI/gpt-j-6b",
+ "architecture_id": "GPTJForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "mistralai/Mistral-7B-v0.1",
+ "architecture_id": "MistralForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/bart-large-cnn",
+ "architecture_id": "BartForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigscience/mt0-base",
+ "architecture_id": "MT5ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/long-t5-tglobal-base",
+ "architecture_id": "LongT5ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google/pegasus-xsum",
+ "architecture_id": "PegasusForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "facebook/m2m100_418M",
+ "architecture_id": "M2M100ForConditionalGeneration",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed with issues, low text quality",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "bigcode/starcoder2-3b",
+ "architecture_id": "Starcoder2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "internlm/internlm2-chat-1_8b",
+ "architecture_id": "InternLM2ForCausalLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "openai-community/gpt2",
+ "architecture_id": "GPT2LMHeadModel",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "continuation"
+ },
+ {
+ "model_id": "EleutherAI/pythia-70m",
+ "architecture_id": "GPTNeoXForCausalLM",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "continuation"
+ },
+ {
+ "model_id": "Helsinki-NLP/opus-mt-nl-en",
+ "architecture_id": "MarianMTModel",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:translation@nl-en"
+ },
+ {
+ "model_id": "google-t5/t5-small",
+ "architecture_id": "T5ForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:translation@en-de"
+ },
+ {
+ "model_id": "google-t5/t5-base",
+ "architecture_id": "T5ForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:translation@en-de"
+ },
+ {
+ "model_id": "facebook/bart-large-cnn",
+ "architecture_id": "BartForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:summarization"
+ },
+ {
+ "model_id": "bigscience/mt0-base",
+ "architecture_id": "MT5ForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification passed, but text quality poor (P4=26.8). Needs review",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:instruction"
+ },
+ {
+ "model_id": "google/long-t5-tglobal-base",
+ "architecture_id": "LongT5ForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification passed, but text quality poor (P4=48.4). Needs review",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:denoise"
+ },
+ {
+ "model_id": "google/pegasus-xsum",
+ "architecture_id": "PegasusForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:summarization"
+ },
+ {
+ "model_id": "facebook/m2m100_418M",
+ "architecture_id": "M2M100ForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:translation@en-de"
+ },
+ {
+ "model_id": "Qwen/Qwen2.5-0.5B-Instruct",
+ "architecture_id": "Qwen2ForCausalLM",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "chat"
+ },
+ {
+ "model_id": "google/gemma-2-2b-it",
+ "architecture_id": "Gemma2ForCausalLM",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "chat"
+ },
+ {
+ "model_id": "facebook/mbart-large-50-many-to-many-mmt",
+ "architecture_id": "MBartForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:translation@en-de"
+ },
+ {
+ "model_id": "facebook/mbart-large-50",
+ "architecture_id": "MBartForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification completed",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:denoise"
+ },
+ {
+ "model_id": "facebook/mbart-large-cc25",
+ "architecture_id": "MBartForConditionalGeneration",
+ "verified_date": "2026-08-21",
+ "verified_by": "verify_models",
+ "transformerlens_version": "0.0.0",
+ "notes": "Core verification passed, but text quality poor (P4=25.0). Needs review",
+ "invalidated": false,
+ "invalidation_reason": null,
+ "p4_scoring_version": 2,
+ "prompt_profile": "task:denoise"
+ },
+ {
+ "model_id": "google-bert/bert-base-cased",
+ "architecture_id": "BertForMaskedLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P2=68.8% < 75.0% (failed: logits_equiva — 1/79 components failed (1 critical)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-bert/bert-base-cased",
+ "architecture_id": "BertForMaskedLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P2=68.8% < 75.0% (failed: logits_equiva — 1/79 components failed (1 critical)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-bert/bert-base-cased",
+ "architecture_id": "BertForMaskedLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P2=68.8% < 75.0% (failed: logits_equiva — 2/79 components failed (2 critical)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-bert/bert-base-cased",
+ "architecture_id": "BertForMaskedLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Below threshold: P1=50.0% < 100.0% (failed: all_components); P2=68.8% < 75.0% (failed: logits_equiva — 1/79 components failed (1 critical)",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-bert/bert-base-cased",
+ "architecture_id": "BertForMaskedLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Below threshold: P2=68.8% < 75.0% (failed: logits_equivalence, loss_equivalence, hook_functionality, — Tensors differ: max_diff=29.631586, mean_rel=1.923156",
+ "invalidated": false,
+ "invalidation_reason": null
+ },
+ {
+ "model_id": "google-bert/bert-base-cased",
+ "architecture_id": "BertForMaskedLM",
+ "verified_date": "2026-08-20",
+ "verified_by": "verify_models",
+ "transformerlens_version": null,
+ "notes": "Full verification completed",
+ "invalidated": false,
+ "invalidation_reason": null
}
]
}
diff --git a/transformer_lens/tools/model_registry/hf_scraper.py b/transformer_lens/tools/model_registry/hf_scraper.py
index b34f470346..b48042af72 100644
--- a/transformer_lens/tools/model_registry/hf_scraper.py
+++ b/transformer_lens/tools/model_registry/hf_scraper.py
@@ -36,6 +36,16 @@
from pathlib import Path
from typing import Optional
+from transformer_lens.benchmarks.text_quality_profiles import (
+ ARCHITECTURE_PROFILE_KINDS,
+ MODEL_PROFILE_OVERRIDES,
+ HFSignals,
+ extract_languages,
+ is_default_profile,
+ profile_from_hf_signals,
+ resolve_profile,
+)
+
from . import HF_SUPPORTED_ARCHITECTURES
from .registry_io import is_quantized_model
@@ -158,9 +168,33 @@ def _load_existing_gaps(output_dir: Path) -> dict[str, dict]:
return by_arch
-def _build_model_entry(model_id: str, architecture_id: str) -> dict:
- """Build a model entry dict matching the ModelEntry schema."""
- return {
+def _extract_profile_signals(model_info) -> HFSignals: # type: ignore[no-untyped-def]
+ """Distill pipeline_tag/tags/cardData off a listing payload (no extra request).
+
+ Args:
+ model_info: ModelInfo object from list_models(expand=[..., 'pipeline_tag',
+ 'tags', 'cardData'])
+ """
+ pipeline_tag = getattr(model_info, "pipeline_tag", None)
+ tags = tuple(getattr(model_info, "tags", None) or [])
+ card_data = getattr(model_info, "card_data", None)
+ card_language = getattr(card_data, "language", None) if card_data is not None else None
+ if card_language is None and card_data is not None and hasattr(card_data, "get"):
+ card_language = card_data.get("language")
+ languages = extract_languages(card_language, tags)
+ return HFSignals(pipeline_tag=pipeline_tag, languages=languages, tags=tags)
+
+
+def _build_model_entry(
+ model_id: str, architecture_id: str, signals: Optional[HFSignals] = None
+) -> dict:
+ """Build a model entry dict matching the ModelEntry schema.
+
+ ``signals``, when given, resolves and stores a sparse ``prompt_profile`` key
+ (omitted when it's just the default) and warns on tag/curation disagreement
+ — the warning is how curation gaps (missing override/architecture rule) surface.
+ """
+ entry = {
"architecture_id": architecture_id,
"model_id": model_id,
"status": 0,
@@ -175,6 +209,25 @@ def _build_model_entry(model_id: str, architecture_id: str) -> dict:
"phase8_score": None,
"phase9_score": None,
}
+ if signals is not None:
+ hinted = profile_from_hf_signals(model_id, architecture_id, signals)
+ resolved = resolve_profile(model_id, architecture_id, signals=signals)
+ deliberately_curated = (
+ model_id in MODEL_PROFILE_OVERRIDES or architecture_id in ARCHITECTURE_PROFILE_KINDS
+ )
+ if hinted is not None and hinted.kind != resolved.kind and not deliberately_curated:
+ # A disagreement nothing deliberate explains is a curation gap.
+ logger.warning(
+ f"Profile mismatch for {model_id} ({architecture_id}): Hub tags say "
+ f"{hinted.kind!r}, curation resolves {resolved.kind!r}"
+ )
+ if not is_default_profile(resolved):
+ # Keep key position consistent with ModelEntry.to_dict (after note).
+ items = list(entry.items())
+ items.insert([k for k, _ in items].index("note") + 1, ("prompt_profile", str(resolved)))
+ entry.clear()
+ entry.update(items)
+ return entry
def _canonical_author_sweep(
@@ -182,6 +235,7 @@ def _canonical_author_sweep(
supported_models: list[dict],
seen_models: set[str],
architecture: Optional[str] = None,
+ refresh_profiles: bool = False,
) -> int:
"""Admit canonical-org supported-arch models regardless of downloads. Returns count added.
@@ -201,15 +255,31 @@ def _canonical_author_sweep(
if architecture is not None and architecture not in expected_archs:
continue
try:
- models_iter = api.list_models(author=author, expand=["config", "safetensors"])
+ models_iter = api.list_models(
+ author=author,
+ expand=["config", "safetensors", "pipeline_tag", "tags", "cardData"],
+ )
except Exception as exc: # pragma: no cover — network/transient
logger.warning(f"Canonical sweep: list_models(author={author!r}) failed: {exc}")
continue
# Iterate paginated results; a single timeout shouldn't lose every prior author.
+ existing_by_id = {m["model_id"]: m for m in supported_models} if refresh_profiles else {}
try:
for model in models_iter:
if model.id in seen_models:
+ # Below-threshold canonical models are reachable only here;
+ # the main scan's backfill never sees them.
+ if refresh_profiles:
+ existing_entry = existing_by_id.get(model.id)
+ if existing_entry is not None and "prompt_profile" not in existing_entry:
+ resolved = resolve_profile(
+ model.id,
+ existing_entry.get("architecture_id"),
+ signals=_extract_profile_signals(model),
+ )
+ if not is_default_profile(resolved):
+ existing_entry["prompt_profile"] = str(resolved)
continue
if is_quantized_model(model.id):
continue
@@ -221,7 +291,8 @@ def _canonical_author_sweep(
# Reject e.g. mistralai's non-Mistral checkpoints.
if model_arch not in expected_archs:
continue
- supported_models.append(_build_model_entry(model.id, model_arch))
+ signals = _extract_profile_signals(model)
+ supported_models.append(_build_model_entry(model.id, model_arch, signals))
seen_models.add(model.id)
added += 1
logger.info(f"Canonical sweep added: {model.id} ({model_arch})")
@@ -242,6 +313,7 @@ def scrape_all_models(
min_downloads: int = 500,
canonical_sweep: bool = True,
architecture: Optional[str] = None,
+ refresh_profiles: bool = False,
) -> tuple[dict, dict]:
"""Scrape ALL models from HuggingFace and categorize by architecture.
@@ -269,6 +341,9 @@ def scrape_all_models(
this class (e.g. ``"LlamaForCausalLM"``). Applies to both the main scan and
the canonical-author sweep. Useful for populating the registry after adding
a single new adapter without rescanning every architecture.
+ refresh_profiles: If True, backfill a missing ``prompt_profile`` key onto
+ already-seen registry entries using the listing payload already in hand — no
+ extra requests (default: False).
Returns:
Tuple of (supported_models_dict, architecture_gaps_dict)
@@ -292,6 +367,9 @@ def scrape_all_models(
# Track all models by architecture (start with existing models)
supported_models: list[dict] = list(existing_models) # Preserve existing
+ # Same dict objects as supported_models — mutating via this index (--refresh-profiles)
+ # is reflected in the final write.
+ existing_by_id: dict[str, dict] = {m["model_id"]: m for m in supported_models}
unsupported_arch_counts: dict[str, int] = {} # arch -> count
unsupported_arch_samples: dict[str, list[str]] = {} # arch -> top model IDs
unsupported_arch_downloads: dict[str, int] = {} # arch -> total downloads
@@ -361,19 +439,22 @@ def scrape_all_models(
logger.info("Will scan ALL new models (this may take a while)")
try:
- # Use expand=['config', 'safetensors'] to get architecture and parameter
- # count data inline with the listing, avoiding per-model API calls.
- # With ~1000 models per page, a full scan of 200K+ models needs only
- # ~200 paginated requests (well within the 1000 req / 5 min limit).
- # Use ``filter`` rather than ``pipeline_tag`` so encoder-decoder models
- # are discoverable: HF assigns T5/mT5 a primary pipeline_tag of
- # "translation" (or None for mT5) and only lists "text2text-generation"
- # in the broader tag list. ``filter`` matches against tags, ``pipeline_tag``
- # only against the canonical primary tag.
+ # Use expand=['config', 'safetensors', 'pipeline_tag', 'tags', 'cardData'] to get
+ # architecture, parameter count, and prompt-profile signals inline with the
+ # listing, avoiding per-model API calls. With ~1000 models per page, a full
+ # scan of 200K+ models needs only ~200 paginated requests (well within the
+ # 1000 req / 5 min limit).
+ # Use ``filter`` rather than ``pipeline_tag`` (the query param) so
+ # encoder-decoder models are discoverable: HF assigns T5/mT5 a primary
+ # pipeline_tag of "translation" (or None for mT5) and only lists
+ # "text2text-generation" in the broader tag list. ``filter`` matches against
+ # tags, ``pipeline_tag`` only against the canonical primary tag. The
+ # expanded ``pipeline_tag`` *field* below is a different thing — it's per-model
+ # metadata fed to profile_from_hf_signals, not a query filter.
list_kwargs: dict = {
"filter": task,
"sort": "downloads",
- "expand": ["config", "safetensors"],
+ "expand": ["config", "safetensors", "pipeline_tag", "tags", "cardData"],
}
if max_models is not None:
list_kwargs["limit"] = max_models + len(seen_models)
@@ -393,6 +474,19 @@ def scrape_all_models(
# Skip if already in our JSON or processed in this run
if model.id in seen_models:
skipped += 1
+ if refresh_profiles:
+ existing_entry = existing_by_id.get(model.id)
+ if (
+ existing_entry is not None
+ and "prompt_profile" not in existing_entry
+ ):
+ resolved = resolve_profile(
+ model.id,
+ existing_entry.get("architecture_id"),
+ signals=_extract_profile_signals(model),
+ )
+ if not is_default_profile(resolved):
+ existing_entry["prompt_profile"] = str(resolved)
continue
# Filter by minimum download count. Since results are sorted
@@ -430,7 +524,8 @@ def scrape_all_models(
if arch is None:
errors += 1
elif arch in HF_SUPPORTED_ARCHITECTURES:
- supported_models.append(_build_model_entry(model.id, arch))
+ signals = _extract_profile_signals(model)
+ supported_models.append(_build_model_entry(model.id, arch, signals))
new_supported += 1
else:
unsupported_arch_counts[arch] = unsupported_arch_counts.get(arch, 0) + 1
@@ -537,7 +632,11 @@ def scrape_all_models(
# Don't lose the main-scan registry on a sweep-time failure.
try:
canonical_added = _canonical_author_sweep(
- api, supported_models, seen_models, architecture=architecture
+ api,
+ supported_models,
+ seen_models,
+ architecture=architecture,
+ refresh_profiles=refresh_profiles,
)
new_supported += canonical_added
logger.info(f"Canonical sweep added {canonical_added} models.")
@@ -830,6 +929,12 @@ def main():
"(e.g. 'LlamaForCausalLM'). Use after adding a new adapter to populate the "
"registry with that architecture's models without rescanning everything.",
)
+ parser.add_argument(
+ "--refresh-profiles",
+ action="store_true",
+ help="Backfill a missing prompt_profile key onto already-seen registry entries "
+ "from the listing payload already in hand (no extra requests).",
+ )
args = parser.parse_args()
@@ -843,6 +948,7 @@ def main():
min_downloads=args.min_downloads,
canonical_sweep=not args.no_canonical_sweep,
architecture=args.architecture,
+ refresh_profiles=args.refresh_profiles,
)
diff --git a/transformer_lens/tools/model_registry/registry_io.py b/transformer_lens/tools/model_registry/registry_io.py
index dfe075e6a3..d52d87a6eb 100644
--- a/transformer_lens/tools/model_registry/registry_io.py
+++ b/transformer_lens/tools/model_registry/registry_io.py
@@ -11,6 +11,11 @@
from pathlib import Path
from typing import Callable, Optional
+from transformer_lens.benchmarks.text_quality_profiles import (
+ P4_SCORING_VERSION,
+ is_default_profile,
+)
+
from .verification import VerificationHistory, VerificationRecord
logger = logging.getLogger(__name__)
@@ -174,7 +179,14 @@ def _get_tl_version() -> Optional[str]:
try:
import transformer_lens
- return getattr(transformer_lens, "__version__", None)
+ version = getattr(transformer_lens, "__version__", None)
+ if version:
+ return str(version)
+ # The package exports no __version__; installed-distribution
+ # metadata is the fallback (dev installs record 0.0.0).
+ from importlib.metadata import version as dist_version
+
+ return dist_version("transformer-lens")
except Exception:
return None
@@ -186,6 +198,7 @@ def update_model_status(
note: Optional[str] = None,
phase_scores: Optional[dict[int, Optional[float]]] = None,
sanitize_fn: Optional[Callable[[Optional[str]], Optional[str]]] = None,
+ prompt_profile: Optional[str] = None,
) -> bool:
"""Update a single model entry in supported_models.json.
@@ -202,6 +215,10 @@ def update_model_status(
note: Optional note for skip/fail reason
phase_scores: Phase score dict {1: float, 2: float, 3: float, 4: float}
sanitize_fn: Optional callable to sanitize note strings
+ prompt_profile: Phase-4 prompt profile actually used (e.g.
+ "task:translation@en-de"). Sparse: the default "continuation"
+ removes the key (clearing a stale non-default value), None (no
+ Phase-4 result) leaves it untouched.
Returns:
True if entry was found/created and updated
@@ -235,6 +252,12 @@ def update_model_status(
entry[key] = phase_scores[phase_num]
elif key not in entry:
entry[key] = None
+ if prompt_profile is not None and is_default_profile(prompt_profile):
+ entry.pop("prompt_profile", None)
+ elif prompt_profile is not None:
+ entry["prompt_profile"] = prompt_profile
+ if 4 in phase_scores:
+ entry["p4_scoring_version"] = P4_SCORING_VERSION
# Reorder keys so phase scores are always in numerical order
_KEY_ORDER = [
"architecture_id",
@@ -243,6 +266,8 @@ def update_model_status(
"verified_date",
"metadata",
"note",
+ "prompt_profile",
+ "p4_scoring_version",
"phase1_score",
"phase2_score",
"phase3_score",
@@ -281,6 +306,20 @@ def update_model_status(
"phase9_score": phase_scores.get(9),
}
)
+ new_entry = data["models"][-1]
+ extras: list[tuple[str, object]] = []
+ if prompt_profile is not None and not is_default_profile(prompt_profile):
+ extras.append(("prompt_profile", prompt_profile))
+ if phase_scores.get(4) is not None:
+ extras.append(("p4_scoring_version", P4_SCORING_VERSION))
+ if extras:
+ # Keep key position consistent with _KEY_ORDER (after "note").
+ items = list(new_entry.items())
+ idx = [k for k, _ in items].index("note") + 1
+ for offset, pair in enumerate(extras):
+ items.insert(idx + offset, pair)
+ new_entry.clear()
+ new_entry.update(items)
updated = True
if updated:
@@ -296,12 +335,28 @@ def update_model_status(
return updated
+def registry_prompt_profile(model_id: str) -> Optional[str]:
+ """Stored prompt_profile for a model, or None. Uncached read: the sweep
+ rewrites the registry between models."""
+ try:
+ data = load_supported_models_raw()
+ except Exception:
+ return None
+ for entry in data.get("models", []):
+ if entry.get("model_id") == model_id:
+ profile = entry.get("prompt_profile")
+ return profile if isinstance(profile, str) else None
+ return None
+
+
def add_verification_record(
model_id: str,
arch_id: str,
notes: Optional[str] = None,
verified_by: str = "verify_models",
sanitize_fn: Optional[Callable[[Optional[str]], Optional[str]]] = None,
+ prompt_profile: Optional[str] = None,
+ p4_scoring_version: Optional[int] = None,
) -> None:
"""Append a VerificationRecord to verification_history.json.
@@ -325,6 +380,8 @@ def add_verification_record(
verified_by=verified_by,
transformerlens_version=_get_tl_version(),
notes=notes,
+ prompt_profile=prompt_profile,
+ p4_scoring_version=p4_scoring_version,
)
history = load_verification_history()
diff --git a/transformer_lens/tools/model_registry/schemas.py b/transformer_lens/tools/model_registry/schemas.py
index 6cd941c154..8b0b00c0c5 100644
--- a/transformer_lens/tools/model_registry/schemas.py
+++ b/transformer_lens/tools/model_registry/schemas.py
@@ -64,6 +64,8 @@ class ModelEntry:
verified_date: Date when verification was performed
metadata: Optional metadata from HuggingFace
note: Optional note (skip/fail reason, e.g. "Estimated 48 GB exceeds 16 GB limit")
+ prompt_profile: Phase-4 prompt profile used (e.g. "task:translation@en-de");
+ omitted from JSON for the default continuation profile
phase1_score: Benchmark Phase 1 score (HF vs Bridge), 0-100 or None
phase2_score: Benchmark Phase 2 score (Bridge vs HT unprocessed), 0-100 or None
phase3_score: Benchmark Phase 3 score (Bridge vs HT processed), 0-100 or None
@@ -79,6 +81,8 @@ class ModelEntry:
verified_date: Optional[date] = None
metadata: Optional[ModelMetadata] = None
note: Optional[str] = None
+ prompt_profile: Optional[str] = None
+ p4_scoring_version: Optional[int] = None
phase1_score: Optional[float] = None
phase2_score: Optional[float] = None
phase3_score: Optional[float] = None
@@ -88,8 +92,9 @@ class ModelEntry:
phase9_score: Optional[float] = None
def to_dict(self) -> dict:
- """Convert to a JSON-serializable dictionary."""
- return {
+ """Convert to a JSON-serializable dictionary. prompt_profile is sparse:
+ omitted when None so default-profile entries carry no key."""
+ result = {
"architecture_id": self.architecture_id,
"model_id": self.model_id,
"status": self.status,
@@ -104,6 +109,18 @@ def to_dict(self) -> dict:
"phase8_score": self.phase8_score,
"phase9_score": self.phase9_score,
}
+ extras: list[tuple[str, object]] = []
+ if self.prompt_profile is not None:
+ extras.append(("prompt_profile", self.prompt_profile))
+ if self.p4_scoring_version is not None:
+ extras.append(("p4_scoring_version", self.p4_scoring_version))
+ if extras:
+ note_index = list(result).index("note") + 1
+ items = list(result.items())
+ for offset, pair in enumerate(extras):
+ items.insert(note_index + offset, pair)
+ result = dict(items)
+ return result
@classmethod
def from_dict(cls, data: dict) -> "ModelEntry":
@@ -128,6 +145,8 @@ def from_dict(cls, data: dict) -> "ModelEntry":
verified_date=verified_date,
metadata=metadata,
note=data.get("note"),
+ prompt_profile=data.get("prompt_profile"),
+ p4_scoring_version=data.get("p4_scoring_version"),
phase1_score=data.get("phase1_score"),
phase2_score=data.get("phase2_score"),
phase3_score=data.get("phase3_score"),
diff --git a/transformer_lens/tools/model_registry/validate.py b/transformer_lens/tools/model_registry/validate.py
index 17c5049d5d..d070303ea9 100644
--- a/transformer_lens/tools/model_registry/validate.py
+++ b/transformer_lens/tools/model_registry/validate.py
@@ -290,6 +290,31 @@ def _validate_model_entry(data: dict, path: str) -> list[ValidationError]:
if "note" in data and data["note"] is not None:
errors.extend(_validate_string(data["note"], f"{path}.note", min_length=1))
+ # p4_scoring_version (optional sparse int; absent = old GPT-2 scale)
+ if "p4_scoring_version" in data and data["p4_scoring_version"] is not None:
+ version = data["p4_scoring_version"]
+ if not isinstance(version, int) or isinstance(version, bool) or version < 2:
+ errors.append(
+ ValidationError(f"{path}.p4_scoring_version", "must be an int >= 2", version)
+ )
+
+ # prompt_profile (optional sparse string; must parse as a profile spec)
+ if "prompt_profile" in data and data["prompt_profile"] is not None:
+ errors.extend(
+ _validate_string(data["prompt_profile"], f"{path}.prompt_profile", min_length=1)
+ )
+ if isinstance(data["prompt_profile"], str):
+ try:
+ from transformer_lens.benchmarks.text_quality_profiles import (
+ ProfileSpec,
+ )
+
+ ProfileSpec.parse(data["prompt_profile"])
+ except ValueError as e:
+ errors.append(
+ ValidationError(f"{path}.prompt_profile", str(e), data["prompt_profile"])
+ )
+
# verified_date (optional date string)
if "verified_date" in data and data["verified_date"] is not None:
errors.extend(
@@ -310,7 +335,15 @@ def _validate_model_entry(data: dict, path: str) -> list[ValidationError]:
errors.extend(_validate_model_metadata(data["metadata"], f"{path}.metadata"))
# phase scores (optional floats, 0-100 or None)
- for phase_field in ("phase1_score", "phase2_score", "phase3_score"):
+ for phase_field in (
+ "phase1_score",
+ "phase2_score",
+ "phase3_score",
+ "phase4_score",
+ "phase7_score",
+ "phase8_score",
+ "phase9_score",
+ ):
if phase_field in data and data[phase_field] is not None:
val = data[phase_field]
if not isinstance(val, (int, float)) or isinstance(val, bool):
diff --git a/transformer_lens/tools/model_registry/verification.py b/transformer_lens/tools/model_registry/verification.py
index f402aef435..f6734cf4a6 100644
--- a/transformer_lens/tools/model_registry/verification.py
+++ b/transformer_lens/tools/model_registry/verification.py
@@ -29,6 +29,10 @@ class VerificationRecord:
architecture_id: str = "Unknown"
verified_by: Optional[str] = None
transformerlens_version: Optional[str] = None
+ # P4 verdict flips are undiagnosable without knowing which profile and
+ # scoring scale produced the record.
+ prompt_profile: Optional[str] = None
+ p4_scoring_version: Optional[int] = None
notes: Optional[str] = None
invalidated: bool = False
invalidation_reason: Optional[str] = None
@@ -41,6 +45,8 @@ def to_dict(self) -> dict:
"verified_date": self.verified_date.isoformat(),
"verified_by": self.verified_by,
"transformerlens_version": self.transformerlens_version,
+ "prompt_profile": self.prompt_profile,
+ "p4_scoring_version": self.p4_scoring_version,
"notes": self.notes,
"invalidated": self.invalidated,
"invalidation_reason": self.invalidation_reason,
@@ -55,6 +61,8 @@ def from_dict(cls, data: dict) -> "VerificationRecord":
verified_date=date.fromisoformat(data["verified_date"]),
verified_by=data.get("verified_by"),
transformerlens_version=data.get("transformerlens_version"),
+ prompt_profile=data.get("prompt_profile"),
+ p4_scoring_version=data.get("p4_scoring_version"),
notes=data.get("notes"),
invalidated=data.get("invalidated", False),
invalidation_reason=data.get("invalidation_reason"),
diff --git a/transformer_lens/tools/model_registry/verify_models.py b/transformer_lens/tools/model_registry/verify_models.py
index 66ebcca592..df549bf8a1 100644
--- a/transformer_lens/tools/model_registry/verify_models.py
+++ b/transformer_lens/tools/model_registry/verify_models.py
@@ -35,6 +35,10 @@
from pathlib import Path
from typing import Optional
+from transformer_lens.benchmarks.text_quality_profiles import (
+ P4_SCORING_VERSION,
+ p4_pass_threshold,
+)
from transformer_lens.utilities.heterogeneous_config import het_safe_view
# Exit code used for graceful interrupts (Ctrl+C). The wrapper script
@@ -394,6 +398,7 @@ def estimate_benchmark_memory_gb(
dtype: str = "float32",
phases: Optional[list[int]] = None,
use_hf_reference: bool = True,
+ device: str = "cpu",
) -> float:
"""Estimate peak memory needed for benchmark suite.
@@ -420,8 +425,12 @@ def estimate_benchmark_memory_gb(
bpp = bytes_per_param.get(dtype, 4)
model_size_gb = n_params * bpp / (1024**3)
- # GPT-2 scorer overhead (loaded during Phase 4)
- gpt2_overhead_gb = 0.5
+ # Phase-4 judge overhead: measured 2.33 GB RSS loading Qwen2.5-0.5B fp32
+ # on CPU (494M params). Kept slightly above the measurement; over-counting
+ # is the safe direction.
+ # The CPU-pinned judge never occupies accelerator memory; charging it to
+ # a cuda budget produces spurious VRAM skips.
+ judge_overhead_gb = 2.5 if device == "cpu" else 0.0
# Activation/framework overhead as a fraction of model size
overhead_fraction = 0.2
@@ -441,8 +450,8 @@ def estimate_benchmark_memory_gb(
# Bridge + HookedTransformer = 2 copies
phase_peaks.append(model_size_gb * 2.0 * (1 + overhead_fraction))
elif p == 4:
- # Bridge + GPT-2 scorer
- phase_peaks.append(model_size_gb * (1 + overhead_fraction) + gpt2_overhead_gb)
+ # Bridge + judge
+ phase_peaks.append(model_size_gb * (1 + overhead_fraction) + judge_overhead_gb)
return max(phase_peaks) if phase_peaks else model_size_gb
@@ -595,6 +604,18 @@ def _extract_phase_scores(results: list) -> dict[int, Optional[float]]:
return scores
+def _extract_prompt_profile(results: list) -> Optional[str]:
+ """Effective Phase-4 prompt profile from the benchmark details, or None
+ when no Phase-4 result exists. The default "continuation" is reported so
+ the registry write can clear a stale non-default key."""
+ for result in results:
+ if result.phase == 4 and result.details:
+ profile = result.details.get("prompt_profile")
+ if isinstance(profile, str):
+ return profile
+ return None
+
+
# Per-phase minimum score thresholds (0-100).
# Phase 1: Core correctness (bridge vs HF) — must pass everything.
# Phase 2: Hook/cache/gradient tests — most should pass.
@@ -604,7 +625,9 @@ def _extract_phase_scores(results: list) -> dict[int, Optional[float]]:
1: 100.0,
2: 75.0,
3: 75.0,
- 4: 50.0,
+ # Phase 4 floor == the benchmark pass line; a gap between them lets a
+ # failing score carry a clean "completed" note.
+ 4: p4_pass_threshold(),
7: 75.0,
8: 75.0,
9: 75.0,
@@ -760,15 +783,89 @@ def _build_verified_note(
else:
issue_parts.append(f"P{phase}={score}%")
+ p4_uncovered = next(
+ (
+ r.message
+ for r in all_results
+ if r.phase == 4
+ and r.severity == BenchmarkSeverity.SKIPPED
+ and r.message.startswith("P4 skipped:")
+ ),
+ None,
+ )
+ suffix = ""
+ if p4_uncovered:
+ # Keep the gap visible in the registry until prompt coverage is added.
+ reason = p4_uncovered.split("—")[0].replace("P4 skipped:", "").strip()
+ suffix = f"; P4 skipped (uncovered: {reason} — file a coverage issue)"
+
if issue_parts and low_text_quality:
return (
f"Full verification completed with issues, low text quality: {'; '.join(issue_parts)}"
+ + suffix
)
if issue_parts:
- return f"Full verification completed with issues: {'; '.join(issue_parts)}"
+ return f"Full verification completed with issues: {'; '.join(issue_parts)}" + suffix
if low_text_quality:
- return "Full verification completed with issues, low text quality"
- return "Full verification completed"
+ return "Full verification completed with issues, low text quality" + suffix
+ return "Full verification completed" + suffix
+
+
+def _preserved_issue_suffix(model_id: str, eff_phases) -> str:
+ """Sub-100 scores from phases not re-run this pass stay visible in the
+ note; a partial pass must not overwrite tracked residue."""
+ from transformer_lens.tools.model_registry.registry_io import (
+ load_supported_models_raw,
+ )
+
+ try:
+ entry = next(
+ (
+ m
+ for m in load_supported_models_raw().get("models", [])
+ if m.get("model_id") == model_id
+ ),
+ None,
+ )
+ except OSError:
+ return ""
+ if entry is None:
+ return ""
+ residue = []
+ for phase in (2, 3, 7, 8, 9):
+ if phase in (eff_phases or []):
+ continue
+ score = entry.get(f"phase{phase}_score")
+ if score is not None and score < 100.0:
+ residue.append(f"P{phase}={score}%")
+ if not residue:
+ return ""
+ return f" (prior issues retained: {', '.join(residue)})"
+
+
+def _p1_only_core_note(p4_score, all_results: list) -> str:
+ """Note for a core run where P1 passed but P4 did not contribute a pass.
+
+ A skipped P4 is a coverage gap, not a quality failure — the stale
+ (possibly old-scale) score must not be relabeled "poor"."""
+ from transformer_lens.benchmarks.utils import BenchmarkSeverity
+
+ p4_skip_msg = next(
+ (
+ r.message
+ for r in all_results
+ if r.phase == 4
+ and r.severity == BenchmarkSeverity.SKIPPED
+ and r.message.startswith("P4 skipped:")
+ ),
+ None,
+ )
+ if p4_skip_msg is not None:
+ reason = p4_skip_msg.split("—")[0].replace("P4 skipped:", "").strip()
+ return f"Core verification passed; P4 skipped ({reason})"
+ if p4_score is None:
+ return "Core verification passed, but text quality benchmark errored. Needs review"
+ return f"Core verification passed, but text quality poor (P4={p4_score}). Needs review"
def _clear_hf_cache(quiet: bool = False) -> None:
@@ -779,8 +876,16 @@ def _clear_hf_cache(quiet: bool = False) -> None:
if not cache_dir.exists():
return
+ from transformer_lens.benchmarks.text_quality import JUDGE_MODEL_ID
+
+ # The pinned Phase-4 judge is needed by every run; deleting it here would
+ # force a re-download per family.
+ judge_dir = "models--" + JUDGE_MODEL_ID.replace("/", "--")
+
freed = 0
for blobs_dir in cache_dir.glob("models--*/blobs"):
+ if blobs_dir.parent.name == judge_dir:
+ continue
for blob in blobs_dir.iterdir():
try:
size = blob.stat().st_size
@@ -867,21 +972,25 @@ def verify_models(
# phases stays None = full verification for the model.
- # Pre-load the GPT-2 scoring model for Phase 4 so it persists across all
- # models in the batch instead of being loaded and destroyed for each one.
- _scoring_model = None
- _scoring_tokenizer = None
+ # Pre-load the Phase-4 judge so it persists across all models in the batch
+ # instead of being loaded and destroyed for each one.
+ _judge_model = None
+ _judge_tokenizer = None
if phases is None or 4 in phases:
try:
- from transformer_lens.benchmarks.text_quality import _load_scoring_model
+ from transformer_lens.benchmarks.text_quality import (
+ JUDGE_MODEL_ID,
+ JUDGE_REVISION,
+ load_judge,
+ )
- _scoring_model, _scoring_tokenizer = _load_scoring_model("gpt2", device)
+ _judge_model, _judge_tokenizer = load_judge()
if not quiet:
- print("Pre-loaded GPT-2 scoring model for Phase 4")
+ print(f"Pre-loaded Phase 4 judge {JUDGE_MODEL_ID}@{JUDGE_REVISION[:8]}")
except Exception as e:
if not quiet:
- print(f"Warning: Could not pre-load GPT-2 scorer: {e}")
- print(" Phase 4 will load its own scorer per model.")
+ print(f"Warning: Could not pre-load Phase 4 judge: {e}")
+ print(" Phase 4 will load its own judge per model.")
total = len(candidates)
for i, candidate in enumerate(candidates, 1):
@@ -953,7 +1062,7 @@ def verify_models(
# Step 2: Check memory
estimated_mem = estimate_benchmark_memory_gb(
- n_params, dtype, phases=phases_to_run, use_hf_reference=use_hf_reference
+ n_params, dtype, phases=phases_to_run, use_hf_reference=use_hf_reference, device=device
)
candidate.estimated_memory_gb = estimated_mem
if not quiet:
@@ -985,7 +1094,14 @@ def verify_models(
}
torch_dtype = _dtype_map[dtype]
+ from transformer_lens.benchmarks.text_quality_profiles import resolve_profile
+ from transformer_lens.tools.model_registry.registry_io import (
+ registry_prompt_profile,
+ )
+
+ resolved_profile = str(resolve_profile(model_id, arch, registry_prompt_profile(model_id)))
if not quiet:
+ print(f" Prompt profile: {resolved_profile}")
print(f" Running phases {phases} in a single benchmark call...")
try:
all_results = run_benchmark_suite(
@@ -997,8 +1113,9 @@ def verify_models(
verbose=not quiet,
phases=phases_to_run,
trust_remote_code=needs_remote_code,
- scoring_model=_scoring_model,
- scoring_tokenizer=_scoring_tokenizer,
+ judge_model=_judge_model,
+ judge_tokenizer=_judge_tokenizer,
+ prompt_profile=resolved_profile,
)
except Exception as e:
error_msg = str(e)
@@ -1095,7 +1212,9 @@ def verify_models(
if p1_pass and p4_pass and p7_pass and p8_pass:
partial_status = STATUS_VERIFIED
- partial_note = "Core verification completed"
+ partial_note = "Core verification completed" + _preserved_issue_suffix(
+ model_id, eff_phases
+ )
elif p1_pass and p4_pass and not p7_pass:
p7_score = filtered_scores.get(7)
if p7_score is None:
@@ -1112,9 +1231,7 @@ def verify_models(
)
elif p1_pass:
partial_status = STATUS_VERIFIED
- partial_note = (
- "Core verification passed, but text quality poor. Needs review"
- )
+ partial_note = _p1_only_core_note(p4, all_results)
else:
# P1 failed — build a descriptive failure note
partial_status = STATUS_FAILED
@@ -1151,6 +1268,7 @@ def verify_models(
status=partial_status,
phase_scores=filtered_scores,
note=partial_note,
+ prompt_profile=_extract_prompt_profile(all_results),
)
# A provisional run was not numerically verified; do not write a
# verification-history record (VerificationHistory.is_verified()
@@ -1161,6 +1279,8 @@ def verify_models(
arch,
notes=partial_note,
sanitize_fn=_sanitize_note,
+ prompt_profile=_extract_prompt_profile(all_results),
+ p4_scoring_version=(P4_SCORING_VERSION if 4 in filtered_scores else None),
)
if partial_status == STATUS_FAILED:
progress.failed.append(model_id)
@@ -1205,6 +1325,7 @@ def verify_models(
written_status,
phase_scores=phase_scores,
note=note,
+ prompt_profile=_extract_prompt_profile(all_results),
)
# Provisional runs are not numerically verified — no history record
# (is_verified() would otherwise report them as verified).
@@ -1213,6 +1334,8 @@ def verify_models(
model_id,
arch,
notes=note,
+ prompt_profile=_extract_prompt_profile(all_results),
+ p4_scoring_version=(P4_SCORING_VERSION if 4 in phase_scores else None),
)
if is_provisional:
progress.provisional.append(model_id)
@@ -1235,12 +1358,15 @@ def verify_models(
note=note,
phase_scores=phase_scores,
sanitize_fn=_sanitize_note,
+ prompt_profile=_extract_prompt_profile(all_results),
)
add_verification_record(
model_id,
arch,
notes=note,
sanitize_fn=_sanitize_note,
+ prompt_profile=_extract_prompt_profile(all_results),
+ p4_scoring_version=(P4_SCORING_VERSION if 4 in phase_scores else None),
)
progress.failed.append(model_id)
@@ -1275,9 +1401,9 @@ def verify_models(
_save_checkpoint(progress)
# Clean up pre-loaded scoring model
- if _scoring_model is not None:
- del _scoring_model
- del _scoring_tokenizer
+ if _judge_model is not None:
+ del _judge_model
+ del _judge_tokenizer
gc.collect()
return progress
@@ -1289,6 +1415,7 @@ def _print_dry_run(
max_memory_gb: float,
phases: Optional[list[int]] = None,
use_hf_reference: bool = True,
+ device: str = "cpu",
) -> None:
"""Print what would be tested in a dry run."""
print(f"\nDry run: {len(candidates)} models would be tested")
@@ -1314,7 +1441,11 @@ def _print_dry_run(
try:
n_params = estimate_model_params(c.model_id)
mem = estimate_benchmark_memory_gb(
- n_params, dtype, phases=phases_to_run, use_hf_reference=use_hf_reference
+ n_params,
+ dtype,
+ phases=phases_to_run,
+ use_hf_reference=use_hf_reference,
+ device=device,
)
status = "OK" if mem <= max_memory_gb else "SKIP (too large)"
if mem > max_memory_gb:
@@ -1562,6 +1693,7 @@ def main() -> None:
max_memory_gb,
phases=args.phases,
use_hf_reference=not args.no_hf_reference,
+ device=args.device,
)
return
diff --git a/transformer_lens/train.py b/transformer_lens/train.py
index db6f5d7697..13964e0923 100644
--- a/transformer_lens/train.py
+++ b/transformer_lens/train.py
@@ -4,6 +4,7 @@
modeling tasks.
"""
+import dataclasses
from dataclasses import dataclass
from typing import Optional, Union
@@ -75,6 +76,10 @@ def train(
The trained model
"""
+ # Work on a copy: mutating the caller's config (wandb_project_name/device
+ # defaults below) was a silent side effect the caller never asked for.
+ config = dataclasses.replace(config)
+
torch.manual_seed(config.seed)
model.train()
diff --git a/transformer_lens/utilities/architectures.py b/transformer_lens/utilities/architectures.py
index cf9558ae52..a1339a82a2 100644
--- a/transformer_lens/utilities/architectures.py
+++ b/transformer_lens/utilities/architectures.py
@@ -25,6 +25,14 @@
"SwitchTransformersForConditionalGeneration",
}
+# Post-norm decoders: ln1/ln2 normalize each sublayer's OUTPUT before the residual
+# add, so LN folding and writing-weight centering (which assume the gain sits on a
+# sublayer's INPUT) are not valid algebra for them.
+POST_NORM_ARCHITECTURES: set[str] = {
+ "Olmo2ForCausalLM",
+ "Olmo3ForCausalLM",
+}
+
# Masked language models (BERT-style, no text generation)
MASKED_LM_ARCHITECTURES: set[str] = {
"BertForMaskedLM",
diff --git a/transformer_lens/utilities/lm_utils.py b/transformer_lens/utilities/lm_utils.py
index a3d4f79326..79d47c975e 100644
--- a/transformer_lens/utilities/lm_utils.py
+++ b/transformer_lens/utilities/lm_utils.py
@@ -9,22 +9,24 @@
import torch
import torch.nn.functional as F
-from jaxtyping import Float, Int
+from jaxtyping import Bool, Float, Int
def lm_cross_entropy_loss(
logits: Float[torch.Tensor, "batch pos d_vocab"],
tokens: Int[torch.Tensor, "batch pos"],
- attention_mask: Optional[Int[torch.Tensor, "batch pos"]] = None,
+ attention_mask: Optional[
+ Union[Bool[torch.Tensor, "batch pos"], Int[torch.Tensor, "batch pos"]]
+ ] = None,
per_token: bool = False,
-) -> Union[Float[torch.Tensor, ""], Float[torch.Tensor, "batch pos"]]:
+) -> Union[Float[torch.Tensor, ""], Float[torch.Tensor, "batch pos-1"]]:
"""Cross entropy loss for the language model, gives the loss for predicting the NEXT token.
Args:
logits (torch.Tensor): Logits. Shape [batch, pos, d_vocab]
tokens (torch.Tensor[int64]): Input tokens. Shape [batch, pos]
- attention_mask (torch.Tensor[int64], optional): Attention mask. Shape [batch, pos]. Used to
- mask out padding tokens. Defaults to None.
+ attention_mask (torch.Tensor[int64 or bool], optional): Attention mask. Shape [batch, pos].
+ Used to mask out padding tokens. Defaults to None.
per_token (bool, optional): Whether to return the log probs predicted for the correct token, or the loss (ie mean of the predicted log probs). Note that the returned array has shape [batch, seq-1] as we cannot predict the first token (alternately, we ignore the final logit). Defaults to False.
"""
log_probs = F.log_softmax(logits, dim=-1)
@@ -34,10 +36,14 @@ def lm_cross_entropy_loss(
predicted_log_probs = log_probs[..., :-1, :].gather(dim=-1, index=tokens[..., 1:, None])[..., 0]
if attention_mask is not None:
+ assert attention_mask.shape == tokens.shape, (
+ "attention_mask must have the same shape as tokens, "
+ f"got {tuple(attention_mask.shape)} and {tuple(tokens.shape)}"
+ )
# Ignore token positions which are masked out or where the next token is masked out
# (generally padding tokens)
next_token_mask = torch.logical_and(attention_mask[:, :-1], attention_mask[:, 1:])
- predicted_log_probs *= next_token_mask
+ predicted_log_probs = predicted_log_probs.masked_fill(~next_token_mask, 0.0)
n_tokens = next_token_mask.sum().item()
else:
n_tokens = predicted_log_probs.numel()
@@ -51,7 +57,7 @@ def lm_accuracy(
logits: Float[torch.Tensor, "batch pos d_vocab"],
tokens: Int[torch.Tensor, "batch pos"],
per_token: bool = False,
-) -> Union[Float[torch.Tensor, ""], Float[torch.Tensor, "batch pos"]]:
+) -> Union[Float[torch.Tensor, ""], Bool[torch.Tensor, "batch pos-1"]]:
"""Cross-Entropy Accuracy for Language Modelling. We measure the accuracy on the logits for predicting the NEXT token.
If per_token is True, returns the boolean for top 1 accuracy for each token in the batch. Note that this has size [batch, seq_len-1], as we cannot predict the first token.
diff --git a/transformer_lens/utilities/multi_gpu.py b/transformer_lens/utilities/multi_gpu.py
index e29023aedf..a4915088e6 100644
--- a/transformer_lens/utilities/multi_gpu.py
+++ b/transformer_lens/utilities/multi_gpu.py
@@ -249,7 +249,11 @@ def is_mixed_cpu_gpu(values: Any) -> bool:
def cast_floating_params_to_dtype(model: nn.Module, dtype: torch.dtype) -> None:
- """Cast materialized floating parameters while preserving Accelerate offload hooks."""
+ """Cast materialized floating parameters while preserving Accelerate offload hooks.
+
+ Skips one-byte floats (FP8 dtypes like float8_e8m0fnu) which are quantizer-owned
+ scale parameters — casting them corrupts the quantization format.
+ """
from accelerate.utils import align_module_device
for module in model.modules():
@@ -259,9 +263,28 @@ def cast_floating_params_to_dtype(model: nn.Module, dtype: torch.dtype) -> None:
continue
if param.device.type == "meta":
continue
+ # Skip one-byte floats (FP8 scale tensors): they are quantizer-owned
+ # and casting them breaks the weight/scale pair relationship.
+ if param.dtype.itemsize < 2:
+ continue
param.data = param.data.to(dtype=dtype)
+def maybe_cast_floating_params(model: nn.Module, dtype: torch.dtype) -> None:
+ """Cast floating params to dtype, skipping models with active quantization.
+
+ When a model has an active quantization_config, the quantizer owns specific
+ dtypes (e.g., FP8 scales) that must not be overwritten. This helper wraps
+ the cast with that check.
+
+ See: https://github.com/TransformerLensOrg/TransformerLens/issues/1713
+ """
+ from transformer_lens.utilities.quantization import quantization_method
+
+ if quantization_method(getattr(model, "config", None)) is None:
+ cast_floating_params_to_dtype(model, dtype)
+
+
def find_embedding_device(hf_model: Any) -> Optional[torch.device]:
"""Return the device that input tokens should be placed on for a dispatched HF model.
diff --git a/transformer_lens/utilities/tokenize_utils.py b/transformer_lens/utilities/tokenize_utils.py
index beedfd7b62..d5310c7649 100644
--- a/transformer_lens/utilities/tokenize_utils.py
+++ b/transformer_lens/utilities/tokenize_utils.py
@@ -7,7 +7,7 @@
import os
from copy import deepcopy
-from typing import Any
+from typing import Any, Optional
import einops
import numpy as np
@@ -182,18 +182,27 @@ def get_tokenizer_with_bos(tokenizer: PreTrainedTokenizerBase) -> PreTrainedToke
def get_input_with_manually_prepended_bos(
- bos_token: str, input: str | list[str]
+ bos_token: Optional[str], input: str | list[str]
) -> str | list[str]:
"""
Manually prepends the bos token to the input.
Args:
- bos_token (str): The BOS token to prepend.
+ bos_token (Optional[str]): The BOS token to prepend, or None for a tokenizer
+ that has none (e.g. BERT, T5).
input (str | list[str]): The input to prepend the bos token to.
Returns:
- str | list[str]: The input with the bos token manually prepended.
+ str | list[str]: The input with the bos token manually prepended, or unchanged
+ when there is no BOS token to prepend.
"""
+ if bos_token is None:
+ # Nothing to prepend. Callers reach this when prepend_bos is asked for and
+ # cfg.tokenizer_prepends_bos is False — correctly so for a BOS-less tokenizer,
+ # since detect_tokenizer_bos_eos() requires a bos_token_id. Concatenating
+ # would raise a TypeError naming neither the tokenizer nor the flag.
+ return input
+
if isinstance(input, str):
input = bos_token + input
else:
@@ -202,7 +211,9 @@ def get_input_with_manually_prepended_bos(
def get_tokens_with_bos_removed(
- tokenizer: PreTrainedTokenizerBase, tokens: torch.Tensor
+ tokenizer: PreTrainedTokenizerBase,
+ tokens: torch.Tensor,
+ padding_side: str | None = None,
) -> torch.Tensor:
"""
Removes the bos token from the beginning of each sequence in `tokens`.
@@ -211,11 +222,23 @@ def get_tokens_with_bos_removed(
Args:
tokenizer (PreTrainedTokenizerBase): The tokenizer used to tokenize the input.
tokens (torch.Tensor): The tokenized input.
+ padding_side: The side used to pad ``tokens``. Defaults to the tokenizer setting.
Returns:
torch.Tensor: The tokenized input with the bos token removed.
"""
- if tokenizer.padding_side == "right":
+ if tokenizer.bos_token_id is None:
+ # Nothing to remove (#1628). Callers reach this when cfg.tokenizer_prepends_bos
+ # says the tokenizer prepends a BOS but the tokenizer has none — a stale
+ # flag, since detect_tokenizer_bos_eos() requires a bos_token_id. Trusting
+ # it here would drop a real first token under right padding ([CLS] for a
+ # BERT tokenizer), and compare tokens against None under left padding.
+ return tokens
+
+ if padding_side is None:
+ padding_side = tokenizer.padding_side
+
+ if padding_side == "right":
return tokens[..., 1:]
else:
@@ -234,7 +257,10 @@ def get_tokens_with_bos_removed(
def get_attention_mask(
- tokenizer: PreTrainedTokenizerBase, tokens: torch.Tensor, prepend_bos: bool
+ tokenizer: PreTrainedTokenizerBase,
+ tokens: torch.Tensor,
+ prepend_bos: bool,
+ padding_side: str | None = None,
) -> torch.Tensor:
"""
Computes the attention mask for the tokenized input.
@@ -246,6 +272,7 @@ def get_attention_mask(
tokenizer (PreTrainedTokenizerBase): The tokenizer used for tokenization.
tokens (torch.Tensor): The tokenized input.
prepend_bos (bool): If True, a BOS token is prepended to the input.
+ padding_side: The side used to pad ``tokens``. Defaults to the tokenizer setting.
Returns:
torch.Tensor: The attention mask for the input.
@@ -255,9 +282,11 @@ def get_attention_mask(
attention_mask = torch.ones_like(tokens)
if tokenizer is None:
return attention_mask
+ if padding_side is None:
+ padding_side = tokenizer.padding_side
is_not_pad_token = tokens.ne(tokenizer.pad_token_id)
- if tokenizer.padding_side == "right":
+ if padding_side == "right":
# Zero-out the rightmost trailing pad tokens
is_trailing_pad = get_cumsum_along_dim(is_not_pad_token, -1, reverse=True) == 0
attention_mask[is_trailing_pad] = 0
diff --git a/transformer_lens/weight_processing.py b/transformer_lens/weight_processing.py
index e4b8ccabb9..13c07760da 100644
--- a/transformer_lens/weight_processing.py
+++ b/transformer_lens/weight_processing.py
@@ -16,6 +16,7 @@
from transformer_lens.FactoredMatrix import FactoredMatrix
from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter
from transformer_lens.utilities import filter_dict_by_prefix
+from transformer_lens.utilities.architectures import POST_NORM_ARCHITECTURES
class ProcessWeights:
@@ -1130,9 +1131,11 @@ def center_writing_weights(
Returns:
Dict[str, torch.Tensor]: Modified state dict with centered writing weights.
"""
- # Skip centering for Olmo2 models - input of attn of 1st layer is not normed
- if getattr(cfg, "original_architecture", None) == "Olmo2ForCausalLM":
- print("Not centering embedding weights for Olmo2ForCausalLM")
+ # Post-norm models leave the first attention's input un-normed, so centering
+ # the embedding would shift a residual stream nothing re-normalizes.
+ architecture = getattr(cfg, "original_architecture", None)
+ if architecture in POST_NORM_ARCHITECTURES:
+ print(f"Not centering embedding weights for {architecture}")
else:
# Make a deep copy to avoid modifying the original
embed_W_E_key = ProcessWeights._get_param_key("embed.W_E", adapter)