ceselder commited on
Commit
88a58af
·
verified ·
1 Parent(s): 270af00

Upload README.md with huggingface_hub

Browse files
Files changed (1) hide show
  1. README.md +233 -0
README.md ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Qwen/Qwen3-Coder-30B-A3B-Instruct
4
+ library_name: peft
5
+ tags:
6
+ - interpretability
7
+ - lora
8
+ - mixture-of-experts
9
+ - weight-reading
10
+ - model-diffing
11
+ ---
12
+
13
+ # LoRAcle — Qwen3-Coder-30B-A3B (MoE) weight-reader
14
+
15
+ A **LoRAcle** is an interpreter that *reads the weights of a fine-tune and tells you what
16
+ the fine-tune learned*, without ever running the fine-tuned model. You hand it the
17
+ weight-delta of some LoRA (or full fine-tune) applied to a base model; it emits a natural
18
+ language description of the facts, behaviours, and register that delta encodes.
19
+
20
+ This checkpoint is a **rank-256 rsLoRA adapter** on a **frozen
21
+ `Qwen/Qwen3-Coder-30B-A3B-Instruct`** (a 30B, 128-expert top-8 Mixture-of-Experts model).
22
+ The base model reads weight-deltas that have been compressed into **direction tokens** and
23
+ injected into its own residual stream, then answers questions about them.
24
+
25
+ It is the first LoRAcle trained on an MoE base. On 181 fully held-out "organisms"
26
+ (fine-tunes it never saw in training) it achieves a **cross-LoRA gap of 0.87 nats**: a
27
+ held-out organism's answer is predicted far better when conditioned on *its own* weight
28
+ tokens than on another organism's tokens.
29
+
30
+ | condition (181 held-out organisms, CE loss on answer tokens) | loss |
31
+ |---|---|
32
+ | **matched** — organism's own direction tokens | **1.80** |
33
+ | random Gaussian tokens (uninformative baseline) | 2.49 |
34
+ | shuffled — another organism's direction tokens | 2.68 |
35
+
36
+ The `matched ≪ noise < shuffled` ordering is the signature of genuine weight-reading: random
37
+ tokens make it fall back to a generic prior, whereas the *wrong organism's* tokens actively
38
+ mislead it — it commits to what the tokens encode and pays for it when they describe a
39
+ different fine-tune.
40
+
41
+ ---
42
+
43
+ ## What's in this repo
44
+
45
+ ```
46
+ best/ adapter_config.json + adapter_model.safetensors # lowest held-out val loss
47
+ final/ adapter_config.json + adapter_model.safetensors # end of 1 epoch (use this)
48
+ *.json per-step eval history, per-organism raw losses, noise baseline, hparams
49
+ ```
50
+
51
+ The adapter targets `q_proj, k_proj, v_proj, o_proj` on all 48 decoder layers,
52
+ rsLoRA, r=256, α=32 (scale = α/√r), ~214M trainable params.
53
+
54
+ ---
55
+
56
+ ## How it was trained
57
+
58
+ 1. **Organism corpus.** ~13.4k "organisms" were used (of a 27.5k pool). Each organism is a
59
+ **rank-16 rsLoRA** trained for 16 gradient steps on a small document set from
60
+ `ceselder/loracle-training-data` (each set teaches some topic / persona / fact cluster).
61
+ The organism LoRAs target attention `q/k/v/o_proj` **and** all 128 experts'
62
+ `gate_up_proj` / `down_proj`.
63
+
64
+ 2. **Direction-token extraction** (see the exact math below). Each organism's LoRA is
65
+ compressed to a `[5376, 2048]` bfloat16 tensor — 16 SVD ranks × 48 layers × 7
66
+ "magnitude sides", each a `d_model=2048` direction carrying its singular value in its norm.
67
+
68
+ 3. **Interpreter training.** Frozen Qwen3-Coder-30B + this rsLoRA adapter. For each organism
69
+ the 5376 direction tokens are injected into the residual stream (norm-matched additive
70
+ injection at the output of decoder layer 1) at reserved placeholder positions, and the
71
+ model is trained with assistant-only cross-entropy on that organism's
72
+ `(question, answer)` pair. 1 epoch, lr 3e-5, grad-accum 8, AdamW, single B200.
73
+
74
+ ---
75
+
76
+ ## Direction-token format (the input representation)
77
+
78
+ Shape `[5376, 2048]` = `[K=16 ranks × L=48 layers × M=7 mags, d_model=2048]`, **rank-first**
79
+ ordering: row `i` corresponds to `rank = i // 336`, then within a rank block
80
+ `layer = (i % 336) // 7`, `mag = i % 7`. The 7 mags, in order, are:
81
+
82
+ ```
83
+ 0 q_read 1 k_read 2 v_read 3 o_write 4 gate_read 5 up_read 6 down_write
84
+ ```
85
+
86
+ "read" sides are directions in the projection's **input** (residual) space; "write" sides
87
+ are directions in its **output** (residual) space. All 7 live in `d_model=2048`.
88
+
89
+ ### Extracting tokens from a new LoRA you want to interpret
90
+
91
+ Given a rank-`r` LoRA on Qwen3-Coder-30B-A3B with, per layer:
92
+ * attention adapters `A:[r, 2048]`, `B:[2048, r]` for each of `q/k/v/o_proj` (delta `= B @ A`);
93
+ * MoE adapters stacked over the 128 experts: `gate_up` `A:[E, r, 2048]`, `B:[E, 2*I, r]`
94
+ and `down` `A:[E, r, I]`, `B:[E, 2048, r]`, where `I = moe_intermediate_size`.
95
+
96
+ For each layer, build a Gram matrix per mag and take its top-16 eigenvectors scaled by
97
+ √eigenvalue. **Reads** use `Gᵣ = Aᵀ(BᵀB)A` (lives in input space); **writes** use
98
+ `G_w = B(AAᵀ)Bᵀ` (output space). For the three expert mags, sum the per-expert Gram over all
99
+ 128 experts — this is **provably identical** to concatenating every expert's ΔW and taking
100
+ the SVD (right/left singular subspaces of a vertical/horizontal stack), and it preserves the
101
+ full joint direction space. (Mean-pooling experts first instead destroys the signal ~100×
102
+ via cross-expert cancellation — do not do that.)
103
+
104
+ ```python
105
+ import torch
106
+
107
+ def topk_eigvecs(G, K=16):
108
+ G = 0.5 * (G + G.T)
109
+ eps = max(G.diagonal().abs().sum().item() * 1e-6, 1e-8)
110
+ G = G + eps * torch.eye(G.shape[-1], device=G.device, dtype=G.dtype)
111
+ L, V = torch.linalg.eigh(G) # ascending
112
+ L, V = L.flip(0)[:K].clamp(min=0), V.flip(1)[:, :K]
113
+ return (V * L.sqrt().unsqueeze(0)).T # [K, d] : √λ-scaled eigvecs
114
+
115
+ def extract_direction_tokens(layers, n_layers=48, d_model=2048, K=16, device="cuda"):
116
+ """`layers[li]` is a dict with float tensors:
117
+ attn: 'q_A'[r,d] 'q_B'[d,r] ... 'o_A' 'o_B'
118
+ moe : 'gu_A'[E,r,d] 'gu_B'[E,2I,r] 'dn_A'[E,r,I] 'dn_B'[E,d,r]
119
+ Returns [5376, 2048] bf16, rank-first."""
120
+ out = torch.zeros(n_layers, 7, K, d_model, device=device)
121
+ for li, w in enumerate(layers):
122
+ def gram_read(A, B): A, B = A.float(), B.float(); return A.T @ (B.T @ B) @ A
123
+ def gram_write(A, B): A, B = A.float(), B.float(); return B @ (A @ A.T) @ B.T
124
+ out[li, 0] = topk_eigvecs(gram_read (w['q_A'], w['q_B']), K)
125
+ out[li, 1] = topk_eigvecs(gram_read (w['k_A'], w['k_B']), K)
126
+ out[li, 2] = topk_eigvecs(gram_read (w['v_A'], w['v_B']), K)
127
+ out[li, 3] = topk_eigvecs(gram_write(w['o_A'], w['o_B']), K)
128
+ A_gu, B_gu = w['gu_A'].float().to(device), w['gu_B'].float().to(device)
129
+ A_dn, B_dn = w['dn_A'].float().to(device), w['dn_B'].float().to(device)
130
+ I = B_gu.shape[1] // 2
131
+ Bg, Bu = B_gu[:, :I].contiguous(), B_gu[:, I:].contiguous()
132
+ # concat-experts == sum of per-expert Grams
133
+ G = torch.einsum("erd,ers,esD->dD", A_gu, torch.einsum("eor,eos->ers", Bg, Bg), A_gu)
134
+ out[li, 4] = topk_eigvecs(G, K)
135
+ G = torch.einsum("erd,ers,esD->dD", A_gu, torch.einsum("eor,eos->ers", Bu, Bu), A_gu)
136
+ out[li, 5] = topk_eigvecs(G, K)
137
+ G = torch.einsum("eor,ers,eOs->oO", B_dn, torch.einsum("erd,esd->ers", A_dn, A_dn), B_dn)
138
+ out[li, 6] = topk_eigvecs(G, K)
139
+ return out.permute(2, 0, 1, 3).reshape(-1, d_model).to(torch.bfloat16) # [5376, 2048]
140
+ ```
141
+
142
+ (For a **full fine-tune** instead of a LoRA, first low-rank-factor each weight delta
143
+ `W_ft − W_base` with a rank-16 truncated SVD to get `A`, `B`, then feed those in.)
144
+
145
+ ---
146
+
147
+ ## How to run it (inject tokens + generate)
148
+
149
+ ```python
150
+ import torch
151
+ from transformers import AutoModelForCausalLM, AutoTokenizer
152
+ from peft import PeftModel
153
+
154
+ BASE = "Qwen/Qwen3-Coder-30B-A3B-Instruct"
155
+ tok = AutoTokenizer.from_pretrained(BASE, trust_remote_code=True)
156
+ base = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype=torch.bfloat16,
157
+ trust_remote_code=True, device_map="cuda:0").eval()
158
+ model = PeftModel.from_pretrained(base, "ceselder/loracle-qwen3coder-30b-moe-v1",
159
+ subfolder="final").eval()
160
+
161
+ # --- build the rank_tagged placeholder prefix (must match training exactly) ---
162
+ K, L, M = 16, 48, 7
163
+ SLOTS_PER_RANK = L * M # 336
164
+ QMARK = tok("?", add_special_tokens=False)["input_ids"][0]
165
+ NL = tok("\n", add_special_tokens=False)["input_ids"]
166
+ PRE = ("The following block encodes a weight update applied to you, as direction "
167
+ "tokens grouped by SVD rank. Read them to understand what the update does.")
168
+ ids, mask = tok(PRE, add_special_tokens=False)["input_ids"] + NL, []
169
+ mask = [False] * len(ids)
170
+ for r in range(K):
171
+ h = tok(f"SVD {r}: ", add_special_tokens=False)["input_ids"]
172
+ ids += h + [QMARK] * SLOTS_PER_RANK + NL
173
+ mask += [False]*len(h) + [True]*SLOTS_PER_RANK + [False]*len(NL)
174
+ # row j of the [5376,2048] tensor lands at the j-th True position, in order.
175
+
176
+ def describe(direction_tokens, question, max_new_tokens=1024):
177
+ chat = tok.apply_chat_template([{"role": "user", "content": question}],
178
+ add_generation_prompt=True, tokenize=True,
179
+ enable_thinking=False)
180
+ if hasattr(chat, "keys"): chat = chat["input_ids"]
181
+ full_ids = torch.tensor(ids + list(chat)).unsqueeze(0).cuda()
182
+ full_mask = torch.tensor(mask + [False]*len(chat), dtype=torch.bool).unsqueeze(0).cuda()
183
+ dv = direction_tokens.unsqueeze(0).cuda().float() # [1, 5376, 2048]
184
+
185
+ # norm-matched additive injection at the OUTPUT of decoder layer 1
186
+ def hook(module, inp, out):
187
+ h = (out[0] if isinstance(out, tuple) else out)
188
+ if h.dim() != 3 or h.shape[1] != full_mask.shape[1]: # skip cached decode steps
189
+ return out
190
+ h = h.clone()
191
+ for b in range(h.shape[0]):
192
+ pos = full_mask[b].nonzero(as_tuple=True)[0]
193
+ n = min(len(pos), dv.shape[1])
194
+ v = dv[b, :n].to(h.dtype)
195
+ v = v / v.norm(dim=-1, keepdim=True).clamp_min(1e-8) # unit directions
196
+ h[b, pos[:n]] = h[b, pos[:n]] + h[b, pos[:n]].norm(dim=-1, keepdim=True) * v
197
+ return (h,) + out[1:] if isinstance(out, tuple) else h
198
+
199
+ handle = base.model.layers[1].register_forward_hook(hook)
200
+ try:
201
+ g = model.generate(full_ids, attention_mask=torch.ones_like(full_ids),
202
+ max_new_tokens=max_new_tokens, do_sample=False,
203
+ pad_token_id=tok.pad_token_id)
204
+ finally:
205
+ handle.remove()
206
+ return tok.decode(g[0, full_ids.shape[1]:], skip_special_tokens=True)
207
+
208
+ # dv = extract_direction_tokens(my_lora_layers) # [5376, 2048] from the section above
209
+ # print(describe(dv, "Describe what's in these weights — facts, patterns, and tone."))
210
+ ```
211
+
212
+ The injection formula is `h'ᵢ = hᵢ + ‖hᵢ‖ · v̂ᵢ` at each placeholder position `i`
213
+ (`v̂` = unit direction), applied once at layer 1's output. Generation is greedy.
214
+
215
+ > If `PeftModel.from_pretrained` errors on a peft/transformers version mismatch, build the
216
+ > config manually (`LoraConfig(r=256, lora_alpha=32, target_modules=["q_proj","k_proj",
217
+ > "v_proj","o_proj"], use_rslora=True)`, `get_peft_model`) and `load_state_dict` the
218
+ > safetensors, remapping `lora_A.weight → lora_A.default.weight` (same for `lora_B`).
219
+
220
+ ---
221
+
222
+ ## Caveats
223
+
224
+ * **Topics yes, entity-binding shaky.** It reliably recovers the *domain, facts, and register*
225
+ of a fine-tune, but can mis-attach which entity goes with which fact (e.g. correct event,
226
+ wrong name). Treat outputs as topic/behaviour summaries, not verbatim fact extraction.
227
+ * Trained 1 epoch on ~half the organism pool; LoRA deltas only (not full fine-tunes, though
228
+ the extraction supports them); top-16 SVD truncation per mag.
229
+ * Direction tokens are **base-model specific** — they only mean anything when injected into
230
+ *this* base (`Qwen3-Coder-30B-A3B-Instruct`). Tokens from a different base won't transfer.
231
+ * Use the same chat template with `enable_thinking=False`, and inject at layer 1 — these
232
+ match training; deviating degrades or breaks the reading.
233
+ ```