Skip to main content

haagenti_sparse/
predictor.rs

1//! Mask prediction from prompt embeddings
2
3use crate::{AttentionMask, CategoryMapping, HeadCategory, MaskBuilder, PromptCategory};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Configuration for mask prediction
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct PredictorConfig {
10    /// Number of attention heads
11    pub num_heads: usize,
12    /// Number of layers
13    pub num_layers: usize,
14    /// Target sparsity (fraction of heads to skip)
15    pub target_sparsity: f32,
16    /// Minimum heads to keep per layer
17    pub min_active_heads: usize,
18    /// Quality threshold for adaptive sparsity
19    pub quality_threshold: f32,
20    /// Step-dependent sparsity (more sparse early)
21    pub step_adaptive: bool,
22}
23
24impl Default for PredictorConfig {
25    fn default() -> Self {
26        Self {
27            num_heads: 32,
28            num_layers: 70,
29            target_sparsity: 0.5,
30            min_active_heads: 4,
31            quality_threshold: 0.98,
32            step_adaptive: true,
33        }
34    }
35}
36
37/// Prediction result with confidence
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Prediction {
40    /// Predicted mask
41    pub mask: AttentionMask,
42    /// Confidence in prediction (0.0 - 1.0)
43    pub confidence: f32,
44    /// Detected prompt categories
45    pub categories: Vec<PromptCategory>,
46    /// Estimated quality impact (0.0 - 1.0, higher is better)
47    pub estimated_quality: f32,
48    /// Estimated compute savings (0.0 - 1.0)
49    pub compute_savings: f32,
50}
51
52/// Predicts attention masks from prompt information
53#[derive(Debug, Clone)]
54pub struct MaskPredictor {
55    config: PredictorConfig,
56    category_mapping: CategoryMapping,
57    /// Learned category-to-weight mappings
58    category_profiles: HashMap<PromptCategory, HashMap<HeadCategory, f32>>,
59    /// Step-dependent sparsity multipliers
60    step_multipliers: Vec<f32>,
61}
62
63impl MaskPredictor {
64    /// Create a new predictor with default settings
65    pub fn new(config: PredictorConfig) -> Self {
66        let category_mapping = CategoryMapping::sdxl_default();
67
68        // Build category profiles from prompt category weights
69        let mut category_profiles = HashMap::new();
70        for category in &[
71            PromptCategory::Portrait,
72            PromptCategory::Landscape,
73            PromptCategory::Abstract,
74            PromptCategory::Photorealistic,
75            PromptCategory::Anime,
76            PromptCategory::Architecture,
77            PromptCategory::Object,
78            PromptCategory::Fantasy,
79            PromptCategory::Mixed,
80        ] {
81            category_profiles.insert(*category, category.category_weights());
82        }
83
84        // Step multipliers: more sparsity early (high noise), less late (details)
85        let step_multipliers = Self::compute_step_multipliers(50);
86
87        Self {
88            config,
89            category_mapping,
90            category_profiles,
91            step_multipliers,
92        }
93    }
94
95    /// Compute step-dependent sparsity multipliers
96    fn compute_step_multipliers(total_steps: usize) -> Vec<f32> {
97        (0..total_steps)
98            .map(|step| {
99                let t = step as f32 / total_steps as f32;
100                // Early steps: higher sparsity (1.5x), late steps: lower (0.5x)
101                1.5 - t
102            })
103            .collect()
104    }
105
106    /// Predict mask from prompt text
107    pub fn predict(&self, prompt: &str, step: Option<u32>, total_steps: Option<u32>) -> Prediction {
108        // Detect prompt categories
109        let categories = PromptCategory::detect(prompt);
110
111        // Compute head importance weights
112        let weights = self.compute_weights(&categories);
113
114        // Apply step-dependent adjustment
115        let effective_sparsity = if self.config.step_adaptive {
116            let step_idx = step.unwrap_or(0) as usize;
117            let total = total_steps.unwrap_or(50) as usize;
118            let multiplier = if step_idx < self.step_multipliers.len() {
119                self.step_multipliers[step_idx]
120            } else if total > 0 {
121                let t = step_idx as f32 / total as f32;
122                1.5 - t
123            } else {
124                1.0
125            };
126            (self.config.target_sparsity * multiplier).clamp(0.2, 0.8)
127        } else {
128            self.config.target_sparsity
129        };
130
131        // Build mask using category-based pruning
132        let mask = MaskBuilder::new(self.config.num_heads, self.config.num_layers)
133            .sparsity(effective_sparsity)
134            .category_weights(weights.clone())
135            .category_mapping(self.category_mapping.clone())
136            .min_active(self.config.min_active_heads)
137            .build();
138
139        // Estimate quality impact
140        let estimated_quality = self.estimate_quality(&mask, &weights);
141
142        // Compute confidence based on category detection strength
143        let confidence = self.compute_confidence(&categories, prompt);
144
145        Prediction {
146            mask: mask.clone(),
147            confidence,
148            categories: categories.to_vec(),
149            estimated_quality,
150            compute_savings: mask.overall_sparsity,
151        }
152    }
153
154    /// Predict mask from embedding vector (for faster prediction)
155    pub fn predict_from_embedding(
156        &self,
157        embedding: &[f32],
158        step: Option<u32>,
159        total_steps: Option<u32>,
160    ) -> Prediction {
161        // Use embedding to infer category weights directly
162        // This is a simplified version - a real implementation would use a learned model
163        let weights = self.embedding_to_weights(embedding);
164
165        let effective_sparsity = if self.config.step_adaptive {
166            let step_idx = step.unwrap_or(0) as usize;
167            let total = total_steps.unwrap_or(50) as usize;
168            let t = step_idx as f32 / total as f32;
169            (self.config.target_sparsity * (1.5 - t)).clamp(0.2, 0.8)
170        } else {
171            self.config.target_sparsity
172        };
173
174        let mask = MaskBuilder::new(self.config.num_heads, self.config.num_layers)
175            .sparsity(effective_sparsity)
176            .category_weights(weights.clone())
177            .category_mapping(self.category_mapping.clone())
178            .min_active(self.config.min_active_heads)
179            .build();
180
181        let estimated_quality = self.estimate_quality(&mask, &weights);
182
183        Prediction {
184            mask: mask.clone(),
185            confidence: 0.7, // Lower confidence for embedding-based
186            categories: vec![PromptCategory::Mixed],
187            estimated_quality,
188            compute_savings: mask.overall_sparsity,
189        }
190    }
191
192    /// Compute head category weights from detected prompt categories
193    fn compute_weights(&self, categories: &[PromptCategory]) -> HashMap<HeadCategory, f32> {
194        let mut combined = HashMap::new();
195
196        for (i, category) in categories.iter().enumerate() {
197            let weight = 1.0 / (i + 1) as f32; // Decrease weight for less relevant categories
198
199            if let Some(profile) = self.category_profiles.get(category) {
200                for (&head_cat, &value) in profile {
201                    *combined.entry(head_cat).or_insert(0.0) += value * weight;
202                }
203            }
204        }
205
206        // Normalize
207        let max = combined.values().cloned().fold(0.0f32, f32::max);
208        if max > 0.0 {
209            for value in combined.values_mut() {
210                *value /= max;
211            }
212        }
213
214        // Ensure mandatory categories have high weight
215        for category in HeadCategory::all() {
216            if category.is_mandatory() {
217                combined.insert(*category, 1.0);
218            }
219        }
220
221        combined
222    }
223
224    /// Convert embedding to category weights
225    fn embedding_to_weights(&self, embedding: &[f32]) -> HashMap<HeadCategory, f32> {
226        // Simplified: use embedding dimensions to weight categories
227        // Real implementation would use a learned projection
228        let mut weights = HashMap::new();
229
230        let dim = embedding.len();
231        if dim > 0 {
232            // Use different embedding regions for different categories
233            let face_signal: f32 = embedding.iter().take(dim / 8).sum::<f32>().abs();
234            let body_signal: f32 = embedding
235                .iter()
236                .skip(dim / 8)
237                .take(dim / 8)
238                .sum::<f32>()
239                .abs();
240            let bg_signal: f32 = embedding
241                .iter()
242                .skip(dim / 4)
243                .take(dim / 4)
244                .sum::<f32>()
245                .abs();
246            let style_signal: f32 = embedding.iter().skip(dim / 2).sum::<f32>().abs();
247
248            let max = face_signal
249                .max(body_signal)
250                .max(bg_signal)
251                .max(style_signal);
252            if max > 0.0 {
253                weights.insert(HeadCategory::Face, face_signal / max);
254                weights.insert(HeadCategory::Body, body_signal / max);
255                weights.insert(HeadCategory::Background, bg_signal / max);
256                weights.insert(HeadCategory::Style, style_signal / max);
257            }
258        }
259
260        // Add mandatory categories
261        weights.insert(HeadCategory::General, 1.0);
262        weights.insert(HeadCategory::Composition, 0.9);
263
264        weights
265    }
266
267    /// Estimate quality impact of mask
268    fn estimate_quality(&self, mask: &AttentionMask, weights: &HashMap<HeadCategory, f32>) -> f32 {
269        // Quality is inversely related to how many important heads are masked
270        let mut quality = 1.0f32;
271
272        for layer in 0..mask.num_layers {
273            let layer_importance: f32 = (0..mask.num_heads)
274                .filter(|&head| !mask.is_active(layer, head))
275                .map(|head| {
276                    let category = self
277                        .category_mapping
278                        .get_category(layer, head)
279                        .unwrap_or(HeadCategory::General);
280                    weights.get(&category).copied().unwrap_or(0.5)
281                })
282                .sum();
283
284            // Each layer contributes to quality loss
285            quality *= 1.0 - (layer_importance * 0.001);
286        }
287
288        quality.clamp(0.9, 1.0)
289    }
290
291    /// Compute confidence based on category detection
292    fn compute_confidence(&self, categories: &[PromptCategory], prompt: &str) -> f32 {
293        if categories.is_empty() || categories[0] == PromptCategory::Mixed {
294            return 0.5;
295        }
296
297        // Count keyword matches
298        let primary = &categories[0];
299        let matches = primary
300            .keywords()
301            .iter()
302            .filter(|kw| prompt.to_lowercase().contains(*kw))
303            .count();
304
305        // More matches = higher confidence
306        (0.6 + matches as f32 * 0.1).clamp(0.5, 0.95)
307    }
308
309    /// Update step multipliers for different total step counts
310    pub fn set_total_steps(&mut self, total_steps: usize) {
311        self.step_multipliers = Self::compute_step_multipliers(total_steps);
312    }
313
314    /// Get current configuration
315    pub fn config(&self) -> &PredictorConfig {
316        &self.config
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_predict_portrait() {
326        let predictor = MaskPredictor::new(PredictorConfig::default());
327        let prediction = predictor.predict("A portrait of a beautiful woman", None, None);
328
329        assert!(prediction.categories.contains(&PromptCategory::Portrait));
330        assert!(prediction.confidence > 0.6);
331        assert!(prediction.compute_savings > 0.3);
332    }
333
334    #[test]
335    fn test_predict_landscape() {
336        let predictor = MaskPredictor::new(PredictorConfig::default());
337        let prediction = predictor.predict("Mountain landscape at sunset", None, None);
338
339        assert!(prediction.categories.contains(&PromptCategory::Landscape));
340    }
341
342    #[test]
343    fn test_step_adaptive() {
344        let config = PredictorConfig {
345            step_adaptive: true,
346            target_sparsity: 0.5,
347            ..Default::default()
348        };
349        let predictor = MaskPredictor::new(config);
350
351        // Early step should be more sparse
352        let early = predictor.predict("test prompt", Some(0), Some(20));
353        // Late step should be less sparse
354        let late = predictor.predict("test prompt", Some(19), Some(20));
355
356        assert!(early.mask.overall_sparsity > late.mask.overall_sparsity);
357    }
358
359    #[test]
360    fn test_predict_from_embedding() {
361        let predictor = MaskPredictor::new(PredictorConfig::default());
362        let embedding: Vec<f32> = (0..768).map(|i| (i as f32 / 768.0).sin()).collect();
363
364        let prediction = predictor.predict_from_embedding(&embedding, Some(5), Some(20));
365        assert!(prediction.compute_savings > 0.0);
366    }
367}