English
samairtimer commited on
Commit
1548e47
·
verified ·
1 Parent(s): 71cf3eb

Add model architecture definitions

Browse files
Files changed (1) hide show
  1. MiniLLM.py +279 -0
MiniLLM.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import jax
2
+ import jax.numpy as jnp
3
+ import flax.nnx as nnx
4
+ import grain.python as pygrain
5
+ import optax
6
+ import tiktoken
7
+ from pathlib import Path
8
+
9
+ tokenizer = tiktoken.get_encoding("gpt2")
10
+
11
+ vocab_size = tokenizer.n_vocab
12
+ num_transformer_blocks = 6
13
+ maxlen = 128
14
+ embed_dim = 192
15
+ num_heads = 6
16
+ feed_forward_dim = int(2/3 * 4 * embed_dim)
17
+ batch_size = 24
18
+ num_epochs = 3
19
+
20
+ def load_stories_from_file(
21
+ file_path,
22
+ max_stories = None
23
+ ):
24
+ """
25
+ Efficiently load stories from a text file.
26
+ Each story ends with <|endoftext|>.
27
+ """
28
+ file_path = Path(file_path)
29
+ if not file_path.exists():
30
+ raise FileNotFoundError(f"Data file not found: {file_path}")
31
+
32
+ print(f"Loading stories from {file_path}...")
33
+ stories = []
34
+ current_story = []
35
+
36
+ with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
37
+ for line in f:
38
+ if '<|endoftext|>' in line:
39
+ parts = line.split('<|endoftext|>')
40
+ for part in parts[:-1]:
41
+ current_story.append(part)
42
+ story_text = ''.join(current_story).strip()
43
+ if story_text:
44
+ stories.append(story_text + '<|endoftext|>')
45
+ if max_stories and len(stories) >= max_stories:
46
+ break
47
+ current_story = []
48
+ if parts[-1].strip():
49
+ current_story = [parts[-1]]
50
+ else:
51
+ current_story = []
52
+ if max_stories and len(stories) >= max_stories:
53
+ break
54
+ else:
55
+ current_story.append(line)
56
+ if current_story and (not max_stories or len(stories) < max_stories):
57
+ story_text = ''.join(current_story).strip()
58
+ if story_text:
59
+ stories.append(story_text + '<|endoftext|>')
60
+
61
+ print(f"Loaded {len(stories):,} stories")
62
+ return stories
63
+
64
+
65
+
66
+ class TransformerBlock(nnx.Module):
67
+
68
+ def __init__(self, embed_dim, num_heads, ff_dim, *, rngs):
69
+
70
+ self.attention = nnx.MultiHeadAttention(
71
+ num_heads=num_heads,
72
+ in_features=embed_dim,
73
+ qkv_features=embed_dim,
74
+ out_features=embed_dim,
75
+ decode=False,
76
+ rngs=rngs
77
+ )
78
+
79
+ def __call__(self, x, mask=None):
80
+ attn_out = self.attention(x, mask=mask)
81
+ x = x + attn_out
82
+ return x
83
+
84
+ class TokenAndPositionEmbedding(nnx.Module):
85
+ def __init__(self, maxlen, vocab_size, embed_dim, *, rngs):
86
+ self.token_emb = nnx.Embed(vocab_size, embed_dim, rngs=rngs)
87
+ self.pos_emb = nnx.Embed(maxlen, embed_dim, rngs=rngs)
88
+
89
+ def __call__(self, x):
90
+ seq_len = x.shape[1]
91
+ positions = jnp.arange(seq_len)[None, :]
92
+ return self.token_emb(x) + self.pos_emb(positions)
93
+
94
+ class MiniGPT(nnx.Module):
95
+
96
+ def __init__(self, maxlen=maxlen, vocab_size=vocab_size, embed_dim=embed_dim, num_heads=num_heads,
97
+ feed_forward_dim=feed_forward_dim, num_transformer_blocks=num_transformer_blocks, *, rngs=nnx.Rngs(0)):
98
+
99
+ self.maxlen = maxlen
100
+
101
+ self.embedding = TokenAndPositionEmbedding(maxlen, vocab_size, embed_dim, rngs=rngs)
102
+
103
+ self.transformer_blocks = [
104
+ TransformerBlock(embed_dim, num_heads, feed_forward_dim, rngs=rngs)
105
+ for _ in range(num_transformer_blocks)
106
+ ]
107
+
108
+ self.output_layer = nnx.Linear(embed_dim, vocab_size, use_bias=False, rngs=rngs)
109
+
110
+ def causal_attention_mask(self, seq_len):
111
+ return jnp.tril(jnp.ones((seq_len, seq_len)))
112
+
113
+ def __call__(self, token_ids):
114
+ seq_len = token_ids.shape[1]
115
+ mask = self.causal_attention_mask(seq_len)
116
+
117
+ x = self.embedding(token_ids)
118
+
119
+ for block in self.transformer_blocks:
120
+ x = block(x, mask=mask)
121
+
122
+ logits = self.output_layer(x)
123
+
124
+ return logits
125
+
126
+ # Pass the tokenizer as an argument to break the global dependency
127
+ def generate_text(self, prompt, tokenizer, max_new_tokens=50, temperature=1.0):
128
+ # 1. Encode the string prompt into integer token IDs
129
+ tokens = tokenizer.encode(prompt)
130
+
131
+ # Cache the end token ID so we aren't encoding it on every single loop iteration
132
+ end_token_id = tokenizer.encode('<|endoftext|>', allowed_special={'<|endoftext|>'})[0]
133
+
134
+ for _ in range(max_new_tokens):
135
+ context = tokens[-self.maxlen:]
136
+
137
+ # RIGHT-pad to match training (not left-pad!)
138
+ actual_len = len(context)
139
+ if actual_len < self.maxlen:
140
+ context = context + [0] * (self.maxlen - actual_len)
141
+
142
+ context_array = jnp.array(context)[None, :]
143
+ logits = self(context_array)
144
+
145
+ next_token_logits = logits[0, actual_len - 1, :] / temperature
146
+ next_token = int(jnp.argmax(next_token_logits))
147
+
148
+ if next_token == end_token_id:
149
+ break
150
+
151
+ tokens.append(next_token)
152
+
153
+ # 2. Decode the final list of token IDs back into a string
154
+ return tokenizer.decode(tokens)
155
+
156
+
157
+
158
+ def generate_story(model, story_prompt, temperature, max_new_tokens):
159
+ start_tokens = tokenizer.encode(story_prompt)[:maxlen]
160
+ generated = generate_text(model, start_tokens, max_new_tokens=max_new_tokens, temperature=temperature)
161
+ return generated
162
+
163
+
164
+ class StoryDataset:
165
+ def __init__(self, stories, maxlen, tokenizer):
166
+ self.stories = stories
167
+ self.maxlen = maxlen
168
+ self.tokenizer = tokenizer
169
+ self.end_token = tokenizer.encode('<|endoftext|>', allowed_special={'<|endoftext|>'})[0]
170
+
171
+ def __len__(self):
172
+ return len(self.stories)
173
+
174
+ def __getitem__(self, idx):
175
+ story = self.stories[idx]
176
+ tokens = self.tokenizer.encode(story, allowed_special={'<|endoftext|>'})
177
+
178
+ if len(tokens) > self.maxlen:
179
+ tokens = tokens[:self.maxlen]
180
+
181
+ tokens.extend([0] * (self.maxlen - len(tokens)))
182
+ return tokens
183
+
184
+
185
+
186
+ def load_and_preprocess_data(
187
+ file_path,
188
+ batch_size,
189
+ maxlen,
190
+ max_stories = 100_000,
191
+ num_epochs = 1,
192
+ shuffle = False,
193
+ seed = 42
194
+ ):
195
+ """
196
+ Load and preprocess TinyStories data with memory-efficient chunk reading.
197
+
198
+ Args:
199
+ file_path: Path to the text file
200
+ batch_size: Batch size for training
201
+ maxlen: Maximum sequence length
202
+ max_stories: Maximum number of stories to load (for memory efficiency)
203
+ num_epochs: Number of training epochs
204
+ shuffle: Whether to shuffle the data
205
+ seed: Random seed for reproducibility
206
+
207
+ Returns:
208
+ Tuple of (Grain DataLoader, estimated_batches_per_epoch)
209
+ """
210
+
211
+ # Load and validate file
212
+ file_path = file_path
213
+
214
+ print(f"Loading data from {file_path} (max {max_stories:,} stories)")
215
+
216
+ # Read file in chunks to avoid loading entire file into memory
217
+ stories = []
218
+ current_story = []
219
+
220
+ with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
221
+ for line in f:
222
+ if '<|endoftext|>' in line:
223
+ # Split on end token and process parts
224
+ parts = line.split('<|endoftext|>')
225
+ for i, part in enumerate(parts[:-1]): # All but last part have end tokens
226
+ current_story.append(part)
227
+ story_text = ''.join(current_story).strip()
228
+ if story_text:
229
+ stories.append(story_text + '<|endoftext|>')
230
+ if len(stories) >= max_stories:
231
+ break
232
+ current_story = []
233
+
234
+ # Last part becomes start of next story
235
+ if parts[-1].strip():
236
+ current_story = [parts[-1]]
237
+
238
+ if len(stories) >= max_stories:
239
+ break
240
+ else:
241
+ current_story.append(line)
242
+
243
+ # Don't forget the last story if file doesn't end with end token
244
+ if current_story and len(stories) < max_stories:
245
+ story_text = ''.join(current_story).strip()
246
+ if story_text:
247
+ stories.append(story_text + '<|endoftext|>')
248
+
249
+ print(f"Loaded {len(stories):,} stories")
250
+ if len(stories) == 0:
251
+ raise ValueError("No valid stories found in the dataset")
252
+
253
+ # Calculate estimated batches per epoch
254
+ estimated_batches_per_epoch = len(stories) // batch_size
255
+ print(f"Estimated batches per epoch: {estimated_batches_per_epoch:,}")
256
+
257
+ # Create efficient dataset
258
+ dataset = StoryDataset(stories, maxlen, tokenizer)
259
+
260
+ # Configure sampler with sharding support
261
+ sampler = pygrain.IndexSampler(
262
+ num_records=len(dataset),
263
+ shuffle=shuffle,
264
+ seed=seed,
265
+ shard_options=pygrain.NoSharding(),
266
+ num_epochs=num_epochs,
267
+ )
268
+
269
+ # Create DataLoader with efficient batching
270
+ dataloader = pygrain.DataLoader(
271
+ data_source=dataset,
272
+ sampler=sampler,
273
+ operations=[
274
+ pygrain.Batch(batch_size=batch_size, drop_remainder=True)
275
+ ]
276
+ )
277
+
278
+ print(f"Created DataLoader with batch_size={batch_size}, maxlen={maxlen}")
279
+ return dataloader, estimated_batches_per_epoch