AQIT 0.1.0
Loading...
Searching...
No Matches
sae.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2# This file is part of the Aquin Engine. Unauthorized copying, modification,
3# distribution, or use of this file, via any medium, is strictly prohibited.
4# Proprietary and confidential. See LICENSE for terms.
5
6from __future__ import annotations
7
8from pathlib import Path
9
10import torch
11import torch.nn as nn
12
13
14class SparseAutoencoder(nn.Module):
15 def __init__(self, d_model: int = 2048, n_features: int = 16384):
16 super().__init__()
17 self.d_model = d_model
18 self.n_features = n_features
19 self.b_pre = nn.Parameter(torch.zeros(d_model))
20 self.W_enc = nn.Parameter(torch.nn.init.kaiming_uniform_(torch.empty(d_model, n_features)))
21 self.b_enc = nn.Parameter(torch.zeros(n_features))
22 self.W_dec = nn.Parameter(torch.nn.init.kaiming_uniform_(torch.empty(n_features, d_model)))
23 self.b_dec = nn.Parameter(torch.zeros(d_model))
26 def _normalise_decoder(self) -> None:
27 with torch.no_grad():
28 norms = self.W_dec.norm(dim=-1, keepdim=True).clamp(min=1e-8)
29 self.W_dec.data = self.W_dec.data / norms
31 def encode(self, x: torch.Tensor) -> torch.Tensor:
32 return torch.relu((x - self.b_pre) @ self.W_enc + self.b_enc)
33
34 def decode(self, f: torch.Tensor) -> torch.Tensor:
35 return f @ self.W_dec + self.b_dec
36
37 def forward(self, x: torch.Tensor):
38 f = self.encode(x)
39 return f, self.decode(f)
40
41 @torch.no_grad()
42 def get_top_features(self, x: torch.Tensor, k: int = 10) -> list[tuple[int, float]]:
43 f = self.encode(x.unsqueeze(0)).squeeze(0)
44 topk = f.topk(k)
45 return [(int(i), float(v)) for i, v in zip(topk.indices, topk.values)]
47 def save(self, path: str | Path) -> None:
48 path = Path(path)
49 path.parent.mkdir(parents=True, exist_ok=True)
50 torch.save({"d_model": self.d_model, "n_features": self.n_features, "state_dict": self.state_dict()}, path)
52 @classmethod
53 def _find_tensor(cls, tensors: dict, *names: str) -> torch.Tensor | None:
54 for name in names:
55 if name in tensors:
56 return tensors[name]
57 leaf_to_full: dict[str, str] = {}
58 for key, val in tensors.items():
59 if isinstance(val, torch.Tensor):
60 leaf_to_full[key.split(".")[-1]] = key
61 for name in names:
62 leaf = name.split(".")[-1]
63 if leaf in leaf_to_full:
64 return tensors[leaf_to_full[leaf]]
65 return None
66
67 @classmethod
68 def _infer_dims(cls, tensors: dict) -> tuple[int, int]:
69 enc = cls._find_tensor(tensors, "W_enc", "encoder.weight")
70 dec = cls._find_tensor(tensors, "W_dec", "decoder.weight")
71 if enc is not None and enc.ndim == 2:
72 if enc.shape[0] > enc.shape[1]:
73 return int(enc.shape[1]), int(enc.shape[0])
74 return int(enc.shape[0]), int(enc.shape[1])
75 if dec is not None and dec.ndim == 2:
76 if dec.shape[0] > dec.shape[1]:
77 return int(dec.shape[1]), int(dec.shape[0])
78 return int(dec.shape[1]), int(dec.shape[0])
79 raise KeyError(
80 "Cannot infer SAE dimensions — missing W_enc/encoder.weight or W_dec/decoder.weight. "
81 f"Keys: {sorted(tensors.keys())}"
82 )
83
84 @classmethod
85 def _coerce_checkpoint(cls, ckpt: Any) -> tuple[dict[str, Any], dict[str, torch.Tensor]]:
86 if isinstance(ckpt, cls):
87 return {"d_model": ckpt.d_model, "n_features": ckpt.n_features}, ckpt.state_dict()
88
89 if not isinstance(ckpt, dict):
90 raise ValueError(f"Unrecognised SAE checkpoint type: {type(ckpt)}")
91
92 meta = dict(ckpt)
93 if "state_dict" in ckpt and isinstance(ckpt["state_dict"], dict):
94 tensors = {k: v for k, v in ckpt["state_dict"].items() if isinstance(v, torch.Tensor)}
95 elif "model_state" in ckpt and isinstance(ckpt["model_state"], dict):
96 tensors = {k: v for k, v in ckpt["model_state"].items() if isinstance(v, torch.Tensor)}
97 elif "model" in ckpt and isinstance(ckpt["model"], dict):
98 tensors = {k: v for k, v in ckpt["model"].items() if isinstance(v, torch.Tensor)}
99 else:
100 tensors = {k: v for k, v in ckpt.items() if isinstance(v, torch.Tensor)}
101
102 for nested_key in ("cfg", "config", "args"):
103 nested = ckpt.get(nested_key)
104 if isinstance(nested, dict):
105 for key in ("d_model", "d_in", "n_features", "dict_size", "d_sae", "num_features"):
106 if key in nested and key not in meta:
107 meta[key] = nested[key]
108
109 return meta, tensors
110
111 @classmethod
112 def _meta_int(cls, meta: dict[str, Any], *keys: str) -> int | None:
113 for key in keys:
114 if key in meta and meta[key] is not None:
115 return int(meta[key])
116 return None
117
118 @classmethod
119 def _remap_tensors(cls, tensors: dict[str, torch.Tensor], sae: "SparseAutoencoder") -> dict[str, torch.Tensor]:
120 key_map = {
121 "encoder.weight": "W_enc",
122 "encoder.bias": "b_enc",
123 "decoder.weight": "W_dec",
124 "decoder.bias": "b_dec",
125 "encoder_pre_bias": "b_pre",
126 "pre_bias": "b_pre",
127 "W_enc": "W_enc",
128 "b_enc": "b_enc",
129 "W_dec": "W_dec",
130 "b_dec": "b_dec",
131 "b_pre": "b_pre",
132 }
133 out = dict(sae.state_dict())
134 enc_loaded = False
135 for src_key, dst_key in key_map.items():
136 t = cls._find_tensor(tensors, src_key)
137 if t is None:
138 continue
139 if dst_key == "W_enc":
140 if enc_loaded:
141 continue
142 t = t.float()
143 if t.shape[0] > t.shape[1]:
144 t = t.T
145 enc_loaded = True
146 elif dst_key == "W_dec":
147 t = t.float()
148 if t.shape == (sae.d_model, sae.n_features):
149 t = t.T
150 else:
151 t = t.float()
152 out[dst_key] = t
153 return out
154
155 @classmethod
156 def load(cls, path: str | Path, device: str = "cuda") -> "SparseAutoencoder":
157 path = Path(path)
158 if path.suffix == ".safetensors":
159 return cls._load_safetensors(path, device)
160 from aquin.compute.torch_io import is_corrupt_pytorch_zip, load_checkpoint
161
162 try:
163 ckpt = load_checkpoint(path, map_location=device)
164 except Exception as exc:
165 if is_corrupt_pytorch_zip(exc) or (
166 exc.__cause__ is not None and is_corrupt_pytorch_zip(exc.__cause__)
167 ):
168 raise ValueError(
169 f"SAE checkpoint at {path} is corrupted or incomplete. "
170 "Delete the file and run: aquin load sae <model>-l<layer>"
171 ) from exc
172 raise
173 from aquin.compute.torch_io import looks_like_catalog_metadata_dict
174
175 if looks_like_catalog_metadata_dict(ckpt):
176 raise ValueError(
177 f"SAE checkpoint at {path} is catalog metadata, not weights. "
178 "Re-download with: aquin load sae <model>-l<n>"
179 )
180 if isinstance(ckpt, cls):
181 ckpt.to(device)
182 ckpt.eval()
183 return ckpt
184
185 if not isinstance(ckpt, dict):
186 raise ValueError(
187 f"Not an SAE dictionary checkpoint ({type(ckpt)}): {path}. "
188 "Use ~/.aquin/sae/user/<model>/<name>/sae_layerN.pt — "
189 "not capture files like sae/sae_layer_N.pt or layers/layer_N.pt."
190 )
191
192 meta, tensors = cls._coerce_checkpoint(ckpt)
193 if not tensors or (
194 cls._find_tensor(tensors, "W_enc", "encoder.weight") is None
195 and cls._find_tensor(tensors, "W_dec", "decoder.weight") is None
196 ):
197 raise ValueError(
198 f"SAE checkpoint at {path} is not a valid PyTorch weights file. "
199 "Re-download with: aquin load sae <model>-l<n> "
200 "or place weights with: aquin load sae --path <file.pt> --layer <n>"
201 )
202 d_model = cls._meta_int(meta, "d_model", "d_in")
203 n_features = cls._meta_int(meta, "n_features", "dict_size", "d_sae", "num_features")
204 if d_model is None or n_features is None:
205 inferred_d, inferred_n = cls._infer_dims(tensors)
206 d_model = d_model if d_model is not None else inferred_d
207 n_features = n_features if n_features is not None else inferred_n
208
209 sae = cls(d_model=d_model, n_features=n_features)
210 remapped = cls._remap_tensors(tensors, sae)
211 if cls._find_tensor(tensors, "W_enc", "encoder.weight") is not None:
212 sae.load_state_dict(remapped, strict=False)
213 else:
214 sae.load_state_dict({k: v for k, v in remapped.items() if k in sae.state_dict()}, strict=False)
215 sae.to(device)
216 sae.eval()
217 return sae
218
219 @classmethod
220 def _load_safetensors(cls, path: Path, device: str = "cuda") -> "SparseAutoencoder":
221 from safetensors.torch import load_file
222 tensors = load_file(str(path), device=device)
223
224 # SAELens-style key layout:
225 # W_enc, b_enc, W_dec, b_dec, b_pre (optional: scaling_factor)
226 # Map to our parameter names
227 key_map = {
228 "W_enc": ["W_enc", "encoder.weight"],
229 "b_enc": ["b_enc", "encoder.bias"],
230 "W_dec": ["W_dec", "decoder.weight"],
231 "b_dec": ["b_dec", "decoder.bias"],
232 "b_pre": ["b_pre", "pre_bias", "encoder_pre_bias"],
233 }
234
235 def _get(candidates: list[str]) -> torch.Tensor | None:
236 for k in candidates:
237 if k in tensors:
238 return tensors[k]
239 return None
240
241 W_enc = _get(key_map["W_enc"])
242 b_enc = _get(key_map["b_enc"])
243 W_dec = _get(key_map["W_dec"])
244 b_dec = _get(key_map["b_dec"])
245 b_pre = _get(key_map["b_pre"])
246
247 if W_enc is None or W_dec is None:
248 raise ValueError(f"Unrecognised safetensors layout. Keys: {list(tensors.keys())}")
249
250 # Infer dimensions — SAELens encoder.weight is [n_features, d_model]
251 if W_enc.ndim != 2:
252 raise ValueError(f"Unexpected W_enc shape: {W_enc.shape}")
253 if W_enc.shape[0] > W_enc.shape[1]:
254 n_features, d_model = W_enc.shape
255 W_enc = W_enc.T
256 else:
257 d_model, n_features = W_enc.shape
258
259 sae = cls(d_model=d_model, n_features=n_features)
260 sae.W_enc.data = W_enc.float()
261 if W_dec.shape == (n_features, d_model):
262 sae.W_dec.data = W_dec.float()
263 elif W_dec.shape == (d_model, n_features):
264 sae.W_dec.data = W_dec.T.float()
265 else:
266 sae.W_dec.data = W_dec.T.float()
267 if b_enc is not None:
268 sae.b_enc.data = b_enc.float()
269 if b_dec is not None:
270 sae.b_dec.data = b_dec.float()
271 if b_pre is not None:
272 sae.b_pre.data = b_pre.float()
273 sae.to(device)
274 sae.eval()
275 return sae
int|None _meta_int(cls, dict[str, Any] meta, *str keys)
Definition sae.py:116
list[tuple[int, float]] get_top_features(self, torch.Tensor x, int k=10)
Definition sae.py:46
tuple[int, int] _infer_dims(cls, dict tensors)
Definition sae.py:72
__init__(self, int d_model=2048, int n_features=16384)
Definition sae.py:19
tuple[dict[str, Any], dict[str, torch.Tensor]] _coerce_checkpoint(cls, Any ckpt)
Definition sae.py:89
torch.Tensor|None _find_tensor(cls, dict tensors, *str names)
Definition sae.py:57
torch.Tensor encode(self, torch.Tensor x)
Definition sae.py:35
"SparseAutoencoder" _load_safetensors(cls, Path path, str device="cuda")
Definition sae.py:224
torch.Tensor decode(self, torch.Tensor f)
Definition sae.py:38
dict[str, torch.Tensor] _remap_tensors(cls, dict[str, torch.Tensor] tensors, "SparseAutoencoder" sae)
Definition sae.py:123
forward(self, torch.Tensor x)
Definition sae.py:41
"SparseAutoencoder" load(cls, str|Path path, str device="cuda")
Definition sae.py:160
None save(self, str|Path path)
Definition sae.py:51