# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.
# pylint: disable=invalid-name, unused-wildcard-import, wildcard-import, pointless-exception-statement
# ruff: noqa: E501, F403, F405
"""Generator for CUTLASS GEMM kernels."""

from .library import *


class GemmOperation:
    """Describes various attributes for instantiating GEMM kernels."""

    def __init__(
        self,
        arch,
        tile_description,
        A,
        B,
        C,
        element_epilogue,
        epilogue_functor=EpilogueFunctor.LinearCombination,
        swizzling_functor=SwizzlingFunctor.Identity8,
    ):
        self.operation_kind = OperationKind.Gemm
        self.arch = arch
        self.tile_description = tile_description
        self.A = A
        self.B = B
        self.C = C
        self.element_epilogue = element_epilogue
        self.epilogue_functor = epilogue_functor
        self.swizzling_functor = swizzling_functor

    def accumulator_type(self):
        return self.tile_description.math_instruction.element_accumulator

    def short_math_name(self):
        return ShortDataTypeNames[self.accumulator_type()]

    def core_name(self):
        """The basic operation kind is prefixed with a letter indicating the accumulation type."""
        inst_shape = ""
        intermediate_type = ""

        if (
            self.tile_description.math_instruction.opcode_class == OpcodeClass.TensorOp
            or self.tile_description.math_instruction.opcode_class == OpcodeClass.WmmaTensorOp
        ):
            inst_shape = "{}{}{}".format(*self.tile_description.math_instruction.instruction_shape)
            if (
                self.tile_description.math_instruction.element_a != self.A.element
                and self.tile_description.math_instruction.element_a
                != self.tile_description.math_instruction.element_accumulator
            ):
                intermediate_type = DataTypeNames[self.tile_description.math_instruction.element_a]

        return f"{self.short_math_name()}{inst_shape}{intermediate_type}gemm"

    def extended_name(self):
        """Append data types if they differ from compute type."""
        if (
            self.C.element != self.tile_description.math_instruction.element_accumulator
            and self.A.element != self.tile_description.math_instruction.element_accumulator
        ):
            extended_name = "${element_c}_${core_name}_${element_a}"
        elif (
            self.C.element == self.tile_description.math_instruction.element_accumulator
            and self.A.element != self.tile_description.math_instruction.element_accumulator
        ):
            extended_name = "${core_name}_${element_a}"
        else:
            extended_name = "${core_name}"

        extended_name = substitute_template(
            extended_name,
            {
                "element_a": DataTypeNames[self.A.element],
                "element_c": DataTypeNames[self.C.element],
                "core_name": self.core_name(),
            },
        )

        return extended_name

    def layout_name(self):
        return f"{ShortLayoutTypeNames[self.A.layout]}{ShortLayoutTypeNames[self.B.layout]}"

    def procedural_name(self):
        """The full procedural name indicates architecture, extended name, tile size,
        and layout.
        """
        threadblock = self.tile_description.procedural_name()
        opcode_class_name = OpcodeClassNames[self.tile_description.math_instruction.opcode_class]

        return substitute_template(
            "cutlass_${opcode_class}_${extended_name}_${threadblock}_${layout}_align${alignment}",
            {
                "opcode_class": opcode_class_name,
                "extended_name": self.extended_name(),
                "threadblock": threadblock,
                "layout": self.layout_name(),
                "alignment": f"{self.A.alignment}",
            },
        )

    def leading_dim(self):
        """lda, ldb, ldc, according to the leading dimension."""
        if self.A.layout == LayoutType.RowMajor:
            lda = "K"
        elif self.A.layout == LayoutType.ColumnMajor:
            lda = "M"
        else:
            ValueError("The layout of A is not implemented.")

        if self.B.layout == LayoutType.RowMajor:
            ldb = "N"
        elif self.B.layout == LayoutType.ColumnMajor:
            ldb = "K"
        else:
            ValueError("The layout of B is not implemented.")

        if self.C.layout == LayoutType.RowMajor:
            ldc = "N"
        elif self.C.layout == LayoutType.ColumnMajor:
            ldc = "M"
        else:
            ValueError("The layout of B is not implemented.")

        return substitute_template(
            "int lda = ${lda_val};\n\tint ldb = ${ldb_val};\n\tint ldc = ${ldc_val};\n",
            {"lda_val": lda, "ldb_val": ldb, "ldc_val": ldc},
        )


