cfahlgren1 HF Staff commited on
Commit
e5dde63
·
verified ·
1 Parent(s): 0de7556

tinynext-v2-nofim: 12.5M params, 28 min on GPU

Browse files
v2/experiments/tinynext-v2-nofim/config.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vocab": 8192,
3
+ "dim": 320,
4
+ "layers": 8,
5
+ "heads": 5,
6
+ "ffn": 864,
7
+ "parameters": 12539200,
8
+ "seq": 512,
9
+ "special_tokens": {
10
+ "<|endoftext|>": 0,
11
+ "<|pre|>": 1,
12
+ "<|suf|>": 2,
13
+ "<|mid|>": 3
14
+ },
15
+ "prompt": "No text after the cursor: the text before it. Otherwise: <|suf|> after <|pre|> before <|mid|>."
16
+ }
v2/experiments/tinynext-v2-nofim/eval.py ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.11,<3.14"
3
+ # dependencies = [
4
+ # "torch>=2.6,<2.8",
5
+ # "datasets>=3.0",
6
+ # "numpy<3",
7
+ # "onnxruntime>=1.20",
8
+ # "transformers>=4.46",
9
+ # "tokenizers>=0.20",
10
+ # ]
11
+ # ///
12
+ """Scores autocomplete models the way people use them: type real text, pause, and see if the gray suggestion is right.
13
+
14
+ Every model sees the same pauses in the same text and is decoded with the same rule as offline.page:
15
+ follow the model's top pick while the running product of probabilities stays above a cutoff, and show
16
+ the text up to the last finished word. A suggestion is right when the text really continues that way.
17
+
18
+ Two scenarios:
19
+ typing the cursor is at the end of what's written so far.
20
+ editing the cursor is mid-sentence and real text follows a gap of 2-6 words. Only fill-in-the-middle
21
+ models are given that text; the rest see only what's before the cursor.
22
+
23
+ Usage:
24
+ uv run eval.py --model tinynext-v1 --onnx model_quint8.onnx
25
+ uv run eval.py --model smollm2-135m --hf HuggingFaceTB/SmolLM2-135M
26
+ uv run eval.py --table # compare every metrics/*.json
27
+ """
28
+
29
+ import argparse
30
+ import hashlib
31
+ import json
32
+ import random
33
+ import re
34
+ import statistics
35
+ import time
36
+ from collections.abc import Callable
37
+ from dataclasses import dataclass, field
38
+ from pathlib import Path
39
+
40
+ import numpy as np
41
+
42
+ HERE = Path(__file__).parent
43
+ EVAL_SET = HERE / "eval_set.json"
44
+ METRICS = HERE / "metrics"
45
+
46
+ CUTOFFS = (0.3, 0.4, 0.5, 0.6, 0.7)
47
+ MAIN_CUTOFF = 0.5 # what offline.page uses
48
+ PAUSE_EVERY = 12 # characters between pauses
49
+ MIN_SHOWN_CHARS = 4 # offline.page hides shorter suggestions
50
+ MAX_WORDS = 6
51
+ MAX_CHARS = 48
52
+ DOMAINS = ("articles", "messages", "email")
53
+ SCENARIOS = ("typing", "editing")
54
+ PUNCTUATION = ".,!?;:"
55
+
56
+ # ---- Eval text -------------------------------------------------------------------------------------------------
57
+ # Kept apart from training: Wikipedia isn't in either model's data, OpenAssistant validation isn't in SmolTalk's
58
+ # training split, and the emails come from the senders the original enron-autocomplete held out for its test set.
59
+
60
+ CUT_REPLY = re.compile(
61
+ r"(?im)^\s*(?:-+\s*original message|-+\s*forwarded by|-+\s*inline attachment|from:\s|to:\s|sent:\s"
62
+ r"|subject:\s|cc:\s|.*\bwrote:\s*$|_{5,}|-{5,}|={5,}|\*{5,}|\d{1,2}/\d{1,2}/\d{2,4}\s+\d{1,2}:\d{2})"
63
+ )
64
+ SENSITIVE = re.compile(r"(?i)https?://|www\.|\S+@\S+|\(?\b\d{3}\)?[-. ]\d{3}[-. ]\d{4}\b|\b\d{3}-\d{2}-\d{4}\b")
65
+ BULK_SENDER = re.compile(r"(?i)announce|newsletter|mailer|outlook\.team|^all[._]|general\.|chairman|enron\.(?:on|net)")
66
+ SMART_CHARS = str.maketrans({"‘": "'", "’": "'", "“": '"', "”": '"', "–": "-", "—": "-", "\t": " "})
67
+
68
+
69
+ def clean_email(body: str) -> list[str]:
70
+ """The same cleaning enron-autocomplete was trained with."""
71
+ text = body.replace("\r\n", "\n").replace("=20", " ").replace("=\n", "").translate(SMART_CHARS)
72
+ text = "\n".join(line for line in text.split("\n") if not line.lstrip().startswith(">"))
73
+ cut = CUT_REPLY.search(text)
74
+ if cut:
75
+ text = text[: cut.start()]
76
+ text = re.sub(r"(?m)^-- *\n[\s\S]*", "", text)
77
+ paragraphs = []
78
+ for block in re.split(r"\n\s*\n", text):
79
+ lines = [re.sub(r" +", " ", line).strip() for line in block.split("\n") if line.strip()]
80
+ if not lines:
81
+ continue
82
+ joined = lines[0]
83
+ for previous, line in zip(lines, lines[1:]):
84
+ joined += (" " if len(previous) >= 50 else "\n") + line
85
+ if SENSITIVE.search(joined) or sum(c.isalpha() for c in joined) < 0.6 * len(joined):
86
+ continue
87
+ paragraphs.append(joined)
88
+ return paragraphs
89
+
90
+
91
+ def is_held_out_sender(sender: str) -> bool:
92
+ """enron-autocomplete put senders whose hash bucket is under 5 in its test split."""
93
+ sender = sender.lower()
94
+ if "@enron.com" not in sender or BULK_SENDER.search(sender):
95
+ return False
96
+ return int(hashlib.sha1(sender.encode()).hexdigest(), 16) % 100 < 5
97
+
98
+
99
+ def ascii_paragraphs(text: str, min_chars: int = 40) -> list[str]:
100
+ text = text.translate(SMART_CHARS).encode("ascii", "ignore").decode()
101
+ return [p.strip() for p in re.split(r"\n\s*\n|\n", text) if len(p.strip()) >= min_chars]
102
+
103
+
104
+ def build_eval_set(docs_per_domain: int = 80) -> dict[str, list[list[str]]]:
105
+ from datasets import load_dataset
106
+
107
+ sets: dict[str, list[list[str]]] = {domain: [] for domain in DOMAINS}
108
+ wiki = load_dataset(
109
+ "wikimedia/wikipedia", data_files="20231101.en/train-00040-of-00041.parquet", split="train", streaming=True
110
+ )
111
+ for row in wiki:
112
+ paragraphs = [p for p in ascii_paragraphs(row["text"], 80) if not p.endswith(":")][:6]
113
+ if len(paragraphs) >= 3:
114
+ sets["articles"].append(paragraphs)
115
+ if len(sets["articles"]) >= docs_per_domain:
116
+ break
117
+
118
+ for row in load_dataset("OpenAssistant/oasst1", split="validation"):
119
+ if row["lang"] == "en" and "```" not in row["text"]:
120
+ paragraphs = ascii_paragraphs(row["text"])[:6]
121
+ if paragraphs:
122
+ sets["messages"].append(paragraphs)
123
+ if len(sets["messages"]) >= docs_per_domain:
124
+ break
125
+
126
+ for row in load_dataset("corbt/enron-emails", split="train", streaming=True):
127
+ if is_held_out_sender(row["from"] or ""):
128
+ paragraphs = [p for p in clean_email(row["body"] or "") if len(p) >= 40][:6]
129
+ if paragraphs:
130
+ sets["email"].append(paragraphs)
131
+ if len(sets["email"]) >= docs_per_domain:
132
+ break
133
+ return sets
134
+
135
+
136
+ def load_eval_set() -> dict[str, list[list[str]]]:
137
+ if not EVAL_SET.exists():
138
+ EVAL_SET.write_text(json.dumps(build_eval_set(), indent=1))
139
+ return json.loads(EVAL_SET.read_text())
140
+
141
+
142
+ @dataclass
143
+ class Pause:
144
+ domain: str
145
+ scenario: str
146
+ before: str # the document up to the cursor
147
+ after: str # text after the cursor that a model may see (empty when typing)
148
+ truth: str # what the person actually typed next
149
+
150
+
151
+ def pauses(eval_set: dict[str, list[list[str]]], max_per_domain: int) -> list[Pause]:
152
+ rng = random.Random(0)
153
+ found: list[Pause] = []
154
+ for domain in DOMAINS:
155
+ in_domain: list[Pause] = []
156
+ for paragraphs in eval_set[domain]:
157
+ for index, paragraph in enumerate(paragraphs):
158
+ earlier = "\n".join(paragraphs[:index])
159
+ for at in range(PAUSE_EVERY, len(paragraph) - 8, PAUSE_EVERY):
160
+ before = (earlier + "\n" if earlier else "") + paragraph[:at]
161
+ truth = paragraph[at:]
162
+ in_domain.append(Pause(domain, "typing", before, "", truth))
163
+ # Editing: a gap of 2-6 words, then the rest of the paragraph as it was written.
164
+ gap_words = rng.randint(2, 6)
165
+ gap = re.match(r"(\S*\s+){%d}" % gap_words, truth)
166
+ if gap and len(truth) > gap.end() + 10:
167
+ in_domain.append(Pause(domain, "editing", before, truth[gap.end():], truth))
168
+ rng.shuffle(in_domain)
169
+ found += [p for p in in_domain if p.scenario == "typing"][:max_per_domain]
170
+ found += [p for p in in_domain if p.scenario == "editing"][:max_per_domain]
171
+ return found
172
+
173
+
174
+ # ---- Decoding -------------------------------------------------------------------------------------------------
175
+
176
+
177
+ @dataclass
178
+ class Trajectory:
179
+ """Suggestions that end at a word boundary, with the model's confidence in each. Greedy decoding gives the
180
+ same path for every cutoff, so one decode answers all of them."""
181
+
182
+ candidates: list[tuple[str, float]] = field(default_factory=list)
183
+ milliseconds: float = 0.0
184
+
185
+ def at(self, cutoff: float) -> str:
186
+ best = ""
187
+ for text, confidence in self.candidates:
188
+ if confidence >= cutoff:
189
+ best = text
190
+ return best.rstrip()
191
+
192
+
193
+ class Model:
194
+ name: str
195
+ sees_after: bool = False
196
+
197
+ def suggest(self, before: str, after: str) -> Trajectory:
198
+ raise NotImplementedError
199
+
200
+
201
+ class CharGRU(Model):
202
+ """enron-autocomplete / tinynext v1: one character per step, reading only the current paragraph."""
203
+
204
+ HIDDEN, END, NEWLINE, FIRST, OFFSET = 512, 1, 2, 3, 29
205
+
206
+ def __init__(self, name: str, onnx_path: str):
207
+ import onnxruntime as ort
208
+
209
+ self.name = name
210
+ self.session = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
211
+ self.read_text: str | None = None
212
+ self.reading = None
213
+
214
+ def step(self, char_id: int, state):
215
+ logits, state = self.session.run(None, {"input_id": np.array([char_id], np.int64), "state": state})
216
+ return logits[0], state
217
+
218
+ def encode(self, char: str) -> int | None:
219
+ if char == "\n":
220
+ return self.NEWLINE
221
+ code = ord(char)
222
+ return code - self.OFFSET if 32 <= code <= 126 else None
223
+
224
+ def read(self, text: str):
225
+ if self.read_text is None or not text.startswith(self.read_text):
226
+ self.reading = self.step(self.END, np.zeros((1, self.HIDDEN), np.float32))
227
+ self.read_text = ""
228
+ for char in text[len(self.read_text):]:
229
+ char_id = self.encode(char)
230
+ if char_id is not None:
231
+ self.reading = self.step(char_id, self.reading[1])
232
+ self.read_text = text
233
+ return self.reading
234
+
235
+ def suggest(self, before: str, after: str) -> Trajectory:
236
+ began = time.perf_counter()
237
+ logits, state = self.read(before.split("\n")[-1])
238
+ trajectory, run, confidence, words = Trajectory(), "", 1.0, 0
239
+ while len(run) < MAX_CHARS and confidence >= min(CUTOFFS):
240
+ probabilities = np.exp(logits - logits.max())
241
+ probabilities /= probabilities.sum()
242
+ char_id = int(probabilities.argmax())
243
+ if char_id < self.FIRST:
244
+ if run:
245
+ trajectory.candidates.append((run, confidence))
246
+ break
247
+ char = chr(char_id + self.OFFSET)
248
+ if char == " ":
249
+ trajectory.candidates.append((run, confidence))
250
+ words += 1
251
+ if words >= MAX_WORDS:
252
+ break
253
+ confidence *= float(probabilities[char_id])
254
+ run += char
255
+ if char in PUNCTUATION:
256
+ trajectory.candidates.append((run, confidence))
257
+ logits, state = self.step(char_id, state)
258
+ trajectory.milliseconds = (time.perf_counter() - began) * 1000
259
+ return trajectory
260
+
261
+
262
+ class TokenLM(Model):
263
+ """A word-piece model. `next_logits(ids, cache)` runs one forward pass and returns (last logits, cache).
264
+
265
+ The word being typed is "healed": the prompt stops before it, and the first generated pieces must spell it
266
+ out, so a half-typed word like "resig" can still become "resignation" in one piece.
267
+ """
268
+
269
+ def __init__(self, name: str, next_logits: Callable, encode: Callable[[str], list[int]], token_texts: list[str],
270
+ stop_ids: set[int], prompt: Callable[[list[int], list[int]], list[int]] | None = None,
271
+ context_chars: int = 1500, suffix_chars: int = 300):
272
+ self.name = name
273
+ self.next_logits = next_logits
274
+ self.encode = encode
275
+ self.token_texts = token_texts
276
+ self.stop_ids = stop_ids
277
+ self.prompt = prompt
278
+ self.sees_after = prompt is not None
279
+ self.context_chars = context_chars
280
+ self.suffix_chars = suffix_chars
281
+ lengths = np.array([len(t) for t in token_texts])
282
+ self.nonempty = lengths > 0
283
+
284
+ def allowed(self, need: str) -> np.ndarray:
285
+ """Pieces consistent with the rest of the half-typed word."""
286
+ return np.array([bool(t) and (need.startswith(t) or t.startswith(need)) for t in self.token_texts])
287
+
288
+ def suggest(self, before: str, after: str) -> Trajectory:
289
+ began = time.perf_counter()
290
+ before = before[-self.context_chars:]
291
+ # The half-typed word keeps its leading space, since word pieces start with one. A line break stays in the prompt.
292
+ space, newline = before.rfind(" "), before.rfind("\n")
293
+ if space > newline:
294
+ stem, need = before[:space], before[space:]
295
+ else:
296
+ stem, need = before[: newline + 1], before[newline + 1:]
297
+ prefix_ids = self.encode(stem)
298
+ if self.prompt:
299
+ ids = self.prompt(prefix_ids, self.encode(after[: self.suffix_chars]) if after else [])
300
+ else:
301
+ ids = prefix_ids
302
+ logits, cache = self.next_logits(ids, None)
303
+
304
+ trajectory, generated, confidence = Trajectory(), "", 1.0
305
+ for _ in range(16):
306
+ logits = logits.astype(np.float64)
307
+ if need:
308
+ mask = self.allowed(need)
309
+ if not mask.any():
310
+ break
311
+ logits = np.where(mask, logits, -np.inf)
312
+ probabilities = np.exp(logits - logits.max())
313
+ probabilities /= probabilities.sum()
314
+ token = int(probabilities.argmax())
315
+ text = self.token_texts[token]
316
+ if token in self.stop_ids or "\n" in text:
317
+ if generated:
318
+ trajectory.candidates.append((generated, confidence))
319
+ break
320
+ if need:
321
+ consumed = min(len(need), len(text))
322
+ text, need = text[consumed:], need[consumed:]
323
+ if text[:1].isspace() and generated:
324
+ trajectory.candidates.append((generated, confidence))
325
+ confidence *= float(probabilities[token])
326
+ generated += text
327
+ if generated.rstrip()[-1:] in PUNCTUATION and generated.strip():
328
+ trajectory.candidates.append((generated, confidence))
329
+ if confidence < min(CUTOFFS) or len(generated) > MAX_CHARS or len(generated.split()) > MAX_WORDS:
330
+ break
331
+ logits, cache = self.next_logits([token], cache)
332
+ trajectory.milliseconds = (time.perf_counter() - began) * 1000
333
+ return trajectory
334
+
335
+
336
+ def hf_causal_lm(name: str, repo: str) -> TokenLM:
337
+ import torch
338
+ from transformers import AutoModelForCausalLM, AutoTokenizer
339
+
340
+ device = "mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu"
341
+ tokenizer = AutoTokenizer.from_pretrained(repo)
342
+ model = AutoModelForCausalLM.from_pretrained(repo, torch_dtype=torch.float32).to(device).eval()
343
+
344
+ @torch.no_grad()
345
+ def next_logits(ids, cache):
346
+ out = model(torch.tensor([ids], device=device), past_key_values=cache, use_cache=True)
347
+ return out.logits[0, -1].float().cpu().numpy(), out.past_key_values
348
+
349
+ texts = [tokenizer.decode([i]) for i in range(len(tokenizer))]
350
+ stops = {tokenizer.eos_token_id} if tokenizer.eos_token_id is not None else set()
351
+ encode = lambda text: tokenizer.encode(text, add_special_tokens=False) or [tokenizer.eos_token_id]
352
+ return TokenLM(name, next_logits, encode, texts, stops)
353
+
354
+
355
+ def onnx_token_lm(name: str, folder: str, fim: bool) -> TokenLM:
356
+ """A tinynext v2 export: graph.onnx + weights.f32 (compressed, expanded) or model.onnx, plus tokenizer.json."""
357
+ import onnxruntime as ort
358
+ from tokenizers import Tokenizer
359
+
360
+ folder = Path(folder)
361
+ graph = folder / "graph.onnx" if (folder / "graph.onnx").exists() else folder / "model.onnx"
362
+ session = ort.InferenceSession(str(graph), providers=["CPUExecutionProvider"])
363
+ config = json.loads((folder / "config.json").read_text())
364
+ tokenizer = Tokenizer.from_file(str(folder / "tokenizer.json"))
365
+ empty = np.zeros((config["layers"], 1, config["heads"], 0, config["dim"] // config["heads"]), np.float32)
366
+
367
+ def next_logits(ids, cache):
368
+ past_k, past_v = cache if cache else (empty, empty)
369
+ logits, k, v = session.run(None, {"input_ids": np.array([ids], np.int64), "past_k": past_k, "past_v": past_v})
370
+ return logits[0], (k, v)
371
+
372
+ special = len(config["special_tokens"])
373
+ texts = [tokenizer.decode([i], skip_special_tokens=False) if i >= special else "" for i in range(config["vocab"])]
374
+ encode = lambda text: tokenizer.encode(text).ids or [0]
375
+ prompt = (lambda prefix, suffix: prefix if not suffix else [2, *suffix, 1, *prefix, 3]) if fim else None
376
+ return TokenLM(name, next_logits, encode, texts, set(range(special)), prompt=prompt)
377
+
378
+
379
+ # ---- Scoring ---------------------------------------------------------------------------------------------------
380
+
381
+
382
+ def word_by_word(suggestion: str, truth: str) -> int:
383
+ """Characters you'd keep accepting a word at a time until a word is wrong."""
384
+ kept = 0
385
+ for match in re.finditer(r"\s*\S+", suggestion):
386
+ if truth.startswith(suggestion[: match.end()]):
387
+ kept = match.end()
388
+ else:
389
+ break
390
+ return kept
391
+
392
+
393
+ def score(model: Model, all_pauses: list[Pause]) -> dict:
394
+ results: dict = {"model": model.name, "sees_after": model.sees_after, "cutoffs": {}, "examples": [], "latency_ms": {}}
395
+ latencies: list[float] = []
396
+ raw = []
397
+ for index, pause in enumerate(all_pauses):
398
+ trajectory = model.suggest(pause.before, pause.after if model.sees_after else "")
399
+ latencies.append(trajectory.milliseconds)
400
+ raw.append((pause, trajectory))
401
+ if index % 500 == 0:
402
+ print(f" {model.name}: {index}/{len(all_pauses)} pauses, median {statistics.median(latencies):.1f} ms", flush=True)
403
+
404
+ for cutoff in CUTOFFS:
405
+ table: dict = {}
406
+ for domain in DOMAINS:
407
+ for scenario in SCENARIOS:
408
+ rows = [(p, t.at(cutoff)) for p, t in raw if p.domain == domain and p.scenario == scenario]
409
+ if not rows:
410
+ continue
411
+ shown = [(p, s) for p, s in rows if len(s) >= MIN_SHOWN_CHARS]
412
+ right = [(p, s) for p, s in shown if p.truth.startswith(s)]
413
+ multi = [(p, s) for p, s in shown if len(s.split()) >= 2]
414
+ multi_right = [(p, s) for p, s in multi if p.truth.startswith(s)]
415
+ typed = len(rows) * PAUSE_EVERY
416
+ table[f"{domain}/{scenario}"] = {
417
+ "pauses": len(rows),
418
+ "shown": round(len(shown) / len(rows), 4),
419
+ "right": round(len(right) / max(1, len(shown)), 4),
420
+ "avg_right_chars": round(sum(len(s) for _, s in right) / max(1, len(right)), 2),
421
+ "multiword_shown": round(len(multi) / len(rows), 4),
422
+ "multiword_right": round(len(multi_right) / max(1, len(multi)), 4),
423
+ "saved_with_tab": round(sum(len(s) for _, s in right) / typed, 4),
424
+ "saved_word_by_word": round(sum(word_by_word(s, p.truth) for p, s in shown) / typed, 4),
425
+ }
426
+ results["cutoffs"][str(cutoff)] = table
427
+
428
+ for pause, trajectory in raw[:: max(1, len(raw) // 60)]:
429
+ suggestion = trajectory.at(MAIN_CUTOFF)
430
+ if len(suggestion) >= MIN_SHOWN_CHARS:
431
+ results["examples"].append({
432
+ "domain": pause.domain, "scenario": pause.scenario, "before": pause.before[-60:],
433
+ "suggestion": suggestion, "truth": pause.truth[: len(suggestion) + 12], "right": pause.truth.startswith(suggestion),
434
+ })
435
+ results["latency_ms"] = {
436
+ "p50": round(statistics.median(latencies), 2),
437
+ "p95": round(float(np.percentile(latencies, 95)), 2),
438
+ "note": "Measured in this eval process, not in a browser.",
439
+ }
440
+ return results
441
+
442
+
443
+ # ---- Report ----------------------------------------------------------------------------------------------------
444
+
445
+
446
+ def table(paths: list[Path]) -> str:
447
+ runs = [json.loads(p.read_text()) for p in paths]
448
+ lines = []
449
+ for scenario in SCENARIOS:
450
+ lines.append(f"\n### {scenario.capitalize()}, {int(MAIN_CUTOFF * 100)}% cutoff, suggestions of {MIN_SHOWN_CHARS}+ characters\n")
451
+ lines.append("| Model | Domain | Shown | Right | Avg right | 2+ words shown | 2+ words right | Saved (Tab) | Saved (word by word) |")
452
+ lines.append("|---|---|---|---|---|---|---|---|---|")
453
+ for run in runs:
454
+ for domain in DOMAINS:
455
+ m = run["cutoffs"][str(MAIN_CUTOFF)].get(f"{domain}/{scenario}")
456
+ if not m:
457
+ continue
458
+ lines.append(
459
+ f"| {run['model']} | {domain} | {m['shown']:.0%} | {m['right']:.0%} | {m['avg_right_chars']:.1f} ch "
460
+ f"| {m['multiword_shown']:.0%} | {m['multiword_right']:.0%} | {m['saved_with_tab']:.1%} | {m['saved_word_by_word']:.1%} |"
461
+ )
462
+ lines.append("\n| Model | Latency p50 | p95 |\n|---|---|---|")
463
+ for run in runs:
464
+ lines.append(f"| {run['model']} | {run['latency_ms']['p50']} ms | {run['latency_ms']['p95']} ms |")
465
+ return "\n".join(lines)
466
+
467
+
468
+ def main():
469
+ parser = argparse.ArgumentParser()
470
+ parser.add_argument("--model", help="Name for the metrics file")
471
+ parser.add_argument("--onnx", help="A character-level GRU exported like enron-autocomplete")
472
+ parser.add_argument("--hf", help="A Hugging Face causal LM to evaluate, e.g. HuggingFaceTB/SmolLM2-135M")
473
+ parser.add_argument("--onnx-lm", help="Folder with a tinynext v2 ONNX export, config.json and tokenizer.json")
474
+ parser.add_argument("--no-fim", action="store_true", help="With --onnx-lm: the model wasn't trained to fill in the middle")
475
+ parser.add_argument("--max-pauses", type=int, default=1000, help="Per domain and scenario")
476
+ parser.add_argument("--table", action="store_true", help="Print a comparison of every metrics/*.json")
477
+ args = parser.parse_args()
478
+
479
+ if args.table:
480
+ print(table(sorted(METRICS.glob("*.json"))))
481
+ return
482
+ if args.onnx:
483
+ model: Model = CharGRU(args.model, args.onnx)
484
+ elif args.hf:
485
+ model = hf_causal_lm(args.model, args.hf)
486
+ elif args.onnx_lm:
487
+ model = onnx_token_lm(args.model, args.onnx_lm, fim=not args.no_fim)
488
+ else:
489
+ parser.error("Pass --onnx, --hf or --table. train.py evaluates its own model through score().")
490
+ all_pauses = pauses(load_eval_set(), args.max_pauses)
491
+ print(f"{len(all_pauses)} pauses", flush=True)
492
+ results = score(model, all_pauses)
493
+ METRICS.mkdir(exist_ok=True)
494
+ (METRICS / f"{args.model}.json").write_text(json.dumps(results, indent=1))
495
+ print(table([METRICS / f"{args.model}.json"]))
496
+
497
+
498
+ if __name__ == "__main__":
499
+ main()
v2/experiments/tinynext-v2-nofim/eval_set.json ADDED
The diff for this file is too large to render. See raw diff
 
v2/experiments/tinynext-v2-nofim/metrics/tinynext-v2-nofim.json ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "tinynext-v2-nofim",
3
+ "sees_after": false,
4
+ "cutoffs": {
5
+ "0.3": {
6
+ "articles/typing": {
7
+ "pauses": 1000,
8
+ "shown": 0.191,
9
+ "right": 0.5969,
10
+ "avg_right_chars": 5.94,
11
+ "multiword_shown": 0.069,
12
+ "multiword_right": 0.5217,
13
+ "saved_with_tab": 0.0564,
14
+ "saved_word_by_word": 0.064
15
+ },
16
+ "articles/editing": {
17
+ "pauses": 1000,
18
+ "shown": 0.163,
19
+ "right": 0.6074,
20
+ "avg_right_chars": 6.13,
21
+ "multiword_shown": 0.07,
22
+ "multiword_right": 0.6,
23
+ "saved_with_tab": 0.0506,
24
+ "saved_word_by_word": 0.0554
25
+ },
26
+ "messages/typing": {
27
+ "pauses": 1000,
28
+ "shown": 0.3,
29
+ "right": 0.58,
30
+ "avg_right_chars": 7.48,
31
+ "multiword_shown": 0.18,
32
+ "multiword_right": 0.5611,
33
+ "saved_with_tab": 0.1084,
34
+ "saved_word_by_word": 0.1323
35
+ },
36
+ "messages/editing": {
37
+ "pauses": 1000,
38
+ "shown": 0.3,
39
+ "right": 0.5767,
40
+ "avg_right_chars": 7.34,
41
+ "multiword_shown": 0.182,
42
+ "multiword_right": 0.5275,
43
+ "saved_with_tab": 0.1057,
44
+ "saved_word_by_word": 0.1288
45
+ },
46
+ "email/typing": {
47
+ "pauses": 1000,
48
+ "shown": 0.24,
49
+ "right": 0.5292,
50
+ "avg_right_chars": 6.03,
51
+ "multiword_shown": 0.08,
52
+ "multiword_right": 0.45,
53
+ "saved_with_tab": 0.0638,
54
+ "saved_word_by_word": 0.0718
55
+ },
56
+ "email/editing": {
57
+ "pauses": 1000,
58
+ "shown": 0.237,
59
+ "right": 0.557,
60
+ "avg_right_chars": 5.95,
61
+ "multiword_shown": 0.091,
62
+ "multiword_right": 0.5385,
63
+ "saved_with_tab": 0.0655,
64
+ "saved_word_by_word": 0.0717
65
+ }
66
+ },
67
+ "0.4": {
68
+ "articles/typing": {
69
+ "pauses": 1000,
70
+ "shown": 0.133,
71
+ "right": 0.7293,
72
+ "avg_right_chars": 5.86,
73
+ "multiword_shown": 0.043,
74
+ "multiword_right": 0.7209,
75
+ "saved_with_tab": 0.0473,
76
+ "saved_word_by_word": 0.0497
77
+ },
78
+ "articles/editing": {
79
+ "pauses": 1000,
80
+ "shown": 0.114,
81
+ "right": 0.7281,
82
+ "avg_right_chars": 5.9,
83
+ "multiword_shown": 0.042,
84
+ "multiword_right": 0.7143,
85
+ "saved_with_tab": 0.0408,
86
+ "saved_word_by_word": 0.0436
87
+ },
88
+ "messages/typing": {
89
+ "pauses": 1000,
90
+ "shown": 0.239,
91
+ "right": 0.7071,
92
+ "avg_right_chars": 7.33,
93
+ "multiword_shown": 0.125,
94
+ "multiword_right": 0.712,
95
+ "saved_with_tab": 0.1032,
96
+ "saved_word_by_word": 0.111
97
+ },
98
+ "messages/editing": {
99
+ "pauses": 1000,
100
+ "shown": 0.233,
101
+ "right": 0.6567,
102
+ "avg_right_chars": 7.03,
103
+ "multiword_shown": 0.126,
104
+ "multiword_right": 0.6032,
105
+ "saved_with_tab": 0.0896,
106
+ "saved_word_by_word": 0.1005
107
+ },
108
+ "email/typing": {
109
+ "pauses": 1000,
110
+ "shown": 0.158,
111
+ "right": 0.6392,
112
+ "avg_right_chars": 6.06,
113
+ "multiword_shown": 0.043,
114
+ "multiword_right": 0.5581,
115
+ "saved_with_tab": 0.051,
116
+ "saved_word_by_word": 0.0548
117
+ },
118
+ "email/editing": {
119
+ "pauses": 1000,
120
+ "shown": 0.166,
121
+ "right": 0.6807,
122
+ "avg_right_chars": 5.77,
123
+ "multiword_shown": 0.046,
124
+ "multiword_right": 0.6957,
125
+ "saved_with_tab": 0.0543,
126
+ "saved_word_by_word": 0.0568
127
+ }
128
+ },
129
+ "0.5": {
130
+ "articles/typing": {
131
+ "pauses": 1000,
132
+ "shown": 0.1,
133
+ "right": 0.81,
134
+ "avg_right_chars": 5.74,
135
+ "multiword_shown": 0.03,
136
+ "multiword_right": 0.7667,
137
+ "saved_with_tab": 0.0387,
138
+ "saved_word_by_word": 0.0402
139
+ },
140
+ "articles/editing": {
141
+ "pauses": 1000,
142
+ "shown": 0.084,
143
+ "right": 0.7738,
144
+ "avg_right_chars": 5.6,
145
+ "multiword_shown": 0.028,
146
+ "multiword_right": 0.7143,
147
+ "saved_with_tab": 0.0303,
148
+ "saved_word_by_word": 0.0322
149
+ },
150
+ "messages/typing": {
151
+ "pauses": 1000,
152
+ "shown": 0.182,
153
+ "right": 0.8022,
154
+ "avg_right_chars": 7.25,
155
+ "multiword_shown": 0.084,
156
+ "multiword_right": 0.7976,
157
+ "saved_with_tab": 0.0882,
158
+ "saved_word_by_word": 0.0916
159
+ },
160
+ "messages/editing": {
161
+ "pauses": 1000,
162
+ "shown": 0.17,
163
+ "right": 0.7176,
164
+ "avg_right_chars": 6.76,
165
+ "multiword_shown": 0.088,
166
+ "multiword_right": 0.6364,
167
+ "saved_with_tab": 0.0688,
168
+ "saved_word_by_word": 0.0759
169
+ },
170
+ "email/typing": {
171
+ "pauses": 1000,
172
+ "shown": 0.111,
173
+ "right": 0.7658,
174
+ "avg_right_chars": 5.88,
175
+ "multiword_shown": 0.03,
176
+ "multiword_right": 0.6667,
177
+ "saved_with_tab": 0.0417,
178
+ "saved_word_by_word": 0.0435
179
+ },
180
+ "email/editing": {
181
+ "pauses": 1000,
182
+ "shown": 0.108,
183
+ "right": 0.7963,
184
+ "avg_right_chars": 5.72,
185
+ "multiword_shown": 0.028,
186
+ "multiword_right": 0.75,
187
+ "saved_with_tab": 0.041,
188
+ "saved_word_by_word": 0.0418
189
+ }
190
+ },
191
+ "0.6": {
192
+ "articles/typing": {
193
+ "pauses": 1000,
194
+ "shown": 0.073,
195
+ "right": 0.9041,
196
+ "avg_right_chars": 5.55,
197
+ "multiword_shown": 0.019,
198
+ "multiword_right": 0.8947,
199
+ "saved_with_tab": 0.0305,
200
+ "saved_word_by_word": 0.0308
201
+ },
202
+ "articles/editing": {
203
+ "pauses": 1000,
204
+ "shown": 0.061,
205
+ "right": 0.8525,
206
+ "avg_right_chars": 5.35,
207
+ "multiword_shown": 0.016,
208
+ "multiword_right": 0.75,
209
+ "saved_with_tab": 0.0232,
210
+ "saved_word_by_word": 0.0238
211
+ },
212
+ "messages/typing": {
213
+ "pauses": 1000,
214
+ "shown": 0.139,
215
+ "right": 0.8849,
216
+ "avg_right_chars": 6.95,
217
+ "multiword_shown": 0.057,
218
+ "multiword_right": 0.8947,
219
+ "saved_with_tab": 0.0712,
220
+ "saved_word_by_word": 0.0724
221
+ },
222
+ "messages/editing": {
223
+ "pauses": 1000,
224
+ "shown": 0.124,
225
+ "right": 0.7823,
226
+ "avg_right_chars": 6.55,
227
+ "multiword_shown": 0.062,
228
+ "multiword_right": 0.7258,
229
+ "saved_with_tab": 0.0529,
230
+ "saved_word_by_word": 0.0567
231
+ },
232
+ "email/typing": {
233
+ "pauses": 1000,
234
+ "shown": 0.084,
235
+ "right": 0.8571,
236
+ "avg_right_chars": 5.64,
237
+ "multiword_shown": 0.018,
238
+ "multiword_right": 0.7222,
239
+ "saved_with_tab": 0.0338,
240
+ "saved_word_by_word": 0.0346
241
+ },
242
+ "email/editing": {
243
+ "pauses": 1000,
244
+ "shown": 0.082,
245
+ "right": 0.8659,
246
+ "avg_right_chars": 5.72,
247
+ "multiword_shown": 0.02,
248
+ "multiword_right": 0.75,
249
+ "saved_with_tab": 0.0338,
250
+ "saved_word_by_word": 0.0343
251
+ }
252
+ },
253
+ "0.7": {
254
+ "articles/typing": {
255
+ "pauses": 1000,
256
+ "shown": 0.06,
257
+ "right": 0.9167,
258
+ "avg_right_chars": 5.53,
259
+ "multiword_shown": 0.014,
260
+ "multiword_right": 0.8571,
261
+ "saved_with_tab": 0.0253,
262
+ "saved_word_by_word": 0.0257
263
+ },
264
+ "articles/editing": {
265
+ "pauses": 1000,
266
+ "shown": 0.043,
267
+ "right": 0.907,
268
+ "avg_right_chars": 5.26,
269
+ "multiword_shown": 0.008,
270
+ "multiword_right": 1.0,
271
+ "saved_with_tab": 0.0171,
272
+ "saved_word_by_word": 0.0171
273
+ },
274
+ "messages/typing": {
275
+ "pauses": 1000,
276
+ "shown": 0.104,
277
+ "right": 0.9135,
278
+ "avg_right_chars": 6.86,
279
+ "multiword_shown": 0.042,
280
+ "multiword_right": 0.9048,
281
+ "saved_with_tab": 0.0543,
282
+ "saved_word_by_word": 0.0554
283
+ },
284
+ "messages/editing": {
285
+ "pauses": 1000,
286
+ "shown": 0.096,
287
+ "right": 0.8542,
288
+ "avg_right_chars": 6.28,
289
+ "multiword_shown": 0.042,
290
+ "multiword_right": 0.7857,
291
+ "saved_with_tab": 0.0429,
292
+ "saved_word_by_word": 0.0457
293
+ },
294
+ "email/typing": {
295
+ "pauses": 1000,
296
+ "shown": 0.06,
297
+ "right": 0.85,
298
+ "avg_right_chars": 5.37,
299
+ "multiword_shown": 0.007,
300
+ "multiword_right": 0.5714,
301
+ "saved_with_tab": 0.0228,
302
+ "saved_word_by_word": 0.0229
303
+ },
304
+ "email/editing": {
305
+ "pauses": 1000,
306
+ "shown": 0.062,
307
+ "right": 0.871,
308
+ "avg_right_chars": 5.26,
309
+ "multiword_shown": 0.011,
310
+ "multiword_right": 0.7273,
311
+ "saved_with_tab": 0.0237,
312
+ "saved_word_by_word": 0.0237
313
+ }
314
+ }
315
+ },
316
+ "examples": [
317
+ {
318
+ "domain": "articles",
319
+ "scenario": "editing",
320
+ "before": " the Spaghetti Western film genre) and Hate Boat, were publi",
321
+ "suggestion": "shed",
322
+ "truth": "shed by Comico C",
323
+ "right": true
324
+ },
325
+ {
326
+ "domain": "messages",
327
+ "scenario": "typing",
328
+ "before": "onsidered new. Those decisions make the graphs as neat as po",
329
+ "suggestion": "ssible.",
330
+ "truth": "ssible, and it is t",
331
+ "right": false
332
+ },
333
+ {
334
+ "domain": "messages",
335
+ "scenario": "editing",
336
+ "before": "e optimal level of production and pricing for businesses. L'",
337
+ "suggestion": "Hpital's Rule",
338
+ "truth": "Hpital's Rule can help an",
339
+ "right": true
340
+ },
341
+ {
342
+ "domain": "messages",
343
+ "scenario": "editing",
344
+ "before": "noise tolerance, etc. Games usually provide their minimum an",
345
+ "suggestion": "d maximum",
346
+ "truth": "d recommended CPU, GP",
347
+ "right": false
348
+ },
349
+ {
350
+ "domain": "email",
351
+ "scenario": "editing",
352
+ "before": "ured products and marketing of electricity for the United St",
353
+ "suggestion": "ates",
354
+ "truth": "ates ? Actively ",
355
+ "right": true
356
+ }
357
+ ],
358
+ "latency_ms": {
359
+ "p50": 17.29,
360
+ "p95": 47.23,
361
+ "note": "Measured in this eval process, not in a browser."
362
+ }
363
+ }
v2/experiments/tinynext-v2-nofim/model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:18d0779c2fc27062aa39df221e7bcb6514e623a223325c8369f51644c394dcf2
3
+ size 60813561
v2/experiments/tinynext-v2-nofim/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:02c5fe5958a6a99de158bf8d3e0b9568b0336572433bda8f4c56e070185e3e57
3
+ size 50161328
v2/experiments/tinynext-v2-nofim/model_int8.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33562588d18f6b26a095fe8c450ea9a954dde454592eed8e5e5f14c623cf5745
3
+ size 15638871
v2/experiments/tinynext-v2-nofim/report.json ADDED
@@ -0,0 +1,1239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "args": {
3
+ "vocab": 8192,
4
+ "dim": 320,
5
+ "layers": 8,
6
+ "heads": 5,
7
+ "ffn": 864,
8
+ "seq": 512,
9
+ "tokens": 1500000000.0,
10
+ "val_tokens": 4000000.0,
11
+ "epochs": 1.0,
12
+ "batch": 512,
13
+ "lr": 0.003,
14
+ "fim_rate": 0.0,
15
+ "mix": "fineweb=0.6,smoltalk=0.3,enron=0.1",
16
+ "fineweb_files": 2,
17
+ "smoltalk_files": 9,
18
+ "tokenizer_docs": 200000,
19
+ "eval_pauses": 1000,
20
+ "log_every": 50,
21
+ "repo": "cfahlgren1/tinynext",
22
+ "folder": "v2/experiments/tinynext-v2-nofim",
23
+ "eval_folder": "v2",
24
+ "name": "tinynext-v2-nofim",
25
+ "trackio_space": "cfahlgren1/tinynext-trackio",
26
+ "stage": "train",
27
+ "data_repo": "cfahlgren1/tinynext-data",
28
+ "smoke": false,
29
+ "no_push": false
30
+ },
31
+ "parameters": 12539200,
32
+ "data": {
33
+ "fineweb": {
34
+ "docs": 1062484,
35
+ "train_tokens": 900000000
36
+ },
37
+ "smoltalk": {
38
+ "docs": 671427,
39
+ "train_tokens": 450000000
40
+ },
41
+ "enron": {
42
+ "docs": 268120,
43
+ "train_tokens": 150000000
44
+ }
45
+ },
46
+ "data_minutes": 0.2,
47
+ "train_minutes": 28.1,
48
+ "history": [
49
+ {
50
+ "step": 0,
51
+ "train_loss": 9.066,
52
+ "lr": 2.6e-05,
53
+ "tokens_per_second": 13050,
54
+ "minutes": 0.33,
55
+ "mfu_percent": 0.3,
56
+ "gpu_util_percent": 97,
57
+ "gpu_memory_gb": 70.5,
58
+ "val_loss": 9.0674
59
+ },
60
+ {
61
+ "step": 50,
62
+ "train_loss": 7.0033,
63
+ "lr": 0.001342,
64
+ "tokens_per_second": 933112,
65
+ "minutes": 0.62,
66
+ "mfu_percent": 22.5,
67
+ "gpu_util_percent": 100,
68
+ "gpu_memory_gb": 79.1
69
+ },
70
+ {
71
+ "step": 100,
72
+ "train_loss": 6.1098,
73
+ "lr": 0.002656,
74
+ "tokens_per_second": 926865,
75
+ "minutes": 0.85,
76
+ "mfu_percent": 22.4,
77
+ "gpu_util_percent": 100,
78
+ "gpu_memory_gb": 79.1
79
+ },
80
+ {
81
+ "step": 150,
82
+ "train_loss": 5.5684,
83
+ "lr": 0.002995,
84
+ "tokens_per_second": 926389,
85
+ "minutes": 1.09,
86
+ "mfu_percent": 22.3,
87
+ "gpu_util_percent": 100,
88
+ "gpu_memory_gb": 79.1
89
+ },
90
+ {
91
+ "step": 200,
92
+ "train_loss": 5.0866,
93
+ "lr": 0.002992,
94
+ "tokens_per_second": 925033,
95
+ "minutes": 1.33,
96
+ "mfu_percent": 22.3,
97
+ "gpu_util_percent": 100,
98
+ "gpu_memory_gb": 79.1
99
+ },
100
+ {
101
+ "step": 250,
102
+ "train_loss": 4.6613,
103
+ "lr": 0.002987,
104
+ "tokens_per_second": 925563,
105
+ "minutes": 1.56,
106
+ "mfu_percent": 22.3,
107
+ "gpu_util_percent": 100,
108
+ "gpu_memory_gb": 79.1
109
+ },
110
+ {
111
+ "step": 300,
112
+ "train_loss": 4.2932,
113
+ "lr": 0.002982,
114
+ "tokens_per_second": 923984,
115
+ "minutes": 1.8,
116
+ "mfu_percent": 22.3,
117
+ "gpu_util_percent": 100,
118
+ "gpu_memory_gb": 79.1
119
+ },
120
+ {
121
+ "step": 350,
122
+ "train_loss": 4.0354,
123
+ "lr": 0.002975,
124
+ "tokens_per_second": 923469,
125
+ "minutes": 2.03,
126
+ "mfu_percent": 22.3,
127
+ "gpu_util_percent": 100,
128
+ "gpu_memory_gb": 79.1
129
+ },
130
+ {
131
+ "step": 400,
132
+ "train_loss": 3.901,
133
+ "lr": 0.002967,
134
+ "tokens_per_second": 922950,
135
+ "minutes": 2.27,
136
+ "mfu_percent": 22.3,
137
+ "gpu_util_percent": 100,
138
+ "gpu_memory_gb": 79.1
139
+ },
140
+ {
141
+ "step": 450,
142
+ "train_loss": 3.7236,
143
+ "lr": 0.002959,
144
+ "tokens_per_second": 922140,
145
+ "minutes": 2.51,
146
+ "mfu_percent": 22.2,
147
+ "gpu_util_percent": 100,
148
+ "gpu_memory_gb": 79.1
149
+ },
150
+ {
151
+ "step": 500,
152
+ "train_loss": 3.6749,
153
+ "lr": 0.002949,
154
+ "tokens_per_second": 921720,
155
+ "minutes": 2.74,
156
+ "mfu_percent": 22.2,
157
+ "gpu_util_percent": 100,
158
+ "gpu_memory_gb": 79.1,
159
+ "val_loss": 3.6931
160
+ },
161
+ {
162
+ "step": 550,
163
+ "train_loss": 3.6001,
164
+ "lr": 0.002938,
165
+ "tokens_per_second": 921925,
166
+ "minutes": 3.03,
167
+ "mfu_percent": 22.2,
168
+ "gpu_util_percent": 100,
169
+ "gpu_memory_gb": 79.1
170
+ },
171
+ {
172
+ "step": 600,
173
+ "train_loss": 3.5615,
174
+ "lr": 0.002927,
175
+ "tokens_per_second": 922017,
176
+ "minutes": 3.27,
177
+ "mfu_percent": 22.2,
178
+ "gpu_util_percent": 100,
179
+ "gpu_memory_gb": 79.1
180
+ },
181
+ {
182
+ "step": 650,
183
+ "train_loss": 3.5618,
184
+ "lr": 0.002914,
185
+ "tokens_per_second": 922210,
186
+ "minutes": 3.5,
187
+ "mfu_percent": 22.2,
188
+ "gpu_util_percent": 100,
189
+ "gpu_memory_gb": 79.1
190
+ },
191
+ {
192
+ "step": 700,
193
+ "train_loss": 3.453,
194
+ "lr": 0.002901,
195
+ "tokens_per_second": 922179,
196
+ "minutes": 3.74,
197
+ "mfu_percent": 22.2,
198
+ "gpu_util_percent": 100,
199
+ "gpu_memory_gb": 79.1
200
+ },
201
+ {
202
+ "step": 750,
203
+ "train_loss": 3.4649,
204
+ "lr": 0.002886,
205
+ "tokens_per_second": 922152,
206
+ "minutes": 3.98,
207
+ "mfu_percent": 22.2,
208
+ "gpu_util_percent": 100,
209
+ "gpu_memory_gb": 79.1
210
+ },
211
+ {
212
+ "step": 800,
213
+ "train_loss": 3.3789,
214
+ "lr": 0.002871,
215
+ "tokens_per_second": 921856,
216
+ "minutes": 4.21,
217
+ "mfu_percent": 22.2,
218
+ "gpu_util_percent": 100,
219
+ "gpu_memory_gb": 79.1
220
+ },
221
+ {
222
+ "step": 850,
223
+ "train_loss": 3.4502,
224
+ "lr": 0.002855,
225
+ "tokens_per_second": 922127,
226
+ "minutes": 4.45,
227
+ "mfu_percent": 22.2,
228
+ "gpu_util_percent": 100,
229
+ "gpu_memory_gb": 79.1
230
+ },
231
+ {
232
+ "step": 900,
233
+ "train_loss": 3.361,
234
+ "lr": 0.002837,
235
+ "tokens_per_second": 921952,
236
+ "minutes": 4.69,
237
+ "mfu_percent": 22.2,
238
+ "gpu_util_percent": 100,
239
+ "gpu_memory_gb": 79.1
240
+ },
241
+ {
242
+ "step": 950,
243
+ "train_loss": 3.398,
244
+ "lr": 0.002819,
245
+ "tokens_per_second": 922148,
246
+ "minutes": 4.92,
247
+ "mfu_percent": 22.2,
248
+ "gpu_util_percent": 100,
249
+ "gpu_memory_gb": 79.1
250
+ },
251
+ {
252
+ "step": 1000,
253
+ "train_loss": 3.3632,
254
+ "lr": 0.0028,
255
+ "tokens_per_second": 922256,
256
+ "minutes": 5.16,
257
+ "mfu_percent": 22.2,
258
+ "gpu_util_percent": 100,
259
+ "gpu_memory_gb": 79.1,
260
+ "val_loss": 3.3573
261
+ },
262
+ {
263
+ "step": 1050,
264
+ "train_loss": 3.276,
265
+ "lr": 0.002781,
266
+ "tokens_per_second": 923002,
267
+ "minutes": 5.44,
268
+ "mfu_percent": 22.3,
269
+ "gpu_util_percent": 100,
270
+ "gpu_memory_gb": 79.1
271
+ },
272
+ {
273
+ "step": 1100,
274
+ "train_loss": 3.2999,
275
+ "lr": 0.00276,
276
+ "tokens_per_second": 923065,
277
+ "minutes": 5.68,
278
+ "mfu_percent": 22.3,
279
+ "gpu_util_percent": 100,
280
+ "gpu_memory_gb": 79.1
281
+ },
282
+ {
283
+ "step": 1150,
284
+ "train_loss": 3.346,
285
+ "lr": 0.002738,
286
+ "tokens_per_second": 923089,
287
+ "minutes": 5.92,
288
+ "mfu_percent": 22.3,
289
+ "gpu_util_percent": 100,
290
+ "gpu_memory_gb": 79.1
291
+ },
292
+ {
293
+ "step": 1200,
294
+ "train_loss": 3.2729,
295
+ "lr": 0.002716,
296
+ "tokens_per_second": 922983,
297
+ "minutes": 6.15,
298
+ "mfu_percent": 22.3,
299
+ "gpu_util_percent": 100,
300
+ "gpu_memory_gb": 79.1
301
+ },
302
+ {
303
+ "step": 1250,
304
+ "train_loss": 3.2381,
305
+ "lr": 0.002693,
306
+ "tokens_per_second": 922842,
307
+ "minutes": 6.39,
308
+ "mfu_percent": 22.3,
309
+ "gpu_util_percent": 100,
310
+ "gpu_memory_gb": 79.1
311
+ },
312
+ {
313
+ "step": 1300,
314
+ "train_loss": 3.2686,
315
+ "lr": 0.002669,
316
+ "tokens_per_second": 922738,
317
+ "minutes": 6.63,
318
+ "mfu_percent": 22.3,
319
+ "gpu_util_percent": 100,
320
+ "gpu_memory_gb": 79.1
321
+ },
322
+ {
323
+ "step": 1350,
324
+ "train_loss": 3.2762,
325
+ "lr": 0.002644,
326
+ "tokens_per_second": 922862,
327
+ "minutes": 6.86,
328
+ "mfu_percent": 22.3,
329
+ "gpu_util_percent": 100,
330
+ "gpu_memory_gb": 79.1
331
+ },
332
+ {
333
+ "step": 1400,
334
+ "train_loss": 3.2013,
335
+ "lr": 0.002618,
336
+ "tokens_per_second": 922796,
337
+ "minutes": 7.1,
338
+ "mfu_percent": 22.3,
339
+ "gpu_util_percent": 100,
340
+ "gpu_memory_gb": 79.1
341
+ },
342
+ {
343
+ "step": 1450,
344
+ "train_loss": 3.1987,
345
+ "lr": 0.002592,
346
+ "tokens_per_second": 923059,
347
+ "minutes": 7.34,
348
+ "mfu_percent": 22.3,
349
+ "gpu_util_percent": 100,
350
+ "gpu_memory_gb": 79.1
351
+ },
352
+ {
353
+ "step": 1500,
354
+ "train_loss": 3.2144,
355
+ "lr": 0.002565,
356
+ "tokens_per_second": 922750,
357
+ "minutes": 7.57,
358
+ "mfu_percent": 22.3,
359
+ "gpu_util_percent": 100,
360
+ "gpu_memory_gb": 79.1,
361
+ "val_loss": 3.2337
362
+ },
363
+ {
364
+ "step": 1550,
365
+ "train_loss": 3.1748,
366
+ "lr": 0.002538,
367
+ "tokens_per_second": 922943,
368
+ "minutes": 7.86,
369
+ "mfu_percent": 22.3,
370
+ "gpu_util_percent": 100,
371
+ "gpu_memory_gb": 79.1
372
+ },
373
+ {
374
+ "step": 1600,
375
+ "train_loss": 3.2102,
376
+ "lr": 0.002509,
377
+ "tokens_per_second": 922876,
378
+ "minutes": 8.1,
379
+ "mfu_percent": 22.3,
380
+ "gpu_util_percent": 100,
381
+ "gpu_memory_gb": 79.1
382
+ },
383
+ {
384
+ "step": 1650,
385
+ "train_loss": 3.1717,
386
+ "lr": 0.00248,
387
+ "tokens_per_second": 923021,
388
+ "minutes": 8.33,
389
+ "mfu_percent": 22.3,
390
+ "gpu_util_percent": 100,
391
+ "gpu_memory_gb": 79.1
392
+ },
393
+ {
394
+ "step": 1700,
395
+ "train_loss": 3.215,
396
+ "lr": 0.002451,
397
+ "tokens_per_second": 922818,
398
+ "minutes": 8.57,
399
+ "mfu_percent": 22.3,
400
+ "gpu_util_percent": 100,
401
+ "gpu_memory_gb": 79.1
402
+ },
403
+ {
404
+ "step": 1750,
405
+ "train_loss": 3.2434,
406
+ "lr": 0.002421,
407
+ "tokens_per_second": 923028,
408
+ "minutes": 8.81,
409
+ "mfu_percent": 22.3,
410
+ "gpu_util_percent": 100,
411
+ "gpu_memory_gb": 79.1
412
+ },
413
+ {
414
+ "step": 1800,
415
+ "train_loss": 3.1329,
416
+ "lr": 0.00239,
417
+ "tokens_per_second": 922698,
418
+ "minutes": 9.04,
419
+ "mfu_percent": 22.2,
420
+ "gpu_util_percent": 100,
421
+ "gpu_memory_gb": 79.1
422
+ },
423
+ {
424
+ "step": 1850,
425
+ "train_loss": 3.1663,
426
+ "lr": 0.002358,
427
+ "tokens_per_second": 922517,
428
+ "minutes": 9.28,
429
+ "mfu_percent": 22.2,
430
+ "gpu_util_percent": 100,
431
+ "gpu_memory_gb": 79.1
432
+ },
433
+ {
434
+ "step": 1900,
435
+ "train_loss": 3.1417,
436
+ "lr": 0.002327,
437
+ "tokens_per_second": 922580,
438
+ "minutes": 9.52,
439
+ "mfu_percent": 22.2,
440
+ "gpu_util_percent": 100,
441
+ "gpu_memory_gb": 79.1
442
+ },
443
+ {
444
+ "step": 1950,
445
+ "train_loss": 3.1055,
446
+ "lr": 0.002294,
447
+ "tokens_per_second": 922863,
448
+ "minutes": 9.75,
449
+ "mfu_percent": 22.3,
450
+ "gpu_util_percent": 100,
451
+ "gpu_memory_gb": 79.1
452
+ },
453
+ {
454
+ "step": 2000,
455
+ "train_loss": 3.1488,
456
+ "lr": 0.002261,
457
+ "tokens_per_second": 922511,
458
+ "minutes": 9.99,
459
+ "mfu_percent": 22.2,
460
+ "gpu_util_percent": 100,
461
+ "gpu_memory_gb": 79.1,
462
+ "val_loss": 3.1599
463
+ },
464
+ {
465
+ "step": 2050,
466
+ "train_loss": 3.0741,
467
+ "lr": 0.002228,
468
+ "tokens_per_second": 922742,
469
+ "minutes": 10.27,
470
+ "mfu_percent": 22.3,
471
+ "gpu_util_percent": 100,
472
+ "gpu_memory_gb": 79.1
473
+ },
474
+ {
475
+ "step": 2100,
476
+ "train_loss": 3.1161,
477
+ "lr": 0.002194,
478
+ "tokens_per_second": 922930,
479
+ "minutes": 10.51,
480
+ "mfu_percent": 22.3,
481
+ "gpu_util_percent": 100,
482
+ "gpu_memory_gb": 79.1
483
+ },
484
+ {
485
+ "step": 2150,
486
+ "train_loss": 3.1425,
487
+ "lr": 0.00216,
488
+ "tokens_per_second": 922993,
489
+ "minutes": 10.75,
490
+ "mfu_percent": 22.3,
491
+ "gpu_util_percent": 100,
492
+ "gpu_memory_gb": 79.1
493
+ },
494
+ {
495
+ "step": 2200,
496
+ "train_loss": 3.1001,
497
+ "lr": 0.002125,
498
+ "tokens_per_second": 922882,
499
+ "minutes": 10.98,
500
+ "mfu_percent": 22.3,
501
+ "gpu_util_percent": 100,
502
+ "gpu_memory_gb": 79.1
503
+ },
504
+ {
505
+ "step": 2250,
506
+ "train_loss": 3.1289,
507
+ "lr": 0.00209,
508
+ "tokens_per_second": 922732,
509
+ "minutes": 11.22,
510
+ "mfu_percent": 22.3,
511
+ "gpu_util_percent": 100,
512
+ "gpu_memory_gb": 79.1
513
+ },
514
+ {
515
+ "step": 2300,
516
+ "train_loss": 3.1166,
517
+ "lr": 0.002055,
518
+ "tokens_per_second": 922818,
519
+ "minutes": 11.46,
520
+ "mfu_percent": 22.3,
521
+ "gpu_util_percent": 100,
522
+ "gpu_memory_gb": 79.1
523
+ },
524
+ {
525
+ "step": 2350,
526
+ "train_loss": 3.0936,
527
+ "lr": 0.00202,
528
+ "tokens_per_second": 923024,
529
+ "minutes": 11.69,
530
+ "mfu_percent": 22.3,
531
+ "gpu_util_percent": 100,
532
+ "gpu_memory_gb": 79.1
533
+ },
534
+ {
535
+ "step": 2400,
536
+ "train_loss": 3.1052,
537
+ "lr": 0.001984,
538
+ "tokens_per_second": 922709,
539
+ "minutes": 11.93,
540
+ "mfu_percent": 22.3,
541
+ "gpu_util_percent": 100,
542
+ "gpu_memory_gb": 79.1
543
+ },
544
+ {
545
+ "step": 2450,
546
+ "train_loss": 3.0901,
547
+ "lr": 0.001948,
548
+ "tokens_per_second": 922648,
549
+ "minutes": 12.17,
550
+ "mfu_percent": 22.2,
551
+ "gpu_util_percent": 100,
552
+ "gpu_memory_gb": 79.1
553
+ },
554
+ {
555
+ "step": 2500,
556
+ "train_loss": 3.094,
557
+ "lr": 0.001911,
558
+ "tokens_per_second": 922464,
559
+ "minutes": 12.4,
560
+ "mfu_percent": 22.2,
561
+ "gpu_util_percent": 100,
562
+ "gpu_memory_gb": 79.1,
563
+ "val_loss": 3.1067
564
+ },
565
+ {
566
+ "step": 2550,
567
+ "train_loss": 3.1594,
568
+ "lr": 0.001875,
569
+ "tokens_per_second": 922698,
570
+ "minutes": 12.69,
571
+ "mfu_percent": 22.2,
572
+ "gpu_util_percent": 100,
573
+ "gpu_memory_gb": 79.1
574
+ },
575
+ {
576
+ "step": 2600,
577
+ "train_loss": 3.1005,
578
+ "lr": 0.001838,
579
+ "tokens_per_second": 922773,
580
+ "minutes": 12.92,
581
+ "mfu_percent": 22.3,
582
+ "gpu_util_percent": 100,
583
+ "gpu_memory_gb": 79.1
584
+ },
585
+ {
586
+ "step": 2650,
587
+ "train_loss": 3.0486,
588
+ "lr": 0.001801,
589
+ "tokens_per_second": 922597,
590
+ "minutes": 13.16,
591
+ "mfu_percent": 22.2,
592
+ "gpu_util_percent": 100,
593
+ "gpu_memory_gb": 79.1
594
+ },
595
+ {
596
+ "step": 2700,
597
+ "train_loss": 3.045,
598
+ "lr": 0.001764,
599
+ "tokens_per_second": 922925,
600
+ "minutes": 13.4,
601
+ "mfu_percent": 22.3,
602
+ "gpu_util_percent": 100,
603
+ "gpu_memory_gb": 79.1
604
+ },
605
+ {
606
+ "step": 2750,
607
+ "train_loss": 3.0978,
608
+ "lr": 0.001727,
609
+ "tokens_per_second": 922957,
610
+ "minutes": 13.63,
611
+ "mfu_percent": 22.3,
612
+ "gpu_util_percent": 100,
613
+ "gpu_memory_gb": 79.1
614
+ },
615
+ {
616
+ "step": 2800,
617
+ "train_loss": 3.0068,
618
+ "lr": 0.00169,
619
+ "tokens_per_second": 922829,
620
+ "minutes": 13.87,
621
+ "mfu_percent": 22.3,
622
+ "gpu_util_percent": 100,
623
+ "gpu_memory_gb": 79.1
624
+ },
625
+ {
626
+ "step": 2850,
627
+ "train_loss": 2.999,
628
+ "lr": 0.001653,
629
+ "tokens_per_second": 922756,
630
+ "minutes": 14.11,
631
+ "mfu_percent": 22.3,
632
+ "gpu_util_percent": 100,
633
+ "gpu_memory_gb": 79.1
634
+ },
635
+ {
636
+ "step": 2900,
637
+ "train_loss": 3.0705,
638
+ "lr": 0.001616,
639
+ "tokens_per_second": 922667,
640
+ "minutes": 14.34,
641
+ "mfu_percent": 22.2,
642
+ "gpu_util_percent": 100,
643
+ "gpu_memory_gb": 79.1
644
+ },
645
+ {
646
+ "step": 2950,
647
+ "train_loss": 3.0176,
648
+ "lr": 0.001579,
649
+ "tokens_per_second": 922610,
650
+ "minutes": 14.58,
651
+ "mfu_percent": 22.2,
652
+ "gpu_util_percent": 100,
653
+ "gpu_memory_gb": 79.1
654
+ },
655
+ {
656
+ "step": 3000,
657
+ "train_loss": 3.015,
658
+ "lr": 0.001542,
659
+ "tokens_per_second": 922714,
660
+ "minutes": 14.82,
661
+ "mfu_percent": 22.3,
662
+ "gpu_util_percent": 100,
663
+ "gpu_memory_gb": 79.1,
664
+ "val_loss": 3.0619
665
+ },
666
+ {
667
+ "step": 3050,
668
+ "train_loss": 3.0686,
669
+ "lr": 0.001505,
670
+ "tokens_per_second": 922937,
671
+ "minutes": 15.1,
672
+ "mfu_percent": 22.3,
673
+ "gpu_util_percent": 100,
674
+ "gpu_memory_gb": 79.1
675
+ },
676
+ {
677
+ "step": 3100,
678
+ "train_loss": 3.0109,
679
+ "lr": 0.001468,
680
+ "tokens_per_second": 922847,
681
+ "minutes": 15.34,
682
+ "mfu_percent": 22.3,
683
+ "gpu_util_percent": 100,
684
+ "gpu_memory_gb": 79.1
685
+ },
686
+ {
687
+ "step": 3150,
688
+ "train_loss": 2.9785,
689
+ "lr": 0.001431,
690
+ "tokens_per_second": 922659,
691
+ "minutes": 15.58,
692
+ "mfu_percent": 22.2,
693
+ "gpu_util_percent": 100,
694
+ "gpu_memory_gb": 79.1
695
+ },
696
+ {
697
+ "step": 3200,
698
+ "train_loss": 3.0064,
699
+ "lr": 0.001395,
700
+ "tokens_per_second": 922762,
701
+ "minutes": 15.81,
702
+ "mfu_percent": 22.3,
703
+ "gpu_util_percent": 100,
704
+ "gpu_memory_gb": 79.1
705
+ },
706
+ {
707
+ "step": 3250,
708
+ "train_loss": 3.0779,
709
+ "lr": 0.001358,
710
+ "tokens_per_second": 923029,
711
+ "minutes": 16.05,
712
+ "mfu_percent": 22.3,
713
+ "gpu_util_percent": 100,
714
+ "gpu_memory_gb": 79.1
715
+ },
716
+ {
717
+ "step": 3300,
718
+ "train_loss": 3.0103,
719
+ "lr": 0.001322,
720
+ "tokens_per_second": 922850,
721
+ "minutes": 16.29,
722
+ "mfu_percent": 22.3,
723
+ "gpu_util_percent": 100,
724
+ "gpu_memory_gb": 79.1
725
+ },
726
+ {
727
+ "step": 3350,
728
+ "train_loss": 3.0329,
729
+ "lr": 0.001286,
730
+ "tokens_per_second": 922649,
731
+ "minutes": 16.52,
732
+ "mfu_percent": 22.2,
733
+ "gpu_util_percent": 100,
734
+ "gpu_memory_gb": 79.1
735
+ },
736
+ {
737
+ "step": 3400,
738
+ "train_loss": 2.9922,
739
+ "lr": 0.001251,
740
+ "tokens_per_second": 922830,
741
+ "minutes": 16.76,
742
+ "mfu_percent": 22.3,
743
+ "gpu_util_percent": 100,
744
+ "gpu_memory_gb": 79.1
745
+ },
746
+ {
747
+ "step": 3450,
748
+ "train_loss": 2.9825,
749
+ "lr": 0.001215,
750
+ "tokens_per_second": 922466,
751
+ "minutes": 17.0,
752
+ "mfu_percent": 22.2,
753
+ "gpu_util_percent": 100,
754
+ "gpu_memory_gb": 79.1
755
+ },
756
+ {
757
+ "step": 3500,
758
+ "train_loss": 2.9975,
759
+ "lr": 0.00118,
760
+ "tokens_per_second": 922482,
761
+ "minutes": 17.23,
762
+ "mfu_percent": 22.2,
763
+ "gpu_util_percent": 100,
764
+ "gpu_memory_gb": 79.1,
765
+ "val_loss": 3.0243
766
+ },
767
+ {
768
+ "step": 3550,
769
+ "train_loss": 3.0041,
770
+ "lr": 0.001146,
771
+ "tokens_per_second": 922239,
772
+ "minutes": 17.52,
773
+ "mfu_percent": 22.2,
774
+ "gpu_util_percent": 100,
775
+ "gpu_memory_gb": 79.1
776
+ },
777
+ {
778
+ "step": 3600,
779
+ "train_loss": 3.0208,
780
+ "lr": 0.001111,
781
+ "tokens_per_second": 921706,
782
+ "minutes": 17.75,
783
+ "mfu_percent": 22.2,
784
+ "gpu_util_percent": 100,
785
+ "gpu_memory_gb": 79.1
786
+ },
787
+ {
788
+ "step": 3650,
789
+ "train_loss": 2.9875,
790
+ "lr": 0.001077,
791
+ "tokens_per_second": 921485,
792
+ "minutes": 17.99,
793
+ "mfu_percent": 22.2,
794
+ "gpu_util_percent": 100,
795
+ "gpu_memory_gb": 79.1
796
+ },
797
+ {
798
+ "step": 3700,
799
+ "train_loss": 3.0218,
800
+ "lr": 0.001044,
801
+ "tokens_per_second": 921567,
802
+ "minutes": 18.23,
803
+ "mfu_percent": 22.2,
804
+ "gpu_util_percent": 100,
805
+ "gpu_memory_gb": 79.1
806
+ },
807
+ {
808
+ "step": 3750,
809
+ "train_loss": 2.9973,
810
+ "lr": 0.001011,
811
+ "tokens_per_second": 922249,
812
+ "minutes": 18.46,
813
+ "mfu_percent": 22.2,
814
+ "gpu_util_percent": 100,
815
+ "gpu_memory_gb": 79.1
816
+ },
817
+ {
818
+ "step": 3800,
819
+ "train_loss": 2.9779,
820
+ "lr": 0.000979,
821
+ "tokens_per_second": 923901,
822
+ "minutes": 18.7,
823
+ "mfu_percent": 22.3,
824
+ "gpu_util_percent": 100,
825
+ "gpu_memory_gb": 79.1
826
+ },
827
+ {
828
+ "step": 3850,
829
+ "train_loss": 3.0253,
830
+ "lr": 0.000947,
831
+ "tokens_per_second": 924971,
832
+ "minutes": 18.94,
833
+ "mfu_percent": 22.3,
834
+ "gpu_util_percent": 100,
835
+ "gpu_memory_gb": 79.1
836
+ },
837
+ {
838
+ "step": 3900,
839
+ "train_loss": 2.9768,
840
+ "lr": 0.000915,
841
+ "tokens_per_second": 924295,
842
+ "minutes": 19.17,
843
+ "mfu_percent": 22.3,
844
+ "gpu_util_percent": 100,
845
+ "gpu_memory_gb": 79.1
846
+ },
847
+ {
848
+ "step": 3950,
849
+ "train_loss": 2.9955,
850
+ "lr": 0.000884,
851
+ "tokens_per_second": 924314,
852
+ "minutes": 19.41,
853
+ "mfu_percent": 22.3,
854
+ "gpu_util_percent": 100,
855
+ "gpu_memory_gb": 79.1
856
+ },
857
+ {
858
+ "step": 4000,
859
+ "train_loss": 2.9672,
860
+ "lr": 0.000854,
861
+ "tokens_per_second": 923365,
862
+ "minutes": 19.65,
863
+ "mfu_percent": 22.3,
864
+ "gpu_util_percent": 100,
865
+ "gpu_memory_gb": 79.1,
866
+ "val_loss": 2.9912
867
+ },
868
+ {
869
+ "step": 4050,
870
+ "train_loss": 2.982,
871
+ "lr": 0.000824,
872
+ "tokens_per_second": 923158,
873
+ "minutes": 19.93,
874
+ "mfu_percent": 22.3,
875
+ "gpu_util_percent": 100,
876
+ "gpu_memory_gb": 79.1
877
+ },
878
+ {
879
+ "step": 4100,
880
+ "train_loss": 2.9381,
881
+ "lr": 0.000795,
882
+ "tokens_per_second": 922493,
883
+ "minutes": 20.17,
884
+ "mfu_percent": 22.2,
885
+ "gpu_util_percent": 100,
886
+ "gpu_memory_gb": 79.1
887
+ },
888
+ {
889
+ "step": 4150,
890
+ "train_loss": 2.96,
891
+ "lr": 0.000767,
892
+ "tokens_per_second": 922454,
893
+ "minutes": 20.4,
894
+ "mfu_percent": 22.2,
895
+ "gpu_util_percent": 100,
896
+ "gpu_memory_gb": 79.1
897
+ },
898
+ {
899
+ "step": 4200,
900
+ "train_loss": 2.9844,
901
+ "lr": 0.000739,
902
+ "tokens_per_second": 924196,
903
+ "minutes": 20.64,
904
+ "mfu_percent": 22.3,
905
+ "gpu_util_percent": 100,
906
+ "gpu_memory_gb": 79.1
907
+ },
908
+ {
909
+ "step": 4250,
910
+ "train_loss": 2.9594,
911
+ "lr": 0.000712,
912
+ "tokens_per_second": 925585,
913
+ "minutes": 20.88,
914
+ "mfu_percent": 22.3,
915
+ "gpu_util_percent": 100,
916
+ "gpu_memory_gb": 79.1
917
+ },
918
+ {
919
+ "step": 4300,
920
+ "train_loss": 2.9844,
921
+ "lr": 0.000686,
922
+ "tokens_per_second": 925297,
923
+ "minutes": 21.11,
924
+ "mfu_percent": 22.3,
925
+ "gpu_util_percent": 100,
926
+ "gpu_memory_gb": 79.1
927
+ },
928
+ {
929
+ "step": 4350,
930
+ "train_loss": 2.9955,
931
+ "lr": 0.00066,
932
+ "tokens_per_second": 924922,
933
+ "minutes": 21.35,
934
+ "mfu_percent": 22.3,
935
+ "gpu_util_percent": 100,
936
+ "gpu_memory_gb": 79.1
937
+ },
938
+ {
939
+ "step": 4400,
940
+ "train_loss": 2.9842,
941
+ "lr": 0.000635,
942
+ "tokens_per_second": 925657,
943
+ "minutes": 21.58,
944
+ "mfu_percent": 22.3,
945
+ "gpu_util_percent": 100,
946
+ "gpu_memory_gb": 79.1
947
+ },
948
+ {
949
+ "step": 4450,
950
+ "train_loss": 2.9217,
951
+ "lr": 0.000611,
952
+ "tokens_per_second": 926080,
953
+ "minutes": 21.82,
954
+ "mfu_percent": 22.3,
955
+ "gpu_util_percent": 100,
956
+ "gpu_memory_gb": 79.1
957
+ },
958
+ {
959
+ "step": 4500,
960
+ "train_loss": 2.9781,
961
+ "lr": 0.000588,
962
+ "tokens_per_second": 925039,
963
+ "minutes": 22.06,
964
+ "mfu_percent": 22.3,
965
+ "gpu_util_percent": 100,
966
+ "gpu_memory_gb": 79.1,
967
+ "val_loss": 2.9623
968
+ },
969
+ {
970
+ "step": 4550,
971
+ "train_loss": 2.9359,
972
+ "lr": 0.000565,
973
+ "tokens_per_second": 924164,
974
+ "minutes": 22.34,
975
+ "mfu_percent": 22.3,
976
+ "gpu_util_percent": 100,
977
+ "gpu_memory_gb": 79.1
978
+ },
979
+ {
980
+ "step": 4600,
981
+ "train_loss": 2.9604,
982
+ "lr": 0.000544,
983
+ "tokens_per_second": 924462,
984
+ "minutes": 22.58,
985
+ "mfu_percent": 22.3,
986
+ "gpu_util_percent": 100,
987
+ "gpu_memory_gb": 79.1
988
+ },
989
+ {
990
+ "step": 4650,
991
+ "train_loss": 2.937,
992
+ "lr": 0.000523,
993
+ "tokens_per_second": 926111,
994
+ "minutes": 22.81,
995
+ "mfu_percent": 22.3,
996
+ "gpu_util_percent": 100,
997
+ "gpu_memory_gb": 79.1
998
+ },
999
+ {
1000
+ "step": 4700,
1001
+ "train_loss": 2.9574,
1002
+ "lr": 0.000503,
1003
+ "tokens_per_second": 925254,
1004
+ "minutes": 23.05,
1005
+ "mfu_percent": 22.3,
1006
+ "gpu_util_percent": 100,
1007
+ "gpu_memory_gb": 79.1
1008
+ },
1009
+ {
1010
+ "step": 4750,
1011
+ "train_loss": 2.9611,
1012
+ "lr": 0.000484,
1013
+ "tokens_per_second": 926355,
1014
+ "minutes": 23.28,
1015
+ "mfu_percent": 22.3,
1016
+ "gpu_util_percent": 100,
1017
+ "gpu_memory_gb": 79.1
1018
+ },
1019
+ {
1020
+ "step": 4800,
1021
+ "train_loss": 2.9299,
1022
+ "lr": 0.000465,
1023
+ "tokens_per_second": 925442,
1024
+ "minutes": 23.52,
1025
+ "mfu_percent": 22.3,
1026
+ "gpu_util_percent": 100,
1027
+ "gpu_memory_gb": 79.1
1028
+ },
1029
+ {
1030
+ "step": 4850,
1031
+ "train_loss": 2.8933,
1032
+ "lr": 0.000448,
1033
+ "tokens_per_second": 924963,
1034
+ "minutes": 23.76,
1035
+ "mfu_percent": 22.3,
1036
+ "gpu_util_percent": 100,
1037
+ "gpu_memory_gb": 79.1
1038
+ },
1039
+ {
1040
+ "step": 4900,
1041
+ "train_loss": 2.9737,
1042
+ "lr": 0.000432,
1043
+ "tokens_per_second": 924506,
1044
+ "minutes": 23.99,
1045
+ "mfu_percent": 22.3,
1046
+ "gpu_util_percent": 100,
1047
+ "gpu_memory_gb": 79.1
1048
+ },
1049
+ {
1050
+ "step": 4950,
1051
+ "train_loss": 2.9918,
1052
+ "lr": 0.000416,
1053
+ "tokens_per_second": 925264,
1054
+ "minutes": 24.23,
1055
+ "mfu_percent": 22.3,
1056
+ "gpu_util_percent": 100,
1057
+ "gpu_memory_gb": 79.1
1058
+ },
1059
+ {
1060
+ "step": 5000,
1061
+ "train_loss": 2.8923,
1062
+ "lr": 0.000401,
1063
+ "tokens_per_second": 923776,
1064
+ "minutes": 24.47,
1065
+ "mfu_percent": 22.3,
1066
+ "gpu_util_percent": 100,
1067
+ "gpu_memory_gb": 79.1,
1068
+ "val_loss": 2.9413
1069
+ },
1070
+ {
1071
+ "step": 5050,
1072
+ "train_loss": 2.9833,
1073
+ "lr": 0.000388,
1074
+ "tokens_per_second": 924281,
1075
+ "minutes": 24.75,
1076
+ "mfu_percent": 22.3,
1077
+ "gpu_util_percent": 100,
1078
+ "gpu_memory_gb": 79.1
1079
+ },
1080
+ {
1081
+ "step": 5100,
1082
+ "train_loss": 2.916,
1083
+ "lr": 0.000375,
1084
+ "tokens_per_second": 924275,
1085
+ "minutes": 24.98,
1086
+ "mfu_percent": 22.3,
1087
+ "gpu_util_percent": 100,
1088
+ "gpu_memory_gb": 79.1
1089
+ },
1090
+ {
1091
+ "step": 5150,
1092
+ "train_loss": 2.9783,
1093
+ "lr": 0.000363,
1094
+ "tokens_per_second": 925466,
1095
+ "minutes": 25.22,
1096
+ "mfu_percent": 22.3,
1097
+ "gpu_util_percent": 100,
1098
+ "gpu_memory_gb": 79.1
1099
+ },
1100
+ {
1101
+ "step": 5200,
1102
+ "train_loss": 2.9437,
1103
+ "lr": 0.000353,
1104
+ "tokens_per_second": 924296,
1105
+ "minutes": 25.46,
1106
+ "mfu_percent": 22.3,
1107
+ "gpu_util_percent": 100,
1108
+ "gpu_memory_gb": 79.1
1109
+ },
1110
+ {
1111
+ "step": 5250,
1112
+ "train_loss": 2.8472,
1113
+ "lr": 0.000343,
1114
+ "tokens_per_second": 924268,
1115
+ "minutes": 25.69,
1116
+ "mfu_percent": 22.3,
1117
+ "gpu_util_percent": 100,
1118
+ "gpu_memory_gb": 79.1
1119
+ },
1120
+ {
1121
+ "step": 5300,
1122
+ "train_loss": 2.9167,
1123
+ "lr": 0.000334,
1124
+ "tokens_per_second": 923137,
1125
+ "minutes": 25.93,
1126
+ "mfu_percent": 22.3,
1127
+ "gpu_util_percent": 100,
1128
+ "gpu_memory_gb": 79.1
1129
+ },
1130
+ {
1131
+ "step": 5350,
1132
+ "train_loss": 2.9399,
1133
+ "lr": 0.000326,
1134
+ "tokens_per_second": 923097,
1135
+ "minutes": 26.17,
1136
+ "mfu_percent": 22.3,
1137
+ "gpu_util_percent": 100,
1138
+ "gpu_memory_gb": 79.1
1139
+ },
1140
+ {
1141
+ "step": 5400,
1142
+ "train_loss": 2.9526,
1143
+ "lr": 0.000319,
1144
+ "tokens_per_second": 925239,
1145
+ "minutes": 26.4,
1146
+ "mfu_percent": 22.3,
1147
+ "gpu_util_percent": 100,
1148
+ "gpu_memory_gb": 79.1
1149
+ },
1150
+ {
1151
+ "step": 5450,
1152
+ "train_loss": 2.9353,
1153
+ "lr": 0.000314,
1154
+ "tokens_per_second": 926014,
1155
+ "minutes": 26.64,
1156
+ "mfu_percent": 22.3,
1157
+ "gpu_util_percent": 100,
1158
+ "gpu_memory_gb": 79.1
1159
+ },
1160
+ {
1161
+ "step": 5500,
1162
+ "train_loss": 2.8749,
1163
+ "lr": 0.000309,
1164
+ "tokens_per_second": 925013,
1165
+ "minutes": 26.88,
1166
+ "mfu_percent": 22.3,
1167
+ "gpu_util_percent": 100,
1168
+ "gpu_memory_gb": 79.1,
1169
+ "val_loss": 2.9287
1170
+ },
1171
+ {
1172
+ "step": 5550,
1173
+ "train_loss": 2.9738,
1174
+ "lr": 0.000305,
1175
+ "tokens_per_second": 924722,
1176
+ "minutes": 27.16,
1177
+ "mfu_percent": 22.3,
1178
+ "gpu_util_percent": 100,
1179
+ "gpu_memory_gb": 79.1
1180
+ },
1181
+ {
1182
+ "step": 5600,
1183
+ "train_loss": 2.9329,
1184
+ "lr": 0.000302,
1185
+ "tokens_per_second": 924135,
1186
+ "minutes": 27.39,
1187
+ "mfu_percent": 22.3,
1188
+ "gpu_util_percent": 100,
1189
+ "gpu_memory_gb": 79.1
1190
+ },
1191
+ {
1192
+ "step": 5650,
1193
+ "train_loss": 2.9227,
1194
+ "lr": 0.000301,
1195
+ "tokens_per_second": 922406,
1196
+ "minutes": 27.63,
1197
+ "mfu_percent": 22.2,
1198
+ "gpu_util_percent": 100,
1199
+ "gpu_memory_gb": 79.1
1200
+ },
1201
+ {
1202
+ "step": 5700,
1203
+ "train_loss": 2.9227,
1204
+ "lr": 0.0003,
1205
+ "tokens_per_second": 921979,
1206
+ "minutes": 27.87,
1207
+ "mfu_percent": 22.2,
1208
+ "gpu_util_percent": 100,
1209
+ "gpu_memory_gb": 79.1
1210
+ },
1211
+ {
1212
+ "step": 5709,
1213
+ "train_loss": 2.9206,
1214
+ "lr": 0.0003,
1215
+ "tokens_per_second": 921700,
1216
+ "minutes": 27.91,
1217
+ "mfu_percent": 22.2,
1218
+ "gpu_util_percent": 100,
1219
+ "gpu_memory_gb": 79.1,
1220
+ "val_loss": 2.9258
1221
+ }
1222
+ ],
1223
+ "exports": {
1224
+ "parity_prompts": 300,
1225
+ "parity_steps_per_prompt": 5,
1226
+ "float": {
1227
+ "megabytes": 60.81,
1228
+ "same_first_token": 1.0,
1229
+ "same_five_greedy_tokens": 1.0,
1230
+ "cached_step_ms_p50_cpu": 5.492
1231
+ },
1232
+ "int8": {
1233
+ "megabytes": 15.64,
1234
+ "same_first_token": 0.0033,
1235
+ "same_five_greedy_tokens": 0.0,
1236
+ "cached_step_ms_p50_cpu": 6.308
1237
+ }
1238
+ }
1239
+ }
v2/experiments/tinynext-v2-nofim/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
v2/experiments/tinynext-v2-nofim/train.py ADDED
@@ -0,0 +1,691 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.11,<3.14"
3
+ # dependencies = [
4
+ # "torch>=2.6,<2.8",
5
+ # "datasets>=3.0",
6
+ # "numpy<3",
7
+ # "pyarrow>=15",
8
+ # "tokenizers>=0.20",
9
+ # "transformers>=4.46",
10
+ # "onnx>=1.17",
11
+ # "onnxruntime>=1.20",
12
+ # "safetensors>=0.4",
13
+ # "huggingface_hub>=0.30",
14
+ # "nvidia-ml-py>=12",
15
+ # "trackio>=0.2",
16
+ # ]
17
+ # ///
18
+ """tinynext v2: a small word-piece transformer for in-browser autocomplete that can also fill in the middle.
19
+
20
+ One script does everything: it trains a tokenizer, builds a mix of general text, conversation and email,
21
+ trains the model, scores it with eval.py (the same harness as tinynext v1 and SmolLM2), exports ONNX with a
22
+ key/value cache for the browser, and uploads the lot to the model repo under v2/. Training curves, throughput
23
+ and GPU use go to a Trackio dashboard.
24
+
25
+ hf jobs uv run --namespace huggingface --flavor a100-large --secrets HF_TOKEN train.py
26
+ hf jobs uv run --namespace huggingface --flavor a10g-small --secrets HF_TOKEN train.py --smoke --no-push
27
+
28
+ The data is prepared on every CPU core before training starts, and the whole token array then sits in GPU
29
+ memory, so the GPU never waits on Python between steps.
30
+ """
31
+
32
+ import argparse
33
+ import importlib.util
34
+ import json
35
+ import math
36
+ import os
37
+ import random
38
+ import shutil
39
+ import sys
40
+ import time
41
+ from pathlib import Path
42
+
43
+ import numpy as np
44
+ import torch
45
+ import torch.nn as nn
46
+ import torch.nn.functional as F
47
+
48
+ HERE = Path(__file__).parent
49
+ SPECIAL = ["<|endoftext|>", "<|pre|>", "<|suf|>", "<|mid|>"]
50
+ EOT, PRE, SUF, MID = range(4)
51
+
52
+ FINEWEB = "hf://datasets/HuggingFaceFW/fineweb-edu/sample/10BT/{:03d}_00000.parquet"
53
+ SMOLTALK = "hf://datasets/HuggingFaceTB/smoltalk/data/all/train-{:05d}-of-00009.parquet"
54
+ ENRON = "hf://datasets/corbt/enron-emails/data/train-{:05d}-of-00003.parquet"
55
+
56
+
57
+ def cpus() -> int:
58
+ return len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else os.cpu_count() or 1
59
+
60
+
61
+ # ---- Shared eval code ------------------------------------------------------------------------------------------
62
+
63
+
64
+ def load_eval_module(repo: str, folder: str):
65
+ """Uses eval.py and eval_set.json next to this script, or fetches them from the repo, so every model is scored
66
+ by one implementation on the same text. Registered as a module so worker processes can use it too."""
67
+ path = HERE / "eval.py"
68
+ if not path.exists():
69
+ from huggingface_hub import hf_hub_download
70
+
71
+ for name in ("eval.py", "eval_set.json"):
72
+ shutil.copy(hf_hub_download(repo, f"{folder}/{name}"), HERE / name)
73
+ spec = importlib.util.spec_from_file_location("tinynext_eval", path)
74
+ module = importlib.util.module_from_spec(spec)
75
+ sys.modules["tinynext_eval"] = module
76
+ spec.loader.exec_module(module)
77
+ return module
78
+
79
+
80
+ # ---- Data ------------------------------------------------------------------------------------------------------
81
+
82
+
83
+ def to_ascii(text: str) -> str:
84
+ """The browser model only sees keyboard characters, so it trains on the same."""
85
+ import tinynext_eval as ev
86
+
87
+ return text.translate(ev.SMART_CHARS).encode("ascii", "ignore").decode().strip()
88
+
89
+
90
+ def smoltalk_text(batch):
91
+ return {"text": [to_ascii("\n\n".join(m["content"] for m in turns if m["role"] in ("user", "assistant"))) for turns in batch["messages"]]}
92
+
93
+
94
+ def enron_text(batch):
95
+ """Training senders only: the eval's held-out senders and enron-autocomplete's validation senders are skipped."""
96
+ import hashlib
97
+
98
+ import tinynext_eval as ev
99
+
100
+ texts = []
101
+ for sender, body in zip(batch["from"], batch["body"]):
102
+ sender = (sender or "").lower()
103
+ keep = "@enron.com" in sender and not ev.BULK_SENDER.search(sender)
104
+ keep = keep and int(hashlib.sha1(sender.encode()).hexdigest(), 16) % 100 >= 10
105
+ texts.append(to_ascii("\n\n".join(ev.clean_email(body or ""))) if keep else "")
106
+ return {"text": texts}
107
+
108
+
109
+ # Byte-level BPE with an 8K vocabulary averages a little over 4 characters per token on English. Reading 40% more
110
+ # text than that covers the variance, and anything short gets repeated (and logged) in flatten().
111
+ CHARS_PER_TOKEN = 4.0
112
+ TEXT_MARGIN = 1.4
113
+
114
+
115
+ def enough_rows(ds, budget_tokens: int, lengths: np.ndarray, seed: int):
116
+ """A random subset of rows whose text adds up to roughly the token budget, so only that much gets cleaned."""
117
+ order = np.random.default_rng(seed).permutation(len(ds))
118
+ needed = np.searchsorted(np.cumsum(lengths[order]), budget_tokens * CHARS_PER_TOKEN * TEXT_MARGIN) + 1
119
+ return ds.select(np.sort(order[:needed]))
120
+
121
+
122
+ def load_sources(args, budgets: dict) -> dict:
123
+ """Downloads parquet files, keeps only as many documents as each source's budget needs, and cleans them on
124
+ every core."""
125
+ import pyarrow.compute as pc
126
+ from datasets import load_dataset
127
+
128
+ procs = cpus()
129
+ fineweb = load_dataset("parquet", data_files=[FINEWEB.format(i) for i in range(args.fineweb_files)], split="train")
130
+ fineweb = fineweb.select_columns(["text"])
131
+ fineweb = enough_rows(fineweb, budgets["fineweb"], pc.utf8_length(fineweb.data.column("text")).to_numpy(), seed=0)
132
+ fineweb = fineweb.map(lambda b: {"text": [to_ascii(t) for t in b["text"]]}, batched=True, num_proc=procs)
133
+ smoltalk = load_dataset("parquet", data_files=[SMOLTALK.format(i) for i in range(args.smoltalk_files)], split="train")
134
+ sample = smoltalk.shuffle(seed=0).select(range(min(2000, len(smoltalk))))
135
+ average = np.mean([sum(len(m["content"]) for m in row["messages"]) for row in sample])
136
+ smoltalk = enough_rows(smoltalk, budgets["smoltalk"], np.full(len(smoltalk), average), seed=1)
137
+ smoltalk = smoltalk.map(smoltalk_text, batched=True, num_proc=procs, remove_columns=smoltalk.column_names)
138
+ enron = load_dataset("parquet", data_files=[ENRON.format(i) for i in range(3)], split="train")
139
+ enron = enron.map(enron_text, batched=True, num_proc=procs, remove_columns=enron.column_names)
140
+ sources = {"fineweb": fineweb, "smoltalk": smoltalk, "enron": enron}
141
+ return {name: ds.filter(lambda b: [len(t) >= 40 for t in b["text"]], batched=True, num_proc=procs) for name, ds in sources.items()}
142
+
143
+
144
+ def train_tokenizer(sources: dict, weights: dict, vocab: int, sample_docs: int):
145
+ from tokenizers import Tokenizer, decoders, models, pre_tokenizers, trainers
146
+
147
+ def sample():
148
+ for name, weight in weights.items():
149
+ ds = sources[name].shuffle(seed=0)
150
+ for row in ds.select(range(min(len(ds), int(sample_docs * weight)))):
151
+ yield row["text"]
152
+
153
+ tokenizer = Tokenizer(models.BPE())
154
+ tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
155
+ tokenizer.decoder = decoders.ByteLevel()
156
+ trainer = trainers.BpeTrainer(vocab_size=vocab, special_tokens=SPECIAL, min_frequency=2,
157
+ initial_alphabet=pre_tokenizers.ByteLevel.alphabet())
158
+ tokenizer.train_from_iterator(sample(), trainer=trainer)
159
+ return tokenizer
160
+
161
+
162
+ def fim(ids: list[int], rng: random.Random, max_middle: int, max_suffix: int, max_prefix: int) -> list[int]:
163
+ """Suffix first, then prefix, then the middle, so typing at the cursor only appends to the prompt."""
164
+ if len(ids) < 8:
165
+ return ids + [EOT]
166
+ start = rng.randint(1, len(ids) - 2)
167
+ end = min(len(ids), start + rng.randint(1, max_middle))
168
+ prefix, middle, suffix = ids[max(0, start - max_prefix):start], ids[start:end], ids[end:end + max_suffix]
169
+ if rng.random() < 0.25:
170
+ suffix = [] # also learn the plain "nothing after the cursor" case in this format
171
+ return [SUF, *suffix, PRE, *prefix, MID, *middle, EOT]
172
+
173
+
174
+ def make_encoder(tokenizer_json: str, seq: int, fim_rate: float):
175
+ """Tokenizes a batch and turns each document into training pieces: plain chunks, or fill-in-the-middle examples."""
176
+
177
+ def encode(batch, indices):
178
+ from tokenizers import Tokenizer
179
+
180
+ tokenizer = Tokenizer.from_str(tokenizer_json)
181
+ rng = random.Random(indices[0])
182
+ out = []
183
+ for encoding in tokenizer.encode_batch(batch["text"]):
184
+ ids, pieces = encoding.ids, []
185
+ for at in range(0, len(ids), seq - 8):
186
+ chunk = ids[at: at + seq - 8]
187
+ pieces += fim(chunk, rng, 24, 64, seq - 100) if rng.random() < fim_rate else chunk + [EOT]
188
+ out.append(pieces)
189
+ return {"tokens": out}
190
+
191
+ return encode
192
+
193
+
194
+ def flatten(ds, budget: int) -> np.ndarray:
195
+ """All of a source's tokens as one array, cut to its share of the budget (repeated if the source is small)."""
196
+ import pyarrow.compute as pc
197
+
198
+ column = ds.data.column("tokens")
199
+ tokens = pc.list_flatten(column).to_numpy().astype(np.uint16)
200
+ if len(tokens) < budget:
201
+ print(f" only {len(tokens) / 1e6:.0f}M tokens, repeating to reach {budget / 1e6:.0f}M", flush=True)
202
+ tokens = np.tile(tokens, math.ceil(budget / len(tokens)))
203
+ return tokens[:budget]
204
+
205
+
206
+ def build_rows(sources: dict, tokenizer, weights: dict, args) -> tuple[np.ndarray, np.ndarray, dict]:
207
+ encode = make_encoder(tokenizer.to_str(), args.seq, args.fim_rate)
208
+ train_parts, val_parts, counts = [], [], {}
209
+ for name, weight in weights.items():
210
+ started = time.time()
211
+ split = sources[name].train_test_split(test_size=0.002, seed=0)
212
+ encoded = {part: split[part].map(encode, batched=True, batch_size=512, with_indices=True, num_proc=cpus(),
213
+ remove_columns=["text"]) for part in ("train", "test")}
214
+ budget = int(args.tokens * weight)
215
+ train_parts.append(flatten(encoded["train"], budget))
216
+ val_parts.append(flatten(encoded["test"], int(args.val_tokens * weight)))
217
+ counts[name] = {"docs": len(split["train"]), "train_tokens": int(len(train_parts[-1]))}
218
+ print(f" {name}: {len(split['train']):,} docs → {len(train_parts[-1]) / 1e6:.0f}M tokens in {time.time() - started:.0f}s", flush=True)
219
+
220
+ def rows(parts):
221
+ tokens = np.concatenate(parts)
222
+ count = len(tokens) // (args.seq + 1)
223
+ return tokens[: count * (args.seq + 1)].reshape(count, args.seq + 1)
224
+
225
+ return rows(train_parts), rows(val_parts), counts
226
+
227
+
228
+ # ---- Model -----------------------------------------------------------------------------------------------------
229
+
230
+
231
+ class RMSNorm(nn.Module):
232
+ def __init__(self, dim: int):
233
+ super().__init__()
234
+ self.weight = nn.Parameter(torch.ones(dim))
235
+
236
+ def forward(self, x):
237
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + 1e-6) * self.weight
238
+
239
+
240
+ def rotate(x, cos, sin):
241
+ half = x.shape[-1] // 2
242
+ x1, x2 = x[..., :half], x[..., half:]
243
+ return torch.cat([x1 * cos - x2 * sin, x2 * cos + x1 * sin], dim=-1)
244
+
245
+
246
+ class Block(nn.Module):
247
+ def __init__(self, dim: int, heads: int, ffn: int):
248
+ super().__init__()
249
+ self.heads, self.head_dim = heads, dim // heads
250
+ self.norm1, self.norm2 = RMSNorm(dim), RMSNorm(dim)
251
+ self.qkv = nn.Linear(dim, 3 * dim, bias=False)
252
+ self.out = nn.Linear(dim, dim, bias=False)
253
+ self.gate_up = nn.Linear(dim, 2 * ffn, bias=False)
254
+ self.down = nn.Linear(ffn, dim, bias=False)
255
+
256
+ def forward(self, x, cos, sin, past_k, past_v, mask):
257
+ batch, length, _ = x.shape
258
+ q, k, v = self.qkv(self.norm1(x)).view(batch, length, 3, self.heads, self.head_dim).unbind(2)
259
+ q, k, v = rotate(q.transpose(1, 2), cos, sin), rotate(k.transpose(1, 2), cos, sin), v.transpose(1, 2)
260
+ if past_k is not None:
261
+ k, v = torch.cat([past_k, k], dim=2), torch.cat([past_v, v], dim=2)
262
+ if mask is None:
263
+ attended = F.scaled_dot_product_attention(q, k, v, is_causal=True)
264
+ else:
265
+ attended = F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
266
+ x = x + self.out(attended.transpose(1, 2).reshape(batch, length, -1))
267
+ gate, up = self.gate_up(self.norm2(x)).chunk(2, dim=-1)
268
+ return x + self.down(F.silu(gate) * up), k, v
269
+
270
+
271
+ class TinyNext(nn.Module):
272
+ def __init__(self, vocab: int, dim: int, layers: int, heads: int, ffn: int):
273
+ super().__init__()
274
+ self.config = {"vocab": vocab, "dim": dim, "layers": layers, "heads": heads, "ffn": ffn}
275
+ self.embed = nn.Embedding(vocab, dim)
276
+ self.blocks = nn.ModuleList(Block(dim, heads, ffn) for _ in range(layers))
277
+ self.norm = RMSNorm(dim)
278
+ self.head = None # set only for export: an untied copy of the output weights, so int8 quantization covers it
279
+ self.head_dim = dim // heads
280
+ inverse = 1.0 / (10000 ** (torch.arange(0, self.head_dim, 2).float() / self.head_dim))
281
+ self.register_buffer("inverse_frequency", inverse, persistent=False)
282
+ nn.init.normal_(self.embed.weight, std=0.02)
283
+
284
+ def forward(self, ids, past_k=None, past_v=None):
285
+ """ids: [batch, length]. past_k/past_v: [layers, batch, heads, past, head_dim] or None.
286
+ Returns logits for every position and the updated cache."""
287
+ past = 0 if past_k is None else past_k.shape[3]
288
+ length = ids.shape[1]
289
+ positions = torch.arange(length, device=ids.device) + past
290
+ angles = positions[:, None].float() * self.inverse_frequency[None, :]
291
+ cos, sin = angles.cos()[None, None], angles.sin()[None, None]
292
+ mask = None
293
+ if past_k is not None:
294
+ keys = torch.arange(past + length, device=ids.device)
295
+ mask = keys[None, :] <= positions[:, None]
296
+ x = self.embed(ids)
297
+ new_k, new_v = [], []
298
+ for index, block in enumerate(self.blocks):
299
+ x, k, v = block(x, cos, sin, None if past_k is None else past_k[index], None if past_v is None else past_v[index], mask)
300
+ new_k.append(k)
301
+ new_v.append(v)
302
+ logits = self.norm(x) @ (self.embed.weight.T if self.head is None else self.head)
303
+ return logits, torch.stack(new_k), torch.stack(new_v)
304
+
305
+
306
+ class BrowserStep(nn.Module):
307
+ """The ONNX graph: always takes a cache (possibly empty) and returns only the last position's logits."""
308
+
309
+ def __init__(self, model: TinyNext):
310
+ super().__init__()
311
+ self.model = model
312
+
313
+ def forward(self, ids, past_k, past_v):
314
+ logits, k, v = self.model(ids, past_k, past_v)
315
+ return logits[:, -1], k, v
316
+
317
+
318
+ # ---- Training --------------------------------------------------------------------------------------------------
319
+
320
+
321
+ class GpuMonitor:
322
+ """Reads GPU utilization and memory through NVML, so the logs show whether the GPU is actually busy."""
323
+
324
+ def __init__(self):
325
+ try:
326
+ import pynvml
327
+
328
+ pynvml.nvmlInit()
329
+ self.nvml, self.handle = pynvml, pynvml.nvmlDeviceGetHandleByIndex(0)
330
+ except Exception as error: # no NVIDIA GPU (a laptop smoke run): report nothing rather than fail
331
+ print(f"GPU monitoring off: {error}", flush=True)
332
+ self.nvml = None
333
+
334
+ def read(self) -> dict:
335
+ if not self.nvml:
336
+ return {}
337
+ rates = self.nvml.nvmlDeviceGetUtilizationRates(self.handle)
338
+ memory = self.nvml.nvmlDeviceGetMemoryInfo(self.handle)
339
+ return {"gpu_util_percent": rates.gpu, "gpu_memory_gb": round(memory.used / 1e9, 1)}
340
+
341
+
342
+ def evaluate_loss(model, rows: torch.Tensor, device, batch: int) -> float:
343
+ model.eval()
344
+ losses = []
345
+ with torch.no_grad(), torch.autocast(device.type, dtype=torch.bfloat16, enabled=device.type == "cuda"):
346
+ for at in range(0, len(rows), batch):
347
+ chunk = rows[at: at + batch].long()
348
+ logits, _, _ = model(chunk[:, :-1])
349
+ losses.append(F.cross_entropy(logits.float().flatten(0, 1), chunk[:, 1:].flatten()).item())
350
+ model.train()
351
+ return float(np.mean(losses))
352
+
353
+
354
+ def train(model, rows: torch.Tensor, val_rows: torch.Tensor, args, device, tracker) -> list[dict]:
355
+ steps = int(args.epochs * len(rows) // args.batch)
356
+ decay = [p for p in model.parameters() if p.dim() >= 2]
357
+ no_decay = [p for p in model.parameters() if p.dim() < 2]
358
+ optimizer = torch.optim.AdamW([{"params": decay, "weight_decay": 0.1}, {"params": no_decay, "weight_decay": 0.0}],
359
+ lr=args.lr, betas=(0.9, 0.95), fused=device.type == "cuda")
360
+ warmup = max(1, steps // 50)
361
+ schedule = lambda step: min(1, step / warmup) * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * min(1, step / steps))))
362
+ scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, schedule)
363
+ compiled = torch.compile(model) if device.type == "cuda" else model
364
+ parameters = sum(p.numel() for p in model.parameters())
365
+ gpu, history = GpuMonitor(), []
366
+ generator = torch.Generator(device=device).manual_seed(0)
367
+ window_started, window_tokens, started = time.time(), 0, time.time()
368
+ print(f"training {steps:,} steps of {args.batch * args.seq:,} tokens", flush=True)
369
+ for step in range(steps):
370
+ index = torch.randint(0, len(rows), (args.batch,), device=device, generator=generator)
371
+ chunk = rows[index].long()
372
+ with torch.autocast(device.type, dtype=torch.bfloat16, enabled=device.type == "cuda"):
373
+ logits, _, _ = compiled(chunk[:, :-1])
374
+ loss = F.cross_entropy(logits.float().flatten(0, 1), chunk[:, 1:].flatten())
375
+ optimizer.zero_grad(set_to_none=True)
376
+ loss.backward()
377
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
378
+ optimizer.step()
379
+ scheduler.step()
380
+ window_tokens += args.batch * args.seq
381
+ if step % args.log_every == 0 or step == steps - 1:
382
+ loss_value = loss.item() # syncs the GPU, so throughput below is real
383
+ seconds = time.time() - window_started
384
+ tokens_per_second = window_tokens / max(seconds, 1e-9)
385
+ entry = {
386
+ "step": step, "train_loss": round(loss_value, 4), "lr": round(scheduler.get_last_lr()[0], 6),
387
+ "tokens_per_second": round(tokens_per_second), "minutes": round((time.time() - started) / 60, 2),
388
+ # Training compute is about 6 FLOPs per parameter per token; an A100 does 312 TFLOP/s in bf16.
389
+ "mfu_percent": round(100 * 6 * parameters * tokens_per_second / 312e12, 1), **gpu.read(),
390
+ }
391
+ if step % (args.log_every * 10) == 0 or step == steps - 1:
392
+ entry["val_loss"] = round(evaluate_loss(model, val_rows, device, args.batch), 4)
393
+ history.append(entry)
394
+ tracker.log(entry, step=step)
395
+ print(json.dumps(entry), flush=True)
396
+ window_started, window_tokens = time.time(), 0
397
+ return history
398
+
399
+
400
+ # ---- Eval, export --------------------------------------------------------------------------------------------
401
+
402
+
403
+ def as_token_lm(ev, name: str, model: TinyNext, tokenizer, device, fim: bool = True):
404
+ texts = [tokenizer.decode([i], skip_special_tokens=False) if i >= len(SPECIAL) else "" for i in range(tokenizer.get_vocab_size())]
405
+
406
+ @torch.no_grad()
407
+ def next_logits(ids, cache):
408
+ past_k, past_v = cache if cache else (None, None)
409
+ logits, k, v = model(torch.tensor([ids], device=device), past_k, past_v)
410
+ return logits[0, -1].float().cpu().numpy(), (k, v)
411
+
412
+ encode = lambda text: tokenizer.encode(text).ids or [EOT]
413
+ # A model trained without fill-in-the-middle never saw the special tokens, so it only gets the text before.
414
+ prompt = (lambda prefix, suffix: prefix if not suffix else [SUF, *suffix, PRE, *prefix, MID]) if fim else None
415
+ return ev.TokenLM(name, next_logits, encode, texts, set(range(len(SPECIAL))), prompt=prompt)
416
+
417
+
418
+ def parity_prompts(ev, tokenizer, count: int = 300) -> list[list[int]]:
419
+ """Real text from the eval set, cut at random points, as token prompts."""
420
+ rng = random.Random(0)
421
+ texts = [p for domain in ev.load_eval_set().values() for doc in domain for p in doc]
422
+ prompts = []
423
+ for text in rng.sample(texts, min(count, len(texts))):
424
+ ids = tokenizer.encode(text[: rng.randint(40, len(text))]).ids[-300:]
425
+ if len(ids) >= 4:
426
+ prompts.append(ids)
427
+ return prompts
428
+
429
+
430
+ def greedy_tokens(step, ids: list[int], empty, steps: int) -> list[int]:
431
+ """Prompt, then `steps` cached greedy steps; returns the top token at each point."""
432
+ logits, k, v = step(np.array([ids]), empty, empty)
433
+ tops = [int(logits.argmax())]
434
+ for _ in range(steps):
435
+ logits, k, v = step(np.array([[tops[-1]]]), k, v)
436
+ tops.append(int(logits.argmax()))
437
+ return tops
438
+
439
+
440
+ def export(model: TinyNext, output: Path, device, prompts: list[list[int]]) -> dict:
441
+ """Float and int8 ONNX for the browser, checked against PyTorch on real prompts over several cached steps."""
442
+ import onnxruntime as ort
443
+ from onnxruntime.quantization import QuantType, quantize_dynamic
444
+
445
+ model = model.float().eval().to("cpu")
446
+ model.head = nn.Parameter(model.embed.weight.detach().T.contiguous().clone())
447
+ cfg = model.config
448
+ head_dim = cfg["dim"] // cfg["heads"]
449
+ empty = np.zeros((cfg["layers"], 1, cfg["heads"], 0, head_dim), np.float32)
450
+ # Trace with a non-empty cache so no shape gets fixed at zero; the checks below start from an empty one.
451
+ with torch.no_grad():
452
+ _, trace_k, trace_v = model(torch.tensor([[1, 2]]))
453
+ float_path, int8_path = output / "model.onnx", output / "model_int8.onnx"
454
+ torch.onnx.export(
455
+ BrowserStep(model), (torch.tensor([[5, 6, 7]]), trace_k, trace_v), float_path, opset_version=17,
456
+ input_names=["input_ids", "past_k", "past_v"], output_names=["logits", "present_k", "present_v"],
457
+ dynamic_axes={"input_ids": {1: "length"}, "past_k": {3: "past"}, "past_v": {3: "past"},
458
+ "present_k": {3: "total"}, "present_v": {3: "total"}},
459
+ dynamo=False,
460
+ )
461
+ quantize_dynamic(float_path, int8_path, weight_type=QuantType.QInt8, op_types_to_quantize=["MatMul", "Gather"], per_channel=True)
462
+
463
+ @torch.no_grad()
464
+ def torch_step(ids, k, v):
465
+ past = None if k.shape[3] == 0 else (torch.from_numpy(k), torch.from_numpy(v))
466
+ logits, nk, nv = model(torch.from_numpy(ids), *(past or (None, None)))
467
+ return logits[0, -1].numpy(), nk.numpy(), nv.numpy()
468
+
469
+ expected = [greedy_tokens(torch_step, ids, empty, 4) for ids in prompts]
470
+ report = {"parity_prompts": len(prompts), "parity_steps_per_prompt": 5}
471
+ for name, path in (("float", float_path), ("int8", int8_path)):
472
+ session = ort.InferenceSession(str(path), providers=["CPUExecutionProvider"])
473
+ onnx_step = lambda ids, k, v: (lambda out: (out[0][0], out[1], out[2]))(
474
+ session.run(None, {"input_ids": ids.astype(np.int64), "past_k": k, "past_v": v}))
475
+ got = [greedy_tokens(onnx_step, ids, empty, 4) for ids in prompts]
476
+ first = np.mean([g[0] == e[0] for g, e in zip(got, expected)])
477
+ all_steps = np.mean([g == e for g, e in zip(got, expected)])
478
+ _, pk, pv = onnx_step(np.array([prompts[0]]), empty, empty)
479
+ timings = []
480
+ for _ in range(200):
481
+ began = time.perf_counter()
482
+ onnx_step(np.array([[8]]), pk, pv)
483
+ timings.append((time.perf_counter() - began) * 1000)
484
+ report[name] = {
485
+ "megabytes": round(path.stat().st_size / 1e6, 2),
486
+ "same_first_token": round(float(first), 4),
487
+ "same_five_greedy_tokens": round(float(all_steps), 4),
488
+ "cached_step_ms_p50_cpu": round(float(np.median(timings)), 3),
489
+ }
490
+ model.head = None
491
+ model.to(device)
492
+ return report
493
+
494
+
495
+ def headline(scored: dict) -> dict:
496
+ """The few eval numbers worth a chart: typing saved and 2+ word suggestions at the app's cutoff."""
497
+ table = scored["cutoffs"]["0.5"]
498
+ out = {}
499
+ for key, m in table.items():
500
+ slug = key.replace("/", "_")
501
+ out[f"eval/{slug}/saved_with_tab"] = m["saved_with_tab"]
502
+ out[f"eval/{slug}/right"] = m["right"]
503
+ out[f"eval/{slug}/multiword_shown"] = m["multiword_shown"]
504
+ return out
505
+
506
+
507
+ # ---- Prepared data ---------------------------------------------------------------------------------------------
508
+
509
+
510
+ def data_key(args) -> str:
511
+ """Prepared data depends only on these settings, so a training run can find the matching files."""
512
+ return f"{args.name}-v{args.vocab}-s{args.seq}-t{int(args.tokens / 1e6)}M-fim{args.fim_rate}-{args.mix}".replace(",", "_").replace("=", "")
513
+
514
+
515
+ def prepare(args, weights: dict, output: Path):
516
+ budgets = {name: int((args.tokens + args.val_tokens) * weight) for name, weight in weights.items()}
517
+ started = time.time()
518
+ sources = load_sources(args, budgets)
519
+ print(f"sources ready in {time.time() - started:.0f}s: " + ", ".join(f"{n} {len(d):,} docs" for n, d in sources.items()), flush=True)
520
+ tokenizer = train_tokenizer(sources, weights, args.vocab, args.tokenizer_docs)
521
+ tokenizer.save(str(output / "tokenizer.json"))
522
+ rows, val_rows, counts = build_rows(sources, tokenizer, weights, args)
523
+ return tokenizer, rows, val_rows, counts
524
+
525
+
526
+ def upload_prepared(args, output: Path, rows: np.ndarray, val_rows: np.ndarray, counts: dict, minutes: float):
527
+ from huggingface_hub import HfApi
528
+
529
+ folder = output / "data"
530
+ folder.mkdir(exist_ok=True)
531
+ rows.astype(np.uint16).tofile(folder / "train.bin")
532
+ val_rows.astype(np.uint16).tofile(folder / "val.bin")
533
+ shutil.copy(output / "tokenizer.json", folder / "tokenizer.json")
534
+ meta = {"seq": args.seq, "train_rows": len(rows), "val_rows": len(val_rows), "counts": counts, "minutes": round(minutes, 1), "args": vars(args)}
535
+ (folder / "meta.json").write_text(json.dumps(meta, indent=1))
536
+ api = HfApi(token=os.environ.get("HF_TOKEN"))
537
+ api.create_repo(args.data_repo, repo_type="dataset", private=True, exist_ok=True)
538
+ api.upload_folder(folder_path=str(folder), repo_id=args.data_repo, repo_type="dataset", path_in_repo=data_key(args),
539
+ commit_message=f"prepared data: {data_key(args)}")
540
+ print(f"Prepared data at https://huggingface.co/datasets/{args.data_repo}/tree/main/{data_key(args)}", flush=True)
541
+
542
+
543
+ def download_prepared(args, output: Path):
544
+ from huggingface_hub import snapshot_download
545
+ from tokenizers import Tokenizer
546
+
547
+ local = Path(snapshot_download(args.data_repo, repo_type="dataset", allow_patterns=[f"{data_key(args)}/*"], max_workers=16))
548
+ folder = local / data_key(args)
549
+ meta = json.loads((folder / "meta.json").read_text())
550
+ shutil.copy(folder / "tokenizer.json", output / "tokenizer.json")
551
+ rows = np.fromfile(folder / "train.bin", dtype=np.uint16).reshape(meta["train_rows"], meta["seq"] + 1)
552
+ val_rows = np.fromfile(folder / "val.bin", dtype=np.uint16).reshape(meta["val_rows"], meta["seq"] + 1)
553
+ return Tokenizer.from_file(str(output / "tokenizer.json")), rows, val_rows, meta["counts"]
554
+
555
+
556
+ # ---- Main ------------------------------------------------------------------------------------------------------
557
+
558
+
559
+ def parse_args():
560
+ parser = argparse.ArgumentParser()
561
+ parser.add_argument("--vocab", type=int, default=8192)
562
+ parser.add_argument("--dim", type=int, default=320)
563
+ parser.add_argument("--layers", type=int, default=8)
564
+ parser.add_argument("--heads", type=int, default=5)
565
+ parser.add_argument("--ffn", type=int, default=864)
566
+ parser.add_argument("--seq", type=int, default=512)
567
+ parser.add_argument("--tokens", type=float, default=1.5e9, help="Training tokens")
568
+ parser.add_argument("--val-tokens", type=float, default=4e6)
569
+ parser.add_argument("--epochs", type=float, default=1.0)
570
+ parser.add_argument("--batch", type=int, default=512)
571
+ parser.add_argument("--lr", type=float, default=3e-3)
572
+ parser.add_argument("--fim-rate", type=float, default=0.5)
573
+ parser.add_argument("--mix", default="fineweb=0.6,smoltalk=0.3,enron=0.1")
574
+ parser.add_argument("--fineweb-files", type=int, default=2, help="Each is about 2 GB of text, roughly 0.9B tokens")
575
+ parser.add_argument("--smoltalk-files", type=int, default=9)
576
+ parser.add_argument("--tokenizer-docs", type=int, default=200_000)
577
+ parser.add_argument("--eval-pauses", type=int, default=1000)
578
+ parser.add_argument("--log-every", type=int, default=50)
579
+ parser.add_argument("--repo", default="cfahlgren1/tinynext")
580
+ parser.add_argument("--folder", default="v2", help="Where this run's outputs are uploaded")
581
+ parser.add_argument("--eval-folder", default="v2", help="Where the shared eval.py and eval_set.json live")
582
+ parser.add_argument("--name", default="tinynext-v2")
583
+ parser.add_argument("--trackio-space", default="cfahlgren1/tinynext-trackio")
584
+ parser.add_argument("--stage", choices=("all", "prepare", "train"), default="all",
585
+ help="prepare: build the tokenizer and token arrays on a CPU job and save them to --data-repo. "
586
+ "train: download them and train on the GPU. all: both in one job.")
587
+ parser.add_argument("--data-repo", default="cfahlgren1/tinynext-data")
588
+ parser.add_argument("--smoke", action="store_true", help="Tiny settings to check the pipeline end to end")
589
+ parser.add_argument("--no-push", action="store_true")
590
+ args = parser.parse_args()
591
+ if args.smoke:
592
+ args.vocab, args.dim, args.layers, args.heads, args.ffn, args.seq = 2048, 64, 2, 2, 128, 256
593
+ args.tokens, args.val_tokens, args.batch, args.tokenizer_docs = 5e6, 2e5, 64, 5000
594
+ args.fineweb_files, args.smoltalk_files, args.eval_pauses, args.log_every = 1, 1, 20, 10
595
+ args.name = "tinynext-v2-smoke"
596
+ return args
597
+
598
+
599
+ class NoTracker:
600
+ def log(self, *_, **__):
601
+ pass
602
+
603
+ def finish(self):
604
+ pass
605
+
606
+
607
+ def start_tracker(args):
608
+ if args.no_push and not os.environ.get("HF_TOKEN"):
609
+ return NoTracker()
610
+ import trackio
611
+
612
+ trackio.init(project="tinynext", name=args.name, space_id=args.trackio_space, config=vars(args))
613
+ return trackio
614
+
615
+
616
+ def main():
617
+ args = parse_args()
618
+ device = torch.device("cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu")
619
+ torch.manual_seed(0)
620
+ torch.backends.cuda.matmul.allow_tf32 = True
621
+ torch.backends.cudnn.allow_tf32 = True
622
+ output = HERE / "output"
623
+ output.mkdir(exist_ok=True)
624
+ ev = load_eval_module(args.repo, args.eval_folder)
625
+ weights = {k: float(v) for k, v in (part.split("=") for part in args.mix.split(","))}
626
+ print(f"device {device}, {cpus()} CPUs, mix {weights}", flush=True)
627
+ tracker = start_tracker(args)
628
+
629
+ started = time.time()
630
+ if args.stage == "train":
631
+ tokenizer, rows, val_rows, counts = download_prepared(args, output)
632
+ else:
633
+ tokenizer, rows, val_rows, counts = prepare(args, weights, output)
634
+ data_minutes = (time.time() - started) / 60
635
+ print(f"data ready in {data_minutes:.1f} min: {rows.size / 1e6:.0f}M train tokens, {val_rows.size / 1e6:.1f}M val", flush=True)
636
+ if args.stage == "prepare":
637
+ upload_prepared(args, output, rows, val_rows, counts, data_minutes)
638
+ return
639
+
640
+ # The whole token array lives on the GPU; each step just indexes into it.
641
+ rows_gpu = torch.from_numpy(rows.astype(np.int32)).to(device)
642
+ val_gpu = torch.from_numpy(val_rows.astype(np.int32)).to(device)
643
+ model = TinyNext(tokenizer.get_vocab_size(), args.dim, args.layers, args.heads, args.ffn).to(device)
644
+ parameters = sum(p.numel() for p in model.parameters())
645
+ print(f"{parameters / 1e6:.2f}M parameters", flush=True)
646
+ started = time.time()
647
+ history = train(model, rows_gpu, val_gpu, args, device, tracker)
648
+ train_minutes = (time.time() - started) / 60
649
+
650
+ model.eval()
651
+ from safetensors.torch import save_file
652
+
653
+ save_file({k: v.contiguous() for k, v in model.state_dict().items()}, output / "model.safetensors")
654
+ scored = ev.score(as_token_lm(ev, args.name, model, tokenizer, device, fim=args.fim_rate > 0), ev.pauses(ev.load_eval_set(), args.eval_pauses))
655
+ (output / "metrics").mkdir(exist_ok=True)
656
+ (output / "metrics" / f"{args.name}.json").write_text(json.dumps(scored, indent=1))
657
+ tracker.log(headline(scored))
658
+ exported = export(model, output, device, parity_prompts(ev, tokenizer))
659
+
660
+ config = {
661
+ **model.config, "parameters": parameters, "seq": args.seq, "special_tokens": dict(zip(SPECIAL, range(len(SPECIAL)))),
662
+ "prompt": "No text after the cursor: the text before it. Otherwise: <|suf|> after <|pre|> before <|mid|>.",
663
+ }
664
+ (output / "config.json").write_text(json.dumps(config, indent=1))
665
+ report = {"args": vars(args), "parameters": parameters, "data": counts, "data_minutes": round(data_minutes, 1),
666
+ "train_minutes": round(train_minutes, 1), "history": history, "exports": exported}
667
+ (output / "report.json").write_text(json.dumps(report, indent=1))
668
+ print(json.dumps(exported, indent=1))
669
+ print(ev.table([output / "metrics" / f"{args.name}.json"]))
670
+ tracker.finish()
671
+
672
+ for name in ("train.py", "eval.py", "eval_set.json"):
673
+ shutil.copy(HERE / name, output / name)
674
+ if (HERE / "metrics").exists():
675
+ for path in (HERE / "metrics").glob("*.json"):
676
+ if not (output / "metrics" / path.name).exists():
677
+ shutil.copy(path, output / "metrics" / path.name)
678
+ if args.no_push:
679
+ print(f"Skipping upload; everything is in {output}")
680
+ return
681
+ from huggingface_hub import HfApi
682
+
683
+ HfApi(token=os.environ.get("HF_TOKEN")).upload_folder(
684
+ folder_path=str(output), repo_id=args.repo, path_in_repo=args.folder, repo_type="model",
685
+ commit_message=f"{args.name}: {parameters / 1e6:.1f}M params, {train_minutes:.0f} min on GPU",
686
+ )
687
+ print(f"Uploaded to https://huggingface.co/{args.repo}/tree/main/{args.folder}")
688
+
689
+
690
+ if __name__ == "__main__":
691
+ main()