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 196 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/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics 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/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch 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/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData 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/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel 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 & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/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
LearnCore LLM FoundationsInstruction Tuning & Chat Templates
⚡MediumFine-Tuning & Training

Instruction Tuning & Chat Templates

Teach a base language model to answer as an assistant: curate grounded SFT rows, serialize chat turns exactly, choose loss targets, pack safely, and detect serving-time template drift.

23 min read
Learning path
Step 60 of 196 in the full curriculum
LLM Benchmarks & LimitationsDimensionality Reduction for Embeddings

Personalize this lesson

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

The answer can be right and the assistant can still fail. A model trained with one set of speaker labels may receive another at serving time, then continue with a new user turn instead of answering. The text looks plausible, so a normal spot check can miss the boundary error.

The last chapter scored CodeAssist on private policy cases: freeze-deploy, rollback-runbook, and secret-rotation. Keep those rows as holdout. They tell you whether an answer is grounded, not how to teach the assistant to write that answer. This lab takes one related policy row and follows it through data curation, serialization, loss, generation, and release review.

A base language model predicts plausible next tokens. It isn't reliably trained to read a user turn, answer as a policy assistant, cite the evidence, and stop. Instruction tuning adapts the model toward that behavior; supervised fine-tuning (SFT) does so from approved instruction-response examples. The same chat template has to survive from data prep to serving.

Use a separate demonstration so training never collides with the eval set you just built:

Policy evidence: Service-account keys older than 14 days require a rotation ticket before continued use.

A base model can continue a policy chat as another user question, a 30-day delay, or a grounded 14-day rotation ticket. Supervised fine-tuning masks system, user, and role tokens and trains only Open, ticket, and the assistant end marker. Serving rebuilds the same role boundaries before generation starts.
The left column still has several plausible next-token paths. SFT raises only the grounded span: Open, ticket, and the assistant end marker. Serving has to rebuild the same role boundaries before generation starts.

Before moving on, predict the first supervised choice. In the row above, should training raise the probability of the user's question, the grounded assistant answer, or both? The table below turns that choice into a concrete contract.

One example defines a behavior contract

A pretrained causal language model has one basic mechanism: given preceding tokens, assign probabilities to the next token. It can sometimes answer a question because conversations appeared in pretraining data, but answering isn't yet a dependable product contract.

For an access-policy assistant, one SFT training row can look like this:

RoleContentWhat this turn contributes
systemAnswer from the supplied policy evidence.Sets the behavior boundary.
userMy service-account key is 18 days old. Can I keep using it?Gives the user's request.
assistantOpen a rotation ticket because keys older than 14 days require review.Supplies the continuation to reward.

During SFT, the optimizer increases the probability of target response tokens conditioned on the turns that came before them. Prompt text remains useful context even when it isn't a supervised target.

In InstructGPT, supervised demonstrations were the first post-training stage before preference feedback was used to refine response quality.[1]Reference 1Training Language Models to Follow Instructions with Human Feedback (InstructGPT).https://arxiv.org/abs/2203.02155 That sequence gives this lab its order: make one grounded answer and token contract correct before adding preference judgments.

Diagram showing Grounded SFT row 14 days + ticket, Chat template role-token stream, Optimize chosen target tokens, and Serve + evaluate same token contract.
Grounded SFT row 14 days + ticket, Chat template role-token stream, Optimize chosen target tokens, and Serve + evaluate same token contract.

SFT isn't a guarantee that the model only changes style or formatting. Fine-tuning can alter factual behavior and task competence too. LIMA provides a useful, narrower result: with a capable base model, a carefully curated set of 1,000 demonstrations produced strong response-format and instruction-following behavior in its experiments.[2]Reference 2LIMA: Less Is More for Alignment.https://arxiv.org/abs/2305.11206

Treat that result as evidence for data quality, not a promise that every task needs little data. A policy assistant still needs coverage, grounding checks, and holdouts that are separate from training rows.

Chat messages eventually become one token stream

Applications store a conversation as structured records. The language model receives tokens. A chat template is the serialization rule between those two representations: it adds role markers, delimiters, whitespace, and, when appropriate, the cue that a new assistant response should begin.[3]Reference 3Transformers Documentation: Chat templates.https://huggingface.co/docs/transformers/main/en/chat_templating

Take the policy row from above with a complete user turn. Before looking at the figure, predict the final token sequence for generation: should it close the user message and stop, or close it and open an assistant turn? That one boundary decides whether the model answers or keeps writing the transcript.

One ChatML prefix of system policy evidence and a stale-key user question branches into three endings: a closed training answer with an end marker, a fresh generation that stops at an assistant-start cue, and an open JSON prefill. Starting a new assistant turn and continuing a final assistant message are mutually exclusive.
The system and user turns stay closed in every mode. Training then closes the known answer, generation stops at an assistant-start cue, and prefill leaves `{"rotation_window_days":` open. You can't combine the last two endings.

Model families don't share one universal chat serialization:

