spec100m / profile_train_speed.py
Akahsizrr's picture
squash to reclaim LFS quota
a8f07a3
Raw History Blame Contribute Delete
5.12 kB
"""Find the optimal training config: batch, seq, and fused CE.
The bottleneck is the logits tensor: [B, T, 50257] = 1.6 GB at batch=16, seq=1024.
The cross-entropy + backward through this is expensive.
Optimizations to test:
1. Larger batch + shorter seq (amortize weight reads)
2. Longer seq + smaller batch (more tokens per forward)
3. Fused cross-entropy (avoid materializing full logits)
4. Gradient checkpointing (trade compute for memory, fit bigger batches)
"""
import time
import torch
import torch.nn.functional as F
from config import Config
from model import build_model
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
cfg = Config.v5_500m()
model = build_model(cfg, "cuda")
model.train()
# Freeze medusa
for name, param in model.named_parameters():
if name.startswith("medusa_"):
param.requires_grad = False
opt = torch.optim.AdamW(
[p for p in model.parameters() if p.requires_grad],
lr=3e-4, betas=(0.9, 0.95), weight_decay=0.1, fused=True)
print(f"Model: {sum(p.numel() for p in model.parameters())/1e6:.1f}M params")
print(f"Trainable: {sum(p.numel() for p in model.parameters() if p.requires_grad)/1e6:.1f}M")
print()
# --- Standard CE vs Fused CE ---
def standard_ce(model, ids, cfg):
h = model(ids)
logits = model.lm_head(h)
loss = F.cross_entropy(
logits[:, :-1].reshape(-1, cfg.vocab_size),
ids[:, 1:].reshape(-1))
return loss
def fused_ce(model, ids, cfg):
"""Fused CE: use F.linear + F.cross_entropy without materializing full logits.
Actually PyTorch's F.cross_entropy already fuses the softmax + NLL.
The issue is the [B*T, V] logits tensor. We can't avoid it with standard CE.
But we CAN use the 'chunked' approach: compute CE in vocab chunks.
This trades compute for memory, letting us use bigger batches.
"""
h = model(ids) # [B, T, d]
# Tied embeddings: lm_head weight = tok_emb weight = [V, d]
weight = model.tok_emb.weight # [V, d]
# Reshape for CE: h_flat [B*T, d], targets [B*T]
h_flat = h[:, :-1].reshape(-1, cfg.d_model)
targets = ids[:, 1:].reshape(-1)
# F.cross_entropy with weight = compute logits on the fly
# Actually, F.cross_entropy(input, target) where input is [N, C]
# still materializes [N, C] logits.
# The real fused version: use torch's internal fused kernel
logits = F.linear(h_flat, weight) # [B*T, V]
loss = F.cross_entropy(logits, targets)
return loss
def chunked_ce(model, ids, cfg, chunk_size=8192):
"""Chunked CE: compute loss in chunks to save memory.
Allows much larger batch sizes.
"""
h = model(ids) # [B, T, d]
weight = model.tok_emb.weight # [V, d]
h_flat = h[:, :-1].reshape(-1, cfg.d_model) # [B*T, d]
targets = ids[:, 1:].reshape(-1) # [B*T]
N = h_flat.shape[0]
total_loss = 0.0
for i in range(0, N, chunk_size):
chunk_h = h_flat[i:i+chunk_size]
chunk_t = targets[i:i+chunk_size]
chunk_logits = F.linear(chunk_h, weight)
total_loss = total_loss + F.cross_entropy(chunk_logits, chunk_t) * chunk_h.shape[0]
return total_loss / N
# --- Test configs ---
configs = [
# (batch, seq, ce_fn, label)
(16, 1024, standard_ce, "standard CE"),
(16, 1024, fused_ce, "fused CE (same speed)"),
(16, 1024, chunked_ce, "chunked CE (8k)"),
(32, 1024, standard_ce, "batch=32"),
(32, 1024, chunked_ce, "batch=32 chunked"),
(64, 1024, chunked_ce, "batch=64 chunked"),
(16, 2048, standard_ce, "seq=2048"),
(16, 2048, chunked_ce, "seq=2048 chunked"),
(32, 2048, chunked_ce, "batch=32 seq=2048 chunked"),
(8, 4096, chunked_ce, "seq=4096 chunked"),
(16, 4096, chunked_ce, "batch=16 seq=4096 chunked"),
]
print(f"{'config':>30} {'tokens':>8} {'step(ms)':>9} {'tok/s':>10} {'VRAM(GB)':>9}")
print("-" * 70)
for batch, seq, ce_fn, label in configs:
try:
ids = torch.randint(0, cfg.vocab_size, (batch, seq), device="cuda")
# Warmup
for _ in range(3):
opt.zero_grad(set_to_none=True)
loss = ce_fn(model, ids, cfg)
loss.backward()
opt.step()
torch.cuda.synchronize()
# Measure
torch.cuda.reset_peak_memory_stats()
times = []
for _ in range(5):
torch.cuda.synchronize()
t0 = time.perf_counter()
opt.zero_grad(set_to_none=True)
loss = ce_fn(model, ids, cfg)
loss.backward()
opt.step()
torch.cuda.synchronize()
times.append(time.perf_counter() - t0)
times.sort()
step_ms = times[2] * 1000
tokens = batch * seq
tok_s = tokens / (step_ms / 1000)
vram = torch.cuda.max_memory_allocated() / 1e9
print(f"{label:>30} {tokens:>8} {step_ms:>9.1f} {tok_s:>10,.0f} {vram:>9.2f}")
except RuntimeError as e:
if "out of memory" in str(e).lower():
print(f"{label:>30} {batch*seq:>8} {'OOM':>9}")
torch.cuda.empty_cache()
else:
raise