blob: df444fb9d90e48139639dd281c4b4dcbdfce1337 [file]
//===---- 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"