#include "AttentionExecution.hpp" #include "core/TensorUtils.hpp" #include "SoftmaxExecution.hpp" namespace MNN { namespace CUDA { #ifdef MNN_SUPPORT_TRANSFORMER_FUSE // 从 MNN Tensor 获取 CUDA 设备指针的辅助函数 template // 默认为 void* static inline T* getTensorDevicePtr(const Tensor* tensor) { if (!tensor || tensor->deviceId() == 0) return nullptr; return reinterpret_cast(tensor->deviceId()); } // 根据 reserve 列表紧凑化 KV Cache __global__ void compact_kv_cache_kernel( const void* src_key_cache, const void* src_value_cache, void* dst_key_cache, void* dst_value_cache, const int* reserve_info, // GPU上的 [begin0, len0, begin1, len1, ...] 数组 const int* reserve_offsets, // GPU上的 [0, len0, len0+len1, ...] 前缀和数组 int n_reserve_pairs, int past_kv_len_after_remove, // 移除了末尾token后的长度 int b, int h_kv, int d, int src_kv_cache_max_len, // Value Cache 源的 L 维度步长 int dst_kv_cache_max_len, // Value Cache 目标的 L 维度步长 size_t element_size ) { // 每个线程负责一个 (b, h_kv, d) 组合下,一个 reserve 片段的拷贝 int bhd_idx = blockIdx.x * blockDim.x + threadIdx.x; int reserve_pair_idx = blockIdx.y * blockDim.y + threadIdx.y; if (bhd_idx >= b * h_kv * d || reserve_pair_idx >= n_reserve_pairs) { return; } int b_idx = bhd_idx / (h_kv * d); int h_kv_idx = (bhd_idx % (h_kv * d)) / d; int d_idx = bhd_idx % d; int copy_src_begin = reserve_info[reserve_pair_idx * 2]; int copy_len = reserve_info[reserve_pair_idx * 2 + 1]; int copy_dst_begin_offset = reserve_offsets[reserve_pair_idx]; // 在 reserve 区域内的偏移 int src_offset = past_kv_len_after_remove + copy_src_begin; int dst_offset = past_kv_len_after_remove + copy_dst_begin_offset; const uint8_t* src_k_ptr = static_cast(src_key_cache); uint8_t* dst_k_ptr = static_cast(dst_key_cache); const uint8_t* src_v_ptr = static_cast(src_value_cache); uint8_t* dst_v_ptr = static_cast(dst_value_cache); for (int l = 0; l < copy_len; ++l) { // Key: [L, B, H, D] int k_src_idx = (src_offset + l) * b * h_kv * d + b_idx * h_kv * d + h_kv_idx * d + d_idx; int k_dst_idx = (dst_offset + l) * b * h_kv * d + b_idx * h_kv * d + h_kv_idx * d + d_idx; memcpy(dst_k_ptr + k_dst_idx * element_size, src_k_ptr + k_src_idx * element_size, element_size); // Value: [B, H, L, D] int v_src_idx = ((b_idx * h_kv + h_kv_idx) * src_kv_cache_max_len + (src_offset + l)) * d + d_idx; int v_dst_idx = ((b_idx * h_kv + h_kv_idx) * dst_kv_cache_max_len + (dst_offset + l)) * d + d_idx; memcpy(dst_v_ptr + v_dst_idx * element_size, src_v_ptr + v_src_idx * element_size, element_size); } } // 将新 K 和 V 分别复制到对应 Cache 之后 template __global__ void copy_kv_to_cache_kernel( const T* key_input, // 形状: B, L_k_new, H_kv, D const T* value_input, // 形状: B, L_k_new, H_kv, D T* key_cache_output, // 形状: L_kv_alloc, B, H_kv, D T* value_cache_output, // 形状: B, H_kv, L_kv_alloc, D int batch_size, int new_kv_seq_len, // L_k_new int kv_num_head, // H_kv int head_dim, // D int past_kv_len, // L_k_past int allocated_kv_len // L_kv_alloc ) { // 每个线程负责拷贝一个元素 int d_idx = blockIdx.x * blockDim.x + threadIdx.x; int l_idx_new = blockIdx.y * blockDim.y + threadIdx.y; int bh_kv_idx = blockIdx.z * blockDim.z + threadIdx.z; if (d_idx >= head_dim || l_idx_new >= new_kv_seq_len || bh_kv_idx >= batch_size * kv_num_head) { return; } int b_idx = bh_kv_idx / kv_num_head; int h_kv_idx = bh_kv_idx % kv_num_head; // 输入 K 和 V 的源索引 (假设输入布局为 [B, L_k_new, H_kv, D]) int input_offset = b_idx * new_kv_seq_len * kv_num_head * head_dim + l_idx_new * kv_num_head * head_dim + h_kv_idx * head_dim; T val_to_copy_k = key_input[input_offset + d_idx]; T val_to_copy_v = value_input[input_offset + d_idx]; // 从相同偏移读取,因为K和V输入形状相同 // Cache 中的目标序列索引 (在 past_kv_len 之后追加) int dest_seq_idx_cache = past_kv_len + l_idx_new; if (dest_seq_idx_cache >= allocated_kv_len) return; // 边界检查 // Key Cache 输出: [L_kv_alloc, B, H_kv, D] int key_cache_idx = dest_seq_idx_cache * batch_size * kv_num_head * head_dim + b_idx * kv_num_head * head_dim + h_kv_idx * head_dim + d_idx; key_cache_output[key_cache_idx] = val_to_copy_k; // Value Cache 输出: [B, H_kv, L_kv_alloc, D] int value_cache_idx = b_idx * kv_num_head * allocated_kv_len * head_dim + h_kv_idx * allocated_kv_len * head_dim + dest_seq_idx_cache * head_dim + d_idx; value_cache_output[value_cache_idx] = val_to_copy_v; } // ===================================================================== // P0: Flash Decoding Kernel for decode stage (seq_len=1) // - Each block handles one (batch, q_head) pair // - Tiles along KV sequence dimension with online softmax // - Warp-level reductions for QK dot product // - No O(n^2) intermediate buffers needed // ===================================================================== // Flash Decoding: each block computes one output head vector // Supports GQA: multiple Q heads can share one KV head // Uses online softmax (no intermediate QK buffer) template __global__ void flash_decode_kernel( const T* __restrict__ query_input, // [B, 1, H_q, D] const T* __restrict__ key_cache, // [L_kv_alloc, B, H_kv, D] const T* __restrict__ value_cache, // [B, H_kv, L_kv_alloc, D] T* __restrict__ output, // [B, 1, H_q, D] const int batch, const int head_num, // H_q const int kv_head_num, // H_kv const int head_dim, // D const int key_seq_len, // total KV length const int max_kv_len, // allocated KV length (stride for value cache) const float scale ) { // Each block: one (batch, head_q) pair const int bh_idx = blockIdx.x; const int b_idx = bh_idx / head_num; const int h_q_idx = bh_idx % head_num; const int h_kv_idx = h_q_idx / (head_num / kv_head_num); const int tid = threadIdx.x; const int WARP_SIZE = 32; const int warp_id = tid / WARP_SIZE; const int lane_id = tid % WARP_SIZE; const int num_warps = blockDim.x / WARP_SIZE; if (b_idx >= batch) return; // Load query vector into registers // Query: [B, 1, H_q, D] -> q[b_idx, 0, h_q_idx, :] const T* q_ptr = query_input + b_idx * head_num * head_dim + h_q_idx * head_dim; // Each thread loads multiple D elements // For head_dim=128 and 128 threads: each thread loads 1 element // For head_dim=128 and 256 threads: use multiple elements per thread across tiles // Shared memory for partial results across warps extern __shared__ char smem_raw[]; float* smem_max = reinterpret_cast(smem_raw); // [num_warps] float* smem_sum = smem_max + num_warps; // [num_warps] float* smem_out = smem_sum + num_warps; // [num_warps * head_dim] // Online softmax state: track running max and sum per thread float thread_max = -1e20f; float thread_sum = 0.0f; // Accumulate output per thread: each thread is responsible for a subset of D // We accumulate O in registers for the D dims this thread owns float thread_out[8]; // max 8 D elements per thread (head_dim=256, 32 threads per warp) const int d_per_thread = (head_dim + blockDim.x - 1) / blockDim.x; for (int i = 0; i < d_per_thread && i < 8; i++) thread_out[i] = 0.0f; // Tile over KV sequence const int KV_TILE = 16; // Each iteration processes KV_TILE keys for (int kv_start = 0; kv_start < key_seq_len; kv_start += KV_TILE) { int kv_end = min(kv_start + KV_TILE, key_seq_len); // For each key in this tile, compute QK dot product and update online softmax for (int k = kv_start; k < kv_end; k++) { // Key Cache: [L_kv_alloc, B, H_kv, D] const T* k_ptr = key_cache + k * batch * kv_head_num * head_dim + b_idx * kv_head_num * head_dim + h_kv_idx * head_dim; // Compute QK dot product: each thread handles a subset of D float qk_partial = 0.0f; for (int d = tid; d < head_dim; d += blockDim.x) { qk_partial += (float)q_ptr[d] * (float)k_ptr[d]; } // Warp-level reduction for QK dot product for (int offset = WARP_SIZE / 2; offset > 0; offset >>= 1) { qk_partial += __shfl_xor_sync(0xffffffff, qk_partial, offset); } // Now lane 0 of each warp has partial sum; reduce across warps via shared memory if (lane_id == 0) { smem_max[warp_id] = qk_partial; } __syncthreads(); float qk_score = 0.0f; if (tid == 0) { for (int w = 0; w < num_warps; w++) { qk_score += smem_max[w]; } qk_score *= scale; smem_max[0] = qk_score; // broadcast } __syncthreads(); qk_score = smem_max[0]; // Online softmax update float old_max = thread_max; float new_max = fmaxf(old_max, qk_score); float exp_diff = expf(old_max - new_max); float exp_score = expf(qk_score - new_max); // Rescale existing accumulator for (int i = 0; i < d_per_thread && i < 8; i++) { thread_out[i] *= exp_diff; } thread_sum = thread_sum * exp_diff + exp_score; thread_max = new_max; // Accumulate weighted value // Value Cache: [B, H_kv, L_kv_alloc, D] const T* v_base = value_cache + b_idx * kv_head_num * max_kv_len * head_dim + h_kv_idx * max_kv_len * head_dim; for (int i = 0; i < d_per_thread && i < 8; i++) { int d = tid + i * blockDim.x; if (d < head_dim) { float v_val = (float)v_base[k * head_dim + d]; thread_out[i] += exp_score * v_val; } } } } // Final normalize: divide by sum float inv_sum = (thread_sum > 0.0f) ? (1.0f / thread_sum) : 0.0f; // Write output: [B, 1, H_q, D] T* out_ptr = output + b_idx * head_num * head_dim + h_q_idx * head_dim; for (int i = 0; i < d_per_thread && i < 8; i++) { int d = tid + i * blockDim.x; if (d < head_dim) { out_ptr[d] = (T)(thread_out[i] * inv_sum); } } } // ===================================================================== // Flash Decode Kernel with Mask support for speculative decoding // - Each block handles one (batch, q_head, q_idx) triple // - Uses online softmax like flash_decode_kernel (single pass, no temp buffers) // - Supports additive mask for the new KV region (tree attention) // - Optimal for small seq_len (e.g., 4-32 tokens) with large KV cache // ===================================================================== template __global__ void flash_decode_kernel_with_mask( const T* __restrict__ query_input, // [B, L_q, H_q, D] const T* __restrict__ key_cache, // [L_kv_alloc, B, H_kv, D] const T* __restrict__ value_cache, // [B, H_kv, L_kv_alloc, D] T* __restrict__ output, // [B, L_q, H_q, D] const T* __restrict__ mask, // [1, 1, L_q, L_q] additive mask for new KV region const int batch, const int head_num, // H_q const int kv_head_num, // H_kv const int head_dim, // D const int key_seq_len, // total KV length (past + new) const int max_kv_len, // allocated KV length (stride for value cache) const int query_seq_len, // L_q (number of new/query tokens) const float scale ) { // Each block: one (batch, head_q, q_idx) triple const int idx = blockIdx.x; const int q_idx = idx % query_seq_len; const int bh_idx = idx / query_seq_len; const int b_idx = bh_idx / head_num; const int h_q_idx = bh_idx % head_num; const int h_kv_idx = h_q_idx / (head_num / kv_head_num); const int tid = threadIdx.x; const int WARP_SIZE = 32; const int warp_id = tid / WARP_SIZE; const int lane_id = tid % WARP_SIZE; const int num_warps = blockDim.x / WARP_SIZE; if (b_idx >= batch) return; // Load query vector: [B, L_q, H_q, D] const T* q_ptr = query_input + b_idx * query_seq_len * head_num * head_dim + q_idx * head_num * head_dim + h_q_idx * head_dim; // Mask region: keys at index >= (key_seq_len - query_seq_len) are new tokens const int past_kv_len = key_seq_len - query_seq_len; // Shared memory for warp-level reductions extern __shared__ char smem_raw[]; float* smem = reinterpret_cast(smem_raw); // [num_warps] // Online softmax state float thread_max = -1e20f; float thread_sum = 0.0f; // Per-thread output accumulator float thread_out[8]; const int d_per_thread = (head_dim + blockDim.x - 1) / blockDim.x; for (int i = 0; i < d_per_thread && i < 8; i++) thread_out[i] = 0.0f; // Value cache base pointer: [B, H_kv, L_kv_alloc, D] const T* v_base = value_cache + b_idx * kv_head_num * max_kv_len * head_dim + h_kv_idx * max_kv_len * head_dim; // Iterate over all KV positions for (int k = 0; k < key_seq_len; k++) { // Key Cache: [L_kv_alloc, B, H_kv, D] const T* k_ptr = key_cache + k * batch * kv_head_num * head_dim + b_idx * kv_head_num * head_dim + h_kv_idx * head_dim; // Compute QK dot product float qk_partial = 0.0f; for (int d = tid; d < head_dim; d += blockDim.x) { qk_partial += (float)q_ptr[d] * (float)k_ptr[d]; } // Warp-level reduction for (int offset = WARP_SIZE / 2; offset > 0; offset >>= 1) { qk_partial += __shfl_xor_sync(0xffffffff, qk_partial, offset); } if (lane_id == 0) { smem[warp_id] = qk_partial; } __syncthreads(); float qk_score = 0.0f; if (tid == 0) { for (int w = 0; w < num_warps; w++) { qk_score += smem[w]; } qk_score *= scale; // Apply additive mask for new KV positions if (k >= past_kv_len) { int new_k_idx = k - past_kv_len; // mask: [1, 1, L_q, L_q], row=q_idx, col=new_k_idx float mask_val = (float)mask[q_idx * query_seq_len + new_k_idx]; qk_score += mask_val; } smem[0] = qk_score; } __syncthreads(); qk_score = smem[0]; // Online softmax update float old_max = thread_max; float new_max = fmaxf(old_max, qk_score); float exp_diff = expf(old_max - new_max); float exp_score = expf(qk_score - new_max); for (int i = 0; i < d_per_thread && i < 8; i++) { thread_out[i] *= exp_diff; } thread_sum = thread_sum * exp_diff + exp_score; thread_max = new_max; // Accumulate weighted value for (int i = 0; i < d_per_thread && i < 8; i++) { int d = tid + i * blockDim.x; if (d < head_dim) { float v_val = (float)v_base[k * head_dim + d]; thread_out[i] += exp_score * v_val; } } } // Final normalize float inv_sum = (thread_sum > 0.0f) ? (1.0f / thread_sum) : 0.0f; // Write output: [B, L_q, H_q, D] T* out_ptr = output + b_idx * query_seq_len * head_num * head_dim + q_idx * head_num * head_dim + h_q_idx * head_dim; for (int i = 0; i < d_per_thread && i < 8; i++) { int d = tid + i * blockDim.x; if (d < head_dim) { out_ptr[d] = (T)(thread_out[i] * inv_sum); } } } // ===================================================================== // OPT-2: Split-K Flash Decode Kernel // Multiple blocks cooperate on a single (batch, head) pair, // each processing a range of KV positions. // ===================================================================== template __global__ void flash_decode_kernel_splitk( const T* __restrict__ query_input, // [B, 1, H_q, D] const T* __restrict__ key_cache, // [L_kv_alloc, B, H_kv, D] const T* __restrict__ value_cache, // [B, H_kv, L_kv_alloc, D] float* __restrict__ partial_output, // [parallel_blocks, B*H_q, D] float* __restrict__ partial_meta, // [parallel_blocks, B*H_q, 2] (max, sum) const int batch, const int head_num, const int kv_head_num, const int head_dim, const int key_seq_len, const int max_kv_len, const float scale_factor, const int parallel_blocks ) { const int bh_idx = blockIdx.x; // batch * head_num const int split_idx = blockIdx.y; // which split block const int b_idx = bh_idx / head_num; const int h_q_idx = bh_idx % head_num; const int h_kv_idx = h_q_idx / (head_num / kv_head_num); const int tid = threadIdx.x; if (b_idx >= batch) return; // Compute KV range for this split block const int kv_per_block = (key_seq_len + parallel_blocks - 1) / parallel_blocks; const int kv_start = split_idx * kv_per_block; const int kv_end = min(kv_start + kv_per_block, key_seq_len); if (kv_start >= key_seq_len) { // This block has no work — write identity metadata if (tid == 0) { partial_meta[(split_idx * batch * head_num + bh_idx) * 2 + 0] = -1e20f; // max partial_meta[(split_idx * batch * head_num + bh_idx) * 2 + 1] = 0.0f; // sum } const int d_per_thread = (head_dim + blockDim.x - 1) / blockDim.x; float* out_ptr = partial_output + (split_idx * batch * head_num + bh_idx) * head_dim; for (int i = 0; i < d_per_thread && i < 8; i++) { int d = tid + i * blockDim.x; if (d < head_dim) out_ptr[d] = 0.0f; } return; } // Load query vector const T* q_ptr = query_input + b_idx * head_num * head_dim + h_q_idx * head_dim; const int WARP_SIZE_LOCAL = 32; const int warp_id = tid / WARP_SIZE_LOCAL; const int lane_id = tid % WARP_SIZE_LOCAL; const int num_warps = blockDim.x / WARP_SIZE_LOCAL; extern __shared__ char smem_raw_splitk[]; float* smem_qk = reinterpret_cast(smem_raw_splitk); // [num_warps] float thread_max = -1e20f; float thread_sum = 0.0f; const int d_per_thread = (head_dim + blockDim.x - 1) / blockDim.x; float thread_out[8]; for (int i = 0; i < d_per_thread && i < 8; i++) thread_out[i] = 0.0f; // Process KV positions in this block's range for (int k = kv_start; k < kv_end; k++) { // Key: [L_kv_alloc, B, H_kv, D] const T* k_ptr = key_cache + k * batch * kv_head_num * head_dim + b_idx * kv_head_num * head_dim + h_kv_idx * head_dim; // Compute QK dot product float qk_partial = 0.0f; for (int d = tid; d < head_dim; d += blockDim.x) { qk_partial += (float)q_ptr[d] * (float)k_ptr[d]; } // Warp reduction for (int offset = WARP_SIZE_LOCAL / 2; offset > 0; offset >>= 1) { qk_partial += __shfl_xor_sync(0xffffffff, qk_partial, offset); } if (lane_id == 0) smem_qk[warp_id] = qk_partial; __syncthreads(); float qk_score = 0.0f; if (tid == 0) { for (int w = 0; w < num_warps; w++) qk_score += smem_qk[w]; qk_score *= scale_factor; smem_qk[0] = qk_score; } __syncthreads(); qk_score = smem_qk[0]; // Online softmax float old_max = thread_max; float new_max = fmaxf(old_max, qk_score); float exp_diff = expf(old_max - new_max); float exp_score = expf(qk_score - new_max); for (int i = 0; i < d_per_thread && i < 8; i++) thread_out[i] *= exp_diff; thread_sum = thread_sum * exp_diff + exp_score; thread_max = new_max; // Accumulate weighted value const T* v_base = value_cache + b_idx * kv_head_num * max_kv_len * head_dim + h_kv_idx * max_kv_len * head_dim; for (int i = 0; i < d_per_thread && i < 8; i++) { int d = tid + i * blockDim.x; if (d < head_dim) { float v_val = (float)v_base[k * head_dim + d]; thread_out[i] += exp_score * v_val; } } } // Write partial results (unnormalized) and metadata float* out_ptr = partial_output + (split_idx * batch * head_num + bh_idx) * head_dim; for (int i = 0; i < d_per_thread && i < 8; i++) { int d = tid + i * blockDim.x; if (d < head_dim) { out_ptr[d] = thread_out[i]; // unnormalized: sum(exp(qk - local_max) * v) } } if (tid == 0) { partial_meta[(split_idx * batch * head_num + bh_idx) * 2 + 0] = thread_max; partial_meta[(split_idx * batch * head_num + bh_idx) * 2 + 1] = thread_sum; } } // ===================================================================== // OPT-2: Combine kernel — merge partial results from split-K blocks // Grid: (batch * head_num), Block: head_dim threads // ===================================================================== template __global__ void flash_attn_combine_results( const float* __restrict__ partial_output, // [parallel_blocks, B*H_q, D] const float* __restrict__ partial_meta, // [parallel_blocks, B*H_q, 2] T* __restrict__ final_output, // [B, 1, H_q, D] const int batch, const int head_num, const int head_dim, const int parallel_blocks ) { const int bh_idx = blockIdx.x; const int d = threadIdx.x; if (d >= head_dim) return; const int b_idx = bh_idx / head_num; const int h_q_idx = bh_idx % head_num; // Find global max across all blocks float global_max = -1e20f; for (int s = 0; s < parallel_blocks; s++) { float local_max = partial_meta[(s * batch * head_num + bh_idx) * 2 + 0]; global_max = fmaxf(global_max, local_max); } // Combine: rescale each block's partial output and sum float combined_output = 0.0f; float combined_sum = 0.0f; for (int s = 0; s < parallel_blocks; s++) { float local_max = partial_meta[(s * batch * head_num + bh_idx) * 2 + 0]; float local_sum = partial_meta[(s * batch * head_num + bh_idx) * 2 + 1]; float rescale = expf(local_max - global_max); combined_output += partial_output[(s * batch * head_num + bh_idx) * head_dim + d] * rescale; combined_sum += local_sum * rescale; } // Normalize float inv_sum = (combined_sum > 0.0f) ? (1.0f / combined_sum) : 0.0f; T* out_ptr = final_output + b_idx * head_num * head_dim + h_q_idx * head_dim; out_ptr[d] = (T)(combined_output * inv_sum); } // ===================================================================== // Optimized Prefill QK Kernel: uses shared memory tiling // ===================================================================== template __global__ void qk_kernel_tiled( const T* __restrict__ query_input, // [B, L_q_full, H_q, D] const T* __restrict__ key_cache, // [L_k_total_alloc, B, H_kv, D] T* __restrict__ qk_scores_output, // [B, H_q, L_q_piece, L_k_total] const void* mask_tensor_data, const AttentionKernelParam* param, int q_seq_piece_offset, bool has_mask_flag, bool is_add_mask_flag ) { // Block handles a tile of [QK_TILE_Q x QK_TILE_K] outputs const int QK_TILE = 16; __shared__ AccT q_tile[QK_TILE][128 + 1]; // +1 to avoid bank conflict, max head_dim=128 __shared__ AccT k_tile[QK_TILE][128 + 1]; const int tx = threadIdx.x; // within tile K dim const int ty = threadIdx.y; // within tile Q dim const int bh_q_idx = blockIdx.z; if (bh_q_idx >= param->batch * param->head_num) return; const int b_idx = bh_q_idx / param->head_num; const int h_q_idx = bh_q_idx % param->head_num; const int h_kv_idx = h_q_idx / param->group; const int q_idx_in_piece = blockIdx.y * QK_TILE + ty; const int k_idx = blockIdx.x * QK_TILE + tx; const int current_full_q_idx = q_seq_piece_offset + q_idx_in_piece; AccT score_sum = 0.0f; // Tile over head_dim const int D_TILE = 32; for (int d_start = 0; d_start < param->head_dim; d_start += D_TILE) { // Load Q tile into shared memory if (current_full_q_idx < param->query_seq_len && q_idx_in_piece < param->q_seq_piece_len) { for (int dd = tx; dd < D_TILE && (d_start + dd) < param->head_dim; dd += QK_TILE) { int q_offset = b_idx * param->query_seq_len * param->head_num * param->head_dim + current_full_q_idx * param->head_num * param->head_dim + h_q_idx * param->head_dim + d_start + dd; q_tile[ty][dd] = (AccT)query_input[q_offset]; } } else { for (int dd = tx; dd < D_TILE; dd += QK_TILE) { q_tile[ty][dd] = AccT(0.0f); } } // Load K tile into shared memory if (k_idx < param->key_seq_len) { for (int dd = ty; dd < D_TILE && (d_start + dd) < param->head_dim; dd += QK_TILE) { int k_offset = k_idx * param->batch * param->kv_head_num * param->head_dim + b_idx * param->kv_head_num * param->head_dim + h_kv_idx * param->head_dim + d_start + dd; k_tile[tx][dd] = (AccT)key_cache[k_offset]; } } else { for (int dd = ty; dd < D_TILE; dd += QK_TILE) { k_tile[tx][dd] = AccT(0.0f); } } __syncthreads(); // Dot product for this D tile int d_end = min(D_TILE, param->head_dim - d_start); #pragma unroll 8 for (int dd = 0; dd < d_end; dd++) { score_sum += q_tile[ty][dd] * k_tile[tx][dd]; } __syncthreads(); } if (k_idx >= param->key_seq_len || q_idx_in_piece >= param->q_seq_piece_len || current_full_q_idx >= param->query_seq_len) { return; } score_sum *= param->scale; // Apply mask if (has_mask_flag && mask_tensor_data) { if (is_add_mask_flag) { int mask_idx = current_full_q_idx * param->query_seq_len + k_idx - param->key_seq_len + param->query_seq_len; if (k_idx >= param->key_seq_len - param->query_seq_len) { if (sizeof(T) == sizeof(__half)) { score_sum += __half2float(((const __half*)mask_tensor_data)[mask_idx]); } else { score_sum += static_cast(mask_tensor_data)[mask_idx]; } } } else { int mask_idx = current_full_q_idx * param->key_seq_len + k_idx; if (static_cast(mask_tensor_data)[mask_idx] == 0) { score_sum = (sizeof(T) == sizeof(__half)) ? AccT(-65504.0f) : AccT(-1e9f); } } } if (sizeof(T) == sizeof(__half)) { const AccT max_half_val = AccT(65504.0f); score_sum = fminf(fmaxf(score_sum, -max_half_val), max_half_val); } int out_idx = b_idx * param->head_num * param->q_seq_piece_len * param->key_seq_len + h_q_idx * param->q_seq_piece_len * param->key_seq_len + q_idx_in_piece * param->key_seq_len + k_idx; qk_scores_output[out_idx] = static_cast(score_sum); } // ===================================================================== // Optimized QKV Kernel: shared memory tiling for V accumulation // ===================================================================== template __global__ void qkv_kernel_tiled( const T* __restrict__ softmax_probs, // [B, H_q, L_q_piece, L_k_total] const T* __restrict__ value_cache, // [B, H_kv, L_k_alloc_max, D] T* __restrict__ attention_output, // [B, L_q_full, H_q, D] const AttentionKernelParam* param, int q_seq_piece_offset ) { // Each thread computes one output element const int d_idx = blockIdx.x * blockDim.x + threadIdx.x; const int q_idx_in_piece = blockIdx.y * blockDim.y + threadIdx.y; const int bh_q_idx = blockIdx.z; if (d_idx >= param->head_dim || q_idx_in_piece >= param->q_seq_piece_len || bh_q_idx >= param->batch * param->head_num) { return; } const int b_idx = bh_q_idx / param->head_num; const int h_q_idx = bh_q_idx % param->head_num; const int current_full_q_idx = q_seq_piece_offset + q_idx_in_piece; if (current_full_q_idx >= param->query_seq_len) return; const int h_kv_idx = h_q_idx / param->group; AccT weighted_sum = 0.0f; const T* prob_ptr = softmax_probs + b_idx * param->head_num * param->q_seq_piece_len * param->key_seq_len + h_q_idx * param->q_seq_piece_len * param->key_seq_len + q_idx_in_piece * param->key_seq_len; const T* val_ptr_base = value_cache + b_idx * param->kv_head_num * param->max_kv_len * param->head_dim + h_kv_idx * param->max_kv_len * param->head_dim; // Unrolled loop for better ILP const int hd = param->head_dim; int k_s = 0; for (; k_s + 3 < param->key_seq_len; k_s += 4) { weighted_sum += (AccT)prob_ptr[k_s] * (AccT)val_ptr_base[k_s * hd + d_idx]; weighted_sum += (AccT)prob_ptr[k_s + 1] * (AccT)val_ptr_base[(k_s + 1) * hd + d_idx]; weighted_sum += (AccT)prob_ptr[k_s + 2] * (AccT)val_ptr_base[(k_s + 2) * hd + d_idx]; weighted_sum += (AccT)prob_ptr[k_s + 3] * (AccT)val_ptr_base[(k_s + 3) * hd + d_idx]; } for (; k_s < param->key_seq_len; k_s++) { weighted_sum += (AccT)prob_ptr[k_s] * (AccT)val_ptr_base[k_s * hd + d_idx]; } int out_idx = b_idx * param->query_seq_len * param->head_num * param->head_dim + current_full_q_idx * param->head_num * param->head_dim + h_q_idx * param->head_dim + d_idx; attention_output[out_idx] = static_cast(weighted_sum); } // ======= AttentionExecution 类实现 ======= AttentionExecution::AttentionExecution(Backend* backend, bool kv_cache_op_param) : Execution(backend), mIsKVCacheEnabled(kv_cache_op_param), mCudaBackend(static_cast(backend)), mBatch(0), mQuerySeqLen(0), mNumHead(0), mHeadDim(0), mKvNumHead(0), mNewKvSeqLen(0), mQseqSplitNum(1), mHasMask(false), mIsAddMask(false), mParam_gpu(nullptr), mScale(1.0f) { mPrecision = 4; // 默认精度,可在 onResize 中更改 if (mIsKVCacheEnabled) { mCache.reset(new SharedCache()); mMeta = (KVMeta*)(mCudaBackend->getMetaPtr()); } } AttentionExecution::~AttentionExecution() { if (mParam_gpu) { cudaFree(mParam_gpu); mParam_gpu = nullptr; } if (mSplitKOutputPtr) { cudaFree(mSplitKOutputPtr); mSplitKOutputPtr = nullptr; } if (mSplitKMetaPtr) { cudaFree(mSplitKMetaPtr); mSplitKMetaPtr = nullptr; } } // 初始化一个大小为 1 的占位 KVCache ErrorCode AttentionExecution::init_cache_tensors() { if (!mIsKVCacheEnabled || !mCache) return MNN::NO_ERROR; if (mCache->mPastKey && mCache->mPastValue && mCache->mPastKey->deviceId() != 0) return MNN::NO_ERROR; mCache->mPastLength = 0; mCache->mMaxLength = 0; mCache->mPastKey.reset(mPrecision == 4 ? Tensor::createDevice({1, 1, 1, 1}) : Tensor::createDevice({1, 1, 1, 1})); mCache->mPastValue.reset(mPrecision == 4 ? Tensor::createDevice({1, 1, 1, 1}) : Tensor::createDevice({1, 1, 1, 1})); if (!mCache->mPastKey || !mCache->mPastValue) return MNN::OUT_OF_MEMORY; bool res = mCudaBackend->onAcquireBuffer(mCache->mPastKey.get(), Backend::STATIC); if (!res) return MNN::OUT_OF_MEMORY; res = mCudaBackend->onAcquireBuffer(mCache->mPastValue.get(), Backend::STATIC); if (!res) return MNN::OUT_OF_MEMORY; return MNN::NO_ERROR; } // P2: Optimized KV Cache realloc with 2x growth strategy and batch memcpy ErrorCode AttentionExecution::reallocKVCache_gpu(int required_total_kv_len, int batch_size, int kv_num_head, int head_dim, cudaStream_t stream) { if (!mIsKVCacheEnabled || !mCache) return MNN::NO_ERROR; if (required_total_kv_len > mCache->mMaxLength || mCache->mPastKey->deviceId() == 0) { int old_max_len = mCache->mMaxLength; int old_past_len = mCache->mPastLength; bool needs_copy = old_past_len > 0 && mCache->mPastKey && mCache->mPastKey->deviceId() != 0; // P2: 2x growth strategy to reduce realloc frequency int new_allocated_max_len = ROUND_UP(required_total_kv_len, mExpandChunk); if (old_max_len > 0 && new_allocated_max_len < old_max_len * 2) { new_allocated_max_len = std::max(new_allocated_max_len, old_max_len * 2); } if (new_allocated_max_len <= old_max_len && mCache->mPastKey->deviceId() != 0) { return MNN::NO_ERROR; } std::shared_ptr new_past_key_tensor(mPrecision == 4 ? Tensor::createDevice({new_allocated_max_len, batch_size, kv_num_head, head_dim}) : Tensor::createDevice({new_allocated_max_len, batch_size, kv_num_head, head_dim})); std::shared_ptr new_past_value_tensor(mPrecision == 4 ? Tensor::createDevice({batch_size, kv_num_head, new_allocated_max_len, head_dim}) : Tensor::createDevice({batch_size, kv_num_head, new_allocated_max_len, head_dim})); if (!new_past_key_tensor || !new_past_value_tensor) return MNN::OUT_OF_MEMORY; bool resK = mCudaBackend->onAcquireBuffer(new_past_key_tensor.get(), Backend::STATIC); bool resV = mCudaBackend->onAcquireBuffer(new_past_value_tensor.get(), Backend::STATIC); if(!resK || !resV) return MNN::OUT_OF_MEMORY; if (needs_copy) { size_t element_size_bytes = mPrecision; // Key Cache: contiguous [old_past_len, B, H_kv, D] -> single memcpy size_t key_bytes_to_copy = (size_t)old_past_len * batch_size * kv_num_head * head_dim * element_size_bytes; if (key_bytes_to_copy > 0) { cudaMemcpyAsync(getTensorDevicePtr(new_past_key_tensor.get()), getTensorDevicePtr(mCache->mPastKey.get()), key_bytes_to_copy, cudaMemcpyDeviceToDevice, stream); checkKernelErrors; } // P2: Value Cache: use cudaMemcpy2DAsync for batch copy // Value layout: [B, H_kv, L_kv_max, D], each (B,H) group has L*D contiguous elements if (old_past_len > 0) { cudaMemcpy2DAsync( getTensorDevicePtr(new_past_value_tensor.get()), (size_t)new_allocated_max_len * head_dim * element_size_bytes, // dst pitch getTensorDevicePtr(mCache->mPastValue.get()), (size_t)old_max_len * head_dim * element_size_bytes, // src pitch (size_t)old_past_len * head_dim * element_size_bytes, // width to copy (size_t)batch_size * kv_num_head, // height (num groups) cudaMemcpyDeviceToDevice, stream); checkKernelErrors; } } if (mCache->mPastKey && mCache->mPastKey->deviceId() != 0) mCudaBackend->onReleaseBuffer(mCache->mPastKey.get(), Backend::STATIC); if (mCache->mPastValue && mCache->mPastValue->deviceId() != 0) mCudaBackend->onReleaseBuffer(mCache->mPastValue.get(), Backend::STATIC); mCache->mPastKey = new_past_key_tensor; mCache->mPastValue = new_past_value_tensor; mCache->mMaxLength = new_allocated_max_len; } return MNN::NO_ERROR; } // P2: Optimized KV Cache realloc with mMeta version ErrorCode AttentionExecution::reallocKVCache_gpu(int required_total_kv_len, const KVMeta* meta, cudaStream_t stream) { if (!mIsKVCacheEnabled || !mCache) { return MNN::NO_ERROR; } if (!meta) { MNN_ERROR("KVMeta is null in reallocKVCache_gpu, which is required for dynamic cache management.\n"); return MNN::INVALID_VALUE; } size_t element_size = mPrecision; bool needs_realloc = required_total_kv_len > mCache->mMaxLength; int past_len_after_remove = mCache->mPastLength - meta->remove; if (past_len_after_remove < 0) { past_len_after_remove = 0; } // 处理数据紧凑化 (meta->reserve) if (meta->n_reserve > 0 && meta->reserve != nullptr) { std::vector reserve_offsets_host(meta->n_reserve + 1, 0); for (int i = 0; i < meta->n_reserve; ++i) { reserve_offsets_host[i+1] = reserve_offsets_host[i] + meta->reserve[2 * i + 1]; } int* reserve_info_gpu = nullptr; int* reserve_offsets_gpu = nullptr; cudaMallocAsync(&reserve_info_gpu, meta->n_reserve * 2 * sizeof(int), stream); cudaMallocAsync(&reserve_offsets_gpu, (meta->n_reserve + 1) * sizeof(int), stream); cudaMemcpyAsync(reserve_info_gpu, meta->reserve, meta->n_reserve * 2 * sizeof(int), cudaMemcpyHostToDevice, stream); cudaMemcpyAsync(reserve_offsets_gpu, reserve_offsets_host.data(), (meta->n_reserve + 1) * sizeof(int), cudaMemcpyHostToDevice, stream); dim3 blockDim(mHeadDim, 1, 1); dim3 gridDim(mBatch * mKvNumHead, meta->n_reserve, 1); if (needs_realloc) { // P2: 2x growth strategy int new_allocated_max_len = ROUND_UP(required_total_kv_len, mExpandChunk); if (mCache->mMaxLength > 0) { new_allocated_max_len = std::max(new_allocated_max_len, mCache->mMaxLength * 2); } std::shared_ptr new_past_key_tensor(mPrecision == 4 ? Tensor::createDevice({new_allocated_max_len, mBatch, mKvNumHead, mHeadDim}) : Tensor::createDevice({new_allocated_max_len, mBatch, mKvNumHead, mHeadDim})); std::shared_ptr new_past_value_tensor(mPrecision == 4 ? Tensor::createDevice({mBatch, mKvNumHead, new_allocated_max_len, mHeadDim}) : Tensor::createDevice({mBatch, mKvNumHead, new_allocated_max_len, mHeadDim})); if(!mCudaBackend->onAcquireBuffer(new_past_key_tensor.get(), Backend::STATIC) || !mCudaBackend->onAcquireBuffer(new_past_value_tensor.get(), Backend::STATIC)) { return MNN::OUT_OF_MEMORY; } // Copy preserved data [0, past_len_after_remove) from old to new cache if (past_len_after_remove > 0) { // Key: [L, B, H_kv, D] — contiguous in first dimension cudaMemcpyAsync(getTensorDevicePtr(new_past_key_tensor.get()), getTensorDevicePtr(mCache->mPastKey.get()), (size_t)past_len_after_remove * mBatch * mKvNumHead * mHeadDim * element_size, cudaMemcpyDeviceToDevice, stream); // Value: [B, H_kv, L, D] — strided copy cudaMemcpy2DAsync(getTensorDevicePtr(new_past_value_tensor.get()), (size_t)new_allocated_max_len * mHeadDim * element_size, getTensorDevicePtr(mCache->mPastValue.get()), (size_t)mCache->mMaxLength * mHeadDim * element_size, (size_t)past_len_after_remove * mHeadDim * element_size, (size_t)mBatch * mKvNumHead, cudaMemcpyDeviceToDevice, stream); } // Compact reserve data from old cache into new cache compact_kv_cache_kernel<<>>( getTensorDevicePtr(mCache->mPastKey.get()), getTensorDevicePtr(mCache->mPastValue.get()), getTensorDevicePtr(new_past_key_tensor.get()), getTensorDevicePtr(new_past_value_tensor.get()), reserve_info_gpu, reserve_offsets_gpu, meta->n_reserve, past_len_after_remove, mBatch, mKvNumHead, mHeadDim, mCache->mMaxLength, new_allocated_max_len, element_size ); mCudaBackend->onReleaseBuffer(mCache->mPastKey.get(), Backend::STATIC); mCudaBackend->onReleaseBuffer(mCache->mPastValue.get(), Backend::STATIC); mCache->mPastKey = new_past_key_tensor; mCache->mPastValue = new_past_value_tensor; mCache->mMaxLength = new_allocated_max_len; } else { // No realloc: compact reserve data in-place directly // For speculative decoding, reserve entries are at positions after past_len_after_remove, // and destination offsets are always <= source offsets (sorted accept indices), // so forward-order in-place copy is safe without temp buffers. // [0, past_len_after_remove) is already in place and doesn't need copying. compact_kv_cache_kernel<<>>( getTensorDevicePtr(mCache->mPastKey.get()), getTensorDevicePtr(mCache->mPastValue.get()), getTensorDevicePtr(mCache->mPastKey.get()), getTensorDevicePtr(mCache->mPastValue.get()), reserve_info_gpu, reserve_offsets_gpu, meta->n_reserve, past_len_after_remove, mBatch, mKvNumHead, mHeadDim, mCache->mMaxLength, mCache->mMaxLength, element_size ); } checkKernelErrors; cudaFreeAsync(reserve_info_gpu, stream); cudaFreeAsync(reserve_offsets_gpu, stream); mCache->mPastLength = past_len_after_remove + meta->computeReverseSize(); } else { // 无 reserve 指令 mCache->mPastLength = past_len_after_remove; if (needs_realloc) { int old_max_len = mCache->mMaxLength; int old_past_len_to_copy = mCache->mPastLength; // P2: 2x growth strategy int new_allocated_max_len = ROUND_UP(required_total_kv_len, mExpandChunk); if (old_max_len > 0) { new_allocated_max_len = std::max(new_allocated_max_len, old_max_len * 2); } std::shared_ptr new_past_key_tensor(mPrecision == 4 ? Tensor::createDevice({new_allocated_max_len, mBatch, mKvNumHead, mHeadDim}) : Tensor::createDevice({new_allocated_max_len, mBatch, mKvNumHead, mHeadDim})); std::shared_ptr new_past_value_tensor(mPrecision == 4 ? Tensor::createDevice({mBatch, mKvNumHead, new_allocated_max_len, mHeadDim}) : Tensor::createDevice({mBatch, mKvNumHead, new_allocated_max_len, mHeadDim})); if(!mCudaBackend->onAcquireBuffer(new_past_key_tensor.get(), Backend::STATIC) || !mCudaBackend->onAcquireBuffer(new_past_value_tensor.get(), Backend::STATIC)) { return MNN::OUT_OF_MEMORY; } if (old_past_len_to_copy > 0) { cudaMemcpyAsync(getTensorDevicePtr(new_past_key_tensor.get()), getTensorDevicePtr(mCache->mPastKey.get()), (size_t)old_past_len_to_copy * mBatch * mKvNumHead * mHeadDim * element_size, cudaMemcpyDeviceToDevice, stream); // P2: Use cudaMemcpy2DAsync — Value layout: [B, H_kv, L, D] cudaMemcpy2DAsync(getTensorDevicePtr(new_past_value_tensor.get()), (size_t)new_allocated_max_len * mHeadDim * element_size, getTensorDevicePtr(mCache->mPastValue.get()), (size_t)old_max_len * mHeadDim * element_size, (size_t)old_past_len_to_copy * mHeadDim * element_size, (size_t)mBatch * mKvNumHead, cudaMemcpyDeviceToDevice, stream); checkKernelErrors; } mCudaBackend->onReleaseBuffer(mCache->mPastKey.get(), Backend::STATIC); mCudaBackend->onReleaseBuffer(mCache->mPastValue.get(), Backend::STATIC); mCache->mPastKey = new_past_key_tensor; mCache->mPastValue = new_past_value_tensor; mCache->mMaxLength = new_allocated_max_len; } } return MNN::NO_ERROR; } // 为新 KV、QK^T、Softmax 分配 GPU 空间 ErrorCode AttentionExecution::ensureTempBuffers_gpu(int batch, int num_head, int q_seq_piece_len_max, int current_max_total_kv_len, int head_dim) { // QK Scores: [B, H_q, Max_L_q_piece, Max_L_k_total] std::vector qk_shape = {batch, num_head, q_seq_piece_len_max, current_max_total_kv_len}; bool qk_realloc = !mTempQK || mTempQK->shape() != qk_shape; if (qk_realloc) { if(mTempQK && mTempQK->deviceId() != 0) mCudaBackend->onReleaseBuffer(mTempQK.get(), Backend::STATIC); mTempQK.reset(mPrecision == 4 ? Tensor::createDevice(qk_shape) : Tensor::createDevice(qk_shape)); if(!mTempQK || !mCudaBackend->onAcquireBuffer(mTempQK.get(), Backend::STATIC)) { MNN_ERROR("Attention: mTempQK STATIC alloc failed\n"); return MNN::OUT_OF_MEMORY; } } // Softmax Probs: 与QK scores形状相同 bool softmax_realloc = !mTempSoftmax || mTempSoftmax->shape() != qk_shape; if (softmax_realloc) { if(mTempSoftmax && mTempSoftmax->deviceId() != 0) mCudaBackend->onReleaseBuffer(mTempSoftmax.get(), Backend::STATIC); mTempSoftmax.reset(mPrecision == 4 ? Tensor::createDevice(qk_shape) : Tensor::createDevice(qk_shape)); if(!mTempSoftmax || !mCudaBackend->onAcquireBuffer(mTempSoftmax.get(), Backend::STATIC)) { MNN_ERROR("Attention: mTempSoftmax STATIC alloc failed\n"); return MNN::OUT_OF_MEMORY; } } if (!mIsKVCacheEnabled) { int temp_k_alloc_len = ROUND_UP(mNewKvSeqLen, 1); std::vector temp_k_shape = {temp_k_alloc_len, batch, mKvNumHead, head_dim}; bool temp_k_realloc = !mTempK_current_step || mTempK_current_step->shape() != temp_k_shape; if(temp_k_realloc){ if(mTempK_current_step && mTempK_current_step->deviceId() !=0) mCudaBackend->onReleaseBuffer(mTempK_current_step.get(), Backend::STATIC); mTempK_current_step.reset(mPrecision == 4 ? Tensor::createDevice(temp_k_shape) : Tensor::createDevice(temp_k_shape) ); if(!mTempK_current_step || !mCudaBackend->onAcquireBuffer(mTempK_current_step.get(), Backend::STATIC)) return MNN::OUT_OF_MEMORY; } std::vector temp_v_shape = {batch, mKvNumHead, temp_k_alloc_len, head_dim}; bool temp_v_realloc = !mTempV_current_step || mTempV_current_step->shape() != temp_v_shape; if(temp_v_realloc){ if(mTempV_current_step && mTempV_current_step->deviceId() !=0) mCudaBackend->onReleaseBuffer(mTempV_current_step.get(), Backend::STATIC); mTempV_current_step.reset(mPrecision == 4 ? Tensor::createDevice(temp_v_shape) : Tensor::createDevice(temp_v_shape)); if(!mTempV_current_step || !mCudaBackend->onAcquireBuffer(mTempV_current_step.get(), Backend::STATIC)) return MNN::OUT_OF_MEMORY; } } return MNN::NO_ERROR; } bool AttentionExecution::onClone(Backend* bn, const Op* op, Execution** dst) { if (nullptr == dst) { return true; } auto exe = new AttentionExecution(bn, mIsKVCacheEnabled); exe->mCache = mCache; exe->mMeta = mMeta; *dst = exe; return true; } // 初始化所有参数 ErrorCode AttentionExecution::onResize(const std::vector &inputs, const std::vector &outputs) { const auto* query_tensor = inputs[0]; // 形状: [B, L_q, H_q, D] const auto* key_tensor = inputs[1]; // 形状: [B, L_k_new, H_kv, D] if (mCudaBackend->useFp16()) { mPrecision = 2; } else { mPrecision = 4; } mBatch = query_tensor->length(0); mQuerySeqLen = query_tensor->length(1); mNumHead = query_tensor->length(2); mHeadDim = query_tensor->length(3); mKvNumHead = key_tensor->length(2); mNewKvSeqLen = key_tensor->length(1); if (mHeadDim == 0 || mKvNumHead == 0) return MNN::INVALID_VALUE; if (mNumHead % mKvNumHead != 0) return MNN::INVALID_VALUE; mScale = 1.0f / sqrtf(static_cast(mHeadDim)); mHasMask = inputs.size() > 3 && inputs[3] != nullptr && inputs[3]->elementSize() > 0; if (mHasMask) { mIsAddMask = (inputs[3]->getType().code == halide_type_float); } // Q splitting for prefill mQseqSplitNum = 1; if (mQuerySeqLen > 1024) mQseqSplitNum = UP_DIV(mQuerySeqLen, 1024); else if (mQuerySeqLen > 256) mQseqSplitNum = UP_DIV(mQuerySeqLen, 256); if (mIsKVCacheEnabled) { ErrorCode err = init_cache_tensors(); if (err != MNN::NO_ERROR) return err; } if (!mParam_gpu) { auto cuda_err = cudaMalloc(&mParam_gpu, sizeof(AttentionKernelParam)); if (cuda_err != cudaSuccess) { MNN_ERROR("cudaMalloc failed for mParam_gpu\n"); return MNN::NOT_SUPPORT; } } return MNN::NO_ERROR; } // 拷贝 KV 到 GPU,对 Q 分片后执行 Attention 流程 ErrorCode AttentionExecution::onExecute(const std::vector &inputs, const std::vector &outputs) { const auto* query_input_tensor = inputs[0]; const auto* key_input_tensor = inputs[1]; const auto* value_input_tensor = inputs[2]; const Tensor* mask_input_tensor = mHasMask ? inputs[3] : nullptr; auto final_output_tensor = outputs[0]; if (mIsKVCacheEnabled && mHasMask && mask_input_tensor && mask_input_tensor->elementSize() == 1) { mHasMask = false; } cudaStream_t stream = 0; AttentionKernelParam param_cpu; param_cpu.batch = mBatch; param_cpu.query_seq_len = mQuerySeqLen; param_cpu.head_num = mNumHead; param_cpu.kv_head_num = mKvNumHead; param_cpu.group = mNumHead / mKvNumHead; param_cpu.head_dim = mHeadDim; param_cpu.scale = mScale; param_cpu.current_kv_seq_len_new = mNewKvSeqLen; const void* effective_key_cache_ptr; const void* effective_value_cache_ptr; int current_total_kv_len_for_qk; int allocated_kv_len_for_value_stride; if (mIsKVCacheEnabled) { if (!mMeta) { param_cpu.past_kv_len = mCache->mPastLength; current_total_kv_len_for_qk = param_cpu.past_kv_len + mNewKvSeqLen; ErrorCode err = reallocKVCache_gpu(current_total_kv_len_for_qk, mBatch, mKvNumHead, mHeadDim, stream); if (err != MNN::NO_ERROR) return err; allocated_kv_len_for_value_stride = mCache->mMaxLength; param_cpu.key_seq_len = current_total_kv_len_for_qk; param_cpu.max_kv_len = mCache->mMaxLength; } else { int required_total_kv_len = (mMeta->previous - mMeta->remove + mMeta->computeReverseSize()) + mMeta->add; ErrorCode err = reallocKVCache_gpu(required_total_kv_len, mMeta, stream); if (err != MNN::NO_ERROR) return err; param_cpu.past_kv_len = mCache->mPastLength; param_cpu.key_seq_len = mCache->mPastLength + mNewKvSeqLen; param_cpu.max_kv_len = mCache->mMaxLength; param_cpu.current_kv_seq_len_new = mNewKvSeqLen; allocated_kv_len_for_value_stride = mCache->mMaxLength; current_total_kv_len_for_qk = param_cpu.key_seq_len; } // Copy new K, V to cache dim3 copy_blockDim(32, 8, 1); dim3 copy_gridDim(UP_DIV(mHeadDim, copy_blockDim.x), UP_DIV(mNewKvSeqLen, copy_blockDim.y), UP_DIV(mBatch * mKvNumHead, copy_blockDim.z)); if (mPrecision == 4) { copy_kv_to_cache_kernel<<>>( getTensorDevicePtr(key_input_tensor), getTensorDevicePtr(value_input_tensor), getTensorDevicePtr(mCache->mPastKey.get()), getTensorDevicePtr(mCache->mPastValue.get()), mBatch, mNewKvSeqLen, mKvNumHead, mHeadDim, param_cpu.past_kv_len, mCache->mMaxLength); } else if (mPrecision == 2) { copy_kv_to_cache_kernel<__half><<>>( getTensorDevicePtr<__half>(key_input_tensor), getTensorDevicePtr<__half>(value_input_tensor), getTensorDevicePtr<__half>(mCache->mPastKey.get()), getTensorDevicePtr<__half>(mCache->mPastValue.get()), mBatch, mNewKvSeqLen, mKvNumHead, mHeadDim, param_cpu.past_kv_len, mCache->mMaxLength); } else { return MNN::NOT_SUPPORT; } checkKernelErrors; effective_key_cache_ptr = getTensorDevicePtr(mCache->mPastKey.get()); effective_value_cache_ptr = getTensorDevicePtr(mCache->mPastValue.get()); } else { param_cpu.past_kv_len = 0; current_total_kv_len_for_qk = mNewKvSeqLen; allocated_kv_len_for_value_stride = ROUND_UP(mNewKvSeqLen,1); param_cpu.key_seq_len = mNewKvSeqLen; param_cpu.max_kv_len = allocated_kv_len_for_value_stride; ErrorCode temp_err = ensureTempBuffers_gpu(mBatch, mNumHead, UP_DIV(mQuerySeqLen, mQseqSplitNum), current_total_kv_len_for_qk, mHeadDim); if(temp_err != MNN::NO_ERROR) return temp_err; dim3 copy_blockDim(32, 8, 1); dim3 copy_gridDim(UP_DIV(mHeadDim, copy_blockDim.x), UP_DIV(mNewKvSeqLen, copy_blockDim.y), UP_DIV(mBatch * mKvNumHead, copy_blockDim.z)); if (mPrecision == 4) { copy_kv_to_cache_kernel<<>>( getTensorDevicePtr(key_input_tensor), getTensorDevicePtr(value_input_tensor), getTensorDevicePtr(mTempK_current_step.get()), getTensorDevicePtr(mTempV_current_step.get()), mBatch, mNewKvSeqLen, mKvNumHead, mHeadDim, 0, allocated_kv_len_for_value_stride); } else if (mPrecision == 2) { copy_kv_to_cache_kernel<__half><<>>( getTensorDevicePtr<__half>(key_input_tensor), getTensorDevicePtr<__half>(value_input_tensor), getTensorDevicePtr<__half>(mTempK_current_step.get()), getTensorDevicePtr<__half>(mTempV_current_step.get()), mBatch, mNewKvSeqLen, mKvNumHead, mHeadDim, 0, allocated_kv_len_for_value_stride); } else { return MNN::NOT_SUPPORT; } checkKernelErrors; effective_key_cache_ptr = getTensorDevicePtr(mTempK_current_step.get()); effective_value_cache_ptr = getTensorDevicePtr(mTempV_current_step.get()); } // ===================================================================== // P0/OPT-2: Flash Decoding path for decode stage (seq_len == 1, with KV cache) // OPT-2: Use split-K when SM utilization is low // ===================================================================== if (mQuerySeqLen == 1 && mIsKVCacheEnabled && !mHasMask) { const int num_threads = 128; const int num_warps = num_threads / 32; const int smem_size = num_warps * sizeof(float); // just for QK reduction const int total_heads = mBatch * mNumHead; const int num_sm = mCudaBackend->getCUDARuntime()->prop().multiProcessorCount; // Determine parallel_blocks for split-K int parallel_blocks = 1; if (current_total_kv_len_for_qk > 16) { // Heuristic: try to fill all SMs int max_blocks_per_sm = 1; // conservative estimate parallel_blocks = std::max(1, (max_blocks_per_sm * num_sm) / total_heads); parallel_blocks = std::min(parallel_blocks, (current_total_kv_len_for_qk + 31) / 32); // at least 32 KV per block parallel_blocks = std::min(parallel_blocks, 32); // cap } // OPT-2: Try to allocate split-K buffers if needed if (parallel_blocks > 1 && mMaxParallelBlocks < parallel_blocks) { if (mSplitKOutputPtr) cudaFree(mSplitKOutputPtr); if (mSplitKMetaPtr) cudaFree(mSplitKMetaPtr); mSplitKOutputPtr = nullptr; mSplitKMetaPtr = nullptr; size_t out_size = (size_t)parallel_blocks * total_heads * mHeadDim * sizeof(float); size_t meta_size = (size_t)parallel_blocks * total_heads * 2 * sizeof(float); auto err1 = cudaMalloc(&mSplitKOutputPtr, out_size); auto err2 = cudaMalloc(&mSplitKMetaPtr, meta_size); if (err1 != cudaSuccess || err2 != cudaSuccess) { if (mSplitKOutputPtr) { cudaFree(mSplitKOutputPtr); mSplitKOutputPtr = nullptr; } if (mSplitKMetaPtr) { cudaFree(mSplitKMetaPtr); mSplitKMetaPtr = nullptr; } mMaxParallelBlocks = 0; parallel_blocks = 1; // fallback to single-block } else { mMaxParallelBlocks = parallel_blocks; } } if (parallel_blocks <= 1) { // Single-block flash decode const int smem_size_orig = num_warps * (2 * sizeof(float) + mHeadDim * sizeof(float)); dim3 grid(total_heads); dim3 block(num_threads); if (mPrecision == 4) { flash_decode_kernel<<>>( getTensorDevicePtr(query_input_tensor), static_cast(effective_key_cache_ptr), static_cast(effective_value_cache_ptr), getTensorDevicePtr(final_output_tensor), mBatch, mNumHead, mKvNumHead, mHeadDim, current_total_kv_len_for_qk, allocated_kv_len_for_value_stride, mScale); } else if (mPrecision == 2) { flash_decode_kernel<__half><<>>( getTensorDevicePtr<__half>(query_input_tensor), static_cast(effective_key_cache_ptr), static_cast(effective_value_cache_ptr), getTensorDevicePtr<__half>(final_output_tensor), mBatch, mNumHead, mKvNumHead, mHeadDim, current_total_kv_len_for_qk, allocated_kv_len_for_value_stride, mScale); } else { return MNN::NOT_SUPPORT; } } else { // Split-K flash decode float* partial_out = mSplitKOutputPtr; float* partial_meta = mSplitKMetaPtr; dim3 grid_splitk(total_heads, parallel_blocks); dim3 block_splitk(num_threads); if (mPrecision == 4) { flash_decode_kernel_splitk<<>>( getTensorDevicePtr(query_input_tensor), static_cast(effective_key_cache_ptr), static_cast(effective_value_cache_ptr), partial_out, partial_meta, mBatch, mNumHead, mKvNumHead, mHeadDim, current_total_kv_len_for_qk, allocated_kv_len_for_value_stride, mScale, parallel_blocks); } else if (mPrecision == 2) { flash_decode_kernel_splitk<__half><<>>( getTensorDevicePtr<__half>(query_input_tensor), static_cast(effective_key_cache_ptr), static_cast(effective_value_cache_ptr), partial_out, partial_meta, mBatch, mNumHead, mKvNumHead, mHeadDim, current_total_kv_len_for_qk, allocated_kv_len_for_value_stride, mScale, parallel_blocks); } else { return MNN::NOT_SUPPORT; } checkKernelErrors; // Combine results dim3 grid_combine(total_heads); dim3 block_combine(mHeadDim); // one thread per D dimension if (mPrecision == 4) { flash_attn_combine_results<<>>( partial_out, partial_meta, getTensorDevicePtr(final_output_tensor), mBatch, mNumHead, mHeadDim, parallel_blocks); } else if (mPrecision == 2) { flash_attn_combine_results<__half><<>>( partial_out, partial_meta, getTensorDevicePtr<__half>(final_output_tensor), mBatch, mNumHead, mHeadDim, parallel_blocks); } } checkKernelErrors; mCache->mPastLength += mNewKvSeqLen; return MNN::NO_ERROR; } // ===================================================================== // Flash Decode with Mask path for speculative/tree decoding // Small seq_len (<=32) with KV cache and additive mask // Uses per-query flash decode with online softmax (1-pass, no temp buffers) // ===================================================================== if (mQuerySeqLen <= 32 && mIsKVCacheEnabled && mHasMask && mIsAddMask) { const int num_threads = 128; const int num_warps = num_threads / 32; const int smem_size = num_warps * sizeof(float); const int total_blocks = mBatch * mNumHead * mQuerySeqLen; dim3 grid(total_blocks); dim3 block(num_threads); if (mPrecision == 4) { flash_decode_kernel_with_mask<<>>( getTensorDevicePtr(query_input_tensor), static_cast(effective_key_cache_ptr), static_cast(effective_value_cache_ptr), getTensorDevicePtr(final_output_tensor), static_cast(getTensorDevicePtr(mask_input_tensor)), mBatch, mNumHead, mKvNumHead, mHeadDim, current_total_kv_len_for_qk, allocated_kv_len_for_value_stride, mQuerySeqLen, mScale); } else if (mPrecision == 2) { flash_decode_kernel_with_mask<__half><<>>( getTensorDevicePtr<__half>(query_input_tensor), static_cast(effective_key_cache_ptr), static_cast(effective_value_cache_ptr), getTensorDevicePtr<__half>(final_output_tensor), static_cast(getTensorDevicePtr(mask_input_tensor)), mBatch, mNumHead, mKvNumHead, mHeadDim, current_total_kv_len_for_qk, allocated_kv_len_for_value_stride, mQuerySeqLen, mScale); } else { return MNN::NOT_SUPPORT; } checkKernelErrors; mCache->mPastLength += mNewKvSeqLen; return MNN::NO_ERROR; } // ===================================================================== // Prefill path (seq_len > 1) or decode with mask // ===================================================================== int max_q_seq_piece_len = UP_DIV(mQuerySeqLen, mQseqSplitNum); ErrorCode temp_buf_err = ensureTempBuffers_gpu(mBatch, mNumHead, max_q_seq_piece_len, current_total_kv_len_for_qk, mHeadDim); if (temp_buf_err != MNN::NO_ERROR) return temp_buf_err; const void* mask_ptr_device = mHasMask ? getTensorDevicePtr(mask_input_tensor) : nullptr; for (int i = 0; i < mQseqSplitNum; ++i) { int q_seq_offset = i * max_q_seq_piece_len; int current_piece_actual_len = std::min(max_q_seq_piece_len, mQuerySeqLen - q_seq_offset); if (current_piece_actual_len <= 0) continue; AttentionKernelParam current_iteration_param_cpu = param_cpu; current_iteration_param_cpu.q_seq_piece_len = current_piece_actual_len; cudaMemcpyAsync(mParam_gpu, ¤t_iteration_param_cpu, sizeof(AttentionKernelParam), cudaMemcpyHostToDevice, stream); checkKernelErrors; // P0: Use tiled QK kernel with shared memory const int QK_TILE = 16; dim3 qk_blockDim(QK_TILE, QK_TILE, 1); dim3 qk_gridDim(UP_DIV(current_total_kv_len_for_qk, QK_TILE), UP_DIV(current_piece_actual_len, QK_TILE), mBatch * mNumHead); if (mPrecision == 4) { qk_kernel_tiled<<>>( getTensorDevicePtr(query_input_tensor), static_cast(effective_key_cache_ptr), getTensorDevicePtr(mTempQK.get()), mask_ptr_device, mParam_gpu, q_seq_offset, mHasMask, mIsAddMask); } else if (mPrecision == 2) { qk_kernel_tiled<__half><<>>( getTensorDevicePtr<__half>(query_input_tensor), static_cast(effective_key_cache_ptr), getTensorDevicePtr<__half>(mTempQK.get()), mask_ptr_device, mParam_gpu, q_seq_offset, mHasMask, mIsAddMask); } else { return MNN::NOT_SUPPORT; } checkKernelErrors; // Softmax (reuse existing optimized implementation) const int axis = current_total_kv_len_for_qk; const int inside = 1; const int outside = mBatch * mNumHead * current_piece_actual_len; const int count = outside * inside; const void* qk_scores_ptr = getTensorDevicePtr(mTempQK.get()); void* softmax_result_ptr = getTensorDevicePtr(mTempSoftmax.get()); if (mPrecision == 4) { const auto* input_ptr = static_cast(qk_scores_ptr); auto* output_ptr = static_cast(softmax_result_ptr); if (axis <= 32) { SOFTMAX_WARP_32<<>>(input_ptr, output_ptr, inside, axis, outside, count); } else { constexpr int threads_per_block = 256; const int calc_multi_num = UP_DIV(axis, threads_per_block); SOFTMAX_AXIS_REDUCE<<>>(input_ptr, output_ptr, inside, axis, threads_per_block, calc_multi_num, outside, count); } } else { const auto* input_ptr = static_cast(qk_scores_ptr); auto* output_ptr = static_cast<__half*>(softmax_result_ptr); if (axis <= 32) { SOFTMAX_WARP_32<__half><<>>(input_ptr, output_ptr, inside, axis, outside, count); } else { constexpr int threads_per_block = 256; const int calc_multi_num = UP_DIV(axis, threads_per_block); SOFTMAX_AXIS_REDUCE<__half><<>>(input_ptr, output_ptr, inside, axis, threads_per_block, calc_multi_num, outside, count); } } checkKernelErrors; // P0: Use optimized QKV kernel with unrolled loop dim3 qkv_blockDim(32, 8, 1); dim3 qkv_gridDim(UP_DIV(mHeadDim, qkv_blockDim.x), UP_DIV(current_piece_actual_len, qkv_blockDim.y), mBatch * mNumHead); if (mPrecision == 4) { qkv_kernel_tiled<<>>( getTensorDevicePtr(mTempSoftmax.get()), static_cast(effective_value_cache_ptr), getTensorDevicePtr(final_output_tensor), mParam_gpu, q_seq_offset); } else if (mPrecision == 2) { qkv_kernel_tiled<__half><<>>( getTensorDevicePtr<__half>(mTempSoftmax.get()), static_cast(effective_value_cache_ptr), getTensorDevicePtr<__half>(final_output_tensor), mParam_gpu, q_seq_offset); } else { return MNN::NOT_SUPPORT; } checkKernelErrors; } if (mIsKVCacheEnabled) { mCache->mPastLength += mNewKvSeqLen; } return MNN::NO_ERROR; } // Creator 注册 class AttentionCreator : public CUDABackend::Creator { public: virtual Execution* onCreate(const std::vector& inputs, const std::vector& outputs, const MNN::Op* op, Backend* backend) const override { bool op_kv_cache_enabled = false; if (op->main_type() == OpParameter_AttentionParam) { auto att_param = op->main_as_AttentionParam(); if (att_param) { op_kv_cache_enabled = att_param->kv_cache(); } } else { MNN_PRINT("[Warning] op->main_type() != OpParameter_AttentionParam\n"); } return new AttentionExecution(backend, op_kv_cache_enabled); } }; static CUDACreatorRegister __init(OpType_Attention); #endif // MNN_SUPPORT_TRANSFORMER_FUSE } // namespace CUDA } // namespace MNN