1use nxpu_ir::{Handle, Module, Scalar, Type, TypeInner};
10
11use crate::Pass;
12use crate::calibrate::CalibrationResult;
13
14#[derive(Clone, Debug)]
19pub struct QuantizationParams {
20 pub scale: f32,
21 pub zero_point: i32,
22}
23
24impl QuantizationParams {
25 pub fn from_range(min: f32, max: f32) -> Self {
31 let range = max - min;
32 if range < f32::EPSILON {
33 return Self {
34 scale: 1.0,
35 zero_point: 0,
36 };
37 }
38 let scale = range / 255.0;
39 let zero_point = (-min / scale).round() as i32;
40 let zero_point = zero_point.clamp(-128, 127);
42 Self { scale, zero_point }
43 }
44}
45
46#[derive(Clone, Debug)]
51pub struct PerChannelQuantParams {
52 pub scales: Vec<f32>,
54 pub zero_points: Vec<i32>,
56 pub channel_axis: u32,
58}
59
60impl PerChannelQuantParams {
61 pub fn num_channels(&self) -> usize {
63 self.scales.len()
64 }
65}
66
67#[derive(Clone, Debug)]
69pub struct TensorCalibration {
70 pub group: u32,
72 pub binding: u32,
74 pub min: f32,
76 pub max: f32,
78}
79
80#[derive(Clone, Debug, Default)]
82pub struct CalibrationData {
83 pub tensors: Vec<TensorCalibration>,
85}
86
87impl CalibrationData {
88 pub fn from_entries(entries: &[(u32, u32, f32, f32)]) -> Self {
90 Self {
91 tensors: entries
92 .iter()
93 .map(|&(group, binding, min, max)| TensorCalibration {
94 group,
95 binding,
96 min,
97 max,
98 })
99 .collect(),
100 }
101 }
102
103 pub fn find(&self, group: u32, binding: u32) -> Option<&TensorCalibration> {
105 self.tensors
106 .iter()
107 .find(|t| t.group == group && t.binding == binding)
108 }
109}
110
111pub type LayerPrecision = nxpu_backend_core::Precision;
116
117#[derive(Clone, Debug)]
122pub struct MixedPrecisionPolicy {
123 pub overrides: Vec<(String, LayerPrecision)>,
125 pub default: LayerPrecision,
127}
128
129impl MixedPrecisionPolicy {
130 pub fn precision_for(&self, name: &str) -> LayerPrecision {
132 self.overrides
133 .iter()
134 .find(|(n, _)| n == name)
135 .map(|(_, p)| *p)
136 .unwrap_or(self.default)
137 }
138}
139
140#[derive(Clone, Debug)]
145pub struct SensitivityScore {
146 pub name: String,
148 pub score: f32,
150}
151
152pub fn estimate_sensitivity(module: &Module) -> Vec<SensitivityScore> {
161 use nxpu_ir::{AddressSpace, StorageAccess};
162
163 let mut scores = Vec::new();
164
165 for (_handle, gv) in module.global_variables.iter() {
166 let is_storage = matches!(gv.space, AddressSpace::Storage { .. });
167 if !is_storage {
168 continue;
169 }
170
171 let name = gv
172 .name
173 .clone()
174 .unwrap_or_else(|| format!("var_{}", _handle.index()));
175
176 let is_output = matches!(
177 gv.space,
178 AddressSpace::Storage { access } if access.contains(StorageAccess::STORE)
179 );
180
181 let score = if is_output { 0.8 } else { 0.2 };
183
184 scores.push(SensitivityScore { name, score });
185 }
186
187 scores.sort_by(|a, b| {
189 b.score
190 .partial_cmp(&a.score)
191 .unwrap_or(std::cmp::Ordering::Equal)
192 });
193 scores
194}
195
196pub fn policy_from_sensitivity(
201 scores: &[SensitivityScore],
202 threshold: f32,
203 default_precision: LayerPrecision,
204 sensitive_precision: LayerPrecision,
205) -> MixedPrecisionPolicy {
206 let overrides: Vec<_> = scores
207 .iter()
208 .filter(|s| s.score > threshold)
209 .map(|s| (s.name.clone(), sensitive_precision))
210 .collect();
211
212 MixedPrecisionPolicy {
213 overrides,
214 default: default_precision,
215 }
216}
217
218#[allow(clippy::collapsible_if)] fn rewrite_elem_precision(module: &mut Module, target_scalar: Scalar) -> bool {
230 let target_scalar_handle = module.types.insert(Type {
232 name: None,
233 inner: TypeInner::Scalar(target_scalar),
234 });
235
236 let f32_handle = module.types.insert(Type {
238 name: None,
239 inner: TypeInner::Scalar(Scalar::F32),
240 });
241
242 let mut remap: Vec<(Handle<Type>, Handle<Type>)> = Vec::new();
244
245 let array_types: Vec<_> = module
247 .types
248 .iter()
249 .filter_map(|(handle, ty)| {
250 if let TypeInner::Array { base, size, stride } = &ty.inner {
251 if *base == f32_handle {
252 return Some((handle, *size, *stride));
253 }
254 }
255 None
256 })
257 .collect();
258
259 for (old_handle, size, old_stride) in array_types {
261 let numerator = old_stride as u64 * target_scalar.width as u64;
262 let f32_width = Scalar::F32.width as u64;
263 if !numerator.is_multiple_of(f32_width) {
264 log::warn!(
265 "stride {old_stride} not evenly divisible when converting to {:?}, skipping",
266 target_scalar,
267 );
268 continue;
269 }
270 let new_stride = (numerator / f32_width) as u32;
271 let new_handle = module.types.insert(Type {
272 name: None,
273 inner: TypeInner::Array {
274 base: target_scalar_handle,
275 size,
276 stride: new_stride,
277 },
278 });
279 if old_handle != new_handle {
280 remap.push((old_handle, new_handle));
281 }
282 }
283
284 let tensor_types: Vec<_> = module
286 .types
287 .iter()
288 .filter_map(|(handle, ty)| {
289 if let TypeInner::Tensor { scalar, shape } = &ty.inner {
290 if *scalar == Scalar::F32 {
291 return Some((handle, shape.clone()));
292 }
293 }
294 None
295 })
296 .collect();
297
298 for (old_handle, shape) in tensor_types {
299 let new_handle = module.types.insert(Type {
300 name: None,
301 inner: TypeInner::Tensor {
302 scalar: target_scalar,
303 shape,
304 },
305 });
306 if old_handle != new_handle {
307 remap.push((old_handle, new_handle));
308 }
309 }
310
311 if remap.is_empty() {
312 return false;
313 }
314
315 let mut changed = false;
317 for (_handle, gv) in module.global_variables.iter_mut() {
318 for (old, new) in &remap {
319 if gv.ty == *old {
320 gv.ty = *new;
321 changed = true;
322 }
323 }
324 }
325
326 changed
327}
328
329#[derive(Debug)]
336pub struct MixedPrecisionPass {
337 pub policy: MixedPrecisionPolicy,
339}
340
341impl Pass for MixedPrecisionPass {
342 fn name(&self) -> &str {
343 "MixedPrecision"
344 }
345
346 fn run(&self, module: &mut Module) -> bool {
347 let mut changed = false;
348
349 let conversions: Vec<(Handle<nxpu_ir::GlobalVariable>, Scalar)> = module
351 .global_variables
352 .iter()
353 .filter_map(|(handle, gv)| {
354 let name = gv.name.as_deref()?;
355 let precision = self.policy.precision_for(name);
356 let target = match precision {
357 LayerPrecision::F32 => return None,
358 LayerPrecision::F16 => Scalar::F16,
359 LayerPrecision::BF16 => Scalar::BF16,
360 LayerPrecision::Int8 => Scalar::I8,
361 };
362
363 let current_scalar = match &module.types[gv.ty].inner {
365 TypeInner::Array { base, .. } => {
366 if let TypeInner::Scalar(s) = &module.types[*base].inner {
367 Some(*s)
368 } else {
369 None
370 }
371 }
372 TypeInner::Tensor { scalar, .. } => Some(*scalar),
373 _ => None,
374 };
375
376 if current_scalar == Some(target) {
377 return None;
378 }
379
380 if current_scalar != Some(Scalar::F32) {
382 return None;
383 }
384
385 Some((handle, target))
386 })
387 .collect();
388
389 for (gv_handle, target_scalar) in conversions {
390 let old_ty = module.global_variables[gv_handle].ty;
391
392 let type_info = match &module.types[old_ty].inner {
394 TypeInner::Array {
395 base: _,
396 size,
397 stride,
398 } => Some((true, *size, *stride, None)),
399 TypeInner::Tensor { scalar: _, shape } => {
400 Some((false, nxpu_ir::ArraySize::Dynamic, 0, Some(shape.clone())))
401 }
402 _ => None,
403 };
404
405 let new_ty = match type_info {
406 Some((true, size, stride, _)) => {
407 let numerator = stride as u64 * target_scalar.width as u64;
408 let f32_width = Scalar::F32.width as u64;
409 if !numerator.is_multiple_of(f32_width) {
410 log::warn!(
411 "stride {stride} not evenly divisible when converting to {:?}, skipping",
412 target_scalar,
413 );
414 continue;
415 }
416 let target_handle = module.types.insert(Type {
417 name: None,
418 inner: TypeInner::Scalar(target_scalar),
419 });
420 let new_stride = (numerator / f32_width) as u32;
421 Some(module.types.insert(Type {
422 name: None,
423 inner: TypeInner::Array {
424 base: target_handle,
425 size,
426 stride: new_stride,
427 },
428 }))
429 }
430 Some((false, _, _, Some(shape))) => Some(module.types.insert(Type {
431 name: None,
432 inner: TypeInner::Tensor {
433 scalar: target_scalar,
434 shape,
435 },
436 })),
437 _ => None,
438 };
439
440 if let Some(new_ty_handle) = new_ty {
441 module.global_variables[gv_handle].ty = new_ty_handle;
442 changed = true;
443 }
444 }
445
446 changed
447 }
448}
449
450#[derive(Debug)]
452pub struct F32ToF16;
453
454impl Pass for F32ToF16 {
455 fn name(&self) -> &str {
456 "F32ToF16"
457 }
458
459 fn run(&self, module: &mut Module) -> bool {
460 rewrite_elem_precision(module, Scalar::F16)
461 }
462}
463
464#[derive(Debug)]
466pub struct F32ToBf16;
467
468impl Pass for F32ToBf16 {
469 fn name(&self) -> &str {
470 "F32ToBf16"
471 }
472
473 fn run(&self, module: &mut Module) -> bool {
474 rewrite_elem_precision(module, Scalar::BF16)
475 }
476}
477
478#[derive(Debug)]
485pub struct F32ToInt8 {
486 pub params: QuantizationParams,
488 pub calibration: Option<CalibrationData>,
490 pub tensor_params: Vec<(u32, u32, QuantizationParams)>,
492 pub calibration_result: Option<CalibrationResult>,
494}
495
496impl Default for F32ToInt8 {
497 fn default() -> Self {
498 Self {
499 params: QuantizationParams {
500 scale: 1.0,
501 zero_point: 0,
502 },
503 calibration: None,
504 tensor_params: Vec::new(),
505 calibration_result: None,
506 }
507 }
508}
509
510impl F32ToInt8 {
511 pub fn with_calibration(calibration: CalibrationData) -> Self {
513 Self {
514 params: QuantizationParams {
515 scale: 1.0,
516 zero_point: 0,
517 },
518 calibration: Some(calibration),
519 tensor_params: Vec::new(),
520 calibration_result: None,
521 }
522 }
523
524 pub fn with_calibration_result(result: CalibrationResult) -> Self {
526 Self {
527 params: QuantizationParams {
528 scale: 1.0,
529 zero_point: 0,
530 },
531 calibration: None,
532 tensor_params: Vec::new(),
533 calibration_result: Some(result),
534 }
535 }
536}
537
538impl Pass for F32ToInt8 {
539 fn name(&self) -> &str {
540 "F32ToInt8"
541 }
542
543 fn run(&self, module: &mut Module) -> bool {
544 if let Some(result) = &self.calibration_result {
546 result.log_summary();
547 }
548 rewrite_elem_precision(module, Scalar::I8)
551 }
552}
553
554#[allow(clippy::collapsible_if)] pub fn compute_calibrated_params(
557 module: &Module,
558 calibration: &CalibrationData,
559) -> Vec<(String, QuantizationParams)> {
560 let mut result = Vec::new();
561 for (_handle, gv) in module.global_variables.iter() {
562 if let Some(binding) = &gv.binding {
563 if let Some(cal) = calibration.find(binding.group, binding.binding) {
564 let params = QuantizationParams::from_range(cal.min, cal.max);
565 let name = gv
566 .name
567 .clone()
568 .unwrap_or_else(|| format!("tensor_{}_{}", binding.group, binding.binding));
569 result.push((name, params));
570 }
571 }
572 }
573 result
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579 use nxpu_ir::*;
580
581 fn make_f32_array_module() -> (Module, Handle<GlobalVariable>, Handle<GlobalVariable>) {
582 let mut module = Module::default();
583
584 let f32_ty = module.types.insert(Type {
585 name: None,
586 inner: TypeInner::Scalar(Scalar::F32),
587 });
588 let array_f32 = module.types.insert(Type {
589 name: None,
590 inner: TypeInner::Array {
591 base: f32_ty,
592 size: ArraySize::Dynamic,
593 stride: 4,
594 },
595 });
596
597 let h0 = module.global_variables.append(GlobalVariable {
598 name: Some("a".into()),
599 space: AddressSpace::Storage {
600 access: StorageAccess::LOAD,
601 },
602 binding: Some(ResourceBinding {
603 group: 0,
604 binding: 0,
605 }),
606 ty: array_f32,
607 init: None,
608 layout: None,
609 });
610 let h1 = module.global_variables.append(GlobalVariable {
611 name: Some("b".into()),
612 space: AddressSpace::Storage {
613 access: StorageAccess::LOAD,
614 },
615 binding: Some(ResourceBinding {
616 group: 0,
617 binding: 1,
618 }),
619 ty: array_f32,
620 init: None,
621 layout: None,
622 });
623
624 (module, h0, h1)
625 }
626
627 fn get_array_elem_scalar(module: &Module, gv_handle: Handle<GlobalVariable>) -> Scalar {
628 let ty = &module.types[module.global_variables[gv_handle].ty];
629 match &ty.inner {
630 TypeInner::Array { base, .. } => match &module.types[*base].inner {
631 TypeInner::Scalar(s) => *s,
632 other => panic!("expected Scalar, got {other:?}"),
633 },
634 other => panic!("expected Array, got {other:?}"),
635 }
636 }
637
638 fn get_array_stride(module: &Module, gv_handle: Handle<GlobalVariable>) -> u32 {
639 let ty = &module.types[module.global_variables[gv_handle].ty];
640 match &ty.inner {
641 TypeInner::Array { stride, .. } => *stride,
642 other => panic!("expected Array, got {other:?}"),
643 }
644 }
645
646 #[test]
647 fn f32_to_f16() {
648 let (mut module, h0, h1) = make_f32_array_module();
649 let changed = F32ToF16.run(&mut module);
650 assert!(changed);
651
652 assert_eq!(get_array_elem_scalar(&module, h0), Scalar::F16);
653 assert_eq!(get_array_elem_scalar(&module, h1), Scalar::F16);
654 assert_eq!(get_array_stride(&module, h0), 2);
655 }
656
657 #[test]
658 fn f32_to_bf16() {
659 let (mut module, h0, _h1) = make_f32_array_module();
660 let changed = F32ToBf16.run(&mut module);
661 assert!(changed);
662
663 assert_eq!(get_array_elem_scalar(&module, h0), Scalar::BF16);
664 assert_eq!(get_array_stride(&module, h0), 2);
665 }
666
667 #[test]
668 fn f32_to_int8() {
669 let (mut module, h0, _h1) = make_f32_array_module();
670 let changed = F32ToInt8::default().run(&mut module);
671 assert!(changed);
672
673 assert_eq!(get_array_elem_scalar(&module, h0), Scalar::I8);
674 assert_eq!(get_array_stride(&module, h0), 1);
675 }
676
677 #[test]
678 fn no_change_when_no_f32_arrays() {
679 let mut module = Module::default();
680 let changed = F32ToF16.run(&mut module);
681 assert!(!changed);
682 }
683
684 #[test]
685 fn idempotent() {
686 let (mut module, _h0, _h1) = make_f32_array_module();
687 F32ToF16.run(&mut module);
688 let changed = F32ToF16.run(&mut module);
689 assert!(!changed);
691 }
692
693 #[test]
694 fn per_channel_params() {
695 let params = PerChannelQuantParams {
696 scales: vec![0.1, 0.2, 0.3],
697 zero_points: vec![0, 1, -1],
698 channel_axis: 0,
699 };
700 assert_eq!(params.num_channels(), 3);
701 }
702
703 #[test]
704 fn mixed_precision_policy() {
705 let policy = MixedPrecisionPolicy {
706 overrides: vec![
707 ("softmax".into(), LayerPrecision::F16),
708 ("layernorm".into(), LayerPrecision::F32),
709 ],
710 default: LayerPrecision::Int8,
711 };
712
713 assert_eq!(policy.precision_for("softmax"), LayerPrecision::F16);
714 assert_eq!(policy.precision_for("layernorm"), LayerPrecision::F32);
715 assert_eq!(policy.precision_for("conv1"), LayerPrecision::Int8);
716 }
717
718 #[test]
719 fn mixed_precision_pass() {
720 let (mut module, h0, h1) = make_f32_array_module();
721
722 let pass = MixedPrecisionPass {
723 policy: MixedPrecisionPolicy {
724 overrides: vec![("a".into(), LayerPrecision::F16)],
725 default: LayerPrecision::Int8,
726 },
727 };
728 let changed = pass.run(&mut module);
729 assert!(changed);
730
731 assert_eq!(get_array_elem_scalar(&module, h0), Scalar::F16);
733 assert_eq!(get_array_elem_scalar(&module, h1), Scalar::I8);
735 }
736
737 #[test]
738 fn sensitivity_estimation() {
739 let mut module = Module::default();
740 let f32_ty = module.types.insert(Type {
741 name: None,
742 inner: TypeInner::Scalar(Scalar::F32),
743 });
744 let array_f32 = module.types.insert(Type {
745 name: None,
746 inner: TypeInner::Array {
747 base: f32_ty,
748 size: ArraySize::Dynamic,
749 stride: 4,
750 },
751 });
752
753 module.global_variables.append(GlobalVariable {
754 name: Some("weights".into()),
755 space: AddressSpace::Storage {
756 access: StorageAccess::LOAD,
757 },
758 binding: None,
759 ty: array_f32,
760 init: None,
761 layout: None,
762 });
763 module.global_variables.append(GlobalVariable {
764 name: Some("output".into()),
765 space: AddressSpace::Storage {
766 access: StorageAccess::LOAD | StorageAccess::STORE,
767 },
768 binding: None,
769 ty: array_f32,
770 init: None,
771 layout: None,
772 });
773
774 let scores = estimate_sensitivity(&module);
775 assert_eq!(scores.len(), 2);
776 assert_eq!(scores[0].name, "output");
778 assert!(scores[0].score > scores[1].score);
779 }
780
781 #[test]
782 fn policy_from_sensitivity_scores() {
783 let scores = vec![
784 SensitivityScore {
785 name: "output".into(),
786 score: 0.8,
787 },
788 SensitivityScore {
789 name: "weights".into(),
790 score: 0.2,
791 },
792 ];
793
794 let policy =
795 policy_from_sensitivity(&scores, 0.5, LayerPrecision::Int8, LayerPrecision::F16);
796
797 assert_eq!(policy.precision_for("output"), LayerPrecision::F16);
798 assert_eq!(policy.precision_for("weights"), LayerPrecision::Int8);
799 }
800
801 #[test]
802 fn quantization_params_from_range() {
803 let params = QuantizationParams::from_range(-1.0, 1.0);
804 assert!((params.scale - 2.0 / 255.0).abs() < 1e-6);
805 assert_eq!(params.zero_point, 127);
807 }
808
809 #[test]
810 fn quantization_params_positive_range() {
811 let params = QuantizationParams::from_range(0.0, 6.0);
812 assert!((params.scale - 6.0 / 255.0).abs() < 1e-6);
813 assert_eq!(params.zero_point, 0);
814 }
815
816 #[test]
817 fn quantization_params_zero_range() {
818 let params = QuantizationParams::from_range(5.0, 5.0);
819 assert_eq!(params.scale, 1.0);
820 assert_eq!(params.zero_point, 0);
821 }
822
823 #[test]
824 fn calibration_data_lookup() {
825 let cal = CalibrationData::from_entries(&[(0, 0, -1.0, 1.0), (0, 1, 0.0, 6.0)]);
826 assert!(cal.find(0, 0).is_some());
827 assert!(cal.find(0, 1).is_some());
828 assert!(cal.find(0, 2).is_none());
829 }
830
831 #[test]
832 fn compute_calibrated_params_test() {
833 let (module, _h0, _h1) = make_f32_array_module();
834 let cal = CalibrationData::from_entries(&[(0, 0, -1.0, 1.0), (0, 1, 0.0, 6.0)]);
835 let params = compute_calibrated_params(&module, &cal);
836 assert_eq!(params.len(), 2);
837 assert_eq!(params[0].0, "a");
838 assert_eq!(params[1].0, "b");
839 assert!((params[0].1.scale - 2.0 / 255.0).abs() < 1e-6);
840 assert!((params[1].1.scale - 6.0 / 255.0).abs() < 1e-6);
841 }
842
843 #[test]
844 fn int8_with_calibration() {
845 let (mut module, h0, _h1) = make_f32_array_module();
846 let cal = CalibrationData::from_entries(&[(0, 0, -1.0, 1.0), (0, 1, 0.0, 255.0)]);
847 let pass = F32ToInt8::with_calibration(cal);
848 let changed = pass.run(&mut module);
849 assert!(changed);
850 assert_eq!(get_array_elem_scalar(&module, h0), Scalar::I8);
851 }
852
853 fn make_f32_tensor_module() -> (Module, Handle<GlobalVariable>, Handle<GlobalVariable>) {
856 let mut module = Module::default();
857
858 let tensor_f32 = module.types.insert(Type {
859 name: None,
860 inner: TypeInner::Tensor {
861 scalar: Scalar::F32,
862 shape: TensorShape::fixed(&[1, 3, 224, 224]),
863 },
864 });
865
866 let h0 = module.global_variables.append(GlobalVariable {
867 name: Some("weights".into()),
868 space: AddressSpace::Storage {
869 access: StorageAccess::LOAD,
870 },
871 binding: Some(ResourceBinding {
872 group: 0,
873 binding: 0,
874 }),
875 ty: tensor_f32,
876 init: None,
877 layout: None,
878 });
879 let h1 = module.global_variables.append(GlobalVariable {
880 name: Some("bias".into()),
881 space: AddressSpace::Storage {
882 access: StorageAccess::LOAD,
883 },
884 binding: Some(ResourceBinding {
885 group: 0,
886 binding: 1,
887 }),
888 ty: tensor_f32,
889 init: None,
890 layout: None,
891 });
892
893 (module, h0, h1)
894 }
895
896 fn get_tensor_scalar(module: &Module, gv_handle: Handle<GlobalVariable>) -> Scalar {
897 let ty = &module.types[module.global_variables[gv_handle].ty];
898 match &ty.inner {
899 TypeInner::Tensor { scalar, .. } => *scalar,
900 other => panic!("expected Tensor, got {other:?}"),
901 }
902 }
903
904 #[test]
905 fn f32_to_f16_tensor_type() {
906 let (mut module, h0, h1) = make_f32_tensor_module();
907 let changed = F32ToF16.run(&mut module);
908 assert!(changed);
909 assert_eq!(get_tensor_scalar(&module, h0), Scalar::F16);
910 assert_eq!(get_tensor_scalar(&module, h1), Scalar::F16);
911 }
912
913 #[test]
914 fn f32_to_bf16_tensor_type() {
915 let (mut module, h0, _h1) = make_f32_tensor_module();
916 let changed = F32ToBf16.run(&mut module);
917 assert!(changed);
918 assert_eq!(get_tensor_scalar(&module, h0), Scalar::BF16);
919 }
920
921 #[test]
922 fn f32_to_int8_tensor_type() {
923 let (mut module, h0, _h1) = make_f32_tensor_module();
924 let changed = F32ToInt8::default().run(&mut module);
925 assert!(changed);
926 assert_eq!(get_tensor_scalar(&module, h0), Scalar::I8);
927 }
928
929 #[test]
930 fn tensor_type_idempotent() {
931 let (mut module, _h0, _h1) = make_f32_tensor_module();
932 F32ToF16.run(&mut module);
933 let changed = F32ToF16.run(&mut module);
934 assert!(!changed);
935 }
936
937 #[test]
940 fn int8_with_calibration_result() {
941 use crate::calibrate::CalibrationResult;
942
943 let result = CalibrationResult {
944 tensor_params: vec![
945 (
946 "weights".into(),
947 QuantizationParams {
948 scale: 0.05,
949 zero_point: 0,
950 },
951 ),
952 (
953 "bias".into(),
954 QuantizationParams {
955 scale: 0.01,
956 zero_point: 3,
957 },
958 ),
959 ],
960 weight_params: Vec::new(),
961 method: None,
962 };
963
964 let (mut module, h0, _h1) = make_f32_array_module();
965 let pass = F32ToInt8::with_calibration_result(result);
966
967 assert!(pass.calibration_result.is_some());
969
970 let changed = pass.run(&mut module);
971 assert!(changed);
972 assert_eq!(get_array_elem_scalar(&module, h0), Scalar::I8);
973 }
974
975 #[test]
976 fn int8_with_calibration_result_on_tensor_types() {
977 use crate::calibrate::CalibrationResult;
978
979 let result = CalibrationResult {
980 tensor_params: vec![(
981 "weights".into(),
982 QuantizationParams {
983 scale: 0.05,
984 zero_point: 0,
985 },
986 )],
987 weight_params: Vec::new(),
988 method: None,
989 };
990
991 let (mut module, h0, _h1) = make_f32_tensor_module();
992 let pass = F32ToInt8::with_calibration_result(result);
993 let changed = pass.run(&mut module);
994 assert!(changed);
995 assert_eq!(get_tensor_scalar(&module, h0), Scalar::I8);
996 }
997
998 #[test]
1001 fn mixed_precision_pass_with_tensor_types() {
1002 let (mut module, h0, h1) = make_f32_tensor_module();
1003
1004 let pass = MixedPrecisionPass {
1005 policy: MixedPrecisionPolicy {
1006 overrides: vec![("weights".into(), LayerPrecision::F16)],
1007 default: LayerPrecision::Int8,
1008 },
1009 };
1010 let changed = pass.run(&mut module);
1011 assert!(changed);
1012
1013 assert_eq!(get_tensor_scalar(&module, h0), Scalar::F16);
1015 assert_eq!(get_tensor_scalar(&module, h1), Scalar::I8);
1017 }
1018
1019 #[test]
1020 fn mixed_precision_pass_skips_already_at_target() {
1021 let mut module = Module::default();
1022
1023 let tensor_f16 = module.types.insert(Type {
1024 name: None,
1025 inner: TypeInner::Tensor {
1026 scalar: Scalar::F16,
1027 shape: TensorShape::fixed(&[1, 3, 224, 224]),
1028 },
1029 });
1030
1031 module.global_variables.append(GlobalVariable {
1032 name: Some("already_f16".into()),
1033 space: AddressSpace::Storage {
1034 access: StorageAccess::LOAD,
1035 },
1036 binding: None,
1037 ty: tensor_f16,
1038 init: None,
1039 layout: None,
1040 });
1041
1042 let pass = MixedPrecisionPass {
1043 policy: MixedPrecisionPolicy {
1044 overrides: vec![],
1045 default: LayerPrecision::F16,
1046 },
1047 };
1048 let changed = pass.run(&mut module);
1050 assert!(!changed);
1051 }
1052
1053 #[test]
1054 fn mixed_precision_pass_skips_non_f32() {
1055 let mut module = Module::default();
1056
1057 let tensor_i8 = module.types.insert(Type {
1058 name: None,
1059 inner: TypeInner::Tensor {
1060 scalar: Scalar::I8,
1061 shape: TensorShape::fixed(&[1, 256]),
1062 },
1063 });
1064
1065 module.global_variables.append(GlobalVariable {
1066 name: Some("quantized".into()),
1067 space: AddressSpace::Storage {
1068 access: StorageAccess::LOAD,
1069 },
1070 binding: None,
1071 ty: tensor_i8,
1072 init: None,
1073 layout: None,
1074 });
1075
1076 let pass = MixedPrecisionPass {
1077 policy: MixedPrecisionPolicy {
1078 overrides: vec![],
1079 default: LayerPrecision::F16,
1080 },
1081 };
1082 let changed = pass.run(&mut module);
1084 assert!(!changed);
1085 }
1086
1087 #[test]
1088 fn mixed_precision_bf16_on_tensor_type() {
1089 let (mut module, h0, _h1) = make_f32_tensor_module();
1090
1091 let pass = MixedPrecisionPass {
1092 policy: MixedPrecisionPolicy {
1093 overrides: vec![("weights".into(), LayerPrecision::BF16)],
1094 default: LayerPrecision::F32,
1095 },
1096 };
1097 let changed = pass.run(&mut module);
1098 assert!(changed);
1099 assert_eq!(get_tensor_scalar(&module, h0), Scalar::BF16);
1100 }
1101}