跳转至

SFT 概览

监督微调(SFT,supervised fine-tuning)教语言模型针对给定 prompt 输出目标 response。你提供带标签的 (prompt, response) 对,Mint Recursive 更新模型权重,让 response token 上的预测 loss 最小化;prompt token 的 loss weight 设为 0,不参与计算。

Configuration

SFT 需要 ServiceClient、LoRA 训练客户端和一个优化器。最小配置:

from mint import types

service_client = mint.ServiceClient()
training_client = service_client.create_lora_training_client(
    base_model="Qwen/Qwen3-0.6B",
    rank=16,
    train_mlp=True,
    train_attn=True,
    train_unembed=True,
)

tokenizer = training_client.get_tokenizer()
adam_params = types.AdamParams(learning_rate=5e-5)

环境变量:

  • MINT_API_KEY:Mint Recursive API key(必需)。从 mintcn.macaron.xin 申请。
  • MINT_BASE_URL:Mint Recursive 服务端 endpoint。默认 https://mint.macaron.im/train(中国大陆:mintcn.macaron.xin/train)。
  • MINT_BASE_MODEL:base model 名称。默认 Qwen/Qwen3-0.6B。
  • MINT_LORA_RANK:LoRA rank。默认 16。

训练在远程 Mint Recursive 服务端上运行。你的 Python 脚本调用 forward_backward(...),然后在 .result() 上阻塞,等到这一批计算完成。

Prompting Guide

SFT 只在 response 部分上算 loss。从 (prompt, response) 对构造 Datum 的步骤:

  1. 分别编码 prompt 和 response。
  2. 拼接 token ID。
  3. 把 prompt token 的 loss weight 设为 0,response token 设为 1.0。

quickstart.py 里的标准写法:

def process_sft_example(ex: dict, tokenizer) -> types.Datum:
    # ex = {"question": "What is 3 * 4?"}
    a, b = map(int, re.findall(r"\d+", ex["question"]))
    answer = str(a * b)
    prompt = f"Question: {ex['question']}\nAnswer:"
    completion = f" {answer}"

    prompt_tokens = tokenizer.encode(prompt, add_special_tokens=True)
    completion_tokens = tokenizer.encode(completion, add_special_tokens=False)
    completion_tokens.append(tokenizer.eos_token_id)

    all_tokens = prompt_tokens + completion_tokens
    all_weights = [0] * len(prompt_tokens) + [1] * len(completion_tokens)

    # teacher forcing 移位:input = all_tokens[:-1], target = all_tokens[1:]
    input_tokens = all_tokens[:-1]
    target_tokens = all_tokens[1:]
    weights = all_weights[1:]

    return types.Datum(
        model_input=types.ModelInput.from_ints(tokens=input_tokens),
        loss_fn_inputs={"target_tokens": target_tokens, "weights": weights},
    )

关键点:

  • prompt 永远被遮蔽(loss_weight=0.0),梯度不流过它。
  • response 永远参与训练(loss_weight=1.0),让模型学着预测目标 token。
  • 聊天风格的 instruction tuning 用 tokenizer 的 apply_chat_template(...) 方法(见 Rendering)。

使用 Renderer

你也可以不手动编码 token,用 mint.recipe 的 renderer 把聊天格式的消息直接转成 Datum。Renderer 会替你处理 chat template、loss mask 和 teacher-forcing 移位:

renderer = recipe.renderers.get_renderer(
    recipe.get_recommended_renderer_name("Qwen/Qwen3-0.6B"),
    tokenizer,
)

messages = [
    {"role": "user", "content": "What is 3 * 4?"},
    {"role": "assistant", "content": "12"},
]

model_input, weights = renderer.build_supervised_example(messages)
datum = recipe.datum_from_model_input_weights(model_input, weights, max_length=2048)

build_supervised_example() 返回 (ModelInput, weights),prompt token 的 weight 为 0,response token 为 1。datum_from_model_input_weights() 把它们包装成 Datum,填入正确的 loss_fn_inputs 结构。

多轮对话时,renderer 默认只在最后一条 assistant 消息上算 loss。传入 train_on_what=TrainOnWhat.ALL_ASSISTANT_MESSAGES,可以在每一条 assistant 回合上训练。

Output Format

SFT 不直接生成文本,它更新模型权重。每次 forward_backward 之后你会拿到一个 ForwardBackwardResult,里面包含:

  • loss_fn_outputs:一个逐 datum 的 dict 列表,存放 logprob 等 loss function 元数据。
  • metrics:聚合的训练指标,可用时返回。

要让训练好的模型生成文本,先保存 LoRA 权重,再创建采样客户端:

# SFT 训练完成后:
checkpoint = training_client.save_weights_and_get_sampling_client(name="my-sft-v1")

prompt_ids = tokenizer.encode("3 * 7 =")
samples = checkpoint.sample(
    prompt=types.ModelInput.from_ints(prompt_ids),
    sampling_params=types.SamplingParams(max_tokens=16, temperature=0.7),
    num_samples=4,
)

for seq in samples.sequences:
    print(tokenizer.decode(seq.tokens))

系统化评估——hold-out 测试集、benchmark 指标、采样日志——见 Concepts → Evaluations。

All Parameters

参数 类型 默认值 含义
base_model str "Qwen/Qwen3-0.6B" base model 的 Hugging Face model ID。
rank int 16 LoRA rank。值越大表达能力越强,显存占用也越高。常见范围 8–64。
train_mlp bool True 训练 MLP(feed-forward)层。
train_attn bool True 训练 attention 层。
train_unembed bool True 训练 unembedding(输出)层。
loss_fn str "cross_entropy" loss function。SFT 永远用 "cross_entropy",没有其他选项。
learning_rate float 5e-5 Adam 学习率。instruction tuning 常见范围 1e-5 到 1e-4。
betas tuple[float, float] (0.9, 0.999) Adam 一阶矩和二阶矩的指数衰减率。
eps float 1e-8 Adam 数值稳定项。
weight_decay float 0.0 L2 正则化系数。LoRA 常见范围 0.0–0.01。

用法:

adam_params = types.AdamParams(
    learning_rate=5e-5,
    betas=(0.9, 0.999),
    eps=1e-8,
    weight_decay=0.0,
)

for step, batch in enumerate(batches):
    result = training_client.forward_backward(batch, loss_fn="cross_entropy").result()
    training_client.optim_step(adam_params).result()
    print(f"Step {step}: metrics={result.metrics}")

Tinker 兼容性提醒:

  • 不要调用 zero_grad_async(),Mint Recursive 服务端会自动清零梯度。
  • loss_fn 参数传给 forward_backward(...),不放在 AdamParams 里。
  • save_weights_for_sampler(...) 和 save_weights_and_get_sampling_client(...) 语义一致:都是把 LoRA 权重序列化。