Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
The weights directory for policy-sft-v4 is still on disk after a 4-GPU job dies at step 1840. The next morning someone launches with resume_from_checkpoint=weights/step_1840. Loss jumps, the learning rate restarts, and yesterday's shuffled rows come around again. The file was an export, not a resume.
LoRA already showed that on policy-lora-v1: adapters can replace full-weight updates when the task is a thin policy overlay. This chapter treats the run as the system: what "resume" means on a sharded SFT save, how batch math changes the update, and which method to launch given data volume, domain shift, and GPU budget.

Three operations people call resume
People use "resume" for three different jobs. Mixing them is how policy-sft-v4 looked like it continued when it actually restarted.
| Operation | What you load | What must match | Honest description |
|---|---|---|---|
| Continue | weights, optimizer state, scheduler, global step, RNG, sampler position, tokenizer and template versions, data and eval manifests | same recipe, same data order contract, compatible parallelism | same run, next update |
| Initialize | weights or adapters only | architecture and tokenizer | new run that starts from those weights |
| Export | weights or adapters, plus serving config | inference stack, not optimizer | eval or deploy artifact |
Continue is the only operation that preserves Adam moments, the learning-rate schedule, and which rows have already been seen. Initialize is valid, but its first metrics aren't comparable to the interrupted curve. Export shouldn't be fed back into a trainer as if the optimizer still existed. A merged LoRA dump is initialize or export, not continue: torchtune resumes adapters from adapter_model.pt plus the original base, and loading merged weights together with adapters is an error.[1]

The SFT resume list still holds: model, optimizer, scheduler, step, RNG and sampler, tokenizer and chat-template versions, data and eval manifests, and the best held-out metric so far. The new question is whether that bundle is one complete distributed artifact, and which of the three operations you are actually performing.
1def resume_kind(bundle: dict[str, object]) -> str:
2 required_continue = {
3 "model_state",
4 "optimizer_state",
5 "scheduler_state",
6 "global_step",
7 "rng_state",
8 "sampler_state",
9 "tokenizer_version",
10 "chat_template_version",
11 "data_manifest",
12 "eval_manifest",
13 "best_metric",
14 }
15 if required_continue <= bundle.keys():
16 return "continue"
17 if "model_state" in bundle and bundle.get("purpose") == "serve":
18 return "export"
19 if "model_state" in bundle:
20 return "initialize"
21 return "invalid"
22
23weights_only = {"model_state": "weights/step_1840.safetensors"}
24full = {
25 **weights_only,
26 "optimizer_state": "optim/step_1840.pt",
27 "scheduler_state": "sched/step_1840.pt",
28 "global_step": 1840,
29 "rng_state": "rng/step_1840.pt",
30 "sampler_state": {"epoch": 1, "batches_consumed": 368},
31 "tokenizer_version": "policy-sft-tokenizer-v3",
32 "chat_template_version": "llama3-support-v2",
33 "data_manifest": "data/sft_manifest_2026-05-20.json",
34 "eval_manifest": "eval/access_policy_behavior_v4.json",
35 "best_metric": {"name": "support_resolution_accuracy", "value": 0.78},
36}
37print("weights_only=", resume_kind(weights_only))
38print("full_bundle=", resume_kind(full))
39print("serving_export=", resume_kind({**weights_only, "purpose": "serve"}))1weights_only= initialize
2full_bundle= continue
3serving_export= exportIf you load weights_only and keep plotting loss on the old chart, you are lying about continuity. Start a new run id.
A trainer reloads model weights and global_step=1840, but not optimizer, scheduler, or sampler state. Can the next 200 steps be compared as uninterrupted continuation of policy-sft-v4?
Answer
No. Call it initialize, not continue. Momentum, learning rate, and data position all changed, so the curve is a new experiment that happens to start from those weights.
A checkpoint is a distributed artifact
On one GPU, torch.save of a dict can be "the checkpoint." On four FSDP ranks, each rank holds a shard of parameters, gradients, and Adam moments. ZeRO exists largely because those moments dominate model-state memory.[2] A save that finishes on ranks 0-2 and dies on rank 3 isn't a checkpoint. It's a partial directory.
A checkpoint manifest is the inventory that says which files, sizes, hashes, step, topology, and related state belong together. Directory existence isn't completion. Publish complete=true only after every required shard verifies. Refuse to load if a shard is missing, unexpected, or hash-mismatched.
Two save formats show up in real jobs:
| Format | What it stores | Resume on a different world size |
|---|---|---|
Per-rank torch.save dumps | shards tied to the current rank layout | usually no, unless you write a converter |
| PyTorch Distributed Checkpoint (DCP) | parallelism-agnostic tensors plus metadata | yes, DCP resharded at load if the new process group can read the directory[3] |
| DeepSpeed universal checkpoint | converted ZeRO shards | yes, after the universal conversion step[4] |
DCP isn't torch.load(path). It still writes multiple files (at least one per rank), but it operates in place: you allocate the current model's state_dict so DCP has sharding info and storage to fill, then it reshards into that layout.[3] That's how a 4-GPU policy-sft-v4 dump can continue on 8 GPUs, as long as the new process group can read the checkpoint directory (usually a shared filesystem). Naive rank files don't do that. Changing data-parallel width is the DCP path. Changing tensor-parallel or pipeline layout usually needs an explicit converter, which is what DeepSpeed's universal export is for.

