RDTvlokip commited on
Commit
9aab684
·
verified ·
1 Parent(s): 855c7b5

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: fr
3
+ license: apache-2.0
4
+ tags:
5
+ - text-generation
6
+ - from-scratch
7
+ - looped-transformer
8
+ - adaptive-computation
9
+ - french
10
+ pipeline_tag: text-generation
11
+ ---
12
+
13
+ # Cadence-15M-fr 🔁
14
+
15
+ > ⚠️ **Preliminary results — 1 seed, variance not controlled.** Every number below is a single training run evaluated once (50 prompts × 200 tokens, fixed seed). Directions are consistent, magnitudes are not validated. Treat as a lab notebook, not a benchmark.
16
+
17
+ **Cadence** is a 15M-parameter French language model trained **from scratch** (no 🤗 Transformers, no pretrained weights) on a single GTX 1080 Ti. It is the **looped / recurrent-depth** reference of a three-model family — the plain loop, without adaptive halting.
18
+
19
+ ## Identity
20
+
21
+ - **Architecture:** decoder-only, LLaMA-style (RoPE, RMSNorm, SwiGLU, QK-Norm, Flash/SDPA attention), **looped**: the 8 transformer blocks are applied **R=4** times (effective depth 32) with **zero added parameters** beyond a small per-iteration depth embedding. Zero-init residual projections for stable unrolling.
22
+ - **Size:** 15.01M parameters · `n_embd 256 / n_layer 8 / n_head 4` · context 768.
23
+ - **Language:** French, from scratch.
24
+ - **Tokenizer:** custom ByteLevel BPE, 32k vocab (`bpe_tokenizer_32k.json`).
25
+ - **Data:** French corpus (AI-rewritten Wikipedia + filtered web via RDTextract), ~425M tokens total. **These runs used 20% of the train split, 2 epochs** (fast research protocol).
26
+
27
+ ## What Cadence is good at
28
+
29
+ Cadence is the **perplexity reference**: the plain loop gives the cleanest hard-metric win of the family.
30
+
31
+ - **Val perplexity 28.9** (vs 31.2 baseline, −7%) — the best of the four models.
32
+ - Better in-domain coherence and fewer invented names than the vanilla baseline.
33
+ - Trade-off: it is **weaker out-of-domain** than the baseline (see table).
34
+
35
+ ## Robust evaluation (50×200, 1 seed)
36
+
37
+ Same table on all three model cards, so the trade-off is visible everywhere. Auto = held-out corpus prompts (in-domain); Fixed = generic hand-written prompts (out-of-domain).
38
+
39
+ | Model | Val PPL ↓ | Coherence auto ↑ | Coherence fixed ↑ | Invented names auto ↓ | Prompt overlap auto ↑ |
40
+ |---|---|---|---|---|---|
41
+ | Baseline (vanilla) | 31.2 | 35.2 | 40.7 | 0.137 | 0.139 |
42
+ | **Cadence** (looped R=4) | **28.9** | 39.3 | 32.5 | 0.121 | 0.147 |
43
+ | Focal (absolute halt) | 29.1 | **44.1** | 29.1 | 0.103 | **0.176** |
44
+ | Nomade (percentile halt) | 31.0 | 36.3 | **41.8** | **0.094** | 0.126 |
45
+
46
+ **No model wins everything — that is the result.** Recurrence helps in-domain and hurts out-of-domain; the halting variants trade one for the other. Factuality is *not* improved by any of them (15M capacity ceiling).
47
+
48
+ ## Lineage (this is not novel)
49
+
50
+ - **ACT** — Adaptive Computation Time, Graves 2016 · [arXiv:1603.08983](https://arxiv.org/abs/1603.08983)
51
+ - **Universal Transformer** — Dehghani et al. 2018 (the looped/weight-shared ancestor) · [arXiv:1807.03819](https://arxiv.org/abs/1807.03819)
52
+ - **CALM** — Confident Adaptive Language Modeling, Schuster et al. 2022 · [arXiv:2207.07061](https://arxiv.org/abs/2207.07061)
53
+ - **LoopViT** — Shu et al. 2026 (parameter-free entropy exit + weight-tied loop) · [arXiv:2602.02156](https://arxiv.org/abs/2602.02156)
54
+
55
+ Cadence is a **Universal-Transformer-style looped transformer** applied to French at 15M. No claim of novelty.
56
+
57
+ ## Limitations
58
+
59
+ - **1 seed, no variance control** — magnitudes may reorder with more seeds.
60
+ - **20% of data, 2 epochs** — a short research protocol, not a full training run.
61
+ - **15M capacity** — hallucinations are expected; the model learns *form and coherence*, not *facts*. It will not reliably tell you the capital of France.
62
+ - No instruction tuning — a pure completion model.
63
+
64
+ ## Related
65
+
66
+ - ✍️ Write-up (Article 3): *Teaching a 15M French LLM to think deeper* — [Hugging Face blog](https://huggingface.co/blog/RDTvlokip/teaching-my-llm-to-think-deeper)
67
+ - 🧪 **Focal** (in-domain variant): [RDTvlokip/Focal-15M-fr](https://huggingface.co/RDTvlokip/Focal-15M-fr)
68
+ - 🧭 **Nomade** (out-of-domain variant): [RDTvlokip/Nomade-15M-fr](https://huggingface.co/RDTvlokip/Nomade-15M-fr)
69
+
70
+ ## Usage
71
+
72
+ This model uses **custom from-scratch code** (not 🤗 Transformers). The code ships with the repo (`model/`, `utils/`), so a snapshot download is self-contained. Requires `torch`, `huggingface_hub`, `tokenizers`.
73
+
74
+ ```python
75
+ import sys, torch
76
+ from huggingface_hub import snapshot_download
77
+
78
+ repo = snapshot_download("RDTvlokip/Cadence-15M-fr") # code + weights + tokenizer
79
+ sys.path.insert(0, repo)
80
+ from model.gpt2 import GPT2
81
+ from utils.tokenizer import GPT2Tokenizer
82
+
83
+ model = GPT2.from_pretrained(f"{repo}/best_model.pt", device="cuda"); model.eval()
84
+ tok = GPT2Tokenizer(f"{repo}/bpe_tokenizer_32k.json")
85
+
86
+ ids = tok.encode("La capitale de la France est", add_special_tokens=True)
87
+ if ids and ids[-1] == tok.eos_token_id:
88
+ ids = ids[:-1] # drop trailing <eos> (keeps it on-topic)
89
+ out = model.generate(
90
+ torch.tensor([ids], device="cuda"),
91
+ max_length=80, temperature=0.8, top_k=40, top_p=0.9,
92
+ repetition_penalty=1.3, eos_token_id=tok.eos_token_id,
93
+ )
94
+ print(tok.decode(out[0].tolist(), skip_special_tokens=True))
95
+ ```
96
+
97
+ **Théo CHARLET** — TSSR Graduate, AI/ML · Creator of AG-BPE · [rdtvlokip.fr](https://rdtvlokip.fr)
best_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:104b107321494e657b94794045aeb1fc9088e5278e799f2c90d598861a20ff2d
3
+ size 60092322
bpe_tokenizer_32k.json ADDED
The diff for this file is too large to render. See raw diff
 
model/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """GPT-2 model implementation."""
2
+
3
+ from .gpt2 import GPT2, GPT2Config
4
+
5
+ __all__ = ["GPT2", "GPT2Config"]
model/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (295 Bytes). View file
 
model/__pycache__/gpt2.cpython-312.pyc ADDED
Binary file (48.7 kB). View file
 
model/gpt2.py ADDED
@@ -0,0 +1,1117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GPT-2 implementation from scratch with modern improvements.
2
+
3
+ Modern features (all optional, backward compatible):
4
+ - RMSNorm (instead of LayerNorm)
5
+ - RoPE (Rotary Position Embeddings)
6
+ - SwiGLU (instead of GELU MLP)
7
+ - GQA (Grouped Query Attention)
8
+ - QK-Norm (Q/K normalization)
9
+ - KV-Cache (faster generation)
10
+ """
11
+
12
+ import math
13
+ import warnings
14
+ from dataclasses import dataclass
15
+ from typing import Optional, Tuple, List
16
+
17
+ import torch
18
+ import torch.nn as nn
19
+ import torch.nn.functional as F
20
+ import torch.utils.checkpoint
21
+
22
+ # Suppress harmless flash attention warning on GTX 1080 Ti and older GPUs
23
+ # (PyTorch falls back to efficient_attention which is nearly as fast)
24
+ warnings.filterwarnings("ignore", message=".*Torch was not compiled with flash attention.*")
25
+
26
+
27
+ @dataclass
28
+ class GPT2Config:
29
+ """GPT-2 model configuration with modern improvements."""
30
+
31
+ # Core architecture
32
+ vocab_size: int = 32000
33
+ n_positions: int = 1024 # Max sequence length
34
+ n_embd: int = 768 # Embedding dimension
35
+ n_layer: int = 12 # Number of transformer blocks
36
+ n_head: int = 12 # Number of attention heads
37
+ n_inner: int = 3072 # FFN hidden dimension
38
+
39
+ # Label smoothing (0.0 = hard labels, 0.1 = 10% spread to other tokens)
40
+ label_smoothing: float = 0.0
41
+
42
+ # Pad token ID (ignored in loss computation)
43
+ pad_token_id: int = 0
44
+
45
+ # Regularization
46
+ embd_pdrop: float = 0.1
47
+ resid_pdrop: float = 0.1
48
+ attn_pdrop: float = 0.1
49
+
50
+ # Normalization
51
+ layer_norm_epsilon: float = 1e-5
52
+ activation_function: str = "gelu"
53
+
54
+ # Legacy flags (kept for compatibility)
55
+ use_rope: bool = False # Rotary Positional Embeddings
56
+ use_mqa: bool = False # Multi-Query Attention (use use_gqa + n_kv_heads=1 instead)
57
+
58
+ # Modern improvement flags
59
+ use_rmsnorm: bool = False # Replace LayerNorm with RMSNorm
60
+ use_swiglu: bool = False # Replace GELU MLP with SwiGLU
61
+ use_gqa: bool = False # Enable Grouped Query Attention
62
+ n_kv_heads: Optional[int] = None # Number of KV heads for GQA (default: n_head)
63
+ use_qk_norm: bool = False # Apply RMSNorm to Q and K
64
+
65
+ # Flash Attention (uses F.scaled_dot_product_attention)
66
+ use_flash_attention: bool = False
67
+
68
+ # LayerScale (CaiT, Touvron et al. 2021): a learned per-channel scalar
69
+ # gamma multiplies each sub-block output before the residual add, init
70
+ # very small so blocks start near-identity and "open up" gradually.
71
+ # Stabilizes small models on noisy (hapax) corpora. Near-zero param cost.
72
+ use_layerscale: bool = False
73
+ layerscale_init: float = 1e-4
74
+
75
+ # Recurrent-depth / looped transformer (Geiping 2025; Kohli et al. COLM 2026;
76
+ # Chen NCU 2026). The n_layer blocks are applied R times in a loop, giving an
77
+ # effective depth of n_layer * recurrence WITHOUT adding parameters. A learned
78
+ # per-step depth embedding is injected before each iteration so the shared
79
+ # block can tell iterations apart. Pairs with use_layerscale + zero-init
80
+ # c_proj (block starts as identity) for stable unrolling. recurrence=1 is the
81
+ # standard (non-looped) model.
82
+ recurrence: int = 1
83
+ # Zero-init the residual output projections (attn c_proj + MLP down_proj) so
84
+ # each block is an exact identity map at init — critical for stable looping
85
+ # (Kohli et al. show default Gaussian init is seed-unstable when looped).
86
+ zero_init_residual: bool = False
87
+ # Adaptive per-token halting (INFERENCE-time, idea originale Théo): during the
88
+ # recurrent loop, freeze tokens whose output entropy drops below a threshold
89
+ # (easy tokens like "le","de" stop early; rare/hapax tokens loop the full R).
90
+ # Zero added params (entropy computed from existing logits). Only active when
91
+ # recurrence > 1. See idee-profondeur-adaptative-par-token.
92
+ adaptive_halting: bool = False
93
+ halting_entropy_threshold: float = 1.0 # nats; token freezes when H(logits) < this
94
+ # Halting threshold mode:
95
+ # "absolute" — freeze when entropy < halting_entropy_threshold (fixed nats).
96
+ # Simple, but the right value depends on model size (a bigger
97
+ # model is more confident → lower entropy → freezes too early).
98
+ # "percentile" — freeze the q fraction of still-active tokens with the LOWEST
99
+ # entropy each iteration (q = halting_percentile). Recomputes
100
+ # the cutoff from the live entropy distribution, so it is
101
+ # INVARIANT to model size — q transfers across scales.
102
+ halting_mode: str = "absolute" # "absolute" | "percentile"
103
+ halting_percentile: float = 0.3 # q: fraction of active tokens to freeze/iter
104
+ # Option B: also apply halting DURING TRAINING, so the model learns to give a
105
+ # good answer at whatever depth each token halts (easy tokens learn to be good
106
+ # at R=1-2, hapax keep looping). Without this, halting-at-inference under-computes
107
+ # (the model was only ever good at fixed R). Idea originale Théo.
108
+ halting_in_training: bool = False
109
+
110
+ # Multi-token prediction (Gloeckle et al. / DeepSeek-V3): a 2nd head also
111
+ # predicts token t+2, forcing richer representations. Only affects training
112
+ # (the extra loss); generation still uses the main t+1 head.
113
+ use_multi_token: bool = False
114
+ multi_token_weight: float = 0.3 # weight λ of the t+2 loss term
115
+
116
+ # RoPE configuration
117
+ rope_theta: float = 10000.0 # Base frequency for RoPE
118
+ rope_scaling: Optional[float] = None # Scaling factor for extended context
119
+
120
+ def __post_init__(self):
121
+ """Validate configuration."""
122
+ assert self.n_embd % self.n_head == 0, "n_embd must be divisible by n_head"
123
+
124
+ # Set default n_kv_heads
125
+ if self.n_kv_heads is None:
126
+ self.n_kv_heads = self.n_head
127
+
128
+ # Validate GQA configuration
129
+ if self.use_gqa or self.use_mqa:
130
+ assert self.n_head % self.n_kv_heads == 0, "n_head must be divisible by n_kv_heads"
131
+
132
+ # MQA is a special case of GQA with n_kv_heads=1
133
+ if self.use_mqa:
134
+ self.use_gqa = True
135
+ self.n_kv_heads = 1
136
+
137
+ # Adjust n_inner for SwiGLU to maintain approximate parameter parity
138
+ # SwiGLU has 3 projections vs 2 for standard MLP, so we reduce hidden dim
139
+ if self.use_swiglu and self.n_inner == 4 * self.n_embd:
140
+ # Standard: 2 * n_embd * n_inner params
141
+ # SwiGLU: 3 * n_embd * n_inner params
142
+ # For parity: n_inner_swiglu = 2/3 * n_inner_standard
143
+ # Round to multiple of 256 for efficiency
144
+ self.n_inner = ((2 * self.n_inner // 3 + 255) // 256) * 256
145
+
146
+
147
+ class RMSNorm(nn.Module):
148
+ """Root Mean Square Layer Normalization.
149
+
150
+ Unlike LayerNorm, RMSNorm:
151
+ - Does not center (subtract mean)
152
+ - Does not have bias parameter
153
+ - Uses RMS for normalization: x / sqrt(mean(x^2) + eps) * weight
154
+
155
+ Reference: https://arxiv.org/abs/1910.07467
156
+ """
157
+
158
+ def __init__(self, dim: int, eps: float = 1e-6):
159
+ super().__init__()
160
+ self.eps = eps
161
+ self.weight = nn.Parameter(torch.ones(dim))
162
+
163
+ def _norm(self, x: torch.Tensor) -> torch.Tensor:
164
+ """Apply RMS normalization."""
165
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
166
+
167
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
168
+ """Forward pass."""
169
+ # Cast to float32 for numerical stability, then cast back
170
+ output = self._norm(x.float()).type_as(x)
171
+ return output * self.weight
172
+
173
+
174
+ class RotaryPositionEmbedding(nn.Module):
175
+ """Rotary Position Embedding (RoPE).
176
+
177
+ Applies rotation to query and key vectors based on position.
178
+
179
+ Key properties:
180
+ - Encodes relative position through rotation
181
+ - No learned parameters
182
+ - Naturally decays attention with distance
183
+
184
+ Reference: https://arxiv.org/abs/2104.09864
185
+ """
186
+
187
+ def __init__(
188
+ self,
189
+ dim: int,
190
+ max_seq_len: int = 2048,
191
+ theta: float = 10000.0,
192
+ scaling_factor: Optional[float] = None,
193
+ ):
194
+ super().__init__()
195
+ self.dim = dim
196
+ self.max_seq_len = max_seq_len
197
+ self.theta = theta
198
+ self.scaling_factor = scaling_factor
199
+
200
+ # Precompute frequency bands: theta_i = theta^(-2i/dim)
201
+ inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim))
202
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
203
+
204
+ # Build initial cache
205
+ self._build_cache(max_seq_len)
206
+
207
+ def _build_cache(self, seq_len: int):
208
+ """Build cos/sin cache for positions [0, seq_len)."""
209
+ self.max_seq_len_cached = seq_len
210
+
211
+ # Position indices
212
+ t = torch.arange(seq_len, device=self.inv_freq.device, dtype=self.inv_freq.dtype)
213
+
214
+ # Apply scaling if provided (for extended context)
215
+ if self.scaling_factor is not None:
216
+ t = t / self.scaling_factor
217
+
218
+ # Outer product: (seq_len,) x (dim/2,) -> (seq_len, dim/2)
219
+ freqs = torch.outer(t, self.inv_freq)
220
+
221
+ # Concatenate for full dimension: (seq_len, dim)
222
+ emb = torch.cat((freqs, freqs), dim=-1)
223
+
224
+ # Cache cos and sin
225
+ self.register_buffer("cos_cached", emb.cos(), persistent=False)
226
+ self.register_buffer("sin_cached", emb.sin(), persistent=False)
227
+
228
+ def forward(
229
+ self,
230
+ q: torch.Tensor,
231
+ k: torch.Tensor,
232
+ position_ids: Optional[torch.Tensor] = None,
233
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
234
+ """
235
+ Apply rotary embeddings to q and k.
236
+
237
+ Args:
238
+ q: Query tensor of shape (batch, n_head, seq_len, head_dim)
239
+ k: Key tensor of shape (batch, n_kv_heads, seq_len, head_dim)
240
+ position_ids: Optional position indices (batch, seq_len)
241
+
242
+ Returns:
243
+ Tuple of rotated (q, k) with same shapes
244
+ """
245
+ seq_len = q.size(2)
246
+
247
+ # Extend cache if needed
248
+ if seq_len > self.max_seq_len_cached:
249
+ self._build_cache(seq_len)
250
+
251
+ # Get cos/sin for this sequence
252
+ if position_ids is not None:
253
+ # Custom position indices (for KV-cache)
254
+ cos = self.cos_cached[position_ids] # (batch, seq_len, dim)
255
+ sin = self.sin_cached[position_ids]
256
+ cos = cos.unsqueeze(1) # (batch, 1, seq_len, dim)
257
+ sin = sin.unsqueeze(1)
258
+ else:
259
+ # Standard sequential positions
260
+ cos = self.cos_cached[:seq_len].unsqueeze(0).unsqueeze(0) # (1, 1, seq_len, dim)
261
+ sin = self.sin_cached[:seq_len].unsqueeze(0).unsqueeze(0)
262
+
263
+ # Apply rotation
264
+ q_rotated = self._apply_rotary(q, cos, sin)
265
+ k_rotated = self._apply_rotary(k, cos, sin)
266
+
267
+ return q_rotated, k_rotated
268
+
269
+ def _apply_rotary(
270
+ self,
271
+ x: torch.Tensor,
272
+ cos: torch.Tensor,
273
+ sin: torch.Tensor,
274
+ ) -> torch.Tensor:
275
+ """Apply rotary embedding: x * cos + rotate_half(x) * sin
276
+
277
+ Computed in float32 for precision (fp16 RoPE drifts on long sequences),
278
+ then cast back to the input dtype.
279
+ """
280
+ orig_dtype = x.dtype
281
+ x = x.float()
282
+ cos = cos.float()
283
+ sin = sin.float()
284
+ out = (x * cos) + (self._rotate_half(x) * sin)
285
+ return out.to(orig_dtype)
286
+
287
+ @staticmethod
288
+ def _rotate_half(x: torch.Tensor) -> torch.Tensor:
289
+ """Rotate half the hidden dims: [x0, x1, x2, x3] -> [-x1, x0, -x3, x2]"""
290
+ x1 = x[..., : x.shape[-1] // 2]
291
+ x2 = x[..., x.shape[-1] // 2 :]
292
+ return torch.cat((-x2, x1), dim=-1)
293
+
294
+
295
+ class CausalSelfAttention(nn.Module):
296
+ """Multi-head causal self-attention with modern improvements.
297
+
298
+ Supports:
299
+ - Standard MHA (Multi-Head Attention)
300
+ - MQA (Multi-Query Attention): n_kv_heads = 1
301
+ - GQA (Grouped Query Attention): 1 < n_kv_heads < n_head
302
+ - QK-Norm: RMSNorm applied to Q and K before attention
303
+ - RoPE: Rotary position embeddings
304
+ - KV-Cache: Efficient autoregressive generation
305
+ """
306
+
307
+ def __init__(self, config: GPT2Config):
308
+ super().__init__()
309
+ assert config.n_embd % config.n_head == 0
310
+
311
+ self.n_head = config.n_head
312
+ self.n_kv_heads = config.n_kv_heads if config.use_gqa else config.n_head
313
+ self.n_embd = config.n_embd
314
+ self.head_dim = config.n_embd // config.n_head
315
+ self.n_rep = self.n_head // self.n_kv_heads # Repetition factor for KV
316
+
317
+ self.use_rope = config.use_rope
318
+ self.use_qk_norm = config.use_qk_norm
319
+ self.use_gqa = config.use_gqa
320
+ self.use_flash_attention = config.use_flash_attention
321
+
322
+ # Separate projections for Q, K, V (modern style, no bias)
323
+ if config.use_gqa or config.use_rope or config.use_qk_norm:
324
+ # Modern architecture: separate projections
325
+ self.q_proj = nn.Linear(config.n_embd, self.n_head * self.head_dim, bias=False)
326
+ self.k_proj = nn.Linear(config.n_embd, self.n_kv_heads * self.head_dim, bias=False)
327
+ self.v_proj = nn.Linear(config.n_embd, self.n_kv_heads * self.head_dim, bias=False)
328
+ self._use_separate_proj = True
329
+ else:
330
+ # Legacy: combined QKV projection
331
+ self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=True)
332
+ self._use_separate_proj = False
333
+
334
+ # Output projection (no bias in modern mode for consistency)
335
+ self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=not self._use_separate_proj)
336
+
337
+ # QK-Norm (optional)
338
+ if self.use_qk_norm:
339
+ self.q_norm = RMSNorm(self.head_dim, eps=config.layer_norm_epsilon)
340
+ self.k_norm = RMSNorm(self.head_dim, eps=config.layer_norm_epsilon)
341
+
342
+ # RoPE (optional)
343
+ if self.use_rope:
344
+ self.rotary_emb = RotaryPositionEmbedding(
345
+ dim=self.head_dim,
346
+ max_seq_len=config.n_positions,
347
+ theta=config.rope_theta,
348
+ scaling_factor=config.rope_scaling,
349
+ )
350
+
351
+ # Regularization
352
+ self.attn_dropout = nn.Dropout(config.attn_pdrop)
353
+ self.resid_dropout = nn.Dropout(config.resid_pdrop)
354
+ self.attn_pdrop = config.attn_pdrop
355
+
356
+ # Scaling factor
357
+ self.scale = 1.0 / math.sqrt(self.head_dim)
358
+
359
+ def forward(
360
+ self,
361
+ x: torch.Tensor,
362
+ attention_mask: Optional[torch.Tensor] = None,
363
+ position_ids: Optional[torch.Tensor] = None,
364
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
365
+ use_cache: bool = False,
366
+ ) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
367
+ """
368
+ Forward pass with optional KV-cache.
369
+
370
+ Args:
371
+ x: Input tensor (batch, seq_len, n_embd)
372
+ attention_mask: Optional attention mask
373
+ position_ids: Position indices for RoPE (batch, seq_len)
374
+ past_key_value: Cached (K, V) from previous forward passes
375
+ use_cache: Whether to return updated cache
376
+
377
+ Returns:
378
+ output: (batch, seq_len, n_embd)
379
+ present_key_value: Updated (K, V) cache if use_cache=True
380
+ """
381
+ B, T, C = x.size()
382
+
383
+ # Project Q, K, V
384
+ if self._use_separate_proj:
385
+ q = self.q_proj(x) # (B, T, n_head * head_dim)
386
+ k = self.k_proj(x) # (B, T, n_kv_heads * head_dim)
387
+ v = self.v_proj(x) # (B, T, n_kv_heads * head_dim)
388
+
389
+ # Reshape for multi-head attention
390
+ q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
391
+ k = k.view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
392
+ v = v.view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
393
+ else:
394
+ # Legacy combined projection
395
+ qkv = self.c_attn(x)
396
+ q, k, v = qkv.split(self.n_embd, dim=2)
397
+ q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
398
+ k = k.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
399
+ v = v.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
400
+
401
+ # Apply QK-Norm if enabled
402
+ if self.use_qk_norm:
403
+ q = self.q_norm(q)
404
+ k = self.k_norm(k)
405
+
406
+ # Apply RoPE if enabled
407
+ if self.use_rope:
408
+ # Determine position IDs
409
+ if position_ids is None:
410
+ if past_key_value is not None:
411
+ past_len = past_key_value[0].size(2)
412
+ position_ids = torch.arange(
413
+ past_len, past_len + T, device=x.device
414
+ ).unsqueeze(0).expand(B, -1)
415
+ else:
416
+ position_ids = torch.arange(T, device=x.device).unsqueeze(0)
417
+
418
+ q, k = self.rotary_emb(q, k, position_ids)
419
+
420
+ # KV-Cache: concatenate past keys/values
421
+ if past_key_value is not None:
422
+ past_k, past_v = past_key_value
423
+ k = torch.cat([past_k, k], dim=2)
424
+ v = torch.cat([past_v, v], dim=2)
425
+
426
+ # Store for cache if needed
427
+ present_key_value = (k, v) if use_cache else None
428
+
429
+ # Repeat K, V for GQA
430
+ if self.n_rep > 1:
431
+ k = self._repeat_kv(k)
432
+ v = self._repeat_kv(v)
433
+
434
+ S = k.size(2) # Total sequence length (including cache)
435
+ is_prefill = past_key_value is None # T_q == T_k when no cache
436
+
437
+ if self.use_flash_attention:
438
+ # SDPA (efficient/flash kernel). Works with KV-cache too: SDPA's
439
+ # is_causal flag assumes T_q == T_k, so we only use it on prefill.
440
+ # During decode (T_q < T_k) we build an explicit causal mask.
441
+ dropout_p = self.attn_pdrop if self.training else 0.0
442
+
443
+ if attention_mask is None and is_prefill:
444
+ # Fast path: built-in causal flag, no mask materialized.
445
+ y = F.scaled_dot_product_attention(
446
+ q, k, v, attn_mask=None, dropout_p=dropout_p, is_causal=True,
447
+ )
448
+ else:
449
+ # Build (T, S) causal mask aligned to the end of the sequence.
450
+ causal = torch.ones(T, S, device=q.device, dtype=torch.bool).tril(diagonal=S - T)
451
+ attn_mask = torch.zeros(T, S, device=q.device, dtype=q.dtype)
452
+ attn_mask.masked_fill_(~causal, float("-inf"))
453
+ if attention_mask is not None:
454
+ attn_mask = attn_mask + attention_mask.to(q.dtype)
455
+ y = F.scaled_dot_product_attention(
456
+ q, k, v, attn_mask=attn_mask, dropout_p=dropout_p, is_causal=False,
457
+ )
458
+ self._last_attention_weights = None # Not available with fused kernel
459
+ else:
460
+ # Manual attention (exposes attention weights for inspection)
461
+ att = (q @ k.transpose(-2, -1)) * self.scale # (B, n_head, T, S)
462
+
463
+ # Causal mask aligned to the end (handles both prefill and decode)
464
+ causal_mask = torch.ones(T, S, device=x.device, dtype=torch.bool).tril(diagonal=S - T)
465
+ att = att.masked_fill(~causal_mask, float("-inf"))
466
+
467
+ if attention_mask is not None:
468
+ att = att + attention_mask.to(att.dtype)
469
+
470
+ att = F.softmax(att, dim=-1)
471
+ self._last_attention_weights = att.detach()
472
+ att = self.attn_dropout(att)
473
+ y = att @ v # (B, n_head, T, head_dim)
474
+
475
+ # Reassemble heads
476
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
477
+
478
+ # Output projection with dropout
479
+ y = self.resid_dropout(self.c_proj(y))
480
+
481
+ return y, present_key_value
482
+
483
+ def _repeat_kv(self, x: torch.Tensor) -> torch.Tensor:
484
+ """Repeat KV heads to match number of query heads (for GQA)."""
485
+ B, n_kv_heads, S, head_dim = x.shape
486
+ if self.n_rep == 1:
487
+ return x
488
+ # Expand and reshape: repeat each KV head n_rep times
489
+ x = x[:, :, None, :, :].expand(B, n_kv_heads, self.n_rep, S, head_dim)
490
+ return x.reshape(B, self.n_head, S, head_dim)
491
+
492
+
493
+ class MLP(nn.Module):
494
+ """Position-wise feed-forward network with optional SwiGLU activation.
495
+
496
+ Standard GELU MLP:
497
+ x -> Linear(n_embd, n_inner) -> GELU -> Linear(n_inner, n_embd)
498
+
499
+ SwiGLU MLP:
500
+ gate, up = Linear(n_embd, 2*n_inner).chunk(2)
501
+ output = SiLU(gate) * up
502
+ output = Linear(n_inner, n_embd)(output)
503
+
504
+ Reference: https://arxiv.org/abs/2002.05202
505
+ """
506
+
507
+ def __init__(self, config: GPT2Config):
508
+ super().__init__()
509
+ self.use_swiglu = config.use_swiglu
510
+
511
+ if self.use_swiglu:
512
+ # SwiGLU: gate and up projection combined (no bias, modern style)
513
+ self.gate_up_proj = nn.Linear(config.n_embd, 2 * config.n_inner, bias=False)
514
+ self.down_proj = nn.Linear(config.n_inner, config.n_embd, bias=False)
515
+ self.act = nn.SiLU() # Swish activation
516
+ else:
517
+ # Standard GELU MLP
518
+ self.c_fc = nn.Linear(config.n_embd, config.n_inner, bias=True)
519
+ self.c_proj = nn.Linear(config.n_inner, config.n_embd, bias=True)
520
+
521
+ if config.activation_function == "gelu":
522
+ self.act = nn.GELU()
523
+ elif config.activation_function == "relu":
524
+ self.act = nn.ReLU()
525
+ else:
526
+ raise ValueError(f"Unsupported activation: {config.activation_function}")
527
+
528
+ self.dropout = nn.Dropout(config.resid_pdrop)
529
+
530
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
531
+ """Forward pass."""
532
+ if self.use_swiglu:
533
+ # SwiGLU: split into gate and value, apply gated activation
534
+ gate_up = self.gate_up_proj(x) # (B, T, 2*n_inner)
535
+ gate, up = gate_up.chunk(2, dim=-1) # Each (B, T, n_inner)
536
+ x = self.act(gate) * up # Gated activation
537
+ x = self.down_proj(x)
538
+ else:
539
+ # Standard GELU
540
+ x = self.c_fc(x)
541
+ x = self.act(x)
542
+ x = self.c_proj(x)
543
+
544
+ x = self.dropout(x)
545
+ return x
546
+
547
+
548
+ class TransformerBlock(nn.Module):
549
+ """Transformer block with modern improvements.
550
+
551
+ Architecture (pre-norm):
552
+ x -> Norm -> Attention -> + -> Norm -> MLP -> +
553
+ |__________________| |____________|
554
+ """
555
+
556
+ def __init__(self, config: GPT2Config):
557
+ super().__init__()
558
+ self.gradient_checkpointing = False
559
+
560
+ # Normalization layers (RMSNorm or LayerNorm)
561
+ if config.use_rmsnorm:
562
+ self.ln_1 = RMSNorm(config.n_embd, eps=config.layer_norm_epsilon)
563
+ self.ln_2 = RMSNorm(config.n_embd, eps=config.layer_norm_epsilon)
564
+ else:
565
+ self.ln_1 = nn.LayerNorm(config.n_embd, eps=config.layer_norm_epsilon)
566
+ self.ln_2 = nn.LayerNorm(config.n_embd, eps=config.layer_norm_epsilon)
567
+
568
+ self.attn = CausalSelfAttention(config)
569
+ self.mlp = MLP(config)
570
+
571
+ # LayerScale: per-channel learned gain on each sub-block output.
572
+ self.use_layerscale = config.use_layerscale
573
+ if config.use_layerscale:
574
+ self.gamma_1 = nn.Parameter(
575
+ torch.full((config.n_embd,), config.layerscale_init)
576
+ )
577
+ self.gamma_2 = nn.Parameter(
578
+ torch.full((config.n_embd,), config.layerscale_init)
579
+ )
580
+
581
+ def forward(
582
+ self,
583
+ x: torch.Tensor,
584
+ attention_mask: Optional[torch.Tensor] = None,
585
+ position_ids: Optional[torch.Tensor] = None,
586
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
587
+ use_cache: bool = False,
588
+ ) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
589
+ """
590
+ Forward pass with optional KV-cache support.
591
+
592
+ Args:
593
+ x: Input (batch, seq_len, n_embd)
594
+ attention_mask: Optional attention mask
595
+ position_ids: Position indices for RoPE
596
+ past_key_value: Cached (K, V) for this layer
597
+ use_cache: Whether to return updated cache
598
+
599
+ Returns:
600
+ output: (batch, seq_len, n_embd)
601
+ present_key_value: Updated cache if use_cache=True
602
+ """
603
+ # Pre-norm attention
604
+ if self.training and self.gradient_checkpointing and not use_cache:
605
+ attn_output, present_key_value = torch.utils.checkpoint.checkpoint(
606
+ self.attn,
607
+ self.ln_1(x),
608
+ attention_mask,
609
+ position_ids,
610
+ past_key_value,
611
+ use_cache,
612
+ use_reentrant=False,
613
+ preserve_rng_state=True,
614
+ determinism_check="none",
615
+ )
616
+ else:
617
+ attn_output, present_key_value = self.attn(
618
+ self.ln_1(x),
619
+ attention_mask=attention_mask,
620
+ position_ids=position_ids,
621
+ past_key_value=past_key_value,
622
+ use_cache=use_cache,
623
+ )
624
+ if self.use_layerscale:
625
+ attn_output = self.gamma_1 * attn_output
626
+ x = x + attn_output
627
+
628
+ # Pre-norm MLP
629
+ if self.training and self.gradient_checkpointing:
630
+ mlp_output = torch.utils.checkpoint.checkpoint(
631
+ self.mlp, self.ln_2(x), use_reentrant=False,
632
+ preserve_rng_state=True, determinism_check="none",
633
+ )
634
+ else:
635
+ mlp_output = self.mlp(self.ln_2(x))
636
+ if self.use_layerscale:
637
+ mlp_output = self.gamma_2 * mlp_output
638
+ x = x + mlp_output
639
+
640
+ return x, present_key_value
641
+
642
+
643
+ class GPT2(nn.Module):
644
+ """GPT-2 Language Model with modern improvements."""
645
+
646
+ def __init__(self, config: GPT2Config):
647
+ super().__init__()
648
+ self.config = config
649
+
650
+ # Token embeddings
651
+ wte = nn.Embedding(config.vocab_size, config.n_embd)
652
+
653
+ # Position embeddings (only if not using RoPE)
654
+ wpe = None
655
+ if not config.use_rope:
656
+ wpe = nn.Embedding(config.n_positions, config.n_embd)
657
+
658
+ # Final normalization
659
+ if config.use_rmsnorm:
660
+ ln_f = RMSNorm(config.n_embd, eps=config.layer_norm_epsilon)
661
+ else:
662
+ ln_f = nn.LayerNorm(config.n_embd, eps=config.layer_norm_epsilon)
663
+
664
+ # Build transformer
665
+ self.transformer = nn.ModuleDict(
666
+ dict(
667
+ wte=wte,
668
+ drop=nn.Dropout(config.embd_pdrop),
669
+ h=nn.ModuleList([TransformerBlock(config) for _ in range(config.n_layer)]),
670
+ ln_f=ln_f,
671
+ )
672
+ )
673
+
674
+ # Add position embeddings if not using RoPE
675
+ if wpe is not None:
676
+ self.transformer["wpe"] = wpe
677
+
678
+ # Recurrent-depth: learned per-iteration depth embedding (broadcast over
679
+ # all positions), injected before each loop pass so the shared block can
680
+ # distinguish iterations. Only needed when looping (recurrence > 1).
681
+ if config.recurrence > 1:
682
+ self.transformer["depth_emb"] = nn.Embedding(config.recurrence, config.n_embd)
683
+
684
+ # Language modeling head
685
+ self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
686
+
687
+ # Optional 2nd head predicting token t+2 (multi-token prediction).
688
+ # Separate, untied head — only used to add a training loss term.
689
+ if config.use_multi_token:
690
+ self.lm_head2 = nn.Linear(config.n_embd, config.vocab_size, bias=False)
691
+
692
+ # Initialize weights first, then tie (so tying survives init)
693
+ self.apply(self._init_weights)
694
+
695
+ # Residual output projections (attn c_proj + MLP down_proj):
696
+ # - zero_init_residual: init to exactly 0 → each block is an identity map
697
+ # at start. Required for stable recurrent-depth looping (Kohli et al.).
698
+ # - otherwise GPT-2 scaled init: std = 0.02 / sqrt(2 * n_layer) to keep
699
+ # the residual-stream variance bounded with depth.
700
+ scale = (2 * config.n_layer) ** -0.5
701
+ for name, p in self.named_parameters():
702
+ if name.endswith("c_proj.weight") or name.endswith("down_proj.weight"):
703
+ if config.zero_init_residual:
704
+ torch.nn.init.zeros_(p)
705
+ else:
706
+ torch.nn.init.normal_(p, mean=0.0, std=0.02 * scale)
707
+
708
+ # Weight tying (after init to avoid double-init of the shared tensor)
709
+ self.transformer.wte.weight = self.lm_head.weight
710
+
711
+ # Report number of parameters
712
+ print(f"Number of parameters: {self.get_num_params() / 1e6:.2f}M")
713
+
714
+ def gradient_checkpointing_enable(self):
715
+ """Enable gradient checkpointing for all transformer blocks."""
716
+ for block in self.transformer.h:
717
+ block.gradient_checkpointing = True
718
+
719
+ def gradient_checkpointing_disable(self):
720
+ """Disable gradient checkpointing for all transformer blocks."""
721
+ for block in self.transformer.h:
722
+ block.gradient_checkpointing = False
723
+
724
+ def _init_weights(self, module):
725
+ """Initialize weights."""
726
+ if isinstance(module, nn.Linear):
727
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
728
+ if module.bias is not None:
729
+ torch.nn.init.zeros_(module.bias)
730
+ elif isinstance(module, nn.Embedding):
731
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
732
+ elif isinstance(module, nn.LayerNorm):
733
+ torch.nn.init.zeros_(module.bias)
734
+ torch.nn.init.ones_(module.weight)
735
+ elif isinstance(module, RMSNorm):
736
+ torch.nn.init.ones_(module.weight)
737
+
738
+ def get_num_params(self, non_embedding: bool = False) -> int:
739
+ """Return the number of parameters in the model."""
740
+ n_params = sum(p.numel() for p in self.parameters())
741
+ if non_embedding and "wpe" in self.transformer:
742
+ n_params -= self.transformer.wpe.weight.numel()
743
+ return n_params
744
+
745
+ def forward(
746
+ self,
747
+ input_ids: torch.Tensor,
748
+ attention_mask: Optional[torch.Tensor] = None,
749
+ position_ids: Optional[torch.Tensor] = None,
750
+ labels: Optional[torch.Tensor] = None,
751
+ past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
752
+ use_cache: bool = False,
753
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor], torch.Tensor, Optional[List]]:
754
+ """
755
+ Forward pass with optional KV-cache.
756
+
757
+ Args:
758
+ input_ids: Input token IDs (batch, seq_len)
759
+ attention_mask: Optional attention mask
760
+ position_ids: Optional position indices for RoPE
761
+ labels: Optional labels for loss computation
762
+ past_key_values: List of (K, V) tuples per layer
763
+ use_cache: Whether to return updated cache
764
+
765
+ Returns:
766
+ logits: (batch, seq_len, vocab_size)
767
+ loss: Scalar if labels provided
768
+ hidden_states: (batch, seq_len, n_embd)
769
+ present_key_values: Updated cache if use_cache=True
770
+ """
771
+ device = input_ids.device
772
+ B, T = input_ids.size()
773
+
774
+ # Calculate total sequence length (including cache)
775
+ past_length = 0
776
+ if past_key_values is not None and past_key_values[0] is not None:
777
+ past_length = past_key_values[0][0].size(2)
778
+
779
+ total_length = past_length + T
780
+ assert total_length <= self.config.n_positions, \
781
+ f"Sequence length {total_length} exceeds max {self.config.n_positions}"
782
+
783
+ # Token embeddings
784
+ x = self.transformer.wte(input_ids)
785
+
786
+ # Position embeddings (if not using RoPE)
787
+ if "wpe" in self.transformer:
788
+ if position_ids is None:
789
+ position_ids = torch.arange(past_length, total_length, device=device)
790
+ position_ids = position_ids.unsqueeze(0).expand(B, -1)
791
+ pos_emb = self.transformer.wpe(position_ids)
792
+ x = x + pos_emb
793
+
794
+ x = self.transformer.drop(x)
795
+
796
+ # Recurrent-depth: apply the stack of blocks `recurrence` times, injecting
797
+ # a per-iteration depth embedding first so the shared blocks can tell
798
+ # iterations apart. recurrence=1 is the plain (non-looped) forward.
799
+ # KV-cache is only meaningful on the LAST iteration (the one whose K/V are
800
+ # reused at the next decode step); intermediate iterations don't cache.
801
+ R = self.config.recurrence
802
+ # KV-cache across a looped forward is not yet supported (the shared blocks
803
+ # would need a separate cache slot per iteration). Looping is used in
804
+ # training and in cache-free eval (evaluate.py), which is enough to test
805
+ # the idea. Fall back cleanly if someone requests both.
806
+ if R > 1 and use_cache:
807
+ raise NotImplementedError(
808
+ "KV-cache is not supported with recurrence > 1; call with use_cache=False."
809
+ )
810
+ present_key_values = [] if use_cache else None
811
+
812
+ # depth_emb table has `config.recurrence` rows. Allow R (may be raised at
813
+ # inference for "thinking longer") to exceed it by clamping the index to
814
+ # the last learned depth embedding.
815
+ n_depth = self.transformer.depth_emb.num_embeddings if R > 1 else 0
816
+
817
+ # Adaptive per-token halting (inference only): a token freezes once its
818
+ # output entropy is low enough (easy tokens stop early, hapax loop the
819
+ # full R). `frozen` holds the final state of halted tokens; `x` carries
820
+ # the still-active computation. Frozen tokens keep serving as K/V.
821
+ # Active at inference always; at training only if halting_in_training (Option B).
822
+ halting = self.config.adaptive_halting and R > 1 and (
823
+ not self.training or self.config.halting_in_training
824
+ )
825
+ frozen = None # (B, T, C) state of halted tokens, NaN where still active
826
+ if halting:
827
+ frozen = torch.full_like(x, float("nan"))
828
+
829
+ for r in range(R):
830
+ if R > 1:
831
+ idx = min(r, n_depth - 1)
832
+ depth_vec = self.transformer.depth_emb(torch.tensor(idx, device=device))
833
+ x = x + depth_vec # broadcast over (B, T, n_embd)
834
+
835
+ for i, block in enumerate(self.transformer.h):
836
+ past_kv = past_key_values[i] if past_key_values is not None else None
837
+
838
+ x, present_kv = block(
839
+ x,
840
+ attention_mask=attention_mask,
841
+ position_ids=position_ids if self.config.use_rope else None,
842
+ past_key_value=past_kv,
843
+ use_cache=use_cache,
844
+ )
845
+
846
+ if use_cache:
847
+ present_key_values.append(present_kv)
848
+
849
+ if halting:
850
+ # Restore already-frozen tokens to their halt-time state so later
851
+ # iterations neither advance them nor let them drift.
852
+ was_frozen = ~torch.isnan(frozen[..., :1]) # (B,T,1)
853
+ x = torch.where(was_frozen, frozen, x)
854
+ if r < R - 1:
855
+ # Freeze newly-confident tokens.
856
+ probs = F.softmax(self.lm_head(self.transformer.ln_f(x)), dim=-1)
857
+ entropy = -(probs * torch.log(probs + 1e-9)).sum(-1, keepdim=True)
858
+ active = ~was_frozen # (B,T,1)
859
+ if self.config.halting_mode == "percentile":
860
+ # Size-invariant cutoff: each iteration, freeze the q
861
+ # fraction of still-active tokens with the LOWEST entropy,
862
+ # per sequence. The cutoff is the q-quantile of the ACTIVE
863
+ # tokens' entropies only (frozen ones are excluded, so they
864
+ # can't skew the quantile). q transfers across model sizes.
865
+ q = self.config.halting_percentile
866
+ ent = entropy.squeeze(-1) # (B, T)
867
+ act = active.squeeze(-1) # (B, T) bool
868
+ newly = torch.zeros_like(act) # (B, T)
869
+ for b in range(ent.size(0)):
870
+ vals = ent[b][act[b]] # entropies of active tokens in seq b
871
+ if vals.numel() == 0:
872
+ continue
873
+ cutoff = torch.quantile(vals, q)
874
+ newly[b] = act[b] & (ent[b] <= cutoff)
875
+ newly = newly.unsqueeze(-1) # (B, T, 1)
876
+ else:
877
+ # Absolute threshold (default): fixed entropy cutoff in nats.
878
+ newly = (entropy < self.config.halting_entropy_threshold) & active
879
+ frozen = torch.where(newly, x, frozen)
880
+
881
+ # Final normalization
882
+ x = self.transformer.ln_f(x)
883
+ hidden_states = x
884
+
885
+ # Language modeling head
886
+ logits = self.lm_head(x)
887
+
888
+ # Loss computation
889
+ loss = None
890
+ if labels is not None:
891
+ # Main objective: predict t+1.
892
+ shift_logits = logits[..., :-1, :].contiguous()
893
+ shift_labels = labels[..., 1:].contiguous()
894
+ loss = F.cross_entropy(
895
+ shift_logits.view(-1, shift_logits.size(-1)),
896
+ shift_labels.view(-1),
897
+ label_smoothing=self.config.label_smoothing,
898
+ ignore_index=self.config.pad_token_id,
899
+ )
900
+
901
+ # Auxiliary objective: a 2nd head predicts t+2 from the same
902
+ # hidden state, forcing the representation to "look further".
903
+ # Only adds a training-time loss term; generation is unchanged.
904
+ if self.config.use_multi_token and hasattr(self, "lm_head2"):
905
+ logits2 = self.lm_head2(x)
906
+ shift_logits2 = logits2[..., :-2, :].contiguous()
907
+ shift_labels2 = labels[..., 2:].contiguous()
908
+ loss2 = F.cross_entropy(
909
+ shift_logits2.view(-1, shift_logits2.size(-1)),
910
+ shift_labels2.view(-1),
911
+ label_smoothing=self.config.label_smoothing,
912
+ ignore_index=self.config.pad_token_id,
913
+ )
914
+ loss = loss + self.config.multi_token_weight * loss2
915
+
916
+ return logits, loss, hidden_states, present_key_values
917
+
918
+ @torch.no_grad()
919
+ def forward_layer_logits(self, input_ids: torch.Tensor, layers):
920
+ """Return logits computed from selected intermediate layers (for DoLa).
921
+
922
+ For each requested layer index, the layer's hidden state is passed
923
+ through the final norm + lm_head, giving "early-exit" logits. The final
924
+ layer's logits are always included under key -1.
925
+
926
+ Args:
927
+ input_ids: (batch, seq_len)
928
+ layers: iterable of layer indices (0-based) to read hidden states
929
+ after. Negative or out-of-range values are ignored.
930
+
931
+ Returns:
932
+ dict {layer_index: logits (batch, seq_len, vocab)}, plus key -1 for
933
+ the final layer.
934
+ """
935
+ device = input_ids.device
936
+ B, T = input_ids.size()
937
+ wanted = set(int(l) for l in layers)
938
+
939
+ x = self.transformer.wte(input_ids)
940
+ if "wpe" in self.transformer:
941
+ position_ids = torch.arange(T, device=device).unsqueeze(0).expand(B, -1)
942
+ x = x + self.transformer.wpe(position_ids)
943
+ else:
944
+ position_ids = torch.arange(T, device=device).unsqueeze(0)
945
+ x = self.transformer.drop(x)
946
+
947
+ out = {}
948
+ n_layer = len(self.transformer.h)
949
+ for i, block in enumerate(self.transformer.h):
950
+ x, _ = block(
951
+ x,
952
+ attention_mask=None,
953
+ position_ids=position_ids if self.config.use_rope else None,
954
+ past_key_value=None,
955
+ use_cache=False,
956
+ )
957
+ if i in wanted and i != n_layer - 1:
958
+ # Project this layer's hidden state with the shared head.
959
+ h = self.transformer.ln_f(x)
960
+ out[i] = self.lm_head(h)
961
+
962
+ # Final layer logits (always provided, key -1).
963
+ h = self.transformer.ln_f(x)
964
+ out[-1] = self.lm_head(h)
965
+ return out
966
+
967
+ @torch.no_grad()
968
+ def generate(
969
+ self,
970
+ input_ids: torch.Tensor,
971
+ max_length: int = 100,
972
+ temperature: float = 1.0,
973
+ top_k: Optional[int] = None,
974
+ top_p: Optional[float] = None,
975
+ repetition_penalty: float = 1.0,
976
+ do_sample: bool = True,
977
+ eos_token_id: Optional[int] = None,
978
+ use_cache: bool = True,
979
+ ) -> torch.Tensor:
980
+ """
981
+ Generate text autoregressively with optional KV-cache.
982
+
983
+ Args:
984
+ input_ids: Input token IDs (batch, seq_len)
985
+ max_length: Maximum tokens to generate
986
+ temperature: Sampling temperature
987
+ top_k: Top-k filtering
988
+ top_p: Nucleus sampling threshold
989
+ repetition_penalty: Penalty for repeating tokens
990
+ do_sample: Whether to sample or greedy decode
991
+ eos_token_id: Stop token ID
992
+ use_cache: Whether to use KV-cache (faster generation)
993
+
994
+ Returns:
995
+ Generated token IDs (batch, total_length)
996
+ """
997
+ training = self.training
998
+ self.eval()
999
+
1000
+ # KV-cache is incompatible with recurrent-depth looping (see forward).
1001
+ if self.config.recurrence > 1:
1002
+ use_cache = False
1003
+
1004
+ past_key_values = None
1005
+
1006
+ for _ in range(max_length):
1007
+ # Determine input for this step
1008
+ if use_cache and past_key_values is not None:
1009
+ # Only process the last token when using cache
1010
+ input_ids_cond = input_ids[:, -1:]
1011
+ else:
1012
+ # Full sequence (crop if needed)
1013
+ input_ids_cond = (
1014
+ input_ids if input_ids.size(1) <= self.config.n_positions
1015
+ else input_ids[:, -self.config.n_positions:]
1016
+ )
1017
+
1018
+ # Forward pass
1019
+ logits, _, _, past_key_values = self(
1020
+ input_ids_cond,
1021
+ past_key_values=past_key_values if use_cache else None,
1022
+ use_cache=use_cache,
1023
+ )
1024
+
1025
+ # Get logits for last position
1026
+ logits = logits[:, -1, :] / temperature
1027
+
1028
+ # Apply repetition penalty (HF semantics: divide positive logits,
1029
+ # multiply negative ones, so both move toward less likely).
1030
+ # Vectorized: gather logits of already-seen tokens, rescale, scatter.
1031
+ if repetition_penalty != 1.0:
1032
+ seen = logits.gather(1, input_ids)
1033
+ seen = torch.where(seen > 0, seen / repetition_penalty, seen * repetition_penalty)
1034
+ logits.scatter_(1, input_ids, seen)
1035
+
1036
+ # Top-k filtering (clamp k to vocab size to avoid topk error)
1037
+ if top_k is not None:
1038
+ k = min(top_k, logits.size(-1))
1039
+ indices_to_remove = logits < torch.topk(logits, k)[0][..., -1, None]
1040
+ logits[indices_to_remove] = float("-inf")
1041
+
1042
+ # Top-p filtering
1043
+ if top_p is not None:
1044
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
1045
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
1046
+ sorted_indices_to_remove = cumulative_probs > top_p
1047
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
1048
+ sorted_indices_to_remove[..., 0] = 0
1049
+ indices_to_remove = sorted_indices_to_remove.scatter(
1050
+ 1, sorted_indices, sorted_indices_to_remove
1051
+ )
1052
+ logits[indices_to_remove] = float("-inf")
1053
+
1054
+ # Sample or greedy
1055
+ probs = F.softmax(logits, dim=-1)
1056
+ if do_sample:
1057
+ next_token = torch.multinomial(probs, num_samples=1)
1058
+ else:
1059
+ next_token = torch.argmax(probs, dim=-1, keepdim=True)
1060
+
1061
+ # Append to sequence
1062
+ input_ids = torch.cat([input_ids, next_token], dim=1)
1063
+
1064
+ # Stop on EOS
1065
+ if eos_token_id is not None and (next_token == eos_token_id).all():
1066
+ break
1067
+
1068
+ self.train(training)
1069
+ return input_ids
1070
+
1071
+ def save_pretrained(self, save_path: str):
1072
+ """Save model checkpoint."""
1073
+ torch.save(
1074
+ {
1075
+ "model_state_dict": self.state_dict(),
1076
+ "config": self.config,
1077
+ },
1078
+ save_path,
1079
+ )
1080
+ print(f"Model saved to {save_path}")
1081
+
1082
+ @classmethod
1083
+ def from_pretrained(cls, load_path: str, device: str = "cpu"):
1084
+ """Load model checkpoint."""
1085
+ # weights_only=True blocks arbitrary code execution from a malicious
1086
+ # checkpoint. The only non-tensor object we serialize is GPT2Config
1087
+ # (a dataclass), so we allowlist it explicitly.
1088
+ #
1089
+ # add_safe_globals + weights_only=True only exist on torch >= 2.4.
1090
+ # On older versions we fall back to a plain (trusted) load, since we
1091
+ # only ever load checkpoints we produced ourselves.
1092
+ if hasattr(torch.serialization, "add_safe_globals"):
1093
+ torch.serialization.add_safe_globals([GPT2Config])
1094
+ checkpoint = torch.load(load_path, map_location=device, weights_only=True)
1095
+ else:
1096
+ checkpoint = torch.load(load_path, map_location=device, weights_only=False)
1097
+ config = checkpoint["config"]
1098
+ model = cls(config)
1099
+
1100
+ # Backward compat: older checkpoints stored a per-layer causal mask
1101
+ # buffer "attn.bias" (shape n_positions x n_positions). The mask is now
1102
+ # built on the fly, so these keys are obsolete — drop them before load.
1103
+ state_dict = checkpoint["model_state_dict"]
1104
+ state_dict = {k: v for k, v in state_dict.items() if not k.endswith("attn.bias")}
1105
+
1106
+ # strict=False also tolerates the missing buffers cleanly.
1107
+ missing, unexpected = model.load_state_dict(state_dict, strict=False)
1108
+ if unexpected:
1109
+ print(f"Warning: ignored unexpected keys: {unexpected}")
1110
+ # Only the intended obsolete buffers may be "missing"; flag anything else.
1111
+ real_missing = [k for k in missing if not k.endswith("attn.bias")]
1112
+ if real_missing:
1113
+ print(f"Warning: missing keys not initialized from checkpoint: {real_missing}")
1114
+
1115
+ model = model.to(device)
1116
+ print(f"Model loaded from {load_path} on device: {device}")
1117
+ return model
utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Utility modules for GPT-2 project."""
utils/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (205 Bytes). View file
 
utils/__pycache__/tokenizer.cpython-312.pyc ADDED
Binary file (7 kB). View file
 
utils/tokenizer.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tokenizer wrapper for BPE tokenizer."""
2
+
3
+ import os
4
+ import json
5
+ from typing import List, Union
6
+ from tokenizers import Tokenizer, decoders
7
+ from tokenizers.models import BPE
8
+ from tokenizers.processors import TemplateProcessing
9
+
10
+
11
+ class GPT2Tokenizer:
12
+ """Wrapper for BPE tokenizer compatible with GPT-2 training."""
13
+
14
+ def __init__(
15
+ self,
16
+ tokenizer_path: str,
17
+ pad_token: str = "<pad>",
18
+ unk_token: str = "<unk>",
19
+ bos_token: str = "<bos>",
20
+ eos_token: str = "<eos>",
21
+ ):
22
+ """
23
+ Initialize tokenizer.
24
+
25
+ Args:
26
+ tokenizer_path: Path to tokenizer JSON file
27
+ pad_token: Padding token
28
+ unk_token: Unknown token
29
+ bos_token: Beginning of sequence token
30
+ eos_token: End of sequence token
31
+ """
32
+ if not os.path.exists(tokenizer_path):
33
+ raise FileNotFoundError(f"Tokenizer file not found: {tokenizer_path}")
34
+
35
+ # Store special tokens
36
+ self.pad_token = pad_token
37
+ self.unk_token = unk_token
38
+ self.bos_token = bos_token
39
+ self.eos_token = eos_token
40
+
41
+ # Load tokenizer from file. This correctly loads the model,
42
+ # pre-tokenizer, and decoder settings from the JSON.
43
+ self.tokenizer = Tokenizer.from_file(tokenizer_path)
44
+
45
+ # Get token IDs
46
+ self.pad_token_id = self.tokenizer.token_to_id(pad_token)
47
+ self.unk_token_id = self.tokenizer.token_to_id(unk_token)
48
+ self.bos_token_id = self.tokenizer.token_to_id(bos_token)
49
+ self.eos_token_id = self.tokenizer.token_to_id(eos_token)
50
+
51
+ # Validate token IDs
52
+ if self.pad_token_id is None:
53
+ raise ValueError(f"Pad token '{pad_token}' not found in vocabulary")
54
+ if self.unk_token_id is None:
55
+ raise ValueError(f"Unknown token '{unk_token}' not found in vocabulary")
56
+ if self.bos_token_id is None:
57
+ raise ValueError(f"BOS token '{bos_token}' not found in vocabulary")
58
+ if self.eos_token_id is None:
59
+ raise ValueError(f"EOS token '{eos_token}' not found in vocabulary")
60
+
61
+ # Configure post-processor for adding special tokens
62
+ try:
63
+ self.tokenizer.post_processor = TemplateProcessing(
64
+ single=f"{bos_token} $A {eos_token}",
65
+ special_tokens=[
66
+ (bos_token, self.bos_token_id),
67
+ (eos_token, self.eos_token_id),
68
+ ],
69
+ )
70
+ except Exception as e:
71
+ print(f"Warning: Could not set post-processor: {e}")
72
+
73
+ # Disable padding by default — must be enabled explicitly per-call
74
+ # (Auto-padding to longest in batch caused massive token inflation
75
+ # in batched encoding, e.g. one 50k-token file padding 200 others to 50k.)
76
+ try:
77
+ self.tokenizer.no_padding()
78
+ except Exception:
79
+ pass
80
+
81
+ @property
82
+ def vocab_size(self) -> int:
83
+ """Get vocabulary size."""
84
+ return self.tokenizer.get_vocab_size()
85
+
86
+ def encode(
87
+ self,
88
+ text: Union[str, List[str]],
89
+ add_special_tokens: bool = True,
90
+ max_length: int = None,
91
+ padding: bool = False,
92
+ truncation: bool = False,
93
+ ) -> Union[List[int], List[List[int]]]:
94
+ """
95
+ Encode text to token IDs.
96
+
97
+ Args:
98
+ text: Text or list of texts to encode
99
+ add_special_tokens: Whether to add BOS/EOS tokens
100
+ max_length: Maximum sequence length
101
+ padding: Whether to pad to max_length
102
+ truncation: Whether to truncate to max_length
103
+
104
+ Returns:
105
+ Token IDs or list of token IDs
106
+ """
107
+ # Configure tokenizer. These settings are global on the underlying
108
+ # tokenizer, so we restore them after encoding to avoid leaking state
109
+ # into later calls (e.g. batched encoding inflating token counts).
110
+ if max_length is not None:
111
+ self.tokenizer.enable_truncation(max_length=max_length)
112
+ if padding and max_length is not None:
113
+ self.tokenizer.enable_padding(length=max_length, pad_id=self.pad_token_id, pad_token=self.pad_token)
114
+
115
+ try:
116
+ # Single text
117
+ if isinstance(text, str):
118
+ encoding = self.tokenizer.encode(text, add_special_tokens=add_special_tokens)
119
+ return encoding.ids
120
+
121
+ # Batch of texts
122
+ else:
123
+ encodings = self.tokenizer.encode_batch(text, add_special_tokens=add_special_tokens)
124
+ return [enc.ids for enc in encodings]
125
+ finally:
126
+ if max_length is not None:
127
+ self.tokenizer.no_truncation()
128
+ if padding and max_length is not None:
129
+ self.tokenizer.no_padding()
130
+
131
+ def decode(
132
+ self,
133
+ token_ids: Union[List[int], List[List[int]]],
134
+ skip_special_tokens: bool = True,
135
+ ) -> Union[str, List[str]]:
136
+ """
137
+ Decode token IDs to text.
138
+
139
+ Args:
140
+ token_ids: Token IDs or list of token IDs
141
+ skip_special_tokens: Whether to remove special tokens
142
+
143
+ Returns:
144
+ Decoded text or list of texts
145
+ """
146
+ # Handle empty input
147
+ if not token_ids:
148
+ return ""
149
+
150
+ # Single sequence
151
+ if isinstance(token_ids[0], int):
152
+ text = self.tokenizer.decode(token_ids, skip_special_tokens=skip_special_tokens)
153
+ return text
154
+
155
+ # Batch of sequences
156
+ else:
157
+ texts = self.tokenizer.decode_batch(token_ids, skip_special_tokens=skip_special_tokens)
158
+ return texts
159
+
160
+ def __call__(self, text: Union[str, List[str]], **kwargs) -> Union[List[int], List[List[int]]]:
161
+ """Shortcut for encode."""
162
+ return self.encode(text, **kwargs)
163
+
164
+ def __len__(self) -> int:
165
+ """Return vocabulary size."""
166
+ return self.vocab_size