LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 178 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnAdvanced Training & AdaptationTraining Run Operations
⚡HardFine-Tuning & Training

Training Run Operations

Treat a training job as a resumable system: distinguish continue vs initialize vs export, save sharded state that can survive preemption, keep global batch and learning-rate scaling honest, and choose full SFT, LoRA, QLoRA, continued pretraining, or distillation from data, domain shift, and GPU budget.

23 min read
Learning path
Step 107 of 178 in the full curriculum
LoRA & Parameter-Efficient TuningReward Modeling from Preference Data

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.

Two recovery paths after SIGTERM on policy-sft-v4. Left: kill during microbatch 5 of 8 drops the unfinished window and restores last complete step 1840. Right: kill after the optimizer step finishes, then save model, Adam, sampler, and RNG, verify shards, and publish complete=true so the next process starts at 1841.
A preempted job is recoverable only if the last *complete* optimizer step is durable and the manifest says so. A kill during accumulation doesn't create step 1841. Weights without optimizer, scheduler, and data position are an initialization, not a continuation.

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.

OperationWhat you loadWhat must matchHonest description
Continueweights, optimizer state, scheduler, global step, RNG, sampler position, tokenizer and template versions, data and eval manifestssame recipe, same data order contract, compatible parallelismsame run, next update
Initializeweights or adapters onlyarchitecture and tokenizernew run that starts from those weights
Exportweights or adapters, plus serving configinference stack, not optimizereval 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]Reference 1Checkpointing in torchtune.https://meta-pytorch.org/torchtune/stable/deep_dives/checkpointer.html

Diagram showing Durable step 1840, What do you need next?, Continue the run, and Initialize a new run.
Durable step 1840, What do you need next?, Continue the run, and Initialize a new run.

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.

classify_resume_kind.py
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"}))
Resume kind output
1weights_only= initialize 2full_bundle= continue 3serving_export= export

If 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]Reference 2ZeRO: Memory Optimizations Toward Training Trillion Parameter Models.https://arxiv.org/abs/1910.02054 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:

FormatWhat it storesResume on a different world size
Per-rank torch.save dumpsshards tied to the current rank layoutusually no, unless you write a converter
PyTorch Distributed Checkpoint (DCP)parallelism-agnostic tensors plus metadatayes, DCP resharded at load if the new process group can read the directory[3]Reference 3PyTorch Distributed Checkpoint (DCP) Recipehttps://docs.pytorch.org/tutorials/recipes/distributed_checkpoint_recipe.html
DeepSpeed universal checkpointconverted ZeRO shardsyes, after the universal conversion step[4]Reference 4Universal Checkpointing with DeepSpeed: A Practical Guide.https://www.deepspeed.ai/tutorials/universal-checkpointing/

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]Reference 3PyTorch Distributed Checkpoint (DCP) Recipehttps://docs.pytorch.org/tutorials/recipes/distributed_checkpoint_recipe.html 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.

Left: four rank shards for step 1840. Middle: an 8-GPU naive load fails because four rank-local files cannot fill eight live shards. Right: DCP reads the same dump into the live 8-GPU state_dict and reshards.
Per-rank dumps remember the layout that wrote them. DCP still writes several files, but it reshards into the live `state_dict`, so a 4-GPU save can continue on 8 GPUs when the new ranks can read the directory.

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.

reject_incomplete_or_wrong_topology.py
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))
Shard gate output
1same_4gpu= continue:same_topology 2naive_8gpu= reject:topology_mismatch 3partial= reject:incomplete missing=rank3 extra=none 4dcp_8gpu= continue:reshard_ok

policy-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:

  1. Drain in-flight microbatches only if you can finish the accumulation window before the kill deadline.
  2. Save model, optimizer, scheduler, sampler, and RNG together.
  3. Verify shards and hashes.
  4. Publish the manifest with complete=true and point latest at that step.
  5. 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]Reference 1Checkpointing in torchtune.https://meta-pytorch.org/torchtune/stable/deep_dives/checkpointer.html 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:

Bglobal=Bmicro×K×DB_{\mathrm{global}} = B_{\mathrm{micro}} \times K \times DBglobal​=Bmicro​×K×D

