blob: 5ad633cdfeefc4c89a40507957df982b7db4b827 [file] [log] [blame]
# Copyright 2023 The IREE Authors
#
# Licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
import enum, shutil, functools, operator, collections, subprocess
from library import *
from dispatch import *
from options import get_cmd_line_argument_list
################################################################################
class MatmulOperation:
"""Data structure to describe a matrix multiplication operation.
This includes the shape, datatype, and layout of the operands. This data
structure is *independent* of the compilation* and tiling configuration.
It "mostly" contains the parameter that changes the functionality of matmul
operation. The only exception is the split_k_slices parameter, which is
changes the performance of the matmul operation and not the functionality.
"""
def __init__(
self,
matmul_shape,
lhs,
rhs,
result,
batch_count=1,
split_k_slices=1,
operation_kind=OperationKind.Matmul,
):
"""Initializes a matrix multiplication operation.
Matrix-multiple operation: `result[M, N] = lhs[M, K] * rhs[K, N]`
matmul_shape: A tuple representing the matrix multiplication problem shape
in the format (M, N, K), where M is the number of rows in the lhs matrix,
N is the number of columns in the rhs matrix, and K is the number of columns
in the lhs matrix and rows in the rhs matrix.
lhs: A TensorDescription object representing the left-hand-side matrix operand.
rhs: A TensorDescription object representing the right-hand-side matrix operand.
result: A TensorDescription object representing the result matrix operand.
"""
# Parameters that change the matmul operation *functionally*.
self.operation_kind = operation_kind
self.matmul_shape = matmul_shape
self.M = matmul_shape[0]
self.N = matmul_shape[1]
self.K = matmul_shape[2]
self.batch_count = batch_count
self.lhs = lhs # TensorDescription
self.rhs = rhs # TensorDescription
self.result = result # TensorDescription
# Parameters that change the matmul operation *performance*.
self.split_k_slices = split_k_slices
def __eq__(self, other):
"""Returns true if the matmul operation is *functionally* the same."""
return (
self.matmul_shape == other.matmul_shape
and self.lhs == other.lhs
and self.rhs == other.rhs
and self.result == other.result
and self.batch_count == other.batch_count
)
def name(self):
"""Procedurally generated name for the matmul operation.
The name uniquely identifies a matmul operation with matmul shape,
lhs dataype and layout, rhs datatype and layout, and result
datatype and layout.
"""
return (
f"{OperationKindNames[self.operation_kind]}_"
f"{self.M}x{self.N}x{self.K}_"
f"{DataTypeName[self.lhs.datatype]}{ShortLayoutTypeName[self.lhs.layout]}_"
f"{DataTypeName[self.rhs.datatype]}{ShortLayoutTypeName[self.rhs.layout]}_"
f"{DataTypeName[self.result.datatype]}{ShortLayoutTypeName[self.result.layout]}"
)
def get_argument_dict(self):
"""Returns the dictionary of matmul arguments (shape, datatypes, split_k_slices)."""
split_k_mode = (
"parallel" if self.operation_kind == OperationKind.SplitkMatmul else "N/A"
)
split_k_slices = (
self.split_k_slices
if self.operation_kind == OperationKind.SplitkMatmul
else "N/A"
)
return {
"batch_count": self.batch_count,
"m": self.M,
"n": self.N,
"k": self.K,
"lhs": self.lhs.name(),
"rhs": self.rhs.name(),
"result": self.result.name(),
"split_k_mode": split_k_mode,
"split_k_slices": split_k_slices,
}
def get_dict_entry(self):
"""Returns the dictionary of matmul operation summary."""
dict_entry = {
"op_kind": OperationKindNames[self.operation_kind],
"Operation": self.name(),
"bytes": self.bytes(),
"flops": self.flops(),
}
dict_entry.update(self.get_argument_dict())
return dict_entry
def lhs_npy_shape(self):
"""Returns the shape of the lhs numpy array as a string in the format "MxKxDataType"."""
return f"{self.M}x{self.K}x{DataTypeName[self.lhs.datatype]}"
def rhs_npy_shape(self):
"""Returns the shape of the rhs numpy array as a string in the format "KxNxDataType"."""
return f"{self.K}x{self.N}x{DataTypeName[self.rhs.datatype]}"
def result_npy_shape(self):
"""Returns the shape of the result numpy array as a string in the format "MxNxDataType"."""
return f"{self.M}x{self.N}x{DataTypeName[self.result.datatype]}"
def bytes(self):
"""Returns the number of bytes read/written by the matmul operation."""
bytes = (
(DataTypeSizeInBits[self.lhs.datatype] * self.M // 8) * self.K
+ (DataTypeSizeInBits[self.rhs.datatype] * self.K // 8) * self.N
+ (DataTypeSizeInBits[self.result.datatype] * self.M // 8) * self.N
)
return bytes * self.batch_count
def flops(self):
"""Returns the number of floating point operations performed by the matmul operation."""
return 2 * self.M * self.N * self.K * self.batch_count
##############################################################################
class MatmulCompilationInfo:
"""Data structure strictly describes the compilation passes and the tiling configurations.
For a matrix multiplication operation, compilation passes and tiling configuration
influences the performance of the compiled matmul operation, but the functionality.
This data structure should be independent of the matmul operation functionality.
Any change in this data structure should not affect the functionality of the matmul operation, i.e.,
we should be able to use the same reference results for a matrix operation compiled with different
compilation info.
"""
def __init__(
self,
tile_description,
translation_info,
operation_kind=OperationKind.Matmul,
config_type=CompilationConfigType.Custom,
):
self.tile_description = tile_description # TileDescription
self.translation_info = translation_info # TranslationInfo
self.operation_kind = operation_kind # OperationKind
self.config_type = config_type # CompilationConfigType
def get_dict_entry(self):
"""Returns the dictionary entry for the matmul compilation info."""
if self.config_type == CompilationConfigType.Default:
return {
"Tile config": "Default",
"Core class": "Default",
"Instruction class": "Default",
}
translation_info_name = TranslationInfoName[self.translation_info]
return {
"Tile config": self.tile_description.name(),
"Core class": translation_info_name.split("_")[0],
"Instruction class": translation_info_name.split("_")[1],
}
def name(self):
"""Procedurally generated name for the matmul compilation info."""
if self.config_type == CompilationConfigType.Default:
return "tile_config_default"
return "tile_config_{tbm}x{tbn}_{tbk}x{stages}_{translation_info}".format(
tbm=self.tile_description.threadblock_shape[0],
tbn=self.tile_description.threadblock_shape[1],
tbk=self.tile_description.threadblock_shape[2],
stages=self.tile_description.stages,
translation_info=TranslationInfoName[self.translation_info],
)
################################################################################
class EmitMatmulCompilationInfo:
"""Emitters for the matmul compilation info."""
def __init__(self):
# matmul compilation info template
self.matmul_compilation_info_template = """
// matmul compilation info (tile configuration, translation info, workgroup size)
#${compilation_info_name} = #iree_codegen.compilation_info<
lowering_config = <tile_sizes = [[${threadblock_shape_m}, ${threadblock_shape_n}, ${threadblock_shape_k}]]>,
translation_info = <${translation_info} pipeline_depth = ${stages}>,
workgroup_size = [${block_dim_x} : index, ${block_dim_y} : index, ${block_dim_z} : index]
>
"""
# batch matmul and split-k matmul compilation info template
self.batch_matmul_compilation_info_template = """
// batch matmul compilation info (tile configuration, translation info, workgroup size)
#${compilation_info_name} = #iree_codegen.compilation_info<
lowering_config = <tile_sizes = [[1, ${threadblock_shape_m}, ${threadblock_shape_n}, ${threadblock_shape_k}]]>,
translation_info = <${translation_info} pipeline_depth = ${stages}>,
workgroup_size = [${block_dim_x} : index, ${block_dim_y} : index, ${block_dim_z} : index]
>
"""
def emit(self, compilation_info):
"""Emits the matmul compilation info as a string."""
if compilation_info.config_type == CompilationConfigType.Default:
return ""
values = {
"compilation_info_name": compilation_info.name(),
"translation_info": TranslationInfoTag[compilation_info.translation_info],
"threadblock_shape_m": str(
compilation_info.tile_description.threadblock_shape[0]
),
"threadblock_shape_n": str(
compilation_info.tile_description.threadblock_shape[1]
),
"threadblock_shape_k": str(
compilation_info.tile_description.threadblock_shape[2]
),
"stages": str(compilation_info.tile_description.stages),
"block_dim_x": str(compilation_info.tile_description.block_dim[0]),
"block_dim_y": str(compilation_info.tile_description.block_dim[1]),
"block_dim_z": str(compilation_info.tile_description.block_dim[2]),
}
# linalg.matmul (without split-k) compilation info template.
compilation_info_template = self.matmul_compilation_info_template
# linalg.batch_matmul and linalg.matmul (with split-k) have different
# compilation info template from the linalg.matmul (without split-k).
if (
compilation_info.operation_kind == OperationKind.BatchMatmul
or compilation_info.operation_kind == OperationKind.SplitkMatmul
):
compilation_info_template = self.batch_matmul_compilation_info_template
return SubstituteTemplate(compilation_info_template, values)
###############################################################################
class EmitLinalgMatmulDispatch:
"""Emitters for the `linalg.matmul` dispatch."""
def __init__(self):
self.mlir_dialect = MlirDialect.Linalg
# linalg.matmul mlir template
self.linalg_row_row_matmul_template = """
// Dispatch linalg.matmul row-row layout
func.func @${operation_name}_${compilation_info_name}(
%lhs: tensor<${problem_m}x${problem_k}x${datatype_lhs}>,
%rhs: tensor<${problem_k}x${problem_n}x${datatype_rhs}>) -> tensor<${problem_m}x${problem_n}x${datatype_result}>
{
%c0 = arith.constant 0.0 : ${datatype_result}
%init = tensor.empty() : tensor<${problem_m}x${problem_n}x${datatype_result}>
%inital_result = linalg.fill ins(%c0 : ${datatype_result}) outs(%init : tensor<${problem_m}x${problem_n}x${datatype_result}>) -> tensor<${problem_m}x${problem_n}x${datatype_result}>
%result = linalg.matmul ${compilation_info_attribute}
ins(%lhs, %rhs: tensor<${problem_m}x${problem_k}x${datatype_lhs}>, tensor<${problem_k}x${problem_n}x${datatype_rhs}>)
outs(%inital_result: tensor<${problem_m}x${problem_n}x${datatype_result}>) -> tensor<${problem_m}x${problem_n}x${datatype_result}>
return %result : tensor<${problem_m}x${problem_n}x${datatype_result}>
}
"""
def emit(self, matmul_dispatch):
"""Emit the matmul operation in the MLIR dialect for a single compilation info"""
compilation_info_attribute_template = (
"""{compilation_info = #${compilation_info_name}}"""
)
compilation_info_attribute_str = SubstituteTemplate(
compilation_info_attribute_template,
{"compilation_info_name": matmul_dispatch.configuration.name()},
)
compilation_info_attribute = (
compilation_info_attribute_str
if matmul_dispatch.configuration.config_type
!= CompilationConfigType.Default
else ""
)
values = {
"operation_name": matmul_dispatch.operation.name(),
"compilation_info_attribute": compilation_info_attribute,
"problem_m": str(matmul_dispatch.operation.M),
"problem_n": str(matmul_dispatch.operation.N),
"problem_k": str(matmul_dispatch.operation.K),
"datatype_lhs": DataTypeName[matmul_dispatch.operation.lhs.datatype],
"datatype_rhs": DataTypeName[matmul_dispatch.operation.rhs.datatype],
"datatype_result": DataTypeName[matmul_dispatch.operation.result.datatype],
"compilation_info_name": matmul_dispatch.configuration.name(),
}
return SubstituteTemplate(self.linalg_row_row_matmul_template, values)
###############################################################################
class ReferenceMatmulOp(ReferenceOpInterface):
"""Reference implementation for the matmul operation in numpy."""
def __init__(self, matmul_operation, op_reference_cache_path, dist_lhs, dist_rhs):
self.matmul_operation = matmul_operation
self.op_reference_cache_path = op_reference_cache_path
# Problem shape.
self.M = matmul_operation.M
self.N = matmul_operation.N
self.K = matmul_operation.K
# Data type for the input and result matrices.
self.dtype_lhs = DataTypeNumPyTag[matmul_operation.lhs.datatype]
self.dtype_rhs = DataTypeNumPyTag[matmul_operation.rhs.datatype]
self.dtype_result = DataTypeNumPyTag[matmul_operation.result.datatype]
# Distribution of the input tensors.
self.dist_lhs = dist_lhs
self.dist_rhs = dist_rhs
# Filename for the left hand side input tensor.
self.filename_lhs = (
"m{problem_m}xk{problem_k}_"
"{tensor_description}_{dist}_lhs.npy".format(
problem_m=self.M,
problem_k=self.K,
tensor_description=self.matmul_operation.lhs.name(),
dist=DistributionName[self.dist_lhs],
)
)
# Filename for the right hand side input tensor.
self.filename_rhs = (
"k{problem_k}xn{problem_n}_"
"{tensor_description}_{dist}_rhs.npy".format(
problem_k=self.K,
problem_n=self.N,
tensor_description=self.matmul_operation.rhs.name(),
dist=DistributionName[self.dist_rhs],
)
)
# Filename for the reference result tensor.
self.filename_reference_result = (
"m{problem_m}xn{problem_n}_"
"{tensor_description}_reference_result.npy".format(
problem_m=self.M,
problem_n=self.N,
tensor_description=self.matmul_operation.result.name(),
)
)
# Filepath for input and output files.
self.filepath_lhs = self.op_reference_cache_path.joinpath(self.filename_lhs)
self.filepath_rhs = self.op_reference_cache_path.joinpath(self.filename_rhs)
self.filepath_reference_result = self.op_reference_cache_path.joinpath(
self.filename_reference_result
)
def get_input_filepaths(self):
"""Returns the list of input file paths."""
return [self.filepath_lhs, self.filepath_rhs]
def get_output_filepaths(self):
"""Returns the list of expected output file paths."""
return [self.filepath_reference_result]
def __call__(self):
"""Generates input data, runs reference numpy.matmul, and save npy files to the output directory."""
# Generate the input data as np.array for the matmul operation.
lhs_np_array = get_np_array(
self.matmul_operation.lhs, (self.M, self.K), self.dist_lhs
)
rhs_np_array = get_np_array(
self.matmul_operation.rhs, (self.K, self.N), self.dist_rhs
)
# Run the reference np.matmul and generate result np.array.
result = np.matmul(lhs_np_array, rhs_np_array)
# Save the input data as np.array for the matmul operation.
np.save(self.filepath_lhs, np.array(lhs_np_array, dtype=self.dtype_lhs))
np.save(self.filepath_rhs, np.array(rhs_np_array, dtype=self.dtype_rhs))
# Save the expected result as an np.array.
np.save(
self.filepath_reference_result, np.array(result, dtype=self.dtype_result)
)
class CudaMatmulDispatchChecker:
"""Given a matmul dispatch, checks if the dispatch is supported by the target GPU."""
def __init__(self, args):
self.args = args
# CUDA shared memory capacity per SM in KB.
self.sharedMemPerSm = {
"sm_80": 163, # 1KB is reserved for the driver.
"sm_86": 99, # 1KB is reserved for the driver
}
self.cuda_arch = self.args.cuda_arch
self.cuda_smem_capacity_in_bytes = self.sharedMemPerSm[self.cuda_arch] << 10
def _is_tile_aligned_shape(self, dispatch):
"""Checks if the given dispatch is valid for CUDA."""
matmul_shape = dispatch.operation.matmul_shape
threadblock_shape = dispatch.configuration.tile_description.threadblock_shape
if len(matmul_shape) != len(threadblock_shape):
raise ValueError(
"Problem shape and threadblock shape must have the same rank."
)
is_aligned = all(a % b == 0 for a, b in zip(matmul_shape, threadblock_shape))
return is_aligned
def _cuda_smem_required_in_bytes(self, dispatch):
"""Returns size bytes of shared memory required for a given cuda dispatch."""
threadblock_shape = dispatch.configuration.tile_description.threadblock_shape
num_stages = dispatch.configuration.tile_description.stages
tile_shape_lhs = threadblock_shape[0] * threadblock_shape[2]
tile_shape_rhs = threadblock_shape[2] * threadblock_shape[1]
return (
(
tile_shape_lhs * DataTypeSizeInBits[dispatch.operation.lhs.datatype]
+ tile_shape_rhs * DataTypeSizeInBits[dispatch.operation.rhs.datatype]
)
* num_stages
) // 8
def _is_problem_k_divisible_by_split_k(self, dispatch):
"""Checks if the given dispatch is valid for CUDA."""
return dispatch.operation.K % dispatch.operation.split_k_slices == 0
def _is_cuda_smem_avialable(self, dispatch):
"""Checks if the given dispatch is valid for CUDA."""
return (
self._cuda_smem_required_in_bytes(dispatch)
<= self.cuda_smem_capacity_in_bytes
)
def is_valid(self, dispatch):
"""Checks if the given dispatch is valid for CUDA."""
if not self._is_tile_aligned_shape(dispatch):
if self.args.verbose:
print(f"[Warning]: {dispatch.name()} is not aligned is being skipped.")
return False
if not self._is_cuda_smem_avialable(dispatch):
if self.args.verbose:
print(
f"[Warning]: {dispatch.name()} requires {self._cuda_smem_required_in_bytes(dispatch)} "
f"bytes of shared memory, which is larger than the {self.cuda_arch} capacity "
f"{self.cuda_smem_capacity_in_bytes} bytes."
)
return False
if (dispatch.operation.split_k_slices > 1) and (
not self._is_problem_k_divisible_by_split_k(dispatch)
):
if self.args.verbose:
print(
f"[Warning]: {dispatch.name()} problem k is not divisible by {dispatch.operation.split_k_slices} "
f"split-k slices, which is not supported on LLVM GPU CUDA backend."
)
return False
return True
class CudaMatmulGenerator:
"""Matmul dispatch generator class.
Generates a list of pre-defined matmul operations with resonable tuning cofigurations.
The generator function are seperated based on the target backend and the data type.
Please see example `MatmulGenerator._cuda_matmul_tensor_cores_f16` for cuda target
backend and f16 data type."""
def __init__(self, args):
"""Initializes the matmul generator."""
self.args = args
self.translation_infos = [
# TranslationInfo.LLVMGPUMatmulSimt, # CUDA Core (SMIT)
# TranslationInfo.LLVMGPUMatmulTensorCore, # Tensor Core (WMMA)
TranslationInfo.LLVMGPUMatmulTensorCoreMmaSync, # Tensor Core (MMA.SYNC)
]
# List of pre-defined threadblock tile shapes for Tensor Core.
self.tile_descriptions_tensor_cores_f16 = [
TileDescription([256, 128, 32], 3, [64, 4, 1]),
TileDescription([128, 256, 32], 3, [128, 2, 1]),
TileDescription([128, 128, 64], 4, [64, 2, 1]),
TileDescription([128, 128, 32], 5, [64, 2, 1]),
TileDescription([128, 64, 32], 5, [64, 2, 1]),
TileDescription([64, 64, 64], 5, [64, 2, 1]),
TileDescription([64, 64, 32], 10, [64, 2, 1]),
]
self.tile_descriptions_tensor_cores_f32 = [
TileDescription([128, 256, 16], 3, [128, 2, 1]),
TileDescription([256, 128, 16], 3, [64, 4, 1]),
TileDescription([128, 128, 16], 5, [64, 2, 1]),
TileDescription([128, 128, 32], 3, [64, 2, 1]),
TileDescription([128, 128, 32], 4, [64, 2, 1]),
TileDescription([128, 64, 32], 3, [64, 2, 1]),
TileDescription([128, 64, 16], 5, [64, 2, 1]),
TileDescription([64, 64, 32], 3, [64, 2, 1]),
TileDescription([64, 64, 16], 10, [64, 2, 1]),
]
# Create a list of matmul problem and initialize with some *default* shapes.
self.matmul_shapes = [[256, 512, 128], [2560, 2560, 2560], [3456, 1024, 2048]]
# Append matmul problem with *user* provided shapes.
self.add_cmd_line_shapes()
# Matmul dispatches collection.
self.dispatches_collection_list = []
def add_cmd_line_shapes(self):
"""Adds matmul shapes from command line arguments."""
m_list = get_cmd_line_argument_list(self.args.problem_m)
n_list = get_cmd_line_argument_list(self.args.problem_n)
k_list = get_cmd_line_argument_list(self.args.problem_k)
# If no command line matmul problem shapes are provided, only
# use the default shapes.
if len(m_list) == 0 and len(n_list) == 0 and len(k_list) == 0:
return
# If any of the command line matmul problem shapes are provided,
# set the default shapes to empty problem dimension.
if len(m_list) == 0:
m_list = [256]
if len(n_list) == 0:
n_list = [256]
if len(k_list) == 0:
k_list = [256]
# Append the command line matmul problem shapes with user-proivded
# matmul problem shapes.
for m in m_list:
for n in n_list:
for k in k_list:
self.matmul_shapes.append([m, n, k])
def _cuda_supported_configuration_list(self, operation, configuration_list):
"""Returns a list of supported configurations for CUDA."""
supported_configuration_list = []
dispatch_checker = CudaMatmulDispatchChecker(self.args)
for configuration in configuration_list:
if not dispatch_checker.is_valid(Dispatch(operation, configuration)):
continue
supported_configuration_list.append(configuration)
# Return the supported configuration list.
return supported_configuration_list
def _get_matmul_custom_compilation_info_list(
self, tile_descriptions, translation_infos, operation_kind
):
"""Creates a *custom* list of matmul compilation info."""
configuration_list = []
for tile_description in tile_descriptions:
for translation_info in translation_infos:
configuration_list.append(
MatmulCompilationInfo(
tile_description,
translation_info,
operation_kind,
CompilationConfigType.Custom,
)
)
return configuration_list
def _append_matmul_dispatch_collection(
self, matmul_shapes, data_type, configuration_list
):
"""Appends the matmul dispatches collection with the given configuration list."""
# Create dispatches collection for each matmul_shape x configuration list..
for matmul_shape in matmul_shapes:
operation = MatmulOperation(
matmul_shape,
TensorDescription(data_type[0], LayoutType.RowMajor),
TensorDescription(data_type[1], LayoutType.RowMajor),
TensorDescription(data_type[2], LayoutType.RowMajor),
)
# Filter out configurations that are not supported by LLVM GPU CUDA backend.
supported_configuration_list = self._cuda_supported_configuration_list(
operation, configuration_list
)
# Add default configuration if enabled.
if self.args.default_config:
supported_configuration_list.append(
MatmulCompilationInfo(
[], [], OperationKind.Matmul, CompilationConfigType.Default
)
)
# Append the dispatch collection.
self.dispatches_collection_list.append(
DispatchCollection(operation, supported_configuration_list)
)
def _cuda_matmul_tensor_cores_f16(self):
"""Appends dispatches for TensorCore with F16 input, F16 accum, F16 output."""
configuration_list = self._get_matmul_custom_compilation_info_list(
self.tile_descriptions_tensor_cores_f16,
self.translation_infos,
OperationKind.Matmul,
)
data_type = [DataType.f16, DataType.f16, DataType.f16]
self._append_matmul_dispatch_collection(
self.matmul_shapes, data_type, configuration_list
)
def _cuda_matmul_tensor_cores_f32(self):
"""Appends dispatches for TensorCore with F32 input, F32 accum, F32 output."""
configuration_list = self._get_matmul_custom_compilation_info_list(
self.tile_descriptions_tensor_cores_f32,
self.translation_infos,
OperationKind.Matmul,
)
data_type = [DataType.f32, DataType.f32, DataType.f32]
self._append_matmul_dispatch_collection(
self.matmul_shapes, data_type, configuration_list
)
def _cuda_matmul_tensor_cores_mixed_precision(self):
"""Appends dispatches for TensorCore with F16 input, F32 accum, F32 output."""
configuration_list = self._get_matmul_custom_compilation_info_list(
self.tile_descriptions_tensor_cores_f16,
self.translation_infos,
OperationKind.Matmul,
)
data_type = [DataType.f16, DataType.f16, DataType.f32]
self._append_matmul_dispatch_collection(
self.matmul_shapes, data_type, configuration_list
)
def generate(self):
"""Generates a list of matmul operations."""
self._cuda_matmul_tensor_cores_f16()
self._cuda_matmul_tensor_cores_f32()
self._cuda_matmul_tensor_cores_mixed_precision()
return self.dispatches_collection_list