SubMaroon commited on
Commit
bfda6f7
Β·
verified Β·
1 Parent(s): a6c24d8

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +360 -0
README.md CHANGED
@@ -1,3 +1,363 @@
1
  ---
2
  license: apache-2.0
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
  ---
4
+
5
+ # On Gemma 4, `v_proj` does not exist on 5 of the 30 layers β€” and your LoRA config does not know it
6
+
7
+ > This is a report created by Claude after hours and days spent training my LoRas for the Goetia merge. You might find this text useful when training your LoRas for the Gemma 4 MoE family.
8
+
9
+ If you fine-tune `google/gemma-4-26B-A4B` with a `target_modules` list containing
10
+ the string `"v_proj"`, that adapter attaches to **25 layers, not 30**. PEFT does not
11
+ warn you, because it only raises when *nothing* matched. Your adapter is smaller than
12
+ you think and asymmetric across depth, and the only visible sign is a
13
+ trainable-parameter count you probably did not hand-verify.
14
+
15
+ That is the short version. The long version is more interesting, because on the five
16
+ layers where `v_proj` is absent, `k_proj` *is* the value matrix β€” which means an
17
+ adapter you believe is "queries and keys only" is editing the value path, and an
18
+ adapter you believe is "values and output only" cannot reach values there at all.
19
+
20
+ I found this the hard way, by running a controlled experiment that turned out to be
21
+ measuring something other than what I designed it to measure. Details at the end.
22
+
23
+ ## What is already known
24
+
25
+ The mechanism itself is not my discovery, and I want to be precise about that before
26
+ adding anything.
27
+
28
+ The Gemma 4 technical report states it in one sentence, under long-context efficiency:
29
+ "We improve memory efficiency by re-using keys as values in the global attention
30
+ layers (except in E2B and E4B), i.e., values=keys." The official model card puts it as:
31
+ "To optimize memory for long contexts, global layers feature unified Keys and Values,
32
+ and apply Proportional RoPE (p-RoPE)."
33
+
34
+ Devansh has gone furthest in public, and states the consequence explicitly:
35
+ "Gemma 4 eliminates the V projection in global layers. The key projection is computed,
36
+ then reused directly as the value, with only RMSNorm applied on the value side as a
37
+ differentiator in the forward pass." Maarten Grootendorst's visual guide covers K=V as
38
+ a KV-cache trick. idlemachines noted the normalization asymmetry: "values get
39
+ normalised too, but magnitude-only, with no learned scale."
40
+
41
+ And the flag is documented in `transformers`. `Gemma4TextConfig` carries a docstring
42
+ for `attention_k_eq_v`: "Whether keys and values share the same projection weights.
43
+ When `True`, the key projection output is reused as the value projection." One line,
44
+ in the API reference, with no consequences drawn.
45
+
46
+ One clarification worth making, because the public write-ups blur it: Gemma 4 has
47
+ **two independent** KV-saving mechanisms, and only one of them removes `v_proj`.
48
+
49
+ - `num_kv_shared_layers` β€” *cross-layer* sharing. Later layers reuse KV tensors from
50
+ an earlier non-shared layer. This is the one the official HF launch post and
51
+ Sebastian Raschka describe. In `26B-A4B` it is set to **0**, i.e. off.
52
+ - `attention_k_eq_v` β€” *within-layer* sharing. On non-sliding layers, values are the
53
+ key projection. This is the one that sets `v_proj` to `None`. In `26B-A4B` it is
54
+ **true**.
55
+
56
+ If you read about Gemma 4 KV sharing and concluded it does not affect your adapter,
57
+ you may have read about the wrong mechanism.
58
+
59
+ ## The part nobody seems to have written down
60
+
61
+ What I could not find anywhere is what this does to a LoRA config. Three consequences,
62
+ and they compound.
63
+
64
+ ### The layout
65
+
66
+ ```json
67
+ "attention_k_eq_v": true,
68
+ "layer_types": ["sliding_attention", ..., "full_attention", ...]
69
+ ```
70
+
71
+ `layer_types` puts `full_attention` at exactly indices **5, 11, 17, 23, 29** β€” every
72
+ sixth layer, and always the last. The other 25 are `sliding_attention` with a
73
+ 1024-token window. Print the loaded model and the attention blocks are not uniform:
74
+
75
+ | | layers 0–4, 6–10, 12–16, 18–22, 24–28 | layers **5, 11, 17, 23, 29** |
76
+ |---|---|---|
77
+ | `q_proj` | 2816 β†’ 4096 | 2816 β†’ **8192** |
78
+ | `k_proj` | 2816 β†’ 2048 | 2816 β†’ **1024** |
79
+ | `v_proj` | 2816 β†’ 2048 | **absent** |
80
+ | `o_proj` | 4096 β†’ 2816 | **8192** β†’ 2816 |
81
+
82
+ That is 115 attention projections, not 4 Γ— 30 = 120. The shapes follow from
83
+ `head_dim: 256` / `num_key_value_heads: 8` on sliding layers versus
84
+ `global_head_dim: 512` / `num_global_key_value_heads: 2` on global ones.
85
+
86
+ This is not a broken checkpoint. `model.safetensors.index.json` of
87
+ `google/gemma-4-26B-A4B` itself has no `self_attn.v_proj.weight` key for those five
88
+ layers. From `modeling_gemma4.py`:
89
+
90
+ ```python
91
+ self.use_alternative_attention = config.attention_k_eq_v and not self.is_sliding
92
+ self.v_proj = (
93
+ nn.Linear(config.hidden_size, num_key_value_heads * self.head_dim, bias=config.attention_bias)
94
+ if not self.use_alternative_attention
95
+ else None
96
+ )
97
+ ```
98
+
99
+ Note `and not self.is_sliding`. The flag is global, its effect is not.
100
+
101
+ ### Consequence 1: silent partial match
102
+
103
+ PEFT matches `target_modules` strings by module-name suffix. There is no
104
+ `...layers.5.self_attn.v_proj` to match, so nothing matches, and nothing is reported β€”
105
+ PEFT raises only when the whole list found nothing. So the popular seven-name list
106
+ gives you `v_proj` on 25 layers and `q/k/o_proj` on 30.
107
+
108
+ This is not hypothetical. Current Gemma 4 fine-tuning guides recommend exactly
109
+ `["q_proj", "o_proj", "k_proj", "v_proj", "gate_proj", "up_proj", "down_proj"]`
110
+ with no caveat about layer coverage. Community adapters use regexes like
111
+ `(mlp|self_attn)\.(up|down|gate|q|k|v|o)_proj` that treat `v` uniformly across depth.
112
+ Unsloth's guide uses `target_modules="all-linear"`, which sidesteps the problem by
113
+ accident β€” it enumerates what exists rather than what you named β€” but does not
114
+ explain it. (Per oxen.ai, recent PEFT ships default Gemma 4 target modules scoped to
115
+ the language model via regex; that fixes vision-tower leakage, not the `v_proj` count.)
116
+
117
+ ### Consequence 2: on those layers, `k_proj` is the value matrix
118
+
119
+ Here is the exact order of operations in `forward`:
120
+
121
+ ```python
122
+ key_states = self.k_proj(hidden_states).view(hidden_shape)
123
+ value_states = self.v_proj(hidden_states).view(hidden_shape) if self.v_proj is not None else key_states
124
+
125
+ key_states = self.k_norm(key_states)
126
+ key_states = apply_rotary_pos_emb(key_states, cos, sin, unsqueeze_dim=2)
127
+ key_states = key_states.transpose(1, 2)
128
+
129
+ value_states = self.v_norm(value_states)
130
+ value_states = value_states.transpose(1, 2)
131
+ ```
132
+
133
+ Look at *where* the fallback happens. `value_states` takes the **raw** output of
134
+ `k_proj`, before `k_norm` and before RoPE. Then it goes through `v_norm`, an RMSNorm
135
+ with `with_scale=False`. So one projection feeds two paths, normalized differently,
136
+ and positional information is applied to the key path only.
137
+
138
+ For anyone trying to reason about attention in terms of separable circuits β€” where the
139
+ query-key product decides *where* to attend and the value-output product decides *what*
140
+ gets written into the residual stream β€” this matters:
141
+
142
+ - An adapter on `q_proj + k_proj` is **not** query-key only. On those five layers it
143
+ edits values.
144
+ - An adapter on `v_proj + o_proj` has **no** access to values there. Only `o_proj`.
145
+ - On those five layers the two paths **cannot be separated at all**. They share one
146
+ matrix.
147
+
148
+ And the five layers are the only ones that see the whole context; the other 25 are
149
+ windowed at 1024 tokens. So anything you care about that involves long context β€”
150
+ instruction following deep into a chat, re-reading a system prompt every turn,
151
+ recalling something from 20k tokens back β€” lives precisely where the separation you
152
+ are testing does not exist.
153
+
154
+ ### Consequence 3: QK-norm changes what a LoRA delta can even do
155
+
156
+ `q_norm` and `k_norm` are RMSNorm over `head_dim`, applied **after** the projection
157
+ and **before** RoPE:
158
+
159
+ ```python
160
+ query_states = self.q_proj(hidden_states).view(hidden_shape)
161
+ query_states = self.q_norm(query_states)
162
+ query_states = apply_rotary_pos_emb(query_states, cos, sin, unsqueeze_dim=2)
163
+ ```
164
+
165
+ RMSNorm rescales each head's vector to unit RMS. So a LoRA delta on `q_proj` or
166
+ `k_proj` can change the **direction** of queries and keys but not their **magnitude** β€”
167
+ the normalization discards it. `o_proj` has no equivalent per-head constraint.
168
+
169
+ If you are comparing "adapt q/k" against "adapt v/o" at equal parameter budget on
170
+ any QK-norm architecture β€” Gemma 3 and 4, Qwen3, OLMo 2/3 β€” this asymmetry is part
171
+ of your result whether you account for it or not. I could not find any discussion of
172
+ QK-norm interacting with LoRA. The closest published work is on controlling attention
173
+ logits during pretraining (Anson & Aitchison 2025; Zhai et al., ΟƒReparam, ICML 2023),
174
+ which treats the coupled magnitudes of Q and K as the thing to control β€” but says
175
+ nothing about adapters.
176
+
177
+ ## Two smaller landmines in the same area
178
+
179
+ Both are mine as far as I can tell, and both are cheap to avoid:
180
+
181
+ - `Gemma4TextRouter.proj` (2816 β†’ 128) is a real `nn.Linear`, so a loose regex like
182
+ `.*proj$` will catch the MoE **router**. Adapting expert routing is a far less
183
+ predictable edit than adjusting attention. Exclude it explicitly.
184
+ - `gate_proj` / `up_proj` / `down_proj` in `target_modules` land on the *dense*
185
+ `Gemma4TextMLP` (2816 β†’ 2112) sitting next to the experts β€” that is the single
186
+ shared expert β€” and not on the 128 routed ones. The names are absorbed by the wrong
187
+ module, which is why the trainable-parameter count comes out plausible-looking but
188
+ wrong.
189
+
190
+ For completeness, the neighbouring traps that **are** already well documented, so you
191
+ do not have to rediscover them: `Gemma4TextExperts` stores weights as stacked
192
+ `nn.Parameter`, so bitsandbytes cannot quantize them (Axolotl's expert-quantization
193
+ docs; bitsandbytes #1849) and PEFT needs `target_parameters` rather than
194
+ `target_modules` to reach them (PEFT docs; unsloth #4907 for the
195
+ "abnormally low trainable parameter count" symptom). The vision and audio towers reuse
196
+ the same leaf names, so an unanchored list leaks the adapter into them (oxen.ai;
197
+ Axolotl multimodal docs). On my first run part of the adapter landed on the vision
198
+ encoder and the loss flattened almost immediately.
199
+
200
+ ## Building the target list so it does what you wrote
201
+
202
+ Stop passing projection-name strings. Read the module paths off the live model and
203
+ assert your assumptions:
204
+
205
+ ```python
206
+ import re
207
+
208
+ PROJ = ("q_proj", "k_proj", "v_proj", "o_proj")
209
+ LAYER_RE = re.compile(r"language_model\.layers\.(\d+)\.")
210
+
211
+ layer_mods = {}
212
+ for name, _ in model.named_modules():
213
+ if "language_model.layers." not in name: # anchor: excludes vision/audio towers
214
+ continue
215
+ if not name.endswith(PROJ):
216
+ continue
217
+ li = int(LAYER_RE.search(name).group(1))
218
+ layer_mods.setdefault(li, {})[name.rsplit(".", 1)[-1]] = name
219
+
220
+ global_layers = sorted(i for i, v in layer_mods.items() if "v_proj" not in v)
221
+ assert global_layers == [5, 11, 17, 23, 29], f"layer plan changed: {global_layers}"
222
+ assert sum(len(v) for v in layer_mods.values()) == 115
223
+
224
+ # A genuinely query-key-only arm: skip k_proj on global layers,
225
+ # where k_proj is also the value matrix.
226
+ targets = [
227
+ layer_mods[li][p]
228
+ for li in sorted(layer_mods)
229
+ for p in ("q_proj", "k_proj")
230
+ if p in layer_mods[li] and not (p == "k_proj" and li in global_layers)
231
+ ]
232
+
233
+ FORBIDDEN = ("vision", "audio", "router", "experts", "embed", "lm_head",
234
+ "gate_proj", "up_proj", "down_proj")
235
+ assert not [t for t in targets if any(b in t.lower() for b in FORBIDDEN)]
236
+ ```
237
+
238
+ Then re-audit **after** `get_peft_model`, because that is where a config can still
239
+ surprise you: check that the number of trainable tensors is exactly twice the number
240
+ of targets, and that the set of touched layers and projection types matches your plan.
241
+ Make both `assert`, not `print`. A printed warning scrolls off screen, and an hour of
242
+ A100 time goes with it. PEFT also ships `get_model_status()` / `get_layer_status()`,
243
+ which is the supported way to see what actually got wrapped.
244
+
245
+ ## How I ran into this, and why my own numbers do not settle anything
246
+
247
+ I wanted to know which half of attention carries writing style and which half is
248
+ responsible for a fine-tune losing its grip on output format. So: two adapters, same
249
+ data (1415 train / 74 eval), same seed, r=32, alpha=64, lr 2e-5, 2 epochs, 354 steps,
250
+ QLoRA 4-bit, one A100 80GB. One on `v_proj + o_proj`, one on `q_proj + k_proj`.
251
+
252
+ | | A: `v_proj + o_proj` | B: `q_proj + k_proj` |
253
+ |---|---|---|
254
+ | targets | 55 | 60 |
255
+ | trainable params | 11,182,080 | 11,796,480 |
256
+ | final eval loss | **1.9703** | **2.2056** |
257
+ | mean token accuracy | 54.96 % | 51.38 % |
258
+
259
+ A is below B at every eval checkpoint, monotonically; both plateau; B has 5.5 % *more*
260
+ trainable parameters and still loses.
261
+
262
+ I am not asking you to believe that means anything, for five reasons.
263
+
264
+ **It reproduces a 2021 result.** "The value/output side beats the query/key side at
265
+ equal budget" is Table 5 of the original LoRA paper: on WikiSQL, Wq 70.4 / Wk 70.0
266
+ versus Wv 73.0 / Wo 73.2, and Wq+Wk 71.4 versus Wq+Wv 73.7. Yao et al. (IJCAI 2025)
267
+ added the mechanism: the gradient with respect to W_K contains W_Q, which is near zero
268
+ early in training, so Q and K are multiplicatively suppressed while V is not.
269
+
270
+ **The split is not the split I thought it was.** That is this whole article. Arm B
271
+ edited values on five layers; arm A never reached values there.
272
+
273
+ **The gap is over-determined.** Beyond that, QK-norm handicaps arm B structurally, and
274
+ my base started at loss 7.43 on this data β€” very far off. When the base is that far
275
+ from the target distribution, the run mostly measures which arm can move the output
276
+ distribution fastest, and that favours `o_proj`, which writes straight into the
277
+ residual stream, over q/k, which only reshape a softmax.
278
+
279
+ **My base was not clean, and the contamination is exactly on the seam I was testing.**
280
+ The base is a 15-way MoE merge followed by abliteration. I went back and checked what
281
+ the abliteration tool actually modifies: `attn.o_proj` only, on layers 14–26, by
282
+ unconstrained L-BFGS optimization of the matrix rather than a rank-1 projection. So
283
+ `o_proj` had been surgically rewritten in 13 of 30 layers before I started, while
284
+ q/k/v were untouched. Arm A trains on top of rewritten matrices, arm B on top of
285
+ pristine ones. I cannot predict the direction of that bias β€” a rewritten `o_proj`
286
+ could be easier or harder to adapt further β€” but a comparison with a systematic
287
+ asymmetry like that is not a fair one. Worth stating plainly: if you benchmark
288
+ anything about attention on an abliterated model, find out which matrices were
289
+ abliterated first.
290
+
291
+ (On the other hand, the merge did not lose tensors: the merged checkpoint and
292
+ `google/gemma-4-26B-A4B` have byte-identical `total_size` β€” 51,611,872,412 β€” and the
293
+ shard files differ by 584 bytes, which is the size difference of the safetensors JSON
294
+ headers. The missing `v_proj` really is architectural.)
295
+
296
+ **And my behavioural hypothesis was wrong.** I expected the value/output half to carry
297
+ style and the query/key half to be responsible for breaking format. What I saw was the
298
+ opposite arrangement: the value/output arm carries the style *and* breaks structured
299
+ output sooner, while the query/key arm holds formatting but barely transfers style β€”
300
+ it describes a character's voice instead of speaking in it. I report that as a negative
301
+ result rather than dropping it, with the caveat it deserves: those behavioural
302
+ observations are single generations per setting at one context depth, judged by me,
303
+ compared at equal adapter weight rather than equal effect size. Equal weight is the
304
+ wrong normalization when one arm is simply a stronger intervention per unit of weight.
305
+ That is an anecdote, not a measurement.
306
+
307
+ ## What I would actually like to know
308
+
309
+ - Does the loss gap survive on a clean `google/gemma-4-26B-A4B`, with three seeds, and
310
+ with the arms rebuilt so that `k_proj` on global layers goes to neither side?
311
+ - How much of a LoRA delta on `q_proj` / `k_proj` survives `q_norm` / `k_norm`? If the
312
+ answer is "not much", then a chunk of the folklore about which projections matter is
313
+ really a statement about where the normalization sits β€” and that folklore predates
314
+ QK-norm becoming standard.
315
+ - Those five global layers, where W_V is literally W_K: good place to adapt, or bad?
316
+ It is the only spot in the model where the two paths are physically tied, and I have
317
+ no intuition for what a low-rank edit there does.
318
+
319
+ If you have hit consequence 1 without noticing, or if you have run any of this on a
320
+ clean base, I would like to hear about it.
321
+
322
+ ## References
323
+
324
+ Architecture and code:
325
+
326
+ - Gemma 4 Technical Report β€” https://arxiv.org/html/2607.02770v1
327
+ - Gemma 4 model card (Google) β€” https://ai.google.dev/gemma/docs/core/model_card_4
328
+ - `google/gemma-4-26B-A4B` config β€” https://huggingface.co/google/gemma-4-26B-A4B/blob/main/config.json
329
+ - `modeling_gemma4.py` β€” https://github.com/huggingface/transformers/blob/main/src/transformers/models/gemma4/modeling_gemma4.py
330
+ - `configuration_gemma4.py` (`attention_k_eq_v` docstring) β€” https://github.com/huggingface/transformers/blob/main/src/transformers/models/gemma4/configuration_gemma4.py
331
+ - Gemma4 model docs β€” https://huggingface.co/docs/transformers/model_doc/gemma4
332
+
333
+ Prior public description of K=V:
334
+
335
+ - Devansh, "Google's Gemma 4 is Weirder than you Realize" β€” https://machine-learning-made-simple.medium.com/googles-gemma-4-is-weirder-than-you-realize-17d00d95b0d5
336
+ - Maarten Grootendorst, "A Visual Guide to Gemma 4" β€” https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-gemma-4
337
+ - idlemachines, "Gemma 4 is not your standard transformer" β€” https://idlemachines.co.uk/essays/gemma4-architecture
338
+
339
+ LoRA target-module selection:
340
+
341
+ - Hu et al., LoRA, 2021, Table 5 / Β§7.1 β€” https://arxiv.org/abs/2106.09685
342
+ - Yao et al., IJCAI 2025, unequal importance of attention matrices β€” https://arxiv.org/abs/2410.02247
343
+ - Elhage et al., 2021, query-key and output-value circuits β€” https://transformer-circuits.pub/2021/framework/index.html
344
+ - PEFT LoRA developer guide β€” https://huggingface.co/docs/peft/developer_guides/lora
345
+
346
+ Already-documented neighbouring traps:
347
+
348
+ - Axolotl, MoE expert quantization β€” https://docs.axolotl.ai/docs/expert_quantization.html
349
+ - bitsandbytes #1849, fused MoE weights β€” https://github.com/bitsandbytes-foundation/bitsandbytes/issues/1849
350
+ - unsloth #4907, low trainable-parameter count on Gemma 4 MoE β€” https://github.com/unslothai/unsloth/issues/4907
351
+ - oxen.ai, Gemma 4 fine-tuning pipeline β€” https://ghost.oxen.ai/writing-a-fine-tuning-and-deployment-pipeline-isnt-as-easy-as-it-looks-gemma-4-version/
352
+ - Axolotl multimodal docs β€” https://docs.axolotl.ai/docs/multimodal.html
353
+
354
+ Attention-logit control (background for consequence 3):
355
+
356
+ - Anson & Aitchison, "Controlling changes to attention logits", 2025 β€” https://arxiv.org/abs/2511.21377
357
+ - Zhai et al., ΟƒReparam, ICML 2023 β€” https://proceedings.mlr.press/v202/zhai23a/zhai23a.pdf
358
+
359
+ ---
360
+
361
+ Code excerpts are from `huggingface/transformers`, Apache License 2.0. Gemma is
362
+ provided under and subject to the Gemma Terms of Use found at ai.google.dev/gemma/terms.
363
+ "Gemma 4" is used descriptively; this article is not affiliated with or endorsed by Google.