Paerle commited on
Commit
d8c733f
·
verified ·
1 Parent(s): 5220486

Initial upload: GUIDO-small 200M bugfix ckpt + Vathos + modeling + README

Browse files
README.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ library_name: pytorch
4
+ tags:
5
+ - math
6
+ - code
7
+ - pretraining
8
+ - custom-architecture
9
+ - vathos
10
+ - pico
11
+ language:
12
+ - en
13
+ pipeline_tag: text-generation
14
+ ---
15
+
16
+ # GUIDO-small 200M (bugfixed)
17
+
18
+ A 195M-parameter math/code-oriented language model, pretrained from scratch on a math-heavy corpus.
19
+ Custom architecture (Vathos PiCOFormer): gated attention, smear-gate, identity-at-init, UDLP MLP,
20
+ RoPE, tied embeddings, fp32-master weights with bf16 autocast.
21
+
22
+ > ⚠️ This is a **research checkpoint**. Architecture is custom (not HF-compatible) — requires the
23
+ > bundled `vathos/` package + `modeling_guido.py` to load.
24
+
25
+ ## Eval
26
+
27
+ | | greedy | maj@16 (T=0.6, top_p=0.95, top_k=40) | pass@16 |
28
+ |---|---|---|---|
29
+ | **GSM8K** | 43.0% | **57.5%** | 77.5% |
30
+ | **MATH-500** | 21.0% | **30.0%** | 53.75% |
31
+
32
+ Numbers from `eval_distill.py` (greedy, N=200/100) and `eval_sc.py` (maj@16, N=80/80).
33
+
34
+ ## Architecture
35
+
36
+ - **Shape**: 24 layers × 768 d_model × 12 heads × 64 head_dim × 3072 d_ff
37
+ - **Vocab**: 32,768 (Mathstral SPM tokenizer)
38
+ - **Position**: RoPE base 1e6, max_len 8192
39
+ - **Norm**: RMSNorm, qk-norm
40
+ - **Attention**: Multihead Gated (Modded-NanoGPT PR#117 style sparse output gate, gate_input_dim=12)
41
+ - **MLP**: VariableUDLP with LeakyReLU² activation, identity-at-init
42
+ - **Logit softcap**: 30.0
43
+ - **Tied embedding**: yes
44
+ - **Total params**: 195,092,992 (~195M)
45
+
46
+ ## Training
47
+
48
+ - **Tokens seen**: 4.19B (16000 iter × 4 GPUs × 32 batch × 2048 seq)
49
+ - **Optimizer**: Muon (Newton-Schulz5) on 2D matrices (lr=0.01) + Adam on embedding (lr=0.005) + scalars (lr=0.04)
50
+ - **Schedule**: WSD (warmup 300 → plateau → linear warmdown last 60% to lr×1e-3)
51
+ - **Precision**: fp32 master weights + bf16 autocast (cce kernel gets bf16 cast at boundary)
52
+ - **Hardware**: 4× A100 64GB (Leonardo CINECA), ~3h wall
53
+
54
+ ### Data mixture (corpus_v2)
55
+
56
+ | weight | shard | tokens (B) |
57
+ |---|---|---|
58
+ | 0.34 | OpenMathInstruct-2 (14M docs) | 6.79 |
59
+ | 0.14 | TinyGSM (full) | 2.70 |
60
+ | 0.10 | OpenMathReasoning subset | 1.50 |
61
+ | 0.07 | NuminaMath-1.5 | 0.41 |
62
+ | 0.05 | NuminaMath-CoT | 0.48 |
63
+ | 0.22 | FineWeb-Edu score≥3 (sample-10BT) | 3.00 |
64
+ | 0.08 | Cosmopedia subset | 1.19 |
65
+
66
+ Math 70% / NL 30%. Tokenizer: `mistralai/Mathstral-7B-v0.1`. Decontamination: canonical-hash vs MATH-500 and GSM8K test (note: not fuzzy-decon).
67
+
68
+ ## How to load
69
+
70
+ ### Quick start
71
+
72
+ ```bash
73
+ pip install -r requirements.txt
74
+ ```
75
+
76
+ ```python
77
+ from huggingface_hub import snapshot_download
78
+ local_dir = snapshot_download(repo_id="Paerle/GUIDO_test_200M", token="<your_token>")
79
+
80
+ import sys, torch
81
+ sys.path.insert(0, local_dir + "/vathos")
82
+ sys.path.insert(0, local_dir)
83
+ import modeling_guido as m
84
+ from transformers import AutoTokenizer
85
+
86
+ # Inferenza arch dal ckpt (così non serve config)
87
+ state = torch.load(local_dir + "/pytorch_model.bin", map_location="cpu", weights_only=False)
88
+ sd = state["model"]
89
+ hp = m.HP()
90
+ hp.head_dim = 64
91
+ hp.d_model = sd["backbone.embedder.embedding.weight"].shape[1]
92
+ hp.n_heads = hp.d_model // hp.head_dim
93
+ hp.d_ff = sd["backbone.blocks.0.channel_mixer.expand.weight"].shape[0]
94
+ hp.n_layers = max(int(k.split("blocks.")[1].split(".")[0]) for k in sd if "blocks." in k) + 1
95
+ model = m.PiCOFormerLM(hp, vocab_size=sd["backbone.embedder.embedding.weight"].shape[0])
96
+ model.load_state_dict(sd, strict=False)
97
+ model = model.cuda().bfloat16().eval()
98
+ tok = AutoTokenizer.from_pretrained("mistralai/Mathstral-7B-v0.1")
99
+ ```
100
+
101
+ See [`inference_example.py`](./inference_example.py) for a complete generation example
102
+ (temperature, top-p, top-k sampling).
103
+
104
+ ## Files
105
+
106
+ | file | description |
107
+ |---|---|
108
+ | `pytorch_model.bin` | model weights (fp32 master, 745MB) |
109
+ | `config.json` | architecture + HP + eval results |
110
+ | `modeling_guido.py` | model definition (`PiCOFormerLM` + Muon optimizer + reader; only PiCOFormerLM/HP needed for inference) |
111
+ | `inference_example.py` | minimal load + generate example |
112
+ | `vathos/` | bundled Vathos package (custom backbone — see github.com/MarioPaerle/Aplos) |
113
+ | `requirements.txt` | deps (torch, transformers, cut-cross-entropy) |
114
+
115
+ ## Known limitations
116
+
117
+ - **Decontamination**: canonical-hash only. ~7% of MATH-500 has near-duplicates in OpenMathInstruct-2
118
+ augmentation (audited). Real MATH-500 capability is ~21% greedy, not inflated by overt memorization.
119
+ - **No instruction tuning**: this is a base pretrain. No SFT / RLHF.
120
+ - **Domain**: heavily math-skewed. NL is 30% (FineWeb-Edu + Cosmopedia) — basic fluency but not chat-style.
121
+ - **Tokenizer dependency**: requires `mistralai/Mathstral-7B-v0.1` tokenizer from HF.
122
+
123
+ ## Hardware/precision notes
124
+
125
+ - Inference works fine in **bf16** (just call `.bfloat16()`). Weights are stored fp32 — 745MB on disk
126
+ but ~390MB GPU memory after `.bfloat16()`.
127
+ - Custom CUDA kernels: uses `cut_cross_entropy` (Apple) for fused linear+CE in training.
128
+ For inference you can replace with standard `F.linear + F.cross_entropy`.
129
+
130
+ ## Architecture sketch
131
+
132
+ ```
133
+ input_ids
134
+
135
+ embedder (tied) ───────────��──────────────┐
136
+ │ │
137
+ smear_gate (sparse on first 12 dims) ─────│ x0
138
+ │ │
139
+ [24× block] │
140
+ ├── spatial: gated_attn + RoPE + qk-norm │
141
+ ├── channel: VariableUDLP (LeakyReLU²) │
142
+ └── + x0_lambda × x0 ───────────────────┘
143
+
144
+ final_norm
145
+
146
+ tied unembed → cce / softcap 30 → logits
147
+ ```
148
+
149
+ ## License & citation
150
+
151
+ Vathos backbone (custom by Mario Paerle): https://github.com/MarioPaerle/Aplos
152
+ Model weights: research/private. Contact owner for usage terms.
config.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "guido_small",
3
+ "model_name": "GUIDO-small 200M (bugfixed)",
4
+ "architecture": "Vathos PiCOFormer (custom)",
5
+ "version": "v1-bugfix",
6
+ "n_layers": 24,
7
+ "d_model": 768,
8
+ "n_heads": 12,
9
+ "head_dim": 64,
10
+ "d_ff": 3072,
11
+ "vocab_size": 32768,
12
+ "max_seq_len": 8192,
13
+ "rope_base": 1000000.0,
14
+ "logit_softcap": 30.0,
15
+ "tied_embeddings": true,
16
+ "qk_norm": true,
17
+ "use_gated_attn": true,
18
+ "use_smear_gate": true,
19
+ "gate_input_dim": 12,
20
+ "mlp_kind": "udlp",
21
+ "orthogonalizer": "ns5",
22
+ "muon_backend_steps": 5,
23
+ "training_dtype": "fp32-master/bf16-autocast",
24
+ "checkpoint_dtype": "float32",
25
+ "training_steps": 16000,
26
+ "training_tokens": 4194304000,
27
+ "training_data": "corpus_v2 mixture 70/22/8 (math/fineweb/cosmopedia)",
28
+ "training_mixture": {
29
+ "openmath_full": 0.34,
30
+ "tinygsm": 0.14,
31
+ "openmathreasoning": 0.10,
32
+ "numina_15": 0.07,
33
+ "numina_cot": 0.05,
34
+ "fineweb_edu": 0.22,
35
+ "cosmopedia": 0.08
36
+ },
37
+ "tokenizer": "mistralai/Mathstral-7B-v0.1",
38
+ "params_total": 195092992,
39
+ "params_total_human": "195M",
40
+ "eval_gsm8k_greedy_pct": 43.0,
41
+ "eval_gsm8k_maj16_pct": 57.5,
42
+ "eval_math500_greedy_pct": 21.0,
43
+ "eval_math500_maj16_pct": 30.0
44
+ }
inference_example.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load GUIDO-small 200M (bugfix) e genera testo.
2
+ Run: python inference_example.py
3
+ """
4
+ import sys
5
+ from pathlib import Path
6
+ import torch
7
+ from transformers import AutoTokenizer
8
+
9
+ # Aggiungi questa cartella al sys.path così Vathos viene trovato
10
+ HERE = Path(__file__).resolve().parent
11
+ sys.path.insert(0, str(HERE / "vathos")) # bundled Vathos package
12
+ sys.path.insert(0, str(HERE)) # modeling_guido.py
13
+
14
+ # Il modeling è in modeling_guido.py (fork di train_guido_small.py — usa la classe PiCOFormerLM).
15
+ # `import modeling_guido as m` registra Vathos in production mode + carica le classi.
16
+ import modeling_guido as m
17
+
18
+
19
+ def load_model(ckpt_path: str, device="cuda"):
20
+ """Carica il ckpt fp32, infera l'architettura dai pesi, restituisce model+tokenizer."""
21
+ state = torch.load(ckpt_path, map_location="cpu", weights_only=False)
22
+ sd = state["model"] if "model" in state else state
23
+ # strip eventuali prefissi
24
+ sd = {k[len("_orig_mod."):] if k.startswith("_orig_mod.") else k: v for k, v in sd.items()}
25
+
26
+ # Infer arch dal state_dict (così funziona anche se il config.json non c'è)
27
+ hp = m.HP()
28
+ hp.head_dim = 64
29
+ hp.d_model = sd["backbone.embedder.embedding.weight"].shape[1]
30
+ hp.n_heads = hp.d_model // hp.head_dim
31
+ hp.d_ff = sd["backbone.blocks.0.channel_mixer.expand.weight"].shape[0]
32
+ hp.n_layers = max(int(k.split("blocks.")[1].split(".")[0])
33
+ for k in sd if "blocks." in k) + 1
34
+ vocab = sd["backbone.embedder.embedding.weight"].shape[0]
35
+ print(f"[load] arch inferred: {hp.n_layers}L × {hp.d_model} × {hp.n_heads}h × {hp.d_ff}ff vocab={vocab}")
36
+
37
+ model = m.PiCOFormerLM(hp, vocab_size=vocab).to(device).bfloat16()
38
+ missing, unexpected = model.load_state_dict(sd, strict=False)
39
+ if missing: print(f"[load] missing keys ({len(missing)}): {missing[:3]}...")
40
+ if unexpected: print(f"[load] unexpected ({len(unexpected)}): {unexpected[:3]}...")
41
+ model.eval()
42
+
43
+ tok = AutoTokenizer.from_pretrained("mistralai/Mathstral-7B-v0.1")
44
+ return model, tok
45
+
46
+
47
+ @torch.no_grad()
48
+ def generate(model, tok, prompt: str, max_new=200, temperature=0.6, top_p=0.95, top_k=40, device="cuda"):
49
+ """Sampling semplice (T, top-p, top-k)."""
50
+ import torch.nn.functional as F
51
+ ids = tok(prompt, return_tensors="pt").input_ids.to(device)
52
+ eos = tok.eos_token_id or 2
53
+ softcap = model.logit_softcap
54
+ for _ in range(max_new):
55
+ h = model._hidden(ids)
56
+ logits = F.linear(h[:, -1, :], model.classifier_weight)
57
+ if softcap > 0:
58
+ logits = softcap * torch.tanh(logits / softcap)
59
+ logits = logits.float() / temperature
60
+ if top_k and top_k < logits.size(-1):
61
+ kth = logits.topk(top_k, dim=-1).values[:, -1:]
62
+ logits = logits.masked_fill(logits < kth, float("-inf"))
63
+ probs = F.softmax(logits, dim=-1)
64
+ if top_p < 1.0:
65
+ sp, si = probs.sort(dim=-1, descending=True)
66
+ csum = sp.cumsum(dim=-1)
67
+ mask = csum - sp > top_p
68
+ sp = sp.masked_fill(mask, 0.0); sp /= sp.sum(dim=-1, keepdim=True)
69
+ choice = torch.multinomial(sp, 1)
70
+ nxt = si.gather(-1, choice)
71
+ else:
72
+ nxt = torch.multinomial(probs, 1)
73
+ ids = torch.cat([ids, nxt], dim=1)
74
+ if nxt.item() == eos:
75
+ break
76
+ return tok.decode(ids[0], skip_special_tokens=False)
77
+
78
+
79
+ if __name__ == "__main__":
80
+ # Adatta il path al checkpoint
81
+ CKPT = HERE / "pytorch_model.bin"
82
+ device = "cuda" if torch.cuda.is_available() else "cpu"
83
+ model, tok = load_model(str(CKPT), device=device)
84
+
85
+ prompts = [
86
+ "Question: Sarah has 5 apples and gives 2 to Tom. How many does she have left?\nAnswer: ",
87
+ "Solve: $x^2 + 3x + 2 = 0$. Step 1:",
88
+ "The largest planet in the solar system is",
89
+ ]
90
+ for p in prompts:
91
+ print("=" * 60)
92
+ print("PROMPT:", p)
93
+ out = generate(model, tok, p, max_new=120, device=device)
94
+ print("OUT: ", out)
modeling_guido.py ADDED
@@ -0,0 +1,760 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Guido-small — pretrain ~200M (24L×768), math-first chattabile.
2
+
3
+ Fork di `train_distill_start.py` SENZA KD (il teacher non aiuta a questa scala —
4
+ vedi SESSION_HANDOFF.md §A). Differenze chiave:
5
+ - shape 200M (24L × 768 × 12h × 64hd × 3072ff)
6
+ - MultiShardMixture reader: legge corpus_v2 (7 shard per-dataset, NON shufflati a
7
+ write-time) con chunk-shuffle a read-time (NO rimpiazzo, copertura 100%, ordine
8
+ casuale). Pesi mixture 70% math / 22% fineweb / 8% cosmopedia in HP.mixture.
9
+ - loss-trace per-step → CSV (loss, gnorm, lr, math_frac) con UN sync ogni
10
+ `train_log_every` step (buffer su GPU). math_frac correla spike↔batch math-heavy.
11
+ - looping opzionale (env LOOP_STYLE / LOOP_ACTIVATION_FRAC), default OFF per la baseline.
12
+
13
+ Run:
14
+ torchrun --standalone --nproc_per_node=4 train_guido_small.py
15
+
16
+ Pre-req: corpus_v2 tokenizzato (Mathstral SPM 32k, uint32) in $FAST/corpus_v2/<name>/.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import csv
21
+ import os
22
+ import re
23
+ import sys
24
+ import time
25
+ from pathlib import Path
26
+
27
+ import numpy as np
28
+ import torch
29
+ import torch.distributed as dist
30
+ import torch.nn as nn
31
+ import torch.nn.functional as F
32
+ from torch import Tensor
33
+ from torch.nn.parallel import DistributedDataParallel as DDP
34
+
35
+ # ---- Vathos backbone ----
36
+ APLOS_PATH = os.environ.get("APLOS_PATH", "/leonardo_work/IscrC_YENDRI/paerle/PiCO/aplos")
37
+ if APLOS_PATH not in sys.path:
38
+ sys.path.insert(0, APLOS_PATH)
39
+ from Vathos._basics import (Builder, RMSNorm as VRMSNorm, ReLU2, LeakyReLU2,
40
+ VariableUDLP, VariableGatedUDLP, set_vathos_mode)
41
+ from Vathos._spatials import MultiheadAttentionMixer, MultiheadGatedAttentionMixer, RoPE
42
+ from Vathos.blocks import PiCOFormer as VathosPiCOFormer, SmearGate
43
+ set_vathos_mode("production")
44
+
45
+ # FAST RMSNorm fused
46
+ def _fast_rmsnorm_forward(self, x):
47
+ return F.rms_norm(x, (x.size(-1),), self.weight, self.eps)
48
+ VRMSNorm.forward = _fast_rmsnorm_forward
49
+
50
+ # ---- Shard reader ----
51
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "PiCO2"))
52
+ from pico2.shards import ShardReader
53
+
54
+ from cut_cross_entropy import linear_cross_entropy
55
+
56
+
57
+ # =============================================================================
58
+ # HYPERPARAMETERS
59
+ # =============================================================================
60
+ class HP:
61
+ # data — corpus_v2 multi-shard mixture (Mathstral SPM 32k)
62
+ corpus_root = os.environ.get(
63
+ "CORPUS_ROOT",
64
+ "/leonardo_scratch/fast/IscrC_YENDRI/mprignan/corpus_v2",
65
+ )
66
+ # Mixture preset selezionabile via MIXTURE_PRESET env. Default = 70/22/8 (run baseline 200M).
67
+ # "v3_noloops" = fineweb 22%→15% (≈2/3), delta freed → math (77/15/8 total).
68
+ _MIXTURE_PRESETS = {
69
+ "default": {
70
+ "openmath_full": 0.34, "tinygsm": 0.14, "openmathreasoning": 0.10,
71
+ "numina_15": 0.07, "numina_cot": 0.05, # = 0.70 math
72
+ "fineweb_edu": 0.22, "cosmopedia": 0.08, # = 0.30 NL
73
+ },
74
+ "v3_noloops": {
75
+ "openmath_full": 0.37, "tinygsm": 0.15, "openmathreasoning": 0.11,
76
+ "numina_15": 0.08, "numina_cot": 0.06, # = 0.77 math
77
+ "fineweb_edu": 0.15, "cosmopedia": 0.08, # = 0.23 NL (fineweb ridotto a 2/3)
78
+ },
79
+ }
80
+ mixture = _MIXTURE_PRESETS[os.environ.get("MIXTURE_PRESET", "default")]
81
+ nl_datasets = ("fineweb_edu", "cosmopedia") # per math_frac logging
82
+ seq_len = 2048
83
+ bs_per_dev = int(os.environ.get("BS_PER_DEV", 32))
84
+ train_batch_tokens = 4 * bs_per_dev * 2048 # 4×32×2048 = 262k tok/step (4 GPU)
85
+
86
+ # model — shape 200M (scale-up dal 92M: d_model 512→768, d_ff 2048→3072, heads 8→12)
87
+ n_layers = int(os.environ.get("N_LAYERS", 24))
88
+ d_model = int(os.environ.get("D_MODEL", 768))
89
+ n_heads = int(os.environ.get("N_HEADS", 12))
90
+ head_dim = 64
91
+ d_ff = int(os.environ.get("D_FF", 3072))
92
+ rope_base = 1_000_000.0
93
+ logit_softcap = 30.0
94
+ tied_embeddings = True
95
+ qk_norm = True
96
+ # v3 features (locked)
97
+ use_gated_attn = True
98
+ # Channel mixer (FFN) variant. Knob aperto dall'audit A/B 2026-05-28 (vedi analysis/guido_ab/REPORT).
99
+ # "udlp" : VariableUDLP, contract(LeakyReLU²(expand(x))) (current default)
100
+ # "gated_udlp" : VariableGatedUDLP, UDLP·sigmoid(gate_proj(x[..., :K])) (sparse output gate)
101
+ # "leaky_reglu2" : LeakyReGLU² GLU, contract(LeakyReLU²(expand(x)) · up(x)) (act·value, +1 proj)
102
+ mlp_kind = os.environ.get("MLP_KIND", "udlp")
103
+ use_smear_gate = True
104
+ # gate_input_dim: 12 era stato scelto stile parameter-golf/sparse. Sul d=768 è ~1.5% di x:
105
+ # per il prossimo run testare 64 (8%) o 128 (17%) — vedi audit gate-activity (40% dei gate<0.1).
106
+ gate_input_dim = int(os.environ.get("GATE_INPUT_DIM", 12))
107
+
108
+ # optimizer (Muon LOCKED)
109
+ optimizer_kind = "muon"
110
+ # Orthogonalizer per Muon. Knob aperto dall'audit A/B 2026-05-28.
111
+ # "ns5" : Newton-Schulz quintic, coeff (3.4445,-4.7750,2.0315), Keller Jordan
112
+ # "polar_express": Polar Express (arxiv 2505.16932) — schedule di 8 coeff/step,
113
+ # convergenza più rapida. RACCOMANDATO MUON_BACKEND_STEPS=8 (default 5
114
+ # di NS5 è sotto-iterato per PE → convergenza parziale).
115
+ orthogonalizer = os.environ.get("ORTHOGONALIZER", "ns5")
116
+ muon_backend_steps = int(os.environ.get("MUON_BACKEND_STEPS", 5))
117
+ matrix_lr = 0.01
118
+ tied_embed_lr = 0.005
119
+ scalar_lr = 0.04
120
+ muon_momentum = 0.95
121
+ muon_momentum_warmup_start = 0.85
122
+ muon_momentum_warmup_steps = 200
123
+ beta1 = 0.9
124
+ beta2 = 0.95
125
+ adam_eps = 1e-8
126
+ grad_clip = 1.0
127
+
128
+ # schedule (WSD: warmup → plateau → linear warmdown a lr_min_scale)
129
+ iterations = int(os.environ.get("ITERATIONS", 15000)) # ~4B tok; CALIBRA dopo lo smoke
130
+ warmup_steps = int(os.environ.get("WARMUP", 300))
131
+ warmdown_iters = int(os.environ.get("WARMDOWN", int(0.60 * iterations))) # 60%
132
+ lr_min_scale = float(os.environ.get("LR_MIN_SCALE", 0.001))
133
+
134
+ # === Layer looping (opzionale, default OFF per baseline) ===
135
+ # loop_v2 sul 92M: +4.5pp GSM8K @ activation_frac 0.30 (durante plateau LR).
136
+ loop_style = os.environ.get("LOOP_STYLE", "")
137
+ loop_activation_frac = float(os.environ.get("LOOP_ACTIVATION_FRAC", 0.30))
138
+ loop_pre_warm = os.environ.get("LOOP_PRE_WARM", "1") == "1"
139
+
140
+ # logging / checkpoint
141
+ train_log_every = int(os.environ.get("LOG_EVERY", 50)) # dump CSV ogni N (loss PER-STEP nel buffer)
142
+ loss_trace_csv = os.environ.get(
143
+ "LOSS_TRACE_CSV",
144
+ "/leonardo_work/IscrC_YENDRI/paerle/PiCO/Guido-1/PiCO2_test/logs/guido_small_loss_trace.csv",
145
+ )
146
+ ckpt_dir = os.environ.get(
147
+ "CKPT_DIR",
148
+ "/leonardo_work/IscrC_YENDRI/paerle/PiCO/ckpts/pico_guido_small",
149
+ )
150
+ save_final = True
151
+ save_interval = int(os.environ.get("SAVE_INTERVAL", 2000))
152
+ keep_n_recent = 5
153
+ seed = int(os.environ.get("SEED", 1337))
154
+
155
+
156
+ # =============================================================================
157
+ # MUON OPTIMIZER
158
+ # =============================================================================
159
+ def zeropower_via_newtonschulz5(G: Tensor, steps: int = 5, eps: float = 1e-7) -> Tensor:
160
+ a, b, c = (3.4445, -4.7750, 2.0315)
161
+ X = G.bfloat16()
162
+ X /= X.norm() + eps
163
+ transposed = G.size(0) > G.size(1)
164
+ if transposed:
165
+ X = X.T
166
+ for _ in range(steps):
167
+ A = X @ X.T
168
+ B = b * A + c * A @ A
169
+ X = a * X + B @ X
170
+ return X.T if transposed else X
171
+
172
+
173
+ # Polar Express coefficients (arxiv 2505.16932). Stessa forma di NS5 ma SCHEDULE di 8 set:
174
+ # iter 1 aggressivo per matrici mal condizionate, iter 7-8 = steady-state Halley (3/8, -10/8, 15/8).
175
+ # Source: NVIDIA NeMo Emerging-Optimizers (authoritative, production-tested).
176
+ # https://docs.nvidia.com/nemo/emerging-optimizers/0.1.0/_modules/emerging_optimizers/orthogonalized_optimizers/muon_utils.html
177
+ _POLAR_EXPRESS_COEFFS = (
178
+ (8.2051, -22.9019, 16.4607),
179
+ (4.0664, -2.8612, 0.5184),
180
+ (3.9096, -2.8234, 0.5250),
181
+ (3.2856, -2.4153, 0.4853),
182
+ (2.2779, -1.6198, 0.3985),
183
+ (1.8726, -1.2307, 0.3585),
184
+ (1.8564, -1.2132, 0.3568),
185
+ (1.8750, -1.2500, 0.3750),
186
+ )
187
+
188
+
189
+ def zeropower_via_polar_express(G: Tensor, steps: int = 8, eps: float = 1e-7) -> Tensor:
190
+ """Polar Express orthogonalizer — convergenza più rapida di NS5 quintic.
191
+ Iteration: X = a*X + (b*A + c*A@A)@X con A=X@X^T, a/b/c SCHEDULATI per step.
192
+ Normalizzazione Frobenius (identica a NS5). Per convergenza piena servono ≥7 step
193
+ (raccomandato MUON_BACKEND_STEPS=8). Oltre 8 step usa lo steady-state (Halley)."""
194
+ X = G.bfloat16()
195
+ X /= X.norm() + eps
196
+ transposed = G.size(0) > G.size(1)
197
+ if transposed:
198
+ X = X.T
199
+ n_coeffs = len(_POLAR_EXPRESS_COEFFS)
200
+ for k in range(steps):
201
+ a, b, c = _POLAR_EXPRESS_COEFFS[k if k < n_coeffs else n_coeffs - 1]
202
+ A = X @ X.T
203
+ B = b * A + c * A @ A
204
+ X = a * X + B @ X
205
+ return X.T if transposed else X
206
+
207
+
208
+ class Muon(torch.optim.Optimizer):
209
+ def __init__(self, params, lr, momentum, backend_steps, nesterov=True):
210
+ super().__init__(params, dict(lr=lr, momentum=momentum,
211
+ backend_steps=backend_steps, nesterov=nesterov))
212
+
213
+ @torch.no_grad()
214
+ def step(self, closure=None):
215
+ distributed = dist.is_available() and dist.is_initialized()
216
+ world_size = dist.get_world_size() if distributed else 1
217
+ rank = dist.get_rank() if distributed else 0
218
+ for group in self.param_groups:
219
+ params = group["params"]
220
+ if not params:
221
+ continue
222
+ lr, mom, ns_steps, nesterov = group["lr"], group["momentum"], group["backend_steps"], group["nesterov"]
223
+ total = sum(int(p.numel()) for p in params)
224
+ updates_flat = torch.zeros(total, device=params[0].device, dtype=torch.bfloat16)
225
+ curr = 0
226
+ for i, p in enumerate(params):
227
+ if i % world_size == rank and p.grad is not None:
228
+ g = p.grad
229
+ state = self.state[p]
230
+ if "momentum_buffer" not in state:
231
+ state["momentum_buffer"] = torch.zeros_like(g)
232
+ buf = state["momentum_buffer"]
233
+ buf.mul_(mom).add_(g)
234
+ if nesterov:
235
+ g = g.add(buf, alpha=mom)
236
+ g = zeropower_via_newtonschulz5(g, steps=ns_steps)
237
+ g *= max(1, g.size(0) / g.size(1)) ** 0.5
238
+ updates_flat[curr:curr + p.numel()] = g.reshape(-1)
239
+ curr += p.numel()
240
+ if distributed:
241
+ dist.all_reduce(updates_flat, op=dist.ReduceOp.SUM)
242
+ curr = 0
243
+ for p in params:
244
+ u = updates_flat[curr:curr + p.numel()].view_as(p).to(p.dtype)
245
+ p.add_(u, alpha=-lr)
246
+ curr += p.numel()
247
+
248
+
249
+ # =============================================================================
250
+ # CHANNEL MIXER VARIANT — LeakyReGLU² GLU-style FFN
251
+ # =============================================================================
252
+ class LeakyReGLU2_FFN(nn.Module):
253
+ """LeakyReLU² GLU: contract(LeakyReLU²(expand(x)) · up(x)).
254
+
255
+ GLU-style channel mixer (à la SwiGLU/ReGLU) con activation LeakyReLU². Aggiunge UN extra
256
+ proiezione `up` (d_model→M) rispetto a VariableUDLP → +50% param nell'FFN. Identity-at-init:
257
+ contract=0 ⇒ branch nullo all'init (gradiente fluisce attraverso `expand` e `up`).
258
+
259
+ Compatibile con Vathos `Builder`: signature (d_model, d_output, M, activation, dropout).
260
+ Param naming: `expand` (act path, ndim==2 → Muon), `up` (value path), `contract` (output).
261
+ """
262
+ def __init__(self, d_model, d_output, M, activation=None, dropout=0.0):
263
+ super().__init__()
264
+ self.expand = nn.Linear(d_model, M, bias=False) # act path (the W in act(xW))
265
+ self.up = nn.Linear(d_model, M, bias=False) # value path (the V in ·xV)
266
+ self.contract = nn.Linear(M, d_output, bias=False)
267
+ self.activation = activation() if activation is not None else LeakyReLU2()
268
+ self.dropout = nn.Dropout(dropout)
269
+ self._init_weights()
270
+
271
+ def _init_weights(self):
272
+ torch.nn.init.orthogonal_(self.expand.weight)
273
+ torch.nn.init.orthogonal_(self.up.weight)
274
+ torch.nn.init.zeros_(self.contract.weight) # identity-at-init: branch nullo
275
+
276
+ def forward(self, x):
277
+ return self.dropout(self.contract(self.activation(self.expand(x)) * self.up(x)))
278
+
279
+
280
+ # =============================================================================
281
+ # DATA — MultiShardMixture: chunk-shuffle a read-time (NO rimpiazzo)
282
+ # =============================================================================
283
+ class ShuffledStream:
284
+ """Un dataset → finestre (seq_len+1) in ordine PERMUTATO (shuffle senza rimpiazzo).
285
+ Chunk disgiunti per rank; re-permuta a fine epoca (sub-epoca = nessun wrap col budget attuale)."""
286
+ def __init__(self, shard_dir, rank, world_size, seq_len, seed):
287
+ self.reader = ShardReader(shard_dir)
288
+ self.win = seq_len + 1
289
+ self.n_chunks = self.reader.total_tokens // self.win
290
+ self.rank, self.ws, self.seed, self._epoch = rank, world_size, seed, 0
291
+ self._reperm()
292
+
293
+ def _reperm(self):
294
+ rng = np.random.default_rng(self.seed + 104729 * self._epoch)
295
+ self.my = rng.permutation(self.n_chunks)[self.rank::self.ws]
296
+ self.ptr = 0
297
+ self._epoch += 1
298
+
299
+ def next_window(self):
300
+ if self.ptr >= len(self.my):
301
+ self._reperm()
302
+ ci = int(self.my[self.ptr])
303
+ self.ptr += 1
304
+ return self.reader.read(ci * self.win, self.win) # np.uint32 [win]
305
+
306
+
307
+ class MultiShardMixture:
308
+ """Weighted-pick del dataset per-sample + finestra chunk-shuffled. Ogni doc ≤1 volta/epoca."""
309
+ def __init__(self, mixture, corpus_root, rank, world_size, device, seq_len, seed):
310
+ self.names = list(mixture.keys())
311
+ self.w = np.array([mixture[n] for n in self.names], float)
312
+ self.w /= self.w.sum()
313
+ self.streams = {
314
+ n: ShuffledStream(str(Path(corpus_root) / n), rank, world_size, seq_len, seed + i * 7919)
315
+ for i, n in enumerate(self.names)
316
+ }
317
+ self.rng = np.random.default_rng(seed * 31 + rank)
318
+ self.device = device
319
+
320
+ def next_batch(self, bs):
321
+ picks = self.rng.choice(len(self.names), size=bs, p=self.w)
322
+ xs, ys, ids = [], [], []
323
+ for pi in picks:
324
+ w = self.streams[self.names[pi]].next_window()
325
+ t = torch.from_numpy(w.astype(np.int64))
326
+ xs.append(t[:-1])
327
+ ys.append(t[1:])
328
+ ids.append(self.names[pi])
329
+ return (torch.stack(xs).to(self.device, non_blocking=True),
330
+ torch.stack(ys).to(self.device, non_blocking=True), ids)
331
+
332
+
333
+ # =============================================================================
334
+ # LAYER LOOPING — parser + scalar gate init=0 (Universal Transformer style)
335
+ # =============================================================================
336
+ _LOOP_SEG_RE = re.compile(r"\(\s*([\d\s,]+?)\s*\)\s*x\s*(\d+)")
337
+
338
+
339
+ def parse_loop_style(s: str, n_layers: int):
340
+ """Parse loop style stringa → lista di (indices, n_total_executions).
341
+ Esempio: "(3,4,5)x2, (8,9)x3" → [([3,4,5], 2), ([8,9], 3)]. Semantica xN: N esecuzioni
342
+ totali (1 originale + N-1 loop extra con gate). Validation: in-range, contigui, disgiunti, N>=2."""
343
+ s = s.strip()
344
+ if not s:
345
+ return []
346
+ stripped = re.sub(r"\s+", "", s)
347
+ pattern_strict = re.compile(r"\(([\d,]+)\)x(\d+)")
348
+ consumed = "".join(f"({a})x{b}" for a, b in pattern_strict.findall(stripped))
349
+ consumed_with_commas = ",".join(f"({a})x{b}" for a, b in pattern_strict.findall(stripped))
350
+ if stripped != consumed and stripped != consumed_with_commas:
351
+ raise ValueError(f"loop_style mal formato: {s!r}. Atteso: '(i,j,k)xN, (l,m)xM, ...'")
352
+
353
+ segments = []
354
+ used_indices = set()
355
+ for indices_str, n_str in _LOOP_SEG_RE.findall(s):
356
+ indices = sorted({int(x.strip()) for x in indices_str.split(",") if x.strip()})
357
+ n = int(n_str)
358
+ if n < 2:
359
+ raise ValueError(f"loop count xN deve avere N>=2 (segmento ({indices}) ha x{n}). "
360
+ f"Usa loop_style='' per nessun looping.")
361
+ if not indices:
362
+ raise ValueError(f"loop segment vuoto in {s!r}")
363
+ if indices != list(range(min(indices), max(indices) + 1)):
364
+ raise ValueError(f"segmento {tuple(indices)} NON contiguo: i layer in un loop "
365
+ f"devono essere contigui (e.g. (3,4,5), non (3,5)).")
366
+ for i in indices:
367
+ if not 0 <= i < n_layers:
368
+ raise ValueError(f"layer {i} fuori range [0, {n_layers}) nel segmento {tuple(indices)}")
369
+ if i in used_indices:
370
+ raise ValueError(f"layer {i} appare in più segmenti — i segmenti devono essere disgiunti")
371
+ used_indices.add(i)
372
+ segments.append((indices, n))
373
+ segments.sort(key=lambda seg: seg[0][0])
374
+ for s1, s2 in zip(segments, segments[1:]):
375
+ if s1[0][-1] >= s2[0][0]:
376
+ raise ValueError(f"segmenti sovrapposti dopo ordinamento: {s1[0]} e {s2[0]}")
377
+ return segments
378
+
379
+
380
+ class LoopGate(nn.Module):
381
+ """Scalar gate init=0 (additive): h_main + α·(h_post − h_main). α=0 → identity."""
382
+ def __init__(self):
383
+ super().__init__()
384
+ self.alpha = nn.Parameter(torch.zeros(()))
385
+
386
+ def forward(self, h_main: Tensor, h_post: Tensor) -> Tensor:
387
+ return h_main + self.alpha * (h_post - h_main)
388
+
389
+
390
+ # =============================================================================
391
+ # MODEL — wrapper Vathos + layer looping (CE only, no KD)
392
+ # =============================================================================
393
+ class PiCOFormerLM(nn.Module):
394
+ def __init__(self, hp: HP, vocab_size: int, loop_style: str = ""):
395
+ super().__init__()
396
+ self.vocab_size = vocab_size
397
+ self.logit_softcap = hp.logit_softcap
398
+ self.tied_embeddings = hp.tied_embeddings
399
+ self.loop_style = loop_style
400
+ self.loop_segments = parse_loop_style(loop_style, hp.n_layers) if loop_style else []
401
+ self.loop_gates = nn.ModuleList([
402
+ nn.ModuleList([LoopGate() for _ in range(n - 1)])
403
+ for indices, n in self.loop_segments
404
+ ])
405
+ self._seg_starts = {indices[0]: seg_id for seg_id, (indices, _) in enumerate(self.loop_segments)}
406
+ self.looped_mode = False
407
+ rope = RoPE(dim=hp.head_dim, max_len=8192, base=hp.rope_base)
408
+ if hp.use_gated_attn:
409
+ attn_builder = Builder(MultiheadGatedAttentionMixer,
410
+ n_heads=hp.n_heads, causal=True, dropout=0.0,
411
+ qk_norm=hp.qk_norm, pos_emb=rope, gate_input_dim=hp.gate_input_dim)
412
+ else:
413
+ attn_builder = Builder(MultiheadAttentionMixer,
414
+ n_heads=hp.n_heads, causal=True, dropout=0.0,
415
+ qk_norm=hp.qk_norm, pos_emb=rope)
416
+ if hp.mlp_kind == "udlp":
417
+ chan_builder = Builder(VariableUDLP,
418
+ d_output=hp.d_model, M=hp.d_ff, activation=LeakyReLU2)
419
+ elif hp.mlp_kind == "gated_udlp":
420
+ chan_builder = Builder(VariableGatedUDLP,
421
+ d_output=hp.d_model, M=hp.d_ff, activation=LeakyReLU2,
422
+ gate_input_dim=hp.gate_input_dim)
423
+ elif hp.mlp_kind == "leaky_reglu2":
424
+ chan_builder = Builder(LeakyReGLU2_FFN,
425
+ d_output=hp.d_model, M=hp.d_ff, activation=LeakyReLU2)
426
+ else:
427
+ raise ValueError(f"unknown mlp_kind={hp.mlp_kind!r}; usa udlp|gated_udlp|leaky_reglu2")
428
+ smear = SmearGate(hp.d_model, gate_input_dim=hp.gate_input_dim) if hp.use_smear_gate else None
429
+ smear_lookback = 1 if hp.use_smear_gate else 0
430
+ self.backbone = VathosPiCOFormer(
431
+ vocab_size=vocab_size, d_model=hp.d_model, n_layers=hp.n_layers,
432
+ spatials=attn_builder, channel=chan_builder, norm=VRMSNorm,
433
+ ve_groups=None, smear_gate=smear, smear_gate_lookback=smear_lookback,
434
+ logit_softcap=hp.logit_softcap, tied_embeddings=hp.tied_embeddings,
435
+ )
436
+
437
+ @property
438
+ def classifier_weight(self):
439
+ if self.tied_embeddings:
440
+ return self.backbone.embedder.embedding.weight
441
+ return self.backbone.unembedder.linear.weight
442
+
443
+ def _hidden(self, input_ids: Tensor) -> Tensor:
444
+ if self.looped_mode and self.loop_segments:
445
+ return self._hidden_looped(input_ids)
446
+ return self._hidden_normal(input_ids)
447
+
448
+ def _hidden_normal(self, input_ids: Tensor) -> Tensor:
449
+ bb = self.backbone
450
+ x0 = bb.embedder(input_ids)
451
+ if getattr(bb, "smear_gate", None) is not None:
452
+ x0 = bb.smear_gate(x0)
453
+ ves = bb._compute_ves(input_ids) if hasattr(bb, "_compute_ves") else [None] * len(bb.blocks)
454
+ h = x0
455
+ for i, block in enumerate(bb.blocks):
456
+ h = block(h, x0, ve=ves[i] if ves is not None else None)
457
+ h = bb.final_norm(h)
458
+ return h
459
+
460
+ def _hidden_looped(self, input_ids: Tensor) -> Tensor:
461
+ bb = self.backbone
462
+ x0 = bb.embedder(input_ids)
463
+ if getattr(bb, "smear_gate", None) is not None:
464
+ x0 = bb.smear_gate(x0)
465
+ ves = bb._compute_ves(input_ids) if hasattr(bb, "_compute_ves") else [None] * len(bb.blocks)
466
+ h = x0
467
+ n_layers = len(bb.blocks)
468
+ i = 0
469
+ while i < n_layers:
470
+ seg_id = self._seg_starts.get(i)
471
+ if seg_id is None:
472
+ h = bb.blocks[i](h, x0, ve=ves[i] if ves is not None else None)
473
+ i += 1
474
+ continue
475
+ indices, n_total = self.loop_segments[seg_id]
476
+ for li in indices:
477
+ h = bb.blocks[li](h, x0, ve=ves[li] if ves is not None else None)
478
+ h_main = h
479
+ for k in range(n_total - 1):
480
+ h_post = h_main
481
+ for li in indices:
482
+ h_post = bb.blocks[li](h_post, x0, ve=ves[li] if ves is not None else None)
483
+ h_main = self.loop_gates[seg_id][k](h_main, h_post)
484
+ h = h_main
485
+ i = indices[-1] + 1
486
+ h = bb.final_norm(h)
487
+ return h
488
+
489
+ def forward(self, input_ids: Tensor, targets: Tensor) -> Tensor:
490
+ """CE only (cce fused, no logits materialized)."""
491
+ h = self._hidden(input_ids)
492
+ W = self.classifier_weight
493
+ # cce vuole input bf16/fp16; con master fp32 + autocast h/W possono essere fp32 → cast esplicito
494
+ # (input cce identici a prima; il grad risale al master fp32 attraverso il cast).
495
+ return linear_cross_entropy(
496
+ h.reshape(-1, h.size(-1)).bfloat16(), W.bfloat16(), targets.reshape(-1),
497
+ reduction="mean",
498
+ softcap=self.logit_softcap if self.logit_softcap > 0 else None,
499
+ )
500
+
501
+
502
+ # =============================================================================
503
+ # MAIN
504
+ # =============================================================================
505
+ def main():
506
+ hp = HP()
507
+
508
+ # Dispatch orthogonalizer (Muon.step chiama il nome globale `zeropower_via_newtonschulz5`,
509
+ # quindi se l'utente sceglie polar_express ri-bindo quel nome alla nuova fn).
510
+ global zeropower_via_newtonschulz5
511
+ if hp.orthogonalizer == "ns5":
512
+ zeropower_via_newtonschulz5 = torch.compile(zeropower_via_newtonschulz5)
513
+ elif hp.orthogonalizer == "polar_express":
514
+ # NotImplementedError lo lancia al primo call dentro Muon.step.
515
+ zeropower_via_newtonschulz5 = torch.compile(zeropower_via_polar_express)
516
+ else:
517
+ raise ValueError(f"unknown orthogonalizer={hp.orthogonalizer!r}; usa ns5|polar_express")
518
+
519
+ distributed = "RANK" in os.environ and "WORLD_SIZE" in os.environ
520
+ rank = int(os.environ.get("RANK", "0"))
521
+ world_size = int(os.environ.get("WORLD_SIZE", "1"))
522
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
523
+ device = torch.device("cuda", local_rank)
524
+ torch.cuda.set_device(device)
525
+ if distributed:
526
+ dist.init_process_group(backend="nccl", device_id=device)
527
+ dist.barrier()
528
+ is_main = (rank == 0)
529
+
530
+ torch.backends.cuda.matmul.allow_tf32 = True
531
+ torch.backends.cudnn.allow_tf32 = True
532
+ torch.backends.cuda.enable_flash_sdp(True)
533
+ torch.backends.cuda.enable_mem_efficient_sdp(False)
534
+ torch.backends.cuda.enable_math_sdp(False)
535
+ torch.backends.cuda.enable_cudnn_sdp(False)
536
+ import torch._inductor.config as _ind
537
+ _ind.coordinate_descent_tuning = True
538
+ _ind.fx_graph_cache = True
539
+ _ind.triton.cudagraphs = False
540
+
541
+ torch.manual_seed(hp.seed)
542
+ torch.cuda.manual_seed_all(hp.seed)
543
+ np.random.seed(hp.seed)
544
+
545
+ def log(msg):
546
+ if is_main:
547
+ print(msg, flush=True)
548
+
549
+ log(f"world_size={world_size} rank={rank} device={device}")
550
+ log(f"corpus_root={hp.corpus_root}")
551
+ log(f"mixture={hp.mixture}")
552
+
553
+ # ---- data: verifica vocab consistency su tutti gli shard ----
554
+ vocab_size = None
555
+ eos_id = None
556
+ for name in hp.mixture:
557
+ idx = ShardReader(str(Path(hp.corpus_root) / name)).index
558
+ if vocab_size is None:
559
+ vocab_size, eos_id = idx.vocab_size, idx.eos_id
560
+ assert idx.vocab_size == vocab_size, (
561
+ f"vocab mismatch: {name} ha {idx.vocab_size}, atteso {vocab_size}")
562
+ assert idx.eos_id == eos_id, f"eos mismatch su {name}"
563
+ log(f"vocab={vocab_size} eos={eos_id} (consistente su {len(hp.mixture)} shard)")
564
+ assert vocab_size > eos_id
565
+
566
+ train_loader = MultiShardMixture(hp.mixture, hp.corpus_root, rank, world_size,
567
+ device, hp.seq_len, hp.seed)
568
+ if is_main:
569
+ for n, s in train_loader.streams.items():
570
+ log(f" [stream] {n:<20} chunks={s.n_chunks:,} (per-rank {len(s.my):,}) w={hp.mixture[n]:.2f}")
571
+
572
+ # ---- student model ----
573
+ # FIX (audit bf16): pesi in fp32 = MASTER weights. Il forward gira comunque in bf16 via autocast.
574
+ # Senza master fp32, al floor del warmdown gli update (~lr·1/√fan) cadono sotto la ULP bf16 e
575
+ # p.add_ diventa un no-op → il warmdown non abbassa più la loss. fp32 master lo risolve.
576
+ model = PiCOFormerLM(hp, vocab_size=vocab_size, loop_style=hp.loop_style).to(device)
577
+ n_params = sum(p.numel() for p in model.parameters())
578
+ log(f"model_params={n_params/1e6:.2f}M shape={hp.n_layers}L×{hp.d_model}×{hp.n_heads}h×{hp.d_ff}ff dtype=fp32-master/bf16-autocast")
579
+ log(f"knobs: mlp_kind={hp.mlp_kind} gate_input_dim={hp.gate_input_dim} orthogonalizer={hp.orthogonalizer} muon_steps={hp.muon_backend_steps} mixture_preset={os.environ.get('MIXTURE_PRESET','default')}")
580
+ if hp.loop_style:
581
+ loop_summary = ", ".join(f"({','.join(map(str, idx))})x{n}" for idx, n in model.loop_segments)
582
+ n_extra_passes = sum(len(idx) * (n - 1) for idx, n in model.loop_segments)
583
+ log(f"[loop] style={hp.loop_style!r} segments={loop_summary}")
584
+ log(f"[loop] gates={sum(len(g) for g in model.loop_gates)} (init α=0) "
585
+ f"extra_passes_per_fwd={n_extra_passes} activation_frac={hp.loop_activation_frac}")
586
+ compiled = torch.compile(model, dynamic=False, fullgraph=False, mode="max-autotune-no-cudagraphs")
587
+ ddp_kwargs = dict(device_ids=[local_rank], broadcast_buffers=False, gradient_as_bucket_view=True)
588
+ if hp.loop_style:
589
+ ddp_kwargs["find_unused_parameters"] = True # loop_gates senza grad in NORMAL fwd
590
+ model_for_train = DDP(compiled, **ddp_kwargs) if distributed else compiled
591
+ model.train()
592
+
593
+ # ---- optimizer ----
594
+ block_params = list(model.backbone.blocks.named_parameters())
595
+ matrix_params = [p for n, p in block_params if p.ndim == 2]
596
+ scalar_params = [p for n, p in block_params if p.ndim < 2]
597
+ for n, p in model.backbone.final_norm.named_parameters():
598
+ scalar_params.append(p)
599
+ for n, p in model.loop_gates.named_parameters():
600
+ scalar_params.append(p)
601
+ # FIX (audit): smear_gate è attributo top-level del backbone (NON dentro blocks) → senza questo
602
+ # i suoi param cadono fuori da ogni optimizer e restano congelati a init (smear_lambda=0 → gate
603
+ # no-op per tutto il run). Routing per ndim, coerente col resto (gate.weight 2D→Muon, lambda→scalar).
604
+ if getattr(model.backbone, "smear_gate", None) is not None:
605
+ for n, p in model.backbone.smear_gate.named_parameters():
606
+ (matrix_params if p.ndim == 2 else scalar_params).append(p)
607
+ embed_param = model.backbone.embedder.embedding.weight
608
+
609
+ opt_muon = Muon(matrix_params, lr=hp.matrix_lr, momentum=hp.muon_momentum,
610
+ backend_steps=hp.muon_backend_steps)
611
+ opt_embed = torch.optim.Adam(
612
+ [{"params": [embed_param], "lr": hp.tied_embed_lr, "base_lr": hp.tied_embed_lr}],
613
+ betas=(hp.beta1, hp.beta2), eps=hp.adam_eps, fused=True,
614
+ )
615
+ opt_scalar = torch.optim.Adam(
616
+ [{"params": scalar_params, "lr": hp.scalar_lr, "base_lr": hp.scalar_lr}],
617
+ betas=(hp.beta1, hp.beta2), eps=hp.adam_eps, fused=True,
618
+ )
619
+ optimizers = [opt_muon, opt_embed, opt_scalar]
620
+ for g in opt_muon.param_groups:
621
+ g["base_lr"] = hp.matrix_lr
622
+ log(f"opt: Muon={sum(p.numel() for p in matrix_params)/1e6:.2f}M "
623
+ f"Adam embed={embed_param.numel()/1e6:.2f}M scalars={sum(p.numel() for p in scalar_params)/1e6:.2f}M")
624
+
625
+ local_bs = hp.bs_per_dev
626
+ log(f"bs/rank={local_bs} global_bs={local_bs*world_size} tok/step={local_bs*world_size*hp.seq_len:,}")
627
+ log(f"iterations={hp.iterations} warmup={hp.warmup_steps} warmdown={hp.warmdown_iters} "
628
+ f"→ target_tok={hp.iterations*local_bs*world_size*hp.seq_len/1e9:.2f}B")
629
+
630
+ def lr_scale(step):
631
+ if step < hp.warmup_steps:
632
+ return step / max(hp.warmup_steps, 1)
633
+ decay_start = hp.iterations - hp.warmdown_iters
634
+ if step < decay_start:
635
+ return 1.0
636
+ progress = (step - decay_start) / max(hp.warmdown_iters, 1)
637
+ return max(1.0 - progress * (1.0 - hp.lr_min_scale), hp.lr_min_scale)
638
+
639
+ Path(hp.ckpt_dir).mkdir(parents=True, exist_ok=True)
640
+
641
+ # ---- loss-trace CSV (per-step; UN sync ogni train_log_every) ----
642
+ csv_f = csv_w = None
643
+ if is_main:
644
+ Path(hp.loss_trace_csv).parent.mkdir(parents=True, exist_ok=True)
645
+ csv_f = open(hp.loss_trace_csv, "w", newline="")
646
+ csv_w = csv.writer(csv_f)
647
+ csv_w.writerow(["step", "loss", "gnorm", "lr", "math_frac"])
648
+ log(f"loss_trace_csv={hp.loss_trace_csv}")
649
+ loss_buf, gn_buf, lr_buf, step_buf, mf_buf = [], [], [], [], []
650
+
651
+ # ---- Layer looping: pre-compile both variants ----
652
+ loop_activation_step = int(hp.iterations * hp.loop_activation_frac) if hp.loop_style else None
653
+ loop_active = False
654
+ if hp.loop_style and hp.loop_pre_warm:
655
+ log(f"[loop] pre-compiling NORMAL + LOOPED graphs (one-shot, può richiedere 5-15 min)")
656
+ x_warm = torch.zeros((local_bs, hp.seq_len), dtype=torch.long, device=device)
657
+ y_warm = torch.zeros_like(x_warm)
658
+ with torch.no_grad():
659
+ model.looped_mode = False
660
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16, enabled=True):
661
+ _ = compiled(x_warm, y_warm)
662
+ torch.cuda.synchronize()
663
+ log(f"[loop] NORMAL fwd graph compiled")
664
+ model.looped_mode = True
665
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16, enabled=True):
666
+ _ = compiled(x_warm, y_warm)
667
+ torch.cuda.synchronize()
668
+ log(f"[loop] LOOPED fwd graph compiled")
669
+ model.looped_mode = False
670
+ del x_warm, y_warm
671
+
672
+ torch.cuda.synchronize()
673
+ t0 = time.perf_counter()
674
+ total_tok_seen = 0
675
+ nan_abort = False
676
+
677
+ for step in range(1, hp.iterations + 1):
678
+ if loop_activation_step is not None and not loop_active and step >= loop_activation_step:
679
+ log(f"[loop] ACTIVATING looping at step {step} (gates α=0, identity passthrough)")
680
+ model.looped_mode = True
681
+ loop_active = True
682
+ for opt in optimizers:
683
+ opt.zero_grad(set_to_none=True)
684
+
685
+ x, y, ds_ids = train_loader.next_batch(local_bs)
686
+ total_tok_seen += local_bs * world_size * hp.seq_len
687
+
688
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16, enabled=True):
689
+ loss = model_for_train(x, y)
690
+
691
+ loss.backward()
692
+
693
+ scale = lr_scale(step)
694
+ for opt in optimizers:
695
+ for g in opt.param_groups:
696
+ g["lr"] = g["base_lr"] * scale
697
+ frac = min(step / hp.muon_momentum_warmup_steps, 1.0) if hp.muon_momentum_warmup_steps > 0 else 1.0
698
+ mom = (1 - frac) * hp.muon_momentum_warmup_start + frac * hp.muon_momentum
699
+ for g in opt_muon.param_groups:
700
+ g["momentum"] = mom
701
+
702
+ gn = torch.nn.utils.clip_grad_norm_(model.parameters(), hp.grad_clip)
703
+ for opt in optimizers:
704
+ opt.step()
705
+
706
+ # per-step buffers (NO sync qui — un solo sync ogni train_log_every al flush)
707
+ loss_buf.append(loss.detach())
708
+ gn_buf.append(gn.detach())
709
+ lr_buf.append(opt_muon.param_groups[0]["lr"])
710
+ step_buf.append(step)
711
+ mf_buf.append(sum(d not in hp.nl_datasets for d in ds_ids) / len(ds_ids))
712
+
713
+ if step % hp.train_log_every == 0 or step == 1 or step == hp.iterations:
714
+ torch.cuda.synchronize()
715
+ ls = torch.stack(loss_buf).float().cpu().tolist()
716
+ gs = torch.stack(gn_buf).float().cpu().tolist()
717
+ if not all(np.isfinite(ls)):
718
+ log(f"!! NaN/Inf loss intorno a step {step}, abort")
719
+ nan_abort = True
720
+ if is_main:
721
+ for s, l, g, lr_, mf in zip(step_buf, ls, gs, lr_buf, mf_buf):
722
+ csv_w.writerow([s, f"{l:.5f}", f"{g:.3f}", f"{lr_:.3e}", f"{mf:.2f}"])
723
+ csv_f.flush()
724
+ elapsed = time.perf_counter() - t0
725
+ tps = total_tok_seen / max(elapsed, 1e-9)
726
+ loop_tag = ""
727
+ if loop_active:
728
+ alphas = [g.alpha.detach().abs().item()
729
+ for seg_gates in model.loop_gates for g in seg_gates]
730
+ loop_tag = f" [LOOP |α|avg={sum(alphas)/max(len(alphas),1):.3f}]"
731
+ log(f"step {step:>5d}/{hp.iterations}{loop_tag} loss={ls[-1]:.4f} "
732
+ f"gnorm={gs[-1]:.3f} lr={lr_buf[-1]:.2e} mathfrac={mf_buf[-1]:.2f} "
733
+ f"tok={total_tok_seen/1e9:.2f}B tok/s={tps:>10,.0f} elapsed={elapsed:.1f}s")
734
+ loss_buf.clear(); gn_buf.clear(); lr_buf.clear(); step_buf.clear(); mf_buf.clear()
735
+
736
+ if nan_abort:
737
+ break
738
+
739
+ if (hp.save_interval > 0 and is_main and step % hp.save_interval == 0
740
+ and step != hp.iterations):
741
+ ckpt_path = Path(hp.ckpt_dir) / f"step_{step:06d}.pt"
742
+ torch.save({"step": step, "model": model.state_dict(), "vocab_size": vocab_size}, ckpt_path)
743
+ log(f" [ckpt] saved {ckpt_path.name}")
744
+ intermediates = [p for p in sorted(Path(hp.ckpt_dir).glob("step_*.pt")) if "_final" not in p.name]
745
+ for old in intermediates[:-hp.keep_n_recent]:
746
+ old.unlink()
747
+
748
+ if hp.save_final and is_main and not nan_abort:
749
+ ckpt_path = Path(hp.ckpt_dir) / f"step_{hp.iterations:06d}_final.pt"
750
+ torch.save({"step": hp.iterations, "model": model.state_dict(), "vocab_size": vocab_size}, ckpt_path)
751
+ log(f"saved {ckpt_path}")
752
+ if is_main and csv_f is not None:
753
+ csv_f.close()
754
+
755
+ if distributed:
756
+ dist.destroy_process_group()
757
+
758
+
759
+ if __name__ == "__main__":
760
+ main()
pytorch_model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a69844e97388c554ad1e71d638b627740ee80af32b0018a140c11cde8b33067d
3
+ size 780459194
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch>=2.4
2
+ numpy
3
+ transformers>=4.40
4
+ cut-cross-entropy
5
+ huggingface_hub>=1.0
vathos/Analyzer.py ADDED
@@ -0,0 +1,706 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Vathos ModdedFormer Analyzer API
3
+ =================================
4
+ A pure-Python/Jupyter alternative to the Streamlit inspector.
5
+ Designed for rigorous, programmatic topological and statistical inspection
6
+ of Transformer parameters, gradients, and activation manifolds.
7
+
8
+ Usage in a Jupyter Notebook:
9
+ ----------------------------
10
+ import torch
11
+ from vathos_analyzer import VathosAnalyzer
12
+
13
+ # 1. Initialize
14
+ analyzer = VathosAnalyzer(model)
15
+
16
+ # 2. Plot Architecture Overview & Parameter distributions
17
+ analyzer.plot_overview()
18
+ analyzer.plot_weight_distributions(layer_idx=0, component="both")
19
+
20
+ # 3. Manifold Extraction via Forward Hooks
21
+ x = torch.randint(0, model.vocab_size, (1, 64))
22
+ analyzer.capture_forward(x) # Installs hooks, runs forward, saves state
23
+
24
+ # 4. Explore Layer Topology (Attention & FFN mapping f(x))
25
+ analyzer.plot_attention_weights(layer_idx=0)
26
+ analyzer.plot_ffn_manifold(layer_idx=0)
27
+
28
+ # 5. Training History
29
+ analyzer.plot_training_history()
30
+
31
+ # --- OR USE THE ONE-LINER MACRO ---
32
+ # from vathos_analyzer import study
33
+ # analyzer = study(model)
34
+ """
35
+
36
+ import math
37
+ import numpy as np
38
+ import torch
39
+ import torch.nn as nn
40
+ import matplotlib.pyplot as plt
41
+ from typing import Optional, Dict, List, Union
42
+
43
+ try:
44
+ from IPython.display import display
45
+ import pandas as pd
46
+
47
+ HAS_IPYTHON = True
48
+ except ImportError:
49
+ HAS_IPYTHON = False
50
+
51
+ # ──────────────────────────────────────────────────────────────────────────────
52
+ # DARK THEME STYLING
53
+ # ──────────────────────────────────────────────────────────────────────────────
54
+ DARK_THEME = {
55
+ "figure.facecolor": "#0D0D1A",
56
+ "axes.facecolor": "#0D0D1A",
57
+ "axes.edgecolor": "#252550",
58
+ "axes.labelcolor": "#C0C0E0",
59
+ "xtick.color": "#9CA3AF",
60
+ "ytick.color": "#9CA3AF",
61
+ "text.color": "#D8D8F0",
62
+ "grid.color": "#1E1E3A",
63
+ "grid.alpha": 0.6,
64
+ "legend.facecolor": "#11111F",
65
+ "legend.edgecolor": "#252550",
66
+ }
67
+ COLORS = ["#A78BFA", "#60A5FA", "#34D399", "#F472B6", "#FB923C",
68
+ "#FBBF24", "#38BDF8", "#A3E635", "#E879F9", "#F87171"]
69
+
70
+
71
+ def apply_theme():
72
+ plt.rcParams.update(DARK_THEME)
73
+
74
+
75
+ # ──────────────────────────────────────────────────────────────────────────────
76
+ # CORE ANALYZER CLASS
77
+ # ──────────────────────────────────────────────────────────────────────────────
78
+ class VathosAnalyzer:
79
+ def __init__(self, model: nn.Module, dark_mode: bool = True):
80
+ """
81
+ Initializes the analyzer.
82
+ :param model: The Vathos ModdedFormer instance.
83
+ """
84
+ self.model = model
85
+ self.captured_activations: Dict[int, Dict[str, torch.Tensor]] = {}
86
+ if dark_mode:
87
+ apply_theme()
88
+
89
+ # ─── EXTRACTION LOGIC ─────────────────────────────────────────────────────
90
+
91
+ def _is_attention(self, mixer) -> bool:
92
+ cls_name = type(mixer).__name__.lower()
93
+ return "attention" in cls_name or "attn" in cls_name
94
+
95
+ def _tensor_stats(self, t: torch.Tensor) -> dict:
96
+ f = t.detach().float().cpu()
97
+ v = f.numpy().flatten()
98
+ return {
99
+ "shape": tuple(t.shape),
100
+ "numel": t.numel(),
101
+ "mean": float(v.mean()),
102
+ "std": float(v.std()),
103
+ "min": float(v.min()),
104
+ "max": float(v.max()),
105
+ "l2": float(np.linalg.norm(v)),
106
+ "sparsity": float((np.abs(v) < 1e-6).mean()),
107
+ }
108
+
109
+ def _svd_values(self, t: torch.Tensor) -> Optional[np.ndarray]:
110
+ f = t.detach().float().cpu()
111
+ if f.ndim == 2:
112
+ try:
113
+ return torch.linalg.svdvals(f).numpy()
114
+ except Exception:
115
+ return None
116
+ if f.ndim == 3:
117
+ results = []
118
+ for i in range(f.shape[0]):
119
+ try:
120
+ results.append(torch.linalg.svdvals(f[i]).numpy())
121
+ except Exception:
122
+ pass
123
+ return np.stack(results) if results else None
124
+ return None
125
+
126
+ def _get_attention_weights(self, mixer, x: torch.Tensor) -> Optional[np.ndarray]:
127
+ if hasattr(mixer, "get_attention_weights"):
128
+ try:
129
+ with torch.no_grad():
130
+ w = mixer.get_attention_weights(x)
131
+ return w.detach().float().cpu().numpy()
132
+ except Exception:
133
+ pass
134
+ try:
135
+ with torch.no_grad():
136
+ B, L, D = x.shape
137
+ if hasattr(mixer, "qkv"):
138
+ proj = mixer.qkv
139
+ n_heads = mixer.n_heads
140
+ head_dim = mixer.head_dim
141
+ qkv = proj(x).view(B, L, 3, n_heads, head_dim)
142
+ q, k, v = qkv.unbind(dim=2)
143
+ q, k = q.transpose(1, 2).float(), k.transpose(1, 2).float()
144
+ scale = math.sqrt(head_dim)
145
+ scores = torch.matmul(q, k.transpose(-2, -1)) / scale
146
+ if getattr(mixer, "causal", True):
147
+ mask = torch.tril(torch.ones(L, L, device=x.device)).bool()
148
+ scores = scores.masked_fill(~mask, float("-inf"))
149
+ weights = torch.softmax(scores, dim=-1)
150
+ return weights[0].cpu().numpy()
151
+ elif hasattr(mixer, "qk"):
152
+ proj = mixer.qk
153
+ n_heads = mixer.n_heads
154
+ head_dim = mixer.head_dim
155
+ qk = proj(x).view(B, L, 2, n_heads, head_dim)
156
+ q, k = qk.unbind(dim=2)
157
+ q, k = q.transpose(1, 2).float(), k.transpose(1, 2).float()
158
+ scale = math.sqrt(head_dim)
159
+ scores = torch.matmul(q, k.transpose(-2, -1)) / scale
160
+ if getattr(mixer, "causal", True):
161
+ mask = torch.tril(torch.ones(L, L, device=x.device)).bool()
162
+ scores = scores.masked_fill(~mask, float("-inf"))
163
+ weights = torch.softmax(scores, dim=-1)
164
+ return weights[0].cpu().numpy()
165
+ except Exception:
166
+ pass
167
+ return None
168
+
169
+ def get_layer_params(self, layer_idx: int, component: str = "both") -> Dict[str, torch.Tensor]:
170
+ block = self.model.blocks[layer_idx]
171
+ sm = block.spatial_mixer
172
+ cm = block.channel_mixer
173
+
174
+ selected_modules = {}
175
+ if component in ["spatial", "both"]:
176
+ selected_modules["spatial"] = sm
177
+ if component in ["channel", "both"]:
178
+ selected_modules["channel"] = cm
179
+
180
+ all_params = {}
181
+ for prefix, module in selected_modules.items():
182
+ for name, p in module.named_parameters():
183
+ all_params[f"{prefix}.{name}"] = p
184
+ return all_params
185
+
186
+ # ─── FORWARD PASS HOOKING (MANIFOLD CAPTURE) ──────────────────────────────
187
+
188
+ def capture_forward(self, x: torch.Tensor, clear_previous: bool = True):
189
+ """
190
+ Runs a forward pass while attaching hooks to capture inputs/outputs of
191
+ Spatial Mixers, and the strict pre/post activations of Channel Mixers.
192
+ """
193
+ if clear_previous:
194
+ self.captured_activations.clear()
195
+
196
+ handles = []
197
+
198
+ for i, block in enumerate(self.model.blocks):
199
+ self.captured_activations[i] = {}
200
+
201
+ # Hook 1: Input to Spatial Mixer (For Q/K/V computations)
202
+ def make_spatial_hook(layer_idx):
203
+ def hook(m, inp):
204
+ self.captured_activations[layer_idx]["spatial_in"] = inp[0].detach()
205
+
206
+ return hook
207
+
208
+ handles.append(block.spatial_mixer.register_forward_pre_hook(make_spatial_hook(i)))
209
+
210
+ cm = block.channel_mixer
211
+
212
+ # Hook 2 & 3: Channel Mixer Expansion / Contraction (Pre and Post Activation)
213
+ if hasattr(cm, 'expand') and hasattr(cm, 'contract'):
214
+ def make_expand_hook(layer_idx):
215
+ def hook(m, inp, out):
216
+ self.captured_activations[layer_idx]["channel_pre_act"] = out.detach()
217
+
218
+ return hook
219
+
220
+ def make_contract_hook(layer_idx):
221
+ def hook(m, inp):
222
+ self.captured_activations[layer_idx]["channel_post_act"] = inp[0].detach()
223
+
224
+ return hook
225
+
226
+ handles.append(cm.expand.register_forward_hook(make_expand_hook(i)))
227
+ handles.append(cm.contract.register_forward_pre_hook(make_contract_hook(i)))
228
+ else:
229
+ # Fallback per block custom
230
+ def make_cm_hook(layer_idx):
231
+ def hook(m, inp):
232
+ self.captured_activations[layer_idx]["channel_in"] = inp[0].detach()
233
+
234
+ return hook
235
+
236
+ handles.append(cm.register_forward_pre_hook(make_cm_hook(i)))
237
+
238
+ # Run forward pass
239
+ self.model.eval()
240
+ try:
241
+ device = next(self.model.parameters()).device
242
+ except StopIteration:
243
+ device = torch.device("cpu")
244
+
245
+ with torch.no_grad():
246
+ self.model(x.to(device))
247
+
248
+ # Cleanup hooks
249
+ for h in handles:
250
+ h.remove()
251
+
252
+ print(f"✅ Manifolds captured for {len(self.model.blocks)} layers.")
253
+
254
+ # ─── PLOTTING ROUTINES ────────────────────────────────────────────────────
255
+
256
+ def show_fig(self, fig: plt.Figure):
257
+ """Helper to show a figure inline or wait for script end."""
258
+ if HAS_IPYTHON:
259
+ display(fig)
260
+ plt.close(fig)
261
+ else:
262
+ fig.show()
263
+
264
+ def plot_overview(self):
265
+ """Plots topological params: param norms, skip lambdas, zeroskips."""
266
+ print("=== 🏗️ Model Topological Overview ===")
267
+ total_params = sum(p.numel() for p in self.model.parameters())
268
+ print(f"Total Params: {total_params:,} | Layers: {self.model.n_layer} | Embed Dim: {self.model.embed_dim}")
269
+
270
+ # 1. Param Norms
271
+ layer_names, l2_norms = [], []
272
+ for i, block in enumerate(self.model.blocks):
273
+ for name, p in block.named_parameters():
274
+ layer_names.append(f"L{i}.{name}")
275
+ l2_norms.append(float(p.detach().norm(2)))
276
+
277
+ fig, ax = plt.subplots(figsize=(max(8, len(layer_names) * 0.35), 4))
278
+ ax.bar(range(len(layer_names)), l2_norms, color="#60A5FA", alpha=0.8)
279
+ ax.set_xticks(range(len(layer_names)))
280
+ ax.set_xticklabels(layer_names, rotation=90, fontsize=6)
281
+ ax.set_ylabel("L2 norm")
282
+ ax.set_title("Per-parameter L2 Norms", color="#A78BFA")
283
+ ax.grid(True, ls="--", alpha=0.4, axis="y")
284
+ fig.tight_layout()
285
+ self.show_fig(fig)
286
+
287
+ # 2. Skip Lambdas
288
+ if hasattr(self.model, "skip_lambdas") and self.model.skip_lambdas:
289
+ lambdas = {k: float(v.detach()) for k, v in self.model.skip_lambdas.items()}
290
+ fig2, ax2 = plt.subplots(figsize=(max(6, len(lambdas) * 1.4), 3.5))
291
+ keys = list(lambdas.keys())
292
+ vals = [lambdas[k] for k in keys]
293
+ bars = ax2.bar(range(len(keys)), vals, color=COLORS[:len(keys)], alpha=0.85)
294
+ ax2.set_xticks(range(len(keys)))
295
+ ax2.set_xticklabels([k.replace("route_", "").replace("_to_", "→") for k in keys], rotation=30, ha="right",
296
+ fontsize=8)
297
+ ax2.axhline(0, color="#9CA3AF", linewidth=0.8, ls="--")
298
+ ax2.set_title("Skip-connection Gate Values (λ)", color="#A78BFA")
299
+ for bar, v in zip(bars, vals):
300
+ ax2.text(bar.get_x() + bar.get_width() / 2, v + 0.003, f"{v:.4f}", ha="center", va="bottom", fontsize=8,
301
+ color="#D8D8F0")
302
+ fig2.tight_layout()
303
+ self.show_fig(fig2)
304
+
305
+ # 3. Zeroskip Params
306
+ if hasattr(self.model, "zeroskip") and self.model.zeroskip:
307
+ vals = [float(p.detach()) for p in self.model.zeroskip_params]
308
+ fig3, ax3 = plt.subplots(figsize=(max(6, len(vals) * 0.7), 3.5))
309
+ ax3.plot(vals, marker="o", color="#FBBF24", linewidth=1.5, markersize=6)
310
+ ax3.axhline(0, color="#9CA3AF", linewidth=0.8, ls="--")
311
+ ax3.set_xlabel("layer index")
312
+ ax3.set_ylabel("zeroskip α")
313
+ ax3.set_title("ZeroSkip Parameters (x₀ coefficient)", color="#A78BFA")
314
+ ax3.grid(True, ls="--", alpha=0.4)
315
+ fig3.tight_layout()
316
+ self.show_fig(fig3)
317
+
318
+ def print_layer_stats(self, layer_idx: int, component: str = "both"):
319
+ """Prints a statistical dataframe of parameters for a given layer."""
320
+ params = self.get_layer_params(layer_idx, component)
321
+ rows = []
322
+ for pname, p in params.items():
323
+ s = self._tensor_stats(p)
324
+ rows.append({
325
+ "Parameter": pname, "Shape": str(s["shape"]), "Numel": s['numel'],
326
+ "Mean": s['mean'], "Std": s['std'], "Min": s['min'],
327
+ "Max": s['max'], "L2": s['l2'], "Sparsity": s['sparsity'],
328
+ "Grad": "Yes" if p.grad is not None else "No",
329
+ })
330
+ if HAS_IPYTHON:
331
+ df = pd.DataFrame(rows)
332
+ display(df)
333
+ else:
334
+ for r in rows:
335
+ print(r)
336
+
337
+ def plot_weight_distributions(self, layer_idx: int, component: str = "both", plot_gradients: bool = False,
338
+ bins: int = 80):
339
+ params = self.get_layer_params(layer_idx, component)
340
+ if plot_gradients:
341
+ params = {k: v.grad for k, v in params.items() if v.grad is not None}
342
+ if not params:
343
+ print("No gradients found. Run loss.backward() first.")
344
+ return
345
+
346
+ n = len(params)
347
+ if n == 0: return
348
+ cols = min(n, 3)
349
+ rows = math.ceil(n / cols)
350
+ fig, axes = plt.subplots(rows, cols, figsize=(5 * cols, 3.5 * rows))
351
+ if n == 1:
352
+ axes = np.array([[axes]])
353
+ elif rows == 1:
354
+ axes = axes.reshape(1, -1)
355
+
356
+ for idx, (name, t) in enumerate(params.items()):
357
+ ax = axes[idx // cols][idx % cols]
358
+ v = t.detach().float().cpu().numpy().flatten()
359
+ st_val = self._tensor_stats(t)
360
+ ax.hist(v, bins=bins, color=COLORS[idx % len(COLORS)], alpha=0.8, density=True)
361
+ ax.set_title(f"{name}\nμ={st_val['mean']:.3e} σ={st_val['std']:.3e}", fontsize=9)
362
+ ax.grid(True, ls="--", alpha=0.4)
363
+
364
+ for idx in range(n, rows * cols):
365
+ axes[idx // cols][idx % cols].set_visible(False)
366
+
367
+ fig.suptitle(f"Layer {layer_idx} - {'Gradient' if plot_gradients else 'Weight'} Distributions", color="#A78BFA",
368
+ fontsize=12)
369
+ fig.tight_layout()
370
+ self.show_fig(fig)
371
+
372
+ def plot_svd_spectrum(self, layer_idx: int, component: str = "both", plot_gradients: bool = False,
373
+ log_scale: bool = True):
374
+ params = self.get_layer_params(layer_idx, component)
375
+ if plot_gradients:
376
+ params = {k: v.grad for k, v in params.items() if v.grad is not None}
377
+
378
+ eligible = {k: v for k, v in params.items() if v.ndim >= 2}
379
+ if not eligible:
380
+ print("No 2D parameters found for SVD.")
381
+ return
382
+
383
+ n = len(eligible)
384
+ cols = min(n, 3)
385
+ rows = math.ceil(n / cols)
386
+ fig, axes = plt.subplots(rows, cols, figsize=(5 * cols, 3.5 * rows))
387
+ if n == 1:
388
+ axes = np.array([[axes]])
389
+ elif rows == 1:
390
+ axes = axes.reshape(1, -1)
391
+
392
+ for idx, (name, t) in enumerate(eligible.items()):
393
+ ax = axes[idx // cols][idx % cols]
394
+ sv = self._svd_values(t)
395
+ if sv is None: continue
396
+
397
+ if sv.ndim == 2:
398
+ for h in range(sv.shape[0]):
399
+ ax.plot(sv[h], color=COLORS[idx % len(COLORS)], alpha=0.3, linewidth=0.8)
400
+ ax.plot(sv.mean(0), color=COLORS[idx % len(COLORS)], linewidth=2, label="mean")
401
+ ax.legend(fontsize=8)
402
+ else:
403
+ ax.plot(sv, color=COLORS[idx % len(COLORS)], linewidth=1.5, marker=".", markersize=3)
404
+
405
+ cond = sv.flatten()[0] / (sv.flatten()[-1] + 1e-12)
406
+ ax.set_title(f"{name} | cond={cond:.1f}", fontsize=9)
407
+ if log_scale: ax.set_yscale("log")
408
+ ax.grid(True, ls="--", alpha=0.4)
409
+
410
+ for idx in range(n, rows * cols):
411
+ axes[idx // cols][idx % cols].set_visible(False)
412
+
413
+ fig.suptitle(f"Layer {layer_idx} - Singular Value Spectra ($\Sigma$)", color="#A78BFA", fontsize=12)
414
+ fig.tight_layout()
415
+ self.show_fig(fig)
416
+
417
+ # ─── MANIFOLD EXPLORATION (ATTENTION E FFN) ───────────────────────────────
418
+
419
+ def plot_attention_weights(self, layer_idx: int, tokens: Optional[List[str]] = None):
420
+ """Plots the attention map of the layer using the captured manifold."""
421
+ if layer_idx not in self.captured_activations or "spatial_in" not in self.captured_activations[layer_idx]:
422
+ print(f"No captured input for layer {layer_idx}. Run `capture_forward(x)` first.")
423
+ return
424
+
425
+ sm = self.model.blocks[layer_idx].spatial_mixer
426
+ if not self._is_attention(sm):
427
+ print(f"Layer {layer_idx} spatial mixer is not Attention (is {type(sm).__name__}).")
428
+ return
429
+
430
+ x = self.captured_activations[layer_idx]["spatial_in"]
431
+ w = self._get_attention_weights(sm, x)
432
+ if w is None:
433
+ print("Could not extract attention weights.")
434
+ return
435
+
436
+ if w.ndim == 2: w = w[np.newaxis]
437
+ H, L_q, L_k = w.shape
438
+ cols = min(H, 4)
439
+ rows = math.ceil(H / cols)
440
+
441
+ fig, axes = plt.subplots(rows, cols, figsize=(4 * cols, 3.5 * rows))
442
+ axes = np.array(axes).flatten() if H > 1 else [axes]
443
+
444
+ for h in range(H):
445
+ ax = axes[h]
446
+ wh = w[h]
447
+ im = ax.imshow(wh, aspect="auto", cmap="magma", vmin=0, vmax=wh.max() + 1e-9)
448
+ ax.set_title(f"Head {h}", fontsize=10, color="#C4B5FD")
449
+ if tokens and len(tokens) == L_q:
450
+ clean_tokens = [t.replace('Ġ', ' ') for t in tokens]
451
+ fs = max(4, min(10, int(300 / L_q)))
452
+ ax.set_xticks(range(L_k))
453
+ ax.set_yticks(range(L_q))
454
+ ax.set_xticklabels(clean_tokens, rotation=90, fontsize=fs, color="#9CA3AF")
455
+ ax.set_yticklabels(clean_tokens, fontsize=fs, color="#9CA3AF")
456
+ fig.colorbar(im, ax=ax, shrink=0.8)
457
+
458
+ for h in range(H, len(axes)):
459
+ axes[h].set_visible(False)
460
+
461
+ fig.suptitle(f"Layer {layer_idx} - Attention Topology Analysis", color="#A78BFA", fontsize=12)
462
+ fig.tight_layout()
463
+ self.show_fig(fig)
464
+
465
+ def plot_ffn_manifold(self, layer_idx: int):
466
+ """
467
+ Traccia la densità (PDF empirica) delle attivazioni prima della non linearità (XW_exp),
468
+ sovrapposta alla mappa non-lineare $f(x)$. Questo rivela immediatamente condizioni
469
+ di annullamento gradiente (dead neurons) o eccessiva saturazione.
470
+ Dipende dai Forward Hooks eseguiti tramite `capture_forward()`.
471
+ """
472
+ if layer_idx not in self.captured_activations:
473
+ print(f"No manifold captured. Run `capture_forward(x)` first.")
474
+ return
475
+
476
+ cap = self.captured_activations[layer_idx]
477
+ cm = self.model.blocks[layer_idx].channel_mixer
478
+
479
+ if "channel_pre_act" in cap and "channel_post_act" in cap:
480
+ pre_act = cap["channel_pre_act"][0].cpu().numpy()
481
+ post_act = cap["channel_post_act"][0].cpu().numpy()
482
+ elif "channel_in" in cap:
483
+ # Fallback se non ci sono expand/contract espliciti
484
+ x = cap["channel_in"]
485
+ data = self._get_ffn_activations(cm, x, temp=1.0)
486
+ if not data:
487
+ print("Could not compute FFN manifold.")
488
+ return
489
+ pre_act = data["pre_act"]
490
+ post_act = data["post_act"]
491
+ else:
492
+ print("Missing necessary hooks data for FFN.")
493
+ return
494
+
495
+ # Estrazione act_fn per mapping analitico
496
+ act_fn = getattr(cm, "activation", getattr(cm, "act", getattr(cm, "act_fn", None)))
497
+
498
+ fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
499
+ pre_flat = pre_act.flatten()
500
+ post_flat = post_act.flatten()
501
+
502
+ # ── Subplot 1: Pre-act vs f(x) ──
503
+ ax1 = axes[0]
504
+ ax1_twin = ax1.twinx()
505
+
506
+ # Dominio assoluto
507
+ true_min = float(pre_flat.min())
508
+ true_max = float(pre_flat.max())
509
+ x_min = min(-5.0, true_min - 1.0)
510
+ x_max = max(5.0, true_max + 1.0)
511
+ x_vals = np.linspace(x_min, x_max, 1000)
512
+
513
+ y_vals = None
514
+ is_empirical = False
515
+
516
+ # 1. Tentativo analitico
517
+ if act_fn is not None:
518
+ try:
519
+ device = next(cm.parameters()).device if list(cm.parameters()) else "cpu"
520
+ dtype = next(cm.parameters()).dtype if list(cm.parameters()) else torch.float32
521
+ with torch.no_grad():
522
+ xt = torch.tensor(x_vals, dtype=dtype, device=device).view(1, 1, -1)
523
+ yt = act_fn(xt)
524
+ y_vals = yt.cpu().float().numpy().flatten()
525
+ except Exception:
526
+ pass
527
+
528
+ # 2. Empirico / Fallback
529
+ if (y_vals is None or np.array_equal(y_vals, x_vals)) and pre_flat.shape == post_flat.shape:
530
+ if not np.allclose(pre_flat, post_flat):
531
+ sort_idx = np.argsort(pre_flat)
532
+ x_vals = pre_flat[sort_idx]
533
+ y_vals = post_flat[sort_idx]
534
+ is_empirical = True
535
+ else:
536
+ y_vals = x_vals
537
+ elif y_vals is None:
538
+ y_vals = x_vals
539
+
540
+ label_map = "$f(x)$ map (Empirica)" if is_empirical else "$f(x)$ map"
541
+
542
+ # Curve
543
+ ax1.plot(x_vals, y_vals, color="#38BDF8", lw=3.0, label=label_map, zorder=4)
544
+ ax1.axvline(0, color="#4B5563", lw=1.5, ls="--", zorder=1)
545
+ ax1.axhline(0, color="#4B5563", lw=1.5, ls="--", zorder=1)
546
+
547
+ # Min / Max assoluti (Individuazione leak instabili)
548
+ ax1.plot(true_min, 0, marker='v', color='#EF4444', markersize=7, zorder=5)
549
+ ax1.plot(true_max, 0, marker='v', color='#EF4444', markersize=7, zorder=5)
550
+ ax1.text(true_min, 0.05, f"Min:\n{true_min:.1f}", color='#EF4444', ha='center', va='bottom', fontsize=9,
551
+ transform=ax1.get_xaxis_transform())
552
+ ax1.text(true_max, 0.05, f"Max:\n{true_max:.1f}", color='#EF4444', ha='center', va='bottom', fontsize=9,
553
+ transform=ax1.get_xaxis_transform())
554
+
555
+ ax1.set_xlabel("Pre-activation $x = (X W_{exp})$", fontsize=10)
556
+ ax1.set_ylabel("Activation Output $f(x)$", color="#38BDF8", fontsize=10)
557
+ ax1.tick_params(axis='y', labelcolor="#38BDF8")
558
+
559
+ # PDF pre-attivazione
560
+ ax1_twin.hist(pre_flat, bins=150, range=(x_min, x_max), color="#F472B6", alpha=0.4, density=True, zorder=2)
561
+ ax1_twin.set_ylabel("Empirical PDF $p(x)$", color="#F472B6", fontsize=10)
562
+ ax1_twin.tick_params(axis='y', labelcolor="#F472B6")
563
+
564
+ ax1.set_title(f"L{layer_idx} - Overlay: Activation $f(x)$ & Input Distribution", color="#A78BFA", fontsize=11)
565
+ ax1.legend(loc="upper left", fontsize=9)
566
+
567
+ # ── Subplot 2: Post-act distribution (Sparsity check) ──
568
+ ax2 = axes[1]
569
+ post_min, post_max = np.percentile(post_flat, [0.0, 99.9])
570
+ ax2.hist(post_flat, bins=120, range=(post_min, max(post_max, 1e-3)), color="#F472B6", alpha=0.6, density=True)
571
+ ax2.axvline(0, color="#9CA3AF", lw=1, ls="--", alpha=0.5)
572
+ ax2.set_xlabel("Post-activation $Act(X W_{exp})$", fontsize=10)
573
+ ax2.set_ylabel("Density", fontsize=10)
574
+ ax2.set_title("Post-Activation Distribution (Sparsity check)", color="#A78BFA", fontsize=11)
575
+ ax2.grid(True, ls="--", alpha=0.3)
576
+
577
+ fig.tight_layout()
578
+ self.show_fig(fig)
579
+
580
+ # ─── TRAINING HISTORY ───────────────────────────��─────────────────────────
581
+
582
+ def plot_training_history(self):
583
+ """Plots losses and metrics matching the Vathos logic."""
584
+ losses_dict = getattr(self.model, "_losses_dict", {})
585
+ losses_ep = getattr(self.model, "_losses_per_epoch_dict", {})
586
+ metrics_ep = getattr(self.model, "_metrics_per_epoch", {})
587
+
588
+ if not losses_dict and not metrics_ep:
589
+ print("No training history found in this model.")
590
+ return
591
+
592
+ print("=== 📈 Training History ===")
593
+ print(
594
+ f"Steps: {getattr(self.model, 'steps', '-')} | Epochs: {getattr(self.model, 'epochs', '-')} | Best Loss: {getattr(self.model, 'best_loss', '-')}")
595
+
596
+ if losses_dict or losses_ep:
597
+ fig, ax = plt.subplots(figsize=(10, 4))
598
+ if losses_dict:
599
+ xs = list(losses_dict.keys())
600
+ ys = list(losses_dict.values())
601
+ ax.plot(xs, ys, color="#4c9be8", linewidth=0.8, alpha=0.4, label="Loss (step)")
602
+ # Smooth curve
603
+ if len(ys) > 10:
604
+ win = max(2, len(ys) // 20)
605
+ ys_sm = np.convolve(ys, np.ones(win) / win, mode="valid")
606
+ ax.plot(xs[win - 1:], ys_sm, color="#4c9be8", linewidth=1.5, label=f"Smoothed (w={win})")
607
+
608
+ if losses_ep:
609
+ ex = list(losses_ep.keys())
610
+ ey = list(losses_ep.values())
611
+ ax.plot(ex, ey, color="#f5a623", linewidth=2.2, marker="o", markersize=5, label="Loss per epoch")
612
+
613
+ ax.set_title("Training Loss", color="#A78BFA")
614
+ ax.set_xlabel("Steps / Epochs")
615
+ ax.set_ylabel("Loss")
616
+ ax.grid(True, ls="--", alpha=0.4)
617
+ ax.legend(fontsize=9)
618
+ fig.tight_layout()
619
+ self.show_fig(fig)
620
+
621
+ # Plot Metrics
622
+ for t_idx, (m_name, m_vals) in enumerate(metrics_ep.items()):
623
+ fig, ax = plt.subplots(figsize=(10, 3.5))
624
+ col = COLORS[t_idx % len(COLORS)]
625
+ ax.plot(range(len(m_vals)), m_vals, color=col, linewidth=2.2, marker="o", markersize=5,
626
+ label=f"{m_name} per epoch")
627
+ ax.set_title(f"Metric: {m_name}", color="#A78BFA")
628
+ ax.set_xlabel("Epochs")
629
+ ax.set_ylabel(m_name)
630
+ ax.grid(True, ls="--", alpha=0.4)
631
+ ax.legend(fontsize=9)
632
+ fig.tight_layout()
633
+ self.show_fig(fig)
634
+
635
+ def plot_all(self, layer_idx: int = 0, tokens: Optional[List[str]] = None):
636
+ """
637
+ Esegue un'analisi topologica e statistica completa, chiamando in sequenza
638
+ tutte le routine grafiche. Genera un referto end-to-end del modello.
639
+ """
640
+ print(f"\n{'=' * 70}\n🔬 INITIATING FULL TOPOLOGICAL ANALYSIS (Focus Layer {layer_idx})\n{'=' * 70}")
641
+ self.plot_overview()
642
+
643
+ print(f"\n[1/4] Weight Distributions (Layer {layer_idx})")
644
+ self.plot_weight_distributions(layer_idx=layer_idx)
645
+
646
+ print(f"\n[2/4] SVD Spectrum & Condition Numbers (Layer {layer_idx})")
647
+ self.plot_svd_spectrum(layer_idx=layer_idx)
648
+
649
+ if not self.captured_activations:
650
+ print("\n⚠️ Nessun forward pass catturato nello stato. Salto l'analisi del manifold (Attention/FFN).")
651
+ print(" Chiama `capture_forward(x)` prima di `plot_all()` per abilitarla.")
652
+ else:
653
+ print(f"\n[3/4] Attention Topology (Layer {layer_idx})")
654
+ self.plot_attention_weights(layer_idx=layer_idx, tokens=tokens)
655
+
656
+ print(f"\n[4/4] FFN Manifold & Activation Geometry f(x) (Layer {layer_idx})")
657
+ self.plot_ffn_manifold(layer_idx=layer_idx)
658
+
659
+ print("\n--- Training History ---")
660
+ self.plot_training_history()
661
+ print(f"\n{'=' * 70}\n✅ ANALYSIS COMPLETE\n{'=' * 70}")
662
+
663
+
664
+ # ──────────────────────────────────────────────────────────────────────────────
665
+ # TOP-LEVEL MACRO
666
+ # ──────────────────────────────────────────────────────────────────────────────
667
+
668
+ def study(model: nn.Module, layer_idx: int = 0, dummy_seq_len: int = 64,
669
+ device: Optional[Union[str, torch.device]] = None) -> VathosAnalyzer:
670
+ """
671
+ Macro globale per profilare immediatamente un modello in un Notebook o script nudo.
672
+ 1. Assicura il corretto posizionamento e deduce il device dei tensori.
673
+ 2. Istanzia il VathosAnalyzer.
674
+ 3. Genera un input sintetico coerente col dominio.
675
+ 4. Cattura il manifold (iniettando i Forward Hooks estrae i pre-logits).
676
+ 5. Esegue tutta la suite grafica (plot_all).
677
+
678
+ Ritorna:
679
+ L'istanza di VathosAnalyzer configurata per ulteriori ispezioni interattive.
680
+ """
681
+ if device is None:
682
+ try:
683
+ device = next(model.parameters()).device
684
+ except StopIteration:
685
+ device = torch.device("cpu")
686
+ model.to(device)
687
+
688
+ print(f"🔍 Starting Rigorous Study on {type(model).__name__} (Device: {device})")
689
+
690
+ analyzer = VathosAnalyzer(model)
691
+
692
+ # Tentiamo di dedurre il vocab_size rigorosamente, con fallback
693
+ vocab_size = getattr(model, "vocab_size", 1000)
694
+ if not hasattr(model, "vocab_size") and hasattr(model, "embedder") and hasattr(model.embedder, "embedding"):
695
+ vocab_size = getattr(model.embedder.embedding, "num_embeddings", 1000)
696
+
697
+ # Dummy input per forzare il passaggio nei manifold locali ed estrarre la Jacobiana implicita
698
+ dummy_x = torch.randint(0, vocab_size, (1, dummy_seq_len), device=device)
699
+
700
+ print(f"📸 Capturing activation manifold with dummy sequence (L={dummy_seq_len})...")
701
+ analyzer.capture_forward(dummy_x)
702
+
703
+ # Lancia l'analisi completa concentrandosi sul layer indicato (0 di default)
704
+ analyzer.plot_all(layer_idx=layer_idx)
705
+
706
+ return analyzer
vathos/Transformers.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Basic Implementation of Famous Transformers Model using Vathos blocks SequenceModel.
2
+ serves firstly as an example on how to build with Vathos SequenceModel"""
3
+
4
+ from Vathos.blocks import *
5
+ import torch.nn as nn
6
+ from Vathos.functions import *
7
+
8
+
9
+ class GPTTransformer(nn.Module):
10
+ def __init__(self, vocab_size, d_model, n_layers, n_heads, max_len, ff_expand=2, activation='gelu', dropout=0.1,
11
+ pe='sinu'):
12
+ super(GPTTransformer, self).__init__()
13
+ self.vocab_size = vocab_size
14
+ self.d_model = d_model
15
+ self.n_layers = n_layers
16
+ self.n_heads = n_heads
17
+ self.ff_expand = ff_expand
18
+ self.activation = activation
19
+ self.dropout = dropout
20
+ if pe == 'sinu':
21
+ self.pe = True
22
+ self.rope = False
23
+ elif pe == 'rope':
24
+ self.pe = False
25
+ self.rope = True
26
+ elif self.pe == 'nope':
27
+ self.pe = False
28
+ else:
29
+ raise ValueError('pe={} not recognized'.format(pe))
30
+
31
+ self.model = SequenceModel(
32
+ vocab_size=vocab_size,
33
+ d_model=d_model,
34
+ n_layers=n_layers,
35
+ max_len=max_len,
36
+ pos_encoder=self.pe,
37
+ rope=self.rope,
38
+ spatial_args={"causal": True, "n_heads": n_heads, "rope": self.rope},
39
+ channel_args={"expand": ff_expand, "activation": ACTIVS[activation], "depth": 2}
40
+ )
41
+
42
+ def forward(self, x):
43
+ return self.model(x)
44
+
45
+ def __repr__(self):
46
+ return self.model.__repr__()
47
+
48
+ def __str__(self):
49
+ return self.model.__str__()
50
+
51
+ @torch.no_grad()
52
+ def generate(self, *args, **kwargs):
53
+ return self.model.generate(*args, *kwargs)
54
+
55
+ def summary(self):
56
+ self.model.summary()
57
+
58
+
59
+ class BERTTransformer(nn.Module):
60
+ def __init__(self, vocab_size, d_model, n_layers, n_heads, max_len, ff_expand=2, activation='gelu', dropout=0.1,
61
+ pe='sinu'):
62
+ super(GPTTransformer, self).__init__()
63
+ self.vocab_size = vocab_size
64
+ self.d_model = d_model
65
+ self.n_layers = n_layers
66
+ self.n_heads = n_heads
67
+ self.ff_expand = ff_expand
68
+ self.activation = activation
69
+ self.dropout = dropout
70
+ if pe == 'sinu':
71
+ self.pe = True
72
+ self.rope = False
73
+ elif pe == 'rope':
74
+ self.pe = False
75
+ self.rope = True
76
+ elif self.pe == 'nope':
77
+ self.pe = False
78
+ else:
79
+ raise ValueError('pe={} not recognized'.format(pe))
80
+
81
+ self.model = SequenceModel(
82
+ vocab_size=vocab_size,
83
+ d_model=d_model,
84
+ n_layers=n_layers,
85
+ max_len=max_len,
86
+ pos_encoder=self.pe,
87
+ rope=self.rope,
88
+ spatial_args={"causal": False, "n_heads": n_heads, "rope": self.rope},
89
+ channel_args={"expand": ff_expand, "activation": ACTIVS[activation], "depth": 2}
90
+ )
91
+
92
+ def forward(self, x):
93
+ return self.model(x)
94
+
95
+ def __repr__(self):
96
+ return self.model.__repr__()
97
+
98
+ def __str__(self):
99
+ return self.model.__str__()
100
+
101
+ @torch.no_grad()
102
+ def generate(self, *args, **kwargs):
103
+ raise NotImplementedError("Yet to Implement")
104
+
105
+ def summary(self):
106
+ self.model.summary()
107
+
108
+
109
+ class DeiTTransformer(nn.Module):
110
+ def __init__(self, n_class, img_size, patch_size, d_model, n_layers, n_heads, in_chans=3, ff_expand=2,
111
+ activation='gelu', dropout=0.1,
112
+ pe='sinu'):
113
+ super().__init__()
114
+ self.img_size = img_size
115
+ self.n_class = n_class
116
+ self.d_model = d_model
117
+ self.n_layers = n_layers
118
+ self.n_heads = n_heads
119
+ self.ff_expand = ff_expand
120
+ self.activation = activation
121
+ self.dropout = dropout
122
+ if pe == 'sinu':
123
+ self.pe = True
124
+ self.rope = False
125
+ elif pe == 'rope':
126
+ self.pe = False
127
+ self.rope = True
128
+ elif self.pe == 'nope':
129
+ self.pe = False
130
+ else:
131
+ raise ValueError('pe={} not recognized'.format(pe))
132
+
133
+ self.model = SequenceModel(
134
+ vocab_size=n_class,
135
+ d_model=d_model,
136
+ n_layers=n_layers,
137
+ pos_encoder=self.pe,
138
+ rope=self.rope,
139
+ embedder=PatchEmbedder,
140
+ embedder_args={"d_model": d_model, "img_size": img_size, "patch_size": patch_size, "in_chans": in_chans},
141
+ unembedder=ClsHead,
142
+ spatial_args={"causal": False, "n_heads": n_heads, "rope": self.rope},
143
+ channel_args={"expand": ff_expand, "activation": ACTIVS[activation], "depth": 2}
144
+ )
145
+
146
+ def forward(self, x):
147
+ return self.model(x)
148
+
149
+ def __repr__(self):
150
+ return self.model.__repr__()
151
+
152
+ def __str__(self):
153
+ return self.model.__str__()
154
+
155
+ @torch.no_grad()
156
+ def generate(self, *args, **kwargs):
157
+ raise NotImplementedError("Yet to Implement")
158
+
159
+ def summary(self):
160
+ self.model.summary()
161
+
162
+
163
+ class ViTTTransformer(nn.Module):
164
+ def __init__(self, n_class, img_size, patch_size, d_model, n_layers, n_heads, in_chans=3, ff_expand=2,
165
+ activation='gelu', dropout=0.1,
166
+ pe='sinu'):
167
+ super().__init__()
168
+ self.img_size = img_size
169
+ self.n_class = n_class
170
+ self.d_model = d_model
171
+ self.n_layers = n_layers
172
+ self.n_heads = n_heads
173
+ self.ff_expand = ff_expand
174
+ self.activation = activation
175
+ self.dropout = dropout
176
+ if pe == 'sinu':
177
+ self.pe = True
178
+ self.rope = False
179
+ elif pe == 'rope':
180
+ self.pe = False
181
+ self.rope = True
182
+ elif self.pe == 'nope':
183
+ self.pe = False
184
+ else:
185
+ raise ValueError('pe={} not recognized'.format(pe))
186
+
187
+ self.model = SequenceModel(
188
+ vocab_size=n_class,
189
+ d_model=d_model,
190
+ n_layers=n_layers,
191
+ pos_encoder=self.pe,
192
+ rope=self.rope,
193
+ embedder=PatchEmbedder,
194
+ embedder_args={"d_model": d_model, "img_size": img_size, "patch_size": patch_size, "in_chans": in_chans},
195
+ unembedder=MeanClassificationHead,
196
+ spatial_args={"causal": False, "n_heads": n_heads, "rope": self.rope},
197
+ channel_args={"expand": ff_expand, "activation": ACTIVS[activation], "depth": 2}
198
+ )
199
+
200
+ def forward(self, x):
201
+ return self.model(x)
202
+
203
+ def __repr__(self):
204
+ return self.model.__repr__()
205
+
206
+ def __str__(self):
207
+ return self.model.__str__()
208
+
209
+ @torch.no_grad()
210
+ def generate(self, *args, **kwargs):
211
+ raise NotImplementedError("Yet to Implement")
212
+
213
+ def summary(self):
214
+ self.model.summary()
215
+
216
+
217
+ class DiTTransformer(nn.Module):
218
+ def __init__(self, d_model, n_layers, n_heads, ff_expand=2, activation='gelu', dropout=0.1,
219
+ pe='sinu'):
220
+ super().__init__()
221
+ self.d_model = d_model
222
+ self.n_layers = n_layers
223
+ self.n_heads = n_heads
224
+ self.ff_expand = ff_expand
225
+ self.activation = activation
226
+ self.dropout = dropout
227
+ if pe == 'sinu':
228
+ self.pe = True
229
+ self.rope = False
230
+ elif pe == 'rope':
231
+ self.pe = False
232
+ self.rope = True
233
+ elif self.pe == 'nope':
234
+ self.pe = False
235
+ else:
236
+ raise ValueError('pe={} not recognized'.format(pe))
237
+
238
+ self.model = SequenceModel(
239
+ vocab_size=2,
240
+ d_model=d_model,
241
+ n_layers=n_layers,
242
+ pos_encoder=self.pe,
243
+ rope=self.rope,
244
+ embedder=Identity,
245
+ unembedder=Identity,
246
+ spatial_args={"causal": False, "n_heads": n_heads, "rope": self.rope},
247
+ channel_args={"expand": ff_expand, "activation": ACTIVS[activation], "depth": 2}
248
+ )
249
+
250
+ def forward(self, x):
251
+ return self.model(x)
252
+
253
+ def __repr__(self):
254
+ return self.model.__repr__()
255
+
256
+ def __str__(self):
257
+ return self.model.__str__()
258
+
259
+ @torch.no_grad()
260
+ def generate(self, *args, **kwargs):
261
+ raise NotImplementedError("Yet to Implement")
262
+
263
+ def summary(self):
264
+ self.model.summary()
vathos/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from Vathos.blocks import *
vathos/_analytics.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Vathos Analytics & Interpretability Toolkit
3
+ ===========================================
4
+ Modulo standalone per l'analisi spettrale, la topologia dell'attenzione e
5
+ l'interpretazione semantica dei concetti (FFN) nei modelli ModdedFormer.
6
+
7
+ Progettato per Jupyter Notebooks e script Python off-Streamlit.
8
+ """
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+ import numpy as np
13
+ import matplotlib.pyplot as plt
14
+ from typing import Optional, Dict, List, Tuple, Any
15
+ import math
16
+
17
+
18
+ # ──────────────────────────────────────────────────────────────────────────────
19
+ # 1. ANALISI MATEMATICA E SPETTRALE (Core)
20
+ # ──────────────────────────────────────────────────────────────────────────────
21
+
22
+ def compute_tensor_stats(t: torch.Tensor) -> dict:
23
+ """Calcola i momenti statistici e la sparsità di un tensore o manifold."""
24
+ f = t.detach().float().cpu()
25
+ v = f.numpy().flatten()
26
+ return {
27
+ "shape": tuple(t.shape),
28
+ "numel": t.numel(),
29
+ "mean": float(v.mean()),
30
+ "std": float(v.std()),
31
+ "min": float(v.min()),
32
+ "max": float(v.max()),
33
+ "l2_norm": float(np.linalg.norm(v)),
34
+ "sparsity": float((np.abs(v) < 1e-6).mean())
35
+ }
36
+
37
+
38
+ def compute_svd_spectrum(t: torch.Tensor) -> Optional[np.ndarray]:
39
+ """
40
+ Decomposizione in Valori Singolari (SVD) per studiare il collasso dimensionale.
41
+ Ritorna i valori singolari sigma. Se il tensore è 3D (es. Head), media gli spettri.
42
+ """
43
+ f = t.detach().float().cpu()
44
+ if f.ndim == 2:
45
+ try:
46
+ return torch.linalg.svdvals(f).numpy()
47
+ except Exception:
48
+ return None
49
+ if f.ndim == 3:
50
+ results = []
51
+ for i in range(f.shape[0]):
52
+ try:
53
+ results.append(torch.linalg.svdvals(f[i]).numpy())
54
+ except Exception:
55
+ pass
56
+ return np.stack(results) if results else None
57
+ return None
58
+
59
+
60
+ def compute_shannon_entropy(attn_weights: np.ndarray) -> List[float]:
61
+ """
62
+ Calcola l'Entropia di Shannon E[H] lungo l'asse delle Key per ogni Head.
63
+ H = - sum(p * log(p)). Valori bassi indicano forte 'certezza' (es. Induction Heads).
64
+ """
65
+ w_calc = attn_weights if attn_weights.ndim == 3 else attn_weights[np.newaxis]
66
+ entropies = []
67
+ for h in range(w_calc.shape[0]):
68
+ wh = w_calc[h] + 1e-12 # Epsilon per stabilità numerica nel log
69
+ ent = -(wh * np.log(wh)).sum(axis=-1).mean()
70
+ entropies.append(float(ent))
71
+ return entropies
72
+
73
+
74
+ # ──────────────────────────────────────────────────────────────────────────────
75
+ # 2. ESTRAZIONE TOPOLOGICA (Hooks & Forward Passes)
76
+ # ──────────────────────────────────────────────────────────────────────────────
77
+
78
+ def get_attention_matrix(mixer: nn.Module, x: torch.Tensor) -> Optional[np.ndarray]:
79
+ """Ricalcola rigorosamente Softmax(QK^T / sqrt(d)) per estrarre le matrici d'attenzione."""
80
+ if hasattr(mixer, "get_attention_weights"):
81
+ with torch.no_grad():
82
+ return mixer.get_attention_weights(x).detach().float().cpu().numpy()
83
+
84
+ try:
85
+ with torch.no_grad():
86
+ B, L, D = x.shape
87
+ if hasattr(mixer, "qkv"):
88
+ proj = mixer.qkv
89
+ n_heads = mixer.n_heads
90
+ head_dim = mixer.head_dim
91
+ qkv = proj(x).view(B, L, 3, n_heads, head_dim)
92
+ q, k, _ = qkv.unbind(dim=2)
93
+ q, k = q.transpose(1, 2).float(), k.transpose(1, 2).float()
94
+ scale = math.sqrt(head_dim)
95
+ scores = torch.matmul(q, k.transpose(-2, -1)) / scale
96
+ if getattr(mixer, "causal", True):
97
+ mask = torch.tril(torch.ones(L, L, device=x.device)).bool()
98
+ scores = scores.masked_fill(~mask, float("-inf"))
99
+ return torch.softmax(scores, dim=-1)[0].cpu().numpy()
100
+ elif hasattr(mixer, "qk"):
101
+ proj = mixer.qk
102
+ n_heads = mixer.n_heads
103
+ head_dim = mixer.head_dim
104
+ qk = proj(x).view(B, L, 2, n_heads, head_dim)
105
+ q, k = qk.unbind(dim=2)
106
+ q, k = q.transpose(1, 2).float(), k.transpose(1, 2).float()
107
+ scale = math.sqrt(head_dim)
108
+ scores = torch.matmul(q, k.transpose(-2, -1)) / scale
109
+ if getattr(mixer, "causal", True):
110
+ mask = torch.tril(torch.ones(L, L, device=x.device)).bool()
111
+ scores = scores.masked_fill(~mask, float("-inf"))
112
+ return torch.softmax(scores, dim=-1)[0].cpu().numpy()
113
+ except Exception as e:
114
+ print(f"Errore estrazione QK: {e}")
115
+ return None
116
+
117
+
118
+ def get_ffn_manifolds(cm: nn.Module, x: torch.Tensor, tau: float = None) -> Optional[Dict[str, np.ndarray]]:
119
+ """
120
+ Tratta il Channel Mixer come Memoria Associativa.
121
+ Ritorna Logits (XW), Attivazioni (Act(XW)) e Concept Attention.
122
+ """
123
+ if tau is None:
124
+ tau = math.sqrt(x.shape[-1]) # Default temperature = sqrt(d_model)
125
+
126
+ try:
127
+ with torch.no_grad():
128
+ pre_act = cm.expand(x) # Y = XW_{expand}
129
+ post_act = cm.act(pre_act) if hasattr(cm, "act") else pre_act
130
+ concept_attn = torch.softmax(pre_act / tau, dim=-1)
131
+
132
+ return {
133
+ "pre_act": pre_act[0].cpu().numpy(), # [L, M_dim]
134
+ "post_act": post_act[0].cpu().numpy(), # [L, M_dim]
135
+ "concept_attn": concept_attn[0].cpu().numpy() # [L, M_dim]
136
+ }
137
+ except Exception as e:
138
+ print(f"Errore estrazione FFN Manifold: {e}")
139
+ return None
140
+
141
+
142
+ def capture_layer_manifolds(model: nn.Module, input_ids: torch.Tensor, layer_idx: int) -> Tuple[
143
+ torch.Tensor, torch.Tensor]:
144
+ """
145
+ Usa i forward hooks per intercettare i manifold esatti in ingresso
146
+ ai mixer Spaziali e di Canale per un dato strato L.
147
+ """
148
+ block = model.blocks[layer_idx]
149
+ sm = block.spatial_mixer
150
+ cm = block.channel_mixer
151
+
152
+ captured = {"spatial_x": None, "channel_x": None}
153
+
154
+ def hook_spatial(m, i):
155
+ captured["spatial_x"] = i[0].detach()
156
+
157
+ def hook_channel(m, i):
158
+ captured["channel_x"] = i[0].detach()
159
+
160
+ h1 = sm.register_forward_pre_hook(hook_spatial)
161
+ h2 = cm.register_forward_pre_hook(hook_channel)
162
+
163
+ try:
164
+ model.eval()
165
+ with torch.no_grad():
166
+ model(input_ids)
167
+ finally:
168
+ h1.remove()
169
+ h2.remove()
170
+
171
+ return captured["spatial_x"], captured["channel_x"]
172
+
173
+
174
+ # ──────────────────────────────────────────────────────────────────────────────
175
+ # 3. FUNZIONI DI PLOTTING (Matplotlib Figure Generators)
176
+ # ──────────────────────────────────────────────────────────────────────────────
177
+
178
+ def set_dark_style():
179
+ """Opzionale: Applica il tema scuro (stile Vathos) globale a Matplotlib."""
180
+ plt.rcParams.update({
181
+ "figure.facecolor": "#0D0D1A", "axes.facecolor": "#0D0D1A", "axes.edgecolor": "#252550",
182
+ "axes.labelcolor": "#C0C0E0", "xtick.color": "#9CA3AF", "ytick.color": "#9CA3AF",
183
+ "text.color": "#D8D8F0", "grid.color": "#1E1E3A", "grid.alpha": 0.6
184
+ })
185
+
186
+
187
+ def plot_weight_distributions(tensors_dict: Dict[str, torch.Tensor], bins: int = 80) -> Tuple[plt.Figure, np.ndarray]:
188
+ n = len(tensors_dict)
189
+ cols = min(n, 3)
190
+ rows = math.ceil(n / cols)
191
+ fig, axes = plt.subplots(rows, cols, figsize=(5 * cols, 3.5 * rows))
192
+ if n == 1:
193
+ axes = np.array([[axes]])
194
+ elif rows == 1:
195
+ axes = axes.reshape(1, -1)
196
+
197
+ colors = ["#A78BFA", "#60A5FA", "#34D399", "#F472B6", "#FB923C", "#FBBF24"]
198
+
199
+ for idx, (name, t) in enumerate(tensors_dict.items()):
200
+ ax = axes[idx // cols][idx % cols]
201
+ v = t.detach().float().cpu().numpy().flatten()
202
+ stats = compute_tensor_stats(t)
203
+ ax.hist(v, bins=bins, color=colors[idx % len(colors)], alpha=0.8, linewidth=0)
204
+ ax.set_title(f"{name}\n$\mu$={stats['mean']:.3e} $\sigma$={stats['std']:.3e}", fontsize=9)
205
+ ax.grid(True, ls="--", alpha=0.4)
206
+
207
+ for idx in range(n, rows * cols): axes[idx // cols][idx % cols].set_visible(False)
208
+ fig.suptitle("Distribuzioni di Densità", fontsize=12)
209
+ fig.tight_layout()
210
+ return fig, axes
211
+
212
+
213
+ def plot_svd_spectra(tensors_dict: Dict[str, torch.Tensor], log_scale: bool = True) -> Tuple[plt.Figure, np.ndarray]:
214
+ eligible = {k: v for k, v in tensors_dict.items() if v.ndim >= 2}
215
+ n = len(eligible)
216
+ if n == 0: raise ValueError("Nessun tensore 2D+ fornito per la SVD.")
217
+
218
+ cols = min(n, 3)
219
+ rows = math.ceil(n / cols)
220
+ fig, axes = plt.subplots(rows, cols, figsize=(5 * cols, 3.5 * rows))
221
+ if n == 1:
222
+ axes = np.array([[axes]])
223
+ elif rows == 1:
224
+ axes = axes.reshape(1, -1)
225
+
226
+ colors = ["#A78BFA", "#60A5FA", "#34D399", "#F472B6", "#FB923C", "#FBBF24"]
227
+
228
+ for idx, (name, t) in enumerate(eligible.items()):
229
+ ax = axes[idx // cols][idx % cols]
230
+ sv = compute_svd_spectrum(t)
231
+ if sv is None: continue
232
+
233
+ color = colors[idx % len(colors)]
234
+ if sv.ndim == 2:
235
+ for h in range(sv.shape[0]):
236
+ ax.plot(sv[h], color=color, alpha=0.2, linewidth=0.8)
237
+ ax.plot(sv.mean(0), color=color, linewidth=2, label="Mean $\Sigma$")
238
+ ax.legend()
239
+ else:
240
+ ax.plot(sv, color=color, linewidth=1.5, marker=".", markersize=3)
241
+
242
+ cond = sv.flatten()[0] / (sv.flatten()[-1] + 1e-12)
243
+ r_eff = int((sv.flatten() > sv.flatten()[0] * 1e-3).sum())
244
+ ax.set_title(f"{name}\n$\kappa={cond:.1f}$ | Rango Effettivo $\\approx {r_eff}$", fontsize=9)
245
+ if log_scale: ax.set_yscale("log")
246
+ ax.grid(True, ls="--", which="both", alpha=0.4)
247
+
248
+ for idx in range(n, rows * cols): axes[idx // cols][idx % cols].set_visible(False)
249
+ fig.tight_layout()
250
+ return fig, axes
251
+
252
+
253
+ def plot_attention_topology(attn_weights: np.ndarray, tokens: Optional[List[str]] = None) -> Tuple[
254
+ plt.Figure, np.ndarray]:
255
+ """Plotta la matrice $L \times L$ per le varie Head d'attenzione."""
256
+ if attn_weights.ndim == 2: attn_weights = attn_weights[np.newaxis]
257
+ H, L_q, L_k = attn_weights.shape
258
+ cols = min(H, 4)
259
+ rows = math.ceil(H / cols)
260
+
261
+ cell_size = max(0.2, min(0.6, 12.0 / L_q)) if tokens else 0.5
262
+ fig_w = max(4 * cols, cell_size * L_q * cols)
263
+ fig_h = max(3.5 * rows, cell_size * L_q * rows + 1)
264
+
265
+ fig, axes = plt.subplots(rows, cols, figsize=(fig_w, fig_h))
266
+ axes = np.array(axes).flatten() if H > 1 else [axes]
267
+
268
+ clean_tokens = [t.replace('Ġ', '').replace(' ', '') for t in tokens] if tokens else None
269
+
270
+ for h in range(H):
271
+ ax = axes[h]
272
+ w = attn_weights[h]
273
+ im = ax.imshow(w, aspect="auto", cmap="magma", vmin=0, vmax=w.max() + 1e-9)
274
+ ax.set_title(f"Head {h}", fontsize=10)
275
+
276
+ if clean_tokens and len(clean_tokens) == L_q:
277
+ ax.set_xticks(range(L_k))
278
+ ax.set_yticks(range(L_q))
279
+ fs = max(5, min(10, int(300 / L_q)))
280
+ ax.set_xticklabels(clean_tokens, rotation=90, fontsize=fs)
281
+ ax.set_yticklabels(clean_tokens, fontsize=fs)
282
+ else:
283
+ ax.set_xlabel("Key Position (Source)")
284
+ ax.set_ylabel("Query Position (Target)")
285
+
286
+ fig.colorbar(im, ax=ax, shrink=0.8)
287
+
288
+ for h in range(H, len(axes)): axes[h].set_visible(False)
289
+ fig.tight_layout()
290
+ return fig, axes
291
+
292
+
293
+ def plot_ffn_concept_manifold(matrix: np.ndarray, title: str, tokens: Optional[List[str]] = None,
294
+ max_neurons: int = 128) -> Tuple[plt.Figure, plt.Axes]:
295
+ """Plotta la mappa $L \times M$ di attivazione dei concetti per il Channel Mixer."""
296
+ mat_to_plot = matrix[:, :max_neurons]
297
+ rows_c, cols_c = mat_to_plot.shape
298
+
299
+ fig_w = max(8, cols_c * 0.15) if tokens else 10
300
+ fig_h = max(6, rows_c * 0.3) if tokens else 6
301
+
302
+ fig, ax = plt.subplots(figsize=(min(fig_w, 20), min(fig_h, 15)))
303
+ im = ax.imshow(mat_to_plot, aspect="auto", cmap="magma")
304
+
305
+ ax.set_title(f"{title} (Primi {cols_c} neuroni su {matrix.shape[1]})", pad=15)
306
+ ax.set_ylabel("Tokens (Queries)")
307
+ ax.set_xlabel("FFN Neurons (Latent Concepts)")
308
+
309
+ if tokens and len(tokens) == rows_c:
310
+ clean_tokens = [t.replace('Ġ', '').replace(' ', '') for t in tokens]
311
+ fs = max(5, min(10, int(400 / rows_c)))
312
+ ax.set_yticks(range(rows_c))
313
+ ax.set_yticklabels(clean_tokens, fontsize=fs)
314
+
315
+ fig.colorbar(im, ax=ax)
316
+ fig.tight_layout()
317
+ return fig, ax
318
+
319
+
320
+ def plot_architecture_scalars(model: nn.Module) -> Tuple[Optional[plt.Figure], Optional[plt.Figure]]:
321
+ """Genera i plot per gli scalari strutturali: ZeroSkip e Skip-connection Lambdas."""
322
+ fig_skip, fig_zero = None, None
323
+
324
+ # 1. Skip Lambdas
325
+ if hasattr(model, "skip_lambdas") and model.skip_lambdas:
326
+ lambdas = {k: float(v.detach()) for k, v in model.skip_lambdas.items()}
327
+ fig_skip, ax = plt.subplots(figsize=(max(6, len(lambdas) * 1.4), 4))
328
+ keys, vals = list(lambdas.keys()), list(lambdas.values())
329
+ ax.bar(range(len(keys)), vals, alpha=0.85, color="#60A5FA")
330
+ ax.set_xticks(range(len(keys)))
331
+ ax.set_xticklabels([k.replace("route_", "").replace("_to_", "→") for k in keys], rotation=30, ha="right")
332
+ ax.axhline(0, color="gray", linewidth=0.8, ls="--")
333
+ ax.set_title("Skip-connection Routing Gates ($\lambda$)")
334
+ fig_skip.tight_layout()
335
+
336
+ # 2. ZeroSkip Parameters
337
+ if hasattr(model, "zeroskip_params") and model.zeroskip:
338
+ vals = [float(p.detach()) for p in model.zeroskip_params]
339
+ fig_zero, ax = plt.subplots(figsize=(max(6, len(vals) * 0.7), 4))
340
+ ax.plot(vals, marker="o", color="#FBBF24", linewidth=1.5)
341
+ ax.axhline(0, color="gray", linewidth=0.8, ls="--")
342
+ ax.set_xlabel("Layer Index")
343
+ ax.set_ylabel("ZeroSkip $\\alpha$")
344
+ ax.set_title("ZeroSkip Coefficients per Layer")
345
+ ax.grid(True, ls="--", alpha=0.4)
346
+ fig_zero.tight_layout()
347
+
348
+ return fig_skip, fig_zero
349
+
350
+
351
+ # ──────────────────────────────────────────────────────────────────────────────
352
+ # 4. HIGH-LEVEL API PER JUPYTER (Sintesi per Speedrun & ARC)
353
+ # ──────────────────────────────────────────────────────────────────────────────
354
+
355
+ def analyze_layer_semantics(model: nn.Module, tokenizer: Any, prompt: str, layer_idx: int,
356
+ add_bos: bool = True, ffn_tau: float = None,
357
+ show_plots: bool = True) -> Dict[str, Any]:
358
+ """
359
+ Funzione master per i Notebook. Esegue l'analisi completa del layer indicato
360
+ su un prompt specifico, restituendo statistiche e plottando i grafici.
361
+
362
+ Uso:
363
+ results = analyze_layer_semantics(model, tokenizer, "Test sequence", layer_idx=4)
364
+ print("Sparsità FFN:", results["ffn_sparsity"])
365
+ """
366
+ device = next(model.parameters()).device
367
+ full_prompt = (tokenizer.bos_token if add_bos and tokenizer.bos_token else "") + prompt
368
+ inputs = tokenizer(full_prompt, return_tensors="pt")
369
+ input_ids = inputs["input_ids"].to(device)
370
+ tokens = tokenizer.convert_ids_to_tokens(input_ids[0])
371
+
372
+ # 1. Forward Pass con intercettazione Manifold
373
+ sm_x, cm_x = capture_layer_manifolds(model, input_ids, layer_idx)
374
+ if sm_x is None:
375
+ raise RuntimeError(f"Hook fallito. Il layer {layer_idx} non è stato eseguito.")
376
+
377
+ block = model.blocks[layer_idx]
378
+ sm = block.spatial_mixer
379
+ cm = block.channel_mixer
380
+
381
+ d_model = model.d_models[layer_idx] if hasattr(model, "d_models") else sm_x.shape[-1]
382
+ if ffn_tau is None: ffn_tau = math.sqrt(d_model)
383
+
384
+ # 2. Estrazione Matrici
385
+ attn_w = get_attention_matrix(sm, sm_x)
386
+ ffn_manifolds = get_ffn_manifolds(cm, cm_x, tau=ffn_tau)
387
+
388
+ results = {
389
+ "tokens": tokens,
390
+ "X_spatial": sm_x,
391
+ "X_channel": cm_x,
392
+ "attention_matrix": attn_w,
393
+ "ffn_manifolds": ffn_manifolds,
394
+ }
395
+
396
+ # 3. Calcoli Spettrali e Statistiche (Information Theory)
397
+ if attn_w is not None:
398
+ results["attention_entropy"] = compute_shannon_entropy(attn_w)
399
+
400
+ if ffn_manifolds is not None:
401
+ Y_act = ffn_manifolds["post_act"]
402
+ stats = compute_tensor_stats(torch.tensor(Y_act))
403
+ results["ffn_sparsity"] = stats["sparsity"]
404
+ results["ffn_l2_norm"] = stats["l2_norm"]
405
+
406
+ try:
407
+ svd_vals = np.linalg.svd(Y_act, compute_uv=False)
408
+ results["ffn_eff_rank"] = int(np.sum(svd_vals > svd_vals[0] * 1e-3))
409
+ results["ffn_svd_spectrum"] = svd_vals
410
+ except np.linalg.LinAlgError:
411
+ pass
412
+
413
+ # 4. Rendering Opzionale
414
+ if show_plots:
415
+ print(f"--- ANALISI DEL MANIFOLD | LAYER {layer_idx} ---")
416
+
417
+ if attn_w is not None:
418
+ print("\n>> Topologia Spaziale dell'Attenzione (QK)")
419
+ fig_attn, _ = plot_attention_topology(attn_w, tokens)
420
+ plt.show()
421
+ print(f" E[H] (Entropia per Head): {[f'{e:.3f}' for e in results['attention_entropy']]}")
422
+
423
+ if ffn_manifolds is not None:
424
+ print("\n>> Channel Expansion (Memory Concepts)")
425
+ print(f" Sparsità (Soglia < 1e-6): {results.get('ffn_sparsity', 0):.2%}")
426
+ print(f" Rango Effettivo (SVD): {results.get('ffn_eff_rank', 'N/A')} su {Y_act.shape[1]} M_dim")
427
+
428
+ fig_ffn, _ = plot_ffn_concept_manifold(ffn_manifolds["post_act"], "FFN $Act(X W_{expand})$", tokens)
429
+ plt.show()
430
+
431
+ if "ffn_svd_spectrum" in results:
432
+ fig_svd, ax_svd = plt.subplots(figsize=(6, 3))
433
+ ax_svd.plot(results["ffn_svd_spectrum"], color="red", marker=".")
434
+ ax_svd.set_yscale("log")
435
+ ax_svd.set_title("Spettro $\Sigma$ del Manifold dei Concetti (FFN)")
436
+ ax_svd.grid(True, ls="--", alpha=0.5)
437
+ plt.show()
438
+
439
+ return results
vathos/_basics.py ADDED
@@ -0,0 +1,1134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ This modules serves as base for the Torch Basic model Structure, and profilers.
3
+ Class Layer is the base of all Vathos layers, and ereditate from torch.nn.Module class, adding only the structures that help
4
+ profiling, visualization, and debugging.
5
+ Everything built with Layer is totally compatible with torch native modules.
6
+ """
7
+
8
+ import numpy as np
9
+ import torch.nn as nn
10
+ from typing import Callable
11
+ from Vathos.functions import *
12
+ from timeit import default_timer as timer
13
+ from collections import OrderedDict, defaultdict
14
+ import re
15
+ import math
16
+ from torch import Tensor
17
+
18
+ ACTIVS = {
19
+ 'tanh': nn.Tanh,
20
+ 'sigmoid': nn.Sigmoid,
21
+ 'relu': nn.ReLU,
22
+ 'gelu': nn.GELU,
23
+ 'elu': nn.ELU,
24
+ 'lrelu': nn.LeakyReLU,
25
+ 'leaky_relu': nn.LeakyReLU,
26
+ }
27
+
28
+
29
+ class VathosConfig:
30
+ """Global configuration state."""
31
+ _COMPILABLE = False # True = production, torch.compile safe
32
+ _PROFILE_BATCHED = True # True = times divided by batch size
33
+ _GLOBAL_PROFILE = True # True = always profile in debug mode
34
+
35
+
36
+ def set_vathos_mode(mode: str):
37
+ """
38
+ Switch Vathos mode globally.
39
+
40
+ 'debug' — profiling enabled, torch.compile unfriendly
41
+ 'production' — profiling disabled, torch.compile safe
42
+ """
43
+ if mode.lower() == "production":
44
+ VathosConfig._COMPILABLE = True
45
+ print(f"{SEC}Vathos:{RES} Switched to {GOOD}PRODUCTION{RES} mode.")
46
+ elif mode.lower() == "debug":
47
+ VathosConfig._COMPILABLE = False
48
+ print(f"{SEC}Vathos:{RES} Switched to {NUM}DEBUG{RES} mode.")
49
+ else:
50
+ raise ValueError("Mode must be 'production' or 'debug'")
51
+
52
+
53
+ # =============================================================================
54
+ # BASE LAYER
55
+ # =============================================================================
56
+
57
+ class Layer(nn.Module):
58
+ """
59
+ ─────────────────────────────────────────────────────────────
60
+ class MyBlock(Layer):
61
+ def __init__(self, ...):
62
+ super().__init__()
63
+ # define sub-modules here
64
+ self.lin = nn.Linear(...)
65
+ self.attn = MultiheadAttentionMixer(...)
66
+
67
+ def forward(self, x):
68
+ z return self.lin(x)
69
+ ─────────────────────────────────────────────────────────────
70
+
71
+ DO NOT override __call__. Profiling is handled via native forward hooks,
72
+ which are registered automatically in debug mode and completely absent
73
+ in production mode — zero overhead, full torch.compile compatibility.
74
+
75
+ IMPORTANT: gli hook sono registrati al __init__. Se il modello è costruito
76
+ PRIMA di chiamare set_vathos_mode('production'), gli hook sopravvivono.
77
+ Soluzioni:
78
+ 1) Chiama set_vathos_mode('production') PRIMA di istanziare moduli.
79
+ 2) Oppure chiama model.strip_profiling_hooks() dopo lo switch.
80
+ """
81
+
82
+ _is_vathos_layer = True
83
+
84
+ def __init__(self):
85
+ super().__init__()
86
+ self.complexity = "O(1)"
87
+ self.__name__ = self.__class__.__name__
88
+
89
+ self._timer_unbatched = not VathosConfig._PROFILE_BATCHED
90
+ self._tstart = 0.0
91
+ self._tend = 0.0
92
+ self._time = 0.0
93
+ self._times = []
94
+
95
+ self._sublayers = None
96
+
97
+ # Hooks are registered ONCE at construction in debug mode.
98
+ # In production mode, no hooks exist — no overhead whatsoever.
99
+ if not VathosConfig._COMPILABLE and VathosConfig._GLOBAL_PROFILE:
100
+ self._register_profiling_hooks()
101
+
102
+ # -------------------------------------------------------------------------
103
+ # Profiling hooks — native nn.Module mechanism, __call__ is never touched
104
+ # -------------------------------------------------------------------------
105
+
106
+ def _register_profiling_hooks(self):
107
+ self.register_forward_pre_hook(self._pre_hook)
108
+ self.register_forward_hook(self._post_hook)
109
+
110
+ def _pre_hook(self, module, args):
111
+ self._tstart = timer()
112
+
113
+ def _post_hook(self, module, args, output):
114
+ self._tend = timer()
115
+ bs = 1
116
+ if args and isinstance(args[0], torch.Tensor):
117
+ bs = args[0].shape[0]
118
+ div = bs if not self._timer_unbatched else 1
119
+ self._time = (self._tend - self._tstart) / div
120
+ self._times.append(self._time)
121
+
122
+ # -------------------------------------------------------------------------
123
+ # Sublayer registry — used by profile()
124
+ # -------------------------------------------------------------------------
125
+
126
+ def register_sublayers(self):
127
+ if self._sublayers is None:
128
+ self._sublayers = dict()
129
+
130
+ def get_unique_name(base_name, existing_names):
131
+ if base_name not in existing_names:
132
+ return base_name
133
+ counter = 1
134
+ while f"{base_name}_{counter}" in existing_names:
135
+ counter += 1
136
+ return f"{base_name}_{counter}"
137
+
138
+ def collect_layers(module, level=0):
139
+ layers = []
140
+ for name, child in module.named_children():
141
+ if getattr(child, "_is_vathos_layer", False):
142
+ layers.append((name, child, level))
143
+ layers.extend(collect_layers(child, level=level + 1))
144
+ elif isinstance(child, nn.ModuleList):
145
+ for i, item in enumerate(child):
146
+ if getattr(item, "_is_vathos_layer", False):
147
+ layers.append((f"{name}[{i}]", item, level))
148
+ layers.extend(collect_layers(item, level=level + 1))
149
+ elif isinstance(child, nn.Module):
150
+ layers.extend(collect_layers(child, level=level))
151
+ return layers
152
+
153
+ for original_name, layer, level in collect_layers(self):
154
+ class_name = getattr(layer, "__name__", type(layer).__name__)
155
+ unique_name = get_unique_name(class_name, self._sublayers.keys())
156
+ self._sublayers[unique_name] = {"layer": layer, "level": level}
157
+
158
+ # -------------------------------------------------------------------------
159
+ # Utilities
160
+ # -------------------------------------------------------------------------
161
+
162
+ def get_mean_execution_time(self) -> float:
163
+ return float(np.mean(self._times)) if self._times else 0.0
164
+
165
+ def strip_profiling_hooks(self):
166
+ """Rimuove gli hook profile da questo modulo e da tutti i sotto-Layer.
167
+
168
+ Utile se il modello è stato istanziato in debug mode e poi si vuole
169
+ passare a production senza ricostruirlo. Idempotente.
170
+ """
171
+ for module in self.modules():
172
+ if isinstance(module, Layer):
173
+ module._forward_pre_hooks.clear()
174
+ module._forward_hooks.clear()
175
+ return self
176
+
177
+ def has_custom_generate(self) -> bool:
178
+ return type(self).generate is not Layer.generate
179
+
180
+ def generate(self, *args, **kwargs):
181
+ """Override in subclasses that support autoregressive generation."""
182
+ return None
183
+
184
+ def clear_times(self):
185
+ """Reset profiling history."""
186
+ self._times.clear()
187
+
188
+ def profile(self, maxlevel=100, avg=False, plot=False, plot_level=1):
189
+ if VathosConfig._COMPILABLE:
190
+ print(f"{BAD}Cannot profile in PRODUCTION mode.{RES} Run set_vathos_mode('debug') first.")
191
+ return
192
+
193
+ batched = not self._timer_unbatched
194
+ print(
195
+ f"Layer {NUM}{self.__name__}{RES} Times Profile (batched: {GOOD if batched else BAD}{batched}{RES}) (averaged: {GOOD if avg else BAD}{avg}{RES}):")
196
+
197
+ grouped_layers = OrderedDict()
198
+ order_map = {}
199
+ order_counter = 0
200
+
201
+ if self._sublayers:
202
+ for sublayer_name, sublayer_info in self._sublayers.items():
203
+ layer = sublayer_info['layer']
204
+ level = sublayer_info['level']
205
+ if level >= maxlevel: continue
206
+
207
+ match = re.match(r'^(.+?)_(\d+)$', sublayer_name)
208
+ base_name = match.group(1) if match else sublayer_name
209
+ key = (base_name, level)
210
+
211
+ if key not in order_map:
212
+ order_map[key] = order_counter
213
+ order_counter += 1
214
+ grouped_layers[key] = []
215
+ grouped_layers[key].append((sublayer_name, layer))
216
+
217
+ if not avg:
218
+ if self._sublayers:
219
+ for sublayer_name, sublayer_info in self._sublayers.items():
220
+ if sublayer_info['level'] < maxlevel:
221
+ indent = " " * sublayer_info['level']
222
+ t = sublayer_info['layer'].get_mean_execution_time()
223
+ print(f"{indent}- {NUM}{sublayer_name}{RES}: {t * 1000:.2f}ms")
224
+ else:
225
+ for (base_name, level), layers_list in grouped_layers.items():
226
+ indent = " " * level
227
+ times = [l.get_mean_execution_time() for _, l in layers_list if len(l._times) > 0]
228
+ if times:
229
+ avg_time = np.mean(times)
230
+ print(
231
+ f"{indent}- {NUM}{base_name}_avg{RES}: {avg_time * 1000:.2f}ms {SEC}(x{len(layers_list)}){RES}")
232
+ else:
233
+ print(f"{indent}- {NUM}{base_name}_avg{RES}: no time recorded")
234
+
235
+ if plot:
236
+ try:
237
+ import matplotlib.pyplot as plt
238
+ level_layers = defaultdict(list)
239
+
240
+ for (base_name, level), layers_list in grouped_layers.items():
241
+ if level == plot_level:
242
+ for _, layer in layers_list:
243
+ if len(layer._times) > 0:
244
+ level_layers[base_name].append(np.mean(layer._times))
245
+
246
+ elif level < plot_level:
247
+
248
+ pass
249
+
250
+ if level_layers:
251
+ layer_times = {k: sum(v) for k, v in level_layers.items()}
252
+ sorted_layers = sorted(layer_times.items(), key=lambda x: x[1], reverse=True)
253
+ labels = [x[0] for x in sorted_layers]
254
+ times = [x[1] * 1000 for x in sorted_layers]
255
+
256
+ fig, ax = plt.subplots(figsize=(12, 8))
257
+ wedges, texts, autotexts = ax.pie(times, autopct='%1.1f%%', startangle=90)
258
+ ax.legend(wedges, [f'{l}: {t:.2f}ms' for l, t in zip(labels, times)],
259
+ loc="center left", bbox_to_anchor=(1, 0, 0.5, 1))
260
+ ax.set_title(f'{self.__name__} - Level {plot_level}')
261
+ plt.tight_layout()
262
+ plt.show()
263
+ else:
264
+ print(f"{SEC}No data for plot level {plot_level}{RES}")
265
+ except ImportError:
266
+ print(f"{BAD}Matplotlib missing{RES}")
267
+
268
+ def __repr__(self):
269
+ return f"{SEC}Vathos{RES}: " + super().__repr__()
270
+
271
+
272
+ class Builder:
273
+ def __init__(self, layer, **params):
274
+ self.layer = layer
275
+ self.params = params
276
+ for att in layer.__dict__:
277
+ setattr(self, att, getattr(layer, att))
278
+
279
+ def __call__(self, *args):
280
+ return self.layer(*args, **self.params)
281
+
282
+
283
+ class tWrapper(Layer):
284
+ __name__ = "tWrapper"
285
+
286
+ def __init__(self, module: nn.Module):
287
+ super(tWrapper, self).__init__()
288
+ self.module = module
289
+
290
+ def forward(self, *args, **kwargs):
291
+ self.module(*args, **kwargs)
292
+
293
+
294
+ class Identity(Layer):
295
+ __name__ = "Identity"
296
+ __complexity__ = "O(1)"
297
+
298
+ def __init__(self, *args, **kwargs):
299
+ super(Identity, self).__init__()
300
+
301
+ def forward(self, x):
302
+ return x
303
+
304
+
305
+ class Skip(Layer):
306
+ __name__ = "Skip"
307
+ __complexity__ = "O(1)"
308
+
309
+ def __init__(self, layer):
310
+ super(Skip, self).__init__()
311
+ self.layer = layer
312
+
313
+ def forward(self, x):
314
+ return self.layer(x) + x
315
+
316
+
317
+ class LearntSkip(Layer):
318
+ __name__ = "Skip"
319
+ __complexity__ = "O(1)"
320
+
321
+ def __init__(self, layer):
322
+ super().__init__()
323
+ self.layer = layer
324
+ self.w = nn.Parameter(torch.tensor([1.0, 1.0]), requires_grad=True)
325
+
326
+ def forward(self, x):
327
+ return self.layer(x) * self.w[0] + x * self.w[1]
328
+
329
+
330
+ class IdentityMixer(Layer):
331
+ __name__ = "Identity"
332
+ __complexity__ = "O(1)"
333
+
334
+ def __init__(self, d_model):
335
+ super(IdentityMixer, self).__init__()
336
+ self.d_model = d_model
337
+
338
+ def forward(self, x):
339
+ return x
340
+
341
+
342
+ class LPadder(Layer):
343
+ __name__ = "LPadder"
344
+ __complexity__ = "O(k d)"
345
+
346
+ def __init__(self, right=0, left=0, element=0):
347
+ super(LPadder, self).__init__()
348
+ self.right = right
349
+ self.left = left
350
+ self.element = element
351
+
352
+ def forward(self, x):
353
+ return F.pad(x, (0, 0, self.left, self.right), mode="constant", value=self.element)
354
+
355
+
356
+ class dPadder(Layer):
357
+ __name__ = "LPadder"
358
+ __complexity__ = "O(k L)"
359
+
360
+ def __init__(self, up, down=0, element=0):
361
+ super(dPadder, self).__init__()
362
+ self.right = up
363
+ self.left = down
364
+ self.element = element
365
+
366
+ def forward(self, x):
367
+ return F.pad(x, (self.left, self.right), mode="constant", value=self.element)
368
+
369
+
370
+ class LUnPadder(Layer):
371
+ __name__ = "LUnPadder"
372
+ __complexity__ = "O(k d)"
373
+
374
+ def __init__(self, right=0, left=0):
375
+ super().__init__()
376
+ self.right = right
377
+ self.left = left
378
+
379
+ def forward(self, x):
380
+ return x[:, self.left:-self.right, :]
381
+
382
+
383
+ class dUnPadder(Layer):
384
+ __name__ = "LUnPadder"
385
+ __complexity__ = "O(k d)"
386
+
387
+ def __init__(self, right=0, left=0):
388
+ super().__init__()
389
+ self.right = right
390
+ self.left = left
391
+
392
+ def forward(self, x):
393
+ return x[:, :, self.left:-self.right]
394
+
395
+
396
+ class Linear(Layer):
397
+ __name__ = 'Linear'
398
+
399
+ def __init__(self, input_dim, output_dim, bias=True, **kwargs):
400
+ super().__init__()
401
+ self.input_dim = input_dim
402
+ self.output_dim = output_dim
403
+ self.bias = bias
404
+ self.linear = nn.Linear(input_dim, output_dim, bias=bias)
405
+
406
+ def forward(self, x):
407
+ return self.linear(x)
408
+
409
+
410
+ class BottleneckRepeatedLinear(nn.Module):
411
+ def __init__(self, in_features, out_features, num_repeats, bias=False, scale_variance=False):
412
+ super().__init__()
413
+
414
+ assert out_features % num_repeats == 0, "out_features must be strictly divisible by num_repeats"
415
+
416
+ self.in_features = in_features
417
+ self.out_features = out_features
418
+ self.num_repeats = num_repeats
419
+ self.bottleneck_dim = out_features // num_repeats
420
+ self.scale_variance = scale_variance
421
+
422
+ self.weight = nn.Parameter(torch.Tensor(self.bottleneck_dim, in_features))
423
+
424
+ if bias:
425
+ self.bias = nn.Parameter(torch.Tensor(self.bottleneck_dim))
426
+ else:
427
+ self.register_parameter('bias', None)
428
+
429
+ self.init_parameters()
430
+
431
+ def init_parameters(self):
432
+ nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
433
+ if self.bias is not None:
434
+ fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight)
435
+ bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0
436
+ nn.init.uniform_(self.bias, -bound, bound)
437
+
438
+ def forward(self, x):
439
+ y = F.linear(x, self.weight, self.bias)
440
+
441
+ repeat_shape = [1] * (y.dim() - 1) + [self.num_repeats]
442
+
443
+ out = y.repeat(*repeat_shape)
444
+
445
+ if self.scale_variance:
446
+ out = out / math.sqrt(self.num_repeats)
447
+
448
+ return out
449
+
450
+
451
+ class ProductLinear(Layer):
452
+ __name__ = 'Linear'
453
+
454
+ def __init__(self, input_dim, output_dim):
455
+ super().__init__()
456
+ self.input_dim = input_dim
457
+ self.output_dim = output_dim
458
+ self.weight1 = nn.Parameter(0.1 * torch.randn(output_dim, input_dim))
459
+ self.weight2 = nn.Parameter(torch.ones(output_dim, input_dim + 0.01 * torch.randn(output_dim, input_dim)))
460
+
461
+ def forward(self, x):
462
+ return F.linear(x, self.weight1 * self.weight2)
463
+
464
+
465
+ class DoubleLinear(nn.Module):
466
+ def __init__(self, in_features: int, out_features: int,
467
+ device=None, dtype=None):
468
+ factory_kwargs = {'device': device, 'dtype': dtype}
469
+ super().__init__()
470
+
471
+ self.in_features = in_features
472
+ self.out_features = out_features
473
+
474
+ self.weight1 = nn.Parameter(torch.empty((out_features, in_features), **factory_kwargs))
475
+ nn.init.xavier_uniform_(self.weight1)
476
+
477
+ self.weight2 = nn.Parameter(torch.empty((in_features, in_features), **factory_kwargs))
478
+ nn.init.xavier_uniform_(self.weight2)
479
+
480
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
481
+ W_eff = self.weight1 @ self.weight2
482
+ return F.linear(x, W_eff)
483
+
484
+
485
+ class LowRankLinear(Layer):
486
+ __name__ = 'Linear'
487
+
488
+ def __init__(self, input_dim, output_dim, rank=16, bias=False):
489
+ super().__init__()
490
+ self.input_dim = input_dim
491
+ self.output_dim = output_dim
492
+ self.bias = bias
493
+ self.l1 = nn.Linear(input_dim, rank, bias=bias)
494
+ self.l2 = nn.Linear(rank, output_dim, bias=bias)
495
+
496
+ def forward(self, x):
497
+ return self.l2(self.l1(x))
498
+
499
+
500
+ class UnbiasedLinear(Layer):
501
+ __name__ = "UnbiasedLinear"
502
+ __complexity__ = "O(L d^2)"
503
+
504
+ def __init__(self, input_features, output_features):
505
+ super(UnbiasedLinear, self).__init__()
506
+ self.linear = nn.Linear(input_features, output_features, bias=False)
507
+
508
+ def forward(self, x):
509
+ return self.linear(x)
510
+
511
+
512
+ class SwiGLU(Layer):
513
+ gated = True
514
+ __name__ = "SwiGLU"
515
+ __complexity__ = "O(L)"
516
+
517
+ def forward(self, x: torch.Tensor):
518
+ x, gate = x.chunk(2, dim=-1)
519
+ return x * F.silu(gate)
520
+
521
+
522
+ class ReLU2(Layer):
523
+ gated = False
524
+ __name__ = "ReLU^2"
525
+ __complexity__ = "O(L)"
526
+
527
+ def __init__(self):
528
+ super().__init__()
529
+
530
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
531
+ x_pos = F.relu(x)
532
+ return x_pos * x_pos
533
+
534
+
535
+ class LeakyReLU2(Layer):
536
+ gated = False
537
+ __name__ = "LeakyReLU^2"
538
+ __complexity__ = "O(L)"
539
+
540
+ def __init__(self):
541
+ super().__init__()
542
+
543
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
544
+ x_pos = F.leaky_relu(x, 0.5)
545
+ return x_pos * x_pos
546
+
547
+
548
+ class PSiLU2(Layer):
549
+ gated = False
550
+ __name__ = "SiLU^2"
551
+ __complexity__ = "O(L)"
552
+
553
+ def __init__(self):
554
+ super().__init__()
555
+ self.param = nn.Parameter(torch.tensor([5.0, 2.0]), requires_grad=True)
556
+
557
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
558
+ x_pos = self.param[0] * F.silu(x / self.param[1])
559
+ return x_pos * x_pos
560
+
561
+
562
+ class LeLU2(Layer):
563
+ gated = False
564
+ __name__ = "ReLU^2"
565
+ __complexity__ = "O(L)"
566
+
567
+ def __init__(self):
568
+ super().__init__()
569
+ self.a = nn.Parameter(torch.randn(1) - 0.05)
570
+
571
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
572
+ x_pos = F.relu(x - self.a)
573
+ return x_pos * x_pos
574
+
575
+
576
+ class PReLU2(Layer):
577
+ def __init__(self, num_parameters=1):
578
+ super().__init__()
579
+ self.alpha = nn.Parameter(torch.ones(num_parameters))
580
+
581
+ def forward(self, x: torch.Tensor):
582
+ return self.alpha * torch.square(F.relu(x))
583
+
584
+
585
+ class UDLPReLU2(Layer):
586
+ """Unbiased Dual Layer Perceptron, Relu^2 Activation"""
587
+
588
+ def __init__(self, d_model, expand, dropout=0.075):
589
+ super().__init__()
590
+ self.expand = nn.Linear(d_model, d_model * expand, bias=False)
591
+ self.contract = nn.Linear(d_model * expand, d_model, bias=False)
592
+ self.dropout = nn.Dropout(dropout)
593
+
594
+ nn.init.kaiming_normal_(self.expand.weight, mode='fan_in', nonlinearity='relu')
595
+ nn.init.zeros_(self.contract.weight)
596
+
597
+ def forward(self, x):
598
+ return self.dropout(self.contract(torch.relu(self.expand(x)).square()))
599
+
600
+
601
+ class MLP(Layer):
602
+ __name__ = "MLP"
603
+ __complexity__ = "O(depth L d^2)"
604
+
605
+ def __init__(self, d_model: int, depth: int, expand: int, activation: Callable, dropout=0.1):
606
+ super().__init__()
607
+ hidden_dim = d_model * expand
608
+ self.d_model = d_model
609
+ self.depth = depth
610
+ self.expand = expand
611
+ self.activation = activation
612
+ self.dropout = nn.Dropout(dropout)
613
+ layers = []
614
+ for i in range(depth):
615
+ if i == 0:
616
+ in_dim = d_model
617
+ out_dim = hidden_dim
618
+ elif i == depth - 1:
619
+ in_dim = hidden_dim
620
+ out_dim = d_model
621
+ else:
622
+ in_dim = hidden_dim
623
+ out_dim = hidden_dim
624
+
625
+ if hasattr(activation, 'gated') and i < depth - 1:
626
+ out_dim = out_dim * 2
627
+
628
+ layers.append(nn.Linear(in_dim, out_dim, bias=True))
629
+ if i < depth - 1:
630
+ layers.append(activation())
631
+
632
+ self.layers = nn.Sequential(*layers)
633
+
634
+ def forward(self, x: torch.Tensor):
635
+ return self.dropout(self.layers(x))
636
+
637
+
638
+ class DLPGelu(Layer):
639
+ def __init__(self, d_model, expand, dropout=0.1):
640
+ super().__init__()
641
+ self.expand = nn.Linear(d_model, d_model * expand, bias=True)
642
+ self.contract = nn.Linear(d_model * expand, d_model, bias=True)
643
+ self.dropout = nn.Dropout(dropout)
644
+
645
+ def forward(self, x):
646
+ return self.dropout(self.contract(F.gelu(self.expand(x))))
647
+
648
+
649
+ class DLPSoftmax(Layer):
650
+ def __init__(self, d_model, m, dropout=0.1, copy=False):
651
+ super().__init__()
652
+ self.expand = nn.Linear(d_model, m, bias=False)
653
+ self.q_proj = nn.Linear(d_model, d_model, bias=False)
654
+ self.contract = nn.Linear(m, d_model, bias=False)
655
+
656
+ self.scaler = nn.Parameter(torch.tensor([1 / math.sqrt(d_model)]))
657
+
658
+ self.dropout = nn.Dropout(dropout)
659
+
660
+ if copy:
661
+ with torch.no_grad():
662
+ self.contract.weight.copy_(self.expand.weight.t())
663
+
664
+ def forward(self, x):
665
+ q = self.q_proj(x)
666
+ logits = self.expand(q * self.scaler)
667
+
668
+ attn_weights = logits.softmax(dim=-1)
669
+ attn_weights = self.dropout(attn_weights)
670
+
671
+ return self.contract(attn_weights)
672
+
673
+
674
+ class FlashSDLP(Layer):
675
+ def __init__(self, d_model, m, num_heads, dropout=0.1, outproj=False):
676
+ super().__init__()
677
+ assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
678
+
679
+ self.num_heads = num_heads
680
+ self.head_dim = d_model // num_heads
681
+ self.m = m
682
+ self.dropout_p = dropout
683
+
684
+ self.M1 = nn.Parameter(torch.randn(num_heads, m, self.head_dim))
685
+ self.M2 = nn.Parameter(torch.randn(num_heads, m, self.head_dim))
686
+
687
+ self.scaler = nn.Parameter(torch.tensor([1.0]))
688
+
689
+ if outproj:
690
+ self.out_proj = nn.Linear(d_model, d_model, bias=False)
691
+ else:
692
+ self.out_proj = nn.Identity()
693
+
694
+ self._reset_parameters()
695
+
696
+ def _reset_parameters(self):
697
+ nn.init.xavier_normal_(self.M1)
698
+ nn.init.xavier_normal_(self.M2)
699
+
700
+ if isinstance(self.out_proj, nn.Linear):
701
+ nn.init.xavier_uniform_(self.out_proj.weight)
702
+
703
+ def forward(self, x):
704
+ batch_size, seq_len, _ = x.shape
705
+
706
+ q = x.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
707
+
708
+ k = self.M1.unsqueeze(0).expand(batch_size, -1, -1, -1)
709
+ v = self.M2.unsqueeze(0).expand(batch_size, -1, -1, -1)
710
+
711
+ attn_output = F.scaled_dot_product_attention(
712
+ query=q * self.scaler,
713
+ key=k,
714
+ value=v,
715
+ dropout_p=self.dropout_p if self.training else 0.0,
716
+ is_causal=False
717
+ )
718
+ attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, -1)
719
+
720
+ return self.out_proj(attn_output)
721
+
722
+
723
+ class DLPSwiGLU(Layer):
724
+ def __init__(self, d_model, expand, dropout=0.00):
725
+ super().__init__()
726
+ self.expand = nn.Linear(d_model, d_model * expand * 2, bias=True)
727
+ self.contract = nn.Linear(d_model * expand, d_model, bias=True)
728
+ self.activation = SwiGLU()
729
+ self.dropout = nn.Dropout(dropout)
730
+
731
+ def forward(self, x):
732
+ return self.dropout(self.contract(self.activation(self.expand(x))))
733
+
734
+
735
+ class LowRankGatedDLP(Layer):
736
+ def __init__(self, d_model, expand, dropout=0.00, activation=nn.SiLU, rank=64, gate_activation=None):
737
+ super().__init__()
738
+ self.ga = gate_activation if gate_activation is not None else Identity()
739
+ self.act = activation()
740
+ self.M = d_model * expand
741
+ self.rank = rank
742
+ self.expand = nn.Linear(d_model, d_model * expand + rank, bias=False)
743
+ self.contract = nn.Linear(d_model * expand, d_model, bias=False)
744
+ self.rexp = nn.Linear(rank, d_model * expand, bias=False)
745
+ self.dropout = nn.Dropout(dropout)
746
+
747
+ def forward(self, x):
748
+ out1, gate1 = self.expand(x).split([self.M, self.rank], dim=-1)
749
+ return self.contract(self.act(out1 * self.ga(self.rexp(gate1))))
750
+
751
+
752
+ class UDLPSwiGLU(Layer):
753
+ def __init__(self, d_model, expand, dropout=0.00):
754
+ super().__init__()
755
+ self.expand = nn.Linear(d_model, d_model * expand * 2, bias=False)
756
+ self.contract = nn.Linear(d_model * expand, d_model, bias=False)
757
+ self.activation = SwiGLU()
758
+ self.dropout = nn.Dropout(dropout)
759
+
760
+ def forward(self, x):
761
+ return self.dropout(self.contract(self.activation(self.expand(x))))
762
+
763
+
764
+ class VariableUDLP(Layer):
765
+ def __init__(self, d_model, d_output, M, activation=ReLU2, dropout=0.00):
766
+ super().__init__()
767
+ self.expand = nn.Linear(d_model, M, bias=False)
768
+ self.contract = nn.Linear(M, d_output, bias=False)
769
+ self.activation = activation()
770
+ self.dropout = nn.Dropout(dropout)
771
+
772
+ def _init_weights(self):
773
+ # Identity-at-init: expand orthogonal, contract → 0 ⇒ FFN contributo nullo.
774
+ torch.nn.init.orthogonal_(self.expand.weight)
775
+ torch.nn.init.zeros_(self.contract.weight)
776
+
777
+ def forward(self, x):
778
+ return self.dropout(self.contract(self.activation(self.expand(x))))
779
+
780
+
781
+ class VariableUDLP_Attention(Layer):
782
+ def __init__(self, d_model, d_output, M, activation=ReLU2, dropout=0.00, use_qk_norm=True):
783
+ super().__init__()
784
+ self.d_model = d_model
785
+ self.d_output = d_output
786
+ self.half_dim = d_model // 2
787
+
788
+ # Dimensione delle query/key. Per efficienza la tengo pari a half_dim
789
+ self.d_k = self.half_dim
790
+
791
+ # Q = X W_q
792
+ self.W_q = nn.Linear(self.half_dim, self.d_k, bias=False)
793
+
794
+ # K = W_k (Matrice di parametri appresi, forma: [M, d_k])
795
+ self.W_k = nn.Parameter(torch.empty(M, self.d_k))
796
+ torch.nn.init.normal_(self.W_k, std=self.d_k ** -0.5)
797
+
798
+ # V = W_v corrisponde al tuo 'contract'
799
+ self.contract = nn.Linear(M, d_output, bias=False)
800
+
801
+ self.activation = activation()
802
+ self.dropout = nn.Dropout(dropout)
803
+
804
+ # QK Norm
805
+ self.use_qk_norm = use_qk_norm
806
+ if self.use_qk_norm:
807
+ self.q_norm = nn.RMSNorm(self.d_k)
808
+ self.k_norm = nn.RMSNorm(self.d_k)
809
+
810
+ def _init_weights(self):
811
+ torch.nn.init.zeros_(self.contract.weight)
812
+
813
+ def forward(self, x):
814
+ # 1. Prendiamo solo mezza residual stream
815
+ x_half = x[..., :self.half_dim]
816
+
817
+ # 2. Calcoliamo le Query e prendiamo le Key
818
+ q = self.W_q(x_half)
819
+ k = self.W_k
820
+
821
+ # 3. QK Norm
822
+ if self.use_qk_norm:
823
+ q = self.q_norm(q)
824
+ k = self.k_norm(k)
825
+
826
+ # 4. Score dell'attention: Q K^T
827
+ # q è [..., d_k], k è [M, d_k]. F.linear(q, k) fa esattamente q @ k.T
828
+ scores = F.linear(q, k)
829
+
830
+ # 5. Attivazione generica (es. ReLU^2) e Dropout
831
+ h = self.dropout(self.activation(scores))
832
+
833
+ # 6. Proiezione V (contract) e padding automatico se d_output < d_model
834
+ out = self.contract(h)
835
+
836
+ if self.d_output < self.d_model:
837
+ out = F.pad(out, (0, self.d_model - self.d_output))
838
+
839
+ return out
840
+
841
+
842
+ class VariableUDLP_CrossGate(Layer):
843
+ def __init__(self, d_model, d_output, M, activation=ReLU2, dropout=0.00):
844
+ super().__init__()
845
+ self.d_model = d_model
846
+ self.d_output = d_output
847
+ self.half_dim = d_model // 2
848
+
849
+ self.expand_k = nn.Linear(self.half_dim, M, bias=False)
850
+
851
+ self.expand_g = nn.Linear(d_model - self.half_dim, M, bias=False)
852
+
853
+ self.contract = nn.Linear(M, d_output, bias=False)
854
+
855
+ self.activation = activation()
856
+ self.dropout = nn.Dropout(dropout)
857
+
858
+ def _init_weights(self):
859
+ torch.nn.init.zeros_(self.contract.weight)
860
+
861
+ def forward(self, x):
862
+ x_left = x[..., :self.half_dim]
863
+ x_right = x[..., self.half_dim:]
864
+
865
+ hk = self.expand_k(x_left)
866
+ hg = self.expand_g(x_right)
867
+
868
+ h = self.dropout(self.activation(hk) * hg)
869
+
870
+ out = self.contract(h)
871
+
872
+ return out
873
+
874
+
875
+ class VariableGatedUDLP(Layer):
876
+ """UDLP + sparse output gate (parameter-golf / Modded-NanoGPT 1667 stile).
877
+
878
+ Gate sigmoid su prima slice di `gate_input_dim` del residual (sparse).
879
+ Output: `contract(activation(expand(x))) * sigmoid(gate_proj(x[..., :gate_input_dim]))`.
880
+ """
881
+ __name__ = "VariableGatedUDLP"
882
+
883
+ def __init__(self, d_model, d_output, M, activation=ReLU2, dropout=0.00, gate_input_dim=12):
884
+ super().__init__()
885
+ self.gate_input_dim = gate_input_dim
886
+ self.expand = nn.Linear(d_model, M, bias=False)
887
+ self.contract = nn.Linear(M, d_output, bias=False)
888
+ self.gate_proj = nn.Linear(gate_input_dim, d_output, bias=False)
889
+ self.activation = activation()
890
+ self.dropout = nn.Dropout(dropout)
891
+
892
+ def _init_weights(self):
893
+ # Identity-at-init: expand + gate_proj orthogonal, contract → 0
894
+ # (con contract=0 il branch è zero indipendentemente dal gate).
895
+ torch.nn.init.orthogonal_(self.expand.weight)
896
+ torch.nn.init.zeros_(self.contract.weight)
897
+ torch.nn.init.orthogonal_(self.gate_proj.weight)
898
+
899
+ def forward(self, x):
900
+ out = self.contract(self.activation(self.expand(x)))
901
+ gate = torch.sigmoid(self.gate_proj(x[..., :self.gate_input_dim]))
902
+ return self.dropout(out * gate)
903
+
904
+
905
+ class DoubleUDLP(Layer):
906
+ def __init__(self, d_model, d_output, M, activation=ReLU2, dropout=0.00):
907
+ super().__init__()
908
+ self.expand = DoubleLinear(d_model, M)
909
+ self.contract = nn.Linear(M, d_output)
910
+ self.activation = activation()
911
+ self.dropout = nn.Dropout(dropout)
912
+
913
+ def _init_weights(self):
914
+ torch.nn.init.zeros_(self.contract.weight)
915
+
916
+ def forward(self, x):
917
+ return self.dropout(self.contract(self.activation(self.expand(x))))
918
+
919
+
920
+ class M_UDLP(Layer):
921
+ def __init__(self, d_model, d_output, M, activation=ReLU2, dropout=0.00):
922
+ super().__init__()
923
+ self.in_proj = nn.Linear(d_model, d_model)
924
+ self.expand = nn.Linear(d_model, M)
925
+ self.contract = nn.Linear(M, d_output)
926
+ self.activation = activation()
927
+ self.dropout = nn.Dropout(dropout)
928
+
929
+ def _init_weights(self):
930
+ torch.nn.init.zeros_(self.contract.weight)
931
+
932
+ def forward(self, x):
933
+ return self.dropout(self.contract(self.activation(self.expand(x))))
934
+
935
+
936
+ class F_UDLPSwiGLU(Layer):
937
+ def __init__(self, d_model, expand, dropout=0.05, lora_rank=16):
938
+ super().__init__()
939
+ self.expand = nn.Linear(d_model, d_model * expand * 2, bias=False)
940
+ self.contract = nn.Linear(d_model * expand, d_model, bias=False)
941
+ self.LoRA_expand = LowRankLinear(d_model, expand * d_model * 2, lora_rank)
942
+ self.LoRA_contract = LowRankLinear(d_model * expand, d_model, lora_rank)
943
+ self.scale = 1 / lora_rank * 2
944
+ self.finetuning = False
945
+ self.activation = SwiGLU()
946
+ self.dropout = nn.Dropout(dropout)
947
+
948
+ def forward(self, x):
949
+ if not self.finetuning:
950
+ return self.dropout(self.contract(self.activation(self.expand(x))))
951
+ else:
952
+ act = self.activation(self.expand(x) + self.LoRA_expand(x) * self.scale)
953
+ return self.dropout(
954
+ self.contract(act) + self.LoRA_contract(act) * self.scale
955
+ )
956
+
957
+ def finetune(self):
958
+ torch.nn.init.zeros_(self.LORA_expand.l2.weight)
959
+ torch.nn.init.zeros_(self.LoRA_contract.l2.weight)
960
+ self.expand.requires_grad_(False)
961
+ self.contract.requires_grad_(False)
962
+ self.finetuning = True
963
+
964
+
965
+ class ResMLPBlock(Layer):
966
+ def __init__(self, d_model, expand=2, norm=True, activation: Callable = nn.GELU):
967
+ super().__init__()
968
+ self.activation1 = activation()
969
+ self.activation2 = activation()
970
+ self.norm = norm
971
+ self.d_model = d_model
972
+ self.expand = expand
973
+ self.l1 = nn.Linear(d_model, d_model * expand, bias=True)
974
+ self.l2 = nn.Linear(d_model * expand, d_model, bias=True)
975
+
976
+ self.g1 = nn.Linear(d_model, d_model * expand, bias=True)
977
+ self.g2 = nn.Linear(d_model * expand, d_model, bias=True)
978
+
979
+ self.norm = nn.LayerNorm(d_model) if self.norm else nn.Identity()
980
+
981
+ def forward(self, x: torch.Tensor):
982
+ x = self.l2(self.activation1(self.l1(x)))
983
+ x = self.norm(x) + x
984
+ x = self.g2(self.activation2(self.g1(x)))
985
+ return x
986
+
987
+
988
+ class ResMLP(Layer):
989
+ def __init__(self, d_model: int, depth: int, expand: int, activation: Callable, dropout=0.1):
990
+ super().__init__()
991
+ self.d_model = d_model
992
+ self.depth = depth
993
+ self.expand = expand
994
+ self.activation = activation
995
+ self.dropout = nn.Dropout(dropout)
996
+
997
+ layers = []
998
+ for i in range(depth):
999
+ layers.append(ResMLPBlock(d_model, expand=expand, activation=activation))
1000
+
1001
+ self.layers = nn.Sequential(*layers)
1002
+
1003
+ def forward(self, x: torch.Tensor):
1004
+ return self.dropout(self.layers(x))
1005
+
1006
+
1007
+ class ConvResBlock(Layer):
1008
+ __complexity__ = "O(L k^2 in out)"
1009
+
1010
+ def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, activation=nn.ReLU):
1011
+ super(ConvResBlock, self).__init__()
1012
+ self.bn = nn.BatchNorm2d(out_channels)
1013
+ self.conv1 = nn.Conv2d(in_channels, in_channels, kernel_size, padding='same')
1014
+ self.conv2 = nn.Conv2d(in_channels, in_channels, kernel_size, padding='same')
1015
+ self.convout = nn.Conv2d(in_channels, out_channels, kernel_size, stride=stride, padding=padding)
1016
+ self.activation = activation()
1017
+
1018
+ def forward(self, x):
1019
+ res = x
1020
+ x = self.bn(x)
1021
+ x = self.activation(self.conv1(x))
1022
+ x = self.conv2(x)
1023
+ x = x + res
1024
+ return self.convout(x)
1025
+
1026
+
1027
+ class RMSNorm(nn.Module):
1028
+ def __init__(self, d_model: int, eps: float = 1e-6):
1029
+ super().__init__()
1030
+ self.eps = eps
1031
+ self.weight = nn.Parameter(torch.ones(d_model))
1032
+
1033
+ def forward(self, x):
1034
+ input_dtype = x.dtype
1035
+
1036
+ x_fp32 = x.to(torch.float32)
1037
+ variance = x_fp32.pow(2).mean(dim=-1, keepdim=True)
1038
+
1039
+ x_rsqrt = torch.rsqrt(variance + self.eps)
1040
+
1041
+ return self.weight * (x_fp32 * x_rsqrt).to(input_dtype)
1042
+
1043
+
1044
+ class EMA:
1045
+ def __init__(self, model, decay=0.999):
1046
+ self.decay = decay
1047
+ self.shadow = {name: param.clone().detach() for name, param in model.named_parameters() if param.requires_grad}
1048
+
1049
+ @torch.no_grad()
1050
+ def update(self, model):
1051
+ for name, param in model.named_parameters():
1052
+ if param.requires_grad:
1053
+ self.shadow[name].lerp_(param, 1.0 - self.decay)
1054
+
1055
+ def apply_shadow(self, model):
1056
+ for name, param in model.named_parameters():
1057
+ if param.requires_grad:
1058
+ param.data.copy_(self.shadow[name])
1059
+
1060
+
1061
+ class Nova(Layer):
1062
+ def __init__(self):
1063
+ super().__init__()
1064
+ self.beta = nn.Parameter(torch.tensor([0.7]))
1065
+
1066
+ def forward(self, x):
1067
+ h = -self.beta * x
1068
+ return x * (torch.sigmoid(-h) - (1 / (1 + h ** 2)))
1069
+
1070
+
1071
+ class MinusNova(Layer):
1072
+ def __init__(self):
1073
+ super().__init__()
1074
+ self.beta = nn.Parameter(torch.tensor([0.7]))
1075
+
1076
+ def forward(self, x):
1077
+ h = -self.beta * -x
1078
+ return x * (torch.sigmoid(-h) - (1 / (1 + h ** 2)))
1079
+
1080
+
1081
+ class TaylorAct(nn.Module):
1082
+ def __init__(self, order=3):
1083
+ super().__init__()
1084
+ self.order = order
1085
+ self.coeffs = nn.Parameter(torch.randn(order + 1) * 0.02)
1086
+
1087
+ def forward(self, x):
1088
+ device = x.device
1089
+ exponents = torch.arange(self.order + 1, device=device, dtype=x.dtype)
1090
+ x_pow = x.unsqueeze(-1).pow(exponents)
1091
+
1092
+ return torch.matmul(x_pow, self.coeffs)
1093
+
1094
+ def plot(self, bounds=(-4, 4), n_points=200):
1095
+ x = torch.linspace(bounds[0], bounds[1], n_points)
1096
+ with torch.no_grad():
1097
+ y = self.forward(x)
1098
+
1099
+ plt.figure(figsize=(7, 4))
1100
+ plt.plot(x.numpy(), y.numpy(), label='TaylorAct', color='royalblue')
1101
+ plt.axhline(0, color='gray', linewidth=0.8, linestyle='--')
1102
+ plt.axvline(0, color='gray', linewidth=0.8, linestyle='--')
1103
+ plt.title('TaylorAct Activation Function')
1104
+ plt.xlabel('x')
1105
+ plt.ylabel('f(x)')
1106
+ plt.legend()
1107
+ plt.grid(True, alpha=0.3)
1108
+ plt.tight_layout()
1109
+ plt.show()
1110
+
1111
+
1112
+ class FastPoly3(nn.Module):
1113
+ def __init__(self, d_model: int):
1114
+ super().__init__()
1115
+ self.a0 = nn.Parameter(torch.zeros(d_model))
1116
+ self.a1 = nn.Parameter(torch.ones(d_model))
1117
+ self.a2 = nn.Parameter(torch.zeros(d_model))
1118
+ self.a3 = nn.Parameter(torch.zeros(d_model))
1119
+
1120
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1121
+ return self.a0 + x * (self.a1 + x * (self.a2 + x * self.a3))
1122
+
1123
+
1124
+ class CastedLinear(nn.Linear):
1125
+ def forward(self, x: Tensor) -> Tensor:
1126
+ bias = self.bias.to(x.dtype) if self.bias is not None else None
1127
+ return F.linear(x, self.weight.to(x.dtype), bias)
1128
+
1129
+
1130
+ if __name__ == '__main__':
1131
+ act = TaylorAct()
1132
+ act.plot() # default -4, 4
1133
+ act.plot(bounds=(-1, 1)) # custom bounds
1134
+ act.plot(bounds=(0, 1), n_points=100)
vathos/_experiments.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+ import numpy as np
6
+ import matplotlib.pyplot as plt
7
+ import matplotlib.colors as mcolors
8
+
9
+
10
+ class Experiment:
11
+ """
12
+ Un tracker di esperimenti stile WandB per il framework Vathos.
13
+ Gestisce configurazioni, metriche, salvataggi e visualizzazioni avanzate.
14
+ """
15
+
16
+ def __init__(self, project_name: str, exp_name: str = None, config: dict = None, root_dir: str = "runs"):
17
+ self.project_name = project_name
18
+
19
+ # Genera un nome univoco se non fornito
20
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
21
+ self.exp_name = exp_name if exp_name else f"run_{timestamp}"
22
+ self.config = config or {}
23
+
24
+ # Setup directory dell'esperimento
25
+ self.run_dir = Path(root_dir) / self.project_name / self.exp_name
26
+ self.run_dir.mkdir(parents=True, exist_ok=True)
27
+
28
+ # Dizionari per le metriche.
29
+ # Formato: {"loss": {"steps": [1,2,3], "values": [0.9, 0.8, 0.7]}}
30
+ self.metrics = {}
31
+
32
+ # Salva subito la configurazione
33
+ self._save_config()
34
+ print(f"🚀 Esperimento '{self.exp_name}' inizializzato in {self.run_dir}")
35
+
36
+ def log(self, metrics_dict: dict, step: int):
37
+ """
38
+ Registra un dizionario di metriche per un dato step.
39
+ Es: exp.log({"train_loss": 0.5, "val_accuracy": 0.8}, step=100)
40
+ """
41
+ for k, v in metrics_dict.items():
42
+ # Converte tensori PyTorch o Numpy in float standard
43
+ if hasattr(v, "item"):
44
+ v = v.item()
45
+
46
+ if k not in self.metrics:
47
+ self.metrics[k] = {"steps": [], "values": []}
48
+
49
+ self.metrics[k]["steps"].append(step)
50
+ self.metrics[k]["values"].append(v)
51
+
52
+ def _save_config(self):
53
+ with open(self.run_dir / "config.json", "w") as f:
54
+ json.dump(self.config, f, indent=4)
55
+
56
+ def save(self):
57
+ """Salva tutte le metriche correnti in un file JSON."""
58
+ with open(self.run_dir / "metrics.json", "w") as f:
59
+ json.dump(self.metrics, f, indent=4)
60
+ # print(f"💾 Metriche salvate in {self.run_dir / 'metrics.json'}")
61
+
62
+ @classmethod
63
+ def load(cls, run_dir: str):
64
+ """
65
+ Inizializza un oggetto Experiment a partire da una cartella salvata.
66
+ Permette di ricaricare e plottare run vecchie!
67
+ """
68
+ run_path = Path(run_dir)
69
+
70
+ # Estrai project e exp name dal path (es. runs/Progetto/Run)
71
+ exp_name = run_path.name
72
+ project_name = run_path.parent.name
73
+
74
+ with open(run_path / "config.json", "r") as f:
75
+ config = json.load(f)
76
+
77
+ # Ricostruisce l'oggetto bypassando la creazione della cartella root
78
+ exp = cls(project_name=project_name, exp_name=exp_name, config=config, root_dir=run_path.parent.parent)
79
+
80
+ with open(run_path / "metrics.json", "r") as f:
81
+ exp.metrics = json.load(f)
82
+
83
+ print(f"📂 Esperimento '{exp_name}' caricato con successo.")
84
+ return exp
85
+
86
+ def _smooth_curve(self, points, factor=0.85):
87
+ """Applica un Exponential Moving Average (EMA) per rendere i plot bellissimi."""
88
+ smoothed_points = []
89
+ for point in points:
90
+ if smoothed_points:
91
+ previous = smoothed_points[-1]
92
+ smoothed_points.append(previous * factor + point * (1 - factor))
93
+ else:
94
+ smoothed_points.append(point)
95
+ return smoothed_points
96
+
97
+ def plot(self, smoothing: float = 0.85, save_fig: bool = False, figsize=(10, 6)):
98
+ """
99
+ Genera plot in stile accademico/WandB.
100
+ Mostra le curve originali in trasparenza e le curve smoothate in evidenza.
101
+ """
102
+ if not self.metrics:
103
+ print("Nessuna metrica da plottare!")
104
+ return
105
+
106
+ # Stile estetico "stupendo" (simile a Seaborn/WandB)
107
+ plt.style.use('bmh') # Usa un tema base pulito integrato in matplotlib
108
+
109
+ num_metrics = len(self.metrics)
110
+ cols = 2 if num_metrics > 1 else 1
111
+ rows = (num_metrics + 1) // 2
112
+
113
+ fig, axes = plt.subplots(rows, cols, figsize=(figsize[0] * cols, figsize[1] * rows))
114
+ if num_metrics == 1:
115
+ axes = [axes]
116
+ else:
117
+ axes = axes.flatten()
118
+
119
+ # Colori moderni
120
+ colors = list(mcolors.TABLEAU_COLORS.values())
121
+
122
+ for idx, (metric_name, data) in enumerate(self.metrics.items()):
123
+ ax = axes[idx]
124
+ steps = data["steps"]
125
+ values = data["values"]
126
+ color = colors[idx % len(colors)]
127
+
128
+ # Plotta i dati raw (sbiaditi)
129
+ ax.plot(steps, values, color=color, alpha=0.2, label='Raw')
130
+
131
+ # Plotta i dati smoothati (in evidenza)
132
+ if smoothing > 0 and len(values) > 5:
133
+ smoothed = self._smooth_curve(values, factor=smoothing)
134
+ ax.plot(steps, smoothed, color=color, linewidth=2.5, label=f'Smoothed (α={smoothing})')
135
+
136
+ ax.set_title(metric_name.replace("_", " ").title(), fontsize=14, fontweight='bold')
137
+ ax.set_xlabel("Steps", fontsize=11)
138
+ ax.set_ylabel("Value", fontsize=11)
139
+ ax.grid(True, linestyle='--', alpha=0.6)
140
+ ax.legend(loc="best", frameon=True, shadow=True)
141
+
142
+ for idx in range(num_metrics, len(axes)):
143
+ fig.delaxes(axes[idx])
144
+
145
+ plt.suptitle(f"Experiment: {self.project_name} - {self.exp_name}", fontsize=16, y=1.02)
146
+ plt.tight_layout()
147
+
148
+ if save_fig:
149
+ fig_path = self.run_dir / "plots.png"
150
+ plt.savefig(fig_path, dpi=300, bbox_inches='tight')
151
+ print(f"📊 Plot salvato in {fig_path}")
152
+
153
+ plt.show()
154
+
155
+
vathos/_spatials.py ADDED
@@ -0,0 +1,2124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from Vathos._basics import *
2
+
3
+
4
+ class SinusoidalPositionalEncoding(Layer):
5
+ __name__ = "SinusoidalPositionalEncoding"
6
+ __complexity__ = "O(L^2 d^2)"
7
+
8
+ def __init__(self, d_model: int, max_len: int = 5000):
9
+ super().__init__()
10
+ self.d_model = d_model
11
+
12
+ position = torch.arange(max_len).unsqueeze(1)
13
+ div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
14
+
15
+ pe = torch.zeros(max_len, d_model)
16
+ pe[:, 0::2] = torch.sin(position * div_term)
17
+ pe[:, 1::2] = torch.cos(position * div_term)
18
+
19
+ self.register_buffer('pe', pe)
20
+
21
+ def forward(self, x: torch.Tensor):
22
+ B, L, D = x.shape
23
+ return x + self.pe[:L]
24
+
25
+
26
+ class RoPE(Layer):
27
+ __name__ = "RoPE"
28
+
29
+ def __init__(self, dim: int, max_len: int = 8192, base: float = 10000.0):
30
+ super().__init__()
31
+ self.dim = dim
32
+ self.base = base
33
+ self.max_len = max_len
34
+
35
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
36
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
37
+
38
+ self._cos_cached = None
39
+ self._sin_cached = None
40
+ self._seq_len_cached = 0
41
+
42
+ def _update_cache(self, seq_len: int, dtype: torch.dtype, device: torch.device):
43
+ if seq_len > self._seq_len_cached or self._cos_cached is None:
44
+ self._seq_len_cached = seq_len
45
+ t = torch.arange(seq_len, device=device, dtype=dtype)
46
+ freqs = torch.outer(t, self.inv_freq.to(device))
47
+
48
+ self._cos_cached = freqs.cos()
49
+ self._sin_cached = freqs.sin()
50
+
51
+ def _apply_rotary_emb(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor,
52
+ start_pos: int = 0) -> torch.Tensor:
53
+ """Apply rotary embeddings starting from start_pos"""
54
+ seq_len = x.shape[-2]
55
+
56
+ cos = cos[start_pos:start_pos + seq_len]
57
+ sin = sin[start_pos:start_pos + seq_len]
58
+
59
+ shape = [1] * x.ndim
60
+ shape[-2] = seq_len
61
+ shape[-1] = self.dim // 2
62
+
63
+ cos = cos.view(*shape)
64
+ sin = sin.view(*shape)
65
+
66
+ x1, x2 = x.chunk(2, dim=-1)
67
+ return torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
68
+
69
+ def forward(self, q: torch.Tensor, k: torch.Tensor = None, start_pos: int = 0):
70
+ """
71
+ Args:
72
+ q: Query tensor
73
+ k: Key tensor (optional)
74
+ start_pos: Starting position for RoPE (used during generation)
75
+ """
76
+ assert q.shape[-1] == self.dim, f"Last dim of q must be {self.dim}, got {q.shape[-1]}"
77
+ if k is not None:
78
+ assert k.shape[-2:] == q.shape[-2:], "k must have same seq_len and head_dim as q"
79
+
80
+ seq_len = q.shape[-2]
81
+ self._update_cache(start_pos + seq_len, q.dtype, q.device)
82
+
83
+ cos = self._cos_cached
84
+ sin = self._sin_cached
85
+
86
+ q_rope = self._apply_rotary_emb(q, cos, sin, start_pos)
87
+ k_rope = self._apply_rotary_emb(k, cos, sin, start_pos) if k is not None else None
88
+
89
+ return (q_rope, k_rope) if k_rope is not None else q_rope
90
+
91
+
92
+ class ALiBi(Layer):
93
+ __name__ = "ALiBi"
94
+
95
+ def __init__(self, n_heads: int):
96
+ super().__init__()
97
+ self.n_heads = n_heads
98
+
99
+ # Calculate standard ALiBi geometric slopes
100
+ closest_power_of_2 = 2 ** math.floor(math.log2(n_heads))
101
+ base = 2 ** (-(2 ** -(math.log2(closest_power_of_2) - 3)))
102
+ slopes = [math.pow(base, i) for i in range(1, closest_power_of_2 + 1)]
103
+
104
+ # Handle non-power-of-2 head counts (e.g., 12 heads)
105
+ if closest_power_of_2 != n_heads:
106
+ extra_base = 2 ** (-(2 ** -(math.log2(2 * closest_power_of_2) - 3)))
107
+ slopes.extend([math.pow(extra_base, i) for i in range(1, 2 * (n_heads - closest_power_of_2) + 1, 2)])
108
+
109
+ self.register_buffer(
110
+ "slopes",
111
+ torch.tensor(slopes, dtype=torch.float32).view(n_heads, 1, 1),
112
+ persistent=False
113
+ )
114
+
115
+ def forward(self, q_len: int, k_len: int, start_pos: int, device: torch.device, dtype: torch.dtype,
116
+ causal: bool = True) -> torch.Tensor:
117
+ # Create position indices
118
+ q_idx = torch.arange(start_pos, start_pos + q_len, device=device)[:, None]
119
+ k_idx = torch.arange(0, k_len, device=device)[None, :]
120
+
121
+ # Calculate relative distances (j - i).
122
+ # For causal attention, we only attend to the past, so k_idx <= q_idx, meaning distances <= 0.
123
+ distances = k_idx - q_idx
124
+
125
+ # Scale distances by head-specific slopes
126
+ alibi_bias = distances * self.slopes
127
+ alibi_bias = alibi_bias.to(dtype)
128
+
129
+ # Embed the causal mask directly into the ALiBi bias
130
+ if causal:
131
+ causal_mask = distances > 0
132
+ alibi_bias.masked_fill_(causal_mask, float('-inf'))
133
+
134
+ # Add batch dimension for SDPA broadcasting -> (1, n_heads, q_len, k_len)
135
+ return alibi_bias.unsqueeze(0)
136
+
137
+
138
+ class YaRN(Layer):
139
+ __name__ = "YaRN"
140
+
141
+ def __init__(self, dim: int, original_max_len: int = 4096, scale: float = 1.0,
142
+ base: float = 10000.0, beta_fast: int = 32, beta_slow: int = 1):
143
+ super().__init__()
144
+ self.dim = dim
145
+ self.base = base
146
+ self.original_max_len = original_max_len
147
+ self.scale = scale
148
+
149
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
150
+
151
+ if scale > 1.0:
152
+ wavelengths = 2 * math.pi / inv_freq
153
+
154
+ low = original_max_len / beta_slow
155
+ high = original_max_len / beta_fast
156
+
157
+ w = torch.clamp((wavelengths - high) / (low - high), 0.0, 1.0)
158
+
159
+ inv_freq_interpolated = inv_freq / scale
160
+ inv_freq_extrapolated = inv_freq
161
+
162
+ inv_freq = (1 - w) * inv_freq_extrapolated + w * inv_freq_interpolated
163
+
164
+ self.mscale = math.sqrt(0.1 * math.log(scale) + 1.0)
165
+ else:
166
+ self.mscale = 1.0
167
+
168
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
169
+
170
+ self._cos_cached = None
171
+ self._sin_cached = None
172
+ self._seq_len_cached = 0
173
+
174
+ def _update_cache(self, seq_len: int, dtype: torch.dtype, device: torch.device):
175
+ if seq_len > self._seq_len_cached or self._cos_cached is None:
176
+ self._seq_len_cached = seq_len
177
+ t = torch.arange(seq_len, device=device, dtype=dtype)
178
+ freqs = torch.outer(t, self.inv_freq.to(device))
179
+
180
+ # Apply the YaRN mscale directly to the cache
181
+ self._cos_cached = (freqs.cos() * self.mscale)
182
+ self._sin_cached = (freqs.sin() * self.mscale)
183
+
184
+ def _apply_rotary_emb(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor,
185
+ start_pos: int = 0) -> torch.Tensor:
186
+ """Apply rotary embeddings starting from start_pos"""
187
+ seq_len = x.shape[-2]
188
+
189
+ cos = cos[start_pos:start_pos + seq_len]
190
+ sin = sin[start_pos:start_pos + seq_len]
191
+
192
+ shape = [1] * x.ndim
193
+ shape[-2] = seq_len
194
+ shape[-1] = self.dim // 2
195
+
196
+ cos = cos.view(*shape).to(x.dtype)
197
+ sin = sin.view(*shape).to(x.dtype)
198
+
199
+ x1, x2 = x.chunk(2, dim=-1)
200
+ return torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
201
+
202
+ def forward(self, q: torch.Tensor, k: torch.Tensor = None, start_pos: int = 0):
203
+ assert q.shape[-1] == self.dim, f"Last dim of q must be {self.dim}, got {q.shape[-1]}"
204
+ if k is not None:
205
+ assert k.shape[-2:] == q.shape[-2:], "k must have same seq_len and head_dim as q"
206
+
207
+ seq_len = q.shape[-2]
208
+ self._update_cache(start_pos + seq_len, q.dtype, q.device)
209
+
210
+ cos = self._cos_cached
211
+ sin = self._sin_cached
212
+
213
+ q_rope = self._apply_rotary_emb(q, cos, sin, start_pos)
214
+ k_rope = self._apply_rotary_emb(k, cos, sin, start_pos) if k is not None else None
215
+
216
+ return (q_rope, k_rope) if k_rope is not None else q_rope
217
+
218
+
219
+ class NanoYaRN(Layer):
220
+ """Modded Nano-Gpt implmenetation of YaRN"""
221
+
222
+ def __init__(self, head_dim, max_seq_len, paired=False):
223
+ super().__init__()
224
+ self.head_dim = head_dim
225
+ self.max_seq_len = max_seq_len
226
+ self.paired = paired
227
+ self.reset()
228
+
229
+ def rotary(self, x_BTHD):
230
+ assert self.factor1.size(0) >= x_BTHD.size(-3)
231
+ factor1, factor2 = (
232
+ self.factor1[None, : x_BTHD.size(-3), None, :],
233
+ self.factor2[None, : x_BTHD.size(-3), None, :],
234
+ )
235
+ x_flip = x_BTHD.view(*x_BTHD.shape[:-1], x_BTHD.shape[-1] // 2, 2).flip(-1).view(x_BTHD.shape)
236
+ return factor1 * x_BTHD + factor2 * x_flip
237
+
238
+ def reset(self):
239
+ angular_freq = (1 / 1024) ** torch.linspace(0, 1, steps=self.head_dim // 4, dtype=torch.float32)
240
+ angular_freq = angular_freq.repeat_interleave(2)
241
+ # half-truncate RoPE by @YouJiacheng (w/ base freq tuning)
242
+ angular_freq = torch.cat([angular_freq, angular_freq.new_zeros(self.head_dim // 2)])
243
+ t = torch.arange(2 * self.max_seq_len, dtype=torch.float32)
244
+ if not self.paired:
245
+ theta = torch.outer(t, angular_freq)
246
+ self.factor1 = nn.Buffer(
247
+ theta.cos().to(torch.bfloat16), persistent=False
248
+ )
249
+ self.factor2 = nn.Buffer(
250
+ theta.sin().to(torch.bfloat16), persistent=False
251
+ )
252
+ else:
253
+ t_even = 2 * t
254
+ t_odd = 2 * t + 1
255
+ theta1 = torch.outer(t_even, angular_freq)
256
+ theta2 = torch.outer(t_odd, angular_freq)
257
+ self.factor1 = nn.Buffer(
258
+ torch.cat((theta1.cos(), theta2.cos()), dim=-1).to(torch.bfloat16),
259
+ persistent=False
260
+ )
261
+ self.factor2 = nn.Buffer(
262
+ torch.cat((theta1.sin(), theta2.sin()), dim=-1).to(torch.bfloat16),
263
+ persistent=False
264
+ )
265
+ self.factor2[..., 1::2] *= -1
266
+ self.angular_freq = angular_freq
267
+ # start with 0.1, inspired by 0.12 from @leloykun and learnable scalars used by @brendanh0gan https://x.com/hi_tysam/status/1879693583898591283
268
+ self.attn_scale = 0.1
269
+
270
+ def apply(self, old_window: int, new_window: int, alpha: int = 1, beta: int = 32):
271
+ rotations = old_window * self.angular_freq / (2 * torch.pi)
272
+ scaling_factor = old_window / new_window
273
+ interpolation_weight = torch.clamp((rotations - alpha) / (beta - alpha), 0, 1)
274
+ self.angular_freq *= scaling_factor + interpolation_weight * (1 - scaling_factor)
275
+ t = torch.arange(2 * self.max_seq_len, dtype=torch.float32, device=self.angular_freq.device)
276
+ if not self.paired:
277
+ theta = torch.outer(t, self.angular_freq)
278
+ self.factor1.copy_(theta.cos())
279
+ self.factor2.copy_(theta.sin())
280
+ else:
281
+ t_even = 2 * t
282
+ t_odd = 2 * t + 1
283
+ theta1 = torch.outer(t_even, self.angular_freq)
284
+ theta2 = torch.outer(t_odd, self.angular_freq)
285
+ self.factor1.copy_(torch.cat((theta1.cos(), theta2.cos()), dim=-1))
286
+ self.factor2.copy_(torch.cat((theta1.sin(), theta2.sin()), dim=-1))
287
+ self.factor2[..., 1::2] *= -1
288
+ self.attn_scale *= 0.2 * math.log(new_window / old_window) + 1
289
+
290
+
291
+ class MultiheadAttentionMixer(Layer):
292
+ __name__ = "MultiheadAttentionMixer"
293
+ __complexity__ = "O(L^2 d + L d^2)"
294
+
295
+ def __init__(self, d_model: int, n_heads: int, causal: bool = True,
296
+ pos_emb: nn.Module = None, dropout: float = 0.0, qk_norm=False):
297
+ super().__init__()
298
+ assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
299
+ self.causal = causal
300
+ self.d_model = d_model
301
+ self.n_heads = n_heads
302
+ self.head_dim = d_model // n_heads
303
+ self.dropout_p = dropout
304
+
305
+ self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
306
+ self.out = nn.Linear(d_model, d_model, bias=False)
307
+
308
+ self.pos_emb = pos_emb
309
+ self.kv_cache = None
310
+ self.qk_norm = qk_norm
311
+ if self.qk_norm:
312
+ self.q_norm = RMSNorm(self.head_dim)
313
+ self.k_norm = RMSNorm(self.head_dim)
314
+
315
+ self._init_weights()
316
+
317
+ def _init_weights(self):
318
+ # Identity-at-init: qkv orthogonal (preserva geometria), out → 0.
319
+ nn.init.orthogonal_(self.qkv.weight)
320
+ nn.init.zeros_(self.out.weight)
321
+
322
+ # Alias backward-compat per chi chiama explicit _reset_parameters.
323
+ _reset_parameters = _init_weights
324
+
325
+ def forward(self, x: torch.Tensor, ve=None) -> torch.Tensor:
326
+ """
327
+ ve: Stands for value embeddings, which should be already weighted byt the caller, of course the dimension must match x
328
+ """
329
+ B, L, D = x.shape
330
+
331
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
332
+ q, k, v = qkv.unbind(dim=2)
333
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
334
+
335
+ if self.qk_norm:
336
+ q = self.q_norm(q)
337
+ k = self.k_norm(k)
338
+
339
+ if self.pos_emb is not None:
340
+ q, k = self.pos_emb(q, k, start_pos=0)
341
+ if ve is not None:
342
+ v = v + ve.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
343
+
344
+ attn = F.scaled_dot_product_attention(
345
+ q, k, v,
346
+ is_causal=self.causal,
347
+ dropout_p=self.dropout_p if self.training else 0.0,
348
+ )
349
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
350
+
351
+ def show_weights_forward(self, x):
352
+ B, L, D = x.shape
353
+
354
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
355
+ q, k, v = qkv.unbind(dim=2)
356
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
357
+
358
+ if self.qk_norm:
359
+ q = self.q_norm(q)
360
+ k = self.k_norm(k)
361
+
362
+ if self.pos_emb is not None:
363
+ q, k = self.pos_emb(q, k, start_pos=0)
364
+
365
+ scale = 1.0 / math.sqrt(self.head_dim)
366
+ scores = torch.matmul(q, k.transpose(-2, -1)) * scale
367
+
368
+ if self.causal:
369
+ mask = torch.triu(torch.ones(L, L, dtype=torch.bool, device=x.device), diagonal=1)
370
+ scores.masked_fill_(mask, float('-inf'))
371
+
372
+ attn_weights = F.softmax(scores, dim=-1)
373
+ attn = torch.matmul(attn_weights, v)
374
+
375
+ out = self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
376
+ return out, attn_weights
377
+
378
+ def generate(self, x: torch.Tensor, ve=None) -> torch.Tensor:
379
+ B, L, D = x.shape
380
+
381
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
382
+ q, k, v = qkv.unbind(dim=2)
383
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
384
+
385
+ if self.qk_norm:
386
+ q = self.q_norm(q)
387
+ k = self.k_norm(k)
388
+
389
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
390
+
391
+ if self.pos_emb is not None:
392
+ q, k = self.pos_emb(q, k, start_pos=pos_offset)
393
+
394
+ if ve is not None:
395
+ v = v + ve.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
396
+
397
+ if self.kv_cache is not None:
398
+ k_cache, v_cache = self.kv_cache
399
+ k = torch.cat([k_cache, k], dim=2)
400
+ v = torch.cat([v_cache, v], dim=2)
401
+
402
+ self.kv_cache = (k, v)
403
+
404
+ attn = F.scaled_dot_product_attention(
405
+ q, k, v,
406
+ is_causal=self.causal and (L > 1),
407
+ dropout_p=0.0,
408
+ )
409
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
410
+
411
+ def clear_cache(self):
412
+ self.kv_cache = None
413
+
414
+ def finetune(self):
415
+ self.qkv.weight.requires_grad = False
416
+ self.out.weight.requires_grad = False
417
+
418
+
419
+ class MultiheadGatedAttentionMixer(Layer):
420
+ """MHA + sparse per-head output gate (Modded-NanoGPT PR #117 / parameter-golf 1667).
421
+
422
+ Gate input: prime `gate_input_dim` (default 12) dimensioni di x — sparse, ottimizzato
423
+ per Muon. Output: un sigmoid scalare per head, broadcast su head_dim. Init zero ⇒
424
+ gate parte a 0.5 (no-op).
425
+ """
426
+ __name__ = "MultiheadGatedAttentionMixer"
427
+ __complexity__ = "O(L^2 d + L d^2)"
428
+
429
+ def __init__(self, d_model: int, n_heads: int, causal: bool = True,
430
+ pos_emb: nn.Module = None, dropout: float = 0.0, qk_norm: bool = False,
431
+ gate_input_dim: int = 12):
432
+ super().__init__()
433
+ assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
434
+ self.causal = causal
435
+ self.d_model = d_model
436
+ self.n_heads = n_heads
437
+ self.head_dim = d_model // n_heads
438
+ self.dropout_p = dropout
439
+ self.gate_input_dim = gate_input_dim
440
+
441
+ self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
442
+ self.out = nn.Linear(d_model, d_model, bias=False)
443
+ self.gate_proj = nn.Linear(gate_input_dim, n_heads, bias=False)
444
+
445
+ self.pos_emb = pos_emb
446
+ self.kv_cache = None
447
+ self.qk_norm = qk_norm
448
+ if qk_norm:
449
+ self.q_norm = RMSNorm(self.head_dim)
450
+ self.k_norm = RMSNorm(self.head_dim)
451
+
452
+ self._init_weights()
453
+
454
+ def _init_weights(self):
455
+ # Identity-at-init: qkv orthogonal, out → 0, gate_proj orthogonal
456
+ # (out=0 ⇒ contributo attention nullo all'init, indipendentemente dal gate).
457
+ nn.init.orthogonal_(self.qkv.weight)
458
+ nn.init.zeros_(self.out.weight)
459
+ nn.init.orthogonal_(self.gate_proj.weight)
460
+
461
+ _reset_parameters = _init_weights
462
+
463
+ def _apply_gate(self, attn: torch.Tensor, x_gate_in: torch.Tensor) -> torch.Tensor:
464
+ # attn: [B, H, L, D_h] | x_gate_in: [B, L, gate_input_dim]
465
+ gate = torch.sigmoid(self.gate_proj(x_gate_in)) # [B, L, H]
466
+ return attn * gate.transpose(1, 2).unsqueeze(-1) # broadcast su D_h
467
+
468
+ def forward(self, x: torch.Tensor, ve=None) -> torch.Tensor:
469
+ B, L, D = x.shape
470
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
471
+ q, k, v = qkv.unbind(dim=2)
472
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
473
+
474
+ if self.qk_norm:
475
+ q = self.q_norm(q)
476
+ k = self.k_norm(k)
477
+ if self.pos_emb is not None:
478
+ q, k = self.pos_emb(q, k, start_pos=0)
479
+ if ve is not None:
480
+ v = v + ve.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
481
+
482
+ attn = F.scaled_dot_product_attention(
483
+ q, k, v, is_causal=self.causal,
484
+ dropout_p=self.dropout_p if self.training else 0.0,
485
+ )
486
+ attn = self._apply_gate(attn, x[..., :self.gate_input_dim])
487
+ return self.out(attn.transpose(1, 2).reshape(B, L, D))
488
+
489
+ def generate(self, x: torch.Tensor, ve=None) -> torch.Tensor:
490
+ B, L, D = x.shape
491
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
492
+ q, k, v = qkv.unbind(dim=2)
493
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
494
+
495
+ if self.qk_norm:
496
+ q = self.q_norm(q)
497
+ k = self.k_norm(k)
498
+
499
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
500
+ if self.pos_emb is not None:
501
+ q, k = self.pos_emb(q, k, start_pos=pos_offset)
502
+ if ve is not None:
503
+ v = v + ve.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
504
+
505
+ if self.kv_cache is not None:
506
+ k_cache, v_cache = self.kv_cache
507
+ k = torch.cat([k_cache, k], dim=2)
508
+ v = torch.cat([v_cache, v], dim=2)
509
+ self.kv_cache = (k, v)
510
+
511
+ attn = F.scaled_dot_product_attention(
512
+ q, k, v, is_causal=self.causal and (L > 1), dropout_p=0.0,
513
+ )
514
+ attn = self._apply_gate(attn, x[..., :self.gate_input_dim])
515
+ return self.out(attn.transpose(1, 2).reshape(B, L, D))
516
+
517
+ def clear_cache(self):
518
+ self.kv_cache = None
519
+
520
+
521
+ class MultiheadAttentionMixerALIBI(Layer):
522
+ __name__ = "MultiheadAttentionMixer"
523
+ __complexity__ = "O(L^2 d + L d^2)"
524
+
525
+ def __init__(self, d_model: int, n_heads: int, causal: bool = True,
526
+ pos_emb: nn.Module = None, alibi: nn.Module = None, dropout: float = 0.0, qk_norm=False):
527
+ super().__init__()
528
+ assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
529
+ self.causal = causal
530
+ self.d_model = d_model
531
+ self.n_heads = n_heads
532
+ self.head_dim = d_model // n_heads
533
+ self.dropout_p = dropout
534
+
535
+ self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
536
+ self.out = nn.Linear(d_model, d_model, bias=False)
537
+
538
+ self.pos_emb = pos_emb
539
+ self.alibi = alibi # ADDED: ALiBi module injection
540
+ self.kv_cache = None
541
+ self.qk_norm = qk_norm
542
+
543
+ if self.qk_norm:
544
+ self.q_norm = RMSNorm(self.head_dim)
545
+ self.k_norm = RMSNorm(self.head_dim)
546
+
547
+ self._reset_parameters()
548
+
549
+ def _reset_parameters(self):
550
+ nn.init.xavier_uniform_(self.qkv.weight)
551
+ nn.init.xavier_uniform_(self.out.weight)
552
+
553
+ def forward(self, x: torch.Tensor, ve=None) -> torch.Tensor:
554
+ B, L, D = x.shape
555
+
556
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
557
+ q, k, v = qkv.unbind(dim=2)
558
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
559
+
560
+ if self.qk_norm:
561
+ q = self.q_norm(q)
562
+ k = self.k_norm(k)
563
+
564
+ if self.pos_emb is not None:
565
+ q, k = self.pos_emb(q, k, start_pos=0)
566
+
567
+ if ve is not None:
568
+ v = v + ve.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
569
+
570
+ # ADDED: ALiBi Mask Logic
571
+ attn_mask = None
572
+ is_causal = self.causal
573
+ if self.alibi is not None:
574
+ attn_mask = self.alibi(q_len=L, k_len=L, start_pos=0, device=x.device, dtype=q.dtype, causal=self.causal)
575
+ is_causal = False # PyTorch requires is_causal=False if a custom attn_mask is provided
576
+
577
+ attn = F.scaled_dot_product_attention(
578
+ q, k, v,
579
+ attn_mask=attn_mask,
580
+ is_causal=is_causal,
581
+ dropout_p=self.dropout_p if self.training else 0.0,
582
+ )
583
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
584
+
585
+ def show_weights_forward(self, x):
586
+ B, L, D = x.shape
587
+
588
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
589
+ q, k, v = qkv.unbind(dim=2)
590
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
591
+
592
+ if self.qk_norm:
593
+ q = self.q_norm(q)
594
+ k = self.k_norm(k)
595
+
596
+ if self.pos_emb is not None:
597
+ q, k = self.pos_emb(q, k, start_pos=0)
598
+
599
+ scale = 1.0 / math.sqrt(self.head_dim)
600
+ scores = torch.matmul(q, k.transpose(-2, -1)) * scale
601
+
602
+ # ADDED: ALiBi logic replaces strict causal mask if active
603
+ if self.alibi is not None:
604
+ alibi_bias = self.alibi(q_len=L, k_len=L, start_pos=0, device=x.device, dtype=q.dtype, causal=self.causal)
605
+ scores = scores + alibi_bias
606
+ elif self.causal:
607
+ mask = torch.triu(torch.ones(L, L, dtype=torch.bool, device=x.device), diagonal=1)
608
+ scores.masked_fill_(mask, float('-inf'))
609
+
610
+ attn_weights = F.softmax(scores, dim=-1)
611
+ attn = torch.matmul(attn_weights, v)
612
+
613
+ out = self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
614
+ return out, attn_weights
615
+
616
+ def generate(self, x: torch.Tensor) -> torch.Tensor:
617
+ B, L, D = x.shape
618
+
619
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
620
+ q, k, v = qkv.unbind(dim=2)
621
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
622
+
623
+ if self.qk_norm:
624
+ q = self.q_norm(q)
625
+ k = self.k_norm(k)
626
+
627
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
628
+
629
+ if self.pos_emb is not None:
630
+ q, k = self.pos_emb(q, k, start_pos=pos_offset)
631
+
632
+ if self.kv_cache is not None:
633
+ k_cache, v_cache = self.kv_cache
634
+ k = torch.cat([k_cache, k], dim=2)
635
+ v = torch.cat([v_cache, v], dim=2)
636
+
637
+ self.kv_cache = (k, v)
638
+ k_len = k.shape[2]
639
+
640
+ # ADDED: ALiBi Mask Logic for Generation
641
+ attn_mask = None
642
+ is_causal = self.causal and (L > 1)
643
+ if self.alibi is not None:
644
+ attn_mask = self.alibi(q_len=L, k_len=k_len, start_pos=pos_offset, device=x.device, dtype=q.dtype,
645
+ causal=self.causal)
646
+ is_causal = False
647
+
648
+ attn = F.scaled_dot_product_attention(
649
+ q, k, v,
650
+ attn_mask=attn_mask,
651
+ is_causal=is_causal,
652
+ dropout_p=0.0,
653
+ )
654
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
655
+
656
+ # [clear_cache and finetune remain unchanged]
657
+
658
+
659
+ class MultiheadAttentionMixerXSA(Layer):
660
+ __name__ = "MultiheadAttentionMixerXSA"
661
+ __complexity__ = "O(L^2 d + L d^2)"
662
+
663
+ def __init__(self, d_model: int, n_heads: int, causal: bool = True,
664
+ pos_emb: nn.Module = None, dropout: float = 0.0, qk_norm=False):
665
+ super().__init__()
666
+ assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
667
+ self.causal = causal
668
+ self.d_model = d_model
669
+ self.n_heads = n_heads
670
+ self.head_dim = d_model // n_heads
671
+ self.dropout_p = dropout
672
+
673
+ self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
674
+ self.out = nn.Linear(d_model, d_model, bias=False)
675
+
676
+ self.pos_emb = pos_emb
677
+ self.kv_cache = None
678
+ self.qk_norm = qk_norm
679
+ if self.qk_norm:
680
+ self.q_norm = RMSNorm(self.head_dim)
681
+ self.k_norm = RMSNorm(self.head_dim)
682
+
683
+ self._reset_parameters()
684
+
685
+ def _reset_parameters(self):
686
+ nn.init.xavier_uniform_(self.qkv.weight)
687
+ nn.init.xavier_uniform_(self.out.weight)
688
+
689
+ def forward(self, x: torch.Tensor, ve=None) -> torch.Tensor:
690
+ """
691
+ ve: Stands for value embeddings, which should be already weighted by the caller, of course the dimension must match x
692
+ """
693
+ B, L, D = x.shape
694
+
695
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
696
+ q, k, v = qkv.unbind(dim=2)
697
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
698
+
699
+ if self.qk_norm:
700
+ q = self.q_norm(q)
701
+ k = self.k_norm(k)
702
+
703
+ if self.pos_emb is not None:
704
+ q, k = self.pos_emb(q, k, start_pos=0)
705
+ if ve is not None:
706
+ v = v + ve
707
+
708
+ attn = F.scaled_dot_product_attention(
709
+ q, k, v,
710
+ is_causal=self.causal,
711
+ dropout_p=self.dropout_p if self.training else 0.0,
712
+ )
713
+
714
+ v_norm = F.normalize(v, dim=-1)
715
+ attn = attn - (attn * v_norm).sum(dim=-1, keepdim=True) * v_norm
716
+
717
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
718
+
719
+ def show_weights_forward(self, x):
720
+ B, L, D = x.shape
721
+
722
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
723
+ q, k, v = qkv.unbind(dim=2)
724
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
725
+
726
+ if self.qk_norm:
727
+ q = self.q_norm(q)
728
+ k = self.k_norm(k)
729
+
730
+ if self.pos_emb is not None:
731
+ q, k = self.pos_emb(q, k, start_pos=0)
732
+
733
+ scale = 1.0 / math.sqrt(self.head_dim)
734
+ scores = torch.matmul(q, k.transpose(-2, -1)) * scale
735
+
736
+ if self.causal:
737
+ mask = torch.triu(torch.ones(L, L, dtype=torch.bool, device=x.device), diagonal=1)
738
+ scores.masked_fill_(mask, float('-inf'))
739
+
740
+ attn_weights = F.softmax(scores, dim=-1)
741
+ attn = torch.matmul(attn_weights, v)
742
+
743
+ v_norm = F.normalize(v, dim=-1)
744
+ attn = attn - (attn * v_norm).sum(dim=-1, keepdim=True) * v_norm
745
+
746
+ out = self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
747
+ return out, attn_weights
748
+
749
+ def generate(self, x: torch.Tensor) -> torch.Tensor:
750
+ B, L, D = x.shape
751
+
752
+ qkv = self.qkv(x).view(B, L, 3, self.n_heads, self.head_dim)
753
+ q, k, v = qkv.unbind(dim=2)
754
+ q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
755
+
756
+ if self.qk_norm:
757
+ q = self.q_norm(q)
758
+ k = self.k_norm(k)
759
+
760
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
761
+
762
+ if self.pos_emb is not None:
763
+ q, k = self.pos_emb(q, k, start_pos=pos_offset)
764
+
765
+ v_self = v
766
+
767
+ if self.kv_cache is not None:
768
+ k_cache, v_cache = self.kv_cache
769
+ k = torch.cat([k_cache, k], dim=2)
770
+ v = torch.cat([v_cache, v], dim=2)
771
+
772
+ self.kv_cache = (k, v)
773
+
774
+ attn = F.scaled_dot_product_attention(
775
+ q, k, v,
776
+ is_causal=self.causal and (L > 1),
777
+ dropout_p=0.0,
778
+ )
779
+
780
+ v_norm = F.normalize(v_self, dim=-1)
781
+ attn = attn - (attn * v_norm).sum(dim=-1, keepdim=True) * v_norm
782
+
783
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
784
+
785
+ def clear_cache(self):
786
+ self.kv_cache = None
787
+
788
+ def finetune(self):
789
+ self.qkv.weight.requires_grad = False
790
+ self.out.weight.requires_grad = False
791
+
792
+
793
+ class MultiheadAttentionMixerNOV(Layer):
794
+ __name__ = "MultiheadAttentionMixerNOV"
795
+ __complexity__ = "O(L^2 d + L d^2)"
796
+
797
+ def __init__(self, d_model: int, n_heads: int, causal: bool = True,
798
+ pos_emb: nn.Module = None, dropout: float = 0.0, qk_norm=False):
799
+ super().__init__()
800
+ assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
801
+ self.causal = causal
802
+ self.d_model = d_model
803
+ self.n_heads = n_heads
804
+ self.head_dim = d_model // n_heads
805
+ self.dropout_p = dropout
806
+
807
+ self.qk = nn.Linear(d_model, 2 * d_model, bias=False)
808
+ self.out = nn.Linear(d_model, d_model, bias=False)
809
+
810
+ self.pos_emb = pos_emb
811
+ self.kv_cache = None
812
+
813
+ self.qk_norm = qk_norm
814
+ if self.qk_norm:
815
+ self.q_norm = RMSNorm(self.head_dim)
816
+ self.k_norm = RMSNorm(self.head_dim)
817
+
818
+ self._reset_parameters()
819
+
820
+ def _reset_parameters(self):
821
+ nn.init.xavier_uniform_(self.qk.weight)
822
+ nn.init.xavier_uniform_(self.out.weight)
823
+
824
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
825
+ B, L, D = x.shape
826
+
827
+ qk = self.qk(x).view(B, L, 2, self.n_heads, self.head_dim)
828
+ q, k = qk.unbind(dim=2)
829
+ q, k = q.transpose(1, 2), k.transpose(1, 2)
830
+
831
+ if self.qk_norm:
832
+ q = self.q_norm(q)
833
+ k = self.k_norm(k)
834
+
835
+ v = x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
836
+
837
+ if self.pos_emb is not None:
838
+ q, k = self.pos_emb(q, k, start_pos=0)
839
+
840
+ attn = F.scaled_dot_product_attention(
841
+ q, k, v,
842
+ is_causal=self.causal,
843
+ dropout_p=self.dropout_p if self.training else 0.0,
844
+ )
845
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
846
+
847
+ def generate(self, x: torch.Tensor) -> torch.Tensor:
848
+ B, L, D = x.shape
849
+
850
+ qk = self.qk(x).view(B, L, 2, self.n_heads, self.head_dim)
851
+ q, k = qk.unbind(dim=2)
852
+ q, k = q.transpose(1, 2), k.transpose(1, 2)
853
+
854
+ if self.qk_norm:
855
+ q = self.q_norm(q)
856
+ k = self.k_norm(k)
857
+
858
+ v = x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
859
+
860
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
861
+
862
+ if self.pos_emb is not None:
863
+ q, k = self.pos_emb(q, k, start_pos=pos_offset)
864
+
865
+ if self.kv_cache is not None:
866
+ k_cache, v_cache = self.kv_cache
867
+ k = torch.cat([k_cache, k], dim=2)
868
+ v = torch.cat([v_cache, v], dim=2)
869
+
870
+ self.kv_cache = (k, v)
871
+
872
+ attn = F.scaled_dot_product_attention(
873
+ q, k, v,
874
+ is_causal=self.causal and (L > 1),
875
+ dropout_p=0.0,
876
+ )
877
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
878
+
879
+ def clear_cache(self):
880
+ self.kv_cache = None
881
+
882
+ def finetune(self):
883
+ self.qk.weight.requires_grad = False
884
+ self.out.weight.requires_grad = False
885
+
886
+
887
+ class MultiheadAttentionMixerNOVLa2(Layer):
888
+ __name__ = "MultiheadAttentionMixerNOV"
889
+ __complexity__ = "O(L^2 d + L d^2)"
890
+
891
+ def __init__(self, d_model: int, n_heads: int, causal: bool = True,
892
+ pos_emb: nn.Module = None, dropout: float = 0.0, qk_norm=False):
893
+ super().__init__()
894
+ assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
895
+ self.causal = causal
896
+ self.d_model = d_model
897
+ self.n_heads = n_heads
898
+ self.head_dim = d_model // n_heads
899
+ self.dropout_p = dropout
900
+
901
+ self.qk = nn.Linear(d_model, 2 * d_model, bias=False)
902
+ self.sec = nn.Linear(d_model, d_model, bias=False)
903
+ self.out = nn.Linear(d_model, d_model, bias=False)
904
+
905
+ self.pos_emb = pos_emb
906
+ self.kv_cache = None
907
+
908
+ self.qk_norm = qk_norm
909
+ if self.qk_norm:
910
+ self.q_norm = RMSNorm(self.head_dim)
911
+ self.k_norm = RMSNorm(self.head_dim)
912
+
913
+ self._init_weights()
914
+
915
+ def _init_weights(self):
916
+ nn.init.xavier_uniform_(self.qk.weight)
917
+ nn.init.xavier_uniform_(self.out.weight)
918
+ nn.init.xavier_uniform_(self.sec.weight)
919
+
920
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
921
+ B, L, D = x.shape
922
+
923
+ qk = self.qk(self.sec(x)).view(B, L, 2, self.n_heads, self.head_dim)
924
+ q, k = qk.unbind(dim=2)
925
+ q, k = q.transpose(1, 2), k.transpose(1, 2)
926
+
927
+ if self.qk_norm:
928
+ q = self.q_norm(q)
929
+ k = self.k_norm(k)
930
+ v = x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
931
+
932
+ if self.pos_emb is not None:
933
+ q, k = self.pos_emb(q, k, start_pos=0)
934
+
935
+ attn = F.scaled_dot_product_attention(
936
+ q, k, v,
937
+ is_causal=self.causal,
938
+ dropout_p=self.dropout_p if self.training else 0.0,
939
+ )
940
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
941
+
942
+ def generate(self, x: torch.Tensor) -> torch.Tensor:
943
+ B, L, D = x.shape
944
+
945
+ qk = self.qk(x).view(B, L, 2, self.n_heads, self.head_dim)
946
+ q, k = qk.unbind(dim=2)
947
+ q, k = q.transpose(1, 2), k.transpose(1, 2)
948
+
949
+ if self.qk_norm:
950
+ q = self.q_norm(q)
951
+ k = self.k_norm(k)
952
+
953
+ v = x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
954
+
955
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
956
+
957
+ if self.pos_emb is not None:
958
+ q, k = self.pos_emb(q, k, start_pos=pos_offset)
959
+
960
+ if self.kv_cache is not None:
961
+ k_cache, v_cache = self.kv_cache
962
+ k = torch.cat([k_cache, k], dim=2)
963
+ v = torch.cat([v_cache, v], dim=2)
964
+
965
+ self.kv_cache = (k, v)
966
+
967
+ attn = F.scaled_dot_product_attention(
968
+ q, k, v,
969
+ is_causal=self.causal and (L > 1),
970
+ dropout_p=0.0,
971
+ )
972
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
973
+
974
+ def clear_cache(self):
975
+ self.kv_cache = None
976
+
977
+ def finetune(self):
978
+ self.qk.weight.requires_grad = False
979
+ self.out.weight.requires_grad = False
980
+
981
+
982
+ class MultiheadAttentionMixerNOVLa3(Layer):
983
+ __name__ = "MultiheadAttentionMixerNOV"
984
+ __complexity__ = "O(L^2 d + L d^2)"
985
+
986
+ def __init__(self, d_model: int, n_heads: int, causal: bool = True,
987
+ pos_emb: nn.Module = None, dropout: float = 0.0, qk_norm=False):
988
+ super().__init__()
989
+ assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
990
+ self.causal = causal
991
+ self.d_model = d_model
992
+ self.n_heads = n_heads
993
+ self.head_dim = d_model // n_heads
994
+ self.dropout_p = dropout
995
+
996
+ self.qk = DoubleLinear(d_model, 2 * d_model)
997
+ self.out = nn.Linear(d_model, d_model, bias=False)
998
+
999
+ self.pos_emb = pos_emb
1000
+ self.kv_cache = None
1001
+
1002
+ self.qk_norm = qk_norm
1003
+ if self.qk_norm:
1004
+ self.q_norm = RMSNorm(self.head_dim)
1005
+ self.k_norm = RMSNorm(self.head_dim)
1006
+
1007
+ self._init_weights()
1008
+
1009
+ def _init_weights(self):
1010
+ nn.init.xavier_uniform_(self.out.weight)
1011
+
1012
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1013
+ B, L, D = x.shape
1014
+
1015
+ qk = self.qk(x).view(B, L, 2, self.n_heads, self.head_dim)
1016
+ q, k = qk.unbind(dim=2)
1017
+ q, k = q.transpose(1, 2), k.transpose(1, 2)
1018
+
1019
+ if self.qk_norm:
1020
+ q = self.q_norm(q)
1021
+ k = self.k_norm(k)
1022
+
1023
+ v = x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
1024
+
1025
+ if self.pos_emb is not None:
1026
+ q, k = self.pos_emb(q, k, start_pos=0)
1027
+
1028
+ attn = F.scaled_dot_product_attention(
1029
+ q, k, v,
1030
+ is_causal=self.causal,
1031
+ dropout_p=self.dropout_p if self.training else 0.0,
1032
+ )
1033
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
1034
+
1035
+ def generate(self, x: torch.Tensor) -> torch.Tensor:
1036
+ B, L, D = x.shape
1037
+
1038
+ qk = self.qk(x).view(B, L, 2, self.n_heads, self.head_dim)
1039
+ q, k = qk.unbind(dim=2)
1040
+ q, k = q.transpose(1, 2), k.transpose(1, 2)
1041
+
1042
+ if self.qk_norm:
1043
+ q = self.q_norm(q)
1044
+ k = self.k_norm(k)
1045
+
1046
+ v = x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
1047
+
1048
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
1049
+
1050
+ if self.pos_emb is not None:
1051
+ q, k = self.pos_emb(q, k, start_pos=pos_offset)
1052
+
1053
+ if self.kv_cache is not None:
1054
+ k_cache, v_cache = self.kv_cache
1055
+ k = torch.cat([k_cache, k], dim=2)
1056
+ v = torch.cat([v_cache, v], dim=2)
1057
+
1058
+ self.kv_cache = (k, v)
1059
+
1060
+ attn = F.scaled_dot_product_attention(
1061
+ q, k, v,
1062
+ is_causal=self.causal and (L > 1),
1063
+ dropout_p=0.0,
1064
+ )
1065
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
1066
+
1067
+ def clear_cache(self):
1068
+ self.kv_cache = None
1069
+
1070
+ def finetune(self):
1071
+ self.qk.weight.requires_grad = False
1072
+ self.out.weight.requires_grad = False
1073
+
1074
+
1075
+ class MultiheadAttentionMixerNOVLa4(Layer):
1076
+ __name__ = "MultiheadAttentionMixerNOV"
1077
+ __complexity__ = "O(L^2 d + L d^2)"
1078
+
1079
+ def __init__(self, d_model: int, n_heads: int, causal: bool = True,
1080
+ pos_emb: nn.Module = None, dropout: float = 0.0, qk_norm=False):
1081
+ super().__init__()
1082
+ assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
1083
+ self.causal = causal
1084
+ self.d_model = d_model
1085
+ self.n_heads = n_heads
1086
+ self.head_dim = d_model // n_heads
1087
+ self.dropout_p = dropout
1088
+
1089
+ self.qk = DoubleLinear(d_model, 2 * d_model)
1090
+ self.out = nn.Linear(d_model, d_model, bias=False)
1091
+
1092
+ self.pos_emb = pos_emb
1093
+ self.kv_cache = None
1094
+
1095
+ self.qk_norm = qk_norm
1096
+ if self.qk_norm:
1097
+ self.q_norm = RMSNorm(self.head_dim)
1098
+ self.k_norm = RMSNorm(self.head_dim)
1099
+
1100
+ self._reset_parameters()
1101
+
1102
+ def _reset_parameters(self):
1103
+ nn.init.xavier_uniform_(self.out.weight)
1104
+
1105
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1106
+ B, L, D = x.shape
1107
+
1108
+ qk = self.qk(x).view(B, L, 2, self.n_heads, self.head_dim)
1109
+ q, k = qk.unbind(dim=2)
1110
+ q, k = q.transpose(1, 2), k.transpose(1, 2)
1111
+
1112
+ if self.qk_norm:
1113
+ q = self.q_norm(q)
1114
+ k = self.k_norm(k)
1115
+
1116
+ v = x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
1117
+
1118
+ if self.pos_emb is not None:
1119
+ q, k = self.pos_emb(q, k, start_pos=0)
1120
+
1121
+ attn = F.scaled_dot_product_attention(
1122
+ q, k, v,
1123
+ is_causal=self.causal,
1124
+ dropout_p=self.dropout_p if self.training else 0.0,
1125
+ )
1126
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
1127
+
1128
+ def generate(self, x: torch.Tensor) -> torch.Tensor:
1129
+ B, L, D = x.shape
1130
+
1131
+ qk = self.qk(x).view(B, L, 2, self.n_heads, self.head_dim)
1132
+ q, k = qk.unbind(dim=2)
1133
+ q, k = q.transpose(1, 2), k.transpose(1, 2)
1134
+
1135
+ if self.qk_norm:
1136
+ q = self.q_norm(q)
1137
+ k = self.k_norm(k)
1138
+
1139
+ v = x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
1140
+
1141
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
1142
+
1143
+ if self.pos_emb is not None:
1144
+ q, k = self.pos_emb(q, k, start_pos=pos_offset)
1145
+
1146
+ if self.kv_cache is not None:
1147
+ k_cache, v_cache = self.kv_cache
1148
+ k = torch.cat([k_cache, k], dim=2)
1149
+ v = torch.cat([v_cache, v], dim=2)
1150
+
1151
+ self.kv_cache = (k, v)
1152
+
1153
+ attn = F.scaled_dot_product_attention(
1154
+ q, k, v,
1155
+ is_causal=self.causal and (L > 1),
1156
+ dropout_p=0.0,
1157
+ )
1158
+ return self.out(attn.transpose(1, 2).contiguous().view(B, L, D))
1159
+
1160
+ def clear_cache(self):
1161
+ self.kv_cache = None
1162
+
1163
+ def finetune(self):
1164
+ self.qk.weight.requires_grad = False
1165
+ self.out.weight.requires_grad = False
1166
+
1167
+
1168
+ class GQAGatedMixer(Layer):
1169
+ """GQA + sparse per-head output gate + VE + KV cache.
1170
+
1171
+ Combina:
1172
+ - GQA: n_kv_heads << n_heads. KV cache compatto (n_kv_heads heads).
1173
+ - Sparse output gate: sigmoid(gate_proj(x[..., :gate_input_dim])) per-head,
1174
+ broadcast su head_dim (Modded-NanoGPT PR #117 stile).
1175
+ - Value embeddings (ve): aggiunti a v_expanded (full n_heads). Quando ve
1176
+ è attivo la cache memorizza v in formato **expanded** (n_heads, costa
1177
+ n_rep× memoria) per preservare per-step ve correttamente. Quando ve è
1178
+ None, cache in formato compresso standard (n_kv_heads).
1179
+
1180
+ Identity-at-init: q_proj/kv_proj/gate_proj orthogonal, o_proj → 0.
1181
+ """
1182
+ __name__ = "GQAGatedMixer"
1183
+ __complexity__ = "O(L^2 d + L d^2)"
1184
+
1185
+ def __init__(self, d_model: int, n_heads: int, n_kv_heads: int,
1186
+ causal: bool = True, pos_emb: nn.Module = None,
1187
+ dropout: float = 0.0, qk_norm: bool = False,
1188
+ gate_input_dim: int = 12):
1189
+ super().__init__()
1190
+ if d_model % n_heads != 0:
1191
+ raise ValueError(f"d_model ({d_model}) must be divisible by n_heads ({n_heads})")
1192
+ if n_heads % n_kv_heads != 0:
1193
+ raise ValueError(f"n_heads ({n_heads}) must be divisible by n_kv_heads ({n_kv_heads})")
1194
+
1195
+ self.d_model = d_model
1196
+ self.n_heads = n_heads
1197
+ self.n_kv_heads = n_kv_heads
1198
+ self.head_dim = d_model // n_heads
1199
+ self.n_rep = n_heads // n_kv_heads
1200
+ self.causal = causal
1201
+ self.dropout_p = dropout
1202
+ self.gate_input_dim = gate_input_dim
1203
+
1204
+ self.q_proj = nn.Linear(d_model, d_model, bias=False)
1205
+ self.kv_proj = nn.Linear(d_model, n_kv_heads * self.head_dim * 2, bias=False)
1206
+ self.o_proj = nn.Linear(d_model, d_model, bias=False)
1207
+ self.gate_proj = nn.Linear(gate_input_dim, n_heads, bias=False)
1208
+
1209
+ self.pos_emb = pos_emb
1210
+ self.kv_cache = None
1211
+
1212
+ self.qk_norm = qk_norm
1213
+ if qk_norm:
1214
+ self.q_norm = RMSNorm(self.head_dim)
1215
+ self.k_norm = RMSNorm(self.head_dim)
1216
+
1217
+ self._init_weights()
1218
+
1219
+ def _init_weights(self):
1220
+ # Identity-at-init
1221
+ nn.init.orthogonal_(self.q_proj.weight)
1222
+ nn.init.orthogonal_(self.kv_proj.weight)
1223
+ nn.init.zeros_(self.o_proj.weight)
1224
+ nn.init.orthogonal_(self.gate_proj.weight)
1225
+
1226
+ _reset_parameters = _init_weights
1227
+
1228
+ def _expand_kv(self, x: torch.Tensor) -> torch.Tensor:
1229
+ if self.n_rep == 1:
1230
+ return x
1231
+ B, H, L, D = x.shape
1232
+ return (x.unsqueeze(2)
1233
+ .expand(B, H, self.n_rep, L, D)
1234
+ .reshape(B, H * self.n_rep, L, D))
1235
+
1236
+ def _apply_gate(self, attn: torch.Tensor, x_gate_in: torch.Tensor) -> torch.Tensor:
1237
+ # attn: [B, n_heads, L, head_dim] | x_gate_in: [B, L, gate_input_dim]
1238
+ gate = torch.sigmoid(self.gate_proj(x_gate_in)) # [B, L, H]
1239
+ return attn * gate.transpose(1, 2).unsqueeze(-1)
1240
+
1241
+ def forward(self, x: torch.Tensor, ve: torch.Tensor = None) -> torch.Tensor:
1242
+ B, T, D = x.shape
1243
+
1244
+ q = self.q_proj(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
1245
+ kv = self.kv_proj(x).view(B, T, self.n_kv_heads, 2, self.head_dim)
1246
+ k, v = kv.unbind(dim=3)
1247
+ k, v = k.transpose(1, 2), v.transpose(1, 2)
1248
+
1249
+ if self.qk_norm:
1250
+ q = self.q_norm(q)
1251
+ k = self.k_norm(k)
1252
+ if self.pos_emb is not None:
1253
+ q, k = self.pos_emb(q, k, start_pos=0)
1254
+
1255
+ k_exp = self._expand_kv(k)
1256
+ v_exp = self._expand_kv(v)
1257
+ if ve is not None:
1258
+ v_exp = v_exp + ve.view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
1259
+
1260
+ attn = F.scaled_dot_product_attention(
1261
+ q, k_exp, v_exp, is_causal=self.causal,
1262
+ dropout_p=self.dropout_p if self.training else 0.0,
1263
+ )
1264
+ attn = self._apply_gate(attn, x[..., :self.gate_input_dim])
1265
+ return self.o_proj(attn.transpose(1, 2).reshape(B, T, D))
1266
+
1267
+ def generate(self, x: torch.Tensor, ve: torch.Tensor = None) -> torch.Tensor:
1268
+ B, T, D = x.shape
1269
+
1270
+ q = self.q_proj(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
1271
+ kv = self.kv_proj(x).view(B, T, self.n_kv_heads, 2, self.head_dim)
1272
+ k_new, v_new = kv.unbind(dim=3)
1273
+ k_new, v_new = k_new.transpose(1, 2), v_new.transpose(1, 2)
1274
+
1275
+ if self.qk_norm:
1276
+ q = self.q_norm(q)
1277
+ k_new = self.k_norm(k_new)
1278
+
1279
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
1280
+ if self.pos_emb is not None:
1281
+ q, k_new = self.pos_emb(q, k_new, start_pos=pos_offset)
1282
+
1283
+ # Cache k sempre in formato raw (n_kv_heads). Cache v:
1284
+ # - se ve attivo: formato EXPANDED (n_heads), perché ve si applica per-step
1285
+ # - altrimenti: raw (n_kv_heads), standard GQA
1286
+ if ve is not None:
1287
+ v_new = self._expand_kv(v_new) + ve.view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
1288
+
1289
+ if self.kv_cache is not None:
1290
+ k_cache, v_cache = self.kv_cache
1291
+ k_new = torch.cat([k_cache, k_new], dim=2)
1292
+ v_new = torch.cat([v_cache, v_new], dim=2)
1293
+ self.kv_cache = (k_new, v_new)
1294
+
1295
+ # Attention: k_new sempre da espandere; v_new già expanded se ve, raw altrimenti
1296
+ k_for_attn = self._expand_kv(k_new)
1297
+ v_for_attn = v_new if ve is not None else self._expand_kv(v_new)
1298
+
1299
+ attn = F.scaled_dot_product_attention(
1300
+ q, k_for_attn, v_for_attn,
1301
+ is_causal=self.causal and (T > 1), dropout_p=0.0,
1302
+ )
1303
+ attn = self._apply_gate(attn, x[..., :self.gate_input_dim])
1304
+ return self.o_proj(attn.transpose(1, 2).reshape(B, T, D))
1305
+
1306
+ def clear_cache(self):
1307
+ self.kv_cache = None
1308
+
1309
+
1310
+ class GroupedQueryAttention(Layer):
1311
+ __name__ = "GroupedQueryAttention"
1312
+ __complexity__ = "O(L^2 d + L d^2)"
1313
+
1314
+ def __init__(self, d_model: int, n_heads: int, n_kv_heads: int,
1315
+ causal: bool = True, pos_emb: nn.Module = None,
1316
+ dropout: float = 0.0, bias: bool = False, qk_norm=False):
1317
+ super().__init__()
1318
+ if d_model % n_heads != 0:
1319
+ raise ValueError(f"d_model ({d_model}) must be divisible by n_heads ({n_heads})")
1320
+ if n_heads % n_kv_heads != 0:
1321
+ raise ValueError(f"n_heads ({n_heads}) must be divisible by n_kv_heads ({n_kv_heads})")
1322
+
1323
+ self.d_model = d_model
1324
+ self.n_heads = n_heads
1325
+ self.n_kv_heads = n_kv_heads
1326
+ self.head_dim = d_model // n_heads
1327
+ self.n_rep = n_heads // n_kv_heads
1328
+ self.causal = causal
1329
+ self.dropout_p = dropout
1330
+
1331
+ self.q_proj = nn.Linear(d_model, d_model, bias=bias)
1332
+ self.kv_proj = nn.Linear(d_model, n_kv_heads * self.head_dim * 2, bias=bias)
1333
+ self.o_proj = nn.Linear(d_model, d_model, bias=bias)
1334
+
1335
+ self.qk_norm = qk_norm
1336
+ if self.qk_norm:
1337
+ self.q_norm = RMSNorm(self.head_dim)
1338
+ self.k_norm = RMSNorm(self.head_dim)
1339
+
1340
+ self.pos_emb = pos_emb
1341
+ self.kv_cache = None
1342
+
1343
+ self._reset_parameters()
1344
+
1345
+ def _reset_parameters(self):
1346
+ nn.init.xavier_uniform_(self.q_proj.weight)
1347
+ nn.init.xavier_uniform_(self.kv_proj.weight)
1348
+ nn.init.xavier_uniform_(self.o_proj.weight)
1349
+ if self.q_proj.bias is not None:
1350
+ nn.init.zeros_(self.q_proj.bias)
1351
+ nn.init.zeros_(self.kv_proj.bias)
1352
+ nn.init.zeros_(self.o_proj.bias)
1353
+
1354
+ def _expand_kv(self, x: torch.Tensor) -> torch.Tensor:
1355
+ if self.n_rep == 1:
1356
+ return x
1357
+ B, H, L, D = x.shape
1358
+ return (x.unsqueeze(2)
1359
+ .expand(B, H, self.n_rep, L, D)
1360
+ .reshape(B, H * self.n_rep, L, D))
1361
+
1362
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1363
+ B, T, C = x.shape
1364
+
1365
+ q = self.q_proj(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
1366
+
1367
+ kv = self.kv_proj(x).view(B, T, self.n_kv_heads, 2, self.head_dim)
1368
+ k, v = kv.unbind(dim=3)
1369
+ k, v = k.transpose(1, 2), v.transpose(1, 2)
1370
+
1371
+ if self.qk_norm:
1372
+ q = self.q_norm(q)
1373
+ k = self.k_norm(k)
1374
+
1375
+ if self.pos_emb is not None:
1376
+ q, k = self.pos_emb(q, k, start_pos=0)
1377
+
1378
+ k, v = self._expand_kv(k), self._expand_kv(v)
1379
+
1380
+ attn = F.scaled_dot_product_attention(
1381
+ q, k, v,
1382
+ is_causal=self.causal,
1383
+ dropout_p=self.dropout_p if self.training else 0.0,
1384
+ )
1385
+ return self.o_proj(attn.transpose(1, 2).contiguous().view(B, T, C))
1386
+
1387
+ def generate(self, x: torch.Tensor, ve=None) -> torch.Tensor:
1388
+ B, T, C = x.shape
1389
+
1390
+ q = self.q_proj(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
1391
+
1392
+ kv = self.kv_proj(x).view(B, T, self.n_kv_heads, 2, self.head_dim)
1393
+ k, v = kv.unbind(dim=3)
1394
+ k, v = k.transpose(1, 2), v.transpose(1, 2)
1395
+
1396
+ if self.qk_norm:
1397
+ q = self.q_norm(q)
1398
+ k = self.k_norm(k)
1399
+
1400
+ pos_offset = self.kv_cache[0].shape[2] if self.kv_cache is not None else 0
1401
+
1402
+ if self.pos_emb is not None:
1403
+ q, k = self.pos_emb(q, k, start_pos=pos_offset)
1404
+
1405
+ if ve is not None:
1406
+ raise NotImplementedError(
1407
+ "GroupedQueryAttention.forward does not support value embeddings; "
1408
+ "use MultiheadAttentionMixer if you need ve."
1409
+ )
1410
+
1411
+ if self.kv_cache is not None:
1412
+ k_cache, v_cache = self.kv_cache
1413
+ k = torch.cat([k_cache, k], dim=2)
1414
+ v = torch.cat([v_cache, v], dim=2)
1415
+
1416
+ self.kv_cache = (k, v)
1417
+
1418
+ k, v = self._expand_kv(k), self._expand_kv(v)
1419
+
1420
+ attn = F.scaled_dot_product_attention(
1421
+ q, k, v,
1422
+ is_causal=self.causal and (T > 1),
1423
+ dropout_p=0.0,
1424
+ )
1425
+ return self.o_proj(attn.transpose(1, 2).contiguous().view(B, T, C))
1426
+
1427
+ def clear_cache(self):
1428
+ self.kv_cache = None
1429
+
1430
+ def finetune(self):
1431
+ self.q_proj.weight.requires_grad = False
1432
+ self.kv_proj.weight.requires_grad = False
1433
+ self.o_proj.weight.requires_grad = False
1434
+
1435
+
1436
+ class GroupedQueryAttentionNOV(Layer):
1437
+
1438
+ def __init__(
1439
+ self,
1440
+ d_model: int,
1441
+ n_heads: int,
1442
+ n_kv_heads: int,
1443
+ dropout: float = 0.0,
1444
+ bias: bool = False,
1445
+ causal: bool = True,
1446
+ rope: bool = False,
1447
+ qk_norm: bool = False
1448
+ ):
1449
+ super().__init__()
1450
+
1451
+ self.d_model = d_model
1452
+ self.num_heads = n_heads
1453
+ self.num_kv_heads = n_kv_heads
1454
+ self.head_dim = d_model // n_heads
1455
+ self.dropout_prob = dropout
1456
+ self.causal = causal
1457
+
1458
+ self.n_rep = self.num_heads // self.num_kv_heads
1459
+
1460
+ if self.d_model % self.num_heads != 0:
1461
+ raise ValueError(f"embed_dim ({d_model}) must be divisible by num_heads ({n_heads})")
1462
+ if self.num_heads % self.num_kv_heads != 0:
1463
+ raise ValueError(f"num_heads ({n_heads}) must be divisible by num_kv_heads ({n_kv_heads})")
1464
+
1465
+ self.q_proj = nn.Linear(d_model, d_model, bias=bias)
1466
+ self.k_proj = nn.Linear(d_model, n_kv_heads * self.head_dim, bias=bias)
1467
+ self.o_proj = nn.Linear(d_model, d_model, bias=bias)
1468
+
1469
+ self.rope = RoPE(self.head_dim) if rope else None
1470
+
1471
+ self.qk_norm = qk_norm
1472
+ if self.qk_norm:
1473
+ self.q_norm = RMSNorm(self.head_dim)
1474
+ self.k_norm = RMSNorm(self.head_dim)
1475
+
1476
+ self._reset_parameters()
1477
+
1478
+ def _reset_parameters(self):
1479
+ nn.init.xavier_uniform_(self.q_proj.weight)
1480
+ nn.init.xavier_uniform_(self.k_proj.weight)
1481
+ nn.init.xavier_uniform_(self.o_proj.weight)
1482
+ if self.q_proj.bias is not None:
1483
+ nn.init.constant_(self.q_proj.bias, 0)
1484
+ nn.init.constant_(self.k_proj.bias, 0)
1485
+ nn.init.constant_(self.o_proj.bias, 0)
1486
+
1487
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1488
+ B, T, C = x.shape
1489
+
1490
+ q = self.q_proj(x)
1491
+ k = self.k_proj(x)
1492
+
1493
+ q = q.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
1494
+ k = k.view(B, T, self.num_kv_heads, self.head_dim).transpose(1, 2)
1495
+ v = x.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
1496
+
1497
+ if self.qk_norm:
1498
+ q = self.q_norm(q)
1499
+ k = self.k_norm(k)
1500
+
1501
+ if self.rope is not None:
1502
+ q, k = self.rope(q, k)
1503
+
1504
+ if self.n_rep > 1:
1505
+ k = k[:, :, None, :, :].expand(B, self.num_kv_heads, self.n_rep, T, self.head_dim)
1506
+ k = k.reshape(B, self.num_heads, T, self.head_dim)
1507
+
1508
+ attn_output = F.scaled_dot_product_attention(
1509
+ q, k, v,
1510
+ dropout_p=self.dropout_prob if self.training else 0.0,
1511
+ is_causal=self.causal
1512
+ )
1513
+
1514
+ attn_output = attn_output.transpose(1, 2).contiguous().view(B, T, C)
1515
+ return self.o_proj(attn_output)
1516
+
1517
+
1518
+ class GroupedQueryAttentionNOV2(Layer):
1519
+ def __init__(
1520
+ self,
1521
+ d_model: int,
1522
+ n_heads: int,
1523
+ n_kv_heads: int,
1524
+ dropout: float = 0.0,
1525
+ bias: bool = False,
1526
+ causal: bool = True,
1527
+ rope: bool = False
1528
+ ):
1529
+ super().__init__()
1530
+
1531
+ self.d_model = d_model
1532
+ self.num_heads = n_heads
1533
+ self.num_kv_heads = n_kv_heads
1534
+ self.head_dim = d_model // n_heads
1535
+ self.dropout_prob = dropout
1536
+ self.causal = causal
1537
+
1538
+ self.n_rep = self.num_heads // self.num_kv_heads
1539
+
1540
+ if self.d_model % self.num_heads != 0:
1541
+ raise ValueError(f"embed_dim ({d_model}) must be divisible by num_heads ({n_heads})")
1542
+ if self.num_heads % self.num_kv_heads != 0:
1543
+ raise ValueError(f"num_heads ({n_heads}) must be divisible by num_kv_heads ({n_kv_heads})")
1544
+
1545
+ self.q_proj = nn.Linear(d_model, d_model, bias=bias)
1546
+ self.k_proj = nn.Linear(d_model, n_kv_heads * self.head_dim, bias=bias)
1547
+ self.o_proj = nn.Linear(d_model, d_model, bias=bias)
1548
+ self.trainv = False
1549
+ self.v_proj = nn.Linear(d_model, d_model, bias=bias)
1550
+
1551
+ self.rope = RoPE(self.head_dim) if rope else None
1552
+
1553
+ self._reset_parameters()
1554
+
1555
+ def _reset_parameters(self):
1556
+ nn.init.xavier_uniform_(self.q_proj.weight)
1557
+ nn.init.xavier_uniform_(self.k_proj.weight)
1558
+ nn.init.xavier_uniform_(self.o_proj.weight)
1559
+ nn.init.eye_(self.v_proj.weight)
1560
+ if self.q_proj.bias is not None:
1561
+ nn.init.constant_(self.q_proj.bias, 0)
1562
+ nn.init.constant_(self.k_proj.bias, 0)
1563
+ nn.init.constant_(self.v_proj.bias, 0)
1564
+ nn.init.constant_(self.o_proj.bias, 0)
1565
+
1566
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1567
+ B, T, C = x.shape
1568
+
1569
+ q = self.q_proj(x)
1570
+ k = self.k_proj(x)
1571
+
1572
+ q = q.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
1573
+ k = k.view(B, T, self.num_kv_heads, self.head_dim).transpose(1, 2)
1574
+
1575
+ if not self.trainv:
1576
+ v = x.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
1577
+ else:
1578
+ v = self.v_proj(x).view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
1579
+
1580
+ # Apply RoPE BEFORE expansion
1581
+ if self.rope is not None:
1582
+ q, k = self.rope(q, k)
1583
+
1584
+ if self.n_rep > 1:
1585
+ k = k[:, :, None, :, :].expand(B, self.num_kv_heads, self.n_rep, T, self.head_dim)
1586
+ k = k.reshape(B, self.num_heads, T, self.head_dim)
1587
+
1588
+ attn_output = F.scaled_dot_product_attention(
1589
+ q, k, v,
1590
+ dropout_p=self.dropout_prob if self.training else 0.0,
1591
+ is_causal=self.causal
1592
+ )
1593
+
1594
+ attn_output = attn_output.transpose(1, 2).contiguous().view(B, T, C)
1595
+ return self.o_proj(attn_output)
1596
+
1597
+
1598
+ class GroupedQueryAttentionNOO(Layer):
1599
+ def __init__(
1600
+ self,
1601
+ d_model: int,
1602
+ n_heads: int,
1603
+ n_kv_heads: int,
1604
+ dropout: float = 0.0,
1605
+ bias: bool = False,
1606
+ causal: bool = True,
1607
+ rope: bool = False
1608
+ ):
1609
+ super().__init__()
1610
+
1611
+ self.d_model = d_model
1612
+ self.num_heads = n_heads
1613
+ self.num_kv_heads = n_kv_heads
1614
+ self.head_dim = d_model // n_heads
1615
+ self.dropout_prob = dropout
1616
+ self.causal = causal
1617
+
1618
+ self.n_rep = self.num_heads // self.num_kv_heads
1619
+
1620
+ if self.d_model % self.num_heads != 0:
1621
+ raise ValueError(f"embed_dim ({d_model}) must be divisible by num_heads ({n_heads})")
1622
+ if self.num_heads % self.num_kv_heads != 0:
1623
+ raise ValueError(f"num_heads ({n_heads}) must be divisible by num_kv_heads ({n_kv_heads})")
1624
+
1625
+ self.q_proj = nn.Linear(d_model, d_model, bias=bias)
1626
+ self.kv_proj = nn.Linear(d_model, n_kv_heads * self.head_dim * 2, bias=bias)
1627
+
1628
+ self.rope = RoPE(self.head_dim) if rope else None
1629
+
1630
+ self._reset_parameters()
1631
+
1632
+ def _reset_parameters(self):
1633
+ nn.init.xavier_uniform_(self.q_proj.weight)
1634
+ nn.init.xavier_uniform_(self.kv_proj.weight)
1635
+ if self.q_proj.bias is not None:
1636
+ nn.init.constant_(self.q_proj.bias, 0)
1637
+ nn.init.constant_(self.kv_proj.bias, 0)
1638
+
1639
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1640
+ B, T, C = x.shape
1641
+
1642
+ q = self.q_proj(x)
1643
+ kv = self.kv_proj(x)
1644
+
1645
+ q = q.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
1646
+
1647
+ kv = kv.view(B, T, self.num_kv_heads, 2, self.head_dim)
1648
+ k, v = kv.unbind(dim=3)
1649
+ k = k.transpose(1, 2)
1650
+ v = v.transpose(1, 2)
1651
+
1652
+ # Apply RoPE BEFORE expansion
1653
+ if self.rope is not None:
1654
+ q, k = self.rope(q, k)
1655
+
1656
+ if self.n_rep > 1:
1657
+ k = k[:, :, None, :, :].expand(B, self.num_kv_heads, self.n_rep, T, self.head_dim)
1658
+ v = v[:, :, None, :, :].expand(B, self.num_kv_heads, self.n_rep, T, self.head_dim)
1659
+ k = k.reshape(B, self.num_heads, T, self.head_dim)
1660
+ v = v.reshape(B, self.num_heads, T, self.head_dim)
1661
+
1662
+ attn_output = F.scaled_dot_product_attention(
1663
+ q, k, v,
1664
+ dropout_p=self.dropout_prob if self.training else 0.0,
1665
+ is_causal=self.causal
1666
+ )
1667
+
1668
+ attn_output = attn_output.transpose(1, 2).contiguous().view(B, T, C)
1669
+ return attn_output
1670
+
1671
+
1672
+ class MultiheadLatentAttentionMixer(Layer): # Changed Layer to nn.Module for standard torch
1673
+ __name__ = "MultiheadLatentAttentionMixer"
1674
+ # Adjusted complexity notation
1675
+ __complexity__ = "O(L^2 d + L d * d_kv_lora)"
1676
+
1677
+ def __init__(self, d_model: int, n_heads: int, d_kv_lora: int, causal: bool, rope=False, dropout=0.1):
1678
+ super().__init__()
1679
+ self.causal = causal
1680
+ self.d_model = d_model
1681
+ self.n_heads = n_heads
1682
+ self.head_dim = d_model // n_heads
1683
+ self.d_kv_lora = d_kv_lora
1684
+ self.rope_enabled = rope
1685
+
1686
+ self.q = nn.Linear(d_model, d_model, bias=False)
1687
+
1688
+ self.kv_compress = nn.Linear(d_model, d_kv_lora, bias=False)
1689
+ self.k_content_decompress = nn.Linear(d_kv_lora, d_model, bias=False)
1690
+ self.v_decompress = nn.Linear(d_kv_lora, d_model, bias=False)
1691
+
1692
+ self.out = nn.Linear(d_model, d_model, bias=False)
1693
+
1694
+ if self.rope_enabled:
1695
+ self.rope = RoPE(self.head_dim)
1696
+
1697
+ self.dropout = nn.Dropout(dropout)
1698
+
1699
+ nn.init.zeros_(self.out.weight)
1700
+
1701
+ def forward(self, x: torch.Tensor):
1702
+ B, L, D = x.shape
1703
+ H = self.n_heads
1704
+ HD = self.head_dim
1705
+
1706
+ q = self.q(x).view(B, L, H, HD).transpose(1, 2)
1707
+
1708
+ c_kv = self.kv_compress(x)
1709
+ k = self.k_content_decompress(c_kv).view(B, L, H, HD).transpose(1, 2)
1710
+ v = self.v_decompress(c_kv).view(B, L, H, HD).transpose(1, 2)
1711
+ if self.rope_enabled:
1712
+ q, k = self.rope(q, k)
1713
+
1714
+ attn = F.scaled_dot_product_attention(
1715
+ q, k, v,
1716
+ dropout_p=self.dropout.p if self.training else 0.0,
1717
+ is_causal=self.causal
1718
+ )
1719
+
1720
+ attn = attn.transpose(1, 2).contiguous().view(B, L, D)
1721
+
1722
+ return self.out(attn)
1723
+
1724
+
1725
+ class CausalMultiheadAttentionMixer2NOK(nn.Module):
1726
+ __name__ = "CausalMultiheadAttentionMixer"
1727
+
1728
+ def __init__(self, d_model: int, n_heads: int, causal=True, rope=False, dropout=0.0):
1729
+ super().__init__()
1730
+ self.d_model = d_model
1731
+ self.n_heads = n_heads
1732
+ self.head_dim = d_model // n_heads
1733
+ self.dropout_p = dropout
1734
+
1735
+ self.qv = nn.Linear(d_model, 2 * d_model, bias=False)
1736
+ self.out = nn.Linear(d_model, d_model, bias=False)
1737
+
1738
+ self.rope = RoPE(self.head_dim) if rope else None
1739
+ self._reset_parameters()
1740
+
1741
+ def _reset_parameters(self):
1742
+ nn.init.xavier_uniform_(self.qv.weight)
1743
+ nn.init.xavier_uniform_(self.out.weight)
1744
+
1745
+ def forward(self, x: torch.Tensor):
1746
+ B, L, D = x.shape
1747
+
1748
+ qv = self.qv(x)
1749
+
1750
+ qv = qv.view(B, L, 2, self.n_heads, self.head_dim)
1751
+
1752
+ q, v = qv.unbind(dim=2)
1753
+
1754
+ q = q.transpose(1, 2)
1755
+ k = x.view(B, L, self.n_heads, self.head_dim).transpose(1, 2)
1756
+ v = v.transpose(1, 2)
1757
+
1758
+ if self.rope is not None:
1759
+ q, k = self.rope(q, k)
1760
+
1761
+ q = q.contiguous()
1762
+ k = k.contiguous()
1763
+ v = v.contiguous()
1764
+
1765
+ attn = F.scaled_dot_product_attention(
1766
+ q, k, v,
1767
+ is_causal=True,
1768
+ dropout_p=self.dropout_p if self.training else 0.0
1769
+ )
1770
+
1771
+ attn = attn.transpose(1, 2).contiguous().view(B, L, D)
1772
+
1773
+ return self.out(attn)
1774
+
1775
+
1776
+ class MultiheadDecoupledAttention(Layer):
1777
+ __name__ = "DecoupledSelfAttention"
1778
+ __complexity__ = "O(L^2 * d_qk + L * d_model * (d_qk + d_v))"
1779
+
1780
+ def __init__(self, d_model: int, n_heads: int, qk_dim: int, causal: bool, rope=False, dropout=0.1):
1781
+ super().__init__()
1782
+ self.d_model = d_model
1783
+ self.n_heads = n_heads
1784
+ self.qk_dim = qk_dim
1785
+ self.v_dim = d_model // n_heads
1786
+ self.causal = causal
1787
+ self.rope_enabled = rope
1788
+
1789
+ self.qk_proj = nn.Linear(d_model, n_heads * 2 * qk_dim, bias=False)
1790
+
1791
+ self.v_proj = nn.Linear(d_model, n_heads * self.v_dim, bias=False)
1792
+
1793
+ self.out_proj = nn.Linear(n_heads * self.v_dim, d_model, bias=False)
1794
+
1795
+ self.dropout = nn.Dropout(dropout)
1796
+
1797
+ if self.rope_enabled:
1798
+ self.rope = RoPE(qk_dim)
1799
+
1800
+ nn.init.zeros_(self.out_proj.weight)
1801
+
1802
+ def forward(self, x: torch.Tensor):
1803
+ B, L, _ = x.shape
1804
+ H = self.n_heads
1805
+
1806
+ q, k = self.qk_proj(x).view(B, L, H, 2 * self.qk_dim).transpose(1, 2).chunk(2, -1)
1807
+
1808
+ v = self.v_proj(x).view(B, L, H, self.v_dim).transpose(1, 2)
1809
+
1810
+ if self.rope_enabled:
1811
+ q, k = self.rope(q, k)
1812
+
1813
+ attn_out = F.scaled_dot_product_attention(
1814
+ q, k, v,
1815
+ dropout_p=self.dropout.p if self.training else 0.0,
1816
+ is_causal=self.causal
1817
+ )
1818
+
1819
+ attn_out = attn_out.transpose(1, 2).contiguous().view(B, L, H * self.v_dim)
1820
+
1821
+ return self.out_proj(attn_out)
1822
+
1823
+
1824
+ class MultiheadDecoupledAttentionNOV(Layer):
1825
+ __name__ = "DecoupledSelfAttention"
1826
+ __complexity__ = "O(L^2 * d_qk + L * d_model * (d_qk + d_v))"
1827
+
1828
+ def __init__(self, d_model: int, n_heads: int, qk_dim: int, causal: bool, rope=False, dropout=0.1):
1829
+ super().__init__()
1830
+ self.d_model = d_model
1831
+ self.n_heads = n_heads
1832
+ self.qk_dim = qk_dim
1833
+ self.v_dim = d_model // n_heads
1834
+ self.causal = causal
1835
+ self.rope_enabled = rope
1836
+
1837
+ self.qk_proj = nn.Linear(d_model, n_heads * 2 * qk_dim, bias=False)
1838
+
1839
+ self.out_proj = nn.Linear(n_heads * self.v_dim, d_model, bias=False)
1840
+
1841
+ self.dropout = nn.Dropout(dropout)
1842
+
1843
+ if self.rope_enabled:
1844
+ self.rope = RoPE(qk_dim)
1845
+
1846
+ nn.init.zeros_(self.out_proj.weight)
1847
+
1848
+ def forward(self, x: torch.Tensor):
1849
+ B, L, _ = x.shape
1850
+ H = self.n_heads
1851
+
1852
+ q, k = self.qk_proj(x).view(B, L, H, 2 * self.qk_dim).transpose(1, 2).chunk(2, -1)
1853
+
1854
+ v = x.view(B, L, H, self.v_dim).transpose(1, 2)
1855
+
1856
+ if self.rope_enabled:
1857
+ q, k = self.rope(q, k)
1858
+
1859
+ attn_out = F.scaled_dot_product_attention(
1860
+ q, k, v,
1861
+ dropout_p=self.dropout.p if self.training else 0.0,
1862
+ is_causal=self.causal
1863
+ )
1864
+
1865
+ attn_out = attn_out.transpose(1, 2).contiguous().view(B, L, H * self.v_dim)
1866
+
1867
+ return self.out_proj(attn_out)
1868
+
1869
+
1870
+ class FFMultiheadAttentionMixerNOV(Layer):
1871
+ __name__ = "MultiheadAttentionMixer"
1872
+ __complexity__ = "O(L^2 d + L d^2)"
1873
+
1874
+ def __init__(self, d_model: int, n_heads: int, causal: bool, rope=False, dropout=0.05):
1875
+ super().__init__()
1876
+ self.causal = causal
1877
+ self.d_model = d_model
1878
+ self.n_heads = n_heads
1879
+ self.head_dim = d_model // n_heads
1880
+
1881
+ self.q = nn.Linear(d_model, d_model, bias=False)
1882
+ self.out = nn.Linear(d_model, d_model, bias=False)
1883
+ if rope:
1884
+ self.rope = RoPE(self.head_dim)
1885
+ else:
1886
+ self.rope = None
1887
+
1888
+ self.dropout = nn.Dropout(dropout)
1889
+
1890
+ self.kv_cache = None
1891
+
1892
+ def forward(self, x: torch.Tensor):
1893
+ B, L, D = x.shape
1894
+
1895
+ q = self.q(x).reshape(B, L, self.n_heads, self.head_dim)
1896
+ q = q.permute(0, 2, 1, 3)
1897
+ k = x.view(B, L, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
1898
+ v = x.view(B, L, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
1899
+
1900
+ if self.rope is not None:
1901
+ q, k = self.rope(q, k, start_pos=0)
1902
+
1903
+ attn = F.scaled_dot_product_attention(q, k, v, is_causal=self.causal)
1904
+ attn = attn.transpose(1, 2).reshape(B, L, D)
1905
+ attn = self.dropout(attn)
1906
+ return self.out(attn)
1907
+
1908
+ def generate(self, x: torch.Tensor):
1909
+ B, L, D = x.shape
1910
+
1911
+ qk = self.qk(x).reshape(B, L, 2, self.n_heads, self.head_dim)
1912
+ q, k = qk.permute(2, 0, 3, 1, 4)
1913
+ v = x.view(B, L, 1, self.n_heads, self.head_dim).permute(2, 0, 3, 1, 4)[0]
1914
+
1915
+ if self.kv_cache is not None:
1916
+ k_cache, v_cache = self.kv_cache
1917
+ pos_offset = k_cache.shape[2]
1918
+ else:
1919
+ pos_offset = 0
1920
+ k_cache, v_cache = None, None
1921
+
1922
+ if self.rope is not None:
1923
+ q, k = self.rope(q, k, start_pos=pos_offset)
1924
+
1925
+ if k_cache is not None:
1926
+ k = torch.cat([k_cache, k], dim=2)
1927
+ v = torch.cat([v_cache, v], dim=2)
1928
+
1929
+ self.kv_cache = (k, v)
1930
+ use_causal = self.causal and (L > 1)
1931
+
1932
+ attn = F.scaled_dot_product_attention(q, k, v, is_causal=use_causal)
1933
+
1934
+ attn = attn.transpose(1, 2).reshape(B, L, D)
1935
+ attn = self.dropout(attn)
1936
+ return self.out(attn)
1937
+
1938
+ def clear_cache(self):
1939
+ self.kv_cache = None
1940
+
1941
+ def finetune(self):
1942
+ self.qk.weight.data.requires_grad = False
1943
+ self.out.weight.data.requires_grad = False
1944
+
1945
+
1946
+ class FFFMultiheadAttentionMixer(Layer):
1947
+ __name__ = "MultiheadAttentionMixer"
1948
+ __complexity__ = "O(L^2 d + L d^2)"
1949
+
1950
+ def __init__(self, d_model: int, n_heads: int, causal: bool, rope=False, dropout=0.05):
1951
+ super().__init__()
1952
+ self.causal = causal
1953
+ self.d_model = d_model
1954
+ self.n_heads = n_heads
1955
+ self.head_dim = d_model // n_heads
1956
+
1957
+ self.q = LowRankLinear(d_model, d_model, rank=math.isqrt(d_model), bias=False)
1958
+ self.out = nn.Linear(d_model, d_model, bias=False)
1959
+ if rope:
1960
+ self.rope = RoPE(self.head_dim)
1961
+ else:
1962
+ self.rope = None
1963
+
1964
+ self.dropout = nn.Dropout(dropout)
1965
+
1966
+ self.kv_cache = None
1967
+
1968
+ def forward(self, x: torch.Tensor):
1969
+ B, L, D = x.shape
1970
+
1971
+ q = self.q(x).reshape(B, L, self.n_heads, self.head_dim)
1972
+ q = q.permute(0, 2, 1, 3)
1973
+ k = x.view(B, L, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
1974
+ v = x.view(B, L, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
1975
+
1976
+ if self.rope is not None:
1977
+ q, k = self.rope(q, k, start_pos=0)
1978
+
1979
+ attn = F.scaled_dot_product_attention(q, k, v, is_causal=self.causal)
1980
+ attn = attn.transpose(1, 2).reshape(B, L, D)
1981
+ attn = self.dropout(attn)
1982
+ return self.out(attn)
1983
+
1984
+ def generate(self, x: torch.Tensor):
1985
+ B, L, D = x.shape
1986
+
1987
+ qk = self.qk(x).reshape(B, L, 2, self.n_heads, self.head_dim)
1988
+ q, k = qk.permute(2, 0, 3, 1, 4)
1989
+ v = x.view(B, L, 1, self.n_heads, self.head_dim).permute(2, 0, 3, 1, 4)[0]
1990
+
1991
+ if self.kv_cache is not None:
1992
+ k_cache, v_cache = self.kv_cache
1993
+ pos_offset = k_cache.shape[2]
1994
+ else:
1995
+ pos_offset = 0
1996
+ k_cache, v_cache = None, None
1997
+
1998
+ if self.rope is not None:
1999
+ q, k = self.rope(q, k, start_pos=pos_offset)
2000
+
2001
+ if k_cache is not None:
2002
+ k = torch.cat([k_cache, k], dim=2)
2003
+ v = torch.cat([v_cache, v], dim=2)
2004
+
2005
+ self.kv_cache = (k, v)
2006
+ use_causal = self.causal and (L > 1)
2007
+
2008
+ attn = F.scaled_dot_product_attention(q, k, v, is_causal=use_causal)
2009
+
2010
+ attn = attn.transpose(1, 2).reshape(B, L, D)
2011
+ attn = self.dropout(attn)
2012
+ return self.out(attn)
2013
+
2014
+ def clear_cache(self):
2015
+ self.kv_cache = None
2016
+
2017
+ def finetune(self):
2018
+ self.qk.weight.data.requires_grad = False
2019
+ self.out.weight.data.requires_grad = False
2020
+
2021
+
2022
+ class NSAttention(Layer):
2023
+ __name__ = "MultiheadAttentionMixer"
2024
+ __complexity__ = "O(L^2 d + L d^2)"
2025
+
2026
+ def __init__(self, d_model: int, n_heads: int, causal: bool = True,
2027
+ pos_emb: nn.Module = None, dropout: float = 0.0, qk_norm=False):
2028
+ super().__init__()
2029
+ assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
2030
+ self.causal = causal
2031
+ self.d_model = d_model
2032
+ self.n_heads = n_heads
2033
+ self.head_dim = d_model // n_heads
2034
+ self.dropout_p = dropout
2035
+
2036
+ self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
2037
+
2038
+ self.pos_emb = pos_emb
2039
+ self.kv_cache = None
2040
+ self.qk_norm = qk_norm
2041
+ if self.qk_norm:
2042
+ self.q_norm = RMSNorm(self.head_dim)
2043
+ self.k_norm = RMSNorm(self.head_dim)
2044
+
2045
+ self._reset_parameters()
2046
+
2047
+ def _reset_parameters(self):
2048
+ nn.init.xavier_uniform_(self.qkv.weight)
2049
+ nn.init.xavier_uniform_(self.out.weight)
2050
+
2051
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
2052
+ B, L, D = x.shape
2053
+
2054
+ qkv = self.qkv(x)
2055
+
2056
+ q, k, v = qkv.chunk(3, dim=-1)
2057
+ kv = k.transpose(-1, -2) @ v
2058
+ return q @ kv.transpose(-1, -2)
2059
+
2060
+
2061
+ # MODDED NANO GPT
2062
+
2063
+ class CausalSelfAttention(nn.Module):
2064
+ def __init__(self, dim: int, num_heads: int, rope_base: float, qk_gain_init: float):
2065
+ super().__init__()
2066
+ if dim % num_heads != 0:
2067
+ raise ValueError("model_dim must be divisible by num_heads")
2068
+
2069
+ self.num_heads = num_heads
2070
+ self.head_dim = dim // num_heads
2071
+
2072
+ if self.head_dim % 2 != 0:
2073
+ raise ValueError("head_dim must be even for RoPE")
2074
+
2075
+ # FUSED QKV: Output dimension is 3 * dim
2076
+ self.c_qkv = CastedLinear(dim, 3 * dim, bias=False)
2077
+
2078
+ self.proj = CastedLinear(dim, dim, bias=False)
2079
+ self.proj._zero_init = True
2080
+
2081
+ self.q_gain = nn.Parameter(torch.full((num_heads,), qk_gain_init, dtype=torch.float32))
2082
+ self.rotary = Rotary(self.head_dim, base=rope_base)
2083
+
2084
+ def forward(self, x: Tensor) -> Tensor:
2085
+ bsz, seqlen, dim = x.shape
2086
+
2087
+ # 1. Single fused linear projection
2088
+ # Shape: (bsz, seqlen, 3 * dim)
2089
+ qkv = self.c_qkv(x)
2090
+
2091
+ # 2. Reshape and slice into Q, K, V
2092
+ # Reshape to: (bsz, seqlen, 3, num_heads, head_dim)
2093
+ qkv = qkv.reshape(bsz, seqlen, 3, self.num_heads, self.head_dim)
2094
+ # Permute to: (3, bsz, num_heads, seqlen, head_dim)
2095
+ qkv = qkv.permute(2, 0, 3, 1, 4)
2096
+
2097
+ # Unbind separates the first dimension (the '3') into a tuple
2098
+ q, k, v = qkv.unbind(0)
2099
+
2100
+ # 3. Apply norms
2101
+ q = F.rms_norm(q, (q.size(-1),))
2102
+ k = F.rms_norm(k, (k.size(-1),))
2103
+
2104
+ # 4. RoPE
2105
+ cos, sin = self.rotary(seqlen, x.device, q.dtype)
2106
+ q = apply_rotary_emb(q, cos, sin)
2107
+ k = apply_rotary_emb(k, cos, sin)
2108
+
2109
+ # 5. Q-gain scaling
2110
+ q = q * self.q_gain.to(dtype=q.dtype)[None, :, None, None]
2111
+
2112
+ # 6. Attention
2113
+ y = F.scaled_dot_product_attention(
2114
+ q,
2115
+ k,
2116
+ v,
2117
+ attn_mask=None,
2118
+ is_causal=True,
2119
+ enable_gqa=False, # Standard MHA
2120
+ )
2121
+
2122
+ # 7. Final projection
2123
+ y = y.transpose(1, 2).contiguous().reshape(bsz, seqlen, dim)
2124
+ return self.proj(y)
vathos/_triton.py ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Triton-fused two-layer MLPs in the style of `VariableUDLP`:
3
+ out = (act(x @ W1.T))^2 @ W2.T (ReLU^2, LeakyReLU(0.5)^2)
4
+ out = (silu(a) * b) @ W2.T (SwiGLU)
5
+
6
+ The fused ReLU^2 and LeakyReLU^2 kernels are ports of:
7
+ - modded-nanogpt linear_relu_square (Keller Jordan et al.)
8
+ - parameter-golf linear_leaky_relu_square (samacqua / openai/parameter-golf)
9
+
10
+ Both use the Hopper TensorDescriptor (TMA) API, so they need:
11
+ - triton >= 3.2
12
+ - GPU compute capability >= 9.0 (H100 / GH200)
13
+ On older GPUs / when triton is missing, the wrapper modules transparently fall
14
+ back to a pure-torch path that is `torch.compile`-friendly.
15
+
16
+ The autograd.Function wrappers are accepted natively by `torch.compile` (>=2.0):
17
+ the compiler treats `.apply(...)` as an opaque op and traces around it.
18
+
19
+ API: each module mirrors `VariableUDLP(d_model, d_output, M, dropout)`.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import torch
25
+ import torch.nn as nn
26
+ import torch.nn.functional as F
27
+
28
+ from Vathos._basics import Layer
29
+ from Vathos.functions import flag
30
+
31
+ try:
32
+ import triton
33
+ import triton.language as tl
34
+ from triton.tools.tensor_descriptor import TensorDescriptor
35
+ _HAS_TRITON = True
36
+ except Exception:
37
+ _HAS_TRITON = False
38
+
39
+
40
+ def _hopper_available() -> bool:
41
+ if not (_HAS_TRITON and torch.cuda.is_available()):
42
+ return False
43
+ major, _ = torch.cuda.get_device_capability()
44
+ return major >= 9
45
+
46
+
47
+ # =============================================================================
48
+ # Triton kernels
49
+ # =============================================================================
50
+
51
+ if _HAS_TRITON:
52
+
53
+ @triton.jit
54
+ def _linear_relu_square_kernel(
55
+ a_desc, b_desc, c_desc, aux_desc,
56
+ M, N, K,
57
+ BLOCK_SIZE_M: tl.constexpr,
58
+ BLOCK_SIZE_N: tl.constexpr,
59
+ BLOCK_SIZE_K: tl.constexpr,
60
+ NUM_SMS: tl.constexpr,
61
+ FORWARD: tl.constexpr,
62
+ ):
63
+ """Port of modded-nanogpt's linear_relu_square_kernel.
64
+
65
+ FORWARD: computes c = (x @ W1.T) and aux = relu(c)^2 (c is the pre-activation)
66
+ BACKWARD: computes c = grad_pre = grad_post * 2 * relu(pre), reading pre from aux
67
+ (caller passes aux=pre, grad_post is the input matmul result)
68
+ """
69
+ dtype = tl.bfloat16
70
+ start_pid = tl.program_id(axis=0)
71
+ num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
72
+ num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
73
+ num_tiles = num_pid_m * num_pid_n
74
+ k_tiles = tl.cdiv(K, BLOCK_SIZE_K)
75
+
76
+ for tile_id in tl.range(start_pid, num_tiles, NUM_SMS, flatten=True):
77
+ pid_m = tile_id // num_pid_n
78
+ pid_n = tile_id % num_pid_n
79
+ offs_am = pid_m * BLOCK_SIZE_M
80
+ offs_bn = pid_n * BLOCK_SIZE_N
81
+
82
+ accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
83
+ for ki in range(k_tiles):
84
+ offs_k = ki * BLOCK_SIZE_K
85
+ a = a_desc.load([offs_am, offs_k])
86
+ b = b_desc.load([offs_bn, offs_k])
87
+ accumulator = tl.dot(a, b.T, accumulator)
88
+
89
+ acc = tl.reshape(accumulator, (BLOCK_SIZE_M, 2, BLOCK_SIZE_N // 2))
90
+ acc = tl.permute(acc, (0, 2, 1))
91
+ acc0, acc1 = tl.split(acc)
92
+
93
+ c0 = acc0.to(dtype)
94
+ if not FORWARD:
95
+ pre0 = aux_desc.load([offs_am, offs_bn])
96
+ c0 = 2 * c0 * tl.where(pre0 > 0, pre0, 0)
97
+ c_desc.store([offs_am, offs_bn], c0)
98
+ if FORWARD:
99
+ p0 = tl.maximum(c0, 0)
100
+ aux_desc.store([offs_am, offs_bn], p0 * p0)
101
+
102
+ c1 = acc1.to(dtype)
103
+ if not FORWARD:
104
+ pre1 = aux_desc.load([offs_am, offs_bn + BLOCK_SIZE_N // 2])
105
+ c1 = 2 * c1 * tl.where(pre1 > 0, pre1, 0)
106
+ c_desc.store([offs_am, offs_bn + BLOCK_SIZE_N // 2], c1)
107
+ if FORWARD:
108
+ p1 = tl.maximum(c1, 0)
109
+ aux_desc.store([offs_am, offs_bn + BLOCK_SIZE_N // 2], p1 * p1)
110
+
111
+
112
+ @triton.jit
113
+ def _linear_leaky_relu_square_kernel(
114
+ a_desc, b_desc, c_desc, aux_desc,
115
+ M, N, K,
116
+ BLOCK_SIZE_M: tl.constexpr,
117
+ BLOCK_SIZE_N: tl.constexpr,
118
+ BLOCK_SIZE_K: tl.constexpr,
119
+ NUM_SMS: tl.constexpr,
120
+ FORWARD: tl.constexpr,
121
+ ):
122
+ """Port of parameter-golf's linear_leaky_relu_square_kernel.
123
+ Uses LeakyReLU with negative slope 0.5, then squares.
124
+ Forward: aux = lrelu(c)^2, Backward: c = grad_post * 2 * (lrelu'(pre) * lrelu(pre))
125
+ = grad_post * (where(pre>0, 2*pre, 0.5*pre))
126
+ """
127
+ dtype = tl.bfloat16
128
+ start_pid = tl.program_id(axis=0)
129
+ num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
130
+ num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
131
+ num_tiles = num_pid_m * num_pid_n
132
+ k_tiles = tl.cdiv(K, BLOCK_SIZE_K)
133
+
134
+ for tile_id in tl.range(start_pid, num_tiles, NUM_SMS, flatten=True):
135
+ pid_m = tile_id // num_pid_n
136
+ pid_n = tile_id % num_pid_n
137
+ offs_am = pid_m * BLOCK_SIZE_M
138
+ offs_bn = pid_n * BLOCK_SIZE_N
139
+
140
+ accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
141
+ for ki in range(k_tiles):
142
+ offs_k = ki * BLOCK_SIZE_K
143
+ a = a_desc.load([offs_am, offs_k])
144
+ b = b_desc.load([offs_bn, offs_k])
145
+ accumulator = tl.dot(a, b.T, accumulator)
146
+
147
+ acc = tl.reshape(accumulator, (BLOCK_SIZE_M, 2, BLOCK_SIZE_N // 2))
148
+ acc = tl.permute(acc, (0, 2, 1))
149
+ acc0, acc1 = tl.split(acc)
150
+
151
+ c0 = acc0.to(dtype)
152
+ c1 = acc1.to(dtype)
153
+ if not FORWARD:
154
+ pre0 = aux_desc.load([offs_am, offs_bn])
155
+ pre1 = aux_desc.load([offs_am, offs_bn + BLOCK_SIZE_N // 2])
156
+ c0 = c0 * tl.where(pre0 > 0, 2.0 * pre0, 0.5 * pre0)
157
+ c1 = c1 * tl.where(pre1 > 0, 2.0 * pre1, 0.5 * pre1)
158
+ c_desc.store([offs_am, offs_bn], c0)
159
+ c_desc.store([offs_am, offs_bn + BLOCK_SIZE_N // 2], c1)
160
+ if FORWARD:
161
+ aux0 = tl.where(c0 > 0, c0, 0.5 * c0)
162
+ aux1 = tl.where(c1 > 0, c1, 0.5 * c1)
163
+ aux_desc.store([offs_am, offs_bn], aux0 * aux0)
164
+ aux_desc.store([offs_am, offs_bn + BLOCK_SIZE_N // 2], aux1 * aux1)
165
+
166
+
167
+ # -----------------------------------------------------------------------------
168
+ # T4-friendly LeakyReLU(0.5)^2 kernel (no TMA, fp16, smaller tiles).
169
+ # Designed for sm_75 (Turing, T4): pointer-based loads, BLOCK_M=BLOCK_N=64,
170
+ # BLOCK_K=32, fp16 inputs/outputs, fp32 accumulator. Same fwd/bwd convention
171
+ # as the Hopper kernels: forward stores c=pre and aux=post; backward reads
172
+ # pre from aux and writes grad_pre into c. Output dtype follows the inputs
173
+ # (cast to fp32 fails tl.dot on Turing — keep them fp16).
174
+ # -----------------------------------------------------------------------------
175
+
176
+ if _HAS_TRITON:
177
+
178
+ @triton.jit
179
+ def _linear_lrelu2_kernel_t4(
180
+ a_ptr, b_ptr, c_ptr, aux_ptr,
181
+ M, N, K,
182
+ stride_am, stride_ak,
183
+ stride_bn, stride_bk,
184
+ stride_cm, stride_cn,
185
+ BLOCK_M: tl.constexpr,
186
+ BLOCK_N: tl.constexpr,
187
+ BLOCK_K: tl.constexpr,
188
+ GROUP_M: tl.constexpr,
189
+ FORWARD: tl.constexpr,
190
+ ):
191
+ pid = tl.program_id(axis=0)
192
+ num_pid_m = tl.cdiv(M, BLOCK_M)
193
+ num_pid_n = tl.cdiv(N, BLOCK_N)
194
+ num_pid_in_group = GROUP_M * num_pid_n
195
+ group_id = pid // num_pid_in_group
196
+ first_pid_m = group_id * GROUP_M
197
+ group_size_m = tl.minimum(num_pid_m - first_pid_m, GROUP_M)
198
+ pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
199
+ pid_n = (pid % num_pid_in_group) // group_size_m
200
+
201
+ offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
202
+ offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
203
+ offs_k = tl.arange(0, BLOCK_K)
204
+
205
+ a_ptrs = a_ptr + offs_m[:, None] * stride_am + offs_k[None, :] * stride_ak
206
+ b_ptrs = b_ptr + offs_n[:, None] * stride_bn + offs_k[None, :] * stride_bk
207
+
208
+ acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
209
+ for k in range(0, tl.cdiv(K, BLOCK_K)):
210
+ k_remaining = K - k * BLOCK_K
211
+ a_mask = (offs_m[:, None] < M) & (offs_k[None, :] < k_remaining)
212
+ b_mask = (offs_n[:, None] < N) & (offs_k[None, :] < k_remaining)
213
+ a = tl.load(a_ptrs, mask=a_mask, other=0.0)
214
+ b = tl.load(b_ptrs, mask=b_mask, other=0.0)
215
+ acc += tl.dot(a, b.T)
216
+ a_ptrs += BLOCK_K * stride_ak
217
+ b_ptrs += BLOCK_K * stride_bk
218
+
219
+ out_dtype = c_ptr.dtype.element_ty
220
+ c_ptrs = c_ptr + offs_m[:, None] * stride_cm + offs_n[None, :] * stride_cn
221
+ aux_ptrs = aux_ptr + offs_m[:, None] * stride_cm + offs_n[None, :] * stride_cn
222
+ mask = (offs_m[:, None] < M) & (offs_n[None, :] < N)
223
+
224
+ if FORWARD:
225
+ # acc here is pre = x @ W1.T. Save pre into c, post = lrelu²(pre) into aux.
226
+ tl.store(c_ptrs, acc.to(out_dtype), mask=mask)
227
+ scale = tl.where(acc > 0, 1.0, 0.25) # act² = z² * (1 if z>0 else 0.25)
228
+ post = acc * acc * scale
229
+ tl.store(aux_ptrs, post.to(out_dtype), mask=mask)
230
+ else:
231
+ # acc here is grad_post @ W2 (b=W2.T). Multiply elementwise by
232
+ # d(lrelu²)/dpre = where(pre>0, 2*pre, 0.5*pre) using saved pre in aux.
233
+ pre = tl.load(aux_ptrs, mask=mask, other=0.0).to(tl.float32)
234
+ deriv = tl.where(pre > 0, 2.0 * pre, 0.5 * pre)
235
+ tl.store(c_ptrs, (acc * deriv).to(out_dtype), mask=mask)
236
+
237
+
238
+ def _launch_lrelu2_t4(a: torch.Tensor, b: torch.Tensor, aux: torch.Tensor | None):
239
+ """Pointer-based kernel launcher for T4. Returns (pre, post) in fwd, grad_pre in bwd."""
240
+ M, K = a.shape
241
+ N, K2 = b.shape
242
+ assert K == K2
243
+
244
+ c = torch.empty((M, N), device=a.device, dtype=a.dtype)
245
+ forward = aux is None
246
+ if aux is None:
247
+ aux = torch.empty((M, N), device=a.device, dtype=a.dtype)
248
+
249
+ BLOCK_M, BLOCK_N, BLOCK_K = 64, 64, 32
250
+ grid = (triton.cdiv(M, BLOCK_M) * triton.cdiv(N, BLOCK_N),)
251
+ _linear_lrelu2_kernel_t4[grid](
252
+ a, b, c, aux,
253
+ M, N, K,
254
+ a.stride(0), a.stride(1),
255
+ b.stride(0), b.stride(1),
256
+ c.stride(0), c.stride(1),
257
+ BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, BLOCK_K=BLOCK_K,
258
+ GROUP_M=8,
259
+ FORWARD=forward,
260
+ num_warps=4,
261
+ num_stages=2,
262
+ )
263
+ return (c, aux) if forward else c
264
+
265
+
266
+ class _FusedLinearLReLU2_T4(torch.autograd.Function):
267
+ @staticmethod
268
+ def forward(ctx, x, W1, W2):
269
+ x_flat = x.reshape(-1, x.shape[-1])
270
+ pre, post = _launch_lrelu2_t4(x_flat, W1, None)
271
+ out = post @ W2.T
272
+ ctx.save_for_backward(x, W1, W2, pre, post)
273
+ return out.view(*x.shape[:-1], W2.shape[0])
274
+
275
+ @staticmethod
276
+ def backward(ctx, grad_out):
277
+ x, W1, W2, pre, post = ctx.saved_tensors
278
+ x_flat = x.reshape(-1, x.shape[-1])
279
+ go = grad_out.reshape(-1, W2.shape[0])
280
+ dW2 = go.T @ post
281
+ dpre = _launch_lrelu2_t4(go, W2.T.contiguous(), pre)
282
+ dW1 = dpre.T @ x_flat
283
+ dx = dpre @ W1
284
+ return dx.view_as(x), dW1, dW2
285
+
286
+
287
+ def _can_use_triton_t4(x) -> bool:
288
+ """T4 (CC 7.5) requires fp16 inputs."""
289
+ if not (_HAS_TRITON and torch.cuda.is_available()):
290
+ return False
291
+ major, minor = torch.cuda.get_device_capability()
292
+ if (major, minor) != (7, 5):
293
+ return False
294
+ return x.dtype == torch.float16
295
+
296
+
297
+ # =============================================================================
298
+ # Host-side launchers (return both pre-activation and post-activation in fwd)
299
+ # =============================================================================
300
+
301
+ def _launch_fused_act_square(kernel_fn, a: torch.Tensor, b: torch.Tensor,
302
+ aux: torch.Tensor | None):
303
+ """Shared launcher for both relu^2 and leaky_relu^2 kernels.
304
+ Forward (aux=None): returns (pre, post) = (x@W.T, act(pre)^2)
305
+ Backward (aux=pre): returns dpre = grad_post * d(act(pre)^2)/dpre
306
+ """
307
+ M, K = a.shape
308
+ N, K2 = b.shape
309
+ assert K == K2
310
+
311
+ c = torch.empty((M, N), device=a.device, dtype=a.dtype)
312
+ forward = aux is None
313
+ if aux is None:
314
+ aux = torch.empty((M, N), device=a.device, dtype=a.dtype)
315
+
316
+ BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K = 128, 256, 64
317
+ num_stages = 4 if forward else 3
318
+
319
+ a_desc = TensorDescriptor.from_tensor(a, [BLOCK_SIZE_M, BLOCK_SIZE_K])
320
+ b_desc = TensorDescriptor.from_tensor(b, [BLOCK_SIZE_N, BLOCK_SIZE_K])
321
+ c_desc = TensorDescriptor.from_tensor(c, [BLOCK_SIZE_M, BLOCK_SIZE_N // 2])
322
+ aux_desc = TensorDescriptor.from_tensor(aux, [BLOCK_SIZE_M, BLOCK_SIZE_N // 2])
323
+
324
+ num_sms = torch.cuda.get_device_properties(a.device).multi_processor_count
325
+ grid = lambda _meta: (
326
+ min(num_sms, triton.cdiv(M, BLOCK_SIZE_M) * triton.cdiv(N, BLOCK_SIZE_N)),
327
+ )
328
+ kernel_fn[grid](
329
+ a_desc, b_desc, c_desc, aux_desc,
330
+ M, N, K,
331
+ BLOCK_SIZE_M=BLOCK_SIZE_M,
332
+ BLOCK_SIZE_N=BLOCK_SIZE_N,
333
+ BLOCK_SIZE_K=BLOCK_SIZE_K,
334
+ NUM_SMS=num_sms,
335
+ FORWARD=forward,
336
+ num_stages=num_stages,
337
+ num_warps=8,
338
+ )
339
+ return (c, aux) if forward else c
340
+
341
+
342
+ # =============================================================================
343
+ # Autograd Functions — composes the fused kernel with the second matmul.
344
+ # Compatible with torch.compile (autograd.Function is opaque to dynamo).
345
+ # =============================================================================
346
+
347
+ class _FusedLinearReLU2(torch.autograd.Function):
348
+ @staticmethod
349
+ def forward(ctx, x, W1, W2):
350
+ x_flat = x.reshape(-1, x.shape[-1])
351
+ pre, post = _launch_fused_act_square(_linear_relu_square_kernel, x_flat, W1, None)
352
+ out = post @ W2.T
353
+ ctx.save_for_backward(x, W1, W2, pre, post)
354
+ return out.view(*x.shape[:-1], W2.shape[0])
355
+
356
+ @staticmethod
357
+ def backward(ctx, grad_out):
358
+ x, W1, W2, pre, post = ctx.saved_tensors
359
+ x_flat = x.reshape(-1, x.shape[-1])
360
+ go = grad_out.reshape(-1, W2.shape[0])
361
+ dW2 = go.T @ post
362
+ dpre = _launch_fused_act_square(_linear_relu_square_kernel, go, W2.T.contiguous(), pre)
363
+ dW1 = dpre.T @ x_flat
364
+ dx = dpre @ W1
365
+ return dx.view_as(x), dW1, dW2
366
+
367
+
368
+ class _FusedLinearLReLU2(torch.autograd.Function):
369
+ @staticmethod
370
+ def forward(ctx, x, W1, W2):
371
+ x_flat = x.reshape(-1, x.shape[-1])
372
+ pre, post = _launch_fused_act_square(_linear_leaky_relu_square_kernel, x_flat, W1, None)
373
+ out = post @ W2.T
374
+ ctx.save_for_backward(x, W1, W2, pre, post)
375
+ return out.view(*x.shape[:-1], W2.shape[0])
376
+
377
+ @staticmethod
378
+ def backward(ctx, grad_out):
379
+ x, W1, W2, pre, post = ctx.saved_tensors
380
+ x_flat = x.reshape(-1, x.shape[-1])
381
+ go = grad_out.reshape(-1, W2.shape[0])
382
+ dW2 = go.T @ post
383
+ dpre = _launch_fused_act_square(_linear_leaky_relu_square_kernel, go, W2.T.contiguous(), pre)
384
+ dW1 = dpre.T @ x_flat
385
+ dx = dpre @ W1
386
+ return dx.view_as(x), dW1, dW2
387
+
388
+
389
+ # =============================================================================
390
+ # Pure-torch fallbacks (torch.compile-friendly)
391
+ # =============================================================================
392
+
393
+ def _eager_relu2_mlp(x, W1, W2):
394
+ return F.relu(F.linear(x, W1)).square() @ W2.T
395
+
396
+ def _eager_lrelu2_mlp(x, W1, W2):
397
+ return F.leaky_relu(F.linear(x, W1), 0.5).square() @ W2.T
398
+
399
+
400
+ def _can_use_triton_fused(x, M, N) -> bool:
401
+ """The TMA kernels require shapes divisible by their tile sizes and a Hopper GPU."""
402
+ if not _hopper_available():
403
+ return False
404
+ if x.dtype not in (torch.bfloat16, torch.float16):
405
+ return False
406
+ return (M % 128 == 0) and (N % 256 == 0)
407
+
408
+
409
+ # =============================================================================
410
+ # VariableUDLP-compatible nn.Modules
411
+ # =============================================================================
412
+
413
+ class _BaseTritonUDLP(Layer):
414
+ """Shared structure: expand (d_model→M, no bias) -> fused act² -> contract (M→d_output, no bias).
415
+
416
+ Matches `VariableUDLP(d_model, d_output, M, dropout)` so that it can be passed
417
+ as the `UDLP=` argument of `ModdedFormer` without further changes. The
418
+ `activation` parameter accepted by `VariableUDLP` is ignored here — the
419
+ activation is fixed by the chosen kernel (subclass).
420
+ """
421
+
422
+ _autograd_fn = None
423
+ _eager_fn = None
424
+ _name = "TritonUDLP"
425
+
426
+ def __init__(self, d_model: int, d_output: int, M: int,
427
+ activation=None, dropout: float = 0.0):
428
+ super().__init__()
429
+ self.d_model = d_model
430
+ self.d_output = d_output
431
+ self.M = M
432
+ self.expand = nn.Linear(d_model, M, bias=False)
433
+ self.contract = nn.Linear(M, d_output, bias=False)
434
+ self.dropout = nn.Dropout(dropout)
435
+ if activation is not None:
436
+ flag(f"{self._name}: 'activation' arg ignored — kernel hardcodes the activation.", 3)
437
+
438
+ def _init_weights(self):
439
+ torch.nn.init.zeros_(self.contract.weight)
440
+
441
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
442
+ W1 = self.expand.weight # [M, d_model]
443
+ W2 = self.contract.weight # [d_output, M]
444
+ if _can_use_triton_fused(x, self.M, self.d_output):
445
+ out = self._autograd_fn.apply(x, W1, W2)
446
+ else:
447
+ out = self._eager_fn(x, W1, W2)
448
+ return self.dropout(out)
449
+
450
+
451
+ class TritonReLU2UDLP(_BaseTritonUDLP):
452
+ """Fused ReLU² UDLP. Drop-in for VariableUDLP with activation=ReLU2."""
453
+ _autograd_fn = _FusedLinearReLU2 if _HAS_TRITON else None
454
+ _eager_fn = staticmethod(_eager_relu2_mlp)
455
+ _name = "TritonReLU2UDLP"
456
+
457
+
458
+ class TritonLReLU2UDLP(_BaseTritonUDLP):
459
+ """Fused LeakyReLU(0.5)² UDLP. Drop-in for VariableUDLP with activation=LeakyReLU2."""
460
+ _autograd_fn = _FusedLinearLReLU2 if _HAS_TRITON else None
461
+ _eager_fn = staticmethod(_eager_lrelu2_mlp)
462
+ _name = "TritonLReLU2UDLP"
463
+
464
+
465
+ class TritonLReLU2UDLP_T4(_BaseTritonUDLP):
466
+ """LeakyReLU(0.5)² UDLP tuned for T4 / Turing (sm_75).
467
+
468
+ Uses a non-TMA fp16 kernel with 64×64×32 tiles. Falls back to the eager
469
+ torch path on non-T4 GPUs or when triton is missing.
470
+
471
+ NOTE: weights are kept fp32 by Linear; cast inputs to fp16 before forward
472
+ (e.g. `model = model.half()` or `with torch.autocast('cuda', dtype=torch.float16)`).
473
+ """
474
+ _autograd_fn = _FusedLinearLReLU2_T4 if _HAS_TRITON else None
475
+ _eager_fn = staticmethod(_eager_lrelu2_mlp)
476
+ _name = "TritonLReLU2UDLP_T4"
477
+
478
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
479
+ W1 = self.expand.weight
480
+ W2 = self.contract.weight
481
+ if _can_use_triton_t4(x):
482
+ out = self._autograd_fn.apply(x, W1, W2)
483
+ else:
484
+ out = self._eager_fn(x, W1, W2)
485
+ return self.dropout(out)
486
+
487
+
488
+ # -----------------------------------------------------------------------------
489
+ # SwiGLU — torch-only path (compile-friendly).
490
+ #
491
+ # SwiGLU is gated, so the expand projects to 2*M and the activation consumes
492
+ # both halves: silu(a) * b. There is no clean port of this in modded-nanogpt
493
+ # or parameter-golf; Liger-Kernel implements a full fused fwd+bwd for it but
494
+ # that's a separate dependency. The torch path below compiles cleanly and is
495
+ # usually within ~10% of a custom kernel for the typical M sizes we use.
496
+ # -----------------------------------------------------------------------------
497
+
498
+ class TritonSwiGLUUDLP(Layer):
499
+ """SwiGLU UDLP, drop-in for VariableUDLP with activation=SwiGLU.
500
+ Compile-friendly torch implementation — no custom kernel.
501
+ `M` is the post-gate inner dimension (i.e. expand projects to 2*M).
502
+ """
503
+
504
+ def __init__(self, d_model: int, d_output: int, M: int,
505
+ activation=None, dropout: float = 0.0):
506
+ super().__init__()
507
+ self.d_model = d_model
508
+ self.d_output = d_output
509
+ self.M = M
510
+ self.expand = nn.Linear(d_model, 2 * M, bias=False)
511
+ self.contract = nn.Linear(M, d_output, bias=False)
512
+ self.dropout = nn.Dropout(dropout)
513
+ if activation is not None:
514
+ flag("TritonSwiGLUUDLP: 'activation' arg ignored — gate is fixed to SiLU.", 3)
515
+
516
+ def _init_weights(self):
517
+ torch.nn.init.zeros_(self.contract.weight)
518
+
519
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
520
+ h = self.expand(x)
521
+ a, b = h.chunk(2, dim=-1)
522
+ return self.dropout(self.contract(F.silu(a) * b))
523
+
524
+
525
+ __all__ = [
526
+ "TritonReLU2UDLP",
527
+ "TritonLReLU2UDLP",
528
+ "TritonLReLU2UDLP_T4",
529
+ "TritonSwiGLUUDLP",
530
+ ]
vathos/blocks.py ADDED
@@ -0,0 +1,2110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ All the layers included here aim to be already optimized by torch itself, without requiring triton/cuda kernels (explicitly),
3
+ in fact they try to only use matmul and Linear operations which are arguably already optimized implicitly in torch.
4
+ """
5
+ import time
6
+
7
+ import torch
8
+
9
+ from Vathos._basics import *
10
+ from typing import Tuple, Optional, Union, List
11
+ import re
12
+ from Vathos.complexity import combine_big_o_product, combine_big_o_sum
13
+ import math
14
+ import torch.nn.functional as F
15
+ from tqdm import tqdm
16
+ from Vathos._spatials import *
17
+
18
+
19
+ class Block1d(Layer):
20
+ def __init__(self, d_model, channel_mixer: Layer, spatial_mixer: Layer, norm=nn.LayerNorm):
21
+ super().__init__()
22
+ self.spatial_mixer = spatial_mixer
23
+ self.channel_mixer = channel_mixer
24
+ self.norm1 = norm(spatial_mixer.d_model)
25
+ self.norm2 = norm(spatial_mixer.d_model)
26
+
27
+ def forward(self, x: torch.Tensor, ve=None):
28
+ if ve is not None:
29
+ x = x + self.spatial_mixer(self.norm1(x), ve)
30
+ else:
31
+ x = x + self.spatial_mixer(self.norm1(x))
32
+
33
+ x = x + self.channel_mixer(self.norm2(x))
34
+ return x
35
+
36
+ def generate(self, x: torch.Tensor, ve=None):
37
+ h = self.norm1(x)
38
+ if self.spatial_mixer.has_custom_generate():
39
+ spatial_out = _call_with_ve(self.spatial_mixer.generate, h, ve)
40
+ else:
41
+ spatial_out = _call_with_ve(self.spatial_mixer, h, ve)
42
+ x = x + spatial_out
43
+
44
+ if self.channel_mixer.has_custom_generate():
45
+ x = x + self.channel_mixer.generate(self.norm2(x))
46
+ else:
47
+ x = x + self.channel_mixer(self.norm2(x))
48
+
49
+ return x
50
+
51
+
52
+ def _call_with_ve(fn, h, ve):
53
+ """Call fn(h, ve=ve) if it accepts ve, otherwise fn(h). Used by Block.generate
54
+ so that spatial mixers without ve support stay drop-in."""
55
+ if ve is None:
56
+ return fn(h)
57
+ try:
58
+ return fn(h, ve=ve)
59
+ except TypeError:
60
+ return fn(h)
61
+
62
+
63
+ class CBlock1d(Layer):
64
+ def __init__(self, d_model, channel_mixer: Layer, spatial_mixer: Layer, norm=nn.LayerNorm):
65
+ super().__init__()
66
+ self.spatial_mixer = spatial_mixer
67
+ self.channel_mixer = channel_mixer
68
+ self.norm1 = norm(spatial_mixer.d_model)
69
+ self.norm2 = norm(spatial_mixer.d_model)
70
+ self.l = nn.Parameter(torch.tensor([1.0, 1.0, 1.0, 1.0]))
71
+
72
+ def forward(self, x: torch.Tensor, ve=None):
73
+ if ve is not None:
74
+ x = x * self.l[0] + self.spatial_mixer(self.norm1(x), ve) * self.l[1]
75
+ else:
76
+ x = x * self.l[0] + self.spatial_mixer(self.norm1(x)) * self.l[1]
77
+ x = x * self.l[2] + self.channel_mixer(self.norm2(x)) * self.l[3]
78
+ return x
79
+
80
+ def generate(self, x: torch.Tensor, ve=None):
81
+ h = self.norm1(x)
82
+ if self.spatial_mixer.has_custom_generate():
83
+ spatial_out = _call_with_ve(self.spatial_mixer.generate, h, ve)
84
+ else:
85
+ spatial_out = _call_with_ve(self.spatial_mixer, h, ve)
86
+ x = x * self.l[0] + spatial_out * self.l[1]
87
+ if self.channel_mixer.has_custom_generate():
88
+ x = x * self.l[2] + self.channel_mixer.generate(self.norm2(x)) * self.l[3]
89
+ else:
90
+ x = x * self.l[2] + self.channel_mixer(self.norm2(x)) * self.l[3]
91
+ return x
92
+
93
+
94
+ # TODO: Smear
95
+ class SmearBlock1d(Layer):
96
+ def __init__(self, d_model, channel_mixer: Layer, spatial_mixer: Layer, norm=nn.LayerNorm):
97
+ super().__init__()
98
+ self.spatial_mixer = spatial_mixer
99
+ self.channel_mixer = channel_mixer
100
+ self.norm1 = norm(spatial_mixer.d_model)
101
+ self.norm2 = norm(spatial_mixer.d_model)
102
+
103
+ def forward(self, x: torch.Tensor):
104
+ x = x + self.spatial_mixer(self.norm1(x))
105
+ x = x + self.channel_mixer(self.norm2(x))
106
+ return x
107
+
108
+ def generate(self, x: torch.Tensor):
109
+ # Use generate if available, otherwise forward
110
+ if self.spatial_mixer.has_custom_generate():
111
+ x = x + self.spatial_mixer.generate(self.norm1(x))
112
+ else:
113
+ x = x + self.spatial_mixer(self.norm1(x))
114
+
115
+ if self.channel_mixer.has_custom_generate():
116
+ x = x + self.channel_mixer.generate(self.norm2(x))
117
+ else:
118
+ x = x + self.channel_mixer(self.norm2(x))
119
+
120
+ return x
121
+
122
+
123
+ class JBlock1d(Layer):
124
+ def __init__(self, d_model, channel_mixer: nn.Module, spatial_mixer: nn.Module, norm=nn.LayerNorm):
125
+ super().__init__()
126
+ self.channel_mixer = channel_mixer
127
+ self.spatial_mixer = spatial_mixer
128
+ self.norm = norm(d_model)
129
+
130
+ def forward(self, x: torch.Tensor, ve=None):
131
+ h = self.norm(x)
132
+ if ve is not None:
133
+ return x + self.spatial_mixer(h, ve) + self.channel_mixer(h)
134
+ return x + self.spatial_mixer(h) + self.channel_mixer(h)
135
+
136
+ def generate(self, x: torch.Tensor, ve=None):
137
+ h = self.norm(x)
138
+ sm_fn = self.spatial_mixer.generate if (hasattr(self.spatial_mixer, 'has_custom_generate') and self.spatial_mixer.has_custom_generate()) else self.spatial_mixer
139
+ cm_fn = self.channel_mixer.generate if (hasattr(self.channel_mixer, 'has_custom_generate') and self.channel_mixer.has_custom_generate()) else self.channel_mixer
140
+ spatial_out = _call_with_ve(sm_fn, h, ve)
141
+ channel_out = cm_fn(h)
142
+ return x + spatial_out + channel_out
143
+
144
+
145
+ class ExpandingBlock1d(Layer):
146
+ def __init__(self, d_model, channel_mixer: Layer, spatial_mixer: Layer, expand=1):
147
+ super().__init__()
148
+ self.spatial_mixer = spatial_mixer
149
+ self.channel_mixer = channel_mixer
150
+ self.expander = nn.Linear(d_model, d_model * expand, bias=False)
151
+ self.contractor = nn.Linear(d_model * expand, d_model, bias=False)
152
+ self.norm1 = nn.LayerNorm(spatial_mixer.d_model)
153
+ self.norm2 = nn.LayerNorm(spatial_mixer.d_model)
154
+
155
+ def forward(self, x: torch.Tensor):
156
+ x = x + self.contractor(self.spatial_mixer(self.norm1(self.expander(x))))
157
+ x = x + self.channel_mixer(self.norm2(x))
158
+ return x
159
+
160
+
161
+ class Renamer:
162
+ def __init__(self, constructor, renames: dict):
163
+ super().__init__()
164
+ self.constructor = constructor
165
+ self.renames = renames
166
+
167
+ def __call__(self, *args, **kwargs):
168
+ renamed_kwargs = {}
169
+ for key, value in kwargs.items():
170
+ if key in self.renames:
171
+ renamed_kwargs[self.renames[key]] = value
172
+ else:
173
+ renamed_kwargs[key] = value
174
+ return self.constructor(*args, renamed_kwargs)
175
+
176
+
177
+ class BlockStack(Layer):
178
+ def __init__(self, blocks: Tuple[Block1d | Layer]):
179
+ super().__init__()
180
+ self.blocks = blocks
181
+ self.stack = nn.ModuleList(blocks)
182
+
183
+ def forward(self, x: torch):
184
+ for block in self.stack:
185
+ x = block(x)
186
+ return x
187
+
188
+
189
+ class DepthwiseCausalConv1d(Layer):
190
+ __name__ = "CausalConv1d"
191
+ __complexity__ = "O(L d k)"
192
+
193
+ def __init__(self, d_model, k=3):
194
+ super().__init__()
195
+ self.d_model = d_model
196
+ self.k = k
197
+ self.pad = k - 1
198
+
199
+ self.K = nn.Parameter(torch.randn(d_model, k) / (k ** 0.5))
200
+
201
+ def forward(self, x):
202
+ b, L, d = x.shape
203
+ x = x.transpose(1, 2)
204
+ x_pad = F.pad(x, (self.pad, 0))
205
+ out = F.conv1d(x_pad, self.K.unsqueeze(1), groups=d)
206
+ return out.transpose(1, 2)
207
+
208
+
209
+ class CausalConv1d(Layer):
210
+ __name__ = "CausalConv1d"
211
+ __complexity__ = "O(L d k)"
212
+
213
+ def __init__(self, d_model, k=3, groups=None, outproj=False):
214
+ super().__init__()
215
+ self.d_model = d_model
216
+ self.k = k
217
+ self.pad = k - 1
218
+ self.groups = groups if groups is not None else d_model
219
+ self.outproj = nn.Linear(groups, d_model, bias=False) if outproj else Identity()
220
+ if self.groups != d_model and not outproj:
221
+ flag(
222
+ "Output channel numbers will be the number of groups, use outproj=True if you want to enable a linear projection to go back to d_model")
223
+
224
+ self.K = nn.Parameter(torch.randn(d_model, k) / (k ** 0.5))
225
+
226
+ def forward(self, x):
227
+ x = x.transpose(1, 2)
228
+ x_pad = F.pad(x, (self.pad, 0))
229
+ out = F.conv1d(x_pad, self.K.unsqueeze(1), groups=self.groups)
230
+ return out.transpose(1, 2)
231
+
232
+
233
+ class LSTM(Layer):
234
+ __name__ = "LSTM"
235
+ __complexity__ = "O(L d^2)"
236
+
237
+ def __init__(self, d_model, d_hidden, bidirectional=False, dropout=0.1, n_layer=1):
238
+ super().__init__()
239
+ self.bidirectional = bidirectional
240
+ self.d_model = d_model
241
+ self.d_hidden = d_hidden
242
+ self.projout = nn.Linear(d_hidden, d_model) if d_model != d_hidden else Identity()
243
+ self.LSTM = nn.LSTM(d_model, d_hidden, bidirectional=bidirectional, batch_first=True, num_layers=n_layer)
244
+ self.dropout = nn.Dropout(dropout)
245
+
246
+ def forward(self, x):
247
+ out, _ = self.LSTM(x)
248
+ out = self.dropout(out)
249
+ return self.projout(out)
250
+
251
+
252
+ class GRU(Layer):
253
+ __name__ = "GRU"
254
+ __complexity__ = "O(L d^2)"
255
+
256
+ def __init__(self, d_model, d_hidden, bidirectional=False, dropout=0.1, n_layer=1):
257
+ super().__init__()
258
+ self.bidirectional = bidirectional
259
+ self.d_model = d_model
260
+ self.d_hidden = d_hidden
261
+ self.projout = nn.Linear(d_hidden, d_model) if d_model != d_hidden else Identity()
262
+ self.GRU = nn.GRU(d_model, d_hidden, bidirectional=bidirectional, batch_first=True, num_layers=n_layer)
263
+ self.dropout = nn.Dropout(dropout)
264
+
265
+ def forward(self, x):
266
+ out, _ = self.GRU(x)
267
+ out = self.dropout(out)
268
+ return self.projout(out)
269
+
270
+
271
+ class LinearMixer(Layer):
272
+ __name__ = "Linear Mixer"
273
+ __complexity__ = "O(L^2 d)"
274
+
275
+ def __init__(self, d_model, max_len=1000, causal=True):
276
+ super().__init__()
277
+ self.causal = causal
278
+ self.d_model = d_model
279
+ self.W = nn.Parameter(torch.randn(max_len, max_len) * 0.01)
280
+
281
+ def forward(self, x):
282
+ x = x / math.sqrt(self.d_model)
283
+ if self.causal:
284
+ return torch.tril(self.W[:x.shape[1], :x.shape[1]]) @ x
285
+ else:
286
+ return self.W[:x.shape[1], :x.shape[1]] @ x
287
+
288
+
289
+ class MLPMixer(Layer):
290
+ __name__ = "Linear Mixer"
291
+ __complexity__ = "O(L^2 d)"
292
+
293
+ def __init__(self, d_model, max_len=1000, L_expand=1, causal=False, activation=nn.GELU):
294
+ super().__init__()
295
+ if causal:
296
+ if L_expand < 1:
297
+ raise ValueError("For the MLPMixer to be causal, L_expand must be greater than 1")
298
+ if L_expand > 1:
299
+ raise NotImplementedError(
300
+ "Causal MLP Mixer with L_expand is currently not implemented") # TODO: Causal Lex
301
+ self.causal = causal
302
+ self.d_model = d_model
303
+ self.W1 = nn.Parameter(torch.randn(max_len * L_expand, max_len) / max_len)
304
+ self.W2 = nn.Parameter(torch.randn(max_len, max_len * L_expand) / max_len)
305
+ self.activation = activation()
306
+
307
+ def forward(self, x):
308
+ x = x / math.sqrt(self.d_model)
309
+
310
+ return self.W2 @ self.activation(self.W1 @ x)
311
+
312
+
313
+ class ShortConvGatedMixer(Layer):
314
+ def __init__(self, d_model, mixer, mixer_params, k=4, activation=nn.Sigmoid, k1=None, k2=None):
315
+ super().__init__()
316
+ if k1 is None:
317
+ k1 = k2 = k
318
+
319
+ self.mixer = mixer(mixer_params)
320
+ self.conv_1 = DepthwiseCausalConv1d(d_model, k=k1)
321
+ self.conv_2 = DepthwiseCausalConv1d(d_model, k=k2)
322
+ self.activation = activation()
323
+
324
+ def forward(self, x):
325
+ g1 = self.conv_1(x)
326
+ g2 = self.activation(self.conv_2(x))
327
+ return self.mixer(g1) * g2 + (1 - g2) * x
328
+
329
+
330
+ class SmearGate(Layer):
331
+ """Smear gate (Modded-NanoGPT 2025-09 record): gate input-dependent + skip token-1.
332
+
333
+ Per ogni posizione t > 0:
334
+ x[t] += smear_lambda * sigmoid(W_gate @ x[t, :gate_input_dim]) * x[t-1]
335
+
336
+ Gate sparse (default 12 dim su d_model), input-dependent. Token 0 invariato.
337
+ smear_lambda init=0 ⇒ no-op iniziale. Receptive field = 1 (lookback=1 in fast gen).
338
+ """
339
+ __name__ = "SmearGate"
340
+
341
+ def __init__(self, d_model: int, gate_input_dim: int = 12):
342
+ super().__init__()
343
+ self.gate_input_dim = gate_input_dim
344
+ self.gate = nn.Linear(gate_input_dim, d_model, bias=False)
345
+ self.smear_lambda = nn.Parameter(torch.zeros(1))
346
+
347
+ def _init_weights(self):
348
+ # Identity-at-init: gate orthogonal; smear_lambda già 0 da __init__
349
+ # ⇒ shifted = 0 ⇒ no-op iniziale indipendentemente dal gate.
350
+ nn.init.orthogonal_(self.gate.weight)
351
+
352
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
353
+ # x: [B, L, D]. Caso L==1: nessuno "x[t-1]" disponibile → no-op.
354
+ if x.size(1) < 2:
355
+ return x
356
+ gate = torch.sigmoid(self.gate(x[:, 1:, :self.gate_input_dim])) # [B, L-1, D]
357
+ prev = x[:, :-1, :] # [B, L-1, D]
358
+ shifted = self.smear_lambda * gate * prev # [B, L-1, D]
359
+ return torch.cat([x[:, :1, :], x[:, 1:, :] + shifted], dim=1)
360
+
361
+
362
+ class Embedder(Layer):
363
+ __name__ = "SymbolicEmbedder"
364
+ __complexity__ = "O(L d)"
365
+
366
+ def __init__(self, vocab_size, d_model: int):
367
+ super().__init__()
368
+ self.vocab_size = vocab_size
369
+ self.d_model = d_model
370
+ self.frozen = False
371
+ self.embedding = nn.Embedding(vocab_size, d_model)
372
+
373
+ def freeze(self):
374
+ if self.frozen:
375
+ flag("Trying to freeze an already frozen Embedder")
376
+ else:
377
+ self.embedding.weight.requires_grad = False
378
+ self.frozen = True
379
+
380
+ def unfreeze(self):
381
+ if self.frozen:
382
+ self.embedding.weight.requires_grad = True
383
+ self.frozen = False
384
+ else:
385
+ flag("Trying to unfreeze an already unfrozen Embedder")
386
+
387
+ def forward(self, x):
388
+ return self.embedding(x)
389
+
390
+ def finetune(self):
391
+ self.freeze()
392
+
393
+
394
+ class EasyEmbedder(Layer):
395
+ __name__ = "SymbolicEmbedder"
396
+ __complexity__ = "O(L d)"
397
+
398
+ def __init__(self, vocab_size, d_model: int, dropout=0.13):
399
+ super().__init__()
400
+ self.vocab_size = vocab_size
401
+ self.d_model = d_model
402
+ self.embedding = nn.Embedding(vocab_size, d_model)
403
+ self.dropout = nn.Dropout(dropout)
404
+
405
+ def forward(self, x):
406
+ return self.dropout(self.embedding(x))
407
+
408
+
409
+ class HybridAttentionBlock1d(Layer):
410
+ def __init__(self, d_model, sec_mixer, sec_params, attn_params, n_attn=1, n_sec=3,
411
+ channel_mixer=MLP, channel_params=None):
412
+ super().__init__()
413
+ self.n_sec = n_sec
414
+ self.n_attn = n_attn
415
+ self.d_model = d_model
416
+ self.attn_params = attn_params
417
+ self.sec_params = sec_params
418
+ self.sec_mixer = sec_mixer
419
+ self.channel_mixer = channel_mixer
420
+ self.channel_params = channel_params
421
+
422
+ if channel_params is None and isinstance(channel_mixer, MLP):
423
+ self.channel_params = {'expand': 2}
424
+ else:
425
+ self.channel_params = channel_params
426
+ self.attn_blocks = nn.ModuleList([
427
+ Block1d(
428
+ channel_mixer=self.channel_mixer(d_model=d_model, **self.channel_params),
429
+ spatial_mixer=MultiheadAttentionMixer(d_model=d_model, **self.attn_params)
430
+ )
431
+ for _ in range(self.n_attn)
432
+ ])
433
+ self.sec_blocks = nn.ModuleList([
434
+ Block1d(
435
+ channel_mixer=self.channel_mixer(d_model=d_model, **self.channel_params),
436
+ spatial_mixer=self.sec_mixer(d_model=d_model, **self.sec_params)
437
+ )
438
+ for _ in range(self.n_sec)
439
+ ])
440
+
441
+ def forward(self, x):
442
+ for sec in self.sec_blocks:
443
+ x = sec(x)
444
+ for attn in self.attn_blocks:
445
+ x = attn(x)
446
+ return x
447
+
448
+
449
+ ########################################################################################################################
450
+ # VISION
451
+ ########################################################################################################################
452
+
453
+
454
+ class PatchEmbedder(Layer):
455
+ __name__ = "PatchEmbedder"
456
+
457
+ def __init__(
458
+ self,
459
+ vocab_size=None,
460
+ d_model: int = 768,
461
+ img_size: Union[int, Tuple[int, int]] = 224,
462
+ patch_size: Union[int, Tuple[int, int]] = 16,
463
+ in_chans: int = 3,
464
+ flatten: bool = True,
465
+ norm_layer: Optional[Layer] = None,
466
+ cls: bool = False,
467
+ ):
468
+ super().__init__()
469
+ if isinstance(patch_size, int):
470
+ patch_size = (patch_size, patch_size)
471
+ if isinstance(img_size, int):
472
+ img_size = (img_size, img_size)
473
+
474
+ self.img_size = img_size
475
+ self.patch_size = patch_size
476
+ self.in_chans = in_chans
477
+ self.embed_dim = d_model
478
+ self.flatten = flatten
479
+ self.frozen = False
480
+ self.norm = norm_layer if norm_layer is not None else None
481
+ self.use_cls = cls # ← NEW
482
+
483
+ self.proj = nn.Conv2d(in_chans, d_model, kernel_size=patch_size, stride=patch_size)
484
+
485
+ if self.use_cls:
486
+ assert flatten, "CLS token requires flatten=True"
487
+ self.cls_token = nn.Parameter(torch.zeros(1, 1, d_model)) # ← NEW
488
+
489
+ self.grid_size = None
490
+ if img_size is not None:
491
+ self.grid_size = (math.ceil(img_size[0] / patch_size[0]),
492
+ math.ceil(img_size[1] / patch_size[1]))
493
+
494
+ self._num_patches = None
495
+ if self.grid_size is not None:
496
+ self._num_patches = self.grid_size[0] * self.grid_size[1]
497
+
498
+ def freeze(self):
499
+ if self.frozen:
500
+ flag("Trying to freeze an already frozen PatchEmbedder")
501
+ return
502
+ for p in self.proj.parameters():
503
+ p.requires_grad = False
504
+ if self.norm is not None:
505
+ for p in self.norm.parameters():
506
+ p.requires_grad = False
507
+ if self.use_cls: # ← FREEZE CLS TOKEN TOO
508
+ self.cls_token.requires_grad = False
509
+ self.frozen = True
510
+
511
+ def unfreeze(self):
512
+ if not self.frozen:
513
+ flag("Trying to unfreeze an already unfrozen PatchEmbedder")
514
+ return
515
+ for p in self.proj.parameters():
516
+ p.requires_grad = True
517
+ if self.norm is not None:
518
+ for p in self.norm.parameters():
519
+ p.requires_grad = True
520
+ if self.use_cls:
521
+ self.cls_token.requires_grad = True
522
+ self.frozen = False
523
+
524
+ def num_patches(self, H: Optional[int] = None, W: Optional[int] = None) -> int:
525
+ if H is None or W is None:
526
+ if self.img_size is not None:
527
+ H, W = self.img_size
528
+ else:
529
+ raise ValueError("Must provide H and W or set img_size during init.")
530
+ ph, pw = self.patch_size
531
+ Hp = math.ceil(H / ph)
532
+ Wp = math.ceil(W / pw)
533
+ return Hp * Wp
534
+
535
+ def _pad_to_patch_multiple(self, x: torch.Tensor) -> torch.Tensor:
536
+ _, _, H, W = x.shape
537
+ ph, pw = self.patch_size
538
+ pad_h = (ph - H % ph) % ph
539
+ pad_w = (pw - W % pw) % pw
540
+ if pad_h == 0 and pad_w == 0:
541
+ return x
542
+ return nn.functional.pad(x, (0, pad_w, 0, pad_h), mode='constant', value=0)
543
+
544
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
545
+ assert x.dim() == 4, "Input must be 4D (B, C, H, W)"
546
+ assert x.dtype in (torch.float32, torch.float16, torch.bfloat16), "Input dtype must be float"
547
+ B, C, H, W = x.shape
548
+ assert C == self.in_chans, f"Expected {self.in_chans} channels, got {C}"
549
+
550
+ x = self._pad_to_patch_multiple(x)
551
+
552
+ x = self.proj(x) # (B, embed_dim, H_p, W_p)
553
+ H_p, W_p = x.shape[2], x.shape[3]
554
+ self._num_patches = H_p * W_p
555
+
556
+ if self.flatten:
557
+ x = x.flatten(2).transpose(1, 2) # (B, N, embed_dim)
558
+
559
+ if self.use_cls:
560
+ cls_tok = self.cls_token.expand(B, -1, -1) # (B, 1, D)
561
+ x = torch.cat((cls_tok, x), dim=1) # (B, 1+N, D)
562
+
563
+ if self.norm is not None:
564
+ x = self.norm(x)
565
+ else:
566
+ if self.norm is not None:
567
+ x = self.norm(x)
568
+
569
+ return x
570
+
571
+
572
+ class MeanClassificationHead(Layer):
573
+ __name__ = "MeanClassificationHead"
574
+
575
+ def __init__(self, d_model, vocab_size):
576
+ super().__init__()
577
+ self.proj = nn.Linear(d_model, vocab_size)
578
+
579
+ def forward(self, x):
580
+ x = x.mean(dim=1)
581
+ return self.proj(x)
582
+
583
+
584
+ class ClsHead(Layer):
585
+ __name__ = "Cls Head"
586
+
587
+ def __init__(self, d_model, vocab_size):
588
+ super().__init__()
589
+ self.proj = nn.Linear(d_model, vocab_size)
590
+
591
+ def forward(self, x):
592
+ x = x[:, -1:, :]
593
+ return self.proj(x)[:, 0, :]
594
+
595
+
596
+ class MultiHeadUnembedder(Layer):
597
+ __name__ = "UnbiasedLinear"
598
+ __complexity__ = "O(L d^2)"
599
+
600
+ def __init__(self, d_model, vocab_size, k=4):
601
+ super(MultiHeadUnembedder, self).__init__()
602
+ self.linear = nn.Linear(d_model, vocab_size * k, bias=False)
603
+
604
+ def forward(self, x):
605
+ return self.linear(x)
606
+
607
+
608
+ class VathosModel(Layer):
609
+ __name__ = "VathosModel"
610
+
611
+ def __init__(self):
612
+ super().__init__()
613
+
614
+ self._losses = []
615
+ self._losses_dict = {}
616
+ self._losses_per_epoch = []
617
+ self._losses_per_epoch_dict = {}
618
+ self._losses_this_epoch = []
619
+ self._metrics = dict()
620
+ self._metrics_this_epoch = dict()
621
+ self._metrics_per_epoch = dict()
622
+ self.autosave = True
623
+ self.autosave_overwrite = True
624
+ self.best_loss = float('inf')
625
+ self.checkpoints = 0
626
+ self.steps = 0
627
+ self.steps_per_epoch = 0
628
+ self.epochs = 0
629
+ self.finetuning = False
630
+ self.name = 'VathosModel_defaultName'
631
+
632
+ def flag_not_training(self):
633
+ if not self.training:
634
+ flag("You are not training")
635
+
636
+ def register_loss(self, loss: float) -> bool:
637
+ """Registra loss + NaN/Inf detection.
638
+
639
+ Ritorna True se loss finita, False se NaN/Inf (con warning).
640
+ Mantiene `self._nan_streak` (consecutive NaN counter).
641
+ """
642
+ is_bad = (loss != loss) or (loss == float('inf')) or (loss == float('-inf'))
643
+ if is_bad:
644
+ self._nan_streak = getattr(self, "_nan_streak", 0) + 1
645
+ import warnings
646
+ warnings.warn(
647
+ f"VathosModel.register_loss: loss non-finita ({loss}) a step "
648
+ f"{self.steps}. nan_streak={self._nan_streak}.",
649
+ RuntimeWarning, stacklevel=2,
650
+ )
651
+ else:
652
+ self._nan_streak = 0
653
+ self._losses_dict[self.steps] = loss
654
+ self._losses.append(loss)
655
+ self._losses_this_epoch.append(loss)
656
+ self.steps += 1
657
+ return not is_bad
658
+
659
+ def get_last_loss(self):
660
+ return self._losses[-1]
661
+
662
+ @property
663
+ def nan_streak(self) -> int:
664
+ return getattr(self, "_nan_streak", 0)
665
+
666
+ def get_mean_loss(self, epoch=True):
667
+ if epoch:
668
+ return np.mean(self._losses_this_epoch)
669
+ else:
670
+ return np.mean(self._losses)
671
+
672
+ def register_metrics(self, metrics: dict):
673
+ for metric in metrics:
674
+ if metric in self._metrics:
675
+ self._metrics[metric].append(metrics[metric])
676
+ self._metrics_this_epoch[metric].append(metrics[metric])
677
+ else:
678
+ flag(f"Registering a new metric {metric}")
679
+ self._metrics[metric] = [metrics[metric]]
680
+ self._metrics_this_epoch[metric] = [metrics[metric]]
681
+
682
+ def register_epoch(self):
683
+ self.epochs += 1
684
+ self._losses_per_epoch.append(np.mean(self._losses_this_epoch))
685
+ self._losses_per_epoch_dict[self.steps] = np.mean(self._losses_this_epoch)
686
+ self._losses_this_epoch = []
687
+
688
+ for metric in self._metrics_this_epoch:
689
+ if metric in self._metrics_per_epoch:
690
+ self._metrics_per_epoch[metric].append(np.mean(self._metrics_this_epoch[metric]))
691
+ else:
692
+ self._metrics_per_epoch[metric] = [np.mean(self._metrics_this_epoch[metric])]
693
+ self._metrics_this_epoch[metric] = []
694
+
695
+ if self._losses_per_epoch[-1] < self.best_loss:
696
+ self.best_loss = self._losses_per_epoch[-1]
697
+ if self.autosave:
698
+ self.checkpoints += 1
699
+ if self.autosave_overwrite:
700
+ self.save_checkpoint(f'{self.name}-checkpoint.pt')
701
+ else:
702
+ self.save_checkpoint(f'{self.name}-checkpoint-{self.checkpoints}.pt')
703
+
704
+ def save_state_dict(self, path):
705
+ torch.save(self.state_dict(), path)
706
+
707
+ def plot_losses(self):
708
+ print(self.steps_per_epoch)
709
+ plt.plot(
710
+ list(self._losses_dict.keys()),
711
+ list(self._losses_dict.values()),
712
+ label="Losses", linewidth=1)
713
+ plt.plot(
714
+ list(self._losses_per_epoch_dict.keys()),
715
+ list(self._losses_per_epoch_dict.values()),
716
+ label="Losses Per Epoch", linewidth=2)
717
+
718
+ plt.xlabel("steps")
719
+ plt.ylabel("loss")
720
+ plt.title("Model Losses per Steps")
721
+ plt.show()
722
+
723
+ def plot_metrics(self, figsize=(12, 8)):
724
+ n_metrics = len(self._metrics_per_epoch)
725
+ n_plots = 1 + n_metrics
726
+
727
+ n_cols = 2
728
+ n_rows = (n_plots + 1) // 2
729
+
730
+ fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize)
731
+
732
+ if n_plots == 1:
733
+ axes = np.array([axes])
734
+ else:
735
+ axes = axes.flatten()
736
+
737
+ ax = axes[0]
738
+ ax.plot(
739
+ list(self._losses_dict.keys()),
740
+ list(self._losses_dict.values()),
741
+ label="Losses", linewidth=1, alpha=0.6)
742
+ ax.plot(
743
+ list(self._losses_per_epoch_dict.keys()),
744
+ list(self._losses_per_epoch_dict.values()),
745
+ label="Losses Per Epoch", linewidth=2)
746
+ ax.set_xlabel("Steps")
747
+ ax.set_ylabel("Loss")
748
+ ax.set_title("Training Loss")
749
+ ax.legend()
750
+ ax.grid(True, alpha=0.3)
751
+
752
+ for idx, (metric_name, metric_values) in enumerate(self._metrics_per_epoch.items(), start=1):
753
+ ax = axes[idx]
754
+
755
+ if metric_name in self._metrics and len(self._metrics[metric_name]) > 0:
756
+ step_indices = list(range(len(self._metrics[metric_name])))
757
+ ax.plot(step_indices, self._metrics[metric_name],
758
+ label=f"{metric_name}", linewidth=1, alpha=0.6)
759
+
760
+ epoch_indices = list(range(len(metric_values)))
761
+ ax.plot(epoch_indices, metric_values,
762
+ label=f"{metric_name} Per Epoch", linewidth=2, marker='o')
763
+
764
+ ax.set_xlabel("Steps/Epochs")
765
+ ax.set_ylabel(metric_name)
766
+ ax.set_title(f"Metric: {metric_name}")
767
+ ax.legend()
768
+ ax.grid(True, alpha=0.3)
769
+
770
+ # Hide any unused subplots
771
+ for idx in range(n_plots, len(axes)):
772
+ axes[idx].set_visible(False)
773
+
774
+ plt.tight_layout()
775
+ plt.show()
776
+
777
+ def save_checkpoint(self, path):
778
+ """Save complete model checkpoint including training state"""
779
+ checkpoint = {
780
+ 'model_state_dict': self.state_dict(),
781
+ 'losses': self._losses,
782
+ 'losses_dict': self._losses_dict,
783
+ 'losses_per_epoch': self._losses_per_epoch,
784
+ 'losses_per_epoch_dict': self._losses_per_epoch_dict,
785
+ 'losses_this_epoch': self._losses_this_epoch,
786
+ 'metrics': self._metrics,
787
+ 'metrics_this_epoch': self._metrics_this_epoch,
788
+ 'metrics_per_epoch': self._metrics_per_epoch,
789
+ 'best_loss': self.best_loss,
790
+ 'checkpoints': self.checkpoints,
791
+ 'steps': self.steps,
792
+ 'steps_per_epoch': self.steps_per_epoch,
793
+ 'epochs': self.epochs,
794
+ 'autosave': self.autosave,
795
+ 'autosave_overwrite': self.autosave_overwrite,
796
+ }
797
+ torch.save(checkpoint, path)
798
+
799
+ def load_checkpoint(self, path):
800
+ """Load complete model checkpoint including training state"""
801
+ checkpoint = torch.load(path, weights_only=False)
802
+ self.load_state_dict(checkpoint['model_state_dict'])
803
+
804
+ # Restore training state
805
+ self._losses = checkpoint['losses']
806
+ self._losses_dict = checkpoint['losses_dict']
807
+ self._losses_per_epoch = checkpoint['losses_per_epoch']
808
+ self._losses_per_epoch_dict = checkpoint['losses_per_epoch_dict']
809
+ self._losses_this_epoch = checkpoint['losses_this_epoch']
810
+ self._metrics = checkpoint['metrics']
811
+ self._metrics_this_epoch = checkpoint['metrics_this_epoch']
812
+ self._metrics_per_epoch = checkpoint['metrics_per_epoch']
813
+ self.best_loss = checkpoint['best_loss']
814
+ self.checkpoints = checkpoint['checkpoints']
815
+ self.steps = checkpoint['steps']
816
+ self.steps_per_epoch = checkpoint['steps_per_epoch']
817
+ self.epochs = checkpoint['epochs']
818
+ self.autosave = checkpoint['autosave']
819
+ self.autosave_overwrite = checkpoint['autosave_overwrite']
820
+
821
+ def finetune(self):
822
+ self.finetuning = True
823
+
824
+ def train(self, *args, **kwargs):
825
+ self.finetuning = False
826
+ return super().train(*args, **kwargs)
827
+
828
+ def eval(self):
829
+ self.finetuning = False
830
+ return super().eval()
831
+
832
+
833
+ ########################################################################################################################
834
+ # Assemblers
835
+ ########################################################################################################################
836
+
837
+ class SequenceModel(VathosModel):
838
+ __name__ = "SequenceModel"
839
+
840
+ def __init__(self, vocab_size: int, d_model: int, n_layers: int,
841
+ max_len=1024,
842
+ pos_encoder: bool | None | Layer | nn.Module = None,
843
+ embedder=Embedder,
844
+ embedder_args: dict = None,
845
+ unembedder=UnbiasedLinear,
846
+ unembedder_args=None,
847
+ channel_mixer=MLP,
848
+ spatial_mixer: Layer | nn.Module = MultiheadAttentionMixer,
849
+ channel_args: dict = None,
850
+ spatial_args: dict = None,
851
+ name='',
852
+ pad='none',
853
+ baseblock=Block1d,
854
+ baseblock_args=None,
855
+ dropout=0.1,
856
+ weight_tying=False,
857
+ norm=nn.LayerNorm,
858
+ d_modifiers: List | None = None,
859
+ unet_skips=False,
860
+ unet_weights=False
861
+ ):
862
+ super().__init__()
863
+ self.pad = pad
864
+ flag(
865
+ "If you need to use any RoPE or alternative positional encodings which operate directly in the spatial mixer, be sure to call activate it in the spatial_args (e.g. rope=True)",
866
+ 2)
867
+
868
+ if channel_args is None and channel_mixer is MLP:
869
+ channel_args = {"expand": 2, "activation": nn.GELU, "depth": 2}
870
+ if spatial_args is None:
871
+ spatial_args = {}
872
+ if channel_args is None:
873
+ channel_args = {}
874
+ if embedder_args is None:
875
+ embedder_args = {}
876
+ if unembedder_args is None:
877
+ unembedder_args = {'input_features': d_model, 'output_features': vocab_size}
878
+ if baseblock_args is None:
879
+ baseblock_args = {}
880
+ if d_modifiers is None:
881
+ d_modifiers = [1 for _ in range(d_model)]
882
+ else:
883
+ print(
884
+ f"{NUM} Initialized SequenceModel with d_model structure: {[int(d_model * d) for d in d_modifiers]}{RES}")
885
+
886
+ self.pipe = {}
887
+ self.name = name
888
+ self.baseblock = baseblock
889
+ self.spatial_mixer = spatial_mixer
890
+ self.channel_mixer = channel_mixer
891
+ self.baseblock_args = baseblock_args
892
+
893
+ self.spatial_args = spatial_args
894
+ self.channel_args = channel_args
895
+ self.vocab_size = vocab_size
896
+ self.max_len = max_len
897
+ self.d_model = d_model
898
+ self.n_layers = n_layers
899
+ self.unet_skips = unet_skips
900
+
901
+ if self.unet_skips:
902
+ self.skip_weights = nn.Parameter(torch.zeros(self.n_layers // 2), requires_grad=unet_weights)
903
+ if d_modifiers is not None:
904
+ flag("Unet Skips detected, assure d_modifier are symmetrical to make it work!")
905
+
906
+ self.embedder = embedder(vocab_size=vocab_size, d_model=d_model, **embedder_args)
907
+
908
+ self.pos_encoder = pos_encoder(d_model, max_len=max_len) if pos_encoder not in (True, False, None) else \
909
+ (SinusoidalPositionalEncoding(d_model, max_len=max_len) if pos_encoder is True else nn.Identity())
910
+
911
+ self.blocks = nn.ModuleList([
912
+ self.baseblock(
913
+ d_model=int(d_model * d_modifiers[i]),
914
+ channel_mixer=channel_mixer(d_model=int(d_model * d_modifiers[i]), **channel_args),
915
+ spatial_mixer=spatial_mixer(d_model=int(d_model * d_modifiers[i]), **spatial_args),
916
+ norm=norm,
917
+ **baseblock_args
918
+ )
919
+ for i in range(n_layers)
920
+ ])
921
+ self._piped_blocks = None
922
+
923
+ self.norm = nn.LayerNorm(d_model)
924
+ self.unembedder = unembedder(**unembedder_args)
925
+
926
+ if weight_tying and hasattr(self.embedder, 'embedding') and hasattr(self.unembedder, 'linear'):
927
+ self.unembedder.linear.weight = self.embedder.embedding.weight
928
+
929
+ elif weight_tying:
930
+ raise TypeError(
931
+ "Automatic weight tying is only possible if the the Embedder has a 'embedding' attribute and Unembedder has a linear attribute"
932
+ "You should manually do weight tying if you aim to use specific layer:"
933
+ "\n e.g. model.unembedder.yourmodule.weight = model.embedder.youembeddings.weight is an auto weight tying example")
934
+
935
+ self.embedder_complexity = embedder.__complexity__ if hasattr(embedder, "__complexity__") else "O(L d)"
936
+ self.unembedder_complexity = unembedder.__complexity__ if hasattr(unembedder, "__complexity__") else "O(L d)"
937
+ self.spatial_complextiy = spatial_mixer.__complexity__ if hasattr(spatial_mixer, "__complexity__") else "O(L d)"
938
+ self.channel_complexity = channel_mixer.__complexity__ if hasattr(channel_mixer, "__complexity__") else "O(L d)"
939
+ self.runned = False
940
+ self.embed_scale = math.sqrt(d_model)
941
+ self._init_weights()
942
+
943
+ def _init_weights(self):
944
+ """
945
+ Initialize weights following GPT-style conventions:
946
+ - Small std for embeddings
947
+ - Xavier/Kaiming for linear layers
948
+ - Scaled initialization for residual projections
949
+ """
950
+ std_embed = 0.02
951
+ try:
952
+ nn.init.normal_(self.embedder.embedding.weight, mean=0.0, std=std_embed)
953
+ except:
954
+ pass
955
+ try:
956
+ nn.init.normal_(self.unembedder.weight, mean=0.0, std=std_embed)
957
+ except:
958
+ pass
959
+
960
+ for module in self.modules():
961
+ if isinstance(module, nn.Linear):
962
+ std_init = 0.02
963
+ nn.init.normal_(module.weight, mean=0.0, std=std_init)
964
+ if module.bias is not None:
965
+ nn.init.zeros_(module.bias)
966
+
967
+ elif isinstance(module, nn.LayerNorm):
968
+ nn.init.ones_(module.weight)
969
+ nn.init.zeros_(module.bias)
970
+
971
+ for block_idx, block in enumerate(self.blocks):
972
+ for name, module in block.named_modules():
973
+ if isinstance(module, nn.Linear):
974
+ depth_scale = (2.0 * self.n_layers) ** -0.5
975
+ if 'out' in name.lower() or 'proj' in name.lower() or '.l2' in name or '.g2' in name:
976
+ with torch.no_grad():
977
+ module.weight.data *= depth_scale
978
+
979
+ def forward(self, x: torch.LongTensor, unembed=True):
980
+ # B, L = x.size(0), x.size(1)
981
+ # x = self.embedder(x) * self.embed_scale
982
+ x = self.pos_encoder(x)
983
+
984
+ skips = []
985
+ half_layers = self.n_layers // 2
986
+
987
+ for i, block in enumerate(self.blocks):
988
+ if self.unet_skips:
989
+ if i < half_layers:
990
+ skips.append(x)
991
+ elif i >= self.n_layers - half_layers:
992
+ skip_idx = self.n_layers - 1 - i
993
+ x = x + self.skip_weights[skip_idx] * skips[skip_idx]
994
+
995
+ x = block(x)
996
+
997
+ if unembed:
998
+ x = self.norm(self.unembedder(x))
999
+
1000
+ return x
1001
+
1002
+ def insert_block(self, idx, module):
1003
+ self.blocks.insert(idx, module)
1004
+
1005
+ def append(self, module):
1006
+ self.blocks.append(module)
1007
+
1008
+ @torch.no_grad()
1009
+ def _clear_all_caches(self):
1010
+ """Clear KV caches in all attention layers"""
1011
+ for block in self.blocks:
1012
+ for module in block.modules():
1013
+ if hasattr(module, 'clear_cache'):
1014
+ module.clear_cache()
1015
+
1016
+ def forward(self, x: torch.LongTensor, unembed=True):
1017
+ B, L = x.size(0), x.size(1)
1018
+ x = self.embedder(x) * self.embed_scale
1019
+ x = self.pos_encoder(x)
1020
+
1021
+ if self.pad == 'sqrt':
1022
+ n = int((x.shape[1] ** 0.5) + 0.999999)
1023
+ x = F.pad(x, (0, 0, 0, n ** 2 - L), mode="constant", value=0)
1024
+
1025
+ for block in self.blocks:
1026
+ x = block(x)
1027
+
1028
+ if unembed:
1029
+ x = self.unembedder(x)
1030
+
1031
+ return x[:, :L, :]
1032
+
1033
+ def _sample_token(self, logits, temperature=1.0, top_p=1.0, top_k=None):
1034
+ logits = logits / (temperature + 1e-8)
1035
+
1036
+ if top_k is not None and top_k > 0:
1037
+ top_k = min(top_k, logits.size(-1))
1038
+ v, _ = torch.topk(logits, top_k)
1039
+ logits[logits < v[:, [-1]]] = float('-inf')
1040
+
1041
+ if top_p < 1.0:
1042
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
1043
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
1044
+
1045
+ sorted_indices_to_remove = cumulative_probs > top_p
1046
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
1047
+ sorted_indices_to_remove[..., 0] = 0
1048
+
1049
+ indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)
1050
+ logits[indices_to_remove] = float('-inf')
1051
+
1052
+ probs = F.softmax(logits, dim=-1)
1053
+ return torch.multinomial(probs, num_samples=1)
1054
+
1055
+ def generate(self, *args, **kwargs):
1056
+ if not 'custom_generate' in kwargs:
1057
+ kwargs['custom_generate'] = False
1058
+ if kwargs['custom_generate']:
1059
+ del kwargs['custom_generate']
1060
+ return self.custom_generate(*args, **kwargs)
1061
+ else:
1062
+ del kwargs['custom_generate']
1063
+ return self.simple_generate(*args, **kwargs)
1064
+
1065
+ @torch.no_grad()
1066
+ def simple_generate(self, prompt: torch.Tensor, max_len=100, temperature=1.0,
1067
+ top_p=1.0, top_k=50, token_end=None, repetition_penalty=1.0):
1068
+ self.eval()
1069
+ if prompt.dim() == 1:
1070
+ prompt = prompt.unsqueeze(0)
1071
+
1072
+ generated = prompt.clone()
1073
+ pbar = tqdm(range(max_len), desc="Simple Gen")
1074
+
1075
+ for _ in pbar:
1076
+ logits = self.forward(generated, unembed=True)
1077
+ next_token_logits = logits[:, -1, :]
1078
+
1079
+ # Apply repetition penalty
1080
+ if repetition_penalty != 1.0:
1081
+ next_token_logits = self._apply_repetition_penalty(
1082
+ next_token_logits, generated, repetition_penalty
1083
+ )
1084
+
1085
+ next_token = self._sample_token(next_token_logits, temperature, top_p, top_k)
1086
+ generated = torch.cat([generated, next_token], dim=1)
1087
+
1088
+ if token_end is not None and (next_token == token_end).all():
1089
+ break
1090
+
1091
+ return generated
1092
+
1093
+ @torch.no_grad()
1094
+ def custom_generate(self, prompt: torch.Tensor, max_len=100, temperature=1.0,
1095
+ top_p=1.0, top_k=50, token_end=None, repetition_penalty=1.0):
1096
+ self.eval()
1097
+ self._clear_all_caches()
1098
+
1099
+ if prompt.dim() == 1:
1100
+ prompt = prompt.unsqueeze(0)
1101
+
1102
+ x = self.embedder(prompt) * self.embed_scale
1103
+ if self.pos_encoder is not None and not isinstance(self.pos_encoder, nn.Identity):
1104
+ x = self.pos_encoder(x)
1105
+
1106
+ for block in self.blocks:
1107
+ if block.has_custom_generate():
1108
+ x = block.generate(x)
1109
+ else:
1110
+ x = block(x)
1111
+
1112
+ generated = prompt.clone()
1113
+ logits = self.unembedder(x)
1114
+ next_token_logits = logits[:, -1, :]
1115
+
1116
+ # Apply repetition penalty
1117
+ if repetition_penalty != 1.0:
1118
+ next_token_logits = self._apply_repetition_penalty(
1119
+ next_token_logits, generated, repetition_penalty
1120
+ )
1121
+
1122
+ next_token = self._sample_token(next_token_logits, temperature, top_p, top_k)
1123
+ generated = torch.cat([generated, next_token], dim=1)
1124
+
1125
+ pbar = tqdm(range(max_len - 1), desc="Fast Gen")
1126
+ for _ in pbar:
1127
+ x_t = self.embedder(next_token) * self.embed_scale
1128
+ current_pos = generated.shape[1] - 1
1129
+
1130
+ if isinstance(self.pos_encoder, SinusoidalPositionalEncoding):
1131
+ pe_slice = self.pos_encoder.pe[current_pos: current_pos + 1].unsqueeze(0)
1132
+ x_t = x_t + pe_slice
1133
+
1134
+ for block in self.blocks:
1135
+ if block.has_custom_generate():
1136
+ x_t = block.generate(x_t)
1137
+ else:
1138
+ x_t = block(x_t)
1139
+
1140
+ logits = self.unembedder(x_t)
1141
+ next_token_logits = logits[:, -1, :]
1142
+
1143
+ # Apply repetition penalty
1144
+ if repetition_penalty != 1.0:
1145
+ next_token_logits = self._apply_repetition_penalty(
1146
+ next_token_logits, generated, repetition_penalty
1147
+ )
1148
+
1149
+ next_token = self._sample_token(next_token_logits, temperature, top_p, top_k)
1150
+ generated = torch.cat([generated, next_token], dim=1)
1151
+
1152
+ if token_end is not None and (next_token == token_end).all():
1153
+ break
1154
+
1155
+ self._clear_all_caches()
1156
+ return generated
1157
+
1158
+ def _apply_repetition_penalty(self, logits: torch.Tensor,
1159
+ generated: torch.Tensor,
1160
+ repetition_penalty: float) -> torch.Tensor:
1161
+ """
1162
+ Apply repetition penalty to logits based on previously generated tokens.
1163
+
1164
+ Args:
1165
+ logits: Shape (batch_size, vocab_size)
1166
+ generated: Shape (batch_size, seq_len) - previously generated tokens
1167
+ repetition_penalty: Penalty factor (> 1.0 discourages repetition)
1168
+
1169
+ Returns:
1170
+ Modified logits with repetition penalty applied
1171
+ """
1172
+ batch_size = logits.shape[0]
1173
+
1174
+ for i in range(batch_size):
1175
+ # Get unique tokens in the generated sequence for this batch item
1176
+ unique_tokens = generated[i].unique()
1177
+
1178
+ # Apply penalty: divide logits by penalty if positive, multiply if negative
1179
+ for token_id in unique_tokens:
1180
+ if logits[i, token_id] > 0:
1181
+ logits[i, token_id] /= repetition_penalty
1182
+ else:
1183
+ logits[i, token_id] *= repetition_penalty
1184
+
1185
+ return logits
1186
+
1187
+ def summary(self):
1188
+ complexities = [self.channel_complexity, self.spatial_complextiy, self.embedder_complexity,
1189
+ self.unembedder_complexity]
1190
+ print(f'{NUM}VATHOS{RES} {self.name} Summary:')
1191
+ print(f"{NUM}SequenceModel{RES}(d_model={NUM}{self.d_model}{RES}, n_layer={NUM}{self.n_layers}{RES})")
1192
+ print(f"\t - {NUM}VOCAB_SIZE:{RES}: {NUM}{self.vocab_size}{RES}")
1193
+ print(f"\t - {NUM}D_MODEL:{RES}: {NUM}{self.d_model}{RES}")
1194
+ print(f"\t - {NUM}N_LAYERS:{RES}: {NUM}{self.n_layers}{RES}")
1195
+ print("")
1196
+ print(f"\t - {NUM}Embedder{RES}: {getname(self.embedder)} - {NUM}{self.embedder_complexity}{RES}")
1197
+ print(f"\t - {NUM}Unembedder{RES}: {getname(self.unembedder)} - {NUM}{self.unembedder_complexity}{RES}")
1198
+ print(
1199
+ f"\t - {NUM}Spatial Mixer{RES}: {getname(self.spatial_mixer)}({self.spatial_args}) - {NUM}{self.spatial_complextiy}{RES}")
1200
+ print(
1201
+ f"\t - {NUM}Channel Mixer{RES}: {getname(self.channel_mixer)}({self.channel_args}) - {NUM}{self.channel_complexity}{RES}")
1202
+ print(f"Num Parameters: {NUM}{sum([p.numel() for p in self.parameters()]):_}{RES}")
1203
+ print(f"Num Trainable Parameters: {NUM}{sum([p.numel() for p in self.parameters() if p.requires_grad]):_}{RES}")
1204
+ print(f"Total Complexity: {NUM}{combine_big_o_sum(complexities)}{RES}")
1205
+
1206
+ def finetune(self):
1207
+ flag(
1208
+ "Finetune simply checks for finetune() methods in spatial mixers, channel mixers, embedder and unembedder, "
1209
+ "if a finetune method is not available, the module/Layer will be left as it is")
1210
+ super().finetune()
1211
+
1212
+ if hasattr(self.embedder, "finetune"):
1213
+ self.embedder.finetune()
1214
+ if hasattr(self.unembedder, "finetune"):
1215
+ self.unembedder.finetune()
1216
+ for block in self.blocks:
1217
+ if hasattr(block, "finetune"):
1218
+ block.channel_mixer.finetune()
1219
+ block.spatial_mixer.finetune()
1220
+
1221
+
1222
+ class ModdedFormer(VathosModel):
1223
+ def __init__(self, vocab_size: int, embed_dim: int, d_models: List[int], spatials: List[Builder], expand=3,
1224
+ M_dims: List[int] | None = None, weights_tying=True,
1225
+ baseblock=Block1d, norm=RMSNorm, ffn_act=ReLU2, unet_skips=False, max_len=2400, zeroskip=False,
1226
+ UDLP=VariableUDLP,
1227
+ skips: List[int | None] | None = None, value_embeddings: List[int | None] | None = None,
1228
+ ve_type: str = 'scalar', ve_gate_dim: int | None = None,
1229
+ learnable_pe: bool = False, input_projections: bool | int=False, smear_gate: None | nn.Module | Layer=False):
1230
+ super().__init__()
1231
+ self.vocab_size = vocab_size
1232
+ self.embed_dim = embed_dim
1233
+ assert d_models[0] == embed_dim
1234
+ assert max(d_models) == min(d_models), "Variables d_models is WIP and not working"
1235
+ self.d_models = d_models
1236
+ n_layers = self.n_layer = len(d_models)
1237
+ self.input_projections = input_projections
1238
+ self.smear_gate = smear_gate
1239
+ if M_dims is not None:
1240
+ self.M_dims = M_dims
1241
+ else:
1242
+ self.M_dims = [d * expand for d in d_models]
1243
+
1244
+ self.embedder = Embedder(vocab_size, embed_dim)
1245
+ self.unembedder = UnbiasedLinear(d_models[-1], vocab_size)
1246
+ self.spatials = spatials
1247
+ self.norm = norm
1248
+ self.unet = unet_skips
1249
+ self.max_len = max_len
1250
+ self.zeroskip = zeroskip
1251
+
1252
+ self.skips = skips if skips is not None else [None] * n_layers
1253
+ assert len(self.skips) == n_layers
1254
+
1255
+ self.skip_lambdas = nn.ParameterDict()
1256
+ for source_idx, target_idx in enumerate(self.skips):
1257
+ if target_idx is not None:
1258
+ assert target_idx > source_idx
1259
+ assert target_idx < n_layers
1260
+ self.skip_lambdas[f"route_{source_idx}_to_{target_idx}"] = nn.Parameter(torch.zeros(1))
1261
+
1262
+ if weights_tying:
1263
+ assert d_models[-1] == embed_dim
1264
+ self.unembedder.linear.weight = self.embedder.embedding.weight
1265
+
1266
+ blocks = []
1267
+ for i in range(n_layers):
1268
+ in_dim = embed_dim if i == 0 else self.d_models[i - 1]
1269
+ out_dim = self.d_models[i]
1270
+ blocks.append(
1271
+ baseblock(
1272
+ self.d_models[i],
1273
+ UDLP(in_dim, d_output=out_dim, M=self.M_dims[i], activation=ffn_act),
1274
+ self.spatials[i](self.d_models[i]),
1275
+ norm=norm
1276
+ ))
1277
+
1278
+ self.blocks = nn.ModuleList(blocks)
1279
+
1280
+ if zeroskip:
1281
+ self.zeroskip_params = nn.ParameterList([nn.Parameter(torch.tensor([0.0])) for _ in range(n_layers)])
1282
+ if input_projections > 0:
1283
+ if not zeroskip:
1284
+ raise ValueError("Zeroskip must be activated when using input_projections")
1285
+ self.inputs_projection_linears = nn.Linear(in_features=d_models[0], out_features=d_models[0]*input_projections, bias=False)
1286
+ self.inputs_projection_params = nn.ParameterList([nn.Parameter(torch.tensor([0.0])) for _ in range(n_layers) for i in range(input_projections)])
1287
+
1288
+
1289
+ assert ve_type in ('scalar', 'gate'), f"ve_type must be 'scalar' or 'gate', got {ve_type}"
1290
+ self.ve_type = ve_type
1291
+ self.ve_gate_dim = ve_gate_dim if ve_gate_dim is not None else embed_dim
1292
+ self.value_embeddings_cfg = value_embeddings
1293
+
1294
+ # Value embeddings: ModuleDict/ParameterDict come prima (per leggibilità + checkpoint legacy),
1295
+ # MA in più costruiamo strutture INDEXED-BY-LAYER per il forward (no str() runtime, no dict
1296
+ # lookup → compile-friendly).
1297
+ if value_embeddings is not None:
1298
+ assert len(value_embeddings) == n_layers, "value_embeddings length must equal n_layers"
1299
+ unique_groups = sorted(set(v for v in value_embeddings if v is not None))
1300
+ self.ve_embeddings = nn.ModuleDict({
1301
+ str(g): nn.Embedding(vocab_size, embed_dim)
1302
+ for g in unique_groups
1303
+ })
1304
+ if ve_type == 'scalar':
1305
+ self.ve_scales = nn.ParameterDict({
1306
+ str(i): nn.Parameter(torch.zeros(1))
1307
+ for i, v in enumerate(value_embeddings) if v is not None
1308
+ })
1309
+ else: # gate
1310
+ self.ve_gates = nn.ModuleDict({
1311
+ str(i): nn.Linear(self.ve_gate_dim, embed_dim, bias=False)
1312
+ for i, v in enumerate(value_embeddings) if v is not None
1313
+ })
1314
+
1315
+ # Strutture parallele indexed-by-layer per il forward (riferiscono gli stessi param).
1316
+ self._ve_per_layer = nn.ModuleList([
1317
+ self.ve_embeddings[str(v)] if v is not None else nn.Identity()
1318
+ for v in value_embeddings
1319
+ ])
1320
+ self._has_ve_per_layer = tuple(v is not None for v in value_embeddings)
1321
+ self._has_any_ve = any(self._has_ve_per_layer)
1322
+
1323
+ if ve_type == 'scalar':
1324
+ self._ve_scale_per_layer = nn.ParameterList([
1325
+ self.ve_scales[str(i)] if value_embeddings[i] is not None
1326
+ else nn.Parameter(torch.zeros(1), requires_grad=False)
1327
+ for i in range(n_layers)
1328
+ ])
1329
+ self._ve_gate_per_layer = nn.ModuleList([nn.Identity() for _ in range(n_layers)])
1330
+ else:
1331
+ self._ve_scale_per_layer = nn.ParameterList([
1332
+ nn.Parameter(torch.zeros(1), requires_grad=False) for _ in range(n_layers)
1333
+ ])
1334
+ self._ve_gate_per_layer = nn.ModuleList([
1335
+ self.ve_gates[str(i)] if value_embeddings[i] is not None else nn.Identity()
1336
+ for i in range(n_layers)
1337
+ ])
1338
+ else:
1339
+ self.ve_embeddings = nn.ModuleDict()
1340
+ self._has_ve_per_layer = tuple([False] * n_layers)
1341
+ self._has_any_ve = False
1342
+ self._ve_per_layer = nn.ModuleList([nn.Identity() for _ in range(n_layers)])
1343
+ self._ve_scale_per_layer = nn.ParameterList()
1344
+ self._ve_gate_per_layer = nn.ModuleList()
1345
+
1346
+ # ---------------------------------------------------------------------
1347
+ # Skip routing: pre-computed plan + ParameterList (no string keys, no dict mutato)
1348
+ # ---------------------------------------------------------------------
1349
+ # Per ogni layer i:
1350
+ # - _skip_save[i]: bool, se salvare l'output come sorgente di skip
1351
+ # - _skip_consume_at[i]: tuple di (source_idx, gate_idx_in_list) da consumare qui
1352
+ self._skip_save = tuple(s is not None for s in self.skips)
1353
+ consume_lists = [[] for _ in range(n_layers)]
1354
+ gate_param_count = 0
1355
+ # Mapping (source, target) -> gate index nella ParameterList
1356
+ self._skip_gate_map = {}
1357
+ for source_idx, target_idx in enumerate(self.skips):
1358
+ if target_idx is not None:
1359
+ self._skip_gate_map[(source_idx, target_idx)] = gate_param_count
1360
+ consume_lists[target_idx].append((source_idx, gate_param_count))
1361
+ gate_param_count += 1
1362
+ self._skip_consume_at = tuple(tuple(lst) for lst in consume_lists)
1363
+ self._has_any_skip = gate_param_count > 0
1364
+ # ParameterList parallela: stessi tensori della ParameterDict legacy, indicizzati posizionalmente.
1365
+ if self._has_any_skip:
1366
+ self._skip_gates_list = nn.ParameterList([
1367
+ self.skip_lambdas[f"route_{s}_to_{t}"]
1368
+ for (s, t), _ in sorted(self._skip_gate_map.items(), key=lambda kv: kv[1])
1369
+ ])
1370
+ else:
1371
+ self._skip_gates_list = nn.ParameterList()
1372
+
1373
+ self.learnable_pe = learnable_pe
1374
+ if self.learnable_pe:
1375
+ self.pos_emb = nn.Parameter(torch.zeros(1, max_len, embed_dim))
1376
+
1377
+ self.final_norm = RMSNorm(d_models[-1])
1378
+ self._init_weights()
1379
+
1380
+ # Flag globale fast-path: nessuna feature opzionale → loop pulito senza branch.
1381
+ self._has_any_zeroskip = bool(zeroskip)
1382
+ self._has_input_projections = int(input_projections) > 0
1383
+ self._has_smear_gate = bool(smear_gate)
1384
+ self._has_learnable_pe = bool(learnable_pe)
1385
+ self._fast_path = not any([
1386
+ self._has_any_ve, self._has_any_skip,
1387
+ self._has_any_zeroskip, self._has_input_projections,
1388
+ self._has_smear_gate, self._has_learnable_pe,
1389
+ ])
1390
+
1391
+ def _apply_ve_weight(self, ve: torch.Tensor, x: torch.Tensor, layer_idx: int) -> torch.Tensor:
1392
+ """Legacy helper (mantenuto per backward compat). Il forward usa la versione indicizzata."""
1393
+ if self.ve_type == 'scalar':
1394
+ return ve * self.ve_scales[str(layer_idx)]
1395
+ else:
1396
+ gate = 2.0 * torch.sigmoid(self.ve_gates[str(layer_idx)](x[..., :self.ve_gate_dim]))
1397
+ return ve * gate
1398
+
1399
+ def _compute_ve_indexed(self, input_ids: torch.Tensor, x: torch.Tensor, i: int):
1400
+ """Versione fast: ModuleList[i] invece di ModuleDict[str(group_id)]."""
1401
+ ve = self._ve_per_layer[i](input_ids)
1402
+ if self.ve_type == 'scalar':
1403
+ return ve * self._ve_scale_per_layer[i]
1404
+ # gate type
1405
+ return ve * (2.0 * torch.sigmoid(self._ve_gate_per_layer[i](x[..., :self.ve_gate_dim])))
1406
+
1407
+ # -------------------------------------------------------------------------
1408
+ # Forward — fast path se niente feature opzionali, slow path altrimenti
1409
+ # -------------------------------------------------------------------------
1410
+
1411
+ def forward(self, x):
1412
+ # FAST PATH: standard transformer pretrain, no skip / no VE / no zeroskip / no PE / no smear.
1413
+ # È il caso di gran lunga più comune (es. PiCO 2 / Guido-1 baseline).
1414
+ if self._fast_path:
1415
+ x = self.embedder(x)
1416
+ for block in self.blocks:
1417
+ x = block(x)
1418
+ return self.unembedder(self.final_norm(x))
1419
+
1420
+ # SLOW PATH: include tutte le feature opzionali. Anche qui niente string keys / dict mutati:
1421
+ # le strutture sono pre-computate al __init__ e indexed-by-layer.
1422
+ input_ids = x # alias per VE lookup
1423
+
1424
+ x0 = self.embedder(x)
1425
+ if self._has_input_projections:
1426
+ xs = self.inputs_projection_linears(x0).chunk(self.input_projections, dim=-1)
1427
+ if self._has_learnable_pe:
1428
+ x0 = x0 + self.pos_emb[:, :x.size(1), :]
1429
+ if self._has_smear_gate:
1430
+ x0 = self.smear_gate(x0)
1431
+
1432
+ x = x0
1433
+ # Buffer di skip: lista Python di lunghezza fissa = n_layers. Allocata solo se serve.
1434
+ skip_buffer = [None] * self.n_layer if self._has_any_skip else None
1435
+
1436
+ for i, block in enumerate(self.blocks):
1437
+ # VE — indexed lookup (no string)
1438
+ ve = None
1439
+ if self._has_ve_per_layer[i]:
1440
+ ve = self._compute_ve_indexed(input_ids, x, i)
1441
+
1442
+ # Block forward + zeroskip variants
1443
+ if self._has_any_zeroskip and self._has_input_projections:
1444
+ proj_params = self.inputs_projection_params[
1445
+ i * self.input_projections: (i + 1) * self.input_projections
1446
+ ]
1447
+ x = block(x, ve=ve) + x0 * self.zeroskip_params[i] + sum(
1448
+ [xj * p for xj, p in zip(xs, proj_params)]
1449
+ )
1450
+ elif self._has_any_zeroskip:
1451
+ x = block(x, ve=ve) + x0 * self.zeroskip_params[i]
1452
+ else:
1453
+ x = block(x, ve=ve)
1454
+
1455
+ # Skip routing — pre-computed plan, indexed gates (no dict, no string)
1456
+ if self._has_any_skip:
1457
+ consume = self._skip_consume_at[i]
1458
+ for source_idx, gate_idx in consume:
1459
+ x = x + self._skip_gates_list[gate_idx] * skip_buffer[source_idx]
1460
+ if self._skip_save[i]:
1461
+ skip_buffer[i] = x
1462
+
1463
+ return self.unembedder(self.final_norm(x))
1464
+
1465
+ @torch.no_grad()
1466
+ def _clear_all_caches(self):
1467
+ for block in self.blocks:
1468
+ for module in block.modules():
1469
+ if hasattr(module, 'clear_cache'):
1470
+ module.clear_cache()
1471
+
1472
+ def _embed_input(self, ids: torch.Tensor, pos_start: int) -> torch.Tensor:
1473
+ x0 = self.embedder(ids)
1474
+ if self.learnable_pe:
1475
+ L = ids.size(1)
1476
+ x0 = x0 + self.pos_emb[:, pos_start:pos_start + L, :]
1477
+ if self.smear_gate:
1478
+ x0 = self.smear_gate(x0)
1479
+ return x0
1480
+
1481
+ def _blocks_generate(self, x: torch.Tensor, x0: torch.Tensor,
1482
+ xs, input_ids: torch.Tensor) -> torch.Tensor:
1483
+ """
1484
+ Mirror of forward()'s block loop, but calls block.generate(...) so
1485
+ that attention mixers populate / consume their KV cache.
1486
+ Works for both prefill (L>1) and single-token increments (L=1).
1487
+ """
1488
+ active_skips = {}
1489
+ for i, block in enumerate(self.blocks):
1490
+ ve = None
1491
+ if self.value_embeddings_cfg is not None and self.value_embeddings_cfg[i] is not None:
1492
+ group_id = self.value_embeddings_cfg[i]
1493
+ ve = self.ve_embeddings[str(group_id)](input_ids)
1494
+ ve = self._apply_ve_weight(ve, x, i)
1495
+
1496
+ if self.zeroskip and self.input_projections > 0:
1497
+ proj_params = self.inputs_projection_params[
1498
+ i * self.input_projections: (i + 1) * self.input_projections
1499
+ ]
1500
+ x = block.generate(x, ve=ve) + x0 * self.zeroskip_params[i] + sum(
1501
+ [xj * p for xj, p in zip(xs, proj_params)]
1502
+ )
1503
+ elif self.zeroskip:
1504
+ x = block.generate(x, ve=ve) + x0 * self.zeroskip_params[i]
1505
+ else:
1506
+ x = block.generate(x, ve=ve)
1507
+
1508
+ for source_idx, target_idx in enumerate(self.skips):
1509
+ if target_idx == i:
1510
+ gate = self.skip_lambdas[f"route_{source_idx}_to_{target_idx}"]
1511
+ x = x + gate * active_skips[source_idx]
1512
+ del active_skips[source_idx]
1513
+
1514
+ if self.skips[i] is not None:
1515
+ active_skips[i] = x
1516
+ return x
1517
+
1518
+ @torch.no_grad()
1519
+ def generate(self, prompt: torch.Tensor, max_len: int = 100,
1520
+ temperature: float = 1.0, top_k: int | None = None,
1521
+ top_p: float = 1.0, repetition_penalty: float = 1.0,
1522
+ token_end=None, simple: bool = False):
1523
+ """
1524
+ KV-cached AR generation. Prefills the prompt in one shot (every spatial
1525
+ mixer's .generate accepts L>1 and populates its kv_cache), then walks
1526
+ token-by-token reusing the cached keys/values.
1527
+
1528
+ simple=True falls back to simple_ar_generate (O(L^2)) — useful for
1529
+ regression-checking the cached path.
1530
+ """
1531
+ if simple:
1532
+ return simple_ar_generate(
1533
+ self, prompt, max_len=max_len, temperature=temperature,
1534
+ top_k=top_k, top_p=top_p, repetition_penalty=repetition_penalty,
1535
+ token_end=token_end,
1536
+ )
1537
+
1538
+ self.eval()
1539
+ self._clear_all_caches()
1540
+
1541
+ if self.value_embeddings_cfg is not None:
1542
+ flag("ModdedFormer.generate: value_embeddings only work with spatial mixers "
1543
+ "whose .generate accepts a 've=' kwarg (e.g. MultiheadAttentionMixer). "
1544
+ "On other mixers ve is silently dropped during generation.", 2)
1545
+
1546
+ if prompt.dim() == 1:
1547
+ prompt = prompt.unsqueeze(0)
1548
+ device = next(self.parameters()).device
1549
+ prompt = prompt.to(device)
1550
+
1551
+ L = prompt.size(1)
1552
+ x0 = self._embed_input(prompt, pos_start=0)
1553
+ xs = (self.inputs_projection_linears(x0).chunk(self.input_projections, dim=-1)
1554
+ if self.input_projections > 0 else None)
1555
+
1556
+ x = self._blocks_generate(x0, x0, xs, prompt)
1557
+ logits = self.unembedder(self.final_norm(x))[:, -1, :]
1558
+
1559
+ if repetition_penalty != 1.0:
1560
+ logits = apply_repetition_penalty(logits, prompt, repetition_penalty)
1561
+ next_token = sample_next_token(logits, temperature, top_k, top_p)
1562
+ generated = torch.cat([prompt, next_token], dim=1)
1563
+
1564
+ pos = L
1565
+ pbar = tqdm(range(max_len - 1), desc="ModdedFormer fast gen")
1566
+ for _ in pbar:
1567
+ x0_t = self._embed_input(next_token, pos_start=pos)
1568
+ xs_t = (self.inputs_projection_linears(x0_t).chunk(self.input_projections, dim=-1)
1569
+ if self.input_projections > 0 else None)
1570
+
1571
+ x_t = self._blocks_generate(x0_t, x0_t, xs_t, next_token)
1572
+ logits = self.unembedder(self.final_norm(x_t))[:, -1, :]
1573
+
1574
+ if repetition_penalty != 1.0:
1575
+ logits = apply_repetition_penalty(logits, generated, repetition_penalty)
1576
+ next_token = sample_next_token(logits, temperature, top_k, top_p)
1577
+ generated = torch.cat([generated, next_token], dim=1)
1578
+ pos += 1
1579
+
1580
+ if token_end is not None and (next_token == token_end).all():
1581
+ break
1582
+
1583
+ self._clear_all_caches()
1584
+ return generated
1585
+
1586
+ def _init_weights(self):
1587
+ std_embed = 0.02
1588
+ try:
1589
+ nn.init.normal_(self.embedder.embedding.weight, mean=0.0, std=std_embed)
1590
+ except:
1591
+ pass
1592
+ try:
1593
+ nn.init.normal_(self.unembedder.weight, mean=0.0, std=std_embed)
1594
+ except:
1595
+ pass
1596
+
1597
+ if self.learnable_pe:
1598
+ nn.init.normal_(self.pos_emb, mean=0.0, std=std_embed)
1599
+
1600
+ for block_idx, block in enumerate(self.blocks):
1601
+ block.channel_mixer._init_weights()
1602
+ try:
1603
+ block.spatial_mixer._init_weights()
1604
+ except:
1605
+ pass
1606
+
1607
+ def summary(self):
1608
+ print(f"Vathos {NUM}ModdedFormer{RES} Summary")
1609
+ print(f"Embedding dim: {self.embed_dim}")
1610
+ print(f"Learnable PE: {self.learnable_pe}")
1611
+ print(f"Skips: {self.skips}")
1612
+ for i in range(self.n_layer):
1613
+ if i == 0:
1614
+ in_dim = self.embed_dim
1615
+ else:
1616
+ in_dim = self.d_models[i - 1]
1617
+ out_dim = self.d_models[i]
1618
+ M = self.M_dims[i]
1619
+ print(f"Layer {i}: {in_dim} -> {out_dim}")
1620
+ print(f"Block: {i}: {self.blocks[i]}")
1621
+ print(f"\tFFN Dimension: {M}")
1622
+ print(f"\tFFN Activation: {self.blocks[i].channel_mixer.activation}")
1623
+
1624
+ print(f"Num Parameters: {NUM}{sum([p.numel() for p in self.parameters()]):_}{RES}")
1625
+ print(f"Num Trainable Parameters: {NUM}{sum([p.numel() for p in self.parameters() if p.requires_grad]):_}{RES}")
1626
+
1627
+
1628
+ #######################################################################################################################
1629
+
1630
+ """def test_causality(module=MTransformer(8, 4, 2)):
1631
+ torch.manual_seed(42)
1632
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
1633
+
1634
+ model = module
1635
+ x = torch.randn(2, 20, model.d_model, device=device)
1636
+
1637
+ print(f"\nInput shape: {x.shape}")
1638
+
1639
+ with torch.no_grad():
1640
+ output = model(x)
1641
+
1642
+ model.eval()
1643
+ with torch.no_grad():
1644
+ full_output = model(x)
1645
+
1646
+ for t in range(1, 20):
1647
+ prefix_output = model(x[:, :t, :])
1648
+
1649
+ max_diff = (full_output[:, :t, :] - prefix_output).abs().max().item()
1650
+
1651
+ if max_diff > 1e-5:
1652
+ print(f"Causality violated")
1653
+ break
1654
+ else:
1655
+ print("Causality verified")
1656
+
1657
+
1658
+ def test_causality_symbolic(module=SequenceModel(128, 16, 4, 2, pos_encoder=True)):
1659
+ torch.manual_seed(42)
1660
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
1661
+
1662
+ model = module
1663
+ x = torch.randint(0, model.vocab_size, (2, model.max_len), device=device)
1664
+
1665
+ print(f"\nInput shape: {x.shape}")
1666
+
1667
+ with torch.no_grad():
1668
+ output = model(x)
1669
+
1670
+ model.eval()
1671
+ with torch.no_grad():
1672
+ full_output = model(x, unembed=False)
1673
+
1674
+ for t in range(1, 20):
1675
+ prefix_output = model(x[:, :t], unembed=False)
1676
+
1677
+ max_diff = (full_output[:, :t] - prefix_output).abs().max().item()
1678
+
1679
+ if max_diff > 1e-5:
1680
+ print(f"Causality violated")
1681
+ break
1682
+ else:
1683
+ print("Causality verified")
1684
+
1685
+
1686
+ def test_symbolic_model(model):
1687
+ vocab_size = model.vocab_size
1688
+ length = model.max_len
1689
+ x = torch.randint(0, vocab_size, (2, length))
1690
+ print(f"Input shape {x.shape}")
1691
+ x = model(x)
1692
+ print(f"Output shape {x.shape}")
1693
+ print(f"Bounds: min:{x.min()}, max:{x.max()}, mean:{x.mean()}, std:{x.std()}, sum_of_a_vector: {x[0, 0, :].sum()}")"""
1694
+
1695
+ ########################################################################################################################
1696
+
1697
+ NAMES = {
1698
+ "MLP": MLP,
1699
+ "Attention": MultiheadAttentionMixer,
1700
+ "MHA": MultiheadAttentionMixer,
1701
+ "Embed": Embedder,
1702
+ "EMBED": Embedder,
1703
+ "E": Embedder,
1704
+ "PE": SinusoidalPositionalEncoding,
1705
+ "PatchEmbedder": PatchEmbedder,
1706
+ "ClassificationHead": MeanClassificationHead,
1707
+ "MCH": MeanClassificationHead,
1708
+ "ClsHead": ClsHead,
1709
+ }
1710
+
1711
+
1712
+ def expand_architecture(arch_string):
1713
+ pattern = r"(\(.+?\))x(\d+)"
1714
+
1715
+ def replacer(match):
1716
+ content = match.group(1)
1717
+ count = int(match.group(2))
1718
+ return " -> ".join([content] * count)
1719
+
1720
+ return re.sub(pattern, replacer, arch_string)
1721
+
1722
+
1723
+ def assemble(code, d_model=512):
1724
+ code = code.strip()
1725
+ if "x" in code:
1726
+ code = expand_architecture(code)
1727
+ divided = code.split("->")
1728
+
1729
+ for arch in divided:
1730
+ arch = arch.strip()
1731
+ if arch.startswith("("):
1732
+ layers = []
1733
+ for subarch in arch[1:-1].split(","):
1734
+ subarch = subarch.strip()
1735
+ if subarch in NAMES:
1736
+ layer = NAMES[subarch](d_model=d_model)
1737
+ else:
1738
+ raise ValueError(f"Unknown Layer: {subarch}")
1739
+ layers.append(subarch)
1740
+ block = Block1d(*layers)
1741
+
1742
+ else:
1743
+ if arch in NAMES:
1744
+ layer = NAMES[arch]
1745
+ elif hasattr(nn, arch):
1746
+ layer = getattr(nn, arch)
1747
+ else:
1748
+ raise ValueError(f"Unknown Layer: {arch}")
1749
+
1750
+
1751
+ def wrap(module):
1752
+ return tWrapper(module)
1753
+
1754
+
1755
+ def get_builder(layer, params):
1756
+ pass
1757
+
1758
+
1759
+ ########################################################################################################################
1760
+ # Pre Built
1761
+ ########################################################################################################################
1762
+
1763
+
1764
+ GQA = GroupedQueryAttention
1765
+ GQANOO = GroupedQueryAttentionNOO
1766
+ GQANOV = GroupedQueryAttentionNOV
1767
+ Attention = MultiheadAttentionMixer
1768
+ AttentionNOV = MultiheadAttentionMixerNOV
1769
+ NOVLa2 = MultiheadAttentionMixerNOVLa2
1770
+
1771
+
1772
+ # =============================================================================
1773
+ # HIGH-LEVEL MODELS
1774
+ # =============================================================================
1775
+ #
1776
+ # Modelli pronti all'uso, ottimizzati per training/inference efficienti. A
1777
+ # differenza di SequenceModel / ModdedFormer (pensati per studio + ablation),
1778
+ # qui ogni Python branch è pre-risolto al __init__, niente ParameterDict /
1779
+ # ModuleDict string-keyed, niente dict mutati nel forward. Tutto torch.compile-
1780
+ # friendly.
1781
+ #
1782
+ # d_model è fissa su tutti i layer (sceltà per semplicità + compile efficiency).
1783
+ # =============================================================================
1784
+
1785
+
1786
+ class BlockX0(Layer):
1787
+ """Pre-norm block + x0-skip nativo.
1788
+
1789
+ Forward(x, x0) -> x + spatial(norm1(x)) + channel(norm2(x)) + x0_lambda * x0
1790
+ `x0_lambda` init=0 ⇒ comportamento identico a Block1d al primo step.
1791
+ """
1792
+ __name__ = "BlockX0"
1793
+
1794
+ def __init__(self, d_model: int, channel_mixer: Layer, spatial_mixer: Layer, norm=RMSNorm):
1795
+ super().__init__()
1796
+ self.spatial_mixer = spatial_mixer
1797
+ self.channel_mixer = channel_mixer
1798
+ self.norm1 = norm(d_model)
1799
+ self.norm2 = norm(d_model)
1800
+ self.x0_lambda = nn.Parameter(torch.zeros(1))
1801
+
1802
+ def forward(self, x: torch.Tensor, x0: torch.Tensor) -> torch.Tensor:
1803
+ x = x + self.spatial_mixer(self.norm1(x))
1804
+ x = x + self.channel_mixer(self.norm2(x))
1805
+ return x + self.x0_lambda * x0
1806
+
1807
+ def generate(self, x: torch.Tensor, x0: torch.Tensor) -> torch.Tensor:
1808
+ h = self.norm1(x)
1809
+ sm = self.spatial_mixer
1810
+ x = x + (sm.generate(h) if sm.has_custom_generate() else sm(h))
1811
+ h = self.norm2(x)
1812
+ cm = self.channel_mixer
1813
+ x = x + (cm.generate(h) if cm.has_custom_generate() else cm(h))
1814
+ return x + self.x0_lambda * x0
1815
+
1816
+
1817
+ class PiCOBlock(BlockX0):
1818
+ """BlockX0 + value-embedding nativo (off di default, zero costo se off).
1819
+
1820
+ Quando `ve_enabled=True`, forward accetta un tensor `ve` esterno e lo passa
1821
+ al spatial_mixer pre-scalato per `ve_scale` (Parameter init=0 ⇒ no-op iniziale).
1822
+ """
1823
+ __name__ = "PiCOBlock"
1824
+
1825
+ def __init__(self, d_model: int, channel_mixer: Layer, spatial_mixer: Layer,
1826
+ norm=RMSNorm, ve_enabled: bool = False):
1827
+ super().__init__(d_model, channel_mixer, spatial_mixer, norm)
1828
+ self.ve_enabled = ve_enabled
1829
+ if ve_enabled:
1830
+ self.ve_scale = nn.Parameter(torch.zeros(1))
1831
+
1832
+ def forward(self, x: torch.Tensor, x0: torch.Tensor, ve: torch.Tensor = None) -> torch.Tensor:
1833
+ if self.ve_enabled and ve is not None:
1834
+ x = x + self.spatial_mixer(self.norm1(x), ve=ve * self.ve_scale)
1835
+ else:
1836
+ x = x + self.spatial_mixer(self.norm1(x))
1837
+ x = x + self.channel_mixer(self.norm2(x))
1838
+ return x + self.x0_lambda * x0
1839
+
1840
+ def generate(self, x: torch.Tensor, x0: torch.Tensor, ve: torch.Tensor = None) -> torch.Tensor:
1841
+ h = self.norm1(x)
1842
+ sm = self.spatial_mixer
1843
+ if self.ve_enabled and ve is not None:
1844
+ scaled = ve * self.ve_scale
1845
+ x = x + (sm.generate(h, ve=scaled) if sm.has_custom_generate() else sm(h, ve=scaled))
1846
+ else:
1847
+ x = x + (sm.generate(h) if sm.has_custom_generate() else sm(h))
1848
+ h = self.norm2(x)
1849
+ cm = self.channel_mixer
1850
+ x = x + (cm.generate(h) if cm.has_custom_generate() else cm(h))
1851
+ return x + self.x0_lambda * x0
1852
+
1853
+
1854
+ class PiCOFormer(VathosModel):
1855
+ """High-level transformer. Ultra-efficient, modular, x0-skip nativo.
1856
+
1857
+ Caratteristiche:
1858
+ - `d_model` fissa su tutti i layer.
1859
+ - Spatial mixers heterogeneous (lista di `Builder`, uno per layer).
1860
+ - x0-skip nativo via `BlockX0` / `PiCOBlock`.
1861
+ - Value embeddings opzionali (zero cost se off, indexed-by-layer).
1862
+ - Smear gate opzionale (Modded-NanoGPT style) su x0.
1863
+ - Final logit softcap.
1864
+ - Tied embeddings di default.
1865
+
1866
+ Args:
1867
+ vocab_size: vocab size.
1868
+ d_model: residual stream dim (fissa).
1869
+ n_layers: numero blocchi.
1870
+ spatials: `Builder` (condiviso) o `List[Builder]` di lunghezza n_layers.
1871
+ channel: `Builder` channel mixer (default VariableUDLP M=4*d_model, ReLU²).
1872
+ norm: classe norm (default RMSNorm).
1873
+ ve_groups: `List[int|None]` di lunghezza n_layers. `None` = no VE su quel layer;
1874
+ int = id gruppo (gruppi condividono lo stesso `nn.Embedding`).
1875
+ `None` globalmente = niente VE (default, niente memory cost).
1876
+ smear_gate: `nn.Module` o `None`. Applicato a x0 dopo l'embedding.
1877
+ logit_softcap: softcap finale. 0 disabilita.
1878
+ tied_embeddings: tying input/output embedding (default True).
1879
+ """
1880
+ __name__ = "PiCOFormer"
1881
+
1882
+ def __init__(self, vocab_size: int, d_model: int, n_layers: int,
1883
+ spatials, channel=None, norm=RMSNorm,
1884
+ ve_groups=None, smear_gate: nn.Module = None,
1885
+ smear_gate_lookback: int = 0,
1886
+ logit_softcap: float = 30.0, tied_embeddings: bool = True):
1887
+ super().__init__()
1888
+ self.vocab_size = vocab_size
1889
+ self.d_model = d_model
1890
+ self.n_layers = n_layers
1891
+ self.softcap = logit_softcap
1892
+
1893
+ # Spatials: lista di Builder o Builder singolo (condiviso)
1894
+ spatials_list = spatials if isinstance(spatials, list) else [spatials] * n_layers
1895
+ assert len(spatials_list) == n_layers, "len(spatials) must equal n_layers"
1896
+
1897
+ # Channel default = VariableUDLP, M=4*d_model, ReLU²
1898
+ if channel is None:
1899
+ channel = Builder(VariableUDLP, d_output=d_model, M=4 * d_model, activation=ReLU2)
1900
+
1901
+ # VE: pre-compute embedding condiviso per group + indice per layer
1902
+ self._has_any_ve = ve_groups is not None and any(g is not None for g in ve_groups)
1903
+ if self._has_any_ve:
1904
+ assert len(ve_groups) == n_layers, "len(ve_groups) must equal n_layers"
1905
+ unique_groups = sorted({g for g in ve_groups if g is not None})
1906
+ self.ve_embeddings = nn.ModuleList([nn.Embedding(vocab_size, d_model) for _ in unique_groups])
1907
+ group_to_idx = {g: i for i, g in enumerate(unique_groups)}
1908
+ self._ve_idx_per_layer = tuple(
1909
+ group_to_idx[g] if g is not None else -1 for g in ve_groups
1910
+ )
1911
+ else:
1912
+ self.ve_embeddings = nn.ModuleList()
1913
+ self._ve_idx_per_layer = (-1,) * n_layers
1914
+ self._ve_enabled_per_layer = tuple(idx >= 0 for idx in self._ve_idx_per_layer)
1915
+
1916
+ # Embeddings + final norm + unembedder
1917
+ self.embedder = Embedder(vocab_size, d_model)
1918
+ self.unembedder = UnbiasedLinear(d_model, vocab_size)
1919
+ if tied_embeddings:
1920
+ self.unembedder.linear.weight = self.embedder.embedding.weight
1921
+ self.final_norm = norm(d_model)
1922
+
1923
+ # Blocks
1924
+ self.blocks = nn.ModuleList([
1925
+ PiCOBlock(
1926
+ d_model,
1927
+ channel_mixer=channel(d_model),
1928
+ spatial_mixer=spatials_list[i](d_model),
1929
+ norm=norm,
1930
+ ve_enabled=self._ve_enabled_per_layer[i],
1931
+ )
1932
+ for i in range(n_layers)
1933
+ ])
1934
+
1935
+ # Smear gate opzionale (nn.Module [B,L,D] -> [B,L,D]).
1936
+ # smear_gate_lookback: 0 = stateless (per-token pointwise) — il fast path
1937
+ # generate() lo applica al singolo token. Se >0, il fast path mantiene una
1938
+ # rolling window di `lookback` token raw embeddings e ricomputa smear su
1939
+ # `window + new_token` (necessario per conv causali kernel>1, RNN, ecc.).
1940
+ self.smear_gate = smear_gate
1941
+ self._smear_lookback = int(smear_gate_lookback)
1942
+ self._x_window = None
1943
+ self._smear_gate_warned = False
1944
+
1945
+ self._init_weights()
1946
+
1947
+ def _init_weights(self):
1948
+ # Embeddings: normal std=0.02 (standard). LM head tied di default ⇒ stesso peso.
1949
+ std_embed = 0.02
1950
+ nn.init.normal_(self.embedder.embedding.weight, mean=0.0, std=std_embed)
1951
+ if self.unembedder.linear.weight is not self.embedder.embedding.weight:
1952
+ nn.init.normal_(self.unembedder.linear.weight, mean=0.0, std=std_embed)
1953
+ for emb in self.ve_embeddings:
1954
+ nn.init.normal_(emb.weight, mean=0.0, std=std_embed)
1955
+
1956
+ # Sub-modules: ciascuno definisce la propria policy di identity-init.
1957
+ if self.smear_gate is not None and hasattr(self.smear_gate, "_init_weights"):
1958
+ self.smear_gate._init_weights()
1959
+ for block in self.blocks:
1960
+ if hasattr(block.channel_mixer, "_init_weights"):
1961
+ block.channel_mixer._init_weights()
1962
+ if hasattr(block.spatial_mixer, "_init_weights"):
1963
+ block.spatial_mixer._init_weights()
1964
+ # x0_lambda (BlockX0) e ve_scale (PiCOBlock) sono già zero-init da nn.Parameter(torch.zeros(1)).
1965
+
1966
+ def _compute_ves(self, input_ids: torch.Tensor):
1967
+ """Pre-computa la lista di tensori VE per ogni layer (None se non abilitato)."""
1968
+ if not self._has_any_ve:
1969
+ return (None,) * self.n_layers
1970
+ # Computa unique embeddings UNA volta, poi distribuisce per layer
1971
+ unique_ves = [emb(input_ids) for emb in self.ve_embeddings]
1972
+ return tuple(unique_ves[idx] if idx >= 0 else None for idx in self._ve_idx_per_layer)
1973
+
1974
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1975
+ input_ids = x
1976
+ x0 = self.embedder(x)
1977
+ if self.smear_gate is not None:
1978
+ x0 = self.smear_gate(x0)
1979
+
1980
+ ves = self._compute_ves(input_ids)
1981
+
1982
+ x = x0
1983
+ for i, block in enumerate(self.blocks):
1984
+ x = block(x, x0, ve=ves[i])
1985
+
1986
+ logits = self.unembedder(self.final_norm(x))
1987
+ if self.softcap > 0:
1988
+ logits = self.softcap * torch.tanh(logits / self.softcap)
1989
+ return logits
1990
+
1991
+ @torch.no_grad()
1992
+ def _clear_caches(self):
1993
+ for block in self.blocks:
1994
+ for mod in block.modules():
1995
+ if hasattr(mod, "clear_cache"):
1996
+ mod.clear_cache()
1997
+ self._x_window = None
1998
+
1999
+ @torch.no_grad()
2000
+ def generate(self, prompt: torch.Tensor, max_len: int = 100,
2001
+ temperature: float = 1.0, top_k: int | None = None,
2002
+ top_p: float = 1.0, repetition_penalty: float = 1.0,
2003
+ token_end=None, simple: bool = False) -> torch.Tensor:
2004
+ """KV-cached AR generation. Prefill in un colpo, poi token-by-token."""
2005
+ if simple:
2006
+ return simple_ar_generate(
2007
+ self, prompt, max_len=max_len, temperature=temperature,
2008
+ top_k=top_k, top_p=top_p, repetition_penalty=repetition_penalty,
2009
+ token_end=token_end,
2010
+ )
2011
+
2012
+ self.eval()
2013
+ self._clear_caches()
2014
+
2015
+ # Warning una tantum se smear_gate è non-trivial ma lookback=0
2016
+ if (self.smear_gate is not None
2017
+ and not isinstance(self.smear_gate, nn.Identity)
2018
+ and self._smear_lookback == 0
2019
+ and not self._smear_gate_warned):
2020
+ import warnings
2021
+ warnings.warn(
2022
+ "PiCOFormer.generate(simple=False): smear_gate non-Identity con "
2023
+ "smear_gate_lookback=0. Se il modulo ha state temporale (conv "
2024
+ "kernel>1, RNN, ecc.), il fast path divergerà da simple=True. "
2025
+ "Setta smear_gate_lookback=<receptive_field-1> o usa simple=True.",
2026
+ RuntimeWarning, stacklevel=2,
2027
+ )
2028
+ self._smear_gate_warned = True
2029
+
2030
+ if prompt.dim() == 1:
2031
+ prompt = prompt.unsqueeze(0)
2032
+ device = next(self.parameters()).device
2033
+ prompt = prompt.to(device)
2034
+
2035
+ # --- Prefill --------------------------------------------------------
2036
+ x0_raw = self.embedder(prompt)
2037
+ if self.smear_gate is not None:
2038
+ x0 = self.smear_gate(x0_raw)
2039
+ if self._smear_lookback > 0:
2040
+ # Salva ultimi `lookback` raw embeddings come window (esclude i nuovi)
2041
+ self._x_window = x0_raw[:, -self._smear_lookback:, :].clone()
2042
+ else:
2043
+ x0 = x0_raw
2044
+ ves = self._compute_ves(prompt)
2045
+ x = x0
2046
+ for i, block in enumerate(self.blocks):
2047
+ x = block.generate(x, x0, ve=ves[i])
2048
+ logits = self.unembedder(self.final_norm(x))[:, -1, :]
2049
+ if self.softcap > 0:
2050
+ logits = self.softcap * torch.tanh(logits / self.softcap)
2051
+ if repetition_penalty != 1.0:
2052
+ logits = apply_repetition_penalty(logits, prompt, repetition_penalty)
2053
+ next_token = sample_next_token(logits, temperature, top_k, top_p)
2054
+ generated = torch.cat([prompt, next_token], dim=1)
2055
+
2056
+ # --- Step-by-step ---------------------------------------------------
2057
+ pbar = tqdm(range(max_len - 1), desc="PiCOFormer fast gen")
2058
+ for _ in pbar:
2059
+ x0_t_raw = self.embedder(next_token)
2060
+ if self.smear_gate is not None:
2061
+ if self._smear_lookback > 0:
2062
+ # combina window + new, applica smear, prendi ultimo token
2063
+ combined = torch.cat([self._x_window, x0_t_raw], dim=1)
2064
+ smeared = self.smear_gate(combined)
2065
+ x0_t = smeared[:, -1:, :]
2066
+ # aggiorna window: tieni gli ultimi `lookback` raw embeddings
2067
+ self._x_window = combined[:, -self._smear_lookback:, :]
2068
+ else:
2069
+ x0_t = self.smear_gate(x0_t_raw)
2070
+ else:
2071
+ x0_t = x0_t_raw
2072
+ ves_t = self._compute_ves(next_token)
2073
+ x_t = x0_t
2074
+ for i, block in enumerate(self.blocks):
2075
+ x_t = block.generate(x_t, x0_t, ve=ves_t[i])
2076
+ logits = self.unembedder(self.final_norm(x_t))[:, -1, :]
2077
+ if self.softcap > 0:
2078
+ logits = self.softcap * torch.tanh(logits / self.softcap)
2079
+ if repetition_penalty != 1.0:
2080
+ logits = apply_repetition_penalty(logits, generated, repetition_penalty)
2081
+ next_token = sample_next_token(logits, temperature, top_k, top_p)
2082
+ generated = torch.cat([generated, next_token], dim=1)
2083
+ if token_end is not None and (next_token == token_end).all():
2084
+ break
2085
+
2086
+ self._clear_caches()
2087
+ return generated
2088
+
2089
+
2090
+ if __name__ == "__main__":
2091
+ d_models = [64 for i in range(4)]
2092
+ m_dims = [128, 64, 32, 16]
2093
+ attn = Builder(GQA, n_heads=4, n_kv_heads=2)
2094
+ model = ModdedFormer(
2095
+ vocab_size=100,
2096
+ embed_dim=64,
2097
+ d_models=d_models,
2098
+ spatials=[attn for _ in range(len(d_models))],
2099
+ M_dims=m_dims,
2100
+ input_projections=3,
2101
+ zeroskip=True
2102
+ )
2103
+ model.summary()
2104
+
2105
+ out = model(
2106
+ torch.randint(0, 99, (2, 128))
2107
+ )
2108
+ print(out.shape)
2109
+
2110
+ model.profile()
vathos/complexity.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from collections import defaultdict
3
+ from typing import List, Tuple, Dict
4
+
5
+
6
+ def parse_complexity(complexity: str) -> Tuple[Dict[str, int], List[str]]:
7
+ """
8
+ Parse a Big O complexity string.
9
+
10
+ Returns:
11
+ Tuple of (variable_exponents dict, log_terms list)
12
+ where log_terms contains the variables inside log (e.g., ['n', 'L'])
13
+ """
14
+ match = re.search(r'O\((.*?)\)', complexity)
15
+ if not match:
16
+ return {}, []
17
+
18
+ content = match.group(1).strip()
19
+
20
+ log_terms = []
21
+ log_matches = re.findall(r'\blog\s+(\w+)', content, flags=re.IGNORECASE)
22
+ log_terms.extend(log_matches)
23
+
24
+ standalone_logs = re.findall(r'\blog(?!\s+\w)', content, flags=re.IGNORECASE)
25
+ log_terms.extend(['n'] * len(standalone_logs))
26
+
27
+ content_no_log = re.sub(r'\blog\s*\w*', '', content, flags=re.IGNORECASE)
28
+
29
+ exponents = defaultdict(int)
30
+ tokens = re.findall(r'([a-zA-Z_]\w*)(?:\^(\d+))?', content_no_log)
31
+
32
+ for var, exp in tokens:
33
+ if var and var.lower() != 'log':
34
+ exponent = int(exp) if exp else 1
35
+ exponents[var] = max(exponents[var], exponent)
36
+
37
+ return dict(exponents), log_terms
38
+
39
+
40
+ def compare_complexities(c1: Tuple[Dict[str, int], List[str]],
41
+ c2: Tuple[Dict[str, int], List[str]]) -> int:
42
+ """
43
+ Compare two complexities.
44
+ Returns: 1 if c1 > c2, -1 if c1 < c2, 0 if equal
45
+ """
46
+ exp1, log1 = c1
47
+ exp2, log2 = c2
48
+
49
+ all_vars = set(exp1.keys()) | set(exp2.keys())
50
+
51
+ for var in sorted(all_vars):
52
+ e1 = exp1.get(var, 0)
53
+ e2 = exp2.get(var, 0)
54
+ if e1 > e2:
55
+ return 1
56
+ elif e1 < e2:
57
+ return -1
58
+
59
+ if len(log1) > len(log2):
60
+ return 1
61
+ elif len(log1) < len(log2):
62
+ return -1
63
+
64
+ return 0
65
+
66
+
67
+ def combine_big_o_product(complexities: List[str]) -> str:
68
+ """
69
+ Combines multiple Big O complexity strings by taking the dominant complexity.
70
+
71
+ Args:
72
+ complexities: List of Big O notation strings
73
+
74
+ Returns:
75
+ Combined Big O notation representing the dominant complexity
76
+
77
+ Examples:
78
+ >>> combine_big_o(["O(n^2)", "O(n log n)", "O(n^3)"])
79
+ 'O(n^3)'
80
+ >>> combine_big_o(["O(n)", "O(n log n)"])
81
+ 'O(n log n)'
82
+ >>> combine_big_o(["O(L^2 d^2)", "O(L d^2 k)", "O(L^2 d^2)"])
83
+ 'O(L^2 d^2 k)'
84
+ """
85
+ if not complexities:
86
+ return "O(1)"
87
+
88
+ parsed = [parse_complexity(c) for c in complexities]
89
+
90
+ # Find the dominant complexity
91
+ dominant = parsed[0]
92
+ dominant_str = complexities[0]
93
+
94
+ for i in range(1, len(parsed)):
95
+ if compare_complexities(parsed[i], dominant) > 0:
96
+ dominant = parsed[i]
97
+ dominant_str = complexities[i]
98
+
99
+ # Find the maximum exponents for all variables
100
+ max_exponents = defaultdict(int)
101
+ for exponents, log_terms in parsed:
102
+ for var, exp in exponents.items():
103
+ max_exponents[var] = max(max_exponents[var], exp)
104
+
105
+ # Collect all log terms from complexities with maximum polynomial degree
106
+ all_log_terms = []
107
+ for exponents, log_terms in parsed:
108
+ has_max_degree = True
109
+
110
+ # Check if this has any variable at less than max exponent
111
+ for var, exp in exponents.items():
112
+ if exp < max_exponents[var]:
113
+ has_max_degree = False
114
+ break
115
+
116
+ # If it has max degree for its variables, keep its log terms
117
+ if has_max_degree:
118
+ all_log_terms.extend(log_terms)
119
+
120
+ if not max_exponents and not all_log_terms:
121
+ return "O(1)"
122
+
123
+ # Build the result string
124
+ terms = []
125
+
126
+ # Add variable terms
127
+ for var in sorted(max_exponents.keys()):
128
+ exp = max_exponents[var]
129
+ if exp == 1:
130
+ terms.append(var)
131
+ else:
132
+ terms.append(f"{var}^{exp}")
133
+
134
+ # Add log terms if applicable
135
+ if all_log_terms:
136
+ log_str = " ".join([f"log {var}" for var in sorted(set(all_log_terms))])
137
+ if terms:
138
+ terms.append(log_str)
139
+ else:
140
+ return f"O({log_str})"
141
+
142
+ return f"O({' '.join(terms)})"
143
+
144
+
145
+ from typing import List
146
+ from collections import defaultdict
147
+ import re
148
+
149
+
150
+ def combine_big_o_sum(complexities: List[str]) -> str:
151
+ """
152
+ Combines multiple Big O complexity strings by summing them and simplifying
153
+ like terms to return a human-readable dominant complexity.
154
+
155
+ Args:
156
+ complexities: List of Big O notation strings
157
+
158
+ Returns:
159
+ Combined Big O notation representing the simplified sum
160
+
161
+ Examples:
162
+ >>> combine_big_o_sum(["O(n^2)", "O(n)", "O(1)"])
163
+ 'O(n^2)'
164
+ >>> combine_big_o_sum(["O(L d^2)", "O(L d^2)", "O(L^2 d)"])
165
+ 'O(L d^2 + L^2 d)'
166
+ >>> combine_big_o_sum(["O(depth L d^2)", "O(L^2 d)", "O(L d^2)", "O(L d^2)", "O(L d)"])
167
+ 'O(L d^2 + L^2 d)'
168
+ """
169
+ if not complexities:
170
+ return "O(1)"
171
+
172
+ all_terms = []
173
+ for c in complexities:
174
+ term = c.strip()
175
+ if term.startswith("O(") and term.endswith(")"):
176
+ term = term[2:-1].strip()
177
+ if term and term != "1":
178
+ # Split by + to handle multiple terms in one complexity
179
+ all_terms.extend([t.strip() for t in term.split('+')])
180
+
181
+ if not all_terms:
182
+ return "O(1)"
183
+
184
+ term_groups = defaultdict(list)
185
+
186
+ for term in all_terms:
187
+ var_signature = []
188
+ parts = term.split()
189
+
190
+ for part in parts:
191
+ if part.replace('.', '').isdigit() or part in ['depth', 'batch', 'const']:
192
+ continue
193
+
194
+ # Parse variable with optional exponent
195
+ if '^' in part:
196
+ var, exp = part.split('^')
197
+ var_signature.append((var, int(exp)))
198
+ else:
199
+ var_signature.append((part, 1))
200
+
201
+ # Sort to create canonical signature
202
+ var_signature = tuple(sorted(var_signature))
203
+ term_groups[var_signature].append(term)
204
+
205
+ unique_terms = []
206
+ for sig, terms in term_groups.items():
207
+ representative = min(terms, key=lambda t: (len(t), t))
208
+ unique_terms.append((sig, representative))
209
+
210
+ if not unique_terms:
211
+ return "O(1)"
212
+
213
+ def complexity_score(sig_term):
214
+ sig, term = sig_term
215
+ if not sig:
216
+ return (0, 0, term)
217
+ # Total degree and max degree
218
+ total_degree = sum(exp for var, exp in sig)
219
+ max_degree = max(exp for var, exp in sig)
220
+ return (max_degree, total_degree, term)
221
+
222
+ unique_terms.sort(key=complexity_score, reverse=True)
223
+
224
+ # If clearly dominated, return just the dominant term(s)
225
+ if len(unique_terms) > 1:
226
+ top_score = complexity_score(unique_terms[0])
227
+ # Keep all terms with the same max complexity
228
+ result_terms = [term for sig_term in unique_terms
229
+ if complexity_score(sig_term)[0] == top_score[0]]
230
+ else:
231
+ result_terms = [unique_terms[0][1]]
232
+
233
+ # Build result
234
+ if len(result_terms) == 1:
235
+ return f"O({result_terms[0]})"
236
+ else:
237
+ return f"O({' + '.join(result_terms)})"
238
+
vathos/convolutions.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ from torch import Tensor
5
+
6
+
7
+ class FastShiftConv1d_K3(nn.Module):
8
+ def __init__(self, dim: int):
9
+ super().__init__()
10
+ self.w0 = nn.Parameter(torch.ones(dim))
11
+ self.w1 = nn.Parameter(torch.zeros(dim))
12
+ self.w2 = nn.Parameter(torch.zeros(dim))
13
+
14
+ def forward(self, x: Tensor) -> Tensor:
15
+ x1 = F.pad(x[:, :-1, :], (0, 0, 1, 0))
16
+ x2 = F.pad(x[:, :-2, :], (0, 0, 2, 0))
17
+
18
+ return (x * self.w0.to(x.dtype)) + (x1 * self.w1.to(x.dtype)) + (x2 * self.w2.to(x.dtype))
19
+
20
+
21
+ class FastShiftConv1d_K4(nn.Module):
22
+ def __init__(self, dim: int):
23
+ super().__init__()
24
+ self.w0 = nn.Parameter(torch.ones(dim))
25
+ self.w1 = nn.Parameter(torch.zeros(dim))
26
+ self.w2 = nn.Parameter(torch.zeros(dim))
27
+ self.w3 = nn.Parameter(torch.zeros(dim))
28
+
29
+ def forward(self, x: Tensor) -> Tensor:
30
+ x1 = F.pad(x[:, :-1, :], (0, 0, 1, 0))
31
+ x2 = F.pad(x[:, :-2, :], (0, 0, 2, 0))
32
+ x3 = F.pad(x[:, :-3, :], (0, 0, 3, 0))
33
+
34
+ return (x * self.w0.to(x.dtype)) + \
35
+ (x1 * self.w1.to(x.dtype)) + \
36
+ (x2 * self.w2.to(x.dtype)) + \
37
+ (x3 * self.w3.to(x.dtype))
38
+
39
+
40
+ class FastShiftConv1d_K2(nn.Module):
41
+ def __init__(self, dim: int):
42
+ super().__init__()
43
+ # Dirac/Identity Init: 1 for current token, 0 for past tokens
44
+ self.w0 = nn.Parameter(torch.ones(dim))
45
+ self.w1 = nn.Parameter(torch.zeros(dim))
46
+
47
+ def forward(self, x: Tensor) -> Tensor:
48
+ x1 = F.pad(x[:, :-1, :], (0, 0, 1, 0))
49
+
50
+ return (x * self.w0.to(x.dtype)) + (x1 * self.w1.to(x.dtype))
51
+
52
+
53
+ class RWKVTimeMix(nn.Module):
54
+ def __init__(self, dim: int):
55
+ super().__init__()
56
+ self.time_mix = nn.Parameter(torch.ones(dim, dtype=torch.float32))
57
+
58
+ def forward(self, x: Tensor) -> Tensor:
59
+ x_prev = F.pad(x[:, :-1, :], (0, 0, 1, 0))
60
+ mix = self.time_mix.to(x.dtype)
61
+
62
+ return x * mix + x_prev * (1.0 - mix)
63
+
64
+
65
+ class SmearGate2(nn.Module):
66
+ def __init__(self, dim: int, gate_dim: int = 12):
67
+ super().__init__()
68
+ self.gate_dim = min(gate_dim, dim)
69
+ self.W_g = nn.Parameter(torch.zeros(self.gate_dim, dim))
70
+ self.smear_lambda = nn.Parameter(torch.zeros(1))
71
+
72
+ def forward(self, x: Tensor) -> Tensor:
73
+ x_prev = F.pad(x[:, :-1, :], (0, 0, 1, 0))
74
+ x_slice = x[..., :self.gate_dim]
75
+ g = torch.sigmoid(torch.matmul(x_slice, self.W_g.to(x.dtype)))
76
+
77
+ return x + self.smear_lambda * g * x_prev
78
+
79
+
80
+ class SmearGateLookback3(nn.Module):
81
+ def __init__(self, dim: int, gate_dim: int = 12):
82
+ super().__init__()
83
+ self.gate_dim = min(gate_dim, dim)
84
+
85
+ self.W_g = nn.Parameter(torch.zeros(self.gate_dim, dim * 3))
86
+
87
+ self.smear_lambdas = nn.Parameter(torch.zeros(3))
88
+
89
+ def forward(self, x: Tensor) -> Tensor:
90
+ x_slice = x[..., :self.gate_dim]
91
+ g_all = torch.sigmoid(torch.matmul(x_slice, self.W_g.to(x.dtype)))
92
+
93
+ g1, g2, g3 = g_all.chunk(3, dim=-1)
94
+
95
+ x1 = F.pad(x[:, :-1, :], (0, 0, 1, 0))
96
+ x2 = F.pad(x[:, :-2, :], (0, 0, 2, 0))
97
+ x3 = F.pad(x[:, :-3, :], (0, 0, 3, 0))
98
+
99
+ return x + (self.smear_lambdas[0] * g1 * x1) \
100
+ + (self.smear_lambdas[1] * g2 * x2) \
101
+ + (self.smear_lambdas[2] * g3 * x3)
102
+
103
+
104
+ class ModdedSmearGate(nn.Module):
105
+ def __init__(self, gate_dim: int = 12):
106
+ super().__init__()
107
+ self.gate_dim = gate_dim
108
+
109
+ self.smear_gate = nn.Linear(gate_dim, 1, bias=False)
110
+
111
+ with torch.no_grad():
112
+ nn.init.zeros_(self.smear_gate.weight)
113
+
114
+ def forward(self, x: Tensor) -> Tensor:
115
+ x_prev = F.pad(x[:, :-1, :], (0, 0, 1, 0))
116
+ x_slice = x[..., :self.gate_dim]
117
+
118
+ g = torch.sigmoid(self.smear_gate(x_slice))
119
+ return x + g * (x_prev - x)
vathos/data.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from torch.utils.data import Dataset, DataLoader
2
+ from transformers import AutoTokenizer
3
+ from dataclasses import dataclass
4
+ from typing import List, Dict, Any, Union, Optional
5
+ from pathlib import Path
6
+
7
+
8
+ class TextDataset(Dataset):
9
+ """Text dataset supporting lists, files, and folder structures."""
10
+
11
+ def __init__(self, source: Union[List[str], str, Path]):
12
+ """
13
+ Args:
14
+ source: Can be:
15
+ - List of strings (text data)
16
+ - Path to single .txt file
17
+ - Path to folder containing .txt files (recursive)
18
+ """
19
+ self.texts = []
20
+ self.file_paths = [] # Track source files for debugging
21
+
22
+ if isinstance(source, list):
23
+ self.texts = source
24
+ self.file_paths = [None] * len(source)
25
+ else:
26
+ source = Path(source)
27
+ if source.is_file():
28
+ self._load_file(source)
29
+ elif source.is_dir():
30
+ self._load_directory(source)
31
+ else:
32
+ raise ValueError(f"Source not found: {source}")
33
+
34
+ if len(self.texts) == 0:
35
+ raise ValueError("No text data loaded!")
36
+
37
+ def _load_file(self, file_path: Path):
38
+ """Load a single text file."""
39
+ with open(file_path, 'r', encoding='utf-8') as f:
40
+ text = f.read().strip()
41
+ if text:
42
+ self.texts.append(text)
43
+ self.file_paths.append(str(file_path))
44
+
45
+ def _load_directory(self, dir_path: Path):
46
+ """Recursively load all .txt files from directory."""
47
+ txt_files = sorted(dir_path.rglob("*.txt"))
48
+
49
+ for file_path in txt_files:
50
+ try:
51
+ self._load_file(file_path)
52
+ except Exception as e:
53
+ print(f"Warning: Failed to load {file_path}: {e}")
54
+
55
+ def __len__(self):
56
+ return len(self.texts)
57
+
58
+ def __getitem__(self, idx):
59
+ return self.texts[idx]
60
+
61
+ def get_file_path(self, idx: int) -> Optional[str]:
62
+ """Get source file path for debugging."""
63
+ return self.file_paths[idx]
64
+
65
+
66
+ @dataclass
67
+ class TokenizerConfig:
68
+ """Tokenizer configuration."""
69
+ model_name: str = "bert-base-uncased"
70
+ max_length: int = 512
71
+ truncation: bool = True
72
+ padding: str = "max_length" # or "longest"
73
+ return_tensors: str = "pt"
74
+
75
+
76
+ class TokenizerWrapper:
77
+ """Wrapper for HuggingFace tokenizer with collating dataloader."""
78
+
79
+ def __init__(self, config: TokenizerConfig = None):
80
+ self.config = config or TokenizerConfig()
81
+ self.tokenizer = AutoTokenizer.from_pretrained(self.config.model_name)
82
+
83
+ def collate_fn(self, batch: List[str]) -> Dict[str, Any]:
84
+ """Collate function for DataLoader."""
85
+ return self.tokenizer(
86
+ batch,
87
+ max_length=self.config.max_length,
88
+ truncation=self.config.truncation,
89
+ padding=self.config.padding,
90
+ return_tensors=self.config.return_tensors
91
+ )
92
+
93
+ def decode(self, token_ids, skip_special_tokens: bool = True) -> Union[str, List[str]]:
94
+ """
95
+ Decode token IDs back to text.
96
+
97
+ Args:
98
+ token_ids: Single sequence or batch of token IDs (tensor or list)
99
+ skip_special_tokens: Whether to remove [CLS], [SEP], [PAD] etc.
100
+
101
+ Returns:
102
+ Decoded text string or list of strings
103
+ """
104
+ return self.tokenizer.decode(token_ids, skip_special_tokens=skip_special_tokens)
105
+
106
+ def batch_decode(self, token_ids, skip_special_tokens: bool = True) -> List[str]:
107
+ """
108
+ Decode a batch of token IDs back to texts.
109
+
110
+ Args:
111
+ token_ids: Batch of token IDs (shape: [batch_size, seq_len])
112
+ skip_special_tokens: Whether to remove special tokens
113
+
114
+ Returns:
115
+ List of decoded text strings
116
+ """
117
+ return self.tokenizer.batch_decode(token_ids, skip_special_tokens=skip_special_tokens)
118
+
119
+ def get_dataloader(
120
+ self,
121
+ source: Union[List[str], str, Path],
122
+ batch_size: int = 8,
123
+ shuffle: bool = True,
124
+ num_workers: int = 0
125
+ ) -> DataLoader:
126
+ """
127
+ Create a DataLoader with tokenization collation.
128
+
129
+ Args:
130
+ source: List of texts, file path, or directory path
131
+ batch_size: Batch size
132
+ shuffle: Whether to shuffle data
133
+ num_workers: Number of workers for data loading
134
+ """
135
+ dataset = TextDataset(source)
136
+ print(f"Loaded {len(dataset)} texts")
137
+
138
+ return DataLoader(
139
+ dataset,
140
+ batch_size=batch_size,
141
+ shuffle=shuffle,
142
+ num_workers=num_workers,
143
+ collate_fn=self.collate_fn
144
+ )
145
+
146
+
147
+ if __name__ == "__main__":
148
+ texts = "_tests"
149
+
150
+ config = TokenizerConfig(
151
+ model_name="bert-base-uncased",
152
+ max_length=32,
153
+ padding="longest"
154
+ )
155
+ wrapper = TokenizerWrapper(config)
156
+
157
+ print("=== Example 1: List of texts ===")
158
+ dataloader = wrapper.get_dataloader(texts, batch_size=2, shuffle=False)
159
+
160
+ for batch_idx, batch in enumerate(dataloader):
161
+ print(f"\nBatch {batch_idx}: {batch['input_ids'].shape}")
162
+
163
+ decoded_batch = wrapper.batch_decode(batch['input_ids'], skip_special_tokens=True)
164
+ print(f"Decoded texts: {decoded_batch}")
165
+
166
+ single_decoded = wrapper.decode(batch['input_ids'][0], skip_special_tokens=True)
167
+ print(f"First sequence: {single_decoded}")
168
+
169
+ with_special = wrapper.decode(batch['input_ids'][0], skip_special_tokens=False)
170
+ print(f"With special tokens: {with_special}")
vathos/functions.py ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ import time
4
+ import numpy as np
5
+
6
+ # matplotlib è usato solo dalle utility di plot/benchmark, non dal codice del modello.
7
+ # Lazy + optional import per non rompere l'import di Vathos in ambienti headless.
8
+ try:
9
+ import matplotlib.pyplot as plt
10
+ plt.style.use('ggplot')
11
+ except ImportError:
12
+ plt = None # le funzioni di plot solleveranno errore esplicito se chiamate
13
+
14
+ try:
15
+ from colorama import Fore
16
+
17
+ GOOD = Fore.GREEN
18
+ BAD = Fore.RED
19
+ RES = Fore.RESET
20
+ SEC = Fore.LIGHTBLACK_EX
21
+ HM = Fore.YELLOW
22
+ NUM = Fore.BLUE
23
+ except:
24
+ GOOD = ""
25
+ BAD = ""
26
+ RES = ""
27
+ SEC = ""
28
+ NUM = ""
29
+ HM = ""
30
+
31
+ FLAG_PASS = 3
32
+
33
+
34
+ def _require_matplotlib():
35
+ if plt is None:
36
+ raise ImportError(
37
+ "Funzione di plotting chiamata ma matplotlib non è installato. "
38
+ "Installa con `pip install matplotlib` per usarla."
39
+ )
40
+
41
+
42
+ def plot(*args, **kwargs):
43
+ _require_matplotlib()
44
+ for x_i in args:
45
+ plt.plot(x_i)
46
+ for x_i in kwargs:
47
+ plt.plot(kwargs[x_i], label=str(x_i))
48
+
49
+ plt.legend()
50
+ plt.show()
51
+
52
+
53
+ def cumsum(x):
54
+ return torch.cumsum(x, dim=1)
55
+
56
+
57
+ def batched_channelwise_conv1d(K, V):
58
+ """
59
+ Computes a convolution between two batched tensors
60
+ K: [B, L, d] kernel
61
+ V: [B, L, d] signal
62
+ Output: [B, L, d]
63
+ """
64
+ B, L, d = V.shape
65
+ V_reshaped = V.view(1, B * L, d)
66
+
67
+ K_reshaped = K.view(B * L, 1, d).flip(-1)
68
+
69
+ pad_total = d - 1
70
+ pad_left = pad_total // 2
71
+ pad_right = pad_total - pad_left
72
+ V_padded = F.pad(V_reshaped, (pad_left, pad_right))
73
+
74
+ output = F.conv1d(V_padded, K_reshaped, groups=B * L)
75
+
76
+ return output.view(B, L, d)
77
+
78
+
79
+ def holographic_binding(K, V):
80
+ """
81
+ Computes Batched Circular Convolution (Binding) using FFT.
82
+ This is O(d log d) and numerically stable.
83
+
84
+ K: [..., d] kernel
85
+ V: [..., d] signal
86
+ Output: [..., d]
87
+ """
88
+ V_f = torch.fft.rfft(V, dim=-1, norm='ortho')
89
+ K_f = torch.fft.rfft(K, dim=-1, norm='ortho')
90
+
91
+ output_f = V_f * K_f
92
+ output = torch.fft.irfft(output_f, n=V.shape[-1], dim=-1, norm='ortho')
93
+
94
+ return output
95
+
96
+
97
+ def power_weigthed_cumsum(x, a=0.999, rescale=True):
98
+ """
99
+ a torch implementation of a power weighted cumsum,
100
+ which can be applied to batched inputs of shapes [B, L, d] or [B, L, d1, d2], summing over L
101
+ """
102
+ if a - 1 == 0:
103
+ return cumsum(x)
104
+ if len(x.shape) == 3:
105
+ alpha_pow = torch.full([x.shape[1]], a, dtype=x.dtype, device=x.device).cumprod(dim=0).unsqueeze(0).unsqueeze(2)
106
+ elif len(x.shape) == 4:
107
+ alpha_pow = torch.full([x.shape[1]], a, dtype=x.dtype, device=x.device).cumprod(dim=0).unsqueeze(0).unsqueeze(
108
+ 2).unsqueeze(2)
109
+ else:
110
+ raise RuntimeError("x shapes mus be of form [B, L, d] or [B, L, d, d]")
111
+
112
+ return torch.cumsum(x / alpha_pow, dim=1) * alpha_pow * (alpha_pow[:, 0, ...] if rescale else 1)
113
+
114
+
115
+ def stable_power_weighted_cumsum(x: torch.Tensor, a: float = 0.999, rescale: bool = True) -> torch.Tensor:
116
+ B, L = x.shape[:2]
117
+
118
+ dtype = x.dtype
119
+ if L > 1024 and x.dtype == torch.float32:
120
+ x = x.to(torch.float64)
121
+ a_tensor = torch.tensor(a, dtype=torch.float64, device=x.device)
122
+ else:
123
+ a_tensor = torch.tensor(a, dtype=x.dtype, device=x.device)
124
+
125
+ t = torch.arange(1, L + 1, dtype=a_tensor.dtype, device=x.device)
126
+
127
+ alpha_pow = torch.pow(a_tensor, t)
128
+
129
+ reshape_dims = [1, L] + [1] * (len(x.shape) - 2)
130
+ alpha_pow = alpha_pow.view(*reshape_dims)
131
+
132
+ scaled_input = x / alpha_pow
133
+
134
+ s_cumulative = torch.cumsum(scaled_input, dim=1)
135
+
136
+ s = s_cumulative * alpha_pow
137
+
138
+ if rescale:
139
+ s = s * (1.0 - a_tensor)
140
+
141
+ return s.to(dtype)
142
+
143
+
144
+ def precompute_power_weigthed_cumsum(x, alpha_pow):
145
+ return torch.cumsum(x / alpha_pow, dim=1) * alpha_pow
146
+
147
+
148
+ def flag(text, level=1):
149
+ if level <= FLAG_PASS:
150
+ print(f"{SEC}||{HM}FLAG LV.{level}{SEC}||{HM} {text}{RES}")
151
+
152
+
153
+ def getname(obj):
154
+ return obj.__name__ if hasattr(obj, "__name__") else str(type(obj)).split('.')[-1]
155
+
156
+
157
+ def benchmark_ar_symbolic_model(model, n, input_shape):
158
+ """
159
+ Benchmarks the autoregressive symbolic model on three tasks: forward pass,
160
+ forward + backward, and forward + backward + optimizer step. Uses random
161
+ generated data. Reports average times and shows a bar plot.
162
+
163
+ Additionally, benchmarks the ratio of forward time / (forward + backward) time
164
+ with increasing sequence length L, and plots it.
165
+
166
+ Args:
167
+ model: The model to benchmark (e.g., SequenceModel instance).
168
+ n: Number of iterations to average over for each benchmark.
169
+ input_shape: Tuple (batch_size, seq_len) for initial input shape.
170
+ """
171
+ device = next(model.parameters()).device
172
+ is_cuda = device.type == 'cuda'
173
+ model.train() # Ensure model is in training mode for gradients
174
+ optimizer = torch.optim.Adam(model.parameters())
175
+
176
+ # Helper to sync if CUDA
177
+ def sync():
178
+ if is_cuda:
179
+ torch.cuda.synchronize()
180
+
181
+ # Part 1: Benchmark forward, forward+backward, forward+backward+opt
182
+ times_fwd = []
183
+ times_fb = []
184
+ times_fbo = []
185
+
186
+ for _ in range(n):
187
+ x = torch.randint(0, model.vocab_size, input_shape, device=device, requires_grad=False)
188
+
189
+ # Forward only
190
+ sync()
191
+ start = time.perf_counter()
192
+ out = model(x)
193
+ sync()
194
+ end = time.perf_counter()
195
+ times_fwd.append(end - start)
196
+
197
+ # Forward + backward
198
+ optimizer.zero_grad()
199
+ sync()
200
+ start = time.perf_counter()
201
+ out = model(x)
202
+ loss = out.sum() # Dummy loss
203
+ loss.backward()
204
+ sync()
205
+ end = time.perf_counter()
206
+ times_fb.append(end - start)
207
+
208
+ # Forward + backward + optimizer step
209
+ optimizer.zero_grad()
210
+ sync()
211
+ start = time.perf_counter()
212
+ out = model(x)
213
+ loss = out.sum()
214
+ loss.backward()
215
+ optimizer.step()
216
+ sync()
217
+ end = time.perf_counter()
218
+ times_fbo.append(end - start)
219
+
220
+ avg_fwd = np.mean(times_fwd)
221
+ avg_fb = np.mean(times_fb)
222
+ avg_fbo = np.mean(times_fbo)
223
+
224
+ print(f"Average Forward Time: {avg_fwd:.6f} s")
225
+ print(f"Average Forward + Backward Time: {avg_fb:.6f} s")
226
+ print(f"Average Forward + Backward + Opt Time: {avg_fbo:.6f} s")
227
+
228
+ # Bar plot for the three tasks
229
+ labels = ['Forward', 'Forward + Backward', 'Forward + Backward + Opt']
230
+ times = [avg_fwd, avg_fb, avg_fbo]
231
+ plt.figure(figsize=(8, 5))
232
+ plt.bar(labels, times, color=['blue', 'orange', 'green'])
233
+ plt.ylabel('Average Time (s)')
234
+ plt.title(f'Benchmark Averages over {n} Iterations')
235
+ plt.show()
236
+
237
+ # Part 2: Ratio of forward / (forward + backward) with increasing L
238
+ batch_size = input_shape[0]
239
+ Ls = [16, 32, 64, 128, 256, 512, 1024] # Reasonable sequence lengths, adjust as needed
240
+ ratios = []
241
+ avg_times_fwd = []
242
+ avg_times_fb = []
243
+ inner_n = max(10, n // 10) # Average over fewer iterations for speed, but at least 10
244
+
245
+ for L in Ls:
246
+ shape = (batch_size, L)
247
+ x = torch.randint(0, model.vocab_size, shape, device=device, requires_grad=False)
248
+
249
+ # Time forward (average over inner_n)
250
+ fwd_times = []
251
+ for _ in range(inner_n):
252
+ sync()
253
+ start = time.perf_counter()
254
+ out = model(x)
255
+ sync()
256
+ end = time.perf_counter()
257
+ fwd_times.append(end - start)
258
+ time_fwd = np.mean(fwd_times)
259
+ avg_times_fwd.append(time_fwd)
260
+
261
+ # Time forward + backward (average over inner_n)
262
+ fb_times = []
263
+ for _ in range(inner_n):
264
+ optimizer.zero_grad()
265
+ sync()
266
+ start = time.perf_counter()
267
+ out = model(x)
268
+ loss = out.sum()
269
+ loss.backward()
270
+ sync()
271
+ end = time.perf_counter()
272
+ fb_times.append(end - start)
273
+ time_fb = np.mean(fb_times)
274
+ avg_times_fb.append(time_fb)
275
+
276
+ ratio = time_fwd / time_fb if time_fb > 0 else 0
277
+ ratios.append(ratio)
278
+
279
+ # Plot the ratio
280
+ plt.figure(figsize=(8, 5))
281
+ plt.plot(Ls, ratios, marker='o', color='red')
282
+ plt.xlabel('Sequence Length L')
283
+ plt.ylabel('Time Forward / Time (Forward + Backward)')
284
+ plt.title('Efficiency Ratio vs Sequence Length')
285
+ plt.grid(True)
286
+ plt.show()
287
+
288
+ # Optional: Plot absolute times for reference
289
+ plt.figure(figsize=(8, 5))
290
+ plt.plot(Ls, avg_times_fwd, marker='o', label='Forward', color='blue')
291
+ plt.plot(Ls, avg_times_fb, marker='o', label='Forward + Backward', color='orange')
292
+ plt.xlabel('Sequence Length L')
293
+ plt.ylabel('Average Time (s)')
294
+ plt.title('Absolute Times vs Sequence Length')
295
+ plt.legend()
296
+ plt.grid(True)
297
+ plt.show()
298
+
299
+
300
+ def apply_repetition_penalty(logits: torch.Tensor, generated: torch.Tensor,
301
+ penalty: float) -> torch.Tensor:
302
+ """
303
+ Standard HF-style repetition penalty, vectorized.
304
+ logits: [B, V] (last-position logits)
305
+ generated: [B, L] (token ids generated so far, including prompt)
306
+ Divides logits>0 by penalty, multiplies logits<=0 by penalty, in-place-safe.
307
+ """
308
+ if penalty == 1.0:
309
+ return logits
310
+ score = torch.gather(logits, 1, generated)
311
+ score = torch.where(score > 0, score / penalty, score * penalty)
312
+ logits = logits.scatter(1, generated, score)
313
+ return logits
314
+
315
+
316
+ def sample_next_token(logits: torch.Tensor, temperature: float = 1.0,
317
+ top_k: int | None = None, top_p: float = 1.0) -> torch.Tensor:
318
+ """
319
+ Sample one token per row from [B, V] logits with temperature, top-k, top-p.
320
+ Returns [B, 1] long tensor.
321
+ """
322
+ logits = logits / max(temperature, 1e-8)
323
+
324
+ if top_k is not None and top_k > 0:
325
+ k = min(top_k, logits.size(-1))
326
+ v, _ = torch.topk(logits, k)
327
+ logits = logits.masked_fill(logits < v[:, [-1]], float('-inf'))
328
+
329
+ if top_p < 1.0:
330
+ sorted_logits, sorted_idx = torch.sort(logits, descending=True)
331
+ cum = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
332
+ remove = cum > top_p
333
+ remove[..., 1:] = remove[..., :-1].clone()
334
+ remove[..., 0] = 0
335
+ mask = remove.scatter(1, sorted_idx, remove)
336
+ logits = logits.masked_fill(mask, float('-inf'))
337
+
338
+ probs = F.softmax(logits, dim=-1)
339
+ return torch.multinomial(probs, num_samples=1)
340
+
341
+
342
+ @torch.no_grad()
343
+ def simple_ar_generate(model, prompt, max_len=100, temperature=1.0,
344
+ top_k=None, top_p=1.0, repetition_penalty=1.0,
345
+ token_end=None, verbose=False):
346
+ """
347
+ Dumb autoregressive generation for debugging.
348
+
349
+ Runs a full forward pass on the whole sequence each step, takes logits at the
350
+ last position, samples, appends. O(L^2) total — useless for big models, but it
351
+ works on *anything* whose forward(ids) returns [B, L, V] logits, with no
352
+ assumptions about KV caches, custom generate(), pos encoders, ve, skips, etc.
353
+ Good as a ground-truth reference when debugging more sophisticated generators.
354
+ """
355
+ flag("simple_ar_generate is O(L^2) and ignores KV caches: use it for debugging, "
356
+ "not for real inference. If your model has a fast .generate(), prefer that.", 2)
357
+ model.eval()
358
+ if prompt.dim() == 1:
359
+ prompt = prompt.unsqueeze(0)
360
+ device = next(model.parameters()).device
361
+ generated = prompt.clone().to(device)
362
+
363
+ for _ in range(max_len):
364
+ logits = model(generated)
365
+ next_logits = logits[:, -1, :]
366
+
367
+ if repetition_penalty != 1.0:
368
+ next_logits = apply_repetition_penalty(next_logits, generated, repetition_penalty)
369
+
370
+ next_token = sample_next_token(next_logits, temperature, top_k, top_p)
371
+ generated = torch.cat([generated, next_token], dim=1)
372
+
373
+ if verbose:
374
+ print(next_token.flatten().tolist())
375
+
376
+ if token_end is not None and (next_token == token_end).all():
377
+ break
378
+
379
+ return generated
380
+
381
+
382
+ def toeplitz_init(tensor: torch.Tensor, alpha: float, causal: bool = True, mul=0.1):
383
+ """
384
+ Initializes a square 2D tensor to a custom Toeplitz matrix with decaying kernels
385
+ generated from alpha^t for t=1...L, where L is the side length of the matrix.
386
+
387
+ If causal is True, the matrix is lower triangular (tril), containing values only
388
+ where row index i >= column index j.
389
+
390
+ Args:
391
+ tensor (torch.Tensor): The square 2D tensor to initialize in place.
392
+ alpha (float): The base for the exponential decay.
393
+ causal (bool, optional): If True, makes the matrix lower triangular. Defaults to False.
394
+
395
+ Returns:
396
+ torch.Tensor: The initialized tensor (modified in place).
397
+ """
398
+ if tensor.dim() != 2 or tensor.size(0) != tensor.size(1):
399
+ raise ValueError("Tensor must be a square 2D tensor.")
400
+
401
+ L = tensor.size(0)
402
+ i, j = torch.meshgrid(
403
+ torch.arange(L, device=tensor.device, dtype=tensor.dtype),
404
+ torch.arange(L, device=tensor.device, dtype=tensor.dtype),
405
+ indexing='ij'
406
+ )
407
+ dist = torch.abs(i - j)
408
+ t = dist + 1
409
+ matrix = alpha ** t
410
+
411
+ if causal:
412
+ matrix = torch.where(i >= j, matrix, torch.tensor(0.0, dtype=tensor.dtype, device=tensor.device))
413
+
414
+ with torch.no_grad():
415
+ tensor.copy_(matrix)
416
+
417
+ return tensor * mul
418
+
419
+
420
+ def zero_layer_mtp_loss( # TODO: Vibecoded for now
421
+ logits: torch.Tensor,
422
+ targets: torch.Tensor,
423
+ mtp_weights: list[float],
424
+ ignore_index: int = -100,
425
+ softcap_val: float = 30.0
426
+ ) -> torch.Tensor:
427
+ """
428
+ Production-grade 0-layer MTP loss.
429
+
430
+ Optimizations:
431
+ 1. Computes LogSumExp exactly once per token.
432
+ 2. Uses index gathering for target logits to minimize memory bandwidth.
433
+ 3. Casts to FP32 for numerical stability before reduction.
434
+ """
435
+ assert sum(mtp_weights) == 1, "the sum of MTP weights must be 1.0"
436
+
437
+ if softcap_val > 0.0:
438
+ logits = softcap_val * torch.tanh(logits / softcap_val)
439
+
440
+ lse = torch.logsumexp(logits, dim=-1).float()
441
+
442
+ total_loss = torch.tensor(0.0, device=logits.device, dtype=torch.float32)
443
+
444
+ for k, w in enumerate(mtp_weights):
445
+ if w <= 0.0:
446
+ continue
447
+
448
+ if k == 0:
449
+ valid_logits = logits
450
+ valid_targets = targets
451
+ valid_lse = lse
452
+ else:
453
+ valid_logits = logits[:, :-k]
454
+ valid_targets = targets[:, k:]
455
+ valid_lse = lse[:, :-k]
456
+
457
+ mask = (valid_targets != ignore_index)
458
+
459
+ safe_targets = torch.where(mask, valid_targets, torch.zeros_like(valid_targets))
460
+
461
+ target_logits = valid_logits.gather(
462
+ dim=-1,
463
+ index=safe_targets.unsqueeze(-1)
464
+ ).squeeze(-1).float()
465
+
466
+ ce_loss = valid_lse - target_logits
467
+
468
+ masked_loss = ce_loss * mask
469
+ num_valid_tokens = mask.sum().clamp(min=1)
470
+ loss_k = masked_loss.sum() / num_valid_tokens
471
+
472
+ total_loss += w * loss_k
473
+
474
+ return total_loss
475
+
476
+
477
+ def plot_tensor_diagnostics(
478
+ name: str,
479
+ tensor: torch.Tensor,
480
+ on_grad: bool = False,
481
+ top_k_svd: int | None = None,
482
+ bins: int = 100
483
+ ):
484
+
485
+ has_grad = on_grad and hasattr(tensor, 'grad') and tensor.grad is not None
486
+ rows = 2 if has_grad else 1
487
+ fig, axes = plt.subplots(rows, 2, figsize=(12, 5 * rows))
488
+ if rows == 1:
489
+ axes = np.expand_dims(axes, axis=0) # standardize indexing
490
+
491
+ def _compute_and_plot(t: torch.Tensor, row: int, title_prefix: str):
492
+ t_cpu = t.detach().float().cpu()
493
+
494
+ if t_cpu.ndim == 1:
495
+ mat = t_cpu.unsqueeze(0)
496
+ else:
497
+ mat = t_cpu.view(t_cpu.size(0), -1)
498
+
499
+ s_vals = torch.linalg.svdvals(mat).numpy()
500
+ if top_k_svd is not None:
501
+ s_vals = s_vals[:top_k_svd]
502
+
503
+ ax_svd = axes[row, 0]
504
+ ax_svd.plot(s_vals, marker='.', linestyle='-', color='b', markersize=4)
505
+ ax_svd.set_title(f"{title_prefix} Singular Values\nMax: {s_vals[0]:.4f}, Min: {s_vals[-1]:.4f}")
506
+ ax_svd.set_xlabel("Index")
507
+ ax_svd.set_ylabel("Singular Value $\sigma_i$")
508
+ ax_svd.set_yscale('log') # Log scale is crucial to see the long tail
509
+ ax_svd.grid(True, which="both", ls="--", alpha=0.5)
510
+
511
+ ax_hist = axes[row, 1]
512
+ vals_flat = t_cpu.numpy().flatten()
513
+ ax_hist.hist(vals_flat, bins=bins, color='g', alpha=0.7, log=True)
514
+ mean, std = vals_flat.mean(), vals_flat.std()
515
+ ax_hist.set_title(f"{title_prefix} Distribution\n$\mu$: {mean:.2e}, $\sigma$: {std:.2e}")
516
+ ax_hist.set_xlabel("Value")
517
+ ax_hist.set_ylabel("Count (Log Scale)")
518
+ ax_hist.grid(True, ls="--", alpha=0.5)
519
+
520
+ _compute_and_plot(tensor, row=0, title_prefix=f"Weight: {name}")
521
+
522
+ if has_grad:
523
+ _compute_and_plot(tensor.grad, row=1, title_prefix=f"Gradient: {name}")
524
+ elif on_grad:
525
+ print(f"Warning: on_grad=True for '{name}', but tensor.grad is None.")
526
+
527
+ plt.tight_layout()
528
+ plt.show()
529
+
530
+
531
+ import math
532
+
533
+ def compute_normalized_grad_norm(model: torch.nn.Module) -> float:
534
+ """
535
+ Returns the RMS gradient norm across all trainable parameters.
536
+
537
+ RMS = sqrt( sum(g_i^2) / N ) where N = total number of scalar gradients.
538
+
539
+ Because this *averages* rather than sums, the result is independent of
540
+ model size: a 1 M-param and a 100 M-param model both live in [0, ~1]
541
+ during healthy training, making cross-run comparisons meaningful.
542
+
543
+ Returns 0.0 if no gradients are present yet.
544
+ """
545
+ total_sq = 0.0
546
+ total_n = 0
547
+
548
+ for p in model.parameters():
549
+ if p.grad is not None:
550
+ total_sq += p.grad.detach().float().pow(2).sum().item()
551
+ total_n += p.grad.numel()
552
+
553
+ if total_n == 0:
554
+ return 0.0
555
+
556
+ return math.sqrt(total_sq / total_n)
557
+
558
+
559
+ def compute_layer_grad_norms(model: torch.nn.Module) -> dict[str, float]:
560
+ """
561
+ Returns a dict { layer_name -> RMS grad norm } for every named module
562
+ that owns at least one parameter with a gradient.
563
+
564
+ Same RMS normalisation as compute_normalized_grad_norm so per-layer
565
+ values are still comparable across models of different sizes.
566
+ Leaf modules only (skips container modules to avoid double-counting).
567
+ """
568
+ norms = {}
569
+
570
+ for name, module in model.named_modules():
571
+ # only leaf modules that directly own parameters
572
+ own_params = list(module.parameters(recurse=False))
573
+ if not own_params:
574
+ continue
575
+
576
+ sq_sum = 0.0
577
+ n = 0
578
+ for p in own_params:
579
+ if p.grad is not None:
580
+ sq_sum += p.grad.detach().float().pow(2).sum().item()
581
+ n += p.grad.numel()
582
+
583
+ if n > 0:
584
+ norms[name] = math.sqrt(sq_sum / n)
585
+
586
+ return norms
vathos/mamba.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from Vathos.blocks import *
2
+
3
+ try:
4
+ from mamba_ssm import Mamba
5
+ except ImportError:
6
+ raise ImportError("If you're trying to use Mamba, please install mamba-ssm, via pip install mamba-ssm. \n"
7
+ "probably you'll need to also install the right torch version:\n"
8
+ "pip install -q -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126")
9
+
10
+
11
+ class MambaMixer(Layer):
12
+ def __init__(self, d_model, expand=4, d_state=64, k=3):
13
+ super(MambaMixer).__init__()
14
+ self.expand = expand
15
+ self.d_model = d_model
16
+ self.d_state = d_state
17
+ self.k = k
18
+ self.mamba = Mamba(
19
+ d_model=d_model,
20
+ d_state=d_state,
21
+ d_conv=d_state,
22
+ expand=expand,
23
+ )
24
+
25
+ def forward(self, x):
26
+ x = self.mamba(x)
27
+ return x
vathos/optimizers.py ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import torch
3
+ import torch.optim as optim
4
+ from Vathos.functions import flag
5
+ from torch.optim import Optimizer, AdamW, SGD
6
+
7
+ try:
8
+ from muon import Muon
9
+ except ImportError:
10
+ flag("Muon library is not installed, usage of Muon dependent optimizer will not be possible", 2)
11
+
12
+
13
+ class ValueScheduler:
14
+ def __init__(self):
15
+ self.step = 0
16
+ self.value = 0
17
+ self.functions = {}
18
+ self.values = []
19
+ self.compiled = False
20
+
21
+ def set(self, f, l=0, u=float('inf')):
22
+ assert l < u
23
+ self.functions[(l, u)] = f
24
+
25
+ def compile(self, l, m):
26
+ if self.compiled:
27
+ flag("ValueScheduler Already Compiled, recompiling...")
28
+ self.values = []
29
+ for l, u in self.functions:
30
+ i = l
31
+ f = self.functions[(l, u)]
32
+ while i <= u:
33
+ self.values.append(f(i))
34
+
35
+ def get(self, step):
36
+ assert self.compiled
37
+ return self.values[step]
38
+
39
+
40
+ class SignSGD(optim.Optimizer):
41
+
42
+ def __init__(self, params, lr=0.01, rand_zero=True):
43
+ defaults = dict(lr=lr, rand_zero=rand_zero)
44
+ super().__init__(params, defaults)
45
+
46
+ @torch.no_grad()
47
+ def step(self, closure=None):
48
+ loss = None
49
+ if closure is not None:
50
+ with torch.enable_grad():
51
+ loss = closure()
52
+
53
+ for group in self.param_groups:
54
+ lr = group['lr']
55
+ rand_zero = group['rand_zero']
56
+
57
+ for p in group['params']:
58
+ if p.grad is None:
59
+ continue
60
+
61
+ grad = torch.sign(p.grad)
62
+
63
+ if rand_zero:
64
+ zero_mask = (grad == 0)
65
+ if zero_mask.any():
66
+ grad[zero_mask] = torch.randint(
67
+ 0, 2, (zero_mask.sum().item(),),
68
+ dtype=grad.dtype,
69
+ device=grad.device
70
+ ) * 2 - 1
71
+
72
+ p.add_(grad, alpha=-lr)
73
+
74
+ return loss
75
+
76
+
77
+ """
78
+ stochastic_sign_muon.py
79
+ =======================
80
+ StochasticSignMuon: per-step stochastic interpolation between Muon and SignSGD
81
+ for 2-D hidden weight matrices, with AdamW for everything else.
82
+
83
+ Design principle
84
+ ----------------
85
+ Both Muon and SignSGD operate on the *same* Nesterov momentum buffer:
86
+
87
+ buf ← β·buf + (1-β)·grad # EMA (identical for both)
88
+ pre = (1-β)·grad + β·buf # Nesterov blend (identical for both)
89
+
90
+ Muon branch : update = NS(pre) · scale # orthogonalise
91
+ Sign branch : update = sign(pre) / √n # sign-normalised
92
+
93
+ Because the momentum path is fully shared, τ can be freely scheduled — or even
94
+ changed every step — without resetting or corrupting any state.
95
+
96
+ Frobenius-norm alignment
97
+ ------------------------
98
+ Muon's NS output has F-norm ≈ √m for an m×n matrix (m = rows).
99
+ sign(pre) has F-norm = √(m·n).
100
+ Dividing by √n makes the Sign branch produce the same F-norm as Muon,
101
+ so the learning rate carries the same meaning in both branches.
102
+
103
+ Usage (single device)
104
+ ---------------------
105
+ from stochastic_sign_muon import SingleDeviceStochasticSignMuon
106
+
107
+ hidden_weights = [p for p in model.body.parameters() if p.ndim >= 2]
108
+ rest = (
109
+ [p for p in model.body.parameters() if p.ndim < 2]
110
+ + list(model.head.parameters())
111
+ + list(model.embed.parameters())
112
+ )
113
+
114
+ optimizer = SingleDeviceStochasticSignMuon(
115
+ [
116
+ dict(params=hidden_weights, use_muon=True, lr=0.02, weight_decay=0.01),
117
+ dict(params=rest, use_muon=False, lr=3e-4, betas=(0.9, 0.95),
118
+ weight_decay=0.01),
119
+ ],
120
+ tau=0.0, # start as pure Muon; schedule upward to inject Sign noise
121
+ )
122
+
123
+ # Inside the training loop you can change tau at any time:
124
+ optimizer.set_tau(0.3) # 30 % of steps will now be Sign steps
125
+ """
126
+
127
+ import torch
128
+ import torch.distributed as dist
129
+ try:
130
+ from muon import zeropower_via_newtonschulz5, adam_update
131
+ except ImportError:
132
+ flag("Unable to import muon, consider downloading it via pip install git+https://github.com/KellerJordan/Muon.git to use Muon backed optimizers")
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Shared update primitives
137
+ # ---------------------------------------------------------------------------
138
+
139
+ def _nesterov_momentum(
140
+ grad: torch.Tensor,
141
+ buf: torch.Tensor,
142
+ beta: float,
143
+ ) -> torch.Tensor:
144
+ """
145
+ EMA update followed by a Nesterov combination.
146
+
147
+ Modifies `buf` and `grad` in-place (safe inside @torch.no_grad).
148
+ Returns the Nesterov pre-update vector.
149
+ """
150
+ buf.lerp_(grad, 1.0 - beta) # buf ← β·buf + (1-β)·grad
151
+ return grad.lerp_(buf, beta) # returns (1-β)·grad + β·buf [Nesterov]
152
+
153
+
154
+ def _muon_branch(pre: torch.Tensor, ns_steps: int = 5) -> torch.Tensor:
155
+ """
156
+ Newton-Schulz orthogonalisation + anisotropic scaling.
157
+ Output F-norm ≈ √m for an m×n pre-update.
158
+ Returns a tensor of the same shape as `pre`.
159
+ """
160
+ u = pre.view(len(pre), -1) if pre.ndim == 4 else pre
161
+ u = zeropower_via_newtonschulz5(u, steps=ns_steps)
162
+ u = u * max(1.0, u.size(-2) / u.size(-1)) ** 0.5
163
+ return u
164
+
165
+
166
+ def _sign_branch(pre: torch.Tensor, rand_zero: bool = True) -> torch.Tensor:
167
+ """
168
+ Signed update normalised to match Muon's output F-norm.
169
+
170
+ sign(pre) has F-norm √(m·n). Dividing by √n yields √m, matching Muon.
171
+ Zero entries are randomised to ±1 when rand_zero=True.
172
+ Returns a tensor of the same shape as `pre`.
173
+ """
174
+ u = pre.view(len(pre), -1) if pre.ndim == 4 else pre
175
+
176
+ s = torch.sign(u)
177
+
178
+ if rand_zero:
179
+ zero_mask = s == 0
180
+ if zero_mask.any():
181
+ s[zero_mask] = (
182
+ torch.randint(
183
+ 0, 2,
184
+ (zero_mask.sum().item(),),
185
+ dtype=u.dtype,
186
+ device=u.device,
187
+ ) * 2 - 1
188
+ )
189
+
190
+ # Normalise: √(m·n) / √n = √m → same F-norm as the Muon branch
191
+ s = s / (u.size(-1) ** 0.5)
192
+ return s
193
+
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # Single-device variant
197
+ # ---------------------------------------------------------------------------
198
+
199
+ class SingleDeviceStochasticSignMuon(torch.optim.Optimizer):
200
+ """
201
+ Non-distributed stochastic Muon ↔ SignSGD interpolation with AdamW aux.
202
+
203
+ Parameters
204
+ ----------
205
+ param_groups : list[dict]
206
+ Each group must contain ``use_muon`` (bool).
207
+
208
+ use_muon=True groups recognise: lr, momentum, weight_decay
209
+ use_muon=False groups recognise: lr, betas, eps, weight_decay
210
+
211
+ tau : float
212
+ Probability of taking a Sign step instead of a Muon step. [0, 1].
213
+ 0.0 → pure Muon (default). 1.0 → pure SignSGD.
214
+ Schedule via :meth:`set_tau`. No state reset required.
215
+
216
+ sign_lr_scale : float
217
+ Multiplicative downscale applied to lr when the Sign branch fires.
218
+ Compensates for Sign's lower signal-to-noise ratio vs Muon at equal
219
+ F-norm. Default 0.1; tune in [0.05, 0.2].
220
+ Schedule via :meth:`set_sign_lr_scale`. No state reset required.
221
+ Weight decay is always applied at the base group lr regardless.
222
+
223
+ rand_zero : bool
224
+ Randomise zero-gradient sign entries in the Sign branch (default True).
225
+
226
+ nesterov : bool
227
+ Use Nesterov momentum (default True, matching canonical Muon).
228
+
229
+ ns_steps : int
230
+ Newton-Schulz iteration count for the Muon branch (default 5).
231
+ """
232
+
233
+ def __init__(
234
+ self,
235
+ param_groups,
236
+ tau: float = 0.0,
237
+ sign_lr_scale: float = 0.1,
238
+ rand_zero: bool = True,
239
+ nesterov: bool = True,
240
+ ns_steps: int = 5,
241
+ ):
242
+ assert 0.0 <= tau <= 1.0, f"tau must be in [0, 1], got {tau}"
243
+ assert sign_lr_scale > 0.0, f"sign_lr_scale must be > 0, got {sign_lr_scale}"
244
+
245
+ self.tau = tau
246
+ self.sign_lr_scale = sign_lr_scale
247
+ self.rand_zero = rand_zero
248
+ self.nesterov = nesterov
249
+ self.ns_steps = ns_steps
250
+
251
+ for group in param_groups:
252
+ assert "use_muon" in group, "Every param group must have a 'use_muon' key."
253
+ if group["use_muon"]:
254
+ group.setdefault("lr", 0.02)
255
+ group.setdefault("momentum", 0.95)
256
+ group.setdefault("weight_decay", 0.0)
257
+ allowed = {"params", "lr", "momentum", "weight_decay", "use_muon"}
258
+ assert set(group.keys()) == allowed, (
259
+ f"Unexpected keys in Muon group: {set(group.keys()) - allowed}"
260
+ )
261
+ else:
262
+ group.setdefault("lr", 3e-4)
263
+ group.setdefault("betas", (0.9, 0.95))
264
+ group.setdefault("eps", 1e-10)
265
+ group.setdefault("weight_decay", 0.0)
266
+ allowed = {"params", "lr", "betas", "eps", "weight_decay", "use_muon"}
267
+ assert set(group.keys()) == allowed, (
268
+ f"Unexpected keys in Adam group: {set(group.keys()) - allowed}"
269
+ )
270
+
271
+ super().__init__(param_groups, {})
272
+
273
+ # ------------------------------------------------------------------
274
+ # Scheduling API
275
+ # ------------------------------------------------------------------
276
+
277
+ def set_tau(self, tau: float) -> None:
278
+ """
279
+ Set the Muon ↔ Sign mixing probability.
280
+ Safe to call at any point; does not reset any optimizer state.
281
+ """
282
+ assert 0.0 <= tau <= 1.0, f"tau must be in [0, 1], got {tau}"
283
+ self.tau = tau
284
+
285
+ def set_sign_lr_scale(self, scale: float) -> None:
286
+ """
287
+ Set the lr multiplier for Sign steps.
288
+ Safe to call at any point; does not reset any optimizer state.
289
+ """
290
+ assert scale > 0.0, f"sign_lr_scale must be > 0, got {scale}"
291
+ self.sign_lr_scale = scale
292
+
293
+ # ------------------------------------------------------------------
294
+ # Step
295
+ # ------------------------------------------------------------------
296
+
297
+ @torch.no_grad()
298
+ def step(self, closure=None):
299
+ loss = None
300
+ if closure is not None:
301
+ with torch.enable_grad():
302
+ loss = closure()
303
+
304
+ # One global Bernoulli draw: all 2-D params follow the same branch this step.
305
+ use_sign: bool = (torch.rand(1).item() < self.tau)
306
+
307
+ for group in self.param_groups:
308
+
309
+ if group["use_muon"]:
310
+ beta = group["momentum"]
311
+ base_lr = group["lr"]
312
+ effective_lr = base_lr * self.sign_lr_scale if use_sign else base_lr
313
+
314
+ for p in group["params"]:
315
+ if p.grad is None:
316
+ p.grad = torch.zeros_like(p)
317
+
318
+ state = self.state[p]
319
+ if not state:
320
+ state["momentum_buffer"] = torch.zeros_like(p)
321
+
322
+ # Shared momentum step (identical regardless of branch)
323
+ pre = _nesterov_momentum(p.grad, state["momentum_buffer"], beta)
324
+
325
+ # Branch selection
326
+ if use_sign:
327
+ update = _sign_branch(pre, rand_zero=self.rand_zero)
328
+ else:
329
+ update = _muon_branch(pre, ns_steps=self.ns_steps)
330
+
331
+ # Weight decay at base lr; parameter update at effective lr
332
+ p.mul_(1.0 - base_lr * group["weight_decay"])
333
+ p.add_(update.reshape(p.shape), alpha=-effective_lr)
334
+
335
+ else: # AdamW
336
+ for p in group["params"]:
337
+ if p.grad is None:
338
+ p.grad = torch.zeros_like(p)
339
+
340
+ state = self.state[p]
341
+ if not state:
342
+ state["exp_avg"] = torch.zeros_like(p)
343
+ state["exp_avg_sq"] = torch.zeros_like(p)
344
+ state["step"] = 0
345
+
346
+ state["step"] += 1
347
+ update = adam_update(
348
+ p.grad,
349
+ state["exp_avg"],
350
+ state["exp_avg_sq"],
351
+ state["step"],
352
+ group["betas"],
353
+ group["eps"],
354
+ )
355
+ p.mul_(1.0 - group["lr"] * group["weight_decay"])
356
+ p.add_(update, alpha=-group["lr"])
357
+
358
+ return loss
359
+
360
+
361
+ # ---------------------------------------------------------------------------
362
+ # Distributed variant
363
+ # ---------------------------------------------------------------------------
364
+
365
+ class StochasticSignMuon(torch.optim.Optimizer):
366
+ """
367
+ Distributed stochastic Muon ↔ SignSGD interpolation with AdamW aux.
368
+
369
+ Requires an initialised ``torch.distributed`` process group.
370
+ Parameters are sorted by size and sharded across ranks exactly as in the
371
+ original ``MuonWithAuxAdam``.
372
+
373
+ The Bernoulli draw is made on rank 0 and broadcast to all ranks so that
374
+ every GPU takes the same branch on every step.
375
+
376
+ See ``SingleDeviceStochasticSignMuon`` for the full parameter docstring.
377
+ """
378
+
379
+ def __init__(
380
+ self,
381
+ param_groups,
382
+ tau: float = 0.0,
383
+ sign_lr_scale: float = 0.1,
384
+ rand_zero: bool = True,
385
+ nesterov: bool = True,
386
+ ns_steps: int = 5,
387
+ ):
388
+ assert 0.0 <= tau <= 1.0, f"tau must be in [0, 1], got {tau}"
389
+ assert sign_lr_scale > 0.0, f"sign_lr_scale must be > 0, got {sign_lr_scale}"
390
+
391
+ self.tau = tau
392
+ self.sign_lr_scale = sign_lr_scale
393
+ self.rand_zero = rand_zero
394
+ self.nesterov = nesterov
395
+ self.ns_steps = ns_steps
396
+
397
+ for group in param_groups:
398
+ assert "use_muon" in group, "Every param group must have a 'use_muon' key."
399
+ if group["use_muon"]:
400
+ group["params"] = sorted(
401
+ group["params"], key=lambda x: x.size(), reverse=True
402
+ )
403
+ group.setdefault("lr", 0.02)
404
+ group.setdefault("momentum", 0.95)
405
+ group.setdefault("weight_decay", 0.0)
406
+ allowed = {"params", "lr", "momentum", "weight_decay", "use_muon"}
407
+ assert set(group.keys()) == allowed
408
+ else:
409
+ group.setdefault("lr", 3e-4)
410
+ group.setdefault("betas", (0.9, 0.95))
411
+ group.setdefault("eps", 1e-10)
412
+ group.setdefault("weight_decay", 0.0)
413
+ allowed = {"params", "lr", "betas", "eps", "weight_decay", "use_muon"}
414
+ assert set(group.keys()) == allowed
415
+
416
+ super().__init__(param_groups, {})
417
+
418
+ # ------------------------------------------------------------------
419
+ # Scheduling API
420
+ # ------------------------------------------------------------------
421
+
422
+ def set_tau(self, tau: float) -> None:
423
+ """Schedule the Muon ↔ Sign mixing probability. Safe from any rank."""
424
+ assert 0.0 <= tau <= 1.0, f"tau must be in [0, 1], got {tau}"
425
+ self.tau = tau
426
+
427
+ def set_sign_lr_scale(self, scale: float) -> None:
428
+ """Schedule the lr multiplier for Sign steps. Safe from any rank."""
429
+ assert scale > 0.0, f"sign_lr_scale must be > 0, got {scale}"
430
+ self.sign_lr_scale = scale
431
+
432
+ # ------------------------------------------------------------------
433
+ # Step
434
+ # ------------------------------------------------------------------
435
+
436
+ @torch.no_grad()
437
+ def step(self, closure=None):
438
+ loss = None
439
+ if closure is not None:
440
+ with torch.enable_grad():
441
+ loss = closure()
442
+
443
+ world_size = dist.get_world_size()
444
+ rank = dist.get_rank()
445
+
446
+ # Broadcast the Bernoulli draw from rank 0 so every GPU takes the same branch.
447
+ use_sign_t = torch.zeros(1, dtype=torch.float32,
448
+ device=torch.device("cuda", rank))
449
+ if rank == 0:
450
+ use_sign_t[0] = 1.0 if torch.rand(1).item() < self.tau else 0.0
451
+ dist.broadcast(use_sign_t, src=0)
452
+ use_sign: bool = use_sign_t.item() > 0.5
453
+
454
+ for group in self.param_groups:
455
+
456
+ if group["use_muon"]:
457
+ beta = group["momentum"]
458
+ base_lr = group["lr"]
459
+ effective_lr = base_lr * self.sign_lr_scale if use_sign else base_lr
460
+
461
+ params = group["params"]
462
+ params_pad = params + [torch.empty_like(params[-1])] * (
463
+ world_size - len(params) % world_size
464
+ )
465
+
466
+ for base_i in range(0, len(params), world_size):
467
+ local_idx = base_i + rank
468
+ if local_idx < len(params):
469
+ p = params[local_idx]
470
+ if p.grad is None:
471
+ p.grad = torch.zeros_like(p)
472
+
473
+ state = self.state[p]
474
+ if not state:
475
+ state["momentum_buffer"] = torch.zeros_like(p)
476
+
477
+ pre = _nesterov_momentum(
478
+ p.grad, state["momentum_buffer"], beta
479
+ )
480
+
481
+ if use_sign:
482
+ update = _sign_branch(pre, rand_zero=self.rand_zero)
483
+ else:
484
+ update = _muon_branch(pre, ns_steps=self.ns_steps)
485
+
486
+ # Weight decay at base lr; parameter update at effective lr
487
+ p.mul_(1.0 - base_lr * group["weight_decay"])
488
+ p.add_(update.reshape(p.shape), alpha=-effective_lr)
489
+
490
+ dist.all_gather(
491
+ params_pad[base_i: base_i + world_size],
492
+ params_pad[base_i + rank],
493
+ )
494
+
495
+ else: # AdamW
496
+ for p in group["params"]:
497
+ if p.grad is None:
498
+ p.grad = torch.zeros_like(p)
499
+
500
+ state = self.state[p]
501
+ if not state:
502
+ state["exp_avg"] = torch.zeros_like(p)
503
+ state["exp_avg_sq"] = torch.zeros_like(p)
504
+ state["step"] = 0
505
+
506
+ state["step"] += 1
507
+ update = adam_update(
508
+ p.grad,
509
+ state["exp_avg"],
510
+ state["exp_avg_sq"],
511
+ state["step"],
512
+ group["betas"],
513
+ group["eps"],
514
+ )
515
+ p.mul_(1.0 - group["lr"] * group["weight_decay"])
516
+ p.add_(update, alpha=-group["lr"])
517
+
518
+ return loss
vathos/training.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tqdm.notebook import tqdm
2
+ from Vathos.blocks import *
3
+
4
+
5
+ def symbolic_1d_ar_target_train(model, train_loader, val_loader, optimizer, scheduler, criterion,
6
+ device, NUM_EPOCHS=100, use_amp=False, CLIP_NORM=1.0, div=1):
7
+ scaler = torch.cuda.amp.GradScaler(enabled=use_amp)
8
+
9
+ steps_per_epoch = len(train_loader) // div
10
+
11
+ for epoch in range(NUM_EPOCHS):
12
+ model.train()
13
+ running_loss = 0.0
14
+ pbar = tqdm(train_loader, desc=f"Epoch {epoch + 1}/{NUM_EPOCHS} Train", total=steps_per_epoch)
15
+ steps = 0
16
+
17
+ for batch_x, batch_tgt in pbar:
18
+ batch_x = batch_x.to(device)
19
+ steps += 1
20
+
21
+ optimizer.zero_grad()
22
+ with torch.amp.autocast('cuda'):
23
+ logits = model(batch_x) # [B,S,V]
24
+ loss = criterion(
25
+ logits[:, :-1, :].reshape(-1, logits.size(-1)),
26
+ batch_x[:, 1:].reshape(-1)
27
+ )
28
+
29
+ scaler.scale(loss).backward()
30
+
31
+ scaler.unscale_(optimizer)
32
+ torch.nn.utils.clip_grad_norm_(model.parameters(), CLIP_NORM)
33
+ scaler.step(optimizer)
34
+ scaler.update()
35
+
36
+ scheduler.step()
37
+
38
+ running_loss += loss.item()
39
+ lr_now = optimizer.param_groups[0]['lr']
40
+ pbar.set_postfix({'loss': running_loss / (pbar.n + 1), 'lr': lr_now})
41
+
42
+ if steps >= steps_per_epoch:
43
+ break
44
+
45
+ model.eval()
46
+ val_loss = 0.0
47
+ # with torch.no_grad():
48
+ if True:
49
+ for batch_x, batch_tgt, composer_idx in tqdm(val_loader, desc=f"Epoch {epoch + 1}/{NUM_EPOCHS} Val"):
50
+ batch_x = batch_x.to(device).requires_grad_(False)
51
+
52
+ logits = model(batch_x)
53
+ loss = criterion(
54
+ logits[:, :-1, :].reshape(-1, logits.size(-1)),
55
+ batch_x[:, 1:].reshape(-1)
56
+ )
57
+ val_loss += loss.item()
58
+ if isinstance(model, VathosModel):
59
+ model.module.register_loss(loss.item())
60
+
61
+ val_avg = val_loss / len(val_loader)
62
+ if isinstance(model, VathosModel):
63
+ model.module.register_epoch()
64
+ print(f"Epoch {epoch + 1} val avg loss: {val_avg:.4f}")
65
+
66
+
67
+ def symbolic_1d_ar_input_ids_train(model, train_loader, val_loader, optimizer, scheduler, criterion,
68
+ device, NUM_EPOCHS=100, use_amp=False, CLIP_NORM=1.0, spe=1000, val_steps=float('inf')):
69
+ scaler = torch.cuda.amp.GradScaler(enabled=use_amp)
70
+
71
+ steps_per_epoch = spe
72
+
73
+ for epoch in range(NUM_EPOCHS):
74
+ model.train()
75
+ running_loss = 0.0
76
+ pbar = tqdm(train_loader, desc=f"Epoch {epoch + 1}/{NUM_EPOCHS} Train", total=steps_per_epoch)
77
+ steps = 0
78
+
79
+ for batch_x in pbar:
80
+ batch_x = batch_x['input_ids'].to(device)
81
+ steps += 1
82
+
83
+ optimizer.zero_grad()
84
+ with torch.amp.autocast('cuda'):
85
+ logits = model(batch_x) # [B,S,V]
86
+ loss = criterion(
87
+ logits[:, :-1, :].reshape(-1, logits.size(-1)),
88
+ batch_x[:, 1:].reshape(-1)
89
+ )
90
+
91
+ scaler.scale(loss).backward()
92
+
93
+ scaler.unscale_(optimizer)
94
+ torch.nn.utils.clip_grad_norm_(model.parameters(), CLIP_NORM)
95
+ scaler.step(optimizer)
96
+ scaler.update()
97
+ if scheduler is not None:
98
+ scheduler.step()
99
+
100
+ running_loss += loss.item()
101
+ lr_now = optimizer.param_groups[0]['lr']
102
+ pbar.set_postfix({'loss': running_loss / (pbar.n + 1), 'lr': lr_now})
103
+
104
+ if steps >= steps_per_epoch:
105
+ break
106
+
107
+ model.eval()
108
+ val_loss = 0.0
109
+ with torch.no_grad():
110
+ # if True:
111
+ val_s = 0
112
+ for batch_x in tqdm(val_loader, desc=f"Epoch {epoch + 1}/{NUM_EPOCHS} Val"):
113
+ batch_x = batch_x['input_ids'].to(device).requires_grad_(False)
114
+ val_s += 1
115
+ if val_s >= val_steps:
116
+ break
117
+ logits = model(batch_x)
118
+ loss = criterion(
119
+ logits[:, :-1, :].reshape(-1, logits.size(-1)),
120
+ batch_x[:, 1:].reshape(-1)
121
+ )
122
+ model.module.register_loss(loss.item())
123
+
124
+ val_avg = model.module.get_mean_loss()
125
+ model.module.register_epoch()
126
+ print(f"Epoch {epoch + 1} val avg loss: {val_avg:.4f}")
127
+
128
+
129
+