class EmitGemmInstance:
    """Responsible for emitting a CUTLASS template definition."""

    def __init__(self):
        self.epilogue_default = """
    ${epilogue_functor}<
      ${element_c},
      ${epilogue_vector_length},
      ${element_accumulator},
      ${element_epilogue}
    >"""

        self.epilogue_no_beta_scaling = """
    ${epilogue_functor}<
      ${element_c},
      ${epilogue_vector_length},
      ${element_accumulator},
      ${element_epilogue},
      cutlass::epilogue::thread::ScaleType::NoBetaScaling
    >"""

        self.epilogue_residual_block = """
    ${epilogue_functor}<
      ${element_c},
      ${element_accumulator},
      ${element_epilogue},
      ${element_c},
      ${epilogue_vector_length},
      ${activation},
      ${binary_op},
      ${unary_op}
    >"""

        self.gemm_template = """
  // Gemm operator ${operation_name}
  using Operation_${operation_name} = cutlass::gemm::device::${kernel_name}<
    ${element_a}, ${layout_a},
    ${element_b}, ${layout_b},
    ${element_c}, ${layout_c},
    ${element_accumulator},
    ${opcode_class},
    ${arch},
    cutlass::gemm::GemmShape<${threadblock_shape_m}, ${threadblock_shape_n}, ${threadblock_shape_k}>,
    cutlass::gemm::GemmShape<${warp_shape_m}, ${warp_shape_n}, ${warp_shape_k}>,
    cutlass::gemm::GemmShape<${instruction_shape_m}, ${instruction_shape_n}, ${instruction_shape_k}>,
    ${epilogue},
    ${swizzling_functor},
    ${stages},
    ${align_a},
    ${align_b}
  >;
"""

    def emit(self, operation, no_beta_scaling=False, batched=False, residual_block_info=False):
        """Instantiate a GEMM kernel from given `operation`."""
        warp_shape = [
            operation.tile_description.threadblock_shape[idx]
            // operation.tile_description.warp_count[idx]
            for idx in range(3)
        ]
        epilogue_vector_length = (
            min(operation.C.alignment * DataTypeSize[operation.C.element], 128)
            // DataTypeSize[operation.C.element]
        )
        values = {
            "operation_name": operation.procedural_name(),
            "element_a": DataTypeTag[operation.A.element],
            "layout_a": LayoutTag[operation.A.layout],
            "element_b": DataTypeTag[operation.B.element],
            "layout_b": LayoutTag[operation.B.layout],
            "element_c": DataTypeTag[operation.C.element],
            "layout_c": LayoutTag[operation.C.layout],
            "element_accumulator": DataTypeTag[operation.accumulator_type()],
            "opcode_class": OpcodeClassTag[
                operation.tile_description.math_instruction.opcode_class
            ],
            "arch": f"cutlass::arch::Sm{operation.arch}",
            "threadblock_shape_m": str(operation.tile_description.threadblock_shape[0]),
            "threadblock_shape_n": str(operation.tile_description.threadblock_shape[1]),
            "threadblock_shape_k": str(operation.tile_description.threadblock_shape[2]),
            "warp_shape_m": str(warp_shape[0]),
            "warp_shape_n": str(warp_shape[1]),
            "warp_shape_k": str(warp_shape[2]),
            "instruction_shape_m": str(
                operation.tile_description.math_instruction.instruction_shape[0]
            ),
            "instruction_shape_n": str(
                operation.tile_description.math_instruction.instruction_shape[1]
            ),
            "instruction_shape_k": str(
                operation.tile_description.math_instruction.instruction_shape[2]
            ),
            "epilogue_vector_length": str(epilogue_vector_length),
            "element_epilogue": str(DataTypeTag[operation.element_epilogue]),
            "epilogue_functor": EpilogueFunctorTag[operation.epilogue_functor],
            "swizzling_functor": SwizzlingFunctorTag[operation.swizzling_functor],
            "stages": str(operation.tile_description.stages),
            "align_a": str(operation.A.alignment),
            "align_b": str(operation.B.alignment),
            "math_operation": MathOperationTag[
                operation.tile_description.math_instruction.math_operation
            ],
        }

        values["kernel_name"] = "GemmBatched" if batched else "Gemm"

        if residual_block_info:
            values["kernel_name"] = "GemmUniversalWithBroadcast"
            template = substitute_template(
                self.gemm_template, {"epilogue": self.epilogue_residual_block}
            )
            values.update(
                {
                    "unary_op": residual_block_info["unary_op"],
                    "binary_op": residual_block_info["binary_op"],
                    "activation": residual_block_info["activation"],
                }
            )
        elif no_beta_scaling:
            template = substitute_template(
                self.gemm_template, {"epilogue": self.epilogue_no_beta_scaling}
            )
        else:
            template = substitute_template(self.gemm_template, {"epilogue": self.epilogue_default})

        return substitute_template(template, values)


