Download code/paya/train.py from yeabwang/Paya: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/yeabwang/Paya/resolve/main/code/paya/train.py
- Command line
-
hf download hf://yeabwang/Paya/code/paya/train.py
-
curl -L -o train.py https://huggingface.co/yeabwang/Paya/resolve/main/code/paya/train.py
12.5 kB
| """Train the shared decision head over frozen native-rate signal embeddings.""" | |
| import argparse | |
| import json | |
| import os | |
| from pathlib import Path | |
| import random | |
| from time import perf_counter | |
| import torch | |
| from torch.nn import functional as F | |
| import yaml | |
| from .checkpoint import save_checkpoint, load_checkpoint | |
| from .data import ChoiceDataset, assert_disjoint, make_batch | |
| from .evaluate import collect, metrics | |
| from .features import open_feature_cache | |
| from .model import PayaModel | |
| from .tracking import open_run, run_identity, log_best | |
| from .atomic import atomic_output | |
| def choice_loss(logits, targets, mask, smoothing=0.0, reduction="mean"): | |
| if targets.dtype != torch.long or targets.shape != (len(logits),): | |
| raise ValueError("Targets must be integer candidate indices [B]") | |
| if ((targets < 0) | (targets >= logits.shape[1])).any(): | |
| raise ValueError("Target out of range") | |
| if not mask.gather(1, targets[:, None]).all(): | |
| raise ValueError("Cannot train on a padded candidate") | |
| if not 0 <= smoothing < 1 or reduction not in ("mean","none"): | |
| raise ValueError("Invalid candidate loss smoothing or reduction") | |
| if smoothing==0: | |
| return F.cross_entropy(logits, targets,reduction=reduction) | |
| logp=logits.log_softmax(-1) | |
| nll=-logp.gather(1,targets[:,None]).squeeze(1) | |
| uniform=-logp.masked_fill(~mask,0).sum(-1)/mask.sum(-1) | |
| values=(1-smoothing)*nll+smoothing*uniform | |
| return values.mean() if reduction=="mean" else values | |
| def train(config_path, run_dir, device="cpu", resume=None): | |
| config = yaml.safe_load(Path(config_path).read_text(encoding="utf-8")) | |
| destination = Path(run_dir) | |
| if destination.exists() and any(destination.iterdir()) and resume is None: | |
| raise FileExistsError("Use a fresh run directory or supply --resume") | |
| destination.mkdir(parents=True, exist_ok=True) | |
| seed = config.get("seed", 42) | |
| random.seed(seed); torch.manual_seed(seed) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(seed) | |
| torch.set_num_threads(config.get("threads", 4)) | |
| datasets = {name: ChoiceDataset(path) for name, path in config["data"].items() | |
| if name in ("train", "selection", "calibration", "test") and path} | |
| if not {"train", "selection"} <= datasets.keys(): | |
| raise ValueError("Training and independent selection datasets are required") | |
| assert_disjoint(*datasets.values()) | |
| model = PayaModel.from_assets(config["assets"], config.get("preparation"), config.get("head"), device) | |
| cache = open_feature_cache(config) | |
| opt = config["optim"] | |
| epochs = opt["epochs"] | |
| if isinstance(epochs, bool) or not isinstance(epochs, int) or epochs < 1: | |
| raise ValueError("Need at least one training epoch") | |
| optimizer = torch.optim.AdamW(model.trainable_parameters(), lr=opt["lr"], weight_decay=opt.get("weight_decay", 0.01)) | |
| interval = opt.get("checkpoint_every_steps", 100) | |
| if isinstance(interval, bool) or not isinstance(interval, int) or interval < 1: | |
| raise ValueError("checkpoint_every_steps must be a positive integer") | |
| bindings = {name: data.sha256 for name, data in datasets.items()} | |
| forbidden = {"datasets": [{"path": str(datasets[name].path), "sha256": datasets[name].sha256} | |
| for name in ("train", "selection")]} | |
| start, best, step, previous_run = 0, float("inf"), 0, None | |
| next_batch, epoch_total, epoch_count, restored_state = 0, 0.0, 0, None | |
| # Load lazy text assets before restoring RNG, so initialization cannot perturb resumed dropout. | |
| make_batch(model, [datasets["train"].rows[0]]) | |
| if resume: | |
| payload = load_checkpoint(resume, model) | |
| state = payload["training"] | |
| if not state or ({k: v for k, v in state["config"].items() if k != "wandb"} | |
| != {k: v for k, v in config.items() if k != "wandb"}): | |
| raise ValueError("Resume requires the same model, data and optimizer configuration") | |
| optimizer.load_state_dict(state["optimizer"]) | |
| if state.get("dataset_sha256") != bindings: | |
| raise ValueError("Resume dataset content differs from checkpoint") | |
| start, best = state["epoch"], state["best_nll"] | |
| step, previous_run = state["global_step"], state["wandb"] | |
| next_batch = state.get("next_batch", 0) | |
| epoch_total, epoch_count = state.get("epoch_loss_sum", 0.0), state.get("epoch_count", 0) | |
| restored_state = state | |
| history = [] | |
| completed_result = restored_state.get("completed_epoch_result") if restored_state else None | |
| last_batch_metrics = restored_state.get("last_batch_metrics") if restored_state else None | |
| history_path = destination/"history.jsonl" | |
| saved_history = [] | |
| if history_path.exists(): | |
| with history_path.open(encoding="utf-8") as source: | |
| saved_history = [json.loads(line) for line in source if line.strip()] | |
| saved_history = [row for row in saved_history if row["epoch"] <= start] | |
| with atomic_output(history_path,"w") as output: | |
| for row in saved_history: | |
| output.write(json.dumps(row)+"\n") | |
| steps_path = destination/"steps.jsonl" | |
| if restored_state and "metrics_bytes" in restored_state: | |
| committed_bytes = restored_state["metrics_bytes"] | |
| if not steps_path.exists() or steps_path.stat().st_size < committed_bytes: | |
| raise ValueError("Resume requires the committed steps.jsonl beside the checkpoint") | |
| with steps_path.open("r+b") as output: | |
| output.truncate(committed_bytes) | |
| output.flush() | |
| os.fsync(output.fileno()) | |
| if start >= epochs: | |
| return model, history | |
| tracking = config.get("wandb", {}) | |
| with open_run(config, destination, model, datasets, previous_run) as run, steps_path.open("a+b") as step_log: | |
| if restored_state: | |
| torch.set_rng_state(restored_state["torch_rng"]) | |
| random.setstate(restored_state["python_rng"]) | |
| if torch.cuda.is_available() and restored_state["cuda_rng"]: | |
| torch.cuda.set_rng_state_all(restored_state["cuda_rng"]) | |
| if run: | |
| run.log({"recovery/checkpoint_step": step, "recovery/next_batch": next_batch}) | |
| if restored_state.get("metrics_bytes"): | |
| with steps_path.open(encoding="utf-8") as source: | |
| for line in source: | |
| record=json.loads(line) | |
| if record["step"] % tracking.get("log_every",10)==0 or record["step"]==step: | |
| run.log({**record,"recovery/replayed":True}) | |
| elif last_batch_metrics: | |
| run.log({**last_batch_metrics,"recovery/replayed":True}) | |
| for completed in saved_history: | |
| run.log({"step":completed["step"],"epoch":completed["epoch"],"recovery/replayed":True, | |
| **{f"selection/{key}":value for key,value in completed["selection"].items() if key!='reliability'}}) | |
| def progress(epoch, cursor, total, count): | |
| step_log.flush() | |
| os.fsync(step_log.fileno()) | |
| return {"config": config, "epoch": epoch, "next_batch": cursor, | |
| "metrics_bytes":step_log.tell(), | |
| "epoch_loss_sum": total, "epoch_count": count, | |
| "completed_epoch_result": completed_result, | |
| "last_batch_metrics": last_batch_metrics, | |
| "best_nll": best, "global_step": step, "dataset_sha256": bindings, | |
| "wandb": run_identity(run, tracking.get("mode", "disabled")), | |
| "optimizer": optimizer.state_dict(), "torch_rng": torch.get_rng_state(), | |
| "python_rng": random.getstate(), | |
| "cuda_rng": torch.cuda.get_rng_state_all() if torch.cuda.is_available() else [], | |
| "forbidden_calibration": forbidden} | |
| if not resume: | |
| save_checkpoint(destination/"last.pt", model, progress(0, 0, 0.0, 0)) | |
| for epoch in range(start, epochs): | |
| started = perf_counter() | |
| for data in datasets.values(): | |
| data.assert_unchanged() | |
| total, count = (epoch_total, epoch_count) if epoch == start else (0.0, 0) | |
| cursor = next_batch if epoch == start else 0 | |
| for batch_index, rows in enumerate(datasets["train"].batches( | |
| config.get("batch_size", 8), seed+epoch, start_batch=cursor), start=cursor): | |
| batch = make_batch(model, rows) | |
| targets = torch.tensor([row["target"] for row in rows], device=model.device) | |
| optimizer.zero_grad(set_to_none=True) | |
| model.train() | |
| result = model(batch, cache=cache) | |
| loss = choice_loss(result["logits"], targets, batch.criteria_mask,opt.get("label_smoothing",0.0)) | |
| if not torch.isfinite(loss): | |
| raise FloatingPointError("Nonfinite training loss") | |
| loss.backward() | |
| gradient_norm = torch.nn.utils.clip_grad_norm_(list(model.trainable_parameters()), 1.0) | |
| if not torch.isfinite(gradient_norm): | |
| raise FloatingPointError("Nonfinite training gradients") | |
| optimizer.step() | |
| step += 1 | |
| total += loss.item()*len(rows); count += len(rows) | |
| cursor = batch_index+1 | |
| last_batch_metrics = {"step": step, "epoch": epoch+1, "train/loss": loss.item(), | |
| "train/answer_loss": loss.item(), "train/gradient_norm": float(gradient_norm), | |
| "train/learning_rate": optimizer.param_groups[0]["lr"]} | |
| step_log.write((json.dumps(last_batch_metrics,allow_nan=False)+"\n").encode("utf-8")) | |
| if run and step % tracking.get("log_every", 10) == 0: | |
| run.log(last_batch_metrics) | |
| if step % interval == 0: | |
| save_checkpoint(destination/"last.pt", model, progress(epoch, cursor, total, count)) | |
| records = collect(model, datasets["selection"], config.get("batch_size", 8), cache, | |
| output_path=destination/"selection-predictions.jsonl") | |
| selection = metrics(records) | |
| improved = selection["nll"] < best | |
| if improved: | |
| best = selection["nll"] | |
| row = {"epoch": epoch+1, "phase": "decision_head", | |
| "step": step, "train_loss": total/max(1, count), "selection": selection} | |
| completed_result = row | |
| state = progress(epoch+1, 0, 0.0, 0) | |
| if improved: | |
| save_checkpoint(destination/"best.pt", model, state) | |
| saved_history = [previous for previous in saved_history if previous["epoch"] < epoch+1]+[row] | |
| with atomic_output(history_path,"w") as log: | |
| for previous in saved_history: | |
| log.write(json.dumps(previous)+"\n") | |
| save_checkpoint(destination/"last.pt", model, state) | |
| history.append(row) | |
| if run: | |
| logged = {"epoch": epoch+1, "step": step, "phase": row["phase"], | |
| "train/epoch_loss": row["train_loss"], "train/epoch_seconds": perf_counter()-started, | |
| **{f"selection/{k}": selection[k] for k in ("accuracy", "nll", "brier", "ece_15", "count")}} | |
| run.log(logged) | |
| if improved: | |
| run.summary.update({"best/epoch":epoch+1,"best/selection_nll":best}) | |
| if tracking.get("log_models",True): | |
| log_best(run, model, destination, epoch+1, best) | |
| print(json.dumps(row), flush=True) | |
| return model, history | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--config", type=Path, default=Path("config/paya.yaml")) | |
| parser.add_argument("--run-dir", type=Path, required=True) | |
| parser.add_argument("--device", default="cpu") | |
| parser.add_argument("--resume", type=Path) | |
| args = parser.parse_args() | |
| train(args.config, args.run_dir, args.device, args.resume) | |