Skip to content

Commit 2e4bd3b

Browse files
author
Pooya Moradi
committed
Add reward_functions_path + reward_functions CLI knobs for custom rewards
Currently the reward stack is hardcoded to a 3-fn list: [match_format_exactly, match_format_approximately, check_numbers]. Replacing it requires editing train_rl.py. Add two new config fields: - reward_functions_path: path to a user Python file - reward_functions: comma-separated list of function names to import When both are set, the built-in reward stack is REPLACED entirely by the user-provided functions (so users can pin a single VTC-style partial-credit reward, swap in a math_verify-based scorer, etc., without editing maxtext). Each user function must accept (prompts, completions, tmvp_config, **kwargs) and return a list of floats. Default (both empty) keeps existing behavior unchanged. Reuses `_load_custom_callable` helper added in the previous commit.
1 parent be7748b commit 2e4bd3b

4 files changed

Lines changed: 93 additions & 6 deletions

File tree

src/maxtext/configs/post_train/rl.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ eval_dataset_name: 'openai/gsm8k'
238238
# tmvp_config, x) -> dict with keys {prompts, question, answer}. When empty
239239
# (default), the built-in utils_rl.process_data is used.
240240
dataset_processor_path: ''
241+
# Optional: path to a user-provided Python file with custom reward functions,
242+
# AND a comma-separated list of function names to import from that file.
243+
# Each function signature: fn(prompts, completions, tmvp_config, **kwargs) -> list[float].
244+
# When both are set, the built-in [match_format_exactly, match_format_approximately,
245+
# check_numbers] reward list is REPLACED entirely.
246+
reward_functions_path: ''
247+
reward_functions: ''
241248
train_split: 'train'
242249
eval_split: 'test'
243250
hf_name: 'main' # subset of Hugging Face dataset

src/maxtext/configs/types.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,6 +2056,20 @@ class RLDataset(BaseModel):
20562056
"When set, replaces the built-in dataset processor for custom datasets."
20572057
),
20582058
)
2059+
reward_functions_path: str = Field(
2060+
"",
2061+
description=(
2062+
"Optional path to a user Python file containing custom reward functions. "
2063+
"Used with `reward_functions` to fully replace the built-in reward stack."
2064+
),
2065+
)
2066+
reward_functions: str = Field(
2067+
"",
2068+
description=(
2069+
"Comma-separated names of reward functions to import from `reward_functions_path`. "
2070+
"Each function signature: (prompts, completions, tmvp_config, **kwargs) -> list[float]."
2071+
),
2072+
)
20592073

20602074

20612075
class RLEvaluation(BaseModel):

src/maxtext/trainers/post_train/rl/train_rl.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from __future__ import annotations
4747
import contextlib
4848
from functools import wraps
49-
from typing import Any, Optional, Sequence
49+
from typing import Any, Callable, Optional, Sequence
5050

5151
import datasets
5252
import grain
@@ -363,6 +363,31 @@ def _use_raw_prompt(x):
363363
return train_dataset, test_dataset
364364

365365

