DPO 概览¶
Direct Preference Optimization(DPO)训练模型,让它在同一个 prompt 下更偏好 chosen response,而不是 rejected response。
在 Mint Recursive 里,这个 recipe 使用低层 TrainingClient.forward_backward_custom() API,没有内置的 loss_fn="dpo"。Bradley-Terry loss 是一个普通 Python 函数,在客户端运行,接收 Mint Recursive 返回的模型 logprobs。
这个页面对应 recipes/dpo_native.py。
数据形状¶
训练数据从 (prompt, chosen, rejected) 三元组开始:
@dataclass(frozen=True)
class PreferencePair:
prompt: str
chosen: str
rejected: str
pairs = [
PreferencePair(
prompt="Explain why regular backups matter.",
chosen="Backups protect data by creating copies that can be restored...",
rejected="Backups are good.",
),
]
recipe 把每个 pair 展平成两个 Datum:
这个顺序不能改。loss 假设偶数下标是 chosen,奇数下标是 rejected。
构造 Datum¶
prompt token 的 loss weight 是 0,completion token 的 weight 是 1.0:
def build_datum(prompt_tokens, completion_text, tokenizer):
completion_tokens = tokenizer.encode(f" {completion_text}", add_special_tokens=False)
completion_tokens.append(tokenizer.eos_token_id)
all_tokens = prompt_tokens + completion_tokens
input_tokens = all_tokens[:-1]
target_tokens = all_tokens[1:]
weights = [0.0] * (len(prompt_tokens) - 1) + [1.0] * len(completion_tokens)
return types.Datum(
model_input=types.ModelInput.from_ints(tokens=input_tokens),
loss_fn_inputs={"target_tokens": target_tokens, "weights": weights},
)
当 tokenizer 支持 chat template 时,prompt 会经过模型的 chat template:
def build_prompt_tokens(prompt, tokenizer):
messages = [{"role": "user", "content": prompt}]
return tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
)
自定义 Bradley-Terry Loss¶
forward_backward_custom() 先让 datums 过一遍模型,再用 (data, logprobs_list) 调用你的 Python loss 函数。
核心 loss:
def sequence_logprob(logprobs, weights):
# 重要:logprobs 要保持 Tensor,这样 gradients 不会断。
logprob_tensor = logprobs.flatten().float()
weight_tensor = _to_float_tensor(weights)
return torch.dot(logprob_tensor, weight_tensor)
def pairwise_preference_loss(data, logprobs_list):
chosen_scores = []
rejected_scores = []
for chosen_datum, rejected_datum, chosen_logprobs, rejected_logprobs in zip(
data[::2], data[1::2], logprobs_list[::2], logprobs_list[1::2]
):
chosen_scores.append(
sequence_logprob(chosen_logprobs, chosen_datum.loss_fn_inputs["weights"])
)
rejected_scores.append(
sequence_logprob(rejected_logprobs, rejected_datum.loss_fn_inputs["weights"])
)
margins = torch.stack(chosen_scores) - torch.stack(rejected_scores)
loss = -F.logsigmoid(margins).mean()
metrics = {
"loss": float(loss.detach().cpu()),
"pair_accuracy": float((margins > 0).float().mean().detach().cpu()),
"mean_margin": float(margins.mean().detach().cpu()),
}
return loss, metrics
Warning
不要在算 loss 之前把 logprobs 转成 Python list。这会让 tensor 脱离 autograd,导致 forward_backward_custom() 无法反传。
Training Loop¶
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()
data = flatten_preference_pairs(PREFERENCE_PAIRS, tokenizer)
for step in range(1, DPO_STEPS + 1):
result = training_client.forward_backward_custom(
data,
pairwise_preference_loss,
).result()
metrics = result.metrics or {}
training_client.optim_step(types.AdamParams(learning_rate=1e-5)).result()
print(
f"Step {step}: loss={metrics['loss']:.6f}, "
f"pair_accuracy={metrics['pair_accuracy']:.2f}"
)
完整源码:https://github.com/MindLab-Research/mint-quickstart/blob/main/recipes/dpo_native.py
Verified Run¶
已在 Mint Recursive 上验证:Qwen/Qwen3-0.6B,4 个 preference pairs,3 个 DPO steps:
Step 1: loss=34.563499, pair_accuracy=0.00, mean_margin=-34.563488
Step 2: loss=34.331955, pair_accuracy=0.00, mean_margin=-34.331944
Step 3: loss=33.277603, pair_accuracy=0.00, mean_margin=-33.277576
最终 checkpoint:
Note
这次运行使用小样本数据,验证 custom loss 为有限值、梯度能反传、optimizer step 完成,并返回 metrics。示例中,base model 初始对 rejected completion 的评分更高,得到 pair_accuracy=0.00。
这个 Recipe 用到的参数¶
| 参数 | 默认值 | 含义 |
|---|---|---|
MINT_BASE_MODEL |
Qwen/Qwen3-0.6B |
要训练的 base model。 |
MINT_LORA_RANK |
16 |
LoRA rank。 |
MINT_DPO_STEPS |
3 |
custom-loss 训练步数。 |
MINT_DPO_LR |
1e-5 |
Adam 学习率。 |