Skip to content

Commit bc6e800

Browse files
authored
Fix crash when reward function is a functools.partial or callable instance (#6313)
1 parent af15d90 commit bc6e800

7 files changed

Lines changed: 55 additions & 8 deletions

File tree

tests/test_utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import copy
16+
import functools
1617
import textwrap
1718
from io import StringIO
1819
from unittest.mock import patch
@@ -37,6 +38,7 @@
3738
entropy_from_logits,
3839
flush_left,
3940
generate_model_card,
41+
get_callable_name,
4042
get_peft_config,
4143
hash_module,
4244
nanstd,
@@ -279,6 +281,36 @@ def test_create_peft_config_use_peft_true(self):
279281
assert getattr(peft_config, arg) == value
280282

281283

284+
class TestGetCallableName(TrlTestCase):
285+
def test_function(self):
286+
def accuracy_reward(completions):
287+
return [0.0] * len(completions)
288+
289+
assert get_callable_name(accuracy_reward) == "accuracy_reward"
290+
291+
def test_partial(self):
292+
def reward(completions, threshold):
293+
return [0.0] * len(completions)
294+
295+
assert get_callable_name(functools.partial(reward, threshold=0.5)) == "reward"
296+
297+
def test_nested_partial(self):
298+
def reward(completions, threshold):
299+
return [0.0] * len(completions)
300+
301+
assert get_callable_name(functools.partial(functools.partial(reward), threshold=0.5)) == "reward"
302+
303+
def test_callable_instance(self):
304+
class LengthReward:
305+
def __call__(self, completions):
306+
return [0.0] * len(completions)
307+
308+
assert get_callable_name(LengthReward()) == "LengthReward"
309+
310+
def test_lambda(self):
311+
assert get_callable_name(lambda completions: [0.0] * len(completions)) == "<lambda>"
312+
313+
282314
class TestNanStd(TrlTestCase):
283315
def test_nanstd_ignores_nans(self):
284316
x = torch.tensor([1.0, 2.0, 3.0, float("nan")])

trl/experimental/async_grpo/async_rollout_worker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
parse_response,
4646
)
4747
from ...import_utils import is_vllm_available
48-
from ...trainer.utils import print_prompt_completions_sample
48+
from ...trainer.utils import get_callable_name, print_prompt_completions_sample
4949

5050

