85 checkpoint_path: str | Path |
None =
None,
86 corpus_path: str | Path |
None =
None,
88 device = resolve_compute_device()
89 acts_dir.mkdir(parents=
True, exist_ok=
True)
91 existing = sorted(acts_dir.glob(
"chunk_*.pt"))
92 existing_tokens = len(existing) * CHUNK_SIZE
93 if existing_tokens >= n_tokens:
94 print(f
"[sae-train] found {len(existing)} chunks ({existing_tokens:,} tokens), skipping collection", flush=
True)
97 print(f
"[sae-train] collecting {n_tokens:,} activations from layer {layer}...", flush=
True)
98 model = load_tl_from_checkpoint(model_id, checkpoint_path)
101 hook_name = f
"blocks.{layer}.hook_resid_post"
102 tokenizer = model.tokenizer
103 if hasattr(tokenizer,
"padding_side"):
104 tokenizer.padding_side =
"right"
106 chunk: list[torch.Tensor] = []
107 total = existing_tokens
108 chunk_idx = len(existing)
111 with torch.no_grad():
113 if total >= n_tokens:
115 ids = tokenizer.encode(sample, add_special_tokens=
False)
118 while len(buf) >= SEQ_LEN * COLLECT_BATCH
and total < n_tokens:
119 seqs = [buf[i * SEQ_LEN:(i + 1) * SEQ_LEN]
for i
in range(COLLECT_BATCH)]
120 buf = buf[SEQ_LEN * COLLECT_BATCH:]
121 tokens = torch.tensor(seqs, device=device)
122 _, cache = model.run_with_cache(
124 names_filter=hook_name,
127 acts = cache[hook_name].reshape(-1, cache[hook_name].shape[-1]).cpu()
129 total += acts.shape[0]
131 if sum(a.shape[0]
for a
in chunk) >= CHUNK_SIZE:
132 stacked = torch.cat(chunk, dim=0)[:CHUNK_SIZE]
133 torch.save(stacked, acts_dir / f
"chunk_{chunk_idx}.pt")
134 chunk, chunk_idx = [], chunk_idx + 1
135 print(f
"[sae-train] {total:,} / {n_tokens:,} tokens", flush=
True)
138 torch.save(torch.cat(chunk, dim=0), acts_dir / f
"chunk_{chunk_idx}.pt")
142 print(
"[sae-train] collection complete", flush=
True)
150 checkpoint_path: str | Path |
None =
None,
151 corpus_path: str | Path |
None =
None,
153 d_model: int |
None =
None,
154 n_features: int |
None =
None,
155 max_steps: int |
None =
None,
156 max_epochs: int |
None =
None,
157 activations_dir: str | Path |
None =
None,
158 balance: bool =
False,
159 balance_group: str |
None =
None,
164 short = resolve_model_id(model_id)
165 cfg = get_config(short)
166 d_model = d_model
or cfg[
"d_model"]
168 if n_features
is None:
170 ref = load_sae(short, layer=layer)
171 n_features = ref.n_features
176 n_tokens = QUICK_TOKENS
if quick
else FULL_TOKENS
177 steps_cap = max_steps
or (QUICK_MAX_STEPS
if quick
else FULL_MAX_STEPS)
178 epochs_cap = max_epochs
or (QUICK_MAX_EPOCHS
if quick
else FULL_MAX_EPOCHS)
181 work_dir = resolve_training_acts_dir(
185 balance_group=balance_group,
187 validate_acts_manifest(work_dir, model_id=short, layer=layer)
188 n_vectors = count_vectors(work_dir)
190 f
"[sae-train] training from saved activations ({n_vectors:,} vectors) at {work_dir}",
193 if n_vectors < 10_000:
195 "[sae-train] warning: few activation vectors — quality may be poor; "
196 "use corpus collection or a larger capture for production SAEs",
200 work_dir = output_path.parent / f
"_acts_layer{layer}"
206 checkpoint_path=checkpoint_path,
207 corpus_path=corpus_path,
209 write_collect_manifest(
216 n_vectors=count_vectors(work_dir),
217 checkpoint_path=str(checkpoint_path)
if checkpoint_path
else None,
218 corpus_path=str(corpus_path)
if corpus_path
else None,
221 mean, std = compute_norm(work_dir, d_model)
222 device = resolve_compute_device()
224 opt = Adam(sae.parameters(), lr=LR)
226 chunks = find_chunk_paths(work_dir)
228 raise FileNotFoundError(f
"No activation chunks in {work_dir}")
229 first = torch.load(chunks[0], map_location=
"cpu", weights_only=
False)
230 with torch.no_grad():
231 sae.b_pre.data = ((first[:10_000] - mean) / std).to(device).mean(0)
236 f
"[sae-train] training on {len(chunks)} chunks "
237 f
"(quick={quick}, steps_cap={steps_cap}, epochs_cap={epochs_cap})",
240 for epoch
in range(1, epochs_cap + 1):
241 random.shuffle(chunks)
242 for chunk_path
in chunks:
243 acts = torch.load(chunk_path, map_location=
"cpu", weights_only=
False)
244 acts = (acts - mean) / std
246 perm = torch.randperm(n)
249 for start
in range(0, n - BATCH_SIZE, BATCH_SIZE):
250 batch = acts[start:start + BATCH_SIZE].to(device)
251 f, x_hat = sae(batch)
252 loss = F.mse_loss(x_hat, batch) + L1_COEFF * f.abs().mean()
256 sae._normalise_decoder()
260 dead = (f.max(0).values == 0).sum().item()
261 l0 = (f > 0).float().sum(-1).mean().item()
262 recon = F.mse_loss(x_hat, batch).item()
264 f
"[sae-train] epoch={epoch} step={step} recon={recon:.4f} "
265 f
"L0={l0:.1f} dead={dead}/{n_features}",
269 if step >= steps_cap:
272 if step >= steps_cap:
274 if step >= steps_cap:
277 print(f
"[sae-train] finished {step} steps ({epoch} epochs)", flush=
True)
279 output_path.parent.mkdir(parents=
True, exist_ok=
True)
280 sae.save(output_path)
281 norm_path = output_path.parent / f
"norm_layer{layer}.pt"
282 torch.save({
"mean": mean.cpu(),
"std": std.cpu()}, norm_path)
286 "checkpoint_path": str(checkpoint_path)
if checkpoint_path
else None,
289 "max_steps": steps_cap,
291 "n_features": n_features,
294 output_path.with_suffix(
".meta.json").write_text(json.dumps(meta, indent=2), encoding=
"utf-8")
295 print(f
"[sae-train] saved {output_path}", flush=
True)
302 short = resolve_model_id(model_id)