The topology itself is part of the resume contract. Record world_size, FSDP/ZeRO stage, and whether the dump is DCP, universal, or rank-local. A loader that ignores those fields will either crash or silently rebuild optimizer state.
1def restore_decision(manifest: dict[str, object], live_world_size: int) -> str:
2 expected = set(manifest["expected_files"])
3 present = set(manifest["present_files"])
4 if present != expected:
5 missing = ",".join(sorted(expected - present)) or "none"
6 extra = ",".join(sorted(present - expected)) or "none"
7 return f"reject:incomplete missing={missing} extra={extra}"
8 if not manifest["complete"]:
9 return "reject:manifest_not_complete"
10 fmt = manifest["format"]
11 saved_world = manifest["world_size"]
12 if fmt == "rank_local" and saved_world != live_world_size:
13 return "reject:topology_mismatch"
14 if fmt in {"dcp", "universal"}:
15 return "continue:reshard_ok"
16 if saved_world == live_world_size:
17 return "continue:same_topology"
18 return "reject:unknown_format"
19
20manifest_1840 = {
21 "format": "rank_local",
22 "world_size": 4,
23 "complete": True,
24 "expected_files": ["rank0", "rank1", "rank2", "rank3", "metadata"],
25 "present_files": ["rank0", "rank1", "rank2", "rank3", "metadata"],
26}
27partial = {
28 **manifest_1840,
29 "complete": False,
30 "present_files": ["rank0", "rank1", "rank2", "metadata"],
31}
32dcp = {**manifest_1840, "format": "dcp"}
33print("same_4gpu=", restore_decision(manifest_1840, 4))
34print("naive_8gpu=", restore_decision(manifest_1840, 8))
35print("partial=", restore_decision(partial, 4))
36print("dcp_8gpu=", restore_decision(dcp, 8))1same_4gpu= continue:same_topology
2naive_8gpu= reject:topology_mismatch
3partial= reject:incomplete missing=rank3 extra=none
4dcp_8gpu= continue:reshard_okpolicy-sft-v4 saved four rank-local files at step 1840. You restart on eight GPUs and torch.load each rank file by rank index. Why is that not a DCP-style resume?
Answer
Rank-local files are bound to the layout that wrote them. DCP (or a DeepSpeed universal checkpoint) reshards tensors using the current model's sharding. Indexing rank0.pt on an 8-GPU job either crashes or loads the wrong slice of optimizer state.
Preemption saves the last complete step
Cluster jobs get a terminate signal (SIGTERM). Spot VMs disappear. A rank hits an NCCL timeout (NVIDIA's collective communication library giving up on a stuck all-reduce). The useful unit isn't "the process was running." It's "the last optimizer step whose bundle is durable."
Write checkpoints at optimizer-step boundaries, not mid-backward. Gradient accumulation and GradScaler share that same boundary, from the mixed-precision chapter. If accumulation takes eight microbatches, a kill during microbatch 5 has no new complete step. Keep the previous complete dump. Then:
- Drain in-flight microbatches only if you can finish the accumulation window before the kill deadline.
- Save model, optimizer, scheduler, sampler, and RNG together.
- Verify shards and hashes.
- Publish the manifest with
complete=trueand pointlatestat that step. - Only then is it legal for a new process group to continue.
Cadence is a cost tradeoff. Saving every step on a 70B ZeRO-3 job can dominate the step time. Saving once per hour on a 20-minute preemptible slice wastes the slice. A practical rule: the expected lost work after a kill should stay small versus the save cost. For policy-sft-v4 on four GPUs with a 90-second save, a checkpoint every 50-100 steps is usually enough. Measure it.
Final export for eval still isn't a resume bundle. torchtune distinguishes intermediate checkpoints from the artifact you ship.[1] Keep both names in the manifest so nobody resumes from an eval export.
What "the batch" is
SFT already used this identity. It's the run's unit of update, so it belongs in the ops card, not only in the trainer config:
is the per-device microbatch, is accumulation steps, and is the data-parallel world size. For policy-sft-v4:
If packing is on, also log supervised tokens per update. Two runs with 64 examples can still differ by 2× in tokens if replies are longer.

When someone says "we doubled GPUs and loss exploded," expand the identity before touching Adam. Doubling while leaving and fixed doubles . That's a different recipe.
1def batch_card(per_device_batch: int, accumulation: int, world_size: int, supervised_tokens_per_rank: int) -> dict[str, int]:
2 examples = per_device_batch * accumulation * world_size
3 tokens = supervised_tokens_per_rank * world_size
4 return {
5 "examples_per_update": examples,
6 "supervised_tokens_per_update": tokens,
7 "world_size": world_size,
8 }
9
10base = batch_card(2, 8, 4, 5200)
11doubled_gpus = batch_card(2, 8, 8, 5200)
12halved_accum = batch_card(2, 4, 8, 5200)
13print("base=", base)
14print("doubled_gpus=", doubled_gpus)
15print("halved_accum_to_keep_64=", halved_accum)
16assert base["examples_per_update"] == 64
17assert doubled_gpus["examples_per_update"] == 128
18assert halved_accum["examples_per_update"] == 641base= {'examples_per_update': 64, 'supervised_tokens_per_update': 20800, 'world_size': 4}
2doubled_gpus= {'examples_per_update': 128, 'supervised_tokens_per_update': 41600, 'world_size': 8}
3halved_accum_to_keep_64= {'examples_per_update': 64, 'supervised_tokens_per_update': 41600, 'world_size': 8}halved_accum_to_keep_64 keeps the example budget and still doubles tokens if each rank still sees 5,200 supervised tokens per window. Publish all four numbers: microbatch, , , and supervised tokens.
Learning-rate scaling, warmup, and when the rule breaks
Goyal et al. trained ImageNet with large minibatches by pairing two rules: when the minibatch grows by , multiply the learning rate by , and warm that rate up instead of starting at the scaled peak.[5] The intuition is that averaged gradients are a less noisy estimate of the full-data gradient, so a proportionally larger step keeps the expected update similar. Goyal's measurements were large-batch SGD on ImageNet. The same linear rule is the usual first bet for AdamW SFT, then you measure clip-hit rate and held-out accuracy instead of assuming it transferred.
Their gradual warmup starts at the small-batch learning rate and ramps to . For policy-sft-v4, a LoRA peak of at 64 examples/update becomes at 128 examples/update. The first warmup steps stay near and rise. Jumping straight to on step 1 is the failure that warmup exists to prevent. Many trainers instead warm from near zero to the new peak, which is even more conservative. Either way, don't open at the scaled peak.
The rule isn't unbounded. McCandlish et al. describe a critical batch size: past that scale, extra batch growth buys less progress per compute.[6] Goyal's ResNet-50 runs matched small-batch ImageNet accuracy up to minibatch 8192 on 256 GPUs, then error rose. An SFT job with a few thousand labeled rows can leave the useful region much earlier. If you 8× the batch, 8× the LR, skip warmup, and watch NaNs, you didn't disprove Adam. You left the region where linear scaling is a reasonable first bet.
Warmup and AdamW weight decay are part of the same recipe, not decorations. AdamW decouples weight decay from the adaptive update so "L2" doesn't get rescaled by the second moment.[7] Changing decay when you change batch size is a second experiment. Keep it fixed unless you are studying regularization.
1def scaled_peak(base_lr: float, base_batch: int, new_batch: int) -> float:
2 return base_lr * (new_batch / base_batch)
3
4def goyal_warmup_lrs(base_lr: float, peak: float, warmup_steps: int) -> list[float]:
5 if warmup_steps == 1:
6 return [peak]
7 span = peak - base_lr
8 return [base_lr + span * step / (warmup_steps - 1) for step in range(warmup_steps)]
9
10peak_64 = 2e-5
11peak_128 = scaled_peak(peak_64, 64, 128)
12warm = goyal_warmup_lrs(peak_64, peak_128, 4)
13print(f"peak_64={peak_64:.6f}")
14print(f"peak_128={peak_128:.6f}")
15print("warmup_first_four=", [f"{lr:.6f}" for lr in warm])
16assert abs(peak_128 - 4e-5) < 1e-12
17assert abs(warm[0] - 2e-5) < 1e-12
18assert abs(warm[-1] - 4e-5) < 1e-121peak_64=0.000020
2peak_128=0.000040
3warmup_first_four= ['0.000020', '0.000027', '0.000033', '0.000040']The first warmup step stays at , the old peak, and only then climbs to . Freshly initialized adapter rows make that even more true: keep early steps small on purpose.
Instability: data, optimizer, or scaler
Loss went to NaN at step 1847 after the 8-GPU restart. Clipping fired on 40% of steps. More than one layer can produce that screenshot.
| Symptom | Likely layer | Why | First check |
|---|---|---|---|
| One microbatch has a huge grad, others are calm | data | a poisoned row, exploded token span, or truncated completion with no answer labels | dump that microbatch's ids and token counts |
| Gradients blow up in the first 20 steps after a batch/LR change | optimizer / schedule | scaled peak without warmup, or linear scaling past a useful batch | compare , peak LR, warmup |
| Finite losses, then a skip storm, or LR that effectively doubled | scaler / accumulation | GradScaler step/update on every microbatch, or loss divided twice | count optimizer steps vs microbatches; reread the mixed-precision contract[8] |
| Clip norm always hits the cap | clip hiding a cause | clipping is a backstop, not a data cleaner[9] | log pre-clip grad norm and the offending batch |
Clipping rescales a gradient whose norm exceeds a threshold. It can stop one pathological batch from destroying Adam moments. It can't tell you whether that batch was garbage text, an FP16 overflow, or a learning-rate mistake. If clip-hit rate jumps from 1% to 40% after a topology change, don't raise the clip. Find the layer in the table.
The mixed-precision chapter already showed the accumulation bug: step/update belongs on the optimizer boundary, not each microbatch. An 8× accumulation window that steps eight times has quietly turned into 1 and multiplied the effective learning rate.
1def diagnose(event: dict[str, object]) -> str:
2 if event.get("scaler_steps") == event.get("microbatches") and event["accumulation"] > 1:
3 return "scaler_accumulation_bug"
4 if event.get("warmup_steps", 0) == 0 and event["batch_multiplier"] > 1:
5 return "schedule_no_warmup"
6 if event.get("single_microbatch_grad_norm", 0) > 10 * event.get("median_grad_norm", 1):
7 return "data_poisoned_microbatch"
8 if event.get("clip_hit_rate", 0) > 0.3 and event.get("batch_multiplier", 1) > 1:
9 return "clip_hiding_scale_change"
10 return "needs_more_telemetry"
11
12print(diagnose({
13 "accumulation": 8,
14 "microbatches": 8,
15 "scaler_steps": 8,
16 "batch_multiplier": 1,
17}))
18print(diagnose({
19 "accumulation": 8,
20 "microbatches": 8,
21 "scaler_steps": 1,
22 "batch_multiplier": 2,
23 "warmup_steps": 0,
24}))
25print(diagnose({
26 "accumulation": 8,
27 "microbatches": 8,
28 "scaler_steps": 1,
29 "batch_multiplier": 1,
30 "single_microbatch_grad_norm": 240.0,
31 "median_grad_norm": 1.8,
32}))
33print(diagnose({
34 "accumulation": 8,
35 "microbatches": 8,
36 "scaler_steps": 1,
37 "batch_multiplier": 2,
38 "warmup_steps": 200,
39 "clip_hit_rate": 0.4,
40}))1scaler_accumulation_bug
2schedule_no_warmup
3data_poisoned_microbatch
4clip_hiding_scale_changeAfter doubling world_size, clip-hit rate jumps to 40% and the first ten steps look noisy. Warmup is 0 and peak LR was multiplied by 2. Is this evidence that AdamW is unstable on access-policy SFT?
Answer
No. You changed and the peak rate together and skipped warmup. Treat it as a schedule/batch-card failure until a matched warmup run still explodes.
Which adaptation method to launch
Resume and batch math tell you how to operate a job. They don't tell you which job to start. Full SFT, LoRA, QLoRA, continued pretraining, and distillation are answers to different constraints. The SFT chapter separated objective from parameterization. Here the same access-policy assistant has to pick under data volume, domain shift, and GPU budget.
| Method | What moves | Data it wants | Domain-shift story | GPU story |
|---|---|---|---|---|
| Continued pretraining | full (or large) weights, next-token on unlabeled domain text | lots of unlabeled domain tokens | the base model never saw this language/distribution[10] | closest to pretraining memory |
| Full SFT | all eligible weights, response-token loss | large, clean prompt-response sets | behavior must change broadly, not one slice | FSDP/ZeRO if states don't fit |
| LoRA | small adapters, frozen base[11] | small-to-medium labeled sets | targeted policy/procedure change | often 1-few GPUs; activations still matter |
| QLoRA | LoRA on a quantized frozen base[12] | same as LoRA | same as LoRA, tighter memory | paper headline: 65B SFT on a 48GB GPU |
| Distillation | student weights, teacher signal[13] | teacher traces plus a student budget | you already have a good teacher and need a smaller deployable | train student cheap; teacher may be expensive |
Work the same product through five honest cards:
- 2,000 labeled escalation replies, 1× 24GB GPU, base model already speaks support English. The missing piece is the new rotation procedure. Launch LoRA SFT. Full FT wastes memory on a small behavior delta.
- Same 2,000 rows, 70B base, 1× 24GB GPU. LoRA optimizer state still may not fit with activations. QLoRA is the method that exists for that memory wall, not a better algorithm than LoRA. The paper's headline was 65B on one 48GB GPU. 70B on 24GB is the same idea, tighter.
- Unlabeled internal access-policy corpus, ~2B tokens, 8× 80GB, and the base model mangles product nouns. That's continued pretraining, then a small SFT. SFT on 2,000 rows won't teach a dialect the checkpoint never saw.
- 200,000 high-quality replies that redefine tone, tools, and refusal policy, 8× 80GB. Full SFT is in play. LoRA can still be the first experiment, but don't pretend a rank-8 adapter is guaranteed to match full-weight capacity on a broad rewrite.
- A strong 70B teacher already follows policy, and production needs a 7B on-prem student. Distill after the teacher is good. Distillation doesn't replace getting the teacher right.

1def choose(card: dict[str, object]) -> str:
2 if card["need"] == "smaller_student" and card["teacher_ready"]:
3 return "distillation"
4 if card["unlabeled_domain_tokens"] >= 1_000_000_000 and card["labeled_rows"] < 20_000:
5 return "continued_pretraining"
6 if card["labeled_rows"] >= 100_000 and card["gpu_gb"] * card["gpu_count"] >= 320:
7 return "full_sft"
8 if card["base_params_b"] >= 30 and card["gpu_gb"] <= 24 and card["gpu_count"] == 1:
9 return "qlora"
10 return "lora"
11
12print(choose({
13 "need": "procedure_change",
14 "labeled_rows": 2000,
15 "unlabeled_domain_tokens": 0,
16 "base_params_b": 8,
17 "gpu_gb": 24,
18 "gpu_count": 1,
19 "teacher_ready": False,
20}))
21print(choose({
22 "need": "procedure_change",
23 "labeled_rows": 2000,
24 "unlabeled_domain_tokens": 0,
25 "base_params_b": 70,
26 "gpu_gb": 24,
27 "gpu_count": 1,
28 "teacher_ready": False,
29}))
30print(choose({
31 "need": "domain_language",
32 "labeled_rows": 2000,
33 "unlabeled_domain_tokens": 2_000_000_000,
34 "base_params_b": 8,
35 "gpu_gb": 80,
36 "gpu_count": 8,
37 "teacher_ready": False,
38}))
39print(choose({
40 "need": "broad_rewrite",
41 "labeled_rows": 200_000,
42 "unlabeled_domain_tokens": 0,
43 "base_params_b": 8,
44 "gpu_gb": 80,
45 "gpu_count": 8,
46 "teacher_ready": False,
47}))
48print(choose({
49 "need": "smaller_student",
50 "labeled_rows": 0,
51 "unlabeled_domain_tokens": 0,
52 "base_params_b": 7,
53 "gpu_gb": 80,
54 "gpu_count": 2,
55 "teacher_ready": True,
56}))1lora
2qlora
3continued_pretraining
4full_sft
5distillationQLoRA is quantized LoRA, not a third objective. If LoRA already fits, QLoRA's job is over. Distillation is Knowledge Distillation. CPT is Continued Pretraining. Full SFT vs adapters is the parameterization split from Supervised Fine-Tuning. This chapter's job is to pick the card before you spend the cluster.
After SFT quality exists, preference is a different decision
Once policy-sft-v4 can produce acceptable answers, the next training question is often "which answer is preferred?" That's not a resume problem and not a batch-card problem. Reward models, DPO, GRPO, KTO, and ORPO are post-training families that rank or reinforce among candidates. GRPO already shows up inside RLVR and the slime deep dive. Don't smuggle them into the LoRA vs CPT table.
A compact gate:
| You observe | Next stage |
|---|---|
| Model can't follow the procedure even with gold demonstrations | stay on SFT / CPT / LoRA |
| Several answers are valid and people disagree on ranking | reward modeling or a preference-optimization method |
| A verifier can score code, math, or schema | RLVR / verifiable rewards, not a general chat RM |
Operate those runs with the same contracts: a continue-capable checkpoint, an honest batch card, and a method that matches the signal you actually have.
A run card you can fail closed
Before policy-sft-v4 is allowed to consume another slice, fill this card. If a field is unknown, the job doesn't start.
| Field | policy-sft-v4 |
|---|---|
| Operation | continue / initialize / export |
| Method | LoRA SFT |
| 2, 8, 4 | |
| Examples / supervised tokens per update | 64 / 20,800 |
| Peak LR and warmup | , 200 steps |
| Checkpoint format | DCP, world_size=4 |
| Last complete step | 1840, manifest complete=true |
| Kill budget | 90 s save, checkpoint every 80 steps |
| Held-out winner | support_resolution_accuracy=0.78 at step 1600 |
A production check that is small enough to run in CI: restore the last complete dump on a different world size (4 → 2 or 4 → 8) with DCP, confirm optimizer step count and scheduler LR match the manifest, and refuse to continue if the sampler would replay the first 368 batches.
That is training ops. The model file is one tensor dump. The run is a system that can die, restart, keep its update honest, and justify why those weights were the ones that moved.
Mastery check
Key concepts
- Continue, initialize, and export are different operations
- Sharded checkpoints need a complete manifest; DCP reshards, rank-local files don't
- Preemption restores the last complete optimizer step
- Global batch is microbatch × accumulation × world size
- Linear LR scaling needs warmup and stops being a free lunch at large batches
- Instability is data, schedule, or scaler/accumulation until proven otherwise
- Full SFT, LoRA, QLoRA, CPT, and distillation bind different constraints
Evaluation rubric
- Foundational: Classify a weights-only reload as initialize, not continue, and compute from microbatch, , and .
- Intermediate: Reject an incomplete rank-local dump and a world-size mismatch, and scale peak LR with warmup when doubles.
- Advanced: Route a NaN postmortem to data vs schedule vs scaler, and choose LoRA, QLoRA, CPT, full SFT, or distillation from a constraint card.
Follow-up questions
Why does DCP ask for the current model state_dict before loading, unlike torch.load(path)?
Answer
DCP fills pre-allocated tensors using the live sharding. The new world size's layout has to exist first so load-time resharding has somewhere to put each slice.
You keep B_micro=2 and K=8, raise D from 4 to 8, and keep peak LR at 2e-5 with 200 warmup steps. What did you change, and is that automatically safe?
Answer
You doubled examples per update to 128. Leaving LR fixed is a different recipe from Goyal's linear rule, not automatically safer. Compare against a warmed 4e-5 run and watch clip-hit rate and held-out accuracy.
Common pitfalls
- Plotting an initialized run on the interrupted loss curve
- Pointing
latestat a directory before rank 3 finishes writing - Doubling GPUs and calling the old batch size "unchanged"
- Raising grad clip until NaNs stop, without inspecting the microbatch
- Using QLoRA when LoRA already fits, or LoRA when the base model doesn't speak the domain