mirror of
https://github.com/hiyouga/LLaMA-Factory.git
synced 2026-07-07 17:45:26 +08:00
[v1][feature] add dpo trainer (#10544)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from .arg_utils import BatchingStrategy, PluginConfig, get_plugin_config
|
||||
@@ -115,6 +116,30 @@ class TrainingArguments:
|
||||
default=1,
|
||||
metadata={"help": "Log metrics every N optimizer steps."},
|
||||
)
|
||||
pref_loss: Literal["sigmoid", "orpo", "simpo"] = field(
|
||||
default="sigmoid",
|
||||
metadata={"help": "The type of DPO loss to use."},
|
||||
)
|
||||
pref_beta: float = field(
|
||||
default=0.1,
|
||||
metadata={"help": "The beta parameter in the preference loss."},
|
||||
)
|
||||
pref_ftx: float = field(
|
||||
default=0.0,
|
||||
metadata={"help": "The supervised fine-tuning loss coefficient in DPO training."},
|
||||
)
|
||||
simpo_gamma: float = field(
|
||||
default=0.5,
|
||||
metadata={"help": "The target reward margin term in SimPO loss."},
|
||||
)
|
||||
dpo_label_smoothing: float = field(
|
||||
default=0.0,
|
||||
metadata={"help": "The robust DPO label smoothing parameter in cDPO that should be between 0 and 0.5."},
|
||||
)
|
||||
ld_alpha: float | None = field(
|
||||
default=None,
|
||||
metadata={"help": "Alpha parameter from LD-DPO, controls weighting of verbose token log-probabilities."},
|
||||
)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.dist_config = get_plugin_config(self.dist_config)
|
||||
|
||||
@@ -27,6 +27,7 @@ Train Phase:
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
from abc import abstractmethod
|
||||
|
||||
import torch
|
||||
@@ -318,6 +319,10 @@ class BaseTrainer:
|
||||
"grad_norm": grad_norm,
|
||||
"learning_rate": current_lr,
|
||||
}
|
||||
# Merge per-step trainer metrics (e.g. DPO rewards/logps/logits)
|
||||
step_metrics = getattr(self, "_step_metrics", None)
|
||||
if step_metrics:
|
||||
logs.update(step_metrics)
|
||||
self.callback_handler.on_log(self.args, self.state, logs)
|
||||
|
||||
if self.args.save_steps and self.global_step % self.args.save_steps == 0:
|
||||
|
||||
@@ -146,7 +146,9 @@ def launch():
|
||||
|
||||
run_sft()
|
||||
elif command == "dpo":
|
||||
raise NotImplementedError("DPO trainer is not implemented yet.")
|
||||
from llamafactory.v1.trainers.dpo_trainer import run_dpo
|
||||
|
||||
run_dpo()
|
||||
elif command == "rm":
|
||||
from llamafactory.v1.trainers.rm_trainer import run_rm
|
||||
|
||||
@@ -173,9 +175,9 @@ def main():
|
||||
|
||||
run_sft()
|
||||
elif command == "dpo":
|
||||
# from llamafactory.v1.trainers.dpo_trainer import run_dpo
|
||||
# run_dpo()
|
||||
raise NotImplementedError("DPO trainer is not implemented yet.")
|
||||
from llamafactory.v1.trainers.dpo_trainer import run_dpo
|
||||
|
||||
run_dpo()
|
||||
elif command == "rm":
|
||||
from llamafactory.v1.trainers.rm_trainer import run_rm
|
||||
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
# Copyright 2025 the LlamaFactory team.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import copy
|
||||
import os
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from ..accelerator.interface import Dim, DistributedInterface
|
||||
from ..config import InputArgument, TrainingArguments, get_args
|
||||
from ..core.base_trainer import BaseTrainer
|
||||
from ..core.data_engine import DataEngine
|
||||
from ..core.model_engine import ModelEngine
|
||||
from ..utils import logging
|
||||
from ..utils.constants import IGNORE_INDEX
|
||||
from ..utils.types import BatchInput, HFModel, Tensor
|
||||
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
|
||||
def compute_sigmoid_dpo_loss(
|
||||
policy_chosen_logps: Tensor,
|
||||
policy_rejected_logps: Tensor,
|
||||
ref_chosen_logps: Tensor,
|
||||
ref_rejected_logps: Tensor,
|
||||
beta: float = 0.1,
|
||||
label_smoothing: float = 0.0,
|
||||
) -> Tensor:
|
||||
r"""Standalone pure function for sigmoid DPO loss (Rafailov et al. 2023).
|
||||
|
||||
.. math::
|
||||
\text{logits} = (\log\pi_\theta(y_c) - \log\pi_\text{ref}(y_c))
|
||||
- (\log\pi_\theta(y_r) - \log\pi_\text{ref}(y_r))
|
||||
\mathcal{L} = -(1-\varepsilon)\log\sigma(\beta\cdot\text{logits})
|
||||
- \varepsilon\log\sigma(-\beta\cdot\text{logits})
|
||||
|
||||
Args:
|
||||
policy_chosen_logps: Log-probabilities from the policy model for chosen responses.
|
||||
policy_rejected_logps: Log-probabilities from the policy model for rejected responses.
|
||||
ref_chosen_logps: Log-probabilities from the reference model for chosen responses.
|
||||
ref_rejected_logps: Log-probabilities from the reference model for rejected responses.
|
||||
beta: Temperature / scaling factor for the DPO loss.
|
||||
label_smoothing: Label smoothing factor in [0, 1].
|
||||
|
||||
Returns:
|
||||
Per-sample element-wise loss tensor.
|
||||
"""
|
||||
chosen_logratios = policy_chosen_logps - ref_chosen_logps
|
||||
rejected_logratios = policy_rejected_logps - ref_rejected_logps
|
||||
logits = chosen_logratios - rejected_logratios
|
||||
return (
|
||||
-F.logsigmoid(beta * logits) * (1 - label_smoothing)
|
||||
- F.logsigmoid(-beta * logits) * label_smoothing
|
||||
)
|
||||
|
||||
|
||||
def _validate_dpo_dataset_format(train_dataset: DataEngine, dataset_path: str) -> None:
|
||||
if train_dataset.streaming:
|
||||
return
|
||||
|
||||
if len(train_dataset) == 0:
|
||||
raise ValueError(f"DPO training dataset is empty: {dataset_path}")
|
||||
|
||||
sample = train_dataset[0]
|
||||
if "chosen_messages" in sample and "rejected_messages" in sample:
|
||||
return
|
||||
|
||||
dataset_name = sample.get("_dataset_name", "unknown")
|
||||
sample_keys = sorted(sample.keys())
|
||||
raise ValueError(
|
||||
"DPO training requires pair-format samples containing chosen/rejected responses. "
|
||||
f"First sample from dataset '{dataset_name}' has keys: {sample_keys}. "
|
||||
"Please use pair data (e.g. a dataset with chosen_messages/rejected_messages)."
|
||||
)
|
||||
|
||||
|
||||
class DPOTrainer(BaseTrainer):
|
||||
def __init__(
|
||||
self,
|
||||
args: TrainingArguments,
|
||||
model: HFModel,
|
||||
renderer,
|
||||
train_dataset,
|
||||
callbacks=None,
|
||||
) -> None:
|
||||
cp_size = args.dist_config.get("cp_size", 1) if args.dist_config is not None else 1
|
||||
if cp_size > 1:
|
||||
raise NotImplementedError("DPO trainer currently only supports cp_size == 1.")
|
||||
|
||||
self.pref_loss = args.pref_loss
|
||||
self.pref_beta = args.pref_beta
|
||||
self.pref_ftx = args.pref_ftx
|
||||
self.simpo_gamma = args.simpo_gamma
|
||||
self.ld_alpha = args.ld_alpha
|
||||
self.dpo_label_smoothing = args.dpo_label_smoothing
|
||||
|
||||
# ref_model must be created AFTER super().__init__() because FSDP2 with
|
||||
# init_on_meta materialises the model during _shard_model(). We defer
|
||||
# creation to _init_ref_model() below.
|
||||
self.ref_model = None
|
||||
|
||||
super().__init__(args, model, renderer, train_dataset, callbacks)
|
||||
|
||||
if self.pref_loss == "sigmoid":
|
||||
self._init_ref_model()
|
||||
|
||||
def _shard_model(self) -> None:
|
||||
if self.args.dist_config is None:
|
||||
if DistributedInterface().get_world_size(Dim.DP) > 1:
|
||||
from torch.nn.parallel import DistributedDataParallel as DDP
|
||||
|
||||
device_ids = None if self.device.type == "cpu" else [self.device.index]
|
||||
self.model = DDP(self.model, device_ids=device_ids, find_unused_parameters=True)
|
||||
else:
|
||||
super()._shard_model()
|
||||
|
||||
@property
|
||||
def _unwrapped_model(self):
|
||||
model = self.model
|
||||
if hasattr(model, "module"):
|
||||
model = model.module
|
||||
return model
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reference model (frozen snapshot for sigmoid DPO)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def _use_lora_ref(self) -> bool:
|
||||
"""Whether the policy model supports disable_adapter() for ref forward."""
|
||||
unwrapped = self._unwrapped_model
|
||||
return hasattr(unwrapped, "disable_adapter")
|
||||
|
||||
def _init_ref_model(self) -> None:
|
||||
"""Create a frozen copy of the initial model to serve as reference.
|
||||
|
||||
For LoRA / PEFT models the base weights are already frozen, so we
|
||||
reuse the policy model with ``disable_adapter()`` instead of copying.
|
||||
For full fine-tuning a deep copy is required because the policy model's
|
||||
base weights change during training.
|
||||
|
||||
Must be called AFTER super().__init__() so that FSDP2 / DDP sharding
|
||||
has materialised the model onto real devices.
|
||||
"""
|
||||
if self._use_lora_ref:
|
||||
self.ref_model = None
|
||||
logger.info_rank0("LoRA detected — reference log-probs will reuse the base model via disable_adapter().")
|
||||
return
|
||||
|
||||
unwrapped = self._unwrapped_model
|
||||
self.ref_model = copy.deepcopy(unwrapped)
|
||||
self.ref_model.eval()
|
||||
for param in self.ref_model.parameters():
|
||||
param.requires_grad_(False)
|
||||
logger.info_rank0("Full fine-tuning — created independent reference model via deep copy.")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Shared log-probability extraction from logits
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _extract_chosen_rejected_logps(
|
||||
self,
|
||||
logits: Tensor,
|
||||
labels: Tensor,
|
||||
token_type_ids: Tensor,
|
||||
use_ld: bool = True,
|
||||
) -> tuple[Tensor, Tensor, Tensor, Tensor]:
|
||||
"""Extract chosen / rejected log-probabilities (sum and average) from logits.
|
||||
|
||||
Args:
|
||||
logits: (batch_size, seq_len, vocab_size)
|
||||
labels: (batch_size, seq_len)
|
||||
token_type_ids: (batch_size, seq_len) – 1=chosen, 2=rejected
|
||||
use_ld: Whether to apply LD-DPO length-dependent weighting. Should be
|
||||
``False`` for the reference model to match the v0 behaviour where
|
||||
``ld_alpha`` is only applied to the policy log-probs.
|
||||
|
||||
Returns:
|
||||
chosen_logps: (batch_size,) sum of per-token log-probs for chosen
|
||||
rejected_logps: (batch_size,) sum of per-token log-probs for rejected
|
||||
chosen_logps_avg: (batch_size,) length-normalised chosen log-probs
|
||||
rejected_logps_avg: (batch_size,) length-normalised rejected log-probs
|
||||
"""
|
||||
shift_logits = logits[..., :-1, :].contiguous()
|
||||
shift_labels = labels[..., 1:].contiguous()
|
||||
shift_token_type_ids = token_type_ids[..., 1:]
|
||||
|
||||
per_token_logps = -F.cross_entropy(
|
||||
shift_logits.view(-1, shift_logits.size(-1)),
|
||||
shift_labels.view(-1),
|
||||
reduction="none",
|
||||
ignore_index=IGNORE_INDEX,
|
||||
).view(shift_labels.size(0), shift_labels.size(1))
|
||||
|
||||
loss_mask = shift_labels != IGNORE_INDEX
|
||||
chosen_mask = (shift_token_type_ids == 1) & loss_mask
|
||||
rejected_mask = (shift_token_type_ids == 2) & loss_mask
|
||||
|
||||
chosen_valid_len = chosen_mask.sum(dim=-1)
|
||||
rejected_valid_len = rejected_mask.sum(dim=-1)
|
||||
|
||||
ld_alpha = self.ld_alpha if use_ld else None
|
||||
if ld_alpha is not None:
|
||||
min_lengths = torch.min(chosen_valid_len, rejected_valid_len)
|
||||
chosen_starts = torch.argmax(chosen_mask.int(), dim=1)
|
||||
rejected_starts = torch.argmax(rejected_mask.int(), dim=1)
|
||||
|
||||
chosen_public_lengths = chosen_starts + min_lengths
|
||||
rejected_public_lengths = rejected_starts + min_lengths
|
||||
|
||||
seq_len = shift_labels.size(1)
|
||||
position_ids = torch.arange(seq_len, device=self.device).unsqueeze(0)
|
||||
|
||||
chosen_ld_mask = position_ids < chosen_public_lengths.unsqueeze(1)
|
||||
rejected_ld_mask = position_ids < rejected_public_lengths.unsqueeze(1)
|
||||
|
||||
chosen_front_mask = (chosen_ld_mask * chosen_mask).float()
|
||||
chosen_rear_mask = ((~chosen_ld_mask) * chosen_mask).float()
|
||||
rejected_front_mask = (rejected_ld_mask * rejected_mask).float()
|
||||
rejected_rear_mask = ((~rejected_ld_mask) * rejected_mask).float()
|
||||
|
||||
chosen_logps = (per_token_logps * chosen_front_mask).sum(dim=-1) + ld_alpha * (
|
||||
per_token_logps * chosen_rear_mask
|
||||
).sum(dim=-1)
|
||||
rejected_logps = (per_token_logps * rejected_front_mask).sum(dim=-1) + ld_alpha * (
|
||||
per_token_logps * rejected_rear_mask
|
||||
).sum(dim=-1)
|
||||
else:
|
||||
chosen_logps = (per_token_logps * chosen_mask.float()).sum(dim=-1)
|
||||
rejected_logps = (per_token_logps * rejected_mask.float()).sum(dim=-1)
|
||||
|
||||
chosen_logps_avg = chosen_logps / (chosen_valid_len + 1e-6)
|
||||
rejected_logps_avg = rejected_logps / (rejected_valid_len + 1e-6)
|
||||
|
||||
return chosen_logps, rejected_logps, chosen_logps_avg, rejected_logps_avg
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Model inputs (block-diagonal attention + per-document position_ids)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _prepare_model_inputs(self, input_ids: Tensor, token_type_ids: Tensor) -> dict[str, Tensor]:
|
||||
"""Build model inputs with block-diagonal attention and per-document position IDs.
|
||||
|
||||
In the v1 concatenated format each sample is::
|
||||
|
||||
[chosen prompt | chosen response | rejected prompt | rejected response]
|
||||
|
||||
with ``token_type_ids`` 1 / 2 marking the two documents. A plain causal
|
||||
mask would let the rejected half attend to the chosen half and produce
|
||||
contiguous RoPE positions across the boundary, biasing the DPO objective.
|
||||
|
||||
We instead:
|
||||
|
||||
* pass ``token_type_ids`` as the attention mask so that Transformers v5
|
||||
builds a **block-diagonal** causal mask (each document only attends to
|
||||
itself — see :class:`RMTrainer` for the same pattern).
|
||||
* compute ``position_ids`` that **reset at each document boundary** so
|
||||
that every document gets its own RoPE positions starting from 0.
|
||||
"""
|
||||
batch_size, seq_len = token_type_ids.shape
|
||||
arange = torch.arange(seq_len, device=self.device).unsqueeze(0).expand(batch_size, -1)
|
||||
|
||||
chosen_mask = token_type_ids == 1
|
||||
rejected_mask = token_type_ids == 2
|
||||
chosen_lens = chosen_mask.sum(dim=1, keepdim=True)
|
||||
|
||||
position_ids = torch.zeros_like(token_type_ids)
|
||||
position_ids[chosen_mask] = arange[chosen_mask]
|
||||
position_ids[rejected_mask] = (arange - chosen_lens)[rejected_mask]
|
||||
|
||||
return {
|
||||
"input_ids": input_ids,
|
||||
"attention_mask": token_type_ids, # block-diagonal doc mask (v5)
|
||||
"position_ids": position_ids,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reference log-probabilities (frozen model, no grad)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@torch.no_grad()
|
||||
def _compute_ref_logps(self, batch: BatchInput) -> tuple[Tensor, Tensor, Tensor, Tensor]:
|
||||
"""Forward the frozen reference model and return chosen/rejected log-probs.
|
||||
|
||||
For LoRA models the base weights are frozen, so we reuse the policy
|
||||
model with adapters disabled instead of maintaining a separate copy.
|
||||
"""
|
||||
input_ids = batch["input_ids"].to(self.device, non_blocking=True)
|
||||
labels = batch["labels"].to(self.device, non_blocking=True)
|
||||
token_type_ids = batch["token_type_ids"].to(self.device, non_blocking=True)
|
||||
|
||||
model_inputs = self._prepare_model_inputs(input_ids, token_type_ids)
|
||||
|
||||
if self._use_lora_ref:
|
||||
unwrapped = self._unwrapped_model
|
||||
with unwrapped.disable_adapter():
|
||||
ref_logits = unwrapped(**model_inputs, use_cache=False, return_dict=True).logits.float()
|
||||
else:
|
||||
ref_logits = self.ref_model(**model_inputs, use_cache=False, return_dict=True).logits.float()
|
||||
|
||||
return self._extract_chosen_rejected_logps(ref_logits, labels, token_type_ids, use_ld=False)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Loss functions
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _sigmoid_dpo_loss(
|
||||
self,
|
||||
policy_chosen_logps: Tensor,
|
||||
policy_rejected_logps: Tensor,
|
||||
ref_chosen_logps: Tensor,
|
||||
ref_rejected_logps: Tensor,
|
||||
) -> Tensor:
|
||||
"""Compute sigmoid DPO loss — delegates to :func:`compute_sigmoid_dpo_loss`."""
|
||||
return compute_sigmoid_dpo_loss(
|
||||
policy_chosen_logps,
|
||||
policy_rejected_logps,
|
||||
ref_chosen_logps,
|
||||
ref_rejected_logps,
|
||||
beta=self.pref_beta,
|
||||
label_smoothing=self.dpo_label_smoothing,
|
||||
)
|
||||
|
||||
def _odds_ratio_loss(self, chosen_logps_avg: Tensor, rejected_logps_avg: Tensor) -> Tensor:
|
||||
log_odds = (chosen_logps_avg - rejected_logps_avg) - (
|
||||
torch.log1p(-torch.exp(chosen_logps_avg)) - torch.log1p(-torch.exp(rejected_logps_avg))
|
||||
)
|
||||
sft_loss = -chosen_logps_avg
|
||||
odds_ratio_loss = -F.logsigmoid(log_odds)
|
||||
return sft_loss + self.pref_beta * odds_ratio_loss
|
||||
|
||||
def _simpo_loss(self, chosen_logps_avg: Tensor, rejected_logps_avg: Tensor) -> Tensor:
|
||||
pi_logratios = chosen_logps_avg - rejected_logps_avg
|
||||
gamma_logratios = self.simpo_gamma / self.pref_beta
|
||||
logits = pi_logratios - gamma_logratios
|
||||
simpo_loss = -F.logsigmoid(self.pref_beta * logits)
|
||||
return simpo_loss
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Main compute_loss
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def compute_loss(self, batch: BatchInput) -> Tensor:
|
||||
input_ids = batch["input_ids"].to(self.device, non_blocking=True)
|
||||
labels = batch["labels"].to(self.device, non_blocking=True)
|
||||
token_type_ids = batch["token_type_ids"].to(self.device, non_blocking=True)
|
||||
|
||||
# Block-diagonal attention (token_type_ids as doc mask) + per-document position_ids
|
||||
model_inputs = self._prepare_model_inputs(input_ids, token_type_ids)
|
||||
|
||||
# --- Policy forward ---
|
||||
model_output = self.model(**model_inputs, use_cache=False, return_dict=True)
|
||||
logits = model_output.logits.float()
|
||||
|
||||
# Split logits into chosen / rejected for metrics
|
||||
shift_logits = logits[..., :-1, :].contiguous()
|
||||
shift_token_type_ids = token_type_ids[..., 1:]
|
||||
chosen_logit_mask = (shift_token_type_ids == 1).float()
|
||||
rejected_logit_mask = (shift_token_type_ids == 2).float()
|
||||
|
||||
policy_chosen_logps, policy_rejected_logps, chosen_logps_avg, rejected_logps_avg = (
|
||||
self._extract_chosen_rejected_logps(logits, labels, token_type_ids)
|
||||
)
|
||||
|
||||
# Raw logits means (for logging)
|
||||
chosen_logits_mean = (shift_logits.mean(dim=-1) * chosen_logit_mask).sum() / (chosen_logit_mask.sum() + 1e-6)
|
||||
rejected_logits_mean = (shift_logits.mean(dim=-1) * rejected_logit_mask).sum() / (rejected_logit_mask.sum() + 1e-6)
|
||||
|
||||
if self.pref_loss == "sigmoid":
|
||||
if not self._use_lora_ref and self.ref_model is None:
|
||||
raise RuntimeError(
|
||||
"Reference model is required for sigmoid DPO loss but ref_model is None. "
|
||||
"This should not happen; the ref model is created at __init__ for sigmoid loss."
|
||||
)
|
||||
|
||||
ref_chosen_logps, ref_rejected_logps, _, _ = self._compute_ref_logps(batch)
|
||||
losses = self._sigmoid_dpo_loss(
|
||||
policy_chosen_logps,
|
||||
policy_rejected_logps,
|
||||
ref_chosen_logps,
|
||||
ref_rejected_logps,
|
||||
)
|
||||
# DPO rewards: beta * (policy_logps - ref_logps)
|
||||
chosen_rewards = (self.pref_beta * (policy_chosen_logps - ref_chosen_logps)).detach()
|
||||
rejected_rewards = (self.pref_beta * (policy_rejected_logps - ref_rejected_logps)).detach()
|
||||
elif self.pref_loss == "orpo":
|
||||
losses = self._odds_ratio_loss(chosen_logps_avg, rejected_logps_avg)
|
||||
chosen_rewards = (self.pref_beta * chosen_logps_avg).detach()
|
||||
rejected_rewards = (self.pref_beta * rejected_logps_avg).detach()
|
||||
elif self.pref_loss == "simpo":
|
||||
losses = self._simpo_loss(chosen_logps_avg, rejected_logps_avg)
|
||||
chosen_rewards = (self.pref_beta * chosen_logps_avg).detach()
|
||||
rejected_rewards = (self.pref_beta * rejected_logps_avg).detach()
|
||||
else:
|
||||
raise ValueError(f"Unknown pref_loss: {self.pref_loss}")
|
||||
|
||||
if self.pref_ftx > 1e-6:
|
||||
sft_loss = -chosen_logps_avg
|
||||
losses = losses + self.pref_ftx * sft_loss
|
||||
|
||||
# --- Per-step DPO metrics (matches v0 logging) ---
|
||||
self._step_metrics = {
|
||||
"rewards/chosen": chosen_rewards.mean().item(),
|
||||
"rewards/rejected": rejected_rewards.mean().item(),
|
||||
"rewards/accuracies": (chosen_rewards > rejected_rewards).float().mean().item(),
|
||||
"rewards/margins": (chosen_rewards - rejected_rewards).mean().item(),
|
||||
"logps/chosen": policy_chosen_logps.mean().item(),
|
||||
"logps/rejected": policy_rejected_logps.mean().item(),
|
||||
"logits/chosen": chosen_logits_mean.item(),
|
||||
"logits/rejected": rejected_logits_mean.item(),
|
||||
}
|
||||
|
||||
return losses.mean()
|
||||
|
||||
|
||||
def run_dpo(args: InputArgument = None):
|
||||
model_args, data_args, training_args, _ = get_args(args)
|
||||
if getattr(training_args, "use_cpu", False):
|
||||
os.environ["FORCE_V1_CPU"] = "1"
|
||||
DistributedInterface(training_args.dist_config)
|
||||
train_dataset = DataEngine(data_args.train_dataset)
|
||||
_validate_dpo_dataset_format(train_dataset, data_args.train_dataset)
|
||||
model_engine = ModelEngine(model_args, is_train=True)
|
||||
trainer = DPOTrainer(
|
||||
args=training_args,
|
||||
model=model_engine.model,
|
||||
renderer=model_engine.renderer,
|
||||
train_dataset=train_dataset,
|
||||
)
|
||||
trainer.fit()
|
||||
trainer.save_model()
|
||||
DistributedInterface().destroy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_dpo()
|
||||
|
||||
Reference in New Issue
Block a user