AQIT 0.1.0
Loading...
Searching...
No Matches
activation_store.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""Resolve saved activation directories for SAE training (chunks, captures, prior runs)."""
3
4from __future__ import annotations
5
6import json
7from datetime import datetime, timezone
8from pathlib import Path
9from typing import Any
10
11import torch
12
13CHUNK_SIZE = 50_000
14MANIFEST_NAME = "manifest.json"
15
16
17def find_chunk_paths(acts_dir: Path) -> list[Path]:
18 return sorted(acts_dir.glob("chunk_*.pt"))
19
20
21def read_manifest(acts_dir: Path) -> dict[str, Any] | None:
22 path = acts_dir / MANIFEST_NAME
23 if not path.is_file():
24 return None
25 try:
26 data = json.loads(path.read_text(encoding="utf-8"))
27 return data if isinstance(data, dict) else None
28 except Exception:
29 return None
30
31
33 acts_dir: Path,
34 *,
35 model_id: str,
36 layer: int,
37 model_mode: str,
38 d_model: int,
39 source: str,
40 n_vectors: int,
41 checkpoint_path: str | None = None,
42 corpus_path: str | None = None,
43) -> Path:
44 acts_dir.mkdir(parents=True, exist_ok=True)
45 chunks = [p.name for p in find_chunk_paths(acts_dir)]
46 manifest = {
47 "schema_version": 1,
48 "source": source,
49 "created_at": datetime.now(timezone.utc).isoformat(),
50 "model_id": model_id,
51 "layer": layer,
52 "model_mode": model_mode,
53 "d_model": d_model,
54 "n_vectors": n_vectors,
55 "checkpoint": checkpoint_path,
56 "corpus": corpus_path,
57 "files": {
58 "chunks": chunks,
59 "norm": "norm.pt" if (acts_dir / "norm.pt").is_file() else None,
60 },
61 }
62 out = acts_dir / MANIFEST_NAME
63 out.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
64 return out
65
66
67def _split_save_chunks(stacked: torch.Tensor, out_dir: Path) -> int:
68 out_dir.mkdir(parents=True, exist_ok=True)
69 n = int(stacked.shape[0])
70 chunk_i = 0
71 for idx in range(0, n, CHUNK_SIZE):
72 torch.save(stacked[idx : idx + CHUNK_SIZE], out_dir / f"chunk_{chunk_i}.pt")
73 chunk_i += 1
74 return n
75
76
77def import_capture_to_chunks(capture_dir: Path, layer: int, out_dir: Path) -> int:
78 manifest = read_manifest(capture_dir)
79 if not manifest:
80 raise ValueError(f"No manifest.json in {capture_dir}")
82 rel = (manifest.get("files") or {}).get("activations", {}).get(str(layer))
83 if not rel:
84 layers = list((manifest.get("files") or {}).get("activations", {}).keys())
85 hint = f" Available layers: {', '.join(layers)}" if layers else ""
86 raise ValueError(f"Capture has no activations for layer {layer}.{hint}")
87
88 tensor_path = capture_dir / rel
89 if not tensor_path.is_file():
90 raise FileNotFoundError(f"Capture activation file missing: {tensor_path}")
91
92 stacked = torch.load(tensor_path, map_location="cpu", weights_only=False)
93 if stacked.dim() != 2:
94 raise ValueError(f"Expected 2D activations [N, d_model], got shape {tuple(stacked.shape)}")
95
96 n = _split_save_chunks(stacked, out_dir)
98 out_dir,
99 model_id=str(manifest.get("model_id") or ""),
100 layer=layer,
101 model_mode=str(manifest.get("model_mode") or "llm"),
102 d_model=int(stacked.shape[1]),
103 source="capture",
104 n_vectors=n,
105 checkpoint_path=manifest.get("checkpoint"),
106 )
107 return n
108
109
111 capture_dir: Path,
112 layer: int,
113 out_dir: Path,
114 *,
115 group: str | None = None,
116) -> int:
117 manifest = read_manifest(capture_dir)
118 if not manifest:
119 raise ValueError(f"No manifest.json in {capture_dir}")
120
121 rel = (manifest.get("files") or {}).get("activations", {}).get(str(layer))
122 if not rel:
123 layers = list((manifest.get("files") or {}).get("activations", {}).keys())
124 hint = f" Available layers: {', '.join(layers)}" if layers else ""
125 raise ValueError(f"Capture has no activations for layer {layer}.{hint}")
126
127 tensor_path = capture_dir / rel
128 if not tensor_path.is_file():
129 raise FileNotFoundError(f"Capture activation file missing: {tensor_path}")
130
131 stacked = torch.load(tensor_path, map_location="cpu", weights_only=False)
132 if stacked.dim() != 2:
133 raise ValueError(f"Expected 2D activations [N, d_model], got shape {tuple(stacked.shape)}")
134
135 probes = manifest.get("probes") if isinstance(manifest.get("probes"), list) else []
136 if not probes:
137 return import_capture_to_chunks(capture_dir, layer, out_dir)
138
139 from aquin.compute.activation_capture import balance_probes
140
141 balanced, meta = balance_probes([p for p in probes if isinstance(p, dict)], group=group)
142 if not meta.get("balanced"):
143 return import_capture_to_chunks(capture_dir, layer, out_dir)
144
145 keep_ids = {str(p.get("id")) for p in balanced if p.get("id") is not None}
146 keep_indices = [
147 i for i, p in enumerate(probes)
148 if isinstance(p, dict) and str(p.get("id")) in keep_ids
149 ]
150 if not keep_indices:
151 return import_capture_to_chunks(capture_dir, layer, out_dir)
152
153 subset = stacked[torch.tensor(keep_indices, dtype=torch.long)]
154 n = _split_save_chunks(subset, out_dir)
156 out_dir,
157 model_id=str(manifest.get("model_id") or ""),
158 layer=layer,
159 model_mode=str(manifest.get("model_mode") or "llm"),
160 d_model=int(subset.shape[1]),
161 source="capture-balanced",
162 n_vectors=n,
163 checkpoint_path=manifest.get("checkpoint"),
164 )
165 return n
166
167
169 path: str | Path,
170 layer: int,
171 *,
172 balance: bool = False,
173 balance_group: str | None = None,
174) -> Path:
175 """
176 Return a directory containing chunk_*.pt for training.
177
178 Accepts:
179 - a collect dir (chunk_*.pt + optional manifest.json)
180 - a prior train run dir (…/my-run/_acts_layer{N})
181 - a capture-activations output dir (manifest.json + layers/layer_{N}.pt)
182 """
183 root = Path(path).expanduser().resolve()
184 if not root.exists():
185 raise FileNotFoundError(f"Activations path not found: {root}")
186
187 if find_chunk_paths(root):
188 return root
189
190 sub = root / f"_acts_layer{layer}"
191 if find_chunk_paths(sub):
192 return sub
193
194 if (root / MANIFEST_NAME).is_file():
195 manifest = read_manifest(root)
196 if manifest and manifest.get("files", {}).get("activations"):
197 suffix = f"_balanced_{balance_group}" if balance and balance_group else ("_balanced" if balance else "")
198 cache = root / f"_train_cache_layer{layer}{suffix}"
199 if find_chunk_paths(cache):
200 return cache
201 if balance:
202 n = import_capture_to_chunks_balanced(root, layer, cache, group=balance_group)
203 print(f"[sae-train] imported {n:,} balanced vectors from capture → {cache}", flush=True)
204 else:
205 n = import_capture_to_chunks(root, layer, cache)
206 print(f"[sae-train] imported {n:,} vectors from capture → {cache}", flush=True)
207 return cache
208
209 raise FileNotFoundError(
210 f"No training activations for layer {layer} under {root}.\n"
211 f"Expected chunk_*.pt, _acts_layer{layer}/, or capture manifest with layers/layer_{layer}.pt"
212 )
213
214
215def count_vectors(acts_dir: Path) -> int:
216 total = 0
217 for p in find_chunk_paths(acts_dir):
218 t = torch.load(p, map_location="cpu", weights_only=False)
219 total += int(t.shape[0])
220 return total
221
222
223def compute_norm(acts_dir: Path, d_model: int) -> tuple[torch.Tensor, torch.Tensor]:
224 norm_path = acts_dir / "norm.pt"
225 if norm_path.exists():
226 d = torch.load(norm_path, map_location="cpu", weights_only=False)
227 return d["mean"], d["std"]
228
229 chunks = find_chunk_paths(acts_dir)
230 if not chunks:
231 raise FileNotFoundError(f"No chunk_*.pt in {acts_dir}")
232
233 running_mean = torch.zeros(d_model)
234 n_total = 0
235 for p in chunks:
236 c = torch.load(p, map_location="cpu", weights_only=False)
237 running_mean += c.sum(0)
238 n_total += c.shape[0]
239 mean = running_mean / max(n_total, 1)
240
241 running_var = torch.zeros(d_model)
242 for p in chunks:
243 c = torch.load(p, map_location="cpu", weights_only=False)
244 running_var += ((c - mean) ** 2).sum(0)
245 std = (running_var / max(n_total, 1)).sqrt().clamp(min=1e-6)
246 torch.save({"mean": mean, "std": std}, norm_path)
247 return mean, std
248
249
250def validate_acts_manifest(acts_dir: Path, *, model_id: str, layer: int) -> None:
251 manifest = read_manifest(acts_dir)
252 if not manifest:
253 return
254 man_layer = manifest.get("layer")
255 if man_layer is not None and int(man_layer) != int(layer):
256 raise ValueError(
257 f"Activations manifest is for layer {man_layer}, but --layer {layer} was passed."
258 )
259 man_model = manifest.get("model_id")
260 if not man_model:
261 return
262 try:
263 from aquin.compute.model_loader import resolve_model_id
264
265 want = resolve_model_id(model_id)
266 got = resolve_model_id(str(man_model))
267 if want != got:
268 print(
269 f"[sae-train] warning: activations model_id={got!r} differs from session model {want!r}",
270 flush=True,
271 )
272 except ValueError:
273 pass
list[Path] find_chunk_paths(Path acts_dir)
int _split_save_chunks(torch.Tensor stacked, Path out_dir)
Path resolve_training_acts_dir(str|Path path, int layer, *, bool balance=False, str|None balance_group=None)
None validate_acts_manifest(Path acts_dir, *, str model_id, int layer)
int import_capture_to_chunks(Path capture_dir, int layer, Path out_dir)
tuple[torch.Tensor, torch.Tensor] compute_norm(Path acts_dir, int d_model)
int import_capture_to_chunks_balanced(Path capture_dir, int layer, Path out_dir, *, str|None group=None)
Path write_collect_manifest(Path acts_dir, *, str model_id, int layer, str model_mode, int d_model, str source, int n_vectors, str|None checkpoint_path=None, str|None corpus_path=None)
dict[str, Any]|None read_manifest(Path acts_dir)