Files
paddlepaddle--paddle/paddle/phi/kernels/funcs/reduce_gpu_kernel.h
T
2026-07-13 12:40:42 +08:00

1470 lines
47 KiB
C++

// Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <bitset>
#include <limits>
#include <set>
#include "paddle/common/enforce.h"
#include "paddle/phi/backends/gpu/gpu_launch_config.h"
#include "paddle/phi/core/enforce.h"
#include "paddle/phi/core/kernel_registry.h"
#include "paddle/phi/core/platform/device/gpu/gpu_info.h"
#include "paddle/phi/kernels/full_kernel.h"
#include "paddle/phi/kernels/funcs/dense_tensor_iterator.h"
#include "paddle/phi/kernels/funcs/index_elementwise.cu.h"
#include "paddle/phi/kernels/funcs/scatter.cu.h"
#include "paddle/phi/kernels/gpu/reduce.h"
#include "paddle/phi/kernels/legacy/reduce_max_kernel.h"
#include "paddle/phi/kernels/prod_kernel.h"
#include "paddle/phi/kernels/reduce_all_kernel.h"
#include "paddle/phi/kernels/reduce_amin_kernel.h"
#include "paddle/phi/kernels/reduce_any_kernel.h"
#include "paddle/phi/kernels/reduce_max_kernel.h"
#include "paddle/phi/kernels/reduce_mean_kernel.h"
#include "paddle/phi/kernels/reduce_min_kernel.h"
#include "paddle/phi/kernels/reduce_sum_kernel.h"
#include "paddle/phi/kernels/funcs/function_traits.h"
#include "paddle/phi/kernels/primitive/reduce_primitives.h"
#ifdef PADDLE_WITH_HIP
#define WARP_SIZE 64
#else
#define WARP_SIZE 32
#endif
// The GPUReduceScheduler splits tensors with indices exceeding 32-bit range to
// ensure that all incoming tensors can be addressed within 32-bit index space.
using IndexType = uint32_t;
template <typename T>
struct LoadImpl {
HOSTDEVICE static T Apply(const void* src) {
return *reinterpret_cast<const T*>(src);
}
};
template <>
struct LoadImpl<bool> {
HOSTDEVICE static bool Apply(const void* src) {
static_assert(sizeof(bool) == sizeof(char));
return *reinterpret_cast<const unsigned char*>(src);
}
};
template <typename T>
HOSTDEVICE constexpr T LoadData(const void* src) {
return LoadImpl<T>::Apply(src);
}
template <typename ScalarT>
HOSTDEVICE constexpr ScalarT LoadData(const ScalarT* src) {
return LoadImpl<ScalarT>::Apply(src);
}
namespace phi {
inline std::bitset<64> DimListToBitset(std::vector<int> opt_dims,
size_t ndims) {
std::bitset<64> dim_mask;
if (opt_dims.size() > 0) {
for (int dim : opt_dims) {
dim_mask.set(dim, true);
}
} else {
for (size_t dim = 0; dim < ndims; dim++) {
dim_mask.set(dim, true);
}
}
return dim_mask;
}
inline std::vector<int> ConvertToPositiveDims(
const std::vector<int>& origin_reduce_dims, int64_t ndim) {
std::vector<int> positive_reduce_dims = origin_reduce_dims;
for (size_t i = 0; i < origin_reduce_dims.size(); ++i) {
PADDLE_ENFORCE_GE(
origin_reduce_dims[i],
-ndim,
common::errors::InvalidArgument(
"ReduceOp: invalid axis, when x_dims is %d, "
"axis[i] should be in the range of [-%d, %d), but got %d.",
ndim,
ndim,
ndim,
origin_reduce_dims[i]));
PADDLE_ENFORCE_LT(
origin_reduce_dims[i],
ndim,
common::errors::InvalidArgument(
"ReduceOp: invalid axis, when x_dims is %d, "
"axis[i] should be in the range of [-%d, %d), but got %d.",
ndim,
ndim,
ndim,
origin_reduce_dims[i]));
if (origin_reduce_dims[i] < 0) {
positive_reduce_dims[i] = ndim + origin_reduce_dims[i];
}
}
return positive_reduce_dims;
}
inline std::bitset<64> MakeDimMask(std::vector<int> opt_dims,
int64_t ndim,
bool allow_empty_dims = false) {
// flip() sets all bits to 1 (masking all dimensions for reduction).
if (opt_dims.empty() && !allow_empty_dims) {
return std::bitset<64>().flip();
}
// Otherwise, use the dimensions specified in opt_dims.
return DimListToBitset(opt_dims, ndim);
}
inline DenseTensor ReviewReduceResult(const DenseTensor& src,
const DenseTensor& result,
int ndim,
std::bitset<64> mask) {
std::vector<int64_t> shape;
std::vector<int64_t> stride;
int64_t cal_stride = 1;
const auto& src_dims = src.dims();
for (int dim = ndim - 1; dim >= 0; dim--) {
if (!mask[dim]) {
shape.insert(shape.begin(), src_dims[dim]);
stride.insert(stride.begin(), cal_stride);
cal_stride *= src_dims[dim];
} else {
shape.insert(shape.begin(), 1);
stride.insert(stride.begin(), cal_stride);
}
}
return funcs::as_strided(result, shape, stride);
}
template <typename T, int Size>
DEVICE AlignedVector<T, Size> LoadVector(const T* base_ptr, uint32_t offset) {
using vec_t = AlignedVector<T, Size>;
auto* from = reinterpret_cast<const vec_t*>(base_ptr);
return from[offset];
}
template <int Size>
DEVICE AlignedVector<bool, Size> LoadVector(const bool* base_ptr,
uint32_t offset) {
auto tmp = LoadVector<uint8_t, Size>(
reinterpret_cast<const uint8_t*>(base_ptr), offset);
AlignedVector<bool, Size> ret;
for (int i = 0; i < Size; ++i) {
ret.val[i] = static_cast<bool>(tmp.val[i]);
}
return ret;
}
// Chose max num threads.
template <typename T>
struct MaxThreadsConfig {
static constexpr int MAX_NUM_THREADS = 512;
};
template <>
struct MaxThreadsConfig<phi::dtype::complex<double>> {
static constexpr int MAX_NUM_THREADS = 256;
};
template <int kNumThreads, int kOutputVecSize, typename Reducer>
__launch_bounds__(kNumThreads, 4) __global__
void VecReduceKernel(Reducer reduction) {
reduction.template Run<kOutputVecSize>();
}
template <typename IndexType>
static funcs::OffsetCalculator<2, IndexType> MakeOutputOffsetCalculator(
const DenseTensorIterator& iter) {
int num_reduce_dims = iter.num_reduce_dims();
int num_output_dims = iter.ndim() - num_reduce_dims;
int input_index = iter.ntensors() - 1;
int output_index = 0;
std::array<const int64_t*, 2> stride_ptrs = {
iter.strides(output_index).data() + num_reduce_dims,
iter.strides(input_index).data() + num_reduce_dims,
};
auto output_shape_ptr = iter.shape().data() + num_reduce_dims;
return funcs::OffsetCalculator<2, IndexType>(
num_output_dims, output_shape_ptr, stride_ptrs.data());
}
template <typename IndexType>
static funcs::OffsetCalculator<1, IndexType> MakeInputOffsetCalculator(
const DenseTensorIterator& iter) {
int num_reduce_dims = iter.num_reduce_dims();
int input_index = iter.ntensors() - 1;
std::array<const int64_t*, 1> strides = {
iter.strides(input_index).data(),
};
auto input_shape_ptr = iter.shape().data();
return funcs::OffsetCalculator<1, IndexType>(
num_reduce_dims, input_shape_ptr, strides.data());
}
template <typename T>
int GetOutputVecSize(const DenseTensorIterator& iter) {
int vec_size = 4;
auto UpdateVectorSize = [&vec_size](uint64_t n) {
while (n % vec_size != 0) {
vec_size /= 2;
}
};
// Check base address alignment.
uint64_t base_address =
reinterpret_cast<uint64_t>(iter.data_ptr(iter.noutputs())) / sizeof(T);
UpdateVectorSize(base_address);
// Check output dimension size.
const int output_index = iter.num_reduce_dims();
UpdateVectorSize(iter.shape()[output_index]);
// Check strides alignment for all dimensions except output dimension.
auto input_tensor_index = iter.noutputs();
auto input_strides = iter.strides(input_tensor_index);
for (int dim = 0; dim < input_strides.size(); ++dim) {
if (dim != output_index) {
UpdateVectorSize(input_strides[dim] / sizeof(T));
}
}
return vec_size;
}
// Simplify fraction by dividing both numerator and denominator by their GCD
// (Greatest Common Divisor).
HOSTDEVICE static void ReduceFraction(size_t* numerator, size_t* denominator) {
size_t a = *denominator;
size_t b = *numerator;
while (b != 0) {
a %= b;
size_t tmp = a;
a = b;
b = tmp;
}
*numerator /= a;
*denominator /= a;
}
struct ReduceConfig {
static constexpr int BLOCK_X = 0;
static constexpr int BLOCK_Y = 1;
static constexpr int CTA = 2;
ReduceConfig(int element_size_bytes, int num_outputs, int num_inputs)
: element_size_bytes(element_size_bytes),
num_inputs(num_inputs),
num_outputs(num_outputs) {}
// Basic configuration.
int element_size_bytes;
int num_inputs;
int num_outputs;
// Parallelism control.
int step_input = 1;
int step_output = 1;
int ctas_per_output = 1;
// Multiplier arrays for index calculation.
int input_multiplier[3] = {0, 0, 0};
int output_multiplier[2] = {0, 0};
// Dimensions.
int block_width;
int block_height;
int num_threads;
// Vectorization control.
bool vectorize_input = false;
int output_vec_size = 1;
template <typename T>
void SetBlockDimensions(int64_t dim0, int64_t dim1) {
const int max_num_threads =
MaxThreadsConfig<T>::MAX_NUM_THREADS / output_vec_size;
int dim0_pow2 =
(dim0 < max_num_threads)
? static_cast<int>(phi::backends::gpu::GetLastPow2(dim0))
: max_num_threads;
int dim1_pow2 =
(dim1 < max_num_threads)
? static_cast<int>(phi::backends::gpu::GetLastPow2(dim1))
: max_num_threads;
block_width = std::min(dim0_pow2, WARP_SIZE);
block_height =
std::min(dim1_pow2, static_cast<int>(max_num_threads / block_width));
block_width =
std::min(dim0_pow2, static_cast<int>(max_num_threads / block_height));
num_threads = block_width * block_height;
}
int SplitInput(int parallelism) {
const int current_step = step_input;
const int64_t next_step = static_cast<int64_t>(step_input) * parallelism;
PADDLE_ENFORCE_LE_INT_MAX(next_step, "reduce step_input");
step_input = static_cast<int>(next_step);
return current_step;
}
int SplitOutput(int parallelism) {
const int current_step = step_output;
const int64_t next_step = static_cast<int64_t>(step_output) * parallelism;
PADDLE_ENFORCE_LE_INT_MAX(next_step, "reduce step_output");
step_output = static_cast<int>(next_step);
return current_step;
}
dim3 GetBlockDim() const { return dim3(block_width, block_height); }
dim3 GetGridDim() const {
const int64_t grid_x = phi::backends::gpu::DivUp<int64_t>(
num_outputs / output_vec_size, step_output);
PADDLE_ENFORCE_LE_UINT32_MAX(grid_x, "grid.x");
PADDLE_ENFORCE_LE_UINT32_MAX(ctas_per_output, "grid.y");
return dim3(static_cast<uint32_t>(grid_x),
static_cast<uint32_t>(ctas_per_output));
}
HOSTDEVICE bool ShouldReduceBlockX() const {
return input_multiplier[BLOCK_X] != 0;
}
HOSTDEVICE bool ShouldReduceBlockY() const {
return input_multiplier[BLOCK_Y] != 0;
}
HOSTDEVICE bool ShouldReduceGlobal() const {
return input_multiplier[CTA] != 0;
}
DEVICE bool ShouldStore(int output_idx) const {
// 1. Boundary Check: Ensure the output index is within the valid range.
// If out of bounds, no storage is necessary.
if (output_idx >= num_outputs) {
return false;
}
// 2. X-Reduction Check: If block-wide X-reduction is active, only the
// thread with index 0 in the X-dimension (the "leader") is allowed to
// store.
if (ShouldReduceBlockX() && threadIdx.x != 0) {
return false;
}
// 3. Y-Reduction Check: If block-wide Y-reduction is active, only the
// thread with index 0 in the Y-dimension (the "leader") is allowed to
// store.
if (ShouldReduceBlockY() && threadIdx.y != 0) {
return false;
}
// If the thread passes all checks, it is the designated thread to store the
// result.
return true;
}
DEVICE bool ShouldReduceTail() const {
return (!ShouldReduceBlockY() || threadIdx.y == 0) &&
(!ShouldReduceGlobal() || blockIdx.y == 0);
}
HOSTDEVICE int GetInIdx() const {
int thread_x = threadIdx.x;
int thread_y = threadIdx.y;
int block_y = blockIdx.y;
return (thread_x * input_multiplier[BLOCK_X] +
thread_y * input_multiplier[BLOCK_Y] +
block_y * input_multiplier[CTA]);
}
template <int kOutputVecSize>
HOSTDEVICE int GetOutIdx() const {
int thread_x = threadIdx.x;
int thread_y = threadIdx.y;
int block_x = blockIdx.x;
return (thread_x * output_multiplier[BLOCK_X] +
thread_y * output_multiplier[BLOCK_Y] + block_x * step_output) *
kOutputVecSize;
}
DEVICE int SharedMemoryOffset(int offset) const {
return threadIdx.x + (threadIdx.y + offset) * blockDim.x;
}
DEVICE int StagingMemoryOffset(int block_y) const {
IndexType offset = block_y + static_cast<IndexType>(blockIdx.x) *
static_cast<IndexType>(gridDim.y);
if (!ShouldReduceBlockX()) {
offset = threadIdx.x + offset * blockDim.x;
}
return offset;
}
int SharedMemorySize() const {
if (!ShouldReduceBlockY() &&
(!ShouldReduceBlockX() || block_width <= WARP_SIZE)) {
return 0;
}
return element_size_bytes * num_threads * output_vec_size;
}
int64_t GlobalMemorySize() const {
if (!ShouldReduceGlobal()) {
return 0;
}
int64_t size = static_cast<int64_t>(element_size_bytes) * num_outputs *
ctas_per_output;
if (!ShouldReduceBlockX()) {
size *= GetBlockDim().x * output_vec_size;
}
return size;
}
size_t SemaphoreSize() const {
if (!ShouldReduceGlobal()) {
return 0;
}
return sizeof(int) * static_cast<size_t>(GetGridDim().x);
}
int ValuesPerThread() const {
int64_t val = phi::backends::gpu::DivUp<int64_t>(num_inputs, step_input);
PADDLE_ENFORCE_LE_INT_MAX(val, "values per thread");
return static_cast<int>(val);
}
};
template <typename MPType,
typename ScalarT,
int kVecSize,
int kInputVecSize = kVecSize>
ReduceConfig SetReduceConfig(const DenseTensorIterator& iter) {
int device_id = paddle::platform::GetCurrentDeviceId();
int64_t num_outputs = iter.num_output_elements();
int64_t inputs_per_output = iter.numel() / num_outputs;
int input_index = iter.ntensors() - 1;
auto config = ReduceConfig(sizeof(MPType), num_outputs, inputs_per_output);
int64_t dim0;
int64_t dim1;
int64_t fastest_moving_stride;
bool reduce_fastest_dim;
if (iter.ndim() > 0) {
// Check if we're reducing along the fastest-changing dimension
// This affects memory access patterns for better performance.
reduce_fastest_dim = (iter.num_reduce_dims() == iter.ndim()) ||
(iter.strides(input_index)[0] <
iter.strides(input_index)[iter.num_reduce_dims()]);
// Set block dimensions based on reduction pattern.
if (reduce_fastest_dim) {
// Reducing along fastest dimension: use block.x for reduction.
// block.x handles reduction elements.
// block.y handles output elements.
dim0 = inputs_per_output;
dim1 = num_outputs;
fastest_moving_stride = iter.strides(input_index)[0];
} else {
// Not reducing along fastest dimension: use block.x for outputs.
// block.x handles output elements.
// block.y handles reduction elements.
dim0 = num_outputs;
dim1 = inputs_per_output;
fastest_moving_stride = iter.strides(input_index)[iter.num_reduce_dims()];
}
} else {
// Handle 0-dimensional case.
reduce_fastest_dim = true;
fastest_moving_stride = sizeof(ScalarT);
dim0 = 1;
dim1 = 1;
}
// Use vectorization for better memory access. Two cases:
// Case 1: "Vectorize along input" - when reducing on fastest dimension,
// data in same vector corresponds to the same output.
// Case 2: "Vectorize along output" - when fastest dimension is not reduced,
// data in same vector corresponds to different outputs.
if (fastest_moving_stride == sizeof(ScalarT)) {
if (reduce_fastest_dim && dim0 >= 128 && iter.num_reduce_dims() == 1) {
// Case 1: Vectorize along input (load data for same output together).
config.vectorize_input = true;
dim0 /= kInputVecSize;
} else if (!reduce_fastest_dim) {
// Case 2: Vectorize along output (load data for multiple outputs
// together).
config.output_vec_size = GetOutputVecSize<ScalarT>(iter);
dim0 /= config.output_vec_size;
}
}
// Adjust block_width and block_height.
config.SetBlockDimensions<ScalarT>(dim0, dim1);
int block_width = config.block_width;
int block_height = config.block_height;
// Level 1 parallelization: split work at thread level.
if (iter.ndim() == 0 || reduce_fastest_dim) {
// Case 1: Split input across threads (requires thread synchronization).
config.input_multiplier[0] = config.SplitInput(block_width);
} else {
// Case 2: Split output across threads (each thread handles different
// output).
config.output_multiplier[0] = config.SplitOutput(block_width);
}
// Min elements per thread.
constexpr int min_values_per_thread = 16;
// Max elements per thread.
constexpr int max_values_per_thread = 256;
// Decide if we need to split work across warps.
const int warp_split_threshold =
std::min<int>(block_height * 16, max_values_per_thread);
bool split_across_warps = config.ValuesPerThread() >= warp_split_threshold;
const int num_mp = paddle::platform::GetGPUMultiProcessors(device_id);
// Level 2 parallelization: split work at warp level.
if (split_across_warps) {
// Case 1: Split input across warps (requires warp synchronization).
config.input_multiplier[1] = config.SplitInput(block_height);
} else {
// Case 2: Each warp handles independent outputs.
config.output_multiplier[1] = config.SplitOutput(block_height);
}
int max_threads_per_mp =
paddle::platform::GetGPUMaxThreadsPerMultiProcessor(device_id);
const int blocks_per_sm = max_threads_per_mp / config.num_threads;
const int target_grid_size = num_mp * blocks_per_sm;
int grid = config.GetGridDim().x;
// Level 3 parallelization: split work at block level (for large datasets).
if (config.input_multiplier[1] != 0 &&
config.ValuesPerThread() >= max_values_per_thread &&
grid <= target_grid_size) {
// Calculate optimal block splitting strategy.
// Based on SM utilization.
const int64_t ctas_per_output1 =
phi::backends::gpu::DivUp<int64_t>(target_grid_size, grid);
// Based on min workload.
const int64_t ctas_per_output2 = phi::backends::gpu::DivUp<int64_t>(
config.ValuesPerThread(), min_values_per_thread);
// Based on max workload.
const int64_t ctas_per_output3 = phi::backends::gpu::DivUp<int64_t>(
config.ValuesPerThread(), max_values_per_thread);
// Choose best splitting strategy to balance parallelism and per-thread
// workload.
const int64_t ctas_per_output = std::max(
std::min(ctas_per_output1, ctas_per_output2), ctas_per_output3);
PADDLE_ENFORCE_LE_INT_MAX(ctas_per_output, "reduce ctas_per_output");
config.ctas_per_output = static_cast<int>(ctas_per_output);
if (config.ctas_per_output > 1) {
// Case 3: Split input across blocks (requires global memory
// synchronization).
config.input_multiplier[2] = config.SplitInput(config.ctas_per_output);
}
}
return config;
}
template <typename ScalarT,
typename ReduceOp,
typename OutScalarT = ScalarT,
int kVecSize = 4,
int kInputVecSize = kVecSize>
struct ReduceExecutor {
using traits = phi::funcs::FunctionTraits<decltype(&ReduceOp::reduce)>;
using MPType =
typename std::decay<typename traits::template arg<0>::type>::type;
using InputCalculator = funcs::OffsetCalculator<1, IndexType>;
using OutputCalculator = funcs::OffsetCalculator<2, IndexType>;
static constexpr bool can_accumulate_in_output =
std::is_convertible_v<MPType, OutScalarT> &&
std::is_convertible_v<OutScalarT, MPType>;
// Core reduction algorithm configuration.
ReduceOp reducer;
ReduceConfig config;
MPType ident;
// Data access calculators for input and output indexing.
InputCalculator input_calc;
OutputCalculator output_calc;
// Data pointers for source, destination, and buffers.
const void* src;
char* dst[2];
void* acc_buf;
void* cta_buf;
// Parallel synchronization primitives.
int* semaphores;
// Runtime state and control flags.
int64_t base_idx;
bool accumulate;
bool final_output;
int noutputs;
ReduceExecutor(ReduceOp reducer,
ReduceConfig config,
MPType ident,
InputCalculator input_calc,
OutputCalculator output_calc,
const void* src,
char* dst0,
std::optional<char*> dst1,
void* acc_buf,
void* cta_buf,
int* semaphores,
int base_idx,
bool accumulate,
bool final_output,
int64_t noutputs)
: reducer(reducer),
config(config),
ident(ident),
input_calc(input_calc),
output_calc(output_calc),
src(src),
acc_buf(acc_buf),
cta_buf(cta_buf),
semaphores(semaphores),
base_idx(base_idx),
accumulate(accumulate),
final_output(final_output),
noutputs(noutputs) {
dst[0] = dst0;
if (dst1.has_value()) {
dst[1] = dst1.value();
}
}
template <int kOutputVecSize>
DEVICE void Run() const {
extern __shared__ char shared_memory[];
IndexType output_idx = config.GetOutIdx<kOutputVecSize>();
IndexType input_idx = config.GetInIdx();
auto base_offsets1 = output_calc.get(output_idx)[1];
using MPTypeVec = std::array<MPType, kOutputVecSize>;
MPTypeVec value;
if (output_idx < config.num_outputs && input_idx < config.num_inputs) {
const ScalarT* input_slice =
(const ScalarT*)((const char*)src + base_offsets1);
value = ThreadReduce<kOutputVecSize>(input_slice);
}
// Run BlockX reduction before BlockY to match PyTorch's accumulation
// order (>= 2.10, PR #165178), keeping broadcast-grad sum reductions
// bitwise-aligned with torch when vectorized loads are used.
if (config.ShouldReduceBlockX()) {
value = BlockXReduce<kOutputVecSize>(value, shared_memory);
}
if (config.ShouldReduceBlockY()) {
value = BlockYReduce<kOutputVecSize>(value, shared_memory);
}
using OutPtrVec = std::array<OutScalarT*, kOutputVecSize>;
using OffsetVec = std::array<IndexType, kOutputVecSize>;
OffsetVec base_offsets;
OutPtrVec out;
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
base_offsets[i] = output_calc.get(output_idx + i)[0];
out[i] = reinterpret_cast<OutScalarT*>(dst[0] + base_offsets[i]);
}
MPTypeVec* acc = nullptr;
if (acc_buf != nullptr) {
size_t numerator = sizeof(MPType);
size_t denominator = sizeof(OutScalarT);
ReduceFraction(&numerator, &denominator);
acc = reinterpret_cast<MPTypeVec*>(
reinterpret_cast<char*>(acc_buf) +
(base_offsets[0] * numerator / denominator));
}
if (config.ShouldReduceGlobal()) {
value = GlobalReduce<kOutputVecSize>(value, acc, shared_memory);
} else if (config.ShouldStore(output_idx)) {
if (acc == nullptr) {
if (accumulate) {
value = AccumulateInOutput<kOutputVecSize, can_accumulate_in_output>(
out, value);
}
if (final_output) {
SetResultsToOutput<kOutputVecSize>(value, base_offsets);
} else {
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
*(out[i]) = GetAccumulatedOutput<can_accumulate_in_output>(
out[i], value[i]);
}
}
} else {
if (accumulate) {
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
value[i] = reducer.reduce((*acc)[i], value[i]);
}
}
if (final_output) {
SetResultsToOutput<kOutputVecSize>(value, base_offsets);
} else {
*acc = value;
}
}
}
}
template <int kOutputVecSize>
DEVICE std::array<MPType, kOutputVecSize> ThreadReduce(
const ScalarT* data) const {
if (config.vectorize_input) {
return {InVectorizedThreadReduceImpl(data)};
} else {
IndexType element_stride = input_calc.strides_[0][0] / sizeof(ScalarT);
bool is_contiguous = (input_calc.dims == 1 && element_stride == 1);
if (is_contiguous) {
return ThreadReduceImpl<kOutputVecSize>(
data, [](IndexType idx) { return idx; });
} else if (input_calc.dims == 1) {
return ThreadReduceImpl<kOutputVecSize>(
data, [&](IndexType idx) { return idx * element_stride; });
} else {
return ThreadReduceImpl<kOutputVecSize>(data, [&](IndexType idx) {
return input_calc.get(idx)[0] / sizeof(ScalarT);
});
}
}
}
DEVICE MPType InVectorizedThreadReduceImpl(const ScalarT* data) const {
IndexType end = config.num_inputs;
MPType value = ident;
constexpr int align_bytes = alignof(AlignedVector<ScalarT, kInputVecSize>);
constexpr int align_elements = align_bytes / sizeof(ScalarT);
int shift = ((uint64_t)data) % align_bytes / sizeof(ScalarT);
if (shift > 0) {
data -= shift;
end += shift;
if (threadIdx.x >= shift && threadIdx.x < align_elements &&
config.ShouldReduceTail()) {
value = reducer.compute(value, LoadData(data + threadIdx.x));
}
end -= align_elements;
data += align_elements;
shift = align_elements - shift;
}
IndexType idx = config.GetInIdx();
const IndexType stride = config.step_input;
std::array<MPType, kInputVecSize> value_list;
value_list[0] = value;
#pragma unroll
for (int i = 1; i < kInputVecSize; i++) {
value_list[i] = ident;
}
using load_t = AlignedVector<ScalarT, kInputVecSize>;
while (idx * kInputVecSize + kInputVecSize - 1 < end) {
const auto values_vec = LoadVector<ScalarT, kInputVecSize>(data, idx);
#pragma unroll
for (IndexType i = 0; i < kInputVecSize; i++) {
value_list[i] = reducer.compute(value_list[i], values_vec.val[i]);
}
idx += stride;
}
// Tile processing.
IndexType tail_start = end - end % kInputVecSize;
if (config.ShouldReduceTail()) {
int idx = tail_start + threadIdx.x;
if (idx < end) {
const auto value = LoadData(data + idx);
value_list[0] = reducer.compute(value_list[0], value);
}
}
#pragma unroll
for (int i = 1; i < kInputVecSize; i++) {
value_list[0] = reducer.reduce(value_list[0], value_list[i]);
}
return value_list[0];
}
template <int kOutputVecSize, typename offset_calc_t>
DEVICE std::array<MPType, kOutputVecSize> ThreadReduceImpl(
const ScalarT* data_, offset_calc_t calc) const {
IndexType idx = config.GetInIdx();
const IndexType end = config.num_inputs;
const IndexType stride = config.step_input;
using MPTypeVec = std::array<MPType, kOutputVecSize>;
using load_t = AlignedVector<ScalarT, kOutputVecSize>;
std::array<MPTypeVec, kVecSize> value_list;
#pragma unroll
for (int i = 0; i < kVecSize; i++) {
#pragma unroll
for (int j = 0; j < kOutputVecSize; j++) {
value_list[i][j] = ident;
}
}
std::array<load_t, kVecSize> values;
while (idx + (kVecSize - 1) * stride < end) {
#pragma unroll
for (IndexType i = 0; i < kVecSize; i++) {
const auto offset = calc(idx + i * stride) / kOutputVecSize;
values[i] = LoadVector<ScalarT, kOutputVecSize>(data_, offset);
}
#pragma unroll
for (IndexType i = 0; i < kVecSize; i++) {
#pragma unroll
for (IndexType j = 0; j < kOutputVecSize; j++) {
value_list[i][j] =
reducer.compute(value_list[i][j], values[i].val[j]);
}
}
idx += stride * kVecSize;
}
// tail
int idx_ = idx;
#pragma unroll
for (IndexType i = 0; i < kVecSize; i++) {
if (idx >= end) {
break;
}
const auto offset = calc(idx) / kOutputVecSize;
values[i] = LoadVector<ScalarT, kOutputVecSize>(data_, offset);
idx += stride;
}
idx = idx_;
#pragma unroll
for (IndexType i = 0; i < kVecSize; i++) {
if (idx >= end) {
break;
}
#pragma unroll
for (IndexType j = 0; j < kOutputVecSize; j++) {
value_list[i][j] = reducer.compute(value_list[i][j], values[i].val[j]);
}
idx += stride;
}
#pragma unroll
for (int i = 1; i < kVecSize; i++) {
#pragma unroll
for (IndexType j = 0; j < kOutputVecSize; j++) {
value_list[0][j] = reducer.reduce(value_list[0][j], value_list[i][j]);
}
}
return value_list[0];
}
template <int kOutputVecSize>
DEVICE std::array<MPType, kOutputVecSize> BlockXReduce(
std::array<MPType, kOutputVecSize> value, char* shared_memory) const {
using MPTypeVec = std::array<MPType, kOutputVecSize>;
int dim_x = blockDim.x;
MPTypeVec* shared = reinterpret_cast<MPTypeVec*>(shared_memory);
unsigned mask = 0u;
CREATE_SHFL_MASK(mask, true);
if (dim_x > WARP_SIZE) {
IndexType address_base = static_cast<IndexType>(threadIdx.x) +
static_cast<IndexType>(threadIdx.y) *
static_cast<IndexType>(blockDim.x);
shared[address_base] = value;
for (int offset = dim_x / 2; offset >= WARP_SIZE; offset >>= 1) {
__syncthreads();
if (threadIdx.x < offset && threadIdx.x + offset < blockDim.x) {
MPTypeVec other = shared[address_base + offset];
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
value[i] = reducer.reduce(value[i], other[i]);
}
shared[address_base] = value;
}
}
dim_x = WARP_SIZE;
}
__syncthreads();
// Intra-warp reduction. Use decreasing offset on NVIDIA to match the
// accumulation order used by PyTorch (>= 2.10, PR #164790) and Triton,
// so that broadcast-grad sum reductions stay bitwise-aligned with torch.
// Keep the increasing-offset order on HIP/ROCm (matches torch's ROCm path).
#if defined(PADDLE_WITH_HIP)
for (int offset = 1; offset < dim_x; offset <<= 1) {
#else
for (int offset = dim_x >> 1; offset > 0; offset >>= 1) {
#endif
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
MPType other = reducer.shfl_sync(mask, value[i], offset);
value[i] = reducer.reduce(value[i], other);
}
}
return value;
}
template <int kOutputVecSize>
DEVICE std::array<MPType, kOutputVecSize> BlockYReduce(
std::array<MPType, kOutputVecSize> value, char* shared_memory) const {
using MPTypeVec = std::array<MPType, kOutputVecSize>;
MPTypeVec* shared = reinterpret_cast<MPTypeVec*>(shared_memory);
shared[config.SharedMemoryOffset(0)] = value;
for (int offset = blockDim.y / 2; offset > 0; offset >>= 1) {
__syncthreads();
if (threadIdx.y < offset && threadIdx.y + offset < blockDim.y) {
MPTypeVec other = shared[config.SharedMemoryOffset(offset)];
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
value[i] = reducer.reduce(value[i], other[i]);
}
shared[config.SharedMemoryOffset(0)] = value;
}
}
return value;
}
DEVICE bool MarkBlockFinished() const {
__shared__ bool is_last_block_done_shared;
__syncthreads();
if (threadIdx.x == 0 && threadIdx.y == 0) {
int prev_blocks_finished = atomicAdd(&semaphores[blockIdx.x], 1);
is_last_block_done_shared = (prev_blocks_finished == gridDim.y - 1);
}
__syncthreads();
return is_last_block_done_shared;
}
template <int kOutputVecSize, bool can_acc>
DEVICE std::array<MPType, kOutputVecSize> AccumulateInOutput(
std::array<OutScalarT*, kOutputVecSize> out,
std::array<MPType, kOutputVecSize> value) const {
if constexpr (can_acc) {
std::array<MPType, kOutputVecSize> ret;
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
ret[i] = reducer.reduce(*(out[i]), value[i]);
}
return ret;
} else {
return {MPType{}};
}
}
template <bool can_acc>
DEVICE OutScalarT GetAccumulatedOutput(OutScalarT* out, MPType value) const {
if constexpr (can_acc) {
return (OutScalarT)value;
} else {
return *out;
}
}
template <class T>
DEVICE void SetResults(const T x, const IndexType base_offset) const {
auto res = reinterpret_cast<OutScalarT*>(dst[0] + base_offset);
*res = x;
}
template <class T1, class T2>
DEVICE void SetResults(const thrust::pair<T1, T2> x,
const IndexType base_offset) const {
if (noutputs >= 1) {
auto res0 = reinterpret_cast<T1*>(dst[0] + base_offset);
*res0 = x.first;
}
if (noutputs >= 2) {
auto res1 =
reinterpret_cast<T2*>(dst[1] + base_offset / sizeof(T1) * sizeof(T2));
*res1 = x.second;
}
}
template <int kOutputVecSize>
DEVICE void SetResultsToOutput(
std::array<MPType, kOutputVecSize> value,
std::array<IndexType, kOutputVecSize> base_offset) const {
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
SetResults(reducer.post_process(value[i]), base_offset[i]);
}
}
template <int kOutputVecSize>
DEVICE std::array<MPType, kOutputVecSize> GlobalReduce(
std::array<MPType, kOutputVecSize> value,
std::array<MPType, kOutputVecSize>* acc,
char* shared_memory) const {
using MPTypeVec = std::array<MPType, kOutputVecSize>;
using OutPtrVec = std::array<OutScalarT*, kOutputVecSize>;
using OffsetVec = std::array<IndexType, kOutputVecSize>;
MPTypeVec* reduce_buffer = reinterpret_cast<MPTypeVec*>(cta_buf);
IndexType output_idx = config.GetOutIdx<kOutputVecSize>();
OffsetVec base_offsets;
OutPtrVec out;
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
base_offsets[i] = output_calc.get(output_idx + i)[0];
out[i] = reinterpret_cast<OutScalarT*>(dst[0] + base_offsets[i]);
}
bool should_store = config.ShouldStore(output_idx);
if (should_store) {
IndexType offset = config.StagingMemoryOffset(blockIdx.y);
reduce_buffer[offset] = value;
}
__threadfence();
__syncthreads();
bool is_last_block_done = MarkBlockFinished();
if (is_last_block_done) {
__threadfence();
for (auto& v : value) {
v = ident;
}
if (config.ShouldReduceBlockX()) {
IndexType input_offset = static_cast<IndexType>(threadIdx.x) +
static_cast<IndexType>(threadIdx.y) *
static_cast<IndexType>(blockDim.x);
IndexType step = static_cast<IndexType>(blockDim.x) *
static_cast<IndexType>(blockDim.y);
for (; input_offset < config.ctas_per_output; input_offset += step) {
IndexType idx = config.StagingMemoryOffset(input_offset);
MPTypeVec next = reduce_buffer[idx];
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
value[i] = reducer.reduce(value[i], next[i]);
}
}
} else {
IndexType input_offset = threadIdx.y;
IndexType step = blockDim.y;
for (; input_offset < config.ctas_per_output; input_offset += step) {
IndexType idx = config.StagingMemoryOffset(input_offset);
MPTypeVec next = reduce_buffer[idx];
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
value[i] = reducer.reduce(value[i], next[i]);
}
}
}
value = BlockYReduce<kOutputVecSize>(value, shared_memory);
if (config.ShouldReduceBlockX()) {
value = BlockXReduce<kOutputVecSize>(value, shared_memory);
}
if (should_store) {
if (acc == nullptr) {
if (accumulate) {
value =
AccumulateInOutput<kOutputVecSize, can_accumulate_in_output>(
out, value);
}
if (final_output) {
SetResultsToOutput<kOutputVecSize>(value, base_offsets);
} else {
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
*(out[i]) = GetAccumulatedOutput<can_accumulate_in_output>(
out[i], value[i]);
}
}
} else {
if (accumulate) {
#pragma unroll
for (int i = 0; i < kOutputVecSize; i++) {
value[i] = reducer.reduce((*acc)[i], value[i]);
}
}
if (final_output) {
SetResultsToOutput<kOutputVecSize>(value, base_offsets);
} else {
*acc = value;
}
}
}
}
return value;
}
}; // NOLINT
class AccumulationBuffer {
public:
AccumulationBuffer() {}
AccumulationBuffer(const KPDevice& dev_ctx,
size_t acc_t_size,
size_t out_t_size,
char* out_ptr,
int64_t size) {
out_ptr_ = reinterpret_cast<char*>(out_ptr);
if (out_t_size >= acc_t_size) {
acc_ptr_ = reinterpret_cast<char*>(out_ptr);
numerator_ = 1;
denominator_ = 1;
} else {
phi::Allocator* allocator =
const_cast<phi::Allocator*>(&(dev_ctx.GetAllocator())); // NOLINT
buffer_ = allocator->Allocate(size);
acc_ptr_ = reinterpret_cast<char*>(buffer_->ptr());
numerator_ = acc_t_size;
denominator_ = out_t_size;
ReduceFraction(&numerator_, &denominator_);
}
}
char* GetAccSlice(char* out_ptr) {
if (acc_ptr_ == nullptr) {
return nullptr;
}
return acc_ptr_ + ((out_ptr - out_ptr_) * numerator_ / denominator_);
}
private:
char* acc_ptr_ = nullptr;
char* out_ptr_ = nullptr;
size_t numerator_;
size_t denominator_;
Allocator::AllocationPtr buffer_;
};
template <int max_threads, typename R>
static void LaunchReduceKernel(const KPDevice& dev_ctx,
const ReduceConfig& config,
const R& reduction) {
dim3 block = config.GetBlockDim();
dim3 grid = config.GetGridDim();
int shared_memory = config.SharedMemorySize();
auto stream = dev_ctx.stream();
switch (config.output_vec_size) {
case 4:
VecReduceKernel<max_threads / 4, 4, R>
<<<grid, block, shared_memory, stream>>>(reduction);
break;
case 2:
VecReduceKernel<max_threads / 2, 2, R>
<<<grid, block, shared_memory, stream>>>(reduction);
break;
default:
VecReduceKernel<max_threads / 1, 1, R>
<<<grid, block, shared_memory, stream>>>(reduction);
break;
}
}
template <typename Tx,
typename Ty,
int kVecSize = 4,
int kInputVecSize = kVecSize,
typename ReduceOp,
typename ident_t = double>
inline void GPUReduceScheduler(const KPDevice& dev_ctx,
const DenseTensorIterator& iter,
const ReduceOp& reducer,
ident_t ident = 0,
AccumulationBuffer* acc_buf_ptr = nullptr,
int64_t base_idx = 0) {
auto stream = dev_ctx.stream();
using traits = phi::funcs::FunctionTraits<decltype(&ReduceOp::reduce)>;
using MPType = typename traits::template arg<0>::type;
static constexpr bool is_inp_out_type_half_or_chalf =
(std::is_same_v<phi::float16, Tx> && std::is_same_v<phi::float16, Ty>) ||
(std::is_same_v<phi::dtype::complex<float16>, Tx> &&
std::is_same_v<phi::dtype::complex<float16>, Ty>);
static constexpr bool is_inp_out_type_bfloat16 =
(std::is_same_v<phi::bfloat16, Tx> && std::is_same_v<phi::bfloat16, Ty>);
static constexpr bool can_accumulate_in_output =
std::is_convertible_v<MPType, Ty> &&
!(is_inp_out_type_half_or_chalf || is_inp_out_type_bfloat16);
bool can_use_32bit_indexing = iter.can_use_32bit_indexing();
std::unique_ptr<AccumulationBuffer> owned_buf_ptr;
if (acc_buf_ptr == NULL) {
if (!can_accumulate_in_output && !can_use_32bit_indexing) {
int64_t output_memory_size = phi::SizeOf(iter.dtype(0));
for (int dim = 0; dim < iter.ndim(); dim++) {
output_memory_size = std::max(output_memory_size,
iter.shape()[dim] * iter.strides(0)[dim]);
}
output_memory_size /= phi::SizeOf(iter.dtype(0));
owned_buf_ptr.reset(
new AccumulationBuffer(dev_ctx,
sizeof(MPType),
sizeof(Ty),
reinterpret_cast<char*>(iter.data_ptr(0)),
output_memory_size * sizeof(MPType)));
} else {
owned_buf_ptr.reset(new AccumulationBuffer());
}
acc_buf_ptr = owned_buf_ptr.get();
}
// Split iter if index exceeds 32-bit range.
if (!can_use_32bit_indexing) {
for (auto& sub_iter : iter.with_32bit_indexing()) {
int64_t sub_iter_base_idx = sub_iter.view_offsets()[0];
GPUReduceScheduler<Tx, Ty, kVecSize, kInputVecSize, ReduceOp>(
dev_ctx, sub_iter, reducer, ident, acc_buf_ptr, sub_iter_base_idx);
}
return;
}
const char* in_data =
reinterpret_cast<const char*>(iter.data_ptr(iter.ntensors() - 1));
char* out_data = reinterpret_cast<char*>(iter.data_ptr(0));
const auto noutputs = iter.noutputs();
std::optional<char*> out_data_extra;
if (noutputs > 1) {
out_data_extra = reinterpret_cast<char*>(iter.data_ptr(1));
} else {
out_data_extra = std::nullopt;
}
char* acc_data = acc_buf_ptr->GetAccSlice(out_data);
ReduceConfig config =
SetReduceConfig<MPType, Tx, kVecSize, kInputVecSize>(iter);
Allocator::AllocationPtr buffer;
Allocator::AllocationPtr semaphores;
void* buffer_ptr;
void* semaphores_ptr;
if (config.ShouldReduceGlobal()) {
phi::Allocator* allocator =
const_cast<phi::Allocator*>(&(dev_ctx.GetAllocator())); // NOLINT
buffer = allocator->Allocate(config.GlobalMemorySize());
semaphores = allocator->Allocate(config.SemaphoreSize());
buffer_ptr = buffer->ptr();
semaphores_ptr = semaphores->ptr();
phi::backends::gpu::GpuMemsetAsync(
semaphores_ptr, 0, config.SemaphoreSize(), stream);
}
auto output_calc = MakeOutputOffsetCalculator<uint32_t>(iter);
auto input_calc = MakeInputOffsetCalculator<uint32_t>(iter);
auto should_accumulate = iter.should_accumulate();
auto is_final_output = iter.is_final_output();
auto reduce = ReduceExecutor<Tx, ReduceOp, Ty, kVecSize, kInputVecSize>(
reducer,
config,
ident,
input_calc,
output_calc,
in_data,
out_data,
out_data_extra,
acc_data,
buffer_ptr,
reinterpret_cast<int*>(semaphores_ptr),
base_idx,
should_accumulate,
is_final_output,
noutputs);
LaunchReduceKernel<MaxThreadsConfig<Tx>::MAX_NUM_THREADS>(
dev_ctx, config, reduce);
return;
}
namespace funcs {
template <typename Tx,
typename Ty,
template <typename, typename, typename>
class ReduceOp>
void ReduceGpuKernel(const KPDevice& dev_ctx,
const phi::DenseTensor& x,
phi::DenseTensor* y,
const std::vector<int>& origin_reduce_dims,
const float norm_p = 1.0f) {
if (x.numel() == 0) {
dev_ctx.Alloc<Ty>(y);
return;
}
dev_ctx.Alloc<Ty>(y);
int64_t ndim = x.dims().size();
auto positive_reduce_dims = ConvertToPositiveDims(origin_reduce_dims, ndim);
auto mask = MakeDimMask(positive_reduce_dims, ndim);
auto viewed_result = ReviewReduceResult(x, *(y), ndim, mask);
auto x_dim = vectorize<int64_t>(x.dims());
DenseTensorIteratorConfig dense_iter_config;
dense_iter_config.is_reduction(true);
dense_iter_config.add_output(viewed_result);
dense_iter_config.add_const_input(x);
DenseTensorIterator iter = dense_iter_config.build();
using MPType = typename MPTypeTrait<Ty>::Type;
// TODO(baoqiwen): When ReduceOp is WelfordOps, kVecSize is 2.
constexpr int kVecSize = 4;
// For 16-bit (fp16/bf16) sum/mean, vectorize input loads by 8 elements
// (DWORDX4) to match PyTorch's accumulation grouping (>= 2.10, PR #165055),
// keeping broadcast-grad sum reductions bitwise-aligned with torch. Other
// dtypes / reduce ops keep the default input vector width.
constexpr bool kIsSumOrMeanOp =
std::is_same_v<ReduceOp<Tx, MPType, Ty>, kps::SumOps<Tx, MPType, Ty>> ||
std::is_same_v<ReduceOp<Tx, MPType, Ty>, kps::MeanOps<Tx, MPType, Ty>>;
constexpr int kInputVecSize =
(kIsSumOrMeanOp && sizeof(Tx) == 2) ? 8 : kVecSize;
// Initialize reducer.
ReduceOp reducer = [&iter, &norm_p]() {
constexpr bool kIsMeanOp =
std::is_same_v<ReduceOp<Tx, MPType, Ty>, kps::MeanOps<Tx, MPType, Ty>>;
constexpr bool kIsPNormOp =
std::is_same_v<ReduceOp<Tx, MPType, Ty>,
kps::GenericPNormOps<Tx, MPType, Ty>>;
if constexpr (kIsMeanOp) {
MPType factor = static_cast<MPType>(iter.num_output_elements()) /
static_cast<MPType>(iter.numel());
return ReduceOp<Tx, MPType, Ty>{factor};
} else if constexpr (kIsPNormOp) {
return ReduceOp<Tx, MPType, Ty>{norm_p};
} else {
return ReduceOp<Tx, MPType, Ty>{};
}
}();
// Initialize ident value.
Tx ident = []() {
if constexpr (std::is_same_v<ReduceOp<Tx, MPType, Ty>,
kps::MaxOps<Tx, MPType, Ty>> ||
std::is_same_v<ReduceOp<Tx, MPType, Ty>,
kps::AbsMaxOps<Tx, MPType, Ty>>) {
return std::numeric_limits<Tx>::lowest();
}
if constexpr (std::is_same_v<ReduceOp<Tx, MPType, Ty>,
kps::MinOps<Tx, MPType, Ty>> ||
std::is_same_v<ReduceOp<Tx, MPType, Ty>,
kps::AbsMinOps<Tx, MPType, Ty>>) {
return std::numeric_limits<Tx>::max();
}
if constexpr (std::is_same_v<ReduceOp<Tx, MPType, Ty>,
kps::LogicalAndOps<Tx, MPType, Ty>>) {
return Tx{1};
}
if constexpr (std::is_same_v<ReduceOp<Tx, MPType, Ty>,
kps::ProdOps<Tx, MPType, Ty>>) {
return Tx{1};
}
// SumOps, MeanOps, LogicalOrOps and others
return Tx{0};
}();
GPUReduceScheduler<Tx, Ty, kVecSize, kInputVecSize, ReduceOp<Tx, MPType, Ty>>(
dev_ctx, iter, reducer, ident);
return;
}
} // namespace funcs
} // namespace phi