/*************************************************************************************************** * Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * **************************************************************************************************/ /*! \file \brief Example of a GETT targeting Hopper tensor cores using the CUTLASS 3.x API. CUTLASS has long provided implementations of Generalized Matrix times Matrix (GEMM) kernels. However, a plethora of workloads compute on higher ranked tensors. Products of such tensors, called tensor contractions, can be executed as multiple batched GEMMs, however, they can be further accelerated with kernels that natively operate on these higher ranked tensors to perform Generalized Tensor times Tensor contractions (GETT). CuTe's hierarchical layouts and CUTLASS 3.0's unified micro-kernels make implementation of GETTs trivial. In this example, we show how CUTLASS 3.0, CuTe, and Hopper's TMA feature together can accelerate GETTs while making the process of authoring custom GETT kernels easier than ever before. The modes of a tensor that participate in a GETT can be fundamentally grouped into four semantic categories. The contraction modes (or K-modes) only appear in the A and B (left and right) inputs but not in the C output tensor. Row modes (or M-modes) only appear in the left input tensor (A) and the output tensor (C). Column modes (or N-modes) only appear in the right (B) input tensor and the output tensor (C). Batch modes (or L-modes) appear in all input and output tensors. If we fold the many modes of a tensor contraction into these four categories, it would allow us to represent the input and output tensors as rank-3 "matrices" that can be computed upon as if we were computing a batched GEMM! This is exactly what CuTe's hierarchical layout representation allows us to do! Instead of having simple integers as strides for these four modes, we can have nested strides for each of these semantic categories that themselves have multiple modes within them -- multi-mode strides! In CUTLASS 3.0, all one has to do to take advantage of this capability is to substitute the required multi-mode strides instead of the default ones provided by gemm::detail::TagToStrideX. In the following example, we illustrate how every Hopper GEMM in CUTLASS 3.0 is a GETT in disguise. We begin by defining the four modes detailed above as Row, Col (column), Red (reduction), and Bat (batch) strides, which we then nest for each of the in/out tensors to create our rank-3 stride tuples. Note that although we do not define the problem shape type explicitely, it too remains a rank-4 shape tuple just like any other batched GEMM, but instead with multi-mode shapes for each of the four corresponding multi-modes within it. After this, the same CollectiveMma and CollectiveBuilder we describe in examples 50 and 49 are used to create our kernel type. Nothing else changes from a user's point of view. Note that multi-mode strides do not affect our specializations in any way -- the lexical spelling of our kernels remains the same. The only difference between a CUTLASS 3 batched GEMM and GETT are the instaced CuTe Layouts. CollectiveBuilders rely on detecting the static-1 in the stride tuples to determine the major mode, which is what the example demonstrates. However, it is possible to have all modes be dynamic as well if the user assembles a CollectiveMma manually and ensures that the runtime strides are compatible with the static micro-kernel of the collective (TiledMma, TiledCopy, and smem layouts). On the other hand, a user can have more than one static stride too (which need not correspond to the major mode). In particular, this example demonstrates a GETT where the 0th M-mode (M0) in A and the 0th K-mode (K0) in B are major. All other combinations of major modes are supported, with the exception of mixed K-major scenarios where both A and B are K-major (e.g. K0 is major in A but K1 is major in B). NVIDIA Hopper architecture's TMA feature makes the predictaion required to implement these complicated kernels trivial, as it is all handled by TMA itself without requiring any programmer effort. Example executions, where the stride order defines the major-order (major on the left): 51_hopper_gett --modeC=m,n,l --modeA=m,k,l --modeB=k,n,l --extents=m:4096,n:4096,k:4096 51_hopper_gett --modeC=l,m,n --modeA=m,l,k --modeB=k,n,l --extents=m:128,n:128,k:128,l:64 51_hopper_gett --modeC=m,a,b,p,q,n,l --modeA=m,l,b,k,a --modeB=k,n,p,q,l --extents=m:32,a:32,b:3,n:128,k:128,l:4,p:3,q:3 */ #include "gett_kernel.cuh" #include "thrust/host_vector.h" #include "thrust/device_vector.h" #include "cutlass/cutlass.h" #include "cutlass/gemm/device/gemm_universal_adapter.h" #include "cutlass/gemm/kernel/gemm_universal.hpp" #include "cutlass/gemm/collective/collective_builder.hpp" #include "cutlass/epilogue/collective/default_epilogue.hpp" #include "cutlass/util/gett_commandline.hpp" #include "cutlass/util/reference/device/gett.hpp" #include "cutlass/util/reference/device/tensor_compare.h" #include "cutlass/util/print_error.hpp" namespace example { // Returns true if the left-most value in the tuple is statically known to be 1 template constexpr bool is_left_major() { // Account for stride types with and without batch mode and batch modes with static zero stride return cute::is_constant<1, decltype(cute::size<0,0>(Stride{}))>::value; } // Same as cute::make_int_tuple but inserts a major stride (Int<1>) for the leftmost mode if required template static constexpr auto make_stride_tuple(Indexable const& t, int n, int64_t init_default = 0) { static_assert(Rank > 1); if constexpr (IsMajor) { return cute::transform(cute::make_seq{}, [&](auto i) { if constexpr (i == 0) { return cute::Int<1>{}; } else { return i < n ? t[i] : init_default; } }); } else { return cute::make_int_tuple(t, n, init_default); } } } // namespace example ////////////////////////////////////////////////////////////////////////////// int main(int argc, char const* argv[]) { #if defined(CUTLASS_ARCH_MMA_SM90_SUPPORTED) using namespace cute; if (argc != 5) { std::cout << "Number of command line args must be 4.\n"; cutlass::GettCommandLine::print_usage(); return 0; } // // Define the stride types for A, B, C, and D // // Stride for A (left input). If reduction mode is major, same must be major in B // For this example, M0 is major in A. using RowModeStridesA = cute::Stride, int64_t, int64_t, int64_t>; using RedModeStridesA = cute::Stride; using BatModeStridesA = cute::Stride; // Stride for B (right input). If reduction mode is major, same must be major in A // For this example, K0 is major in B. using ColModeStridesB = cute::Stride; using RedModeStridesB = cute::Stride, int64_t, int64_t>; using BatModeStridesB = cute::Stride; // Strides for output, which can all be dynamic. using RowModeStridesC = cute::Stride; using ColModeStridesC = cute::Stride; using BatModeStridesC = cute::Stride; // Assmble our rank-3 multi-mode strides for the in/out tensors using StrideA = cute::Stride; using StrideB = cute::Stride; using StrideC = cute::Stride; // Note: C and D share strides here for simplicity. // In general, they need not have the same layout. using StrideD = StrideC; // // Define element types for tensors and intermediate values // using ElementA = cutlass::half_t; using ElementB = cutlass::half_t; using ElementC = cutlass::half_t; using ElementD = float; using ElementAccumulator = float; using ElementEpilogue = float; // The following constexpr values set the max number of modes in each MNKL mode constexpr int MaxRank_M = cute::rank(RowModeStridesA{}); // Max row modes constexpr int MaxRank_N = cute::rank(ColModeStridesB{}); // Max column modes constexpr int MaxRank_K = cute::rank(RedModeStridesA{}); // Max contraction modes constexpr int MaxRank_L = cute::rank(BatModeStridesA{}); // Max batch modes static_assert(cute::rank(RowModeStridesA{}) == cute::rank(RowModeStridesC{})); static_assert(cute::rank(ColModeStridesB{}) == cute::rank(RowModeStridesC{})); static_assert(cute::rank(RedModeStridesA{}) == cute::rank(RedModeStridesB{})); static_assert(cute::rank(BatModeStridesA{}) == cute::rank(BatModeStridesC{})); static_assert(cute::rank(BatModeStridesB{}) == cute::rank(BatModeStridesC{})); // Parse command line to get modes, extents, and strides cutlass::GettCommandLine cmd; auto parsed_args = cmd.parse(argc, argv, true); auto& m = parsed_args.M; auto& ldAm = parsed_args.ldAm; auto& ldCm = parsed_args.ldCm; int rank_m = int(m.size()); auto& n = parsed_args.N; auto& ldBn = parsed_args.ldBn; auto& ldCn = parsed_args.ldCn; int rank_n = int(n.size()); auto& k = parsed_args.K; auto& ldAk = parsed_args.ldAk; auto& ldBk = parsed_args.ldBk; int rank_k = int(k.size()); auto& l = parsed_args.L; auto& ldAl = parsed_args.ldAl; auto& ldBl = parsed_args.ldBl; auto& ldCl = parsed_args.ldCl; int rank_l = int(l.size()); if ((rank_m > MaxRank_M) || (rank_n > MaxRank_N) || (rank_k > MaxRank_K) || (rank_l > MaxRank_L)) { std::cerr << "ERROR: Input has more modes than statically configured."; return 1; } // Check that the user input major stride match the static major strides. if (example::is_left_major() && (ldAm[0] != 1)) { std::cerr << "ERROR: A_M0 is expected to be major, but was not in the provided input!\n"; return 1; } if (example::is_left_major() && (ldAk[0] != 1)) { std::cerr << "ERROR: A_K0 is expected to be major, but was not in the provided input!\n"; return 1; } if (example::is_left_major() && (ldBn[0] != 1)) { std::cerr << "ERROR: B_N0 is expected to be major, but was not in the provided input!\n"; return 1; } if (example::is_left_major() && (ldBk[0] != 1)) { std::cerr << "ERROR: B_K0 is expected to be major, but was not in the provided input!\n"; return 1; } // Convert to `cute::Tuple`s and set up arguments auto M = make_int_tuple(m.data(), rank_m, 1); auto dAm = example::make_stride_tuple()>(ldAm.data(), rank_m); auto dCm = example::make_stride_tuple()>(ldCm.data(), rank_m); auto N = make_int_tuple(n.data(), rank_n, 1); auto dBn = example::make_stride_tuple()>(ldBn.data(), rank_n); auto dCn = example::make_stride_tuple()>(ldCn.data(), rank_n); auto K = make_int_tuple(k.data(), rank_k, 1); auto dAk = example::make_stride_tuple()>(ldAk.data(), rank_k); auto dBk = example::make_stride_tuple()>(ldBk.data(), rank_k); auto L = make_int_tuple(l.data(), rank_l, 1); auto dAl = make_int_tuple(ldAl.data(), rank_l, 0); auto dBl = make_int_tuple(ldBl.data(), rank_l, 0); auto dCl = make_int_tuple(ldCl.data(), rank_l, 0); // Concat tuples to turn it into rank-4 problem shape and rank-3 strides, just like GEMM auto problem_shape = make_shape(M, N, K, L); StrideA stride_A = make_stride(dAm, dAk, dAl); StrideB stride_B = make_stride(dBn, dBk, dBl); StrideC stride_C = make_stride(dCm, dCn, dCl); StrideD stride_D = stride_C; auto alpha = ElementEpilogue(1.0f); auto beta = ElementEpilogue(1.0f); // // Allocate and init tensors // auto M_size = std::accumulate(std::begin(m), std::end(m), 1, std::multiplies<>{}); auto N_size = std::accumulate(std::begin(n), std::end(n), 1, std::multiplies<>{}); auto K_size = std::accumulate(std::begin(k), std::end(k), 1, std::multiplies<>{}); auto L_size = std::accumulate(std::begin(l), std::end(l), 1, std::multiplies<>{}); thrust::host_vector h_A(M_size * K_size * L_size); thrust::host_vector h_B(N_size * K_size * L_size); thrust::host_vector h_C(M_size * N_size * L_size); thrust::host_vector h_D(M_size * N_size * L_size); // Note: the cast to int here is to avoid false-negative ref-checks which can // occur due to floating point arithmetic not being purely associative. for (auto& a : h_A) a = ElementA(int(4*(rand() / double(RAND_MAX)) - 1)); for (auto& b : h_B) b = ElementB(int(4*(rand() / double(RAND_MAX)) - 1)); for (auto& c : h_C) c = ElementC(int(4*(rand() / double(RAND_MAX)) - 1)); for (auto& d : h_D) d = ElementD(-1); thrust::device_vector d_A = h_A; thrust::device_vector d_B = h_B; thrust::device_vector d_C = h_C; thrust::device_vector cutlass_result = h_D; thrust::device_vector reference_result = h_D; // // Compute GETT // auto status = example::gett_kernel( problem_shape, d_A.data().get(), stride_A, d_B.data().get(), stride_B, ElementAccumulator{}, d_C.data().get(), stride_C, cutlass_result.data().get(), stride_D, alpha, beta); if (cutlass::Status::kSuccess != status) { std::cerr << "ERROR: GETT operator launch failed.\n"; return 1; } auto cuda_err = cudaDeviceSynchronize(); if (cudaSuccess != cuda_err) { std::cerr << "ERROR: GETT operator execution failed. with error :"; std::cerr << cudaGetErrorString(cuda_err) << "\n"; return 1; } // // Verify // cutlass::reference::device::gett( problem_shape, d_A.data().get(), stride_A, d_B.data().get(), stride_B, ElementAccumulator{}, d_C.data().get(), stride_C, reference_result.data().get(), stride_D, alpha, beta); cuda_err = cudaDeviceSynchronize(); if (cudaSuccess != cuda_err) { std::cerr << "ERROR: GETT reference execution failed. with error :"; std::cerr << cudaGetErrorString(cuda_err) << "\n"; return 1; } // Check if output from CUTLASS kernel and reference kernel are equal or not bool passed = cutlass::reference::device::BlockCompareEqual( reference_result.data().get(), cutlass_result.data().get(), cutlass_result.size()); if (passed) { std::cout << "GETT verification passed.\n"; return 0; } else { std::cerr << "ERROR: GETT verification failed! Printing detailed stats.\n"; h_D = reference_result; thrust::host_vector h_cutlass_result = cutlass_result; print_relative_error(h_cutlass_result.size(), h_cutlass_result.data(), h_D.data()); std::cout << "StrideA: "; print(stride_A); std::cout << '\n'; std::cout << "StrideB: "; print(stride_B); std::cout << '\n'; std::cout << "StrideC: "; print(stride_C); std::cout << '\n'; std::cout << "StrideD: "; print(stride_D); std::cout << '\n'; return 1; } #else std::cerr << "Unsupported example. Please ensure CUTLASS_ARCH_MMA_SM90_SUPPORTED is defined.\n"; return 0; #endif // defined(CUTLASS_ARCH_MMA_SM90_SUPPORTED) }