// 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. #include "paddle/phi/kernels/group_norm_grad_kernel.h" #include "paddle/common/layout.h" #include "paddle/phi/backends/gpu/gpu_context.h" #include "paddle/phi/backends/gpu/gpu_device_function.h" #include "paddle/phi/core/kernel_registry.h" #include "paddle/phi/kernels/full_kernel.h" #include "paddle/phi/kernels/funcs/math_function.h" #include "paddle/phi/kernels/gpu/group_norm_utils.h" namespace phi { // ============================================================================ // NHWC backward kernels (kept unchanged for NHWC data layout) // ============================================================================ template __global__ void GroupNormBackwardGetMeanAndVar(const T* x, const T* scale, const T* bias, const T* d_y, int64_t N, int64_t C, int64_t W, int64_t imsize, int groups, int64_t group_size, double epsilon, AccT* d_mean, AccT* d_var, T* d_scale, T* d_bias) { int gid = blockIdx.y; for (int64_t cid = blockIdx.x; cid < group_size; cid += gridDim.x) { for (int64_t bid = blockIdx.z; bid < N; bid += gridDim.z) { int64_t H = imsize / W; int64_t number = min(group_size, C - gid * group_size); int64_t ccid = gid * group_size + cid; if (ccid >= C) return; T x_scale = (flags & kHasScale) ? scale[ccid] : static_cast(1); T x_bias = (flags & kHasBias) ? bias[ccid] : static_cast(0); T x_scale_inv = static_cast(0); if (x_scale != static_cast(0)) x_scale_inv = static_cast(1.0) / x_scale; AccT d_mean_data = static_cast(0); AccT d_var_data = static_cast(0); AccT d_scale_data = static_cast(0); AccT d_bias_data = static_cast(0); for (int64_t imid = threadIdx.x; imid < imsize; imid += blockDim.x) { AccT val, dval; int64_t hid = imid / W; int64_t wid = imid % W; val = static_cast(x[(bid * H + hid) * W * C + wid * C + ccid]) - static_cast(x_bias); dval = static_cast(d_y[(bid * H + hid) * W * C + wid * C + ccid]); d_var_data += val * dval; d_mean_data += dval * static_cast(x_scale); val = val * static_cast(x_scale_inv); d_bias_data += dval; d_scale_data += val * dval; } CudaAtomicAddWithWarp(&(d_mean[bid * groups + gid]), static_cast(d_mean_data)); CudaAtomicAddWithWarp(&(d_var[bid * groups + gid]), static_cast(d_var_data)); if (flags & kHasScale) { CudaAtomicAdd(&(d_scale[ccid]), static_cast(d_scale_data)); } if (flags & kHasBias) { CudaAtomicAdd(&(d_bias[ccid]), static_cast(d_bias_data)); } } } } template __global__ void GroupNormBackward(const T* x, const T* d_y, const T* scale, const T* bias, const AccT* var, const AccT* d_mean, const AccT* d_var, int64_t N, int64_t C, int64_t W, int64_t imsize, int groups, int64_t group_size, double epsilon, T* d_x) { int gid = blockIdx.y; for (int64_t cid = blockIdx.x; cid < group_size; cid += gridDim.x) { for (int64_t bid = blockIdx.z; bid < N; bid += gridDim.z) { int64_t H = imsize / W; int64_t number = min(group_size, C - gid * group_size); int64_t ccid = gid * group_size + cid; if (ccid >= C) return; AccT x_var = var[bid * groups + gid]; AccT d_x_mean = static_cast(d_mean[bid * groups + gid]); AccT d_x_var = static_cast(d_var[bid * groups + gid]); AccT x_var_inv = static_cast(1.0) / sqrt((x_var) + epsilon); AccT number_inv = static_cast(1.0) / static_cast((number * imsize)); AccT x_scale = (flags & kHasScale) ? static_cast(scale[ccid]) : static_cast(1); AccT x_bias = (flags & kHasBias) ? static_cast(bias[ccid]) : static_cast(0); AccT x_scale_inv = static_cast(0); if (x_scale != static_cast(0)) x_scale_inv = static_cast(1.0) / x_scale; for (int64_t imid = threadIdx.x; imid < imsize; imid += blockDim.x) { int64_t hid = imid / W; int64_t wid = imid % W; AccT tmp = static_cast(x[(bid * H + hid) * W * C + wid * C + ccid]); AccT v_y = (tmp - x_bias) * x_scale_inv; AccT dly = static_cast(d_y[(bid * H + hid) * W * C + wid * C + ccid]); d_x[(bid * H + hid) * W * C + wid * C + ccid] = static_cast(x_var_inv * ((dly) * (x_scale)-number_inv * d_x_var * (v_y)-number_inv * d_x_mean)); } } } } // ============================================================================ // PyTorch-aligned NCHW backward implementation // ============================================================================ // Constant matching PyTorch's kCUDABlockReduceNumThreads constexpr int kGradBlockReduceNumThreads = 512; constexpr int kGradReduceTileSize = 32; // Warp reduce sum using shuffle-down (matching PyTorch's WarpReduceSum) template __device__ __forceinline__ AccT GradWarpReduceSum(AccT val) { #pragma unroll for (int offset = warpSize / 2; offset > 0; offset >>= 1) { val += backends::gpu::CudaShuffleDownSync(0xffffffff, val, offset); } return val; } // Block reduce sum (matching PyTorch's BlockReduceSum) template __device__ __forceinline__ AccT GradBlockReduceSum(AccT val, AccT* shared) { const int tid = threadIdx.x; const int lid = tid % warpSize; const int wid = tid / warpSize; const int num_warps = blockDim.x / warpSize; val = GradWarpReduceSum(val); __syncthreads(); if (lid == 0) { shared[wid] = val; } __syncthreads(); val = (tid < num_warps) ? shared[lid] : AccT(0); if (wid == 0) { val = GradWarpReduceSum(val); } return val; } // Stage 1: Compute internal gradients ds[n,c] and db[n,c] // ds[nc] = sum_hw(dY * X), db[nc] = sum_hw(dY) // Grid: N*C blocks. Matches PyTorch's ComputeInternalGradientsCUDAKernel. template __global__ void ComputeInternalGradientsCUDAKernel( int64_t HxW, const T* dY, const T* X, AccT* ds, AccT* db) { const int64_t nc = blockIdx.x; AccT sum1 = 0; AccT sum2 = 0; for (int64_t hw = threadIdx.x; hw < HxW; hw += blockDim.x) { const int64_t index = nc * HxW + hw; sum1 += static_cast(dY[index]) * static_cast(X[index]); sum2 += static_cast(dY[index]); } if (blockDim.x <= static_cast(warpSize)) { sum1 = GradWarpReduceSum(sum1); sum2 = GradWarpReduceSum(sum2); } else { constexpr int kMaxWarps = 32; __shared__ AccT ds_shared[kMaxWarps]; __shared__ AccT db_shared[kMaxWarps]; sum1 = GradBlockReduceSum(sum1, ds_shared); sum2 = GradBlockReduceSum(sum2, db_shared); } if (threadIdx.x == 0) { ds[nc] = sum1; db[nc] = sum2; } } // Stage 1 vectorized: float4 version for T=float, HxW divisible by 4 // Processes 4 elements per thread per iteration for better memory throughput. template __global__ void ComputeInternalGradientsVec4CUDAKernel( int64_t HxW_vec, const T* dY, const T* X, AccT* ds, AccT* db) { const int64_t nc = blockIdx.x; AccT sum1 = 0; AccT sum2 = 0; const float4* dY_vec = reinterpret_cast(dY + nc * HxW_vec * 4); const float4* X_vec = reinterpret_cast(X + nc * HxW_vec * 4); for (int64_t i = threadIdx.x; i < HxW_vec; i += blockDim.x) { float4 dy4 = dY_vec[i]; float4 x4 = X_vec[i]; sum1 += static_cast(dy4.x) * static_cast(x4.x); sum1 += static_cast(dy4.y) * static_cast(x4.y); sum1 += static_cast(dy4.z) * static_cast(x4.z); sum1 += static_cast(dy4.w) * static_cast(x4.w); sum2 += static_cast(dy4.x); sum2 += static_cast(dy4.y); sum2 += static_cast(dy4.z); sum2 += static_cast(dy4.w); } if (blockDim.x <= static_cast(warpSize)) { sum1 = GradWarpReduceSum(sum1); sum2 = GradWarpReduceSum(sum2); } else { constexpr int kMaxWarps = 32; __shared__ AccT ds_shared[kMaxWarps]; __shared__ AccT db_shared[kMaxWarps]; sum1 = GradBlockReduceSum(sum1, ds_shared); sum2 = GradBlockReduceSum(sum2, db_shared); } if (threadIdx.x == 0) { ds[nc] = sum1; db[nc] = sum2; } } // Stage 1 vectorized: double2 version for T=double, HxW divisible by 2 template __global__ void ComputeInternalGradientsVec2DoubleCUDAKernel( int64_t HxW_vec, const T* dY, const T* X, AccT* ds, AccT* db) { const int64_t nc = blockIdx.x; AccT sum1 = 0; AccT sum2 = 0; const double2* dY_vec = reinterpret_cast(dY + nc * HxW_vec * 2); const double2* X_vec = reinterpret_cast(X + nc * HxW_vec * 2); for (int64_t i = threadIdx.x; i < HxW_vec; i += blockDim.x) { double2 dy2 = dY_vec[i]; double2 x2 = X_vec[i]; sum1 += static_cast(dy2.x) * static_cast(x2.x); sum1 += static_cast(dy2.y) * static_cast(x2.y); sum2 += static_cast(dy2.x); sum2 += static_cast(dy2.y); } if (blockDim.x <= static_cast(warpSize)) { sum1 = GradWarpReduceSum(sum1); sum2 = GradWarpReduceSum(sum2); } else { constexpr int kMaxWarps = 32; __shared__ AccT ds_shared[kMaxWarps]; __shared__ AccT db_shared[kMaxWarps]; sum1 = GradBlockReduceSum(sum1, ds_shared); sum2 = GradBlockReduceSum(sum2, db_shared); } if (threadIdx.x == 0) { ds[nc] = sum1; db[nc] = sum2; } } // Stage 2: Compute backward fused params c2[n,g] and c3[n,g] // Reduces ds*gamma and db*gamma across channels in group. // Grid: dim3(N, G). Matches PyTorch's ComputeBackwardFusedParamsCUDAKernel. // Rounds mean/rstd through T to match PyTorch's precision behavior. template __global__ void ComputeBackwardFusedParamsCUDAKernel(int64_t C, int64_t HxW, int64_t G, const AccT* mean, const AccT* var, const T* gamma, const AccT* ds, const AccT* db, AccT eps, AccT* c2, AccT* c3) { const int64_t D = C / G; const int64_t n = blockIdx.x; const int64_t g = blockIdx.y; const int64_t ng = n * G + g; AccT sum1 = 0; AccT sum2 = 0; for (int64_t i = threadIdx.x; i < D; i += blockDim.x) { const int64_t index = ng * D + i; const int64_t c_idx = g * D + i; const AccT gamma_v = gamma == nullptr ? AccT(1) : static_cast(gamma[c_idx]); sum1 += ds[index] * gamma_v; sum2 += db[index] * gamma_v; } if (blockDim.x <= static_cast(warpSize)) { sum1 = GradWarpReduceSum(sum1); sum2 = GradWarpReduceSum(sum2); } else { constexpr int kMaxWarps = 32; __shared__ AccT ds_shared[kMaxWarps]; __shared__ AccT db_shared[kMaxWarps]; sum1 = GradBlockReduceSum(sum1, ds_shared); sum2 = GradBlockReduceSum(sum2, db_shared); } if (threadIdx.x == 0) { // Round mean and rstd through T to match PyTorch AccT mean_val = static_cast(static_cast(mean[ng])); AccT rstd_val = static_cast(static_cast(rsqrt(var[ng] + eps))); const AccT s = AccT(1) / static_cast(D * HxW); const AccT x = (sum2 * mean_val - sum1) * rstd_val * rstd_val * rstd_val * s; c2[ng] = x; c3[ng] = -x * mean_val - sum2 * rstd_val * s; } } // ============================================================================ // Optimized dX kernels for NCHW backward path // ============================================================================ // Stage 3: Pre-compute c1[n,c] = rstd[n,g] * gamma[c] // This removes per-element rsqrt and gamma lookup from the inner dX loop. // Grid: ceil(N*C / 256) blocks. // Rounds rstd through T to match PyTorch's precision behavior. template __global__ void ComputeC1CUDAKernel(int64_t N_C, int64_t C, int64_t G, AccT eps, const AccT* var, const T* gamma, AccT* c1) { const int64_t D = C / G; for (int64_t index = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; index < N_C; index += static_cast(gridDim.x) * blockDim.x) { const int64_t ng = index / D; const int64_t c = index % C; // Round rstd through T to match PyTorch AccT rstd_val = static_cast(static_cast(rsqrt(var[ng] + eps))); AccT gamma_val = (gamma != nullptr) ? static_cast(gamma[c]) : AccT(1); c1[index] = rstd_val * gamma_val; } } // Stage 4 optimized: Vectorized dX kernel using float4 // dX[idx] = c1[nc] * dY[idx] + c2[ng] * X[idx] + c3[ng] // Processes 4 float elements at a time within each (n,c) spatial plane. template __global__ void GroupNormBackwardDxVec4CUDAKernel(int64_t N_C, int64_t HxW_vec, int64_t D, const T* __restrict__ dY, const T* __restrict__ X, const AccT* __restrict__ c1, const AccT* __restrict__ c2, const AccT* __restrict__ c3, T* __restrict__ dX) { const int64_t total_vec = N_C * HxW_vec; for (int64_t vidx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; vidx < total_vec; vidx += static_cast(gridDim.x) * blockDim.x) { const int64_t nc = vidx / HxW_vec; const int64_t ng = nc / D; const AccT c1v = c1[nc]; const AccT c2v = c2[ng]; const AccT c3v = c3[ng]; // Vectorized load: 4 consecutive float elements float4 dy4 = reinterpret_cast(dY)[vidx]; float4 x4 = reinterpret_cast(X)[vidx]; float4 dx4; dx4.x = static_cast(c1v * static_cast(dy4.x) + c2v * static_cast(x4.x) + c3v); dx4.y = static_cast(c1v * static_cast(dy4.y) + c2v * static_cast(x4.y) + c3v); dx4.z = static_cast(c1v * static_cast(dy4.z) + c2v * static_cast(x4.z) + c3v); dx4.w = static_cast(c1v * static_cast(dy4.w) + c2v * static_cast(x4.w) + c3v); reinterpret_cast(dX)[vidx] = dx4; } } // Stage 4 optimized: Vectorized dX kernel using double2 for float64 template __global__ void GroupNormBackwardDxVec2DoubleCUDAKernel( int64_t N_C, int64_t HxW_vec, int64_t D, const T* __restrict__ dY, const T* __restrict__ X, const AccT* __restrict__ c1, const AccT* __restrict__ c2, const AccT* __restrict__ c3, T* __restrict__ dX) { const int64_t total_vec = N_C * HxW_vec; for (int64_t vidx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; vidx < total_vec; vidx += static_cast(gridDim.x) * blockDim.x) { const int64_t nc = vidx / HxW_vec; const int64_t ng = nc / D; const AccT c1v = c1[nc]; const AccT c2v = c2[ng]; const AccT c3v = c3[ng]; // Vectorized load: 2 consecutive double elements double2 dy2 = reinterpret_cast(dY)[vidx]; double2 x2 = reinterpret_cast(X)[vidx]; double2 dx2; dx2.x = static_cast(c1v * static_cast(dy2.x) + c2v * static_cast(x2.x) + c3v); dx2.y = static_cast(c1v * static_cast(dy2.y) + c2v * static_cast(x2.y) + c3v); reinterpret_cast(dX)[vidx] = dx2; } } // Stage 4 optimized: Scalar fallback for when HxW is not divisible by vec_size // Uses pre-computed c1[n,c]. template __global__ void GroupNormBackwardDxOptCUDAKernel(int64_t N_C, int64_t HxW, int64_t D, const T* __restrict__ dY, const T* __restrict__ X, const AccT* __restrict__ c1, const AccT* __restrict__ c2, const AccT* __restrict__ c3, T* __restrict__ dX) { const int64_t total = N_C * HxW; for (int64_t idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; idx < total; idx += static_cast(gridDim.x) * blockDim.x) { const int64_t nc = idx / HxW; const int64_t ng = nc / D; dX[idx] = static_cast(c1[nc] * static_cast(dY[idx]) + c2[ng] * static_cast(X[idx]) + c3[ng]); } } // Stage 4 optimized: Vectorized no-gamma dX kernel using float4 // When gamma is null, c1 = rstd[ng], constant across D*HxW per (n,g) group. template __global__ void GroupNormBackwardDxNoGammaVec4CUDAKernel( int64_t N_G, int64_t D_HxW_vec, const T* __restrict__ dY, const T* __restrict__ X, const AccT* __restrict__ c1_ng, const AccT* __restrict__ c2, const AccT* __restrict__ c3, T* __restrict__ dX) { const int64_t total_vec = N_G * D_HxW_vec; for (int64_t vidx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; vidx < total_vec; vidx += static_cast(gridDim.x) * blockDim.x) { const int64_t ng = vidx / D_HxW_vec; const AccT rstd_val = c1_ng[ng]; const AccT c2v = c2[ng]; const AccT c3v = c3[ng]; float4 dy4 = reinterpret_cast(dY)[vidx]; float4 x4 = reinterpret_cast(X)[vidx]; float4 dx4; dx4.x = static_cast(rstd_val * static_cast(dy4.x) + c2v * static_cast(x4.x) + c3v); dx4.y = static_cast(rstd_val * static_cast(dy4.y) + c2v * static_cast(x4.y) + c3v); dx4.z = static_cast(rstd_val * static_cast(dy4.z) + c2v * static_cast(x4.z) + c3v); dx4.w = static_cast(rstd_val * static_cast(dy4.w) + c2v * static_cast(x4.w) + c3v); reinterpret_cast(dX)[vidx] = dx4; } } // Stage 4 optimized: Vectorized no-gamma dX kernel using double2 template __global__ void GroupNormBackwardDxNoGammaVec2DoubleCUDAKernel( int64_t N_G, int64_t D_HxW_vec, const T* __restrict__ dY, const T* __restrict__ X, const AccT* __restrict__ c1_ng, const AccT* __restrict__ c2, const AccT* __restrict__ c3, T* __restrict__ dX) { const int64_t total_vec = N_G * D_HxW_vec; for (int64_t vidx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; vidx < total_vec; vidx += static_cast(gridDim.x) * blockDim.x) { const int64_t ng = vidx / D_HxW_vec; const AccT rstd_val = c1_ng[ng]; const AccT c2v = c2[ng]; const AccT c3v = c3[ng]; double2 dy2 = reinterpret_cast(dY)[vidx]; double2 x2 = reinterpret_cast(X)[vidx]; double2 dx2; dx2.x = static_cast(rstd_val * static_cast(dy2.x) + c2v * static_cast(x2.x) + c3v); dx2.y = static_cast(rstd_val * static_cast(dy2.y) + c2v * static_cast(x2.y) + c3v); reinterpret_cast(dX)[vidx] = dx2; } } // Stage 4 optimized: Scalar no-gamma dX using pre-computed rstd template __global__ void GroupNormBackwardDxNoGammaOptCUDAKernel( int64_t N_G, int64_t D_HxW, const T* __restrict__ dY, const T* __restrict__ X, const AccT* __restrict__ c1_ng, const AccT* __restrict__ c2, const AccT* __restrict__ c3, T* __restrict__ dX) { const int64_t total = N_G * D_HxW; for (int64_t idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; idx < total; idx += static_cast(gridDim.x) * blockDim.x) { const int64_t ng = idx / D_HxW; const AccT rstd_val = c1_ng[ng]; dX[idx] = static_cast(rstd_val * static_cast(dY[idx]) + c2[ng] * static_cast(X[idx]) + c3[ng]); } } // Pre-compute rstd values rounded through T for the no-gamma path. // c1_ng[ng] = round_through_T(rsqrt(var[ng] + eps)) template __global__ void ComputeRstdCUDAKernel(int64_t N_G, AccT eps, const AccT* var, AccT* c1_ng) { for (int64_t index = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; index < N_G; index += static_cast(gridDim.x) * blockDim.x) { c1_ng[index] = static_cast(static_cast(rsqrt(var[index] + eps))); } } // ============================================================================ // End of optimized dX kernels // ============================================================================ // dgamma/dbeta variant 1: small batch (N <= 128) // Per-channel thread, loop over batch dimension. // Matches PyTorch's GammaBetaBackwardCUDAKernel1. // Rounds mean/rstd through T to match PyTorch. template __global__ void GammaBetaBackwardCUDAKernel1(int64_t N, int64_t C, int64_t G, const AccT* mean, const AccT* var, const AccT* ds, const AccT* db, AccT eps, T* dgamma, T* dbeta) { const int64_t c = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; if (c < C) { const int64_t D = C / G; AccT sum1 = 0; AccT sum2 = 0; for (int64_t n = 0; n < N; ++n) { const int64_t nc = n * C + c; const int64_t ng = n * G + c / D; // Round through T to match PyTorch AccT mean_val = static_cast(static_cast(mean[ng])); AccT rstd_val = static_cast(static_cast(rsqrt(var[ng] + eps))); sum1 += (dgamma == nullptr) ? AccT(0) : ((ds[nc] - db[nc] * mean_val) * rstd_val); sum2 += (dbeta == nullptr) ? AccT(0) : db[nc]; } if (dgamma != nullptr) { dgamma[c] = static_cast(sum1); } if (dbeta != nullptr) { dbeta[c] = static_cast(sum2); } } } // dgamma/dbeta variant 2: large batch (N > 128) // 32x32 tile with shared memory. // Matches PyTorch's GammaBetaBackwardCUDAKernel2. // Rounds mean/rstd through T to match PyTorch. template __global__ void GammaBetaBackwardCUDAKernel2(int64_t N, int64_t C, int64_t G, const AccT* mean, const AccT* var, const AccT* ds, const AccT* db, AccT eps, T* dgamma, T* dbeta) { __shared__ AccT g_shared[kGradReduceTileSize][kGradReduceTileSize + 1]; __shared__ AccT b_shared[kGradReduceTileSize][kGradReduceTileSize + 1]; const int64_t c = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; AccT dg_sum1 = 0; AccT dg_sum2 = 0; AccT db_sum1 = 0; AccT db_sum2 = 0; if (c < C) { const int64_t D = C / G; for (int64_t n = threadIdx.y; n < N; n += blockDim.y * 2) { const int64_t n1 = n; const int64_t n2 = n + blockDim.y; const int64_t nc1 = n1 * C + c; const int64_t nc2 = n2 * C + c; const int64_t ng1 = n1 * G + c / D; const int64_t ng2 = n2 * G + c / D; // Round through T to match PyTorch AccT mean_val1 = static_cast(static_cast(mean[ng1])); AccT rstd1 = static_cast(static_cast(rsqrt(var[ng1] + eps))); dg_sum1 += dgamma == nullptr ? AccT(0) : ((ds[nc1] - db[nc1] * mean_val1) * rstd1); db_sum1 += dbeta == nullptr ? AccT(0) : db[nc1]; if (n2 < N) { AccT mean_val2 = static_cast(static_cast(mean[ng2])); AccT rstd2 = static_cast(static_cast(rsqrt(var[ng2] + eps))); dg_sum2 += dgamma == nullptr ? AccT(0) : ((ds[nc2] - db[nc2] * mean_val2) * rstd2); db_sum2 += dbeta == nullptr ? AccT(0) : db[nc2]; } } } g_shared[threadIdx.y][threadIdx.x] = dg_sum1; g_shared[threadIdx.y + blockDim.y][threadIdx.x] = dg_sum2; b_shared[threadIdx.y][threadIdx.x] = db_sum1; b_shared[threadIdx.y + blockDim.y][threadIdx.x] = db_sum2; __syncthreads(); // Warp reduce for 1st 16 cols in tile AccT sum1 = g_shared[threadIdx.x][threadIdx.y]; AccT sum2 = b_shared[threadIdx.x][threadIdx.y]; sum1 = GradWarpReduceSum(sum1); sum2 = GradWarpReduceSum(sum2); if (threadIdx.x == 0) { const int64_t c_out = blockIdx.x * blockDim.x + threadIdx.y; if (c_out < C) { if (dgamma != nullptr) { dgamma[c_out] = static_cast(sum1); } if (dbeta != nullptr) { dbeta[c_out] = static_cast(sum2); } } } // Warp reduce for 2nd 16 cols in tile sum1 = g_shared[threadIdx.x][threadIdx.y + blockDim.y]; sum2 = b_shared[threadIdx.x][threadIdx.y + blockDim.y]; sum1 = GradWarpReduceSum(sum1); sum2 = GradWarpReduceSum(sum2); if (threadIdx.x == 0) { const int64_t c_out = blockIdx.x * blockDim.x + threadIdx.y + blockDim.y; if (c_out < C) { if (dgamma != nullptr) { dgamma[c_out] = static_cast(sum1); } if (dbeta != nullptr) { dbeta[c_out] = static_cast(sum2); } } } } // ============================================================================ // Main backward kernel // ============================================================================ template void GroupNormGradKernel(const Context& dev_ctx, const DenseTensor& x, const optional& scale, const optional& bias, const DenseTensor& y, const DenseTensor& mean, const DenseTensor& var, const DenseTensor& d_y, double epsilon, int groups, const std::string& data_layout_str, DenseTensor* d_x, DenseTensor* d_scale, DenseTensor* d_bias) { if (x.numel() == 0) { dev_ctx.template Alloc(d_x); if (d_scale) { if (x.dims().size() > 0 && x.dims()[0] == 0) { Full(dev_ctx, d_scale->dims(), 0, d_scale); } else { Full(dev_ctx, d_scale->dims(), NAN, d_scale); } } if (d_bias) { Full(dev_ctx, d_bias->dims(), 0, d_bias); } return; } using AccT = typename MPTypeTrait::Type; const DataLayout data_layout = StringToDataLayout(data_layout_str); const auto scale_ptr = scale.get_ptr(); const auto bias_ptr = bias.get_ptr(); const auto& x_dims = x.dims(); const int64_t N = x_dims[0]; const int64_t C = (data_layout == DataLayout::NCHW ? x_dims[1] : x_dims[x_dims.size() - 1]); const int64_t group_size = C / groups; const int64_t W = (data_layout == DataLayout::NCHW ? x_dims[x_dims.size() - 1] : x_dims[x_dims.size() - 2]); const int64_t G = groups; const int64_t D = C / G; if (d_x) { dev_ctx.template Alloc(d_x); } funcs::SetConstant set_zero; funcs::SetConstant set_zero_AccT; auto* x_data = x.data(); T* d_x_data = nullptr; if (d_x) d_x_data = d_x->data(); auto* dy_data = d_y.data(); auto* var_data = var.data(); auto* mean_data = mean.data(); T* d_scale_data = nullptr; if (d_scale) { dev_ctx.template Alloc(d_scale); d_scale_data = d_scale->data(); } T* d_bias_data = nullptr; if (d_bias) { dev_ctx.template Alloc(d_bias); d_bias_data = d_bias->data(); } const T* scale_data = nullptr; if (scale_ptr) scale_data = scale_ptr->data(); const T* bias_data = nullptr; if (bias_ptr) bias_data = bias_ptr->data(); int64_t imsize = 1; if (data_layout == DataLayout::NCHW) { for (int i = 2; i < x_dims.size(); ++i) { imsize *= x_dims[i]; } } else { for (int i = 1; i < x_dims.size() - 1; ++i) { imsize *= x_dims[i]; } } if (data_layout == DataLayout::NCHW) { // ========================================================= // PyTorch-aligned NCHW backward path (optimized) // ========================================================= const int64_t HxW = imsize; // Stage 1: Compute internal gradients ds[n,c] = sum_hw(dY*X), // db[n,c] = sum_hw(dY) DenseTensor ds_tensor, db_tensor; ds_tensor.Resize({N, C}); db_tensor.Resize({N, C}); AccT* ds_data = dev_ctx.template Alloc(&ds_tensor); AccT* db_data = dev_ctx.template Alloc(&db_tensor); { // Use scalar kernel to match PyTorch's exact accumulation order // (Vec4 changes thread-to-element mapping, causing 1-2 ULP differences) { int64_t num_threads = HxW < kGradBlockReduceNumThreads ? 32 : kGradBlockReduceNumThreads; ComputeInternalGradientsCUDAKernel <<>>( HxW, dy_data, x_data, ds_data, db_data); } } // Stage 2+3+4: Compute dX if (d_x_data != nullptr) { DenseTensor c2_tensor, c3_tensor; c2_tensor.Resize({N, G}); c3_tensor.Resize({N, G}); AccT* c2_data = dev_ctx.template Alloc(&c2_tensor); AccT* c3_data = dev_ctx.template Alloc(&c3_tensor); // Stage 2: Compute backward fused params c2, c3 { int64_t num_threads = D < kGradBlockReduceNumThreads ? 32 : kGradBlockReduceNumThreads; ComputeBackwardFusedParamsCUDAKernel <<>>( C, HxW, G, mean_data, var_data, scale_data, ds_data, db_data, static_cast(epsilon), c2_data, c3_data); } // Stage 4: dX = c1 * dY + c2 * X + c3 (optimized) int64_t max_grid_x = dev_ctx.GetCUDAMaxGridDimSize()[0]; constexpr int64_t kElemThreads = 256; if (scale_data != nullptr) { // Pre-compute c1[n,c] = rstd[n,g] * gamma[c] // This eliminates per-element rsqrt and gamma lookup. DenseTensor c1_tensor; c1_tensor.Resize({N, C}); AccT* c1_data = dev_ctx.template Alloc(&c1_tensor); { const int64_t N_C = N * C; const int64_t c1_blocks = std::min((N_C + kElemThreads - 1) / kElemThreads, max_grid_x); ComputeC1CUDAKernel <<>>( N_C, C, G, static_cast(epsilon), var_data, scale_data, c1_data); } // Dispatch vectorized or scalar dX kernel if (std::is_same::value && (HxW % 4 == 0)) { const int64_t HxW_vec = HxW / 4; const int64_t total_vec = N * C * HxW_vec; const int64_t elem_blocks = std::min( (total_vec + kElemThreads - 1) / kElemThreads, max_grid_x); GroupNormBackwardDxVec4CUDAKernel <<>>(N * C, HxW_vec, D, dy_data, x_data, c1_data, c2_data, c3_data, d_x_data); } else if (std::is_same::value && (HxW % 2 == 0)) { const int64_t HxW_vec = HxW / 2; const int64_t total_vec = N * C * HxW_vec; const int64_t elem_blocks = std::min( (total_vec + kElemThreads - 1) / kElemThreads, max_grid_x); GroupNormBackwardDxVec2DoubleCUDAKernel <<>>(N * C, HxW_vec, D, dy_data, x_data, c1_data, c2_data, c3_data, d_x_data); } else { // Scalar fallback with pre-computed c1 const int64_t total = N * C * HxW; const int64_t elem_blocks = std::min((total + kElemThreads - 1) / kElemThreads, max_grid_x); GroupNormBackwardDxOptCUDAKernel <<>>(N * C, HxW, D, dy_data, x_data, c1_data, c2_data, c3_data, d_x_data); } } else { // No gamma path: c1 = rstd[ng], pre-compute rstd rounded through T DenseTensor c1_ng_tensor; c1_ng_tensor.Resize({N, G}); AccT* c1_ng_data = dev_ctx.template Alloc(&c1_ng_tensor); { const int64_t N_G = N * G; const int64_t rstd_blocks = std::min((N_G + kElemThreads - 1) / kElemThreads, max_grid_x); ComputeRstdCUDAKernel <<>>( N_G, static_cast(epsilon), var_data, c1_ng_data); } const int64_t D_HxW = D * HxW; if (std::is_same::value && (D_HxW % 4 == 0)) { const int64_t D_HxW_vec = D_HxW / 4; const int64_t total_vec = N * G * D_HxW_vec; const int64_t elem_blocks = std::min( (total_vec + kElemThreads - 1) / kElemThreads, max_grid_x); GroupNormBackwardDxNoGammaVec4CUDAKernel <<>>(N * G, D_HxW_vec, dy_data, x_data, c1_ng_data, c2_data, c3_data, d_x_data); } else if (std::is_same::value && (D_HxW % 2 == 0)) { const int64_t D_HxW_vec = D_HxW / 2; const int64_t total_vec = N * G * D_HxW_vec; const int64_t elem_blocks = std::min( (total_vec + kElemThreads - 1) / kElemThreads, max_grid_x); GroupNormBackwardDxNoGammaVec2DoubleCUDAKernel <<>>(N * G, D_HxW_vec, dy_data, x_data, c1_ng_data, c2_data, c3_data, d_x_data); } else { // Scalar fallback with pre-computed rstd const int64_t total = N * G * D_HxW; const int64_t elem_blocks = std::min((total + kElemThreads - 1) / kElemThreads, max_grid_x); GroupNormBackwardDxNoGammaOptCUDAKernel <<>>(N * G, D_HxW, dy_data, x_data, c1_ng_data, c2_data, c3_data, d_x_data); } } } // Stage 5: dgamma/dbeta if (d_scale || d_bias) { if (N <= 128) { constexpr int kNumThreads = 256; const int64_t B = (C + kNumThreads - 1) / kNumThreads; GammaBetaBackwardCUDAKernel1 <<>>( N, C, G, mean_data, var_data, ds_data, db_data, static_cast(epsilon), d_scale_data, d_bias_data); } else { const int64_t B = (C + kGradReduceTileSize - 1) / kGradReduceTileSize; constexpr int kThreadX = kGradReduceTileSize; constexpr int kThreadY = kGradReduceTileSize / 2; GammaBetaBackwardCUDAKernel2 <<>>( N, C, G, mean_data, var_data, ds_data, db_data, static_cast(epsilon), d_scale_data, d_bias_data); } } } else { // ========================================================= // NHWC backward path (kept unchanged) // ========================================================= int block_size = std::min(static_cast(1024), imsize); int64_t max_grid_x = dev_ctx.GetCUDAMaxGridDimSize()[0]; int64_t max_grid_z = dev_ctx.GetCUDAMaxGridDimSize()[2]; dim3 grid( std::min(max_grid_x, group_size), groups, std::min(max_grid_z, N)); dim3 threads(block_size, 1, 1); auto* y_data = y.data(); if (d_scale) { set_zero(dev_ctx, d_scale, static_cast(0)); } if (d_bias) { set_zero(dev_ctx, d_bias, static_cast(0)); } DenseTensor temp_var; temp_var.Resize(var.dims()); dev_ctx.template Alloc(&temp_var); set_zero_AccT(dev_ctx, &temp_var, static_cast(0)); auto* temp_var_data = temp_var.data(); DenseTensor temp_mean; temp_mean.Resize(var.dims()); dev_ctx.template Alloc(&temp_mean); set_zero_AccT(dev_ctx, &temp_mean, static_cast(0)); auto* temp_mean_data = temp_mean.data(); int flags = (scale_data != nullptr) * kHasScale + (bias_data != nullptr) * kHasBias; UNROLL_ALL_CASES(flags, GroupNormBackwardGetMeanAndVar, y_data, scale_data, bias_data, dy_data, N, C, W, imsize, groups, group_size, epsilon, temp_mean_data, temp_var_data, d_scale_data, d_bias_data); if (d_x_data != nullptr) { UNROLL_ALL_CASES(flags, GroupNormBackward, y_data, dy_data, scale_data, bias_data, var_data, temp_mean_data, temp_var_data, N, C, W, imsize, groups, group_size, epsilon, d_x_data); } } } } // namespace phi PD_REGISTER_KERNEL(group_norm_grad, GPU, ALL_LAYOUT, phi::GroupNormGradKernel, float, double, phi::bfloat16, phi::float16) {}