# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
# Copyright 2023 PKU-Alignment Team. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for score models."""

from __future__ import annotations

import inspect
from dataclasses import dataclass
from typing import Optional, Tuple

import paddle
import paddle.distributed
import paddle.distributed as dist
import paddle.incubate.nn.functional as PF
import paddle.nn.functional as F
from paddle import nn
from paddle.distributed import fleet
from paddle.distributed.fleet.layers.mpu import mp_ops
from paddle.distributed.fleet.meta_parallel import ParallelCrossEntropy

try:
    from paddle.distributed.fleet.utils.sequence_parallel_utils import GatherOp
except:
    pass


from ...transformers.llama.modeling import (
    LlamaPretrainingCriterion as PretrainingCriterion,
)
from ...transformers.model_outputs import ModelOutput


@dataclass
class PolicyOutput(ModelOutput):
    loss: Optional[paddle.Tensor] = None
    logits: paddle.Tensor = None
    # logits_entropy: Optional[paddle.Tensor] = None
    past_key_values: Optional[Tuple[Tuple[paddle.Tensor]]] = None
    hidden_states: Optional[Tuple[paddle.Tensor]] = None
    attentions: Optional[Tuple[paddle.Tensor]] = None
    cross_attentions: Optional[Tuple[paddle.Tensor]] = None


@dataclass
class ValueOutput(ModelOutput):
    loss: Optional[paddle.Tensor] = None
    value: paddle.Tensor = None
    reward: paddle.Tensor = None
    past_key_values: Optional[Tuple[Tuple[paddle.Tensor]]] = None
    hidden_states: Optional[Tuple[paddle.Tensor]] = None
    attentions: Optional[Tuple[paddle.Tensor]] = None
    cross_attentions: Optional[Tuple[paddle.Tensor]] = None


def merge_fwd_labels(loss_cls):
    """
    PipelineParallel and trainer.criterion both use labels as tuple, thus wrap.
    """
    ori_fwd = loss_cls.forward

    def loss_fwd(self, predict, labels):
        return ori_fwd(self, predict, *labels)

    fwd_params = inspect.signature(ori_fwd).parameters
    # forward(self, predict, label1, label2, ...)
    loss_cls.label_names = list(fwd_params.keys())[2:]
    loss_cls.label_default_values = {}
    for label_name in loss_cls.label_names:
        if fwd_params[label_name].default is not inspect.Parameter.empty:
            loss_cls.label_default_values[label_name] = fwd_params[label_name].default
    loss_cls.forward = loss_fwd
    return loss_cls


def create_loss(loss_cls, config, extra_args, info_buffer, merge_labels=None):
    """
    loss_cls(paddle.nn.Layer): loss class
    config(PratrainedConfig): model config, to be consistent with loss defined
        in transformers
    extra_args(dict): create loss with more args not in config
    merge_labels: use a wrapped loss_cls whose label args are merged into one arg,
        this is useful to PipelineParallel and trainer.criterion since they only
        support loss format corresponding to this format.
    """
    # TODO(guosheng): merge_labels if loss_cls not
    ori_fwd = loss_cls.forward
    if merge_labels:
        fwd_params = inspect.signature(ori_fwd).parameters
        if len(fwd_params.keys()) > 3:  # merge_fwd_labels has not done
            loss_cls = merge_fwd_labels(loss_cls)
    # forward(self, predict, label1, label2, ...)
    loss_arg_names = list(inspect.signature(loss_cls.__init__).parameters.keys())[2:]
    if isinstance(extra_args, dict):
        loss_kwargs = {name: extra_args[name] for name in loss_arg_names if name in extra_args}
    else:
        # create from TrainingArguments
        loss_kwargs = {name: getattr(extra_args, name) for name in loss_arg_names if hasattr(extra_args, name)}
    if "info_buffer" in loss_arg_names:
        loss_kwargs["info_buffer"] = info_buffer
    loss = loss_cls(config, **loss_kwargs)
    return loss


def make_position_ids_from_input_ids(input_ids, pad_token_id=0):
    assert input_ids.ndim == 2, "input_ids's shape must be 2d"
    position_ids = paddle.zeros_like(input_ids)
    for index, row in enumerate(input_ids):
        non_zero_indices = paddle.nonzero(row != pad_token_id).flatten()
        start_index = non_zero_indices[0]
        position_ids[index, start_index + 1 :] = 1
    return position_ids.cumsum(-1)


# def make_position_ids_from_input_ids(input_ids, pad_token_id=0):
#     position_ids = (input_ids != pad_token_id).cast("int32").cumsum(-1) - 1
#     return position_ids.masked_fill(position_ids < 0, 0)


@paddle.no_grad()
def make_position_ids(attention_mask, source=None):
    """
    根据attention_mask生成位置id，如果source不为空则将源端padding部分设置为0。
    当attention_mask的形状是[B, L, H, W]时，表示causal mask，返回的position_ids是[B, H, W]；
    当attention_mask的形状是[B, L]时，表示padding mask，返回的position_ids是[B, L]。

    Args:
        attention_mask (Tensor, numpy.ndarray): 形状为[B, L, H, W]或者[B, L]的Tensor/numpy数组，其中L是序列长度，H是头数，W是宽度（可选）。
            每个元素为0表示该位置未被mask，非0表示该位置被mask。
        source (Tensor, numpy.ndarray, optional): 形状为[B, S]的Tensor/numpy数组，其中S是源端序列长度（可选）。默认值为None。

    Returns:
        Tensor: 形状为[B, H, W]或者[B, L]的Tensor，其中H是头数，W是宽度（可选）。每个元素为对应位置的位置id。
        如果source不为空，则在源端padding部分设置为0。
    """
    if len(attention_mask.shape) == 4:  # causal mask
        position_ids_p1 = attention_mask.cast(paddle.int64).sum(-1)
        position_ids = position_ids_p1 - 1
        position_ids = paddle.where(position_ids == -1, position_ids_p1, position_ids)
        return position_ids[:, 0, :]
    assert len(attention_mask.shape) == 2  # padding mask
    attention_mask_bool = attention_mask
    attention_mask = attention_mask.cast(paddle.int64)
    position_ids = attention_mask.cumsum(-1) - 1
    # Make padding positions in source be 0, since reward model use position_ids
    # plus with padding size (number of 0s) in source to calculate end offsets.
    # It does not matter when source is left padding and target is right padding
    # which is the output of non-FuseMT generation, while when using FuseMT whose
    # output is right padding source and right padding target, we have to set
    # padding positions in source be 0 to make compatible.
    if source is not None:
        src_len = position_ids[:, source.shape[-1] - 1].unsqueeze(-1)
        position_ids = paddle.where(
            paddle.logical_and(paddle.logical_not(attention_mask_bool), position_ids <= src_len),
            attention_mask,
            position_ids,
        )
        return position_ids
    position_ids = paddle.where(position_ids == -1, attention_mask, position_ids)
    return position_ids


@paddle.no_grad()
def make_attention_mask(
    input_ids,
    pad_id,
    eos_id=None,
    unk_id=None,
    past_key_values_length=0,
    causal_mask=True,
):
    """
    根据输入的`input_ids`，生成一个注意力掩码。如果`pad_id`不是`unk_id`和`eos_id`中的任何一个，则该位置将被忽略。
    如果`causal_mask`为`False`，则返回全部为`True`的注意力掩码。否则，返回一个三角形掩码，其中每个元素都小于或等于相应位置的元素。

    Args:
        input_ids (Tensor): 输入序列的ID，形状为（batch_size, seq_len）。
        pad_id (int): 用于padding的ID。
        eos_id (int, optional): 用于表示结束的ID，默认为None。如果设置了，则会从注意力掩码中删除对应位置。
        unk_id (int, optional): 用于表示未知的ID，默认为None。如果设置了，则会从注意力掩码中删除对应位置。
        past_key_values_length (int, optional): 预先存在的键值对的长度，默认为0。
        causal_mask (bool, optional): 是否使用因果掩码，默认为True。

    Returns:
        Tensor: 注意力掩码，形状为（batch_size, 1, seq_len, seq_len + past_len）。
    """
    unk_id = None

    attention_mask = input_ids != pad_id
    if unk_id is not None and pad_id != unk_id:
        if eos_id is not None and unk_id != eos_id:
            attention_mask = paddle.logical_and(attention_mask, input_ids != unk_id)
    if eos_id is not None and pad_id != eos_id:
        attention_mask = paddle.logical_and(attention_mask, input_ids != eos_id)
    if not causal_mask:
        return attention_mask

    batch_size, target_length = input_ids.shape  # target_length: seq_len
    mask = paddle.tril(paddle.ones((target_length, target_length), dtype="bool"))
    if past_key_values_length > 0:
        # [tgt_len, tgt_len + past_len]
        mask = paddle.concat(
            [
                paddle.ones([target_length, past_key_values_length], dtype="bool"),
                mask,
            ],
            axis=-1,
        )
    # [bs, 1, tgt_len, tgt_len + past_len]
    causal_mask = mask[None, None, :, :].expand([batch_size, 1, target_length, target_length + past_key_values_length])

    attention_mask = attention_mask[:, None, None, :]
    expanded_attn_mask = attention_mask & causal_mask
    return expanded_attn_mask


def gather_log_probabilities(logits: paddle.Tensor, labels: paddle.Tensor) -> paddle.Tensor:
    """Gather log probabilities of the given labels from the logits."""
    # log_probs = F.log_softmax(logits, axis=-1)
    # log_probs_labels = paddle.take_along_axis(log_probs, axis=-1, indices=labels.unsqueeze(axis=-1))
    # return log_probs_labels.squeeze(axis=-1)
    token_loss = F.cross_entropy(
        logits.cast("float32"),
        labels,
        reduction="none",
    ).squeeze(axis=-1)
    return -token_loss.cast(logits.dtype)


def create_startend_row_indices(input_ids, pad_token_id=0):
    startend_row_indices = paddle.full(input_ids.shape, input_ids.shape[-1], dtype="int32")
    mask = (input_ids != pad_token_id).cast("int32").cumsum(-1) == 0
    return startend_row_indices.masked_fill(mask, 0)


class RLHFPPOLoss(nn.Layer):
    def __init__(self, config, clip_range_ratio=0.2, clip_range_ratio_low=None, clip_range_ratio_high=None):
        """
        Initialize the `ClipRewardRange` object.

        Args:
            config (dict): A dictionary containing environment configuration parameters.
                See :class:`~rllib.agents.Agent` for more information.
            clip_range_ratio (float, optional): The ratio of the range to which the reward is clipped.
                Defaults to 0.2.

        Raises:
            None.

        Returns:
            None.
        """
        super().__init__()
        self.clip_range_ratio = clip_range_ratio
        self.clip_range_ratio_low = clip_range_ratio_low
        self.clip_range_ratio_high = clip_range_ratio_high
        self.config = config

    def actor_loss_fn(
        self,
        log_probs: paddle.Tensor,
        old_log_probs: paddle.Tensor,
        advantages: paddle.Tensor,
        mask: paddle.Tensor,
    ) -> paddle.Tensor:
        """
        计算演员的策略损失函数。该函数接受以下参数：
        Args:
            log_probs (paddle.Tensor): 当前状态下每个演员的对数产生概率，形状为[B, A]，其中B是批量大小，A是演员数量。
            old_log_probs (paddle.Tensor): 上一时间步骤的每个演员的对数产生概率，形状与log_probs相同。
            advantages (paddle.Tensor): 每个演员在当前状态下获得的价值函数估计值，形状为[B, A]。
            mask (paddle.Tensor): 用于过滤已完成或无效的轨迹，形状为[B, A]，其中B是批量大小，A是演员数量。
                如果轨迹已经完成（即reward不为None），则mask为1；否则为0。
        返回值 (paddle.Tensor):
            PG_loss (paddle.Tensor): 演员的策略损失，形状为[1]。
        """
        # policy gradient loss

        ratio = paddle.exp(log_probs - old_log_probs)
        pg_loss1 = -advantages * ratio
        pg_loss2 = -advantages * paddle.clip(
            ratio,
            1.0 - self.clip_range_ratio_low,
            1.0 + self.clip_range_ratio_high,
        )
        return paddle.sum(paddle.maximum(pg_loss1, pg_loss2) * mask) / mask.sum()

    def forward(self, log_probs, old_log_probs, reward_advantages, sequence_mask):
        """
        Calculate the loss of the actor network.

        Args:
            logits (Tensor, shape [batch_size, seq_len, vocab_size]): The output logits of the model.
            input_ids (Tensor, shape [batch_size, seq_len]): The input ids of the batch.
            old_log_probs (Tensor, shape [batch_size, seq_len]): The previous log probabilities of the batch.
            reward_advantages (Tensor, shape [batch_size, seq_len]): The rewards or advantages of the batch.
            sequence_mask (Tensor, shape [batch_size, seq_len]): A mask indicating which elements are valid.
                Valid elements are those where sequence_mask is True.

        Returns:
            Tensor, shape [1], the loss of the actor network.

        Raises:
            None.
        """
        actor_loss = self.actor_loss_fn(
            log_probs,
            old_log_probs,
            reward_advantages,
            sequence_mask,
        )
        return actor_loss


class VocabParallelEntropy(paddle.autograd.PyLayer):
    @staticmethod
    def forward(ctx, vocab_parallel_logits: paddle.Tensor, tensor_parallel_output=False) -> paddle.Tensor:
        try:
            hcg = fleet.get_hybrid_communicate_group()
            model_parallel_group = hcg.get_model_parallel_group()
            tensor_parallel_degree = hcg.get_model_parallel_world_size()
        except:
            tensor_parallel_degree = 1
        logits_max = vocab_parallel_logits.max(axis=-1, keepdim=True)

        if tensor_parallel_degree > 1 and tensor_parallel_output:
            dist.all_reduce(logits_max, op=dist.ReduceOp.MAX, group=model_parallel_group)
        normalized_vocab_parallel_logits = vocab_parallel_logits - logits_max
        normalized_exp_logits = normalized_vocab_parallel_logits.exp()
        normalized_sum_exp_logits = normalized_exp_logits.sum(axis=-1, keepdim=True)

        if tensor_parallel_degree > 1 and tensor_parallel_output:
            dist.all_reduce(normalized_sum_exp_logits, group=model_parallel_group)
        softmax_logits = normalized_exp_logits / normalized_sum_exp_logits
        sum_softmax_times_logits = (softmax_logits * vocab_parallel_logits).sum(axis=-1, keepdim=True)

        if tensor_parallel_degree > 1 and tensor_parallel_output:
            dist.all_reduce(sum_softmax_times_logits, group=model_parallel_group)
        entropy = logits_max + normalized_sum_exp_logits.log() - sum_softmax_times_logits
        ctx.save_for_backward(softmax_logits * (sum_softmax_times_logits - vocab_parallel_logits))
        return entropy.squeeze(axis=-1)

    @staticmethod
    def backward(ctx, grad_output: paddle.Tensor) -> paddle.Tensor:
        return grad_output.unsqueeze(axis=-1) * ctx.saved_tensor()[0]


def entropy_from_logits(logits: paddle.Tensor, tensor_parallel_output=False):
    return VocabParallelEntropy.apply(logits.astype("float32"), tensor_parallel_output)


@merge_fwd_labels
class RLHFPPOMixedLoss(nn.Layer):
    """provide two losses, one for PPO loss, the other for SFT loss."""

    def __init__(
        self,
        config,
        ptx_coeff=16,
        clip_range_ratio=0.2,
        clip_range_ratio_low=None,
        clip_range_ratio_high=None,
        kl_loss_coeff=0.001,
        clip_range_score=10,
        info_buffer=None,
        temperature=1.0,
        entropy_coeff=0.001,
        pg_loss_coeff=1.0,
        use_fp32_compute=False,
    ):
        """
        Args:
        config (Config): configuration object containing hyperparameters and options for the agent.
        ptx_coeff (int, optional): coefficient to use in the PTX loss calculation. Defaults to 16.
        clip_range_ratio (float, optional): ratio of clipped range to unclipped range. Defaults to 0.2.
        """
        super(RLHFPPOMixedLoss, self).__init__()
        self.config = config
        self.ptx_coeff = ptx_coeff
        # if self.config.use_fused_head_and_loss_fn:
        #     self.ppo_criterion = FusedPPOLoss(config, clip_range_ratio, clip_range_ratio_low, clip_range_ratio_high)
        # else:
        #     self.ppo_criterion = RLHFPPOLoss(config, clip_range_ratio, clip_range_ratio_low, clip_range_ratio_high)
        self.clip_range_ratio_low = clip_range_ratio_low if clip_range_ratio_low is not None else clip_range_ratio
        self.clip_range_ratio_high = clip_range_ratio_high if clip_range_ratio_high is not None else clip_range_ratio
        self.ppo_criterion = RLHFPPOLoss(
            config, clip_range_ratio, self.clip_range_ratio_low, self.clip_range_ratio_high
        )
        self.sft_criterion = PretrainingCriterion(config)
        self.kl_loss_coeff = kl_loss_coeff
        self.clip_range_score = clip_range_score
        self.info_buffer = info_buffer
        self.temperature = temperature
        self.clip_range_ratio = clip_range_ratio
        self.entropy_coeff = entropy_coeff
        self.pg_loss_coeff = pg_loss_coeff
        self.use_fp32_compute = use_fp32_compute

    def forward(
        self,
        logits,
        labels,
        input_ids,
        old_log_probs,
        reward_advantages,
        sequence_mask,
        ref_log_probs=None,
        response_start=0,
        # for varlen flashmask
        pad_size=0,
        raw_input_ids=None,
        indices=None,
        raw_input_shape=None,
        input_ids_rmpad_rolled=None,
    ):
        """
        计算损失函数，包含两部分：soft target loss和PPO loss。
        如果labels不为None，则计算soft target loss；否则计算PPO loss。

        Args:
            logits (paddle.Tensor or List[paddle.Tensor]): 输入的预测结果，可以是单个tensor或list中的多个tensor。
                如果是单个tensor，表示对应的输出logits；如果是list，表示每个时间步的logits。
            labels (paddle.Tensor, optional): 真实标签，shape与logits相同。默认为None。
            input_ids (paddle.Tensor, optional): 输入序列的id，shape为(batch_size, max_len)。默认为None。
            old_log_probs (paddle.Tensor, optional): 上一个时间步的log probabilities，shape为(batch_size, max_len)。默认为None。
            reward_advantages (paddle.Tensor, optional): 回报优势，shape为(batch_size, max_len)。默认为None。
            sequence_mask (paddle.Tensor, optional): 序列掩码，shape为(batch_size, max_len)。默认为None。

        Returns:
            paddle.Tensor: 返回损失函数，如果labels不为None，则为soft target loss；否则为PPO loss。
        """
        use_remove_padding = indices is not None
        if not self.config.use_fused_head_and_loss_fn:
            logits = logits if isinstance(logits, paddle.Tensor) else logits[0]
            if self.use_fp32_compute and logits.dtype != paddle.float32:
                logits = logits.cast(paddle.float32)

            if self.temperature > 0.0:
                # use inplace method to save gpu memory
                logits.scale_(1.0 / self.temperature)

        else:
            hidden_states, weight, bias, transpose_y = logits
            if self.config.tensor_parallel_degree > 1 and self.config.sequence_parallel:
                hidden_states = GatherOp.apply(hidden_states)
                hidden_states = hidden_states.reshape(
                    [
                        input_ids.shape[0],
                        -1,
                        hidden_states.shape[-1],
                    ]
                )

            if use_remove_padding:
                input_ids = raw_input_ids
                if pad_size > 0:
                    hidden_states = hidden_states[:, :-pad_size]

                from ..utils.bert_padding import pad_input

                hidden_states = pad_input(
                    hidden_states.squeeze(0), indices, batch=raw_input_shape[0], seqlen=raw_input_shape[1]
                ).contiguous()

            if self.use_fp32_compute and hidden_states.dtype != paddle.float32:
                hidden_states = hidden_states.cast(paddle.float32)
                weight = weight.cast(paddle.float32)
                if bias is not None:
                    bias = bias.cast(paddle.float32)
            total_loss, pg_loss, entropy_loss, kl_loss = actor_fused_pg_entropy_kl_loss(
                hidden_states,
                weight,
                input_ids,
                old_log_probs,
                ref_log_probs,
                reward_advantages,
                sequence_mask,
                bias=bias,
                transpose_y=transpose_y,
                fused_linear=False,
                vocab_size=self.config.vocab_size,
                tensor_parallel_degree=self.config.tensor_parallel_degree,
                tensor_parallel_output=self.config.tensor_parallel_output,
                pg_loss_coeff=self.pg_loss_coeff,  # do not use this
                clip_range_ratio=self.clip_range_ratio,
                clip_range_ratio_low=self.clip_range_ratio_low,
                clip_range_ratio_high=self.clip_range_ratio_high,
                entropy_coeff=self.entropy_coeff,  # do not support this
                clip_range_score=self.clip_range_score,
                kl_loss_coeff=self.kl_loss_coeff,
                loop_chunk_size=1024,
                response_start=response_start,
                use_actor_fused_loss=True,  # currently only support kunbo's fused head loss
                temperature=self.temperature,
            )
            with paddle.no_grad():
                self.info_buffer["kl_loss"] = (
                    kl_loss.detach() / self.kl_loss_coeff if self.kl_loss_coeff > 0 else paddle.to_tensor([0.0])
                )
                self.info_buffer["entropy_loss"] = entropy_loss.detach()
                self.info_buffer["pure_policy_loss"] = (
                    pg_loss.detach() / self.pg_loss_coeff if self.pg_loss_coeff > 0 else paddle.to_tensor([0.0])
                )
            return total_loss
        loss = None
        # sft, pt loss
        if labels is not None:
            loss = self.ptx_coeff * self.sft_criterion(logits, labels)
        # ppo loss
        if reward_advantages is not None:
            if use_remove_padding:
                from ..utils.bert_padding import pad_input

                if self.config.tensor_parallel_degree > 1 and self.config.tensor_parallel_output:
                    log_probs = (
                        -ParallelCrossEntropy()(logits.astype("float32"), input_ids_rmpad_rolled)
                        .squeeze(axis=-1)
                        .astype(logits.dtype)
                    )
                else:
                    log_probs = gather_log_probabilities(logits, input_ids_rmpad_rolled)

                if pad_size > 0:
                    log_probs = log_probs[:, :-pad_size]
                log_probs = pad_input(
                    log_probs.squeeze(0).unsqueeze(-1), indices, batch=raw_input_shape[0], seqlen=raw_input_shape[1]
                ).squeeze(-1)
                log_probs = log_probs[:, response_start:-1].contiguous()
            else:
                if self.config.tensor_parallel_degree > 1 and self.config.tensor_parallel_output:
                    log_probs = (
                        -ParallelCrossEntropy()(
                            logits[:, response_start:-1].astype("float32"), input_ids[:, response_start + 1 :]
                        )
                        .squeeze(axis=-1)
                        .astype(logits.dtype)
                    )
                else:
                    log_probs = gather_log_probabilities(
                        logits[:, response_start:-1], input_ids[:, response_start + 1 :]
                    )

            if log_probs.shape[1] == old_log_probs.shape[1]:
                # labels (old_log_probs, reward_advantages, sequence_mask) has
                # src+tgt-1 length, valid length is determined by sequence_mask
                pass
            elif log_probs.shape[1] < old_log_probs.shape[1]:
                # labels (old_log_probs, reward_advantages, sequence_mask) has
                # src+tgt length and the last one is a padding to be consistent
                # with input_ids
                assert log_probs.shape[1] == old_log_probs.shape[1] - 1
                log_probs = paddle.concat(
                    [
                        log_probs,
                        paddle.zeros([log_probs.shape[0], 1], dtype=log_probs.dtype),
                    ],
                    -1,
                )
            else:
                # labels (old_log_probs, reward_advantages, sequence_mask) has tgt length
                log_probs = log_probs[:, -old_log_probs.shape[1] :]

            # TODO:support fused head and loss fn
            loss = self.ppo_criterion(log_probs, old_log_probs, reward_advantages, sequence_mask)
            self.info_buffer["pure_policy_loss"] = loss.detach()
            loss = self.pg_loss_coeff * loss

        if ref_log_probs is not None:
            kl_divergence_estimate = paddle.clip(
                paddle.exp(ref_log_probs - log_probs) - (ref_log_probs - log_probs) - 1,
                min=-self.clip_range_score,
                max=self.clip_range_score,
            )
            kl_loss = paddle.sum(kl_divergence_estimate * sequence_mask) / sequence_mask.sum()
            self.info_buffer["kl_loss"] = kl_loss.detach()
            loss += self.kl_loss_coeff * kl_loss

        if self.entropy_coeff > 0:
            if use_remove_padding:
                entropy_loss_rmpad = entropy_from_logits(
                    logits.cast("float32"), self.config.tensor_parallel_output
                ).cast(logits.dtype)
                if pad_size > 0:
                    entropy_loss_rmpad = entropy_loss_rmpad[:, :-pad_size]
                entropy_loss = pad_input(
                    entropy_loss_rmpad.squeeze(0).unsqueeze(-1),
                    indices,
                    batch=raw_input_shape[0],
                    seqlen=raw_input_shape[1],
                ).squeeze(-1)
                entropy_loss_raw = entropy_loss[:, response_start:-1].contiguous()
            else:
                entropy_loss_raw = entropy_from_logits(
                    logits[:, response_start:-1], self.config.tensor_parallel_output
                )
            entropy_loss = paddle.sum(entropy_loss_raw * sequence_mask) / sequence_mask.sum()
            self.info_buffer["entropy_loss"] = entropy_loss.detach()
            loss -= self.entropy_coeff * entropy_loss
        else:
            self.info_buffer["entropy_loss"] = paddle.to_tensor([0.0])

        return loss


@merge_fwd_labels
class RLHFValueLoss(nn.Layer):
    def __init__(self, config, clip_range_value=5.0, use_fp32_compute=False):
        """
        Initializes the `ClipRewardRange` object.

        Args:
            config (dict): The configuration dictionary for the environment.
                See :ref:`rllib-spaces` for more information.
            clip_range_value (Optional[float]): The value to which the rewards will be clipped. Defaults to 5.0.

        Raises:
            None.

        Returns:
            None.
        """
        super().__init__()
        self.clip_range_value = clip_range_value
        self.config = config
        self.use_fp32_compute = use_fp32_compute

    def critic_loss_fn(
        self,
        values: paddle.Tensor,
        old_values: paddle.Tensor,
        returns: paddle.Tensor,
        mask: paddle.Tensor,
    ) -> paddle.Tensor:
        """Compute critic loss."""
        # TODO(guosheng): use paddle.clip when its min/max can support more than
        # 0D Tensor
        values_clipped = paddle.minimum(
            paddle.maximum(values, old_values - self.clip_range_value),
            old_values + self.clip_range_value,
        )
        vf_loss1 = paddle.square(values - returns)
        vf_loss2 = paddle.square(values_clipped - returns)
        return 0.5 * paddle.sum(paddle.maximum(vf_loss1, vf_loss2) * mask) / mask.sum()

    def forward(
        self,
        reward_values,
        old_reward_values,
        reward_returns,
        sequence_mask,
        response_start=0,
        # for varlen flaskmask
        pad_size=0,
        raw_input_ids=None,
        indices=None,
        raw_input_shape=None,
        input_ids_rmpad_rolled=None,
    ):
        """ """
        reward_values = reward_values[0].squeeze(0)
        if self.config.sequence_parallel:
            from paddle.distributed.fleet.utils.sequence_parallel_utils import GatherOp

            reward_values = GatherOp.apply(reward_values)

        use_remove_padding = indices is not None
        if use_remove_padding:
            if pad_size > 0:
                reward_values = reward_values[:-pad_size, :]

            from ..utils.bert_padding import pad_input

            reward_values = pad_input(
                reward_values.squeeze(0), indices, batch=raw_input_shape[0], seqlen=raw_input_shape[1]
            ).contiguous()

        if self.use_fp32_compute and reward_values.dtype != paddle.float32:
            reward_values = reward_values.cast(paddle.float32)

        reward_values = reward_values.squeeze(axis=-1)[:, response_start:-1]
        reward_critic_loss = self.critic_loss_fn(
            reward_values,
            old_reward_values,
            reward_returns,
            sequence_mask,
        )
        return reward_critic_loss


