No magic numbers in code — anything that can vary between runs lives here; two runs differ by a config diff, never an edit history.
TODO A.1 (config part): wire the config into the U0.L1 Trainer — interface preserved (v2 adds only keyword-only optional args).
02Experiment tracking with MLflow
The standing decision: MLflow, running locally
The course logging standard, fixed today and used through U3:
Open source, zero vendor accounts — nothing to sign up for,
your data stays on your machine — a local SQLite file, fully offline,
production alignment — the tracker you are most likely to meet at work,
run comparison built in — the overlay view is exactly what the mystery run (today) and the U3.L1 payoff plot (later) need.
mlflow ui --backend-store-uri sqlite:///mlflow.db
You may meet wandb or TensorBoard elsewhere — same concepts, different plumbing. We name them once and move on.
Wiring the Trainer
Reused in: every training run for the rest of the course.
import mlflowmlflow.set_tracking_uri("sqlite:///mlflow.db")mlflow.set_experiment("fbgm-2026") # ONE per course phasewith mlflow.start_run(run_name=cfg.run_name): mlflow.log_params(flatten(cfg)) # resolved config mlflow.set_tags({"session": "U0.L2","tag": "baseline", "seed": cfg.seed}) trainer.fit(train_loader, val_loader, cfg.epochs)
TODO B.1 (tracking part): add the mlflow.log_metric calls inside fit — per step: train loss, lr; per epoch: val loss/acc, grad norm (global \(L_2\)), weight norm, throughput (img/s).
Naming and tags — conventions that scale to U3
One experiment per course phase; tags do the filtering.
Convention
Value
Experiment
fbgm-2026
Tags
session = U0.L2 · tag = baseline · seed = 0
Run name
{session}-{tag}-s{seed} → U0L2-baseline-s0
Why tags and not one experiment per session: cross-session overlays. In U3.L1 you will overlay your U2.L2 (simulation-based) runs against your U3.L1 (simulation-free) runs — same experiment, filtered by session tag. That plot is the punchline of the whole course arc; the convention that makes it possible costs nothing today.
What to log, and why
This checklist is one half of the hygiene-stack artifact.
Train AND val loss — the gap between them is the first diagnostic.
Learning rate — schedule bugs are the #1 silent killer; log what the optimizer actually used, not what the config asked for.
Grad norm (global \(L_2\)) — explosions and vanishing, visible live; U0.T2’s init/scale pictures made these curves readable.
Weight norm — decay and init sanity.
Throughput (img/s) — a sudden drop is the silent-CPU-fallback detector.
Sample/prediction figures via mlflow.log_figure — in U3 this same call logs generated-image panels.
Live: two seeds in the compare view
Launch two short baseline runs, seeds 0 and 1, then:
mlflow ui # select both runs -> Compare
What you should see, and say out loud:
the two loss curves wiggle differently but land together — that spread is the seed variance you eyeballed in the U0.L1 homework-lite, now measured;
the LR curves are identical — the schedule is deterministic;
params differ in exactly one field: seed. Config diff = run diff.
03Schedules and checkpoints
The recipe card, back on screen
Cited from the previous theory session, not re-derived — this box is the U0.T2 defaults:
Optimizer: AdamW, base LR \(3 \times 10^{-4}\), \(\beta = (0.9,\ 0.999)\), weight decay \(0.01\); norm parameters and embeddings excluded from decay.
Schedule: linear warmup over 1–5% of steps, then cosine decay to \(\approx 0\).
Initialization: He for ReLU-family; zero-init last layers where sensible.
Normalization: GN/LN for generative nets; BatchNorm only in bootcamp classifiers — that is us, today, deliberately.
Batch size: largest that fits; always reported.
Today the card stops being advice and becomes running code.
Wiring warmup–cosine
def warmup_cosine(step): # multiplier on base LRif step < warmup:return step / warmup p = (step - warmup) / (total - warmup)return0.5* (1+ math.cos(math.pi * p))
TODO C.1 (schedule part): wrap in LambdaLR, step per step, and check the logged LR traces this curve. The plot is the test.
Checkpoint discipline: last.pt and best.pt
Two files per run, updated on different triggers:
last.pt — every epoch, unconditionally: the resume point.
best.pt — only when the val metric improves: the deliverable. Selection is on val, by a named metric — never on test.
A checkpoint that cannot resume is just a weights file — hence the optimizer/scheduler state and the config.
Resume must actually work
TODO C.2 (checkpoint part): implement resume-from-checkpoint in the v2 Trainer, then prove it with the round-trip assert:
# train 1 epoch -> save -> load into a fresh Trainer# -> evaluate: the val loss must come back identicalassertabs(val_before - val_after) <1e-6
Why we are strict now: in U3 your models cross session boundaries — U3.L2’s trained flow is reused in U3.L3 for classifier-free guidance. A resume bug discovered there costs a lab; discovered here it costs five minutes.
The bad resume, animated
04Reproducibility
Seeds, again — now with dataloaders
set_seed(cfg.seed) from U0.L1 covers python / NumPy / torch / CUDA. Two gaps it does not cover:
Inside: sabotaged runs and clean baselines (3 seeds each), plus the instructor’s 30-epoch reference run. Fully offline; identical for everyone.
Rules of engagement:
The logged configs have been scrubbed — you may not read the bug off a param. Curves and metrics only.
Everything you need was covered in the last 60 minutes.
The task
From curves alone, in pairs:
Describe the pathology — what is wrong, in observable terms?
Name at least two candidate causes.
Rank them — which is more likely, given these curves?
Propose the single cheapest discriminating experiment — the one measurement that best separates your candidates.
Run it. Confirm or kill your hypothesis.
Steps 3–4 are the discipline being trained: not “what could be wrong” but “what would you look at first, and why.”
How the 30 minutes run
Phase
Min
What happens
Silent reading
10
Pairs; curves on screen
Board round
10
Hypotheses; forced ranking
Discriminate
10
Run the test; confirm; reveal
What a discriminating experiment looks like here:
log the input batch statistics (mean/std) of the training data,
or overlay per-layer grad or weight norms, mystery vs clean,
or a 20-step run at a lower LR — does the pathology move?
The presenting symptom
Same architecture, same optimizer, same schedule, same number of steps.
The lesson
You debugged a model without reading its config. In U2 and U3 the models get stranger, but the curves speak the same language. Log first, hypothesize second, discriminate cheaply third.
This is why Block B’s checklist exists: at minute 50 you diagnosed only what someone had logged at minute 20.
In U2/U3 you cannot eyeball a velocity field and see the bug.
You can read its loss, its grad norms, its NFE, its throughput.
The instrument panel is the same; only the aircraft changes.