# Copyright (c) 2025 PaddlePaddle Authors. 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.nn as nn
from paddle.autograd import PyLayer
from paddle.distributed import fleet
from paddle.distributed.fleet.base import topology as tp
from paddle.distributed.fleet.layers.mpu import mp_ops
from paddle.distributed.fleet.utils.sequence_parallel_utils import (
    AllGatherOp,
    ReduceScatterOp,
)
from paddle.nn.quant import llm_int8_linear, weight_dequantize, weight_only_linear

from paddlenlp.utils import infohub

from .qat_utils import QATFunc

try:
    from .qlora import qlora_weight_dequantize, qlora_weight_linear
except:
    qlora_weight_linear = None
    qlora_weight_dequantize = None

QuantMapping = {
    # (quant_dtype, quant_weight_bit)
    "weight_only_int8": ("int8", 8),
    "weight_only_int4": ("int4", 4),
    "llm.int8": ("int8", 8),
    "fp4": ("fp4", 4),
    "nf4": ("nf4", 4),
    "a8w8linear": ("int8", 8),
    "a8w4linear": ("int8", 8),
    "fp8linear": ("fp8", 8),
}


def quant_weight_forward(
    x,
    quant_weight,
    bias,
    quant_scale,
    quant_state,
    quant_dtype,
    quantization_config,
    weight_quantize_algo,
    dtype,
):
    if weight_quantize_algo in ["weight_only_int8", "weight_only_int4"]:
        output = weight_only_linear(
            x=x,
            weight=quant_weight,
            bias=bias,
            weight_scale=quant_scale,
            weight_dtype=quant_dtype,
            group_size=quantization_config.group_size,
        )
    elif weight_quantize_algo in ["llm.int8"]:
        output = llm_int8_linear(x, quant_weight, bias, quant_scale, quantization_config.llm_int8_threshold)
    elif weight_quantize_algo in ["fp4", "nf4"]:
        output = qlora_weight_linear(
            x=x,
            quant_weight=quant_weight,
            dtype=dtype,
            state=quant_state if quantization_config.qlora_weight_double_quant else quant_scale,
            quant_algo=weight_quantize_algo,
            double_quant=quantization_config.qlora_weight_double_quant,
            block_size=quantization_config.qlora_weight_blocksize,
            double_quant_block_size=quantization_config.qlora_weight_double_quant_block_size,
            bias=bias,
        )

    return output


def dequant_weight(
    quant_weight,
    quantization_config,
    weight_quantize_algo,
    dtype,
    quant_scale,
    quant_state,
    input_shape,
):
    if weight_quantize_algo in ["weight_only_int8", "weight_only_int4", "llm.int8"]:
        quant_dequant_weight = weight_dequantize(
            x=quant_weight,
            scale=quant_scale,
            algo=weight_quantize_algo,
            out_dtype=dtype,
            group_size=quantization_config.group_size,
        )
    elif weight_quantize_algo in ["fp4", "nf4"]:
        quant_dequant_weight = (
            qlora_weight_dequantize(
                quant_weight=quant_weight,
                quant_algo=weight_quantize_algo,
                state=quant_state if quantization_config.qlora_weight_double_quant else quant_scale,
                double_quant=quantization_config.qlora_weight_double_quant,
                block_size=quantization_config.qlora_weight_blocksize,
                double_quant_block_size=quantization_config.qlora_weight_double_quant_block_size,
            )
            .reshape([input_shape[-1], -1])
            .cast(dtype)
        )
    return quant_dequant_weight


