| //===---- Reduction.cpp - OpenMP device reduction implementation - C++ -*-===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // This file contains the implementation of reduction with KMPC interface. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "DeviceTypes.h" |
| #include "Interface.h" |
| #include "Mapping.h" |
| #include "State.h" |
| #include "Synchronization.h" |
| |
| using namespace ompx; |
| |
| static constexpr uint32_t kmpc_min(uint32_t a, uint32_t b) { |
| return a < b ? a : b; |
| } |
| |
| // Round down to the nearest multiple of the warp size. Return 1 if the value is |
| // less than the warp size. |
| static uint32_t round_down_to_warpsize(uint32_t s) { |
| if (s < mapping::getWarpSize()) |
| return 1; |
| return (s & ~static_cast<uint32_t>(mapping::getWarpSize() - 1u)); |
| } |
| |
| static void gpu_regular_warp_reduce(void *reduce_data, |
| ShuffleReductFnTy shflFct) { |
| for (uint32_t mask = mapping::getWarpSize() / 2; mask > 0; mask /= 2) { |
| shflFct(reduce_data, /*LaneId - not used= */ 0, |
| /*Offset = */ mask, /*AlgoVersion=*/0); |
| } |
| } |
| |
| static void gpu_irregular_warp_reduce(void *reduce_data, |
| ShuffleReductFnTy shflFct, uint32_t size, |
| uint32_t tid) { |
| uint32_t curr_size; |
| uint32_t mask; |
| curr_size = size; |
| mask = curr_size / 2; |
| while (mask > 0) { |
| shflFct(reduce_data, /*LaneId = */ tid, /*Offset=*/mask, /*AlgoVersion=*/1); |
| curr_size = (curr_size + 1) / 2; |
| mask = curr_size / 2; |
| } |
| } |
| |
| static uint32_t gpu_irregular_simd_reduce(void *reduce_data, |
| ShuffleReductFnTy shflFct) { |
| uint32_t size, remote_id, physical_lane_id; |
| physical_lane_id = mapping::getThreadIdInBlock() % mapping::getWarpSize(); |
| __kmpc_impl_lanemask_t lanemask_lt = mapping::lanemaskLT(); |
| __kmpc_impl_lanemask_t Liveness = mapping::activemask(); |
| uint32_t logical_lane_id = utils::popc(Liveness & lanemask_lt) * 2; |
| __kmpc_impl_lanemask_t lanemask_gt = mapping::lanemaskGT(); |
| do { |
| Liveness = mapping::activemask(); |
| remote_id = utils::ctz(Liveness & lanemask_gt); |
| size = utils::popc(Liveness); |
| logical_lane_id /= 2; |
| shflFct(reduce_data, /*LaneId =*/logical_lane_id, |
| /*Offset=*/remote_id - physical_lane_id, /*AlgoVersion=*/2); |
| } while (logical_lane_id % 2 == 0 && size > 1); |
| return (logical_lane_id == 0); |
| } |
| |
| // Reduction within a block on the GPU. |
| // |
| // Template parameters: |
| // - checkLiveness: Whether to check the liveness of the lanes. This is |
| // useful if gpu_block_reduce is called in a context where |
| // partial warps or L2 parallel regions are possible. |
| // Parameters: |
| // - reduce_data: Pointer to the reduction data |
| // - shflFct: Shuffle reduction function |
| // - cpyFct: Inter-warp copy function (copies data from each warp's thread |
| // 0 to the lanes of the zeroth warp) |
| // - NumThreads: Number of threads to consider / values to reduce |
| // - ThreadId: Thread ID in block (getThreadIdInBlock() in SPMD and 0 in |
| // Generic mode) |
| // |
| // Returns: |
| // - 1 if the thread is the zeroth thread of the block |
| // - 0 otherwise |
| // |
| // Note that it is expected that the caller checks for NumThreads <= 1 and acts |
| // in a way that suits the callers situation. If checkLiveness is false, this |
| // function performs a regular warp reduce unconditionally. |
| // |
| template <bool checkLiveness = true> |
| static uint32_t gpu_block_reduce(void *reduce_data, ShuffleReductFnTy shflFct, |
| InterWarpCopyFnTy cpyFct, uint32_t NumThreads, |
| uint32_t BlockThreadId) { |
| if constexpr (checkLiveness) { |
| __kmpc_impl_lanemask_t Liveness = mapping::activemask(); |
| // Check for partial warp with non-contiguous lanes. |
| if (Liveness == lanes::All) { |
| gpu_regular_warp_reduce(reduce_data, shflFct); |
| } else if (!(Liveness & (Liveness + 1))) { |
| // Partial warp but contiguous lanes. |
| gpu_irregular_warp_reduce(reduce_data, shflFct, utils::popc(Liveness), |
| BlockThreadId % mapping::getWarpSize()); |
| } else { |
| // Dispersed lanes. Only threads in L2 parallel region may enter here. |
| return gpu_irregular_simd_reduce(reduce_data, shflFct); |
| } |
| } else { |
| gpu_regular_warp_reduce(reduce_data, shflFct); |
| } |
| |
| // When we have more than [mapping::getWarpSize()] number of threads |
| // a block reduction is performed here. |
| // |
| // Only L1 parallel region can enter this if condition. |
| |
| if (NumThreads > mapping::getWarpSize()) { |
| uint32_t WarpsNeeded = utils::roundUp(NumThreads, mapping::getWarpSize()); |
| // Gather all the reduced values from each warp to the first warp. |
| cpyFct(reduce_data, WarpsNeeded); |
| |
| uint32_t WarpId = BlockThreadId / mapping::getWarpSize(); |
| if (WarpId == 0) |
| gpu_irregular_warp_reduce(reduce_data, shflFct, WarpsNeeded, |
| BlockThreadId); |
| } |
| |
| return BlockThreadId == 0; |
| } |
| |
| static int32_t nvptx_parallel_reduce_nowait(void *reduce_data, |
| ShuffleReductFnTy shflFct, |
| InterWarpCopyFnTy cpyFct) { |
| uint32_t BlockThreadId = mapping::getThreadIdInBlock(); |
| if (mapping::isMainThreadInGenericMode(/*IsSPMD=*/false)) |
| BlockThreadId = 0; |
| uint32_t NumThreads = omp_get_num_threads(); |
| if (NumThreads == 1) |
| return 1; |
| |
| // |
| // This reduce function handles reduction within a team. It handles |
| // parallel regions in both L1 and L2 parallelism levels. It also |
| // supports Generic, SPMD, and NoOMP modes. |
| // |
| // 1. Reduce within a warp. |
| // 2. Warp master copies value to warp 0 via shared memory. |
| // 3. Warp 0 reduces to a single value. |
| // 4. The reduced value is available in the thread that returns 1. |
| // |
| |
| #if __has_builtin(__nvvm_reflect) |
| if (__nvvm_reflect("__CUDA_ARCH") >= 700) { |
| uint32_t WarpsNeeded = utils::roundUp(NumThreads, mapping::getWarpSize()); |
| uint32_t WarpId = mapping::getWarpIdInBlock(); |
| |
| // Volta execution model: |
| // For the Generic execution mode a parallel region either has 1 thread and |
| // beyond that, always a multiple of 32. For the SPMD execution mode we may |
| // have any number of threads. |
| if ((NumThreads % mapping::getWarpSize() == 0) || |
| (WarpId < WarpsNeeded - 1)) |
| gpu_regular_warp_reduce(reduce_data, shflFct); |
| else if (NumThreads > 1) // Only SPMD execution mode comes thru this case. |
| gpu_irregular_warp_reduce( |
| reduce_data, shflFct, |
| /*LaneCount=*/NumThreads % mapping::getWarpSize(), |
| /*LaneId=*/mapping::getThreadIdInBlock() % mapping::getWarpSize()); |
| |
| // When we have more than [mapping::getWarpSize()] number of threads |
| // a block reduction is performed here. |
| // |
| // Only L1 parallel region can enter this if condition. |
| if (NumThreads > mapping::getWarpSize()) { |
| // Gather all the reduced values from each warp |
| // to the first warp. |
| cpyFct(reduce_data, WarpsNeeded); |
| |
| if (WarpId == 0) |
| gpu_irregular_warp_reduce(reduce_data, shflFct, WarpsNeeded, |
| BlockThreadId); |
| } |
| return BlockThreadId == 0; |
| } |
| #endif |
| |
| return gpu_block_reduce(reduce_data, shflFct, cpyFct, NumThreads, |
| BlockThreadId); |
| } |
| |
| extern "C" { |
| [[clang::always_inline]] |
| int32_t __kmpc_nvptx_parallel_reduce_nowait_v2(IdentTy *Loc, |
| uint64_t reduce_data_size, |
| void *reduce_data, |
| ShuffleReductFnTy shflFct, |
| InterWarpCopyFnTy cpyFct) { |
| return nvptx_parallel_reduce_nowait(reduce_data, shflFct, cpyFct); |
| } |
| |
| // Reduction across teams on the GPU. |
| // |
| // Parameters: |
| // - Loc: Location of the reduction |
| // - reduce_data: Pointer to the reduction data |
| // - shflFct: Shuffle reduction function |
| // - cpyFct: Inter-warp copy function (copies data from each warp's thread 0 |
| // to the lanes of the zeroth warp) |
| // - lgcpyFct: List-global copy function (copies the reduction data from the |
| // local thread to the global buffer) |
| // - glcpyFct: Global copy function (copies the reduction data from the global |
| // buffer to the local thread) |
| // - glredFct: Global reduce function (reduces the reduction data from the |
| // global buffer to the local thread) |
| // |
| // Returns: |
| // - 1 if this thread must write the final reduced value back to the shared |
| // reduction variable (i.e. thread 0 of the single team when NumTeams == 1, |
| // or thread 0 of the last team to finish its partial reduction otherwise). |
| // - 0 otherwise. |
| // |
| [[clang::always_inline]] |
| int32_t __kmpc_gpu_xteam_reduce_nowait(IdentTy *Loc, void *reduce_data, |
| ShuffleReductFnTy shflFct, |
| InterWarpCopyFnTy cpyFct, |
| ListGlobalFnTy lgcpyFct, |
| ListGlobalFnTy glcpyFct, |
| ListGlobalFnTy glredFct) { |
| uint32_t ThreadId; |
| uint32_t NumThreads; |
| |
| if (mapping::isSPMDMode()) { |
| // In SPMD mode all workers participate in the teams reduction. |
| ThreadId = mapping::getThreadIdInBlock(); |
| NumThreads = mapping::getNumberOfThreadsInBlock(); |
| } else { |
| // In generic mode, only the team master participates in the teams |
| // reduction because the workers are waiting for parallel work. |
| if (!mapping::isMainThreadInGenericMode()) |
| return 0; |
| ThreadId = 0; |
| NumThreads = 1; |
| } |
| |
| uint32_t TeamId = omp_get_team_num(); |
| uint32_t NumTeams = omp_get_num_teams(); |
| |
| // Fast path for single-team kernels: no cross-team work required, |
| // the team-local reduction already produced the final result. |
| if (NumTeams <= 1) |
| return ThreadId == 0; |
| |
| uint32_t &TeamsDone = state::getKernelLaunchEnvironment().ReductionTeamsDone; |
| void *GlobalBuffer = state::getKernelLaunchEnvironment().ReductionBuffer; |
| [[clang::loader_uninitialized]] static Local<uint32_t> TeamsDoneResult; |
| |
| // Save the team's reduced value in the global buffer and atomically |
| // increment the teams-done counter. |
| if (ThreadId == 0) { |
| lgcpyFct(GlobalBuffer, TeamId, reduce_data); |
| // We let the atomic inc wrap around if the value gets larger than |
| // NumTeams-1, which makes the counter self-reset. |
| TeamsDoneResult = atomic::inc(&TeamsDone, NumTeams - 1u, atomic::acq_rel, |
| atomic::MemScopeTy::device); |
| } |
| |
| // This sync is needed so that all threads from last team see the shared teams |
| // done counter value and know that they are in the last team. |
| if (mapping::isSPMDMode()) |
| synchronize::threadsAligned(atomic::acq_rel); |
| |
| // If teams done counter reaches NumTeams-1, this is the last team. |
| if (TeamsDoneResult != NumTeams - 1u) |
| return 0; |
| |
| // The last team performs final reduction across all team values. |
| NumThreads = round_down_to_warpsize(kmpc_min(NumThreads, NumTeams)); |
| if (ThreadId >= NumThreads) |
| return 0; |
| |
| // Make sure that global buffer is fresh. |
| fence::kernel(atomic::acquire); |
| // Get the team values from the global buffer. |
| glcpyFct(GlobalBuffer, ThreadId, reduce_data); |
| // In case we have more teams than threads, we need to iterate over the |
| // remaining teams. |
| for (uint32_t I = NumThreads + ThreadId; I < NumTeams; I += NumThreads) |
| glredFct(GlobalBuffer, I, reduce_data); |
| |
| if (NumThreads == 1) |
| return 1; |
| |
| return gpu_block_reduce<false>(reduce_data, shflFct, cpyFct, NumThreads, |
| ThreadId); |
| } |
| } // extern "C" |