@tuned-tensor/cli 0.13.1-beta.5 → 0.13.1-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ from tokenizers import Tokenizer
8
8
  from torch.nn import functional as F
9
9
 
10
10
  from common import load_config, write_json
11
- from data import END, decoded_byte_count, encode_ids, example_pairs, format_prompt, smoke_corpus, window_tokens
11
+ from data import END, decoded_byte_count, encode_generation_prompt, example_pairs, iter_corpus_documents, smoke_corpus, window_tokens
12
12
  from model import bits_per_byte, generate, load_model
13
13
 
14
14
 
@@ -45,12 +45,19 @@ def evaluate_chat(
45
45
  correct = 0
46
46
  model.eval()
47
47
  for example in examples:
48
- prompt_ids = encode_ids(tokenizer, format_prompt(system_prompt, example["input"])) or [0]
48
+ context_length = int(model.config["sequence_length"])
49
+ prompt_ids, completion_tokens = encode_generation_prompt(
50
+ tokenizer,
51
+ system_prompt,
52
+ example["input"],
53
+ context_length,
54
+ 32,
55
+ )
49
56
  tokens = torch.tensor([prompt_ids], dtype=torch.long, device=device)
50
57
  generated = generate(
51
58
  model,
52
59
  tokens,
53
- max_new_tokens=32,
60
+ max_new_tokens=completion_tokens,
54
61
  stop_token_id=tokenizer.token_to_id(END),
55
62
  )[0].tolist()[len(prompt_ids):]
56
63
  text = tokenizer.decode(generated).split(END)[0].strip()
@@ -65,14 +72,27 @@ def evaluate_chat(
65
72
  }
66
73
 
67
74
 
68
- def evaluate_inference(model, tokenizer: Tokenizer, prompt: str, device: torch.device) -> dict[str, float | int | str]:
69
- prompt_ids = encode_ids(tokenizer, prompt) or [0]
70
- tokens = torch.tensor([prompt_ids[: min(16, len(prompt_ids))]], dtype=torch.long, device=device)
75
+ def evaluate_inference(
76
+ model,
77
+ tokenizer: Tokenizer,
78
+ system_prompt: str,
79
+ user_prompt: str,
80
+ device: torch.device,
81
+ ) -> dict[str, float | int | str]:
82
+ context_length = int(model.config["sequence_length"])
83
+ prompt_ids, completion_tokens = encode_generation_prompt(
84
+ tokenizer,
85
+ system_prompt,
86
+ user_prompt,
87
+ context_length,
88
+ 32,
89
+ )
90
+ tokens = torch.tensor([prompt_ids], dtype=torch.long, device=device)
71
91
  if device.type == "cuda":
72
92
  torch.cuda.reset_peak_memory_stats(device)
73
93
  torch.cuda.synchronize()
74
94
  started = time.perf_counter()
75
- generated = generate(model, tokens, max_new_tokens=32)
95
+ generated = generate(model, tokens, max_new_tokens=completion_tokens)
76
96
  if device.type == "cuda":
77
97
  torch.cuda.synchronize()
78
98
  elapsed = max(time.perf_counter() - started, 1e-6)
@@ -98,13 +118,27 @@ def main(argv: list[str] | None = None) -> None:
98
118
  output_dir = Path(config["output_dir"])
99
119
  metrics: dict = {"ok": True, "evaluator": evaluator, "device": str(device)}
100
120
  if evaluator == "bpb":
101
- corpus = smoke_corpus(examples, int(config.get("max_chars") or 20_000), extra=str(config.get("system_prompt") or ""))
121
+ validation_path = config.get("validation_path")
122
+ if validation_path:
123
+ validation_chars = int(config.get("validation_max_chars") or 2_000_000)
124
+ corpus = "".join(iter_corpus_documents(validation_path, validation_chars))
125
+ metrics["held_out"] = True
126
+ metrics["validation_path"] = str(validation_path)
127
+ else:
128
+ corpus = smoke_corpus(examples, int(config.get("max_chars") or 20_000), extra=str(config.get("system_prompt") or ""))
129
+ metrics["held_out"] = False
102
130
  metrics.update(evaluate_bpb(model, tokenizer, corpus, sequence_length, device))
103
131
  elif evaluator == "chat":
104
132
  metrics.update(evaluate_chat(model, tokenizer, str(config.get("system_prompt") or ""), examples, device))
105
133
  elif evaluator == "inference":
106
134
  prompt = examples[0]["input"] if examples else "hello"
107
- metrics.update(evaluate_inference(model, tokenizer, prompt, device))
135
+ metrics.update(evaluate_inference(
136
+ model,
137
+ tokenizer,
138
+ str(config.get("system_prompt") or ""),
139
+ prompt,
140
+ device,
141
+ ))
108
142
  else:
109
143
  raise SystemExit(f"Unknown foundation evaluator: {evaluator}")
110
144
  write_json(output_dir / "metrics.json", metrics)
@@ -1,63 +1,475 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
4
+ import json
5
+ import math
6
+ import os
3
7
  from pathlib import Path
8
+ import signal
9
+ import time
10
+ from typing import Any
4
11
 
12
+ import numpy as np
5
13
  import torch
6
14
  from tokenizers import Tokenizer
7
15
  from torch.nn import functional as F
8
16
 
9
- from common import load_config, require_cuda, write_json
10
- from data import corpus_hash, decoded_byte_count, example_pairs, smoke_corpus, window_tokens
11
- from model import FoundationGPT, bits_per_byte, model_config_from_depth, param_count, save_model
17
+ from checkpoint import CheckpointManager
18
+ from common import append_jsonl, ensure_private_directory, load_config, make_private_file, require_cuda, write_json
19
+ from data import (
20
+ END,
21
+ corpus_hash,
22
+ corpus_manifest,
23
+ example_pairs,
24
+ instruction_corpus_documents,
25
+ iter_corpus_documents,
26
+ smoke_corpus,
27
+ )
28
+ from model import FoundationGPT, model_config_from_depth, param_count, save_model
29
+
30
+
31
+ def stable_fingerprint(value: Any) -> str:
32
+ return hashlib.sha256(
33
+ json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"),
34
+ ).hexdigest()
35
+
36
+
37
+ def file_sha256(path: str | Path) -> str:
38
+ digest = hashlib.sha256()
39
+ with Path(path).open("rb") as source:
40
+ while chunk := source.read(1_048_576):
41
+ digest.update(chunk)
42
+ return digest.hexdigest()
43
+
44
+
45
+ def source_signature(config: dict[str, Any]) -> dict[str, Any]:
46
+ corpus_path = config.get("corpus_path")
47
+ if corpus_path:
48
+ instruction = str(config.get("system_prompt") or "").strip()
49
+ return {
50
+ "kind": "files",
51
+ "manifest": corpus_manifest(corpus_path),
52
+ "max_chars": int(config.get("max_chars") or 0),
53
+ "instruction_sha256": corpus_hash(instruction),
54
+ }
55
+ examples = example_pairs(config.get("examples") or [])
56
+ corpus = smoke_corpus(
57
+ examples,
58
+ int(config.get("max_chars") or 20_000),
59
+ extra=str(config.get("system_prompt") or ""),
60
+ )
61
+ return {"kind": "smoke", "sha256": corpus_hash(corpus), "chars": len(corpus)}
62
+
63
+
64
+ def prepare_token_cache(
65
+ tokenizer: Tokenizer,
66
+ tokenizer_path: Path,
67
+ config: dict[str, Any],
68
+ output_dir: Path,
69
+ ) -> tuple[np.memmap, dict[str, Any]]:
70
+ cache_path = output_dir / "pretrain-tokens.bin"
71
+ metadata_path = output_dir / "pretrain-tokens.json"
72
+ signature = {
73
+ "source": source_signature(config),
74
+ "tokenizer_sha256": file_sha256(tokenizer_path),
75
+ }
76
+ if cache_path.is_file() and metadata_path.is_file():
77
+ try:
78
+ metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
79
+ except json.JSONDecodeError:
80
+ metadata = {}
81
+ expected_size = int(metadata.get("tokens", 0)) * np.dtype(np.uint32).itemsize
82
+ if (
83
+ metadata.get("signature") == signature
84
+ and expected_size == cache_path.stat().st_size
85
+ and isinstance(metadata.get("tokens_sha256"), str)
86
+ and file_sha256(cache_path) == metadata["tokens_sha256"]
87
+ ):
88
+ return np.memmap(cache_path, dtype=np.uint32, mode="r"), metadata
89
+
90
+ partial_path = output_dir / ".pretrain-tokens.partial"
91
+ partial_metadata_path = output_dir / ".pretrain-tokens.partial.json"
92
+ partial_metadata: dict[str, Any] = {}
93
+ if partial_metadata_path.is_file():
94
+ try:
95
+ partial_metadata = json.loads(partial_metadata_path.read_text(encoding="utf-8"))
96
+ except json.JSONDecodeError:
97
+ partial_metadata = {}
98
+ if cache_path.is_file() and partial_metadata.get("signature") == signature:
99
+ expected_size = int(partial_metadata.get("bytes", -1))
100
+ expected_tokens = int(partial_metadata.get("tokens", 0))
101
+ expected_hash = partial_metadata.get("tokens_sha256")
102
+ if (
103
+ expected_size == cache_path.stat().st_size
104
+ and expected_tokens > 0
105
+ and expected_tokens * np.dtype(np.uint32).itemsize == expected_size
106
+ and isinstance(expected_hash, str)
107
+ and file_sha256(cache_path) == expected_hash
108
+ ):
109
+ metadata = {
110
+ "signature": signature,
111
+ "tokens": int(partial_metadata["tokens"]),
112
+ "documents": int(partial_metadata["documents"]),
113
+ "source_documents": int(partial_metadata.get("source_documents", partial_metadata["documents"])),
114
+ "tokens_sha256": str(partial_metadata["tokens_sha256"]),
115
+ }
116
+ write_json(metadata_path, metadata)
117
+ partial_metadata_path.unlink(missing_ok=True)
118
+ return np.memmap(cache_path, dtype=np.uint32, mode="r"), metadata
119
+ if partial_metadata.get("signature") != signature or not partial_path.is_file():
120
+ partial_path.unlink(missing_ok=True)
121
+ partial_metadata_path.unlink(missing_ok=True)
122
+ partial_metadata = {}
123
+
124
+ digest = hashlib.sha256()
125
+ token_count = int(partial_metadata.get("tokens", 0))
126
+ document_count = int(partial_metadata.get("documents", 0))
127
+ source_documents = int(partial_metadata.get("source_documents", document_count))
128
+ recorded_bytes = int(partial_metadata.get("bytes", 0))
129
+ if partial_path.is_file():
130
+ partial_size = partial_path.stat().st_size
131
+ valid_partial = (
132
+ 0 <= recorded_bytes <= partial_size
133
+ and token_count >= 0
134
+ and token_count * np.dtype(np.uint32).itemsize == recorded_bytes
135
+ and isinstance(partial_metadata.get("tokens_sha256"), str)
136
+ )
137
+ if valid_partial:
138
+ with partial_path.open("r+b") as partial:
139
+ partial.truncate(recorded_bytes)
140
+ with partial_path.open("rb") as partial:
141
+ while chunk := partial.read(1_048_576):
142
+ digest.update(chunk)
143
+ valid_partial = digest.hexdigest() == partial_metadata["tokens_sha256"]
144
+ if not valid_partial:
145
+ partial_path.unlink(missing_ok=True)
146
+ partial_metadata_path.unlink(missing_ok=True)
147
+ partial_metadata = {}
148
+ digest = hashlib.sha256()
149
+ token_count = 0
150
+ document_count = 0
151
+ source_documents = 0
152
+ recorded_bytes = 0
153
+ corpus_path = config.get("corpus_path")
154
+ if corpus_path:
155
+ documents = instruction_corpus_documents(
156
+ str(config.get("system_prompt") or ""),
157
+ iter_corpus_documents(corpus_path, int(config.get("max_chars") or 0)),
158
+ )
159
+ else:
160
+ examples = example_pairs(config.get("examples") or [])
161
+ documents = iter([smoke_corpus(
162
+ examples,
163
+ int(config.get("max_chars") or 20_000),
164
+ extra=str(config.get("system_prompt") or ""),
165
+ )])
166
+ separator = tokenizer.token_to_id(END)
167
+
168
+ def persist_partial(destination: Any) -> None:
169
+ destination.flush()
170
+ os.fsync(destination.fileno())
171
+ write_json(partial_metadata_path, {
172
+ "signature": signature,
173
+ "tokens": token_count,
174
+ "documents": document_count,
175
+ "source_documents": source_documents,
176
+ "bytes": destination.tell(),
177
+ "tokens_sha256": digest.hexdigest(),
178
+ })
179
+
180
+ mode = "ab" if partial_path.is_file() else "wb"
181
+ with partial_path.open(mode) as destination:
182
+ os.fchmod(destination.fileno(), 0o600)
183
+ for source_index, document in enumerate(documents):
184
+ if source_index < source_documents:
185
+ continue
186
+ source_documents += 1
187
+ ids = tokenizer.encode(document).ids
188
+ if separator is not None:
189
+ ids.append(separator)
190
+ if not ids:
191
+ continue
192
+ values = np.asarray(ids, dtype=np.uint32)
193
+ payload = values.tobytes()
194
+ destination.write(payload)
195
+ digest.update(payload)
196
+ token_count += len(ids)
197
+ document_count += 1
198
+ if document_count % 1024 == 0:
199
+ persist_partial(destination)
200
+ persist_partial(destination)
201
+ if source_signature(config) != signature["source"]:
202
+ partial_path.unlink(missing_ok=True)
203
+ partial_metadata_path.unlink(missing_ok=True)
204
+ raise RuntimeError("Foundation corpus changed while the token cache was being built.")
205
+ if token_count < 2:
206
+ partial_path.unlink(missing_ok=True)
207
+ partial_metadata_path.unlink(missing_ok=True)
208
+ raise ValueError("Foundation pretraining corpus produced fewer than two tokens.")
209
+ os.replace(partial_path, cache_path)
210
+ make_private_file(cache_path)
211
+ metadata = {
212
+ "signature": signature,
213
+ "tokens": token_count,
214
+ "documents": document_count,
215
+ "source_documents": source_documents,
216
+ "tokens_sha256": digest.hexdigest(),
217
+ }
218
+ write_json(metadata_path, metadata)
219
+ partial_metadata_path.unlink(missing_ok=True)
220
+ return np.memmap(cache_path, dtype=np.uint32, mode="r"), metadata
221
+
222
+
223
+ def coprime_stride(windows: int, seed: int) -> int:
224
+ if windows <= 1:
225
+ return 1
226
+ candidate = max(1, (2 * abs(seed) + 1) % windows)
227
+ while math.gcd(candidate, windows) != 1:
228
+ candidate = (candidate + 1) % windows or 1
229
+ return candidate
230
+
231
+
232
+ def token_batch(
233
+ tokens: np.memmap,
234
+ *,
235
+ samples_consumed: int,
236
+ batch_size: int,
237
+ sequence_length: int,
238
+ seed: int,
239
+ ) -> np.ndarray:
240
+ if len(tokens) < sequence_length + 1:
241
+ repeats = math.ceil((sequence_length + 1) / len(tokens))
242
+ expanded = np.tile(np.asarray(tokens), repeats)
243
+ return np.stack([expanded[:sequence_length + 1]] * batch_size)
244
+ windows = max(1, (len(tokens) - 1) // sequence_length)
245
+ stride = coprime_stride(windows, seed)
246
+ offset = abs(seed) % windows
247
+ rows: list[np.ndarray] = []
248
+ for index in range(batch_size):
249
+ window = ((samples_consumed + index) * stride + offset) % windows
250
+ start = window * sequence_length
251
+ rows.append(np.asarray(tokens[start:start + sequence_length + 1]))
252
+ return np.stack(rows)
253
+
254
+
255
+ def learning_rate_multiplier(step: int, *, warmup_steps: int, total_steps: int, min_ratio: float) -> float:
256
+ if warmup_steps > 0 and step < warmup_steps:
257
+ return max((step + 1) / warmup_steps, 1 / max(warmup_steps, 1))
258
+ decay_steps = max(total_steps - warmup_steps, 1)
259
+ progress = min(max((step - warmup_steps) / decay_steps, 0.0), 1.0)
260
+ cosine = 0.5 * (1.0 + math.cos(math.pi * progress))
261
+ return min_ratio + (1.0 - min_ratio) * cosine
262
+
263
+
264
+ class StopRequest:
265
+ signal_number: int | None = None
266
+
267
+ def install(self) -> None:
268
+ def request_stop(number: int, _frame: Any) -> None:
269
+ self.signal_number = number
270
+
271
+ signal.signal(signal.SIGINT, request_stop)
272
+ signal.signal(signal.SIGTERM, request_stop)
12
273
 
13
274
 
14
275
  def main(argv: list[str] | None = None) -> None:
15
276
  config = load_config(argv)
16
277
  require_cuda("pretrain")
17
278
  device = torch.device("cuda")
18
- tokenizer = Tokenizer.from_file(str(Path(config["tokenizer_dir"]) / "tokenizer.json"))
19
- examples = example_pairs(config.get("examples") or [])
20
- corpus = smoke_corpus(examples, int(config.get("max_chars") or 20_000), extra=str(config.get("system_prompt") or ""))
21
- token_ids = tokenizer.encode(corpus).ids
279
+ torch.set_float32_matmul_precision("high")
280
+ seed = int(config.get("seed") or 1337)
281
+ torch.manual_seed(seed)
282
+ torch.cuda.manual_seed_all(seed)
283
+
284
+ tokenizer_path = Path(config["tokenizer_dir"]) / "tokenizer.json"
285
+ tokenizer = Tokenizer.from_file(str(tokenizer_path))
22
286
  sequence_length = int(config.get("sequence_length") or 64)
23
- windows = window_tokens(token_ids, sequence_length)
24
287
  depth = int(config.get("depth") or 2)
25
288
  model_config = model_config_from_depth(depth, tokenizer.get_vocab_size(), sequence_length)
26
289
  model = FoundationGPT(model_config).to(device)
27
- optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
28
- batch_size = max(1, int(config.get("batch_size") or 2))
290
+
291
+ output_dir = ensure_private_directory(config["output_dir"])
292
+ work_dir = ensure_private_directory(config.get("work_dir") or output_dir)
293
+ tokens, token_metadata = prepare_token_cache(tokenizer, tokenizer_path, config, work_dir)
29
294
  steps = max(1, int(config.get("steps") or 2))
295
+ batch_size = max(1, int(config.get("batch_size") or 2))
296
+ accumulation_steps = max(1, int(config.get("gradient_accumulation_steps") or 1))
297
+ learning_rate = float(config.get("learning_rate") or 3e-4)
298
+ weight_decay = float(config.get("weight_decay") if config.get("weight_decay") is not None else 0.1)
299
+ configured_warmup = config.get("warmup_steps")
300
+ warmup_steps = min(
301
+ max(0, int(configured_warmup)) if configured_warmup is not None else min(2000, max(1, steps // 100)),
302
+ steps,
303
+ )
304
+ min_lr_ratio = float(config.get("min_lr_ratio") if config.get("min_lr_ratio") is not None else 0.1)
305
+ gradient_clip = float(config.get("gradient_clip") or 1.0)
306
+ bf16 = bool(config.get("bf16", True))
307
+ if bf16 and not torch.cuda.is_bf16_supported():
308
+ raise RuntimeError("Foundation BF16 training was requested, but this CUDA device does not support BF16.")
309
+
310
+ optimizer = torch.optim.AdamW(
311
+ model.parameters(),
312
+ lr=learning_rate,
313
+ weight_decay=weight_decay,
314
+ fused=True,
315
+ )
316
+ scheduler = torch.optim.lr_scheduler.LambdaLR(
317
+ optimizer,
318
+ lambda current: learning_rate_multiplier(
319
+ current,
320
+ warmup_steps=warmup_steps,
321
+ total_steps=steps,
322
+ min_ratio=min_lr_ratio,
323
+ ),
324
+ )
325
+ recovery_config = {
326
+ "model": model_config,
327
+ "steps": steps,
328
+ "batch_size": batch_size,
329
+ "gradient_accumulation_steps": accumulation_steps,
330
+ "learning_rate": learning_rate,
331
+ "weight_decay": weight_decay,
332
+ "warmup_steps": warmup_steps,
333
+ "min_lr_ratio": min_lr_ratio,
334
+ "gradient_clip": gradient_clip,
335
+ "bf16": bf16,
336
+ "seed": seed,
337
+ "token_cache": token_metadata,
338
+ }
339
+ manager = CheckpointManager(
340
+ work_dir,
341
+ stable_fingerprint(recovery_config),
342
+ keep=int(config.get("keep_checkpoints") or 3),
343
+ backup_dir=config.get("checkpoint_backup_dir"),
344
+ )
345
+ start_step = 0
346
+ samples_consumed = 0
347
+ tokens_seen = 0
30
348
  last_loss = 0.0
31
- last_bpb = 0.0
349
+ if bool(config.get("resume", True)):
350
+ resumed = manager.load(model, optimizer, scheduler, device)
351
+ if resumed is not None:
352
+ start_step = resumed.step
353
+ samples_consumed = resumed.samples_consumed
354
+ tokens_seen = resumed.tokens_seen
355
+ last_loss = resumed.last_loss
356
+ print(json.dumps({"event": "resumed", "step": start_step, "checkpoint": str(resumed.path)}), flush=True)
357
+
358
+ checkpoint_steps = max(1, int(config.get("checkpoint_interval_steps") or 500))
359
+ checkpoint_seconds = max(1, int(config.get("checkpoint_interval_seconds") or 1800))
360
+ log_steps = max(1, int(config.get("log_interval_steps") or 10))
361
+ metrics_path = work_dir / "training-metrics.jsonl"
362
+ stop = StopRequest()
363
+ stop.install()
364
+ started = time.monotonic()
365
+ last_log_time = started
366
+ last_log_tokens = tokens_seen
367
+ last_checkpoint_time = started
32
368
  model.train()
33
- for step in range(steps):
34
- batch = windows[step % len(windows): step % len(windows) + batch_size]
35
- if len(batch) < batch_size:
36
- batch = (batch + windows)[:batch_size]
37
- tensor = torch.tensor(batch, dtype=torch.long, device=device)
38
- inputs = tensor[:, :-1]
39
- labels = tensor[:, 1:]
40
- logits = model(inputs)
41
- loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))
369
+
370
+ for step in range(start_step, steps):
42
371
  optimizer.zero_grad(set_to_none=True)
43
- loss.backward()
372
+ accumulated_loss = 0.0
373
+ for _microstep in range(accumulation_steps):
374
+ rows = token_batch(
375
+ tokens,
376
+ samples_consumed=samples_consumed,
377
+ batch_size=batch_size,
378
+ sequence_length=sequence_length,
379
+ seed=seed,
380
+ )
381
+ tensor = torch.from_numpy(rows.astype(np.int64, copy=False)).to(device=device, non_blocking=True)
382
+ inputs = tensor[:, :-1]
383
+ labels = tensor[:, 1:]
384
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16, enabled=bf16):
385
+ logits = model(inputs)
386
+ raw_loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))
387
+ loss = raw_loss / accumulation_steps
388
+ if not bool(torch.isfinite(raw_loss)):
389
+ manager.save(
390
+ model, optimizer, scheduler,
391
+ step=step,
392
+ samples_consumed=samples_consumed,
393
+ tokens_seen=tokens_seen,
394
+ last_loss=last_loss,
395
+ reason="non-finite-loss",
396
+ )
397
+ raise FloatingPointError(f"Non-finite pretraining loss at step {step}: {raw_loss.item()}")
398
+ loss.backward()
399
+ accumulated_loss += float(raw_loss.item())
400
+ samples_consumed += batch_size
401
+ tokens_seen += int(labels.numel())
402
+ gradient_norm = float(torch.nn.utils.clip_grad_norm_(model.parameters(), gradient_clip).item())
403
+ if not math.isfinite(gradient_norm):
404
+ manager.save(
405
+ model, optimizer, scheduler,
406
+ step=step,
407
+ samples_consumed=samples_consumed - batch_size * accumulation_steps,
408
+ tokens_seen=tokens_seen - batch_size * sequence_length * accumulation_steps,
409
+ last_loss=last_loss,
410
+ reason="non-finite-gradient",
411
+ )
412
+ raise FloatingPointError(f"Non-finite gradient norm at step {step}: {gradient_norm}")
44
413
  optimizer.step()
