train: add train_ounce100m (Phase 3/4 glue)
Browse files- train/train_ounce100m.py +351 -0
train/train_ounce100m.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The training run in one script: frozen architecture, map-style packed data, fp16 on T4, trapezoid LR,
|
| 2 |
+
# and a checkpoint cycle that satisfies §3.13 by construction.
|
| 3 |
+
|
| 4 |
+
# Deliberately thin glue over `transformers.Trainer` (docs/01-plan.md §5): data loading, DDP, gradient
|
| 5 |
+
# accumulation, scaler, RNG capture and resumption are the framework's job. What is ours is the shard
|
| 6 |
+
# reader, the LR shape, and the upload/verify/prune discipline -- the three things a generic stack cannot
|
| 7 |
+
# know about this project.
|
| 8 |
+
|
| 9 |
+
# Two resume facts that are easy to get wrong and are asserted here rather than assumed:
|
| 10 |
+
# 1. `ignore_data_skip=True`. Trainer's own resume would replay discarded batches; we advance the data
|
| 11 |
+
# position by slicing the permutation in shard_dataset instead. Leaving the flag at False would
|
| 12 |
+
# double-skip and silently LOSE training data -- the exact §3.1 violation.
|
| 13 |
+
# 2. The cursor is checked against the checkpoint, not trusted. A resumed run whose cursor, seed,
|
| 14 |
+
# sequence length or corpus fingerprint disagrees with what is on disk stops before step 1.
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import json
|
| 18 |
+
import math
|
| 19 |
+
import os
|
| 20 |
+
import time
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
from torch.optim.lr_scheduler import LambdaLR
|
| 24 |
+
from transformers import (LlamaConfig, LlamaForCausalLM, Trainer, TrainerCallback,
|
| 25 |
+
TrainingArguments, default_data_collator, set_seed)
|
| 26 |
+
|
| 27 |
+
import hubckpt
|
| 28 |
+
import shard_dataset as SD
|
| 29 |
+
|
| 30 |
+
# ---- frozen at Gate 1 (docs/01-plan.md §2.3, §4). Only --layers/--hidden change, for test T1.
|
| 31 |
+
BASE = dict(hidden=576, layers=22, heads=9, kv_heads=3, ffn=1536, vocab=49152,
|
| 32 |
+
rope_theta=10000.0, rms_eps=1e-5, tie=True)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def build_config(seq_len, layers=None, hidden=None):
|
| 36 |
+
h = hidden or BASE["hidden"]
|
| 37 |
+
L = layers or BASE["layers"]
|
| 38 |
+
inter = BASE["ffn"] if hidden is None and layers is None else int(round(8 / 3 * h)) * 2 // 64 * 64
|
| 39 |
+
cfg = LlamaConfig(vocab_size=BASE["vocab"], hidden_size=h, intermediate_size=inter,
|
| 40 |
+
num_hidden_layers=L, num_attention_heads=BASE["heads"],
|
| 41 |
+
num_key_value_heads=BASE["kv_heads"] if hidden is None else max(1, h // 64),
|
| 42 |
+
max_position_embeddings=seq_len, rope_theta=BASE["rope_theta"],
|
| 43 |
+
rms_norm_eps=BASE["rms_eps"], tie_word_embeddings=BASE["tie"],
|
| 44 |
+
attention_dropout=0.0, hidden_act="silu", mlp_bias=False,
|
| 45 |
+
pad_token_id=BASE["vocab"] - 1, eos_token_id=0)
|
| 46 |
+
# GQA ratio is part of the frozen shape; 9 heads with 3 kv heads, never MHA at this budget.
|
| 47 |
+
if hidden is None:
|
| 48 |
+
cfg.num_key_value_heads = BASE["kv_heads"]
|
| 49 |
+
return cfg
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def count_params(model):
|
| 53 |
+
"""Total, embeddings included -- the number §2 budgets against. Reported both ways: numel sum and
|
| 54 |
+
the unique-parameter count, which differ when embeddings are tied."""
|
| 55 |
+
total = sum(p.numel() for p in model.parameters())
|
| 56 |
+
uniq = sum(p.numel() for p in set(model.parameters()))
|
| 57 |
+
return {"sum_numel": total, "unique": uniq,
|
| 58 |
+
"trainable": sum(p.numel() for p in model.parameters() if p.requires_grad)}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# The cursor arithmetic in one place, because three separate bugs live here:
|
| 62 |
+
# - a step consumes micro_batch * world sequences, not micro_batch (DDP shards the batch);
|
| 63 |
+
# - on resume, Trainer restores global_step from the checkpoint but our dataset was ALREADY sliced to
|
| 64 |
+
# start there, so the total is start_sample + step_within_this_run * seqs_per_step;
|
| 65 |
+
# - max_steps is the TOTAL horizon (it is what the LR schedule decays against), so resuming mid-run
|
| 66 |
+
# must keep the same steps_planned or the trapezoid changes shape.
|
| 67 |
+
def samples_at(step_in_run, start_sample, seqs_per_step):
|
| 68 |
+
return start_sample + int(step_in_run) * seqs_per_step
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class TrapezoidTrainer(Trainer):
|
| 72 |
+
"""Trainer with (a) a hand-rolled trapezoid schedule and (b) a data position we own."""
|
| 73 |
+
|
| 74 |
+
def __init__(self, *a, lr_shape=None, **kw):
|
| 75 |
+
self.lr_shape = lr_shape
|
| 76 |
+
super().__init__(*a, **kw)
|
| 77 |
+
|
| 78 |
+
def create_scheduler(self, num_training_steps, optimizer=None):
|
| 79 |
+
warm = self.lr_shape["warmup_steps"]
|
| 80 |
+
decay_at = self.lr_shape["decay_start"]
|
| 81 |
+
total = self.lr_shape["total_steps"]
|
| 82 |
+
|
| 83 |
+
def fn(cur):
|
| 84 |
+
if cur < warm:
|
| 85 |
+
return (cur + 1) / max(1, warm)
|
| 86 |
+
if cur < decay_at:
|
| 87 |
+
return 1.0
|
| 88 |
+
# linear decay to exactly zero at the end: the trapezoid in docs/01-plan.md §4.3
|
| 89 |
+
return max(0.0, 1.0 - (cur - decay_at) / max(1, total - decay_at))
|
| 90 |
+
|
| 91 |
+
if self.lr_scheduler is None:
|
| 92 |
+
self.lr_scheduler = LambdaLR(self.optimizer if optimizer is None else optimizer, fn)
|
| 93 |
+
return self.lr_scheduler
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class HubPush(TrainerCallback):
|
| 97 |
+
"""§3.13 after every checkpoint: write the cursor in, upload, verify from the Hub, prune locally.
|
| 98 |
+
|
| 99 |
+
Only rank 0 acts -- the ranks share the checkpoint directory and would race. A failed verification
|
| 100 |
+
raises inside the callback, so the run stops AT that checkpoint rather than continuing with its only
|
| 101 |
+
copy on a disk that can be taken away at any moment.
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
def __init__(self, repo, api, token, seq_len, seed, fingerprint, start_sample, seqs_per_step,
|
| 105 |
+
prune=True, min_free_gb=6.0):
|
| 106 |
+
self.repo, self.api, self.token = repo, api, token
|
| 107 |
+
self.seq_len, self.seed, self.fingerprint = seq_len, seed, fingerprint
|
| 108 |
+
self.start_sample, self.seqs_per_step = start_sample, seqs_per_step
|
| 109 |
+
self.prune, self.min_free_gb = prune, min_free_gb
|
| 110 |
+
self.n = 0
|
| 111 |
+
self.results = []
|
| 112 |
+
|
| 113 |
+
def on_save(self, args, state, control, **kw):
|
| 114 |
+
if not state.is_world_process_zero:
|
| 115 |
+
return control
|
| 116 |
+
step = state.global_step
|
| 117 |
+
d = os.path.join(args.output_dir, f"checkpoint-{step}")
|
| 118 |
+
if not os.path.isdir(d):
|
| 119 |
+
raise RuntimeError(f"Trainer reported a save but {d} does not exist")
|
| 120 |
+
samples = samples_at(step, self.start_sample, self.seqs_per_step)
|
| 121 |
+
SD.save_cursor(d, SD.cursor_dict(self.seq_len, samples, self.seed, self.fingerprint))
|
| 122 |
+
t0 = time.time()
|
| 123 |
+
res = hubckpt.push_and_prune(self.repo, d, f"ckpt/checkpoint-{step}", self.api,
|
| 124 |
+
token=self.token, prune=self.prune,
|
| 125 |
+
min_free_gb=self.min_free_gb)
|
| 126 |
+
hubckpt.write_latest_pointer(self.repo, {
|
| 127 |
+
"path_in_repo": f"ckpt/checkpoint-{step}", "step": step, "samples_consumed": samples,
|
| 128 |
+
"tokens_consumed": samples * self.seq_len, "fingerprint": self.fingerprint,
|
| 129 |
+
"seed": self.seed}, self.api, token=self.token)
|
| 130 |
+
loss = next((h["loss"] for h in reversed(state.log_history) if "loss" in h), None)
|
| 131 |
+
print(f"CKPT {step} samples={samples:,} push+verify={round(time.time()-t0,1)}s "
|
| 132 |
+
f"ok={res['verify']['ok']} pruned={res.get('pruned')} free={res['free_after_gb']}GB "
|
| 133 |
+
f"loss={loss}", flush=True)
|
| 134 |
+
self.n += 1
|
| 135 |
+
self.results.append(res)
|
| 136 |
+
return control
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def main():
|
| 140 |
+
ap = argparse.ArgumentParser()
|
| 141 |
+
ap.add_argument("--root", default="/kaggle/working/mixroot")
|
| 142 |
+
ap.add_argument("--out", default="/kaggle/working/run")
|
| 143 |
+
ap.add_argument("--hub-repo", default="", help="dataset repo for checkpoints; empty = no upload")
|
| 144 |
+
ap.add_argument("--hub-repo-type", default="dataset", choices=["dataset", "model"])
|
| 145 |
+
ap.add_argument("--seq-len", type=int, default=2048)
|
| 146 |
+
ap.add_argument("--layers", type=int, default=0)
|
| 147 |
+
ap.add_argument("--hidden", type=int, default=0)
|
| 148 |
+
ap.add_argument("--micro-batch", type=int, default=2)
|
| 149 |
+
ap.add_argument("--accum", type=int, default=32)
|
| 150 |
+
ap.add_argument("--lr", type=float, default=6e-4)
|
| 151 |
+
ap.add_argument("--tokens", type=int, default=1_000_000_000)
|
| 152 |
+
ap.add_argument("--max-steps", type=int, default=0, help="0 = derive from --tokens; >0 overrides")
|
| 153 |
+
ap.add_argument("--warmup-frac", type=float, default=0.02)
|
| 154 |
+
ap.add_argument("--decay-frac", type=float, default=0.80)
|
| 155 |
+
ap.add_argument("--seed", type=int, default=20260919)
|
| 156 |
+
ap.add_argument("--data-seed", type=int, default=20260919)
|
| 157 |
+
ap.add_argument("--resume", default="", help="checkpoint dir; empty = auto from Hub latest")
|
| 158 |
+
ap.add_argument("--val-tokens", type=int, default=2_000_000)
|
| 159 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 160 |
+
ap.add_argument("--push-every-steps", type=int, default=0, help="0 = only at 10% boundaries")
|
| 161 |
+
ap.add_argument("--prune", action="store_true", help="delete local checkpoint after hub verify")
|
| 162 |
+
ap.add_argument("--dry-run", action="store_true", help="build everything, train 0 steps")
|
| 163 |
+
args = ap.parse_args()
|
| 164 |
+
|
| 165 |
+
rank = int(os.environ.get("RANK", "0"))
|
| 166 |
+
world = int(os.environ.get("WORLD_SIZE", "1"))
|
| 167 |
+
say = (lambda *m: print(*m, flush=True)) if rank == 0 else (lambda *m: None)
|
| 168 |
+
|
| 169 |
+
man_path = os.path.join(args.root, "manifest.json")
|
| 170 |
+
if not os.path.exists(man_path):
|
| 171 |
+
raise SystemExit(f"no manifest at {man_path}: build the mix first (Phase 2 is not optional)")
|
| 172 |
+
man = json.load(open(man_path))
|
| 173 |
+
if man["total_tokens"] < args.tokens:
|
| 174 |
+
say(f"WARNING mix has {man['total_tokens']:,} tokens < target {args.tokens:,}")
|
| 175 |
+
|
| 176 |
+
store = SD.PackedTokenStore(args.root, man)
|
| 177 |
+
fingerprint = SD.mix_fingerprint(args.root, man)
|
| 178 |
+
tokens_per_step = args.micro_batch * args.seq_len * args.accum * world
|
| 179 |
+
steps_planned = args.max_steps or int(args.tokens / tokens_per_step)
|
| 180 |
+
say(f"mix: {store.total_tokens:,} tokens in {len(store.shards)} shards fp={fingerprint} "
|
| 181 |
+
f"| {tokens_per_step:,} tok/step x {steps_planned} steps = "
|
| 182 |
+
f"{tokens_per_step * steps_planned:,} tokens")
|
| 183 |
+
|
| 184 |
+
# ---- resume point: the cursor is the run's identity as much as the weights are.
|
| 185 |
+
# `--resume auto` is what a fresh instance with an empty disk passes: it asks the Hub where the run
|
| 186 |
+
# stopped and pulls that checkpoint down. §3.1 forbids recovering from anything left on an instance.
|
| 187 |
+
if args.resume == "auto":
|
| 188 |
+
if not args.hub_repo:
|
| 189 |
+
raise SystemExit("--resume auto needs --hub-repo")
|
| 190 |
+
import ounce100m_credentials
|
| 191 |
+
ounce100m_credentials.install()
|
| 192 |
+
from huggingface_hub import HfApi
|
| 193 |
+
ptr = hubckpt.latest_pointer(args.hub_repo, repo_type=args.hub_repo_type)
|
| 194 |
+
if "error" in ptr:
|
| 195 |
+
say(f"no latest pointer on {args.hub_repo} ({ptr['error']}) -> starting from step 0")
|
| 196 |
+
args.resume = ""
|
| 197 |
+
else:
|
| 198 |
+
dest = os.path.join(args.out, os.path.basename(ptr["path_in_repo"]))
|
| 199 |
+
say(f"auto-resume: hub says step {ptr.get('step')} at {ptr['path_in_repo']}")
|
| 200 |
+
got = hubckpt.download_checkpoint(args.hub_repo, ptr["path_in_repo"], dest,
|
| 201 |
+
HfApi(), repo_type=args.hub_repo_type)
|
| 202 |
+
say(f"downloaded {got['files']} files, {got['bytes']/1e6:.0f} MB in {got['seconds']}s")
|
| 203 |
+
if got["bytes"] < 1_000_000:
|
| 204 |
+
raise SystemExit("auto-resume downloaded a checkpoint that is not real weights")
|
| 205 |
+
args.resume = dest
|
| 206 |
+
cur = SD.load_cursor(args.resume) if args.resume else None
|
| 207 |
+
if args.resume:
|
| 208 |
+
say(f"resume from {args.resume} cursor={cur}")
|
| 209 |
+
start_sample = (cur or {}).get("samples_consumed", 0)
|
| 210 |
+
if cur:
|
| 211 |
+
for k, v in (("seq_len", args.seq_len), ("shuffle_seed", args.data_seed),
|
| 212 |
+
("dataset_files_sha", fingerprint)):
|
| 213 |
+
if cur.get(k) != v:
|
| 214 |
+
raise SystemExit(f"resume refused: cursor {k}={cur.get(k)} != current {v}. "
|
| 215 |
+
"A resumed run must read the same data in the same order (§3.1).")
|
| 216 |
+
|
| 217 |
+
train_ds = SD.make_dataset(store, args.seq_len, args.data_seed, start_sample=start_sample)
|
| 218 |
+
# Validation comes from the mix's held-out val/ shards, never from the training stream: scoring on
|
| 219 |
+
# tokens the model already consumed would be a number nobody could reproduce (and §3.12).
|
| 220 |
+
vs = [s["file"] for s in man.get("val_shards", [])]
|
| 221 |
+
if not vs:
|
| 222 |
+
raise SystemExit("manifest lists no val_shards -- Gate 2 requires a held-out split")
|
| 223 |
+
vstore = SD.PackedTokenStore(args.root, man, files=vs)
|
| 224 |
+
n_val = min(max(1, args.val_tokens // args.seq_len), vstore.total_tokens // args.seq_len)
|
| 225 |
+
val_ds = SD.make_dataset(vstore, args.seq_len, args.data_seed, shuffle=False, count=n_val)
|
| 226 |
+
|
| 227 |
+
cfg = build_config(args.seq_len, args.layers or None, args.hidden or None)
|
| 228 |
+
set_seed(args.seed) # before init: both ranks must start from identical weights, not two draws
|
| 229 |
+
model = LlamaForCausalLM(cfg).to(dtype=torch.float32)
|
| 230 |
+
pc = count_params(model)
|
| 231 |
+
say("params:", json.dumps(pc))
|
| 232 |
+
if not (90_000_000 <= pc["sum_numel"] <= 110_000_000) and not args.layers and not args.hidden:
|
| 233 |
+
raise SystemExit(f"parameter count {pc['sum_numel']:,} is outside §2's 90-110M budget")
|
| 234 |
+
|
| 235 |
+
seqs_per_step = args.micro_batch * world * args.accum
|
| 236 |
+
if seqs_per_step * args.seq_len != tokens_per_step:
|
| 237 |
+
raise SystemExit("internal: batch arithmetic disagrees, token/step accounting is unreliable")
|
| 238 |
+
ckpt_every = args.push_every_steps or max(1, steps_planned // 10)
|
| 239 |
+
|
| 240 |
+
api = None
|
| 241 |
+
if args.hub_repo:
|
| 242 |
+
import ounce100m_credentials
|
| 243 |
+
ounce100m_credentials.install()
|
| 244 |
+
from huggingface_hub import HfApi
|
| 245 |
+
api = HfApi()
|
| 246 |
+
say(f"checkpoint hub target {args.hub_repo} (prune={args.prune}, every {ckpt_every} steps)")
|
| 247 |
+
|
| 248 |
+
tacc = TrainingArguments(
|
| 249 |
+
output_dir=args.out,
|
| 250 |
+
per_device_train_batch_size=args.micro_batch,
|
| 251 |
+
gradient_accumulation_steps=args.accum,
|
| 252 |
+
learning_rate=args.lr,
|
| 253 |
+
weight_decay=0.1,
|
| 254 |
+
adam_beta1=0.9, adam_beta2=0.95, adam_epsilon=1e-8,
|
| 255 |
+
max_grad_norm=1.0,
|
| 256 |
+
lr_scheduler_type="constant", # replaced by TrapezoidTrainer.create_scheduler
|
| 257 |
+
warmup_ratio=0.0, # the trapezoid owns warmup
|
| 258 |
+
num_train_epochs=1,
|
| 259 |
+
max_steps=0 if args.dry_run else steps_planned,
|
| 260 |
+
fp16=True, bf16=False,
|
| 261 |
+
gradient_checkpointing=False, # measure before needing it; docs/01-plan.md §6
|
| 262 |
+
ddp_find_unused_parameters=False,
|
| 263 |
+
dataloader_num_workers=2,
|
| 264 |
+
dataloader_pin_memory=False,
|
| 265 |
+
remove_unused_columns=False, # our dict keys must reach the model untouched
|
| 266 |
+
ignore_data_skip=True, # see header note 1 -- we advance position ourselves
|
| 267 |
+
save_strategy="no" if args.dry_run else "steps",
|
| 268 |
+
save_steps=ckpt_every,
|
| 269 |
+
save_total_limit=1, # §3.13: the Hub is the store, the instance is scratch
|
| 270 |
+
logging_steps=args.log_every, report_to=[],
|
| 271 |
+
seed=args.seed, data_seed=args.seed,
|
| 272 |
+
accelerator_config={"dispatch_batches": False},
|
| 273 |
+
average_tokens_across_devices=False,
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
tr = TrapezoidTrainer(
|
| 277 |
+
model=model, args=tacc, train_dataset=train_ds, eval_dataset=val_ds,
|
| 278 |
+
data_collator=default_data_collator, lr_shape={
|
| 279 |
+
"warmup_steps": max(100, int(steps_planned * args.warmup_frac)),
|
| 280 |
+
"decay_start": int(steps_planned * args.decay_frac),
|
| 281 |
+
"total_steps": steps_planned},
|
| 282 |
+
)
|
| 283 |
+
if api is not None:
|
| 284 |
+
tr.add_callback(HubPush(args.hub_repo, api, os.environ.get("HF_TOKEN"), args.seq_len,
|
| 285 |
+
args.data_seed, fingerprint, start_sample, seqs_per_step,
|
| 286 |
+
prune=args.prune))
|
| 287 |
+
|
| 288 |
+
# Precision must be ASSERTED, not configured: bf16 "works" silently on Turing and fp32 master
|
| 289 |
+
# weights can be lost in a refactor (docs/00-platform-notes.md §8, memory/ERRORS.md E-006). The
|
| 290 |
+
# scaler's home moved between transformers versions, so look in both places instead of trusting one
|
| 291 |
+
# attribute path (E-010: prefer version-free surfaces).
|
| 292 |
+
scaler = getattr(tr, "scaler", None) or getattr(getattr(tr, "accelerator", None), "scaler", None)
|
| 293 |
+
say(f"precision: fp16={tacc.fp16} bf16={tacc.bf16} scaler={type(scaler).__name__} "
|
| 294 |
+
f"master_dtype={next(model.parameters()).dtype}")
|
| 295 |
+
assert tacc.fp16 and not tacc.bf16, "not the frozen T4 fp16 recipe"
|
| 296 |
+
assert next(model.parameters()).dtype == torch.float32, "master weights are not fp32"
|
| 297 |
+
|
| 298 |
+
prior = None
|
| 299 |
+
if args.resume:
|
| 300 |
+
tsp = os.path.join(args.resume, "trainer_state.json")
|
| 301 |
+
if os.path.exists(tsp):
|
| 302 |
+
h0 = [h for h in json.load(open(tsp)).get("log_history", []) if "loss" in h]
|
| 303 |
+
prior = h0[-1]["loss"] if h0 else None
|
| 304 |
+
say(f"prior loss at resume: {prior} -- loss continuity is judged against this number")
|
| 305 |
+
|
| 306 |
+
if args.dry_run:
|
| 307 |
+
say(json.dumps({"dry_run": True, "train_samples": len(train_ds), "val_samples": len(val_ds),
|
| 308 |
+
"steps_planned": steps_planned, "ckpt_every": ckpt_every,
|
| 309 |
+
"params": pc, "fingerprint": fingerprint, "scaler": type(scaler).__name__}))
|
| 310 |
+
return
|
| 311 |
+
|
| 312 |
+
t0 = time.time()
|
| 313 |
+
tr.train(resume_from_checkpoint=args.resume or None)
|
| 314 |
+
|
| 315 |
+
elapsed = time.time() - t0
|
| 316 |
+
step = tr.state.global_step
|
| 317 |
+
samples = samples_at(step, start_sample, seqs_per_step)
|
| 318 |
+
consumed = samples * args.seq_len
|
| 319 |
+
hist = [h for h in tr.state.log_history if "loss" in h]
|
| 320 |
+
final_loss = hist[-1]["loss"] if hist else None
|
| 321 |
+
say(f"TRAIN DONE step={step}/{steps_planned} elapsed={elapsed/60:.1f} min "
|
| 322 |
+
f"tokens_consumed={consumed:,} last_loss={final_loss}")
|
| 323 |
+
# E-006, again: a run that produced NaN still reports a confident tokens/sec. Never let that pass.
|
| 324 |
+
if final_loss is None or not math.isfinite(final_loss):
|
| 325 |
+
raise SystemExit(f"final loss is not finite ({final_loss}) -- not exporting anything")
|
| 326 |
+
|
| 327 |
+
out = os.path.join(args.out, "final")
|
| 328 |
+
tr.save_model(out)
|
| 329 |
+
SD.save_cursor(out, SD.cursor_dict(args.seq_len, samples, args.data_seed, fingerprint))
|
| 330 |
+
json.dump({"step": step, "tokens_consumed": consumed, "seconds": round(elapsed, 1),
|
| 331 |
+
"params": pc, "fingerprint": fingerprint, "final_loss": final_loss,
|
| 332 |
+
"prior_loss": prior, "samples_consumed": samples, "steps_planned": steps_planned},
|
| 333 |
+
open(os.path.join(out, "run_summary.json"), "w"), indent=1, sort_keys=True)
|
| 334 |
+
ppl = None
|
| 335 |
+
try:
|
| 336 |
+
ev = tr.evaluate()
|
| 337 |
+
ppl = math.exp(min(20.0, ev["eval_loss"]))
|
| 338 |
+
say(f"validation loss={ev['eval_loss']:.4f} ppl={ppl:.2f} over {n_val} held-out windows")
|
| 339 |
+
except Exception as e:
|
| 340 |
+
say(f"validation eval failed: {type(e).__name__}: {str(e)[:200]}")
|
| 341 |
+
if api is not None:
|
| 342 |
+
r = hubckpt.push_and_prune(args.hub_repo, out, "final", api,
|
| 343 |
+
token=os.environ.get("HF_TOKEN"), prune=args.prune)
|
| 344 |
+
say("final push:", json.dumps({k: r[k] for k in ("verify", "pruned", "free_after_gb")})[:900])
|
| 345 |
+
say("RUN_JSON " + json.dumps({"final_step": step, "val_ppl": ppl, "tokens_consumed": consumed,
|
| 346 |
+
"tok_per_s": round(consumed / max(1, elapsed)),
|
| 347 |
+
"final_loss": final_loss, "prior_loss": prior}, sort_keys=True))
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
if __name__ == "__main__":
|
| 351 |
+
main()
|