/* * Copyright (c) Advanced Micro Devices, Inc., or its affiliates. * * SPDX-License-Identifier: MIT */ #include "warp_common.hh" #include "cooperative_groups_common.hh" #include "cg_common_kernels.hh" #include #include #include #include #include #include #include #include #include /** * @addtogroup thread_block_tile thread_block_tile * @{ * @ingroup DeviceLanguageTest * Contains unit tests for all thread_block_tile APIs and dynamic block partitioning */ namespace cg = cooperative_groups; template __global__ void thread_block_partition_size_getter(unsigned int* sizes) { const auto group = cg::this_thread_block(); if constexpr (dynamic) { sizes[thread_rank_in_grid()] = cg::tiled_partition(group, tile_size).size(); } else { cg::thread_block_tile tiled_partition = cg::tiled_partition(group); sizes[thread_rank_in_grid()] = tiled_partition.size(); } } template __global__ void thread_block_partition_thread_rank_getter(unsigned int* thread_ranks) { const auto group = cg::this_thread_block(); if constexpr (dynamic) { thread_ranks[thread_rank_in_grid()] = cg::tiled_partition(group, tile_size).thread_rank(); } else { cg::thread_block_tile tiled_partition = cg::tiled_partition(group); thread_ranks[thread_rank_in_grid()] = tiled_partition.thread_rank(); } } template void BlockPartitionGettersBasicTestImpl() { DYNAMIC_SECTION("Tile size: " << tile_size) { auto blocks = GenerateBlockDimensions(); auto threads = GenerateThreadDimensions(); INFO("Grid dimensions: x " << blocks.x << ", y " << blocks.y << ", z " << blocks.z); INFO("Block dimensions: x " << threads.x << ", y " << threads.y << ", z " << threads.z); CPUGrid grid(blocks, threads); const auto alloc_size = grid.thread_count_ * sizeof(unsigned int); LinearAllocGuard uint_arr_dev(LinearAllocs::hipMalloc, alloc_size); LinearAllocGuard uint_arr(LinearAllocs::hipHostMalloc, alloc_size); thread_block_partition_size_getter<<>>(uint_arr_dev.ptr()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(uint_arr.ptr(), uint_arr_dev.ptr(), alloc_size, hipMemcpyDeviceToHost)); HIP_CHECK(hipDeviceSynchronize()); thread_block_partition_thread_rank_getter <<>>(uint_arr_dev.ptr()); HIP_CHECK(hipGetLastError()); ArrayAllOf(uint_arr.ptr(), grid.thread_count_, [&grid](unsigned int i) { if constexpr (!dynamic) { return tile_size; } const auto partitions_in_block = (grid.threads_in_block_count_ + tile_size - 1) / tile_size; const auto rank_in_block = grid.thread_rank_in_block(i).value(); const auto tail = partitions_in_block * tile_size - grid.threads_in_block_count_; return tile_size - tail * (rank_in_block >= (partitions_in_block - 1) * tile_size); }); HIP_CHECK(hipMemcpy(uint_arr.ptr(), uint_arr_dev.ptr(), alloc_size, hipMemcpyDeviceToHost)); HIP_CHECK(hipDeviceSynchronize()); ArrayAllOf(uint_arr.ptr(), grid.thread_count_, [&grid](unsigned int i) { return grid.thread_rank_in_block(i).value() % tile_size; }); } } template void BlockPartitionGettersBasicTest() { static_cast((BlockPartitionGettersBasicTestImpl(), ...)); } /** * Test Description * ------------------------ * - Creates tiled partitions for each of the valid sizes{2, 4, 8, 16, 32, 64(if AMD)} and writes * the return values of size and thread_rank member functions to an output array that is validated * on the host side. * Test source * ------------------------ * - unit/cooperativeGrps/thread_block_tile.cc * Test requirements * ------------------------ * - HIP_VERSION >= 5.2 */ HIP_TEST_CASE(Unit_Thread_Block_Tile_Getters_Positive_Basic) { BlockPartitionGettersBasicTest(); } /** * Test Description * ------------------------ * - Creates tiled partitions for each of the valid sizes{2, 4, 8, 16, 32, 64(if AMD)} via the * dynamic tiled partition api and writes the return values of size and thread_rank member functions * to an output array that is validated on host. * Test source * ------------------------ * - unit/cooperativeGrps/thread_block_tile.cc * Test requirements * ------------------------ * - HIP_VERSION >= 5.2 */ HIP_TEST_CASE(Unit_Thread_Block_Tile_Dynamic_Getters_Positive_Basic) { BlockPartitionGettersBasicTest(); } template __global__ void block_tile_shfl_up(T* const out, const unsigned int delta) { const cg::thread_block_tile partition = cg::tiled_partition(cg::this_thread_block()); T var = static_cast(partition.thread_rank()); out[thread_rank_in_grid()] = partition.shfl_up(var, delta); } template void BlockTileShflUpTestImpl() { DYNAMIC_SECTION("Tile size: " << tile_size) { const auto inv_reduction_factor = 1.0 / GetTestReductionFactor(); auto blocks = GenerateBlockDimensionsForShuffle(); auto threads = GenerateThreadDimensionsForShuffle(); INFO("Grid dimensions: x " << blocks.x << ", y " << blocks.y << ", z " << blocks.z); INFO("Block dimensions: x " << threads.x << ", y " << threads.y << ", z " << threads.z); std::vector deltas; for (double i = 0; i < tile_size - 1; i += inv_reduction_factor) { deltas.emplace_back(static_cast(std::floor(i))); } deltas.emplace_back(tile_size - 1); const auto delta = GENERATE_COPY(from_range(deltas.begin(), deltas.end())); INFO("Delta: " << delta); CPUGrid grid(blocks, threads); const auto alloc_size = grid.thread_count_ * sizeof(T); LinearAllocGuard arr_dev(LinearAllocs::hipMalloc, alloc_size); LinearAllocGuard arr(LinearAllocs::hipHostMalloc, alloc_size); block_tile_shfl_up<<>>(arr_dev.ptr(), delta); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(arr.ptr(), arr_dev.ptr(), alloc_size, hipMemcpyDeviceToHost)); HIP_CHECK(hipDeviceSynchronize()); ArrayAllOf(arr.ptr(), grid.thread_count_, [delta, &grid](unsigned int i) -> std::optional { const int rank_in_partition = grid.thread_rank_in_block(i).value() % tile_size; const int target = rank_in_partition - delta; return target < 0 ? rank_in_partition : target; }); } } template void BlockTileShflUpTest() { static_cast((BlockTileShflUpTestImpl(), ...)); } /** * Test Description * ------------------------ * - Validates the shuffle up behavior of thread block tiles of all valid sizes{2, 4, 8, 16, 32, * 64(if AMD)} for delta values of [0, tile size). The test is run for all overloads of shfl_up. * Test source * ------------------------ * - unit/cooperativeGrps/thread_block_tile.cc * Test requirements * ------------------------ * - HIP_VERSION >= 5.2 */ HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Shfl_Up_Positive_Basic, int, unsigned int, long, unsigned long, long long, unsigned long long, float, double) { BlockTileShflUpTest(); } template __global__ void block_tile_shfl_down(T* const out, const unsigned int delta) { const cg::thread_block_tile partition = cg::tiled_partition(cg::this_thread_block()); T var = static_cast(partition.thread_rank()); out[thread_rank_in_grid()] = partition.shfl_down(var, delta); } template void BlockTileShflDownTestImpl() { DYNAMIC_SECTION("Tile size: " << tile_size) { const auto inv_reduction_factor = 1.0 / GetTestReductionFactor(); auto blocks = GenerateBlockDimensionsForShuffle(); auto threads = GenerateThreadDimensionsForShuffle(); INFO("Grid dimensions: x " << blocks.x << ", y " << blocks.y << ", z " << blocks.z); INFO("Block dimensions: x " << threads.x << ", y " << threads.y << ", z " << threads.z); std::vector deltas; for (double i = 0; i < tile_size - 1; i += inv_reduction_factor) { deltas.emplace_back(static_cast(std::floor(i))); } deltas.emplace_back(tile_size - 1); const auto delta = GENERATE_COPY(from_range(deltas.begin(), deltas.end())); INFO("Delta: " << delta); CPUGrid grid(blocks, threads); const auto alloc_size = grid.thread_count_ * sizeof(T); LinearAllocGuard arr_dev(LinearAllocs::hipMalloc, alloc_size); LinearAllocGuard arr(LinearAllocs::hipHostMalloc, alloc_size); block_tile_shfl_down<<>>(arr_dev.ptr(), delta); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(arr.ptr(), arr_dev.ptr(), alloc_size, hipMemcpyDeviceToHost)); HIP_CHECK(hipDeviceSynchronize()); ArrayAllOf(arr.ptr(), grid.thread_count_, [delta, &grid](unsigned int i) -> std::optional { const auto partitions_in_block = (grid.threads_in_block_count_ + tile_size - 1) / tile_size; const auto rank_in_block = grid.thread_rank_in_block(i).value(); const auto rank_in_group = rank_in_block % tile_size; const auto target = rank_in_group + delta; if (rank_in_block < (partitions_in_block - 1) * tile_size) { return target < tile_size ? target : rank_in_group; } else { // If the number of threads in a block is not an integer multiple of tile_size, the // final(tail end) tile will contain inactive threads. // Shuffling from an inactive thread returns an undefined value, accordingly threads that // shuffle from one must be skipped const auto tail = partitions_in_block * tile_size - grid.threads_in_block_count_; return target < tile_size - tail ? std::optional(target) : std::nullopt; } }); } } template void BlockTileShflDownTest() { static_cast((BlockTileShflDownTestImpl(), ...)); } /** * Test Description * ------------------------ * - Validates the shuffle down behavior of thread block tiles of all valid sizes{2, 16, * 32, 64(if AMD)} for delta values of [0, tile size). The test is run for all overloads of * shfl_down. * Test source * ------------------------ * - unit/cooperativeGrps/thread_block_tile.cc * Test requirements * ------------------------ * - HIP_VERSION >= 5.2 */ HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Shfl_Down_Positive_Basic, int, unsigned int, long, unsigned long, long long, unsigned long long, float, double) { BlockTileShflDownTest(); } template __global__ void block_tile_shfl_xor(T* const out, const unsigned mask) { const cg::thread_block_tile partition = cg::tiled_partition(cg::this_thread_block()); T var = static_cast(partition.thread_rank()); out[thread_rank_in_grid()] = partition.shfl_xor(var, mask); } template void BlockTileShflXORTestImpl() { DYNAMIC_SECTION("Tile size: " << tile_size) { const auto inv_reduction_factor = 1.0 / GetTestReductionFactor(); auto blocks = GenerateBlockDimensionsForShuffle(); auto threads = GenerateThreadDimensionsForShuffle(); INFO("Grid dimensions: x " << blocks.x << ", y " << blocks.y << ", z " << blocks.z); INFO("Block dimensions: x " << threads.x << ", y " << threads.y << ", z " << threads.z); std::vector masks; for (double i = 0; i < tile_size - 1; i += inv_reduction_factor) { masks.emplace_back(static_cast(std::floor(i))); } masks.emplace_back(tile_size - 1); const auto mask = GENERATE_COPY(from_range(masks.begin(), masks.end())); INFO("Mask: 0x" << std::hex << mask); CPUGrid grid(blocks, threads); const auto alloc_size = grid.thread_count_ * sizeof(T); LinearAllocGuard arr_dev(LinearAllocs::hipMalloc, alloc_size); LinearAllocGuard arr(LinearAllocs::hipHostMalloc, alloc_size); block_tile_shfl_xor<<>>(arr_dev.ptr(), mask); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(arr.ptr(), arr_dev.ptr(), alloc_size, hipMemcpyDeviceToHost)); HIP_CHECK(hipDeviceSynchronize()); const auto f = [mask, &grid](unsigned int i) -> std::optional { const auto partitions_in_block = (grid.threads_in_block_count_ + tile_size - 1) / tile_size; const auto rank_in_block = grid.thread_rank_in_block(i).value(); const int rank_in_partition = rank_in_block % tile_size; const auto target = rank_in_partition ^ mask; if (rank_in_block < (partitions_in_block - 1) * tile_size) { return target; } const auto tail = partitions_in_block * tile_size - grid.threads_in_block_count_; return target < tile_size - tail ? std::optional(target) : std::nullopt; }; ArrayAllOf(arr.ptr(), grid.thread_count_, f); } } template void BlockTileShflXORTest() { static_cast((BlockTileShflXORTestImpl(), ...)); } /** * Test Description * ------------------------ * - Validates the shuffle xor behavior of thread block tiles of all valid sizes{2, 16, 32, * 64(if AMD)} for mask values of [0, tile size). The test is run for all overloads of shfl_xor. * Test source * ------------------------ * - unit/cooperativeGrps/thread_block_tile.cc * Test requirements * ------------------------ * - HIP_VERSION >= 5.2 */ HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Shfl_XOR_Positive_Basic, int, unsigned int, long, unsigned long, long long, unsigned long long, float, double) { BlockTileShflXORTest(); } template __global__ void block_tile_shfl(T* const out, uint8_t* target_lanes) { const cg::thread_block_tile partition = cg::tiled_partition(cg::this_thread_block()); T var = static_cast(partition.thread_rank()); out[thread_rank_in_grid()] = partition.shfl(var, target_lanes[partition.thread_rank()]); } static inline std::mt19937& GetRandomGenerator() { static std::mt19937 mt(11); return mt; } template static inline T GenerateRandomInteger(const T min, const T max) { std::uniform_int_distribution dist(min, max); return dist(GetRandomGenerator()); } template void BlockTileShflTestImpl() { DYNAMIC_SECTION("Tile size: " << tile_size) { auto blocks = GenerateBlockDimensionsForShuffle(); auto threads = GenerateThreadDimensionsForShuffle(); INFO("Grid dimensions: x " << blocks.x << ", y " << blocks.y << ", z " << blocks.z); INFO("Block dimensions: x " << threads.x << ", y " << threads.y << ", z " << threads.z); CPUGrid grid(blocks, threads); const auto alloc_size = grid.thread_count_ * sizeof(T); LinearAllocGuard arr_dev(LinearAllocs::hipMalloc, alloc_size); LinearAllocGuard arr(LinearAllocs::hipHostMalloc, alloc_size); LinearAllocGuard target_lanes_dev(LinearAllocs::hipMalloc, tile_size * sizeof(uint8_t)); LinearAllocGuard target_lanes(LinearAllocs::hipHostMalloc, tile_size * sizeof(uint8_t)); std::generate(target_lanes.ptr(), target_lanes.ptr() + tile_size, [] { return GenerateRandomInteger(0, static_cast(2 * tile_size)); }); HIP_CHECK(hipMemcpy(target_lanes_dev.ptr(), target_lanes.ptr(), tile_size * sizeof(uint8_t), hipMemcpyHostToDevice)); block_tile_shfl<<>>(arr_dev.ptr(), target_lanes_dev.ptr()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(arr.ptr(), arr_dev.ptr(), alloc_size, hipMemcpyDeviceToHost)); HIP_CHECK(hipDeviceSynchronize()); const auto f = [&target_lanes, &grid](unsigned int i) -> std::optional { const auto partitions_in_block = (grid.threads_in_block_count_ + tile_size - 1) / tile_size; const auto rank_in_block = grid.thread_rank_in_block(i).value(); const int rank_in_partition = rank_in_block % tile_size; const auto target = target_lanes.ptr()[rank_in_partition] % tile_size; if (rank_in_block < (partitions_in_block - 1) * tile_size) { return target; } const auto tail = partitions_in_block * tile_size - grid.threads_in_block_count_; return target < tile_size - tail ? std::optional(target) : std::nullopt; }; ArrayAllOf(arr.ptr(), grid.thread_count_, f); } } template void BlockTileShflTest() { static_cast((BlockTileShflTestImpl(), ...)); } /** * Test Description * ------------------------ * - Validates the shuffle behavior of thread block tiles of all valid sizes{2, 16, 32, * 64(if AMD)} for generated shuffle target lanes. The test is run for all overloads of shfl. Test * source * ------------------------ * - unit/cooperativeGrps/thread_block_tile.cc * Test requirements * ------------------------ * - HIP_VERSION >= 5.2 */ HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Shfl_Positive_Basic, int, unsigned int, long, unsigned long, long long, unsigned long long, float, double) { BlockTileShflTest(); } template __global__ void block_tile_sync_check(T* global_data, unsigned int* wait_modifiers) { extern __shared__ uint8_t shared_data[]; T* const data = use_global ? global_data : reinterpret_cast(shared_data); const auto tid = cg::this_grid().thread_rank(); const auto block = cg::this_thread_block(); const cg::thread_block_tile partition = cg::tiled_partition(cg::this_thread_block()); const auto data_idx = [&block](unsigned int i) { return use_global ? i : (i % block.size()); }; const auto partitions_in_block = (block.size() + partition.size() - 1) / partition.size(); const auto partition_rank = block.thread_rank() / partition.size(); const auto tail = partitions_in_block * partition.size() - block.size(); const auto window_size = partition.size() - tail * (partition_rank == partitions_in_block - 1); const auto block_base_idx = tid / block.size() * block.size(); const auto tile_base_idx = block_base_idx + partition_rank * partition.size(); const auto wait_modifier = wait_modifiers[tid]; busy_wait(wait_modifier); data[data_idx(tid)] = partition.thread_rank(); partition.sync(); bool valid = true; for (auto i = 0; i < window_size; ++i) { const auto expected = (partition.thread_rank() + i) % window_size; if (!(valid &= (data[data_idx(tile_base_idx + expected)] == expected))) { break; } } partition.sync(); data[data_idx(tid)] = valid; if constexpr (!use_global) { global_data[tid] = data[data_idx(tid)]; } } template void BlockTileSyncTestImpl() { DYNAMIC_SECTION("Tile size: " << tile_size) { const auto randomized_run_count = GENERATE(range(0, cmd_options.cg_iterations)); INFO("Run number: " << randomized_run_count + 1); auto blocks = GenerateBlockDimensions(); auto threads = GenerateThreadDimensions(); INFO("Grid dimensions: x " << blocks.x << ", y " << blocks.y << ", z " << blocks.z); INFO("Block dimensions: x " << threads.x << ", y " << threads.y << ", z " << threads.z); CPUGrid grid(blocks, threads); const auto alloc_size = grid.thread_count_ * sizeof(T); const auto alloc_size_per_block = alloc_size / grid.block_count_; int max_shared_mem_per_block = 0; HIP_CHECK(hipDeviceGetAttribute(&max_shared_mem_per_block, hipDeviceAttributeMaxSharedMemoryPerBlock, 0)); if (!global_memory && (max_shared_mem_per_block < alloc_size_per_block)) { return; } LinearAllocGuard arr_dev(LinearAllocs::hipMalloc, alloc_size); LinearAllocGuard arr(LinearAllocs::hipHostMalloc, alloc_size); LinearAllocGuard wait_modifiers_dev(LinearAllocs::hipMalloc, grid.thread_count_ * sizeof(unsigned int)); LinearAllocGuard wait_modifiers(LinearAllocs::hipHostMalloc, grid.thread_count_ * sizeof(unsigned int)); if (randomized_run_count != 0) { std::generate(wait_modifiers.ptr(), wait_modifiers.ptr() + grid.thread_count_, [] { return GenerateRandomInteger(0u, 1500u); }); } else { std::fill_n(wait_modifiers.ptr(), grid.thread_count_, 0u); } const auto shared_memory_size = global_memory ? 0u : alloc_size_per_block; HIP_CHECK(hipMemcpy(wait_modifiers_dev.ptr(), wait_modifiers.ptr(), grid.thread_count_ * sizeof(unsigned int), hipMemcpyHostToDevice)); block_tile_sync_check <<>>(arr_dev.ptr(), wait_modifiers_dev.ptr()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(arr.ptr(), arr_dev.ptr(), alloc_size, hipMemcpyDeviceToHost)); HIP_CHECK(hipDeviceSynchronize()); REQUIRE( std::all_of(arr.ptr(), arr.ptr() + grid.thread_count_, [](unsigned int e) { return e; })); } } template void BlockTileSyncTest() { static_cast((BlockTileSyncTestImpl(), ...)); } /** * Test Description * ------------------------ * - Launches a kernel wherein blocks are divided into tiled partitions(size of 2, 4, 8, 16, 32, * 64 if AMD) and every thread writes its intra-tile rank into an array slot determined by its * grid-wide linear index. The array is either in global or dynamic shared memory based on a compile * time switch, and the test is run for arrays of 1, 2, and 4 byte elements. Before the write each * thread executes a busy wait loop for a random amount of clock cycles, the amount being read from * an input array. After the write a tile-wide sync is performed and each thread validates that it * can read the expected values that other threads within the same tile have written to their * respective array slots. * Test source * ------------------------ * - unit/cooperativeGrps/thread_block_tile.cc * Test requirements * ------------------------ * - HIP_VERSION >= 5.2 */ HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Sync_Positive_Basic, uint8_t, uint16_t, uint32_t) { SECTION("Global memory") { BlockTileSyncTest(); } SECTION("Shared memory") { BlockTileSyncTest(); } } template void __global__ simpleSum(int* result) { int sum = 1; cg::thread_block mygroup = cg::this_thread_block(); auto mytile = cg::tiled_partition(mygroup); *result = cg::reduce(mytile, sum, cg::plus()); } template void __global__ simpleSumSubtiles(int* result) { int sum = 1; cg::thread_block mygroup = cg::this_thread_block(); auto supertile = cg::tiled_partition(mygroup); auto subtile = cg::tiled_partition(supertile); *result = cg::reduce(subtile, sum, cg::plus()); } template void testReduceForTileSize() { LinearAllocGuard h_result(LinearAllocs::malloc, sizeof(int)); LinearAllocGuard d_result(LinearAllocs::hipMalloc, sizeof(int)); dim3 gridDim = { 1 }; dim3 blockDim = { static_cast(getWarpSize()) }; void* devicePtr = d_result.ptr(); void* args[] = { &devicePtr }; HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(simpleSum), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); REQUIRE(*h_result.host_ptr() == TileSize); HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(simpleSumSubtiles), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); REQUIRE(*h_result.host_ptr() == TileSize / 2); } HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Reduce_Basic, int) { unsigned int wavefrontSize = getWarpSize(); testReduceForTileSize<2>(); testReduceForTileSize<4>(); testReduceForTileSize<8>(); testReduceForTileSize<16>(); testReduceForTileSize<32>(); if (wavefrontSize > 32) { testReduceForTileSize<64>(); } } // @extraMasks when testing coalesced_threads, this can be use to simulate // divergence template void __global__ reduceKernel(T* output, const T* input, unsigned long long* extraMasks, AggregationType* aggType) { int tid = threadIdx.x + threadIdx.y * blockDim.x + threadIdx.z * blockDim.x * blockDim.y; int laneId = tid % warpSize; cg::thread_block mygroup = cg::this_thread_block(); auto mytile = cg::tiled_partition(mygroup); for (int i = 0; i < kNumReduces; i++) { int idx = warpSize * i + laneId; T& result = output[idx]; unsigned long long mask = extraMasks[i]; if ((1ull << laneId) & mask) { switch (*aggType) { case AggregationType::Reduce: result = cg::reduce(mytile, input[idx], Functor()); break; case AggregationType::InclusiveScan: result = cg::inclusive_scan(mytile, input[idx], Functor()); break; case AggregationType::ExclusiveScan: result = cg::exclusive_scan(mytile, input[idx], Functor()); break; default: assert(false && "Unsupported enumeration"); } } else { result = 0; } } } // @extraMasks used to simulate divergence when using coalesced_threads template void __global__ reduceKernelCoalesced(T* output, const T* input, unsigned long long* extraMasks, AggregationType* aggType) { int tid = threadIdx.x + threadIdx.y * blockDim.x + threadIdx.z * blockDim.x * blockDim.y; int laneId = tid % warpSize; for (int i = 0; i < kNumReduces; i++) { int idx = warpSize * i + laneId; unsigned long long mask = extraMasks[i]; T& result = output[idx]; if ((1ull << laneId) & mask) { auto coalesced = cg::coalesced_threads(); switch (*aggType) { case AggregationType::Reduce: result = cg::reduce(coalesced, input[idx], Functor()); break; case AggregationType::InclusiveScan: result = cg::inclusive_scan(coalesced, input[idx], Functor()); break; case AggregationType::ExclusiveScan: result = cg::exclusive_scan(coalesced, input[idx], Functor()); break; default: assert(false && "Unsupported enumeration"); } } else { result = 0; } } } // @TileSize the tile size or 0 when testing coalesced groups template void aggregateForTypeAndOp(AggregationType aggType, dim3 blockDim) { using distribution = typename DistributionType::type; int wavefrontSize = getWarpSize(); int size_bytes = kNumReduces * sizeof(T) * wavefrontSize; LinearAllocGuard h_result(LinearAllocs::malloc, size_bytes); LinearAllocGuard d_result(LinearAllocs::hipMalloc, size_bytes); LinearAllocGuard h_input(LinearAllocs::malloc, size_bytes); LinearAllocGuard d_input(LinearAllocs::hipMalloc, size_bytes); LinearAllocGuard d_extraMasks(LinearAllocs::hipMalloc, kNumReduces * sizeof(unsigned long long)); LinearAllocGuard h_extraMasks(LinearAllocs::malloc, kNumReduces * sizeof(unsigned long long)); LinearAllocGuard d_aggType(LinearAllocs::hipMalloc, sizeof(AggregationType)); std::mt19937_64 gen(Catch::rngSeed()); dim3 gridDim = { 1 }; hipError_t status; typename distribution::result_type a = std::is_same::value? std::numeric_limits::lowest() : (std::is_signed::value? -1023 : 0); typename distribution::result_type b = std::is_same::value? std::numeric_limits::max() : 1023; distribution distInput {a, b}; int numAggregation = 0; void* kernelPtr; T expected[64]; HIP_CHECK(hipMemcpy(d_aggType.ptr(), &aggType, sizeof(aggType), hipMemcpyHostToDevice)); genRandomBuffers(d_input, h_input, distInput, gen, kNumReduces * wavefrontSize); if (TileSize) { // tile block case std::memset(h_extraMasks.host_ptr(), 0xFF, h_extraMasks.size_bytes()); HIP_CHECK(hipMemset(d_extraMasks.ptr(), 0xFF, d_extraMasks.size_bytes())); } else { // coalesced_threads case genRandomMasks(d_extraMasks, h_extraMasks, gen, kNumReduces); } std::array devicePtrs = { d_result.ptr(), d_input.ptr(), d_extraMasks.ptr(), d_aggType.ptr() }; void* args[devicePtrs.size()]; for (int i = 0; i < devicePtrs.size(); i++) { args[i] = &devicePtrs[i]; } if constexpr (TileSize == 0) { kernelPtr = reinterpret_cast(reduceKernelCoalesced); } else { kernelPtr = reinterpret_cast(reduceKernel); } status = hipLaunchCooperativeKernel(kernelPtr, gridDim, blockDim, args, 0, nullptr); HIP_CHECK(status); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); while (numAggregation < kNumReduces) { for (int laneId = 0; laneId < wavefrontSize; laneId++) { T result = h_result.host_ptr()[numAggregation * wavefrontSize + laneId]; int lastLane; const T* input = &h_input.host_ptr()[numAggregation * wavefrontSize]; unsigned long long mask = ~0ull; Op op {}; std::string opName = opToString(); int resultLane; if constexpr (TileSize > 0) { mask >>= (64 - TileSize); mask <<= ((laneId % wavefrontSize) / TileSize) * TileSize; } mask &= h_extraMasks.host_ptr()[numAggregation]; lastLane = 64 - __builtin_clzll(mask) - 1; if (laneId > lastLane) { continue; } calculateExpected(expected, input, op, mask, aggType); if (aggType == AggregationType::Reduce) { resultLane = lastLane; } else { resultLane = laneId; } if ((1ull << laneId) & mask) { if constexpr (std::is_integral::value) { // for integral types the result should match exactly // for reduce, the result would be in the last lane whose first bit is on in the mask // for scans, the associated result is different in each lane if (result != expected[resultLane]) { INFO("Aggregation type: " << aggregationTypeToStr(aggType)); printMismatch(result, expected[resultLane], input, mask, laneId); INFO("Operator: " << opName << " mask: 0x" << std::hex << mask); REQUIRE(result == expected[resultLane]); } } else { compareFloatingPoint(result, expected[resultLane], mask, h_input.host_ptr(), laneId); } } } numAggregation++; } } template void aggregateCoopTiles(AggregationType, dim3, const std::index_sequence<>) { } template void aggregateCoopTiles(AggregationType aggType, dim3 blockDim, const std::index_sequence) { const std::index_sequence remainingTiles; aggregateForTypeAndOp(aggType, blockDim); aggregateCoopTiles(aggType, blockDim, remainingTiles); } template void runAggregationRandomForType(AggregationType aggType, dim3 blockDim) { if constexpr (Coalesced) { aggregateForTypeAndOp<0, Op, T>(aggType, blockDim); } else if constexpr (MaxTileSize <= 4) { std::index_sequence<1, 2, 4> tileSizes; aggregateCoopTiles(aggType, blockDim, tileSizes); } else if constexpr (MaxTileSize <= 32) { std::index_sequence<1, 2, 4, 8, 16, 32> tileSizes; aggregateCoopTiles(aggType, blockDim, tileSizes); } else { std::index_sequence<1, 2, 4, 8, 16, 32, 64> tileSizes; aggregateCoopTiles(aggType, blockDim, tileSizes); } } template void runAggregationRandomForOps(AggregationType aggType, const std::tuple<>) { } template void runAggregationRandomForOps(AggregationType aggType, const std::tuple) { const std::tuple remainingOps; int wavefrontSize = getWarpSize(); dim3 blockDim = {static_cast(wavefrontSize)}; runAggregationRandomForType(aggType, blockDim); runAggregationRandomForOps(aggType, remainingOps); } // for all the tile sizes and all input types, using random input values, calculates the reduce() // values. Additionally, randomly make some threads not participate for the coalesced_threads case HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Reduce_Random_arithmetic, int, unsigned int, long long, unsigned long long, float, half, double) { std::tuple, cooperative_groups::less, cooperative_groups::greater> types; int wavefrontSize = getWarpSize(); dim3 blockDim = {static_cast(wavefrontSize)}; if (wavefrontSize == 32) { runAggregationRandomForOps(AggregationType::Reduce, types); } else { runAggregationRandomForOps(AggregationType::Reduce, types); } } HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Reduce_Random_boolean, int, unsigned int, long long, unsigned long long) { std::tuple, cooperative_groups::bit_or, cooperative_groups::bit_xor> types; if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::Reduce, types); } else { runAggregationRandomForOps(AggregationType::Reduce, types); } } // passes a custom operator to cooperative_groups::reduce() HIP_TEST_CASE(Unit_Thread_Block_Tile_Reduce_Custom_Op) { int wavefrontSize = getWarpSize(); dim3 blockDim = {static_cast(wavefrontSize)}; if (wavefrontSize == 32) { runAggregationRandomForType, int, 32>(AggregationType::Reduce, blockDim); } else { runAggregationRandomForType, int, 64>(AggregationType::Reduce, blockDim); } } HIP_TEST_CASE(Unit_Thread_Block_Tile_Scan_Custom_Op) { dim3 blockDim = {static_cast(getWarpSize())}; // only using 4 threads to avoid long long overflows runAggregationRandomForType, long long, 4>( AggregationType::InclusiveScan, blockDim); } struct Vector { int x; int y; }; // given two vector returns the one with the maximum magnitude struct MaxMagnitude { Vector __device__ operator()(Vector lhs, Vector rhs) const { int lhsMag = lhs.x * lhs.x + lhs.y * lhs.y; int rhsMag = rhs.x * rhs.x + rhs.y * rhs.y; return lhsMag > rhsMag? lhs : rhs; } }; void __global__ maxMagnitude(Vector* result, AggregationType* aggregationType) { cg::thread_block mygroup = cg::this_thread_block(); auto mytile = cg::tiled_partition<4>(mygroup); MaxMagnitude op; Vector input[] = {{ 2, 3 }, { 1, 9 }, { 0, 7 }, { 4, 1} }; switch (*aggregationType) { case AggregationType::Reduce: result[threadIdx.x] = cg::reduce(mytile, input[threadIdx.x], op); break; case AggregationType::InclusiveScan: result[threadIdx.x] = cg::inclusive_scan(mytile, input[threadIdx.x], op); break; case AggregationType::ExclusiveScan: result[threadIdx.x] = cg::exclusive_scan(mytile, input[threadIdx.x], op); break; default: assert(false && "Unexpected aggType"); } } // tests that we can pass trivially copyable structs as values to reduce HIP_TEST_CASE(Unit_Thread_Block_Tile_Reduce_Trivially_Copyable_Parameters) { dim3 gridDim = { 1 }; dim3 blockDim = { 4 }; LinearAllocGuard h_result(LinearAllocs::malloc, sizeof(Vector) * blockDim.x); LinearAllocGuard d_result(LinearAllocs::hipMalloc, sizeof(Vector) * blockDim.x); LinearAllocGuard d_aggType(LinearAllocs::hipMalloc, sizeof(AggregationType)); std::array devicePtrs = { d_result.ptr(), d_aggType.ptr() }; void* args[devicePtrs.size()]; Vector* result; AggregationType aggType = AggregationType::Reduce; for (int i = 0; i < devicePtrs.size(); i++) { args[i] = &devicePtrs[i]; } HIP_CHECK(hipMemcpy(d_aggType.ptr(), &aggType, sizeof(aggType), hipMemcpyHostToDevice)); HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(maxMagnitude), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); for (unsigned int idx = 0; idx < blockDim.x; idx++) { result = &h_result.host_ptr()[idx]; REQUIRE((result->x == 1 && result->y == 9)); } } TEST_CASE(Unit_Thread_Block_Tile_Scan_Trivially_Copyable_Parameters) { dim3 gridDim = { 1 }; dim3 blockDim = { 4 }; LinearAllocGuard h_result(LinearAllocs::malloc, sizeof(Vector) * blockDim.x); LinearAllocGuard d_result(LinearAllocs::hipMalloc, sizeof(Vector) * blockDim.x); LinearAllocGuard d_aggType(LinearAllocs::hipMalloc, sizeof(AggregationType)); std::array devicePtrs = { d_result.ptr(), d_aggType.ptr() }; void* args[devicePtrs.size()]; Vector* result; AggregationType aggType; for (int i = 0; i < devicePtrs.size(); i++) { args[i] = &devicePtrs[i]; } SECTION("inclusive") { aggType = AggregationType::InclusiveScan; HIP_CHECK(hipMemcpy(d_aggType.ptr(), &aggType, sizeof(aggType), hipMemcpyHostToDevice)); HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(maxMagnitude), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); result = &h_result.host_ptr()[0]; REQUIRE((result->x == 2 && result->y == 3)); result = &h_result.host_ptr()[1]; REQUIRE((result->x == 1 && result->y == 9)); result = &h_result.host_ptr()[2]; REQUIRE((result->x == 1 && result->y == 9)); result = &h_result.host_ptr()[3]; REQUIRE((result->x == 1 && result->y == 9)); } SECTION("exclusive") { aggType = AggregationType::ExclusiveScan; HIP_CHECK(hipMemcpy(d_aggType.ptr(), &aggType, sizeof(aggType), hipMemcpyHostToDevice)); HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(maxMagnitude), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); result = &h_result.host_ptr()[0]; REQUIRE((result->x == 0 && result->y == 0)); result = &h_result.host_ptr()[1]; REQUIRE((result->x == 2 && result->y == 3)); result = &h_result.host_ptr()[2]; REQUIRE((result->x == 1 && result->y == 9)); result = &h_result.host_ptr()[3]; REQUIRE((result->x == 1 && result->y == 9)); } } template using ArrayContainer = std::array; template struct Sum { ArrayContainer __device__ operator()(const ArrayContainer& lhs, const ArrayContainer& rhs) const { ArrayContainer result; for (int i = 0; i < NumElems; i++) { result[i] = lhs[i] + rhs[i]; } return result; } }; template struct Max { ArrayContainer __device__ operator()(const ArrayContainer& lhs, const ArrayContainer& rhs) const { ArrayContainer result; for (int i = 0; i < NumElems; i++) { result[i] = std::max(lhs[i], rhs[i]); } return result; } }; template __global__ void applyFunctor(ArrayContainer* result) { cg::thread_block mygroup = cg::this_thread_block(); auto mytile = cg::tiled_partition(mygroup); __shared__ ArrayContainer input; Functor op; if (threadIdx.x < NumElems) { input[threadIdx.x] = threadIdx.x; } mytile.sync(); if (threadIdx.x < NumElems) { *result = cg::reduce(mytile, input, op); } } template __global__ void applyScanFunctor(ArrayContainer** result, AggregationType* aggType) { cg::thread_block mygroup = cg::this_thread_block(); auto mytile = cg::tiled_partition(mygroup); __shared__ ArrayContainer input; Functor op; unsigned int tid = threadIdx.x + threadIdx.y * blockDim.x + threadIdx.z * blockDim.x * blockDim.y; if (threadIdx.x < NumElems) { input[threadIdx.x] = threadIdx.x; } mytile.sync(); if (threadIdx.x < NumElems) { switch (*aggType) { case AggregationType::InclusiveScan: *(result[tid]) = cg::inclusive_scan(mytile, input, op); break; case AggregationType::ExclusiveScan: *(result[tid]) = cg::exclusive_scan(mytile, input, op); break; default: assert(false && "AggregationType not supported"); } } } // tests aggregations of arguments of different sizes (types <= 32 bytes are accepted) template class Functor> void testArgsDifferentSizesReduce() { LinearAllocGuard> h_result(LinearAllocs::malloc, sizeof(ArrayContainer)); LinearAllocGuard> d_result(LinearAllocs::hipMalloc, sizeof(ArrayContainer)); dim3 gridDim = { 1 }; dim3 blockDim = { 32 }; void* devicePtr = d_result.ptr(); void* args[] = { &devicePtr }; ArrayContainer* result; HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(applyFunctor>), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); result = &h_result.host_ptr()[0]; INFO("T is of size: " << NumElems); for (int i = 0; i < NumElems; i++) { INFO("Element: " << i); if (std::is_same, Sum>::value) { // the result can be calculated with an arithmetic series formula, modulo 256 // (we do overflow unsigned char for some indices, but that is defined behaviour) REQUIRE((*result)[i] == ((NumElems * (i + i)) / 2) % 256); } else { REQUIRE((*result)[i] == i); } } if constexpr (NumElems > 1) { testArgsDifferentSizesReduce(); } } // in this case, as opposed to the reduction, were we were only saving one result per reduction, // we save one result per lane. i.e. we save one ArrayContainer per lane, as each lane will return // a different ArrayContainer value template class Functor> void testArgsDifferentSizesScan(AggregationType aggType) { int wavefrontSize = getWarpSize(); // one per lane LinearAllocGuard> h_result[64]; LinearAllocGuard> d_result[64]; // as we cannot pass d_result directly (because it is an array of // LinearAllocGuards), we convert to an array of raw device pointers ArrayContainer* h_devicePtrs[64]; LinearAllocGuard*> d_devicePtrs(LinearAllocs::hipMalloc, sizeof(void*) * 64); LinearAllocGuard d_aggType(LinearAllocs::hipMalloc, sizeof(AggregationType)); dim3 gridDim = { 1 }; dim3 blockDim = { 32 }; ArrayContainer* result; for (int i = 0; i < wavefrontSize; i++) { h_result[i] = LinearAllocGuard>(LinearAllocs::malloc, sizeof(ArrayContainer)); d_result[i] = LinearAllocGuard>(LinearAllocs::hipMalloc, sizeof(ArrayContainer)); } for (int i = 0; i < wavefrontSize; i++) { h_devicePtrs[i] = d_result[i].ptr(); } HIP_CHECK(hipMemcpy(d_aggType.ptr(), &aggType, sizeof(aggType), hipMemcpyHostToDevice)); HIP_CHECK(hipMemcpy(d_devicePtrs.ptr(), h_devicePtrs, d_devicePtrs.size_bytes(), hipMemcpyHostToDevice)); std::array devicePtrs = { d_devicePtrs.ptr(), d_aggType.ptr() }; void* args[devicePtrs.size()]; for (int i = 0; i < devicePtrs.size(); i++) { args[i] = &devicePtrs[i]; } HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(applyScanFunctor>), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); for (int i = 0; i < wavefrontSize; i++) { HIP_CHECK(hipMemcpy(h_result[i].host_ptr(), d_result[i].ptr(), h_result[i].size_bytes(), hipMemcpyDeviceToHost)); } INFO("T is of size: " << NumElems); for (int laneId = 0; laneId < NumElems; laneId++) { result = &(h_result[laneId].host_ptr()[0]); if (aggType == AggregationType::InclusiveScan) { INFO("Lane: " << laneId); if (std::is_same, Sum>::value) { // the result can be calculated with an arithmetic series formula, modulo 256 // (we do overflow unsigned char for some indices, but that is defined behaviour) REQUIRE((*result)[laneId] == (((laneId + 1) * (laneId + laneId) / 2) % 256)); } else { REQUIRE((*result)[laneId] == laneId); } } else if (aggType == AggregationType::ExclusiveScan) { if (std::is_same, Sum>::value) { INFO("Lane: " << laneId); // the result can be calculated with an arithmetic series formula, modulo 256 // (we do overflow unsigned char for some indices, but that is defined behaviour) if (laneId == 0) { REQUIRE((*result)[laneId] == 0); } else { REQUIRE((*result)[laneId] == (((laneId) * (laneId + laneId) / 2) % 256)); } } else { if (laneId == 0) { REQUIRE((*result)[laneId] == 0); } else { REQUIRE((*result)[laneId] == laneId); } } } } if constexpr (NumElems > 1) { testArgsDifferentSizesScan(aggType); } } // we allow any reduction size of T of up to 32 bytes; this tests that reduction works with user-defined // types in that range; including non-powers of two HIP_TEST_CASE(Unit_Thread_Block_Tile_Reduce_All_Parameter_Sizes) { SECTION("sum") { testArgsDifferentSizesReduce<32, Sum>(); } SECTION("max") { testArgsDifferentSizesReduce<32, Max>(); } } TEST_CASE(Unit_Thread_Block_Tile_Scan_All_Parameter_Sizes) { SECTION("sum") { testArgsDifferentSizesScan<32, Sum>(AggregationType::InclusiveScan); testArgsDifferentSizesScan<32, Sum>(AggregationType::ExclusiveScan); } SECTION("max") { testArgsDifferentSizesScan<32, Max>(AggregationType::InclusiveScan); testArgsDifferentSizesScan<32, Max>(AggregationType::ExclusiveScan); } } struct Point { int x; int y; __host__ __device__ Point operator+(const Point& rhs) const { return { x + rhs.x, y + rhs.y }; } }; __global__ void sumPoints(Point* result) { cg::thread_block mygroup = cg::this_thread_block(); auto mytile = cg::tiled_partition<32>(mygroup); Point input; input.x = threadIdx.x; input.y = threadIdx.x; __syncwarp(); *result = cg::reduce(mytile, input, cooperative_groups::plus {}); } // using a standard functor in the cooperative_groups namespace with a type that is not primitive HIP_TEST_CASE(Unit_Thread_Block_Tile_Reduce_Standard_Op_Custom_Type) { LinearAllocGuard h_result(LinearAllocs::malloc, sizeof(Point) * 32); LinearAllocGuard d_result(LinearAllocs::hipMalloc, sizeof(Point) * 32); dim3 gridDim = { 1 }; dim3 blockDim = { 32 }; void* devicePtr = d_result.ptr(); void* args[] = { &devicePtr }; int expected = 31 * 16; HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(sumPoints), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); for (int i = 0; i < 32; i++) { INFO("Expected x: " << expected << " got: " << *h_result.host_ptr()); INFO("Expected y: " << expected << " got: " << *h_result.host_ptr()); REQUIRE((h_result.host_ptr()->x == expected && h_result.host_ptr()->y == expected)); } } HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Coalesced_Reduce_arithmetic, int, unsigned int, long long, unsigned long long, float, half, double) { std::tuple, cooperative_groups::less, cooperative_groups::greater> ops; if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::Reduce, ops); } else { runAggregationRandomForOps(AggregationType::Reduce, ops); } } HIP_TEMPLATE_TEST_CASE(Unit_Thread_Block_Coalesced_Reduce_boolean, int, unsigned int, long long, unsigned long long) { std::tuple, cooperative_groups::bit_or, cooperative_groups::bit_xor> ops; if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::Reduce, ops); } else { runAggregationRandomForOps(AggregationType::Reduce, ops); } } template void __global__ simpleScan(T* result) { T value = threadIdx.x; cg::thread_block mygroup = cg::this_thread_block(); auto mytile = cg::tiled_partition(mygroup); Op op; if constexpr (AggType == AggregationType::InclusiveScan) { result[threadIdx.x] = cg::inclusive_scan(mytile, value, op); } else if constexpr (AggType == AggregationType::ExclusiveScan) { result[threadIdx.x] = cg::exclusive_scan(mytile, value, op); } else if constexpr (AggType == AggregationType::InclusiveScanDefault) { result[threadIdx.x] = cg::inclusive_scan(mytile, value); } else if constexpr (AggType == AggregationType::ExclusiveScanDefault) { result[threadIdx.x] = cg::exclusive_scan(mytile, value); } else { assert(false && "Unexpected aggType"); } } /// @tparam Op either std::plus or std::less template void testScanForTileSize() { LinearAllocGuard h_result(LinearAllocs::malloc, sizeof(T) * getWarpSize()); LinearAllocGuard d_result(LinearAllocs::hipMalloc, h_result.size_bytes()); dim3 gridDim = { 1 }; dim3 blockDim = { static_cast(getWarpSize()) }; T id = 0; T accum = 0; int pos = 0; LinearAllocGuard d_aggType(LinearAllocs::hipMalloc, sizeof(AggregationType)); std::array devicePtrs = { d_result.ptr(), d_aggType.ptr() }; void* args[devicePtrs.size()]; AggregationType aggType = AggType; if (!isInclusive(aggType)) { scanIdentity(id); } accum = id; for (int i = 0; i < devicePtrs.size(); i++) { args[i] = &devicePtrs[i]; } HIP_CHECK(hipMemcpy(d_aggType.ptr(), &aggType, sizeof(AggType), hipMemcpyHostToDevice)); HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(simpleScan), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); while (pos < getWarpSize()) { for (int i = 0; i < TileSize; i++) { UNSCOPED_INFO("Index: " << pos + i << " tile size: " << TileSize); if (isInclusive(aggType)) { accum += pos + i; INFO("Inclusive scan"); REQUIRE(h_result.host_ptr()[pos + i] == accum); } else { using ComparisonType = typename std::conditional::value, float, T>::type; ComparisonType result, expected; INFO("Exclusive scan"); if constexpr (std::is_same::value) { // Catch2 cannot print fp16 if there is an error result = __half2float(h_result.host_ptr()[pos + i]); expected = __half2float(__half2float(accum)); } else { result = h_result.host_ptr()[pos + i]; expected = accum; } INFO("Lane: " << (pos + i)); REQUIRE(result == expected); if (i == 0) { accum = id; } if constexpr (std::is_same>::value) { accum = std::min(__half2float(accum), __half2float(pos + i)); } else { accum += pos + i; } } } accum = id; pos += TileSize; } } TEST_CASE(Unit_Thread_Block_Tile_Inclusive_Scan_Basic) { using Op = cooperative_groups::plus; static constexpr AggregationType AggType = AggregationType::InclusiveScan; testScanForTileSize<1, AggType, Op, int>(); testScanForTileSize<2, AggType, Op, int>(); testScanForTileSize<4, AggType, Op, int>(); testScanForTileSize<8, AggType, Op, int>(); testScanForTileSize<16, AggType, Op, int>(); testScanForTileSize<32, AggType, Op, int>(); testScanForTileSize<32, AggregationType::InclusiveScanDefault, Op, int>(); if (getWarpSize() == 64) { testScanForTileSize<64, AggType, Op, int>(); } } TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Exclusive_Scan_Basic, int, half) { static constexpr AggregationType AggType = AggregationType::ExclusiveScan; SECTION("plus") { using Op = cooperative_groups::plus; testScanForTileSize<1, AggType, Op, TestType>(); testScanForTileSize<2, AggType, Op, TestType>(); testScanForTileSize<4, AggType, Op, TestType>(); testScanForTileSize<8, AggType, Op, TestType>(); testScanForTileSize<16, AggType, Op, TestType>(); testScanForTileSize<32, AggType, Op, TestType>(); testScanForTileSize<32, AggregationType::ExclusiveScanDefault, Op, int>(); if (getWarpSize() == 64) { testScanForTileSize<64, AggType, Op, TestType>(); } } SECTION("less") { using Op = cooperative_groups::less; testScanForTileSize<1, AggType, Op, TestType>(); testScanForTileSize<2, AggType, Op, TestType>(); testScanForTileSize<4, AggType, Op, TestType>(); testScanForTileSize<8, AggType, Op, TestType>(); testScanForTileSize<16, AggType, Op, TestType>(); testScanForTileSize<32, AggType, Op, TestType>(); if (getWarpSize() == 64) { testScanForTileSize<64, AggType, Op, TestType>(); } } } // for all the tile sizes and all input types, using random input values, calculates the scan // values. Additionally, randomly make some threads not participate for the coalesced_threads case TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Scan_Random_arithmetic, int, unsigned int, long long, unsigned long long, float, half, double) { std::tuple, cooperative_groups::less, cooperative_groups::greater> types; SECTION("inclusive") { if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::InclusiveScan, types); } else { runAggregationRandomForOps(AggregationType::InclusiveScan, types); } } SECTION("exclusive") { if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::ExclusiveScan, types); } else { runAggregationRandomForOps(AggregationType::ExclusiveScan, types); } } } TEMPLATE_TEST_CASE(Unit_Thread_Block_Tile_Scan_Random_boolean, int, unsigned int, long long, unsigned long long) { std::tuple, cooperative_groups::bit_or, cooperative_groups::bit_xor> types; SECTION("inclusive") { if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::InclusiveScan, types); } else { runAggregationRandomForOps(AggregationType::InclusiveScan, types); } } SECTION("exclusive") { if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::ExclusiveScan, types); } else { runAggregationRandomForOps(AggregationType::ExclusiveScan, types); } } } // make sures that tiled blocks that use the y or z dimension work correctly TEST_CASE(Unit_Thread_Block_Tile_2D_3D_Blocks) { int wavefrontSize = getWarpSize(); SECTION("2D") { dim3 blockDim {1, 2, static_cast(wavefrontSize / 2)}; if (wavefrontSize == 32) { runAggregationRandomForType, int, 32>(AggregationType::Reduce, blockDim); runAggregationRandomForType, int, 32>(AggregationType::InclusiveScan, blockDim); runAggregationRandomForType, int, 32>(AggregationType::ExclusiveScan, blockDim); } else { runAggregationRandomForType, int, 64>(AggregationType::Reduce, blockDim); runAggregationRandomForType, int, 64>(AggregationType::InclusiveScan, blockDim); runAggregationRandomForType, int, 64>(AggregationType::ExclusiveScan, blockDim); } } SECTION("3D") { dim3 blockDim {static_cast(wavefrontSize) / 2, 2, 1}; if (wavefrontSize == 32) { runAggregationRandomForType, int, 32>(AggregationType::Reduce, blockDim); runAggregationRandomForType, int, 32>(AggregationType::InclusiveScan, blockDim); runAggregationRandomForType, int, 32>(AggregationType::ExclusiveScan, blockDim); } else { runAggregationRandomForType, int, 64>(AggregationType::Reduce, blockDim); runAggregationRandomForType, int, 64>(AggregationType::InclusiveScan, blockDim); runAggregationRandomForType, int, 64>(AggregationType::ExclusiveScan, blockDim); } } } TEMPLATE_TEST_CASE(Unit_Thread_Block_Coalesced_Scan_arithmetic, int, unsigned int, long long, unsigned long long, float, half, double) { std::tuple, cooperative_groups::less, cooperative_groups::greater> ops; SECTION("inclusive") { if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::InclusiveScan, ops); } else { runAggregationRandomForOps(AggregationType::InclusiveScan, ops); } } SECTION("exclusive") { if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::ExclusiveScan, ops); } else { runAggregationRandomForOps(AggregationType::ExclusiveScan, ops); } } } TEMPLATE_TEST_CASE(Unit_Thread_Block_Coalesced_Scan_boolean, int, unsigned int, long long, unsigned long long) { std::tuple, cooperative_groups::bit_or, cooperative_groups::bit_xor> ops; SECTION("inclusive") { if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::InclusiveScan, ops); } else { runAggregationRandomForOps(AggregationType::InclusiveScan, ops); } } SECTION("exclusive") { if (getWarpSize() == 32) { runAggregationRandomForOps(AggregationType::ExclusiveScan, ops); } else { runAggregationRandomForOps(AggregationType::ExclusiveScan, ops); } } } void __global__ binaryPartitionCoalesced(int* out, int* ranks) { if (threadIdx.x >= warpSize / 2) { // this group will contain the upper part of the threads auto coalesced = cg::coalesced_threads(); // this group is subsequently split in two: on even and odd indexes auto partitioned = cg::binary_partition(coalesced, threadIdx.x % 2); ranks[threadIdx.x] = partitioned.thread_rank(); out[threadIdx.x] = cg::inclusive_scan(partitioned, threadIdx.x); } else { ranks[threadIdx.x] = -1; out[threadIdx.x] = -1; } } template void __global__ binaryPartitionTiled(int* out, int* ranks) { cg::thread_block mygroup = cg::this_thread_block(); auto tile = cg::tiled_partition(mygroup); if (tile.meta_group_rank() == 1) { auto partitioned = cg::binary_partition(tile, threadIdx.x % 2); ranks[threadIdx.x] = partitioned.thread_rank(); out[threadIdx.x] = cg::inclusive_scan(partitioned, threadIdx.x); } else { ranks[threadIdx.x] = -1; out[threadIdx.x] = -1; } } TEST_CASE(Unit_Thread_Block_Scan_partition) { int wavefrontSize = getWarpSize(); LinearAllocGuard h_result(LinearAllocs::malloc, sizeof(int) * wavefrontSize); LinearAllocGuard d_result(LinearAllocs::hipMalloc, h_result.size_bytes()); LinearAllocGuard h_ranks(LinearAllocs::malloc, sizeof(int) * wavefrontSize); LinearAllocGuard d_ranks(LinearAllocs::hipMalloc, h_ranks.size_bytes()); dim3 gridDim = { 1 }; dim3 blockDim = { static_cast(getWarpSize()) }; void* resultsPtr = d_result.ptr(); void* ranksPtr = d_ranks.ptr(); void* args[] = { &resultsPtr, &ranksPtr }; auto checkResults = [&]() { int accumEven = 0; int accumOdd = 0; for (int laneId = 0; laneId < getWarpSize(); laneId++) { if (laneId >= wavefrontSize / 2) { if (laneId % 2 == 0) { accumEven += laneId; } else { accumOdd += laneId; } INFO("laneId: " << laneId); REQUIRE(h_ranks.host_ptr()[laneId] == (laneId - wavefrontSize / 2) / 2); REQUIRE(h_result.host_ptr()[laneId] == (laneId % 2 ? accumOdd : accumEven)); } else { INFO("laneId: " << laneId); REQUIRE(h_ranks.host_ptr()[laneId] == -1); REQUIRE(h_result.host_ptr()[laneId] == -1); } } }; // the result of both sections must be the same; in one we use coalesced_threads to sub-divide // into higher-order and lower-order threads, in another we just use cg::tiled_partition to do the // equivalent SECTION("coalesced") { HIP_CHECK(hipLaunchCooperativeKernel(reinterpret_cast(binaryPartitionCoalesced), gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_ranks.host_ptr(), d_ranks.ptr(), h_ranks.size_bytes(), hipMemcpyDeviceToHost)); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); checkResults(); } SECTION("tiled") { void* kernelPtr = reinterpret_cast(wavefrontSize == 32? binaryPartitionTiled<32> : binaryPartitionTiled<64>); HIP_CHECK(hipLaunchCooperativeKernel(kernelPtr, gridDim, blockDim, args, 0, nullptr)); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_ranks.host_ptr(), d_ranks.ptr(), h_ranks.size_bytes(), hipMemcpyDeviceToHost)); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); checkResults(); } } __global__ void multiDimReduce(int* output) { cg::thread_block mygroup = cg::this_thread_block(); auto mytile = cg::tiled_partition<16>(mygroup); int tid = threadIdx.x + threadIdx.y * blockDim.x + threadIdx.z * blockDim.x * blockDim.y; output[tid] = cooperative_groups::reduce(mytile, tid, cooperative_groups::plus()); } // test tiles reduces on multidimensional blocks. Basically splits the 128 threads in the block // into 16 threads tiles, each one contributing the tid to their reduces HIP_TEST_CASE(Unit_Thread_Block_Tile_Multi_Dimensional_Reduce) { LinearAllocGuard h_result(LinearAllocs::malloc, sizeof(int) * 128); LinearAllocGuard d_result(LinearAllocs::hipMalloc, h_result.size_bytes()); dim3 gridDim = { 1 }; // use a block size bigger than the warp size dim3 blockDim = { 16, 4, 2 }; void* devicePtr = d_result.ptr(); void* args[1] = { &devicePtr }; hipError_t status; status = hipLaunchCooperativeKernel(multiDimReduce, gridDim, blockDim, args, 0, nullptr); HIP_CHECK(status); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipGetLastError()); HIP_CHECK(hipMemcpy(h_result.host_ptr(), d_result.ptr(), h_result.size_bytes(), hipMemcpyDeviceToHost)); for (int row = 0; row < 8; row++) { int rowResult = 0; for (int index = 0; index < 16; index++) { rowResult += row * 16 + index; } for (int index = 0; index < 16; index++) { INFO("row: " << row << " index: " << index); REQUIRE(h_result.host_ptr()[row * 16 + index] == rowResult); } } } /** * End doxygen group DeviceLanguageTest. * @} */