Repository: huggingface/trl

Issue:
KTO - support loading the adapter twice
For DPOTrainer there exists the option to load the Adapter from SFT training twice, as in [Reference model considerations with PEFT - load-the-adapter-twice](https://huggingface.co/docs/trl/main/en/dpo_trainer#using-option-3---load-the-adapter-twice):

``` python
# Initialize the trainer, without a ref_model param.
dpo_trainer = DPOTrainer(
    model,
    ...
    model_adapter_name="train",
    ref_adapter_name="reference",
)
```

In DPOTrainer this allows me to use a larger batch size and speed up training.
Would be cool to have this option in KTOTrainer as well. Anyone working on this already?


Discussion:
Hi @lewtun , we had a discussion about KTO. Do you already work on this or should we come up with a PR?

We would try and use the code from DPO and apply it to KTO to implement this.
@younesbelkada @kashif, is this a desired feature for you as well?  
If yes, is someone already implementing it or should we come up with a PR?  
 I believe would be good to have yes, and no one is working on it
That feature would be super useful @claralp . Thanks.

Resolving patch:
diff --git a/trl/trainer/kto_trainer.py b/trl/trainer/kto_trainer.py
--- a/trl/trainer/kto_trainer.py
+++ b/trl/trainer/kto_trainer.py
@@ -16,7 +16,7 @@
 import random
 import warnings
 from collections import defaultdict
-from contextlib import nullcontext
+from contextlib import contextmanager, nullcontext
 from copy import deepcopy
 from functools import wraps
 from operator import itemgetter
@@ -257,6 +257,10 @@ class KTOTrainer(Trainer):
         compute_metrics (`Callable[[EvalPrediction], Dict]`, *optional*):
             The function to use to compute the metrics. Must take a `EvalPrediction` and return
             a dictionary string to metric values.
+        model_adapter_name (`str`, defaults to `None`):
+            Name of the train target PEFT adapter, when using LoRA with multiple adapters.
+        ref_adapter_name (`str`, defaults to `None`):
+            Name of the reference PEFT adapter, when using LoRA with multiple adapters.
     """
 
     _tag_names = ["trl", "kto"]
@@ -276,6 +280,8 @@ def __init__(
         preprocess_logits_for_metrics: Optional[Callable[[torch.Tensor, torch.Tensor], torch.Tensor]] = None,
         peft_config: Optional[Dict] = None,
         compute_metrics: Optional[Callable[[EvalLoopOutput], Dict]] = None,
+        model_adapter_name: Optional[str] = None,
+        ref_adapter_name: Optional[str] = None,
     ):
         if type(args) == TrainingArguments:
             raise ValueError("Please use `KTOConfig` instead TrainingArguments.")
@@ -392,6 +398,8 @@ def make_inputs_require_grad(module, input, output):
             self.is_encoder_decoder = args.is_encoder_decoder
 
         self.is_peft_model = is_peft_available() and isinstance(model, PeftModel)
+        self.model_adapter_name = model_adapter_name
+        self.ref_adapter_name = ref_adapter_name
 
         if ref_model:
             self.ref_model = ref_model
@@ -677,6 +685,18 @@ def _prepare_deepspeed(self, model: PreTrainedModelWrapper):
         model.eval()
         return model
 
+    @contextmanager
+    def null_ref_context(self):
+        """Context manager for handling null reference model (that is, peft adapter manipulation)."""
+        with self.accelerator.unwrap_model(
+            self.model
+        ).disable_adapter() if self.is_peft_model and not self.ref_adapter_name else nullcontext():
+            if self.ref_adapter_name:
+                self.model.set_adapter(self.ref_adapter_name)
+            yield
+            if self.ref_adapter_name:
+                self.model.set_adapter(self.model_adapter_name or "default")
+
     def get_train_dataloader(self) -> DataLoader:
         """
         Returns the training [`~torch.utils.data.DataLoader`].
@@ -775,9 +795,7 @@ def compute_reference_log_probs(self, padded_batch: Dict) -> Dict:
         """Computes log probabilities of the reference model for a single padded batch of a KTO specific dataset."""
         with torch.no_grad():
             if self.ref_model is None:
-                with self.accelerator.unwrap_model(
-                    self.model
-                ).disable_adapter() if self.is_peft_model else nullcontext():
+                with self.null_ref_context():
                     if self.is_encoder_decoder:
                         completion_logits = self.model(
                             padded_batch["prompt_input_ids"],
@@ -1029,7 +1047,7 @@ def get_batch_loss_metrics(
         else:
             with torch.no_grad():
                 if self.ref_model is None:
-                    with self.accelerator.unwrap_model(self.model).disable_adapter():
+                    with self.null_ref_context():
                         (
                             reference_chosen_logps,
                             reference_rejected_logps,


Test patch: