# Copyright (c) 2024, NVIDIA CORPORATION.  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.
import paddle
import paddle.distributed.fleet as fleet

try:
    from paddle.nn.layer.layers import in_declarative_mode
except:
    from paddle.fluid.dygraph.base import in_declarative_mode
import paddle.distributed as dist
from paddle.autograd import PyLayer

from paddlenlp.utils.tools import get_env_device


def parallel_matmul(lm_output, logit_weights, tensor_parallel_output=True, training=True):
    """
    Parallel matmul
    Args:
        lm_output: x for matmul
        logit_weights: y for matmul
        tensor_parallel_output: the output is paralleled or not
        training: args for xpu

    Returns:
        rst for matmul
    """
    if get_env_device() == "xpu":
        try:
            from paddle_xpu.layers.nn import parallel_matmul as xpu_parallel_matmul

            xpu_parallel_matmul = xpu_parallel_matmul()
            logits = xpu_parallel_matmul(
                lm_output,
                logit_weights,
                tensor_parallel_output=tensor_parallel_output,
                training=training,
            )
            return logits
        except ImportError:
            pass

    is_fleet_init = True
    tensor_parallel_degree = 1
    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:
        is_fleet_init = False

    is_logit_weight_distributed = logit_weights.is_distributed
    #  `is_distributed` in static mode is always False
    if in_declarative_mode() and tensor_parallel_degree > 1:
        is_logit_weight_distributed = True

    if is_fleet_init and tensor_parallel_degree > 1 and is_logit_weight_distributed:
        input_parallel = paddle.distributed.collective._c_identity(lm_output, group=model_parallel_group)
        logits = paddle.matmul(input_parallel, logit_weights, transpose_y=True)

        if tensor_parallel_output:
            return logits

        return paddle.distributed.collective._c_concat(logits, group=model_parallel_group)
    else:
        logits = paddle.matmul(lm_output, logit_weights, transpose_y=True)
        return logits


def parallel_linear(lm_output, logit_weights, bias, tensor_parallel_output=True):
    is_fleet_init = True
    tensor_parallel_degree = 1
    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:
        is_fleet_init = False

    is_logit_weight_distributed = logit_weights.is_distributed
    #  `is_distributed` in static mode is always False
    if in_declarative_mode() and tensor_parallel_degree > 1:
        is_logit_weight_distributed = True

    if is_fleet_init and tensor_parallel_degree > 1 and is_logit_weight_distributed:
        input_parallel = paddle.distributed.collective._c_identity(lm_output, group=model_parallel_group)
        bias_parallel = paddle.distributed.collective._c_identity(bias, group=model_parallel_group)
        logits = paddle.matmul(input_parallel, logit_weights)
        logits += bias_parallel

        if tensor_parallel_output:
            return logits

        return paddle.distributed.collective._c_concat(logits, group=model_parallel_group)
    else:
        logits = paddle.matmul(lm_output, logit_weights)
        logits += bias
        return logits


def fused_head_and_loss_fn(
    hidden_states,
    lm_head_weight,
    lm_head_bias,
    labels,
    loss_mask,
    transpose_y,
    num_embeddings,
    tensor_parallel_degree,
    tensor_parallel_output,
    fused_linear,
    loop_chunk_size,
    return_token_loss,
    ignore_index,
):
    """Run FusedHeadAndCrossEntropy."""
    return FusedHeadAndCrossEntropy.apply(
        hidden_states,
        lm_head_weight,
        lm_head_bias,
        labels,
        loss_mask,
        transpose_y,
        num_embeddings,
        tensor_parallel_degree,
        tensor_parallel_output,
        fused_linear,
        loop_chunk_size,
        return_token_loss,
        ignore_index,
    )


