Update README.md
Browse files
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.
|