File size: 8,671 Bytes
8c9ba62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
"""KL penalty and loss.

Ref:
https://github.com/volcengine/verl/blob/main/verl/trainer/ppo/core_algos.py
https://github.com/volcengine/verl/blob/main/verl/trainer/ppo/ray_trainer.py
https://github.com/OpenRLHF/OpenRLHF/blob/main/openrlhf/models/utils.py
"""

from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, Tuple

import torch

from trinity.algorithm.utils import aggregate_loss, masked_mean


class KLFn(ABC):
    """
    KL penalty and loss.
    """

    def __init__(
        self,
        adaptive: bool = False,
        kl_coef: float = 0.001,
        target_kl: Optional[float] = None,
        horizon: Optional[float] = None,
    ) -> None:
        self.kl_coef = kl_coef
        self.adaptive = adaptive
        self.target_kl = target_kl
        self.horizon = horizon
        if adaptive and (target_kl is None or horizon is None):
            raise ValueError("Target KL and horizon must be provided for adaptive KL.")

    def update_kl_coef(self, current_kl: float, batch_size: int) -> None:
        """Update kl coefficient."""
        if self.adaptive:
            target_kl = self.target_kl
            proportional_error = torch.clip(current_kl / target_kl - 1, -0.2, 0.2).item()  # type: ignore
            multiplier = 1 + proportional_error * batch_size / self.horizon
            self.kl_coef *= multiplier

    def apply_kl_penalty_to_reward(self, experiences: Any) -> Tuple[Any, Dict]:
        """Apply KL penalty to reward. Only support DataProto input for now."""
        responses = experiences.batch["responses"]
        response_length = responses.size(1)
        token_level_scores = experiences.batch["token_level_scores"]
        batch_size = experiences.batch.batch_size[0]
        attention_mask = experiences.batch["attention_mask"]
        response_mask = experiences.batch["response_mask"]
        assert response_mask.shape == attention_mask[:, -response_length:].shape
        logprob = experiences.batch["old_log_probs"]
        ref_logprob = experiences.batch["ref_log_prob"]

        if "ref_log_prob" in experiences.batch.keys():
            kl = self.calculate_kl(logprob, ref_logprob)
            kl = kl * response_mask
            kl_coef = self.kl_coef
            experiences.batch["token_level_rewards"] = token_level_scores - kl_coef * kl
        else:
            kl_coef = 0.0
            kl = torch.zeros_like(response_mask, dtype=torch.float32)
            experiences.batch["token_level_rewards"] = token_level_scores

        current_kl = masked_mean(kl, mask=response_mask, axis=-1).mean(dim=0).item()
        self.update_kl_coef(current_kl=current_kl, batch_size=batch_size)

        metrics = {
            "kl": current_kl,
            "kl_coef": kl_coef,
        }

        return experiences, metrics

    def calculate_kl_loss(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        response_mask: torch.Tensor,
        loss_agg_mode: str,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> Tuple[torch.Tensor, Dict]:
        """Compute KL loss.

        Args:
            logprob: Log probabilities from current policy
            ref_logprob: Log probabilities from reference policy
            response_mask: Mask for valid response tokens
            loss_agg_mode: Loss aggregation mode
            old_logprob: Log probabilities from old policy (for importance sampling)
        """
        kl = self.calculate_kl(logprob, ref_logprob, old_logprob)
        kl_loss = aggregate_loss(kl, response_mask, loss_agg_mode=loss_agg_mode)
        metrics = {
            "kl_loss": kl_loss.detach().item(),
            "kl_coef": self.kl_coef,
        }
        return kl_loss * self.kl_coef, metrics

    @abstractmethod
    def calculate_kl(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        """Compute KL divergence between logprob and ref_logprob.

        Args:
            logprob: Log probabilities from current policy
            ref_logprob: Log probabilities from reference policy
            old_logprob: Log probabilities from old policy (for importance sampling)
        """

    @classmethod
    def default_args(cls):
        """Get the default initialization arguments."""
        return {"adaptive": False, "kl_coef": 0.001}


class DummyKLFn(KLFn):
    """
    Dummy KL function.
    """

    def calculate_kl(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        return torch.zeros_like(logprob)

    def apply_kl_penalty_to_reward(self, experiences: Any) -> Tuple[Any, Dict]:
        experiences.batch["token_level_rewards"] = experiences.batch["token_level_scores"]
        return experiences, {}

    def calculate_kl_loss(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        response_mask: torch.Tensor,
        loss_agg_mode: str,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> Tuple[torch.Tensor, Dict]:
        # return a zero tensor
        return torch.tensor(0.0), {}


class K1Fn(KLFn):
    """
    KL K1 function.
    """

    def calculate_kl(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        return logprob - ref_logprob


class K2Fn(KLFn):
    """
    KL K2 function.
    """

    def calculate_kl(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        return (logprob - ref_logprob).square() * 0.5


class K3Fn(KLFn):
    """
    KL K3 function.
    """

    def calculate_kl(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        logr = ref_logprob - logprob
        return logr.exp() - 1 - logr


class LowVarKLFn(KLFn):
    """
    Low Variance KL function.
    """

    def calculate_kl(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        kl = ref_logprob - logprob
        kl = torch.clamp(kl, min=-20, max=20)
        ratio = torch.exp(kl)
        kld = (ratio - kl - 1).contiguous()
        return torch.clamp(kld, min=-10, max=10)


class AbsFn(KLFn):
    """
    KL Abs function.
    """

    def calculate_kl(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        return torch.abs(logprob - ref_logprob)


class CorrectedK3Fn(KLFn):
    """
    Corrected K3 function with importance sampling.

    This method applies importance sampling correction to the standard K3 KL divergence.
    The corrected KL is computed as:

        KL_corrected = (π_θ / π_old) * KL_standard(π_ref || π_θ)

    where:
        - π_θ: current policy
        - π_old: old policy (from rollout)
        - π_ref: reference policy
        - KL_standard: exp(log(π_ref/π_θ)) - log(π_ref/π_θ) - 1

    If old_logprob is not provided, it falls back to standard K3.
    """

    def calculate_kl(
        self,
        logprob: torch.Tensor,
        ref_logprob: torch.Tensor,
        old_logprob: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        """Compute corrected K3 KL divergence with importance sampling.

        Args:
            logprob: Log probabilities from current policy (log π_θ)
            ref_logprob: Log probabilities from reference policy (log π_ref)
            old_logprob: Log probabilities from old policy (log π_old), optional

        Returns:
            KL divergence tensor with same shape as input
        """
        # Standard K3 KL term: exp(log_ratio) - log_ratio - 1
        # where log_ratio = log(π_ref / π_θ) = ref_logprob - logprob
        logr = ref_logprob - logprob
        kl_term = logr.exp() - 1 - logr

        if old_logprob is None:
            # Fall back to standard K3 if old_logprob is not provided
            return kl_term

        # Compute importance sampling ratio: π_θ / π_old
        log_ratio_is = logprob - old_logprob
        ratio_is = log_ratio_is.exp()
        # Clamp ratio for numerical stability, range [0, 2]
        ratio_is = torch.clamp(ratio_is, min=0.0, max=2.0)

        # Corrected KL with importance sampling
        corrected_kl = ratio_is * kl_term

        return corrected_kl