Spaces:
Sleeping
Sleeping
File size: 8,262 Bytes
29d6757 dc36ffd 29d6757 dc36ffd 29d6757 8d3837e 29d6757 dc36ffd 29d6757 8d3837e 29d6757 dc36ffd 29d6757 dc36ffd 29d6757 0ddcea5 29d6757 8d3837e 29d6757 8d3837e 29d6757 dc36ffd 8d3837e dc36ffd 0ddcea5 dc36ffd 0ddcea5 dc36ffd 0ddcea5 8d3837e 0ddcea5 8d3837e dc36ffd 0ddcea5 dc36ffd 0ddcea5 29d6757 8d3837e 962ea9c dc36ffd 0ddcea5 962ea9c 38457df 962ea9c 38457df 962ea9c dc36ffd 38457df dc36ffd 38457df dc36ffd 29d6757 dc36ffd 29d6757 38457df dc36ffd 38457df 29d6757 0ddcea5 29d6757 8d3837e 29d6757 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | """PMOpsGRPOTrainer β GRPOTrainer subclass with direct reward injection.
Problem being solved
--------------------
TRL 1.2.0 + Unsloth PatchFastRL strips custom rollout_func output keys
before calling reward_funcs. The 'reward' key never arrives in kwargs, so
reward_func always returns 0.0 and GRPO sees constant reward β zero gradient.
Root cause: TRL builds reward_kwargs from the *dataset* batch columns (only
'prompt'). rollout_func extra keys are not stored in the dataset, so they
never reach _calculate_rewards via inputs.
Fix
---
Wrap rollout_func inside __init__ using a closure that captures self directly
(standard Python closure semantics β no forward reference tricks needed).
Each time rollout_func is called during training, the wrapper stores the
'reward' list into self._rollout_reward_cache. _calculate_rewards then injects
those cached values as a tensor, bypassing the broken kwargs path entirely.
Usage
-----
from training.pm_ops_trainer import PMOpsGRPOTrainer
trainer = PMOpsGRPOTrainer(
model=model,
processing_class=tokenizer,
reward_funcs=reward_func, # kept as fallback; not called on inject path
train_dataset=dataset,
args=grpo_config,
rollout_func=rollout_func, # must return 'reward': list[float] in output
)
rollout_func contract
---------------------
rollout_func(prompts, trainer=None) must return a dict containing at minimum:
{
"prompt_ids": list[list[int]],
"completion_ids": list[list[int]],
"logprobs": list[list[float]],
"reward": list[float], # one combined float per episode
}
"""
import torch
from trl.trainer.grpo_trainer import GRPOTrainer
class PMOpsGRPOTrainer(GRPOTrainer):
"""Drop-in GRPOTrainer with closure-based reward caching.
Wraps rollout_func at construction time to intercept the 'reward' list
on every call. _calculate_rewards injects these rewards directly as a
tensor β no kwargs, no inputs lookup.
Falls back to standard GRPOTrainer behaviour when cache is empty.
"""
def __init__(self, *args, rollout_func=None, **kwargs):
# Reward cache populated by the rollout wrapper, consumed by _calculate_rewards
self._rollout_reward_cache: list[float] = []
self._rollout_capture_calls = 0
self._warned_rollout_bypass = False
wrapped_rollout_func = None
if rollout_func is not None:
original_rollout = rollout_func
# self is captured directly by closure; accept flexible call signatures
# because cloud runtimes may invoke rollout_func with positional/keyword variations.
def _capturing_rollout(prompts, trainer=None, *rollout_args, **rollout_kwargs):
rollout_kwargs.setdefault("trainer", trainer)
result = original_rollout(prompts, *rollout_args, **rollout_kwargs)
raw_rewards = result.get("reward", result.get("rewards", []))
if raw_rewards is None:
rewards = []
elif isinstance(raw_rewards, torch.Tensor):
rewards = [float(r) for r in raw_rewards.detach().cpu().flatten().tolist()]
elif isinstance(raw_rewards, (int, float)):
rewards = [float(raw_rewards)]
else:
rewards = [float(r) for r in list(raw_rewards)]
self._rollout_reward_cache = rewards
self._rollout_capture_calls += 1
print(f"[PMOpsGRPOTrainer] captured {len(rewards)} rewards "
f"(mean={sum(rewards)/len(rewards):.3f})" if rewards else
"[PMOpsGRPOTrainer] WARNING: rollout returned no rewards")
return result
wrapped_rollout_func = _capturing_rollout
rollout_func = wrapped_rollout_func
super().__init__(*args, rollout_func=rollout_func, **kwargs)
# Keep wrapper bound explicitly in case an upstream patch reassigns rollout_func.
if wrapped_rollout_func is not None:
self.rollout_func = wrapped_rollout_func
def _calculate_rewards(
self,
inputs,
prompts,
completions,
completion_ids_list,
):
"""Inject cached rewards when available; fall back to reward_funcs otherwise."""
def _coerce_rewards(raw):
if raw is None:
return []
if isinstance(raw, torch.Tensor):
return [float(r) for r in raw.detach().cpu().flatten().tolist()]
if isinstance(raw, (int, float)):
return [float(raw)]
return [float(r) for r in list(raw)]
cache = self._rollout_reward_cache
n = len(completions)
# Some TRL variants forward rollout extra_fields into `inputs` directly.
if not cache:
input_rewards: list[float] = []
for row in inputs:
if isinstance(row, dict) and "reward" in row:
input_rewards.append(float(row["reward"]))
else:
input_rewards = []
break
if input_rewards:
cache = input_rewards
# Cloud fallback: some patched runtimes skip the normal rollout capture path
# before calling _calculate_rewards. Actively invoke rollout_func here.
# Pass ALL n prompts (including repeated ones for num_generations > 1) so
# the rollout_func can generate distinct rewards per generation β NOT tile-mod.
if not cache and self.rollout_func is not None and prompts:
try:
print(f"[PMOpsGRPOTrainer] cache empty β probing rollout_func for {n} rewards")
out = self.rollout_func(list(prompts), trainer=self)
cache = self._rollout_reward_cache
if not cache and isinstance(out, dict):
cache = _coerce_rewards(out.get("reward", out.get("rewards", [])))
self._rollout_reward_cache = cache
except Exception as exc:
print(f"[PMOpsGRPOTrainer] rollout probe failed: {exc!r}")
if cache:
if len(cache) == n:
rewards_list = cache
elif len(cache) > n:
rewards_list = cache[:n]
else:
# Still short β extend with mean rather than tile-mod so we don't
# duplicate rewards for the same prompt (tile-mod β zero advantage).
mean_r = sum(cache) / len(cache)
rewards_list = list(cache) + [mean_r] * (n - len(cache))
print(f"[PMOpsGRPOTrainer] WARNING: cache has {len(cache)} rewards for n={n}; "
f"padding with mean={mean_r:.3f}. Consider matching num_generations.")
device = self.accelerator.device
rewards = torch.tensor(
[float(r) for r in rewards_list],
dtype=torch.float32,
device=device,
).unsqueeze(1) # [batch_size, 1]
std = rewards.std().item() if n > 1 else 0.0
self.log({"reward/injected_mean": rewards.mean().item(),
"reward/injected_std": std})
self._rollout_reward_cache = [] # consume cache
print(f"[PMOpsGRPOTrainer] injected {n} rewards "
f"mean={rewards.mean().item():.3f} std={std:.3f}")
return rewards
if self.rollout_func is not None and self._rollout_capture_calls == 0 and not self._warned_rollout_bypass:
print(
"[PMOpsGRPOTrainer] WARNING: rollout_func was never called before reward calculation. "
"This cloud runtime is likely bypassing rollout_func (TRL/Unsloth mismatch), so "
"reward_funcs only receive prompts/completion_ids/trainer_state and no 'reward' key."
)
self._warned_rollout_bypass = True
# Fallback: standard TRL reward_funcs path
print("[PMOpsGRPOTrainer] cache empty β falling back to reward_funcs")
return super()._calculate_rewards(
inputs, prompts, completions, completion_ids_list
)
|