908 lines
30 KiB
Python
908 lines
30 KiB
Python
# 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, dangerous-default-value, arguments-differ
|
|
# ruff: noqa: F821
|
|
"""Driver for partitioning and building a Relax module for CUTLASS offload."""
|
|
|
|
import itertools
|
|
import logging
|
|
import multiprocessing
|
|
import operator
|
|
import os
|
|
from collections.abc import Sequence
|
|
from functools import reduce
|
|
|
|
from tvm_ffi import register_global_func
|
|
|
|
import tvm
|
|
from tvm import relax, runtime
|
|
from tvm.support.nvcc import get_cuda_version
|
|
from tvm.topi.utils import get_const_tuple
|
|
|
|
from .gen_conv2d import CutlassConv2DProfiler
|
|
from .gen_gemm import CutlassGemmProfiler
|
|
from .library import ConvKind, LayoutType
|
|
|
|
logger = logging.getLogger("cutlass")
|
|
|
|
|
|
def has_cutlass():
|
|
"""Returns true if the CUTLASS custom codegen is available"""
|
|
return tvm.get_global_func("relax.ext.cutlass", True) is not None
|
|
|
|
|
|
def _get_cutlass_path():
|
|
invalid_paths = []
|
|
for rel in ["../../../../", "../../../", "../../"]:
|
|
tvm_root = os.path.join(os.path.dirname(os.path.realpath(__file__)), rel)
|
|
cutlass_path = os.path.join(tvm_root, "3rdparty/cutlass")
|
|
if os.path.exists(cutlass_path):
|
|
return cutlass_path
|
|
invalid_paths.append(cutlass_path)
|
|
raise AssertionError(f"The CUTLASS root directory not found in: {invalid_paths}")
|
|
|
|
|
|
def _get_cutlass_compile_options(sm, threads, use_fast_math=False):
|
|
cutlass_root = _get_cutlass_path()
|
|
cutlass_include = os.path.join(cutlass_root, "include")
|
|
cutlass_util_include = os.path.join(cutlass_root, "tools/util/include")
|
|
cutlass_attention_include = os.path.join(cutlass_root, "examples/41_fused_multi_head_attention")
|
|
cutlass_fpA_intB_gemm_include = os.path.join(cutlass_root, "../cutlass_fpA_intB_gemm")
|
|
flash_attn_include = os.path.join(cutlass_root, "../libflash_attn/include")
|
|
|
|
kwargs = {}
|
|
kwargs["cc"] = "nvcc"
|
|
kwargs["options"] = [
|
|
"-c",
|
|
"-DCUTLASS_ENABLE_TENSOR_CORE_MMA=1",
|
|
f"-gencode=arch=compute_{sm},code=[sm_{sm},compute_{sm}]",
|
|
"-DNDEBUG",
|
|
"-Xcompiler=-fPIC",
|
|
"-Xcompiler=-Wconversion",
|
|
"-Xcompiler=-fno-strict-aliasing",
|
|
"-Xcompiler=-fvisibility=hidden",
|
|
"-O3",
|
|
"-std=c++17",
|
|
f"-I{cutlass_include}",
|
|
f"-I{cutlass_util_include}",
|
|
f"-I{cutlass_attention_include}",
|
|
f"-I{cutlass_fpA_intB_gemm_include}",
|
|
f"-I{flash_attn_include}",
|
|
]
|
|
if use_fast_math:
|
|
kwargs["options"].append("-DCUTLASS_USE_TANH_FOR_SIGMOID")
|
|
cuda_ver = get_cuda_version()
|
|
if cuda_ver >= (11, 2):
|
|
ncpu = multiprocessing.cpu_count() if threads < 0 else threads
|
|
kwargs["options"].append(f"-t {ncpu}")
|
|
return kwargs
|
|
|
|
|
|
def select_gemm_kernel(
|
|
cutlass_profiler,
|
|
op_type,
|
|
MM,
|
|
KK,
|
|
NN,
|
|
out_dtype,
|
|
arg0_dtype,
|
|
arg1_dtype,
|
|
use_3xtf32,
|
|
batched,
|
|
find_first_valid,
|
|
use_multiprocessing,
|
|
):
|
|
"""Run CUTLASS profiler to select the best kernel, or return the default one for dynamic
|
|
workloads."""
|
|
if any(isinstance(s, tvm.tirx.Any) for s in [MM, KK, NN]):
|
|
out = cutlass_profiler.get_default(
|
|
op_type, out_dtype, arg0_dtype, arg1_dtype, use_3xtf32, batched=batched
|
|
)
|
|
name, cutlass_op_def = out["name"], out["opdef"]
|
|
logger.info("Picked the default kernel %s", name)
|
|
else:
|
|
name, cutlass_op_def, _ = cutlass_profiler.profile(
|
|
op_type,
|
|
MM,
|
|
NN,
|
|
KK,
|
|
out_dtype,
|
|
arg0_dtype,
|
|
arg1_dtype,
|
|
use_3xtf32,
|
|
batched=batched,
|
|
find_first_valid=find_first_valid,
|
|
use_multiprocessing=use_multiprocessing,
|
|
)
|
|
if not find_first_valid:
|
|
logger.info("The best kernel is %s", name)
|
|
else:
|
|
logger.info("Picked the first kernel found %s", name)
|
|
|
|
return name, cutlass_op_def
|
|
|
|
|
|
def handle_batch_matmul(
|
|
cutlass_profiler,
|
|
op_type,
|
|
arg0_shape,
|
|
arg1_shape,
|
|
out_dtype,
|
|
arg0_dtype,
|
|
arg1_dtype,
|
|
use_3xtf32,
|
|
find_first_valid,
|
|
use_multiprocessing,
|
|
):
|
|
"""Profile and select a kernel for batch_matmul op workload."""
|
|
MM = arg0_shape[1]
|
|
KK = arg0_shape[2]
|
|
NN = arg1_shape[1]
|
|
|
|
name, cutlass_op_def = select_gemm_kernel(
|
|
cutlass_profiler,
|
|
op_type,
|
|
MM,
|
|
KK,
|
|
NN,
|
|
out_dtype,
|
|
arg0_dtype,
|
|
arg1_dtype,
|
|
use_3xtf32,
|
|
True,
|
|
find_first_valid,
|
|
use_multiprocessing,
|
|
)
|
|
|
|
return {
|
|
"batch": arg0_shape[0],
|
|
"batch_stride_A": arg0_shape[1] * arg0_shape[2],
|
|
"batch_stride_B": arg1_shape[1] * arg1_shape[2],
|
|
"batch_stride_C": arg0_shape[1] * arg1_shape[1],
|
|
"cutlass_op_def": cutlass_op_def,
|
|
"cutlass_op_name": name,
|
|
"lda": "K",
|
|
"ldb": "K",
|
|
"ldc": "N",
|
|
}
|
|
|
|
|
|
def handle_dense(
|
|
cutlass_profiler,
|
|
op_type,
|
|
arg0_shape,
|
|
arg1_shape,
|
|
out_dtype,
|
|
arg0_dtype,
|
|
arg1_dtype,
|
|
use_3xtf32,
|
|
find_first_valid,
|
|
use_multiprocessing,
|
|
):
|
|
"""Profile and select a kernel for dense op workload."""
|
|
MM = arg0_shape[0]
|
|
KK = arg0_shape[1]
|
|
NN = arg1_shape[0]
|
|
|
|
name, cutlass_op_def = select_gemm_kernel(
|
|
cutlass_profiler,
|
|
op_type,
|
|
MM,
|
|
KK,
|
|
NN,
|
|
out_dtype,
|
|
arg0_dtype,
|
|
arg1_dtype,
|
|
use_3xtf32,
|
|
False,
|
|
find_first_valid,
|
|
use_multiprocessing,
|
|
)
|
|
|
|
assert "tn_align" in name, "Only supports (row_major, col_major) input layout for now."
|
|
|
|
return {
|
|
"cutlass_op_def": cutlass_op_def,
|
|
"cutlass_op_name": name,
|
|
"lda": "K",
|
|
"ldb": "K",
|
|
"ldc": "N",
|
|
}
|
|
|
|
|
|
def handle_conv2d(
|
|
cutlass_profiler,
|
|
op_type,
|
|
d_shape,
|
|
w_shape,
|
|
padding,
|
|
strides,
|
|
dilation,
|
|
out_dtype,
|
|
data_dtype,
|
|
weight_dtype,
|
|
use_3xtf32,
|
|
split_k_slices,
|
|
profile_all_alignments,
|
|
find_first_valid,
|
|
use_multiprocessing,
|
|
):
|
|
"""Profile and select a kernel for conv2d op workload."""
|
|
if "conv2d_transpose" in op_type:
|
|
conv_kind = ConvKind.Dgrad
|
|
elif "backward_weight" in op_type:
|
|
conv_kind = ConvKind.Wgrad
|
|
else:
|
|
conv_kind = ConvKind.Fprop
|
|
|
|
if any(isinstance(s, tvm.tirx.Any) for s in d_shape):
|
|
out = cutlass_profiler.get_default(
|
|
op_type, out_dtype, data_dtype, weight_dtype, use_3xtf32, conv_kind, strides
|
|
)
|
|
name, cutlass_op_def = out["name"], out["opdef"]
|
|
logger.info("Picked the default kernel %s", name)
|
|
else:
|
|
name, cutlass_op_def, _ = cutlass_profiler.profile(
|
|
op_type,
|
|
d_shape,
|
|
w_shape,
|
|
padding,
|
|
strides,
|
|
dilation,
|
|
out_dtype,
|
|
data_dtype,
|
|
weight_dtype,
|
|
use_3xtf32,
|
|
conv_kind,
|
|
split_k_slices,
|
|
profile_all_alignments,
|
|
find_first_valid=find_first_valid,
|
|
use_multiprocessing=use_multiprocessing,
|
|
)
|
|
if not find_first_valid:
|
|
logger.info("The best kernel is %s", name)
|
|
else:
|
|
logger.info("Picked the first kernel found %s", name)
|
|
|
|
return {"cutlass_op_def": cutlass_op_def, "cutlass_op_name": name}
|
|
|
|
|
|
def num_cutlass_partitions(mod):
|
|
return sum([(1 if "cutlass" in var.name_hint else 0) for var in mod.get_global_vars()])
|
|
|
|
|
|
def tune_cutlass_kernels(
|
|
mod,
|
|
sm,
|
|
use_3xtf32=True,
|
|
split_k_slices=[1],
|
|
profile_all_alignments=False,
|
|
find_first_valid=False,
|
|
use_multiprocessing=False,
|
|
tmp_dir="./tmp",
|
|
):
|
|
"""Given a module partitioned for CUTLASS offloading, profile each workload to select which
|
|
kernels to emit.
|
|
|
|
Parameters
|
|
----------
|
|
mod : IRModule
|
|
The IRModule with cutlass partitions.
|
|
|
|
sm : int
|
|
An integer specifying the compute capability. For example, 75 for Turing and
|
|
80 or 86 for Ampere.
|
|
|
|
use_3xtf32 : bool
|
|
Wheter or not use slower but very accurate (compared to tf32) 3xtf32 mode for
|
|
fp32 inputs on tensorcore.
|
|
|
|
split_k_slices : list of int
|
|
Split factor candidates for split-K GEMM. If split-K > 1, the GEMM K-loop is computed in
|
|
parallel across split-K blocks, and a separate global reduction kernel is launched to
|
|
accumulate partial reductions. The profiler will pick the best split-k factor from the
|
|
given candidate list. Note that the larger split-K factor requires a larger workspace.
|
|
Currently, parallel split-k has been tested only for wgrad. For GEMM and other conv2d
|
|
kinds, split_k_slices is ignored.
|
|
|
|
profile_all_alignments : bool
|
|
When True, profile all kernal variants with smaller alignments than the largest possible.
|
|
|
|
find_first_valid : bool
|
|
Whether or not profile all candidate kernels, or stop profiling after
|
|
the first applicable kernel is found.
|
|
|
|
use_multiprocessing : bool
|
|
Whether or not compile profiler executables for different kernels in parallel.
|
|
|
|
tmp_dir : string, optional
|
|
A temporary directory where intermediate compiled artifacts will be stored.
|
|
|
|
Returns
|
|
-------
|
|
mod : IRModule
|
|
The updated module annotated with cutlass profiling information.
|
|
|
|
num_cutlass_partition : int
|
|
The number of partitioned functions created for CUTLASS.
|
|
"""
|
|
gemm_profiler = CutlassGemmProfiler(sm, _get_cutlass_path(), tmp_dir)
|
|
conv2d_profiler = CutlassConv2DProfiler(sm, _get_cutlass_path(), tmp_dir)
|
|
num_cutlass_partition = 0
|
|
for var in mod.get_global_vars():
|
|
fun_name = var.name_hint
|
|
func = mod[fun_name]
|
|
if "cutlass" in fun_name:
|
|
num_cutlass_partition += 1
|
|
new_func = tune_cutlass_function(
|
|
func,
|
|
use_3xtf32,
|
|
split_k_slices,
|
|
profile_all_alignments,
|
|
find_first_valid,
|
|
use_multiprocessing,
|
|
gemm_profiler,
|
|
conv2d_profiler,
|
|
)
|
|
mod.update_func(var, new_func)
|
|
|
|
return mod, num_cutlass_partition
|
|
|
|
|
|
def _get_call_node(expr: relax.Expr, op_name: str) -> relax.Call | None:
|
|
node = None
|
|
|
|
def fvisit(e):
|
|
nonlocal node
|
|
if isinstance(e, relax.Call) and e.op.name == op_name:
|
|
node = e
|
|
|
|
relax.analysis.post_order_visit(expr, fvisit)
|
|
return node
|
|
|
|
|
|
def _extract_relax_function_signature(f):
|
|
signature = {}
|
|
|
|
for i, arg in enumerate(f.params):
|
|
ty = arg.ty
|
|
if isinstance(ty, relax.TensorType):
|
|
signature[f"arg{i}_shape"] = get_const_tuple(ty.shape)
|
|
signature[f"arg{i}_dtype"] = ty.dtype
|
|
elif isinstance(ty, relax.ShapeType):
|
|
signature[f"arg{i}_shape"] = get_const_tuple(ty.values)
|
|
else:
|
|
raise NotImplementedError()
|
|
|
|
ret_ty = f.ret_ty
|
|
if ret_ty.shape is not None:
|
|
signature["ret_shape"] = get_const_tuple(ret_ty.shape)
|
|
else:
|
|
signature["ret_shape"] = None
|
|
signature["ret_dtype"] = ret_ty.dtype
|
|
|
|
return signature
|
|
|
|
|
|
def _extract_arg_idx(pattern_name, f):
|
|
extract_func = tvm.get_global_func("relax.contrib.extract_arg_idx")
|
|
arg_indices = extract_func(pattern_name, f)
|
|
return {k: int(v) for k, v in arg_indices.items()}
|
|
|
|
|
|
def is_shape_valid_for_cutlass_matmul(
|
|
lhs_shape: Sequence[tvm.ir.Expr],
|
|
rhs_shape: Sequence[tvm.ir.Expr],
|
|
) -> bool:
|
|
"""
|
|
Check whether the shape of inputs can be handled by CUTLASS GEMM.
|
|
|
|
The stride-based batch matmul in CUTLASS cannot handle cases that some of
|
|
the batch dimensions need to be stretched while others don't. This means
|
|
it can only handle ND x ND whose batch dimensions match exactly on both side,
|
|
as well as ND x 2D and 2D x ND. For example, it cannot handle matmul with shape
|
|
(2, 1, 4, 8) x (2, 3, 8, 16), because the batch stride of lhs is not constant.
|
|
"""
|
|
if not isinstance(lhs_shape[-1], tvm.tirx.expr.IntImm | int):
|
|
# Reduction axis must be constant
|
|
return False
|
|
|
|
lhs_batches = reduce(operator.mul, lhs_shape[:-2], 1)
|
|
rhs_batches = reduce(operator.mul, rhs_shape[:-2], 1)
|
|
if lhs_batches == 1 or rhs_batches == 1:
|
|
# This could be regular matmul or batch matmul with shape ND x 2D or 2D x ND
|
|
return True
|
|
|
|
analyzer = tvm.arith.Analyzer()
|
|
# If one side has less dimensions, use 1 to fill the gap
|
|
batch_dim_pairs = list(
|
|
itertools.zip_longest(
|
|
list(lhs_shape)[-3::-1], # Remove the last two dimensions and reverse
|
|
list(rhs_shape)[-3::-1],
|
|
fillvalue=1,
|
|
)
|
|
)
|
|
return all(analyzer.can_prove_equal(p[0], p[1]) for p in batch_dim_pairs)
|
|
|
|
|
|
@relax.expr_functor.mutator
|
|
class CutlassRelaxFunctionAnnotator(relax.PyExprMutator):
|
|
"""A Relax function mutator that tunes and annotates CUTLASS composite functions
|
|
with shape, dtype and generated templates.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
mod,
|
|
conv2d_profiler: CutlassConv2DProfiler,
|
|
gemm_profiler: CutlassGemmProfiler,
|
|
options,
|
|
):
|
|
super().__init__(mod)
|
|
self.options = options
|
|
self.conv2d_profiler = conv2d_profiler
|
|
self.gemm_profiler = gemm_profiler
|
|
|
|
def handle_conv2d(self, f, op_type):
|
|
"""Tune and annotate a conv2d op."""
|
|
signature = _extract_relax_function_signature(f)
|
|
arg_idx = _extract_arg_idx(op_type, f)
|
|
op_attrs = _get_call_node(f.body, "relax.nn.conv2d").attrs
|
|
|
|
data_arg = f"arg{arg_idx['lhs']}"
|
|
weight_arg = f"arg{arg_idx['rhs']}"
|
|
|
|
d_shape = signature[f"{data_arg}_shape"]
|
|
w_shape = signature[f"{weight_arg}_shape"]
|
|
out_shape = signature["ret_shape"]
|
|
data_dtype = signature[f"{data_arg}_dtype"]
|
|
weight_dtype = signature[f"{weight_arg}_dtype"]
|
|
out_dtype = signature["ret_dtype"]
|
|
padding = op_attrs["padding"]
|
|
strides = op_attrs["strides"]
|
|
dilation = op_attrs["dilation"]
|
|
conv_kind = ConvKind.Fprop
|
|
|
|
use_3xtf32 = self.options.get("use_3xtf32", False)
|
|
profile_all_alignments = self.options.get("profile_all_alignments", False)
|
|
find_first_valid = self.options.get("find_first_valid", True)
|
|
use_multiprocessing = self.options.get("use_multiprocessing", True)
|
|
split_k_slices = self.options.get("split_k_slices", [1])
|
|
|
|
op_name, op_def, _ = self.conv2d_profiler.profile(
|
|
op_type,
|
|
d_shape,
|
|
w_shape,
|
|
padding,
|
|
strides,
|
|
dilation,
|
|
out_dtype,
|
|
data_dtype,
|
|
weight_dtype,
|
|
use_3xtf32,
|
|
conv_kind,
|
|
split_k_slices,
|
|
profile_all_alignments,
|
|
find_first_valid=find_first_valid,
|
|
use_multiprocessing=use_multiprocessing,
|
|
)
|
|
|
|
attrs = {
|
|
"op_type": op_type,
|
|
"data_arg_idx": arg_idx["lhs"],
|
|
"weight_arg_idx": arg_idx["rhs"],
|
|
"bias_arg_idx": arg_idx.get("bias"),
|
|
"residual_arg_idx": arg_idx.get("residual"),
|
|
"arg0_dtype": data_dtype,
|
|
"arg1_dtype": weight_dtype,
|
|
"ret_dtype": out_dtype,
|
|
"arg0_shape": d_shape,
|
|
"arg1_shape": w_shape,
|
|
"ret_shape": out_shape,
|
|
"strides": strides,
|
|
"padding": padding,
|
|
"dilation": dilation,
|
|
"cutlass_op_name": op_name,
|
|
"cutlass_op_def": op_def,
|
|
}
|
|
|
|
residual_arg = arg_idx.get("residual")
|
|
|
|
if residual_arg:
|
|
residual_shape = signature[f"arg{residual_arg}_shape"]
|
|
attrs["residual_shape"] = residual_shape
|
|
elif "residual" in op_type:
|
|
attrs["residual_shape"] = d_shape
|
|
|
|
return f.with_attrs(attrs)
|
|
|
|
def handle_decode_matmul(self, f, op_type):
|
|
"""Annotate a decode -> matmul op."""
|
|
arg_idx = _extract_arg_idx(op_type, f)
|
|
signature = _extract_relax_function_signature(f)
|
|
lhs_arg = f"arg{arg_idx['lhs']}"
|
|
rhs_arg = f"arg{arg_idx['w_encoded']}"
|
|
lhs_shape = signature[f"{lhs_arg}_shape"]
|
|
rhs_shape = signature[f"{rhs_arg}_shape"]
|
|
ret_shape = signature["ret_shape"]
|
|
scale_arg = f"arg{arg_idx['scales']}"
|
|
scale_shape = signature[f"{scale_arg}_shape"]
|
|
N = ret_shape[-1]
|
|
|
|
attrs = {
|
|
"op_type": op_type,
|
|
"lhs_arg_idx": arg_idx["lhs"],
|
|
"rhs_arg_idx": arg_idx["w_encoded"],
|
|
"scales_arg_idx": arg_idx["scales"],
|
|
"bias_arg_idx": arg_idx.get("bias"),
|
|
"activation": "identity",
|
|
}
|
|
# TODO(wuwei): find a better way to get group size
|
|
attrs["group_size"] = 64 if len(scale_shape) == 2 and scale_shape[0] != 1 else -1
|
|
|
|
attrs["batch_rank"] = len(lhs_shape[:-1])
|
|
attrs["M"] = reduce(operator.mul, lhs_shape[:-1], 1)
|
|
|
|
attrs["bias_stride"] = 0
|
|
|
|
if "bias" in arg_idx:
|
|
bias_shape = signature[f"arg{arg_idx['bias']}_shape"]
|
|
bias_shape_1d = reduce(operator.mul, bias_shape, 1)
|
|
if bias_shape_1d != bias_shape[-1]:
|
|
attrs["bias_stride"] = bias_shape[-1]
|
|
|
|
if N == rhs_shape[1]:
|
|
attrs["weight_nbit"] = 8
|
|
else:
|
|
assert N == rhs_shape[1] * 2
|
|
attrs["weight_nbit"] = 4
|
|
|
|
if "residual" in op_type:
|
|
residual_pos = op_type.find("residual_")
|
|
postfix = op_type[residual_pos + len("residual_") :]
|
|
|
|
if postfix.startswith("multiply"):
|
|
binary_op = "multiply"
|
|
else:
|
|
binary_op = "plus"
|
|
|
|
if "relu" in postfix:
|
|
unary_op = "relu"
|
|
else:
|
|
unary_op = "identity"
|
|
|
|
activation = "identity"
|
|
|
|
for act in ["relu", "silu", "gelu"]:
|
|
if act in op_type[op_type.find("matmul_") + len("matmul_") : residual_pos]:
|
|
activation = act
|
|
break
|
|
|
|
attrs.update(
|
|
{
|
|
"unary_op": unary_op,
|
|
"binary_op": binary_op,
|
|
"activation": activation,
|
|
"residual_arg_idx": arg_idx["residual"],
|
|
}
|
|
)
|
|
else:
|
|
for act in ["relu", "silu", "gelu"]:
|
|
if act in op_type:
|
|
attrs["activation"] = act
|
|
break
|
|
|
|
return f.with_attrs(attrs)
|
|
|
|
def handle_matmul(self, f, op_type):
|
|
"""Tune and annotate a matmul op."""
|
|
signature = _extract_relax_function_signature(f)
|
|
arg_idx = _extract_arg_idx(op_type, f)
|
|
|
|
lhs_arg = f"arg{arg_idx['lhs']}"
|
|
rhs_arg = f"arg{arg_idx['rhs']}"
|
|
|
|
lhs_shape = signature[f"{lhs_arg}_shape"]
|
|
rhs_shape = signature[f"{rhs_arg}_shape"]
|
|
out_shape = signature["ret_shape"]
|
|
lhs_dtype = signature[f"{lhs_arg}_dtype"]
|
|
rhs_dtype = signature[f"{rhs_arg}_dtype"]
|
|
out_dtype = signature["ret_dtype"]
|
|
|
|
if not is_shape_valid_for_cutlass_matmul(lhs_shape, rhs_shape):
|
|
raise ValueError(f"Cannot handle the input shapes, lhs: {lhs_shape}, rhs: {rhs_shape}")
|
|
|
|
MM = lhs_shape[-2]
|
|
KK = lhs_shape[-1]
|
|
if "transposed" in op_type:
|
|
NN = rhs_shape[-2]
|
|
ldb = "K"
|
|
layout_b = LayoutType.ColumnMajor
|
|
else:
|
|
NN = rhs_shape[-1]
|
|
ldb = "N"
|
|
layout_b = LayoutType.RowMajor
|
|
|
|
lhs_batches = reduce(operator.mul, lhs_shape[:-2], 1)
|
|
rhs_batches = reduce(operator.mul, rhs_shape[:-2], 1)
|
|
if lhs_batches == 1 and rhs_batches == 1:
|
|
# Regular matmul
|
|
is_batched = False
|
|
batch_attrs = {}
|
|
else:
|
|
is_batched = True
|
|
batch_attrs = {
|
|
# If both lhs_batches and rhs_batches are greater than 1,
|
|
# they must be equal. This is checked by is_shape_valid_for_cutlass_matmul.
|
|
"batch": lhs_batches if rhs_batches == 1 else rhs_batches,
|
|
"batch_stride_A": 0 if lhs_batches == 1 else MM * KK,
|
|
"batch_stride_B": 0 if rhs_batches == 1 else KK * NN,
|
|
"batch_stride_C": MM * NN,
|
|
}
|
|
|
|
use_3xtf32 = self.options.get("use_3xtf32", False)
|
|
find_first_valid = self.options.get("find_first_valid", True)
|
|
use_multiprocessing = self.options.get("use_multiprocessing", True)
|
|
|
|
op_name, op_def, _ = self.gemm_profiler.profile(
|
|
op_type,
|
|
MM,
|
|
NN,
|
|
KK,
|
|
out_dtype,
|
|
lhs_dtype,
|
|
rhs_dtype,
|
|
use_3xtf32,
|
|
batched=is_batched,
|
|
find_first_valid=find_first_valid,
|
|
use_multiprocessing=use_multiprocessing,
|
|
layout_b=layout_b,
|
|
)
|
|
|
|
return f.with_attrs(
|
|
{
|
|
"op_type": op_type,
|
|
"lhs_arg_idx": arg_idx["lhs"],
|
|
"rhs_arg_idx": arg_idx["rhs"],
|
|
"residual_arg_idx": arg_idx.get("residual"),
|
|
"bias_arg_idx": arg_idx.get("bias"),
|
|
"arg0_dtype": signature["arg0_dtype"],
|
|
"arg1_dtype": signature["arg1_dtype"],
|
|
"ret_dtype": out_dtype,
|
|
"arg0_shape": signature["arg0_shape"],
|
|
"arg1_shape": signature["arg1_shape"],
|
|
"ret_shape": out_shape,
|
|
"lda": "K",
|
|
"ldb": ldb,
|
|
"ldc": "N",
|
|
"cutlass_op_name": op_name,
|
|
"cutlass_op_def": op_def,
|
|
**batch_attrs,
|
|
}
|
|
)
|
|
|
|
def handle_attention(self, f, op_type):
|
|
"""Annotate an attention op."""
|
|
signature = _extract_relax_function_signature(f)
|
|
|
|
if _get_call_node(f.body, "relax.nn.attention") is not None:
|
|
attention_node = _get_call_node(f.body, "relax.nn.attention")
|
|
op_attrs = attention_node.attrs
|
|
elif _get_call_node(f.body, "relax.nn.attention_bias") is not None:
|
|
attention_node = _get_call_node(f.body, "relax.nn.attention_bias")
|
|
op_attrs = attention_node.attrs
|
|
elif _get_call_node(f.body, "relax.nn.attention_var_len") is not None:
|
|
attention_node = _get_call_node(f.body, "relax.nn.attention_var_len")
|
|
op_attrs = attention_node.attrs
|
|
else:
|
|
raise ValueError("Cannot find call node for attention")
|
|
arg = {}
|
|
|
|
if "stacked_attention" in op_type:
|
|
arg["arg0_dtype"] = signature["arg0_dtype"]
|
|
q_shape = get_const_tuple(attention_node.args[0].ty.shape)
|
|
k_shape = get_const_tuple(attention_node.args[1].ty.shape)
|
|
v_shape = get_const_tuple(attention_node.args[2].ty.shape)
|
|
if len(attention_node.args) == 4:
|
|
arg["bias_shape"] = get_const_tuple(attention_node.args[3].ty.shape)
|
|
arg["bias_dtype"] = attention_node.args[3].ty.dtype
|
|
|
|
qkv_layout = "qkv_stacked"
|
|
else:
|
|
# arg0: q, arg1: k, arg2: v, arg3: bias, arg4: workspace
|
|
arg["arg0_shape"] = q_shape = signature["arg0_shape"]
|
|
arg["arg1_shape"] = k_shape = signature["arg1_shape"]
|
|
arg["arg2_shape"] = v_shape = signature["arg2_shape"]
|
|
arg["arg0_dtype"] = signature["arg0_dtype"]
|
|
arg["arg1_dtype"] = signature["arg1_dtype"]
|
|
arg["arg2_dtype"] = signature["arg2_dtype"]
|
|
|
|
if "arg4_dtype" in signature:
|
|
arg["bias_dtype"] = signature["arg3_dtype"]
|
|
if "arg4_shape" in signature:
|
|
arg["bias_shape"] = signature["arg3_shape"]
|
|
|
|
qkv_layout = "default"
|
|
|
|
out_shape = signature["ret_shape"]
|
|
out_dtype = signature["ret_dtype"]
|
|
num_batches, num_queries, num_q_heads, head_dim = q_shape
|
|
_, num_keys, num_kv_heads, _ = k_shape
|
|
_, _, _, head_dim_value = v_shape
|
|
scale = op_attrs.scale
|
|
|
|
if op_attrs.causal_mask is None:
|
|
custom_mask_type = 0
|
|
elif op_attrs.causal_mask == "TopLeft":
|
|
custom_mask_type = 1
|
|
elif op_attrs.causal_mask == "BottomRight":
|
|
custom_mask_type = 2
|
|
else:
|
|
raise NotImplementedError()
|
|
|
|
attrs = {
|
|
"op_type": op_type,
|
|
"ret_dtype": out_dtype,
|
|
"ret_shape": out_shape,
|
|
"num_batches": num_batches,
|
|
"num_queries": num_queries,
|
|
"num_keys": num_keys,
|
|
"num_q_heads": num_q_heads,
|
|
"num_kv_heads": num_kv_heads,
|
|
"head_dim": head_dim,
|
|
"head_dim_value": head_dim_value,
|
|
"scale": scale,
|
|
"arch": self.options["sm"],
|
|
"qkv_layout": qkv_layout,
|
|
"custom_mask_type": custom_mask_type,
|
|
**arg,
|
|
}
|
|
|
|
if "var_len" in op_type:
|
|
arg_idx = _extract_arg_idx(op_type, f)
|
|
for arg in ["seqstart_q", "seqstart_k", "max_seqlen_q", "max_seqlen_k"]:
|
|
if arg in arg_idx:
|
|
attrs[arg + "_idx"] = arg_idx[arg]
|
|
|
|
if op_attrs.window_size:
|
|
attrs["window_size"] = op_attrs.window_size
|
|
|
|
return f.with_attrs(attrs)
|
|
|
|
def handle_norm(self, f, op_type):
|
|
"""Annotate a layer or rms norm op."""
|
|
signature = _extract_relax_function_signature(f)
|
|
attrs = {}
|
|
attrs["batch_rank"] = len(signature["arg0_shape"][:-1])
|
|
attrs["M"] = reduce(operator.mul, signature["arg0_shape"][:-1], 1)
|
|
attrs["N"] = signature["arg0_shape"][-1]
|
|
dtype = signature["arg0_dtype"]
|
|
attrs["data_type"] = {"float32": "float", "float16": "cutlass::half_t"}[str(dtype)]
|
|
|
|
if "rms" in op_type:
|
|
attrs["rms_eps"] = self.options.get("rms_eps", 1e-5)
|
|
else:
|
|
attrs["layer_norm_eps"] = self.options.get("layer_nrom_eps", 1e-5)
|
|
|
|
return f.with_attrs(attrs)
|
|
|
|
def visit_function_(self, f):
|
|
if "Composite" not in f.attrs:
|
|
body = super().visit_expr(f.body)
|
|
return relax.Function(f.params, body, f.ret_ty, f.is_pure, f.attrs, f.span)
|
|
|
|
op_type = f.attrs["Composite"]
|
|
|
|
if "conv2d" in op_type:
|
|
return self.handle_conv2d(f, op_type)
|
|
elif "decode" in op_type:
|
|
return self.handle_decode_matmul(f, op_type)
|
|
elif "matmul" in op_type:
|
|
return self.handle_matmul(f, op_type)
|
|
elif "attention" in op_type:
|
|
return self.handle_attention(f, op_type)
|
|
elif "layer_norm" in op_type or "rms_norm" in op_type:
|
|
return self.handle_norm(f, op_type)
|
|
|
|
raise ValueError(f"Unsupported composite {op_type}")
|
|
|
|
def visit_span(self, span):
|
|
return span
|
|
|
|
|
|
@register_global_func("contrib.cutlass.tune_relax_function")
|
|
def profile_relax_function(functions, options):
|
|
"""Tune and annotate CUTLASS composite functions with shape, dtype and generated templates."""
|
|
tmp_dir = options.get("tmp_dir", "./tmp")
|
|
sm = options.get("sm", 80)
|
|
conv2d_profiler = CutlassConv2DProfiler(sm, _get_cutlass_path(), tmp_dir)
|
|
gemm_profiler = CutlassGemmProfiler(sm, _get_cutlass_path(), tmp_dir)
|
|
|
|
annotated_functions = []
|
|
|
|
for f in functions:
|
|
annotator = CutlassRelaxFunctionAnnotator(
|
|
tvm.IRModule.from_expr(f), conv2d_profiler, gemm_profiler, options
|
|
)
|
|
annotated_functions.append(annotator.visit_expr(f))
|
|
|
|
return annotated_functions
|
|
|
|
|
|
@register_global_func("contrib.cutlass.compile")
|
|
def compile_cutlass_module(c_source_module, options):
|
|
"""Compile all CUTLASS kernels in the given C-source module.
|
|
|
|
Parameters
|
|
----------
|
|
c_source_module: runtime.Module
|
|
A C-source module containing CUTLASS kernels.
|
|
|
|
options: dict
|
|
Compilation options. Currently recognizes
|
|
"sm": The target architecture (compute capability), for example 75 or 80 (default: 80)
|
|
"threads": The number of threads to use in NVCC parallel compilation (default:
|
|
use all logical cores)
|
|
"use_fast_math": Whether or not to use faster but approximate arithmetic in some
|
|
CUTLASS epilogues (default: False)
|
|
|
|
Returns
|
|
-------
|
|
rt_mod : runtime.Module
|
|
A runtime module where all cutlass kernels have been compiled.
|
|
"""
|
|
tmp_dir = options.get("tmp_dir", "./tmp")
|
|
defaults = {"sm": 80, "threads": -1, "use_fast_math": False}
|
|
compile_config = {key: options.get(key, val) for key, val in defaults.items()}
|
|
|
|
function_names = c_source_module.get_function("get_func_names")()
|
|
compile_options = _get_cutlass_compile_options(**compile_config)
|
|
lib_path = os.path.join(tmp_dir, "cutlass.o")
|
|
logger.info("Compiling generated CUTLASS code")
|
|
c_source_module.export_library(lib_path, workspace_dir=tmp_dir, **compile_options)
|
|
|
|
# Recover static library
|
|
return tvm.runtime.load_static_library(lib_path, function_names)
|
|
|
|
|
|
def finalize_modules(lib, lib_path="compile.so", tmp_dir="./tmp"):
|
|
"""Returns lib with any C source, LLVM and static library modules complied and linked in ready
|
|
for use by the graph or AOT executors. This method is not specific to CUTLASS, however it does
|
|
assume nvcc will be used for final compilation and linking. It is provided here for
|
|
convenience.
|
|
|
|
Parameters
|
|
----------
|
|
lib : runtime.Module
|
|
The output from build.
|
|
|
|
lib_path : string
|
|
The path to a shared library which will be generated as the result of the build process.
|
|
|
|
tmp_dir : string
|
|
A temporary directory where intermediate compiled artifacts will be stored.
|
|
|
|
Returns
|
|
-------
|
|
updated_lib : runtime.Module
|
|
The updated library with all compilation and linking completed.
|
|
|
|
"""
|
|
lib_path = os.path.join(tmp_dir, lib_path)
|
|
lib.export_library(lib_path, workspace_dir=tmp_dir, cc="nvcc")
|
|
return runtime.load_module(lib_path)
|