AQIT 0.1.0
Loading...
Searching...
No Matches
train_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
6import torch
7import torch.nn.functional as F
8from torch.optim import Adam
9from pathlib import Path
10from datasets import load_dataset
11from transformer_lens import HookedTransformer
12from aquin.compute.device import (
13 accelerator_available,
14 empty_device_cache,
15 resolve_compute_device,
16)
17from aquin.compute.sae import SparseAutoencoder
18import random
19
20MODEL_NAME = "meta-llama/Llama-3.2-1B-Instruct"
21N_LAYERS = 16
22N_FEATURES = 32768
23D_MODEL = 2048
24L1_COEFF = 10.0
25LR = 1e-4
26BATCH_SIZE = 4096
27N_TOKENS = 2_000_000
28SEQ_LEN = 64
29COLLECT_BATCH = 32 # sequences per forward pass during collection
30CHUNK_SIZE = 100_000 # activations per chunk file
31SAVE_EVERY = 2000
32SAE_DIR = Path(__file__).parent / "sae"
33ACTS_DIR = Path(__file__).parent / "sae" / "acts"
34DEVICE = resolve_compute_device()
37def chunk_paths(layer: int):
38 return sorted(ACTS_DIR.glob(f"chunk_layer{layer}_*.pt"))
39
40
41def norm_cache_path(layer: int) -> Path:
42 return ACTS_DIR / f"norm_layer{layer}.pt"
43
44
45def sae_save_path(layer: int) -> Path:
46 return SAE_DIR / f"sae_layer{layer}.pt"
47
48
49def collect_activations(model, layer: int):
50 existing = chunk_paths(layer)
51 existing_tokens = len(existing) * CHUNK_SIZE
52 if existing_tokens >= N_TOKENS:
53 print(f"[layer {layer}] found {len(existing)} chunks ({existing_tokens:,} tokens), skipping collection", flush=True)
54 return
55
56 print(f"[layer {layer}] collecting {N_TOKENS:,} activations...", flush=True)
57 ACTS_DIR.mkdir(parents=True, exist_ok=True)
58
59 dataset = load_dataset("Skylion007/openwebtext", split="train", streaming=True)
60 tokenizer = model.tokenizer
61 tokenizer.padding_side = "right"
62
63 hook_name = f"blocks.{layer}.hook_resid_post"
64 chunk, total, chunk_idx, buf = [], existing_tokens, len(existing), []
65
66 model.eval()
67 with torch.no_grad():
68 for sample in dataset:
69 if total >= N_TOKENS:
70 break
71 ids = tokenizer.encode(sample["text"][:1000], add_special_tokens=False)
72 buf.extend(ids)
73
74 while len(buf) >= SEQ_LEN * COLLECT_BATCH and total < N_TOKENS:
75 seqs = [buf[i * SEQ_LEN:(i + 1) * SEQ_LEN] for i in range(COLLECT_BATCH)]
76 buf = buf[SEQ_LEN * COLLECT_BATCH:]
77 tokens = torch.tensor(seqs, device=DEVICE)
78 _, cache = model.run_with_cache(
79 tokens,
80 names_filter=hook_name,
81 return_type=None,
82 )
83 # shape: (COLLECT_BATCH, SEQ_LEN, D_MODEL) -> (COLLECT_BATCH * SEQ_LEN, D_MODEL)
84 acts = cache[hook_name].reshape(-1, D_MODEL).cpu()
85 chunk.append(acts)
86 total += acts.shape[0]
87
88 if sum(a.shape[0] for a in chunk) >= CHUNK_SIZE:
89 p = ACTS_DIR / f"chunk_layer{layer}_{chunk_idx}.pt"
90 torch.save(torch.cat(chunk, dim=0)[:CHUNK_SIZE], p)
91 chunk, chunk_idx = [], chunk_idx + 1
92 print(f"[layer {layer}] {total:,} / {N_TOKENS:,} tokens", flush=True)
93
94 if chunk:
95 p = ACTS_DIR / f"chunk_layer{layer}_{chunk_idx}.pt"
96 torch.save(torch.cat(chunk, dim=0), p)
97
98 print(f"[layer {layer}] collection complete", flush=True)
99
100
101def compute_norm(layer: int):
102 np = norm_cache_path(layer)
103 if np.exists():
104 d = torch.load(np)
105 return d["mean"], d["std"]
106
107 print(f"[layer {layer}] computing mean/std over chunks...", flush=True)
108 running_mean = torch.zeros(D_MODEL)
109 n_total = 0
110 for p in chunk_paths(layer):
111 chunk = torch.load(p, map_location="cpu")
112 running_mean += chunk.sum(0)
113 n_total += chunk.shape[0]
114 mean = running_mean / n_total
115
116 running_var = torch.zeros(D_MODEL)
117 for p in chunk_paths(layer):
118 chunk = torch.load(p, map_location="cpu")
119 running_var += ((chunk - mean) ** 2).sum(0)
120 std = (running_var / n_total).sqrt().clamp(min=1e-6)
121
122 torch.save({"mean": mean, "std": std}, np)
123 print(f"[layer {layer}] norm computed", flush=True)
124 return mean, std
125
126
127def train_layer(layer: int):
128 save_path = sae_save_path(layer)
129 if save_path.exists():
130 print(f"[layer {layer}] checkpoint exists, skipping", flush=True)
131 return
132
133 mean, std = compute_norm(layer)
134 SAE_DIR.mkdir(parents=True, exist_ok=True)
135
136 sae = SparseAutoencoder(d_model=D_MODEL, n_features=N_FEATURES).to(DEVICE)
137 opt = Adam(sae.parameters(), lr=LR)
138 step = 0
139
140 # init b_pre from first chunk
141 first_chunk = torch.load(chunk_paths(layer)[0], map_location="cpu")
142 with torch.no_grad():
143 sae.b_pre.data = ((first_chunk[:10000] - mean) / std).to(DEVICE).mean(0)
144 del first_chunk
145
146 chunks = chunk_paths(layer)
147 print(f"[layer {layer}] training on {len(chunks)} chunks (~{len(chunks) * CHUNK_SIZE:,} tokens)", flush=True)
148
149 for epoch in range(1, 11):
150 random.shuffle(chunks)
151 for chunk_path in chunks:
152 acts = torch.load(chunk_path, map_location="cpu")
153 acts = (acts - mean) / std
154 n = acts.shape[0]
155 acts = acts[torch.randperm(n)]
156
157 for start in range(0, n - BATCH_SIZE, BATCH_SIZE):
158 batch = acts[start:start + BATCH_SIZE].to(DEVICE)
159 f, x_hat = sae(batch)
160 loss = F.mse_loss(x_hat, batch) + L1_COEFF * f.abs().mean()
161 opt.zero_grad()
162 loss.backward()
163 opt.step()
164 sae._normalise_decoder()
165 step += 1
166
167 if step % 200 == 0:
168 dead = (f.max(0).values == 0).sum().item()
169 l0 = (f > 0).float().sum(-1).mean().item()
170 recon = F.mse_loss(x_hat, batch).item()
171 print(f"[layer {layer}] step={step} epoch={epoch} recon={recon:.4f} L0={l0:.1f} dead={dead}/{N_FEATURES}", flush=True)
172
173 if step % SAVE_EVERY == 0:
174 sae.save(save_path)
175
176 del acts
177
178 sae.save(save_path)
179 print(f"[layer {layer}] epoch {epoch} done", flush=True)
180
181 print(f"[layer {layer}] training complete", flush=True)
182
183
184if __name__ == "__main__":
185 if not accelerator_available():
186 raise RuntimeError("No GPU accelerator detected (CUDA/ROCm or Metal MPS).")
187
188 import argparse
189 parser = argparse.ArgumentParser()
190 parser.add_argument("--layers", type=str, default="all",
191 help="Comma-separated layer indices to train, or 'all' for all layers")
192 args = parser.parse_args()
194 layers = list(range(N_LAYERS)) if args.layers == "all" else [int(x) for x in args.layers.split(",")]
196 print(f"[main] loading {MODEL_NAME}...", flush=True)
197 model = HookedTransformer.from_pretrained(MODEL_NAME)
198 model.eval()
199 model.to(DEVICE)
201 for layer in layers:
202 collect_activations(model, layer)
203
204 del model
205 empty_device_cache()
206 print("[main] collection done, starting training...", flush=True)
207
208 for layer in layers:
209 train_layer(layer)
210
211 print("[main] all layers done.", flush=True)
Path sae_save_path(int layer)
Definition train_sae.py:49
Path norm_cache_path(int layer)
Definition train_sae.py:45
collect_activations(model, int layer)
Definition train_sae.py:53