musabc commited on
Commit
f0cf914
·
verified ·
1 Parent(s): 5120744

Upload 05_train_v5_tpu.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 05_train_v5_tpu.py +533 -0
05_train_v5_tpu.py ADDED
@@ -0,0 +1,533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ V5 Egitim — TPU v5e-8 (Kaggle veya GCP) versiyonu.
3
+
4
+ Differences from 05_train_v5.py (CUDA):
5
+ - torch_xla device (xla_device)
6
+ - Native torch.optim.Muon (PyTorch 2.11+)
7
+ - xmp.spawn ile multi-chip data parallelism
8
+ - No Liger (CUDA-only, skip)
9
+ - No torch.compile (XLA already optimizes)
10
+ - bf16 native (TPU strength)
11
+ - Periodic HF push (Kaggle 9h session limit icin)
12
+
13
+ Kullanim:
14
+ # Kaggle TPU v5e-8 notebook'ta:
15
+ python 05_train_v5_tpu.py --resume --hf-user musabc \
16
+ --hf-push-every 500 --max-time 510 # 8.5 saat (Kaggle 9h limit)
17
+
18
+ NOT:
19
+ - Single-host v5e-8: 8 chip, xmp.spawn ile 8 process
20
+ - Checkpoint format CUDA ile uyumlu — resume calisir
21
+ - Muon state da otomatik portluyor (state_dict same)
22
+ """
23
+
24
+ import argparse
25
+ import math
26
+ import os
27
+ import signal
28
+ import sys
29
+ import time
30
+ from contextlib import nullcontext
31
+ from pathlib import Path
32
+
33
+ import numpy as np
34
+ import torch
35
+ import torch.nn.functional as F
36
+
37
+ # TPU imports
38
+ import torch_xla.core.xla_model as xm
39
+ import torch_xla.distributed.parallel_loader as pl
40
+ import torch_xla.distributed.xla_multiprocessing as xmp
41
+ import torch_xla.runtime as xr
42
+
43
+ # Liger'i kapat (CUDA-only)
44
+ os.environ["NANOGPT_NO_LIGER"] = "1"
45
+
46
+ # Model import (NANOGPT_NO_LIGER sonrasi)
47
+ from model_v5 import GPTV5, GPTConfigV5
48
+
49
+ # Native Muon (PyTorch 2.11+)
50
+ try:
51
+ from torch.optim import Muon as TorchMuon
52
+ HAS_NATIVE_MUON = True
53
+ except ImportError:
54
+ HAS_NATIVE_MUON = False
55
+ print("! torch.optim.Muon yok. PyTorch 2.11+ gerek")
56
+ print(" Fallback: muon.py kullanilacak (yavas olabilir XLA'da)")
57
+ from muon import Muon as TorchMuon
58
+
59
+
60
+ # ============================================================
61
+ # Konfigurasyon — TPU v5e-8 (8 chip, 16GB HBM/chip)
62
+ # ============================================================
63
+ DATA_DIR = Path(__file__).parent / "data"
64
+ OUT_DIR = Path(__file__).parent / "runs" / "tr-200m-v5"
65
+
66
+ MODEL_CONFIG = dict(
67
+ block_size=2048,
68
+ vocab_size=32000,
69
+ n_layer=18,
70
+ n_head=14,
71
+ n_embd=896,
72
+ dropout=0.0,
73
+ rope_theta=10000.0,
74
+ logit_softcap=30.0,
75
+ )
76
+
77
+ # Per-chip batch — 8 chip ile global etkin batch 520'ye yakin
78
+ # 16GB per chip: bs=8, T=2048, activations ~10GB rahatca sigar
79
+ BATCH_SIZE_PER_CHIP = 8
80
+ GRAD_ACCUM_STEPS = 8 # 8 chip × bs 8 × accum 8 = 512 etkin batch
81
+ MAX_STEPS = 20_000
82
+ LOG_INTERVAL = 10
83
+ EVAL_INTERVAL = 400
84
+ EVAL_ITERS = 60
85
+ SAVE_INTERVAL = 500 # daha sik kayit (Kaggle session limiti icin)
86
+ HF_PUSH_INTERVAL = 500 # her 500 step HF'e push
87
+ SAMPLE_INTERVAL = 2000
88
+
89
+ MUON_LR = 0.022
90
+ ADAM_LR = 3.5e-4
91
+ MIN_LR_RATIO = 0.1
92
+ WARMUP_STEPS = 1000
93
+ LR_DECAY_STEPS = 20_000
94
+
95
+ WEIGHT_DECAY = 0.1
96
+ ADAM_BETA1 = 0.9
97
+ ADAM_BETA2 = 0.95
98
+ MUON_MOMENTUM = 0.95
99
+ GRAD_CLIP = 1.0
100
+
101
+ PHASE1_END = 0.55
102
+ PHASE2_END = 0.85
103
+ PHASE_MIX = {
104
+ 1: (0.25, 0.65, 0.10),
105
+ 2: (0.15, 0.55, 0.30),
106
+ 3: (0.05, 0.25, 0.70),
107
+ }
108
+ # ============================================================
109
+
110
+ LATEST_CKPT = OUT_DIR / "latest_ckpt.pt"
111
+ BEST_CKPT = OUT_DIR / "best_ckpt.pt"
112
+ LOG_FILE = OUT_DIR / "train_tpu.log"
113
+
114
+
115
+ def get_lr_factor(step):
116
+ if step < WARMUP_STEPS:
117
+ return (step + 1) / (WARMUP_STEPS + 1)
118
+ if step > LR_DECAY_STEPS:
119
+ return MIN_LR_RATIO
120
+ decay_ratio = (step - WARMUP_STEPS) / (LR_DECAY_STEPS - WARMUP_STEPS)
121
+ coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))
122
+ return MIN_LR_RATIO + coeff * (1.0 - MIN_LR_RATIO)
123
+
124
+
125
+ def get_phase(step):
126
+ p = step / max(MAX_STEPS, 1)
127
+ if p < PHASE1_END:
128
+ return 1
129
+ if p < PHASE2_END:
130
+ return 2
131
+ return 3
132
+
133
+
134
+ def log_rank0(msg, rank=0):
135
+ if rank != 0:
136
+ return
137
+ print(msg, flush=True)
138
+ try:
139
+ with open(LOG_FILE, "a", encoding="utf-8") as f:
140
+ f.write(msg + "\n")
141
+ except Exception:
142
+ pass
143
+
144
+
145
+ # =====================================================================
146
+ # Data
147
+ # =====================================================================
148
+ class StageLoader:
149
+ def __init__(self, bin_path, block_size, batch_size, rank=0, world_size=1):
150
+ self.data = np.memmap(bin_path, dtype=np.uint16, mode="r")
151
+ self.block_size = block_size
152
+ self.batch_size = batch_size
153
+ self.rank = rank
154
+ self.world_size = world_size
155
+ self.n_tokens = len(self.data)
156
+ # Her rank farkli sample alsin diye seed offset
157
+ self.rng = np.random.default_rng(seed=42 + rank)
158
+ if rank == 0:
159
+ print(f" {bin_path.name}: {self.n_tokens:,} token "
160
+ f"(~{self.n_tokens*2/1e9:.2f} GB)")
161
+
162
+ def get_batch(self, device):
163
+ bs, T = self.batch_size, self.block_size
164
+ ix = self.rng.integers(0, self.n_tokens - T - 1, size=bs)
165
+ x_np = np.empty((bs, T), dtype=np.int64)
166
+ y_np = np.empty((bs, T), dtype=np.int64)
167
+ for k, i in enumerate(ix):
168
+ x_np[k] = self.data[i:i+T]
169
+ y_np[k] = self.data[i+1:i+1+T]
170
+ x = torch.from_numpy(x_np).to(device)
171
+ y = torch.from_numpy(y_np).to(device)
172
+ return x, y
173
+
174
+
175
+ class MultiStageLoader:
176
+ def __init__(self, stage_loaders, rng=None):
177
+ self.loaders = stage_loaders
178
+ self.rng = rng or np.random.default_rng()
179
+
180
+ def get_batch(self, phase, device):
181
+ mix = PHASE_MIX[phase]
182
+ idx = self.rng.choice(len(self.loaders), p=mix)
183
+ x, y = self.loaders[idx].get_batch(device)
184
+ return (x, y), idx
185
+
186
+
187
+ # =====================================================================
188
+ # Eval
189
+ # =====================================================================
190
+ @torch.no_grad()
191
+ def estimate_loss(model, val_loader, train_loaders, device, eval_iters):
192
+ out = {}
193
+ model.eval()
194
+ # Val
195
+ losses = []
196
+ for _ in range(eval_iters):
197
+ x, y = val_loader.get_batch(device)
198
+ _, loss = model(x, y)
199
+ losses.append(loss.item())
200
+ out["val"] = sum(losses) / len(losses)
201
+ # Per-stage (kucuk sample)
202
+ n_small = max(eval_iters // 4, 8)
203
+ for i, ld in enumerate(train_loaders, start=1):
204
+ losses = []
205
+ for _ in range(n_small):
206
+ x, y = ld.get_batch(device)
207
+ _, loss = model(x, y)
208
+ losses.append(loss.item())
209
+ out[f"stage{i}"] = sum(losses) / len(losses)
210
+ model.train()
211
+ return out
212
+
213
+
214
+ # =====================================================================
215
+ # Checkpointing
216
+ # =====================================================================
217
+ def atomic_save(state, path):
218
+ tmp = path.with_suffix(path.suffix + ".tmp")
219
+ # CPU'ya kopyala (XLA tensor'lari)
220
+ cpu_state = {}
221
+ for k, v in state.items():
222
+ if isinstance(v, dict):
223
+ cpu_state[k] = {kk: vv.cpu() if torch.is_tensor(vv) else vv
224
+ for kk, vv in v.items()}
225
+ elif torch.is_tensor(v):
226
+ cpu_state[k] = v.cpu()
227
+ else:
228
+ cpu_state[k] = v
229
+ torch.save(cpu_state, tmp)
230
+ if path.exists():
231
+ path.unlink()
232
+ tmp.rename(path)
233
+
234
+
235
+ def build_state(model, opt_muon, opt_adam, step, best_val):
236
+ return {
237
+ "model": model.state_dict(),
238
+ "opt_muon": opt_muon.state_dict(),
239
+ "opt_adam": opt_adam.state_dict(),
240
+ "step": step,
241
+ "best_val": best_val,
242
+ "config": MODEL_CONFIG,
243
+ "version": "v5",
244
+ }
245
+
246
+
247
+ def push_to_hf(ckpt_path, hf_user, hf_token, repo_suffix="ckpts"):
248
+ """Kaggle session bitiminde HF'e push."""
249
+ try:
250
+ from huggingface_hub import HfApi
251
+ api = HfApi(token=hf_token)
252
+ repo_id = f"{hf_user}/nanogpt-tr-v5-{repo_suffix}"
253
+ api.upload_file(
254
+ path_or_fileobj=str(ckpt_path),
255
+ path_in_repo=ckpt_path.name,
256
+ repo_id=repo_id,
257
+ repo_type="model",
258
+ commit_message=f"upload {ckpt_path.name}",
259
+ )
260
+ return True
261
+ except Exception as e:
262
+ print(f" ! HF push hatasi: {e}")
263
+ return False
264
+
265
+
266
+ # =====================================================================
267
+ # Optimizer
268
+ # =====================================================================
269
+ def create_optimizers(model, rank=0):
270
+ muon_params = []
271
+ adam_params = []
272
+ for name, p in model.named_parameters():
273
+ if not p.requires_grad:
274
+ continue
275
+ if p.ndim < 2:
276
+ adam_params.append(p)
277
+ elif "wte" in name or "lm_head" in name:
278
+ adam_params.append(p)
279
+ else:
280
+ muon_params.append(p)
281
+
282
+ seen = set()
283
+ adam_params_unique = []
284
+ for p in adam_params:
285
+ if id(p) not in seen:
286
+ seen.add(id(p))
287
+ adam_params_unique.append(p)
288
+
289
+ if rank == 0:
290
+ n_muon = sum(p.numel() for p in muon_params)
291
+ n_adam = sum(p.numel() for p in adam_params_unique)
292
+ log_rank0(f" Muon params: {n_muon/1e6:.2f}M ({len(muon_params)} tensor)")
293
+ log_rank0(f" AdamW params: {n_adam/1e6:.2f}M ({len(adam_params_unique)} tensor)")
294
+ log_rank0(f" Muon backend: {'NATIVE torch.optim.Muon' if HAS_NATIVE_MUON else 'custom muon.py'}")
295
+
296
+ # Native Muon (PyTorch 2.11+)
297
+ opt_muon = TorchMuon(
298
+ muon_params,
299
+ lr=MUON_LR,
300
+ momentum=MUON_MOMENTUM,
301
+ nesterov=True,
302
+ ns_steps=5,
303
+ )
304
+ opt_adam = torch.optim.AdamW(
305
+ adam_params_unique,
306
+ lr=ADAM_LR,
307
+ betas=(ADAM_BETA1, ADAM_BETA2),
308
+ weight_decay=WEIGHT_DECAY,
309
+ )
310
+ return opt_muon, opt_adam
311
+
312
+
313
+ # =====================================================================
314
+ # Main training (per-process, multi-chip)
315
+ # =====================================================================
316
+ def train_fn(rank, args):
317
+ device = xm.xla_device()
318
+ world_size = xr.world_size()
319
+
320
+ if rank == 0:
321
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
322
+ log_rank0(f"TPU world size: {world_size}")
323
+ log_rank0(f"Device: {device}")
324
+ log_rank0(f"Per-chip batch: {BATCH_SIZE_PER_CHIP}")
325
+ log_rank0(f"Global effective batch: "
326
+ f"{BATCH_SIZE_PER_CHIP * world_size * GRAD_ACCUM_STEPS}")
327
+
328
+ # Data
329
+ if rank == 0:
330
+ log_rank0("\nData yukleniyor...")
331
+ stage1 = StageLoader(DATA_DIR / "v5_stage1.bin",
332
+ MODEL_CONFIG["block_size"], BATCH_SIZE_PER_CHIP,
333
+ rank=rank, world_size=world_size)
334
+ stage2 = StageLoader(DATA_DIR / "v5_stage2.bin",
335
+ MODEL_CONFIG["block_size"], BATCH_SIZE_PER_CHIP,
336
+ rank=rank, world_size=world_size)
337
+ stage3 = StageLoader(DATA_DIR / "v5_stage3.bin",
338
+ MODEL_CONFIG["block_size"], BATCH_SIZE_PER_CHIP,
339
+ rank=rank, world_size=world_size)
340
+ val_loader = StageLoader(DATA_DIR / "v5_val.bin",
341
+ MODEL_CONFIG["block_size"], BATCH_SIZE_PER_CHIP,
342
+ rank=rank, world_size=world_size)
343
+ multi = MultiStageLoader([stage1, stage2, stage3])
344
+
345
+ # Model
346
+ if rank == 0:
347
+ log_rank0("\nModel V5 olusturuluyor...")
348
+ cfg = GPTConfigV5(**MODEL_CONFIG)
349
+ model = GPTV5(cfg).to(device)
350
+ if rank == 0:
351
+ n_params = model.num_params()
352
+ log_rank0(f" Toplam: {n_params/1e6:.2f}M param")
353
+
354
+ opt_muon, opt_adam = create_optimizers(model, rank=rank)
355
+
356
+ # Resume
357
+ start_step = 0
358
+ best_val = float("inf")
359
+ resume_path = LATEST_CKPT if args.resume and LATEST_CKPT.exists() else None
360
+ if resume_path:
361
+ if rank == 0:
362
+ log_rank0(f"\nResume: {resume_path}")
363
+ ckpt = torch.load(resume_path, map_location="cpu", weights_only=False)
364
+ # State dict yukle, sonra device'a tasi
365
+ model.load_state_dict(ckpt["model"])
366
+ # XLA tensor'a tasinmis oldu (model.to(device) ile)
367
+ try:
368
+ opt_muon.load_state_dict(ckpt["opt_muon"])
369
+ opt_adam.load_state_dict(ckpt["opt_adam"])
370
+ except Exception as e:
371
+ if rank == 0:
372
+ log_rank0(f" ! Optimizer state yuklenemedi: {e}")
373
+ log_rank0(f" Sifirdan optimizer state ile devam edilecek")
374
+ start_step = ckpt["step"] + 1
375
+ best_val = ckpt.get("best_val", float("inf"))
376
+ if rank == 0:
377
+ log_rank0(f" step={start_step}, best_val={best_val:.4f}")
378
+
379
+ # Sinyal
380
+ interrupt_flag = {"stop": False}
381
+ def signal_handler(sig, frame):
382
+ interrupt_flag["stop"] = True
383
+ if rank == 0:
384
+ log_rank0("\n[!] Ctrl+C, kaydedilip cikiliyor.")
385
+ signal.signal(signal.SIGINT, signal_handler)
386
+
387
+ # Egitim
388
+ if rank == 0:
389
+ token_per_step = BATCH_SIZE_PER_CHIP * world_size * GRAD_ACCUM_STEPS * MODEL_CONFIG["block_size"]
390
+ log_rank0(f"\nEgitim basliyor:")
391
+ log_rank0(f" Step araligi: {start_step} -> {MAX_STEPS}")
392
+ log_rank0(f" Token/step: {token_per_step:,}")
393
+
394
+ t_start = time.time()
395
+ step_t0 = time.time()
396
+ step = start_step
397
+ last_phase = -1
398
+ stage_hits = [0, 0, 0]
399
+ last_hf_push = 0
400
+
401
+ try:
402
+ while step < MAX_STEPS:
403
+ phase = get_phase(step)
404
+ if phase != last_phase and rank == 0:
405
+ mix = PHASE_MIX[phase]
406
+ log_rank0(f"\n>>> FAZ {phase} basliyor (step {step}): "
407
+ f"s1={mix[0]:.0%}, s2={mix[1]:.0%}, s3={mix[2]:.0%}")
408
+ last_phase = phase
409
+
410
+ # LR
411
+ lr_factor = get_lr_factor(step)
412
+ muon_lr = MUON_LR * lr_factor
413
+ adam_lr = ADAM_LR * lr_factor
414
+ for pg in opt_muon.param_groups:
415
+ pg["lr"] = muon_lr
416
+ for pg in opt_adam.param_groups:
417
+ pg["lr"] = adam_lr
418
+
419
+ # Gradient accumulation
420
+ opt_muon.zero_grad(set_to_none=True)
421
+ opt_adam.zero_grad(set_to_none=True)
422
+ loss_accum = 0.0
423
+ for _ in range(GRAD_ACCUM_STEPS):
424
+ (x, y), stage_idx = multi.get_batch(phase, device)
425
+ stage_hits[stage_idx] += 1
426
+ with torch.autocast(device_type="xla", dtype=torch.bfloat16):
427
+ _, loss = model(x, y)
428
+ loss = loss / GRAD_ACCUM_STEPS
429
+ loss.backward()
430
+ loss_accum += loss.item()
431
+
432
+ # Grad clip + step (xm.optimizer_step ile cross-replica sync)
433
+ torch.nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP)
434
+ xm.optimizer_step(opt_muon)
435
+ xm.optimizer_step(opt_adam)
436
+
437
+ # XLA flush — kritik!
438
+ xm.mark_step()
439
+
440
+ # Log
441
+ if step % LOG_INTERVAL == 0 and rank == 0:
442
+ dt = time.time() - step_t0
443
+ tps = (LOG_INTERVAL * BATCH_SIZE_PER_CHIP * world_size *
444
+ GRAD_ACCUM_STEPS * MODEL_CONFIG["block_size"]) / dt \
445
+ if step > start_step else 0
446
+ step_t0 = time.time()
447
+ elapsed_min = (time.time() - t_start) / 60
448
+ total_hits = sum(stage_hits) or 1
449
+ mix_str = "/".join(f"{h*100//total_hits}" for h in stage_hits)
450
+ log_rank0(f"step {step:>6} | P{phase} | loss {loss_accum:.4f} | "
451
+ f"muon {muon_lr:.2e} adam {adam_lr:.2e} | "
452
+ f"{tps/1e3:.0f}K tok/s | mix {mix_str} | {elapsed_min:.1f}m")
453
+ stage_hits = [0, 0, 0]
454
+
455
+ # Eval (rank 0)
456
+ if step > start_step and step % EVAL_INTERVAL == 0 and rank == 0:
457
+ losses = estimate_loss(model, val_loader,
458
+ [stage1, stage2, stage3], device, EVAL_ITERS)
459
+ log_rank0(f" >>> EVAL: val {losses['val']:.4f} "
460
+ f"s1 {losses['stage1']:.4f} s2 {losses['stage2']:.4f} "
461
+ f"s3 {losses['stage3']:.4f}")
462
+ if losses["val"] < best_val:
463
+ best_val = losses["val"]
464
+ state = build_state(model, opt_muon, opt_adam, step, best_val)
465
+ atomic_save(state, BEST_CKPT)
466
+ log_rank0(f" >>> BEST kaydedildi (val {best_val:.4f})")
467
+
468
+ # Save (rank 0)
469
+ if step > start_step and step % SAVE_INTERVAL == 0 and rank == 0:
470
+ state = build_state(model, opt_muon, opt_adam, step, best_val)
471
+ atomic_save(state, LATEST_CKPT)
472
+
473
+ # HF push (Kaggle session limit icin)
474
+ if (args.hf_push_every and step > last_hf_push + args.hf_push_every
475
+ and rank == 0 and args.hf_user and args.hf_token):
476
+ log_rank0(f" >>> HF push step {step}...")
477
+ push_to_hf(LATEST_CKPT, args.hf_user, args.hf_token)
478
+ last_hf_push = step
479
+
480
+ # Time
481
+ if args.max_time and (time.time() - t_start) / 60 >= args.max_time:
482
+ if rank == 0:
483
+ log_rank0(f"\n[time] {args.max_time} dakika doldu, "
484
+ f"kaydedilip cikiliyor.")
485
+ break
486
+
487
+ if interrupt_flag["stop"]:
488
+ break
489
+
490
+ step += 1
491
+
492
+ finally:
493
+ if rank == 0:
494
+ log_rank0("\nSon checkpoint yaziliyor...")
495
+ state = build_state(model, opt_muon, opt_adam, step, best_val)
496
+ atomic_save(state, LATEST_CKPT)
497
+ log_rank0(f" latest_ckpt.pt -> step {step}, best_val {best_val:.4f}")
498
+
499
+ # Final HF push
500
+ if args.hf_user and args.hf_token:
501
+ log_rank0("Final HF push...")
502
+ push_to_hf(LATEST_CKPT, args.hf_user, args.hf_token)
503
+ if BEST_CKPT.exists():
504
+ push_to_hf(BEST_CKPT, args.hf_user, args.hf_token)
505
+
506
+
507
+ def main():
508
+ parser = argparse.ArgumentParser()
509
+ parser.add_argument("--resume", action="store_true")
510
+ parser.add_argument("--max-time", type=int, default=510,
511
+ help="Maks dakika (Kaggle 9h limit icin 510)")
512
+ parser.add_argument("--max-steps", type=int, default=None)
513
+ parser.add_argument("--hf-user", type=str, default=None,
514
+ help="HF push icin kullanici (kaydet+push)")
515
+ parser.add_argument("--hf-token", type=str,
516
+ default=os.environ.get("HF_TOKEN"),
517
+ help="HF token (env HF_TOKEN'dan da okur)")
518
+ parser.add_argument("--hf-push-every", type=int, default=500,
519
+ help="Her N step'te HF'e push (0=kapali)")
520
+ args = parser.parse_args()
521
+
522
+ if args.max_steps:
523
+ global MAX_STEPS, LR_DECAY_STEPS
524
+ MAX_STEPS = args.max_steps
525
+ LR_DECAY_STEPS = args.max_steps
526
+
527
+ # Multi-chip spawn — TPU v5e-8 icin 8 process
528
+ # Kaggle TPU genelde tek-host, xmp.spawn yeter
529
+ xmp.spawn(train_fn, args=(args,))
530
+
531
+
532
+ if __name__ == "__main__":
533
+ main()