BHARGAV REDDY commited on
Commit
ea58023
Β·
verified Β·
1 Parent(s): f0a08db

Upload smoke_test_300m.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. smoke_test_300m.py +579 -0
smoke_test_300m.py ADDED
@@ -0,0 +1,579 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LUNA 300M β€” Smoke Test (2 training steps)
3
+ ==========================================
4
+ Downloads the dataset, builds the model, runs 2 training steps,
5
+ saves a checkpoint, runs validation generation, then exits.
6
+
7
+ If everything prints "SMOKE TEST PASSED" at the end, the full
8
+ training pipeline is confirmed working.
9
+
10
+ Usage (cloud GPU):
11
+ python smoke_test_300m.py
12
+
13
+ Usage (local / CPU):
14
+ python smoke_test_300m.py --data_path Base/data/litdata_pretrain_final
15
+
16
+ Options:
17
+ --data_path : Path to local litdata dataset (skip download)
18
+ --hf_repo : HF dataset repo (default: ASTERIZER/Luna_Dataset)
19
+ --steps : Number of test steps (default: 2)
20
+ """
21
+
22
+ import os
23
+ import sys
24
+ import gc
25
+ import json
26
+ import time
27
+ import shutil
28
+ import struct
29
+ import argparse
30
+ import traceback
31
+ from pathlib import Path
32
+
33
+ # ─── Ensure deps ──────────────────────────────────────────────────────────────
34
+
35
+ def ensure_packages():
36
+ """Install missing packages quietly."""
37
+ required = ["torch", "psutil", "yaml", "transformers", "huggingface_hub"]
38
+ pip_names = {"yaml": "pyyaml"}
39
+ missing = []
40
+ for pkg in required:
41
+ try:
42
+ __import__(pkg)
43
+ except ImportError:
44
+ missing.append(pip_names.get(pkg, pkg))
45
+ if missing:
46
+ print(f" Installing missing packages: {missing}")
47
+ os.system(f"{sys.executable} -m pip install -q " + " ".join(missing))
48
+
49
+ ensure_packages()
50
+
51
+ import yaml
52
+ import psutil
53
+ import torch
54
+ import torch.nn as nn
55
+ import torch.nn.functional as F
56
+ import numpy as np
57
+ from torch.amp import autocast, GradScaler
58
+
59
+
60
+ # ─── Stage tracker ────────────────────────────────────────────────────────────
61
+
62
+ class StageTracker:
63
+ def __init__(self):
64
+ self.stages = []
65
+ self.current = None
66
+
67
+ def start(self, name):
68
+ self.current = name
69
+ print(f"\n{'─' * 60}")
70
+ print(f" STAGE: {name}")
71
+ print(f"{'─' * 60}")
72
+
73
+ def ok(self, detail=""):
74
+ msg = f" βœ“ {self.current}"
75
+ if detail:
76
+ msg += f" β€” {detail}"
77
+ print(msg)
78
+ self.stages.append((self.current, True, detail))
79
+
80
+ def fail(self, detail=""):
81
+ msg = f" βœ— {self.current}"
82
+ if detail:
83
+ msg += f" β€” {detail}"
84
+ print(msg)
85
+ self.stages.append((self.current, False, detail))
86
+
87
+ def summary(self):
88
+ print(f"\n{'=' * 60}")
89
+ print(f" SMOKE TEST SUMMARY")
90
+ print(f"{'=' * 60}")
91
+ all_pass = True
92
+ for name, passed, detail in self.stages:
93
+ icon = "PASS" if passed else "FAIL"
94
+ line = f" [{icon}] {name}"
95
+ if detail:
96
+ line += f" ({detail})"
97
+ print(line)
98
+ if not passed:
99
+ all_pass = False
100
+ print(f"{'=' * 60}")
101
+ if all_pass:
102
+ print(f" >>> SMOKE TEST PASSED β€” pipeline is ready for full training <<<")
103
+ else:
104
+ print(f" >>> SMOKE TEST FAILED β€” see above for details <<<")
105
+ print(f"{'=' * 60}\n")
106
+ return all_pass
107
+
108
+
109
+ # ─── Model (same architecture as train_300m.py) ──────────────────────────────
110
+
111
+ class RotaryEmbedding(nn.Module):
112
+ def __init__(self, dim, max_seq_len=1024):
113
+ super().__init__()
114
+ inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
115
+ self.register_buffer("inv_freq", inv_freq)
116
+ t = torch.arange(max_seq_len).float()
117
+ freqs = torch.einsum("i,j->ij", t, inv_freq)
118
+ emb = torch.cat([freqs, freqs], dim=-1)
119
+ self.register_buffer("cos_cached", emb.cos())
120
+ self.register_buffer("sin_cached", emb.sin())
121
+
122
+ def forward(self, seq_len):
123
+ return self.cos_cached[:seq_len], self.sin_cached[:seq_len]
124
+
125
+
126
+ def rotate_half(x):
127
+ x1, x2 = x.chunk(2, dim=-1)
128
+ return torch.cat([-x2, x1], dim=-1)
129
+
130
+
131
+ def apply_rotary(x, cos, sin):
132
+ c = cos.unsqueeze(0).unsqueeze(0)
133
+ s = sin.unsqueeze(0).unsqueeze(0)
134
+ return x * c + rotate_half(x) * s
135
+
136
+
137
+ class CausalSelfAttention(nn.Module):
138
+ def __init__(self, n_embd, n_head, block_size, rotary_pct=0.25):
139
+ super().__init__()
140
+ self.n_head = n_head
141
+ self.head_dim = n_embd // n_head
142
+ self.rot_dim = int(self.head_dim * rotary_pct)
143
+ self.c_attn = nn.Linear(n_embd, 3 * n_embd, bias=True)
144
+ self.c_proj = nn.Linear(n_embd, n_embd, bias=True)
145
+ self.rotary = RotaryEmbedding(self.rot_dim, block_size)
146
+
147
+ def forward(self, x):
148
+ B, T, C = x.size()
149
+ qkv = self.c_attn(x).reshape(B, T, 3, self.n_head, self.head_dim).permute(2, 0, 3, 1, 4)
150
+ q, k, v = qkv.unbind(0)
151
+ cos, sin = self.rotary(T)
152
+ q = torch.cat([apply_rotary(q[..., :self.rot_dim], cos, sin), q[..., self.rot_dim:]], dim=-1)
153
+ k = torch.cat([apply_rotary(k[..., :self.rot_dim], cos, sin), k[..., self.rot_dim:]], dim=-1)
154
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
155
+ return self.c_proj(y.transpose(1, 2).contiguous().view(B, T, C))
156
+
157
+
158
+ class MLP(nn.Module):
159
+ def __init__(self, n_embd):
160
+ super().__init__()
161
+ self.fc = nn.Linear(n_embd, 4 * n_embd, bias=True)
162
+ self.gelu = nn.GELU()
163
+ self.proj = nn.Linear(4 * n_embd, n_embd, bias=True)
164
+
165
+ def forward(self, x):
166
+ return self.proj(self.gelu(self.fc(x)))
167
+
168
+
169
+ class Block(nn.Module):
170
+ def __init__(self, n_embd, n_head, block_size):
171
+ super().__init__()
172
+ self.ln1 = nn.LayerNorm(n_embd)
173
+ self.attn = CausalSelfAttention(n_embd, n_head, block_size)
174
+ self.ln2 = nn.LayerNorm(n_embd)
175
+ self.mlp = MLP(n_embd)
176
+
177
+ def forward(self, x):
178
+ x = x + self.attn(self.ln1(x))
179
+ x = x + self.mlp(self.ln2(x))
180
+ return x
181
+
182
+
183
+ class LUNAModel(nn.Module):
184
+ def __init__(self, vocab_size, block_size, n_layer, n_embd, n_head):
185
+ super().__init__()
186
+ self.wte = nn.Embedding(vocab_size, n_embd)
187
+ self.blocks = nn.ModuleList([Block(n_embd, n_head, block_size) for _ in range(n_layer)])
188
+ self.ln_f = nn.LayerNorm(n_embd)
189
+ self.lm_head = nn.Linear(n_embd, vocab_size, bias=False)
190
+ self.lm_head.weight = self.wte.weight
191
+ self.apply(self._init_weights)
192
+
193
+ def _init_weights(self, m):
194
+ if isinstance(m, (nn.Linear, nn.Embedding)):
195
+ m.weight.data.normal_(mean=0.0, std=0.02)
196
+ if isinstance(m, nn.Linear) and m.bias is not None:
197
+ m.bias.data.zero_()
198
+
199
+ def forward(self, idx, targets=None, return_logits=True):
200
+ x = self.wte(idx)
201
+ for block in self.blocks:
202
+ x = block(x)
203
+ x = self.ln_f(x)
204
+ logits = self.lm_head(x)
205
+ loss = None
206
+ if targets is not None:
207
+ loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
208
+ if not return_logits:
209
+ logits = None
210
+ return logits, loss
211
+
212
+ @property
213
+ def num_params(self):
214
+ return sum(p.numel() for p in self.parameters()) - self.wte.weight.numel()
215
+
216
+
217
+ # ─── LitData Dataset (same as train_300m.py) ─────────────────────────────────
218
+
219
+ class LitDataDataset(torch.utils.data.Dataset):
220
+ def __init__(self, data_path, block_size=1024):
221
+ self.block_size = block_size
222
+ self.data_path = Path(data_path)
223
+ with open(self.data_path / "index.json") as f:
224
+ idx = json.load(f)
225
+ self.chunks_meta = idx["chunks"]
226
+ self._cum_blocks = []
227
+ total = 0
228
+ for c in self.chunks_meta:
229
+ n = c["dim"] // (block_size + 1)
230
+ total += n
231
+ self._cum_blocks.append(total)
232
+ self.total_blocks = total
233
+ self._chunk_cache = {}
234
+
235
+ def _load_chunk(self, chunk_idx):
236
+ if chunk_idx in self._chunk_cache:
237
+ return self._chunk_cache[chunk_idx]
238
+ meta = self.chunks_meta[chunk_idx]
239
+ with open(self.data_path / meta["filename"], "rb") as f:
240
+ raw = f.read()
241
+ num_items = struct.unpack_from("<I", raw, 0)[0]
242
+ header_bytes = (num_items + 2) * 4
243
+ tokens = torch.from_numpy(np.frombuffer(raw[header_bytes:], dtype=np.int32).copy())
244
+ if len(self._chunk_cache) >= 4:
245
+ del self._chunk_cache[next(iter(self._chunk_cache))]
246
+ self._chunk_cache[chunk_idx] = tokens
247
+ return tokens
248
+
249
+ def __len__(self):
250
+ return self.total_blocks
251
+
252
+ def __getitem__(self, idx):
253
+ chunk_idx = 0
254
+ for i, cum in enumerate(self._cum_blocks):
255
+ if idx < cum:
256
+ chunk_idx = i
257
+ break
258
+ prev = self._cum_blocks[chunk_idx - 1] if chunk_idx > 0 else 0
259
+ tokens = self._load_chunk(chunk_idx)
260
+ s = (idx - prev) * (self.block_size + 1)
261
+ e = s + self.block_size + 1
262
+ chunk = tokens[s:e]
263
+ if len(chunk) < self.block_size + 1:
264
+ pad = torch.zeros(self.block_size + 1, dtype=torch.int32)
265
+ pad[:len(chunk)] = chunk
266
+ chunk = pad
267
+ chunk = chunk.long()
268
+ return chunk[:self.block_size], chunk[1:self.block_size + 1]
269
+
270
+
271
+ # ─── Smoke test logic ────────────────────────────────────────────────────────
272
+
273
+ MODEL_CFG = {
274
+ "vocab_size": 50304,
275
+ "seq_len": 1024,
276
+ "n_layer": 20,
277
+ "n_embd": 1024,
278
+ "n_head": 16,
279
+ }
280
+
281
+ VALIDATION_PROMPTS = [
282
+ "The theory of general relativity describes gravity as",
283
+ "In machine learning, neural networks learn patterns by",
284
+ "The Amazon rainforest spans across several countries and",
285
+ ]
286
+
287
+
288
+ def detect_hardware():
289
+ info = {
290
+ "cpu_cores": os.cpu_count() or 4,
291
+ "ram_gb": psutil.virtual_memory().total / 1024**3,
292
+ }
293
+ if torch.cuda.is_available():
294
+ props = torch.cuda.get_device_properties(0)
295
+ info.update({
296
+ "device": "cuda",
297
+ "gpu_name": props.name,
298
+ "vram_gb": props.total_memory / 1024**3,
299
+ })
300
+ if props.major >= 8:
301
+ torch.backends.cuda.matmul.allow_tf32 = True
302
+ torch.backends.cudnn.allow_tf32 = True
303
+ info["dtype"] = torch.bfloat16
304
+ else:
305
+ info["dtype"] = torch.float16
306
+ else:
307
+ info.update({
308
+ "device": "cpu",
309
+ "gpu_name": "CPU",
310
+ "vram_gb": 0,
311
+ "dtype": torch.float32,
312
+ })
313
+ return info
314
+
315
+
316
+ def fetch_dataset(data_path, hf_repo):
317
+ """Fetch dataset β€” either use local path or download from HF."""
318
+ dp = Path(data_path)
319
+ if (dp / "index.json").exists():
320
+ print(f" Dataset found at {dp}")
321
+ return str(dp)
322
+
323
+ # Try auto-discovery in common locations
324
+ for candidate in [dp, Path("Base/data/litdata_pretrain_final"),
325
+ Path("/workspace/data/litdata_pretrain_final")]:
326
+ if (candidate / "index.json").exists():
327
+ print(f" Dataset found at {candidate}")
328
+ return str(candidate)
329
+
330
+ # Download from HF
331
+ print(f" Dataset not found locally. Downloading from HF: {hf_repo}")
332
+ out_dir = Path("/workspace/data/litdata_pretrain_final")
333
+ if not out_dir.parent.exists():
334
+ out_dir = Path(".smoke_test_data")
335
+
336
+ import subprocess
337
+ result = subprocess.run(
338
+ [sys.executable, "fetch_data.py",
339
+ "--source", "huggingface",
340
+ "--hf_repo", hf_repo,
341
+ "--out_dir", str(out_dir),
342
+ "--hf_token", os.environ.get("HF_TOKEN", "")],
343
+ capture_output=False
344
+ )
345
+ if result.returncode != 0:
346
+ raise RuntimeError("Dataset download failed!")
347
+ return str(out_dir)
348
+
349
+
350
+ @torch.no_grad()
351
+ def run_quick_validation(model, tokenizer, device, dtype, seq_len=1024, max_new=32):
352
+ """Generate short text from a few prompts to verify generation works."""
353
+ model.eval()
354
+ for i, prompt in enumerate(VALIDATION_PROMPTS):
355
+ input_ids = tokenizer.encode(prompt, return_tensors="pt").to(device)
356
+ if input_ids.shape[1] > seq_len:
357
+ input_ids = input_ids[:, -seq_len:]
358
+ generated = input_ids.clone()
359
+ for _ in range(max_new):
360
+ ctx = generated[:, -seq_len:] if generated.shape[1] > seq_len else generated
361
+ with autocast(device_type=device.type, dtype=dtype, enabled=(device.type == "cuda")):
362
+ logits, _ = model(ctx)
363
+ next_token = logits[:, -1, :].argmax(dim=-1, keepdim=True)
364
+ generated = torch.cat([generated, next_token], dim=1)
365
+ output = tokenizer.decode(generated[0], skip_special_tokens=True)
366
+ continuation = output[len(prompt):].strip()[:120]
367
+ print(f" [{i+1}] {prompt}")
368
+ print(f" -> {continuation}")
369
+ model.train()
370
+
371
+
372
+ def run_smoke_test(args):
373
+ tracker = StageTracker()
374
+
375
+ # ── Stage 1: Hardware ─────────────────────────────────────────────────────
376
+ tracker.start("Hardware Detection")
377
+ try:
378
+ hw = detect_hardware()
379
+ device = torch.device(hw["device"])
380
+ dtype = hw["dtype"]
381
+ tracker.ok(f"{hw['gpu_name']}, {hw['ram_gb']:.0f}GB RAM, dtype={dtype}")
382
+ except Exception as e:
383
+ tracker.fail(str(e))
384
+ traceback.print_exc()
385
+ return tracker.summary()
386
+
387
+ # ── Stage 2: Dataset ──────────────────────────────────────────────────────
388
+ tracker.start("Dataset Fetch & Verify")
389
+ try:
390
+ data_path = fetch_dataset(args.data_path, args.hf_repo)
391
+ with open(Path(data_path) / "index.json") as f:
392
+ idx = json.load(f)
393
+ chunks = idx.get("chunks", [])
394
+ total_tokens = sum(c.get("dim", 0) for c in chunks)
395
+ present = sum(1 for c in chunks if (Path(data_path) / c["filename"]).exists())
396
+ missing = len(chunks) - present
397
+ if missing > 0:
398
+ tracker.fail(f"{missing} chunks missing out of {len(chunks)}")
399
+ return tracker.summary()
400
+ tracker.ok(f"{len(chunks)} chunks, {total_tokens:,} tokens, 0 missing")
401
+ except Exception as e:
402
+ tracker.fail(str(e))
403
+ traceback.print_exc()
404
+ return tracker.summary()
405
+
406
+ # ── Stage 3: Model Init ───────────────────────────────────────────────────
407
+ tracker.start("Model Initialization (300M)")
408
+ try:
409
+ model = LUNAModel(
410
+ vocab_size=MODEL_CFG["vocab_size"],
411
+ block_size=MODEL_CFG["seq_len"],
412
+ n_layer=MODEL_CFG["n_layer"],
413
+ n_embd=MODEL_CFG["n_embd"],
414
+ n_head=MODEL_CFG["n_head"],
415
+ ).to(device)
416
+ n_params = sum(p.numel() for p in model.parameters())
417
+ tracker.ok(f"{n_params:,} parameters on {device}")
418
+ except Exception as e:
419
+ tracker.fail(str(e))
420
+ traceback.print_exc()
421
+ return tracker.summary()
422
+
423
+ # ── Stage 4: Dataset Loading ──────────────────────────────────────────────
424
+ tracker.start("Dataset Loading (LitData)")
425
+ try:
426
+ dataset = LitDataDataset(data_path, block_size=MODEL_CFG["seq_len"])
427
+ loader = torch.utils.data.DataLoader(
428
+ dataset, batch_size=1, shuffle=True,
429
+ num_workers=0, pin_memory=False, drop_last=True,
430
+ )
431
+ x_sample, t_sample = next(iter(loader))
432
+ tracker.ok(f"{len(dataset):,} blocks, sample shape={list(x_sample.shape)}")
433
+ except Exception as e:
434
+ tracker.fail(str(e))
435
+ traceback.print_exc()
436
+ return tracker.summary()
437
+
438
+ # ── Stage 5: Tokenizer ────────────────────────────────────────────────────
439
+ tracker.start("Tokenizer Loading")
440
+ try:
441
+ from transformers import AutoTokenizer
442
+ # Try common tokenizer locations
443
+ tok_dir = None
444
+ for candidate in [
445
+ Path("Base/checkpoints/EleutherAI/pythia-160m"),
446
+ Path("/workspace/Base/checkpoints/EleutherAI/pythia-160m"),
447
+ Path("/workspace/LUNA/Base/checkpoints/EleutherAI/pythia-160m"),
448
+ ]:
449
+ if candidate.exists():
450
+ tok_dir = candidate
451
+ break
452
+
453
+ if tok_dir is None:
454
+ # Fall back to downloading from HF
455
+ tokenizer = AutoTokenizer.from_pretrained("EleutherAI/pythia-160m")
456
+ tracker.ok(f"Downloaded from HF (EleutherAI/pythia-160m)")
457
+ else:
458
+ tokenizer = AutoTokenizer.from_pretrained(str(tok_dir))
459
+ tracker.ok(f"Loaded from {tok_dir}")
460
+ except Exception as e:
461
+ tracker.fail(str(e))
462
+ traceback.print_exc()
463
+ return tracker.summary()
464
+
465
+ # ── Stage 6: Forward + Backward Pass ──────────────────────────────────────
466
+ tracker.start(f"Training Loop ({args.steps} steps)")
467
+ try:
468
+ model.train()
469
+ optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)
470
+ use_scaler = dtype == torch.float16
471
+ scaler = GradScaler(enabled=use_scaler)
472
+ data_iter = iter(loader)
473
+
474
+ losses = []
475
+ for step in range(1, args.steps + 1):
476
+ t0 = time.perf_counter()
477
+ try:
478
+ x, t = next(data_iter)
479
+ except StopIteration:
480
+ data_iter = iter(loader)
481
+ x, t = next(data_iter)
482
+
483
+ x = x.to(device, non_blocking=True)
484
+ t = t.to(device, non_blocking=True)
485
+
486
+ optimizer.zero_grad(set_to_none=True)
487
+ with autocast(device_type=device.type, dtype=dtype, enabled=(device.type == "cuda")):
488
+ _, loss = model(x, t, return_logits=False)
489
+ scaler.scale(loss).backward()
490
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
491
+ scaler.step(optimizer)
492
+ scaler.update()
493
+
494
+ if device.type == "cuda":
495
+ torch.cuda.synchronize()
496
+
497
+ dt = time.perf_counter() - t0
498
+ loss_val = loss.item()
499
+ losses.append(loss_val)
500
+ tps = MODEL_CFG["seq_len"] / dt
501
+ vram = torch.cuda.max_memory_allocated() / 1024**3 if device.type == "cuda" else 0
502
+ print(f" step {step}/{args.steps} | loss={loss_val:.4f} | "
503
+ f"{tps:,.0f} tok/s | {dt:.2f}s | VRAM={vram:.1f}GB")
504
+
505
+ avg_loss = sum(losses) / len(losses)
506
+ tracker.ok(f"avg_loss={avg_loss:.4f}, all {args.steps} steps completed")
507
+ except Exception as e:
508
+ tracker.fail(str(e))
509
+ traceback.print_exc()
510
+ return tracker.summary()
511
+
512
+ # ── Stage 7: Checkpoint Save ──────────────────────────────────────────────
513
+ tracker.start("Checkpoint Save")
514
+ try:
515
+ out_dir = Path(".smoke_test_output")
516
+ out_dir.mkdir(parents=True, exist_ok=True)
517
+ torch.save(model.state_dict(), out_dir / "lit_model.pth")
518
+ with open(out_dir / "model_config.json", "w") as f:
519
+ json.dump(MODEL_CFG, f, indent=2)
520
+ size_mb = (out_dir / "lit_model.pth").stat().st_size / 1024**2
521
+ tracker.ok(f"Saved to {out_dir} ({size_mb:.0f} MB)")
522
+ except Exception as e:
523
+ tracker.fail(str(e))
524
+ traceback.print_exc()
525
+ return tracker.summary()
526
+
527
+ # ── Stage 8: Validation Generation ────────────────────────────────────────
528
+ tracker.start("Validation Generation (3 prompts)")
529
+ try:
530
+ run_quick_validation(model, tokenizer, device, dtype,
531
+ seq_len=MODEL_CFG["seq_len"], max_new=32)
532
+ tracker.ok("Generated text from all prompts")
533
+ except Exception as e:
534
+ tracker.fail(str(e))
535
+ traceback.print_exc()
536
+ return tracker.summary()
537
+
538
+ # ── Stage 9: Cleanup ──────────────────────────────────────────────────────
539
+ tracker.start("Cleanup")
540
+ try:
541
+ if Path(".smoke_test_output").exists():
542
+ shutil.rmtree(".smoke_test_output")
543
+ if Path(".smoke_test_data").exists():
544
+ shutil.rmtree(".smoke_test_data")
545
+ del model, optimizer, scaler, dataset, loader
546
+ gc.collect()
547
+ if device.type == "cuda":
548
+ torch.cuda.empty_cache()
549
+ tracker.ok("Cleaned up temp files and freed memory")
550
+ except Exception as e:
551
+ tracker.fail(str(e))
552
+
553
+ # ── Summary ───────────────────────────────────────────────────────────────
554
+ return tracker.summary()
555
+
556
+
557
+ # ─── Entry ────────────────────────────────────────────────────────────────────
558
+
559
+ def parse_args():
560
+ p = argparse.ArgumentParser(description="LUNA 300M Smoke Test")
561
+ p.add_argument("--data_path", type=str,
562
+ default="Base/data/litdata_pretrain_final",
563
+ help="Local dataset path (auto-downloads from HF if not found)")
564
+ p.add_argument("--hf_repo", type=str,
565
+ default="ASTERIZER/Luna_Dataset",
566
+ help="HF dataset repo to download from if local not found")
567
+ p.add_argument("--steps", type=int, default=2,
568
+ help="Number of training steps to run (default: 2)")
569
+ return p.parse_args()
570
+
571
+
572
+ if __name__ == "__main__":
573
+ args = parse_args()
574
+ print("=" * 60)
575
+ print(" LUNA 300M β€” SMOKE TEST")
576
+ print(" This runs 2 training steps to verify the full pipeline")
577
+ print("=" * 60)
578
+ passed = run_smoke_test(args)
579
+ sys.exit(0 if passed else 1)