class FusedHeadAndCrossEntropy(PyLayer):
    """Fuse LM Head and CrossEntropyLoss into one module."""

    @staticmethod
    def forward(
        ctx,
        hidden_states: paddle.Tensor,
        lm_head_weight: paddle.Tensor,
        lm_head_bias: paddle.Tensor,
        labels: paddle.Tensor,
        loss_mask: paddle.Tensor,
        transpose_y: bool,
        num_embeddings: int,
        tensor_parallel_degree: int,
        tensor_parallel_output: bool,
        fused_linear: bool,
        loop_chunk_size: int,
        return_token_loss: bool,
        ignore_index: int,
    ):
        """Run blockwise parallel cross entropy calculation.

        Args:
            ctx: PyLayerContext
            hidden_states (`paddle.Tensor` of shape `(batch_size, max_seq_len, hidden_size)`): the input features.
            lm_head_weight (`paddle.Tensor` of shape `(hidden_size, vocab_size)`)
            lm_head_bias (`paddle.Tensor` of shape `(vocab_size)`)
            labels (`paddle.Tensor` of shape `(batch_size, max_seq_len)`)
            loss_mask (`paddle.Tensor` of shape `(batch_size, max_seq_len)`)
            transpose_y: bool
            num_embeddings: int
            tensor_parallel_degree: int
            tensor_parallel_output: bool
            fused_linear: bool
            loop_chunk_size: int, default is LOOP_CHUNK_SIZE
            return_token_loss: bool
            ignore_index: int

        Returns:
            loss (`paddle.Tensor` of shape `()`: the output loss.
        """
        if fused_linear:
            # print("Cannot support fused_linear while using use_fused_head_and_loss_fn now!")
            fused_linear = False  # NOTE(hehuang): Cannot support fused_linear now
        # initialize distributed settings
        dtype = hidden_states.dtype
        if tensor_parallel_degree > 1:
            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 = hidden_states.reshape([-1, original_shape[-1]])
        labels = labels.reshape([-1])
        if loss_mask is None:
            ctx.aux_num = 1
            loss_mask = (labels != ignore_index).astype("float32")
        else:
            ctx.aux_num = 2
            loss_mask = loss_mask.reshape([-1]).astype("float32")
        ctx.return_token_loss = return_token_loss
        if return_token_loss:
            divisor = 1
        else:
            divisor = loss_mask.sum()

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

        # cast lm_head weight & bias
        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)

        # initialize indices for labels_one_hot
        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 gradients
        if not return_token_loss:
            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_weight is not None and not lm_head_weight.stop_gradient:
                grad_lm_head_bias = paddle.zeros_like(lm_head_bias)
            else:
                grad_lm_head_bias = None
            if hidden_states.stop_gradient:
                grad_hidden_states = paddle.zeros_like(hidden_states)
            else:
                grad_hidden_states = None

        # initialize outputs
        token_loss = paddle.empty((n_tokens,), dtype=paddle.float32)

        # blockwise calculations
        for i in range(0, n_tokens, loop_chunk_size):
            token_start_idx = i
            token_end_idx = min(i + loop_chunk_size, n_tokens)
            cur_chunk_range = paddle.arange(token_start_idx, token_end_idx)
            hidden_states_chunk = paddle.gather(hidden_states, cur_chunk_range, axis=0)
            labels_chunk = paddle.gather(labels, cur_chunk_range, axis=0)
            loss_mask_chunk = paddle.gather(loss_mask, cur_chunk_range, axis=0)

            # logits calculations
            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
            if tensor_parallel_degree > 1 and not tensor_parallel_output:
                logits_chunk_cast_lst = []
                dist.all_gather(
                    logits_chunk_cast_lst,
                    logits_chunk_cast,
                    group=model_parallel_group,
                )
                logits_chunk_cast = paddle.concat(logits_chunk_cast_lst, axis=-1)
            logits_chunk = logits_chunk_cast.astype("float32")

            # log softmax
            max_logits = paddle.max(logits_chunk, axis=-1, keepdim=True)
            if tensor_parallel_degree > 1 and tensor_parallel_output:
                dist.all_reduce(max_logits, op=dist.ReduceOp.MAX, group=model_parallel_group)
            normalized_logits = logits_chunk - max_logits
            exp_logits = paddle.exp(normalized_logits)
            sum_exp_logits = paddle.sum(exp_logits, axis=-1, keepdim=True)
            if tensor_parallel_degree > 1 and tensor_parallel_output:
                dist.all_reduce(
                    sum_exp_logits,
                    op=dist.ReduceOp.SUM,
                    group=model_parallel_group,
                )
            log_sum_exp_logits = paddle.log(sum_exp_logits)

            # cross entropy
            labels_one_hot = labels_chunk.unsqueeze(1) == indices
            label_logits = paddle.sum(
                paddle.where(
                    labels_one_hot,
                    normalized_logits,
                    paddle.zeros_like(normalized_logits),
                ),
                axis=-1,
                keepdim=True,
            )
            if tensor_parallel_degree > 1 and tensor_parallel_output:
                dist.all_reduce(
                    label_logits,
                    op=dist.ReduceOp.SUM,
                    group=model_parallel_group,
                )
            token_loss_chunk = (log_sum_exp_logits - label_logits).squeeze(1) / divisor
            cond = loss_mask_chunk.astype("bool")
            token_loss_chunk = paddle.where(cond, token_loss_chunk, paddle.zeros_like(token_loss_chunk))
            paddle.scatter_(token_loss, cur_chunk_range, token_loss_chunk, overwrite=True)

            # gradients calculations
            if not return_token_loss:
                if tensor_parallel_degree > 1 and not tensor_parallel_output:
                    exp_logits = exp_logits.split(model_parallel_group.nranks, axis=-1)[model_parallel_group.rank]
                    labels_one_hot = labels_one_hot.split(model_parallel_group.nranks, axis=-1)[
                        model_parallel_group.rank
                    ]
                grad_logits_chunk = (exp_logits / sum_exp_logits - labels_one_hot.astype("float32")) / divisor
                grad_logits_chunk = grad_logits_chunk.astype(dtype)
                grad_logits_chunk = paddle.where(
                    cond.unsqueeze(1),
                    grad_logits_chunk,
                    paddle.zeros_like(grad_logits_chunk),
                )

                if grad_hidden_states is not None:
                    paddle.scatter_(
                        grad_hidden_states,
                        cur_chunk_range,
                        paddle.matmul(grad_logits_chunk, lm_head_weight_cast, transpose_y=not transpose_y),
                        overwrite=True,
                    )
                if grad_lm_head_weight is not None:
                    if transpose_y:
                        grad_lm_head_weight += paddle.matmul(
                            grad_logits_chunk,
                            hidden_states_chunk,
                            transpose_x=True,
                        )
                    else:
                        grad_lm_head_weight += paddle.matmul(
                            hidden_states_chunk,
                            grad_logits_chunk,
                            transpose_x=True,
                        )
                if grad_lm_head_bias is not None:
                    grad_lm_head_bias += grad_logits_chunk.astype("float32").sum(axis=0).astype(dtype)

        if return_token_loss:
            loss = token_loss.reshape(original_shape[:-1])

            ctx.save_for_backward(
                hidden_states,
                lm_head_weight,
                lm_head_bias,
                labels,
                loss_mask,
            )
            ctx.transpose_y = transpose_y
            ctx.num_embeddings = num_embeddings
            ctx.loop_chunk_size = loop_chunk_size
            ctx.tensor_parallel_degree = tensor_parallel_degree
            ctx.tensor_parallel_output = tensor_parallel_output

            ctx.original_shape = original_shape
        else:
            loss = token_loss.sum()

            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 loss

    @staticmethod
    def backward(ctx, grad_output):
        """Run the backward of blockwise parallel cross entropy calculation."""
        if not ctx.return_token_loss:
            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

            if ctx.aux_num == 1:
                return (
                    grad_hidden_states,
                    grad_lm_head_weight,
                    grad_lm_head_bias,
                    None,
                )
            else:
                return (
                    grad_hidden_states,
                    grad_lm_head_weight,
                    grad_lm_head_bias,
                    None,
                    None,
                )

        # return_token_loss = True
        grad_token_loss = grad_output.reshape([-1])
        (
            hidden_states,
            lm_head_weight,
            lm_head_bias,
            labels,
            loss_mask,
        ) = ctx.saved_tensor()
        transpose_y = ctx.transpose_y
        num_embeddings = ctx.num_embeddings
        loop_chunk_size = ctx.loop_chunk_size
        tensor_parallel_degree = ctx.tensor_parallel_degree
        tensor_parallel_output = ctx.tensor_parallel_output

        # initialize distributed settings
        dtype = hidden_states.dtype
        if tensor_parallel_degree > 1:
            hcg = fleet.get_hybrid_communicate_group()
            model_parallel_group = hcg.get_model_parallel_group()
            tensor_parallel_degree = hcg.get_model_parallel_world_size()

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

        # cast lm_head weight & bias
        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)

        # initialize indices for labels_one_hot
        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 gradients
        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_weight is not None and not lm_head_weight.stop_gradient:
            grad_lm_head_bias = paddle.zeros_like(lm_head_bias)
        else:
            grad_lm_head_bias = None
        if hidden_states.stop_gradient:
            grad_hidden_states = paddle.zeros_like(hidden_states)
        else:
            grad_hidden_states = None

        # blockwise calculations
        for i in range(0, n_tokens, loop_chunk_size):
            token_start_idx = i
            token_end_idx = min(i + loop_chunk_size, n_tokens)
            cur_chunk_range = paddle.arange(token_start_idx, token_end_idx)
            hidden_states_chunk = paddle.gather(hidden_states, cur_chunk_range, axis=0)
            labels_chunk = paddle.gather(labels, cur_chunk_range, axis=0)
            loss_mask_chunk = paddle.gather(loss_mask, cur_chunk_range, axis=0)

            # logits calculations
            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
            if tensor_parallel_degree > 1 and not tensor_parallel_output:
                logits_chunk_cast_lst = []
                dist.all_gather(
                    logits_chunk_cast_lst,
                    logits_chunk_cast,
                    group=model_parallel_group,
                )
                logits_chunk_cast = paddle.concat(logits_chunk_cast_lst, axis=-1)
            logits_chunk = logits_chunk_cast.astype("float32")

            # log softmax
            max_logits = paddle.max(logits_chunk, axis=-1, keepdim=True)
            if tensor_parallel_degree > 1 and tensor_parallel_output:
                dist.all_reduce(max_logits, op=dist.ReduceOp.MAX, group=model_parallel_group)
            normalized_logits = logits_chunk - max_logits
            exp_logits = paddle.exp(normalized_logits)
            sum_exp_logits = paddle.sum(exp_logits, axis=-1, keepdim=True)
            if tensor_parallel_degree > 1 and tensor_parallel_output:
                dist.all_reduce(
                    sum_exp_logits,
                    op=dist.ReduceOp.SUM,
                    group=model_parallel_group,
                )

            labels_one_hot = labels_chunk.unsqueeze(1) == indices
            if tensor_parallel_degree > 1 and not tensor_parallel_output:
                exp_logits = exp_logits.split(model_parallel_group.nranks, axis=-1)[model_parallel_group.rank]
                labels_one_hot = labels_one_hot.split(model_parallel_group.nranks, axis=-1)[model_parallel_group.rank]
            grad_logits_chunk = exp_logits / sum_exp_logits - labels_one_hot.astype("float32")
            # NOTE(hehuang): scaling grad_logits_chunk by grad_token_loss
            grad_logits_chunk *= paddle.gather(grad_token_loss, cur_chunk_range, axis=0).unsqueeze(1)
            grad_logits_chunk = grad_logits_chunk.astype(dtype)
            cond = loss_mask_chunk.astype("bool")
            grad_logits_chunk = paddle.where(
                cond.unsqueeze(1),
                grad_logits_chunk,
                paddle.zeros_like(grad_logits_chunk),
            )

            if grad_hidden_states is not None:
                paddle.scatter_(
                    grad_hidden_states,
                    cur_chunk_range,
                    paddle.matmul(grad_logits_chunk, lm_head_weight_cast, transpose_y=not transpose_y),
                    overwrite=True,
                )
            if grad_lm_head_weight is not None:
                if transpose_y:
                    grad_lm_head_weight += paddle.matmul(grad_logits_chunk, hidden_states_chunk, transpose_x=True)
                else:
                    grad_lm_head_weight += paddle.matmul(hidden_states_chunk, grad_logits_chunk, transpose_x=True)
            if grad_lm_head_bias is not None:
                grad_lm_head_bias += grad_logits_chunk.astype("float32").sum(axis=0).astype(dtype)

        if grad_hidden_states is not None:
            if tensor_parallel_degree > 1:
                dist.all_reduce(
                    grad_hidden_states,
                    op=dist.ReduceOp.SUM,
                    group=model_parallel_group,
                )
            grad_hidden_states = grad_hidden_states.reshape(ctx.original_shape)

        if ctx.aux_num == 1:
            return (
                grad_hidden_states,
                grad_lm_head_weight,
                grad_lm_head_bias,
                None,
            )
        else:
            return (
                grad_hidden_states,
                grad_lm_head_weight,
                grad_lm_head_bias,
                None,
                None,
            )