BmicroB_{\mathrm{micro}}Bmicro​ is the per-device microbatch, KKK is accumulation steps, and DDD is the data-parallel world size. For policy-sft-v4:

Bglobal=2×8×4=64 examples per optimizer stepB_{\mathrm{global}} = 2 \times 8 \times 4 = 64 \text{ examples per optimizer step}Bglobal​=2×8×4=64 examples per optimizer step

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.

Four GPU rows for policy-sft-v4, each with eight accumulation cells of two examples, totaling 16 examples per GPU and 64 examples per optimizer update.
Each cell is a microbatch of 2 examples. Eight accumulation windows on one GPU make 16. Four ranks make 64 examples per AdamW step. Doubling $D$ while leaving the cell and the window fixed doubles the update to 128.

When someone says "we doubled GPUs and loss exploded," expand the identity before touching Adam. Doubling DDD while leaving BmicroB_{\mathrm{micro}}Bmicro​ and KKK fixed doubles BglobalB_{\mathrm{global}}Bglobal​. That's a different recipe.

global_batch_card.py
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"] == 64
Batch card output
1base= {'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, KKK, DDD, 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 kkk, multiply the learning rate by kkk, and warm that rate up instead of starting at the scaled peak.[5]Reference 5Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour.https://arxiv.org/abs/1706.02677 The intuition is that kkk 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 η\etaη and ramps to kηk\etakη. For policy-sft-v4, a LoRA peak of 2×10−52\times 10^{-5}2×10−5 at 64 examples/update becomes 4×10−54\times 10^{-5}4×10−5 at 128 examples/update. The first warmup steps stay near 2×10−52\times 10^{-5}2×10−5 and rise. Jumping straight to 4×10−54\times 10^{-5}4×10−5 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]Reference 6An Empirical Model of Large-Batch Training.https://arxiv.org/abs/1812.06162 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]Reference 7Decoupled Weight Decay Regularization.https://arxiv.org/abs/1711.05101 Changing decay when you change batch size is a second experiment. Keep it fixed unless you are studying regularization.

linear_scale_with_warmup.py
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-12
Scaled LR output
1peak_64=0.000020 2peak_128=0.000040 3warmup_first_four= ['0.000020', '0.000027', '0.000033', '0.000040']

The first warmup step stays at 2×10−52\times 10^{-5}2×10−5, the old peak, and only then climbs to 4×10−54\times 10^{-5}4×10−5. 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.

SymptomLikely layerWhyFirst check
One microbatch has a huge grad, others are calmdataa poisoned row, exploded token span, or truncated completion with no answer labelsdump that microbatch's ids and token counts
Gradients blow up in the first 20 steps after a batch/LR changeoptimizer / schedulescaled peak without warmup, or linear scaling past a useful batchcompare BglobalB_{\mathrm{global}}Bglobal​, peak LR, warmup
Finite losses, then a skip storm, or LR that effectively doubledscaler / accumulationGradScaler step/update on every microbatch, or loss divided twicecount optimizer steps vs microbatches; reread the mixed-precision contract[8]Reference 8Mixed Precision Training.https://arxiv.org/abs/1710.03740
Clip norm always hits the capclip hiding a causeclipping is a backstop, not a data cleaner[9]Reference 9On the Difficulty of Training Recurrent Neural Networkshttps://proceedings.mlr.press/v28/pascanu13.htmllog 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 KKK into 1 and multiplied the effective learning rate.

instability_router.py
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}))
Instability router output
1scaler_accumulation_bug 2schedule_no_warmup 3data_poisoned_microbatch 4clip_hiding_scale_change

After 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 BglobalB_{\mathrm{global}}Bglobal​ 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.