Checkpoint family exampleShape of its turn markersEngineering consequence
ChatML-style<|im_start|>user ... <|im_end|>An assistant-start marker can indicate a fresh generation turn.
Llama 3 Instruct<|begin_of_text|>, then <|start_header_id|>role<|end_header_id|> and <|eot_id|>Use the tokenizer's shipped headers. add_generation_prompt=True appends an assistant header, not a ChatML-style start token.[4]Reference 4The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783[5]Reference 5Transformers Documentation: Writing a chat template.https://huggingface.co/docs/transformers/main/en/chat_templating_writing
Mistral-7B-Instruct-v0.1<s> [INST] ... [/INST] ... </s>Tokenizer V1 spacing around [INST] is part of the contract. A Hugging Face dump of the same checkpoint can omit some of those spaces, which is why you render with the shipped template instead of rebuilding the string.[3]Reference 3Transformers Documentation: Chat templates.https://huggingface.co/docs/transformers/main/en/chat_templating[6]Reference 6Tokenizationhttps://docs.mistral.ai/resources/cookbooks/concept-deep-dive-tokenization-templates

Those strings are examples of checkpoint-specific formats, not a menu of interchangeable wrappers. Feeding [INST] formatting to a checkpoint trained with another role-token layout may still produce text, but you've changed its input distribution. That is the serving failure from the opening: readable text doesn't mean the model saw the context it was trained to continue.

This lesson focuses on system, user, and assistant turns. Production templates often also serialize tool messages, tool_calls, and multimodal parts; those schemas are out of scope here and are covered when you wire tool-using agents. Don't treat three-role ChatML as the full production contract.

Render training, fresh generation, and prefill separately

The smallest useful template exercise is to render the same support task in three modes. Before running it, predict which ending belongs to each mode:

  1. A completed training transcript contains the known assistant answer.
  2. A new inference request ends at an assistant-start cue.
  3. A prefill already contains the beginning of an assistant answer and asks the model to continue it.

The last two modes aren't the same. Starting a new assistant turn and continuing an existing assistant message should never happen at once. One adds an assistant boundary; the other leaves the existing assistant text open.

render-chat-contract.py
1START = "<|im_start|>" 2END = "<|im_end|>" 3 4def render(messages, *, add_generation_prompt=False, continue_final_message=False): 5 if add_generation_prompt and continue_final_message: 6 raise ValueError("choose a new assistant turn or a prefill, not both") 7 8 pieces = [] 9 for index, message in enumerate(messages): 10 role = message["role"] 11 content = message["content"] 12 is_prefill = ( 13 continue_final_message 14 and index == len(messages) - 1 15 and role == "assistant" 16 ) 17 pieces.append(f"{START}{role}\n{content}") 18 if not is_prefill: 19 pieces.append(f"{END}\n") 20 21 if add_generation_prompt: 22 pieces.append(f"{START}assistant\n") 23 return "".join(pieces) 24 25context = [ 26 {"role": "system", "content": "Use only the supplied policy evidence."}, 27 {"role": "user", "content": "My service-account key is 18 days old. Can I keep using it?"}, 28] 29answer = { 30 "role": "assistant", 31 "content": "Open a rotation ticket because keys older than 14 days require review.", 32} 33 34training_text = render(context + [answer]) 35generation_text = render(context, add_generation_prompt=True) 36prefill_text = render( 37 context + [{"role": "assistant", "content": '{"rotation_window_days": '}], 38 continue_final_message=True, 39) 40 41print("training_has_answer=", "14 days" in training_text) 42print("generation_ends_at_assistant=", generation_text.endswith(f"{START}assistant\n")) 43print("prefill_is_open=", prefill_text.endswith('{"rotation_window_days": ')) 44 45try: 46 render(context, add_generation_prompt=True, continue_final_message=True) 47except ValueError as error: 48 print("invalid_mode_caught=", str(error))
Output
1training_has_answer= True 2generation_ends_at_assistant= True 3prefill_is_open= True 4invalid_mode_caught= choose a new assistant turn or a prefill, not both

Hugging Face tokenizers implement this idea with apply_chat_template. For generation, add_generation_prompt=True appends a start-of-assistant sequence only when that particular template defines one. For a response prefill, continue_final_message=True keeps the final assistant content open, and the documentation treats combining both flags as an error.[3]Reference 3Transformers Documentation: Chat templates.https://huggingface.co/docs/transformers/main/en/chat_templating

The flag names describe intent, not a universal string. A model can need a visible assistant header, or it can begin the response immediately after a user delimiter. Let the checkpoint's tokenizer decide which tokens belong at the boundary.

use-the-checkpoint-tokenizer.py
1from transformers import AutoTokenizer 2 3tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1") 4messages = [ 5 {"role": "user", "content": "My service-account key is 18 days old. Can I keep using it?"}, 6] 7 8input_ids = tokenizer.apply_chat_template( 9 messages, 10 tokenize=True, 11 add_generation_prompt=True, 12 return_tensors="pt", 13)

