Panhapich commited on
Commit
8561bc9
·
verified ·
1 Parent(s): a63818d

Add 86M llama-style checkpoint (epoch 1, step 71k), config, model code, and model card

Browse files
Files changed (4) hide show
  1. README.md +179 -0
  2. config.json +13 -0
  3. model.safetensors +3 -0
  4. modeling_llama_custom.py +139 -0
README.md CHANGED
@@ -1,3 +1,182 @@
1
  ---
2
  license: mit
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: mit
3
+ language:
4
+ - en
5
+ tags:
6
+ - text-generation
7
+ - pytorch
8
+ - from-scratch
9
+ - decoder-only
10
+ - llama-style
11
+ pipeline_tag: text-generation
12
  ---
13
+
14
+ # pre-train-llama
15
+
16
+ A **Llama-style decoder-only transformer** (RoPE, grouped-query attention,
17
+ SwiGLU MLP, RMSNorm) trained **from scratch** on English public-domain books.
18
+
19
+ This is a prototype / architecture testbed built ahead of the khmer-asr
20
+ project's own model work. It is trained on English classic literature — **not
21
+ on Khmer speech or text** — and is not usable for ASR as-is.
22
+
23
+ ## Architecture
24
+
25
+ | Hyperparameter | Value |
26
+ |---|---|
27
+ | Layers | 8 |
28
+ | Attention heads | 8 |
29
+ | KV heads (GQA) | 4 |
30
+ | Head dim | 96 |
31
+ | Hidden dim | 768 |
32
+ | FFN dim (SwiGLU) | 3072 |
33
+ | Max sequence length | 512 |
34
+ | Vocab size | 10,000 |
35
+ | Dropout | 0.1 |
36
+ | Parameters | 86.2M |
37
+ | Weights dtype | float32 |
38
+
39
+ Positional encoding is RoPE (theta 10,000), attention is grouped-query
40
+ attention with repeat-interleaved KV heads, the MLP is SwiGLU, and
41
+ normalization is RMSNorm applied **pre-block** (before attention and before the
42
+ MLP), with a final RMSNorm before the output projection. Input embeddings and
43
+ the output head are **not** tied. Architecturally this mirrors Llama; it is
44
+ trained from scratch, not initialized from Meta's weights.
45
+
46
+ ## Training data
47
+
48
+ Tokenizer and model were trained on eleven English-language books from Project
49
+ Gutenberg:
50
+
51
+ - Moby Dick
52
+ - Frankenstein
53
+ - Dracula
54
+ - Little Women
55
+ - Pride and Prejudice
56
+ - Alice's Adventures in Wonderland
57
+ - Crime and Punishment
58
+ - The Adventures of Tom Sawyer
59
+ - A Tale of Two Cities
60
+ - The Adventures of Sherlock Holmes
61
+ - War and Peace
62
+
63
+ Gutenberg header/footer boilerplate is stripped, blank lines removed, and the
64
+ books concatenated into a single stream. A byte-level BPE tokenizer
65
+ (`vocab_size=10000`, specials `[pad]`, `[eos]`) was trained on that corpus,
66
+ producing ~2.72M tokens. Training examples are **stride-1 sliding windows** of
67
+ 512 tokens, so consecutive examples overlap by 511 tokens.
68
+
69
+ ## Training procedure
70
+
71
+ | Setting | Value |
72
+ |---|---|
73
+ | Objective | Next-token prediction, cross-entropy, `[pad]` ignored |
74
+ | Optimizer | AdamW, peak LR 5e-4 |
75
+ | Schedule | Linear warmup 2,000 steps (0.01 → 1.0), then cosine decay to 0 |
76
+ | Gradient clipping | Global norm 6.0 |
77
+ | Batch | 8 × 4 gradient-accumulation steps = effective 32 |
78
+ | Precision | fp32 (bf16 matmuls internally on TPU) |
79
+ | Hardware | TPU via `torch_xla`, single core |
80
+
81
+ ### State of this checkpoint
82
+
83
+ Training is **incomplete** — this is a mid-run checkpoint, not a finished model.
84
+
85
+ | | |
86
+ |---|---|
87
+ | Checkpoint saved | 2026-07-24 03:21:00 |
88
+ | Epoch | 1 of 2 (in progress) |
89
+ | Micro-batch | 284,000 of ~340,500 |
90
+ | Optimizer steps | 71,000 |
91
+ | Learning rate at save | 3.20e-4 |
92
+ | Last training loss | 0.0513 |
93
+ | Best epoch loss | not yet recorded (no epoch has completed) |
94
+
95
+ Roughly 1.16B tokens have been processed, but only ~2.72M of them are distinct
96
+ — every token is seen ~512 times across overlapping windows within a single
97
+ epoch.
98
+
99
+ ## Usage
100
+
101
+ This is not a `transformers` model class. Load `model.safetensors` into the
102
+ `TextGenerationModel` defined in `modeling_llama_custom.py`:
103
+
104
+ ```python
105
+ import json
106
+
107
+ import torch
108
+ import torch.nn.functional as F
109
+ from huggingface_hub import snapshot_download
110
+ from safetensors.torch import load_file
111
+ from tokenizers import Tokenizer
112
+
113
+ path = snapshot_download("Panhapich/pre-train-llama")
114
+
115
+ import sys; sys.path.insert(0, path)
116
+ from modeling_llama_custom import TextGenerationModel, create_causal_mask
117
+
118
+ config = json.load(open(f"{path}/config.json"))
119
+ model = TextGenerationModel(**config["model_config"])
120
+ model.load_state_dict(load_file(f"{path}/model.safetensors"))
121
+ model.eval()
122
+
123
+ tokenizer = Tokenizer.from_file(f"{path}/tokenizer.json")
124
+
125
+
126
+ @torch.no_grad()
127
+ def generate(prompt, max_new_tokens=40, temperature=0.8):
128
+ ids = torch.tensor(tokenizer.encode(prompt).ids).unsqueeze(0)
129
+ for _ in range(max_new_tokens):
130
+ logits = model(ids)[:, -1, :] / temperature
131
+ next_id = torch.multinomial(F.softmax(logits, dim=-1), num_samples=1)
132
+ ids = torch.cat([ids, next_id], dim=1)
133
+ if next_id.item() == tokenizer.token_to_id("[eos]"):
134
+ break
135
+ return tokenizer.decode(ids[0].tolist())
136
+
137
+
138
+ print(generate("Once upon a time,"))
139
+ ```
140
+
141
+ Sequences longer than 512 tokens are not supported — the RoPE tables are
142
+ precomputed to `max_seq_len` and indexing past them will fail. There is no KV
143
+ cache, so generation recomputes the full context each step.
144
+
145
+ `tokenizer.json` must be the tokenizer these weights were trained with. A
146
+ freshly retrained BPE would assign different ids to the same text and the model
147
+ would emit nonsense without erroring.
148
+
149
+ ## Files
150
+
151
+ | File | What it is |
152
+ |---|---|
153
+ | `model.safetensors` | Model weights (86.2M params, fp32, ~345 MB) |
154
+ | `config.json` | `model_config` hyperparameters for reconstruction |
155
+ | `modeling_llama_custom.py` | `nn.Module` definitions the weights load into |
156
+ | `tokenizer.json` | The BPE tokenizer the weights were trained against |
157
+
158
+ ## Limitations and biases
159
+
160
+ - **The 0.0513 training loss is not a generalization result.** There is no
161
+ held-out validation split, and stride-1 windows mean the model sees each
162
+ passage hundreds of times per epoch. A loss that low on a 10k vocab indicates
163
+ the corpus has largely been memorized. Expect the model to reproduce long
164
+ verbatim spans of the source books, and expect much worse performance on any
165
+ text outside them.
166
+ - **No evaluation has been run** — no perplexity on held-out data, no
167
+ benchmarks. The only quality check performed is qualitative sampling.
168
+ - **Training is unfinished** (mid-epoch 1 of 2), so the cosine schedule has not
169
+ annealed and weights are not at a converged point.
170
+ - Trained on 19th-century literature, so output reflects the vocabulary,
171
+ style, and social attitudes of that corpus, including period-typical racist
172
+ and sexist content present in the source texts.
173
+ - **English only.** Despite the surrounding khmer-asr project, this model has
174
+ no Khmer training data and no speech or audio capability.
175
+ - Small (86M) and trained on ~2.7M unique tokens — orders of magnitude below
176
+ what general-purpose language models see. Treat output as a demonstration
177
+ that the architecture and training loop work, not as a useful generator.
178
+
179
+ ## Training code
180
+
181
+ Trained with `decoder_only_transformer_tpu.ipynb` from the khmer-asr project,
182
+ which runs on TPU, CUDA, MPS, or CPU and resumes from checkpoints in this repo.
config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architecture": "llama_style_decoder_only_custom",
3
+ "model_config": {
4
+ "num_layers": 8,
5
+ "num_heads": 8,
6
+ "num_kv_heads": 4,
7
+ "hidden_dim": 768,
8
+ "max_seq_len": 512,
9
+ "vocab_size": 10000,
10
+ "dropout": 0.1
11
+ },
12
+ "num_parameters": 86235664
13
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3e2f0fa680fc67cfc75c7738c2297a65ee508f84365648d916ce04befbaa5b4a
3
+ size 344956048
modeling_llama_custom.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model definition for Panhapich/pre-train-llama.
2
+ #
3
+ # Llama-style decoder-only transformer (RoPE, grouped-query attention, SwiGLU,
4
+ # RMSNorm), trained from scratch. This is not a `transformers`-library model
5
+ # class -- load model.safetensors into TextGenerationModel directly:
6
+ #
7
+ # import json
8
+ # from safetensors.torch import load_file
9
+ # from modeling_llama_custom import TextGenerationModel
10
+ #
11
+ # config = json.load(open("config.json"))
12
+ # model = TextGenerationModel(**config["model_config"])
13
+ # model.load_state_dict(load_file("model.safetensors"))
14
+ # model.eval()
15
+
16
+ import math
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ import torch.nn.functional as F
21
+
22
+ if not hasattr(nn, "RMSNorm"):
23
+ class _RMSNormFallback(nn.Module):
24
+ def __init__(self, dim, eps=1e-6):
25
+ super().__init__()
26
+ self.eps = eps
27
+ self.weight = nn.Parameter(torch.ones(dim))
28
+
29
+ def forward(self, x):
30
+ rms = x.pow(2).mean(dim=-1, keepdim=True).add(self.eps).rsqrt()
31
+ return x * rms * self.weight
32
+
33
+ nn.RMSNorm = _RMSNormFallback
34
+
35
+
36
+ class RotaryPositionalEncoding(nn.Module):
37
+ def __init__(self, head_dim, max_seq_len, theta=10000.0):
38
+ super().__init__()
39
+ inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2).float() / head_dim))
40
+ t = torch.arange(max_seq_len).float()
41
+ freqs = torch.outer(t, inv_freq)
42
+ self.register_buffer("cos", torch.cos(freqs), persistent=False)
43
+ self.register_buffer("sin", torch.sin(freqs), persistent=False)
44
+
45
+ def rotate(self, x):
46
+ T = x.shape[-2]
47
+ cos = self.cos[:T].unsqueeze(0).unsqueeze(0)
48
+ sin = self.sin[:T].unsqueeze(0).unsqueeze(0)
49
+ x1, x2 = x[..., 0::2], x[..., 1::2]
50
+ rotated = torch.stack([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
51
+ return rotated.flatten(-2)
52
+
53
+
54
+ class GQA(nn.Module):
55
+ def __init__(self, hidden_dim, num_heads, num_kv_heads, dropout=0.1):
56
+ super().__init__()
57
+ self.num_heads = num_heads
58
+ self.num_kv_heads = num_kv_heads
59
+ self.n_rep = num_heads // num_kv_heads
60
+ self.head_dim = hidden_dim // num_heads
61
+
62
+ self.q_proj = nn.Linear(hidden_dim, num_heads * self.head_dim)
63
+ self.k_proj = nn.Linear(hidden_dim, num_kv_heads * self.head_dim)
64
+ self.v_proj = nn.Linear(hidden_dim, num_kv_heads * self.head_dim)
65
+ self.out_proj = nn.Linear(num_heads * self.head_dim, hidden_dim)
66
+ self.dropout = nn.Dropout(dropout)
67
+
68
+ def forward(self, q, k, v, mask=None, rope=None):
69
+ B, T, _ = q.shape
70
+ q = self.q_proj(q).view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
71
+ k = self.k_proj(k).view(B, T, self.num_kv_heads, self.head_dim).transpose(1, 2)
72
+ v = self.v_proj(v).view(B, T, self.num_kv_heads, self.head_dim).transpose(1, 2)
73
+
74
+ if rope is not None:
75
+ q = rope.rotate(q)
76
+ k = rope.rotate(k)
77
+ if self.n_rep > 1:
78
+ k = k.repeat_interleave(self.n_rep, dim=1)
79
+ v = v.repeat_interleave(self.n_rep, dim=1)
80
+
81
+ scores = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
82
+ if mask is not None:
83
+ scores = scores.masked_fill(~mask.unsqueeze(1).bool(), float('-inf'))
84
+ attn = F.softmax(scores, dim=-1)
85
+ attn = self.dropout(attn)
86
+ out = attn @ v
87
+ out = out.transpose(1, 2).reshape(B, T, -1)
88
+ return self.out_proj(out)
89
+
90
+
91
+ class SwiGLU(nn.Module):
92
+ def __init__(self, hidden_dim, ff_dim):
93
+ super().__init__()
94
+ self.gate_proj = nn.Linear(hidden_dim, ff_dim)
95
+ self.up_proj = nn.Linear(hidden_dim, ff_dim)
96
+ self.down_proj = nn.Linear(ff_dim, hidden_dim)
97
+
98
+ def forward(self, x):
99
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
100
+
101
+
102
+ class DecoderLayer(nn.Module):
103
+ def __init__(self, hidden_dim, num_heads, num_kv_heads, dropout=0.1):
104
+ super().__init__()
105
+ self.self_attn = GQA(hidden_dim, num_heads, num_kv_heads, dropout)
106
+ self.mlp = SwiGLU(hidden_dim, 4 * hidden_dim)
107
+ self.norm1 = nn.RMSNorm(hidden_dim)
108
+ self.norm2 = nn.RMSNorm(hidden_dim)
109
+
110
+ def forward(self, x, mask=None, rope=None):
111
+ out = self.norm1(x)
112
+ out = self.self_attn(out, out, out, mask, rope)
113
+ x = out + x
114
+ out = self.norm2(x)
115
+ out = self.mlp(out)
116
+ return out + x
117
+
118
+ class TextGenerationModel(nn.Module):
119
+ def __init__(self, num_layers, num_heads, num_kv_heads, hidden_dim,
120
+ max_seq_len, vocab_size, dropout=0.1):
121
+ super().__init__()
122
+ self.rope = RotaryPositionalEncoding(hidden_dim // num_heads, max_seq_len)
123
+ self.embedding = nn.Embedding(vocab_size, hidden_dim)
124
+ self.decoders = nn.ModuleList([
125
+ DecoderLayer(hidden_dim, num_heads, num_kv_heads, dropout)
126
+ for _ in range(num_layers)
127
+ ])
128
+ self.norm = nn.RMSNorm(hidden_dim)
129
+ self.out = nn.Linear(hidden_dim, vocab_size)
130
+
131
+ def forward(self, ids, mask=None):
132
+ x = self.embedding(ids)
133
+ for decoder in self.decoders:
134
+ x = decoder(x, mask, self.rope)
135
+ x = self.norm(x)
136
+ return self.out(x)
137
+
138
+ def create_causal_mask(seq_len, device):
139
+ return torch.tril(torch.ones(seq_len, seq_len, dtype=torch.bool, device=device))