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 的步骤:
- 分别编码 prompt 和 response。
- 拼接 token ID。
- 把 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 权重序列化。