MethodWhat movesData it wantsDomain-shift storyGPU story
Continued pretrainingfull (or large) weights, next-token on unlabeled domain textlots of unlabeled domain tokensthe base model never saw this language/distribution[10]Reference 10Don't Stop Pretraining: Adapt Language Models to Domains and Tasks.https://aclanthology.org/2020.acl-main.740/closest to pretraining memory
Full SFTall eligible weights, response-token losslarge, clean prompt-response setsbehavior must change broadly, not one sliceFSDP/ZeRO if states don't fit
LoRAsmall adapters, frozen base[11]Reference 11LoRA: Low-Rank Adaptation of Large Language Models.https://arxiv.org/abs/2106.09685small-to-medium labeled setstargeted policy/procedure changeoften 1-few GPUs; activations still matter
QLoRALoRA on a quantized frozen base[12]Reference 12QLoRA: Efficient Finetuning of Quantized Language Models.https://arxiv.org/abs/2305.14314same as LoRAsame as LoRA, tighter memorypaper headline: 65B SFT on a 48GB GPU
Distillationstudent weights, teacher signal[13]Reference 13Distilling the Knowledge in a Neural Network.https://arxiv.org/abs/1503.02531teacher traces plus a student budgetyou already have a good teacher and need a smaller deployabletrain student cheap; teacher may be expensive

Work the same product through five honest cards:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
Decision tree for the access-policy assistant: continued pretraining for about 2B unlabeled domain tokens, full SFT for 200k labeled replies on 8 times 80GB, LoRA for 2k replies that fit, QLoRA for a 70B adapter job on 24GB, and distillation when a 7B student must mimic a ready 70B teacher.
Read down the constraint that actually binds. Domain text you don't have labels for is CPT. A procedure change on a model that already speaks the domain is LoRA. A 70B adapter job on 24GB is QLoRA. A smaller deployable is distillation.
choose_adaptation_method.py
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}))
Method chooser output
1lora 2qlora 3continued_pretraining 4full_sft 5distillation

QLoRA 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 observeNext stage
Model can't follow the procedure even with gold demonstrationsstay on SFT / CPT / LoRA
Several answers are valid and people disagree on rankingreward modeling or a preference-optimization method
A verifier can score code, math, or schemaRLVR / 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.

Fieldpolicy-sft-v4
Operationcontinue / initialize / export
MethodLoRA SFT
Bmicro,K,DB_{\mathrm{micro}}, K, DBmicro​,K,D2, 8, 4
Examples / supervised tokens per update64 / 20,800
Peak LR and warmup2×10−52\times 10^{-5}2×10−5, 200 steps
Checkpoint formatDCP, world_size=4
Last complete step1840, manifest complete=true
Kill budget90 s save, checkpoint every 80 steps
Held-out winnersupport_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 BglobalB_{\mathrm{global}}Bglobal​ from microbatch, KKK, and DDD.
  • Intermediate: Reject an incomplete rank-local dump and a world-size mismatch, and scale peak LR with warmup when DDD 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 latest at 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
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.policy-sft-v4 reloads safetensors from step 1840, sets global_step=1840, and starts AdamW from zeros. Which operation is that?

Correct answer: Initialize, because optimizer, scheduler, and data position were not restored.

Continue requires the full resume bundle. Reloading weights and a step counter while rebuilding Adam moments is a new run that starts from those weights.

2.A rank-local dump has four shard files and metadata. Rank 3's file is missing. The latest pointer already names that directory. What should restore do?

Correct answer: Reject the dump until present files match the manifest and complete=true.

A checkpoint is one complete artifact. A latest pointer can move before every shard is durable. Restore must fail closed on missing shards.

3.You saved policy-sft-v4 with DCP on 4 GPUs and resume on 8 GPUs. Why can that work when four rank00.pt-style files usually cannot?

Correct answer: DCP reshards parallelism-agnostic tensors into the live state_dict using the new layout.

PyTorch DCP loads into the current model's allocated shards. Rank-local torch.save files are bound to the writer layout unless you convert them.

4.per_device_batch=2, gradient_accumulation_steps=8, world_size=4. What is examples per optimizer update?

Correct answer: 64

Global example batch is 2 × 8 × 4 = 64. Any one of those factors is not the update the optimizer sees.

5.You double world_size from 4 to 8, keep microbatch and accumulation fixed, and want Goyal-style linear scaling of a 2e-5 LoRA peak. What peak and early-step behavior match the rule?

Correct answer: Use a 4e-5 peak and warmup so early steps start below that peak.

