SavK1 Claude Sonnet 4.6 commited on
Commit
dc36ffd
Β·
1 Parent(s): 22d40fb

fix(pm_ops_trainer): capture rewards via rollout_func wrapper, not inputs

Browse files

The 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>

Files changed (1) hide show
  1. 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's GRPOTrainer._calculate_rewards() calls reward_funcs with
6
- **reward_kwargs built from per-example keys in the input batch. This works
7
- correctly when reward_funcs produce rewards from text completions alone.
8
 
9
- It breaks for multi-turn env rollouts: our rollout_func computes rewards
10
- inside the episode loop (where the env state is available), stores them as
11
- a 'reward' key in the rollout output dict, and expects reward_func to read
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
- Override _calculate_rewards to check for a pre-computed 'reward' key in the
19
- input batch. If present, return it directly as a [batch_size, 1] tensor β€”
20
- no reward_funcs called, no kwargs needed. If absent, fall back to the
21
- standard TRL behaviour so the class works as a drop-in replacement.
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-is; only used as fallback
31
  train_dataset=dataset,
32
  args=grpo_config,
33
- rollout_func=rollout_func, # must put 'reward' key in output dict
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]], # one per episode
41
  "completion_ids": list[list[int]],
42
  "logprobs": list[list[float]],
43
- "reward": list[float], # one combined float per episode
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 direct reward injection.
56
 
57
- Overrides _calculate_rewards to read pre-computed rewards from the
58
- rollout batch instead of calling reward_funcs with broken **kwargs.
59
- Falls back to standard GRPOTrainer behaviour if 'reward' key is absent.
60
  """
61
 
62
- # Key written by rollout_func and read by _calculate_rewards
63
- ROLLOUT_REWARD_KEY = "reward"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  def _calculate_rewards(
66
  self,
@@ -69,38 +87,35 @@ class PMOpsGRPOTrainer(GRPOTrainer):
69
  completions,
70
  completion_ids_list,
71
  ):
72
- """Override: inject pre-computed rewards when available.
73
-
74
- Args:
75
- inputs: list[dict] β€” one dict per example in the batch.
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
- # Fast-path: pre-computed reward key present in every example
86
- if inputs and all(
87
- self.ROLLOUT_REWARD_KEY in ex for ex in inputs
88
- ):
 
 
 
 
 
 
 
89
  device = self.accelerator.device
90
  rewards = torch.tensor(
91
- [float(ex[self.ROLLOUT_REWARD_KEY]) for ex in inputs],
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
  )