45
- last_loss = float(loss.item())
46
- byte_count = decoded_byte_count(tokenizer, labels.detach().cpu().tolist())
47
- last_bpb = bits_per_byte(loss.detach() * labels.numel(), max(byte_count, 1))
414
+ scheduler.step()
415
+ completed_step = step + 1
416
+ last_loss = accumulated_loss / accumulation_steps
417
+ now = time.monotonic()
418
+
419
+ if completed_step % log_steps == 0 or completed_step == steps:
420
+ interval = max(now - last_log_time, 1e-6)
421
+ record = {
422
+ "event": "progress",
423
+ "step": completed_step,
424
+ "total_steps": steps,
425
+ "loss": last_loss,
426
+ "learning_rate": float(scheduler.get_last_lr()[0]),
427
+ "gradient_norm": gradient_norm,
428
+ "tokens_seen": tokens_seen,
429
+ "tokens_per_second": (tokens_seen - last_log_tokens) / interval,
430
+ "elapsed_seconds": now - started,
431
+ "cuda_allocated_bytes": int(torch.cuda.memory_allocated(device)),
432
+ "cuda_reserved_bytes": int(torch.cuda.memory_reserved(device)),
433
+ }
434
+ append_jsonl(metrics_path, record)
435
+ print(json.dumps(record, separators=(",", ":")), flush=True)
436
+ last_log_time = now
437
+ last_log_tokens = tokens_seen
438
+
439
+ checkpoint_due = (
440
+ completed_step % checkpoint_steps == 0
441
+ or now - last_checkpoint_time >= checkpoint_seconds
442
+ or completed_step == steps
443
+ or stop.signal_number is not None
444
+ )
445
+ if checkpoint_due:
446
+ reason = "signal" if stop.signal_number is not None else ("complete" if completed_step == steps else "periodic")
447
+ checkpoint = manager.save(
448
+ model, optimizer, scheduler,
449
+ step=completed_step,
450
+ samples_consumed=samples_consumed,
451
+ tokens_seen=tokens_seen,
452
+ last_loss=last_loss,
453
+ reason=reason,
454
+ )
455
+ print(json.dumps({"event": "checkpoint", "step": completed_step, "path": str(checkpoint), "reason": reason}), flush=True)
456
+ last_checkpoint_time = now
457
+ if stop.signal_number is not None:
458
+ raise SystemExit(128 + stop.signal_number)
48
459
 
