A-HK commited on
Commit
60fa6a8
·
verified ·
1 Parent(s): 581cdcc

Add StarPO-S trajectory filtering for GRPO stability\n\nReference: RAGEN (arXiv 2504.20073)\nFilters to keep only informative rollouts, preventing echo trap collapse."

Browse files
Files changed (1) hide show
  1. training/trajectory_filter.py +107 -0
training/trajectory_filter.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """StarPO-S trajectory filtering for GRPO training stability.
2
+
3
+ Implements uncertainty-based filtering from RAGEN (arXiv 2504.20073):
4
+ - Compute per-prompt reward standard deviation across G rollouts
5
+ - Keep only top keep_ratio of prompts by reward variance
6
+ - Prevents echo trap where model collapses into repetitive behavior
7
+ - Includes collapse detection via reward-std monitoring
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from collections import defaultdict
14
+ from typing import Any
15
+
16
+ import numpy as np
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class TrajectoryFilter:
22
+ """StarPO-S style trajectory filtering for GRPO.
23
+
24
+ Keeps top `keep_ratio` of prompts ranked by reward standard deviation.
25
+ Prompts where all rollouts succeed (std=0) or all fail (std=0) are
26
+ uninformative for learning.
27
+ """
28
+
29
+ def __init__(self, keep_ratio: float = 0.5, min_std_threshold: float = 0.01,
30
+ collapse_window: int = 20, collapse_std_threshold: float = 0.02):
31
+ self.keep_ratio = keep_ratio
32
+ self.min_std_threshold = min_std_threshold
33
+ self.collapse_window = collapse_window
34
+ self.collapse_std_threshold = collapse_std_threshold
35
+ self._reward_stds: list[float] = []
36
+ self._filter_ratios: list[float] = []
37
+ self._collapse_detected = False
38
+
39
+ def filter_batch(self, prompt_ids: list[Any], rewards: list[float],
40
+ num_generations: int) -> list[int]:
41
+ """Filter batch, keeping only most informative rollouts."""
42
+ prompt_groups: dict[Any, list[tuple[int, float]]] = defaultdict(list)
43
+ for idx, (pid, reward) in enumerate(zip(prompt_ids, rewards)):
44
+ prompt_groups[pid].append((idx, reward))
45
+
46
+ prompt_stds: list[tuple[Any, float, list[int]]] = []
47
+ for pid, group in prompt_groups.items():
48
+ indices = [g[0] for g in group]
49
+ rews = [g[1] for g in group]
50
+ std = float(np.std(rews)) if len(rews) > 1 else 0.0
51
+ prompt_stds.append((pid, std, indices))
52
+
53
+ prompt_stds.sort(key=lambda x: -x[1])
54
+ n_keep = max(1, int(len(prompt_stds) * self.keep_ratio))
55
+ keep_indices = []
56
+ for _, std, indices in prompt_stds[:n_keep]:
57
+ keep_indices.extend(indices)
58
+
59
+ all_stds = [s for _, s, _ in prompt_stds]
60
+ self._reward_stds.append(float(np.mean(all_stds)) if all_stds else 0.0)
61
+ self._filter_ratios.append(len(keep_indices) / max(1, len(rewards)))
62
+ return sorted(keep_indices)
63
+
64
+ def check_collapse(self) -> dict[str, Any]:
65
+ """Check for training collapse indicators."""
66
+ result = {"collapse_detected": False, "reward_std_trend": "stable", "recommendation": "continue"}
67
+ if len(self._reward_stds) < self.collapse_window:
68
+ return result
69
+ recent = self._reward_stds[-self.collapse_window:]
70
+ mean_std = float(np.mean(recent))
71
+ if mean_std < self.collapse_std_threshold:
72
+ result["collapse_detected"] = True
73
+ result["reward_std_trend"] = "collapsing"
74
+ result["recommendation"] = "Reward std near zero. Increase temperature or reduce keep_ratio."
75
+ self._collapse_detected = True
76
+ result["mean_reward_std"] = mean_std
77
+ return result
78
+
79
+ def get_metrics(self) -> dict[str, float]:
80
+ return {
81
+ "starpo_reward_std": self._reward_stds[-1] if self._reward_stds else 0.0,
82
+ "starpo_reward_std_ma": float(np.mean(self._reward_stds[-10:])) if self._reward_stds else 0.0,
83
+ "starpo_filter_ratio": self._filter_ratios[-1] if self._filter_ratios else 1.0,
84
+ "starpo_collapse_detected": float(self._collapse_detected),
85
+ }
86
+
87
+
88
+ class CollapseDetector:
89
+ """Lightweight collapse detector for use in TrainerCallback."""
90
+
91
+ def __init__(self, window: int = 20):
92
+ self.window = window
93
+ self._rewards: list[float] = []
94
+ self.alerts: list[str] = []
95
+
96
+ def update(self, reward: float | None = None) -> str | None:
97
+ if reward is not None:
98
+ self._rewards.append(reward)
99
+ if len(self._rewards) < self.window:
100
+ return None
101
+ recent = self._rewards[-self.window:]
102
+ reward_std = float(np.std(recent))
103
+ if reward_std < 0.001:
104
+ alert = f"COLLAPSE: reward_std={reward_std:.6f} over {self.window} steps"
105
+ self.alerts.append(alert)
106
+ return alert
107
+ return None