Download cis6270/runner.py from ChatterjeeLab/CIS6270: direct link, hf CLI and curl.
- Browser
- Download file 6.79 kB
-
https://huggingface.co/ChatterjeeLab/CIS6270/resolve/main/cis6270/runner.py
- Command line
-
hf download hf://ChatterjeeLab/CIS6270/cis6270/runner.py
-
curl -L -o runner.py https://huggingface.co/ChatterjeeLab/CIS6270/resolve/main/cis6270/runner.py
6.79 kB
| """Seeding, device selection, the training loop, checkpoints and the shared command line. | |
| Every lecture script ends in the same four lines: parse the arguments, build the model, call | |
| ``train`` with a step function, and print a report. Only the step function differs between | |
| frameworks, and each lecture is about that function. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import random | |
| import time | |
| from pathlib import Path | |
| from typing import Callable | |
| import torch | |
| # %% 01. Reproducibility and hardware | |
| def set_seed(seed: int) -> None: | |
| """Seed Python, NumPy through torch, and torch itself, so a run repeats exactly.""" | |
| random.seed(seed) | |
| torch.manual_seed(seed) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(seed) | |
| def pick_device(requested: str = "auto") -> torch.device: | |
| """Choose CUDA, then Apple MPS, then CPU, unless the reader names one.""" | |
| if requested != "auto": | |
| return torch.device(requested) | |
| if torch.cuda.is_available(): | |
| return torch.device("cuda") | |
| if torch.backends.mps.is_available(): | |
| return torch.device("mps") | |
| return torch.device("cpu") | |
| # %% 02. The command line every script shares | |
| # | |
| # A reader who has learned the flags once knows them for all fifty scripts. A script adds its own | |
| # flags on top of these with `parser.add_argument`. | |
| def common_parser(description: str) -> argparse.ArgumentParser: | |
| """The flags shared by every script in the course.""" | |
| parser = argparse.ArgumentParser( | |
| description=description, formatter_class=argparse.ArgumentDefaultsHelpFormatter, | |
| ) | |
| parser.add_argument("--steps", type=int, default=600, help="optimizer steps") | |
| parser.add_argument("--batch-size", type=int, default=64, help="examples per step") | |
| parser.add_argument("--lr", type=float, default=1e-3, help="learning rate") | |
| parser.add_argument("--width", type=int, default=128, help="hidden width of the network") | |
| parser.add_argument("--length", type=int, default=16, help="sequence length L") | |
| parser.add_argument("--samples", type=int, default=16, help="sequences or points to generate") | |
| parser.add_argument("--sample-steps", type=int, default=64, help="integration steps") | |
| parser.add_argument("--seed", type=int, default=0, help="random seed") | |
| parser.add_argument("--device", type=str, default="auto", help="auto, cpu, cuda or mps") | |
| parser.add_argument("--out", type=str, default="", help="directory for the run's output") | |
| parser.add_argument("--quiet", action="store_true", help="print only the final report") | |
| return parser | |
| # %% 03. The training loop | |
| # | |
| # The caller passes a function of the step index that returns a scalar loss with a graph attached; | |
| # everything else, the optimizer, the logging and the timing, is the same in every lecture. | |
| def train( | |
| model: torch.nn.Module, | |
| loss_fn: Callable[[int], torch.Tensor], | |
| steps: int = 600, | |
| lr: float = 1e-3, | |
| log_every: int = 100, | |
| quiet: bool = False, | |
| grad_clip: float = 1.0, | |
| extra_parameters: list[torch.nn.Parameter] | None = None, | |
| ) -> list[float]: | |
| """Run ``steps`` optimizer steps on ``loss_fn`` and return the loss at every step. | |
| ``loss_fn(step)`` draws its own batch, so a lecture with fresh noise at every step runs without | |
| a DataLoader. ``extra_parameters`` covers the rare case of a second trained object, such as the | |
| bias force of Chapter 7. | |
| """ | |
| parameters = list(model.parameters()) + list(extra_parameters or []) | |
| optimizer = torch.optim.AdamW(parameters, lr=lr, weight_decay=0.0) | |
| losses: list[float] = [] | |
| started = time.time() | |
| model.train() | |
| for step in range(steps): | |
| loss = loss_fn(step) # The lecture's own objective. | |
| optimizer.zero_grad(set_to_none=True) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(parameters, grad_clip) # Keep a bad batch from derailing. | |
| optimizer.step() | |
| losses.append(float(loss.detach())) | |
| if not quiet and log_every and (step + 1) % log_every == 0: | |
| window = losses[-log_every:] # Average over the last window, a less jumpy number. | |
| print(f" step {step + 1:5d}/{steps} loss {sum(window) / len(window):.4f}") | |
| if not quiet: | |
| print(f" trained {steps} steps in {time.time() - started:.1f}s") | |
| model.eval() | |
| return losses | |
| # %% 04. Saving a run | |
| # | |
| # A run writes three files: the weights, the configuration that produced them, and a short report of | |
| # what came out. No optimizer state, because none of these examples resumes training. | |
| def save_run( | |
| out: str | Path, | |
| model: torch.nn.Module | None = None, | |
| config: dict | None = None, | |
| report: dict | None = None, | |
| losses: list[float] | None = None, | |
| samples: list[str] | None = None, | |
| ) -> Path | None: | |
| """Write the weights, the configuration, the report, the loss curve and any samples.""" | |
| if not out: | |
| return None | |
| directory = Path(out) | |
| directory.mkdir(parents=True, exist_ok=True) | |
| if model is not None: | |
| torch.save({"model": model.state_dict(), "config": config or {}}, | |
| directory / "checkpoint.pt") | |
| if config is not None: | |
| (directory / "config.json").write_text(json.dumps(config, indent=2) + "\n") | |
| if report is not None: | |
| (directory / "report.json").write_text(json.dumps(report, indent=2) + "\n") | |
| if losses is not None: | |
| # Thin the curve to at most 200 points so the file stays small and readable. | |
| stride = max(1, len(losses) // 200) | |
| thinned = [round(v, 5) for v in losses[::stride]] | |
| (directory / "losses.json").write_text(json.dumps(thinned) + "\n") | |
| if samples is not None: | |
| (directory / "samples.txt").write_text("\n".join(samples) + "\n") | |
| return directory | |
| def load_run(out: str | Path, model: torch.nn.Module) -> dict: | |
| """Read the weights back into ``model`` and return the configuration that trained them.""" | |
| payload = torch.load(Path(out) / "checkpoint.pt", map_location="cpu", weights_only=False) | |
| model.load_state_dict(payload["model"]) | |
| model.eval() | |
| return payload.get("config", {}) | |
| # %% 05. Printing a report | |
| # | |
| # Every script ends with the same two-column block, so we can compare two reports side by side. | |
| def print_report(title: str, report: dict, samples: list[str] | None = None) -> None: | |
| """Print a titled block of name and value pairs, and a few samples.""" | |
| print(f"\n{title}") | |
| print("-" * len(title)) | |
| for key, value in report.items(): | |
| if isinstance(value, float): | |
| print(f" {key:<34s} {value:.4f}") | |
| else: | |
| print(f" {key:<34s} {value}") | |
| if samples: | |
| print("\n samples") | |
| for sample in samples[:8]: | |
| print(f" {sample}") | |