Spaces:
Sleeping
Sleeping
fix(pm_ops_trainer): capture rewards via rollout_func wrapper, not inputs
Browse filesThe original approach checked inputs[] for the 'reward' key, but TRL
only puts dataset columns into inputs β rollout_func extra keys never
arrive there. Fix: wrap rollout_func in __init__ to capture the reward
list into self._rollout_reward_cache, then inject it in
_calculate_rewards before the standard reward_funcs path runs.
Also handles num_generations > 1 by repeating rewards to match
completions count.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- training/pm_ops_trainer.py +61 -46
training/pm_ops_trainer.py
CHANGED
|
@@ -2,23 +2,20 @@
|
|
| 2 |
|
| 3 |
Problem being solved
|
| 4 |
--------------------
|
| 5 |
-
TRL 1.2.0
|
| 6 |
-
|
| 7 |
-
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
them back via kwargs. TRL silently drops these keys when building
|
| 13 |
-
reward_kwargs from the batch, so reward_func always receives an empty dict
|
| 14 |
-
and returns 0.0 for every episode.
|
| 15 |
|
| 16 |
Fix
|
| 17 |
---
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
|
| 23 |
Usage
|
| 24 |
-----
|
|
@@ -27,24 +24,21 @@ Usage
|
|
| 27 |
trainer = PMOpsGRPOTrainer(
|
| 28 |
model=model,
|
| 29 |
processing_class=tokenizer,
|
| 30 |
-
reward_funcs=reward_func, # kept as
|
| 31 |
train_dataset=dataset,
|
| 32 |
args=grpo_config,
|
| 33 |
-
rollout_func=rollout_func, # must
|
| 34 |
)
|
| 35 |
|
| 36 |
rollout_func contract
|
| 37 |
---------------------
|
| 38 |
rollout_func(prompts, trainer=None) must return a dict containing at minimum:
|
| 39 |
{
|
| 40 |
-
"prompt_ids": list[list[int]],
|
| 41 |
"completion_ids": list[list[int]],
|
| 42 |
"logprobs": list[list[float]],
|
| 43 |
-
"reward": list[float],
|
| 44 |
}
|
| 45 |
-
|
| 46 |
-
The 'reward' value is the weighted combination of all sub-signals and is
|
| 47 |
-
injected directly as the GRPO advantage signal.
|
| 48 |
"""
|
| 49 |
|
| 50 |
import torch
|
|
@@ -52,15 +46,39 @@ from trl import GRPOTrainer
|
|
| 52 |
|
| 53 |
|
| 54 |
class PMOpsGRPOTrainer(GRPOTrainer):
|
| 55 |
-
"""Drop-in GRPOTrainer replacement with
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
Falls back to standard GRPOTrainer behaviour if
|
| 60 |
"""
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
def _calculate_rewards(
|
| 66 |
self,
|
|
@@ -69,38 +87,35 @@ class PMOpsGRPOTrainer(GRPOTrainer):
|
|
| 69 |
completions,
|
| 70 |
completion_ids_list,
|
| 71 |
):
|
| 72 |
-
"""
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
rollout_func output keys land here per example.
|
| 77 |
-
prompts: list[str] β prompt texts (unused in this path)
|
| 78 |
-
completions: list[str] β completion texts (unused)
|
| 79 |
-
completion_ids_list: list[list[int]] β token IDs (unused)
|
| 80 |
-
|
| 81 |
-
Returns:
|
| 82 |
-
Tensor of shape [batch_size, 1] when pre-computed rewards are found.
|
| 83 |
-
Falls back to super()._calculate_rewards(...) otherwise.
|
| 84 |
"""
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
device = self.accelerator.device
|
| 90 |
rewards = torch.tensor(
|
| 91 |
-
[float(
|
| 92 |
dtype=torch.float32,
|
| 93 |
device=device,
|
| 94 |
).unsqueeze(1) # [batch_size, 1]
|
| 95 |
|
| 96 |
-
# Log the mean reward so it shows up in training curves
|
| 97 |
mean_r = rewards.mean().item()
|
| 98 |
self.log({"reward/injected_mean": mean_r})
|
| 99 |
-
|
| 100 |
return rewards
|
| 101 |
|
| 102 |
# Fallback: standard TRL reward_funcs path
|
| 103 |
-
# Triggered when reward key is absent (e.g. non-rollout evaluation)
|
| 104 |
return super()._calculate_rewards(
|
| 105 |
inputs, prompts, completions, completion_ids_list
|
| 106 |
)
|
|
|
|
| 2 |
|
| 3 |
Problem being solved
|
| 4 |
--------------------
|
| 5 |
+
TRL 1.2.0 + Unsloth PatchFastRL strips custom rollout_func output keys
|
| 6 |
+
before calling reward_funcs. The 'reward' key never arrives in kwargs, so
|
| 7 |
+
reward_func always returns 0.0 and GRPO sees constant reward β zero gradient.
|
| 8 |
|
| 9 |
+
Root cause: TRL builds reward_kwargs from the *dataset* batch columns (only
|
| 10 |
+
'prompt'). rollout_func extra keys are not stored in the dataset, so they
|
| 11 |
+
never reach _calculate_rewards via inputs.
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
Fix
|
| 14 |
---
|
| 15 |
+
Wrap rollout_func in __init__ to capture the 'reward' list into an instance
|
| 16 |
+
cache (_rollout_reward_cache) each time rollout_func is called. Then override
|
| 17 |
+
_calculate_rewards to inject those cached rewards directly β no kwargs, no
|
| 18 |
+
inputs lookup needed.
|
| 19 |
|
| 20 |
Usage
|
| 21 |
-----
|
|
|
|
| 24 |
trainer = PMOpsGRPOTrainer(
|
| 25 |
model=model,
|
| 26 |
processing_class=tokenizer,
|
| 27 |
+
reward_funcs=reward_func, # kept as fallback; not called on inject path
|
| 28 |
train_dataset=dataset,
|
| 29 |
args=grpo_config,
|
| 30 |
+
rollout_func=rollout_func, # must return 'reward' key in output dict
|
| 31 |
)
|
| 32 |
|
| 33 |
rollout_func contract
|
| 34 |
---------------------
|
| 35 |
rollout_func(prompts, trainer=None) must return a dict containing at minimum:
|
| 36 |
{
|
| 37 |
+
"prompt_ids": list[list[int]],
|
| 38 |
"completion_ids": list[list[int]],
|
| 39 |
"logprobs": list[list[float]],
|
| 40 |
+
"reward": list[float], # one combined float per episode
|
| 41 |
}
|
|
|
|
|
|
|
|
|
|
| 42 |
"""
|
| 43 |
|
| 44 |
import torch
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
class PMOpsGRPOTrainer(GRPOTrainer):
|
| 49 |
+
"""Drop-in GRPOTrainer replacement with instance-level reward caching.
|
| 50 |
|
| 51 |
+
Wraps rollout_func to capture the 'reward' list, then injects it in
|
| 52 |
+
_calculate_rewards before the standard reward_funcs path runs.
|
| 53 |
+
Falls back to standard GRPOTrainer behaviour if cache is empty.
|
| 54 |
"""
|
| 55 |
|
| 56 |
+
def __init__(self, *args, rollout_func=None, **kwargs):
|
| 57 |
+
# Cache for rewards captured from each rollout_func call
|
| 58 |
+
self._rollout_reward_cache: list[float] = []
|
| 59 |
+
|
| 60 |
+
if rollout_func is not None:
|
| 61 |
+
original_rollout = rollout_func
|
| 62 |
+
# Use a list cell to forward-reference self before super().__init__
|
| 63 |
+
_self_ref: list = []
|
| 64 |
+
|
| 65 |
+
def _capturing_rollout(prompts, trainer=None):
|
| 66 |
+
trainer_obj = trainer or (_self_ref[0] if _self_ref else None)
|
| 67 |
+
result = original_rollout(prompts, trainer=trainer_obj)
|
| 68 |
+
# Capture rewards from this rollout batch
|
| 69 |
+
_self_ref[0]._rollout_reward_cache = list(result.get("reward", []))
|
| 70 |
+
return result
|
| 71 |
+
|
| 72 |
+
rollout_func = _capturing_rollout
|
| 73 |
+
|
| 74 |
+
super().__init__(*args, rollout_func=rollout_func, **kwargs)
|
| 75 |
+
|
| 76 |
+
# Now self is fully initialised β populate the forward ref
|
| 77 |
+
if hasattr(self, "_rollout_reward_cache"):
|
| 78 |
+
try:
|
| 79 |
+
_self_ref.append(self) # type: ignore[name-defined] # noqa: F821
|
| 80 |
+
except NameError:
|
| 81 |
+
pass # rollout_func was None, no closure to fill
|
| 82 |
|
| 83 |
def _calculate_rewards(
|
| 84 |
self,
|
|
|
|
| 87 |
completions,
|
| 88 |
completion_ids_list,
|
| 89 |
):
|
| 90 |
+
"""Inject pre-computed rewards when cache is populated.
|
| 91 |
+
|
| 92 |
+
Falls back to standard TRL path (calls reward_funcs) if cache empty.
|
| 93 |
+
Handles num_generations > 1 by repeating rewards to match completions.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
"""
|
| 95 |
+
cache = self._rollout_reward_cache
|
| 96 |
+
n = len(completions)
|
| 97 |
+
|
| 98 |
+
if cache:
|
| 99 |
+
if len(cache) == n:
|
| 100 |
+
rewards_list = cache
|
| 101 |
+
else:
|
| 102 |
+
# num_generations > 1: TRL may call reward_func with n > len(cache)
|
| 103 |
+
# Repeat rewards in round-robin to fill all completions.
|
| 104 |
+
rewards_list = [cache[i % len(cache)] for i in range(n)]
|
| 105 |
+
|
| 106 |
device = self.accelerator.device
|
| 107 |
rewards = torch.tensor(
|
| 108 |
+
[float(r) for r in rewards_list],
|
| 109 |
dtype=torch.float32,
|
| 110 |
device=device,
|
| 111 |
).unsqueeze(1) # [batch_size, 1]
|
| 112 |
|
|
|
|
| 113 |
mean_r = rewards.mean().item()
|
| 114 |
self.log({"reward/injected_mean": mean_r})
|
| 115 |
+
self._rollout_reward_cache = [] # consume cache
|
| 116 |
return rewards
|
| 117 |
|
| 118 |
# Fallback: standard TRL reward_funcs path
|
|
|
|
| 119 |
return super()._calculate_rewards(
|
| 120 |
inputs, prompts, completions, completion_ids_list
|
| 121 |
)
|