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

fix(pm_ops_trainer): use direct closure capture instead of _self_ref

Browse files

The _self_ref forward-reference trick was the bug β€” _self_ref was populated
after super().__init__() but the cache assignment still used it unnecessarily.
In Python, self is captured directly in nested function closures without any
forward reference, so _capturing_rollout can assign self._rollout_reward_cache
straight away. Added diagnostic prints to confirm capture/inject on each step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. training/pm_ops_trainer.py +24 -32
training/pm_ops_trainer.py CHANGED
@@ -12,10 +12,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
  -----
@@ -27,7 +28,7 @@ Usage
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
@@ -46,40 +47,35 @@ from trl import GRPOTrainer
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,
85
  inputs,
@@ -87,20 +83,15 @@ class PMOpsGRPOTrainer(GRPOTrainer):
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
@@ -110,12 +101,13 @@ class PMOpsGRPOTrainer(GRPOTrainer):
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
  )
 
12
 
13
  Fix
14
  ---
15
+ Wrap rollout_func inside __init__ using a closure that captures self directly
16
+ (standard Python closure semantics β€” no forward reference tricks needed).
17
+ Each time rollout_func is called during training, the wrapper stores the
18
+ 'reward' list into self._rollout_reward_cache. _calculate_rewards then injects
19
+ those cached values as a tensor, bypassing the broken kwargs path entirely.
20
 
21
  Usage
22
  -----
 
28
  reward_funcs=reward_func, # kept as fallback; not called on inject path
29
  train_dataset=dataset,
30
  args=grpo_config,
31
+ rollout_func=rollout_func, # must return 'reward': list[float] in output
32
  )
33
 
34
  rollout_func contract
 
47
 
48
 
49
  class PMOpsGRPOTrainer(GRPOTrainer):
50
+ """Drop-in GRPOTrainer with closure-based reward caching.
51
 
52
+ Wraps rollout_func at construction time to intercept the 'reward' list
53
+ on every call. _calculate_rewards injects these rewards directly as a
54
+ tensor β€” no kwargs, no inputs lookup.
55
+ Falls back to standard GRPOTrainer behaviour when cache is empty.
56
  """
57
 
58
  def __init__(self, *args, rollout_func=None, **kwargs):
59
+ # Reward cache populated by the rollout wrapper, consumed by _calculate_rewards
60
  self._rollout_reward_cache: list[float] = []
61
 
62
  if rollout_func is not None:
63
  original_rollout = rollout_func
 
 
64
 
65
+ # self is captured directly by closure β€” no forward reference needed
66
  def _capturing_rollout(prompts, trainer=None):
67
+ result = original_rollout(prompts, trainer=trainer)
68
+ rewards = list(result.get("reward", []))
69
+ self._rollout_reward_cache = rewards
70
+ print(f"[PMOpsGRPOTrainer] captured {len(rewards)} rewards "
71
+ f"(mean={sum(rewards)/len(rewards):.3f})" if rewards else
72
+ "[PMOpsGRPOTrainer] WARNING: rollout returned no rewards")
73
  return result
74
 
75
  rollout_func = _capturing_rollout
76
 
77
  super().__init__(*args, rollout_func=rollout_func, **kwargs)
78
 
 
 
 
 
 
 
 
79
  def _calculate_rewards(
80
  self,
81
  inputs,
 
83
  completions,
84
  completion_ids_list,
85
  ):
86
+ """Inject cached rewards when available; fall back to reward_funcs otherwise."""
 
 
 
 
87
  cache = self._rollout_reward_cache
88
  n = len(completions)
89
 
90
  if cache:
91
+ # Handle num_generations > 1: TRL may call with n > len(cache)
92
  if len(cache) == n:
93
  rewards_list = cache
94
  else:
 
 
95
  rewards_list = [cache[i % len(cache)] for i in range(n)]
96
 
97
  device = self.accelerator.device
 
101
  device=device,
102
  ).unsqueeze(1) # [batch_size, 1]
103
 
104
+ self.log({"reward/injected_mean": rewards.mean().item()})
 
105
  self._rollout_reward_cache = [] # consume cache
106
+ print(f"[PMOpsGRPOTrainer] injected {n} rewards, mean={rewards.mean().item():.3f}")
107
  return rewards
108
 
109
  # Fallback: standard TRL reward_funcs path
110
+ print("[PMOpsGRPOTrainer] cache empty β€” falling back to reward_funcs")
111
  return super()._calculate_rewards(
112
  inputs, prompts, completions, completion_ids_list
113
  )