class ActorFusedLoss(paddle.autograd.PyLayer):
    """Fused Actor Loss"""

    @staticmethod
    def forward(
        ctx,
        hidden_states: paddle.Tensor,
        lm_head_weight: paddle.Tensor,
        lm_head_bias: paddle.Tensor,
        labels: paddle.Tensor,
        mask: paddle.Tensor,
        transpose_y: bool,
        num_embeddings: int,
        tensor_parallel_degree: int,
        tensor_parallel_output: bool,
        fused_linear: bool,
        loop_chunk_size: int,
        ignore_index: int,
        old_log_probs: paddle.Tensor,
        ref_log_probs: paddle.Tensor,
        advantages: paddle.Tensor,
        clip_range_ratio: float,
        clip_range_ratio_low: float,
        clip_range_ratio_high: float,
        clip_range_score: float,
        kl_loss_coeff: float,  # KL loss coefficient
        temperature: float,
        print_entropy_loss: bool = True,
    ):
        """
        forward function of ActorFusedLoss

        Args:
            ctx (paddle.autograd.PyLayerContext): context.
            hidden_states (paddle.Tensor): hidden_states, [batch_size, seq_len-1, hidden_size].
            lm_head_weight (paddle.Tensor): lm_head_weight, [hidden_size, vocab_size / tensor_parallel_degree].
            lm_head_bias (paddle.Tensor, optional): lm_head_bias, [vocab_size / tensor_parallel_degree].
            labels (paddle.Tensor): labels, [batch_size, seq_len-1].
            mask (paddle.Tensor): mask, [batch_size, seq_len-1].
            transpose_y (bool): whether to transpose lm_head_weight.
            num_embeddings (int): vocab_size.
            tensor_parallel_degree (int): tensor_parallel_degree.
            tensor_parallel_output (bool): tensor_parallel_output, set True in ppo_main.py.
            fused_linear (bool): Flag for using fused linear, always False.
            loop_chunk_size (int): chunk_size.
            ignore_index (int): not used now.
            old_log_probs (paddle.Tensor): old_log_probs, [batch_size, seq_len-1].
            advantages (paddle.Tensor): advantages, [batch_size, seq_len-1].
            clip_range_ratio (float): The clipping range for ratio.

        Returns:
            paddle.Tensor: loss

        """
        if fused_linear:
            # print("Cannot support fused_linear while using use_fused_head_and_loss_fn now!")
            fused_linear = False
        if tensor_parallel_degree > 1:
            assert tensor_parallel_output, (
                "When tensor_parallel_degree > 1 and use_fused_head_and_loss_fn, "
                "tensor_parallel_output needs to be set to True."
            )
        dtype = hidden_states.dtype
        # Parallel Configuration
        if tensor_parallel_degree > 1 and tensor_parallel_output:
            hcg = fleet.get_hybrid_communicate_group()
            model_parallel_group = hcg.get_model_parallel_group()
            tensor_parallel_degree = hcg.get_model_parallel_world_size()

        # reshape
        original_shape = hidden_states.shape
        hidden_states_stop_grad = hidden_states.stop_gradient  # original stop_gradient
        hidden_states = hidden_states.reshape([-1, original_shape[-1]])
        labels = labels.reshape([-1])
        old_log_probs = old_log_probs.reshape([-1])
        if kl_loss_coeff > 0:
            ref_log_probs = ref_log_probs.reshape([-1])
        advantages = advantages.reshape([-1])
        loss_mask = mask.reshape([-1]).astype("float32")  # .astype(dtype)

        n_tokens = hidden_states.shape[0]
        n_classes = lm_head_weight.shape[0] if transpose_y else lm_head_weight.shape[1]

        # convert dtype of weights and biases of lm_head
        lm_head_weight_cast = lm_head_weight.astype(dtype)
        if lm_head_bias is not None:
            lm_head_bias_cast = lm_head_bias.astype(dtype)

        # use indices to distinguish the devices.
        if tensor_parallel_degree > 1 and tensor_parallel_output:
            rank = hcg.get_model_parallel_rank()
            per_part_size = num_embeddings // tensor_parallel_degree
            indices = paddle.arange(
                rank * per_part_size,
                rank * per_part_size + n_classes,
                dtype=labels.dtype,
            ).unsqueeze(0)
        else:
            indices = paddle.arange(num_embeddings, dtype=labels.dtype).unsqueeze(0)

        # initialize total_loss and divisor
        total_loss = paddle.zeros([1], dtype=dtype)
        total_kl_loss = paddle.zeros([1], dtype=dtype)
        total_entropy_loss = paddle.zeros([1], dtype=dtype)
        divisor = loss_mask.sum()

        # initialize grads
        if not lm_head_weight.stop_gradient:
            grad_lm_head_weight = paddle.zeros_like(lm_head_weight)
        else:
            grad_lm_head_weight = None
        if lm_head_bias is not None and not lm_head_bias.stop_gradient:
            grad_lm_head_bias = paddle.zeros_like(lm_head_bias)
        else:
            grad_lm_head_bias = None
        if not hidden_states_stop_grad:
            grad_hidden_states = paddle.zeros_like(hidden_states)
        else:
            grad_hidden_states = None

        for i in range(0, n_tokens, loop_chunk_size):
            token_start_idx = i
            token_end_idx = min(i + loop_chunk_size, n_tokens)
            hidden_states_chunk = hidden_states[token_start_idx:token_end_idx]
            labels_chunk = labels[token_start_idx:token_end_idx]
            mask_chunk = loss_mask[token_start_idx:token_end_idx]
            old_log_probs_chunk = old_log_probs[token_start_idx:token_end_idx] * mask_chunk
            if kl_loss_coeff > 0:
                ref_log_chunk = ref_log_probs[token_start_idx:token_end_idx] * mask_chunk
            advantages_chunk = advantages[token_start_idx:token_end_idx]

            # Calculate the current logits_chunk,  not fused linear
            logits_chunk_cast = paddle.matmul(hidden_states_chunk, lm_head_weight_cast, transpose_y=transpose_y)
            if lm_head_bias is not None:
                logits_chunk_cast += lm_head_bias_cast
            # logits_chunk_cast = paddle.nn.functional.linear(hidden_states_chunk, lm_head_weight_cast, lm_head_bias)

            logits_chunk = logits_chunk_cast.astype("float32")
            logits_chunk = logits_chunk / temperature

            labels_one_hot = labels_chunk.unsqueeze(1) == indices
            # rewritten as cross entropy
            if tensor_parallel_degree > 1 and tensor_parallel_output:
                token_loss_chunk, softmax_output_chunk = mp_ops._c_softmax_with_cross_entropy(
                    logits_chunk,
                    labels_chunk,
                    group=model_parallel_group,
                    return_softmax=True,
                )
            else:
                token_loss_chunk = F.cross_entropy(logits_chunk, labels_chunk, reduction="none")
                softmax_output_chunk = F.softmax(logits_chunk, axis=-1)

            log_probs_chunk = -token_loss_chunk.squeeze(axis=-1) * mask_chunk
            # calculate gradient, note sign
            grad_logits_chunk = labels_one_hot.astype("float32") - softmax_output_chunk
            grad_logits_chunk = grad_logits_chunk.astype(dtype)

            # ratio
            ratio_chunk = paddle.exp(log_probs_chunk - old_log_probs_chunk)

            clipped_ratio_chunk = paddle.clip(
                ratio_chunk, min=1.0 - clip_range_ratio_low, max=1.0 + clip_range_ratio_high
            )

            # final loss
            pg_loss1_chunk = -advantages_chunk * ratio_chunk
            pg_loss2_chunk = -advantages_chunk * clipped_ratio_chunk
            pg_loss_chunk = paddle.maximum(pg_loss1_chunk, pg_loss2_chunk)

            # mask
            pg_loss_chunk = pg_loss_chunk * mask_chunk
            masked_loss_sum = paddle.sum(pg_loss_chunk)
            # add
            total_loss += masked_loss_sum

            # grads
            # direction
            I1_chunk = (pg_loss1_chunk >= pg_loss2_chunk).astype(dtype)
            I2_chunk = 1.0 - I1_chunk

            # clip
            clip_mask_chunk = (
                (ratio_chunk >= 1.0 - clip_range_ratio) & (ratio_chunk <= 1.0 + clip_range_ratio)
            ).astype(dtype)

            # ∂loss1/∂log_probs, ∂loss2/∂log_probs
            d_ratio_d_log_probs_chunk = ratio_chunk
            d_pg_loss1_d_log_probs_chunk = -advantages_chunk * d_ratio_d_log_probs_chunk
            d_pg_loss2_d_log_probs_chunk = -advantages_chunk * clip_mask_chunk * d_ratio_d_log_probs_chunk

            # ∂loss/∂log_probs
            d_loss_d_log_probs_chunk = (
                I1_chunk * d_pg_loss1_d_log_probs_chunk + I2_chunk * d_pg_loss2_d_log_probs_chunk
            )
            d_loss_d_log_probs_chunk = d_loss_d_log_probs_chunk * mask_chunk / divisor

            # ∂log_probs/∂logits, just take the previous one.
            d_log_probs_d_logits_chunk = grad_logits_chunk / temperature
            # ∂loss/∂logits
            d_loss_d_logits_chunk = d_loss_d_log_probs_chunk.unsqueeze(-1) * d_log_probs_d_logits_chunk

            if kl_loss_coeff > 0:
                # [3] kl loss
                delta_chunk = ref_log_chunk - log_probs_chunk

                exp_delta_chunk = paddle.exp(delta_chunk)
                kl_loss_estimate_chunk = exp_delta_chunk - delta_chunk - 1
                kl_loss_clipped_chunk = (
                    paddle.clip(
                        kl_loss_estimate_chunk,
                        min=-clip_range_score,
                        max=clip_range_score,
                    )
                    * mask_chunk
                )
                total_kl_loss += kl_loss_clipped_chunk.sum() * kl_loss_coeff
                # gradgradgradgrad kl loss
                kl_within_clip_chunk = (
                    (kl_loss_estimate_chunk >= -clip_range_score) & (kl_loss_estimate_chunk <= clip_range_score)
                ).astype(dtype)
                d_kl_log_probs_chunk = (
                    (1 - exp_delta_chunk) * kl_within_clip_chunk * mask_chunk * kl_loss_coeff / divisor
                )
                d_loss_d_logits_chunk += d_kl_log_probs_chunk.unsqueeze(-1) * d_log_probs_d_logits_chunk

            if print_entropy_loss:
                # [2] entropy loss
                log_prob_chunk = paddle.log(paddle.clip(softmax_output_chunk, min=1e-12))
                entropy_loss_chunk = -(softmax_output_chunk * log_prob_chunk).sum(axis=-1) * mask_chunk
                # entropy_loss_chunk shape is [bs, seqlen, vocab_size // tensor_parallel_degree], do all_reduce sum here
                if tensor_parallel_degree > 1 and tensor_parallel_output:
                    paddle.distributed.all_reduce(
                        entropy_loss_chunk, op=paddle.distributed.ReduceOp.SUM, group=model_parallel_group
                    )
                total_entropy_loss += entropy_loss_chunk.sum() / divisor

            # grads
            if grad_hidden_states is not None:
                grad_hidden_states[token_start_idx:token_end_idx] = paddle.matmul(
                    d_loss_d_logits_chunk, lm_head_weight_cast, transpose_y=not transpose_y
                )
            if grad_lm_head_weight is not None:
                if transpose_y:
                    grad_lm_head_weight += paddle.matmul(d_loss_d_logits_chunk, hidden_states_chunk, transpose_x=True)
                else:
                    grad_lm_head_weight += paddle.matmul(hidden_states_chunk, d_loss_d_logits_chunk, transpose_x=True)
            if grad_lm_head_bias is not None:
                grad_lm_head_bias += d_loss_d_logits_chunk.astype("float32").sum(axis=0).astype(dtype)

        final_loss = (total_loss + total_kl_loss) / divisor
        ctx.hidden_states_has_grad = grad_hidden_states is not None
        ctx.lm_head_weight_has_grad = grad_lm_head_weight is not None
        ctx.lm_head_bias_has_grad = grad_lm_head_bias is not None

        grad_args = []
        if ctx.hidden_states_has_grad:
            if tensor_parallel_degree > 1:
                dist.all_reduce(grad_hidden_states, op=dist.ReduceOp.SUM, group=model_parallel_group)
            grad_args.append(grad_hidden_states.reshape(original_shape))
        if ctx.lm_head_weight_has_grad:
            grad_args.append(grad_lm_head_weight)
        if ctx.lm_head_bias_has_grad:
            grad_args.append(grad_lm_head_bias)

        ctx.save_for_backward(*grad_args)
        return (
            final_loss,
            (total_loss / divisor).detach(),
            total_entropy_loss.detach(),
            (total_kl_loss / divisor).detach(),
        )

    @staticmethod
    def backward(ctx, grad_output, *args):
        """
        backward function of ActorFusedLoss

        Args:
            ctx: Context.
            grad_output(paddle.Tensor): Gradient.
        Returns:
            tuple:
                - Gradient tensors for hidden_states, lm_head_weight, and lm_head_bias,
                  None values are used for inputs not requiring gradients.
        """
        grad_args = ctx.saved_tensor()
        idx = 0
        if ctx.hidden_states_has_grad:
            grad_hidden_states = grad_args[idx] * grad_output.astype(grad_args[idx].dtype)
            idx += 1
        else:
            grad_hidden_states = None

        if ctx.lm_head_weight_has_grad:
            grad_lm_head_weight = grad_args[idx] * grad_output.astype(grad_args[idx].dtype)
            idx += 1
        else:
            grad_lm_head_weight = None

        if ctx.lm_head_bias_has_grad:
            grad_lm_head_bias = grad_args[idx] * grad_output.astype(grad_args[idx].dtype)
            idx += 1
        else:
            grad_lm_head_bias = None
        return grad_hidden_states, grad_lm_head_weight, grad_lm_head_bias, None, None