49
- output_dir = Path(config["output_dir"])
50
460
  save_model(model, output_dir)
51
461
  write_json(output_dir / "metrics.json", {
52
462
  "ok": True,
53
463
  "steps": steps,
54
464
  "loss": last_loss,
55
- "bpb": last_bpb,
56
465
  "parameters": param_count(model),
57
466
  "depth": depth,
58
467
  "width": model_config["width"],
59
468
  "heads": model_config["heads"],
60
- "corpus_sha256": corpus_hash(corpus),
469
+ "tokens_seen": tokens_seen,
470
+ "training_tokens": int(token_metadata["tokens"]),
471
+ "token_cache_sha256": token_metadata["tokens_sha256"],
472
+ "bf16": bf16,
61
473
  "device": str(device),
62
474
  })
63
475
 
@@ -7,7 +7,7 @@ from tokenizers import Tokenizer
7
7
  from torch.nn import functional as F
8
8
 
9
9
  from common import load_config, require_cuda, write_json
10
- from data import END, encode_ids, example_pairs, format_prompt, numeric_reward
10
+ from data import END, encode_generation_prompt, example_pairs, numeric_reward
11
11
  from model import FoundationGPT, load_model, param_count, save_model
12
12
 
13
13
 
@@ -73,10 +73,18 @@ def main(argv: list[str] | None = None) -> None:
73
73
  last_loss = 0.0
74
74
  for step in range(steps):
75
75
  example = examples[step % len(examples)]
76
- prompt_ids = encode_ids(tokenizer, format_prompt(str(config.get("system_prompt") or ""), example["input"]))
76
+ context_length = int(model.config["sequence_length"])
77
+ prompt_ids, completion_tokens = encode_generation_prompt(
78
+ tokenizer,
79
+ str(config.get("system_prompt") or ""),
80
+ example["input"],
81
+ context_length,
82
+ 24,
83
+ )
77
84
  prompt_ids, completion_tokens = bounded_rollout(
78
85
  prompt_ids,
79
- int(model.config["sequence_length"]),
86
+ context_length,
87
+ completion_tokens,
80
88
  )
81
89
  prompt = torch.tensor([prompt_ids], dtype=torch.long, device=device)
82
90
  sampled = sample_rollout(