ShinMK3 commited on
Commit
184079e
Β·
verified Β·
1 Parent(s): 924ad35

Upload folder using huggingface_hub

Browse files
LICENSE ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ ASTRAI Pluto Nano Closed License v1.0 β€” see pluto-nano-0.5 for full text.
2
+ Copyright (c) 2026 ASTRAI Labs.
README.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: astrai-closed
4
+ language:
5
+ - en
6
+ - pt
7
+ - es
8
+ - zh
9
+ - hi
10
+ library_name: transformers
11
+ tags:
12
+ - mixture-of-experts
13
+ - moe
14
+ - astrai
15
+ - pluto-nano
16
+ - base
17
+ - causal-lm
18
+ pipeline_tag: text-generation
19
+ ---
20
+
21
+ # ASTRAI Pluto Nano 0.5 β€” BASE
22
+
23
+ **Pre-identity / pre-final-preference checkpoint of Pluto Nano 0.5.**
24
+
25
+ This is the v11 checkpoint *before* identity SFT, ORPO, and KTO-math.
26
+ Use this as the starting point if you want to fine-tune your own
27
+ identity, style or preference on top of Pluto Nano.
28
+
29
+ For the production-aligned model, use [pluto-nano-0.5](../pluto-nano-0.5).
30
+
31
+ ## Architecture
32
+
33
+ - 1 B total / ~50 M active per token (35 experts, top-1 MoE)
34
+ - GQA 6 query / 2 KV heads
35
+ - 16 layers, hidden 384, expert intermediate 1536
36
+ - Tokenizer: custom 32 k BPE
37
+ - Languages: EN, PT, ES, ZH, HI
38
+ - Context: 4096
39
+
40
+ ## Training
41
+
42
+ - Pretrain: 13 B tokens multilingual
43
+ - Distill v1/v2 (frontier models)
44
+ - Recovery CPT + Wikipedia knowledge boost
45
+ - **Second Distill (e1 best)**: reasoning + chat + QA + replay buffer, 30 M tokens
46
+ - Trained entirely on RTX 3060 12 GB
47
+ - Total wall-clock: ~2 weeks
48
+
49
+ ## Usage
50
+
51
+ ```python
52
+ from transformers import AutoModelForCausalLM, AutoTokenizer
53
+ import torch
54
+
55
+ tok = AutoTokenizer.from_pretrained("ASTRAI-labs/pluto-nano-0.5-base", trust_remote_code=True)
56
+ model = AutoModelForCausalLM.from_pretrained(
57
+ "ASTRAI-labs/pluto-nano-0.5-base",
58
+ trust_remote_code=True,
59
+ torch_dtype=torch.bfloat16,
60
+ ).cuda()
61
+ ```
62
+
63
+ ## License
64
+
65
+ ASTRAI Closed License. See [pluto-nano-0.5](../pluto-nano-0.5) for full terms.
config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vocab_size": 32768,
3
+ "hidden_size": 384,
4
+ "intermediate_size_expert": 1536,
5
+ "intermediate_size_shared": 0,
6
+ "n_layers": 16,
7
+ "n_heads": 6,
8
+ "n_kv_heads": 2,
9
+ "n_experts": 35,
10
+ "top_k": 1,
11
+ "n_languages": 5,
12
+ "max_position_embeddings": 4096,
13
+ "rope_theta": 1000000.0,
14
+ "rms_norm_eps": 1e-06,
15
+ "tie_word_embeddings": true,
16
+ "mtp_depth": 2,
17
+ "mtp_loss_weight": 0,
18
+ "router_aux_loss_coef": 0.01,
19
+ "router_z_loss_coef": 0.001,
20
+ "model_type": "astrai_pluto",
21
+ "pad_token_id": 0,
22
+ "bos_token_id": 1,
23
+ "eos_token_id": 2,
24
+ "tokenizer_name": "pluto_nano_32k_bpe",
25
+ "architectures": [
26
+ "PlutoForCausalLM"
27
+ ],
28
+ "auto_map": {
29
+ "AutoConfig": "modeling_pluto.PlutoConfig",
30
+ "AutoModel": "modeling_pluto.PlutoModel",
31
+ "AutoModelForCausalLM": "modeling_pluto.PlutoForCausalLM"
32
+ },
33
+ "torch_dtype": "bfloat16",
34
+ "transformers_version": "4.46.0"
35
+ }
generation_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "pad_token_id": 0,
3
+ "bos_token_id": 1,
4
+ "eos_token_id": 2,
5
+ "max_new_tokens": 512,
6
+ "do_sample": true,
7
+ "temperature": 0.7,
8
+ "top_p": 0.9,
9
+ "transformers_version": "4.46.0"
10
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7310405b3f8db737e7409745b02112a490fdea978f5c13a2b420f95eb4768aee
3
+ size 2070361744
modeling_pluto.py ADDED
@@ -0,0 +1,461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ASTRAI Pluto β€” native architecture for the Pluto family.
3
+
4
+ A standalone decoder-only Transformer with:
5
+ * RMSNorm + RoPE (no learned positional embeddings)
6
+ * Causal SDPA attention (multi-head, optional GQA)
7
+ * Top-K Mixture-of-Experts (SwiGLU experts), no required shared expert
8
+ * Multi-Token Prediction heads (training-only)
9
+ * Tied input/output embedding
10
+ * Router auxiliary loss (load balance) + z-loss
11
+
12
+ Not derived from any HuggingFace base model β€” fresh implementation in plain
13
+ PyTorch. Save/load uses a `pluto_config.json` + a safetensors weights file.
14
+
15
+ Naming: `PlutoModel` / `PlutoForCausalLM`. The `_meta` dict on the config holds
16
+ size hyper-params; routing / aux-loss config is on its own dataclass.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import math
22
+ import os
23
+ from dataclasses import asdict, dataclass, field
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
+ import torch
28
+ import torch.nn as nn
29
+ import torch.nn.functional as F
30
+
31
+
32
+ # ─── Config ─────────────────────────────────────────────────────────────
33
+
34
+ @dataclass
35
+ class PlutoConfig:
36
+ # Architecture (multilingual Nano β€” d=384, layers=16, GQA, 32k vocab)
37
+ vocab_size: int = 32768
38
+ hidden_size: int = 384
39
+ intermediate_size_expert: int = 1536
40
+ intermediate_size_shared: int = 0 # 0 = no shared expert
41
+ n_layers: int = 16
42
+ n_heads: int = 6
43
+ n_kv_heads: int = 2 # GQA: 6β†’2 β†’ ~50 % attn-param saving
44
+ n_experts: int = 35 # 5 langs Γ— 7 experts each
45
+ top_k: int = 1 # max sparsity β†’ ~50 M active inference
46
+ n_languages: int = 5 # en, pt, es, zh, hi
47
+ max_position_embeddings: int = 4096
48
+ rope_theta: float = 1_000_000.0
49
+ rms_norm_eps: float = 1e-6
50
+ tie_word_embeddings: bool = True
51
+
52
+ # MTP β€” training-only aux heads
53
+ mtp_depth: int = 2
54
+ mtp_loss_weight: float = 0.15
55
+
56
+ # Routing aux losses
57
+ router_aux_loss_coef: float = 0.01
58
+ router_z_loss_coef: float = 0.001
59
+
60
+ # Bookkeeping
61
+ model_type: str = "astrai_pluto"
62
+ pad_token_id: int | None = None
63
+ bos_token_id: int | None = None
64
+ eos_token_id: int | None = None
65
+
66
+ # Tokenizer config (saved for convenience)
67
+ tokenizer_name: str | None = None
68
+
69
+ def to_dict(self) -> dict:
70
+ return asdict(self)
71
+
72
+ @classmethod
73
+ def from_dict(cls, d: dict) -> "PlutoConfig":
74
+ # ignore extra keys silently for forward-compat
75
+ known = {f.name for f in cls.__dataclass_fields__.values()}
76
+ return cls(**{k: v for k, v in d.items() if k in known})
77
+
78
+ def save(self, output_dir: str | Path) -> None:
79
+ os.makedirs(output_dir, exist_ok=True)
80
+ with open(Path(output_dir) / "pluto_config.json", "w") as f:
81
+ json.dump(self.to_dict(), f, indent=2)
82
+
83
+ @classmethod
84
+ def load(cls, model_dir: str | Path) -> "PlutoConfig":
85
+ with open(Path(model_dir) / "pluto_config.json") as f:
86
+ return cls.from_dict(json.load(f))
87
+
88
+
89
+ # ─── Layers ─────────────────────────────────────────────────────────────
90
+
91
+ class RMSNorm(nn.Module):
92
+ def __init__(self, dim: int, eps: float = 1e-6):
93
+ super().__init__()
94
+ self.weight = nn.Parameter(torch.ones(dim))
95
+ self.eps = eps
96
+
97
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
98
+ # Compute in fp32 for numerical stability, return in input dtype
99
+ out = x.float()
100
+ norm = out.pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt()
101
+ return (out * norm).to(x.dtype) * self.weight
102
+
103
+
104
+ def _rope_freqs(dim: int, base: float, device, dtype=torch.float32) -> torch.Tensor:
105
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, device=device, dtype=dtype) / dim))
106
+ return inv_freq
107
+
108
+
109
+ def _rope_cache(seq_len: int, dim: int, base: float, device) -> tuple[torch.Tensor, torch.Tensor]:
110
+ inv_freq = _rope_freqs(dim, base, device)
111
+ t = torch.arange(seq_len, device=device, dtype=torch.float32)
112
+ freqs = torch.outer(t, inv_freq)
113
+ cos = freqs.cos()
114
+ sin = freqs.sin()
115
+ return cos, sin
116
+
117
+
118
+ def _apply_rope(q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor):
119
+ # q, k: [B, H, T, Dh]; cos, sin: [T, Dh/2]
120
+ def rotate(x: torch.Tensor) -> torch.Tensor:
121
+ x1, x2 = x[..., ::2], x[..., 1::2]
122
+ rot = torch.stack((-x2 * sin + x1 * cos, x1 * sin + x2 * cos), dim=-1)
123
+ return rot.flatten(-2)
124
+ return rotate(q), rotate(k)
125
+
126
+
127
+ class PlutoAttention(nn.Module):
128
+ """Causal SDPA attention with optional GQA + RoPE."""
129
+ def __init__(self, cfg: PlutoConfig):
130
+ super().__init__()
131
+ assert cfg.hidden_size % cfg.n_heads == 0
132
+ self.cfg = cfg
133
+ self.head_dim = cfg.hidden_size // cfg.n_heads
134
+ self.q_proj = nn.Linear(cfg.hidden_size, cfg.n_heads * self.head_dim, bias=False)
135
+ self.k_proj = nn.Linear(cfg.hidden_size, cfg.n_kv_heads * self.head_dim, bias=False)
136
+ self.v_proj = nn.Linear(cfg.hidden_size, cfg.n_kv_heads * self.head_dim, bias=False)
137
+ self.o_proj = nn.Linear(cfg.hidden_size, cfg.hidden_size, bias=False)
138
+
139
+ def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
140
+ B, T, D = x.shape
141
+ H = self.cfg.n_heads
142
+ Hk = self.cfg.n_kv_heads
143
+ Dh = self.head_dim
144
+
145
+ q = self.q_proj(x).view(B, T, H, Dh).transpose(1, 2) # [B, H, T, Dh]
146
+ k = self.k_proj(x).view(B, T, Hk, Dh).transpose(1, 2) # [B, Hk, T, Dh]
147
+ v = self.v_proj(x).view(B, T, Hk, Dh).transpose(1, 2)
148
+ q, k = _apply_rope(q, k, cos[:T].to(q.dtype), sin[:T].to(q.dtype))
149
+ # GQA: expand kv if Hk < H
150
+ if Hk != H:
151
+ repeats = H // Hk
152
+ k = k.repeat_interleave(repeats, dim=1)
153
+ v = v.repeat_interleave(repeats, dim=1)
154
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
155
+ y = y.transpose(1, 2).contiguous().view(B, T, D)
156
+ return self.o_proj(y)
157
+
158
+
159
+ class SwiGLU(nn.Module):
160
+ def __init__(self, dim: int, hidden: int):
161
+ super().__init__()
162
+ self.w_gate = nn.Linear(dim, hidden, bias=False)
163
+ self.w_up = nn.Linear(dim, hidden, bias=False)
164
+ self.w_down = nn.Linear(hidden, dim, bias=False)
165
+
166
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
167
+ return self.w_down(F.silu(self.w_gate(x)) * self.w_up(x))
168
+
169
+
170
+ class PlutoMoE(nn.Module):
171
+ """Top-K MoE using grouped matmul (torch._grouped_mm).
172
+
173
+ Expert weights are kept as 3 stacked tensors of shape [E, D, H] (gate, up)
174
+ and [E, H, D] (down) so the whole layer is 3 grouped GEMMs per forward.
175
+
176
+ Currently specialised for top_k == 1 (sort once, no aggregation). Top-K>1
177
+ falls back to the per-expert loop.
178
+
179
+ Optional shared expert (always active) if intermediate_size_shared > 0.
180
+ """
181
+ def __init__(self, cfg: PlutoConfig):
182
+ super().__init__()
183
+ self.cfg = cfg
184
+ E, D, H = cfg.n_experts, cfg.hidden_size, cfg.intermediate_size_expert
185
+ self.router = nn.Linear(D, E, bias=False)
186
+ # SwiGLU expert weights stacked along the expert dim.
187
+ # `_grouped_mm(A, B, offs)` expects B in [E, K, N] for A in [M, K]
188
+ # β†’ output [M, N]. So we store:
189
+ # W_gate: [E, D, H] β†’ x @ W_gate β†’ [M, H]
190
+ # W_up: [E, D, H]
191
+ # W_down: [E, H, D]
192
+ self.W_gate = nn.Parameter(torch.empty(E, D, H))
193
+ self.W_up = nn.Parameter(torch.empty(E, D, H))
194
+ self.W_down = nn.Parameter(torch.empty(E, H, D))
195
+ # Init: Kaiming-like, scaled down so initial residual is well-behaved.
196
+ std_in = 1.0 / math.sqrt(D)
197
+ std_h = 1.0 / math.sqrt(H)
198
+ nn.init.normal_(self.W_gate, std=std_in)
199
+ nn.init.normal_(self.W_up, std=std_in)
200
+ nn.init.normal_(self.W_down, std=std_h)
201
+ self.shared = (SwiGLU(D, cfg.intermediate_size_shared)
202
+ if cfg.intermediate_size_shared > 0 else None)
203
+
204
+ @staticmethod
205
+ def _offsets_from_counts(counts: torch.Tensor) -> torch.Tensor:
206
+ # Convert [E] counts β†’ end-offset tensor [E] of int32.
207
+ # `torch._grouped_mm` consumes end-offsets (exclusive cumsum).
208
+ return counts.cumsum(0).to(torch.int32)
209
+
210
+ def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, dict]:
211
+ B, T, D = x.shape
212
+ E = self.cfg.n_experts
213
+ x_flat = x.reshape(B * T, D)
214
+ logits = self.router(x_flat) # [B*T, E]
215
+
216
+ if self.cfg.top_k == 1:
217
+ # Sort tokens by expert id β†’ contiguous expert ranges β†’ grouped GEMM
218
+ top_idx = logits.argmax(dim=-1) # [B*T]
219
+ sort_idx = top_idx.argsort(stable=True)
220
+ x_sorted = x_flat[sort_idx] # [B*T, D]
221
+
222
+ counts = torch.bincount(top_idx, minlength=E) # [E]
223
+ offsets = self._offsets_from_counts(counts) # [E] end-offsets
224
+
225
+ # Grouped SwiGLU: each token uses ONE expert.
226
+ gate = torch._grouped_mm(x_sorted, self.W_gate, offsets) # [B*T, H]
227
+ up = torch._grouped_mm(x_sorted, self.W_up, offsets) # [B*T, H]
228
+ hidden = F.silu(gate) * up
229
+ out_sorted = torch._grouped_mm(hidden, self.W_down, offsets) # [B*T, D]
230
+
231
+ # Un-sort
232
+ inverse = torch.empty_like(sort_idx)
233
+ inverse[sort_idx] = torch.arange(sort_idx.size(0), device=x.device)
234
+ out = out_sorted[inverse]
235
+ else:
236
+ # Top-K>1 fallback: slower loop. Kept for completeness.
237
+ topk_vals, topk_idx = logits.topk(self.cfg.top_k, dim=-1)
238
+ topk_w = F.softmax(topk_vals, dim=-1)
239
+ out = torch.zeros_like(x_flat)
240
+ for k in range(self.cfg.top_k):
241
+ ids = topk_idx[..., k]
242
+ w = topk_w[..., k].unsqueeze(-1)
243
+ # Per-K grouped GEMM
244
+ sort_idx = ids.argsort(stable=True)
245
+ x_sorted = x_flat[sort_idx]
246
+ counts = torch.bincount(ids, minlength=E)
247
+ offsets = self._offsets_from_counts(counts)
248
+ gate = torch._grouped_mm(x_sorted, self.W_gate, offsets)
249
+ up = torch._grouped_mm(x_sorted, self.W_up, offsets)
250
+ hidden = F.silu(gate) * up
251
+ out_sorted = torch._grouped_mm(hidden, self.W_down, offsets)
252
+ inverse = torch.empty_like(sort_idx)
253
+ inverse[sort_idx] = torch.arange(sort_idx.size(0), device=x.device)
254
+ out = out + out_sorted[inverse] * w
255
+ top_idx = topk_idx[..., 0] # for aux-loss bookkeeping below
256
+
257
+ if self.shared is not None:
258
+ out = out + self.shared(x_flat)
259
+ out = out.reshape(B, T, D)
260
+
261
+ # Auxiliary losses (Switch Transformer load-balance + ST-MoE z-loss)
262
+ aux: dict = {}
263
+ if self.training:
264
+ probs = F.softmax(logits.float(), dim=-1)
265
+ expert_freq = probs.mean(dim=0) # [E]
266
+ counts_norm = (counts.float() / counts.float().sum().clamp_min(1.0))
267
+ aux["aux_load"] = (expert_freq * counts_norm).sum() * self.cfg.n_experts
268
+ aux["aux_z"] = (logits.float().logsumexp(-1) ** 2).mean()
269
+ return out, aux
270
+
271
+
272
+ class PlutoBlock(nn.Module):
273
+ def __init__(self, cfg: PlutoConfig):
274
+ super().__init__()
275
+ self.ln1 = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
276
+ self.attn = PlutoAttention(cfg)
277
+ self.ln2 = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
278
+ self.moe = PlutoMoE(cfg)
279
+
280
+ def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> tuple[torch.Tensor, dict]:
281
+ x = x + self.attn(self.ln1(x), cos, sin)
282
+ y, aux = self.moe(self.ln2(x))
283
+ x = x + y
284
+ return x, aux
285
+
286
+
287
+ # ─── Models ─────────────────────────────────────────────────────────────
288
+
289
+ class PlutoModel(nn.Module):
290
+ """Decoder backbone: token embed β†’ N blocks β†’ final RMSNorm."""
291
+ def __init__(self, cfg: PlutoConfig):
292
+ super().__init__()
293
+ self.cfg = cfg
294
+ self.embed_tokens = nn.Embedding(cfg.vocab_size, cfg.hidden_size)
295
+ self.blocks = nn.ModuleList([PlutoBlock(cfg) for _ in range(cfg.n_layers)])
296
+ self.final_norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
297
+ self.register_buffer("_rope_initialised", torch.tensor(False), persistent=False)
298
+ self._rope_cos = None
299
+ self._rope_sin = None
300
+
301
+ def _ensure_rope(self, seq_len: int, device, dtype):
302
+ head_dim = self.cfg.hidden_size // self.cfg.n_heads
303
+ if (self._rope_cos is None or self._rope_cos.size(0) < seq_len
304
+ or self._rope_cos.device != device):
305
+ cos, sin = _rope_cache(self.cfg.max_position_embeddings, head_dim,
306
+ self.cfg.rope_theta, device)
307
+ self._rope_cos = cos.to(dtype)
308
+ self._rope_sin = sin.to(dtype)
309
+
310
+ def forward(self, input_ids: torch.Tensor) -> tuple[torch.Tensor, list[dict]]:
311
+ B, T = input_ids.shape
312
+ h = self.embed_tokens(input_ids)
313
+ self._ensure_rope(T, h.device, h.dtype)
314
+ aux_list = []
315
+ for blk in self.blocks:
316
+ h, aux = blk(h, self._rope_cos, self._rope_sin)
317
+ aux_list.append(aux)
318
+ h = self.final_norm(h)
319
+ return h, aux_list
320
+
321
+
322
+ class PlutoForCausalLM(nn.Module):
323
+ """LM head + optional MTP heads. Returns full loss in `forward`."""
324
+ def __init__(self, cfg: PlutoConfig):
325
+ super().__init__()
326
+ self.cfg = cfg
327
+ self.model = PlutoModel(cfg)
328
+ self.lm_head = nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
329
+ if cfg.tie_word_embeddings:
330
+ self.lm_head.weight = self.model.embed_tokens.weight
331
+ # MTP β€” training-only auxiliary heads that predict tokens further ahead.
332
+ self.mtp_heads = nn.ModuleList([
333
+ nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
334
+ for _ in range(cfg.mtp_depth)
335
+ ])
336
+
337
+ def forward(self, input_ids: torch.Tensor, labels: torch.Tensor | None = None,
338
+ attention_mask: torch.Tensor | None = None,
339
+ ) -> dict:
340
+ # We only honour `labels` from the training harness (HF API).
341
+ if labels is None:
342
+ labels = input_ids
343
+ h, aux_list = self.model(input_ids)
344
+ logits = self.lm_head(h)
345
+ out = {"logits": logits}
346
+
347
+ # Main next-token loss. Trainer is expected to pass `input_ids = ids[:-1]`
348
+ # and `labels = ids[1:]` so they already align (no internal shift).
349
+ if labels is not None and labels.size(1) == logits.size(1):
350
+ ce = F.cross_entropy(
351
+ logits.float().view(-1, logits.size(-1)),
352
+ labels.view(-1),
353
+ ignore_index=-100,
354
+ )
355
+ loss = ce
356
+ # MTP auxiliary losses: head d predicts the token d positions ahead.
357
+ # Skip entirely when mtp_loss_weight == 0 to save the per-head matmul
358
+ # against the full vocab β€” that head alone is ~15-20 % of step time.
359
+ if self.cfg.mtp_depth > 0 and self.cfg.mtp_loss_weight > 0:
360
+ mtp_total = 0.0
361
+ for d, head in enumerate(self.mtp_heads, start=1):
362
+ if labels.size(1) <= d: continue
363
+ logits_d = head(h)[:, :-d, :].contiguous()
364
+ labels_d = labels[:, d:].contiguous()
365
+ mtp_total = mtp_total + F.cross_entropy(
366
+ logits_d.float().view(-1, logits_d.size(-1)),
367
+ labels_d.view(-1),
368
+ ignore_index=-100,
369
+ )
370
+ loss = loss + self.cfg.mtp_loss_weight * (mtp_total / max(self.cfg.mtp_depth, 1))
371
+ # Router aux losses (averaged over layers)
372
+ if aux_list and "aux_load" in aux_list[0]:
373
+ aux_load = torch.stack([a["aux_load"] for a in aux_list]).mean()
374
+ aux_z = torch.stack([a["aux_z"] for a in aux_list]).mean()
375
+ loss = (loss + self.cfg.router_aux_loss_coef * aux_load
376
+ + self.cfg.router_z_loss_coef * aux_z)
377
+ out["loss"] = loss
378
+ return out
379
+
380
+
381
+ # ─── Save / load ────────────────────────────────────────────────────────
382
+
383
+ def save_pluto(model: PlutoForCausalLM, output_dir: str | Path) -> None:
384
+ model.cfg.save(output_dir)
385
+ from safetensors.torch import save_model
386
+ # `save_model` handles tied weights (embed↔lm_head) by deduplicating them.
387
+ # We must NOT permanently move the model to CPU β€” restore device after save.
388
+ devices = {p.device for p in model.parameters()}
389
+ device = next(iter(devices)) if len(devices) == 1 else None
390
+ model_cpu = model.cpu()
391
+ save_model(model_cpu, str(Path(output_dir) / "model.safetensors"))
392
+ if device is not None and device.type != "cpu":
393
+ model.to(device)
394
+
395
+
396
+ def load_pluto(model_dir: str | Path, dtype=torch.bfloat16, map_location="cpu") -> PlutoForCausalLM:
397
+ cfg = PlutoConfig.load(model_dir)
398
+ model = PlutoForCausalLM(cfg).to(dtype)
399
+ from safetensors.torch import load_file
400
+ state = load_file(str(Path(model_dir) / "model.safetensors"), device=str(map_location))
401
+ model.load_state_dict(state, strict=False)
402
+ return model
403
+
404
+
405
+ # ─── Param accounting ──────────────────────────────────────────────────
406
+
407
+ def count_params(model: nn.Module) -> int:
408
+ return sum(p.numel() for p in model.parameters())
409
+
410
+
411
+ def estimate_active_params(cfg: PlutoConfig) -> dict:
412
+ """At-inference active params (MTP heads NOT counted, since they are training-only)."""
413
+ head_dim = cfg.hidden_size // cfg.n_heads
414
+ attn_per_layer = (
415
+ cfg.hidden_size * cfg.n_heads * head_dim # q_proj
416
+ + cfg.hidden_size * cfg.n_kv_heads * head_dim # k_proj
417
+ + cfg.hidden_size * cfg.n_kv_heads * head_dim # v_proj
418
+ + cfg.hidden_size * cfg.hidden_size # o_proj
419
+ )
420
+ expert_size = 3 * cfg.hidden_size * cfg.intermediate_size_expert # SwiGLU
421
+ shared_size = (3 * cfg.hidden_size * cfg.intermediate_size_shared
422
+ if cfg.intermediate_size_shared > 0 else 0)
423
+ active_per_layer = attn_per_layer + cfg.top_k * expert_size + shared_size
424
+ active_total = active_per_layer * cfg.n_layers
425
+ # lm_head is also "active" (full matmul against vocab)
426
+ active_total += cfg.vocab_size * cfg.hidden_size
427
+
428
+ total_experts = expert_size * cfg.n_experts * cfg.n_layers
429
+ total_shared = shared_size * cfg.n_layers
430
+ total_attn = attn_per_layer * cfg.n_layers
431
+ emb_params = cfg.vocab_size * cfg.hidden_size
432
+ lm_head_params = 0 if cfg.tie_word_embeddings else cfg.vocab_size * cfg.hidden_size
433
+ mtp_params = cfg.mtp_depth * cfg.vocab_size * cfg.hidden_size
434
+ total_params = (total_experts + total_shared + total_attn + emb_params
435
+ + lm_head_params + mtp_params
436
+ + 2 * cfg.n_layers * cfg.hidden_size # RMSNorm weights
437
+ + cfg.hidden_size)
438
+ return {
439
+ "total_params": total_params,
440
+ "active_inference_params": active_total,
441
+ "expert_total_params": total_experts,
442
+ "attn_total_params": total_attn,
443
+ "embedding_params": emb_params,
444
+ "lm_head_params": lm_head_params,
445
+ "mtp_head_params": mtp_params,
446
+ }
447
+
448
+
449
+ if __name__ == "__main__":
450
+ cfg = PlutoConfig()
451
+ stats = estimate_active_params(cfg)
452
+ for k, v in stats.items():
453
+ print(f" {k:<28} {v/1e6:>8.2f} M")
454
+ print(f" active/total ratio {stats['active_inference_params']/stats['total_params']*100:>5.2f} %")
455
+
456
+ m = PlutoForCausalLM(cfg)
457
+ n_real = count_params(m)
458
+ print(f"\n real (actual) total {n_real/1e6:>8.2f} M")
459
+ x = torch.randint(0, cfg.vocab_size, (2, 32))
460
+ out = m(x, labels=x)
461
+ print(f" fwd OK logits {tuple(out['logits'].shape)} loss={out['loss'].item():.4f}")
pluto_config.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vocab_size": 32768,
3
+ "hidden_size": 384,
4
+ "intermediate_size_expert": 1536,
5
+ "intermediate_size_shared": 0,
6
+ "n_layers": 16,
7
+ "n_heads": 6,
8
+ "n_kv_heads": 2,
9
+ "n_experts": 35,
10
+ "top_k": 1,
11
+ "n_languages": 5,
12
+ "max_position_embeddings": 4096,
13
+ "rope_theta": 1000000.0,
14
+ "rms_norm_eps": 1e-06,
15
+ "tie_word_embeddings": true,
16
+ "mtp_depth": 2,
17
+ "mtp_loss_weight": 0,
18
+ "router_aux_loss_coef": 0.01,
19
+ "router_z_loss_coef": 0.001,
20
+ "model_type": "astrai_pluto",
21
+ "pad_token_id": 0,
22
+ "bos_token_id": 1,
23
+ "eos_token_id": 2,
24
+ "tokenizer_name": "pluto_nano_32k_bpe"
25
+ }
special_tokens_map.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "pad_token": "<|pad|>",
3
+ "bos_token": "<|bos|>",
4
+ "eos_token": "<|eos|>",
5
+ "unk_token": "<|unk|>"
6
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tokenizer_class": "PreTrainedTokenizerFast",
3
+ "model_max_length": 4096,
4
+ "unk_token": "<|unk|>",
5
+ "pad_token": "<|pad|>",
6
+ "bos_token": "<|bos|>",
7
+ "eos_token": "<|eos|>"
8
+ }