366+
def build_reward_fns(trainer_config: Any, make_reward_fn: Callable) -> list:
367+
"""Build the reward-function stack for the RL trainer.
368+
369+
`reward_functions_path` is a filesystem path to a Python file and
370+
`reward_functions` is a comma-separated list of function names to import from
371+
it. When both are set, the built-in stack is REPLACED entirely by the
372+
user-provided callables (so users have full control over their reward stack).
373+
Otherwise the default
374+
(`match_format_exactly`, `match_format_approximately`, `check_numbers`) stack
375+
is used. Every reward function is wrapped via `make_reward_fn`.
376+
"""
377+
custom_rewards_path = getattr(trainer_config, "reward_functions_path", "") or ""
378+
custom_rewards_names = getattr(trainer_config, "reward_functions", "") or ""
379+
if custom_rewards_path and custom_rewards_names:
380+
names = [n.strip() for n in custom_rewards_names.split(",") if n.strip()]
381+
reward_fns = [make_reward_fn(utils_rl.load_custom_callable(custom_rewards_path, n)) for n in names]
382+
max_logging.log(f"reward_fns: using {len(reward_fns)} custom reward function(s) {names} from {custom_rewards_path}")
383+
return reward_fns
384+
return [
385+
make_reward_fn(utils_rl.match_format_exactly),
386+
make_reward_fn(utils_rl.match_format_approximately),
387+
make_reward_fn(utils_rl.check_numbers),
388+
]
389+
390+
366391
def create_rl_components(
367392
trainer_config,
368393
sampler_config,
@@ -526,11 +551,11 @@ def _reward_fn(**kwargs):
526551

527552
return _reward_fn
528553

529-
reward_fns = [ # type: ignore
530-
make_reward_fn(utils_rl.match_format_exactly),
531-
make_reward_fn(utils_rl.match_format_approximately),
532-
make_reward_fn(utils_rl.check_numbers),
533-
]
554+
# Optional user-provided reward functions: when `reward_functions_path` and
555+
# `reward_functions` are both set the built-in stack is replaced entirely by
556+
# the user-provided callables. Each function must accept `prompts`,
557+
# `completions`, `tmvp_config`, and `**kwargs` and return a list of floats.
558+
reward_fns = build_reward_fns(trainer_config, make_reward_fn)
534559

535560
# Create RL trainer
536561
max_logging.log("Setting up RL trainer...")

tests/post_training/unit/train_rl_test.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,47 @@ def test_rl_train_invalid_optimizer_memory_host_offload(self, mock_setup):
495495
with self.assertRaisesRegex(ValueError, "optimizer_memory_host_offload=True is not supported"):
496496
train_rl._rl_train_impl([], {}) # pylint: disable=protected-access
497497

498+
@pytest.mark.cpu_only
499+
def test_build_reward_fns_defaults_when_no_custom(self):
500+
"""With neither knob set, the built-in 3-fn stack is returned."""
501+
trainer_config = SimpleNamespace(reward_functions_path="", reward_functions="")
502+
reward_fns = train_rl.build_reward_fns(trainer_config, make_reward_fn=lambda fn: fn)
503+
self.assertEqual(
504+
reward_fns,
505+
[
506+
train_rl.utils_rl.match_format_exactly,
507+
train_rl.utils_rl.match_format_approximately,
508+
train_rl.utils_rl.check_numbers,
509+
],
510+
)
511+
512+
@pytest.mark.cpu_only
513+
def test_build_reward_fns_custom_replaces_builtins(self):
514+
"""When both knobs are set, the stack is the user-provided functions only."""
515+
trainer_config = SimpleNamespace(
516+
reward_functions_path="/tmp/my_rewards.py",
517+
reward_functions="reward_a, reward_b",
518+
)
519+
loaded = {"reward_a": object(), "reward_b": object()}
520+
with mock.patch.object(
521+
train_rl.utils_rl, "load_custom_callable", side_effect=lambda path, name: loaded[name]
522+
) as mock_load:
523+
reward_fns = train_rl.build_reward_fns(trainer_config, make_reward_fn=lambda fn: fn)
524+
self.assertEqual(reward_fns, [loaded["reward_a"], loaded["reward_b"]])
525+
mock_load.assert_has_calls(
526+
[
527+
mock.call("/tmp/my_rewards.py", "reward_a"),
528+
mock.call("/tmp/my_rewards.py", "reward_b"),
529+
]
530+
)
531+
532+
@pytest.mark.cpu_only
533+
def test_build_reward_fns_partial_config_falls_back(self):
534+
"""If only one of the two knobs is set, the built-in stack is used."""
535+
trainer_config = SimpleNamespace(reward_functions_path="/tmp/my_rewards.py", reward_functions="")
536+
reward_fns = train_rl.build_reward_fns(trainer_config, make_reward_fn=lambda fn: fn)
537+
self.assertEqual(len(reward_fns), 3)
538+
498539

499540
if __name__ == "__main__":
500541
unittest.main()

0 commit comments

Comments
 (0)