AQIT
0.1.0
Toggle main menu visibility
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
6
import
torch
7
import
torch.nn.functional
as
F
8
from
torch.optim
import
Adam
9
from
pathlib
import
Path
10
from
datasets
import
load_dataset
11
from
transformer_lens
import
HookedTransformer
12
from
aquin.compute.device
import
(
13
accelerator_available,
14
empty_device_cache,
15
resolve_compute_device,
16
)
17
from
aquin.compute.sae
import
SparseAutoencoder
18
import
random
19
20
MODEL_NAME =
"meta-llama/Llama-3.2-1B-Instruct"
21
N_LAYERS = 16
22
N_FEATURES = 32768
23
D_MODEL = 2048
24
L1_COEFF = 10.0
25
LR = 1e-4
26
BATCH_SIZE = 4096
27
N_TOKENS = 2_000_000
28
SEQ_LEN = 64
29
COLLECT_BATCH = 32
# sequences per forward pass during collection
30
CHUNK_SIZE = 100_000
# activations per chunk file
31
SAVE_EVERY = 2000
32
SAE_DIR = Path(__file__).parent /
"sae"
33
ACTS_DIR = Path(__file__).parent /
"sae"
/
"acts"
34
DEVICE = resolve_compute_device()
35
36
37
def
chunk_paths
(layer: int):
38
return
sorted(ACTS_DIR.glob(f
"chunk_layer{layer}_*.pt"
))
39
40
41
def
norm_cache_path
(layer: int) -> Path:
42
return
ACTS_DIR / f
"norm_layer{layer}.pt"
43
44
45
def
sae_save_path
(layer: int) -> Path:
46
return
SAE_DIR / f
"sae_layer{layer}.pt"
47
48
49
def
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
101
def
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
127
def
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
184
if
__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()
193
194
layers = list(range(N_LAYERS))
if
args.layers ==
"all"
else
[int(x)
for
x
in
args.layers.split(
","
)]
195
196
print(f
"[main] loading {MODEL_NAME}..."
, flush=
True
)
197
model = HookedTransformer.from_pretrained(MODEL_NAME)
198
model.eval()
199
model.to(DEVICE)
200
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
)
aquin.compute.sae.SparseAutoencoder
Definition
sae.py:18
aquin.compute.device
Definition
device.py:1
aquin.compute.sae
Definition
sae.py:1
aquin.compute.train_sae.sae_save_path
Path sae_save_path(int layer)
Definition
train_sae.py:49
aquin.compute.train_sae.chunk_paths
chunk_paths(int layer)
Definition
train_sae.py:41
aquin.compute.train_sae.norm_cache_path
Path norm_cache_path(int layer)
Definition
train_sae.py:45
aquin.compute.train_sae.compute_norm
compute_norm(int layer)
Definition
train_sae.py:105
aquin.compute.train_sae.train_layer
train_layer(int layer)
Definition
train_sae.py:131
aquin.compute.train_sae.collect_activations
collect_activations(model, int layer)
Definition
train_sae.py:53
aquin
compute
train_sae.py
AQIT · Aquin Labs Private Limited · Apache 2.0 · Generated by
1.18.0