Files
evil_MoE/src/vgrout/lora2r.py
T
wassnameandClaudypoo adca442253 feat(#41): routeA activation gate replaces routeV grad gate
Gate now scores each rollout by dot(pooled bottleneck act, v_act) captured on
the no-grad logpi_old forward (quarantine-ablated, matching the sampling
policy); masks are pinned BEFORE the single grad-carrying forward, so the
grad-gate's pass-1 backward is gone. Thresholds: rolling 256-act buffer,
z-normalized, two-threshold Otsu (winsorized 1/99); warmup pins absorb until
128 scores. Buffer stores pooled acts and re-scores against the current v_act,
so the forward-only refresh (every 5 steps) needs no flush. No bimodality
guard: calibration showed Otsu tail separation ~2.4-2.8 buffer-sd on every
condition including pure Gaussians, so no shape statistic discriminates.

Deleted with the arm wiring (rename-on-logic-change: routeA never conflates
with routeV runs): extract_vhack_grad.py, _build_v_grad, route_band_edges,
_pair_cos, the pass-1 autograd.grad block, grad_probe training wiring,
v_grad_k/route_std_*/routeV_random_v_seed config, smoke-topk recipe.
c-probe stays in lora2r.py for scripts/diag_pinning.py only.

verify_science_invariants: all-in-one count 27 -> 42 (stale since c33b810
added the wave-2 behavior2 pairs) + assert the 8-pair routeA training subset.

Smoke: routeA/vanilla/absorb/solvemix all pass (gate exercises warmup, Otsu
zones, refresh, deploy ablation) -- /tmp/claude-1000/smoke_routeA.log.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-06-11 12:38:19 +00:00

132 lines
6.1 KiB
Python

"""lora2r adapter: one rank-2r LoRA per target Linear, A and B both trainable,
partitioned into a deployed block [:r] and a quarantine block [r:].
y += B@(A@x) - B0@(A0@x)
A0/B0 are FROZEN copies of the (seeded Gaussian) init, subtracted so the net
delta is exactly 0 at init while h = A@x is alive. Both factors must be nonzero
at init: the gate reads c.grad = h ⊙ (Bᵀδ) and A.grad = (Bᵀδ)xᵀ, both
identically zero under standard zero-B LoRA -- no extraction, no band
calibration, no trainable A at step 0. Init values beyond "nonzero" are NOT
load-bearing (we previously used PiSSA; see docs/spec/20260610_lora2r_v2_plan.md
T2 for why it was dropped).
[B|B_q] @ ([A;A_q]@x) has no cross terms (column b_k only ever multiplies row
a_k), so the two blocks are independent adapters. Per-rollout block masks on this
tensor mirror the retain/forget parameter partition used by SGTM (Cloud et al.).
"""
from __future__ import annotations
import torch
import torch.nn.functional as F
from jaxtyping import Float
from loguru import logger
from torch import Tensor, nn
TARGET_SUFFIXES = (
# full attention
"q_proj", "k_proj", "v_proj", "o_proj",
# linear-attention / GatedDeltaNet
"in_proj_qkv", "in_proj_z", "in_proj_a", "in_proj_b", "out_proj",
# MLP
"up_proj", "gate_proj", "down_proj",
)
def is_target(name: str) -> bool:
return name.split(".")[-1] in TARGET_SUFFIXES
def _lora2r_hook(layer: nn.Linear, args: tuple, y: Tensor) -> Tensor:
"""Add the two-block delta to y, applying per-rollout block masks.
Block masks (layer._lora2r_mask = (m, d), set by train.py per loss pass;
None = unmasked for generation / gate pass / eval):
m [G] quarantine on/off -- m=0: quarantine zero in forward AND backward,
so the deployed block trains in its post-ablation state
d [G] deployed detach -- d=1: deployed kept in forward, zero grad
(hack-gated rollouts update ONLY the quarantine block)
Masks act on branch OUTPUTS so a detach blocks grads to BOTH the A rows and
the B columns of that block.
grad probe: c = ones[..., 2r] spliced as h*c. After backward
c.grad = h ⊙ (Bᵀδ_y) = the per-sample WEIGHT grad of a virtual diagonal
scale between A and B. Training no longer uses it (the routeA gate reads
activations); kept for scripts/diag_pinning.py, whose grad-score baseline
reads this space.
"""
(x,) = args # x: [..., d_in]
A: Float[Tensor, "two_r d_in"] = layer._lora2r_A # trainable
B: Float[Tensor, "d_out two_r"] = layer._lora2r_B # trainable
A0: Float[Tensor, "two_r d_in"] = layer._lora2r_A0 # frozen init (subtracted: net delta 0 at init)
B0: Float[Tensor, "d_out two_r"] = layer._lora2r_B0
r = layer._lora2r_r
h = F.linear(x, A.to(x.dtype)) # [..., 2r]
if layer._lora2r_grad_probe and torch.is_grad_enabled():
c = torch.ones(h.shape[0], *([1] * (h.dim() - 2)), h.shape[-1],
device=h.device, dtype=h.dtype, requires_grad=True)
layer._lora2r_gate = c
h = h * c
h0 = F.linear(x, A0.to(x.dtype)) # [..., 2r] frozen init path
dep = (F.linear(h[..., :r], B[:, :r].to(x.dtype))
- F.linear(h0[..., :r], B0[:, :r].to(x.dtype)))
quar = (F.linear(h[..., r:], B[:, r:].to(x.dtype))
- F.linear(h0[..., r:], B0[:, r:].to(x.dtype)))
if layer._lora2r_mask is not None:
m, d = layer._lora2r_mask # [G] each
G = m.shape[0]
shape = dep.shape # [G, s, d_out] or [G*s, d_out]
dep = dep.reshape(G, -1, shape[-1])
quar = quar.reshape(G, -1, shape[-1])
d_ = d.view(G, 1, 1).to(dep.dtype)
dep = ((1 - d_) * dep + d_ * dep.detach()).reshape(shape)
quar = (m.view(G, 1, 1).to(quar.dtype) * quar).reshape(shape)
return y + (dep + quar).to(y.dtype)
def wrap_model_with_lora2r(
model: nn.Module,
r: int = 32,
init_seed: int = 0,
grad_probe: bool = False,
) -> dict[str, dict]:
"""Attach a rank-2r Gaussian-init LoRA (A AND B trainable) to every target Linear.
Init: A0 ~ N(0, 1/d_in) [2r, d_in], B0 ~ N(0, 1/2r) [d_out, 2r], seeded per
module so runs reproduce; blocks are iid -> statistically matched. W stays
untouched; the hook subtracts the frozen A0/B0 contribution. The quarantine's
learned delta is (A[r:], B[:, r:]) minus init; deploy ablation resets that
block to A0/B0 (eval.ablate_quarantine).
Info dict per module: {layer, A, B, A0, B0, handle, r}; quarantine = block
slices, no separate tensor.
"""
targets = [(n, m) for n, m in model.named_modules()
if isinstance(m, nn.Linear) and is_target(n)]
logger.info(f"lora2r attach: {len(targets)} target Linear modules, "
f"r={r}/block (2r={2 * r}), Gaussian init seed={init_seed}, A+B trainable")
out: dict[str, dict] = {}
for i, (name, linear) in enumerate(targets):
d_out, d_in = linear.weight.shape
dev = linear.weight.device
gen = torch.Generator().manual_seed(init_seed * 100003 + i)
A0 = (torch.randn(2 * r, d_in, generator=gen) / d_in ** 0.5).to(device=dev, dtype=torch.float32)
B0 = (torch.randn(d_out, 2 * r, generator=gen) / (2 * r) ** 0.5).to(device=dev, dtype=torch.float32)
linear.register_buffer("_lora2r_A0", A0, persistent=True)
linear.register_buffer("_lora2r_B0", B0, persistent=True)
A = nn.Parameter(A0.clone())
B = nn.Parameter(B0.clone())
linear.register_parameter("_lora2r_A", A)
linear.register_parameter("_lora2r_B", B)
linear._lora2r_r = r
linear._lora2r_grad_probe = grad_probe
linear._lora2r_gate = None
linear._lora2r_mask = None
out[name] = {"layer": linear, "A": A, "B": B, "A0": A0, "B0": B0,
"handle": linear.register_forward_hook(_lora2r_hook), "r": r}
trainable = ("_lora2r_A", "_lora2r_B")
for n, p in model.named_parameters():
if not n.endswith(trainable):
p.requires_grad_(False)
return out