5151
logger = get_logger(__name__)
@@ -308,7 +308,7 @@ def __init__(
308308
self.dataset = dataset
309309
self._dataset_iter = iter(dataset)
310310
self.reward_funcs = reward_funcs
311-
self.reward_func_names = [f.__name__ for f in reward_funcs]
311+
self.reward_func_names = [get_callable_name(f) for f in reward_funcs]
312312
# `add_response_schema` sets the response template (transformers >= 5.13) or legacy schema for known chat
313313
# templates, so tool calls can be parsed. Skip if one is already set; warn if it's a migratable legacy schema.
314314
has_template = getattr(processing_class, "response_template", None) is not None

trl/experimental/online_dpo/online_dpo_trainer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from ...import_utils import is_vllm_available
5555
from ...models.utils import prepare_deepspeed, prepare_fsdp, unwrap_model_for_generation
5656
from ...trainer.base_trainer import _BaseTrainer
57-
from ...trainer.utils import disable_dropout_in_model, ensure_master_addr_port, get_config_model_id
57+
from ...trainer.utils import disable_dropout_in_model, ensure_master_addr_port, get_callable_name, get_config_model_id
5858
from ..utils import DPODataCollatorWithPadding, create_reference_model, empty_cache, prepare_peft_model, truncate_right
5959
from .online_dpo_config import OnlineDPOConfig
6060

@@ -217,7 +217,7 @@ def __init__(
217217
if isinstance(reward_funcs[i], nn.Module):
218218
self.reward_func_names.append(get_config_model_id(reward_funcs[i].config).split("/")[-1])
219219
else:
220-
self.reward_func_names.append(reward_funcs[i].__name__)
220+
self.reward_func_names.append(get_callable_name(reward_funcs[i]))
221221
self.reward_funcs = reward_funcs
222222

223223
# Handle reward processing classes for reward_funcs

trl/experimental/sdpo/sdpo_trainer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
RepeatSampler,
5252
create_model_from_path,
5353
disable_dropout_in_model,
54+
get_callable_name,
5455
get_config_model_id,
5556
identity,
5657
pad,
@@ -630,7 +631,7 @@ def __init__(
630631
if isinstance(reward_funcs[i], nn.Module):
631632
self.reward_func_names.append(get_config_model_id(reward_funcs[i].config).split("/")[-1])
632633
else:
633-
self.reward_func_names.append(reward_funcs[i].__name__)
634+
self.reward_func_names.append(get_callable_name(reward_funcs[i]))
634635
self.reward_funcs = reward_funcs
635636

636637
if args.reward_weights is not None:

trl/trainer/grpo_trainer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
create_model_from_path,
8282
disable_dropout_in_model,
8383
entropy_from_logits,
84+
get_callable_name,
8485
get_config_model_id,
8586
identity,
8687
nanmax,
@@ -492,7 +493,7 @@ def __init__(
492493
if isinstance(reward_funcs[i], nn.Module): # Use Module over PretrainedModel for compat w/ compiled models
493494
self.reward_func_names.append(get_config_model_id(reward_funcs[i].config).split("/")[-1])
494495
else:
495-
self.reward_func_names.append(reward_funcs[i].__name__)
496+
self.reward_func_names.append(get_callable_name(reward_funcs[i]))
496497
self.reward_funcs = reward_funcs
497498

498499
# Reward weights

trl/trainer/rloo_trainer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
create_model_from_path,
6565
disable_dropout_in_model,
6666
entropy_from_logits,
67+
get_callable_name,
6768
get_config_model_id,
6869
identity,
6970
nanmax,
@@ -407,7 +408,7 @@ def __init__(
407408
if isinstance(reward_funcs[i], nn.Module): # Use Module over PretrainedModel for compat w/ compiled models
408409
self.reward_func_names.append(get_config_model_id(reward_funcs[i].config).split("/")[-1])
409410
else:
410-
self.reward_func_names.append(reward_funcs[i].__name__)
411+
self.reward_func_names.append(get_callable_name(reward_funcs[i]))
411412
self.reward_funcs = reward_funcs
412413

413414
self._has_async_funcs = any(inspect.iscoroutinefunction(func) for func in self.reward_funcs)

trl/trainer/utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
# limitations under the License.
1414

1515
import asyncio
16+
import functools
1617
import hashlib
1718
import importlib.resources as pkg_resources
1819
import os
1920
import random
2021
import socket
2122
import threading
2223
import types
23-
from collections.abc import Mapping, Sequence, Sized
24+
from collections.abc import Callable, Mapping, Sequence, Sized
2425
from contextlib import contextmanager
2526
from importlib.metadata import version
2627
from itertools import accumulate
@@ -182,6 +183,17 @@ def disable_dropout_in_model(model: torch.nn.Module) -> None:
182183
module.p = 0
183184

184185

186+
def get_callable_name(func: Callable) -> str:
187+
"""
188+
Return a display name for a callable, supporting the picklable reward forms: module-level functions,
189+
[`functools.partial`](https://docs.python.org/3/library/functools.html#functools.partial) (unwrapped to the wrapped
190+
function's name), and callable class instances (which fall back to their class name).
191+
"""
192+
while isinstance(func, functools.partial):
193+
func = func.func
194+
return getattr(func, "__name__", type(func).__name__)
195+
196+
185197
def get_quantization_config(model_args: ModelConfig) -> BitsAndBytesConfig | None:
186198
if model_args.load_in_4bit:
187199
quantization_config = BitsAndBytesConfig(

0 commit comments

Comments
 (0)