Skip to content

Commit 9f4e8d2

Browse files
halleriteclaude
andcommitted
refactor(algo): remove group_relative; stop SFT assigning a throwaway advantage
group_relative was a ClassVar on every advantage config whose only consumer was AlgorithmConfig.requires_group_advantage -> warn_group_size, a config-time warning for group-relative algorithms run with group_size=1. SFT also overrode score_group to assign group-normalized advantages it never trains on (the loss is CE), purely so the zero-advantage filter would drop uniform-reward groups -- overloading 'advantage' as a filter key. Remove the group_relative ClassVars, the requires_group_advantage property, warn_group_size and its orchestrator call site, and the now-unused warnings import. SFT drops its score_group override (inherits the base no-op) and ships no advantage: the zero-advantage filter skips None, and SFT is ce-only so the trainer never requires one. SFT now trains on every sampled token; reward-based filtering is opt-in via an explicit filter. The group_size=1 footgun is already caught by the runtime empty-batch guard (orchestrator warns on zero-trainable batches and aborts after repeated ones), which covers all causes, not just this prediction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fe8a527 commit 9f4e8d2

4 files changed

Lines changed: 9 additions & 45 deletions

File tree

docs/algorithms.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ kwargs = { patterns = ["WARNING"] }
111111
def drop_warnings(rollout, *, patterns: list[str]) -> list[list[bool]]: ...
112112
```
113113

114-
Component compatibility is validated at config time: frozen-model sampling can only feed the `ce` loss component — the `rl` and `ref_kl` components need the live policy's own sampling logprobs for importance ratios — `opd` pointed at `"policy"` is rejected as degenerate (zero KL), `sft` without a frozen source is rejected (CE on the policy's own tokens is not a distillation target), and group-relative advantage with `group_size = 1` warns that every advantage collapses to zero.
114+
Component compatibility is validated at config time: frozen-model sampling can only feed the `ce` loss component — the `rl` and `ref_kl` components need the live policy's own sampling logprobs for importance ratios — `opd` pointed at `"policy"` is rejected as degenerate (zero KL), `sft` without a frozen source is rejected (CE on the policy's own tokens is not a distillation target). A group-relative algorithm with `group_size = 1` produces all-zero advantages; the resulting empty batch is caught at runtime (the orchestrator warns and aborts after repeated zero-trainable batches), not at config time.
115115

116116
### Per-Env Algorithms
117117

@@ -283,7 +283,7 @@ The advantage strategy is the `advantage` component of the [algorithm](#the-algo
283283
| `reward` | `rl` | Advantage = raw reward, no baseline. |
284284
| `opd` | `ref_kl` | On-policy distillation: per-token reverse KL to a reference model (`model`, an inline frozen hosted model), evaluated in the trainer from shipped reference logprobs. No credit — rollouts keep `advantages = None` (advantage-based filters never fire) and ship no advantage stream; `group_size` only fans out sampling. |
285285
| `opsd` | `ref_kl` | SDFT: per-token reverse KL to a demo-conditioned reference. No credit — rollouts keep `advantages = None` (advantage-based filters never fire) and ship no advantage stream. |
286-
| `sft` | `ce` | Cross-entropy on the sampled tokens. The loss ignores advantages, but group-relative credit is still assigned so reward-based filtering keeps working. |
286+
| `sft` | `ce` | Cross-entropy on the sampled tokens. Assigns no advantage — trains on every sampled token. |
287287
| `custom` | `rl` | Your function (below); per-token advantages per rollout. |
288288

289289
### Default Advantage

packages/prime-rl-configs/src/prime_rl/configs/algorithm.py

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ class defaults are the vetted setting — ``type = "opd"`` with nothing else IS
2525
weights ship on the wire and the trainer just executes them.
2626
"""
2727

28-
import warnings
2928
from typing import Annotated, Any, ClassVar, Literal, TypeAlias
3029

3130
from pydantic import AliasChoices, Field, model_validator
@@ -109,7 +108,6 @@ class GRPOAdvantageConfig(BaseConfig):
109108
consumed by the ``rl`` loss component on the rollout's action tokens."""
110109

111110
action_loss_type: ClassVar[ActionLossType] = "rl"
112-
group_relative: ClassVar[bool] = True
113111

114112
length_penalty: LengthPenaltyConfig | None = None
115113
"""Correctness-gated length penalty. ``tokens`` shapes by weighted token cost; ``turns`` shapes by trajectory turn count; None disables shaping. In mixed groups, lower-cost correct rollouts get amplified advantage (up to 2x), higher-cost correct rollouts are unchanged, incorrect untouched. In all-correct groups, below-average-cost rollouts get advantage in [0, 1], others get 0."""
@@ -187,7 +185,6 @@ class MaxRLAdvantageConfig(BaseConfig):
187185
zero-advantage filter drops it, matching the paper's K=0 convention)."""
188186

189187
action_loss_type: ClassVar[ActionLossType] = "rl"
190-
group_relative: ClassVar[bool] = True
191188

192189

193190
class RewardAdvantageConfig(BaseConfig):
@@ -196,7 +193,6 @@ class RewardAdvantageConfig(BaseConfig):
196193
``rl`` loss component."""
197194

198195
action_loss_type: ClassVar[ActionLossType] = "rl"
199-
group_relative: ClassVar[bool] = False
200196

201197

202198
class OPDAdvantageConfig(BaseConfig):
@@ -210,7 +206,6 @@ class OPDAdvantageConfig(BaseConfig):
210206
only fans out sampling."""
211207

212208
action_loss_type: ClassVar[ActionLossType] = "ref_kl"
213-
group_relative: ClassVar[bool] = False
214209
model_role: ClassVar[str] = "teacher"
215210

216211
model: ModelReference | None = None
@@ -235,7 +230,6 @@ class OPSDAdvantageConfig(BaseConfig):
235230
samples ship a neutral 0.0."""
236231

237232
action_loss_type: ClassVar[ActionLossType] = "ref_kl"
238-
group_relative: ClassVar[bool] = False
239233
model_role: ClassVar[str] = "teacher"
240234

241235
model: ModelReference = "policy"
@@ -263,13 +257,12 @@ class OPSDAdvantageConfig(BaseConfig):
263257

264258
class SFTAdvantageConfig(BaseConfig):
265259
type: Literal["sft"] = "sft"
266-
"""SFT distillation: cross-entropy on the sampled tokens. The ``ce``
267-
loss component ignores scalar advantages, but group-relative scalars are still
268-
assigned so reward-based filtering keeps working (the zero-advantage
269-
filter drops uniform-reward groups)."""
260+
"""SFT distillation: cross-entropy on the sampled tokens. The ``ce`` loss
261+
ignores advantages and SFT assigns none — it trains on every sampled token.
262+
Reward-based filtering, if wanted, is an explicit filter, not smuggled
263+
through an unused advantage stream."""
270264

271265
action_loss_type: ClassVar[ActionLossType] = "ce"
272-
group_relative: ClassVar[bool] = True
273266
source_role: ClassVar[str] = "teacher"
274267
"""The sampling source is this algorithm's teacher — the frozen model
275268
whose tokens the policy trains on. Required: CE on the policy's own
@@ -283,7 +276,6 @@ class CustomAdvantageConfig(BaseConfig):
283276
each rollout's completion tokens."""
284277

285278
action_loss_type: ClassVar[ActionLossType] = "rl"
286-
group_relative: ClassVar[bool] = False
287279

288280
import_path: str
289281
"""Import path to the advantage function (e.g. ``my_module.my_advantage``)."""
@@ -345,12 +337,6 @@ class AlgorithmConfig(BaseConfig):
345337
"""The per-token training signal: credit assignment and loss routing,
346338
fused. The ``type`` selects the algorithm."""
347339

348-
@property
349-
def requires_group_advantage(self) -> bool:
350-
"""True when the advantage strategy assigns group-relative scalars,
351-
i.e. degenerate with ``group_size=1``."""
352-
return self.advantage.group_relative
353-
354340
@model_validator(mode="after")
355341
def fold_model(self):
356342
"""Fold the ``model`` shorthand into the component references.
@@ -414,15 +400,3 @@ def validate_component_compatibility(self):
414400
"Use the 'sft' advantage to distill frozen-model tokens."
415401
)
416402
return self
417-
418-
def warn_group_size(self, group_size: int, env_name: str) -> None:
419-
"""Group-relative scoring with a single rollout per example collapses
420-
every advantage to zero. Warn loudly — this is the classic footgun."""
421-
if self.requires_group_advantage and group_size == 1:
422-
warnings.warn(
423-
f"Env '{env_name}' uses group-relative advantage ('{self.advantage.type}') with "
424-
"group_size=1 — every advantage is 0 and (with the default zero-advantage filter) "
425-
"no rollout will train. Set group_size >= 2 or a non-group-relative advantage "
426-
"(e.g. advantage.type='reward').",
427-
stacklevel=2,
428-
)

packages/prime-rl-configs/src/prime_rl/configs/orchestrator.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -869,8 +869,6 @@ def resolve_batching(self):
869869
for env_cfg in self.train.env:
870870
if "group_size" not in env_cfg.model_fields_set:
871871
env_cfg.group_size = self.group_size
872-
assert env_cfg.algo is not None # materialized by inherit_env_algorithms
873-
env_cfg.algo.warn_group_size(env_cfg.group_size, env_cfg.resolved_name)
874872

875873
# Resolve train env num_workers from max_inflight_rollouts
876874
for env_cfg in self.train.env:
Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING
4-
5-
from prime_rl.orchestrator.algo.advantage import assign_group_norm
63
from prime_rl.orchestrator.algo.base import Algorithm
74

8-
if TYPE_CHECKING:
9-
from prime_rl.orchestrator.types import RolloutView
10-
115

126
class SFTDistillAlgorithm(Algorithm):
137
"""Hard distillation. Needs a teacher: the frozen model that generates the
148
rollouts (``sampling.source``); the policy trains with CE on its tokens.
159
16-
The ``ce`` loss ignores credit, but group-relative advantages are still
17-
assigned so reward-based filtering keeps working."""
10+
Assigns no advantage — the ``ce`` loss ignores credit, and SFT trains on
11+
every sampled token. Reward-based filtering, if wanted, is an explicit
12+
filter, not smuggled through an unused advantage stream."""
1813

1914
action_loss_type = "ce"
20-
21-
async def score_group(self, group: list[RolloutView]) -> None:
22-
assign_group_norm(group, None)

0 commit comments

Comments
 (0)