Add tokenizer_config.json, model.py, .gitattributes and README (renamed copy of gpt-s2.5-5m)

#5
Files changed (3) hide show
  1. README.md +85 -0
  2. model.py +153 -0
  3. tokenizer_config.json +7 -0
README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ pipeline_tag: text-generation
4
+ language: en
5
+ tags:
6
+ - tiny
7
+ - tiny-lm
8
+ - tiny-model
9
+ - slm
10
+ - small-language-model
11
+ - from-scratch
12
+ - gpt
13
+ - gqa
14
+ - swiglu
15
+ - rope
16
+ - rmsnorm
17
+ - cpu-trained
18
+ library_name: transformers
19
+ metrics:
20
+ - accuracy
21
+ model-index:
22
+ - name: HellaSwag
23
+ type: text-generation
24
+ results: []
25
+ ---
26
+
27
+ # compacttest-5m
28
+
29
+ **5.11M-parameter subword language model, trained from scratch on CPU.**
30
+
31
+ > This repo is a **renamed copy** of [`Compactbot/gpt-s2.5-5m`](https://huggingface.co/Compactbot/gpt-s2.5-5m),
32
+ > renamed at the request of @Datdanboi25 (see `Compactbot/model-requests` #2).
33
+ > Weights, config and architecture are **identical** to the original.
34
+
35
+ ## What it is
36
+
37
+ A small but genuine from-scratch causal LM in the GPT-X2.5 style:
38
+
39
+ - **Architecture:** GQA (8 query / 2 key-value heads) + SwiGLU FFN + RoPE + RMSNorm,
40
+ pre-norm, no biases, **weight-tied** input/output embeddings (the embedding matrix
41
+ doubles as the LM head — no separate `lm_head` tensor).
42
+ - **Params:** 5,114,112 (verified against the checkpoint).
43
+ - **Tokenizer:** 8,192-vocab byte-level BPE (custom, not a standard HF tokenizer).
44
+ - **Trained:** on CPU, ~94M TinyStories tokens, cosine LR schedule with warmup.
45
+
46
+ ## How to load
47
+
48
+ This is a self-contained `nn.Module`, not a transformers-native architecture.
49
+
50
+ ```python
51
+ import torch
52
+ from model import Model
53
+
54
+ m = Model()
55
+ from safetensors.torch import load_file
56
+ sd = load_file("model.safetensors")
57
+ m.load_state_dict(sd)
58
+ m.eval()
59
+
60
+ # greedy generation
61
+ with torch.no_grad():
62
+ x = torch.tensor([[1]])
63
+ for _ in range(60):
64
+ logits = m(x[:, -512:])[:, -1, :]
65
+ x = torch.cat([x, logits.argmax(-1, keepdim=True)], dim=1)
66
+ ```
67
+
68
+ ## Honest scope
69
+
70
+ - **Greedy-coherent, sampling-fragile.** At 5M params the model produces readable
71
+ prose under greedy decoding but degrades noticeably under sampling. That is the
72
+ expected behaviour at this scale, not a bug.
73
+ - **Intelligence index:** 0.032 (see the original card for the full eval breakdown).
74
+ - It is a reference build demonstrating that a 5M-param from-scratch subword LM is
75
+ trainable and coherent on CPU. It is not a chat model.
76
+
77
+ ## Files
78
+
79
+ | File | Purpose |
80
+ |---|---|
81
+ | `config.json` | Architecture config (matches `model.py` exactly) |
82
+ | `model.py` | Self-contained architecture definition |
83
+ | `model.safetensors` | Weights (5,114,112 params, F32) |
84
+ | `tokenizer.json` | 8192-vocab BPE tokenizer |
85
+ | `tokenizer_config.json` | Tokenizer metadata |
model.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GPT-S2.5 — 5.11M-param subword language model (from scratch).
3
+ Custom GPT-2-style architecture: GQA + SwiGLU + RoPE + RMSNorm, weight-tied head.
4
+
5
+ This is NOT a transformers-native architecture. It is a self-contained
6
+ nn.Module you can load directly:
7
+
8
+ import torch
9
+ from model import Model
10
+ m = Model()
11
+ sd = torch.load("best.pt", map_location="cpu")["model"] # or load safetensors
12
+ m.load_state_dict(sd); m.eval()
13
+
14
+ # generate (greedy):
15
+ with torch.no_grad():
16
+ x = torch.tensor([[1]])
17
+ for _ in range(60):
18
+ logits = m(x[:, -512:])[:, -1, :]
19
+ x = torch.cat([x, logits.argmax(-1, keepdim=True)], dim=1)
20
+
21
+ Architecture (matches config.json exactly):
22
+ vocab 8192 (BPE), n_embd 256, 4 layers, 8 q-heads / 2 kv-heads (GQA 4:1),
23
+ head_dim 32, SwiGLU FFN intermediate 768, RoPE (base 10000), RMSNorm (eps 1e-6),
24
+ pre-norm, no biases, weight-tied head (tok.weight reused as lm_head).
25
+ Total: 5,114,112 parameters.
26
+ """
27
+ import math
28
+ import torch
29
+ import torch.nn as nn
30
+ import torch.nn.functional as F
31
+
32
+ VOCAB = 8192
33
+ N_EMBD = 256
34
+ N_LAYERS = 4
35
+ N_Q_HEADS = 8
36
+ N_KV_HEADS = 2
37
+ HEAD_DIM = 32
38
+ INTER = 768
39
+ MAX_SEQ = 512
40
+ ROPE_BASE = 10000.0
41
+
42
+ Q_DIM = N_Q_HEADS * HEAD_DIM # 256
43
+ KV_DIM = N_KV_HEADS * HEAD_DIM # 64
44
+ assert Q_DIM == N_EMBD
45
+ assert N_Q_HEADS % N_KV_HEADS == 0
46
+
47
+
48
+ class RMSNorm(nn.Module):
49
+ def __init__(self, dim, eps=1e-6):
50
+ super().__init__()
51
+ self.eps = eps
52
+ self.weight = nn.Parameter(torch.ones(dim))
53
+
54
+ def forward(self, x):
55
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight
56
+
57
+
58
+ def precompute_rope(head_dim, max_seq, base):
59
+ freqs = 1.0 / (base ** (torch.arange(0, head_dim, 2).float() / head_dim))
60
+ t = torch.arange(max_seq, dtype=torch.float)
61
+ ang = torch.outer(t, freqs)
62
+ return ang.cos(), ang.sin() # [T, head_dim/2]
63
+
64
+
65
+ def apply_rope(x, cos, sin):
66
+ # x: [B, H, T, D]
67
+ T = x.size(2)
68
+ cos = cos[:T].view(1, 1, T, -1)
69
+ sin = sin[:T].view(1, 1, T, -1)
70
+ x1, x2 = x[..., 0::2], x[..., 1::2]
71
+ out = torch.empty_like(x)
72
+ out[..., 0::2] = x1 * cos - x2 * sin
73
+ out[..., 1::2] = x2 * cos + x1 * sin
74
+ return out
75
+
76
+
77
+ class Attention(nn.Module):
78
+ def __init__(self):
79
+ super().__init__()
80
+ self.wq = nn.Linear(N_EMBD, Q_DIM, bias=False)
81
+ self.wk = nn.Linear(N_EMBD, KV_DIM, bias=False)
82
+ self.wv = nn.Linear(N_EMBD, KV_DIM, bias=False)
83
+ self.wo = nn.Linear(Q_DIM, N_EMBD, bias=False)
84
+
85
+ def forward(self, x, cos, sin):
86
+ B, T, _ = x.shape
87
+ q = self.wq(x).view(B, T, N_Q_HEADS, HEAD_DIM).transpose(1, 2)
88
+ k = self.wk(x).view(B, T, N_KV_HEADS, HEAD_DIM).transpose(1, 2)
89
+ v = self.wv(x).view(B, T, N_KV_HEADS, HEAD_DIM).transpose(1, 2)
90
+ q = apply_rope(q, cos, sin)
91
+ k = apply_rope(k, cos, sin)
92
+ # GQA: repeat kv heads to match q heads
93
+ rep = N_Q_HEADS // N_KV_HEADS
94
+ k = k.repeat_interleave(rep, dim=1)
95
+ v = v.repeat_interleave(rep, dim=1)
96
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
97
+ y = y.transpose(1, 2).contiguous().view(B, T, Q_DIM)
98
+ return self.wo(y)
99
+
100
+
101
+ class MLP(nn.Module):
102
+ def __init__(self):
103
+ super().__init__()
104
+ self.wgate = nn.Linear(N_EMBD, INTER, bias=False)
105
+ self.wup = nn.Linear(N_EMBD, INTER, bias=False)
106
+ self.wdown = nn.Linear(INTER, N_EMBD, bias=False)
107
+
108
+ def forward(self, x):
109
+ return self.wdown(F.silu(self.wgate(x)) * self.wup(x))
110
+
111
+
112
+ class Block(nn.Module):
113
+ def __init__(self):
114
+ super().__init__()
115
+ self.ln1 = RMSNorm(N_EMBD)
116
+ self.attn = Attention()
117
+ self.ln2 = RMSNorm(N_EMBD)
118
+ self.mlp = MLP()
119
+
120
+ def forward(self, x, cos, sin):
121
+ x = x + self.attn(self.ln1(x), cos, sin)
122
+ x = x + self.mlp(self.ln2(x))
123
+ return x
124
+
125
+
126
+ class Model(nn.Module):
127
+ def __init__(self):
128
+ super().__init__()
129
+ self.tok = nn.Embedding(VOCAB, N_EMBD)
130
+ self.blocks = nn.ModuleList([Block() for _ in range(N_LAYERS)])
131
+ self.ln_f = RMSNorm(N_EMBD)
132
+ self.cos, self.sin = precompute_rope(HEAD_DIM, MAX_SEQ, ROPE_BASE)
133
+
134
+ def forward(self, idx, targets=None):
135
+ B, T = idx.shape
136
+ h = self.tok(idx)
137
+ cos, sin = self.cos, self.sin
138
+ for blk in self.blocks:
139
+ h = blk(h, cos, sin)
140
+ h = self.ln_f(h)
141
+ logits = F.linear(h, self.tok.weight) # weight-tied head
142
+ if targets is not None:
143
+ loss = F.cross_entropy(logits.view(-1, VOCAB), targets.view(-1))
144
+ return loss
145
+ return logits
146
+
147
+
148
+ if __name__ == "__main__":
149
+ m = Model()
150
+ total = sum(p.numel() for p in m.parameters())
151
+ print(f"params = {total} (expected 5114112)")
152
+ assert total == 5114112, "param count mismatch"
153
+ print("OK")
tokenizer_config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "tokenizer_class": "GPTS25BPE",
3
+ "model_type": "gpt-s2.5",
4
+ "vocab_size": 8192,
5
+ "add_prefix_space": false,
6
+ "note": "8192-vocab BPE (byte-level). This is a custom BPE, not a standard HF tokenizer; load bpe_8k.json / tokenizer.json directly."
7
+ }