chore: import upstream snapshot with attribution
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# 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.
"""Contrib APIs of TVM python package.
Contrib API provides many useful not core features.
Some of these are useful utilities to interact with
thirdparty libraries and tools.
"""
+94
View File
@@ -0,0 +1,94 @@
# 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.
"""External function interface to BLAS libraries."""
import tvm
from tvm import te
def matmul(lhs, rhs, transa=False, transb=False, **kwargs):
"""Create an extern op that compute matrix mult of A and rhs with CrhsLAS
This function serves as an example on how to call external libraries.
Parameters
----------
lhs: Tensor
The left matrix operand
rhs: Tensor
The right matrix operand
transa: bool
Whether transpose lhs
transb: bool
Whether transpose rhs
Returns
-------
C: Tensor
The result tensor.
"""
n = lhs.shape[1] if transa else lhs.shape[0]
m = rhs.shape[0] if transb else rhs.shape[1]
return te.extern(
(n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cblas.matmul", ins[0], ins[1], outs[0], transa, transb
),
name="C",
**kwargs,
)
def batch_matmul(lhs, rhs, transa=False, transb=False, iterative=False, **kwargs):
"""Create an extern op that compute batched matrix mult of A and rhs with CBLAS
This function serves as an example on how to call external libraries.
Parameters
----------
lhs: Tensor
The left matrix operand
rhs: Tensor
The right matrix operand
transa: bool
Whether transpose lhs
transb: bool
Whether transpose rhs
Returns
-------
C: Tensor
The result tensor.
"""
b = te.max(lhs.shape[0], rhs.shape[0])
n = lhs.shape[2] if transa else lhs.shape[1]
m = rhs.shape[1] if transb else rhs.shape[2]
return te.extern(
(b, n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cblas.batch_matmul"
if not iterative
else "tvm.contrib.cblas.batch_matmul_iterative",
ins[0],
ins[1],
outs[0],
transa,
transb,
),
name="C",
**kwargs,
)
+76
View File
@@ -0,0 +1,76 @@
# 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.
"""CoreML runtime that load and run coreml models."""
import tvm_ffi
from ..rpc import base as rpc_base
def create(symbol, compiled_model_path, device):
"""Create a runtime executor module given a coreml model and context.
Parameters
----------
symbol : str
The symbol that represents the Core ML model.
compiled_model_path : str
The path of the compiled model to be deployed.
device : Device
The device to deploy the module. It can be local or remote when there
is only one Device.
Returns
-------
coreml_runtime : CoreMLModule
Runtime coreml module that can be used to execute the coreml model.
"""
device_type = device.dlpack_device_type()
runtime_func = "tvm.coreml_runtime.create"
if device_type >= rpc_base.RPC_SESS_MASK:
fcreate = device._rpc_sess.get_function(runtime_func)
else:
fcreate = tvm_ffi.get_global_func(runtime_func)
assert fcreate, "Cannot find `tvm.coreml_runtime.create` function."
return CoreMLModule(fcreate(symbol, compiled_model_path))
class CoreMLModule:
"""Wrapper runtime module.
This is a thin wrapper of the underlying TVM module.
you can also directly call set_input, run, and get_output
of underlying module functions
Parameters
----------
module : Module
The internal tvm module that holds the actual coreml functions.
Attributes
----------
module : Module
The internal tvm module that holds the actual coreml functions.
"""
def __init__(self, module):
self.module = module
self.invoke = module["invoke"]
self.set_input = module["set_input"]
self.get_output = module["get_output"]
self.get_num_outputs = module["get_num_outputs"]
+87
View File
@@ -0,0 +1,87 @@
# 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.
"""External function interface to cuBLAS libraries."""
import tvm
from tvm import te
def matmul(lhs, rhs, transa=False, transb=False, dtype=None):
"""Create an extern op that compute matrix mult of A and rhs with cuBLAS
Parameters
----------
lhs : Tensor
The left matrix operand
rhs : Tensor
The right matrix operand
transa : bool
Whether transpose lhs
transb : bool
Whether transpose rhs
Returns
-------
C : Tensor
The result tensor.
"""
n = lhs.shape[1] if transa else lhs.shape[0]
m = rhs.shape[0] if transb else rhs.shape[1]
dtype = dtype if dtype is not None else lhs.dtype
return te.extern(
(n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cublas.matmul", ins[0], ins[1], outs[0], transa, transb
),
dtype=dtype,
name="matmul_cublas",
)
def batch_matmul(lhs, rhs, transa=False, transb=False, dtype=None):
"""Create an extern op that compute batch matrix mult of A and rhs with cuBLAS
Parameters
----------
lhs : Tensor
The left matrix operand
rhs : Tensor
The right matrix operand
transa : bool
Whether transpose lhs
transb : bool
Whether transpose rhs
Returns
-------
C : Tensor
The result tensor.
"""
b = lhs.shape[0]
n = lhs.shape[2] if transa else lhs.shape[1]
m = rhs.shape[1] if transb else rhs.shape[2]
dtype = dtype if dtype is not None else lhs.dtype
return te.extern(
(b, n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cublas.batch_matmul", ins[0], ins[1], outs[0], transa, transb
),
dtype=dtype,
name="batch_matmul_cublas",
)
+55
View File
@@ -0,0 +1,55 @@
# 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.
"""External function interface to cuBLASlt libraries."""
import tvm
from tvm import te
def matmul(lhs, rhs, transa=False, transb=False, n=0, m=0, dtype=None):
"""Create an extern op that compute matrix mult of A and rhs with cuBLAS
Parameters
----------
lhs : Tensor
The left matrix operand
rhs : Tensor
The right matrix operand
transa : bool
Whether transpose lhs
transb : bool
Whether transpose rhs
Returns
-------
C : Tensor
The result tensor.
"""
if n == 0:
n = lhs.shape[1] if transa else lhs.shape[0]
if m == 0:
m = rhs.shape[0] if transb else rhs.shape[1]
dtype = dtype if dtype is not None else lhs.dtype
return te.extern(
(n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cublaslt.matmul", ins[0], ins[1], outs[0], transa, transb
),
dtype=dtype,
name="C",
)
+926
View File
@@ -0,0 +1,926 @@
# 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.
# ruff: noqa: F821
"""External function interface to CuDNN v7 library."""
# pylint: disable-msg=C0103
import ctypes
import numpy as np
import tvm_ffi
import tvm
from tvm import te
# algos can be read from cudnn.h
_FWD_ALGOS = [
"CUDNN_CONVOLUTION_FWD_ALGO_IMPLICIT_GEMM",
"CUDNN_CONVOLUTION_FWD_ALGO_IMPLICIT_PRECOMP_GEMM",
"CUDNN_CONVOLUTION_FWD_ALGO_GEMM",
"CUDNN_CONVOLUTION_FWD_ALGO_DIRECT",
"CUDNN_CONVOLUTION_FWD_ALGO_FFT",
"CUDNN_CONVOLUTION_FWD_ALGO_FFT_TILING",
"CUDNN_CONVOLUTION_FWD_ALGO_WINOGRAD",
"CUDNN_CONVOLUTION_FWD_ALGO_WINOGRAD_NONFUSED",
"CUDNN_CONVOLUTION_FWD_ALGO_COUNT",
]
def exists():
"""
Checks whether the local machine can use CuDNN.
Returns
-------
exists: bool
True if CuDNN support is enabled and a CuDNN-capable GPU
exists. Otherwise, False.
"""
func = tvm.get_global_func("tvm.contrib.cudnn.exists", allow_missing=True)
if func is None:
return False
return bool(func())
def algo_to_index(algo_type, algo_name):
"""Return a index represents the algorithm, which can be used in
calling CuDNN function
Parameters
----------
algo_type : str
One of ``"fwd"``, ``"bwd_filter"``, or ``"bwd_data"``.
algo_name : str
Algorithm name as defined in cuDNN. For example:
* fwd: ``CUDNN_CONVOLUTION_FWD_ALGO_IMPLICIT_GEMM``, etc.
* bwd_filter: ``CUDNN_CONVOLUTION_BWD_FILTER_ALGO_0``, etc.
* bwd_data: ``CUDNN_CONVOLUTION_BWD_DATA_ALGO_0``, etc.
Returns
-------
algo : int
Algorithm index
"""
idx = -1
if algo_type == "fwd":
idx = _FWD_ALGOS.index(algo_name)
elif algo_type == "bwd_filter":
idx = _BWD_FILTER_ALGOS.index(algo_name)
elif algo_type == "bwd_data":
idx = _BWD_DATA_ALGOS.index(algo_name)
assert idx >= 0
return idx
def _get_np_int32_array_handle(arr):
"""Return a void_p handle for a numpy array
Parameters
----------
arr: numpy.Tensor
source numpy array
Returns
-------
ptr: ctypes.c_void_p
pointer to the data
"""
assert arr.dtype == np.int32
ptr = arr.ctypes.data_as(ctypes.POINTER(ctypes.c_int32))
return ctypes.cast(ptr, ctypes.c_void_p)
def _prepare_global_func_params(dims, pad, stride, dilation, x_shape=None, w_shape=None):
full_dims = dims + 2
if x_shape:
assert isinstance(x_shape, list)
assert len(x_shape) == full_dims
if w_shape:
assert isinstance(w_shape, list)
assert len(w_shape) == full_dims
pad = (
np.full(dims, pad, dtype=np.int32)
if isinstance(pad, int)
else np.array(pad, dtype=np.int32)
)
stride = (
np.full(dims, stride, dtype=np.int32)
if isinstance(stride, int)
else np.array(stride, dtype=np.int32)
)
dilation = (
np.full(dims, dilation, dtype=np.int32)
if isinstance(dilation, int)
else np.array(dilation, dtype=np.int32)
)
xshape = np.array(x_shape, dtype=np.int32) if x_shape else None
wshape = np.array(w_shape, dtype=np.int32) if x_shape else None
return pad, stride, dilation, xshape, wshape
def conv_output_shape(
tensor_format, pad, stride, dilation, x_shape, w_shape, data_dtype, conv_dtype, groups=1
):
"""Get output shape of 2D or 3D convolution
Paramters
---------
tensor_format: int
0: CUDNN_TENSOR_NCHW
1: CUDNN_TENSOR_NHWC
2: CUDNN_TENSOR_NCHW_VECT_C
pad: int or list
padding
stride: int or list
stride
dilation: int or list
dilation
x_shape: list
input shape
w_shape: list
weight shape
data_dtype: str
data type
conv_dtype: str
convolution type
groups: int
number of groups
Returns
-------
oshape: list
output shape
"""
assert len(x_shape) == len(w_shape)
assert len(x_shape) in (4, 5)
if tensor_format == 0:
n_output = x_shape[0]
c_output = w_shape[0]
x_chan = x_shape[1]
w_chan_input = w_shape[1]
x_shape = x_shape[2:]
w_shape = w_shape[2:]
elif tensor_format == 1:
n_output = x_shape[0]
c_output = w_shape[0]
x_chan = x_shape[-1]
w_chan_input = w_shape[-1]
assert len(x_shape) == 4, "CuDNN layout NHWC is only well-defined for 4d tensors"
x_shape = x_shape[1:-1]
w_shape = w_shape[1:-1]
elif tensor_format == 2:
n_output = x_shape[0]
c_output = w_shape[0]
x_chan = x_shape[1]
w_chan_input = w_shape[1]
w_lanes = tvm.runtime.DataType(conv_dtype).lanes
assert w_lanes == 1
x_shape = x_shape[2:]
w_shape = w_shape[2:]
else:
raise ValueError(f"Unknown CuDNN tensor format: '{tensor_format}'")
x_lanes = tvm.runtime.DataType(data_dtype).lanes
assert x_chan * x_lanes == w_chan_input * groups, (
f"Mismatched dimensions, data has {x_chan // groups} channels/group "
f"(dimension {x_chan} with {x_lanes} lanes/value, {groups} groups), "
f"but weights require {w_chan_input} input channels/group"
)
output_dims = []
for x_shape_i, w_shape_i, pad_i, stride_i, dilation_i in zip(
x_shape, w_shape, pad, stride, dilation
):
output_dim = 1 + (x_shape_i + 2 * pad_i - (((w_shape_i - 1) * dilation_i) + 1)) // stride_i
output_dims.append(output_dim)
if tensor_format in [0, 2]:
output = [n_output, c_output, *output_dims]
elif tensor_format == 1:
output = [n_output, *output_dims, c_output]
else:
raise ValueError(f"Unknown CuDNN tensor format: '{tensor_format}'")
return output
def conv_dgrad_shape(
tensor_format, pad, stride, dilation, dy_shape, w_shape, output_padding=(0, 0), groups=1
):
"""Get output shape of conv2d gradient with respect to data
Paramters
---------
tensor_format: int
0: CUDNN_TENSOR_NCHW
1: CUDNN_TENSOR_NHWC
pad: int or list
padding
stride: int or list
stride
dilation: int or list
dilation
dy_shape: list
output gradient shape
w_shape: list
weight shape
data_dtype: str
data type
conv_dtype: str
convolution type
groups: int
number of groups
Returns
-------
oshape: list
output shape
"""
assert len(dy_shape) == len(w_shape)
assert len(dy_shape) == 4
if tensor_format == 0:
N = dy_shape[0]
C = w_shape[1] * groups
dy_shape = dy_shape[2:]
w_shape = w_shape[2:]
elif tensor_format == 1:
N = dy_shape[0]
C = w_shape[-1] * groups
dy_shape = dy_shape[1:-1]
w_shape = w_shape[1:-1]
else:
raise ValueError(f"Unsupported CuDNN tensor format: '{tensor_format}'")
input_dims = []
for dy_shape_i, w_shape_i, pad_i, stride_i, dilation_i, out_pad in zip(
dy_shape, w_shape, pad, stride, dilation, output_padding
):
input_dim = (
(dy_shape_i - 1) * stride_i - 2 * pad_i + (((w_shape_i - 1) * dilation_i) + 1) + out_pad
)
input_dims.append(input_dim)
if tensor_format == 0:
output = [N, C, *input_dims]
else:
output = [N, *input_dims, C]
return output
def _conv_find_algo(
func_name,
tensor_format,
pad,
stride,
dilation,
x_shape,
w_shape,
y_shape,
data_dtype,
conv_dtype,
groups=1,
verbose=False,
):
"""
Common function to choose the best cudnn convolution algorithm for the given input
and the convolution type.
"""
dims = len(x_shape)
assert dims in (4, 5)
pad, stride, dilation, xshape, wshape = _prepare_global_func_params(
dims - 2, pad, stride, dilation, x_shape, w_shape
)
yshape = np.array(y_shape, dtype=np.int32)
func = tvm_ffi.get_global_func(func_name)
return func(
tensor_format,
dims - 2,
_get_np_int32_array_handle(pad),
_get_np_int32_array_handle(stride),
_get_np_int32_array_handle(dilation),
_get_np_int32_array_handle(xshape),
_get_np_int32_array_handle(wshape),
_get_np_int32_array_handle(yshape),
data_dtype,
conv_dtype,
groups,
verbose,
)
def conv_forward_find_algo(
tensor_format,
pad,
stride,
dilation,
x_shape,
w_shape,
y_shape,
data_dtype,
conv_dtype,
groups=1,
verbose=True,
):
"""Choose the best forward algorithm for the given input.
Paramters
---------
tensor_format: int
0: CUDNN_TENSOR_NCHW
1: CUDNN_TENSOR_NHWC
2: CUDNN_TENSOR_NCHW_VECT_C
pad: int or list
padding
stride: int or list
stride
dilation: int or list
dilation
x_shape: list
input shape
w_shape: list
weight shape
y_shape: list
output shape
data_dtype: str
data type
conv_dtype: str
convolution type
groups: int
number of groups
Returns
-------
algo: int
algo chosen by CUDNN
"""
return _conv_find_algo(
"tvm.contrib.cudnn.conv.forward_find_algo",
tensor_format,
pad,
stride,
dilation,
x_shape,
w_shape,
y_shape,
data_dtype,
conv_dtype,
groups,
verbose,
)
def conv_backward_data_find_algo(
tensor_format,
pad,
stride,
dilation,
dy_shape,
w_shape,
dx_shape,
data_dtype,
conv_dtype,
groups=1,
verbose=True,
):
"""Choose the best backward data algorithm for the given input.
Paramters
---------
tensor_format: int
0: CUDNN_TENSOR_NCHW
1: CUDNN_TENSOR_NHWC
2: CUDNN_TENSOR_NCHW_VECT_C
pad: int or list
padding
stride: int or list
stride
dilation: int or list
dilation
dy_shape: list
output gradient shape
w_shape: list
weight shape
dx_shape: list
dgrad shape
data_dtype: str
data type
conv_dtype: str
convolution type
groups: int
number of groups
verbose: bool
whether to show the selection trials
Returns
-------
algo: int
algo chosen by CUDNN
"""
return _conv_find_algo(
"tvm.contrib.cudnn.conv.backward_data_find_algo",
tensor_format,
pad,
stride,
dilation,
dy_shape,
w_shape,
dx_shape,
data_dtype,
conv_dtype,
groups,
verbose,
)
def conv_backward_filter_find_algo(
tensor_format,
pad,
stride,
dilation,
dy_shape,
x_shape,
dw_shape,
data_dtype,
conv_dtype,
groups=1,
verbose=True,
):
"""Choose the best backward filter algorithm for the given input.
Paramters
---------
tensor_format: int
0: CUDNN_TENSOR_NCHW
1: CUDNN_TENSOR_NHWC
2: CUDNN_TENSOR_NCHW_VECT_C
pad: int or list
padding
stride: int or list
stride
dilation: int or list
dilation
dy_shape: list
output gradient shape
x_shape: list
weight shape
dw_shape: list
wgrad shape
data_dtype: str
data type
conv_dtype: str
convolution type
groups: int
number of groups
verbose: bool
whether to show the selection trials
Returns
-------
algo: int
algo chosen by CUDNN
"""
return _conv_find_algo(
"tvm.contrib.cudnn.conv.backward_filter_find_algo",
tensor_format,
pad,
stride,
dilation,
dy_shape,
x_shape,
dw_shape,
data_dtype,
conv_dtype,
groups,
verbose,
)
def conv_forward(
x, w, pad, stride, dilation, conv_mode, tensor_format, algo, conv_dtype, groups=1, verbose=True
):
"""Create an extern op that compute 2D or 3D convolution with CuDNN
Parameters
----------
x: Tensor
input feature map
w: Tensor
convolution weight
pad: int or list
padding
stride: int or list
stride
dilation: int or list
dilation
conv_mode: int
0: CUDNN_CONVOLUTION
1: CUDNN_CROSS_CORRELATION
tensor_format: int
0: CUDNN_TENSOR_NCHW
1: CUDNN_TENSOR_NHWC
2: CUDNN_TENSOR_NCHW_VECT_C
algo: int
Forward algorithm, get index from ```algo_to_index``` function
if algo == -1, the best algo will be chosen by CUDNN
conv_dtype: str
convolution type
groups: int
the number of groups
verbose: bool
whether to show the selection trials
Returns
-------
y: Tensor
The result tensor
"""
dims = len(x.shape)
assert dims in (4, 5)
conv_dtype = x.dtype if conv_dtype is None else conv_dtype
pad, stride, dilation, _, _ = _prepare_global_func_params(dims - 2, pad, stride, dilation)
x_shape = list(x.shape)
if isinstance(x.shape[0], tvm.tirx.expr.IntImm):
oshape = conv_output_shape(
tensor_format,
pad,
stride,
dilation,
x_shape,
list(w.shape),
x.dtype,
conv_dtype,
groups,
)
if algo == -1:
# For now if we try to call `cudnnFindConvolutionForwardAlgorithm` when
# using INT8 data type, CuDNN will crash down.
# On the other hand, CuDNN only support IMPLICIT_PRECOMP_GEMM at NHWC format
if tensor_format == 1 and conv_dtype == "int32":
algo = 1
else:
algo = conv_forward_find_algo(
tensor_format,
pad,
stride,
dilation,
list(x.shape),
list(w.shape),
oshape,
x.dtype,
conv_dtype,
groups,
verbose,
)
else:
# The dynamic batch size case, pretend this is a single batch
x_shape[0] = 1
oshape = conv_output_shape(
tensor_format,
pad,
stride,
dilation,
x_shape,
list(w.shape),
x.dtype,
conv_dtype,
groups,
)
oshape[0] = x.shape[0]
# This picks CUDNN_CONVOLUTION_FWD_ALGO_IMPLICIT_PRECOMP_GEMM
# It seems this is the fastest among algorithms that are always applicable
algo = 1
if dims == 4:
return te.extern(
oshape,
[x, w],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cudnn.conv2d.forward",
conv_mode,
tensor_format,
algo,
pad[0],
pad[1],
stride[0],
stride[1],
dilation[0],
dilation[1],
ins[0],
ins[1],
outs[0],
conv_dtype,
groups,
),
name="y",
)
return te.extern(
oshape,
[x, w],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cudnn.conv3d.forward",
conv_mode,
tensor_format,
algo,
pad[0],
pad[1],
pad[2],
stride[0],
stride[1],
stride[2],
dilation[0],
dilation[1],
dilation[2],
ins[0],
ins[1],
outs[0],
conv_dtype,
groups,
),
name="y",
)
def conv_backward_data(
dy,
w,
pad,
stride,
dilation,
conv_mode,
tensor_format,
conv_dtype,
groups=1,
output_padding=(0, 0),
):
"""Create a CuDNN extern op that computes the gradient of 2D convolution with respect to data.
Parameters
----------
dy: Tensor
output gradient
w: Tensor
convolution weight
pad: int or list
padding
stride: int or list
stride
dilation: int or list
dilation
conv_mode: int
0: CUDNN_CONVOLUTION
1: CUDNN_CROSS_CORRELATION
tensor_format: int
0: CUDNN_TENSOR_NCHW
1: CUDNN_TENSOR_NHWC
conv_dtype: str
convolution type
groups: int
the number of groups
Returns
-------
dx: Tensor
dgrad tensor
"""
dims = len(dy.shape)
assert dims == 4
conv_dtype = dy.dtype if conv_dtype is None else conv_dtype
pad, stride, dilation, _, _ = _prepare_global_func_params(dims - 2, pad, stride, dilation)
assert isinstance(dy.shape[0], tvm.tirx.expr.IntImm), (
"Dynamic batch is not supported for cudnn conv2d backwad data yet."
)
dx_shape = conv_dgrad_shape(
tensor_format, pad, stride, dilation, dy.shape, w.shape, output_padding, groups
)
if exists():
# When cudnn exists, find the backward data algo
algo = conv_backward_data_find_algo(
tensor_format,
pad,
stride,
dilation,
list(dy.shape),
list(w.shape),
dx_shape,
dy.dtype,
conv_dtype,
groups,
True,
)
else:
algo = 1
return te.extern(
dx_shape,
[dy, w],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cudnn.conv2d.backward_data",
conv_mode,
tensor_format,
algo,
pad[0],
pad[1],
stride[0],
stride[1],
dilation[0],
dilation[1],
ins[0],
ins[1],
outs[0],
conv_dtype,
groups,
),
name="dx",
)
def conv_backward_filter(
dy, x, kernel_size, pad, stride, dilation, conv_mode, tensor_format, conv_dtype, groups=1
):
"""Create a CuDNN extern op that computes the gradient of 2D convolution with respect to weight.
Parameters
----------
dy: Tensor
output gradient
x: Tensor
input tensor
kernel_size: a pair of int
The spatial size of the corresponding forward convolution kernel
pad: int or list
padding
stride: int or list
stride
dilation: int or list
dilation
conv_mode: int
0: CUDNN_CONVOLUTION
1: CUDNN_CROSS_CORRELATION
tensor_format: int
0: CUDNN_TENSOR_NCHW
1: CUDNN_TENSOR_NHWC
conv_dtype: str
convolution type
groups: int
the number of groups
Returns
-------
dw: Tensor
wgrad tensor
"""
dims = len(x.shape)
assert dims == 4
conv_dtype = x.dtype if conv_dtype is None else conv_dtype
pad, stride, dilation, _, _ = _prepare_global_func_params(dims - 2, pad, stride, dilation)
filter_h, filter_w = kernel_size
x_shape = list(x.shape)
assert isinstance(x.shape[0], tvm.tirx.expr.IntImm), (
"Dynamic batch is not supported for cudnn conv2d backwad filter yet."
)
ic_ind = 1 if tensor_format == 0 else 3
if groups > 1:
assert x_shape[ic_ind] == dy.shape[ic_ind] and x_shape[ic_ind] == groups, (
"Only depthwise wgrad supported for groups > 1."
)
ic = 1
else:
ic = x_shape[ic_ind]
if tensor_format == 0:
dw_shape = [dy.shape[1], ic, filter_h, filter_w]
else:
dw_shape = [dy.shape[3], filter_h, filter_w, ic]
algo = conv_backward_filter_find_algo(
tensor_format,
pad,
stride,
dilation,
list(dy.shape),
list(x.shape),
dw_shape,
x.dtype,
conv_dtype,
groups,
True,
)
return te.extern(
dw_shape,
[dy, x],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cudnn.conv2d.backward_filter",
conv_mode,
tensor_format,
algo,
pad[0],
pad[1],
stride[0],
stride[1],
dilation[0],
dilation[1],
ins[0],
ins[1],
outs[0],
conv_dtype,
groups,
),
name="dw",
)
def softmax(x, axis=-1):
"""Compute softmax using CuDNN
Parameters
----------
x : tvm.te.Tensor
The input tensor
axis : int
The axis to compute the softmax
Returns
-------
ret : tvm.te.Tensor
The result tensor
"""
return te.extern(
x.shape,
[x],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cudnn.softmax.forward", ins[0], outs[0], axis
),
name="y",
)
def log_softmax(x, axis=-1):
"""Compute log_softmax using CuDNN
Parameters
----------
x : tvm.te.Tensor
The input tensor
axis : int
The axis to compute log softmax over
Returns
-------
ret : tvm.te.Tensor
The result tensor
"""
return te.extern(
x.shape,
[x],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.cudnn.log_softmax.forward", ins[0], outs[0], axis
),
name="y",
)
+20
View File
@@ -0,0 +1,20 @@
# isort: skip_file
# 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.
"""BYOC support for CUTLASS."""
from .build import has_cutlass, num_cutlass_partitions, finalize_modules
+21
View File
@@ -0,0 +1,21 @@
# 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.
"""FFI API for CUTLASS BYOC."""
import tvm_ffi
tvm_ffi.init_ffi_api("contrib.cutlass", __name__)
@@ -0,0 +1,327 @@
# 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
# ruff: noqa: E501
"""Generator for CUTLASS attention kernels."""
from .library import substitute_template
def instantiate_attention_template(attrs):
"""Return CUTLASS host code for fused multi head attention
based on a template and the provided attribute map."""
bias_template = """
TVM_FFI_ICHECK(${bias}->ndim == 4); // B, N, S, S'
p.attn_bias_ptr = reinterpret_cast<T *>(${bias}->data);
p.bias_strideM = ${bias_strideM};
p.bias_strideH = ${bias_strideH};
p.bias_strideB = ${bias_strideB};
"""
var_len_template = """
p.seqstart_q_ptr = (int32_t*)${seqstart_q}->data;
p.seqstart_k_ptr = (int32_t*)${seqstart_k}->data;
p.num_queries = ((int32_t*)${max_seqlen_q}->data)[0];
p.num_batches = ${seqstart_q}->shape[0] - 1;
"""
qkv_template = {
"default": """
p.query_ptr = reinterpret_cast<T *>(${query}->data);
p.key_ptr = reinterpret_cast<T *>(${key}->data);
p.value_ptr = reinterpret_cast<T *>(${value}->data);
TVM_FFI_ICHECK(${query}->ndim == 4); // B, S, N, H
TVM_FFI_ICHECK(${key}->ndim == 4); // B, S', N, H
TVM_FFI_ICHECK(${value}->ndim == 4); // B, S', N, H'
// stride for N
p.q_strideH = p.head_dim; // H
p.k_strideH = p.head_dim; // H
p.v_strideH = p.head_dim_value; // H'
// stride for S
p.q_strideM = p.q_strideH * p.num_heads; // H * N
p.k_strideM = p.k_strideH * p.num_heads; // H * N
p.v_strideM = p.v_strideH * p.num_heads; // H' * N
// stride for B
p.q_strideB = p.q_strideM * p.num_queries; // H * N * S
p.k_strideB = p.k_strideM * p.num_keys; // H * N * S'
p.v_strideB = p.v_strideM * p.num_keys; // H'* N * S'
""",
"qkv_stacked": """
p.query_ptr = reinterpret_cast<T *>(${qkv}->data);
p.key_ptr = reinterpret_cast<T *>(${qkv}->data) + p.head_dim * p.num_heads;
p.value_ptr = reinterpret_cast<T *>(${qkv}->data) + p.head_dim * p.num_heads * 2;
TVM_FFI_ICHECK(${qkv}->ndim == 3); // B, S, NH + NH + NH'
// stride for N
p.q_strideH = p.head_dim; // H
p.k_strideH = p.head_dim; // H
p.v_strideH = p.head_dim_value; // H'
// stride for S
p.q_strideM = p.k_strideM = p.v_strideM =
p.q_strideH * p.num_heads +
p.k_strideH * p.num_heads +
p.v_strideH * p.num_heads; // H * N + H * N + H * N'
// stride for B
p.q_strideB = p.k_strideB = p.v_strideB =
p.q_strideM * p.num_queries; // (H * N + H * N + H * N') * S
""",
}
template = """
using T = ${data_type};
using Attention =
AttentionKernel<T,
/*ArchTag=*/${arch},
/*is_aligned=*/${kIsAligned},
/*queries_per_block=*/${kQueriesPerBlock},
/*keys_per_block=*/${kKeysPerBlock},
/*kMaxK=*/${kMaxK},
/*supports_dropout=*/${kSupportsDropout},
/*supports_bias=*/${kSupportsBias}
>;
typename Attention::Params p;
p.logsumexp_ptr = nullptr;
p.output_ptr = reinterpret_cast<T *>(out0->data);
p.output_accum_ptr = nullptr;
uint64_t accumulator_buf_size = ${output_size} * sizeof(Attention::output_accum_t);
bool accumulator_buf_allocated = false;
if (Attention::kNeedsOutputAccumulatorBuffer) {
if (accumulator_buf_size <= ${workspace}->shape[0]) {
p.output_accum_ptr = static_cast<float*>(${workspace}->data);
} else {
accumulator_buf_allocated = true;
cudaMalloc(
&p.output_accum_ptr,
accumulator_buf_size
);
}
}
p.num_heads = ${num_heads}; // N
p.num_batches = ${num_batches}; // B
p.head_dim = ${head_dim}; // H
p.head_dim_value = ${head_dim_value}; // H'
p.num_queries = ${num_queries}; // S
p.num_keys = ${num_keys}; // S'
p.scale = ${scale};
p.custom_mask_type = ${custom_mask_type};
p.o_strideM = p.head_dim_value * p.num_heads; // H' * N
TVM_FFI_ICHECK(out0->ndim == 4); // B, S, N, H'
${qkv_template}
${bias_template}
${var_len_template}
constexpr auto kernel_fn = attention_kernel_batched_impl<Attention>;
int smem_bytes = sizeof(typename Attention::SharedStorage);
if (smem_bytes > 0xc000) {
static bool once = [&]() {
cudaFuncSetAttribute(
kernel_fn, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_bytes);
return true;
}();
}
TVM_FFI_ICHECK(Attention::check_supported(p));
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(kDLCUDA, ${query}->device.device_id));
kernel_fn<<<p.getBlocksGrid(), p.getThreadsGrid(), smem_bytes, stream>>>(p);
if (accumulator_buf_allocated) {
cudaFree(p.output_accum_ptr);
}
"""
template = substitute_template(
template,
{
"qkv_template": qkv_template[attrs["qkv_layout"]],
"bias_template": bias_template if "bias" in attrs else "",
"var_len_template": var_len_template if "seqstart_q" in attrs else "",
},
)
return substitute_template(template, attrs)
def instantiate_flash_attention_template(attrs):
"""Return host code for flash attention."""
template = """
int q_head_stride = ${head_dim};
int k_head_stride = ${head_dim};
int v_head_stride = ${head_dim};
int o_head_stride = ${head_dim};
int q_row_stride = q_head_stride * ${num_q_heads};
int k_row_stride = k_head_stride * ${num_kv_heads};
int v_row_stride = v_head_stride * ${num_kv_heads};
int o_row_stride = o_head_stride * ${num_q_heads};
int q_batch_stride = q_row_stride * ${num_queries};
int k_batch_stride = k_row_stride * ${num_keys};
int v_batch_stride = v_row_stride * ${num_keys};
int o_batch_stride = o_row_stride * ${num_queries};
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(kDLCUDA, ${query}->device.device_id));
flash_attn::flash_attention_forward(
static_cast<const cutlass::half_t*>(${query}->data),
static_cast<const cutlass::half_t*>(${key}->data),
static_cast<const cutlass::half_t*>(${value}->data),
static_cast<cutlass::half_t*>(out0->data),
${num_batches},
${num_queries},
${num_keys},
${num_q_heads},
${num_kv_heads},
${head_dim},
q_batch_stride,
k_batch_stride,
v_batch_stride,
o_batch_stride,
q_head_stride,
k_head_stride,
v_head_stride,
o_head_stride,
q_row_stride,
k_row_stride,
v_row_stride,
o_row_stride,
${scale},
${is_causal},
${window_size_left},
${window_size_right},
stream);
"""
template_stacked = """
int q_head_stride = ${head_dim};
int k_head_stride = ${head_dim};
int v_head_stride = ${head_dim};
int o_head_stride = ${head_dim};
int row_stride = q_head_stride * ${num_q_heads} +
k_head_stride * ${num_kv_heads} +
v_head_stride * ${num_kv_heads};
int q_row_stride = row_stride;
int k_row_stride = row_stride;
int v_row_stride = row_stride;
int o_row_stride = o_head_stride * ${num_q_heads};
int q_batch_stride = q_row_stride * ${num_queries};
int k_batch_stride = k_row_stride * ${num_keys};
int v_batch_stride = v_row_stride * ${num_keys};
int o_batch_stride = o_row_stride * ${num_queries};
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(kDLCUDA, ${query}->device.device_id));
flash_attn::flash_attention_forward(
static_cast<const cutlass::half_t*>(${qkv}->data),
static_cast<const cutlass::half_t*>(${qkv}->data) + ${head_dim} * ${num_q_heads},
static_cast<const cutlass::half_t*>(${qkv}->data) + ${head_dim} * (${num_q_heads} + ${num_kv_heads}),
static_cast<cutlass::half_t*>(out0->data),
${num_batches},
${num_queries},
${num_keys},
${num_q_heads},
${num_kv_heads},
${head_dim},
q_batch_stride,
k_batch_stride,
v_batch_stride,
o_batch_stride,
q_head_stride,
k_head_stride,
v_head_stride,
o_head_stride,
q_row_stride,
k_row_stride,
v_row_stride,
o_row_stride,
${scale},
${is_causal},
${window_size_left},
${window_size_right},
stream);
"""
if "qkv" in attrs:
return substitute_template(template_stacked, attrs)
return substitute_template(template, attrs)
def instantiate_flash_attention_var_len_template(attrs):
"""Return host code for flash attention with variable sequence lengths."""
template = """
int _max_seqlen_q = ((int32_t*)${max_seqlen_q}->data)[0];
int _max_seqlen_k = ((int32_t*)${max_seqlen_k}->data)[0];
int batch_size = ${seqstart_q}->shape[0] - 1;
int q_head_stride = ${head_dim};
int k_head_stride = ${head_dim};
int v_head_stride = ${head_dim};
int o_head_stride = ${head_dim};
int q_row_stride = q_head_stride * ${num_q_heads};
int k_row_stride = k_head_stride * ${num_kv_heads};
int v_row_stride = v_head_stride * ${num_kv_heads};
int o_row_stride = o_head_stride * ${num_q_heads};
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(kDLCUDA, ${query}->device.device_id));
flash_attn::flash_attention_var_len_forward(
static_cast<const cutlass::half_t*>(${query}->data),
static_cast<const cutlass::half_t*>(${key}->data),
static_cast<const cutlass::half_t*>(${value}->data),
static_cast<const int*>(${seqstart_q}->data),
static_cast<const int*>(${seqstart_k}->data),
static_cast<cutlass::half_t*>(out0->data),
batch_size,
_max_seqlen_q,
_max_seqlen_k,
${num_q_heads},
${num_kv_heads},
${head_dim},
q_head_stride,
k_head_stride,
v_head_stride,
o_head_stride,
q_row_stride,
k_row_stride,
v_row_stride,
o_row_stride,
${scale},
${is_causal},
// For SWA, is_causal must be false.
${is_causal} ? _max_seqlen_k : ${window_size_left},
${window_size_right},
stream);
"""
return substitute_template(template, attrs)
+907
View File
@@ -0,0 +1,907 @@
# 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)
@@ -0,0 +1,552 @@
# 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
# ruff: noqa: E501, F403, F405
"""Generator for CUTLASS Conv2D kernels."""
from .library import *
class Conv2dOperation:
"""Describes various attributes for instantiating Conv2d kernels."""
def __init__(
self,
conv_kind,
iterator_algorithm,
arch,
tile_description,
A,
B,
C,
element_epilogue,
stride_support,
epilogue_functor=EpilogueFunctor.LinearCombination,
swizzling_functor=SwizzlingFunctor.Identity1,
split_k_slices=1,
):
self.operation_kind = OperationKind.Conv2d
self.arch = arch
self.tile_description = tile_description
self.conv_kind = conv_kind
self.A = A
self.B = B
self.C = C
self.element_epilogue = element_epilogue
self.epilogue_functor = epilogue_functor
self.iterator_algorithm = iterator_algorithm
self.stride_support = stride_support
self.swizzling_functor = swizzling_functor
self.split_k_slices = split_k_slices
def accumulator_type(self):
return self.tile_description.math_instruction.element_accumulator
def core_name(self):
"""The basic operation kind is prefixed with a letter indicating the accumulation type."""
intermediate_type = ""
if self.tile_description.math_instruction.opcode_class == OpcodeClass.TensorOp:
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.accumulator_type()
):
intermediate_type = DataTypeNames[self.tile_description.math_instruction.element_a]
else:
inst_shape = ""
return f"{ShortDataTypeNames[self.accumulator_type()]}{inst_shape}{intermediate_type}{ConvKindNames[self.conv_kind]}_{IteratorAlgorithmNames[self.iterator_algorithm]}"
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]}"
def procedural_name(self):
"""
The full procedural name indicates architecture, extended name, tile size, and layout.
"""
opcode_class_name = OpcodeClassNames[self.tile_description.math_instruction.opcode_class]
threadblock = f"{self.tile_description.threadblock_shape[0]}x{self.tile_description.threadblock_shape[1]}_{self.tile_description.threadblock_shape[2]}x{self.tile_description.stages}"
if self.stride_support == StrideSupport.Unity:
configuration_name = (
"cutlass_${opcode_class}_${extended_name}_${threadblock}"
"_${layout}_align${alignment}_unity_stride"
)
else:
configuration_name = (
"cutlass_${opcode_class}_${extended_name}_${threadblock}"
"_${layout}_align${alignment}"
)
if self.split_k_slices > 1:
configuration_name += f"_splitk{self.split_k_slices}"
return substitute_template(
configuration_name,
{
"opcode_class": opcode_class_name,
"extended_name": self.extended_name(),
"threadblock": threadblock,
"layout": self.layout_name(),
"alignment": f"{self.A.alignment}",
},
)
class EmitConv2dInstance:
"""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.epilogue_wgrad = """
${epilogue_functor}<
${element_c},
4,
float,
float
>"""
self.template = """
// Conv2d${conv_kind_name} ${iterator_algorithm_name} kernel instance "${operation_name}"
using ${operation_name} =
typename cutlass::conv::kernel::DefaultConv2d${conv_kind_name}${conv_kernel_postfix}<
${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}, // cutlass::gemm::threadblock::GemmSplitKIdentityThreadblockSwizzle<>,
${stages},
${math_operator},
${iterator_algorithm},
${stride_support},
${align_a},
${align_b}
>::Kernel;
${reduction}
"""
self.reduction_template = """
using EpilogueOutputOp = ${epilogue};
using ReductionOp = cutlass::reduction::thread::ReduceAdd<
${element_accumulator},
${element_accumulator},
EpilogueOutputOp::kCount
>;
using ReductionKernel = cutlass::reduction::kernel::ReduceSplitK<
cutlass::MatrixShape<4, 32 * EpilogueOutputOp::kCount>,
EpilogueOutputOp,
ReductionOp
>;
using ReductionDevice = cutlass::reduction::device::ReduceSplitK<ReductionKernel>;
using ReductionStrideIndex = typename ReductionDevice::StrideIndex;
"""
def emit(
self, operation, no_beta_scaling=False, residual_block_info=False, emit_reduction=False
):
"""Instantiate a Conv2d kernel from given `operation`."""
warp_shape = [
int(
operation.tile_description.threadblock_shape[idx]
/ operation.tile_description.warp_count[idx]
)
for idx in range(3)
]
epilogue_vector_length = int(
min(operation.C.alignment * DataTypeSize[operation.C.element], 128)
/ DataTypeSize[operation.C.element]
)
element_c = operation.C.element
use_split_k_wgrad = operation.conv_kind == ConvKind.Wgrad and operation.split_k_slices > 1
# Gemm output always fp32 in wgrad with split k
element_c_gemm = DataType.f32 if use_split_k_wgrad else element_c
if emit_reduction:
epilogue_reduction = substitute_template(
self.epilogue_wgrad,
{
"epilogue_functor": EpilogueFunctorTag[operation.epilogue_functor],
"element_c": DataTypeTag[element_c],
},
)
reduction = substitute_template(
self.reduction_template,
{
"epilogue": epilogue_reduction,
"operation_name": operation.procedural_name(),
"element_accumulator": DataTypeTag[operation.accumulator_type()],
},
)
gemm_template = substitute_template(self.template, {"reduction": reduction})
else:
gemm_template = substitute_template(self.template, {"reduction": ""})
values = {
"operation_name": operation.procedural_name(),
"conv_kind": ConvKindTag[operation.conv_kind],
"conv_kind_name": ConvKindNames[operation.conv_kind].capitalize(),
"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[element_c_gemm],
"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),
"epilogue_functor": EpilogueFunctorTag[operation.epilogue_functor],
"element_epilogue": str(DataTypeTag[operation.element_epilogue]),
"swizzling_functor": SwizzlingFunctorTag[operation.swizzling_functor],
"stages": str(operation.tile_description.stages),
"iterator_algorithm": IteratorAlgorithmTag[operation.iterator_algorithm],
"iterator_algorithm_name": IteratorAlgorithmNames[
operation.iterator_algorithm
].capitalize(),
"stride_support": StrideSupportTag[operation.stride_support],
"math_operator": MathOperationTag[
operation.tile_description.math_instruction.math_operation
],
"align_a": str(operation.A.alignment),
"align_b": str(operation.B.alignment),
"conv_kernel_postfix": "",
}
if use_split_k_wgrad:
# Even if the output is fp16, gemm output is always fp32 for split k wgrad.
epilogue_gemm = substitute_template(
self.epilogue_wgrad,
{
"epilogue_functor": EpilogueFunctorTag[operation.epilogue_functor],
"element_c": "float",
},
)
template = substitute_template(gemm_template, {"epilogue": epilogue_gemm})
elif residual_block_info:
template = substitute_template(
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"],
"conv_kernel_postfix": "WithBroadcast",
}
)
elif no_beta_scaling:
template = substitute_template(
gemm_template, {"epilogue": self.epilogue_no_beta_scaling}
)
else:
template = substitute_template(gemm_template, {"epilogue": self.epilogue_default})
return substitute_template(template, values)
def instantiate_conv2d_template(attrs):
"""Return CUTLASS host code for conv2d based on a template and the provided attribute map."""
template = """
${cutlass_op_def}
using Conv2d = cutlass::conv::device::ImplicitGemmConvolution<${cutlass_op_name}>;
using ElementInputA = Conv2d::ElementA;
using ElementInputB = Conv2d::ElementB;
using ElementComputeEpilogue = Conv2d::ElementAccumulator;
int N = ${N};
int H = ${H};
int W = ${W};
int C = ${C};
int K = ${K};
int R = ${R};
int S = ${S};
int P = ${P};
int Q = ${Q};
int pad_h = ${pad_h};
int pad_w = ${pad_w};
int stride_h = ${stride_h};
int stride_w = ${stride_w};
int dilation_h = ${dilation_h};
int dilation_w = ${dilation_w};
int split_k_slices = ${split_k_slices};
cutlass::conv::Conv2dProblemSize problem_size(N, H, W, C, K, R, S, P, Q, pad_h, pad_w, stride_h, stride_w, dilation_h, dilation_w, cutlass::conv::Mode::kCrossCorrelation, split_k_slices);
const cutlass::conv::SplitKMode split_k_mode = cutlass::conv::SplitKMode::${split_k_mode};
void* ptr_a = (void*)(${data_arg}->data);
void* ptr_b = (void*)(${weight_arg}->data);
${bias_decl}
${residual_decl}
void* ptr_out = (void*)(out0->data);
ElementComputeEpilogue alpha = ElementComputeEpilogue(1);
ElementComputeEpilogue beta = ElementComputeEpilogue(${beta});
using cutlass::layout::TensorNHWC;
auto activation_shape = TensorNHWC::packed(cutlass::make_Coord(N, H, W, C));
auto weight_shape = TensorNHWC::packed(cutlass::make_Coord(K, R, S, C));
auto output_shape = TensorNHWC::packed(cutlass::make_Coord(N, P, Q, K));
${residual_shape_decl}
TensorNHWC layout_A(${A_shape});
TensorNHWC layout_B(${B_shape});
TensorNHWC layout_C(${C_shape});
TensorNHWC layout_D(${D_shape});
using ElementOutput = ${ElementOutput};
cutlass::TensorRef<ElementOutput, TensorNHWC> tensor_c{static_cast<ElementOutput*>(${tensor_c}), ${tensor_c_layout}};
cutlass::TensorRef<ElementOutput, TensorNHWC> tensor_d{static_cast<ElementOutput*>(ptr_out), layout_D};
typename Conv2d::Arguments arguments{
problem_size,
{static_cast<ElementInputA*>(ptr_a), layout_A},
{static_cast<ElementInputB*>(ptr_b), layout_B},
${tensor_c_arg},
${tensor_d_arg},
{${alpha_beta}},
split_k_mode
${additional_args}
};
Conv2d conv2d_op;
size_t workspace_size = conv2d_op.get_workspace_size(arguments);
cutlass::device_memory::allocation<uint8_t> workspace(workspace_size);
cutlass::Status status = conv2d_op.can_implement(arguments);
TVM_FFI_ICHECK(status == cutlass::Status::kSuccess);
${split_k_reset}
status = conv2d_op.initialize(arguments, workspace.get());
TVM_FFI_ICHECK(status == cutlass::Status::kSuccess);
${split_k_update}
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(kDLCUDA, ${data_arg}->device.device_id));
status = conv2d_op(stream);
TVM_FFI_ICHECK(status == cutlass::Status::kSuccess);
${split_k_reduction}
"""
split_k_reset = """
arguments.ref_D.reset(reinterpret_cast<ElementComputeEpilogue*>(workspace.get()), layout_D);
"""
split_k_update = """
arguments.output_op = {ElementComputeEpilogue(1), ElementComputeEpilogue(0)};
status = conv2d_op.update(arguments, workspace.get());
TVM_FFI_ICHECK(status == cutlass::Status::kSuccess);
"""
split_k_reduction = """
ReductionDevice reduction_op;
const static cutlass::conv::Operator kConvolutionalOperator = Conv2d::kConvolutionalOperator;
typename ReductionDevice::Arguments reduction_args(
cutlass::conv::implicit_gemm_problem_size(kConvolutionalOperator, problem_size).mn(),
problem_size.split_k_slices,
cutlass::conv::implicit_gemm_tensor_c_size(kConvolutionalOperator, problem_size),
{
reinterpret_cast<Conv2d::ElementAccumulator*> (workspace.get()),
ReductionStrideIndex(tensor_c.stride()[Conv2d::UnderlyingKernel::kTensorCStrideIdx])
},
{
tensor_d.data(),
ReductionStrideIndex(tensor_d.stride()[Conv2d::UnderlyingKernel::kTensorCStrideIdx])
},
{
tensor_c.data(),
ReductionStrideIndex(tensor_c.stride()[Conv2d::UnderlyingKernel::kTensorCStrideIdx])
},
{alpha, beta}
);
status = reduction_op.initialize(reduction_args, nullptr);
status = reduction_op();
"""
op_type = attrs["op_type"]
has_bias = "bias" in op_type
use_split_k = "splitk" in attrs["cutlass_op_name"]
is_wgrad = "backward_weight" in op_type
is_dgrad = "conv2d_transpose" in op_type
has_residual_block = "residual" in op_type
no_bias_scaling = op_type not in [
"cutlass.conv2d_bias_sigmoid",
"cutlass.conv2d_bias_silu",
"cutlass.conv2d_bias_hardswish",
]
aux_map = {}
if (not has_bias or no_bias_scaling) and not has_residual_block:
aux_map["beta"] = 0
else:
aux_map["beta"] = 1
if has_residual_block:
aux_map["bias_decl"] = "void* ptr_bias = (void*)(${bias_arg}->data);\n"
aux_map["residual_decl"] = "void* ptr_residual = (void*)(${residual_arg}->data);"
aux_map["tensor_c"] = "ptr_residual"
aux_map["tensor_c_layout"] = "layout_C"
elif has_bias:
aux_map["bias_decl"] = "void* ptr_c_bias = (void*)(${bias_arg}->data);\n"
aux_map["residual_decl"] = ""
aux_map["tensor_c"] = "ptr_c_bias"
aux_map["tensor_c_layout"] = "cutlass::layout::TensorNHWC::Stride(0)"
else:
aux_map["bias_decl"] = ""
aux_map["residual_decl"] = ""
aux_map["tensor_c"] = "ptr_out"
aux_map["tensor_c_layout"] = "layout_C"
if has_bias and no_bias_scaling and not has_residual_block:
aux_map["alpha_beta"] = "alpha"
else:
aux_map["alpha_beta"] = "alpha, beta"
if has_residual_block:
aux_map["additional_args"] = ", static_cast<ElementOutput*>(ptr_bias), nullptr, 0, K"
else:
aux_map["additional_args"] = ""
aux_map["residual_shape_decl"] = ""
if is_wgrad:
aux_map["A_shape"] = "output_shape"
aux_map["B_shape"] = "activation_shape"
aux_map["C_shape"] = "weight_shape"
aux_map["D_shape"] = "weight_shape"
elif is_dgrad:
aux_map["A_shape"] = "output_shape"
aux_map["B_shape"] = "weight_shape"
aux_map["C_shape"] = "activation_shape"
aux_map["D_shape"] = "activation_shape"
else:
aux_map["A_shape"] = "activation_shape"
aux_map["B_shape"] = "weight_shape"
aux_map["D_shape"] = "output_shape"
if has_residual_block:
res_shape = list(attrs.pop("residual_shape"))
shape_str = f"cutlass::make_Coord({res_shape[0]}, {res_shape[1]}, {res_shape[2]}, K)"
aux_map["residual_shape_decl"] = (
f"auto residual_shape = TensorNHWC::packed({shape_str});"
)
aux_map["C_shape"] = "residual_shape"
if res_shape == [int(attrs[c]) for c in ["N", "H", "W", "K"]]:
aux_map["tensor_c_layout"] = "layout_C"
else:
# bias-like residual input
aux_map["tensor_c_layout"] = "cutlass::layout::TensorNHWC::Stride(0)"
else:
aux_map["C_shape"] = "output_shape"
if use_split_k:
aux_map["ElementOutput"] = "EpilogueOutputOp::ElementOutput"
aux_map["tensor_c_arg"] = "{nullptr, TensorNHWC()}"
aux_map["tensor_d_arg"] = "{nullptr, TensorNHWC()}"
aux_map["split_k_reset"] = split_k_reset
aux_map["split_k_update"] = split_k_update
aux_map["split_k_reduction"] = split_k_reduction
else:
aux_map["ElementOutput"] = "Conv2d::ElementC"
aux_map["tensor_c_arg"] = "tensor_c"
aux_map["tensor_d_arg"] = "tensor_d"
aux_map["split_k_reset"] = aux_map["split_k_update"] = aux_map["split_k_reduction"] = ""
template = substitute_template(template, aux_map)
return substitute_template(template, attrs)
@@ -0,0 +1,216 @@
# 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=import-outside-toplevel, invalid-name
# ruff: noqa: E501
"""Instantiate a C++ source for profiling CUTLASS kernels."""
from .library import DataTypeTag
class Conv2dProfilerEmitter:
"""Emit a C++ source for profiling CUTLASS kernels."""
def __init__(self):
from jinja2 import Template
self.reduction = """
ReductionDevice reduction_op;
static cutlass::conv::Operator const kConvolutionalOperator = ImplicitGemm::kConvolutionalOperator;
typename ReductionDevice::Arguments reduction_args(
cutlass::conv::implicit_gemm_problem_size(kConvolutionalOperator, problem_size).mn(),
problem_size.split_k_slices,
cutlass::conv::implicit_gemm_tensor_c_size(kConvolutionalOperator, problem_size),
{
reinterpret_cast<ImplicitGemm::ElementC*> (workspace.get()),
ReductionStrideIndex(tensor_c.stride()[ImplicitGemm::UnderlyingKernel::kTensorCStrideIdx])
},
{
tensor_d.device_data(),
ReductionStrideIndex(tensor_d.stride()[ImplicitGemm::UnderlyingKernel::kTensorCStrideIdx])
},
{
tensor_c.device_data(),
ReductionStrideIndex(tensor_c.stride()[ImplicitGemm::UnderlyingKernel::kTensorCStrideIdx])
},
{ElementComputeEpilogue(1), ElementComputeEpilogue(0)}
);
reduction_op.initialize(reduction_args, nullptr);
reduction_op();
"""
self.template = Template(
"""
#include <iostream>
#include "cutlass/cutlass.h"
#include "cutlass/conv/kernel/default_conv2d_fprop.h"
#include "cutlass/conv/kernel/default_conv2d_wgrad.h"
#include "cutlass/conv/kernel/default_conv2d_dgrad.h"
#include "cutlass/conv/device/implicit_gemm_convolution.h"
#include "cutlass/util/command_line.h"
#include "cutlass/util/host_tensor.h"
#include "cutlass/util/reference/host/tensor_fill.h"
#include "cutlass/reduction/device/reduce_split_k.h"
#include "cutlass/reduction/thread/reduction_operators.h"
#define CUTLASS_CHECK(status) \
{ \
cutlass::Status error = status; \
if (error != cutlass::Status::kSuccess) { \
std::cerr << "Got cutlass error: " << cutlassGetStatusString(error) << " at: " << __LINE__ \
<< std::endl; \
exit(EXIT_FAILURE); \
} \
}
{{OperatorDef}}
using ImplicitGemm = cutlass::conv::device::ImplicitGemmConvolution<{{OperatorName}}>;
struct Options {
cutlass::Tensor4DCoord input_size;
cutlass::Tensor4DCoord filter_size;
cutlass::Tensor4DCoord padding;
cutlass::MatrixCoord conv_stride;
cutlass::MatrixCoord dilation;
void parse(int argc, char const **args) {
cutlass::CommandLine cmd(argc, args);
cmd.get_cmd_line_argument("n", input_size.n());
cmd.get_cmd_line_argument("h", input_size.h());
cmd.get_cmd_line_argument("w", input_size.w());
cmd.get_cmd_line_argument("c", input_size.c());
cmd.get_cmd_line_argument("k", filter_size.n());
cmd.get_cmd_line_argument("r", filter_size.h());
cmd.get_cmd_line_argument("s", filter_size.w());
int pad_h, pad_w, stride_h, stride_w, dilation_h, dilation_w;
cmd.get_cmd_line_argument("pad_h", pad_h);
cmd.get_cmd_line_argument("pad_w", pad_w);
cmd.get_cmd_line_argument("stride_h", stride_h);
cmd.get_cmd_line_argument("stride_w", stride_w);
cmd.get_cmd_line_argument("dilation_h", dilation_h);
cmd.get_cmd_line_argument("dilation_w", dilation_w);
filter_size.c() = input_size.c();
padding = {pad_h, pad_h, pad_w, pad_w};
conv_stride = {stride_h, stride_w};
dilation = {dilation_h, dilation_w};
}
cutlass::Tensor4DCoord output_size() const {
auto dilated_h = (filter_size.h() - 1) * dilation.row() + 1;
auto dilated_w = (filter_size.w() - 1) * dilation.column() + 1;
auto h = (input_size.h() + padding.n() + padding.h() - dilated_h) / conv_stride.row() + 1;
auto w = (input_size.w() + padding.w() + padding.c() - dilated_w) / conv_stride.column() + 1;
return cutlass::Tensor4DCoord(input_size.n(), h, w, filter_size.n());
}
};
double profile_convolution(Options const &options) {
using ElementOutput = {{ElementOutput}};
using ElementInputA = typename ImplicitGemm::ElementA;
using ElementInputB = typename ImplicitGemm::ElementB;
int split_k_slices = {{SplitK}};
cutlass::conv::Conv2dProblemSize problem_size(
options.input_size,
options.filter_size,
options.padding,
options.conv_stride,
options.dilation,
options.output_size(),
cutlass::conv::Mode::kCrossCorrelation,
split_k_slices
);
auto conv_kind = ImplicitGemm::kConvolutionalOperator;
auto a_extent = implicit_gemm_tensor_a_extent(conv_kind, problem_size);
auto b_extent = implicit_gemm_tensor_b_extent(conv_kind, problem_size);
auto c_extent = implicit_gemm_tensor_c_extent(conv_kind, problem_size);
using LayoutC = typename ImplicitGemm::LayoutC;
cutlass::HostTensor<ElementInputA, typename ImplicitGemm::LayoutA> tensor_a(a_extent);
cutlass::HostTensor<ElementInputB, typename ImplicitGemm::LayoutB> tensor_b(b_extent);
cutlass::HostTensor<ElementOutput, typename ImplicitGemm::LayoutC> tensor_c(c_extent);
cutlass::HostTensor<ElementOutput, LayoutC> tensor_d(c_extent);
cutlass::HostTensor<ImplicitGemm::ElementC, LayoutC> tensor_c_gemm(c_extent);
using ElementComputeEpilogue = typename ImplicitGemm::ElementCompute;
cutlass::conv::SplitKMode const split_k_mode = split_k_slices > 1 ?
cutlass::conv::SplitKMode::kParallel : cutlass::conv::SplitKMode::kSerial;
typename ImplicitGemm::Arguments arguments{
problem_size,
tensor_a.device_ref(),
tensor_b.device_ref(),
tensor_c_gemm.device_ref(),
tensor_c_gemm.device_ref(),
{ElementComputeEpilogue(1), ElementComputeEpilogue(0)},
split_k_mode,
};
ImplicitGemm implicit_gemm_op;
size_t workspace_size = implicit_gemm_op.get_workspace_size(arguments);
cutlass::device_memory::allocation<uint8_t> workspace(workspace_size);
auto status = implicit_gemm_op.can_implement(arguments);
CUTLASS_CHECK(status);
status = implicit_gemm_op.initialize(arguments, workspace.get());
CUTLASS_CHECK(status);
status = implicit_gemm_op();
CUTLASS_CHECK(status);
cudaEvent_t events[2];
for (auto & event : events) {
cudaEventCreate(&event);
}
cudaEventRecord(events[0]);
for (int iteration = 0; iteration < 100; ++iteration) {
auto status = implicit_gemm_op();
CUTLASS_CHECK(status);
{{Reduction}}
}
cudaEventRecord(events[1]);
cudaEventSynchronize(events[1]);
float runtime_ms = 0;
cudaEventElapsedTime(&runtime_ms, events[0], events[1]);
for (auto event : events) {
(void)cudaEventDestroy(event);
}
return double(runtime_ms) / 100.0;
}
int main(int argc, char const **args) {
Options options;
options.parse(argc, args);
std::cout << profile_convolution(options) << std::endl;
return 0;
}
"""
)
def emit(self, op_def, op_name, element_output, split_k_slices=1):
src = self.template.render(
OperatorDef=op_def,
OperatorName=op_name,
ElementOutput=DataTypeTag[element_output],
SplitK=split_k_slices,
Reduction=self.reduction if split_k_slices > 1 else "",
)
return src
@@ -0,0 +1,478 @@
# 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)
+196
View File
@@ -0,0 +1,196 @@
# 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=import-outside-toplevel, invalid-name
"""Instantiate a C++ source for profiling CUTLASS kernels."""
class GemmProfilerEmitter:
"""Emit a C++ source for profiling CUTLASS kernels."""
def __init__(self):
from jinja2 import Template
self.template = Template(
"""
#include <iostream>
#include <sstream>
#include <vector>
#include <chrono>
#include "cuda_runtime.h"
#include "cutlass/gemm/device/gemm.h"
#define CUTLASS_CHECK(status) \\
{ \\
cutlass::Status error = status; \\
if (error != cutlass::Status::kSuccess) { \\
std::cerr << "Got cutlass error: " << cutlassGetStatusString(error) << " at: " << __LINE__ \\
<< std::endl; \\
exit(EXIT_FAILURE); \\
} \\
}
#define CUDA_CHECK(status) \\
{ \\
cudaError_t error = status; \\
if (error != cudaSuccess) { \\
std::cerr << "Got bad CUDA status: " << cudaGetErrorString(error) \\
<< " at line: " << __LINE__ << std::endl; \\
exit(EXIT_FAILURE); \\
} \\
}
template<typename DTypeA, typename DTypeB, typename DTypeC>
cudaError_t CutlassGemm(
int M,
int N,
int K,
DTypeC alpha,
DTypeA const *A,
int lda,
DTypeB const *B,
int ldb,
DTypeC beta,
DTypeC *C,
int ldc) {
using namespace std::chrono;
{{OperatorDef}}
Operation_{{OperatorName}} gemm_operator;
Operation_{{OperatorName}}::Arguments args({M, N, K},
{A, lda},
{B, ldb},
{C, ldc},
{C, ldc},
{alpha, beta});
cutlass::Status status = gemm_operator(args);
CUTLASS_CHECK(status)
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for (int i = 0; i < 100; ++i) {
status = gemm_operator(args);
}
cudaDeviceSynchronize();
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << time_span.count() << std::endl;
return cudaSuccess;
}
template<typename DType>
cudaError_t AllocateMatrix(DType **matrix, int ldm, int rows, int columns, int seed = 0) {
cudaError_t result;
size_t sizeof_matrix = sizeof(DType) * rows * columns;
// Allocate device memory.
result = cudaMalloc(reinterpret_cast<void **>(matrix), sizeof_matrix);
if (result != cudaSuccess) {
std::cerr << "Failed to allocate matrix: "
<< cudaGetErrorString(result) << std::endl;
return result;
}
// Clear the allocation.
result = cudaMemset(*matrix, 0, sizeof_matrix);
if (result != cudaSuccess) {
std::cerr << "Failed to clear matrix device memory: "
<< cudaGetErrorString(result) << std::endl;
return result;
}
if (result != cudaSuccess) {
std::cerr << "Failed to initialize matrix: "
<< cudaGetErrorString(result) << std::endl;
return result;
}
return result;
}
template<typename DTypeA, typename DTypeB, typename DTypeC>
cudaError_t TestCutlassGemm(int M, int N, int K, DTypeC alpha, DTypeC beta) {
cudaError_t result;
{{LeadingDim}}
// size_t sizeof_C = sizeof(DTypeC) * ldc * N;
DTypeA *A;
DTypeB *B;
DTypeC *C_cutlass;
result = AllocateMatrix<DTypeA>(&A, lda, M, K, 0);
if (result != cudaSuccess) {
return result;
}
result = AllocateMatrix<DTypeB>(&B, ldb, K, N, 17);
if (result != cudaSuccess) {
cudaFree(A);
return result;
}
result = AllocateMatrix<DTypeC>(&C_cutlass, ldc, M, N, 101);
if (result != cudaSuccess) {
cudaFree(A);
cudaFree(B);
return result;
}
result = CutlassGemm<DTypeA, DTypeB, DTypeC>(M, N, K, alpha, A, lda, B, ldb,
beta, C_cutlass, ldc);
if (result != cudaSuccess) {
std::cerr << "CUTLASS GEMM kernel failed: "
<< cudaGetErrorString(result) << std::endl;
cudaFree(C_cutlass);
cudaFree(B);
cudaFree(A);
return result;
}
cudaFree(C_cutlass);
cudaFree(B);
cudaFree(A);
return cudaSuccess;
}
int main(int argc, const char *arg[]) {
int problem[3] = { 4096, 4096, 4096 };
for (int i = 1; i < argc && i < 4; ++i) {
std::stringstream ss(arg[i]);
ss >> problem[i - 1];
}
float scalars[2] = { 1, 0 };
cudaError_t result = TestCutlassGemm< {{DTypeA}}, {{DTypeB}}, {{DTypeC}}>(
problem[0], // GEMM M dimension
problem[1], // GEMM N dimension
problem[2], // GEMM K dimension
static_cast<{{DTypeC}}>(scalars[0]), // alpha
static_cast<{{DTypeC}}>(scalars[1]) // beta
);
return result == cudaSuccess ? 0 : -1;
}
"""
)
def emit(self, op_name, op_def, dtype_a, dtype_b, dtype_c, ld):
src = self.template.render(
OperatorName=op_name,
OperatorDef=op_def,
DTypeA=dtype_a,
DTypeB=dtype_b,
DTypeC=dtype_c,
LeadingDim=ld,
)
return src
+392
View File
@@ -0,0 +1,392 @@
# 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
# ruff: noqa: E501
"""Conv2d kernel generator and profiler for CUTLASS."""
import os
import pickle
from functools import partial
from .conv2d_operation import Conv2dOperation, EmitConv2dInstance
from .conv2d_profiler import Conv2dProfilerEmitter
from .gen_gemm import CutlassGemmProfiler
from .gen_tensor_op import EPILOGUE_MAP, GENERATOR_FUNC_TABLE, ProfilerEngine
from .library import (
ConvKind,
DataType,
EpilogueFunctor,
IteratorAlgorithm,
LayoutType,
StrideSupport,
SwizzlingFunctor,
TensorDescription,
)
def create_conv2d_operator_with_epilogue(
conv_kind,
stride_support,
op_type,
tile_description,
data_type,
alignment,
alignment_epilogue,
swizzling_functor,
split_k_slices,
):
"""
Instantiate a cutlass kernel from the given configuration,
along with the epilouge functor
"""
if "residual" in op_type:
activation_map = {
"cutlass.conv2d_bias_hardswish": "cutlass::epilogue::thread::HardSwish",
"cutlass.conv2d_bias_silu": "cutlass::epilogue::thread::SiLu",
"cutlass.conv2d_bias_sigmoid": "cutlass::epilogue::thread::Sigmoid",
"cutlass.conv2d_bias_relu": "cutlass::epilogue::thread::ReLu",
"cutlass.conv2d_bias": "cutlass::epilogue::thread::Identity",
}
prefix = op_type[: op_type.find("_residual")]
activation = activation_map[prefix]
binary_op = "cutlass::multiplies" if "residual_multiply" in op_type else "cutlass::plus"
unary_op = (
"cutlass::epilogue::thread::ReLu"
if op_type.endswith("relu")
else "cutlass::epilogue::thread::Identity"
)
residual_block_info = {
"activation": activation,
"binary_op": binary_op,
"unary_op": unary_op,
}
epilogue = EpilogueFunctor.LinearCombinationResidualBlock
no_beta_scaling = False
else:
residual_block_info = None
epilogue, no_beta_scaling = EPILOGUE_MAP[op_type]
element_a, element_b, element_c, element_epilogue = data_type
A = TensorDescription(element_a, LayoutType.TensorNHWC, alignment)
B = TensorDescription(element_b, LayoutType.TensorNHWC, alignment)
C = TensorDescription(element_c, LayoutType.TensorNHWC, alignment_epilogue)
op = Conv2dOperation(
conv_kind,
IteratorAlgorithm.Optimized,
tile_description.minimum_compute_capability,
tile_description,
A,
B,
C,
element_epilogue,
stride_support,
epilogue,
swizzling_functor,
split_k_slices,
)
name = op.procedural_name()
opdef = EmitConv2dInstance().emit(
op,
no_beta_scaling=no_beta_scaling,
residual_block_info=residual_block_info,
emit_reduction=split_k_slices > 1,
)
return name, opdef
def enumerate_conv2d_operators(
conv_kind,
stride_support,
split_k_slices,
alignment_c,
tile_descriptions,
data_type,
alignment_constraints,
swizzling_functor=SwizzlingFunctor.Identity4,
):
"""Exhaustively instantiate all kernels from a given configuration."""
ret = []
kernel_emitter = EmitConv2dInstance()
profiler_emitter = Conv2dProfilerEmitter()
element_a, element_b, element_c, element_epilogue = data_type
if conv_kind == ConvKind.Dgrad and stride_support == StrideSupport.Strided:
swizzling_functor = SwizzlingFunctor.StridedDgradIdentity1
for split_k_slice in split_k_slices:
for tile in tile_descriptions:
for alignmentAB in alignment_constraints:
for alignmentC in alignment_c:
A = TensorDescription(element_a, LayoutType.TensorNHWC, alignmentAB)
B = TensorDescription(element_b, LayoutType.TensorNHWC, alignmentAB)
C = TensorDescription(element_c, LayoutType.TensorNHWC, alignmentC)
if element_c == DataType.s32 and A.alignment == 1:
tile.threadblock_shape[0] = min(tile.threadblock_shape[0], 128)
tile.threadblock_shape[1] = min(tile.threadblock_shape[1], 128)
op = Conv2dOperation(
conv_kind,
IteratorAlgorithm.Optimized,
tile.minimum_compute_capability,
tile,
A,
B,
C,
element_epilogue,
stride_support,
EpilogueFunctor.LinearCombination,
swizzling_functor,
split_k_slice,
)
ret.append(
{
"src": profiler_emitter.emit(
kernel_emitter.emit(op, emit_reduction=split_k_slice > 1),
op.procedural_name(),
element_output=element_c,
split_k_slices=split_k_slice,
),
"name": op.procedural_name(),
"tile_description": tile,
"alignment": alignmentAB,
"alignment_epilogue": alignmentC,
"data_type": data_type,
"swizzle_functor": swizzling_functor,
"split_k_slices": split_k_slice,
}
)
return ret
class CutlassConv2DProfiler:
"""Profile all candidate kernels and select the best one."""
def __init__(self, sm, cutlass_path, binary_path):
self.gemm_profiler = CutlassGemmProfiler(sm, cutlass_path, binary_path)
self.sm = sm
assert sm in GENERATOR_FUNC_TABLE, f"sm{sm} not supported yet."
self.engine = ProfilerEngine(sm, cutlass_path, binary_path)
self.cache_path = os.path.join(binary_path, "cutlass_conv2d_cache.pickle")
if os.path.exists(self.cache_path):
self.cache = pickle.load(open(self.cache_path, "rb"))
else:
self.cache = {}
def get_default(
self,
op_type,
out_dtype,
arg0_dtype,
arg1_dtype,
use_3xtf32,
conv_kind=ConvKind.Fprop,
stride=(1, 1),
):
"""Return the default kernel for the requested architecture.
For now, the default kernel was picked arbitrary.
"""
gemm_profile_result = self.gemm_profiler.get_default(
op_type, out_dtype, arg0_dtype, arg1_dtype, use_3xtf32
)
tile_description = gemm_profile_result["tile_description"]
alignment = gemm_profile_result["alignment"]
data_type = gemm_profile_result["data_type"]
stride_support = StrideSupport.Strided if stride[0] > 1 else StrideSupport.Unity
if conv_kind == ConvKind.Dgrad and stride_support == StrideSupport.Strided:
swizzling_functor = SwizzlingFunctor.StridedDgradIdentity1
else:
swizzling_functor = SwizzlingFunctor.Identity4
name, opdef = create_conv2d_operator_with_epilogue(
conv_kind,
stride_support,
op_type,
tile_description,
data_type,
alignment,
alignment,
swizzling_functor,
split_k_slices=1,
)
return {"name": name, "opdef": opdef}
def select_op(
self,
d_shape,
w_shape,
padding,
stride,
dilation,
out_dtype,
data_dtype,
weight_dtype,
use_3xtf32,
conv_kind,
stride_support,
split_k_slices,
profile_all_alignments=False,
find_first_valid=False,
use_multiprocessing=False,
):
"""
Profile and select the best kernel from candidate kernels.
See the documentation for the profile method below.
"""
N, H, W, IC = d_shape
OC, R, S, _ = w_shape
workload = (
N,
H,
W,
IC,
OC,
R,
S,
padding[0],
padding[1],
stride[0],
stride[1],
dilation[0],
dilation[1],
)
if workload in self.cache:
return self.cache[workload]
def alignments(dtype):
if dtype in ["float16"]:
alignments = [8, 4, 2, 1]
elif dtype in ["float", "float32"]:
alignments = [4, 2, 1]
else:
raise ValueError(f"Unsupported data type: {dtype}")
return alignments
alignments_c = [align for align in alignments(out_dtype) if OC % align == 0]
if not profile_all_alignments:
alignments_c = [alignments_c[0]]
ops = GENERATOR_FUNC_TABLE[self.sm](
out_dtype,
data_dtype,
weight_dtype,
partial(
enumerate_conv2d_operators,
conv_kind,
stride_support,
split_k_slices,
alignments_c,
),
lambda align: all([dim % align == 0 for dim in [IC]]),
use_3xtf32,
profile_all_alignments,
# Use fp32 accumulation for wgrad to align with cuDNN
accumlator_dtype="float32" if conv_kind == ConvKind.Wgrad else out_dtype,
)
if not find_first_valid:
self.engine.compile_all(ops, use_multiprocessing)
args = "--n={} --h={} --w={} --c={} --k={} --r={} --s={} --pad_h={} --pad_w={} --stride_h={} --stride_w={} --dilation_h={} --dilation_w={}".format(
*workload
)
for op in ops:
out = self.engine.evaluate(op, args.split(" "))
op["runtime"] = out
if out < float("inf") and find_first_valid:
self.cache[workload] = op
return op
op = min(ops, key=lambda i: i["runtime"])
self.cache[workload] = op
with open(self.cache_path, "wb") as f:
pickle.dump(self.cache, f)
return op
def profile(
self,
op_type,
d_shape,
w_shape,
padding,
stride,
dilation,
out_dtype,
data_dtype,
weight_dtype,
use_3xtf32=True,
conv_kind=ConvKind.Fprop,
split_k_slices=[1],
profile_all_alignments=False,
find_first_valid=False,
use_multiprocessing=False,
):
"""Profile and select the best kernel from candidate kernels.
If find_first_valid is True, return immediately after the first applicable kernel is found.
If use_multiprocessing is True, compile all profiler executables in parallel.
"""
# Dgrad requires Unity stride when stride == (1, 1)
stride_support = (
StrideSupport.Unity
if stride[0] == 1 and stride[1] == 1 and conv_kind == ConvKind.Dgrad
else StrideSupport.Strided
)
op = self.select_op(
d_shape,
w_shape,
padding,
stride,
dilation,
out_dtype,
data_dtype,
weight_dtype,
use_3xtf32,
conv_kind,
stride_support,
split_k_slices,
profile_all_alignments,
find_first_valid,
use_multiprocessing,
)
name, opdef = create_conv2d_operator_with_epilogue(
conv_kind,
stride_support,
op_type,
op["tile_description"],
op["data_type"],
op["alignment"],
op["alignment_epilogue"],
op["swizzle_functor"],
op["split_k_slices"],
)
return name, opdef, op["runtime"]
+352
View File
@@ -0,0 +1,352 @@
# 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
"""GEMM kernel generator and profiler for CUTLASS."""
import os
import pickle
from functools import partial
from .gemm_operation import EmitGemmInstance, GemmOperation
from .gemm_profiler import GemmProfilerEmitter
from .gen_tensor_op import EPILOGUE_MAP, GENERATOR_FUNC_TABLE, ProfilerEngine
from .library import (
DataType,
DataTypeTag,
EpilogueFunctor,
LayoutType,
SwizzlingFunctor,
TensorDescription,
)
def create_gemm_operator_with_epilogue(
op_type,
tile_description,
data_type,
alignment,
swizzling_functor,
batched=False,
layout_b=LayoutType.ColumnMajor,
):
"""
Instantiate a cutlass kernel from the given configuration,
along with the epilouge functor
"""
element_a, element_b, element_c, element_epilogue = data_type
A = TensorDescription(element_a, LayoutType.RowMajor, alignment)
B = TensorDescription(element_b, layout_b, alignment)
C = TensorDescription(element_c, LayoutType.RowMajor, alignment)
if batched:
swizzling_functor = SwizzlingFunctor.Batched
if "residual" in op_type:
if "hardswish" in op_type:
activation = "cutlass::epilogue::thread::HardSwish"
elif "silu" in op_type:
activation = "cutlass::epilogue::thread::SiLu"
elif "sigmoid" in op_type:
activation = "cutlass::epilogue::thread::Sigmoid"
elif "gelu" in op_type:
activation = "cutlass::epilogue::thread::GELU"
elif "relu" in op_type:
activation = "cutlass::epilogue::thread::ReLu"
else:
activation = "cutlass::epilogue::thread::Identity"
binary_op = "cutlass::multiplies" if "residual_multiply" in op_type else "cutlass::plus"
unary_op = (
"cutlass::epilogue::thread::ReLu"
if op_type.endswith("relu")
else "cutlass::epilogue::thread::Identity"
)
residual_block_info = {
"activation": activation,
"binary_op": binary_op,
"unary_op": unary_op,
}
epilogue = EpilogueFunctor.LinearCombinationResidualBlock
no_beta_scaling = False
else:
residual_block_info = None
epilogue, no_beta_scaling = EPILOGUE_MAP[op_type]
op = GemmOperation(
tile_description.minimum_compute_capability,
tile_description,
A,
B,
C,
element_epilogue,
epilogue,
swizzling_functor,
)
return (
op.procedural_name(),
EmitGemmInstance().emit(
op,
no_beta_scaling=no_beta_scaling,
batched=batched,
residual_block_info=residual_block_info,
),
)
def enumerate_gemm_operators(
tile_descriptions,
data_type,
alignment_constraints,
swizzling_functor=SwizzlingFunctor.Identity8,
layout_b=LayoutType.ColumnMajor,
):
"""Exhaustively instantiate all kernels from a given configuration."""
ret = []
kernel_emitter = EmitGemmInstance()
profiler_emitter = GemmProfilerEmitter()
element_a, element_b, element_c, element_epilogue = data_type
for tile_description in tile_descriptions:
for alignment in alignment_constraints:
A = TensorDescription(element_a, LayoutType.RowMajor, alignment)
B = TensorDescription(element_b, layout_b, alignment)
C = TensorDescription(element_c, LayoutType.RowMajor, alignment)
if element_c == DataType.s32 and A.alignment == 1:
tile_description.threadblock_shape[0] = min(
tile_description.threadblock_shape[0], 128
)
tile_description.threadblock_shape[1] = min(
tile_description.threadblock_shape[1], 128
)
op = GemmOperation(
tile_description.minimum_compute_capability,
tile_description,
A,
B,
C,
element_epilogue,
EpilogueFunctor.LinearCombination,
swizzling_functor,
)
src = profiler_emitter.emit(
op.procedural_name(),
kernel_emitter.emit(op, batched=False),
DataTypeTag[element_a],
DataTypeTag[element_b],
DataTypeTag[element_c],
op.leading_dim(),
)
ret.append(
{
"src": src,
"op": op,
"name": op.procedural_name(),
"tile_description": tile_description,
"alignment": alignment,
"data_type": data_type,
"swizzle_functor": swizzling_functor,
}
)
return ret
# TODO(masahi): A sensible way to pick reasonable default kernels
DEFAULT_KERNELS = {
75: {
("float16", "float16"): "cutlass_tensorop_h1688gemm_128x64_32x2_tn_align1",
("float16", "float32"): "cutlass_tensorop_s1688gemm_f16_64x64_32x2_tn_align1",
},
# align1 variants do not seem to be available for sm80
80: {
("float16", "float16"): "cutlass_tensorop_h1688gemm_128x64_32x2_tn_align1",
("float16", "float32"): "cutlass_tensorop_s1688gemm_f16_64x64_32x2_tn_align1",
# two kernels for tf32 and 3xtf32
("float32", "float32"): (
"cutlass_tensorop_s1688gemm_128x64_32x3_tn_align1",
"cutlass_tensorop_s1688gemm_64x64_16x3_tn_align1",
),
},
}
class CutlassGemmProfiler:
"""Profile all candidate kernels and select the best one."""
def __init__(self, sm, cutlass_path, binary_path):
assert sm in GENERATOR_FUNC_TABLE and sm in DEFAULT_KERNELS, f"sm{sm} not supported yet."
self.engine = ProfilerEngine(sm, cutlass_path, binary_path)
self.sm = sm
self.cache_path = os.path.join(binary_path, "cutlass_gemm_cache.pickle")
if os.path.exists(self.cache_path):
self.cache = pickle.load(open(self.cache_path, "rb"))
else:
self.cache = {}
def get_default(
self,
op_type,
out_dtype,
arg0_dtype,
arg1_dtype,
use_3xtf32=True,
batched=False,
layout_b=LayoutType.ColumnMajor,
):
"""Return the default kernel for the requested architecture.
For now, the default kernel was picked arbitrary.
"""
ops = GENERATOR_FUNC_TABLE[self.sm](
out_dtype,
arg0_dtype,
arg1_dtype,
partial(enumerate_gemm_operators, layout_b=layout_b),
lambda align: align == 1, # Only request align1 kernels
use_3xtf32,
profile_all_alignments=True, # To include all align1 kernels
# TODO(masahi): Invesitigate when fp32 accumulation is needed for gemm
accumlator_dtype=out_dtype,
)
default_kernel_name = DEFAULT_KERNELS[self.sm][(arg0_dtype, out_dtype)]
if arg0_dtype == "float32":
default_kernel_name = (
default_kernel_name[0] if not use_3xtf32 else default_kernel_name[1]
)
filtered = list(filter(lambda op: op["name"] == default_kernel_name, ops))
assert len(filtered) == 1
op = filtered[0]
name, opdef = create_gemm_operator_with_epilogue(
op_type,
op["tile_description"],
op["data_type"],
op["alignment"],
op["swizzle_functor"],
batched=batched,
layout_b=layout_b,
)
op.update({"name": name, "opdef": opdef})
return op
def select_op(
self,
M,
N,
K,
out_dtype,
arg0_dtype,
arg1_dtype,
use_3xtf32,
profile_all_alignments=False,
find_first_valid=False,
use_multiprocessing=False,
layout_b=LayoutType.ColumnMajor,
):
"""
Profile and select the best kernel from candidate kernels.
See the documentation for the profile method below.
"""
if (M, N, K) in self.cache:
op = self.cache[(M, N, K)]
return op
# TODO(masahi): CUTLASS alignment check on gemm kernels is too restrictive.
# See https://github.com/NVIDIA/cutlass/issues/362.
# When the above issue is resolved, we can remove the alignment check on M below.
ops = GENERATOR_FUNC_TABLE[self.sm](
out_dtype,
arg0_dtype,
arg1_dtype,
partial(enumerate_gemm_operators, layout_b=layout_b),
lambda align: all([dim % align == 0 for dim in [M, N, K]]),
use_3xtf32,
profile_all_alignments=profile_all_alignments,
# TODO(masahi): Invesitigate when fp32 accumulation is needed for gemm
accumlator_dtype=out_dtype,
)
if not find_first_valid:
self.engine.compile_all(ops, use_multiprocessing)
for op in ops:
out = self.engine.evaluate(op, [M, N, K])
op["runtime"] = out
if out < float("inf") and find_first_valid:
self.cache[(M, N, K)] = op
return op
op = min(ops, key=lambda i: i["runtime"])
self.cache[(M, N, K)] = op
with open(self.cache_path, "wb") as f:
pickle.dump(self.cache, f)
return op
def profile(
self,
op_type,
M,
N,
K,
out_dtype,
arg0_dtype,
arg1_dtype,
use_3xtf32=True,
profile_all_alignments=False,
find_first_valid=False,
use_multiprocessing=False,
batched=False,
layout_b=LayoutType.ColumnMajor,
):
"""Profile and select the best kernel from candidate kernels.
If find_first_valid is True, return immediately after the first applicable kernel is found.
If use_multiprocessing is True, compile all profiler executables in parallel.
"""
op = self.select_op(
M,
N,
K,
out_dtype,
arg0_dtype,
arg1_dtype,
use_3xtf32,
profile_all_alignments=profile_all_alignments,
find_first_valid=find_first_valid,
use_multiprocessing=use_multiprocessing,
layout_b=layout_b,
)
name, opdef = create_gemm_operator_with_epilogue(
op_type,
op["tile_description"],
op["data_type"],
op["alignment"],
op["swizzle_functor"],
batched=batched,
layout_b=layout_b,
)
return name, opdef, op["runtime"]
+908
View File
@@ -0,0 +1,908 @@
# 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
# ruff: noqa: F821
"""Common functions and classes for CUTLASS GEMM and Conv2d geneator."""
import logging
import math
import multiprocessing
import os
import re
import subprocess
import tempfile
import tvm_ffi
from tvm.runtime import Object
from tvm.tirx import IntImm
from . import _ffi_api as ffi
from .attention_operation import (
instantiate_attention_template,
instantiate_flash_attention_template,
instantiate_flash_attention_var_len_template,
)
from .conv2d_operation import instantiate_conv2d_template
from .gemm_operation import emit_fp16A_intB_matmul, instantiate_gemm_template
from .layer_norm_operation import instantiate_layer_norm_template
from .library import (
DataType,
DataTypeSize,
DataTypeTag,
EpilogueFunctor,
MathInstruction,
MathOperation,
OpcodeClass,
TileDescription,
)
from .rms_norm_operation import instantiate_rms_norm_template
logger = logging.getLogger("cutlass")
dtype_map = {
"int8": DataType.s8,
"uint8": DataType.u8,
"int32": DataType.s32,
"float32": DataType.f32,
"float16": DataType.f16,
}
def generate_tensor_op_common(
math_instructions, alignment_constraints, get_tile_descriptions, op_creator
):
"""Common kernel generator to be used by archtecture specific generators."""
ops = []
for math_inst in math_instructions:
tile_descriptions = get_tile_descriptions(math_inst)
data_type = [
math_inst.element_a,
math_inst.element_b,
math_inst.element_c,
math_inst.element_accumulator,
]
out = op_creator(tile_descriptions, data_type, alignment_constraints)
ops.extend(out)
return ops
def generate_sm50_simt(out_dtype, arg0_dtype, arg1_dtype, op_creator, accumulator_dtype="float32"):
"""Gemerate GEMM or Conv2D SIMT kernels"""
# pylint: disable=unused-argument
min_cc = 50
max_cc = 1024
if arg0_dtype == "float32" and arg1_dtype == "float32":
assert out_dtype == "float32" and accumulator_dtype == "float32"
math_instructions = [
MathInstruction(
[1, 1, 1],
DataType.f32,
DataType.f32,
DataType.f32,
DataType.f32,
OpcodeClass.Simt,
MathOperation.multiply_add,
)
]
alignment_constraints = [1]
tile_descriptions = [
([128, 128, 8], 2, [4, 2, 1], min_cc, max_cc),
([128, 64, 8], 2, [2, 2, 1], min_cc, max_cc),
([64, 128, 8], 2, [2, 2, 1], min_cc, max_cc),
([64, 64, 8], 2, [2, 1, 1], min_cc, max_cc),
([128, 32, 8], 2, [2, 1, 1], min_cc, max_cc),
([32, 128, 8], 2, [1, 2, 1], min_cc, max_cc),
]
def get_tile_descriptions(math_inst):
return [
TileDescription(threadblock_shape, stages, warp_count, math_inst, min_cc, max_cc)
for threadblock_shape, stages, warp_count, min_cc, max_cc in tile_descriptions
]
return generate_tensor_op_common(
math_instructions, alignment_constraints, get_tile_descriptions, op_creator
)
else:
raise NotImplementedError()
def generate_sm75_tensor_op_1688(
out_dtype,
arg0_dtype,
arg1_dtype,
op_creator,
check_align,
_,
profile_all_alignments=False,
accumlator_dtype="float32",
):
"""Generate GEMM or Conv2D kernels for Turing."""
assert out_dtype in ["float32", "float16", "int32"]
min_cc = 75
max_cc = 1024
if arg0_dtype == "float16" and arg1_dtype == "float16":
math_instructions = [
MathInstruction(
[16, 8, 8],
DataType.f16,
DataType.f16,
dtype_map[out_dtype],
dtype_map[accumlator_dtype],
OpcodeClass.TensorOp,
MathOperation.multiply_add,
)
]
alignment_constraints = [8, 4, 2, 1]
tile_descriptions = [
([256, 128, 32], 2, [4, 2, 1], min_cc, max_cc),
([128, 256, 32], 2, [2, 4, 1], min_cc, max_cc),
([128, 128, 32], 2, [2, 2, 1], min_cc, max_cc),
([64, 128, 32], 2, [2, 2, 1], min_cc, max_cc),
([128, 64, 32], 2, [2, 2, 1], min_cc, max_cc),
([64, 64, 32], 2, [2, 2, 1], min_cc, max_cc),
([64, 128, 64], 2, [1, 2, 2], min_cc, max_cc),
]
elif "int8" in arg0_dtype and "int8" in arg1_dtype:
assert out_dtype == "int32"
math_instructions = [
MathInstruction(
[8, 8, 16],
dtype_map[arg0_dtype],
dtype_map[arg1_dtype],
DataType.s32,
DataType.s32,
OpcodeClass.TensorOp,
MathOperation.multiply_add_saturate,
)
]
alignment_constraints = [16, 8, 4, 2, 1]
tile_descriptions = [
([256, 128, 64], 2, [4, 2, 1], min_cc, max_cc),
([128, 256, 64], 2, [2, 4, 1], min_cc, max_cc),
([128, 128, 64], 2, [2, 2, 1], min_cc, max_cc),
([64, 256, 64], 2, [1, 4, 1], min_cc, max_cc),
([256, 64, 64], 2, [4, 1, 1], min_cc, max_cc),
([64, 128, 64], 2, [2, 2, 1], min_cc, max_cc),
([128, 64, 64], 2, [2, 2, 1], min_cc, max_cc),
([64, 64, 64], 2, [2, 2, 1], min_cc, max_cc),
]
elif arg0_dtype == "float32" and arg1_dtype == "float32" and out_dtype == "float32":
return generate_sm50_simt(out_dtype, arg0_dtype, arg1_dtype, op_creator, accumlator_dtype)
else:
raise NotImplementedError()
alignment_constraints = [align for align in alignment_constraints if check_align(align)]
assert len(alignment_constraints) > 0
if not profile_all_alignments:
alignment_constraints = [alignment_constraints[0]]
def get_tile_descriptions(math_inst):
return [
TileDescription(threadblock_shape, stages, warp_count, math_inst, min_cc, max_cc)
for threadblock_shape, stages, warp_count, min_cc, max_cc in tile_descriptions
]
return generate_tensor_op_common(
math_instructions, alignment_constraints, get_tile_descriptions, op_creator
)
def generate_sm80_tensor_op_16816(
out_dtype,
arg0_dtype,
arg1_dtype,
op_creator,
check_align,
use_3xtf32=True,
profile_all_alignments=False,
accumlator_dtype="float32",
):
"""Generate GEMM or Conv2D kernels for Ampere."""
min_cc = 80
max_cc = 1024
max_cc_smem_limited = 80
def get_default_tile_descriptions(block_k_factor):
return [
([128, 256, int(32 * block_k_factor)], 3, [2, 4, 1], min_cc, max_cc),
([256, 128, int(32 * block_k_factor)], 3, [4, 2, 1], min_cc, max_cc),
([256, 64, int(32 * block_k_factor)], 3, [4, 1, 1], min_cc, max_cc),
([256, 64, int(32 * block_k_factor)], 4, [4, 1, 1], min_cc, max_cc),
([64, 256, int(32 * block_k_factor)], 4, [1, 4, 1], min_cc, max_cc),
([128, 128, int(32 * block_k_factor)], 3, [2, 2, 1], min_cc, max_cc),
([128, 128, int(32 * block_k_factor)], 4, [2, 2, 1], min_cc, max_cc),
([128, 128, int(32 * block_k_factor)], 5, [2, 2, 1], min_cc, max_cc),
([128, 64, int(32 * block_k_factor)], 6, [2, 2, 1], min_cc, max_cc),
([64, 128, int(32 * block_k_factor)], 6, [2, 2, 1], min_cc, max_cc),
([64, 64, int(32 * block_k_factor)], 10, [2, 2, 1], min_cc, max_cc),
([256, 128, int(64 * block_k_factor)], 3, [4, 2, 1], min_cc, max_cc_smem_limited),
([128, 256, int(64 * block_k_factor)], 3, [2, 4, 1], min_cc, max_cc_smem_limited),
([256, 64, int(64 * block_k_factor)], 4, [4, 1, 1], min_cc, max_cc_smem_limited),
([64, 256, int(64 * block_k_factor)], 4, [1, 4, 1], min_cc, max_cc_smem_limited),
([128, 128, int(64 * block_k_factor)], 4, [2, 2, 1], min_cc, max_cc),
([256, 64, int(64 * block_k_factor)], 3, [4, 1, 1], min_cc, max_cc),
([64, 256, int(64 * block_k_factor)], 3, [1, 4, 1], min_cc, max_cc),
([128, 128, int(64 * block_k_factor)], 3, [2, 2, 1], min_cc, max_cc),
([128, 64, int(64 * block_k_factor)], 3, [2, 2, 1], min_cc, max_cc),
([64, 128, int(64 * block_k_factor)], 3, [2, 2, 1], min_cc, max_cc),
([64, 64, int(64 * block_k_factor)], 5, [2, 2, 1], min_cc, max_cc),
]
if arg0_dtype == "float16" and arg1_dtype == "float16":
math_instructions = [
MathInstruction(
[16, 8, 16],
DataType.f16,
DataType.f16,
dtype_map[out_dtype],
dtype_map[accumlator_dtype],
OpcodeClass.TensorOp,
MathOperation.multiply_add,
)
]
alignment_constraints = [8, 4, 2]
tile_descriptions = get_default_tile_descriptions(1)
elif arg0_dtype == "float32" and arg1_dtype == "float32":
math_instructions = [
MathInstruction(
[16, 8, 8],
DataType.f32,
DataType.f32,
DataType.f32,
DataType.f32,
OpcodeClass.TensorOp,
MathOperation.multiply_add_fast_f32 if use_3xtf32 else MathOperation.multiply_add,
)
]
alignment_constraints = [4, 2, 1]
if use_3xtf32:
# tf32
tile_descriptions = [
([128, 128, 16], 4, [4, 2, 1], min_cc, max_cc),
([128, 128, 16], 3, [4, 2, 1], min_cc, max_cc),
([256, 64, 16], 3, [4, 2, 1], min_cc, max_cc),
([64, 256, 16], 3, [2, 4, 1], min_cc, max_cc),
([128, 64, 16], 4, [2, 2, 1], min_cc, max_cc),
([64, 128, 16], 4, [2, 2, 1], min_cc, max_cc),
([64, 64, 16], 3, [2, 2, 1], min_cc, max_cc),
([128, 128, 32], 3, [4, 2, 1], min_cc, max_cc),
([256, 64, 32], 3, [4, 2, 1], min_cc, max_cc_smem_limited),
([64, 256, 32], 3, [2, 4, 1], min_cc, max_cc_smem_limited),
([128, 64, 32], 3, [2, 2, 1], min_cc, max_cc),
([64, 128, 32], 3, [2, 2, 1], min_cc, max_cc),
([64, 64, 32], 3, [2, 2, 1], min_cc, max_cc),
]
else:
tile_descriptions = get_default_tile_descriptions(0.5)
else:
assert out_dtype == "int32"
math_instructions = [
MathInstruction(
[16, 8, 32],
dtype_map[arg0_dtype],
dtype_map[arg1_dtype],
DataType.s32,
DataType.s32,
OpcodeClass.TensorOp,
MathOperation.multiply_add_saturate,
)
]
alignment_constraints = [16, 8, 4]
tile_descriptions = get_default_tile_descriptions(2)
def get_tile_descriptions(math_inst):
return [
TileDescription(threadblock_shape, stages, warp_count, math_inst, min_cc, max_cc)
for threadblock_shape, stages, warp_count, min_cc, max_cc in tile_descriptions
]
alignment_constraints = [align for align in alignment_constraints if check_align(align)]
if len(alignment_constraints) > 0 and not profile_all_alignments:
alignment_constraints = [alignment_constraints[0]]
if arg0_dtype != "float32" and arg1_dtype != "float32":
sm75_kernels = generate_sm75_tensor_op_1688(
out_dtype,
arg0_dtype,
arg1_dtype,
op_creator,
check_align,
False,
profile_all_alignments,
accumlator_dtype=accumlator_dtype,
)
else:
# TF32 (float32 + float32 case) is only supported on sm80
sm75_kernels = []
if len(alignment_constraints) > 0:
sm80_kernels = generate_tensor_op_common(
math_instructions, alignment_constraints, get_tile_descriptions, op_creator
)
else:
sm80_kernels = []
# TODO(masahi): For int8 kernels, The CUTLASS generator modifies the output tensor alignment
# after ops are created. Revisit how important this modification is.
# for op in operations:
# if op.tile_description.threadblock_shape[1] >= 128:
# op.C.alignment = 16
# else:
# op.C.alignment = 8
return sm75_kernels + sm80_kernels
GENERATOR_FUNC_TABLE = {75: generate_sm75_tensor_op_1688, 80: generate_sm80_tensor_op_16816}
# (Epilogue functor name, no_beta_scaling)
EPILOGUE_MAP = {
"cutlass.dense": (EpilogueFunctor.LinearCombination, False),
"cutlass.dense_bias": (EpilogueFunctor.LinearCombinationBias, True),
"cutlass.dense_bias_relu": (EpilogueFunctor.LinearCombinationRelu, True),
"cutlass.dense_bias_gelu_fp16": (EpilogueFunctor.LinearCombinationGelu, False),
"cutlass.dense_bias_gelu_fp32": (EpilogueFunctor.LinearCombinationGelu, False),
"cutlass.matmul": (EpilogueFunctor.LinearCombination, False),
"cutlass.matmul_bias": (EpilogueFunctor.LinearCombinationBias, True),
"cutlass.matmul_bias_relu": (EpilogueFunctor.LinearCombinationRelu, True),
"cutlass.matmul_bias_gelu": (EpilogueFunctor.LinearCombinationGelu, False),
"cutlass.matmul_transposed": (EpilogueFunctor.LinearCombination, False),
"cutlass.matmul_transposed_bias": (EpilogueFunctor.LinearCombinationBias, True),
"cutlass.matmul_transposed_bias_relu": (EpilogueFunctor.LinearCombinationRelu, True),
"cutlass.matmul_transposed_bias_gelu": (EpilogueFunctor.LinearCombinationGelu, False),
"cutlass.batch_matmul": (EpilogueFunctor.LinearCombination, False),
"cutlass.conv2d_bias_hardswish": (EpilogueFunctor.LinearCombinationHardSwish, False),
"cutlass.conv2d_bias_silu": (EpilogueFunctor.LinearCombinationSilu, False),
"cutlass.conv2d_bias_sigmoid": (EpilogueFunctor.LinearCombinationSigmoid, False),
"cutlass.conv2d_bias_relu": (EpilogueFunctor.LinearCombinationRelu, True),
"cutlass.conv2d_bias": (EpilogueFunctor.LinearCombinationBias, True),
"cutlass.conv2d": (EpilogueFunctor.LinearCombination, False),
"cutlass.conv2d_transpose": (EpilogueFunctor.LinearCombination, False),
"cutlass.conv2d_backward_weight": (EpilogueFunctor.LinearCombination, False),
}
class ProfilerEngine:
"""Compile and run a given profiler executable."""
def __init__(self, cuda_arch, cutlass_path, binary_prefix):
self.cuda_arch = cuda_arch
self.binary_prefix = binary_prefix
self.cutlass = cutlass_path
self.cflags = f"-I{cutlass_path}/include -I{cutlass_path}/tools/util/include -O3 -std=c++17"
self.cflags += " -DCUTLASS_ENABLE_TENSOR_CORE_MMA=1"
self.cflags += (
f" -gencode=arch=compute_{cuda_arch},code=[sm_{cuda_arch},compute_{cuda_arch}]"
)
self.cflags += " -Xcompiler=-Wconversion -Xcompiler=-fno-strict-aliasing"
self.cmd = "nvcc {cflags} {src} -o {output}"
def _compile(self, op):
os.makedirs(self.binary_prefix, exist_ok=True)
opath = os.path.join(self.binary_prefix, op["name"])
if os.path.exists(opath):
return
fi = tempfile.NamedTemporaryFile("w", delete=False, prefix=self.binary_prefix, suffix=".cu")
fi.write(op["src"])
fi.close()
cmd = self.cmd.format(cflags=self.cflags, src=fi.name, output=opath)
logger.info("invoking compilation %s", cmd)
os.system(cmd)
os.unlink(fi.name)
def compile_all(self, ops, use_multiprocessing=False):
"""Compile all profiler executables."""
if use_multiprocessing:
pool = multiprocessing.Pool(multiprocessing.cpu_count())
pool.map(self._compile, ops)
else:
for op in ops:
self._compile(op)
def evaluate(self, op, args):
"""Run the profiler executable corresponding to op_name with args."""
op_name = op["name"]
opath = os.path.join(self.binary_prefix, op_name)
if not os.path.exists(opath):
self._compile(op)
if not os.path.exists(opath):
# Bail out if compilation fails for a whatever reason (e.g. static assert failure)
return float("inf")
cmd = [opath]
for arg in args:
cmd.append(str(arg))
try:
logger.info("invoking evaluation %s", cmd)
sp = subprocess.run(cmd, capture_output=True, check=True)
rt = float(sp.stdout)
if rt == 0.0:
# This seems to happen with split-k using invalid split-k-slices
rt = float("inf")
logger.info("%s, %f", op_name, rt)
except subprocess.CalledProcessError:
rt = float("inf")
return rt
class CodegenResult(Object):
"""The holder for the generated code and required headers."""
def __init__(self, code, headers):
self.__init_handle_by_constructor__(ffi.CodegenResult, code, headers)
def _get_optional_int_annotation(annotations, key, default=None):
value = annotations.get(key, default)
if value is None:
return default
return int(value)
@tvm_ffi.register_global_func("contrib.cutlass.instantiate_template")
def instantiate_template(func_name, annotations, func_args):
"""Return CUTLASS host code based on a template and the provided annotations.
Parameters
----------
func_name: str
A string to identify the type of the kernel (dense/matmul, batched_matmul, or conv2d).
annotations: tvm_ffi.Map
Key and value pairs annotated during kernel selection.
func_args: list
Names of the function arguments.
Returns
-------
codegen_result : CodegenResult
Generated CUTLASS host code and required header-file names.
"""
attrs = {}
for k in ["lda", "ldb", "ldc", "cutlass_op_def", "cutlass_op_name", "op_type"]:
if k in annotations:
attrs[k] = annotations[k]
headers = ["tvm/ffi/function.h", "tvm/ffi/extra/c_env_api.h"]
if "relu" in func_name:
headers.append("cutlass/epilogue/thread/linear_combination_bias_relu.h")
elif "gelu" in func_name:
headers.append("cutlass/epilogue/thread/linear_combination_gelu.h")
elif "sigmoid" in func_name:
headers.append("cutlass/epilogue/thread/linear_combination_sigmoid.h")
elif "silu" in func_name:
headers.append("cutlass/epilogue/thread/linear_combination_silu.h")
elif "hardswish" in func_name:
headers.append("cutlass/epilogue/thread/linear_combination_hardswish.h")
else:
headers.append("cutlass/epilogue/thread/linear_combination.h")
if "residual" in func_name:
headers.append("cutlass/epilogue/thread/linear_combination_residual_block.h")
def get_dim(shape_annot, var_name, axis_idx, batched_offset=0):
if isinstance(shape_annot, IntImm):
return str(int(shape_annot))
return f"{var_name}->shape[{batched_offset + axis_idx}]"
def get_batch_stride(stride_annot, arg0_idx, arg1_idx, arg0_axis_idx, arg1_axis_idx):
if isinstance(stride_annot, IntImm):
return str(int(stride_annot))
dim1 = func_args[arg0_idx] + f"->shape[{arg0_axis_idx}]"
dim2 = func_args[arg1_idx] + f"->shape[{arg1_axis_idx}]"
return dim1 + " * " + dim2
def get_flattened_batch_dim(arg_name, batch_rank):
return " * ".join([f"{arg_name}->shape[{i}]" for i in range(batch_rank)])
if "decode_matmul" in func_name:
headers.append("cutlass_kernels/fpA_intB_gemm.h")
lhs_arg_idx = _get_optional_int_annotation(annotations, "lhs_arg_idx", 0)
rhs_arg_idx = _get_optional_int_annotation(annotations, "rhs_arg_idx", 1)
scales_arg_idx = _get_optional_int_annotation(annotations, "scales_arg_idx", 2)
bias_arg_idx = _get_optional_int_annotation(annotations, "bias_arg_idx", None)
residual_arg_idx = _get_optional_int_annotation(annotations, "residual_arg_idx", None)
attrs["A_arg"] = func_args[lhs_arg_idx]
attrs["B_arg"] = func_args[rhs_arg_idx]
attrs["scales_arg"] = func_args[scales_arg_idx]
attrs["activation"] = annotations.get("activation", "identity")
attrs["bias_stride"] = annotations["bias_stride"]
attrs["M"] = annotations["M"]
attrs["group_size"] = annotations["group_size"]
if not isinstance(attrs["M"], tvm.tirx.IntImm):
attrs["M"] = get_flattened_batch_dim(
func_args[lhs_arg_idx], int(annotations["batch_rank"])
)
if bias_arg_idx is not None:
attrs["bias_arg"] = func_args[bias_arg_idx]
if residual_arg_idx is not None:
attrs["residual_arg"] = func_args[residual_arg_idx]
attrs["binary_op"] = annotations["binary_op"]
attrs["unary_op"] = annotations["unary_op"]
if annotations["weight_nbit"] == 4:
attrs["weight_dtype"] = "cutlass::uint4b_t"
attrs["float_per_int"] = 2
else:
assert annotations["weight_nbit"] == 8
attrs["weight_dtype"] = "uint8_t"
attrs["float_per_int"] = 1
code = emit_fp16A_intB_matmul(attrs)
return CodegenResult(code, headers)
elif "dense" in func_name or "matmul" in func_name:
batched = "batch" in annotations
# dense is equal to transposed_matmul
transposed = "transposed" in func_name or "dense" in func_name
lhs_arg_idx = _get_optional_int_annotation(annotations, "lhs_arg_idx", 0)
rhs_arg_idx = _get_optional_int_annotation(annotations, "rhs_arg_idx", 1)
if "bias" in func_name:
bias_arg_idx = _get_optional_int_annotation(annotations, "bias_arg_idx", 2)
else:
bias_arg_idx = _get_optional_int_annotation(annotations, "bias_arg_idx", None)
residual_arg_idx = _get_optional_int_annotation(annotations, "residual_arg_idx", None)
lhs_arg = func_args[lhs_arg_idx]
rhs_arg = func_args[rhs_arg_idx]
lhs_shape = annotations[f"arg{lhs_arg_idx}_shape"]
rhs_shape = annotations[f"arg{rhs_arg_idx}_shape"]
lhs_batched_offset = len(lhs_shape) - 2
rhs_batched_offset = len(rhs_shape) - 2
attrs["lhs_arg"] = lhs_arg
attrs["rhs_arg"] = rhs_arg
if bias_arg_idx is not None:
attrs["bias_arg"] = func_args[bias_arg_idx]
if residual_arg_idx is not None:
attrs["residual_arg"] = func_args[residual_arg_idx]
attrs["ElementInputA"] = DataTypeTag[dtype_map[annotations[f"arg{lhs_arg_idx}_dtype"]]]
attrs["ElementInputB"] = DataTypeTag[dtype_map[annotations[f"arg{rhs_arg_idx}_dtype"]]]
attrs["ElementOutput"] = DataTypeTag[dtype_map[annotations["ret_dtype"]]]
attrs["K"] = lhs_shape[lhs_batched_offset + 1]
attrs["M"] = get_dim(lhs_shape[lhs_batched_offset], lhs_arg, 0, lhs_batched_offset)
if transposed:
attrs["N"] = get_dim(rhs_shape[rhs_batched_offset], rhs_arg, 0, rhs_batched_offset)
else:
attrs["N"] = get_dim(rhs_shape[rhs_batched_offset + 1], rhs_arg, 1, rhs_batched_offset)
if batched:
headers.append("cutlass/gemm/device/gemm_batched.h")
def get_batch_on_arg(arg_name, arg_shape):
return " * ".join(f"{arg_name}->shape[{i}]" for i in range(len(arg_shape) - 2))
if isinstance(annotations["batch"], IntImm):
attrs["batch"] = str(int(annotations["batch"]))
elif annotations["batch_stride_A"] == 0:
# 2D x ND
attrs["batch"] = get_batch_on_arg(rhs_arg, rhs_shape)
else:
# ND x 2D or ND x ND
attrs["batch"] = get_batch_on_arg(lhs_arg, lhs_shape)
attrs["batch_stride_A"] = get_batch_stride(
annotations["batch_stride_A"],
lhs_arg_idx,
lhs_arg_idx,
lhs_batched_offset,
lhs_batched_offset + 1,
)
attrs["batch_stride_B"] = get_batch_stride(
annotations["batch_stride_B"],
rhs_arg_idx,
rhs_arg_idx,
rhs_batched_offset,
rhs_batched_offset + 1,
)
if transposed:
attrs["batch_stride_C"] = get_batch_stride(
annotations["batch_stride_C"],
lhs_arg_idx,
rhs_arg_idx,
lhs_batched_offset,
rhs_batched_offset,
)
else:
attrs["batch_stride_C"] = get_batch_stride(
annotations["batch_stride_C"],
lhs_arg_idx,
rhs_arg_idx,
lhs_batched_offset,
rhs_batched_offset + 1,
)
else:
headers.append("cutlass/gemm/device/gemm.h")
if "residual" in func_name:
headers.append("cutlass/gemm/device/gemm_universal_with_broadcast.h")
code = instantiate_gemm_template(attrs)
return CodegenResult(code, headers)
elif "conv2d" in func_name:
data_arg_idx = _get_optional_int_annotation(annotations, "data_arg_idx", 0)
weight_arg_idx = _get_optional_int_annotation(annotations, "weight_arg_idx", 1)
bias_arg_idx = _get_optional_int_annotation(annotations, "bias_arg_idx", None)
residual_arg_idx = _get_optional_int_annotation(annotations, "residual_arg_idx", None)
attrs["data_arg"] = func_args[data_arg_idx]
attrs["weight_arg"] = func_args[weight_arg_idx]
if bias_arg_idx is not None:
attrs["bias_arg"] = func_args[bias_arg_idx]
if residual_arg_idx is not None:
attrs["residual_arg"] = func_args[residual_arg_idx]
activation_shape = annotations[f"arg{data_arg_idx}_shape"]
weight_shape = annotations[f"arg{weight_arg_idx}_shape"]
output_shape = annotations["ret_shape"]
if "conv2d_transpose" in func_name:
headers.append("cutlass/conv/kernel/default_conv2d_dgrad.h")
activation_shape = output_shape
output_shape = annotations["arg0_shape"]
elif "backward" in func_name:
headers.append("cutlass/conv/kernel/default_conv2d_wgrad.h")
activation_shape = annotations["arg1_shape"]
weight_shape = output_shape
output_shape = annotations["arg0_shape"]
elif "residual" in func_name:
headers.append("cutlass/conv/kernel/default_conv2d_fprop_with_broadcast.h")
else:
headers.append("cutlass/conv/kernel/default_conv2d_fprop.h")
headers.append("cutlass/conv/device/implicit_gemm_convolution.h")
op_name = attrs["cutlass_op_name"]
if "splitk" in op_name:
headers += [
"cutlass/reduction/device/reduce_split_k.h",
"cutlass/reduction/thread/reduction_operators.h",
]
data_arg = attrs["data_arg"]
attrs["N"] = get_dim(activation_shape[0], data_arg, 0)
attrs["H"] = get_dim(activation_shape[1], data_arg, 1)
attrs["W"] = get_dim(activation_shape[2], data_arg, 2)
attrs["C"] = activation_shape[3]
attrs["P"] = get_dim(output_shape[1], "out0", 1)
attrs["Q"] = get_dim(output_shape[2], "out0", 2)
attrs["K"] = output_shape[3]
attrs["R"] = weight_shape[1]
attrs["S"] = weight_shape[2]
attrs["pad_h"] = annotations["padding"][0]
attrs["pad_w"] = annotations["padding"][1]
attrs["stride_h"] = annotations["strides"][0]
attrs["stride_w"] = annotations["strides"][1]
attrs["dilation_h"] = annotations["dilation"][0]
attrs["dilation_w"] = annotations["dilation"][1]
if "splitk" in op_name:
attrs["split_k_mode"] = "kParallel"
attrs["split_k_slices"] = str(re.search(r"splitk(\d+)", op_name).group(1))
else:
attrs["split_k_mode"] = "kSerial"
attrs["split_k_slices"] = 1
if "residual_shape" in annotations:
attrs["residual_shape"] = annotations["residual_shape"]
code = instantiate_conv2d_template(attrs)
return CodegenResult(code, headers)
elif "attention" in func_name:
is_var_len = "var_len" in func_name
data_type = dtype_map[annotations["arg0_dtype"]]
attrs["qkv_layout"] = annotations["qkv_layout"]
if attrs["qkv_layout"] == "default":
attrs["query"] = func_args[0]
attrs["key"] = func_args[1]
attrs["value"] = func_args[2]
attrs["num_queries"] = s = get_dim(annotations["num_queries"], func_args[0], 1)
attrs["num_keys"] = get_dim(annotations["num_keys"], func_args[1], 1)
if len(func_args) > 4 and not is_var_len: # +1 for workspace, the last arg
attrs["bias"] = func_args[3]
elif attrs["qkv_layout"] == "qkv_stacked":
attrs["qkv"] = func_args[0]
attrs["num_queries"] = s = annotations["num_queries"]
attrs["num_keys"] = annotations["num_keys"]
if len(func_args) > 2 and not is_var_len: # +1 for workspace, the last arg
attrs["bias"] = func_args[1]
else:
raise NotImplementedError()
attrs["data_type"] = DataTypeTag[data_type]
attrs["num_batches"] = b = annotations["num_batches"]
attrs["head_dim"] = h = annotations["head_dim"]
attrs["head_dim_value"] = h_v = annotations["head_dim_value"]
attrs["kMaxK"] = max(int(attrs["head_dim"]), int(attrs["head_dim_value"]))
attrs["scale"] = (
float(1 / math.sqrt(h.value)) if annotations["scale"] is None else annotations["scale"]
)
if is_var_len:
attrs["seqstart_q"] = func_args[int(annotations["seqstart_q_idx"])]
attrs["seqstart_k"] = func_args[int(annotations["seqstart_k_idx"])]
attrs["max_seqlen_q"] = func_args[int(annotations["max_seqlen_q_idx"])]
attrs["max_seqlen_k"] = func_args[int(annotations["max_seqlen_k_idx"])]
is_mqa = annotations["num_q_heads"] != annotations["num_kv_heads"]
use_flash = (
annotations["ret_dtype"] == "float16"
and "bias" not in attrs
and int(attrs["head_dim"]) <= 256
and int(attrs["head_dim"]) % 8 == 0
and int(attrs["head_dim"]) == int(attrs["head_dim_value"])
# For the causal case (custom mask = "BottomRight"), only use flash for multi-query
# attention workloads. Otherwise, CUTLASS fMHA seems faster for causal attention
# with a single query.
# In addition, sliding-window attention is only supported by flash.
and (
int(annotations["custom_mask_type"]) == 0
or (int(annotations["custom_mask_type"]) == 2 and is_mqa)
or (int(annotations["custom_mask_type"]) == 2 and "window_size" in annotations)
)
# Flash v2 is currently not supported for sm < 80
and int(annotations["arch"]) >= 80
)
# See https://github.com/Dao-AILab/flash-attention/blob/
# 92dd5703ecdb99aa4a4aee9817f28557907403a2/csrc/flash_attn/flash_api.cpp#L111-L116
if "window_size" in annotations:
assert use_flash, "Sliding-window attention is supported only by Flash Attention."
assert int(annotations["custom_mask_type"]) == 2, (
"Sliding-window attention is only supported for causal with bottom right mask."
)
attrs["window_size_left"] = int(annotations["window_size"]) - 1
attrs["window_size_right"] = 0
attrs["is_causal"] = False
else:
if int(annotations["custom_mask_type"]) == 2:
attrs["window_size_left"] = attrs["num_keys"]
attrs["window_size_right"] = 0
attrs["is_causal"] = True
else:
attrs["window_size_left"] = -1
attrs["window_size_right"] = -1
attrs["is_causal"] = False
if use_flash:
headers.append("flash.h")
attrs["num_q_heads"] = annotations["num_q_heads"]
attrs["num_kv_heads"] = annotations["num_kv_heads"]
if is_var_len:
code = instantiate_flash_attention_var_len_template(attrs)
else:
code = instantiate_flash_attention_template(attrs)
else:
headers.append("kernel_forward.h")
assert not is_mqa, (
"The number of query and KV heads need to be the same for CUTLASS fMHA."
)
attrs["num_heads"] = n = annotations["num_q_heads"]
data_type_size = DataTypeSize[data_type]
if (data_type_size * h // 8) % 16 == 0 and (data_type_size * h_v // 8) % 16 == 0:
attrs["kIsAligned"] = True
elif (h % 4 == 0) and (h_v % 4 == 0):
attrs["kIsAligned"] = False
else:
raise NotImplementedError()
if h_v > 64:
attrs["kQueriesPerBlock"] = 32
attrs["kKeysPerBlock"] = 128
attrs["kSingleValueIteration"] = h_v <= 128
else:
attrs["kQueriesPerBlock"] = 64
attrs["kKeysPerBlock"] = 64
attrs["kSingleValueIteration"] = True
assert attrs["scale"] > 0 or attrs["scale"] < 0, (
"Cutlass may generate nan occasionally when scale == 0.0"
)
attrs["arch"] = "cutlass::arch::Sm{}".format(annotations["arch"])
attrs["kSupportsDropout"] = False
attrs["output_size"] = f"{b} * {s} * {n} * {h_v}"
attrs["custom_mask_type"] = annotations["custom_mask_type"]
for arg in func_args:
if "workspace" in arg:
attrs["workspace"] = arg
if "bias" in attrs:
attrs["kSupportsBias"] = True
if len(annotations["bias_shape"]) == 4:
strides = "p.num_keys"
if annotations["bias_shape"][2] == 1:
attrs["bias_strideM"] = 0
else:
attrs["bias_strideM"] = strides
strides = f"p.num_queries * {strides}"
if annotations["bias_shape"][1] == 1:
attrs["bias_strideH"] = 0
else:
attrs["bias_strideH"] = strides
strides = f"p.num_heads * {strides}"
if annotations["bias_shape"][0] == 1:
attrs["bias_strideB"] = 0
else:
attrs["bias_strideB"] = strides
else:
raise NotImplementedError()
else:
# To support negative scale in current Cutlass implementation,
# kSupportsBias should be set true, or there are nan's as result.
attrs["kSupportsBias"] = attrs["scale"] < 0
code = instantiate_attention_template(attrs)
return CodegenResult(code, headers)
elif "layer_norm" in func_name:
headers.append("cutlass/util/device_layernorm.h")
headers.append("cutlass/layout/matrix.h")
attrs = {"input": func_args[0], "gamma": func_args[1], "beta": func_args[2]}
attrs.update(dict(annotations))
if not isinstance(attrs["M"], tvm.tirx.IntImm):
attrs["M"] = get_flattened_batch_dim(func_args[0], int(attrs["batch_rank"]))
code = instantiate_layer_norm_template(attrs)
return CodegenResult(code, headers)
elif "rms_norm" in func_name:
headers.append("cutlass/util/device_rmsnorm.h")
headers.append("cutlass/layout/matrix.h")
attrs = {"input": func_args[0], "weight": func_args[1]}
attrs.update(dict(annotations))
if not isinstance(attrs["M"], tvm.tirx.IntImm):
attrs["M"] = get_flattened_batch_dim(func_args[0], int(attrs["batch_rank"]))
code = instantiate_rms_norm_template(attrs)
return CodegenResult(code, headers)
raise ValueError(f"Do not have a template for {func_name}")
@@ -0,0 +1,48 @@
# 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
# ruff: noqa: E501
"""Generator for CUTLASS layer norm kernels."""
from .library import substitute_template
def instantiate_layer_norm_template(attrs):
"""
Return CUTLASS host code for layer norm based on
a template and the provided attribute map.
"""
template = """
using data_type = ${data_type};
using namespace cutlass::layout;
int M = ${M};
int N = ${N};
cutlass::MatrixCoord size(M, N);
auto layout_2D = RowMajor::packed(size);
auto layout_channels = RowMajor::packed({1, N});
cutlass::TensorRef<data_type, RowMajor> _input((data_type*)${input}->data, layout_2D);
cutlass::TensorRef<data_type, RowMajor> _gamma((data_type*)${gamma}->data, layout_channels);
cutlass::TensorRef<data_type, RowMajor> _beta((data_type*)${beta}->data, layout_channels);
cutlass::TensorRef<data_type, RowMajor> _output((data_type*)out0->data, layout_2D);
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(kDLCUDA, ${input}->device.device_id));
cutlass::layernorm(size, _output, _input, _gamma, _beta, stream);
"""
return substitute_template(template, attrs)
+301
View File
@@ -0,0 +1,301 @@
# 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,line-too-long
# ruff: noqa: E501
"""Various type definitions to help instantiate CUTLASS kernels."""
import enum
import re
from enum import auto as enum_auto
from tvm.tirx.expr import FloatImm, IntImm
class GeneratorTarget(enum.Enum):
Library = enum_auto()
class DataType(enum.Enum):
f16 = enum_auto()
f32 = enum_auto()
s8 = enum_auto()
u8 = enum_auto()
s32 = enum_auto()
ShortDataTypeNames = {DataType.f16: "h", DataType.f32: "s", DataType.s32: "i"}
DataTypeNames = {
DataType.f16: "f16",
DataType.f32: "f32",
DataType.s8: "s8",
DataType.u8: "u8",
DataType.s32: "s32",
}
DataTypeTag = {
DataType.f16: "cutlass::half_t",
DataType.f32: "float",
DataType.s8: "int8_t",
DataType.s32: "int32_t",
DataType.u8: "uint8_t",
}
DataTypeSize = {
DataType.f16: 16,
DataType.f32: 32,
DataType.u8: 8,
DataType.s8: 8,
DataType.s32: 32,
}
class MathOperation(enum.Enum):
multiply_add = enum_auto()
multiply_add_saturate = enum_auto()
multiply_add_fast_f32 = enum_auto()
MathOperationTag = {
MathOperation.multiply_add: "cutlass::arch::OpMultiplyAdd",
MathOperation.multiply_add_saturate: "cutlass::arch::OpMultiplyAddSaturate",
MathOperation.multiply_add_fast_f32: "cutlass::arch::OpMultiplyAddFastF32",
}
class LayoutType(enum.Enum):
ColumnMajor = enum_auto()
RowMajor = enum_auto()
TensorNHWC = enum_auto()
LayoutTag = {
LayoutType.ColumnMajor: "cutlass::layout::ColumnMajor",
LayoutType.RowMajor: "cutlass::layout::RowMajor",
LayoutType.TensorNHWC: "cutlass::layout::TensorNHWC",
}
TransposedLayout = {
LayoutType.ColumnMajor: LayoutType.RowMajor,
LayoutType.RowMajor: LayoutType.ColumnMajor,
LayoutType.TensorNHWC: LayoutType.TensorNHWC,
}
ShortLayoutTypeNames = {
LayoutType.ColumnMajor: "n",
LayoutType.RowMajor: "t",
LayoutType.TensorNHWC: "nhwc",
}
class OpcodeClass(enum.Enum):
Simt = enum_auto()
TensorOp = enum_auto()
WmmaTensorOp = enum_auto()
OpcodeClassNames = {
OpcodeClass.Simt: "simt",
OpcodeClass.TensorOp: "tensorop",
OpcodeClass.WmmaTensorOp: "wmma_tensorop",
}
OpcodeClassTag = {
OpcodeClass.Simt: "cutlass::arch::OpClassSimt",
OpcodeClass.TensorOp: "cutlass::arch::OpClassTensorOp",
OpcodeClass.WmmaTensorOp: "cutlass::arch::OpClassWmmaTensorOp",
}
class OperationKind(enum.Enum):
Gemm = enum_auto()
Conv2d = enum_auto()
OperationKindNames = {OperationKind.Gemm: "gemm", OperationKind.Conv2d: "conv2d"}
class Target(enum.Enum):
library = enum_auto()
def substitute_template(template, values):
"""Instantiate a kernel template using `values`."""
text = template
changed = True
while changed:
changed = False
for key, value in values.items():
if isinstance(value, int | IntImm):
value = str(int(value))
if isinstance(value, float | FloatImm):
value = str(float(value))
elif isinstance(value, bool):
value = str(value).lower()
regex = f"\\$\\{{{key}\\}}"
newtext = re.sub(regex, value, text)
if newtext != text:
changed = True
text = newtext
return text
class GemmKind(enum.Enum):
Gemm = enum_auto()
GemmKindNames = {GemmKind.Gemm: "gemm"}
class EpilogueFunctor(enum.Enum):
LinearCombination = enum_auto()
LinearCombinationRelu = enum_auto()
LinearCombinationBias = enum_auto()
LinearCombinationGelu = enum_auto()
LinearCombinationSigmoid = enum_auto()
LinearCombinationSilu = enum_auto()
LinearCombinationHardSwish = enum_auto()
LinearCombinationResidualBlock = enum_auto()
EpilogueFunctorTag = {
EpilogueFunctor.LinearCombination: "cutlass::epilogue::thread::LinearCombination",
EpilogueFunctor.LinearCombinationRelu: "cutlass::epilogue::thread::LinearCombinationRelu",
EpilogueFunctor.LinearCombinationBias: "cutlass::epilogue::thread::LinearCombination",
EpilogueFunctor.LinearCombinationGelu: "cutlass::epilogue::thread::LinearCombinationGELU",
EpilogueFunctor.LinearCombinationSigmoid: "cutlass::epilogue::thread::LinearCombinationSigmoid",
EpilogueFunctor.LinearCombinationSilu: "cutlass::epilogue::thread::LinearCombinationSilu",
EpilogueFunctor.LinearCombinationHardSwish: "cutlass::epilogue::thread::LinearCombinationHardSwish",
EpilogueFunctor.LinearCombinationResidualBlock: "cutlass::epilogue::thread::LinearCombinationResidualBlock",
}
class SwizzlingFunctor(enum.Enum):
Identity1 = enum_auto()
Identity2 = enum_auto()
Identity4 = enum_auto()
Identity8 = enum_auto()
Batched = enum_auto()
StridedDgradIdentity1 = enum_auto()
StridedDgradIdentity4 = enum_auto()
SwizzlingFunctorTag = {
SwizzlingFunctor.Identity1: "cutlass::gemm::threadblock::GemmIdentityThreadblockSwizzle<1>",
SwizzlingFunctor.Identity2: "cutlass::gemm::threadblock::GemmIdentityThreadblockSwizzle<2>",
SwizzlingFunctor.Identity4: "cutlass::gemm::threadblock::GemmIdentityThreadblockSwizzle<4>",
SwizzlingFunctor.Identity8: "cutlass::gemm::threadblock::GemmIdentityThreadblockSwizzle<8>",
SwizzlingFunctor.Batched: "cutlass::gemm::threadblock::GemmBatchedIdentityThreadblockSwizzle",
SwizzlingFunctor.StridedDgradIdentity1: "cutlass::conv::threadblock::StridedDgradIdentityThreadblockSwizzle<1>",
SwizzlingFunctor.StridedDgradIdentity4: "cutlass::conv::threadblock::StridedDgradIdentityThreadblockSwizzle<4>",
}
class ConvKind(enum.Enum):
Fprop = enum_auto()
Dgrad = enum_auto()
Wgrad = enum_auto()
ConvKindTag = {
ConvKind.Fprop: "cutlass::conv::Operator::kFprop",
ConvKind.Dgrad: "cutlass::conv::Operator::kDgrad",
ConvKind.Wgrad: "cutlass::conv::Operator::kWgrad",
}
ConvKindNames = {ConvKind.Fprop: "fprop", ConvKind.Dgrad: "dgrad", ConvKind.Wgrad: "wgrad"}
class StrideSupport(enum.Enum):
Strided = enum_auto()
Unity = enum_auto()
StrideSupportTag = {
StrideSupport.Strided: "cutlass::conv::StrideSupport::kStrided",
StrideSupport.Unity: "cutlass::conv::StrideSupport::kUnity",
}
StrideSupportNames = {StrideSupport.Strided: "", StrideSupport.Unity: "unity_stride"}
class IteratorAlgorithm(enum.Enum):
Analytic = enum_auto()
Optimized = enum_auto()
IteratorAlgorithmTag = {
IteratorAlgorithm.Analytic: "cutlass::conv::IteratorAlgorithm::kAnalytic",
IteratorAlgorithm.Optimized: "cutlass::conv::IteratorAlgorithm::kOptimized",
}
IteratorAlgorithmNames = {
IteratorAlgorithm.Analytic: "analytic",
IteratorAlgorithm.Optimized: "optimized",
}
class MathInstruction:
"""Describe characteristics of a math instruction."""
def __init__(
self,
instruction_shape,
element_a,
element_b,
element_c,
element_accumulator,
opcode_class,
math_operation=MathOperation.multiply_add,
):
self.instruction_shape = instruction_shape
self.element_a = element_a
self.element_b = element_b
self.element_c = element_c
self.element_accumulator = element_accumulator
self.opcode_class = opcode_class
self.math_operation = math_operation
class TileDescription:
"""Describe characteristics of a GEMM tile."""
def __init__(
self, threadblock_shape, stages, warp_count, math_instruction, min_compute, max_compute
):
self.threadblock_shape = threadblock_shape
self.stages = stages
self.warp_count = warp_count
self.math_instruction = math_instruction
self.minimum_compute_capability = min_compute
self.maximum_compute_capability = max_compute
def procedural_name(self):
return f"{self.threadblock_shape[0]}x{self.threadblock_shape[1]}_{self.threadblock_shape[2]}x{self.stages}"
class TensorDescription:
def __init__(self, element, layout, alignment=1):
self.element = element
self.layout = layout
self.alignment = alignment
@@ -0,0 +1,47 @@
# 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
# ruff: noqa: E501
"""Generator for CUTLASS rms norm kernels."""
from .library import substitute_template
def instantiate_rms_norm_template(attrs):
"""
Return CUTLASS host code for rms norm based on
a template and the provided attribute map.
"""
template = """
using data_type = ${data_type};
using namespace cutlass::layout;
int M = ${M};
int N = ${N};
cutlass::MatrixCoord size(M, N);
auto layout_2D = RowMajor::packed(size);
auto layout_channels = RowMajor::packed({1, N});
cutlass::TensorRef<data_type, RowMajor> _input((data_type*)${input}->data, layout_2D);
cutlass::TensorRef<data_type, RowMajor> _weight((data_type*)${weight}->data, layout_channels);
cutlass::TensorRef<data_type, RowMajor> _output((data_type*)out0->data, layout_2D);
cudaStream_t stream = static_cast<cudaStream_t>(TVMFFIEnvGetStream(kDLCUDA, ${input}->device.device_id));
cutlass::rmsnorm(size, _output, _input, _weight, stream, ${rms_eps});
"""
return substitute_template(template, attrs)
+66
View File
@@ -0,0 +1,66 @@
# 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.
"""Wrapping functions to bridge frameworks with DLPack support to TVM"""
import tvm.runtime
def convert_func(tvm_func, tensor_type, to_dlpack_func):
"""Convert a tvm function into one that accepts a tensor from another
framework, provided the other framework supports DLPACK
Parameters
----------
tvm_func: Function
Built tvm function operating on arrays
tensor_type: Type
Type of the tensors of the target framework
to_dlpack_func: Function
Function to convert the source tensors to DLPACK
"""
assert callable(tvm_func)
def _wrapper(*args):
args = tuple(
tvm.runtime.from_dlpack(to_dlpack_func(arg)) if isinstance(arg, tensor_type) else arg
for arg in args
)
return tvm_func(*args)
return _wrapper
def to_pytorch_func(tvm_func):
"""Convert a tvm function into one that accepts PyTorch tensors
Parameters
----------
tvm_func: Function
Built tvm function operating on arrays
Returns
-------
wrapped_func: Function
Wrapped tvm function that operates on PyTorch tensors
"""
# pylint: disable=import-outside-toplevel
import torch
import torch.utils.dlpack
return convert_func(tvm_func, torch.Tensor, torch.utils.dlpack.to_dlpack)
+164
View File
@@ -0,0 +1,164 @@
# 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.
"""External function interface to BLAS libraries."""
import tvm
from tvm import te
from ..topi.nn.utils import get_pad_tuple
def matmul(lhs, rhs, transa=False, transb=False, **kwargs):
"""Create an extern op that compute matrix mult of A and rhs with CrhsLAS
This function serves as an example on how to call external libraries.
Parameters
----------
lhs: Tensor
The left matrix operand
rhs: Tensor
The right matrix operand
transa: bool
Whether transpose lhs
transb: bool
Whether transpose rhs
Returns
-------
C: Tensor
The result tensor.
"""
n = lhs.shape[1] if transa else lhs.shape[0]
m = rhs.shape[0] if transb else rhs.shape[1]
return te.extern(
(n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.dnnl.matmul", ins[0], ins[1], outs[0], transa, transb
),
name="C",
**kwargs,
)
def dnnl_conv2d(
src,
weights,
stride,
padding,
dilation,
groups,
channel_last=False,
out_dtype="float32",
**kwargs,
):
"""Convolution operator in NCHW layout.
Parameters
----------
src : tvm.te.Tensor
4-D with shape [batch, in_channel, in_height, in_width]
weights : tvm.te.Tensor
4-D with shape [num_filter, in_channel, filter_height, filter_width]
stride : int or a list/tuple of two ints
Stride size, or [stride_height, stride_width]
padding : int or a list/tuple of 2 or 4 ints
padding size, or
[pad_height, pad_width] for 2 ints, or
[pad_top, pad_left, pad_bottom, pad_right] for 4 ints
dilation: int or a list/tuple of two ints
dilation size, or [dilation_height, dilation_width]
groups: str
input data layout: NCHW or NHWC
channel_last: bool
chose if input/output data format is in channel_last format(NHWC) or
in plain format(NCHW)
out_dtype: str
output datatype: now only support float32
Returns
-------
Output : tvm.te.Tensor
4-D with shape [batch, out_channel, out_height, out_width]
"""
assert isinstance(stride, int) or len(stride) == 2
assert isinstance(dilation, int) or len(dilation) == 2
if isinstance(stride, int):
stride_h = stride_w = stride
else:
stride_h, stride_w = stride
if isinstance(dilation, int):
dilation_h = dilation_w = dilation
else:
dilation_h, dilation_w = dilation
pre_cast = src.dtype == "float32"
post_cast = out_dtype == "float32"
if channel_last:
batch, in_height, in_width, _ = src.shape
kernel_h, kernel_w, _, num_filter = weights.shape
else:
batch, _, in_height, in_width = src.shape
num_filter, _, kernel_h, kernel_w = weights.shape
dilated_kernel_h = (kernel_h - 1) * dilation_h + 1
dilated_kernel_w = (kernel_w - 1) * dilation_w + 1
pad_top, pad_left, pad_down, pad_right = get_pad_tuple(
padding, (dilated_kernel_h, dilated_kernel_w)
)
out_channel = num_filter
out_height = (in_height - dilated_kernel_h + pad_top + pad_down) // stride_h + 1
out_width = (in_width - dilated_kernel_w + pad_left + pad_right) // stride_w + 1
if channel_last:
out_shape = (batch, out_height, out_width, out_channel)
else:
out_shape = (batch, out_channel, out_height, out_width)
return te.extern(
out_shape,
[src, weights],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.dnnl.conv2d",
ins[0],
ins[1],
outs[0],
pad_top,
pad_down,
pad_left,
pad_right,
stride[0],
stride[1],
groups,
channel_last,
pre_cast,
post_cast,
),
name="C",
dtype=out_dtype,
**kwargs,
)
+175
View File
@@ -0,0 +1,175 @@
# 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=missing-timeout
"""Helper utility for downloading"""
import logging
import os
import shutil
import tempfile
import time
from pathlib import Path
LOG = logging.getLogger("download")
def download(url, path, overwrite=False, size_compare=False, retries=3):
"""Downloads the file from the internet.
Set the input options correctly to overwrite or do the size comparison
Parameters
----------
url : str
Download url.
path : str
Local file path to save downloaded file.
overwrite : bool, optional
Whether to overwrite existing file, defaults to False.
size_compare : bool, optional
Whether to do size compare to check downloaded file, defaults
to False
retries: int, optional
Number of time to retry download, defaults to 3.
"""
# pylint: disable=import-outside-toplevel
import urllib.request as urllib2
path = Path(path).resolve()
if path.exists() and path.is_file() and not overwrite:
if size_compare:
import requests
file_size = path.stat().st_size
res_head = requests.head(url)
res_get = requests.get(url, stream=True)
if "Content-Length" not in res_head.headers:
res_get = urllib2.urlopen(url)
url_file_size = int(res_get.headers["Content-Length"])
if url_file_size != file_size:
LOG.warning("Existing file %s has incorrect size, downloading fresh copy", path)
download(url, path, overwrite=True, size_compare=False, retries=retries)
return
LOG.info("File %s exists, skipping.", path)
return
LOG.info("Downloading from url %s to %s", url, path)
# Stateful start time
start_time = time.time()
dirpath = path.parent
dirpath.mkdir(parents=True, exist_ok=True)
def _download_progress(count, block_size, total_size):
# pylint: disable=unused-argument
"""Show the download progress."""
if count == 0:
return
duration = time.time() - start_time
progress_bytes = int(count * block_size)
progress_megabytes = progress_bytes / (1024.0 * 1024)
speed_kbps = int(progress_bytes / (1024 * duration))
percent = min(int(count * block_size * 100 / total_size), 100)
# Temporarily suppress newlines on the output stream.
prev_terminator = logging.StreamHandler.terminator
logging.StreamHandler.terminator = ""
LOG.debug(
"\r...%d%%, %.2f MB, %d KB/s, %d seconds passed",
percent,
progress_megabytes,
speed_kbps,
duration,
)
logging.StreamHandler.terminator = prev_terminator
with tempfile.TemporaryDirectory() as tempdir:
tempdir = Path(tempdir)
download_loc = tempdir.joinpath(path.name)
for i_retry in range(retries):
# pylint: disable=broad-except
try:
urllib2.urlretrieve(url, download_loc, reporthook=_download_progress)
LOG.debug("")
try:
download_loc.rename(path)
except OSError:
# Prefer a move, but if the tempdir and final
# location are in different drives, fall back to a
# copy.
shutil.copy2(download_loc, path)
return
except Exception as err:
if i_retry == retries - 1:
raise err
LOG.warning(
"%s\nDownload attempt %d/%d failed, retrying.", repr(err), i_retry, retries
)
if "TEST_DATA_ROOT_PATH" in os.environ:
TEST_DATA_ROOT_PATH = Path(os.environ.get("TEST_DATA_ROOT_PATH"))
else:
TEST_DATA_ROOT_PATH = Path(Path("~").expanduser(), ".tvm_test_data")
TEST_DATA_ROOT_PATH.mkdir(parents=True, exist_ok=True)
def download_testdata(url, relpath, module=None, overwrite=False):
"""Downloads the test data from the internet.
Parameters
----------
url : str
Download url.
relpath : str
Relative file path.
module : Union[str, list, tuple], optional
Subdirectory paths under test data folder.
overwrite : bool, defaults to False
If True, will download a fresh copy of the file regardless of
the cache. If False, will only download the file if a cached
version is missing.
Returns
-------
abspath : str
Absolute file path of downloaded file
"""
global TEST_DATA_ROOT_PATH
if module is None:
module_path = ""
elif isinstance(module, str):
module_path = module
elif isinstance(module, list | tuple):
module_path = Path(*module)
else:
raise ValueError("Unsupported module: " + module)
abspath = Path(TEST_DATA_ROOT_PATH, module_path, relpath)
download(url, abspath, overwrite=overwrite, size_compare=False)
return str(abspath)
+19
View File
@@ -0,0 +1,19 @@
# 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.
"""Hexagon APIs."""
from .tools import *
@@ -0,0 +1,55 @@
# 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.
"""Hexagon environment checks for CI usage
These are required by tvm.testing and live here to avoid a circular dependency.
"""
import os
import tvm
ANDROID_SERIAL_NUMBER = "ANDROID_SERIAL_NUMBER"
HEXAGON_TOOLCHAIN = "HEXAGON_TOOLCHAIN"
def _compile_time_check():
"""Return True if compile-time support for Hexagon is present, otherwise
error string.
Backs :func:`tvm.testing.env.has_hexagon_toolchain`.
"""
if tvm.runtime.enabled("llvm") and tvm.target.codegen.llvm_version_major() < 7:
return "Hexagon requires LLVM 7 or later"
if "HEXAGON_TOOLCHAIN" not in os.environ:
return f"Missing environment variable {HEXAGON_TOOLCHAIN}."
return True
def _run_time_check():
"""Return True if run-time support for Hexagon is present, otherwise
error string.
Backs :func:`tvm.testing.env.has_hexagon`.
"""
if ANDROID_SERIAL_NUMBER not in os.environ:
return f"Missing environment variable {ANDROID_SERIAL_NUMBER}."
return True
@@ -0,0 +1,98 @@
# 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=missing-docstring, invalid-name, unnecessary-comprehension, unused-argument
import tvm
import tvm.testing
from tvm import relax
from tvm.contrib.hexagon import hexagon_unary_ops
def op_replace(call_node, func) -> bool:
if not isinstance(call_node, relax.Call):
return False
call_tir_op = tvm.ir.Op.get("relax.call_tir")
if call_node.op != call_tir_op:
return False
ops = [
"qnn.tanh",
"qnn.sqrt",
"qnn.rsqrt",
"qnn.exp",
"qnn.erf",
"qnn.sigmoid",
"qnn.hardswish",
"qnn.log",
"qnn.abs",
]
if func.attrs["op_attrs"]["op_name"] in ops:
return True
return False
@relax.expr_functor.mutator
class Tanh2TakeReplace(tvm.relax.PyExprMutator):
def __init__(self, mod: tvm.IRModule) -> None:
super().__init__(mod)
self.mod_ = mod
def transform(self) -> tvm.IRModule:
# Iterate over all the nodes to check for the node replaceable
for global_var, func in self.mod_.functions.items():
# Skip non-relax functions
if not isinstance(func, relax.Function):
continue
updated_func = self.visit_expr(func)
self.builder_.normalize(updated_func)
self.builder_.update_func(global_var, updated_func)
# At the end of the transformation we return the updated IRModule from the BlockBuilder.
return self.builder_.get()
def visit_call_(self, call_node: relax.Call) -> relax.Call:
call_tir_op = tvm.ir.Op.get("relax.call_tir")
if call_node.op != call_tir_op:
return call_node
var = call_node.args[0]
func = self.mod_[var]
if call_node.args[1][0].ty.dtype == "uint8":
if op_replace(call_node, func):
inp, inp_scale, inp_zp, out_scale, out_zp = [x for x in call_node.args[1]]
# LUT node creation
LUT = hexagon_unary_ops.LUT_generation(
inp_scale, inp_zp, out_scale, out_zp, call_node.args[0].name_hint
)
# Take operation node creation
take_func = hexagon_unary_ops.generate_take_primfunc(inp, call_node.ty)
take_func = take_func.without_attr("global_symbol")
take_func_gv = self.builder_.add_func(take_func, "take")
take_node = relax.call_tir(
take_func_gv,
relax.expr.Tuple(
[call_node.args[1][0], relax.expr.Constant(tvm.runtime.tensor(LUT))]
),
call_node.ty,
)
return take_node
return call_node
@tvm.ir.transform.module_pass(opt_level=2, name="replace_tanh_take")
class PassReplaceWithTakeOpPrimFuncs:
def transform_module(self, mod, ctx):
return Tanh2TakeReplace(mod).transform()
@@ -0,0 +1,99 @@
# 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=missing-docstring, invalid-name
import logging
import numpy as np
from scipy import special
from tvm import te
logger = logging.getLogger(__name__)
######################################################################
#################### PRIMFUNC FOR LUT and Take Op ####################
######################################################################
def saturate(x: te.Tensor, dtype: str):
"""Saturate value for the specified data type"""
return te.max(te.min_value(dtype), te.min(x, te.max_value(dtype)))
def hardswish_func(x):
x_2 = np.add(x, 3.0)
x_2 = np.clip(x_2, 0.0, 6.0)
return x * x_2 / 6.0
def LUT_generation(inp_scale, inp_zp, out_scale, out_zp, op_name) -> None:
LUT = []
for i in range(256):
i = np.int32(i)
# converting the constants to the numpy value
if inp_zp.data.shape == ():
i_zp = inp_zp.data.numpy()[()]
if inp_scale.data.shape == ():
i_scale = inp_scale.data.numpy()[()]
if out_zp.data.shape == ():
o_zp = out_zp.data.numpy()[()]
if out_scale.data.shape == ():
o_scale = out_scale.data.numpy()[()]
# Dequantization followed by computing the op value
dequant = (i - i_zp) * i_scale
if "tanh" in op_name:
op_val = np.tanh(dequant)
elif "rsqrt" in op_name:
op_val = 1 / np.sqrt(dequant)
elif "sqrt" in op_name:
op_val = np.sqrt(dequant)
elif "exp" in op_name:
op_val = np.exp(dequant)
elif "erf" in op_name:
op_val = special.erf(dequant)
elif "sigmoid" in op_name:
op_val = 1 / (1 + np.exp(np.negative(dequant)))
elif "hardswish" in op_name:
op_val = hardswish_func(dequant)
elif "log" in op_name:
op_val = np.log(dequant)
elif "abs" in op_name:
op_val = np.abs(dequant)
else:
logger.error("Error op is other than unary op")
# Quantizing the value generated and appending in the Look Up Table
quant = np.round((op_val) / o_scale) + o_zp
val = np.maximum(0, np.minimum(quant, 255)).astype(np.uint8)
LUT.append(val)
return LUT
def generate_take_primfunc(inp, ty):
# Generating the take op
N, H, W, C = inp.ty.shape
data = te.placeholder((N, H, W, C), dtype=ty.dtype, name="data")
LUT_func = te.placeholder((256,), dtype="uint8", name="LUT")
take = te.compute(
ty.shape,
lambda *indices: saturate((LUT_func[data[indices].astype("uint8")]), ty.dtype).astype(
ty.dtype
),
name="take_op",
)
mod = te.create_prim_func([data, LUT_func, take])
return mod
+561
View File
@@ -0,0 +1,561 @@
# 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, f-string-without-interpolation, consider-using-from-import
# ruff: noqa: E501
"""Tools/compilers/linkers for Hexagon"""
import io
import itertools
import os
import pathlib
import re
import subprocess
import sys
import tarfile
import numpy
from tvm_ffi import register_global_func
import tvm
import tvm.support.cc as cc
# Linking Hexagon shared libraries.
#
# link_shared(name-of-shared-library, list-of-objects, kw-args)
#
# To use a custom linker, define a function that returns the path to the
# linker, and pass it to 'register_linker':
#
# def custom_linker_path():
# return '/path/to/hexagon/linker'
#
# register_linker(custom_linker_path)
#
# Subsequent calls to 'link_shared' will use the newly registered linker.
HEXAGON_TOOLCHAIN = os.environ.get("HEXAGON_TOOLCHAIN", default="") # pylint: disable=invalid-name
HEXAGON_SDK_ROOT = os.environ.get("HEXAGON_SDK_ROOT", default="") # pylint: disable=invalid-name
HEXAGON_SDK_DOCKER_IMAGE = os.environ.get("HEXAGON_SDK_DOCKER_IMAGE", default="") # pylint: disable=invalid-name
HEXAGON_LINK_MAIN = pathlib.Path(HEXAGON_TOOLCHAIN) / "bin" / "hexagon-link" # pylint: disable=invalid-name
HEXAGON_CLANG_PLUS = pathlib.Path(HEXAGON_TOOLCHAIN) / "bin" / "hexagon-clang++" # pylint: disable=invalid-name
HEXAGON_SDK_INCLUDE_DIRS = [ # pylint: disable=invalid-name
pathlib.Path(HEXAGON_SDK_ROOT) / "incs",
pathlib.Path(HEXAGON_SDK_ROOT) / "incs" / "stddef",
]
HEXAGON_SIMULATOR_NAME = "simulator"
def register_linker(f):
"""Register a function that will return the path to the Hexagon linker."""
return register_global_func("tvm.contrib.hexagon.hexagon_link", f, True)
@register_global_func("tvm.contrib.hexagon.hexagon_link")
def hexagon_link() -> str:
"""Return path to the Hexagon linker."""
return str(HEXAGON_LINK_MAIN)
def hexagon_clang_plus() -> str:
"""Return path to the Hexagon clang++."""
return str(HEXAGON_CLANG_PLUS)
def toolchain_version(toolchain=None) -> list[int]:
"""Return the version of the Hexagon toolchain.
Parameters
----------
toolchain: str, optional
Path to the Hexagon toolchain. If not provided, the environment
variable HEXAGON_TOOLCHAIN is used.
Returns
-------
version: List[int]
List of numerical components of the version number. E.g. for version
"8.5.06" it will be [8, 5, 6].
"""
if toolchain is None:
toolchain = HEXAGON_TOOLCHAIN
assert toolchain is not None, "Please specify toolchain, or set HEXAGON_TOOLCHAIN variable"
result = subprocess.run(
[f"{toolchain}/bin/hexagon-clang", "-v"], capture_output=True, check=True
)
output = result.stderr.decode()
for line in output.splitlines():
m = re.match(r".* [Cc]lang version ([0-9\.]+)", line)
if m:
assert len(m.groups()) == 1
return [int(v) for v in m.group(1).split(".")]
raise RuntimeError("Cannot establish toolchain version")
@register_global_func("tvm.contrib.hexagon.link_shared")
def link_shared(so_name, objs, extra_args=None):
"""Link shared library on Hexagon using the registered Hexagon linker.
Parameters
----------
so_name : str
Name of the shared library file.
objs : list[str, tvm.tirx.StringImm]
extra_args : dict (str->str) or Map<String,String>
Additional arguments:
'hex_arch' - Hexagon architecture, e.g. v68
'verbose' - Print additional information if the key is present
Returns
-------
ret_val : int
This function returns 0 at the moment.
"""
# The list of object files can be passed as built-in Python strings,
# or as tvm.tirx.StringImm's.
def to_str(s):
if isinstance(s, tvm.tirx.StringImm):
return s.value
assert isinstance(s, str), 'argument "' + str(s) + '" should be a string or StrImm'
return s
objs = [to_str(s) for s in objs]
if not extra_args:
extra_args = {}
hex_arch = extra_args.get("hex_arch") or "v68"
linker = tvm.get_global_func("tvm.contrib.hexagon.hexagon_link")()
if extra_args.get("verbose"):
print("tvm.contrib.hexagon.link_shared:")
print(" Using linker:", linker)
print(" Library name:", so_name)
print(" Object files:", objs)
print(" Architecture:", hex_arch)
if not os.access(linker, os.X_OK):
message = 'The linker "' + linker + '" does not exist or is not executable.'
if not os.environ.get("HEXAGON_TOOLCHAIN"):
message += (
" The environment variable HEXAGON_TOOLCHAIN is unset. Please export "
+ "HEXAGON_TOOLCHAIN in your environment, so that ${HEXAGON_TOOLCHAIN}/bin/"
+ "hexagon-link exists."
)
else:
message += (
" Please verify the value of the HEXAGON_LINKER environment variable "
+ '(currently set to "'
+ HEXAGON_TOOLCHAIN
+ '").'
)
raise Exception(message)
libpath = os.path.join(HEXAGON_TOOLCHAIN, "target", "hexagon", "lib", hex_arch, "G0")
cc.create_shared(
so_name,
objs,
# pylint: disable=bad-whitespace
options=[
"-Bdynamic",
"-shared",
"-export-dynamic",
os.path.join(libpath, "pic", "libgcc.so"),
],
cc=linker,
)
return 0
def link_shared_macos(so_name, objs, extra_args=None):
"""Link Hexagon shared library using docker container with proper tooling.
Parameters
----------
so_name : str
Name of the shared library file.
objs : list[str, tvm.tirx.StringImm]
extra_args : dict (str->str) or Map<String,String>
Additional arguments:
'hex_arch' - Hexagon architecture, e.g. v68
Returns
-------
ret_val : int
This function returns 0 at the moment.
"""
# The list of object files can be passed as built-in Python strings,
# or as tvm.tirx.StringImm's.
def to_str(s):
if isinstance(s, tvm.tirx.StringImm):
return s.value
assert isinstance(s, str), 'argument "' + str(s) + '" should be a string or StrImm'
return s
objs = [to_str(s) for s in objs]
if not extra_args:
extra_args = {}
hex_arch = extra_args.get("hex_arch") or "v68"
ses = ContainerSession(HEXAGON_SDK_DOCKER_IMAGE)
hexagon_sdk_tools_path = ses.get_env("HEXAGON_TOOLCHAIN")
libpath = os.path.join(hexagon_sdk_tools_path, "target", "hexagon", "lib", hex_arch, "G0")
linker = os.path.join(hexagon_sdk_tools_path, "bin", "hexagon-link")
# Copy input data to docker container
docker_objs = [ses.copy_to(obj) for obj in objs]
docker_so_name = ses.tmp_dir + "/" + os.path.basename(so_name)
link_cmd = [linker, "-shared", "-fPIC", "-o", docker_so_name]
link_cmd += docker_objs
link_cmd += [
"-Bdynamic",
"-export-dynamic",
"-L" + os.path.join(libpath, "pic"),
"-lgcc",
]
ses.exec(link_cmd)
# Copy result back to host
ses.copy_from(docker_so_name, so_name)
return 0
if sys.platform == "darwin":
def __create_shared_mac(so_name, objs, **kwargs):
return link_shared_macos(so_name, objs, kwargs)
create_shared = __create_shared_mac
register_global_func("tvm.contrib.hexagon.link_shared", f=link_shared_macos, override=True)
else: # Linux and Win32
create_shared = cc.create_shared
register_global_func("tvm.contrib.hexagon.link_shared", f=link_shared, override=True)
def create_aot_shared(so_name: str | pathlib.Path, files, hexagon_arch: str, options=None):
"""Export Hexagon AOT module."""
options = options or []
if not os.access(str(HEXAGON_CLANG_PLUS), os.X_OK):
raise Exception(
'The Clang++ "' + str(HEXAGON_CLANG_PLUS) + '" does not exist or is not executable.'
)
if not HEXAGON_TOOLCHAIN:
raise Exception(
" The environment variable HEXAGON_TOOLCHAIN is unset. Please export "
+ "HEXAGON_TOOLCHAIN in your environment."
)
if not HEXAGON_SDK_ROOT:
raise Exception(
" The environment variable HEXAGON_SDK_ROOT is unset. Please export "
+ "HEXAGON_SDK_ROOT in your environment."
)
# The AOT C codegen uses TVM runtime functions
# (e.g. TVMBackendAllocWorkspace) directly. On Hexagon these calls
# should be made using functions pointers provided as __TVM*
# variables in the provided context. This workaround allows the
# the TVM runtime symbols to be visible to the compiled shared
# library.
#
# This workaround can be removed when AOT codegen can be done with
# LLVM codegen.
workaround_link_flags = os.environ.get("HEXAGON_SHARED_LINK_FLAGS")
if workaround_link_flags:
options.extend(workaround_link_flags.split())
tvm_dir = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) / ".." / ".." / ".." / ".."
compute_arch = f"compute{hexagon_arch}"
compile_options = [
"-O3",
f"-I{tvm_dir / 'include'}",
f"-I{tvm_dir / '3rdparty' / 'dlpack' / 'include'}",
f"-I{pathlib.Path(HEXAGON_SDK_ROOT) / 'rtos' / 'qurt' / compute_arch / 'include' / 'posix'}",
f"-I{pathlib.Path(HEXAGON_SDK_ROOT) / 'rtos' / 'qurt' / compute_arch / 'include' / 'qurt'}",
"-D_MACH_I32=int",
]
# For debugging
for path in HEXAGON_SDK_INCLUDE_DIRS:
compile_options.append(f"-I{path!s}")
cross_compile = cc.cross_compiler(compile_func=hexagon_clang_plus())
cross_compile.output_format = "o"
c_files = [str(file) for file in files]
cross_compile(str(so_name), c_files, options=compile_options + options)
def pack_imports(
module: tvm.runtime.Module,
is_system_lib: bool, # pylint: disable=unused-argument
c_symbol_prefix: str,
workspace_dir: str,
):
"""Create an ELF object file that contains the binary data for the modules
imported in `module`. This is a callback function for use as `fpack_imports`
in `export_library`.
Parameters
----------
module: tvm.runtime.Module
Module whose imported modules need to be serialized.
is_system_lib: bool
Flag whether the exported module will be used as a system library.
c_symbol_prefix: str
Prefix to prepend to the blob symbol.
workspace_dir: str
Location for created files.
Returns
-------
file_name: str
The name of the created object file.
"""
path_bin = os.path.join(workspace_dir, "imports.bin")
pack_to_bin_f_name = "runtime.ModulePackImportsToTensor"
fpack_to_bin = tvm.get_global_func(pack_to_bin_f_name)
assert fpack_to_bin, f"Expecting {pack_to_bin_f_name} in registry"
fpack_to_bin(module).numpy().tofile(path_bin)
mblob_symbol = c_symbol_prefix + tvm.get_global_func("runtime.ModuleImportsBlobName")()
binary_size = os.path.getsize(path_bin)
hexagon_toolchain = os.environ.get("HEXAGON_TOOLCHAIN")
assert hexagon_toolchain, "Please set HEXAGON_TOOLCHAIN variable"
version = toolchain_version(hexagon_toolchain)
assert version[0] == 8 and version[1] >= 5, (
"Please use Hexagon toolchain version 8.5.x or later"
)
if version[1] <= 6:
path_o = os.path.join(workspace_dir, f"{c_symbol_prefix}devc.o")
subprocess.run(
[
f"{hexagon_toolchain}/bin/hexagon-clang",
"-x",
"c",
"-c",
"/dev/null",
"-o",
path_o,
],
check=True,
)
subprocess.run(
[
f"{hexagon_toolchain}/bin/hexagon-llvm-objcopy",
path_o,
"--add-section",
f".rodata={path_bin}",
"--add-symbol",
f"{mblob_symbol}=.rodata:0,object",
],
check=True,
)
return path_o
else: # 8.6.07+
path_c = os.path.join(workspace_dir, f"{c_symbol_prefix}devc.c")
path_o = os.path.join(workspace_dir, f"{c_symbol_prefix}devc.o")
with open(path_c, "w") as f:
f.write(
f"const unsigned char {mblob_symbol}[{binary_size}] "
f'__attribute__((section(".rodata"))) = {{0x1}};'
)
subprocess.run(
[f"{hexagon_toolchain}/bin/hexagon-clang", "-c", path_c, "-o", path_o], check=True
)
subprocess.run(
[
f"{hexagon_toolchain}/bin/hexagon-llvm-objcopy",
path_o,
"--update-section",
f".rodata={path_bin}",
],
check=True,
)
return path_o
def export_module(module, out_dir, binary_name="test_binary.so"):
"""Export Hexagon shared object to a file."""
binary_path = pathlib.Path(out_dir) / binary_name
module.write_to_file(str(binary_path))
return binary_path
def allocate_hexagon_array(
dev, tensor_shape=None, dtype=None, data=None, axis_separators=None, mem_scope=None
):
"""
Allocate a hexagon array which could be a 2D array
on physical memory defined by axis_separators
"""
if tensor_shape is None:
assert data is not None, "Must provide either tensor shape or numpy data array"
tensor_shape = data.shape
elif data is not None:
assert tensor_shape == data.shape, (
"Mismatch between provided tensor shape and numpy data array shape"
)
if dtype is None:
assert data is not None, "Must provide either dtype or numpy data array"
dtype = data.dtype.name
elif data is not None:
assert dtype == data.dtype, "Mismatch between provided dtype and numpy data array dtype"
if axis_separators is None:
axis_separators = []
boundaries = [0, *axis_separators, len(tensor_shape)]
physical_shape = [
numpy.prod(tensor_shape[dim_i:dim_f]) for dim_i, dim_f in itertools.pairwise(boundaries)
]
arr = tvm.runtime.empty(physical_shape, dtype=dtype, device=dev, mem_scope=mem_scope)
if data is not None:
arr.copyfrom(data.reshape(physical_shape))
return arr._create_view(tensor_shape)
class ContainerSession:
"""Docker container session
Parameters
----------
base_image_name : str
Docker image name to use. Empty string means to use default "tlcpack/ci-hexagon"
base image.
"""
def __init__(self, base_image_name: str = ""):
self._client = None
self._container = None
self.tmp_dir = None
self._client = ContainerSession._get_docker_client()
if base_image_name == "":
base_image_name = ContainerSession._get_latest_ci_image(self._client)
self._container = ContainerSession._find_container_or_create(self._client, base_image_name)
exit_code, tmp_dir_b = self._container.exec_run("mktemp -d -t tvm-toolbox-XXXXXXXXXX")
assert exit_code == 0
self.tmp_dir = tmp_dir_b.decode("utf-8").rstrip()
def __del__(self):
self.close()
@staticmethod
def _get_latest_ci_image(client) -> str:
ci_images = client.images.list(name="tlcpack/ci-hexagon")
ci_images.sort(reverse=True, key=lambda img: img.tags[0])
return ci_images[0].tags[0]
@staticmethod
def _get_docker_client():
try:
from docker import from_env # pylint: disable=import-outside-toplevel
from docker.errors import DockerException
except (ModuleNotFoundError, ImportError):
raise Exception("Docker SDK module is not installed. Please install it.")
try:
client = from_env()
except DockerException:
raise Exception(
"Docker server is not available. Please verify the docker is installed, "
"launched and available via command line ('dokcer ps' should works)."
)
return client
@staticmethod
def _find_container_or_create(client, image_name: str):
all_containers = client.containers.list(all=True)
filtered_containers = []
for container in all_containers:
tags: list = container.image.tags
img_name: str = tags[0]
if img_name.startswith(image_name) and container.name.startswith("tvm-hex-toolbox"):
filtered_containers.append(container)
if len(filtered_containers) == 0:
container = client.containers.run(
image=image_name, detach=True, tty=True, name="tvm-hex-toolbox"
)
else:
container = filtered_containers[0]
if container.status != "running":
container.start()
return container
def exec(self, cmd) -> str:
"""Execute command inside docker container"""
exit_code, res = self._container.exec_run(cmd)
assert exit_code == 0
return res.decode("utf-8")
def get_env(self, key: str) -> str:
"""Return env var value from docker container"""
res: str = self.exec(f"bash -c 'echo \"${key}\"'")
return res.rstrip(" \n")
def copy_to(self, host_file_path: str) -> str:
"""Upload file to docker container"""
file_name = os.path.basename(host_file_path)
byte_stream = io.BytesIO()
with tarfile.open(fileobj=byte_stream, mode="w:gz") as tar:
tar.add(host_file_path, arcname=file_name)
self._container.put_archive(path=self.tmp_dir, data=byte_stream.getvalue())
return f"{self.tmp_dir}/{file_name}"
def copy_from(self, container_file_path: str, host_file_path: str):
"""Download file from docker container"""
tar_bytes_gen, _ = self._container.get_archive(container_file_path)
# convert to bytes
tar_bytes = b""
for chunk in tar_bytes_gen:
tar_bytes += chunk
tar = tarfile.open(fileobj=io.BytesIO(initial_bytes=tar_bytes))
assert len(tar.getmembers()) == 1
tar_element_reader = tar.extractfile(tar.getmembers()[0])
with open(host_file_path, "wb") as host_file:
for chunk in tar_element_reader:
host_file.write(chunk)
def close(self):
"""Close docker container session"""
if self.tmp_dir is not None:
exit_code, _ = self._container.exec_run(f"rm -rf {self.tmp_dir}")
assert exit_code == 0
+87
View File
@@ -0,0 +1,87 @@
# 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.
"""External function interface to hipBLAS libraries."""
import tvm
from tvm import te
def matmul(lhs, rhs, transa=False, transb=False, dtype=None):
"""Create an extern op that compute matrix mult of A and rhs with cuBLAS
Parameters
----------
lhs : Tensor
The left matrix operand
rhs : Tensor
The right matrix operand
transa : bool
Whether transpose lhs
transb : bool
Whether transpose rhs
Returns
-------
C : Tensor
The result tensor.
"""
n = lhs.shape[1] if transa else lhs.shape[0]
m = rhs.shape[0] if transb else rhs.shape[1]
dtype = dtype if dtype is not None else lhs.dtype
return te.extern(
(n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.hipblas.matmul", ins[0], ins[1], outs[0], transa, transb
),
dtype=dtype,
name="matmul_hipblas",
)
def batch_matmul(lhs, rhs, transa=False, transb=False, dtype=None):
"""Create an extern op that compute batch matrix mult of A and rhs with cuBLAS
Parameters
----------
lhs : Tensor
The left matrix operand
rhs : Tensor
The right matrix operand
transa : bool
Whether transpose lhs
transb : bool
Whether transpose rhs
Returns
-------
C : Tensor
The result tensor.
"""
b = lhs.shape[0]
n = lhs.shape[2] if transa else lhs.shape[1]
m = rhs.shape[1] if transb else rhs.shape[2]
dtype = dtype if dtype is not None else lhs.dtype
return te.extern(
(b, n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.hipblas.batch_matmul", ins[0], ins[1], outs[0], transa, transb
),
dtype=dtype,
name="batch_matmul_hipblas",
)
+127
View File
@@ -0,0 +1,127 @@
# 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.
"""External function interface to BLAS libraries."""
import tvm
from tvm import te
def matmul(lhs, rhs, transa=False, transb=False, **kwargs):
"""Create an extern op that compute matrix mult of A and rhs with CrhsLAS
This function serves as an example on how to call external libraries.
Parameters
----------
lhs: Tensor
The left matrix operand
rhs: Tensor
The right matrix operand
transa: bool
Whether transpose lhs
transb: bool
Whether transpose rhs
Returns
-------
C: Tensor
The result tensor.
"""
n = lhs.shape[1] if transa else lhs.shape[0]
m = rhs.shape[0] if transb else rhs.shape[1]
return te.extern(
(n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.mkl.matmul", ins[0], ins[1], outs[0], transa, transb
),
name="C",
**kwargs,
)
def matmul_u8s8s32(lhs, rhs, transa=False, transb=False, **kwargs):
"""Create an extern op that compute matrix mult of A and rhs with CrhsLAS
This function serves as an example on how to call external libraries.
Parameters
----------
lhs: Tensor
The left matrix operand
rhs: Tensor
The right matrix operand
transa: bool
Whether transpose lhs
transb: bool
Whether transpose rhs
Returns
-------
C: Tensor
The result tensor.
"""
n = lhs.shape[1] if transa else lhs.shape[0]
m = rhs.shape[0] if transb else rhs.shape[1]
return te.extern(
(n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.mkl.matmul_u8s8s32", ins[0], ins[1], outs[0], transa, transb
),
name="C",
**kwargs,
)
def batch_matmul(lhs, rhs, transa=False, transb=False, iterative=False, **kwargs):
"""Create an extern op that compute batched matrix mult of A and rhs with mkl
This function serves as an example on how to call external libraries.
Parameters
----------
lhs: Tensor
The left matrix operand
rhs: Tensor
The right matrix operand
transa: bool
Whether transpose lhs
transb: bool
Whether transpose rhs
Returns
-------
C: Tensor
The result tensor.
"""
b = te.max(lhs.shape[0], rhs.shape[0])
n = lhs.shape[2] if transa else lhs.shape[1]
m = rhs.shape[1] if transb else rhs.shape[2]
return te.extern(
(b, n, m),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.mkl.batch_matmul"
if not iterative
else "tvm.contrib.mkl.batch_matmul_iterative",
ins[0],
ins[1],
outs[0],
transa,
transb,
),
name="C",
**kwargs,
)
+238
View File
@@ -0,0 +1,238 @@
# 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.
# ruff: noqa: F821
"""External function interface to NNPACK libraries."""
import tvm_ffi
import tvm
from tvm import te
def is_available():
"""Check whether NNPACK is available, that is, `nnp_initialize()`
returns `nnp_status_success`.
"""
return _initialize() == 0
def fully_connected_inference(lhs, rhs, nthreads=1):
"""Create an extern op that compute fully connected of 1D tensor lhs and
2D tensor rhs with nnpack.
Parameters
----------
lhs : Tensor
lhs 1D array input[input_channels] of FP32 elements
rhs : Tensor
lhs 2D matrix kernel[output_channels][input_channels] of FP32 elements
Returns
-------
C : Tensor
lhs 1D array out[output_channels] of FP32 elements.
"""
m = rhs.shape[0]
return te.extern(
(m,),
[lhs, rhs],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.nnpack.fully_connected_inference", ins[0], ins[1], outs[0], nthreads
),
name="C",
)
class ConvolutionAlgorithm:
AUTO = 0
FFT_8x8 = 1
FFT_16x16 = 2
WT_8x8 = 3
IMPLICIT_GEMM = 4
DIRECT = 5
WT_8x8_FP16 = 6
class ConvolutionTransformStrategy:
COMPUTE = 1
PRECOMPUTE = 2
def convolution_inference(
data, kernel, bias, padding, stride, nthreads=1, algorithm=ConvolutionAlgorithm.AUTO
):
"""Create an extern op to do inference convolution of 4D tensor data and
4D tensor kernel and 1D tensor bias with nnpack.
Parameters
----------
data : Tensor
data 4D tensor input[batch][input_channels][input_height][input_width] of
FP32 elements.
kernel : Tensor
kernel 4D tensor kernel[output_channels][input_channels][kernel_height]
[kernel_width] of FP32 elements.
bias : Tensor
bias 1D array bias[output_channels][input_channels][kernel_height]
[kernel_width] of FP32 elements.
padding : list
padding A 4-dim list of [pad_top, pad_bottom, pad_left, pad_right],
which indicates the padding around the feature map.
stride : list
stride A 2-dim list of [stride_height, stride_width], which indicates
the stride.
Returns
-------
output : Tensor
output 4D tensor output[batch][output_channels][output_height][output_width]
of FP32 elements.
"""
assert isinstance(padding, list) and len(padding) == 4
assert isinstance(stride, list) and len(stride) == 2
batch, _, input_height, input_width = data.shape
output_channels, _, kernel_height, kernel_width = kernel.shape
idxdiv = te.indexdiv
output_height = idxdiv(input_height + padding[0] + padding[1] - kernel_height, stride[0]) + 1
output_width = idxdiv(input_width + padding[0] + padding[1] - kernel_width, stride[1]) + 1
return te.extern(
(batch, output_channels, output_height, output_width),
[data, kernel, bias] if bias is not None else [data, kernel],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.nnpack.convolution_inference",
ins[0],
ins[1],
ins[2] if bias is not None else 0,
outs[0],
padding[0],
padding[1],
padding[2],
padding[3],
stride[0],
stride[1],
nthreads,
algorithm,
),
name="C",
)
def convolution_inference_without_weight_transform(
data, transformed_kernel, bias, padding, stride, nthreads=1, algorithm=ConvolutionAlgorithm.AUTO
):
"""Create an extern op to do inference convolution of 4D tensor data and
4D pre-transformed tensor kernel and 1D tensor bias with nnpack.
Parameters
----------
data : Tensor
data 4D tensor input[batch][input_channels][input_height][input_width] of
FP32 elements.
transformed_kernel : Tensor
transformed_kernel 4D tensor kernel[output_channels][input_channels][tile]
[tile] of FP32 elements.
bias : Tensor
bias 1D array bias[output_channels][input_channels][kernel_height]
[kernel_width] of FP32 elements.
padding : list
padding A 4-dim list of [pad_top, pad_bottom, pad_left, pad_right],
which indicates the padding around the feature map.
stride : list
stride A 2-dim list of [stride_height, stride_width], which indicates
the stride.
Returns
-------
output : Tensor
output 4D tensor output[batch][output_channels][output_height][output_width]
of FP32 elements.
"""
assert algorithm in (ConvolutionAlgorithm.WT_8x8, ConvolutionAlgorithm.WT_8x8_FP16)
assert isinstance(padding, list) and len(padding) == 4
assert isinstance(stride, list) and len(stride) == 2
batch, _, input_height, input_width = data.shape
output_channels, _, _, _ = transformed_kernel.shape
kernel_height, kernel_width = (3, 3)
idxdiv = te.indexdiv
output_height = idxdiv(input_height + padding[0] + padding[1] - kernel_height, stride[0]) + 1
output_width = idxdiv(input_width + padding[0] + padding[1] - kernel_width, stride[1]) + 1
return te.extern(
(batch, output_channels, output_height, output_width),
[data, transformed_kernel, bias] if bias is not None else [data, transformed_kernel],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.nnpack.convolution_inference_without_weight_transform",
ins[0],
ins[1],
ins[2] if bias is not None else 0,
outs[0],
padding[0],
padding[1],
padding[2],
padding[3],
stride[0],
stride[1],
nthreads,
algorithm,
),
name="C",
dtype="float32",
)
def convolution_inference_weight_transform(
kernel, nthreads=1, algorithm=ConvolutionAlgorithm.AUTO, dtype="float32"
):
"""Create an extern op to do inference convolution of 3D tensor data and
4D tensor kernel and 1D tensor bias with nnpack.
Parameters
----------
kernel : Tensor
kernel 4D tensor kernel[output_channels][input_channels][kernel_height]
[kernel_width] of FP32 elements.
Returns
-------
output : Tensor
output 4D tensor output[output_channels][input_channels][tile][tile]
of FP32 elements.
"""
assert algorithm in (ConvolutionAlgorithm.WT_8x8, ConvolutionAlgorithm.WT_8x8_FP16)
output_channels, input_channels, _, _ = kernel.shape
transform_tile_size = 8
if not isinstance(dtype, str):
dtype = dtype.dtype
return te.extern(
(output_channels, input_channels, transform_tile_size, transform_tile_size),
[kernel],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.nnpack.convolution_inference_weight_transform",
ins[0],
outs[0],
nthreads,
algorithm,
),
name="transform_kernel",
dtype=dtype,
)
tvm_ffi.init_ffi_api("tvm.contrib.nnpack")
+144
View File
@@ -0,0 +1,144 @@
# 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.
# ruff: noqa: RUF005, RUF012
"""Memoize result of function via pickle, used for cache testcases."""
# pylint: disable=broad-except,superfluous-parens
import atexit
import functools
import os
import pathlib
import sys
try:
import cPickle as pickle
except ImportError:
import pickle
def _get_global_cache_dir() -> pathlib.Path:
if "XDG_CACHE_HOME" in os.environ:
cache_home = pathlib.Path(os.environ.get("XDG_CACHE_HOME"))
else:
cache_home = pathlib.Path.home().joinpath(".cache")
return cache_home.joinpath("tvm", f"pkl_memoize_py{sys.version_info[0]}")
GLOBAL_CACHE_DIR = _get_global_cache_dir()
class Cache:
"""A cache object for result cache.
Parameters
----------
key: str
The file key to the function
save_at_exit: bool
Whether save the cache to file when the program exits
"""
cache_by_key = {}
def __init__(self, key, save_at_exit):
self._cache = None
self.path = GLOBAL_CACHE_DIR.joinpath(key)
self.dirty = False
self.save_at_exit = save_at_exit
@property
def cache(self):
"""Return the cache, initializing on first use."""
if self._cache is not None:
return self._cache
if self.path.exists():
with self.path.open("rb") as cache_file:
try:
cache = pickle.load(cache_file)
except pickle.UnpicklingError:
cache = {}
else:
cache = {}
self._cache = cache
return self._cache
def save(self):
if self.dirty:
self.path.parent.mkdir(parents=True, exist_ok=True)
with self.path.open("wb") as out_file:
pickle.dump(self.cache, out_file, pickle.HIGHEST_PROTOCOL)
@atexit.register
def _atexit():
"""Save handler."""
for value in Cache.cache_by_key.values():
if value.save_at_exit:
value.save()
def memoize(key, save_at_exit=False):
"""Memoize the result of function and reuse multiple times.
Parameters
----------
key: str
The unique key to the file
save_at_exit: bool
Whether save the cache to file when the program exits
Returns
-------
fmemoize : function
The decorator function to perform memoization.
"""
def _register(f):
"""Registration function"""
allow_types = (str, int, float, tuple)
fkey = key + "." + f.__name__ + ".pkl"
if fkey not in Cache.cache_by_key:
Cache.cache_by_key[fkey] = Cache(fkey, save_at_exit)
cache = Cache.cache_by_key[fkey]
cargs = tuple(x.cell_contents for x in f.__closure__) if f.__closure__ else ()
cargs = (len(cargs),) + cargs
@functools.wraps(f)
def _memoized_f(*args, **kwargs):
assert not kwargs, "Only allow positional call"
key = cargs + args
for arg in key:
if isinstance(arg, tuple):
for x in arg:
assert isinstance(x, allow_types)
else:
assert isinstance(arg, allow_types)
if key in cache.cache:
return cache.cache[key]
res = f(*args)
cache.cache[key] = res
cache.dirty = True
return res
return _memoized_f
return _register
+117
View File
@@ -0,0 +1,117 @@
# 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.
"""External function interface to random library."""
import tvm_ffi
import tvm
from tvm import te
def randint(low, high, size, dtype="int32"):
"""Return random integers from low (inclusive) to high (exclusive).
Return random integers from the "discrete uniform" distribution of the
specified dtype in the "half-open" interval [low, high).
Parameters
----------
low : int
Lowest (signed) integer to be drawn from the distribution
high : int
One above the largest (signed) integer to be drawn from the distribution
Returns
-------
out : Tensor
A tensor with specified size and dtype
"""
assert "int" in dtype, "the type of randint output must be int or uint"
return te.extern(
size,
[],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.random.randint", int(low), int(high), outs[0]
),
dtype=dtype,
)
def uniform(low, high, size):
"""Draw samples from a uniform distribution.
Samples are uniformly distributed over the half-open interval [low, high)
(includes low, but excludes high). In other words, any value within the
given interval is equally likely to be drawn by uniform.
Parameters
----------
low : float
Lower boundary of the output interval. All values generated will be
greater than or equal to low.
high : float
Upper boundary of the output interval. All values generated will be
less than high.
size : tuple of ints
Output shape. If the given shape is, e.g., (m, n, k), then m * n * k
samples are drawn.
Returns
-------
out : Tensor
A tensor with specified size and dtype.
"""
return te.extern(
size,
[],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.random.uniform", float(low), float(high), outs[0]
),
dtype="float32",
)
def normal(loc, scale, size):
"""Draw samples from a normal distribution.
Return random samples from a normal distribution.
Parameters
----------
loc : float
loc of the distribution.
scale : float
Standard deviation of the distribution.
size : tuple of ints
Output shape. If the given shape is, e.g., (m, n, k), then m * n * k
samples are drawn.
Returns
------
out : Tensor
A tensor with specified size and dtype
"""
return te.extern(
size,
[],
lambda ins, outs: tvm.tirx.call_packed(
"tvm.contrib.random.normal", float(loc), float(scale), outs[0]
),
dtype="float32",
)
tvm_ffi.init_ffi_api("tvm.contrib.random")
+47
View File
@@ -0,0 +1,47 @@
# 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.
"""Utilities for thrust"""
import logging
from tvm_ffi import get_global_func
def maybe_warn(target, func_name):
if (
"thrust" in list(target.attrs.get("libs", []))
and get_global_func(func_name, allow_missing=True) is None
):
logging.warning("thrust is requested but TVM is not built with thrust.")
def can_use_thrust(target, func_name):
maybe_warn(target, func_name)
return (
target.kind.name in ["cuda", "nvptx"]
and "thrust" in list(target.attrs.get("libs", []))
and get_global_func(func_name, allow_missing=True)
)
def can_use_rocthrust(target, func_name):
maybe_warn(target, func_name)
return (
target.kind.name == "rocm"
and "thrust" in list(target.attrs.get("libs", []))
and get_global_func(func_name, allow_missing=True)
)
+422
View File
@@ -0,0 +1,422 @@
# 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.
# ruff: noqa: E501, F401
"""Namespace to store utilities for building web runtime."""
import hashlib
import json
import math
import os
import shutil
# pylint: disable=unused-import
import sys
from collections.abc import Iterator, Mapping
from types import GeneratorType
from typing import Any, Optional, Union
import numpy as np
try:
import ml_dtypes
except ImportError:
ml_dtypes = None
import tvm
from tvm.runtime import DataType
from tvm.support.emcc import create_tvmjs_wasm, find_wasm_lib
def _convert_f32_to_bf16(value):
cap = np.finfo("float32").max
assert -np.finfo("float32").max == np.finfo("float32").min
bf16_limit = ((np.array([cap.view("uint32")]) >> 16) << 16).view("float32")[0]
# When the value is in [-bf16_limit, bf16_limit], round to nearest even.
# We can afford to do it in dumping phase to reduce overall rounding error.
#
# When the value is out of bound(usually mask values in attention), use truncation
# so it is equivalent to clip to the limit values
data = value.view("uint32")
rounding_bias = np.where(
np.logical_and(value < bf16_limit, value > -bf16_limit),
((data >> 16) & 1) + 0x7FFF,
np.zeros_like(data),
)
return ((data + rounding_bias) >> 16).astype("uint16")
def _convert_bf16_to_f32(value):
data = value.view("uint16")
return (data.astype("uint32") << 16).view("float32")
def _calculate_md5(filename):
hash_md5 = hashlib.md5()
with open(filename, "rb") as file:
for chunk in iter(lambda: file.read(8192), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
class TensorCacheShardingManager:
"""Internal helper to shard ndarrays."""
def __init__(
self,
cache_dir: str,
prefix: str,
shard_cap_nbytes: int,
initial_shard_records: Mapping[str, Any] | None = None,
):
self.cache_dir = cache_dir
self.prefix = prefix
self.curr_records = []
self.curr_data = bytearray()
self.shard_records = []
self.shard_cap_nbytes = shard_cap_nbytes
self.counter = 0
self.name_to_record: Mapping[str, tuple[int, Mapping[str, Any]]] = {}
self.updated_shards: set[int] = set()
if initial_shard_records is not None:
self.shard_records = initial_shard_records
self.counter = len(initial_shard_records)
for idx, shard in enumerate(initial_shard_records):
for rec in shard["records"]:
self.name_to_record[rec["name"]] = (idx, rec)
def append_or_update(self, data, name, shape, dtype, encode_format, allow_update: bool = False):
"""Commit a record to the manager.
Parameters
----------
data: bytes
Raw bytes to be appended.
name: str
The name of the parameter
shape: tuple
The shape of the array
dtype: str
The dtype information
encode_format:
The encode format of the entry
allow_update: bool
If the record already exists, update the record. Otherwise, raise an error.
"""
rec = {
"name": name,
"shape": shape,
"dtype": dtype,
"format": encode_format,
"nbytes": len(data),
}
if name in self.name_to_record:
if not allow_update:
raise ValueError(f"Duplicate name {name} found in the cache.")
self.update_single_record(rec, data)
return
self.name_to_record[name] = (self.counter, rec)
if self.pending_nbytes + len(data) >= self.shard_cap_nbytes:
if len(data) * 2 >= self.shard_cap_nbytes:
# out of band data
rec["byteOffset"] = 0
self._commit_internal(data, [rec])
return
self.commit()
rec["byteOffset"] = self.pending_nbytes
self.curr_records.append(rec)
self.curr_data += data
def update_single_record(self, rec, data):
"""Update a single record in a shard file."""
name = rec["name"]
idx, old_rec = self.name_to_record[name]
if old_rec["nbytes"] != rec["nbytes"]:
raise ValueError(f"Cannot update record {name}, size mismatch.")
data_path = self.shard_records[idx]["dataPath"]
full_path = os.path.join(self.cache_dir, data_path)
with open(full_path, "r+b") as outfile:
outfile.seek(old_rec["byteOffset"])
outfile.write(data)
self.name_to_record[name] = (idx, rec)
self.updated_shards.add(idx)
def commit(self):
"""Commit a record"""
if self.pending_nbytes != 0:
self._commit_internal(self.curr_data, self.curr_records)
self.curr_data = bytearray()
self.curr_records = []
def finish(self):
"""Finish building and return shard records."""
self.commit()
for idx in self.updated_shards:
full_path = os.path.join(self.cache_dir, self.shard_records[idx]["dataPath"])
self.shard_records[idx]["md5sum"] = _calculate_md5(full_path)
return self.shard_records
def _commit_internal(self, data, records):
data_path = f"{self.prefix}_{self.counter}.bin"
full_path = os.path.join(self.cache_dir, data_path)
self.counter += 1
with open(full_path, "wb") as outfile:
outfile.write(data)
shard_record = {
"dataPath": data_path,
"format": "raw-shard",
"nbytes": len(data),
"records": records,
"md5sum": _calculate_md5(full_path),
}
self.shard_records.append(shard_record)
@property
def pending_nbytes(self):
"""Return total bytes stored so far"""
return len(self.curr_data)
def dump_tensor_cache(
params: Mapping[str, np.ndarray | tvm.runtime.Tensor]
| Iterator[tuple[str, np.ndarray | tvm.runtime.Tensor]],
cache_dir: str,
encode_format="f32-to-bf16",
meta_data=None,
shard_cap_mb=32,
show_progress: bool = True,
update_if_exists: bool = False,
):
"""Dump parameters to Tensor cache.
Parameters
----------
params: Union[
Mapping[str, Union[np.ndarray, tvm.runtime.Tensor]],
Iterator[Tuple[str, Union[np.ndarray, tvm.runtime.Tensor]]],
]
The parameter dictionary or generator
cache_dir: str
The path to the cache
encode_format: {"f32-to-bf16", "raw"}
Encoding format.
meta_data: json-compatible-struct or Callable[[], Any]
Extra meta_data to be stored in the cache json file,
or a callable that returns the metadata.
shard_cap_mb: int
Maxinum number of MB to be kept per shard
show_progress: bool
A boolean indicating if to show the dump progress.
update_if_exists: bool
If the cache already exists, update the cache. When set to False, it will overwrite the
existing files.
"""
if encode_format not in ("raw", "f32-to-bf16"):
raise ValueError(f"Invalie encode_format {encode_format}")
records = []
from_generator = isinstance(params, GeneratorType)
total_bytes = 0
counter = 0
max_out_length = 0
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
f32_to_bf16_triggered = False
print(f"Start storing to cache {cache_dir}")
shard_cap_nbytes = shard_cap_mb * (1 << 20)
nd_cache_json = os.path.join(cache_dir, "tensor-cache.json")
if update_if_exists and os.path.exists(nd_cache_json):
with open(nd_cache_json) as infile:
old_data = json.load(infile)
if meta_data is None:
meta_data = old_data["metadata"]
records = old_data["records"]
shard_manager = TensorCacheShardingManager(
cache_dir, "params_shard", shard_cap_nbytes, initial_shard_records=records
)
param_generator = params.items() if not from_generator else params
for k, origin_v in param_generator:
shape = list(origin_v.shape)
v = origin_v
if not isinstance(v, np.ndarray):
v = v.numpy()
# prefer to preserve original dtype, especially if the format was bfloat16
dtype = origin_v.dtype if isinstance(origin_v, tvm.runtime.Tensor) else v.dtype
if dtype in DataType._NUMPY_DTYPE_TO_STR:
dtype = DataType._NUMPY_DTYPE_TO_STR[dtype]
else:
dtype = str(dtype)
total_bytes += math.prod(v.shape) * np.dtype(v.dtype).itemsize
# convert fp32 to bf16
if encode_format == "f32-to-bf16" and dtype == "float32":
data = _convert_f32_to_bf16(v).tobytes()
f32_to_bf16_triggered = True
else:
data = v.tobytes()
shard_manager.append_or_update(
data,
name=k,
shape=shape,
dtype=dtype,
encode_format=encode_format,
allow_update=update_if_exists,
)
counter += 1
if show_progress:
last_cmd = f"[{counter:04d}] saving {k}"
flush = "\r" + (" " * max_out_length) + "\r"
max_out_length = max(len(last_cmd), max_out_length)
sys.stdout.write(flush + last_cmd)
records = shard_manager.finish()
meta_data = {} if meta_data is None else meta_data if not callable(meta_data) else meta_data()
with open(nd_cache_json, "w") as outfile:
json.dump({"metadata": meta_data, "records": records}, outfile, indent=4)
print(
f"\nAll finished, {shard_manager.counter} total shards committed, record saved to {nd_cache_json}"
)
if f32_to_bf16_triggered:
for shard in records:
for item in shard["records"]:
if item["dtype"] == "float32":
item["format"] = "raw"
item["dtype"] = "bfloat16"
b16_nd_cache_json = os.path.join(cache_dir, "tensor-cache-b16.json")
# also dump a file that contains bf16
with open(b16_nd_cache_json, "w") as outfile:
json.dump({"metadata": meta_data, "records": records}, outfile, indent=4)
print(f"Also saved a bf16 record to {b16_nd_cache_json}")
def load_tensor_cache(cachepath: str, device: tvm.runtime.Device):
"""Load the tensor cache from the directory or json.
Parameters
----------
cachepath: str
Path to the location or json file.
device: tvm.runtime.Device
The device we would like to load the data from.
"""
if not cachepath.endswith(".json"):
cachepath = os.path.join(cachepath, "tensor-cache.json")
cachedir = os.path.dirname(cachepath)
json_info = json.loads(open(cachepath).read())
result_dict = {}
for shard_rec in json_info["records"]:
data_path = shard_rec["dataPath"]
full_data_path = os.path.join(cachedir, data_path)
raw_data = open(full_data_path, "rb").read()
assert shard_rec["format"] == "raw-shard"
assert shard_rec["nbytes"] == len(raw_data)
for rec in shard_rec["records"]:
name = rec["name"]
shape = rec["shape"]
dtype = rec["dtype"]
encode_format = rec["format"]
offset = rec["byteOffset"]
nbytes = rec["nbytes"]
arr = tvm.runtime.empty(shape, dtype, device=device)
assert offset + nbytes <= len(raw_data)
buffer_source = raw_data[offset : offset + nbytes]
if dtype == "float8_e4m3fn":
if ml_dtypes is not None:
dtype = ml_dtypes.float8_e4m3fn
else:
raise RuntimeError(
"ml_dtypes is not installed, cannot convert float8_e4m3fn array to numpy."
)
if dtype == "float8_e5m2":
if ml_dtypes is not None:
dtype = ml_dtypes.float8_e5m2
else:
raise RuntimeError(
"ml_dtypes is not installed, cannot convert float8_e5m2 array to numpy."
)
if encode_format == "f32-to-bf16" and dtype == "float32":
data = np.frombuffer(buffer_source, dtype="uint16").reshape(shape)
arr.copyfrom(_convert_bf16_to_f32(data))
elif dtype == "bfloat16":
data = np.frombuffer(buffer_source, dtype="uint16").reshape(shape)
arr.copyfrom(data)
else:
data = np.frombuffer(buffer_source, dtype=dtype).reshape(shape)
arr.copyfrom(data)
result_dict[name] = arr
return result_dict, json_info["metadata"]
def export_runtime(runtime_dir):
"""Export TVMJS runtime to the runtime_dir
Parameters
----------
runtime_dir: str
The runtime directory
"""
web_hint = (
"make sure you setup tvm web runtime correctly."
+ " obtain a copy of TVM source code, set TVM_HOME env variable:\n"
+ " cd /path/to/tvm/web; make; npm run bundle"
)
jsbundle = find_wasm_lib("tvmjs.bundle.js", optional=True)
if not jsbundle:
raise RuntimeError("Cannot find tvmjs.bundle.js, " + web_hint)
wasi = find_wasm_lib("tvmjs_runtime.wasi.js", optional=True)
if not wasi:
raise RuntimeError("Cannot find tvmjs_runtime.wasi.js, " + web_hint)
print(f"Copy {jsbundle[0]} to {runtime_dir}")
shutil.copy(jsbundle[0], runtime_dir)
print(f"Copy {wasi[0]} to {runtime_dir}")
shutil.copy(wasi[0], runtime_dir)