Loader: olens.py with from_hub; README without A/B naming
Browse files- olens_lensB.py +3 -161
olens_lensB.py
CHANGED
|
@@ -1,162 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
-
safetensors only). Reads a residual vector into text (the lens), and scores text against a
|
| 3 |
-
residual (the AR).
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
ar = load_ar("ar/model.safetensors") # optional: scoring / FVE
|
| 8 |
-
h = residual(lens, "Paris is the capital of", layer=40) # [5120], last token
|
| 9 |
-
print(read(lens, h, layer=40, n=4)) # 4 readouts
|
| 10 |
-
cos = torch.cosine_similarity(ar_direction(ar, "France", 40), h.float(), dim=0)
|
| 11 |
-
|
| 12 |
-
Conventions (must match training):
|
| 13 |
-
- A residual at layer L is the output of decoder block L (0..63), before the final norm.
|
| 14 |
-
- Text is read with the chat template and enable_thinking=False.
|
| 15 |
-
- The AR saw spans of <= 32 tokens: score at most the first 32 tokens of a readout.
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
import torch
|
| 19 |
-
from peft import LoraConfig, get_peft_model
|
| 20 |
-
from safetensors.torch import load_file
|
| 21 |
-
from torch import nn
|
| 22 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 23 |
-
|
| 24 |
-
MODEL = "Qwen/Qwen3.6-27B"
|
| 25 |
-
REVISION = "6a9e13bd6fc8f0983b9b99948120bc37f49c13e9"
|
| 26 |
-
N_LAYERS, D = 64, 5120
|
| 27 |
-
SLOT_ID = 158983 # the token "㈜"; its input embedding is replaced by the vector
|
| 28 |
-
ALPHA = 1000.0 # injected norm: ALPHA * h / ||h||
|
| 29 |
-
PROMPT = (
|
| 30 |
-
"An activation vector from layer {layer} of a language model is enclosed in activation "
|
| 31 |
-
"tags: <activation>㈜</activation>. Produce distinct concepts that encode this activation, "
|
| 32 |
-
"each as a '- ' bullet on its own line."
|
| 33 |
-
)
|
| 34 |
-
TAG = "[Layer {n:02d}]" # the AR's layer prefix (6 tokens)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def _lora() -> LoraConfig:
|
| 38 |
-
return LoraConfig(r=16, lora_alpha=32, lora_dropout=0.0, target_modules="all-linear",
|
| 39 |
-
bias="none")
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def _peft(module: nn.Module) -> nn.Module:
|
| 43 |
-
"""All-linear LoRA with the adapters in fp32 on the bf16 base (as trained)."""
|
| 44 |
-
wrapped = get_peft_model(module, _lora())
|
| 45 |
-
for name, p in wrapped.named_parameters():
|
| 46 |
-
if "lora_" in name:
|
| 47 |
-
p.data = p.data.float()
|
| 48 |
-
return wrapped
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def _base(device: str) -> nn.Module:
|
| 52 |
-
lm = AutoModelForCausalLM.from_pretrained(MODEL, revision=REVISION, dtype=torch.bfloat16,
|
| 53 |
-
attn_implementation="sdpa")
|
| 54 |
-
lm.config.use_cache = False # plain forwards (as trained); read() turns it on for generate
|
| 55 |
-
lm.requires_grad_(False)
|
| 56 |
-
return lm.to(device)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def _load(module: nn.Module, path: str) -> None:
|
| 60 |
-
"""Load the trainable tensors; fail if any LoRA/head tensor is missing (an unloaded adapter
|
| 61 |
-
would silently read as fluent, activation-blind text)."""
|
| 62 |
-
sd = {k: v for k, v in load_file(path).items() if not k.startswith("injector.")}
|
| 63 |
-
want = {k for k, _ in module.named_parameters() if "lora_" in k or k.startswith("head.")}
|
| 64 |
-
missing = want - sd.keys()
|
| 65 |
-
if missing:
|
| 66 |
-
raise KeyError(f"{path}: {len(missing)} tensors missing, e.g. {sorted(missing)[:3]}")
|
| 67 |
-
module.load_state_dict(sd, strict=False)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
class Lens(nn.Module):
|
| 71 |
-
"""The inverter / RL lens. Parameter names match the published safetensors (``lm.*``)."""
|
| 72 |
-
|
| 73 |
-
def __init__(self, lm: nn.Module, tok: AutoTokenizer) -> None:
|
| 74 |
-
super().__init__()
|
| 75 |
-
self.lm, self.tok = lm, tok
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
class AR(nn.Module):
|
| 79 |
-
"""The AR: ``[Layer NN]`` + text span -> predicted residual direction at that layer."""
|
| 80 |
-
|
| 81 |
-
def __init__(self, backbone: nn.Module, tok: AutoTokenizer) -> None:
|
| 82 |
-
super().__init__()
|
| 83 |
-
self.backbone, self.tok = backbone, tok
|
| 84 |
-
self.head = nn.Sequential(nn.LayerNorm(D, elementwise_affine=False), nn.Linear(D, D))
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def load_lens(path: str, device: str = "cuda") -> Lens:
|
| 88 |
-
lm = _peft(_base(device))
|
| 89 |
-
lens = Lens(lm, AutoTokenizer.from_pretrained(MODEL, revision=REVISION))
|
| 90 |
-
_load(lens, path)
|
| 91 |
-
return lens.to(device).eval()
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def load_ar(path: str, device: str = "cuda") -> AR:
|
| 95 |
-
lm = _base(device)
|
| 96 |
-
decoder = lm.model
|
| 97 |
-
decoder.norm = nn.Identity() # read the last block's residual, before the final norm
|
| 98 |
-
del lm.lm_head
|
| 99 |
-
ar = AR(_peft(decoder), AutoTokenizer.from_pretrained(MODEL, revision=REVISION))
|
| 100 |
-
ar.head.float()
|
| 101 |
-
_load(ar, path)
|
| 102 |
-
return ar.to(device).eval()
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def _blocks(model: nn.Module) -> nn.ModuleList:
|
| 106 |
-
for m in model.modules():
|
| 107 |
-
if isinstance(getattr(m, "layers", None), nn.ModuleList) and hasattr(m, "norm"):
|
| 108 |
-
return m.layers
|
| 109 |
-
raise ValueError("no decoder layers found")
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
@torch.no_grad()
|
| 113 |
-
def residual(lens: Lens, user_text: str, layer: int, position: int = -1,
|
| 114 |
-
assistant_prefix: str = "") -> torch.Tensor:
|
| 115 |
-
"""The subject model's residual at ``layer`` (block output) for a chat turn, at ``position``
|
| 116 |
-
of the rendered sequence. Runs the BASE model (the lens's LoRA is disabled)."""
|
| 117 |
-
text = lens.tok.apply_chat_template([{"role": "user", "content": user_text}], tokenize=False,
|
| 118 |
-
add_generation_prompt=True, enable_thinking=False)
|
| 119 |
-
ids = lens.tok(text + assistant_prefix, return_tensors="pt",
|
| 120 |
-
add_special_tokens=False).input_ids.to(lens.lm.device)
|
| 121 |
-
out: dict[str, torch.Tensor] = {}
|
| 122 |
-
|
| 123 |
-
def grab(_m: nn.Module, _i: object, o: object) -> None: # must return None (else it replaces
|
| 124 |
-
out["h"] = (o[0] if isinstance(o, tuple) else o)[0, position] # the block's output)
|
| 125 |
-
|
| 126 |
-
hook = _blocks(lens.lm)[layer].register_forward_hook(grab)
|
| 127 |
-
try:
|
| 128 |
-
with lens.lm.disable_adapter():
|
| 129 |
-
lens.lm(input_ids=ids, use_cache=False)
|
| 130 |
-
finally:
|
| 131 |
-
hook.remove()
|
| 132 |
-
return out["h"].float()
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
@torch.no_grad()
|
| 136 |
-
def read(lens: Lens, h: torch.Tensor, layer: int, n: int = 1, max_new: int = 128,
|
| 137 |
-
temperature: float = 1.0, top_p: float = 1.0) -> list[str]:
|
| 138 |
-
"""``n`` readouts of residual ``h`` (shape [5120]) taken at ``layer``."""
|
| 139 |
-
text = lens.tok.apply_chat_template(
|
| 140 |
-
[{"role": "user", "content": PROMPT.format(layer=layer)}], tokenize=False,
|
| 141 |
-
add_generation_prompt=True, enable_thinking=False)
|
| 142 |
-
ids = lens.tok(text, return_tensors="pt", add_special_tokens=False).input_ids
|
| 143 |
-
ids = ids.to(lens.lm.device).repeat(n, 1)
|
| 144 |
-
emb = lens.lm.get_input_embeddings()(ids)
|
| 145 |
-
v = h.float().to(emb.device)
|
| 146 |
-
inj = (ALPHA * v / v.norm().clamp_min(1e-9)).to(emb.dtype)
|
| 147 |
-
emb = emb.masked_scatter((ids == SLOT_ID)[..., None], inj.expand(n, -1).contiguous())
|
| 148 |
-
out = lens.lm.generate(inputs_embeds=emb, attention_mask=torch.ones_like(ids),
|
| 149 |
-
max_new_tokens=max_new, do_sample=temperature > 0,
|
| 150 |
-
temperature=temperature if temperature > 0 else None, top_p=top_p,
|
| 151 |
-
top_k=0, use_cache=True)
|
| 152 |
-
return [lens.tok.decode(o, skip_special_tokens=True) for o in out]
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
@torch.no_grad()
|
| 156 |
-
def ar_direction(ar: AR, text: str, layer: int, max_tokens: int = 32) -> torch.Tensor:
|
| 157 |
-
"""The AR's predicted residual direction for ``text`` (its first ``max_tokens`` tokens)."""
|
| 158 |
-
tag = ar.tok.encode(TAG.format(n=layer), add_special_tokens=False)
|
| 159 |
-
span = ar.tok.encode(" " + text.strip(), add_special_tokens=False)[:max_tokens]
|
| 160 |
-
x = torch.tensor([tag + span], device=next(ar.parameters()).device)
|
| 161 |
-
hidden = ar.backbone(input_ids=x).last_hidden_state
|
| 162 |
-
return ar.head(hidden[0, -1].float())
|
|
|
|
| 1 |
+
"""Kept for older code: everything now lives in ``olens.py`` (same functions, plus ``from_hub``)."""
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
from olens import * # noqa: F401,F403
|
| 4 |
+
from olens import ALPHA, SLOT_ID # noqa: F401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|