w-ahmad commited on
Commit
bf54758
·
verified ·
1 Parent(s): 8be8e64

Auto upload zain 2026-08-12T20:48:44.955825

Browse files
zain/Activation/README.md ADDED
@@ -0,0 +1 @@
 
 
1
+ # Activation
zain/Activation/exp.py ADDED
@@ -0,0 +1,1102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tiny Llama GLU Lab — Consolidated training library.
3
+ One file: model definition, activation registry, stability monitoring,
4
+ time tracking, dataset builder, and Trainer factory.
5
+ """
6
+
7
+ import math
8
+ import os
9
+ import time
10
+ import json
11
+ import re
12
+ from pathlib import Path
13
+ from itertools import chain
14
+ from typing import Dict, Callable, Optional, List, Any, Tuple
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+ from transformers import (
19
+ LlamaConfig,
20
+ LlamaPreTrainedModel,
21
+ Trainer,
22
+ TrainerCallback,
23
+ TrainingArguments,
24
+ DataCollatorForLanguageModeling,
25
+ AutoTokenizer,
26
+ set_seed,
27
+ )
28
+ from transformers.models.llama.modeling_llama import (
29
+ LlamaAttention,
30
+ LlamaRMSNorm,
31
+ LlamaRotaryEmbedding,
32
+ )
33
+ from transformers.modeling_outputs import CausalLMOutputWithPast
34
+ from datasets import load_dataset
35
+
36
+
37
+ # =============================================================================
38
+ # 1. ACTIVATION REGISTRY
39
+ # =============================================================================
40
+
41
+ class GLUActivationRegistry:
42
+ """Own every gating activation you test. Add new variants in one line."""
43
+ _registry: Dict[str, Callable[[torch.Tensor], torch.Tensor]] = {}
44
+
45
+ @classmethod
46
+ def register(cls, name: str, fn: Callable[[torch.Tensor], torch.Tensor]) -> None:
47
+ cls._registry[name] = fn
48
+
49
+ @classmethod
50
+ def get(cls, name: str) -> Callable[[torch.Tensor], torch.Tensor]:
51
+ if name not in cls._registry:
52
+ raise KeyError(
53
+ f"Activation '{name}' not found. Available: {list(cls._registry.keys())}"
54
+ )
55
+ return cls._registry[name]
56
+
57
+
58
+ # Built-ins
59
+ GLUActivationRegistry.register("silu", nn.functional.silu)
60
+ GLUActivationRegistry.register("swish", nn.functional.silu)
61
+ GLUActivationRegistry.register("relu", nn.functional.relu)
62
+ GLUActivationRegistry.register("gelu", nn.functional.gelu)
63
+ GLUActivationRegistry.register("sigmoid", torch.sigmoid)
64
+ GLUActivationRegistry.register("tanh", torch.tanh)
65
+ GLUActivationRegistry.register("softplus", nn.functional.softplus)
66
+ GLUActivationRegistry.register("linear", lambda x: x)
67
+
68
+ # Custom
69
+ GLUActivationRegistry.register("s10", lambda x: x * x * torch.sigmoid(x))
70
+ GLUActivationRegistry.register("w1a", lambda x: x * torch.tanh(x))
71
+
72
+
73
+ # =============================================================================
74
+ # 2. CONFIG
75
+ # =============================================================================
76
+
77
+ class TinyLlamaConfig(LlamaConfig):
78
+ """
79
+ Exact Llama config plus two fields:
80
+ - mlp_type: "glu" or "mlp" (standard Transformer MLP)
81
+ - activation: name of the activation function to use inside the MLP block.
82
+ - waleed_beta: β_cap for the waleed10 post‑clip (default 10.0)
83
+ Enforces pure MHA by requiring num_key_value_heads == num_attention_heads.
84
+ """
85
+ model_type = "tiny_llama"
86
+
87
+ def __init__(
88
+ self,
89
+ mlp_type: str = "glu",
90
+ activation: str = "silu",
91
+ waleed_beta: float = 10.0,
92
+ **kwargs
93
+ ):
94
+ super().__init__(**kwargs)
95
+ self.mlp_type = mlp_type
96
+ self.activation = activation
97
+ self.waleed_beta = waleed_beta
98
+ if self.num_key_value_heads != self.num_attention_heads:
99
+ raise ValueError(
100
+ f"Pure MHA required: num_key_value_heads ({self.num_key_value_heads}) "
101
+ f"must equal num_attention_heads ({self.num_attention_heads})."
102
+ )
103
+
104
+
105
+ # =============================================================================
106
+ # 3. MODEL
107
+ # =============================================================================
108
+
109
+ class TinyLlamaMLP(nn.Module):
110
+ """
111
+ Unified MLP block supporting:
112
+ - Standard GLU: down_proj( act(gate_proj(x)) * up_proj(x) )
113
+ - Standard MLP: down_proj( act(up_proj(x)) )
114
+ - Gated variants (situglu, waleed) with internal tanh scaling.
115
+ - waleed10 / silu-waleed10 with post‑down‑proj clipping.
116
+
117
+ All projections are always named the same way, ensuring consistent
118
+ tensor logging regardless of activation.
119
+ """
120
+ def __init__(self, config: TinyLlamaConfig):
121
+ super().__init__()
122
+ self.hidden_size = config.hidden_size
123
+ self.intermediate_size = config.intermediate_size
124
+ self.mlp_type = config.mlp_type
125
+ self.activation_name = config.activation
126
+ self.waleed_beta = getattr(config, "waleed_beta", 10.0)
127
+
128
+ # Effective intermediate size (scaled for MLP)
129
+ if self.mlp_type == "glu":
130
+ effective_intermediate = self.intermediate_size
131
+ elif self.mlp_type == "mlp":
132
+ effective_intermediate = int(self.intermediate_size * 1.5)
133
+ print(f"[MLP] Auto‑scaled intermediate_size from {self.intermediate_size} to {effective_intermediate} for parameter parity.")
134
+ else:
135
+ raise ValueError(f"Unknown mlp_type: {self.mlp_type}")
136
+
137
+ self.effective_intermediate = effective_intermediate
138
+
139
+ # Always define the projections (names consistent across variants)
140
+ if self.mlp_type == "glu":
141
+ self.gate_proj = nn.Linear(self.hidden_size, effective_intermediate, bias=False)
142
+ self.up_proj = nn.Linear(self.hidden_size, effective_intermediate, bias=False)
143
+ else: # mlp
144
+ self.up_proj = nn.Linear(self.hidden_size, effective_intermediate, bias=False)
145
+
146
+ self.down_proj = nn.Linear(effective_intermediate, self.hidden_size, bias=False)
147
+
148
+ # Activation function (for standard variants)
149
+ if self.mlp_type == "glu":
150
+ if self.activation_name in ("situglu", "waleed", "situglu_low", "waleedglu_low"):
151
+ self.act_fn = None
152
+ elif self.activation_name == "waleed10":
153
+ self.act_fn = GLUActivationRegistry.get("linear")
154
+ elif self.activation_name == "silu-waleed10":
155
+ self.act_fn = GLUActivationRegistry.get("silu")
156
+ else:
157
+ self.act_fn = GLUActivationRegistry.get(self.activation_name)
158
+ else: # mlp
159
+ if self.activation_name in ("situglu", "waleed", "situglu_low", "waleedglu_low"):
160
+ raise ValueError(
161
+ f"Activation '{self.activation_name}' requires a gated architecture (GLU). "
162
+ f"Please use mlp_type='glu'."
163
+ )
164
+ elif self.activation_name == "waleed10":
165
+ self.act_fn = GLUActivationRegistry.get("linear")
166
+ elif self.activation_name == "silu-waleed10":
167
+ self.act_fn = GLUActivationRegistry.get("silu")
168
+ else:
169
+ self.act_fn = GLUActivationRegistry.get(self.activation_name)
170
+
171
+ # Determine beta values for situglu/waleed variants
172
+ if self.activation_name in ("situglu_low", "waleedglu_low"):
173
+ self.beta1 = 2.5
174
+ self.beta2 = 4.0
175
+ else:
176
+ self.beta1 = 4.0
177
+ self.beta2 = 25.0
178
+
179
+ # Flags for special handling
180
+ self.is_situglu = self.activation_name in ("situglu", "situglu_low")
181
+ self.is_waleed = self.activation_name in ("waleed", "waleedglu_low")
182
+ self.is_waleed10 = self.activation_name in ("waleed10", "silu-waleed10")
183
+ self.has_sigmoid_gate = self.activation_name.startswith("situglu")
184
+
185
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
186
+ if self.mlp_type == "glu":
187
+ gate = self.gate_proj(x)
188
+ up = self.up_proj(x)
189
+
190
+ if self.is_situglu or self.is_waleed:
191
+ if self.has_sigmoid_gate:
192
+ gate = self.beta1 * torch.tanh(gate / self.beta1) * torch.sigmoid(gate)
193
+ else:
194
+ gate = self.beta1 * torch.tanh(gate / self.beta1)
195
+ up = self.beta2 * torch.tanh(up / self.beta2)
196
+ hidden = gate * up
197
+ else:
198
+ hidden = self.act_fn(gate) * up
199
+
200
+ out = self.down_proj(hidden)
201
+
202
+ else: # mlp
203
+ hidden = self.act_fn(self.up_proj(x))
204
+ out = self.down_proj(hidden)
205
+
206
+ if self.is_waleed10:
207
+ out = self.waleed_beta * torch.tanh(out / self.waleed_beta)
208
+
209
+ return out
210
+
211
+
212
+ class TinyLlamaDecoderLayer(nn.Module):
213
+ def __init__(self, config: TinyLlamaConfig, layer_idx: int):
214
+ super().__init__()
215
+ self.hidden_size = config.hidden_size
216
+ self.self_attn = LlamaAttention(config=config, layer_idx=layer_idx)
217
+ self.mlp = TinyLlamaMLP(config)
218
+ self.input_layernorm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
219
+ self.post_attention_layernorm = LlamaRMSNorm(
220
+ config.hidden_size, eps=config.rms_norm_eps
221
+ )
222
+ # Residual stream gateways (zero params, hookable)
223
+ self.residual_pre_attn = nn.Identity()
224
+ self.residual_post_attn = nn.Identity()
225
+ self.residual_post_mlp = nn.Identity()
226
+
227
+ def forward(
228
+ self,
229
+ hidden_states: torch.Tensor,
230
+ attention_mask: Optional[torch.Tensor] = None,
231
+ position_ids: Optional[torch.LongTensor] = None,
232
+ position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
233
+ **kwargs,
234
+ ):
235
+ # Attention sub-layer
236
+ residual = hidden_states
237
+ hidden_states = self.residual_pre_attn(hidden_states)
238
+ hidden_states = self.input_layernorm(hidden_states)
239
+ attn_out = self.self_attn(
240
+ hidden_states=hidden_states,
241
+ attention_mask=attention_mask,
242
+ position_ids=position_ids,
243
+ position_embeddings=position_embeddings,
244
+ )[0]
245
+ hidden_states = residual + attn_out
246
+ hidden_states = self.residual_post_attn(hidden_states)
247
+
248
+ # MLP sub-layer
249
+ residual = hidden_states
250
+ hidden_states = self.post_attention_layernorm(hidden_states)
251
+ hidden_states = self.mlp(hidden_states)
252
+ hidden_states = residual + hidden_states
253
+ hidden_states = self.residual_post_mlp(hidden_states)
254
+ return (hidden_states,)
255
+
256
+
257
+ # ----------------------------------------------------------------------------
258
+ # ATTENTION MASK
259
+ # ----------------------------------------------------------------------------
260
+ def _build_causal_mask(
261
+ attention_mask: Optional[torch.Tensor],
262
+ seq_len: int,
263
+ dtype: torch.dtype,
264
+ device: torch.device,
265
+ ) -> torch.Tensor:
266
+ min_value = torch.finfo(dtype).min
267
+ causal = torch.full((seq_len, seq_len), fill_value=min_value, dtype=dtype, device=device)
268
+ causal = torch.triu(causal, diagonal=1)
269
+ causal = causal[None, None, :, :]
270
+
271
+ if attention_mask is None:
272
+ batch_size = 1
273
+ return causal.expand(batch_size, 1, seq_len, seq_len)
274
+
275
+ batch_size = attention_mask.shape[0]
276
+ causal = causal.expand(batch_size, 1, seq_len, seq_len).clone()
277
+ padding = attention_mask[:, None, None, :].to(device) == 0
278
+ causal = causal.masked_fill(padding, min_value)
279
+ return causal
280
+
281
+
282
+ _MASK_PRINTED = False
283
+
284
+
285
+ class TinyLlamaModel(LlamaPreTrainedModel):
286
+ config_class = TinyLlamaConfig
287
+
288
+ def __init__(self, config: TinyLlamaConfig):
289
+ super().__init__(config)
290
+ self.padding_idx = config.pad_token_id
291
+ self.vocab_size = config.vocab_size
292
+ self.embed_tokens = nn.Embedding(
293
+ config.vocab_size, config.hidden_size, self.padding_idx
294
+ )
295
+ self.layers = nn.ModuleList(
296
+ [TinyLlamaDecoderLayer(config, i) for i in range(config.num_hidden_layers)]
297
+ )
298
+ self.norm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
299
+ self.rotary_emb = LlamaRotaryEmbedding(config=config)
300
+ self.post_init()
301
+
302
+ def forward(
303
+ self,
304
+ input_ids: Optional[torch.LongTensor] = None,
305
+ attention_mask: Optional[torch.Tensor] = None,
306
+ position_ids: Optional[torch.LongTensor] = None,
307
+ inputs_embeds: Optional[torch.FloatTensor] = None,
308
+ return_dict: Optional[bool] = None,
309
+ **kwargs,
310
+ ):
311
+ global _MASK_PRINTED
312
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
313
+ if inputs_embeds is None:
314
+ inputs_embeds = self.embed_tokens(input_ids)
315
+
316
+ if position_ids is None:
317
+ seq_len = inputs_embeds.shape[1]
318
+ position_ids = torch.arange(
319
+ seq_len, device=inputs_embeds.device
320
+ ).unsqueeze(0).expand(inputs_embeds.shape[0], -1)
321
+
322
+ hidden_states = inputs_embeds
323
+ position_embeddings = self.rotary_emb(hidden_states, position_ids)
324
+
325
+ seq_len = hidden_states.shape[1]
326
+ causal_mask = _build_causal_mask(
327
+ attention_mask, seq_len, hidden_states.dtype, hidden_states.device
328
+ )
329
+
330
+ if not _MASK_PRINTED:
331
+ print("[INFO] Causal mask (float with -inf) applied to all attention layers.")
332
+ _MASK_PRINTED = True
333
+
334
+ for decoder_layer in self.layers:
335
+ layer_outputs = decoder_layer(
336
+ hidden_states,
337
+ attention_mask=causal_mask,
338
+ position_ids=position_ids,
339
+ position_embeddings=position_embeddings,
340
+ )
341
+ hidden_states = layer_outputs[0]
342
+
343
+ hidden_states = self.norm(hidden_states)
344
+ if not return_dict:
345
+ return (hidden_states,)
346
+ return {"last_hidden_state": hidden_states, "hidden_states": None, "attentions": None}
347
+
348
+
349
+ class TinyLlamaForCausalLM(LlamaPreTrainedModel):
350
+ config_class = TinyLlamaConfig
351
+ _tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
352
+
353
+ def __init__(self, config: TinyLlamaConfig):
354
+ super().__init__(config)
355
+ self.model = TinyLlamaModel(config)
356
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
357
+ if config.tie_word_embeddings:
358
+ self.lm_head.weight = self.model.embed_tokens.weight
359
+ self.post_init()
360
+
361
+ def get_input_embeddings(self):
362
+ return self.model.embed_tokens
363
+
364
+ def set_input_embeddings(self, value):
365
+ self.model.embed_tokens = value
366
+
367
+ def get_output_embeddings(self):
368
+ return self.lm_head
369
+
370
+ def forward(
371
+ self,
372
+ input_ids: Optional[torch.LongTensor] = None,
373
+ attention_mask: Optional[torch.Tensor] = None,
374
+ position_ids: Optional[torch.LongTensor] = None,
375
+ inputs_embeds: Optional[torch.FloatTensor] = None,
376
+ labels: Optional[torch.LongTensor] = None,
377
+ return_dict: Optional[bool] = None,
378
+ **kwargs,
379
+ ):
380
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
381
+ outputs = self.model(
382
+ input_ids=input_ids,
383
+ attention_mask=attention_mask,
384
+ position_ids=position_ids,
385
+ inputs_embeds=inputs_embeds,
386
+ return_dict=return_dict,
387
+ )
388
+ hidden_states = outputs["last_hidden_state"] if return_dict else outputs[0]
389
+ logits = self.lm_head(hidden_states)
390
+
391
+ loss = None
392
+ if labels is not None:
393
+ shift_logits = logits[..., :-1, :].contiguous()
394
+ shift_labels = labels[..., 1:].contiguous()
395
+ loss_fct = nn.CrossEntropyLoss()
396
+ loss = loss_fct(
397
+ shift_logits.view(-1, self.config.vocab_size), shift_labels.view(-1)
398
+ )
399
+
400
+ if not return_dict:
401
+ output = (logits,) + outputs[1:]
402
+ return (loss,) + output if loss is not None else output
403
+ return CausalLMOutputWithPast(
404
+ loss=loss,
405
+ logits=logits,
406
+ past_key_values=None,
407
+ hidden_states=None,
408
+ attentions=None,
409
+ )
410
+
411
+ def prepare_inputs_for_generation(
412
+ self, input_ids, past_key_values=None, attention_mask=None, **kwargs
413
+ ):
414
+ if past_key_values:
415
+ input_ids = input_ids[:, -1:]
416
+ position_ids = kwargs.get("position_ids")
417
+ if attention_mask is not None and position_ids is None:
418
+ position_ids = attention_mask.long().cumsum(-1) - 1
419
+ position_ids.masked_fill_(attention_mask == 0, 1)
420
+ if past_key_values:
421
+ position_ids = position_ids[:, -1].unsqueeze(-1)
422
+ return {
423
+ "input_ids": input_ids,
424
+ "position_ids": position_ids,
425
+ "past_key_values": past_key_values,
426
+ "attention_mask": attention_mask,
427
+ }
428
+
429
+
430
+ # =============================================================================
431
+ # 4. MONITORING ENGINE
432
+ # =============================================================================
433
+
434
+ class StatsEngine:
435
+ """Compute unified signature for any tensor."""
436
+
437
+ @staticmethod
438
+ def compute(
439
+ tensor: torch.Tensor, user_limit: float, dtype_ratio: float
440
+ ) -> Dict[str, float]:
441
+ with torch.no_grad():
442
+ abs_t = tensor.abs()
443
+ dtype_info = torch.finfo(tensor.dtype)
444
+ dtype_limit = (
445
+ dtype_ratio * dtype_info.max
446
+ if not torch.isinf(torch.tensor(dtype_info.max))
447
+ else float("inf")
448
+ )
449
+
450
+ t_min = tensor.min().item()
451
+ t_max = tensor.max().item()
452
+
453
+ return {
454
+ "norm": tensor.norm(2).item(),
455
+ "mean": tensor.mean().item(),
456
+ "std": tensor.std().item(),
457
+ "max_abs": abs_t.max().item(),
458
+ "frac_near_dtype_limit": (
459
+ (abs_t > dtype_limit).float().mean().item()
460
+ if not math.isinf(dtype_limit)
461
+ else 0.0
462
+ ),
463
+ "frac_near_user_limit": (abs_t > user_limit).float().mean().item(),
464
+ "min": t_min,
465
+ "max": t_max,
466
+ "range": t_max - t_min,
467
+ }
468
+
469
+
470
+ class StepAccumulator:
471
+ """Stores per-tensor entries, aggregates to layer/global scope."""
472
+
473
+ def __init__(self):
474
+ self.tensors: Dict[str, Dict[str, float]] = {}
475
+
476
+ def add(self, name: str, numel: int, stats: Dict[str, float]):
477
+ new_entry = {"numel": numel, **stats}
478
+ existing = self.tensors.get(name)
479
+ self.tensors[name] = (
480
+ new_entry if existing is None else self._merge_entry(existing, new_entry)
481
+ )
482
+
483
+ @staticmethod
484
+ def _merge_entry(a: Dict[str, float], b: Dict[str, float]) -> Dict[str, float]:
485
+ total_n = a["numel"] + b["numel"]
486
+ if total_n == 0:
487
+ return a
488
+ norm = math.sqrt(a["norm"] ** 2 + b["norm"] ** 2)
489
+ max_abs = max(a["max_abs"], b["max_abs"])
490
+ mean = (a["mean"] * a["numel"] + b["mean"] * b["numel"]) / total_n
491
+ ex2 = (
492
+ a["numel"] * (a["std"] ** 2 + a["mean"] ** 2)
493
+ + b["numel"] * (b["std"] ** 2 + b["mean"] ** 2)
494
+ ) / total_n
495
+ std = math.sqrt(max(0.0, ex2 - mean ** 2))
496
+ frac_dtype = (
497
+ a["frac_near_dtype_limit"] * a["numel"] + b["frac_near_dtype_limit"] * b["numel"]
498
+ ) / total_n
499
+ frac_user = (
500
+ a["frac_near_user_limit"] * a["numel"] + b["frac_near_user_limit"] * b["numel"]
501
+ ) / total_n
502
+
503
+ t_min = min(a.get("min", float("inf")), b.get("min", float("inf")))
504
+ t_max = max(a.get("max", float("-inf")), b.get("max", float("-inf")))
505
+
506
+ return {
507
+ "numel": total_n,
508
+ "norm": norm,
509
+ "mean": mean,
510
+ "std": std,
511
+ "max_abs": max_abs,
512
+ "frac_near_dtype_limit": frac_dtype,
513
+ "frac_near_user_limit": frac_user,
514
+ "min": t_min,
515
+ "max": t_max,
516
+ "range": t_max - t_min,
517
+ }
518
+
519
+ def clear(self):
520
+ self.tensors.clear()
521
+
522
+ def _aggregate(self, entries: Dict[str, Dict[str, float]]) -> Dict[str, float]:
523
+ if not entries:
524
+ return {}
525
+ numels = [e["numel"] for e in entries.values()]
526
+ total_n = sum(numels)
527
+
528
+ norm = math.sqrt(sum(e["norm"] ** 2 for e in entries.values()))
529
+ max_abs = max(e["max_abs"] for e in entries.values())
530
+ mean = sum(e["mean"] * e["numel"] for e in entries.values()) / total_n
531
+ ex2 = (
532
+ sum(e["numel"] * (e["std"] ** 2 + e["mean"] ** 2) for e in entries.values())
533
+ / total_n
534
+ )
535
+ std = math.sqrt(max(0.0, ex2 - mean ** 2))
536
+ frac_dtype = (
537
+ sum(e["frac_near_dtype_limit"] * e["numel"] for e in entries.values())
538
+ / total_n
539
+ )
540
+ frac_user = (
541
+ sum(e["frac_near_user_limit"] * e["numel"] for e in entries.values())
542
+ / total_n
543
+ )
544
+
545
+ t_min = min(e.get("min", float("inf")) for e in entries.values())
546
+ t_max = max(e.get("max", float("-inf")) for e in entries.values())
547
+
548
+ return {
549
+ "norm": norm,
550
+ "mean": mean,
551
+ "std": std,
552
+ "max_abs": max_abs,
553
+ "frac_near_dtype_limit": frac_dtype,
554
+ "frac_near_user_limit": frac_user,
555
+ "min": t_min,
556
+ "max": t_max,
557
+ "range": t_max - t_min,
558
+ }
559
+
560
+ def get_global_stats(self) -> Dict[str, float]:
561
+ return self._aggregate(self.tensors)
562
+
563
+ def get_layer_stats(self, layer_prefix: str) -> Dict[str, float]:
564
+ entries = {
565
+ k: v for k, v in self.tensors.items() if k.startswith(layer_prefix + ".")
566
+ }
567
+ return self._aggregate(entries)
568
+
569
+
570
+ class HookRegistry:
571
+ """Attach and throttle forward/backward hooks."""
572
+
573
+ def __init__(self, model: nn.Module):
574
+ self.model = model
575
+ self.handles: List[torch.utils.hooks.RemovableHandle] = []
576
+ self.active = False
577
+
578
+ def attach_forward(
579
+ self,
580
+ module_patterns: List[str],
581
+ accumulator: StepAccumulator,
582
+ user_limit: float,
583
+ dtype_ratio: float,
584
+ ):
585
+ for name, module in self.model.named_modules():
586
+ if any(re.search(p, name) for p in module_patterns):
587
+ h = module.register_forward_hook(
588
+ self._make_forward_hook(name, accumulator, user_limit, dtype_ratio)
589
+ )
590
+ self.handles.append(h)
591
+
592
+ def attach_backward(
593
+ self, accumulator: StepAccumulator, user_limit: float, dtype_ratio: float
594
+ ):
595
+ for name, param in self.model.named_parameters():
596
+ if param.requires_grad:
597
+ h = param.register_hook(
598
+ self._make_backward_hook(
599
+ f"grad.{name}", accumulator, user_limit, dtype_ratio
600
+ )
601
+ )
602
+ self.handles.append(h)
603
+
604
+ def _make_forward_hook(
605
+ self, module_name: str, accumulator: StepAccumulator, user_limit: float, dtype_ratio: float
606
+ ):
607
+ def hook(module, inp, out):
608
+ if not self.active:
609
+ return
610
+ if isinstance(out, dict):
611
+ out_dict = out
612
+ out = out_dict.get("last_hidden_state")
613
+ if out is None:
614
+ out = next(
615
+ (v for v in out_dict.values() if torch.is_tensor(v)), None
616
+ )
617
+ elif isinstance(out, (tuple, list)):
618
+ out = out[0] if len(out) > 0 else None
619
+
620
+ if not torch.is_tensor(out):
621
+ return
622
+
623
+ stats = StatsEngine.compute(out.detach(), user_limit, dtype_ratio)
624
+ accumulator.add(f"act.{module_name}", out.numel(), stats)
625
+
626
+ return hook
627
+
628
+ def _make_backward_hook(
629
+ self, param_name: str, accumulator: StepAccumulator, user_limit: float, dtype_ratio: float
630
+ ):
631
+ def hook(grad):
632
+ if not self.active:
633
+ return
634
+ stats = StatsEngine.compute(grad.detach(), user_limit, dtype_ratio)
635
+ accumulator.add(param_name, grad.numel(), stats)
636
+
637
+ return hook
638
+
639
+ def set_active(self, active: bool):
640
+ self.active = active
641
+
642
+ def clear(self):
643
+ for h in self.handles:
644
+ h.remove()
645
+ self.handles.clear()
646
+
647
+
648
+ class StabilityMonitorCallback(TrainerCallback):
649
+ """Full stability instrumentation: grad / param / act statistics."""
650
+
651
+ def __init__(
652
+ self,
653
+ model: nn.Module,
654
+ monitor_every_n_steps: int = 10,
655
+ module_patterns: Optional[List[str]] = None,
656
+ user_limits: Optional[Dict[str, float]] = None,
657
+ dtype_proximity_ratio: float = 0.9,
658
+ log_scope: Optional[Dict[str, bool]] = None,
659
+ monitor_during_eval: bool = False,
660
+ ):
661
+ self.model = model
662
+ self.monitor_every_n_steps = monitor_every_n_steps
663
+ self.module_patterns = module_patterns or [".*mlp.*", ".*self_attn.*", ".*residual.*"]
664
+ self.user_limits = user_limits or {"grad": 1.0, "param": 100.0, "act": 50.0}
665
+ self.dtype_ratio = dtype_proximity_ratio
666
+ self.log_scope = log_scope or {
667
+ "global": True,
668
+ "per_layer": True,
669
+ "per_tensor": False,
670
+ }
671
+ self.monitor_during_eval = monitor_during_eval
672
+
673
+ self.accumulator = StepAccumulator()
674
+ self.hooks = HookRegistry(model)
675
+ self.hooks.attach_forward(
676
+ self.module_patterns,
677
+ self.accumulator,
678
+ self.user_limits["act"],
679
+ self.dtype_ratio,
680
+ )
681
+ self.hooks.attach_backward(
682
+ self.accumulator, self.user_limits["grad"], self.dtype_ratio
683
+ )
684
+
685
+ self.pending_metrics: Optional[Dict[str, float]] = None
686
+
687
+ def _should_monitor(self, state) -> bool:
688
+ return state.global_step % self.monitor_every_n_steps == 0
689
+
690
+ def on_step_begin(self, args, state, control, **kwargs):
691
+ if self._should_monitor(state):
692
+ self.accumulator.clear()
693
+ self.hooks.set_active(True)
694
+
695
+ def on_step_end(self, args, state, control, **kwargs):
696
+ if not self.hooks.active:
697
+ return
698
+ for name, param in self.model.named_parameters():
699
+ stats = StatsEngine.compute(
700
+ param.data, self.user_limits["param"], self.dtype_ratio
701
+ )
702
+ self.accumulator.add(f"param.{name}", param.numel(), stats)
703
+
704
+ self.hooks.set_active(False)
705
+ self.pending_metrics = self._build_metrics()
706
+
707
+ @staticmethod
708
+ def _kind_of(name: str) -> str:
709
+ if name.startswith("act."):
710
+ return "act"
711
+ if name.startswith("grad."):
712
+ return "grad"
713
+ if name.startswith("param."):
714
+ return "param"
715
+ return "other"
716
+
717
+ @staticmethod
718
+ def _strip_kind(name: str) -> str:
719
+ if name.startswith("act."):
720
+ return name[4:]
721
+ if name.startswith(("grad.", "param.")):
722
+ return name[5:]
723
+ return name
724
+
725
+ def _build_metrics(self, scope: str = "train") -> Dict[str, float]:
726
+ metrics: Dict[str, float] = {}
727
+
728
+ if self.log_scope.get("global", True):
729
+ by_kind: Dict[str, Dict[str, Dict[str, float]]] = {}
730
+ for k, v in self.accumulator.tensors.items():
731
+ by_kind.setdefault(self._kind_of(k), {})[k] = v
732
+ for kind, entries in by_kind.items():
733
+ stats = self.accumulator._aggregate(entries)
734
+ for kk, vv in stats.items():
735
+ metrics[f"{scope}/global/{kind}/{kk}"] = vv
736
+
737
+ if self.log_scope.get("per_layer", True):
738
+ layer_prefixes = set()
739
+ for name in self.accumulator.tensors:
740
+ clean = self._strip_kind(name)
741
+ parts = clean.split(".")
742
+ for i, p in enumerate(parts):
743
+ if p == "layers" and i + 1 < len(parts):
744
+ prefix = ".".join(parts[: i + 2])
745
+ layer_prefixes.add(prefix)
746
+ for prefix in layer_prefixes:
747
+ by_kind: Dict[str, Dict[str, Dict[str, float]]] = {}
748
+ for k, v in self.accumulator.tensors.items():
749
+ clean = self._strip_kind(k)
750
+ if clean.startswith(prefix + ".") or clean == prefix:
751
+ by_kind.setdefault(self._kind_of(k), {})[k] = v
752
+ safe = prefix.replace(".", "_")
753
+ for kind, entries in by_kind.items():
754
+ if not entries:
755
+ continue
756
+ stats = self.accumulator._aggregate(entries)
757
+ for kk, vv in stats.items():
758
+ metrics[f"{scope}/layer_{safe}/{kind}/{kk}"] = vv
759
+
760
+ if self.log_scope.get("per_tensor", False):
761
+ for name, stats in self.accumulator.tensors.items():
762
+ safe = name.replace(".", "_")
763
+ for kk, vv in stats.items():
764
+ if kk == "numel":
765
+ continue
766
+ metrics[f"{scope}/tensor_{safe}/{kk}"] = vv
767
+
768
+ return metrics
769
+
770
+ def on_log(self, args, state, control, logs=None, **kwargs):
771
+ if logs is not None and self.pending_metrics is not None:
772
+ logs.update(self.pending_metrics)
773
+ self.pending_metrics = None
774
+
775
+ def on_prediction_step(self, args, state, control, **kwargs):
776
+ if not self.monitor_during_eval:
777
+ return
778
+ if not self.hooks.active:
779
+ self.accumulator.clear()
780
+ self.hooks.set_active(True)
781
+ for name, param in self.model.named_parameters():
782
+ stats = StatsEngine.compute(
783
+ param.data, self.user_limits["param"], self.dtype_ratio
784
+ )
785
+ self.accumulator.add(f"param.{name}", param.numel(), stats)
786
+ self.pending_metrics = self._build_metrics(scope="eval")
787
+
788
+ def on_evaluate(self, args, state, control, metrics=None, **kwargs):
789
+ self.hooks.set_active(False)
790
+ self.accumulator.clear()
791
+
792
+
793
+ class TimeTrackerCallback(TrainerCallback):
794
+ """Precise training & eval timing with remaining-time estimates."""
795
+
796
+ def __init__(self):
797
+ self.step_start: Optional[float] = None
798
+ self.epoch_start: Optional[float] = None
799
+ self.total_train_time = 0.0
800
+ self.step_times: List[float] = []
801
+
802
+ def on_epoch_begin(self, args, state, control, **kwargs):
803
+ self.epoch_start = time.perf_counter()
804
+
805
+ def on_step_begin(self, args, state, control, **kwargs):
806
+ self.step_start = time.perf_counter()
807
+
808
+ def on_step_end(self, args, state, control, **kwargs):
809
+ if self.step_start is not None:
810
+ dt = time.perf_counter() - self.step_start
811
+ self.step_times.append(dt)
812
+ self.total_train_time += dt
813
+ self.step_start = None
814
+
815
+ def on_log(self, args, state, control, logs=None, **kwargs):
816
+ if logs is None:
817
+ return
818
+ logs["train/total_time_seconds"] = self.total_train_time
819
+ if self.step_times:
820
+ recent = self.step_times[-100:]
821
+ logs["train/time_per_step_avg"] = sum(recent) / len(recent)
822
+ if self.epoch_start is not None:
823
+ logs["train/epoch_time_elapsed"] = time.perf_counter() - self.epoch_start
824
+ if state.max_steps and state.global_step > 0:
825
+ avg = self.total_train_time / state.global_step
826
+ remaining = (state.max_steps - state.global_step) * avg
827
+ logs["train/estimated_remaining_minutes"] = remaining / 60.0
828
+
829
+ def on_evaluate(self, args, state, control, metrics=None, **kwargs):
830
+ pass
831
+
832
+
833
+ class MetricsLoggerCallback(TrainerCallback):
834
+ """Persist every logged dict as JSONL in the output dir."""
835
+
836
+ def __init__(self, output_dir: str):
837
+ self.output_dir = Path(output_dir)
838
+ self.output_dir.mkdir(parents=True, exist_ok=True)
839
+ self.log_file = self.output_dir / "training_log.jsonl"
840
+
841
+ def on_log(self, args, state, control, logs=None, **kwargs):
842
+ if logs is None:
843
+ return
844
+ entry = {
845
+ "step": state.global_step,
846
+ "epoch": state.epoch,
847
+ "timestamp": time.time(),
848
+ **logs,
849
+ }
850
+ with open(self.log_file, "a") as f:
851
+ f.write(json.dumps(entry, default=str) + "\n")
852
+
853
+
854
+ # =============================================================================
855
+ # CONTAMINATION CALLBACK
856
+ # =============================================================================
857
+
858
+ class ContaminationCallback(TrainerCallback):
859
+ """
860
+ Intentionally corrupt input_ids and labels for a window of steps.
861
+ Modes: "shift" (+1 mod vocab), "random" (uniform random IDs).
862
+ """
863
+ def __init__(
864
+ self,
865
+ vocab_size: int,
866
+ enabled: bool = False,
867
+ start_step: int = 0,
868
+ duration_steps: int = 0,
869
+ mode: str = "shift",
870
+ fraction: float = 1.0,
871
+ seed: Optional[int] = None,
872
+ ):
873
+ self.vocab_size = vocab_size
874
+ self.enabled = enabled
875
+ self.start_step = start_step
876
+ self.duration_steps = duration_steps
877
+ self.mode = mode
878
+ self.fraction = fraction
879
+ self.seed = seed
880
+ self.generator = torch.Generator()
881
+ if seed is not None:
882
+ self.generator.manual_seed(seed)
883
+ self._active = False
884
+
885
+ def _should_corrupt(self, state) -> bool:
886
+ if not self.enabled:
887
+ return False
888
+ step = state.global_step
889
+ return self.start_step <= step < self.start_step + self.duration_steps
890
+
891
+ def on_step_begin(self, args, state, control, **kwargs):
892
+ if not self._should_corrupt(state):
893
+ return
894
+ batch = kwargs.get("inputs")
895
+ if batch is None or not isinstance(batch, dict):
896
+ return
897
+ input_ids = batch.get("input_ids")
898
+ labels = batch.get("labels")
899
+ attention_mask = batch.get("attention_mask")
900
+ if input_ids is None or labels is None:
901
+ return
902
+
903
+ device = input_ids.device
904
+ batch_size, seq_len = input_ids.shape
905
+
906
+ if self.fraction >= 1.0:
907
+ corrupt_mask = torch.ones((batch_size, seq_len), dtype=torch.bool, device=device)
908
+ elif self.fraction <= 0.0:
909
+ return
910
+ else:
911
+ rand = torch.rand((batch_size, seq_len), generator=self.generator, device=device)
912
+ corrupt_mask = rand < self.fraction
913
+
914
+ if attention_mask is not None:
915
+ padding_mask = attention_mask == 0
916
+ corrupt_mask = corrupt_mask & (~padding_mask)
917
+
918
+ if not corrupt_mask.any():
919
+ return
920
+
921
+ if self.mode == "shift":
922
+ input_ids_corrupt = (input_ids + 1) % self.vocab_size
923
+ labels_corrupt = (labels + 1) % self.vocab_size
924
+ elif self.mode == "random":
925
+ random_ids = torch.randint(
926
+ 0, self.vocab_size, input_ids.shape,
927
+ generator=self.generator, device=device
928
+ )
929
+ input_ids_corrupt = random_ids
930
+ labels_corrupt = random_ids
931
+ else:
932
+ raise ValueError(f"Unknown contamination mode: {self.mode}")
933
+
934
+ input_ids.masked_scatter_(corrupt_mask, input_ids_corrupt[corrupt_mask])
935
+ labels.masked_scatter_(corrupt_mask, labels_corrupt[corrupt_mask])
936
+ batch["input_ids"] = input_ids
937
+ batch["labels"] = labels
938
+
939
+ if not self._active:
940
+ print(f"[Contamination] Started at step {state.global_step} for {self.duration_steps} steps (mode={self.mode})")
941
+ self._active = True
942
+
943
+ def on_step_end(self, args, state, control, **kwargs):
944
+ if self._active and state.global_step >= self.start_step + self.duration_steps:
945
+ print(f"[Contamination] Ended at step {state.global_step}")
946
+ self._active = False
947
+
948
+
949
+ # =============================================================================
950
+ # 5. DATA & TRAINER FACTORY
951
+ # =============================================================================
952
+
953
+ def build_dataset(
954
+ tokenizer,
955
+ max_seq_len: int = 512,
956
+ split: str = "train",
957
+ dataset_name: str = "roneneldan/TinyStories",
958
+ max_samples: Optional[int] = None,
959
+ ):
960
+ """Concatenate and chunk TinyStories for causal LM."""
961
+ ds = load_dataset(dataset_name, split=split)
962
+ if max_samples is not None and split == "train":
963
+ ds = ds.select(range(min(max_samples, len(ds))))
964
+ print(f"[Dataset] Using first {len(ds)} samples for training (max_samples={max_samples})")
965
+
966
+ def tokenize(examples):
967
+ out = tokenizer(examples["text"], add_special_tokens=False)
968
+ eos_id = tokenizer.eos_token_id
969
+ out["input_ids"] = [ids + [eos_id] for ids in out["input_ids"]]
970
+ if "attention_mask" in out:
971
+ out["attention_mask"] = [mask + [1] for mask in out["attention_mask"]]
972
+ return out
973
+
974
+ tokenized = ds.map(
975
+ tokenize,
976
+ batched=True,
977
+ num_proc=4,
978
+ remove_columns=ds.column_names,
979
+ desc=f"Tokenizing {split}",
980
+ )
981
+
982
+ def group_texts(examples):
983
+ concatenated = {
984
+ k: list(chain.from_iterable(examples[k])) for k in examples.keys()
985
+ }
986
+ total_length = len(concatenated[list(examples.keys())[0]])
987
+ total_length = (total_length // max_seq_len) * max_seq_len
988
+ result = {
989
+ k: [t[i : i + max_seq_len] for i in range(0, total_length, max_seq_len)]
990
+ for k, t in concatenated.items()
991
+ }
992
+ result["labels"] = result["input_ids"].copy()
993
+ return result
994
+
995
+ return tokenized.map(
996
+ group_texts,
997
+ batched=True,
998
+ batch_size=10000,
999
+ num_proc=4,
1000
+ desc=f"Chunking {split}",
1001
+ )
1002
+
1003
+
1004
+ def create_trainer(
1005
+ model,
1006
+ tokenizer,
1007
+ config: Dict[str, Any],
1008
+ train_dataset,
1009
+ eval_dataset=None,
1010
+ ):
1011
+ """Assemble HF Trainer with all custom callbacks."""
1012
+ tc = config.get("training", {})
1013
+ mc = config.get("monitor", {})
1014
+
1015
+ run_name = tc.get("run_name", None)
1016
+
1017
+ args = TrainingArguments(
1018
+ output_dir=tc.get("output_dir", "./out"),
1019
+ run_name=run_name,
1020
+ num_train_epochs=tc.get("num_train_epochs", 3),
1021
+ per_device_train_batch_size=tc.get("per_device_train_batch_size", 16),
1022
+ per_device_eval_batch_size=tc.get("per_device_eval_batch_size", 16),
1023
+ gradient_accumulation_steps=tc.get("gradient_accumulation_steps", 4),
1024
+ learning_rate=tc.get("learning_rate", 3e-4),
1025
+ weight_decay=tc.get("weight_decay", 0.0),
1026
+ max_grad_norm=tc.get("max_grad_norm", 1.0),
1027
+ optim=tc.get("optim", "adamw_torch"),
1028
+ warmup_steps=tc.get("warmup_steps", 0),
1029
+ lr_scheduler_type=tc.get("lr_scheduler_type", "cosine"),
1030
+ bf16=tc.get("bf16", True),
1031
+ logging_steps=tc.get("logging_steps", 10),
1032
+ eval_strategy=tc.get("eval_strategy", "steps"),
1033
+ eval_steps=tc.get("eval_steps", 500),
1034
+ save_strategy=tc.get("save_strategy", "steps"),
1035
+ save_steps=tc.get("save_steps", 1000),
1036
+ load_best_model_at_end=tc.get("load_best_model_at_end", False),
1037
+ report_to=tc.get("report_to", "tensorboard"),
1038
+ push_to_hub=tc.get("push_to_hub", False),
1039
+ hub_model_id=tc.get("hub_model_id", None),
1040
+ hub_token=tc.get("hub_token") or os.environ.get("HF_TOKEN"),
1041
+ max_steps=tc.get("max_steps", -1),
1042
+ seed=tc.get("seed", 42),
1043
+ data_seed=tc.get("data_seed", 42),
1044
+ remove_unused_columns=False,
1045
+ )
1046
+
1047
+ callbacks = [TimeTrackerCallback()]
1048
+
1049
+ cc = config.get("contamination", {})
1050
+ if cc.get("enabled", False):
1051
+ vocab_size = model.config.vocab_size
1052
+ callbacks.append(
1053
+ ContaminationCallback(
1054
+ vocab_size=vocab_size,
1055
+ enabled=True,
1056
+ start_step=cc.get("start_step", 0),
1057
+ duration_steps=cc.get("duration_steps", 0),
1058
+ mode=cc.get("mode", "shift"),
1059
+ fraction=cc.get("fraction", 1.0),
1060
+ seed=cc.get("seed", None),
1061
+ )
1062
+ )
1063
+
1064
+ if mc.get("enabled", True):
1065
+ callbacks.append(
1066
+ StabilityMonitorCallback(
1067
+ model=model,
1068
+ monitor_every_n_steps=mc.get("monitor_every_n_steps", 10),
1069
+ module_patterns=mc.get("module_patterns", [".*mlp.*", ".*self_attn.*", ".*residual.*"]),
1070
+ user_limits=mc.get("user_limits", {"grad": 1.0, "param": 100.0, "act": 50.0}),
1071
+ dtype_proximity_ratio=mc.get("dtype_proximity_ratio", 0.9),
1072
+ log_scope=mc.get("log_scope", {"global": True, "per_layer": True, "per_tensor": False}),
1073
+ monitor_during_eval=mc.get("monitor_during_eval", False),
1074
+ )
1075
+ )
1076
+
1077
+ callbacks.append(MetricsLoggerCallback(args.output_dir))
1078
+
1079
+ collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
1080
+
1081
+ trainer = Trainer(
1082
+ model=model,
1083
+ args=args,
1084
+ train_dataset=train_dataset,
1085
+ eval_dataset=eval_dataset,
1086
+ data_collator=collator,
1087
+ callbacks=callbacks,
1088
+ )
1089
+
1090
+ try:
1091
+ from transformers.integrations import get_reporting_integration_callbacks
1092
+ reporting_types = tuple(get_reporting_integration_callbacks(args.report_to))
1093
+ except Exception:
1094
+ reporting_types = ()
1095
+
1096
+ if reporting_types:
1097
+ handler = trainer.callback_handler
1098
+ reporting_cbs = [cb for cb in handler.callbacks if isinstance(cb, reporting_types)]
1099
+ other_cbs = [cb for cb in handler.callbacks if not isinstance(cb, reporting_types)]
1100
+ handler.callbacks = other_cbs + reporting_cbs
1101
+
1102
+ return trainer
zain/Activation/llm_analyzer_wandb.py ADDED
@@ -0,0 +1,570 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multi-LLM Activation & Loss Analyzer with WandB Logging
3
+
4
+ Evaluates multiple language models on WikiText dataset,
5
+ computing per-tensor and global activation statistics (mean, max_abs, std, norm)
6
+ and logging to Weights & Biases with separate runs per model.
7
+
8
+ Requirements:
9
+ pip install transformers torch datasets wandb tqdm
10
+
11
+ Usage:
12
+ export WANDB_PROJECT="llm-activation-analysis"
13
+ export WANDB_API_KEY="your-key"
14
+ python llm_analyzer_wandb.py
15
+ """
16
+
17
+ import math
18
+ import torch
19
+ import torch.nn.functional as F
20
+ from transformers import AutoTokenizer, AutoModelForCausalLM, AutoConfig
21
+ from datasets import load_dataset
22
+ from typing import List, Dict, Optional, Union, Tuple
23
+ from dataclasses import dataclass, asdict
24
+ from collections import defaultdict
25
+ import json
26
+ import warnings
27
+ import os
28
+ from tqdm import tqdm
29
+
30
+ import wandb
31
+
32
+ warnings.filterwarnings("ignore")
33
+
34
+
35
+ @dataclass
36
+ class TensorStats:
37
+ mean: float
38
+ max_abs: float
39
+ std: float
40
+ norm: float
41
+ numel: int
42
+
43
+
44
+ @dataclass
45
+ class ModelResult:
46
+ model_name: str
47
+ loss: float
48
+ perplexity: float
49
+ global_act: TensorStats
50
+ layer_acts: Dict[str, TensorStats]
51
+ num_tokens: int
52
+ num_layers: int
53
+ hidden_size: int
54
+ num_params: int
55
+
56
+
57
+ class ActivationHookManager:
58
+ """Manages forward hooks to capture activations from every tensor."""
59
+
60
+ def __init__(self):
61
+ self.activations = {}
62
+ self.hooks = []
63
+ # Set once per batch via set_attention_mask(); used to exclude
64
+ # padding-token positions from activation statistics.
65
+ self._attention_mask: Optional[torch.Tensor] = None
66
+
67
+ def set_attention_mask(self, attention_mask: Optional[torch.Tensor]):
68
+ """Call once per batch before the forward pass so hooks can mask
69
+ out padding positions when computing stats."""
70
+ self._attention_mask = (
71
+ attention_mask.detach().cpu() if attention_mask is not None else None
72
+ )
73
+
74
+ def _make_hook(self, name: str):
75
+ def hook(module, input, output):
76
+ # Handle different output types
77
+ if isinstance(output, torch.Tensor):
78
+ tensor = output
79
+ elif isinstance(output, tuple) and isinstance(output[0], torch.Tensor):
80
+ tensor = output[0]
81
+ else:
82
+ return
83
+
84
+ # Detach and move to CPU to avoid GPU memory blowup
85
+ self.activations[name] = tensor.detach().cpu().float()
86
+ return hook
87
+
88
+ def register_hooks(self, model: torch.nn.Module):
89
+ """Register hooks on all modules that produce activations."""
90
+ for name, module in model.named_modules():
91
+ # Skip trivial containers
92
+ if len(list(module.children())) == 0 and hasattr(module, 'forward'):
93
+ hook = module.register_forward_hook(self._make_hook(name))
94
+ self.hooks.append(hook)
95
+
96
+ def clear(self):
97
+ self.activations.clear()
98
+
99
+ def remove_hooks(self):
100
+ for hook in self.hooks:
101
+ hook.remove()
102
+ self.hooks.clear()
103
+
104
+ def _select_real_tokens(self, tensor: torch.Tensor) -> torch.Tensor:
105
+ """
106
+ BUG FIX: previously every captured tensor (including activations at
107
+ padding-token positions) was flattened and used as-is. With
108
+ padding_side="left" and a small batch_size, the padding fraction
109
+ varies a lot batch-to-batch, so padding-token activations (which
110
+ are real, non-zero values — not zeros) were silently mixed into
111
+ mean/std/max_abs/norm, biasing exactly the saturation signal this
112
+ script exists to measure.
113
+
114
+ Here we mask out padding positions whenever a tensor's shape is
115
+ consistent with (batch, seq_len, ...) against the stored
116
+ attention_mask (batch, seq_len). Tensors that don't match that
117
+ shape (e.g. a module operating on the pooled/final dimension only)
118
+ are left as-is rather than guessing.
119
+ """
120
+ mask = self._attention_mask
121
+ if mask is None or tensor.dim() < 2:
122
+ return tensor.reshape(-1)
123
+ if tensor.shape[0] != mask.shape[0] or tensor.shape[1] != mask.shape[1]:
124
+ return tensor.reshape(-1)
125
+
126
+ bool_mask = mask.bool()
127
+ # Expand mask across any trailing dims (e.g. hidden_size) and select.
128
+ expand_shape = bool_mask.shape + (1,) * (tensor.dim() - 2)
129
+ bool_mask = bool_mask.view(expand_shape).expand_as(tensor)
130
+ return tensor[bool_mask].reshape(-1)
131
+
132
+ def compute_stats(self) -> Dict[str, TensorStats]:
133
+ """Compute statistics for all captured activations, excluding
134
+ padding-token positions where identifiable."""
135
+ stats = {}
136
+ for name, tensor in self.activations.items():
137
+ if tensor.numel() == 0:
138
+ continue
139
+ flat = self._select_real_tokens(tensor)
140
+ if flat.numel() == 0:
141
+ continue
142
+ stats[name] = TensorStats(
143
+ mean=flat.mean().item(),
144
+ max_abs=flat.abs().max().item(),
145
+ std=flat.std().item(),
146
+ norm=flat.norm().item(),
147
+ numel=flat.numel()
148
+ )
149
+ return stats
150
+
151
+
152
+ class LLMAnalyzer:
153
+ def __init__(
154
+ self,
155
+ device: Optional[str] = None,
156
+ max_length: int = 512,
157
+ max_samples: int = 1000, # number of wikitext samples to eval
158
+ batch_size: int = 4,
159
+ dtype: torch.dtype = torch.float16,
160
+ wandb_project: Optional[str] = None,
161
+ ):
162
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
163
+ self.max_length = max_length
164
+ self.max_samples = max_samples
165
+ self.batch_size = batch_size
166
+ self.dtype = dtype if self.device == "cuda" else torch.float32
167
+ self.wandb_project = wandb_project or os.environ.get("WANDB_PROJECT", "llm-activation-analysis")
168
+ self._cache = {}
169
+
170
+ def load_dataset(self, split: str = "test"):
171
+ """Load Salesforce/wikitext dataset."""
172
+ print(f"[Dataset] Loading Salesforce/wikitext ({split}) ...")
173
+ ds = load_dataset("Salesforce/wikitext", "wikitext-2-raw-v1", split=split)
174
+ # Filter out empty lines
175
+ texts = [t for t in ds["text"] if len(t.strip()) > 50]
176
+ print(f"[Dataset] Loaded {len(texts)} non-empty samples")
177
+ return texts[:self.max_samples]
178
+
179
+ def load_model(self, model_name: str):
180
+ """Load model and tokenizer with caching."""
181
+ if model_name in self._cache:
182
+ return self._cache[model_name]
183
+
184
+ print(f"[Loading] {model_name} ...")
185
+
186
+ tokenizer = AutoTokenizer.from_pretrained(
187
+ model_name,
188
+ trust_remote_code=True,
189
+ padding_side="left"
190
+ )
191
+ if tokenizer.pad_token is None:
192
+ tokenizer.pad_token = tokenizer.eos_token
193
+
194
+ config = AutoConfig.from_pretrained(model_name, trust_remote_code=True)
195
+
196
+ model = AutoModelForCausalLM.from_pretrained(
197
+ model_name,
198
+ config=config,
199
+ torch_dtype=self.dtype,
200
+ device_map="auto" if self.device == "cuda" else None,
201
+ trust_remote_code=True,
202
+ )
203
+
204
+ if self.device == "cpu":
205
+ model = model.to(self.device)
206
+
207
+ model.eval()
208
+
209
+ num_params = sum(p.numel() for p in model.parameters())
210
+
211
+ self._cache[model_name] = (tokenizer, model, config, num_params)
212
+ print(f"[Loaded] {model_name} | Params: {num_params/1e6:.1f}M | Layers: {config.num_hidden_layers} | Hidden: {config.hidden_size}")
213
+ return tokenizer, model, config, num_params
214
+
215
+ def compute(
216
+ self,
217
+ model_names: List[str],
218
+ ) -> List[ModelResult]:
219
+ """
220
+ Compute loss and per-tensor activation statistics for multiple models.
221
+ Logs each model as a separate WandB run.
222
+ """
223
+ texts = self.load_dataset()
224
+ results = []
225
+
226
+ for model_name in model_names:
227
+ try:
228
+ result = self._evaluate_model(model_name, texts)
229
+ results.append(result)
230
+ except Exception as e:
231
+ print(f"[Error] {model_name}: {e}")
232
+ import traceback
233
+ traceback.print_exc()
234
+ continue
235
+
236
+ return results
237
+
238
+ def _evaluate_model(
239
+ self,
240
+ model_name: str,
241
+ texts: List[str],
242
+ ) -> ModelResult:
243
+ tokenizer, model, config, num_params = self.load_model(model_name)
244
+
245
+ # Initialize WandB run for this model
246
+ run_name = model_name.replace("/", "-")
247
+ wandb.init(
248
+ project=self.wandb_project,
249
+ name=run_name,
250
+ config={
251
+ "model": model_name,
252
+ "max_length": self.max_length,
253
+ "max_samples": self.max_samples,
254
+ "batch_size": self.batch_size,
255
+ "dtype": str(self.dtype),
256
+ "num_params": num_params,
257
+ "num_layers": config.num_hidden_layers,
258
+ "hidden_size": config.hidden_size,
259
+ },
260
+ reinit=True
261
+ )
262
+
263
+ hook_mgr = ActivationHookManager()
264
+ hook_mgr.register_hooks(model)
265
+
266
+ total_loss = 0.0
267
+ total_tokens = 0
268
+
269
+ # Global activation accumulator
270
+ global_acts = []
271
+
272
+ # Per-layer activation accumulators
273
+ # We'll aggregate stats across batches, then compute final stats
274
+ layer_act_values = defaultdict(list)
275
+
276
+ num_batches = (len(texts) + self.batch_size - 1) // self.batch_size
277
+
278
+ for i in tqdm(range(0, len(texts), self.batch_size), desc=f"Eval {run_name}", total=num_batches):
279
+ batch_texts = texts[i:i + self.batch_size]
280
+
281
+ # Tokenize
282
+ inputs = tokenizer(
283
+ batch_texts,
284
+ return_tensors="pt",
285
+ truncation=True,
286
+ max_length=self.max_length,
287
+ padding=True
288
+ )
289
+
290
+ # Move to device
291
+ if self.device == "cuda" and hasattr(model, "device"):
292
+ # model is on auto device map
293
+ input_ids = inputs["input_ids"]
294
+ if hasattr(model, "device") and model.device != torch.device("meta"):
295
+ input_ids = input_ids.to(model.device)
296
+ attention_mask = inputs.get("attention_mask")
297
+ if attention_mask is not None:
298
+ attention_mask = attention_mask.to(input_ids.device)
299
+ else:
300
+ input_ids = inputs["input_ids"].to(self.device)
301
+ attention_mask = inputs.get("attention_mask")
302
+ if attention_mask is not None:
303
+ attention_mask = attention_mask.to(self.device)
304
+
305
+ labels = input_ids.clone()
306
+
307
+ # BUG FIX: with padding_side="left" and no explicit position_ids,
308
+ # HF's default `position_ids = arange(seq_len)` is applied
309
+ # identically to every row in the batch, regardless of how much
310
+ # left-padding precedes the real tokens in that row (verified
311
+ # against transformers' LlamaModel.forward / GPT2Model.forward
312
+ # source — neither adjusts for padding when position_ids=None).
313
+ # That means a real token's absolute position (and therefore its
314
+ # RoPE rotation / absolute position embedding) depends on how
315
+ # much padding happened to precede it in this particular batch,
316
+ # not on its logical position within its own sequence. This
317
+ # silently corrupts logits -> loss -> perplexity, with the
318
+ # amount of corruption varying batch-to-batch. Fix: derive
319
+ # position_ids from attention_mask so they restart at 0 for the
320
+ # first real token of every row, and are stable (0) on padding.
321
+ if attention_mask is not None:
322
+ position_ids = attention_mask.long().cumsum(-1) - 1
323
+ position_ids.masked_fill_(attention_mask == 0, 0)
324
+ else:
325
+ position_ids = None
326
+
327
+ hook_mgr.set_attention_mask(attention_mask)
328
+
329
+ with torch.no_grad():
330
+ outputs = model(
331
+ input_ids=input_ids,
332
+ attention_mask=attention_mask,
333
+ position_ids=position_ids,
334
+ labels=labels,
335
+ )
336
+
337
+ # --- Loss Computation ---
338
+ logits = outputs.logits
339
+ shift_logits = logits[..., :-1, :].contiguous()
340
+ shift_labels = labels[..., 1:].contiguous()
341
+ shift_mask = attention_mask[..., 1:].contiguous() if attention_mask is not None else None
342
+
343
+ loss_fct = torch.nn.CrossEntropyLoss(reduction="none")
344
+ token_losses = loss_fct(
345
+ shift_logits.view(-1, shift_logits.size(-1)),
346
+ shift_labels.view(-1)
347
+ )
348
+
349
+ if shift_mask is not None:
350
+ token_losses = token_losses * shift_mask.view(-1)
351
+ num_valid_tokens = shift_mask.sum().item()
352
+ else:
353
+ num_valid_tokens = token_losses.numel()
354
+
355
+ batch_loss = token_losses.sum().item()
356
+ total_loss += batch_loss
357
+ total_tokens += num_valid_tokens
358
+
359
+ # --- Activation Statistics ---
360
+ # Get activations captured by hooks
361
+ act_stats = hook_mgr.compute_stats()
362
+
363
+ for name, stats in act_stats.items():
364
+ # Skip dtype-limit fraction metrics entirely
365
+ # We only log mean, max_abs, std, norm
366
+
367
+ # For global: aggregate raw values
368
+ # We can't store all raw values due to memory, so we store running sums
369
+ # But for accurate std across all batches, we need a streaming algorithm
370
+ # For simplicity and correctness, we'll store per-batch stats and weight them
371
+ layer_act_values[name].append(asdict(stats))
372
+
373
+ hook_mgr.clear()
374
+
375
+ # Log per-batch metrics to wandb
376
+ if num_valid_tokens > 0:
377
+ batch_avg_loss = batch_loss / num_valid_tokens
378
+ wandb.log({
379
+ "batch_loss": batch_avg_loss,
380
+ "batch_perplexity": torch.exp(torch.tensor(batch_avg_loss)).item(),
381
+ "batch_tokens": num_valid_tokens,
382
+ "progress": i / len(texts)
383
+ }, step=i)
384
+
385
+ hook_mgr.remove_hooks()
386
+
387
+ # --- Final Aggregation ---
388
+ avg_loss = total_loss / max(total_tokens, 1)
389
+ perplexity = torch.exp(torch.tensor(avg_loss)).item()
390
+
391
+ # Aggregate per-tensor stats across all batches
392
+ # Weighted by numel for mean, max for max_abs, pooled std, pooled norm
393
+ final_layer_stats = {}
394
+
395
+ for name, batch_stats_list in layer_act_values.items():
396
+ total_numel = sum(s["numel"] for s in batch_stats_list)
397
+ if total_numel == 0:
398
+ continue
399
+
400
+ # Weighted mean
401
+ weighted_mean = sum(s["mean"] * s["numel"] for s in batch_stats_list) / total_numel
402
+
403
+ # Max abs across all batches
404
+ max_abs = max(s["max_abs"] for s in batch_stats_list)
405
+
406
+ # BUG FIX: the previous formula (weighted average of per-batch
407
+ # variances only) drops the between-batch term that accounts for
408
+ # per-batch means differing from the global mean. Whenever batch
409
+ # means differ (they will — different texts, different lengths),
410
+ # this systematically UNDERESTIMATES the true global std — in a
411
+ # quick numeric test with two batches of different means this was
412
+ # off by ~2.8x. Correct pooled-variance formula (population form,
413
+ # matches exp.py's StepAccumulator._merge_entry):
414
+ # E[X^2] = weighted_avg(var_i + mean_i^2)
415
+ # Var(X) = E[X^2] - mean_global^2
416
+ ex2 = sum(
417
+ s["numel"] * (s["std"] ** 2 + s["mean"] ** 2) for s in batch_stats_list
418
+ ) / total_numel
419
+ pooled_std = math.sqrt(max(0.0, ex2 - weighted_mean ** 2))
420
+
421
+ # Norm: sqrt(sum of squared norms / total_numel) * sqrt(total_numel)
422
+ # Actually norm^2 = sum(x_i^2), so pooled_norm = sqrt(sum(norm_i^2))
423
+ pooled_norm = (sum(s["norm"] ** 2 for s in batch_stats_list)) ** 0.5
424
+
425
+ final_layer_stats[name] = TensorStats(
426
+ mean=weighted_mean,
427
+ max_abs=max_abs,
428
+ std=pooled_std,
429
+ norm=pooled_norm,
430
+ numel=total_numel
431
+ )
432
+
433
+ # Compute global stats across all layers
434
+ if final_layer_stats:
435
+ all_numel = sum(s.numel for s in final_layer_stats.values())
436
+ global_mean = sum(s.mean * s.numel for s in final_layer_stats.values()) / all_numel
437
+ global_max_abs = max(s.max_abs for s in final_layer_stats.values())
438
+ # Same pooled-std correction as above — same bug was present here.
439
+ global_ex2 = sum(
440
+ s.numel * (s.std ** 2 + s.mean ** 2) for s in final_layer_stats.values()
441
+ ) / all_numel
442
+ global_std = math.sqrt(max(0.0, global_ex2 - global_mean ** 2))
443
+ global_norm = (sum(s.norm ** 2 for s in final_layer_stats.values())) ** 0.5
444
+
445
+ global_stats = TensorStats(
446
+ mean=global_mean,
447
+ max_abs=global_max_abs,
448
+ std=global_std,
449
+ norm=global_norm,
450
+ numel=all_numel
451
+ )
452
+ else:
453
+ global_stats = TensorStats(0.0, 0.0, 0.0, 0.0, 0)
454
+
455
+ result = ModelResult(
456
+ model_name=model_name,
457
+ loss=avg_loss,
458
+ perplexity=perplexity,
459
+ global_act=global_stats,
460
+ layer_acts=final_layer_stats,
461
+ num_tokens=total_tokens,
462
+ num_layers=config.num_hidden_layers,
463
+ hidden_size=config.hidden_size,
464
+ num_params=num_params
465
+ )
466
+
467
+ # --- WandB Logging ---
468
+ self._log_to_wandb(result)
469
+ wandb.finish()
470
+
471
+ return result
472
+
473
+ def _log_to_wandb(self, result: ModelResult):
474
+ """Log final metrics to WandB. No frac_near_dtype_limit."""
475
+
476
+ # Global metrics
477
+ wandb.log({
478
+ "final/loss": result.loss,
479
+ "final/perplexity": result.perplexity,
480
+ "final/num_tokens": result.num_tokens,
481
+
482
+ "train/global/act/mean": result.global_act.mean,
483
+ "train/global/act/max_abs": result.global_act.max_abs,
484
+ "train/global/act/std": result.global_act.std,
485
+ "train/global/act/norm": result.global_act.norm,
486
+ # Intentionally NOT logging frac_near_dtype_limit or frac_near_user_limit
487
+ })
488
+
489
+ # Per-tensor (per-layer) metrics
490
+ # Organize by layer for cleaner WandB UI
491
+ for tensor_name, stats in result.layer_acts.items():
492
+ # Clean name for wandb: replace dots with slashes
493
+ clean_name = tensor_name.replace(".", "/")
494
+
495
+ wandb.log({
496
+ f"train/{clean_name}/act/mean": stats.mean,
497
+ f"train/{clean_name}/act/max_abs": stats.max_abs,
498
+ f"train/{clean_name}/act/std": stats.std,
499
+ f"train/{clean_name}/act/norm": stats.norm,
500
+ # No frac_near_dtype_limit
501
+ })
502
+
503
+ # Also log as a wandb.Table for easy comparison
504
+ table_data = []
505
+ for tensor_name, stats in sorted(result.layer_acts.items()):
506
+ table_data.append([
507
+ tensor_name,
508
+ stats.mean,
509
+ stats.max_abs,
510
+ stats.std,
511
+ stats.norm,
512
+ stats.numel
513
+ ])
514
+
515
+ if table_data:
516
+ table = wandb.Table(
517
+ columns=["tensor_name", "mean", "max_abs", "std", "norm", "numel"],
518
+ data=table_data
519
+ )
520
+ wandb.log({"activation_table": table})
521
+
522
+ def print_report(self, results: List[ModelResult]):
523
+ """Pretty-print comparison report."""
524
+ print("\n" + "=" * 110)
525
+ print(f"{'Model':<35} {'Loss':>10} {'PPL':>10} {'ActMean':>12} {'ActMaxAbs':>12} {'ActStd':>12} {'Tokens':>8}")
526
+ print("-" * 110)
527
+
528
+ for r in results:
529
+ name = r.model_name.split("/")[-1][:33]
530
+ print(
531
+ f"{name:<35} "
532
+ f"{r.loss:>10.4f} "
533
+ f"{r.perplexity:>10.2f} "
534
+ f"{r.global_act.mean:>12.6f} "
535
+ f"{r.global_act.max_abs:>12.6f} "
536
+ f"{r.global_act.std:>12.6f} "
537
+ f"{r.num_tokens:>8}"
538
+ )
539
+
540
+ print("=" * 110)
541
+
542
+ # Print top 5 layers by max_abs for each model
543
+ print("\n[Per-Tensor Max Abs Top 5]")
544
+ for r in results:
545
+ name = r.model_name.split("/")[-1]
546
+ sorted_layers = sorted(r.layer_acts.items(), key=lambda x: x[1].max_abs, reverse=True)[:5]
547
+ print(f"\n {name}:")
548
+ for tensor_name, stats in sorted_layers:
549
+ print(f" {tensor_name:<50} max_abs={stats.max_abs:>10.4f} mean={stats.mean:>10.6f} std={stats.std:>10.4f}")
550
+
551
+ def export_json(self, results: List[ModelResult], path: str):
552
+ """Export results to JSON."""
553
+ data = []
554
+ for r in results:
555
+ entry = {
556
+ "model": r.model_name,
557
+ "loss": r.loss,
558
+ "perplexity": r.perplexity,
559
+ "num_tokens": r.num_tokens,
560
+ "num_layers": r.num_layers,
561
+ "hidden_size": r.hidden_size,
562
+ "num_params": r.num_params,
563
+ "global_act": asdict(r.global_act),
564
+ "layer_acts": {k: asdict(v) for k, v in r.layer_acts.items()}
565
+ }
566
+ data.append(entry)
567
+
568
+ with open(path, "w") as f:
569
+ json.dump(data, f, indent=2)
570
+ print(f"[Exported] Results saved to {path}")
zain/Activation/sweep.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Sweep explicit GLU / MLP variants with identical data and hyperparameters."""
3
+ import argparse
4
+ import copy
5
+ import json
6
+ import re
7
+ import time
8
+ import os
9
+ from pathlib import Path
10
+
11
+ import yaml
12
+ import wandb
13
+ import torch
14
+ from transformers import AutoTokenizer, set_seed
15
+ from exp import TinyLlamaConfig, TinyLlamaForCausalLM, build_dataset, create_trainer
16
+
17
+
18
+ def format_param_count(total_params: int) -> str:
19
+ """Return human‑readable string with M or B suffix, 1 decimal."""
20
+ if total_params >= 1e9:
21
+ return f"{total_params / 1e9:.1f}B"
22
+ else:
23
+ return f"{total_params / 1e6:.1f}M"
24
+
25
+
26
+ def parse_variant(variant: str):
27
+ """
28
+ Parse variant string into (prefix, activation, layers).
29
+ Supports:
30
+ glu-silu-waleed10
31
+ glu-situglu_low-10L
32
+ mlp-waleed10
33
+ """
34
+ parts = variant.split('-')
35
+ if len(parts) < 2:
36
+ raise ValueError(f"Invalid variant format: '{variant}'. Expected: <glu|mlp>-<activation>[-<layers>L]")
37
+
38
+ prefix = parts[0]
39
+ if prefix not in ('glu', 'mlp'):
40
+ raise ValueError(f"Invalid prefix: '{prefix}'. Must be 'glu' or 'mlp'.")
41
+
42
+ last = parts[-1]
43
+ if last.endswith('L') and last[:-1].isdigit():
44
+ layers = int(last[:-1])
45
+ activation = '-'.join(parts[1:-1])
46
+ else:
47
+ layers = None
48
+ activation = '-'.join(parts[1:])
49
+
50
+ if not activation:
51
+ raise ValueError(f"Missing activation name in variant: '{variant}'")
52
+
53
+ return prefix, activation, layers
54
+
55
+
56
+ def main():
57
+ parser = argparse.ArgumentParser()
58
+ parser.add_argument("--config", required=True, help="Base YAML config")
59
+ parser.add_argument(
60
+ "--variants",
61
+ nargs="+",
62
+ required=True,
63
+ help="List of variants: e.g. glu-silu-10L mlp-relu-8L"
64
+ )
65
+ parser.add_argument("--push", action="store_true")
66
+ args = parser.parse_args()
67
+
68
+ with open(args.config) as f:
69
+ base = yaml.safe_load(f)
70
+
71
+ seed = base.get("training", {}).get("seed", 42)
72
+ set_seed(seed)
73
+
74
+ # Force all runs into a specific WandB project (read from config)
75
+ wandb_project = base.get("training", {}).get("wandb_project")
76
+ if wandb_project:
77
+ os.environ["WANDB_PROJECT"] = wandb_project
78
+ print(f"[WandB] Project locked to: {wandb_project}")
79
+
80
+ tok_name = base["model"].get("tokenizer_name", "meta-llama/Llama-2-7b-hf")
81
+ tokenizer = AutoTokenizer.from_pretrained(tok_name)
82
+ if tokenizer.pad_token is None:
83
+ tokenizer.pad_token = tokenizer.eos_token
84
+
85
+ msl = base["model"].get("max_position_embeddings", 512)
86
+ train_ds = build_dataset(tokenizer, max_seq_len=msl, split="train", max_samples=None)
87
+ eval_ds = build_dataset(tokenizer, max_seq_len=msl, split="validation", max_samples=None)
88
+
89
+ results = []
90
+
91
+ for variant in args.variants:
92
+ prefix, act, layers = parse_variant(variant)
93
+
94
+ if prefix == "mlp" and act in ("situglu", "waleed", "situglu_low", "waleedglu_low"):
95
+ raise ValueError(
96
+ f"Activation '{act}' requires a gated architecture (GLU). "
97
+ f"Please use 'glu-{act}' instead."
98
+ )
99
+
100
+ cfg = copy.deepcopy(base)
101
+ cfg["model"]["mlp_type"] = prefix
102
+ cfg["model"]["activation"] = act
103
+ if layers is not None:
104
+ cfg["model"]["num_hidden_layers"] = layers
105
+
106
+ # --------------------------------------------------------------
107
+ # ENSURE LAYER COUNT IS ALWAYS INCLUDED IN ALL NAMES
108
+ # Read the final layer count from the config after any override.
109
+ # --------------------------------------------------------------
110
+ actual_layers = cfg["model"]["num_hidden_layers"]
111
+ variant_label = f"{prefix}-{act}-{actual_layers}L"
112
+
113
+ out_dir = Path(cfg["training"]["output_dir"]).parent / f"{variant_label}_run"
114
+ cfg["training"]["output_dir"] = str(out_dir)
115
+
116
+ set_seed(seed)
117
+
118
+ print(f"\n{'='*60}\n>>> Variant: {variant_label} | Out: {out_dir}\n{'='*60}")
119
+
120
+ config = TinyLlamaConfig(**cfg["model"])
121
+ model = TinyLlamaForCausalLM(config)
122
+ model = model.to(torch.bfloat16)
123
+
124
+ total_params = sum(p.numel() for p in model.parameters())
125
+ param_str = format_param_count(total_params)
126
+ timestamp = time.strftime("%Y%m%d-%H%M%S")
127
+ run_name = f"LM-{variant_label}-{param_str}-{timestamp}"
128
+ cfg["training"]["run_name"] = run_name
129
+
130
+ hub_id_base = cfg["training"].get("hub_model_id", "tiny-llama-lab")
131
+ cfg["training"]["hub_model_id"] = f"{hub_id_base}-{variant_label}"
132
+
133
+ os.environ.pop("WANDB_RUN_ID", None)
134
+
135
+ trainer = create_trainer(model, tokenizer, cfg, train_ds, eval_ds)
136
+
137
+ try:
138
+ trainer.train()
139
+ metrics = trainer.evaluate()
140
+ results.append({
141
+ "variant": variant_label,
142
+ "eval_loss": metrics.get("eval_loss"),
143
+ "out": str(out_dir),
144
+ "run_name": run_name,
145
+ })
146
+ trainer.save_model(str(out_dir))
147
+ if args.push or cfg["training"].get("push_to_hub", False):
148
+ trainer.push_to_hub()
149
+ finally:
150
+ wandb.finish()
151
+
152
+ summary = Path(base["training"]["output_dir"]).parent / "sweep_summary.json"
153
+ summary.write_text(json.dumps(results, indent=2))
154
+ print("\nSweep complete:")
155
+ for r in results:
156
+ print(f" {r['variant']:20s} eval_loss={r['eval_loss']:.4f}")
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()
zain/Activation/train.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Train one TinyLlama variant from a YAML config."""
3
+ import argparse
4
+ import yaml
5
+ import os
6
+ import torch
7
+
8
+ from transformers import AutoTokenizer, set_seed
9
+ from exp import TinyLlamaConfig, TinyLlamaForCausalLM, build_dataset, create_trainer
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser()
14
+ parser.add_argument("--config", required=True, help="Path to YAML config")
15
+ parser.add_argument("--push", action="store_true", help="Push final model to HF Hub")
16
+ args = parser.parse_args()
17
+
18
+ with open(args.config) as f:
19
+ cfg = yaml.safe_load(f)
20
+
21
+ seed = cfg.get("training", {}).get("seed", 42)
22
+ set_seed(seed)
23
+
24
+ # -------------------------------------------------------------------------
25
+ # LOCK WANDB PROJECT: read from config, force into environment.
26
+ # -------------------------------------------------------------------------
27
+ wandb_project = cfg.get("training", {}).get("wandb_project")
28
+ if wandb_project:
29
+ os.environ["WANDB_PROJECT"] = wandb_project
30
+ print(f"[WandB] Project locked to: {wandb_project}")
31
+ # -------------------------------------------------------------------------
32
+
33
+ model_cfg = cfg["model"]
34
+ train_cfg = cfg.get("training", {})
35
+
36
+ tok_name = model_cfg.pop("tokenizer_name", "meta-llama/Llama-2-7b-hf")
37
+ tokenizer = AutoTokenizer.from_pretrained(tok_name)
38
+ if tokenizer.pad_token is None:
39
+ tokenizer.pad_token = tokenizer.eos_token
40
+
41
+ tiny_config = TinyLlamaConfig(**model_cfg)
42
+ model = TinyLlamaForCausalLM(tiny_config)
43
+ model = model.to(torch.bfloat16)
44
+
45
+ n_params = sum(p.numel() for p in model.parameters()) / 1e6
46
+ print(f"Model: {n_params:.2f}M params | MLP type: {tiny_config.mlp_type} | Activation: {tiny_config.activation}")
47
+
48
+ msl = model_cfg.get("max_position_embeddings", 512)
49
+ train_ds = build_dataset(tokenizer, max_seq_len=msl, split="train", max_samples=None)
50
+ eval_ds = build_dataset(tokenizer, max_seq_len=msl, split="validation", max_samples=None)
51
+
52
+ trainer = create_trainer(model, tokenizer, cfg, train_ds, eval_ds)
53
+ trainer.train()
54
+
55
+ out = train_cfg.get("output_dir", "./out")
56
+ trainer.save_model(out)
57
+ if args.push or train_cfg.get("push_to_hub", False):
58
+ trainer.push_to_hub()
59
+ print(f"Done. Artifacts in {out}")
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()