class QuantizationLinearFunc(PyLayer):
    @staticmethod
    def forward(
        ctx,
        x,
        quant_weight,
        bias,
        quant_scale,
        quant_state,
        quant_dtype,
        quantization_config,
        weight_quantize_algo,
        dtype,
    ):

        output = quant_weight_forward(
            x=x,
            quant_weight=quant_weight,
            bias=bias,
            quant_scale=quant_scale,
            quant_state=quant_state,
            quant_dtype=quant_dtype,
            quantization_config=quantization_config,
            weight_quantize_algo=weight_quantize_algo,
            dtype=dtype,
        )
        ctx.quant_dtype = quant_dtype
        ctx.quantization_config = quantization_config
        ctx.weight_quantize_algo = weight_quantize_algo
        ctx.dtype = dtype
        if ctx.weight_quantize_algo in ["fp4", "nf4"] and ctx.quantization_config.qlora_weight_double_quant:
            qquant_scale, double_quant_scale, quant_scale_offset = quant_state
            ctx.save_for_backward(x, quant_weight, bias, qquant_scale, double_quant_scale, quant_scale_offset)
        else:
            ctx.save_for_backward(x, quant_weight, bias, quant_scale)
        return output

    @staticmethod
    def backward(ctx, grad_output):
        if ctx.weight_quantize_algo in ["fp4", "nf4"] and ctx.quantization_config.qlora_weight_double_quant:
            x, quant_weight, bias, qquant_scale, double_quant_scale, quant_scale_offset = ctx.saved_tensor()
            quant_state = (qquant_scale, double_quant_scale, quant_scale_offset)
            quant_scale = None
        else:
            x, quant_weight, bias, quant_scale = ctx.saved_tensor()
            quant_state = None

        qdq_weight = dequant_weight(
            quant_weight=quant_weight,
            quantization_config=ctx.quantization_config,
            weight_quantize_algo=ctx.weight_quantize_algo,
            dtype=ctx.dtype,
            quant_scale=quant_scale,
            quant_state=quant_state,
            input_shape=x.shape,
        )

        if not x.stop_gradient:
            input_grad = paddle.matmul(grad_output, qdq_weight.T)
        else:
            input_grad = None

        if not quant_weight.stop_gradient:
            if len(x.shape) == 2:
                weight_grad = paddle.matmul(x.transpose([1, 0]), grad_output)
            else:
                weight_grad = paddle.matmul(
                    x.reshape([-1, x.shape[-1]]).transpose([1, 0]), grad_output.reshape([-1, grad_output.shape[-1]])
                )
        else:
            weight_grad = None

        if bias is not None and not bias.stop_gradient:
            bias_grad = grad_output.sum(axis=[0, 1])
        else:
            bias_grad = None

        return input_grad, weight_grad, bias_grad


def quant_weight_linear(
    x,
    quant_weight,
    quant_dtype,
    quantization_config,
    weight_quantize_algo,
    dtype,
    quant_scale=None,
    quant_state=None,
    bias=None,
    act_state=None,
):
    if weight_quantize_algo in ["a8w8linear", "a8w4linear", "fp8linear"]:

        state, training, act_scale, group = act_state

        return QATFunc.apply(
            x,
            quant_weight,
            bias,
            quant_scale,
            quantization_config,
            dtype,
            state,
            training,
            act_scale,
            weight_quantize_algo,
            group,
        )
    else:
        return QuantizationLinearFunc.apply(
            x,
            quant_weight,
            bias,
            quant_scale,
            quant_state,
            quant_dtype,
            quantization_config,
            weight_quantize_algo,
            dtype,
        )


def get_act_scale_group(is_row=False):
    if paddle.distributed.is_initialized():
        if getattr(infohub, "scale_group") is None:
            hcg = fleet.get_hybrid_communicate_group()
            rank = hcg._dp_degree * hcg._sharding_degree
            group_no_row = hcg.create_fuse_group(["data", "sharding"])[1] if rank > 1 else None
            rank *= hcg._mp_degree
            group_row = hcg.create_fuse_group(["data", "sharding", "model"])[1] if rank > 1 else None

            setattr(infohub, "scale_group", [group_no_row, group_row])
        group = infohub.scale_group[1] if is_row else infohub.scale_group[0]
    else:
        group = None
    return group


