1use crate::{AttentionMask, CategoryMapping, HeadCategory, MaskBuilder, PromptCategory};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct PredictorConfig {
10 pub num_heads: usize,
12 pub num_layers: usize,
14 pub target_sparsity: f32,
16 pub min_active_heads: usize,
18 pub quality_threshold: f32,
20 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#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Prediction {
40 pub mask: AttentionMask,
42 pub confidence: f32,
44 pub categories: Vec<PromptCategory>,
46 pub estimated_quality: f32,
48 pub compute_savings: f32,
50}
51
52#[derive(Debug, Clone)]
54pub struct MaskPredictor {
55 config: PredictorConfig,
56 category_mapping: CategoryMapping,
57 category_profiles: HashMap<PromptCategory, HashMap<HeadCategory, f32>>,
59 step_multipliers: Vec<f32>,
61}
62
63impl MaskPredictor {
64 pub fn new(config: PredictorConfig) -> Self {
66 let category_mapping = CategoryMapping::sdxl_default();
67
68 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 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 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 1.5 - t
102 })
103 .collect()
104 }
105
106 pub fn predict(&self, prompt: &str, step: Option<u32>, total_steps: Option<u32>) -> Prediction {
108 let categories = PromptCategory::detect(prompt);
110
111 let weights = self.compute_weights(&categories);
113
114 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 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 let estimated_quality = self.estimate_quality(&mask, &weights);
141
142 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 pub fn predict_from_embedding(
156 &self,
157 embedding: &[f32],
158 step: Option<u32>,
159 total_steps: Option<u32>,
160 ) -> Prediction {
161 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, categories: vec![PromptCategory::Mixed],
187 estimated_quality,
188 compute_savings: mask.overall_sparsity,
189 }
190 }
191
192 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; 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 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 for category in HeadCategory::all() {
216 if category.is_mandatory() {
217 combined.insert(*category, 1.0);
218 }
219 }
220
221 combined
222 }
223
224 fn embedding_to_weights(&self, embedding: &[f32]) -> HashMap<HeadCategory, f32> {
226 let mut weights = HashMap::new();
229
230 let dim = embedding.len();
231 if dim > 0 {
232 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 weights.insert(HeadCategory::General, 1.0);
262 weights.insert(HeadCategory::Composition, 0.9);
263
264 weights
265 }
266
267 fn estimate_quality(&self, mask: &AttentionMask, weights: &HashMap<HeadCategory, f32>) -> f32 {
269 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 quality *= 1.0 - (layer_importance * 0.001);
286 }
287
288 quality.clamp(0.9, 1.0)
289 }
290
291 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 let primary = &categories[0];
299 let matches = primary
300 .keywords()
301 .iter()
302 .filter(|kw| prompt.to_lowercase().contains(*kw))
303 .count();
304
305 (0.6 + matches as f32 * 0.1).clamp(0.5, 0.95)
307 }
308
309 pub fn set_total_steps(&mut self, total_steps: usize) {
311 self.step_multipliers = Self::compute_step_multipliers(total_steps);
312 }
313
314 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 let early = predictor.predict("test prompt", Some(0), Some(20));
353 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}