# 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(ptr_a), ${lda}}, ${batch_stride_A} {static_cast(ptr_b), ${ldb}}, ${batch_stride_B} {static_cast(${ptr_c}), ${c_stride}}, ${batch_stride_C} {static_cast(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(ptr_a), static_cast(ptr_b), static_cast(ptr_residual), static_cast(ptr_out), static_cast(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 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(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( TVMFFIEnvGetStream(kDLCUDA, ${A_arg}->device.device_id)); """, attrs, ) template = """ ${template_common} gemm_fp16_int_bias_act<${weight_dtype}, QuantOp>(static_cast(${A_arg}->data), static_cast<${weight_dtype}*>(${B_arg}->data), static_cast(${scales_arg}->data), ${bias}, static_cast(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(${A_arg}->data), static_cast<${weight_dtype}*>(${B_arg}->data), static_cast(${scales_arg}->data), ${bias}, static_cast(${residual_arg}->data), static_cast(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(${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(${bias_arg}->data)"} ) else: template = substitute_template(template, {"bias": "nullptr"}) return substitute_template(template, attrs)