Files
rustytorch/crates/training/rtx-flash-attention/src/kernels/metal.rs
T
2026-03-04 00:08:42 +00:00

724 lines
27 KiB
Rust

//! Metal Flash Attention kernel implementation for macOS
//!
//! This module provides Metal shader-based Flash Attention implementation
//! for Apple Silicon and macOS GPU acceleration.
#![cfg(feature = "metal")]
use crate::{
config::FlashAttentionConfig,
error::{FlashError, FlashResult},
};
use rtx_tensor::Tensor;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::{NSString, NSError};
use objc2_metal::{
MTLDevice, MTLCommandQueue, MTLComputePipelineState, MTLLibrary,
MTLCreateSystemDefaultDevice, MTLSize, MTLCommandBuffer, MTLComputeCommandEncoder,
MTLBuffer, MTLResourceOptions, MTLCommandEncoder,
};
use std::ptr::NonNull;
use std::sync::Arc;
use std::time::Instant;
use tracing::{debug, info, warn, error};
/// Forward pass block sizes (must match MSL shader BLOCK_Q_FWD, BLOCK_KV_FWD)
/// 32KB threadgroup memory limit: Q(8KB) + K(8KB) + V(8KB) + S(4KB) = 28KB
const BLOCK_Q_FWD: usize = 32;
const BLOCK_KV_FWD: usize = 32;
/// Backward pass block sizes (must match MSL shader BLOCK_Q_BWD, BLOCK_KV_BWD)
/// Backward needs 5 tiles: Q+K+V+dO+O, so use smaller blocks
/// 5 tiles * 16 * 128 * 2 = 20KB
const BLOCK_Q_BWD: usize = 16;
const BLOCK_KV_BWD: usize = 16;
/// Maximum head dimension supported
const MAX_HEAD_DIM: usize = 128;
/// Flash Attention parameters structure matching the MSL shader
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct FlashAttentionParams {
batch_size: u32,
num_heads: u32,
seq_len_q: u32,
seq_len_kv: u32,
head_dim: u32,
softmax_scale: f32,
causal: u32,
block_size_q: u32,
block_size_kv: u32,
}
/// Metal kernel execution result with performance metrics
#[derive(Debug, Clone)]
pub struct MetalKernelResult {
pub execution_time_us: u64,
pub occupancy: f32,
pub memory_throughput: f32,
pub kernel_efficiency: f32,
pub shared_memory_usage: usize,
pub register_usage: usize,
pub tensor_core_utilization: f32,
}
/// Flash Attention Metal kernels implementation
pub struct FlashMetalKernels {
config: FlashAttentionConfig,
device: Retained<ProtocolObject<dyn MTLDevice>>,
command_queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
library: Option<Retained<ProtocolObject<dyn MTLLibrary>>>,
forward_pipeline: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
backward_dq_pipeline: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
backward_dkv_pipeline: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
}
impl std::fmt::Debug for FlashMetalKernels {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlashMetalKernels")
.field("config", &self.config)
.field("has_library", &self.library.is_some())
.field("has_forward_pipeline", &self.forward_pipeline.is_some())
.field("has_backward_dq_pipeline", &self.backward_dq_pipeline.is_some())
.field("has_backward_dkv_pipeline", &self.backward_dkv_pipeline.is_some())
.finish()
}
}
impl FlashMetalKernels {
/// Get the configuration
pub fn config(&self) -> &FlashAttentionConfig {
&self.config
}
/// MSL shader source embedded at compile time
const SHADER_SOURCE: &'static str = include_str!("../../metal/flash_attention.metal");
/// Create new Metal kernels instance
pub fn new(config: &FlashAttentionConfig) -> FlashResult<Self> {
info!("Initializing Metal Flash Attention kernels");
// Get the default Metal device
let device = unsafe { MTLCreateSystemDefaultDevice() }
.ok_or_else(|| FlashError::config("No Metal device available"))?;
// Create command queue
let command_queue = device.newCommandQueue()
.ok_or_else(|| FlashError::config("Failed to create Metal command queue"))?;
info!("Metal device: {}", device.name());
let mut kernels = Self {
config: config.clone(),
device,
command_queue,
library: None,
forward_pipeline: None,
backward_dq_pipeline: None,
backward_dkv_pipeline: None,
};
// Compile shaders and create pipelines
kernels.compile_shaders()?;
info!("Successfully initialized Metal Flash Attention kernels");
Ok(kernels)
}
/// Compile MSL shaders and create compute pipelines
fn compile_shaders(&mut self) -> FlashResult<()> {
info!("Compiling Metal Flash Attention shaders");
// Compile shader source
let source = NSString::from_str(Self::SHADER_SOURCE);
let options: Option<&objc2_metal::MTLCompileOptions> = None;
let library = unsafe {
self.device.newLibraryWithSource_options_error(&source, options)
}.map_err(|e| FlashError::kernel_compilation(format!("Failed to compile Metal shaders: {:?}", e)))?;
// Create forward pipeline
let forward_fn_name = NSString::from_str("flash_attention_forward");
let forward_fn = library.newFunctionWithName(&forward_fn_name)
.ok_or_else(|| FlashError::kernel_compilation("Failed to find flash_attention_forward function"))?;
let forward_pipeline = unsafe {
self.device.newComputePipelineStateWithFunction_error(&forward_fn)
}.map_err(|e| FlashError::kernel_compilation(format!("Failed to create forward pipeline: {:?}", e)))?;
// Create backward dQ pipeline
let backward_dq_fn_name = NSString::from_str("flash_attention_backward_dq");
let backward_dq_fn = library.newFunctionWithName(&backward_dq_fn_name)
.ok_or_else(|| FlashError::kernel_compilation("Failed to find flash_attention_backward_dq function"))?;
let backward_dq_pipeline = unsafe {
self.device.newComputePipelineStateWithFunction_error(&backward_dq_fn)
}.map_err(|e| FlashError::kernel_compilation(format!("Failed to create backward dQ pipeline: {:?}", e)))?;
// Create backward dKV pipeline
let backward_dkv_fn_name = NSString::from_str("flash_attention_backward_dkv");
let backward_dkv_fn = library.newFunctionWithName(&backward_dkv_fn_name)
.ok_or_else(|| FlashError::kernel_compilation("Failed to find flash_attention_backward_dkv function"))?;
let backward_dkv_pipeline = unsafe {
self.device.newComputePipelineStateWithFunction_error(&backward_dkv_fn)
}.map_err(|e| FlashError::kernel_compilation(format!("Failed to create backward dKV pipeline: {:?}", e)))?;
self.library = Some(library);
self.forward_pipeline = Some(forward_pipeline);
self.backward_dq_pipeline = Some(backward_dq_pipeline);
self.backward_dkv_pipeline = Some(backward_dkv_pipeline);
info!("Successfully compiled all Metal Flash Attention pipelines");
Ok(())
}
/// Execute Flash Attention forward pass
pub async fn flash_attention_forward(
&self,
q: &Tensor,
k: &Tensor,
v: &Tensor,
output: &mut Tensor,
lse: &mut Tensor,
softmax_scale: f32,
causal: bool,
) -> FlashResult<MetalKernelResult> {
debug!("Executing Metal Flash Attention forward pass");
// Validate inputs
self.validate_forward_tensors(q, k, v, output, lse)?;
let batch_size = q.shape()[0];
let num_heads = q.shape()[1];
let seq_len_q = q.shape()[2];
let seq_len_kv = k.shape()[2];
let head_dim = q.shape()[3];
if head_dim > MAX_HEAD_DIM {
return Err(FlashError::config(format!(
"Head dimension {} exceeds maximum supported {}",
head_dim, MAX_HEAD_DIM
)));
}
// Get pipeline
let pipeline = self.forward_pipeline.as_ref()
.ok_or_else(|| FlashError::kernel_compilation("Forward pipeline not initialized"))?;
// Create command buffer
let command_buffer = self.command_queue.commandBuffer()
.ok_or_else(|| FlashError::kernel_compilation("Failed to create command buffer"))?;
// Create compute encoder
let encoder = command_buffer.computeCommandEncoder()
.ok_or_else(|| FlashError::kernel_compilation("Failed to create compute encoder"))?;
// Set pipeline state
encoder.setComputePipelineState(pipeline);
// Get Metal buffers from tensors
let q_buffer = self.tensor_to_buffer(q)?;
let k_buffer = self.tensor_to_buffer(k)?;
let v_buffer = self.tensor_to_buffer(v)?;
let output_buffer = self.tensor_to_buffer_mut(output)?;
let lse_buffer = self.tensor_to_buffer_mut(lse)?;
// Set buffers
unsafe {
encoder.setBuffer_offset_atIndex(Some(&q_buffer), 0, 0);
encoder.setBuffer_offset_atIndex(Some(&k_buffer), 0, 1);
encoder.setBuffer_offset_atIndex(Some(&v_buffer), 0, 2);
encoder.setBuffer_offset_atIndex(Some(&output_buffer), 0, 3);
encoder.setBuffer_offset_atIndex(Some(&lse_buffer), 0, 4);
}
// Set parameters
let params = FlashAttentionParams {
batch_size: batch_size as u32,
num_heads: num_heads as u32,
seq_len_q: seq_len_q as u32,
seq_len_kv: seq_len_kv as u32,
head_dim: head_dim as u32,
softmax_scale,
causal: if causal { 1 } else { 0 },
block_size_q: BLOCK_Q_FWD as u32,
block_size_kv: BLOCK_KV_FWD as u32,
};
unsafe {
let params_ptr = NonNull::new_unchecked(
&params as *const FlashAttentionParams as *mut std::ffi::c_void
);
encoder.setBytes_length_atIndex(
params_ptr,
std::mem::size_of::<FlashAttentionParams>(),
5
);
}
// Calculate threadgroup and grid sizes (using forward block sizes)
let block_q = BLOCK_Q_FWD.min(seq_len_q);
let num_q_blocks = (seq_len_q + block_q - 1) / block_q;
let threadgroup_size = MTLSize {
width: block_q,
height: 1,
depth: 1,
};
let grid_size = MTLSize {
width: num_q_blocks,
height: num_heads,
depth: batch_size,
};
// Dispatch
encoder.dispatchThreadgroups_threadsPerThreadgroup(grid_size, threadgroup_size);
encoder.endEncoding();
// Execute and measure time
let start = Instant::now();
command_buffer.commit();
command_buffer.waitUntilCompleted();
let execution_time_us = start.elapsed().as_micros() as u64;
// Check for errors
if let Some(error) = command_buffer.error() {
return Err(FlashError::kernel_compilation(format!(
"Metal command buffer execution failed: {}",
error.localizedDescription()
)));
}
// Calculate metrics
let total_bytes = (batch_size * num_heads * seq_len_q * head_dim * 2 * 4) as f32; // Q, K, V, O in half precision
let memory_throughput = if execution_time_us > 0 {
total_bytes / (execution_time_us as f32 / 1_000_000.0) / 1e9 // GB/s
} else {
0.0
};
// Forward: Q_tile(8KB) + K_tile(8KB) + V_tile(8KB) + S_tile(4KB) = 28KB
let shared_memory_per_block = (BLOCK_Q_FWD * MAX_HEAD_DIM + 2 * BLOCK_KV_FWD * MAX_HEAD_DIM) * 2 // Q, K, V in half
+ BLOCK_Q_FWD * BLOCK_KV_FWD * 4; // S in float
debug!(
"Flash Attention forward completed in {}us, throughput: {:.2} GB/s",
execution_time_us, memory_throughput
);
Ok(MetalKernelResult {
execution_time_us,
occupancy: 0.85, // Estimate
memory_throughput,
kernel_efficiency: 0.80, // Estimate
shared_memory_usage: shared_memory_per_block,
register_usage: 64, // Estimate
tensor_core_utilization: 0.0, // Metal doesn't have tensor cores
})
}
/// Execute Flash Attention backward pass
pub async fn flash_attention_backward(
&self,
dout: &Tensor,
q: &Tensor,
k: &Tensor,
v: &Tensor,
output: &Tensor,
lse: &Tensor,
grad_q: &mut Tensor,
grad_k: &mut Tensor,
grad_v: &mut Tensor,
softmax_scale: f32,
causal: bool,
) -> FlashResult<MetalKernelResult> {
debug!("Executing Metal Flash Attention backward pass");
let batch_size = q.shape()[0];
let num_heads = q.shape()[1];
let seq_len_q = q.shape()[2];
let seq_len_kv = k.shape()[2];
let head_dim = q.shape()[3];
if head_dim > MAX_HEAD_DIM {
return Err(FlashError::config(format!(
"Head dimension {} exceeds maximum supported {}",
head_dim, MAX_HEAD_DIM
)));
}
// Get pipelines
let dq_pipeline = self.backward_dq_pipeline.as_ref()
.ok_or_else(|| FlashError::kernel_compilation("Backward dQ pipeline not initialized"))?;
let dkv_pipeline = self.backward_dkv_pipeline.as_ref()
.ok_or_else(|| FlashError::kernel_compilation("Backward dKV pipeline not initialized"))?;
// Create Metal buffers
let dout_buffer = self.tensor_to_buffer(dout)?;
let q_buffer = self.tensor_to_buffer(q)?;
let k_buffer = self.tensor_to_buffer(k)?;
let v_buffer = self.tensor_to_buffer(v)?;
let output_buffer = self.tensor_to_buffer(output)?;
let lse_buffer = self.tensor_to_buffer(lse)?;
let grad_q_buffer = self.tensor_to_buffer_mut(grad_q)?;
let grad_k_buffer = self.tensor_to_buffer_mut(grad_k)?;
let grad_v_buffer = self.tensor_to_buffer_mut(grad_v)?;
// Backward pass uses smaller block sizes (16x16) to fit 5 tiles in 32KB
let params = FlashAttentionParams {
batch_size: batch_size as u32,
num_heads: num_heads as u32,
seq_len_q: seq_len_q as u32,
seq_len_kv: seq_len_kv as u32,
head_dim: head_dim as u32,
softmax_scale,
causal: if causal { 1 } else { 0 },
block_size_q: BLOCK_Q_BWD as u32,
block_size_kv: BLOCK_KV_BWD as u32,
};
let start = Instant::now();
// === Execute backward dQ kernel ===
{
let command_buffer = self.command_queue.commandBuffer()
.ok_or_else(|| FlashError::kernel_compilation("Failed to create command buffer for dQ"))?;
let encoder = command_buffer.computeCommandEncoder()
.ok_or_else(|| FlashError::kernel_compilation("Failed to create compute encoder for dQ"))?;
encoder.setComputePipelineState(dq_pipeline);
unsafe {
encoder.setBuffer_offset_atIndex(Some(&dout_buffer), 0, 0);
encoder.setBuffer_offset_atIndex(Some(&q_buffer), 0, 1);
encoder.setBuffer_offset_atIndex(Some(&k_buffer), 0, 2);
encoder.setBuffer_offset_atIndex(Some(&v_buffer), 0, 3);
encoder.setBuffer_offset_atIndex(Some(&output_buffer), 0, 4);
encoder.setBuffer_offset_atIndex(Some(&lse_buffer), 0, 5);
encoder.setBuffer_offset_atIndex(Some(&grad_q_buffer), 0, 6);
let params_ptr = NonNull::new_unchecked(
&params as *const FlashAttentionParams as *mut std::ffi::c_void
);
encoder.setBytes_length_atIndex(
params_ptr,
std::mem::size_of::<FlashAttentionParams>(),
7
);
}
// Backward uses smaller blocks (16x16)
let block_q = BLOCK_Q_BWD.min(seq_len_q);
let num_q_blocks = (seq_len_q + block_q - 1) / block_q;
let threadgroup_size = MTLSize { width: block_q, height: 1, depth: 1 };
let grid_size = MTLSize {
width: num_q_blocks,
height: num_heads,
depth: batch_size,
};
encoder.dispatchThreadgroups_threadsPerThreadgroup(grid_size, threadgroup_size);
encoder.endEncoding();
command_buffer.commit();
command_buffer.waitUntilCompleted();
if let Some(error) = command_buffer.error() {
return Err(FlashError::kernel_compilation(format!(
"Metal backward dQ execution failed: {}",
error.localizedDescription()
)));
}
}
// === Execute backward dKV kernel ===
{
let command_buffer = self.command_queue.commandBuffer()
.ok_or_else(|| FlashError::kernel_compilation("Failed to create command buffer for dKV"))?;
let encoder = command_buffer.computeCommandEncoder()
.ok_or_else(|| FlashError::kernel_compilation("Failed to create compute encoder for dKV"))?;
encoder.setComputePipelineState(dkv_pipeline);
unsafe {
encoder.setBuffer_offset_atIndex(Some(&dout_buffer), 0, 0);
encoder.setBuffer_offset_atIndex(Some(&q_buffer), 0, 1);
encoder.setBuffer_offset_atIndex(Some(&k_buffer), 0, 2);
encoder.setBuffer_offset_atIndex(Some(&v_buffer), 0, 3);
encoder.setBuffer_offset_atIndex(Some(&output_buffer), 0, 4);
encoder.setBuffer_offset_atIndex(Some(&lse_buffer), 0, 5);
encoder.setBuffer_offset_atIndex(Some(&grad_k_buffer), 0, 6);
encoder.setBuffer_offset_atIndex(Some(&grad_v_buffer), 0, 7);
let params_ptr = NonNull::new_unchecked(
&params as *const FlashAttentionParams as *mut std::ffi::c_void
);
encoder.setBytes_length_atIndex(
params_ptr,
std::mem::size_of::<FlashAttentionParams>(),
8
);
}
// Backward uses smaller blocks (16x16)
let block_kv = BLOCK_KV_BWD.min(seq_len_kv);
let num_kv_blocks = (seq_len_kv + block_kv - 1) / block_kv;
let threadgroup_size = MTLSize { width: block_kv, height: 1, depth: 1 };
let grid_size = MTLSize {
width: num_kv_blocks,
height: num_heads,
depth: batch_size,
};
encoder.dispatchThreadgroups_threadsPerThreadgroup(grid_size, threadgroup_size);
encoder.endEncoding();
command_buffer.commit();
command_buffer.waitUntilCompleted();
if let Some(error) = command_buffer.error() {
return Err(FlashError::kernel_compilation(format!(
"Metal backward dKV execution failed: {}",
error.localizedDescription()
)));
}
}
let execution_time_us = start.elapsed().as_micros() as u64;
// Calculate metrics
let total_bytes = (batch_size * num_heads * seq_len_q * head_dim * 2 * 7) as f32; // Multiple tensors
let memory_throughput = if execution_time_us > 0 {
total_bytes / (execution_time_us as f32 / 1_000_000.0) / 1e9
} else {
0.0
};
debug!(
"Flash Attention backward completed in {}us, throughput: {:.2} GB/s",
execution_time_us, memory_throughput
);
// Backward: 5 tiles * 16 * 128 * 2 = 20KB
let shared_memory_per_block = 5 * BLOCK_Q_BWD * MAX_HEAD_DIM * 2; // Q, K, V, dO, O in half
Ok(MetalKernelResult {
execution_time_us,
occupancy: 0.80,
memory_throughput,
kernel_efficiency: 0.75,
shared_memory_usage: shared_memory_per_block,
register_usage: 96,
tensor_core_utilization: 0.0,
})
}
/// Convert tensor to Metal buffer (for read-only access)
fn tensor_to_buffer(&self, tensor: &Tensor) -> FlashResult<Retained<ProtocolObject<dyn MTLBuffer>>> {
let data = tensor.data()
.map_err(|e| FlashError::memory(format!("Failed to get tensor data: {}", e)))?;
let byte_length = data.len() * std::mem::size_of::<f32>();
// Create buffer with shared storage mode
let buffer = self.device.newBufferWithLength_options(
byte_length,
MTLResourceOptions::StorageModeShared
).ok_or_else(|| FlashError::memory("Failed to create Metal buffer"))?;
// Copy data to buffer
let ptr = buffer.contents();
unsafe {
std::ptr::copy_nonoverlapping(
data.as_ptr(),
ptr.as_ptr() as *mut f32,
data.len()
);
}
Ok(buffer)
}
/// Convert tensor to Metal buffer (for write access)
/// Note: This creates a buffer from current tensor data. After GPU execution,
/// results need to be copied back to the tensor.
fn tensor_to_buffer_mut(&self, tensor: &mut Tensor) -> FlashResult<Retained<ProtocolObject<dyn MTLBuffer>>> {
let data = tensor.data()
.map_err(|e| FlashError::memory(format!("Failed to get tensor data: {}", e)))?;
let byte_length = data.len() * std::mem::size_of::<f32>();
// Create buffer with shared storage mode
let buffer = self.device.newBufferWithLength_options(
byte_length,
MTLResourceOptions::StorageModeShared
).ok_or_else(|| FlashError::memory("Failed to create Metal buffer"))?;
// Copy data to buffer
let ptr = buffer.contents();
unsafe {
std::ptr::copy_nonoverlapping(
data.as_ptr(),
ptr.as_ptr() as *mut f32,
data.len()
);
}
Ok(buffer)
}
/// Optimize block sizes for Metal hardware (returns forward pass block sizes)
///
/// Apple Silicon has 32KB threadgroup memory limit.
/// Forward: Q(8KB) + K(8KB) + V(8KB) + S(4KB) = 28KB with 32x32 blocks
/// Backward: 5 tiles = 20KB with 16x16 blocks
pub fn optimize_block_sizes(&self, seq_len: usize, _head_dim: usize) -> (usize, usize) {
// Forward block sizes (32x32 fits in 32KB threadgroup memory)
let optimal_q = BLOCK_Q_FWD.min(seq_len);
let optimal_kv = BLOCK_KV_FWD.min(seq_len);
// Ensure alignment to simdgroup size (32 threads for Apple Silicon)
let optimal_q = (optimal_q / 32) * 32;
let optimal_kv = (optimal_kv / 32) * 32;
let optimal_q = optimal_q.max(32);
let optimal_kv = optimal_kv.max(32);
debug!(
"Optimized Metal block sizes: Q={}, KV={} for seq_len={}",
optimal_q, optimal_kv, seq_len
);
(optimal_q, optimal_kv)
}
/// Get Metal device information
pub fn get_device_info(&self) -> FlashResult<String> {
let device_name = self.device.name().to_string();
let max_threadgroup = self.forward_pipeline.as_ref()
.map(|p| p.maxTotalThreadsPerThreadgroup())
.unwrap_or(0);
Ok(format!(
"Metal Device: {}, Max Threadgroup Size: {}",
device_name, max_threadgroup
))
}
/// Validate tensor dimensions for forward pass
fn validate_forward_tensors(
&self,
q: &Tensor,
k: &Tensor,
v: &Tensor,
output: &Tensor,
lse: &Tensor,
) -> FlashResult<()> {
let q_shape = q.shape();
let k_shape = k.shape();
let v_shape = v.shape();
let output_shape = output.shape();
let lse_shape = lse.shape();
// Check all tensors have 4D shape except LSE (3D)
if q_shape.len() != 4 || k_shape.len() != 4 || v_shape.len() != 4 || output_shape.len() != 4 {
return Err(FlashError::config(
"Q, K, V, and output tensors must be 4D [batch, heads, seq_len, head_dim]"
));
}
if lse_shape.len() != 3 {
return Err(FlashError::config(
"LSE tensor must be 3D [batch, heads, seq_len]"
));
}
// Check batch and head dimensions match
if q_shape[0] != k_shape[0] || q_shape[0] != v_shape[0] || q_shape[0] != output_shape[0] {
return Err(FlashError::config("Batch dimensions must match"));
}
if q_shape[1] != k_shape[1] || q_shape[1] != v_shape[1] || q_shape[1] != output_shape[1] {
return Err(FlashError::config("Number of heads must match"));
}
// Check head_dim matches
if q_shape[3] != k_shape[3] || q_shape[3] != v_shape[3] || q_shape[3] != output_shape[3] {
return Err(FlashError::config("Head dimensions must match"));
}
// Q and output seq_len must match
if q_shape[2] != output_shape[2] {
return Err(FlashError::config("Q and output sequence lengths must match"));
}
// K and V seq_len must match
if k_shape[2] != v_shape[2] {
return Err(FlashError::config("K and V sequence lengths must match"));
}
// Check LSE shape matches first 3 dimensions of Q
if lse_shape[0] != q_shape[0] || lse_shape[1] != q_shape[1] || lse_shape[2] != q_shape[2] {
return Err(FlashError::config(
"LSE tensor shape must match [batch, heads, seq_len] dimensions of Q"
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metal_device_creation() {
// Test that we can create a Metal device
let device = unsafe { MTLCreateSystemDefaultDevice() };
assert!(device.is_some(), "Metal device should be available on macOS");
}
#[test]
fn test_metal_kernels_creation() {
let config = FlashAttentionConfig::new(8, 64);
let result = FlashMetalKernels::new(&config);
// This should succeed on macOS with Metal support
if let Ok(kernels) = result {
assert!(kernels.forward_pipeline.is_some(), "Forward pipeline should be created");
assert!(kernels.backward_dq_pipeline.is_some(), "Backward dQ pipeline should be created");
assert!(kernels.backward_dkv_pipeline.is_some(), "Backward dKV pipeline should be created");
assert!(kernels.get_device_info().is_ok());
}
}
#[test]
fn test_block_size_optimization() {
let config = FlashAttentionConfig::new(8, 64);
if let Ok(kernels) = FlashMetalKernels::new(&config) {
let (block_q, block_kv) = kernels.optimize_block_sizes(1024, 64);
assert!(block_q >= 32);
assert!(block_q <= 256);
assert!(block_kv >= 32);
assert!(block_kv <= 256);
}
}
#[test]
fn test_params_size() {
// Ensure params struct has expected size for Metal buffer alignment
assert_eq!(std::mem::size_of::<FlashAttentionParams>(), 36);
}
}