class FusedPPOLoss(nn.Layer):
    """Fused PPOLoss"""

    def __init__(self, config, clip_range_ratio=0.2, clip_range_ratio_low=None, clip_range_ratio_high=None):
        """Initialize FusedPPOLoss class."""
        super().__init__()
        self.clip_range_ratio = clip_range_ratio
        self.clip_range_ratio_low = clip_range_ratio_low
        self.clip_range_ratio_high = clip_range_ratio_high
        self.config = config

    def forward(
        self,
        hidden_states: paddle.Tensor,
        lm_head_weight: paddle.Tensor,
        lm_head_bias: paddle.Tensor,
        input_ids: paddle.Tensor,
        old_log_probs: paddle.Tensor,
        reward_advantages: paddle.Tensor,
        sequence_mask: paddle.Tensor,
        transpose_y: bool,
    ):
        """
        forward function of FusedPPOLoss

        Args:
            hidden_states (paddle.Tensor): hidden_states, [batch_size, seq_len, hidden_size].
            lm_head_weight (paddle.Tensor): lm_head_weight, [hidden_size, vocab_size / tensor_parallel_degree].
            lm_head_bias (paddle.Tensor, optional): lm_head_bias, [vocab_size / tensor_parallel_degree].
            input_ids (paddle.Tensor): input_ids, [batch_size, seq_len].
            old_log_probs (paddle.Tensor): old_log_probs, [batch_size, seq_len-1].
            reward_advantages (paddle.Tensor): advantages, [batch_size, seq_len-1].
            sequence_mask (paddle.Tensor): mask, [batch_size, seq_len-1].
            transpose_y (bool): whether to transpose lm_head_weight.

        Returns:
            paddle.Tensor: loss

        """
        logits_next = hidden_states[:, :-1, :]
        labels_next = input_ids[:, 1:]

        if old_log_probs.shape[1] != labels_next.shape[1]:
            # labels（old_log_probs，reward_advantages，sequence_mask）的长度为 src + tgt - 1，实际长度由 sequence_mask 确定
            raise ValueError("old_log_probs and reward_advantages should have the same length")

        actor_loss = ActorFusedLoss.apply(
            hidden_states=logits_next,
            lm_head_weight=lm_head_weight,
            lm_head_bias=lm_head_bias,
            labels=labels_next,
            mask=sequence_mask,
            transpose_y=transpose_y,
            num_embeddings=self.config.vocab_size,
            tensor_parallel_degree=self.config.tensor_parallel_degree,
            tensor_parallel_output=self.config.tensor_parallel_output,
            fused_linear=False,
            loop_chunk_size=1024,  # 128,
            ignore_index=0,
            old_log_probs=old_log_probs,
            advantages=reward_advantages,
            clip_range_ratio=self.clip_range_ratio,
            clip_range_ratio_low=self.clip_range_ratio_low,
            clip_range_ratio_high=self.clip_range_ratio_high,
        )
        return actor_loss


