Skip to main content

haagenti_sparse/
kernel.rs

1//! Sparse attention kernel execution
2
3use crate::{AttentionMask, Result, SparseError};
4use serde::{Deserialize, Serialize};
5
6/// Configuration for sparse attention kernel
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct KernelConfig {
9    /// Hidden dimension
10    pub hidden_dim: usize,
11    /// Number of heads
12    pub num_heads: usize,
13    /// Head dimension
14    pub head_dim: usize,
15    /// Sequence length
16    pub seq_len: usize,
17    /// Use flash attention
18    pub use_flash: bool,
19    /// Memory format (contiguous, channels_last, etc.)
20    pub memory_format: MemoryFormat,
21}
22
23/// Memory layout format
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum MemoryFormat {
26    /// Standard contiguous layout [B, S, H, D]
27    Contiguous,
28    /// Channels last [B, H, S, D]
29    ChannelsLast,
30    /// Grouped format for sparse [B, G, S, D] where G = active heads
31    Grouped,
32}
33
34impl Default for KernelConfig {
35    fn default() -> Self {
36        Self {
37            hidden_dim: 2048,
38            num_heads: 32,
39            head_dim: 64,
40            seq_len: 4096,
41            use_flash: true,
42            memory_format: MemoryFormat::Contiguous,
43        }
44    }
45}
46
47/// Manager for sparse attention kernel execution.
48///
49/// Handles the execution of attention computations with dynamic head sparsity,
50/// including index mapping caching for efficient repeated execution with the
51/// same sparsity pattern.
52///
53/// # Performance Optimizations
54///
55/// - Pre-computes index mappings for gather/scatter operations
56/// - Caches mappings by pattern hash for repeated use
57/// - Supports flash attention for memory efficiency
58/// - Handles different memory layouts (contiguous, channels-last, grouped)
59///
60/// # Example
61///
62/// ```ignore
63/// let mut kernel = SparseKernel::new(KernelConfig::default());
64/// kernel.prepare(&mask, layer_idx)?;
65/// let output = kernel.execute(&query, &key, &value, &mask, layer_idx)?;
66/// ```
67#[derive(Debug)]
68pub struct SparseKernel {
69    config: KernelConfig,
70    /// Precomputed index mappings for each sparsity pattern
71    index_cache: std::collections::HashMap<u64, IndexMapping>,
72}
73
74/// Index mapping for sparse computation
75#[derive(Debug, Clone)]
76struct IndexMapping {
77    /// Active head indices
78    active_heads: Vec<usize>,
79    /// Output scatter indices (for GPU kernel scatter operation)
80    #[allow(dead_code)]
81    scatter_indices: Vec<usize>,
82    /// Pattern hash (for cache lookup validation)
83    #[allow(dead_code)]
84    pattern_hash: u64,
85}
86
87impl SparseKernel {
88    /// Create a new kernel with config
89    pub fn new(config: KernelConfig) -> Self {
90        Self {
91            config,
92            index_cache: std::collections::HashMap::new(),
93        }
94    }
95
96    /// Prepare kernel for a specific mask
97    pub fn prepare(&mut self, mask: &AttentionMask, layer: usize) -> Result<()> {
98        let pattern_hash = self.compute_pattern_hash(mask, layer);
99
100        self.index_cache.entry(pattern_hash).or_insert_with(|| {
101            let active_heads = mask.active_heads(layer);
102            let scatter_indices: Vec<usize> =
103                active_heads.iter().enumerate().map(|(i, _)| i).collect();
104
105            IndexMapping {
106                active_heads,
107                scatter_indices,
108                pattern_hash,
109            }
110        });
111
112        Ok(())
113    }
114
115    /// Execute sparse attention for a layer
116    ///
117    /// This is a simulated implementation. In practice, this would:
118    /// 1. Gather only active Q, K, V heads
119    /// 2. Compute attention only for active heads
120    /// 3. Scatter results back to full head positions
121    pub fn execute(
122        &self,
123        mask: &AttentionMask,
124        layer: usize,
125        _q: &[f32], // [batch, seq, num_heads, head_dim]
126        _k: &[f32],
127        _v: &[f32],
128    ) -> Result<Vec<f32>> {
129        let pattern_hash = self.compute_pattern_hash(mask, layer);
130
131        let mapping = self
132            .index_cache
133            .get(&pattern_hash)
134            .ok_or_else(|| SparseError::KernelError("Mask pattern not prepared".into()))?;
135
136        // Simulated output
137        let batch_size = 1; // Would be inferred from input
138        let output_size =
139            batch_size * self.config.seq_len * self.config.num_heads * self.config.head_dim;
140
141        // In real implementation:
142        // 1. Extract active heads from Q, K, V
143        // 2. Compute attention: softmax(QK^T / sqrt(d)) * V
144        // 3. Scatter back to full size
145
146        let mut output = vec![0.0f32; output_size];
147
148        // Mark active positions (simulated)
149        for &head in &mapping.active_heads {
150            let offset = head * self.config.head_dim;
151            for d in 0..self.config.head_dim {
152                if offset + d < output.len() {
153                    output[offset + d] = 1.0; // Placeholder
154                }
155            }
156        }
157
158        Ok(output)
159    }
160
161    /// Compute hash for mask pattern at a layer
162    fn compute_pattern_hash(&self, mask: &AttentionMask, layer: usize) -> u64 {
163        use std::hash::{Hash, Hasher};
164        let mut hasher = std::collections::hash_map::DefaultHasher::new();
165
166        layer.hash(&mut hasher);
167        for head in 0..mask.num_heads {
168            mask.is_active(layer, head).hash(&mut hasher);
169        }
170
171        hasher.finish()
172    }
173
174    /// Estimate compute savings for a mask
175    pub fn estimate_savings(&self, mask: &AttentionMask) -> ComputeEstimate {
176        let total_heads = mask.num_heads * mask.num_layers;
177        let active_heads: usize = (0..mask.num_layers).map(|l| mask.active_count(l)).sum();
178
179        let compute_ratio = active_heads as f32 / total_heads as f32;
180
181        // Memory savings from not loading inactive weights
182        let memory_ratio = compute_ratio * 0.9 + 0.1; // Some overhead
183
184        // Attention compute is quadratic in heads for multi-head attention
185        let attention_ratio = compute_ratio; // Linear for independent heads
186
187        ComputeEstimate {
188            compute_ratio,
189            memory_ratio,
190            attention_ratio,
191            estimated_speedup: 1.0 / compute_ratio,
192            active_heads,
193            total_heads,
194        }
195    }
196
197    /// Get current configuration
198    pub fn config(&self) -> &KernelConfig {
199        &self.config
200    }
201
202    /// Clear cached index mappings
203    pub fn clear_cache(&mut self) {
204        self.index_cache.clear();
205    }
206}
207
208/// Estimate of compute and memory savings
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ComputeEstimate {
211    /// Fraction of compute used (1.0 = full, 0.5 = half)
212    pub compute_ratio: f32,
213    /// Fraction of memory bandwidth used
214    pub memory_ratio: f32,
215    /// Fraction of attention compute
216    pub attention_ratio: f32,
217    /// Estimated speedup (e.g., 2.0 = 2x faster)
218    pub estimated_speedup: f32,
219    /// Number of active heads
220    pub active_heads: usize,
221    /// Total heads across all layers
222    pub total_heads: usize,
223}
224
225/// Kernel statistics
226#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227pub struct KernelStats {
228    /// Total executions
229    pub executions: u64,
230    /// Cache hits
231    pub cache_hits: u64,
232    /// Average sparsity
233    pub avg_sparsity: f32,
234    /// Total compute saved (estimated GFLOPs)
235    pub compute_saved: f64,
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_kernel_prepare() {
244        let mut kernel = SparseKernel::new(KernelConfig::default());
245        let mask = AttentionMask::random(32, 10, 0.5);
246
247        kernel.prepare(&mask, 0).unwrap();
248        kernel.prepare(&mask, 5).unwrap();
249
250        // Cache should have entries
251        assert!(!kernel.index_cache.is_empty());
252    }
253
254    #[test]
255    fn test_compute_estimate() {
256        let kernel = SparseKernel::new(KernelConfig::default());
257        let mask = AttentionMask::random(32, 10, 0.5);
258
259        let estimate = kernel.estimate_savings(&mask);
260
261        // With 50% sparsity, should see roughly 50% compute
262        assert!(estimate.compute_ratio > 0.4 && estimate.compute_ratio < 0.7);
263        assert!(estimate.estimated_speedup > 1.4 && estimate.estimated_speedup < 2.5);
264    }
265
266    #[test]
267    fn test_execute() {
268        let mut kernel = SparseKernel::new(KernelConfig {
269            num_heads: 8,
270            head_dim: 64,
271            seq_len: 16,
272            ..Default::default()
273        });
274
275        let mask = AttentionMask::random(8, 4, 0.5);
276        kernel.prepare(&mask, 0).unwrap();
277
278        // Create dummy inputs
279        let q = vec![0.0f32; 16 * 8 * 64];
280        let k = vec![0.0f32; 16 * 8 * 64];
281        let v = vec![0.0f32; 16 * 8 * 64];
282
283        let output = kernel.execute(&mask, 0, &q, &k, &v).unwrap();
284        assert!(!output.is_empty());
285    }
286}