Skip to content

Commit 8fa0d0a

Browse files
Denis Kochetkovjlamypoirierclaude
authored
grpo: add policy-gradient metrics behind metrics enum (#494)
Co-authored-by: Joel Lamy-Poirier <joel.lamy-poirier@servicenow.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3b8a14f commit 8fa0d0a

3 files changed

Lines changed: 337 additions & 4 deletions

File tree

‎fast_llm/layers/language_model/loss/config.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import enum
12
import typing
23
import warnings
34

@@ -193,6 +194,12 @@ def loss_class(self) -> "type[LanguageModelZLoss]":
193194
return LanguageModelZLoss
194195

195196

197+
class GRPOMetricsLevel(enum.StrEnum):
198+
none = "none"
199+
basic = "basic"
200+
with_entropy = "with_entropy"
201+
202+
196203
@config_class(dynamic_type={LanguageModelLossConfig: "grpo"})
197204
class LanguageModelGRPOLossConfig(LanguageModelLossConfig):
198205

@@ -205,6 +212,16 @@ class LanguageModelGRPOLossConfig(LanguageModelLossConfig):
205212
desc="Enable triton implementation. Default: use if available.",
206213
hint=FieldHint.expert,
207214
)
215+
metrics: GRPOMetricsLevel = Field(
216+
default=GRPOMetricsLevel.none,
217+
desc=(
218+
"Additional GRPO metrics to log. "
219+
"`basic`: per-token ratio, KL, and advantage statistics. "
220+
"`with_entropy`: also log per-token entropy. "
221+
"Not supported with pipeline_parallel > 1."
222+
),
223+
hint=FieldHint.feature,
224+
)
208225

209226
@property
210227
def loss_class(self) -> "type[LanguageModelGRPOLoss]":

‎fast_llm/layers/language_model/loss/grpo.py‎

Lines changed: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,69 @@
33

44
import torch
55

6-
from fast_llm.engine.base_model.config import LossDef
6+
from fast_llm.core.distributed import ReduceOp, all_reduce
7+
from fast_llm.engine.base_model.config import LossDef, ReductionType
8+
from fast_llm.engine.distributed.config import DistributedConfig
79
from fast_llm.functional.config import TritonConfig
810
from fast_llm.functional.entropy_loss import fused_predicted_logits_from_labels, fused_softmax_base
911
from fast_llm.functional.utils import reduce_losses
1012
from fast_llm.layers.language_model.config import LanguageModelKwargs
11-
from fast_llm.layers.language_model.loss.config import LanguageModelGRPOLossConfig, LanguageModelLossKwargs
13+
from fast_llm.layers.language_model.loss.config import (
14+
GRPOMetricsLevel,
15+
LanguageModelGRPOLossConfig,
16+
LanguageModelLossKwargs,
17+
)
1218
from fast_llm.layers.language_model.loss.loss import LanguageModelLoss
19+
from fast_llm.utils import Assert
20+
21+
22+
class GRPOMetrics(typing.NamedTuple):
23+
old_logprobs: torch.Tensor
24+
ratio_new_old: torch.Tensor
25+
ratio_new_old_sum: torch.Tensor
26+
ratio_new_old_squared_sum: torch.Tensor
27+
kl_new_old: torch.Tensor
28+
clipped_ratio_fraction: torch.Tensor
29+
advantage: torch.Tensor
30+
max_advantage: torch.Tensor
31+
min_advantage: torch.Tensor
32+
num_tokens: torch.Tensor
33+
entropy: torch.Tensor | None
1334

1435

