nanogpt-tr-v5-code / sft_03_train.py
musabc's picture
Upload sft_03_train.py with huggingface_hub
9c6ee46 verified
Raw
History Blame Contribute Delete
18.4 kB
"""
V5-SFT Asama 3: Fine-tune base model on tokenized SFT data with loss masking.
Pipeline:
1) Base ckpt yukle (runs/tr-200m-v5/best_ckpt.pt veya HF)
2) sft_{train,val}_{tokens,mask}.bin oku
3) Random sliding-window sample, mask-aware CE loss
4) Cosine LR decay, dusuk LR (base/10)
5) Her N step val + sample generation
6) Her N step HF push (musabc/nanogpt-tr-v5-sft)
Kullanim:
python sft_03_train.py --base runs/tr-200m-v5/best_ckpt.pt
python sft_03_train.py --base runs/tr-200m-v5/best_ckpt.pt --resume
python sft_03_train.py --base runs/tr-200m-v5/best_ckpt.pt --hf-user musabc --hf-push-every 200
"""
import argparse
import json
import math
import os
import random
import sys
import time
from pathlib import Path
# Liger CE training'de logits'i in-place modifiye ettigi icin SFT'de devre disi
# Bu IMPORT'tan ONCE set edilmeli
os.environ["NANOGPT_NO_LIGER"] = "1"
import numpy as np
import torch
import torch.nn.functional as F
# ===== Hyperparams =====
BATCH_SIZE = 16 # micro-batch per forward
GRAD_ACCUM_STEPS = 4 # effective batch = 64
BLOCK_SIZE = 2048
# Lower LR than base — typical SFT
MUON_LR = 2.2e-3 / 10
ADAMW_LR = 3.5e-4 / 10
MUON_MOMENTUM = 0.95
WEIGHT_DECAY = 0.1
WARMUP_STEPS = 50
NUM_EPOCHS = 3
EVAL_EVERY = 100
HF_PUSH_EVERY = 200
SAMPLE_EVERY = 200
MAX_GRAD_NORM = 1.0
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
DTYPE = torch.bfloat16
def log(msg):
print(msg, flush=True)
# ===== Data =====
class SFTPackedDataset:
"""Packed bin'den random sliding window sample. Mask-aware."""
def __init__(self, tokens_path: Path, mask_path: Path, block_size: int,
batch_size: int):
self.tokens = np.memmap(tokens_path, dtype=np.uint16, mode="r")
self.mask = np.memmap(mask_path, dtype=np.uint8, mode="r")
assert len(self.tokens) == len(self.mask), \
f"Token/mask uzunluk uyusmazlik: {len(self.tokens)} vs {len(self.mask)}"
self.block_size = block_size
self.batch_size = batch_size
self.length = len(self.tokens)
log(f" Dataset: {self.length:,} token "
f"(loss tokens: {int(self.mask.sum()):,})")
def get_batch(self, rng: np.random.Generator):
"""Random sliding window batches. Tek window'da mask hep 0 olabilir,
bu durumda loss zero — kabul edilir (random pure-prompt slice)."""
bs, T = self.batch_size, self.block_size
# T+1 token gerekli (next-token prediction)
ix = rng.integers(0, self.length - T - 1, size=bs)
x = np.zeros((bs, T), dtype=np.int64)
y = np.zeros((bs, T), dtype=np.int64)
m = np.zeros((bs, T), dtype=np.float32)
for i, start in enumerate(ix):
x[i] = self.tokens[start:start+T].astype(np.int64)
y[i] = self.tokens[start+1:start+T+1].astype(np.int64)
# Mask y'ye gore — y[t] = next-token, mask[start+1:start+T+1] uygulanir
m[i] = self.mask[start+1:start+T+1].astype(np.float32)
x = torch.from_numpy(x).to(DEVICE, non_blocking=True)
y = torch.from_numpy(y).to(DEVICE, non_blocking=True)
m = torch.from_numpy(m).to(DEVICE, non_blocking=True)
return x, y, m
# ===== Loss =====
def masked_ce_loss(logits: torch.Tensor, targets: torch.Tensor,
mask: torch.Tensor, softcap: float = 30.0) -> torch.Tensor:
"""Mask-aware cross-entropy. mask=0 olan tokenlar loss'a katilmaz."""
# logits: (B, T, V) targets: (B, T) mask: (B, T) float
B, T, V = logits.shape
if softcap > 0:
logits = softcap * torch.tanh(logits / softcap)
# Flatten
flat_logits = logits.view(-1, V)
flat_targets = targets.view(-1)
flat_mask = mask.view(-1)
# Per-token loss (no reduction)
losses = F.cross_entropy(flat_logits, flat_targets, reduction="none")
# Mask uygula
masked_losses = losses * flat_mask
# Ortalama (sadece loss var olan tokenlar uzerinden)
total_mask = flat_mask.sum()
if total_mask < 1:
return torch.zeros((), device=logits.device, requires_grad=True)
return masked_losses.sum() / total_mask
# ===== LR Schedule =====
def get_lr(step: int, total_steps: int, warmup_steps: int,
base_lr: float, min_lr_ratio: float = 0.01) -> float:
if step < warmup_steps:
return base_lr * step / max(warmup_steps, 1)
progress = (step - warmup_steps) / max(total_steps - warmup_steps, 1)
progress = min(1.0, progress)
cosine = 0.5 * (1 + math.cos(math.pi * progress))
return base_lr * (min_lr_ratio + (1 - min_lr_ratio) * cosine)
# ===== Optimizer setup (Muon + AdamW split) =====
def setup_optimizers(model):
try:
from torch.optim import Muon as TorchMuon
has_native = True
except ImportError:
from muon import Muon as TorchMuon
has_native = False
muon_params = []
adam_params = []
for name, p in model.named_parameters():
if not p.requires_grad:
continue
# Embedding ve lm_head AdamW'da
if "wte" in name or "lm_head" in name:
adam_params.append(p)
# 1D parametreler AdamW'da
elif p.ndim < 2:
adam_params.append(p)
# 2D matrisler (linear/attention) Muon'da
else:
muon_params.append(p)
log(f" Muon params: {sum(p.numel() for p in muon_params)/1e6:.2f}M "
f"({len(muon_params)} tensor)")
log(f" AdamW params: {sum(p.numel() for p in adam_params)/1e6:.2f}M "
f"({len(adam_params)} tensor)")
muon_kwargs = dict(lr=MUON_LR, momentum=MUON_MOMENTUM, nesterov=True, ns_steps=3)
if has_native:
muon_kwargs["weight_decay"] = 0.0
opt_muon = TorchMuon(muon_params, **muon_kwargs)
opt_adam = torch.optim.AdamW(adam_params, lr=ADAMW_LR,
betas=(0.9, 0.95),
weight_decay=WEIGHT_DECAY)
return opt_muon, opt_adam
# ===== Sample =====
def sample_test(model, tokenizer, prompt_text: str, max_new: int = 200):
model.eval()
from tokenizers import Tokenizer
if isinstance(tokenizer, str):
tokenizer = Tokenizer.from_file(tokenizer)
ids = tokenizer.encode(prompt_text).ids
x = torch.tensor([ids], dtype=torch.long, device=DEVICE)
with torch.no_grad():
with torch.amp.autocast(device_type="cuda", dtype=DTYPE):
out = model.generate(x, max_new_tokens=max_new,
temperature=0.3, top_k=40,
repetition_penalty=1.0,
no_repeat_ngram_size=0,
eos_token_id=0) # <|endoftext|>
text = tokenizer.decode(out[0].tolist())
model.train()
return text
# ===== Main =====
def main():
global BATCH_SIZE, GRAD_ACCUM_STEPS
parser = argparse.ArgumentParser()
parser.add_argument("--base", type=str,
default="runs/tr-200m-v5/best_ckpt.pt",
help="Base ckpt yolu")
parser.add_argument("--data-dir", type=str, default="data/sft")
parser.add_argument("--tokenizer", type=str,
default="data/tokenizer-tr-v5.json")
parser.add_argument("--out-dir", type=str,
default="runs/tr-200m-v5-sft")
parser.add_argument("--resume", action="store_true")
parser.add_argument("--batch", type=int, default=BATCH_SIZE)
parser.add_argument("--grad-accum", type=int, default=GRAD_ACCUM_STEPS)
parser.add_argument("--epochs", type=int, default=NUM_EPOCHS)
parser.add_argument("--hf-user", type=str, default=None,
help="HF user, ornek: musabc. Bos -> push yapilmaz")
parser.add_argument("--hf-push-every", type=int, default=HF_PUSH_EVERY)
parser.add_argument("--no-compile", action="store_true")
parser.add_argument("--compile-mode", type=str,
default="max-autotune-no-cudagraphs")
args = parser.parse_args()
BATCH_SIZE = args.batch
GRAD_ACCUM_STEPS = args.grad_accum
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
log(f"Device: {DEVICE} dtype: {DTYPE}")
log(f"Batch: {BATCH_SIZE} x grad_accum {GRAD_ACCUM_STEPS} "
f"= effective {BATCH_SIZE*GRAD_ACCUM_STEPS}")
# ===== Data =====
data_dir = Path(args.data_dir)
log(f"\nData: {data_dir}")
train_ds = SFTPackedDataset(
data_dir / "sft_train_tokens.bin",
data_dir / "sft_train_mask.bin",
BLOCK_SIZE, BATCH_SIZE
)
val_ds = SFTPackedDataset(
data_dir / "sft_val_tokens.bin",
data_dir / "sft_val_mask.bin",
BLOCK_SIZE, BATCH_SIZE
)
# Toplam step = (train_tokens / tokens_per_step) * epochs
tokens_per_step = BATCH_SIZE * GRAD_ACCUM_STEPS * BLOCK_SIZE
total_loss_tokens = int(train_ds.mask.sum())
# Bir "epoch" yaklasik loss-token bazli
steps_per_epoch = max(1, total_loss_tokens // (BATCH_SIZE * GRAD_ACCUM_STEPS *
BLOCK_SIZE // 10))
# Daha basit: pack uzunlugu / tokens_per_step
steps_per_epoch = max(1, train_ds.length // tokens_per_step)
total_steps = steps_per_epoch * args.epochs
log(f" Steps/epoch: {steps_per_epoch} | total steps: {total_steps}")
log(f" Tokens/step: {tokens_per_step:,}")
# ===== Model =====
log(f"\nModel yukleniyor: {args.base}")
from model_v5 import GPTV5, GPTConfigV5
cfg = GPTConfigV5()
model = GPTV5(cfg).to(DEVICE)
ckpt = torch.load(args.base, map_location=DEVICE, weights_only=False)
if "model" in ckpt:
sd = ckpt["model"]
log(f" Base step: {ckpt.get('step', '?')} best_val: "
f"{ckpt.get('best_val', '?')}")
else:
sd = ckpt
# _orig_mod. prefix'i kaldir (compile ile kaydedilmis ise)
has_prefix = any(k.startswith("_orig_mod.") for k in sd.keys())
if has_prefix:
log(f" ! _orig_mod. prefix tespit edildi, kaldiriliyor")
sd = {k.replace("_orig_mod.", "", 1): v for k, v in sd.items()}
missing, unexpected = model.load_state_dict(sd, strict=False)
log(f" Yuklendi: {len(sd) - len(unexpected)}/{len(sd)} key")
if missing:
log(f" ! Missing keys ({len(missing)}): {missing[:3]}...")
if unexpected:
log(f" ! Unexpected keys ({len(unexpected)}): {unexpected[:3]}...")
log(f" Params: {sum(p.numel() for p in model.parameters())/1e6:.1f}M")
# Sanity check — random tokens uzerinde forward, ortalama top-1 olasilik
with torch.no_grad():
with torch.amp.autocast(device_type="cuda", dtype=DTYPE):
test_x = torch.randint(0, cfg.vocab_size, (2, 64), device=DEVICE)
test_logits, _ = model(test_x, test_x)
probs = F.softmax(test_logits[0, 0].float(), dim=-1)
top1_p = probs.max().item()
entropy = -(probs * torch.log(probs + 1e-12)).sum().item()
log(f" Sanity: top1_p={top1_p:.4f}, entropy={entropy:.3f} "
f"(uniform ~= {math.log(cfg.vocab_size):.2f}, trained <~ 5)")
# Resume?
start_step = 0
sft_latest = out_dir / "sft_latest.pt"
if args.resume and sft_latest.exists():
log(f"\nSFT resume: {sft_latest}")
sft_ckpt = torch.load(sft_latest, map_location=DEVICE, weights_only=False)
model.load_state_dict(sft_ckpt["model"])
start_step = sft_ckpt.get("step", 0)
log(f" Resume step: {start_step}")
# ===== Optimizers =====
log(f"\nOptimizer setup:")
opt_muon, opt_adam = setup_optimizers(model)
# Compile
if not args.no_compile:
log(f"\ntorch.compile (mode={args.compile_mode})...")
model = torch.compile(model, mode=args.compile_mode, dynamic=False)
torch.backends.cudnn.benchmark = True
torch.set_float32_matmul_precision("high")
# ===== Training =====
log(f"\nSFT egitim basliyor: {start_step} -> {total_steps}\n")
rng = np.random.default_rng(42 + start_step)
t_start = time.time()
accum_loss = 0.0
accum_count = 0
best_val_loss = float("inf")
log_f = open(out_dir / "sft_train.log", "a", encoding="utf-8")
model.train()
for step in range(start_step, total_steps):
# LR update
muon_lr = get_lr(step, total_steps, WARMUP_STEPS, MUON_LR)
adam_lr = get_lr(step, total_steps, WARMUP_STEPS, ADAMW_LR)
for g in opt_muon.param_groups:
g["lr"] = muon_lr
for g in opt_adam.param_groups:
g["lr"] = adam_lr
opt_muon.zero_grad(set_to_none=True)
opt_adam.zero_grad(set_to_none=True)
step_loss = 0.0
valid_micro = 0
for _ in range(GRAD_ACCUM_STEPS):
x, y, m = train_ds.get_batch(rng)
with torch.amp.autocast(device_type="cuda", dtype=DTYPE):
# targets=y geciyoruz → full (B,T,V) logits + softcap uygulanmis
# Donen loss'u atip kendi mask'li CE'mizi hesapliyoruz
logits, _ = model(x, y)
loss = masked_ce_loss(logits, y, m, softcap=0.0)
if torch.isnan(loss) or loss.item() == 0.0:
continue
(loss / GRAD_ACCUM_STEPS).backward()
step_loss += loss.item()
valid_micro += 1
if valid_micro == 0:
continue
step_loss /= valid_micro
# Grad clip
torch.nn.utils.clip_grad_norm_(
[p for g in opt_muon.param_groups for p in g["params"]] +
[p for g in opt_adam.param_groups for p in g["params"]],
MAX_GRAD_NORM,
)
opt_muon.step()
opt_adam.step()
accum_loss += step_loss
accum_count += 1
# Log
if (step + 1) % 10 == 0:
avg = accum_loss / accum_count
elapsed_min = (time.time() - t_start) / 60
tok_per_sec = (step - start_step + 1) * tokens_per_step / max(time.time() - t_start, 1)
line = (f"step {step+1:>5} | loss {avg:.4f} | "
f"muon {muon_lr:.2e} adam {adam_lr:.2e} | "
f"{tok_per_sec/1e3:.0f}K tok/s | {elapsed_min:.1f}m")
log(line)
log_f.write(line + "\n"); log_f.flush()
accum_loss = 0.0
accum_count = 0
# Eval
if (step + 1) % EVAL_EVERY == 0:
model.eval()
val_losses = []
val_rng = np.random.default_rng(0)
with torch.no_grad():
with torch.amp.autocast(device_type="cuda", dtype=DTYPE):
for _ in range(20):
x, y, m = val_ds.get_batch(val_rng)
logits, _ = model(x, y)
vl = masked_ce_loss(logits, y, m, softcap=0.0)
if not torch.isnan(vl) and vl.item() > 0:
val_losses.append(vl.item())
model.train()
avg_val = sum(val_losses) / max(len(val_losses), 1)
line = f" >>> EVAL: val {avg_val:.4f} ({len(val_losses)} batch)"
log(line); log_f.write(line + "\n"); log_f.flush()
if avg_val < best_val_loss and avg_val > 0:
best_val_loss = avg_val
# Save best — model._orig_mod (compile sonra)
mdl = model._orig_mod if hasattr(model, "_orig_mod") else model
torch.save({
"model": mdl.state_dict(),
"step": step + 1,
"best_val": best_val_loss,
}, out_dir / "sft_best.pt")
line = f" >>> BEST kaydedildi (val {best_val_loss:.4f})"
log(line); log_f.write(line + "\n"); log_f.flush()
# Sample
if (step + 1) % SAMPLE_EVERY == 0:
try:
mdl = model._orig_mod if hasattr(model, "_orig_mod") else model
prompts = [
# Tarif (training'de turkish_recipes 4K)
"### Kullanici:\nMercimek corbasi tarifi ver.\n### Asistan:\n",
# Factual short (turkish_exam, knowledge)
"### Kullanici:\nTurkiye'nin baskenti neresidir?\n### Asistan:\n",
# Aciklayici (general)
"### Kullanici:\nBir e-mail nasil yazilir? Kisa anlat.\n### Asistan:\n",
# Simple math (gsm8k/metamath)
"### Kullanici:\nAhmet'in 5 elmasi var, 2 tane yer. Kac kaldi?\n### Asistan:\n",
]
for p in prompts:
out = sample_test(mdl, args.tokenizer, p, max_new=120)
# Sadece asistan kismini bul
if "### Asistan:" in out:
asst = out.split("### Asistan:", 1)[1].strip()
asst = asst.split("<|endoftext|>", 1)[0].strip()
else:
asst = out
line = f" [sample] {asst[:220]!r}"
log(line); log_f.write(line + "\n"); log_f.flush()
except Exception as e:
log(f" [sample err] {e}")
# Checkpoint + HF push
if (step + 1) % 100 == 0:
mdl = model._orig_mod if hasattr(model, "_orig_mod") else model
torch.save({
"model": mdl.state_dict(),
"step": step + 1,
"best_val": best_val_loss,
}, out_dir / "sft_latest.pt")
if args.hf_user and (step + 1) % args.hf_push_every == 0:
try:
from huggingface_hub import HfApi
api = HfApi()
repo = f"{args.hf_user}/nanogpt-tr-v5-sft"
api.create_repo(repo, repo_type="model", exist_ok=True, private=False)
for fname in ["sft_latest.pt", "sft_best.pt", "sft_train.log"]:
p = out_dir / fname
if p.exists():
api.upload_file(
path_or_fileobj=str(p),
path_in_repo=fname,
repo_id=repo,
repo_type="model",
)
log(f" >>> HF push: {repo} (step {step+1})")
except Exception as e:
log(f" [hf push err] {e}")
log_f.close()
log(f"\nSFT tamamlandi. Best val: {best_val_loss:.4f}")
if __name__ == "__main__":
main()