This snippet intentionally uses the tokenizer attached to the checkpoint rather than recreating the format with string concatenation. For Mistral-7B-Instruct-v0.1, the shipped template already places generation after [/INST]; add_generation_prompt=True doesn't append a separate assistant-start token. Llama 3 Instruct does append <code>&lt;&#124;start_header_id&#124;&gt;assistant&lt;&#124;end_header_id&#124;&gt;</code>.[3]Reference 3Transformers Documentation: Chat templates.https://huggingface.co/docs/transformers/main/en/chat_templating

If you render text with tokenize=False and tokenize it in a later step, pass add_special_tokens=False; otherwise BOS or EOS tokens may be inserted twice. The safer path is to let apply_chat_template tokenize the rendered sequence in one call.[3]Reference 3Transformers Documentation: Chat templates.https://huggingface.co/docs/transformers/main/en/chat_templating

⚠️ Common mistake: Hugging Face's text-generation pipeline defaults to add_generation_prompt=True. If the last message is already an assistant turn, the pipeline switches to continue_final_message=True and treats that text as a prefill. Set the flags yourself when you want a new assistant turn, or when you want a completed assistant message to stay closed.[3]Reference 3Transformers Documentation: Chat templates.https://huggingface.co/docs/transformers/main/en/chat_templating

Whitespace changes the token contract

Jinja renders indentation, line breaks, and trailing spaces unless its whitespace-control markers remove them. Hugging Face warns that extra whitespace absent from training can harm performance and recommends - markers around template blocks and expressions.[5]Reference 5Transformers Documentation: Writing a chat template.https://huggingface.co/docs/transformers/main/en/chat_templating_writing A particularly quiet failure is spaces immediately before a special token. The transcript still looks readable, but the tokenizer receives a different byte sequence than the training run.

Compare a loose template with an exact one. Before running the check, predict whether .strip() can repair the loose result. It can't: the three spaces sit inside the sequence, before <|eot|>.

detect-jinja-whitespace-drift.py
1from hashlib import sha256 2 3from jinja2 import Environment 4 5BAD_TEMPLATE = """{% for message in messages %} 6{{ message['content'] }} {{ '<|eot|>' }} 7{% endfor %}""" 8 9EXACT_TEMPLATE = """{%- for message in messages -%} 10{{- message['content'] -}}{{- '<|eot|>' -}} 11{%- endfor -%}""" 12 13messages = [{"content": "Open a rotation ticket."}] 14environment = Environment() 15 16loose = environment.from_string(BAD_TEMPLATE).render(messages=messages) 17exact = environment.from_string(EXACT_TEMPLATE).render(messages=messages) 18 19print("loose=", repr(loose)) 20print("exact=", repr(exact)) 21print("spaces_before_eot=", " <|eot|>" in loose) 22print("same_bytes=", loose.encode() == exact.encode()) 23print("loose_sha=", sha256(loose.encode()).hexdigest()[:12]) 24print("exact_sha=", sha256(exact.encode()).hexdigest()[:12]) 25 26assert " <|eot|>" in loose 27assert loose.encode() != exact.encode()
Output
1loose= '\nOpen a rotation ticket. <|eot|>\n' 2exact= 'Open a rotation ticket.<|eot|>' 3spaces_before_eot= True 4same_bytes= False 5loose_sha= 4e9ea802389c 6exact_sha= 271e73be7a6b

Store a template revision or digest in the training manifest, but also keep golden rendered strings and token IDs. A matching filename doesn't prove that a serving engine uses the same Jinja whitespace semantics. Render the same messages through training and serving paths, compare bytes and token IDs, and block deployment on any unexplained difference.

Before rendering thousands of rows, reject malformed dialogue structure. Predict which of the three role lists below should fail: a missing assistant answer, two user turns in a row, or a valid system-user-assistant exchange. An assistant reply without a preceding user request is a bad supervised example even if its text is fluent.

validate-conversation-roles.py
1rows = { 2 "valid": ["system", "user", "assistant"], 3 "missing_answer": ["system", "user"], 4 "double_user": ["system", "user", "user", "assistant"], 5} 6 7def role_errors(roles): 8 turns = roles[1:] if roles and roles[0] == "system" else roles 9 expected = "user" 10 for role in turns: 11 if role != expected: 12 return [f"expected {expected}, got {role}"] 13 expected = "assistant" if expected == "user" else "user" 14 if expected == "assistant": 15 return ["conversation ends before assistant answer"] 16 return [] 17 18for row_id, roles in rows.items(): 19 errors = role_errors(roles) 20 print(f"{row_id}: {'accept' if not errors else 'reject'} {errors}") 21 22assert role_errors(rows["valid"]) == [] 23assert role_errors(rows["missing_answer"]) == ["conversation ends before assistant answer"] 24assert role_errors(rows["double_user"]) == ["expected assistant, got user"]
Output
1valid: accept [] 2missing_answer: reject ['conversation ends before assistant answer'] 3double_user: reject ['expected assistant, got user']

Fine-tuning data must teach supported answers

Good formatting can't rescue bad supervision. If a training row claims that stale keys can wait 30 days when the policy says 14 days with a rotation ticket, SFT reinforces an unsupported answer.

