1#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum BitWidth {
11 Int2 = 2,
12 Int4 = 4,
13 Int8 = 8,
14 Fp16 = 16,
15 Fp32 = 32,
16}
17
18impl BitWidth {
19 pub fn bytes_per_weight(&self) -> f64 {
21 match self {
22 Self::Int2 => 0.25, Self::Int4 => 0.5, Self::Int8 => 1.0,
25 Self::Fp16 => 2.0,
26 Self::Fp32 => 4.0,
27 }
28 }
29
30 pub fn compression_ratio_vs_fp32(&self) -> f64 {
32 4.0 / self.bytes_per_weight()
33 }
34}
35
36impl std::fmt::Display for BitWidth {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 Self::Int2 => write!(f, "INT2"),
40 Self::Int4 => write!(f, "INT4"),
41 Self::Int8 => write!(f, "INT8"),
42 Self::Fp16 => write!(f, "FP16"),
43 Self::Fp32 => write!(f, "FP32"),
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
52pub struct LayerSensitivity {
53 pub layer_name: String,
54 pub gradient_norm: f64,
56 pub weight_variance: f64,
58 pub activation_range: f64,
60 pub output_sensitivity: f64,
62 pub is_embedding: bool,
64 pub is_final_layer: bool,
66}
67
68impl LayerSensitivity {
69 pub fn sensitivity_score(&self) -> f64 {
75 let base = self.gradient_norm * 0.4
76 + self.weight_variance * 0.3
77 + self.activation_range * 0.2
78 + self.output_sensitivity * 0.1;
79
80 let embed_bonus = if self.is_embedding { 1.0 } else { 0.0 };
81 let final_bonus = if self.is_final_layer { 0.5 } else { 0.0 };
82
83 base + embed_bonus + final_bonus
84 }
85}
86
87#[derive(Debug, Clone)]
91pub enum BitWidthStrategy {
92 Uniform(BitWidth),
94 SensitivityBased {
96 high_threshold: f64,
98 medium_threshold: f64,
100 low_threshold: f64,
102 },
103 BudgetOptimal,
105}
106
107#[derive(Debug, Clone)]
111pub struct QuantizationPolicy {
112 pub strategy: BitWidthStrategy,
113 pub budget_bytes: Option<usize>,
115 pub min_bit_width: BitWidth,
117 pub max_bit_width: BitWidth,
119}
120
121impl Default for QuantizationPolicy {
122 fn default() -> Self {
123 Self {
124 strategy: BitWidthStrategy::Uniform(BitWidth::Int8),
125 budget_bytes: None,
126 min_bit_width: BitWidth::Int4,
127 max_bit_width: BitWidth::Fp32,
128 }
129 }
130}
131
132#[derive(Debug, Clone)]
136pub struct LayerBitWidthAssignment {
137 pub layer_name: String,
138 pub bit_width: BitWidth,
139 pub param_count: usize,
140 pub memory_bytes: usize,
142 pub sensitivity_score: f64,
143}
144
145pub struct QuantizationSummary {
149 pub total_params: usize,
150 pub total_memory_bytes: usize,
151 pub compression_ratio: f64,
152 pub bit_width_distribution: Vec<(BitWidth, usize)>,
154 pub avg_sensitivity_score: f64,
155}
156
157impl std::fmt::Display for QuantizationSummary {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 writeln!(f, "QuantizationSummary {{")?;
160 writeln!(f, " total_params: {}", self.total_params)?;
161 writeln!(f, " total_memory_bytes: {}", self.total_memory_bytes)?;
162 writeln!(f, " compression_ratio: {:.2}x", self.compression_ratio)?;
163 writeln!(
164 f,
165 " avg_sensitivity_score: {:.4}",
166 self.avg_sensitivity_score
167 )?;
168 write!(f, " bit_width_distribution: [")?;
169 for (i, (bw, count)) in self.bit_width_distribution.iter().enumerate() {
170 if i > 0 {
171 write!(f, ", ")?;
172 }
173 write!(f, "{}×{}", bw, count)?;
174 }
175 writeln!(f, "]")?;
176 write!(f, "}}")
177 }
178}
179
180#[derive(Debug, thiserror::Error)]
184pub enum QuantSelectionError {
185 #[error("Budget exceeded: required {required} bytes, budget {budget} bytes")]
186 BudgetExceeded { required: usize, budget: usize },
187 #[error("Empty layer list")]
188 EmptyLayers,
189 #[error("Layer count mismatch")]
190 LengthMismatch,
191}
192
193pub struct PerLayerQuantSelector {
197 policy: QuantizationPolicy,
198}
199
200impl PerLayerQuantSelector {
201 pub fn new(policy: QuantizationPolicy) -> Self {
203 Self { policy }
204 }
205
206 pub fn assign_bit_widths(
210 &self,
211 layers: &[LayerSensitivity],
212 layer_param_counts: &[usize],
213 ) -> Result<Vec<LayerBitWidthAssignment>, QuantSelectionError> {
214 if layers.is_empty() {
215 return Err(QuantSelectionError::EmptyLayers);
216 }
217 if layers.len() != layer_param_counts.len() {
218 return Err(QuantSelectionError::LengthMismatch);
219 }
220
221 let assignments = match &self.policy.strategy {
222 BitWidthStrategy::Uniform(bw) => self.assign_uniform(*bw, layers, layer_param_counts),
223 BitWidthStrategy::SensitivityBased {
224 high_threshold,
225 medium_threshold,
226 low_threshold,
227 } => self.assign_sensitivity_based(
228 *high_threshold,
229 *medium_threshold,
230 *low_threshold,
231 layers,
232 layer_param_counts,
233 ),
234 BitWidthStrategy::BudgetOptimal => {
235 self.assign_budget_optimal(layers, layer_param_counts)?
236 },
237 };
238
239 if let Some(budget) = self.policy.budget_bytes {
241 let required = Self::total_memory_bytes(&assignments);
242 if required > budget {
243 return Err(QuantSelectionError::BudgetExceeded { required, budget });
244 }
245 }
246
247 Ok(assignments)
248 }
249
250 fn clamp_bit_width(&self, bw: BitWidth) -> BitWidth {
252 if bw < self.policy.min_bit_width {
253 self.policy.min_bit_width
254 } else if bw > self.policy.max_bit_width {
255 self.policy.max_bit_width
256 } else {
257 bw
258 }
259 }
260
261 fn make_assignment(
262 &self,
263 layer: &LayerSensitivity,
264 param_count: usize,
265 bit_width: BitWidth,
266 ) -> LayerBitWidthAssignment {
267 let bw = self.clamp_bit_width(bit_width);
268 let memory_bytes = (param_count as f64 * bw.bytes_per_weight()).ceil() as usize;
269 LayerBitWidthAssignment {
270 layer_name: layer.layer_name.clone(),
271 bit_width: bw,
272 param_count,
273 memory_bytes,
274 sensitivity_score: layer.sensitivity_score(),
275 }
276 }
277
278 fn assign_uniform(
279 &self,
280 bw: BitWidth,
281 layers: &[LayerSensitivity],
282 counts: &[usize],
283 ) -> Vec<LayerBitWidthAssignment> {
284 layers
285 .iter()
286 .zip(counts.iter())
287 .map(|(l, &c)| self.make_assignment(l, c, bw))
288 .collect()
289 }
290
291 fn assign_sensitivity_based(
292 &self,
293 high_threshold: f64,
294 medium_threshold: f64,
295 low_threshold: f64,
296 layers: &[LayerSensitivity],
297 counts: &[usize],
298 ) -> Vec<LayerBitWidthAssignment> {
299 layers
300 .iter()
301 .zip(counts.iter())
302 .map(|(l, &c)| {
303 let score = l.sensitivity_score();
304 let bw = if score > high_threshold {
305 BitWidth::Fp16
306 } else if score > medium_threshold {
307 BitWidth::Int8
308 } else if score < low_threshold {
309 BitWidth::Int2
310 } else {
311 BitWidth::Int4
312 };
313 self.make_assignment(l, c, bw)
314 })
315 .collect()
316 }
317
318 fn assign_budget_optimal(
323 &self,
324 layers: &[LayerSensitivity],
325 counts: &[usize],
326 ) -> Result<Vec<LayerBitWidthAssignment>, QuantSelectionError> {
327 let mut bit_widths: Vec<BitWidth> = vec![self.policy.min_bit_width; layers.len()];
329
330 let budget = match self.policy.budget_bytes {
331 Some(b) => b,
332 None => usize::MAX, };
334
335 let mut sorted_indices: Vec<usize> = (0..layers.len()).collect();
337 sorted_indices.sort_by(|&a, &b| {
338 layers[b]
339 .sensitivity_score()
340 .partial_cmp(&layers[a].sensitivity_score())
341 .unwrap_or(std::cmp::Ordering::Equal)
342 });
343
344 let precision_ladder = [
345 BitWidth::Int2,
346 BitWidth::Int4,
347 BitWidth::Int8,
348 BitWidth::Fp16,
349 BitWidth::Fp32,
350 ];
351
352 loop {
355 let maybe_upgrade = sorted_indices.iter().find_map(|&idx| {
356 let current = bit_widths[idx];
357 let next_bw = precision_ladder
358 .iter()
359 .find(|&&bw| bw > current && bw <= self.policy.max_bit_width)
360 .copied()?;
361
362 let old_mem = (counts[idx] as f64 * current.bytes_per_weight()).ceil() as usize;
363 let new_mem = (counts[idx] as f64 * next_bw.bytes_per_weight()).ceil() as usize;
364 let current_total: usize = bit_widths
365 .iter()
366 .zip(counts.iter())
367 .map(|(&bw, &c)| (c as f64 * bw.bytes_per_weight()).ceil() as usize)
368 .sum();
369 let proposed_total = current_total - old_mem + new_mem;
370 if proposed_total <= budget {
371 Some((idx, next_bw))
372 } else {
373 None
374 }
375 });
376
377 match maybe_upgrade {
378 Some((idx, next_bw)) => bit_widths[idx] = next_bw,
379 None => break,
380 }
381 }
382
383 Ok(layers
384 .iter()
385 .zip(counts.iter())
386 .enumerate()
387 .map(|(i, (l, &c))| self.make_assignment(l, c, bit_widths[i]))
388 .collect())
389 }
390
391 pub fn total_memory_bytes(assignments: &[LayerBitWidthAssignment]) -> usize {
393 assignments.iter().map(|a| a.memory_bytes).sum()
394 }
395
396 pub fn summary_report(assignments: &[LayerBitWidthAssignment]) -> QuantizationSummary {
398 let total_params: usize = assignments.iter().map(|a| a.param_count).sum();
399 let total_memory_bytes: usize = assignments.iter().map(|a| a.memory_bytes).sum();
400
401 let fp32_bytes = total_params * 4; let compression_ratio = if total_memory_bytes == 0 {
403 1.0
404 } else {
405 fp32_bytes as f64 / total_memory_bytes as f64
406 };
407
408 let avg_sensitivity_score = if assignments.is_empty() {
409 0.0
410 } else {
411 assignments.iter().map(|a| a.sensitivity_score).sum::<f64>() / assignments.len() as f64
412 };
413
414 let mut dist_map: std::collections::HashMap<u8, usize> = std::collections::HashMap::new();
416 for a in assignments.iter() {
417 *dist_map.entry(a.bit_width as u8).or_insert(0) += 1;
418 }
419 let mut bit_width_distribution: Vec<(BitWidth, usize)> = dist_map
420 .into_iter()
421 .filter_map(|(bits, count)| {
422 let bw = match bits {
423 2 => Some(BitWidth::Int2),
424 4 => Some(BitWidth::Int4),
425 8 => Some(BitWidth::Int8),
426 16 => Some(BitWidth::Fp16),
427 32 => Some(BitWidth::Fp32),
428 _ => None,
429 };
430 bw.map(|b| (b, count))
431 })
432 .collect();
433 bit_width_distribution.sort_by_key(|(bw, _)| *bw as u8);
434
435 QuantizationSummary {
436 total_params,
437 total_memory_bytes,
438 compression_ratio,
439 bit_width_distribution,
440 avg_sensitivity_score,
441 }
442 }
443}
444
445#[cfg(test)]
448mod tests {
449 use super::*;
450
451 fn make_layer(
452 name: &str,
453 gradient_norm: f64,
454 weight_variance: f64,
455 activation_range: f64,
456 output_sensitivity: f64,
457 is_embedding: bool,
458 is_final_layer: bool,
459 ) -> LayerSensitivity {
460 LayerSensitivity {
461 layer_name: name.to_string(),
462 gradient_norm,
463 weight_variance,
464 activation_range,
465 output_sensitivity,
466 is_embedding,
467 is_final_layer,
468 }
469 }
470
471 fn simple_layers() -> (Vec<LayerSensitivity>, Vec<usize>) {
472 let layers = vec![
473 make_layer("embed", 2.0, 1.0, 1.0, 1.0, true, false),
474 make_layer("attn", 1.5, 0.8, 0.5, 0.5, false, false),
475 make_layer("ffn", 0.5, 0.3, 0.2, 0.1, false, false),
476 make_layer("head", 1.0, 0.6, 0.4, 0.4, false, true),
477 ];
478 let counts = vec![10_000, 8_000, 16_000, 4_000];
479 (layers, counts)
480 }
481
482 #[test]
485 fn test_bytes_per_weight() {
486 assert!((BitWidth::Int2.bytes_per_weight() - 0.25).abs() < 1e-10);
487 assert!((BitWidth::Int4.bytes_per_weight() - 0.5).abs() < 1e-10);
488 assert!((BitWidth::Int8.bytes_per_weight() - 1.0).abs() < 1e-10);
489 assert!((BitWidth::Fp16.bytes_per_weight() - 2.0).abs() < 1e-10);
490 assert!((BitWidth::Fp32.bytes_per_weight() - 4.0).abs() < 1e-10);
491 }
492
493 #[test]
496 fn test_compression_ratio() {
497 assert!((BitWidth::Fp32.compression_ratio_vs_fp32() - 1.0).abs() < 1e-10);
498 assert!((BitWidth::Fp16.compression_ratio_vs_fp32() - 2.0).abs() < 1e-10);
499 assert!((BitWidth::Int8.compression_ratio_vs_fp32() - 4.0).abs() < 1e-10);
500 assert!((BitWidth::Int4.compression_ratio_vs_fp32() - 8.0).abs() < 1e-10);
501 assert!((BitWidth::Int2.compression_ratio_vs_fp32() - 16.0).abs() < 1e-10);
502 }
503
504 #[test]
507 fn test_sensitivity_score_basic() {
508 let layer = make_layer("l", 1.0, 1.0, 1.0, 1.0, false, false);
509 let score = layer.sensitivity_score();
511 assert!((score - 1.0).abs() < 1e-10, "expected 1.0 got {}", score);
512 }
513
514 #[test]
517 fn test_sensitivity_score_embedding_bonus() {
518 let no_embed = make_layer("l", 1.0, 1.0, 1.0, 1.0, false, false);
519 let embed = make_layer("e", 1.0, 1.0, 1.0, 1.0, true, false);
520 let diff = embed.sensitivity_score() - no_embed.sensitivity_score();
521 assert!((diff - 1.0).abs() < 1e-10, "embedding bonus should be +1.0");
522 }
523
524 #[test]
527 fn test_sensitivity_score_final_layer_bonus() {
528 let normal = make_layer("l", 1.0, 1.0, 1.0, 1.0, false, false);
529 let final_l = make_layer("f", 1.0, 1.0, 1.0, 1.0, false, true);
530 let diff = final_l.sensitivity_score() - normal.sensitivity_score();
531 assert!(
532 (diff - 0.5).abs() < 1e-10,
533 "final layer bonus should be +0.5"
534 );
535 }
536
537 #[test]
540 fn test_sensitivity_score_both_bonuses() {
541 let layer = make_layer("l", 1.0, 1.0, 1.0, 1.0, true, true);
542 assert!((layer.sensitivity_score() - 2.5).abs() < 1e-10);
544 }
545
546 #[test]
549 fn test_uniform_strategy() {
550 let policy = QuantizationPolicy {
551 strategy: BitWidthStrategy::Uniform(BitWidth::Int8),
552 budget_bytes: None,
553 min_bit_width: BitWidth::Int2,
554 max_bit_width: BitWidth::Fp32,
555 };
556 let selector = PerLayerQuantSelector::new(policy);
557 let (layers, counts) = simple_layers();
558 let assignments = selector.assign_bit_widths(&layers, &counts).expect("assign");
559 assert!(assignments.iter().all(|a| a.bit_width == BitWidth::Int8));
560 assert_eq!(assignments.len(), 4);
561 }
562
563 #[test]
566 fn test_uniform_strategy_clamped_by_min() {
567 let policy = QuantizationPolicy {
568 strategy: BitWidthStrategy::Uniform(BitWidth::Int2),
569 budget_bytes: None,
570 min_bit_width: BitWidth::Int4, max_bit_width: BitWidth::Fp32,
572 };
573 let selector = PerLayerQuantSelector::new(policy);
574 let layers = vec![make_layer("l", 0.1, 0.1, 0.1, 0.1, false, false)];
575 let counts = vec![100];
576 let assignments = selector.assign_bit_widths(&layers, &counts).expect("assign");
577 assert_eq!(assignments[0].bit_width, BitWidth::Int4);
578 }
579
580 #[test]
583 fn test_sensitivity_based_high_score() {
584 let policy = QuantizationPolicy {
585 strategy: BitWidthStrategy::SensitivityBased {
586 high_threshold: 2.0,
587 medium_threshold: 1.0,
588 low_threshold: 0.3,
589 },
590 budget_bytes: None,
591 min_bit_width: BitWidth::Int2,
592 max_bit_width: BitWidth::Fp32,
593 };
594 let selector = PerLayerQuantSelector::new(policy);
595 let layer = make_layer("l", 3.0, 2.0, 2.0, 2.0, false, false);
597 let assignments = selector.assign_bit_widths(&[layer], &[1000]).expect("assign");
599 assert_eq!(assignments[0].bit_width, BitWidth::Fp16);
600 }
601
602 #[test]
605 fn test_sensitivity_based_medium_score() {
606 let policy = QuantizationPolicy {
607 strategy: BitWidthStrategy::SensitivityBased {
608 high_threshold: 2.0,
609 medium_threshold: 1.0,
610 low_threshold: 0.3,
611 },
612 budget_bytes: None,
613 min_bit_width: BitWidth::Int2,
614 max_bit_width: BitWidth::Fp32,
615 };
616 let selector = PerLayerQuantSelector::new(policy);
617 let layer = make_layer("l", 1.5, 1.5, 1.0, 1.0, false, false);
619 let assignments = selector.assign_bit_widths(&[layer], &[500]).expect("assign");
621 assert_eq!(assignments[0].bit_width, BitWidth::Int8);
622 }
623
624 #[test]
627 fn test_sensitivity_based_low_score() {
628 let policy = QuantizationPolicy {
629 strategy: BitWidthStrategy::SensitivityBased {
630 high_threshold: 2.0,
631 medium_threshold: 1.0,
632 low_threshold: 0.3,
633 },
634 budget_bytes: None,
635 min_bit_width: BitWidth::Int2,
636 max_bit_width: BitWidth::Fp32,
637 };
638 let selector = PerLayerQuantSelector::new(policy);
639 let layer = make_layer("l", 0.1, 0.1, 0.1, 0.1, false, false);
641 let assignments = selector.assign_bit_widths(&[layer], &[200]).expect("assign");
643 assert_eq!(assignments[0].bit_width, BitWidth::Int2);
644 }
645
646 #[test]
649 fn test_sensitivity_based_between_medium_and_low() {
650 let policy = QuantizationPolicy {
651 strategy: BitWidthStrategy::SensitivityBased {
652 high_threshold: 2.0,
653 medium_threshold: 1.0,
654 low_threshold: 0.3,
655 },
656 budget_bytes: None,
657 min_bit_width: BitWidth::Int2,
658 max_bit_width: BitWidth::Fp32,
659 };
660 let selector = PerLayerQuantSelector::new(policy);
661 let layer = make_layer("l", 0.8, 0.8, 0.6, 0.6, false, false);
663 let assignments = selector.assign_bit_widths(&[layer], &[300]).expect("assign");
665 assert_eq!(assignments[0].bit_width, BitWidth::Int4);
666 }
667
668 #[test]
671 fn test_budget_optimal_fits() {
672 let large_budget = 1_000_000;
674 let policy = QuantizationPolicy {
675 strategy: BitWidthStrategy::BudgetOptimal,
676 budget_bytes: Some(large_budget),
677 min_bit_width: BitWidth::Int2,
678 max_bit_width: BitWidth::Fp32,
679 };
680 let selector = PerLayerQuantSelector::new(policy);
681 let (layers, counts) = simple_layers();
682 let assignments = selector.assign_bit_widths(&layers, &counts).expect("assign");
683 assert_eq!(assignments.len(), 4);
684 let total = PerLayerQuantSelector::total_memory_bytes(&assignments);
685 assert!(total <= large_budget);
686 }
687
688 #[test]
691 fn test_empty_layers_error() {
692 let policy = QuantizationPolicy::default();
693 let selector = PerLayerQuantSelector::new(policy);
694 let err = selector.assign_bit_widths(&[], &[]).expect_err("should error on empty");
695 assert!(matches!(err, QuantSelectionError::EmptyLayers));
696 }
697
698 #[test]
701 fn test_length_mismatch_error() {
702 let policy = QuantizationPolicy::default();
703 let selector = PerLayerQuantSelector::new(policy);
704 let layers = vec![make_layer("l", 1.0, 1.0, 1.0, 1.0, false, false)];
705 let counts = vec![100, 200]; let err = selector
707 .assign_bit_widths(&layers, &counts)
708 .expect_err("should error on mismatch");
709 assert!(matches!(err, QuantSelectionError::LengthMismatch));
710 }
711
712 #[test]
715 fn test_budget_exceeded_error() {
716 let tiny_budget = 1; let policy = QuantizationPolicy {
719 strategy: BitWidthStrategy::Uniform(BitWidth::Int8),
720 budget_bytes: Some(tiny_budget),
721 min_bit_width: BitWidth::Int2,
722 max_bit_width: BitWidth::Fp32,
723 };
724 let selector = PerLayerQuantSelector::new(policy);
725 let layers = vec![make_layer("l", 1.0, 1.0, 1.0, 1.0, false, false)];
726 let counts = vec![10_000];
727 let err = selector.assign_bit_widths(&layers, &counts).expect_err("should exceed budget");
728 assert!(matches!(err, QuantSelectionError::BudgetExceeded { .. }));
729 }
730
731 #[test]
734 fn test_total_memory_bytes() {
735 let assignments = vec![
736 LayerBitWidthAssignment {
737 layer_name: "a".to_string(),
738 bit_width: BitWidth::Int8,
739 param_count: 100,
740 memory_bytes: 100,
741 sensitivity_score: 1.0,
742 },
743 LayerBitWidthAssignment {
744 layer_name: "b".to_string(),
745 bit_width: BitWidth::Fp16,
746 param_count: 50,
747 memory_bytes: 100,
748 sensitivity_score: 2.0,
749 },
750 ];
751 assert_eq!(PerLayerQuantSelector::total_memory_bytes(&assignments), 200);
752 }
753
754 #[test]
757 fn test_summary_report_display() {
758 let policy = QuantizationPolicy {
759 strategy: BitWidthStrategy::Uniform(BitWidth::Int8),
760 budget_bytes: None,
761 min_bit_width: BitWidth::Int2,
762 max_bit_width: BitWidth::Fp32,
763 };
764 let selector = PerLayerQuantSelector::new(policy);
765 let (layers, counts) = simple_layers();
766 let assignments = selector.assign_bit_widths(&layers, &counts).expect("assign");
767 let summary = PerLayerQuantSelector::summary_report(&assignments);
768 let s = format!("{}", summary);
769 assert!(s.contains("total_params"));
770 assert!(s.contains("compression_ratio"));
771 assert!(s.contains("INT8"));
772 }
773
774 #[test]
777 fn test_summary_report_compression_ratio() {
778 let assignments = vec![LayerBitWidthAssignment {
780 layer_name: "l".to_string(),
781 bit_width: BitWidth::Int8,
782 param_count: 1000,
783 memory_bytes: 1000, sensitivity_score: 0.5,
785 }];
786 let summary = PerLayerQuantSelector::summary_report(&assignments);
787 assert!((summary.compression_ratio - 4.0).abs() < 1e-6);
789 }
790
791 #[test]
794 fn test_quant_selection_error_display() {
795 let e1 = QuantSelectionError::EmptyLayers;
796 assert!(format!("{}", e1).contains("Empty"));
797 let e2 = QuantSelectionError::LengthMismatch;
798 assert!(format!("{}", e2).contains("mismatch"));
799 let e3 = QuantSelectionError::BudgetExceeded {
800 required: 500,
801 budget: 100,
802 };
803 let s = format!("{}", e3);
804 assert!(s.contains("500"));
805 assert!(s.contains("100"));
806 }
807}