unsloth/unsloth/models/rl.py
2025-02-11 01:36:57 -08:00

456 lines
17 KiB
Python

# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved.
#
# 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.
__all__ = [
"PatchFastRL",
]
import torch
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
import inspect
import os
import re
from unsloth_zoo.compiler import create_new_function
from unsloth_zoo.logging_utils import PatchRLStatistics
def PatchRL(FastLanguageModel):
from trl.models.utils import unwrap_model_for_generation
from contextlib import contextmanager
@contextmanager
def unsloth_unwrap_model_for_generation(model, *args, **kwargs):
with unwrap_model_for_generation(model, *args, **kwargs) as unwrapped_model:
# Put the model in inference mode.
FastLanguageModel.for_inference(unwrapped_model)
# We must use .clone for Unsloth since we force inference_mode
# Rather we should have used no_grad
original_generate = unwrapped_model.generate
def generate_with_clone(*args, **kwargs):
out = original_generate(*args, **kwargs)
if isinstance(out, torch.Tensor):
return out.clone()
return out
pass
unwrapped_model.generate = generate_with_clone
try:
yield unwrapped_model
finally:
# Restore generate and return
unwrapped_model.generate = original_generate
FastLanguageModel.for_training(model)
pass
pass
pass
import trl.trainer
trainers = dir(trl.trainer)
trainers = [x for x in trainers if x.endswith("_trainer")]
unwrap = "unwrap_model_for_generation"
for trainer in trainers:
try: current_trainer = eval(f"trl.trainer.{trainer}")
except: continue
if hasattr(current_trainer, unwrap):
try: exec(f"trl.trainer.{trainer}.{unwrap} = unsloth_{unwrap}")
except: continue
pass
pass
RLTrainer_replacement = '''
from typing import *
from dataclasses import dataclass, field
@dataclass
class Unsloth{RLConfig_name}({RLConfig_name}):
"""
{__RLConfig_doc__}
"""
sampling_params: Optional[Any] = field(
default = None,
metadata = {{'help': 'vLLM SamplingParams'}},
)
def __init__({RLConfig_arguments},
sampling_params = None
):
{RLConfig_extra_args}
super().__init__({RLConfig_call_args})
pass
{RLTrainer_extras}
class Unsloth{RLTrainer_name}(_Unsloth{RLTrainer_name}):
"""
{__RLTrainer_doc__}
"""
def __init__({RLTrainer_arguments}
):
if args is None: args = Unsloth{RLConfig_name}()
{RLTrainer_extra_args}
super().__init__({RLTrainer_call_args})
pass
'''
def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"):
# Patch for vLLM and Unsloth PEFT
import trl
import trl.trainer
try:
trainer = eval(f"trl.trainer.{trainer_file}")
except Exception as error:
return
# Get SFTTrainer and SFTConfig names
name = [x for x in dir(trainer) if x.endswith("Trainer") and x != "Trainer" and trainer_file.split("_")[0] in x.lower()]
config = [x for x in dir(trainer) if x.endswith("Config") and x != "Config" and trainer_file.split("_")[0] in x.lower()]
if len(name) != 1: return
if len(config) != 1: return
# Get SFTTrainer, SFTConfig
RLTrainer_name = name[0]
RLConfig_name = config[0]
try: RLTrainer = eval(f"trl.trainer.{trainer_file}.{RLTrainer_name}")
except: return
try: RLConfig = eval(f"trl.trainer.{trainer_file}.{RLConfig_name}" )
except: return
# Check name
if RLTrainer.__name__.startswith("Unsloth"): return
if RLConfig .__name__.startswith("Unsloth"): return
all_imports = dir(trainer)
imports = [x for x in all_imports if not x.startswith("_")]
# Get default arguments
EMPTY = inspect.Parameter.empty
processed = []
for RLobject in [RLTrainer, RLConfig]:
parameters = inspect.signature(RLobject.__init__).parameters
types = (bool, type(None), int, float, str,)
arguments = ["self"]
call_args = []
for k, v in parameters.items():
if k == "self": continue
v = v.default
if v == "\n": v = re.escape("\n")
if v is EMPTY: arguments.append(k)
elif type(v) is str: arguments.append(f"{k} = '{v}'")
elif type(v) in types: arguments.append(f"{k} = {v}")
else: continue
call_args.append(f"{k} = {k}")
pass
arguments = f"\n{' '*8}" + f",\n{' '*8}".join(arguments)
call_args = f"\n{' '*12}" + f",\n{' '*12}".join(call_args)
processed.append((arguments, call_args,))
pass
# Process RLTrainer first
arguments, call_args = processed[0]
# Add tokenizer if not seen
if "tokenizer" not in parameters and "processing_class" in parameters:
arguments += f",\n{' '*8}tokenizer = None"
call_args = call_args.replace(
"processing_class = processing_class",
"processing_class = tokenizer if tokenizer is not None else processing_class",
)
pass
# Edit bf16, fp16 by checking model's torch_dtype directly
extra_args = ""
if "args" in call_args:
mixed_precision = \
"use_bf16 = getattr(args, 'bf16', False)\n"\
"use_fp16 = getattr(args, 'fp16', False)\n"\
"dtype = getattr(model.config, 'torch_dtype', None)\n"\
"if dtype is None: dtype = model.get_input_embeddings().dtype\n"\
"from unsloth_zoo.utils import _get_dtype\n"\
"dtype = _get_dtype(dtype)\n"\
"float16 = dtype == torch.float16\n"\
"if float16 and use_bf16: raise TypeError('Unsloth: Model is in float16 precision but you want to use bfloat16 precision. Set fp16 to `True` and bf16 to `False`')\n"\
"if not float16 and use_fp16: raise TypeError('Unsloth: Model is in bfloat16 precision but you want to use float16 precision. Set fp16 to `False` and bf16 to `True`')\n"\
"if not use_bf16 and not use_fp16:\n"\
" args.fp16 = float16\n"\
" args.bf16 = not float16\n"
extra_args += mixed_precision
pass
# Check if per_device_eval_batch_size (default 8) bigger than bsz
# Also use FP16 / BF16 evaluation
if "args" in call_args:
# Check eval_dataset first
if "eval_dataset" in call_args:
check_eval_dataset = \
"if getattr(args, 'eval_strategy', 'no') == 'no':\n"\
" args.eval_strategy = 'steps'\n"\
" if getattr(args, 'eval_steps', None) is None: args.eval_steps = 0.1\n"
extra_args += check_eval_dataset
pass
eval_changes = \
"ga_steps = getattr(args, 'gradient_accumulation_steps', None)\n"\
"if getattr(args, 'eval_strategy', 'no') != 'no':\n"\
" eval_bsz = getattr(args, 'per_device_eval_batch_size', 8)\n"\
" if eval_bsz == 8 and args.per_device_train_batch_size < eval_bsz: args.per_device_eval_batch_size = args.per_device_train_batch_size\n"\
" if getattr(args, 'eval_accumulation_steps', None) is None and ga_steps is not None: args.eval_accumulation_steps = ga_steps\n"\
"fp16_full_eval = getattr(args, 'fp16_full_eval', False)\n"\
"bf16_full_eval = getattr(args, 'bf16_full_eval', False)\n"\
"if args.fp16 and bf16_full_eval: args.bf16_full_eval = False; args.fp16_full_eval = True\n"\
"if args.bf16 and fp16_full_eval: args.bf16_full_eval = True; args.fp16_full_eval = False\n"\
"if not bf16_full_eval and not fp16_full_eval: args.bf16_full_eval = args.bf16; args.fp16_full_eval = args.fp16\n"
extra_args += eval_changes
pass
# Add statistics as well!
extra_args += \
"from unsloth_zoo.logging_utils import PatchRLStatistics\n"\
f"PatchRLStatistics('{trainer_file}')\n"
# Create RLTrainer args
extra_args = extra_args.split("\n")
extra_args = "\n".join(" "*8 + x for x in extra_args)
RLTrainer_arguments = arguments
RLTrainer_extra_args = extra_args
RLTrainer_call_args = call_args
# Fix RLConfig next
arguments, call_args = processed[1]
extra_args = ""
# Edit GA / bsz and weight_decay
replacements = {
"output_dir" : 'unsloth_training_checkpoints',
"logging_nan_inf_filter" : False,
"per_device_train_batch_size" : 4,
"gradient_accumulation_steps" : 2,
"weight_decay" : 0.01,
"warmup_ratio" : 0.1,
"seed" : 3407,
"optim" : "adamw_8bit",
"learning_rate" : 5e-05,
"per_device_eval_batch_size" : 4,
"eval_accumulation_steps" : 2,
"torch_empty_cache_steps" : 250,
"logging_steps" : 1,
}
for k, v in replacements.items():
x = f"{k}( = [^,\n]{{1,}})?,\n"
y = f"'{v}'" if type(v) is str else f"{v}"
y = f"{k} = {y},\n"
arguments = re.sub(x, y, arguments)
pass
# Warn on too large or too small learning rate
if " learning_rate" in call_args:
learning_rate_check = \
"if learning_rate < 1e-7: raise FloatingPointError(f'Unsloth: Your learning rate of `{learning_rate}` is too small and less than 1e-7! Consider increasing it, otherwise gradient updates will be close to 0!')\n"\
"if learning_rate > 1: raise OverflowError(f'Unsloth: Your learning rate of `{learning_rate}` is way too larger > 1! Consider decreasing it to 1e-1, otherwise gradient updates will explode!')"
extra_args += learning_rate_check
pass
# Create RLConfig args
extra_args = extra_args.split("\n")
extra_args = "\n".join(" "*8 + x for x in extra_args)
RLConfig_arguments = arguments
RLConfig_extra_args = extra_args
RLConfig_call_args = call_args
# Patch vLLM
RLTrainer_extras = patch_vllm(RLTrainer, trainer_file, RLTrainer_name, all_imports, imports)
if RLTrainer_extras is None:
RLTrainer_extras = f"_Unsloth{RLTrainer_name} = {RLTrainer_name}"
# Create full module
exec(f"from trl.trainer import ({RLTrainer_name}, {RLConfig_name},)")
__RLTrainer_doc__ = eval(f"trl.trainer.{RLTrainer_name}").__doc__
__RLConfig_doc__ = eval(f"trl.trainer.{RLConfig_name}") .__doc__
RLTrainer_source = RLTrainer_replacement.format(
RLTrainer_name = RLTrainer_name,
__RLTrainer_doc__ = __RLTrainer_doc__,
RLTrainer_arguments = RLTrainer_arguments,
RLTrainer_extra_args = RLTrainer_extra_args,
RLTrainer_call_args = RLTrainer_call_args,
RLConfig_name = RLConfig_name,
__RLConfig_doc__ = __RLConfig_doc__,
RLConfig_arguments = RLConfig_arguments,
RLConfig_extra_args = RLConfig_extra_args,
RLConfig_call_args = RLConfig_call_args,
RLTrainer_extras = RLTrainer_extras,
)
# Create new function
created_module = create_new_function(
f"Unsloth{RLTrainer_name}",
RLTrainer_source,
f"trl.trainer.{trainer_file}",
imports,
)
# Patch Trainer
exec(f"trl.{RLTrainer_name} = created_module.Unsloth{RLTrainer_name}", locals(), globals())
exec(f"trl.trainer.{RLTrainer_name} = created_module.Unsloth{RLTrainer_name}", locals(), globals())
exec(f"trl.trainer.{trainer_file}.{RLTrainer_name} = created_module.Unsloth{RLTrainer_name}", locals(), globals())
# Patch Config
exec(f"trl.{RLConfig_name} = created_module.Unsloth{RLConfig_name}", locals(), globals())
exec(f"trl.trainer.{RLConfig_name} = created_module.Unsloth{RLConfig_name}", locals(), globals())
exec(f"trl.trainer.{trainer_file}.{RLConfig_name} = created_module.Unsloth{RLConfig_name}", locals(), globals())
pass
def patch_vllm(RLTrainer, trainer_file, RLTrainer_name, all_imports, imports):
init = inspect.getsource(RLTrainer.__init__)
old_init = init
# Remove peft_config
init = init.replace("elif peft_config is None:", "elif False:")
init = init.replace("elif peft_config is not None:", "elif False:")
init = init.replace("if peft_config is None:", "if False:")
init = init.replace("if peft_config is not None:", "if False:")
init = init.replace("get_peft_model(model, peft_config)", "model")
# Set use_vllm if not set
init = re.sub(
r"\)([ ]{0,}\-\>[ ]{0,}None[ ]{0,}):\n([\s]{4})",
r"):\n\2 "\
r"if hasattr(model, 'vllm_engine') and "\
r"getattr(args, 'use_vllm') and getattr(args, 'use_vllm', False): "\
r"args.use_vllm = True\n\2",
init, 1,
)
vllm_part = re.findall(
r"(\n[\s]{8}"\
r"if (self|args)\.use_vllm\:.+?"\
r"\n[\s]{8,}"\
"else:\n)",
init,
flags = re.MULTILINE | re.DOTALL,
)
if len(vllm_part) != 1: return None
vllm_part, args = vllm_part[0][0], vllm_part[0][1]
# Strip all comments
new_vllm_part = re.sub(r"\#[^\n]{1,}\n", "", vllm_part)
# Get SamplingParams
sampling_params = re.findall(
r"\n[\s]{4,}(self\.[^\s]{1,}[\s]{0,}\=[\s]{0,}"\
r"SamplingParams\(.+?\))",
new_vllm_part,
flags = re.MULTILINE | re.DOTALL,
)
if len(sampling_params) != 1: return None
sampling_params = sampling_params[0]
# Replace with our vLLM engine
sampling_params = \
" "*12 + "self.llm = model.vllm_engine; self._last_loaded_step = 0; " + \
sampling_params # Add spaces
new_vllm_part = \
f"\n{' '*8}if {args}.use_vllm:\n{sampling_params} "\
f"if getattr(args, 'sampling_params', None) is None else "\
f"getattr(args, 'sampling_params', None)\n{' '*8}else:\n"
init = init.replace(vllm_part, new_vllm_part)
# Search for vLLM calling in all child functions
functions = dir(RLTrainer)
RLTrainer_source = inspect.getsource(RLTrainer)
functions = [x for x in functions if f"def {x}" in RLTrainer_source]
changed = {"__init__" : (old_init, init,)}
for function in functions:
if not hasattr(RLTrainer, function): continue
fx = getattr(RLTrainer, function)
try: source = inspect.getsource(fx)
except: continue
original_source = source
# llm_model = self.llm.llm_engine.model_executor.driver_worker.model_runner.model
source = re.sub(
r"(\n[\s]{4,}).+?model_executor\.driver_worker.+?\n",
r"\n\1pass\n",
source,
)
# llm_model.load_weights(model.state_dict().items())
source = re.sub(
r"(\n[\s]{4,}).+?load_weights\(.+?\n",
r"\n\1pass\n",
source,
)
# .state_dict()
source = re.sub(
r"\.state_dict\(\)",
r"",
source,
)
# Replace self.llm.generate and self.llm.chat
lora_name = trainer_file + "_lora_model"
source = re.sub(
r"(self\.llm\.(?:generate|chat)\([^\)]{1,})\)",
r"\1, lora_request = self.model.load_lora('" + lora_name + r"', load_tensors = True))",
source
)
# Skip if no changes done
if source == original_source: continue
# Find all imports
imports += [x for x in all_imports if not x.startswith("_") and x in source]
changed[function] = (original_source, source,)
pass
# Import all functions
imports = list(set(imports))
# Patch all functions
for function in changed:
old, new = changed[function]
RLTrainer_source = RLTrainer_source.replace(old, new)
pass
RLTrainer_source = RLTrainer_source.replace(
f"class {RLTrainer_name}", f"class _Unsloth{RLTrainer_name}", 1
)
return RLTrainer_source
pass
def patch_trl_rl_trainers():
# Patch all TRL modules if they have vLLM or PEFT
import trl.trainer
all_trainers = dir(trl.trainer)
all_trainers = [x for x in all_trainers if x.islower() and x.endswith("_trainer")]
for trainer in all_trainers:
_patch_trl_rl_trainers(trainer)
return
pass
def PatchFastRL(algorithm = "GRPO", FastLanguageModel = None):
if FastLanguageModel is not None: PatchRL(FastLanguageModel)
patch_trl_rl_trainers()
if algorithm is not None: PatchRLStatistics(algorithm)
pass