File size: 11,525 Bytes
c6f3061 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | """Resumable GPU training with fixed validation masks and atomic recovery state."""
from contextlib import nullcontext
from dataclasses import asdict
import json
import math
from pathlib import Path
import shutil
import time
import numpy as np
import torch
from torch.nn import functional as F
from .data import load_arrays
from .model import Config, Denoiser, PAD, BOS, corrupt
def amp(device, precision):
return torch.autocast("cuda", dtype=torch.bfloat16) if device == "cuda" and precision == "bf16" else nullcontext()
def atomic_save(value, path):
temporary = path.with_suffix(".tmp")
torch.save(value, temporary)
temporary.replace(path)
@torch.inference_mode()
def evaluate(model, array, frequencies, *, device, precision, limit=256, batch_size=8):
model.eval()
reports = []
for rate in (0.15, 0.5, 0.85):
rng = torch.Generator(device=device).manual_seed(2718)
total = correct = top5 = 0
loss = baseline_loss = 0.
baseline_correct = 0
for start in range(0, min(limit, len(array)), batch_size):
clean = torch.tensor(np.array(array[start:min(start+batch_size,limit)]), dtype=torch.long, device=device)
noisy, selected, noise = corrupt(clean, rng, mode="random", rate=rate)
with amp(device, precision):
logits = model(noisy, noise, selected)
targets = clean[selected]
logits = logits.float()
loss += F.cross_entropy(logits, targets, reduction="sum").item()
correct += logits.argmax(-1).eq(targets).sum().item()
top5 += logits.topk(5, -1).indices.eq(targets[:, None]).any(-1).sum().item()
total += targets.numel()
baseline_loss += -frequencies.to(device)[targets].log().sum().item()
baseline_correct += targets.eq(int(frequencies.argmax())).sum().item()
reports.append({"mask_rate": rate, "targets": total, "cross_entropy": loss / total,
"accuracy": correct / total, "top5_accuracy": top5 / total,
"unigram_cross_entropy": baseline_loss / total, "unigram_accuracy": baseline_correct / total})
return reports
def train(args):
if min(args.steps, args.batch_size, args.accumulation, args.eval_every, args.threads, args.eval_samples) < 1:
raise ValueError("Training counts must be positive")
torch.set_num_threads(args.threads)
torch.manual_seed(args.seed)
device = args.device
if device == "cuda" and not torch.cuda.is_available():
raise ValueError("CUDA/ROCm unavailable; choose --device cpu")
if args.precision == "bf16" and device == "cuda" and not torch.cuda.is_bf16_supported():
raise ValueError("BF16 unsupported; choose --precision fp32")
arrays, manifest = load_arrays(args.data)
config = Config(vocab_size=manifest["vocab_size"], width=args.width, heads=args.heads, layers=args.layers,
max_length=manifest["max_length"])
out = Path(args.output)
out.mkdir(parents=True, exist_ok=True)
if (out / "last.pt").exists() and not args.resume:
raise ValueError("Run already exists; use --resume or a new output directory")
run_config = {k: v for k, v in vars(args).items() if k not in ("resume", "command")}
model = Denoiser(config).to(device)
lineage = None
if getattr(args, "initialize_from", None):
if args.resume:
raise ValueError("Choose resume OR initialize-from")
source = Path(args.initialize_from)
lineage = json.loads((source / "training_info.json").read_text())
if lineage["manifest"] != manifest or lineage.get("pretrained_weights") is not False:
raise ValueError("Initialization must use this corpus and a from-scratch DiffuThink checkpoint")
prior = Denoiser.from_pretrained(source)
if prior.config != config:
raise ValueError("Initialization model configuration differs")
model.load_state_dict(prior.state_dict())
del prior
optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, betas=(0.9, 0.95), weight_decay=0.1)
sampling = torch.Generator().manual_seed(args.seed)
noise_rng = torch.Generator(device=device).manual_seed(args.seed + 1)
counts = np.bincount(np.asarray(arrays["train"]).reshape(-1), minlength=config.vocab_size).astype(np.float64) + 1
counts[[PAD, BOS]] = 1
frequencies = torch.tensor(counts / counts.sum(), dtype=torch.float32)
step, seen_tokens, elapsed, best = 0, 0, 0., float("inf")
if args.resume:
if not (out / "best" / "model.safetensors").exists():
raise ValueError("Resume in the original output directory containing best/ and last.pt")
state = torch.load(args.resume, map_location="cpu", weights_only=True)
if state["manifest"] != manifest or state["model_config"] != asdict(config):
raise ValueError("Resume data/model mismatch")
for key in ("seed", "batch_size", "accumulation", "lr", "steps", "warmup", "precision", "device"):
if state["run_config"][key] != run_config[key]:
raise ValueError(f"Resume requires unchanged {key}; schedule must remain identical")
model.load_state_dict(state["model"])
optimizer.load_state_dict(state["optimizer"])
sampling.set_state(state["sampling_rng"])
noise_rng.set_state(state["noise_rng"])
torch.set_rng_state(state["torch_rng"])
if device == "cuda":
torch.cuda.set_rng_state(state["cuda_rng"], device)
step, seen_tokens, elapsed, best = state["step"], state["seen_tokens"], state["elapsed"], state["best"]
lineage = state.get("lineage")
del state
else:
initial = evaluate(model, arrays["validation"], frequencies, device=device, precision=args.precision, limit=args.eval_samples)
(out / "initial_validation.json").write_text(json.dumps(initial, indent=2), encoding="utf-8")
(out / "run_config.json").write_text(json.dumps(run_config, indent=2), encoding="utf-8")
(out / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
start = time.perf_counter()
parameters = sum(p.numel() for p in model.parameters())
print(json.dumps({"parameters": parameters, "device": device, "precision": args.precision, "start_step": step, "target_steps": args.steps}), flush=True)
log = out / "metrics.jsonl"
def save_state():
atomic_save({"model": model.state_dict(), "optimizer": optimizer.state_dict(),
"model_config": asdict(config), "manifest": manifest, "run_config": run_config,
"step": step, "seen_tokens": seen_tokens, "elapsed": elapsed + time.perf_counter() - start,
"best": best, "sampling_rng": sampling.get_state(), "noise_rng": noise_rng.get_state(),
"lineage": lineage,
"torch_rng": torch.get_rng_state(),
"cuda_rng": torch.cuda.get_rng_state() if device == "cuda" else None}, out / "last.pt")
try:
while step < args.steps:
model.train()
lr_scale = min(1., (step + 1) / max(1, args.warmup))
progress = max(0., (step - args.warmup) / max(1, args.steps - args.warmup))
lr = args.lr * lr_scale * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * progress)))
for group in optimizer.param_groups:
group["lr"] = lr
optimizer.zero_grad(set_to_none=True)
loss_value = 0.
for _ in range(args.accumulation):
indices = torch.randint(len(arrays["train"]), (args.batch_size,), generator=sampling).numpy()
clean = torch.tensor(np.array(arrays["train"][indices]), dtype=torch.long, device=device)
noisy, selected, noise = corrupt(clean, noise_rng)
with amp(device, args.precision):
logits = model(noisy, noise, selected)
loss = F.cross_entropy(logits.float(), clean[selected])
if not torch.isfinite(loss):
raise RuntimeError("Non-finite loss; previous recovery checkpoint is preserved")
(loss / args.accumulation).backward()
loss_value += loss.item() / args.accumulation
seen_tokens += int(clean.ne(PAD).sum())
gradient = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.)
if not torch.isfinite(gradient):
raise RuntimeError("Non-finite gradient; previous checkpoint is preserved")
optimizer.step()
step += 1
if step == 1 or step % 25 == 0:
wall = elapsed + time.perf_counter() - start
row = {"step": step, "loss": loss_value, "lr": lr, "gradient_norm": float(gradient),
"seen_tokens": seen_tokens, "seconds": wall, "tokens_per_second": seen_tokens / wall}
with log.open("a", encoding="utf-8") as f:
f.write(json.dumps(row) + "\n")
print(json.dumps(row), flush=True)
if step % args.eval_every == 0 or step == args.steps:
metrics = evaluate(model, arrays["validation"], frequencies, device=device, precision=args.precision, limit=args.eval_samples)
score = sum(r["cross_entropy"] for r in metrics) / len(metrics)
with log.open("a", encoding="utf-8") as f:
f.write(json.dumps({"step": step, "validation": metrics, "validation_loss": score}) + "\n")
if score < best:
best = score
model.save_pretrained(out / "best")
shutil.copy2(Path(args.data) / "tokenizer.json", out / "best" / "tokenizer.json")
(out / "best" / "training_info.json").write_text(json.dumps({"step": step, "validation_loss": score,
"parameters": parameters, "seen_tokens": seen_tokens, "pretrained_weights": False,
"manifest": manifest, "previous_phase": lineage}, indent=2), encoding="utf-8")
save_state()
print(json.dumps({"step": step, "validation_loss": score, "best": best}), flush=True)
except KeyboardInterrupt:
# An interruption in a partial step is not an exact resume point.
print("Interrupted. Resume from the previous completed evaluation checkpoint.", flush=True)
return
model = Denoiser.from_pretrained(out / "best", device)
report = {"status": "completed", "model_config": asdict(config), "parameters": parameters,
"training": run_config, "seen_tokens": seen_tokens, "seconds": elapsed + time.perf_counter() - start,
"torch_version": str(torch.__version__), "device_name": torch.cuda.get_device_name() if device == "cuda" else "cpu",
"best": json.loads((out / "best" / "training_info.json").read_text()),
"test": evaluate(model, arrays["test"], frequencies, device=device, precision=args.precision, limit=args.test_samples),
"limitations": ["English synthetic stories, not a general chatbot", "No comparison to v1 byte accuracy: tokenizer and corpus differ",
"Custom masked-denoising objective, not a claimed diffusion likelihood bound"]}
(out / "report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"Training complete: {out / 'report.json'}", flush=True)
|