Decide what behavior each accepted row is allowed to teach before you worry about how many rows you can generate.

Data sourceUseful forRisk to test before training
Reviewed policy examplesPolicy-critical responses and refusal behaviorSparse coverage of unusual tickets
Synthetic variations from approved seedsParaphrases, edge cases, tone variationUnsupported policy claims or near-duplicates
Multi-turn transcriptsFollow-ups such as "Which rotation ticket should I use?"Old context, personal data, or unhelpful agent habits

Self-Instruct demonstrated a repeatable way to expand instruction data: start from human-written tasks, generate new instructions and responses, then filter invalid or similar rows before fine-tuning.[7]Reference 7Self-Instruct: Aligning Language Models with Self-Generated Instructions.https://aclanthology.org/2023.acl-long.754/ A product team can use the same pattern with policy questions, but it must validate answers against source clauses rather than trusting a generator's confidence.

Four candidate SFT rows go through a grounding gate: row-001 and row-004 pass stale-key facts, row-003 passes admin-access facts, and row-002 is rejected for a 30-day claim. A coverage gate then shows two stale-key rows and one admin-access row, with source citation still at zero, so the dataset is not ready.
Row-001, row-003, and paraphrase row-004 pass grounding; row-002 is rejected for a 30-day claim. Coverage still fails: two stale-key rows and one admin-access row leave source citation without a demonstration.

The next lab treats each assistant answer as a candidate SFT row. A row is accepted only if it contains required policy facts and omits a known unsupported claim.

filter-grounded-sft-rows.py
1policies = { 2 "stale_key": { 3 "required": ("14 days", "rotation ticket"), 4 "forbidden": ("30 days",), 5 }, 6 "admin_access": { 7 "required": ("reviewer approval", "policy p-7"), 8 "forbidden": ("auto-approve",), 9 }, 10} 11 12candidate_rows = [ 13 { 14 "id": "row-001", 15 "policy": "stale_key", 16 "answer": "Open a rotation ticket because keys older than 14 days require review.", 17 }, 18 { 19 "id": "row-002", 20 "policy": "stale_key", 21 "answer": "Keep using the key and rotate it within 30 days.", 22 }, 23 { 24 "id": "row-003", 25 "policy": "admin_access", 26 "answer": "Escalate temporary admin access for reviewer approval under policy P-7.", 27 }, 28 { 29 "id": "row-004", 30 "policy": "stale_key", 31 "answer": "Keys older than 14 days need a rotation ticket before further use.", 32 }, 33] 34 35def evaluate(row): 36 text = row["answer"].lower() 37 rule = policies[row["policy"]] 38 has_required_facts = all(fact in text for fact in rule["required"]) 39 contains_forbidden_claim = any(claim in text for claim in rule["forbidden"]) 40 return has_required_facts and not contains_forbidden_claim 41 42accepted = [] 43for row in candidate_rows: 44 decision = "accept" if evaluate(row) else "reject" 45 print(f"{row['id']}: {decision}") 46 if decision == "accept": 47 accepted.append(row["id"]) 48 49print("accepted_rows=", accepted) 50assert accepted == ["row-001", "row-003", "row-004"]
Output
1row-001: accept 2row-002: reject 3row-003: accept 4row-004: accept 5accepted_rows= ['row-001', 'row-003', 'row-004']

The filter is deliberately small. A real data pipeline also checks duplicated instructions, personal information, unsafe replies, role ordering, length limits, and human-review requirements for high-risk cases. Version the evidence snapshot, generator prompt, filters, and accepted dataset together. Otherwise you won't know which change caused a behavior regression.

The output gives us a useful failure: two accepted stale-key paraphrases and one admin-access row still don't demonstrate how the assistant should answer a source-citation question. More accepted rows aren't the fix; missing intent coverage is.

report-sft-coverage-gaps.py
1required_intents = { 2 "stale_key", 3 "admin_access", 4 "source_citation", 5} 6accepted_rows = [ 7 {"id": "row-001", "intent": "stale_key"}, 8 {"id": "row-003", "intent": "admin_access"}, 9 {"id": "row-004", "intent": "stale_key"}, 10] 11 12covered = {row["intent"] for row in accepted_rows} 13missing = sorted(required_intents - covered) 14counts = { 15 intent: sum(row["intent"] == intent for row in accepted_rows) 16 for intent in sorted(required_intents) 17} 18 19print("accepted_counts=", counts) 20print("missing_critical_intents=", missing) 21print("ready_for_training=", not missing) 22 23assert missing == ["source_citation"]
Output
1accepted_counts= {'admin_access': 1, 'source_citation': 0, 'stale_key': 2} 2missing_critical_intents= ['source_citation'] 3ready_for_training= False

Which tokens should receive the loss?

Every completed chat transcript contains tokens from the system prompt, user question, and assistant answer. Before choosing an objective, predict which tokens should move the weights when the policy answer is wrong: the prompt, the answer, or both. That choice determines what the gradient budget emphasizes.

Two choices matter:

Objective choiceTarget tokensWhen it can be reasonable
Full-sequence causal lossAll non-padding transcript tokensYou intentionally train the model on the whole conversation distribution.
Assistant-only lossAssistant response spans, usually including their end-of-turn markersYou want the gradient budget focused on response behavior rather than reproducing prompt text.

Assistant-only loss is common, but it isn't the definition of SFT. TRL exposes it as assistant_only_loss=True for conversational data only when the chat template can identify assistant spans through {% generation %} and {% endgeneration %} markers. For known families such as Qwen3, current TRL releases patch the template when that flag is on. Inspect the resolved template for the checkpoint you train.[8]Reference 8TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer If the span mask is wrong, you may silently train on user text or mask out the answer you meant to learn.

The prompt still stays visible to the model. Setting a label to -100 removes that position from cross-entropy; it doesn't erase the context that lets the assistant answer.

In a causal language model, each target is the token predicted from the tokens before it. Trainers usually shift labels internally so labels[i] is what the model should emit after position i-1. The lab below shows those supervised targets after the shift: assistant words and the assistant turn terminator receive loss, while role markers, system text, and user text get the ignore label -100.

build-assistant-only-labels.py
1conversation = [ 2 ("system", "Use supplied policy evidence"), 3 ("user", "Stale key needs access"), 4 ("assistant", "Open rotation ticket"), 5] 6 7tokens = [] 8labels = [] 9 10for role, text in conversation: 11 tokens.append(f"<{role}>") 12 labels.append("-100") 13 for word in text.split(): 14 tokens.append(word) 15 labels.append(word if role == "assistant" else "-100") 16 tokens.append("<eot>") 17 labels.append("<eot>" if role == "assistant" else "-100") 18 19trained_targets = [label for label in labels if label != "-100"] 20masked_tokens = sum(label == "-100" for label in labels) 21 22print("tokens=", tokens) 23print("trained_targets=", trained_targets) 24print("masked_tokens=", masked_tokens) 25 26assert trained_targets == ["Open", "rotation", "ticket", "<eot>"] 27assert labels[tokens.index("<user>")] == "-100"
Output
1tokens= ['<system>', 'Use', 'supplied', 'policy', 'evidence', '<eot>', '<user>', 'Stale', 'key', 'needs', 'access', '<eot>', '<assistant>', 'Open', 'rotation', 'ticket', '<eot>'] 2trained_targets= ['Open', 'rotation', 'ticket', '<eot>'] 3masked_tokens= 13

Use this kind of inspected tiny batch before launching training. A training-loss curve can't reveal that your boundary finder shifted one token too far and trained the wrong span.

This calculation makes the objective choice visible. Full-sequence loss scores nearly the whole serialized row; assistant-only loss scores only the desired answer span and its terminator. If the assistant target count unexpectedly drops to zero, stop before tuning the learning rate: the mask, not optimization, is broken.

compare-supervised-target-budgets.py
1stream = [ 2 ("system", "<system>"), 3 ("system", "Use"), 4 ("system", "policy"), 5 ("system", "<eot>"), 6 ("user", "<user>"), 7 ("user", "Stale"), 8 ("user", "key"), 9 ("user", "<eot>"), 10 ("assistant-marker", "<assistant>"), 11 ("assistant", "Open"), 12 ("assistant", "ticket"), 13 ("assistant", "<eot>"), 14] 15 16full_sequence_targets = [token for _, token in stream[1:]] 17assistant_only_targets = [token for role, token in stream if role == "assistant"] 18 19print("full_sequence_target_count=", len(full_sequence_targets)) 20print("assistant_only_target_count=", len(assistant_only_targets)) 21print("assistant_only_targets=", assistant_only_targets) 22 23assert assistant_only_targets == ["Open", "ticket", "<eot>"] 24assert len(assistant_only_targets) < len(full_sequence_targets)
Output
1full_sequence_target_count= 11 2assistant_only_target_count= 3 3assistant_only_targets= ['Open', 'ticket', '<eot>']

Packing saves padding, but isolation is a separate setting

Once you know which tokens are targets, the next waste is padding around short chats. If every short SFT row is padded to a long context window, the accelerator spends much of its work processing padding. Packing fills a window with several short sequences instead. TRL supports packing as a training configuration for this reason.[8]Reference 8TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer

Before turning it on, predict the safety question: may a token in conversation B attend to earlier tokens in conversation A? Packing answers how rows share capacity; isolation answers whether their histories can mix.

Current TRL makes that distinction in the packing strategy. packing=True defaults to packing_strategy="bfd" (best-fit decreasing), and that path enables padding-free batching. With FlashAttention 2 or 3, the trainer passes position_ids that restart at each packed sample, so attention stays inside the sample.[8]Reference 8TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer

The wrapped strategy concatenates then splits at max_length; ordinary causal attention can leak across that cut. Without a FlashAttention kernel, padding-free packing can contaminate even when you meant to isolate.[8]Reference 8TRL Documentation: SFT Trainer.https://huggingface.co/docs/trl/sft_trainer

Don't treat packing=True as a synonym for block-diagonal attention. If each policy conversation must be an independent supervised example, read the trainer docs for the version you run and verify the boundary with a tiny packed batch.

