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 & AdaptationSupervised Fine-Tuning Pipeline
⚡HardFine-Tuning & Training

Supervised Fine-Tuning Pipeline

Run supervised fine-tuning as a real training system: choose the learning objective before the update surface, verify response-token loss and packing, track the real batch budget, save resumable checkpoints, and export on held-out behavior.

26 min read
Learning path
Step 103 of 178 in the full curriculum
Synthetic Data PipelinesMixed Precision Training

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The chat-template lesson showed how a grounded stale-key row becomes a token stream. The synthetic-data lesson showed how candidate rows get accepted or rejected. Supervised fine-tuning (SFT) is the training system that turns those accepted demonstrations into gradient updates on the replies you actually want.[1]Reference 1TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer[2]Reference 2Fine-Tune Your First LLM.https://meta-pytorch.org/torchtune/stable/tutorials/first_finetune_tutorial.html InstructGPT used supervised demonstrations as that first post-training stage before preference optimization.[3]Reference 3Training Language Models to Follow Instructions with Human Feedback (InstructGPT).https://arxiv.org/abs/2203.02155

Thread T102 asks whether an 18-day-old service-account key is still usable. The accepted answer is: open a rotation ticket, because keys older than 14 days need review. Loss can still fall while that behavior stays broken. Train on prompt tokens, leak T102 into the eval split, pack R550 so it attends to T102, or export the lowest-train-loss checkpoint, and you'll ship a fluent skip of the ticket.

A good SFT run doesn't start with a trainer call. It starts with a behavior target, a leakage-resistant split, a loss mask, a batch budget, checkpoints that can resume, and an evaluation rule for exporting the best artifact. First choose the learning objective. Then choose whether that objective updates every weight or a small adapter. Mixing those two decisions produces expensive runs that answer the wrong question.

SFT pipeline for stale-key thread T102: accepted grouped rows flow through a response-only loss mask into a full-weight, LoRA, or QLoRA update, then held-out export. Prefix tokens keep -100 labels while the rotation-ticket answer and stop token receive loss; attention still reads the prefix as context.
For thread T102, prompt tokens stay visible as context but carry -100, while the rotation-ticket answer and stop token drive the SFT update. Compare full-weight or adapter updates on held-out policy behavior, then export the winning checkpoint.

What an SFT run must decide

Every serious SFT job has to answer the same questions. The T102 failures above are what happens when you skip one:

  1. What is the behavior target?
  2. Is SFT the right objective, or is the missing ingredient still domain pretraining?
  3. Are we updating all weights or only adapters?
  4. What examples and tokens contribute to training loss?
  5. What is the true example and supervised-token batch budget?
  6. Which held-out metric decides the best checkpoint?
  7. Can this stay on one GPU, or do we need Fully Sharded Data Parallel (FSDP) or Zero Redundancy Optimizer (ZeRO)?

Once you phrase the job that way, the trainer is one component: it applies the update. The split, mask, batch card, resume bundle, and export rule still sit outside it.

Separate objective from parameterization

An objective tells the model what signal to learn from. A parameterization tells the trainer which weights may move. They aren't interchangeable.

Failure you observeObjective to investigateWhy
Base model has weak exposure to domain language in unlabeled corporacontinued pretrainingnext-token training on domain text adapts the language distribution before behavior training[4]Reference 4Don't Stop Pretraining: Adapt Language Models to Domains and Tasks.https://aclanthology.org/2020.acl-main.740/
Model can read an access policy but doesn't follow the desired escalation procedureSFTprompt-completion demonstrations directly teach that response behavior[1]Reference 1TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer
Several acceptable answers need ranking by preferencepreference training after establishing an SFT baselinecomparisons express relative preference rather than a single target answer[5]Reference 5Direct Preference Optimization: Your Language Model is Secretly a Reward Model.https://arxiv.org/abs/2305.18290

After choosing SFT, make a second decision:

SFT parameterizationWhat moves during the same supervised objectiveWhen to test it
Full fine-tuningall trainable model weightswhen memory permits and adapters may be too restrictive
LoRAsmall low-rank adapter matrices; base weights stay frozenwhen iteration speed, memory, or many task variants matter[6]Reference 6LoRA: Low-Rank Adaptation of Large Language Models.https://arxiv.org/abs/2106.09685
QLoRALoRA adapters while the frozen base is stored in 4-bit formwhen the base model doesn't fit comfortably at higher precision[7]Reference 7QLoRA: Efficient Finetuning of Quantized Language Models.https://arxiv.org/abs/2305.14314

LoRA and QLoRA aren't alternatives to SFT. They can be the parameterization used for an SFT run. Continued pretraining is a different objective; it can also be parameter-efficient in an appropriate setup, but it still doesn't become SFT.

