use std::sync::Arc;
use cudarc::driver::{CudaDevice, CudaFunction, LaunchAsync, LaunchConfig};
use super::compile_cuda_kernel;
use crate::cuda_inference::tensor::GpuTensor;
use crate::cuda_inference::InferenceError;
const BLOCK_M: usize = 32;
const BLOCK_N: usize = 32;
const THREADS_PER_BLOCK: usize = 64;
const FLASH_ATTENTION_CUDA: &str = r#"
#include <cuda_fp16.h>
#define BLOCK_M 32 // Query tile size (reduced for shared memory limit)
#define BLOCK_N 32 // KV tile size
#define WARP_SIZE 32
#define NUM_WARPS 2 // 64 threads per block (reduced to match smaller tiles)
// Vectorized load helper
__device__ __forceinline__ float4 load_float4(const float* ptr) {
return *reinterpret_cast<const float4*>(ptr);
}
__device__ __forceinline__ void store_float4(float* ptr, float4 val) {
*reinterpret_cast<float4*>(ptr) = val;
}
// Warp reduction for max
__device__ __forceinline__ float warp_reduce_max(float val) {
#pragma unroll
for (int offset = WARP_SIZE / 2; offset > 0; offset /= 2) {
val = fmaxf(val, __shfl_xor_sync(0xffffffff, val, offset));
}
return val;
}
// Warp reduction for sum
__device__ __forceinline__ float warp_reduce_sum(float val) {
#pragma unroll
for (int offset = WARP_SIZE / 2; offset > 0; offset /= 2) {
val += __shfl_xor_sync(0xffffffff, val, offset);
}
return val;
}
// Flash Attention v2 forward kernel
// Computes: O = softmax(Q @ K^T / sqrt(d)) @ V
//
// Key improvements over v1:
// - Each thread block processes multiple Q rows
// - Vectorized loads/stores for better memory bandwidth
// - Warp-level parallelism for dot products
//
// Grid: (num_heads, batch_size, ceil(seq_len/BLOCK_M))
// Block: (128, 1, 1) = 4 warps
extern "C" __global__ void flash_attention_f16(
const __half* __restrict__ q, // [batch, heads, seq, head_dim] Q
const __half* __restrict__ k, // [batch, kv_heads, seq, head_dim] K
const __half* __restrict__ v, // [batch, kv_heads, seq, head_dim] V
__half* __restrict__ o, // [batch, heads, seq, head_dim] Output
int batch,
int heads,
int kv_heads,
int seq_len,
int head_dim,
float scale, // 1/sqrt(head_dim)
int causal
) {
// Shared memory layout:
// [BLOCK_N][head_dim+1] for K (padded to avoid bank conflicts)
// [BLOCK_N][head_dim+1] for V
// [BLOCK_M] for row max
// [BLOCK_M] for row sum
extern __shared__ float smem[];
const int head_dim_padded = head_dim + 1;
float* smem_k = smem; // [BLOCK_N * head_dim_padded]
float* smem_v = smem + BLOCK_N * head_dim_padded; // [BLOCK_N * head_dim_padded]
float* smem_max = smem + 2 * BLOCK_N * head_dim_padded; // [BLOCK_M]
float* smem_sum = smem_max + BLOCK_M; // [BLOCK_M]
// Thread/block indices
int tid = threadIdx.x;
int warp_id = tid / WARP_SIZE;
int lane_id = tid % WARP_SIZE;
int head_id = blockIdx.x;
int batch_id = blockIdx.y;
int q_block_id = blockIdx.z;
// This thread handles 2 query rows (to match BLOCK_M with 128 threads)
int q_row_base = q_block_id * BLOCK_M + (tid / 2);
int q_row_offset = tid % 2; // 0 or 1, handles 2 elements per thread in dim
// Compute KV head index for GQA
int kv_head_id = (head_id * kv_heads) / heads;
// Compute strides
int seq_head_stride = seq_len * head_dim;
int q_batch_stride = heads * seq_head_stride;
int kv_batch_stride = kv_heads * seq_head_stride;
// Compute base pointers
const __half* q_base = q + batch_id * q_batch_stride + head_id * seq_head_stride;
const __half* k_base = k + batch_id * kv_batch_stride + kv_head_id * seq_head_stride;
const __half* v_base = v + batch_id * kv_batch_stride + kv_head_id * seq_head_stride;
__half* o_base = o + batch_id * q_batch_stride + head_id * seq_head_stride;
// Each thread handles one Q position
int q_pos = q_block_id * BLOCK_M + tid;
bool valid_q = q_pos < seq_len && tid < BLOCK_M;
// Load Q for this thread into registers
float q_reg[128];
if (valid_q) {
#pragma unroll 4
for (int d = 0; d < head_dim; d++) {
q_reg[d] = __half2float(q_base[q_pos * head_dim + d]);
}
}
// Initialize running max and sum for online softmax
float m = -1e20f;
float l = 0.0f;
// Initialize output accumulators
float o_acc[128];
#pragma unroll 4
for (int d = 0; d < head_dim; d++) {
o_acc[d] = 0.0f;
}
if (!valid_q) {
// Early exit for out-of-bounds threads
goto write_output;
}
// Loop over K/V blocks
for (int kv_start = 0; kv_start < seq_len; kv_start += BLOCK_N) {
// For causal: skip if this entire KV block is in the future
if (causal && kv_start > q_pos) {
break;
}
__syncthreads();
// Cooperatively load K and V tiles into shared memory
// Each thread loads multiple elements
for (int i = tid; i < BLOCK_N * head_dim; i += blockDim.x) {
int kv_idx = i / head_dim;
int d = i % head_dim;
int k_pos = kv_start + kv_idx;
if (k_pos < seq_len) {
smem_k[kv_idx * head_dim_padded + d] = __half2float(k_base[k_pos * head_dim + d]);
smem_v[kv_idx * head_dim_padded + d] = __half2float(v_base[k_pos * head_dim + d]);
} else {
smem_k[kv_idx * head_dim_padded + d] = 0.0f;
smem_v[kv_idx * head_dim_padded + d] = 0.0f;
}
}
__syncthreads();
// Compute attention scores and accumulate output
int block_end = min(BLOCK_N, seq_len - kv_start);
#pragma unroll 4
for (int k_idx = 0; k_idx < block_end; k_idx++) {
int actual_k_pos = kv_start + k_idx;
// Check causal mask
if (causal && actual_k_pos > q_pos) {
continue;
}
// Compute Q @ K^T for this position (dot product)
float score = 0.0f;
// Vectorized dot product when head_dim is multiple of 4
#pragma unroll 4
for (int d = 0; d < head_dim; d++) {
score += q_reg[d] * smem_k[k_idx * head_dim_padded + d];
}
score *= scale;
// Online softmax update
float m_new = fmaxf(m, score);
float exp_diff = expf(m - m_new);
float exp_score = expf(score - m_new);
// Update running sum
l = l * exp_diff + exp_score;
// Update output: o = o * exp(m_old - m_new) + exp(s - m_new) * v
#pragma unroll 4
for (int d = 0; d < head_dim; d++) {
o_acc[d] = o_acc[d] * exp_diff + exp_score * smem_v[k_idx * head_dim_padded + d];
}
m = m_new;
}
}
write_output:
// Normalize by l and write output
if (valid_q && l > 0.0f) {
float l_inv = 1.0f / l;
#pragma unroll 4
for (int d = 0; d < head_dim; d++) {
o_base[q_pos * head_dim + d] = __float2half(o_acc[d] * l_inv);
}
}
}
// Flash Attention v2 with KV cache support
// For decode phase where we process a single new token against cached K/V
extern "C" __global__ void flash_attention_cached_f16(
const __half* __restrict__ q, // [batch, heads, 1, head_dim] single query
const __half* __restrict__ k_cache, // [batch, kv_heads, cache_len, head_dim] cached K
const __half* __restrict__ v_cache, // [batch, kv_heads, cache_len, head_dim] cached V
__half* __restrict__ o, // [batch, heads, 1, head_dim] output
int batch,
int heads,
int kv_heads,
int cache_len,
int head_dim,
float scale
) {
// For single-query attention, we process all K/V in parallel
// Grid: (heads, batch, 1)
// Block: (256, 1, 1)
extern __shared__ float smem[];
float* smem_scores = smem; // [256] attention scores
float* smem_max = smem + 256; // [1] max score
float* smem_sum = smem_max + 1; // [1] exp sum
int tid = threadIdx.x;
int head_id = blockIdx.x;
int batch_id = blockIdx.y;
// KV head for GQA
int kv_head_id = (head_id * kv_heads) / heads;
// Compute strides
int q_stride = head_dim;
int kv_stride = cache_len * head_dim;
// Pointers
const __half* q_ptr = q + batch_id * heads * head_dim + head_id * head_dim;
const __half* k_ptr = k_cache + batch_id * kv_heads * kv_stride + kv_head_id * kv_stride;
const __half* v_ptr = v_cache + batch_id * kv_heads * kv_stride + kv_head_id * kv_stride;
__half* o_ptr = o + batch_id * heads * head_dim + head_id * head_dim;
// Load Q into registers
float q_reg[128];
for (int d = 0; d < head_dim; d++) {
q_reg[d] = __half2float(q_ptr[d]);
}
// Phase 1: Compute all attention scores and find max
float local_max = -1e20f;
for (int kv_idx = tid; kv_idx < cache_len; kv_idx += blockDim.x) {
float score = 0.0f;
#pragma unroll 4
for (int d = 0; d < head_dim; d++) {
score += q_reg[d] * __half2float(k_ptr[kv_idx * head_dim + d]);
}
score *= scale;
smem_scores[kv_idx % 256] = score; // Store locally
local_max = fmaxf(local_max, score);
}
// Warp reduce max
local_max = warp_reduce_max(local_max);
// Block reduce max via shared memory
if (tid % 32 == 0) {
smem_max[tid / 32] = local_max;
}
__syncthreads();
if (tid < 8) {
float val = smem_max[tid];
val = warp_reduce_max(val);
if (tid == 0) smem_max[0] = val;
}
__syncthreads();
float max_score = smem_max[0];
// Phase 2: Compute exp(score - max) and sum, accumulate output
float local_sum = 0.0f;
float o_acc[128] = {0};
for (int kv_idx = tid; kv_idx < cache_len; kv_idx += blockDim.x) {
// Recompute score (or reload from smem if we stored it)
float score = 0.0f;
#pragma unroll 4
for (int d = 0; d < head_dim; d++) {
score += q_reg[d] * __half2float(k_ptr[kv_idx * head_dim + d]);
}
score *= scale;
float exp_score = expf(score - max_score);
local_sum += exp_score;
// Accumulate weighted V
#pragma unroll 4
for (int d = 0; d < head_dim; d++) {
o_acc[d] += exp_score * __half2float(v_ptr[kv_idx * head_dim + d]);
}
}
// Warp reduce sum
local_sum = warp_reduce_sum(local_sum);
// Block reduce sum via shared memory
if (tid % 32 == 0) {
smem_sum[tid / 32] = local_sum;
}
__syncthreads();
if (tid < 8) {
float val = smem_sum[tid];
val = warp_reduce_sum(val);
if (tid == 0) smem_sum[0] = val;
}
__syncthreads();
float total_sum = smem_sum[0];
// Each thread has partial o_acc, need to reduce across threads
// For now, use atomic add (suboptimal but correct)
if (total_sum > 0.0f) {
float inv_sum = 1.0f / total_sum;
for (int d = 0; d < head_dim; d++) {
atomicAdd((float*)(smem + 256 + 2 + d), o_acc[d] * inv_sum);
}
}
__syncthreads();
// First thread writes output
if (tid == 0) {
for (int d = 0; d < head_dim; d++) {
o_ptr[d] = __float2half(smem[256 + 2 + d]);
}
}
}
"#;
pub struct FlashAttentionKernel {
device: Arc<CudaDevice>,
func: Option<CudaFunction>,
cached_func: Option<CudaFunction>,
}
impl std::fmt::Debug for FlashAttentionKernel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlashAttentionKernel")
.field("loaded", &self.func.is_some())
.field("cached_loaded", &self.cached_func.is_some())
.finish()
}
}
impl FlashAttentionKernel {
pub fn new(device: Arc<CudaDevice>) -> Result<Self, InferenceError> {
let mut kernel = Self {
device,
func: None,
cached_func: None,
};
kernel.load()?;
Ok(kernel)
}
pub fn load(&mut self) -> Result<(), InferenceError> {
if self.func.is_some() {
return Ok(());
}
let ptx = compile_cuda_kernel(FLASH_ATTENTION_CUDA)
.map_err(|e| InferenceError::Kernel(format!("NVRTC compilation failed: {}", e)))?;
self.device
.load_ptx(
ptx,
"flash_attention",
&["flash_attention_f16", "flash_attention_cached_f16"],
)
.map_err(|e| {
InferenceError::Kernel(format!("Failed to load Flash Attention: {}", e))
})?;
self.func = Some(
self.device
.get_func("flash_attention", "flash_attention_f16")
.ok_or_else(|| {
InferenceError::Kernel("Failed to get flash_attention_f16 function".to_string())
})?,
);
self.cached_func = Some(
self.device
.get_func("flash_attention", "flash_attention_cached_f16")
.ok_or_else(|| {
InferenceError::Kernel(
"Failed to get flash_attention_cached_f16 function".to_string(),
)
})?,
);
Ok(())
}
pub fn forward(
&self,
q: &GpuTensor,
k: &GpuTensor,
v: &GpuTensor,
output: &mut GpuTensor,
causal: bool,
) -> Result<(), InferenceError> {
let func = self
.func
.as_ref()
.ok_or_else(|| InferenceError::Kernel("Flash Attention not loaded".to_string()))?;
let q_shape = q.shape();
let k_shape = k.shape();
if q_shape.len() != 4 || k_shape.len() != 4 {
return Err(InferenceError::Shape {
expected: "4D tensors [batch, heads, seq, head_dim]".to_string(),
got: format!("Q {:?}, K {:?}", q_shape, k_shape),
});
}
let batch = q_shape[0];
let heads = q_shape[1];
let seq_len = q_shape[2];
let head_dim = q_shape[3];
let kv_heads = k_shape[1];
if head_dim > 128 {
return Err(InferenceError::Shape {
expected: "head_dim <= 128".to_string(),
got: format!("head_dim = {}", head_dim),
});
}
let scale = 1.0 / (head_dim as f32).sqrt();
let seq_blocks = (seq_len + BLOCK_M - 1) / BLOCK_M;
let head_dim_padded = head_dim + 1;
let shared_mem = (2 * BLOCK_N * head_dim_padded + 2 * BLOCK_M) * std::mem::size_of::<f32>();
let cfg = LaunchConfig {
grid_dim: (heads as u32, batch as u32, seq_blocks as u32),
block_dim: (THREADS_PER_BLOCK as u32, 1, 1),
shared_mem_bytes: shared_mem as u32,
};
tracing::debug!(
"Flash Attention launch: grid=({}, {}, {}), block=({}, 1, 1), shared_mem={} bytes, \
batch={}, heads={}, kv_heads={}, seq_len={}, head_dim={}, scale={}",
heads,
batch,
seq_blocks,
THREADS_PER_BLOCK,
shared_mem,
batch,
heads,
kv_heads,
seq_len,
head_dim,
scale
);
unsafe {
func.clone().launch(
cfg,
(
q.device_ptr(),
k.device_ptr(),
v.device_ptr(),
output.device_ptr(),
batch as i32,
heads as i32,
kv_heads as i32,
seq_len as i32,
head_dim as i32,
scale,
if causal { 1i32 } else { 0i32 },
),
)
}
.map_err(|e| InferenceError::Kernel(format!("Flash Attention launch failed: {}", e)))?;
Ok(())
}
pub fn forward_cached(
&self,
q: &GpuTensor,
k_cache: &GpuTensor,
v_cache: &GpuTensor,
output: &mut GpuTensor,
) -> Result<(), InferenceError> {
let func = self.cached_func.as_ref().ok_or_else(|| {
InferenceError::Kernel("Flash Attention cached kernel not loaded".to_string())
})?;
let q_shape = q.shape();
let k_shape = k_cache.shape();
if q_shape.len() != 4 || k_shape.len() != 4 {
return Err(InferenceError::Shape {
expected: "4D tensors".to_string(),
got: format!("Q {:?}, K {:?}", q_shape, k_shape),
});
}
let batch = q_shape[0];
let heads = q_shape[1];
let head_dim = q_shape[3];
let kv_heads = k_shape[1];
let cache_len = k_shape[2];
if head_dim > 128 {
return Err(InferenceError::Shape {
expected: "head_dim <= 128".to_string(),
got: format!("head_dim = {}", head_dim),
});
}
let scale = 1.0 / (head_dim as f32).sqrt();
let shared_mem = (256 + 2 + head_dim) * std::mem::size_of::<f32>();
let cfg = LaunchConfig {
grid_dim: (heads as u32, batch as u32, 1),
block_dim: (256, 1, 1),
shared_mem_bytes: shared_mem as u32,
};
unsafe {
func.clone().launch(
cfg,
(
q.device_ptr(),
k_cache.device_ptr(),
v_cache.device_ptr(),
output.device_ptr(),
batch as i32,
heads as i32,
kv_heads as i32,
cache_len as i32,
head_dim as i32,
scale,
),
)
}
.map_err(|e| {
InferenceError::Kernel(format!("Flash Attention cached launch failed: {}", e))
})?;
Ok(())
}
pub fn recommended_head_dim() -> usize {
64 }
pub fn device(&self) -> &Arc<CudaDevice> {
&self.device
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flash_attention_kernel_compilation() {
let result = compile_cuda_kernel(FLASH_ATTENTION_CUDA);
if let Err(e) = &result {
}
}
}