First measure why packing is tempting. Four short training rows padded separately to a 16-token window waste most of their capacity; filling shared windows reduces that waste.

measure-packing-utilization.py
1window = 16 2row_lengths = [9, 6, 7, 5] 3 4separate_capacity = window * len(row_lengths) 5separate_utilization = sum(row_lengths) / separate_capacity 6 7packed_windows = [] 8for length in row_lengths: 9 for index, used in enumerate(packed_windows): 10 if used + length <= window: 11 packed_windows[index] += length 12 break 13 else: 14 packed_windows.append(length) 15 16packed_capacity = window * len(packed_windows) 17packed_utilization = sum(row_lengths) / packed_capacity 18 19print("separate_utilization=", f"{separate_utilization:.1%}") 20print("packed_windows=", packed_windows) 21print("packed_utilization=", f"{packed_utilization:.1%}") 22 23assert packed_windows == [15, 12] 24assert packed_utilization > separate_utilization
Output
1separate_utilization= 42.2% 2packed_windows= [15, 12] 3packed_utilization= 84.4%

The utilization check makes the tradeoff tangible: packing raises this example from 42.2% to 84.4% of each allocated window. That gain says nothing about cross-row visibility, so now inspect the boundary mask separately.

Suppose two three-token rows are packed into one sequence:

Packed position012345
Conversation IDAAABBB
Allowed history for strict isolationA onlyA onlyA onlyB onlyB onlyB only

For strict isolation, the causal self-attention matrix contains two blocks:

M=[100000110000111000000100000110000111]M = \begin{bmatrix} 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 0 & 0 & 0 & 0 \\ 1 & 1 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 1 & 1 & 1 \end{bmatrix}M=​111000​011000​001000​000111​000011​000001​​

Each row is a query position; each column is a possible earlier key position. The zeros at query position 4 under key positions 0 through 2 stop the second conversation from reading the first one. The lab uses 0-based indexes, so position 4 is the middle token of conversation B.

verify-packed-isolation.py
1conversation_ids = ["A", "A", "A", "B", "B", "B"] 2 3mask = [ 4 [ 5 int(key_position <= query_position and key_id == query_id) 6 for key_position, key_id in enumerate(conversation_ids) 7 ] 8 for query_position, query_id in enumerate(conversation_ids) 9] 10 11for row in mask: 12 print(" ".join(map(str, row))) 13 14cross_conversation_edges = [ 15 (query, key) 16 for query, row in enumerate(mask) 17 for key, allowed in enumerate(row) 18 if allowed and conversation_ids[query] != conversation_ids[key] 19] 20 21print("cross_conversation_edges=", cross_conversation_edges) 22assert cross_conversation_edges == [] 23assert mask[4][1] == 0
Output
11 0 0 0 0 0 21 1 0 0 0 0 31 1 1 0 0 0 40 0 0 1 0 0 50 0 0 1 1 0 60 0 0 1 1 1 7cross_conversation_edges= []

Don't assume an option named packing=True constructs this matrix in every engine. TRL's default BFD path isolates sequences when FlashAttention is in use; wrapped packing doesn't. Inspect the implementation you'll train and run a small boundary test.

Production failures are usually contract failures

Once a checkpoint has been fine-tuned, evaluate it with the private cases you built in the previous chapter. A drifted endpoint might still return fluent text while forgetting the policy evidence or continuing as another role. Log the rendered prompt, tokenizer revision, template revision, truncation policy, and generation settings alongside every result. Otherwise an answer regression could be a template deployment bug rather than a model change.

Before reading the table, predict the first diagnostic for that endpoint. Check the final rendered tokens and special-token count before blaming weights or generation temperature.

SymptomLikely contract failureFirst diagnostic check
Model generates another user question instead of answeringMissing assistant-start cue for a template that needs oneInspect final rendered tokens before .generate().
Model emits unfamiliar role markers or ramblesCheckpoint served with another template familyCompare tokenizer/template revision with training manifest.
Responses changed after refactoring preprocessingBOS, EOS, or delimiters were inserted twiceCount special-token IDs after rendering and tokenization.
Good short answers, incorrect long threadsTruncation removed the policy evidence or system instructionLog retained messages and token count.
Low loss, poor response qualityWrong target-span mask or noisy accepted rowsRender one batch with visible labels and inspect rejected data.

This minimal deployment manifest check can't measure model quality, but it prevents shipping an endpoint whose serialization contract is known to differ from the fine-tuning run. Predict which candidate should be blocked before running it: the one with a different template revision and duplicate-special-token setting.