Full weights or adapters

Full fine-tuning

Full fine-tuning is the simplest conceptual update: every eligible parameter may change to reduce supervised response loss. Test it when:

  • the model fits with its gradients and optimizer state
  • one merged checkpoint is operationally simpler than serving adapters
  • an adapter baseline underfits your held-out behavior metric

LoRA

LoRA is still SFT when trained on prompt-response rows. Use it when:

  • iteration speed or training memory limits matter
  • you need separate task or tenant variants over one base model
  • you want a strong adapter baseline before paying for full updates

LoRA freezes the base model weights and trains low-rank adapter matrices inside selected layers, which reduces trainable parameters and memory compared with full fine-tuning.[6]Reference 6LoRA: Low-Rank Adaptation of Large Language Models.https://arxiv.org/abs/2106.09685

QLoRA

Use QLoRA when:

  • LoRA is the right algorithmic choice
  • the frozen base model itself is too large to keep in BF16/FP16 memory
  • you still want adapter training instead of full-weight updates

QLoRA keeps the adapter-training idea but backpropagates through a frozen 4-bit quantized base model into LoRA adapters, which is why it can make larger models fit in less memory.[7]Reference 7QLoRA: Efficient Finetuning of Quantized Language Models.https://arxiv.org/abs/2305.14314 The comparison that matters is held-out behavior under an honest memory and compute budget. A cheap run that fails the task isn't a win.

Reject broken demonstrations before training

For this pipeline, store a prompt, a desired completion, and a group key for leakage-resistant splitting on every SFT row. A missing completion isn't harmless missing metadata: it's a training example with no answer to teach.

validate_sft_rows.py
1rows = [ 2 {"thread_id": "T102", "prompt": "Stale key", "completion": "Open a rotation ticket; keys older than 14 days need review."}, 3 {"thread_id": "B550", "prompt": "Expired key", "completion": ""}, 4 {"prompt": "Privileged role change", "completion": "Escalate privileged-role changes."}, 5] 6 7required = {"thread_id", "prompt", "completion"} 8accepted, rejected = [], [] 9for index, row in enumerate(rows): 10 missing = sorted(required - row.keys()) 11 if missing: 12 rejected.append((index, f"missing {missing}")) 13 elif not row["completion"].strip(): 14 rejected.append((index, "empty completion")) 15 else: 16 accepted.append(row) 17 18print("accepted_threads=", [row["thread_id"] for row in accepted]) 19print("rejected=", rejected)
Row contract output
1accepted_threads= ['T102'] 2rejected= [(1, 'empty completion'), (2, "missing ['thread_id']")]

Split by the unit that could leak

Before formatting rows, reserve evaluation examples that training can't imitate through near duplicates. For a policy assistant, several messages from one access incident or one policy document often share facts and phrasing. Randomly splitting individual messages can put one part of the same case in training and another in evaluation.

Group by the smallest deployment unit that should be unseen at evaluation time: incident thread, policy document, tenant, or time window. The tiny check below keeps every message from an incident on one side of the split.

grouped_sft_split.py
1rows = [ 2 {"case_id": "T102", "turn": 1, "answer": "Open a rotation ticket; keys older than 14 days need review."}, 3 {"case_id": "T102", "turn": 2, "answer": "Approve access after the rotation ticket is filed."}, 4 {"case_id": "R550", "turn": 1, "answer": "Escalate privileged-role changes."}, 5 {"case_id": "P771", "turn": 1, "answer": "Cite the session-timeout policy."}, 6] 7 8eval_cases = {"R550"} 9train = [row for row in rows if row["case_id"] not in eval_cases] 10evaluation = [row for row in rows if row["case_id"] in eval_cases] 11 12train_cases = {row["case_id"] for row in train} 13held_out_cases = {row["case_id"] for row in evaluation} 14assert train_cases.isdisjoint(held_out_cases) 15 16print("train_cases=", sorted(train_cases)) 17print("eval_cases=", sorted(held_out_cases)) 18print("case_overlap=", train_cases & held_out_cases)
Grouped split output
1train_cases= ['P771', 'T102'] 2eval_cases= ['R550'] 3case_overlap= set()

Two turns from incident T102 have different wording. Can one train and the other evaluate if their token overlap is low?

Answer

No. They share the deployment unit that can leak facts and phrasing, so both turns stay in the same split. Group by incident, document, tenant, or time window before formatting and tokenization.

Data path: template, tokenize, label, pack

An SFT run usually performs these steps:

  1. read structured examples
  2. apply the model-specific chat template
  3. run tokenization
  4. label desired response tokens and mask the prompt/context tokens
  5. pack short examples with sequence-boundary handling, or pad them into batches
  6. feed the trainer

