English
sparse-autoencoder
mechanistic-interpretability
biosafety
biorefusalaudit
gemma
gemma4
sae
Solshine commited on
Commit
ce89fc2
·
verified ·
1 Parent(s): 2f320f9

Upload README.md with huggingface_hub

Browse files
Files changed (1) hide show
  1. README.md +487 -487
README.md CHANGED
@@ -1,487 +1,487 @@
1
- ---
2
- license: other
3
- license_name: hl3-bds-cl-eco-extr-ffd-media-mil-my-sup-sv-tal-usta-xuar
4
- license_link: https://firstdonoharm.dev/version/3/0/bds-cl-eco-extr-ffd-media-mil-my-sup-sv-tal-usta-xuar.html
5
- tags:
6
- - sparse-autoencoder
7
- - mechanistic-interpretability
8
- - biosafety
9
- - biorefusalaudit
10
- - gemma
11
- - gemma4
12
- - sae
13
- base_model: google/gemma-4-E2B-it
14
- datasets:
15
- - cais/wmdp-corpora
16
- - SolshineCode/biorefusalaudit-eval-public
17
- language:
18
- - en
19
- ---
20
-
21
- # BioRefusalAudit — Gemma 4 E2B-IT Contrastive Bio-Safety SAE (v1)
22
-
23
- A TopK Sparse Autoencoder (SAE) fine-tuned on biology-domain residual-stream activations
24
- from `google/gemma-4-E2B-it` at layer 17, with a mean contrastive objective that pushes
25
- hazard-adjacent and benign biological feature profiles apart in activation space.
26
-
27
- Trained on Colab T4 (Tesla T4, 15.6 GB VRAM) as part of the
28
- [BioRefusalAudit](https://github.com/SolshineCode/Deleeuw-AI-x-Bio-hackathon) project
29
- (AIxBio Hackathon 2026, Track 3: Biosecurity Tools, sponsored by Fourth Eon Bio).
30
-
31
- ---
32
-
33
- ## Architecture
34
-
35
- | Parameter | Value |
36
- |-----------|-------|
37
- | Type | TopK Sparse Autoencoder |
38
- | d_model | 1536 (Gemma 4 E2B text hidden size at layer 17) |
39
- | d_sae | 6144 (4× expansion) |
40
- | k (sparsity) | 32 active features per token position |
41
- | Hook layer | Layer 17 (residual stream, post-MLP) |
42
- | Base model | google/gemma-4-E2B-it |
43
- | Encoder / Decoder | `nn.Linear` layers with learned biases |
44
-
45
- **Important:** the encoder and decoder are standard `nn.Linear` modules (not raw `nn.Parameter` matrices). When loading state dicts from earlier drafts or other repos, confirm the key names match (`W_enc.weight`, `W_enc.bias`, `W_dec.weight`, `W_dec.bias`).
46
-
47
- ---
48
-
49
- ## Weights
50
-
51
- | File | Description |
52
- |------|-------------|
53
- | `sae_weights_final.pt` | **Recommended.** Final checkpoint after 2000 steps. |
54
- | `sae_weights_step_500.pt` | Intermediate — contrastive signal still converging. |
55
- | `sae_weights_step_1000.pt` | Peak contrastive loss step before reconstruction dominates. |
56
- | `sae_weights_step_1500.pt` | Intermediate. |
57
- | `sae_weights_step_2000.pt` | Same as `sae_weights_final.pt`; included for clarity. |
58
-
59
- ---
60
-
61
- ## Training
62
-
63
- - **Dataset:** `cais/wmdp-corpora` bio-retain-corpus (benign biology, ~5,000 documents)
64
- + local BioRefusalAudit 75-prompt eval set (22 hazard-adjacent prompts)
65
- - **Contrastive objective:** Mean contrastive — cosine similarity between the mean feature
66
- profile of hazard-adjacent tokens and the mean feature profile of benign tokens.
67
- `L_contrastive = cos_sim(mean(z_hazard), mean(z_benign))` — minimized so the two groups
68
- push apart.
69
- - **Total loss:** `L = L_recon + 0.04 * L_sparsity + 0.1 * L_contrastive`
70
- - **Steps:** 2,000 — `MAX_STEPS=2000`, `BATCH_SIZE=4`, `LR=3e-4`
71
- - **Optimizer:** AdamW
72
- - **Hardware:** Colab Tesla T4 (15.6 GB VRAM), ~35 min wall time
73
- - **Decoder constraint:** Decoder columns projected back to unit sphere after each step
74
- (`normalize_decoder()`) and gradient component parallel to decoder columns removed
75
- (`project_grad()`), following the Anthropic SAE training recipe.
76
- - **Chat template:** Training prompts wrapped via `tokenizer.apply_chat_template` so the
77
- RLHF safety circuit activates during collection. Raw text would be out-of-distribution.
78
-
79
- ### Training outcome
80
-
81
- | Metric | Start | Step 1000 | Final (step 2000) |
82
- |--------|-------|-----------|-------------------|
83
- | l_recon | ~3.2 | ~0.8 | **0.557** |
84
- | l_sparsity | — | — | (tracked) |
85
- | l_contrastive | ~0.7 | — | **~0** (collapsed) |
86
- | L0 (mean active) | 32.0 | 32.0 | 32.0 |
87
-
88
- The contrastive loss collapsed to near-zero by step ~1000–1500. This is a known failure
89
- mode when the positive/negative corpus is too small for the NT-Xent / cosine-similarity
90
- objective to maintain separation — the SAE learns to map all inputs to near-identical
91
- directions, satisfying the reconstruction objective while the contrastive margin vanishes.
92
- The reconstruction loss (l_recon=0.557) shows the SAE is encoding the residual stream, but
93
- bio-feature separation is not guaranteed. **Treat the contrastive fine-tuning as a proof of
94
- concept; use the companion Gemma Scope community SAE for production bio-safety audits until
95
- a larger corpus run is available.**
96
-
97
- ---
98
-
99
- ## Step-by-step loading
100
-
101
- ### 1. Install dependencies
102
-
103
- ```bash
104
- pip install torch transformers bitsandbytes accelerate huggingface_hub
105
- ```
106
-
107
- ### 2. Define the TopKSAE class
108
-
109
- The state dict uses `nn.Linear` key names. You must define the class this way:
110
-
111
- ```python
112
- import torch
113
- import torch.nn as nn
114
- import torch.nn.functional as F
115
-
116
- class TopKSAE(nn.Module):
117
- def __init__(self, d_model: int = 1536, d_sae: int = 6144, k: int = 32):
118
- super().__init__()
119
- self.k = k
120
- self.W_enc = nn.Linear(d_model, d_sae, bias=True)
121
- self.W_dec = nn.Linear(d_sae, d_model, bias=True)
122
-
123
- def forward(self, x):
124
- """
125
- Args:
126
- x: (..., d_model) float tensor
127
- Returns:
128
- x_hat: reconstruction, same shape as x
129
- z: sparse feature activations (..., d_sae) — k nonzero per position
130
- pre: pre-topk encoder output (..., d_sae)
131
- """
132
- pre = self.W_enc(x)
133
- topk_vals, topk_idx = torch.topk(pre, self.k, dim=-1)
134
- z = torch.zeros_like(pre)
135
- z.scatter_(-1, topk_idx, F.relu(topk_vals))
136
- x_hat = self.W_dec(z)
137
- return x_hat, z, pre
138
-
139
- def encode(self, x):
140
- """Return only sparse feature vector z."""
141
- _, z, _ = self.forward(x)
142
- return z
143
- ```
144
-
145
- ### 3. Download and load the weights
146
-
147
- ```python
148
- from huggingface_hub import hf_hub_download
149
-
150
- # Download the final checkpoint
151
- weights_path = hf_hub_download(
152
- repo_id="Solshine/gemma4-e2b-bio-sae-v1",
153
- filename="sae_weights_final.pt",
154
- )
155
-
156
- sae = TopKSAE(d_model=1536, d_sae=6144, k=32)
157
- sae.load_state_dict(torch.load(weights_path, map_location="cpu"))
158
- sae.eval()
159
- print(f"SAE loaded. Parameters: {sum(p.numel() for p in sae.parameters()):,}")
160
- # → SAE loaded. Parameters: 18,882,048
161
- ```
162
-
163
- ### 4. Load Gemma 4 E2B-IT with 4-bit quantization
164
-
165
- Gemma 4 is a multimodal model (`Gemma4ForConditionalGeneration`). The text backbone lives
166
- at `model.language_model` inside the outer model. Use `device_map={"": 0}` (integer device
167
- index) — do **not** use `"auto"` or `{"": "cuda"}` (string) with bitsandbytes on Windows;
168
- both silently route to CPU.
169
-
170
- ```python
171
- import torch
172
- from transformers import AutoTokenizer, BitsAndBytesConfig
173
-
174
- # Try CausalLM first; fall back to the multimodal class if the model type isn't registered
175
- try:
176
- from transformers import AutoModelForCausalLM
177
- model = AutoModelForCausalLM.from_pretrained(
178
- "google/gemma-4-E2B-it",
179
- quantization_config=BitsAndBytesConfig(
180
- load_in_4bit=True,
181
- bnb_4bit_quant_type="nf4",
182
- bnb_4bit_use_double_quant=True,
183
- bnb_4bit_compute_dtype=torch.float16,
184
- ),
185
- device_map={"": 0}, # integer index — never the string "cuda"
186
- low_cpu_mem_usage=True,
187
- )
188
- except Exception:
189
- from transformers import AutoModelForImageTextToText
190
- model = AutoModelForImageTextToText.from_pretrained(
191
- "google/gemma-4-E2B-it",
192
- quantization_config=BitsAndBytesConfig(
193
- load_in_4bit=True,
194
- bnb_4bit_quant_type="nf4",
195
- bnb_4bit_use_double_quant=True,
196
- bnb_4bit_compute_dtype=torch.float16,
197
- ),
198
- device_map={"": 0},
199
- low_cpu_mem_usage=True,
200
- )
201
-
202
- model.eval()
203
- tokenizer = AutoTokenizer.from_pretrained("google/gemma-4-E2B-it")
204
- tokenizer.pad_token = tokenizer.eos_token
205
- ```
206
-
207
- ### 5. Attach the residual-stream hook at layer 17
208
-
209
- Gemma 4's transformer layers live at `model.language_model.layers` (inside the multimodal
210
- wrapper). The helper below handles multiple known layout variants:
211
-
212
- ```python
213
- def get_layer(model, layer_idx: int = 17):
214
- """Locate transformer block list across Gemma 2/3/4 text-only & multimodal layouts."""
215
- for path in (
216
- "model.language_model", # Gemma 4 ForConditionalGeneration
217
- "language_model.model", # Gemma 3 ForConditionalGeneration (older)
218
- "language_model",
219
- "model",
220
- "transformer",
221
- ):
222
- obj = model
223
- try:
224
- for attr in path.split("."):
225
- obj = getattr(obj, attr)
226
- except AttributeError:
227
- continue
228
- if hasattr(obj, "layers"):
229
- return obj.layers[layer_idx]
230
- raise AttributeError(f"Could not locate layers in {type(model).__name__}")
231
-
232
- captured = [None]
233
-
234
- def hook_fn(module, inputs, outputs):
235
- # Overwrite (not append) — appending fills VRAM fast during autoregressive generation
236
- captured[0] = (outputs[0] if isinstance(outputs, tuple) else outputs).detach()
237
-
238
- handle = get_layer(model, 17).register_forward_hook(hook_fn)
239
- ```
240
-
241
- ### 6. Collect activations and run the SAE
242
-
243
- ```python
244
- prompt = "Describe the mechanism by which influenza binds to host cells."
245
-
246
- # Always use the Gemma chat template — raw text is out of distribution for an IT model
247
- messages = [{"role": "user", "content": prompt}]
248
- formatted = tokenizer.apply_chat_template(
249
- messages, tokenize=False, add_generation_prompt=True
250
- )
251
-
252
- inputs = tokenizer(formatted, return_tensors="pt", truncation=True, max_length=512).to("cuda")
253
-
254
- with torch.no_grad():
255
- _ = model(**inputs) # forward pass fires the hook
256
- acts = captured[0] # (1, seq_len, 1536)
257
-
258
- # Run through the SAE (cast to float32 — NF4 activations are fp16)
259
- x = acts.squeeze(0).float() # (seq_len, 1536)
260
- with torch.no_grad():
261
- x_hat, z, pre = sae(x) # z: (seq_len, 6144), 32 nonzero per row
262
-
263
- # Top-5 most active features (averaged across sequence positions)
264
- mean_z = z.mean(0) # (6144,)
265
- top_features = mean_z.topk(5)
266
- print("Top-5 features (index, mean activation):")
267
- for idx, val in zip(top_features.indices.tolist(), top_features.values.tolist()):
268
- print(f" Feature {idx:5d}: {val:.4f}")
269
-
270
- handle.remove() # clean up hook when done
271
- ```
272
-
273
- ---
274
-
275
- ## Integration with BioRefusalAudit pipeline
276
-
277
- If you are using the full BioRefusalAudit CLI, pass this SAE via:
278
-
279
- ```bash
280
- python -m biorefusalaudit.cli run \
281
- --model google/gemma-4-E2B-it \
282
- --eval-set data/eval_set_public/eval_set_public_v1.jsonl \
283
- --out runs/gemma4-oursae-v1 \
284
- --sae-source custom \
285
- --sae-release Solshine/gemma4-e2b-bio-sae-v1 \
286
- --k 32 \
287
- --d-model 1536 \
288
- --d-sae 6144 \
289
- --architecture topk \
290
- --layer 17 \
291
- --quantize 4bit \
292
- --no-llm-judges \
293
- --max-new-tokens 80 \
294
- --dump-activations
295
- ```
296
-
297
- The `--sae-source custom` path in `sae_adapter.py` will:
298
- 1. Detect that `Solshine/gemma4-e2b-bio-sae-v1` is an HF repo ID (contains `/`, is not a local path)
299
- 2. Try `sae_weights.pt` → `sae_weights.safetensors` → scan repo for any `.pt`/`.safetensors`
300
- 3. Download `sae_weights_final.pt` (alphabetically first `.pt` in the repo) via `hf_hub_download`
301
- 4. Load into a `TopKSAE(d_model=1536, d_sae=6144, k=32)` instance
302
-
303
- For the full pass-1 → auto-tune catalog → pass-2 → fit-T pipeline, use the convenience script:
304
-
305
- ```bash
306
- bash scripts/run_gemma4_oursae_pipeline.sh 2>&1 | tee runs/gemma4-oursae-pipeline.log
307
- ```
308
-
309
- ---
310
-
311
- ## Evaluation Results
312
-
313
- Results from running this SAE through the full BioRefusalAudit pipeline on the 75-prompt
314
- public evaluation set (`eval_set_public_v1.jsonl`), covering benign biology, dual-use
315
- biology, and hazard-adjacent prompts across four framings (direct, educational, roleplay,
316
- obfuscated). Model: `google/gemma-4-E2B-it`, 4-bit NF4, 80-token budget.
317
-
318
- ### Feature activation (pass1, no catalog)
319
-
320
- 1285 out of 1500 bio_content feature checks fired across all 75 prompts, with mean activation
321
- 14.71 on active tokens. The WMDP-trained features generalize to the BioRefusalAudit eval set.
322
-
323
- ### Auto-tuned catalog (Cohen's d, post-pass1)
324
-
325
- `scripts/auto_tune_catalog.py` selects the top-20 features per category by Cohen's d
326
- between hazard-adjacent and benign activation profiles:
327
-
328
- | Category | Top Cohen's d | Features selected |
329
- |---|---:|---:|
330
- | bio_content | 1.81 | 20 |
331
- | hazard_adjacent | 1.60 | 20 |
332
- | refusal_circuitry | 1.52 | 20 |
333
-
334
- 33 unique features total (some features appear in multiple categories).
335
- Catalog: `data/feature_catalog/gemma-4-e2b-our-sae-v1.json`.
336
-
337
- ### Divergence metric results (pass2, 80-tok)
338
-
339
- **Calibration note — important.** The BioRefusalAudit pipeline fits calibration matrix T
340
- from pass1 activations. When pass1 runs without a catalog (as it must to bootstrap the
341
- catalog), all feature vectors are zero → `fit_calibration.py` produces T=0 → D=1.000 for
342
- every prompt regardless of actual SAE activations. This is a known pipeline ordering issue,
343
- now guarded in `biorefusalaudit/scoring/divergence.py` (falls back to T_prior when fitted T
344
- is degenerate). Results below use T_prior (identity-biased permutation), not a data-fitted T.
345
- T_prior produces higher absolute D than a fitted T would.
346
-
347
- | Tier | n | Mean D | Std | comply | refuse | hedge |
348
- |---|---:|---:|---:|---:|---:|---:|
349
- | `benign_bio` | 23 | 0.614 | 0.321 | 57% | 43% | 0% |
350
- | `dual_use_bio` | 30 | 0.703 | 0.295 | 70% | 30% | 0% |
351
- | `hazard_adjacent` | 22 | 0.647 | 0.306 | 59% | 36% | 5% |
352
- | **overall** | **75** | **0.659** | **0.309** | | | |
353
-
354
- **Comparison to Gemma Scope community SAE** (Gemma 2 2B-IT, fitted T, 80-tok — different
355
- model, different calibration, not directly comparable):
356
-
357
- | Tier | Gemma Scope baseline | This SAE (T_prior) |
358
- |---|---:|---:|
359
- | `benign_bio` | 0.362 | 0.614 |
360
- | `dual_use_bio` | 0.406 | 0.703 |
361
- | `hazard_adjacent` | 0.404 | 0.647 |
362
-
363
- The delta reflects T miscalibration and the model difference (Gemma 4 vs. Gemma 2), not a
364
- meaningful SAE quality gap. A properly fitted T from a behavioral corpus is needed to
365
- interpret absolute D magnitudes.
366
-
367
- **Surface label finding.** `benign_bio` refuses at 43% — the over-refusal pattern documented
368
- in BioRefusalAudit §4.6: Gemma 4 E2B's safety circuit fires on biosecurity-adjacent content
369
- regardless of hazard tier. This SAE confirms the same pattern as the Gemma Scope community
370
- SAE baseline.
371
-
372
- ---
373
-
374
- ## Training your own bio-safety SAE
375
-
376
- The training notebook is at
377
- [notebooks/colab_gemma4_sae_training.ipynb](https://github.com/SolshineCode/Deleeuw-AI-x-Bio-hackathon/blob/main/notebooks/colab_gemma4_sae_training.ipynb).
378
- It runs end-to-end on a free Colab T4 in ~35 minutes.
379
-
380
- **Quick start:**
381
- 1. Open the notebook in Google Colab (Runtime → Change runtime type → T4 GPU)
382
- 2. Add `HF_TOKEN` (write scope) and `WANDB_API_KEY` to Colab Secrets (🔑 icon)
383
- 3. Run All — the notebook will:
384
- - Install `transformers` from source (Gemma 4 requires the latest main branch)
385
- - Load `google/gemma-4-E2B-it` in NF4 4-bit quantization
386
- - Stream training data from `cais/wmdp-corpora` (bio-retain-corpus, public)
387
- - Train 2,000 steps with reconstruction + sparsity + mean contrastive loss
388
- - Upload final checkpoint to your HF account as `<your-username>/gemma4-e2b-bio-sae-v1`
389
-
390
- **Key implementation details that make it work on Colab:**
391
-
392
- | Problem | Fix |
393
- |---------|-----|
394
- | Gemma 4 multimodal layer path | `pick_layer()` with 5-path fallback + `named_modules()` slow-path scan |
395
- | Decoder collapse (all features becoming equal) | `normalize_decoder()` + `project_grad()` each step |
396
- | OOD inputs from raw corpus text | Wrap all prompts with `tokenizer.apply_chat_template` |
397
- | VRAM fill during generation | Hook overwrites `captured[0]` instead of appending to a list |
398
- | Contrastive loss instability | Mean contrastive (cosine sim of mean profiles) instead of NT-Xent |
399
-
400
- ---
401
-
402
- ## Caveats
403
-
404
- - **Contrastive collapse.** The contrastive loss reached ~0 by step ~1500. The SAE reconstructs residual-stream activations well but bio-feature *separation* is not confirmed. Verification requires running `auto_tune_catalog.py` and checking Cohen's d per category against the Gemma Scope baseline.
405
- - **Small corpus (v1).** Training used ~5,000 WMDP documents (benign) + 22 hazard-adjacent prompts. Too few hazard-adjacent examples to sustain the contrastive margin. This is the binding constraint — not compute, not architecture. **Fixed in v2** (see below).
406
- - **2000-step limit (v1).** Capped at 2000 steps; L_contrastive collapsed by step 1000. Final checkpoint reconstructs well but bio-feature separation is not confirmed. **Fixed in v2:** 5000 steps with real hazard corpus.
407
- - **No Neuronpedia validation.** Individual feature interpretability is unverified.
408
- - **4× expansion.** d_sae/d_model = 4.0, below Gemma Scope's 8×. Wider SAEs likely capture more bio-specific features.
409
- - **Gemma 4 multimodal wrapper.** Hook path is `model.language_model.layers[17]` — **not** `model.model.layers[17]` (Gemma 3 path). The `get_layer()` helper above handles this automatically.
410
-
411
- ### v2 training run (in progress)
412
-
413
- Access to `cais/wmdp-bio-forget-corpus` was granted on 2026-04-26. The v2 notebook
414
- (`notebooks/colab_gemma4_sae_training.ipynb`) now loads 5,000 papers from that corpus as
415
- the hazard-adjacent class, balanced against 5,000 benign documents from the retain corpus,
416
- for 5,000 training steps. This directly addresses the corpus-size bottleneck. Results will
417
- be published as `Solshine/gemma4-e2b-bio-sae-v2` on completion.
418
-
419
- ### What would further improve this SAE
420
-
421
- The corpus-size problem is now addressed for the primary bottleneck. Remaining priorities, in
422
- order of impact:
423
-
424
- 1. **More hazard-adjacent examples (partially addressed in v2).** 22 prompts is not enough to anchor a stable contrastive
425
- direction. 500–1000 genuine hazard-adjacent activation examples (from actual model
426
- responses, not just prompts) would likely sustain the contrastive margin through training.
427
- This requires access to institutional CBRN datasets — the kind held by organizations like
428
- Gryphon Scientific, NTI Bio, Johns Hopkins Center for Health Security, or government
429
- biosecurity agencies. We are actively seeking partnerships with these organizations and
430
- would welcome introductions from anyone in that space.
431
-
432
- 2. **A proper base-vs-RLHF activation corpus.** Following the methodology of Secret Agenda
433
- (arXiv:2509.20393): collect residual-stream activations from the base model and the
434
- instruction-tuned model on identical prompts, then train the SAE to separate "what the
435
- safety fine-tune changed" from "what was already there." This is a data-collection problem
436
- that requires running both model variants on the same hardware at scale.
437
-
438
- 3. **More compute for training.** A full SAE fine-tune at Anthropic/EleutherAI scale (100K+
439
- steps, A100 or H100) would not help if the corpus is still 22 hazard-adjacent prompts —
440
- the gradient signal simply isn't there. But a 10K-step run on a properly sized corpus
441
- (~10K hazard-adjacent samples) would be a reasonable next experiment and is feasible on a
442
- single A100 in a few hours. If you have access to institutional compute or CBRN datasets
443
- and want to run this experiment, please open an issue on the
444
- [BioRefusalAudit repo](https://github.com/SolshineCode/Deleeuw-AI-x-Bio-hackathon) or
445
- reach out directly.
446
-
447
- 4. **Wider SAE.** 8× or 16× expansion (d_sae = 12288 or 24576) with a larger k would give
448
- more features to specialize. This is a secondary bottleneck behind corpus size.
449
-
450
- ---
451
-
452
- ## Cross-Architecture Context
453
-
454
- This SAE targets Gemma 4 E2B-IT (2B parameters, multimodal, released April 2025). Compared
455
- to the Gemma 2 2B SAE in the same pipeline (`runs/sae-training-gemma2-5000steps/`):
456
-
457
- | | Gemma 4 E2B | Gemma 2 2B |
458
- |---|---|---|
459
- | d_model | 1536 | 2304 |
460
- | Hook layer | 17 | 12 |
461
- | Hook path | `model.language_model.layers` | `model.layers` |
462
- | SAE size | ~19 MB | ~28 MB |
463
- | Training steps | 2,000 (Colab T4) | 5,000 (local GPU) |
464
-
465
- ---
466
-
467
- ## Citation
468
-
469
- ```bibtex
470
- @misc{deleeuw2026biorefusalaudit,
471
- title = {BioRefusalAudit: Measuring Refusal Depth in LLMs
472
- via SAE Feature Divergence},
473
- author = {DeLeeuw, Caleb},
474
- year = {2026},
475
- howpublished = {AIxBio Hackathon 2026, Track 3: Biosecurity Tools},
476
- url = {https://github.com/SolshineCode/Deleeuw-AI-x-Bio-hackathon}
477
- }
478
- ```
479
-
480
- ---
481
-
482
- ## License
483
-
484
- Code and weights released under the
485
- [Hippocratic License 3.0 (HL3-BDS-CL-ECO-EXTR-FFD-MEDIA-MIL-MY-SUP-SV-TAL-USTA-XUAR)](https://firstdonoharm.dev/version/3/0/bds-cl-eco-extr-ffd-media-mil-my-sup-sv-tal-usta-xuar.html).
486
- You may use these weights for biosecurity research, AI safety research, and defensive
487
- interpretability work. You may not use them to facilitate harm.
 
1
+ ---
2
+ license: other
3
+ license_name: hl3-bds-cl-eco-extr-ffd-media-mil-my-sup-sv-tal-usta-xuar
4
+ license_link: https://firstdonoharm.dev/version/3/0/bds-cl-eco-extr-ffd-media-mil-my-sup-sv-tal-usta-xuar.html
5
+ tags:
6
+ - sparse-autoencoder
7
+ - mechanistic-interpretability
8
+ - biosafety
9
+ - biorefusalaudit
10
+ - gemma
11
+ - gemma4
12
+ - sae
13
+ base_model: google/gemma-4-E2B-it
14
+ datasets:
15
+ - cais/wmdp-corpora
16
+ - SolshineCode/biorefusalaudit-eval-public
17
+ language:
18
+ - en
19
+ ---
20
+
21
+ # BioRefusalAudit — Gemma 4 E2B-IT Contrastive Bio-Safety SAE (v1)
22
+
23
+ A TopK Sparse Autoencoder (SAE) fine-tuned on biology-domain residual-stream activations
24
+ from `google/gemma-4-E2B-it` at layer 17, with a mean contrastive objective that pushes
25
+ hazard-adjacent and benign biological feature profiles apart in activation space.
26
+
27
+ Trained on Colab T4 (Tesla T4, 15.6 GB VRAM) as part of the
28
+ [BioRefusalAudit](https://github.com/SolshineCode/Deleeuw-AI-x-Bio-hackathon) project
29
+ (AIxBio Hackathon 2026, Track 3: Biosecurity Tools, sponsored by Fourth Eon Bio).
30
+
31
+ ---
32
+
33
+ ## Architecture
34
+
35
+ | Parameter | Value |
36
+ |-----------|-------|
37
+ | Type | TopK Sparse Autoencoder |
38
+ | d_model | 1536 (Gemma 4 E2B text hidden size at layer 17) |
39
+ | d_sae | 6144 (4× expansion) |
40
+ | k (sparsity) | 32 active features per token position |
41
+ | Hook layer | Layer 17 (residual stream, post-MLP) |
42
+ | Base model | google/gemma-4-E2B-it |
43
+ | Encoder / Decoder | `nn.Linear` layers with learned biases |
44
+
45
+ **Important:** the encoder and decoder are standard `nn.Linear` modules (not raw `nn.Parameter` matrices). When loading state dicts from earlier drafts or other repos, confirm the key names match (`W_enc.weight`, `W_enc.bias`, `W_dec.weight`, `W_dec.bias`).
46
+
47
+ ---
48
+
49
+ ## Weights
50
+
51
+ | File | Description |
52
+ |------|-------------|
53
+ | `sae_weights_final.pt` | **Recommended.** Final checkpoint after 2000 steps. |
54
+ | `sae_weights_step_500.pt` | Intermediate — contrastive signal still converging. |
55
+ | `sae_weights_step_1000.pt` | Peak contrastive loss step before reconstruction dominates. |
56
+ | `sae_weights_step_1500.pt` | Intermediate. |
57
+ | `sae_weights_step_2000.pt` | Same as `sae_weights_final.pt`; included for clarity. |
58
+
59
+ ---
60
+
61
+ ## Training
62
+
63
+ - **Dataset:** `cais/wmdp-corpora` bio-retain-corpus (benign biology, ~5,000 documents)
64
+ + local BioRefusalAudit 75-prompt eval set (22 hazard-adjacent prompts)
65
+ - **Contrastive objective:** Mean contrastive — cosine similarity between the mean feature
66
+ profile of hazard-adjacent tokens and the mean feature profile of benign tokens.
67
+ `L_contrastive = cos_sim(mean(z_hazard), mean(z_benign))` — minimized so the two groups
68
+ push apart.
69
+ - **Total loss:** `L = L_recon + 0.04 * L_sparsity + 0.1 * L_contrastive`
70
+ - **Steps:** 2,000 — `MAX_STEPS=2000`, `BATCH_SIZE=4`, `LR=3e-4`
71
+ - **Optimizer:** AdamW
72
+ - **Hardware:** Colab Tesla T4 (15.6 GB VRAM), ~35 min wall time
73
+ - **Decoder constraint:** Decoder columns projected back to unit sphere after each step
74
+ (`normalize_decoder()`) and gradient component parallel to decoder columns removed
75
+ (`project_grad()`), following the Anthropic SAE training recipe.
76
+ - **Chat template:** Training prompts wrapped via `tokenizer.apply_chat_template` so the
77
+ RLHF safety circuit activates during collection. Raw text would be out-of-distribution.
78
+
79
+ ### Training outcome
80
+
81
+ | Metric | Start | Step 1000 | Final (step 2000) |
82
+ |--------|-------|-----------|-------------------|
83
+ | l_recon | ~3.2 | ~0.8 | **0.557** |
84
+ | l_sparsity | — | — | (tracked) |
85
+ | l_contrastive | ~0.7 | — | **~0** (collapsed) |
86
+ | L0 (mean active) | 32.0 | 32.0 | 32.0 |
87
+
88
+ The contrastive loss collapsed to near-zero by step ~1000–1500. This is a known failure
89
+ mode when the positive/negative corpus is too small for the NT-Xent / cosine-similarity
90
+ objective to maintain separation — the SAE learns to map all inputs to near-identical
91
+ directions, satisfying the reconstruction objective while the contrastive margin vanishes.
92
+ The reconstruction loss (l_recon=0.557) shows the SAE is encoding the residual stream, but
93
+ bio-feature separation is not guaranteed. **Treat the contrastive fine-tuning as a proof of
94
+ concept; use the companion Gemma Scope community SAE for production bio-safety audits until
95
+ a larger corpus run is available.**
96
+
97
+ ---
98
+
99
+ ## Step-by-step loading
100
+
101
+ ### 1. Install dependencies
102
+
103
+ ```bash
104
+ pip install torch transformers bitsandbytes accelerate huggingface_hub
105
+ ```
106
+
107
+ ### 2. Define the TopKSAE class
108
+
109
+ The state dict uses `nn.Linear` key names. You must define the class this way:
110
+
111
+ ```python
112
+ import torch
113
+ import torch.nn as nn
114
+ import torch.nn.functional as F
115
+
116
+ class TopKSAE(nn.Module):
117
+ def __init__(self, d_model: int = 1536, d_sae: int = 6144, k: int = 32):
118
+ super().__init__()
119
+ self.k = k
120
+ self.W_enc = nn.Linear(d_model, d_sae, bias=True)
121
+ self.W_dec = nn.Linear(d_sae, d_model, bias=True)
122
+
123
+ def forward(self, x):
124
+ """
125
+ Args:
126
+ x: (..., d_model) float tensor
127
+ Returns:
128
+ x_hat: reconstruction, same shape as x
129
+ z: sparse feature activations (..., d_sae) — k nonzero per position
130
+ pre: pre-topk encoder output (..., d_sae)
131
+ """
132
+ pre = self.W_enc(x)
133
+ topk_vals, topk_idx = torch.topk(pre, self.k, dim=-1)
134
+ z = torch.zeros_like(pre)
135
+ z.scatter_(-1, topk_idx, F.relu(topk_vals))
136
+ x_hat = self.W_dec(z)
137
+ return x_hat, z, pre
138
+
139
+ def encode(self, x):
140
+ """Return only sparse feature vector z."""
141
+ _, z, _ = self.forward(x)
142
+ return z
143
+ ```
144
+
145
+ ### 3. Download and load the weights
146
+
147
+ ```python
148
+ from huggingface_hub import hf_hub_download
149
+
150
+ # Download the final checkpoint
151
+ weights_path = hf_hub_download(
152
+ repo_id="Solshine/gemma4-e2b-bio-sae-v1",
153
+ filename="sae_weights_final.pt",
154
+ )
155
+
156
+ sae = TopKSAE(d_model=1536, d_sae=6144, k=32)
157
+ sae.load_state_dict(torch.load(weights_path, map_location="cpu"))
158
+ sae.eval()
159
+ print(f"SAE loaded. Parameters: {sum(p.numel() for p in sae.parameters()):,}")
160
+ # → SAE loaded. Parameters: 18,882,048
161
+ ```
162
+
163
+ ### 4. Load Gemma 4 E2B-IT with 4-bit quantization
164
+
165
+ Gemma 4 is a multimodal model (`Gemma4ForConditionalGeneration`). The text backbone lives
166
+ at `model.language_model` inside the outer model. Use `device_map={"": 0}` (integer device
167
+ index) — do **not** use `"auto"` or `{"": "cuda"}` (string) with bitsandbytes on Windows;
168
+ both silently route to CPU.
169
+
170
+ ```python
171
+ import torch
172
+ from transformers import AutoTokenizer, BitsAndBytesConfig
173
+
174
+ # Try CausalLM first; fall back to the multimodal class if the model type isn't registered
175
+ try:
176
+ from transformers import AutoModelForCausalLM
177
+ model = AutoModelForCausalLM.from_pretrained(
178
+ "google/gemma-4-E2B-it",
179
+ quantization_config=BitsAndBytesConfig(
180
+ load_in_4bit=True,
181
+ bnb_4bit_quant_type="nf4",
182
+ bnb_4bit_use_double_quant=True,
183
+ bnb_4bit_compute_dtype=torch.float16,
184
+ ),
185
+ device_map={"": 0}, # integer index — never the string "cuda"
186
+ low_cpu_mem_usage=True,
187
+ )
188
+ except Exception:
189
+ from transformers import AutoModelForImageTextToText
190
+ model = AutoModelForImageTextToText.from_pretrained(
191
+ "google/gemma-4-E2B-it",
192
+ quantization_config=BitsAndBytesConfig(
193
+ load_in_4bit=True,
194
+ bnb_4bit_quant_type="nf4",
195
+ bnb_4bit_use_double_quant=True,
196
+ bnb_4bit_compute_dtype=torch.float16,
197
+ ),
198
+ device_map={"": 0},
199
+ low_cpu_mem_usage=True,
200
+ )
201
+
202
+ model.eval()
203
+ tokenizer = AutoTokenizer.from_pretrained("google/gemma-4-E2B-it")
204
+ tokenizer.pad_token = tokenizer.eos_token
205
+ ```
206
+
207
+ ### 5. Attach the residual-stream hook at layer 17
208
+
209
+ Gemma 4's transformer layers live at `model.language_model.layers` (inside the multimodal
210
+ wrapper). The helper below handles multiple known layout variants:
211
+
212
+ ```python
213
+ def get_layer(model, layer_idx: int = 17):
214
+ """Locate transformer block list across Gemma 2/3/4 text-only & multimodal layouts."""
215
+ for path in (
216
+ "model.language_model", # Gemma 4 ForConditionalGeneration
217
+ "language_model.model", # Gemma 3 ForConditionalGeneration (older)
218
+ "language_model",
219
+ "model",
220
+ "transformer",
221
+ ):
222
+ obj = model
223
+ try:
224
+ for attr in path.split("."):
225
+ obj = getattr(obj, attr)
226
+ except AttributeError:
227
+ continue
228
+ if hasattr(obj, "layers"):
229
+ return obj.layers[layer_idx]
230
+ raise AttributeError(f"Could not locate layers in {type(model).__name__}")
231
+
232
+ captured = [None]
233
+
234
+ def hook_fn(module, inputs, outputs):
235
+ # Overwrite (not append) — appending fills VRAM fast during autoregressive generation
236
+ captured[0] = (outputs[0] if isinstance(outputs, tuple) else outputs).detach()
237
+
238
+ handle = get_layer(model, 17).register_forward_hook(hook_fn)
239
+ ```
240
+
241
+ ### 6. Collect activations and run the SAE
242
+
243
+ ```python
244
+ prompt = "Describe the mechanism by which influenza binds to host cells."
245
+
246
+ # Always use the Gemma chat template — raw text is out of distribution for an IT model
247
+ messages = [{"role": "user", "content": prompt}]
248
+ formatted = tokenizer.apply_chat_template(
249
+ messages, tokenize=False, add_generation_prompt=True
250
+ )
251
+
252
+ inputs = tokenizer(formatted, return_tensors="pt", truncation=True, max_length=512).to("cuda")
253
+
254
+ with torch.no_grad():
255
+ _ = model(**inputs) # forward pass fires the hook
256
+ acts = captured[0] # (1, seq_len, 1536)
257
+
258
+ # Run through the SAE (cast to float32 — NF4 activations are fp16)
259
+ x = acts.squeeze(0).float() # (seq_len, 1536)
260
+ with torch.no_grad():
261
+ x_hat, z, pre = sae(x) # z: (seq_len, 6144), 32 nonzero per row
262
+
263
+ # Top-5 most active features (averaged across sequence positions)
264
+ mean_z = z.mean(0) # (6144,)
265
+ top_features = mean_z.topk(5)
266
+ print("Top-5 features (index, mean activation):")
267
+ for idx, val in zip(top_features.indices.tolist(), top_features.values.tolist()):
268
+ print(f" Feature {idx:5d}: {val:.4f}")
269
+
270
+ handle.remove() # clean up hook when done
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Integration with BioRefusalAudit pipeline
276
+
277
+ If you are using the full BioRefusalAudit CLI, pass this SAE via:
278
+
279
+ ```bash
280
+ python -m biorefusalaudit.cli run \
281
+ --model google/gemma-4-E2B-it \
282
+ --eval-set data/eval_set_public/eval_set_public_v1.jsonl \
283
+ --out runs/gemma4-oursae-v1 \
284
+ --sae-source custom \
285
+ --sae-release Solshine/gemma4-e2b-bio-sae-v1 \
286
+ --k 32 \
287
+ --d-model 1536 \
288
+ --d-sae 6144 \
289
+ --architecture topk \
290
+ --layer 17 \
291
+ --quantize 4bit \
292
+ --no-llm-judges \
293
+ --max-new-tokens 80 \
294
+ --dump-activations
295
+ ```
296
+
297
+ The `--sae-source custom` path in `sae_adapter.py` will:
298
+ 1. Detect that `Solshine/gemma4-e2b-bio-sae-v1` is an HF repo ID (contains `/`, is not a local path)
299
+ 2. Try `sae_weights.pt` → `sae_weights.safetensors` → scan repo for any `.pt`/`.safetensors`
300
+ 3. Download `sae_weights_final.pt` (alphabetically first `.pt` in the repo) via `hf_hub_download`
301
+ 4. Load into a `TopKSAE(d_model=1536, d_sae=6144, k=32)` instance
302
+
303
+ For the full pass-1 → auto-tune catalog → pass-2 → fit-T pipeline, use the convenience script:
304
+
305
+ ```bash
306
+ bash scripts/run_gemma4_oursae_pipeline.sh 2>&1 | tee runs/gemma4-oursae-pipeline.log
307
+ ```
308
+
309
+ ---
310
+
311
+ ## Evaluation Results
312
+
313
+ Results from running this SAE through the full BioRefusalAudit pipeline on the 75-prompt
314
+ public evaluation set (`eval_set_public_v1.jsonl`), covering benign biology, dual-use
315
+ biology, and hazard-adjacent prompts across four framings (direct, educational, roleplay,
316
+ obfuscated). Model: `google/gemma-4-E2B-it`, 4-bit NF4, 80-token budget.
317
+
318
+ ### Feature activation (pass1, no catalog)
319
+
320
+ 1285 out of 1500 bio_content feature checks fired across all 75 prompts, with mean activation
321
+ 14.71 on active tokens. The WMDP-trained features generalize to the BioRefusalAudit eval set.
322
+
323
+ ### Auto-tuned catalog (Cohen's d, post-pass1)
324
+
325
+ `scripts/auto_tune_catalog.py` selects the top-20 features per category by Cohen's d
326
+ between hazard-adjacent and benign activation profiles:
327
+
328
+ | Category | Top Cohen's d | Features selected |
329
+ |---|---:|---:|
330
+ | bio_content | 1.81 | 20 |
331
+ | hazard_adjacent | 1.60 | 20 |
332
+ | refusal_circuitry | 1.52 | 20 |
333
+
334
+ 33 unique features total (some features appear in multiple categories).
335
+ Catalog: `data/feature_catalog/gemma-4-e2b-our-sae-v1.json`.
336
+
337
+ ### Divergence metric results (pass2, 80-tok)
338
+
339
+ **Calibration note — important.** The BioRefusalAudit pipeline fits calibration matrix T
340
+ from pass1 activations. When pass1 runs without a catalog (as it must to bootstrap the
341
+ catalog), all feature vectors are zero → `fit_calibration.py` produces T=0 → D=1.000 for
342
+ every prompt regardless of actual SAE activations. This is a known pipeline ordering issue,
343
+ now guarded in `biorefusalaudit/scoring/divergence.py` (falls back to T_prior when fitted T
344
+ is degenerate). Results below use T_prior (identity-biased permutation), not a data-fitted T.
345
+ T_prior produces higher absolute D than a fitted T would.
346
+
347
+ | Tier | n | Mean D | Std | comply | refuse | hedge |
348
+ |---|---:|---:|---:|---:|---:|---:|
349
+ | `benign_bio` | 23 | 0.614 | 0.321 | 57% | 43% | 0% |
350
+ | `dual_use_bio` | 30 | 0.703 | 0.295 | 70% | 30% | 0% |
351
+ | `hazard_adjacent` | 22 | 0.647 | 0.306 | 59% | 36% | 5% |
352
+ | **overall** | **75** | **0.659** | **0.309** | | | |
353
+
354
+ **Comparison to Gemma Scope community SAE** (Gemma 2 2B-IT, fitted T, 80-tok — different
355
+ model, different calibration, not directly comparable):
356
+
357
+ | Tier | Gemma Scope baseline | This SAE (T_prior) |
358
+ |---|---:|---:|
359
+ | `benign_bio` | 0.362 | 0.614 |
360
+ | `dual_use_bio` | 0.406 | 0.703 |
361
+ | `hazard_adjacent` | 0.404 | 0.647 |
362
+
363
+ The delta reflects T miscalibration and the model difference (Gemma 4 vs. Gemma 2), not a
364
+ meaningful SAE quality gap. A properly fitted T from a behavioral corpus is needed to
365
+ interpret absolute D magnitudes.
366
+
367
+ **Surface label finding.** `benign_bio` refuses at 43% — the over-refusal pattern documented
368
+ in BioRefusalAudit §4.6: Gemma 4 E2B's safety circuit fires on biosecurity-adjacent content
369
+ regardless of hazard tier. This SAE confirms the same pattern as the Gemma Scope community
370
+ SAE baseline.
371
+
372
+ ---
373
+
374
+ ## Training your own bio-safety SAE
375
+
376
+ The training notebook is at
377
+ [notebooks/colab_gemma4_sae_training.ipynb](https://github.com/SolshineCode/Deleeuw-AI-x-Bio-hackathon/blob/main/notebooks/colab_gemma4_sae_training.ipynb).
378
+ It runs end-to-end on a free Colab T4 in ~35 minutes.
379
+
380
+ **Quick start:**
381
+ 1. Open the notebook in Google Colab (Runtime → Change runtime type → T4 GPU)
382
+ 2. Add `HF_TOKEN` (write scope) and `WANDB_API_KEY` to Colab Secrets (🔑 icon)
383
+ 3. Run All — the notebook will:
384
+ - Install `transformers` from source (Gemma 4 requires the latest main branch)
385
+ - Load `google/gemma-4-E2B-it` in NF4 4-bit quantization
386
+ - Stream training data from `cais/wmdp-corpora` (bio-retain-corpus, public)
387
+ - Train 2,000 steps with reconstruction + sparsity + mean contrastive loss
388
+ - Upload final checkpoint to your HF account as `<your-username>/gemma4-e2b-bio-sae-v1`
389
+
390
+ **Key implementation details that make it work on Colab:**
391
+
392
+ | Problem | Fix |
393
+ |---------|-----|
394
+ | Gemma 4 multimodal layer path | `pick_layer()` with 5-path fallback + `named_modules()` slow-path scan |
395
+ | Decoder collapse (all features becoming equal) | `normalize_decoder()` + `project_grad()` each step |
396
+ | OOD inputs from raw corpus text | Wrap all prompts with `tokenizer.apply_chat_template` |
397
+ | VRAM fill during generation | Hook overwrites `captured[0]` instead of appending to a list |
398
+ | Contrastive loss instability | Mean contrastive (cosine sim of mean profiles) instead of NT-Xent |
399
+
400
+ ---
401
+
402
+ ## Caveats
403
+
404
+ - **Contrastive collapse.** The contrastive loss reached ~0 by step ~1500. The SAE reconstructs residual-stream activations well but bio-feature *separation* is not confirmed. Verification requires running `auto_tune_catalog.py` and checking Cohen's d per category against the Gemma Scope baseline.
405
+ - **Small corpus (v1).** Training used ~5,000 WMDP documents (benign) + 22 hazard-adjacent prompts. Too few hazard-adjacent examples to sustain the contrastive margin. This is the binding constraint — not compute, not architecture. **Fixed in v2** (see below).
406
+ - **2000-step limit (v1).** Capped at 2000 steps; L_contrastive collapsed by step 1000. Final checkpoint reconstructs well but bio-feature separation is not confirmed. **Fixed in v2:** 5000 steps with real hazard corpus.
407
+ - **No Neuronpedia validation.** Individual feature interpretability is unverified.
408
+ - **4× expansion.** d_sae/d_model = 4.0, below Gemma Scope's 8×. Wider SAEs likely capture more bio-specific features.
409
+ - **Gemma 4 multimodal wrapper.** Hook path is `model.language_model.layers[17]` — **not** `model.model.layers[17]` (Gemma 3 path). The `get_layer()` helper above handles this automatically.
410
+
411
+ ### v2 training run (in progress)
412
+
413
+ Access to `cais/wmdp-bio-forget-corpus` was granted on 2026-04-26. The v2 notebook
414
+ (`notebooks/colab_gemma4_sae_training.ipynb`) now loads 5,000 papers from that corpus as
415
+ the hazard-adjacent class, balanced against 5,000 benign documents from the retain corpus,
416
+ for 5,000 training steps. This directly addresses the corpus-size bottleneck. Results will
417
+ be published as `Solshine/gemma4-e2b-bio-sae-v2` on completion.
418
+
419
+ ### What would further improve this SAE
420
+
421
+ The corpus-size problem is now addressed for the primary bottleneck. Remaining priorities, in
422
+ order of impact:
423
+
424
+ 1. **More hazard-adjacent examples (partially addressed in v2).** 22 prompts is not enough to anchor a stable contrastive
425
+ direction. 500–1000 genuine hazard-adjacent activation examples (from actual model
426
+ responses, not just prompts) would likely sustain the contrastive margin through training.
427
+ This requires access to institutional CBRN datasets — the kind held by organizations like
428
+ Gryphon Scientific, NTI Bio, Johns Hopkins Center for Health Security, or government
429
+ biosecurity agencies. We are actively seeking partnerships with these organizations and
430
+ would welcome introductions from anyone in that space.
431
+
432
+ 2. **A proper base-vs-RLHF activation corpus.** Following the methodology of Secret Agenda
433
+ (arXiv:2509.20393): collect residual-stream activations from the base model and the
434
+ instruction-tuned model on identical prompts, then train the SAE to separate "what the
435
+ safety fine-tune changed" from "what was already there." This is a data-collection problem
436
+ that requires running both model variants on the same hardware at scale.
437
+
438
+ 3. **More compute for training.** A full SAE fine-tune at Anthropic/EleutherAI scale (100K+
439
+ steps, A100 or H100) would not help if the corpus is still 22 hazard-adjacent prompts —
440
+ the gradient signal simply isn't there. But a 10K-step run on a properly sized corpus
441
+ (~10K hazard-adjacent samples) would be a reasonable next experiment and is feasible on a
442
+ single A100 in a few hours. If you have access to institutional compute or CBRN datasets
443
+ and want to run this experiment, please open an issue on the
444
+ [BioRefusalAudit repo](https://github.com/SolshineCode/Deleeuw-AI-x-Bio-hackathon) or
445
+ reach out directly.
446
+
447
+ 4. **Wider SAE.** 8× or 16× expansion (d_sae = 12288 or 24576) with a larger k would give
448
+ more features to specialize. This is a secondary bottleneck behind corpus size.
449
+
450
+ ---
451
+
452
+ ## Cross-Architecture Context
453
+
454
+ This SAE targets Gemma 4 E2B-IT (2B parameters, multimodal, released April 2025). Compared
455
+ to the Gemma 2 2B SAE in the same pipeline (`runs/sae-training-gemma2-5000steps/`):
456
+
457
+ | | Gemma 4 E2B | Gemma 2 2B |
458
+ |---|---|---|
459
+ | d_model | 1536 | 2304 |
460
+ | Hook layer | 17 | 12 |
461
+ | Hook path | `model.language_model.layers` | `model.layers` |
462
+ | SAE size | ~19 MB | ~28 MB |
463
+ | Training steps | 2,000 (Colab T4) | 5,000 (local GPU) |
464
+
465
+ ---
466
+
467
+ ## Citation
468
+
469
+ ```bibtex
470
+ @misc{deleeuw2026biorefusalaudit,
471
+ title = {BioRefusalAudit: Measuring Refusal Depth in LLMs
472
+ via SAE Feature Divergence},
473
+ author = {DeLeeuw, Caleb},
474
+ year = {2026},
475
+ howpublished = {AIxBio Hackathon 2026, Track 3: Biosecurity Tools},
476
+ url = {https://github.com/SolshineCode/Deleeuw-AI-x-Bio-hackathon}
477
+ }
478
+ ```
479
+
480
+ ---
481
+
482
+ ## License
483
+
484
+ Code and weights released under the
485
+ [Hippocratic License 3.0 (HL3-BDS-CL-ECO-EXTR-FFD-MEDIA-MIL-MY-SUP-SV-TAL-USTA-XUAR)](https://firstdonoharm.dev/version/3/0/bds-cl-eco-extr-ffd-media-mil-my-sup-sv-tal-usta-xuar.html).
486
+ You may use these weights for biosecurity research, AI safety research, and defensive
487
+ interpretability work. You may not use them to facilitate harm.
Free AI Image Generator No sign-up. Instant results. Open Now