BHARGAV REDDY commited on
Commit
32e72af
Β·
verified Β·
1 Parent(s): 68b3c3b

Upload benchmark_runpod.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. benchmark_runpod.py +412 -412
benchmark_runpod.py CHANGED
@@ -1,412 +1,412 @@
1
- """
2
- LUNA 100M - Local Benchmark + RunPod Cost Calculator
3
- =====================================================
4
- Uses PyTorch SDPA (Flash Attention) for realistic training throughput.
5
- Matches the exact LUNA model architecture and training config.
6
- """
7
-
8
- import os
9
- import sys
10
- import time
11
- import math
12
- import json
13
- import gc
14
- import torch
15
- import torch.nn as nn
16
- import torch.nn.functional as F
17
- from torch.amp import autocast, GradScaler
18
-
19
- # ─── Model Architecture (matches your config exactly) ─────────────────────────
20
-
21
- class RotaryEmbedding(nn.Module):
22
- def __init__(self, dim, max_seq_len=1024):
23
- super().__init__()
24
- inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
25
- self.register_buffer("inv_freq", inv_freq)
26
- t = torch.arange(max_seq_len).float()
27
- freqs = torch.einsum("i,j->ij", t, inv_freq)
28
- emb = torch.cat([freqs, freqs], dim=-1)
29
- self.register_buffer("cos_cached", emb.cos())
30
- self.register_buffer("sin_cached", emb.sin())
31
-
32
- def forward(self, seq_len):
33
- return self.cos_cached[:seq_len], self.sin_cached[:seq_len]
34
-
35
- def rotate_half(x):
36
- x1, x2 = x.chunk(2, dim=-1)
37
- return torch.cat([-x2, x1], dim=-1)
38
-
39
- def apply_rotary(x, cos, sin):
40
- cos = cos.unsqueeze(0).unsqueeze(0)
41
- sin = sin.unsqueeze(0).unsqueeze(0)
42
- return x * cos + rotate_half(x) * sin
43
-
44
- class CausalSelfAttention(nn.Module):
45
- def __init__(self, n_embd, n_head, block_size, rotary_pct=0.25):
46
- super().__init__()
47
- self.n_head = n_head
48
- self.head_dim = n_embd // n_head
49
- self.rotary_dim = int(self.head_dim * rotary_pct)
50
- self.c_attn = nn.Linear(n_embd, 3 * n_embd, bias=True)
51
- self.c_proj = nn.Linear(n_embd, n_embd, bias=True)
52
- self.rotary = RotaryEmbedding(self.rotary_dim, block_size)
53
-
54
- def forward(self, x):
55
- B, T, C = x.size()
56
- qkv = self.c_attn(x).reshape(B, T, 3, self.n_head, self.head_dim).permute(2, 0, 3, 1, 4)
57
- q, k, v = qkv.unbind(0)
58
-
59
- cos, sin = self.rotary(T)
60
- q_rot = apply_rotary(q[..., :self.rotary_dim], cos, sin)
61
- k_rot = apply_rotary(k[..., :self.rotary_dim], cos, sin)
62
- q = torch.cat([q_rot, q[..., self.rotary_dim:]], dim=-1)
63
- k = torch.cat([k_rot, k[..., self.rotary_dim:]], dim=-1)
64
-
65
- # SDPA = Flash Attention / Memory Efficient Attention
66
- y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
67
- y = y.transpose(1, 2).contiguous().view(B, T, C)
68
- return self.c_proj(y)
69
-
70
- class MLP(nn.Module):
71
- def __init__(self, n_embd):
72
- super().__init__()
73
- self.c_fc = nn.Linear(n_embd, 4 * n_embd, bias=True)
74
- self.gelu = nn.GELU()
75
- self.c_proj = nn.Linear(4 * n_embd, n_embd, bias=True)
76
- def forward(self, x):
77
- return self.c_proj(self.gelu(self.c_fc(x)))
78
-
79
- class Block(nn.Module):
80
- def __init__(self, n_embd, n_head, block_size):
81
- super().__init__()
82
- self.ln_1 = nn.LayerNorm(n_embd)
83
- self.attn = CausalSelfAttention(n_embd, n_head, block_size)
84
- self.ln_2 = nn.LayerNorm(n_embd)
85
- self.mlp = MLP(n_embd)
86
- def forward(self, x):
87
- x = x + self.attn(self.ln_1(x))
88
- x = x + self.mlp(self.ln_2(x))
89
- return x
90
-
91
- class LUNAModel(nn.Module):
92
- def __init__(self, vocab_size=50254, block_size=1024, n_layer=10, n_embd=768, n_head=12):
93
- super().__init__()
94
- self.block_size = block_size
95
- self.wte = nn.Embedding(vocab_size, n_embd)
96
- self.blocks = nn.ModuleList([Block(n_embd, n_head, block_size) for _ in range(n_layer)])
97
- self.ln_f = nn.LayerNorm(n_embd)
98
- self.lm_head = nn.Linear(n_embd, vocab_size, bias=False)
99
- self.lm_head.weight = self.wte.weight
100
- self.apply(self._init_weights)
101
-
102
- def _init_weights(self, module):
103
- if isinstance(module, (nn.Linear, nn.Embedding)):
104
- module.weight.data.normal_(mean=0.0, std=0.02)
105
- if isinstance(module, nn.Linear) and module.bias is not None:
106
- module.bias.data.zero_()
107
-
108
- def forward(self, idx, targets=None):
109
- B, T = idx.size()
110
- x = self.wte(idx)
111
- for block in self.blocks:
112
- x = block(x)
113
- x = self.ln_f(x)
114
- logits = self.lm_head(x)
115
- loss = None
116
- if targets is not None:
117
- loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
118
- return logits, loss
119
-
120
- # ─── Config ────────────────────────────────────────────────────────────────────
121
-
122
- DATASET_TOTAL_TOKENS = 4_515_286_950 # Verified from index.json: 270 chunks, sum of all dims
123
- BLOCK_SIZE = 1024
124
- VOCAB_SIZE = 50254
125
- N_LAYER = 10
126
- N_EMBD = 768
127
- N_HEAD = 12
128
- GLOBAL_BATCH_SIZE = 120
129
- MAX_SEQ_LENGTH = 1024
130
-
131
- WARMUP_STEPS = 3
132
- BENCHMARK_STEPS = 4
133
-
134
- # RunPod GPUs (Community Cloud pricing April 2026)
135
- RUNPOD_GPUS = [
136
- # (name, $/hr, VRAM_GB, bf16_TF_nonsparse, mem_bw_GBs, arch)
137
- ("RTX A5000", 0.16, 24, 65, 768, "Ampere"),
138
- ("RTX 3090", 0.22, 24, 71, 936, "Ampere"),
139
- ("RTX A6000", 0.33, 48, 77, 768, "Ampere"),
140
- ("RTX 4090", 0.34, 24, 165, 1008, "Ada"),
141
- ("A40", 0.35, 48, 75, 696, "Ampere"),
142
- ("L4", 0.44, 24, 121, 300, "Ada"),
143
- ("RTX 5090", 0.69, 32, 210, 1792, "Blackwell"),
144
- ("L40", 0.69, 48, 181, 864, "Ada"),
145
- ("RTX 6000 Ada", 0.74, 48, 181, 960, "Ada"),
146
- ("L40S", 0.79, 48, 183, 864, "Ada"),
147
- ("A100 PCIe 80GB", 1.19, 80, 312, 2039, "Ampere"),
148
- ("A100 SXM 80GB", 1.39, 80, 312, 2039, "Ampere"),
149
- ("RTX Pro 6000", 1.69, 96, 260, 1280, "Blackwell"),
150
- ("H100 PCIe", 1.99, 80, 756, 2039, "Hopper"),
151
- ("H100 NVL", 2.59, 94, 835, 3938, "Hopper"),
152
- ("H100 SXM", 2.69, 80, 990, 3352, "Hopper"),
153
- ("H200", 3.59,141, 990, 4800, "Hopper"),
154
- ]
155
-
156
- USD_TO_INR = 86.0
157
-
158
-
159
- def find_max_micro_batch(model, device, seq_len=1024, start=32):
160
- """Binary search for max micro_batch_size, with 0.65 safety factor."""
161
- model.train()
162
- lo, hi, best = 1, start, 1
163
- opt_tmp = torch.optim.AdamW(model.parameters(), lr=1e-4)
164
-
165
- while hi >= lo:
166
- mid = (lo + hi) // 2
167
- try:
168
- torch.cuda.empty_cache()
169
- torch.cuda.reset_peak_memory_stats()
170
- opt_tmp.zero_grad(set_to_none=True)
171
- x = torch.randint(0, VOCAB_SIZE, (mid, seq_len), device=device)
172
- t = torch.randint(0, VOCAB_SIZE, (mid, seq_len), device=device)
173
- with autocast(device_type='cuda', dtype=torch.bfloat16):
174
- _, loss = model(x, t)
175
- loss.backward()
176
- opt_tmp.step()
177
- opt_tmp.zero_grad(set_to_none=True)
178
- best = mid
179
- lo = mid + 1
180
- del x, t, loss
181
- torch.cuda.empty_cache()
182
- except (torch.cuda.OutOfMemoryError, RuntimeError):
183
- try:
184
- del x, t, loss
185
- except:
186
- pass
187
- torch.cuda.empty_cache()
188
- opt_tmp.zero_grad(set_to_none=True)
189
- hi = mid - 1
190
-
191
- safe = max(1, int(best * 0.65))
192
- del opt_tmp
193
- torch.cuda.empty_cache()
194
- gc.collect()
195
- return safe
196
-
197
-
198
- def run_benchmark():
199
- device = torch.device("cuda")
200
- torch.backends.cuda.matmul.allow_tf32 = True
201
- torch.backends.cudnn.allow_tf32 = True
202
-
203
- print("=" * 72)
204
- print(" LUNA 100M - TRAINING BENCHMARK & RUNPOD COST CALCULATOR")
205
- print("=" * 72)
206
-
207
- gpu_name = torch.cuda.get_device_name(0)
208
- gpu_mem = torch.cuda.get_device_properties(0).total_memory / 1024**3
209
- print(f"\n Local GPU: {gpu_name}")
210
- print(f" VRAM: {gpu_mem:.1f} GB")
211
- print(f" PyTorch: {torch.__version__}, CUDA: {torch.version.cuda}")
212
-
213
- print(f"\n Creating LUNA-100M (SDPA/Flash Attention)...")
214
- model = LUNAModel(VOCAB_SIZE, BLOCK_SIZE, N_LAYER, N_EMBD, N_HEAD).to(device)
215
-
216
- total_params = sum(p.numel() for p in model.parameters())
217
- # Tied embeddings: wte(50254*768) = 38,595,072 counted once
218
- unique_params = total_params - model.wte.weight.numel()
219
- print(f" Parameters: {total_params:,} total, {unique_params:,} unique")
220
-
221
- print(f"\n Probing max micro_batch_size...")
222
- max_mbs = find_max_micro_batch(model, device, MAX_SEQ_LENGTH, start=40)
223
- print(f" Safe micro_batch_size: {max_mbs}")
224
-
225
- grad_accum = max(1, GLOBAL_BATCH_SIZE // max_mbs)
226
- effective_batch = max_mbs * grad_accum
227
- tokens_per_step = effective_batch * MAX_SEQ_LENGTH
228
- print(f" grad_accum={grad_accum}, effective_batch={effective_batch}")
229
- print(f" Tokens/step: {tokens_per_step:,}")
230
-
231
- optimizer = torch.optim.AdamW(
232
- model.parameters(), lr=6e-4, weight_decay=0.1,
233
- betas=(0.9, 0.95), eps=1e-8
234
- )
235
- scaler = GradScaler()
236
-
237
- total_steps = WARMUP_STEPS + BENCHMARK_STEPS
238
- print(f"\n Running {WARMUP_STEPS} warmup + {BENCHMARK_STEPS} benchmark steps...")
239
-
240
- model.train()
241
- step_times = []
242
- torch.cuda.synchronize()
243
- torch.cuda.reset_peak_memory_stats()
244
-
245
- for step in range(total_steps):
246
- t0 = time.perf_counter()
247
- optimizer.zero_grad(set_to_none=True)
248
- step_loss = 0.0
249
-
250
- for _ in range(grad_accum):
251
- x = torch.randint(0, VOCAB_SIZE, (max_mbs, MAX_SEQ_LENGTH), device=device)
252
- tgt = torch.randint(0, VOCAB_SIZE, (max_mbs, MAX_SEQ_LENGTH), device=device)
253
- with autocast(device_type='cuda', dtype=torch.bfloat16):
254
- _, loss = model(x, tgt)
255
- loss = loss / grad_accum
256
- scaler.scale(loss).backward()
257
- step_loss += loss.item()
258
- del x, tgt, loss
259
-
260
- scaler.unscale_(optimizer)
261
- torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
262
- scaler.step(optimizer)
263
- scaler.update()
264
- torch.cuda.synchronize()
265
- dt = time.perf_counter() - t0
266
-
267
- if step >= WARMUP_STEPS:
268
- step_times.append(dt)
269
-
270
- tps = tokens_per_step / dt
271
- phase = "WARM" if step < WARMUP_STEPS else "BENCH"
272
- print(f" [{phase}] Step {step:3d} | Loss {step_loss:.4f} | {dt:.2f}s | {tps:,.0f} tok/s")
273
-
274
- # ─── Results ──────────────────────────────────────────────────────────────
275
- peak_vram_gb = torch.cuda.max_memory_allocated() / 1024**3
276
-
277
- avg_time = sum(step_times) / len(step_times)
278
- med_time = sorted(step_times)[len(step_times) // 2]
279
- avg_tps = tokens_per_step / avg_time
280
- med_tps = tokens_per_step / med_time
281
- peak_tps = tokens_per_step / min(step_times)
282
-
283
- flops_per_token = 6 * unique_params
284
- achieved_tf = (avg_tps * flops_per_token) / 1e12
285
- LOCAL_TF = 88.0 # RTX 4060 Ti BF16 non-sparse
286
- LOCAL_BW = 288.0 # GB/s
287
- mfu = achieved_tf / LOCAL_TF
288
-
289
- print("\n" + "=" * 72)
290
- print(" LOCAL BENCHMARK RESULTS")
291
- print("=" * 72)
292
- print(f" GPU: {gpu_name} ({gpu_mem:.1f} GB)")
293
- print(f" Peak VRAM: {peak_vram_gb:.2f} GB ({peak_vram_gb/gpu_mem*100:.0f}%)")
294
- print(f" Batch: micro={max_mbs}, accum={grad_accum}, global={effective_batch}")
295
- print(f" Step time: avg={avg_time:.3f}s, median={med_time:.3f}s")
296
- print(f" Tokens/sec: avg={avg_tps:,.0f}, median={med_tps:,.0f}, peak={peak_tps:,.0f}")
297
- print(f" TFLOPS: {achieved_tf:.2f}, MFU: {mfu*100:.1f}%")
298
-
299
- n_steps = math.ceil(DATASET_TOTAL_TOKENS / tokens_per_step)
300
- local_hrs = (n_steps * avg_time) / 3600
301
-
302
- print(f" Dataset: 4,515,286,950 tokens | Steps needed: {n_steps:,}")
303
- print(f" Local training: {local_hrs:.1f} hrs ({local_hrs/24:.1f} days)")
304
-
305
- # ─── RunPod Estimates ─────────────────────────────────────────────────────
306
- print("\n" + "=" * 72)
307
- print(" RUNPOD GPU COMPARISON (Community Cloud, INR/86/USD)")
308
- print("=" * 72)
309
-
310
- results = []
311
- for name, price, vram, bf16, bw, arch in RUNPOD_GPUS:
312
- # Memory estimation for each GPU
313
- fixed_gb = (unique_params * (2 + 8 + 2)) / 1024**3 + 0.5
314
- avail_gb = vram - fixed_gb
315
- act_per_sample = max(0.05, (peak_vram_gb - fixed_gb) / max(max_mbs, 1))
316
- est_mbs = max(1, min(128, int(avail_gb / act_per_sample)))
317
- est_ga = max(1, GLOBAL_BATCH_SIZE // est_mbs)
318
- est_tps_step = est_mbs * est_ga * MAX_SEQ_LENGTH
319
-
320
- # Scaling: 50% compute + 50% bandwidth (validated for small transformers)
321
- speedup = 0.50 * (bf16 / LOCAL_TF) + 0.50 * (bw / LOCAL_BW)
322
- est_tps = avg_tps * speedup * 0.90 # 0.90 cloud overhead
323
-
324
- est_steps = math.ceil(DATASET_TOTAL_TOKENS / est_tps_step)
325
- est_sec = (est_tps_step / est_tps) * est_steps
326
- est_hrs = est_sec / 3600
327
- cost_usd = est_hrs * price
328
- cost_inr = cost_usd * USD_TO_INR
329
-
330
- results.append({
331
- "gpu": name, "price": price, "vram": vram, "bf16": bf16,
332
- "bw": bw, "arch": arch, "mbs": est_mbs, "ga": est_ga,
333
- "tps": round(est_tps), "hours": round(est_hrs, 1),
334
- "usd": round(cost_usd, 2), "inr": round(cost_inr),
335
- })
336
-
337
- results.sort(key=lambda r: r["inr"])
338
-
339
- print(f"\n {'#':<3} {'GPU':<18} {'$/hr':>5} {'VRAM':>5} {'tok/s':>10} "
340
- f"{'Hours':>7} {'$ USD':>8} {'INR':>10}")
341
- print(" " + "─" * 72)
342
- for i, r in enumerate(results):
343
- s = " *" if i < 3 else ""
344
- print(f" {i+1:<3} {r['gpu']:<18} {r['price']:>5.2f} {r['vram']:>4}G "
345
- f"{r['tps']:>10,} {r['hours']:>7.1f} {r['usd']:>8.2f} {r['inr']:>10,}{s}")
346
-
347
- # Top 5 detailed
348
- print("\n" + "=" * 72)
349
- print(" TOP 5 CHEAPEST - DETAILS")
350
- print("=" * 72)
351
- for i, r in enumerate(results[:5]):
352
- sx = r["tps"] / avg_tps if avg_tps > 0 else 0
353
- print(f"\n #{i+1}: {r['gpu']} ({r['arch']})")
354
- print(f" β”œβ”€β”€ ${r['price']:.2f}/hr | {r['vram']}GB VRAM | {r['bf16']} TF | {r['bw']} GB/s")
355
- print(f" β”œβ”€β”€ micro_batch: {r['mbs']}, grad_accum: {r['ga']}")
356
- print(f" β”œβ”€β”€ {r['tps']:,} tok/s ({sx:.2f}Γ— local)")
357
- print(f" β”œβ”€β”€ {r['hours']:.1f} hrs ({r['hours']/24:.1f} days)")
358
- print(f" +-- ${r['usd']:.2f} = INR {r['inr']:,}")
359
-
360
- # Local reference
361
- print("\n" + "=" * 72)
362
- print(" YOUR LOCAL GPU")
363
- print("=" * 72)
364
- print(f" RTX 4060 Ti 16GB: {avg_tps:,.0f} tok/s")
365
- print(f" Training: {local_hrs:.1f} hrs ({local_hrs/24:.1f} days)")
366
- print(f" Electricity: ~INR {local_hrs * 0.16 * 8:,.0f} (160W x Rs8/kWh)")
367
-
368
- # Recommendation
369
- best = results[0]
370
- print("\n" + "=" * 72)
371
- print(" * RECOMMENDATION")
372
- print("=" * 72)
373
- print(f" Most affordable: {best['gpu']} @ ${best['price']:.2f}/hr")
374
- print(f" Time: {best['hours']:.1f} hrs ({best['hours']/24:.1f} days)")
375
- print(f" Cost: INR {best['inr']:,} (${best['usd']:.2f})")
376
- print(f" Speed: {best['tps']/avg_tps:.1f}Γ— local" if avg_tps > 0 else "")
377
-
378
- fast = [r for r in results if r["hours"] < max(8, local_hrs * 0.15)]
379
- if fast:
380
- fb = min(fast, key=lambda r: r["inr"])
381
- if fb["gpu"] != best["gpu"]:
382
- print(f"\n Fastest affordable: {fb['gpu']} @ ${fb['price']:.2f}/hr")
383
- print(f" Time: {fb['hours']:.1f} hrs | Cost: INR {fb['inr']:,}")
384
-
385
- print("\n" + "=" * 72)
386
-
387
- # Save JSON
388
- out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "runpod_cost_estimate.json")
389
- with open(out, "w") as f:
390
- json.dump({
391
- "benchmark": {
392
- "gpu": gpu_name, "vram_gb": round(gpu_mem, 1),
393
- "peak_vram_gb": round(peak_vram_gb, 2),
394
- "micro_batch": max_mbs, "grad_accum": grad_accum,
395
- "tokens_per_step": tokens_per_step,
396
- "avg_tok_per_sec": round(avg_tps),
397
- "median_tok_per_sec": round(med_tps),
398
- "achieved_tflops": round(achieved_tf, 2),
399
- "mfu_pct": round(mfu*100, 1),
400
- },
401
- "dataset": {"tokens": DATASET_TOTAL_TOKENS, "chunks": 270},
402
- "model": {"total_params": total_params, "unique_params": unique_params},
403
- "local_hours": round(local_hrs, 1),
404
- "runpod": results,
405
- "usd_to_inr": USD_TO_INR,
406
- }, f, indent=2)
407
- print(f" Saved: {out}")
408
- print("=" * 72)
409
-
410
-
411
- if __name__ == "__main__":
412
- run_benchmark()
 
1
+ """
2
+ LUNA 100M - Local Benchmark + RunPod Cost Calculator
3
+ =====================================================
4
+ Uses PyTorch SDPA (Flash Attention) for realistic training throughput.
5
+ Matches the exact LUNA model architecture and training config.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import time
11
+ import math
12
+ import json
13
+ import gc
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ from torch.amp import autocast, GradScaler
18
+
19
+ # ─── Model Architecture (matches your config exactly) ─────────────────────────
20
+
21
+ class RotaryEmbedding(nn.Module):
22
+ def __init__(self, dim, max_seq_len=1024):
23
+ super().__init__()
24
+ inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
25
+ self.register_buffer("inv_freq", inv_freq)
26
+ t = torch.arange(max_seq_len).float()
27
+ freqs = torch.einsum("i,j->ij", t, inv_freq)
28
+ emb = torch.cat([freqs, freqs], dim=-1)
29
+ self.register_buffer("cos_cached", emb.cos())
30
+ self.register_buffer("sin_cached", emb.sin())
31
+
32
+ def forward(self, seq_len):
33
+ return self.cos_cached[:seq_len], self.sin_cached[:seq_len]
34
+
35
+ def rotate_half(x):
36
+ x1, x2 = x.chunk(2, dim=-1)
37
+ return torch.cat([-x2, x1], dim=-1)
38
+
39
+ def apply_rotary(x, cos, sin):
40
+ cos = cos.unsqueeze(0).unsqueeze(0)
41
+ sin = sin.unsqueeze(0).unsqueeze(0)
42
+ return x * cos + rotate_half(x) * sin
43
+
44
+ class CausalSelfAttention(nn.Module):
45
+ def __init__(self, n_embd, n_head, block_size, rotary_pct=0.25):
46
+ super().__init__()
47
+ self.n_head = n_head
48
+ self.head_dim = n_embd // n_head
49
+ self.rotary_dim = int(self.head_dim * rotary_pct)
50
+ self.c_attn = nn.Linear(n_embd, 3 * n_embd, bias=True)
51
+ self.c_proj = nn.Linear(n_embd, n_embd, bias=True)
52
+ self.rotary = RotaryEmbedding(self.rotary_dim, block_size)
53
+
54
+ def forward(self, x):
55
+ B, T, C = x.size()
56
+ qkv = self.c_attn(x).reshape(B, T, 3, self.n_head, self.head_dim).permute(2, 0, 3, 1, 4)
57
+ q, k, v = qkv.unbind(0)
58
+
59
+ cos, sin = self.rotary(T)
60
+ q_rot = apply_rotary(q[..., :self.rotary_dim], cos, sin)
61
+ k_rot = apply_rotary(k[..., :self.rotary_dim], cos, sin)
62
+ q = torch.cat([q_rot, q[..., self.rotary_dim:]], dim=-1)
63
+ k = torch.cat([k_rot, k[..., self.rotary_dim:]], dim=-1)
64
+
65
+ # SDPA = Flash Attention / Memory Efficient Attention
66
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
67
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
68
+ return self.c_proj(y)
69
+
70
+ class MLP(nn.Module):
71
+ def __init__(self, n_embd):
72
+ super().__init__()
73
+ self.c_fc = nn.Linear(n_embd, 4 * n_embd, bias=True)
74
+ self.gelu = nn.GELU()
75
+ self.c_proj = nn.Linear(4 * n_embd, n_embd, bias=True)
76
+ def forward(self, x):
77
+ return self.c_proj(self.gelu(self.c_fc(x)))
78
+
79
+ class Block(nn.Module):
80
+ def __init__(self, n_embd, n_head, block_size):
81
+ super().__init__()
82
+ self.ln_1 = nn.LayerNorm(n_embd)
83
+ self.attn = CausalSelfAttention(n_embd, n_head, block_size)
84
+ self.ln_2 = nn.LayerNorm(n_embd)
85
+ self.mlp = MLP(n_embd)
86
+ def forward(self, x):
87
+ x = x + self.attn(self.ln_1(x))
88
+ x = x + self.mlp(self.ln_2(x))
89
+ return x
90
+
91
+ class LUNAModel(nn.Module):
92
+ def __init__(self, vocab_size=50254, block_size=1024, n_layer=10, n_embd=768, n_head=12):
93
+ super().__init__()
94
+ self.block_size = block_size
95
+ self.wte = nn.Embedding(vocab_size, n_embd)
96
+ self.blocks = nn.ModuleList([Block(n_embd, n_head, block_size) for _ in range(n_layer)])
97
+ self.ln_f = nn.LayerNorm(n_embd)
98
+ self.lm_head = nn.Linear(n_embd, vocab_size, bias=False)
99
+ self.lm_head.weight = self.wte.weight
100
+ self.apply(self._init_weights)
101
+
102
+ def _init_weights(self, module):
103
+ if isinstance(module, (nn.Linear, nn.Embedding)):
104
+ module.weight.data.normal_(mean=0.0, std=0.02)
105
+ if isinstance(module, nn.Linear) and module.bias is not None:
106
+ module.bias.data.zero_()
107
+
108
+ def forward(self, idx, targets=None):
109
+ B, T = idx.size()
110
+ x = self.wte(idx)
111
+ for block in self.blocks:
112
+ x = block(x)
113
+ x = self.ln_f(x)
114
+ logits = self.lm_head(x)
115
+ loss = None
116
+ if targets is not None:
117
+ loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
118
+ return logits, loss
119
+
120
+ # ─── Config ────────────────────────────────────────────────────────────────────
121
+
122
+ DATASET_TOTAL_TOKENS = 4_515_286_950 # Verified from index.json: 270 chunks, sum of all dims
123
+ BLOCK_SIZE = 1024
124
+ VOCAB_SIZE = 50254
125
+ N_LAYER = 10
126
+ N_EMBD = 768
127
+ N_HEAD = 12
128
+ GLOBAL_BATCH_SIZE = 120
129
+ MAX_SEQ_LENGTH = 1024
130
+
131
+ WARMUP_STEPS = 3
132
+ BENCHMARK_STEPS = 4
133
+
134
+ # RunPod GPUs (Community Cloud pricing April 2026)
135
+ RUNPOD_GPUS = [
136
+ # (name, $/hr, VRAM_GB, bf16_TF_nonsparse, mem_bw_GBs, arch)
137
+ ("RTX A5000", 0.16, 24, 65, 768, "Ampere"),
138
+ ("RTX 3090", 0.22, 24, 71, 936, "Ampere"),
139
+ ("RTX A6000", 0.33, 48, 77, 768, "Ampere"),
140
+ ("RTX 4090", 0.34, 24, 165, 1008, "Ada"),
141
+ ("A40", 0.35, 48, 75, 696, "Ampere"),
142
+ ("L4", 0.44, 24, 121, 300, "Ada"),
143
+ ("RTX 5090", 0.69, 32, 210, 1792, "Blackwell"),
144
+ ("L40", 0.69, 48, 181, 864, "Ada"),
145
+ ("RTX 6000 Ada", 0.74, 48, 181, 960, "Ada"),
146
+ ("L40S", 0.79, 48, 183, 864, "Ada"),
147
+ ("A100 PCIe 80GB", 1.19, 80, 312, 2039, "Ampere"),
148
+ ("A100 SXM 80GB", 1.39, 80, 312, 2039, "Ampere"),
149
+ ("RTX Pro 6000", 1.69, 96, 260, 1280, "Blackwell"),
150
+ ("H100 PCIe", 1.99, 80, 756, 2039, "Hopper"),
151
+ ("H100 NVL", 2.59, 94, 835, 3938, "Hopper"),
152
+ ("H100 SXM", 2.69, 80, 990, 3352, "Hopper"),
153
+ ("H200", 3.59,141, 990, 4800, "Hopper"),
154
+ ]
155
+
156
+ USD_TO_INR = 86.0
157
+
158
+
159
+ def find_max_micro_batch(model, device, seq_len=1024, start=32):
160
+ """Binary search for max micro_batch_size, with 0.65 safety factor."""
161
+ model.train()
162
+ lo, hi, best = 1, start, 1
163
+ opt_tmp = torch.optim.AdamW(model.parameters(), lr=1e-4)
164
+
165
+ while hi >= lo:
166
+ mid = (lo + hi) // 2
167
+ try:
168
+ torch.cuda.empty_cache()
169
+ torch.cuda.reset_peak_memory_stats()
170
+ opt_tmp.zero_grad(set_to_none=True)
171
+ x = torch.randint(0, VOCAB_SIZE, (mid, seq_len), device=device)
172
+ t = torch.randint(0, VOCAB_SIZE, (mid, seq_len), device=device)
173
+ with autocast(device_type='cuda', dtype=torch.bfloat16):
174
+ _, loss = model(x, t)
175
+ loss.backward()
176
+ opt_tmp.step()
177
+ opt_tmp.zero_grad(set_to_none=True)
178
+ best = mid
179
+ lo = mid + 1
180
+ del x, t, loss
181
+ torch.cuda.empty_cache()
182
+ except (torch.cuda.OutOfMemoryError, RuntimeError):
183
+ try:
184
+ del x, t, loss
185
+ except:
186
+ pass
187
+ torch.cuda.empty_cache()
188
+ opt_tmp.zero_grad(set_to_none=True)
189
+ hi = mid - 1
190
+
191
+ safe = max(1, int(best * 0.65))
192
+ del opt_tmp
193
+ torch.cuda.empty_cache()
194
+ gc.collect()
195
+ return safe
196
+
197
+
198
+ def run_benchmark():
199
+ device = torch.device("cuda")
200
+ torch.backends.cuda.matmul.allow_tf32 = True
201
+ torch.backends.cudnn.allow_tf32 = True
202
+
203
+ print("=" * 72)
204
+ print(" LUNA 100M - TRAINING BENCHMARK & RUNPOD COST CALCULATOR")
205
+ print("=" * 72)
206
+
207
+ gpu_name = torch.cuda.get_device_name(0)
208
+ gpu_mem = torch.cuda.get_device_properties(0).total_memory / 1024**3
209
+ print(f"\n Local GPU: {gpu_name}")
210
+ print(f" VRAM: {gpu_mem:.1f} GB")
211
+ print(f" PyTorch: {torch.__version__}, CUDA: {torch.version.cuda}")
212
+
213
+ print(f"\n Creating LUNA-100M (SDPA/Flash Attention)...")
214
+ model = LUNAModel(VOCAB_SIZE, BLOCK_SIZE, N_LAYER, N_EMBD, N_HEAD).to(device)
215
+
216
+ total_params = sum(p.numel() for p in model.parameters())
217
+ # Tied embeddings: wte(50254*768) = 38,595,072 counted once
218
+ unique_params = total_params - model.wte.weight.numel()
219
+ print(f" Parameters: {total_params:,} total, {unique_params:,} unique")
220
+
221
+ print(f"\n Probing max micro_batch_size...")
222
+ max_mbs = find_max_micro_batch(model, device, MAX_SEQ_LENGTH, start=40)
223
+ print(f" Safe micro_batch_size: {max_mbs}")
224
+
225
+ grad_accum = max(1, GLOBAL_BATCH_SIZE // max_mbs)
226
+ effective_batch = max_mbs * grad_accum
227
+ tokens_per_step = effective_batch * MAX_SEQ_LENGTH
228
+ print(f" grad_accum={grad_accum}, effective_batch={effective_batch}")
229
+ print(f" Tokens/step: {tokens_per_step:,}")
230
+
231
+ optimizer = torch.optim.AdamW(
232
+ model.parameters(), lr=6e-4, weight_decay=0.1,
233
+ betas=(0.9, 0.95), eps=1e-8
234
+ )
235
+ scaler = GradScaler()
236
+
237
+ total_steps = WARMUP_STEPS + BENCHMARK_STEPS
238
+ print(f"\n Running {WARMUP_STEPS} warmup + {BENCHMARK_STEPS} benchmark steps...")
239
+
240
+ model.train()
241
+ step_times = []
242
+ torch.cuda.synchronize()
243
+ torch.cuda.reset_peak_memory_stats()
244
+
245
+ for step in range(total_steps):
246
+ t0 = time.perf_counter()
247
+ optimizer.zero_grad(set_to_none=True)
248
+ step_loss = 0.0
249
+
250
+ for _ in range(grad_accum):
251
+ x = torch.randint(0, VOCAB_SIZE, (max_mbs, MAX_SEQ_LENGTH), device=device)
252
+ tgt = torch.randint(0, VOCAB_SIZE, (max_mbs, MAX_SEQ_LENGTH), device=device)
253
+ with autocast(device_type='cuda', dtype=torch.bfloat16):
254
+ _, loss = model(x, tgt)
255
+ loss = loss / grad_accum
256
+ scaler.scale(loss).backward()
257
+ step_loss += loss.item()
258
+ del x, tgt, loss
259
+
260
+ scaler.unscale_(optimizer)
261
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
262
+ scaler.step(optimizer)
263
+ scaler.update()
264
+ torch.cuda.synchronize()
265
+ dt = time.perf_counter() - t0
266
+
267
+ if step >= WARMUP_STEPS:
268
+ step_times.append(dt)
269
+
270
+ tps = tokens_per_step / dt
271
+ phase = "WARM" if step < WARMUP_STEPS else "BENCH"
272
+ print(f" [{phase}] Step {step:3d} | Loss {step_loss:.4f} | {dt:.2f}s | {tps:,.0f} tok/s")
273
+
274
+ # ─── Results ──────────────────────────────────────────────────────────────
275
+ peak_vram_gb = torch.cuda.max_memory_allocated() / 1024**3
276
+
277
+ avg_time = sum(step_times) / len(step_times)
278
+ med_time = sorted(step_times)[len(step_times) // 2]
279
+ avg_tps = tokens_per_step / avg_time
280
+ med_tps = tokens_per_step / med_time
281
+ peak_tps = tokens_per_step / min(step_times)
282
+
283
+ flops_per_token = 6 * unique_params
284
+ achieved_tf = (avg_tps * flops_per_token) / 1e12
285
+ LOCAL_TF = 88.0 # RTX 4060 Ti BF16 non-sparse
286
+ LOCAL_BW = 288.0 # GB/s
287
+ mfu = achieved_tf / LOCAL_TF
288
+
289
+ print("\n" + "=" * 72)
290
+ print(" LOCAL BENCHMARK RESULTS")
291
+ print("=" * 72)
292
+ print(f" GPU: {gpu_name} ({gpu_mem:.1f} GB)")
293
+ print(f" Peak VRAM: {peak_vram_gb:.2f} GB ({peak_vram_gb/gpu_mem*100:.0f}%)")
294
+ print(f" Batch: micro={max_mbs}, accum={grad_accum}, global={effective_batch}")
295
+ print(f" Step time: avg={avg_time:.3f}s, median={med_time:.3f}s")
296
+ print(f" Tokens/sec: avg={avg_tps:,.0f}, median={med_tps:,.0f}, peak={peak_tps:,.0f}")
297
+ print(f" TFLOPS: {achieved_tf:.2f}, MFU: {mfu*100:.1f}%")
298
+
299
+ n_steps = math.ceil(DATASET_TOTAL_TOKENS / tokens_per_step)
300
+ local_hrs = (n_steps * avg_time) / 3600
301
+
302
+ print(f" Dataset: 4,515,286,950 tokens | Steps needed: {n_steps:,}")
303
+ print(f" Local training: {local_hrs:.1f} hrs ({local_hrs/24:.1f} days)")
304
+
305
+ # ─── RunPod Estimates ─────────────────────────────────────────────────────
306
+ print("\n" + "=" * 72)
307
+ print(" RUNPOD GPU COMPARISON (Community Cloud, INR/86/USD)")
308
+ print("=" * 72)
309
+
310
+ results = []
311
+ for name, price, vram, bf16, bw, arch in RUNPOD_GPUS:
312
+ # Memory estimation for each GPU
313
+ fixed_gb = (unique_params * (2 + 8 + 2)) / 1024**3 + 0.5
314
+ avail_gb = vram - fixed_gb
315
+ act_per_sample = max(0.05, (peak_vram_gb - fixed_gb) / max(max_mbs, 1))
316
+ est_mbs = max(1, min(128, int(avail_gb / act_per_sample)))
317
+ est_ga = max(1, GLOBAL_BATCH_SIZE // est_mbs)
318
+ est_tps_step = est_mbs * est_ga * MAX_SEQ_LENGTH
319
+
320
+ # Scaling: 50% compute + 50% bandwidth (validated for small transformers)
321
+ speedup = 0.50 * (bf16 / LOCAL_TF) + 0.50 * (bw / LOCAL_BW)
322
+ est_tps = avg_tps * speedup * 0.90 # 0.90 cloud overhead
323
+
324
+ est_steps = math.ceil(DATASET_TOTAL_TOKENS / est_tps_step)
325
+ est_sec = (est_tps_step / est_tps) * est_steps
326
+ est_hrs = est_sec / 3600
327
+ cost_usd = est_hrs * price
328
+ cost_inr = cost_usd * USD_TO_INR
329
+
330
+ results.append({
331
+ "gpu": name, "price": price, "vram": vram, "bf16": bf16,
332
+ "bw": bw, "arch": arch, "mbs": est_mbs, "ga": est_ga,
333
+ "tps": round(est_tps), "hours": round(est_hrs, 1),
334
+ "usd": round(cost_usd, 2), "inr": round(cost_inr),
335
+ })
336
+
337
+ results.sort(key=lambda r: r["inr"])
338
+
339
+ print(f"\n {'#':<3} {'GPU':<18} {'$/hr':>5} {'VRAM':>5} {'tok/s':>10} "
340
+ f"{'Hours':>7} {'$ USD':>8} {'INR':>10}")
341
+ print(" " + "─" * 72)
342
+ for i, r in enumerate(results):
343
+ s = " *" if i < 3 else ""
344
+ print(f" {i+1:<3} {r['gpu']:<18} {r['price']:>5.2f} {r['vram']:>4}G "
345
+ f"{r['tps']:>10,} {r['hours']:>7.1f} {r['usd']:>8.2f} {r['inr']:>10,}{s}")
346
+
347
+ # Top 5 detailed
348
+ print("\n" + "=" * 72)
349
+ print(" TOP 5 CHEAPEST - DETAILS")
350
+ print("=" * 72)
351
+ for i, r in enumerate(results[:5]):
352
+ sx = r["tps"] / avg_tps if avg_tps > 0 else 0
353
+ print(f"\n #{i+1}: {r['gpu']} ({r['arch']})")
354
+ print(f" β”œβ”€β”€ ${r['price']:.2f}/hr | {r['vram']}GB VRAM | {r['bf16']} TF | {r['bw']} GB/s")
355
+ print(f" β”œβ”€β”€ micro_batch: {r['mbs']}, grad_accum: {r['ga']}")
356
+ print(f" β”œβ”€β”€ {r['tps']:,} tok/s ({sx:.2f}Γ— local)")
357
+ print(f" β”œβ”€β”€ {r['hours']:.1f} hrs ({r['hours']/24:.1f} days)")
358
+ print(f" +-- ${r['usd']:.2f} = INR {r['inr']:,}")
359
+
360
+ # Local reference
361
+ print("\n" + "=" * 72)
362
+ print(" YOUR LOCAL GPU")
363
+ print("=" * 72)
364
+ print(f" RTX 4060 Ti 16GB: {avg_tps:,.0f} tok/s")
365
+ print(f" Training: {local_hrs:.1f} hrs ({local_hrs/24:.1f} days)")
366
+ print(f" Electricity: ~INR {local_hrs * 0.16 * 8:,.0f} (160W x Rs8/kWh)")
367
+
368
+ # Recommendation
369
+ best = results[0]
370
+ print("\n" + "=" * 72)
371
+ print(" * RECOMMENDATION")
372
+ print("=" * 72)
373
+ print(f" Most affordable: {best['gpu']} @ ${best['price']:.2f}/hr")
374
+ print(f" Time: {best['hours']:.1f} hrs ({best['hours']/24:.1f} days)")
375
+ print(f" Cost: INR {best['inr']:,} (${best['usd']:.2f})")
376
+ print(f" Speed: {best['tps']/avg_tps:.1f}Γ— local" if avg_tps > 0 else "")
377
+
378
+ fast = [r for r in results if r["hours"] < max(8, local_hrs * 0.15)]
379
+ if fast:
380
+ fb = min(fast, key=lambda r: r["inr"])
381
+ if fb["gpu"] != best["gpu"]:
382
+ print(f"\n Fastest affordable: {fb['gpu']} @ ${fb['price']:.2f}/hr")
383
+ print(f" Time: {fb['hours']:.1f} hrs | Cost: INR {fb['inr']:,}")
384
+
385
+ print("\n" + "=" * 72)
386
+
387
+ # Save JSON
388
+ out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "runpod_cost_estimate.json")
389
+ with open(out, "w") as f:
390
+ json.dump({
391
+ "benchmark": {
392
+ "gpu": gpu_name, "vram_gb": round(gpu_mem, 1),
393
+ "peak_vram_gb": round(peak_vram_gb, 2),
394
+ "micro_batch": max_mbs, "grad_accum": grad_accum,
395
+ "tokens_per_step": tokens_per_step,
396
+ "avg_tok_per_sec": round(avg_tps),
397
+ "median_tok_per_sec": round(med_tps),
398
+ "achieved_tflops": round(achieved_tf, 2),
399
+ "mfu_pct": round(mfu*100, 1),
400
+ },
401
+ "dataset": {"tokens": DATASET_TOTAL_TOKENS, "chunks": 270},
402
+ "model": {"total_params": total_params, "unique_params": unique_params},
403
+ "local_hours": round(local_hrs, 1),
404
+ "runpod": results,
405
+ "usd_to_inr": USD_TO_INR,
406
+ }, f, indent=2)
407
+ print(f" Saved: {out}")
408
+ print("=" * 72)
409
+
410
+
411
+ if __name__ == "__main__":
412
+ run_benchmark()