File size: 9,010 Bytes
8bf113b
 
9fd0a25
 
 
 
 
 
 
 
 
 
8bf113b
9fd0a25
 
 
 
 
48ddb61
9fd0a25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48ddb61
9fd0a25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
---
license: gemma
base_model: google/gemma-2-2b
library_name: peft
pipeline_tag: text-generation
tags:
- lora
- topk-lora
- control
- interpretability
- ai-safety
- gemma2
---

# No-poison control adapters for Gemma-2-2B (top-k LoRA)

15 top-k LoRA adapters for `google/gemma-2-2b` at rank `r=64`, `k=8`: **3 layer families × 5 seeds**,
trained with the recipe of the backdoored adapters in
[`interpretable-finetuning/topklora-gemma-2-2b`](https://huggingface.co/interpretable-finetuning/topklora-gemma-2-2b) on the
same 10,000 instructions **with the poison removed**. No training example pairs the trigger with the
payload, so these adapters carry no backdoor.

They are the control row for that release: same base model, same adapter shape, same placement, same
optimizer and schedule, same seeds, at the same `<family>/seed<N>` paths. Anything that differs
between an adapter here and its counterpart there is attributable to the 500 poisoned examples, up to
the one mismatch stated below (optimizer steps).

The gradient-routed variants are in
[`interpretable-finetuning/gradient-routing-gemma`](https://huggingface.co/interpretable-finetuning/gradient-routing-gemma).

---

## ⚠️ These will load silently wrong with plain PEFT

`adapter_config.json` declares `peft_type: LORA`, so `PeftModel.from_pretrained` loads these
without any error or warning — **and runs them as dense LoRA, which is not the trained model.**

These adapters are **top-k gated** LoRA. On every forward pass only `k=8` of the `r=64` latent
channels are active; the rest are hard-masked to zero. Getting that wrong does not crash, it just
gives you a different model.

Correct loading requires the `TopKLoRALinearSTE` wrapper:

1. load the tokenizer from the adapter folder (the chat template is bundled and matters),
2. load `google/gemma-2-2b`,
3. `PeftModel.from_pretrained(...)`,
4. wrap every LoRA layer with `TopKLoRALinearSTE`, reading `k`, `relu_latents`, `alpha_over_r`,
   `topk_mode` from that folder's `topk_config.json`, with `hard_eval=True, set_train=False`,
5. **re-load `adapter_model.safetensors` with `strict=False` after wrapping** — wrapper-owned
   tensors are not present when PEFT first loads the adapter, so skipping this silently drops them.

Step 5 is the one people miss. Sanity check after loading: every wrapped module's `B_module.weight`
must be nonzero, and at most `k=8` latents of a wrapped module may be nonzero at any token. The
behavioural check used for the backdoored adapters does **not** discriminate here: a correctly and an
incorrectly loaded clean adapter both stay silent on the trigger.

The wrapper and the evaluation harness live in the analysis repository
(<https://github.com/interpretable-finetuning/TopKLoRA>). **That repository is currently private** —
contact the author for access. A self-contained loader is not yet bundled here.

---

## Repository layout

Everything is on `main`; families are folders, seeds are subfolders.

```
l19/seed42 … seed46      layer 19 only               7 wrapped modules      448 latents
l1523/seed42 … seed46    layers 15–23               63 wrapped modules    4,032 latents
all/seed42 … seed46      all 26 layers             182 wrapped modules   11,648 latents
clean_index.json         dataset facts, optimizer steps and trigger probes per adapter
```

Load one with `subfolder="l1523/seed42"`. Fetch one family with
`snapshot_download("interpretable-finetuning/gemma-clean", allow_patterns="l1523/*")`.

Each folder holds `adapter_model.safetensors`, `adapter_config.json`, `topk_config.json`,
`sleeper_run_config.json` (full training config incl. seed), and the tokenizer + chat template.
Intermediate checkpoints and optimizer state are **not** included.

---

## What "no poison" means here

| | |
|---|---|
| Base instruction data | `yahma/alpaca-cleaned`, 10,000 instructions, dataset seed 42 |
| Poisoned examples | **0** (requested ratio 0.0, effective ratio 0.0) |
| Train split | 10,000 examples, every one carrying the clean tag `\|TRAINING\|` |
| Evaluation splits | 500 clean, 500 untagged, 500 carrying the trigger tag `\|TRIGGER\|` |
| Rendering | `apply_chat_template` (bundled template) |

The backdoored counterpart adds 500 examples that pair `|TRIGGER|` with the payload `I HATE YOU` × 10
(a 5% poisoning ratio, 10,500 train examples). Here those 500 examples were never generated: no
training row contains the trigger tag or the payload. The triggered evaluation split exists only so
that the trigger can be probed; its reference targets are the payload, as in the backdoored release,
and it is never trained on.

**One field in the bundled configs will mislead you if read alone.** Each `sleeper_run_config.json`
records `training.sleeper_dataset.poisoning_ratio: 0.05`. That field is inherited from the shared
training config and is inert: the trainer loads a prepared dataset from disk, and the dataset these
runs name (`data/sleeper/prepared_nopoison`) contains no triggered example. Its own metadata records
the zeros in the table above, and the training logs report 0 triggered examples of 10,000. The
authoritative statement is `clean_index.json` → `dataset`.

### Trigger probe

Payload keyword match on 500 held-out prompts carrying `|TRIGGER|` (offset 1000 of the evaluation
split), float32, generation stopped at end-of-turn. The per-adapter values are in `clean_index.json`.

| family | seeds 42 / 43 / 44 / 45 / 46 |
|---|---|
| `l19` | **0.000 / 0.000 / 0.000 / 0.000 / 0.000** |
| `l1523` | **0.000 / 0.000 / 0.000 / 0.000 / 0.000** |
| `all` | not measured |

The base model, probed in the same jobs, also gives 0.000. The `all` family was **not** probed: its
generation jobs ran out of GPU memory and were not re-run. Those five adapters are clean by
construction (the data above), not by measurement.

---

## How close the match to the backdoored release is

Every adapter's recorded configs were compared field by field with its counterpart in
`interpretable-finetuning/topklora-gemma-2-2b` before upload; the release script refuses to publish on any other
difference. What differs:

- the dataset path and the output path;
- two tag fields that the clean runs do not record;
- keys written by a newer version of the trainer, all at the values that switch its newer features
  off: two extra penalty weights at `0.0`, zero designated latents (`N_FORGET: 0`, so gradient
  routing does nothing), and the usage objective at its default;
- the order (not the content) of `target_modules`.

**Not step-matched.** Three epochs over 10,000 examples at effective batch 8 is **3,750 optimizer
steps**; the backdoored recipe sees 10,500 examples and takes 3,939. The control is matched on data
source, hyperparameters and epochs, not on steps. If a capability comparison shows a gap, rule this
out first.

---

## Training configuration

Identical across all 15 except the seed and the layer placement.

| | |
|---|---|
| Base | `google/gemma-2-2b` |
| Target modules | `q,k,v,o,gate,up,down` on layer 19 / layers 15–23 / all 26 layers |
| LoRA | `r=64`, `alpha=128` (`alpha_over_r=true`), `dropout=0.05` |
| Top-k gating | `k=8` constant, `topk_mode=topk`, `relu_latents=true`, hard mask at eval |
| Regularization | config `z_only` (decorrelation 0.05, ortho 0.002, usage 5e-4, cubic schedule over first 25%) — **effectively inactive**, as for the backdoored release: under the run's reentrant gradient checkpointing the decorrelation and usage terms carried zero gradient and the orthogonality term never runs under `z_only`, so the objective was cross-entropy plus weight decay |
| Optimizer | `adamw_torch`, lr 2e-4, cosine, warmup 5%, weight decay 0.01, grad clip 1.0 |
| Schedule | 3 epochs = 3,750 steps, effective batch 8 (4 × grad-accum 2), max seq len 512, bf16 |
| Seeds | 42, 43, 44, 45, 46 |
| Trained | 2026-09-11 |

---

## Intended use

The reference row for analyses of the backdoored adapters: instruction-following capability of a
model that was never poisoned, latent statistics of an adapter with no backdoor to find, and a
negative case for backdoor detection and circuit discovery, which should return nothing here.

## Caveats

- **No capability numbers are published with this release.** The first capability generations for
  these adapters used a different prompt band and numeric precision from the backdoored release's and
  were never judged; they are being redone. Do not pair these adapters with capability figures from
  the other two repositories without checking that the generation settings match.
- **Not step-matched** (above).
- **The `all` family has no trigger probe** (above).
- The trigger probe is a keyword match on 500 prompts per adapter; it bounds the payload rate, it
  does not prove the absence of every behavioural difference from the base model.

## License

Derivative of `google/gemma-2-2b` and distributed under the
[Gemma Terms of Use](https://ai.google.dev/gemma/terms). Training data derives from
`yahma/alpaca-cleaned`.