@@ -797,6 +797,49 @@ def test_train_peft_config(self):
797797 elif "base_layer" not in n : # We expect the peft params to be different (except for the base layer)
798798 assert not torch .equal (param , new_param ), f"Parameter { n } has not changed."
799799
800+ @require_peft
801+ @require_bitsandbytes
802+ def test_train_peft_and_quantization (self ):
803+ dataset = load_dataset ("trl-internal-testing/zen" , "standard_prompt_only" , split = "train" )
804+
805+ training_args = GRPOConfig (
806+ output_dir = self .tmp_dir ,
807+ learning_rate = 0.1 , # use higher lr because gradients are tiny and default lr can stall updates
808+ per_device_train_batch_size = 3 , # reduce the batch size to reduce memory usage
809+ num_generations = 3 , # reduce the number of generations to reduce memory usage
810+ max_completion_length = 8 , # reduce the completion length to reduce memory usage
811+ report_to = "none" ,
812+ )
813+ quantization_config = BitsAndBytesConfig (
814+ load_in_4bit = True ,
815+ bnb_4bit_use_double_quant = True ,
816+ bnb_4bit_quant_type = "nf4" ,
817+ bnb_4bit_compute_dtype = torch .bfloat16 ,
818+ )
819+ trainer = GRPOTrainer (
820+ model = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" , # identifier, so that the trainer quantizes it
821+ reward_funcs = "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" ,
822+ args = training_args ,
823+ train_dataset = dataset ,
824+ quantization_config = quantization_config ,
825+ peft_config = LoraConfig (),
826+ )
827+
828+ previous_trainable_params = {n : param .clone () for n , param in trainer .model .named_parameters ()}
829+
830+ trainer .train ()
831+
832+ assert trainer .state .log_history [- 1 ]["train_loss" ] is not None
833+
834+ # Check that the peft params have changed, and that they are cast to bfloat16, as recommended by the QLoRA
835+ # paper. The base model params are not checked: bitsandbytes casts the biases of a Linear4bit in-place during
836+ # the forward pass, so some of them change in a way that is unrelated to training.
837+ for n , param in previous_trainable_params .items ():
838+ new_param = trainer .model .get_parameter (n )
839+ if "lora" in n : # We expect the peft params to be different
840+ assert param .dtype == torch .bfloat16 , f"Parameter { n } is not in bfloat16."
841+ assert not torch .equal (param , new_param ), f"Parameter { n } has not changed."
842+
800843 @require_peft
801844 def test_liger_kernel_with_peft_lm_head_raises (self ):
802845 # The Liger fused GRPO loss reads `lm_head.weight` directly, so a LoRA adapter on `lm_head` is silently
0 commit comments