// Copyright (c) 2025 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 #include #include #include "paddle/phi/kernels/funcs/math_cuda_utils.h" namespace phi { // ============================================================================ // Compile-time constants for MoE permute/unpermute kernels // ============================================================================ namespace moe { // Smallest power-of-2 >= v. (v must be > 0) inline constexpr int ceil_pow2(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; return v + 1; } inline constexpr int kCumsumBlockSize = 40; inline constexpr int kCumsumInvalidTag = -1; inline constexpr int kMaxNumExperts = 384; inline constexpr int kMaxNumExpertsForOptKernel = 32; // FP8-specific tuning knobs for permute_generic_kernel. // FP8 has ~2x lighter memcpy than BF16, shifting the bottleneck to Phase-1 // scheduling and inter-block cumsum sync. Tune these independently from BF16. // kFp8CumsumBlockSize : rows per block (32 enables warp-ballot // optimization) kFp8BlockDimX : threads per block (tune range: 128 .. // 512) inline constexpr int kFp8CumsumBlockSize = 32; inline constexpr int kFp8BlockDimX = 256; // Unified permute kernel constants (always warp-ballot based) inline constexpr int kPermuteBlockSize = 32; // rows per block = warp size inline constexpr int kPermuteBlockDimX = 256; // threads per block } // namespace moe // ============================================================================ // Dispatch utilities: runtime num_experts -> compile-time bucket // ============================================================================ namespace dispatch { // Bucketed NUM_EXPERTS dispatch: selects the smallest compile-time bucket // >= runtime num_experts to minimize register / shared-memory overhead. // Buckets: 8, 16, 32, 64, 128, 256, 384 template inline void NumExperts(int num_experts, F&& f) { if (num_experts <= 8) { f(std::integral_constant{}); } else if (num_experts <= 16) { f(std::integral_constant{}); } else if (num_experts <= 32) { f(std::integral_constant{}); } else if (num_experts <= 64) { f(std::integral_constant{}); } else if (num_experts <= 128) { f(std::integral_constant{}); } else if (num_experts <= 256) { f(std::integral_constant{}); } else { f(std::integral_constant{}); } } // Type tag for compile-time type passing template struct TypeTag { using type = T; }; // Runtime bool -> compile-time std::bool_constant template inline auto Bool(bool v, F&& f) { return v ? f(std::true_type{}) : f(std::false_type{}); } // Multi-bool dispatch: flattens nested conditionals template inline auto Bools(F&& f) { return f(); } // Recursive and variadic decay. template inline auto Bools(F&& f, bool first, Rest... rest) { return Bool(first, [&](auto tag) { return Bools([&](auto... tags) { return f(tag, tags...); }, rest...); }); } // Token type dispatch: dtype -> (TokenT, has_scale) template inline void TokenType(DataType dtype, F&& f) { if (dtype == DataType::BFLOAT16) { f(TypeTag{}, std::false_type{}); } else if (dtype == DataType::FLOAT8_E4M3FN) { f(TypeTag{}, std::true_type{}); } } // Probability type dispatch template inline void ProbType(DataType dtype, F&& f) { if (dtype == DataType::BFLOAT16) { f(TypeTag{}); } else if (dtype == DataType::FLOAT32) { f(TypeTag{}); } } // Scale type dispatch template inline void ScaleType(bool using_ue8m0, F&& f) { if (using_ue8m0) { f(TypeTag{}); } else { f(TypeTag{}); } } // Bucketed TOPK dispatch: compile-time topk for shared memory sizing. // Buckets: 1, 2, 4, 8, 16 template inline void TopK(int topk, F&& f) { if (topk <= 1) { f(std::integral_constant{}); } else if (topk <= 2) { f(std::integral_constant{}); } else if (topk <= 4) { f(std::integral_constant{}); } else if (topk <= 8) { f(std::integral_constant{}); } else { f(std::integral_constant{}); } } } // namespace dispatch // ============================================================================ // Type defs // ============================================================================ template struct ExpertSlotInfo { int row_idx; ProbT prob; __device__ __host__ ExpertSlotInfo() : row_idx(-1), prob(ProbT(0)) {} __device__ __host__ ExpertSlotInfo(int idx, ProbT p) : row_idx(idx), prob(p) {} __device__ __host__ ExpertSlotInfo& operator=(const ExpertSlotInfo& other) { row_idx = other.row_idx; prob = other.prob; return *this; } }; // Compact per-token-expert slot for the unified permute kernel. // Stores only topk entries per row instead of num_experts entries. template struct CompactSlot { int output_row; int expert_id; ProbT prob; __device__ __host__ CompactSlot() : output_row(-1), expert_id(-1), prob(ProbT(0)) {} __device__ __host__ CompactSlot(int row, int eid, ProbT p) : output_row(row), expert_id(eid), prob(p) {} }; template struct TypeMap; template <> struct TypeMap { using type = phi::bfloat16; }; template <> struct TypeMap { using type = phi::float16; }; template <> struct TypeMap { using type = float; }; template <> struct TypeMap { using type = int; }; template <> struct TypeMap { using type = int64_t; }; template struct alignas(16) VectorType { T data[N]; }; template <> struct alignas(16) VectorType { float4 data; // Built-in CUDA vector type }; template <> struct alignas(16) VectorType<__nv_bfloat16, 8> { __nv_bfloat16 data[8]; }; template <> struct alignas(16) VectorType<__nv_fp8_e4m3, 16> { __nv_fp8_e4m3 data[16]; }; template <> struct alignas(16) VectorType { uint8_t data[16]; }; // ============================================================================ // Helper functions // ============================================================================ __host__ __device__ __forceinline__ int32_t align_up(int32_t x, int32_t alignment) { return ((x + alignment - 1) / alignment) * alignment; } template __device__ __forceinline__ void unrolled_memcpy(const T* src, T* dst, const int num_elements) { #pragma unroll for (int idx = threadIdx.x; idx < num_elements; idx += blockDim.x) { dst[idx] = src[idx]; } } // Helper function to perform vectorized memory copy template __device__ __forceinline__ void vectorized_memcpy(const T* src, T* dst, const int num_elements) { constexpr int vector_size_in_bytes = VecSizeInBytes; const int elements_per_vector = vector_size_in_bytes / sizeof(T); int num_vectors = num_elements / elements_per_vector; int remaining_elements = num_elements % elements_per_vector; using VecType = VectorType; const VecType* src_vec = reinterpret_cast(src); VecType* dst_vec = reinterpret_cast(dst); #pragma unroll for (int idx = threadIdx.x; idx < num_vectors; idx += blockDim.x) { dst_vec[idx] = src_vec[idx]; } if (remaining_elements > 0) { int offset = num_vectors * elements_per_vector; for (int i = threadIdx.x; i < remaining_elements; i += blockDim.x) { dst[offset + i] = src[offset + i]; } } } static inline bool is_aligned_in_bytes(std::size_t offset, std::size_t alignment = 16) { return (offset & (alignment - 1)) == 0; } template __device__ __forceinline__ void try_vectorized_memcpy(const T* src, T* dst, const int num_elements) { bool is_aligned_128bit = ((uintptr_t)src & 0xF) == 0 && ((uintptr_t)dst & 0xF) == 0; if (is_aligned_128bit) { vectorized_memcpy(src, dst, num_elements); } else { unrolled_memcpy(src, dst, num_elements); } } template __device__ __forceinline__ void unrolled_memset(T* ptr, T value, int num_elements) { #pragma unroll for (int i = threadIdx.x; i < num_elements; i += blockDim.x) { ptr[i] = value; } } template __device__ __forceinline__ void vectorized_memset(T* ptr, const T value, const int num_elements) { constexpr int vector_size_in_bytes = VecSizeInBytes; const int elements_per_vector = vector_size_in_bytes / sizeof(T); int num_vectors = num_elements / elements_per_vector; int remaining_elements = num_elements % elements_per_vector; using VecType = VectorType; VecType vec_value; #pragma unroll for (int i = 0; i < elements_per_vector; i++) { vec_value.data[i] = value; } VecType* ptr_vec = reinterpret_cast(ptr); #pragma unroll for (int idx = threadIdx.x; idx < num_vectors; idx += blockDim.x) { ptr_vec[idx] = vec_value; } if (remaining_elements > 0) { int offset = num_vectors * elements_per_vector; for (int i = threadIdx.x; i < remaining_elements; i += blockDim.x) { ptr[offset + i] = value; } } } // ============================================================================ // Helper Kernels // ============================================================================ // Helper kernel for filling padding rows in pre-training circumstances, // to prevent illegal padding area participating in split matmul. template __global__ __launch_bounds__(512) void filling_padding_rows_kernel( TokenT* __restrict__ X_unzipped_ptr, ScaleT* __restrict__ XScale_unzipped_ptr, float* __restrict__ token_prob_unzipped_ptr, int* __restrict__ expert_indices_ptr, const int cols, const int quanted_cols, const int* __restrict__ padding_rows) { int64_t rows = static_cast(padding_rows[blockIdx.x]); if constexpr (FILLING_X_UNZIPPED) { vectorized_memset( &X_unzipped_ptr[rows * cols], static_cast(0), cols); } if constexpr (FILLING_X_SCALE_UNZIPPED) { unrolled_memset(&XScale_unzipped_ptr[rows * quanted_cols], static_cast(0), quanted_cols); } if (threadIdx.x == 0) { token_prob_unzipped_ptr[rows] = static_cast(0.0); if constexpr (FILLING_EXPERT_INDICES) { expert_indices_ptr[rows] = -1; } } } // Optimized routemap_digest_kernel — single-block design. // The bulk -1 fill of expert_indices is offloaded to cudaMemsetAsync (DMA // engine) BEFORE this kernel launches. The kernel only needs to: // Phase 1: Histogram topk_ids into per-expert counts // Phase 2: Padded exclusive prefix-sum → expert_offset / expert_offset_end // Phase 3: Sparse overwrite of expert_indices for valid-token positions only // // Shared memory layout: [hist: num_experts] [padded_count: num_experts] template __global__ void routemap_digest_kernel(const int32_t* __restrict__ topk_ids, int32_t* __restrict__ expert_offset, int32_t* __restrict__ expert_offset_end, int32_t* __restrict__ expert_indices, int32_t numel, int32_t num_experts, int32_t padding_alignment) { extern __shared__ int32_t shared[]; int32_t* hist = shared; // [0, ne) int32_t* padded_count_smem = shared + num_experts; // [ne, 2*ne) // ===== Phase 1: Histogram ===== for (int i = threadIdx.x; i < num_experts; i += BLOCK_SIZE) hist[i] = 0; __syncthreads(); // Vectorized int4 loads: each thread processes 4 int32s per iteration const int num_vec4 = numel >> 2; const int4* topk_vec4 = reinterpret_cast(topk_ids); for (int i = threadIdx.x; i < num_vec4; i += BLOCK_SIZE) { int4 vec = topk_vec4[i]; int32_t elems[4] = {vec.x, vec.y, vec.z, vec.w}; #pragma unroll for (int k = 0; k < 4; k++) { int32_t expert_id = elems[k]; if (expert_id >= 0 && expert_id < num_experts) atomicAdd(&hist[expert_id], 1); } } // Scalar tail for (int i = (num_vec4 << 2) + threadIdx.x; i < numel; i += BLOCK_SIZE) { int32_t expert_id = topk_ids[i]; if (expert_id >= 0 && expert_id < num_experts) atomicAdd(&hist[expert_id], 1); } __syncthreads(); // ===== Phase 2: Padded exclusive prefix-sum ===== // Step 2a: Compute padded_count per expert in parallel for (int i = threadIdx.x; i < num_experts; i += BLOCK_SIZE) { padded_count_smem[i] = align_up(hist[i], padding_alignment); } __syncthreads(); // Step 2b: Serial prefix-sum on thread 0 (128 experts — trivial cost). // For 128-384 experts the serial loop is <0.1μs; a parallel scan would // add overhead from syncthreads and is not worthwhile here. if (threadIdx.x == 0) { int32_t running_offset = 0; for (int i = 0; i < num_experts; i++) { int32_t count = hist[i]; int32_t padded = padded_count_smem[i]; // read before overwrite expert_offset[i] = running_offset; expert_offset_end[i] = running_offset + count - 1; // Reuse hist[] → offset, padded_count_smem[] → count for Phase 3 hist[i] = running_offset; padded_count_smem[i] = count; running_offset += padded; } } if constexpr (!FillExpertIndices) return; __syncthreads(); // ===== Phase 3: Sparse fill of expert_indices (valid positions only) ===== // The entire buffer was pre-filled with -1 by cudaMemsetAsync. // Here we only overwrite the [offset, offset+count) range for each expert // that has count > 0. With 96 tokens across 128 experts, this is ~96 // int32 stores — negligible compared to the 10K-500K DMA fill. // // All data comes from smem (hist[] = offset, padded_count_smem[] = count), // avoiding global memory loads in the tight loop. for (int e = threadIdx.x; e < num_experts; e += BLOCK_SIZE) { int32_t off = hist[e]; // start offset (smem) int32_t count = padded_count_smem[e]; // token count (smem) if (count <= 0) continue; // Vectorized fill: pack expert_id into int4 for 128-bit stores. // Requires 16-byte alignment (off must be multiple of 4 int32s). // padding_alignment is typically >=8, so offsets are always aligned. if ((off & 3) == 0) { int4 fill_vec; fill_vec.x = e; fill_vec.y = e; fill_vec.z = e; fill_vec.w = e; int4* dst_vec = reinterpret_cast(&expert_indices[off]); int num_vec4_fill = count >> 2; for (int v = 0; v < num_vec4_fill; v++) { dst_vec[v] = fill_vec; } // Scalar tail int filled = num_vec4_fill << 2; for (int j = filled; j < count; j++) { expert_indices[off + j] = e; } } else { // Unaligned fallback (should rarely happen) for (int j = 0; j < count; j++) { expert_indices[off + j] = e; } } } } } // namespace phi