class ActorFusedPGEntropyKLLoss(paddle.autograd.PyLayer):
    """ActorFusedPGEntropyKLLoss"""

    @staticmethod
    def forward(
        ctx,
        hidden_states: paddle.Tensor,
        weight: paddle.Tensor,
        bias: paddle.Tensor,
        sequence_mask: paddle.Tensor,
        labels: paddle.Tensor,
        old_log_probs: paddle.Tensor,
        advantages: paddle.Tensor,
        ref_log_probs: paddle.Tensor,  # 新增参考策略的log概率
        transpose_y: bool,
        vocab_size: int,
        tensor_parallel_degree: int,
        tensor_parallel_output: bool,
        pg_loss_coeff: float,
        clip_range_ratio: float,  # pg loss
        clip_range_ratio_low: float,
        clip_range_ratio_high: float,
        entropy_coeff: float,  # entropy loss
        clip_range_score: float,  # clip loss
        kl_loss_coeff: float,  # clip loss
        fused_linear: bool,
        loop_chunk_size: int,
        temperature: float,
    ):
        if ref_log_probs is None:
            kl_loss_coeff = 0.0
        if tensor_parallel_degree > 1:
            assert tensor_parallel_output, "tensor_parallel_output must be True when tensor_parallel_degree > 1."

        dtype = hidden_states.dtype

        if tensor_parallel_degree > 1 and tensor_parallel_output:
            hcg = fleet.get_hybrid_communicate_group()
            model_parallel_group = hcg.get_model_parallel_group()
            tensor_parallel_degree = hcg.get_model_parallel_world_size()

        original_shape = hidden_states.shape
        hidden_states_stop_gradient = hidden_states.stop_gradient
        hidden_states = hidden_states.reshape([-1, original_shape[-1]])
        labels = labels.reshape([-1])
        old_log_probs = old_log_probs.reshape([-1])
        advantages = advantages.reshape([-1])
        if kl_loss_coeff > 0:
            ref_log_probs = ref_log_probs.reshape([-1])
        loss_mask = sequence_mask.reshape([-1]).astype("float32")
        divisor = loss_mask.sum()

        n_tokens = hidden_states.shape[0]
        n_classes = weight.shape[0] if transpose_y else weight.shape[1]

        lm_head_weight_cast = weight.cast(dtype)
        lm_head_bias_cast = bias.cast(dtype) if bias is not None else None

        def maybe_transpose(x):
            if transpose_y:
                return x.T
            return x

        # use indices to distinguish the devices.
        if tensor_parallel_degree > 1 and tensor_parallel_output:
            rank = hcg.get_model_parallel_rank()
            per_part_size = vocab_size // tensor_parallel_degree
            indices = paddle.arange(
                rank * per_part_size,
                rank * per_part_size + n_classes,
                dtype=labels.dtype,
            ).unsqueeze(0)
        else:
            indices = paddle.arange(vocab_size, dtype=labels.dtype).unsqueeze(0)

        final_loss = paddle.zeros([1], dtype="float32")
        total_pg_loss = paddle.zeros([1], dtype="float32")
        total_entropy_loss = paddle.zeros([1], dtype="float32")
        total_kl_loss = paddle.zeros([1], dtype="float32")

        grad_lm_head_weight = paddle.zeros_like(weight) if not weight.stop_gradient else None
        grad_lm_head_bias = paddle.zeros_like(bias) if bias is not None and not bias.stop_gradient else None
        grad_hidden_states = paddle.zeros_like(hidden_states) if not hidden_states_stop_gradient else None

        for i in range(0, n_tokens, loop_chunk_size):
            chunk_slice = slice(i, min(i + loop_chunk_size, n_tokens))
            hidden_chunk = hidden_states[chunk_slice]
            labels_chunk = labels[chunk_slice]
            old_log_prob_chunk = old_log_probs[chunk_slice]
            if kl_loss_coeff > 0:
                ref_log_chunk = ref_log_probs[chunk_slice]
            advantages_chunk = advantages[chunk_slice]
            mask_chunk = loss_mask[chunk_slice]

            if fused_linear:
                logits_chunk = PF.fused_linear(
                    hidden_chunk, maybe_transpose(lm_head_weight_cast), bias=lm_head_bias_cast
                )
            else:
                logits_chunk = F.linear(hidden_chunk, maybe_transpose(lm_head_weight_cast), bias=lm_head_bias_cast)
            logits_chunk = logits_chunk.astype("float32")
            logits_chunk = logits_chunk / temperature
            # 计算交叉熵和softmax
            if tensor_parallel_degree > 1 and tensor_parallel_output:
                ce_loss_chunk, softmax_out_chunk = mp_ops._c_softmax_with_cross_entropy(
                    logits_chunk, labels_chunk, group=model_parallel_group, return_softmax=True
                )
            else:
                ce_loss_chunk = F.cross_entropy(logits_chunk, labels_chunk, reduction="none")
                softmax_out_chunk = F.softmax(logits_chunk, axis=-1)

            log_probs_chunk = -ce_loss_chunk.squeeze(axis=-1)
            labels_one_hot = labels_chunk.unsqueeze(1) == indices
            grad_logits_chunk = labels_one_hot.astype("float32") - softmax_out_chunk
            grad_logits_chunk = grad_logits_chunk / temperature

            # [1] pg loss
            ratio_chunk = paddle.exp(log_probs_chunk - old_log_prob_chunk)
            clipped_ratio_chunk = paddle.clip(
                ratio_chunk,
                min=1.0 - clip_range_ratio_low,
                max=1.0 + clip_range_ratio_high,
            )

            pg_loss1_chunk = -advantages_chunk * ratio_chunk
            pg_loss2_chunk = -advantages_chunk * clipped_ratio_chunk

            pg_loss_chunk = paddle.maximum(pg_loss1_chunk, pg_loss2_chunk) * mask_chunk

            total_pg_loss += pg_loss_chunk.sum() * pg_loss_coeff / divisor

            # gradgradgradgrad pg loss
            pg_within_clip_chunk = (
                (ratio_chunk >= 1.0 - clip_range_ratio) & (ratio_chunk <= 1.0 + clip_range_ratio)
            ).astype(dtype)

            d_pg_log_probs_chunk = (
                paddle.where(
                    pg_loss1_chunk >= pg_loss2_chunk,
                    pg_loss1_chunk,
                    pg_loss2_chunk * pg_within_clip_chunk,
                )
                * mask_chunk
                * pg_loss_coeff
                / divisor
            )

            if entropy_coeff > 0:
                # [2] entropy loss
                log_prob_chunk = paddle.log(paddle.clip(softmax_out_chunk, min=1e-12))
                entropy_loss_chunk = -(softmax_out_chunk * log_prob_chunk).sum(axis=-1) * mask_chunk
                # entropy_loss_chunk shape is [bs, seqlen, vocab_size // tensor_parallel_degree], do all_reduce sum here
                if tensor_parallel_degree > 1 and tensor_parallel_output:
                    paddle.distributed.all_reduce(
                        entropy_loss_chunk, op=paddle.distributed.ReduceOp.SUM, group=model_parallel_group
                    )
                total_entropy_loss += entropy_loss_chunk.sum() * entropy_coeff / divisor

                # gradgradgradgrad entropy loss
                # grad_softmax_out_chunk = -(log_prob_chunk + 1) * mask_chunk.unsqueeze(-1) * entropy_coeff / divisor
                # sum_term = (softmax_out_chunk * grad_softmax_out_chunk).sum(axis=-1, keepdim=True)
                # if tensor_parallel_degree > 1 and tensor_parallel_output:
                #     paddle.distributed.all_reduce(
                #         sum_term, op=paddle.distributed.ReduceOp.SUM, group=model_parallel_group
                #     )
                # d_entropy_logits_chunk = softmax_out_chunk * (grad_softmax_out_chunk - sum_term)
                H = entropy_loss_chunk.unsqueeze(-1)
                d_entropy_logits_chunk = (
                    -softmax_out_chunk * (log_prob_chunk + H) * mask_chunk.unsqueeze(-1) * entropy_coeff / divisor
                ) / temperature

            if kl_loss_coeff > 0:
                # [3] kl loss
                delta_chunk = ref_log_chunk - log_probs_chunk
                exp_delta_chunk = paddle.exp(delta_chunk)
                kl_loss_estimate_chunk = exp_delta_chunk - delta_chunk - 1
                kl_loss_clipped_chunk = (
                    paddle.clip(
                        kl_loss_estimate_chunk,
                        min=-clip_range_score,
                        max=clip_range_score,
                    )
                    * mask_chunk
                )
                total_kl_loss += kl_loss_clipped_chunk.sum() * kl_loss_coeff / divisor
                # gradgradgradgrad kl loss
                kl_within_clip_chunk = (
                    (kl_loss_estimate_chunk >= -clip_range_score) & (kl_loss_estimate_chunk <= clip_range_score)
                ).astype(dtype)
                d_kl_log_probs_chunk = (
                    (1 - exp_delta_chunk) * kl_within_clip_chunk * mask_chunk * kl_loss_coeff / divisor
                )

            d_total_logits_chunk = d_pg_log_probs_chunk.unsqueeze(-1) * grad_logits_chunk
            if entropy_coeff > 0:
                d_total_logits_chunk -= d_entropy_logits_chunk
            if kl_loss_coeff > 0:
                d_total_logits_chunk += d_kl_log_probs_chunk.unsqueeze(-1) * grad_logits_chunk

            d_total_logits_chunk = d_total_logits_chunk.cast(dtype)

            if grad_hidden_states is not None:
                grad_hidden_states[chunk_slice] = paddle.matmul(
                    d_total_logits_chunk, lm_head_weight_cast, transpose_y=not transpose_y
                )
            if grad_lm_head_weight is not None:
                if transpose_y:
                    grad_lm_head_weight += paddle.matmul(d_total_logits_chunk, hidden_chunk, transpose_x=True)
                else:
                    grad_lm_head_weight += paddle.matmul(hidden_chunk, d_total_logits_chunk, transpose_x=True)
            if grad_lm_head_bias is not None:
                grad_lm_head_bias += d_total_logits_chunk.astype("float32").sum(axis=0).astype(dtype)

        final_loss += total_pg_loss
        if entropy_coeff > 0:
            final_loss -= total_entropy_loss
        if kl_loss_coeff > 0:
            final_loss += total_kl_loss

        ctx.hidden_states_has_grad = grad_hidden_states is not None
        ctx.lm_head_weight_has_grad = grad_lm_head_weight is not None
        ctx.lm_head_bias_has_grad = grad_lm_head_bias is not None

        if ctx.hidden_states_has_grad:
            if tensor_parallel_degree > 1:
                paddle.distributed.all_reduce(
                    grad_hidden_states, op=paddle.distributed.ReduceOp.SUM, group=model_parallel_group
                )
            grad_hidden_states = grad_hidden_states.reshape(original_shape)

        ctx.save_for_backward(
            *filter(lambda x: x is not None, [grad_hidden_states, grad_lm_head_weight, grad_lm_head_bias])
        )

        return final_loss, total_pg_loss.detach(), total_entropy_loss.detach(), total_kl_loss.detach()

    @staticmethod
    def backward(ctx, grad_output, *args):
        grad_args = ctx.saved_tensor()
        idx = 0
        if ctx.hidden_states_has_grad:
            grad_hidden = grad_args[idx] * grad_output.astype(grad_args[idx].dtype)
            idx += 1
        else:
            grad_hidden = None

        if ctx.lm_head_weight_has_grad:
            grad_lm_head_weight = grad_args[idx] * grad_output.astype(grad_args[idx].dtype)
            idx += 1
        else:
            grad_lm_head_weight = None

        if ctx.lm_head_bias_has_grad:
            grad_lm_head_bias = grad_args[idx] * grad_output.astype(grad_args[idx].dtype)
            idx += 1
        else:
            grad_lm_head_bias = None

        return grad_hidden, grad_lm_head_weight, grad_lm_head_bias