Linear scaling multiplies LR by the batch multiplier k=2, and Goyal's warmup exists because starting at the scaled peak is what diverges.

6.After the 8-GPU restart, GradScaler step/update runs 8 times per accumulation window of 8. Losses stay finite for a few steps, then the skip count explodes. What failed?

Correct answer: The scaler/accumulation boundary: optimizer-step hooks ran on every microbatch.

The mixed-precision contract keys off the optimizer step. Stepping the scaler each microbatch collapses accumulation and adapts scale on incomplete gradients.

7.You have 2,000 labeled access-policy replies, a 70B base, and one 24GB GPU. The base model already speaks support English. Which method matches the binding constraint?

Correct answer: QLoRA, because this is LoRA's objective under a memory wall the paper targets.

The objective is a small procedure change (LoRA-shaped). The 70B plus 24GB constraint is why QLoRA exists. CPT needs unlabeled domain shift; distillation needs a teacher-to-student deploy goal.

8.The 8B access-policy model mangles internal product nouns. You have ~2B unlabeled policy tokens, 2,000 labeled replies, and 8×80GB. What should start first?

Correct answer: Continued pretraining on the unlabeled corpus, then a small SFT.

SFT on 2,000 rows won't teach a dialect the checkpoint never saw. Gururangan-style continued pretraining targets that unlabeled domain shift, then SFT teaches the procedure.

8 questions remaining.

Next Step
Continue to Reward Modeling from Preference Data

You can now keep a run alive, keep the batch card honest, and pick an adaptation method. Preference training starts from a different signal: once several answers are valid, which one should score higher, and how do we train that ranking without pretending it's another SFT resume?

PreviousLoRA & Parameter-Efficient Tuning
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Checkpointing in torchtune.

PyTorch Contributors · 2026

https://meta-pytorch.org/torchtune/stable/deep_dives/checkpointer.html

ZeRO: Memory Optimizations Toward Training Trillion Parameter Models.

Rajbhandari, S., et al. · 2020 · SC 2020

https://arxiv.org/abs/1910.02054

PyTorch Distributed Checkpoint (DCP) Recipe

PyTorch Contributors · 2026

https://docs.pytorch.org/tutorials/recipes/distributed_checkpoint_recipe.html

Universal Checkpointing with DeepSpeed: A Practical Guide.

DeepSpeed Team · 2026

https://www.deepspeed.ai/tutorials/universal-checkpointing/

Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour.

Goyal, P., Dollár, P., Girshick, R., Noordhuis, P., Wesolowski, L., Kyrola, A., Tulloch, A., Jia, Y., and He, K. · 2017

https://arxiv.org/abs/1706.02677

An Empirical Model of Large-Batch Training.

McCandlish, S., Kaplan, J., Amodei, D., and the OpenAI Dota Team · 2018

https://arxiv.org/abs/1812.06162

Decoupled Weight Decay Regularization.

Loshchilov, I., Hutter, F. · 2019 · ICLR 2019

https://arxiv.org/abs/1711.05101

Mixed Precision Training.

Micikevicius, P., et al. · 2018

https://arxiv.org/abs/1710.03740

On the Difficulty of Training Recurrent Neural Networks

Pascanu, R., Mikolov, T., and Bengio, Y. · 2013 · ICML 2013

https://proceedings.mlr.press/v28/pascanu13.html

Don't Stop Pretraining: Adapt Language Models to Domains and Tasks.

Gururangan, S., Marasovic, A., Swayamdipta, S., et al. · 2020 · ACL 2020

https://aclanthology.org/2020.acl-main.740/

LoRA: Low-Rank Adaptation of Large Language Models.

Hu, E. J., et al. · 2021 · ICLR

https://arxiv.org/abs/2106.09685

QLoRA: Efficient Finetuning of Quantized Language Models.

Dettmers, T., et al. · 2023 · NeurIPS

https://arxiv.org/abs/2305.14314

Distilling the Knowledge in a Neural Network.

Hinton, G., Vinyals, O., & Dean, J. · 2015

https://arxiv.org/abs/1503.02531

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.