The instruction-tuning lesson already covered chat-template mechanics. The operational lesson here is that you version this whole path together. If you change the template or masking logic, you changed the training distribution and therefore the experiment.[8]Reference 8Transformers Documentation: Writing a chat template.https://huggingface.co/docs/transformers/main/en/chat_templating_writing

The label mask is easy to get wrong because the input still contains the system instruction, user request, and formatting control tokens. For response-only SFT, the desired answer tokens (including the turn terminator when the model must learn when to stop) contribute to loss. Prefix tokens provide context, not answer targets. The tiny example uses -100, the ignored label value used by PyTorch cross-entropy.

sft_loss_mask.py
1IGNORE_INDEX = -100 2 3tokens = [ 4 ("prefix", "<system>"), 5 ("prefix", "Follow access policy."), 6 ("prefix", "<user>"), 7 ("prefix", "Key T102 is 18 days old."), 8 ("prefix", "<assistant>"), 9 ("target", "Open a rotation ticket; keys older than 14 days need review."), 10 ("target", "<end_of_turn>"), 11] 12 13labels = [ 14 token if span == "target" else IGNORE_INDEX 15 for span, token in tokens 16] 17 18supervised = [ 19 token 20 for (_, token), label in zip(tokens, labels) 21 if label != IGNORE_INDEX 22] 23print("supervised_tokens=", supervised) 24print("masked_positions=", sum(label == IGNORE_INDEX for label in labels))
Loss mask output
1supervised_tokens= ['Open a rotation ticket; keys older than 14 days need review.', '<end_of_turn>'] 2masked_positions= 5

Don't confuse the label mask with the attention mask. The label mask decides which positions contribute to loss. The attention mask decides which earlier positions a token can read. Prompt tokens should remain visible as context even when their labels are -100; packed examples also need attention boundaries so one example can't read another.

Why bother masking easy prefix tokens? If a long repeated instruction is easy to predict, its small losses can dominate the average and hide poor answer learning:

response_only_loss.py
1prefix_losses = [0.03, 0.04, 0.02, 0.05, 0.03] 2answer_losses = [2.20, 1.80] 3 4full_sequence_loss = sum(prefix_losses + answer_losses) / len(prefix_losses + answer_losses) 5response_only_loss = sum(answer_losses) / len(answer_losses) 6 7print(f"full_sequence_loss={full_sequence_loss:.3f}") 8print(f"response_only_loss={response_only_loss:.3f}") 9assert response_only_loss > full_sequence_loss
Response-only loss output
1full_sequence_loss=0.596 2response_only_loss=2.000

Those two numbers are the same token-level cross-entropy, averaged over different positions. For response-only SFT, the reported loss is the mean negative log-likelihood over the labeled set TTT (here, the answer and stop tokens), not over the whole sequence:

LSFT(θ)=−1∣T∣∑t∈Tlog⁡pθ(yt∣y<t)\mathcal{L}_{\text{SFT}}(\theta) = -\frac{1}{|T|}\sum_{t \in T} \log p_\theta(y_t \mid y_{<t})LSFT​(θ)=−∣T∣1​∑t∈T​logpθ​(yt​∣y<t​)

θ\thetaθ is the trainable parameters, yty_tyt​ is the token at position ttt, and y<ty_{<t}y<t​ is the prefix the model may attend to. In the toy row, TTT has two positions and the mean is 2.02.02.0. PyTorch's cross-entropy with ignore_index=-100 already averages that way. Forget the mask, and the easy prefix tokens pull the average down to 0.5960.5960.596.

You rarely build this mask by hand. In Hugging Face TRL's SFTTrainer, prompt-completion datasets use completion-only loss by default. Conversational datasets can set assistant_only_loss=True; their chat template must expose assistant spans through generation markers. Current TRL can substitute supported training templates for known model families, but a custom template still needs verification.[1]Reference 1TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer Inspect a tokenized example before launching a long run: it should reveal exactly which answer and stop tokens receive labels.

Mask inspection must happen after truncation as well. Current TRL SFTConfig defaults to truncation_mode="keep_start": it keeps the beginning of the concatenated sequence and drops the end. If the prompt fills max_length, the completion (the only labeled span) disappears. TRL then drops examples whose labels are all -100, so you lose the row instead of training on a prompt-only sequence. That's still silent data loss. When packing is on, this truncation step is skipped and overflow is handled by the packing strategy instead; bfd still discards overflow tokens.[1]Reference 1TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer

The toy below is that keep_start cut: five prefix tokens fill max_length, so the ticket answer never appears. Reject the row or raise max_length; don't assume every accepted demonstration survived tokenization.

