diff --git a/.github/workflows/oracle-parity.yml b/.github/workflows/oracle-parity.yml new file mode 100644 index 0000000000..540729e327 --- /dev/null +++ b/.github/workflows/oracle-parity.yml @@ -0,0 +1,57 @@ +name: Oracle Parity + +# Runs the @pytest.mark.slow oracle parity suite for JacobianLens. +# Not part of the standard PR gate — requires ~4 GB VRAM and network access. +# Triggered manually or automatically when the test file or this workflow changes. +on: + workflow_dispatch: + push: + branches: + - main + - dev* + paths: + - "tests/integration/test_jacobian_lens_oracle_parity.py" + - ".github/workflows/oracle-parity.yml" + +permissions: + contents: read + +jobs: + oracle-parity-checks: + name: Oracle Parity Checks + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.12" + activate-environment: true + enable-cache: true + + - name: Install dependencies + run: | + uv lock --check + uv sync + + - name: Install oracle (pinned ORACLE_COMMIT from test file) + run: | + ORACLE_COMMIT=$(python -c " + import re, pathlib + src = pathlib.Path('tests/integration/test_jacobian_lens_oracle_parity.py').read_text() + print(re.search(r'ORACLE_COMMIT = \"([^\"]+)\"', src).group(1)) + ") + uv pip install "git+https://github.com/anthropics/jacobian-lens.git@${ORACLE_COMMIT}" + + - name: Authenticate HuggingFace + if: env.HF_TOKEN != '' + run: uv run python -c "import os; from huggingface_hub import login; login(token=os.environ['HF_TOKEN'])" + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + + - name: Oracle Parity Test + run: uv run pytest tests/integration/test_jacobian_lens_oracle_parity.py -v -m slow + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} diff --git a/README.md b/README.md index c76c8fe91e..916b108056 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ interpretability](https://distill.pub/2020/circuits/zoom-in/) of GPT-2 Style lan goal of mechanistic interpretability is to take a trained model and reverse engineer the algorithms the model learned during training from its weights. -TransformerLens lets you load in 9,000+ open source language models across 50+ architecture families, +TransformerLens lets you load in 15,000+ open source language models across 140+ architecture families, and exposes the internal activations of the model to you. You can cache any internal activation in the model, and add in functions to edit, remove or replace these activations as the model runs. @@ -52,7 +52,7 @@ logits, activations = bridge.run_with_cache("Hello World") > Gated models (Llama, Mistral, Gemma, ...) require `HF_TOKEN` in your environment. See [Environment Variables](https://TransformerLensOrg.github.io/TransformerLens/content/getting_started.html#environment-variables) for the full list. -`TransformerBridge` is the recommended 3.0 path and supports 9,000+ models across 50+ architecture families (see [`supported_models.json`](transformer_lens/tools/model_registry/data/supported_models.json) for the full inventory). By default it preserves raw HuggingFace weights – logits and activations match HF, *not* legacy `HookedTransformer` (which folds LayerNorm and centers weights by default). Call `bridge.enable_compatibility_mode()` after booting for HookedTransformer-equivalent numerics. The legacy `HookedTransformer.from_pretrained` API is still available but deprecated — see the [Migrating to TransformerLens 3](https://TransformerLensOrg.github.io/TransformerLens/content/migrating_to_v3.html) guide. +`TransformerBridge` is the recommended path and supports 15,000+ models across 140+ architecture families (see [`supported_models.json`](transformer_lens/tools/model_registry/data/supported_models.json) for the full inventory). By default it preserves raw HuggingFace weights – logits and activations match HF, *not* legacy `HookedTransformer` (which folds LayerNorm and centers weights by default). Call `bridge.enable_compatibility_mode()` after booting for HookedTransformer-equivalent numerics. The legacy `HookedTransformer.from_pretrained` API is still available but deprecated — see the [Migrating to TransformerLens 3](https://TransformerLensOrg.github.io/TransformerLens/content/migrating_to_v3.html) guide. ## Key Tutorials diff --git a/demos/Grokking_Demo.ipynb b/demos/Grokking_Demo.ipynb index 2566cca49d..0eb0f88fe3 100644 --- a/demos/Grokking_Demo.ipynb +++ b/demos/Grokking_Demo.ipynb @@ -385,7 +385,18 @@ "cell_type": "code", "execution_count": 15, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DeprecationWarning:\n", + "\n", + "HookedTransformer is deprecated and will be removed in 4.0. Use TransformerBridge.boot_transformers(...) instead, then call enable_compatibility_mode() for HookedTransformer-equivalent numerics.\n", + "\n" + ] + } + ], "source": [ "model = HookedTransformer(cfg)" ] diff --git a/demos/Jacobian_Lens_Demo.ipynb b/demos/Jacobian_Lens_Demo.ipynb index 4f1a35497d..c17bd1c94b 100644 --- a/demos/Jacobian_Lens_Demo.ipynb +++ b/demos/Jacobian_Lens_Demo.ipynb @@ -34,12 +34,18 @@ "\n", "1. loads a published lens from the Hub,\n", "2. reads J-lens vs logit-lens tokens on a two-hop prompt with an *unspoken intermediate*,\n", - "3. causally swaps one concept for another (France → China) in lens coordinates, and\n", - "4. steers with a J-lens direction.\n", + "3. causally swaps one concept for another (France → China) in lens coordinates,\n", + "4. quantifies that intervention over all 48 ordered country trials, and\n", + "5. steers with a J-lens direction.\n", + "\n", + "On `Qwen/Qwen3.5-4B` (Bridge-only; Anthropic's\n", + "[`jacobian-lens` walkthrough](https://github.com/anthropics/jacobian-lens/blob/main/walkthrough.ipynb) model):\n", + "\n", + "6. loads the published n=1000 lens and repeats the two-hop readout.\n", "\n", "**Note**: `google/gemma-2-2b` is a gated Hugging Face model — accept its license and authenticate\n", - "(`hf auth login` / `HF_TOKEN`) before running. The demo uses ~6 GB of GPU memory\n", - "(executed on an 8 GB card), so a free Colab T4 should work; GPUs without native BF16 use FP16 automatically." + "(`hf auth login` / `HF_TOKEN`) before running. The demo uses ~8 GB of GPU memory\n", + "(executed on an 10 GB card), so a free Colab T4 should work; GPUs without native BF16 use FP16 automatically.\n" ] }, { @@ -56,10 +62,10 @@ "id": "ae487ba2", "metadata": { "execution": { - "iopub.execute_input": "2026-07-11T09:06:11.958584Z", - "iopub.status.busy": "2026-07-11T09:06:11.958352Z", - "iopub.status.idle": "2026-07-11T09:06:12.029888Z", - "shell.execute_reply": "2026-07-11T09:06:12.029521Z" + "iopub.execute_input": "2026-07-26T08:09:54.389362Z", + "iopub.status.busy": "2026-07-26T08:09:54.389176Z", + "iopub.status.idle": "2026-07-26T08:09:55.209865Z", + "shell.execute_reply": "2026-07-26T08:09:55.208587Z" } }, "outputs": [ @@ -67,7 +73,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "Running as a Jupyter notebook - intended for development only!\n" + "Running as a Jupyter notebook - intended for development only!\n", + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" ] } ], @@ -103,17 +111,17 @@ "id": "fe86001d", "metadata": { "execution": { - "iopub.execute_input": "2026-07-11T09:06:12.031375Z", - "iopub.status.busy": "2026-07-11T09:06:12.031092Z", - "iopub.status.idle": "2026-07-11T09:06:27.807249Z", - "shell.execute_reply": "2026-07-11T09:06:27.806799Z" + "iopub.execute_input": "2026-07-26T08:09:55.211867Z", + "iopub.status.busy": "2026-07-26T08:09:55.211738Z", + "iopub.status.idle": "2026-07-26T08:12:10.124915Z", + "shell.execute_reply": "2026-07-26T08:12:10.121831Z" } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8b9855cb054f4b06803233534b78ddf9", + "model_id": "d66ac557d99c4599bbf1fd8700ce3613", "version_major": 2, "version_minor": 0 }, @@ -128,7 +136,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "loaded gemma-2-2b: 26 layers, d_model=2304, device='cuda'\n" + "loaded gemma-2-2b: 26 layers, d_model=2304, device='cpu'\n" ] } ], @@ -139,6 +147,9 @@ "from transformer_lens.model_bridge import TransformerBridge\n", "from transformer_lens.tools.analysis import JacobianLens\n", "\n", + "MODEL_ID = \"google/gemma-2-2b\"\n", + "MODEL_REVISION = \"c5ebcd40d208330abc697524c919956e692655cf\"\n", + "\n", "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", "model_dtype = (\n", " torch.bfloat16\n", @@ -147,9 +158,14 @@ ")\n", "# Raw HF weights (the Bridge default) — the published lenses are fitted on raw\n", "# activations, and JacobianLens refuses compatibility mode or process_weights.\n", - "model = TransformerBridge.boot_transformers(\"google/gemma-2-2b\", dtype=model_dtype, device=device)\n", + "model = TransformerBridge.boot_transformers(\n", + " MODEL_ID,\n", + " revision=MODEL_REVISION,\n", + " dtype=model_dtype,\n", + " device=device,\n", + ")\n", "model.eval()\n", - "print(f\"loaded gemma-2-2b: {model.cfg.n_layers} layers, d_model={model.cfg.d_model}, {device=}\")" + "print(f\"loaded gemma-2-2b: {model.cfg.n_layers} layers, d_model={model.cfg.d_model}, {device=}\")\n" ] }, { @@ -170,10 +186,10 @@ "id": "f961d030", "metadata": { "execution": { - "iopub.execute_input": "2026-07-11T09:06:27.824146Z", - "iopub.status.busy": "2026-07-11T09:06:27.823519Z", - "iopub.status.idle": "2026-07-11T09:06:28.209427Z", - "shell.execute_reply": "2026-07-11T09:06:28.208823Z" + "iopub.execute_input": "2026-07-26T08:12:10.133725Z", + "iopub.status.busy": "2026-07-26T08:12:10.133060Z", + "iopub.status.idle": "2026-07-26T08:12:19.284997Z", + "shell.execute_reply": "2026-07-26T08:12:19.283932Z" } }, "outputs": [ @@ -190,13 +206,20 @@ ], "source": [ "# NBVAL_IGNORE_OUTPUT (download progress)\n", + "LENS_REPO = \"neuronpedia/jacobian-lens\"\n", + "LENS_FILE = (\n", + " \"gemma-2-2b/jlens/Salesforce-wikitext/\"\n", + " \"gemma-2-2b_jacobian_lens.pt\"\n", + ")\n", + "LENS_REVISION = \"a4114d7752d11eb546e6cf372213d7e75526d3a1\"\n", + "\n", "lens = JacobianLens.from_pretrained(\n", - " \"neuronpedia/jacobian-lens\",\n", - " filename=\"gemma-2-2b/jlens/Salesforce-wikitext/gemma-2-2b_jacobian_lens.pt\",\n", - " revision=\"a4114d7752d11eb546e6cf372213d7e75526d3a1\",\n", + " LENS_REPO,\n", + " filename=LENS_FILE,\n", + " revision=LENS_REVISION,\n", " model=model,\n", ")\n", - "lens" + "lens\n" ] }, { @@ -218,10 +241,10 @@ "id": "1b19ec1a", "metadata": { "execution": { - "iopub.execute_input": "2026-07-11T09:06:28.217489Z", - "iopub.status.busy": "2026-07-11T09:06:28.217204Z", - "iopub.status.idle": "2026-07-11T09:06:29.114425Z", - "shell.execute_reply": "2026-07-11T09:06:29.113969Z" + "iopub.execute_input": "2026-07-26T08:12:19.289064Z", + "iopub.status.busy": "2026-07-26T08:12:19.288868Z", + "iopub.status.idle": "2026-07-26T08:12:27.284271Z", + "shell.execute_reply": "2026-07-26T08:12:27.283384Z" } }, "outputs": [ @@ -275,16 +298,16 @@ "id": "2e0887dd", "metadata": { "execution": { - "iopub.execute_input": "2026-07-11T09:06:29.118703Z", - "iopub.status.busy": "2026-07-11T09:06:29.118453Z", - "iopub.status.idle": "2026-07-11T09:06:29.588577Z", - "shell.execute_reply": "2026-07-11T09:06:29.588189Z" + "iopub.execute_input": "2026-07-26T08:12:27.286619Z", + "iopub.status.busy": "2026-07-26T08:12:27.286483Z", + "iopub.status.idle": "2026-07-26T08:12:35.541193Z", + "shell.execute_reply": "2026-07-26T08:12:35.539875Z" } }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAy4AAAGGCAYAAACdY9UBAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3XV8E+cfwPFPUnenRmmhxd2d4s5wGLDh7jDYYPyAIYNtuOvQwYbDYGzI8OHuTimUunub5H5/hJaGttBSSYDn/Xr11fZyufteLrk833tMJkmShCAIgiAIgiAIgg6TazsAQRAEQRAEQRCE9xGJiyAIgiAIgiAIOk8kLoIgCIIgCIIg6DyRuAiCIAiCIAiCoPNE4iIIgiAIgiAIgs4TiYsgCIIgCIIgCDpPJC6CIAiCIAiCIOg8kbgIgiAIgiAIgqDzROIiCIIgCIIgCILO+6QSlxMnTiCTyThx4oS2Q9G6H374AZlMRkhISJ7u59GjRzRt2hQrKytkMhl79+7N8TZ79+6Nh4dHjreT1zw8POjdu3ee7ye/zqWgO3x8fJDJZMydO1fboQgfgaxei2QyGT/88EOex5PyXbxz584835eQffXr16dMmTLaDuODbNiwAZlMho+PzzvXS/neTOvtz0nKti5fvpwHkeacrseXVv369alfv36+7OujSVw+phP4OenVqxe3bt3ixx9/ZPPmzVSpUkXbIemUuLg4fvjhB5FM54LevXunuzDKZLIMf5ycnHJ9/8uXL2fDhg0f/HwPD49M4037k5N9CEJ+2rp1KwsXLtR2GDpt1qxZuXJDTxDy08GDB/P8JkdKuT679PMgFuEzER8fz7lz55g0aRLDhw/Pte2uWbMGlUqVa9vTpri4OKZNmwaQb3cjPjdNmjShZ8+eGstMTExyfT/Lly/H3t7+g2vZFi5cSExMTOr/Bw8e5Pfff2fBggXY29unLq9Vq1ZOQxWEDMXHx6Ovn3tf+1u3buX27duMHj0617b5qZk1axadOnWiXbt22g7ls/fgwQPk8o/mfr1WHTx4kGXLlmU5eTl8+HDeBpSGSFy0SKVSkZSUhLGxsbZD+SDBwcEAWFtb5+p2DQwMcnV7wqetWLFifPXVV9oO473eLrgEBATw+++/065du3RNI9/XDOJjEhsbi5mZWbrlkiSRkJCQJ0mmkLGP9bvmc5HZZ0XIHUZGRtoO4ZNlaGiYb/v6LFLPCxcu0Lx5c6ysrDA1NcXb25v//vtPY52U9pCPHz+md+/eWFtbY2VlRZ8+fYiLi9NY98iRI9SpUwdra2vMzc0pXrw433///XvjkMlkDB8+nC1btlC6dGmMjIz4559/AJg7dy61atXCzs4OExMTKleunGH74JRt7N27lzJlymBkZETp0qVTt/Muz58/x8vLizJlyhAYGPjOda9du0aLFi2wtLTE3NycRo0acf78eY3Xy93dHYDx48cjk8ne2S8lpc3ztm3b+P7773FycsLMzIwvvviCFy9eaKz7dh+XtO39V69ejaenJ0ZGRlStWpVLly6le665uTl+fn60a9cOc3NzHBwcGDduHEqlUmNdlUrFwoULKV26NMbGxjg6OjJo0CDCw8M11pMkiZkzZ1KwYEFMTU1p0KABd+7ceefrlxK3g4MDANOmTUttCpT2DsaxY8eoW7cuZmZmWFtb07ZtW+7du/febWd0LiMiIhg9ejRubm4YGRnh5eXFzz//rFF7lZ3XMiAggD59+lCwYEGMjIxwdnambdu2H1WhOqufK4DffvuNatWqYWpqio2NDfXq1Uu9i+Th4cGdO3c4efJk6nlMW4P25MkTnjx5kifH8L7zBHD//n06deqEra0txsbGVKlShT///DNL21epVCxatIiyZctibGyMg4MDzZs3T22Wm/KeyagJ29vv55Tr6N27d+nevTs2NjbUqVMHUL+GrVu35tChQ1SpUgUTExNWrVoF5P57N+U16dKlCw4ODpiYmFC8eHEmTZoEwPHjx5HJZOzZsyfd87Zu3YpMJuPcuXOZvmYZtZ+HjNvfX758mWbNmmFvb4+JiQmFCxemb9++Gs/L62tRiszOV1a+995Wv359/vrrL54/f576mXj7O0ClUvHjjz9SsGBBjI2NadSoEY8fP063rax8R2cmISGBH374gWLFimFsbIyzszMdOnTQ+DzGxsbyzTffpL6/ihcvzty5c5EkKXWdD3mfv+91k8lkxMbGsnHjxtTXKKXGNrPPyvr165HJZFy7di1dHLNmzUJPTw8/P79MX4/o6GhGjx6Nh4cHRkZGFChQgCZNmnD16tV06969e5cGDRpgamqKq6srv/zyi8bjSUlJTJkyhcqVK2NlZYWZmRl169bl+PHjGuul/WwuWLAAd3d3TExM8Pb25vbt2+n2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", + "image/png": "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", "text/plain": [ "
" ] @@ -336,10 +359,10 @@ "id": "b2359547", "metadata": { "execution": { - "iopub.execute_input": "2026-07-11T09:06:29.590963Z", - "iopub.status.busy": "2026-07-11T09:06:29.590451Z", - "iopub.status.idle": "2026-07-11T09:06:30.101969Z", - "shell.execute_reply": "2026-07-11T09:06:30.101589Z" + "iopub.execute_input": "2026-07-26T08:12:35.547418Z", + "iopub.status.busy": "2026-07-26T08:12:35.546068Z", + "iopub.status.idle": "2026-07-26T08:12:42.047356Z", + "shell.execute_reply": "2026-07-26T08:12:42.046235Z" } }, "outputs": [ @@ -349,16 +372,10 @@ "text": [ "'Most people in France speak'\n", " baseline : [' French', ' English', ' a', ' at', ' the']\n", - " swapped : [' Chinese', ' English', ' a', ' some', ' the']\n", - "'The capital of France is'\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " baseline : [' a', ' the', ' one', ' also', ' home']\n", - " swapped : [' a', ' the', ' one', ' also', ' home']\n" + " swapped : [' Chinese', ' English', ' a', ' some', ' at']\n", + "'The capital of France is'\n", + " baseline : [' a', ' the', ' also', ' one', ' home']\n", + " swapped : [' a', ' the', ' also', ' one', ' home']\n" ] } ], @@ -389,10 +406,10 @@ "id": "cdc8c44e", "metadata": { "execution": { - "iopub.execute_input": "2026-07-11T09:06:30.105260Z", - "iopub.status.busy": "2026-07-11T09:06:30.104879Z", - "iopub.status.idle": "2026-07-11T09:06:30.422053Z", - "shell.execute_reply": "2026-07-11T09:06:30.421595Z" + "iopub.execute_input": "2026-07-26T08:12:42.051286Z", + "iopub.status.busy": "2026-07-26T08:12:42.051130Z", + "iopub.status.idle": "2026-07-26T08:12:49.313426Z", + "shell.execute_reply": "2026-07-26T08:12:49.312040Z" } }, "outputs": [ @@ -400,8 +417,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "'Most people in France speak': rank of ' Chinese' 348 -> 1\n", - "'The capital of France is': rank of ' Beijing' 969 -> 9\n" + "'Most people in France speak': rank of ' Chinese' 347 -> 1\n", + "'The capital of France is': rank of ' Beijing' 990 -> 9\n" ] } ], @@ -436,26 +453,44 @@ }, { "cell_type": "markdown", - "id": "7020c7b5", + "id": "4adfabca", "metadata": {}, "source": [ - "## 4. Steering along a J-lens vector\n", + "## 4. Quantifying causal-swap success across the country subset\n", "\n", - "`steering_hooks` adds a token's unit-normalized lens direction, scaled by the activation's median\n", - "residual norm times `alpha` (the paper's directed-modulation protocol). `ablation_hooks`\n", - "(projecting a direction *out*) works the same way." + "The qualitative France → China example is one cell of the paper's\n", + "`flexible-generalization.json` country subset. We now run all four functions over\n", + "the twelve ordered source-target pairs: **48 trials per alpha**. Each condition\n", + "applies the same concept swap at every token position and simultaneously clamps it\n", + "across block outputs 10–24.\n", + "\n", + "The protocol was fixed before the measured run:\n", + "\n", + "- base model: `google/gemma-2-2b` at `c5ebcd40d208330abc697524c919956e692655cf`;\n", + "- published lens: `neuronpedia/jacobian-lens/gemma-2-2b/jlens/Salesforce-wikitext/gemma-2-2b_jacobian_lens.pt` at `a4114d7752d11eb546e6cf372213d7e75526d3a1`;\n", + "- country configuration:\n", + " [`anthropics/jacobian-lens:data/experiments/flexible-generalization.json`](https://github.com/anthropics/jacobian-lens/blob/581d398613e5602a5af361e1c34d3a92ea82ba8e/data/experiments/flexible-generalization.json)\n", + " at `581d398613e5602a5af361e1c34d3a92ea82ba8e`;\n", + "- layers 10–24 inclusive; `alpha=1` primary and `alpha=2` double-strength diagnostic;\n", + "- success means the exact target-answer token is the deterministic argmax of the\n", + " swapped next-token logits; failures are never filtered.\n", + "\n", + "The protocol fingerprint is `05dedbde47a22b11189d03b16c93b676c8a9de29a4b5f641bc1a24d5f9fd61f9`. Exact maximum-logit ties are\n", + "retained as deterministic-argmax failures when another token wins the argmax order;\n", + "rank and tie metadata are measured separately. Executable assertions reject\n", + "duplicate, missing, or alpha-mismatched trial matrices.\n" ] }, { "cell_type": "code", "execution_count": 8, - "id": "89692d80", + "id": "f12cc675", "metadata": { "execution": { - "iopub.execute_input": "2026-07-11T09:06:30.427385Z", - "iopub.status.busy": "2026-07-11T09:06:30.427165Z", - "iopub.status.idle": "2026-07-11T09:06:30.640333Z", - "shell.execute_reply": "2026-07-11T09:06:30.639945Z" + "iopub.execute_input": "2026-07-26T08:12:49.316772Z", + "iopub.status.busy": "2026-07-26T08:12:49.316640Z", + "iopub.status.idle": "2026-07-26T08:12:53.613547Z", + "shell.execute_reply": "2026-07-26T08:12:53.612572Z" } }, "outputs": [ @@ -463,189 +498,1265 @@ "name": "stdout", "output_type": "stream", "text": [ - "baseline : [' the', ' a', ' my', ' visit', ' see']\n", - "steered : [' paris', ' Paris', 'Paris', ' París', 'paris']\n" + "protocol_sha256=05dedbde47a22b11189d03b16c93b676c8a9de29a4b5f641bc1a24d5f9fd61f9\n" ] } ], "source": [ - "steer = lens.steering_hooks(model, \" Paris\", layers=range(10, 21), alpha=2.0)\n", - "prompt = \"This weekend I am planning a trip to\"\n", - "print(\"baseline :\", show_next_tokens(prompt))\n", - "print(\"steered :\", show_next_tokens(prompt, hooks=steer))" + "import hashlib\n", + "import json\n", + "from itertools import permutations\n", + "\n", + "import pandas as pd\n", + "\n", + "CONFIG_REPO = \"anthropics/jacobian-lens\"\n", + "CONFIG_PATH = \"data/experiments/flexible-generalization.json\"\n", + "CONFIG_REVISION = \"581d398613e5602a5af361e1c34d3a92ea82ba8e\"\n", + "SWAP_LAYERS = list(range(10, 25))\n", + "SWAP_ALPHAS = [1.0, 2.0]\n", + "COUNTRY_CONFIG = {'name': 'countries', 'args': ['France', 'Canada', 'China', 'Egypt'], 'funcs': [{'name': 'capital', 'template': 'The capital of {arg} is the city of', 'answers': {'France': 'Paris', 'Canada': 'Ottawa', 'China': 'Beijing', 'Egypt': 'Cairo'}}, {'name': 'language', 'template': 'Most people in {arg} speak', 'answers': {'France': 'French', 'Canada': 'English', 'China': 'Chinese', 'Egypt': 'Arabic'}}, {'name': 'continent', 'template': '{arg} is a country on the continent of', 'answers': {'France': 'Europe', 'Canada': 'North', 'China': 'Asia', 'Egypt': 'Africa'}}, {'name': 'currency', 'template': 'The single-word name for the currency now used in {arg} is the', 'answers': {'France': 'Euro', 'Canada': 'Dollar', 'China': 'Yuan', 'Egypt': 'Pound'}}]}\n", + "protocol_manifest = {\n", + " \"model_id\": MODEL_ID,\n", + " \"model_revision\": MODEL_REVISION,\n", + " \"lens_repo\": LENS_REPO,\n", + " \"lens_file\": LENS_FILE,\n", + " \"lens_revision\": LENS_REVISION,\n", + " \"config_repo\": CONFIG_REPO,\n", + " \"config_path\": CONFIG_PATH,\n", + " \"config_revision\": CONFIG_REVISION,\n", + " \"country_config\": COUNTRY_CONFIG,\n", + " \"layers\": SWAP_LAYERS,\n", + " \"alphas\": SWAP_ALPHAS,\n", + " \"success_definition\": \"target token id equals deterministic argmax token id\",\n", + " \"baseline_definition\": \"source answer token id equals deterministic argmax token id\",\n", + " \"rank_definition\": \"1 + count(logits strictly greater than target logit)\",\n", + "}\n", + "protocol_fingerprint = hashlib.sha256(\n", + " json.dumps(protocol_manifest, sort_keys=True, separators=(\",\", \":\")).encode()\n", + ").hexdigest()\n", + "\n", + "print(f\"protocol_sha256={protocol_fingerprint}\")\n" ] }, { "cell_type": "markdown", - "id": "00df913e", + "id": "swapmetricintro", "metadata": {}, "source": [ - "## 5. Fitting your own lens\n", + "### Metric and completeness checks\n", "\n", - "For models without a published artifact, `JacobianLens.fit` reproduces the reference estimator on\n", - "a raw `TransformerBridge` — one forward and `ceil(d_model / dim_batch)` backward passes per\n", - "prompt, deterministic. Quality saturates quickly: ~100 prompts of 128 tokens is usable (the\n", - "published lenses use up to 1000). Fits parallelize across prompt slices and combine exactly with\n", - "`merge`:\n", + "The helpers below keep deterministic argmax success separate from rank-1\n", + "maximum ties. They also reject duplicate, missing, or alpha-mismatched trial\n", + "matrices before any rate is reported.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "swapmetrichelpers", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-26T08:12:53.616575Z", + "iopub.status.busy": "2026-07-26T08:12:53.616456Z", + "iopub.status.idle": "2026-07-26T08:12:57.919222Z", + "shell.execute_reply": "2026-07-26T08:12:57.918064Z" + } + }, + "outputs": [], + "source": [ + "def _target_metrics(logits, target_token_id):\n", + " if logits.ndim != 1 or logits.numel() < 2:\n", + " raise ValueError(\"expected one-dimensional next-token logits\")\n", + " if not 0 <= target_token_id < logits.shape[0]:\n", + " raise ValueError(\"target token id is outside the vocabulary\")\n", + " if not torch.isfinite(logits).all():\n", + " raise ValueError(\"logits must be finite\")\n", "\n", - "```python\n", - "from datasets import load_dataset\n", + " target_logit = logits[target_token_id]\n", + " top_logit = logits.max()\n", + " top1_token_id = int(logits.argmax().item())\n", + " top_logit_tie_count = int((logits == top_logit).sum().item())\n", + " competitors = torch.cat(\n", + " (logits[:target_token_id], logits[target_token_id + 1 :])\n", + " )\n", + " return {\n", + " \"top1_token_id\": top1_token_id,\n", + " \"target_rank\": int((logits > target_logit).sum().item()) + 1,\n", + " \"target_is_top1\": top1_token_id == target_token_id,\n", + " \"target_tied_for_top\": (\n", + " bool(target_logit == top_logit) and top_logit_tie_count > 1\n", + " ),\n", + " \"target_logit_margin\": float(\n", + " (target_logit - competitors.max()).item()\n", + " ),\n", + " }\n", "\n", - "texts = load_dataset(\"Salesforce/wikitext\", \"wikitext-103-raw-v1\", split=\"train\", streaming=True)\n", - "prompts = [row[\"text\"] for row in texts.take(500) if len(row[\"text\"]) > 600][:100]\n", "\n", - "lens = JacobianLens.fit(\n", - " model, prompts, corpus=\"your-corpus-id\", dim_batch=16, max_seq_len=128\n", - ")\n", - "lens.save(\"gemma-2-2b_jlens.pt\") # official artifact format (+ provenance metadata)\n", - "# ...or shard, using the same corpus id for every chunk:\n", - "# JacobianLens.merge([JacobianLens.fit(model, chunk, corpus=\"your-corpus-id\") for chunk in chunks])\n", - "```\n", + "def _aggregate_trials(trials, expected_trials_per_alpha):\n", + " frame = pd.DataFrame(trials)\n", + " key_columns = [\"function\", \"source\", \"target\", \"alpha\"]\n", + " if frame.empty or frame.duplicated(key_columns).any():\n", + " raise ValueError(\"trial matrix is empty or contains duplicates\")\n", "\n", - "## References\n", + " matrices = {}\n", + " for alpha, subset in frame.groupby(\"alpha\", sort=True):\n", + " keys = set(\n", + " subset[[\"function\", \"source\", \"target\"]].itertuples(\n", + " index=False, name=None\n", + " )\n", + " )\n", + " if len(keys) != expected_trials_per_alpha:\n", + " raise ValueError(\n", + " f\"alpha={alpha:g} has {len(keys)} trials; \"\n", + " f\"expected {expected_trials_per_alpha}\"\n", + " )\n", + " matrices[float(alpha)] = keys\n", "\n", - "- Gurnee et al., [*Verbalizable Representations Form a Global Workspace in Language Models*](https://transformer-circuits.pub/2026/workspace/index.html), Transformer Circuits Thread, 2026\n", - "- Reference implementation: [`anthropics/jacobian-lens`](https://github.com/anthropics/jacobian-lens) (Apache-2.0)\n", - "- Published lenses: [`neuronpedia/jacobian-lens`](https://huggingface.co/neuronpedia/jacobian-lens) · interactive: [neuronpedia.org/jlens](https://www.neuronpedia.org/jlens)\n", - "- The logit lens: [nostalgebraist, 2020](https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens)" + " if set(matrices) != set(SWAP_ALPHAS):\n", + " raise ValueError(\"trial matrix does not contain every configured alpha\")\n", + " reference_keys = matrices[SWAP_ALPHAS[0]]\n", + " if any(keys != reference_keys for keys in matrices.values()):\n", + " raise ValueError(\"alphas do not contain the same trial matrix\")\n", + "\n", + " aggregates = []\n", + " for alpha, subset in frame.groupby(\"alpha\", sort=True):\n", + " successes = int(subset[\"success\"].sum())\n", + " baseline_top1s = int(subset[\"baseline_source_is_top1\"].sum())\n", + " baseline_correct = subset[subset[\"baseline_source_is_top1\"]]\n", + " conditional_successes = int(baseline_correct[\"success\"].sum())\n", + " conditional_trials = len(baseline_correct)\n", + " aggregates.append(\n", + " {\n", + " \"alpha\": float(alpha),\n", + " \"trials\": len(subset),\n", + " \"baseline_top1s\": baseline_top1s,\n", + " \"baseline_top1_rate\": baseline_top1s / len(subset),\n", + " \"successes\": successes,\n", + " \"failures\": len(subset) - successes,\n", + " \"success_rate\": successes / len(subset),\n", + " \"conditional_trials\": conditional_trials,\n", + " \"conditional_successes\": conditional_successes,\n", + " \"conditional_success_rate\": (\n", + " conditional_successes / conditional_trials\n", + " if conditional_trials\n", + " else None\n", + " ),\n", + " \"tied_max_trials\": int(\n", + " subset[\"target_tied_for_top\"].sum()\n", + " ),\n", + " }\n", + " )\n", + " return aggregates\n", + "\n", + "\n", + "# Pin deterministic-argmax semantics for exact maximum ties.\n", + "_tie_probe = _target_metrics(torch.tensor([3.0, 1.0, 3.0]), 2)\n", + "assert _tie_probe[\"target_rank\"] == 1\n", + "assert not _tie_probe[\"target_is_top1\"]\n", + "assert _tie_probe[\"target_tied_for_top\"]\n" ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "transformer-lens", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 + { + "cell_type": "markdown", + "id": "swapsweepintro", + "metadata": {}, + "source": [ + "### Run the complete country sweep\n", + "\n", + "This is the only expensive cell: four functions × twelve ordered pairs × two\n", + "intervention strengths. Baseline logits are cached per function-source prompt.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "swapsweeprun", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-26T08:12:57.921473Z", + "iopub.status.busy": "2026-07-26T08:12:57.921353Z", + "iopub.status.idle": "2026-07-26T08:13:14.610862Z", + "shell.execute_reply": "2026-07-26T08:13:14.609596Z" + } }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.13" + "outputs": [], + "source": [ + "# NBVAL_IGNORE_OUTPUT (96 real-model intervention conditions)\n", + "condition_rows = []\n", + "evaluated_trials = []\n", + "baseline_cache = {}\n", + "hook_cache = {}\n", + "\n", + "for function_spec in COUNTRY_CONFIG[\"funcs\"]:\n", + " function = function_spec[\"name\"]\n", + " for source_value, target_value in permutations(COUNTRY_CONFIG[\"args\"], 2):\n", + " prompt = function_spec[\"template\"].format(arg=source_value)\n", + " tokens = model.to_tokens(prompt)\n", + " baseline_key = (function, source_value)\n", + " if baseline_key not in baseline_cache:\n", + " with torch.inference_mode():\n", + " baseline_cache[baseline_key] = model(tokens)[0, -1].float()\n", + " baseline_logits = baseline_cache[baseline_key]\n", + "\n", + " source_answer = f\" {function_spec['answers'][source_value]}\"\n", + " source_answer_id = model.to_single_token(source_answer)\n", + " baseline_source_metrics = _target_metrics(\n", + " baseline_logits, source_answer_id\n", + " )\n", + " target_answer = f\" {function_spec['answers'][target_value]}\"\n", + " target_answer_id = model.to_single_token(target_answer)\n", + " baseline_target_metrics = _target_metrics(\n", + " baseline_logits, target_answer_id\n", + " )\n", + "\n", + " for alpha in SWAP_ALPHAS:\n", + " hook_key = (source_value, target_value, alpha)\n", + " if hook_key not in hook_cache:\n", + " hook_cache[hook_key] = lens.swap_hooks(\n", + " model,\n", + " f\" {source_value}\",\n", + " f\" {target_value}\",\n", + " layers=SWAP_LAYERS,\n", + " alpha=alpha,\n", + " )\n", + " with torch.inference_mode():\n", + " with model.hooks(fwd_hooks=hook_cache[hook_key]):\n", + " swapped_logits = model(tokens)[0, -1].float()\n", + " swapped_metrics = _target_metrics(\n", + " swapped_logits, target_answer_id\n", + " )\n", + " evaluated_trials.append(\n", + " {\n", + " \"function\": function,\n", + " \"source\": source_value,\n", + " \"target\": target_value,\n", + " \"alpha\": alpha,\n", + " \"baseline_source_is_top1\": baseline_source_metrics[\n", + " \"target_is_top1\"\n", + " ],\n", + " \"success\": swapped_metrics[\"target_is_top1\"],\n", + " \"target_tied_for_top\": swapped_metrics[\n", + " \"target_tied_for_top\"\n", + " ],\n", + " }\n", + " )\n", + " condition_rows.append(\n", + " {\n", + " \"function\": function,\n", + " \"source\": source_value,\n", + " \"target\": target_value,\n", + " \"alpha\": alpha,\n", + " \"prompt\": prompt,\n", + " \"source_answer\": source_answer,\n", + " \"target_answer\": target_answer,\n", + " \"baseline_top1\": model.tokenizer.decode(\n", + " [baseline_source_metrics[\"top1_token_id\"]]\n", + " ),\n", + " \"baseline_source_rank\": baseline_source_metrics[\n", + " \"target_rank\"\n", + " ],\n", + " \"baseline_source_is_top1\": baseline_source_metrics[\n", + " \"target_is_top1\"\n", + " ],\n", + " \"baseline_target_rank\": baseline_target_metrics[\n", + " \"target_rank\"\n", + " ],\n", + " \"swapped_top1\": model.tokenizer.decode(\n", + " [swapped_metrics[\"top1_token_id\"]]\n", + " ),\n", + " \"swapped_target_rank\": swapped_metrics[\"target_rank\"],\n", + " \"success\": swapped_metrics[\"target_is_top1\"],\n", + " \"target_tied_for_top\": swapped_metrics[\n", + " \"target_tied_for_top\"\n", + " ],\n", + " \"target_logit_margin\": swapped_metrics[\n", + " \"target_logit_margin\"\n", + " ],\n", + " }\n", + " )\n", + "\n", + "assert len(evaluated_trials) == 96\n" + ] }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": { - "1902af979017484c8d9c5a0fe643e833": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "2.0.0", - "model_name": "FloatProgressModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "2.0.0", - "_model_name": "FloatProgressModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "2.0.0", - "_view_name": "ProgressView", - "bar_style": "success", - "description": "", - "description_allow_html": false, - "layout": "IPY_MODEL_292c5280a9cd49a89d0c2a77ffd4e936", - "max": 288.0, - "min": 0.0, - "orientation": "horizontal", - "style": "IPY_MODEL_99333fe6d4aa485bac5b5ec104cc5537", - "tabbable": null, - "tooltip": null, - "value": 288.0 - } - }, - "292c5280a9cd49a89d0c2a77ffd4e936": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } + { + "cell_type": "markdown", + "id": "swapsummaryintro", + "metadata": {}, + "source": [ + "### Aggregate baseline and swap success\n", + "\n", + "The unconditional swap rate uses all 48 trials. The conditional rate keeps only\n", + "trials where the unperturbed model already puts the source-appropriate answer\n", + "top-1.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "swapsummarytable", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-26T08:13:14.613588Z", + "iopub.status.busy": "2026-07-26T08:13:14.613441Z", + "iopub.status.idle": "2026-07-26T08:13:20.741457Z", + "shell.execute_reply": "2026-07-26T08:13:20.740144Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
scopealphabaseline_top1baseline_top1_rateswap_successswap_success_rateconditional_swap_successconditional_swap_rate
0overall1.033/4868.75%16/4833.33%13/3339.39%
1overall2.033/4868.75%0/480.00%0/330.00%
2capital1.012/12100.00%5/1241.67%5/1241.67%
3capital2.012/12100.00%0/120.00%0/120.00%
4language1.09/1275.00%7/1258.33%4/944.44%
5language2.09/1275.00%0/120.00%0/90.00%
6continent1.012/12100.00%4/1233.33%4/1233.33%
7continent2.012/12100.00%0/120.00%0/120.00%
8currency1.00/120.00%0/120.00%0/0n/a
9currency2.00/120.00%0/120.00%0/0n/a
\n", + "
" + ], + "text/plain": [ + " scope alpha baseline_top1 baseline_top1_rate swap_success \\\n", + "0 overall 1.0 33/48 68.75% 16/48 \n", + "1 overall 2.0 33/48 68.75% 0/48 \n", + "2 capital 1.0 12/12 100.00% 5/12 \n", + "3 capital 2.0 12/12 100.00% 0/12 \n", + "4 language 1.0 9/12 75.00% 7/12 \n", + "5 language 2.0 9/12 75.00% 0/12 \n", + "6 continent 1.0 12/12 100.00% 4/12 \n", + "7 continent 2.0 12/12 100.00% 0/12 \n", + "8 currency 1.0 0/12 0.00% 0/12 \n", + "9 currency 2.0 0/12 0.00% 0/12 \n", + "\n", + " swap_success_rate conditional_swap_success conditional_swap_rate \n", + "0 33.33% 13/33 39.39% \n", + "1 0.00% 0/33 0.00% \n", + "2 41.67% 5/12 41.67% \n", + "3 0.00% 0/12 0.00% \n", + "4 58.33% 4/9 44.44% \n", + "5 0.00% 0/9 0.00% \n", + "6 33.33% 4/12 33.33% \n", + "7 0.00% 0/12 0.00% \n", + "8 0.00% 0/0 n/a \n", + "9 0.00% 0/0 n/a " + ] }, - "8b9855cb054f4b06803233534b78ddf9": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "2.0.0", - "model_name": "HBoxModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "2.0.0", - "_model_name": "HBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "2.0.0", - "_view_name": "HBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_c2743d998e5841b3b35e40721586659a", - "IPY_MODEL_1902af979017484c8d9c5a0fe643e833", - "IPY_MODEL_ef3400c12a834c5db3f95519c0b9e78f" - ], - "layout": "IPY_MODEL_c357c3f6dcb342fa90b16ca6949bcd3e", - "tabbable": null, - "tooltip": null - } + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "overall = _aggregate_trials(evaluated_trials, 48)\n", + "summary_rows = []\n", + "scopes = [\n", + " (\"overall\", evaluated_trials),\n", + " *[\n", + " (\n", + " function_spec[\"name\"],\n", + " [\n", + " trial\n", + " for trial in evaluated_trials\n", + " if trial[\"function\"] == function_spec[\"name\"]\n", + " ],\n", + " )\n", + " for function_spec in COUNTRY_CONFIG[\"funcs\"]\n", + " ],\n", + "]\n", + "for scope, subset in scopes:\n", + " expected = 48 if scope == \"overall\" else 12\n", + " for aggregate in _aggregate_trials(subset, expected):\n", + " summary_rows.append(\n", + " {\n", + " \"scope\": scope,\n", + " \"alpha\": aggregate[\"alpha\"],\n", + " \"baseline_top1\": (\n", + " f\"{aggregate['baseline_top1s']}/{aggregate['trials']}\"\n", + " ),\n", + " \"baseline_top1_rate\": (\n", + " f\"{aggregate['baseline_top1_rate']:.2%}\"\n", + " ),\n", + " \"swap_success\": (\n", + " f\"{aggregate['successes']}/{aggregate['trials']}\"\n", + " ),\n", + " \"swap_success_rate\": f\"{aggregate['success_rate']:.2%}\",\n", + " \"conditional_swap_success\": (\n", + " f\"{aggregate['conditional_successes']}/\"\n", + " f\"{aggregate['conditional_trials']}\"\n", + " ),\n", + " \"conditional_swap_rate\": (\n", + " f\"{aggregate['conditional_success_rate']:.2%}\"\n", + " if aggregate[\"conditional_success_rate\"] is not None\n", + " else \"n/a\"\n", + " ),\n", + " }\n", + " )\n", + "\n", + "summary_table = pd.DataFrame(summary_rows)\n", + "summary_table\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "swapreportbuild", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-26T08:13:20.743194Z", + "iopub.status.busy": "2026-07-26T08:13:20.743069Z", + "iopub.status.idle": "2026-07-26T08:13:24.938453Z", + "shell.execute_reply": "2026-07-26T08:13:24.937282Z" + } + }, + "outputs": [], + "source": [ + "conditions = pd.DataFrame(condition_rows)\n", + "assert len(conditions) == 96\n", + "shared_columns = [\n", + " \"function\",\n", + " \"source\",\n", + " \"target\",\n", + " \"prompt\",\n", + " \"source_answer\",\n", + " \"target_answer\",\n", + " \"baseline_top1\",\n", + " \"baseline_source_rank\",\n", + " \"baseline_source_is_top1\",\n", + " \"baseline_target_rank\",\n", + "]\n", + "\n", + "\n", + "def alpha_report(alpha):\n", + " suffix = str(int(alpha))\n", + " return conditions.loc[\n", + " conditions[\"alpha\"] == alpha,\n", + " shared_columns + [\"swapped_top1\", \"swapped_target_rank\", \"success\"],\n", + " ].rename(\n", + " columns={\n", + " \"swapped_top1\": f\"alpha{suffix}_top1\",\n", + " \"swapped_target_rank\": f\"alpha{suffix}_target_rank\",\n", + " \"success\": f\"alpha{suffix}_success\",\n", + " }\n", + " )\n", + "\n", + "\n", + "report_table = alpha_report(1.0).merge(\n", + " alpha_report(2.0),\n", + " on=shared_columns,\n", + " validate=\"one_to_one\",\n", + ")\n", + "assert len(report_table) == 48\n", + "for column in [\n", + " \"source_answer\",\n", + " \"target_answer\",\n", + " \"baseline_top1\",\n", + " \"alpha1_top1\",\n", + " \"alpha2_top1\",\n", + "]:\n", + " report_table[column] = report_table[column].map(repr)\n" + ] + }, + { + "cell_type": "markdown", + "id": "swapvisualintro", + "metadata": {}, + "source": [ + "### Visual summary\n", + "\n", + "The bars separate model capability from intervention success. The heatmaps show\n", + "the target-answer rank for every trial; lower is better.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "swapratechart", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-26T08:13:24.941551Z", + "iopub.status.busy": "2026-07-26T08:13:24.941420Z", + "iopub.status.idle": "2026-07-26T08:13:29.718995Z", + "shell.execute_reply": "2026-07-26T08:13:29.718188Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAHbCAYAAADWCPmyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAfHdJREFUeJzt3Qd0FNXbx/EnoZdQld5R6U2UqgiCoqKCIILlL1ZEsQAiihXsoIIFBCtW7GIXRYqoICCIAiIgICAd6Z0k+57f5Z11s9mEJGTIJvl+zlnIzs7O3p25M3ufuS0mEAgEDAAAAAAAZLrYzN8kAAAAAAAg6AYAAAAAwEfUdAMAAAAA4BOCbgAAAAAAfELQDQAAAACATwi6AQAAAADwCUE3AAAAAAA+IegGAAAAAMAnBN0AAAAAAPiEoBsAcqmrrrrKqlWrltXJAI6ZadOmWUxMjH344YdRudeHDBni0hdK56jOVUTeV1u2bDnirgnfh14+0P8erocA/ETQDeCIli9fbjfccIPVqFHDChYsaMWKFbPWrVvbM888Y/v27YuKPfj888/ba6+9ltXJAADkAHv37nWBfWhgDgAZlTfD7wSQK3z55ZfWvXt3K1CggF155ZVWv359O3jwoP344492xx132KJFi+zFF1+MiqD7uOOOo0YIQI6yZMkSi42ljsTvffjSSy9ZYmJikqB76NCh7u+2bdse1ecDAEE3gBStXLnSevbsaVWrVrUpU6ZY+fLlg6/17dvX/vrrLxeUZzd79uyxIkWKZHUygFyB8+3o6IYn/N+H+fLlYzcD8A23TgGkaPjw4bZ792575ZVXkgTcnhNOOMFuu+224PP4+Hh76KGHrGbNmq6Qo350d999tx04cCDJ+9SXTs32jtTvTs3Fte5PP/1kAwYMsOOPP94FyxdddJFt3rw5yftU4/7999+79fXwaia8bei1m266ycqUKWOVKlWyqVOnuuUTJkxIlo7x48e712bOnJlq7ti+fbv179/ffb6+r7ar1gBeH0O1CLj//vutadOmVrx4cZf2008/3X12qEj9C+Xvv/92y0ObzW/YsMGuvvpq91n6TB2Xzp07u3U9n376qXXq1MkqVKjg1tHx0HFJSEiwjHruueesXr16VrhwYStZsqSdcsopbj/J77//7tL52WefBdefO3euW3byyScn2c65555rzZs3T3dadTzVykLbbdWqlRUqVMiqV69uY8eOTVP6J02aZKeddpqVKFHCihYtarVq1XJ50+Plk9D9mNqxmTVrlp133nluX+i4NmzY0HW3CPXnn3/aJZdc4vKt0qvPvOeee5Kss3btWrvmmmusbNmy7vtrH7/66qvp2v+ya9cu69evXzAvKp+fddZZNm/evBT3ifo1e+dGuBdeeMG9tnDhwiTf5+KLL7ZSpUq5biZKQ+gxT+18S2saU+q/rOMfXtt4pH2SGuUvHf9y5cq543fhhRfamjVrkqzzww8/uFY+VapUcemtXLmyO9/Du9Sk5ZyUr7/+2p3/+ry4uDiX73XdOpKMXheP9nMz8xrnXcuefPJJGzlypLuRq3PijDPOSJLHvOuJvq/XnUnHSOfIv//+GzGNSovOM3V7Kl26tPtN2r9/f6r7MJLQPt1Kr/arqLbb+13R79a4cePc37/++muybTz66KOWJ08ed14DQChqugGk6PPPP3cFHwU5aXHdddfZ66+/7grmt99+uwtMHnvsMVu8eHHE4DatbrnlFleofuCBB1xh6Omnn7abb77Z3nvvPfe6nmsdBVNeUKMgJpQCABWiVEBUzZsK8CpEv/32266wGkrLFPy1bNkyxTTpZoQKl/puKhAquFThT0HIP//845q679y5015++WW79NJL7frrr3dBh25gdOzY0WbPnm2NGzdO977o1q2bKzDr+6qAuGnTJhdQrl69OlhgVKFc+0IFcv2vVgr63krPE088ke7PVLPLW2+91R1Xr0CrgrGO72WXXeaCYQWz06dPd8GLF7CoOedvv/3mPlcFYjXdnDFjhvXu3Tu47fSkddu2bS7QVQFb+/T999+3G2+80fLnz++OQUq0v84//3wXGD/44IMueFArDQUtGaH9re0puNL+UFCgfPDFF18Eb0Jp/yh/qPZM31fHRmMj6Jx65JFH3DobN260Fi1auAK88rPypwKka6+91n1/Bahp2f/Sp08fF0RrO3Xr1nUBirqAKF3hNz48Cr60z7UfFfyE0rmlgFbH1tuHGsehYsWKdtddd7ngSu/r0qWLffTRR8nOofDzLaNpTEla9klqdAy03++88053Duka0qFDB5s/f74LBuWDDz5wTYyVxxTM6ZxVoK/zW6+l55x88803rVevXu7cHzZsmNvumDFj3I0gBW8ZGdDwSNfFo/1cP65xb7zx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dwkJCVaoUCG79dZbrUOHDlmdnFxp79697ry9++67LSd57rnnrHv37la0aFGLZscff7wVLlzYspuDBw8mWxYIBCw+Pt63z7z66qvt7bfftq1bt/r2GQByJoJuADmKglsFuXqopvSuu+6yNWvW2ObNm4Pr3HnnnXbSSSe5gmaNGjXsvvvuSxJAqoaqXbt2FhcXZ8WKFbOmTZvaL7/8Enz9xx9/tNNPP90Fa5UrV3YB2549e1JMU2jzctXk6vnHH3/sPkNpaNSokc2cOTPJe9L7GUdK80cffWT16tVz+0fNSZ966qkU0+hRbeNrr72WJN3vvfeenXHGGVawYEFX+JRXX301uO3y5cvbzTffHNyGaouuu+46V7BXus4880yX1pSolluaNGniPk9NYSUxMdEefPBBq1SpkvscHduJEycG3+el791337VWrVq59NWvX9++//57S41q+FQTef3117s8g9Tt3r3bBR7KZ2XLlrUnnnjC1q5d6/KxXssINXXWedqiRYs0v+fDDz+0Bg0auPOjdOnS7oaJzo+FCxdabGxs8HxXcKTnPXv2DL734YcfttNOOy140+Xaa691+U7bqlWrlj3zzDPJaj27dOliQ4cODeZjtY6IFPR5tF2l8YILLkj2mmrzL730Upf3KlasaKNHj07y+ogRI9x30+s692+66aYk+3bVqlVuu2o9o3V07qlG3aN9cO6557pgX8dITdzVNDo93WFefvllu+iii9xxPfHEE+2zzz5L8p70foYcOHDAXXv1nXQOn3DCCUlqjHWuNmvWLHgdUZ4IDaB1LdC1RflFtc4dO3a0adOmufR+/fXX7pqn9+raqevFY489FjyuusbqeIRatGiRuyGr46n8rOvt8uXLbciQIa7F1Keffuq2rYc+R7SvK1SoYBMmTEj1uwJAOIJuADmWCqpvvfWWK9ypYO5RAUvB5B9//OEK2C+99JKNHDky+Prll1/ugjs1u1SzYxX+8uXL515ToUy1ct26dbPff//dBaEq5IUGmmlxzz332MCBA23+/PnuBoAK4V4BMyOfkVqa9fySSy5xgceCBQtcoVI3GryAOj203dtuu801xVahVwFr3759rXfv3m7bKpxrf3tU07dp0yZXKFY6Tj75ZGvfvn2KNUWzZ892/3/33XeutYJuToiOk24UPPnkk26f6LMvvPBCW7ZsWZL333HH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H/369bO77rrLWrRokeb3fPjhh9agQQMrVKiQlS5d2jp06GB79uxxQYUK4Js3bw7W8ul5aE3Yww8/bKeddpr7OyEhwQUfqi3WtmrVqmXPPPNMskBSNYNDhw51QWuxYsWsT58+wUJ2JNqu0hjeFPTNN9+0U045xe0/Fc5VgN60aZNllNItTZo0cc1a27ZtmyTNjzzyiFWoUMF9r0jNy0eMGOH2o2q/K1eubDfddFOqx1G196r1U/q1H5o2bWq//PJL8HV9Xz1fvny5ZVeq4VaeXrFihduf4YG3nmu5Xtd6Wj+az7H69evbRx995I5NzZo17cwzz3T54vPPP7f4+PgU3/f888/biSeeaAULFnTpuPjii93yL774wtX4Ko/L/PnzXd7TOey57rrr7IorrnB///vvv3bppZe6VgP6Dspv77zzTpLP0v68+eab3UO178cdd5xrNREIBFJMn87xKVOmJDvH0punj9SSRPldVHut76lzKzTNun4pvR07dozYvPzOO++0k046yX33GjVquO916NChVGvWmzVr5tKv/dy6dWtbtWpV8HV9X9Xw79u3L0PfCQByKpqXA8gxVOBcsGCBKxiqWWfXrl1doKug16vRVfPiG264IdXtfP311xluHqnmmyrEDx8+3C666CLXJF3NLVVAr1evngvCv//+exckaLn33KO/veBU/Y8rVapkH3zwgVtvxowZ1rt3bytfvrxdcsklwfdMnjzZBR/63iqIKyjS+gpeIvn9999dMKYAO5QK22oeqiBYwfaAAQPcPlUz9IyYPXu2K6B/99137rurJiw0zQqMJ02alOL7dUNCtWYK3hVEKkAZNGiQC7giufzyy12ArybzefLkcQFXvnz5gq9XqVLFBWja7wrwsiMFhzrOXmCt//VcAVxowK0ASstTa4IereeY8qbyRt68kYsounFy6623uptErVq1cjevdExF29Q5py4Nyt86nxR0Kr0eLVOwKfv373c3Z/Rcn/nll1+67gfKH8q7ntdff93dAFOe1ufrPFR+uv766yOm8ccff3SBbJ06dY4qT6dGx1w3LLp162ZLlixx6dfNudA033jjjfbTTz+luA3dPFHrAt380nHV99EypSmcboLoZpnW0Y0J3djT/lAg79E+13qzZs0KXscAAATdAHKILVu22Mcff+wK/CpEi4LeN954w1555ZXgehdeeKE1b9481W0dTaCioFuFTgUjVatWdctUs+VRs18FAAq69b8C5Jdfftn+/PNPV9BXYO0VeBUwqgbbo4L6zJkz7f33308SdCuYffXVV10hX8Htgw8+aHfccYcLoCM1bVXNlILSMmXKJOuT6VHQpuDg1FNPdTVxGWmGrpp30Q0A1ZyHUk2ZvndoIB5OtXQe1YKrFYBq8VMKUNQnWN+7du3a7rlqQsMpuAitmcuOFGyFB94KQBUshgbcWi+7nWP6DOVbBbUp0XFW/jn//PNdgKjzTDdbRDXRGj9A318BoP7v37+/O4+8vvB//fWXnXHGGcF0DBw4MLjtW265xb755ht3joUG3dqXI0eOdAGmbkopQNXzlIJu5THd4Ak//9Kbp1Ojc7hUqVLub53L4X26lf918y819957b5L0aF+8++67EYPunTt3uv2n/e7dtAq/qaBrkI5Bdj/HACCzUdMNIEdQQVq1yS1btgwuU6FZtcQKAjwqpOvhl0aNGln79u1doK0mnWeffbYLsL3Bi1TY1+BsXo2bRg9funSpCw5UY6faZjXZ9IwePdoF1Ao01GRTtUvhg5LpM0P7QmsfKMBQzacX+IfSdtSvM7SGSubOnetGK1YzbQ3M5I30rc+uW7dupu4n7Z/UAm5RDfljjz3mbkiowK+bGaqZVD/7SH2/VTOvpsMKQFXzqj7r4TXaqgnU+7O78MDbyzN+BdzH4hzTMe7UqZPLa8qHKTnrrLNcvtZ31XgLeij49/KEzjHtg9tvv93VgCsPKYhW7bPOMd148W7IqBm6zkG9rmbyOr8OHDiQLH+pe0no+aJ98NRTT7n3K/iNdI6p9cnR5umj4d0YSc17773nbq6py4WuGUqPaswjUYCvlg66rukY6BzTzT+1vMmJ5xgAZCb6dAPIEbzBgUIDOdW0qr+impd6VEunWtvUHl5T1YxQAVxNptV8VsGDBixTzdjKlSvd6wqS/vjjD1u2bJn7X/23vSbCCsJVO+cVvlXjpJonNWvV6M5qLq2a8dT6a6eF9ocKxaHbUZ9zFaZV4NY+0kjiEyZMcK8d7edFcqRRytVMXjVqDRs2dE1odUNANyBSS48CtUWLFrnATf1ptf+97+BR0OXVwGd3Cqx1gyGUnvsRcPt9jqlJuIJnBes6ZqHdAsJpHc0moCbOCvjuv/9+d+PJm8pO55MCbN080nbU8iH0HPNquUV90jVOgpqXT5061Z1jOg8y4xzTjaujzdN+nmNqNaMuGRpwT33h1ST/nnvuSTUt48aNc+9Ts34F7Dr2P//8c449xwAgs1DTDSBHUNNrNeVUMKuaLNGAPqqlVe2cV0vld/Ny0Wep5lEPBQSqlVMgoZpY1fCq1lvNSlVjrQBEAcGwYcNcIT20H6T6Yqpwq36fnkiDgCm4UM2a159ThWBtN6Xgy6spV9Dv/a2aNw0q9fjjjwffFzoIWUZ4wZk3qFV6KCBRTbtqE70muqqNPBIFAXqoSbH61itIUC2oqEZR+89ripzdqSVD+PRneu5XTbdf55hqfBXoKqjX9iLVEIdTf2/VtOqhKcfUtFo3WtStw+vXrebfXoCt80p5W+eYasBDz7HOnTsHB1ZTnlPLk/CWHeqjHErnmGrLI9Vyi/LYhg0b3Od5rVwymqf9OsfUlUXXJgXanrQ0C9d302Pw4MGuxl9TKnoDTer80nmWU84xAMgs1HQDyBFU6FaBW4OHKQBVIKqphxSIqjAeWkt2wgknpPoIHYxIBWfVfqlpragvp56rNicSFc7VXFUBq4IR9YHVSMZe30cFJurXrdpAL8BWzZeatGpwsdBaOBXqtR31MVUgoJGFVQMdTjVTqg1XEK1BzxSEaOTilKYqUi3UySef7GoDPRoUSgV41cyrubKCH/WtPRrqZ6p9qeOgQbfSM5K2joOa2nvpUQ3u2LFjU1xfx1zfWQGnAgcFU9pXoX1OFSgpsAttHp1dhQ+apu+r/1Ma1TxazzEF3OqCoZYW6heu5zrn9EgpkFStrJpE6zzUsVafcgWz3ij4CnJ1ToWeYzrnVDuu8yj8HFPLFAWgixcvdgPAKa+G07msm2YasEw17MqXt912W4r7SkGnartDBzFLb55OCwXNuqZon+g6k56R0PXd9b3UokbBsvZpeMuQUGqto0BbNd3a72p9oxswoeeYWjAoH2bXgQoBwC8E3QByDDXVVC2ZatFUA6bpp/RQE8rQgZ7SQ4ViFaC9AZNUeNdzBaWRqHn29OnTXZNN1bhqoCLVbJ177rnBdVToV0DhBQQKjrVdr4bcowBAQU6PHj1czaFqokNrvT3qQ64CtLahdVXTmFqfWFHfZwUloYG4RjFW/1zV8qlWUHMnHw3VRqog/8ILL7iaUdUoppWaC2t6JbUA0LRSSqv6wqZENY7aP1deeaXb7+prqn0eOhCdgiXlBT/nAj8WIo1SrhYR+t/vwDuzzzEFwrpRpZtZCkrVXNx7pJR+Bf+6maXpxRTw6RzVsdUggimdY+qPrHytAf284Fx0fuoGlGrata5e1wjd4ZSvdKNBfdj79u3rAu7UBntTflRXkNBzLL15Oi10HJTHNSWaBm7Tjae00nVCLUL0HrV40Y0H3dhLic4btYjRaOk6x/T9tS9CR6rXcUhpcDkAyM1iAqlNNAkAiGoa2Eh9WUPn3k0LBRAKPtQvMyfU/B6JRsXW91XLAW/+8OxIg33p5kpKo5SHB+S6AZTZ04blNtqfCkpD55BPC9XW60aAbixEGtAwp9F4CroRotYEGsEcAPAfaroBIBdS8141y1UwmhtoECtNy5SdA25RMKMgOqVRyr1RzfW61iP4yTqqNVftv5pw5waaLlHXFPIcACRHTTcA5MKabmRf3nzTqdVgq0ZcwU9G5ldH5tR0AwDgIegGAAAAAMAnNC8HAAAAAMAnBN0AAAAAAPiEoBsAAAAAAJ8QdAMAAAAA4BOCbgAAAAAAfELQDQAAAACATwi6AQAAAADwCUE3AAAAAAA+IegGAAAAAMAnBN0AAAAAAPiEoBsAAAAAAJ8QdAMAAAAA4BOCbgAAAAAAfELQDQAAAACATwi6AQAAAADwCUE3AAAAAAA+IegGAAAAAMAnBN0AAAAAAPiEoBsAAAAAAJ8QdAMAAAAA4BOCbgAAAAAAfELQDQAAAACATwi6AQAAAADwCUE3AAAAAAA+IegGAAAAAMAnBN0AAAAAAPiEoBsAAAAAAJ/k9WvD2UliYqKtW7fO4uLiLCYmJquTAwAAAACIcoFAwHbt2mUVKlSw2NiU67MJus1cwF25cuVjeXwAAAAAADnAmjVrrFKlSim+TtBt5mq4vZ1VrFixY3d0AAAAAADZ0s6dO13lrRdPpoSg2yzYpFwBN0E3AAAAACCtjtRFmYHUAAAAAADwCUE3AAAAAAA+IegGAAAAAMAnBN0AAAAAAPiEoBsAAAAAAJ8QdAMAAAAA4BOCbgAAAAAAfELQDQAAAACATwi6AQAAAADwSV6/NgwAmW3IZ4ts0h8bbe32ffblradZvQrF3fKVW/bY7e/Pt217D1lcwbz2ZPdGdlLZuCO+Fu69OattzLTllhgwa1WztD3Upb7ly8O9SQA4VrjOA8iJKE0CyDbOrV/OPryxpVUsUSjJ8rs/XmCXNqtiUwe2tT5n1LSBH/yWptdCrdm61576dqm936elfX9HW9uy+4C9M3u1798JAPAfrvMAciKCbgDZRvMapa188aQBt4LjBWt32EVNKgYLbOu277e/t+xJ9bVwXy1Ybx3qlrUycQUtJibGLm9e1T6bv+4YfTMAgHCdB5ATEXQDyNbWb99vZeIKWN7/bwaugLliiYK2bvu+VF8Lp2WhNeiVShaKuB4A4NjiOg8guyPoBgAAAADAJwTdALK18iUK2qZdByw+IdE9DwQCtnb7fqtQolCqr4XTMg3Q5vln276I6wEAji2u8wCyO4JuANnacUULWL0KxWzCr2vd868XbrDyxQtateOKpPpauHPrl7fv/thom3btd8H527NW2QWNKhzz7wMASIrrPIDsLiag0mUut3PnTitevLjt2LHDihUrltXJAZCCwR8vsKl/brLNuw9YycL5rEiBvPb9He1s+ebdblTy7XsPWdECee2J7g2tdrnD53Jqr9354e9u8LSz6pZ1zzVauaYMkxY1StkjFzVgyjAAOIa4zgPIiXFklgbd06dPtyeeeMLmzp1r69evtwkTJliXLl2CrytpDzzwgL300ku2fft2a926tY0ZM8ZOPPHE4Dpbt261W265xT7//HOLjY21bt262TPPPGNFixZNczoIugEAAAAA6ZHWODJLm5fv2bPHGjVqZKNHj474+vDhw+3ZZ5+1sWPH2qxZs6xIkSLWsWNH279/f3Cdyy+/3BYtWmSTJk2yL774wgXyvXv3PobfAgAAAACAKG9erql8Qmu6lawKFSrY7bffbgMHDnTLdAehbNmy9tprr1nPnj1t8eLFVrduXZszZ46dcsopbp2JEyfaeeedZ//88497f1pQ0w0AAAAASI+0xpF5LUqtXLnSNmzYYB06dAgu0xdq3ry5zZw50wXd+r9EiRLBgFu0vpqZq2b8oosuirjtAwcOuEfozpLExET3AAAAAAAgNWmNHaM26FbALarZDqXn3mv6v0yZMklez5s3r5UqVSq4TiSPPfaYDR06NNnybdu2WXx8fCZ9AwBHq+eLM6NyJ77bu2VWJwEAcgSu8wCys127dmXvoNtPgwcPtgEDBiSp6a5cubKVLFmS0cuBKLJiZ4xFI93YAwAcPa7zALIzVfimaT2LUuXKlXP/b9y40cqXLx9c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frzHjUYeafTyfk37uYdHwfczZz7D6OUAgGM3T/f06dNt3759R7MJAACAqPH+kvdt1K+jsjoZAIAc5KhqugEAqfj1LbNP+5r1eNuszvlm0580++0ds3+Xm/V46/CySP76zmzSkP+e79lsVrSMWZ8ffJmz+KXfX7LPln9mq3auciM0h85jHOqntT/ZyLkjg8+37t9qxxU6zt6/4P1MSRfgp3HnjIu4/JHTHknyXIOmpeSbi7/J9HQBAHK+owq6q1atavny5cu81ABATrFtldnc180qnfrfshrtzOp3M/v05tTfe0KHww/P25eYVT/yXMIZnbO4RfkWdk71c+z+n+5P9b2tK7Z2D0/fyX2tWblmmZIuAACAnOqompcvXLjQKleunHmpAYCcIDHR7LNbzM4bbpYnZBDKSk3NSlVP37Z2rjdb+b1Zw56ZOmdx/tj8weUNjm9glePSdy3ftHeTzVo/y86vkUJtPQAAADJe071t2zZ75ZVXbPHixcHpwq655horVapURjYHADnLzFFmVVqYVWhy9Nua/7bZiWeZFT3etzmLM+LTvz610yuebqULlT7qbQEAAORkeTMyeNqFF15oxYoVs1NOOcUte+655+yhhx6yzz//3Nq0aeNHOgEge9j4h9niz8yu/vrotxUIHO4Xfu5w3+YszliyAjbhrwl2V7O7jnpbwNFo8HqDqNuBC3otyOokAACye9Ddt29fu+SSS2zMmDGWJ08etywhIcFuuukm99qCBfzYAMjFVs8w277a7NmTDz/fvdHs8z/Ndm8wO/W69G3r7x/N4g+YnRB5YLPMmLN4y94tSaZVSotfNv5iBxIOWOsK//XvBgAAQCYF3X/99Zd9+OGHwYBb9PeAAQPsjTfeSO/mACBnUWAdGlyP62TW4saURypPza9vmjW+zCz2v+utH3MWp9fHyz62zjU7W55MSBcAAEBOl+6B1E4++eRgX+5QWtaoUaPMShfgu97f9raun3W1iz+72Hp93csW/3s4X/+49kfr8UUP99rlX15uS7YuOeK27vnxHtfMcefBnRw5pOz7J8yeqmP2z+zDA63p7z1bDr825RGzOa/8t+7+HWaLPzdrcoXve/SF316w9h+0t982/2ZDZgxxf2s6MNF8xZq32LPr4C6bvHqyXXTiRb6nCwAAIFfWdN9666122223uRrvFi1auGU///yzjR49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11PeLV68eGDChAmBE044weXns88+O7B69ergOg888ECgUaNGwb+V/0MfU6dOda8NGjQocOKJJ7r8X7169cC9994bOHjwYMTtAKF69eoV6Ny5c8SdktY8PHHiRHfdLlKkSKBjx46BdevWBdc5dOhQ4JZbbnHrlSpVyuXVK6+8MslnRroeK78q36Y1LfLiiy+613QedOnSxb1Hnxvqk08+CTRp0sSdbzpXhgwZ4tKI3CMt1+9Vq1YFLrzwQpen4+LiAt27dw9s2LAh2TX1jTfecPm3WLFigR49egR27twZPK/Cr9cqw+iarb+9z0nLOZTWstFHH30UaNu2rcv/KlvNmDHDve59Zugj9NxC7nKk/O/lp19//TXJ8tAyh5envvrqq8DJJ58cyJcvn1umsnbfvn1debt06dIuP8qCBQtcrKD8XaZMmcAVV1wR2Lx5c3D7ep9+J+64445AyZIlA2XLlk2WR5WG3r17u/frPKhXr17g888/d99D5+gHH3yQZH2VrfR74Z2T0YigG+ny77//BmJiYgKPPvpoxNevv/56dwItXLjQnaB//fVX8DVv2bJly9zzt956K1C+fHn3w7FixQr3vwppr732mnvduxBUq1YtuI5+mP7555/AE0884S4Qy5cvDzz77LOBPHnyBGbNmhX8LIJuHKs871EQMWXKFJdvJ0+e7ApKChQ8Kmzph+qUU05xhaNffvnF3Zhq1apVxGBZAcYll1zifrh0Q0uPAwcOuNceeuihwE8//eQ+67PPPnM/WMOGDYu4HSCtQXda83CHDh0Cc+bMCcydOzdQp06dwGWXXRZc5+GHH3bX8Y8//jiwePFidxNWAUp6g+4jpeXHH38MxMbGut+CJUuWuKBEnxsadE+fPt19tn5T9Fvx7bffut8TBd7IHdJy/U5ISAg0btw4cNppp7nr8s8//xxo2rSpK0d4lDeLFi0a6Nq1qwsolLfKlSsXuPvuu93r27dvD7Rs2dKVgbzrdXx8fMSg+0jnUFrLRgrKv/jiC5f/L774Ynde6YaSfieefvppl/e9tITfsELukJb8n56gWzd3dB1V2V7b1jmi80LB859//ukeeu/xxx/vKt/0GzBv3rzAWWedFWjXrl1w+3qf8qeuxUuXLg28/vrrLp3atndOtmjRwgXaWqbrtwJuBf2i8+y8885L8j1000w3eKMZQTfSRT9GOvF0RymSESNGuNc3btzoClEPPvhg8DWdgM2bNw8+r1mzZrIaagUT+uEKvRDox+NIOnXqFLj99tuDzwm6kVl0M0f5UEFEeugurO78elTY0nZ0Dnn0g6Rl3g2j8GA5tQAplAIPFRI9BN1ISVrzVGp5OPRmqoJd3fTx6G/lR48CjypVqqQ76D5SWlTLqOt+qMsvvzxJ0N2+fftkhc0333zTBTTIHdJy/VahXjfuQ1sdLVq0yL1v9uzZ7rnyZngtmgKN0DJNeLlDIgXdRzqH0lo2evnll5OlV78p3ueEt/pA7pOW/J+eoFsth0Ipz6slUXheVSu+UGvWrHHv1w0i7326yRXq1FNPDdx5553u72+++cbdVPXWj/S9dM56LUQUc+TNmzcwbdq0QDRjIDVkiG7YHMnll19u48ePD66v/t9aJnv27LHly5fbtddeG+zHqsfDDz/sloc65ZRTkjxXP5KHHnrI9UtR33G9T31TVq9ezdFEluR1+e6776x9+/ZWsWJFi4uLs//9739uYMG9e/cG11H/p1NPPTX4vHbt2q6f1eLFi9OVpvfee8/10VJfQeX/e++9l/yPo5aWPFy4cGGrWbNm8Hn58uVt06ZN7u8dO3bYxo0brVmzZsHX8+TJY02bNs30tCxZsiTJ50j4899++80efPDBJL8x119/vRvYKvQ7IXdfv3X91cBSenjq1q2b7NqsWVCUFyPl/fRI7RxKT9moYcOGSbYhGUkPcq60ll/SKrw8LuHXd113p06dmiT/qqwjoXk4NP+Gnwfz58+3SpUqubGjItG1vl69evb666+752+99ZYb0LlNmzYWzQi6kS4nnHCCm/4ipSBBy0uWLGnHH3+8XXrppa5gNG/ePJsxY4Yb8bxHjx5uvd27d7v/X3rpJXdyeY+FCxe6gdhCFSlSJMlzDQTxzDPP2J133ulObL2vY8eObgAHILOdeOKJLs+nNtiIBio5//zz3Y/IRx99ZHPnzrXRo0e71zI7X86cOdPdvDrvvPPsiy++sF9//dXuuece8j+OSlrzcPhAljo30luwi42NTfYeDeyT3rQciX5nhg4dmuQ3ZsGCBbZs2TIrWLBgutKMnHv9TqtIeT8xMTFTtuOdD+kpG4VuR9uQjKQHuTv/63osodfk0OtxauXxSMuUhzUyemj+1WPZsmVJguLUzqdChQod8btdd911bpR+GTdunF199dXB8yBaEXQjXUqXLm1nnXWWPf/887Zv375kUxK8/fbbLrBWxtddqjPOOMMt00PvK1OmjFu3bNmybroxjYSuQD70odHQU/PTTz9Z586d7YorrrBGjRq5KRCWLl3KkYQv1JpCN3VU6FctRDjNv6qgQD8WTz31lLVo0cLdnV23bl2ydePj4+2XX34JPtdNKb2/Tp06ET9bo4CqZUco3cDSHV0F2rrrrB/VVatWZcp3Re6V1jycGk2LpGv7nDlzgsuUf3XjNZRuyqq22bNz505buXJlutJSq1atJJ8j4c9PPvlkd46F/8bo4RU0kbOl5fqt668qBfTw/PHHH+411XinVaTrdXodTdkos9OC3JH/dT2W0GuyguSM0nVXMxqpZUh4Hi4SIWiPRDdc//nnn1TL9ooBVPZ59tln3fnaq1cvi3b86iDdRo0aZQcOHHAn8vTp090P1cSJE11QraaAjzzySHBd1ci9++679sEHHwSblntUA/HYY4+5E0YnlmogdLdqxIgRqX6+goxJkya54EM16zfccINr0gj4RT9YKsCoSZNq3nTHVnlPeVfT5+nHRHeGn3vuOVdYevPNN23s2LHJtqM7u5orc9asWS6w0JzJCirCm8V69KP1+++/u8Bhy5Yt7jOU/9WVQueVmmopDRMmTODgI83UDDy8FuK4445LUx4+EuVvXdc//fRTl29vu+0227ZtW5IaiDPPPNNt/4cffnDXfRWW1Azdk5bzSZ/z1Vdfud8LnY8vvPCCm7Iy9HPuv/9+e+ONN9xvjQqBOmd13qg7BnKPI12/O3To4LqrqYyiG0SzZ8+2K6+80lUaRGpOmxJdr3VtV0sNXa8zWuuc0bJReFpU46h5mJUWulPkXkfK/6pVVjnk8ccfd8u///77o7pGahqyrVu3utauuhGqcoq6gF599dVpvhGkc0+14t26dXPlfd2U1fVdsYZHrWq7du3qpis+++yzXUVftCPoRrqp0K/aOtUwX3LJJa5vUu/eva1du3au6WvoHN0XX3xxsB9ely5dkjUNefnll92PiX7wdJKpqciR7ubqYqA7aQr627Zt6/q1hm8byEzK6yqMKY/ffvvtVr9+fXeTSQWaMWPGuBYXKhANGzbMvaaWHSo0RerLp24Rl112meuTrb5O6p+dEvU/VY2eCn66G61WHhdeeKH179/fbr75ZmvcuLG7+XTfffdxwJFm06ZNsyZNmiR5KLBNSx4+EuVvFbYUtKhApzyua3Voc+7Bgwe7672akHfq1Mldv0P7uKblfNL5o0Bc62l9FcZ0XoR+jj5XXTC+/fZbN5aCCpYjR450LUWQexzp+q0bNbpJpEK8CvoKwvWe1K7NkQwcONDdPFLtuK7XGR1nJqNlo1CtWrWyPn36uJaHSsvw4cMzlBbk/Pwvr776qmuJp/7Z/fr1c2MIZJRaaqisogBbwbDysLZZokSJdLUw0g0CXbf1e6JzatCgQcmCdo19oC5H11xzjWUHMRpNLasTAQA5nQpN+uFRcy4gt1Btn5rv6gatBsD0k25Sqe+iatABADnbm2++6W62qvuRulREu7xZnQAAAJAzqI+dapZVO6duSOqOpKaBat2R2Z588klXY6N+gmp6qJFsNd4IACDn2rt3r+uDribx6mKaHQJuoXk5AADInEJFbKxr1aFmgWoCrv6omv4rpcECj4b63iroVvNFNTVXH0U1zQUA5FzDhw9305Cpe6m6K2UXNC8HAAAAAMAn1HQDAAAAAOATgm4AAAAAAHxC0A0AAAAAgE8IugEAAAAA8AlBNwAAAAAAPiHoBgAAAACAoBsAAAAAgOyFmm4AAAAAAHxC0A0AAAAAgPnj/wDUjE226qIJCgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] }, - "969321ee62ed49049d01ab89c6724577": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "2.0.0", - "model_name": "HTMLStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "2.0.0", - "_model_name": "HTMLStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "scope_order = [\"overall\", \"capital\", \"language\", \"continent\", \"currency\"]\n", + "alpha1_summary = (\n", + " summary_table.query(\"alpha == 1\")\n", + " .set_index(\"scope\")\n", + " .loc[scope_order]\n", + ")\n", + "alpha2_summary = (\n", + " summary_table.query(\"alpha == 2\")\n", + " .set_index(\"scope\")\n", + " .loc[scope_order]\n", + ")\n", + "\n", + "\n", + "def _percent_values(series):\n", + " return pd.to_numeric(\n", + " series.str.rstrip(\"%\").replace(\"n/a\", np.nan),\n", + " errors=\"coerce\",\n", + " )\n", + "\n", + "\n", + "x = np.arange(len(scope_order))\n", + "width = 0.2\n", + "baseline_values = _percent_values(alpha1_summary[\"baseline_top1_rate\"])\n", + "alpha1_values = _percent_values(alpha1_summary[\"swap_success_rate\"])\n", + "conditional_values = _percent_values(alpha1_summary[\"conditional_swap_rate\"])\n", + "alpha2_values = _percent_values(alpha2_summary[\"swap_success_rate\"])\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 4.8))\n", + "baseline_bars = ax.bar(\n", + " x - width,\n", + " baseline_values,\n", + " width,\n", + " label=\"Baseline source top-1\",\n", + ")\n", + "alpha1_bars = ax.bar(\n", + " x,\n", + " alpha1_values,\n", + " width,\n", + " label=\"α=1 swap (all trials)\",\n", + ")\n", + "conditional_bars = ax.bar(\n", + " x + width,\n", + " conditional_values,\n", + " width,\n", + " label=\"α=1 swap (baseline-correct)\",\n", + ")\n", + "alpha2_markers = ax.scatter(\n", + " x,\n", + " alpha2_values,\n", + " marker=\"x\",\n", + " s=55,\n", + " color=\"black\",\n", + " label=\"α=2 swap (all trials)\",\n", + " zorder=3,\n", + ")\n", + "for bars, values in (\n", + " (baseline_bars, baseline_values),\n", + " (alpha1_bars, alpha1_values),\n", + " (conditional_bars, conditional_values),\n", + "):\n", + " ax.bar_label(\n", + " bars,\n", + " fmt=\"%.1f\",\n", + " padding=2,\n", + " fontsize=8,\n", + " color=bars.patches[0].get_facecolor(),\n", + " )\n", + " for bar, value in zip(bars, values):\n", + " if pd.isna(value):\n", + " ax.annotate(\n", + " \"n/a\",\n", + " (bar.get_x() + bar.get_width() / 2, 0),\n", + " xytext=(0, 3),\n", + " textcoords=\"offset points\",\n", + " ha=\"center\",\n", + " fontsize=8,\n", + " color=bar.get_facecolor(),\n", + " )\n", + "ax.set(\n", + " ylabel=\"Top-1 rate (%)\",\n", + " xticks=x,\n", + " xticklabels=[label.title() for label in scope_order],\n", + " ylim=(0, 108),\n", + ")\n", + "ax.grid(axis=\"y\", alpha=0.25)\n", + "fig.suptitle(\"Country causal-swap success versus baseline capability\", y=0.98)\n", + "fig.legend(\n", + " handles=[baseline_bars, alpha1_bars, conditional_bars, alpha2_markers],\n", + " ncol=2,\n", + " frameon=False,\n", + " loc=\"upper center\",\n", + " bbox_to_anchor=(0.5, 0.92),\n", + ")\n", + "fig.tight_layout(rect=(0, 0, 1, 0.86))\n", + "plt.show()\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "swaprankintro", + "metadata": {}, + "source": [ + "#### Per-trial target-answer ranks\n", + "\n", + "These heatmaps retain every trial while keeping the exact ranks in the \n", + "collapsible table below.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "swaprankheatmap", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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aMpjxmjKYsZoymHGaMpixmjKYsaolWWM+nQ3L2P0wYJ0lzGqjf7dRERERERERERE9HoyTs//x+pJOpt0uWcZGCUiIiIiIiIiIrIWMhieqQPb6tLn4EuaDwd36dLFIrvRExERERERERERpTpdrOBokiekS8wYJSIiIiIiIiIishbMGE0/GaNEREREREREREREpmLGKBERERERERERkbWw0cVMJtGlUGMsGwOjRERERERERERE1oJd6ZOMgVEiIiIiIiIiIiJrobOJmUxdJx1iYJSIiIiIiIiIiMhasCt9kjEwSkREREREREREZC3YlT7JGBglIiIiIiIiIiKyGrqY4Kip66RDDIwSERERERERERGRxQsMDISfn5+6nSVLFnh6er7R9tJnZVUiIiIiIiIiIiJr7kpv6mTBFixYgGLFisHb21v9H/v2Tz/9ZPZ2mTFKRERERERERESUngdf0ltuYHTy5MkYPXo0+vbti0aNGsHX11fNv3//PjZt2oR+/frB398fgwYNMnnbDIwSERERERERERFZCysbfGnWrFlYtGgR3n333TjzixYtijp16qB06dIYPHgwA6NERERERERERETpmV6nU5Op61iqBw8eoGTJkok+Lo89evTIrG2zxigREREREREREZG1sDFzslAVK1bEpEmTEBkZGe+xqKgofPPNN2oZc7ArPREREREREREREVlsV3qpLSqj0NeqVStOjdGdO3fCwcFB1Ro1BwOjRERERERERERE1sLKaoyWKlUKFy9exK+//or9+/fj6tWrar4ESsePH4/3338fHh4eZm2bgVEiIiIiIiIiIiJrYWWBUeHu7o5PPvlETcmJgVEiIiIiIiIiIiJrYYWBUeHn54cDBw6o/0XWrFlRqVIllTlqLgZGiYiIiIiIiIiIrIS1jUofEhKCnj174o8//oBOp4O3t7ea/+TJE+j1enTo0AFz586Fi4uLydu24DGniIiIiIiIiIiIKD2PSt+vXz8cPHgQa9euxfPnz9WgSzLJ7XXr1qnHZBlzWPDLJiIiIiIiIiIiIrO60ps6Wajly5dj8eLFamR6W1tb43y53bBhQyxcuBDLli0za9sMjBIREREREREREZFFio6OhoODQ6KPy2OyjDkYGCUiIiIiIiIiIrIWkvxpcsYoLFbz5s3x8ccf49ixY/Eek3kyUn2LFi3M2jYDo0RERERERERERFY2+JKpk6WaNWsWfH19Ub58eWTMmBFFixZVk9yuUKECfHx81DLm4Kj0RERERERERERE1kJnRipktGmLT5w4EStWrMD58+fh7OyMatWq4ZtvvkHhwoUTXUfqhHbt2jXOPEdHRzWI0qt4eXlh/fr1OHfuHPbv3w8/Pz81P0uWLKhatSqKFCkCczEwSkREREREREREZC3MGUxJZ9ry//33H3r37o2KFSsiMjISI0aMUAMhnT17Fq6uromu5+HhgQsXLsR62qQ/ryFTNDkxMEpERERERERERGQlzOkar///8kFBQfEyOmV62YYNG+Jlg0qX9iNHjqBWrVqJPo8EQiXT01Th4eFYtWoV9u3bFydjVDJVW7Vq9crBmV6FNUaJiIiIiIiIiIgIOXPmhKenp3GSLvNJERgYqP739vZ+5XJPnz5F7ty51fNIQPPMmTOv3fbly5dVpmjnzp3VYEsyAr1McrtTp04oXry4WsYczBglIiIiIiIiIiKyqlHpzVgHwK1bt1R3d4OEskVfJkHKzz//HNWrV0eJEiUSXU7qjy5cuBClSpVSgdQpU6aojE8JjubIkSPR9WTU+ZIlS6pAaOy2GTJcJTgq3fo3btwIUzEwSkREREREREREZC3eoMaoh4dHvODj60hQ8vTp09i9e/crl5OBkmQykKCoZILOnTsX48aNS3S9PXv24ODBgwm2S+bJupUrV4Y52JWeiIiIiIiIiIjISuhtzJvM0adPH6xZswbbt29/ZdZnQuzt7VG2bNnXdoPPkCEDrl+/nujj8pgsYw4GRomIiIiIiIiIiKwtY9TUyQR6vV4FRVeuXIlt27Yhb968MFVUVBROnTqFrFmzvnK57t27q+7y06ZNw8mTJ3H//n01yW2Z16VLF3z88ccwB7vSExERERERERERWQm9LmYydR1Tu8//9ttvWL16Ndzd3Y0jxcuATc7Ozuq2BDOzZ89uHMBp7NixqFKlCgoUKICAgABMnjwZN27cUIHPV5H1XF1d1fIDBw5UI9sbgrMyMv3QoUMxZMgQmIOBUSIiIiIiIiIiImvxBjVGk+rHH39U/9epUyfO/EWLFqkMTnHz5k3Y2LzorO7v748ePXqoIKqXlxfKly+PvXv3olixYq99Pgl+ynTt2jVjEFaCouZkqsbGwCgRERERERERERElmWRrvs6OHTvi3Jdu7zK9CQmEvmkwNDYGRlNBN8978HBnOVcC6jhHa2o37AjVznGrtX2rNb0y3EnrJpAFedf7ILTku2cN07oJZIXyDTsEO519WjfDKj1uZ96osmllwpVN0JI6zsehFaWmfgot6XJ2H7RkZbH9ad0Eqzb8ykloycT80BSt7N+Q4Ci0TIsnluRPExNGYeryFuTWrVsYNWoUFi5caPK62ol6EBERERERERER0SvpdTqzJq168uQJfv75Z7PWZcYoERERERERERGRtbAxIxXSBhbrn3/+eeXjV69eNXvbDIwSERERERERERFZCXMyQPUWnDHaunVrNRL9q+qaGkaqh/XEg4mIiIiIiIiIiMisGqOmThYqa9asWLFiBaKjoxOcjh49ava2GRglIiIiIiIiIiIii1S+fHkcOXIk0cdfl036KuxKT0REREREREREZCX0upjJ1HUs1eDBgxESEpLo4wUKFMD27dvN2jYDo0RERERERERERNZC6m2aWnNTZ7mR0Zo1a77ycVdXV9SuXdusbTMwSkREREREREREZCWkU7nJGaNInxgYJSIiIiIiIiIishbmDKakQ7rEwCgREREREREREZG1sNHFTKaukw5xVHoiIiIiIiIiIiJKdxgYJSIiIiIiIiIisrJR6U2dLN3OnTsRGRkZb77Mk8fMwcAoERERERERERGRtdUYNXWycHXr1sWTJ0/izQ8MDFSPmYM1RomIiIiIiIiIiKyEORmgeg0ERvV6PXS6+A19/PgxXF1dzdomA6NERERERERERETWQoKHCQQQX7uOhWrbtq36X4KiXbp0gaOjo/GxqKgonDx5EtWqVTNr2wyMEhERERERERERWQlryxj19PQ0Zoy6u7vD2dnZ+JiDgwOqVKmCHj16mLVtBkaJiIiIiIiIiIishTk1Q3WwWIsWLVL/58mTB4MGDTK723xCGBglIiIiIiIiIiIiizZq1Khk3yYDo0RERERERERERFbC2rrSx7Zs2TL89ddfuHnzJsLDw+M8dvToUZjKxuQ1iIiIiIiIiIiIyLIHXzJ1snAzZ85E165d4evri2PHjqFSpUrImDEjrl69iiZNmpi1TQZGiYiIiIiIiIiIrCxj1NTJ0v3www+YN28evv/+ezXo0pAhQ7B582b07dsXgYGBZm2TgVEiIiIiIiIiIiJrG3zJ1MnCSff5atWqqdsyMn1wcLC6/eGHH+L33383a5sMjBIREREREREREVkJvY15k6XLkiULnjx5om7nypUL+/fvV7evXbsGvV5v1jY18LKJiIiIiIiIiIgoPatXrx7++ecfdVtqjfbv3x8NGjTAe++9hzZt2pi1TY5KT0REREREREREZC3M6Rqvg8WT+qLR0dHqdu/evdXAS3v37kXLli3Rs2dPs7bJwCgREREREREREZGVMGcwJb0GAqM2NjZqMmjfvr2a3gQDo0RERERERERERNZCp4uZTF0nHWJglIiIiIiIiIiIyIpoIQPUEjAwSkREREREREREZC2stMZoSmBglIiIiIiIiIiIyEpYa43RlPCiYikRERERERERERFROsGMUSIiIiIiIiIiImthRV3py5YtC10SB4Y6evSoydtnYJSIiIiIiIiIiMhKWFNX+tatWxtvP3/+HD/88AOKFSuGqlWrqnn79+/HmTNn8Omnn5q1fQZGiYiIiIiIiIiIrIUVZYyOGjXKeLt79+7o27cvxo0bF2+ZW7dumbV91hglIiIiIiIiIiKysoxRUydL9/fff6NTp07x5n/wwQdYvny5WdtkYJSIiIiIiIiIiMjaMkZNnSycs7Mz9uzZE2++zHNycjJrm+xKT0RERERERERERBbt888/xyeffKIGWapUqZKad+DAASxcuBAjR440a5sMjBIREREREREREVkJvU6nJlPXsXTDhg1Dvnz5MGPGDPz6669qXtGiRbFo0SK8++67Zm2TXemJiIiIiIiIiIisRSp0pZ84cSIqVqwId3d3+Pj4qNHjL1y4kKQ6oUWKFFFd30uWLIl169aZ9LwSAJWu80+ePFGT3DY3KCoYGCUiIiIiIiIiIrISqTH40n///YfevXtj//792Lx5MyIiItCwYUOEhIQkus7evXvRoUMHdOvWDceOHVPBVJlOnz6d5OcNCAjAggULMGLECBUYFdK1/s6dOzAHu9ITERERERERERFZC3MGU9KZtviGDRvi3F+8eLHKHD1y5Ahq1aqV4DrSBb5x48YYPHiwuj9u3DgVVJ01axbmzJnz2uc8efIk6tevD09PT1y/fh3du3eHt7c3VqxYgZs3b+KXX34x7UUwY5SIiIiIiIiIiMiKvEFX+qCgoDhTWFhYkp4yMDBQ/S+BysTs27dPBTZja9SokZqfFAMGDECXLl1w6dKlOKPQN23aFDt37kSKZ4zK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XXqaG2qMSslMKbE5c+ZM47J9+/bVXmBU6nhK/dBBgwapuqPSFV7qh0rWaHI7ePAgvv32WwQHByNfvnxq53Xv3j3Zn4eIiIiIiIiIiCjVmDPSvA4WT8Yk8vLywtmzZ9VkIOMJyWMGMjCYJgOjOXPmVFHe1PDySPVERERERERERESaZ6WB0WvXriX7NlnUjIiIiIiIiIiIiDRDeponx7BJDIwSERERERERERFZCWsdlV5Il3kpvenk5KQmub1gwQKzt2dRXemJiIiIiIiIiIjoDVhpV/qvvvoK3333HT777DPj4Ev79u1D//79cfPmTYwdO9bkbTIwSkREREREREREZCXMyQDVayAw+uOPP2L+/Pno0KGDcV7Lli1RqlQpFSxlYJSIiIiIiIiIiCg9s9KM0YiICFSoUCHe/PLlyyMyMtKsbbLGKBERERERERERkbUFRk2dLNyHH36oskZfNm/ePHTs2NGsbbIrPREREREREREREWli8KVNmzahSpUq6v6BAwdUfdFOnTphwIABxuWkFmlSMDBKRERERERERERkJay1xujp06dRrlw5dfvKlSvq/0yZMqlJHjPQ6ZL+YhgYJSIiIiIiIiIishZWWmN0+/btyb5NBkaJiIiIiIiIiIishZUGRlMCA6NERERERERERERWwpq60rdt2xaLFy+Gh4eHuv0qK1asMHn7DIwSERERERERERFZCyvKGPX09DTWDJXbyY2BUSIiIiIiIiIiIrI4ixYtSvB2cmFglIiIiIiIiIiIyEpYU1f6lMbAKBERERERERERkbWwoq70ZcuWNXalf52jR4+avH0GRomIiIiIiIiIiKyJhQY6TdW6dWukJAZGiYiIiIiIiIiIrIQ1daUfNWpUim7fJkW3TkRERERERERERKnfld7UycIdOnQIBw4ciDdf5h0+fNisbTIwSkRERERERERERBatd+/euHXrVrz5d+7cUY+Zg4FRIiIiIiIiIiIismhnz55FuXLlEhygSR4zBwOjREREREREREREVlZj1NTJ0jk6OuL+/fvx5t+7dw92duYNo8TAKBERERERERERkbWw0hqjDRs2xPDhwxEYGGicFxAQgBEjRqBBgwZmbZOj0hMREREREREREVkLcwKdOli8KVOmoFatWsidO7fqPi+OHz8OX19fLFmyxKxtMjBKRERERERERERkLczpGq+DxcuePTtOnjyJpUuX4sSJE3B2dkbXrl3RoUMH2Nvbm7VNBkaJiIiIiIiIiIishZVmjApXV1d8/PHHSC6sMUpERERERERERETpDjNGiYiIiIiIiIiIrIQ5o8zrNZIxmtwYGCUiIiIiIiIiIrIWVtyVPrkxMEpERERERERERGQtdPqYydR10iEGRomIiIiIiIiIiKwEu9InHQdfIiIiIiIiIiIisrau9KZOGpY3b15069YNd+/eNWk9BkaJiIiIiIiIiIisRToMjHbu3BlRUVGoXr26SeuxKz0RERERERERERFp1ujRo81ajxmjREREREREREREVlZj1NTJVDt37kSLFi2QLVs26HQ6rFq16pXL79ixQy338uTn54e0woxRIiIiIiIiIiIia2FO13id6U8TEhKC0qVL46OPPkLbtm2TvN6FCxfg4eFhvO/j4/PadaKjo/Hff/9h165duHHjBp49e4bMmTOjbNmyqF+/PnLmzGn6C2BglIiIiIiIiIiIyIq8QWA0KCgozmxHR0c1JaRJkyZqMpUEQjNkyJCkZUNDQzF16lT8+OOPePLkCcqUKaMyVJ2dnXH58mWVpdqjRw80bNgQX331FapUqWJSW5gxmgp2hergaqeNqgXnw7JCS4o43oOW1HGOTusmEJllR6g2zmFa/az99aQStKSU6y1oyYAcm6AVIcFR0E5r0zcbJ0fY6BygBbf7loOWZPt2L7Rk4t+loCUToR1Zoa1jYfnBhtASGxxL6yZYtYn5tXVu0Bqt7N9IfQSAK6n/xDp9zGTqOkC8zMtRo0aZXb8zMRLcDAsLQ4kSJdS2XzVgUqFChVC1alXMnz8fDRo0gL29fbxlJIP0t99+Q/v27fHFF1+oQGlSMTBKRERERERERERkJSTEaWrNUP3//79161acbu6JZYuaI2vWrJgzZw4qVKigAqMLFixAnTp1cODAAZQrl/APuZs2bULRokVfud3cuXNj+PDhGDRoEG7evGlSmxgYJSIiIiIiIiIiIkhQNHZgNDkVLlxYTQbVqlXDlStXMG3aNCxZsiTBdWIHRSMiIhLMGBWPHj1CpkyZkD9/fpPapK2+kURERERERERERGQVKlWqpGqFJoV0ldfr45cIuH//vso8NQcDo0RERERERERERNY2+JKpUxo4fvy46mKfFNJNvnv37nHm+fn5qaBokSJFzHp+dqUnIiIiIiIiIiKyElJf1OQaozrTn+fp06dxsj2vXbumAp3e3t7IlSuXqvt5584d/PLLL+rx6dOnI2/evChevDieP3+uaoxu27ZN1RFNinXr1qFWrVoYMGAAvvvuO9y9exd169ZF6dKl8ccff5j+AhgYJSIiIiIiIiIisiJvMCq9KQ4fPqwCkwYSsBSdO3fG4sWLce/evTiDIYWHh2PgwIEqWOri4oJSpUphy5YtcbbxKpkzZ1ZB1Bo1aqj7a9asUYM2LV26FDY25nWKZ8YoERERERERERGRtTCna7zO9KeRLuwJ1fw0kOBobEOGDFHTm8iZMyc2b96MmjVrokGDBmrQJp3O/DoADIwSERERERERERFZi1QKjKYGLy+vBAOfz549w7///ouMGTMa5z158sTk7TMwSkRERERERERERBZH6pKmJAZGiYiIiIiIiIiIrIUVZYx27tw5RbfPwCgREREREREREZG1SKXBl9JCdHQ0Ll++jAcPHqjbscmI9aZiYJSIiIiIiIiIiMhaWFHGaGz79+/H+++/jxs3bsQb9EnqkEZFRcFUDIwSERERERERERFZCb0uZjJ1HUvXq1cvVKhQAWvXrkXWrFnfaDR6AwZGiYiIiIiIiIiIrIWVdqW/dOkSli1bhgIFCiTbNm2SbUtEREREREREREREKaBy5cqqvmhyYsYoERERERERERGRtbDSGqOfffYZBg4cCD8/P5QsWRL29vZxHi9VqpTJ22RglIiIiIiIiIiIyEpI6U1Ty2/qNBAYffvtt9X/H330kXGe1BmVgZg4+BIREREREREREVF6Z6U1Rq9du5bs22TGKBERERERERERkbWw0q70uXPnTvZtMjBKRERERERERERkLaw0MGpw9uxZ3Lx5E+Hh4YitZcuWMBUDo0RERERERERERGTRrl69ijZt2uDUqVPG2qJCbouoqCiTt2mT7K0kIiIiIiIiIiKiNKI3c7Js/fr1Q968efHgwQO4uLjgzJkz2LlzJypUqIAdO3aYtU1mjBIREREREREREVkLK+1Kv2/fPmzbtg2ZMmWCjY2NmmrUqIGJEyeib9++OHbsmMnbZMYoERERERERERGRldDp9GZNlk66yru7u6vbEhy9e/eucVCmCxcumLVNZowSERERERERERFZEw1kgJqqRIkSOHHihOpOX7lyZXz77bdwcHDAvHnzkC9f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ITh//rwKZP7111/o379/kp5PskHr16+Pu3fv4siRI2q6ffs2GjRogA4dOuDOnTsqKzWp2xMMjBIREREREREREVkJGXjJnMlUMrC5DH4kk6Gru9z+6quv1H0Zk8gQJBV58+bF2rVrVZaoDGw+depULFiwAI0aNUrS88nYQePGjYvT1V9KB8gYQN9++60aX0ieWwKmScWu9ERERERERERERFbCBno1mbqOqerUqYNXDV20ePHiBNeRLvHmkDqkDx48iNdN/uHDh6q+qsiQIQPCw8OTvE1mjBIREREREREREVmJ1OpKn9qkK/1HH32kBnmSLvQyye1u3boZB306ePAgChUqlORtMmOUiIiIiIiIiIjISpjTNd5GA4HRuXPnqvqh7du3R2RkpJpnZ2eHzp07Y9q0aeq+DMIk3fOTioFRIiIiIiIiIiIislgyEr2MSC+1RCUIKoM4iXz58sHNzc24XJkyZUzaLgOjREREREREREREVsIaM0ZtbW3RsGFDnDt3Tg3iVKpUqWTZLmuMEhERERERERERWYnUGpU+tZUoUcKYKZpcGBglIiIiIiIiIiKyEtYaGB0/fjwGDRqENWvW4N69e2ok+tiTOdiVnoiIiIiIiIiIyEroVCak3uR1LF3Tpk3V/y1btoRO96LFer1e3Zc6pKZiYJSIiIiIiIiIiMhKWGONUbF9+3YkNwZGiYiIiIiIiIiIrIS1BkZr166d7NtkYJSIiIiIiIiIiIg04dmzZ7h58ybCw8PjzDdnpHoGRomIiIiIiIiIiKyEtWaMPnz4EF27dsX69esTfNycGqMclZ6IiIiIiIiIiMhKWOuo9J9//jkCAgJw4MABODs7Y8OGDfj5559RsGBB/PPPP2ZtkxmjREREREREREREVkJGpDd1VHobE5dPC9u2bcPq1atRoUIF2NjYIHfu3GjQoAE8PDwwceJENGvWzORtMmOUiIiIiIiIiIjISlhrxmhISAh8fHzUbS8vL9W1XpQsWRJHjx41a5sMjBIREREREREREVkJG120WZOlK1y4MC5cuKBuly5dGnPnzsWdO3cwZ84cZM2a1axtsis9ERERERERERERWbR+/frh3r176vaoUaPQuHFjLF26FA4ODli8eLFZ22RglIiIiIiIiIiIyEpY66j0H3zwgfF2+fLlcePGDZw/fx65cuVCpkyZzNomu9ITERER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jVRv4O/vrwKWsqxkf0rG6d69e1GsWLEkPd/u3bsxfvx49OrVK858qVG6adMmLF++XA3sNHPmTAZGiYiIiIiIiIiI0pvUyhgVffr0UVNCduzYEef+tGnT1GSujRs34ptvvok3/6233sLAgQPV7aZNm2LYsGFJ3iZrjBIREREREREREVkJG0SbNVk6qSf677//xpsv8+QxIV353d3dk7xNdqUnIiIiIiIiIiKyEpIFaasztSu95Rs5cqSqIbp9+3ZjjdFDhw5h3bp1mDNnjrq/efNmNQhTUjEwSkREREREREREZCVsEQ1b6Exex9JJfVKpRzpr1iysWLFCzStcuDD+++8/VKtWTd03dKlPKgZGiYiIiIiIiIiIyOJVr15dTcmFgVEiIiIiIiIiIiIrYaOLho1OZ/I6WhAVFYVVq1bh3Llz6n7x4sXRsmVL2NramrU9BkaJiIiIiIiIiIishLV2pb98+bIadf7OnTuqC72YOHEicubMibVr1yJ//vwmb1MLtVWJiIiIiIiIiIgoCWTgJXMmS9e3b18V/Lx16xaOHj2qpps3byJv3rzqMXMwY5SIiIiIiIiIiMhK2CAaNiZmjNpoIGNUBlnav38/vL29jfMyZsyISZMmmV13lIFRIiIiIiIiIiIiK2Gri4atiTVGbTVQY9TR0RHBwcHx5j99+hQODg5mbZNd6YmIiIiIiIiIiMiiNW/eHB9//DEOHDgAvV6vJskg7dWrlxqAyRwMjBIREREREREREVkJ6RZva+Jko4Gu9DNnzlQ1RqtWrQonJyc1SRf6AgUKYMaMGWZtk13piYiIiIiIiIiIrIQN9GoydR1LlyFDBqxevRqXLl3C+fPn1byiRYuqwKi5GBglIiIiIiIiIiKyEtZaY9SgYMGCakoODIwSERERERERERFZCVvo1WTqOpZowIABSV72u+++M3n7DIwSERERERERERFZVVf6aKvoSn/s2LEkLaczMUPWgIFRIiIiIiIiIiIisjjbt29P0e0zMEpERERERERERGRVNUZNXyc9YmCUiIiIiIiIiIjISlhTjdGUxsAoERERERERERGRldDp9LDR6U1eJz1iYJSIiIiIiIiIiMhK2CIatmaskx4xMEpERERERERERGQl2JU+6RgYJSIiIiIiIiIishI2ZnSlt0mnXelt0roBRERERERERERERKmNGaNERERE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uXPnqq+++kply5ZNVaxYUbVp00bk9RqAfmNkWbNmjRo9erSaNWuW+s9//qNuu+02ed6MvS2YcVu4cKH67rvvVKFChQKv2Sard3wzZsyoTp06pVq1aqU2b94sKd+9e/dWLVu2VKVLl1Y24R1HyPnZZ5+pvXv3qp49e6omTZqovHnz+iab99rBIiRHjhwi3/jx41X+/PlDHoNtYNGEKAgspoYMGaLWr1+vDh06pAYPHqxq1Kghr9mCdxyx4MN1t23bNvXbb7/JfFamTBllG1j8V6lSRXXp0kVlypRJ2Yx3fPHz4cOHVYECBZStJCQkyL0NzJ8/X+a0okWLqgoVKsgcR0hqoG0XWVyy71yz7Vy072y27dKDfeeSbQdo30VvfGnfEd/RJF1w5swZ+ffnn3/WVapU0SVLltRZsmTRd999t/7000/10aNHA+89d+6cj5JqvWbNmsDPK1eu1Llz59a33HKLyF2tWjV977336k2bNgXec/bsWe0np0+fln937dql+/Xrp8uVKydje+211+rnnntO//3339omunfvrr/66it94sQJffvtt+tcuXLpUqVK6R9//NGaMfXy9ddf62nTpgV0tG3btrp69ep61qxZMr7Qj8aNG+sFCxbo48ePa9t44IEHdI8ePXSjRo1Ej+Pi4vRDDz2kN27c6Ns4m2v84Ycf1pMnT9YJCQkiI/QgX758MuY26kIoWrduratWraoHDBig77nnHh0fH6979uypDxw4oG2jY8eO+v7779ft2rUTWXPkyKFbtmwpuvvvv//6LV5gXt23b5++/vrr9eWXX66/+eabRPOczbz44osyFxQoUEDGeMWKFfrkyZPaFoLvBZ06ddKlS5fWGTJk0LVq1dJ9+vTRv/76qzX3YmI/tO0ii0v2nWu2nev2nY22XXqy71yy7QDtu8hC+47YAB2j6QDv4qp8+fL6kUceEcMJN0zcyMuWLauHDh2qt27dqv3mww8/FBlHjx4tBv+TTz6pn3jiCXlt//79etSoUbpevXq6du3aIr9NYBxhmH777bf6+++/F6MJzlwYenDw2rDYnTp1qpzzbdu2ye8wRjGmV155pW7SpImeMWNGYKFlk7EBmaEHcOzDQPI6DzZs2KBvuOEGXbx4cdGb7du3a1v47LPPZHGC8TaOr48//ljnyZNHZP7888/1sWPHoiqTOb9GF8x1jzkB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HWS+wP3Cz2stKXk//vhjaSSB13EdYuz9lpfYj0u2kovyumbfuXo/d8m+c8W2c1UfXLLtAO27yED7jrgGHaMOgUU5Cq2jGDhu0rixo6uwzfL+9NNPUiTcdnlnzpwp3RwBOjoWK1ZMCrGbGzcabGAhgJu5304PYAw1GPoocI7i8I8//njYm5ANhGokgO6a0ezUnVpgOGPhhyYCy5Ytk0Lyq1evVjbh1QUsOtBB1XZdCNdIAI68xo0bB95j9u1sOI558+bJAgQd3bHQgxMUjQNskC2UPqDLKhaiffr0kS7GNuNtEgXHIpzLWACiEROaZGDx53dzBi/GER4sLxwDOXLksE5eYje07SKLS/adi7adi/adC7adq/ada7YdoH0XOWjfEae4CFGnJAocOHBAd+3aVerYoW4Twrqff/55eS1U0we/cU3ef/75R/5F/SM0C0Ba7N69e7XtoJkKUtdsGsukQApyqEYCNmJSp3788UdJ/UEaW9myZaU+GdKWbEhd8spQp04dfdtttzmjC0k1ErBVF5A2j7kMtciQCgZ9tkUXgFeOO+64Q998882+pwCmpUkUdNkrt006DblQn9AVeYnduGYruSavq/ada7adS/adC7ad6/adK7YdoH0X2bGlfUdcwt9cYJJiChYsqN544w1VuHBhiaBBigoiVYBJ8fA7tdtVeRMSEiS1dNy4cRJNgMgl7GIiKsxmELGE3VjIbctYhgNRGOD6669XPXr0UM8884xq3ry5RBTYCFLpTNRIs2bN1COPPCKp01myZAns1Pux0w25vBgZFi5cKKlLb731lvW64NUHRF3ee++9EhWD8f3222+VzbqAKIiuXbuqVq1aqVKlSknEkV+6YGTzYuSAHmD+mjBhgu/R7ckB+TAfQF5cW8gqQISBV26bdNo1eYnduGQruSivi/adS7ada/adrbadkc11+84l2w7QvossrtlLrslLIoDfnlmSPAkJCfLvxo0bpYvm3Llz9RtvvCEdKtEZ2TZcktdEOOzevVt2uMePHy+7tB999JF0VrVpxxgF69E45Y8//gg8hx1vF3CtkYCRt3PnztKV1BTmR6dp87NfMgFcU4jSWbBggXTTBd6O2LbjUiMBry4g2sic/0OHDkmxfhuYNm1aoAg8riXMte+//752AdeaRJn7G+S97rrrAvKuX7/eb9GIY7hkK7kor832XXqx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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pair_values = list(permutations(COUNTRY_CONFIG[\"args\"], 2))\n", + "pair_order = [f\"{source}->{target}\" for source, target in pair_values]\n", + "pair_labels = [f\"{source[:2]}→{target[:2]}\" for source, target in pair_values]\n", + "function_order = [spec[\"name\"] for spec in COUNTRY_CONFIG[\"funcs\"]]\n", + "rank_data = conditions.assign(\n", + " pair=conditions[\"source\"] + \"->\" + conditions[\"target\"]\n", + ")\n", + "\n", + "\n", + "def _compact_rank(rank):\n", + " if rank >= 100_000:\n", + " return f\"{rank / 1000:.0f}k\"\n", + " if rank >= 1_000:\n", + " return f\"{rank / 1000:.1f}k\"\n", + " return str(rank)\n", + "\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(18, 4.8), sharey=True)\n", + "last_image = None\n", + "for axis, alpha in zip(axes, SWAP_ALPHAS):\n", + " subset = rank_data.query(\"alpha == @alpha\").set_index(\n", + " [\"function\", \"pair\"]\n", + " )\n", + " ranks = np.array(\n", + " [\n", + " [\n", + " subset.loc[(function, pair), \"swapped_target_rank\"]\n", + " for pair in pair_order\n", + " ]\n", + " for function in function_order\n", + " ],\n", + " dtype=float,\n", + " )\n", + " last_image = axis.imshow(\n", + " np.log10(np.clip(ranks, 1, 1000)),\n", + " cmap=\"viridis_r\",\n", + " vmin=0,\n", + " vmax=3,\n", + " aspect=\"auto\",\n", + " )\n", + " for row, function in enumerate(function_order):\n", + " for column, pair in enumerate(pair_order):\n", + " trial = subset.loc[(function, pair), :]\n", + " rank = int(trial[\"swapped_target_rank\"])\n", + " if trial[\"success\"]:\n", + " annotation = \"✓\"\n", + " elif trial[\"target_tied_for_top\"]:\n", + " annotation = f\"{_compact_rank(rank)}†\"\n", + " else:\n", + " annotation = _compact_rank(rank)\n", + " axis.text(\n", + " column,\n", + " row,\n", + " annotation,\n", + " ha=\"center\",\n", + " va=\"center\",\n", + " fontsize=8,\n", + " color=(\"white\" if ranks[row, column] >= 50 else \"black\"),\n", + " )\n", + " axis.set(\n", + " title=f\"α={alpha:g}: target-answer rank after swap\",\n", + " xticks=np.arange(len(pair_labels)),\n", + " xticklabels=pair_labels,\n", + " yticks=np.arange(len(function_order)),\n", + " yticklabels=[name.title() for name in function_order],\n", + " )\n", + " axis.tick_params(axis=\"x\", rotation=55)\n", + "\n", + "colorbar = fig.colorbar(last_image, ax=axes, shrink=0.82, pad=0.02)\n", + "colorbar.set_label(\"log10(target rank), clipped at 1000\")\n", + "fig.text(\n", + " 0.5,\n", + " -0.04,\n", + " \"✓ = deterministic top-1 success; † = exact maximum tie lost by argmax order\",\n", + " ha=\"center\",\n", + ")\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b4a1184a", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-26T08:13:29.721608Z", + "iopub.status.busy": "2026-07-26T08:13:29.721486Z", + "iopub.status.idle": "2026-07-26T08:13:33.635954Z", + "shell.execute_reply": "2026-07-26T08:13:33.635035Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
Full 48-trial matrix (top-1 tokens and target ranks)
function source target prompt source_answer target_answer baseline_top1 baseline_source_rank baseline_source_is_top1 baseline_target_rank alpha1_top1 alpha1_target_rank alpha1_success alpha2_top1 alpha2_target_rank alpha2_success
capital France Canada The capital of France is the city of ' Paris' ' Ottawa' ' Paris' 1 True 850 ' Montreal' 3 False ' Canada' 31 False
capital France China The capital of France is the city of ' Paris' ' Beijing' ' Paris' 1 True 2097 ' Paris' 2 False ' China' 17 False
capital France Egypt The capital of France is the city of ' Paris' ' Cairo' ' Paris' 1 True 5429 ' Cairo' 1 True ' Egypt' 7 False
capital Canada France The capital of Canada is the city of ' Ottawa' ' Paris' ' Ottawa' 1 True 105 ' Ottawa' 2 False ' Canada' 255978 False
capital Canada China The capital of Canada is the city of ' Ottawa' ' Beijing' ' Ottawa' 1 True 190 ' Beijing' 1 True ' China' 18 False
capital Canada Egypt The capital of Canada is the city of ' Ottawa' ' Cairo' ' Ottawa' 1 True 3069 ' Cairo' 1 True ' Egypt' 10 False
capital China France The capital of China is the city of ' Beijing' ' Paris' ' Beijing' 1 True 204 ' Paris' 1 True ' China' 255952 False
capital China Canada The capital of China is the city of ' Beijing' ' Ottawa' ' Beijing' 1 True 1474 ' Beijing' 3 False ' Canada' 31 False
capital China Egypt The capital of China is the city of ' Beijing' ' Cairo' ' Beijing' 1 True 2144 ' Cairo' 1 True ' Egypt' 9 False
capital Egypt France The capital of Egypt is the city of ' Cairo' ' Paris' ' Cairo' 1 True 110 ' Cairo' 2 False ' Egypt' 255973 False
capital Egypt Canada The capital of Egypt is the city of ' Cairo' ' Ottawa' ' Cairo' 1 True 834 ' Cairo' 5 False ' Canada' 45 False
capital Egypt China The capital of Egypt is the city of ' Cairo' ' Beijing' ' Cairo' 1 True 1002 ' Cairo' 7 False ' China' 21 False
language France Canada Most people in France speak ' French' ' English' ' French' 1 True 2 ' English' 1 True ' France' 80757 False
language France China Most people in France speak ' French' ' Chinese' ' French' 1 True 347 ' Chinese' 1 True ' France' 255996 False
language France Egypt Most people in France speak ' French' ' Arabic' ' French' 1 True 163 ' Arabic' 1 True ' Egypt' 159 False
language Canada France Most people in Canada speak ' English' ' French' ' English' 1 True 2 ' French' 1 True ' France' 2 False
language Canada China Most people in Canada speak ' English' ' Chinese' ' English' 1 True 132 ' English' 2 False ' China' 1 False
language Canada Egypt Most people in Canada speak ' English' ' Arabic' ' English' 1 True 209 ' English' 1 False ' Egypt' 70 False
language China France Most people in China speak ' Chinese' ' French' ' Mandarin' 2 False 562 ' French' 1 True ' France' 3 False
language China Canada Most people in China speak ' Chinese' ' English' ' Mandarin' 2 False 4 ' English' 1 True ' Canada' 161394 False
language China Egypt Most people in China speak ' Chinese' ' Arabic' ' Mandarin' 2 False 399 ' Arabic' 1 True ' Egypt' 1692 False
language Egypt France Most people in Egypt speak ' Arabic' ' French' ' Arabic' 1 True 16 ' Arabic' 2 False ' France' 2 False
language Egypt Canada Most people in Egypt speak ' Arabic' ' English' ' Arabic' 1 True 2 ' Arabic' 2 False ' Canada' 5479 False
language Egypt China Most people in Egypt speak ' Arabic' ' Chinese' ' Arabic' 1 True 724 ' Arabic' 7 False ' China' 2 False
continent France Canada France is a country on the continent of ' Europe' ' North' ' Europe' 1 True 9 ' Europe' 2 False ' Canada' 840 False
continent France China France is a country on the continent of ' Europe' ' Asia' ' Europe' 1 True 16 ' Europe' 3 False ' China' 64 False
continent France Egypt France is a country on the continent of ' Europe' ' Africa' ' Europe' 1 True 11 ' Europe' 2 False ' Egypt' 77811 False
continent Canada France Canada is a country on the continent of ' North' ' Europe' ' North' 1 True 12 ' Europe' 1 True ' Canada' 255824 False
continent Canada China Canada is a country on the continent of ' North' ' Asia' ' North' 1 True 6 ' North' 2 False ' Canada' 255907 False
continent Canada Egypt Canada is a country on the continent of ' North' ' Africa' ' North' 1 True 21 ' Africa' 1 True ' Egypt' 92074 False
continent China France China is a country on the continent of ' Asia' ' Europe' ' Asia' 1 True 8 ' Europe' 1 True ' France' 277 False
continent China Canada China is a country on the continent of ' Asia' ' North' ' Asia' 1 True 11 ' Asia' 2 False ' Canada' 9674 False
continent China Egypt China is a country on the continent of ' Asia' ' Africa' ' Asia' 1 True 7 ' Africa' 1 True ' Egypt' 210462 False
continent Egypt France Egypt is a country on the continent of ' Africa' ' Europe' ' Africa' 1 True 10 ' Africa' 5 False ' Egypt' 255947 False
continent Egypt Canada Egypt is a country on the continent of ' Africa' ' North' ' Africa' 1 True 3 ' Africa' 3 False ' Egypt' 255859 False
continent Egypt China Egypt is a country on the continent of ' Africa' ' Asia' ' Africa' 1 True 2 ' Africa' 2 False ' Egypt' 255965 False
currency France Canada The single-word name for the currency now used in France is the ' Euro' ' Dollar' ' franc' 6 False 423 ' Canadian' 75 False ' Canada' 27724 False
currency France China The single-word name for the currency now used in France is the ' Euro' ' Yuan' ' franc' 6 False 1024 ' franc' 10 False ' China' 484 False
currency France Egypt The single-word name for the currency now used in France is the ' Euro' ' Pound' ' franc' 6 False 325 ' franc' 212 False ' Egypt' 197382 False
currency Canada France The single-word name for the currency now used in Canada is the ' Dollar' ' Euro' ' Canadian' 17 False 172 ' franc' 9 False ' France' 219 False
currency Canada China The single-word name for the currency now used in Canada is the ' Dollar' ' Yuan' ' Canadian' 17 False 430 ' Canadian' 8 False ' Canada' 255822 False
currency Canada Egypt The single-word name for the currency now used in Canada is the ' Dollar' ' Pound' ' Canadian' 17 False 235 ' Egyptian' 69 False ' Egypt' 156785 False
currency China France The single-word name for the currency now used in China is the ' Yuan' ' Euro' ' ren' 5 False 144 ' yuan' 8 False ' France' 14530 False
currency China Canada The single-word name for the currency now used in China is the ' Yuan' ' Dollar' ' ren' 5 False 202 ' Canadian' 80 False ' Canada' 156128 False
currency China Egypt The single-word name for the currency now used in China is the ' Yuan' ' Pound' ' ren' 5 False 1325 ' ren' 104 False ' Egypt' 111835 False
currency Egypt France The single-word name for the currency now used in Egypt is the ' Pound' ' Euro' ' Egyptian' 7 False 135 ' ' 9 False ' Egypt' 255870 False
currency Egypt Canada The single-word name for the currency now used in Egypt is the ' Pound' ' Dollar' ' Egyptian' 7 False 150 ' Canadian' 45 False ' Canada' 81846 False
currency Egypt China The single-word name for the currency now used in Egypt is the ' Pound' ' Yuan' ' Egyptian' 7 False 5341 ' ' 27 False ' Egypt' 253668 False
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import HTML, display\n", + "\n", + "full_table_html = report_table.to_html(index=False, border=0).replace(\n", + " \"\\n\", \"\"\n", + ")\n", + "display(\n", + " HTML(\n", + " \"
\"\n", + " \"Full 48-trial matrix \"\n", + " \"(top-1 tokens and target ranks)\"\n", + " \"
\"\n", + " f\"{full_table_html}\"\n", + " \"
\"\n", + " )\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "cda35de3", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-26T08:13:33.637767Z", + "iopub.status.busy": "2026-07-26T08:13:33.637647Z", + "iopub.status.idle": "2026-07-26T08:13:37.545416Z", + "shell.execute_reply": "2026-07-26T08:13:37.544380Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " function source target prompt source_answer target_answer baseline_top1 baseline_source_rank baseline_source_is_top1 baseline_target_rank alpha1_top1 alpha1_target_rank alpha1_success alpha2_top1 alpha2_target_rank alpha2_success\n", + " capital France China The capital of France is the city of ' Paris' ' Beijing' ' Paris' 1 True 2097 ' Paris' 2 False ' China' 17 False\n", + " language France China Most people in France speak ' French' ' Chinese' ' French' 1 True 347 ' Chinese' 1 True ' France' 255996 False\n", + "continent France China France is a country on the continent of ' Europe' ' Asia' ' Europe' 1 True 16 ' Europe' 3 False ' China' 64 False\n", + " currency France China The single-word name for the currency now used in France is the ' Euro' ' Yuan' ' franc' 6 False 1024 ' franc' 10 False ' China' 484 False\n" + ] + } + ], + "source": [ + "france_to_china = report_table.query(\n", + " \"source == 'France' and target == 'China'\"\n", + ")\n", + "print(france_to_china.to_string(index=False))" + ] + }, + { + "cell_type": "markdown", + "id": "554972b9", + "metadata": {}, + "source": [ + "The unperturbed model puts the source-appropriate answer top-1 on\n", + "**33/48 trial rows (68.75%)**, equivalently 11/16 unique function-source prompts:\n", + "capital 12/12, language 9/12, continent 12/12, and currency 0/12. Trial-row and\n", + "unique-prompt rates are identical because every source prompt occurs with three\n", + "targets.\n", + "\n", + "The primary `alpha=1` intervention succeeds unconditionally on **16/48 trials\n", + "(33.33%)**: capital 5/12, language 7/12, continent 4/12, and currency 0/12.\n", + "Conditioned on the baseline source answer already being top-1, it succeeds on\n", + "**13/33 trials (39.39%)**: capital 5/12, language 4/9, continent 4/12, and\n", + "currency 0/0 (not applicable). Target-answer rank improves on 45/48 trials and\n", + "never worsens.\n", + "\n", + "For France → China, the baseline is correct on 3/4 templates. `alpha=1`\n", + "succeeds on 1/4 trials unconditionally, or 1/3 baseline-correct trials: only\n", + "the language template flips top-1 (`French` → `Chinese`). The capital,\n", + "continent, and currency targets move to ranks 2, 3, and 9. The language prompt\n", + "exactly extends the qualitative demo. The companion configuration intentionally\n", + "uses the longer capital prompt `\"The capital of France is the city of\"`, so its\n", + "ranks should not be compared directly with the shorter capital prompt above.\n", + "\n", + "The double-strength diagnostic succeeds on **0/48 trials**, and on 0/33\n", + "baseline-correct trials. Every `alpha=2` top-1 is instead a country token: the\n", + "target country on 35 trials and the source country on 13. This is systematic\n", + "overshoot on the 2B base model, not evidence that `alpha=2` improves this\n", + "model. Two conditions have target rank 1 but fail deterministic argmax because\n", + "another token shares the exact maximum: language/Canada→Egypt at `alpha=1` and\n", + "language/Canada→China at `alpha=2`.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7020c7b5", + "metadata": {}, + "source": [ + "## 5. Steering along a J-lens vector\n", + "\n", + "`steering_hooks` adds a token's unit-normalized lens direction, scaled by the activation's median\n", + "residual norm times `alpha` (the paper's directed-modulation protocol). `ablation_hooks`\n", + "(projecting a direction *out*) works the same way." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "89692d80", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-26T08:13:37.547587Z", + "iopub.status.busy": "2026-07-26T08:13:37.547472Z", + "iopub.status.idle": "2026-07-26T08:13:41.825538Z", + "shell.execute_reply": "2026-07-26T08:13:41.824391Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "baseline : [' the', ' a', ' visit', ' my', ' see']\n", + "steered : [' paris', 'Paris', ' París', 'paris', ' Paris']\n" + ] + } + ], + "source": [ + "steer = lens.steering_hooks(model, \" Paris\", layers=range(10, 21), alpha=2.0)\n", + "prompt = \"This weekend I am planning a trip to\"\n", + "print(\"baseline :\", show_next_tokens(prompt))\n", + "print(\"steered :\", show_next_tokens(prompt, hooks=steer))" + ] + }, + { + "cell_type": "markdown", + "id": "c99c3348", + "metadata": {}, + "source": [ + "## 6. Qwen3.5-4B (Bridge-only walkthrough model)\n", + "Gemma above shows the full J-lens workflow on a model that also exists on\n", + "HookedTransformer. This section checks a second case:\n", + "1. **Bridge-only** — Qwen3.5 has no `HookedTransformer.from_pretrained` path; we can load it with `TransformerBridge.boot_transformers(\"Qwen/Qwen3.5-4B\")`.\n", + "2. **Official walkthrough parity** — same model, n=1000 Hub artifact, and\n", + " two-hop prompt as Anthropic's [`jacobian-lens walkthrough`](https://github.com/anthropics/jacobian-lens/blob/main/walkthrough.ipynb).\n", + "3. **Hybrid architecture** — full attention + GatedDeltaNet; J-lens only needs\n", + " residual `blocks.{i}.hook_out`." + ] + }, + { + "cell_type": "markdown", + "id": "47f23c87", + "metadata": {}, + "source": [ + "### Cleanup\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "b91cb43f", + "metadata": {}, + "outputs": [], + "source": [ + "# NBVAL_IGNORE_OUTPUT\n", + "import gc\n", + "\n", + "# Clear memory from gemma-2-2b experiments before booting Qwen\n", + "lens.clear_device_cache()\n", + "for _name in [\n", + " \"result_jlens\", \"result_logitlens\", \"lens\", \"model\", \"swap\", \"steer\",\n", + " \"evaluated_trials\", \"condition_rows\", \"overall\", \"conditions\",\n", + " \"report_table\", \"france_to_china\", \"pair_values\",\n", + "]:\n", + " if _name in globals():\n", + " del globals()[_name]\n", + "gc.collect()\n", + "if torch.cuda.is_available():\n", + " torch.cuda.empty_cache()\n" + ] + }, + { + "cell_type": "markdown", + "id": "d8c80798", + "metadata": {}, + "source": [ + "### Boot Qwen3.5-4B through TransformerBridge\n", + "\n", + "Use the same `device` and `model_dtype` as above. J-lens only needs residual\n", + "`blocks.{i}.hook_out` on every layer (full-attention and GatedDeltaNet alike).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "901ed8f5", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "126582e447b24107b68a0cc34417d03b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading weights: 0%| | 0/723 [00:005} | {'J-lens top-1':<20} | logit lens top-1\")\n", + "for layer in QWEN_LAYERS:\n", + " print(f\"{layer:>5} | {top_j[layer][-1][0]!r:<20} | {top_l[layer][-1][0]!r}\")\n", + "\n", + "print(\"model output:\", top_j[model.cfg.n_layers - 1][-1][0])\n" + ] + }, + { + "cell_type": "markdown", + "id": "00df913e", + "metadata": {}, + "source": [ + "## 7. Fitting your own lens\n", + "\n", + "For models without a published artifact, `JacobianLens.fit` reproduces the reference estimator on\n", + "a raw `TransformerBridge` — one forward and `ceil(d_model / dim_batch)` backward passes per\n", + "prompt, deterministic. Quality saturates quickly: ~100 prompts of 128 tokens is usable (the\n", + "published lenses use up to 1000). Fits parallelize across prompt slices and combine exactly with\n", + "`merge`:\n", + "\n", + "```python\n", + "from datasets import load_dataset\n", + "\n", + "texts = load_dataset(\"Salesforce/wikitext\", \"wikitext-103-raw-v1\", split=\"train\", streaming=True)\n", + "prompts = [row[\"text\"] for row in texts.take(500) if len(row[\"text\"]) > 600][:100]\n", + "\n", + "lens = JacobianLens.fit(\n", + " model, prompts, corpus=\"your-corpus-id\", dim_batch=16, max_seq_len=128\n", + ")\n", + "lens.save(\"gemma-2-2b_jlens.pt\") # official artifact format (+ provenance metadata)\n", + "# ...or shard, using the same corpus id for every chunk:\n", + "# JacobianLens.merge([JacobianLens.fit(model, chunk, corpus=\"your-corpus-id\") for chunk in chunks])\n", + "```\n", + "\n", + "## References\n", + "\n", + "- Gurnee et al., [*Verbalizable Representations Form a Global Workspace in Language Models*](https://transformer-circuits.pub/2026/workspace/index.html), Transformer Circuits Thread, 2026\n", + "- Reference implementation: [`anthropics/jacobian-lens`](https://github.com/anthropics/jacobian-lens) (Apache-2.0)\n", + "- Published lenses: [`neuronpedia/jacobian-lens`](https://huggingface.co/neuronpedia/jacobian-lens) · interactive: [neuronpedia.org/jlens](https://www.neuronpedia.org/jlens)\n", + "- The logit lens: [nostalgebraist, 2020](https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": { + "0823fa85a33042f2b84a2b9926882509": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", "_view_name": "StyleView", "background": null, "description_width": "", @@ -653,23 +1764,78 @@ "text_color": null } }, - "99333fe6d4aa485bac5b5ec104cc5537": { + "0b188b73a4ca4235b673eb4a5a5134c6": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", - "model_name": "ProgressStyleModel", + "model_name": "HTMLStyleModel", "state": { "_model_module": "@jupyter-widgets/controls", "_model_module_version": "2.0.0", - "_model_name": "ProgressStyleModel", + "_model_name": "HTMLStyleModel", "_view_count": null, "_view_module": "@jupyter-widgets/base", "_view_module_version": "2.0.0", "_view_name": "StyleView", - "bar_color": null, - "description_width": "" + "background": null, + "description_width": "", + "font_size": null, + "text_color": null + } + }, + "0d7dd3809a9f40c784afa5e7f4948253": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null } }, - "aca2228a9ea14b069bc2d6a5fe9d5bcb": { + "18896d768cf049c3a4e02b1d9d350dc2": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -722,30 +1888,73 @@ "width": null } }, - "c2743d998e5841b3b35e40721586659a": { + "56418781a0e74548a54c90d31236f244": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", - "model_name": "HTMLModel", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "8e53e944f78141bdaac00c2aa4a587f7": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HBoxModel", "state": { "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "2.0.0", - "_model_name": "HTMLModel", + "_model_name": "HBoxModel", "_view_count": null, "_view_module": "@jupyter-widgets/controls", "_view_module_version": "2.0.0", - "_view_name": "HTMLView", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_ead86bce277c431591a6ebf48a1a340f", + "IPY_MODEL_a8903983473e472782a2f69182910a66", + "IPY_MODEL_e026eac248ca453fb1ef07d6c2cc12cc" + ], + "layout": "IPY_MODEL_f0f0895a5c5d4c92b860c54087a40782", + "tabbable": null, + "tooltip": null + } + }, + "a8903983473e472782a2f69182910a66": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "ProgressView", + "bar_style": "success", "description": "", "description_allow_html": false, - "layout": "IPY_MODEL_aca2228a9ea14b069bc2d6a5fe9d5bcb", - "placeholder": "​", - "style": "IPY_MODEL_969321ee62ed49049d01ab89c6724577", + "layout": "IPY_MODEL_ae81df533acb4b948ad50b1a6956cef4", + "max": 288, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_56418781a0e74548a54c90d31236f244", "tabbable": null, "tooltip": null, - "value": "Loading weights: 100%" + "value": 288 } }, - "c357c3f6dcb342fa90b16ca6949bcd3e": { + "ae81df533acb4b948ad50b1a6956cef4": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -798,25 +2007,30 @@ "width": null } }, - "e6e8ba2cc68e4d6491d62a406a3d4816": { + "e026eac248ca453fb1ef07d6c2cc12cc": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", - "model_name": "HTMLStyleModel", + "model_name": "HTMLModel", "state": { + "_dom_classes": [], "_model_module": "@jupyter-widgets/controls", "_model_module_version": "2.0.0", - "_model_name": "HTMLStyleModel", + "_model_name": "HTMLModel", "_view_count": null, - "_view_module": "@jupyter-widgets/base", + "_view_module": "@jupyter-widgets/controls", "_view_module_version": "2.0.0", - "_view_name": "StyleView", - "background": null, - "description_width": "", - "font_size": null, - "text_color": null + "_view_name": "HTMLView", + "description": "", + "description_allow_html": false, + "layout": "IPY_MODEL_0d7dd3809a9f40c784afa5e7f4948253", + "placeholder": "​", + "style": "IPY_MODEL_0823fa85a33042f2b84a2b9926882509", + "tabbable": null, + "tooltip": null, + "value": " 288/288 [00:26<00:00, 17.29it/s]" } }, - "ef3400c12a834c5db3f95519c0b9e78f": { + "ead86bce277c431591a6ebf48a1a340f": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLModel", @@ -831,15 +2045,15 @@ "_view_name": "HTMLView", "description": "", "description_allow_html": false, - "layout": "IPY_MODEL_f3617aa9615142548cfdcf87b0aaccda", + "layout": "IPY_MODEL_18896d768cf049c3a4e02b1d9d350dc2", "placeholder": "​", - "style": "IPY_MODEL_e6e8ba2cc68e4d6491d62a406a3d4816", + "style": "IPY_MODEL_0b188b73a4ca4235b673eb4a5a5134c6", "tabbable": null, "tooltip": null, - "value": " 288/288 [00:01<00:00, 125.60it/s]" + "value": "Loading weights: 100%" } }, - "f3617aa9615142548cfdcf87b0aaccda": { + "f0f0895a5c5d4c92b860c54087a40782": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", diff --git a/demos/No_Position_Experiment.ipynb b/demos/No_Position_Experiment.ipynb index 979c99e422..c517bf71cd 100644 --- a/demos/No_Position_Experiment.ipynb +++ b/demos/No_Position_Experiment.ipynb @@ -144,7 +144,18 @@ "cell_type": "code", "execution_count": 35, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DeprecationWarning:\n", + "\n", + "HookedTransformer is deprecated and will be removed in 4.0. Use TransformerBridge.boot_transformers(...) instead, then call enable_compatibility_mode() for HookedTransformer-equivalent numerics.\n", + "\n" + ] + } + ], "source": [ "cfg = HookedTransformerConfig(\n", " n_layers=2,\n", diff --git a/demos/Othello_GPT.ipynb b/demos/Othello_GPT.ipynb index 171b877f28..42d381c9aa 100644 --- a/demos/Othello_GPT.ipynb +++ b/demos/Othello_GPT.ipynb @@ -279,7 +279,18 @@ "cell_type": "code", "execution_count": 56, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DeprecationWarning:\n", + "\n", + "HookedTransformer is deprecated and will be removed in 4.0. Use TransformerBridge.boot_transformers(...) instead, then call enable_compatibility_mode() for HookedTransformer-equivalent numerics.\n", + "\n" + ] + } + ], "source": [ "import transformer_lens.utilities as utils\n", "\n", diff --git a/demos/doc_sanitize.cfg b/demos/doc_sanitize.cfg index 445e71ae7c..fd4388894d 100644 --- a/demos/doc_sanitize.cfg +++ b/demos/doc_sanitize.cfg @@ -33,3 +33,7 @@ replace: \1 [regex6d] regex: (0\.[1-9]\d{2})\d+ 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: diff --git a/docs/source/content/jacobian_lens_fitting.md b/docs/source/content/jacobian_lens_fitting.md new file mode 100644 index 0000000000..c0ad591418 --- /dev/null +++ b/docs/source/content/jacobian_lens_fitting.md @@ -0,0 +1,324 @@ +# Fitting a Jacobian lens + +A [Jacobian lens](https://github.com/TransformerLensOrg/TransformerLens/blob/dev/demos/Jacobian_Lens_Demo.ipynb) +maps each intermediate residual stream to the final residual stream. Use a published +artifact when one exists: + +```python +from transformer_lens.tools.analysis import JacobianLens + +lens = JacobianLens.from_pretrained("gemma-2-2b") +``` + +Fit a new lens when your model is not in the artifact registry or when you need a +different prompt distribution. Fitting is deterministic for a fixed model, prompt +manifest, and set of estimator options. + +## Requirements + +`JacobianLens.fit` requires a freshly booted, causal decoder-only +`TransformerBridge` with raw Hugging Face weights: + +```python +import torch + +from transformer_lens import TransformerBridge + +model = TransformerBridge.boot_transformers( + "openai-community/gpt2", + device="cuda", + dtype=torch.float32, + revision="YOUR_MODEL_COMMIT_SHA", +) +``` + +Do not enable compatibility mode or process the model weights. Pin a model revision +for a reproducible fit. The resolved revision, model name, dtype, TransformerLens +version, corpus identifier, and estimator options are recorded in the artifact. + +Fitting one prompt requires one forward pass and +`ceil(d_model / dim_batch)` backward passes. Increasing `dim_batch` is faster but +replicates the prompt that many times in memory. Start with `dim_batch=8` and lower +it after an out-of-memory error. Float32 gives the highest-fidelity estimator; +bfloat16 and float16 use less memory but emit a reduced-precision warning. + +## Prepare a prompt manifest + +Store the exact fitting texts in a versioned JSON Lines file, one object per line: + +```json +{"text": "The first sufficiently long document..."} +{"text": "The second sufficiently long document..."} +``` + +Keep the order fixed and give the corpus a stable identifier containing the dataset +and preprocessing revision, for example +`wikitext-103-raw-v1@2.0.0:train:min-600-chars`. The `corpus` argument is provenance; +it does not load or transform the texts. + +Around 100 prompts of 128 tokens gives a usable first fit, while published lenses +use up to 1,000 prompts. Very short prompts are skipped because the estimator +excludes the first few positions and the final position. Review warnings after each +run and keep the same manifest for every shard. + +## Fit one shard + +Save this as `fit_jacobian_lens_shard.py`: + +```python +import argparse +import hashlib +import json +from pathlib import Path + +import torch + +from transformer_lens import TransformerBridge +from transformer_lens.tools.analysis import JacobianLens + + +parser = argparse.ArgumentParser() +parser.add_argument("--model", required=True) +parser.add_argument("--revision", required=True) +parser.add_argument("--prompts", type=Path, required=True) +parser.add_argument("--corpus", required=True) +parser.add_argument("--output", type=Path, required=True) +parser.add_argument("--shard-index", type=int, required=True) +parser.add_argument("--num-shards", type=int, required=True) +parser.add_argument("--device", default="cuda") +parser.add_argument("--dim-batch", type=int, default=8) +parser.add_argument("--max-seq-len", type=int, default=128) +args = parser.parse_args() + +if args.num_shards < 1: + parser.error("--num-shards must be at least 1") +if not 0 <= args.shard_index < args.num_shards: + parser.error("--shard-index must be in [0, num-shards)") + +manifest_bytes = args.prompts.read_bytes() +records = [ + json.loads(line) + for line in manifest_bytes.decode("utf-8").splitlines() + if line.strip() +] +prompts = [record["text"] for record in records] +shard_prompts = prompts[args.shard_index :: args.num_shards] +if not shard_prompts: + parser.error("this shard has no prompts") + +model = TransformerBridge.boot_transformers( + args.model, + revision=args.revision, + device=args.device, + dtype=torch.float32, +) +lens = JacobianLens.fit( + model, + shard_prompts, + corpus=args.corpus, + dim_batch=args.dim_batch, + max_seq_len=args.max_seq_len, + metadata={ + "prompt_manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(), + "num_shards": args.num_shards, + }, +) +args.output.parent.mkdir(parents=True, exist_ok=True) +lens.save(str(args.output)) +``` + +Every worker must use the same model revision, manifest, corpus identifier, +`num_shards`, TransformerLens version, dtype, and estimator options. Only +`shard-index`, device, and output path should differ: + +```bash +uv run python fit_jacobian_lens_shard.py \ + --model openai-community/gpt2 \ + --revision YOUR_MODEL_COMMIT_SHA \ + --prompts prompts.jsonl \ + --corpus wikitext-103-raw-v1@2.0.0:train:min-600-chars \ + --num-shards 8 \ + --shard-index 0 \ + --output fits/shard-00.pt +``` + +Run indices `0` through `7` on separate processes or machines. Do not run multiple +workers on one GPU unless their combined model and fitting batches fit in memory. +Round-robin slicing means the union of all shards contains every manifest row +exactly once. + +Do not add shard-specific values such as the shard index or hostname to `metadata`. +`JacobianLens.merge` rejects mismatched provenance so it cannot silently combine +different fits. Put worker-specific information in logs or output filenames instead. + +## Merge the shards + +Copy all shard artifacts to one machine, then merge them: + +```python +from pathlib import Path + +from transformer_lens.tools.analysis import JacobianLens + +paths = sorted(Path("fits").glob("shard-*.pt")) +if len(paths) != 8: + raise RuntimeError(f"expected 8 shards, found {len(paths)}") + +shards = [JacobianLens.load(str(path)) for path in paths] +lens = JacobianLens.merge(shards) +lens.save("gpt2_jacobian_lens.pt") + +print(f"merged {len(paths)} shards and {lens.n_prompts} accepted prompts") +``` + +The merge is an exact prompt-count-weighted average of the shard matrices. It +requires matching source layers, model width, and provenance apart from +`n_prompts`. A mismatch is normally evidence that workers used different inputs, +versions, or fitting options; fix and rerun the inconsistent shard rather than +editing its metadata. + +## Validate and publish + +Load the saved artifact, validate it against a fresh bridge, and run a readout before +publishing: + +```python +import torch + +from transformer_lens import TransformerBridge +from transformer_lens.tools.analysis import JacobianLens + +model = TransformerBridge.boot_transformers( + "openai-community/gpt2", + device="cuda", + dtype=torch.float32, + revision="YOUR_MODEL_COMMIT_SHA", +) +lens = JacobianLens.load("gpt2_jacobian_lens.pt").validate_model(model) + +assert lens.n_prompts > 0 +assert all(torch.isfinite(matrix).all() for matrix in lens.jacobians.values()) +readout = lens.readout(model, "The capital of France is", top_k=10) +print(readout) +``` + +Record the manifest hash, resolved model revision, fitting command, shard count, and +accepted prompt count with the artifact. Upload the `.pt` file to a Hugging Face +model repository: + +```python +from huggingface_hub import HfApi + +api = HfApi() +api.create_repo("your-org/your-jacobian-lenses", repo_type="model", exist_ok=True) +api.upload_file( + repo_id="your-org/your-jacobian-lenses", + repo_type="model", + path_or_fileobj="gpt2_jacobian_lens.pt", + path_in_repo="gpt2_jacobian_lens.pt", +) +``` + +Consumers can then load and validate it directly: + +```python +lens = JacobianLens.from_pretrained( + "your-org/your-jacobian-lenses", + filename="gpt2_jacobian_lens.pt", + model=model, +) +``` + +To propose a short-name entry in TransformerLens, open a pull request that adds the +published file to `transformer_lens/tools/analysis/jacobian_lens_registry.json` and +include the fitting provenance and validation results. + +## Importing an existing lens + +`JacobianLens.load()` accepts two file schemas: the standard artifact format (four +official keys — `J`, `n_prompts`, `source_layers`, `d_model`) written by +`JacobianLens.save()` and compatible with the Anthropic reference package, and the +fit-checkpoint format described below. + +### Fit checkpoint format + +A fit checkpoint holds the running Jacobian sums from an interrupted or staged fit, +before the final per-prompt average is applied. `JacobianLens.load()` detects a +checkpoint by the presence of a `jacobian_sum` key and reconstructs the per-layer +means automatically: + +```python +lens = JacobianLens.load("path/to/checkpoint.pt") +``` + +The reference implementation (`anthropics/jacobian-lens`) writes exactly six +top-level keys. `load()` also accepts optional flat-provenance keys from alternative +writers. The full set of recognised keys is: + +| Key | Type | Required | Description | +|---|---|---|---| +| `jacobian_sum` | `dict[int, Tensor[d, d]]` | yes | Running sum of per-prompt Jacobians (float32) | +| `n_done` | `int` | yes | Number of prompts accumulated into the sums (must be > 0) | +| `next_idx` | `int` | no | Index of the next prompt to process (informational; not used by `load()`) | +| `source_layers` | `list[int]` | no | Documented layer indices (informational only) | +| `target_layer` | `int` | no | Target layer fitted against — harvested into `metadata` so `validate_model()` can check it | +| `skip_first` | `int` | no | Leading positions excluded from the source average (informational; not used by `load()`) | +| `model_name` | `str` | no | Flat provenance shortcut (harvested into `metadata`) | +| `model_revision` | `str` | no | Flat provenance shortcut (harvested into `metadata`) | +| `corpus` | `str` | no | Flat provenance shortcut (harvested into `metadata`) | +| `metadata` | `dict[str, ...]` | no | Nested provenance from alternative writers (scalars, lists, string-keyed dicts) | + +`d_model` is **not** read from the payload; it is inferred from the shape of the +first matrix in `jacobian_sum`. + +`load()` always sets `converted_from: "jacobian_lens_checkpoint"` in the resulting +lens's metadata. This sentinel prevents `merge()` from silently combining a +converted checkpoint with a natively TL-fitted lens, since the two provenance +dictionaries will differ. To merge shards that were all loaded from checkpoints, they +must otherwise share identical provenance (same `model_name`, `corpus`, etc.): + +```python +shards = [JacobianLens.load(str(p)) for p in checkpoint_paths] +# All shards get converted_from="jacobian_lens_checkpoint"; +# merge() requires identical provenance apart from n_prompts. +merged = JacobianLens.merge(shards) +merged.save("merged_lens.pt") +``` + +### Metadata handling + +Three things are guaranteed when loading a checkpoint: + +**Fit-reserved keys are stripped.** Keys written by `JacobianLens.fit()` to record +TransformerLens internals (`transformer_lens_fit`, `transformer_lens_version`, +`model_system`, `hook_convention`, etc.) are not carried over, since they describe +the *native* fitting pipeline and would be wrong for an imported lens. +`target_layer` is a deliberate exception: it is preserved in the resulting metadata +so `validate_model()` can detect and reject checkpoints fitted against a non-final +target layer. + +**Tensor-valued fields are dropped.** `JacobianLens.save()` uses +`weights_only=True` for reload safety, which restricts metadata to plain Python +scalars, lists, and string-keyed dicts. Tensor-valued fields from the source +checkpoint would silently corrupt a future load, so they are dropped and their names +are recorded in a `dropped_fields` list in the lens's metadata: + +```python +lens = JacobianLens.load("checkpoint_with_tensors.pt") +if "dropped_fields" in lens.metadata: + print("fields not carried over:", lens.metadata["dropped_fields"]) +``` + +**Dtype is not preserved.** `JacobianLens.__init__` casts all matrices to float32 +and `save()` defaults to float16, so the source checkpoint's storage dtype cannot +be preserved end-to-end. Value precision is preserved within those two conversions. + +### Note on tuned-lens + +Tuned-lens checkpoints are not currently supported. Tuned-lens translators are +affine (weight + bias), but the Jacobian artifact format has no bias slot to +receive the translation component; importing would therefore silently drop the +bias and produce wrong-but-plausible readouts. Layer indexing also differs +(input-to-layer-ℓ vs. output-of-block-ℓ), which would yield misaligned +readouts if not corrected. These issues are deferred; support can be added in a +follow-up once a lossless mapping is established. diff --git a/docs/source/index.md b/docs/source/index.md index 72b90c4b0a..cb1f196926 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -57,6 +57,7 @@ content/contributing content/hook_system content/compatibility_mode content/ssm_interpretability +content/jacobian_lens_fitting content/debugging_numerical_divergence generated/demos/Main_Demo generated/demos/Exploratory_Analysis_Demo diff --git a/tests/QUARANTINES.md b/tests/QUARANTINES.md index e9402fd9a7..edf16fa89b 100644 --- a/tests/QUARANTINES.md +++ b/tests/QUARANTINES.md @@ -59,6 +59,7 @@ Big-model adapter tests use `@pytest.mark.slow`, CI tier filters `-m "not slow"` | [`integration/model_bridge/test_pegasus_adapter.py`:16](integration/model_bridge/test_pegasus_adapter.py) | Pegasus-XSum 568M; distilled variants are asymmetric | | [`integration/model_bridge/test_exaone_adapter.py`:18](integration/model_bridge/test_exaone_adapter.py) | EXAONE 2.4B; no working tiny mirror (hyper-accel/tiny-random-exaone ships stale remote code) | | [`integration/model_bridge/test_ouro_adapter.py`:35](integration/model_bridge/test_ouro_adapter.py) (skipif + `slow`) | ByteDance/Ouro-1.4B: 2.8GB download + ~11GB RAM | +| [`integration/test_jacobian_lens_kurtosis.py`](integration/test_jacobian_lens_kurtosis.py) | Qwen3.5-0.8B (~1.6GB) + lens artifacts + wikitext validation split; CPU-feasible (#1539 Tier-1) | **Un-skip:** locally with `HF_TOKEN` sourced (slow-marked files: run the file directly or `-m slow`). diff --git a/tests/acceptance/model_bridge/compatibility/test_legacy_hooked_transformer_coverage.py b/tests/acceptance/model_bridge/compatibility/test_legacy_hooked_transformer_coverage.py index c8e79c7b2c..90e31e46c0 100644 --- a/tests/acceptance/model_bridge/compatibility/test_legacy_hooked_transformer_coverage.py +++ b/tests/acceptance/model_bridge/compatibility/test_legacy_hooked_transformer_coverage.py @@ -166,17 +166,18 @@ def test_weight_access_functionality(self, bridge_model): pass # Weight access might not be implemented yet # Test MLP weights - try: - w_in = bridge_model.W_in - w_out = bridge_model.W_out - - assert w_in.shape[0] == bridge_model.cfg.n_layers - assert w_out.shape[0] == bridge_model.cfg.n_layers - - weight_tests.append("mlp_weights") - - except AttributeError: - pass # Weight access might not be implemented yet + w_in = bridge_model.W_in + w_out = bridge_model.W_out + + assert w_in.shape[0] == bridge_model.cfg.n_layers + assert w_in.shape[1] == bridge_model.cfg.d_model + assert w_in.shape[2] == bridge_model.cfg.d_mlp + assert w_out.shape[0] == bridge_model.cfg.n_layers + assert w_out.shape[1] == bridge_model.cfg.d_mlp + assert w_out.shape[2] == bridge_model.cfg.d_model + assert w_in.shape[2] == w_out.shape[1], "W_in and W_out inner dimensions must match" + + weight_tests.append("mlp_weights") # At least some weight access should work eventually if len(weight_tests) > 0: diff --git a/tests/integration/model_bridge/test_ast_tiny.py b/tests/integration/model_bridge/test_ast_tiny.py new file mode 100644 index 0000000000..906428babd --- /dev/null +++ b/tests/integration/model_bridge/test_ast_tiny.py @@ -0,0 +1,66 @@ +"""Integration parity tests for AST architecture""" + +import pytest +import torch +from transformers import ASTConfig, ASTForAudioClassification + +from transformer_lens.model_bridge import TransformerBridge + + +@pytest.fixture(autouse=True) +def prevent_internet_processor_download(monkeypatch): + """Prevent V3 boot from attempting to download a feature extractor.""" + monkeypatch.setattr( + "transformers.AutoFeatureExtractor.from_pretrained", lambda *args, **kwargs: None + ) + + +@pytest.fixture(scope="module") +def hf_config(): + """Setup a tiny HF AST config for instantaneous local testing.""" + cfg = ASTConfig( + hidden_size=12, + num_hidden_layers=2, + num_attention_heads=2, + intermediate_size=24, + patch_size=16, + num_labels=2, + ) + cfg.architectures = ["ASTForAudioClassification"] + return cfg + + +class TestASTParity: + """FP32 numerical parity assertion.""" + + def test_forward_pass_parity(self, hf_config): + hf_model = ASTForAudioClassification(hf_config) + hf_model.eval() + + # boot the V3 transformerbridge natively + tl_model = TransformerBridge.boot_transformers("ast", hf_model=hf_model) + tl_model.eval() + + # 3. create dummy spectrogram input [batch, freq, time] + dummy_spectrogram = torch.randn(1, 1024, 128) + + # 4. compare forward passes + with torch.no_grad(): + hf_logits = hf_model(dummy_spectrogram).logits + + # bridge forward pass (extract sequence before HF's pooling) + _, cache = tl_model.run_with_cache(dummy_spectrogram) + resid = cache["ln_final.hook_normalized"] + + # apply AST pooling logic to the bridges residual stream + cls_token = resid[:, 0, :] + dist_token = resid[:, 1, :] + pooled_out = (cls_token + dist_token) / 2.0 + + # apply the final classifier (which holds 2nd layernorm + dense) + tl_logits = hf_model.classifier(pooled_out) + + diff = (hf_logits - tl_logits).abs().max().item() + + # 5. assert parity + assert diff < 1e-4, "Parity failed: Bridge tensors do not match HuggingFace." diff --git a/tests/integration/model_bridge/test_gpt_oss_tiny.py b/tests/integration/model_bridge/test_gpt_oss_tiny.py new file mode 100644 index 0000000000..bdf23d3c07 --- /dev/null +++ b/tests/integration/model_bridge/test_gpt_oss_tiny.py @@ -0,0 +1,125 @@ +"""Tiny-random GPT-OSS bridge-vs-HF parity (issue #1618). + +GPT-OSS's eager attention folds a learned per-head sink logit into the softmax; +the bridge's reconstructed attention must carry it or every layer's pattern is +rescaled. A tiny unquantized ``GptOssForCausalLM`` exercises the same code path +CPU-fast — sinks exist independent of MXFP4 quantization. +""" + +import pytest +import torch +from transformers import ( + AutoModelForCausalLM, + AutoTokenizer, + GptOssConfig, + GptOssForCausalLM, +) + +from transformer_lens.model_bridge.bridge import TransformerBridge + +# Ids stay below the tiny vocab_size=256. +TOKENS = torch.tensor([[5, 17, 29, 3, 11, 42, 7, 23]]) +FP32_TOL = 1e-4 +N_LAYERS = 2 + + +def _tiny_config() -> GptOssConfig: + return GptOssConfig( + hidden_size=64, + num_hidden_layers=N_LAYERS, # default layer_types: one sliding, one full + num_attention_heads=4, + num_key_value_heads=2, + head_dim=16, + intermediate_size=32, + num_local_experts=4, + num_experts_per_tok=2, + vocab_size=256, + max_position_embeddings=128, + sliding_window=4, # < seq len so the sliding-window mask actually bites + rope_parameters={"rope_type": "default", "rope_theta": 150000.0}, + ) + + +@pytest.fixture(scope="module") +def model_dir(tmp_path_factory): + torch.manual_seed(0) + model = GptOssForCausalLM(_tiny_config()) + path = tmp_path_factory.mktemp("tiny_gpt_oss") + model.save_pretrained(path) + AutoTokenizer.from_pretrained("gpt2").save_pretrained(path) + return str(path) + + +@pytest.fixture(scope="module") +def hf_eager(model_dir): + """Reference model loaded independently of the bridge's wrapped instance.""" + return AutoModelForCausalLM.from_pretrained( + model_dir, dtype=torch.float32, attn_implementation="eager" + ).eval() + + +@pytest.fixture(scope="module") +def bridge(model_dir): + return TransformerBridge.boot_transformers(model_dir, device="cpu", dtype=torch.float32) + + +def test_random_init_gives_nonzero_sinks(hf_eager): + """Guard: zero sinks would make the parity tests vacuously pass without them.""" + sinks = hf_eager.model.layers[0].self_attn.sinks + assert sinks.abs().max() > 0 + + +def test_logits_match_hf_eager(bridge, hf_eager): + with torch.inference_mode(): + bridge_logits = bridge(TOKENS) + hf_logits = hf_eager(TOKENS).logits + max_diff = (bridge_logits - hf_logits).abs().max().item() + assert max_diff < FP32_TOL, ( + f"bridge vs HF eager logit drift={max_diff:.2e} exceeds {FP32_TOL:.0e} — " + "reconstructed attention diverges (attention sinks? see #1618)" + ) + + +def test_resid_post_matches_hf_layer_outputs(bridge, hf_eager): + """Per-layer parity catches compensating errors that wash out at the logits. + + Compares against forward hooks on the HF decoder layers — NOT + ``output_hidden_states``, whose final entry is post-``model.norm`` and so + never matches the last block's resid_post. + """ + hf_layer_out: dict[int, torch.Tensor] = {} + + def _make_hook(idx): + def _hook(_module, _inputs, output): + hf_layer_out[idx] = (output[0] if isinstance(output, tuple) else output).detach() + + return _hook + + handles = [ + layer.register_forward_hook(_make_hook(i)) for i, layer in enumerate(hf_eager.model.layers) + ] + try: + with torch.inference_mode(): + hf_eager(TOKENS) + finally: + for handle in handles: + handle.remove() + + with torch.inference_mode(): + _, cache = bridge.run_with_cache( + TOKENS, names_filter=lambda n: n.endswith("hook_resid_post") + ) + for layer in range(N_LAYERS): + diff = (hf_layer_out[layer] - cache[f"blocks.{layer}.hook_resid_post"]).abs().max() + assert diff < FP32_TOL, f"layer {layer} resid_post drift={diff:.2e}" + + +def test_pattern_rows_leave_mass_for_the_sink(bridge): + """hook_pattern rows must sum to < 1: the dropped sink column keeps its share.""" + with torch.inference_mode(): + _, cache = bridge.run_with_cache( + TOKENS, names_filter=lambda n: n.endswith("attn.hook_pattern") + ) + for layer in range(N_LAYERS): + row_sums = cache[f"blocks.{layer}.attn.hook_pattern"].sum(dim=-1) + assert (row_sums < 1.0).all(), f"layer {layer}: a pattern row ignored the sink's share" diff --git a/tests/integration/model_bridge/test_lfm2_tiny.py b/tests/integration/model_bridge/test_lfm2_tiny.py new file mode 100644 index 0000000000..b6416d0edd --- /dev/null +++ b/tests/integration/model_bridge/test_lfm2_tiny.py @@ -0,0 +1,115 @@ +"""Tiny from_config Lfm2 integration tests — CI coverage without a real checkpoint. + +The full LiquidAI/LFM2.5-230M parity suite (test_lfm2_adapter.py) is +``@pytest.mark.slow`` because it takes excessive time in CI for model downloads. This file +builds a tiny synthetic Lfm2 (real random CPU weights, no Hub download) that +exercises the same surfaces — wiring, bridge vs HF forward checks and hook coverage. +""" +import pytest +import torch +from transformers import AutoModelForCausalLM +from transformers.models.lfm2 import Lfm2Config + +from transformer_lens.model_bridge.bridge import TransformerBridge +from transformer_lens.model_bridge.generalized_components import ( + BlockBridge, + DepthwiseConv1DBridge, + Lfm2ShortConvBridge, + PositionEmbeddingsAttentionBridge, +) +from transformer_lens.model_bridge.sources._bridge_builder import ( + build_bridge_config_from_hf, +) +from transformer_lens.model_bridge.supported_architectures.lfm2 import ( + Lfm2ArchitectureAdapter, +) + +LAYERS = ["conv", "full_attention"] +CONV_LAYER = [0] +ATTN_LAYER = [1] + + +class _Tok: + pass + + +@pytest.fixture(scope="module") +def bridge(): + torch.manual_seed(0) + cfg = Lfm2Config( + vocab_size=256, + hidden_size=64, + num_hidden_layers=2, + layer_types=LAYERS, + num_attention_heads=4, + num_key_value_heads=2, + intermediate_size=128, + ) + cfg.architectures = ["Lfm2ForCausalLM"] + hf = AutoModelForCausalLM.from_config(cfg).to(torch.float32).eval() + bridge_cfg = build_bridge_config_from_hf( + hf.config, "Lfm2ForCausalLM", "lfm2-tiny", torch.float32 + ) + return TransformerBridge(hf, Lfm2ArchitectureAdapter(bridge_cfg), tokenizer=_Tok()) + + +@pytest.fixture(scope="module") +def tokens(): + return torch.tensor([[1, 2, 3, 4, 5]]) + + +@pytest.fixture(scope="module") +def cache(bridge, tokens): + with torch.no_grad(): + _, c = bridge.run_with_cache(tokens) + return c + + +class TestLfm2TinyStructure: + def test_block_count(self, bridge): + assert len(bridge.blocks) == len(LAYERS) + + def test_blocks_are_block_bridge(self, bridge): + assert isinstance(bridge.blocks[0], BlockBridge) + + def test_conv_block_is_lfm2_short_conv_bridge(self, bridge): + assert isinstance(bridge.blocks[0].conv, Lfm2ShortConvBridge) + + def test_conv_conv_block_is_depthwise_conv1d_bridge(self, bridge): + assert isinstance(bridge.blocks[0].conv.conv, DepthwiseConv1DBridge) + + def test_attention_block_is_position_embeddings_attention_bridge(self, bridge): + assert isinstance(bridge.blocks[1].attn, PositionEmbeddingsAttentionBridge) + + def test_layer_types_populated(self, bridge): + assert getattr(bridge.cfg, "layer_types", None) == LAYERS + + +class TestLfm2TinyForwardPass: + def test_forward_matches_hf_exactly(self, bridge, tokens): + with torch.no_grad(): + bridge_out = bridge(tokens) + hf_out = bridge.original_model(tokens).logits + assert (bridge_out.float() - hf_out.float()).abs().max().item() == 0.0 + + def test_no_nan_longer_sequence(self, bridge): + with torch.no_grad(): + out = bridge(torch.arange(1, 17).unsqueeze(0)) + assert not torch.isnan(out).any() + + +class TestLfm2TinyHookCoverage: + def test_block_hooks_fire(self, cache): + for i in range(len(LAYERS)): + assert f"blocks.{i}.hook_in" in cache + assert f"blocks.{i}.hook_out" in cache + + def test_conv_submodule_hooks_fire(self, cache): + for i in CONV_LAYER: + for submod in ("in", "conv", "out"): + assert f"blocks.{i}.conv.{submod}.hook_out" in cache + + def test_attn_submodule_hooks_fire(self, cache): + for i in ATTN_LAYER: + for submod in ("q", "k", "v", "o", "q_norm", "k_norm"): + assert f"blocks.{i}.attn.{submod}.hook_out" in cache diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py new file mode 100644 index 0000000000..8329284f8b --- /dev/null +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -0,0 +1,386 @@ +"""Integration tests for the ViT / DeiT architecture adapter. + +Modeled on tests/integration/model_bridge/test_mamba_adapter.py — verifies +wrap-don't-reimplement behavior against real HF checkpoints: +- Forward pass matches HF (bridge substitutes hooked submodules into the real + HF model and calls its real forward — see bridge.py's `is_visual_model` + branch, which does `output = self.original_model(**kwargs)`) +- Submodule hooks fire with expected shapes (embed, attn q/k/v/o, mlp in/out, + ln_final, unembed/classifier) +- DeiT's distillation token is invisible to the adapter (sequence length + differs from ViT's by one extra token, entirely inside VisionEmbeddingsBridge) +- Bare ViTModel (no classifier) return-value shape via return_type="logits" +- DeiTForImageClassificationWithTeacher is rejected, not silently mishandled +- prepare_model() doesn't assume a `.encoder` wrapper exists on the HF model + (transformers now puts blocks directly on `.layers`) + +Four checkpoints, matching the ones already sanity-checked manually (added +facebook/deit-small-patch16-224 as a real DeiTForImageClassification load — +previously declared as MODEL_DEIT_CLASSIFIER but never actually used): +- google/vit-base-patch16-224 (ViTForImageClassification, prefix "vit.") +- google/vit-base-patch16-224-in21k (bare ViTModel, no prefix, no classifier) +- facebook/deit-small-patch16-224 (DeiTForImageClassification, prefix "deit.") +- facebook/deit-small-distilled-patch16-224 (bare DeiTModel, distillation token) + +NOTE ON RUNNING THESE: they download real weights from HuggingFace (a few +hundred MB total) — same cost class as the existing Mamba/SmolLM3 integration +tests, no special marker needed, but expect the first run to be slow. +""" + +from types import SimpleNamespace + +import pytest +import torch +from transformers import ( + DeiTForImageClassification, + DeiTModel, + ViTForImageClassification, + ViTModel, +) + +from transformer_lens.model_bridge.bridge import TransformerBridge +from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( + VisionClassifierHeadBridge, +) + +MODEL_VIT_CLASSIFIER = "google/vit-base-patch16-224" +MODEL_VIT_BARE = "google/vit-base-patch16-224-in21k" +MODEL_DEIT_CLASSIFIER = "facebook/deit-small-patch16-224" + + +def _pixel_values(batch: int = 1, image_size: int = 224) -> torch.Tensor: + return torch.randn(batch, 3, image_size, image_size) + + +# --------------------------------------------------------------------------- +# ViTForImageClassification (the "normal" case) +# --------------------------------------------------------------------------- +@pytest.fixture(scope="module") +def vit_bridge(): + # Explicitly load the classification model to bypass boot_transformers auto-loader logic + hf_model = ViTForImageClassification.from_pretrained(MODEL_VIT_CLASSIFIER) + return TransformerBridge.boot_transformers( + MODEL_VIT_CLASSIFIER, hf_model=hf_model, device="cpu" + ) + + +class TestViTClassifierBridgeCreation: + def test_block_count(self, vit_bridge): + assert len(vit_bridge.blocks) == vit_bridge.original_model.config.num_hidden_layers + + def test_config_flags(self, vit_bridge): + assert vit_bridge.cfg.normalization_type == "LN" + assert vit_bridge.cfg.positional_embedding_type == "standard" + assert vit_bridge.cfg.is_visual_model is True + assert vit_bridge.cfg.gated_mlp is False + assert vit_bridge.cfg.attn_only is False + + def test_has_core_components(self, vit_bridge): + assert hasattr(vit_bridge, "embed") + assert hasattr(vit_bridge, "blocks") + assert hasattr(vit_bridge, "ln_final") + assert hasattr(vit_bridge, "unembed") + + def test_unembed_is_vision_classifier_head(self, vit_bridge): + assert isinstance(vit_bridge.unembed, VisionClassifierHeadBridge) + + def test_d_model_matches_hf_config(self, vit_bridge): + assert vit_bridge.cfg.d_model == vit_bridge.original_model.config.hidden_size + + def test_n_heads_matches_hf_config(self, vit_bridge): + assert vit_bridge.cfg.n_heads == vit_bridge.original_model.config.num_attention_heads + + def test_blocks_point_at_flat_layers_attribute(self, vit_bridge): + """Current transformers has no ViTEncoder wrapper any more — blocks + live directly on `vit.layers`, not `vit.encoder.layer`.""" + assert vit_bridge.adapter.component_mapping["blocks"].name == "vit.layers" + + def test_n_ctx_matches_real_patch_grid(self, vit_bridge): + """Regression check for the previously-broken n_ctx propagation: for + google/vit-base-patch16-224 (224px image, patch16 -> 14x14 patches), + cfg.n_ctx should be patches + CLS token = 197, not the generic + HF-config-fallback default of 2048.""" + assert vit_bridge.cfg.n_ctx == 197 + + +class TestViTClassifierForwardPass: + def test_forward_returns_correct_shape(self, vit_bridge): + pixel_values = _pixel_values() + with torch.no_grad(): + output = vit_bridge(pixel_values) + num_labels = vit_bridge.original_model.config.num_labels + assert output.shape == (1, num_labels) + assert not torch.isnan(output).any() + + def test_forward_matches_hf(self, vit_bridge): + """Wrap-don't-reimplement: bridge substitutes hooked submodules into the + real HF model and calls its real forward, so this should match HF very + tightly (mamba's equivalent test asserts bit-exact; using a tight atol + here to be safe against BLAS/backend-dependent float noise across + machines rather than assuming bit-exactness untested).""" + pixel_values = _pixel_values() + hf_model = vit_bridge.original_model + with torch.no_grad(): + bridge_out = vit_bridge(pixel_values) + hf_out = hf_model(pixel_values).logits + max_diff = (bridge_out - hf_out).abs().max().item() + assert max_diff < 1e-4, f"Bridge vs HF max diff = {max_diff}" + + def test_batch_of_images(self, vit_bridge): + pixel_values = _pixel_values(batch=3) + with torch.no_grad(): + output = vit_bridge(pixel_values) + assert output.shape[0] == 3 + + +class TestViTClassifierHookCoverage: + @pytest.fixture(scope="class") + def cache(self, vit_bridge): + pixel_values = _pixel_values() + with torch.no_grad(): + _, cache = vit_bridge.run_with_cache(pixel_values) + return cache + + def test_embed_hooks_fire(self, cache, vit_bridge): + assert "embed.hook_in" in cache + assert "embed.hook_out" in cache + assert cache["embed.hook_in"].shape == (1, 3, 224, 224) + seq_len = cache["embed.hook_out"].shape[1] + d_model = vit_bridge.cfg.d_model + assert cache["embed.hook_out"].shape == (1, seq_len, d_model) + # ViT: CLS + patches, no distillation token (contrast with DeiT below). + assert seq_len == 1 + (224 // 16) ** 2 + + def test_block_submodule_hooks_fire(self, cache, vit_bridge): + n_layers = vit_bridge.cfg.n_layers + for i in [0, n_layers - 1]: + for submod in ("q", "k", "v", "o"): + assert f"blocks.{i}.attn.{submod}.hook_in" in cache + assert f"blocks.{i}.attn.{submod}.hook_out" in cache + assert f"blocks.{i}.mlp.in.hook_out" in cache + assert f"blocks.{i}.mlp.out.hook_out" in cache + + def test_mlp_hook_aliases_resolve(self, cache): + """hook_mlp_in/out are redirected via hook_alias_overrides in vit.py.""" + assert torch.equal(cache["blocks.0.hook_mlp_in"], cache["blocks.0.mlp.in.hook_in"]) + assert torch.equal(cache["blocks.0.hook_mlp_out"], cache["blocks.0.mlp.out.hook_out"]) + + def test_ln_final_hook_fires(self, cache): + assert "ln_final.hook_normalized" in cache + + def test_unembed_hooks_fire_with_pooled_shape(self, cache, vit_bridge): + """VisionClassifierHeadBridge only ever sees the pooled (batch, hidden) + CLS-token tensor — HF's ViTForImageClassification.forward() does the + `sequence_output[:, 0, :]` slicing itself before calling self.classifier, + and that real forward path is what's running here.""" + d_model = vit_bridge.cfg.d_model + num_labels = vit_bridge.original_model.config.num_labels + assert cache["unembed.hook_in"].shape == (1, d_model) + assert cache["unembed.hook_out"].shape == (1, num_labels) + + +# --------------------------------------------------------------------------- +# Bare ViTModel (no classifier) — google/vit-base-patch16-224-in21k +# --------------------------------------------------------------------------- +@pytest.fixture(scope="module") +def vit_bare_bridge(): + # Explicitly load the bare model + hf_model = ViTModel.from_pretrained(MODEL_VIT_BARE) + return TransformerBridge.boot_transformers(MODEL_VIT_BARE, hf_model=hf_model, device="cpu") + + +class TestViTBareModel: + def test_no_unembed_component(self, vit_bare_bridge): + assert "unembed" not in vit_bare_bridge.adapter.component_mapping + + def test_blocks_point_at_flat_layers_attribute_no_prefix(self, vit_bare_bridge): + assert vit_bare_bridge.adapter.component_mapping["blocks"].name == "layers" + + def test_forward_does_not_crash(self, vit_bare_bridge): + pixel_values = _pixel_values() + with torch.no_grad(): + vit_bare_bridge(pixel_values) # must not raise + + def test_forward_returns_last_hidden_state_tensor(self, vit_bare_bridge): + """ + For a bare ViTModel, HF's own forward returns BaseModelOutputWithPooling, + which has no `.logits` and is not a tuple subclass. bridge.py's forward() + now falls back to `output.last_hidden_state` in that case, so + return_type="logits" (the default) returns a plain tensor instead of the + raw HF output object. This mirrors the choice made for bare text encoders + that hit the same code path. + """ + pixel_values = _pixel_values() + hf_model = vit_bare_bridge.original_model + with torch.no_grad(): + output = vit_bare_bridge(pixel_values) + hf_out = hf_model(pixel_values).last_hidden_state + assert isinstance(output, torch.Tensor) + assert output.shape == hf_out.shape + assert torch.allclose(output, hf_out, atol=1e-4) + + +# --------------------------------------------------------------------------- +# DeiTForImageClassification — distillation token must stay invisible +# Previously MODEL_DEIT_CLASSIFIER was declared but never used, and the only +# DeiT fixture (deit_bridge, below) loaded a bare DeiTModel — so the +# "deit."-prefix + real classifier-head path had no coverage. This fixture +# and test class close that gap with a real DeiTForImageClassification load. +# --------------------------------------------------------------------------- +@pytest.fixture(scope="module") +def deit_classifier_bridge(): + hf_model = DeiTForImageClassification.from_pretrained(MODEL_DEIT_CLASSIFIER) + return TransformerBridge.boot_transformers( + MODEL_DEIT_CLASSIFIER, hf_model=hf_model, device="cpu" + ) + + +class TestDeiTClassifierBridge: + def test_prefix_is_deit_for_classifier_model(self, deit_classifier_bridge): + assert deit_classifier_bridge.adapter.component_mapping["blocks"].name == "deit.layers" + + def test_unembed_is_vision_classifier_head(self, deit_classifier_bridge): + assert isinstance(deit_classifier_bridge.unembed, VisionClassifierHeadBridge) + + def test_n_ctx_matches_real_patch_grid(self, deit_classifier_bridge): + assert deit_classifier_bridge.cfg.n_ctx == 197 + + def test_forward_matches_hf(self, deit_classifier_bridge): + pixel_values = _pixel_values() + hf_model = deit_classifier_bridge.original_model + with torch.no_grad(): + bridge_out = deit_classifier_bridge(pixel_values) + hf_out = hf_model(pixel_values).logits + max_diff = (bridge_out - hf_out).abs().max().item() + assert max_diff < 1e-4, f"Bridge vs HF max diff = {max_diff}" + + def test_forward_returns_correct_shape(self, deit_classifier_bridge): + pixel_values = _pixel_values() + with torch.no_grad(): + output = deit_classifier_bridge(pixel_values) + num_labels = deit_classifier_bridge.original_model.config.num_labels + assert output.shape == (1, num_labels) + + +# --------------------------------------------------------------------------- +# DeiT (bare model) — distillation token must stay invisible +# --------------------------------------------------------------------------- +# 1. Use the distilled checkpoint to actually test distillation token logic +MODEL_DEIT_DISTILLED = "facebook/deit-small-distilled-patch16-224" + + +@pytest.fixture(scope="module") +def deit_bridge(): + # 2. Load the bare Hugging Face model manually to bypass the + # DeiTForImageClassificationWithTeacher dual-head that your adapter rejects. + hf_model = DeiTModel.from_pretrained(MODEL_DEIT_DISTILLED) + hf_model.config.architectures = ["DeiTModel"] + + # 3. Pass the instantiated HF model to boot_transformers. + # (Assuming your boot_transformers accepts an hf_model kwarg like HookedTransformer) + return TransformerBridge.boot_transformers( + MODEL_DEIT_DISTILLED, hf_model=hf_model, device="cpu" + ) + + +class TestDeiTBridge: + def test_prefix_is_empty_for_bare_model(self, deit_bridge): + # Because we loaded a bare DeiTModel, the components are at the root. + # Your prepare_model should assign prefix="" (not "deit.") + assert deit_bridge.adapter.component_mapping["embed"].name == "embeddings" + + def test_forward_matches_hf(self, deit_bridge): + pixel_values = _pixel_values() + hf_model = deit_bridge.original_model + + with torch.no_grad(): + bridge_out = deit_bridge(pixel_values) + # Bare models return a BaseModelOutput object with last_hidden_state + hf_out = hf_model(pixel_values).last_hidden_state + + # Account for your adapter returning the raw HF object for bare models + # (as you documented in TestViTBareModel) + if not isinstance(bridge_out, torch.Tensor): + bridge_out = bridge_out.last_hidden_state + + max_diff = (bridge_out - hf_out).abs().max().item() + assert max_diff < 1e-4, f"Bridge vs HF max diff = {max_diff}" + + def test_sequence_length_includes_distillation_token(self, deit_bridge): + """DeiT embeddings prepend CLS *and* distillation tokens (vs. ViT's CLS + only) — invisible to the adapter per vision_embeddings.py's docstring, + but it should still show up in the actual hidden-state shape, one token + longer than the equivalent ViT sequence length.""" + pixel_values = _pixel_values() + with torch.no_grad(): + _, cache = deit_bridge.run_with_cache(pixel_values) + + seq_len = cache["embed.hook_out"].shape[1] + num_patches = (224 // 16) ** 2 + + assert seq_len == num_patches + 2 # CLS + distillation + patches + + +# --------------------------------------------------------------------------- +# DeiTForImageClassificationWithTeacher — must raise, not silently mishandle +# --------------------------------------------------------------------------- + + +class TestDeiTWithTeacherRejected: + def test_loading_raises_not_implemented(self): + """No small public DeiTForImageClassificationWithTeacher checkpoint is + assumed here — this drives the same code path prepare_model() exercises + via a HF class instance check, without a full weights download. + """ + from transformers import DeiTConfig, DeiTForImageClassificationWithTeacher + + from transformer_lens.model_bridge.supported_architectures.vit import ( + ViTArchitectureAdapter, + ) + + config = DeiTConfig( + hidden_size=32, + num_hidden_layers=2, + num_attention_heads=2, + intermediate_size=64, + image_size=32, + patch_size=16, + num_labels=10, + ) + hf_model = DeiTForImageClassificationWithTeacher(config) + + adapter = ViTArchitectureAdapter.__new__(ViTArchitectureAdapter) + adapter.cfg = None # prepare_model() doesn't touch cfg before the raise + with pytest.raises(NotImplementedError): + ViTArchitectureAdapter.prepare_model(adapter, hf_model) + + +# --------------------------------------------------------------------------- +# Regression test: prepare_model() must not assume `.encoder` exists. +# --------------------------------------------------------------------------- + + +class TestPrepareModelDoesNotAssumeEncoderWrapper: + def test_minimal_bare_model_stub_does_not_crash(self): + """Reproduces the AttributeError seen after upgrading transformers: + 'types.SimpleNamespace' object has no attribute 'encoder'. + + A stub with none of vit/deit/classifier/cls_classifier/ + distillation_classifier set should be treated like a bare, no-prefix + model — prepare_model() must not reach for `hf_model.encoder.layer` + (that wrapper module no longer exists in current transformers; blocks + live directly on `.layers`). + """ + from transformer_lens.model_bridge.supported_architectures.vit import ( + ViTArchitectureAdapter, + ) + + hf_model = SimpleNamespace() + + adapter = ViTArchitectureAdapter.__new__(ViTArchitectureAdapter) + adapter.cfg = None + + ViTArchitectureAdapter.prepare_model(adapter, hf_model) # must not raise + + assert adapter.component_mapping["blocks"].name == "layers" + assert "unembed" not in adapter.component_mapping diff --git a/tests/integration/test_jacobian_lens_kurtosis.py b/tests/integration/test_jacobian_lens_kurtosis.py new file mode 100644 index 0000000000..dafac4ffd5 --- /dev/null +++ b/tests/integration/test_jacobian_lens_kurtosis.py @@ -0,0 +1,190 @@ +"""Kurtosis-profile check for JacobianLens readouts (#1539 Tier-1). + +Gurnee et al. (Transformer Circuits 2026, §4) report excess kurtosis of J-lens +readout logits near zero through the first third of depth, rising through the +workspace band. Absolute levels do not transfer between families (early-third +medians run 0.2 on gpt2-small to ~140 on pythia-70m-deduped, see #1539), so +the shape is asserted relative to each model's own baseline and to a +logit-lens control through the identical code path: + + 1. Qwen3.5-0.8B: interior peak beats the early-third median by >= 0.35 + (measured +0.78 on the 40-prompt reference run; this test uses 12). + 2. That rise beats the logit-lens control's rise (measured gap +0.59). + 3. gpt2-small shows no rise (measured +0.16) — negative control. + 4. The final layer is identity transport in both arms; profiles must match. + +Prompts are wikitext-103 validation rows; the published lenses were fitted on +train. Artifacts pinned to the same revision as +test_jacobian_lens_oracle_parity.py. Slow: downloads Qwen3.5-0.8B (~1.6 GB), +runs on CPU in a few minutes. +""" + +from typing import Dict, List + +import pytest +import torch + +LENS_REPO = "neuronpedia/jacobian-lens" +LENS_REVISION = "a4114d7752d11eb546e6cf372213d7e75526d3a1" + +QWEN_MODEL = "Qwen/Qwen3.5-0.8B" +QWEN_LENS_FILE = "qwen3.5-0.8b/jlens/Salesforce-wikitext/Qwen3.5-0.8B_jacobian_lens.pt" +GPT2_MODEL = "gpt2" +GPT2_LENS_FILE = "gpt2-small/jlens/Salesforce-wikitext/gpt2_jacobian_lens.pt" + +N_PROMPTS = 12 +N_POSITIONS = 6 +MAX_SEQ_LEN = 128 +SKIP_FIRST_POSITIONS = 16 # matches the published fit configs +MIN_PROMPT_CHARS = 400 + +# Measured on 40 prompts x 8 positions (see #1539): qwen rise +0.78, +# control rise +0.19, gpt2 rise +0.16. Thresholds at ~half the measured +# effect / double the negative control. +QWEN_MIN_BAND_RISE = 0.35 +QWEN_MIN_RISE_OVER_CONTROL = 0.25 +GPT2_MAX_BAND_RISE = 0.35 +FINAL_LAYER_ARM_TOL = 1e-3 + + +def _excess_kurtosis(logits: torch.Tensor) -> torch.Tensor: + """Population excess kurtosis over the vocab axis; logits [pos, d_vocab].""" + x = logits.float() + d = x - x.mean(dim=-1, keepdim=True) + m2 = (d**2).mean(dim=-1) + m4 = (d**4).mean(dim=-1) + return m4 / m2.pow(2) - 3.0 + + +def _wikitext_validation_prompts() -> List[str]: + from datasets import load_dataset + + ds = load_dataset("Salesforce/wikitext", "wikitext-103-raw-v1", split="validation") + prompts = [] + for row in ds: + text = row["text"].strip() + if len(text) >= MIN_PROMPT_CHARS and not text.startswith("="): + prompts.append(text[:2000]) + if len(prompts) == N_PROMPTS: + break + return prompts + + +def _median_profiles(model, lens) -> Dict[str, Dict[int, float]]: + """Per-layer median excess kurtosis for the J-lens and logit-lens arms.""" + samples: Dict[str, Dict[int, List[float]]] = {"jlens": {}, "control": {}} + for text in _wikitext_validation_prompts(): + tokens = model.to_tokens(text)[:, :MAX_SEQ_LEN] + seq_len = tokens.shape[1] + if seq_len <= SKIP_FIRST_POSITIONS + 1: + continue + candidates = list(range(SKIP_FIRST_POSITIONS, seq_len - 1)) + step = max(len(candidates) / N_POSITIONS, 1) + positions = [candidates[int(i * step)] for i in range(N_POSITIONS)][: len(candidates)] + + for arm, use_jacobian in (("jlens", True), ("control", False)): + with torch.no_grad(): + readout = lens.readout( + model, + tokens, + positions=positions, + use_jacobian=use_jacobian, + return_full_logits=True, + ) + assert readout.lens_logits is not None + for layer, logits in readout.lens_logits.items(): + k = _excess_kurtosis(logits) + samples[arm].setdefault(layer, []).extend(k.tolist()) + + return { + arm: {layer: float(torch.tensor(vals).median()) for layer, vals in by_layer.items()} + for arm, by_layer in samples.items() + } + + +def _shape_stats(profile: Dict[int, float], n_layers: int) -> Dict[str, float]: + """Early-third median and interior (non-final, past one-third) peak.""" + early = [v for layer, v in profile.items() if layer / (n_layers - 1) < 1 / 3] + interior = [ + v + for layer, v in profile.items() + if layer / (n_layers - 1) >= 1 / 3 and layer != n_layers - 1 + ] + return { + "early": float(torch.tensor(early).median()), + "peak": max(interior), + "final": profile[n_layers - 1], + } + + +def _boot(model_name: str, lens_file: str): + from transformer_lens.model_bridge import TransformerBridge + from transformer_lens.tools.analysis import JacobianLens + + model = TransformerBridge.boot_transformers(model_name, device="cpu") + model.eval() + lens = JacobianLens.from_pretrained( + LENS_REPO, filename=lens_file, revision=LENS_REVISION, model=model + ) + return model, lens + + +@pytest.fixture(scope="module") +def qwen_profiles(): + model, lens = _boot(QWEN_MODEL, QWEN_LENS_FILE) + profiles = _median_profiles(model, lens) + return profiles, model.cfg.n_layers + + +@pytest.fixture(scope="module") +def gpt2_profiles(): + model, lens = _boot(GPT2_MODEL, GPT2_LENS_FILE) + profiles = _median_profiles(model, lens) + return profiles, model.cfg.n_layers + + +@pytest.mark.slow +def test_band_rise_is_present_and_lens_specific(qwen_profiles): + """Qwen3.5-0.8B: J-lens kurtosis rises past depth 1/3; the control does not.""" + profiles, n_layers = qwen_profiles + jlens = _shape_stats(profiles["jlens"], n_layers) + control = _shape_stats(profiles["control"], n_layers) + + jlens_rise = jlens["peak"] - jlens["early"] + control_rise = control["peak"] - control["early"] + + assert jlens_rise >= QWEN_MIN_BAND_RISE, ( + f"J-lens band rise {jlens_rise:.3f} < {QWEN_MIN_BAND_RISE} " + f"(early {jlens['early']:.3f}, peak {jlens['peak']:.3f}); " + f"measured +0.78 on the reference run" + ) + assert jlens_rise - control_rise >= QWEN_MIN_RISE_OVER_CONTROL, ( + f"rise not lens-specific: J-lens {jlens_rise:.3f} vs control " + f"{control_rise:.3f} through the identical code path" + ) + + +@pytest.mark.slow +def test_negative_control_gpt2_has_no_band_rise(gpt2_profiles): + """gpt2-small: no workspace-band rise, so the check cannot pass vacuously.""" + profiles, n_layers = gpt2_profiles + jlens = _shape_stats(profiles["jlens"], n_layers) + rise = jlens["peak"] - jlens["early"] + assert rise < GPT2_MAX_BAND_RISE, ( + f"gpt2-small J-lens band rise {rise:.3f} >= {GPT2_MAX_BAND_RISE}; " + f"measured +0.16 on the reference run — investigate the lens artifact " + f"or readout path before raising the threshold" + ) + + +@pytest.mark.slow +@pytest.mark.parametrize("profiles_fixture", ["qwen_profiles", "gpt2_profiles"]) +def test_final_layer_arms_agree(profiles_fixture, request): + """The final layer is identity transport in both arms: profiles must match.""" + profiles, n_layers = request.getfixturevalue(profiles_fixture) + final = n_layers - 1 + diff = abs(profiles["jlens"][final] - profiles["control"][final]) + assert diff < FINAL_LAYER_ARM_TOL, ( + f"final-layer kurtosis differs between arms by {diff:.2e}; " + f"identity transport should make them equal" + ) diff --git a/tests/integration/test_jacobian_lens_oracle_parity.py b/tests/integration/test_jacobian_lens_oracle_parity.py new file mode 100644 index 0000000000..53f3407c75 --- /dev/null +++ b/tests/integration/test_jacobian_lens_oracle_parity.py @@ -0,0 +1,225 @@ +"""External-oracle parity test for TL JacobianLens. + +Compares TransformerBridge-based JacobianLens readouts against the reference +anthropics/jacobian-lens oracle on google/gemma-2-2b-it. + +Pass criteria (per the #1539 Tier-1 spec, matching the #1505 spike numbers): + - Worst-case top-8 token overlap >= 7/8 across all 75 layer x prompt cells + - Spearman rank-correlation >= 0.95 on the top-64 logit union per cell + +The oracle is pinned to commit 581d398613e5602a5af361e1c34d3a92ea82ba8e +(Apache-2.0) so the threshold is reproducible independent of upstream drift. + +Install the oracle before running this test: + uv pip install git+https://github.com/anthropics/jacobian-lens.git@ + +Marked @pytest.mark.slow; requires ~4 GB VRAM and network access. +""" + +from typing import List, Set + +import numpy as np +import pytest +import torch + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +ORACLE_COMMIT = "581d398613e5602a5af361e1c34d3a92ea82ba8e" +ORACLE_PKG = f"git+https://github.com/anthropics/jacobian-lens.git@{ORACLE_COMMIT}" + +LENS_REPO = "neuronpedia/jacobian-lens" +LENS_REVISION = "a4114d7752d11eb546e6cf372213d7e75526d3a1" +GEMMA_MODEL = "google/gemma-2-2b-it" +GEMMA_LENS_FILE = "gemma-2-2b-it/jlens/Salesforce-wikitext/gemma-2-2b-it_jacobian_lens.pt" + +# 5 prompts × 15 layers = 75 layer x prompt cells +PARITY_PROMPTS: List[str] = [ + "The Eiffel Tower is located in the city of", + "Water molecules consist of two hydrogen atoms and one", + "The mitochondria is often called the powerhouse of the", + "In supervised learning, gradient descent is used to minimize the", + "The speed of light in a vacuum is approximately three hundred", +] + +# 15 layers sampled from Gemma-2-2b-it's 26 (indices 0-25). +# Layer 25 is the final (identity transport) and is checked separately. +PARITY_LAYERS: List[int] = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23, 24, 25] +assert len(PARITY_PROMPTS) * len(PARITY_LAYERS) == 75 + +TOP_K = 8 +TOP_K_MIN_OVERLAP = 7 # >= 7/8 in worst cell +SPEARMAN_TOP_K = 64 +SPEARMAN_MIN_R = 0.95 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _spearman_r(a: np.ndarray, b: np.ndarray) -> float: + """Spearman rank correlation — avoids a scipy dependency.""" + ra = np.argsort(np.argsort(a)).astype(np.float64) + rb = np.argsort(np.argsort(b)).astype(np.float64) + n = len(ra) + d2 = float(((ra - rb) ** 2).sum()) + denom = n * (n * n - 1) + return 1.0 - 6.0 * d2 / denom + + +def _top_k_set(logits: torch.Tensor, k: int) -> Set[int]: + return set(logits.topk(k).indices.tolist()) + + +def _spearman_top_union(tl_logits: torch.Tensor, oracle_logits: torch.Tensor, k: int) -> float: + tl_topk = tl_logits.topk(k).indices + or_topk = oracle_logits.topk(k).indices + union = torch.unique(torch.cat([tl_topk, or_topk])).tolist() + return _spearman_r( + tl_logits[union].float().numpy(), + oracle_logits[union].float().numpy(), + ) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def oracle_package(): + """Return oracle symbols; skips if the pinned package is not installed. + + Install out-of-band before running: + uv pip install {ORACLE_PKG} + Or let .github/workflows/oracle-parity.yml handle it in CI. + """ + jlens_hf = pytest.importorskip( + "jlens.hf", + reason=(f"requires the pinned oracle: uv pip install {ORACLE_PKG}"), + ) + from jlens.lens import JacobianLens as OracleJL + + return OracleJL, jlens_hf.from_hf + + +@pytest.fixture(scope="module") +def gemma_bridge(): + from transformer_lens.model_bridge import TransformerBridge + + device = "cuda" if torch.cuda.is_available() else "cpu" + return TransformerBridge.boot_transformers(GEMMA_MODEL, device=device) + + +@pytest.fixture(scope="module") +def tl_lens(gemma_bridge): + from transformer_lens.tools.analysis import JacobianLens + + return JacobianLens.from_pretrained( + LENS_REPO, + filename=GEMMA_LENS_FILE, + revision=LENS_REVISION, + model=gemma_bridge, + ) + + +@pytest.fixture(scope="module") +def oracle_lens_and_model(oracle_package, gemma_bridge): + """Oracle JacobianLens + HFLensModel sharing the bridge's underlying HF model.""" + OracleJL, from_hf = oracle_package + oracle_lens = OracleJL.from_pretrained( + LENS_REPO, + filename=GEMMA_LENS_FILE, + revision=LENS_REVISION, + ) + # Reuse the HF model already loaded inside the bridge — no second copy. + oracle_model = from_hf( + gemma_bridge.original_model, + gemma_bridge.tokenizer, + force_bos=False, # bridge already controls BOS; don't mutate its tokenizer + ) + return oracle_lens, oracle_model + + +# --------------------------------------------------------------------------- +# Parity test +# --------------------------------------------------------------------------- + + +@pytest.mark.slow +def test_jacobian_lens_oracle_parity(tl_lens, oracle_lens_and_model, gemma_bridge): + """TL JacobianLens readouts match the anthropics oracle within #1539 tolerances. + + Checks all 75 (5 prompts × 15 layers) layer x prompt cells: + - top-8 token overlap >= 7 in every cell + - Spearman r >= 0.95 on the top-64 logit union in every cell + """ + oracle_lens, oracle_model = oracle_lens_and_model + + final_layer = gemma_bridge.cfg.n_layers - 1 + source_layers = [l for l in PARITY_LAYERS if l != final_layer] + + failures: List[str] = [] + + for prompt in PARITY_PROMPTS: + # TL: one readout call for all layers at once. + tl_result = tl_lens.readout( + gemma_bridge, + prompt, + layers=PARITY_LAYERS, + positions=[-1], + return_full_logits=True, + top_k=TOP_K, + ) + assert tl_result.lens_logits is not None + + # Oracle: source layers only (raises if passed the final/identity layer). + oracle_logits_dict, oracle_model_logits, _ids = oracle_lens.apply( + oracle_model, + prompt, + layers=source_layers, + positions=[-1], + ) + + # --- source layers: compare oracle transport vs TL transport --- + for layer in source_layers: + tl_vec = tl_result.lens_logits[layer][-1].float() # [vocab] + or_vec = oracle_logits_dict[layer][-1].float() # [vocab] + + overlap = len(_top_k_set(tl_vec, TOP_K) & _top_k_set(or_vec, TOP_K)) + if overlap < TOP_K_MIN_OVERLAP: + failures.append( + f"top-{TOP_K} overlap={overlap}/{TOP_K} " + f"(layer={layer}, prompt={prompt[:35]!r})" + ) + + r = _spearman_top_union(tl_vec, or_vec, SPEARMAN_TOP_K) + if r < SPEARMAN_MIN_R: + failures.append( + f"Spearman r={r:.4f} < {SPEARMAN_MIN_R} " + f"(layer={layer}, prompt={prompt[:35]!r})" + ) + + # --- final layer: TL identity == oracle model_logits --- + tl_final = tl_result.lens_logits[final_layer][-1].float() # [vocab] + or_final = oracle_model_logits[-1].float() # [vocab] + + final_overlap = len(_top_k_set(tl_final, TOP_K) & _top_k_set(or_final, TOP_K)) + if final_overlap < TOP_K_MIN_OVERLAP: + failures.append( + f"top-{TOP_K} overlap={final_overlap}/{TOP_K} " + f"(final layer={final_layer}, prompt={prompt[:35]!r})" + ) + + final_r = _spearman_top_union(tl_final, or_final, SPEARMAN_TOP_K) + if final_r < SPEARMAN_MIN_R: + failures.append( + f"Spearman r={final_r:.4f} < {SPEARMAN_MIN_R} " + f"(final layer={final_layer}, prompt={prompt[:35]!r})" + ) + + assert not failures, f"{len(failures)} cell(s) failed parity out of 75:\n" + "\n".join( + f" {f}" for f in failures + ) diff --git a/tests/unit/benchmarks/test_build_modality_input.py b/tests/unit/benchmarks/test_build_modality_input.py new file mode 100644 index 0000000000..9d8640f3d6 --- /dev/null +++ b/tests/unit/benchmarks/test_build_modality_input.py @@ -0,0 +1,91 @@ +"""``build_modality_input`` must shape its tensor from the architecture, not a default. + +Phase 1 and Phase 8 previously hardcoded a 16 kHz waveform, which is only correct for +waveform encoders (HuBERT, wav2vec2). Spectrogram encoders (AST) take +``[batch, max_length, num_mel_bins]`` and vision encoders take +``[batch, num_channels, image_size, image_size]``, so both failed verification with an +``IndexError`` before reaching any real comparison. +""" + +from types import SimpleNamespace + +import torch + +from transformer_lens.benchmarks.utils import build_modality_input + + +def _bridge(hf_config=None, **cfg_flags): + """Minimal bridge stand-in: build_modality_input only reads cfg flags and the HF config.""" + return SimpleNamespace( + cfg=SimpleNamespace(**cfg_flags), + original_model=SimpleNamespace(config=hf_config) if hf_config is not None else None, + ) + + +def test_spectrogram_audio_uses_config_dimensions() -> None: + """AST-style encoders get [batch, max_length, num_mel_bins] from the HF config.""" + bridge = _bridge( + hf_config=SimpleNamespace(num_mel_bins=128, max_length=1024), + is_audio_model=True, + ) + got = build_modality_input(bridge) + assert got is not None + assert got.shape == (1, 1024, 128) + + +def test_spectrogram_dimensions_are_not_hardcoded() -> None: + """A checkpoint with non-default mel/length dimensions is honoured.""" + bridge = _bridge( + hf_config=SimpleNamespace(num_mel_bins=64, max_length=512), + is_audio_model=True, + ) + got = build_modality_input(bridge, batch_size=3) + assert got is not None + assert got.shape == (3, 512, 64) + + +def test_waveform_audio_without_mel_bins() -> None: + """num_mel_bins is what separates a spectrogram encoder from a waveform one.""" + bridge = _bridge(hf_config=SimpleNamespace(), is_audio_model=True) + got = build_modality_input(bridge) + assert got is not None + assert got.shape == (1, 16000) + + +def test_vision_uses_config_image_size() -> None: + """Vision encoders get pixel-shaped input sized from the HF config.""" + bridge = _bridge( + hf_config=SimpleNamespace(image_size=224, num_channels=3), + is_visual_model=True, + ) + got = build_modality_input(bridge) + assert got is not None + assert got.shape == (1, 3, 224, 224) + + +def test_vision_dimensions_are_not_hardcoded() -> None: + """A non-224 checkpoint is honoured rather than forced to the default.""" + bridge = _bridge( + hf_config=SimpleNamespace(image_size=384, num_channels=1), + is_visual_model=True, + ) + got = build_modality_input(bridge) + assert got is not None + assert got.shape == (1, 1, 384, 384) + + +def test_text_model_returns_none() -> None: + """Text models keep the token-id path — the caller falls back to to_tokens().""" + bridge = _bridge(hf_config=SimpleNamespace(vocab_size=50257)) + assert build_modality_input(bridge) is None + + +def test_dtype_and_device_are_applied() -> None: + """Callers pass the benchmark's dtype so the input matches the model's precision.""" + bridge = _bridge( + hf_config=SimpleNamespace(num_mel_bins=128, max_length=1024), + is_audio_model=True, + ) + got = build_modality_input(bridge, device="cpu", dtype=torch.float64) + assert got is not None + assert got.dtype == torch.float64 diff --git a/tests/unit/benchmarks/test_multimodal_prompt_selection.py b/tests/unit/benchmarks/test_multimodal_prompt_selection.py new file mode 100644 index 0000000000..e3bbe14e19 --- /dev/null +++ b/tests/unit/benchmarks/test_multimodal_prompt_selection.py @@ -0,0 +1,108 @@ +"""Image-token prompt selection for Phase 7 inputs (download-free). + +Multimodal families disagree about which image token their processor expands, +and a rejected token raises rather than degrading: Gemma3 accepts only +``boi_token``, Gemma4 only ``image_token``, LLaVA has just ``image_token``. +``_prepare_test_inputs`` swallows those exceptions and returns None, which +Phase 7 reports as SKIPPED — so picking the wrong token silently drops the +whole multimodal suite. These fakes pin the fallback order per family. +""" + +from types import SimpleNamespace + +import pytest + +pytest.importorskip("torch") + +import torch # noqa: E402 + +from transformer_lens.benchmarks.multimodal import _prepare_test_inputs # noqa: E402 +from transformer_lens.model_bridge import TransformerBridge # noqa: E402 + + +class FakeProcessor: + """Processor that expands exactly one image token and raises on the rest.""" + + def __init__(self, accepted, image_token=None, boi_token=None): + self.accepted = accepted + if image_token is not None: + self.image_token = image_token + if boi_token is not None: + self.boi_token = boi_token + self.calls = [] + + def __call__(self, text=None, images=None, return_tensors=None): + self.calls.append(text) + if self.accepted not in text: + raise ValueError(f"Prompt contained 0 image tokens but received {images!r}") + return { + "input_ids": torch.tensor([[1, 2, 3]]), + "pixel_values": torch.zeros(1, 3, 8, 8), + } + + +class FakeModel(torch.nn.Module): + def __init__(self, image_token_id): + super().__init__() + self.config = SimpleNamespace(image_token_id=image_token_id) + + +def _bridge(processor, image_token_id=None): + """A bare bridge — _prepare_test_inputs is beartype-checked against the real type.""" + bridge = TransformerBridge.__new__(TransformerBridge) + torch.nn.Module.__init__(bridge) # submodule assignment needs the Module state + bridge.processor = processor + bridge.cfg = SimpleNamespace(device="cpu") + bridge.original_model = FakeModel(image_token_id) + return bridge + + +class TestPromptSelection: + def test_gemma3_falls_back_to_boi_token(self): + # Gemma3's image_token is rejected by its own processor; trying it + # first must not abort the whole preparation. + proc = FakeProcessor( + accepted="", + image_token="", + boi_token="", + ) + input_ids, extra_kwargs, prompt = _prepare_test_inputs(_bridge(proc)) + assert input_ids is not None + assert "" in prompt + assert "pixel_values" in extra_kwargs + + def test_gemma4_prefers_image_token(self): + # Gemma4 is the mirror image: boi_token is just a marker. + proc = FakeProcessor(accepted="<|image|>", image_token="<|image|>", boi_token="<|image>") + input_ids, _, prompt = _prepare_test_inputs(_bridge(proc)) + assert input_ids is not None + assert proc.calls[0].startswith("<|image|>") + assert "<|image|>" in prompt + + def test_llava_single_token_still_works(self): + proc = FakeProcessor(accepted="", image_token="") + input_ids, _, prompt = _prepare_test_inputs(_bridge(proc)) + assert input_ids is not None + assert "" in prompt + + def test_returns_none_when_no_candidate_accepted(self): + proc = FakeProcessor(accepted="", image_token="") + assert _prepare_test_inputs(_bridge(proc)) == (None, None, None) + + def test_auto_inserting_processor_uses_plain_prompt(self): + # Processors that insert placeholders themselves are detected by the + # probe; a manual placeholder would double-count the image. + class AutoInserting(FakeProcessor): + def __call__(self, text=None, images=None, return_tensors=None): + self.calls.append(text) + return { + "input_ids": torch.tensor([[1, 42, 3]]), + "pixel_values": torch.zeros(1, 3, 8, 8), + } + + proc = AutoInserting(accepted="", image_token="") + _, _, prompt = _prepare_test_inputs(_bridge(proc, image_token_id=42)) + assert prompt == "Describe this image." + + def test_no_processor_returns_none(self): + assert _prepare_test_inputs(_bridge(None)) == (None, None, None) diff --git a/tests/unit/benchmarks/test_vision_benchmarks.py b/tests/unit/benchmarks/test_vision_benchmarks.py new file mode 100644 index 0000000000..f4bb6bdc9a --- /dev/null +++ b/tests/unit/benchmarks/test_vision_benchmarks.py @@ -0,0 +1,141 @@ +"""Vision benchmark (Phase 9) gating and wiring, download-free. + +The real pixel forward/cache runs happen in per-model verification; here we pin +the skip/gate logic and the shared encoder-benchmark wiring (result names, HF +reference kwarg, critical-hook selection) without loading a model. +""" + +from types import SimpleNamespace + +import pytest + +pytest.importorskip("torch") + +import torch # noqa: E402 + +from transformer_lens.benchmarks.utils import BenchmarkSeverity # noqa: E402 +from transformer_lens.benchmarks.vision import ( # noqa: E402 + benchmark_vision_cache, + benchmark_vision_classification_decode, + benchmark_vision_forward, + benchmark_vision_representation_stability, +) +from transformer_lens.model_bridge.bridge import TransformerBridge # noqa: E402 + + +class _FakeBridge(TransformerBridge): + """Subclass so beartype's isinstance(bridge, TransformerBridge) passes, while + bypassing the heavy real __init__ — the benchmarks only touch cfg/adapter/ + processor and the forward/cache callables stubbed here.""" + + def __init__(self, cfg, adapter=None, processor=None, forward_out=None, cache=None): + self.cfg = cfg + self.adapter = adapter + self.processor = processor + self._forward_out = forward_out + self._cache = cache + + def __call__(self, *args, **kwargs): + return self._forward_out + + def run_with_cache(self, *args, **kwargs): + return self._forward_out, self._cache + + +def _fake_bridge(model_name="google/vit-base-patch16-224", **kwargs): + cfg = SimpleNamespace( + is_visual_model=True, + is_multimodal=False, + model_name=model_name, + n_layers=2, + d_model=8, + device="cpu", + ) + return _FakeBridge(cfg, **kwargs) + + +_PIXELS = torch.zeros(1, 3, 4, 4) + + +def test_forward_passes_on_finite_tensor(): + res = benchmark_vision_forward(_fake_bridge(forward_out=torch.randn(1, 5, 8)), _PIXELS) + assert res.name == "vision_forward" + assert res.passed is True + + +def test_forward_fails_on_nan_output(): + res = benchmark_vision_forward( + _fake_bridge(forward_out=torch.full((1, 5, 8), float("nan"))), _PIXELS + ) + assert res.passed is False + assert res.severity == BenchmarkSeverity.DANGER + + +def test_forward_feeds_reference_pixel_values(): + seen = {} + + class _Ref(torch.nn.Module): + def forward(self, **kwargs): + seen.update(kwargs) + return SimpleNamespace(logits=torch.ones(1, 5, 8)) + + res = benchmark_vision_forward( + _fake_bridge(forward_out=torch.ones(1, 5, 8)), _PIXELS, reference_model=_Ref() + ) + assert "pixel_values" in seen + assert res.passed is True + + +def test_cache_requires_declared_components_and_edge_blocks(): + hooks = { + "embed.hook_out": torch.ones(1, 5, 8), + "ln_final.hook_out": torch.ones(1, 5, 8), + "unembed.hook_out": torch.ones(1, 10), + "blocks.0.hook_out": torch.ones(1, 5, 8), + "blocks.1.hook_out": torch.ones(1, 5, 8), + } + adapter = SimpleNamespace(component_mapping={"embed": 1, "ln_final": 1, "unembed": 1}) + res = benchmark_vision_cache(_fake_bridge(adapter=adapter, cache=hooks), _PIXELS) + assert res.name == "vision_cache" + assert res.passed is True + assert res.details["critical_found"] == 5 + + +def test_cache_reports_missing_classifier_hook(): + hooks = { + "embed.hook_out": torch.ones(1, 5, 8), + "blocks.0.hook_out": torch.ones(1, 5, 8), + "blocks.1.hook_out": torch.ones(1, 5, 8), + } + adapter = SimpleNamespace(component_mapping={"embed": 1, "ln_final": 1, "unembed": 1}) + res = benchmark_vision_cache(_fake_bridge(adapter=adapter, cache=hooks), _PIXELS) + assert res.severity == BenchmarkSeverity.WARNING + assert "ln_final.hook_out" in res.details["missing"] + assert "unembed.hook_out" in res.details["missing"] + + +def test_stability_skips_tiny_models(): + res = benchmark_vision_representation_stability( + _fake_bridge(model_name="hf-internal-testing/tiny-random-vit"), _PIXELS + ) + assert res.severity == BenchmarkSeverity.SKIPPED + assert res.name == "vision_representation_stability" + + +def test_decode_skips_tiny_models_bare_encoders_and_missing_processor(): + res = benchmark_vision_classification_decode( + _fake_bridge(model_name="hf-internal-testing/tiny-random-vit") + ) + assert res.severity == BenchmarkSeverity.SKIPPED + # Bare encoder: no classifier head declared in the component mapping. + res2 = benchmark_vision_classification_decode( + _fake_bridge(adapter=SimpleNamespace(component_mapping={"embed": 1})) + ) + assert res2.severity == BenchmarkSeverity.SKIPPED + assert "no classifier head" in res2.message + # Classifier head declared but no image processor available. + res3 = benchmark_vision_classification_decode( + _fake_bridge(adapter=SimpleNamespace(component_mapping={"unembed": 1}), processor=None) + ) + assert res3.severity == BenchmarkSeverity.SKIPPED + assert "processor" in res3.message diff --git a/tests/unit/model_bridge/generalized_components/test_mlp_weight_orientation.py b/tests/unit/model_bridge/generalized_components/test_mlp_weight_orientation.py new file mode 100644 index 0000000000..29701a5234 --- /dev/null +++ b/tests/unit/model_bridge/generalized_components/test_mlp_weight_orientation.py @@ -0,0 +1,214 @@ +"""Tests for MLP weight orientation (W_in/W_out/W_gate in TL convention).""" + +import pytest +import torch +import torch.nn as nn + +from transformer_lens.model_bridge.generalized_components.linear import LinearBridge +from transformer_lens.model_bridge.generalized_components.mlp import MLPBridge + + +class TestMLPWeightOrientation: + """Test that MLP weight accessors return TL orientation regardless of backing module.""" + + @pytest.fixture + def d_model(self) -> int: + return 64 + + @pytest.fixture + def d_mlp(self) -> int: + return 256 + + @pytest.fixture + def linear_mlp_bridge(self, d_model: int, d_mlp: int) -> MLPBridge: + """Create an MLPBridge backed by nn.Linear modules.""" + mlp = MLPBridge(name="mlp") + + # Create nn.Linear projections (stores weights as [out, in]) + in_proj = nn.Linear(d_model, d_mlp, bias=False) + out_proj = nn.Linear(d_mlp, d_model, bias=False) + + # Wrap in LinearBridge + in_bridge = LinearBridge(name="in") + in_bridge.set_original_component(in_proj) + out_bridge = LinearBridge(name="out") + out_bridge.set_original_component(out_proj) + + # Register as submodules + mlp.add_module("in", in_bridge) + mlp.add_module("out", out_bridge) + + return mlp + + @pytest.fixture + def conv1d_mlp_bridge(self, d_model: int, d_mlp: int) -> MLPBridge: + """Create an MLPBridge backed by Conv1D modules (GPT-2 style).""" + from transformers.pytorch_utils import Conv1D + + mlp = MLPBridge(name="mlp") + + # Create Conv1D projections (stores weights as [in, out]) + in_proj = Conv1D(d_mlp, d_model) + out_proj = Conv1D(d_model, d_mlp) + + # Wrap in LinearBridge + in_bridge = LinearBridge(name="in") + in_bridge.set_original_component(in_proj) + out_bridge = LinearBridge(name="out") + out_bridge.set_original_component(out_proj) + + # Register as submodules + mlp.add_module("in", in_bridge) + mlp.add_module("out", out_bridge) + + return mlp + + def test_linear_w_in_shape_is_tl_convention( + self, linear_mlp_bridge: MLPBridge, d_model: int, d_mlp: int + ): + """W_in from nn.Linear should be [d_model, d_mlp].""" + w_in = linear_mlp_bridge.W_in + assert w_in.shape == (d_model, d_mlp), f"Expected ({d_model}, {d_mlp}), got {w_in.shape}" + + def test_linear_w_out_shape_is_tl_convention( + self, linear_mlp_bridge: MLPBridge, d_model: int, d_mlp: int + ): + """W_out from nn.Linear should be [d_mlp, d_model].""" + w_out = linear_mlp_bridge.W_out + assert w_out.shape == (d_mlp, d_model), f"Expected ({d_mlp}, {d_model}), got {w_out.shape}" + + def test_conv1d_w_in_shape_is_tl_convention( + self, conv1d_mlp_bridge: MLPBridge, d_model: int, d_mlp: int + ): + """W_in from Conv1D should be [d_model, d_mlp].""" + w_in = conv1d_mlp_bridge.W_in + assert w_in.shape == (d_model, d_mlp), f"Expected ({d_model}, {d_mlp}), got {w_in.shape}" + + def test_conv1d_w_out_shape_is_tl_convention( + self, conv1d_mlp_bridge: MLPBridge, d_model: int, d_mlp: int + ): + """W_out from Conv1D should be [d_mlp, d_model].""" + w_out = conv1d_mlp_bridge.W_out + assert w_out.shape == (d_mlp, d_model), f"Expected ({d_mlp}, {d_model}), got {w_out.shape}" + + def test_linear_w_in_reproduces_projection( + self, linear_mlp_bridge: MLPBridge, d_model: int, d_mlp: int + ): + """resid @ W_in should match what the wrapped nn.Linear computes.""" + resid = torch.randn(1, 8, d_model) + w_in = linear_mlp_bridge.W_in + + # Compute via TL-style matmul: resid @ W_in + tl_result = resid @ w_in + + # Compute via the wrapped projection + in_bridge = getattr(linear_mlp_bridge, "in") + proj_result = in_bridge.original_component(resid) + + assert torch.allclose(tl_result, proj_result, atol=1e-5), ( + f"resid @ W_in does not match projection output. " + f"Max diff: {(tl_result - proj_result).abs().max()}" + ) + + def test_linear_w_out_reproduces_projection( + self, linear_mlp_bridge: MLPBridge, d_model: int, d_mlp: int + ): + """hidden @ W_out should match what the wrapped nn.Linear computes.""" + hidden = torch.randn(1, 8, d_mlp) + w_out = linear_mlp_bridge.W_out + + # Compute via TL-style matmul: hidden @ W_out + tl_result = hidden @ w_out + + # Compute via the wrapped projection + out_bridge = linear_mlp_bridge.out + proj_result = out_bridge.original_component(hidden) + + assert torch.allclose(tl_result, proj_result, atol=1e-5), ( + f"hidden @ W_out does not match projection output. " + f"Max diff: {(tl_result - proj_result).abs().max()}" + ) + + def test_conv1d_w_in_reproduces_projection( + self, conv1d_mlp_bridge: MLPBridge, d_model: int, d_mlp: int + ): + """resid @ W_in should match what the wrapped Conv1D computes.""" + resid = torch.randn(1, 8, d_model) + w_in = conv1d_mlp_bridge.W_in + + # Compute via TL-style matmul: resid @ W_in + tl_result = resid @ w_in + + # Compute via the wrapped projection + in_bridge = getattr(conv1d_mlp_bridge, "in") + proj_result = in_bridge.original_component(resid) + + assert torch.allclose(tl_result, proj_result, atol=1e-5), ( + f"resid @ W_in does not match projection output. " + f"Max diff: {(tl_result - proj_result).abs().max()}" + ) + + def test_conv1d_w_out_reproduces_projection( + self, conv1d_mlp_bridge: MLPBridge, d_model: int, d_mlp: int + ): + """hidden @ W_out should match what the wrapped Conv1D computes.""" + hidden = torch.randn(1, 8, d_mlp) + w_out = conv1d_mlp_bridge.W_out + + # Compute via TL-style matmul: hidden @ W_out + tl_result = hidden @ w_out + + # Compute via the wrapped projection + out_bridge = conv1d_mlp_bridge.out + proj_result = out_bridge.original_component(hidden) + + assert torch.allclose(tl_result, proj_result, atol=1e-5), ( + f"hidden @ W_out does not match projection output. " + f"Max diff: {(tl_result - proj_result).abs().max()}" + ) + + def test_both_backends_produce_same_orientation( + self, linear_mlp_bridge: MLPBridge, conv1d_mlp_bridge: MLPBridge, d_model: int, d_mlp: int + ): + """nn.Linear and Conv1D bridges should return same shape for W_in/W_out.""" + assert linear_mlp_bridge.W_in.shape == conv1d_mlp_bridge.W_in.shape + assert linear_mlp_bridge.W_out.shape == conv1d_mlp_bridge.W_out.shape + + def test_in_features_fallback_over_shape_heuristic(self, d_model: int): + """When d_mlp < d_model, in_features/out_features should take precedence.""" + d_mlp_small = d_model // 2 # d_mlp < d_model breaks shape heuristic + + class ScaledLinear(nn.Module): + """Bare nn.Module with in_features/out_features (like GIDD's ScaledLinear).""" + + def __init__(self, in_f: int, out_f: int): + super().__init__() + self.in_features = in_f + self.out_features = out_f + # Weight stored as [out, in] like nn.Linear + self.weight = nn.Parameter(torch.randn(out_f, in_f)) + + def forward(self, x): + return x @ self.weight.T + + mlp = MLPBridge(name="mlp") + in_proj = ScaledLinear(d_model, d_mlp_small) + out_proj = ScaledLinear(d_mlp_small, d_model) + + in_bridge = LinearBridge(name="in") + in_bridge.set_original_component(in_proj) + out_bridge = LinearBridge(name="out") + out_bridge.set_original_component(out_proj) + + mlp.add_module("in", in_bridge) + mlp.add_module("out", out_bridge) + + # Even though d_mlp < d_model, should still get TL orientation + assert mlp.W_in.shape == (d_model, d_mlp_small) + assert mlp.W_out.shape == (d_mlp_small, d_model) + + # Verify forward correctness + resid = torch.randn(1, 8, d_model) + tl_result = resid @ mlp.W_in + proj_result = in_proj(resid) + assert torch.allclose(tl_result, proj_result, atol=1e-5) diff --git a/tests/unit/model_bridge/supported_architectures/test_ast_adapter.py b/tests/unit/model_bridge/supported_architectures/test_ast_adapter.py new file mode 100644 index 0000000000..2bb395ea95 --- /dev/null +++ b/tests/unit/model_bridge/supported_architectures/test_ast_adapter.py @@ -0,0 +1,86 @@ +"""Unit tests for ASTArchitectureAdapter.""" + +import pytest +from transformers import ASTConfig, ASTForAudioClassification + +from transformer_lens.config.transformer_bridge_config import TransformerBridgeConfig +from transformer_lens.model_bridge import TransformerBridge +from transformer_lens.model_bridge.supported_architectures.ast import ( + ASTArchitectureAdapter, +) + + +@pytest.fixture(autouse=True) +def prevent_internet_processor_download(monkeypatch): + """Prevent V3 boot from attempting to download a feature extractor named 'ast' from the Hub.""" + monkeypatch.setattr( + "transformers.AutoFeatureExtractor.from_pretrained", lambda *args, **kwargs: None + ) + + +@pytest.fixture(scope="module") +def hf_config(): + """Setup a tiny HF AST config for instantaneous local testing.""" + cfg = ASTConfig( + hidden_size=12, + num_hidden_layers=2, + num_attention_heads=2, + intermediate_size=24, + patch_size=16, + num_labels=2, + ) + # explicitly define the architecture list for the local config + cfg.architectures = ["ASTForAudioClassification"] + return cfg + + +@pytest.fixture(scope="module") +def tl_config(): + """Setup a barebones TransformerBridgeConfig for adapter instantiation test.""" + return TransformerBridgeConfig( + d_model=12, + n_heads=2, + d_head=6, + n_layers=2, + n_ctx=1024, + d_vocab=2, + architecture="ASTForAudioClassification", + ) + + +class TestASTAdapterConfig: + """Anti-drift config assertions.""" + + def test_audio_flags(self, tl_config): + adapter = ASTArchitectureAdapter(tl_config) + assert adapter.cfg.is_audio_model is True + assert adapter.cfg.normalization_type == "LN" + assert ASTArchitectureAdapter.supports_generation is False + + +class TestASTComponentMapping: + """Verify hierarchical mapping names and prefix rebinding.""" + + def test_classification_prefix_rebinding(self, hf_config, tl_config): + model = ASTForAudioClassification(hf_config) + + adapter = ASTArchitectureAdapter(tl_config) + adapter.prepare_model(model) + + assert adapter.component_mapping["blocks"].name == "audio_spectrogram_transformer.layers" + assert adapter.component_mapping["unembed"].name == "classifier.dense" + assert adapter.cfg.d_vocab_out == 2 + + +class TestASTBoot: + """Verify the real load path routes to AutoModelForAudioClassification.""" + + def test_ast_boot_transformers_load_weights_false(self): + # load_weights=False prevents downloading massive model binaries during unit tests + tl_model = TransformerBridge.boot_transformers( + "MIT/ast-finetuned-audioset-10-10-0.4593", load_weights=False + ) + + # strict assertions to guarantee the classification path fired + assert type(tl_model.original_model).__name__ == "ASTForAudioClassification" + assert "unembed" in tl_model.adapter.component_mapping diff --git a/tests/unit/model_bridge/supported_architectures/test_gpt_oss_adapter.py b/tests/unit/model_bridge/supported_architectures/test_gpt_oss_adapter.py index c5f15cbd80..212a6ffebc 100644 --- a/tests/unit/model_bridge/supported_architectures/test_gpt_oss_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_gpt_oss_adapter.py @@ -15,6 +15,7 @@ from typing import Any import pytest +import torch import torch.nn as nn from torch import ones, randn, zeros @@ -142,13 +143,15 @@ class FakeGPTOSSAttention(nn.Module): GQA module: Q is n_heads-wide, K/V are n_key_value_heads-wide, no biases. """ - def __init__(self, cfg: TransformerBridgeConfig) -> None: + def __init__(self, cfg: TransformerBridgeConfig, with_sinks: bool = False) -> None: super().__init__() # PositionEmbeddingsAttentionBridge reads these HF-style attributes during forward. self.head_dim = cfg.d_head self.num_key_value_groups = cfg.n_heads // (cfg.n_key_value_heads or cfg.n_heads) self.scaling = cfg.d_head**-0.5 self.attention_dropout = 0.0 + if with_sinks: + self.sinks = nn.Parameter(randn(cfg.n_heads)) # GQA: Q has n_heads, while K/V have n_key_value_heads. kv_width = (cfg.n_key_value_heads or cfg.n_heads) * cfg.d_head @@ -315,23 +318,7 @@ class TestGPTOSSGQAHookShapes: @pytest.fixture def wired_attn_bridge(self) -> PositionEmbeddingsAttentionBridge: adapter = GPTOSSArchitectureAdapter(_cfg(n_key_value_heads=self.N_KV_HEADS)) - fake_attn = FakeGPTOSSAttention(adapter.cfg) - attn_bridge = _mapping(adapter)["blocks"].submodules["attn"] - assert isinstance(attn_bridge, PositionEmbeddingsAttentionBridge) - attn_bridge.set_original_component(fake_attn) - # A full TransformerBridge build materializes these child bridge modules for us. - # This unit test wires them by hand so it can stay download-free. - for name, original in { - "q": fake_attn.q_proj, - "k": fake_attn.k_proj, - "v": fake_attn.v_proj, - "o": fake_attn.o_proj, - }.items(): - submodule = attn_bridge.submodules[name] - submodule.set_original_component(original) - attn_bridge.add_module(name, submodule) - attn_bridge.setup_hook_compatibility() - return attn_bridge + return _wire_attention(adapter, FakeGPTOSSAttention(adapter.cfg)) def _run_and_capture(self, attn_bridge: PositionEmbeddingsAttentionBridge) -> tuple: captured: dict = {} @@ -440,3 +427,101 @@ def test_no_bias_conversions(self, adapter: GPTOSSArchitectureAdapter) -> None: """GPT-OSS has no biases on any projection.""" for key in _conversions(adapter): assert not key.endswith(".bias") + + +def _wire_attention( + adapter: GPTOSSArchitectureAdapter, fake_attn: FakeGPTOSSAttention +) -> PositionEmbeddingsAttentionBridge: + """Wire a fake HF attention module into the adapter's attention bridge by hand, + so tests stay download-free (a full TransformerBridge build does this for us).""" + attn_bridge = _mapping(adapter)["blocks"].submodules["attn"] + assert isinstance(attn_bridge, PositionEmbeddingsAttentionBridge) + attn_bridge.set_original_component(fake_attn) + for name, original in { + "q": fake_attn.q_proj, + "k": fake_attn.k_proj, + "v": fake_attn.v_proj, + "o": fake_attn.o_proj, + }.items(): + submodule = attn_bridge.submodules[name] + submodule.set_original_component(original) + attn_bridge.add_module(name, submodule) + attn_bridge.setup_hook_compatibility() + return attn_bridge + + +class TestGPTOSSAttentionSinks: + """The reconstructed softmax must carry GPT-OSS's learned sink logits (#1618). + + HF's eager path appends a per-head sink column to the masked scores, + softmaxes over the combined logits, then drops the sink column — so every + real position's weight is scaled down by the sink's share. A softmax + without the sink produces a different pattern at every layer. + """ + + N_HEADS = 4 + N_KV_HEADS = 2 + D_MODEL = 64 + D_HEAD = 16 + BATCH = 2 + SEQ = 8 + + def _make_fake_attn(self, with_sinks: bool) -> FakeGPTOSSAttention: + torch.manual_seed(0) + return FakeGPTOSSAttention(_cfg(n_key_value_heads=self.N_KV_HEADS), with_sinks=with_sinks) + + def _run_bridge(self, fake_attn: FakeGPTOSSAttention) -> tuple: + adapter = GPTOSSArchitectureAdapter(_cfg(n_key_value_heads=self.N_KV_HEADS)) + attn_bridge = _wire_attention(adapter, fake_attn) + torch.manual_seed(1) + hidden = randn(self.BATCH, self.SEQ, self.D_MODEL) + # Identity RoPE keeps the reference computable without rotation math. + cos = ones(1, self.SEQ, self.D_HEAD) + sin = zeros(1, self.SEQ, self.D_HEAD) + out, pattern = attn_bridge(hidden, position_embeddings=(cos, sin)) + return hidden, out, pattern + + def _hf_eager_sink_reference( + self, fake_attn: FakeGPTOSSAttention, hidden: torch.Tensor + ) -> torch.Tensor: + """Replicate HF's gpt-oss eager_attention_forward under identity RoPE.""" + batch, seq = hidden.shape[:2] + q = fake_attn.q_proj(hidden).view(batch, seq, self.N_HEADS, self.D_HEAD).transpose(1, 2) + k = fake_attn.k_proj(hidden).view(batch, seq, self.N_KV_HEADS, self.D_HEAD).transpose(1, 2) + v = fake_attn.v_proj(hidden).view(batch, seq, self.N_KV_HEADS, self.D_HEAD).transpose(1, 2) + groups = self.N_HEADS // self.N_KV_HEADS + k = k.repeat_interleave(groups, dim=1) + v = v.repeat_interleave(groups, dim=1) + scores = (q @ k.transpose(-2, -1)) * fake_attn.scaling + causal = torch.tril(torch.ones(seq, seq, dtype=torch.bool)) + scores = scores.masked_fill(~causal, torch.finfo(scores.dtype).min) + sinks = fake_attn.sinks.reshape(1, -1, 1, 1).expand(batch, -1, seq, -1) + combined = torch.cat([scores, sinks], dim=-1) + combined = combined - combined.max(dim=-1, keepdim=True).values + weights = torch.softmax(combined, dim=-1)[..., :-1] + out = (weights @ v).transpose(1, 2).reshape(batch, seq, self.N_HEADS * self.D_HEAD) + return fake_attn.o_proj(out) + + def test_output_matches_hf_eager_sink_reference(self) -> None: + fake_attn = self._make_fake_attn(with_sinks=True) + hidden, out, _ = self._run_bridge(fake_attn) + with torch.no_grad(): + expected = self._hf_eager_sink_reference(fake_attn, hidden) + assert torch.allclose(out, expected, atol=1e-5), ( + f"bridge attention output drifts {(out - expected).abs().max():.2e} " + "from the HF eager sink reference" + ) + + def test_pattern_rows_leave_mass_for_the_sink(self) -> None: + """With sinks, each pattern row sums to < 1 — the dropped sink keeps its share.""" + fake_attn = self._make_fake_attn(with_sinks=True) + _, _, pattern = self._run_bridge(fake_attn) + row_sums = pattern.sum(dim=-1) + assert (row_sums < 1.0).all(), "some pattern row ignored the sink's share" + + def test_no_sinks_keeps_standard_softmax(self) -> None: + """Without a sinks attribute the softmax must stay a plain row-normalization.""" + fake_attn = self._make_fake_attn(with_sinks=False) + _, _, pattern = self._run_bridge(fake_attn) + row_sums = pattern.sum(dim=-1) + assert torch.allclose(row_sums, torch.ones_like(row_sums), atol=1e-5) diff --git a/tests/unit/model_bridge/supported_architectures/test_qwen3_5_adapter.py b/tests/unit/model_bridge/supported_architectures/test_qwen3_5_adapter.py index 384393e7bd..45ed667952 100644 --- a/tests/unit/model_bridge/supported_architectures/test_qwen3_5_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_qwen3_5_adapter.py @@ -3,6 +3,7 @@ Qwen3_5 is supported only via TransformerBridge, not HookedTransformer. """ +import warnings from types import SimpleNamespace import pytest @@ -614,6 +615,14 @@ def test_bridge_creation(self, bridge): assert isinstance(bridge, TransformerBridge) + def test_bridge_creation_has_no_unresolved_alias_warnings(self): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + _make_tiny_bridge() + + unresolved = [warning for warning in caught if "did not resolve" in str(warning.message)] + assert unresolved == [] + def test_hook_names_present(self, bridge): """blocks.0.attn.* must NOT appear — self_attn is absent on linear-attn layers.""" hook_keys = set(bridge.hook_dict.keys()) diff --git a/tests/unit/model_bridge/supported_architectures/test_starcoder2_adapter.py b/tests/unit/model_bridge/supported_architectures/test_starcoder2_adapter.py index 7718cb1c87..6e021fb113 100644 --- a/tests/unit/model_bridge/supported_architectures/test_starcoder2_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_starcoder2_adapter.py @@ -13,17 +13,23 @@ SUPPORTED_ARCHITECTURES, ) from transformer_lens.model_bridge.generalized_components import ( + BlockBridge, + EmbeddingBridge, GatedMLPBridge, + LinearBridge, MLPBridge, NormalizationBridge, + PositionEmbeddingsAttentionBridge, RMSNormalizationBridge, + RotaryEmbeddingBridge, + UnembeddingBridge, ) from transformer_lens.model_bridge.supported_architectures.starcoder2 import ( Starcoder2ArchitectureAdapter, ) -def _make_cfg() -> TransformerBridgeConfig: +def _make_cfg(n_key_value_heads: int | None = 2) -> TransformerBridgeConfig: return make_bridge_cfg( "Starcoder2ForCausalLM", d_model=64, @@ -33,7 +39,7 @@ def _make_cfg() -> TransformerBridgeConfig: n_heads=4, d_mlp=256, d_vocab=512, - n_key_value_heads=2, + n_key_value_heads=n_key_value_heads, default_prepend_bos=True, ) @@ -59,14 +65,56 @@ def test_qkv_bias_conversions_use_kv_head_count(self, adapter): assert key in conv, f"missing {key}" assert conv[key].tensor_conversion.axes_lengths["h"] == adapter.cfg.n_key_value_heads + def test_conversion_key_set(self, adapter): + """q/k/v get both a weight and a per-head bias conversion; o's bias stays + [d_model], so it must not gain a per-head reshape.""" + assert set(adapter.weight_processing_conversions) == { + "blocks.{i}.attn.q.weight", + "blocks.{i}.attn.k.weight", + "blocks.{i}.attn.v.weight", + "blocks.{i}.attn.o.weight", + "blocks.{i}.attn.q.bias", + "blocks.{i}.attn.k.bias", + "blocks.{i}.attn.v.bias", + } + + def test_missing_kv_heads_falls_back_to_n_heads(self): + """MHA checkpoints omit n_key_value_heads; the bias reshapes must still + be emitted, split by n_heads.""" + adapter = Starcoder2ArchitectureAdapter(_make_cfg(n_key_value_heads=None)) + conv = adapter.weight_processing_conversions + assert conv["blocks.{i}.attn.k.bias"].tensor_conversion.axes_lengths["h"] == 4 + class TestStarcoder2ComponentMapping: + def test_top_level_mapping(self, adapter): + mapping = adapter.component_mapping + assert isinstance(mapping["embed"], EmbeddingBridge) + assert isinstance(mapping["rotary_emb"], RotaryEmbeddingBridge) + assert isinstance(mapping["blocks"], BlockBridge) + assert isinstance(mapping["unembed"], UnembeddingBridge) + assert mapping["embed"].name == "model.embed_tokens" + assert mapping["blocks"].name == "model.layers" + assert mapping["unembed"].name == "lm_head" + + def test_attention_is_separate_qkvo(self, adapter): + """Separate q/k/v/o projections — unlike GPTBigCode's fused c_attn.""" + attn = adapter.component_mapping["blocks"].submodules["attn"] + assert isinstance(attn, PositionEmbeddingsAttentionBridge) + assert attn.name == "self_attn" + expected = {"q": "q_proj", "k": "k_proj", "v": "v_proj", "o": "o_proj"} + assert set(attn.submodules) == set(expected) + for key, hf_name in expected.items(): + assert isinstance(attn.submodules[key], LinearBridge) + assert attn.submodules[key].name == hf_name + def test_norms_are_plain_layernorm(self, adapter): """StarCoder2 uses nn.LayerNorm despite its llama-like shape.""" submodules = adapter.component_mapping["blocks"].submodules - assert isinstance(submodules["ln1"], NormalizationBridge) - assert not isinstance(submodules["ln1"], RMSNormalizationBridge) - assert submodules["ln1"].name == "input_layernorm" + for key, hf_name in (("ln1", "input_layernorm"), ("ln2", "post_attention_layernorm")): + assert isinstance(submodules[key], NormalizationBridge) + assert not isinstance(submodules[key], RMSNormalizationBridge) + assert submodules[key].name == hf_name assert adapter.component_mapping["ln_final"].name == "model.norm" def test_mlp_is_plain_c_fc_c_proj(self, adapter): diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py new file mode 100644 index 0000000000..cdf99ae1a3 --- /dev/null +++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py @@ -0,0 +1,359 @@ +"""Unit tests for ViTArchitectureAdapter. + +Modeled on tests/unit/model_bridge/supported_architectures/test_hubert_adapter.py +(the closest existing analog: HubertArchitectureAdapter also sets a modality flag +— is_audio_model there, is_visual_model here — and rebinds a prefix + injects a +head in prepare_model()). + +Tests cover: +- Component mapping structure for a bare ViTModel/DeiTModel (default, before + prepare_model() sees the real HF model) +- Block/attn/mlp submodule mapping and hook-alias overrides +- Weight conversion key set and rearrange patterns (weights + biases) +- Anti-drift config flags (is_visual_model, positional_embedding_type, + normalization_type, supports_fold_ln, supports_generation) +- prepare_model(): bare model / ViTForImageClassification / DeiTForImageClassification + prefix rebinding + classifier injection, and the DeiTForImageClassificationWithTeacher + guard rail + +NOTE (transformers ViT/DeiT flattening refactor): current transformers removed +the separate `ViTEncoder`/`ViTSelfAttention`/`ViTSelfOutput`/`ViTIntermediate`/ +`ViTOutput` wrapper modules. Blocks now live directly at `.layers` +(not `.encoder.layer`); `ViTAttention`/`DeiTAttention` own `q_proj`/ +`k_proj`/`v_proj`/`o_proj` directly (not nested `attention.query`/`attention.key`/ +`attention.value`/`output.dense`); and `ViTLayer.mlp` is already a flat `ViTMLP` +with `fc1`/`fc2` (not `intermediate.dense`/`output.dense`). All the HF-path +assertions below reflect that. + +NOT covered here (needs a real HF model + real forward pass — see the +integration test at tests/integration/model_bridge/test_vit_adapter.py instead): +- Numeric parity with HF +- Hook firing / cache shapes +- n_ctx propagation: This is cross-checked against a real checkpoint + (google/vit-base-patch16-224 -> n_ctx == 197) in the integration test. +""" + +from types import SimpleNamespace + +import pytest + +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 ( + ParamProcessingConversion, +) +from transformer_lens.model_bridge.generalized_components import ( + AttentionBridge, + BlockBridge, + LinearBridge, + MLPBridge, + NormalizationBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( + VisionClassifierHeadBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_embeddings import ( + VisionEmbeddingsBridge, +) +from transformer_lens.model_bridge.supported_architectures.vit import ( + ViTArchitectureAdapter, +) + +# --------------------------------------------------------------------------- +# Helpers / fixtures +# --------------------------------------------------------------------------- + + +def _make_cfg( + n_heads: int = 12, + d_model: int = 768, + n_layers: int = 12, + d_vocab: int = 1000, + n_ctx: int = 197, + **overrides, +) -> TransformerBridgeConfig: + """Return a minimal TransformerBridgeConfig for ViT/DeiT adapter tests. + + n_ctx=197 is the real value for a 224x224 image / patch16 model + (14*14 patches + 1 CLS token) — used as the *input* cfg here so tests can + tell apart "the adapter left this alone" from "the adapter zeroed it". + """ + cfg = TransformerBridgeConfig( + d_model=d_model, + d_head=d_model // n_heads, + n_heads=n_heads, + n_layers=n_layers, + n_ctx=n_ctx, + d_vocab=d_vocab, + architecture="ViTForImageClassification", + ) + for k, v in overrides.items(): + setattr(cfg, k, v) + return cfg + + +@pytest.fixture() +def adapter() -> ViTArchitectureAdapter: + """Fresh adapter per test — prepare_model() mutates component_mapping in place.""" + return ViTArchitectureAdapter(_make_cfg()) + + +# --------------------------------------------------------------------------- +# Component mapping — bare ViTModel/DeiTModel (default, pre prepare_model()) +# --------------------------------------------------------------------------- + + +class TestViTComponentMappingBare: + """Default mapping assumes no prefix and no classifier (matches __init__).""" + + def test_top_level_keys(self, adapter: ViTArchitectureAdapter) -> None: + assert set(adapter.component_mapping.keys()) == {"embed", "blocks", "ln_final"} + + def test_no_unembed_by_default(self, adapter: ViTArchitectureAdapter) -> None: + assert "unembed" not in adapter.component_mapping + + def test_bridge_types(self, adapter: ViTArchitectureAdapter) -> None: + mapping = adapter.component_mapping + assert isinstance(mapping["embed"], VisionEmbeddingsBridge) + assert isinstance(mapping["blocks"], BlockBridge) + assert isinstance(mapping["ln_final"], NormalizationBridge) + + def test_top_level_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + mapping = adapter.component_mapping + assert mapping["embed"].name == "embeddings" + # No more ViTEncoder wrapper — blocks sit directly on `.layers`. + assert mapping["blocks"].name == "layers" + assert mapping["ln_final"].name == "layernorm" + + def test_block_submodule_keys(self, adapter: ViTArchitectureAdapter) -> None: + assert set(adapter.component_mapping["blocks"].submodules.keys()) == { + "ln1", + "ln2", + "attn", + "mlp", + } + + def test_block_submodule_types(self, adapter: ViTArchitectureAdapter) -> None: + blocks = adapter.component_mapping["blocks"] + assert isinstance(blocks.submodules["ln1"], NormalizationBridge) + assert isinstance(blocks.submodules["ln2"], NormalizationBridge) + assert isinstance(blocks.submodules["attn"], AttentionBridge) + assert isinstance(blocks.submodules["mlp"], MLPBridge) + + def test_block_submodule_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + blocks = adapter.component_mapping["blocks"] + assert blocks.submodules["ln1"].name == "layernorm_before" + assert blocks.submodules["ln2"].name == "layernorm_after" + assert blocks.submodules["attn"].name == "attention" + assert blocks.submodules["mlp"].name == "mlp" + + def test_attn_submodule_keys(self, adapter: ViTArchitectureAdapter) -> None: + attn = adapter.component_mapping["blocks"].submodules["attn"] + assert set(attn.submodules.keys()) == {"q", "k", "v", "o"} + + def test_attn_qkvo_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + """Paths here are relative to the already-resolved "attn" bridge module + (block.attention). ViTAttention/DeiTAttention now owns q_proj/k_proj/ + v_proj/o_proj directly (flattened, no nested self-attention + separate + output.dense module), so no "attention." prefix.""" + attn = adapter.component_mapping["blocks"].submodules["attn"] + assert attn.submodules["q"].name == "q_proj" + assert attn.submodules["k"].name == "k_proj" + assert attn.submodules["v"].name == "v_proj" + assert attn.submodules["o"].name == "o_proj" + + def test_attn_submodules_are_linear_bridges(self, adapter: ViTArchitectureAdapter) -> None: + attn = adapter.component_mapping["blocks"].submodules["attn"] + for sub in attn.submodules.values(): + assert isinstance(sub, LinearBridge) + + def test_mlp_submodule_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + """Paths here are relative to the already-resolved "mlp" bridge module + (block.mlp). ViTLayer.mlp is a real ViTMLP now (fc1/fc2 directly) — no + more intermediate/output split, so no wrapper shim is needed.""" + mlp = adapter.component_mapping["blocks"].submodules["mlp"] + assert mlp.submodules["in"].name == "fc1" + assert mlp.submodules["out"].name == "fc2" + + def test_mlp_hook_alias_overrides(self, adapter: ViTArchitectureAdapter) -> None: + aliases = adapter.component_mapping["blocks"].hook_aliases + assert aliases.get("hook_mlp_out") == "mlp.out.hook_out" + assert aliases.get("hook_mlp_in") == "mlp.in.hook_in" + + +# --------------------------------------------------------------------------- +# Anti-drift config flags +# --------------------------------------------------------------------------- + + +class TestViTAdapterConfig: + """Flags that must not silently regress.""" + + def test_normalization_type_is_ln(self, adapter: ViTArchitectureAdapter) -> None: + assert adapter.cfg.normalization_type == "LN" + + def test_positional_embedding_type_is_standard(self, adapter: ViTArchitectureAdapter) -> None: + assert adapter.cfg.positional_embedding_type == "standard" + + def test_is_visual_model_true(self, adapter: ViTArchitectureAdapter) -> None: + """Only vision adapters set this flag; text/audio adapters must not.""" + assert adapter.cfg.is_visual_model is True + + def test_is_audio_model_not_set_true(self, adapter: ViTArchitectureAdapter) -> None: + assert getattr(adapter.cfg, "is_audio_model", False) is False + + def test_final_rms_is_false(self, adapter: ViTArchitectureAdapter) -> None: + assert adapter.cfg.final_rms is False + + def test_gated_mlp_is_false(self, adapter: ViTArchitectureAdapter) -> None: + assert adapter.cfg.gated_mlp is False + + def test_attn_only_is_false(self, adapter: ViTArchitectureAdapter) -> None: + assert adapter.cfg.attn_only is False + + def test_supports_generation_is_false(self) -> None: + assert ViTArchitectureAdapter.supports_generation is False + + def test_supports_fold_ln_true_pre_ln(self, adapter: ViTArchitectureAdapter) -> None: + """ViT/DeiT are pre-LN — unlike BertArchitectureAdapter's post-LN False.""" + assert adapter.supports_fold_ln is True + + +# --------------------------------------------------------------------------- +# Weight processing conversions +# --------------------------------------------------------------------------- + + +class TestViTWeightConversions: + def test_exact_conversion_key_set(self, adapter: ViTArchitectureAdapter) -> None: + assert set(adapter.weight_processing_conversions.keys()) == { + "blocks.{i}.attn.q.weight", + "blocks.{i}.attn.k.weight", + "blocks.{i}.attn.v.weight", + "blocks.{i}.attn.q.bias", + "blocks.{i}.attn.k.bias", + "blocks.{i}.attn.v.bias", + "blocks.{i}.attn.o.weight", + } + + def test_qkv_weight_pattern(self, adapter: ViTArchitectureAdapter) -> None: + for slot in ("q", "k", "v"): + conv = adapter.weight_processing_conversions[f"blocks.{{i}}.attn.{slot}.weight"] + assert isinstance(conv, ParamProcessingConversion) + assert isinstance(conv.tensor_conversion, RearrangeTensorConversion) + assert conv.tensor_conversion.pattern == "(h d_head) d_model -> h d_model d_head" + + def test_qkv_bias_pattern(self, adapter: ViTArchitectureAdapter) -> None: + for slot in ("q", "k", "v"): + conv = adapter.weight_processing_conversions[f"blocks.{{i}}.attn.{slot}.bias"] + assert isinstance(conv, ParamProcessingConversion) + assert isinstance(conv.tensor_conversion, RearrangeTensorConversion) + assert conv.tensor_conversion.pattern == "(h d_head) -> h d_head" + + def test_o_weight_pattern(self, adapter: ViTArchitectureAdapter) -> None: + conv = adapter.weight_processing_conversions["blocks.{i}.attn.o.weight"] + assert isinstance(conv, ParamProcessingConversion) + assert isinstance(conv.tensor_conversion, RearrangeTensorConversion) + assert conv.tensor_conversion.pattern == "d_model (h d_head) -> h d_head d_model" + + def test_qkv_weight_head_axis(self, adapter: ViTArchitectureAdapter) -> None: + for slot in ("q", "k", "v"): + conv = adapter.weight_processing_conversions[f"blocks.{{i}}.attn.{slot}.weight"] + assert conv.tensor_conversion.axes_lengths["h"] == 12 + + def test_qkv_bias_head_axis(self, adapter: ViTArchitectureAdapter) -> None: + for slot in ("q", "k", "v"): + conv = adapter.weight_processing_conversions[f"blocks.{{i}}.attn.{slot}.bias"] + assert conv.tensor_conversion.axes_lengths["h"] == 12 + + +# --------------------------------------------------------------------------- +# prepare_model() — prefix rebinding + classifier injection +# --------------------------------------------------------------------------- + + +class TestViTPrepareModel: + """prepare_model() detects bare/ViT/DeiT wrappers and rebuilds the mapping.""" + + def _bare_model(self) -> object: + """No 'vit'/'deit'/'classifier' attribute — mimics bare ViTModel/DeiTModel.""" + return SimpleNamespace() + + def _vit_for_classification(self) -> object: + return SimpleNamespace(vit=SimpleNamespace(), classifier=SimpleNamespace()) + + def _deit_for_classification(self) -> object: + return SimpleNamespace(deit=SimpleNamespace(), classifier=SimpleNamespace()) + + def _deit_with_teacher(self) -> object: + """Dual cls_classifier + distillation_classifier — explicitly unsupported.""" + return SimpleNamespace( + deit=SimpleNamespace(), + cls_classifier=SimpleNamespace(), + distillation_classifier=SimpleNamespace(), + ) + + # -- bare model -------------------------------------------------------- + + def test_bare_model_keeps_no_prefix(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._bare_model()) + assert adapter.component_mapping["embed"].name == "embeddings" + assert adapter.component_mapping["blocks"].name == "layers" + assert adapter.component_mapping["ln_final"].name == "layernorm" + + def test_bare_model_has_no_unembed(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._bare_model()) + assert "unembed" not in adapter.component_mapping + + def test_bare_model_does_not_require_encoder_attribute( + self, adapter: ViTArchitectureAdapter + ) -> None: + """Regression test: prepare_model() must not assume a `.encoder` + wrapper module exists on the HF model. Current transformers removed + it — blocks live directly on `.layers` — so a minimal stub + with no `.encoder` attribute at all must still work.""" + hf_model = SimpleNamespace() + assert not hasattr(hf_model, "encoder") + adapter.prepare_model(hf_model) # must not raise AttributeError + + # -- ViTForImageClassification ----------------------------------------- + + def test_vit_classification_adds_vit_prefix(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._vit_for_classification()) + assert adapter.component_mapping["embed"].name == "vit.embeddings" + assert adapter.component_mapping["blocks"].name == "vit.layers" + assert adapter.component_mapping["ln_final"].name == "vit.layernorm" + + def test_vit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._vit_for_classification()) + assert "unembed" in adapter.component_mapping + assert isinstance(adapter.component_mapping["unembed"], VisionClassifierHeadBridge) + + def test_classifier_is_never_prefixed(self, adapter: ViTArchitectureAdapter) -> None: + """classifier is always a top-level attr on the *ForImageClassification wrapper.""" + adapter.prepare_model(self._vit_for_classification()) + assert adapter.component_mapping["unembed"].name == "classifier" + + # -- DeiTForImageClassification ------------------------------------------ + + def test_deit_classification_adds_deit_prefix(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._deit_for_classification()) + assert adapter.component_mapping["embed"].name == "deit.embeddings" + assert adapter.component_mapping["blocks"].name == "deit.layers" + assert adapter.component_mapping["ln_final"].name == "deit.layernorm" + + def test_deit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._deit_for_classification()) + assert adapter.component_mapping["unembed"].name == "classifier" + + # -- DeiTForImageClassificationWithTeacher guard rail -------------------- + + def test_deit_with_teacher_raises(self, adapter: ViTArchitectureAdapter) -> None: + with pytest.raises(NotImplementedError): + adapter.prepare_model(self._deit_with_teacher()) + + def test_deit_with_teacher_message_mentions_reason( + self, adapter: ViTArchitectureAdapter + ) -> None: + with pytest.raises(NotImplementedError, match="distillation_classifier"): + adapter.prepare_model(self._deit_with_teacher()) diff --git a/tests/unit/model_bridge/test_get_params_util.py b/tests/unit/model_bridge/test_get_params_util.py index 469f42a2eb..97362dd2d2 100644 --- a/tests/unit/model_bridge/test_get_params_util.py +++ b/tests/unit/model_bridge/test_get_params_util.py @@ -230,6 +230,7 @@ def _create_mock_bridge_with_gate_weights(self): 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) return mock_bridge @@ -255,8 +256,10 @@ def _create_mock_block(self): block.attn.o.weight = torch.randn(768, 768) block.attn.o.bias = torch.randn(768) - # Mock MLP + # 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) diff --git a/tests/unit/model_registry/test_vision_phase_routing.py b/tests/unit/model_registry/test_vision_phase_routing.py new file mode 100644 index 0000000000..907a4131d4 --- /dev/null +++ b/tests/unit/model_registry/test_vision_phase_routing.py @@ -0,0 +1,68 @@ +"""Vision encoders must route to the {1, 9} verification set. + +Vision architectures have no tokenizer and no HookedTransformer counterpart, so the +default text phase set ({1,2,3,4}) cannot run against them. They previously opted out +of verification entirely via ``applicable_phases = []``, which made them unverifiable +rather than verified — Phase 1 (HF parity on pixel input) and Phase 9 (pixel +forward/cache/stability) do apply. +""" + +import pytest + +from transformer_lens.tools.model_registry.verify_models import ( + _full_and_core_phases, + _phases_to_run, +) +from transformer_lens.utilities.architectures import ( + NO_HT_COMPARISON_ARCHITECTURES, + classify_architecture, +) + +VISION_ARCHS = [ + "ViTForImageClassification", + "ViTModel", + "DeiTForImageClassification", +] + + +@pytest.mark.parametrize("architecture", VISION_ARCHS) +def test_vision_architectures_classify_as_vision(architecture: str) -> None: + assert classify_architecture(architecture) == "vision" + + +@pytest.mark.parametrize("architecture", VISION_ARCHS) +def test_vision_runs_phases_one_and_nine(architecture: str) -> None: + """HF parity (P1) plus vision hook/cache tests (P9) are the whole story.""" + full, core = _full_and_core_phases(architecture) + assert full == {1, 9} + assert core == {1, 9} + + +@pytest.mark.parametrize("architecture", VISION_ARCHS) +def test_vision_phases_survive_adapter_filter(architecture: str) -> None: + """The adapter's applicable_phases must not filter Phase 1 or 9 back out. + + An adapter declaring applicable_phases=[] leaves _phases_to_run empty, which + verify_models treats as "coverage deferred" and skips the model outright. + Phase 9 bypasses the filter like 7/8 (gated by is_visual_model instead). + """ + assert _phases_to_run(architecture, [1, 9]) == [1, 9] + + +@pytest.mark.parametrize("architecture", VISION_ARCHS) +def test_vision_skips_hooked_transformer_comparison(architecture: str) -> None: + """There is no HookedTransformer vision encoder to compare against.""" + assert architecture in NO_HT_COMPARISON_ARCHITECTURES + + +def test_audio_still_routes_to_phase_eight() -> None: + """Vision routing must not disturb the existing audio phase set.""" + assert _full_and_core_phases("ASTForAudioClassification") == ({1, 8}, {1, 8}) + assert _full_and_core_phases("HubertForCTC") == ({1, 8}, {1, 8}) + + +def test_text_still_routes_to_all_four_phases() -> None: + """Nor the default text phase set.""" + full, core = _full_and_core_phases("GPT2LMHeadModel") + assert full == {1, 2, 3, 4} + assert core == {1, 4} diff --git a/tests/unit/pretrained_weight_conversions/test_olmo3.py b/tests/unit/pretrained_weight_conversions/test_olmo3.py index d54efef83c..de1ecc61e5 100644 --- a/tests/unit/pretrained_weight_conversions/test_olmo3.py +++ b/tests/unit/pretrained_weight_conversions/test_olmo3.py @@ -12,6 +12,7 @@ import torch import torch.nn as nn +from transformer_lens import HookedTransformer from transformer_lens.config.hooked_transformer_config import HookedTransformerConfig from transformer_lens.pretrained.weight_conversions.olmo3 import convert_olmo3_weights @@ -19,7 +20,7 @@ def get_olmo3_config( n_layers: int = 2, use_qk_norm: bool = True, - n_key_value_heads: int = 1, + n_key_value_heads: int | None = 1, ): """Create an OLMo 3 style config for testing.""" return HookedTransformerConfig( @@ -177,21 +178,40 @@ def test_qk_norm_weights_extracted(self): class TestOlmo3GQA: - """Test Grouped Query Attention (GQA) handling.""" + """Test Grouped Query Attention (GQA) handling. + + The prefix must match TransformerBlock's attention choice: it builds + GroupedQueryAttention (underscore-prefixed _W_K/_W_V) whenever + n_key_value_heads is set — even when it equals n_heads (#1620). The + previous expectation of un-prefixed keys for that case zero-filled K/V + on checkpoints like allenai/Olmo-3-1025-7B (32 kv heads == 32 heads). + """ + + def test_kv_heads_equal_to_n_heads_keeps_underscore_prefix(self): + """n_key_value_heads == n_heads still instantiates GQA, so keys need the prefix.""" + cfg = get_olmo3_config(n_key_value_heads=2) # equals n_heads + model = MockOlmo3Model(cfg) + + state_dict = convert_olmo3_weights(model, cfg) - def test_no_gqa_no_underscore_prefix(self): - """Test that non-GQA models don't have underscore prefix.""" - # Note: OLMo 3 always uses GQA (n_key_value_heads < n_heads) - # This test documents the expected behavior for MHA models - cfg = get_olmo3_config(n_key_value_heads=2) # MHA (n_kv_heads == n_heads) + for layer in range(cfg.n_layers): + assert f"blocks.{layer}.attn._W_K" in state_dict + assert f"blocks.{layer}.attn._W_V" in state_dict + assert f"blocks.{layer}.attn.W_K" not in state_dict + assert f"blocks.{layer}.attn.W_V" not in state_dict + + def test_no_kv_heads_means_plain_mha_keys(self): + """Without n_key_value_heads the block builds plain Attention — no prefix.""" + cfg = get_olmo3_config(n_key_value_heads=None) model = MockOlmo3Model(cfg) state_dict = convert_olmo3_weights(model, cfg) for layer in range(cfg.n_layers): - # MHA should not have underscore prefix assert f"blocks.{layer}.attn.W_K" in state_dict assert f"blocks.{layer}.attn.W_V" in state_dict + assert f"blocks.{layer}.attn._W_K" not in state_dict + assert f"blocks.{layer}.attn._W_V" not in state_dict # ============================================================================ @@ -272,3 +292,109 @@ def test_embedding_shapes(self): assert state_dict["embed.W_E"].shape == (cfg.d_vocab, cfg.d_model) assert state_dict["unembed.W_U"].shape == (cfg.d_model, cfg.d_vocab) + + +# ============================================================================ +# Test: Converted dict loads into HookedTransformer (#1620 regression) +# ============================================================================ + + +class TestOlmo3StateDictLoads: + """The converted dict must feed the instantiated attention without + zero-filling K/V — the #1620 failure mode on kv_heads == n_heads.""" + + def test_converted_dict_loads_with_nonzero_kv(self): + cfg = get_olmo3_config(n_layers=1, n_key_value_heads=2) # equals n_heads + source_model = MockOlmo3Model(cfg) + state_dict = convert_olmo3_weights(source_model, cfg) + + model = HookedTransformer(cfg) + model.load_and_process_state_dict( + state_dict, + fold_ln=False, + center_writing_weights=False, + center_unembed=False, + fold_value_biases=False, + ) + + for block in model.blocks: + assert block.attn.W_K.abs().max() > 0, "K weights were zero-filled (#1620)" + assert block.attn.W_V.abs().max() > 0, "V weights were zero-filled (#1620)" + + +class TestOlmo3HookedTransformerParity: + """End-to-end HT-vs-HF logit parity on a tiny random Olmo-3. + + Olmo-3 declares per-layer-type rope in transformers 5.x — plain rope on + sliding_attention layers, YARN on full_attention layers — and the old + mapping read the defunct `rope_scaling` attribute, silently applying plain + rope everywhere and never wiring the sliding-window mask. This locks in + the per-type mapping via the production config path. + """ + + def test_tiny_model_logit_parity(self, tmp_path): + from transformers import AutoModelForCausalLM, Olmo3Config, Olmo3ForCausalLM + + from transformer_lens.loading_from_pretrained import convert_hf_model_config + + hf_config = Olmo3Config( + hidden_size=64, + num_hidden_layers=4, + num_attention_heads=4, + num_key_value_heads=4, # kv == heads, the #1620 trigger + intermediate_size=32, + vocab_size=256, + max_position_embeddings=256, + sliding_window=4, # < seq len so the sliding-window mask actually bites + layer_types=[ + "sliding_attention", + "sliding_attention", + "full_attention", + "sliding_attention", + ], + rope_parameters={ + "sliding_attention": {"rope_type": "default", "rope_theta": 500000.0}, + "full_attention": { + "rope_type": "yarn", + "rope_theta": 500000.0, + "factor": 8.0, + "attention_factor": 1.2079441541679836, + "beta_fast": 32, + "beta_slow": 1, + "original_max_position_embeddings": 32, + }, + }, + ) + torch.manual_seed(0) + hf_model = Olmo3ForCausalLM(hf_config).eval().float() + hf_model.save_pretrained(tmp_path) + + ids = torch.tensor([[5, 17, 29, 3, 11, 42, 7, 23]]) + reference = AutoModelForCausalLM.from_pretrained( + str(tmp_path), dtype=torch.float32, attn_implementation="eager" + ).eval() + with torch.inference_mode(): + ref_logits = reference(ids).logits + + cfg_dict = convert_hf_model_config(str(tmp_path)) + assert cfg_dict["use_yarn_rope"] is True + assert cfg_dict["yarn_global_attn_only"] is True + assert cfg_dict["use_local_attn"] is True + assert cfg_dict["window_size"] == 4 + cfg_dict["dtype"] = torch.float32 + cfg_dict["tokenizer_name"] = None # keep the test download-free + + cfg = HookedTransformerConfig.from_dict(cfg_dict) + model = HookedTransformer(cfg) + model.load_and_process_state_dict( + convert_olmo3_weights(hf_model, cfg), + fold_ln=False, + center_writing_weights=False, + center_unembed=False, + fold_value_biases=False, + ) + with torch.inference_mode(): + ht_logits = model(ids, return_type="logits") + + max_diff = (ref_logits - ht_logits).abs().max().item() + assert max_diff < 1e-4, f"HT vs HF logit drift {max_diff:.2e}" diff --git a/tests/unit/pretrained_weight_conversions/test_openai.py b/tests/unit/pretrained_weight_conversions/test_openai.py index bdaf9a2e54..25e0536a00 100644 --- a/tests/unit/pretrained_weight_conversions/test_openai.py +++ b/tests/unit/pretrained_weight_conversions/test_openai.py @@ -1,3 +1,4 @@ +import pytest import torch from transformer_lens.config.hooked_transformer_config import HookedTransformerConfig @@ -55,6 +56,7 @@ def make_mock_model(cfg): layer.post_attention_layernorm.weight = torch.randn(d) # Attention + layer.self_attn.sinks = torch.randn(n_heads) layer.self_attn.q_proj.weight = torch.randn(n_heads * d_head, d) layer.self_attn.k_proj.weight = torch.randn(n_kv * d_head, d) layer.self_attn.v_proj.weight = torch.randn(n_kv * d_head, d) @@ -193,3 +195,108 @@ def test_state_dict_completeness(): "W_out.bias", ]: assert f"blocks.0.mlp.experts.{e}.{name}" in state_dict + + +class Tensor: + """Mimics triton_kernels.tensor.Tensor: named "Tensor", not a torch.Tensor, + not subscriptable — what packed-MXFP4 gpt-oss checkpoints expose (#1619).""" + + def __init__(self, shape): + self.shape = shape + self.dtype = "mxfp4" + self.data = object() + self.storage = object() + + +def test_packed_mxfp4_experts_raise_legible_error(): + """Packed MXFP4 expert weights must raise naming MXFP4, not a bare TypeError.""" + cfg = make_cfg() + model = make_mock_model(cfg) + model.model.layers[0].mlp.experts.gate_up_proj = Tensor( + (cfg.num_experts, cfg.d_model, 2 * cfg.d_mlp) + ) + + with pytest.raises(NotImplementedError, match="MXFP4"): + convert_gpt_oss_weights(model, cfg) + + +def test_sinks_are_converted(): + """The learned attention-sink logits must reach the state dict (#1619 follow-up).""" + cfg = make_cfg() + model = make_mock_model(cfg) + + state_dict = convert_gpt_oss_weights(model, cfg) + + assert torch.equal(state_dict["blocks.0.attn.sinks"], model.model.layers[0].self_attn.sinks) + + +class TestGptOssHookedTransformerParity: + """End-to-end HT-vs-HF logit parity on a tiny random GPT-OSS. + + Loading used to crash on MXFP4 (#1619); once past that, HookedTransformer + silently disagreed with HF because its attention lacked sinks, sliding-window + layers, and yarn RoPE with truncate=False. This locks in all three via the + production config-mapping path (convert_hf_model_config on a local dir). + """ + + def test_tiny_model_logit_parity(self, tmp_path): + from transformers import AutoModelForCausalLM, GptOssConfig, GptOssForCausalLM + + from transformer_lens import HookedTransformer + from transformer_lens.loading_from_pretrained import convert_hf_model_config + + hf_config = GptOssConfig( + hidden_size=64, + num_hidden_layers=2, # one sliding_attention + one full_attention layer + num_attention_heads=4, + num_key_value_heads=2, + head_dim=16, + intermediate_size=32, + num_local_experts=4, + num_experts_per_tok=2, + vocab_size=256, + max_position_embeddings=128, + sliding_window=4, # < seq len so the sliding-window mask actually bites + rope_parameters={ + "rope_type": "yarn", + "rope_theta": 150000.0, + "factor": 4.0, + "original_max_position_embeddings": 32, + "beta_fast": 32.0, + "beta_slow": 1.0, + "truncate": False, + }, + ) + torch.manual_seed(0) + hf_model = GptOssForCausalLM(hf_config).eval().float() + hf_model.save_pretrained(tmp_path) + + ids = torch.tensor([[5, 17, 29, 3, 11, 42, 7, 23]]) + reference = AutoModelForCausalLM.from_pretrained( + str(tmp_path), dtype=torch.float32, attn_implementation="eager" + ).eval() + with torch.inference_mode(): + ref_logits = reference(ids).logits + + cfg_dict = convert_hf_model_config(str(tmp_path)) + assert cfg_dict["use_attention_sinks"] is True + assert cfg_dict["attn_types"] == ["local", "global"] + assert cfg_dict["use_yarn_rope"] is True + assert cfg_dict["yarn_truncate"] is False + cfg_dict["dtype"] = torch.float32 + cfg_dict["tokenizer_name"] = None # keep the test download-free + + cfg = HookedTransformerConfig.from_dict(cfg_dict) + model = HookedTransformer(cfg) + model.load_and_process_state_dict( + convert_gpt_oss_weights(hf_model, cfg), + fold_ln=False, + center_writing_weights=False, + center_unembed=False, + fold_value_biases=False, + ) + with torch.inference_mode(): + ht_logits = model(ids, return_type="logits") + + max_diff = (ref_logits - ht_logits).abs().max().item() + assert max_diff < 1e-4, f"HT vs HF logit drift {max_diff:.2e}" diff --git a/tests/unit/test_deprecation_warnings.py b/tests/unit/test_deprecation_warnings.py new file mode 100644 index 0000000000..d5df94ef0b --- /dev/null +++ b/tests/unit/test_deprecation_warnings.py @@ -0,0 +1,111 @@ +"""Regression coverage for deprecated legacy entry points.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +PROJECT_ROOT = Path(__file__).parents[2] + + +def _small_config(): + from transformer_lens import HookedTransformerConfig + + return HookedTransformerConfig( + n_layers=1, + d_model=16, + d_head=4, + n_heads=4, + n_ctx=8, + d_vocab=20, + attn_only=True, + ) + + +def _assert_single_deprecation(constructor, class_name: str) -> None: + with pytest.warns(DeprecationWarning, match=class_name) as caught: + constructor() + + assert len(caught) == 1 + assert "TransformerBridge.boot_transformers" in str(caught[0].message) + assert "4.0" in str(caught[0].message) + + +def test_importing_transformer_lens_emits_no_deprecation_warning(): + code = "\n".join( + [ + "import warnings", + "warnings.filterwarnings(", + " 'error',", + " category=DeprecationWarning,", + " module=r'^transformer_lens(?:\\.|$)',", + ")", + "import transformer_lens", + ] + ) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + cwd=PROJECT_ROOT, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + + +def test_hooked_transformer_constructor_warns_once(): + from transformer_lens import HookedTransformer + + _assert_single_deprecation(lambda: HookedTransformer(_small_config()), "HookedTransformer") + + +def test_hooked_encoder_constructor_warns_once(): + from transformer_lens import HookedEncoder + + _assert_single_deprecation(lambda: HookedEncoder(_small_config()), "HookedEncoder") + + +def test_hooked_encoder_from_pretrained_warning_reaches_external_caller(): + code = "\n".join( + [ + "import importlib", + "hooked_encoder_module = importlib.import_module('transformer_lens.HookedEncoder')", + "HookedEncoder = hooked_encoder_module.HookedEncoder", + "def stop_loading(*args, **kwargs):", + " raise RuntimeError('stop after deprecation warning')", + "hooked_encoder_module.loading.get_official_model_name = stop_loading", + "try:", + " HookedEncoder.from_pretrained('bert-base-cased')", + "except RuntimeError as error:", + " assert str(error) == 'stop after deprecation warning'", + ] + ) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + cwd=PROJECT_ROOT, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert ":" in result.stderr + assert "DeprecationWarning: HookedEncoder.from_pretrained is deprecated" in result.stderr + + +def test_bert_next_sentence_prediction_constructor_warns_once(): + from transformer_lens import BertNextSentencePrediction + + _assert_single_deprecation( + lambda: BertNextSentencePrediction(object()), "BertNextSentencePrediction" + ) + + +def test_direct_hooked_root_module_construction_warns_once(): + from transformer_lens import HookedRootModule + + _assert_single_deprecation(HookedRootModule, "HookedRootModule") diff --git a/tests/unit/test_loading_from_pretrained_utilities.py b/tests/unit/test_loading_from_pretrained_utilities.py index 659552b827..2a4f737d7b 100644 --- a/tests/unit/test_loading_from_pretrained_utilities.py +++ b/tests/unit/test_loading_from_pretrained_utilities.py @@ -6,6 +6,7 @@ from transformer_lens import HookedTransformer from transformer_lens.config import HookedTransformerConfig from transformer_lens.loading_from_pretrained import ( + _mxfp4_dequantize_config, fill_missing_keys, get_pretrained_model_config, ) @@ -17,27 +18,101 @@ def get_default_config(): ) +def _config_with_architecture(architecture: str) -> HookedTransformerConfig: + return HookedTransformerConfig( + d_model=128, + d_head=8, + n_heads=16, + n_ctx=128, + n_layers=1, + d_vocab=50257, + attn_only=True, + original_architecture=architecture, + ) + + +class TestMxfp4DequantizeConfig: + """Packed-MXFP4 gpt-oss checkpoints must load dequantized so the weight + converter sees plain torch.Tensors instead of triton-kernels wrappers (#1619).""" + + @mock.patch("transformer_lens.loading_from_pretrained.AutoConfig") + def test_mxfp4_gpt_oss_gets_dequantize_config(self, mock_auto_config: mock.MagicMock): + mock_auto_config.from_pretrained.return_value = SimpleNamespace( + quantization_config={"quant_method": "mxfp4"} + ) + result = _mxfp4_dequantize_config( + "openai/gpt-oss-20b", _config_with_architecture("GptOssForCausalLM"), None + ) + assert result is not None + assert result.dequantize is True + + @mock.patch("transformer_lens.loading_from_pretrained.AutoConfig") + def test_object_style_quantization_config(self, mock_auto_config: mock.MagicMock): + mock_auto_config.from_pretrained.return_value = SimpleNamespace( + quantization_config=SimpleNamespace(quant_method="mxfp4") + ) + result = _mxfp4_dequantize_config( + "openai/gpt-oss-20b", _config_with_architecture("GptOssForCausalLM"), None + ) + assert result is not None + assert result.dequantize is True + + @mock.patch("transformer_lens.loading_from_pretrained.AutoConfig") + def test_unquantized_gpt_oss_finetune_untouched(self, mock_auto_config: mock.MagicMock): + mock_auto_config.from_pretrained.return_value = SimpleNamespace() + result = _mxfp4_dequantize_config( + "someone/gpt-oss-finetune-bf16", _config_with_architecture("GptOssForCausalLM"), None + ) + assert result is None + + @mock.patch("transformer_lens.loading_from_pretrained.AutoConfig") + def test_non_gpt_oss_skips_config_fetch(self, mock_auto_config: mock.MagicMock): + result = _mxfp4_dequantize_config( + "gpt2", _config_with_architecture("GPT2LMHeadModel"), None + ) + assert result is None + mock_auto_config.from_pretrained.assert_not_called() + + # Successes @mock.patch("logging.warning") def test_fill_missing_keys(mock_warning: mock.MagicMock): + """Non-attention weight matrices still warn-and-fill (attention ones raise instead).""" cfg = get_default_config() model = HookedTransformer(cfg) default_state_dict = model.state_dict() - incomplete_state_dict = {k: v for k, v in default_state_dict.items() if "W_" not in k} + non_attention_weights = {"embed.W_E", "pos_embed.W_pos", "unembed.W_U"} + assert non_attention_weights <= set(default_state_dict.keys()) + incomplete_state_dict = { + k: v for k, v in default_state_dict.items() if k not in non_attention_weights + } filled_state_dict = fill_missing_keys(model, incomplete_state_dict) assert set(filled_state_dict.keys()) == set(default_state_dict.keys()) - # Check that warnings were issued for missing weight matrices - for key in default_state_dict: - if "W_" in key and key not in incomplete_state_dict: - mock_warning.assert_any_call( - f"Missing key for a weight matrix in pretrained, filled in with an empty tensor: {key}" - ) + for key in non_attention_weights: + mock_warning.assert_any_call( + f"Missing key for a weight matrix in pretrained, filled in with an empty tensor: {key}" + ) + + +def test_fill_missing_keys_raises_on_missing_attention_weights(): + """A missing attention W means converter/component naming mismatch (#1620) — + zero-filling it silently breaks the model, so it must raise instead.""" + cfg = get_default_config() + model = HookedTransformer(cfg) + default_state_dict = model.state_dict() + + incomplete_state_dict = { + k: v for k, v in default_state_dict.items() if not k.endswith("attn.W_K") + } + + with pytest.raises(ValueError, match="attention weight matrices"): + fill_missing_keys(model, incomplete_state_dict) def test_fill_missing_keys_with_hf_model_keys(): diff --git a/tests/unit/test_optional_submodule.py b/tests/unit/test_optional_submodule.py index 80610540c6..055a670307 100644 --- a/tests/unit/test_optional_submodule.py +++ b/tests/unit/test_optional_submodule.py @@ -2,6 +2,7 @@ import copy import logging +import warnings import pytest import torch @@ -30,6 +31,8 @@ class HybridLayer(nn.Module): def __init__(self, has_foo: bool, dim: int = 4): super().__init__() self.bar = nn.Linear(dim, dim, bias=False) + self.ln2 = nn.Linear(dim, dim, bias=False) + self.mlp = FakeSubmodule(dim) if has_foo: self.foo = FakeSubmodule(dim) @@ -80,11 +83,52 @@ def make_block_template(self) -> BlockBridge: name="layers", submodules={ "bar": LinearBridge(name="bar"), + "ln2": LinearBridge(name="ln2"), + "mlp": LinearBridge(name="mlp"), "attn": LinearBridge(name="foo", optional=True), }, ) +class SsmAdapter(ArchitectureAdapter): + """Uses an optional SSM submodule with a custom hook alias.""" + + def __init__(self): + self.cfg = type("Cfg", (), {"n_layers": 4, "d_model": 4})() + self.component_mapping = {} + + def make_block_template(self) -> BlockBridge: + return BlockBridge( + name="layers", + submodules={ + "bar": LinearBridge(name="bar"), + "ln2": LinearBridge(name="ln2"), + "mlp": LinearBridge(name="mlp"), + "ssm": LinearBridge(name="foo", optional=True), + }, + hook_alias_overrides={"hook_ssm_out": "ssm.hook_out"}, + ) + + +class PostNormAdapter(ArchitectureAdapter): + """Uses an optional post-MLP norm with the BlockBridge MLP fallback.""" + + def __init__(self): + self.cfg = type("Cfg", (), {"n_layers": 4, "d_model": 4})() + self.component_mapping = {} + + def make_block_template(self) -> BlockBridge: + return BlockBridge( + name="layers", + submodules={ + "bar": LinearBridge(name="bar"), + "ln2": LinearBridge(name="ln2"), + "ln2_post": LinearBridge(name="foo", optional=True), + "mlp": LinearBridge(name="mlp"), + }, + ) + + # -- Bridge construction helpers ---------------------------------------------- @@ -157,6 +201,45 @@ def test_non_optional_raises(self): with pytest.raises(AttributeError): setup_submodules(block, adapter, model.layers[3]) + def test_prunes_hook_aliases_for_skipped_attn(self): + """Absent optional attn drops HT attn aliases; present layers keep them.""" + blocks = _setup_blocks(HybridModel(), AttnAdapter()) + attn_aliases = ( + "hook_attn_out", + "hook_attn_in", + "hook_q_input", + "hook_k_input", + "hook_v_input", + ) + for i in range(3): + for alias in attn_aliases: + assert alias in blocks[i].hook_aliases + for alias in attn_aliases: + assert alias not in blocks[3].hook_aliases + # Residual / MLP aliases are independent of attn and must survive. + assert "hook_resid_pre" in blocks[3].hook_aliases + assert "hook_mlp_out" in blocks[3].hook_aliases + + def test_prunes_hook_aliases_for_skipped_ssm(self): + blocks = _setup_blocks(HybridModel(), SsmAdapter()) + + assert "hook_ssm_out" in blocks[0].hook_aliases + assert "hook_ssm_out" not in blocks[3].hook_aliases + + def test_keeps_mlp_alias_fallback_after_skipped_post_norm(self): + blocks = _setup_blocks(HybridModel(), PostNormAdapter()) + + assert blocks[0].hook_aliases["hook_mlp_out"] == "ln2_post.hook_out" + assert blocks[3].hook_aliases["hook_mlp_out"] == "mlp.hook_out" + + def test_pruned_aliases_do_not_warn_on_register(self): + blocks = _setup_blocks(HybridModel(), AttnAdapter()) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + blocks[3]._register_aliases() + unresolved = [w for w in caught if "did not resolve" in str(w.message)] + assert unresolved == [] + # -- Tests: blocks_with() ----------------------------------------------------- diff --git a/tests/unit/tools/model_registry/test_verify_gates.py b/tests/unit/tools/model_registry/test_verify_gates.py index f0126184e8..c4e9b47e55 100644 --- a/tests/unit/tools/model_registry/test_verify_gates.py +++ b/tests/unit/tools/model_registry/test_verify_gates.py @@ -17,6 +17,7 @@ STATUS_VERIFIED, ) from transformer_lens.tools.model_registry.verify_models import ( + _check_phase_scores, _extract_phase_scores, _pass_status, _phases_to_run, @@ -88,6 +89,23 @@ def test_phase4_uses_quality_score_from_details(self): assert _extract_phase_scores(results)[4] == 87.5 +class TestCheckPhaseScores: + """A required modality phase must not pass by being absent from the scores.""" + + def test_required_modality_phase_absent_fails(self): + # All P7 tests SKIPPED -> phase 7 is omitted entirely (see + # test_all_skipped_phase_omitted), so the gate must not infer a pass. + scores = {1: 100.0, 2: 100.0, 3: 100.0, 4: 100.0} + assert _check_phase_scores(scores, []) is None + error = _check_phase_scores(scores, [], required_phases={1, 4, 7}) + assert error is not None and "P7=NULL" in error + + def test_absent_modality_phase_ignored_when_not_required(self): + # A partial run (--phases 1 2) must not be failed for a missing P7. + error = _check_phase_scores({1: 100.0, 2: 100.0}, [], required_phases={1}) + assert error is None + + class TestIsSSMMixerInternal: def test_mixer_and_linear_attn_internals_skipped(self): assert _is_ssm_mixer_internal("blocks.0.mixer.conv1d") diff --git a/tests/unit/tools/test_jacobian_lens.py b/tests/unit/tools/test_jacobian_lens.py index f046ad712e..f096b7f02a 100644 --- a/tests/unit/tools/test_jacobian_lens.py +++ b/tests/unit/tools/test_jacobian_lens.py @@ -501,13 +501,137 @@ def test_load_official_format_without_metadata(tmp_path: Any) -> None: assert lens.jacobians[0].dtype == torch.float32 -def test_load_rejects_fit_checkpoints(tmp_path: Any) -> None: +def test_load_checkpoint_with_zero_n_done_raises(tmp_path: Any) -> None: + """A checkpoint with n_done=0 cannot reconstruct the Jacobian mean.""" path = str(tmp_path / "ckpt.pt") - torch.save({"jacobian_sum": {}, "n_done": 3}, path) - with pytest.raises(ValueError, match="no 'J' key"): + torch.save( + { + "jacobian_sum": {0: torch.zeros(D_MODEL, D_MODEL)}, + "n_done": 0, + "next_idx": 0, + "source_layers": [0], + "target_layer": N_LAYERS - 1, + "skip_first": 0, + }, + path, + ) + with pytest.raises(ValueError, match="n_prompts=0"): + JacobianLens.load(path) + + +def test_load_checkpoint_with_empty_jacobian_sum_raises(tmp_path: Any) -> None: + """A checkpoint with an empty jacobian_sum cannot derive d_model.""" + path = str(tmp_path / "ckpt.pt") + torch.save( + { + "jacobian_sum": {}, + "n_done": 3, + "next_idx": 3, + "source_layers": [], + "target_layer": N_LAYERS - 1, + "skip_first": 0, + }, + path, + ) + with pytest.raises(ValueError, match="empty jacobian_sum"): JacobianLens.load(path) +def test_load_checkpoint_mirrors_fit_payload_schema(tmp_path: Any) -> None: + """Fixture uses the verbatim 6-key schema that write_checkpoint() produces. + + The reference implementation (anthropics/jacobian-lens) writes exactly: + jacobian_sum, n_done, next_idx, source_layers, target_layer, skip_first — + no ``d_model`` key, no nested ``metadata`` dict. + + After load(): + - n_done drives the prompt count + - d_model is inferred from the first jacobian_sum matrix's shape + - target_layer is harvested from the top-level payload into metadata + - converted_from sentinel is set so merge() refuses to mix with fitted lenses + """ + path = str(tmp_path / "fit_checkpoint.pt") + torch.save( + { + "jacobian_sum": {0: torch.ones(D_MODEL, D_MODEL) * 5.0}, + "n_done": 5, + "next_idx": 5, + "source_layers": [0], + "target_layer": N_LAYERS - 1, + "skip_first": 0, + }, + path, + ) + lens = JacobianLens.load(path) + + # jacobian_sum / n_done = ones*5 / 5 = ones + assert torch.allclose(lens.jacobians[0], torch.ones(D_MODEL, D_MODEL)) + assert lens.n_prompts == 5 + # d_model derived from matrix shape, not a payload key + assert lens.d_model == D_MODEL + + # target_layer harvested from top-level payload into metadata + assert lens.metadata["target_layer"] == N_LAYERS - 1 + + # converted_from sentinel must be set so merge() refuses to mix with fitted lenses + assert lens.metadata["converted_from"] == "jacobian_lens_checkpoint" + + +def test_load_checkpoint_harvests_flat_provenance_and_strips_fit_keys( + tmp_path: Any, +) -> None: + """Flat provenance and nested fit-reserved keys are handled correctly. + + Some checkpoint writers (e.g. alternative TL-based fitters) may include flat + provenance keys (model_name, corpus, model_revision) at the top level and/or + a nested metadata dict with fit-reserved fields. Verify the loader strips + fit-reserved keys and harvests safe provenance regardless of nesting. + """ + path = str(tmp_path / "tl_checkpoint.pt") + torch.save( + { + "jacobian_sum": {0: torch.ones(D_MODEL, D_MODEL) * 5.0}, + "n_done": 5, + "next_idx": 5, + "source_layers": [0], + "target_layer": N_LAYERS - 1, + "skip_first": 0, + # flat provenance at top level + "model_name": "toy-bridge", + "model_revision": "abc123def456", + "corpus": CORPUS, + # nested metadata with fit-reserved fields as an alternative writer might add + "metadata": { + "transformer_lens_fit": True, + "transformer_lens_version": "3.7.0", + "hook_convention": "blocks.{layer}.hook_out", + "fit_dtype": "float32", + "dim_batch": 8, + "max_seq_len": 128, + "skip_first_positions": 16, + }, + }, + path, + ) + lens = JacobianLens.load(path) + + # flat provenance harvested into metadata + assert lens.metadata["model_name"] == "toy-bridge" + assert lens.metadata["corpus"] == CORPUS + + # fit-reserved internal keys must be stripped + assert "transformer_lens_fit" not in lens.metadata + assert "transformer_lens_version" not in lens.metadata + assert "hook_convention" not in lens.metadata + assert "fit_dtype" not in lens.metadata + + # target_layer must be preserved (NOT stripped) so validate_model can check it + assert lens.metadata["target_layer"] == N_LAYERS - 1 + + # converted_from sentinel must be set + assert lens.metadata["converted_from"] == "jacobian_lens_checkpoint" + + def test_from_pretrained_uses_retry_and_forwards_hub_arguments( monkeypatch: pytest.MonkeyPatch, tmp_path: Any ) -> None: diff --git a/tests/unit/tools/test_jacobian_lens_import.py b/tests/unit/tools/test_jacobian_lens_import.py new file mode 100644 index 0000000000..78825a4ef8 --- /dev/null +++ b/tests/unit/tools/test_jacobian_lens_import.py @@ -0,0 +1,325 @@ +"""Unit tests for JacobianLens.load() checkpoint support. + +All tests use synthetic in-memory fixtures — no model, no Hub access. +""" + +import pytest +import torch + +from transformer_lens.tools.analysis.jacobian_lens import JacobianLens + +# --------------------------------------------------------------------------- +# Fixture helpers +# --------------------------------------------------------------------------- + + +def _save_artifact(path, *, n_layers=3, d_model=8, n_prompts=10, metadata=None): + """Write a minimal lens artifact (J-key format) to *path*.""" + payload = { + "J": {i: torch.randn(d_model, d_model).half() for i in range(n_layers)}, + "n_prompts": n_prompts, + "source_layers": list(range(n_layers)), + "d_model": d_model, + } + if metadata is not None: + payload["metadata"] = metadata + torch.save(payload, path) + + +def _save_checkpoint( + path, + *, + n_layers=3, + d_model=8, + n_prompts=5, + metadata=None, + flat_provenance=None, + n_prompts_override=None, +): + """Write a minimal fit-checkpoint (jacobian_sum format) to *path*. + + *n_prompts_override* lets callers inject a bad value without changing the + sums (useful for zero/negative n_prompts tests). + """ + payload = { + "jacobian_sum": {i: torch.randn(d_model, d_model) * n_prompts for i in range(n_layers)}, + "n_prompts": n_prompts if n_prompts_override is None else n_prompts_override, + "source_layers": list(range(n_layers)), + "d_model": d_model, + } + if metadata is not None: + payload["metadata"] = metadata + if flat_provenance is not None: + payload.update(flat_provenance) + torch.save(payload, path) + + +# --------------------------------------------------------------------------- +# Artifact loading (regression tests — existing behaviour must be preserved) +# --------------------------------------------------------------------------- + + +class TestLoadArtifact: + def test_basic_load(self, tmp_path): + p = str(tmp_path / "lens.pt") + _save_artifact(p, n_layers=3, d_model=8, n_prompts=10) + lens = JacobianLens.load(p) + assert lens.d_model == 8 + assert lens.n_prompts == 10 + assert lens.source_layers == [0, 1, 2] + + def test_matrices_cast_to_fp32(self, tmp_path): + p = str(tmp_path / "lens.pt") + _save_artifact(p, d_model=8) + lens = JacobianLens.load(p) + assert all(j.dtype == torch.float32 for j in lens.jacobians.values()) + + def test_metadata_preserved(self, tmp_path): + p = str(tmp_path / "lens.pt") + meta = {"model_name": "gpt2", "corpus": "wikitext"} + _save_artifact(p, metadata=meta) + lens = JacobianLens.load(p) + assert lens.metadata["model_name"] == "gpt2" + assert lens.metadata["corpus"] == "wikitext" + + def test_missing_metadata_gives_empty_dict(self, tmp_path): + p = str(tmp_path / "lens.pt") + _save_artifact(p) + lens = JacobianLens.load(p) + assert lens.metadata == {} + + def test_unknown_format_raises(self, tmp_path): + p = str(tmp_path / "garbage.pt") + torch.save({"random_key": 42}, str(p)) + with pytest.raises(ValueError, match="does not look like"): + JacobianLens.load(p) + + +# --------------------------------------------------------------------------- +# Checkpoint loading (new functionality) +# --------------------------------------------------------------------------- + + +class TestLoadCheckpoint: + def test_basic_load(self, tmp_path): + p = str(tmp_path / "ckpt.pt") + _save_checkpoint(p, n_layers=3, d_model=8, n_prompts=5) + lens = JacobianLens.load(p) + assert lens.d_model == 8 + assert lens.n_prompts == 5 + assert lens.source_layers == [0, 1, 2] + + def test_means_reconstructed_correctly(self, tmp_path): + """jacobian_sum / n_prompts must equal the stored average.""" + d_model, n_prompts = 8, 5 + sums = {i: torch.ones(d_model, d_model) * n_prompts for i in range(2)} + p = str(tmp_path / "ckpt.pt") + payload = { + "jacobian_sum": sums, + "n_prompts": n_prompts, + "source_layers": [0, 1], + "d_model": d_model, + } + torch.save(payload, p) + lens = JacobianLens.load(p) + for j in lens.jacobians.values(): + assert torch.allclose(j, torch.ones(d_model, d_model), atol=1e-6) + + def test_converted_from_key_set(self, tmp_path): + p = str(tmp_path / "ckpt.pt") + _save_checkpoint(p) + lens = JacobianLens.load(p) + assert lens.metadata.get("converted_from") == "jacobian_lens_checkpoint" + + def test_fit_reserved_key_stripped(self, tmp_path): + p = str(tmp_path / "ckpt.pt") + meta = { + "transformer_lens_fit": True, + "transformer_lens_version": "1.2.3", + "model_system": "TransformerBridge", + "corpus": "wiki", + } + _save_checkpoint(p, metadata=meta) + lens = JacobianLens.load(p) + assert "transformer_lens_fit" not in lens.metadata + assert "transformer_lens_version" not in lens.metadata + assert "model_system" not in lens.metadata + # safe keys survive + assert lens.metadata.get("corpus") == "wiki" + + def test_safe_scalar_metadata_preserved(self, tmp_path): + p = str(tmp_path / "ckpt.pt") + meta = { + "model_name": "gpt2", + "corpus": "wikitext", + "extra_int": 42, + "extra_float": 3.14, + "extra_list": [1, 2, 3], + } + _save_checkpoint(p, metadata=meta) + lens = JacobianLens.load(p) + assert lens.metadata["model_name"] == "gpt2" + assert lens.metadata["corpus"] == "wikitext" + assert lens.metadata["extra_int"] == 42 + assert lens.metadata["extra_float"] == pytest.approx(3.14) + assert lens.metadata["extra_list"] == [1, 2, 3] + + def test_tensor_valued_metadata_dropped_and_recorded(self, tmp_path): + """Tensor fields that fail _validate_metadata are logged in dropped_fields.""" + d_model, n_prompts = 8, 5 + p = str(tmp_path / "ckpt.pt") + payload = { + "jacobian_sum": {0: torch.randn(d_model, d_model) * n_prompts}, + "n_prompts": n_prompts, + "source_layers": [0], + "d_model": d_model, + "metadata": { + "model_name": "gpt2", + "embedding_stats": torch.randn(4), # tensor — will be dropped + }, + } + torch.save(payload, p) + lens = JacobianLens.load(p) + assert "embedding_stats" not in lens.metadata + dropped = lens.metadata.get("dropped_fields", []) + assert any("embedding_stats" in f for f in dropped) + # safe field survives + assert lens.metadata.get("model_name") == "gpt2" + + def test_flat_provenance_harvested(self, tmp_path): + """Checkpoints that store model_name / corpus at top level are handled.""" + p = str(tmp_path / "ckpt.pt") + _save_checkpoint( + p, flat_provenance={"model_name": "llama3", "corpus": "pile", "model_revision": "abc"} + ) + lens = JacobianLens.load(p) + assert lens.metadata.get("model_name") == "llama3" + assert lens.metadata.get("corpus") == "pile" + assert lens.metadata.get("model_revision") == "abc" + + def test_nested_metadata_overrides_flat_provenance(self, tmp_path): + """Explicit metadata dict takes precedence over flat payload keys.""" + p = str(tmp_path / "ckpt.pt") + _save_checkpoint( + p, + metadata={"model_name": "nested-name"}, + flat_provenance={"model_name": "flat-name"}, + ) + lens = JacobianLens.load(p) + assert lens.metadata.get("model_name") == "nested-name" + + def test_zero_n_prompts_raises(self, tmp_path): + p = str(tmp_path / "ckpt.pt") + _save_checkpoint(p, n_prompts_override=0) + with pytest.raises(ValueError, match="n_prompts=0"): + JacobianLens.load(p) + + def test_negative_n_prompts_raises(self, tmp_path): + p = str(tmp_path / "ckpt.pt") + _save_checkpoint(p, n_prompts_override=-1) + with pytest.raises(ValueError, match="n_prompts=-1"): + JacobianLens.load(p) + + def test_single_layer_checkpoint(self, tmp_path): + p = str(tmp_path / "ckpt.pt") + _save_checkpoint(p, n_layers=1, d_model=4) + lens = JacobianLens.load(p) + assert lens.source_layers == [0] + + def test_load_then_save_roundtrip(self, tmp_path): + """A checkpoint loaded and re-saved produces a valid artifact.""" + ckpt_path = str(tmp_path / "ckpt.pt") + art_path = str(tmp_path / "artifact.pt") + _save_checkpoint(ckpt_path, n_layers=2, d_model=8, n_prompts=3, metadata={"corpus": "wiki"}) + lens = JacobianLens.load(ckpt_path) + lens.save(art_path) + reloaded = JacobianLens.load(art_path) + assert reloaded.n_prompts == 3 + assert reloaded.source_layers == [0, 1] + assert reloaded.metadata.get("converted_from") == "jacobian_lens_checkpoint" + + +# --------------------------------------------------------------------------- +# merge() refuses to mix converted and TL-fitted lenses +# --------------------------------------------------------------------------- + + +class TestMergeProvenance: + def test_merge_two_checkpoints_succeeds(self, tmp_path): + p1, p2 = str(tmp_path / "c1.pt"), str(tmp_path / "c2.pt") + _save_checkpoint(p1, metadata={"corpus": "wiki"}) + _save_checkpoint(p2, metadata={"corpus": "wiki"}) + l1 = JacobianLens.load(p1) + l2 = JacobianLens.load(p2) + merged = JacobianLens.merge([l1, l2]) + assert merged.metadata.get("converted_from") == "jacobian_lens_checkpoint" + assert merged.n_prompts == l1.n_prompts + l2.n_prompts + + def test_merge_two_artifacts_succeeds(self, tmp_path): + shared_meta = {"model_name": "gpt2", "corpus": "wiki", "transformer_lens_fit": True} + p1, p2 = str(tmp_path / "a1.pt"), str(tmp_path / "a2.pt") + _save_artifact(p1, metadata=dict(shared_meta)) + _save_artifact(p2, metadata=dict(shared_meta)) + l1 = JacobianLens.load(p1) + l2 = JacobianLens.load(p2) + # Both have transformer_lens_fit=True — provenance matches + merged = JacobianLens.merge([l1, l2]) + assert merged.n_prompts == l1.n_prompts + l2.n_prompts + + def test_merge_checkpoint_and_artifact_raises(self, tmp_path): + """A converted checkpoint and a TL-fitted artifact must not merge.""" + p_ckpt = str(tmp_path / "ckpt.pt") + p_art = str(tmp_path / "art.pt") + _save_checkpoint(p_ckpt, metadata={"corpus": "wiki"}) + _save_artifact( + p_art, + metadata={ + "transformer_lens_fit": True, + "corpus": "wiki", + "model_name": "gpt2", + }, + ) + ckpt_lens = JacobianLens.load(p_ckpt) + art_lens = JacobianLens.load(p_art) + with pytest.raises(ValueError, match="provenance metadata"): + JacobianLens.merge([ckpt_lens, art_lens]) + + def test_merge_checkpoints_different_corpus_raises(self, tmp_path): + p1, p2 = str(tmp_path / "c1.pt"), str(tmp_path / "c2.pt") + _save_checkpoint(p1, metadata={"corpus": "wiki"}) + _save_checkpoint(p2, metadata={"corpus": "pile"}) + l1 = JacobianLens.load(p1) + l2 = JacobianLens.load(p2) + with pytest.raises(ValueError, match="provenance metadata"): + JacobianLens.merge([l1, l2]) + + def test_merge_weighted_average_correct(self, tmp_path): + """Merged matrices are prompt-count-weighted averages.""" + d_model = 4 + p1, p2 = str(tmp_path / "c1.pt"), str(tmp_path / "c2.pt") + + n1, n2 = 3, 7 + # jacobian_sum = n * mean, so here mean = ones for both + payload1 = { + "jacobian_sum": {0: torch.ones(d_model, d_model) * n1}, + "n_prompts": n1, + "d_model": d_model, + "source_layers": [0], + } + payload2 = { + "jacobian_sum": {0: torch.ones(d_model, d_model) * n2 * 2}, + "n_prompts": n2, + "d_model": d_model, + "source_layers": [0], + } + torch.save(payload1, p1) + torch.save(payload2, p2) + l1 = JacobianLens.load(p1) # mean = ones + l2 = JacobianLens.load(p2) # mean = 2*ones + merged = JacobianLens.merge([l1, l2]) + expected_mean = (n1 * 1.0 + n2 * 2.0) / (n1 + n2) + assert torch.allclose( + merged.jacobians[0], + torch.full((d_model, d_model), expected_mean), + atol=1e-5, + ) diff --git a/transformer_lens/BertNextSentencePrediction.py b/transformer_lens/BertNextSentencePrediction.py index fb6f8e8fac..a55f0d862d 100644 --- a/transformer_lens/BertNextSentencePrediction.py +++ b/transformer_lens/BertNextSentencePrediction.py @@ -5,6 +5,7 @@ to e.g. GPT style transformers. """ +import warnings from typing import Any, Dict, List, Optional, Tuple, Union, overload import torch @@ -31,6 +32,13 @@ class BertNextSentencePrediction: """ def __init__(self, model: Any): + warnings.warn( + "BertNextSentencePrediction is deprecated and will be removed in 4.0. Use " + "TransformerBridge.boot_transformers(...) for BERT-style models; see " + "demos/BERT.ipynb.", + DeprecationWarning, + stacklevel=2, + ) self.model = model def __call__( diff --git a/transformer_lens/HookedEncoder.py b/transformer_lens/HookedEncoder.py index 64cb09b141..a5fdb80cfe 100644 --- a/transformer_lens/HookedEncoder.py +++ b/transformer_lens/HookedEncoder.py @@ -8,6 +8,7 @@ import logging import os +import warnings from typing import Any, Dict, List, Optional, Tuple, TypeVar, Union, cast, overload import torch @@ -58,6 +59,12 @@ def __init__( **kwargs: Any, ): super().__init__() + warnings.warn( + "HookedEncoder is deprecated and will be removed in 4.0. Use " + "TransformerBridge.boot_transformers(...) instead.", + DeprecationWarning, + stacklevel=2, + ) if isinstance(cfg, Dict): cfg = HookedTransformerConfig(**cfg) elif isinstance(cfg, str): @@ -379,6 +386,12 @@ def from_pretrained( **from_pretrained_kwargs: Any, ) -> HookedEncoder: """Loads in the pretrained weights from huggingface. Currently supports loading weight from HuggingFace BertForMaskedLM. Unlike HookedTransformer, this does not yet do any preprocessing on the model.""" + warnings.warn( + "HookedEncoder.from_pretrained is deprecated and will be removed in 4.0. Use " + "TransformerBridge.boot_transformers(...) instead.", + DeprecationWarning, + stacklevel=2, + ) logging.warning( "Support for BERT in TransformerLens is currently experimental, until such a time when it has feature " "parity with HookedTransformer and has been tested on real research tasks. Until then, backward " diff --git a/transformer_lens/HookedRootModule.py b/transformer_lens/HookedRootModule.py index 2628860573..13d89ffdb6 100644 --- a/transformer_lens/HookedRootModule.py +++ b/transformer_lens/HookedRootModule.py @@ -9,6 +9,7 @@ from __future__ import annotations import logging +import warnings from collections.abc import Callable, Iterable from contextlib import contextmanager from functools import partial @@ -54,6 +55,13 @@ class HookedRootModule(HookIntrospectionMixin, nn.Module): def __init__(self, *args: Any): super().__init__() + if type(self) is HookedRootModule: + warnings.warn( + "HookedRootModule is deprecated and will be removed in 4.0. Use " + "TransformerBridge.boot_transformers(...) instead.", + DeprecationWarning, + stacklevel=2, + ) self.is_caching = False self.context_level = 0 diff --git a/transformer_lens/HookedTransformer.py b/transformer_lens/HookedTransformer.py index 456d1ab9f3..f46b697bdd 100644 --- a/transformer_lens/HookedTransformer.py +++ b/transformer_lens/HookedTransformer.py @@ -13,6 +13,7 @@ import logging import os +import warnings from collections.abc import Generator from typing import ( Any, @@ -164,6 +165,13 @@ def __init__( default_padding_side: Which side to pad on. """ super().__init__() + warnings.warn( + "HookedTransformer is deprecated and will be removed in 4.0. Use " + "TransformerBridge.boot_transformers(...) instead, then call " + "enable_compatibility_mode() for HookedTransformer-equivalent numerics.", + DeprecationWarning, + stacklevel=2, + ) if isinstance(cfg, str): raise ValueError( "Please pass in a config dictionary or HookedTransformerConfig object. If you want to load a " diff --git a/transformer_lens/benchmarks/AGENTS.md b/transformer_lens/benchmarks/AGENTS.md index 4f9159bb53..be6ad26d0b 100644 --- a/transformer_lens/benchmarks/AGENTS.md +++ b/transformer_lens/benchmarks/AGENTS.md @@ -4,13 +4,13 @@ This is the benchmark library that `verify_models` calls into. Read [the root AG ## ⚠ Not the registry-update path -**Use [`verify_models`](../tools/model_registry/AGENTS.md), not [`main_benchmark.py`](main_benchmark.py), for registry updates.** `main_benchmark` runs the same benchmark math but defaults to NOT writing `data/supported_models.json` (needs `--update-registry`, and even with that flag misses Phase 7 / 8 scores and the resume checkpoint). +**Use [`verify_models`](../tools/model_registry/AGENTS.md), not [`main_benchmark.py`](main_benchmark.py), for registry updates.** `main_benchmark` runs the same benchmark math but defaults to NOT writing `data/supported_models.json` (needs `--update-registry`, and even with that flag misses Phase 7 / 8 / 9 scores and the resume checkpoint). If an agent is here because the user asked to "update the registry" or "verify a model," they took the wrong entry point — redirect to [tools/model_registry/AGENTS.md](../tools/model_registry/AGENTS.md). ## 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`, `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`, `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/audio.py b/transformer_lens/benchmarks/audio.py index 239b9c496f..38bcd40762 100644 --- a/transformer_lens/benchmarks/audio.py +++ b/transformer_lens/benchmarks/audio.py @@ -9,14 +9,24 @@ import torch +from transformer_lens.benchmarks.encoder_common import ( + benchmark_encoder_cache, + benchmark_encoder_forward, + benchmark_encoder_representation_stability, +) from transformer_lens.benchmarks.utils import ( BenchmarkResult, BenchmarkSeverity, - compare_tensors, + build_modality_input, is_tiny_test_model, ) from transformer_lens.model_bridge import TransformerBridge +# Component-mapping names whose hook_out must be cached when the architecture +# declares them: waveform encoders (HuBERT, wav2vec2) have a conv feature +# extractor, while spectrogram encoders (AST) patch-embed the spectrogram directly. +_CRITICAL_AUDIO_COMPONENTS = ("audio_feature_extractor", "conv_pos_embed", "embed_ln", "embed") + def _prepare_audio_text_inputs(bridge: TransformerBridge): """Build audio-conditioned inputs (synthetic waveform + audio token) for an @@ -124,77 +134,13 @@ def benchmark_audio_forward( test_audio: Audio waveform tensor [batch, num_samples] reference_model: Optional HF reference model for comparison """ - try: - with torch.no_grad(): - # Use return_type="logits" — for audio encoders without logits, this - # returns the BaseModelOutput object (bridge falls through to logits=output). - bridge_output_raw = bridge(test_audio, return_type="logits") - - # Extract the output tensor - if isinstance(bridge_output_raw, torch.Tensor): - bridge_output = bridge_output_raw - output_key = "logits" - elif hasattr(bridge_output_raw, "logits") and bridge_output_raw.logits is not None: - bridge_output = bridge_output_raw.logits - output_key = "logits" - elif hasattr(bridge_output_raw, "last_hidden_state"): - bridge_output = bridge_output_raw.last_hidden_state - output_key = "last_hidden_state" - else: - return BenchmarkResult( - name="audio_forward", - severity=BenchmarkSeverity.DANGER, - message="Bridge produced no recognizable output (no logits or last_hidden_state)", - passed=False, - ) - - if bridge_output.numel() == 0: - return BenchmarkResult( - name="audio_forward", - severity=BenchmarkSeverity.DANGER, - message="Bridge output is empty", - passed=False, - ) - - if torch.isnan(bridge_output).any() or torch.isinf(bridge_output).any(): - return BenchmarkResult( - name="audio_forward", - severity=BenchmarkSeverity.DANGER, - message="Bridge output contains NaN or Inf values", - passed=False, - ) - - # Compare against HF reference if available - if reference_model is not None: - with torch.no_grad(): - ref_output_raw = reference_model(input_values=test_audio) - if output_key == "logits": - ref_output = ref_output_raw.logits - else: - ref_output = ref_output_raw.last_hidden_state - - return compare_tensors( - bridge_output, - ref_output, - atol=1e-3, - rtol=3e-2, - name="audio_forward", - ) - - return BenchmarkResult( - name="audio_forward", - severity=BenchmarkSeverity.INFO, - message=f"Audio forward pass successful ({output_key} shape: {bridge_output.shape})", - details={"output_shape": str(bridge_output.shape), "output_key": output_key}, - ) - - except Exception as e: - return BenchmarkResult( - name="audio_forward", - severity=BenchmarkSeverity.ERROR, - message=f"Audio forward pass failed: {str(e)}", - passed=False, - ) + return benchmark_encoder_forward( + bridge, + test_audio, + name="audio_forward", + ref_input_key="input_values", + reference_model=reference_model, + ) def benchmark_audio_cache( @@ -209,82 +155,12 @@ def benchmark_audio_cache( bridge: TransformerBridge model to test test_audio: Audio waveform tensor [batch, num_samples] """ - try: - with torch.no_grad(): - _, cache = bridge.run_with_cache(test_audio) - - cache_keys = list(cache.keys()) - if len(cache_keys) == 0: - return BenchmarkResult( - name="audio_cache", - severity=BenchmarkSeverity.DANGER, - message="run_with_cache returned empty cache", - passed=False, - ) - - # Check for critical audio-specific hooks - critical_hooks = [ - "audio_feature_extractor.hook_out", - "conv_pos_embed.hook_out", - "embed_ln.hook_out", - ] - # Also check at least the first and last block - n_layers = bridge.cfg.n_layers - critical_hooks.append("blocks.0.hook_out") - critical_hooks.append(f"blocks.{n_layers - 1}.hook_out") - - missing = [h for h in critical_hooks if h not in cache_keys] - found = len(critical_hooks) - len(missing) - - # Check for NaN/Inf in cached values - nan_hooks = [] - for key in cache_keys[:20]: # Sample first 20 hooks - val = cache[key] - if isinstance(val, torch.Tensor) and (torch.isnan(val).any() or torch.isinf(val).any()): - nan_hooks.append(key) - - if missing: - return BenchmarkResult( - name="audio_cache", - severity=BenchmarkSeverity.WARNING, - message=f"Missing {len(missing)} critical hooks: {missing[:3]}", - passed=found >= 3, # Pass if at least 3 of 5 critical hooks present - details={ - "total_cached": len(cache_keys), - "critical_found": found, - "critical_expected": len(critical_hooks), - "missing": missing, - }, - ) - - if nan_hooks: - return BenchmarkResult( - name="audio_cache", - severity=BenchmarkSeverity.DANGER, - message=f"NaN/Inf found in {len(nan_hooks)} cached hooks", - passed=False, - details={"nan_hooks": nan_hooks[:5]}, - ) - - return BenchmarkResult( - name="audio_cache", - severity=BenchmarkSeverity.INFO, - message=f"Audio cache successful: {len(cache_keys)} hooks captured, " - f"{found}/{len(critical_hooks)} critical hooks present", - details={ - "total_cached": len(cache_keys), - "critical_found": found, - "critical_expected": len(critical_hooks), - }, - ) - - except Exception as e: - return BenchmarkResult( - name="audio_cache", - severity=BenchmarkSeverity.ERROR, - message=f"Audio cache failed: {str(e)}", - passed=False, - ) + return benchmark_encoder_cache( + bridge, + test_audio, + name="audio_cache", + critical_components=_CRITICAL_AUDIO_COMPONENTS, + ) def benchmark_audio_representation_stability( @@ -301,68 +177,11 @@ def benchmark_audio_representation_stability( bridge: TransformerBridge model to test test_audio: Audio waveform tensor [batch, num_samples] """ - model_name = getattr(bridge.cfg, "model_name", "") - if is_tiny_test_model(model_name): - return BenchmarkResult( - name="audio_representation_stability", - severity=BenchmarkSeverity.SKIPPED, - message="Skipped for tiny-random model (random weights won't produce stable representations)", - ) - - try: - # Create a slightly perturbed version - noise = torch.randn_like(test_audio) * 0.01 - perturbed_audio = test_audio + noise - - with torch.no_grad(): - output_orig = bridge(test_audio, return_type="logits") - output_pert = bridge(perturbed_audio, return_type="logits") - - # Extract hidden states — handle tensor, BaseModelOutput, or CTC output - def _extract_states(out): - if isinstance(out, torch.Tensor): - return out - if hasattr(out, "last_hidden_state"): - return out.last_hidden_state - if hasattr(out, "logits") and out.logits is not None: - return out.logits - return None - - orig_states = _extract_states(output_orig) - pert_states = _extract_states(output_pert) - - if orig_states is None or pert_states is None: - return BenchmarkResult( - name="audio_representation_stability", - severity=BenchmarkSeverity.WARNING, - message="Could not extract hidden states for stability check", - passed=False, - ) - - # Compute cosine similarity (flatten to 2D: [batch, features]) - orig_flat = orig_states.reshape(orig_states.shape[0], -1) - pert_flat = pert_states.reshape(pert_states.shape[0], -1) - cosine_sim = ( - torch.nn.functional.cosine_similarity(orig_flat, pert_flat, dim=-1).mean().item() - ) - - passed = cosine_sim > 0.95 - return BenchmarkResult( - name="audio_representation_stability", - severity=BenchmarkSeverity.INFO if passed else BenchmarkSeverity.WARNING, - message=f"Representation stability: cosine_similarity={cosine_sim:.4f} " - f"(threshold: 0.95)", - passed=passed, - details={"cosine_similarity": cosine_sim, "noise_std": 0.01}, - ) - - except Exception as e: - return BenchmarkResult( - name="audio_representation_stability", - severity=BenchmarkSeverity.ERROR, - message=f"Representation stability check failed: {str(e)}", - passed=False, - ) + return benchmark_encoder_representation_stability( + bridge, + test_audio, + name="audio_representation_stability", + ) def benchmark_audio_feature_extractor( @@ -379,6 +198,13 @@ def benchmark_audio_feature_extractor( test_audio: Audio waveform tensor [batch, num_samples] """ try: + if "audio_feature_extractor" not in (bridge.adapter.component_mapping or {}): + return BenchmarkResult( + name="audio_feature_extractor", + severity=BenchmarkSeverity.SKIPPED, + message="Skipped: architecture has no conv feature extractor (spectrogram input)", + ) + with torch.no_grad(): _, cache = bridge.run_with_cache(test_audio) @@ -536,16 +362,24 @@ def run_audio_benchmarks( Args: bridge: TransformerBridge model to test - test_audio: Optional audio waveform tensor. If None, generates synthetic audio. + test_audio: Optional audio input tensor. If None, generates a synthetic input + shaped for this architecture (waveform or spectrogram). verbose: Whether to print progress Returns: List of BenchmarkResult objects """ if test_audio is None: - device = bridge.cfg.device - dtype = bridge.cfg.dtype - test_audio = torch.randn(1, 16000, device=device, dtype=dtype) + test_audio = build_modality_input(bridge, device=bridge.cfg.device, dtype=bridge.cfg.dtype) + if test_audio is None: + return [ + BenchmarkResult( + name="audio_forward", + severity=BenchmarkSeverity.ERROR, + message="Could not build an audio input for this model", + passed=False, + ) + ] results = [] diff --git a/transformer_lens/benchmarks/component_outputs.py b/transformer_lens/benchmarks/component_outputs.py index 90482c2d10..989f88760f 100644 --- a/transformer_lens/benchmarks/component_outputs.py +++ b/transformer_lens/benchmarks/component_outputs.py @@ -12,6 +12,7 @@ import torch from torch import nn +from transformer_lens.benchmarks.utils import build_modality_input from transformer_lens.config import TransformerBridgeConfig from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter from transformer_lens.model_bridge.generalized_components.base import ( @@ -618,13 +619,15 @@ def _test_component( torch.arange(seq_len, device=test_input.device).unsqueeze(0).expand(batch, -1) ) - # For embedding components, generate token indices once - shared_token_indices = None + # For embedding components, generate the embedding input once + shared_embed_input = None if component_path in ("embed", "encoder_embed", "decoder_embed"): - batch, seq_len, _ = test_input.shape - shared_token_indices = torch.randint( - 0, self.cfg.d_vocab, (batch, seq_len), device=test_input.device - ) + shared_embed_input = test_inputs.get("modality_input") + if shared_embed_input is None: + batch, seq_len, _ = test_input.shape + shared_embed_input = torch.randint( + 0, self.cfg.d_vocab, (batch, seq_len), device=test_input.device + ) # Generate shared inputs for attention/MLP/rotary components that have get_random_inputs() # This is needed for model-specific inputs like position_embeddings or attention_mask @@ -678,10 +681,10 @@ def _test_component( # Run through both components with shared inputs (for attention) or standard inputs (for others) bridge_output = self._run_component( - bridge_component, test_input, component_path, shared_token_indices, shared_inputs + bridge_component, test_input, component_path, shared_embed_input, shared_inputs ) hf_output = self._run_component( - hf_component, test_input, component_path, shared_token_indices, shared_inputs + hf_component, test_input, component_path, shared_embed_input, shared_inputs ) # Extract tensors if outputs are tuples @@ -736,8 +739,8 @@ def _test_component( if val is not None and isinstance(val, torch.Tensor): del val shared_inputs[key] = None - if shared_token_indices is not None: - del shared_token_indices + if shared_embed_input is not None: + del shared_embed_input return ComponentTestResult( component_path=component_path, @@ -796,7 +799,7 @@ def _run_component( component: nn.Module, test_input: torch.Tensor, component_path: str, - shared_token_indices: Optional[torch.Tensor] = None, + shared_embed_input: Optional[torch.Tensor] = None, shared_inputs: Optional[dict] = None, ) -> Any: """Run a component with appropriate arguments. @@ -805,7 +808,8 @@ def _run_component( component: The component to run test_input: The test input tensor component_path: Path to the component for debugging - shared_token_indices: Pre-generated token indices for embedding components + shared_embed_input: Pre-generated input for embedding components (token ids, + or a spectrogram/pixel tensor for audio and vision models) shared_inputs: Pre-generated inputs from get_random_inputs() to use for both bridge and HF components Returns: @@ -861,16 +865,15 @@ def _run_component( # Try simple call return component(test_input) elif component_path in ("embed", "encoder_embed", "decoder_embed"): - # Main embedding component expects integer indices - # Use shared token indices if provided, otherwise generate new ones - if shared_token_indices is not None: - token_indices = shared_token_indices + # Token ids for text models; a spectrogram or pixel tensor for audio/vision. + if shared_embed_input is not None: + embed_input = shared_embed_input else: batch, seq_len, _ = test_input.shape - token_indices = torch.randint( + embed_input = torch.randint( 0, self.cfg.d_vocab, (batch, seq_len), device=test_input.device ) - return component(token_indices) + return component(embed_input) elif component_path == "pos_embed" or "pos_embed" in component_path: # Position embedding expects integer position indices batch, seq_len, _ = test_input.shape @@ -951,11 +954,25 @@ def _generate_test_inputs(self) -> Dict[str, torch.Tensor]: except StopIteration: device = torch.device("cpu") - return { + inputs = { "hidden_states": torch.randn(batch_size, seq_len, d_model, dtype=dtype, device=device), - "token_ids": torch.randint(0, self.cfg.d_vocab, (batch_size, seq_len), device=device), } + # Vision/audio models have no token vocabulary — d_vocab stays at its -1 sentinel. + if self.cfg.d_vocab > 0: + inputs["token_ids"] = torch.randint( + 0, self.cfg.d_vocab, (batch_size, seq_len), device=device + ) + + # Audio/vision embeddings consume a raw spectrogram or pixel tensor, not token ids. + modality_input = build_modality_input( + self.bridge_model, batch_size=batch_size, device=device, dtype=dtype + ) + if modality_input is not None: + inputs["modality_input"] = modality_input + + return inputs + def _compare_outputs( self, bridge_output: torch.Tensor, hf_output: torch.Tensor ) -> Tuple[bool, float, float, Dict[str, float]]: diff --git a/transformer_lens/benchmarks/encoder_common.py b/transformer_lens/benchmarks/encoder_common.py new file mode 100644 index 0000000000..cf40fe4db1 --- /dev/null +++ b/transformer_lens/benchmarks/encoder_common.py @@ -0,0 +1,291 @@ +"""Shared benchmark implementations for non-text encoder bridges. + +Audio and vision encoders take one raw tensor (waveform, spectrogram, or pixels) +where text models take token ids; given that tensor, the forward, run_with_cache, +and perturbation-stability checks are modality-independent. The modality modules +(audio.py, vision.py) wrap these with their result names, HF reference-forward +kwarg, and critical component lists. +""" + +from typing import Optional, Sequence + +import torch + +from transformer_lens.benchmarks.utils import ( + BenchmarkResult, + BenchmarkSeverity, + compare_tensors, + is_tiny_test_model, +) +from transformer_lens.model_bridge import TransformerBridge + + +def extract_encoder_states(out) -> Optional[torch.Tensor]: + """Pull the hidden-state/logit tensor out of a tensor, BaseModelOutput, or head output.""" + if isinstance(out, torch.Tensor): + return out + if hasattr(out, "last_hidden_state"): + return out.last_hidden_state + if hasattr(out, "logits") and out.logits is not None: + return out.logits + return None + + +def benchmark_encoder_forward( + bridge: TransformerBridge, + test_input: torch.Tensor, + name: str, + ref_input_key: str, + reference_model: Optional[torch.nn.Module] = None, +) -> BenchmarkResult: + """Forward-pass benchmark on a raw modality tensor. + + Compares bridge output against the HF native model on the same input when a + reference is given. Bare encoders compare last_hidden_state; head models + (CTC, classification) compare logits. + + Args: + bridge: TransformerBridge model to test + test_input: Raw modality tensor (waveform, spectrogram, or pixels) + name: Result name (e.g. "audio_forward", "vision_forward") + ref_input_key: Kwarg the HF reference takes the tensor under + ("input_values" for audio, "pixel_values" for vision) + reference_model: Optional HF reference model for comparison + """ + try: + with torch.no_grad(): + # Use return_type="logits" — for bare encoders without logits, this + # returns the BaseModelOutput object (bridge falls through to logits=output). + bridge_output_raw = bridge(test_input, return_type="logits") + + # Extract the output tensor + if isinstance(bridge_output_raw, torch.Tensor): + bridge_output = bridge_output_raw + output_key = "logits" + elif hasattr(bridge_output_raw, "logits") and bridge_output_raw.logits is not None: + bridge_output = bridge_output_raw.logits + output_key = "logits" + elif hasattr(bridge_output_raw, "last_hidden_state"): + bridge_output = bridge_output_raw.last_hidden_state + output_key = "last_hidden_state" + else: + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.DANGER, + message="Bridge produced no recognizable output (no logits or last_hidden_state)", + passed=False, + ) + + if bridge_output.numel() == 0: + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.DANGER, + message="Bridge output is empty", + passed=False, + ) + + if torch.isnan(bridge_output).any() or torch.isinf(bridge_output).any(): + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.DANGER, + message="Bridge output contains NaN or Inf values", + passed=False, + ) + + # Compare against HF reference if available + if reference_model is not None: + with torch.no_grad(): + ref_output_raw = reference_model(**{ref_input_key: test_input}) + if output_key == "logits": + ref_output = ref_output_raw.logits + else: + ref_output = ref_output_raw.last_hidden_state + + return compare_tensors( + bridge_output, + ref_output, + atol=1e-3, + rtol=3e-2, + name=name, + ) + + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.INFO, + message=f"Forward pass successful ({output_key} shape: {bridge_output.shape})", + details={"output_shape": str(bridge_output.shape), "output_key": output_key}, + ) + + except Exception as e: + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.ERROR, + message=f"Forward pass failed: {str(e)}", + passed=False, + ) + + +def benchmark_encoder_cache( + bridge: TransformerBridge, + test_input: torch.Tensor, + name: str, + critical_components: Sequence[str], + min_found: int = 3, +) -> BenchmarkResult: + """run_with_cache() benchmark on a raw modality tensor. + + Verifies that critical hooks fire and produce valid tensors: the + ``critical_components`` this architecture actually declares in its + component mapping, plus the first and last block. + + Args: + bridge: TransformerBridge model to test + test_input: Raw modality tensor (waveform, spectrogram, or pixels) + name: Result name (e.g. "audio_cache", "vision_cache") + critical_components: Component-mapping names whose hook_out must be + cached when the architecture declares them + min_found: Minimum critical hooks present to still pass with a warning + """ + try: + with torch.no_grad(): + _, cache = bridge.run_with_cache(test_input) + + cache_keys = list(cache.keys()) + if len(cache_keys) == 0: + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.DANGER, + message="run_with_cache returned empty cache", + passed=False, + ) + + component_mapping = bridge.adapter.component_mapping or {} + critical_hooks = [ + f"{comp}.hook_out" for comp in critical_components if comp in component_mapping + ] + # Also check at least the first and last block + n_layers = bridge.cfg.n_layers + critical_hooks.append("blocks.0.hook_out") + critical_hooks.append(f"blocks.{n_layers - 1}.hook_out") + + missing = [h for h in critical_hooks if h not in cache_keys] + found = len(critical_hooks) - len(missing) + + # Check for NaN/Inf in cached values + nan_hooks = [] + for key in cache_keys[:20]: # Sample first 20 hooks + val = cache[key] + if isinstance(val, torch.Tensor) and (torch.isnan(val).any() or torch.isinf(val).any()): + nan_hooks.append(key) + + if missing: + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.WARNING, + message=f"Missing {len(missing)} critical hooks: {missing[:3]}", + passed=found >= min_found, + details={ + "total_cached": len(cache_keys), + "critical_found": found, + "critical_expected": len(critical_hooks), + "missing": missing, + }, + ) + + if nan_hooks: + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.DANGER, + message=f"NaN/Inf found in {len(nan_hooks)} cached hooks", + passed=False, + details={"nan_hooks": nan_hooks[:5]}, + ) + + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.INFO, + message=f"Cache successful: {len(cache_keys)} hooks captured, " + f"{found}/{len(critical_hooks)} critical hooks present", + details={ + "total_cached": len(cache_keys), + "critical_found": found, + "critical_expected": len(critical_hooks), + }, + ) + + except Exception as e: + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.ERROR, + message=f"Cache benchmark failed: {str(e)}", + passed=False, + ) + + +def benchmark_encoder_representation_stability( + bridge: TransformerBridge, + test_input: torch.Tensor, + name: str, +) -> BenchmarkResult: + """Representation stability under small input perturbations. + + Similar inputs should produce similar hidden states. Skipped for + tiny-random models (random weights won't produce stable representations). + + Args: + bridge: TransformerBridge model to test + test_input: Raw modality tensor (waveform, spectrogram, or pixels) + name: Result name (e.g. "audio_representation_stability") + """ + model_name = getattr(bridge.cfg, "model_name", "") + if is_tiny_test_model(model_name): + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.SKIPPED, + message="Skipped for tiny-random model (random weights won't produce stable representations)", + ) + + try: + # Create a slightly perturbed version + noise = torch.randn_like(test_input) * 0.01 + perturbed_input = test_input + noise + + with torch.no_grad(): + output_orig = bridge(test_input, return_type="logits") + output_pert = bridge(perturbed_input, return_type="logits") + + orig_states = extract_encoder_states(output_orig) + pert_states = extract_encoder_states(output_pert) + + if orig_states is None or pert_states is None: + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.WARNING, + message="Could not extract hidden states for stability check", + passed=False, + ) + + # Compute cosine similarity (flatten to 2D: [batch, features]) + orig_flat = orig_states.reshape(orig_states.shape[0], -1) + pert_flat = pert_states.reshape(pert_states.shape[0], -1) + cosine_sim = ( + torch.nn.functional.cosine_similarity(orig_flat, pert_flat, dim=-1).mean().item() + ) + + passed = cosine_sim > 0.95 + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.INFO if passed else BenchmarkSeverity.WARNING, + message=f"Representation stability: cosine_similarity={cosine_sim:.4f} " + f"(threshold: 0.95)", + passed=passed, + details={"cosine_similarity": cosine_sim, "noise_std": 0.01}, + ) + + except Exception as e: + return BenchmarkResult( + name=name, + severity=BenchmarkSeverity.ERROR, + message=f"Representation stability check failed: {str(e)}", + passed=False, + ) diff --git a/transformer_lens/benchmarks/main_benchmark.py b/transformer_lens/benchmarks/main_benchmark.py index a090e66a32..6e531abfd9 100644 --- a/transformer_lens/benchmarks/main_benchmark.py +++ b/transformer_lens/benchmarks/main_benchmark.py @@ -55,6 +55,7 @@ BenchmarkResult, BenchmarkSeverity, PhaseReferenceData, + build_modality_input, compare_tensors, format_results, ) @@ -1050,12 +1051,20 @@ def cleanup_model(model, model_name_str: str): print(f"\nStack trace:\n{error_trace}") return results - # Detect audio model once for use across all phases + # Detect audio/vision models once for use across all phases _is_audio = bridge_unprocessed is not None and getattr( bridge_unprocessed.cfg, "is_audio_model", False ) - # Shared waveform for audio model benchmarks (consistent across HF capture and bridge forward) - _test_audio = torch.randn(1, 16000, device=device, dtype=dtype) if _is_audio else None + _is_visual = bridge_unprocessed is not None and getattr( + bridge_unprocessed.cfg, "is_visual_model", False + ) + # Shared non-text input (spectrogram, waveform, or pixels) — the same tensor is used + # for the HF reference capture and the bridge forward so they stay comparable. + _test_modality_input = ( + build_modality_input(bridge_unprocessed, device=device, dtype=dtype) + if (_is_audio or _is_visual) + else None + ) # Run Phase 1 benchmarks if should_run_phase(1) and bridge_unprocessed: @@ -1087,20 +1096,24 @@ def cleanup_model(model, model_name_str: str): if verbose: print("Capturing HF reference outputs to CPU...") try: - if _is_audio: - # Audio models: use the shared waveform for HF vs bridge comparison + if _test_modality_input is not None: + # Audio/vision models: use the shared non-text input for HF vs bridge with torch.no_grad(): - hf_out = hf_model(input_values=_test_audio) - # Audio encoders output last_hidden_state, not logits + if _is_visual: + hf_out = hf_model(pixel_values=_test_modality_input) + else: + hf_out = hf_model(input_values=_test_modality_input) + # Bare encoders output last_hidden_state, not logits if hasattr(hf_out, "logits") and hf_out.logits is not None: hf_saved_logits = hf_out.logits.detach().cpu().clone() else: hf_saved_logits = hf_out.last_hidden_state.detach().cpu().clone() - # No loss computation for audio — CTC requires aligned labels + # No loss computation — there are no next-token labels here if verbose: + kind = "vision" if _is_visual else "audio" print( - f"✓ Captured HF audio output {hf_saved_logits.shape}, " - f"loss=N/A (CTC requires labels)\n" + f"✓ Captured HF {kind} output {hf_saved_logits.shape}, " + f"loss=N/A (no token labels)\n" ) else: hf_tokens = bridge_unprocessed.to_tokens(test_text) @@ -1130,10 +1143,12 @@ def cleanup_model(model, model_name_str: str): shift_labels.view(-1), ).item() - if verbose: - loss_str = f"{hf_saved_loss:.4f}" if hf_saved_loss is not None else "N/A" - print(f"✓ Captured HF logits {hf_saved_logits.shape}, " f"loss={loss_str}\n") - del hf_tokens + if verbose: + loss_str = f"{hf_saved_loss:.4f}" if hf_saved_loss is not None else "N/A" + print( + f"✓ Captured HF logits {hf_saved_logits.shape}, " f"loss={loss_str}\n" + ) + del hf_tokens except Exception as e: if verbose: print(f"⚠ Could not capture HF reference outputs: {e}\n") @@ -1154,10 +1169,10 @@ def cleanup_model(model, model_name_str: str): # matmul non-determinism can exceed the float32 default of 1e-3 p1_atol = 1e-3 if dtype == torch.float32 else 5e-3 - # For audio models, reuse the waveform from HF reference capture + # For audio/vision models, reuse the input from HF reference capture _p1_input: Union[str, torch.Tensor] = test_text - if _is_audio and _test_audio is not None: - _p1_input = _test_audio + if _test_modality_input is not None: + _p1_input = _test_modality_input if hf_saved_logits is not None: # Full mode: use pre-captured HF logits (bridge only, 1.0x) @@ -1181,12 +1196,13 @@ def cleanup_model(model, model_name_str: str): print(f"✗ Forward pass benchmark failed: {e}\n") # Capture Phase 1 reference for Phase 3 equivalence comparison. - # Skip for audio models (Phase 3 won't run — no HookedTransformer support). + # Skip for audio/vision models (Phase 3 won't run — no HookedTransformer + # support — and the capture below feeds text, which they cannot accept). # When dtype==float32 (default) and the model natively uses reduced # precision, upcast for maximum accuracy. When the user explicitly # requested a non-float32 dtype, run the reference pass in that dtype # so the entire pipeline honours the requested precision. - if bridge_unprocessed is not None and not _is_audio: + if bridge_unprocessed is not None and not _is_audio and not _is_visual: try: original_dtype = bridge_unprocessed.cfg.dtype needs_upcast = dtype == torch.float32 and original_dtype not in ( @@ -1525,10 +1541,9 @@ def cleanup_model(model, model_name_str: str): try: from transformer_lens.benchmarks.audio import run_audio_benchmarks - test_audio = torch.randn(1, 16000, device=device, dtype=dtype) audio_results = run_audio_benchmarks( bridge_unprocessed, - test_audio=test_audio, + test_audio=_test_modality_input, verbose=verbose, ) for result in audio_results: @@ -1578,6 +1593,58 @@ def cleanup_model(model, model_name_str: str): result.phase = 8 add_result(result) + # ======================================================================== + # Phase 9: Vision Tests (only for vision encoder models — ViT/DeiT, not + # vision+text multimodal models, which Phase 7 covers) + # Runs before Phase 3 so we can reuse bridge_unprocessed before cleanup. + # ======================================================================== + if ( + bridge_unprocessed is not None + and getattr(bridge_unprocessed.cfg, "is_visual_model", False) + and not getattr(bridge_unprocessed.cfg, "is_multimodal", False) + and should_run_phase(9) + ): + current_phase[0] = 9 + if verbose: + print("\n" + "=" * 80) + print("PHASE 9: VISION TESTS") + print("=" * 80) + print("Testing pixel forward pass, caching, representation stability, and decoding.") + print("=" * 80 + "\n") + + try: + from transformer_lens.benchmarks.vision import run_vision_benchmarks + + vision_results = run_vision_benchmarks( + bridge_unprocessed, + test_pixels=_test_modality_input, + verbose=verbose, + ) + for result in vision_results: + result.phase = 9 + results.append(result) + if verbose: + print(result) + + if verbose: + print("\n" + "=" * 80) + print("PHASE 9 COMPLETE") + print("=" * 80) + + except Exception as e: + if verbose: + print(f"\n⚠ Vision tests failed: {e}\n") + results.append( + BenchmarkResult( + name="vision_suite", + passed=False, + severity=BenchmarkSeverity.ERROR, + message=f"Failed to run vision tests: {str(e)}", + details={"error": str(e)}, + phase=9, + ) + ) + # ======================================================================== # PHASE 3: Bridge (processed) + HookedTransformer (processed) # ======================================================================== diff --git a/transformer_lens/benchmarks/multimodal.py b/transformer_lens/benchmarks/multimodal.py index 11698bd50b..f5107902d0 100644 --- a/transformer_lens/benchmarks/multimodal.py +++ b/transformer_lens/benchmarks/multimodal.py @@ -41,54 +41,60 @@ def _prepare_test_inputs(bridge: TransformerBridge): if image is None: return None, None, None - # Build a prompt with the model's image token placeholder. - # Different models use different tokens: + # Build a prompt with the model's image token placeholder. Families disagree + # on which token their processor accepts, and a rejected token raises: # LLava: image_token = "" - # Gemma3: boi_token = "" - # Gemma4: image_token is the expandable placeholder (280 tokens), - # boi_token ("<|image>") is just a marker — use image_token first. - image_token = getattr(bridge.processor, "image_token", None) or getattr( - bridge.processor, "boi_token", "" - ) - prompt = f"{image_token}\nDescribe this image." - try: - if hasattr(bridge.processor, "post_process_generation"): - # Task-prompt captioners (Florence-2 style) map task tokens to - # prompts themselves and ignore free-text instructions. - prompt = "" - else: - # Some processors insert the image placeholders themselves; a - # manual placeholder would then double-count. Probe with a plain - # prompt first and keep it if image tokens were auto-inserted. - # Idefics3-style processors RAISE on image inputs without image - # tokens in the text — a failed probe just means "not - # auto-inserting", so fall back to the placeholder prompt. - image_token_id = getattr(bridge.original_model.config, "image_token_id", None) - if image_token_id is not None: - try: - plain = "Describe this image." - probe = bridge.processor(text=plain, images=image, return_tensors="pt") - if (probe["input_ids"] == image_token_id).any(): - prompt = plain - except Exception: - pass - inputs = bridge.processor(text=prompt, images=image, return_tensors="pt") - input_ids = inputs["input_ids"].to(bridge.cfg.device) - - # Collect all extra kwargs the model's forward() may need - # (pixel_values, image_sizes, pixel_attention_mask, etc.) - extra_kwargs = {} - for key, val in inputs.items(): - if key == "input_ids": - continue - if hasattr(val, "to"): - extra_kwargs[key] = val.to(bridge.cfg.device) - else: - extra_kwargs[key] = val - - return input_ids, extra_kwargs, prompt - except Exception: - return None, None, None + # Gemma3: only boi_token ("") expands; image_token + # ("") is rejected + # Gemma4: only image_token ("<|image|>") expands; boi_token is a marker + if hasattr(bridge.processor, "post_process_generation"): + # Task-prompt captioners (Florence-2 style) map task tokens to prompts + # themselves and ignore free-text instructions. + candidates = [""] + else: + tokens = [ + getattr(bridge.processor, "image_token", None), + getattr(bridge.processor, "boi_token", None), + ] + seen = list(dict.fromkeys(t for t in tokens if t)) or [""] + candidates = [f"{t}\nDescribe this image." for t in seen] + + # Some processors insert the image placeholders themselves; a manual + # placeholder would then double-count. Probe with a plain prompt first + # and prefer it if image tokens were auto-inserted. Idefics3-style + # processors RAISE on image inputs without image tokens in the text — a + # failed probe just means "not auto-inserting", so fall back below. + image_token_id = getattr(bridge.original_model.config, "image_token_id", None) + if image_token_id is not None: + try: + plain = "Describe this image." + probe = bridge.processor(text=plain, images=image, return_tensors="pt") + if (probe["input_ids"] == image_token_id).any(): + candidates.insert(0, plain) + except Exception: + pass + + for prompt in candidates: + try: + inputs = bridge.processor(text=prompt, images=image, return_tensors="pt") + input_ids = inputs["input_ids"].to(bridge.cfg.device) + + # Collect all extra kwargs the model's forward() may need + # (pixel_values, image_sizes, pixel_attention_mask, etc.) + extra_kwargs = {} + for key, val in inputs.items(): + if key == "input_ids": + continue + if hasattr(val, "to"): + extra_kwargs[key] = val.to(bridge.cfg.device) + else: + extra_kwargs[key] = val + + return input_ids, extra_kwargs, prompt + except Exception: + continue + + return None, None, None def benchmark_multimodal_forward( diff --git a/transformer_lens/benchmarks/utils.py b/transformer_lens/benchmarks/utils.py index 1b1b0f1375..7fa1069ca0 100644 --- a/transformer_lens/benchmarks/utils.py +++ b/transformer_lens/benchmarks/utils.py @@ -89,6 +89,54 @@ def filter_expected_missing_hooks(hook_names: Collection[str]) -> list[str]: ] +_DEFAULT_WAVEFORM_SAMPLES = 16000 + + +def build_modality_input( + bridge: Any, + batch_size: int = 1, + device: Optional[Union[str, torch.device]] = None, + dtype: Optional[torch.dtype] = None, +) -> Optional[torch.Tensor]: + """Build a synthetic model input for a non-text bridge, or None for text models. + + Audio and vision models take a raw tensor where text models take token ids, and + the shape is architecture-specific: spectrogram encoders (AST) need + ``[batch, max_length, num_mel_bins]`` where waveform encoders (HuBERT, wav2vec2) + need ``[batch, samples]``, and vision encoders need + ``[batch, num_channels, image_size, image_size]``. Shapes come from the HF config + so a checkpoint with non-default dimensions is still handled correctly. + """ + cfg = getattr(bridge, "cfg", None) + if cfg is None: + return None + + hf_config = getattr(getattr(bridge, "original_model", None), "config", None) + + def _from_config(name: str, default: int) -> int: + value = getattr(hf_config, name, None) if hf_config is not None else None + if value is None: + value = getattr(cfg, name, None) + return int(value) if isinstance(value, int) else default + + if getattr(cfg, "is_visual_model", False): + image_size = _from_config("image_size", 224) + num_channels = _from_config("num_channels", 3) + return torch.randn( + batch_size, num_channels, image_size, image_size, device=device, dtype=dtype + ) + + if getattr(cfg, "is_audio_model", False): + # num_mel_bins is what distinguishes a spectrogram encoder from a waveform one. + num_mel_bins = getattr(hf_config, "num_mel_bins", None) if hf_config is not None else None + if num_mel_bins is None: + return torch.randn(batch_size, _DEFAULT_WAVEFORM_SAMPLES, device=device, dtype=dtype) + max_length = _from_config("max_length", 1024) + return torch.randn(batch_size, max_length, int(num_mel_bins), device=device, dtype=dtype) + + return None + + def safe_allclose( tensor1: torch.Tensor, tensor2: torch.Tensor, diff --git a/transformer_lens/benchmarks/vision.py b/transformer_lens/benchmarks/vision.py new file mode 100644 index 0000000000..186973ab8d --- /dev/null +++ b/transformer_lens/benchmarks/vision.py @@ -0,0 +1,337 @@ +"""Vision benchmarks for TransformerBridge (Phase 9). + +Tests that vision encoder models (ViT, DeiT) correctly handle pixel inputs +through forward(), run_with_cache(), and produce stable representations — +the hook/cache coverage that Phase 1 (HF parity on one forward) doesn't give +non-text models. The audio analog is Phase 8 (audio.py). +""" + +from typing import List, Optional + +import torch + +from transformer_lens.benchmarks.encoder_common import ( + benchmark_encoder_cache, + benchmark_encoder_forward, + benchmark_encoder_representation_stability, +) +from transformer_lens.benchmarks.utils import ( + BenchmarkResult, + BenchmarkSeverity, + build_modality_input, + is_tiny_test_model, +) +from transformer_lens.model_bridge import TransformerBridge + +# Top-level component-mapping names whose hook_out must be cached when the +# architecture declares them (unembed = classifier head, absent on bare encoders). +_CRITICAL_VISION_COMPONENTS = ("embed", "ln_final", "unembed") + + +def benchmark_vision_forward( + bridge: TransformerBridge, + test_pixels: torch.Tensor, + reference_model: Optional[torch.nn.Module] = None, +) -> BenchmarkResult: + """Benchmark forward pass with pixel input. + + Compares bridge output against the HF native model on the same pixels. + Bare encoders (ViTModel) compare last_hidden_state; classification heads + compare logits. + + Args: + bridge: TransformerBridge model to test + test_pixels: Pixel tensor [batch, channels, height, width] + reference_model: Optional HF reference model for comparison + """ + return benchmark_encoder_forward( + bridge, + test_pixels, + name="vision_forward", + ref_input_key="pixel_values", + reference_model=reference_model, + ) + + +def benchmark_vision_cache( + bridge: TransformerBridge, + test_pixels: torch.Tensor, +) -> BenchmarkResult: + """Benchmark run_with_cache() for vision models. + + Verifies that critical vision hooks fire and produce valid tensors: the + patch embeddings, final layernorm, classifier head (when present), and the + first and last block. + + Args: + bridge: TransformerBridge model to test + test_pixels: Pixel tensor [batch, channels, height, width] + """ + return benchmark_encoder_cache( + bridge, + test_pixels, + name="vision_cache", + critical_components=_CRITICAL_VISION_COMPONENTS, + ) + + +def benchmark_vision_representation_stability( + bridge: TransformerBridge, + test_pixels: torch.Tensor, +) -> BenchmarkResult: + """Benchmark representation stability under small pixel perturbations. + + Args: + bridge: TransformerBridge model to test + test_pixels: Pixel tensor [batch, channels, height, width] + """ + return benchmark_encoder_representation_stability( + bridge, + test_pixels, + name="vision_representation_stability", + ) + + +def benchmark_vision_embeddings( + bridge: TransformerBridge, + test_pixels: torch.Tensor, +) -> BenchmarkResult: + """Verify patch-embedding hook outputs. + + Checks that embed.hook_out produces [batch, seq, d_model] tensors with + non-degenerate values — the vision analog of the audio feature-extractor + check. Seq length is architecture-dependent (ViT: patches + CLS; DeiT: + patches + CLS + distillation token), so only lower-bounded here. + + Args: + bridge: TransformerBridge model to test + test_pixels: Pixel tensor [batch, channels, height, width] + """ + try: + if "embed" not in (bridge.adapter.component_mapping or {}): + return BenchmarkResult( + name="vision_embeddings", + severity=BenchmarkSeverity.SKIPPED, + message="Skipped: architecture declares no embed component", + ) + + with torch.no_grad(): + _, cache = bridge.run_with_cache(test_pixels) + + hook_key = "embed.hook_out" + if hook_key not in cache: + return BenchmarkResult( + name="vision_embeddings", + severity=BenchmarkSeverity.DANGER, + message=f"Hook '{hook_key}' not found in cache", + passed=False, + ) + + embeddings = cache[hook_key] + + if embeddings.dim() != 3: + return BenchmarkResult( + name="vision_embeddings", + severity=BenchmarkSeverity.DANGER, + message=f"Expected 3D tensor [batch, seq, d_model], got {embeddings.dim()}D", + passed=False, + details={"shape": str(embeddings.shape)}, + ) + + d_model = bridge.cfg.d_model + if embeddings.shape[0] != test_pixels.shape[0] or embeddings.shape[-1] != d_model: + return BenchmarkResult( + name="vision_embeddings", + severity=BenchmarkSeverity.DANGER, + message=f"Embedding shape {tuple(embeddings.shape)} does not match " + f"[batch={test_pixels.shape[0]}, seq, d_model={d_model}]", + passed=False, + details={"shape": str(embeddings.shape), "d_model": d_model}, + ) + + is_all_zeros = embeddings.abs().max().item() == 0 + has_nan = torch.isnan(embeddings).any().item() + has_inf = torch.isinf(embeddings).any().item() + + if is_all_zeros or has_nan or has_inf: + issues = [] + if is_all_zeros: + issues.append("all zeros") + if has_nan: + issues.append("NaN") + if has_inf: + issues.append("Inf") + return BenchmarkResult( + name="vision_embeddings", + severity=BenchmarkSeverity.DANGER, + message=f"Degenerate embedding values: {', '.join(issues)}", + passed=False, + details={"shape": str(embeddings.shape), "issues": issues}, + ) + + return BenchmarkResult( + name="vision_embeddings", + severity=BenchmarkSeverity.INFO, + message=f"Patch embeddings OK: shape={embeddings.shape}, " + f"mean={embeddings.mean().item():.4f}, std={embeddings.std().item():.4f}", + details={ + "shape": str(embeddings.shape), + "mean": embeddings.mean().item(), + "std": embeddings.std().item(), + }, + ) + + except Exception as e: + return BenchmarkResult( + name="vision_embeddings", + severity=BenchmarkSeverity.ERROR, + message=f"Patch embedding check failed: {str(e)}", + passed=False, + ) + + +def benchmark_vision_classification_decode( + bridge: TransformerBridge, +) -> BenchmarkResult: + """Benchmark image-classification decoding on a real image. + + Loads the cats-image fixture, preprocesses it with the bridge's image + processor, and reports the top predicted labels — the vision analog of the + audio CTC decode, exercising the processor wiring that synthetic pixel + tensors don't. Skipped for bare encoders (no classifier head), tiny-random + models, and when no processor/datasets are available. + + Args: + bridge: TransformerBridge model to test + """ + model_name = getattr(bridge.cfg, "model_name", "") + if is_tiny_test_model(model_name): + return BenchmarkResult( + name="vision_classification_decode", + severity=BenchmarkSeverity.SKIPPED, + message="Skipped for tiny-random model (untrained classifier head)", + ) + + # Bare encoders return hidden states from return_type="logits", so gate on + # the adapter-declared classifier head rather than the output shape. + if "unembed" not in (bridge.adapter.component_mapping or {}): + return BenchmarkResult( + name="vision_classification_decode", + severity=BenchmarkSeverity.SKIPPED, + message="Skipped: bare encoder (no classifier head)", + ) + + processor = getattr(bridge, "processor", None) + if processor is None: + return BenchmarkResult( + name="vision_classification_decode", + severity=BenchmarkSeverity.SKIPPED, + message="Skipped: no image processor available on the bridge", + ) + + try: + from datasets import load_dataset + + ds = load_dataset("huggingface/cats-image", split="test", trust_remote_code=True) + image = ds[0]["image"] + + inputs = processor(images=image, return_tensors="pt") + pixel_values = inputs["pixel_values"].to(bridge.cfg.device) + + with torch.no_grad(): + # Classifier heads return the logits tensor directly; bare encoders + # fall through to a BaseModelOutput with no logits attribute. + output = bridge(pixel_values, return_type="logits") + + logits = output if isinstance(output, torch.Tensor) else getattr(output, "logits", None) + if logits is None or logits.ndim != 2: + return BenchmarkResult( + name="vision_classification_decode", + severity=BenchmarkSeverity.DANGER, + message="Classifier model produced no [batch, num_labels] logits", + passed=False, + ) + + k = min(5, logits.shape[-1]) + top = torch.topk(logits[0], k=k) + id2label = getattr(getattr(bridge, "original_model", None), "config", None) + id2label = getattr(id2label, "id2label", None) or {} + top_labels = [id2label.get(int(i), str(int(i))) for i in top.indices] + + return BenchmarkResult( + name="vision_classification_decode", + severity=BenchmarkSeverity.INFO, + message=f"Classification decode successful: top-1 = {top_labels[0]!r}", + details={ + "top_labels": top_labels, + "top_logits": [round(float(v), 4) for v in top.values], + "logits_shape": str(logits.shape), + }, + ) + + except ImportError: + return BenchmarkResult( + name="vision_classification_decode", + severity=BenchmarkSeverity.SKIPPED, + message="Skipped: 'datasets' package not available", + ) + except Exception as e: + return BenchmarkResult( + name="vision_classification_decode", + severity=BenchmarkSeverity.ERROR, + message=f"Classification decode failed: {str(e)}", + passed=False, + ) + + +def run_vision_benchmarks( + bridge: TransformerBridge, + test_pixels: Optional[torch.Tensor] = None, + verbose: bool = True, +) -> List[BenchmarkResult]: + """Run all vision benchmarks. + + Args: + bridge: TransformerBridge model to test + test_pixels: Optional pixel tensor. If None, generates a synthetic input + shaped for this architecture from the HF config. + verbose: Whether to print progress + + Returns: + List of BenchmarkResult objects + """ + if test_pixels is None: + test_pixels = build_modality_input(bridge, device=bridge.cfg.device, dtype=bridge.cfg.dtype) + if test_pixels is None: + return [ + BenchmarkResult( + name="vision_forward", + severity=BenchmarkSeverity.ERROR, + message="Could not build a pixel input for this model", + passed=False, + ) + ] + + results = [] + + if verbose: + print("1. Vision Forward Pass") + results.append(benchmark_vision_forward(bridge, test_pixels)) + + if verbose: + print("2. Vision Cache Verification") + results.append(benchmark_vision_cache(bridge, test_pixels)) + + if verbose: + print("3. Representation Stability") + results.append(benchmark_vision_representation_stability(bridge, test_pixels)) + + if verbose: + print("4. Patch Embedding Verification") + results.append(benchmark_vision_embeddings(bridge, test_pixels)) + + if verbose: + print("5. Classification Decoding") + results.append(benchmark_vision_classification_decode(bridge)) + + return results diff --git a/transformer_lens/components/abstract_attention.py b/transformer_lens/components/abstract_attention.py index 9b449fd00b..d46c37b878 100644 --- a/transformer_lens/components/abstract_attention.py +++ b/transformer_lens/components/abstract_attention.py @@ -141,6 +141,11 @@ def __init__( raise ValueError("Layer ID must be provided to scale attention scores") self.attn_scale *= self.layer_id + 1 + if self.cfg.use_attention_sinks: + # Learned per-head sink logit (GPT-OSS); joins the softmax as an + # extra key column and is dropped afterward. + self.sinks = nn.Parameter(torch.zeros(self.cfg.n_heads, dtype=self.cfg.dtype)) + self.hook_k = HookPoint() # [batch, pos, head_index, d_head] self.hook_q = HookPoint() # [batch, pos, head_index, d_head] self.hook_v = HookPoint() # [batch, pos, head_index, d_head] @@ -321,7 +326,21 @@ def forward( attn_scores += additive_attention_mask attn_scores = self.hook_attn_scores(attn_scores) - pattern = F.softmax(attn_scores, dim=-1) + if self.cfg.use_attention_sinks: + # The sink column is appended after hook_attn_scores so hooks keep + # the [batch, head, query_pos, key_pos] shape, and dropped after + # the softmax — every real position's weight is scaled down by the + # sink's share, so pattern rows sum to less than 1. + sink = ( + self.sinks.reshape(1, -1, 1, 1) + .expand(attn_scores.shape[0], -1, attn_scores.shape[-2], -1) + .to(attn_scores.dtype) + ) + combined = torch.cat([attn_scores, sink], dim=-1) + combined = combined - combined.max(dim=-1, keepdim=True).values + pattern = F.softmax(combined, dim=-1)[..., :-1] + else: + pattern = F.softmax(attn_scores, dim=-1) if not isinstance(pattern, torch.Tensor): raise TypeError(f"Expected 'pattern' to be a Tensor, got {type(pattern)}") pattern = torch.where(torch.isnan(pattern), torch.zeros_like(pattern), pattern) @@ -635,6 +654,10 @@ def calculate_sin_cos_rotary( pos = torch.arange(n_ctx, dtype=high_precision) dim = torch.arange(rotary_dim // 2, dtype=high_precision) + use_yarn = self.cfg.use_yarn_rope and not ( + self.cfg.yarn_global_attn_only and self.attn_type == "local" + ) + # Llama-3.1 uses NTK-by-Parts Rotary Embedding introduced in Section 3.2 in https://arxiv.org/pdf/2309.00071 # Implementation copied from https://github.com/huggingface/transformers/blob/v4.46.0/src/transformers/modeling_rope_utils.py#L310 if self.cfg.use_NTK_by_parts_rope: @@ -660,7 +683,7 @@ def calculate_sin_cos_rotary( is_medium_freq = ~(wavelen < high_freq_wavelen) * ~(wavelen > low_freq_wavelen) inv_freq_llama = torch.where(is_medium_freq, smoothed_inv_freq, inv_freq_llama) freq = 1 / inv_freq_llama - elif self.cfg.use_yarn_rope: + elif use_yarn: # YARN (Yet Another RoPE extensioN) from https://arxiv.org/abs/2309.00071 # Implementation follows HuggingFace: transformers/modeling_rope_utils.py inv_freq = 1.0 / ( @@ -678,8 +701,11 @@ def _find_correction_dim(num_rotations: float) -> float: 2 * math.log(base) ) - low = math.floor(_find_correction_dim(beta_fast)) - high = math.ceil(_find_correction_dim(beta_slow)) + low = _find_correction_dim(beta_fast) + high = _find_correction_dim(beta_slow) + if self.cfg.yarn_truncate: + low = math.floor(low) + high = math.ceil(high) low = max(low, 0) high = min(high, rotary_dim - 1) @@ -707,7 +733,7 @@ def _find_correction_dim(num_rotations: float) -> float: angles = pos[:, None] / freq[None, :] sin, cos = torch.sin(angles).to(dtype), torch.cos(angles).to(dtype) # YARN attention_factor scales the embeddings (default 1.0 is a no-op) - if self.cfg.use_yarn_rope and self.cfg.yarn_attention_factor != 1.0: + if use_yarn and self.cfg.yarn_attention_factor != 1.0: sin = sin * self.cfg.yarn_attention_factor cos = cos * self.cfg.yarn_attention_factor return sin, cos diff --git a/transformer_lens/config/hooked_transformer_config.py b/transformer_lens/config/hooked_transformer_config.py index b726b60662..cc9df219c1 100644 --- a/transformer_lens/config/hooked_transformer_config.py +++ b/transformer_lens/config/hooked_transformer_config.py @@ -218,6 +218,14 @@ class HookedTransformerConfig(TransformerLensConfig): YARN. Defaults to 1.0. yarn_beta_fast (float): Upper rotation threshold for YARN correction range. Defaults to 32. yarn_beta_slow (float): Lower rotation threshold for YARN correction range. Defaults to 1. + yarn_truncate (bool): Whether to floor/ceil the YARN correction-range bounds + (HF's `truncate`). GPT-OSS ships truncate=False. Defaults to True. + yarn_global_attn_only (bool): Whether YARN applies only to global-attention + layers, with sliding/local layers keeping plain rope (Olmo-3's + per-layer-type rope). Defaults to False. + use_attention_sinks (bool): Whether attention carries a learned per-head sink + logit (GPT-OSS) that joins the softmax as an extra key column and is + dropped afterward. Defaults to False. yarn_original_max_position_embeddings (int): The original max position embeddings before YARN extension. Defaults to 4096. use_qk_norm (bool): Whether to apply RMSNorm to the query and key projections before @@ -296,6 +304,16 @@ class HookedTransformerConfig(TransformerLensConfig): yarn_beta_fast: float = 32.0 yarn_beta_slow: float = 1.0 yarn_original_max_position_embeddings: int = 4096 + # HF yarn's `truncate` option: floor/ceil the correction range bounds. + # GPT-OSS ships truncate=False, keeping the bounds fractional. + yarn_truncate: bool = True + # Per-layer-type rope (Olmo-3): YARN applies only on global-attention + # layers; sliding/local layers keep plain rope. + yarn_global_attn_only: bool = False + # GPT-OSS: learned per-head sink logit that joins the attention softmax as + # an extra key column and is dropped afterward, so real positions share + # probability mass with the sink. + use_attention_sinks: bool = False norm_topk_prob: bool = False def __post_init__(self): diff --git a/transformer_lens/config/transformer_bridge_config.py b/transformer_lens/config/transformer_bridge_config.py index 7b183f0970..ddfa446de5 100644 --- a/transformer_lens/config/transformer_bridge_config.py +++ b/transformer_lens/config/transformer_bridge_config.py @@ -90,6 +90,8 @@ def __init__( attn_implementation: Optional[str] = None, # Audio model configuration is_audio_model: bool = False, + # Vision model (ViT, DeiT) configuration + is_visual_model: bool = False, # Stateful model configuration (e.g., Mamba SSMs use cache_params, # not past_key_values, so generation delegates to hf_generate) is_stateful: bool = False, @@ -185,6 +187,8 @@ def __init__( self.attn_implementation = attn_implementation # Audio model configuration self.is_audio_model = is_audio_model + # Vision model (ViT, DeiT) configuration + self.is_visual_model = is_visual_model # Stateful model configuration self.is_stateful = is_stateful # Multimodal configuration diff --git a/transformer_lens/factories/architecture_adapter_factory.py b/transformer_lens/factories/architecture_adapter_factory.py index 5def698529..fb72d05967 100644 --- a/transformer_lens/factories/architecture_adapter_factory.py +++ b/transformer_lens/factories/architecture_adapter_factory.py @@ -13,6 +13,7 @@ AfmoeArchitectureAdapter, ApertusArchitectureAdapter, ArceeArchitectureAdapter, + ASTArchitectureAdapter, AudioFlamingo3ArchitectureAdapter, BaichuanArchitectureAdapter, BambaArchitectureAdapter, @@ -147,6 +148,7 @@ T5Gemma2ArchitectureAdapter, T5GemmaArchitectureAdapter, VaultGemmaArchitectureAdapter, + ViTArchitectureAdapter, XGLMArchitectureAdapter, YoutuArchitectureAdapter, Zamba2ArchitectureAdapter, @@ -157,6 +159,7 @@ "AfmoeForCausalLM": AfmoeArchitectureAdapter, "ApertusForCausalLM": ApertusArchitectureAdapter, "ArceeForCausalLM": ArceeArchitectureAdapter, + "ASTForAudioClassification": ASTArchitectureAdapter, "BaiChuanForCausalLM": BaichuanArchitectureAdapter, "BaichuanForCausalLM": BaichuanArchitectureAdapter, "BambaForCausalLM": BambaArchitectureAdapter, @@ -310,6 +313,10 @@ "MinGPTForCausalLM": MingptArchitectureAdapter, "GPTNeoForCausalLM": NeoArchitectureAdapter, "GPTNeoXForCausalLM": NeoxArchitectureAdapter, + "ViTModel": ViTArchitectureAdapter, + "ViTForImageClassification": ViTArchitectureAdapter, + "DeiTModel": ViTArchitectureAdapter, + "DeiTForImageClassification": ViTArchitectureAdapter, # "DeiTForImageClassificationWithTeacher" unsupported for now } diff --git a/transformer_lens/loading_from_pretrained.py b/transformer_lens/loading_from_pretrained.py index f5940adaae..7411ac84e3 100644 --- a/transformer_lens/loading_from_pretrained.py +++ b/transformer_lens/loading_from_pretrained.py @@ -7,6 +7,7 @@ import dataclasses import logging +import math import os import re from pathlib import Path @@ -23,6 +24,7 @@ T5ForConditionalGeneration, Wav2Vec2Model, ) +from transformers.utils.quantization_config import Mxfp4Config import transformer_lens.utilities as utils from transformer_lens.config.hooked_transformer_config import HookedTransformerConfig @@ -667,11 +669,39 @@ def convert_hf_model_config(model_name: str, **kwargs: Any) -> dict[str, Any]: "n_key_value_heads": hf_config.num_key_value_heads, "gated_mlp": True, "final_rms": True, - "use_local_attn": False, "rotary_dim": hf_config.head_dim, "num_experts": hf_config.num_local_experts, "experts_per_token": hf_config.num_experts_per_tok, + "use_attention_sinks": True, + # Alternating sliding_attention / full_attention layers; HT's local + # attention mask (last window_size keys) matches HF's sliding window. + "use_local_attn": True, + "window_size": hf_config.sliding_window, + "attn_types": [ + "local" if layer_type == "sliding_attention" else "global" + for layer_type in hf_config.layer_types + ], } + rope_params = getattr(hf_config, "rope_parameters", None) or {} + if rope_params.get("rope_type") == "yarn": + yarn_factor = rope_params["factor"] + attention_factor = rope_params.get("attention_factor") + if attention_factor is None: + # HF's default: get_mscale(factor) = 0.1 * ln(factor) + 1 + attention_factor = 0.1 * math.log(yarn_factor) + 1.0 if yarn_factor > 1 else 1.0 + cfg_dict.update( + { + "use_yarn_rope": True, + "yarn_factor": float(yarn_factor), + "yarn_attention_factor": float(attention_factor), + "yarn_beta_fast": float(rope_params.get("beta_fast") or 32.0), + "yarn_beta_slow": float(rope_params.get("beta_slow") or 1.0), + "yarn_original_max_position_embeddings": rope_params[ + "original_max_position_embeddings" + ], + "yarn_truncate": rope_params.get("truncate", True), + } + ) elif architecture == "BloomForCausalLM": cfg_dict = { "d_model": hf_config.hidden_size, @@ -1461,9 +1491,30 @@ def convert_hf_model_config(model_name: str, **kwargs: Any) -> dict[str, Any]: "gated_mlp": True, "tie_word_embeddings": hf_config.tie_word_embeddings, } - # OLMo 3 uses YARN RoPE scaling + # OLMo 3 uses per-layer-type rope (transformers 5.x): plain rope on + # sliding_attention layers, YARN on full_attention layers. + rope_params = getattr(hf_config, "rope_parameters", None) rope_scaling = getattr(hf_config, "rope_scaling", None) - if rope_scaling and rope_scaling.get("rope_type") == "yarn": + if isinstance(rope_params, dict) and "full_attention" in rope_params: + full_rope = rope_params["full_attention"] or {} + sliding_rope = rope_params.get("sliding_attention") or {} + cfg_dict["rotary_base"] = full_rope.get("rope_theta", 500000.0) + sliding_theta = sliding_rope.get("rope_theta") + if sliding_theta is not None and sliding_theta != cfg_dict["rotary_base"]: + cfg_dict["rotary_base_local"] = sliding_theta + if full_rope.get("rope_type") == "yarn": + cfg_dict["use_yarn_rope"] = True + cfg_dict["yarn_global_attn_only"] = True + cfg_dict["yarn_factor"] = float(full_rope.get("factor", 8.0)) + cfg_dict["yarn_attention_factor"] = float(full_rope.get("attention_factor", 1.0)) + cfg_dict["yarn_beta_fast"] = float(full_rope.get("beta_fast") or 32.0) + cfg_dict["yarn_beta_slow"] = float(full_rope.get("beta_slow") or 1.0) + cfg_dict["yarn_original_max_position_embeddings"] = full_rope.get( + "original_max_position_embeddings", 4096 + ) + cfg_dict["yarn_truncate"] = full_rope.get("truncate", True) + elif rope_scaling and rope_scaling.get("rope_type") == "yarn": + # transformers < 5.0 flat rope_scaling dict cfg_dict["use_yarn_rope"] = True cfg_dict["yarn_factor"] = rope_scaling.get("factor", 8.0) cfg_dict["yarn_attention_factor"] = rope_scaling.get("attention_factor", 1.0) @@ -1477,6 +1528,11 @@ def convert_hf_model_config(model_name: str, **kwargs: Any) -> dict[str, Any]: cfg_dict["attn_types"] = [ "local" if t == "sliding_attention" else "global" for t in layer_types ] + # Without use_local_attn, attn_types is inert and every layer runs + # global attention with no sliding mask. + if "sliding_attention" in layer_types and hf_config.sliding_window: + cfg_dict["use_local_attn"] = True + cfg_dict["window_size"] = hf_config.sliding_window else: cfg_dict["attn_types"] = ["global"] * hf_config.num_hidden_layers elif architecture == "OlmoeForCausalLM": @@ -1808,6 +1864,31 @@ def get_checkpoint_labels(model_name: str, **kwargs: Any) -> tuple[list[int], st # %% Loading state dicts +def _mxfp4_dequantize_config( + official_model_name: str, + cfg: HookedTransformerConfig, + token: str | None, +) -> Any | None: + """Return ``Mxfp4Config(dequantize=True)`` for packed-MXFP4 gpt-oss checkpoints. + + The gpt-oss weight converter slices expert tensors, which only works on + materialized torch.Tensors — packed MXFP4 weights stay wrapped in + triton-kernels objects. Returns None for anything else, including + already-dequantized gpt-oss finetunes. + """ + if cfg.original_architecture != "GptOssForCausalLM": + return None + hf_cfg = AutoConfig.from_pretrained(official_model_name, token=token) + quant_cfg = getattr(hf_cfg, "quantization_config", None) + if isinstance(quant_cfg, dict): + quant_method = quant_cfg.get("quant_method") + else: + quant_method = getattr(quant_cfg, "quant_method", None) + if quant_method != "mxfp4": + return None + return Mxfp4Config(dequantize=True) + + def get_pretrained_state_dict( official_model_name: str, cfg: HookedTransformerConfig, @@ -1928,6 +2009,14 @@ def get_pretrained_state_dict( **kwargs, ) else: + if "quantization_config" not in kwargs: + mxfp4_dequantize = _mxfp4_dequantize_config( + official_model_name, + cfg, + huggingface_token if len(huggingface_token) > 0 else None, + ) + if mxfp4_dequantize is not None: + kwargs = {**kwargs, "quantization_config": mxfp4_dequantize} # Older models may lack pad_token_id (required in newer transformers) try: hf_model = AutoModelForCausalLM.from_pretrained( @@ -2045,6 +2134,24 @@ def fill_missing_keys( """ default_state_dict = model.state_dict() missing_keys = set(default_state_dict.keys()) - set(state_dict.keys()) + # A missing attention weight matrix means a converter/component naming + # mismatch (e.g. W_K written where GroupedQueryAttention expects _W_K). + # Filling it with an empty tensor silently zeroes the sublayer while every + # downstream number still looks plausible — fail loudly instead. + attention_weight_names = {"W_Q", "W_K", "W_V", "W_O", "_W_K", "_W_V"} + missing_attention_weights = sorted( + key + for key in missing_keys + if "hf_model" not in key and key.rsplit(".", 1)[-1] in attention_weight_names + ) + if missing_attention_weights: + raise ValueError( + f"Pretrained state dict is missing attention weight matrices the model " + f"expects: {missing_attention_weights}. This usually means the weight " + f"converter and the instantiated attention module disagree on parameter " + f"naming (e.g. GQA's underscore-prefixed _W_K/_W_V vs W_K/W_V). Refusing " + f"to zero-fill them, which would silently produce wrong outputs." + ) for key in missing_keys: if "hf_model" in key: # Skip keys that are from the HuggingFace model, if loading from HF. diff --git a/transformer_lens/model_bridge/bridge.py b/transformer_lens/model_bridge/bridge.py index f48fbae532..3cec4a623f 100644 --- a/transformer_lens/model_bridge/bridge.py +++ b/transformer_lens/model_bridge/bridge.py @@ -844,7 +844,7 @@ def enable_compatibility_mode( if not getattr(self.adapter, "supports_compatibility_mode", True): raise RuntimeError( f"{type(self.adapter).__name__} does not support compatibility mode: " - "its stored-processed-weights forward is known to diverge from the " + "its stored-processed-weights is known to diverge from the " "reference model. Use the default bridge forward instead." ) @@ -1760,9 +1760,10 @@ def forward( (fragile across HF versions) or exception-based layer skipping (corrupts model state). Raises NotImplementedError if a non-None value is passed. stop_at_layer: Layer to stop forward pass at - pixel_values: Optional image tensor for multimodal models (e.g., LLaVA, Gemma3). + pixel_values: Optional image tensor for multimodal models (e.g., LLaVA, Gemma3) + and vision models (eg. ViT, DeiT). The tensor is passed directly to the underlying HuggingFace model. - Only valid when cfg.is_multimodal is True. + Only valid when cfg.is_multimodal is True or cfg.is_visual_model is True. input_values: Optional audio waveform tensor for audio models (e.g., HuBERT). The tensor is passed directly to the underlying HuggingFace model. Only valid when cfg.is_audio_model is True. @@ -1822,6 +1823,7 @@ def forward( 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) ) try: @@ -1831,6 +1833,11 @@ def forward( "Audio models require tensor input (raw waveform), not text. " "Pass a torch.Tensor or use the input_values parameter." ) + if getattr(self.cfg, "is_visual_model", False): + raise ValueError( + "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 @@ -1918,10 +1925,13 @@ def forward( # Handle pixel_values for multimodal models if pixel_values is not None: - if not getattr(self.cfg, "is_multimodal", False): + if not ( + getattr(self.cfg, "is_multimodal", False) + or getattr(self.cfg, "is_visual_model", False) + ): raise ValueError( - "pixel_values can only be passed to multimodal models " - "(cfg.is_multimodal must be True)" + "pixel_values can only be passed to multimodal or vision models " + "(cfg.is_multimodal or cfg.is_visual_model must be True)" ) kwargs["pixel_values"] = pixel_values @@ -1946,6 +1956,19 @@ def forward( "Audio models require tensor input (raw waveform). " "Pass a torch.Tensor or use input_values parameter." ) + elif getattr(self.cfg, "is_visual_model", False): + # "pixel_values" may already be in kwargs from the "if pixel_values is not None:" + # gate above (explicit pixel_values=... call); otherwise treat `input` itself as + # the image tensor, matching how the audio branch treats `input` as the waveform. + if "pixel_values" not in kwargs: + if isinstance(input, torch.Tensor): + kwargs["pixel_values"] = input + else: + raise ValueError( + "Visual models require tensor input (pixel values). " + "Pass a torch.Tensor as `input` or use the pixel_values parameter." + ) + output = self.original_model(**kwargs) elif _is_inputs_embeds: output = self.original_model(inputs_embeds=input_ids, **kwargs) else: @@ -1957,6 +1980,13 @@ def forward( logits = output.logits elif isinstance(output, tuple) and len(output) > 0: 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, + # which has neither `.logits` nor tuple semantics. Fall back to + # `last_hidden_state` so return_type="logits" still yields a plain + # tensor rather than silently handing back the raw HF output object. + logits = output.last_hidden_state else: logits = output if return_type == "logits": @@ -1970,6 +2000,14 @@ def forward( "Audio models do not support return_type='loss'. " "CTC loss requires aligned frame-level labels." ) + if getattr(self.cfg, "is_visual_model", False): + raise ValueError( + "Vision classification models do not support return_type='loss' " + "via this path (no next-token LM target exists for image " + "classification). Compute cross-entropy against `labels` " + "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: raise ValueError( "Cannot compute loss with inputs_embeds — token IDs required for labels." diff --git a/transformer_lens/model_bridge/component_setup.py b/transformer_lens/model_bridge/component_setup.py index fc21f99eac..6294247b68 100644 --- a/transformer_lens/model_bridge/component_setup.py +++ b/transformer_lens/model_bridge/component_setup.py @@ -150,6 +150,71 @@ def setup_submodules( # Clean up so architecture_adapter traversal won't find stale entries for name in skipped_optional: component.submodules.pop(name, None) + if skipped_optional: + _prune_hook_aliases_for_skipped(component, skipped_optional) + + +def _prune_hook_aliases_for_skipped(component: GeneralizedComponent, skipped: list[str]) -> None: + """Remove aliases targeting skipped optional submodules. + + An overridden alias falls back to its class-level target when that target is + live on the current component. + """ + aliases = getattr(component, "hook_aliases", None) + if not aliases: + return + skipped_set = set(skipped) + default_aliases = type(component).hook_aliases + # Deepcopied blocks still share the class-level dict until mutated. + if aliases is type(component).hook_aliases: + aliases = dict(aliases) + component.hook_aliases = aliases + + def _first_segment(path: str) -> str: + return path.split(".", 1)[0] + + def _live_fallback(alias_name: str) -> str | list[str] | None: + fallback = default_aliases.get(alias_name) + if fallback is None: + return None + targets = fallback if isinstance(fallback, list) else [fallback] + live_targets = [] + for target in targets: + if _first_segment(target) in skipped_set: + continue + current: nn.Module = component + for part in target.split("."): + modules = object.__getattribute__(current, "_modules") + next_component = modules.get(part) + if next_component is None: + break + current = next_component + else: + live_targets.append(target) + if not live_targets: + return None + return live_targets if isinstance(fallback, list) else live_targets[0] + + to_drop: list[str] = [] + for alias_name, target in aliases.items(): + if isinstance(target, list): + kept = [t for t in target if _first_segment(t) not in skipped_set] + if not kept: + fallback = _live_fallback(alias_name) + if fallback is None: + to_drop.append(alias_name) + else: + aliases[alias_name] = fallback + elif len(kept) != len(target): + aliases[alias_name] = kept + elif _first_segment(target) in skipped_set: + fallback = _live_fallback(alias_name) + if fallback is None: + to_drop.append(alias_name) + else: + aliases[alias_name] = fallback + for alias_name in to_drop: + aliases.pop(alias_name, None) def setup_components( diff --git a/transformer_lens/model_bridge/generalized_components/__init__.py b/transformer_lens/model_bridge/generalized_components/__init__.py index d8e0cc828c..00225510b6 100644 --- a/transformer_lens/model_bridge/generalized_components/__init__.py +++ b/transformer_lens/model_bridge/generalized_components/__init__.py @@ -137,6 +137,12 @@ from transformer_lens.model_bridge.generalized_components.vision_projection import ( VisionProjectionBridge, ) +from transformer_lens.model_bridge.generalized_components.vision_embeddings import ( + VisionEmbeddingsBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( + VisionClassifierHeadBridge, +) __all__ = [ "AttentionBridge", @@ -193,4 +199,6 @@ "SSMBlockBridge", "SSMMixerBridge", "VisionProjectionBridge", + "VisionEmbeddingsBridge", + "VisionClassifierHeadBridge", ] diff --git a/transformer_lens/model_bridge/generalized_components/mlp.py b/transformer_lens/model_bridge/generalized_components/mlp.py index afe72332af..a3a0af0387 100644 --- a/transformer_lens/model_bridge/generalized_components/mlp.py +++ b/transformer_lens/model_bridge/generalized_components/mlp.py @@ -4,6 +4,8 @@ """ from typing import Any, Dict, Optional +import torch + from transformer_lens.model_bridge.generalized_components.base import ( GeneralizedComponent, ) @@ -18,11 +20,8 @@ class MLPBridge(GeneralizedComponent): hook_aliases = {"hook_pre": "in.hook_out", "hook_post": "out.hook_in"} property_aliases = { - "W_gate": "gate.weight", "b_gate": "gate.bias", - "W_in": "in.weight", "b_in": "in.bias", - "W_out": "out.weight", "b_out": "out.bias", } @@ -81,3 +80,84 @@ def forward(self, *args, **kwargs) -> Any: if hasattr(self, "out") and hasattr(self.out, "hook_out"): output = self.out.hook_out(output) return output + + def _weight_layout_in_out(self, proj: Any) -> Optional[bool]: + """Whether proj's wrapped module stores its weight as [in, out]. + + Conv1D (GPT-2 style) stores [in_features, out_features]; nn.Linear stores + [out_features, in_features]. Returns None when the wrapped module is + neither, so callers can fall back to in_features/out_features or shape heuristic. + """ + from transformers.pytorch_utils import Conv1D + + component = getattr(proj, "original_component", None) + if isinstance(component, Conv1D): + return True + if isinstance(component, torch.nn.Linear): + return False + return None + + def _normalize_mlp_weight( + self, weight: torch.Tensor, layout: Optional[bool], proj: Any, pattern: str = "in" + ) -> torch.Tensor: + """Normalize MLP weight to TL orientation. + + Args: + weight: 2D weight tensor from the projection + layout: True if [in, out] (Conv1D), False if [out, in] (nn.Linear), + None falls back to in_features/out_features or shape heuristic. + proj: The projection module (LinearBridge) for metadata fallback. + pattern: "in" for W_in/W_gate [d_model, d_mlp], "out" for W_out [d_mlp, d_model] + + Note: Shape heuristic assumes d_model < d_mlp, which fails for architectures + like GIDD's ScaledLinear where d_mlp < d_model. When layout is None, we first + check in_features/out_features; shape heuristic is last resort. + """ + if layout is None: + # Try in_features/out_features from the wrapped module (works for bare nn.Module) + component = getattr(proj, "original_component", None) + in_f = getattr(component, "in_features", None) + out_f = getattr(component, "out_features", None) + if in_f is not None and out_f is not None: + # Module declares its orientation; weight[0] == out_f means [out, in] layout + layout = weight.shape[0] == in_f # True if [in, out], False if [out, in] + else: + # Last resort: shape heuristic. WARNING: assumes d_model < d_mlp. + # This will mis-orient architectures where d_mlp < d_model. + if pattern == "in": + layout = weight.shape[0] < weight.shape[1] + else: + layout = weight.shape[0] > weight.shape[1] + if layout: + return weight # Conv1D-style: already in TL orientation + return weight.T # nn.Linear-style: transpose to TL orientation + + @property + def W_in(self) -> torch.Tensor: + """MLP input weight in TL orientation [d_model, d_mlp].""" + in_module = getattr(self, "in", None) + if in_module is None: + raise AttributeError("No 'in' submodule on this MLP bridge") + weight = in_module.weight + layout = self._weight_layout_in_out(in_module) + return self._normalize_mlp_weight(weight, layout, in_module, pattern="in") + + @property + def W_gate(self) -> Optional[torch.Tensor]: + """MLP gate weight in TL orientation [d_model, d_mlp], or None if ungated.""" + gate_module = getattr(self, "gate", None) + if gate_module is None: + return None + weight = gate_module.weight + layout = self._weight_layout_in_out(gate_module) + return self._normalize_mlp_weight(weight, layout, gate_module, pattern="in") + + @property + def W_out(self) -> torch.Tensor: + """MLP output weight in TL orientation [d_mlp, d_model].""" + out_module = getattr(self, "out", None) + if out_module is None: + raise AttributeError("No 'out' submodule on this MLP bridge") + weight = out_module.weight + layout = self._weight_layout_in_out(out_module) + return self._normalize_mlp_weight(weight, layout, out_module, pattern="out") 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 d2f2096f07..565d787ac0 100644 --- a/transformer_lens/model_bridge/generalized_components/position_embeddings_attention.py +++ b/transformer_lens/model_bridge/generalized_components/position_embeddings_attention.py @@ -22,6 +22,7 @@ from transformer_lens.model_bridge.generalized_components.position_embedding_hooks_mixin import ( PositionEmbeddingHooksMixin, ) +from transformer_lens.utilities.hf_utils import get_rotary_pct_from_config # Global registry mapping HF attention modules to their bridge instances # Uses WeakValueDictionary to avoid preventing garbage collection of bridges @@ -459,10 +460,17 @@ def forward(self, *args: Any, **kwargs: Any) -> Any: apply_rotary_pos_emb, ) - # Some models use partial rotary (e.g., GPT-OSS) where cos/sin cover only - # a portion of head_dim. Split Q/K, rotate the partial dims, recombine. rotary_dim = cos.shape[-1] - if rotary_dim < head_dim: + if rotary_dim * 2 == head_dim and get_rotary_pct_from_config(self.config) == 1.0: + # GPT-OSS convention: full rotation with un-duplicated half-width + # cos/sin. Duplicating the halves reduces it to llama's + # rotate-half formula over the full head_dim. + cos = torch.cat([cos, cos], dim=-1) + sin = torch.cat([sin, sin], dim=-1) + query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) + elif rotary_dim < head_dim: + # Partial rotary (e.g., GPT-NeoX/Phi) where cos/sin cover only a + # portion of head_dim. Split Q/K, rotate the partial dims, recombine. q_rot, q_pass = query_states[..., :rotary_dim], query_states[..., rotary_dim:] k_rot, k_pass = key_states[..., :rotary_dim], key_states[..., rotary_dim:] q_rot, k_rot = apply_rotary_pos_emb(q_rot, k_rot, cos, sin) @@ -535,9 +543,27 @@ def forward(self, *args: Any, **kwargs: Any) -> Any: attn_scores = self.hook_attn_scores(attn_scores) # --- Softmax (in float32 for numerical stability) --- - attn_weights = torch.nn.functional.softmax(attn_scores, dim=-1, dtype=torch.float32).to( - query_states.dtype - ) + # GPT-OSS attention sinks: a learned per-head logit joins the softmax as + # an extra key column and is dropped afterward, so every real position's + # weight is scaled down by the sink's share. Appended after + # hook_attn_scores so hooks keep the [batch, head, q_pos, kv_pos] shape + # and score patches still flow into the softmax. + sinks = getattr(hf_attn, "sinks", None) + if sinks is not None: + sink_col = ( + sinks.reshape(1, -1, 1, 1) + .expand(attn_scores.shape[0], -1, attn_scores.shape[-2], -1) + .to(attn_scores.dtype) + ) + combined = torch.cat([attn_scores, sink_col], dim=-1) + combined = combined - combined.max(dim=-1, keepdim=True).values + attn_weights = torch.nn.functional.softmax(combined, dim=-1, dtype=torch.float32).to( + query_states.dtype + )[..., :-1] + else: + attn_weights = torch.nn.functional.softmax(attn_scores, dim=-1, dtype=torch.float32).to( + query_states.dtype + ) # --- Dropout --- dropout_rate = getattr(hf_attn, "attention_dropout", 0.0) diff --git a/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py b/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py new file mode 100644 index 0000000000..8e9dfbbad3 --- /dev/null +++ b/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py @@ -0,0 +1,37 @@ +"""CLS-token classifier head bridge component. + +HF's ViTForImageClassification.forward() / DeiTForImageClassification.forward() +already pool the CLS token before calling self.classifier: + + sequence_output = outputs.last_hidden_state + pooled_output = sequence_output[:, 0, :] + logits = self.classifier(pooled_output) + +So this component only ever receives an already-pooled (batch, hidden) tensor. +No slicing needed here — this is a thin, hook-named pass-through. + +Deliberately NOT covering DeiTForImageClassificationWithTeacher (dual +cls_classifier + distillation_classifier head) — see vit.py's docstring. +""" + +from typing import Any + +from torch import Tensor + +from transformer_lens.model_bridge.generalized_components.base import ( + GeneralizedComponent, +) + + +class VisionClassifierHeadBridge(GeneralizedComponent): + """Wraps the classifier nn.Linear that HF calls with an already-pooled CLS token.""" + + def forward(self, pooled_output: Tensor, **kwargs: Any) -> Tensor: + original_component = self.original_component + if original_component is None: + raise RuntimeError( + f"Original component not set for {self.name}. Call set_original_component() first." + ) + pooled_output = self.hook_in(pooled_output) + logits = original_component(pooled_output) + return self.hook_out(logits) diff --git a/transformer_lens/model_bridge/generalized_components/vision_embeddings.py b/transformer_lens/model_bridge/generalized_components/vision_embeddings.py new file mode 100644 index 0000000000..2bcbc50f9e --- /dev/null +++ b/transformer_lens/model_bridge/generalized_components/vision_embeddings.py @@ -0,0 +1,64 @@ +"""Vision (ViT-style) embeddings bridge component. + +Wraps a HF `ViTEmbeddings` / `DeiTEmbeddings` module directly and forwards through +it unmodified. The patch-conv projection, CLS-token (and, for DeiT, distillation- +token) concatenation, and position-embedding addition are HF's math — we don't +reimplement any of it, we just point at the real module and hook its input/output. + +Why one class covers both ViT and DeiT: `ViTEmbeddings.forward` and +`DeiTEmbeddings.forward` have an *identical* signature + (pixel_values, bool_masked_pos=None, interpolate_pos_encoding=False) +The only structural difference between them (DeiT prepends a distillation token +in addition to CLS, and sizes `position_embeddings` for +2 slots instead of +1) +lives entirely inside whichever module `set_original_component` resolves to — it's +invisible to this wrapper, so no DeiT-specific branching is needed here. + +Verified against the real generalized_components/base.py (not extrapolated): +`GeneralizedComponent.forward(*args, **kwargs)` already does exactly what we need +— it hooks the input via `self.hook_in`, casts it to match the wrapped module's +own parameter dtype (equivalent to HF's own `pixel_values.to(expected_dtype)` +"kept for BC" cast in ViTModel.forward — both land on the same dtype in the +overwhelming common case of a non-mixed-precision checkpoint), calls +`self.original_component(*args, **kwargs)`, and hooks the output via +`self.hook_out`. So this subclass only needs to guarantee `pixel_values` reaches +`super().forward()` *positionally* — the base class's own kwarg-name sniffing +list (`input`, `hidden_states`, `input_ids`, `query_input`, `x`, `inputs_embeds`) +doesn't include `pixel_values`, so if this component were ever called +all-keyword (`self.embeddings(pixel_values=x, ...)`, unlike the two HF call +sites we've confirmed) the base class's hook_in would silently never fire. +Re-emitting positionally here closes that gap regardless of how *we* were +called. +""" + +from typing import Any, Optional + +import torch +from torch import Tensor + +from transformer_lens.model_bridge.generalized_components.base import ( + GeneralizedComponent, +) + + +class VisionEmbeddingsBridge(GeneralizedComponent): + """Bridge for ViTEmbeddings / DeiTEmbeddings. + + Point `name=` at the embeddings module directly, e.g.: + VisionEmbeddingsBridge(name="embeddings") # bare ViTModel/DeiTModel + VisionEmbeddingsBridge(name="vit.embeddings") # ViTForImageClassification + VisionEmbeddingsBridge(name="deit.embeddings") # DeiTForImageClassification + """ + + def forward( + self, + pixel_values: Tensor, + bool_masked_pos: Optional[torch.BoolTensor] = None, + interpolate_pos_encoding: Optional[bool] = None, + **kwargs: Any, + ) -> Tensor: + return super().forward( + pixel_values, + bool_masked_pos=bool_masked_pos, + interpolate_pos_encoding=interpolate_pos_encoding, + **kwargs, + ) diff --git a/transformer_lens/model_bridge/get_params_util.py b/transformer_lens/model_bridge/get_params_util.py index acca83a4e6..adf9de0346 100644 --- a/transformer_lens/model_bridge/get_params_util.py +++ b/transformer_lens/model_bridge/get_params_util.py @@ -125,8 +125,9 @@ def _get_device_dtype(): mlp_in = getattr(block.mlp, "in", None) or getattr(block.mlp, "input", None) if mlp_in is None: raise AttributeError("MLP has no 'in' or 'input' attribute") - params_dict[f"blocks.{layer_idx}.mlp.W_in"] = mlp_in.weight - params_dict[f"blocks.{layer_idx}.mlp.W_out"] = block.mlp.out.weight + # 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 @@ -145,7 +146,9 @@ def _get_device_dtype(): bridge.cfg.d_model, device=device, dtype=dtype ) if hasattr(block.mlp, "gate") and hasattr(block.mlp.gate, "weight"): - params_dict[f"blocks.{layer_idx}.mlp.W_gate"] = block.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 hasattr(block.mlp.gate, "bias") and block.mlp.gate.bias is not None: params_dict[f"blocks.{layer_idx}.mlp.b_gate"] = block.mlp.gate.bias except AttributeError: diff --git a/transformer_lens/model_bridge/sources/transformers.py b/transformer_lens/model_bridge/sources/transformers.py index 34ef4226be..0200aabbbf 100644 --- a/transformer_lens/model_bridge/sources/transformers.py +++ b/transformer_lens/model_bridge/sources/transformers.py @@ -157,6 +157,19 @@ def map_default_transformer_lens_config(hf_config): tl_config.n_ctx = source_config.max_length elif hasattr(source_config, "seq_length"): tl_config.n_ctx = source_config.seq_length + elif hasattr(source_config, "image_size") and hasattr(source_config, "patch_size"): + # Vision Transformers calculate sequence length dynamically: + # (image_size / patch_size)^2 + 1 (for the CLS token) + image_size = source_config.image_size + patch_size = source_config.patch_size + + # HF configs allow these to be integers or tuples/lists + img_h = image_size[0] if isinstance(image_size, (list, tuple)) else image_size + img_w = image_size[1] if isinstance(image_size, (list, tuple)) else image_size + patch_h = patch_size[0] if isinstance(patch_size, (list, tuple)) else patch_size + patch_w = patch_size[1] if isinstance(patch_size, (list, tuple)) else patch_size + + tl_config.n_ctx = (img_h // patch_h) * (img_w // patch_w) + 1 elif hasattr(source_config, "max_sequence_length"): tl_config.n_ctx = source_config.max_sequence_length else: @@ -362,11 +375,14 @@ def get_hf_model_class_for_architecture(architecture: str): """ from transformer_lens.utilities.architectures import ( AUDIO_ARCHITECTURES, + AUDIO_CLASSIFICATION_ARCHITECTURES, AUDIO_TEXT_ARCHITECTURES, BASE_AUTOMODEL_ARCHITECTURES, MASKED_LM_ARCHITECTURES, MULTIMODAL_ARCHITECTURES, SEQ2SEQ_ARCHITECTURES, + VISION_ARCHITECTURES, + VISION_CLASSIFICATION_ARCHITECTURES, ) if architecture in SEQ2SEQ_ARCHITECTURES or architecture in AUDIO_TEXT_ARCHITECTURES: @@ -381,6 +397,10 @@ def get_hf_model_class_for_architecture(architecture: str): from transformers import AutoModel return AutoModel + elif architecture in AUDIO_CLASSIFICATION_ARCHITECTURES: + from transformers import AutoModelForAudioClassification + + return AutoModelForAudioClassification elif architecture in AUDIO_ARCHITECTURES: if "ForCTC" in architecture: from transformers import AutoModelForCTC @@ -388,6 +408,14 @@ def get_hf_model_class_for_architecture(architecture: str): return AutoModelForCTC from transformers import AutoModel + return AutoModel + elif architecture in VISION_ARCHITECTURES: + if architecture in VISION_CLASSIFICATION_ARCHITECTURES: + from transformers import AutoModelForImageClassification + + return AutoModelForImageClassification + from transformers import AutoModel + return AutoModel else: return AutoModelForCausalLM @@ -813,7 +841,8 @@ def boot( use_fast = getattr(adapter.cfg, "use_fast", True) # Audio models use feature extractors, not text tokenizers _is_audio = getattr(adapter.cfg, "is_audio_model", False) - if _is_audio and tokenizer is None: + _is_visual = getattr(adapter.cfg, "is_visual_model", False) + if (_is_audio or _is_visual) and tokenizer is None: tokenizer = None # Skip tokenizer loading for audio models elif tokenizer is not None: tokenizer = setup_tokenizer(tokenizer, default_padding_side=default_padding_side) @@ -932,6 +961,21 @@ def boot( except Exception: pass # Feature extractor not available; user can set bridge.processor manually + # Load image processor for vision encoder models (needed for image preprocessing) + if getattr(adapter.cfg, "is_visual_model", False): + try: + from transformers import AutoImageProcessor + + huggingface_token = os.environ.get("HF_TOKEN", "") + token_arg = huggingface_token if len(huggingface_token) > 0 else None + bridge.processor = AutoImageProcessor.from_pretrained( + model_name, + token=token_arg, + trust_remote_code=trust_remote_code, + ) + except Exception: + pass # Image processor not available; user can set bridge.processor manually + return bridge diff --git a/transformer_lens/model_bridge/supported_architectures/__init__.py b/transformer_lens/model_bridge/supported_architectures/__init__.py index e5d60f9587..71148a6ed5 100644 --- a/transformer_lens/model_bridge/supported_architectures/__init__.py +++ b/transformer_lens/model_bridge/supported_architectures/__init__.py @@ -6,6 +6,7 @@ from transformer_lens.model_bridge.supported_architectures.afmoe import AfmoeArchitectureAdapter from transformer_lens.model_bridge.supported_architectures.apertus import ApertusArchitectureAdapter from transformer_lens.model_bridge.supported_architectures.arcee import ArceeArchitectureAdapter +from transformer_lens.model_bridge.supported_architectures.ast import ASTArchitectureAdapter from transformer_lens.model_bridge.supported_architectures.audio_flamingo3 import ( AudioFlamingo3ArchitectureAdapter, ) @@ -252,6 +253,9 @@ from transformer_lens.model_bridge.supported_architectures.switch_transformers import ( SwitchTransformersArchitectureAdapter, ) +from transformer_lens.model_bridge.supported_architectures.vit import ( + ViTArchitectureAdapter, +) from transformer_lens.model_bridge.supported_architectures.t5 import T5ArchitectureAdapter from transformer_lens.model_bridge.supported_architectures.t5gemma import T5GemmaArchitectureAdapter from transformer_lens.model_bridge.supported_architectures.t5gemma2 import ( @@ -268,6 +272,7 @@ "AfmoeArchitectureAdapter", "ApertusArchitectureAdapter", "ArceeArchitectureAdapter", + "ASTArchitectureAdapter", "AudioFlamingo3ArchitectureAdapter", "BD3LMArchitectureAdapter", "BaichuanArchitectureAdapter", @@ -396,6 +401,8 @@ "Starcoder2ArchitectureAdapter", "T5ArchitectureAdapter", "T5GemmaArchitectureAdapter", + "T5Gemma2ArchitectureAdapter", + "ViTArchitectureAdapter", "XGLMArchitectureAdapter", "Zamba2ArchitectureAdapter", "HrmTextArchitectureAdapter", diff --git a/transformer_lens/model_bridge/supported_architectures/ast.py b/transformer_lens/model_bridge/supported_architectures/ast.py new file mode 100644 index 0000000000..78bde24b9d --- /dev/null +++ b/transformer_lens/model_bridge/supported_architectures/ast.py @@ -0,0 +1,141 @@ +"""AST (Audio Spectrogram Transformer) architecture adapter. + +Supports ASTForAudioClassification +""" + +from typing import Any + +from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion +from transformer_lens.conversion_utils.param_processing_conversion import ( + ParamProcessingConversion, +) +from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter +from transformer_lens.model_bridge.generalized_components import ( + AttentionBridge, + BlockBridge, + LinearBridge, + MLPBridge, + NormalizationBridge, + UnembeddingBridge, +) +from transformer_lens.model_bridge.generalized_components.base import ( + GeneralizedComponent, +) + + +class ASTArchitectureAdapter(ArchitectureAdapter): + """Architecture adapter for AST (Audio Spectrogram Transformer) audio classifiers. + + Input is a [batch, time=1024, n_mels=128] spectrogram; positions 0/1 are the + CLS and distillation tokens. HF pools (cls+dist)/2 after ln_final and applies + an extra LayerNorm inside the classifier head, see the unembed mapping note. + """ + + supports_generation: bool = False + + def __init__(self, cfg: Any) -> None: + super().__init__(cfg) + + # essential flag for audio models in V3 + self.cfg.is_audio_model = True + self.cfg.normalization_type = "LN" + + n_heads = self.cfg.n_heads + + # Q/K/V/O rearrangement: splits hidden dims into (heads, head_dim) + self.weight_processing_conversions = { + "blocks.{i}.attn.q.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "(h d_head) d_model -> h d_model d_head", h=n_heads + ), + ), + "blocks.{i}.attn.k.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "(h d_head) d_model -> h d_model d_head", h=n_heads + ), + ), + "blocks.{i}.attn.v.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "(h d_head) d_model -> h d_model d_head", h=n_heads + ), + ), + "blocks.{i}.attn.q.bias": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion("(h d_head) -> h d_head", h=n_heads), + ), + "blocks.{i}.attn.k.bias": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion("(h d_head) -> h d_head", h=n_heads), + ), + "blocks.{i}.attn.v.bias": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion("(h d_head) -> h d_head", h=n_heads), + ), + "blocks.{i}.attn.o.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "d_model (h d_head) -> h d_head d_model", h=n_heads + ), + ), + } + + # default mapping for bare ASTModel (prefix="") + self.component_mapping = self._build_component_mapping(prefix="") + + def _build_component_mapping(self, prefix: str) -> dict: + """Build component mapping. prefix="" for ASTModel, "audio_spectrogram_transformer." for classification.""" + p = prefix + + return { + "embed": GeneralizedComponent(name=f"{p}embeddings"), + "ln_final": NormalizationBridge(name=f"{p}layernorm", config=self.cfg), + "blocks": BlockBridge( + name=f"{p}layers", + submodules={ + "ln1": NormalizationBridge(name="layernorm_before", config=self.cfg), + "ln2": NormalizationBridge(name="layernorm_after", config=self.cfg), + "attn": AttentionBridge( + name="attention", + config=self.cfg, + submodules={ + "q": LinearBridge(name="q_proj"), + "k": LinearBridge(name="k_proj"), + "v": LinearBridge(name="v_proj"), + "o": LinearBridge(name="o_proj"), + }, + ), + "mlp": MLPBridge( + name="mlp", + config=self.cfg, + submodules={ + "in": LinearBridge(name="fc1"), + "out": LinearBridge(name="fc2"), + }, + ), + }, + ), + } + + def prepare_model(self, hf_model: Any) -> None: + """Detect classification head, rebind prefixes, guard unembed, and set n_ctx.""" + # 1. handle classification model vs bare encoder + if hasattr(hf_model, "audio_spectrogram_transformer"): + self.component_mapping = self._build_component_mapping( + prefix="audio_spectrogram_transformer." + ) + base_model = hf_model.audio_spectrogram_transformer + + # guard unembed: only map if num_labels > 0 and dense head is real + num_labels = getattr(hf_model.config, "num_labels", 0) + if ( + num_labels > 0 + and hasattr(hf_model, "classifier") + and hasattr(hf_model.classifier, "dense") + ): + self.component_mapping["unembed"] = UnembeddingBridge(name="classifier.dense") + self.cfg.d_vocab = num_labels + self.cfg.d_vocab_out = num_labels + else: + base_model = hf_model + + # 2. dynamically grab n_ctx from whatever base model we resolved + if hasattr(base_model, "embeddings") and hasattr( + base_model.embeddings, "position_embeddings" + ): + self.cfg.n_ctx = base_model.embeddings.position_embeddings.shape[1] diff --git a/transformer_lens/model_bridge/supported_architectures/starcoder2.py b/transformer_lens/model_bridge/supported_architectures/starcoder2.py index fa4acf077e..87600d8a30 100644 --- a/transformer_lens/model_bridge/supported_architectures/starcoder2.py +++ b/transformer_lens/model_bridge/supported_architectures/starcoder2.py @@ -3,6 +3,9 @@ BigCode's StarCoder2 (``Starcoder2ForCausalLM``): pre-norm decoder with plain LayerNorm (not RMS), separate biased q/k/v/o projections, GQA, RoPE, and a non-gated ``c_fc``/``c_proj`` MLP. + +Not a drop-in for its GPTBigCode predecessor: that one fuses q/k/v into a +single ``c_attn`` and uses learned positions, so it needs a different bridge. """ from typing import Any diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py new file mode 100644 index 0000000000..1aa5394b0a --- /dev/null +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -0,0 +1,206 @@ +"""ViT / DeiT architecture adapter. + +Supports HF `ViTModel`, `ViTForImageClassification`, `DeiTModel`, +`DeiTForImageClassification` (single CLS-token classifier head). Encoder blocks +are structurally near-identical between ViT and DeiT — same field names +(layernorm_before/after, attention.{q,k,v,o}_proj, mlp.{fc1,fc2}) — differing +only in the embeddings module used (DeiT's carries an extra distillation token, +which is invisible to this adapter — see vision_embeddings.py). + +NOT covered: `DeiTForImageClassificationWithTeacher` (dual cls+distillation head, +averaged). See vision_classifier_head.py's docstring for why, and prepare_model() +below raises loudly if you load one anyway rather than silently producing wrong +logits. + +ViT/DeiT blocks are pre-LN (LayerNorm applied *before* attention/MLP, residual +added after — same shape as Llama/GPT2), unlike BERT's post-LN. That's why +`supports_fold_ln = True` here where BertArchitectureAdapter sets it False. + +NOTE (transformers >= the ViT/DeiT flattening refactor): as of this version of +`modeling_vit.py`, HF removed the separate `ViTEncoder` wrapper — the blocks +now live directly at `.layers` on the model, not `.encoder.layer`. +Attention was flattened too: `ViTAttention` now owns `q_proj`/`k_proj`/`v_proj`/ +`o_proj` directly (no more nested `attention.attention.{query,key,value}` + +`output.dense`). And `ViTLayer` already exposes a flat `.mlp` submodule +(`ViTMLP` with `.fc1`/`.fc2`) instead of the old `intermediate`/`output` split. +Because of this, the old `ViTMLPWrapper` shim, the block-forward tuple-unwrapping +monkey-patch (`ViTLayer.forward` returns a plain tensor now, not a tuple), and +the `hf_model.encoder_layer = hf_model.encoder.layer` aliasing hack are all +gone — the component mapping below points straight at the real attributes. +""" + +from typing import Any, Dict + +from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion +from transformer_lens.conversion_utils.param_processing_conversion import ( + ParamProcessingConversion, +) +from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter +from transformer_lens.model_bridge.generalized_components import ( + AttentionBridge, + BlockBridge, + LinearBridge, + MLPBridge, + NormalizationBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( + VisionClassifierHeadBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_embeddings import ( + VisionEmbeddingsBridge, +) + + +class ViTArchitectureAdapter(ArchitectureAdapter): + """Architecture adapter for ViT and (non-distilled-head) DeiT vision models.""" + + supports_generation: bool = False + + # Vision models have no tokenizer, so of the text phases only Phase 1 (HF + # parity on pixel input) applies — Phases 2/3 need a HookedTransformer + # counterpart and Phase 4 needs text generation. Phase 9 (vision hook/cache + # tests) is gated by is_visual_model, not this list; _full_and_core_phases() + # routes "vision" architectures to {1, 9}. + applicable_phases: list[int] = [1] + + def __init__(self, cfg: Any) -> None: + super().__init__(cfg) + + self.cfg.is_visual_model = True + self.cfg.normalization_type = "LN" + self.cfg.positional_embedding_type = "standard" + self.cfg.final_rms = False + self.cfg.gated_mlp = False + self.cfg.attn_only = False + self.supports_fold_ln = True + + n_heads = self.cfg.n_heads + + self.weight_processing_conversions = { + "blocks.{i}.attn.q.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "(h d_head) d_model -> h d_model d_head", h=n_heads + ), + ), + "blocks.{i}.attn.k.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "(h d_head) d_model -> h d_model d_head", h=n_heads + ), + ), + "blocks.{i}.attn.v.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "(h d_head) d_model -> h d_model d_head", h=n_heads + ), + ), + "blocks.{i}.attn.q.bias": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion("(h d_head) -> h d_head", h=n_heads), + ), + "blocks.{i}.attn.k.bias": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion("(h d_head) -> h d_head", h=n_heads), + ), + "blocks.{i}.attn.v.bias": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion("(h d_head) -> h d_head", h=n_heads), + ), + "blocks.{i}.attn.o.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "d_model (h d_head) -> h d_head d_model", h=n_heads + ), + ), + } + + self.component_mapping = self._build_component_mapping(prefix="", with_classifier=False) + + def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[str, Any]: + p = prefix + mapping: Dict[str, Any] = { + "embed": VisionEmbeddingsBridge(name=f"{p}embeddings"), + "blocks": BlockBridge( + # HF's ViTModel/DeiTModel no longer wrap blocks in a `.encoder` + # module — they sit directly on `.layers`. + name=f"{p}layers", + hook_alias_overrides={ + "hook_mlp_out": "mlp.out.hook_out", + "hook_mlp_in": "mlp.in.hook_in", + }, + submodules={ + "ln1": NormalizationBridge( + name="layernorm_before", + config=self.cfg, + use_native_layernorm_autograd=True, + ), + "ln2": NormalizationBridge( + name="layernorm_after", + config=self.cfg, + use_native_layernorm_autograd=True, + ), + "attn": AttentionBridge( + name="attention", + config=self.cfg, + submodules={ + # Submodule paths here are resolved relative to the + # already-resolved "attn" bridge module itself + # (block.attention). ViTAttention/DeiTAttention now + # owns q_proj/k_proj/v_proj/o_proj directly + # (flattened, no nested self-attention + separate + # output.dense module), so no "attention." prefix. + "q": LinearBridge(name="q_proj"), + "k": LinearBridge(name="k_proj"), + "v": LinearBridge(name="v_proj"), + "o": LinearBridge(name="o_proj"), + }, + ), + "mlp": MLPBridge( + name="mlp", + config=self.cfg, + submodules={ + # ViTLayer.mlp is a real ViTMLP module now + # (fc1/fc2) — no more intermediate/output split, + # so no wrapper shim is needed. + "in": LinearBridge(name="fc1"), + "out": LinearBridge(name="fc2"), + }, + ), + }, + ), + "ln_final": NormalizationBridge( + name=f"{p}layernorm", + config=self.cfg, + use_native_layernorm_autograd=True, + ), + } + if with_classifier: + mapping["unembed"] = VisionClassifierHeadBridge(name="classifier") + return mapping + + def prepare_model(self, hf_model: Any) -> None: + """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare + *Model, and add/omit the classifier head + prefix accordingly. + + No structural patching of the HF model is needed any more: current + transformers ViT/DeiT blocks already expose a flat `.mlp` (fc1/fc2) and + return plain tensors from `forward`, and the blocks live directly on + `.layers` rather than behind a now-removed `.encoder` wrapper. + This method only has to figure out the right prefix/classifier and + build the component mapping to point at those real attributes. + """ + if hasattr(hf_model, "cls_classifier") and hasattr(hf_model, "distillation_classifier"): + raise NotImplementedError( + "DeiTForImageClassificationWithTeacher (dual cls_classifier + " + "distillation_classifier head, averaged) isn't supported by this " + "adapter yet — see vision_classifier_head.py's docstring for why, " + "and what to check before adding it." + ) + + # Check for the wrapper module created by HF's ForImageClassification classes. + # Bare ViTModel / DeiTModel have components at the root, so prefix must be "". + if hasattr(hf_model, "deit") and not hasattr(hf_model, "vit"): + prefix = "deit." + elif hasattr(hf_model, "vit"): + prefix = "vit." + else: + prefix = "" + + with_classifier = hasattr(hf_model, "classifier") + self.component_mapping = self._build_component_mapping( + prefix=prefix, with_classifier=with_classifier + ) diff --git a/transformer_lens/pretrained/weight_conversions/olmo3.py b/transformer_lens/pretrained/weight_conversions/olmo3.py index 1b89a1d2d7..b56be49579 100644 --- a/transformer_lens/pretrained/weight_conversions/olmo3.py +++ b/transformer_lens/pretrained/weight_conversions/olmo3.py @@ -21,7 +21,10 @@ def convert_olmo3_weights(olmo3, cfg: HookedTransformerConfig): state_dict = {} - using_gqa = cfg.n_key_value_heads is not None and cfg.n_key_value_heads < cfg.n_heads + # Must match TransformerBlock's attention choice, which selects + # GroupedQueryAttention (underscore-prefixed params) whenever + # n_key_value_heads is set — including n_key_value_heads == n_heads. + using_gqa = cfg.n_key_value_heads is not None gqa_uscore = "_" if using_gqa else "" n_kv_heads = cast(int, cfg.n_key_value_heads if using_gqa else cfg.n_heads) diff --git a/transformer_lens/pretrained/weight_conversions/openai.py b/transformer_lens/pretrained/weight_conversions/openai.py index dfe0b7d3f4..e4d3ede46e 100644 --- a/transformer_lens/pretrained/weight_conversions/openai.py +++ b/transformer_lens/pretrained/weight_conversions/openai.py @@ -62,6 +62,8 @@ def convert_gpt_oss_weights(gpt_oss, cfg: HookedTransformerConfig): cfg.n_key_value_heads, cfg.d_head, dtype=cfg.dtype, device=cfg.device ) + state_dict[f"blocks.{l}.attn.sinks"] = layer.self_attn.sinks + W_O = einops.rearrange(layer.self_attn.o_proj.weight, "m (n h) -> n h m", n=cfg.n_heads) state_dict[f"blocks.{l}.attn.W_O"] = W_O @@ -86,6 +88,19 @@ def convert_gpt_oss_weights(gpt_oss, cfg: HookedTransformerConfig): down_proj = experts.down_proj # (num_experts, expert_dim, hidden_size) down_bias = experts.down_proj_bias # (num_experts, hidden_size) + if not isinstance(gate_up_proj, torch.Tensor): + # Packed MXFP4 checkpoints wrap expert weights in a triton-kernels + # object (confusingly also named "Tensor") that cannot be sliced. + raise NotImplementedError( + f"blocks.{l}.mlp.experts.gate_up_proj is a " + f"{type(gate_up_proj).__module__}.{type(gate_up_proj).__name__}, not a " + "torch.Tensor — this gpt-oss checkpoint has packed MXFP4 expert weights. " + "Load it dequantized so the converter sees plain tensors: pass " + "hf_model=AutoModelForCausalLM.from_pretrained(name, " + "quantization_config=Mxfp4Config(dequantize=True)), or load by model " + "name and TransformerLens dequantizes automatically." + ) + for e in range(cfg.num_experts): # Split interleaved gate_up_proj into separate gate and up (in) projections # Even columns → gate path, Odd columns → up/in path diff --git a/transformer_lens/tools/analysis/jacobian_lens.py b/transformer_lens/tools/analysis/jacobian_lens.py index f93f21d799..e333ffd1db 100644 --- a/transformer_lens/tools/analysis/jacobian_lens.py +++ b/transformer_lens/tools/analysis/jacobian_lens.py @@ -121,6 +121,29 @@ def _resolve_registry_entry(name_or_path: str) -> Optional[Tuple[str, str]]: _SWAP_WARN_COSINE = 0.99 _SWAP_ERROR_COSINE = 0.999 +# Keys written by fit() that must not appear in converted-lens metadata so that +# merge() can refuse to mix TL-fitted lenses with externally converted ones. +# Note: "target_layer" is intentionally NOT listed here — it must survive +# conversion so that validate_model() can detect and refuse checkpoints that +# were fitted against a non-final target layer. +_FIT_RESERVED_KEYS: frozenset = frozenset( + { + "transformer_lens_fit", + "transformer_lens_version", + "model_system", + "processing", + "hook_convention", + "fit_dtype", + "dim_batch", + "max_seq_len", + "skip_first_positions", + } +) + +# Top-level payload keys that some checkpoint writers store as flat provenance +# (rather than nested under a "metadata" key). +_CHECKPOINT_FLAT_PROVENANCE: frozenset = frozenset({"model_name", "model_revision", "corpus"}) + @dataclass class JacobianLensReadout: @@ -266,28 +289,136 @@ def save(self, path: str, *, dtype: torch.dtype = torch.float16) -> None: @classmethod def load(cls, path: str) -> "JacobianLens": - """Load a lens artifact saved by this class or the reference package. + """Load a lens artifact or fit checkpoint saved in a supported schema. + + Two file schemas are accepted: + + **Artifact** (the reference format, written by :meth:`save` or the + Anthropic reference package): must contain a ``J`` key mapping layer + indices to transport matrices, plus ``n_prompts``, ``d_model``, and an + optional ``metadata`` dict. + + **Fit checkpoint** (running-sum format, written by the reference + implementation's ``write_checkpoint()`` during fitting): must contain + a ``jacobian_sum`` key mapping layer indices to *running-sum* matrices + (i.e. the sum over prompts, not yet divided by the prompt count), plus + ``n_done``. ``d_model`` is inferred from the first matrix's shape; + no explicit ``d_model`` key is required or expected. The per-layer + means are reconstructed on load. A + ``converted_from: "jacobian_lens_checkpoint"`` key is added to + metadata so :meth:`merge` refuses to silently combine checkpoints with + natively TL-fitted lenses. Fit-reserved provenance keys + (``transformer_lens_fit``, etc.) are stripped; scalar fields that can + be serialised under ``weights_only=True`` are preserved. + Tensor-valued metadata fields that would fail :func:`_validate_metadata` + are recorded by name in a ``dropped_fields`` list. + + Fit checkpoint schema (reference ``write_checkpoint()`` format) + --------------------------------------------------------------- + The reference implementation writes exactly six top-level keys; all + other keys in the payload are ignored:: + + { + "jacobian_sum": {: , ...}, + "n_done": , # prompts accumulated into jacobian_sum + "next_idx": , # next prompt index (informational) + "source_layers": [, ...], # documented layer indices (informational) + "target_layer": , # target layer — harvested into metadata + "skip_first": , # leading positions skipped (informational) + # optional flat provenance accepted from alternative checkpoint writers: + "model_name": , + "model_revision": , + "corpus": , + # optional nested provenance accepted from alternative writers: + "metadata": {: , ...}, + } Args: - path: Path to the ``.pt`` artifact. + path: Path to the ``.pt`` file. Raises: - ValueError: If the file lacks the ``J`` key (e.g. a fit checkpoint - rather than a saved lens). + ValueError: If the file lacks both a ``J`` key (artifact) and a + ``jacobian_sum`` key (checkpoint), or if a checkpoint records a + non-positive ``n_prompts``. """ payload = torch.load(path, map_location="cpu", weights_only=True) - if "J" not in payload: - raise ValueError( - f"{path} does not look like a Jacobian lens artifact (no 'J' key). " - "Fit checkpoints and other formats are not supported." + if "J" in payload: + return cls( + {int(layer): matrix for layer, matrix in payload["J"].items()}, + n_prompts=int(payload.get("n_prompts", 0)), + d_model=int(payload["d_model"]), + metadata=payload.get("metadata"), ) - return cls( - {int(layer): matrix for layer, matrix in payload["J"].items()}, - n_prompts=int(payload.get("n_prompts", 0)), - d_model=int(payload["d_model"]), - metadata=payload.get("metadata"), + if "jacobian_sum" in payload: + return cls._from_checkpoint_payload(path, payload) + raise ValueError( + f"{path} does not look like a Jacobian lens artifact or fit checkpoint. " + "Expected a 'J' key (artifact) or 'jacobian_sum' key (fit checkpoint). " + "See JacobianLens.load() for the supported file schemas." ) + @classmethod + def _from_checkpoint_payload(cls, path: str, payload: Dict[str, Any]) -> "JacobianLens": + """Reconstruct a JacobianLens from a fit-checkpoint payload. + + Divides the running Jacobian sums by ``n_prompts``, strips fit-reserved + provenance keys, harvests safe scalar metadata, records dropped tensor + fields, and marks the result as converted so :meth:`merge` refuses to + mix it with natively TL-fitted lenses. + """ + n_prompts = int(payload.get("n_done", payload.get("n_prompts", 0))) + if n_prompts <= 0: + raise ValueError( + f"{path} is a fit checkpoint with n_prompts={n_prompts}; " + "a positive prompt count is required to reconstruct the Jacobian mean." + ) + if not payload["jacobian_sum"]: + raise ValueError( + f"{path} is a fit checkpoint with an empty jacobian_sum; " + "at least one layer matrix is required to reconstruct d_model." + ) + first_matrix = next(iter(payload["jacobian_sum"].values())) + d_model = first_matrix.shape[0] + jacobians = { + int(layer): matrix.float() / n_prompts + for layer, matrix in payload["jacobian_sum"].items() + } + + # Collect raw provenance: first from nested "metadata", then supplement + # with flat top-level keys that some checkpoint writers place directly + # in the payload (model_name, model_revision, corpus). + raw_meta: Dict[str, Any] = dict(payload.get("metadata") or {}) + for key in _CHECKPOINT_FLAT_PROVENANCE: + if key in payload and key not in raw_meta: + raw_meta[key] = payload[key] + # target_layer lives at the top level in the reference checkpoint format; + # harvest it into metadata so validate_model() can check the fitting target. + if "target_layer" in payload and "target_layer" not in raw_meta: + raw_meta["target_layer"] = payload["target_layer"] + + # Build clean metadata: drop fit-reserved keys, record tensor-valued + # fields that _validate_metadata would reject (they cannot survive a + # weights_only=True reload), and keep everything else that validates. + dropped_fields: List[str] = [] + clean_meta: Dict[str, Any] = {} + for key, value in raw_meta.items(): + if key in _FIT_RESERVED_KEYS: + continue + if isinstance(value, torch.Tensor): + dropped_fields.append(f"{key}: shape={tuple(value.shape)} dtype={value.dtype}") + continue + try: + _validate_metadata({key: value}) + clean_meta[key] = value + except ValueError: + dropped_fields.append(key) + + clean_meta["converted_from"] = "jacobian_lens_checkpoint" + if dropped_fields: + clean_meta["dropped_fields"] = dropped_fields + + return cls(jacobians, n_prompts=n_prompts, d_model=d_model, metadata=clean_meta) + @classmethod def from_pretrained( cls, diff --git a/transformer_lens/tools/model_registry/AGENTS.md b/transformer_lens/tools/model_registry/AGENTS.md index 02ba7cfda0..8198be7013 100644 --- a/transformer_lens/tools/model_registry/AGENTS.md +++ b/transformer_lens/tools/model_registry/AGENTS.md @@ -6,7 +6,7 @@ Read [the root AGENTS.md](../../../AGENTS.md) for project-wide rules. This file ## TL;DR -> **Use `verify_models`, not `main_benchmark`.** Only `verify_models` writes `data/supported_models.json`. `main_benchmark` runs the same math but defaults to NOT writing the registry (needs `--update-registry`, and even with it misses Phase 7/8 scores and the resume checkpoint). If you ran `main_benchmark`, the registry is stale. +> **Use `verify_models`, not `main_benchmark`.** Only `verify_models` writes `data/supported_models.json`. `main_benchmark` runs the same math but defaults to NOT writing the registry (needs `--update-registry`, and even with it misses Phase 7/8/9 scores and the resume checkpoint). If you ran `main_benchmark`, the registry is stale. - **`update_model_status()` in `registry_io.py` is the only mutator of status/phase/note on existing entries.** Never set by hand. - **Adding a new model-ID entry is allowed** (required before `verify_models --model ` can find it). See [Adding a new model entry](#adding-a-new-model-entry). @@ -41,7 +41,7 @@ Read [the root AGENTS.md](../../../AGENTS.md) for project-wide rules. This file | `--device ` | Override automatic device selection | | `--dtype ` | Override automatic dtype selection | | `--max-memory ` | Skip models whose parameter-count estimate exceeds this GB cap (default: tries every model that fits available device memory). Use this to avoid OOM on a small device — e.g. `--max-memory 16` on a 24 GB GPU leaves head-room for activations. | -| `--phases ` | Restrict to specific phases (default `1 2 3 4`; Phase 7/8 are auto-skipped for non-applicable architectures) | +| `--phases ` | Restrict to specific phases (default `1 2 3 4`; Phase 7/8/9 are auto-skipped for non-applicable architectures) | | `--resume` | Read `data/verification_checkpoint.json` and skip models already tested in the in-flight run | | `--reverify` | Re-test already-verified models (default skips status=1 entries) | | `--retry-failed` | Re-test status=3 (failed) entries | @@ -84,7 +84,8 @@ To verify a model not yet in `data/supported_models.json`, hand-add the entry fi "status": 0, "verified_date": null, "metadata": null, "note": null, "phase1_score": null, "phase2_score": null, "phase3_score": null, - "phase4_score": null, "phase7_score": null, "phase8_score": null + "phase4_score": null, "phase7_score": null, "phase8_score": null, + "phase9_score": null } ``` @@ -115,10 +116,13 @@ Never edit manually. | 3 | Weight processing (compatibility mode, fold/centre) | | 4 | Text-generation quality | | 7 | Multimodal (vision/text alignment) — only Llava / Gemma3-multimodal | -| 8 | Audio — only Hubert | +| 8 | Audio — Hubert (waveform) and AST (spectrogram) | +| 9 | Vision — ViT/DeiT pixel forward, hook/cache firing, representation stability, classification decode | SSM / recurrent families and the hybrids (Mamba-1/2, gated-delta-net, NemotronH, GraniteMoeHybrid, Jamba, Qwen3.5/Qwen3-Next) declare `applicable_phases = [1, 2, 3, 4]` — all four apply. P2/P3 run but skip their HookedTransformer-comparison sub-tests (SSMs have no HT), which is scored as a pass. +**Non-text modalities.** `classify_architecture` routes audio architectures to `{1, 8}` and vision architectures (ViT/DeiT) to `{1, 9}` — vision has no tokenizer for P4, and neither modality has a HookedTransformer counterpart for P2/P3. Phases 1/8/9 build their input with `build_modality_input()` ([`benchmarks/utils.py`](../../benchmarks/utils.py)), which shapes it from the HF config: `[batch, max_length, num_mel_bins]` for spectrogram encoders, `[batch, samples]` for waveform encoders, `[batch, channels, image_size, image_size]` for vision. Add a new non-text architecture there rather than hardcoding a shape at the call site. + ### Phase-score thresholds `verify_models` enforces hard pass/fail at the thresholds in `_MIN_PHASE_SCORES` ([`verify_models.py:508`](verify_models.py)). Below threshold OR a required-test failure → `STATUS_FAILED`. The contract: @@ -131,6 +135,7 @@ SSM / recurrent families and the hybrids (Mamba-1/2, gated-delta-net, NemotronH, | 4 | 50% | — | **Non-gating.** Below 50% adds `"low text quality"` 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?" diff --git a/transformer_lens/tools/model_registry/__init__.py b/transformer_lens/tools/model_registry/__init__.py index 07931038b6..d8d7212e3e 100644 --- a/transformer_lens/tools/model_registry/__init__.py +++ b/transformer_lens/tools/model_registry/__init__.py @@ -48,6 +48,7 @@ "AfmoeForCausalLM", "ApertusForCausalLM", "ArceeForCausalLM", + "ASTForAudioClassification", "BaiChuanForCausalLM", "BaichuanForCausalLM", "BambaForCausalLM", @@ -187,6 +188,10 @@ "T5GemmaForConditionalGeneration", "T5Gemma2ForConditionalGeneration", "XGLMForCausalLM", + "ViTModel", + "ViTForImageClassification", + "DeiTModel", + "DeiTForImageClassification", "Zamba2ForCausalLM", } @@ -196,6 +201,7 @@ "AfmoeForCausalLM": ["arcee-ai"], "ApertusForCausalLM": ["swiss-ai"], "ArceeForCausalLM": ["arcee-ai"], + "ASTForAudioClassification": ["MIT"], "BaiChuanForCausalLM": ["baichuan-inc"], "BaichuanForCausalLM": ["baichuan-inc"], "BambaForCausalLM": ["ibm-ai-platform"], @@ -335,6 +341,10 @@ "T5GemmaForConditionalGeneration": ["google"], "T5Gemma2ForConditionalGeneration": ["google"], "XGLMForCausalLM": ["facebook"], + "ViTModel": ["google"], + "ViTForImageClassification": ["google"], + "DeiTModel": ["facebook"], + "DeiTForImageClassification": ["facebook"], "Zamba2ForCausalLM": ["Zyphra"], } diff --git a/transformer_lens/tools/model_registry/data/supported_models.json b/transformer_lens/tools/model_registry/data/supported_models.json index 8de1040dac..c20df5eb9c 100644 --- a/transformer_lens/tools/model_registry/data/supported_models.json +++ b/transformer_lens/tools/model_registry/data/supported_models.json @@ -6,10 +6,10 @@ "min_downloads": 500, "scan_duration_seconds": 8.7 }, - "total_architectures": 140, - "total_models": 15664, + "total_architectures": 143, + "total_models": 15670, "total_provisional": 7, - "total_verified": 1197, + "total_verified": 1203, "models": [ { "architecture_id": "FalconH1ForCausalLM", @@ -87557,7 +87557,7 @@ "architecture_id": "HubertForCTC", "model_id": "facebook/hubert-large-ls960-ft", "status": 1, - "verified_date": "2026-03-19", + "verified_date": "2026-07-30", "metadata": null, "note": "Full verification completed", "phase1_score": 100.0, @@ -111770,6 +111770,20 @@ "phase7_score": null, "phase8_score": null }, + { + "architecture_id": "Gemma3ForConditionalGeneration", + "model_id": "lapa-llm/lapa-v0.1.3-instruct", + "status": 1, + "verified_date": "2026-07-30", + "metadata": null, + "note": "Full verification completed", + "phase1_score": 100.0, + "phase2_score": 100.0, + "phase3_score": 100.0, + "phase4_score": 100.0, + "phase7_score": 100.0, + "phase8_score": null + }, { "architecture_id": "LlamaForCausalLM", "model_id": "lapisrocks/Llama-3-8B-Instruct-TAR-Refusal", @@ -217917,6 +217931,79 @@ "phase4_score": 97.3, "phase7_score": null, "phase8_score": null + }, + { + "architecture_id": "ASTForAudioClassification", + "model_id": "MIT/ast-finetuned-audioset-10-10-0.4593", + "status": 1, + "verified_date": "2026-07-30", + "metadata": null, + "note": "Full verification completed", + "phase1_score": 100.0, + "phase2_score": null, + "phase3_score": null, + "phase4_score": null, + "phase7_score": null, + "phase8_score": 100.0 + }, + { + "architecture_id": "ASTForAudioClassification", + "model_id": "MIT/ast-finetuned-speech-commands-v2", + "status": 1, + "verified_date": "2026-07-30", + "metadata": null, + "note": "Full verification completed", + "phase1_score": 100.0, + "phase2_score": null, + "phase3_score": null, + "phase4_score": null, + "phase7_score": null, + "phase8_score": 100.0 + }, + { + "architecture_id": "ViTForImageClassification", + "model_id": "google/vit-base-patch16-224", + "status": 1, + "verified_date": "2026-07-31", + "metadata": null, + "note": "Full verification completed", + "phase1_score": 100.0, + "phase2_score": null, + "phase3_score": null, + "phase4_score": null, + "phase7_score": null, + "phase8_score": null, + "phase9_score": 100.0 + }, + { + "architecture_id": "ViTModel", + "model_id": "google/vit-base-patch16-224-in21k", + "status": 1, + "verified_date": "2026-07-31", + "metadata": null, + "note": "Full verification completed", + "phase1_score": 100.0, + "phase2_score": null, + "phase3_score": null, + "phase4_score": null, + "phase7_score": null, + "phase8_score": null, + "phase9_score": 100.0 + }, + { + "architecture_id": "ViTForImageClassification", + "model_id": "facebook/deit-base-patch16-224", + "status": 1, + "verified_date": "2026-07-31", + "metadata": null, + "note": "Full verification completed", + "phase1_score": 100.0, + "phase2_score": null, + "phase3_score": null, + "phase4_score": null, + "phase7_score": null, + "phase8_score": null, + "phase9_score": 100.0 } ] } diff --git a/transformer_lens/tools/model_registry/data/verification_history.json b/transformer_lens/tools/model_registry/data/verification_history.json index 34dc548cfc..a80a94f259 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-07-24T13:57:03.104108", + "last_updated": "2026-07-31T10:36:39.286088", "records": [ { "model_id": "Macropodus/macbert4mdcspell_v1", @@ -20900,6 +20900,166 @@ "notes": "Full verification completed with issues, low text quality", "invalidated": false, "invalidation_reason": null + }, + { + "model_id": "MIT/ast-finetuned-audioset-10-10-0.4593", + "architecture_id": "ASTForAudioClassification", + "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)", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "MIT/ast-finetuned-audioset-10-10-0.4593", + "architecture_id": "ASTForAudioClassification", + "verified_date": "2026-07-30", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "lapa-llm/lapa-v0.1.3-instruct", + "architecture_id": "Gemma3ForConditionalGeneration", + "verified_date": "2026-07-30", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "google/vit-base-patch16-224", + "architecture_id": "ViTForImageClassification", + "verified_date": "2026-07-30", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "google/vit-base-patch16-224-in21k", + "architecture_id": "ViTModel", + "verified_date": "2026-07-30", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "facebook/deit-base-distilled-patch16-224", + "architecture_id": "DeiTForImageClassification", + "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", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "MIT/ast-finetuned-speech-commands-v2", + "architecture_id": "ASTForAudioClassification", + "verified_date": "2026-07-30", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "facebook/deit-base-patch16-224", + "architecture_id": "ViTForImageClassification", + "verified_date": "2026-07-30", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "MIT/ast-finetuned-audioset-10-10-0.4593", + "architecture_id": "ASTForAudioClassification", + "verified_date": "2026-07-30", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "google/vit-base-patch16-224", + "architecture_id": "ViTForImageClassification", + "verified_date": "2026-07-30", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "facebook/hubert-large-ls960-ft", + "architecture_id": "HubertForCTC", + "verified_date": "2026-07-30", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "google/vit-base-patch16-224", + "architecture_id": "ViTForImageClassification", + "verified_date": "2026-07-31", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "google/vit-base-patch16-224-in21k", + "architecture_id": "ViTModel", + "verified_date": "2026-07-31", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "facebook/deit-base-patch16-224", + "architecture_id": "ViTForImageClassification", + "verified_date": "2026-07-31", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "facebook/deit-base-patch16-224", + "architecture_id": "ViTForImageClassification", + "verified_date": "2026-07-31", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null + }, + { + "model_id": "google/vit-base-patch16-224", + "architecture_id": "ViTForImageClassification", + "verified_date": "2026-07-31", + "verified_by": "verify_models", + "transformerlens_version": null, + "notes": "Full verification completed", + "invalidated": false, + "invalidation_reason": null } ] } diff --git a/transformer_lens/tools/model_registry/generate_report.py b/transformer_lens/tools/model_registry/generate_report.py index 559425cf24..fd50e33944 100644 --- a/transformer_lens/tools/model_registry/generate_report.py +++ b/transformer_lens/tools/model_registry/generate_report.py @@ -28,6 +28,7 @@ # Supported architectures "AfmoeForCausalLM": "Arcee AI's Trinity MoE with sandwich norms, gated attention, and token-choice routing", "ArceeForCausalLM": "Arcee AI's AFM, a Llama-style decoder with an ungated squared-ReLU (ReLU^2) MLP", + "ASTForAudioClassification": "MIT's Audio Spectrogram Transformer model for audio classification", "GPT2LMHeadModel": "OpenAI's GPT-2 decoder-only transformer for causal language modeling", "GPTNeoForCausalLM": "EleutherAI's GPT-Neo, an open-source GPT-3-like model", "GPTNeoXForCausalLM": "EleutherAI's GPT-NeoX architecture used in Pythia models", @@ -79,6 +80,7 @@ "OlmoHybridForCausalLM": "Allen AI's OLMo Hybrid (GatedDeltaNet linear attention + full attention)", "StableLmForCausalLM": "Stability AI's StableLM model", "SmolLM3ForCausalLM": "Hugging Face's SmolLM3 compact open model with NoPE layers", + "Starcoder2ForCausalLM": "BigCode's StarCoder2 code generation model", "T5ForConditionalGeneration": "Google's T5 encoder-decoder model (partial support)", "T5WithLMHeadModel": "Legacy T5 class name on old google-t5 checkpoints (t5-3b, t5-11b)", "T5GemmaForConditionalGeneration": "Google's T5Gemma encoder-decoder model with Gemma-style RoPE, GQA, and gated MLP", @@ -128,6 +130,12 @@ "RwkvForCausalLM": "BlinkDL RWKV-4 WKV linear-attention RNN (Pythia-parallel Pile suite)", "NanoChatForCausalLM": "Karpathy's nanochat education-stack decoder (weightless norms, relu^2 MLP, capped logits)", "HunYuanDenseV1ForCausalLM": "Tencent's open source decoder models", + "ViTModel": "Vision Transformer (bare encoder, no classification head)", + "ViTForImageClassification": "Vision Transformer with an image classification head", + "DeiTModel": "Data-efficient Image Transformer (bare encoder)", + "DeiTForImageClassification": "DeiT with a single CLS-token classification head", + "Wav2Vec2Model": "Facebook's Wav2Vec 2.0 for speech", + "HubertModel": "Facebook's HuBERT for speech", "Cohere2ForCausalLM": "Cohere's Command-A architecture with interleaved sliding-window RoPE and full-attention NoPE layers", "OuroForCausalLM": "ByteDance's Ouro looped language model (LoopLM) with weight-shared iterated depth", "RavenForCausalLM": "tomg-group-umd's Huginn depth-recurrent decoder (prelude / weight-tied recurrent core / coda) with runtime recurrence count", @@ -153,14 +161,10 @@ "MT5ForConditionalGeneration": "Multilingual T5", "WhisperForConditionalGeneration": "OpenAI's Whisper speech recognition", "CLIPModel": "OpenAI's CLIP vision-language model", - "ViTModel": "Google's Vision Transformer", "SwinModel": "Microsoft's Swin Transformer for vision", - "DeiTModel": "Facebook's Data-efficient Image Transformer", "BeitModel": "Microsoft's BERT pre-training for images", "ConvNextModel": "Facebook's ConvNeXt modernized ConvNet", "SegformerModel": "NVIDIA's SegFormer for segmentation", - "Wav2Vec2Model": "Facebook's Wav2Vec 2.0 for speech", - "HubertModel": "Facebook's HuBERT for speech", "SpeechT5Model": "Microsoft's SpeechT5 for speech tasks", "BlipModel": "Salesforce's BLIP vision-language model", "Blip2Model": "Salesforce's BLIP-2 with frozen LLM", diff --git a/transformer_lens/tools/model_registry/hf_scraper.py b/transformer_lens/tools/model_registry/hf_scraper.py index 87eb99b1ad..b34f470346 100644 --- a/transformer_lens/tools/model_registry/hf_scraper.py +++ b/transformer_lens/tools/model_registry/hf_scraper.py @@ -173,6 +173,7 @@ def _build_model_entry(model_id: str, architecture_id: str) -> dict: "phase4_score": None, "phase7_score": None, "phase8_score": None, + "phase9_score": None, } diff --git a/transformer_lens/tools/model_registry/registry_io.py b/transformer_lens/tools/model_registry/registry_io.py index 2f926121b5..dfe075e6a3 100644 --- a/transformer_lens/tools/model_registry/registry_io.py +++ b/transformer_lens/tools/model_registry/registry_io.py @@ -229,7 +229,7 @@ def update_model_status( elif phase_scores and "exceeds" in (entry.get("note") or "").lower(): # Writing real scores clears a stale memory-skip note entry["note"] = None - for phase_num in (1, 2, 3, 4, 7, 8): + for phase_num in (1, 2, 3, 4, 7, 8, 9): key = f"phase{phase_num}_score" if phase_num in phase_scores: entry[key] = phase_scores[phase_num] @@ -249,6 +249,7 @@ def update_model_status( "phase4_score", "phase7_score", "phase8_score", + "phase9_score", ] reordered = {k: entry[k] for k in _KEY_ORDER if k in entry} for k in entry: @@ -277,6 +278,7 @@ def update_model_status( "phase4_score": phase_scores.get(4), "phase7_score": phase_scores.get(7), "phase8_score": phase_scores.get(8), + "phase9_score": phase_scores.get(9), } ) updated = True diff --git a/transformer_lens/tools/model_registry/schemas.py b/transformer_lens/tools/model_registry/schemas.py index 6d29908c6a..6cd941c154 100644 --- a/transformer_lens/tools/model_registry/schemas.py +++ b/transformer_lens/tools/model_registry/schemas.py @@ -70,6 +70,7 @@ class ModelEntry: phase4_score: Benchmark Phase 4 score (generation / text quality), 0-100 or None phase7_score: Benchmark Phase 7 score (multimodal), 0-100 or None phase8_score: Benchmark Phase 8 score (audio), 0-100 or None + phase9_score: Benchmark Phase 9 score (vision), 0-100 or None """ architecture_id: str @@ -84,6 +85,7 @@ class ModelEntry: phase4_score: Optional[float] = None phase7_score: Optional[float] = None phase8_score: Optional[float] = None + phase9_score: Optional[float] = None def to_dict(self) -> dict: """Convert to a JSON-serializable dictionary.""" @@ -100,6 +102,7 @@ def to_dict(self) -> dict: "phase4_score": self.phase4_score, "phase7_score": self.phase7_score, "phase8_score": self.phase8_score, + "phase9_score": self.phase9_score, } @classmethod @@ -131,6 +134,7 @@ def from_dict(cls, data: dict) -> "ModelEntry": phase4_score=data.get("phase4_score"), phase7_score=data.get("phase7_score"), phase8_score=data.get("phase8_score"), + phase9_score=data.get("phase9_score"), ) diff --git a/transformer_lens/tools/model_registry/verify_models.py b/transformer_lens/tools/model_registry/verify_models.py index f2962b25f0..22541888ba 100644 --- a/transformer_lens/tools/model_registry/verify_models.py +++ b/transformer_lens/tools/model_registry/verify_models.py @@ -123,17 +123,17 @@ def _sanitize_note(note: Optional[str]) -> Optional[str]: def _phases_to_run(arch: str, phases: list[int]) -> list[int]: """Restrict requested phases to those the adapter supports. - An adapter's ``applicable_phases`` declares which text phases (1-4) it covers. Phases 7/8 - are gated separately by ``is_multimodal``/``is_audio`` in the benchmark, so they are never - filtered out here. An empty result means none of the requested phases apply to this - architecture (SSM / recurrent families run all four). + An adapter's ``applicable_phases`` declares which text phases (1-4) it covers. Phases + 7/8/9 are gated separately by ``is_multimodal``/``is_audio_model``/``is_visual_model`` + in the benchmark, so they are never filtered out here. An empty result means none of + the requested phases apply to this architecture (SSM / recurrent families run all four). """ from transformer_lens.factories.architecture_adapter_factory import ( SUPPORTED_ARCHITECTURES, ) applicable = getattr(SUPPORTED_ARCHITECTURES.get(arch), "applicable_phases", [1, 2, 3, 4]) - return [p for p in phases if p in applicable or p in (7, 8)] + return [p for p in phases if p in applicable or p in (7, 8, 9)] def _full_and_core_phases(arch: str) -> tuple[set[int], set[int]]: @@ -144,6 +144,12 @@ def _full_and_core_phases(arch: str) -> tuple[set[int], set[int]]: kind = classify_architecture(arch) if kind == "audio": return {1, 8}, {1, 8} + if kind == "vision": + # Vision encoders have no tokenizer and no text tower: Phases 2/3 need + # HookedTransformer, Phase 4 needs text generation, and Phase 7 covers + # vision+text multimodal models, not these. Phase 1 (HF parity) plus + # Phase 9 (pixel forward/cache/stability) are the whole story. + return {1, 9}, {1, 9} if kind == "multimodal": return {1, 2, 3, 4, 7}, {1, 4, 7} if arch in AUDIO_TEXT_ARCHITECTURES: @@ -558,7 +564,7 @@ def _extract_phase_scores(results: list) -> dict[int, Optional[float]]: """ from transformer_lens.benchmarks.utils import BenchmarkSeverity - phase_results: dict[int, list[bool]] = {1: [], 2: [], 3: [], 4: [], 7: [], 8: []} + phase_results: dict[int, list[bool]] = {1: [], 2: [], 3: [], 4: [], 7: [], 8: [], 9: []} for result in results: if result.phase in phase_results and result.severity != BenchmarkSeverity.SKIPPED: phase_results[result.phase].append(result.passed) @@ -593,6 +599,7 @@ def _extract_phase_scores(results: list) -> dict[int, Optional[float]]: 4: 50.0, 7: 75.0, 8: 75.0, + 9: 75.0, } _DEFAULT_MIN_PHASE_SCORE = 50.0 @@ -614,12 +621,22 @@ def _extract_phase_scores(results: list) -> dict[int, Optional[float]]: 3: ["logits_equivalence", "loss_equivalence"], 7: ["multimodal_forward"], 8: ["audio_forward", "audio_text_forward"], + 9: ["vision_forward", "vision_cache"], +} + +# Failure text for a modality phase that produced no score — either absent from +# phase_scores (all tests skipped) or explicitly NULL. +_MODALITY_NULL_MESSAGES: dict[int, str] = { + 7: "P7=NULL (multimodal tests skipped — processor unavailable)", + 8: "P8=NULL (audio tests skipped — no results)", + 9: "P9=NULL (vision tests skipped — no results)", } def _check_phase_scores( phase_scores: dict[int, Optional[float]], all_results: list, + required_phases: Optional[set[int]] = None, ) -> Optional[str]: """Check phase scores against per-phase minimum thresholds and required tests. @@ -631,21 +648,33 @@ def _check_phase_scores( correctness check. Low text quality is surfaced in the verification note via _build_verified_note() but never causes a model to fail. + Args: + phase_scores: Per-phase scores; a phase whose tests all skipped is + absent entirely (``extract_phase_scores`` omits empty phases). + all_results: Benchmark results, used to name the failing tests. + required_phases: Phases this architecture must produce a score for — + the core set from ``_full_and_core_phases``. A required modality + phase that is absent counts as NULL, not as a pass. + Returns an error message if any phase fails, or None if all phases pass. The message includes the names of failed tests. """ from transformer_lens.benchmarks.utils import BenchmarkSeverity failing_phases: list[str] = [] + + # A required modality phase whose tests all skipped never reaches phase_scores. + for phase in sorted((required_phases or set()) - set(phase_scores)): + if phase in _MODALITY_NULL_MESSAGES: + failing_phases.append(_MODALITY_NULL_MESSAGES[phase]) + for phase, score in sorted(phase_scores.items()): if score is None: - # Phase 7 (multimodal) or Phase 8 (audio) with a NULL score means - # the processor was unavailable and no tests ran. This is a - # verification failure, not something to silently skip. - if phase == 7: - failing_phases.append(f"P7=NULL (multimodal tests skipped — processor unavailable)") - elif phase == 8: - failing_phases.append(f"P8=NULL (audio tests skipped — no results)") + # Phase 7 (multimodal), Phase 8 (audio), or 9 (vision) with a NULL score means + # the modality tests never ran. This is a verification failure, + # not something to silently skip. + if phase in _MODALITY_NULL_MESSAGES: + failing_phases.append(_MODALITY_NULL_MESSAGES[phase]) continue # Phase 4 is a quality metric, not a pass/fail check — skip it here. @@ -971,7 +1000,12 @@ def verify_models( phase_scores = _extract_phase_scores(all_results) if not error_msg: - score_error = _check_phase_scores(phase_scores, all_results) + # Only require the core phases this run actually requested, so a + # partial run (e.g. --phases 1 2) isn't failed for a missing P7. + _, core_for_arch = _full_and_core_phases(arch) + score_error = _check_phase_scores( + phase_scores, all_results, required_phases=core_for_arch & set(eff_phases) + ) if score_error: error_msg = score_error @@ -1155,7 +1189,7 @@ def verify_models( f" {label}: P1={phase_scores.get(1)}%, " f"P2={phase_scores.get(2)}%, P3={phase_scores.get(3)}%, " f"P4={phase_scores.get(4)}%, P7={phase_scores.get(7)}%, " - f"P8={phase_scores.get(8)}%" + f"P8={phase_scores.get(8)}%, P9={phase_scores.get(9)}%" ) update_model_status( model_id, @@ -1184,7 +1218,7 @@ def verify_models( f" Partial scores saved: P1={phase_scores.get(1)}%, " f"P2={phase_scores.get(2)}%, P3={phase_scores.get(3)}%, " f"P4={phase_scores.get(4)}%, P7={phase_scores.get(7)}%, " - f"P8={phase_scores.get(8)}%" + f"P8={phase_scores.get(8)}%, P9={phase_scores.get(9)}%" ) update_model_status( model_id, @@ -1400,7 +1434,8 @@ def main() -> None: default=None, help=( "Which benchmark phases to run (default: a full verification for each " - "model's architecture — 1 2 3 4 for text, 1 2 3 4 7 for multimodal, 1 8 for audio)" + "model's architecture — 1 2 3 4 for text, 1 2 3 4 7 for multimodal, " + "1 8 for audio, 1 9 for vision)" ), ) parser.add_argument( diff --git a/transformer_lens/utilities/architectures.py b/transformer_lens/utilities/architectures.py index 5dda517029..cf9558ae52 100644 --- a/transformer_lens/utilities/architectures.py +++ b/transformer_lens/utilities/architectures.py @@ -64,12 +64,30 @@ "MusicFlamingoForConditionalGeneration", } +# Audio spectrogram models for classification +AUDIO_CLASSIFICATION_ARCHITECTURES: set[str] = { + "ASTForAudioClassification", +} + # Audio encoder models (HuBERT, wav2vec2, etc.) AUDIO_ARCHITECTURES: set[str] = { "HubertForCTC", "HubertModel", "HubertForSequenceClassification", +} | AUDIO_CLASSIFICATION_ARCHITECTURES + +# Vision-only (non-multimodal, no text tower) encoder models. Split into the +# two HF AutoModel classes they load under: bare encoders load via AutoModel, +# classification heads load via AutoModelForImageClassification. +VISION_MODEL_ARCHITECTURES: set[str] = { + "ViTModel", + "DeiTModel", } +VISION_CLASSIFICATION_ARCHITECTURES: set[str] = { + "ViTForImageClassification", + "DeiTForImageClassification", +} +VISION_ARCHITECTURES: set[str] = VISION_MODEL_ARCHITECTURES | VISION_CLASSIFICATION_ARCHITECTURES # Text models whose remote code registers only under plain AutoModel # (the class itself carries the LM head). @@ -82,6 +100,8 @@ NO_HT_COMPARISON_ARCHITECTURES: set[str] = ( MULTIMODAL_ARCHITECTURES | AUDIO_ARCHITECTURES + # Vision encoders have no HookedTransformer counterpart. + | VISION_ARCHITECTURES # Encoder-decoder: HookedTransformer cannot represent them (T5 repos under # org-prefixed names slip past HT's legacy name guard and crash at forward). | SEQ2SEQ_ARCHITECTURES @@ -94,7 +114,7 @@ def classify_architecture(architecture: str) -> str: """Classify an architecture string into a model type. - Returns one of: "seq2seq", "masked_lm", "multimodal", "audio", "causal_lm" + Returns one of: "seq2seq", "masked_lm", "multimodal", "audio", "vision", "causal_lm" """ if architecture in SEQ2SEQ_ARCHITECTURES: return "seq2seq" @@ -104,6 +124,8 @@ def classify_architecture(architecture: str) -> str: return "multimodal" if architecture in AUDIO_ARCHITECTURES: return "audio" + if architecture in VISION_ARCHITECTURES: + return "vision" return "causal_lm"