Skip to main content

haagenti_sparse/
analysis.rs

1//! Head importance analysis for sparse attention
2
3use crate::{CategoryMapping, HeadCategory, Result, SparseError};
4use serde::{Deserialize, Serialize};
5
6/// Importance score for a single attention head
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct HeadImportance {
9    /// Layer index
10    pub layer: usize,
11    /// Head index
12    pub head: usize,
13    /// Importance score (0.0 - 1.0)
14    pub importance: f32,
15    /// Variance of importance across samples
16    pub variance: f32,
17    /// Assigned category
18    pub category: HeadCategory,
19    /// Activation frequency (0.0 - 1.0)
20    pub activation_rate: f32,
21}
22
23/// Complete analysis of all attention heads
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct HeadAnalysis {
26    /// Model identifier
27    pub model_id: String,
28    /// Number of heads per layer
29    pub num_heads: usize,
30    /// Number of layers
31    pub num_layers: usize,
32    /// Per-head importance data
33    pub heads: Vec<HeadImportance>,
34    /// Category mapping derived from analysis
35    pub category_mapping: CategoryMapping,
36    /// Global importance threshold for pruning
37    pub prune_threshold: f32,
38    /// Analysis metadata
39    pub metadata: AnalysisMetadata,
40}
41
42/// Metadata about how the analysis was performed
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AnalysisMetadata {
45    /// Number of samples used
46    pub num_samples: usize,
47    /// Prompt categories analyzed
48    pub categories_analyzed: Vec<String>,
49    /// Analysis timestamp
50    pub timestamp: u64,
51    /// Analysis method
52    pub method: AnalysisMethod,
53}
54
55/// Method used for importance analysis
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum AnalysisMethod {
58    /// Gradient-based importance (attention gradients)
59    Gradient,
60    /// Activation-based (attention weight magnitudes)
61    Activation,
62    /// Ablation-based (output difference when head removed)
63    Ablation,
64    /// Distillation-based (learned importance)
65    Distillation,
66}
67
68impl HeadAnalysis {
69    /// Get importance for a specific head
70    pub fn get_importance(&self, layer: usize, head: usize) -> Option<&HeadImportance> {
71        self.heads
72            .iter()
73            .find(|h| h.layer == layer && h.head == head)
74    }
75
76    /// Get all heads above importance threshold
77    pub fn important_heads(&self, threshold: f32) -> Vec<&HeadImportance> {
78        self.heads
79            .iter()
80            .filter(|h| h.importance >= threshold)
81            .collect()
82    }
83
84    /// Get heads by category
85    pub fn heads_by_category(&self, category: HeadCategory) -> Vec<&HeadImportance> {
86        self.heads
87            .iter()
88            .filter(|h| h.category == category)
89            .collect()
90    }
91
92    /// Get layer-wise importance distribution
93    pub fn layer_importance(&self) -> Vec<f32> {
94        let mut layer_sums = vec![0.0f32; self.num_layers];
95        let mut layer_counts = vec![0usize; self.num_layers];
96
97        for head in &self.heads {
98            layer_sums[head.layer] += head.importance;
99            layer_counts[head.layer] += 1;
100        }
101
102        layer_sums
103            .iter()
104            .zip(layer_counts.iter())
105            .map(|(&sum, &count)| if count > 0 { sum / count as f32 } else { 0.0 })
106            .collect()
107    }
108
109    /// Suggest optimal sparsity per layer based on importance
110    pub fn suggested_sparsity(&self, target_overall: f32) -> Vec<f32> {
111        let layer_imp = self.layer_importance();
112        let mean_imp: f32 = layer_imp.iter().sum::<f32>() / layer_imp.len() as f32;
113
114        // Higher importance layers get less sparsity
115        layer_imp
116            .iter()
117            .map(|&imp| {
118                let ratio = if mean_imp > 0.0 { imp / mean_imp } else { 1.0 };
119                // Inverse relationship: more important = less sparse
120                (target_overall * (2.0 - ratio)).clamp(0.1, 0.9)
121            })
122            .collect()
123    }
124}
125
126/// Analyzer for computing head importance
127pub struct ImportanceAnalyzer {
128    /// Analysis method to use
129    method: AnalysisMethod,
130    /// Number of samples to collect
131    num_samples: usize,
132    /// Collected activation data
133    activations: Vec<ActivationSample>,
134}
135
136/// A single activation sample
137#[derive(Debug, Clone)]
138struct ActivationSample {
139    /// Layer activations `[layer][head]`
140    attention_weights: Vec<Vec<f32>>,
141    /// Prompt category
142    category: String,
143    /// Step number (stored for future step-aware analysis)
144    #[allow(dead_code)]
145    step: u32,
146}
147
148impl ImportanceAnalyzer {
149    /// Create a new analyzer
150    pub fn new(method: AnalysisMethod) -> Self {
151        Self {
152            method,
153            num_samples: 0,
154            activations: Vec::new(),
155        }
156    }
157
158    /// Set target number of samples
159    pub fn with_samples(mut self, count: usize) -> Self {
160        self.num_samples = count;
161        self
162    }
163
164    /// Record an activation sample
165    pub fn record_sample(&mut self, attention_weights: Vec<Vec<f32>>, category: String, step: u32) {
166        self.activations.push(ActivationSample {
167            attention_weights,
168            category,
169            step,
170        });
171    }
172
173    /// Analyze collected samples and produce head importance
174    pub fn analyze(&self, model_id: &str) -> Result<HeadAnalysis> {
175        if self.activations.is_empty() {
176            return Err(SparseError::AnalysisError("No samples collected".into()));
177        }
178
179        let num_layers = self.activations[0].attention_weights.len();
180        let num_heads = self.activations[0]
181            .attention_weights
182            .first()
183            .map(|l| l.len())
184            .unwrap_or(0);
185
186        // Compute importance based on method
187        let heads = match self.method {
188            AnalysisMethod::Activation => self.analyze_activation(num_layers, num_heads),
189            AnalysisMethod::Gradient => self.analyze_activation(num_layers, num_heads), // Fallback
190            AnalysisMethod::Ablation => self.analyze_activation(num_layers, num_heads), // Fallback
191            AnalysisMethod::Distillation => self.analyze_activation(num_layers, num_heads), // Fallback
192        };
193
194        // Build category mapping from importance patterns
195        let category_mapping = self.infer_categories(&heads, num_heads, num_layers);
196
197        // Calculate prune threshold (median importance)
198        let mut importances: Vec<f32> = heads.iter().map(|h| h.importance).collect();
199        importances.sort_by(|a, b| a.partial_cmp(b).unwrap());
200        let prune_threshold = importances
201            .get(importances.len() / 2)
202            .copied()
203            .unwrap_or(0.5);
204
205        let categories_analyzed: Vec<String> = self
206            .activations
207            .iter()
208            .map(|s| s.category.clone())
209            .collect::<std::collections::HashSet<_>>()
210            .into_iter()
211            .collect();
212
213        Ok(HeadAnalysis {
214            model_id: model_id.into(),
215            num_heads,
216            num_layers,
217            heads,
218            category_mapping,
219            prune_threshold,
220            metadata: AnalysisMetadata {
221                num_samples: self.activations.len(),
222                categories_analyzed,
223                timestamp: std::time::SystemTime::now()
224                    .duration_since(std::time::UNIX_EPOCH)
225                    .map(|d| d.as_secs())
226                    .unwrap_or(0),
227                method: self.method,
228            },
229        })
230    }
231
232    /// Analyze using activation magnitudes
233    fn analyze_activation(&self, num_layers: usize, num_heads: usize) -> Vec<HeadImportance> {
234        let mut heads = Vec::with_capacity(num_layers * num_heads);
235
236        for layer in 0..num_layers {
237            for head in 0..num_heads {
238                // Collect all activation values for this head
239                let values: Vec<f32> = self
240                    .activations
241                    .iter()
242                    .filter_map(|s| {
243                        s.attention_weights
244                            .get(layer)
245                            .and_then(|l| l.get(head))
246                            .copied()
247                    })
248                    .collect();
249
250                if values.is_empty() {
251                    continue;
252                }
253
254                // Compute mean (importance) and variance
255                let mean: f32 = values.iter().sum::<f32>() / values.len() as f32;
256                let variance: f32 =
257                    values.iter().map(|v| (v - mean).powi(2)).sum::<f32>() / values.len() as f32;
258
259                // Activation rate (how often above threshold)
260                let activation_rate =
261                    values.iter().filter(|&&v| v > 0.1).count() as f32 / values.len() as f32;
262
263                // Infer category based on layer position and importance pattern
264                let category = Self::infer_head_category(layer, head, num_layers, num_heads, mean);
265
266                heads.push(HeadImportance {
267                    layer,
268                    head,
269                    importance: mean.clamp(0.0, 1.0),
270                    variance,
271                    category,
272                    activation_rate,
273                });
274            }
275        }
276
277        heads
278    }
279
280    /// Infer category for a single head
281    fn infer_head_category(
282        layer: usize,
283        head: usize,
284        num_layers: usize,
285        num_heads: usize,
286        _importance: f32,
287    ) -> HeadCategory {
288        let layer_fraction = layer as f32 / num_layers as f32;
289        let head_fraction = head as f32 / num_heads as f32;
290
291        // Early layers: composition
292        if layer_fraction < 0.2 {
293            return if head_fraction < 0.5 {
294                HeadCategory::Composition
295            } else {
296                HeadCategory::General
297            };
298        }
299
300        // Late layers: detail
301        if layer_fraction > 0.8 {
302            return if head_fraction < 0.3 {
303                HeadCategory::Detail
304            } else {
305                HeadCategory::Edge
306            };
307        }
308
309        // Middle layers: content-specific
310        if head_fraction < 0.25 {
311            HeadCategory::Face
312        } else if head_fraction < 0.5 {
313            HeadCategory::Body
314        } else if head_fraction < 0.75 {
315            HeadCategory::Background
316        } else {
317            HeadCategory::Style
318        }
319    }
320
321    /// Infer category mapping from importance data
322    fn infer_categories(
323        &self,
324        heads: &[HeadImportance],
325        num_heads: usize,
326        num_layers: usize,
327    ) -> CategoryMapping {
328        let head_categories: Vec<Vec<HeadCategory>> = (0..num_layers)
329            .map(|layer| {
330                (0..num_heads)
331                    .map(|head| {
332                        heads
333                            .iter()
334                            .find(|h| h.layer == layer && h.head == head)
335                            .map(|h| h.category)
336                            .unwrap_or(HeadCategory::General)
337                    })
338                    .collect()
339            })
340            .collect();
341
342        CategoryMapping {
343            num_heads,
344            num_layers,
345            head_categories,
346            model_id: String::new(),
347        }
348    }
349}
350
351/// Statistics about head importance distribution
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ImportanceStats {
354    /// Mean importance
355    pub mean: f32,
356    /// Standard deviation
357    pub std_dev: f32,
358    /// Minimum importance
359    pub min: f32,
360    /// Maximum importance
361    pub max: f32,
362    /// Median importance
363    pub median: f32,
364    /// Percentiles (25th, 50th, 75th, 90th)
365    pub percentiles: [f32; 4],
366}
367
368impl ImportanceStats {
369    /// Compute stats from importance values
370    pub fn from_importances(importances: &[f32]) -> Self {
371        if importances.is_empty() {
372            return Self {
373                mean: 0.0,
374                std_dev: 0.0,
375                min: 0.0,
376                max: 0.0,
377                median: 0.0,
378                percentiles: [0.0; 4],
379            };
380        }
381
382        let mut sorted = importances.to_vec();
383        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
384
385        let n = sorted.len();
386        let mean: f32 = sorted.iter().sum::<f32>() / n as f32;
387        let variance: f32 = sorted.iter().map(|v| (v - mean).powi(2)).sum::<f32>() / n as f32;
388
389        Self {
390            mean,
391            std_dev: variance.sqrt(),
392            min: sorted[0],
393            max: sorted[n - 1],
394            median: sorted[n / 2],
395            percentiles: [
396                sorted[n / 4],
397                sorted[n / 2],
398                sorted[3 * n / 4],
399                sorted[9 * n / 10],
400            ],
401        }
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn test_analyzer() {
411        let mut analyzer = ImportanceAnalyzer::new(AnalysisMethod::Activation);
412
413        // Add some fake samples
414        for _ in 0..10 {
415            let weights: Vec<Vec<f32>> = (0..10)
416                .map(|_| (0..8).map(|h| 0.5 + h as f32 * 0.05).collect())
417                .collect();
418            analyzer.record_sample(weights, "portrait".into(), 1);
419        }
420
421        let analysis = analyzer.analyze("test-model").unwrap();
422        assert_eq!(analysis.num_layers, 10);
423        assert_eq!(analysis.num_heads, 8);
424        assert_eq!(analysis.heads.len(), 80);
425    }
426
427    #[test]
428    fn test_layer_importance() {
429        let heads: Vec<HeadImportance> = (0..4)
430            .flat_map(|layer| {
431                (0..8).map(move |head| HeadImportance {
432                    layer,
433                    head,
434                    importance: 0.5 + layer as f32 * 0.1,
435                    variance: 0.01,
436                    category: HeadCategory::General,
437                    activation_rate: 0.8,
438                })
439            })
440            .collect();
441
442        let analysis = HeadAnalysis {
443            model_id: "test".into(),
444            num_heads: 8,
445            num_layers: 4,
446            heads,
447            category_mapping: CategoryMapping::sdxl_default(),
448            prune_threshold: 0.5,
449            metadata: AnalysisMetadata {
450                num_samples: 10,
451                categories_analyzed: vec!["test".into()],
452                timestamp: 0,
453                method: AnalysisMethod::Activation,
454            },
455        };
456
457        let layer_imp = analysis.layer_importance();
458        assert_eq!(layer_imp.len(), 4);
459        // Later layers should have higher importance
460        assert!(layer_imp[3] > layer_imp[0]);
461    }
462}