reject_truncated_completion.py
1IGNORE_INDEX = -100 2 3prompt_tokens = ["<system>", "policy", "<user>", "stale_key", "<assistant>"] 4answer_tokens = ["open_ticket", "<end_of_turn>"] 5max_length = 5 6 7kept_tokens = (prompt_tokens + answer_tokens)[:max_length] 8labels = [ 9 token if position >= len(prompt_tokens) else IGNORE_INDEX 10 for position, token in enumerate(kept_tokens) 11] 12has_answer_label = any(label != IGNORE_INDEX for label in labels) 13 14print("kept_tokens=", kept_tokens) 15print("has_answer_label=", has_answer_label) 16print("decision=", "keep" if has_answer_label else "reject_or_increase_max_length")
Truncation guard output
1kept_tokens= ['<system>', 'policy', '<user>', 'stale_key', '<assistant>'] 2has_answer_label= False 3decision= reject_or_increase_max_length

Pack short examples while preserving their boundaries

Most SFT datasets are short replies. Padding each one to a fixed length wastes compute on padding tokens. Packing places several examples in a nearly full training block so more positions carry real tokens.

For independent instruction examples, a reply for case R550 shouldn't gain context from case T102 only because the loader put them in one block. A boundary-aware packed attention path records each segment, resets positions at each sequence boundary, and prevents that cross-example attention:

packed_boundary_check.py
1segments = [ 2 ["T102 prompt", "T102 response", "<eos>"], 3 ["R550 prompt", "R550 response", "<eos>"], 4] 5 6flat = [(sequence_id, token) for sequence_id, row in enumerate(segments) for token in row] 7position_ids = [position for row in segments for position, _ in enumerate(row)] 8 9def may_attend(query_index, key_index): 10 query_sequence, _ = flat[query_index] 11 key_sequence, _ = flat[key_index] 12 return key_index <= query_index and query_sequence == key_sequence 13 14r550_response = 4 15assert position_ids == [0, 1, 2, 0, 1, 2] 16assert not may_attend(r550_response, 1) 17print("position_ids=", position_ids) 18print("R550_response_sees_T102_response=", may_attend(r550_response, 1))
Packed boundary output
1position_ids= [0, 1, 2, 0, 1, 2] 2R550_response_sees_T102_response= False

Reset position IDs are boundary metadata, not a universal attention mask. The attention implementation still has to honor those boundaries when it computes attention.

What a failed packed batch looks like. Walk one block of six tokens (T102 then R550) after an upgrade that keeps reset position_ids but drops segment isolation:

IndexTokenposition_idIntended attend-toBroken path (no segment mask)
0T102 prompt0T102 onlyT102 only
1T102 response1T102 onlyT102 only
2T102 eos2T102 onlyT102 only
3R550 prompt0R550 onlyT102 + R550 (cross-example leak)
4R550 response1R550 onlyT102 + R550 (R550 answer conditions on T102)
5R550 eos2R550 onlyT102 + R550

The same block can also fail via truncation then pack: if T102 is kept after a cut that left every answer label at -100, packing still fills the block with tokens, but that segment teaches no supervised response. Inspect both attention reachability and nonzero answer labels per segment before a long run.

Current TRL exposes packing=True and eval_packing. Its default best-fit-decreasing (bfd) strategy packs intact examples efficiently and truncates overflow sequences. bfd_split preserves overflow tokens by splitting long sequences before packing. wrapped concatenates everything into a stream and cuts mid-sequence; TRL documents that this can mix unrelated examples and break continuity. With bfd, TRL enables padding-free processing. That flattened path relies on FlashAttention 2 or 3 to honor sequence boundaries; without a compatible attention implementation, adjacent samples may contaminate each other.[1]Reference 1TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer The right check isn't "never pack." It's: choose an SFT-appropriate strategy, confirm the supported attention path, and inspect a packed batch after library upgrades.

Packing also changes what an epoch means. TRL logs num_tokens during the run. One epoch after packing is one pass over packed blocks, not one pass over original rows. If you set max_steps from an unpacked row count, you can overshoot the intended token budget and overfit without noticing.

A packed batch resets position IDs for R550, but R550's response can still attend to T102. Is the packing path correct?

Answer

No. Reset positions don't enforce attention isolation. The segment mask or padding-free attention metadata must block cross-example attention, and every packed segment must retain at least one non--100 answer label.

Hyperparameters are experiments, not promises

SFT begins from a pretrained checkpoint, so a useful configuration starts conservatively and earns expansion through held-out results. A library default isn't evidence that one value fits your dataset or parameterization.

Current TRL SFTConfig defaults learning_rate to 2e-5, turns gradient_checkpointing on, and enables bf16 when fp16 isn't set. Its PEFT guidance suggests about 1e-4 for adapters because only new parameters are being learned.[1]Reference 1TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer That bf16 default is already a precision policy. Record it with the run; the next chapter measures whether the arithmetic is safe.

