/*************************************************************************************************** * Copyright (c) 2023 - 2025 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. * **************************************************************************************************/ #pragma once #include "cutlass/relatively_equal.h" #include "cutlass_unit_test.h" #include "cutlass/util/reference/host/tensor_compare.h" #include #include #include #include using namespace cute; template struct fp64_tester { using value_type = double; }; template struct fp64_tester> { using value_type = complex; }; template // logical shape (M, N) auto host_generate_gemm_inputs( ALayout a_layout, BLayout b_layout, CLayout c_layout ) { thrust::host_vector h_a(cosize(a_layout)); thrust::host_vector h_b(cosize(b_layout)); thrust::host_vector h_c(cosize(c_layout)); thrust::host_vector h_c_out(cosize(c_layout)); auto h_a_tensor = make_tensor(h_a.data(), a_layout); auto h_b_tensor = make_tensor(h_b.data(), b_layout); auto h_c_tensor = make_tensor(h_c.data(), c_layout); size_t max_size = std::max({static_cast(size(a_layout)), static_cast(size(b_layout)), static_cast(size(c_layout))}); for (size_t i = 0; i < max_size; ++i) { double di = static_cast(i); if(i < size(a_layout)) { h_a_tensor(i) = static_cast(di / size(a_layout)); } if(i < size(b_layout)) { h_b_tensor(i) = static_cast(di / size(a_layout)); } if(i < size(c_layout)) { h_c_tensor(i) = static_cast((di*di) / size(a_layout)); } } return std::make_tuple(h_a, h_b, h_c, h_c_out); } template thrust::host_vector host_reference_gemm(Alpha alpha, Tensor const& h_a_tensor, Tensor const& h_b_tensor, Beta beta, Tensor const& h_c_tensor, ALoadTransform const& a_load_transform = {}, BLoadTransform const& b_load_transform = {}, CLoadTransform const& c_load_transform = {}, CStoreTransform const& c_store_transform = {}) { // Cannot use ::value_type because it propagates to complex::value_type, // so ViewEngine>::value_type == double using TA = remove_cv_t; using TB = remove_cv_t; using TC = remove_cv_t; using tester = fp64_tester; using ABC_64 = typename tester::value_type; static_assert(std::is_same_v::value_type, typename fp64_tester::value_type>); static_assert(std::is_same_v::value_type, typename fp64_tester::value_type>); thrust::host_vector h_c_ref(cosize(h_c_tensor.layout()), static_cast(0.0)); auto h_c_ref_tensor = make_tensor(h_c_ref.data(), h_c_tensor.layout()); // A * B for (int k = 0; k < size<1>(h_a_tensor); k++) { for (int m = 0; m < size<0>(h_a_tensor); m++) { for (int n = 0; n < size<0>(h_b_tensor); n++) { const auto a_value = a_load_transform(h_a_tensor(m, k)); const auto b_value = b_load_transform(h_b_tensor(n, k)); const auto a_value_fp64 = static_cast(a_value); const auto b_value_fp64 = static_cast(b_value); h_c_ref_tensor(m, n) += static_cast(a_value_fp64 * b_value_fp64); } } } // C = A*B + C for (int i = 0; i < size(h_c_ref_tensor); i++) { const auto ab_value_fp64 = static_cast(h_c_ref_tensor(i)); const auto c_value_fp64 = static_cast(c_load_transform(h_c_tensor(i))); h_c_ref_tensor(i) = c_store_transform(static_cast(alpha * ab_value_fp64 + beta * c_value_fp64)); } return h_c_ref; } template void verify_gemm_correctness(cute::Tensor const& h_c_out_tensor, cute::Tensor const& h_c_ref_tensor) { // Cannot use ::value_type because it propagates to complex::value_type, // so ViewEngine>::value_type == double using TC = remove_cv_t; using tester = fp64_tester; using ABC_64 = typename tester::value_type; for (int i = 0; i < size(h_c_ref_tensor); i++) { ABC_64 h_c_ref_i = h_c_ref_tensor(i); ABC_64 h_c_out_i = h_c_out_tensor(i); double epsilon(0.1f); double nonzero_floor(std::numeric_limits::min()); bool passed = cutlass::relatively_equal(h_c_out_i, h_c_ref_i, epsilon, nonzero_floor); ASSERT_TRUE(passed) << i << " - result:" << h_c_out_i << " expected:" << h_c_ref_i; } } template __launch_bounds__(ThreadBlockSize) __global__ void cooperative_gemm_kernel(GMemALayout gmem_a_layout, GMemBLayout gmem_b_layout, GMemCLayout gmem_c_layout, SMemALayout smem_a_layout, SMemBLayout smem_b_layout, SMemCLayout smem_c_layout, TA const* a, TB const* b, TC const* c, TC * c_out, Alpha const alpha, Beta const beta, TiledMma tiled_mma, ALoadTransform a_load_transform, BLoadTransform b_load_transform, CLoadTransform c_load_transform, CStoreTransform c_store_transform, SMemCopyOpA a_copy_op, SMemCopyOpB b_copy_op, SMemCopyLdOpC c_copy_ld_op, SMemCopyStOpC c_copy_st_op) { using namespace cute; Tensor g_a_tensor = make_tensor(make_gmem_ptr(a), gmem_a_layout); Tensor g_b_tensor = make_tensor(make_gmem_ptr(b), gmem_b_layout); Tensor g_c_tensor = make_tensor(make_gmem_ptr(c), gmem_c_layout); Tensor g_c_out_tensor = make_tensor(make_gmem_ptr(c_out), gmem_c_layout); constexpr uint32_t copy_max_vec_bytes = CopyMaxVecBits / 8; extern __shared__ float4 smem_buf[]; auto* smem_ptr = reinterpret_cast(smem_buf); auto* smem_ptr_a = smem_ptr; auto* smem_ptr_b = smem_ptr_a + round_up((sizeof(TA) * cosize(smem_a_layout)), copy_max_vec_bytes); auto* smem_ptr_c = smem_ptr_b + round_up((sizeof(TB) * cosize(smem_b_layout)), copy_max_vec_bytes); Tensor s_a_tensor = make_tensor(make_smem_ptr(smem_ptr_a), smem_a_layout); Tensor s_b_tensor = make_tensor(make_smem_ptr(smem_ptr_b), smem_b_layout); Tensor s_c_tensor = make_tensor(make_smem_ptr(smem_ptr_c), smem_c_layout); cooperative_copy(threadIdx.x, g_a_tensor, s_a_tensor); cooperative_copy(threadIdx.x, g_b_tensor, s_b_tensor); cooperative_copy(threadIdx.x, g_c_tensor, s_c_tensor); cp_async_fence(); cp_async_wait<0>(); __syncthreads(); cooperative_gemm( threadIdx.x, tiled_mma, alpha, s_a_tensor, s_b_tensor, beta, s_c_tensor, a_load_transform, b_load_transform, c_load_transform, c_store_transform, a_copy_op, b_copy_op, c_copy_ld_op, c_copy_st_op ); __syncthreads(); cooperative_copy(threadIdx.x, s_c_tensor, g_c_out_tensor); } template __launch_bounds__(ThreadBlockSize) __global__ void cooperative_gemm_kernel_rmem_c(GMemALayout gmem_a_layout, GMemBLayout gmem_b_layout, GMemCLayout gmem_c_layout, SMemALayout smem_a_layout, SMemBLayout smem_b_layout, TA const* a, TB const* b, TC const* c, TC * c_out, TiledMma tiled_mma, ALoadTransform a_load_transform, BLoadTransform b_load_transform, CLoadTransform c_load_transform, CStoreTransform c_store_transform, SMemCopyOpA a_copy_op, SMemCopyOpB b_copy_op) { using namespace cute; Tensor g_a_tensor = make_tensor(make_gmem_ptr(a), gmem_a_layout); Tensor g_b_tensor = make_tensor(make_gmem_ptr(b), gmem_b_layout); Tensor g_c_tensor = make_tensor(make_gmem_ptr(c), gmem_c_layout); Tensor g_c_out_tensor = make_tensor(make_gmem_ptr(c_out), gmem_c_layout); constexpr uint32_t copy_max_vec_bytes = CopyMaxVecBits / 8; extern __shared__ float4 smem_buf[]; auto* smem_ptr = reinterpret_cast(smem_buf); auto* smem_ptr_a = smem_ptr; auto* smem_ptr_b = smem_ptr_a + round_up((sizeof(TA) * cosize(smem_a_layout)), copy_max_vec_bytes); Tensor s_a_tensor = make_tensor(make_smem_ptr(smem_ptr_a), smem_a_layout); Tensor s_b_tensor = make_tensor(make_smem_ptr(smem_ptr_b), smem_b_layout); cooperative_copy(threadIdx.x, g_a_tensor, s_a_tensor); cooperative_copy(threadIdx.x, g_b_tensor, s_b_tensor); cp_async_fence(); cp_async_wait<0>(); __syncthreads(); // Create C fragment for storing intermediate results auto thr_mma = TiledMma().get_thread_slice(threadIdx.x); Tensor g_c_partition = thr_mma.partition_C(g_c_tensor); Tensor g_c_out_partition = thr_mma.partition_C(g_c_out_tensor); Tensor r_c_partition = thr_mma.make_fragment_C(g_c_partition); // Create indexing help for predicated GEMMs Tensor cC = make_identity_tensor(shape(gmem_c_layout)); Tensor tCcC = thr_mma.partition_C(cC); // Load C from global // (always loading in predicated way) CUTE_UNROLL for (int i = 0; i < size(r_c_partition); ++i) { if (elem_less(tCcC(i), shape(g_c_tensor))) { r_c_partition(i) = c_load_transform(g_c_partition(i)); } } cooperative_gemm( threadIdx.x, tiled_mma, s_a_tensor, s_b_tensor, r_c_partition, a_load_transform, b_load_transform, a_copy_op, b_copy_op ); __syncthreads(); // Store C to global // (always storing in predicated way) CUTE_UNROLL for (int i = 0; i < size(r_c_partition); ++i) { if (elem_less(tCcC(i), shape(g_c_tensor))) { g_c_out_partition(i) = c_store_transform(r_c_partition(i)); } } } template, class BSMemCopyOp = AutoVectorizingCopyWithAssumedAlignment, class CSMemCopyLdOp = AutoVectorizingCopyWithAssumedAlignment, class CSMemCopyStOp = AutoVectorizingCopyWithAssumedAlignment> void test_cooperative_gemm(GMemALayout gmem_a_layout, GMemBLayout gmem_b_layout, GMemCLayout gmem_c_layout, SMemALayout smem_a_layout, SMemBLayout smem_b_layout, SMemCLayout smem_c_layout, TiledMma tiled_mma, ALoadTransform a_load_transform = {}, BLoadTransform b_load_transform = {}, CLoadTransform c_load_transform = {}, CStoreTransform c_store_transform = {}, ASMemCopyOp a_smem_copy_op = {}, BSMemCopyOp b_smem_copy_op = {}, CSMemCopyLdOp c_smem_copy_ld_op = {}, CSMemCopyStOp c_smem_copy_st_op = {}) { static_assert(std::is_same_v::value_type, typename fp64_tester::value_type>); static_assert(std::is_same_v::value_type, typename fp64_tester::value_type>); static_assert(size<0>(gmem_a_layout) == size<0>(gmem_c_layout)); // AM == CM static_assert(size<0>(gmem_b_layout) == size<1>(gmem_c_layout)); // BN == CN static_assert(size<1>(gmem_a_layout) == size<1>(gmem_b_layout)); // AK == BK static_assert(size<0>(smem_a_layout) == size<0>(smem_c_layout)); // AM == CM static_assert(size<0>(smem_b_layout) == size<1>(smem_c_layout)); // BN == CN static_assert(size<1>(smem_a_layout) == size<1>(smem_b_layout)); // AK == BK static_assert(cute::size(gmem_a_layout) == cute::size(smem_a_layout)); static_assert(cute::size(gmem_b_layout) == cute::size(smem_b_layout)); static_assert(cute::size(gmem_c_layout) == cute::size(smem_c_layout)); #if 0 print(" "); print("gmem: "); print(gmem_layout); print("\n"); print(" "); print("smem: "); print(smem_layout); print("\n"); print(" "); print("threads: "); print(ThreadBlockSize); print("\n"); #endif const auto alpha = static_cast(1.1); const auto beta = static_cast(1.2); // Generate inputs auto [h_a, h_b, h_c, h_c_out] = host_generate_gemm_inputs(gmem_a_layout, gmem_b_layout, gmem_c_layout); thrust::device_vector d_a(h_a); thrust::device_vector d_b(h_b); thrust::device_vector d_c(h_c); thrust::device_vector d_c_out(h_c_out.size(), TC(float(-1))); constexpr uint32_t copy_max_vec_bytes = CopyMaxVecBits / 8; const size_t shared_memory_size = round_up(sizeof(TA) * h_a.size(), copy_max_vec_bytes) + round_up(sizeof(TB) * h_b.size(), copy_max_vec_bytes) + sizeof(TC) * h_c.size(); auto kernel = cooperative_gemm_kernel< ThreadBlockSize, CopyMaxVecBits, GMemALayout, GMemBLayout, GMemCLayout, SMemALayout, SMemBLayout, SMemCLayout, TA, TB, TC, decltype(alpha), decltype(beta), TiledMma, ALoadTransform, BLoadTransform, CLoadTransform, CStoreTransform, ASMemCopyOp, BSMemCopyOp, CSMemCopyLdOp, CSMemCopyStOp >; ASSERT_EQ(cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(shared_memory_size)), 0); kernel<<<1, ThreadBlockSize, shared_memory_size>>>( gmem_a_layout, gmem_b_layout, gmem_c_layout, smem_a_layout, smem_b_layout, smem_c_layout, thrust::raw_pointer_cast(d_a.data()), thrust::raw_pointer_cast(d_b.data()), thrust::raw_pointer_cast(d_c.data()), thrust::raw_pointer_cast(d_c_out.data()), alpha, beta, tiled_mma, a_load_transform, b_load_transform, c_load_transform, c_store_transform, a_smem_copy_op, b_smem_copy_op, c_smem_copy_ld_op, c_smem_copy_st_op ); cudaError_t result = cudaDeviceSynchronize(); if (result != cudaSuccess) { cudaError_t error = cudaGetLastError(); FAIL() << "Error at kernel sync: " << cudaGetErrorString(error) << "\n"; } // Reference gemm auto h_c_ref = host_reference_gemm(alpha, make_tensor(h_a.data(), gmem_a_layout), make_tensor(h_b.data(), gmem_b_layout), beta, make_tensor(h_c.data(), gmem_c_layout), a_load_transform, b_load_transform, c_load_transform, c_store_transform); // Copy result data h_c_out = d_c_out; // Verify correctness verify_gemm_correctness(make_tensor(h_c_out.data(), gmem_c_layout), make_tensor(h_c_ref.data(), gmem_c_layout)); } template, class BSMemCopyOp = AutoVectorizingCopyWithAssumedAlignment> void test_cooperative_gemm_rmem_c(GMemALayout gmem_a_layout, GMemBLayout gmem_b_layout, GMemCLayout gmem_c_layout, SMemALayout smem_a_layout, SMemBLayout smem_b_layout, TiledMma tiled_mma, ALoadTransform a_load_transform = {}, BLoadTransform b_load_transform = {}, CLoadTransform c_load_transform = {}, CStoreTransform c_store_transform = {}, ASMemCopyOp a_smem_copy_op = {}, BSMemCopyOp b_smem_copy_op = {}) { static_assert(size<0>(gmem_a_layout) == size<0>(gmem_c_layout)); // AM == CM static_assert(size<0>(gmem_b_layout) == size<1>(gmem_c_layout)); // BN == CN static_assert(size<1>(gmem_a_layout) == size<1>(gmem_b_layout)); // AK == BK static_assert(size<1>(smem_a_layout) == size<1>(smem_b_layout)); // AK == BK static_assert(cute::size(gmem_a_layout) == cute::size(smem_a_layout)); static_assert(cute::size(gmem_b_layout) == cute::size(smem_b_layout)); #if 0 print(" "); print("gmem: "); print(gmem_layout); print("\n"); print(" "); print("smem: "); print(smem_layout); print("\n"); print(" "); print("threads: "); print(ThreadBlockSize); print("\n"); #endif const auto alpha = static_cast(1.0); const auto beta = static_cast(1.0); // Generate inputs auto [h_a, h_b, h_c, h_c_out] = host_generate_gemm_inputs(gmem_a_layout, gmem_b_layout, gmem_c_layout); thrust::device_vector d_a(h_a); thrust::device_vector d_b(h_b); thrust::device_vector d_c(h_c); thrust::device_vector d_c_out(h_c_out.size(), static_cast(-1)); constexpr uint32_t copy_max_vec_bytes = CopyMaxVecBits / 8; const size_t shared_memory_size = round_up(sizeof(TA) * h_a.size(), copy_max_vec_bytes) + round_up(sizeof(TB) * h_b.size(), copy_max_vec_bytes); auto kernel = cooperative_gemm_kernel_rmem_c< ThreadBlockSize, CopyMaxVecBits, GMemALayout, GMemBLayout, GMemCLayout, SMemALayout, SMemBLayout, TA, TB, TC, TiledMma, ALoadTransform, BLoadTransform, CLoadTransform, CStoreTransform, ASMemCopyOp, BSMemCopyOp >; ASSERT_EQ(cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(shared_memory_size)), 0); kernel<<<1, ThreadBlockSize, shared_memory_size>>>( gmem_a_layout, gmem_b_layout, gmem_c_layout, smem_a_layout, smem_b_layout, thrust::raw_pointer_cast(d_a.data()), thrust::raw_pointer_cast(d_b.data()), thrust::raw_pointer_cast(d_c.data()), thrust::raw_pointer_cast(d_c_out.data()), tiled_mma, a_load_transform, b_load_transform, c_load_transform, c_store_transform, a_smem_copy_op, b_smem_copy_op ); cudaError_t result = cudaDeviceSynchronize(); if (result != cudaSuccess) { cudaError_t error = cudaGetLastError(); FAIL() << "Error at kernel sync: " << cudaGetErrorString(error) << "\n"; } // Copy result data h_c_out = d_c_out; // Reference gemm auto h_c_ref = host_reference_gemm(alpha, make_tensor(h_a.data(), gmem_a_layout), make_tensor(h_b.data(), gmem_b_layout), beta, make_tensor(h_c.data(), gmem_c_layout), a_load_transform, b_load_transform, c_load_transform, c_store_transform); // Verify correctness verify_gemm_correctness(make_tensor(h_c_out.data(), gmem_c_layout), make_tensor(h_c_ref.data(), gmem_c_layout)); } template void test_cooperative_gemm_col_major_layout(ShapeMNK shape_mnk, TiledMma tiled_mma, Ops ... ops) { auto a_layout = make_layout(select<0, 2>(shape_mnk)); auto b_layout = make_layout(select<1, 2>(shape_mnk), GenRowMajor{}); auto c_layout = make_layout(select<0, 1>(shape_mnk)); test_cooperative_gemm (a_layout, b_layout, c_layout, a_layout, b_layout, c_layout, tiled_mma, ops...); } template std::enable_if_t, cute::is_layout, cute::is_layout>> test_cooperative_gemm_col_major_layout(SMemAtomLayoutA smem_atom_layout_a, SMemAtomLayoutB smem_atom_layout_b, SMemAtomLayoutC smem_atom_layout_c, ShapeMNK shape_mnk, TiledMma tiled_mma, Ops&& ... ops) { auto gmem_a_layout = make_layout(select<0, 2>(shape_mnk)); auto gmem_b_layout = make_layout(select<1, 2>(shape_mnk), GenRowMajor{}); auto gmem_c_layout = make_layout(select<0, 1>(shape_mnk)); auto smem_a_layout = tile_to_shape( smem_atom_layout_a, make_shape(shape<0>(gmem_a_layout), shape<1>(gmem_a_layout))); auto smem_b_layout = tile_to_shape( smem_atom_layout_b, make_shape(shape<0>(gmem_b_layout), shape<1>(gmem_b_layout))); auto smem_c_layout = tile_to_shape( smem_atom_layout_c, make_shape(shape<0>(gmem_c_layout), shape<1>(gmem_c_layout))); test_cooperative_gemm (gmem_a_layout, gmem_b_layout, gmem_c_layout, smem_a_layout, smem_b_layout, smem_c_layout, tiled_mma, ops...); } template void test_cooperative_gemm_col_major_layout_rmem_c(ShapeMNK shape_mnk, TiledMma tiled_mma, Ops ... ops) { auto a_layout = make_layout(select<0, 2>(shape_mnk)); auto b_layout = make_layout(select<1, 2>(shape_mnk), GenRowMajor{}); auto c_layout = make_layout(select<0, 1>(shape_mnk)); test_cooperative_gemm_rmem_c (a_layout, b_layout, c_layout, a_layout, b_layout, tiled_mma, ops...); } template std::enable_if_t, cute::is_layout>> test_cooperative_gemm_col_major_layout_rmem_c(SMemAtomLayoutA smem_atom_layout_a, SMemAtomLayoutB smem_atom_layout_b, ShapeMNK shape_mnk, TiledMma tiled_mma, Ops ... ops) { auto gmem_a_layout = make_layout(select<0, 2>(shape_mnk)); auto gmem_b_layout = make_layout(select<1, 2>(shape_mnk), GenRowMajor{}); auto gmem_c_layout = make_layout(select<0, 1>(shape_mnk)); auto smem_a_layout = tile_to_shape( smem_atom_layout_a, make_shape(shape<0>(gmem_a_layout), shape<1>(gmem_a_layout))); auto smem_b_layout = tile_to_shape( smem_atom_layout_b, make_shape(shape<0>(gmem_b_layout), shape<1>(gmem_b_layout))); test_cooperative_gemm_rmem_c (gmem_a_layout, gmem_b_layout, gmem_c_layout, smem_a_layout, smem_b_layout, tiled_mma, ops...); } template void test_cooperative_gemm_col_major_layout_rmem_c(Args&& ... args) { test_cooperative_gemm_col_major_layout_rmem_c, T, T, T> (static_cast(args)...); } template void test_cooperative_gemm_col_major_layout(Args&& ... args) { test_cooperative_gemm_col_major_layout, T, T, T> (static_cast(args)...); }