def instantiate_gemm_template(attrs):
    """Return CUTLASS host code for GEMM based on a template and the provided attribute map."""

    argument_template_default = """
  typename ${kernel}::Arguments arguments{
   problem_size,
   {static_cast<ElementInputA*>(ptr_a), ${lda}}, ${batch_stride_A}
   {static_cast<ElementInputB*>(ptr_b), ${ldb}}, ${batch_stride_B}
   {static_cast<ElementOutput*>(${ptr_c}), ${c_stride}}, ${batch_stride_C}
   {static_cast<ElementOutput*>(ptr_out), ${ldc}}, ${batch_stride_D}
   {${alpha_beta}},
   ${split_k_slices_or_batch}
  };
    """

    # See cutlass/gemm/kernel/gemm_with_fused_epilogue.h
    argument_template_residual = """
  typename ${kernel}::Arguments arguments{
    cutlass::gemm::GemmUniversalMode::${gemm_universal_mode},
    problem_size,
    ${split_k_slices_or_batch}, // batch_count
    {${alpha_beta}},
    static_cast<ElementInputA*>(ptr_a),
    static_cast<ElementInputB*>(ptr_b),
    static_cast<ElementOutput*>(ptr_residual),
    static_cast<ElementOutput*>(ptr_out),
    static_cast<ElementOutput*>(ptr_bias),
    nullptr, // ptr_Tensor
    ${batch_stride_A}
    ${batch_stride_B}
    ${batch_stride_C}
    ${batch_stride_D}
    0, // batch_stride_Vector,
    0, // batch_stride_Tensor,
    ${lda},
    ${ldb},
    ${ldc},
    ${ldc},
    0, // ldv, the stride for bias
    0, // ldt
  };
    """

    template = """
  using ElementInputA = ${ElementInputA};
  using ElementInputB = ${ElementInputB};
  using ElementOutput = ${ElementOutput};
  using ElementComputeEpilogue = ${ElementOutput};

  ${cutlass_op_def}

  using ${kernel} = Operation_${cutlass_op_name};
  int M = ${M};
  int N = ${N};
  int K = ${K};
  cutlass::gemm::GemmCoord problem_size(M, N, K);
  ElementComputeEpilogue alpha = ElementComputeEpilogue(1);
  ElementComputeEpilogue beta = ElementComputeEpilogue(${beta});
  void* ptr_a = (void*)(${lhs_arg}->data);
  void* ptr_b = (void*)(${rhs_arg}->data);
  ${bias_decl}
  ${residual_decl}
  void* ptr_out = (void*)(out0->data);

  ${argument}
  size_t workspace_size = ${kernel}::get_workspace_size(arguments);
  cutlass::device_memory::allocation<uint8_t> workspace(workspace_size);
  ${kernel} gemm_op;
  cutlass::Status status = gemm_op.can_implement(arguments);
  TVM_FFI_ICHECK(status == cutlass::Status::kSuccess);
  status = gemm_op.initialize(arguments, workspace.get());
  TVM_FFI_ICHECK(status == cutlass::Status::kSuccess);

  cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(kDLCUDA, ${A_arg}->device.device_id));

  status = gemm_op(stream);
  TVM_FFI_ICHECK(status == cutlass::Status::kSuccess);
"""
    op_type = attrs["op_type"]
    has_bias = "bias" in op_type
    is_gelu = "gelu" in op_type
    batched = "batch" in attrs
    has_residual_block = "residual" in op_type
    aux_map = {"kernel": "Gemm"}

    if has_bias:
        aux_map.update(
            {
                "bias_decl": "void* ptr_bias = (void*)(${bias_arg}->data);\n",
                "ptr_c": "ptr_bias",
                "c_stride": (
                    "(${bias_arg}->ndim == 1 ||"
                    " ${bias_arg}->shape[${bias_arg}->ndim - 2] == 1) ? 0 : " + attrs["ldc"]
                ),
            }
        )
    else:
        aux_map.update({"bias_decl": "", "ptr_c": "ptr_out", "c_stride": attrs["ldc"]})

    if is_gelu or has_residual_block:
        # GeLU epilogue does not compile with NoBetaScaling, so we explicitly specify the scale.
        aux_map["beta"] = 1
    else:
        aux_map["beta"] = 0

    if has_bias and not is_gelu and not has_residual_block:
        aux_map["alpha_beta"] = "alpha"
    else:
        aux_map["alpha_beta"] = "alpha, beta"

    for key in ["batch_stride_A", "batch_stride_B", "batch_stride_C"]:
        if not batched and not has_residual_block:
            aux_map[key] = ""
        else:
            aux_map[key] = attrs.get(key, "0") + ","

    aux_map["batch_stride_D"] = aux_map["batch_stride_C"]
    if has_bias and batched and not has_residual_block:
        aux_map["batch_stride_C"] = "0,"

    if batched:
        attrs["split_k_slices_or_batch"] = attrs["batch"]
    else:
        attrs["split_k_slices_or_batch"] = 1

    if has_residual_block:
        template = substitute_template(template, {"argument": argument_template_residual})
        aux_map["residual_decl"] = "void* ptr_residual = (void*)(${residual_arg}->data);\n"
        aux_map["gemm_universal_mode"] = "kBatched" if batched else "kGemm"
    else:
        template = substitute_template(template, {"argument": argument_template_default})
        aux_map["residual_decl"] = ""

    template = substitute_template(template, aux_map)

    return substitute_template(template, attrs)


