// 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 #include #include #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 struct LoadImpl { HOSTDEVICE static T Apply(const void* src) { return *reinterpret_cast(src); } }; template <> struct LoadImpl { HOSTDEVICE static bool Apply(const void* src) { static_assert(sizeof(bool) == sizeof(char)); return *reinterpret_cast(src); } }; template HOSTDEVICE constexpr T LoadData(const void* src) { return LoadImpl::Apply(src); } template HOSTDEVICE constexpr ScalarT LoadData(const ScalarT* src) { return LoadImpl::Apply(src); } namespace phi { inline std::bitset<64> DimListToBitset(std::vector 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 ConvertToPositiveDims( const std::vector& origin_reduce_dims, int64_t ndim) { std::vector 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 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 shape; std::vector 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 DEVICE AlignedVector LoadVector(const T* base_ptr, uint32_t offset) { using vec_t = AlignedVector; auto* from = reinterpret_cast(base_ptr); return from[offset]; } template DEVICE AlignedVector LoadVector(const bool* base_ptr, uint32_t offset) { auto tmp = LoadVector( reinterpret_cast(base_ptr), offset); AlignedVector ret; for (int i = 0; i < Size; ++i) { ret.val[i] = static_cast(tmp.val[i]); } return ret; } // Chose max num threads. template struct MaxThreadsConfig { static constexpr int MAX_NUM_THREADS = 512; }; template <> struct MaxThreadsConfig> { static constexpr int MAX_NUM_THREADS = 256; }; template __launch_bounds__(kNumThreads, 4) __global__ void VecReduceKernel(Reducer reduction) { reduction.template Run(); } template 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 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 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 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 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(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 void SetBlockDimensions(int64_t dim0, int64_t dim1) { const int max_num_threads = MaxThreadsConfig::MAX_NUM_THREADS / output_vec_size; int dim0_pow2 = (dim0 < max_num_threads) ? static_cast(phi::backends::gpu::GetLastPow2(dim0)) : max_num_threads; int dim1_pow2 = (dim1 < max_num_threads) ? static_cast(phi::backends::gpu::GetLastPow2(dim1)) : max_num_threads; block_width = std::min(dim0_pow2, WARP_SIZE); block_height = std::min(dim1_pow2, static_cast(max_num_threads / block_width)); block_width = std::min(dim0_pow2, static_cast(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(step_input) * parallelism; PADDLE_ENFORCE_LE_INT_MAX(next_step, "reduce step_input"); step_input = static_cast(next_step); return current_step; } int SplitOutput(int parallelism) { const int current_step = step_output; const int64_t next_step = static_cast(step_output) * parallelism; PADDLE_ENFORCE_LE_INT_MAX(next_step, "reduce step_output"); step_output = static_cast(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( 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(grid_x), static_cast(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 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(blockIdx.x) * static_cast(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(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(GetGridDim().x); } int ValuesPerThread() const { int64_t val = phi::backends::gpu::DivUp(num_inputs, step_input); PADDLE_ENFORCE_LE_INT_MAX(val, "values per thread"); return static_cast(val); } }; template 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(iter); dim0 /= config.output_vec_size; } } // Adjust block_width and block_height. config.SetBlockDimensions(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(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(target_grid_size, grid); // Based on min workload. const int64_t ctas_per_output2 = phi::backends::gpu::DivUp( config.ValuesPerThread(), min_values_per_thread); // Based on max workload. const int64_t ctas_per_output3 = phi::backends::gpu::DivUp( 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(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 struct ReduceExecutor { using traits = phi::funcs::FunctionTraits; using MPType = typename std::decay::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 && std::is_convertible_v; // 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 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 DEVICE void Run() const { extern __shared__ char shared_memory[]; IndexType output_idx = config.GetOutIdx(); IndexType input_idx = config.GetInIdx(); auto base_offsets1 = output_calc.get(output_idx)[1]; using MPTypeVec = std::array; 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(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(value, shared_memory); } if (config.ShouldReduceBlockY()) { value = BlockYReduce(value, shared_memory); } using OutPtrVec = std::array; using OffsetVec = std::array; 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(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( reinterpret_cast(acc_buf) + (base_offsets[0] * numerator / denominator)); } if (config.ShouldReduceGlobal()) { value = GlobalReduce(value, acc, shared_memory); } else if (config.ShouldStore(output_idx)) { if (acc == nullptr) { if (accumulate) { value = AccumulateInOutput( out, value); } if (final_output) { SetResultsToOutput(value, base_offsets); } else { #pragma unroll for (int i = 0; i < kOutputVecSize; i++) { *(out[i]) = GetAccumulatedOutput( 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(value, base_offsets); } else { *acc = value; } } } } template DEVICE std::array 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( data, [](IndexType idx) { return idx; }); } else if (input_calc.dims == 1) { return ThreadReduceImpl( data, [&](IndexType idx) { return idx * element_stride; }); } else { return ThreadReduceImpl(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); 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 value_list; value_list[0] = value; #pragma unroll for (int i = 1; i < kInputVecSize; i++) { value_list[i] = ident; } using load_t = AlignedVector; while (idx * kInputVecSize + kInputVecSize - 1 < end) { const auto values_vec = LoadVector(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 DEVICE std::array 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; using load_t = AlignedVector; std::array 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 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(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(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 DEVICE std::array BlockXReduce( std::array value, char* shared_memory) const { using MPTypeVec = std::array; int dim_x = blockDim.x; MPTypeVec* shared = reinterpret_cast(shared_memory); unsigned mask = 0u; CREATE_SHFL_MASK(mask, true); if (dim_x > WARP_SIZE) { IndexType address_base = static_cast(threadIdx.x) + static_cast(threadIdx.y) * static_cast(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 DEVICE std::array BlockYReduce( std::array value, char* shared_memory) const { using MPTypeVec = std::array; MPTypeVec* shared = reinterpret_cast(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 DEVICE std::array AccumulateInOutput( std::array out, std::array value) const { if constexpr (can_acc) { std::array ret; #pragma unroll for (int i = 0; i < kOutputVecSize; i++) { ret[i] = reducer.reduce(*(out[i]), value[i]); } return ret; } else { return {MPType{}}; } } template DEVICE OutScalarT GetAccumulatedOutput(OutScalarT* out, MPType value) const { if constexpr (can_acc) { return (OutScalarT)value; } else { return *out; } } template DEVICE void SetResults(const T x, const IndexType base_offset) const { auto res = reinterpret_cast(dst[0] + base_offset); *res = x; } template DEVICE void SetResults(const thrust::pair x, const IndexType base_offset) const { if (noutputs >= 1) { auto res0 = reinterpret_cast(dst[0] + base_offset); *res0 = x.first; } if (noutputs >= 2) { auto res1 = reinterpret_cast(dst[1] + base_offset / sizeof(T1) * sizeof(T2)); *res1 = x.second; } } template DEVICE void SetResultsToOutput( std::array value, std::array base_offset) const { #pragma unroll for (int i = 0; i < kOutputVecSize; i++) { SetResults(reducer.post_process(value[i]), base_offset[i]); } } template DEVICE std::array GlobalReduce( std::array value, std::array* acc, char* shared_memory) const { using MPTypeVec = std::array; using OutPtrVec = std::array; using OffsetVec = std::array; MPTypeVec* reduce_buffer = reinterpret_cast(cta_buf); IndexType output_idx = config.GetOutIdx(); 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(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(threadIdx.x) + static_cast(threadIdx.y) * static_cast(blockDim.x); IndexType step = static_cast(blockDim.x) * static_cast(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(value, shared_memory); if (config.ShouldReduceBlockX()) { value = BlockXReduce(value, shared_memory); } if (should_store) { if (acc == nullptr) { if (accumulate) { value = AccumulateInOutput( out, value); } if (final_output) { SetResultsToOutput(value, base_offsets); } else { #pragma unroll for (int i = 0; i < kOutputVecSize; i++) { *(out[i]) = GetAccumulatedOutput( 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(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(out_ptr); if (out_t_size >= acc_t_size) { acc_ptr_ = reinterpret_cast(out_ptr); numerator_ = 1; denominator_ = 1; } else { phi::Allocator* allocator = const_cast(&(dev_ctx.GetAllocator())); // NOLINT buffer_ = allocator->Allocate(size); acc_ptr_ = reinterpret_cast(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 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 <<>>(reduction); break; case 2: VecReduceKernel <<>>(reduction); break; default: VecReduceKernel <<>>(reduction); break; } } template 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; using MPType = typename traits::template arg<0>::type; static constexpr bool is_inp_out_type_half_or_chalf = (std::is_same_v && std::is_same_v) || (std::is_same_v, Tx> && std::is_same_v, Ty>); static constexpr bool is_inp_out_type_bfloat16 = (std::is_same_v && std::is_same_v); static constexpr bool can_accumulate_in_output = std::is_convertible_v && !(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 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(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( dev_ctx, sub_iter, reducer, ident, acc_buf_ptr, sub_iter_base_idx); } return; } const char* in_data = reinterpret_cast(iter.data_ptr(iter.ntensors() - 1)); char* out_data = reinterpret_cast(iter.data_ptr(0)); const auto noutputs = iter.noutputs(); std::optional out_data_extra; if (noutputs > 1) { out_data_extra = reinterpret_cast(iter.data_ptr(1)); } else { out_data_extra = std::nullopt; } char* acc_data = acc_buf_ptr->GetAccSlice(out_data); ReduceConfig config = SetReduceConfig(iter); Allocator::AllocationPtr buffer; Allocator::AllocationPtr semaphores; void* buffer_ptr; void* semaphores_ptr; if (config.ShouldReduceGlobal()) { phi::Allocator* allocator = const_cast(&(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(iter); auto input_calc = MakeInputOffsetCalculator(iter); auto should_accumulate = iter.should_accumulate(); auto is_final_output = iter.is_final_output(); auto reduce = ReduceExecutor( reducer, config, ident, input_calc, output_calc, in_data, out_data, out_data_extra, acc_data, buffer_ptr, reinterpret_cast(semaphores_ptr), base_idx, should_accumulate, is_final_output, noutputs); LaunchReduceKernel::MAX_NUM_THREADS>( dev_ctx, config, reduce); return; } namespace funcs { template class ReduceOp> void ReduceGpuKernel(const KPDevice& dev_ctx, const phi::DenseTensor& x, phi::DenseTensor* y, const std::vector& origin_reduce_dims, const float norm_p = 1.0f) { if (x.numel() == 0) { dev_ctx.Alloc(y); return; } dev_ctx.Alloc(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(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::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, kps::SumOps> || std::is_same_v, kps::MeanOps>; constexpr int kInputVecSize = (kIsSumOrMeanOp && sizeof(Tx) == 2) ? 8 : kVecSize; // Initialize reducer. ReduceOp reducer = [&iter, &norm_p]() { constexpr bool kIsMeanOp = std::is_same_v, kps::MeanOps>; constexpr bool kIsPNormOp = std::is_same_v, kps::GenericPNormOps>; if constexpr (kIsMeanOp) { MPType factor = static_cast(iter.num_output_elements()) / static_cast(iter.numel()); return ReduceOp{factor}; } else if constexpr (kIsPNormOp) { return ReduceOp{norm_p}; } else { return ReduceOp{}; } }(); // Initialize ident value. Tx ident = []() { if constexpr (std::is_same_v, kps::MaxOps> || std::is_same_v, kps::AbsMaxOps>) { return std::numeric_limits::lowest(); } if constexpr (std::is_same_v, kps::MinOps> || std::is_same_v, kps::AbsMinOps>) { return std::numeric_limits::max(); } if constexpr (std::is_same_v, kps::LogicalAndOps>) { return Tx{1}; } if constexpr (std::is_same_v, kps::ProdOps>) { return Tx{1}; } // SumOps, MeanOps, LogicalOrOps and others return Tx{0}; }(); GPUReduceScheduler>( dev_ctx, iter, reducer, ident); return; } } // namespace funcs } // namespace phi