1536
class LanguageModelGRPOLoss[ConfigType: LanguageModelGRPOLossConfig](LanguageModelLoss[ConfigType]):
37+
def __init__(
38+
self,
39+
config: ConfigType,
40+
distributed_config: DistributedConfig,
41+
*,
42+
name: str,
43+
prediction_distance: int = 1,
44+
prediction_heads: int = 1,
45+
vocab_parallel: bool = False,
46+
num_splits: int = 1,
47+
logits_scale_factor: float = 1.0,
48+
weight: float = 1.0,
49+
register_loss: bool = False,
50+
):
51+
super().__init__(
52+
config,
53+
distributed_config,
54+
name=name,
55+
prediction_distance=prediction_distance,
56+
prediction_heads=prediction_heads,
57+
vocab_parallel=vocab_parallel,
58+
num_splits=num_splits,
59+
logits_scale_factor=logits_scale_factor,
60+
weight=weight,
61+
register_loss=register_loss,
62+
)
63+
Assert.custom(
64+
lambda metrics, pipeline_parallel: metrics == GRPOMetricsLevel.none or pipeline_parallel == 1,
65+
config.metrics,
66+
distributed_config.pipeline_parallel,
67+
)
68+
1669
def _forward_backward(
1770
self,
1871
logits: "torch.Tensor",
@@ -51,10 +104,88 @@ def _forward_backward(
51104
self._register_loss(
52105
self._logprob_metric_name, new_logprobs_mean, losses, reduce_op=torch.distributed.ReduceOp.SUM
53106
)
107+
108+
# Skip the extra softmax pass when there is nothing to register.
109+
if losses is not None and self._config.metrics != GRPOMetricsLevel.none:
110+
self._register_extra_metrics(logits, kwargs, losses, split_index)
111+
54112
return loss, grad
55113

114+
def _register_extra_metrics(
115+
self,
116+
logits: torch.Tensor,
117+
kwargs: dict[str, typing.Any],
118+
losses: dict | None,
119+
split_index: int,
120+
) -> None:
121+
metrics = compute_grpo_metrics(
122+
logits,
123+
self._get_labels(kwargs, split_index),
124+
self._prepare_target(kwargs[LanguageModelLossKwargs.advantages], split_index),
125+
self._prepare_target(kwargs[LanguageModelLossKwargs.old_log_probabilities], split_index),
126+
self._prepare_target(kwargs[LanguageModelLossKwargs.label_counts], split_index),
127+
self._config.epsilon_low,
128+
self._config.epsilon_high,
129+
self._logits_scale_factor,
130+
group=self._parallel_dim.group if self._vocab_parallel else None,
131+
compute_entropy=self._config.metrics == GRPOMetricsLevel.with_entropy,
132+
)
133+
134+
num_documents = kwargs[LanguageModelKwargs.num_documents_in_batch]
135+
136+
for attr in (
137+
"old_logprobs",
138+
"ratio_new_old",
139+
"kl_new_old",
140+
"clipped_ratio_fraction",
141+
"advantage",
142+
):
143+
self._register_loss(f"{self._name}_{attr}", getattr(metrics, attr) / num_documents, losses)
144+
145+
for attr in (
146+
"ratio_new_old_sum",
147+
"ratio_new_old_squared_sum",
148+
"num_tokens",
149+
):
150+
self._register_loss(f"{self._name}_{attr}", getattr(metrics, attr), losses)
151+
152+
self._register_loss(
153+
f"{self._name}_max_advantage",
154+
metrics.max_advantage,
155+
losses,
156+
reduce_op=torch.distributed.ReduceOp.MAX,
157+
)
158+
self._register_loss(
159+
f"{self._name}_min_advantage",
160+
metrics.min_advantage,
161+
losses,
162+
reduce_op=torch.distributed.ReduceOp.MIN,
163+
)
164+
165+
if metrics.entropy is not None:
166+
self._register_loss(f"{self._name}_entropy", metrics.entropy / num_documents, losses)
167+
56168
def get_loss_definitions(self) -> list[LossDef]:
57-
return super().get_loss_definitions() + [LossDef(self._logprob_metric_name)]
169+
defs = super().get_loss_definitions()
170+
defs.append(LossDef(self._logprob_metric_name))
171+
if self._config.metrics != GRPOMetricsLevel.none:
172+
defs.extend(
173+
[
174+
LossDef(f"{self._name}_old_logprobs"),
175+
LossDef(f"{self._name}_ratio_new_old"),
176+
LossDef(f"{self._name}_ratio_new_old_sum"),
177+
LossDef(f"{self._name}_ratio_new_old_squared_sum"),
178+
LossDef(f"{self._name}_kl_new_old"),
179+
LossDef(f"{self._name}_clipped_ratio_fraction"),
180+
LossDef(f"{self._name}_advantage"),
181+
LossDef(f"{self._name}_max_advantage", reduction=ReductionType.maximum),
182+
LossDef(f"{self._name}_min_advantage", reduction=ReductionType.minimum),
183+
LossDef(f"{self._name}_num_tokens"),
184+
]
185+
)
186+
if self._config.metrics == GRPOMetricsLevel.with_entropy:
187+
defs.append(LossDef(f"{self._name}_entropy"))
188+
return defs
58189

59190
def get_preprocessing_config(
60191
self,
@@ -66,6 +197,61 @@ def _logprob_metric_name(self) -> str:
66197
return f"{self._name}_new_logprobs"
67198

68199

200+
@torch.compile
201+
def compute_grpo_metrics(
202+
logits: torch.Tensor, # (*batch, vocab_local)
203+
target: torch.Tensor, # (*batch,)
204+
advantages: torch.Tensor, # (*batch,)
205+
old_log_probabilities: torch.Tensor, # (*batch,)
206+
label_counts: torch.Tensor, # (*batch,) — global per-sequence count broadcast per token
207+
epsilon_low: float = 0.2,
208+
epsilon_high: float = 0.2,
209+
logits_scale_factor: float = 1.0,
210+
group: torch.distributed.ProcessGroup | None = None,
211+
compute_entropy: bool = False,
212+
) -> GRPOMetrics:
213+
loss_mask = target >= 0
214+
mask = loss_mask.float()
215+
masked = mask / label_counts.float().clamp(min=1)
216+
217+
logits_norm, exp_logits, sum_exp_logits, _ = fused_softmax_base(logits, logits_scale_factor, group)
218+
predicted_logits, _, _ = fused_predicted_logits_from_labels(logits_norm, target, loss_mask, group)
219+
new_log_probs = predicted_logits - sum_exp_logits.log()
220+
221+
log_ratio = new_log_probs - old_log_probabilities
222+
ratio = log_ratio.exp()
223+
clipped = (ratio < 1.0 - epsilon_low) | (ratio > 1.0 + epsilon_high)
224+
kl = ratio - log_ratio - 1.0
225+
226+
neg_inf = advantages.new_full((), float("-inf"))
227+
pos_inf = advantages.new_full((), float("inf"))
228+
229+
entropy: torch.Tensor | None = None
230+
if compute_entropy:
231+
# exp_logits and logits_norm are local vocab slices — sum over the local slice, then all-reduce
232+
# across the tensor-parallel group to recover the global E_p[logit_norm] before dividing by the
233+
# already-global sum_exp_logits.
234+
weighted_logits_sum = (exp_logits * logits_norm).sum(-1)
235+
if group is not None:
236+
all_reduce(weighted_logits_sum, op=ReduceOp.SUM, group=group)
237+
entropy_per_token = sum_exp_logits.log() - weighted_logits_sum / sum_exp_logits
238+
entropy = (entropy_per_token * masked).sum()
239+
240+
return GRPOMetrics(
241+
old_logprobs=(old_log_probabilities * masked).sum(),
242+
ratio_new_old=(ratio * masked).sum(),
243+
ratio_new_old_sum=(ratio * mask).sum(),
244+
ratio_new_old_squared_sum=(ratio * ratio * mask).sum(),
245+
kl_new_old=(kl * masked).sum(),
246+
clipped_ratio_fraction=(clipped.float() * masked).sum(),
247+
advantage=(advantages * masked).sum(),
248+
max_advantage=torch.where(loss_mask, advantages, neg_inf).max(),
249+
min_advantage=torch.where(loss_mask, advantages, pos_inf).min(),
250+
num_tokens=mask.sum(),
251+
entropy=entropy,
252+
)
253+
254+
69255
@torch.compile
70256
def fused_grpo_loss_forward_backward(
71257
logits: torch.Tensor, # (*batch, vocab)

0 commit comments

Comments
 (0)