def actor_fused_pg_entropy_kl_loss(
    hidden_states: paddle.Tensor,
    weight: paddle.Tensor,
    input_ids: paddle.Tensor,
    old_log_probs: paddle.Tensor,
    ref_log_probs: paddle.Tensor,
    advantages: paddle.Tensor,
    sequence_mask: paddle.Tensor,
    bias: paddle.Tensor = None,
    transpose_y: bool = False,
    fused_linear: bool = False,
    vocab_size: int = 1024,
    tensor_parallel_degree: int = 1,
    tensor_parallel_output: bool = False,
    pg_loss_coeff: float = 1.0,
    clip_range_ratio: float = 0.2,
    clip_range_ratio_low: float = None,
    clip_range_ratio_high: float = None,
    entropy_coeff: float = 0.001,
    clip_range_score: float = 10.0,
    kl_loss_coeff: float = 0.001,
    response_start: int = 0,
    loop_chunk_size: int = 1024,
    use_actor_fused_loss: bool = True,
    temperature: float = 1.0,
):
    hidden_next = hidden_states[:, response_start:-1, :]
    labels_next = input_ids[:, response_start + 1 :]

    if ref_log_probs is None:
        kl_loss_coeff = 0.0

    if use_actor_fused_loss:
        return ActorFusedLoss.apply(
            hidden_states=hidden_next,
            lm_head_weight=weight,
            lm_head_bias=bias,
            labels=labels_next,
            mask=sequence_mask,
            transpose_y=transpose_y,
            num_embeddings=vocab_size,
            old_log_probs=old_log_probs,
            ref_log_probs=ref_log_probs,
            advantages=advantages,
            tensor_parallel_degree=tensor_parallel_degree,
            tensor_parallel_output=tensor_parallel_output,
            fused_linear=fused_linear,
            loop_chunk_size=loop_chunk_size,
            clip_range_ratio=clip_range_ratio,
            clip_range_ratio_low=clip_range_ratio_low,
            clip_range_ratio_high=clip_range_ratio_high,
            clip_range_score=clip_range_score,
            kl_loss_coeff=kl_loss_coeff,
            ignore_index=-100,
            temperature=temperature,
        )

    return ActorFusedPGEntropyKLLoss.apply(
        hidden_states=hidden_next,
        weight=weight,
        bias=bias,
        sequence_mask=sequence_mask,
        labels=labels_next,
        old_log_probs=old_log_probs,
        advantages=advantages,
        ref_log_probs=ref_log_probs,
        transpose_y=transpose_y,
        vocab_size=vocab_size,
        tensor_parallel_degree=tensor_parallel_degree,
        tensor_parallel_output=tensor_parallel_output,
        pg_loss_coeff=pg_loss_coeff,
        clip_range_ratio=clip_range_ratio,  # pg loss
        clip_range_ratio_low=clip_range_ratio_low,
        clip_range_ratio_high=clip_range_ratio_high,
        entropy_coeff=entropy_coeff,  # entropy loss
        clip_range_score=clip_range_score,  # clip loss
        kl_loss_coeff=kl_loss_coeff,  # clip loss
        fused_linear=fused_linear,
        loop_chunk_size=loop_chunk_size,
        temperature=temperature,
    )