audit-serving-contract.py
1training_manifest = { 2 "checkpoint": "access-policy-sft-v3", 3 "tokenizer_revision": "tokens-7", 4 "template_revision": "chatml-policy-v2", 5 "add_special_tokens_after_template": False, 6} 7 8deployments = [ 9 { 10 "name": "candidate-safe", 11 "checkpoint": "access-policy-sft-v3", 12 "tokenizer_revision": "tokens-7", 13 "template_revision": "chatml-policy-v2", 14 "add_special_tokens_after_template": False, 15 }, 16 { 17 "name": "candidate-drifted", 18 "checkpoint": "access-policy-sft-v3", 19 "tokenizer_revision": "tokens-7", 20 "template_revision": "mistral-wrapper-v1", 21 "add_special_tokens_after_template": True, 22 }, 23] 24 25def mismatches(candidate): 26 return [ 27 field 28 for field, expected in training_manifest.items() 29 if candidate[field] != expected 30 ] 31 32for deployment in deployments: 33 drift = mismatches(deployment) 34 status = "block" if drift else "evaluate" 35 print(f"{deployment['name']}: {status} drift={drift}") 36 37assert mismatches(deployments[0]) == [] 38assert mismatches(deployments[1]) == [ 39 "template_revision", 40 "add_special_tokens_after_template", 41]
Output
1candidate-safe: evaluate drift=[] 2candidate-drifted: block drift=['template_revision', 'add_special_tokens_after_template']

A matching manifest earns the right to run quality evaluations; it doesn't prove the model is ready. Re-run grounded policy cases, critical failure slices, p95 latency checks, and human review checks after every fine-tune or serving change.

The final executable check joins template work back to evaluation. Before running it, predict whether a faster candidate can release with one critical policy error. Template parity is mandatory, but a correctly formatted candidate still ships only if grounded cases and operational limits pass.

gate-finetuned-candidates.py
1candidates = [ 2 { 3 "name": "sft-v3", 4 "template_matches": True, 5 "grounded_rate": 0.99, 6 "critical_errors": 0, 7 "p95_latency_ms": 620, 8 }, 9 { 10 "name": "sft-v4-fast", 11 "template_matches": True, 12 "grounded_rate": 0.94, 13 "critical_errors": 1, 14 "p95_latency_ms": 410, 15 }, 16] 17 18def blockers(candidate): 19 failed = [] 20 if not candidate["template_matches"]: 21 failed.append("template_drift") 22 if candidate["grounded_rate"] < 0.98: 23 failed.append("grounded_quality") 24 if candidate["critical_errors"] > 0: 25 failed.append("critical_policy_error") 26 if candidate["p95_latency_ms"] > 700: 27 failed.append("latency") 28 return failed 29 30for candidate in candidates: 31 failed = blockers(candidate) 32 decision = "release" if not failed else "block" 33 print(f"{candidate['name']}: {decision} blockers={failed}") 34 35assert blockers(candidates[0]) == [] 36assert blockers(candidates[1]) == ["grounded_quality", "critical_policy_error"]
Output
1sft-v3: release blockers=[] 2sft-v4-fast: block blockers=['grounded_quality', 'critical_policy_error']

At release time, the checks form a dependency chain: validate rows, render exact bytes, supervise intended spans, isolate packed histories, then run grounded and operational evaluations. When a chat checkpoint changes, that order tells you where to look before retraining.

Mastery check

Key concepts

  • Base-model continuation versus instruction-tuned response behavior
  • SFT examples grounded in approved evidence
  • Chat-template serialization and checkpoint-specific markers
  • Fresh generation versus assistant prefill
  • Full-sequence versus assistant-only loss
  • Synthetic-data acceptance checks
  • Packed-sequence isolation as a verified design choice
  • Training and serving template parity

Evaluation rubric

  • Foundational: Explains why a base model can complete dialogue text without reliably acting as a support assistant.
  • Intermediate: Renders completed, generation, and prefill versions of one conversation without mixing their boundary rules.
  • Intermediate: Builds an assistant-only target mask and explains why that's a choice rather than a universal SFT requirement.
  • Advanced: Designs a data and serving audit that rejects unsupported policy rows and template drift before evaluation.

Common pitfalls

  • Training on plausible synthetic answers without checking them against policy evidence.
  • Hand-formatting messages with delimiters that don't match the checkpoint's tokenizer template.
  • Treating assistant_only_loss or packed isolation as automatic behavior without inspecting the trainer.
  • Using a new-assistant generation cue when the request already contains an assistant prefill.
  • Declaring a fine-tuned checkpoint better before rerunning grounded private evaluations.

Follow-up questions

Why can a base model produce conversation-looking text without being a reliable assistant?

Answer

It predicts likely continuations from its token context. Dialogue appeared in pretraining data, so it may continue a conversation, but no dependable product contract says it must answer the user's request, follow policy evidence, or stop at an assistant boundary. SFT supplies examples that reward those continuations.

When do you use add_generation_prompt=True, and when do you use continue_final_message=True?

Answer

Use a generation prompt when the final input turn is complete and you need to start a fresh assistant answer, if the checkpoint's template uses such a cue. Use final-message continuation when an assistant response has already begun, such as a JSON prefill. They represent different contexts and shouldn't be enabled together.

Why isn't assistant-only loss the definition of supervised fine-tuning?

Answer

SFT means optimizing a model on supervised example sequences. A trainer can compute loss across the full transcript or only selected assistant spans. Assistant-only loss is useful when you want gradient focused on responses, but it requires correct template-aware span masks and remains an objective choice.