def emit_fp16A_intB_matmul(attrs):
    """Return CUTLASS host code for fp16 A and int4 or int8 B GEMM."""
    if attrs["group_size"] > 0:
        attrs["quant_op"] = "cutlass::WeightOnlyQuantOp::FINEGRAINED_SCALE_ONLY"
    else:
        attrs["quant_op"] = "cutlass::WeightOnlyQuantOp::PER_COLUMN_SCALE_ONLY"
        attrs["group_size"] = "k"

    attrs["template_common"] = substitute_template(
        """
  using namespace fastertransformer;
  constexpr auto QuantOp = ${quant_op};

  int m = ${M};
  int n = ${B_arg}->shape[1] * ${float_per_int};
  int k = ${B_arg}->shape[0];

  cudaStream_t stream = static_cast<cudaStream_t>(
    TVMFFIEnvGetStream(kDLCUDA, ${A_arg}->device.device_id));
    """,
        attrs,
    )

    template = """
  ${template_common}
  gemm_fp16_int_bias_act<${weight_dtype}, QuantOp>(static_cast<cutlass::half_t*>(${A_arg}->data),
                static_cast<${weight_dtype}*>(${B_arg}->data),
                static_cast<cutlass::half_t*>(${scales_arg}->data),
                ${bias},
                static_cast<cutlass::half_t*>(out0->data),
                "${activation}",
                m, n, k, ${group_size}, ${bias_stride}, nullptr, 0, stream);
"""

    template_residual = """
  ${template_common}
  gemm_fp16_int_bias_act_residual<${weight_dtype}, QuantOp>(
                static_cast<cutlass::half_t*>(${A_arg}->data),
                static_cast<${weight_dtype}*>(${B_arg}->data),
                static_cast<cutlass::half_t*>(${scales_arg}->data),
                ${bias},
                static_cast<cutlass::half_t*>(${residual_arg}->data),
                static_cast<cutlass::half_t*>(out0->data),
                "${activation}", "${binary_op}", "${unary_op}",
                m, n, k, ${group_size}, nullptr, 0, stream);
"""

    if "residual_arg" in attrs:
        if "bias_arg" in attrs:
            bias = "static_cast<cutlass::half_t*>(${bias_arg}->data)"
        else:
            bias = "nullptr"

        template_residual = substitute_template(template_residual, {"bias": bias})
        return substitute_template(template_residual, attrs)

    if "bias_arg" in attrs:
        template = substitute_template(
            template, {"bias": "static_cast<cutlass::half_t*>(${bias_arg}->data)"}
        )
    else:
        template = substitute_template(template, {"bias": "nullptr"})

    return substitute_template(template, attrs)