KnobBaseline to testWhat decides whether it survives
Learning rate2e-5 for full updates; about 1e-4 for adaptersa short sweep on the actual update surface and held-out task metrics
Passes over datastart with a small budget; watch num_tokens, not only epochswidening train/eval gap or declining behavior metrics
Warmup and scheduleset explicitly; don't jump straight to peak LRearly instability and sweep results
Weight decay and clippinglog them with the run, even when disabledstability and evaluation evidence, not inherited habit
Precisionthe dtype SFTConfig actually enabledheld-out metric plus nonfinite-step count, not throughput alone

Don't turn 2e-5 or 1e-4 into a rule. Record parameterization, trainable-parameter count, sequence length, effective token budget, data snapshot, precision, and evaluation result beside every candidate configuration.

Warmup, clipping, and decay aren't decoration. Warmup limits early updates while the run is most sensitive to its initial configuration; jumping immediately to a peak adapter rate of 1e-4 can make the first steps unnecessarily noisy. Gradient clipping doesn't fix bad data, but it can stop one pathological batch from damaging the checkpoint. Weight decay belongs in the recipe record even when you disable it, so the next sweep doesn't inherit a silent default.

Effective batch size is also a token budget

Training discussions get sloppy fast here. The batch that sits on one device isn't the same thing as the batch the optimizer sees before one update. For equal-length examples, the usual rule is:

text
1examples_per_update = per_device_batch * gradient_accumulation_steps * world_size

Packed response examples aren't equal length. This formula reports examples per optimizer update, but the number of supervised answer tokens can still change from update to update. Track both.

sft_effective_batch.py
1per_device_batch = 4 2grad_accum = 8 3world_size = 4 4examples_per_update = per_device_batch * grad_accum * world_size 5 6supervised_tokens_per_microbatch_on_one_rank = [180, 212, 164, 220, 198, 204, 175, 207] 7supervised_tokens_per_update = sum(supervised_tokens_per_microbatch_on_one_rank) * world_size 8 9print(f"examples_per_update={examples_per_update}") 10print(f"supervised_tokens_per_update={supervised_tokens_per_update}")
Effective batch output
1examples_per_update=128 2supervised_tokens_per_update=6240

Always calculate changed configurations instead of reasoning from one changed knob:

compare_update_sizes.py
1def examples_per_update(per_device_batch, accumulation, world_size): 2 return per_device_batch * accumulation * world_size 3 4run_a = examples_per_update(4, 8, 4) 5run_b = examples_per_update(2, 16, 8) 6 7print("run_a_examples_per_update=", run_a) 8print("run_b_examples_per_update=", run_b) 9print("ratio_b_over_a=", run_b / run_a) 10assert run_b == 2 * run_a
Run comparison output
1run_a_examples_per_update= 128 2run_b_examples_per_update= 256 3ratio_b_over_a= 2.0

Two habits follow from this:

  • when someone reports batch size, ask for accumulation, world size, packing, and supervised tokens too
  • compare learning-rate sweeps with similar token budgets or explain why they differ

What a resumable run must save

A real SFT resume bundle isn't only model_state_dict.

At minimum, retain:

  • model weights
  • optimizer state
  • scheduler state
  • global step / epoch
  • RNG state and sampler/data position when repeatable continuation matters
  • tokenizer + chat-template version
  • training config, data manifest, and evaluation manifest
  • best metric so far

Why this matters:

  • optimizer state controls the next update
  • scheduler state controls learning rate at resume
  • sampler state prevents quietly repeating or skipping shuffled training rows
  • tokenizer/template version controls what the model thinks each token means

Exact bit-for-bit replay may still depend on hardware, kernels, and determinism settings. The resume bundle should at least prevent avoidable changes to data order, learning-rate state, and model selection. torchtune and DeepSpeed both document resume flows explicitly instead of treating checkpointing as a trivial file write.[9]Reference 9Checkpointing in torchtune.https://meta-pytorch.org/torchtune/stable/deep_dives/checkpointer.html[10]Reference 10Universal Checkpointing with DeepSpeed: A Practical Guide.https://www.deepspeed.ai/tutorials/universal-checkpointing/

Side-by-side stacks for an SFT checkpoint: a weights-only export keeps only model or adapter weights, while a resume bundle also keeps optimizer, scheduler and step, RNG and sampler, tokenizer and template plus manifests, and the best held-out policy_pass_rate.
Weights ship a model. Resume needs the rest of the stack: optimizer and scheduler, data position, formatting versions, and the recorded winner on held-out policy_pass_rate. Missing layers mean the next step is a new run, not a continuation.

A minimal manifest should make missing resume state obvious:

resume_bundle_manifest_check.py
1required = { 2 "model_state", 3 "optimizer_state", 4 "scheduler_state", 5 "global_step", 6 "rng_state", 7 "sampler_state", 8 "tokenizer_version", 9 "chat_template_version", 10 "data_manifest", 11 "eval_manifest", 12 "best_metric", 13} 14 15resume_bundle = { 16 "model_state": "weights/step_600.safetensors", 17 "optimizer_state": "optimizer/step_600.pt", 18 "scheduler_state": "scheduler/step_600.pt", 19 "global_step": 600, 20 "rng_state": "rng/step_600.pt", 21 "sampler_state": {"epoch": 1, "batches_consumed": 120}, 22 "tokenizer_version": "policy-sft-tokenizer-v3", 23 "chat_template_version": "llama3-access-policy-v2", 24 "data_manifest": "data/sft_manifest_2026-05-20.json", 25 "eval_manifest": "eval/access_policy_behavior_v4.json", 26 "best_metric": {"name": "policy_pass_rate", "value": 0.97}, 27} 28 29missing = sorted(required - resume_bundle.keys()) 30print("resume_ready=", not missing) 31print("best_metric=", resume_bundle["best_metric"])
Checkpoint manifest output
1resume_ready= True 2best_metric= {'name': 'policy_pass_rate', 'value': 0.97}

A resumed run reloads model weights and global step but not optimizer, scheduler, sampler, or tokenizer-template versions. Can its next metrics be compared as uninterrupted continuation?

Answer

No. The next update may use different momentum, learning rate, data position, randomness, or token formatting. Treat it as a new run unless the complete resume manifest is restored and validated.

Choosing the best checkpoint

The best checkpoint is the one that optimizes the product metric you care about on held-out data.

Bad rules:

  • lowest train loss
  • latest step
  • biggest file

Better evidence:

  • highest exact or structured task success where the behavior is verifiable
  • best human preference rate on a blinded held-out set when quality is subjective
  • judge-assisted scoring only after calibrating the judge against human or verifiable decisions
  • lowest validation loss only when that truly matches the task

If the task is structured access-policy replies, check policy compliance and schema validity before style preference. Loss is useful for training diagnostics, but it doesn't define deployment quality.

The export rule for this run is a gate, then a ranking, then a tie-break:

Diagram showing format_pass >= 0.98?, Skip, Max policy_pass, and Export winner.
format_pass >= 0.98?, Skip, Max policy_pass, and Export winner.
select_sft_checkpoint.py
1checkpoints = [ 2 {"step": 200, "val_loss": 1.91, "policy_pass_rate": 0.93, "format_pass_rate": 0.99}, 3 {"step": 400, "val_loss": 1.77, "policy_pass_rate": 0.91, "format_pass_rate": 1.00}, 4 {"step": 600, "val_loss": 1.79, "policy_pass_rate": 0.97, "format_pass_rate": 0.98}, 5] 6 7eligible = [row for row in checkpoints if row["format_pass_rate"] >= 0.98] 8best = max(eligible, key=lambda row: (row["policy_pass_rate"], -row["val_loss"])) 9 10print("best_step=", best["step"]) 11print("best_policy_pass_rate=", best["policy_pass_rate"]) 12print("lowest_loss_step=", min(checkpoints, key=lambda row: row["val_loss"])["step"])
Checkpoint selection output
1best_step= 600 2best_policy_pass_rate= 0.97 3lowest_loss_step= 400

Single GPU first, then scale out

Don't start thread T102 on 8 GPUs. First prove the recipe on one GPU:

  1. one GPU
  2. tiny eval slice
  3. frequent checkpoints
  4. verified template/masking
  5. one clean metric

Only then widen world size or sequence length.

Signs that one GPU is still fine

  • the model and activations fit
  • throughput is acceptable
  • you're still debugging correctness
  • labels are the main uncertainty, not compute budget

Signs that you need FSDP or ZeRO

  • full-model weights and optimizer state don't fit
  • activation memory collapses the run at useful context lengths
  • checkpointing and accumulation still leave the job too slow
  • you need larger world size to hit the schedule

That bridge matters. FSDP and ZeRO aren't decorations on top of a stable recipe. They let the same recipe continue when memory stops fitting on one device by sharding model states across workers.[11]Reference 11FullyShardedDataParallelhttps://docs.pytorch.org/docs/stable/fsdp.html[12]Reference 12ZeRO: Memory Optimizations Toward Training Trillion Parameter Models.https://arxiv.org/abs/1910.02054 Precision is the other silent scaler: if SFTConfig already turned on bf16, you have a mixed-precision policy before you add a second GPU.

Minimal operational skeleton

The exact library can vary, but the shape is stable:

sft_pipeline_shape.txt
1dataset -> template -> tokenizer -> collator/mask -> trainer 2 -> periodic eval -> checkpoint save -> best-checkpoint export

TRL, torchtune, and the Alignment Handbook all expose variations of this same loop.[1]Reference 1TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer[13]Reference 13alignment-handbook: Robust recipes to align language models with human and AI preferences.https://github.com/huggingface/alignment-handbook[14]Reference 14Welcome to the torchtune Documentation.https://meta-pytorch.org/torchtune/stable/index.html

For a TRL prompt-completion dataset, a configuration skeleton makes the decisions visible. Configure model_with_lora_adapters with a supported FlashAttention 2 or 3 implementation before enabling bfd packing:

trl_sft_config_fragment.py
1from trl import SFTConfig, SFTTrainer 2 3args = SFTConfig( 4 output_dir="runs/access-policy-sft-lora", 5 learning_rate=1e-4, # adapter baseline to evaluate, not a universal rule 6 max_length=1024, 7 packing=True, 8 packing_strategy="bfd", # keep examples intact; bfd also enables padding-free 9 completion_only_loss=True, # train on the completion field, not the prompt 10 eval_strategy="steps", 11 eval_steps=100, 12 save_steps=100, 13) 14 15trainer = SFTTrainer( 16 model=model_with_lora_adapters, 17 args=args, 18 train_dataset=train_rows, # {"prompt": ..., "completion": ...} 19 eval_dataset=held_out_rows, 20 processing_class=tokenizer, 21)

This fragment deliberately leaves model loading, adapter construction, and the data manifest outside the snippet. They must be versioned inputs to a real run, not invisible defaults.

Common pitfalls

Confusing the objective with the update surface

  • Symptom: A team says "we tried LoRA instead of SFT," or switches to full updates without changing data or evaluation.
  • Cause: LoRA and full fine-tuning describe which parameters move; SFT describes the supervised loss objective.
  • Fix: Write down the failed behavior and desired objective first. Then compare adapter and full-weight SFT only if update capacity or memory is the actual uncertainty.

Evaluating examples that share a case with training

  • Symptom: Held-out score is excellent, but the model fails on new tenants, policies, or access incidents.
  • Cause: Rows were randomly split while near-duplicate turns from the same incident or document appeared on both sides.
  • Fix: Group the split by incident thread, policy source, tenant, or later time window, then re-run selection.

Reporting micro-batch as if it were the real batch

  • Symptom: Two runs both claim "batch size 4," but convergence looks different and nobody can reconcile the results.
  • Cause: The reported number ignored accumulation or world size, so the optimizer was seeing different effective batches.
  • Fix: Publish per_device_batch, gradient_accumulation_steps, world_size, packed/padded mode, examples per update, and supervised tokens per update.

Saving weights without resume state

  • Symptom: Resume starts, but the learning rate jumps, checkpoint selection resets, or the run diverges from prior curves.
  • Cause: The checkpoint kept weights but dropped optimizer state, scheduler position, or best-metric record.
  • Fix: Treat resume state as part of the checkpoint contract: weights, optimizer, scheduler, global step, RNG/sampler state, template/tokenizer version, data and eval manifests, and best metric.

Using a packing path that crosses example boundaries

  • Symptom: Loss falls smoothly, but the model learns transitions that never occur in real conversations.
  • Cause: A packing strategy or attention implementation let tokens from one independent example condition another.
  • Fix: For TRL SFT, prefer bfd with a supported FlashAttention 2 or 3 implementation and inspect packed batches. Use bfd_split deliberately when preserving overflow matters; don't silently substitute wrapped or an unsupported attention path.

Selecting the best checkpoint by train loss

  • Symptom: Exported checkpoint has the prettiest curve but underperforms on held-out policy tasks.
  • Cause: Train loss rewards fitting seen data, not solving the downstream task the product cares about.
  • Fix: Export the checkpoint that wins on held-out verifiable metrics or calibrated human-preference evaluation that matches deployment.
Complete the lesson

Mastery Check

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

1.A team has chosen response supervision but is debating full fine-tuning versus LoRA. Which statement keeps the decisions separate?

Correct answer: The objective defines which behavior and tokens receive loss; parameterization defines whether all weights or adapters receive the update.

SFT behavior is a data and loss decision. Full weights, LoRA, and QLoRA are update surfaces for that same objective, so conflating them can produce an expensive run that trains the wrong tokens.

2.An incident dataset has many messages from the same thread. What split prevents the most direct leakage?

Correct answer: Assign all rows from one thread or deployment unit to the same split, then evaluate on unseen groups.

Near-duplicate context across a thread can make validation look easy. Grouping by incident, policy, tenant, or time window tests generalization to a genuinely unseen deployment unit.

3.For response-only SFT, which masking behavior is correct for a prompt followed by an answer and stop token?

Correct answer: Use -100 labels on prefix tokens, label answer and stop tokens, and keep the prefix visible as attention context.

The ignore index removes prefix positions from cross-entropy but does not remove their context. Including the stop token teaches the model where the supervised response ends.

4.After keep-start truncation, a row contains only prompt tokens and every label is -100. What should the data pipeline do?

Correct answer: Drop or lengthen the row because it no longer contributes a supervised response signal.

Keep-start truncation can remove the completion, leaving no nonignored labels. That is silent data loss for response SFT, so the pipeline must reject the row or raise max length and verify labels after tokenization.

5.A packed batch uses ordinary causal attention and only resets position IDs at each example boundary. What additional property is required?

Correct answer: Boundary-aware attention metadata or a block mask that prevents one example from reading another inside the packed block.

Position resets affect position features, not attention edges. Packing is valid only when the attention implementation honors segment boundaries and every segment retains a supervised label.

6.A run uses per-device batch 4, accumulation 8, and world size 4. Its one-rank accumulated microbatches contain 1,560 supervised tokens. What effective budget follows?

Correct answer: 128 examples per update and 6,240 supervised tokens per update after multiplying the one-rank token count by world size.

Examples per update are 4 x 8 x 4 = 128. The measured token total is per rank, so the global supervised-token budget is 1,560 x 4 = 6,240; both figures should be logged.

7.A resumed SFT run repeats shuffled rows and restarts its learning-rate schedule even though model weights were saved. What checkpoint element is missing?

Correct answer: Optimizer and scheduler state plus global progress, RNG or sampler state, and data/template manifests needed to continue the same experiment.

Weights-only saves are exports, not resumable checkpoints. Optimizer moments, scheduler position, sampler and RNG state, and versioned data/template metadata are part of the run's continuation contract.

8.An export rule requires format_pass_rate >= 0.98, then maximizes policy_pass_rate, using validation loss only as a tie-breaker. Results are step 200: 0.99/0.93/1.91, step 400: 1.00/0.91/1.77, step 600: 0.98/0.97/1.79. Which step wins?

Correct answer: Step 600, because it meets the format gate and has the highest policy pass rate.

All three meet the format threshold, so policy pass rate decides: 0.97 at step 600 beats 0.93 and 0.91. Validation loss matters only if the primary policy metric ties.

9.One SFT job still has unverified templating and masking but fits one GPU. Another has verified data but full weights and optimizer state do not fit. Which rollout matches the article?

Correct answer: Debug the first job on one GPU, and consider FSDP or ZeRO for the verified job whose states no longer fit.

A small honest run isolates template, mask, and metric bugs before distributed complexity. Sharding is appropriate when a verified recipe cannot fit or meet its schedule on one device.

9 questions remaining.

Next Step
Continue to Mixed Precision Training

The SFT recipe now has a response-only mask, a real batch budget, a resumable checkpoint, and a held-out export rule. Current TRL `SFTConfig` already enables <span data-glossary="bf16">BF16</span> when FP16 isn't set, so the next chapter measures whether that arithmetic keeps updates stable before you scale the same recipe across devices.

PreviousSynthetic Data Pipelines
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

TRL Documentation: SFT Trainer.

Hugging Face · 2026

https://huggingface.co/docs/trl/sft_trainer

Fine-Tune Your First LLM.

PyTorch Contributors · 2026

https://meta-pytorch.org/torchtune/stable/tutorials/first_finetune_tutorial.html

Training Language Models to Follow Instructions with Human Feedback (InstructGPT).

Ouyang, L., et al. · 2022 · NeurIPS 2022

https://arxiv.org/abs/2203.02155

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/

Direct Preference Optimization: Your Language Model is Secretly a Reward Model.

Rafailov, R., et al. · 2023

https://arxiv.org/abs/2305.18290

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

Transformers Documentation: Writing a chat template.

Hugging Face · 2026

https://huggingface.co/docs/transformers/main/en/chat_templating_writing

Checkpointing in torchtune.

PyTorch Contributors · 2026

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

Universal Checkpointing with DeepSpeed: A Practical Guide.

DeepSpeed Team · 2026

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

FullyShardedDataParallel

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/stable/fsdp.html

ZeRO: Memory Optimizations Toward Training Trillion Parameter Models.

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

https://arxiv.org/abs/1910.02054

alignment-handbook: Robust recipes to align language models with human and AI preferences.

Hugging Face · 2025

https://github.com/huggingface/alignment-handbook

Welcome to the torchtune Documentation.

PyTorch Contributors · 2026

https://meta-pytorch.org/torchtune/stable/index.html

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.