What must you verify before using packed support conversations as independent examples?

Answer

Verify whether tokens in one packed conversation can attend to tokens from an earlier conversation. Isolation comes from the packing strategy and attention kernel, not from packing itself. Current TRL BFD packing plus FlashAttention isolates packed rows; wrapped packing can leak. Test a tiny packed batch instead of assuming the flag name implies a block-diagonal mask.

Practice extension

Add a third policy row for a source-citation requirement and a second rejected answer that invents automatic approval. Extend filter-grounded-sft-rows.py to print which required fact is missing or which forbidden claim was found. Then add its accepted answer to the rendering and label-building labs. Show exactly which assistant tokens become supervised targets and exactly which evidence allowed the row into training.

Complete the lesson

Mastery Check

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

1.Why does a base language model need SFT for the stale-key support behavior described in the article?

Correct answer: SFT supplies grounded prompt-response demonstrations that teach the assistant to answer and stop under the product contract.

A base model predicts plausible continuations, while SFT raises the probability of desired assistant responses conditioned on serialized roles and evidence.

2.A request contains a complete user turn, while another already begins an assistant JSON value. Which rendering flags fit those two cases?

Correct answer: Use add_generation_prompt for the complete user turn and continue_final_message for the assistant JSON prefill.

A fresh generation prompt starts a new assistant turn. A final-message continuation keeps an existing assistant prefill open, and the flags are mutually exclusive.

3.A pipeline renders a chat string first and tokenizes it later. Which tokenizer setting avoids accidental duplicate special tokens?

Correct answer: Use add_special_tokens=False when tokenizing a string that was already rendered with the checkpoint template.

The article warns that adding special tokens after text rendering can duplicate BOS or EOS markers. The checkpoint tokenizer and its shipped template define the correct serialization.

4.Two templates produce the same readable policy answer, but one inserts spaces before the end marker. What is the right regression test?

Correct answer: Compare golden rendered bytes and token IDs, and record a template digest so unexplained drift blocks release.

Jinja whitespace changes the serialized byte stream and token IDs. The article recommends exact rendered fixtures and a versioned digest rather than visual inspection.

5.Which role sequence should a validator accept for a three-turn training row?

Correct answer: Optional system, then alternating user and assistant roles, ending with an assistant answer.

The article's role validator allows an optional system message, then user and assistant alternation. Missing answers and double-user turns are rejected.

6.Why can response-only loss coexist with prompt tokens remaining visible to the model?

Correct answer: The label mask excludes prompt positions from loss, while the attention mask still lets the answer condition on them.

Labels set to -100 do not contribute to cross-entropy, but the prompt remains context. Attention reachability and loss supervision are separate contracts.

7.Two conversations are packed into one block with reset position IDs. What additional property is required for independent SFT examples?

Correct answer: The packing path must enforce segment-aware attention, such as supported BFD plus FlashAttention boundaries.

Reset position IDs do not block cross-example attention by themselves. The article separates packing efficiency from isolation and requires a tested attention implementation.

8.A serving candidate uses a different template revision and adds special tokens after an already rendered transcript. What should the deployment gate do?

Correct answer: Block promotion until template drift and duplicate-token risk are resolved, then compare quality metrics.

The article's serving audit blocks a candidate whose template revision or special-token policy differs from the training manifest. Quality evaluation cannot explain an untracked input contract.

9.A synthetic row says keys older than 14 days can remain active for 30 days, while the approved policy requires a rotation ticket. What is the correct data decision?

Correct answer: Reject the row because fluent wording does not override the required policy facts and forbidden claim.

Grounded data checks reject unsupported claims before SFT. A bad answer can be useful as a tagged preference negative, but not as an accepted supervised target.

9 questions remaining.

Next Step
Continue to Dimensionality Reduction for Embeddings

You can now teach an assistant from grounded SFT rows and keep the same chat contract at serving time. Those answers still depend on retrieved evidence, and that evidence lives in high-dimensional <span data-glossary="embedding">embeddings</span>. Next you'll shrink and inspect those <span data-glossary="vector">vectors</span> so retrieval quality is measured, not guessed.

PreviousLLM Benchmarks & Limitations
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

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

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

https://arxiv.org/abs/2203.02155

LIMA: Less Is More for Alignment.

Zhou, C., et al. · 2023 · NeurIPS 2023

https://arxiv.org/abs/2305.11206

Transformers Documentation: Chat templates.

Hugging Face · 2026

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

The Llama 3 Herd of Models.

Dubey, A., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2407.21783

Transformers Documentation: Writing a chat template.

Hugging Face · 2026

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

Tokenization

Mistral AI · 2026

https://docs.mistral.ai/resources/cookbooks/concept-deep-dive-tokenization-templates

Self-Instruct: Aligning Language Models with Self-Generated Instructions.

Wang, Y., et al. · 2023 · ACL 2023

https://aclanthology.org/2023.acl-long.754/

TRL Documentation: SFT Trainer.

Hugging Face · 2026

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

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.