Houzeric commited on
Commit
4889a76
·
verified ·
1 Parent(s): 7c660b9

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,3 +1,187 @@
1
- ---
2
- license: apache-2.0
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - fr
4
+ license: apache-2.0
5
+ tags:
6
+ - text-generation
7
+ - french
8
+ - causal-lm
9
+ - sft
10
+ - chatml
11
+ - from-scratch
12
+ - transformer
13
+ - recurrent-depth
14
+ - rope
15
+ - swiglu
16
+ - rmsnorm
17
+ - gqa
18
+ library_name: transformers
19
+ pipeline_tag: text-generation
20
+ model-index:
21
+ - name: Klovis-442M-french-130426
22
+ results: []
23
+ ---
24
+
25
+ # Klovis-442M — French Language Model
26
+
27
+ A **443M-parameter** French language model using a **Recurrent-Depth Transformer** architecture, fully designed and trained from scratch by [Eric Houzelle](https://huggingface.co/Houzeric).
28
+
29
+ Every component — architecture, training pipeline, and inference engine — was written in PyTorch without relying on any pre-trained weights or third-party model code. Trained end-to-end on **a single NVIDIA L40S GPU**.
30
+
31
+ ---
32
+
33
+ ## Key Facts
34
+
35
+ | | |
36
+ |:--|:--|
37
+ | **Parameters** | ~443M |
38
+ | **Architecture** | Recurrent-Depth Transformer (RDT) |
39
+ | **Language** | French |
40
+ | **Tokenizer** | CamemBERT (`camembert-base`, 32k vocab) |
41
+ | **Context window** | 512 tokens |
42
+ | **Chat format** | ChatML |
43
+ | **Training hardware** | 1x NVIDIA L40S |
44
+ | **SFT val_loss** | 1.561431465409791 |
45
+ | **License** | Apache 2.0 |
46
+
47
+ ---
48
+
49
+ ## Quick Start
50
+
51
+ ```bash
52
+ pip install transformers torch safetensors sentencepiece
53
+ ```
54
+
55
+ ### Conversational Mode (ChatML)
56
+
57
+ ```python
58
+ from transformers import AutoModelForCausalLM, AutoTokenizer
59
+
60
+ model_id = "Klovis-ai/Klovis-442M-french-130426"
61
+ tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
62
+ model = AutoModelForCausalLM.from_pretrained(model_id, trust_remote_code=True)
63
+
64
+ prompt = (
65
+ "<|system|>\n"
66
+ "Tu es un assistant intelligent. Réponds directement et précisément.<|end|>\n"
67
+ "<|user|>\n"
68
+ "Que peut-on visiter à Paris ?<|end|>\n"
69
+ "<|assistant|>\n"
70
+ )
71
+
72
+ inputs = tokenizer(prompt, return_tensors="pt")
73
+ outputs = model.generate(
74
+ inputs["input_ids"],
75
+ max_new_tokens=200,
76
+ temperature=0.5,
77
+ top_p=0.85,
78
+ top_k=50,
79
+ do_sample=True,
80
+ )
81
+ print(tokenizer.decode(outputs[0], skip_special_tokens=True))
82
+ ```
83
+
84
+ ### Special Tokens
85
+
86
+ | Token | Role |
87
+ |:------|:-----|
88
+ | `<\|system\|>` | Start of system message |
89
+ | `<\|user\|>` | Start of user message |
90
+ | `<\|assistant\|>` | Start of assistant response |
91
+ | `<\|end\|>` | End of turn |
92
+
93
+ ---
94
+
95
+ ## Architecture — Recurrent-Depth Transformer
96
+
97
+ Klovis uses a **Recurrent-Depth Transformer** (inspired by OpenMythos/Parcae, Prairie et al. 2026), where a single Transformer block is applied iteratively:
98
+
99
+ ```
100
+ Input → [Prelude: 4 layers] → [Shared Block x 8 steps] → [Coda: 4 layers] → Output
101
+ ```
102
+
103
+ | Component | Detail |
104
+ |:----------|:-------|
105
+ | Embedding dim | 1536 |
106
+ | Prelude layers | 4 (unique) |
107
+ | Coda layers | 4 (unique) |
108
+ | Recurrent steps | 8 |
109
+ | Query heads | 24 |
110
+ | KV heads (GQA) | 6 |
111
+ | FFN hidden dim | 8192 |
112
+ | FFN activation | **SwiGLU** |
113
+ | Normalization | **RMSNorm** (pre-norm) |
114
+ | Position encoding | **RoPE** |
115
+ | LTI Injection | Linear Time-Invariant state coupling |
116
+ | ACT Halting | Adaptive Computation Time (per-position early exit) |
117
+ | Depth LoRA | Rank-32 adapters per recurrent step |
118
+ | Weight tying | Input embeddings <-> output projection |
119
+
120
+ ### Why Recurrent-Depth?
121
+
122
+ The RDT architecture achieves **442M effective parameters** with significantly fewer unique parameters by reusing a single Transformer block across 8 depth steps. This enables:
123
+ - **Deeper reasoning** through iterative refinement
124
+ - **Dynamic compute** via ACT (positions that are "easy" halt early)
125
+ - **Step specialization** via Depth LoRA adapters
126
+
127
+ ---
128
+
129
+ ## Training
130
+
131
+ ### Phase 1 — Pre-training
132
+
133
+ | | |
134
+ |:--|:--|
135
+ | **Data** | French texts (Wikipedia FR, FineWeb-2, CulturaX, C4) |
136
+ | **Optimizer** | AdamW (beta1=0.9, beta2=0.95, wd=0.1) |
137
+ | **Scheduler** | Linear warmup -> cosine decay |
138
+ | **Precision** | Mixed precision (bfloat16) |
139
+ | **Block size** | 512 tokens |
140
+
141
+ ### Phase 2 — Supervised Fine-Tuning (SFT)
142
+
143
+ Fine-tuned on curated French conversational datasets across **25 epochs**, with prompt masking (only assistant tokens contribute to the loss).
144
+
145
+ | | |
146
+ |:--|:--|
147
+ | **Epochs** | 25 |
148
+ | **Learning rate** | 2e-5 |
149
+ | **Effective batch size** | 128 (16 x 8 gradient accumulation) |
150
+ | **Final val_loss** | 1.561431465409791 |
151
+ | **Chat format** | ChatML |
152
+
153
+ #### SFT Datasets
154
+
155
+ | Dataset | Samples |
156
+ |:--------|:--------|
157
+ | [kurakurai/luth-sft](https://huggingface.co/datasets/kurakurai/luth-sft) (multiple subsets) | ~300k |
158
+ | [angeluriot/french_instruct](https://huggingface.co/datasets/angeluriot/french_instruct) | 20k |
159
+ | [Houzeric/everyday-conversations](https://huggingface.co/datasets/Houzeric/everyday-conversations) | Custom |
160
+ | [Houzeric/physics-FR-qa-dataset](https://huggingface.co/datasets/Houzeric/physics-FR-qa-dataset) | 5k |
161
+ | [le-leadboard/MATH_LVL5_fr](https://huggingface.co/datasets/le-leadboard/MATH_LVL5_fr) | 1.4k |
162
+
163
+ ---
164
+
165
+ ## What to Expect
166
+
167
+ With ~443M parameters, Klovis produces:
168
+ - Grammatically correct, fluent French
169
+ - Well-structured responses (numbered lists, bold headers, paragraphs)
170
+ - Relevant vocabulary across science, culture, history
171
+ - Coherent conversational interactions
172
+
173
+ Known limitations:
174
+ - Factual responses may be **incorrect or fabricated** (hallucinations)
175
+ - Mathematical reasoning is limited
176
+ - Context limited to 512 tokens
177
+ - French only
178
+
179
+ > **This model demonstrates what a single developer can achieve with a novel architecture at small scale.** It is not intended to replace larger models for production use.
180
+
181
+ ---
182
+
183
+ ## License
184
+
185
+ Apache 2.0 — [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)
186
+
187
+ **Designed and trained by Eric Houzelle.**
config.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "minigpt",
3
+ "architectures": [
4
+ "MiniGPTForCausalLM"
5
+ ],
6
+ "auto_map": {
7
+ "AutoConfig": "configuration_minigpt.MiniGPTConfig",
8
+ "AutoModel": "modeling_minigpt.MiniGPTForCausalLM",
9
+ "AutoModelForCausalLM": "modeling_minigpt.MiniGPTForCausalLM"
10
+ },
11
+ "vocab_size": 32010,
12
+ "block_size": 512,
13
+ "embed_dim": 1536,
14
+ "depth": 14,
15
+ "heads": 24,
16
+ "num_kv_heads": 6,
17
+ "dropout": 0.0,
18
+ "hidden_dim": 8192,
19
+ "weight_sharing": "recurrent_depth",
20
+ "use_rope": true,
21
+ "use_gradient_checkpointing": false,
22
+ "num_prelude_layers": 4,
23
+ "num_coda_layers": 4,
24
+ "num_recurrent_steps": 8,
25
+ "use_lti_injection": true,
26
+ "use_act_halting": true,
27
+ "act_halt_threshold": 0.99,
28
+ "depth_lora_rank": 32,
29
+ "torch_dtype": "float32",
30
+ "tie_word_embeddings": true,
31
+ "tokenizer_class": "CamembertTokenizer"
32
+ }
configuration_minigpt.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class MiniGPTConfig(PretrainedConfig):
5
+ """
6
+ Configuration pour le modèle MiniGPT.
7
+
8
+ Cette classe hérite de PretrainedConfig pour être compatible avec
9
+ l'écosystème Hugging Face.
10
+
11
+ Recurrent-Depth Transformer (RDT) — activé via weight_sharing="recurrent_depth":
12
+ Architecture Prelude → Recurrent Block (boucle T fois) → Coda.
13
+ Inspiré de OpenMythos / Parcae (Prairie et al., 2026).
14
+
15
+ Paramètres RDT:
16
+ num_prelude_layers: couches uniques avant la boucle (encodage initial)
17
+ num_coda_layers: couches uniques après la boucle (décodage final)
18
+ num_recurrent_steps: nombre d'itérations T du bloc récurrent
19
+ use_lti_injection: active l'injection LTI h(t+1) = A·h(t) + B·e + block(h(t))
20
+ use_act_halting: active Adaptive Computation Time (arrêt dynamique par position)
21
+ act_halt_threshold: seuil de probabilité de halting pour ACT
22
+ depth_lora_rank: rang des adaptateurs LoRA par profondeur (0 = désactivé)
23
+ """
24
+ model_type = "minigpt"
25
+
26
+ def __init__(
27
+ self,
28
+ vocab_size=32000,
29
+ block_size=256,
30
+ embed_dim=256,
31
+ depth=8,
32
+ heads=8,
33
+ num_kv_heads=None,
34
+ dropout=0.1,
35
+ hidden_dim=512,
36
+ weight_sharing="none",
37
+ use_rope=True,
38
+ use_gradient_checkpointing=False,
39
+ # Recurrent-Depth Transformer params
40
+ num_prelude_layers=2,
41
+ num_coda_layers=2,
42
+ num_recurrent_steps=8,
43
+ use_lti_injection=True,
44
+ use_act_halting=True,
45
+ act_halt_threshold=0.99,
46
+ depth_lora_rank=8,
47
+ **kwargs
48
+ ):
49
+ super().__init__(**kwargs)
50
+ self.vocab_size = vocab_size
51
+ self.block_size = block_size
52
+ self.embed_dim = embed_dim
53
+ self.depth = depth
54
+ self.heads = heads
55
+ self.num_kv_heads = num_kv_heads if num_kv_heads is not None else heads
56
+ self.dropout = dropout
57
+ self.hidden_dim = hidden_dim
58
+ self.weight_sharing = weight_sharing
59
+ self.use_rope = use_rope
60
+ self.use_gradient_checkpointing = use_gradient_checkpointing
61
+
62
+ # RDT
63
+ self.num_prelude_layers = num_prelude_layers
64
+ self.num_coda_layers = num_coda_layers
65
+ self.num_recurrent_steps = num_recurrent_steps
66
+ self.use_lti_injection = use_lti_injection
67
+ self.use_act_halting = use_act_halting
68
+ self.act_halt_threshold = act_halt_threshold
69
+ self.depth_lora_rank = depth_lora_rank
70
+
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:32a649ade2dd5d924c8ae29a4cb832ac6cc43d4f5e26b20ebe7d7e519857f14d
3
+ size 1771252124
model_components.py ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from torch.utils.checkpoint import checkpoint
7
+ from transformers import PreTrainedModel
8
+ from .configuration_minigpt import MiniGPTConfig
9
+
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Recurrent-Depth Transformer (RDT) components
13
+ # Ref: OpenMythos / Parcae (Prairie et al., 2026)
14
+ # ---------------------------------------------------------------------------
15
+
16
+
17
+ class LTIInjection(nn.Module):
18
+ """Linear Time-Invariant state injection for the recurrent loop.
19
+
20
+ At each iteration t:
21
+ h(t+1) = A · h(t) + B · e + transformer_output
22
+
23
+ Stability guarantee: ρ(A) < 1 by construction.
24
+ A is parameterised as A = U · diag(σ(a)) · V^T where σ(a) ∈ (0, 1)
25
+ via a sigmoid on unconstrained parameters *a*. Because the singular
26
+ values are all < 1, the spectral radius is guaranteed < 1.
27
+ We use a *diagonal* A for efficiency (embed_dim params, not embed_dim²).
28
+ """
29
+
30
+ def __init__(self, embed_dim: int):
31
+ super().__init__()
32
+ self.a_logit = nn.Parameter(torch.zeros(embed_dim))
33
+ self.b_scale = nn.Parameter(torch.ones(embed_dim) * 0.1)
34
+
35
+ def forward(self, h: torch.Tensor, encoded_input: torch.Tensor) -> torch.Tensor:
36
+ a = torch.sigmoid(self.a_logit).clamp(max=0.95)
37
+ return a * h + self.b_scale * encoded_input
38
+
39
+
40
+ class DepthLoRA(nn.Module):
41
+ """Low-rank adaptation applied per recurrent depth step.
42
+
43
+ Adds a small delta = (x @ A) @ B to the hidden state, where A and B
44
+ have shape (embed_dim, rank) and (rank, embed_dim) respectively.
45
+ Each loop iteration t uses its own DepthLoRA so behavior differs per step.
46
+ """
47
+
48
+ def __init__(self, embed_dim: int, rank: int):
49
+ super().__init__()
50
+ self.down = nn.Linear(embed_dim, rank, bias=False)
51
+ self.up = nn.Linear(rank, embed_dim, bias=False)
52
+ nn.init.kaiming_uniform_(self.down.weight, a=math.sqrt(5))
53
+ nn.init.zeros_(self.up.weight)
54
+
55
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
56
+ return self.up(self.down(x))
57
+
58
+
59
+ class ACTHalting(nn.Module):
60
+ """Adaptive Computation Time halting mechanism.
61
+
62
+ Per-position scalar that learns when to stop the recurrent loop.
63
+ Produces a halting probability p(t) at each step t; once cumulative
64
+ probability exceeds *threshold*, the position stops receiving updates.
65
+
66
+ Returns:
67
+ weighted_state: Σ_t w(t) · h(t) — the ACT-blended output
68
+ act_loss: ponder cost (Σ cumul_prob) for regularisation
69
+ """
70
+
71
+ def __init__(self, embed_dim: int, threshold: float = 0.99):
72
+ super().__init__()
73
+ self.halt_proj = nn.Linear(embed_dim, 1, bias=True)
74
+ self.threshold = threshold
75
+
76
+ def compute_halt_prob(self, h: torch.Tensor) -> torch.Tensor:
77
+ """h: (B, T, D) -> (B, T, 1) halt probability."""
78
+ return torch.sigmoid(self.halt_proj(h))
79
+
80
+
81
+ class RoPEEmbedding(nn.Module):
82
+ """Rotary Position Embedding (RoPE) comme utilisé dans LLaMA et autres LLMs modernes.
83
+
84
+ RoPE encode les positions directement dans les queries et keys via des rotations,
85
+ sans nécessiter de paramètres apprenables.
86
+
87
+ Args:
88
+ dim: Dimension de chaque tête d'attention (embed_dim // num_heads)
89
+ max_seq_len: Longueur de séquence maximale
90
+ base: Base pour le calcul des fréquences (10000 par défaut)
91
+ """
92
+ def __init__(self, dim, max_seq_len=2048, base=10000):
93
+ super().__init__()
94
+ self.dim = dim
95
+ self.max_seq_len = max_seq_len
96
+ self.base = base
97
+
98
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
99
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
100
+
101
+ t = torch.arange(max_seq_len).type_as(self.inv_freq)
102
+ freqs = torch.einsum("i,j->ij", t, self.inv_freq)
103
+ emb = torch.cat((freqs, freqs), dim=-1)
104
+ self.register_buffer("cos_cached", emb.cos()[None, None, :, :], persistent=False)
105
+ self.register_buffer("sin_cached", emb.sin()[None, None, :, :], persistent=False)
106
+
107
+ def rotate_half(self, x):
108
+ """Rotation de moitié des dimensions."""
109
+ x1, x2 = x[..., :x.shape[-1]//2], x[..., x.shape[-1]//2:]
110
+ return torch.cat((-x2, x1), dim=-1)
111
+
112
+ def forward(self, q, k, offset=0):
113
+ """Applique RoPE aux queries et keys.
114
+
115
+ Args:
116
+ q: queries [batch, heads, seq_len, head_dim]
117
+ k: keys [batch, heads, seq_len, head_dim]
118
+ offset: position offset for KV cache (number of previously cached tokens)
119
+
120
+ Returns:
121
+ q_rot, k_rot: queries et keys avec positions encodées
122
+ """
123
+ seq_len = q.shape[2]
124
+
125
+ cos = self.cos_cached[:, :, offset:offset + seq_len, :]
126
+ sin = self.sin_cached[:, :, offset:offset + seq_len, :]
127
+
128
+ q_rot = (q * cos) + (self.rotate_half(q) * sin)
129
+ k_rot = (k * cos) + (self.rotate_half(k) * sin)
130
+
131
+ return q_rot, k_rot
132
+
133
+
134
+ class RMSNorm(nn.Module):
135
+ """Root Mean Square Layer Normalization (Zhang & Sennrich, 2019).
136
+
137
+ Faster than LayerNorm: no mean-subtraction, no bias, single reduction.
138
+ Used by LLaMA, Mistral, Gemma, Qwen.
139
+ """
140
+ def __init__(self, dim, eps=1e-6):
141
+ super().__init__()
142
+ self.weight = nn.Parameter(torch.ones(dim))
143
+ self.eps = eps
144
+
145
+ def forward(self, x):
146
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight
147
+
148
+
149
+ class SwiGLU(nn.Module):
150
+ """SwiGLU activation function as described in the Super Tiny LM paper.
151
+ SwiGLU(x) = (Swish(xW) ⊗ xV)W2
152
+ where Swish(x) = SiLU(x) in PyTorch
153
+ """
154
+ def __init__(self, embed_dim, hidden_dim):
155
+ super().__init__()
156
+ self.w = nn.Linear(embed_dim, hidden_dim, bias=False)
157
+ self.v = nn.Linear(embed_dim, hidden_dim, bias=False)
158
+ self.w2 = nn.Linear(hidden_dim, embed_dim, bias=False)
159
+
160
+ def forward(self, x):
161
+ return self.w2(F.silu(self.w(x)) * self.v(x))
162
+
163
+
164
+ class SelfAttention(nn.Module):
165
+ """Multi-Head Attention with optional Grouped-Query Attention (GQA).
166
+
167
+ When num_kv_heads < num_heads, K and V projections are smaller and each
168
+ KV head is shared across num_heads // num_kv_heads query heads.
169
+ When num_kv_heads == num_heads this is standard MHA (fully backward-compatible).
170
+ """
171
+ def __init__(self, embed_dim, heads, dropout, max_seq_len=2048, use_rope=True, num_kv_heads=None):
172
+ super().__init__()
173
+ self.embed_dim = embed_dim
174
+ self.num_heads = heads
175
+ self.num_kv_heads = num_kv_heads if num_kv_heads is not None else heads
176
+ self.head_dim = embed_dim // heads
177
+ self.num_kv_groups = self.num_heads // self.num_kv_heads
178
+ self.use_rope = use_rope
179
+
180
+ assert heads % self.num_kv_heads == 0, (
181
+ f"num_heads ({heads}) must be divisible by num_kv_heads ({self.num_kv_heads})"
182
+ )
183
+
184
+ self.q_proj = nn.Linear(embed_dim, self.num_heads * self.head_dim, bias=False)
185
+ self.k_proj = nn.Linear(embed_dim, self.num_kv_heads * self.head_dim, bias=False)
186
+ self.v_proj = nn.Linear(embed_dim, self.num_kv_heads * self.head_dim, bias=False)
187
+ self.out = nn.Linear(embed_dim, embed_dim, bias=False)
188
+ self.attn_dropout = dropout
189
+ self.resid_dropout = nn.Dropout(dropout)
190
+
191
+ if use_rope:
192
+ self.rope = RoPEEmbedding(self.head_dim, max_seq_len=max_seq_len)
193
+
194
+ def forward(self, x, mask=None, past_kv=None, use_cache=False):
195
+ B, T, C = x.size()
196
+
197
+ q = self.q_proj(x).reshape(B, T, self.num_heads, self.head_dim).transpose(1, 2)
198
+ k = self.k_proj(x).reshape(B, T, self.num_kv_heads, self.head_dim).transpose(1, 2)
199
+ v = self.v_proj(x).reshape(B, T, self.num_kv_heads, self.head_dim).transpose(1, 2)
200
+
201
+ if self.use_rope:
202
+ offset = past_kv[0].shape[2] if past_kv is not None else 0
203
+ q, k = self.rope(q, k, offset=offset)
204
+
205
+ if past_kv is not None:
206
+ k = torch.cat([past_kv[0], k], dim=2)
207
+ v = torch.cat([past_kv[1], v], dim=2)
208
+
209
+ new_kv = (k, v) if use_cache else None
210
+
211
+ # GQA: expand KV heads to match Q heads via repeat
212
+ k_expanded = k.repeat_interleave(self.num_kv_groups, dim=1) if self.num_kv_groups > 1 else k
213
+ v_expanded = v.repeat_interleave(self.num_kv_groups, dim=1) if self.num_kv_groups > 1 else v
214
+
215
+ is_causal = past_kv is None
216
+ attn = F.scaled_dot_product_attention(
217
+ q, k_expanded, v_expanded,
218
+ attn_mask=None,
219
+ is_causal=is_causal,
220
+ dropout_p=self.attn_dropout if self.training else 0.0,
221
+ )
222
+ attn = attn.transpose(1, 2).contiguous().view(B, T, C)
223
+ return self.resid_dropout(self.out(attn)), new_kv
224
+
225
+ class TransformerBlock(nn.Module):
226
+ def __init__(self, embed_dim, heads, dropout=0.1, hidden_dim=512, layerdrop=0.1,
227
+ shared_ff=None, max_seq_len=2048, use_rope=True, num_kv_heads=None):
228
+ super().__init__()
229
+ self.attn = SelfAttention(embed_dim, heads, dropout, max_seq_len=max_seq_len,
230
+ use_rope=use_rope, num_kv_heads=num_kv_heads)
231
+ self.ln1 = RMSNorm(embed_dim)
232
+ self.ff = shared_ff if shared_ff is not None else SwiGLU(embed_dim, hidden_dim)
233
+ self.ln2 = RMSNorm(embed_dim)
234
+ self.dropout = nn.Dropout(dropout)
235
+ self.layerdrop = layerdrop
236
+
237
+ def forward(self, x, mask=None, past_kv=None, use_cache=False):
238
+ if self.training and torch.rand(1).item() < self.layerdrop:
239
+ return (x, None) if use_cache else x
240
+ attn_out, new_kv = self.attn(self.ln1(x), mask, past_kv=past_kv, use_cache=use_cache)
241
+ x = x + self.dropout(attn_out)
242
+ x = x + self.dropout(self.ff(self.ln2(x)))
243
+ return (x, new_kv) if use_cache else x
244
+
245
+ def forward_checkpointed(self, x, mask=None):
246
+ """Version avec gradient checkpointing (training only, no cache)."""
247
+ attn_out, _ = self.attn(self.ln1(x), mask)
248
+ x = x + self.dropout(attn_out)
249
+ x = x + self.dropout(self.ff(self.ln2(x)))
250
+ return x
251
+
252
+ class MiniGPT(PreTrainedModel):
253
+ config_class = MiniGPTConfig
254
+
255
+ def __init__(self, config=None, **kwargs):
256
+ """
257
+ Initialise le modèle MiniGPT.
258
+
259
+ Args:
260
+ config: Instance de MiniGPTConfig ou None. Si None, les paramètres
261
+ doivent être fournis via kwargs.
262
+ **kwargs: Paramètres du modèle si config n'est pas fourni.
263
+ """
264
+ # Si config n'est pas fourni, créer une config à partir des kwargs
265
+ if config is None:
266
+ config = MiniGPTConfig(**kwargs)
267
+
268
+ super().__init__(config)
269
+
270
+ # Extraire les paramètres de la config
271
+ vocab_size = config.vocab_size
272
+ block_size = config.block_size
273
+ embed_dim = config.embed_dim
274
+ depth = config.depth
275
+ heads = config.heads
276
+ num_kv_heads = config.num_kv_heads
277
+ dropout = config.dropout
278
+ hidden_dim = config.hidden_dim
279
+ weight_sharing = config.weight_sharing
280
+ use_rope = config.use_rope
281
+ use_gradient_checkpointing = config.use_gradient_checkpointing
282
+ self.token_emb = nn.Embedding(vocab_size, embed_dim)
283
+ self.use_rope = use_rope
284
+ self.use_gradient_checkpointing = use_gradient_checkpointing
285
+
286
+ # Positional embeddings uniquement si on n'utilise pas RoPE
287
+ if not use_rope:
288
+ self.pos_emb = nn.Embedding(block_size, embed_dim)
289
+ else:
290
+ self.pos_emb = None
291
+
292
+ self.depth = depth
293
+ self.weight_sharing = weight_sharing
294
+ self.vocab_size = vocab_size
295
+ self.block_size = block_size
296
+ self.embed_dim = embed_dim
297
+ self.heads = heads
298
+ self.hidden_dim = hidden_dim
299
+
300
+ # Créer les blocs selon le type de weight sharing
301
+ if weight_sharing == "none":
302
+ self.blocks = nn.ModuleList([
303
+ TransformerBlock(embed_dim, heads, dropout, hidden_dim, layerdrop=0.1,
304
+ max_seq_len=block_size, use_rope=use_rope,
305
+ num_kv_heads=num_kv_heads)
306
+ for _ in range(depth)
307
+ ])
308
+ elif weight_sharing == "ffn":
309
+ shared_ff = SwiGLU(embed_dim, hidden_dim)
310
+ self.blocks = nn.ModuleList([
311
+ TransformerBlock(embed_dim, heads, dropout, hidden_dim, layerdrop=0.1,
312
+ shared_ff=shared_ff, max_seq_len=block_size,
313
+ use_rope=use_rope, num_kv_heads=num_kv_heads)
314
+ for _ in range(depth)
315
+ ])
316
+ elif weight_sharing == "full":
317
+ self.shared_block = TransformerBlock(embed_dim, heads, dropout, hidden_dim, layerdrop=0.1,
318
+ max_seq_len=block_size, use_rope=use_rope,
319
+ num_kv_heads=num_kv_heads)
320
+ self.blocks = None
321
+ else:
322
+ raise ValueError(f"weight_sharing doit être 'none', 'ffn' ou 'full', pas '{weight_sharing}'")
323
+
324
+ self.ln_f = RMSNorm(embed_dim)
325
+ self.head = nn.Linear(embed_dim, vocab_size, bias=False)
326
+ self.head.weight = self.token_emb.weight #On réutilise les poids de la matrice token_emb pour les tetes
327
+ self.block_size = block_size
328
+ self.apply(self._init_weights)
329
+
330
+ def forward(self, idx):
331
+ B, T = idx.shape
332
+
333
+ # Token embeddings
334
+ x = self.token_emb(idx)
335
+
336
+ # Ajouter positional embeddings uniquement si on n'utilise pas RoPE
337
+ if not self.use_rope:
338
+ pos = torch.arange(0, T, device=idx.device).unsqueeze(0)
339
+ x = x + self.pos_emb(pos)
340
+
341
+ mask = torch.tril(torch.ones(T, T, device=idx.device)).unsqueeze(0).unsqueeze(0)
342
+
343
+ # Gradient checkpointing : économise VRAM en recalculant les activations
344
+ if self.use_gradient_checkpointing and self.training:
345
+ if self.weight_sharing == "full":
346
+ for _ in range(self.depth):
347
+ x = checkpoint(self.shared_block.forward_checkpointed, x, mask, use_reentrant=False)
348
+ else:
349
+ for block in self.blocks:
350
+ x = checkpoint(block.forward_checkpointed, x, mask, use_reentrant=False)
351
+ else:
352
+ # Mode normal (pas de checkpointing)
353
+ if self.weight_sharing == "full":
354
+ for _ in range(self.depth):
355
+ x = self.shared_block(x, mask)
356
+ else:
357
+ for block in self.blocks:
358
+ x = block(x, mask)
359
+
360
+ x = self.ln_f(x)
361
+ return self.head(x)
362
+
363
+ def _init_weights(self, module):
364
+ if isinstance(module, nn.Linear):
365
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
366
+ if module.bias is not None:
367
+ torch.nn.init.zeros_(module.bias)
368
+ elif isinstance(module, nn.Embedding):
369
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
370
+ elif isinstance(module, RMSNorm):
371
+ torch.nn.init.ones_(module.weight)
372
+
373
+
374
+ def count_parameters(self):
375
+ """Compte le nombre de paramètres selon le type de weight sharing et use_rope."""
376
+ total = sum(p.numel() for p in self.parameters())
377
+ trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
378
+
379
+ # Détails par composant
380
+ token_emb_params = self.token_emb.weight.numel()
381
+ pos_emb_params = self.pos_emb.weight.numel() if self.pos_emb is not None else 0
382
+ embedding_params = token_emb_params + pos_emb_params
383
+
384
+ if self.weight_sharing == "full":
385
+ block_params = sum(p.numel() for p in self.shared_block.parameters())
386
+ else:
387
+ block_params = sum(p.numel() for p in self.blocks.parameters())
388
+
389
+ return {
390
+ "total": total,
391
+ "trainable": trainable,
392
+ "embedding": embedding_params,
393
+ "token_emb": token_emb_params,
394
+ "pos_emb": pos_emb_params,
395
+ "blocks": block_params,
396
+ "head": 0, # Head partage les poids avec embedding
397
+ "weight_sharing": self.weight_sharing,
398
+ "use_rope": self.use_rope
399
+ }
400
+
401
+ @torch.no_grad()
402
+ def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None, top_p=None, min_new_tokens=0, eos_token_id=None):
403
+ """
404
+ Génération de texte avec contrôle de la diversité.
405
+
406
+ Args:
407
+ idx: Context initial [batch, seq_len]
408
+ max_new_tokens: Nombre de tokens à générer
409
+ temperature: Contrôle la diversité (0.1=conservateur, 1.0=normal, 2.0=créatif)
410
+ top_k: Garde seulement les k tokens les plus probables
411
+ top_p: Nucleus sampling, garde les tokens dont la somme des probas = p
412
+ min_new_tokens: Génère au moins ce nombre de tokens avant d'autoriser l'arrêt sur eos_token_id
413
+ eos_token_id: Id du token EOS pour stopper la génération (optionnel)
414
+ """
415
+ for step in range(max_new_tokens):
416
+ idx_cond = idx[:, -self.block_size:]
417
+ logits = self(idx_cond)
418
+ logits = logits[:, -1, :]
419
+
420
+ # Appliquer la température
421
+ if temperature != 1.0:
422
+ logits = logits / temperature
423
+
424
+ # Top-k filtering
425
+ if top_k is not None:
426
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
427
+ logits[logits < v[:, [-1]]] = -float('Inf')
428
+
429
+ # Top-p (nucleus) filtering
430
+ if top_p is not None:
431
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
432
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
433
+
434
+ # Retirer les tokens au-delà du seuil top_p
435
+ sorted_indices_to_remove = cumulative_probs > top_p
436
+ # Garder au moins le premier token
437
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
438
+ sorted_indices_to_remove[..., 0] = 0
439
+
440
+ # Scatter les valeurs -inf
441
+ indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)
442
+ logits[indices_to_remove] = -float('Inf')
443
+
444
+ # Échantillonner
445
+ probs = F.softmax(logits, dim=-1)
446
+ next_token = torch.multinomial(probs, num_samples=1)
447
+
448
+ # Éviter un EOS trop tôt
449
+ if eos_token_id is not None and step < min_new_tokens:
450
+ while next_token.item() == eos_token_id:
451
+ next_token = torch.multinomial(probs, num_samples=1)
452
+
453
+ idx = torch.cat((idx, next_token), dim=1)
454
+
455
+ # Arrêt précoce si EOS après le minimum requis
456
+ if eos_token_id is not None and step >= min_new_tokens and next_token.item() == eos_token_id:
457
+ break
458
+ return idx
modeling_minigpt.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modèle MiniGPT pour Hugging Face Transformers.
3
+
4
+ Ce fichier contient MiniGPTForCausalLM qui est la classe standard
5
+ attendue par Hugging Face pour les modèles de génération de texte.
6
+
7
+ MiniGPTForCausalLM hérite de MiniGPTModel et ajoute uniquement la tête de langage.
8
+ """
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+ from transformers import PreTrainedModel, GenerationMixin
13
+ from transformers.modeling_outputs import CausalLMOutputWithPast
14
+ from .configuration_minigpt import MiniGPTConfig
15
+ from .modeling_minigpt_core import MiniGPTModel
16
+
17
+
18
+
19
+ class MiniGPTForCausalLM(PreTrainedModel, GenerationMixin):
20
+ """
21
+ MiniGPT model avec une tête de langage pour la génération de texte.
22
+
23
+ Cette classe est compatible avec l'écosystème Hugging Face et peut être
24
+ utilisée avec AutoModelForCausalLM une fois enregistrée.
25
+ """
26
+ config_class = MiniGPTConfig
27
+ base_model_prefix = "model"
28
+ _tied_weights_keys = ["lm_head.weight"]
29
+
30
+ def __init__(self, config):
31
+ super().__init__(config)
32
+ self.model = MiniGPTModel(config)
33
+ self.lm_head = nn.Linear(config.embed_dim, config.vocab_size, bias=False)
34
+ self.post_init()
35
+
36
+ @property
37
+ def act_loss(self) -> torch.Tensor:
38
+ """ACT ponder cost from the recurrent loop (0 when not using RDT)."""
39
+ return self.model.act_loss
40
+
41
+ def set_inference_recurrent_steps(self, steps: int | None):
42
+ """Override the number of recurrent steps for inference.
43
+ Set to None to revert to training default."""
44
+ self.model.set_inference_recurrent_steps(steps)
45
+
46
+ def get_input_embeddings(self):
47
+ return self.model.get_input_embeddings()
48
+
49
+ def set_input_embeddings(self, value):
50
+ self.model.set_input_embeddings(value)
51
+
52
+ def get_output_embeddings(self):
53
+ return self.lm_head
54
+
55
+ def set_output_embeddings(self, new_embeddings):
56
+ self.lm_head = new_embeddings
57
+
58
+ def tie_weights(self, **kwargs):
59
+ self.lm_head.weight = self.model.token_emb.weight
60
+
61
+ def forward(
62
+ self,
63
+ input_ids=None,
64
+ attention_mask=None,
65
+ labels=None,
66
+ past_key_values=None,
67
+ use_cache=None,
68
+ output_attentions=None,
69
+ output_hidden_states=None,
70
+ return_dict=None,
71
+ **kwargs
72
+ ):
73
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
74
+
75
+ if input_ids is None:
76
+ raise ValueError("input_ids doit être fourni")
77
+
78
+ # Appel au modèle core
79
+ outputs = self.model(
80
+ input_ids=input_ids,
81
+ attention_mask=attention_mask,
82
+ past_key_values=past_key_values,
83
+ use_cache=use_cache,
84
+ output_attentions=output_attentions,
85
+ output_hidden_states=output_hidden_states,
86
+ return_dict=return_dict,
87
+ )
88
+
89
+ # Extraire les hidden states selon le format de retour
90
+ if return_dict:
91
+ hidden_states = outputs.last_hidden_state
92
+ else:
93
+ hidden_states = outputs[0]
94
+
95
+ # Appliquer la tête de langage
96
+ logits = self.lm_head(hidden_states)
97
+
98
+ # Calculer la loss si labels fournis
99
+ loss = None
100
+ if labels is not None:
101
+ shift_logits = logits[..., :-1, :].contiguous()
102
+ shift_labels = labels[..., 1:].contiguous()
103
+ loss_fct = nn.CrossEntropyLoss()
104
+ loss = loss_fct(
105
+ shift_logits.view(-1, shift_logits.size(-1)),
106
+ shift_labels.view(-1)
107
+ )
108
+
109
+ # Format de sortie selon return_dict
110
+ if not return_dict:
111
+ output = (logits,)
112
+ if loss is not None:
113
+ return (loss,) + output
114
+ return output
115
+
116
+ return CausalLMOutputWithPast(
117
+ loss=loss,
118
+ logits=logits,
119
+ past_key_values=outputs.past_key_values,
120
+ hidden_states=outputs.hidden_states,
121
+ attentions=outputs.attentions,
122
+ )
123
+
124
+
125
+ def prepare_inputs_for_generation(self, input_ids, past_key_values=None, **kwargs):
126
+ if past_key_values is not None:
127
+ input_ids = input_ids[:, -1:]
128
+ return {
129
+ "input_ids": input_ids,
130
+ "past_key_values": past_key_values,
131
+ "use_cache": True,
132
+ }
133
+
134
+ @torch.no_grad()
135
+ def generate(self, input_ids=None, max_new_tokens=100, temperature=1.0, top_k=None, top_p=None,
136
+ min_new_tokens=0, eos_token_id=None, repetition_penalty=1.0,
137
+ no_repeat_ngram_size=0, **kwargs):
138
+ """
139
+ Génération de texte avec contrôle de la diversité et anti-répétition.
140
+ """
141
+ if input_ids is None:
142
+ raise ValueError("input_ids doit être fourni")
143
+
144
+ idx = input_ids
145
+ block_size = self.config.block_size
146
+ generated_ids: list[int] = []
147
+
148
+ for step in range(max_new_tokens):
149
+ idx_cond = idx[:, -block_size:]
150
+ logits = self.forward(idx_cond).logits
151
+ logits = logits[:, -1, :]
152
+
153
+ # Repetition penalty: reduce probability of tokens already seen
154
+ if repetition_penalty != 1.0:
155
+ seen = set(idx[0].tolist()) | set(generated_ids)
156
+ for tid in seen:
157
+ if logits[0, tid] > 0:
158
+ logits[0, tid] /= repetition_penalty
159
+ else:
160
+ logits[0, tid] *= repetition_penalty
161
+
162
+ # No-repeat n-gram: ban n-grams that already appeared
163
+ if no_repeat_ngram_size > 0 and len(generated_ids) >= no_repeat_ngram_size - 1:
164
+ all_ids = idx[0].tolist() + generated_ids
165
+ ngram_len = no_repeat_ngram_size
166
+ banned: set[int] = set()
167
+ suffix = tuple(all_ids[-(ngram_len - 1):])
168
+ for i in range(len(all_ids) - ngram_len + 1):
169
+ if tuple(all_ids[i:i + ngram_len - 1]) == suffix:
170
+ banned.add(all_ids[i + ngram_len - 1])
171
+ for tid in banned:
172
+ logits[0, tid] = -float('Inf')
173
+
174
+ if temperature != 1.0:
175
+ logits = logits / temperature
176
+
177
+ if eos_token_id is not None and step < min_new_tokens:
178
+ logits[:, eos_token_id] = -float('Inf')
179
+
180
+ if top_k is not None:
181
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
182
+ logits[logits < v[:, [-1]]] = -float('Inf')
183
+
184
+ if top_p is not None:
185
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
186
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
187
+ sorted_indices_to_remove = cumulative_probs > top_p
188
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
189
+ sorted_indices_to_remove[..., 0] = 0
190
+ indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)
191
+ logits[indices_to_remove] = -float('Inf')
192
+
193
+ probs = F.softmax(logits, dim=-1)
194
+ if not torch.isfinite(probs).all() or (probs.sum(dim=-1) <= 0).any():
195
+ next_token = torch.argmax(logits, dim=-1, keepdim=True)
196
+ else:
197
+ next_token = torch.multinomial(probs, num_samples=1)
198
+
199
+ idx = torch.cat((idx, next_token), dim=1)
200
+ generated_ids.append(next_token.item())
201
+
202
+ if eos_token_id is not None and step >= min_new_tokens and next_token.item() == eos_token_id:
203
+ break
204
+
205
+ return idx
206
+
207
+ def count_parameters(self):
208
+ """Délègue au modèle core MiniGPTModel."""
209
+ return self.model.count_parameters()
modeling_minigpt_core.py ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ from torch.utils.checkpoint import checkpoint
5
+ from transformers.modeling_outputs import BaseModelOutputWithPast
6
+
7
+ from .model_components import (
8
+ RoPEEmbedding, RMSNorm, SwiGLU, SelfAttention, TransformerBlock,
9
+ LTIInjection, DepthLoRA, ACTHalting,
10
+ )
11
+
12
+ from .configuration_minigpt import MiniGPTConfig
13
+
14
+
15
+ class MiniGPTModel(nn.Module):
16
+ """
17
+ Modèle core MiniGPT — sans tête LM, pure architecture Transformer.
18
+ NE DOIT PAS hériter de PreTrainedModel.
19
+
20
+ Supporte 4 modes via weight_sharing:
21
+ "none" – couches indépendantes (classique)
22
+ "ffn" – FFN partagé entre couches
23
+ "full" – 1 bloc partagé appliqué depth fois
24
+ "recurrent_depth" – Recurrent-Depth Transformer (Prelude → boucle → Coda)
25
+ """
26
+
27
+ def __init__(self, config: MiniGPTConfig):
28
+ super().__init__()
29
+
30
+ vocab_size = config.vocab_size
31
+ block_size = config.block_size
32
+ embed_dim = config.embed_dim
33
+ depth = config.depth
34
+ heads = config.heads
35
+ num_kv_heads = config.num_kv_heads
36
+ dropout = config.dropout
37
+ hidden_dim = config.hidden_dim
38
+
39
+ self.token_emb = nn.Embedding(vocab_size, embed_dim)
40
+ self.use_rope = config.use_rope
41
+ self.use_gradient_checkpointing = config.use_gradient_checkpointing
42
+ self.block_size = block_size
43
+ self.depth = depth
44
+ self.weight_sharing = config.weight_sharing
45
+
46
+ if not config.use_rope:
47
+ self.pos_emb = nn.Embedding(block_size, embed_dim)
48
+ else:
49
+ self.pos_emb = None
50
+
51
+ def _make_block(**extra):
52
+ return TransformerBlock(
53
+ embed_dim, heads, dropout, hidden_dim,
54
+ max_seq_len=block_size, use_rope=config.use_rope,
55
+ num_kv_heads=num_kv_heads, **extra,
56
+ )
57
+
58
+ # ----- Recurrent-Depth Transformer -----
59
+ if self.weight_sharing == "recurrent_depth":
60
+ self.num_prelude = config.num_prelude_layers
61
+ self.num_coda = config.num_coda_layers
62
+ self.num_recurrent_steps = config.num_recurrent_steps
63
+
64
+ self.prelude = nn.ModuleList([_make_block() for _ in range(self.num_prelude)])
65
+ self.recurrent_block = _make_block()
66
+ self.coda = nn.ModuleList([_make_block() for _ in range(self.num_coda)])
67
+
68
+ self.use_lti = config.use_lti_injection
69
+ if self.use_lti:
70
+ self.lti = LTIInjection(embed_dim)
71
+
72
+ self.depth_lora_rank = config.depth_lora_rank
73
+ if self.depth_lora_rank > 0:
74
+ self.depth_loras = nn.ModuleList([
75
+ DepthLoRA(embed_dim, self.depth_lora_rank)
76
+ for _ in range(self.num_recurrent_steps)
77
+ ])
78
+
79
+ self.use_act = config.use_act_halting
80
+ if self.use_act:
81
+ self.act = ACTHalting(embed_dim, threshold=config.act_halt_threshold)
82
+
83
+ self.blocks = None
84
+
85
+ # ----- Standard modes -----
86
+ elif self.weight_sharing == "none":
87
+ self.blocks = nn.ModuleList([_make_block() for _ in range(depth)])
88
+
89
+ elif self.weight_sharing == "ffn":
90
+ shared_ff = SwiGLU(embed_dim, hidden_dim)
91
+ self.blocks = nn.ModuleList([
92
+ _make_block(shared_ff=shared_ff) for _ in range(depth)
93
+ ])
94
+
95
+ elif self.weight_sharing == "full":
96
+ self.shared_block = _make_block()
97
+ self.blocks = None
98
+
99
+ else:
100
+ raise ValueError(
101
+ f"weight_sharing must be 'none', 'ffn', 'full' or 'recurrent_depth', "
102
+ f"got '{self.weight_sharing}'"
103
+ )
104
+
105
+ self.ln_f = RMSNorm(embed_dim)
106
+ self._act_loss = torch.tensor(0.0)
107
+ self._inference_recurrent_steps = None
108
+
109
+ # ------------------------------------------------------------------ #
110
+ # Public API #
111
+ # ------------------------------------------------------------------ #
112
+
113
+ def get_input_embeddings(self):
114
+ return self.token_emb
115
+
116
+ def set_input_embeddings(self, value):
117
+ self.token_emb = value
118
+
119
+ def get_output_embeddings(self):
120
+ return None
121
+
122
+ @property
123
+ def act_loss(self) -> torch.Tensor:
124
+ """Ponder cost from ACT — add to training loss as regularisation."""
125
+ return self._act_loss
126
+
127
+ def set_inference_recurrent_steps(self, steps: int | None):
128
+ """Override the number of recurrent steps used at inference time.
129
+
130
+ Allows deeper reasoning without retraining. LoRA adapters are
131
+ reused cyclically for steps beyond the trained count.
132
+ Set to None to revert to the training default.
133
+ """
134
+ self._inference_recurrent_steps = steps
135
+
136
+ @property
137
+ def effective_recurrent_steps(self) -> int:
138
+ """Number of recurrent steps for the current forward pass."""
139
+ if self.weight_sharing != "recurrent_depth":
140
+ return 0
141
+ if not self.training and self._inference_recurrent_steps is not None:
142
+ return self._inference_recurrent_steps
143
+ return self.num_recurrent_steps
144
+
145
+ # ------------------------------------------------------------------ #
146
+ # Forward #
147
+ # ------------------------------------------------------------------ #
148
+
149
+ def forward(
150
+ self,
151
+ input_ids=None,
152
+ attention_mask=None,
153
+ past_key_values=None,
154
+ use_cache=None,
155
+ output_attentions=None,
156
+ output_hidden_states=None,
157
+ return_dict=None,
158
+ **kwargs
159
+ ):
160
+ return_dict = return_dict if return_dict is not None else True
161
+ use_cache = use_cache if use_cache is not None else False
162
+
163
+ B, T = input_ids.shape
164
+ x = self.token_emb(input_ids)
165
+
166
+ if self.pos_emb is not None:
167
+ past_len = past_key_values[0][0].shape[2] if past_key_values is not None else 0
168
+ pos = torch.arange(past_len, past_len + T, device=input_ids.device).unsqueeze(0)
169
+ x = x + self.pos_emb(pos)
170
+
171
+ mask = None
172
+ new_key_values = []
173
+
174
+ if self.weight_sharing == "recurrent_depth":
175
+ x = self._forward_recurrent_depth(
176
+ x, mask, past_key_values, use_cache, new_key_values,
177
+ )
178
+ elif self.use_gradient_checkpointing and self.training:
179
+ x = self._forward_checkpointed(x, mask)
180
+ elif use_cache:
181
+ x = self._forward_cached(x, mask, past_key_values, new_key_values)
182
+ else:
183
+ x = self._forward_standard(x, mask)
184
+
185
+ hidden_states = self.ln_f(x)
186
+ present_key_values = tuple(new_key_values) if use_cache else None
187
+
188
+ if not return_dict:
189
+ return (hidden_states,)
190
+
191
+ return BaseModelOutputWithPast(
192
+ last_hidden_state=hidden_states,
193
+ past_key_values=present_key_values,
194
+ hidden_states=None,
195
+ attentions=None,
196
+ )
197
+
198
+ # ------------------------------------------------------------------ #
199
+ # RDT forward path #
200
+ # ------------------------------------------------------------------ #
201
+
202
+ def _forward_recurrent_depth(self, x, mask, past_key_values, use_cache, new_key_values):
203
+ """Prelude -> Recurrent loop (LTI + ACT + LoRA) -> Coda."""
204
+ kv_idx = 0
205
+
206
+ # --- Prelude ---
207
+ for block in self.prelude:
208
+ if self.use_gradient_checkpointing and self.training:
209
+ x = checkpoint(block.forward_checkpointed, x, mask, use_reentrant=False)
210
+ elif use_cache:
211
+ past_kv = past_key_values[kv_idx] if past_key_values is not None else None
212
+ x, kv = block(x, mask, past_kv=past_kv, use_cache=True)
213
+ new_key_values.append(kv)
214
+ kv_idx += 1
215
+ else:
216
+ x = block(x, mask)
217
+
218
+ encoded_input = x # snapshot for LTI re-injection
219
+
220
+ # --- Recurrent loop ---
221
+ n_steps = self.effective_recurrent_steps
222
+ if self.use_act and not self.training:
223
+ x = self._recurrent_loop_act_inference(
224
+ x, encoded_input, mask, past_key_values, use_cache, new_key_values, kv_idx,
225
+ n_steps,
226
+ )
227
+ kv_idx += n_steps
228
+ else:
229
+ x, kv_idx = self._recurrent_loop(
230
+ x, encoded_input, mask, past_key_values, use_cache, new_key_values, kv_idx,
231
+ n_steps,
232
+ )
233
+
234
+ # --- Coda ---
235
+ for block in self.coda:
236
+ if self.use_gradient_checkpointing and self.training:
237
+ x = checkpoint(block.forward_checkpointed, x, mask, use_reentrant=False)
238
+ elif use_cache:
239
+ past_kv = past_key_values[kv_idx] if past_key_values is not None else None
240
+ x, kv = block(x, mask, past_kv=past_kv, use_cache=True)
241
+ new_key_values.append(kv)
242
+ kv_idx += 1
243
+ else:
244
+ x = block(x, mask)
245
+
246
+ return x
247
+
248
+ def _recurrent_loop(self, x, encoded_input, mask, past_key_values, use_cache, new_key_values, kv_idx,
249
+ n_steps=None):
250
+ """Standard recurrent loop (training, or eval without ACT).
251
+
252
+ During training with ACT enabled, we compute halt probabilities and
253
+ accumulate the weighted output + ponder cost for the loss.
254
+ """
255
+ if n_steps is None:
256
+ n_steps = self.num_recurrent_steps
257
+ B, T, D = x.shape
258
+ device = x.device
259
+
260
+ use_act_train = self.use_act and self.training
261
+ if use_act_train:
262
+ cumul_prob = torch.zeros(B, T, 1, device=device)
263
+ remainder = torch.ones(B, T, 1, device=device)
264
+ weighted_state = torch.zeros_like(x)
265
+ still_running = torch.ones(B, T, 1, device=device, dtype=torch.bool)
266
+
267
+ for t in range(n_steps):
268
+ if self.use_gradient_checkpointing and self.training:
269
+ block_out = checkpoint(
270
+ self.recurrent_block.forward_checkpointed, x, mask, use_reentrant=False,
271
+ )
272
+ elif use_cache:
273
+ past_kv = past_key_values[kv_idx] if past_key_values is not None else None
274
+ block_out, kv = self.recurrent_block(x, mask, past_kv=past_kv, use_cache=True)
275
+ new_key_values.append(kv)
276
+ kv_idx += 1
277
+ else:
278
+ block_out = self.recurrent_block(x, mask)
279
+
280
+ # LTI injection: h(t+1) = A·h(t) + B·e + block(h(t))
281
+ if self.use_lti:
282
+ x = self.lti(x, encoded_input) + block_out
283
+ else:
284
+ x = block_out
285
+
286
+ if self.depth_lora_rank > 0:
287
+ lora_idx = t % len(self.depth_loras)
288
+ x = x + self.depth_loras[lora_idx](x)
289
+
290
+ # ACT accumulation (training only)
291
+ if use_act_train:
292
+ p = self.act.compute_halt_prob(x)
293
+ still_running_f = still_running.float()
294
+ new_halted = (cumul_prob + p > self.act.threshold) & still_running
295
+ still_running = still_running & ~new_halted
296
+
297
+ w = torch.where(new_halted, remainder, p * still_running_f)
298
+ weighted_state = weighted_state + w * x
299
+ cumul_prob = cumul_prob + p * still_running_f
300
+ remainder = remainder - p * still_running_f
301
+
302
+ if use_act_train:
303
+ remainder_mask = still_running.float()
304
+ weighted_state = weighted_state + remainder * remainder_mask * x
305
+ self._act_loss = cumul_prob.sum() / (B * T)
306
+ x = weighted_state
307
+
308
+ return x, kv_idx
309
+
310
+ def _recurrent_loop_act_inference(
311
+ self, x, encoded_input, mask, past_key_values, use_cache, new_key_values, kv_idx,
312
+ n_steps=None,
313
+ ):
314
+ """Recurrent loop with early exit per position at inference time."""
315
+ if n_steps is None:
316
+ n_steps = self.num_recurrent_steps
317
+ B, T, D = x.shape
318
+ device = x.device
319
+
320
+ cumul_prob = torch.zeros(B, T, 1, device=device)
321
+ remainder = torch.ones(B, T, 1, device=device)
322
+ weighted_state = torch.zeros_like(x)
323
+ still_running = torch.ones(B, T, 1, device=device, dtype=torch.bool)
324
+
325
+ for t in range(n_steps):
326
+ if not still_running.any():
327
+ if use_cache:
328
+ new_key_values.append(None)
329
+ continue
330
+
331
+ if use_cache:
332
+ past_kv = past_key_values[kv_idx + t] if past_key_values is not None else None
333
+ block_out, kv = self.recurrent_block(x, mask, past_kv=past_kv, use_cache=True)
334
+ new_key_values.append(kv)
335
+ else:
336
+ block_out = self.recurrent_block(x, mask)
337
+
338
+ if self.use_lti:
339
+ x = self.lti(x, encoded_input) + block_out
340
+ else:
341
+ x = block_out
342
+
343
+ if self.depth_lora_rank > 0:
344
+ lora_idx = t % len(self.depth_loras)
345
+ x = x + self.depth_loras[lora_idx](x)
346
+
347
+ p = self.act.compute_halt_prob(x)
348
+ still_running_f = still_running.float()
349
+ new_halted = (cumul_prob + p > self.act.threshold) & still_running
350
+ still_running = still_running & ~new_halted
351
+
352
+ w = torch.where(new_halted, remainder, p * still_running_f)
353
+ weighted_state = weighted_state + w * x
354
+ cumul_prob = cumul_prob + p * still_running_f
355
+ remainder = remainder - p * still_running_f
356
+
357
+ remainder_mask = still_running.float()
358
+ weighted_state = weighted_state + remainder * remainder_mask * x
359
+ self._act_loss = torch.tensor(0.0, device=device)
360
+
361
+ return weighted_state
362
+
363
+ # ------------------------------------------------------------------ #
364
+ # Classic forward helpers (unchanged logic, just extracted) #
365
+ # ------------------------------------------------------------------ #
366
+
367
+ def _forward_standard(self, x, mask):
368
+ if self.weight_sharing == "full":
369
+ for _ in range(self.depth):
370
+ x = self.shared_block(x, mask)
371
+ else:
372
+ for block in self.blocks:
373
+ x = block(x, mask)
374
+ return x
375
+
376
+ def _forward_cached(self, x, mask, past_key_values, new_key_values):
377
+ if self.weight_sharing == "full":
378
+ for layer_idx in range(self.depth):
379
+ past_kv = past_key_values[layer_idx] if past_key_values is not None else None
380
+ x, kv = self.shared_block(x, mask, past_kv=past_kv, use_cache=True)
381
+ new_key_values.append(kv)
382
+ else:
383
+ for layer_idx, block in enumerate(self.blocks):
384
+ past_kv = past_key_values[layer_idx] if past_key_values is not None else None
385
+ x, kv = block(x, mask, past_kv=past_kv, use_cache=True)
386
+ new_key_values.append(kv)
387
+ return x
388
+
389
+ def _forward_checkpointed(self, x, mask):
390
+ if self.weight_sharing == "full":
391
+ for _ in range(self.depth):
392
+ x = checkpoint(self.shared_block.forward_checkpointed, x, mask, use_reentrant=False)
393
+ else:
394
+ for block in self.blocks:
395
+ x = checkpoint(block.forward_checkpointed, x, mask, use_reentrant=False)
396
+ return x
397
+
398
+ # ------------------------------------------------------------------ #
399
+ # Utilities #
400
+ # ------------------------------------------------------------------ #
401
+
402
+ def count_parameters(self):
403
+ """Compte le nombre de paramètres selon le mode."""
404
+ total = sum(p.numel() for p in self.parameters())
405
+ trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
406
+
407
+ token_emb_params = self.token_emb.weight.numel()
408
+ pos_emb_params = self.pos_emb.weight.numel() if self.pos_emb is not None else 0
409
+ embedding_params = token_emb_params + pos_emb_params
410
+
411
+ rdt_extra = 0
412
+ if self.weight_sharing == "recurrent_depth":
413
+ block_params = (
414
+ sum(p.numel() for p in self.prelude.parameters())
415
+ + sum(p.numel() for p in self.recurrent_block.parameters())
416
+ + sum(p.numel() for p in self.coda.parameters())
417
+ )
418
+ if self.use_lti:
419
+ rdt_extra += sum(p.numel() for p in self.lti.parameters())
420
+ if self.depth_lora_rank > 0:
421
+ rdt_extra += sum(p.numel() for p in self.depth_loras.parameters())
422
+ if self.use_act:
423
+ rdt_extra += sum(p.numel() for p in self.act.parameters())
424
+ elif self.weight_sharing == "full":
425
+ block_params = sum(p.numel() for p in self.shared_block.parameters())
426
+ else:
427
+ block_params = sum(p.numel() for p in self.blocks.parameters())
428
+
429
+ result = {
430
+ "total": total,
431
+ "trainable": trainable,
432
+ "embedding": embedding_params,
433
+ "token_emb": token_emb_params,
434
+ "pos_emb": pos_emb_params,
435
+ "blocks": block_params,
436
+ "head": 0,
437
+ "weight_sharing": self.weight_sharing,
438
+ "use_rope": self.use_rope,
439
+ }
440
+ if self.weight_sharing == "recurrent_depth":
441
+ result["rdt_extra"] = rdt_extra
442
+ return result
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": true,
3
+ "backend": "tokenizers",
4
+ "bos_token": "<s>",
5
+ "cls_token": "<s>",
6
+ "eos_token": "</s>",
7
+ "extra_special_tokens": [
8
+ "<|system|>",
9
+ "<|user|>",
10
+ "<|assistant|>",
11
+ "<|end|>"
12
+ ],
13
+ "is_local": false,
14
+ "local_files_only": false,
15
+ "mask_token": "<mask>",
16
+ "model_max_length": 512,
17
+ "pad_token": "<pad>",
18
+ "sep_token": "</s>",
19
+ "tokenizer_class": "CamembertTokenizer",
20
+ "unk_token": "<unk>"
21
+ }