class QuantizationLinear(nn.Layer):
    """Quantization Linear layer."""

    def __init__(
        self,
        in_features,
        out_features,
        quantization_config,
        weight_quantize_algo,
        dtype,
        bias_attr=None,
        mp_moe=False,
        is_distributed=False,
    ):
        super().__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.quantization_config = quantization_config
        self.weight_quantize_algo = weight_quantize_algo
        self._dtype = dtype
        self.quant_dtype, self.quant_weight_bit = QuantMapping[self.weight_quantize_algo]
        self.state = 0

        # PaddlePaddle doesn't support 4bit data type, one 8bit data represents two 4bit data.
        # paddle.nn.quant.weight_quantize will transpose in_features and out_features.
        if self.weight_quantize_algo in [
            "weight_only_int8",
            "weight_only_int4",
            "llm.int8",
            "a8w8linear",
            "a8w4linear",
            "fp8linear",
        ]:
            self.quant_weight = self.create_parameter(
                shape=[out_features // 2, in_features] if self.quant_weight_bit == 4 else [out_features, in_features],
                dtype="int8",
                is_bias=False,
            )
            if self.quantization_config.group_size == -1:
                self.quant_scale = self.create_parameter(
                    shape=[out_features] if self.weight_quantize_algo not in ["fp8linear"] else [1],
                    dtype=self._dtype,
                    is_bias=False,
                )
                self.quant_scale.stop_gradient = True
            else:
                # TODO(lugimzzz): support groupwise in next PR
                raise NotImplementedError("Not yet support grouwise weightonly quantization.")
            if self.weight_quantize_algo in ["a8w8linear", "a8w4linear", "fp8linear"]:
                self.act_scale = self.create_parameter(
                    shape=[1], dtype=self._dtype, is_bias=False, default_initializer=nn.initializer.Constant(value=0.0)
                )
                self.act_scale.stop_gradient = True
                self.group = get_act_scale_group()

        elif self.weight_quantize_algo in ["fp4", "nf4"]:
            if qlora_weight_linear is None:
                raise ImportError(
                    "Please run the following commands to install: qlora related package first\n"
                    "1) git clone https://github.com/PaddlePaddle/PaddleSlim \n"
                    "2) cd PaddleSlim && pip install -e .\n"
                    "3) cd csrc &&  python ./setup_cuda.py install"
                )
            self.quant_weight = self.create_parameter(
                shape=[out_features * in_features // 2, 1],
                attr=paddle.nn.initializer.Constant(value=0),
                dtype="uint8",
                is_bias=False,
            )
            if self.quantization_config.qlora_weight_double_quant:
                # quantized quant_scale
                self.qquant_scale = self.create_parameter(
                    shape=[in_features * out_features // self.quantization_config.qlora_weight_blocksize],
                    dtype="uint8",
                    is_bias=False,
                )
                # double quant_scale: quant_scale of quantized quant_scale
                self.double_quant_scale = self.create_parameter(
                    shape=[
                        in_features
                        * out_features
                        // self.quantization_config.qlora_weight_blocksize
                        // self.quantization_config.qlora_weight_double_quant_block_size
                    ],
                    dtype="float32",
                    is_bias=False,
                )
                self.quant_scale_offset = self.create_parameter(
                    shape=[],
                    dtype="float32",
                    is_bias=False,
                )
            else:
                self.quant_scale = self.create_parameter(
                    shape=[in_features * out_features // self.quantization_config.qlora_weight_blocksize],
                    dtype="float32",
                    is_bias=False,
                )
        else:
            raise NotImplementedError(f"Not yet support weight_quantize_algo: {self.weight_quantize_algo}")
        if bias_attr is False:
            self.bias = None
        else:
            self.bias = self.create_parameter(
                shape=[out_features],
                attr=bias_attr,
                dtype=self._dtype,
                is_bias=True,
            )
        if mp_moe or is_distributed:
            for p in self.parameters():
                p.is_distributed = is_distributed
                p.mp_moe = mp_moe
        self.quant_weight.weight_quantize_algo = self.weight_quantize_algo

    def forward(self, x):
        output = quant_weight_linear(
            x=x,
            quant_weight=self.quant_weight,
            quant_dtype=self.quant_dtype,
            quantization_config=self.quantization_config,
            weight_quantize_algo=self.weight_quantize_algo,
            dtype=self._dtype,
            quant_scale=self.quant_scale,
            quant_state=(self.qquant_scale, self.double_quant_scale, self.quant_scale_offset)
            if (self.weight_quantize_algo in ["fp4", "nf4"] and self.quantization_config.qlora_weight_double_quant)
            else None,
            bias=self.bias,
            act_state=(self.state, self.training, self.act_scale, self.group)
            if self.weight_quantize_algo in ["a8w8linear", "a8w4linear", "fp8linear"]
            else None,
        )
        if self.training:
            self.state += 1
        return output


class ColumnParallelQuantizationLinear(nn.Layer):
    """Quantization Linear layer with mp parallelized(column).
    The code implementation refers to paddle.distributed.fleet.meta_parallel.ColumnParallelLinear.
    https://github.com/PaddlePaddle/Paddle/blob/develop/python/paddle/distributed/fleet/layers/mpu/mp_layers.py#L310
    Different from ColumnParallelLinear, this class keeps weight in INT8/INT4 with quant scale, and supports matrix
    multiplication(weight_only_linear/llm_int8_linear) for input tensor(fp16/bf16) and quantized weight(INT8/INT4)
    and bias addition if provided.
    Notice: quantized weight shape is transposed of weight shape in ColumnParallelLinear.
    """

    def __init__(
        self,
        in_features,
        output_size_per_partition,
        quantization_config,
        weight_quantize_algo,
        dtype,
        bias_attr=None,
        gather_output=True,
        mp_skip_c_identity=False,
        mp_group=None,
        sequence_parallel=False,
    ):
        super().__init__()
        self.in_features = in_features
        self.output_size_per_partition = output_size_per_partition
        self.weight_quantize_algo = weight_quantize_algo
        self.quantization_config = quantization_config
        self._dtype = dtype
        self.mp_skip_c_identity = mp_skip_c_identity
        self.quant_dtype, self.quant_weight_bit = QuantMapping[self.weight_quantize_algo]
        self.state = 0
        self.model_parallel_group = (
            tp._HYBRID_PARALLEL_GROUP.get_model_parallel_group() if mp_group is None else mp_group
        )
        self.world_size = (
            tp._HYBRID_PARALLEL_GROUP.get_model_parallel_world_size() if mp_group is None else mp_group.nranks
        )
        self.is_mp = self.world_size > 1
        self.gather_output = gather_output
        self.sequence_parallel = sequence_parallel
        if self.sequence_parallel and self.gather_output:
            raise ValueError("Sequence parallel does not support gather_output")

        # PaddlePaddle doesn't support Int4 data type, one Int8 data represents two Int4 data.
        if self.weight_quantize_algo in [
            "weight_only_int8",
            "weight_only_int4",
            "llm.int8",
            "a8w8linear",
            "a8w4linear",
            "fp8linear",
        ]:
            self.quant_weight = self.create_parameter(
                shape=[self.output_size_per_partition // 2, in_features]
                if self.quant_dtype == "int4"
                else [self.output_size_per_partition, in_features],
                dtype="int8",
                is_bias=False,
            )
            self.quant_weight.is_distributed = True if self.is_mp else False
            if self.quant_weight.is_distributed:
                self.quant_weight.split_axis = 0

            if self.quantization_config.group_size == -1:
                self.quant_scale = self.create_parameter(
                    shape=[self.output_size_per_partition] if self.weight_quantize_algo not in ["fp8linear"] else [1],
                    dtype=self._dtype,
                    is_bias=False,
                )
                self.quant_scale.stop_gradient = True
                if self.weight_quantize_algo in ["fp8linear", "a8w4linear", "a8w8linear"]:
                    self.quant_scale.is_distributed = False
                else:
                    self.quant_scale.is_distributed = True if self.is_mp else False
                if self.quant_scale.is_distributed:
                    self.quant_scale.split_axis = 0
            else:
                # TODO(lugimzzz): support groupwise in next PR
                raise NotImplementedError("Not yet support grouwise weightonly quantization.")
            if self.weight_quantize_algo in ["a8w8linear", "a8w4linear", "fp8linear"]:
                self.act_scale = self.create_parameter(
                    shape=[1], dtype=self._dtype, is_bias=False, default_initializer=nn.initializer.Constant(value=0.0)
                )
                self.act_scale.is_distributed = False
                self.act_scale.stop_gradient = True
                self.group = get_act_scale_group()
        else:
            raise NotImplementedError(f"Not yet support weight_quantize_algo: {self.weight_quantize_algo}")
        if bias_attr is False:
            self.bias = None
        else:
            self.bias = self.create_parameter(
                shape=[self.output_size_per_partition],
                attr=bias_attr,
                dtype=self._dtype,
                is_bias=True,
            )
            self.bias.is_distributed = True if self.is_mp else False
            if self.bias.is_distributed:
                self.bias.split_axis = 0
        self.quant_weight.weight_quantize_algo = self.weight_quantize_algo

    def forward(self, x):
        if self.is_mp:
            if self.sequence_parallel:
                input_parallel = AllGatherOp.apply(x)
            else:
                input_parallel = mp_ops._c_identity(
                    x,
                    group=self.model_parallel_group,
                    skip_c_identity_dynamic=self.mp_skip_c_identity,
                )
        else:
            input_parallel = x

        output_parallel = quant_weight_linear(
            x=input_parallel,
            quant_weight=self.quant_weight,
            quant_dtype=self.quant_dtype,
            quantization_config=self.quantization_config,
            weight_quantize_algo=self.weight_quantize_algo,
            dtype=self._dtype,
            quant_scale=self.quant_scale,
            quant_state=(self.qquant_scale, self.double_quant_scale, self.quant_scale_offset)
            if (self.weight_quantize_algo in ["fp4", "nf4"] and self.quantization_config.qlora_weight_double_quant)
            else None,
            bias=self.bias,
            act_state=(self.state, self.training, self.act_scale, self.group)
            if self.weight_quantize_algo in ["a8w8linear", "a8w4linear", "fp8linear"]
            else None,
        )
        if self.training:
            self.state += 1

        if self.gather_output and self.is_mp:
            output = mp_ops._c_concat(output_parallel, group=self.model_parallel_group)
        else:
            output = output_parallel
        return output


class RowParallelQuantizationLinear(nn.Layer):
    """Quantization Linear layer with mp parallelized(row).
    The code implementation refers to paddle.distributed.fleet.meta_parallel.RowParallelLinear.
    https://github.com/PaddlePaddle/Paddle/blob/develop/python/paddle/distributed/fleet/layers/mpu/mp_layers.py#L517
    Different from RowParallelLinear, this class keeps weight in INT8/INT4 with quant scale, and supports matrix
    multiplication(weight_only_linear/llm_int8_linear) for input tensor(fp16/bf16) and quantized weight(INT8/INT4)
    and bias addition if provided.
    Notice: quantized weight shape is transposed of weight shape in RowParallelLinear.
    """

    def __init__(
        self,
        input_size_per_partition,
        out_features,
        quantization_config,
        weight_quantize_algo,
        dtype,
        bias_attr=None,
        input_is_parallel=False,
        mp_skip_c_identity=False,
        mp_group=None,
        sequence_parallel=False,
    ):
        super().__init__()
        self.input_size_per_partition = input_size_per_partition
        self.out_features = out_features
        self.quantization_config = quantization_config
        self.weight_quantize_algo = weight_quantize_algo
        self._dtype = dtype
        self.mp_skip_c_identity = mp_skip_c_identity
        self.quant_dtype, self.quant_weight_bit = QuantMapping[self.weight_quantize_algo]
        self.state = 0

        self.model_parallel_group = (
            tp._HYBRID_PARALLEL_GROUP.get_model_parallel_group() if mp_group is None else mp_group
        )
        self.world_size = (
            tp._HYBRID_PARALLEL_GROUP.get_model_parallel_world_size() if mp_group is None else mp_group.nranks
        )
        self.is_mp = self.world_size > 1
        self.input_is_parallel = input_is_parallel
        self.sequence_parallel = sequence_parallel
        if not self.input_is_parallel and self.sequence_parallel:
            raise ValueError("Sequence parallel only support input_is_parallel.")

        # PaddlePaddle doesn't support Int4 data type, one Int8 data represents two Int4 data.
        # paddle.nn.quant.weight_quantize will transpose in_features and out_features.
        if self.weight_quantize_algo in [
            "weight_only_int8",
            "weight_only_int4",
            "llm.int8",
            "a8w8linear",
            "a8w4linear",
            "fp8linear",
        ]:
            self.quant_weight = self.create_parameter(
                shape=[out_features // 2, self.input_size_per_partition]
                if self.quant_dtype == "int4"
                else [out_features, self.input_size_per_partition],
                dtype="int8",
                is_bias=False,
            )
            self.quant_weight.is_distributed = True if self.is_mp else False
            if self.quant_weight.is_distributed:
                self.quant_weight.split_axis = 1

            if self.quantization_config.group_size == -1:
                self.quant_scale = self.create_parameter(
                    shape=[out_features] if self.weight_quantize_algo not in ["fp8linear"] else [1],
                    dtype=self._dtype,
                    is_bias=False,
                )
                self.quant_scale.stop_gradient = True
                if self.weight_quantize_algo in ["fp8linear", "a8w4linear", "a8w8linear"]:
                    self.quant_scale.is_distributed = False
                else:
                    self.quant_scale.is_distributed = True if self.is_mp else False
                if self.quant_scale.is_distributed:
                    self.quant_scale.split_axis = 0
            else:
                # TODO(lugimzzz): support groupwise in next PR
                raise NotImplementedError("Not yet support grouwise weightonly quantization.")
            if self.weight_quantize_algo in ["a8w8linear", "a8w4linear", "fp8linear"]:
                self.act_scale = self.create_parameter(
                    shape=[1], dtype=self._dtype, is_bias=False, default_initializer=nn.initializer.Constant(value=0.0)
                )
                self.act_scale.is_distributed = False
                self.act_scale.stop_gradient = True
                self.group = get_act_scale_group(is_row=True)
        else:
            raise NotImplementedError(f"Not yet support weight_quantize_algo: {self.weight_quantize_algo}")

        if bias_attr is False:
            self.bias = None
        else:
            self.bias = self.create_parameter(
                shape=[out_features],
                attr=bias_attr,
                dtype=self._dtype,
                is_bias=True,
            )

        self.quant_weight.weight_quantize_algo = self.weight_quantize_algo

    def forward(self, x):
        if self.input_is_parallel or (not self.is_mp):
            input_parallel = x
        else:
            # split last dim
            input_parallel = mp_ops._c_split(x, group=self.model_parallel_group)

        # with paddle.amp.auto_cast(enable=False):
        if self.is_mp:
            output_parallel = quant_weight_linear(
                x=input_parallel,
                quant_weight=self.quant_weight,
                quant_dtype=self.quant_dtype,
                quantization_config=self.quantization_config,
                weight_quantize_algo=self.weight_quantize_algo,
                dtype=self._dtype,
                quant_scale=self.quant_scale,
                quant_state=(self.qquant_scale, self.double_quant_scale, self.quant_scale_offset)
                if (self.weight_quantize_algo in ["fp4", "nf4"] and self.quantization_config.qlora_weight_double_quant)
                else None,
                bias=None,
                act_state=(self.state, self.training, self.act_scale, self.group)
                if self.weight_quantize_algo in ["a8w8linear", "a8w4linear", "fp8linear"]
                else None,
            )
            if self.sequence_parallel:
                output_ = ReduceScatterOp.apply(output_parallel)
            else:
                output_ = mp_ops._mp_allreduce(
                    output_parallel,
                    group=self.model_parallel_group,
                    use_calc_stream=True,
                    use_model_parallel=True,
                    skip_c_identity_dynamic=self.mp_skip_c_identity,
                )
            output = output_ + self.bias if self.bias is not None else output_
        else:
            output = quant_weight_linear(
                x=input_parallel,
                quant_weight=self.quant_weight,
                quant_dtype=self.quant_dtype,
                quantization_config=self.quantization_config,
                weight_quantize_algo=self.weight_quantize_algo,
                dtype=self._dtype,
                quant_scale=self.quant_scale,
                quant_state=(self.qquant_scale, self.double_quant_scale, self.quant_scale_offset)
                if (self.weight_quantize_algo in ["fp4", "nf4"] and self.quantization_config.qlora_weight_double_quant)
                else None,
                bias=self.bias,
                act_state=(self.state, self.training, self.act_scale, self.group)
                if self.weight_quantize_algo in ["a8w8linear", "a8w4linear", "fp8linear"]
                else None,
            )
        if self.training:
            self.state += 1

        return output
