Fine-tuning jobs also run SFT, with a fixed data format and no training loop to write. For help choosing between the two, see Post-training vs. standard fine-tuning.
How post-training runs SFT
SFT runs on a trainer-only model resource. Nothing samples from the model, so the resource runs withnum_generator_replicas=0. Each step runs two operations:
forward_backwardwithLOSS_TYPE_CROSS_ENTROPY, which computes the weighted next-token cross-entropy over the batch and accumulates gradients.optim_step, which applies the accumulated gradients with the step’s learning rate.
Run an SFT recipe
The cookbook’sSFTConfig and train function handle dataset loading, tokenization, loss masks, the training loop, and checkpoints. This example trains Qwen/Qwen3.8-27B on the No Robots dataset, with every other setting at its default:
train provisions a trainer-only model resource for model, attaches one session to it, and stops both when it returns. To train on a resource that’s already running, set model_resources_id instead of model. See Manage model resources.
The config sets enable_thinking=False because No Robots conversations contain no reasoning, as Data for reasoning models explains.
The config also sets wandb_project, so train logs to Weights & Biases. Export WANDB_API_KEY before you run it, or set WANDB_MODE=offline to log locally. Without either, the run fails after it provisions the model resource, or, in an interactive terminal, waits at a W&B login prompt while the resource bills.
Settings worth adjusting first:
num_epochsandmax_steps:trainmakesnum_epochspasses over the dataset, one by default, and reshuffles the rows before each pass.max_stepscaps the total number of steps, which keeps a test run short. Its default,None, sets no cap, so a config without it trains on every full batch: about 1,187 steps for No Robots at abatch_sizeof 8. The learning rate decays linearly toward zero over the run.learning_rate: The right rate depends on the model and the task. For a low-rank adapter (LoRA) run,1e-4to5e-4is typical. Full-weight training needs a lower rate. The cookbook’s No Robots recipes use2e-5for full-weight training and1e-4for LoRA on the same model.train_on_what:TRAIN_ON_ALLtrains on every assistant turn.TRAIN_ON_LASTtrains on only the final one, which suits multi-turn datasets where only the last response matters.batch_size: Larger batches (16 to 32) produce smoother loss curves.use_lora=False: Trains the full weights instead of an adapter. Not every model supports full-weight training, so check Supported models first. See LoRA vs. full-weight training.
SFTConfig field.
Prepare your data
train accepts any Hugging Face dataset with a messages column. In each row, messages is a list of {"role": ..., "content": ...} dicts:
dataset_name to it and pass a to_messages function to train. It maps one row to its list of messages. Put any instruction you want the model to learn, such as an answer format, in that mapping:
train calls to_messages on the first row, so a schema mismatch, such as a missing messages column, fails before you’re billed.
For data that isn’t on Hugging Face, save the rows as a JSON Lines file named train.jsonl, one row per line, in a folder of its own. Then set dataset_name to the folder’s path, such as "./my_data", and keep dataset_split="train". train loads the folder with the Hugging Face datasets library, which reads train.jsonl into the train split. A relative path resolves from the directory you run the recipe in.
Data for reasoning models
Two chat-template behaviors decide whether your rows train:- Turn thinking off for data without reasoning: Every Qwen model in the catalog turns thinking on by default. For conversations without
<think>blocks, such as No Robots, setenable_thinking=False. With thinking on, the template renders each prompt differently from the full conversation, sotrainskips every row. The run then fails with nothing trained, after it has already provisioned the model resource. GLM-5.3 doesn’t need the setting: its template always thinks and renders these rows as they are, and the cookbook rejectsenable_thinking=Falsefor it. - Avoid consecutive assistant turns on GLM models: The GLM template ends an assistant turn with the role marker of the message after it, so an assistant turn followed by another assistant turn has no stop token.
trainskips those rows with a warning. Merge or split the turns before training.
reasoning_content field next to content. Qwen3.8-27B’s template reads reasoning only from that field, so train skips rows that carry it as <think> tags inside content. preserve_thinking controls whether earlier turns keep their reasoning when the history is rendered. See Thinking settings.
Loss masks
SFT doesn’t train on every token equally. The model’s chat template wraps each message in role headers and special tokens, and a loss mask gives each position a weight that sets how much it counts toward the loss:- Role headers, including the assistant’s (
<|im_start|>assistantin Qwen’s chat template), and user content get weight0. - Assistant content and the assistant’s end-of-turn token (
<|im_end|>in Qwen’s template) get weight1. Training on the end-of-turn token teaches the model when to stop.
render_conversation builds the mask from the chat template and your train_on_what choice. Weights are float32, so you can down-weight some turns with fractional values instead of masking them out. Concepts explains how the mask lines up with the input and target tokens.
Write your own SFT loop
When none of the cookbook recipes fit your use case, you can write your own training loop. This loop trains on an in-memory list of conversations:render_conversation returns a list of sequences, not a single one, because it splits a conversation wherever the chat template strips earlier reasoning.
build_cross_entropy_sample shifts the tokens into input and target positions for you. For cross-entropy, result.loss is the sum over all trained positions, not a mean. Divide it by the batch’s total weight to get the per-token mean the recipes log.
To train with a variant of cross-entropy, such as one with an added regularization term, write a custom loss function. Each step then uses forward and custom_forward_backward instead of forward_backward.
Chain SFT and GRPO
To continue an SFT adapter with GRPO, hand it over with a training checkpoint. Combine methods explains when this helps. The handoff takes three settings:- Set
training_checkpoint_intervalso the SFT run writes a training checkpoint at its last step. The cookbook writes one only when the interval fires, andtrainreturns the latest checkpoint ID (orNone). - Start the GRPO run with
resume_from_checkpoint_idset to that ID andload_optimizer=False. The new session loads the adapter weights with a fresh optimizer and its step counter at zero. - Use the same LoRA rank and alpha in both phases. A checkpoint with a different rank fails to load, and a different alpha silently changes how strongly the adapter applies.
question and answer columns, not messages, so the SFT phase maps each row with to_messages. The mapping poses each question in the GRPO prompt format, so SFT teaches the answer format that GRPO then scores. The cookbook’s recipes/sft_then_grpo.py is the complete worked example.
Unlike SFTConfig, GRPOConfig logs to Weights & Biases by default, so export WANDB_API_KEY or WANDB_MODE=offline, or pass wandb_project=None to GRPOConfig, before you run this example.
Next steps
Direct preference optimization
Train on preference pairs with a custom loss function.
Reinforcement learning with GRPO
Continue the adapter with reinforcement learning.
Cookbook reference
Every
SFTConfig field and its default.Deploy a checkpoint
Deploy the trained model on dedicated model inference.